bc/server/game/instance/GameInstance.js

699 lines
40 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');
const PlayerConnectionHandler = require('./PlayerConnectionHandler');
class GameInstance {
constructor(gameId, io, mode = 'ai', gameManager) {
this.id = gameId;
this.io = io;
this.mode = mode;
this.gameManager = gameManager;
this.playerConnectionHandler = new PlayerConnectionHandler(this);
this.gameState = null;
this.aiOpponent = (mode === 'ai');
this.logBuffer = [];
this.playerCharacterKey = null;
this.opponentCharacterKey = null;
this.ownerIdentifier = 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.isGameEffectivelyPaused()
});
},
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}. PlayerConnectionHandler also initialized.`);
}
// --- Геттеры для GameManager и внутреннего использования ---
get playerCount() {
return this.playerConnectionHandler.playerCount;
}
// Этот геттер может быть полезен, если GameManager или другая часть GameInstance
// захочет получить доступ ко всем данным игроков, не зная о PCH.
get players() {
return this.playerConnectionHandler.getAllPlayersInfo();
}
// --- Сеттеры для PCH ---
setPlayerCharacterKey(key) { this.playerCharacterKey = key; }
setOpponentCharacterKey(key) { this.opponentCharacterKey = key; }
setOwnerIdentifier(identifier) { this.ownerIdentifier = identifier; }
// --- Методы, делегирующие PCH ---
addPlayer(socket, chosenCharacterKey, identifier) {
return this.playerConnectionHandler.addPlayer(socket, chosenCharacterKey, identifier);
}
removePlayer(socketId, reason) {
this.playerConnectionHandler.removePlayer(socketId, reason);
}
handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey) {
this.playerConnectionHandler.handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey);
}
handlePlayerReconnected(playerIdRole, newSocket) {
return this.playerConnectionHandler.handlePlayerReconnected(playerIdRole, newSocket);
}
clearAllReconnectTimers() {
this.playerConnectionHandler.clearAllReconnectTimers();
}
isGameEffectivelyPaused() {
return this.playerConnectionHandler.isGameEffectivelyPaused();
}
handlePlayerPermanentlyLeft(playerRole, characterKey, reason) {
console.log(`[GameInstance ${this.id}] Player permanently left. Role: ${playerRole}, Reason: ${reason}`);
if (this.gameState && !this.gameState.isGameOver) {
// Используем геттер playerCount
if (this.mode === 'ai' && playerRole === GAME_CONFIG.PLAYER_ID) {
this.endGameDueToDisconnect(playerRole, characterKey, "player_left_ai_game");
} else if (this.mode === 'pvp') {
if (this.playerCount < 2) {
// Используем геттер players для поиска оставшегося
const remainingActivePlayerEntry = Object.values(this.players).find(p => p.id !== playerRole && !p.isTemporarilyDisconnected);
this.endGameDueToDisconnect(playerRole, characterKey, "opponent_left_pvp_game", remainingActivePlayerEntry?.id);
}
}
} else if (!this.gameState && Object.keys(this.players).length === 0) { // Используем геттер players
this.gameManager._cleanupGame(this.id, "all_players_left_before_start_gi_via_pch");
}
}
_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);
}
}
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_gi_v4', 'AI game init: Human player not found or not active.'); return false; }
if (!this.playerCharacterKey) { this._handleCriticalError('init_ai_no_player_key_gi', 'AI game init: Player character key not set.'); return false;}
this.opponentCharacterKey = 'balard';
} else {
// Используем геттер playerCount
if (this.playerCount === 1 && p1Entry && !this.playerCharacterKey) {
this._handleCriticalError('init_pvp_single_player_no_key_gi', 'PvP init (1 player): Player char key missing.'); return false;
}
if (this.playerCount === 2 && (!this.playerCharacterKey || !this.opponentCharacterKey)) {
console.error(`[GameInstance ${this.id}] PvP init error: activePlayerCount is 2, but keys not set. P1Key: ${this.playerCharacterKey}, P2Key: ${this.opponentCharacterKey}.`);
this._handleCriticalError('init_pvp_char_key_missing_gi_v4', `PvP init: activePlayerCount 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 && p1Entry);
const isOpponentSlotFilledAndActive = !!(opponentData && (this.mode === 'ai' || p2Entry));
if (this.mode === 'ai' && (!isPlayerSlotFilledAndActive || !isOpponentSlotFilledAndActive) ) {
this._handleCriticalError('init_ai_data_fail_gs_gi_v4', '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: (this.mode === 'pvp' ? 'Ожидание Игрока 2...' : 'Противник AI'), 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
};
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 (this.mode === 'ai') ? (isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive) : isPlayerSlotFilledAndActive;
}
_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: gameState or character keys not fully initialized. Attempting re-init.`);
if (!this.initializeGame() || !this.gameState?.player?.characterKey || !this.gameState?.opponent?.characterKey) {
this._handleCriticalError('start_game_reinit_failed_sg_gi_v5', '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_gi_v6', `Failed to load character data at game start. PData: ${!!pData}, OData: ${!!oData}`);
return;
}
this.addToLog('⚔️ Новая битва начинается! ⚔️', GAME_CONFIG.LOG_TYPE_SYSTEM);
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.`);
}
const initialLog = this.consumeLogBuffer();
// Используем геттер this.players
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) {
// Используем геттер this.players
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_gi_v4', `Attacker or Defender state/key invalid.`); return;
}
const attackerData = dataUtils.getCharacterData(attackerState.characterKey);
const defenderData = dataUtils.getCharacterData(defenderState.characterKey);
if (!attackerData || !defenderData) { this._handleCriticalError('action_char_data_fail_process_gi_v4', 'Ошибка данных персонажа при действии.'); 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;
} 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) {
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_gi', `Ending turn actor state or key invalid.`); return; }
const endingTurnActorData = dataUtils.getCharacterData(endingTurnActorState.characterKey);
if (!endingTurnActorData) { this._handleCriticalError('switch_turn_char_data_fail_gi', `Char data missing.`); 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_gi', `Current turn actor state or name invalid.`); return; }
// Используем геттер this.players
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 ${currentTurnPlayerEntry.identifier} disconnected. Timer not started by switchTurn.`);
} 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() { // Остается без изменений, так как использует this.gameState
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_gi', 'Player state invalid for AI turn.'); 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() { // Остается без изменений, так как использует this.gameState
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();
let actualWinnerRole = winnerIfAny;
let winnerActuallyExists = false;
if (actualWinnerRole) {
// Используем геттер this.players
const winnerPlayerEntry = Object.values(this.players).find(p => p.id === actualWinnerRole && !p.isTemporarilyDisconnected);
if (this.mode === 'ai' && actualWinnerRole === GAME_CONFIG.OPPONENT_ID) {
winnerActuallyExists = !!this.gameState.opponent?.characterKey;
} else if (winnerPlayerEntry) {
winnerActuallyExists = true;
}
}
if (!winnerActuallyExists) {
actualWinnerRole = (disconnectedPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID);
// Используем геттер this.players
const defaultWinnerEntry = Object.values(this.players).find(p => p.id === actualWinnerRole && !p.isTemporarilyDisconnected);
if (this.mode === 'ai' && actualWinnerRole === GAME_CONFIG.OPPONENT_ID) {
winnerActuallyExists = !!this.gameState.opponent?.characterKey;
} else if (defaultWinnerEntry) {
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 by disconnect: ${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' || reason === 'opponent_left_pvp_game') ?
(this.gameState[disconnectedPlayerRole]?.name || disconnectedCharacterKey) : undefined
});
this.gameManager._cleanupGame(this.id, `disconnect_game_ended_gi_${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_gi`);
} else {
console.log(`[GameInstance ${this.id}] EndGameDueToDisconnect: no gameState.`);
this.gameManager._cleanupGame(this.id, `no_gamestate_on_disconnect_cleanup_gi`);
}
}
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.`);
if (this.gameState?.isGameOver) this.gameManager._cleanupGame(this.id, `player_left_ai_already_over_gi`);
return;
}
// Используем геттер this.players
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.io.to(this.id).emit('gameOver', {
winnerId: GAME_CONFIG.OPPONENT_ID,
reason: "player_left_ai_game",
finalGameState: this.gameState,
log: this.consumeLogBuffer(),
loserCharacterKey: playerEntry.chosenCharacterKey
});
this.gameManager._cleanupGame(this.id, 'player_left_ai_explicitly_gi');
}
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_gi`); }
console.warn(`[GameInstance ${this.id}] Surrender attempt on inactive/finished game by ${surrenderingPlayerIdentifier}.`);
return;
}
// Используем геттер this.players
const surrenderedPlayerEntry = Object.values(this.players).find(p => p.identifier === surrenderingPlayerIdentifier);
if (!surrenderedPlayerEntry) {
console.error(`[GameInstance ${this.id}] Surrendering player ${surrenderingPlayerIdentifier} not found.`);
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}).`);
}
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_gi");
}
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 winnerPlayerRoleIfHuman = timedOutPlayerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
let winnerActuallyExists = false;
if (this.mode === 'ai' && winnerPlayerRoleIfHuman === GAME_CONFIG.OPPONENT_ID) {
winnerActuallyExists = !!this.gameState.opponent?.characterKey;
} else {
// Используем геттер this.players
const winnerEntry = Object.values(this.players).find(p => p.id === winnerPlayerRoleIfHuman && !p.isTemporarilyDisconnected);
winnerActuallyExists = !!winnerEntry;
}
const result = gameLogic.getGameOverResult(this.gameState, GAME_CONFIG, this.mode, 'turn_timeout', winnerActuallyExists ? winnerPlayerRoleIfHuman : 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_gi_${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_gi_${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;