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

874 lines
55 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/GameInstance.js
const { v4: uuidv4 } = require('uuid');
const TurnTimer = require('./TurnTimer');
const gameLogic = require('../logic');
const dataUtils = require('../../data/dataUtils');
const GAME_CONFIG = require('../../core/config');
class GameInstance {
constructor(gameId, io, mode = 'ai', gameManager) {
this.id = gameId;
this.io = io;
this.mode = mode;
this.gameManager = gameManager;
this.players = {}; // { socketId: { id (role), socket, chosenCharacterKey, identifier, isTemporarilyDisconnected }}
this.playerSockets = {}; // { roleId: socket } -> для быстрого доступа к сокету по роли
this.playerCount = 0; // Только активные, не isTemporarilyDisconnected
this.gameState = null;
this.aiOpponent = (mode === 'ai');
this.logBuffer = [];
this.playerCharacterKey = null;
this.opponentCharacterKey = null;
this.ownerIdentifier = null;
this.reconnectTimers = {}; // { roleId: { timerId, updateIntervalId, startTimeMs, durationMs } }
this.pausedTurnState = null; // { remainingTime: number, forPlayerRoleIsPlayer: boolean, isAiCurrentlyMoving: boolean }
this.turnTimer = new TurnTimer(
GAME_CONFIG.TURN_DURATION_MS,
GAME_CONFIG.TIMER_UPDATE_INTERVAL_MS,
() => this.handleTurnTimeout(),
(remainingTime, isPlayerTurnForTimer, isPaused) => {
this.io.to(this.id).emit('turnTimerUpdate', {
remainingTime,
isPlayerTurn: isPlayerTurnForTimer,
isPaused: isPaused
});
},
this.id
);
if (!this.gameManager || typeof this.gameManager._cleanupGame !== 'function') {
console.error(`[GameInstance ${this.id}] CRITICAL ERROR: GameManager reference invalid.`);
}
console.log(`[GameInstance ${this.id}] Created. Mode: ${mode}.`);
}
_sayTaunt(characterState, opponentCharacterKey, triggerType, subTriggerOrContext = null, contextOverrides = {}) {
if (!characterState || !characterState.characterKey) {
// console.warn(`[Taunt ${this.id}] _sayTaunt: Caller character or characterKey is missing. Speaker: ${characterState?.name}, Trigger: ${triggerType}`);
return;
}
if (!opponentCharacterKey) {
// console.warn(`[Taunt ${this.id}] _sayTaunt: Opponent characterKey is missing for ${characterState.name}. Trigger: ${triggerType}`);
return;
}
if (!gameLogic.getRandomTaunt) {
console.error(`[Taunt ${this.id}] _sayTaunt: gameLogic.getRandomTaunt is not available!`);
return;
}
if (!this.gameState) {
// console.warn(`[Taunt ${this.id}] _sayTaunt: this.gameState is null. Speaker: ${characterState.name}, Trigger: ${triggerType}`);
return;
}
let context = {};
let subTrigger = null;
if (typeof subTriggerOrContext === 'string' || typeof subTriggerOrContext === 'number') {
subTrigger = subTriggerOrContext;
} else if (typeof subTriggerOrContext === 'object' && subTriggerOrContext !== null) {
context = { ...subTriggerOrContext };
}
context = { ...context, ...contextOverrides };
if ((triggerType === 'selfCastAbility' || triggerType === 'onOpponentAction') &&
(typeof subTriggerOrContext === 'string' || typeof subTriggerOrContext === 'number')) {
context.abilityId = subTriggerOrContext;
subTrigger = subTriggerOrContext;
} else if (triggerType === 'onBattleState' && typeof subTriggerOrContext === 'string') {
subTrigger = subTriggerOrContext;
} else if (triggerType === 'basicAttack' && typeof subTriggerOrContext === 'string') {
subTrigger = subTriggerOrContext;
}
const opponentFullData = dataUtils.getCharacterData(opponentCharacterKey);
if (!opponentFullData) {
// console.warn(`[Taunt ${this.id}] _sayTaunt: Could not get full data for opponent ${opponentCharacterKey} when ${characterState.name} tries to taunt.`);
return;
}
const tauntText = gameLogic.getRandomTaunt(
characterState.characterKey,
triggerType,
subTrigger || context,
GAME_CONFIG,
opponentFullData,
this.gameState
);
if (tauntText && tauntText !== "(Молчание)") {
this.addToLog(`${characterState.name}: "${tauntText}"`, GAME_CONFIG.LOG_TYPE_INFO);
}
}
addPlayer(socket, chosenCharacterKey = 'elena', identifier) {
console.log(`[GameInstance ${this.id}] addPlayer attempt. Socket: ${socket.id}, CharKey: ${chosenCharacterKey}, Identifier: ${identifier}`);
const existingPlayerByIdentifier = Object.values(this.players).find(p => p.identifier === identifier);
if (existingPlayerByIdentifier) {
console.warn(`[GameInstance ${this.id}] Identifier ${identifier} already associated with player role ${existingPlayerByIdentifier.id} (socket ${existingPlayerByIdentifier.socket?.id}). Handling as potential reconnect.`);
if (this.gameState && this.gameState.isGameOver) {
console.warn(`[GameInstance ${this.id}] Player ${identifier} trying to (re)join an already finished game. Emitting gameError.`);
socket.emit('gameError', { message: 'Эта игра уже завершена.' });
this.gameManager._cleanupGame(this.id, `rejoin_attempt_to_finished_game_${identifier}`);
return false; // Изменили возврат на boolean, как ожидает GameManager
}
if (existingPlayerByIdentifier.isTemporarilyDisconnected) {
return this.handlePlayerReconnected(existingPlayerByIdentifier.id, socket);
}
socket.emit('gameError', { message: 'Вы уже находитесь в этой игре. Попробуйте обновить страницу.' });
return false; // Изменили возврат
}
if (Object.keys(this.players).length >= 2 && this.playerCount >=2) {
socket.emit('gameError', { message: 'Эта игра уже заполнена.' });
return false; // Изменили возврат
}
let assignedPlayerId;
let actualCharacterKey = chosenCharacterKey || 'elena';
if (this.mode === 'ai') {
if (this.playerSockets[GAME_CONFIG.PLAYER_ID]) { // Проверяем, занят ли слот игрока-человека
socket.emit('gameError', { message: 'Нельзя присоединиться к AI игре как второй игрок.' });
return false; // Изменили возврат
}
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
// this.ownerIdentifier устанавливается в GameManager
} else {
if (!this.playerSockets[GAME_CONFIG.PLAYER_ID]) {
assignedPlayerId = GAME_CONFIG.PLAYER_ID;
} else if (!this.playerSockets[GAME_CONFIG.OPPONENT_ID]) {
assignedPlayerId = GAME_CONFIG.OPPONENT_ID;
const firstPlayerInfo = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey === actualCharacterKey) {
if (actualCharacterKey === 'elena') actualCharacterKey = 'almagest';
else if (actualCharacterKey === 'almagest') actualCharacterKey = 'elena';
}
} else {
socket.emit('gameError', { message: 'Не удалось найти свободный слот в PvP игре.' });
return false; // Изменили возврат
}
}
const oldPlayerSocketIdForRole = Object.keys(this.players).find(sid => this.players[sid].id === assignedPlayerId && this.players[sid].socket?.id !== socket.id);
if (oldPlayerSocketIdForRole) {
const oldPlayerInfo = this.players[oldPlayerSocketIdForRole];
if(oldPlayerInfo.socket) { try { oldPlayerInfo.socket.leave(this.id); } catch(e){} }
delete this.players[oldPlayerSocketIdForRole];
}
this.players[socket.id] = {
id: assignedPlayerId,
socket: socket,
chosenCharacterKey: actualCharacterKey,
identifier: identifier,
isTemporarilyDisconnected: false
};
this.playerSockets[assignedPlayerId] = socket;
this.playerCount++;
socket.join(this.id);
if (assignedPlayerId === GAME_CONFIG.PLAYER_ID) this.playerCharacterKey = actualCharacterKey;
else if (assignedPlayerId === GAME_CONFIG.OPPONENT_ID) this.opponentCharacterKey = actualCharacterKey;
if (!this.ownerIdentifier && (this.mode === 'ai' || (this.mode === 'pvp' && assignedPlayerId === GAME_CONFIG.PLAYER_ID))) {
this.ownerIdentifier = identifier;
}
const charData = dataUtils.getCharacterData(actualCharacterKey);
console.log(`[GameInstance ${this.id}] Player ${identifier} (Socket: ${socket.id}) added as ${assignedPlayerId} with char ${charData?.baseStats?.name || actualCharacterKey}. Active players: ${this.playerCount}. Owner: ${this.ownerIdentifier}`);
return true; // Успешное добавление
}
removePlayer(socketId, reason = "unknown_reason_for_removal") {
const playerInfo = this.players[socketId];
if (playerInfo) {
const playerRole = playerInfo.id;
const playerIdentifier = playerInfo.identifier;
console.log(`[GameInstance ${this.id}] Final removal of player ${playerIdentifier} (Socket: ${socketId}, Role: ${playerRole}). Reason: ${reason}.`);
if (playerInfo.socket) {
try { playerInfo.socket.leave(this.id); } catch (e) { /* ignore */ }
}
if (!playerInfo.isTemporarilyDisconnected) {
this.playerCount--;
}
delete this.players[socketId];
if (this.playerSockets[playerRole]?.id === socketId) {
delete this.playerSockets[playerRole];
}
this.clearReconnectTimer(playerRole);
console.log(`[GameInstance ${this.id}] Player ${playerIdentifier} removed. Active players now: ${this.playerCount}.`);
if (this.gameState && !this.gameState.isGameOver) {
if (this.mode === 'ai' && playerRole === GAME_CONFIG.PLAYER_ID) {
this.endGameDueToDisconnect(playerRole, playerInfo.chosenCharacterKey, "player_left_ai_game");
} else if (this.mode === 'pvp') {
const remainingActivePlayer = Object.values(this.players).find(p => p.socket && p.socket.connected && !p.isTemporarilyDisconnected);
if (this.playerCount < 2) {
this.endGameDueToDisconnect(playerRole, playerInfo.chosenCharacterKey, "opponent_left_pvp_game", remainingActivePlayer?.id);
}
}
} else if (!this.gameState && Object.keys(this.players).length === 0) {
this.gameManager._cleanupGame(this.id, "all_players_left_before_start_gi");
}
}
}
handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey) {
console.log(`[GameInstance ${this.id}] handlePlayerPotentiallyLeft for role ${playerIdRole}, id ${identifier}, char ${characterKey}`);
const playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier);
if (!playerEntry || !playerEntry.socket) { return; }
if (this.gameState && this.gameState.isGameOver) { return; }
if (playerEntry.isTemporarilyDisconnected) { return; }
playerEntry.isTemporarilyDisconnected = true;
this.playerCount--;
console.log(`[GameInstance ${this.id}] Player ${identifier} (role ${playerIdRole}) temp disconnected. Active: ${this.playerCount}. Starting reconnect timer.`);
const disconnectedName = this.gameState?.[playerIdRole]?.name || characterKey || `Игрок (Роль ${playerIdRole})`;
this.addToLog(`🔌 Игрок ${disconnectedName} отключился. Ожидание переподключения...`, GAME_CONFIG.LOG_TYPE_SYSTEM);
this.broadcastLogUpdate();
const otherPlayerRole = playerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
const otherSocket = this.playerSockets[otherPlayerRole];
const otherPlayerEntry = Object.values(this.players).find(p=> p.id === otherPlayerRole); // Получаем запись другого игрока
if (otherSocket?.connected && otherPlayerEntry && !otherPlayerEntry.isTemporarilyDisconnected) { // Уведомляем только если другой активен
otherSocket.emit('opponentDisconnected', {
disconnectedPlayerId: playerIdRole,
disconnectedCharacterName: disconnectedName,
});
}
if (this.turnTimer.isActive() || (this.mode === 'ai' && this.turnTimer.isAiCurrentlyMakingMove) ) {
this.pausedTurnState = this.turnTimer.pause();
console.log(`[GameInstance ${this.id}] Turn timer paused due to disconnect. State:`, JSON.stringify(this.pausedTurnState));
} else {
this.pausedTurnState = null;
}
this.clearReconnectTimer(playerIdRole);
const reconnectDuration = GAME_CONFIG.RECONNECT_TIMEOUT_MS || 30000;
const reconnectStartTime = Date.now();
const updateInterval = setInterval(() => {
const remaining = reconnectDuration - (Date.now() - reconnectStartTime);
if (remaining <= 0) {
if (this.reconnectTimers[playerIdRole]?.updateIntervalId) clearInterval(this.reconnectTimers[playerIdRole].updateIntervalId);
this.io.to(this.id).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: 0 });
return;
}
this.io.to(this.id).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: Math.ceil(remaining) });
}, 1000);
const timeoutId = setTimeout(() => {
this.clearReconnectTimer(playerIdRole);
const stillDiscPlayer = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier);
if (stillDiscPlayer && stillDiscPlayer.isTemporarilyDisconnected) {
this.removePlayer(stillDiscPlayer.socket.id, "reconnect_timeout");
}
}, reconnectDuration);
this.reconnectTimers[playerIdRole] = { timerId: timeoutId, updateIntervalId: updateInterval, startTimeMs: reconnectStartTime, durationMs: reconnectDuration };
}
handlePlayerReconnected(playerIdRole, newSocket) {
const identifier = newSocket.userData?.userId;
console.log(`[GameInstance ${this.id}] handlePlayerReconnected for role ${playerIdRole}, id ${identifier}, newSocket ${newSocket.id}`);
if (this.gameState && this.gameState.isGameOver) {
newSocket.emit('gameError', { message: 'Игра уже завершена.' });
this.gameManager._cleanupGame(this.id, `reconnect_to_finished_game_gi_${identifier}`);
return false;
}
const playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier);
if (playerEntry && playerEntry.isTemporarilyDisconnected) {
this.clearReconnectTimer(playerIdRole);
this.io.to(this.id).emit('reconnectTimerUpdate', { disconnectingPlayerId: playerIdRole, remainingTime: null });
const oldSocketId = playerEntry.socket.id;
if (this.players[oldSocketId] && oldSocketId !== newSocket.id) delete this.players[oldSocketId];
playerEntry.socket = newSocket;
playerEntry.isTemporarilyDisconnected = false;
this.players[newSocket.id] = playerEntry;
this.playerSockets[playerIdRole] = newSocket;
this.playerCount++;
newSocket.join(this.id);
const reconnectedName = this.gameState?.[playerIdRole]?.name || playerEntry.chosenCharacterKey;
console.log(`[GameInstance ${this.id}] Player ${identifier} (${reconnectedName}) reconnected. Active: ${this.playerCount}.`);
this.addToLog(`🔌 Игрок ${reconnectedName} снова в игре!`, GAME_CONFIG.LOG_TYPE_SYSTEM);
const pData = dataUtils.getCharacterData(playerEntry.chosenCharacterKey);
const oppRoleKey = playerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
let oCharKey = this.gameState?.[oppRoleKey]?.characterKey || (playerIdRole === GAME_CONFIG.PLAYER_ID ? this.opponentCharacterKey : this.playerCharacterKey);
const oData = oCharKey ? dataUtils.getCharacterData(oCharKey) : null;
if (!this.gameState) {
if (!this.initializeGame()) { this._handleCriticalError('reconnect_no_gs_after_init_v2', 'GS null after re-init.'); return false; }
}
newSocket.emit('gameStarted', {
gameId: this.id, yourPlayerId: playerIdRole, initialGameState: this.gameState,
playerBaseStats: pData?.baseStats,
opponentBaseStats: oData?.baseStats || dataUtils.getCharacterBaseStats(null) || {name: 'Ожидание...', maxHp:1, maxResource:0, resourceName:'N/A', attackPower:0, characterKey: null},
playerAbilities: pData?.abilities, opponentAbilities: oData?.abilities || [],
log: this.consumeLogBuffer(), clientConfig: { ...GAME_CONFIG }
});
const otherSocket = this.playerSockets[oppRoleKey];
const otherPlayerEntry = Object.values(this.players).find(p=> p.id === oppRoleKey);
if (otherSocket?.connected && otherPlayerEntry && !otherPlayerEntry.isTemporarilyDisconnected) {
otherSocket.emit('playerReconnected', { reconnectedPlayerId: playerIdRole, reconnectedPlayerName: reconnectedName });
if (this.logBuffer.length > 0) otherSocket.emit('logUpdate', { log: this.consumeLogBuffer() });
}
if (!this.isGameEffectivelyPaused() && this.gameState && !this.gameState.isGameOver) {
this.broadcastGameStateUpdate();
if (this.pausedTurnState && typeof this.pausedTurnState.remainingTime === 'number') {
this.turnTimer.resume(
this.pausedTurnState.remainingTime,
this.pausedTurnState.forPlayerRoleIsPlayer,
this.pausedTurnState.isAiCurrentlyMoving
);
this.pausedTurnState = null;
} else {
const currentTurnIsForPlayer = this.gameState.isPlayerTurn;
const isCurrentTurnAi = this.mode === 'ai' && !currentTurnIsForPlayer;
this.turnTimer.start(currentTurnIsForPlayer, isCurrentTurnAi);
}
}
return true;
} else if (playerEntry && !playerEntry.isTemporarilyDisconnected) {
if (playerEntry.socket.id !== newSocket.id) {
newSocket.emit('gameError', {message: "Вы уже активно подключены с другой сессии."}); return false;
}
if (!this.gameState) { if (!this.initializeGame()) {this._handleCriticalError('reconnect_same_socket_no_gs','GS null on same socket'); return false;} }
const pData = dataUtils.getCharacterData(playerEntry.chosenCharacterKey); /* ... как выше ... */
const oppRoleKey = playerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
let oCharKey = this.gameState?.[oppRoleKey]?.characterKey || (playerIdRole === GAME_CONFIG.PLAYER_ID ? this.opponentCharacterKey : this.playerCharacterKey);
const oData = oCharKey ? dataUtils.getCharacterData(oCharKey) : null;
newSocket.emit('gameStarted', { /* ... как выше ... */
gameId: this.id, yourPlayerId: playerIdRole, initialGameState: this.gameState,
playerBaseStats: pData?.baseStats, opponentBaseStats: oData?.baseStats,
playerAbilities: pData?.abilities, opponentAbilities: oData?.abilities,
log: this.consumeLogBuffer(), clientConfig: { ...GAME_CONFIG }
});
return true;
} else {
newSocket.emit('gameError', { message: 'Не удалось восстановить сессию (запись игрока не найдена).' });
return false;
}
}
clearReconnectTimer(playerIdRole) { /* ... Код без изменений ... */
if (this.reconnectTimers[playerIdRole]) {
clearTimeout(this.reconnectTimers[playerIdRole].timerId);
if (this.reconnectTimers[playerIdRole].updateIntervalId) {
clearInterval(this.reconnectTimers[playerIdRole].updateIntervalId);
}
delete this.reconnectTimers[playerIdRole];
// console.log(`[GameInstance ${this.id}] Reconnect timer & interval for role ${playerIdRole} cleared.`);
}
}
clearAllReconnectTimers() { /* ... Код без изменений ... */
// console.log(`[GameInstance ${this.id}] Clearing ALL reconnect timers.`);
for (const roleId in this.reconnectTimers) {
this.clearReconnectTimer(roleId);
}
}
isGameEffectivelyPaused() { /* ... Код без изменений ... */
if (this.mode === 'pvp') {
if (this.playerCount < 2 && Object.keys(this.players).length > 0) { // Если игроков меньше двух, но хотя бы один есть
const p1Entry = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
const p2Entry = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID);
if ((p1Entry && p1Entry.isTemporarilyDisconnected) || (p2Entry && p2Entry.isTemporarilyDisconnected)) {
return true;
}
}
} else if (this.mode === 'ai') {
const humanPlayer = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
return humanPlayer?.isTemporarilyDisconnected ?? false;
}
return false;
}
initializeGame() {
console.log(`[GameInstance ${this.id}] Initializing game state. Mode: ${this.mode}. Active players: ${this.playerCount}. Total entries: ${Object.keys(this.players).length}`);
const p1Entry = Object.values(this.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected);
const p2Entry = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID && !p.isTemporarilyDisconnected);
if (this.mode === 'ai') {
if (!p1Entry) { this._handleCriticalError('init_ai_no_active_player_v2', 'AI game init: Human player not found or not active.'); return false; }
this.playerCharacterKey = p1Entry.chosenCharacterKey;
this.opponentCharacterKey = 'balard';
} else {
this.playerCharacterKey = p1Entry ? p1Entry.chosenCharacterKey : null;
this.opponentCharacterKey = p2Entry ? p2Entry.chosenCharacterKey : null;
if (this.playerCount === 2 && (!this.playerCharacterKey || !this.opponentCharacterKey)) {
console.error(`[GameInstance ${this.id}] PvP init error: playerCount is 2, but keys not set. P1Key: ${this.playerCharacterKey}, P2Key: ${this.opponentCharacterKey}. P1Info: ${!!p1Entry}, P2Info: ${!!p2Entry}`);
this._handleCriticalError('init_pvp_char_key_missing_v2', `PvP init: playerCount is 2, but a charKey is missing.`);
return false;
}
}
const playerData = this.playerCharacterKey ? dataUtils.getCharacterData(this.playerCharacterKey) : null;
const opponentData = this.opponentCharacterKey ? dataUtils.getCharacterData(this.opponentCharacterKey) : null;
const isPlayerSlotFilledAndActive = !!playerData;
const isOpponentSlotFilledAndActive = !!(opponentData && (this.mode === 'ai' || p2Entry));
if (this.mode === 'ai' && (!isPlayerSlotFilledAndActive || !isOpponentSlotFilledAndActive) ) {
this._handleCriticalError('init_ai_data_fail_gs_v2', 'AI game init: Failed to load player or AI data for gameState (active check).'); return false;
}
this.logBuffer = [];
this.gameState = {
player: isPlayerSlotFilledAndActive ?
this._createFighterState(GAME_CONFIG.PLAYER_ID, playerData.baseStats, playerData.abilities) :
this._createFighterState(GAME_CONFIG.PLAYER_ID, { name: 'Ожидание Игрока 1...', maxHp: 1, maxResource: 0, resourceName: 'N/A', attackPower: 0, characterKey: null }, []),
opponent: isOpponentSlotFilledAndActive ?
this._createFighterState(GAME_CONFIG.OPPONENT_ID, opponentData.baseStats, opponentData.abilities) :
this._createFighterState(GAME_CONFIG.OPPONENT_ID, { name: 'Ожидание Игрока 2...', maxHp: 1, maxResource: 0, resourceName: 'N/A', attackPower: 0, characterKey: null }, []),
isPlayerTurn: isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive ? Math.random() < 0.5 : true,
isGameOver: false,
turnNumber: 1,
gameMode: this.mode
};
if (isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive) {
this.addToLog('⚔️ Новая битва начинается! ⚔️', GAME_CONFIG.LOG_TYPE_SYSTEM);
}
console.log(`[GameInstance ${this.id}] Game state initialized. Player: ${this.gameState.player.name} (Key: ${this.playerCharacterKey}). Opponent: ${this.gameState.opponent.name} (Key: ${this.opponentCharacterKey}). Ready for start: ${isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive}`);
return isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive;
}
_createFighterState(roleId, baseStats, abilities) { /* ... Код без изменений ... */
const fighterState = {
id: roleId, characterKey: baseStats.characterKey, name: baseStats.name,
currentHp: baseStats.maxHp, maxHp: baseStats.maxHp,
currentResource: baseStats.maxResource, maxResource: baseStats.maxResource,
resourceName: baseStats.resourceName, attackPower: baseStats.attackPower,
isBlocking: false, activeEffects: [], disabledAbilities: [], abilityCooldowns: {}
};
(abilities || []).forEach(ability => {
if (typeof ability.cooldown === 'number' && ability.cooldown >= 0) {
fighterState.abilityCooldowns[ability.id] = 0;
}
});
if (baseStats.characterKey === 'balard') {
fighterState.silenceCooldownTurns = 0;
fighterState.manaDrainCooldownTurns = 0;
}
return fighterState;
}
startGame() {
if (this.isGameEffectivelyPaused()) {
console.log(`[GameInstance ${this.id}] Start game deferred: game effectively paused.`);
return;
}
if (!this.gameState || !this.gameState.player?.characterKey || !this.gameState.opponent?.characterKey) {
console.warn(`[GameInstance ${this.id}] startGame: GS or char keys not fully initialized. PKey: ${this.gameState?.player?.characterKey}, OKey: ${this.gameState?.opponent?.characterKey}. Attempting re-init.`);
if (!this.initializeGame() || !this.gameState?.player?.characterKey || !this.gameState?.opponent?.characterKey) {
this._handleCriticalError('start_game_reinit_failed_sg_v3', 'Re-init before start failed or keys still missing.');
return;
}
}
console.log(`[GameInstance ${this.id}] Starting game. Player in GS: ${this.gameState.player.name}, Opponent in GS: ${this.gameState.opponent.name}`);
const pData = dataUtils.getCharacterData(this.playerCharacterKey);
const oData = dataUtils.getCharacterData(this.opponentCharacterKey);
if (!pData || !oData) {
this._handleCriticalError('start_char_data_fail_sg_v4', `Failed to load char data at game start. P: ${!!pData}, O: ${!!oData}`);
return;
}
// --- Начальные насмешки ---
if(this.gameState.player?.characterKey && this.gameState.opponent?.characterKey) {
// Убедимся, что gameState.player и .opponent существуют для передачи в _sayTaunt
if (this.gameState.player && this.gameState.opponent) {
this._sayTaunt(this.gameState.player, this.gameState.opponent.characterKey, 'onBattleState', 'start');
this._sayTaunt(this.gameState.opponent, this.gameState.player.characterKey, 'onBattleState', 'start');
}
}
const initialLog = this.consumeLogBuffer();
Object.values(this.players).forEach(playerInfo => {
if (playerInfo.socket?.connected && !playerInfo.isTemporarilyDisconnected) {
const dataForThisClient = playerInfo.id === GAME_CONFIG.PLAYER_ID ?
{ playerBaseStats: pData.baseStats, opponentBaseStats: oData.baseStats, playerAbilities: pData.abilities, opponentAbilities: oData.abilities } :
{ playerBaseStats: oData.baseStats, opponentBaseStats: pData.baseStats, playerAbilities: oData.abilities, opponentAbilities: pData.abilities };
playerInfo.socket.emit('gameStarted', {
gameId: this.id, yourPlayerId: playerInfo.id, initialGameState: this.gameState,
...dataForThisClient, log: [...initialLog], clientConfig: { ...GAME_CONFIG }
});
}
});
const firstTurnActor = this.gameState.isPlayerTurn ? this.gameState.player : this.gameState.opponent;
this.addToLog(`--- Ход ${this.gameState.turnNumber} начинается для: ${firstTurnActor.name} ---`, GAME_CONFIG.LOG_TYPE_TURN);
this.broadcastLogUpdate();
const isFirstTurnAi = this.mode === 'ai' && !this.gameState.isPlayerTurn;
this.turnTimer.start(this.gameState.isPlayerTurn, isFirstTurnAi);
if (isFirstTurnAi) {
setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN);
}
}
processPlayerAction(identifier, actionData) {
const actingPlayerInfo = Object.values(this.players).find(p => p.identifier === identifier);
if (!actingPlayerInfo || !actingPlayerInfo.socket) {
console.error(`[GameInstance ${this.id}] Action from unknown or socketless identifier ${identifier}.`); return;
}
// const requestingSocketId = actingPlayerInfo.socket.id;
if (this.isGameEffectivelyPaused()) {
actingPlayerInfo.socket.emit('gameError', {message: "Действие невозможно: игра на паузе."});
return;
}
if (!this.gameState || this.gameState.isGameOver) { return; }
const actingPlayerRole = actingPlayerInfo.id;
const isCorrectTurn = (this.gameState.isPlayerTurn && actingPlayerRole === GAME_CONFIG.PLAYER_ID) ||
(!this.gameState.isPlayerTurn && actingPlayerRole === GAME_CONFIG.OPPONENT_ID);
if (!isCorrectTurn) { actingPlayerInfo.socket.emit('gameError', { message: "Не ваш ход." }); return; }
if(this.turnTimer.isActive()) this.turnTimer.clear();
const attackerState = this.gameState[actingPlayerRole];
const defenderRole = actingPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
const defenderState = this.gameState[defenderRole];
if (!attackerState || !attackerState.characterKey || !defenderState || !defenderState.characterKey) {
this._handleCriticalError('action_actor_state_invalid_v3', `Attacker or Defender state/key invalid. Attacker: ${attackerState?.characterKey}, Defender: ${defenderState?.characterKey}`); return;
}
const attackerData = dataUtils.getCharacterData(attackerState.characterKey);
const defenderData = dataUtils.getCharacterData(defenderState.characterKey);
if (!attackerData || !defenderData) { this._handleCriticalError('action_char_data_fail_process_v3', 'Ошибка данных персонажа при действии.'); return; }
let actionIsValidAndPerformed = false;
if (actionData.actionType === 'attack') {
this._sayTaunt(attackerState, defenderState.characterKey, 'basicAttack');
gameLogic.performAttack(attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, dataUtils, gameLogic.getRandomTaunt);
actionIsValidAndPerformed = true;
const delayedBuff = attackerState.activeEffects.find(eff => eff.isDelayed && (eff.id === GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH || eff.id === GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_ATTACK) && !eff.justCast);
if (delayedBuff) {
const manaRegenConfig = GAME_CONFIG.NATURE_STRENGTH_MANA_REGEN || 0;
const regen = Math.min(manaRegenConfig, attackerData.baseStats.maxResource - attackerState.currentResource);
if (regen > 0) {
attackerState.currentResource = Math.round(attackerState.currentResource + regen);
this.addToLog(`🌿 ${attackerState.name} восстанавливает ${regen} ${attackerState.resourceName} от "${delayedBuff.name}"!`, GAME_CONFIG.LOG_TYPE_HEAL);
}
delayedBuff.turnsLeft = 0;
}
} else if (actionData.actionType === 'ability' && actionData.abilityId) {
const ability = attackerData.abilities.find(ab => ab.id === actionData.abilityId);
if (!ability) {
actingPlayerInfo.socket.emit('gameError', { message: "Неизвестная способность." });
} else {
const validityCheck = gameLogic.checkAbilityValidity(ability, attackerState, defenderState, GAME_CONFIG);
if (validityCheck.isValid) {
console.log(`[GameInstance Taunt Pre-Call] SelfCast: ${attackerState.name} uses ${ability.name} (${ability.id}) against ${defenderState.name} (${defenderState.characterKey})`);
this._sayTaunt(attackerState, defenderState.characterKey, 'selfCastAbility', ability.id);
attackerState.currentResource = Math.round(attackerState.currentResource - ability.cost);
gameLogic.applyAbilityEffect(ability, attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, dataUtils, gameLogic.getRandomTaunt, null);
gameLogic.setAbilityCooldown(ability, attackerState, GAME_CONFIG);
actionIsValidAndPerformed = true;
} else {
this.addToLog(validityCheck.reason || `${attackerState.name} не может использовать "${ability.name}".`, GAME_CONFIG.LOG_TYPE_INFO);
actionIsValidAndPerformed = false;
}
}
} else {
actionIsValidAndPerformed = false;
}
if (this.checkGameOver()) return;
this.broadcastLogUpdate();
if (actionIsValidAndPerformed) {
setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION);
} else {
const isAiTurnForTimer = this.mode === 'ai' && !this.gameState.isPlayerTurn;
this.turnTimer.start(this.gameState.isPlayerTurn, isAiTurnForTimer);
}
}
switchTurn() { /* ... Код без изменений ... */
if (this.isGameEffectivelyPaused()) { console.log(`[GameInstance ${this.id}] Switch turn deferred: game paused.`); return; }
if (!this.gameState || this.gameState.isGameOver) { return; }
if(this.turnTimer.isActive()) this.turnTimer.clear();
const endingTurnActorRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
const endingTurnActorState = this.gameState[endingTurnActorRole];
if (!endingTurnActorState || !endingTurnActorState.characterKey) { this._handleCriticalError('switch_turn_ending_actor_invalid', `Ending turn actor state or key invalid for role ${endingTurnActorRole}.`); return; }
const endingTurnActorData = dataUtils.getCharacterData(endingTurnActorState.characterKey);
if (!endingTurnActorData) { this._handleCriticalError('switch_turn_char_data_fail', `Char data missing for ${endingTurnActorState.characterKey}.`); return; }
gameLogic.processEffects(endingTurnActorState.activeEffects, endingTurnActorState, endingTurnActorData.baseStats, endingTurnActorRole, this.gameState, this.addToLog.bind(this), GAME_CONFIG, dataUtils);
gameLogic.updateBlockingStatus(endingTurnActorState);
if (endingTurnActorState.abilityCooldowns && endingTurnActorData.abilities) gameLogic.processPlayerAbilityCooldowns(endingTurnActorState.abilityCooldowns, endingTurnActorData.abilities, endingTurnActorState.name, this.addToLog.bind(this), GAME_CONFIG);
if (endingTurnActorState.characterKey === 'balard') gameLogic.processBalardSpecialCooldowns(endingTurnActorState);
if (endingTurnActorState.disabledAbilities?.length > 0 && endingTurnActorData.abilities) gameLogic.processDisabledAbilities(endingTurnActorState.disabledAbilities, endingTurnActorData.abilities, endingTurnActorState.name, this.addToLog.bind(this), GAME_CONFIG);
if (this.checkGameOver()) return;
this.gameState.isPlayerTurn = !this.gameState.isPlayerTurn;
if (this.gameState.isPlayerTurn) this.gameState.turnNumber++;
const currentTurnActorRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
const currentTurnActorState = this.gameState[currentTurnActorRole];
if (!currentTurnActorState || !currentTurnActorState.name) { this._handleCriticalError('switch_turn_current_actor_invalid', `Current turn actor state or name invalid for role ${currentTurnActorRole}.`); return; }
const currentTurnPlayerEntry = Object.values(this.players).find(p => p.id === currentTurnActorRole);
this.addToLog(`--- Ход ${this.gameState.turnNumber} начинается для: ${currentTurnActorState.name} ---`, GAME_CONFIG.LOG_TYPE_TURN);
this.broadcastGameStateUpdate();
if (currentTurnPlayerEntry && currentTurnPlayerEntry.isTemporarilyDisconnected) {
console.log(`[GameInstance ${this.id}] Turn switched to ${currentTurnActorRole}, but player disconnected. Timer not started.`);
} else {
const isNextTurnAi = this.mode === 'ai' && !this.gameState.isPlayerTurn;
this.turnTimer.start(this.gameState.isPlayerTurn, isNextTurnAi);
if (isNextTurnAi) setTimeout(() => this.processAiTurn(), GAME_CONFIG.DELAY_OPPONENT_TURN);
}
}
processAiTurn() { /* ... Код без изменений ... */
if (this.isGameEffectivelyPaused()) { console.log(`[GameInstance ${this.id}] AI turn deferred: game paused.`); return; }
if (!this.gameState || this.gameState.isGameOver || this.gameState.isPlayerTurn || !this.aiOpponent) { return; }
if(this.gameState.opponent?.characterKey !== 'balard' && this.aiOpponent) { console.error(`[GameInstance ${this.id}] AI is not Balard!`); this.switchTurn(); return; }
if(this.turnTimer.isActive()) this.turnTimer.clear();
const aiState = this.gameState.opponent;
const playerState = this.gameState.player;
if (!playerState || !playerState.characterKey) { this._handleCriticalError('ai_turn_player_state_invalid', 'Player state invalid for AI taunt.'); return; }
const aiDecision = gameLogic.decideAiAction(this.gameState, dataUtils, GAME_CONFIG, this.addToLog.bind(this));
let actionIsValidAndPerformedForAI = false;
if (aiDecision.actionType === 'attack') {
this._sayTaunt(aiState, playerState.characterKey, 'basicAttack');
gameLogic.performAttack(aiState, playerState, dataUtils.getCharacterBaseStats(aiState.characterKey), dataUtils.getCharacterBaseStats(playerState.characterKey), this.gameState, this.addToLog.bind(this), GAME_CONFIG, dataUtils, gameLogic.getRandomTaunt);
actionIsValidAndPerformedForAI = true;
} else if (aiDecision.actionType === 'ability' && aiDecision.ability) {
this._sayTaunt(aiState, playerState.characterKey, 'selfCastAbility', aiDecision.ability.id);
aiState.currentResource = Math.round(aiState.currentResource - aiDecision.ability.cost);
gameLogic.applyAbilityEffect(aiDecision.ability, aiState, playerState, dataUtils.getCharacterBaseStats(aiState.characterKey), dataUtils.getCharacterBaseStats(playerState.characterKey), this.gameState, this.addToLog.bind(this), GAME_CONFIG, dataUtils, gameLogic.getRandomTaunt, null);
gameLogic.setAbilityCooldown(aiDecision.ability, aiState, GAME_CONFIG);
actionIsValidAndPerformedForAI = true;
} else if (aiDecision.actionType === 'pass') {
if (aiDecision.logMessage && this.addToLog) this.addToLog(aiDecision.logMessage.message, aiDecision.logMessage.type);
else if (this.addToLog) this.addToLog(`${aiState.name} пропускает ход.`, GAME_CONFIG.LOG_TYPE_INFO);
actionIsValidAndPerformedForAI = true;
}
if (this.checkGameOver()) return;
this.broadcastLogUpdate();
if (actionIsValidAndPerformedForAI) setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION);
else { console.error(`[GameInstance ${this.id}] AI failed action. Forcing switch.`); setTimeout(() => this.switchTurn(), GAME_CONFIG.DELAY_AFTER_PLAYER_ACTION); }
}
checkGameOver() { /* ... Код без изменений ... */
if (!this.gameState || this.gameState.isGameOver) return this.gameState?.isGameOver ?? true;
if (!this.gameState.isGameOver && this.gameState.player?.characterKey && this.gameState.opponent?.characterKey) {
const player = this.gameState.player; const opponent = this.gameState.opponent;
const pData = dataUtils.getCharacterData(player.characterKey); const oData = dataUtils.getCharacterData(opponent.characterKey);
if (pData && oData) {
const nearDefeatThreshold = GAME_CONFIG.OPPONENT_NEAR_DEFEAT_THRESHOLD_PERCENT || 0.2;
if (opponent.currentHp > 0 && (opponent.currentHp / oData.baseStats.maxHp) <= nearDefeatThreshold) this._sayTaunt(player, opponent.characterKey, 'onBattleState', 'opponentNearDefeat');
if (player.currentHp > 0 && (player.currentHp / pData.baseStats.maxHp) <= nearDefeatThreshold) this._sayTaunt(opponent, player.characterKey, 'onBattleState', 'opponentNearDefeat');
}
}
const gameOverResult = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode);
if (gameOverResult.isOver) {
this.gameState.isGameOver = true;
if(this.turnTimer.isActive()) this.turnTimer.clear();
this.clearAllReconnectTimers();
this.addToLog(gameOverResult.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
const winnerState = this.gameState[gameOverResult.winnerRole]; const loserState = this.gameState[gameOverResult.loserRole];
if (winnerState?.characterKey && loserState?.characterKey) this._sayTaunt(winnerState, loserState.characterKey, 'onBattleState', 'opponentNearDefeat');
if (loserState?.characterKey) { /* ... сюжетные логи ... */ }
console.log(`[GameInstance ${this.id}] Game over. Winner: ${gameOverResult.winnerRole || 'None'}. Reason: ${gameOverResult.reason}.`);
this.io.to(this.id).emit('gameOver', { winnerId: gameOverResult.winnerRole, reason: gameOverResult.reason, finalGameState: this.gameState, log: this.consumeLogBuffer(), loserCharacterKey: loserState?.characterKey || 'unknown'});
this.gameManager._cleanupGame(this.id, `game_ended_${gameOverResult.reason}`);
return true;
}
return false;
}
endGameDueToDisconnect(disconnectedPlayerRole, disconnectedCharacterKey, reason = "opponent_disconnected", winnerIfAny = null) { /* ... Код без изменений ... */
if (this.gameState && !this.gameState.isGameOver) {
this.gameState.isGameOver = true;
if(this.turnTimer.isActive()) this.turnTimer.clear();
this.clearAllReconnectTimers();
const actualWinnerRole = winnerIfAny !== null ? winnerIfAny : (disconnectedPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID);
let winnerActuallyExists = false;
if (actualWinnerRole) {
const winnerPlayerEntry = Object.values(this.players).find(p => p.id === actualWinnerRole);
if (this.mode === 'ai' && actualWinnerRole === GAME_CONFIG.OPPONENT_ID) winnerActuallyExists = !!this.gameState.opponent?.characterKey;
else if (winnerPlayerEntry && !winnerPlayerEntry.isTemporarilyDisconnected) winnerActuallyExists = true;
}
const finalWinnerRole = winnerActuallyExists ? actualWinnerRole : null;
const result = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode, reason, finalWinnerRole, disconnectedPlayerRole);
this.addToLog(result.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
console.log(`[GameInstance ${this.id}] Game ended: ${reason}. Winner: ${result.winnerRole || 'Нет'}.`);
this.io.to(this.id).emit('gameOver', { winnerId: result.winnerRole, reason: result.reason, finalGameState: this.gameState, log: this.consumeLogBuffer(), loserCharacterKey: disconnectedCharacterKey, disconnectedCharacterName: reason === 'opponent_disconnected' || reason === 'player_left_ai_game' ? (this.gameState[disconnectedPlayerRole]?.name || disconnectedCharacterKey) : undefined });
this.gameManager._cleanupGame(this.id, `disconnect_game_ended_${result.reason}`);
} else if (this.gameState?.isGameOver) { console.log(`[GameInstance ${this.id}] EndGameDueToDisconnect: already over.`); this.gameManager._cleanupGame(this.id, `already_over_on_disconnect_cleanup`); }
else { console.log(`[GameInstance ${this.id}] EndGameDueToDisconnect: no gameState.`); this.gameManager._cleanupGame(this.id, `no_gamestate_on_disconnect_cleanup`); }
}
playerExplicitlyLeftAiGame(identifier) {
if (this.mode !== 'ai' || (this.gameState && this.gameState.isGameOver)) {
console.log(`[GameInstance ${this.id}] playerExplicitlyLeftAiGame called, but not AI mode or game over. Identifier: ${identifier}`);
if (this.gameState?.isGameOver) this.gameManager._cleanupGame(this.id, `player_left_ai_already_over`);
return;
}
const playerEntry = Object.values(this.players).find(p => p.identifier === identifier);
if (!playerEntry || playerEntry.id !== GAME_CONFIG.PLAYER_ID) {
console.warn(`[GameInstance ${this.id}] playerExplicitlyLeftAiGame: Identifier ${identifier} is not the human player or not found.`);
return;
}
console.log(`[GameInstance ${this.id}] Player ${identifier} explicitly left AI game.`);
if (this.gameState) {
this.gameState.isGameOver = true;
this.addToLog(`Игрок покинул битву с ${this.gameState.opponent?.name || 'AI'}.`, GAME_CONFIG.LOG_TYPE_SYSTEM);
} else {
this.addToLog(`Игрок покинул AI игру до ее полного начала.`, GAME_CONFIG.LOG_TYPE_SYSTEM);
}
if (this.turnTimer.isActive()) this.turnTimer.clear();
this.clearAllReconnectTimers();
this.gameManager._cleanupGame(this.id, 'player_left_ai_explicitly');
}
playerDidSurrender(surrenderingPlayerIdentifier) {
console.log(`[GameInstance ${this.id}] playerDidSurrender called for identifier: ${surrenderingPlayerIdentifier}`);
if (!this.gameState || this.gameState.isGameOver) {
if (this.gameState?.isGameOver) { this.gameManager._cleanupGame(this.id, `surrender_on_finished`); }
console.warn(`[GameInstance ${this.id}] Surrender attempt on inactive/finished game by ${surrenderingPlayerIdentifier}.`);
return;
}
const surrenderedPlayerEntry = Object.values(this.players).find(p => p.identifier === surrenderingPlayerIdentifier);
if (!surrenderedPlayerEntry) {
console.error(`[GameInstance ${this.id}] Surrendering player ${surrenderingPlayerIdentifier} not found in this.players.`);
return;
}
const surrenderingPlayerRole = surrenderedPlayerEntry.id; // ОПРЕДЕЛЯЕМ ЗДЕСЬ
if (this.mode === 'ai') {
if (surrenderingPlayerRole === GAME_CONFIG.PLAYER_ID) {
console.log(`[GameInstance ${this.id}] Player ${surrenderingPlayerIdentifier} "surrendered" (left) AI game.`);
this.playerExplicitlyLeftAiGame(surrenderingPlayerIdentifier);
} else {
console.warn(`[GameInstance ${this.id}] Surrender in AI mode from non-player (role: ${surrenderingPlayerRole}) or unexpected: ${surrenderingPlayerIdentifier}`);
}
return;
}
if (this.mode !== 'pvp') {
console.warn(`[GameInstance ${this.id}] Surrender called in non-PvP, non-AI mode: ${this.mode}. Ignoring.`);
return;
}
const surrenderedPlayerName = this.gameState[surrenderingPlayerRole]?.name || surrenderedPlayerEntry.chosenCharacterKey;
const surrenderedPlayerCharKey = this.gameState[surrenderingPlayerRole]?.characterKey || surrenderedPlayerEntry.chosenCharacterKey;
const winnerRole = surrenderingPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
const winnerName = this.gameState[winnerRole]?.name || `Оппонент`;
const winnerCharKey = this.gameState[winnerRole]?.characterKey;
this.gameState.isGameOver = true;
if(this.turnTimer.isActive()) this.turnTimer.clear();
this.clearAllReconnectTimers();
this.addToLog(`🏳️ ${surrenderedPlayerName} сдался! ${winnerName} объявляется победителем!`, GAME_CONFIG.LOG_TYPE_SYSTEM);
console.log(`[GameInstance ${this.id}] Player ${surrenderedPlayerName} (Role: ${surrenderingPlayerRole}) surrendered. Winner: ${winnerName} (Role: ${winnerRole}).`);
if (winnerCharKey && surrenderedPlayerCharKey && this.gameState[winnerRole]) {
this._sayTaunt(this.gameState[winnerRole], surrenderedPlayerCharKey, 'onBattleState', 'opponentNearDefeat');
}
this.io.to(this.id).emit('gameOver', {
winnerId: winnerRole, reason: "player_surrendered",
finalGameState: this.gameState, log: this.consumeLogBuffer(),
loserCharacterKey: surrenderedPlayerCharKey
});
this.gameManager._cleanupGame(this.id, "player_surrendered");
}
handleTurnTimeout() { /* ... Код без изменений, с вызовом _sayTaunt ... */
if (!this.gameState || this.gameState.isGameOver) return;
console.log(`[GameInstance ${this.id}] Turn timeout occurred.`);
const timedOutPlayerRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
const winnerPlayerRole = timedOutPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
let winnerActuallyExists = false;
if (this.mode === 'ai' && winnerPlayerRole === GAME_CONFIG.OPPONENT_ID) winnerActuallyExists = !!this.gameState.opponent?.characterKey;
else { const winnerEntry = Object.values(this.players).find(p => p.id === winnerPlayerRole && !p.isTemporarilyDisconnected); winnerActuallyExists = !!winnerEntry; }
const result = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode, 'turn_timeout', winnerActuallyExists ? winnerPlayerRole : null, timedOutPlayerRole);
this.gameState.isGameOver = true;
this.clearAllReconnectTimers();
this.addToLog(result.logMessage, GAME_CONFIG.LOG_TYPE_SYSTEM);
if (result.winnerRole && this.gameState[result.winnerRole]?.characterKey && this.gameState[result.loserRole]?.characterKey) this._sayTaunt(this.gameState[result.winnerRole], this.gameState[result.loserRole].characterKey, 'onBattleState', 'opponentNearDefeat');
console.log(`[GameInstance ${this.id}] Turn timed out for ${this.gameState[timedOutPlayerRole]?.name || timedOutPlayerRole}. Winner: ${result.winnerRole ? (this.gameState[result.winnerRole]?.name || result.winnerRole) : 'Нет'}.`);
this.io.to(this.id).emit('gameOver', { winnerId: result.winnerRole, reason: result.reason, finalGameState: this.gameState, log: this.consumeLogBuffer(), loserCharacterKey: this.gameState[timedOutPlayerRole]?.characterKey || 'unknown' });
this.gameManager._cleanupGame(this.id, `timeout_${result.reason}`);
}
_handleCriticalError(reasonCode, logMessage) { /* ... Код без изменений, с переводом логов ... */
console.error(`[GameInstance ${this.id}] CRITICAL ERROR: ${logMessage} (Code: ${reasonCode})`);
if (this.gameState && !this.gameState.isGameOver) this.gameState.isGameOver = true;
else if (!this.gameState) this.gameState = { isGameOver: true, player: {}, opponent: {}, turnNumber: 0, gameMode: this.mode };
if(this.turnTimer.isActive()) this.turnTimer.clear();
this.clearAllReconnectTimers();
this.addToLog(`Критическая ошибка сервера: ${logMessage}. Игра будет завершена.`, GAME_CONFIG.LOG_TYPE_SYSTEM);
this.io.to(this.id).emit('gameOver', { winnerId: null, reason: `server_error_${reasonCode}`, finalGameState: this.gameState, log: this.consumeLogBuffer(), loserCharacterKey: 'unknown'});
this.gameManager._cleanupGame(this.id, `critical_error_${reasonCode}`);
}
addToLog(message, type = GAME_CONFIG.LOG_TYPE_INFO) { /* ... Код без изменений ... */
if (!message) return; this.logBuffer.push({ message, type, timestamp: Date.now() });
}
consumeLogBuffer() { /* ... Код без изменений ... */
const logs = [...this.logBuffer]; this.logBuffer = []; return logs;
}
broadcastGameStateUpdate() { /* ... Код без изменений ... */
if (this.isGameEffectivelyPaused()) { return; } if (!this.gameState) return;
this.io.to(this.id).emit('gameStateUpdate', { gameState: this.gameState, log: this.consumeLogBuffer() });
}
broadcastLogUpdate() { /* ... Код без изменений ... */
if (this.isGameEffectivelyPaused() && this.logBuffer.some(log => log.type !== GAME_CONFIG.LOG_TYPE_SYSTEM)) {
const systemLogs = this.logBuffer.filter(log => log.type === GAME_CONFIG.LOG_TYPE_SYSTEM);
if (systemLogs.length > 0) this.io.to(this.id).emit('logUpdate', { log: systemLogs });
this.logBuffer = this.logBuffer.filter(log => log.type !== GAME_CONFIG.LOG_TYPE_SYSTEM); return;
}
if (this.logBuffer.length > 0) this.io.to(this.id).emit('logUpdate', { log: this.consumeLogBuffer() });
}
}
module.exports = GameInstance;