// /server/game/instance/TurnTimer.js class TurnTimer { constructor(turnDurationMs, updateIntervalMs, onTimeoutCallback, onTickCallback, gameIdForLogs = '') { this.initialTurnDurationMs = turnDurationMs; this.updateIntervalMs = updateIntervalMs; this.onTimeoutCallback = onTimeoutCallback; this.onTickCallback = onTickCallback; // (remainingTimeMs, isForPlayerSlotTurn_timerPerspective, isTimerEffectivelyPaused_byLogic) this.gameId = gameIdForLogs; this.timeoutId = null; this.tickIntervalId = null; this.segmentStartTimeMs = 0; // Время начала текущего активного сегмента (после start/resume) this.segmentDurationMs = 0; // Длительность, с которой был запущен текущий сегмент this.isCurrentlyRunning = false; // Идет ли активный отсчет (не на паузе, не ход AI) this.isManuallyPausedState = false; // Была ли вызвана pause() // Состояние, для которого таймер был запущен (или должен быть запущен) this.isConfiguredForPlayerSlotTurn = false; this.isConfiguredForAiMove = false; console.log(`[TurnTimer ${this.gameId}] Initialized. Duration: ${this.initialTurnDurationMs}ms, Interval: ${this.updateIntervalMs}ms`); } _clearInternalTimers() { if (this.timeoutId) { clearTimeout(this.timeoutId); this.timeoutId = null; } if (this.tickIntervalId) { clearInterval(this.tickIntervalId); this.tickIntervalId = null; } } /** * Запускает или перезапускает таймер хода. * @param {boolean} isPlayerSlotTurn - true, если сейчас ход слота 'player'. * @param {boolean} isAiMakingMove - true, если текущий ход делает AI. * @param {number|null} [customRemainingTimeMs=null] - Если передано, таймер начнется с этого времени. */ start(isPlayerSlotTurn, isAiMakingMove = false, customRemainingTimeMs = null) { console.log(`[TurnTimer ${this.gameId}] Attempting START. ForPlayer: ${isPlayerSlotTurn}, IsAI: ${isAiMakingMove}, CustomTime: ${customRemainingTimeMs}, ManualPause: ${this.isManuallyPausedState}`); this._clearInternalTimers(); // Всегда очищаем старые таймеры перед новым запуском this.isConfiguredForPlayerSlotTurn = isPlayerSlotTurn; this.isConfiguredForAiMove = isAiMakingMove; // Если это не resume (т.е. customRemainingTimeMs не передан явно как результат pause), // то сбрасываем флаг ручной паузы. if (customRemainingTimeMs === null) { this.isManuallyPausedState = false; } if (this.isConfiguredForAiMove) { this.isCurrentlyRunning = false; // Для хода AI основной таймер не "бежит" для игрока console.log(`[TurnTimer ${this.gameId}] START: AI's turn. Player timer not actively ticking.`); if (this.onTickCallback) { // Отправляем состояние "ход AI", таймер не тикает для игрока, не на ручной паузе this.onTickCallback(this.initialTurnDurationMs, this.isConfiguredForPlayerSlotTurn, false); } return; } // Если это не ход AI, то таймер должен работать для игрока (или оппонента-человека) this.segmentDurationMs = (typeof customRemainingTimeMs === 'number' && customRemainingTimeMs > 0) ? customRemainingTimeMs : this.initialTurnDurationMs; this.segmentStartTimeMs = Date.now(); 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}`); this.timeoutId = setTimeout(() => { console.log(`[TurnTimer ${this.gameId}] Main TIMEOUT occurred. WasRunning: ${this.isCurrentlyRunning}, ManualPause: ${this.isManuallyPausedState}`); // Проверяем, что таймер все еще должен был работать и не был на паузе if (this.isCurrentlyRunning && !this.isManuallyPausedState) { this._clearInternalTimers(); // Очищаем все, включая интервал this.isCurrentlyRunning = false; if (this.onTimeoutCallback) { this.onTimeoutCallback(); } } else { console.log(`[TurnTimer ${this.gameId}] Main TIMEOUT ignored (not running or manually paused).`); } }, this.segmentDurationMs); this.tickIntervalId = setInterval(() => { // Таймер должен обновлять UI только если он isCurrentlyRunning и НЕ isManuallyPausedState // isManuallyPausedState проверяется в onTickCallback, который должен передать "isPaused" клиенту if (!this.isCurrentlyRunning) { // Если таймер был остановлен (clear/timeout) this._clearInternalTimers(); // Убедимся, что этот интервал тоже остановлен return; } const elapsedTime = Date.now() - this.segmentStartTimeMs; const remainingTime = Math.max(0, this.segmentDurationMs - elapsedTime); if (this.onTickCallback) { // Передаем isManuallyPausedState как состояние "паузы" для клиента this.onTickCallback(remainingTime, this.isConfiguredForPlayerSlotTurn, this.isManuallyPausedState); } // Не очищаем интервал здесь при remainingTime <= 0, пусть setTimeout это сделает. // Отправка 0 - это нормально. }, this.updateIntervalMs); // Немедленная первая отправка состояния таймера if (this.onTickCallback) { console.log(`[TurnTimer ${this.gameId}] Initial tick after START. Remaining: ${this.segmentDurationMs}, ForPlayer: ${this.isConfiguredForPlayerSlotTurn}, ManualPause: ${this.isManuallyPausedState}`); this.onTickCallback(this.segmentDurationMs, this.isConfiguredForPlayerSlotTurn, this.isManuallyPausedState); } } pause() { console.log(`[TurnTimer ${this.gameId}] Attempting PAUSE. IsRunning: ${this.isCurrentlyRunning}, IsAI: ${this.isConfiguredForAiMove}, ManualPause: ${this.isManuallyPausedState}`); if (this.isManuallyPausedState) { // Уже на ручной паузе console.log(`[TurnTimer ${this.gameId}] PAUSE called, but already manually paused. Returning previous pause state.`); // Нужно вернуть актуальное оставшееся время, которое было на момент установки паузы. // segmentDurationMs при паузе сохраняет это значение. if (this.onTickCallback) { // Уведомляем клиента еще раз, что на паузе this.onTickCallback(this.segmentDurationMs, this.isConfiguredForPlayerSlotTurn, true); } return { remainingTime: this.segmentDurationMs, // Это время, которое осталось на момент паузы forPlayerRoleIsPlayer: this.isConfiguredForPlayerSlotTurn, isAiCurrentlyMoving: this.isConfiguredForAiMove // Важно сохранить, чей ход это был }; } let remainingTimeToSave; if (this.isConfiguredForAiMove) { // Если ход AI, таймер для игрока не тикал, у него полное время remainingTimeToSave = this.initialTurnDurationMs; console.log(`[TurnTimer ${this.gameId}] PAUSED during AI move. Effective remaining: ${remainingTimeToSave}ms for player turn.`); } else if (this.isCurrentlyRunning) { // Таймер активно работал для игрока/оппонента-человека const elapsedTime = Date.now() - this.segmentStartTimeMs; remainingTimeToSave = 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.`); } else { // Таймер не был активен (например, уже истек, был очищен, или это был start() для AI) // В этом случае, если не ход AI, то время 0 remainingTimeToSave = 0; console.log(`[TurnTimer ${this.gameId}] PAUSE called, but timer not actively running (and not AI move). Remaining set to 0.`); } this._clearInternalTimers(); this.isCurrentlyRunning = false; this.isManuallyPausedState = true; this.segmentDurationMs = remainingTimeToSave; // Сохраняем оставшееся время для resume if (this.onTickCallback) { console.log(`[TurnTimer ${this.gameId}] Notifying client of PAUSE. Remaining: ${remainingTimeToSave}, ForPlayer: ${this.isConfiguredForPlayerSlotTurn}`); this.onTickCallback(remainingTimeToSave, this.isConfiguredForPlayerSlotTurn, true); // isPaused = true } return { remainingTime: remainingTimeToSave, forPlayerRoleIsPlayer: this.isConfiguredForPlayerSlotTurn, // Чей ход это был isAiCurrentlyMoving: this.isConfiguredForAiMove // Был ли это ход AI }; } resume(remainingTimeMs, forPlayerSlotTurn, isAiMakingMove) { console.log(`[TurnTimer ${this.gameId}] Attempting RESUME. SavedRemaining: ${remainingTimeMs}, ForPlayer: ${forPlayerSlotTurn}, IsAI: ${isAiMakingMove}, ManualPauseBefore: ${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.`); // Если не был на ручной паузе, возможно, игра уже продолжается или была очищена. // Не вызываем start() отсюда, чтобы избежать неожиданного поведения. // PCH должен решить, нужен ли новый start(). // Однако, если текущий ход совпадает, и таймер просто неактивен, можно запустить. // Но лучше, чтобы PCH всегда вызывал start() с нуля, если resume не применим. // Просто отправим текущее состояние, если onTickCallback есть. if (this.onTickCallback) { const currentElapsedTime = this.isCurrentlyRunning ? (Date.now() - this.segmentStartTimeMs) : 0; const currentRemaining = this.isCurrentlyRunning ? Math.max(0, this.segmentDurationMs - currentElapsedTime) : this.segmentDurationMs; this.onTickCallback(currentRemaining, this.isConfiguredForPlayerSlotTurn, this.isManuallyPausedState); } return; } if (remainingTimeMs <= 0 && !isAiMakingMove) { // Если не ход AI и время вышло console.log(`[TurnTimer ${this.gameId}] RESUME called with 0 or less time (and not AI move). Triggering timeout.`); this.isManuallyPausedState = false; // Сбрасываем флаг this._clearInternalTimers(); // Убедимся, что все остановлено this.isCurrentlyRunning = false; if (this.onTimeoutCallback) { this.onTimeoutCallback(); } return; } // Сбрасываем флаг ручной паузы и запускаем таймер с сохраненным состоянием this.isManuallyPausedState = false; this.start(forPlayerSlotTurn, isAiMakingMove, remainingTimeMs); // `start` теперь правильно обработает customRemainingTimeMs } clear() { console.log(`[TurnTimer ${this.gameId}] CLEAR called. WasRunning: ${this.isCurrentlyRunning}, ManualPause: ${this.isManuallyPausedState}`); this._clearInternalTimers(); this.isCurrentlyRunning = false; // При полном clear сбрасываем и ручную паузу, т.к. таймер полностью останавливается. // `pause` использует этот метод, но затем сам выставляет isManuallyPausedState = true. this.isManuallyPausedState = false; this.segmentDurationMs = 0; // Сбрасываем сохраненную длительность this.segmentStartTimeMs = 0; // Опционально: уведомить клиента, что таймер остановлен (например, null или 0) // if (this.onTickCallback) { // this.onTickCallback(null, this.isConfiguredForPlayerSlotTurn, true); // isPaused = true (т.к. он остановлен) // } } isActive() { // Таймер активен, если он isCurrentlyRunning и не на ручной паузе return this.isCurrentlyRunning && !this.isManuallyPausedState; } isPaused() { // Возвращает, находится ли таймер в состоянии ручной паузы return this.isManuallyPausedState; } // Этот геттер больше не нужен в таком виде, т.к. isConfiguredForAiMove хранит это состояние // get isAiCurrentlyMakingMove() { // return this.isConfiguredForAiMove && !this.isCurrentlyRunning; // } } module.exports = TurnTimer;