239 lines
16 KiB
JavaScript
239 lines
16 KiB
JavaScript
// /server/game/instance/TurnTimer.js
|
||
|
||
class TurnTimer {
|
||
/**
|
||
* Конструктор таймера хода.
|
||
* @param {number} turnDurationMs - Изначальная длительность хода в миллисекундах.
|
||
* @param {number} updateIntervalMs - Интервал для отправки обновлений времени клиентам (в мс).
|
||
* @param {function} onTimeoutCallback - Колбэк, вызываемый при истечении времени хода.
|
||
* @param {function} onTickCallback - Колбэк, вызываемый на каждом тике обновления (передает remainingTime, isPlayerTurnForTimer, isPaused).
|
||
* @param {string} [gameIdForLogs=''] - (Опционально) ID игры для более понятных логов таймера.
|
||
*/
|
||
constructor(turnDurationMs, updateIntervalMs, onTimeoutCallback, onTickCallback, gameIdForLogs = '') {
|
||
this.initialTurnDurationMs = turnDurationMs; // Сохраняем начальную полную длительность хода
|
||
this.currentEffectiveDurationMs = turnDurationMs; // Длительность, с которой стартует текущий отсчет (может быть меньше initial при resume)
|
||
|
||
this.updateIntervalMs = updateIntervalMs;
|
||
this.onTimeoutCallback = onTimeoutCallback;
|
||
this.onTickCallback = onTickCallback;
|
||
this.gameId = gameIdForLogs; // Для логов
|
||
|
||
this.timeoutId = null; // ID для setTimeout (обработка общего таймаута хода)
|
||
this.tickIntervalId = null; // ID для setInterval (периодическое обновление клиента)
|
||
|
||
this.startTimeMs = 0; // Время (Date.now()) начала текущего отсчета таймера
|
||
this.isRunning = false; // Активен ли таймер в данный момент (идет отсчет)
|
||
|
||
// Состояние, для которого был запущен/приостановлен таймер
|
||
this.isForPlayerTurn = false; // true, если таймер отсчитывает ход игрока (слот 'player')
|
||
this.isAiCurrentlyMoving = false; // true, если это ход AI, и таймер для реального игрока не должен "тикать"
|
||
|
||
this.isManuallyPaused = false; // Флаг, что таймер был приостановлен вызовом pause()
|
||
// console.log(`[TurnTimer ${this.gameId}] Initialized. Duration: ${this.initialTurnDurationMs}ms, Interval: ${this.updateIntervalMs}ms`);
|
||
}
|
||
|
||
/**
|
||
* Запускает или перезапускает таймер хода.
|
||
* @param {boolean} isPlayerSlotTurn - true, если сейчас ход слота 'player', false - если ход слота 'opponent'.
|
||
* @param {boolean} isAiMakingMove - true, если текущий ход делает AI (таймер для реального игрока не тикает).
|
||
* @param {number|null} [customRemainingTimeMs=null] - Если передано, таймер начнется с этого оставшегося времени.
|
||
*/
|
||
start(isPlayerSlotTurn, isAiMakingMove = false, customRemainingTimeMs = null) {
|
||
this.clear(true); // Очищаем предыдущие таймеры, сохраняя флаг isManuallyPaused если это resume
|
||
|
||
this.isForPlayerTurn = isPlayerSlotTurn;
|
||
this.isAiCurrentlyMakingMove = isAiMakingMove;
|
||
// При явном старте (не resume) сбрасываем флаг ручной паузы
|
||
if (customRemainingTimeMs === null) {
|
||
this.isManuallyPaused = false;
|
||
}
|
||
|
||
|
||
if (this.isAiCurrentlyMakingMove) {
|
||
this.isRunning = false; // Для хода AI основной таймер не "бежит" для игрока
|
||
// console.log(`[TurnTimer ${this.gameId}] Start: AI's turn. Player timer not ticking.`);
|
||
if (this.onTickCallback) {
|
||
// Уведомляем один раз, что таймер неактивен (ход AI), передаем isPaused = false (т.к. это не ручная пауза)
|
||
// Время может быть полным или оставшимся, если AI "думает"
|
||
this.onTickCallback(this.initialTurnDurationMs, this.isForPlayerTurn, false);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Устанавливаем длительность для текущего запуска
|
||
this.currentEffectiveDurationMs = (typeof customRemainingTimeMs === 'number' && customRemainingTimeMs > 0)
|
||
? customRemainingTimeMs
|
||
: this.initialTurnDurationMs;
|
||
|
||
this.startTimeMs = Date.now();
|
||
this.isRunning = true;
|
||
// console.log(`[TurnTimer ${this.gameId}] Started. Effective Duration: ${this.currentEffectiveDurationMs}ms. For ${this.isForPlayerTurn ? 'PlayerSlot' : 'OpponentSlot'}. AI moving: ${this.isAiCurrentlyMakingMove}`);
|
||
|
||
// Основной таймер на истечение времени хода
|
||
this.timeoutId = setTimeout(() => {
|
||
// console.log(`[TurnTimer ${this.gameId}] Timeout occurred! Was running: ${this.isRunning}`);
|
||
if (this.isRunning) { // Доп. проверка, что таймер все еще должен был работать
|
||
this.isRunning = false;
|
||
if (this.onTimeoutCallback) {
|
||
this.onTimeoutCallback();
|
||
}
|
||
this.clear(); // Очищаем и интервал обновления после таймаута
|
||
}
|
||
}, this.currentEffectiveDurationMs);
|
||
|
||
// Интервал для отправки обновлений клиентам
|
||
this.tickIntervalId = setInterval(() => {
|
||
if (!this.isRunning) {
|
||
// Если таймер был остановлен (например, ход сделан, игра окончена, или pause вызван),
|
||
// но интервал еще не очищен - очищаем.
|
||
this.clear(this.isManuallyPaused); // Сохраняем флаг, если это была ручная пауза
|
||
return;
|
||
}
|
||
|
||
const elapsedTime = Date.now() - this.startTimeMs;
|
||
const remainingTime = Math.max(0, this.currentEffectiveDurationMs - elapsedTime);
|
||
|
||
if (this.onTickCallback) {
|
||
// isManuallyPaused здесь всегда false, т.к. если бы была пауза, isRunning был бы false
|
||
this.onTickCallback(remainingTime, this.isForPlayerTurn, false);
|
||
}
|
||
|
||
if (remainingTime <= 0 && this.isRunning) {
|
||
// Время вышло по интервалу (на всякий случай, setTimeout должен сработать)
|
||
// Не вызываем onTimeoutCallback здесь напрямую, чтобы избежать двойного вызова.
|
||
this.clear(this.isManuallyPaused); // Очищаем интервал, setTimeout сработает для onTimeoutCallback
|
||
}
|
||
}, this.updateIntervalMs);
|
||
|
||
// Отправляем начальное значение немедленно
|
||
if (this.onTickCallback) {
|
||
this.onTickCallback(this.currentEffectiveDurationMs, this.isForPlayerTurn, false);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Приостанавливает таймер и возвращает его текущее состояние.
|
||
* @returns {{remainingTime: number, forPlayerRoleIsPlayer: boolean, isAiCurrentlyMoving: boolean}}
|
||
* - remainingTime: Оставшееся время в мс.
|
||
* - forPlayerRoleIsPlayer: true, если таймер был для хода игрока (слот 'player').
|
||
* - isAiCurrentlyMoving: true, если это был ход AI.
|
||
*/
|
||
pause() {
|
||
// console.log(`[TurnTimer ${this.gameId}] Pause called. isRunning: ${this.isRunning}, isAiCurrentlyMoving: ${this.isAiCurrentlyMoving}`);
|
||
|
||
let remainingTime = 0;
|
||
const wasForPlayerTurn = this.isForPlayerTurn;
|
||
const wasAiMoving = this.isAiCurrentlyMoving;
|
||
|
||
if (this.isAiCurrentlyMakingMove) {
|
||
// Если это был ход AI, таймер для игрока не тикал, считаем, что у него полное время.
|
||
// Однако, если AI "думал" и мы хотим сохранить это, логика должна быть сложнее.
|
||
// Для простоты, если AI ход, то время "не шло" для игрока.
|
||
remainingTime = this.initialTurnDurationMs;
|
||
// console.log(`[TurnTimer ${this.gameId}] Paused during AI move. Effective remaining time for player turn: ${remainingTime}ms.`);
|
||
} else if (this.isRunning) {
|
||
const elapsedTime = Date.now() - this.startTimeMs;
|
||
remainingTime = Math.max(0, this.currentEffectiveDurationMs - elapsedTime);
|
||
// console.log(`[TurnTimer ${this.gameId}] Paused while running. Elapsed: ${elapsedTime}ms, Remaining: ${remainingTime}ms.`);
|
||
} else {
|
||
// Если таймер не был запущен (например, уже истек или был очищен),
|
||
// или был уже на паузе, возвращаем 0 или последнее известное значение.
|
||
// Если isManuallyPaused уже true, то просто возвращаем то, что было.
|
||
remainingTime = this.isManuallyPaused ? this.currentEffectiveDurationMs : 0; // currentEffectiveDurationMs тут может быть уже оставшимся временем
|
||
// console.log(`[TurnTimer ${this.gameId}] Pause called, but timer not actively running or already paused. Returning current/zero remaining time: ${remainingTime}ms.`);
|
||
}
|
||
|
||
this.isManuallyPaused = true; // Устанавливаем флаг ручной паузы
|
||
this.clear(true); // Очищаем внутренние таймеры, сохраняя флаг isManuallyPaused
|
||
this.isRunning = false; // Явно указываем, что отсчет остановлен
|
||
|
||
// Уведомляем клиента, что таймер на паузе
|
||
if (this.onTickCallback) {
|
||
// console.log(`[TurnTimer ${this.gameId}] Notifying client of pause. Remaining: ${remainingTime}, ForPlayer: ${wasForPlayerTurn}`);
|
||
this.onTickCallback(remainingTime, wasForPlayerTurn, true); // isPaused = true
|
||
}
|
||
|
||
return { remainingTime, forPlayerRoleIsPlayer: wasForPlayerTurn, isAiCurrentlyMoving: wasAiMoving };
|
||
}
|
||
|
||
/**
|
||
* Возобновляет таймер с указанного оставшегося времени и для указанного состояния.
|
||
* @param {number} remainingTimeMs - Оставшееся время в миллисекундах для возобновления.
|
||
* @param {boolean} forPlayerSlotTurn - Для чьего хода (слот 'player' = true) возобновляется таймер.
|
||
* @param {boolean} isAiMakingMove - Был ли это ход AI, когда таймер приостановили (и возобновляем ли ход AI).
|
||
*/
|
||
resume(remainingTimeMs, forPlayerSlotTurn, isAiMakingMove) {
|
||
if (!this.isManuallyPaused) {
|
||
// console.warn(`[TurnTimer ${this.gameId}] Resume called, but timer was not manually paused. Starting normally or doing nothing.`);
|
||
// Если не был на ручной паузе, то либо запускаем заново (если не был ход AI), либо ничего не делаем
|
||
// if (!isAiMakingMove) this.start(forPlayerSlotTurn, false, remainingTimeMs > 0 ? remainingTimeMs : null);
|
||
// Безопаснее просто выйти, если не был на ручной паузе, GameInstance должен управлять этим.
|
||
return;
|
||
}
|
||
|
||
if (remainingTimeMs <= 0) {
|
||
// console.log(`[TurnTimer ${this.gameId}] Resume called with 0 or less time. Triggering timeout.`);
|
||
this.isManuallyPaused = false; // Сбрасываем флаг
|
||
if (this.onTimeoutCallback) {
|
||
this.onTimeoutCallback(); // Немедленный таймаут
|
||
}
|
||
return;
|
||
}
|
||
// console.log(`[TurnTimer ${this.gameId}] Resuming. Remaining: ${remainingTimeMs}ms. For ${forPlayerSlotTurn ? 'PlayerSlot' : 'OpponentSlot'}. AI moving: ${isAiMakingMove}`);
|
||
|
||
this.isManuallyPaused = false; // Сбрасываем флаг ручной паузы перед стартом
|
||
// Запускаем таймер с сохраненным состоянием и оставшимся временем
|
||
this.start(forPlayerSlotTurn, isAiMakingMove, remainingTimeMs);
|
||
}
|
||
|
||
/**
|
||
* Очищает (останавливает) все активные таймеры (setTimeout и setInterval).
|
||
* @param {boolean} [preserveManuallyPausedFlag=false] - Если true, не сбрасывает флаг isManuallyPaused.
|
||
* Используется внутренне при вызове clear из pause().
|
||
*/
|
||
clear(preserveManuallyPausedFlag = false) {
|
||
if (this.timeoutId) {
|
||
clearTimeout(this.timeoutId);
|
||
this.timeoutId = null;
|
||
}
|
||
if (this.tickIntervalId) {
|
||
clearInterval(this.tickIntervalId);
|
||
this.tickIntervalId = null;
|
||
}
|
||
|
||
const wasPreviouslyRunning = this.isRunning; // Запоминаем, работал ли он до clear
|
||
this.isRunning = false;
|
||
// this.startTimeMs = 0; // Не сбрасываем startTime, чтобы pause мог корректно вычислить remainingTime
|
||
|
||
if (!preserveManuallyPausedFlag) {
|
||
this.isManuallyPaused = false;
|
||
}
|
||
|
||
// Если таймер был очищен не через pause(), он был активен (и это не был ход AI, который и так не тикает)
|
||
// то опционально можно уведомить клиента, что таймер больше не тикает (например, ход сделан)
|
||
// Это может быть полезно, чтобы клиент сбросил свой отображаемый таймер на '--'
|
||
// if (wasPreviouslyRunning && !this.isAiCurrentlyMakingMove && !this.isManuallyPaused && this.onTickCallback) {
|
||
// // console.log(`[TurnTimer ${this.gameId}] Cleared while running (not AI, not manual pause). Notifying client.`);
|
||
// this.onTickCallback(null, this.isForPlayerTurn, this.isManuallyPaused); // remainingTime = null
|
||
// }
|
||
// console.log(`[TurnTimer ${this.gameId}] Cleared. Was running: ${wasPreviouslyRunning}. PreservePaused: ${preserveManuallyPausedFlag}`);
|
||
}
|
||
|
||
/**
|
||
* Проверяет, активен ли таймер в данный момент (идет ли отсчет).
|
||
* @returns {boolean}
|
||
*/
|
||
isActive() {
|
||
return this.isRunning;
|
||
}
|
||
|
||
/**
|
||
* Проверяет, был ли таймер приостановлен вручную вызовом pause().
|
||
* @returns {boolean}
|
||
*/
|
||
isPaused() {
|
||
return this.isManuallyPaused;
|
||
}
|
||
}
|
||
|
||
module.exports = TurnTimer; |