Compare commits
No commits in common. "b78003b9c08d2f1513b88a9daa66bea0d3d9e95a" and "6e559be867a42a2c47edc33d34461dae697fed5e" have entirely different histories.
b78003b9c0
...
6e559be867
174
package-lock.json
generated
174
package-lock.json
generated
@ -7,7 +7,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"ejs": "^3.1.10",
|
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"mysql2": "^3.14.1",
|
"mysql2": "^3.14.1",
|
||||||
@ -52,27 +51,6 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ansi-styles": {
|
|
||||||
"version": "4.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
|
||||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"color-convert": "^2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/async": {
|
|
||||||
"version": "3.2.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
|
||||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/aws-ssl-profiles": {
|
"node_modules/aws-ssl-profiles": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
|
||||||
@ -82,12 +60,6 @@
|
|||||||
"node": ">= 6.0.0"
|
"node": ">= 6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/balanced-match": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/base64id": {
|
"node_modules/base64id": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||||
@ -126,16 +98,6 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
|
||||||
"version": "1.1.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"balanced-match": "^1.0.0",
|
|
||||||
"concat-map": "0.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/buffer-equal-constant-time": {
|
"node_modules/buffer-equal-constant-time": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
@ -180,46 +142,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chalk": {
|
|
||||||
"version": "4.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
|
||||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-styles": "^4.1.0",
|
|
||||||
"supports-color": "^7.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/color-convert": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"color-name": "~1.1.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=7.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/color-name": {
|
|
||||||
"version": "1.1.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/concat-map": {
|
|
||||||
"version": "0.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
|
||||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/content-disposition": {
|
"node_modules/content-disposition": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
|
||||||
@ -347,21 +269,6 @@
|
|||||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/ejs": {
|
|
||||||
"version": "3.1.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
|
|
||||||
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"jake": "^10.8.5"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"ejs": "bin/cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/encodeurl": {
|
"node_modules/encodeurl": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
@ -547,36 +454,6 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/filelist": {
|
|
||||||
"version": "1.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
|
||||||
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"minimatch": "^5.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/filelist/node_modules/brace-expansion": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"balanced-match": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/filelist/node_modules/minimatch": {
|
|
||||||
"version": "5.1.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
|
||||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"brace-expansion": "^2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/finalhandler": {
|
"node_modules/finalhandler": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
|
||||||
@ -679,15 +556,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/has-flag": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/has-symbols": {
|
"node_modules/has-symbols": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
@ -767,24 +635,6 @@
|
|||||||
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
|
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/jake": {
|
|
||||||
"version": "10.9.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz",
|
|
||||||
"integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"async": "^3.2.3",
|
|
||||||
"chalk": "^4.0.2",
|
|
||||||
"filelist": "^1.0.4",
|
|
||||||
"minimatch": "^3.1.2"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"jake": "bin/cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/jsonwebtoken": {
|
"node_modules/jsonwebtoken": {
|
||||||
"version": "9.0.2",
|
"version": "9.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
||||||
@ -951,18 +801,6 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/minimatch": {
|
|
||||||
"version": "3.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
|
||||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"brace-expansion": "^1.1.7"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@ -1449,18 +1287,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/supports-color": {
|
|
||||||
"version": "7.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
|
||||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"has-flag": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/toidentifier": {
|
"node_modules/toidentifier": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"ejs": "^3.1.10",
|
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"mysql2": "^3.14.1",
|
"mysql2": "^3.14.1",
|
||||||
|
@ -220,11 +220,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div> <!-- Конец .game-wrapper -->
|
</div> <!-- Конец .game-wrapper -->
|
||||||
<script>
|
|
||||||
const base_path = "<%=base_path%>"
|
|
||||||
</script>
|
|
||||||
<!-- Библиотека Socket.IO клиента -->
|
<!-- Библиотека Socket.IO клиента -->
|
||||||
<script src="<%=base_path%>/socket.io/socket.io.js"></script>
|
<script src="/socket.io/socket.io.js"></script>
|
||||||
|
|
||||||
<!-- Ваш скрипт для UI, который может создавать глобальные объекты или функции -->
|
<!-- Ваш скрипт для UI, который может создавать глобальные объекты или функции -->
|
||||||
<!-- Он должен быть загружен до main.js, если main.js ожидает window.gameUI -->
|
<!-- Он должен быть загружен до main.js, если main.js ожидает window.gameUI -->
|
@ -6,11 +6,12 @@ export function initAuth(dependencies) {
|
|||||||
|
|
||||||
const { socket, clientState, ui } = dependencies;
|
const { socket, clientState, ui } = dependencies;
|
||||||
const { loginForm, registerForm, logoutButton } = ui.elements;
|
const { loginForm, registerForm, logoutButton } = ui.elements;
|
||||||
|
|
||||||
console.log('[Auth.js DOM Check] loginForm in initAuth:', loginForm); // <--- ДОБАВЛЕНО
|
console.log('[Auth.js DOM Check] loginForm in initAuth:', loginForm); // <--- ДОБАВЛЕНО
|
||||||
console.log('[Auth.js DOM Check] registerForm in initAuth:', registerForm); // <--- ДОБАВЛЕНО
|
console.log('[Auth.js DOM Check] registerForm in initAuth:', registerForm); // <--- ДОБАВЛЕНО
|
||||||
console.log('[Auth.js DOM Check] logoutButton in initAuth:', logoutButton); // <--- ДОБАВЛЕНО
|
console.log('[Auth.js DOM Check] logoutButton in initAuth:', logoutButton); // <--- ДОБАВЛЕНО
|
||||||
|
|
||||||
const getApiUrl = (path) => `${window.location.origin}${base_path}${path}`;
|
const getApiUrl = (path) => `${window.location.origin}${path}`;
|
||||||
console.log('[Auth.js] API URLs will be relative to:', window.location.origin); // <--- ДОБАВЛЕНО
|
console.log('[Auth.js] API URLs will be relative to:', window.location.origin); // <--- ДОБАВЛЕНО
|
||||||
|
|
||||||
const JWT_TOKEN_KEY = 'jwtToken';
|
const JWT_TOKEN_KEY = 'jwtToken';
|
||||||
|
@ -218,7 +218,7 @@ export function initGameplay(dependencies) {
|
|||||||
handleGameDataReceived(data, 'gameStarted');
|
handleGameDataReceived(data, 'gameStarted');
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('gameState', (data) => { // Это событие было добавлено для поддержки reconnect из старого GameInstance
|
socket.on('gameState', (data) => {
|
||||||
handleGameDataReceived(data, 'gameState (reconnect)');
|
handleGameDataReceived(data, 'gameState (reconnect)');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -246,10 +246,10 @@ export function initGameplay(dependencies) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[CLIENT ${username}] gameStateUpdate - Clearing game status message as game is active.`);
|
console.log(`[CLIENT ${username}] gameStateUpdate - Clearing game status message as game is active.`);
|
||||||
ui.setGameStatusMessage(""); // Очищаем статус, если игра активна
|
ui.setGameStatusMessage("");
|
||||||
|
|
||||||
} else if (clientState.currentGameState && clientState.currentGameState.isGameOver) {
|
} else if (clientState.currentGameState && clientState.currentGameState.isGameOver) {
|
||||||
disableGameControls(); // Отключаем управление, если игра закончилась этим обновлением
|
disableGameControls();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -270,35 +270,34 @@ export function initGameplay(dependencies) {
|
|||||||
|
|
||||||
socket.on('gameOver', (data) => {
|
socket.on('gameOver', (data) => {
|
||||||
if (!clientState.isLoggedIn || !clientState.currentGameId || !window.GAME_CONFIG) {
|
if (!clientState.isLoggedIn || !clientState.currentGameId || !window.GAME_CONFIG) {
|
||||||
// Если нет ID игры, но залогинен, возможно, стоит запросить состояние
|
|
||||||
if (!clientState.currentGameId && clientState.isLoggedIn) socket.emit('requestGameState');
|
if (!clientState.currentGameId && clientState.isLoggedIn) socket.emit('requestGameState');
|
||||||
else if (!clientState.isLoggedIn) ui.showAuthScreen(); // Если не залогинен, показать экран входа
|
else if (!clientState.isLoggedIn) ui.showAuthScreen();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const username = clientState.loggedInUsername || 'N/A';
|
const username = clientState.loggedInUsername || 'N/A';
|
||||||
console.log(`[CLIENT ${username}] Event: gameOver.`);
|
console.log(`[CLIENT ${username}] Event: gameOver.`);
|
||||||
|
|
||||||
const playerWon = data.winnerId === clientState.myPlayerId;
|
const playerWon = data.winnerId === clientState.myPlayerId;
|
||||||
clientState.currentGameState = data.finalGameState; // Обновляем состояние последним полученным
|
clientState.currentGameState = data.finalGameState;
|
||||||
clientState.isInGame = false; // Игра точно закончена
|
clientState.isInGame = false;
|
||||||
|
|
||||||
ui.updateGlobalWindowVariablesForUI(); // Обновляем глобальные переменные для ui.js
|
ui.updateGlobalWindowVariablesForUI();
|
||||||
|
|
||||||
if (window.gameUI?.updateUI) requestAnimationFrame(() => window.gameUI.updateUI()); // Обновляем UI один раз
|
if (window.gameUI?.updateUI) requestAnimationFrame(() => window.gameUI.updateUI());
|
||||||
if (window.gameUI?.addToLog && data.log) {
|
if (window.gameUI?.addToLog && data.log) {
|
||||||
data.log.forEach(log => window.gameUI.addToLog(log.message, log.type));
|
data.log.forEach(log => window.gameUI.addToLog(log.message, log.type));
|
||||||
}
|
}
|
||||||
if (window.gameUI?.showGameOver) {
|
if (window.gameUI?.showGameOver) {
|
||||||
const oppKey = clientState.opponentBaseStatsServer?.characterKey; // Используем сохраненные данные оппонента
|
const oppKey = clientState.opponentBaseStatsServer?.characterKey;
|
||||||
window.gameUI.showGameOver(playerWon, data.reason, oppKey, data);
|
window.gameUI.showGameOver(playerWon, data.reason, oppKey, data);
|
||||||
}
|
}
|
||||||
if (returnToMenuButton) returnToMenuButton.disabled = false;
|
if (returnToMenuButton) returnToMenuButton.disabled = false;
|
||||||
// `ui.setGameStatusMessage` будет установлено специфичным сообщением о результате игры
|
// `ui.setGameStatusMessage` будет установлено специфичным сообщением о результате игры
|
||||||
// ui.setGameStatusMessage("Игра окончена. " + (playerWon ? "Вы победили!" : "Вы проиграли."));
|
// ui.setGameStatusMessage("Игра окончена. " + (playerWon ? "Вы победили!" : "Вы проиграли."));
|
||||||
if (window.gameUI?.updateTurnTimerDisplay) {
|
if (window.gameUI?.updateTurnTimerDisplay) {
|
||||||
window.gameUI.updateTurnTimerDisplay(null, false, clientState.currentGameState?.gameMode); // Сбрасываем таймер
|
window.gameUI.updateTurnTimerDisplay(null, false, clientState.currentGameState?.gameMode);
|
||||||
}
|
}
|
||||||
disableGameControls(); // Отключаем управление игрой
|
disableGameControls();
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('opponentDisconnected', (data) => {
|
socket.on('opponentDisconnected', (data) => {
|
||||||
@ -313,28 +312,24 @@ export function initGameplay(dependencies) {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
if (clientState.currentGameState && !clientState.currentGameState.isGameOver) {
|
if (clientState.currentGameState && !clientState.currentGameState.isGameOver) {
|
||||||
ui.setGameStatusMessage(`Противник (${name}) отключился. Ожидание...`, true); // Показываем сообщение ожидания
|
ui.setGameStatusMessage(`Противник (${name}) отключился. Ожидание...`, true);
|
||||||
disableGameControls(); // Отключаем управление на время ожидания
|
disableGameControls();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('turnTimerUpdate', (data) => {
|
socket.on('turnTimerUpdate', (data) => {
|
||||||
// Проверяем, в игре ли мы и есть ли gameState, прежде чем обновлять таймер
|
|
||||||
if (!clientState.isInGame || !clientState.currentGameState || !window.GAME_CONFIG) {
|
if (!clientState.isInGame || !clientState.currentGameState || !window.GAME_CONFIG) {
|
||||||
// Если не в игре, но gameState есть (например, игра завершена, но экран еще не обновился),
|
|
||||||
// то таймер нужно сбросить/скрыть.
|
|
||||||
if (window.gameUI?.updateTurnTimerDisplay && clientState.currentGameState && !clientState.currentGameState.isGameOver) {
|
if (window.gameUI?.updateTurnTimerDisplay && clientState.currentGameState && !clientState.currentGameState.isGameOver) {
|
||||||
window.gameUI.updateTurnTimerDisplay(null, false, clientState.currentGameState.gameMode);
|
window.gameUI.updateTurnTimerDisplay(null, false, clientState.currentGameState.gameMode);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если игра завершена, таймер не должен обновляться или должен быть сброшен
|
|
||||||
if (clientState.currentGameState.isGameOver) {
|
if (clientState.currentGameState.isGameOver) {
|
||||||
if (window.gameUI?.updateTurnTimerDisplay) {
|
if (window.gameUI?.updateTurnTimerDisplay) {
|
||||||
window.gameUI.updateTurnTimerDisplay(null, false, clientState.currentGameState.gameMode);
|
window.gameUI.updateTurnTimerDisplay(null, false, clientState.currentGameState.gameMode);
|
||||||
}
|
}
|
||||||
disableGameControls(); // Убедимся, что управление отключено
|
disableGameControls();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -349,23 +344,15 @@ export function initGameplay(dependencies) {
|
|||||||
|
|
||||||
window.gameUI.updateTurnTimerDisplay(data.remainingTime, isMyActualTurn, clientState.currentGameState.gameMode);
|
window.gameUI.updateTurnTimerDisplay(data.remainingTime, isMyActualTurn, clientState.currentGameState.gameMode);
|
||||||
|
|
||||||
// Включаем/отключаем управление в зависимости от хода
|
|
||||||
if (isMyActualTurn) {
|
if (isMyActualTurn) {
|
||||||
enableGameControls();
|
enableGameControls();
|
||||||
} else {
|
} else {
|
||||||
disableGameControls();
|
disableGameControls();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если таймер активен и игра не закончена, общее сообщение "Ожидание" должно быть снято
|
|
||||||
// (если оно не специфично для дисконнекта оппонента)
|
|
||||||
if (!clientState.currentGameState.isGameOver) {
|
if (!clientState.currentGameState.isGameOver) {
|
||||||
// Проверяем, не показывается ли уже сообщение о дисконнекте оппонента
|
console.log(`[CLIENT ${username}] turnTimerUpdate - Clearing game status message as timer is active.`);
|
||||||
const statusMsgElement = document.getElementById('game-status-message');
|
ui.setGameStatusMessage("");
|
||||||
const currentStatusText = statusMsgElement ? statusMsgElement.textContent : "";
|
|
||||||
if (!currentStatusText.toLowerCase().includes("отключился")) {
|
|
||||||
console.log(`[CLIENT ${username}] turnTimerUpdate - Clearing game status message as timer is active.`);
|
|
||||||
ui.setGameStatusMessage("");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -98,7 +98,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
console.log('[Main.js] Initializing Socket.IO client...');
|
console.log('[Main.js] Initializing Socket.IO client...');
|
||||||
const socket = io({
|
const socket = io({
|
||||||
path:base_path,
|
|
||||||
autoConnect: false,
|
autoConnect: false,
|
||||||
auth: { token: localStorage.getItem('jwtToken') } // Передаем токен (может быть null, если был очищен)
|
auth: { token: localStorage.getItem('jwtToken') } // Передаем токен (может быть null, если был очищен)
|
||||||
});
|
});
|
||||||
|
132
server/bc.js
132
server/bc.js
@ -6,10 +6,8 @@ const express = require('express');
|
|||||||
const http = require('http');
|
const http = require('http');
|
||||||
const { Server } = require('socket.io');
|
const { Server } = require('socket.io');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const APP_BASE_PATH = process.env.APP_BASE_PATH || "";
|
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
// const cookieParser = require('cookie-parser'); // Раскомментируйте, если решите использовать куки для токена напрямую
|
|
||||||
|
|
||||||
const authService = require('./auth/authService');
|
const authService = require('./auth/authService');
|
||||||
const GameManager = require('./game/GameManager');
|
const GameManager = require('./game/GameManager');
|
||||||
@ -32,58 +30,14 @@ if (!clientOrigin && process.env.NODE_ENV !== 'development' && process.env.NODE_
|
|||||||
app.use(cors({
|
app.use(cors({
|
||||||
origin: clientOrigin,
|
origin: clientOrigin,
|
||||||
methods: ["GET", "POST"],
|
methods: ["GET", "POST"],
|
||||||
credentials: true // Важно для работы с куками, если они используются для токенов
|
credentials: true
|
||||||
}));
|
}));
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
// app.use(cookieParser()); // Раскомментируйте, если JWT будет передаваться через httpOnly cookie
|
|
||||||
|
|
||||||
const publicPath = path.join(__dirname, '..', 'public');
|
const publicPath = path.join(__dirname, '..', 'public');
|
||||||
console.log(`[BC.JS CONFIG] Serving static files from: ${publicPath}`);
|
console.log(`[BC.JS CONFIG] Serving static files from: ${publicPath}`);
|
||||||
app.use(express.static(publicPath));
|
app.use(express.static(publicPath));
|
||||||
|
|
||||||
// --- НАСТРОЙКА EJS ---
|
|
||||||
app.set('view engine', 'ejs');
|
|
||||||
// Указываем, где лежат шаблоны. Папка 'views' рядом с bc.js (т.е. server/views)
|
|
||||||
app.set('views', path.join(__dirname, 'views'));
|
|
||||||
console.log(`[BC.JS CONFIG] EJS view engine configured. Views directory: ${app.get('views')}`);
|
|
||||||
|
|
||||||
|
|
||||||
// --- HTTP МАРШРУТЫ ---
|
|
||||||
|
|
||||||
// Главная страница, рендеринг EJS шаблона
|
|
||||||
app.get('/', (req, res) => {
|
|
||||||
// Попытка извлечь токен из localStorage (недоступно на сервере напрямую)
|
|
||||||
// или из cookie (если настроено).
|
|
||||||
// Для EJS на сервере нам нужно определить состояние пользователя ДО рендеринга.
|
|
||||||
// Это обычно делается через проверку сессии или токена в cookie.
|
|
||||||
// Так как ваш клиент хранит токен в localStorage, при первом GET запросе
|
|
||||||
// на сервер токен не будет доступен в заголовках или куках автоматически.
|
|
||||||
//
|
|
||||||
// Вариант 1: Клиент делает AJAX-запрос для проверки токена после загрузки,
|
|
||||||
// а сервер отдает базовый HTML, который потом обновляется. (Текущий подход с main.js)
|
|
||||||
//
|
|
||||||
// Вариант 2: Сервер отдает базовый HTML, и клиент сам решает, что показывать,
|
|
||||||
// основываясь на токене в localStorage. (Текущий подход с main.js)
|
|
||||||
//
|
|
||||||
// Вариант 3: Передавать токен в cookie (httpOnly для безопасности),
|
|
||||||
// тогда сервер сможет его читать при GET запросе.
|
|
||||||
//
|
|
||||||
// Для простоты демонстрации EJS, предположим, что мы хотим передать
|
|
||||||
// некоторую базовую информацию, а клиентская логика main.js все равно отработает.
|
|
||||||
// Мы не можем здесь напрямую прочитать localStorage клиента.
|
|
||||||
|
|
||||||
res.render('index', { // Рендерим server/views/index.ejs
|
|
||||||
title: 'Battle Club RPG', // Передаем заголовок страницы
|
|
||||||
base_path:APP_BASE_PATH,
|
|
||||||
// Можно передать базовую структуру HTML, а main.js заполнит остальное
|
|
||||||
// Либо, если бы токен был в куках, мы могли бы здесь сделать:
|
|
||||||
// const userData = authService.verifyTokenFromCookie(req.cookies.jwtToken);
|
|
||||||
// isLoggedIn: !!userData, loggedInUsername: userData ? userData.username : '', ...
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// --- HTTP МАРШРУТЫ АУТЕНТИФИКАЦИИ ---
|
// --- HTTP МАРШРУТЫ АУТЕНТИФИКАЦИИ ---
|
||||||
app.post('/auth/register', async (req, res) => {
|
app.post('/auth/register', async (req, res) => {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
@ -95,8 +49,6 @@ app.post('/auth/register', async (req, res) => {
|
|||||||
const result = await authService.registerUser(username, password);
|
const result = await authService.registerUser(username, password);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log(`[BC HTTP /auth/register] Success for "${username}".`);
|
console.log(`[BC HTTP /auth/register] Success for "${username}".`);
|
||||||
// Если вы используете куки для токена:
|
|
||||||
// res.cookie('jwtToken', result.token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict' });
|
|
||||||
res.status(201).json(result);
|
res.status(201).json(result);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[BC HTTP /auth/register] Failed for "${username}": ${result.message}`);
|
console.warn(`[BC HTTP /auth/register] Failed for "${username}": ${result.message}`);
|
||||||
@ -114,8 +66,6 @@ app.post('/auth/login', async (req, res) => {
|
|||||||
const result = await authService.loginUser(username, password);
|
const result = await authService.loginUser(username, password);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log(`[BC HTTP /auth/login] Success for "${username}".`);
|
console.log(`[BC HTTP /auth/login] Success for "${username}".`);
|
||||||
// Если вы используете куки для токена:
|
|
||||||
// res.cookie('jwtToken', result.token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict' });
|
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[BC HTTP /auth/login] Failed for "${username}": ${result.message}`);
|
console.warn(`[BC HTTP /auth/login] Failed for "${username}": ${result.message}`);
|
||||||
@ -144,7 +94,7 @@ const io = new Server(server, {
|
|||||||
console.log(`[BC.JS CONFIG] Socket.IO server configured with path: ${io.path()} and effective CORS origin: ${io.opts.cors.origin === '*' ? "'*'" : io.opts.cors.origin || 'NOT SET'}`);
|
console.log(`[BC.JS CONFIG] Socket.IO server configured with path: ${io.path()} and effective CORS origin: ${io.opts.cors.origin === '*' ? "'*'" : io.opts.cors.origin || 'NOT SET'}`);
|
||||||
|
|
||||||
const gameManager = new GameManager(io);
|
const gameManager = new GameManager(io);
|
||||||
const loggedInUsersBySocketId = {}; // Этот объект используется только для логирования в bc.js, основная логика в GameManager
|
const loggedInUsersBySocketId = {};
|
||||||
|
|
||||||
// --- MIDDLEWARE АУТЕНТИФИКАЦИИ SOCKET.IO ---
|
// --- MIDDLEWARE АУТЕНТИФИКАЦИИ SOCKET.IO ---
|
||||||
io.use(async (socket, next) => {
|
io.use(async (socket, next) => {
|
||||||
@ -163,17 +113,10 @@ io.use(async (socket, next) => {
|
|||||||
return next();
|
return next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(`[BC Socket.IO Middleware] Socket ${socket.id} auth failed: Invalid token. Error: ${err.message}. Proceeding as unauthenticated.`);
|
console.warn(`[BC Socket.IO Middleware] Socket ${socket.id} auth failed: Invalid token. Error: ${err.message}. Proceeding as unauthenticated.`);
|
||||||
// Не вызываем next(new Error(...)) чтобы не отключать сокет сразу,
|
|
||||||
// а позволить клиенту обработать это (например, показать экран логина).
|
|
||||||
// Однако, если бы мы хотели строго запретить неаутентифицированные сокеты:
|
|
||||||
// return next(new Error('Authentication error: Invalid token'));
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(`[BC Socket.IO Middleware] Socket ${socket.id} has no token. Proceeding as unauthenticated.`);
|
console.log(`[BC Socket.IO Middleware] Socket ${socket.id} has no token. Proceeding as unauthenticated.`);
|
||||||
}
|
}
|
||||||
// Если токена нет или он невалиден, все равно вызываем next() без ошибки,
|
|
||||||
// чтобы соединение установилось, но socket.userData не будет установлен.
|
|
||||||
// Логика на стороне сервера должна будет проверять наличие socket.userData.
|
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -185,8 +128,11 @@ io.on('connection', (socket) => {
|
|||||||
|
|
||||||
if (socket.userData && socket.userData.userId) {
|
if (socket.userData && socket.userData.userId) {
|
||||||
console.log(`[BC Socket.IO Connection] Authenticated user ${socket.userData.username} (ID: ${socket.userData.userId}) connected. Socket: ${socket.id}, IP: ${clientIp}, Origin: ${originHeader}, Path: ${socketPath}`);
|
console.log(`[BC Socket.IO Connection] Authenticated user ${socket.userData.username} (ID: ${socket.userData.userId}) connected. Socket: ${socket.id}, IP: ${clientIp}, Origin: ${originHeader}, Path: ${socketPath}`);
|
||||||
loggedInUsersBySocketId[socket.id] = socket.userData; // Для логирования здесь
|
loggedInUsersBySocketId[socket.id] = socket.userData;
|
||||||
|
|
||||||
|
// --- НАЧАЛО ИЗМЕНЕНИЯ ---
|
||||||
|
// Отправляем текущий список доступных PvP игр этому конкретному сокету
|
||||||
|
// после успешной аутентификации.
|
||||||
if (gameManager && typeof gameManager.getAvailablePvPGamesListForClient === 'function') {
|
if (gameManager && typeof gameManager.getAvailablePvPGamesListForClient === 'function') {
|
||||||
console.log(`[BC Socket.IO Connection] Sending initial available PvP games list to authenticated user ${socket.userData.username} (Socket: ${socket.id})`);
|
console.log(`[BC Socket.IO Connection] Sending initial available PvP games list to authenticated user ${socket.userData.username} (Socket: ${socket.id})`);
|
||||||
const availableGames = gameManager.getAvailablePvPGamesListForClient();
|
const availableGames = gameManager.getAvailablePvPGamesListForClient();
|
||||||
@ -194,6 +140,7 @@ io.on('connection', (socket) => {
|
|||||||
} else {
|
} else {
|
||||||
console.error("[BC Socket.IO Connection] CRITICAL: gameManager or getAvailablePvPGamesListForClient not found for sending initial list!");
|
console.error("[BC Socket.IO Connection] CRITICAL: gameManager or getAvailablePvPGamesListForClient not found for sending initial list!");
|
||||||
}
|
}
|
||||||
|
// --- КОНЕЦ ИЗМЕНЕНИЯ ---
|
||||||
|
|
||||||
if (gameManager && typeof gameManager.handleRequestGameState === 'function') {
|
if (gameManager && typeof gameManager.handleRequestGameState === 'function') {
|
||||||
gameManager.handleRequestGameState(socket, socket.userData.userId);
|
gameManager.handleRequestGameState(socket, socket.userData.userId);
|
||||||
@ -202,36 +149,31 @@ io.on('connection', (socket) => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(`[BC Socket.IO Connection] Unauthenticated user connected. Socket: ${socket.id}, IP: ${clientIp}, Origin: ${originHeader}, Path: ${socketPath}.`);
|
console.log(`[BC Socket.IO Connection] Unauthenticated user connected. Socket: ${socket.id}, IP: ${clientIp}, Origin: ${originHeader}, Path: ${socketPath}.`);
|
||||||
// Неаутентифицированные пользователи не должны иметь доступа к игровым функциям,
|
// --- НАЧАЛО ИЗМЕНЕНИЯ (опционально, если неаутентифицированные тоже видят список) ---
|
||||||
// но могут получать базовую информацию, если это предусмотрено.
|
// Если неаутентифицированные пользователи тоже должны видеть список игр
|
||||||
// Например, список игр, если он публичный (в данном проекте он для залогиненных).
|
/*
|
||||||
// socket.emit('authRequired', { message: 'Please login to access game features.' }); // Можно отправить такое сообщение
|
if (gameManager && typeof gameManager.getAvailablePvPGamesListForClient === 'function') {
|
||||||
|
console.log(`[BC Socket.IO Connection] Sending initial available PvP games list to unauthenticated socket ${socket.id}`);
|
||||||
|
const availableGames = gameManager.getAvailablePvPGamesListForClient();
|
||||||
|
socket.emit('availablePvPGamesList', availableGames);
|
||||||
|
} else {
|
||||||
|
console.error("[BC Socket.IO Connection] CRITICAL: gameManager or getAvailablePvPGamesListForClient not found for sending initial list to unauth user!");
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
// --- КОНЕЦ ИЗМЕНЕНИЯ ---
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.on('logout', () => { // Это событие инициируется клиентом, когда он нажимает "Выйти"
|
socket.on('logout', () => {
|
||||||
const username = socket.userData?.username || 'UnknownUserOnLogout';
|
const username = socket.userData?.username || 'UnknownUserOnLogout';
|
||||||
const userId = socket.userData?.userId;
|
const userId = socket.userData?.userId;
|
||||||
console.log(`[BC Socket.IO 'logout' event] User: ${username} (ID: ${userId || 'N/A'}, Socket: ${socket.id}). Performing server-side cleanup for logout.`);
|
console.log(`[BC Socket.IO 'logout' event] User: ${username} (ID: ${userId || 'N/A'}, Socket: ${socket.id}).`);
|
||||||
|
|
||||||
// Здесь важно не просто удалить данные из loggedInUsersBySocketId (это локальный объект для логов),
|
|
||||||
// а также убедиться, что GameManager корректно обрабатывает выход игрока из игры, если он там был.
|
|
||||||
// GameManager.handleDisconnect должен вызываться автоматически при socket.disconnect() со стороны клиента.
|
|
||||||
// Дополнительно, если logout это не просто disconnect, а явное действие:
|
|
||||||
if (userId && gameManager) {
|
|
||||||
// Если игрок был в игре, GameManager.handleDisconnect должен был отработать при последующем socket.disconnect().
|
|
||||||
// Если нужно специфическое действие для logout перед disconnect:
|
|
||||||
// gameManager.handleExplicitLogout(userId, socket.id); // (потребовало бы добавить такой метод в GameManager)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loggedInUsersBySocketId[socket.id]) {
|
if (loggedInUsersBySocketId[socket.id]) {
|
||||||
delete loggedInUsersBySocketId[socket.id];
|
delete loggedInUsersBySocketId[socket.id];
|
||||||
}
|
}
|
||||||
socket.userData = null; // Очищаем данные пользователя на сокете
|
socket.userData = null;
|
||||||
// Клиент сам вызовет socket.disconnect() и socket.connect() с новым (null) токеном.
|
console.log(`[BC Socket.IO 'logout' event] Session data for socket ${socket.id} cleared on server.`);
|
||||||
console.log(`[BC Socket.IO 'logout' event] Session data for socket ${socket.id} cleared on server. Client is expected to disconnect and reconnect.`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
socket.on('playerSurrender', () => {
|
socket.on('playerSurrender', () => {
|
||||||
if (!socket.userData?.userId) {
|
if (!socket.userData?.userId) {
|
||||||
console.warn(`[BC Socket.IO 'playerSurrender'] Denied for unauthenticated socket ${socket.id}.`);
|
console.warn(`[BC Socket.IO 'playerSurrender'] Denied for unauthenticated socket ${socket.id}.`);
|
||||||
@ -260,7 +202,7 @@ io.on('connection', (socket) => {
|
|||||||
console.log(`[BC Socket.IO 'leaveAiGame'] Request from user ${username} (ID: ${identifier}, Socket: ${socket.id})`);
|
console.log(`[BC Socket.IO 'leaveAiGame'] Request from user ${username} (ID: ${identifier}, Socket: ${socket.id})`);
|
||||||
|
|
||||||
if (gameManager && typeof gameManager.handleLeaveAiGame === 'function') {
|
if (gameManager && typeof gameManager.handleLeaveAiGame === 'function') {
|
||||||
gameManager.handleLeaveAiGame(identifier, socket); // Передаем сокет, если он нужен для ответа
|
gameManager.handleLeaveAiGame(identifier);
|
||||||
} else {
|
} else {
|
||||||
console.error("[BC Socket.IO 'leaveAiGame'] CRITICAL: gameManager or handleLeaveAiGame method not found!");
|
console.error("[BC Socket.IO 'leaveAiGame'] CRITICAL: gameManager or handleLeaveAiGame method not found!");
|
||||||
socket.emit('gameError', { message: 'Ошибка сервера при выходе из AI игры.' });
|
socket.emit('gameError', { message: 'Ошибка сервера при выходе из AI игры.' });
|
||||||
@ -287,10 +229,9 @@ io.on('connection', (socket) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const gameId = data?.gameId;
|
const gameId = data?.gameId;
|
||||||
const userId = socket.userData.userId; // Используем userId из socket.userData
|
const userId = socket.userData.userId;
|
||||||
const charKey = data?.characterKey; // Клиент может предлагать персонажа при присоединении
|
console.log(`[BC Socket.IO 'joinGame'] Request from ${socket.userData.username} (ID: ${userId}). GameID: ${gameId}`);
|
||||||
console.log(`[BC Socket.IO 'joinGame'] Request from ${socket.userData.username} (ID: ${userId}). GameID: ${gameId}, Char: ${charKey}`);
|
gameManager.joinGame(socket, gameId, userId);
|
||||||
gameManager.joinGame(socket, gameId, userId, charKey);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('findRandomGame', (data) => {
|
socket.on('findRandomGame', (data) => {
|
||||||
@ -306,23 +247,19 @@ io.on('connection', (socket) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on('requestPvPGameList', () => {
|
socket.on('requestPvPGameList', () => {
|
||||||
// Этот запрос может приходить и от неаутентифицированных, если дизайн это позволяет.
|
|
||||||
// В текущей логике GameManager, список игр формируется для залогиненных.
|
|
||||||
// Если не залогинен, можно отправить пустой список или специальное сообщение.
|
|
||||||
console.log(`[BC Socket.IO 'requestPvPGameList'] Request from socket ${socket.id} (User: ${socket.userData?.username || 'Unauth'}).`);
|
console.log(`[BC Socket.IO 'requestPvPGameList'] Request from socket ${socket.id} (User: ${socket.userData?.username || 'Unauth'}).`);
|
||||||
if (gameManager && typeof gameManager.getAvailablePvPGamesListForClient === 'function') {
|
if (gameManager && typeof gameManager.getAvailablePvPGamesListForClient === 'function') {
|
||||||
const availableGames = gameManager.getAvailablePvPGamesListForClient(); // GameManager сам решит, что вернуть
|
const availableGames = gameManager.getAvailablePvPGamesListForClient();
|
||||||
socket.emit('availablePvPGamesList', availableGames);
|
socket.emit('availablePvPGamesList', availableGames);
|
||||||
} else {
|
} else {
|
||||||
console.error("[BC Socket.IO 'requestPvPGameList'] CRITICAL: gameManager or getAvailablePvPGamesListForClient not found!");
|
console.error("[BC Socket.IO 'requestPvPGameList'] CRITICAL: gameManager or getAvailablePvPGamesListForClient not found!");
|
||||||
socket.emit('availablePvPGamesList', []);
|
socket.emit('availablePvPGamesList', []); // Отправляем пустой список в случае ошибки
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('requestGameState', () => {
|
socket.on('requestGameState', () => {
|
||||||
if (!socket.userData?.userId) {
|
if (!socket.userData?.userId) {
|
||||||
console.warn(`[BC Socket.IO 'requestGameState'] Denied for unauthenticated socket ${socket.id}.`);
|
console.warn(`[BC Socket.IO 'requestGameState'] Denied for unauthenticated socket ${socket.id}.`);
|
||||||
// Важно! Клиент main.js ожидает gameNotFound, чтобы показать экран логина
|
|
||||||
socket.emit('gameNotFound', { message: 'Необходимо войти для восстановления игры.' });
|
socket.emit('gameNotFound', { message: 'Необходимо войти для восстановления игры.' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -343,17 +280,16 @@ io.on('connection', (socket) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on('disconnect', (reason) => {
|
socket.on('disconnect', (reason) => {
|
||||||
const identifier = socket.userData?.userId; // Получаем из socket.userData, если был аутентифицирован
|
const identifier = socket.userData?.userId;
|
||||||
const username = socket.userData?.username || loggedInUsersBySocketId[socket.id]?.username || 'UnauthenticatedOrUnknown';
|
const username = socket.userData?.username || loggedInUsersBySocketId[socket.id]?.username || 'UnauthenticatedOrUnknown';
|
||||||
|
|
||||||
console.log(`[BC Socket.IO Disconnect] User ${username} (ID: ${identifier || 'N/A'}, Socket: ${socket.id}) disconnected. Reason: ${reason}.`);
|
console.log(`[BC Socket.IO Disconnect] User ${username} (ID: ${identifier || 'N/A'}, Socket: ${socket.id}) disconnected. Reason: ${reason}.`);
|
||||||
if (identifier && gameManager) { // Если пользователь был аутентифицирован
|
if (identifier) {
|
||||||
gameManager.handleDisconnect(socket.id, identifier);
|
gameManager.handleDisconnect(socket.id, identifier);
|
||||||
}
|
}
|
||||||
if (loggedInUsersBySocketId[socket.id]) { // Очистка из локального объекта для логов
|
if (loggedInUsersBySocketId[socket.id]) {
|
||||||
delete loggedInUsersBySocketId[socket.id];
|
delete loggedInUsersBySocketId[socket.id];
|
||||||
}
|
}
|
||||||
// socket.userData автоматически очищается при дисконнекте самого объекта сокета
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -376,7 +312,6 @@ server.listen(PORT, HOSTNAME, () => {
|
|||||||
console.log(`[BC Server Startup] Server is listening on a specific interface: ${HOSTNAME}.`);
|
console.log(`[BC Server Startup] Server is listening on a specific interface: ${HOSTNAME}.`);
|
||||||
}
|
}
|
||||||
console.log(`[BC Server Startup] Static files served from: ${publicPath}`);
|
console.log(`[BC Server Startup] Static files served from: ${publicPath}`);
|
||||||
console.log(`[BC.JS Startup] EJS views directory: ${app.get('views')}`);
|
|
||||||
console.log(`[BC.JS Startup] Socket.IO server effective path: ${io.path()}`);
|
console.log(`[BC.JS Startup] Socket.IO server effective path: ${io.path()}`);
|
||||||
console.log(`[BC.JS Startup] HTTP API effective CORS origin: ${clientOrigin === '*' ? "'*'" : clientOrigin || 'NOT SET'}`);
|
console.log(`[BC.JS Startup] HTTP API effective CORS origin: ${clientOrigin === '*' ? "'*'" : clientOrigin || 'NOT SET'}`);
|
||||||
console.log(`[BC.JS Startup] Socket.IO effective CORS origin: ${io.opts.cors.origin === '*' ? "'*'" : io.opts.cors.origin || 'NOT SET'}`);
|
console.log(`[BC.JS Startup] Socket.IO effective CORS origin: ${io.opts.cors.origin === '*' ? "'*'" : io.opts.cors.origin || 'NOT SET'}`);
|
||||||
@ -384,10 +319,9 @@ server.listen(PORT, HOSTNAME, () => {
|
|||||||
|
|
||||||
process.on('unhandledRejection', (reason, promise) => {
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
console.error('[BC Server FATAL UnhandledRejection] Reason:', reason, 'Promise:', promise);
|
console.error('[BC Server FATAL UnhandledRejection] Reason:', reason, 'Promise:', promise);
|
||||||
// process.exit(1); // Можно раскомментировать для падения сервера при неперехваченных промисах
|
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('uncaughtException', (err) => {
|
process.on('uncaughtException', (err) => {
|
||||||
console.error('[BC Server FATAL UncaughtException] Error:', err);
|
console.error('[BC Server FATAL UncaughtException] Error:', err);
|
||||||
process.exit(1); // Критические ошибки должны приводить к перезапуску через process manager
|
process.exit(1);
|
||||||
});
|
});
|
@ -1,6 +1,6 @@
|
|||||||
// /server/game/GameManager.js
|
// /server/game/GameManager.js
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
const GameInstance = require('./instance/GameInstance');
|
const GameInstance = require('./instance/GameInstance'); // Путь к GameInstance с геттерами
|
||||||
const dataUtils = require('../data/dataUtils');
|
const dataUtils = require('../data/dataUtils');
|
||||||
const GAME_CONFIG = require('../core/config');
|
const GAME_CONFIG = require('../core/config');
|
||||||
|
|
||||||
@ -13,78 +13,46 @@ class GameManager {
|
|||||||
console.log("[GameManager] Initialized.");
|
console.log("[GameManager] Initialized.");
|
||||||
}
|
}
|
||||||
|
|
||||||
_cleanupPreviousPendingGameForUser(identifier, reasonSuffix = 'unknown_cleanup_reason') {
|
_removePreviousPendingGames(currentSocketId, identifier, excludeGameId = null) {
|
||||||
|
console.log(`[GameManager._removePreviousPendingGames] User: ${identifier}, Socket: ${currentSocketId}, Exclude: ${excludeGameId}`);
|
||||||
const oldPendingGameId = this.userIdentifierToGameId[identifier];
|
const oldPendingGameId = this.userIdentifierToGameId[identifier];
|
||||||
if (oldPendingGameId && this.games[oldPendingGameId]) {
|
|
||||||
|
if (oldPendingGameId && oldPendingGameId !== excludeGameId && this.games[oldPendingGameId]) {
|
||||||
const gameToRemove = this.games[oldPendingGameId];
|
const gameToRemove = this.games[oldPendingGameId];
|
||||||
// Убеждаемся, что это именно ожидающая PvP игра этого пользователя
|
// Используем gameToRemove.playerCount (через геттер)
|
||||||
if (gameToRemove.mode === 'pvp' &&
|
if (gameToRemove.mode === 'pvp' &&
|
||||||
gameToRemove.ownerIdentifier === identifier && // Он владелец
|
gameToRemove.playerCount === 1 &&
|
||||||
gameToRemove.playerCount === 1 && // В игре только он
|
gameToRemove.ownerIdentifier === identifier &&
|
||||||
this.pendingPvPGames.includes(oldPendingGameId) && // Игра в списке ожидающих
|
this.pendingPvPGames.includes(oldPendingGameId)) {
|
||||||
(!gameToRemove.gameState || !gameToRemove.gameState.isGameOver) // И она не завершена
|
console.log(`[GameManager._removePreviousPendingGames] User ${identifier} creating/joining new. Removing previous pending PvP game: ${oldPendingGameId}`);
|
||||||
) {
|
this._cleanupGame(oldPendingGameId, 'owner_action_removed_pending_game');
|
||||||
console.log(`[GameManager._cleanupPreviousPendingGameForUser] User ${identifier} has an existing pending PvP game ${oldPendingGameId}. Removing it. Reason: ${reasonSuffix}`);
|
|
||||||
this._cleanupGame(oldPendingGameId, `owner_action_removed_pending_pvp_game_${reasonSuffix}`);
|
|
||||||
// _cleanupGame должен удалить запись из userIdentifierToGameId
|
|
||||||
return true; // Успешно очистили
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false; // Нечего было очищать или условия не совпали
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createGame(socket, mode = 'ai', chosenCharacterKey = null, identifier) {
|
createGame(socket, mode = 'ai', chosenCharacterKey = null, identifier) {
|
||||||
console.log(`[GameManager.createGame] User: ${identifier} (Socket: ${socket.id}), Mode: ${mode}, Char: ${chosenCharacterKey || 'Default'}`);
|
console.log(`[GameManager.createGame] User: ${identifier} (Socket: ${socket.id}), Mode: ${mode}, Char: ${chosenCharacterKey || 'Default'}`);
|
||||||
|
|
||||||
const existingGameIdForUser = this.userIdentifierToGameId[identifier];
|
const existingGameId = this.userIdentifierToGameId[identifier];
|
||||||
|
if (existingGameId && this.games[existingGameId]) {
|
||||||
|
const existingGame = this.games[existingGameId];
|
||||||
|
// Используем existingGame.playerCount (через геттер)
|
||||||
|
console.warn(`[GameManager.createGame] User ${identifier} already in game ${existingGameId}. Mode: ${existingGame.mode}, Players: ${existingGame.playerCount}, Owner: ${existingGame.ownerIdentifier}, GameOver: ${existingGame.gameState?.isGameOver}`);
|
||||||
|
|
||||||
// 1. Проверить, не находится ли пользователь уже в какой-либо АКТИВНОЙ игре.
|
if (existingGame.gameState && !existingGame.gameState.isGameOver) {
|
||||||
if (existingGameIdForUser && this.games[existingGameIdForUser]) {
|
// Используем existingGame.playerCount (через геттер)
|
||||||
const existingGame = this.games[existingGameIdForUser];
|
if (existingGame.mode === 'pvp' && existingGame.playerCount === 1 && existingGame.ownerIdentifier === identifier) {
|
||||||
if (existingGame.gameState && existingGame.gameState.isGameOver) {
|
socket.emit('gameError', { message: 'Вы уже создали PvP игру и ожидаете оппонента.' });
|
||||||
console.warn(`[GameManager.createGame] User ${identifier} was in a finished game ${existingGameIdForUser}. Cleaning it up before creating new.`);
|
} else {
|
||||||
this._cleanupGame(existingGameIdForUser, `stale_finished_on_create_${identifier}`);
|
|
||||||
// После _cleanupGame, existingGameIdForUser в userIdentifierToGameId[identifier] должен быть удален
|
|
||||||
} else {
|
|
||||||
// Пользователь в активной игре.
|
|
||||||
// Если это ЕГО ОЖИДАЮЩАЯ PvP игра, и он пытается создать НОВУЮ (любую), то ее нужно будет удалить ниже.
|
|
||||||
// Если это ДРУГАЯ активная игра (не его ожидающая PvP), то отказать.
|
|
||||||
const isHisOwnPendingPvp = existingGame.mode === 'pvp' &&
|
|
||||||
existingGame.ownerIdentifier === identifier &&
|
|
||||||
existingGame.playerCount === 1 &&
|
|
||||||
this.pendingPvPGames.includes(existingGameIdForUser);
|
|
||||||
|
|
||||||
if (!isHisOwnPendingPvp) {
|
|
||||||
// Он в другой активной игре (AI, или PvP с оппонентом, или PvP другого игрока)
|
|
||||||
console.warn(`[GameManager.createGame] User ${identifier} is already in an active game ${existingGameIdForUser} (mode: ${existingGame.mode}, owner: ${existingGame.ownerIdentifier}). Cannot create new.`);
|
|
||||||
socket.emit('gameError', { message: 'Вы уже находитесь в активной игре.' });
|
socket.emit('gameError', { message: 'Вы уже находитесь в активной игре.' });
|
||||||
this.handleRequestGameState(socket, identifier); // Попытаться вернуть в ту игру
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
// Если это его ожидающая PvP, то _cleanupPreviousPendingGameForUser ниже ее удалит.
|
this.handleRequestGameState(socket, identifier);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
this._cleanupGame(existingGameId, `stale_finished_on_create_${identifier}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this._removePreviousPendingGames(socket.id, identifier);
|
||||||
// 2. Удалить предыдущую ОЖИДАЮЩУЮ PvP игру этого пользователя, если он создает новую любую игру.
|
|
||||||
// Это важно сделать ДО создания новой игры, чтобы освободить userIdentifierToGameId.
|
|
||||||
const cleanedUp = this._cleanupPreviousPendingGameForUser(identifier, `creating_new_game_mode_${mode}`);
|
|
||||||
if (cleanedUp) {
|
|
||||||
console.log(`[GameManager.createGame] Successfully cleaned up previous pending PvP game for ${identifier}.`);
|
|
||||||
} else {
|
|
||||||
console.log(`[GameManager.createGame] No previous pending PvP game found or needed cleanup for ${identifier}.`);
|
|
||||||
}
|
|
||||||
console.log(`[GameManager.createGame] After potential cleanup, user ${identifier} mapping: ${this.userIdentifierToGameId[identifier]}`);
|
|
||||||
|
|
||||||
|
|
||||||
// 3. Окончательная проверка: если ПОСЛЕ очистки пользователь все еще привязан к какой-то активной игре
|
|
||||||
// (Это может случиться, если _cleanupPreviousPendingGameForUser не нашла ожидающую, но он был в другой игре, что было бы ошибкой логики выше)
|
|
||||||
const stillExistingGameIdAfterCleanup = this.userIdentifierToGameId[identifier];
|
|
||||||
if (stillExistingGameIdAfterCleanup && this.games[stillExistingGameIdAfterCleanup] && !this.games[stillExistingGameIdAfterCleanup].gameState?.isGameOver) {
|
|
||||||
console.error(`[GameManager.createGame] CRITICAL LOGIC ERROR: User ${identifier} still mapped to active game ${stillExistingGameIdAfterCleanup} after cleanup attempt. Denying creation.`);
|
|
||||||
socket.emit('gameError', { message: 'Ошибка: не удалось освободить предыдущую игровую сессию.' });
|
|
||||||
this.handleRequestGameState(socket, identifier);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const gameId = uuidv4();
|
const gameId = uuidv4();
|
||||||
console.log(`[GameManager.createGame] New GameID: ${gameId}`);
|
console.log(`[GameManager.createGame] New GameID: ${gameId}`);
|
||||||
@ -94,19 +62,20 @@ class GameManager {
|
|||||||
const charKeyForPlayer = mode === 'ai' ? (chosenCharacterKey || 'elena') : (chosenCharacterKey || 'elena');
|
const charKeyForPlayer = mode === 'ai' ? (chosenCharacterKey || 'elena') : (chosenCharacterKey || 'elena');
|
||||||
|
|
||||||
if (game.addPlayer(socket, charKeyForPlayer, identifier)) {
|
if (game.addPlayer(socket, charKeyForPlayer, identifier)) {
|
||||||
this.userIdentifierToGameId[identifier] = gameId; // Связываем пользователя с НОВОЙ игрой
|
this.userIdentifierToGameId[identifier] = gameId;
|
||||||
|
// Получаем роль и актуальный ключ из GameInstance через геттер game.players
|
||||||
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
||||||
const assignedPlayerId = playerInfo?.id;
|
const assignedPlayerId = playerInfo?.id;
|
||||||
const actualCharacterKey = playerInfo?.chosenCharacterKey;
|
const actualCharacterKey = playerInfo?.chosenCharacterKey;
|
||||||
|
|
||||||
if (!assignedPlayerId || !actualCharacterKey) {
|
if (!assignedPlayerId || !actualCharacterKey) {
|
||||||
console.error(`[GameManager.createGame] CRITICAL: Failed to get player role/charKey after addPlayer for ${identifier} in game ${gameId}. Cleaning up.`);
|
console.error(`[GameManager.createGame] CRITICAL: Failed to get player role/charKey after addPlayer for ${identifier} in game ${gameId}. Cleaning up.`);
|
||||||
this._cleanupGame(gameId, 'player_info_missing_after_add_on_create');
|
this._cleanupGame(gameId, 'player_info_missing_after_add');
|
||||||
socket.emit('gameError', { message: 'Ошибка сервера при создании роли в игре.' });
|
socket.emit('gameError', { message: 'Ошибка сервера при создании роли в игре.' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[GameManager.createGame] Player ${identifier} added to game ${gameId} as ${assignedPlayerId}. User map updated. Current map for ${identifier}: ${this.userIdentifierToGameId[identifier]}`);
|
console.log(`[GameManager.createGame] Player ${identifier} added to game ${gameId} as ${assignedPlayerId}. User map updated.`);
|
||||||
socket.emit('gameCreated', {
|
socket.emit('gameCreated', {
|
||||||
gameId: gameId,
|
gameId: gameId,
|
||||||
mode: mode,
|
mode: mode,
|
||||||
@ -116,103 +85,74 @@ class GameManager {
|
|||||||
|
|
||||||
if (mode === 'ai') {
|
if (mode === 'ai') {
|
||||||
if (game.initializeGame()) {
|
if (game.initializeGame()) {
|
||||||
console.log(`[GameManager.createGame] AI game ${gameId} initialized by GameManager, starting...`);
|
|
||||||
game.startGame();
|
game.startGame();
|
||||||
} else {
|
} else {
|
||||||
console.error(`[GameManager.createGame] AI game ${gameId} init failed in GameManager. Cleaning up.`);
|
|
||||||
this._cleanupGame(gameId, 'init_fail_ai_create_gm');
|
this._cleanupGame(gameId, 'init_fail_ai_create_gm');
|
||||||
}
|
}
|
||||||
} else if (mode === 'pvp') {
|
} else if (mode === 'pvp') {
|
||||||
if (game.initializeGame()) { // Для PvP инициализируем даже с одним игроком
|
game.initializeGame();
|
||||||
if (!this.pendingPvPGames.includes(gameId)) {
|
if (!this.pendingPvPGames.includes(gameId)) {
|
||||||
this.pendingPvPGames.push(gameId);
|
this.pendingPvPGames.push(gameId);
|
||||||
}
|
|
||||||
socket.emit('waitingForOpponent');
|
|
||||||
this.broadcastAvailablePvPGames();
|
|
||||||
} else {
|
|
||||||
console.error(`[GameManager.createGame] PvP game ${gameId} (single player) init failed. Cleaning up.`);
|
|
||||||
this._cleanupGame(gameId, 'init_fail_pvp_create_gm_single_player');
|
|
||||||
}
|
}
|
||||||
|
socket.emit('waitingForOpponent');
|
||||||
|
this.broadcastAvailablePvPGames();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error(`[GameManager.createGame] game.addPlayer failed for ${identifier} in ${gameId}. Cleaning up.`);
|
console.error(`[GameManager.createGame] game.addPlayer (instance method) failed for ${identifier} in ${gameId}. Cleaning up.`);
|
||||||
this._cleanupGame(gameId, 'player_add_failed_in_instance_gm_on_create');
|
this._cleanupGame(gameId, 'player_add_failed_in_instance_gm');
|
||||||
// game.addPlayer должен был сам отправить ошибку клиенту
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
joinGame(socket, gameIdToJoin, identifier, chosenCharacterKey = null) {
|
joinGame(socket, gameIdToJoin, identifier, chosenCharacterKey = null) {
|
||||||
console.log(`[GameManager.joinGame] User: ${identifier} (Socket: ${socket.id}) attempts to join ${gameIdToJoin} with char ${chosenCharacterKey || 'Default'}`);
|
console.log(`[GameManager.joinGame] User: ${identifier} (Socket: ${socket.id}) attempts to join ${gameIdToJoin} with char ${chosenCharacterKey || 'Default'}`);
|
||||||
const gameToJoin = this.games[gameIdToJoin];
|
const game = this.games[gameIdToJoin];
|
||||||
|
|
||||||
if (!gameToJoin) { socket.emit('gameError', { message: 'Игра с таким ID не найдена.' }); return; }
|
if (!game) { socket.emit('gameError', { message: 'Игра с таким ID не найдена.' }); return; }
|
||||||
if (gameToJoin.gameState?.isGameOver) { socket.emit('gameError', { message: 'Эта игра уже завершена.' }); this._cleanupGame(gameIdToJoin, `attempt_join_finished_game_${identifier}`); return; }
|
if (game.gameState?.isGameOver) { socket.emit('gameError', { message: 'Эта игра уже завершена.' }); this._cleanupGame(gameIdToJoin, `attempt_join_finished_${identifier}`); return; }
|
||||||
if (gameToJoin.mode !== 'pvp') { socket.emit('gameError', { message: 'К этой игре нельзя присоединиться (не PvP).' }); return; }
|
if (game.mode !== 'pvp') { socket.emit('gameError', { message: 'К этой игре нельзя присоединиться (не PvP).' }); return; }
|
||||||
|
|
||||||
const playerInfoInTargetGame = Object.values(gameToJoin.players).find(p => p.identifier === identifier);
|
// Используем геттер game.players
|
||||||
if (gameToJoin.playerCount >= 2 && !playerInfoInTargetGame?.isTemporarilyDisconnected) {
|
const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier);
|
||||||
|
// Используем game.playerCount (через геттер)
|
||||||
|
if (game.playerCount >= 2 && !playerInfoInGame?.isTemporarilyDisconnected) {
|
||||||
socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return;
|
socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return;
|
||||||
}
|
}
|
||||||
if (gameToJoin.ownerIdentifier === identifier && !playerInfoInTargetGame?.isTemporarilyDisconnected) {
|
if (game.ownerIdentifier === identifier && !playerInfoInGame?.isTemporarilyDisconnected) {
|
||||||
console.warn(`[GameManager.joinGame] User ${identifier} trying to join their own game ${gameIdToJoin} where they are owner and not disconnected. Treating as reconnect request.`);
|
socket.emit('gameError', { message: 'Вы не можете присоединиться к своей же ожидающей игре как новый игрок.' }); this.handleRequestGameState(socket, identifier); return;
|
||||||
this.handleRequestGameState(socket, identifier);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Очистка завершенной игры пользователя, если такая есть
|
const existingGameIdOfUser = this.userIdentifierToGameId[identifier];
|
||||||
const currentActiveGameIdUserIsIn = this.userIdentifierToGameId[identifier];
|
if (existingGameIdOfUser && existingGameIdOfUser !== gameIdToJoin) {
|
||||||
if (currentActiveGameIdUserIsIn && this.games[currentActiveGameIdUserIsIn] && this.games[currentActiveGameIdUserIsIn].gameState?.isGameOver) {
|
const otherGame = this.games[existingGameIdOfUser];
|
||||||
console.warn(`[GameManager.joinGame] User ${identifier} was in a finished game ${currentActiveGameIdUserIsIn} while trying to join ${gameIdToJoin}. Cleaning old one.`);
|
if (otherGame && !otherGame.gameState?.isGameOver) { socket.emit('gameError', { message: 'Вы уже в другой активной игре.' }); this.handleRequestGameState(socket, identifier); return; }
|
||||||
this._cleanupGame(currentActiveGameIdUserIsIn, `stale_finished_on_join_attempt_${identifier}`);
|
else if (otherGame?.gameState?.isGameOver) this._cleanupGame(existingGameIdOfUser, `stale_finished_on_join_${identifier}`);
|
||||||
}
|
}
|
||||||
|
this._removePreviousPendingGames(socket.id, identifier, gameIdToJoin);
|
||||||
// 2. Если пользователь УЖЕ ПРИВЯЗАН к какой-то ДРУГОЙ АКТИВНОЙ игре (не той, к которой пытается присоединиться),
|
|
||||||
// и это НЕ его собственная ожидающая PvP игра, то отказать.
|
|
||||||
// Если это ЕГО ОЖИДАЮЩАЯ PvP игра, то ее нужно удалить.
|
|
||||||
const stillExistingGameIdForUser = this.userIdentifierToGameId[identifier];
|
|
||||||
if (stillExistingGameIdForUser && stillExistingGameIdForUser !== gameIdToJoin && this.games[stillExistingGameIdForUser] && !this.games[stillExistingGameIdForUser].gameState?.isGameOver) {
|
|
||||||
const usersCurrentGame = this.games[stillExistingGameIdForUser];
|
|
||||||
const isHisOwnPendingPvp = usersCurrentGame.mode === 'pvp' &&
|
|
||||||
usersCurrentGame.ownerIdentifier === identifier &&
|
|
||||||
usersCurrentGame.playerCount === 1 &&
|
|
||||||
this.pendingPvPGames.includes(stillExistingGameIdForUser);
|
|
||||||
|
|
||||||
if (isHisOwnPendingPvp) {
|
|
||||||
console.log(`[GameManager.joinGame] User ${identifier} is owner of pending game ${stillExistingGameIdForUser}, but wants to join ${gameIdToJoin}. Cleaning up old game.`);
|
|
||||||
this._cleanupPreviousPendingGameForUser(identifier, `joining_another_game_${gameIdToJoin}`);
|
|
||||||
} else {
|
|
||||||
// Пользователь в другой активной игре (не своей ожидающей)
|
|
||||||
console.warn(`[GameManager.joinGame] User ${identifier} is in another active game ${stillExistingGameIdForUser}. Cannot join ${gameIdToJoin}.`);
|
|
||||||
socket.emit('gameError', { message: 'Вы уже находитесь в другой активной игре.' });
|
|
||||||
this.handleRequestGameState(socket, identifier); // Попытаться вернуть в ту игру
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(`[GameManager.joinGame] After potential cleanup before join, user ${identifier} mapping: ${this.userIdentifierToGameId[identifier]}`);
|
|
||||||
|
|
||||||
const charKeyForJoin = chosenCharacterKey || 'elena';
|
const charKeyForJoin = chosenCharacterKey || 'elena';
|
||||||
if (gameToJoin.addPlayer(socket, charKeyForJoin, identifier)) {
|
if (game.addPlayer(socket, charKeyForJoin, identifier)) {
|
||||||
this.userIdentifierToGameId[identifier] = gameIdToJoin; // Связываем пользователя с игрой, к которой он присоединился
|
this.userIdentifierToGameId[identifier] = gameIdToJoin;
|
||||||
const joinedPlayerInfo = Object.values(gameToJoin.players).find(p => p.identifier === identifier);
|
// Используем геттер game.players
|
||||||
|
const joinedPlayerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
||||||
|
|
||||||
if (!joinedPlayerInfo || !joinedPlayerInfo.id || !joinedPlayerInfo.chosenCharacterKey) {
|
if (!joinedPlayerInfo || !joinedPlayerInfo.id || !joinedPlayerInfo.chosenCharacterKey) {
|
||||||
console.error(`[GameManager.joinGame] CRITICAL: Failed to get player role/charKey after addPlayer for ${identifier} joining ${gameIdToJoin}.`);
|
console.error(`[GameManager.joinGame] CRITICAL: Failed to get player role/charKey after addPlayer for ${identifier} joining ${gameIdToJoin}.`);
|
||||||
socket.emit('gameError', { message: 'Ошибка сервера при назначении роли в игре.' });
|
socket.emit('gameError', { message: 'Ошибка сервера при назначении роли в игре.' });
|
||||||
if (this.userIdentifierToGameId[identifier] === gameIdToJoin) delete this.userIdentifierToGameId[identifier];
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(`[GameManager.joinGame] Player ${identifier} added/reconnected to ${gameIdToJoin} as ${joinedPlayerInfo.id}. User map updated. Current map for ${identifier}: ${this.userIdentifierToGameId[identifier]}`);
|
console.log(`[GameManager.joinGame] Player ${identifier} added/reconnected to ${gameIdToJoin} as ${joinedPlayerInfo.id}.`);
|
||||||
socket.emit('gameCreated', {
|
socket.emit('gameCreated', {
|
||||||
gameId: gameIdToJoin,
|
gameId: gameIdToJoin,
|
||||||
mode: gameToJoin.mode,
|
mode: game.mode,
|
||||||
yourPlayerId: joinedPlayerInfo.id,
|
yourPlayerId: joinedPlayerInfo.id,
|
||||||
chosenCharacterKey: joinedPlayerInfo.chosenCharacterKey
|
chosenCharacterKey: joinedPlayerInfo.chosenCharacterKey
|
||||||
});
|
});
|
||||||
|
|
||||||
if (gameToJoin.playerCount === 2) {
|
// Используем game.playerCount (через геттер)
|
||||||
|
if (game.playerCount === 2) {
|
||||||
console.log(`[GameManager.joinGame] Game ${gameIdToJoin} is now full. Initializing and starting.`);
|
console.log(`[GameManager.joinGame] Game ${gameIdToJoin} is now full. Initializing and starting.`);
|
||||||
if (gameToJoin.initializeGame()) {
|
if (game.initializeGame()) {
|
||||||
gameToJoin.startGame();
|
game.startGame();
|
||||||
} else {
|
} else {
|
||||||
this._cleanupGame(gameIdToJoin, 'full_init_fail_pvp_join_gm'); return;
|
this._cleanupGame(gameIdToJoin, 'full_init_fail_pvp_join_gm'); return;
|
||||||
}
|
}
|
||||||
@ -220,51 +160,33 @@ class GameManager {
|
|||||||
if (idx > -1) this.pendingPvPGames.splice(idx, 1);
|
if (idx > -1) this.pendingPvPGames.splice(idx, 1);
|
||||||
this.broadcastAvailablePvPGames();
|
this.broadcastAvailablePvPGames();
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.warn(`[GameManager.joinGame] gameToJoin.addPlayer returned false for user ${identifier} in game ${gameIdToJoin}.`);
|
|
||||||
// GameInstance должен был отправить причину
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', identifier) {
|
findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', identifier) {
|
||||||
console.log(`[GameManager.findRandomPvPGame] User: ${identifier} (Socket: ${socket.id}), CharForCreation: ${chosenCharacterKeyForCreation}`);
|
console.log(`[GameManager.findRandomPvPGame] User: ${identifier} (Socket: ${socket.id}), CharForCreation: ${chosenCharacterKeyForCreation}`);
|
||||||
|
const existingGameId = this.userIdentifierToGameId[identifier];
|
||||||
const existingGameIdForUser = this.userIdentifierToGameId[identifier];
|
if (existingGameId && this.games[existingGameId]) {
|
||||||
if (existingGameIdForUser && this.games[existingGameIdForUser]) {
|
const existingGame = this.games[existingGameId];
|
||||||
const existingGame = this.games[existingGameIdForUser];
|
if (!existingGame.gameState?.isGameOver) {
|
||||||
if (existingGame.gameState && existingGame.gameState.isGameOver) {
|
|
||||||
console.warn(`[GameManager.findRandomPvPGame] User ${identifier} was in a finished game ${existingGameIdForUser}. Cleaning it up.`);
|
|
||||||
this._cleanupGame(existingGameIdForUser, `stale_finished_on_find_random_${identifier}`);
|
|
||||||
} else {
|
|
||||||
console.warn(`[GameManager.findRandomPvPGame] User ${identifier} is already in an active/pending game ${existingGameIdForUser}. Cannot find random.`);
|
|
||||||
socket.emit('gameError', { message: 'Вы уже в активной или ожидающей игре.' });
|
socket.emit('gameError', { message: 'Вы уже в активной или ожидающей игре.' });
|
||||||
this.handleRequestGameState(socket, identifier); return;
|
this.handleRequestGameState(socket, identifier); return;
|
||||||
|
} else {
|
||||||
|
this._cleanupGame(existingGameId, `stale_finished_on_find_random_${identifier}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this._removePreviousPendingGames(socket.id, identifier);
|
||||||
// Удалить предыдущую ОЖИДАЮЩУЮ PvP игру этого пользователя, если он ищет новую.
|
|
||||||
this._cleanupPreviousPendingGameForUser(identifier, `finding_random_game`);
|
|
||||||
console.log(`[GameManager.findRandomPvPGame] After potential cleanup, user ${identifier} mapping: ${this.userIdentifierToGameId[identifier]}`);
|
|
||||||
|
|
||||||
// Если после очистки пользователь все еще привязан к какой-то *другой* активной игре
|
|
||||||
const stillExistingGameIdAfterCleanup = this.userIdentifierToGameId[identifier];
|
|
||||||
if (stillExistingGameIdAfterCleanup && this.games[stillExistingGameIdAfterCleanup] && !this.games[stillExistingGameIdAfterCleanup].gameState?.isGameOver) {
|
|
||||||
console.error(`[GameManager.findRandomPvPGame] CRITICAL LOGIC ERROR: User ${identifier} still mapped to active game ${stillExistingGameIdAfterCleanup} after cleanup attempt. Denying find random.`);
|
|
||||||
socket.emit('gameError', { message: 'Ошибка: не удалось освободить предыдущую игровую сессию для поиска.' });
|
|
||||||
this.handleRequestGameState(socket, identifier);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let gameIdToJoin = null;
|
let gameIdToJoin = null;
|
||||||
for (const id of [...this.pendingPvPGames]) { // Итерируем копию
|
for (const id of [...this.pendingPvPGames]) {
|
||||||
const pendingGame = this.games[id];
|
const pendingGame = this.games[id];
|
||||||
|
// Используем pendingGame.playerCount (через геттер)
|
||||||
if (pendingGame && pendingGame.mode === 'pvp' &&
|
if (pendingGame && pendingGame.mode === 'pvp' &&
|
||||||
pendingGame.playerCount === 1 &&
|
pendingGame.playerCount === 1 &&
|
||||||
pendingGame.ownerIdentifier !== identifier &&
|
pendingGame.ownerIdentifier !== identifier &&
|
||||||
(!pendingGame.gameState || !pendingGame.gameState.isGameOver)) {
|
!pendingGame.gameState?.isGameOver) {
|
||||||
gameIdToJoin = id; break;
|
gameIdToJoin = id; break;
|
||||||
} else if (!pendingGame || (pendingGame.gameState && pendingGame.gameState.isGameOver)) {
|
} else if (pendingGame?.gameState?.isGameOver) {
|
||||||
console.warn(`[GameManager.findRandomPvPGame] Found stale/finished pending game ${id}. Cleaning up.`);
|
|
||||||
this._cleanupGame(id, `stale_finished_pending_on_find_random`);
|
this._cleanupGame(id, `stale_finished_pending_on_find_random`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -281,25 +203,20 @@ class GameManager {
|
|||||||
|
|
||||||
handlePlayerAction(identifier, actionData) {
|
handlePlayerAction(identifier, actionData) {
|
||||||
const gameId = this.userIdentifierToGameId[identifier];
|
const gameId = this.userIdentifierToGameId[identifier];
|
||||||
|
console.log(`[GameManager.handlePlayerAction] User: ${identifier}, Action: ${actionData?.actionType}, GameID: ${gameId}`);
|
||||||
const game = this.games[gameId];
|
const game = this.games[gameId];
|
||||||
if (game) {
|
if (game) {
|
||||||
if (game.gameState?.isGameOver) {
|
if (game.gameState?.isGameOver) {
|
||||||
|
// Используем геттер game.players
|
||||||
const playerSocket = Object.values(game.players).find(p => p.identifier === identifier)?.socket;
|
const playerSocket = Object.values(game.players).find(p => p.identifier === identifier)?.socket;
|
||||||
if (playerSocket) {
|
if (playerSocket) this.handleRequestGameState(playerSocket, identifier);
|
||||||
console.warn(`[GameManager.handlePlayerAction] Action from ${identifier} for game ${gameId}, but game is over. Requesting state.`);
|
|
||||||
this.handleRequestGameState(playerSocket, identifier);
|
|
||||||
} else {
|
|
||||||
console.warn(`[GameManager.handlePlayerAction] Action from ${identifier} for game ${gameId}, game over, but no socket found for user.`);
|
|
||||||
this._cleanupGame(gameId, `action_on_over_no_socket_gm_${identifier}`);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
game.processPlayerAction(identifier, actionData);
|
game.processPlayerAction(identifier, actionData);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[GameManager.handlePlayerAction] No game found for user ${identifier} (mapped to game ${gameId}). Clearing map entry.`);
|
|
||||||
delete this.userIdentifierToGameId[identifier];
|
delete this.userIdentifierToGameId[identifier];
|
||||||
const clientSocket = this._findClientSocketByIdentifier(identifier);
|
const clientSocket = this._findClientSocketByIdentifier(identifier);
|
||||||
if (clientSocket) clientSocket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена при совершении действия.' });
|
if (clientSocket) clientSocket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена при действии.' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,13 +227,12 @@ class GameManager {
|
|||||||
if (game) {
|
if (game) {
|
||||||
if (game.gameState?.isGameOver) {
|
if (game.gameState?.isGameOver) {
|
||||||
console.warn(`[GameManager.handlePlayerSurrender] User ${identifier} in game ${gameId} surrender, but game ALREADY OVER.`);
|
console.warn(`[GameManager.handlePlayerSurrender] User ${identifier} in game ${gameId} surrender, but game ALREADY OVER.`);
|
||||||
// Не удаляем из userIdentifierToGameId здесь, _cleanupGame сделает это.
|
if (this.userIdentifierToGameId[identifier] === gameId) delete this.userIdentifierToGameId[identifier];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof game.playerDidSurrender === 'function') game.playerDidSurrender(identifier);
|
if (typeof game.playerDidSurrender === 'function') game.playerDidSurrender(identifier);
|
||||||
else { console.error(`[GameManager.handlePlayerSurrender] CRITICAL: GameInstance ${gameId} missing playerDidSurrender!`); this._cleanupGame(gameId, "surrender_missing_method_gm"); }
|
else { console.error(`[GameManager.handlePlayerSurrender] CRITICAL: GameInstance ${gameId} missing playerDidSurrender!`); this._cleanupGame(gameId, "surrender_missing_method"); }
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[GameManager.handlePlayerSurrender] No game found for user ${identifier}. Clearing map entry.`);
|
|
||||||
if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier];
|
if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -335,23 +251,19 @@ class GameManager {
|
|||||||
game.playerExplicitlyLeftAiGame(identifier);
|
game.playerExplicitlyLeftAiGame(identifier);
|
||||||
} else {
|
} else {
|
||||||
console.error(`[GameManager.handleLeaveAiGame] CRITICAL: GameInstance ${gameId} missing playerExplicitlyLeftAiGame! Cleaning up directly.`);
|
console.error(`[GameManager.handleLeaveAiGame] CRITICAL: GameInstance ${gameId} missing playerExplicitlyLeftAiGame! Cleaning up directly.`);
|
||||||
this._cleanupGame(gameId, "leave_ai_missing_method_gm");
|
this._cleanupGame(gameId, "leave_ai_missing_method");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[GameManager.handleLeaveAiGame] User ${identifier} sent leaveAiGame, but game ${gameId} is not AI mode (${game.mode}).`);
|
console.warn(`[GameManager.handleLeaveAiGame] User ${identifier} sent leaveAiGame, but game ${gameId} is not AI mode (${game.mode}).`);
|
||||||
socket.emit('gameError', { message: 'Вы не в AI игре.' }); // Сообщить клиенту об ошибке
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[GameManager.handleLeaveAiGame] No game found for user ${identifier}. Clearing map entry.`);
|
|
||||||
if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier];
|
if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier];
|
||||||
// Сообщить клиенту, что игра не найдена
|
|
||||||
const clientSocket = this._findClientSocketByIdentifier(identifier);
|
|
||||||
if(clientSocket) clientSocket.emit('gameNotFound', { message: 'AI игра не найдена для выхода.' });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_findClientSocketByIdentifier(identifier) {
|
_findClientSocketByIdentifier(identifier) {
|
||||||
for (const s of this.io.sockets.sockets.values()) { // Использование .values() для итератора
|
for (const sid of this.io.sockets.sockets.keys()) {
|
||||||
|
const s = this.io.sockets.sockets.get(sid);
|
||||||
if (s && s.userData && s.userData.userId === identifier && s.connected) return s;
|
if (s && s.userData && s.userData.userId === identifier && s.connected) return s;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -364,10 +276,10 @@ class GameManager {
|
|||||||
|
|
||||||
if (game) {
|
if (game) {
|
||||||
if (game.gameState?.isGameOver) {
|
if (game.gameState?.isGameOver) {
|
||||||
console.log(`[GameManager.handleDisconnect] Game ${gameIdFromMap} for user ${identifier} (socket ${socketId}) ALREADY OVER. Game will be cleaned up by its own logic or next relevant action.`);
|
console.log(`[GameManager.handleDisconnect] Game ${gameIdFromMap} for user ${identifier} (socket ${socketId}) ALREADY OVER.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Используем геттер game.players
|
||||||
const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier);
|
const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier);
|
||||||
|
|
||||||
if (playerInfoInGame && playerInfoInGame.socket?.id === socketId && !playerInfoInGame.isTemporarilyDisconnected) {
|
if (playerInfoInGame && playerInfoInGame.socket?.id === socketId && !playerInfoInGame.isTemporarilyDisconnected) {
|
||||||
@ -379,18 +291,15 @@ class GameManager {
|
|||||||
this._cleanupGame(gameIdFromMap, "missing_reconnect_logic_on_disconnect_gm");
|
this._cleanupGame(gameIdFromMap, "missing_reconnect_logic_on_disconnect_gm");
|
||||||
}
|
}
|
||||||
} else if (playerInfoInGame && playerInfoInGame.socket?.id !== socketId) {
|
} else if (playerInfoInGame && playerInfoInGame.socket?.id !== socketId) {
|
||||||
console.log(`[GameManager.handleDisconnect] Disconnected socket ${socketId} is STALE for user ${identifier}. Active socket in game: ${playerInfoInGame.socket?.id}. No action taken by GM.`);
|
console.log(`[GameManager.handleDisconnect] Disconnected socket ${socketId} is STALE for user ${identifier}. Active socket in game: ${playerInfoInGame.socket?.id}.`);
|
||||||
} else if (playerInfoInGame && playerInfoInGame.isTemporarilyDisconnected) {
|
} else if (playerInfoInGame && playerInfoInGame.isTemporarilyDisconnected) {
|
||||||
console.log(`[GameManager.handleDisconnect] User ${identifier} (socket ${socketId}) disconnected while ALREADY temp disconnected. Reconnect timer in GameInstance handles final cleanup.`);
|
console.log(`[GameManager.handleDisconnect] User ${identifier} (socket ${socketId}) disconnected while already temp disconnected. Reconnect timer handles final cleanup.`);
|
||||||
} else if (!playerInfoInGame) {
|
} else if (!playerInfoInGame) {
|
||||||
console.warn(`[GameManager.handleDisconnect] User ${identifier} mapped to game ${gameIdFromMap}, but not found in game.players. This might indicate a stale userIdentifierToGameId entry. Clearing map for this user.`);
|
console.warn(`[GameManager.handleDisconnect] User ${identifier} mapped to game ${gameIdFromMap}, but not found in game's player list. Clearing map.`);
|
||||||
if (this.userIdentifierToGameId[identifier] === gameIdFromMap) {
|
if (this.userIdentifierToGameId[identifier] === gameIdFromMap) delete this.userIdentifierToGameId[identifier];
|
||||||
delete this.userIdentifierToGameId[identifier];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this.userIdentifierToGameId[identifier]) {
|
if (this.userIdentifierToGameId[identifier]) {
|
||||||
console.warn(`[GameManager.handleDisconnect] No game instance found for gameId ${gameIdFromMap} (user ${identifier}). Clearing stale map entry.`);
|
|
||||||
delete this.userIdentifierToGameId[identifier];
|
delete this.userIdentifierToGameId[identifier];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -401,69 +310,55 @@ class GameManager {
|
|||||||
const game = this.games[gameId];
|
const game = this.games[gameId];
|
||||||
|
|
||||||
if (!game) {
|
if (!game) {
|
||||||
console.warn(`[GameManager._cleanupGame] Game instance for ${gameId} not found in this.games. Cleaning up associated records.`);
|
|
||||||
const pendingIdx = this.pendingPvPGames.indexOf(gameId);
|
const pendingIdx = this.pendingPvPGames.indexOf(gameId);
|
||||||
if (pendingIdx > -1) {
|
if (pendingIdx > -1) { this.pendingPvPGames.splice(pendingIdx, 1); this.broadcastAvailablePvPGames(); }
|
||||||
this.pendingPvPGames.splice(pendingIdx, 1);
|
for (const idKey in this.userIdentifierToGameId) { if (this.userIdentifierToGameId[idKey] === gameId) delete this.userIdentifierToGameId[idKey]; }
|
||||||
console.log(`[GameManager._cleanupGame] Removed ${gameId} from pendingPvPGames.`);
|
|
||||||
}
|
|
||||||
// Важно: итерируем по ключам, так как удаление может изменить объект
|
|
||||||
Object.keys(this.userIdentifierToGameId).forEach(idKey => {
|
|
||||||
if (this.userIdentifierToGameId[idKey] === gameId) {
|
|
||||||
delete this.userIdentifierToGameId[idKey];
|
|
||||||
console.log(`[GameManager._cleanupGame] Removed mapping for user ${idKey} to game ${gameId}.`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.broadcastAvailablePvPGames();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Используем game.playerCount (через геттер)
|
||||||
console.log(`[GameManager._cleanupGame] Cleaning up game ${game.id}. Owner: ${game.ownerIdentifier}. Reason: ${reason}. Players in game: ${game.playerCount}`);
|
console.log(`[GameManager._cleanupGame] Cleaning up game ${game.id}. Owner: ${game.ownerIdentifier}. Reason: ${reason}. Players in game: ${game.playerCount}`);
|
||||||
if (typeof game.turnTimer?.clear === 'function') game.turnTimer.clear();
|
if (typeof game.turnTimer?.clear === 'function') game.turnTimer.clear();
|
||||||
if (typeof game.clearAllReconnectTimers === 'function') game.clearAllReconnectTimers();
|
if (typeof game.clearAllReconnectTimers === 'function') game.clearAllReconnectTimers();
|
||||||
|
|
||||||
if (game.gameState && !game.gameState.isGameOver) {
|
if (game.gameState && !game.gameState.isGameOver) {
|
||||||
console.log(`[GameManager._cleanupGame] Marking game ${game.id} as game over because it's being cleaned up while active.`);
|
|
||||||
game.gameState.isGameOver = true;
|
game.gameState.isGameOver = true;
|
||||||
// Можно рассмотреть отправку gameOver, если игра прерывается извне
|
|
||||||
// game.io.to(game.id).emit('gameOver', { reason: `game_cleanup_${reason}`, finalGameState: game.gameState, log: game.consumeLogBuffer() });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let playersCleanedFromMap = 0;
|
||||||
|
// Используем геттер game.players
|
||||||
Object.values(game.players).forEach(pInfo => {
|
Object.values(game.players).forEach(pInfo => {
|
||||||
if (pInfo?.identifier && this.userIdentifierToGameId[pInfo.identifier] === gameId) {
|
if (pInfo?.identifier && this.userIdentifierToGameId[pInfo.identifier] === gameId) {
|
||||||
delete this.userIdentifierToGameId[pInfo.identifier];
|
delete this.userIdentifierToGameId[pInfo.identifier];
|
||||||
console.log(`[GameManager._cleanupGame] Cleared userIdentifierToGameId for player ${pInfo.identifier}.`);
|
playersCleanedFromMap++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (game.ownerIdentifier && this.userIdentifierToGameId[game.ownerIdentifier] === gameId) {
|
// Используем геттер game.players
|
||||||
if (!Object.values(game.players).some(p => p.identifier === game.ownerIdentifier)) {
|
if (game.ownerIdentifier && this.userIdentifierToGameId[game.ownerIdentifier] === gameId &&
|
||||||
delete this.userIdentifierToGameId[game.ownerIdentifier];
|
!Object.values(game.players).some(p=>p.identifier === game.ownerIdentifier)) {
|
||||||
console.log(`[GameManager._cleanupGame] Cleared userIdentifierToGameId for owner ${game.ownerIdentifier} (was not in players list).`);
|
delete this.userIdentifierToGameId[game.ownerIdentifier];
|
||||||
}
|
playersCleanedFromMap++;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pendingIdx = this.pendingPvPGames.indexOf(gameId);
|
const pendingIdx = this.pendingPvPGames.indexOf(gameId);
|
||||||
if (pendingIdx > -1) {
|
if (pendingIdx > -1) this.pendingPvPGames.splice(pendingIdx, 1);
|
||||||
this.pendingPvPGames.splice(pendingIdx, 1);
|
|
||||||
console.log(`[GameManager._cleanupGame] Removed ${gameId} from pendingPvPGames.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete this.games[gameId];
|
delete this.games[gameId];
|
||||||
console.log(`[GameManager._cleanupGame] Game ${gameId} instance deleted. Games left: ${Object.keys(this.games).length}. Pending: ${this.pendingPvPGames.length}. User map size: ${Object.keys(this.userIdentifierToGameId).length}`);
|
console.log(`[GameManager._cleanupGame] Game ${game.id} instance deleted. Games left: ${Object.keys(this.games).length}. Pending: ${this.pendingPvPGames.length}. User map size: ${Object.keys(this.userIdentifierToGameId).length}`);
|
||||||
this.broadcastAvailablePvPGames();
|
this.broadcastAvailablePvPGames();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAvailablePvPGamesListForClient() {
|
getAvailablePvPGamesListForClient() {
|
||||||
// Итерируем копию массива pendingPvPGames, так как _cleanupGame может его изменять
|
return this.pendingPvPGames
|
||||||
return [...this.pendingPvPGames]
|
|
||||||
.map(gameId => {
|
.map(gameId => {
|
||||||
const game = this.games[gameId];
|
const game = this.games[gameId];
|
||||||
|
// Используем game.playerCount (через геттер)
|
||||||
if (game && game.mode === 'pvp' && game.playerCount === 1 && (!game.gameState || !game.gameState.isGameOver)) {
|
if (game && game.mode === 'pvp' && game.playerCount === 1 && (!game.gameState || !game.gameState.isGameOver)) {
|
||||||
|
// Используем геттер game.players
|
||||||
const p1Entry = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected);
|
const p1Entry = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected);
|
||||||
let p1Username = 'Игрок';
|
let p1Username = 'Игрок';
|
||||||
let p1CharName = 'Неизвестный';
|
let p1CharName = 'Неизвестный';
|
||||||
const ownerId = game.ownerIdentifier; // Это должен быть identifier создателя
|
const ownerId = game.ownerIdentifier;
|
||||||
|
|
||||||
if (p1Entry && p1Entry.socket?.userData) {
|
if (p1Entry && p1Entry.socket?.userData) {
|
||||||
p1Username = p1Entry.socket.userData.username || `User#${String(p1Entry.identifier).substring(0,4)}`;
|
p1Username = p1Entry.socket.userData.username || `User#${String(p1Entry.identifier).substring(0,4)}`;
|
||||||
@ -472,14 +367,11 @@ class GameManager {
|
|||||||
} else if (ownerId){
|
} else if (ownerId){
|
||||||
const ownerSocket = this._findClientSocketByIdentifier(ownerId);
|
const ownerSocket = this._findClientSocketByIdentifier(ownerId);
|
||||||
p1Username = ownerSocket?.userData?.username || `Owner#${String(ownerId).substring(0,4)}`;
|
p1Username = ownerSocket?.userData?.username || `Owner#${String(ownerId).substring(0,4)}`;
|
||||||
const ownerCharKey = game.playerCharacterKey; // Это ключ персонажа для роли PLAYER_ID в этой игре
|
const ownerCharKey = game.playerCharacterKey;
|
||||||
const charData = ownerCharKey ? dataUtils.getCharacterBaseStats(ownerCharKey) : null;
|
const charData = ownerCharKey ? dataUtils.getCharacterBaseStats(ownerCharKey) : null;
|
||||||
p1CharName = charData?.name || ownerCharKey || 'Не выбран';
|
p1CharName = charData?.name || ownerCharKey || 'Не выбран';
|
||||||
}
|
}
|
||||||
return { id: gameId, status: `Ожидает (${p1Username} за ${p1CharName})`, ownerIdentifier: ownerId };
|
return { id: gameId, status: `Ожидает (${p1Username} за ${p1CharName})`, ownerIdentifier: ownerId };
|
||||||
} else if (game && (game.playerCount !== 1 || game.gameState?.isGameOver)) {
|
|
||||||
console.warn(`[GameManager.getAvailablePvPGamesListForClient] Game ${gameId} is in pendingPvPGames but is not a valid pending game (players: ${game.playerCount}, over: ${game.gameState?.isGameOver}). Removing.`);
|
|
||||||
this._cleanupGame(gameId, 'invalid_pending_game_in_list');
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
@ -497,56 +389,47 @@ class GameManager {
|
|||||||
const game = gameIdFromMap ? this.games[gameIdFromMap] : null;
|
const game = gameIdFromMap ? this.games[gameIdFromMap] : null;
|
||||||
|
|
||||||
if (game) {
|
if (game) {
|
||||||
|
// Используем геттер game.players
|
||||||
const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier);
|
const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier);
|
||||||
|
console.log(`[GameManager.handleRequestGameState] Game ${gameIdFromMap} found. PlayerInfo: ${playerInfoInGame ? `Role: ${playerInfoInGame.id}, TempDisco: ${playerInfoInGame.isTemporarilyDisconnected}` : 'Not found in game.players'}`);
|
||||||
|
|
||||||
if (playerInfoInGame) {
|
if (playerInfoInGame) {
|
||||||
if (game.gameState?.isGameOver) {
|
if (game.gameState?.isGameOver) {
|
||||||
socket.emit('gameNotFound', { message: 'Ваша предыдущая игра уже завершена.' });
|
socket.emit('gameNotFound', { message: 'Ваша предыдущая игра уже завершена.' });
|
||||||
// _cleanupGame будет вызвана, когда игра фактически завершается.
|
if(this.userIdentifierToGameId[identifier] === gameIdFromMap) delete this.userIdentifierToGameId[identifier];
|
||||||
// Здесь не удаляем из userIdentifierToGameId, если игра еще есть в this.games.
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof game.handlePlayerReconnected === 'function') {
|
if (typeof game.handlePlayerReconnected === 'function') {
|
||||||
const reconnected = game.handlePlayerReconnected(playerInfoInGame.id, socket);
|
const reconnected = game.handlePlayerReconnected(playerInfoInGame.id, socket);
|
||||||
if (!reconnected) {
|
// ... (обработка результата reconnected, если нужно)
|
||||||
console.warn(`[GameManager.handleRequestGameState] game.handlePlayerReconnected for ${identifier} in ${game.id} returned false.`);
|
|
||||||
// GameInstance должен был отправить ошибку.
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.error(`[GameManager.handleRequestGameState] CRITICAL: GameInstance ${game.id} missing handlePlayerReconnected!`);
|
console.error(`[GameManager.handleRequestGameState] CRITICAL: GameInstance ${game.id} missing handlePlayerReconnected!`);
|
||||||
this._handleGameRecoveryError(socket, game.id, identifier, 'gi_missing_reconnect_method_gm_on_request');
|
this._handleGameRecoveryError(socket, game.id, identifier, 'gi_missing_reconnect_method_gm');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[GameManager.handleRequestGameState] User ${identifier} mapped to game ${gameIdFromMap}, but NOT FOUND in game.players. Cleaning map & sending gameNotFound.`);
|
this._handleGameRecoveryError(socket, gameIdFromMap, identifier, 'player_not_in_gi_players_reconnect_gm');
|
||||||
this._handleGameRecoveryError(socket, gameIdFromMap, identifier, 'player_not_in_gi_players_but_mapped_on_request');
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
socket.emit('gameNotFound', { message: 'Активная игровая сессия не найдена.' });
|
socket.emit('gameNotFound', { message: 'Активная игровая сессия не найдена.' });
|
||||||
if (this.userIdentifierToGameId[identifier]) {
|
if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier];
|
||||||
console.warn(`[GameManager.handleRequestGameState] No game instance found for gameId ${gameIdFromMap} (user ${identifier}). Clearing stale map entry.`);
|
|
||||||
delete this.userIdentifierToGameId[identifier];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleGameRecoveryError(socket, gameId, identifier, reasonCode) {
|
_handleGameRecoveryError(socket, gameId, identifier, reasonCode) {
|
||||||
console.error(`[GameManager._handleGameRecoveryError] Error recovering game (ID: ${gameId || 'N/A'}) for user ${identifier}. Reason: ${reasonCode}.`);
|
console.error(`[GameManager._handleGameRecoveryError] Error recovering game (ID: ${gameId || 'N/A'}) for user ${identifier}. Reason: ${reasonCode}.`);
|
||||||
socket.emit('gameError', { message: 'Ошибка сервера при восстановлении состояния игры. Попробуйте войти снова.' });
|
socket.emit('gameError', { message: 'Ошибка сервера при восстановлении состояния игры.' });
|
||||||
|
|
||||||
if (gameId && this.games[gameId]) {
|
if (gameId && this.games[gameId]) {
|
||||||
this._cleanupGame(gameId, `recovery_error_gm_${reasonCode}_for_${identifier}`);
|
this._cleanupGame(gameId, `recovery_error_${reasonCode}_for_${identifier}`);
|
||||||
} else if (this.userIdentifierToGameId[identifier]) {
|
} else if (this.userIdentifierToGameId[identifier]) {
|
||||||
const problematicGameIdForUser = this.userIdentifierToGameId[identifier];
|
const problematicGameId = this.userIdentifierToGameId[identifier];
|
||||||
// Если игра была удалена, но пользователь к ней привязан, просто чистим карту
|
if (this.games[problematicGameId]) {
|
||||||
delete this.userIdentifierToGameId[identifier];
|
this._cleanupGame(problematicGameId, `recovery_error_stale_map_${identifier}_reason_${reasonCode}`);
|
||||||
console.log(`[GameManager._handleGameRecoveryError] Cleaned stale userIdentifierToGameId[${identifier}] pointing to ${problematicGameIdForUser}.`);
|
} else {
|
||||||
|
delete this.userIdentifierToGameId[identifier];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Убедимся, что после всех очисток пользователь точно не привязан
|
if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier];
|
||||||
if (this.userIdentifierToGameId[identifier]) {
|
socket.emit('gameNotFound', { message: 'Ваша игровая сессия была завершена из-за ошибки.' });
|
||||||
delete this.userIdentifierToGameId[identifier];
|
|
||||||
console.warn(`[GameManager._handleGameRecoveryError] Force cleaned userIdentifierToGameId[${identifier}] as a final measure.`);
|
|
||||||
}
|
|
||||||
socket.emit('gameNotFound', { message: 'Ваша игровая сессия была завершена из-за ошибки. Пожалуйста, войдите снова.' });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user