v0.2.0
This commit is contained in:
		
							
								
								
									
										9
									
								
								.hmrc
									
									
									
									
									
								
							
							
						
						
									
										9
									
								
								.hmrc
									
									
									
									
									
								
							@@ -2,7 +2,7 @@
 | 
				
			|||||||
  "path": "G:\\Other\\Development\\Projects\\[ideas]\\domino-client",
 | 
					  "path": "G:\\Other\\Development\\Projects\\[ideas]\\domino-client",
 | 
				
			||||||
  "name": "domino-client",
 | 
					  "name": "domino-client",
 | 
				
			||||||
  "initialVersion": "0.1.4",
 | 
					  "initialVersion": "0.1.4",
 | 
				
			||||||
  "version": "0.1.12",
 | 
					  "version": "0.2.0",
 | 
				
			||||||
  "docker": {
 | 
					  "docker": {
 | 
				
			||||||
    "useRegistry": true,
 | 
					    "useRegistry": true,
 | 
				
			||||||
    "registry": "192.168.1.115:5000",
 | 
					    "registry": "192.168.1.115:5000",
 | 
				
			||||||
@@ -84,7 +84,7 @@
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  "_backup": {
 | 
					  "_backup": {
 | 
				
			||||||
    "name": "domino-client",
 | 
					    "name": "domino-client",
 | 
				
			||||||
    "version": "0.1.11",
 | 
					    "version": "0.1.12",
 | 
				
			||||||
    "private": true,
 | 
					    "private": true,
 | 
				
			||||||
    "type": "commonjs",
 | 
					    "type": "commonjs",
 | 
				
			||||||
    "scripts": {
 | 
					    "scripts": {
 | 
				
			||||||
@@ -97,8 +97,8 @@
 | 
				
			|||||||
      "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
 | 
					      "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
 | 
				
			||||||
      "format": "prettier --write src/",
 | 
					      "format": "prettier --write src/",
 | 
				
			||||||
      "docker-build": "docker build -t 192.168.1.115:5000/arhuako/domino-client:latest .",
 | 
					      "docker-build": "docker build -t 192.168.1.115:5000/arhuako/domino-client:latest .",
 | 
				
			||||||
      "docker-tag": "docker tag 192.168.1.115:5000/arhuako/domino-client:latest 192.168.1.115:5000/arhuako/domino-client:0.1.11",
 | 
					      "docker-tag": "docker tag 192.168.1.115:5000/arhuako/domino-client:latest 192.168.1.115:5000/arhuako/domino-client:0.1.12",
 | 
				
			||||||
      "docker-push": "docker push 192.168.1.115:5000/arhuako/domino-client:latest && docker push 192.168.1.115:5000/arhuako/domino-client:0.1.11",
 | 
					      "docker-push": "docker push 192.168.1.115:5000/arhuako/domino-client:latest && docker push 192.168.1.115:5000/arhuako/domino-client:0.1.12",
 | 
				
			||||||
      "publish": "npm run docker-build && npm run docker-tag && npm run docker-push",
 | 
					      "publish": "npm run docker-build && npm run docker-tag && npm run docker-push",
 | 
				
			||||||
      "serve": "npm run build-only && http-server ./dist -c-1 -s ",
 | 
					      "serve": "npm run build-only && http-server ./dist -c-1 -s ",
 | 
				
			||||||
      "tauri": "tauri"
 | 
					      "tauri": "tauri"
 | 
				
			||||||
@@ -109,6 +109,7 @@
 | 
				
			|||||||
      "bulma": "^1.0.1",
 | 
					      "bulma": "^1.0.1",
 | 
				
			||||||
      "colorette": "^2.0.20",
 | 
					      "colorette": "^2.0.20",
 | 
				
			||||||
      "dayjs": "^1.11.11",
 | 
					      "dayjs": "^1.11.11",
 | 
				
			||||||
 | 
					      "gsap": "^3.12.5",
 | 
				
			||||||
      "pinia": "^2.1.7",
 | 
					      "pinia": "^2.1.7",
 | 
				
			||||||
      "pino": "^9.2.0",
 | 
					      "pino": "^9.2.0",
 | 
				
			||||||
      "pixi-actions": "^1.1.11",
 | 
					      "pixi-actions": "^1.1.11",
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										28
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -1,18 +1,41 @@
 | 
				
			|||||||
# Changelog
 | 
					# Changelog
 | 
				
			||||||
 | 
					
 | 
				
			||||||
All notable changes to this project will be documented in this file.
 | 
					All notable changes to this project will be documented in this file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Unreleased
 | 
					## Unreleased
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 0.1.12 - 2024-07-22
 | 
					## 0.2.0 - 2024-07-25
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Added
 | 
					### Added
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- session expiration and renew handling
 | 
				
			||||||
 | 
					- Allow unique session per user
 | 
				
			||||||
 | 
					- Socket.io room management
 | 
				
			||||||
 | 
					- Game: added turn waiting timer
 | 
				
			||||||
 | 
					- Game: added timed button on game summaries
 | 
				
			||||||
 | 
					- Added new header
 | 
				
			||||||
 | 
					- New sound library (Howlerjs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Fixed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- App data persistance
 | 
				
			||||||
 | 
					- Some other Fixes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 0.1.12 - 2024-07-22
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Added
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- I18n translations
 | 
					- I18n translations
 | 
				
			||||||
- Win conditions
 | 
					- Win conditions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Fixed
 | 
					### Fixed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Multiplayer join button not accessible
 | 
					- Multiplayer join button not accessible
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 0.1.10 - 2024-07-20
 | 
					## 0.1.10 - 2024-07-20
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Added
 | 
					### Added
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Updater
 | 
					- Updater
 | 
				
			||||||
- Refresh authentication when expires
 | 
					- Refresh authentication when expires
 | 
				
			||||||
- Match summary page phase 1
 | 
					- Match summary page phase 1
 | 
				
			||||||
@@ -26,11 +49,14 @@ All notable changes to this project will be documented in this file.
 | 
				
			|||||||
## 0.1.6 - 2024-07-17
 | 
					## 0.1.6 - 2024-07-17
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 0.1.5 - 2024-07-17
 | 
					## 0.1.5 - 2024-07-17
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Added
 | 
					### Added
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Initial commit
 | 
					- Initial commit
 | 
				
			||||||
- Match page back button
 | 
					- Match page back button
 | 
				
			||||||
- Team play
 | 
					- Team play
 | 
				
			||||||
- Movement synchronized netween clients and AI players
 | 
					- Movement synchronized netween clients and AI players
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Fixed
 | 
					### Fixed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Button statuses
 | 
					- Button statuses
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,9 +7,7 @@
 | 
				
			|||||||
    <title>XXX App</title>
 | 
					    <title>XXX App</title>
 | 
				
			||||||
  </head>
 | 
					  </head>
 | 
				
			||||||
  <body>
 | 
					  <body>
 | 
				
			||||||
    <main class="container">
 | 
					    <div id="app"></div>
 | 
				
			||||||
      <div id="app"></div>
 | 
					 | 
				
			||||||
    </main>
 | 
					 | 
				
			||||||
    <script type="module" src="/src/main.ts"></script>
 | 
					    <script type="module" src="/src/main.ts"></script>
 | 
				
			||||||
  </body>
 | 
					  </body>
 | 
				
			||||||
</html>
 | 
					</html>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										23
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										23
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -1,18 +1,20 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "domino-client",
 | 
					  "name": "domino-client",
 | 
				
			||||||
  "version": "0.1.8",
 | 
					  "version": "0.2.0",
 | 
				
			||||||
  "lockfileVersion": 3,
 | 
					  "lockfileVersion": 3,
 | 
				
			||||||
  "requires": true,
 | 
					  "requires": true,
 | 
				
			||||||
  "packages": {
 | 
					  "packages": {
 | 
				
			||||||
    "": {
 | 
					    "": {
 | 
				
			||||||
      "name": "domino-client",
 | 
					      "name": "domino-client",
 | 
				
			||||||
      "version": "0.1.8",
 | 
					      "version": "0.2.0",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "@pixi/sound": "^6.0.0",
 | 
					        "@pixi/sound": "^6.0.0",
 | 
				
			||||||
        "@tauri-apps/api": "^1.6.0",
 | 
					        "@tauri-apps/api": "^1.6.0",
 | 
				
			||||||
        "bulma": "^1.0.1",
 | 
					        "bulma": "^1.0.1",
 | 
				
			||||||
        "colorette": "^2.0.20",
 | 
					        "colorette": "^2.0.20",
 | 
				
			||||||
        "dayjs": "^1.11.11",
 | 
					        "dayjs": "^1.11.11",
 | 
				
			||||||
 | 
					        "gsap": "^3.12.5",
 | 
				
			||||||
 | 
					        "howler": "^2.2.4",
 | 
				
			||||||
        "pinia": "^2.1.7",
 | 
					        "pinia": "^2.1.7",
 | 
				
			||||||
        "pino": "^9.2.0",
 | 
					        "pino": "^9.2.0",
 | 
				
			||||||
        "pixi-actions": "^1.1.11",
 | 
					        "pixi-actions": "^1.1.11",
 | 
				
			||||||
@@ -28,6 +30,7 @@
 | 
				
			|||||||
        "@rushstack/eslint-patch": "^1.8.0",
 | 
					        "@rushstack/eslint-patch": "^1.8.0",
 | 
				
			||||||
        "@tauri-apps/cli": "^1.6.0",
 | 
					        "@tauri-apps/cli": "^1.6.0",
 | 
				
			||||||
        "@tsconfig/node20": "^20.1.4",
 | 
					        "@tsconfig/node20": "^20.1.4",
 | 
				
			||||||
 | 
					        "@types/howler": "^2.2.11",
 | 
				
			||||||
        "@types/jsdom": "^21.1.7",
 | 
					        "@types/jsdom": "^21.1.7",
 | 
				
			||||||
        "@types/node": "^20.14.5",
 | 
					        "@types/node": "^20.14.5",
 | 
				
			||||||
        "@vitejs/plugin-vue": "^5.0.5",
 | 
					        "@vitejs/plugin-vue": "^5.0.5",
 | 
				
			||||||
@@ -1194,6 +1197,12 @@
 | 
				
			|||||||
      "resolved": "https://registry.npmjs.org/@types/gradient-parser/-/gradient-parser-0.1.5.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/gradient-parser/-/gradient-parser-0.1.5.tgz",
 | 
				
			||||||
      "integrity": "sha512-r7K3NkJz3A95WkVVmjs0NcchhHstC2C/VIYNX4JC6tieviUNo774FFeOHjThr3Vw/WCeMP9kAT77MKbIRlO/4w=="
 | 
					      "integrity": "sha512-r7K3NkJz3A95WkVVmjs0NcchhHstC2C/VIYNX4JC6tieviUNo774FFeOHjThr3Vw/WCeMP9kAT77MKbIRlO/4w=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@types/howler": {
 | 
				
			||||||
 | 
					      "version": "2.2.11",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@types/howler/-/howler-2.2.11.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-7aBoUL6RbSIrqKnpEgfa1wSNUBK06mn08siP2QI0zYk7MXfEJAaORc4tohamQYqCqVESoDyRWSdQn2BOKWj2Qw==",
 | 
				
			||||||
 | 
					      "dev": true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/@types/jsdom": {
 | 
					    "node_modules/@types/jsdom": {
 | 
				
			||||||
      "version": "21.1.7",
 | 
					      "version": "21.1.7",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz",
 | 
				
			||||||
@@ -3182,6 +3191,11 @@
 | 
				
			|||||||
      "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
 | 
					      "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
 | 
				
			||||||
      "dev": true
 | 
					      "dev": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/gsap": {
 | 
				
			||||||
 | 
					      "version": "3.12.5",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.5.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-srBfnk4n+Oe/ZnMIOXt3gT605BX9x5+rh/prT2F1SsNJsU1XuMiP0E2aptW481OnonOGACZWBqseH5Z7csHxhQ=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/has-flag": {
 | 
					    "node_modules/has-flag": {
 | 
				
			||||||
      "version": "4.0.0",
 | 
					      "version": "4.0.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
 | 
				
			||||||
@@ -3248,6 +3262,11 @@
 | 
				
			|||||||
        "he": "bin/he"
 | 
					        "he": "bin/he"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/howler": {
 | 
				
			||||||
 | 
					      "version": "2.2.4",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/howler/-/howler-2.2.4.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/html-encoding-sniffer": {
 | 
					    "node_modules/html-encoding-sniffer": {
 | 
				
			||||||
      "version": "4.0.0",
 | 
					      "version": "4.0.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "domino-client",
 | 
					  "name": "domino-client",
 | 
				
			||||||
  "version": "0.1.12",
 | 
					  "version": "0.2.0",
 | 
				
			||||||
  "private": true,
 | 
					  "private": true,
 | 
				
			||||||
  "type": "commonjs",
 | 
					  "type": "commonjs",
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
@@ -13,8 +13,8 @@
 | 
				
			|||||||
    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
 | 
					    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
 | 
				
			||||||
    "format": "prettier --write src/",
 | 
					    "format": "prettier --write src/",
 | 
				
			||||||
    "docker-build": "docker build -t 192.168.1.115:5000/arhuako/domino-client:latest .",
 | 
					    "docker-build": "docker build -t 192.168.1.115:5000/arhuako/domino-client:latest .",
 | 
				
			||||||
    "docker-tag": "docker tag 192.168.1.115:5000/arhuako/domino-client:latest 192.168.1.115:5000/arhuako/domino-client:0.1.12",
 | 
					    "docker-tag": "docker tag 192.168.1.115:5000/arhuako/domino-client:latest 192.168.1.115:5000/arhuako/domino-client:0.2.0",
 | 
				
			||||||
    "docker-push": "docker push 192.168.1.115:5000/arhuako/domino-client:latest && docker push 192.168.1.115:5000/arhuako/domino-client:0.1.12",
 | 
					    "docker-push": "docker push 192.168.1.115:5000/arhuako/domino-client:latest && docker push 192.168.1.115:5000/arhuako/domino-client:0.2.0",
 | 
				
			||||||
    "publish": "npm run docker-build && npm run docker-tag && npm run docker-push",
 | 
					    "publish": "npm run docker-build && npm run docker-tag && npm run docker-push",
 | 
				
			||||||
    "serve": "npm run build-only && http-server ./dist -c-1 -s ",
 | 
					    "serve": "npm run build-only && http-server ./dist -c-1 -s ",
 | 
				
			||||||
    "tauri": "tauri"
 | 
					    "tauri": "tauri"
 | 
				
			||||||
@@ -25,6 +25,8 @@
 | 
				
			|||||||
    "bulma": "^1.0.1",
 | 
					    "bulma": "^1.0.1",
 | 
				
			||||||
    "colorette": "^2.0.20",
 | 
					    "colorette": "^2.0.20",
 | 
				
			||||||
    "dayjs": "^1.11.11",
 | 
					    "dayjs": "^1.11.11",
 | 
				
			||||||
 | 
					    "gsap": "^3.12.5",
 | 
				
			||||||
 | 
					    "howler": "^2.2.4",
 | 
				
			||||||
    "pinia": "^2.1.7",
 | 
					    "pinia": "^2.1.7",
 | 
				
			||||||
    "pino": "^9.2.0",
 | 
					    "pino": "^9.2.0",
 | 
				
			||||||
    "pixi-actions": "^1.1.11",
 | 
					    "pixi-actions": "^1.1.11",
 | 
				
			||||||
@@ -40,6 +42,7 @@
 | 
				
			|||||||
    "@rushstack/eslint-patch": "^1.8.0",
 | 
					    "@rushstack/eslint-patch": "^1.8.0",
 | 
				
			||||||
    "@tauri-apps/cli": "^1.6.0",
 | 
					    "@tauri-apps/cli": "^1.6.0",
 | 
				
			||||||
    "@tsconfig/node20": "^20.1.4",
 | 
					    "@tsconfig/node20": "^20.1.4",
 | 
				
			||||||
 | 
					    "@types/howler": "^2.2.11",
 | 
				
			||||||
    "@types/jsdom": "^21.1.7",
 | 
					    "@types/jsdom": "^21.1.7",
 | 
				
			||||||
    "@types/node": "^20.14.5",
 | 
					    "@types/node": "^20.14.5",
 | 
				
			||||||
    "@vitejs/plugin-vue": "^5.0.5",
 | 
					    "@vitejs/plugin-vue": "^5.0.5",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,19 @@
 | 
				
			|||||||
<h1>Changelog</h1>
 | 
					<h1>Changelog</h1>
 | 
				
			||||||
<p>All notable changes to this project will be documented in this file.</p>
 | 
					<p>All notable changes to this project will be documented in this file.</p>
 | 
				
			||||||
 | 
					<h2>0.2.0 - 2024-07-24</h2>
 | 
				
			||||||
 | 
					<h3>Added</h3>
 | 
				
			||||||
 | 
					<ul>
 | 
				
			||||||
 | 
					<li>session expiration and renew handling</li>
 | 
				
			||||||
 | 
					<li>Allow unique session per user</li>
 | 
				
			||||||
 | 
					<li>Socket.io room management</li>
 | 
				
			||||||
 | 
					<li>Game: added turn waiting timer</li>
 | 
				
			||||||
 | 
					<li>Game: added timed button on game summaries</li>
 | 
				
			||||||
 | 
					<li>Added new header</li>
 | 
				
			||||||
 | 
					</ul>
 | 
				
			||||||
 | 
					<h3>Fixed</h3>
 | 
				
			||||||
 | 
					<ul>
 | 
				
			||||||
 | 
					<li>App data persistance</li>
 | 
				
			||||||
 | 
					</ul>
 | 
				
			||||||
<h2>0.1.12 - 2024-07-22</h2>
 | 
					<h2>0.1.12 - 2024-07-22</h2>
 | 
				
			||||||
<h3>Added</h3>
 | 
					<h3>Added</h3>
 | 
				
			||||||
<ul>
 | 
					<ul>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  "package": {
 | 
					  "package": {
 | 
				
			||||||
    "productName": "domino-client",
 | 
					    "productName": "domino-client",
 | 
				
			||||||
    "version": "0.1.11"
 | 
					    "version": "0.2.0"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "tauri": {
 | 
					  "tauri": {
 | 
				
			||||||
    "allowlist": {
 | 
					    "allowlist": {
 | 
				
			||||||
@@ -16,16 +16,10 @@
 | 
				
			|||||||
        "all": true
 | 
					        "all": true
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "fs": {
 | 
					      "fs": {
 | 
				
			||||||
        "copyFile": true,
 | 
					        "scope": {
 | 
				
			||||||
        "createDir": true,
 | 
					          "allow": ["$APPDATA/data/**", "$APPDATA/data/*", "$APPDATA/data", "$APPDATA/*"],
 | 
				
			||||||
        "exists": true,
 | 
					          "requireLiteralLeadingDot": false
 | 
				
			||||||
        "readDir": true,
 | 
					        },
 | 
				
			||||||
        "readFile": true,
 | 
					 | 
				
			||||||
        "removeDir": true,
 | 
					 | 
				
			||||||
        "removeFile": true,
 | 
					 | 
				
			||||||
        "renameFile": true,
 | 
					 | 
				
			||||||
        "scope": ["$APPDATA/*", "$APP/*"],
 | 
					 | 
				
			||||||
        "writeFile": true,
 | 
					 | 
				
			||||||
        "all": true
 | 
					        "all": true
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@@ -78,7 +72,7 @@
 | 
				
			|||||||
        "fullscreen": false,
 | 
					        "fullscreen": false,
 | 
				
			||||||
        "height": 720,
 | 
					        "height": 720,
 | 
				
			||||||
        "resizable": true,
 | 
					        "resizable": true,
 | 
				
			||||||
        "title": "Domino v0.1.11",
 | 
					        "title": "Domino v0.2.0",
 | 
				
			||||||
        "width": 1280,
 | 
					        "width": 1280,
 | 
				
			||||||
        "minHeight": 720,
 | 
					        "minHeight": 720,
 | 
				
			||||||
        "minWidth": 1280,
 | 
					        "minWidth": 1280,
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										63
									
								
								src/App.vue
									
									
									
									
									
								
							
							
						
						
									
										63
									
								
								src/App.vue
									
									
									
									
									
								
							@@ -1,60 +1,23 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import { inject, onMounted, onUnmounted } from 'vue'
 | 
					import { inject, onBeforeMount } from 'vue'
 | 
				
			||||||
import { RouterView } from 'vue-router'
 | 
					import { RouterView, useRouter } from 'vue-router'
 | 
				
			||||||
import type { AuthenticationService } from './services/AuthenticationService'
 | 
					import type { AuthenticationService } from './services/AuthenticationService'
 | 
				
			||||||
import { useEventBusStore } from './stores/eventBus'
 | 
					import { useAuthStore } from '@/stores/auth'
 | 
				
			||||||
import { sound } from '@pixi/sound'
 | 
					import { storeToRefs } from 'pinia'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const authStore = useAuthStore()
 | 
				
			||||||
 | 
					const { isLoggedIn } = storeToRefs(authStore)
 | 
				
			||||||
const auth: AuthenticationService = inject<AuthenticationService>('auth') as AuthenticationService
 | 
					const auth: AuthenticationService = inject<AuthenticationService>('auth') as AuthenticationService
 | 
				
			||||||
auth.fromStorage()
 | 
					const router = useRouter()
 | 
				
			||||||
const eventBus = useEventBusStore()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const handleBeforeUnload = (evt: any) => {
 | 
					async function checkLoggedIn() {
 | 
				
			||||||
  // evt.preventDefault()
 | 
					  await auth.fromStorage()
 | 
				
			||||||
  // const isGame = location.pathname === '/game' ? true : ''
 | 
					  if (isLoggedIn.value) {
 | 
				
			||||||
  // console.log('isGame :>> ', isGame)
 | 
					    router.push('home')
 | 
				
			||||||
  // evt.returnValue = isGame
 | 
					  }
 | 
				
			||||||
  // eventBus.publish('window-before-unload')
 | 
					 | 
				
			||||||
  // console.log('location.href :>> ', location.pathname)
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// document.addEventListener('visibilitychange', () => {
 | 
					onBeforeMount(checkLoggedIn)
 | 
				
			||||||
//   console.log('visibilitychange')
 | 
					 | 
				
			||||||
//   let playingOnHide = false
 | 
					 | 
				
			||||||
//   if (document.hidden) {
 | 
					 | 
				
			||||||
//     playingOnHide = true
 | 
					 | 
				
			||||||
//     sound.pauseAll()
 | 
					 | 
				
			||||||
//   } else {
 | 
					 | 
				
			||||||
//     // Page became visible! Resume playing if audio was "playing on hide"
 | 
					 | 
				
			||||||
//     if (playingOnHide) {
 | 
					 | 
				
			||||||
//       sound.resumeAll()
 | 
					 | 
				
			||||||
//     }
 | 
					 | 
				
			||||||
//   }
 | 
					 | 
				
			||||||
// })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// const soundContextResume = () => {
 | 
					 | 
				
			||||||
//   const context = sound.context.audioContext
 | 
					 | 
				
			||||||
//   if (context.state === 'suspended' || context.state === 'interrupted') {
 | 
					 | 
				
			||||||
//     context.resume()
 | 
					 | 
				
			||||||
//   }
 | 
					 | 
				
			||||||
// }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// document.addEventListener('click', function (event) {
 | 
					 | 
				
			||||||
//   console.log('click document :>> ', event)
 | 
					 | 
				
			||||||
//   console.log('screen :>> ', screen)
 | 
					 | 
				
			||||||
//   // if (event.target instanceof HTMLButtonElement) {
 | 
					 | 
				
			||||||
//   //   sound.play('click')
 | 
					 | 
				
			||||||
//   // }
 | 
					 | 
				
			||||||
// })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
onMounted(() => {
 | 
					 | 
				
			||||||
  window.addEventListener('beforeunload', handleBeforeUnload)
 | 
					 | 
				
			||||||
  // window.addEventListener('focus', soundContextResume)
 | 
					 | 
				
			||||||
})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
onUnmounted(() => {
 | 
					 | 
				
			||||||
  window.removeEventListener('beforeunload', handleBeforeUnload)
 | 
					 | 
				
			||||||
})
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template><RouterView /></template>
 | 
					<template><RouterView /></template>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								src/assets/images/profile-stub.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/images/profile-stub.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 1.7 KiB  | 
@@ -2,5 +2,6 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
html {
 | 
					html {
 | 
				
			||||||
  font-size: 16px;
 | 
					  font-size: 16px;
 | 
				
			||||||
  overflow: auto;
 | 
					  overflow-y: auto;
 | 
				
			||||||
 | 
					  overflow-x: hidden;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								src/assets/sounds/button_click.wav
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/sounds/button_click.wav
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								src/assets/sounds/ding-1.wav
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/sounds/ding-1.wav
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										7
									
								
								src/common/errors/ErrorBase.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/common/errors/ErrorBase.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					export class ErrorBase extends Error {
 | 
				
			||||||
 | 
					    constructor(message: string) {
 | 
				
			||||||
 | 
					        super(message);
 | 
				
			||||||
 | 
					        this.name = this.constructor.name;
 | 
				
			||||||
 | 
					        this.stack = (new Error()).stack;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										7
									
								
								src/common/errors/SessionExpiredError.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/common/errors/SessionExpiredError.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					import { ErrorBase } from './ErrorBase'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class SessionExpiredError extends ErrorBase {
 | 
				
			||||||
 | 
					  constructor() {
 | 
				
			||||||
 | 
					    super('Session expired error')
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -44,7 +44,7 @@ export function createButton(options: {
 | 
				
			|||||||
  parent?: Container
 | 
					  parent?: Container
 | 
				
			||||||
}): Container {
 | 
					}): Container {
 | 
				
			||||||
  const { text: textStr, dimension, action, parent } = options
 | 
					  const { text: textStr, dimension, action, parent } = options
 | 
				
			||||||
  const { x, y, width, height = 25 } = dimension
 | 
					  const { x = 0, y = 0, width = 0, height = 25 } = dimension
 | 
				
			||||||
  const rectangle = new Graphics().roundRect(x, y, width + 4, height + 4, 5).fill(0xffff00)
 | 
					  const rectangle = new Graphics().roundRect(x, y, width + 4, height + 4, 5).fill(0xffff00)
 | 
				
			||||||
  const text = new Text({
 | 
					  const text = new Text({
 | 
				
			||||||
    text: textStr,
 | 
					    text: textStr,
 | 
				
			||||||
@@ -87,8 +87,9 @@ export function createButton(options: {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function createCrosshair(container: Container, color: number = 0xff0000, d: Dimension) {
 | 
					export function createCrosshair(container: Container, color: number = 0xff0000, d: Dimension) {
 | 
				
			||||||
  const verticalLine = new Graphics().moveTo(d.x, 0).lineTo(d.x, d.height).stroke(color)
 | 
					  const { x = 0, y = 0, width = 0, height = 0 } = d
 | 
				
			||||||
  const horizontalLine = new Graphics().moveTo(0, d.y).lineTo(d.width, d.y).stroke(color)
 | 
					  const verticalLine = new Graphics().moveTo(x, 0).lineTo(x, height).stroke(color)
 | 
				
			||||||
 | 
					  const horizontalLine = new Graphics().moveTo(0, y).lineTo(width, y).stroke(color)
 | 
				
			||||||
  verticalLine.alpha = 0.2
 | 
					  verticalLine.alpha = 0.2
 | 
				
			||||||
  horizontalLine.alpha = 0.2
 | 
					  horizontalLine.alpha = 0.2
 | 
				
			||||||
  container.addChild(verticalLine)
 | 
					  container.addChild(verticalLine)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -39,6 +39,7 @@ export interface MatchSessionDto {
 | 
				
			|||||||
  matchInProgress: boolean
 | 
					  matchInProgress: boolean
 | 
				
			||||||
  playersReady: number
 | 
					  playersReady: number
 | 
				
			||||||
  gameSummaries: GameSummary[]
 | 
					  gameSummaries: GameSummary[]
 | 
				
			||||||
 | 
					  room: string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface GameDto {
 | 
					export interface GameDto {
 | 
				
			||||||
@@ -79,10 +80,10 @@ export interface ContainerOptions {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface Dimension {
 | 
					export interface Dimension {
 | 
				
			||||||
  width: number
 | 
					  width?: number
 | 
				
			||||||
  height: number
 | 
					  height?: number
 | 
				
			||||||
  x: number
 | 
					  x?: number
 | 
				
			||||||
  y: number
 | 
					  y?: number
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface SocketEvent {
 | 
					export interface SocketEvent {
 | 
				
			||||||
@@ -116,6 +117,7 @@ export interface MatchSessionOptions {
 | 
				
			|||||||
  winType: 'points' | 'rounds'
 | 
					  winType: 'points' | 'rounds'
 | 
				
			||||||
  sessionName: string
 | 
					  sessionName: string
 | 
				
			||||||
  numPlayers: 1 | 2 | 3 | 4
 | 
					  numPlayers: 1 | 2 | 3 | 4
 | 
				
			||||||
 | 
					  turnWaitSeconds: number
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface GameSummary {
 | 
					export interface GameSummary {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										232
									
								
								src/common/testMocks.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										232
									
								
								src/common/testMocks.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,232 @@
 | 
				
			|||||||
 | 
					import type { PlayerDto } from './interfaces'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const players = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    id: '668977662eb15e2ef3bdac3f',
 | 
				
			||||||
 | 
					    name: 'arhuako',
 | 
				
			||||||
 | 
					    score: 0,
 | 
				
			||||||
 | 
					    hand: [],
 | 
				
			||||||
 | 
					    ready: false,
 | 
				
			||||||
 | 
					    teamedWith: null,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    id: 'b4a4a1c8-da5f-492f-b002-69cdda33effd',
 | 
				
			||||||
 | 
					    name: 'Alice (AI)',
 | 
				
			||||||
 | 
					    score: 0,
 | 
				
			||||||
 | 
					    hand: [],
 | 
				
			||||||
 | 
					    ready: true,
 | 
				
			||||||
 | 
					    teamedWith: null,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    id: 'c617989c-6449-4c9c-8fc0-8b97aad62c5e',
 | 
				
			||||||
 | 
					    name: 'Bob (AI)',
 | 
				
			||||||
 | 
					    score: 0,
 | 
				
			||||||
 | 
					    hand: [],
 | 
				
			||||||
 | 
					    ready: true,
 | 
				
			||||||
 | 
					    teamedWith: null,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    id: 'c8579c74-22e0-4efe-9590-55d6c5e58b9e',
 | 
				
			||||||
 | 
					    name: 'Charlie (AI)',
 | 
				
			||||||
 | 
					    score: 0,
 | 
				
			||||||
 | 
					    hand: [],
 | 
				
			||||||
 | 
					    ready: true,
 | 
				
			||||||
 | 
					    teamedWith: null,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const dataPlayer: PlayerDto = {
 | 
				
			||||||
 | 
					  id: '668977662eb15e2ef3bdac3f',
 | 
				
			||||||
 | 
					  name: 'arhuako',
 | 
				
			||||||
 | 
					  score: 0,
 | 
				
			||||||
 | 
					  hand: [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      id: '53978793-a2bc-490c-9e42-6047112aece3',
 | 
				
			||||||
 | 
					      pips: [3, 0],
 | 
				
			||||||
 | 
					      playerId: '668977662eb15e2ef3bdac3f',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      id: '3d35a1f1-ab44-4445-9abf-ed22df2a1cb7',
 | 
				
			||||||
 | 
					      pips: [6, 4],
 | 
				
			||||||
 | 
					      playerId: '668977662eb15e2ef3bdac3f',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      id: 'd4b9bf35-8c84-40e4-afb5-90a35b0acd5c',
 | 
				
			||||||
 | 
					      pips: [4, 4],
 | 
				
			||||||
 | 
					      playerId: '668977662eb15e2ef3bdac3f',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      id: 'bc935092-9328-4af8-a90d-3774b8d4fd0e',
 | 
				
			||||||
 | 
					      pips: [5, 1],
 | 
				
			||||||
 | 
					      playerId: '668977662eb15e2ef3bdac3f',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      id: '987270de-0eec-42b5-8e24-e3cde0bee3e3',
 | 
				
			||||||
 | 
					      pips: [0, 0],
 | 
				
			||||||
 | 
					      playerId: '668977662eb15e2ef3bdac3f',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      id: '4f5626e2-b7b7-4cd3-8d18-498bbf79f796',
 | 
				
			||||||
 | 
					      pips: [3, 2],
 | 
				
			||||||
 | 
					      playerId: '668977662eb15e2ef3bdac3f',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      id: 'c1ff8494-5ece-4a73-a816-475416952c42',
 | 
				
			||||||
 | 
					      pips: [3, 1],
 | 
				
			||||||
 | 
					      playerId: '668977662eb15e2ef3bdac3f',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  ready: true,
 | 
				
			||||||
 | 
					  teamedWith: null,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const dataGameStatePlayer: PlayerDto[] = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    id: '668977662eb15e2ef3bdac3f',
 | 
				
			||||||
 | 
					    name: 'arhuako',
 | 
				
			||||||
 | 
					    score: 0,
 | 
				
			||||||
 | 
					    hand: [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: '53978793-a2bc-490c-9e42-6047112aece3',
 | 
				
			||||||
 | 
					        playerId: '668977662eb15e2ef3bdac3f',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: '3d35a1f1-ab44-4445-9abf-ed22df2a1cb7',
 | 
				
			||||||
 | 
					        playerId: '668977662eb15e2ef3bdac3f',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: 'd4b9bf35-8c84-40e4-afb5-90a35b0acd5c',
 | 
				
			||||||
 | 
					        playerId: '668977662eb15e2ef3bdac3f',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: 'bc935092-9328-4af8-a90d-3774b8d4fd0e',
 | 
				
			||||||
 | 
					        playerId: '668977662eb15e2ef3bdac3f',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: '987270de-0eec-42b5-8e24-e3cde0bee3e3',
 | 
				
			||||||
 | 
					        playerId: '668977662eb15e2ef3bdac3f',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: '4f5626e2-b7b7-4cd3-8d18-498bbf79f796',
 | 
				
			||||||
 | 
					        playerId: '668977662eb15e2ef3bdac3f',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: 'c1ff8494-5ece-4a73-a816-475416952c42',
 | 
				
			||||||
 | 
					        playerId: '668977662eb15e2ef3bdac3f',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    ready: false,
 | 
				
			||||||
 | 
					    teamedWith: null,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    id: 'c60d0a87-dd85-49d3-a1af-6fb543e6a242',
 | 
				
			||||||
 | 
					    name: 'Alice (AI)',
 | 
				
			||||||
 | 
					    score: 0,
 | 
				
			||||||
 | 
					    hand: [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: '68765ee6-9755-4c92-b16b-baf90980cc57',
 | 
				
			||||||
 | 
					        playerId: 'c60d0a87-dd85-49d3-a1af-6fb543e6a242',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: 'f3ad0e4d-4bcb-4382-947e-51da8295fbf4',
 | 
				
			||||||
 | 
					        playerId: 'c60d0a87-dd85-49d3-a1af-6fb543e6a242',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: 'acabba58-10f7-43d3-8388-f1f2f32e42e8',
 | 
				
			||||||
 | 
					        playerId: 'c60d0a87-dd85-49d3-a1af-6fb543e6a242',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: '213fb154-8ac3-457f-9ca2-cdebe39d6059',
 | 
				
			||||||
 | 
					        playerId: 'c60d0a87-dd85-49d3-a1af-6fb543e6a242',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: 'da6168c3-1ea4-48f3-b2fa-181fbc8f18c7',
 | 
				
			||||||
 | 
					        playerId: 'c60d0a87-dd85-49d3-a1af-6fb543e6a242',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: '9131a434-2b2b-466d-8b8a-73fa34b75d1c',
 | 
				
			||||||
 | 
					        playerId: 'c60d0a87-dd85-49d3-a1af-6fb543e6a242',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: '0a777281-8421-4f12-a406-12fe37bdaf5f',
 | 
				
			||||||
 | 
					        playerId: 'c60d0a87-dd85-49d3-a1af-6fb543e6a242',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    ready: true,
 | 
				
			||||||
 | 
					    teamedWith: null,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    id: 'db074bca-2d19-45e5-9646-3dadaddb092a',
 | 
				
			||||||
 | 
					    name: 'Bob (AI)',
 | 
				
			||||||
 | 
					    score: 0,
 | 
				
			||||||
 | 
					    hand: [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: '02375d76-4162-442c-9667-af755c494a62',
 | 
				
			||||||
 | 
					        playerId: 'db074bca-2d19-45e5-9646-3dadaddb092a',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: '6cb15d5c-5512-42d5-b278-1c4ac0584bb1',
 | 
				
			||||||
 | 
					        playerId: 'db074bca-2d19-45e5-9646-3dadaddb092a',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: '695ac5a4-6fbb-4735-ba5a-b938d096f878',
 | 
				
			||||||
 | 
					        playerId: 'db074bca-2d19-45e5-9646-3dadaddb092a',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: '67371083-1493-4de3-aa53-dab9583db002',
 | 
				
			||||||
 | 
					        playerId: 'db074bca-2d19-45e5-9646-3dadaddb092a',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: '988d57fc-295a-4ad3-b8dc-cc799a9084ed',
 | 
				
			||||||
 | 
					        playerId: 'db074bca-2d19-45e5-9646-3dadaddb092a',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: '5128ab10-0b60-4640-8425-e205f89b2a27',
 | 
				
			||||||
 | 
					        playerId: 'db074bca-2d19-45e5-9646-3dadaddb092a',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: '576e9c1a-195c-427b-b54c-d1fdd41b1721',
 | 
				
			||||||
 | 
					        playerId: 'db074bca-2d19-45e5-9646-3dadaddb092a',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    ready: true,
 | 
				
			||||||
 | 
					    teamedWith: null,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    id: '5eccfb2d-2fb7-40ab-be39-9630d4775eab',
 | 
				
			||||||
 | 
					    name: 'Charlie (AI)',
 | 
				
			||||||
 | 
					    score: 0,
 | 
				
			||||||
 | 
					    hand: [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: '6938b04c-d5fb-4f9f-98b2-a1a61c4eaa0b',
 | 
				
			||||||
 | 
					        playerId: '5eccfb2d-2fb7-40ab-be39-9630d4775eab',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: 'fb2a5add-3829-47ef-84d9-37bfa9d78a0e',
 | 
				
			||||||
 | 
					        playerId: '5eccfb2d-2fb7-40ab-be39-9630d4775eab',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: 'd2d0360f-a8f3-43ff-ac4f-14aa4152e9cb',
 | 
				
			||||||
 | 
					        playerId: '5eccfb2d-2fb7-40ab-be39-9630d4775eab',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: '68b10359-df28-4d4d-8e04-45f0b98a0f17',
 | 
				
			||||||
 | 
					        playerId: '5eccfb2d-2fb7-40ab-be39-9630d4775eab',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: '44dbbfc4-fd3f-4091-b71d-5aac80df34bf',
 | 
				
			||||||
 | 
					        playerId: '5eccfb2d-2fb7-40ab-be39-9630d4775eab',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: '7cba5113-de2a-4388-92ff-47a7ea151f9a',
 | 
				
			||||||
 | 
					        playerId: '5eccfb2d-2fb7-40ab-be39-9630d4775eab',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: 'a112485c-ce19-49bd-8ae6-c26266abe6d5',
 | 
				
			||||||
 | 
					        playerId: '5eccfb2d-2fb7-40ab-be39-9630d4775eab',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    ready: true,
 | 
				
			||||||
 | 
					    teamedWith: null,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
@@ -21,8 +21,7 @@ const { updateGameState } = gameStore
 | 
				
			|||||||
const { gameOptions } = storeToRefs(gameOptionsStore)
 | 
					const { gameOptions } = storeToRefs(gameOptionsStore)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const minScreenWidth = 1280
 | 
					const minScreenWidth = 1280
 | 
				
			||||||
const minScreenHeight = 72
 | 
					const minScreenHeight = 720
 | 
				
			||||||
0
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
let screenWidth = window.innerWidth - 10
 | 
					let screenWidth = window.innerWidth - 10
 | 
				
			||||||
let screenHeight = window.innerHeight - 10
 | 
					let screenHeight = window.innerHeight - 10
 | 
				
			||||||
@@ -44,6 +43,7 @@ const defaultOptions: MatchSessionOptions = {
 | 
				
			|||||||
  seed: '',
 | 
					  seed: '',
 | 
				
			||||||
  sessionName: `Test #${Date.now()}`,
 | 
					  sessionName: `Test #${Date.now()}`,
 | 
				
			||||||
  numPlayers: 1,
 | 
					  numPlayers: 1,
 | 
				
			||||||
 | 
					  turnWaitSeconds: 30,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
const gameOptionsValue: MatchSessionOptions = {
 | 
					const gameOptionsValue: MatchSessionOptions = {
 | 
				
			||||||
  ...defaultOptions,
 | 
					  ...defaultOptions,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,10 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import type { MatchSessionOptions } from '@/common/interfaces'
 | 
					import type { MatchSessionOptions } from '@/common/interfaces'
 | 
				
			||||||
import { computed, ref } from 'vue'
 | 
					import type { InfoService } from '@/services/InfoService'
 | 
				
			||||||
 | 
					import { computed, inject, ref } from 'vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const emit = defineEmits(['createMatch', 'startSingleMatch'])
 | 
					const emit = defineEmits(['createMatch', 'startSingleMatch'])
 | 
				
			||||||
 | 
					const info = inject<InfoService>('info')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let options = ref<MatchSessionOptions>({
 | 
					let options = ref<MatchSessionOptions>({
 | 
				
			||||||
  background: 'green',
 | 
					  background: 'green',
 | 
				
			||||||
@@ -10,12 +12,14 @@ let options = ref<MatchSessionOptions>({
 | 
				
			|||||||
  winType: 'points',
 | 
					  winType: 'points',
 | 
				
			||||||
  winTarget: 100,
 | 
					  winTarget: 100,
 | 
				
			||||||
  seed: '',
 | 
					  seed: '',
 | 
				
			||||||
  sessionName: `Test #${Date.now()}`,
 | 
					  sessionName: info?.development ? `Test #${Date.now()}` : `Session #${Date.now()}`,
 | 
				
			||||||
  numPlayers: 1,
 | 
					  numPlayers: 1,
 | 
				
			||||||
 | 
					  turnWaitSeconds: 30,
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const winTargetPointsList = [20, 50, 80, 100, 150, 200]
 | 
					const winTargetPointsList = [20, 50, 80, 100, 150, 200]
 | 
				
			||||||
const winTargetRoundsList = [1, 2, 3, 4, 5, 6]
 | 
					const winTargetRoundsList = [1, 2, 3, 4, 5, 6]
 | 
				
			||||||
 | 
					const turnWaitSecondsList = [15, 20, 30, 40, 50, 60]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const backgroundOptiopnList = [
 | 
					const backgroundOptiopnList = [
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
@@ -123,7 +127,7 @@ function startSingleMatch() {
 | 
				
			|||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="grid">
 | 
					    <div class="grid" v-if="info?.development">
 | 
				
			||||||
      <div class="cell">
 | 
					      <div class="cell">
 | 
				
			||||||
        <div class="field">
 | 
					        <div class="field">
 | 
				
			||||||
          <label class="label">{{ $t('session-name') }}</label>
 | 
					          <label class="label">{{ $t('session-name') }}</label>
 | 
				
			||||||
@@ -235,6 +239,24 @@ function startSingleMatch() {
 | 
				
			|||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="cell">
 | 
				
			||||||
 | 
					        <div class="field">
 | 
				
			||||||
 | 
					          <label for="winTarget" class="label">Turn Wait</label>
 | 
				
			||||||
 | 
					          <div class="control">
 | 
				
			||||||
 | 
					            <div class="select">
 | 
				
			||||||
 | 
					              <select v-model="options.turnWaitSeconds" name="turnWait">
 | 
				
			||||||
 | 
					                <option
 | 
				
			||||||
 | 
					                  v-bind:key="turnWait"
 | 
				
			||||||
 | 
					                  v-for="turnWait in turnWaitSecondsList"
 | 
				
			||||||
 | 
					                  :value="turnWait"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  {{ turnWait }} seconds
 | 
				
			||||||
 | 
					                </option>
 | 
				
			||||||
 | 
					              </select>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="buttons mt-6">
 | 
					    <div class="buttons mt-6">
 | 
				
			||||||
      <button class="button is-primary" @click.prevent="createMatch" v-if="isMultiPlayer">
 | 
					      <button class="button is-primary" @click.prevent="createMatch" v-if="isMultiPlayer">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,21 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { inject, onBeforeUnmount, onMounted, ref } from 'vue'
 | 
				
			||||||
import { useRouter } from 'vue-router'
 | 
					import { useRouter } from 'vue-router'
 | 
				
			||||||
import { RouterView } from 'vue-router'
 | 
					import { RouterView } from 'vue-router'
 | 
				
			||||||
 | 
					import { storeToRefs } from 'pinia'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useAuthStore } from '@/stores/auth'
 | 
				
			||||||
 | 
					import { AuthenticationService } from '@/services/AuthenticationService'
 | 
				
			||||||
 | 
					import { emit, listen } from '@tauri-apps/api/event'
 | 
				
			||||||
 | 
					import profileStubImage from '@/assets/images/profile-stub.png'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const authStore = useAuthStore()
 | 
				
			||||||
const router = useRouter()
 | 
					const router = useRouter()
 | 
				
			||||||
 | 
					const userProfileMenuActive = ref(false)
 | 
				
			||||||
 | 
					const profileMenu = ref<HTMLElement | null>(null)
 | 
				
			||||||
 | 
					const { user } = storeToRefs(authStore)
 | 
				
			||||||
 | 
					const authService = inject<AuthenticationService>('auth')
 | 
				
			||||||
 | 
					const info: any = inject('info')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = defineProps({
 | 
					const props = defineProps({
 | 
				
			||||||
  navbar: Boolean,
 | 
					  navbar: Boolean,
 | 
				
			||||||
@@ -13,27 +26,94 @@ defineOptions({
 | 
				
			|||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function logout() {
 | 
					function logout() {
 | 
				
			||||||
 | 
					  authService?.logout()
 | 
				
			||||||
  router.push({ name: 'landing' })
 | 
					  router.push({ name: 'landing' })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function toggleProfileMenu() {
 | 
				
			||||||
 | 
					  userProfileMenuActive.value = !userProfileMenuActive.value
 | 
				
			||||||
 | 
					  if (userProfileMenuActive.value) {
 | 
				
			||||||
 | 
					    addClickOutsideListener()
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    removeClickOutsideListener()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function addClickOutsideListener() {
 | 
				
			||||||
 | 
					  document.addEventListener('click', closeProfileMenu)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function removeClickOutsideListener() {
 | 
				
			||||||
 | 
					  document.removeEventListener('click', closeProfileMenu)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function closeProfileMenu(event: any) {
 | 
				
			||||||
 | 
					  console.log('profileMenu.value :>> ', profileMenu.value)
 | 
				
			||||||
 | 
					  if (profileMenu.value !== null && !profileMenu.value.contains(event.target)) {
 | 
				
			||||||
 | 
					    userProfileMenuActive.value = false
 | 
				
			||||||
 | 
					    removeClickOutsideListener()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function checkForUpdates() {
 | 
				
			||||||
 | 
					  emit('tauri://update')
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(() => {
 | 
				
			||||||
 | 
					  if (userProfileMenuActive.value) {
 | 
				
			||||||
 | 
					    addClickOutsideListener()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onBeforeUnmount(() => {
 | 
				
			||||||
 | 
					  removeClickOutsideListener()
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Listen for update download progress
 | 
				
			||||||
 | 
					if (info.tauri) {
 | 
				
			||||||
 | 
					  listen('tauri://update-not-available', () => {
 | 
				
			||||||
 | 
					    userProfileMenuActive.value = false
 | 
				
			||||||
 | 
					    removeClickOutsideListener()
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					const mode = import.meta.env.MODE
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="authenticated-layout">
 | 
					  <header class="container" v-if="props.navbar">
 | 
				
			||||||
    <header v-if="props.navbar">
 | 
					    {{ mode }} - {{ info.tauri }} - {{ info }}
 | 
				
			||||||
      <nav class="navbar">
 | 
					    <nav class="navbar is-transparent">
 | 
				
			||||||
 | 
					      <div class="navbar-menu">
 | 
				
			||||||
        <div class="navbar-end">
 | 
					        <div class="navbar-end">
 | 
				
			||||||
          <div class="navbar-item">
 | 
					          <div
 | 
				
			||||||
            <div class="buttons">
 | 
					            class="navbar-item has-dropdown"
 | 
				
			||||||
              <button class="button is-primary" @click="logout">Logout</button>
 | 
					            :class="{ 'is-active': userProfileMenuActive }"
 | 
				
			||||||
 | 
					            ref="profileMenu"
 | 
				
			||||||
 | 
					            v-if="user"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <a class="navbar-link" @click.prevent="toggleProfileMenu">
 | 
				
			||||||
 | 
					              <figure class="image is-32x32">
 | 
				
			||||||
 | 
					                <img class="is-rounded" :src="profileStubImage" alt="{{ user.username }}" />
 | 
				
			||||||
 | 
					              </figure>
 | 
				
			||||||
 | 
					              <span class="ml-2">{{ user.username }}</span>
 | 
				
			||||||
 | 
					            </a>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <div class="navbar-dropdown is-boxed is-right">
 | 
				
			||||||
 | 
					              <a class="navbar-item" @click.prevent="logout">Logout</a>
 | 
				
			||||||
 | 
					              <a class="navbar-item" @click.prevent="checkForUpdates" v-if="info.tauri"
 | 
				
			||||||
 | 
					                >Check for update</a
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					              <hr class="navbar-divider" />
 | 
				
			||||||
 | 
					              <div class="navbar-item">Version 0.2.0</div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </nav>
 | 
					      </div>
 | 
				
			||||||
    </header>
 | 
					    </nav>
 | 
				
			||||||
    <main>
 | 
					  </header>
 | 
				
			||||||
      <RouterView></RouterView>
 | 
					  <main class="container">
 | 
				
			||||||
    </main>
 | 
					    <RouterView></RouterView>
 | 
				
			||||||
  </div>
 | 
					  </main>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped>
 | 
					<style scoped>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,12 @@
 | 
				
			|||||||
import { Application, Container, EventEmitter, Text, Ticker } from 'pixi.js'
 | 
					import { Application, Container, EventEmitter, Text, Ticker } from 'pixi.js'
 | 
				
			||||||
import { Scale, type ScaleFunction } from '@/game/utilities/scale'
 | 
					import { Scale, type ScaleFunction } from '@/game/utilities/scale'
 | 
				
			||||||
import type { AnimationOptions, Movement, PlayerDto, TileDto } from '@/common/interfaces'
 | 
					import type {
 | 
				
			||||||
 | 
					  AnimationOptions,
 | 
				
			||||||
 | 
					  MatchSessionOptions,
 | 
				
			||||||
 | 
					  Movement,
 | 
				
			||||||
 | 
					  PlayerDto,
 | 
				
			||||||
 | 
					  TileDto,
 | 
				
			||||||
 | 
					} from '@/common/interfaces'
 | 
				
			||||||
import { Tile } from '@/game/Tile'
 | 
					import { Tile } from '@/game/Tile'
 | 
				
			||||||
import { createContainer, isTilePair } from '@/common/helpers'
 | 
					import { createContainer, isTilePair } from '@/common/helpers'
 | 
				
			||||||
import { createText } from '@/game/utilities/fonts'
 | 
					import { createText } from '@/game/utilities/fonts'
 | 
				
			||||||
@@ -8,13 +14,14 @@ import { LoggingService } from '@/services/LoggingService'
 | 
				
			|||||||
import { GlowFilter } from 'pixi-filters'
 | 
					import { GlowFilter } from 'pixi-filters'
 | 
				
			||||||
import { DIRECTION_INDEXES, DIRECTIONS, ORIENTATION_ANGLES } from '@/common/constants'
 | 
					import { DIRECTION_INDEXES, DIRECTIONS, ORIENTATION_ANGLES } from '@/common/constants'
 | 
				
			||||||
import type { OtherHand } from './OtherHand'
 | 
					import type { OtherHand } from './OtherHand'
 | 
				
			||||||
import { sound } from '@pixi/sound'
 | 
					import { SoundManager } from '@/game/utilities/SoundManager'
 | 
				
			||||||
import { t } from '@/i18n'
 | 
					import { t } from '@/i18n'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class Board extends EventEmitter {
 | 
					export class Board extends EventEmitter {
 | 
				
			||||||
  private _scale: number = 1
 | 
					  private _scale: number = 1
 | 
				
			||||||
  private _canMove: boolean = false
 | 
					  private _canMove: boolean = false
 | 
				
			||||||
  private logger: LoggingService = new LoggingService()
 | 
					  private logger: LoggingService = new LoggingService()
 | 
				
			||||||
 | 
					  private soundManager: SoundManager = new SoundManager()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ticker: Ticker
 | 
					  ticker: Ticker
 | 
				
			||||||
  height: number
 | 
					  height: number
 | 
				
			||||||
@@ -45,7 +52,10 @@ export class Board extends EventEmitter {
 | 
				
			|||||||
  currentPlayer!: PlayerDto
 | 
					  currentPlayer!: PlayerDto
 | 
				
			||||||
  otherPlayerHands: OtherHand[] = []
 | 
					  otherPlayerHands: OtherHand[] = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(app: Application) {
 | 
					  constructor(
 | 
				
			||||||
 | 
					    app: Application,
 | 
				
			||||||
 | 
					    private options: MatchSessionOptions,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
    super()
 | 
					    super()
 | 
				
			||||||
    this.ticker = app.ticker
 | 
					    this.ticker = app.ticker
 | 
				
			||||||
    this.height = app.canvas.height - 130 - 130
 | 
					    this.height = app.canvas.height - 130 - 130
 | 
				
			||||||
@@ -147,6 +157,7 @@ export class Board extends EventEmitter {
 | 
				
			|||||||
  async playerMove(move: any, playerId: string) {
 | 
					  async playerMove(move: any, playerId: string) {
 | 
				
			||||||
    const { move: lastMove } = move
 | 
					    const { move: lastMove } = move
 | 
				
			||||||
    if (lastMove === null) {
 | 
					    if (lastMove === null) {
 | 
				
			||||||
 | 
					      // Player passed
 | 
				
			||||||
      setTimeout(() => {
 | 
					      setTimeout(() => {
 | 
				
			||||||
        this.emit('game:tile-animation-ended')
 | 
					        this.emit('game:tile-animation-ended')
 | 
				
			||||||
      }, 500)
 | 
					      }, 500)
 | 
				
			||||||
@@ -162,7 +173,6 @@ export class Board extends EventEmitter {
 | 
				
			|||||||
      this.nextTile = tile
 | 
					      this.nextTile = tile
 | 
				
			||||||
      lastMove.tile = tile.toPlain()
 | 
					      lastMove.tile = tile.toPlain()
 | 
				
			||||||
      this.movements.push(lastMove)
 | 
					      this.movements.push(lastMove)
 | 
				
			||||||
      console.log('this.movements :>> ', this.movements)
 | 
					 | 
				
			||||||
      await this.addTile(tile, lastMove)
 | 
					      await this.addTile(tile, lastMove)
 | 
				
			||||||
      this.setFreeEnd(lastMove)
 | 
					      this.setFreeEnd(lastMove)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -237,7 +247,7 @@ export class Board extends EventEmitter {
 | 
				
			|||||||
    this.tiles.push(tile)
 | 
					    this.tiles.push(tile)
 | 
				
			||||||
    const moveSound = this.getRandomClickSound()
 | 
					    const moveSound = this.getRandomClickSound()
 | 
				
			||||||
    await this.animateTile(tile, x, y, orientation, move)
 | 
					    await this.animateTile(tile, x, y, orientation, move)
 | 
				
			||||||
    sound.play(moveSound)
 | 
					    this.soundManager.play(moveSound)
 | 
				
			||||||
    this.emit('game:tile-animation-ended', tile.toPlain())
 | 
					    this.emit('game:tile-animation-ended', tile.toPlain())
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										132
									
								
								src/game/Button.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								src/game/Button.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,132 @@
 | 
				
			|||||||
 | 
					import type { Dimension } from '@/common/interfaces'
 | 
				
			||||||
 | 
					import { BlurFilter, Container, Graphics, Point, Text, TextStyle } from 'pixi.js'
 | 
				
			||||||
 | 
					import { gsap } from 'gsap'
 | 
				
			||||||
 | 
					import { SoundManager } from '@/game/utilities/SoundManager'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class Button extends Container {
 | 
				
			||||||
 | 
					  private dimension: Dimension
 | 
				
			||||||
 | 
					  private isDisabled: boolean = false
 | 
				
			||||||
 | 
					  private soundManager: SoundManager = new SoundManager()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public disabledColor: number = 0xffff00
 | 
				
			||||||
 | 
					  public activeColor: number = 0xffff00
 | 
				
			||||||
 | 
					  public hoverColor: number = 0xffff00
 | 
				
			||||||
 | 
					  public shadowColor: number = 0x121212
 | 
				
			||||||
 | 
					  public clickSound: string = 'snd-click-btn'
 | 
				
			||||||
 | 
					  public textStyle: any = {
 | 
				
			||||||
 | 
					    fontFamily: 'Arial',
 | 
				
			||||||
 | 
					    fontSize: 15,
 | 
				
			||||||
 | 
					    fontWeight: 'bolder',
 | 
				
			||||||
 | 
					    fill: 0x121212,
 | 
				
			||||||
 | 
					    align: 'center',
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    protected text: string,
 | 
				
			||||||
 | 
					    protected point: Point,
 | 
				
			||||||
 | 
					    dimension: Dimension,
 | 
				
			||||||
 | 
					    protected action: Function,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    super()
 | 
				
			||||||
 | 
					    this.dimension = { ...{ width: 100, height: 30 }, ...dimension }
 | 
				
			||||||
 | 
					    this.render()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected getTextString() {
 | 
				
			||||||
 | 
					    return this.text
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected executeAction() {
 | 
				
			||||||
 | 
					    this.action()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  set disabled(value: boolean) {
 | 
				
			||||||
 | 
					    this.isDisabled = value
 | 
				
			||||||
 | 
					    this.off('pointerdown')
 | 
				
			||||||
 | 
					    this.off('pointerover')
 | 
				
			||||||
 | 
					    this.off('pointerout')
 | 
				
			||||||
 | 
					    this.render()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get disabled() {
 | 
				
			||||||
 | 
					    return this.isDisabled
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected getTextObject(): Text {
 | 
				
			||||||
 | 
					    const { x, y } = this.point
 | 
				
			||||||
 | 
					    const { width = 0, height = 0 } = this.dimension
 | 
				
			||||||
 | 
					    const text = new Text({
 | 
				
			||||||
 | 
					      text: this.getTextString(),
 | 
				
			||||||
 | 
					      style: this.textStyle,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    text.anchor = 0.5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    text.y = 2 + y + height / 2
 | 
				
			||||||
 | 
					    text.x = 2 + x + width / 2
 | 
				
			||||||
 | 
					    return text
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected getButtonBack() {
 | 
				
			||||||
 | 
					    const { x, y } = this.point
 | 
				
			||||||
 | 
					    const { width = 0, height = 0 } = this.dimension
 | 
				
			||||||
 | 
					    const color = this.isDisabled ? this.disabledColor : this.activeColor
 | 
				
			||||||
 | 
					    const back = new Graphics().roundRect(x, y, width + 4, height + 4, 5).fill(color)
 | 
				
			||||||
 | 
					    const shadow = new Graphics()
 | 
				
			||||||
 | 
					      .roundRect(x + 3, y + 2, width + 4, height + 4, 5)
 | 
				
			||||||
 | 
					      .fill(this.shadowColor)
 | 
				
			||||||
 | 
					    back.alpha = 0.7
 | 
				
			||||||
 | 
					    shadow.alpha = 1
 | 
				
			||||||
 | 
					    shadow.filters = [new BlurFilter({ strength: 15 })]
 | 
				
			||||||
 | 
					    this.addChild(shadow)
 | 
				
			||||||
 | 
					    this.addChild(back)
 | 
				
			||||||
 | 
					    return back
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render() {
 | 
				
			||||||
 | 
					    this.removeChildren()
 | 
				
			||||||
 | 
					    const back = this.getButtonBack()
 | 
				
			||||||
 | 
					    const text = this.getTextObject()
 | 
				
			||||||
 | 
					    this.addChild(text)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!this.isDisabled) {
 | 
				
			||||||
 | 
					      back.fill(this.activeColor)
 | 
				
			||||||
 | 
					      back.alpha = 0.7
 | 
				
			||||||
 | 
					      this.eventMode = 'static'
 | 
				
			||||||
 | 
					      this.cursor = 'pointer'
 | 
				
			||||||
 | 
					      this.on('pointerdown', (event) => {
 | 
				
			||||||
 | 
					        this.emit('click', event)
 | 
				
			||||||
 | 
					        this.onPointerDown(text, back)
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      this.on('pointerover', () => {
 | 
				
			||||||
 | 
					        back.alpha = 1
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.on('pointerout', () => {
 | 
				
			||||||
 | 
					        back.alpha = 0.7
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      text.alpha = 0.7
 | 
				
			||||||
 | 
					      back.alpha = 0.5
 | 
				
			||||||
 | 
					      this.eventMode = 'none'
 | 
				
			||||||
 | 
					      this.cursor = 'default'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onPointerDown(text: Text, back: Graphics) {
 | 
				
			||||||
 | 
					    this.soundManager.play(this.clickSound)
 | 
				
			||||||
 | 
					    gsap
 | 
				
			||||||
 | 
					      .timeline()
 | 
				
			||||||
 | 
					      .to(
 | 
				
			||||||
 | 
					        [back, text],
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          duration: 0.06,
 | 
				
			||||||
 | 
					          x: '+=1',
 | 
				
			||||||
 | 
					          y: '+=2',
 | 
				
			||||||
 | 
					          ease: 'linear',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        0,
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      .to([back, text], { duration: 0.06, x: '-=1', y: '-=2', ease: 'linear' })
 | 
				
			||||||
 | 
					    this.executeAction()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										125
									
								
								src/game/Game.ts
									
									
									
									
									
								
							
							
						
						
									
										125
									
								
								src/game/Game.ts
									
									
									
									
									
								
							@@ -19,7 +19,7 @@ import { GameSummayView } from './GameSummayView'
 | 
				
			|||||||
import Config from './Config'
 | 
					import Config from './Config'
 | 
				
			||||||
import { createText, grayStyle } from './utilities/fonts'
 | 
					import { createText, grayStyle } from './utilities/fonts'
 | 
				
			||||||
import { t } from '@/i18n'
 | 
					import { t } from '@/i18n'
 | 
				
			||||||
 | 
					import { DIRECTION_INDEXES, DIRECTIONS } from '@/common/constants'
 | 
				
			||||||
export class Game extends EventEmitter {
 | 
					export class Game extends EventEmitter {
 | 
				
			||||||
  public board!: Board
 | 
					  public board!: Board
 | 
				
			||||||
  public hand!: Hand
 | 
					  public hand!: Hand
 | 
				
			||||||
@@ -33,7 +33,7 @@ export class Game extends EventEmitter {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    private options: MatchSessionOptions,
 | 
					    private options: MatchSessionOptions,
 | 
				
			||||||
    private socketService: SocketIoClientService,
 | 
					    private socketService: SocketIoClientService | undefined,
 | 
				
			||||||
    private playerId: string,
 | 
					    private playerId: string,
 | 
				
			||||||
    private sessionId: string,
 | 
					    private sessionId: string,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
@@ -47,6 +47,10 @@ export class Game extends EventEmitter {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get stage() {
 | 
				
			||||||
 | 
					    return this.app.stage
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async setup(): Promise<HTMLCanvasElement> {
 | 
					  async setup(): Promise<HTMLCanvasElement> {
 | 
				
			||||||
    const width = this.options.screen?.width || 1280
 | 
					    const width = this.options.screen?.width || 1280
 | 
				
			||||||
    const height = this.options.screen?.height || 720
 | 
					    const height = this.options.screen?.height || 720
 | 
				
			||||||
@@ -58,8 +62,8 @@ export class Game extends EventEmitter {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  async start(players: PlayerDto[] = []) {
 | 
					  async start(players: PlayerDto[] = []) {
 | 
				
			||||||
    this.iniialStuff(this.app)
 | 
					    this.iniialStuff(this.app)
 | 
				
			||||||
    this.board = new Board(this.app)
 | 
					    this.board = new Board(this.app, this.options)
 | 
				
			||||||
    this.hand = new Hand(this.app)
 | 
					    this.hand = new Hand(this.app, this.options)
 | 
				
			||||||
    this.otherHands = [
 | 
					    this.otherHands = [
 | 
				
			||||||
      new OtherHand(this.app, 'left'),
 | 
					      new OtherHand(this.app, 'left'),
 | 
				
			||||||
      new OtherHand(this.app, 'top'),
 | 
					      new OtherHand(this.app, 'top'),
 | 
				
			||||||
@@ -73,16 +77,21 @@ export class Game extends EventEmitter {
 | 
				
			|||||||
    this.setBoardEvents()
 | 
					    this.setBoardEvents()
 | 
				
			||||||
    this.setHandEvents()
 | 
					    this.setHandEvents()
 | 
				
			||||||
    this.initEventBus()
 | 
					    this.initEventBus()
 | 
				
			||||||
    wait(3000)
 | 
					    await wait(500)
 | 
				
			||||||
    this.socketService.sendMessage('client:set-client-ready', {
 | 
					    this.socketService &&
 | 
				
			||||||
      sessionId: this.sessionId,
 | 
					      this.socketService.sendMessage('client:set-client-ready', {
 | 
				
			||||||
      userId: this.playerId,
 | 
					        sessionId: this.sessionId,
 | 
				
			||||||
    })
 | 
					        userId: this.playerId,
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  iniialStuff(app: Application) {
 | 
					  iniialStuff(app: Application) {
 | 
				
			||||||
    app.stage.addChild(this.backgroundLayer)
 | 
					    app.stage.addChild(this.backgroundLayer)
 | 
				
			||||||
    const background = new TilingSprite(Assets.get(`bg-${this.options.background}`))
 | 
					    const background = new TilingSprite({
 | 
				
			||||||
 | 
					      texture: Assets.get(`bg-${this.options.background}`),
 | 
				
			||||||
 | 
					      width: app.canvas.width,
 | 
				
			||||||
 | 
					      height: app.canvas.height,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.backgroundLayer.addChild(background)
 | 
					    this.backgroundLayer.addChild(background)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -160,19 +169,9 @@ export class Game extends EventEmitter {
 | 
				
			|||||||
      this.board.setPlayerHand(tiles)
 | 
					      this.board.setPlayerHand(tiles)
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    this.hand.on('game:tile-click', (tile: TileDto) => this.highlightMoves(tile))
 | 
					    this.hand.on('game:tile-click', (tile: TileDto) => this.highlightMoves(tile))
 | 
				
			||||||
 | 
					    this.hand.on('game:timer-timeout', this.handleHandPlayerTimeout.bind(this))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.hand.on('game:button-pass-click', async () => {
 | 
					    this.hand.on('game:button-pass-click', this.sendPassEvent.bind(this))
 | 
				
			||||||
      const move: Movement = {
 | 
					 | 
				
			||||||
        id: '',
 | 
					 | 
				
			||||||
        type: 'pass',
 | 
					 | 
				
			||||||
        playerId: this.playerId,
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      this.socketService.sendMessage('client:player-move', {
 | 
					 | 
				
			||||||
        sessionId: this.sessionId,
 | 
					 | 
				
			||||||
        move: move,
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      await this.board.updateBoard(move, undefined)
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.gameSummaryView.on('finishClick', (data) => {
 | 
					    this.gameSummaryView.on('finishClick', (data) => {
 | 
				
			||||||
      this.emit('game:finish-click', data)
 | 
					      this.emit('game:finish-click', data)
 | 
				
			||||||
@@ -181,15 +180,69 @@ export class Game extends EventEmitter {
 | 
				
			|||||||
    this.gameSummaryView.on('nextClick', (data) => {
 | 
					    this.gameSummaryView.on('nextClick', (data) => {
 | 
				
			||||||
      this.board.clean()
 | 
					      this.board.clean()
 | 
				
			||||||
      this.updateScoreboard(data.sessionState)
 | 
					      this.updateScoreboard(data.sessionState)
 | 
				
			||||||
      this.socketService.sendMessage('client:set-client-ready-for-next-game', {
 | 
					      this.socketService &&
 | 
				
			||||||
        userId: this.playerId,
 | 
					        this.socketService.sendMessage('client:set-client-ready-for-next-game', {
 | 
				
			||||||
        sessionId: this.sessionId,
 | 
					          userId: this.playerId,
 | 
				
			||||||
      })
 | 
					          sessionId: this.sessionId,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.hand.on('hand-initialized', () => {})
 | 
					    this.hand.on('hand-initialized', () => {})
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private handleHandPlayerTimeout(tile: TileDto | null) {
 | 
				
			||||||
 | 
					    if (tile === null) {
 | 
				
			||||||
 | 
					      this.sendPassEvent()
 | 
				
			||||||
 | 
					      this.hand.disablePassButton()
 | 
				
			||||||
 | 
					      return
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const freeEnds = this.board.freeEnds
 | 
				
			||||||
 | 
					    console.log('freeEnds :>> ', freeEnds, this.playerId)
 | 
				
			||||||
 | 
					    const move: Movement = {
 | 
				
			||||||
 | 
					      id: '',
 | 
				
			||||||
 | 
					      tile,
 | 
				
			||||||
 | 
					      type: 'right',
 | 
				
			||||||
 | 
					      playerId: this.playerId,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (freeEnds !== undefined) {
 | 
				
			||||||
 | 
					      const side = tile.pips?.includes(freeEnds[0]) ? 'left' : 'right'
 | 
				
			||||||
 | 
					      const direction = side === 'left' ? this.board.leftDirection : this.board.rightDirection
 | 
				
			||||||
 | 
					      let dirIndex = DIRECTION_INDEXES[direction]
 | 
				
			||||||
 | 
					      const validMoves = this.board.nextTileValidMoves(tile, side)
 | 
				
			||||||
 | 
					      const validPoints = this.board.nextTileValidPoints(tile, side, validMoves)
 | 
				
			||||||
 | 
					      let safeCount = 4
 | 
				
			||||||
 | 
					      while (safeCount > 0 && !validMoves[dirIndex % 4]) {
 | 
				
			||||||
 | 
					        dirIndex += 1
 | 
				
			||||||
 | 
					        safeCount -= 1
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      console.log('validMoves :>> ', validMoves)
 | 
				
			||||||
 | 
					      console.log('validPoints :>> ', validPoints)
 | 
				
			||||||
 | 
					      const validPoint = validPoints[dirIndex % 4]
 | 
				
			||||||
 | 
					      if (validPoint !== undefined) {
 | 
				
			||||||
 | 
					        move.x = validPoint[0]
 | 
				
			||||||
 | 
					        move.y = validPoint[1]
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      move.direction = DIRECTIONS[dirIndex % 4]
 | 
				
			||||||
 | 
					      move.type = side
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this.currentMove = move
 | 
				
			||||||
 | 
					    this.board.updateBoard(move, this.hand.tileMoved(tile))
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private async sendPassEvent() {
 | 
				
			||||||
 | 
					    const move: Movement = {
 | 
				
			||||||
 | 
					      id: '',
 | 
				
			||||||
 | 
					      type: 'pass',
 | 
				
			||||||
 | 
					      playerId: this.playerId,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this.socketService &&
 | 
				
			||||||
 | 
					      this.socketService.sendMessage('client:player-move', {
 | 
				
			||||||
 | 
					        sessionId: this.sessionId,
 | 
				
			||||||
 | 
					        move: move,
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    await this.board.updateBoard(move, undefined)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private updateScoreboard(sessionState: MatchSessionDto) {
 | 
					  private updateScoreboard(sessionState: MatchSessionDto) {
 | 
				
			||||||
    const scoreboard = sessionState.scoreboard
 | 
					    const scoreboard = sessionState.scoreboard
 | 
				
			||||||
    const myScore = scoreboard.find((d) => d.id === this.playerId)?.score || 0
 | 
					    const myScore = scoreboard.find((d) => d.id === this.playerId)?.score || 0
 | 
				
			||||||
@@ -269,15 +322,17 @@ export class Game extends EventEmitter {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    this.board.on('game:tile-animation-ended', async (tile) => {
 | 
					    this.board.on('game:tile-animation-ended', async (tile) => {
 | 
				
			||||||
      if (tile !== null && tile !== undefined && tile.playerId === this.playerId) {
 | 
					      if (tile !== null && tile !== undefined && tile.playerId === this.playerId) {
 | 
				
			||||||
        this.socketService.sendMessage('client:player-move', {
 | 
					        this.socketService &&
 | 
				
			||||||
          sessionId: this.sessionId,
 | 
					          this.socketService.sendMessage('client:player-move', {
 | 
				
			||||||
          move: this.currentMove,
 | 
					            sessionId: this.sessionId,
 | 
				
			||||||
        })
 | 
					            move: this.currentMove,
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        this.socketService.sendMessage('client:animation-ended', {
 | 
					        this.socketService &&
 | 
				
			||||||
          sessionId: this.sessionId,
 | 
					          this.socketService.sendMessage('client:animation-ended', {
 | 
				
			||||||
          userId: this.playerId,
 | 
					            sessionId: this.sessionId,
 | 
				
			||||||
        })
 | 
					            userId: this.playerId,
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -306,6 +361,8 @@ export class Game extends EventEmitter {
 | 
				
			|||||||
      if (otherHand) {
 | 
					      if (otherHand) {
 | 
				
			||||||
        otherHand.update(data.move)
 | 
					        otherHand.update(data.move)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // ShowPassed
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,14 @@
 | 
				
			|||||||
import { createButton, createContainer } from '@/common/helpers'
 | 
					import { createContainer } from '@/common/helpers'
 | 
				
			||||||
import type { GameSummary, MatchSessionDto, MatchSessionOptions } from '@/common/interfaces'
 | 
					import type {
 | 
				
			||||||
import { EventEmitter, type Application, type Container } from 'pixi.js'
 | 
					  Dimension,
 | 
				
			||||||
 | 
					  GameSummary,
 | 
				
			||||||
 | 
					  MatchSessionDto,
 | 
				
			||||||
 | 
					  MatchSessionOptions,
 | 
				
			||||||
 | 
					} from '@/common/interfaces'
 | 
				
			||||||
 | 
					import { EventEmitter, Point, type Application, type Container } from 'pixi.js'
 | 
				
			||||||
import { createText, whiteStyle, yellowStyle } from './utilities/fonts'
 | 
					import { createText, whiteStyle, yellowStyle } from './utilities/fonts'
 | 
				
			||||||
import { t } from '@/i18n'
 | 
					import { t } from '@/i18n'
 | 
				
			||||||
 | 
					import { TimedButton } from './TimedButton'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class GameSummayView extends EventEmitter {
 | 
					export class GameSummayView extends EventEmitter {
 | 
				
			||||||
  public width: number
 | 
					  public width: number
 | 
				
			||||||
@@ -136,41 +142,20 @@ export class GameSummayView extends EventEmitter {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  renderButtons() {
 | 
					  renderButtons() {
 | 
				
			||||||
    if (this.type === 'round') {
 | 
					    const x = this.width / 2 - 25
 | 
				
			||||||
      this.layer.addChild(
 | 
					    const y = this.height - 55
 | 
				
			||||||
        createButton({
 | 
					    const d: Dimension = { x, y, width: 90, height: 30 }
 | 
				
			||||||
          text: 'Next',
 | 
					    const text = this.type === 'round' ? 'Next' : 'Finish'
 | 
				
			||||||
          dimension: {
 | 
					    const event = this.type === 'round' ? 'nextClick' : 'finishClick'
 | 
				
			||||||
            x: this.width / 2 - 25,
 | 
					    const action = () => {
 | 
				
			||||||
            y: this.height - 50,
 | 
					      this.emit(event, { sessionState: this.matchState })
 | 
				
			||||||
            width: 60,
 | 
					      this.container.visible = false
 | 
				
			||||||
            height: 25,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          action: () => {
 | 
					 | 
				
			||||||
            this.emit('nextClick', { sessionState: this.matchState })
 | 
					 | 
				
			||||||
            this.container.visible = false
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          parent: this.layer,
 | 
					 | 
				
			||||||
        }),
 | 
					 | 
				
			||||||
      )
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      this.layer.addChild(
 | 
					 | 
				
			||||||
        createButton({
 | 
					 | 
				
			||||||
          text: 'Finish',
 | 
					 | 
				
			||||||
          dimension: {
 | 
					 | 
				
			||||||
            x: this.width / 2 - 25,
 | 
					 | 
				
			||||||
            y: this.height - 50,
 | 
					 | 
				
			||||||
            width: 60,
 | 
					 | 
				
			||||||
            height: 25,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          action: () => {
 | 
					 | 
				
			||||||
            this.emit('finishClick', this.gameSummary)
 | 
					 | 
				
			||||||
            this.container.visible = false
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          parent: this.layer,
 | 
					 | 
				
			||||||
        }),
 | 
					 | 
				
			||||||
      )
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    const btn: TimedButton = new TimedButton(text, new Point(x, y), d, action, 30)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    btn.on('timeout', action)
 | 
				
			||||||
 | 
					    this.layer.addChild(btn)
 | 
				
			||||||
 | 
					    btn.start()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render() {
 | 
					  render() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,17 +1,28 @@
 | 
				
			|||||||
import { Application, Container, EventEmitter, Graphics, Sprite, Texture, Ticker } from 'pixi.js'
 | 
					import {
 | 
				
			||||||
 | 
					  Application,
 | 
				
			||||||
 | 
					  Container,
 | 
				
			||||||
 | 
					  EventEmitter,
 | 
				
			||||||
 | 
					  Graphics,
 | 
				
			||||||
 | 
					  Point,
 | 
				
			||||||
 | 
					  Sprite,
 | 
				
			||||||
 | 
					  Texture,
 | 
				
			||||||
 | 
					  Ticker,
 | 
				
			||||||
 | 
					} from 'pixi.js'
 | 
				
			||||||
import { Tile } from '@/game/Tile'
 | 
					import { Tile } from '@/game/Tile'
 | 
				
			||||||
import type { PlayerDto, TileDto } from '@/common/interfaces'
 | 
					import type { MatchSessionOptions, PlayerDto, TileDto } from '@/common/interfaces'
 | 
				
			||||||
import { GlowFilter } from 'pixi-filters'
 | 
					import { GlowFilter } from 'pixi-filters'
 | 
				
			||||||
import { Scale, type ScaleFunction } from './utilities/scale'
 | 
					import { Scale, type ScaleFunction } from './utilities/scale'
 | 
				
			||||||
import { LoggingService } from '@/services/LoggingService'
 | 
					import { LoggingService } from '@/services/LoggingService'
 | 
				
			||||||
import { createButton, createContainer } from '@/common/helpers'
 | 
					import { createContainer } from '@/common/helpers'
 | 
				
			||||||
import { createText, playerNameText, whiteStyle } from './utilities/fonts'
 | 
					import { createText, playerNameText, whiteStyle } from './utilities/fonts'
 | 
				
			||||||
import Config from '@/game/Config'
 | 
					import Config from '@/game/Config'
 | 
				
			||||||
 | 
					import { TimerText } from './TimerText'
 | 
				
			||||||
 | 
					import { Button } from './Button'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class Hand extends EventEmitter {
 | 
					export class Hand extends EventEmitter {
 | 
				
			||||||
  tiles: Tile[] = []
 | 
					  tiles: Tile[] = []
 | 
				
			||||||
  container: Container = new Container()
 | 
					  container: Container = new Container()
 | 
				
			||||||
  buttonPass: Container = new Container()
 | 
					  buttonPass!: Button
 | 
				
			||||||
  scoreLayer: Container = new Container()
 | 
					  scoreLayer: Container = new Container()
 | 
				
			||||||
  activeLayer: Container = new Container()
 | 
					  activeLayer: Container = new Container()
 | 
				
			||||||
  height: number
 | 
					  height: number
 | 
				
			||||||
@@ -32,8 +43,12 @@ export class Hand extends EventEmitter {
 | 
				
			|||||||
  score: number = 0
 | 
					  score: number = 0
 | 
				
			||||||
  active: boolean = false
 | 
					  active: boolean = false
 | 
				
			||||||
  private player!: PlayerDto
 | 
					  private player!: PlayerDto
 | 
				
			||||||
 | 
					  private timer: TimerText
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(app: Application) {
 | 
					  constructor(
 | 
				
			||||||
 | 
					    app: Application,
 | 
				
			||||||
 | 
					    private options: MatchSessionOptions,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
    super()
 | 
					    super()
 | 
				
			||||||
    app.stage.addChild(this.container)
 | 
					    app.stage.addChild(this.container)
 | 
				
			||||||
    this.ticker = app.ticker
 | 
					    this.ticker = app.ticker
 | 
				
			||||||
@@ -43,6 +58,8 @@ export class Hand extends EventEmitter {
 | 
				
			|||||||
    this.container.x = app.canvas.width / 2 - this.width / 2
 | 
					    this.container.x = app.canvas.width / 2 - this.width / 2
 | 
				
			||||||
    this.container.width = this.width
 | 
					    this.container.width = this.width
 | 
				
			||||||
    this.container.height = this.height
 | 
					    this.container.height = this.height
 | 
				
			||||||
 | 
					    this.buttonPass = this.createPassButton()
 | 
				
			||||||
 | 
					    this.timer = this.createTimer(this.options.turnWaitSeconds)
 | 
				
			||||||
    this.calculateScale()
 | 
					    this.calculateScale()
 | 
				
			||||||
    this.initLayers()
 | 
					    this.initLayers()
 | 
				
			||||||
    this.render()
 | 
					    this.render()
 | 
				
			||||||
@@ -67,6 +84,8 @@ export class Hand extends EventEmitter {
 | 
				
			|||||||
    })
 | 
					    })
 | 
				
			||||||
    this.container.addChild(this.scoreLayer)
 | 
					    this.container.addChild(this.scoreLayer)
 | 
				
			||||||
    this.container.addChild(this.activeLayer)
 | 
					    this.container.addChild(this.activeLayer)
 | 
				
			||||||
 | 
					    this.interactionsLayer.addChild(this.buttonPass)
 | 
				
			||||||
 | 
					    this.interactionsLayer.addChild(this.timer)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  gameFinished() {
 | 
					  gameFinished() {
 | 
				
			||||||
@@ -90,7 +109,7 @@ export class Hand extends EventEmitter {
 | 
				
			|||||||
      ? this.tiles
 | 
					      ? this.tiles
 | 
				
			||||||
      : this.tiles.filter((tile) => this.hasMoves(tile.toPlain(), freeEnds))
 | 
					      : this.tiles.filter((tile) => this.hasMoves(tile.toPlain(), freeEnds))
 | 
				
			||||||
    if (this.availableTiles.length === 0) {
 | 
					    if (this.availableTiles.length === 0) {
 | 
				
			||||||
      this.createPassButton()
 | 
					      this.buttonPass.disabled = false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    this.availableTiles.forEach((tile) => {
 | 
					    this.availableTiles.forEach((tile) => {
 | 
				
			||||||
      tile.animateTo({
 | 
					      tile.animateTo({
 | 
				
			||||||
@@ -99,6 +118,31 @@ export class Hand extends EventEmitter {
 | 
				
			|||||||
      })
 | 
					      })
 | 
				
			||||||
      tile.interactive = true
 | 
					      tile.interactive = true
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					    if (this.timer) {
 | 
				
			||||||
 | 
					      this.timer.reset()
 | 
				
			||||||
 | 
					      this.timer.start()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  createTimer(seconds: number): TimerText {
 | 
				
			||||||
 | 
					    const timer = new TimerText(seconds, new Point(this.width - 60, 45))
 | 
				
			||||||
 | 
					    // timer.animation = false
 | 
				
			||||||
 | 
					    timer.on('timeout', () => {
 | 
				
			||||||
 | 
					      this.timerTimeout()
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    return timer
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  timerTimeout() {
 | 
				
			||||||
 | 
					    if (this.timer === undefined) return
 | 
				
			||||||
 | 
					    this.timer.reset()
 | 
				
			||||||
 | 
					    if (this.availableTiles.length === 0) {
 | 
				
			||||||
 | 
					      this.emit('game:timer-timeout', null)
 | 
				
			||||||
 | 
					      return
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const randomTile = this.availableTiles[Math.floor(Math.random() * this.availableTiles.length)] // pick random tile
 | 
				
			||||||
 | 
					    randomTile.alpha = 1
 | 
				
			||||||
 | 
					    this.emit('game:timer-timeout', randomTile.toPlain())
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  afterMove() {
 | 
					  afterMove() {
 | 
				
			||||||
@@ -110,6 +154,7 @@ export class Hand extends EventEmitter {
 | 
				
			|||||||
      tile.setPosition(tile.x, tile.y + 10)
 | 
					      tile.setPosition(tile.x, tile.y + 10)
 | 
				
			||||||
      tile.interactive = false
 | 
					      tile.interactive = false
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					    this.timer && this.timer.reset()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  hasMoves(tile: TileDto, freeEnds?: [number, number]): boolean {
 | 
					  hasMoves(tile: TileDto, freeEnds?: [number, number]): boolean {
 | 
				
			||||||
@@ -181,18 +226,22 @@ export class Hand extends EventEmitter {
 | 
				
			|||||||
    return tile
 | 
					    return tile
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private createPassButton() {
 | 
					  private createPassButton(): Button {
 | 
				
			||||||
    const lastTile = this.tiles[this.tiles.length - 1]
 | 
					    const x = this.width - 100
 | 
				
			||||||
    const x = lastTile ? lastTile.x + lastTile.width : this.scaleX(0)
 | 
					    const y = this.height - 45
 | 
				
			||||||
    this.buttonPass = createButton({
 | 
					    const btn = new Button('PASS', new Point(x, y), { width: 80, height: 30 }, () => {
 | 
				
			||||||
      text: 'PASS',
 | 
					      this.timer.reset()
 | 
				
			||||||
      dimension: { x, y: this.height / 2, width: 50, height: 20 },
 | 
					      this.buttonPass.disabled = true
 | 
				
			||||||
      action: () => {
 | 
					      this.emit('game:button-pass-click')
 | 
				
			||||||
        this.interactionsLayer.removeChild(this.buttonPass)
 | 
					 | 
				
			||||||
        this.emit('game:button-pass-click')
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      parent: this.interactionsLayer,
 | 
					 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					    btn.disabledColor = 0xbabf95
 | 
				
			||||||
 | 
					    btn.disabled = true
 | 
				
			||||||
 | 
					    this.buttonPass = btn
 | 
				
			||||||
 | 
					    return btn
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  disablePassButton() {
 | 
				
			||||||
 | 
					    this.buttonPass.disabled = true
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  update(playerState: PlayerDto) {
 | 
					  update(playerState: PlayerDto) {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										49
									
								
								src/game/TimedButton.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/game/TimedButton.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
				
			|||||||
 | 
					import type { Point } from 'pixi.js'
 | 
				
			||||||
 | 
					import { Button } from './Button'
 | 
				
			||||||
 | 
					import type { Dimension } from '@/common/interfaces'
 | 
				
			||||||
 | 
					import { Timer } from './utilities/Timer'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class TimedButton extends Button {
 | 
				
			||||||
 | 
					  private timer: Timer
 | 
				
			||||||
 | 
					  private countdown: number = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    protected text: string,
 | 
				
			||||||
 | 
					    protected point: Point,
 | 
				
			||||||
 | 
					    dimension: Dimension,
 | 
				
			||||||
 | 
					    protected action: Function,
 | 
				
			||||||
 | 
					    private seconds: number,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    super(text, point, dimension, action)
 | 
				
			||||||
 | 
					    this.timer = new Timer(seconds)
 | 
				
			||||||
 | 
					    this.timer.on('timeout', this.onTImeOut.bind(this))
 | 
				
			||||||
 | 
					    this.timer.on('tick', this.onTick.bind(this))
 | 
				
			||||||
 | 
					    this.countdown = seconds
 | 
				
			||||||
 | 
					    this.render()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onTImeOut() {
 | 
				
			||||||
 | 
					    this.disabled = true
 | 
				
			||||||
 | 
					    this.timer.reset()
 | 
				
			||||||
 | 
					    this.emit('timeout')
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private onTick(seconds: number) {
 | 
				
			||||||
 | 
					    this.countdown = seconds
 | 
				
			||||||
 | 
					    this.render()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  start() {
 | 
				
			||||||
 | 
					    this.timer.reset()
 | 
				
			||||||
 | 
					    this.timer.start()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected getTextString() {
 | 
				
			||||||
 | 
					    return `${this.text} (${this.countdown})`
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected executeAction() {
 | 
				
			||||||
 | 
					    this.action()
 | 
				
			||||||
 | 
					    this.timer.reset()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										93
									
								
								src/game/TimerText.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/game/TimerText.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,93 @@
 | 
				
			|||||||
 | 
					import { Container, Point, Text, TextStyle } from 'pixi.js'
 | 
				
			||||||
 | 
					import { createText, timerStyle } from './utilities/fonts'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { gsap } from 'gsap'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class TimerText extends Container {
 | 
				
			||||||
 | 
					  private intervalHandle?: any
 | 
				
			||||||
 | 
					  private text?: Text
 | 
				
			||||||
 | 
					  animation: boolean = false
 | 
				
			||||||
 | 
					  textStyle: TextStyle = timerStyle()
 | 
				
			||||||
 | 
					  private countdown: number = 0
 | 
				
			||||||
 | 
					  private isInitiated: boolean = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    private seconds: number,
 | 
				
			||||||
 | 
					    private point: Point,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    super()
 | 
				
			||||||
 | 
					    this.countdown = seconds
 | 
				
			||||||
 | 
					    this.render()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  reset() {
 | 
				
			||||||
 | 
					    this.stop()
 | 
				
			||||||
 | 
					    this.countdown = this.seconds
 | 
				
			||||||
 | 
					    this.isInitiated = false
 | 
				
			||||||
 | 
					    this.render()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  start() {
 | 
				
			||||||
 | 
					    clearInterval(this.intervalHandle)
 | 
				
			||||||
 | 
					    this.isInitiated = true
 | 
				
			||||||
 | 
					    this.removeChildren()
 | 
				
			||||||
 | 
					    this.render()
 | 
				
			||||||
 | 
					    this.intervalHandle = setInterval(() => {
 | 
				
			||||||
 | 
					      this.countdown--
 | 
				
			||||||
 | 
					      !this.animation && this.text?.destroy()
 | 
				
			||||||
 | 
					      this.render()
 | 
				
			||||||
 | 
					      if (this.countdown === 0) {
 | 
				
			||||||
 | 
					        clearInterval(this.intervalHandle)
 | 
				
			||||||
 | 
					        this.emit('timeout')
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }, 1000)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  destroy() {
 | 
				
			||||||
 | 
					    this.stop()
 | 
				
			||||||
 | 
					    this.destroy()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  stop() {
 | 
				
			||||||
 | 
					    clearInterval(this.intervalHandle)
 | 
				
			||||||
 | 
					    this.emit('stop', this.countdown)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private render() {
 | 
				
			||||||
 | 
					    if (!this.animation) this.removeChildren()
 | 
				
			||||||
 | 
					    this.text = createText({
 | 
				
			||||||
 | 
					      text: this.countdown.toString(),
 | 
				
			||||||
 | 
					      x: this.point.x,
 | 
				
			||||||
 | 
					      y: this.point.y,
 | 
				
			||||||
 | 
					      style: this.textStyle,
 | 
				
			||||||
 | 
					      container: this,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    this.text.scale.set(1)
 | 
				
			||||||
 | 
					    if (this.animation && this.isInitiated) this.animate(this.text)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private animate(text: Text) {
 | 
				
			||||||
 | 
					    if (text) {
 | 
				
			||||||
 | 
					      gsap.timeline().to(text, {
 | 
				
			||||||
 | 
					        delay: 0.5,
 | 
				
			||||||
 | 
					        duration: 1.2,
 | 
				
			||||||
 | 
					        x: this.point.x + 2,
 | 
				
			||||||
 | 
					        y: this.point.y - 25,
 | 
				
			||||||
 | 
					        ease: 'power4.out',
 | 
				
			||||||
 | 
					        alpha: 0.0,
 | 
				
			||||||
 | 
					        onComplete: () => {
 | 
				
			||||||
 | 
					          text.destroy()
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        // scale: 1,
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      // gsap.to(this.text, {
 | 
				
			||||||
 | 
					      //   duration: 1,
 | 
				
			||||||
 | 
					      //   alpha: 0,
 | 
				
			||||||
 | 
					      //   x: this.app.canvas.width / 2,
 | 
				
			||||||
 | 
					      //   y: this.app.canvas.height / 2 - 20,
 | 
				
			||||||
 | 
					      //   scale: 1.3,
 | 
				
			||||||
 | 
					      //   ease: 'linear',
 | 
				
			||||||
 | 
					      // })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										60
									
								
								src/game/utilities/SoundManager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/game/utilities/SoundManager.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
				
			|||||||
 | 
					import snd_move_1 from '@/assets/sounds/move-1.mp3'
 | 
				
			||||||
 | 
					import snd_move_2 from '@/assets/sounds/move-2.mp3'
 | 
				
			||||||
 | 
					import snd_move_3 from '@/assets/sounds/move-3.mp3'
 | 
				
			||||||
 | 
					import snd_move_4 from '@/assets/sounds/move-4.mp3'
 | 
				
			||||||
 | 
					import snd_intro from '@/assets/sounds/intro.mp3'
 | 
				
			||||||
 | 
					import snd_click_btn from '@/assets/sounds/button_click.wav'
 | 
				
			||||||
 | 
					import snd_ding_1 from '@/assets/sounds/ding-1.wav'
 | 
				
			||||||
 | 
					import { Howl } from 'howler'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const soundAssets = [
 | 
				
			||||||
 | 
					  { name: 'snd-move-1', src: [snd_move_1], loop: false, volume: 1 },
 | 
				
			||||||
 | 
					  { name: 'snd-move-2', src: [snd_move_2], loop: false, volume: 1 },
 | 
				
			||||||
 | 
					  { name: 'snd-move-3', src: [snd_move_3], loop: false, volume: 1 },
 | 
				
			||||||
 | 
					  { name: 'snd-move-4', src: [snd_move_4], loop: false, volume: 1 },
 | 
				
			||||||
 | 
					  { name: 'snd-intro', src: [snd_intro], loop: true, volume: 1 },
 | 
				
			||||||
 | 
					  { name: 'snd-click-btn', src: [snd_click_btn], loop: false, volume: 1 },
 | 
				
			||||||
 | 
					  { name: 'snd-ding-1', src: [snd_ding_1], loop: false, volume: 1 },
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class SoundManager {
 | 
				
			||||||
 | 
					  private sounds: Record<string, Howl> = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor() {
 | 
				
			||||||
 | 
					    soundAssets.forEach(({ name, src, loop, volume = 1 }) => {
 | 
				
			||||||
 | 
					      this.sounds[name] = new Howl({
 | 
				
			||||||
 | 
					        src,
 | 
				
			||||||
 | 
					        loop,
 | 
				
			||||||
 | 
					        volume: volume,
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  play(name: string) {
 | 
				
			||||||
 | 
					    const sound = this.sounds[name]
 | 
				
			||||||
 | 
					    if (sound === undefined) {
 | 
				
			||||||
 | 
					      console.error(`Sound ${name} not found`)
 | 
				
			||||||
 | 
					      return
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    sound.play()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pause(name: string) {
 | 
				
			||||||
 | 
					    const sound = this.sounds[name]
 | 
				
			||||||
 | 
					    if (sound === undefined) {
 | 
				
			||||||
 | 
					      console.error(`Sound ${name} not found`)
 | 
				
			||||||
 | 
					      return
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    sound.pause()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  stop(name: string) {
 | 
				
			||||||
 | 
					    const sound = this.sounds[name]
 | 
				
			||||||
 | 
					    if (sound === undefined) {
 | 
				
			||||||
 | 
					      console.error(`Sound ${name} not found`)
 | 
				
			||||||
 | 
					      return
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    sound.pause()
 | 
				
			||||||
 | 
					    sound.seek(0)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										35
									
								
								src/game/utilities/Timer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/game/utilities/Timer.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					import { EventEmitter } from 'pixi.js'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class Timer extends EventEmitter {
 | 
				
			||||||
 | 
					  private intervalHandle?: any
 | 
				
			||||||
 | 
					  private countdown: number = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(private seconds: number) {
 | 
				
			||||||
 | 
					    super()
 | 
				
			||||||
 | 
					    this.countdown = seconds
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  start() {
 | 
				
			||||||
 | 
					    clearInterval(this.intervalHandle)
 | 
				
			||||||
 | 
					    this.intervalHandle = setInterval(() => {
 | 
				
			||||||
 | 
					      this.countdown--
 | 
				
			||||||
 | 
					      if (this.countdown === 0) {
 | 
				
			||||||
 | 
					        clearInterval(this.intervalHandle)
 | 
				
			||||||
 | 
					        this.emit('timeout')
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        this.emit('tick', this.countdown)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }, 1000)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  stop() {
 | 
				
			||||||
 | 
					    clearInterval(this.intervalHandle)
 | 
				
			||||||
 | 
					    this.emit('stop', this.countdown)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  reset() {
 | 
				
			||||||
 | 
					    this.stop()
 | 
				
			||||||
 | 
					    this.countdown = this.seconds
 | 
				
			||||||
 | 
					    this.emit('reset', this.countdown)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -33,11 +33,13 @@ import bg_red from '@/assets/images/backgrounds/bg-red.png'
 | 
				
			|||||||
import bg_yellow from '@/assets/images/backgrounds/bg-yellow.png'
 | 
					import bg_yellow from '@/assets/images/backgrounds/bg-yellow.png'
 | 
				
			||||||
import bg_blue from '@/assets/images/backgrounds/bg-blue.png'
 | 
					import bg_blue from '@/assets/images/backgrounds/bg-blue.png'
 | 
				
			||||||
import bg_gray from '@/assets/images/backgrounds/bg-1.png'
 | 
					import bg_gray from '@/assets/images/backgrounds/bg-1.png'
 | 
				
			||||||
import snd_move_1 from '@/assets/sounds/move-1.mp3'
 | 
					// import snd_move_1 from '@/assets/sounds/move-1.mp3'
 | 
				
			||||||
import snd_move_2 from '@/assets/sounds/move-2.mp3'
 | 
					// import snd_move_2 from '@/assets/sounds/move-2.mp3'
 | 
				
			||||||
import snd_move_3 from '@/assets/sounds/move-3.mp3'
 | 
					// import snd_move_3 from '@/assets/sounds/move-3.mp3'
 | 
				
			||||||
import snd_move_4 from '@/assets/sounds/move-4.mp3'
 | 
					// import snd_move_4 from '@/assets/sounds/move-4.mp3'
 | 
				
			||||||
import snd_intro from '@/assets/sounds/intro.mp3'
 | 
					// import snd_intro from '@/assets/sounds/intro.mp3'
 | 
				
			||||||
 | 
					// import snd_click_btn from '@/assets/sounds/button_click.wav'
 | 
				
			||||||
 | 
					// import snd_ding_1 from '@/assets/sounds/ding-1.wav'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const assets = [
 | 
					export const assets = [
 | 
				
			||||||
  { alias: 'tile-back', src: tileBack },
 | 
					  { alias: 'tile-back', src: tileBack },
 | 
				
			||||||
@@ -75,9 +77,11 @@ export const assets = [
 | 
				
			|||||||
  { alias: 'bg-yellow', src: bg_yellow },
 | 
					  { alias: 'bg-yellow', src: bg_yellow },
 | 
				
			||||||
  { alias: 'bg-blue', src: bg_blue },
 | 
					  { alias: 'bg-blue', src: bg_blue },
 | 
				
			||||||
  { alias: 'bg-gray', src: bg_gray },
 | 
					  { alias: 'bg-gray', src: bg_gray },
 | 
				
			||||||
  { alias: 'snd-move-1', src: snd_move_1 },
 | 
					  // { alias: 'snd-move-1', src: snd_move_1 },
 | 
				
			||||||
  { alias: 'snd-move-2', src: snd_move_2 },
 | 
					  // { alias: 'snd-move-2', src: snd_move_2 },
 | 
				
			||||||
  { alias: 'snd-move-3', src: snd_move_3 },
 | 
					  // { alias: 'snd-move-3', src: snd_move_3 },
 | 
				
			||||||
  { alias: 'snd-move-4', src: snd_move_4 },
 | 
					  // { alias: 'snd-move-4', src: snd_move_4 },
 | 
				
			||||||
  { alias: 'snd-intro', src: snd_intro },
 | 
					  // { alias: 'snd-intro', src: snd_intro },
 | 
				
			||||||
 | 
					  // { alias: 'snd-click-btn', src: snd_click_btn },
 | 
				
			||||||
 | 
					  // { alias: 'snd-ding-1', src: snd_ding_1 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -103,6 +103,14 @@ export const whiteStyle = (
 | 
				
			|||||||
    dropShadow,
 | 
					    dropShadow,
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const timerStyle = () =>
 | 
				
			||||||
 | 
					  getStyle({
 | 
				
			||||||
 | 
					    fill: 0xffff66,
 | 
				
			||||||
 | 
					    fontSize: 36,
 | 
				
			||||||
 | 
					    // fontWeight: 'bold',
 | 
				
			||||||
 | 
					    stroke: { color: '#000000', width: 6, join: 'round' },
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const yellowStyle = (
 | 
					export const yellowStyle = (
 | 
				
			||||||
  fontSize: number = 15,
 | 
					  fontSize: number = 15,
 | 
				
			||||||
  fontWeight: TextStyleFontWeight = 'normal',
 | 
					  fontWeight: TextStyleFontWeight = 'normal',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,6 +14,14 @@ import { AuthenticationService } from './services/AuthenticationService'
 | 
				
			|||||||
import { GameService } from './services/GameService'
 | 
					import { GameService } from './services/GameService'
 | 
				
			||||||
import { PersistenceService } from './services/PersistenceService'
 | 
					import { PersistenceService } from './services/PersistenceService'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as PIXI from 'pixi.js'
 | 
				
			||||||
 | 
					import { gsap } from 'gsap'
 | 
				
			||||||
 | 
					import { PixiPlugin } from 'gsap/PixiPlugin'
 | 
				
			||||||
 | 
					import { InfoService } from './services/InfoService'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					gsap.registerPlugin(PixiPlugin)
 | 
				
			||||||
 | 
					PixiPlugin.registerPIXI(PIXI)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const app = createApp(App)
 | 
					const app = createApp(App)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
app.use(createPinia())
 | 
					app.use(createPinia())
 | 
				
			||||||
@@ -24,6 +32,7 @@ app.provide('socket', new SocketIoClientService(import.meta.env.VITE_SOCKET_URL)
 | 
				
			|||||||
app.provide('logger', new LoggingService())
 | 
					app.provide('logger', new LoggingService())
 | 
				
			||||||
app.provide('auth', new AuthenticationService())
 | 
					app.provide('auth', new AuthenticationService())
 | 
				
			||||||
app.provide('game', new GameService())
 | 
					app.provide('game', new GameService())
 | 
				
			||||||
 | 
					app.provide('info', new InfoService())
 | 
				
			||||||
PersistenceService.getInstance()
 | 
					PersistenceService.getInstance()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
app.mount('#app')
 | 
					app.mount('#app')
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,80 +3,98 @@ import AuthenticatedLayout from '@/components/layouts/AuthenticatedLayout.vue'
 | 
				
			|||||||
import UnauthenticatedLayout from '@/components/layouts/UnauthenticatedLayout.vue'
 | 
					import UnauthenticatedLayout from '@/components/layouts/UnauthenticatedLayout.vue'
 | 
				
			||||||
import HomeView from '@/views/HomeView.vue'
 | 
					import HomeView from '@/views/HomeView.vue'
 | 
				
			||||||
import LandingView from '@/views/LandingView.vue'
 | 
					import LandingView from '@/views/LandingView.vue'
 | 
				
			||||||
import { PersistenceService } from '@/services/PersistenceService'
 | 
					 | 
				
			||||||
import { useAuthStore } from '@/stores/auth'
 | 
					import { useAuthStore } from '@/stores/auth'
 | 
				
			||||||
import { storeToRefs } from 'pinia'
 | 
					import { storeToRefs } from 'pinia'
 | 
				
			||||||
 | 
					import TestView from '@/views/TestView.vue'
 | 
				
			||||||
 | 
					import { InfoService } from '@/services/InfoService'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const info = new InfoService()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const routes = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    path: '/',
 | 
				
			||||||
 | 
					    component: UnauthenticatedLayout,
 | 
				
			||||||
 | 
					    children: [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        path: '',
 | 
				
			||||||
 | 
					        component: LandingView,
 | 
				
			||||||
 | 
					        name: 'landing',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    props: { navbar: true },
 | 
				
			||||||
 | 
					    path: '/home',
 | 
				
			||||||
 | 
					    component: AuthenticatedLayout,
 | 
				
			||||||
 | 
					    children: [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        path: '',
 | 
				
			||||||
 | 
					        name: 'home',
 | 
				
			||||||
 | 
					        component: HomeView,
 | 
				
			||||||
 | 
					        meta: {
 | 
				
			||||||
 | 
					          requiresAuth: true,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    // route level code-splitting
 | 
				
			||||||
 | 
					    // this generates a separate chunk (About.[hash].js) for this route
 | 
				
			||||||
 | 
					    // which is lazy-loaded when the route is visited.
 | 
				
			||||||
 | 
					    // component: () => import('../views/AboutView.vue')
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    props: { navbar: true },
 | 
				
			||||||
 | 
					    path: '/match/:id',
 | 
				
			||||||
 | 
					    component: AuthenticatedLayout,
 | 
				
			||||||
 | 
					    children: [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        path: '',
 | 
				
			||||||
 | 
					        name: 'match',
 | 
				
			||||||
 | 
					        component: () => import('@/views/MatchView.vue'),
 | 
				
			||||||
 | 
					        meta: {
 | 
				
			||||||
 | 
					          requiresAuth: true,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    props: { navbar: false },
 | 
				
			||||||
 | 
					    path: '/game/:id',
 | 
				
			||||||
 | 
					    component: AuthenticatedLayout,
 | 
				
			||||||
 | 
					    children: [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        path: '',
 | 
				
			||||||
 | 
					        name: 'game',
 | 
				
			||||||
 | 
					        component: () => import('@/views/GameView.vue'),
 | 
				
			||||||
 | 
					        meta: {
 | 
				
			||||||
 | 
					          requiresAuth: true,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if (info.development) {
 | 
				
			||||||
 | 
					  routes.push({
 | 
				
			||||||
 | 
					    path: '/test',
 | 
				
			||||||
 | 
					    component: UnauthenticatedLayout,
 | 
				
			||||||
 | 
					    children: [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        path: '',
 | 
				
			||||||
 | 
					        component: TestView,
 | 
				
			||||||
 | 
					        name: 'test',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const router = createRouter({
 | 
					const router = createRouter({
 | 
				
			||||||
  history: createWebHistory(import.meta.env.BASE_URL),
 | 
					  history: createWebHistory(import.meta.env.BASE_URL),
 | 
				
			||||||
  routes: [
 | 
					  routes,
 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      path: '/',
 | 
					 | 
				
			||||||
      component: UnauthenticatedLayout,
 | 
					 | 
				
			||||||
      children: [
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          path: '',
 | 
					 | 
				
			||||||
          component: LandingView,
 | 
					 | 
				
			||||||
          name: 'landing',
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      props: { navbar: true },
 | 
					 | 
				
			||||||
      path: '/home',
 | 
					 | 
				
			||||||
      component: AuthenticatedLayout,
 | 
					 | 
				
			||||||
      children: [
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          path: '',
 | 
					 | 
				
			||||||
          name: 'home',
 | 
					 | 
				
			||||||
          component: HomeView,
 | 
					 | 
				
			||||||
          meta: {
 | 
					 | 
				
			||||||
            requiresAuth: true,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      // route level code-splitting
 | 
					 | 
				
			||||||
      // this generates a separate chunk (About.[hash].js) for this route
 | 
					 | 
				
			||||||
      // which is lazy-loaded when the route is visited.
 | 
					 | 
				
			||||||
      // component: () => import('../views/AboutView.vue')
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      props: { navbar: true },
 | 
					 | 
				
			||||||
      path: '/match/:id',
 | 
					 | 
				
			||||||
      component: AuthenticatedLayout,
 | 
					 | 
				
			||||||
      children: [
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          path: '',
 | 
					 | 
				
			||||||
          name: 'match',
 | 
					 | 
				
			||||||
          component: () => import('@/views/MatchView.vue'),
 | 
					 | 
				
			||||||
          meta: {
 | 
					 | 
				
			||||||
            requiresAuth: true,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      props: { navbar: false },
 | 
					 | 
				
			||||||
      path: '/game/:id',
 | 
					 | 
				
			||||||
      component: AuthenticatedLayout,
 | 
					 | 
				
			||||||
      children: [
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          path: '',
 | 
					 | 
				
			||||||
          name: 'game',
 | 
					 | 
				
			||||||
          component: () => import('@/views/GameView.vue'),
 | 
					 | 
				
			||||||
          meta: {
 | 
					 | 
				
			||||||
            requiresAuth: true,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  ],
 | 
					 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router.beforeEach((to, from, next) => {
 | 
					router.beforeEach((to, from, next) => {
 | 
				
			||||||
  const auth = useAuthStore()
 | 
					  const auth = useAuthStore()
 | 
				
			||||||
  const { user } = storeToRefs(auth)
 | 
					  const { user } = storeToRefs(auth)
 | 
				
			||||||
  console.log('user.value :>> ', user.value)
 | 
					 | 
				
			||||||
  const isLoggedIn = user.value === undefined ? false : true
 | 
					  const isLoggedIn = user.value === undefined ? false : true
 | 
				
			||||||
  if (to.name === 'landing' && isLoggedIn) {
 | 
					  if (to.name === 'landing' && isLoggedIn) {
 | 
				
			||||||
    next({ name: 'home' })
 | 
					    next({ name: 'home' })
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,7 +22,8 @@ export class AuthenticationService extends ServiceBase {
 | 
				
			|||||||
      body: { username, password },
 | 
					      body: { username, password },
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    const { token, refreshToken } = response
 | 
					    const { token, refreshToken } = response
 | 
				
			||||||
    this.persist(token, refreshToken)
 | 
					    this.persist(token)
 | 
				
			||||||
 | 
					    this.persistRefreshToken(refreshToken)
 | 
				
			||||||
    return token
 | 
					    return token
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -30,18 +31,17 @@ export class AuthenticationService extends ServiceBase {
 | 
				
			|||||||
    this.removePersistence()
 | 
					    this.removePersistence()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private removePersistence() {
 | 
					  removePersistence() {
 | 
				
			||||||
    const { clearUser, clearToken, clearRefreshToken } = this.auth
 | 
					    const { clearUser, clearToken, clearRefreshToken } = this.auth
 | 
				
			||||||
    clearToken()
 | 
					    clearToken()
 | 
				
			||||||
    clearUser()
 | 
					    clearUser()
 | 
				
			||||||
    clearRefreshToken()
 | 
					    clearRefreshToken()
 | 
				
			||||||
    this.persistanceService.saveToken('')
 | 
					    this.persistanceService.clearTokens()
 | 
				
			||||||
    this.persistanceService.saveRefreshToken('')
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async persist(jwt: string, refreshJwt?: string) {
 | 
					  async persist(jwt: string) {
 | 
				
			||||||
    const { setToken, setUser, setRefreshToken } = this.auth
 | 
					    const { setToken, setUser } = this.auth
 | 
				
			||||||
    const loggedUser = this.parseJwt(jwt)
 | 
					    const { user: loggedUser } = this.parseJwt(jwt)
 | 
				
			||||||
    setToken(jwt)
 | 
					    setToken(jwt)
 | 
				
			||||||
    setUser(loggedUser)
 | 
					    setUser(loggedUser)
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
@@ -49,13 +49,15 @@ export class AuthenticationService extends ServiceBase {
 | 
				
			|||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      this.logger.error(error, 'Error saving token')
 | 
					      this.logger.error(error, 'Error saving token')
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (refreshJwt) {
 | 
					  }
 | 
				
			||||||
      setRefreshToken(refreshJwt)
 | 
					
 | 
				
			||||||
      try {
 | 
					  persistRefreshToken(jwt: string) {
 | 
				
			||||||
        await this.persistanceService.saveRefreshToken(refreshJwt)
 | 
					    const { setRefreshToken } = this.auth
 | 
				
			||||||
      } catch (error) {
 | 
					    setRefreshToken(jwt)
 | 
				
			||||||
        this.logger.error(error, 'Error saving refresh token')
 | 
					    try {
 | 
				
			||||||
      }
 | 
					      this.persistanceService.saveRefreshToken(jwt)
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      this.logger.error(error, 'Error saving refresh token')
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -71,19 +73,20 @@ export class AuthenticationService extends ServiceBase {
 | 
				
			|||||||
  async fromStorage() {
 | 
					  async fromStorage() {
 | 
				
			||||||
    console.log('fromStorage')
 | 
					    console.log('fromStorage')
 | 
				
			||||||
    const auth = useAuthStore()
 | 
					    const auth = useAuthStore()
 | 
				
			||||||
    const { setToken, setUser } = auth
 | 
					    const { setToken, setRefreshToken, setUser } = auth
 | 
				
			||||||
    const token = await this.persistanceService.readToken()
 | 
					    const token = await this.persistanceService.readToken()
 | 
				
			||||||
    const refreshToken = await this.persistanceService.readRefreshToken()
 | 
					    const refreshToken = await this.persistanceService.readRefreshToken()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (token && refreshToken) {
 | 
					    if (token && refreshToken) {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const parsed = this.parseJwt(token)
 | 
					        const { user: parsed } = this.parseJwt(token)
 | 
				
			||||||
        const isAfter = dayjs().isAfter(parsed.exp * 1000)
 | 
					        const isAfter = dayjs().isAfter(parsed.exp * 1000)
 | 
				
			||||||
        if (isAfter) {
 | 
					        if (isAfter) {
 | 
				
			||||||
          this.removePersistence()
 | 
					          this.removePersistence()
 | 
				
			||||||
          return
 | 
					          return
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        setToken(token)
 | 
					        setToken(token)
 | 
				
			||||||
 | 
					        setRefreshToken(refreshToken)
 | 
				
			||||||
        setUser(parsed)
 | 
					        setUser(parsed)
 | 
				
			||||||
        this.logger.debug('Token loaded from storage', parsed)
 | 
					        this.logger.debug('Token loaded from storage', parsed)
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										8
									
								
								src/services/InfoService.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/services/InfoService.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					export class InfoService {
 | 
				
			||||||
 | 
					  mode: string = import.meta.env.MODE
 | 
				
			||||||
 | 
					  development: boolean = import.meta.env.DEV
 | 
				
			||||||
 | 
					  production: boolean = import.meta.env.PROD
 | 
				
			||||||
 | 
					  tauri: boolean = window.__TAURI_METADATA__ ? true : false
 | 
				
			||||||
 | 
					  ssr: boolean = import.meta.env.SSR
 | 
				
			||||||
 | 
					  baseUrl: string = import.meta.env.BASE_URL
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -27,4 +27,12 @@ export class LocalStorageService implements StorageInterface {
 | 
				
			|||||||
    const content: string | null = localStorage.getItem('net.xintanalabs.domino.config')
 | 
					    const content: string | null = localStorage.getItem('net.xintanalabs.domino.config')
 | 
				
			||||||
    return JSON.parse(content || '{}')
 | 
					    return JSON.parse(content || '{}')
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async deleteUserDataText(fileName: string) {
 | 
				
			||||||
 | 
					    localStorage.removeItem(`net.xintanalabs.domino.${fileName}`)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async deleteUserDataJson(fileName: string) {
 | 
				
			||||||
 | 
					    localStorage.removeItem(`net.xintanalabs.domino.${fileName}`)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
import { useAuthStore } from '@/stores/auth'
 | 
					import { useAuthStore } from '@/stores/auth'
 | 
				
			||||||
import { storeToRefs } from 'pinia'
 | 
					import { storeToRefs } from 'pinia'
 | 
				
			||||||
import type { AuthenticationService } from './AuthenticationService'
 | 
					import { PersistenceService } from './PersistenceService'
 | 
				
			||||||
 | 
					import { SessionExpiredError } from '@/common/errors/SessionExpiredError'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface RequestOptions {
 | 
					interface RequestOptions {
 | 
				
			||||||
  uri: string
 | 
					  uri: string
 | 
				
			||||||
@@ -13,6 +14,7 @@ interface RequestOptions {
 | 
				
			|||||||
export class NetworkService {
 | 
					export class NetworkService {
 | 
				
			||||||
  private API_URL = import.meta.env.VITE_API_URL
 | 
					  private API_URL = import.meta.env.VITE_API_URL
 | 
				
			||||||
  private auth = useAuthStore()
 | 
					  private auth = useAuthStore()
 | 
				
			||||||
 | 
					  private persistanceService: PersistenceService = PersistenceService.getInstance()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async post(options: RequestOptions) {
 | 
					  async post(options: RequestOptions) {
 | 
				
			||||||
    options.method = 'POST'
 | 
					    options.method = 'POST'
 | 
				
			||||||
@@ -40,7 +42,7 @@ export class NetworkService {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async request(options: RequestOptions) {
 | 
					  async request(options: RequestOptions) {
 | 
				
			||||||
    const { uri, params } = options
 | 
					    const { uri, params, auth } = options
 | 
				
			||||||
    if (!uri) {
 | 
					    if (!uri) {
 | 
				
			||||||
      throw new Error('URL is required')
 | 
					      throw new Error('URL is required')
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -48,19 +50,20 @@ export class NetworkService {
 | 
				
			|||||||
    const urlParams = this.getURLParams(params)
 | 
					    const urlParams = this.getURLParams(params)
 | 
				
			||||||
    let response = await fetch(`${this.API_URL}${uri}${urlParams}`, fetchOptions)
 | 
					    let response = await fetch(`${this.API_URL}${uri}${urlParams}`, fetchOptions)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (response.status === 401) {
 | 
					    const text = await response.text()
 | 
				
			||||||
      const newAccessToken = await this.refresh()
 | 
					    if (!response.ok) {
 | 
				
			||||||
      if (newAccessToken) {
 | 
					      if (auth && response.status === 401) {
 | 
				
			||||||
        fetchOptions.headers.Authorization = newAccessToken
 | 
					        const newAccessToken = await this.refresh()
 | 
				
			||||||
        response = await fetch(`${this.API_URL}${uri}${urlParams}`, fetchOptions)
 | 
					        if (newAccessToken) {
 | 
				
			||||||
 | 
					          fetchOptions.headers.Authorization = newAccessToken
 | 
				
			||||||
 | 
					          this.persistanceService.saveToken(newAccessToken)
 | 
				
			||||||
 | 
					          response = await fetch(`${this.API_URL}${uri}${urlParams}`, fetchOptions)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        throw new Error('Network response was not ok')
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!response.ok) {
 | 
					 | 
				
			||||||
      throw new Error('Network response was not ok')
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    const text = await response.text()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (text === '') {
 | 
					    if (text === '') {
 | 
				
			||||||
      return
 | 
					      return
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
@@ -71,13 +74,22 @@ export class NetworkService {
 | 
				
			|||||||
  async refresh() {
 | 
					  async refresh() {
 | 
				
			||||||
    const { refreshToken } = storeToRefs(this.auth)
 | 
					    const { refreshToken } = storeToRefs(this.auth)
 | 
				
			||||||
    const { setToken } = this.auth
 | 
					    const { setToken } = this.auth
 | 
				
			||||||
    const response = await await this.post({
 | 
					    try {
 | 
				
			||||||
      uri: '/refresh',
 | 
					      const fetchResponse = await fetch(`${this.API_URL}/refresh`, {
 | 
				
			||||||
      body: { token: refreshToken.value },
 | 
					        method: 'POST',
 | 
				
			||||||
    })
 | 
					        headers: this.getHeaders({ auth: false }),
 | 
				
			||||||
    const { token } = response
 | 
					        body: JSON.stringify({ refreshToken: refreshToken.value }),
 | 
				
			||||||
    setToken(token)
 | 
					      })
 | 
				
			||||||
    return token
 | 
					      if (!fetchResponse.ok) {
 | 
				
			||||||
 | 
					        throw new SessionExpiredError()
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      const response = await fetchResponse.json()
 | 
				
			||||||
 | 
					      const { token } = response
 | 
				
			||||||
 | 
					      setToken(token)
 | 
				
			||||||
 | 
					      return token
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      throw new SessionExpiredError()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getURLParams(params: any) {
 | 
					  getURLParams(params: any) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,15 +1,16 @@
 | 
				
			|||||||
 | 
					import { InfoService } from './InfoService'
 | 
				
			||||||
import { LocalStorageService } from './LocalStorageService'
 | 
					import { LocalStorageService } from './LocalStorageService'
 | 
				
			||||||
import type { StorageInterface } from './StorageInterface'
 | 
					import type { StorageInterface } from './StorageInterface'
 | 
				
			||||||
import { TauriFileStorageService } from './TauriFileStorageService'
 | 
					import { TauriFileStorageService } from './TauriFileStorageService'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class PersistenceService {
 | 
					export class PersistenceService {
 | 
				
			||||||
 | 
					  private infoService: InfoService = new InfoService()
 | 
				
			||||||
  private static instance: PersistenceService
 | 
					  private static instance: PersistenceService
 | 
				
			||||||
  private isTauri: boolean = false
 | 
					 | 
				
			||||||
  private storage: StorageInterface
 | 
					  private storage: StorageInterface
 | 
				
			||||||
  private constructor() {
 | 
					  private constructor() {
 | 
				
			||||||
    this.isTauri = window.__TAURI_METADATA__ ? true : false
 | 
					    this.storage = this.infoService.tauri
 | 
				
			||||||
    this.storage = this.isTauri ? new TauriFileStorageService() : new LocalStorageService()
 | 
					      ? new TauriFileStorageService()
 | 
				
			||||||
    console.log('PersistenceService created', this.isTauri)
 | 
					      : new LocalStorageService()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static getInstance(): PersistenceService {
 | 
					  static getInstance(): PersistenceService {
 | 
				
			||||||
@@ -47,6 +48,11 @@ export class PersistenceService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async clearTokens() {
 | 
				
			||||||
 | 
					    await this.storage.deleteUserDataText('token')
 | 
				
			||||||
 | 
					    await this.storage.deleteUserDataText('refreshToken')
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async saveConfig(config: any) {
 | 
					  async saveConfig(config: any) {
 | 
				
			||||||
    await this.storage.saveConfigData(config)
 | 
					    await this.storage.saveConfigData(config)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -41,6 +41,14 @@ export class SocketIoClientService extends ServiceBase {
 | 
				
			|||||||
    })
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async joinRoom(room: string): Promise<void> {
 | 
				
			||||||
 | 
					    if (this.isConnected) {
 | 
				
			||||||
 | 
					      this.socket.emit('client:join-room', room)
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      this.logger.debug('SOCKET: Not connected to server')
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  addEvents(): void {
 | 
					  addEvents(): void {
 | 
				
			||||||
    this.socket.on('disconnect', () => {
 | 
					    this.socket.on('disconnect', () => {
 | 
				
			||||||
      this.isConnected = false
 | 
					      this.isConnected = false
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,4 +5,6 @@ export interface StorageInterface {
 | 
				
			|||||||
  readUserDataText(fileName: string): Promise<string>
 | 
					  readUserDataText(fileName: string): Promise<string>
 | 
				
			||||||
  readUserDataJson(fileName: string): Promise<any>
 | 
					  readUserDataJson(fileName: string): Promise<any>
 | 
				
			||||||
  readConfigData(): Promise<any>
 | 
					  readConfigData(): Promise<any>
 | 
				
			||||||
 | 
					  deleteUserDataText(fileName: string): Promise<void>
 | 
				
			||||||
 | 
					  deleteUserDataJson(fileName: string): Promise<void>
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,75 +1,70 @@
 | 
				
			|||||||
import { appConfigDir, appLocalDataDir, appCacheDir, appDataDir, join } from '@tauri-apps/api/path'
 | 
					import { appDataDir, join } from '@tauri-apps/api/path'
 | 
				
			||||||
import { writeTextFile, readTextFile, exists, createDir } from '@tauri-apps/api/fs'
 | 
					import { writeTextFile, readTextFile, exists, createDir, removeFile } from '@tauri-apps/api/fs'
 | 
				
			||||||
import type { StorageInterface } from './StorageInterface'
 | 
					import type { StorageInterface } from './StorageInterface'
 | 
				
			||||||
import { ServiceBase } from './ServiceBase'
 | 
					import { ServiceBase } from './ServiceBase'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class TauriFileStorageService extends ServiceBase implements StorageInterface {
 | 
					export class TauriFileStorageService extends ServiceBase implements StorageInterface {
 | 
				
			||||||
  constructor() {
 | 
					  constructor() {
 | 
				
			||||||
    super()
 | 
					    super()
 | 
				
			||||||
    this.showDirs()
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async showDirs() {
 | 
					  private async getFilePath(filename: string, dir: string = 'data'): Promise<string> {
 | 
				
			||||||
    this.logger.debug(`=> appConfigDir ${await appConfigDir()}`)
 | 
					    const userAppDir = await appDataDir()
 | 
				
			||||||
    this.logger.debug(`=> appLocalDataDir ${await appLocalDataDir()}`)
 | 
					    await this.ensureDirExists(await join(userAppDir, dir))
 | 
				
			||||||
    this.logger.debug(`=> appCacheDir ${await appCacheDir()}`)
 | 
					    return await join(userAppDir, dir, filename)
 | 
				
			||||||
    this.logger.debug(`=> appDataDir ${await appDataDir()}`)
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async saveUserDataText(fileName: string, content: any) {
 | 
					  async saveUserDataText(fileName: string, content: any) {
 | 
				
			||||||
    const userAppDir = await appDataDir()
 | 
					    const filePath = await this.getFilePath(fileName + '.txt')
 | 
				
			||||||
    await this.ensureDirExists(userAppDir)
 | 
					 | 
				
			||||||
    const filePath = await join(userAppDir, fileName + '.txt')
 | 
					 | 
				
			||||||
    await this.write(filePath, content, false)
 | 
					    await this.write(filePath, content, false)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async saveUserDataJson(fileName: string, content: any) {
 | 
					  async saveUserDataJson(fileName: string, content: any) {
 | 
				
			||||||
    const userAppDir = await appDataDir()
 | 
					    const filePath = await this.getFilePath(fileName + '.json')
 | 
				
			||||||
    await this.ensureDirExists(userAppDir)
 | 
					 | 
				
			||||||
    const filePath = await join(userAppDir, fileName + '.json')
 | 
					 | 
				
			||||||
    await this.write(filePath, content, true)
 | 
					    await this.write(filePath, content, true)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async readUserDataText(fileName: string) {
 | 
					  async readUserDataText(fileName: string) {
 | 
				
			||||||
    const userAppDir = await appDataDir()
 | 
					    const filePath = await this.getFilePath(fileName + '.txt')
 | 
				
			||||||
    await this.ensureDirExists(userAppDir)
 | 
					 | 
				
			||||||
    const filePath = await join(userAppDir, fileName + '.txt')
 | 
					 | 
				
			||||||
    const content = await this.read(filePath, false)
 | 
					    const content = await this.read(filePath, false)
 | 
				
			||||||
    return content
 | 
					    return content
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async readUserDataJson(fileName: string) {
 | 
					  async readUserDataJson(fileName: string) {
 | 
				
			||||||
    const userAppDir = await appDataDir()
 | 
					    const filePath = await this.getFilePath(fileName + '.json')
 | 
				
			||||||
    await this.ensureDirExists(userAppDir)
 | 
					 | 
				
			||||||
    const filePath = await join(userAppDir, fileName + '.json')
 | 
					 | 
				
			||||||
    const content = await this.read(filePath, true)
 | 
					    const content = await this.read(filePath, true)
 | 
				
			||||||
    return content
 | 
					    return content
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async saveConfigData(content: any) {
 | 
					  async saveConfigData(content: any) {
 | 
				
			||||||
    const userAppDir = await appConfigDir()
 | 
					    const filePath = await this.getFilePath('config.json')
 | 
				
			||||||
    await this.ensureDirExists(userAppDir)
 | 
					 | 
				
			||||||
    const filePath = await join(userAppDir, 'config.json')
 | 
					 | 
				
			||||||
    await this.write(filePath, content)
 | 
					    await this.write(filePath, content)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async readConfigData() {
 | 
					  async readConfigData() {
 | 
				
			||||||
    const userAppDir = await appConfigDir()
 | 
					    const filePath = await this.getFilePath('config.json')
 | 
				
			||||||
    await this.ensureDirExists(userAppDir)
 | 
					 | 
				
			||||||
    const filePath = await join(userAppDir, 'config.json')
 | 
					 | 
				
			||||||
    const content = await this.read(filePath, true)
 | 
					    const content = await this.read(filePath, true)
 | 
				
			||||||
    return content
 | 
					    return content
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async deleteUserDataText(fileName: string) {
 | 
				
			||||||
 | 
					    const filePath = await this.getFilePath(fileName + '.txt')
 | 
				
			||||||
 | 
					    await removeFile(filePath)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async deleteUserDataJson(fileName: string) {
 | 
				
			||||||
 | 
					    const filePath = await this.getFilePath(fileName + '.json')
 | 
				
			||||||
 | 
					    await removeFile(filePath)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async ensureDirExists(dir: string) {
 | 
					  private async ensureDirExists(dir: string) {
 | 
				
			||||||
    const dirExists = await exists(dir)
 | 
					    const dirExists = await exists(dir)
 | 
				
			||||||
    if (!dirExists) {
 | 
					    if (!dirExists) {
 | 
				
			||||||
      await createDir(dir)
 | 
					      await createDir(dir, { recursive: true })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async write(filePath: string, content: any, json: boolean = false) {
 | 
					  private async write(filePath: string, content: any, json: boolean = false) {
 | 
				
			||||||
    this.logger.trace(`write ${filePath}`, content)
 | 
					 | 
				
			||||||
    if (json) {
 | 
					    if (json) {
 | 
				
			||||||
      return writeTextFile(filePath, JSON.stringify(content, null, 2))
 | 
					      return writeTextFile(filePath, JSON.stringify(content, null, 2))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ export const useAuthStore = defineStore('auth', () => {
 | 
				
			|||||||
  const user = ref<any | undefined>(undefined)
 | 
					  const user = ref<any | undefined>(undefined)
 | 
				
			||||||
  const roles = ref<string[]>([])
 | 
					  const roles = ref<string[]>([])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const isLoggedIn = computed(() => jwt.value !== undefined)
 | 
					  const isLoggedIn = computed(() => jwt.value !== undefined && user.value !== undefined)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const token = computed(() => {
 | 
					  const token = computed(() => {
 | 
				
			||||||
    if (jwt.value) {
 | 
					    if (jwt.value) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,11 +7,12 @@ import type { LoggingService } from '@/services/LoggingService'
 | 
				
			|||||||
import type { GameService } from '@/services/GameService'
 | 
					import type { GameService } from '@/services/GameService'
 | 
				
			||||||
import type { MatchSessionOptions, MatchSessionDto } from '@/common/interfaces'
 | 
					import type { MatchSessionOptions, MatchSessionDto } from '@/common/interfaces'
 | 
				
			||||||
import { useEventBusStore } from '@/stores/eventBus'
 | 
					import { useEventBusStore } from '@/stores/eventBus'
 | 
				
			||||||
import { useAuthStore } from '@/stores/auth'
 | 
					 | 
				
			||||||
import { copyToclipboard, wait } from '@/common/helpers'
 | 
					import { copyToclipboard, wait } from '@/common/helpers'
 | 
				
			||||||
import { useGameOptionsStore } from '@/stores/gameOptions'
 | 
					import { useGameOptionsStore } from '@/stores/gameOptions'
 | 
				
			||||||
import MatchConfiguration from '@/components/MatchConfiguration.vue'
 | 
					import MatchConfiguration from '@/components/MatchConfiguration.vue'
 | 
				
			||||||
import { useI18n } from 'vue-i18n'
 | 
					import { useI18n } from 'vue-i18n'
 | 
				
			||||||
 | 
					import { SessionExpiredError } from '@/common/errors/SessionExpiredError'
 | 
				
			||||||
 | 
					import type { AuthenticationService } from '@/services/AuthenticationService'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let teamedWith = ref<string | undefined>(undefined)
 | 
					let teamedWith = ref<string | undefined>(undefined)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -21,16 +22,17 @@ let loadingSessions = ref<boolean>(false)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const router = useRouter()
 | 
					const router = useRouter()
 | 
				
			||||||
const gameStore = useGameStore()
 | 
					const gameStore = useGameStore()
 | 
				
			||||||
const auth = useAuthStore()
 | 
					 | 
				
			||||||
const gameOptionsStore = useGameOptionsStore()
 | 
					const gameOptionsStore = useGameOptionsStore()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const socketService: any = inject('socket')
 | 
					const socketService: any = inject('socket')
 | 
				
			||||||
const gameService: GameService = inject<GameService>('game') as GameService
 | 
					const gameService: GameService = inject<GameService>('game') as GameService
 | 
				
			||||||
const logger: LoggingService = inject<LoggingService>('logger') as LoggingService
 | 
					const logger: LoggingService = inject<LoggingService>('logger') as LoggingService
 | 
				
			||||||
 | 
					const authService: AuthenticationService = inject<AuthenticationService>(
 | 
				
			||||||
 | 
					  'auth',
 | 
				
			||||||
 | 
					) as AuthenticationService
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const { sessionState, isSessionStarted, playerState, amIHost, readyForStart } =
 | 
					const { sessionState, isSessionStarted, playerState, amIHost, readyForStart } =
 | 
				
			||||||
  storeToRefs(gameStore)
 | 
					  storeToRefs(gameStore)
 | 
				
			||||||
const { user } = storeToRefs(auth)
 | 
					 | 
				
			||||||
const { gameOptions } = storeToRefs(gameOptionsStore)
 | 
					const { gameOptions } = storeToRefs(gameOptionsStore)
 | 
				
			||||||
const { updateSessionState, updatePlayerState, updateGameState } = gameStore
 | 
					const { updateSessionState, updatePlayerState, updateGameState } = gameStore
 | 
				
			||||||
const { t } = useI18n()
 | 
					const { t } = useI18n()
 | 
				
			||||||
@@ -44,7 +46,8 @@ async function createMatch(options: MatchSessionOptions) {
 | 
				
			|||||||
  logger.debug('Creating match')
 | 
					  logger.debug('Creating match')
 | 
				
			||||||
  await socketService.connect()
 | 
					  await socketService.connect()
 | 
				
			||||||
  gameOptions.value = options
 | 
					  gameOptions.value = options
 | 
				
			||||||
  await gameService.createMatchSession(options)
 | 
					  const sessionId = await gameService.createMatchSession(options)
 | 
				
			||||||
 | 
					  socketService.joinRoom(`room-${sessionId}`)
 | 
				
			||||||
  logger.debug('Match created successfully')
 | 
					  logger.debug('Match created successfully')
 | 
				
			||||||
  // router.push({ name: 'match', params: { id } })
 | 
					  // router.push({ name: 'match', params: { id } })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -101,7 +104,8 @@ eventBus.subscribe('server:match-starting', (data) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
async function joinMatch(id: string) {
 | 
					async function joinMatch(id: string) {
 | 
				
			||||||
  if (id) {
 | 
					  if (id) {
 | 
				
			||||||
    await socketService.connect()
 | 
					    await socketService.connect(id)
 | 
				
			||||||
 | 
					    socketService.joinRoom(`room-${id}`)
 | 
				
			||||||
    await gameService.joinMatchSession(id)
 | 
					    await gameService.joinMatchSession(id)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -133,14 +137,26 @@ const isMultiplayer = computed(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
async function loadData() {
 | 
					async function loadData() {
 | 
				
			||||||
  loadingSessions.value = true
 | 
					  loadingSessions.value = true
 | 
				
			||||||
  const listResponse = await gameService.listMatchSessions()
 | 
					  let listResponse
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    listResponse = await gameService.listMatchSessions()
 | 
				
			||||||
 | 
					  } catch (error: any) {
 | 
				
			||||||
 | 
					    if (error instanceof SessionExpiredError) {
 | 
				
			||||||
 | 
					      authService.removePersistence()
 | 
				
			||||||
 | 
					      router.push({ name: 'landing' })
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      logger.error('Error loading match sessions', error)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  loadingSessions.value = false
 | 
					  loadingSessions.value = false
 | 
				
			||||||
  matchSessions.value = listResponse.data
 | 
					  matchSessions.value = listResponse.data
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
onMounted(() => {
 | 
					onMounted(() => {
 | 
				
			||||||
  loadData()
 | 
					  loadData()
 | 
				
			||||||
  dataInterval = setInterval(loadData, 5000)
 | 
					  dataInterval = setInterval(loadData, 10000)
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
onUnmounted(() => {
 | 
					onUnmounted(() => {
 | 
				
			||||||
@@ -183,10 +199,10 @@ async function onStartSingleMatch(options: MatchSessionOptions) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="container home">
 | 
					  <div class="container home">
 | 
				
			||||||
    <section class="section">
 | 
					    <section class="mt-4">
 | 
				
			||||||
      <h1 class="title is-2">
 | 
					      <!-- <h1 class="title is-2">
 | 
				
			||||||
        {{ $t('welcome-to-the-user-username-s-home-page', [user.username]) }}
 | 
					        {{ $t('welcome-to-the-user-username-s-home-page', [user.username]) }}
 | 
				
			||||||
      </h1>
 | 
					      </h1> -->
 | 
				
			||||||
      <!-- Tabs -->
 | 
					      <!-- Tabs -->
 | 
				
			||||||
      <div class="block" v-if="!isSessionStarted">
 | 
					      <div class="block" v-if="!isSessionStarted">
 | 
				
			||||||
        <div class="tabs is-centered">
 | 
					        <div class="tabs is-centered">
 | 
				
			||||||
@@ -202,7 +218,7 @@ async function onStartSingleMatch(options: MatchSessionOptions) {
 | 
				
			|||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <!-- Tabs End -->
 | 
					        <!-- Tabs End -->
 | 
				
			||||||
        <!-- Match Configuration -->
 | 
					        <!-- Match Configuration -->
 | 
				
			||||||
        <section class="section" v-if="isCreateTab">
 | 
					        <section class="section pt-5" v-if="isCreateTab">
 | 
				
			||||||
          <MatchConfiguration
 | 
					          <MatchConfiguration
 | 
				
			||||||
            @create-match="onCreateMatch"
 | 
					            @create-match="onCreateMatch"
 | 
				
			||||||
            @start-single-match="onStartSingleMatch"
 | 
					            @start-single-match="onStartSingleMatch"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,14 +3,12 @@ import { AuthenticationService } from '@/services/AuthenticationService'
 | 
				
			|||||||
import { inject, ref } from 'vue'
 | 
					import { inject, ref } from 'vue'
 | 
				
			||||||
import { useRouter } from 'vue-router'
 | 
					import { useRouter } from 'vue-router'
 | 
				
			||||||
import { useI18n } from 'vue-i18n'
 | 
					import { useI18n } from 'vue-i18n'
 | 
				
			||||||
import { emit, listen } from '@tauri-apps/api/event'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const router = useRouter()
 | 
					const router = useRouter()
 | 
				
			||||||
const username = ref('')
 | 
					const username = ref('')
 | 
				
			||||||
const password = ref('')
 | 
					const password = ref('')
 | 
				
			||||||
const errorLogin = ref(false)
 | 
					const errorLogin = ref(false)
 | 
				
			||||||
const { t } = useI18n()
 | 
					const { t } = useI18n()
 | 
				
			||||||
const isTauri = window.__TAURI_METADATA__ ? true : false
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const authService = inject<AuthenticationService>('auth')
 | 
					const authService = inject<AuthenticationService>('auth')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -51,10 +49,6 @@ async function login() {
 | 
				
			|||||||
//   console.log('Update download finished.')
 | 
					//   console.log('Update download finished.')
 | 
				
			||||||
//   // You can notify the user to restart the app
 | 
					//   // You can notify the user to restart the app
 | 
				
			||||||
// })
 | 
					// })
 | 
				
			||||||
 | 
					 | 
				
			||||||
function checkForUpdates() {
 | 
					 | 
				
			||||||
  emit('tauri://update')
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
@@ -98,6 +92,5 @@ function checkForUpdates() {
 | 
				
			|||||||
        </div> -->
 | 
					        </div> -->
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </form>
 | 
					    </form>
 | 
				
			||||||
    <a href="#" @click="checkForUpdates" v-if="isTauri">Update</a>
 | 
					 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										81
									
								
								src/views/TestView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/views/TestView.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div ref="appEl" class="game-app"></div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { wait } from '@/common/helpers'
 | 
				
			||||||
 | 
					import type { MatchSessionOptions } from '@/common/interfaces'
 | 
				
			||||||
 | 
					import { dataGameStatePlayer, dataPlayer, players } from '@/common/testMocks'
 | 
				
			||||||
 | 
					import { Button } from '@/game/Button'
 | 
				
			||||||
 | 
					import { Game } from '@/game/Game'
 | 
				
			||||||
 | 
					import { TimedButton } from '@/game/TimedButton'
 | 
				
			||||||
 | 
					import { Point } from 'pixi.js'
 | 
				
			||||||
 | 
					import { onMounted, ref } from 'vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const minScreenWidth = 1280
 | 
				
			||||||
 | 
					const minScreenHeight = 720
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let screenWidth = window.innerWidth - 10
 | 
				
			||||||
 | 
					let screenHeight = window.innerHeight - 10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if (screenWidth < minScreenWidth) screenWidth = minScreenWidth
 | 
				
			||||||
 | 
					if (screenHeight < minScreenHeight) screenHeight = minScreenHeight
 | 
				
			||||||
 | 
					const minSide = Math.min(screenWidth, screenHeight)
 | 
				
			||||||
 | 
					const boardScale = minSide > 1440 ? 1 : minSide > 1080 ? 0.8 : minSide > 720 ? 0.7 : 0.5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let appEl = ref<HTMLElement | null>(null)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const defaultOptions: MatchSessionOptions = {
 | 
				
			||||||
 | 
					  background: 'green',
 | 
				
			||||||
 | 
					  teamed: false,
 | 
				
			||||||
 | 
					  winType: 'points',
 | 
				
			||||||
 | 
					  winTarget: 100,
 | 
				
			||||||
 | 
					  seed: '',
 | 
				
			||||||
 | 
					  sessionName: `Test #${Date.now()}`,
 | 
				
			||||||
 | 
					  numPlayers: 1,
 | 
				
			||||||
 | 
					  turnWaitSeconds: 30,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					const gameOptionsValue: MatchSessionOptions = {
 | 
				
			||||||
 | 
					  ...defaultOptions,
 | 
				
			||||||
 | 
					  ...{ screen: { width: screenWidth, height: screenHeight, boardScale: boardScale, handScale: 1 } },
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const game = new Game(gameOptionsValue, undefined, dataPlayer.id, 'test-session-id')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(async () => {
 | 
				
			||||||
 | 
					  if (appEl.value === null) return
 | 
				
			||||||
 | 
					  const canvas = await game.setup()
 | 
				
			||||||
 | 
					  appEl.value.appendChild(canvas)
 | 
				
			||||||
 | 
					  await game.preload()
 | 
				
			||||||
 | 
					  game.start(players)
 | 
				
			||||||
 | 
					  game.hand.update(dataPlayer)
 | 
				
			||||||
 | 
					  game.updateOtherHands(dataGameStatePlayer)
 | 
				
			||||||
 | 
					  await wait(1000)
 | 
				
			||||||
 | 
					  game.hand.setActive(true)
 | 
				
			||||||
 | 
					  game.hand.prepareForMove(true)
 | 
				
			||||||
 | 
					  const btn = new Button('TEST BUTTON', new Point(100, 100), { width: 130 }, () => {
 | 
				
			||||||
 | 
					    console.log('Button clicked')
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  btn.disabled = false
 | 
				
			||||||
 | 
					  btn.disabledColor = 0x666666
 | 
				
			||||||
 | 
					  game.stage.addChild(btn)
 | 
				
			||||||
 | 
					  const timedBtn = new TimedButton(
 | 
				
			||||||
 | 
					    'TIMED BUTTON',
 | 
				
			||||||
 | 
					    new Point(100, 400),
 | 
				
			||||||
 | 
					    { width: 150, height: 40 },
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					      console.log('Timed button clicked')
 | 
				
			||||||
 | 
					      timedBtn.disabled = true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    5,
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					  timedBtn.on('timeout', () => {
 | 
				
			||||||
 | 
					    console.log('Timed button timed out')
 | 
				
			||||||
 | 
					    timedBtn.disabled = true
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  game.stage.addChild(timedBtn)
 | 
				
			||||||
 | 
					  timedBtn.start()
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped></style>
 | 
				
			||||||
		Reference in New Issue
	
	Block a user