237 lines
15 KiB
JavaScript
237 lines
15 KiB
JavaScript
// /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; |