bc/server/game/instance/GameInstance.js

863 lines
54 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 = {};
this.playerSockets = {};
this.playerCount = 0;
this.gameState = null;
this.aiOpponent = (mode === 'ai');
this.logBuffer = [];
this.playerCharacterKey = null;
this.opponentCharacterKey = null;
this.ownerIdentifier = null;
this.reconnectTimers = {};
this.pausedTurnState = null;
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) {
return;
}
if (!opponentCharacterKey) {
return;
}
if (!gameLogic.getRandomTaunt) {
console.error(`[Taunt ${this.id}] _sayTaunt: gameLogic.getRandomTaunt is not available!`);
return;
}
if (!this.gameState) {
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) {
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) {
// ... (Код addPlayer без изменений из предыдущего вашего файла) ...
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;
}
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;
} 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) { /* ... Код без изменений, вызывает turnTimer.pause() ... */
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) { /* ... Код без изменений, вызывает turnTimer.resume() или start() ... */
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];
}
}
clearAllReconnectTimers() { /* ... Код без изменений ... */
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_v3', '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}.`);
this._handleCriticalError('init_pvp_char_key_missing_v3', `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)); // p2Entry будет null если его нет
if (this.mode === 'ai' && (!isPlayerSlotFilledAndActive || !isOpponentSlotFilledAndActive) ) {
this._handleCriticalError('init_ai_data_fail_gs_v3', '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
};
// Не добавляем "Новая битва начинается" здесь, это будет в startGame, когда точно оба готовы
console.log(`[GameInstance ${this.id}] Game state initialized. Player: ${this.gameState.player.name}. Opponent: ${this.gameState.opponent.name}. Ready for start if both active: ${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;
}
// Перед стартом игры, убедимся, что gameState полностью инициализирован и содержит обоих персонажей.
// initializeGame должен был это сделать, но на всякий случай.
if (!this.gameState || !this.gameState.player?.characterKey || !this.gameState.opponent?.characterKey) {
console.warn(`[GameInstance ${this.id}] startGame: gameState or character keys not fully initialized. Attempting re-init one last time.`);
if (!this.initializeGame() || !this.gameState?.player?.characterKey || !this.gameState?.opponent?.characterKey) {
this._handleCriticalError('start_game_reinit_failed_sg_v4', 'Re-initialization before start failed or keys still missing in gameState.');
return;
}
}
console.log(`[GameInstance ${this.id}] Starting game. Player in GS: ${this.gameState.player.name} (${this.playerCharacterKey}), Opponent in GS: ${this.gameState.opponent.name} (${this.opponentCharacterKey})`);
const pData = dataUtils.getCharacterData(this.playerCharacterKey);
const oData = dataUtils.getCharacterData(this.opponentCharacterKey);
if (!pData || !oData) {
this._handleCriticalError('start_char_data_fail_sg_v5', `Failed to load character data at game start. PData: ${!!pData}, OData: ${!!oData}`);
return;
}
// Добавляем лог о начале битвы здесь, когда уверены, что оба игрока есть
this.addToLog('⚔️ Новая битва начинается! ⚔️', GAME_CONFIG.LOG_TYPE_SYSTEM);
// --- Начальные насмешки ---
// Убеждаемся, что объекты gameState.player и gameState.opponent существуют и имеют characterKey
if(this.gameState.player?.characterKey && this.gameState.opponent?.characterKey) {
this._sayTaunt(this.gameState.player, this.gameState.opponent.characterKey, 'onBattleState', 'start');
this._sayTaunt(this.gameState.opponent, this.gameState.player.characterKey, 'onBattleState', 'start');
} else {
console.warn(`[GameInstance ${this.id}] Could not say start taunts during startGame, gameState actors/keys not fully ready. GSPlayer: ${this.gameState.player?.name}, GSOpponent: ${this.gameState.opponent?.name}`);
}
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;
}
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;
// --- ИСПРАВЛЕНИЕ ДЛЯ СИЛЫ ПРИРОДЫ ---
// Логика бонуса (реген маны) теперь полностью внутри performAttack в combatLogic.js.
// GameInstance НЕ ДОЛЖЕН здесь "потреблять" эффект (обнулять turnsLeft или удалять).
// Длительность эффекта управляется в effectsLogic.js.
// --- КОНЕЦ ИСПРАВЛЕНИЯ ---
} 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() { /* ... Код без изменений ... */
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;