Обработка ситуаций рекконекта. Доработка.
This commit is contained in:
parent
59ac3520f1
commit
eaaf7ae14c
@ -1,6 +1,6 @@
|
|||||||
// /server/game/instance/GameInstance.js
|
// /server/game/instance/GameInstance.js
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
const TurnTimer = require('./TurnTimer');
|
const TurnTimer = require('./TurnTimer'); // Убедитесь, что это новый TurnTimer.js
|
||||||
const gameLogic = require('../logic');
|
const gameLogic = require('../logic');
|
||||||
const dataUtils = require('../../data/dataUtils');
|
const dataUtils = require('../../data/dataUtils');
|
||||||
const GAME_CONFIG = require('../../core/config');
|
const GAME_CONFIG = require('../../core/config');
|
||||||
@ -27,16 +27,17 @@ class GameInstance {
|
|||||||
GAME_CONFIG.TURN_DURATION_MS,
|
GAME_CONFIG.TURN_DURATION_MS,
|
||||||
GAME_CONFIG.TIMER_UPDATE_INTERVAL_MS,
|
GAME_CONFIG.TIMER_UPDATE_INTERVAL_MS,
|
||||||
() => this.handleTurnTimeout(),
|
() => this.handleTurnTimeout(),
|
||||||
(remainingTime, isPlayerTurnForTimer, isPaused) => {
|
// onTickCallback: (remainingTimeMs, isForPlayerSlotTurn_timerPerspective, isTimerEffectivelyPaused_byLogic)
|
||||||
// Логируем отправку обновления таймера
|
(remainingTime, isPlayerTurnForTimer, isTimerLogicPaused) => {
|
||||||
// console.log(`[GI TURN_TIMER_CB ${this.id}] Sending update. Remaining: ${remainingTime}, isPlayerT: ${isPlayerTurnForTimer}, isPaused (raw): ${isPaused}, effectivelyPaused: ${this.isGameEffectivelyPaused()}`);
|
const socketsInRoom = Array.from(this.io.sockets.adapter.rooms.get(this.id) || []);
|
||||||
|
console.log(`[GI TURN_TIMER_UPDATE_CB ${this.id}] Called! To room ${this.id} (sockets: ${socketsInRoom.join(', ')}). Remaining: ${remainingTime}, isPlayerT_forTimer: ${isPlayerTurnForTimer}, isTimerLogicPaused: ${isTimerLogicPaused}, isGameEffectivelyPaused(GI): ${this.isGameEffectivelyPaused()}`);
|
||||||
this.io.to(this.id).emit('turnTimerUpdate', {
|
this.io.to(this.id).emit('turnTimerUpdate', {
|
||||||
remainingTime,
|
remainingTime,
|
||||||
isPlayerTurn: isPlayerTurnForTimer,
|
isPlayerTurn: isPlayerTurnForTimer, // Чей ход с точки зрения таймера
|
||||||
isPaused: isPaused || this.isGameEffectivelyPaused()
|
isPaused: isTimerLogicPaused || this.isGameEffectivelyPaused() // Общая пауза
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
this.id
|
this.id // gameIdForLogs
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!this.gameManager || typeof this.gameManager._cleanupGame !== 'function') {
|
if (!this.gameManager || typeof this.gameManager._cleanupGame !== 'function') {
|
||||||
@ -61,8 +62,8 @@ class GameInstance {
|
|||||||
return this.playerConnectionHandler.addPlayer(socket, chosenCharacterKey, identifier);
|
return this.playerConnectionHandler.addPlayer(socket, chosenCharacterKey, identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
removePlayer(socketId, reason) {
|
removePlayer(socketId, reason) { // Вызывается из PCH
|
||||||
this.playerConnectionHandler.removePlayer(socketId, reason);
|
// PCH сам обрабатывает удаление, GameInstance реагирует через handlePlayerPermanentlyLeft
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey, disconnectedSocketId) {
|
handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey, disconnectedSocketId) {
|
||||||
@ -78,7 +79,7 @@ class GameInstance {
|
|||||||
this.playerConnectionHandler.clearAllReconnectTimers();
|
this.playerConnectionHandler.clearAllReconnectTimers();
|
||||||
}
|
}
|
||||||
|
|
||||||
isGameEffectivelyPaused() {
|
isGameEffectivelyPaused() { // Определяет, приостановлена ли игра из-за дисконнектов
|
||||||
return this.playerConnectionHandler.isGameEffectivelyPaused();
|
return this.playerConnectionHandler.isGameEffectivelyPaused();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,6 +89,7 @@ class GameInstance {
|
|||||||
if (this.mode === 'ai' && playerRole === GAME_CONFIG.PLAYER_ID) {
|
if (this.mode === 'ai' && playerRole === GAME_CONFIG.PLAYER_ID) {
|
||||||
this.endGameDueToDisconnect(playerRole, characterKey, "player_left_ai_game");
|
this.endGameDueToDisconnect(playerRole, characterKey, "player_left_ai_game");
|
||||||
} else if (this.mode === 'pvp') {
|
} else if (this.mode === 'pvp') {
|
||||||
|
// playerCount уже должен быть обновлен в PCH
|
||||||
if (this.playerCount < 2) {
|
if (this.playerCount < 2) {
|
||||||
const remainingActivePlayerEntry = Object.values(this.players).find(p => p.id !== playerRole && !p.isTemporarilyDisconnected);
|
const remainingActivePlayerEntry = Object.values(this.players).find(p => p.id !== playerRole && !p.isTemporarilyDisconnected);
|
||||||
this.endGameDueToDisconnect(playerRole, characterKey, "opponent_left_pvp_game", remainingActivePlayerEntry?.id);
|
this.endGameDueToDisconnect(playerRole, characterKey, "opponent_left_pvp_game", remainingActivePlayerEntry?.id);
|
||||||
@ -147,16 +149,13 @@ class GameInstance {
|
|||||||
const p1ActiveEntry = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected);
|
const p1ActiveEntry = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected);
|
||||||
const p2ActiveEntry = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID && !p.isTemporarilyDisconnected);
|
const p2ActiveEntry = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID && !p.isTemporarilyDisconnected);
|
||||||
|
|
||||||
// Устанавливаем ключи персонажей, если они еще не установлены, на основе активных игроков в PCH
|
|
||||||
// Это важно, если initializeGame вызывается до того, как PCH успел обновить ключи в GI через сеттеры
|
|
||||||
if (p1ActiveEntry && !this.playerCharacterKey) this.playerCharacterKey = p1ActiveEntry.chosenCharacterKey;
|
if (p1ActiveEntry && !this.playerCharacterKey) this.playerCharacterKey = p1ActiveEntry.chosenCharacterKey;
|
||||||
if (p2ActiveEntry && !this.opponentCharacterKey && this.mode === 'pvp') this.opponentCharacterKey = p2ActiveEntry.chosenCharacterKey;
|
if (p2ActiveEntry && !this.opponentCharacterKey && this.mode === 'pvp') this.opponentCharacterKey = p2ActiveEntry.chosenCharacterKey;
|
||||||
|
|
||||||
|
|
||||||
if (this.mode === 'ai') {
|
if (this.mode === 'ai') {
|
||||||
if (!p1ActiveEntry) { this._handleCriticalError('init_ai_no_active_player_gi', 'Инициализация AI игры: Игрок-человек не найден или не активен.'); return false; }
|
if (!p1ActiveEntry) { this._handleCriticalError('init_ai_no_active_player_gi', 'Инициализация AI игры: Игрок-человек не найден или не активен.'); return false; }
|
||||||
if (!this.playerCharacterKey) { this._handleCriticalError('init_ai_no_player_key_gi', 'Инициализация AI игры: Ключ персонажа игрока не установлен.'); return false;}
|
if (!this.playerCharacterKey) { this._handleCriticalError('init_ai_no_player_key_gi', 'Инициализация AI игры: Ключ персонажа игрока не установлен.'); return false;}
|
||||||
this.opponentCharacterKey = 'balard';
|
if (!this.opponentCharacterKey) this.opponentCharacterKey = 'balard'; // Устанавливаем AI, если еще не установлен
|
||||||
} else { // pvp
|
} else { // pvp
|
||||||
if (this.playerCount === 1 && p1ActiveEntry && !this.playerCharacterKey) {
|
if (this.playerCount === 1 && p1ActiveEntry && !this.playerCharacterKey) {
|
||||||
this._handleCriticalError('init_pvp_single_player_no_key_gi', 'PvP инициализация (1 игрок): Ключ персонажа игрока отсутствует.'); return false;
|
this._handleCriticalError('init_pvp_single_player_no_key_gi', 'PvP инициализация (1 игрок): Ключ персонажа игрока отсутствует.'); return false;
|
||||||
@ -179,7 +178,6 @@ class GameInstance {
|
|||||||
|
|
||||||
this.logBuffer = [];
|
this.logBuffer = [];
|
||||||
|
|
||||||
// Имена берутся из playerData/opponentData, если они есть. PCH обновит их при реконнекте, если они изменились.
|
|
||||||
const playerName = playerData?.baseStats?.name || (p1ActiveEntry?.name || 'Ожидание Игрока 1...');
|
const playerName = playerData?.baseStats?.name || (p1ActiveEntry?.name || 'Ожидание Игрока 1...');
|
||||||
let opponentName;
|
let opponentName;
|
||||||
if (this.mode === 'ai') {
|
if (this.mode === 'ai') {
|
||||||
@ -188,13 +186,12 @@ class GameInstance {
|
|||||||
opponentName = opponentData?.baseStats?.name || (p2ActiveEntry?.name || 'Ожидание Игрока 2...');
|
opponentName = opponentData?.baseStats?.name || (p2ActiveEntry?.name || 'Ожидание Игрока 2...');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
this.gameState = {
|
this.gameState = {
|
||||||
player: isPlayerSlotFilledAndActive ?
|
player: isPlayerSlotFilledAndActive ?
|
||||||
this._createFighterState(GAME_CONFIG.PLAYER_ID, playerData.baseStats, playerData.abilities, playerName) : // Передаем имя
|
this._createFighterState(GAME_CONFIG.PLAYER_ID, playerData.baseStats, playerData.abilities, playerName) :
|
||||||
this._createFighterState(GAME_CONFIG.PLAYER_ID, { name: playerName, maxHp: 1, maxResource: 0, resourceName: 'N/A', attackPower: 0, characterKey: null }, [], playerName),
|
this._createFighterState(GAME_CONFIG.PLAYER_ID, { name: playerName, maxHp: 1, maxResource: 0, resourceName: 'N/A', attackPower: 0, characterKey: null }, [], playerName),
|
||||||
opponent: isOpponentSlotFilledAndActive ?
|
opponent: isOpponentSlotFilledAndActive ?
|
||||||
this._createFighterState(GAME_CONFIG.OPPONENT_ID, opponentData.baseStats, opponentData.abilities, opponentName) : // Передаем имя
|
this._createFighterState(GAME_CONFIG.OPPONENT_ID, opponentData.baseStats, opponentData.abilities, opponentName) :
|
||||||
this._createFighterState(GAME_CONFIG.OPPONENT_ID, { name: opponentName, maxHp: 1, maxResource: 0, resourceName: 'N/A', attackPower: 0, characterKey: null }, [], opponentName),
|
this._createFighterState(GAME_CONFIG.OPPONENT_ID, { name: opponentName, maxHp: 1, maxResource: 0, resourceName: 'N/A', attackPower: 0, characterKey: null }, [], opponentName),
|
||||||
isPlayerTurn: (isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive) ? (Math.random() < 0.5) : true,
|
isPlayerTurn: (isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive) ? (Math.random() < 0.5) : true,
|
||||||
isGameOver: false,
|
isGameOver: false,
|
||||||
@ -207,7 +204,7 @@ class GameInstance {
|
|||||||
|
|
||||||
_createFighterState(roleId, baseStats, abilities, explicitName = null) {
|
_createFighterState(roleId, baseStats, abilities, explicitName = null) {
|
||||||
const fighterState = {
|
const fighterState = {
|
||||||
id: roleId, characterKey: baseStats.characterKey, name: explicitName || baseStats.name, // Используем explicitName если передано
|
id: roleId, characterKey: baseStats.characterKey, name: explicitName || baseStats.name,
|
||||||
currentHp: baseStats.maxHp, maxHp: baseStats.maxHp,
|
currentHp: baseStats.maxHp, maxHp: baseStats.maxHp,
|
||||||
currentResource: baseStats.maxResource, maxResource: baseStats.maxResource,
|
currentResource: baseStats.maxResource, maxResource: baseStats.maxResource,
|
||||||
resourceName: baseStats.resourceName, attackPower: baseStats.attackPower,
|
resourceName: baseStats.resourceName, attackPower: baseStats.attackPower,
|
||||||
@ -234,7 +231,7 @@ class GameInstance {
|
|||||||
|
|
||||||
if (!this.gameState || !this.gameState.player?.characterKey || !this.gameState.opponent?.characterKey) {
|
if (!this.gameState || !this.gameState.player?.characterKey || !this.gameState.opponent?.characterKey) {
|
||||||
console.warn(`[GameInstance ${this.id}] startGame: gameState или ключи персонажей не полностью инициализированы. Попытка повторной инициализации.`);
|
console.warn(`[GameInstance ${this.id}] startGame: gameState или ключи персонажей не полностью инициализированы. Попытка повторной инициализации.`);
|
||||||
if (!this.initializeGame() || !this.gameState?.player?.characterKey || !this.gameState?.opponent?.characterKey) {
|
if (!this.initializeGame() || !this.gameState?.player?.characterKey || !this.gameState?.opponent?.characterKey) { // initializeGame сама установит gameState
|
||||||
this._handleCriticalError('start_game_reinit_failed_sg_gi', 'Повторная инициализация перед стартом не удалась или ключи все еще отсутствуют в gameState.');
|
this._handleCriticalError('start_game_reinit_failed_sg_gi', 'Повторная инициализация перед стартом не удалась или ключи все еще отсутствуют в gameState.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -249,12 +246,9 @@ class GameInstance {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем имена в gameState на основе данных персонажей перед отправкой клиентам
|
|
||||||
// Это гарантирует, что имена из dataUtils (самые "правильные") попадут в первое gameStarted
|
|
||||||
if (this.gameState.player && pData?.baseStats?.name) this.gameState.player.name = pData.baseStats.name;
|
if (this.gameState.player && pData?.baseStats?.name) this.gameState.player.name = pData.baseStats.name;
|
||||||
if (this.gameState.opponent && oData?.baseStats?.name) this.gameState.opponent.name = oData.baseStats.name;
|
if (this.gameState.opponent && oData?.baseStats?.name) this.gameState.opponent.name = oData.baseStats.name;
|
||||||
|
|
||||||
|
|
||||||
this.addToLog('⚔️ Новая битва начинается! ⚔️', GAME_CONFIG.LOG_TYPE_SYSTEM);
|
this.addToLog('⚔️ Новая битва начинается! ⚔️', GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||||
|
|
||||||
if(this.gameState.player?.characterKey && this.gameState.opponent?.characterKey) {
|
if(this.gameState.player?.characterKey && this.gameState.opponent?.characterKey) {
|
||||||
@ -288,7 +282,7 @@ class GameInstance {
|
|||||||
this.broadcastLogUpdate();
|
this.broadcastLogUpdate();
|
||||||
|
|
||||||
const isFirstTurnAi = this.mode === 'ai' && !this.gameState.isPlayerTurn;
|
const isFirstTurnAi = this.mode === 'ai' && !this.gameState.isPlayerTurn;
|
||||||
console.log(`[GameInstance ${this.id}] Запуск таймера в startGame. isPlayerTurn: ${this.gameState.isPlayerTurn}, isFirstTurnAi: ${isFirstTurnAi}`);
|
console.log(`[GameInstance ${this.id}] Запуск таймера в startGame. isPlayerTurn(GS): ${this.gameState.isPlayerTurn}, isAiMakingMove(to timer): ${isFirstTurnAi}`);
|
||||||
this.turnTimer.start(this.gameState.isPlayerTurn, isFirstTurnAi);
|
this.turnTimer.start(this.gameState.isPlayerTurn, isFirstTurnAi);
|
||||||
|
|
||||||
if (isFirstTurnAi) {
|
if (isFirstTurnAi) {
|
||||||
@ -311,7 +305,10 @@ class GameInstance {
|
|||||||
actingPlayerInfo.socket.emit('gameError', {message: "Действие невозможно: игра на паузе."});
|
actingPlayerInfo.socket.emit('gameError', {message: "Действие невозможно: игра на паузе."});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.gameState || this.gameState.isGameOver) { return; }
|
if (!this.gameState || this.gameState.isGameOver) {
|
||||||
|
console.warn(`[GameInstance ${this.id}] processPlayerAction: Действие от ${identifier} проигнорировано (нет gameState или игра завершена). GameOver: ${this.gameState?.isGameOver}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const actingPlayerRole = actingPlayerInfo.id;
|
const actingPlayerRole = actingPlayerInfo.id;
|
||||||
const isCorrectTurn = (this.gameState.isPlayerTurn && actingPlayerRole === GAME_CONFIG.PLAYER_ID) ||
|
const isCorrectTurn = (this.gameState.isPlayerTurn && actingPlayerRole === GAME_CONFIG.PLAYER_ID) ||
|
||||||
@ -323,8 +320,11 @@ class GameInstance {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[GameInstance ${this.id}] Ход корректен. Очистка таймера.`);
|
console.log(`[GameInstance ${this.id}] Ход корректен для ${identifier}. Очистка таймера.`);
|
||||||
if(this.turnTimer.isActive()) this.turnTimer.clear();
|
if(this.turnTimer.isActive() || this.turnTimer.isPaused()) { // Очищаем, даже если на паузе, т.к. действие совершено
|
||||||
|
this.turnTimer.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const attackerState = this.gameState[actingPlayerRole];
|
const attackerState = this.gameState[actingPlayerRole];
|
||||||
const defenderRole = actingPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
const defenderRole = actingPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||||||
@ -365,12 +365,14 @@ class GameInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.checkGameOver()) return;
|
if (this.checkGameOver()) return;
|
||||||
this.broadcastLogUpdate();
|
this.broadcastLogUpdate(); // Отправляем лог сразу после действия
|
||||||
if (actionIsValidAndPerformed) {
|
if (actionIsValidAndPerformed) {
|
||||||
|
// Небольшая задержка перед сменой хода, чтобы клиент успел увидеть результат действия
|
||||||
setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION);
|
setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION);
|
||||||
} else {
|
} else {
|
||||||
|
// Если действие было невалидным, перезапускаем таймер для текущего игрока
|
||||||
const isAiTurnForTimer = this.mode === 'ai' && !this.gameState.isPlayerTurn;
|
const isAiTurnForTimer = this.mode === 'ai' && !this.gameState.isPlayerTurn;
|
||||||
console.log(`[GameInstance ${this.id}] Действие не выполнено, перезапуск таймера. isPlayerTurn: ${this.gameState.isPlayerTurn}, isAiTurnForTimer: ${isAiTurnForTimer}`);
|
console.log(`[GameInstance ${this.id}] Действие не выполнено, перезапуск таймера для ${identifier}. isPlayerTurn(GS): ${this.gameState.isPlayerTurn}, isAiMakingMove(to timer): ${isAiTurnForTimer}`);
|
||||||
this.turnTimer.start(this.gameState.isPlayerTurn, isAiTurnForTimer);
|
this.turnTimer.start(this.gameState.isPlayerTurn, isAiTurnForTimer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -379,7 +381,12 @@ class GameInstance {
|
|||||||
console.log(`[GameInstance ${this.id}] Попытка смены хода. Paused: ${this.isGameEffectivelyPaused()}, GameOver: ${this.gameState?.isGameOver}`);
|
console.log(`[GameInstance ${this.id}] Попытка смены хода. Paused: ${this.isGameEffectivelyPaused()}, GameOver: ${this.gameState?.isGameOver}`);
|
||||||
if (this.isGameEffectivelyPaused()) { console.log(`[GameInstance ${this.id}] Смена хода отложена: игра на паузе.`); return; }
|
if (this.isGameEffectivelyPaused()) { console.log(`[GameInstance ${this.id}] Смена хода отложена: игра на паузе.`); return; }
|
||||||
if (!this.gameState || this.gameState.isGameOver) { return; }
|
if (!this.gameState || this.gameState.isGameOver) { return; }
|
||||||
if(this.turnTimer.isActive()) this.turnTimer.clear();
|
|
||||||
|
// Таймер хода должен быть уже очищен в processPlayerAction или processAiTurn
|
||||||
|
// Но на всякий случай, если switchTurn вызван из другого места (например, после эффектов)
|
||||||
|
if(this.turnTimer.isActive() || this.turnTimer.isPaused()) {
|
||||||
|
this.turnTimer.clear();
|
||||||
|
}
|
||||||
|
|
||||||
const endingTurnActorRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
|
const endingTurnActorRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
|
||||||
const endingTurnActorState = this.gameState[endingTurnActorRole];
|
const endingTurnActorState = this.gameState[endingTurnActorRole];
|
||||||
@ -403,14 +410,14 @@ class GameInstance {
|
|||||||
if (!currentTurnActorState || !currentTurnActorState.name) { this._handleCriticalError('switch_turn_current_actor_invalid_gi', `Состояние или имя текущего актора недействительны.`); return; }
|
if (!currentTurnActorState || !currentTurnActorState.name) { this._handleCriticalError('switch_turn_current_actor_invalid_gi', `Состояние или имя текущего актора недействительны.`); return; }
|
||||||
|
|
||||||
this.addToLog(`--- Ход ${this.gameState.turnNumber} начинается для: ${currentTurnActorState.name} ---`, GAME_CONFIG.LOG_TYPE_TURN);
|
this.addToLog(`--- Ход ${this.gameState.turnNumber} начинается для: ${currentTurnActorState.name} ---`, GAME_CONFIG.LOG_TYPE_TURN);
|
||||||
this.broadcastGameStateUpdate();
|
this.broadcastGameStateUpdate(); // Отправляем обновленное состояние и все накопленные логи
|
||||||
|
|
||||||
const currentTurnPlayerEntry = Object.values(this.players).find(p => p.id === currentTurnActorRole);
|
const currentTurnPlayerEntry = Object.values(this.players).find(p => p.id === currentTurnActorRole);
|
||||||
if (currentTurnPlayerEntry && currentTurnPlayerEntry.isTemporarilyDisconnected) {
|
if (currentTurnPlayerEntry && currentTurnPlayerEntry.isTemporarilyDisconnected) {
|
||||||
console.log(`[GameInstance ${this.id}] Ход перешел к ${currentTurnActorRole}, но игрок ${currentTurnPlayerEntry.identifier} отключен. Таймер не запущен switchTurn.`);
|
console.log(`[GameInstance ${this.id}] Ход перешел к ${currentTurnActorRole}, но игрок ${currentTurnPlayerEntry.identifier} отключен. Таймер не запущен switchTurn.`);
|
||||||
} else {
|
} else {
|
||||||
const isNextTurnAi = this.mode === 'ai' && !this.gameState.isPlayerTurn;
|
const isNextTurnAi = this.mode === 'ai' && !this.gameState.isPlayerTurn;
|
||||||
console.log(`[GameInstance ${this.id}] Запуск таймера в switchTurn. isPlayerTurn: ${this.gameState.isPlayerTurn}, isNextTurnAi: ${isNextTurnAi}`);
|
console.log(`[GameInstance ${this.id}] Запуск таймера в switchTurn. isPlayerTurn(GS): ${this.gameState.isPlayerTurn}, isAiMakingMove(to timer): ${isNextTurnAi}`);
|
||||||
this.turnTimer.start(this.gameState.isPlayerTurn, isNextTurnAi);
|
this.turnTimer.start(this.gameState.isPlayerTurn, isNextTurnAi);
|
||||||
if (isNextTurnAi) {
|
if (isNextTurnAi) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -423,7 +430,7 @@ class GameInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
processAiTurn() {
|
processAiTurn() {
|
||||||
console.log(`[GameInstance ${this.id}] processAiTurn. Paused: ${this.isGameEffectivelyPaused()}, GameOver: ${this.gameState?.isGameOver}, IsPlayerTurn: ${this.gameState?.isPlayerTurn}`);
|
console.log(`[GameInstance ${this.id}] processAiTurn. Paused: ${this.isGameEffectivelyPaused()}, GameOver: ${this.gameState?.isGameOver}, IsPlayerTurn(GS): ${this.gameState?.isPlayerTurn}`);
|
||||||
if (this.isGameEffectivelyPaused()) { console.log(`[GameInstance ${this.id}] Ход AI отложен: игра на паузе.`); return; }
|
if (this.isGameEffectivelyPaused()) { console.log(`[GameInstance ${this.id}] Ход AI отложен: игра на паузе.`); return; }
|
||||||
if (!this.gameState || this.gameState.isGameOver || this.gameState.isPlayerTurn || !this.aiOpponent) { return; }
|
if (!this.gameState || this.gameState.isGameOver || this.gameState.isPlayerTurn || !this.aiOpponent) { return; }
|
||||||
if(this.gameState.opponent?.characterKey !== 'balard' && this.aiOpponent) {
|
if(this.gameState.opponent?.characterKey !== 'balard' && this.aiOpponent) {
|
||||||
@ -431,7 +438,10 @@ class GameInstance {
|
|||||||
this.switchTurn();
|
this.switchTurn();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(this.turnTimer.isActive()) this.turnTimer.clear();
|
|
||||||
|
if(this.turnTimer.isActive() || this.turnTimer.isPaused()) { // Очищаем таймер, так как AI сейчас сделает ход
|
||||||
|
this.turnTimer.clear();
|
||||||
|
}
|
||||||
|
|
||||||
const aiState = this.gameState.opponent;
|
const aiState = this.gameState.opponent;
|
||||||
const playerState = this.gameState.player;
|
const playerState = this.gameState.player;
|
||||||
@ -457,7 +467,7 @@ class GameInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.checkGameOver()) return;
|
if (this.checkGameOver()) return;
|
||||||
this.broadcastLogUpdate();
|
this.broadcastLogUpdate(); // Отправляем лог после действия AI
|
||||||
if (actionIsValidAndPerformedForAI) {
|
if (actionIsValidAndPerformedForAI) {
|
||||||
setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION);
|
setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION);
|
||||||
} else {
|
} else {
|
||||||
@ -486,7 +496,7 @@ class GameInstance {
|
|||||||
const gameOverResult = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode);
|
const gameOverResult = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode);
|
||||||
if (gameOverResult.isOver) {
|
if (gameOverResult.isOver) {
|
||||||
this.gameState.isGameOver = true;
|
this.gameState.isGameOver = true;
|
||||||
if(this.turnTimer.isActive()) this.turnTimer.clear();
|
if(this.turnTimer.isActive() || this.turnTimer.isPaused()) this.turnTimer.clear(); // Очищаем таймер, если игра окончена
|
||||||
this.clearAllReconnectTimers();
|
this.clearAllReconnectTimers();
|
||||||
this.addToLog(gameOverResult.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
this.addToLog(gameOverResult.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||||
|
|
||||||
@ -513,7 +523,7 @@ class GameInstance {
|
|||||||
endGameDueToDisconnect(disconnectedPlayerRole, disconnectedCharacterKey, reason = "opponent_disconnected", winnerIfAny = null) {
|
endGameDueToDisconnect(disconnectedPlayerRole, disconnectedCharacterKey, reason = "opponent_disconnected", winnerIfAny = null) {
|
||||||
if (this.gameState && !this.gameState.isGameOver) {
|
if (this.gameState && !this.gameState.isGameOver) {
|
||||||
this.gameState.isGameOver = true;
|
this.gameState.isGameOver = true;
|
||||||
if(this.turnTimer.isActive()) this.turnTimer.clear();
|
if(this.turnTimer.isActive() || this.turnTimer.isPaused()) this.turnTimer.clear();
|
||||||
this.clearAllReconnectTimers();
|
this.clearAllReconnectTimers();
|
||||||
|
|
||||||
let actualWinnerRole = winnerIfAny;
|
let actualWinnerRole = winnerIfAny;
|
||||||
@ -583,7 +593,7 @@ class GameInstance {
|
|||||||
this.addToLog(`Игрок покинул AI игру до ее полного начала.`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
this.addToLog(`Игрок покинул AI игру до ее полного начала.`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.turnTimer.isActive()) this.turnTimer.clear();
|
if (this.turnTimer.isActive() || this.turnTimer.isPaused()) this.turnTimer.clear();
|
||||||
this.clearAllReconnectTimers();
|
this.clearAllReconnectTimers();
|
||||||
|
|
||||||
this.io.to(this.id).emit('gameOver', {
|
this.io.to(this.id).emit('gameOver', {
|
||||||
@ -634,7 +644,7 @@ class GameInstance {
|
|||||||
const winnerCharKey = this.gameState[winnerRole]?.characterKey;
|
const winnerCharKey = this.gameState[winnerRole]?.characterKey;
|
||||||
|
|
||||||
this.gameState.isGameOver = true;
|
this.gameState.isGameOver = true;
|
||||||
if(this.turnTimer.isActive()) this.turnTimer.clear();
|
if(this.turnTimer.isActive() || this.turnTimer.isPaused()) this.turnTimer.clear();
|
||||||
this.clearAllReconnectTimers();
|
this.clearAllReconnectTimers();
|
||||||
|
|
||||||
this.addToLog(`🏳️ ${surrenderedPlayerName} сдался! ${winnerName} объявляется победителем!`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
this.addToLog(`🏳️ ${surrenderedPlayerName} сдался! ${winnerName} объявляется победителем!`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||||
@ -654,8 +664,10 @@ class GameInstance {
|
|||||||
|
|
||||||
handleTurnTimeout() {
|
handleTurnTimeout() {
|
||||||
if (!this.gameState || this.gameState.isGameOver) return;
|
if (!this.gameState || this.gameState.isGameOver) return;
|
||||||
console.log(`[GameInstance ${this.id}] Произошел таймаут хода.`);
|
console.log(`[GameInstance ${this.id}] Произошел таймаут хода (вызван из TurnTimer).`);
|
||||||
const timedOutPlayerRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
|
const timedOutPlayerRole = this.turnTimer.isConfiguredForPlayerSlotTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
|
||||||
|
// Используем isConfiguredForPlayerSlotTurn из таймера, т.к. gameState.isPlayerTurn мог измениться до фактического вызова этого коллбэка
|
||||||
|
// или если таймаут произошел во время "думания" AI (хотя таймер AI не должен вызывать этот коллбэк для игрока).
|
||||||
|
|
||||||
const winnerPlayerRoleIfHuman = timedOutPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
const winnerPlayerRoleIfHuman = timedOutPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||||||
let winnerActuallyExists = false;
|
let winnerActuallyExists = false;
|
||||||
@ -670,6 +682,7 @@ class GameInstance {
|
|||||||
const result = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode, 'turn_timeout', winnerActuallyExists ? winnerPlayerRoleIfHuman : null, timedOutPlayerRole);
|
const result = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode, 'turn_timeout', winnerActuallyExists ? winnerPlayerRoleIfHuman : null, timedOutPlayerRole);
|
||||||
|
|
||||||
this.gameState.isGameOver = true;
|
this.gameState.isGameOver = true;
|
||||||
|
// turnTimer.clear() уже должен был быть вызван внутри TurnTimer перед onTimeoutCallback, или будет вызван в checkGameOver
|
||||||
this.clearAllReconnectTimers();
|
this.clearAllReconnectTimers();
|
||||||
|
|
||||||
this.addToLog(result.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
this.addToLog(result.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||||
@ -694,7 +707,7 @@ class GameInstance {
|
|||||||
this.gameState = { isGameOver: true, player: {}, opponent: {}, turnNumber: 0, gameMode: this.mode };
|
this.gameState = { isGameOver: true, player: {}, opponent: {}, turnNumber: 0, gameMode: this.mode };
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.turnTimer.isActive()) this.turnTimer.clear();
|
if(this.turnTimer.isActive() || this.turnTimer.isPaused()) this.turnTimer.clear();
|
||||||
this.clearAllReconnectTimers();
|
this.clearAllReconnectTimers();
|
||||||
|
|
||||||
this.addToLog(`Критическая ошибка сервера: ${logMessage}. Игра будет завершена.`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
this.addToLog(`Критическая ошибка сервера: ${logMessage}. Игра будет завершена.`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||||
@ -711,8 +724,6 @@ class GameInstance {
|
|||||||
addToLog(message, type = GAME_CONFIG.LOG_TYPE_INFO) {
|
addToLog(message, type = GAME_CONFIG.LOG_TYPE_INFO) {
|
||||||
if (!message) return;
|
if (!message) return;
|
||||||
this.logBuffer.push({ message, type, timestamp: Date.now() });
|
this.logBuffer.push({ message, type, timestamp: Date.now() });
|
||||||
// Раскомментируйте для немедленной отправки логов, если нужно (но обычно лучше батчинг)
|
|
||||||
// this.broadcastLogUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
consumeLogBuffer() {
|
consumeLogBuffer() {
|
||||||
@ -730,7 +741,7 @@ class GameInstance {
|
|||||||
console.warn(`[GameInstance ${this.id}] broadcastGameStateUpdate: gameState отсутствует.`);
|
console.warn(`[GameInstance ${this.id}] broadcastGameStateUpdate: gameState отсутствует.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(`[GameInstance ${this.id}] Отправка gameStateUpdate. IsPlayerTurn: ${this.gameState.isPlayerTurn}`);
|
console.log(`[GameInstance ${this.id}] Отправка gameStateUpdate. IsPlayerTurn(GS): ${this.gameState.isPlayerTurn}`);
|
||||||
this.io.to(this.id).emit('gameStateUpdate', { gameState: this.gameState, log: this.consumeLogBuffer() });
|
this.io.to(this.id).emit('gameStateUpdate', { gameState: this.gameState, log: this.consumeLogBuffer() });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -740,7 +751,7 @@ class GameInstance {
|
|||||||
if (systemLogs.length > 0) {
|
if (systemLogs.length > 0) {
|
||||||
this.io.to(this.id).emit('logUpdate', { log: systemLogs });
|
this.io.to(this.id).emit('logUpdate', { log: systemLogs });
|
||||||
}
|
}
|
||||||
this.logBuffer = this.logBuffer.filter(log => log.type !== GAME_CONFIG.LOG_TYPE_SYSTEM); // Оставляем несистемные
|
this.logBuffer = this.logBuffer.filter(log => log.type !== GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.logBuffer.length > 0) {
|
if (this.logBuffer.length > 0) {
|
||||||
|
@ -29,14 +29,13 @@ class PlayerConnectionHandler {
|
|||||||
socket.emit('gameError', { message: 'Эта игра уже завершена.' });
|
socket.emit('gameError', { message: 'Эта игра уже завершена.' });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Если игрок уже есть, и это не временное отключение, и сокет другой - это F5 или новая вкладка.
|
// Делегируем handlePlayerReconnected, который разберется, новый ли это сокет или тот же.
|
||||||
// GameManager должен был направить на handleRequestGameState, который вызовет handlePlayerReconnected.
|
|
||||||
// Прямой addPlayer в этом случае - редкий сценарий, но handlePlayerReconnected его обработает.
|
|
||||||
return this.handlePlayerReconnected(existingPlayerByIdentifier.id, socket);
|
return this.handlePlayerReconnected(existingPlayerByIdentifier.id, socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(this.players).length >= 2 && this.playerCount >=2 && this.mode === 'pvp') { // В AI режиме только 1 человек
|
// Проверка на максимальное количество игроков
|
||||||
socket.emit('gameError', { message: 'Эта игра уже заполнена.' });
|
if (this.mode === 'pvp' && this.playerCount >= 2) {
|
||||||
|
socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (this.mode === 'ai' && this.playerCount >= 1) {
|
if (this.mode === 'ai' && this.playerCount >= 1) {
|
||||||
@ -44,16 +43,11 @@ class PlayerConnectionHandler {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let assignedPlayerId;
|
let assignedPlayerId;
|
||||||
let actualCharacterKey = chosenCharacterKey || 'elena';
|
let actualCharacterKey = chosenCharacterKey || 'elena';
|
||||||
const charData = dataUtils.getCharacterData(actualCharacterKey);
|
const charDataForName = dataUtils.getCharacterData(actualCharacterKey); // Для имени
|
||||||
|
|
||||||
if (this.mode === 'ai') {
|
if (this.mode === 'ai') {
|
||||||
// if (this.playerSockets[GAME_CONFIG.PLAYER_ID]) { // Эта проверка уже покрыта playerCount >= 1 выше
|
|
||||||
// socket.emit('gameError', { message: 'Нельзя присоединиться к AI игре как второй игрок.' });
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
|
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
|
||||||
} else { // pvp
|
} else { // pvp
|
||||||
if (!this.playerSockets[GAME_CONFIG.PLAYER_ID]) {
|
if (!this.playerSockets[GAME_CONFIG.PLAYER_ID]) {
|
||||||
@ -62,26 +56,34 @@ class PlayerConnectionHandler {
|
|||||||
assignedPlayerId = GAME_CONFIG.OPPONENT_ID;
|
assignedPlayerId = GAME_CONFIG.OPPONENT_ID;
|
||||||
const firstPlayerInfo = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
const firstPlayerInfo = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||||||
if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey === actualCharacterKey) {
|
if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey === actualCharacterKey) {
|
||||||
if (actualCharacterKey === 'elena') actualCharacterKey = 'almagest';
|
const allKeys = dataUtils.getAllCharacterKeys ? dataUtils.getAllCharacterKeys() : ['elena', 'almagest', 'balard'];
|
||||||
else if (actualCharacterKey === 'almagest') actualCharacterKey = 'elena';
|
const otherKey = allKeys.find(k => k !== firstPlayerInfo.chosenCharacterKey && k !== 'balard'); // Не даем Баларда второму игроку по умолчанию
|
||||||
else actualCharacterKey = dataUtils.getAllCharacterKeys().find(k => k !== firstPlayerInfo.chosenCharacterKey) || 'elena';
|
actualCharacterKey = otherKey || (actualCharacterKey === 'elena' ? 'almagest' : 'elena'); // Фоллбэк
|
||||||
}
|
}
|
||||||
} else { // Оба слота заняты, но playerCount мог быть < 2 если кто-то в процессе дисконнекта
|
} else {
|
||||||
socket.emit('gameError', { message: 'Не удалось найти свободный слот в PvP игре (возможно, все заняты или в процессе переподключения).' });
|
socket.emit('gameError', { message: 'Не удалось найти свободный слот в PvP игре.' });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если для этой роли УЖЕ был игрок (например, старый сокет при F5 до того, как сработал disconnect),
|
// Удаление старой записи, если сокет для этой роли уже существует, но с другим ID
|
||||||
// то handlePlayerReconnected должен был бы это обработать. Этот блок здесь - подстраховка,
|
// (на случай очень быстрой смены сокета до срабатывания disconnect)
|
||||||
// если addPlayer вызван напрямую в таком редком случае.
|
const oldPlayerSocketEntry = Object.entries(this.players).find(([sid, pInfo]) => pInfo.id === assignedPlayerId);
|
||||||
const oldPlayerSocketIdForRole = Object.keys(this.players).find(sid => this.players[sid].id === assignedPlayerId && this.players[sid].socket?.id !== socket.id);
|
if (oldPlayerSocketEntry) {
|
||||||
if (oldPlayerSocketIdForRole) {
|
const [oldSocketId, oldPlayerInfo] = oldPlayerSocketEntry;
|
||||||
const oldPlayerInfo = this.players[oldPlayerSocketIdForRole];
|
if (oldPlayerInfo.socket && oldPlayerInfo.socket.id !== socket.id) {
|
||||||
console.warn(`[PCH ${this.gameId}] addPlayer: Найден старый сокет ${oldPlayerInfo.socket?.id} для роли ${assignedPlayerId}. Удаляем его запись.`);
|
console.warn(`[PCH ${this.gameId}] addPlayer: Найдена старая запись для роли ${assignedPlayerId} с сокетом ${oldPlayerInfo.socket.id}. Новый сокет: ${socket.id}. Удаляем старую запись.`);
|
||||||
if(oldPlayerInfo.socket) { try { oldPlayerInfo.socket.leave(this.gameId); oldPlayerInfo.socket.disconnect(true); } catch(e){} }
|
try {
|
||||||
delete this.players[oldPlayerSocketIdForRole];
|
if (oldPlayerInfo.socket.connected) oldPlayerInfo.socket.disconnect(true);
|
||||||
|
} catch (e) { console.error(`[PCH ${this.gameId}] Ошибка при дисконнекте старого сокета: ${e.message}`); }
|
||||||
|
delete this.players[oldSocketId];
|
||||||
|
if (this.playerSockets[assignedPlayerId] === oldPlayerInfo.socket) {
|
||||||
|
delete this.playerSockets[assignedPlayerId];
|
||||||
}
|
}
|
||||||
|
// Не уменьшаем playerCount здесь, так как это замена, а не уход
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
this.players[socket.id] = {
|
this.players[socket.id] = {
|
||||||
id: assignedPlayerId,
|
id: assignedPlayerId,
|
||||||
@ -89,12 +91,18 @@ class PlayerConnectionHandler {
|
|||||||
chosenCharacterKey: actualCharacterKey,
|
chosenCharacterKey: actualCharacterKey,
|
||||||
identifier: identifier,
|
identifier: identifier,
|
||||||
isTemporarilyDisconnected: false,
|
isTemporarilyDisconnected: false,
|
||||||
name: charData?.baseStats?.name || actualCharacterKey
|
name: charDataForName?.baseStats?.name || actualCharacterKey
|
||||||
};
|
};
|
||||||
this.playerSockets[assignedPlayerId] = socket;
|
this.playerSockets[assignedPlayerId] = socket;
|
||||||
this.playerCount++;
|
this.playerCount++; // Увеличиваем счетчик активных игроков
|
||||||
|
|
||||||
|
try {
|
||||||
socket.join(this.gameId);
|
socket.join(this.gameId);
|
||||||
console.log(`[PCH ${this.gameId}] Сокет ${socket.id} присоединен к комнате ${this.gameId} (addPlayer).`);
|
console.log(`[PCH ${this.gameId}] Сокет ${socket.id} присоединен к комнате ${this.gameId} (addPlayer).`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[PCH ${this.gameId}] КРИТИЧЕСКАЯ ОШИБКА при socket.join: ${e.message}. Игрок ${identifier} может не получать широковещательные сообщения.`);
|
||||||
|
// Возможно, стоит откатить добавление игрока или вернуть false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (assignedPlayerId === GAME_CONFIG.PLAYER_ID) this.gameInstance.setPlayerCharacterKey(actualCharacterKey);
|
if (assignedPlayerId === GAME_CONFIG.PLAYER_ID) this.gameInstance.setPlayerCharacterKey(actualCharacterKey);
|
||||||
@ -116,7 +124,7 @@ class PlayerConnectionHandler {
|
|||||||
console.log(`[PCH ${this.gameId}] Окончательное удаление игрока ${playerIdentifier} (Socket: ${socketId}, Role: ${playerRole}). Причина: ${reason}.`);
|
console.log(`[PCH ${this.gameId}] Окончательное удаление игрока ${playerIdentifier} (Socket: ${socketId}, Role: ${playerRole}). Причина: ${reason}.`);
|
||||||
|
|
||||||
if (playerInfo.socket) {
|
if (playerInfo.socket) {
|
||||||
try { playerInfo.socket.leave(this.gameId); } catch (e) { console.warn(`[PCH ${this.gameId}] Ошибка при playerInfo.socket.leave: ${e.message}`); }
|
try { playerInfo.socket.leave(this.gameId); } catch (e) { console.warn(`[PCH ${this.gameId}] Ошибка при playerInfo.socket.leave в removePlayer: ${e.message}`); }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!playerInfo.isTemporarilyDisconnected) {
|
if (!playerInfo.isTemporarilyDisconnected) {
|
||||||
@ -143,19 +151,17 @@ class PlayerConnectionHandler {
|
|||||||
|
|
||||||
if (!playerEntry || !playerEntry.socket) {
|
if (!playerEntry || !playerEntry.socket) {
|
||||||
console.warn(`[PCH ${this.gameId}] Запись игрока или сокет не найдены для ${identifier} (роль ${playerIdRole}) во время потенциального выхода. disconnectedSocketId: ${disconnectedSocketId}`);
|
console.warn(`[PCH ${this.gameId}] Запись игрока или сокет не найдены для ${identifier} (роль ${playerIdRole}) во время потенциального выхода. disconnectedSocketId: ${disconnectedSocketId}`);
|
||||||
// Если записи нет, возможно, игрок уже удален или это был очень старый сокет.
|
|
||||||
// Проверим, есть ли запись по disconnectedSocketId, и если да, удалим ее.
|
|
||||||
if (this.players[disconnectedSocketId]) {
|
if (this.players[disconnectedSocketId]) {
|
||||||
console.warn(`[PCH ${this.gameId}] Найдена запись по disconnectedSocketId ${disconnectedSocketId}, удаляем ее.`);
|
console.warn(`[PCH ${this.gameId}] Найдена запись по disconnectedSocketId ${disconnectedSocketId} (без playerEntry по роли/id), удаляем ее.`);
|
||||||
this.removePlayer(disconnectedSocketId, 'stale_socket_disconnect_no_entry');
|
this.removePlayer(disconnectedSocketId, 'stale_socket_disconnect_no_main_entry');
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playerEntry.socket.id !== disconnectedSocketId) {
|
if (playerEntry.socket.id !== disconnectedSocketId) {
|
||||||
console.log(`[PCH ${this.gameId}] Событие отключения для УСТАРЕВШЕГО сокета ${disconnectedSocketId} для игрока ${identifier} (Роль ${playerIdRole}). Текущий активный сокет: ${playerEntry.socket.id}. Игрок, вероятно, уже переподключился или сессия обновлена. Игнорируем дальнейшую логику "потенциального выхода" для этого устаревшего сокета.`);
|
console.log(`[PCH ${this.gameId}] Событие отключения для УСТАРЕВШЕГО сокета ${disconnectedSocketId} для игрока ${identifier} (Роль ${playerIdRole}). Текущий активный сокет: ${playerEntry.socket.id}. Игнорируем.`);
|
||||||
if (this.players[disconnectedSocketId]) {
|
if (this.players[disconnectedSocketId]) {
|
||||||
delete this.players[disconnectedSocketId]; // Удаляем только эту запись, не вызываем полный removePlayer
|
delete this.players[disconnectedSocketId];
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -170,7 +176,7 @@ class PlayerConnectionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
playerEntry.isTemporarilyDisconnected = true;
|
playerEntry.isTemporarilyDisconnected = true;
|
||||||
this.playerCount--;
|
this.playerCount--; // Уменьшаем счетчик активных
|
||||||
console.log(`[PCH ${this.gameId}] Игрок ${identifier} (роль ${playerIdRole}, сокет ${disconnectedSocketId}) временно отключен. Активных: ${this.playerCount}. Запускаем таймер переподключения.`);
|
console.log(`[PCH ${this.gameId}] Игрок ${identifier} (роль ${playerIdRole}, сокет ${disconnectedSocketId}) временно отключен. Активных: ${this.playerCount}. Запускаем таймер переподключения.`);
|
||||||
|
|
||||||
const disconnectedName = playerEntry.name || this.gameInstance.gameState?.[playerIdRole]?.name || characterKey || `Игрок (Роль ${playerIdRole})`;
|
const disconnectedName = playerEntry.name || this.gameInstance.gameState?.[playerIdRole]?.name || characterKey || `Игрок (Роль ${playerIdRole})`;
|
||||||
@ -188,22 +194,30 @@ class PlayerConnectionHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.gameInstance.turnTimer && (this.gameInstance.turnTimer.isActive() || (this.mode === 'ai' && this.gameInstance.turnTimer.isConfiguredForAiMove))) {
|
if (this.gameInstance.turnTimer && (this.gameInstance.turnTimer.isActive() || this.gameInstance.turnTimer.getIsConfiguredForAiMove?.())) {
|
||||||
this.pausedTurnState = this.gameInstance.turnTimer.pause();
|
this.pausedTurnState = this.gameInstance.turnTimer.pause();
|
||||||
console.log(`[PCH ${this.gameId}] Таймер хода приостановлен из-за отключения. Состояние:`, JSON.stringify(this.pausedTurnState));
|
console.log(`[PCH ${this.gameId}] Таймер хода приостановлен из-за отключения. Состояние:`, JSON.stringify(this.pausedTurnState));
|
||||||
} else {
|
} else {
|
||||||
this.pausedTurnState = null;
|
this.pausedTurnState = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.clearReconnectTimer(playerIdRole);
|
this.clearReconnectTimer(playerIdRole); // Очищаем старый, если был
|
||||||
const reconnectDuration = GAME_CONFIG.RECONNECT_TIMEOUT_MS || 30000;
|
const reconnectDuration = GAME_CONFIG.RECONNECT_TIMEOUT_MS || 30000;
|
||||||
const reconnectStartTime = Date.now();
|
const reconnectStartTime = Date.now();
|
||||||
|
|
||||||
const updateInterval = setInterval(() => {
|
const updateInterval = setInterval(() => {
|
||||||
|
const timerInfo = this.reconnectTimers[playerIdRole];
|
||||||
|
if (!timerInfo || timerInfo.timerId === null) { // Если основной таймер уже сработал/очищен
|
||||||
|
if (timerInfo?.updateIntervalId) clearInterval(timerInfo.updateIntervalId);
|
||||||
|
if (timerInfo) timerInfo.updateIntervalId = null;
|
||||||
|
this.io.to(this.gameId).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: 0 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const remaining = reconnectDuration - (Date.now() - reconnectStartTime);
|
const remaining = reconnectDuration - (Date.now() - reconnectStartTime);
|
||||||
if (remaining <= 0 || !this.reconnectTimers[playerIdRole] || this.reconnectTimers[playerIdRole]?.timerId === null) { // Добавлена проверка на существование таймера
|
if (remaining <= 0) {
|
||||||
if (this.reconnectTimers[playerIdRole]?.updateIntervalId) clearInterval(this.reconnectTimers[playerIdRole].updateIntervalId);
|
// Даем основному setTimeout сработать, здесь просто останавливаем интервал тиков
|
||||||
if (this.reconnectTimers[playerIdRole]) this.reconnectTimers[playerIdRole].updateIntervalId = null; // Помечаем, что интервал очищен
|
clearInterval(timerInfo.updateIntervalId);
|
||||||
|
timerInfo.updateIntervalId = null;
|
||||||
this.io.to(this.gameId).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: 0 });
|
this.io.to(this.gameId).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: 0 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -211,15 +225,24 @@ class PlayerConnectionHandler {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
if (this.reconnectTimers[playerIdRole]?.updateIntervalId) { // Очищаем интервал, если он еще существует
|
const timerInfo = this.reconnectTimers[playerIdRole];
|
||||||
clearInterval(this.reconnectTimers[playerIdRole].updateIntervalId);
|
if (timerInfo?.updateIntervalId) {
|
||||||
this.reconnectTimers[playerIdRole].updateIntervalId = null;
|
clearInterval(timerInfo.updateIntervalId);
|
||||||
|
timerInfo.updateIntervalId = null;
|
||||||
}
|
}
|
||||||
this.reconnectTimers[playerIdRole].timerId = null; // Помечаем, что основной таймаут сработал или очищен
|
if (timerInfo) timerInfo.timerId = null; // Помечаем, что сработал
|
||||||
|
|
||||||
|
// this.clearReconnectTimer(playerIdRole) здесь вызовет сам себя рекурсивно, если удалить delete this.reconnectTimers[playerIdRole];
|
||||||
|
// Поэтому просто удаляем запись, т.к. таймеры уже очищены или помечены.
|
||||||
|
if (this.reconnectTimers[playerIdRole]) delete this.reconnectTimers[playerIdRole];
|
||||||
|
|
||||||
|
|
||||||
const stillDiscPlayer = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier);
|
const stillDiscPlayer = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier);
|
||||||
if (stillDiscPlayer && stillDiscPlayer.isTemporarilyDisconnected) {
|
if (stillDiscPlayer && stillDiscPlayer.isTemporarilyDisconnected) {
|
||||||
|
console.log(`[PCH ${this.gameId}] Таймаут переподключения для ${identifier}. Удаляем игрока.`);
|
||||||
this.removePlayer(stillDiscPlayer.socket.id, "reconnect_timeout");
|
this.removePlayer(stillDiscPlayer.socket.id, "reconnect_timeout");
|
||||||
|
} else {
|
||||||
|
console.log(`[PCH ${this.gameId}] Таймаут переподключения для ${identifier}, но игрок уже не (или не был) isTemporarilyDisconnected.`);
|
||||||
}
|
}
|
||||||
}, reconnectDuration);
|
}, reconnectDuration);
|
||||||
this.reconnectTimers[playerIdRole] = { timerId: timeoutId, updateIntervalId: updateInterval, startTimeMs: reconnectStartTime, durationMs: reconnectDuration };
|
this.reconnectTimers[playerIdRole] = { timerId: timeoutId, updateIntervalId: updateInterval, startTimeMs: reconnectStartTime, durationMs: reconnectDuration };
|
||||||
@ -239,41 +262,41 @@ class PlayerConnectionHandler {
|
|||||||
|
|
||||||
if (playerEntry) {
|
if (playerEntry) {
|
||||||
const oldSocket = playerEntry.socket;
|
const oldSocket = playerEntry.socket;
|
||||||
|
const wasTemporarilyDisconnected = playerEntry.isTemporarilyDisconnected;
|
||||||
|
|
||||||
// Обновляем сокет в playerEntry и в this.players / this.playerSockets, если сокет новый
|
|
||||||
if (oldSocket && oldSocket.id !== newSocket.id) {
|
if (oldSocket && oldSocket.id !== newSocket.id) {
|
||||||
console.log(`[PCH ${this.gameId}] New socket ${newSocket.id} for player ${identifier}. Old socket: ${oldSocket.id}. Updating records.`);
|
console.log(`[PCH ${this.gameId}] Новый сокет ${newSocket.id} для игрока ${identifier}. Старый сокет: ${oldSocket.id}. Обновляем записи.`);
|
||||||
if (this.players[oldSocket.id]) delete this.players[oldSocket.id]; // Удаляем старую запись по старому socket.id
|
if (this.players[oldSocket.id]) delete this.players[oldSocket.id];
|
||||||
if (oldSocket.connected) { // Пытаемся корректно закрыть старый сокет
|
if (oldSocket.connected) {
|
||||||
console.log(`[PCH ${this.gameId}] Disconnecting old stale socket ${oldSocket.id}.`);
|
console.log(`[PCH ${this.gameId}] Отключаем старый "подвисший" сокет ${oldSocket.id}.`);
|
||||||
oldSocket.disconnect(true);
|
oldSocket.disconnect(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
playerEntry.socket = newSocket; // Обновляем сокет в существующей playerEntry
|
playerEntry.socket = newSocket;
|
||||||
this.players[newSocket.id] = playerEntry; // Убеждаемся, что по новому ID есть актуальная запись
|
this.players[newSocket.id] = playerEntry; // Обновляем/добавляем запись с новым socket.id
|
||||||
|
// Если старый ID был ключом для playerEntry, и он не равен newSocket.id, удаляем старый ключ
|
||||||
if (oldSocket && oldSocket.id !== newSocket.id && this.players[oldSocket.id] === playerEntry) {
|
if (oldSocket && oldSocket.id !== newSocket.id && this.players[oldSocket.id] === playerEntry) {
|
||||||
// Если вдруг playerEntry был взят по старому socket.id, и этот ID теперь должен быть удален
|
|
||||||
delete this.players[oldSocket.id];
|
delete this.players[oldSocket.id];
|
||||||
}
|
}
|
||||||
this.playerSockets[playerIdRole] = newSocket; // Обновляем авторитетный сокет для роли
|
this.playerSockets[playerIdRole] = newSocket;
|
||||||
|
|
||||||
// Всегда заново присоединяем сокет к комнате
|
try {
|
||||||
console.log(`[PCH ${this.gameId}] Forcing newSocket ${newSocket.id} (identifier: ${identifier}) to join room ${this.gameId} during reconnect.`);
|
|
||||||
newSocket.join(this.gameId);
|
newSocket.join(this.gameId);
|
||||||
|
console.log(`[PCH ${this.gameId}] Сокет ${newSocket.id} (identifier: ${identifier}) присоединен/переприсоединен к комнате ${this.gameId} (handlePlayerReconnected).`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[PCH ${this.gameId}] КРИТИЧЕСКАЯ ОШИБКА при newSocket.join в handlePlayerReconnected: ${e.message}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (playerEntry.isTemporarilyDisconnected) {
|
if (wasTemporarilyDisconnected) {
|
||||||
console.log(`[PCH ${this.gameId}] Переподключение игрока ${identifier} (Роль: ${playerIdRole}), который был временно отключен.`);
|
console.log(`[PCH ${this.gameId}] Переподключение игрока ${identifier} (Роль: ${playerIdRole}), который был временно отключен.`);
|
||||||
this.clearReconnectTimer(playerIdRole); // Очищаем таймер реконнекта
|
this.clearReconnectTimer(playerIdRole);
|
||||||
this.io.to(this.gameId).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: null }); // Сообщаем UI, что таймер остановлен
|
this.io.to(this.gameId).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: null });
|
||||||
|
|
||||||
playerEntry.isTemporarilyDisconnected = false;
|
playerEntry.isTemporarilyDisconnected = false;
|
||||||
this.playerCount++; // Восстанавливаем счетчик активных игроков
|
this.playerCount++;
|
||||||
} else {
|
} else {
|
||||||
// Игрок не был помечен как временно отключенный.
|
console.log(`[PCH ${this.gameId}] Игрок ${identifier} (Роль: ${playerIdRole}) переподключился/запросил состояние, не будучи помеченным как 'temporarilyDisconnected'. Старый сокет ID: ${oldSocket?.id}, Новый сокет ID: ${newSocket.id}`);
|
||||||
// Это может быть F5 или запрос состояния на "том же" (или новом, но старый не отвалился) сокете.
|
|
||||||
// playerCount не меняется, т.к. игрок считался активным.
|
|
||||||
console.log(`[PCH ${this.gameId}] Игрок ${identifier} (Роль: ${playerIdRole}) переподключился/запросил состояние, не будучи помеченным как 'temporarilyDisconnected'. Old socket ID: ${oldSocket?.id}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновление имени
|
// Обновление имени
|
||||||
@ -288,28 +311,38 @@ class PlayerConnectionHandler {
|
|||||||
this.gameInstance.addToLog(`🔌 Игрок ${playerEntry.name || identifier} снова в игре! (Сессия обновлена)`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
this.gameInstance.addToLog(`🔌 Игрок ${playerEntry.name || identifier} снова в игре! (Сессия обновлена)`, GAME_CONFIG.LOG_TYPE_SYSTEM);
|
||||||
this.sendFullGameStateOnReconnect(newSocket, playerEntry, playerIdRole);
|
this.sendFullGameStateOnReconnect(newSocket, playerEntry, playerIdRole);
|
||||||
|
|
||||||
if (playerEntry.isTemporarilyDisconnected === false && this.pausedTurnState) { // Если игрок был временно отключен, isTemporarilyDisconnected уже false
|
// Логика возобновления игры/таймера
|
||||||
|
if (wasTemporarilyDisconnected && this.pausedTurnState) {
|
||||||
this.resumeGameLogicAfterReconnect(playerIdRole);
|
this.resumeGameLogicAfterReconnect(playerIdRole);
|
||||||
} else if (playerEntry.isTemporarilyDisconnected === false && !this.pausedTurnState) {
|
} else if (!wasTemporarilyDisconnected) {
|
||||||
// Игрок не был temp disconnected, и не было сохраненного состояния таймера (значит, он и не останавливался из-за этого игрока)
|
// Игрок не был temp disconnected. Таймер на сервере, если шел, то продолжал идти.
|
||||||
// Просто отправляем текущее состояние таймера, если он активен
|
// Клиент получил новое состояние. Нужно, чтобы он начал получать обновления таймера.
|
||||||
console.log(`[PCH ${this.gameId}] Player was not temp disconnected, and no pausedTurnState. Forcing timer update if active.`);
|
// Принудительный join выше должен был помочь.
|
||||||
if (this.gameInstance.turnTimer && this.gameInstance.turnTimer.isActive() && this.gameInstance.turnTimer.onTickCallback) {
|
// Дополнительно заставим таймер отправить текущее состояние.
|
||||||
|
console.log(`[PCH ${this.gameId}] Player was not temp disconnected. Forcing timer update if active (for socket ${newSocket.id}).`);
|
||||||
|
if (this.gameInstance.turnTimer && this.gameInstance.turnTimer.onTickCallback) {
|
||||||
const tt = this.gameInstance.turnTimer;
|
const tt = this.gameInstance.turnTimer;
|
||||||
|
// Если таймер реально работает (не ход AI и не на ручной паузе от другого игрока)
|
||||||
|
if (tt.isCurrentlyRunning && !tt.isManuallyPausedState && !tt.isConfiguredForAiMove) {
|
||||||
const elapsedTime = Date.now() - tt.segmentStartTimeMs;
|
const elapsedTime = Date.now() - tt.segmentStartTimeMs;
|
||||||
const currentRemaining = Math.max(0, tt.segmentDurationMs - elapsedTime);
|
const currentRemaining = Math.max(0, tt.segmentDurationMs - elapsedTime);
|
||||||
|
console.log(`[PCH ${this.gameId}] Forcing onTickCallback. Remaining: ${currentRemaining}, ForPlayer: ${tt.isConfiguredForPlayerSlotTurn}, ManualPause: ${tt.isManuallyPausedState}`);
|
||||||
tt.onTickCallback(currentRemaining, tt.isConfiguredForPlayerSlotTurn, tt.isManuallyPausedState);
|
tt.onTickCallback(currentRemaining, tt.isConfiguredForPlayerSlotTurn, tt.isManuallyPausedState);
|
||||||
} else if (this.gameInstance.turnTimer && !this.gameInstance.turnTimer.isActive() && !this.gameInstance.turnTimer.isPaused() && !this.isGameEffectivelyPaused()) {
|
} else if (tt.isConfiguredForAiMove && !tt.isCurrentlyRunning) { // Если ход AI
|
||||||
// Если таймер не активен, не на паузе, и игра не на общей паузе - возможно, его нужно запустить (если сейчас ход этого игрока)
|
console.log(`[PCH ${this.gameId}] Forcing onTickCallback for AI move state.`);
|
||||||
|
tt.onTickCallback(tt.initialTurnDurationMs, tt.isConfiguredForPlayerSlotTurn, false);
|
||||||
|
} else if (tt.isManuallyPausedState) { // Если на ручной паузе (из-за другого игрока)
|
||||||
|
console.log(`[PCH ${this.gameId}] Forcing onTickCallback for manually paused state. Remaining: ${tt.segmentDurationMs}`);
|
||||||
|
tt.onTickCallback(tt.segmentDurationMs, tt.isConfiguredForPlayerSlotTurn, true);
|
||||||
|
} else if (!tt.isCurrentlyRunning && !tt.isManuallyPausedState && !this.isGameEffectivelyPaused() && this.gameInstance.gameState && !this.gameInstance.gameState.isGameOver) {
|
||||||
|
// Таймер не работает, не на паузе, игра не на паузе - возможно, его нужно запустить
|
||||||
const gs = this.gameInstance.gameState;
|
const gs = this.gameInstance.gameState;
|
||||||
if (gs && !gs.isGameOver) {
|
|
||||||
const isHisTurnNow = (gs.isPlayerTurn && playerIdRole === GAME_CONFIG.PLAYER_ID) || (!gs.isPlayerTurn && playerIdRole === GAME_CONFIG.OPPONENT_ID);
|
const isHisTurnNow = (gs.isPlayerTurn && playerIdRole === GAME_CONFIG.PLAYER_ID) || (!gs.isPlayerTurn && playerIdRole === GAME_CONFIG.OPPONENT_ID);
|
||||||
const isAiTurnNow = this.mode === 'ai' && !gs.isPlayerTurn;
|
const isAiTurnNow = this.mode === 'ai' && !gs.isPlayerTurn;
|
||||||
if(isHisTurnNow || isAiTurnNow) {
|
if(isHisTurnNow || isAiTurnNow) {
|
||||||
console.log(`[PCH ${this.gameId}] Timer not active, not paused. Game not paused. Attempting to start timer for ${playerIdRole}. HisTurn: ${isHisTurnNow}, AITurn: ${isAiTurnNow}`);
|
console.log(`[PCH ${this.gameId}] Timer not active, attempting to start for ${playerIdRole}. HisTurn: ${isHisTurnNow}, AITurn: ${isAiTurnNow}`);
|
||||||
this.gameInstance.turnTimer.start(gs.isPlayerTurn, isAiTurnNow);
|
this.gameInstance.turnTimer.start(gs.isPlayerTurn, isAiTurnNow);
|
||||||
if (isAiTurnNow && !this.gameInstance.turnTimer.isConfiguredForAiMove && !this.gameInstance.turnTimer.isCurrentlyRunning) {
|
if (isAiTurnNow && !this.gameInstance.turnTimer.getIsConfiguredForAiMove?.()) {
|
||||||
// Доп. проверка, чтобы AI точно пошел, если это его ход и таймер не стартовал для него как "AI move"
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!this.isGameEffectivelyPaused() && this.gameInstance.gameState && !this.gameInstance.gameState.isGameOver && this.mode === 'ai' && !this.gameInstance.gameState.isPlayerTurn) {
|
if (!this.isGameEffectivelyPaused() && this.gameInstance.gameState && !this.gameInstance.gameState.isGameOver && this.mode === 'ai' && !this.gameInstance.gameState.isPlayerTurn) {
|
||||||
this.gameInstance.processAiTurn();
|
this.gameInstance.processAiTurn();
|
||||||
@ -322,11 +355,9 @@ class PlayerConnectionHandler {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} else { // playerEntry не найден
|
} else {
|
||||||
console.warn(`[PCH ${this.gameId}] Попытка переподключения для ${identifier} (Роль ${playerIdRole}), но запись playerEntry не найдена. Это может быть новый игрок или сессия истекла.`);
|
console.warn(`[PCH ${this.gameId}] Попытка переподключения для ${identifier} (Роль ${playerIdRole}), но запись playerEntry не найдена.`);
|
||||||
// Если это новый игрок для этой роли, то addPlayer должен был быть вызван GameManager'ом.
|
newSocket.emit('gameError', { message: 'Не удалось найти вашу игровую сессию. Попробуйте создать игру заново.' });
|
||||||
// Если PCH вызывается напрямую, и игрока нет, это ошибка или устаревший запрос.
|
|
||||||
newSocket.emit('gameError', { message: 'Не удалось восстановить сессию (запись игрока не найдена). Попробуйте создать игру заново.' });
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -335,7 +366,7 @@ class PlayerConnectionHandler {
|
|||||||
console.log(`[PCH SEND_STATE_RECONNECT] gameId: ${this.gameId}, Role: ${playerIdRole}, Identifier: ${playerEntry.identifier}`);
|
console.log(`[PCH SEND_STATE_RECONNECT] gameId: ${this.gameId}, Role: ${playerIdRole}, Identifier: ${playerEntry.identifier}`);
|
||||||
if (!this.gameInstance.gameState) {
|
if (!this.gameInstance.gameState) {
|
||||||
console.log(`[PCH SEND_STATE_RECONNECT] gameState отсутствует, попытка инициализации...`);
|
console.log(`[PCH SEND_STATE_RECONNECT] gameState отсутствует, попытка инициализации...`);
|
||||||
if (!this.gameInstance.initializeGame()) { // initializeGame должен установить gameState
|
if (!this.gameInstance.initializeGame()) {
|
||||||
this.gameInstance._handleCriticalError('reconnect_no_gs_after_init_pch_helper', 'PCH Helper: GS null после повторной инициализации при переподключении.');
|
this.gameInstance._handleCriticalError('reconnect_no_gs_after_init_pch_helper', 'PCH Helper: GS null после повторной инициализации при переподключении.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -344,33 +375,25 @@ class PlayerConnectionHandler {
|
|||||||
|
|
||||||
const pData = dataUtils.getCharacterData(playerEntry.chosenCharacterKey);
|
const pData = dataUtils.getCharacterData(playerEntry.chosenCharacterKey);
|
||||||
const oppRoleKey = playerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
const oppRoleKey = playerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||||||
|
|
||||||
// Получаем ключ оппонента из gameState ИЛИ из сохраненных ключей в GameInstance
|
|
||||||
let oCharKey = this.gameInstance.gameState?.[oppRoleKey]?.characterKey ||
|
let oCharKey = this.gameInstance.gameState?.[oppRoleKey]?.characterKey ||
|
||||||
(playerIdRole === GAME_CONFIG.PLAYER_ID ? this.gameInstance.opponentCharacterKey : this.gameInstance.playerCharacterKey);
|
(playerIdRole === GAME_CONFIG.PLAYER_ID ? this.gameInstance.opponentCharacterKey : this.gameInstance.playerCharacterKey);
|
||||||
const oData = oCharKey ? dataUtils.getCharacterData(oCharKey) : null;
|
const oData = oCharKey ? dataUtils.getCharacterData(oCharKey) : null;
|
||||||
|
|
||||||
// Обновляем имена в gameState на основе сохраненных в PCH или данных персонажей
|
|
||||||
if (this.gameInstance.gameState) {
|
if (this.gameInstance.gameState) {
|
||||||
if (this.gameInstance.gameState[playerIdRole]) {
|
if (this.gameInstance.gameState[playerIdRole]) {
|
||||||
this.gameInstance.gameState[playerIdRole].name = playerEntry.name || pData?.baseStats?.name || 'Игрок';
|
this.gameInstance.gameState[playerIdRole].name = playerEntry.name || pData?.baseStats?.name || 'Игрок';
|
||||||
}
|
}
|
||||||
const opponentPCHEntry = Object.values(this.players).find(p => p.id === oppRoleKey);
|
const opponentPCHEntry = Object.values(this.players).find(p => p.id === oppRoleKey);
|
||||||
if (this.gameInstance.gameState[oppRoleKey]) {
|
if (this.gameInstance.gameState[oppRoleKey]) {
|
||||||
if (opponentPCHEntry?.name) {
|
if (opponentPCHEntry?.name) this.gameInstance.gameState[oppRoleKey].name = opponentPCHEntry.name;
|
||||||
this.gameInstance.gameState[oppRoleKey].name = opponentPCHEntry.name;
|
else if (oData?.baseStats?.name) this.gameInstance.gameState[oppRoleKey].name = oData.baseStats.name;
|
||||||
} else if (oData?.baseStats?.name) {
|
else if (this.mode === 'ai' && oppRoleKey === GAME_CONFIG.OPPONENT_ID) this.gameInstance.gameState[oppRoleKey].name = 'Балард';
|
||||||
this.gameInstance.gameState[oppRoleKey].name = oData.baseStats.name;
|
else this.gameInstance.gameState[oppRoleKey].name = (this.mode === 'pvp' ? 'Ожидание Оппонента...' : 'Противник');
|
||||||
} else if (this.mode === 'ai' && oppRoleKey === GAME_CONFIG.OPPONENT_ID) {
|
|
||||||
this.gameInstance.gameState[oppRoleKey].name = 'Балард'; // Фоллбэк для AI
|
|
||||||
} else {
|
|
||||||
this.gameInstance.gameState[oppRoleKey].name = 'Оппонент';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(`[PCH SEND_STATE_RECONNECT] Отправка gameStarted. Player GS: ${this.gameInstance.gameState?.player?.name}, Opponent GS: ${this.gameInstance.gameState?.opponent?.name}. IsPlayerTurn: ${this.gameInstance.gameState?.isPlayerTurn}`);
|
console.log(`[PCH SEND_STATE_RECONNECT] Отправка gameStarted. Player GS: ${this.gameInstance.gameState?.player?.name}, Opponent GS: ${this.gameInstance.gameState?.opponent?.name}. IsPlayerTurn: ${this.gameInstance.gameState?.isPlayerTurn}`);
|
||||||
|
|
||||||
socket.emit('gameStarted', { // Используем 'gameStarted' для полной синхронизации состояния
|
socket.emit('gameStarted', {
|
||||||
gameId: this.gameId,
|
gameId: this.gameId,
|
||||||
yourPlayerId: playerIdRole,
|
yourPlayerId: playerIdRole,
|
||||||
initialGameState: this.gameInstance.gameState,
|
initialGameState: this.gameInstance.gameState,
|
||||||
@ -378,7 +401,7 @@ class PlayerConnectionHandler {
|
|||||||
opponentBaseStats: oData?.baseStats || {name: (this.mode === 'pvp' ? 'Ожидание...' : 'Противник AI'), maxHp:1, maxResource:0, resourceName:'N/A', attackPower:0, characterKey: null},
|
opponentBaseStats: oData?.baseStats || {name: (this.mode === 'pvp' ? 'Ожидание...' : 'Противник AI'), maxHp:1, maxResource:0, resourceName:'N/A', attackPower:0, characterKey: null},
|
||||||
playerAbilities: pData?.abilities,
|
playerAbilities: pData?.abilities,
|
||||||
opponentAbilities: oData?.abilities || [],
|
opponentAbilities: oData?.abilities || [],
|
||||||
log: this.gameInstance.consumeLogBuffer(),
|
log: this.gameInstance.consumeLogBuffer(), // Отправляем все накопленные логи
|
||||||
clientConfig: { ...GAME_CONFIG }
|
clientConfig: { ...GAME_CONFIG }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -396,19 +419,15 @@ class PlayerConnectionHandler {
|
|||||||
reconnectedPlayerId: reconnectedPlayerIdRole,
|
reconnectedPlayerId: reconnectedPlayerIdRole,
|
||||||
reconnectedPlayerName: reconnectedName
|
reconnectedPlayerName: reconnectedName
|
||||||
});
|
});
|
||||||
if (this.gameInstance.logBuffer.length > 0) { // Отправляем накопившиеся логи другому игроку
|
if (this.gameInstance.logBuffer.length > 0) {
|
||||||
otherSocket.emit('logUpdate', { log: this.gameInstance.consumeLogBuffer() });
|
otherSocket.emit('logUpdate', { log: this.gameInstance.consumeLogBuffer() });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем состояние для всех (включая переподключившегося, т.к. его лог мог быть уже потреблен)
|
this.gameInstance.broadcastGameStateUpdate(); // Обновляем состояние для всех
|
||||||
this.gameInstance.broadcastGameStateUpdate(); // Это отправит gameState и оставшиеся логи
|
|
||||||
|
|
||||||
|
|
||||||
if (!this.isGameEffectivelyPaused() && this.gameInstance.gameState && !this.gameInstance.gameState.isGameOver) {
|
if (!this.isGameEffectivelyPaused() && this.gameInstance.gameState && !this.gameInstance.gameState.isGameOver) {
|
||||||
// this.gameInstance.broadcastGameStateUpdate(); // Перенесено выше
|
if (Object.keys(this.reconnectTimers).length === 0) {
|
||||||
|
|
||||||
if (Object.keys(this.reconnectTimers).length === 0) { // Только если нет других ожидающих реконнекта
|
|
||||||
const currentTurnIsForPlayerInGS = this.gameInstance.gameState.isPlayerTurn;
|
const currentTurnIsForPlayerInGS = this.gameInstance.gameState.isPlayerTurn;
|
||||||
const isCurrentTurnAiForTimer = this.mode === 'ai' && !currentTurnIsForPlayerInGS;
|
const isCurrentTurnAiForTimer = this.mode === 'ai' && !currentTurnIsForPlayerInGS;
|
||||||
let resumedFromPausedState = false;
|
let resumedFromPausedState = false;
|
||||||
@ -421,20 +440,20 @@ class PlayerConnectionHandler {
|
|||||||
console.log(`[PCH ${this.gameId}] Возобновляем таймер хода из pausedTurnState. Время: ${this.pausedTurnState.remainingTime}мс. Для игрока (в pausedState): ${this.pausedTurnState.forPlayerRoleIsPlayer}. GS ход игрока: ${currentTurnIsForPlayerInGS}. AI ход (в pausedState): ${this.pausedTurnState.isAiCurrentlyMoving}`);
|
console.log(`[PCH ${this.gameId}] Возобновляем таймер хода из pausedTurnState. Время: ${this.pausedTurnState.remainingTime}мс. Для игрока (в pausedState): ${this.pausedTurnState.forPlayerRoleIsPlayer}. GS ход игрока: ${currentTurnIsForPlayerInGS}. AI ход (в pausedState): ${this.pausedTurnState.isAiCurrentlyMoving}`);
|
||||||
this.gameInstance.turnTimer.resume(
|
this.gameInstance.turnTimer.resume(
|
||||||
this.pausedTurnState.remainingTime,
|
this.pausedTurnState.remainingTime,
|
||||||
this.pausedTurnState.forPlayerRoleIsPlayer, // Это isConfiguredForPlayerSlotTurn для таймера
|
this.pausedTurnState.forPlayerRoleIsPlayer,
|
||||||
this.pausedTurnState.isAiCurrentlyMoving // Это isConfiguredForAiMove для таймера
|
this.pausedTurnState.isAiCurrentlyMoving
|
||||||
);
|
);
|
||||||
resumedFromPausedState = true;
|
resumedFromPausedState = true;
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[PCH ${this.gameId}] pausedTurnState (${JSON.stringify(this.pausedTurnState)}) не совпадает с текущим ходом в gameState (isPlayerTurn: ${currentTurnIsForPlayerInGS}). Сбрасываем pausedTurnState и запускаем таймер заново, если нужно.`);
|
console.warn(`[PCH ${this.gameId}] pausedTurnState (${JSON.stringify(this.pausedTurnState)}) не совпадает с текущим ходом в gameState (isPlayerTurn: ${currentTurnIsForPlayerInGS}). Сбрасываем pausedTurnState и запускаем таймер заново, если нужно.`);
|
||||||
}
|
}
|
||||||
this.pausedTurnState = null; // Сбрасываем в любом случае
|
this.pausedTurnState = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!resumedFromPausedState && this.gameInstance.turnTimer && !this.gameInstance.turnTimer.isActive() && !this.gameInstance.turnTimer.isPaused()) {
|
if (!resumedFromPausedState && this.gameInstance.turnTimer && !this.gameInstance.turnTimer.isActive() && !this.gameInstance.turnTimer.isPaused()) {
|
||||||
console.log(`[PCH ${this.gameId}] Запускаем таймер хода заново после реконнекта (pausedState не использовался или был неактуален, таймер неактивен и не на паузе). GS ход игрока: ${currentTurnIsForPlayerInGS}. AI ход для таймера: ${isCurrentTurnAiForTimer}`);
|
console.log(`[PCH ${this.gameId}] Запускаем таймер хода заново после реконнекта (pausedState не использовался/неактуален, таймер неактивен и не на паузе). GS ход игрока: ${currentTurnIsForPlayerInGS}. AI ход для таймера: ${isCurrentTurnAiForTimer}`);
|
||||||
this.gameInstance.turnTimer.start(currentTurnIsForPlayerInGS, isCurrentTurnAiForTimer);
|
this.gameInstance.turnTimer.start(currentTurnIsForPlayerInGS, isCurrentTurnAiForTimer);
|
||||||
if (isCurrentTurnAiForTimer && !this.gameInstance.turnTimer.isConfiguredForAiMove && !this.gameInstance.turnTimer.isCurrentlyRunning) {
|
if (isCurrentTurnAiForTimer && !this.gameInstance.turnTimer.getIsConfiguredForAiMove?.()) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!this.isGameEffectivelyPaused() && this.gameInstance.gameState && !this.gameInstance.gameState.isGameOver && this.mode === 'ai' && !this.gameInstance.gameState.isPlayerTurn) {
|
if (!this.isGameEffectivelyPaused() && this.gameInstance.gameState && !this.gameInstance.gameState.isGameOver && this.mode === 'ai' && !this.gameInstance.gameState.isPlayerTurn) {
|
||||||
this.gameInstance.processAiTurn();
|
this.gameInstance.processAiTurn();
|
||||||
@ -454,13 +473,15 @@ class PlayerConnectionHandler {
|
|||||||
|
|
||||||
clearReconnectTimer(playerIdRole) {
|
clearReconnectTimer(playerIdRole) {
|
||||||
if (this.reconnectTimers[playerIdRole]) {
|
if (this.reconnectTimers[playerIdRole]) {
|
||||||
|
if (this.reconnectTimers[playerIdRole].timerId) {
|
||||||
clearTimeout(this.reconnectTimers[playerIdRole].timerId);
|
clearTimeout(this.reconnectTimers[playerIdRole].timerId);
|
||||||
this.reconnectTimers[playerIdRole].timerId = null; // Явно обнуляем
|
this.reconnectTimers[playerIdRole].timerId = null;
|
||||||
|
}
|
||||||
if (this.reconnectTimers[playerIdRole].updateIntervalId) {
|
if (this.reconnectTimers[playerIdRole].updateIntervalId) {
|
||||||
clearInterval(this.reconnectTimers[playerIdRole].updateIntervalId);
|
clearInterval(this.reconnectTimers[playerIdRole].updateIntervalId);
|
||||||
this.reconnectTimers[playerIdRole].updateIntervalId = null; // Явно обнуляем
|
this.reconnectTimers[playerIdRole].updateIntervalId = null;
|
||||||
}
|
}
|
||||||
delete this.reconnectTimers[playerIdRole]; // Удаляем всю запись
|
delete this.reconnectTimers[playerIdRole];
|
||||||
console.log(`[PCH ${this.gameId}] Очищен таймер переподключения для роли ${playerIdRole}.`);
|
console.log(`[PCH ${this.gameId}] Очищен таймер переподключения для роли ${playerIdRole}.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -477,14 +498,13 @@ class PlayerConnectionHandler {
|
|||||||
if (this.playerCount < 2 && Object.keys(this.players).length > 0) {
|
if (this.playerCount < 2 && Object.keys(this.players).length > 0) {
|
||||||
const p1Entry = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
const p1Entry = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||||||
const p2Entry = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID);
|
const p2Entry = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID);
|
||||||
|
|
||||||
if ((p1Entry && p1Entry.isTemporarilyDisconnected) || (p2Entry && p2Entry.isTemporarilyDisconnected)) {
|
if ((p1Entry && p1Entry.isTemporarilyDisconnected) || (p2Entry && p2Entry.isTemporarilyDisconnected)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (this.mode === 'ai') {
|
} else if (this.mode === 'ai') {
|
||||||
const humanPlayer = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
const humanPlayer = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||||||
return humanPlayer?.isTemporarilyDisconnected ?? false; // Если игрока нет, не на паузе. Если есть - зависит от его состояния.
|
return humanPlayer?.isTemporarilyDisconnected ?? false;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,33 @@
|
|||||||
// /server/game/instance/TurnTimer.js
|
// /server/game/instance/TurnTimer.js
|
||||||
|
|
||||||
class TurnTimer {
|
class TurnTimer {
|
||||||
|
/**
|
||||||
|
* Конструктор таймера хода.
|
||||||
|
* @param {number} turnDurationMs - Изначальная длительность хода в миллисекундах.
|
||||||
|
* @param {number} updateIntervalMs - Интервал для отправки обновлений времени клиентам (в мс).
|
||||||
|
* @param {function} onTimeoutCallback - Колбэк, вызываемый при истечении времени хода.
|
||||||
|
* @param {function} onTickCallback - Колбэк, вызываемый на каждом тике обновления (передает remainingTimeMs, isForPlayerSlotTurn_timerPerspective, isTimerEffectivelyPaused_byLogic).
|
||||||
|
* @param {string} [gameIdForLogs=''] - (Опционально) ID игры для более понятных логов таймера.
|
||||||
|
*/
|
||||||
constructor(turnDurationMs, updateIntervalMs, onTimeoutCallback, onTickCallback, gameIdForLogs = '') {
|
constructor(turnDurationMs, updateIntervalMs, onTimeoutCallback, onTickCallback, gameIdForLogs = '') {
|
||||||
this.initialTurnDurationMs = turnDurationMs;
|
this.initialTurnDurationMs = turnDurationMs;
|
||||||
this.updateIntervalMs = updateIntervalMs;
|
this.updateIntervalMs = updateIntervalMs;
|
||||||
this.onTimeoutCallback = onTimeoutCallback;
|
this.onTimeoutCallback = onTimeoutCallback;
|
||||||
this.onTickCallback = onTickCallback; // (remainingTimeMs, isForPlayerSlotTurn_timerPerspective, isTimerEffectivelyPaused_byLogic)
|
this.onTickCallback = onTickCallback;
|
||||||
this.gameId = gameIdForLogs;
|
this.gameId = gameIdForLogs;
|
||||||
|
|
||||||
this.timeoutId = null;
|
this.timeoutId = null; // ID для setTimeout (обработка общего таймаута хода)
|
||||||
this.tickIntervalId = null;
|
this.tickIntervalId = null; // ID для setInterval (периодическое обновление клиента)
|
||||||
|
|
||||||
this.segmentStartTimeMs = 0; // Время начала текущего активного сегмента (после start/resume)
|
this.segmentStartTimeMs = 0; // Время (Date.now()) начала текущего активного сегмента (после start/resume)
|
||||||
this.segmentDurationMs = 0; // Длительность, с которой был запущен текущий сегмент
|
this.segmentDurationMs = 0; // Длительность, с которой был запущен текущий активный сегмент
|
||||||
|
|
||||||
this.isCurrentlyRunning = false; // Идет ли активный отсчет (не на паузе, не ход AI)
|
this.isCurrentlyRunning = false; // Идет ли активный отсчет (не на паузе из-за дисконнекта, не ход AI)
|
||||||
this.isManuallyPausedState = false; // Была ли вызвана pause()
|
this.isManuallyPausedState = false; // Была ли вызвана pause() (например, из-за дисконнекта игрока)
|
||||||
|
|
||||||
// Состояние, для которого таймер был запущен (или должен быть запущен)
|
// Состояние, для которого таймер был сконфигурирован при последнем запуске/возобновлении
|
||||||
this.isConfiguredForPlayerSlotTurn = false;
|
this.isConfiguredForPlayerSlotTurn = false; // true, если таймер отсчитывает ход игрока (слот 'player')
|
||||||
this.isConfiguredForAiMove = false;
|
this.isConfiguredForAiMove = false; // true, если это ход AI (таймер для реального игрока не тикает)
|
||||||
|
|
||||||
console.log(`[TurnTimer ${this.gameId}] Initialized. Duration: ${this.initialTurnDurationMs}ms, Interval: ${this.updateIntervalMs}ms`);
|
console.log(`[TurnTimer ${this.gameId}] Initialized. Duration: ${this.initialTurnDurationMs}ms, Interval: ${this.updateIntervalMs}ms`);
|
||||||
}
|
}
|
||||||
@ -28,10 +36,12 @@ class TurnTimer {
|
|||||||
if (this.timeoutId) {
|
if (this.timeoutId) {
|
||||||
clearTimeout(this.timeoutId);
|
clearTimeout(this.timeoutId);
|
||||||
this.timeoutId = null;
|
this.timeoutId = null;
|
||||||
|
// console.log(`[TurnTimer ${this.gameId}] Cleared timeoutId.`);
|
||||||
}
|
}
|
||||||
if (this.tickIntervalId) {
|
if (this.tickIntervalId) {
|
||||||
clearInterval(this.tickIntervalId);
|
clearInterval(this.tickIntervalId);
|
||||||
this.tickIntervalId = null;
|
this.tickIntervalId = null;
|
||||||
|
// console.log(`[TurnTimer ${this.gameId}] Cleared tickIntervalId.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,47 +49,62 @@ class TurnTimer {
|
|||||||
* Запускает или перезапускает таймер хода.
|
* Запускает или перезапускает таймер хода.
|
||||||
* @param {boolean} isPlayerSlotTurn - true, если сейчас ход слота 'player'.
|
* @param {boolean} isPlayerSlotTurn - true, если сейчас ход слота 'player'.
|
||||||
* @param {boolean} isAiMakingMove - true, если текущий ход делает AI.
|
* @param {boolean} isAiMakingMove - true, если текущий ход делает AI.
|
||||||
* @param {number|null} [customRemainingTimeMs=null] - Если передано, таймер начнется с этого времени.
|
* @param {number|null} [customRemainingTimeMs=null] - Если передано, таймер начнется с этого времени (обычно при resume).
|
||||||
*/
|
*/
|
||||||
start(isPlayerSlotTurn, isAiMakingMove = false, customRemainingTimeMs = null) {
|
start(isPlayerSlotTurn, isAiMakingMove = false, customRemainingTimeMs = null) {
|
||||||
console.log(`[TurnTimer ${this.gameId}] Attempting START. ForPlayer: ${isPlayerSlotTurn}, IsAI: ${isAiMakingMove}, CustomTime: ${customRemainingTimeMs}, ManualPause: ${this.isManuallyPausedState}`);
|
console.log(`[TurnTimer ${this.gameId}] Attempting START. ForPlayer: ${isPlayerSlotTurn}, IsAI: ${isAiMakingMove}, CustomTime: ${customRemainingTimeMs}, CurrentManualPauseState: ${this.isManuallyPausedState}`);
|
||||||
this._clearInternalTimers(); // Всегда очищаем старые таймеры перед новым запуском
|
this._clearInternalTimers(); // Всегда очищаем старые таймеры перед новым запуском
|
||||||
|
|
||||||
this.isConfiguredForPlayerSlotTurn = isPlayerSlotTurn;
|
this.isConfiguredForPlayerSlotTurn = isPlayerSlotTurn;
|
||||||
this.isConfiguredForAiMove = isAiMakingMove;
|
this.isConfiguredForAiMove = isAiMakingMove;
|
||||||
|
|
||||||
// Если это не resume (т.е. customRemainingTimeMs не передан явно как результат pause),
|
// Если start вызывается НЕ из resume (т.е. customRemainingTimeMs не передан как результат pause),
|
||||||
// то сбрасываем флаг ручной паузы.
|
// то флаг ручной паузы должен быть сброшен.
|
||||||
|
// Если это вызов из resume, isManuallyPausedState уже был сброшен в resume перед вызовом start.
|
||||||
if (customRemainingTimeMs === null) {
|
if (customRemainingTimeMs === null) {
|
||||||
this.isManuallyPausedState = false;
|
this.isManuallyPausedState = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isConfiguredForAiMove) {
|
if (this.isConfiguredForAiMove) {
|
||||||
this.isCurrentlyRunning = false; // Для хода AI основной таймер не "бежит" для игрока
|
this.isCurrentlyRunning = false; // Для хода AI основной таймер не "бежит" для UI игрока
|
||||||
console.log(`[TurnTimer ${this.gameId}] START: AI's turn. Player timer not actively ticking.`);
|
this.segmentDurationMs = this.initialTurnDurationMs; // Для AI показываем полную длительность (или сколько он думает)
|
||||||
|
this.segmentStartTimeMs = Date.now(); // На всякий случай, хотя не используется для тиков AI
|
||||||
|
console.log(`[TurnTimer ${this.gameId}] START: AI's turn. Player timer not actively ticking. ManualPause: ${this.isManuallyPausedState}`);
|
||||||
if (this.onTickCallback) {
|
if (this.onTickCallback) {
|
||||||
// Отправляем состояние "ход AI", таймер не тикает для игрока, не на ручной паузе
|
// Отправляем состояние "ход AI", таймер не тикает для игрока, не на ручной паузе (т.к. игра идет)
|
||||||
this.onTickCallback(this.initialTurnDurationMs, this.isConfiguredForPlayerSlotTurn, false);
|
this.onTickCallback(this.initialTurnDurationMs, this.isConfiguredForPlayerSlotTurn, false);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если это не ход AI, то таймер должен работать для игрока (или оппонента-человека)
|
// Если это не ход AI, то таймер должен работать для игрока (или оппонента-человека)
|
||||||
this.segmentDurationMs = (typeof customRemainingTimeMs === 'number' && customRemainingTimeMs > 0)
|
this.segmentDurationMs = (typeof customRemainingTimeMs === 'number' && customRemainingTimeMs >= 0) // Допускаем 0 для немедленного таймаута
|
||||||
? customRemainingTimeMs
|
? customRemainingTimeMs
|
||||||
: this.initialTurnDurationMs;
|
: this.initialTurnDurationMs;
|
||||||
|
|
||||||
this.segmentStartTimeMs = Date.now();
|
this.segmentStartTimeMs = Date.now();
|
||||||
this.isCurrentlyRunning = true; // Таймер теперь активен
|
this.isCurrentlyRunning = true; // Таймер теперь активен
|
||||||
// this.isManuallyPausedState остается как есть, если это был resume, или false, если это новый start
|
|
||||||
|
|
||||||
console.log(`[TurnTimer ${this.gameId}] STARTED. Effective Duration: ${this.segmentDurationMs}ms. ForPlayer: ${this.isConfiguredForPlayerSlotTurn}. IsRunning: ${this.isCurrentlyRunning}. ManualPause: ${this.isManuallyPausedState}`);
|
console.log(`[TurnTimer ${this.gameId}] STARTED. Effective Duration: ${this.segmentDurationMs}ms. ForPlayer: ${this.isConfiguredForPlayerSlotTurn}. IsRunning: ${this.isCurrentlyRunning}. ManualPause: ${this.isManuallyPausedState}`);
|
||||||
|
|
||||||
|
if (this.segmentDurationMs <= 0) { // Если время 0 или меньше, сразу таймаут
|
||||||
|
console.log(`[TurnTimer ${this.gameId}] Start with 0 or less time, calling timeout immediately.`);
|
||||||
|
if (this.onTimeoutCallback) {
|
||||||
|
this.onTimeoutCallback();
|
||||||
|
}
|
||||||
|
this._clearInternalTimers();
|
||||||
|
this.isCurrentlyRunning = false;
|
||||||
|
// Отправляем финальный тик с 0 временем
|
||||||
|
if (this.onTickCallback) {
|
||||||
|
this.onTickCallback(0, this.isConfiguredForPlayerSlotTurn, this.isManuallyPausedState);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.timeoutId = setTimeout(() => {
|
this.timeoutId = setTimeout(() => {
|
||||||
console.log(`[TurnTimer ${this.gameId}] Main TIMEOUT occurred. WasRunning: ${this.isCurrentlyRunning}, ManualPause: ${this.isManuallyPausedState}`);
|
console.log(`[TurnTimer ${this.gameId}] Main TIMEOUT occurred. WasRunning: ${this.isCurrentlyRunning}, ManualPause: ${this.isManuallyPausedState}`);
|
||||||
// Проверяем, что таймер все еще должен был работать и не был на паузе
|
|
||||||
if (this.isCurrentlyRunning && !this.isManuallyPausedState) {
|
if (this.isCurrentlyRunning && !this.isManuallyPausedState) {
|
||||||
this._clearInternalTimers(); // Очищаем все, включая интервал
|
this._clearInternalTimers();
|
||||||
this.isCurrentlyRunning = false;
|
this.isCurrentlyRunning = false;
|
||||||
if (this.onTimeoutCallback) {
|
if (this.onTimeoutCallback) {
|
||||||
this.onTimeoutCallback();
|
this.onTimeoutCallback();
|
||||||
@ -90,10 +115,17 @@ class TurnTimer {
|
|||||||
}, this.segmentDurationMs);
|
}, this.segmentDurationMs);
|
||||||
|
|
||||||
this.tickIntervalId = setInterval(() => {
|
this.tickIntervalId = setInterval(() => {
|
||||||
// Таймер должен обновлять UI только если он isCurrentlyRunning и НЕ isManuallyPausedState
|
if (!this.isCurrentlyRunning || this.isManuallyPausedState) {
|
||||||
// isManuallyPausedState проверяется в onTickCallback, который должен передать "isPaused" клиенту
|
// Если таймер остановлен или на ручной паузе, интервал не должен ничего делать, кроме как, возможно,
|
||||||
if (!this.isCurrentlyRunning) { // Если таймер был остановлен (clear/timeout)
|
// сообщить, что он на паузе. Но лучше, чтобы onTickCallback вызывался с флагом паузы.
|
||||||
this._clearInternalTimers(); // Убедимся, что этот интервал тоже остановлен
|
// Если он был остановлен (isCurrentlyRunning=false, но не isManuallyPausedState),
|
||||||
|
// то clear() должен был уже остановить и этот интервал.
|
||||||
|
// Эта проверка - дополнительная защита.
|
||||||
|
// console.log(`[TurnTimer ${this.gameId}] Tick interval fired but timer not running or manually paused. Running: ${this.isCurrentlyRunning}, ManualPause: ${this.isManuallyPausedState}`);
|
||||||
|
if (!this.isCurrentlyRunning && this.tickIntervalId) { // Если совсем остановлен, чистим себя
|
||||||
|
clearInterval(this.tickIntervalId);
|
||||||
|
this.tickIntervalId = null;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,13 +133,16 @@ class TurnTimer {
|
|||||||
const remainingTime = Math.max(0, this.segmentDurationMs - elapsedTime);
|
const remainingTime = Math.max(0, this.segmentDurationMs - elapsedTime);
|
||||||
|
|
||||||
if (this.onTickCallback) {
|
if (this.onTickCallback) {
|
||||||
// Передаем isManuallyPausedState как состояние "паузы" для клиента
|
// Передаем isManuallyPausedState как состояние "паузы" для клиента,
|
||||||
|
// но здесь оно всегда false, т.к. есть проверка `!this.isManuallyPausedState` выше.
|
||||||
|
// Более корректно передавать `this.isManuallyPausedState || !this.isCurrentlyRunning` как общую паузу с точки зрения таймера.
|
||||||
|
// Но PCH передает `isPaused || this.isGameEffectivelyPaused()`.
|
||||||
|
// Для `onTickCallback` здесь, isPaused будет отражать `this.isManuallyPausedState`.
|
||||||
this.onTickCallback(remainingTime, this.isConfiguredForPlayerSlotTurn, this.isManuallyPausedState);
|
this.onTickCallback(remainingTime, this.isConfiguredForPlayerSlotTurn, this.isManuallyPausedState);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Не очищаем интервал здесь при remainingTime <= 0, пусть setTimeout это сделает.
|
|
||||||
// Отправка 0 - это нормально.
|
|
||||||
}, this.updateIntervalMs);
|
}, this.updateIntervalMs);
|
||||||
|
console.log(`[TurnTimer ${this.gameId}] Tick interval started: ${this.tickIntervalId}.`);
|
||||||
|
|
||||||
|
|
||||||
// Немедленная первая отправка состояния таймера
|
// Немедленная первая отправка состояния таймера
|
||||||
if (this.onTickCallback) {
|
if (this.onTickCallback) {
|
||||||
@ -117,121 +152,123 @@ class TurnTimer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pause() {
|
pause() {
|
||||||
console.log(`[TurnTimer ${this.gameId}] Attempting PAUSE. IsRunning: ${this.isCurrentlyRunning}, IsAI: ${this.isConfiguredForAiMove}, ManualPause: ${this.isManuallyPausedState}`);
|
console.log(`[TurnTimer ${this.gameId}] Attempting PAUSE. IsRunning: ${this.isCurrentlyRunning}, IsAI: ${this.isConfiguredForAiMove}, CurrentManualPauseState: ${this.isManuallyPausedState}`);
|
||||||
|
|
||||||
if (this.isManuallyPausedState) { // Уже на ручной паузе
|
if (this.isManuallyPausedState) {
|
||||||
console.log(`[TurnTimer ${this.gameId}] PAUSE called, but already manually paused. Returning previous pause state.`);
|
console.log(`[TurnTimer ${this.gameId}] PAUSE called, but already manually paused. Current saved duration (remaining): ${this.segmentDurationMs}`);
|
||||||
// Нужно вернуть актуальное оставшееся время, которое было на момент установки паузы.
|
if (this.onTickCallback) {
|
||||||
// segmentDurationMs при паузе сохраняет это значение.
|
|
||||||
if (this.onTickCallback) { // Уведомляем клиента еще раз, что на паузе
|
|
||||||
this.onTickCallback(this.segmentDurationMs, this.isConfiguredForPlayerSlotTurn, true);
|
this.onTickCallback(this.segmentDurationMs, this.isConfiguredForPlayerSlotTurn, true);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
remainingTime: this.segmentDurationMs, // Это время, которое осталось на момент паузы
|
remainingTime: this.segmentDurationMs,
|
||||||
forPlayerRoleIsPlayer: this.isConfiguredForPlayerSlotTurn,
|
forPlayerRoleIsPlayer: this.isConfiguredForPlayerSlotTurn,
|
||||||
isAiCurrentlyMoving: this.isConfiguredForAiMove // Важно сохранить, чей ход это был
|
isAiCurrentlyMoving: this.isConfiguredForAiMove
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let remainingTimeToSave;
|
let remainingTimeToSaveOnPause;
|
||||||
|
|
||||||
if (this.isConfiguredForAiMove) {
|
if (this.isConfiguredForAiMove) {
|
||||||
// Если ход AI, таймер для игрока не тикал, у него полное время
|
remainingTimeToSaveOnPause = this.initialTurnDurationMs; // Для AI всегда полное время (или как настроено)
|
||||||
remainingTimeToSave = this.initialTurnDurationMs;
|
console.log(`[TurnTimer ${this.gameId}] PAUSED during AI move. Effective remaining for player: ${remainingTimeToSaveOnPause}ms.`);
|
||||||
console.log(`[TurnTimer ${this.gameId}] PAUSED during AI move. Effective remaining: ${remainingTimeToSave}ms for player turn.`);
|
|
||||||
} else if (this.isCurrentlyRunning) {
|
} else if (this.isCurrentlyRunning) {
|
||||||
// Таймер активно работал для игрока/оппонента-человека
|
|
||||||
const elapsedTime = Date.now() - this.segmentStartTimeMs;
|
const elapsedTime = Date.now() - this.segmentStartTimeMs;
|
||||||
remainingTimeToSave = Math.max(0, this.segmentDurationMs - elapsedTime);
|
remainingTimeToSaveOnPause = Math.max(0, this.segmentDurationMs - elapsedTime);
|
||||||
console.log(`[TurnTimer ${this.gameId}] PAUSED while running. Elapsed: ${elapsedTime}ms, Remaining: ${remainingTimeToSave}ms from segment duration ${this.segmentDurationMs}ms.`);
|
console.log(`[TurnTimer ${this.gameId}] PAUSED while running. Elapsed: ${elapsedTime}ms, Remaining: ${remainingTimeToSaveOnPause}ms from segment duration ${this.segmentDurationMs}ms.`);
|
||||||
} else {
|
} else {
|
||||||
// Таймер не был активен (например, уже истек, был очищен, или это был start() для AI)
|
// Таймер не был активен (и не ход AI). Значит, время 0.
|
||||||
// В этом случае, если не ход AI, то время 0
|
remainingTimeToSaveOnPause = 0;
|
||||||
remainingTimeToSave = 0;
|
|
||||||
console.log(`[TurnTimer ${this.gameId}] PAUSE called, but timer not actively running (and not AI move). Remaining set to 0.`);
|
console.log(`[TurnTimer ${this.gameId}] PAUSE called, but timer not actively running (and not AI move). Remaining set to 0.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._clearInternalTimers();
|
this._clearInternalTimers();
|
||||||
this.isCurrentlyRunning = false;
|
this.isCurrentlyRunning = false; // Отсчет остановлен
|
||||||
this.isManuallyPausedState = true;
|
this.isManuallyPausedState = true; // Устанавливаем флаг ручной паузы
|
||||||
this.segmentDurationMs = remainingTimeToSave; // Сохраняем оставшееся время для resume
|
this.segmentDurationMs = remainingTimeToSaveOnPause; // Сохраняем оставшееся время в segmentDurationMs для resume
|
||||||
|
|
||||||
if (this.onTickCallback) {
|
if (this.onTickCallback) {
|
||||||
console.log(`[TurnTimer ${this.gameId}] Notifying client of PAUSE. Remaining: ${remainingTimeToSave}, ForPlayer: ${this.isConfiguredForPlayerSlotTurn}`);
|
console.log(`[TurnTimer ${this.gameId}] Notifying client of PAUSE state. Remaining: ${remainingTimeToSaveOnPause}, ForPlayer: ${this.isConfiguredForPlayerSlotTurn}`);
|
||||||
this.onTickCallback(remainingTimeToSave, this.isConfiguredForPlayerSlotTurn, true); // isPaused = true
|
this.onTickCallback(remainingTimeToSaveOnPause, this.isConfiguredForPlayerSlotTurn, true); // isPaused = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
remainingTime: remainingTimeToSave,
|
remainingTime: remainingTimeToSaveOnPause,
|
||||||
forPlayerRoleIsPlayer: this.isConfiguredForPlayerSlotTurn, // Чей ход это был
|
forPlayerRoleIsPlayer: this.isConfiguredForPlayerSlotTurn,
|
||||||
isAiCurrentlyMoving: this.isConfiguredForAiMove // Был ли это ход AI
|
isAiCurrentlyMoving: this.isConfiguredForAiMove
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
resume(remainingTimeMs, forPlayerSlotTurn, isAiMakingMove) {
|
resume(remainingTimeMsFromPause, forPlayerSlotTurn, isAiMakingMove) {
|
||||||
console.log(`[TurnTimer ${this.gameId}] Attempting RESUME. SavedRemaining: ${remainingTimeMs}, ForPlayer: ${forPlayerSlotTurn}, IsAI: ${isAiMakingMove}, ManualPauseBefore: ${this.isManuallyPausedState}`);
|
console.log(`[TurnTimer ${this.gameId}] Attempting RESUME. TimeFromPause: ${remainingTimeMsFromPause}, ForPlayer: ${forPlayerSlotTurn}, IsAI: ${isAiMakingMove}, CurrentManualPauseState: ${this.isManuallyPausedState}`);
|
||||||
|
|
||||||
if (!this.isManuallyPausedState) {
|
if (!this.isManuallyPausedState) {
|
||||||
console.warn(`[TurnTimer ${this.gameId}] RESUME called, but timer was not manually paused. Current state - IsRunning: ${this.isCurrentlyRunning}, IsAI: ${this.isConfiguredForAiMove}. Ignoring resume, let PCH handle start if needed.`);
|
console.warn(`[TurnTimer ${this.gameId}] RESUME called, but timer was not manually paused. This might indicate a logic issue elsewhere or a stale resume attempt. Ignoring.`);
|
||||||
// Если не был на ручной паузе, возможно, игра уже продолжается или была очищена.
|
// Если таймер не был на ручной паузе, то он либо работает, либо уже остановлен по другой причине.
|
||||||
// Не вызываем start() отсюда, чтобы избежать неожиданного поведения.
|
// Не вызываем start() отсюда, чтобы PCH мог принять решение о новом старте, если это необходимо.
|
||||||
// PCH должен решить, нужен ли новый start().
|
// Можно отправить текущее состояние, если он работает, для синхронизации.
|
||||||
// Однако, если текущий ход совпадает, и таймер просто неактивен, можно запустить.
|
if (this.isCurrentlyRunning && this.onTickCallback) {
|
||||||
// Но лучше, чтобы PCH всегда вызывал start() с нуля, если resume не применим.
|
const elapsedTime = Date.now() - this.segmentStartTimeMs;
|
||||||
// Просто отправим текущее состояние, если onTickCallback есть.
|
const currentRemaining = Math.max(0, this.segmentDurationMs - elapsedTime);
|
||||||
if (this.onTickCallback) {
|
console.log(`[TurnTimer ${this.gameId}] Resume ignored (not manually paused), sending current state if running. Remaining: ${currentRemaining}`);
|
||||||
const currentElapsedTime = this.isCurrentlyRunning ? (Date.now() - this.segmentStartTimeMs) : 0;
|
this.onTickCallback(currentRemaining, this.isConfiguredForPlayerSlotTurn, false);
|
||||||
const currentRemaining = this.isCurrentlyRunning ? Math.max(0, this.segmentDurationMs - currentElapsedTime) : this.segmentDurationMs;
|
|
||||||
this.onTickCallback(currentRemaining, this.isConfiguredForPlayerSlotTurn, this.isManuallyPausedState);
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remainingTimeMs <= 0 && !isAiMakingMove) { // Если не ход AI и время вышло
|
// Сбрасываем флаг ручной паузы ПЕРЕД вызовом start
|
||||||
|
this.isManuallyPausedState = false;
|
||||||
|
|
||||||
|
if (remainingTimeMsFromPause <= 0 && !isAiMakingMove) {
|
||||||
console.log(`[TurnTimer ${this.gameId}] RESUME called with 0 or less time (and not AI move). Triggering timeout.`);
|
console.log(`[TurnTimer ${this.gameId}] RESUME called with 0 or less time (and not AI move). Triggering timeout.`);
|
||||||
this.isManuallyPausedState = false; // Сбрасываем флаг
|
this._clearInternalTimers();
|
||||||
this._clearInternalTimers(); // Убедимся, что все остановлено
|
|
||||||
this.isCurrentlyRunning = false;
|
this.isCurrentlyRunning = false;
|
||||||
if (this.onTimeoutCallback) {
|
if (this.onTimeoutCallback) {
|
||||||
this.onTimeoutCallback();
|
this.onTimeoutCallback();
|
||||||
}
|
}
|
||||||
|
// Отправляем финальный тик с 0 временем и снятой паузой
|
||||||
|
if (this.onTickCallback) {
|
||||||
|
this.onTickCallback(0, forPlayerSlotTurn, false);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сбрасываем флаг ручной паузы и запускаем таймер с сохраненным состоянием
|
// Запускаем таймер с сохраненным состоянием и оставшимся временем
|
||||||
this.isManuallyPausedState = false;
|
// `start` сама установит isCurrentlyRunning и другие флаги.
|
||||||
this.start(forPlayerSlotTurn, isAiMakingMove, remainingTimeMs); // `start` теперь правильно обработает customRemainingTimeMs
|
this.start(forPlayerSlotTurn, isAiMakingMove, remainingTimeMsFromPause);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Очищает (останавливает) все активные таймеры и сбрасывает состояние.
|
||||||
|
* Вызывается при завершении действия, таймауте, или если игра заканчивается.
|
||||||
|
*/
|
||||||
clear() {
|
clear() {
|
||||||
console.log(`[TurnTimer ${this.gameId}] CLEAR called. WasRunning: ${this.isCurrentlyRunning}, ManualPause: ${this.isManuallyPausedState}`);
|
console.log(`[TurnTimer ${this.gameId}] CLEAR called. WasRunning: ${this.isCurrentlyRunning}, ManualPause: ${this.isManuallyPausedState}`);
|
||||||
this._clearInternalTimers();
|
this._clearInternalTimers();
|
||||||
this.isCurrentlyRunning = false;
|
this.isCurrentlyRunning = false;
|
||||||
// При полном clear сбрасываем и ручную паузу, т.к. таймер полностью останавливается.
|
this.isManuallyPausedState = false; // Полная очистка сбрасывает и ручную паузу
|
||||||
// `pause` использует этот метод, но затем сам выставляет isManuallyPausedState = true.
|
// this.segmentDurationMs = 0; // Можно сбросить, но start() все равно установит новое
|
||||||
this.isManuallyPausedState = false;
|
// this.segmentStartTimeMs = 0;
|
||||||
this.segmentDurationMs = 0; // Сбрасываем сохраненную длительность
|
|
||||||
this.segmentStartTimeMs = 0;
|
|
||||||
|
|
||||||
// Опционально: уведомить клиента, что таймер остановлен (например, null или 0)
|
// При clear не отправляем tickCallback, т.к. это означает конец отсчета для текущего хода.
|
||||||
// if (this.onTickCallback) {
|
// Клиентский UI должен будет обновиться следующим gameStateUpdate или gameStarted.
|
||||||
// this.onTickCallback(null, this.isConfiguredForPlayerSlotTurn, true); // isPaused = true (т.к. он остановлен)
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isActive() {
|
isActive() {
|
||||||
// Таймер активен, если он isCurrentlyRunning и не на ручной паузе
|
// Активен, если запущен И не на ручной паузе И не ход AI (для которого таймер игрока не тикает)
|
||||||
return this.isCurrentlyRunning && !this.isManuallyPausedState;
|
return this.isCurrentlyRunning && !this.isManuallyPausedState && !this.isConfiguredForAiMove;
|
||||||
}
|
}
|
||||||
|
|
||||||
isPaused() { // Возвращает, находится ли таймер в состоянии ручной паузы
|
isPaused() {
|
||||||
|
// Возвращает, находится ли таймер в состоянии ручной паузы (вызванной извне)
|
||||||
return this.isManuallyPausedState;
|
return this.isManuallyPausedState;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Этот геттер больше не нужен в таком виде, т.к. isConfiguredForAiMove хранит это состояние
|
// Геттер для PCH, чтобы знать, сконфигурирован ли таймер для хода AI.
|
||||||
// get isAiCurrentlyMakingMove() {
|
// Это не означает, что AI *прямо сейчас* делает вычисления, а лишь то,
|
||||||
// return this.isConfiguredForAiMove && !this.isCurrentlyRunning;
|
// что таймер был запущен для состояния "ход AI".
|
||||||
// }
|
getIsConfiguredForAiMove() {
|
||||||
|
return this.isConfiguredForAiMove;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = TurnTimer;
|
module.exports = TurnTimer;
|
Loading…
x
Reference in New Issue
Block a user