274 lines
18 KiB
JavaScript
274 lines
18 KiB
JavaScript
// /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; |