bc/server/game/instance/TurnTimer.js
2025-05-25 17:47:38 +03:00

239 lines
16 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 - Колбэк, вызываемый на каждом тике обновления (передает 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;