874 lines
55 KiB
JavaScript
874 lines
55 KiB
JavaScript
// /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; |