bc/server/game/instance/TurnTimer.js

237 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// /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;