bc/server/game/instance/TurnTimer.js

274 lines
18 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 {
/**
* Конструктор таймера хода.
* @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 = '') {
this.initialTurnDurationMs = turnDurationMs;
this.updateIntervalMs = updateIntervalMs;
this.onTimeoutCallback = onTimeoutCallback;
this.onTickCallback = onTickCallback;
this.gameId = gameIdForLogs;
this.timeoutId = null; // ID для setTimeout (обработка общего таймаута хода)
this.tickIntervalId = null; // ID для setInterval (периодическое обновление клиента)
this.segmentStartTimeMs = 0; // Время (Date.now()) начала текущего активного сегмента (после start/resume)
this.segmentDurationMs = 0; // Длительность, с которой был запущен текущий активный сегмент
this.isCurrentlyRunning = false; // Идет ли активный отсчет (не на паузе из-за дисконнекта, не ход AI)
this.isManuallyPausedState = false; // Была ли вызвана pause() (например, из-за дисконнекта игрока)
// Состояние, для которого таймер был сконфигурирован при последнем запуске/возобновлении
this.isConfiguredForPlayerSlotTurn = false; // true, если таймер отсчитывает ход игрока (слот 'player')
this.isConfiguredForAiMove = false; // true, если это ход AI (таймер для реального игрока не тикает)
console.log(`[TurnTimer ${this.gameId}] Initialized. Duration: ${this.initialTurnDurationMs}ms, Interval: ${this.updateIntervalMs}ms`);
}
_clearInternalTimers() {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
// console.log(`[TurnTimer ${this.gameId}] Cleared timeoutId.`);
}
if (this.tickIntervalId) {
clearInterval(this.tickIntervalId);
this.tickIntervalId = null;
// console.log(`[TurnTimer ${this.gameId}] Cleared tickIntervalId.`);
}
}
/**
* Запускает или перезапускает таймер хода.
* @param {boolean} isPlayerSlotTurn - true, если сейчас ход слота 'player'.
* @param {boolean} isAiMakingMove - true, если текущий ход делает AI.
* @param {number|null} [customRemainingTimeMs=null] - Если передано, таймер начнется с этого времени (обычно при resume).
*/
start(isPlayerSlotTurn, isAiMakingMove = false, customRemainingTimeMs = null) {
console.log(`[TurnTimer ${this.gameId}] Attempting START. ForPlayer: ${isPlayerSlotTurn}, IsAI: ${isAiMakingMove}, CustomTime: ${customRemainingTimeMs}, CurrentManualPauseState: ${this.isManuallyPausedState}`);
this._clearInternalTimers(); // Всегда очищаем старые таймеры перед новым запуском
this.isConfiguredForPlayerSlotTurn = isPlayerSlotTurn;
this.isConfiguredForAiMove = isAiMakingMove;
// Если start вызывается НЕ из resume (т.е. customRemainingTimeMs не передан как результат pause),
// то флаг ручной паузы должен быть сброшен.
// Если это вызов из resume, isManuallyPausedState уже был сброшен в resume перед вызовом start.
if (customRemainingTimeMs === null) {
this.isManuallyPausedState = false;
}
if (this.isConfiguredForAiMove) {
this.isCurrentlyRunning = false; // Для хода AI основной таймер не "бежит" для UI игрока
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) {
// Отправляем состояние "ход AI", таймер не тикает для игрока, не на ручной паузе (т.к. игра идет)
this.onTickCallback(this.initialTurnDurationMs, this.isConfiguredForPlayerSlotTurn, false);
}
return;
}
// Если это не ход AI, то таймер должен работать для игрока (или оппонента-человека)
this.segmentDurationMs = (typeof customRemainingTimeMs === 'number' && customRemainingTimeMs >= 0) // Допускаем 0 для немедленного таймаута
? customRemainingTimeMs
: this.initialTurnDurationMs;
this.segmentStartTimeMs = Date.now();
this.isCurrentlyRunning = true; // Таймер теперь активен
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(() => {
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(() => {
if (!this.isCurrentlyRunning || this.isManuallyPausedState) {
// Если таймер остановлен или на ручной паузе, интервал не должен ничего делать, кроме как, возможно,
// сообщить, что он на паузе. Но лучше, чтобы onTickCallback вызывался с флагом паузы.
// Если он был остановлен (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;
}
const elapsedTime = Date.now() - this.segmentStartTimeMs;
const remainingTime = Math.max(0, this.segmentDurationMs - elapsedTime);
if (this.onTickCallback) {
// Передаем isManuallyPausedState как состояние "паузы" для клиента,
// но здесь оно всегда false, т.к. есть проверка `!this.isManuallyPausedState` выше.
// Более корректно передавать `this.isManuallyPausedState || !this.isCurrentlyRunning` как общую паузу с точки зрения таймера.
// Но PCH передает `isPaused || this.isGameEffectivelyPaused()`.
// Для `onTickCallback` здесь, isPaused будет отражать `this.isManuallyPausedState`.
this.onTickCallback(remainingTime, this.isConfiguredForPlayerSlotTurn, this.isManuallyPausedState);
}
}, this.updateIntervalMs);
console.log(`[TurnTimer ${this.gameId}] Tick interval started: ${this.tickIntervalId}.`);
// Немедленная первая отправка состояния таймера
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}, CurrentManualPauseState: ${this.isManuallyPausedState}`);
if (this.isManuallyPausedState) {
console.log(`[TurnTimer ${this.gameId}] PAUSE called, but already manually paused. Current saved duration (remaining): ${this.segmentDurationMs}`);
if (this.onTickCallback) {
this.onTickCallback(this.segmentDurationMs, this.isConfiguredForPlayerSlotTurn, true);
}
return {
remainingTime: this.segmentDurationMs,
forPlayerRoleIsPlayer: this.isConfiguredForPlayerSlotTurn,
isAiCurrentlyMoving: this.isConfiguredForAiMove
};
}
let remainingTimeToSaveOnPause;
if (this.isConfiguredForAiMove) {
remainingTimeToSaveOnPause = this.initialTurnDurationMs; // Для AI всегда полное время (или как настроено)
console.log(`[TurnTimer ${this.gameId}] PAUSED during AI move. Effective remaining for player: ${remainingTimeToSaveOnPause}ms.`);
} else if (this.isCurrentlyRunning) {
const elapsedTime = Date.now() - this.segmentStartTimeMs;
remainingTimeToSaveOnPause = Math.max(0, this.segmentDurationMs - elapsedTime);
console.log(`[TurnTimer ${this.gameId}] PAUSED while running. Elapsed: ${elapsedTime}ms, Remaining: ${remainingTimeToSaveOnPause}ms from segment duration ${this.segmentDurationMs}ms.`);
} else {
// Таймер не был активен (и не ход AI). Значит, время 0.
remainingTimeToSaveOnPause = 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 = remainingTimeToSaveOnPause; // Сохраняем оставшееся время в segmentDurationMs для resume
if (this.onTickCallback) {
console.log(`[TurnTimer ${this.gameId}] Notifying client of PAUSE state. Remaining: ${remainingTimeToSaveOnPause}, ForPlayer: ${this.isConfiguredForPlayerSlotTurn}`);
this.onTickCallback(remainingTimeToSaveOnPause, this.isConfiguredForPlayerSlotTurn, true); // isPaused = true
}
return {
remainingTime: remainingTimeToSaveOnPause,
forPlayerRoleIsPlayer: this.isConfiguredForPlayerSlotTurn,
isAiCurrentlyMoving: this.isConfiguredForAiMove
};
}
resume(remainingTimeMsFromPause, forPlayerSlotTurn, isAiMakingMove) {
console.log(`[TurnTimer ${this.gameId}] Attempting RESUME. TimeFromPause: ${remainingTimeMsFromPause}, ForPlayer: ${forPlayerSlotTurn}, IsAI: ${isAiMakingMove}, CurrentManualPauseState: ${this.isManuallyPausedState}`);
if (!this.isManuallyPausedState) {
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() отсюда, чтобы PCH мог принять решение о новом старте, если это необходимо.
// Можно отправить текущее состояние, если он работает, для синхронизации.
if (this.isCurrentlyRunning && this.onTickCallback) {
const elapsedTime = Date.now() - this.segmentStartTimeMs;
const currentRemaining = Math.max(0, this.segmentDurationMs - elapsedTime);
console.log(`[TurnTimer ${this.gameId}] Resume ignored (not manually paused), sending current state if running. Remaining: ${currentRemaining}`);
this.onTickCallback(currentRemaining, this.isConfiguredForPlayerSlotTurn, false);
}
return;
}
// Сбрасываем флаг ручной паузы ПЕРЕД вызовом 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.`);
this._clearInternalTimers();
this.isCurrentlyRunning = false;
if (this.onTimeoutCallback) {
this.onTimeoutCallback();
}
// Отправляем финальный тик с 0 временем и снятой паузой
if (this.onTickCallback) {
this.onTickCallback(0, forPlayerSlotTurn, false);
}
return;
}
// Запускаем таймер с сохраненным состоянием и оставшимся временем
// `start` сама установит isCurrentlyRunning и другие флаги.
this.start(forPlayerSlotTurn, isAiMakingMove, remainingTimeMsFromPause);
}
/**
* Очищает (останавливает) все активные таймеры и сбрасывает состояние.
* Вызывается при завершении действия, таймауте, или если игра заканчивается.
*/
clear() {
console.log(`[TurnTimer ${this.gameId}] CLEAR called. WasRunning: ${this.isCurrentlyRunning}, ManualPause: ${this.isManuallyPausedState}`);
this._clearInternalTimers();
this.isCurrentlyRunning = false;
this.isManuallyPausedState = false; // Полная очистка сбрасывает и ручную паузу
// this.segmentDurationMs = 0; // Можно сбросить, но start() все равно установит новое
// this.segmentStartTimeMs = 0;
// При clear не отправляем tickCallback, т.к. это означает конец отсчета для текущего хода.
// Клиентский UI должен будет обновиться следующим gameStateUpdate или gameStarted.
}
isActive() {
// Активен, если запущен И не на ручной паузе И не ход AI (для которого таймер игрока не тикает)
return this.isCurrentlyRunning && !this.isManuallyPausedState && !this.isConfiguredForAiMove;
}
isPaused() {
// Возвращает, находится ли таймер в состоянии ручной паузы (вызванной извне)
return this.isManuallyPausedState;
}
// Геттер для PCH, чтобы знать, сконфигурирован ли таймер для хода AI.
// Это не означает, что AI *прямо сейчас* делает вычисления, а лишь то,
// что таймер был запущен для состояния "ход AI".
getIsConfiguredForAiMove() {
return this.isConfiguredForAiMove;
}
}
module.exports = TurnTimer;