Выправление эффекта сила природы.

This commit is contained in:
PsiMagistr 2025-05-25 18:34:54 +03:00
parent fb36c3d2e1
commit 12d85b8385
2 changed files with 172 additions and 132 deletions

View File

@ -12,9 +12,9 @@ class GameInstance {
this.mode = mode; this.mode = mode;
this.gameManager = gameManager; this.gameManager = gameManager;
this.players = {}; // { socketId: { id (role), socket, chosenCharacterKey, identifier, isTemporarilyDisconnected }} this.players = {};
this.playerSockets = {}; // { roleId: socket } -> для быстрого доступа к сокету по роли this.playerSockets = {};
this.playerCount = 0; // Только активные, не isTemporarilyDisconnected this.playerCount = 0;
this.gameState = null; this.gameState = null;
this.aiOpponent = (mode === 'ai'); this.aiOpponent = (mode === 'ai');
@ -24,8 +24,8 @@ class GameInstance {
this.opponentCharacterKey = null; this.opponentCharacterKey = null;
this.ownerIdentifier = null; this.ownerIdentifier = null;
this.reconnectTimers = {}; // { roleId: { timerId, updateIntervalId, startTimeMs, durationMs } } this.reconnectTimers = {};
this.pausedTurnState = null; // { remainingTime: number, forPlayerRoleIsPlayer: boolean, isAiCurrentlyMoving: boolean } this.pausedTurnState = null;
this.turnTimer = new TurnTimer( this.turnTimer = new TurnTimer(
GAME_CONFIG.TURN_DURATION_MS, GAME_CONFIG.TURN_DURATION_MS,
@ -49,11 +49,9 @@ class GameInstance {
_sayTaunt(characterState, opponentCharacterKey, triggerType, subTriggerOrContext = null, contextOverrides = {}) { _sayTaunt(characterState, opponentCharacterKey, triggerType, subTriggerOrContext = null, contextOverrides = {}) {
if (!characterState || !characterState.characterKey) { if (!characterState || !characterState.characterKey) {
// console.warn(`[Taunt ${this.id}] _sayTaunt: Caller character or characterKey is missing. Speaker: ${characterState?.name}, Trigger: ${triggerType}`);
return; return;
} }
if (!opponentCharacterKey) { if (!opponentCharacterKey) {
// console.warn(`[Taunt ${this.id}] _sayTaunt: Opponent characterKey is missing for ${characterState.name}. Trigger: ${triggerType}`);
return; return;
} }
if (!gameLogic.getRandomTaunt) { if (!gameLogic.getRandomTaunt) {
@ -61,7 +59,6 @@ class GameInstance {
return; return;
} }
if (!this.gameState) { if (!this.gameState) {
// console.warn(`[Taunt ${this.id}] _sayTaunt: this.gameState is null. Speaker: ${characterState.name}, Trigger: ${triggerType}`);
return; return;
} }
@ -87,7 +84,6 @@ class GameInstance {
const opponentFullData = dataUtils.getCharacterData(opponentCharacterKey); const opponentFullData = dataUtils.getCharacterData(opponentCharacterKey);
if (!opponentFullData) { if (!opponentFullData) {
// console.warn(`[Taunt ${this.id}] _sayTaunt: Could not get full data for opponent ${opponentCharacterKey} when ${characterState.name} tries to taunt.`);
return; return;
} }
@ -106,6 +102,7 @@ class GameInstance {
} }
addPlayer(socket, chosenCharacterKey = 'elena', identifier) { addPlayer(socket, chosenCharacterKey = 'elena', identifier) {
// ... (Код addPlayer без изменений из предыдущего вашего файла) ...
console.log(`[GameInstance ${this.id}] addPlayer attempt. Socket: ${socket.id}, CharKey: ${chosenCharacterKey}, Identifier: ${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); const existingPlayerByIdentifier = Object.values(this.players).find(p => p.identifier === identifier);
@ -115,30 +112,29 @@ class GameInstance {
console.warn(`[GameInstance ${this.id}] Player ${identifier} trying to (re)join an already finished game. Emitting gameError.`); console.warn(`[GameInstance ${this.id}] Player ${identifier} trying to (re)join an already finished game. Emitting gameError.`);
socket.emit('gameError', { message: 'Эта игра уже завершена.' }); socket.emit('gameError', { message: 'Эта игра уже завершена.' });
this.gameManager._cleanupGame(this.id, `rejoin_attempt_to_finished_game_${identifier}`); this.gameManager._cleanupGame(this.id, `rejoin_attempt_to_finished_game_${identifier}`);
return false; // Изменили возврат на boolean, как ожидает GameManager return false;
} }
if (existingPlayerByIdentifier.isTemporarilyDisconnected) { if (existingPlayerByIdentifier.isTemporarilyDisconnected) {
return this.handlePlayerReconnected(existingPlayerByIdentifier.id, socket); return this.handlePlayerReconnected(existingPlayerByIdentifier.id, socket);
} }
socket.emit('gameError', { message: 'Вы уже находитесь в этой игре. Попробуйте обновить страницу.' }); socket.emit('gameError', { message: 'Вы уже находитесь в этой игре. Попробуйте обновить страницу.' });
return false; // Изменили возврат return false;
} }
if (Object.keys(this.players).length >= 2 && this.playerCount >=2) { if (Object.keys(this.players).length >= 2 && this.playerCount >=2) {
socket.emit('gameError', { message: 'Эта игра уже заполнена.' }); socket.emit('gameError', { message: 'Эта игра уже заполнена.' });
return false; // Изменили возврат return false;
} }
let assignedPlayerId; let assignedPlayerId;
let actualCharacterKey = chosenCharacterKey || 'elena'; let actualCharacterKey = chosenCharacterKey || 'elena';
if (this.mode === 'ai') { if (this.mode === 'ai') {
if (this.playerSockets[GAME_CONFIG.PLAYER_ID]) { // Проверяем, занят ли слот игрока-человека if (this.playerSockets[GAME_CONFIG.PLAYER_ID]) {
socket.emit('gameError', { message: 'Нельзя присоединиться к AI игре как второй игрок.' }); socket.emit('gameError', { message: 'Нельзя присоединиться к AI игре как второй игрок.' });
return false; // Изменили возврат return false;
} }
assignedPlayerId = GAME_CONFIG.PLAYER_ID; assignedPlayerId = GAME_CONFIG.PLAYER_ID;
// this.ownerIdentifier устанавливается в GameManager
} else { } else {
if (!this.playerSockets[GAME_CONFIG.PLAYER_ID]) { if (!this.playerSockets[GAME_CONFIG.PLAYER_ID]) {
assignedPlayerId = GAME_CONFIG.PLAYER_ID; assignedPlayerId = GAME_CONFIG.PLAYER_ID;
@ -151,7 +147,7 @@ class GameInstance {
} }
} else { } else {
socket.emit('gameError', { message: 'Не удалось найти свободный слот в PvP игре.' }); socket.emit('gameError', { message: 'Не удалось найти свободный слот в PvP игре.' });
return false; // Изменили возврат return false;
} }
} }
@ -182,10 +178,10 @@ class GameInstance {
const charData = dataUtils.getCharacterData(actualCharacterKey); 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}`); 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; // Успешное добавление return true;
} }
removePlayer(socketId, reason = "unknown_reason_for_removal") { removePlayer(socketId, reason = "unknown_reason_for_removal") { /* ... Код без изменений ... */
const playerInfo = this.players[socketId]; const playerInfo = this.players[socketId];
if (playerInfo) { if (playerInfo) {
const playerRole = playerInfo.id; const playerRole = playerInfo.id;
@ -222,8 +218,7 @@ class GameInstance {
} }
} }
} }
handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey) { /* ... Код без изменений, вызывает turnTimer.pause() ... */
handlePlayerPotentiallyLeft(playerIdRole, identifier, characterKey) {
console.log(`[GameInstance ${this.id}] handlePlayerPotentiallyLeft for role ${playerIdRole}, id ${identifier}, char ${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); const playerEntry = Object.values(this.players).find(p => p.id === playerIdRole && p.identifier === identifier);
@ -241,8 +236,8 @@ class GameInstance {
const otherPlayerRole = playerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID; const otherPlayerRole = playerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
const otherSocket = this.playerSockets[otherPlayerRole]; const otherSocket = this.playerSockets[otherPlayerRole];
const otherPlayerEntry = Object.values(this.players).find(p=> p.id === otherPlayerRole); // Получаем запись другого игрока const otherPlayerEntry = Object.values(this.players).find(p=> p.id === otherPlayerRole);
if (otherSocket?.connected && otherPlayerEntry && !otherPlayerEntry.isTemporarilyDisconnected) { // Уведомляем только если другой активен if (otherSocket?.connected && otherPlayerEntry && !otherPlayerEntry.isTemporarilyDisconnected) {
otherSocket.emit('opponentDisconnected', { otherSocket.emit('opponentDisconnected', {
disconnectedPlayerId: playerIdRole, disconnectedPlayerId: playerIdRole,
disconnectedCharacterName: disconnectedName, disconnectedCharacterName: disconnectedName,
@ -280,7 +275,7 @@ class GameInstance {
this.reconnectTimers[playerIdRole] = { timerId: timeoutId, updateIntervalId: updateInterval, startTimeMs: reconnectStartTime, durationMs: reconnectDuration }; this.reconnectTimers[playerIdRole] = { timerId: timeoutId, updateIntervalId: updateInterval, startTimeMs: reconnectStartTime, durationMs: reconnectDuration };
} }
handlePlayerReconnected(playerIdRole, newSocket) { handlePlayerReconnected(playerIdRole, newSocket) { /* ... Код без изменений, вызывает turnTimer.resume() или start() ... */
const identifier = newSocket.userData?.userId; const identifier = newSocket.userData?.userId;
console.log(`[GameInstance ${this.id}] handlePlayerReconnected for role ${playerIdRole}, id ${identifier}, newSocket ${newSocket.id}`); console.log(`[GameInstance ${this.id}] handlePlayerReconnected for role ${playerIdRole}, id ${identifier}, newSocket ${newSocket.id}`);
@ -356,11 +351,11 @@ class GameInstance {
newSocket.emit('gameError', {message: "Вы уже активно подключены с другой сессии."}); return false; 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;} } 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 pData = dataUtils.getCharacterData(playerEntry.chosenCharacterKey);
const oppRoleKey = playerIdRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID; 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); let oCharKey = this.gameState?.[oppRoleKey]?.characterKey || (playerIdRole === GAME_CONFIG.PLAYER_ID ? this.opponentCharacterKey : this.playerCharacterKey);
const oData = oCharKey ? dataUtils.getCharacterData(oCharKey) : null; const oData = oCharKey ? dataUtils.getCharacterData(oCharKey) : null;
newSocket.emit('gameStarted', { /* ... как выше ... */ newSocket.emit('gameStarted', {
gameId: this.id, yourPlayerId: playerIdRole, initialGameState: this.gameState, gameId: this.id, yourPlayerId: playerIdRole, initialGameState: this.gameState,
playerBaseStats: pData?.baseStats, opponentBaseStats: oData?.baseStats, playerBaseStats: pData?.baseStats, opponentBaseStats: oData?.baseStats,
playerAbilities: pData?.abilities, opponentAbilities: oData?.abilities, playerAbilities: pData?.abilities, opponentAbilities: oData?.abilities,
@ -380,18 +375,16 @@ class GameInstance {
clearInterval(this.reconnectTimers[playerIdRole].updateIntervalId); clearInterval(this.reconnectTimers[playerIdRole].updateIntervalId);
} }
delete this.reconnectTimers[playerIdRole]; delete this.reconnectTimers[playerIdRole];
// console.log(`[GameInstance ${this.id}] Reconnect timer & interval for role ${playerIdRole} cleared.`);
} }
} }
clearAllReconnectTimers() { /* ... Код без изменений ... */ clearAllReconnectTimers() { /* ... Код без изменений ... */
// console.log(`[GameInstance ${this.id}] Clearing ALL reconnect timers.`);
for (const roleId in this.reconnectTimers) { for (const roleId in this.reconnectTimers) {
this.clearReconnectTimer(roleId); this.clearReconnectTimer(roleId);
} }
} }
isGameEffectivelyPaused() { /* ... Код без изменений ... */ isGameEffectivelyPaused() { /* ... Код без изменений ... */
if (this.mode === 'pvp') { if (this.mode === 'pvp') {
if (this.playerCount < 2 && Object.keys(this.players).length > 0) { // Если игроков меньше двух, но хотя бы один есть 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 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); const p2Entry = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID);
if ((p1Entry && p1Entry.isTemporarilyDisconnected) || (p2Entry && p2Entry.isTemporarilyDisconnected)) { if ((p1Entry && p1Entry.isTemporarilyDisconnected) || (p2Entry && p2Entry.isTemporarilyDisconnected)) {
@ -412,7 +405,7 @@ class GameInstance {
const p2Entry = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID && !p.isTemporarilyDisconnected); const p2Entry = Object.values(this.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID && !p.isTemporarilyDisconnected);
if (this.mode === 'ai') { 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; } 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.playerCharacterKey = p1Entry.chosenCharacterKey;
this.opponentCharacterKey = 'balard'; this.opponentCharacterKey = 'balard';
} else { } else {
@ -420,8 +413,8 @@ class GameInstance {
this.opponentCharacterKey = p2Entry ? p2Entry.chosenCharacterKey : null; this.opponentCharacterKey = p2Entry ? p2Entry.chosenCharacterKey : null;
if (this.playerCount === 2 && (!this.playerCharacterKey || !this.opponentCharacterKey)) { 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}`); 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_v2', `PvP init: playerCount is 2, but a charKey is missing.`); this._handleCriticalError('init_pvp_char_key_missing_v3', `PvP init: playerCount is 2, but a charKey is missing.`);
return false; return false;
} }
} }
@ -430,10 +423,10 @@ class GameInstance {
const opponentData = this.opponentCharacterKey ? dataUtils.getCharacterData(this.opponentCharacterKey) : null; const opponentData = this.opponentCharacterKey ? dataUtils.getCharacterData(this.opponentCharacterKey) : null;
const isPlayerSlotFilledAndActive = !!playerData; const isPlayerSlotFilledAndActive = !!playerData;
const isOpponentSlotFilledAndActive = !!(opponentData && (this.mode === 'ai' || p2Entry)); const isOpponentSlotFilledAndActive = !!(opponentData && (this.mode === 'ai' || p2Entry)); // p2Entry будет null если его нет
if (this.mode === 'ai' && (!isPlayerSlotFilledAndActive || !isOpponentSlotFilledAndActive) ) { 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._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.logBuffer = [];
@ -451,11 +444,8 @@ class GameInstance {
gameMode: this.mode gameMode: this.mode
}; };
if (isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive) { // Не добавляем "Новая битва начинается" здесь, это будет в startGame, когда точно оба готовы
this.addToLog('⚔️ Новая битва начинается! ⚔️', GAME_CONFIG.LOG_TYPE_SYSTEM); 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}`);
}
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; return isPlayerSlotFilledAndActive && isOpponentSlotFilledAndActive;
} }
@ -484,30 +474,35 @@ class GameInstance {
console.log(`[GameInstance ${this.id}] Start game deferred: game effectively paused.`); console.log(`[GameInstance ${this.id}] Start game deferred: game effectively paused.`);
return; return;
} }
// Перед стартом игры, убедимся, что gameState полностью инициализирован и содержит обоих персонажей.
// initializeGame должен был это сделать, но на всякий случай.
if (!this.gameState || !this.gameState.player?.characterKey || !this.gameState.opponent?.characterKey) { 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.`); 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) { 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.'); this._handleCriticalError('start_game_reinit_failed_sg_v4', 'Re-initialization before start failed or keys still missing in gameState.');
return; return;
} }
} }
console.log(`[GameInstance ${this.id}] Starting game. Player in GS: ${this.gameState.player.name}, Opponent in GS: ${this.gameState.opponent.name}`); 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 pData = dataUtils.getCharacterData(this.playerCharacterKey);
const oData = dataUtils.getCharacterData(this.opponentCharacterKey); const oData = dataUtils.getCharacterData(this.opponentCharacterKey);
if (!pData || !oData) { if (!pData || !oData) {
this._handleCriticalError('start_char_data_fail_sg_v4', `Failed to load char data at game start. P: ${!!pData}, O: ${!!oData}`); this._handleCriticalError('start_char_data_fail_sg_v5', `Failed to load character data at game start. PData: ${!!pData}, OData: ${!!oData}`);
return; return;
} }
// Добавляем лог о начале битвы здесь, когда уверены, что оба игрока есть
this.addToLog('⚔️ Новая битва начинается! ⚔️', GAME_CONFIG.LOG_TYPE_SYSTEM);
// --- Начальные насмешки --- // --- Начальные насмешки ---
// Убеждаемся, что объекты gameState.player и gameState.opponent существуют и имеют characterKey
if(this.gameState.player?.characterKey && this.gameState.opponent?.characterKey) { if(this.gameState.player?.characterKey && this.gameState.opponent?.characterKey) {
// Убедимся, что gameState.player и .opponent существуют для передачи в _sayTaunt this._sayTaunt(this.gameState.player, this.gameState.opponent.characterKey, 'onBattleState', 'start');
if (this.gameState.player && this.gameState.opponent) { this._sayTaunt(this.gameState.opponent, this.gameState.player.characterKey, 'onBattleState', 'start');
this._sayTaunt(this.gameState.player, this.gameState.opponent.characterKey, 'onBattleState', 'start'); } else {
this._sayTaunt(this.gameState.opponent, this.gameState.player.characterKey, 'onBattleState', 'start'); 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(); const initialLog = this.consumeLogBuffer();
@ -542,7 +537,6 @@ class GameInstance {
if (!actingPlayerInfo || !actingPlayerInfo.socket) { if (!actingPlayerInfo || !actingPlayerInfo.socket) {
console.error(`[GameInstance ${this.id}] Action from unknown or socketless identifier ${identifier}.`); return; console.error(`[GameInstance ${this.id}] Action from unknown or socketless identifier ${identifier}.`); return;
} }
// const requestingSocketId = actingPlayerInfo.socket.id;
if (this.isGameEffectivelyPaused()) { if (this.isGameEffectivelyPaused()) {
actingPlayerInfo.socket.emit('gameError', {message: "Действие невозможно: игра на паузе."}); actingPlayerInfo.socket.emit('gameError', {message: "Действие невозможно: игра на паузе."});
@ -574,16 +568,11 @@ class GameInstance {
this._sayTaunt(attackerState, defenderState.characterKey, 'basicAttack'); 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); gameLogic.performAttack(attackerState, defenderState, attackerData.baseStats, defenderData.baseStats, this.gameState, this.addToLog.bind(this), GAME_CONFIG, dataUtils, gameLogic.getRandomTaunt);
actionIsValidAndPerformed = true; 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) { // Логика бонуса (реген маны) теперь полностью внутри performAttack в combatLogic.js.
const manaRegenConfig = GAME_CONFIG.NATURE_STRENGTH_MANA_REGEN || 0; // GameInstance НЕ ДОЛЖЕН здесь "потреблять" эффект (обнулять turnsLeft или удалять).
const regen = Math.min(manaRegenConfig, attackerData.baseStats.maxResource - attackerState.currentResource); // Длительность эффекта управляется в effectsLogic.js.
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) { } else if (actionData.actionType === 'ability' && actionData.abilityId) {
const ability = attackerData.abilities.find(ab => ab.id === actionData.abilityId); const ability = attackerData.abilities.find(ab => ab.id === actionData.abilityId);
if (!ability) { if (!ability) {
@ -823,7 +812,7 @@ class GameInstance {
this.gameManager._cleanupGame(this.id, "player_surrendered"); this.gameManager._cleanupGame(this.id, "player_surrendered");
} }
handleTurnTimeout() { /* ... Код без изменений, с вызовом _sayTaunt ... */ handleTurnTimeout() { /* ... Код без изменений ... */
if (!this.gameState || this.gameState.isGameOver) return; if (!this.gameState || this.gameState.isGameOver) return;
console.log(`[GameInstance ${this.id}] Turn timeout occurred.`); console.log(`[GameInstance ${this.id}] Turn timeout occurred.`);
const timedOutPlayerRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID; const timedOutPlayerRole = this.gameState.isPlayerTurn ? GAME_CONFIG.PLAYER_ID : GAME_CONFIG.OPPONENT_ID;
@ -840,7 +829,7 @@ class GameInstance {
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.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}`); this.gameManager._cleanupGame(this.id, `timeout_${result.reason}`);
} }
_handleCriticalError(reasonCode, logMessage) { /* ... Код без изменений, с переводом логов ... */ _handleCriticalError(reasonCode, logMessage) { /* ... Код без изменений ... */
console.error(`[GameInstance ${this.id}] CRITICAL ERROR: ${logMessage} (Code: ${reasonCode})`); console.error(`[GameInstance ${this.id}] CRITICAL ERROR: ${logMessage} (Code: ${reasonCode})`);
if (this.gameState && !this.gameState.isGameOver) this.gameState.isGameOver = true; 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 }; else if (!this.gameState) this.gameState = { isGameOver: true, player: {}, opponent: {}, turnNumber: 0, gameMode: this.mode };

View File

@ -1,15 +1,13 @@
// /server/game/logic/combatLogic.js // /server/game/logic/combatLogic.js
// Предполагается, что gameLogic.getRandomTaunt и dataUtils будут доступны // GAME_CONFIG и dataUtils будут передаваться в функции как параметры.
// через параметры, передаваемые из GameInstance, или через объект gameLogic.
// const GAME_CONFIG_STATIC = require('../../core/config'); // Можно, если нужно
/** /**
* Обрабатывает базовую атаку одного бойца по другому. * Обрабатывает базовую атаку одного бойца по другому.
* @param {object} attackerState - Состояние атакующего бойца из gameState. * @param {object} attackerState - Состояние атакующего бойца из gameState.
* @param {object} defenderState - Состояние защищающегося бойца из gameState. * @param {object} defenderState - Состояние защищающегося бойца из gameState.
* @param {object} attackerBaseStats - Базовые статы атакующего. * @param {object} attackerBaseStats - Базовые статы атакующего (из dataUtils.getCharacterBaseStats).
* @param {object} defenderBaseStats - Базовые статы защищающегося. * @param {object} defenderBaseStats - Базовые статы защищающегося (из dataUtils.getCharacterBaseStats).
* @param {object} currentGameState - Текущее полное состояние игры. * @param {object} currentGameState - Текущее полное состояние игры.
* @param {function} addToLogCallback - Функция для добавления сообщений в лог игры. * @param {function} addToLogCallback - Функция для добавления сообщений в лог игры.
* @param {object} configToUse - Конфигурационный объект игры (GAME_CONFIG). * @param {object} configToUse - Конфигурационный объект игры (GAME_CONFIG).
@ -24,35 +22,97 @@ function performAttack(
currentGameState, currentGameState,
addToLogCallback, addToLogCallback,
configToUse, configToUse,
dataUtils, // Добавлен dataUtils dataUtils,
getRandomTauntFunction // Добавлена функция для насмешек getRandomTauntFunction
) { ) {
// Расчет базового урона с вариацией
let damage = Math.floor( let damage = Math.floor(
attackerBaseStats.attackPower * attackerBaseStats.attackPower *
(configToUse.DAMAGE_VARIATION_MIN + Math.random() * configToUse.DAMAGE_VARIATION_RANGE) (configToUse.DAMAGE_VARIATION_MIN + Math.random() * configToUse.DAMAGE_VARIATION_RANGE)
); );
let wasBlocked = false; let wasBlocked = false;
let attackBonusesLog = []; // Для сбора информации о бонусах к атаке
// --- ПРОВЕРКА И ПРИМЕНЕНИЕ БОНУСА ОТ ОТЛОЖЕННОГО БАФФА АТАКИ ---
// Ищем активный бафф, который должен сработать ПРИ атаке
const delayedAttackBuff = attackerState.activeEffects.find(eff =>
eff.isDelayed &&
(eff.id === configToUse.ABILITY_ID_NATURE_STRENGTH || eff.id === configToUse.ABILITY_ID_ALMAGEST_BUFF_ATTACK) &&
eff.turnsLeft > 0 &&
!eff.justCast
);
if (delayedAttackBuff) {
console.log(`[CombatLogic performAttack] Found active delayed buff: ${delayedAttackBuff.name} for ${attackerState.name}`);
// 1. Применяем бонус к урону (если он есть в конфиге/данных эффекта)
let damageBonus = 0;
if (delayedAttackBuff.id === configToUse.ABILITY_ID_NATURE_STRENGTH) {
// Предположим, что Сила Природы НЕ дает прямого бонуса к урону атаки, а только реген маны.
// Если бы давала, то: damageBonus = configToUse.NATURE_STRENGTH_ATTACK_DAMAGE_BONUS || 0;
} else if (delayedAttackBuff.id === configToUse.ABILITY_ID_ALMAGEST_BUFF_ATTACK) {
// Аналогично для Альмагест
// damageBonus = configToUse.ALMAGEST_ATTACK_BUFF_DAMAGE_BONUS || 0;
}
if (damageBonus > 0) {
damage += damageBonus;
attackBonusesLog.push(`урон +${damageBonus} от "${delayedAttackBuff.name}"`);
}
// 2. Восстановление ресурса (для Силы Природы / Усиления Тьмой)
// Этот бонус (восстановление ресурса) срабатывает при каждой атаке, пока эффект активен
let resourceRegenConfigKey = null;
if (delayedAttackBuff.id === configToUse.ABILITY_ID_NATURE_STRENGTH) {
resourceRegenConfigKey = 'NATURE_STRENGTH_MANA_REGEN';
} else if (delayedAttackBuff.id === configToUse.ABILITY_ID_ALMAGEST_BUFF_ATTACK) {
// Предположим, аналогичный конфиг для Альмагест, если она тоже регенит ресурс при атаке под баффом
resourceRegenConfigKey = 'ALMAGEST_DARK_ENERGY_REGEN';
}
if (resourceRegenConfigKey && configToUse[resourceRegenConfigKey]) {
const regenAmount = configToUse[resourceRegenConfigKey];
const actualRegen = Math.min(regenAmount, attackerBaseStats.maxResource - attackerState.currentResource);
if (actualRegen > 0) {
attackerState.currentResource = Math.round(attackerState.currentResource + actualRegen);
if (addToLogCallback) {
addToLogCallback(
`🌿 ${attackerState.name} восстанавливает ${actualRegen} ${attackerState.resourceName} от "${delayedAttackBuff.name}"!`,
configToUse.LOG_TYPE_HEAL
);
}
// Не добавляем в attackBonusesLog, т.к. это отдельное событие, уже залогированное
}
}
// Важно: НЕ МЕНЯЕМ здесь delayedAttackBuff.turnsLeft и НЕ УДАЛЯЕМ эффект.
// Его длительность будет уменьшаться в effectsLogic.processEffects каждый ход владельца эффекта.
}
// --- КОНЕЦ ПРОВЕРКИ И ПРИМЕНЕНИЯ ОТЛОЖЕННОГО БАФФА АТАКИ ---
// Проверка на блок
if (defenderState.isBlocking) { if (defenderState.isBlocking) {
const initialDamage = damage; const initialDamage = damage;
damage = Math.floor(damage * configToUse.BLOCK_DAMAGE_REDUCTION); damage = Math.floor(damage * configToUse.BLOCK_DAMAGE_REDUCTION);
wasBlocked = true; wasBlocked = true;
if (addToLogCallback) { if (addToLogCallback) {
addToLogCallback( let blockLogMsg = `🛡️ ${defenderBaseStats.name} блокирует атаку ${attackerBaseStats.name}! Урон снижен (${initialDamage} -> ${damage}).`;
`🛡️ ${defenderBaseStats.name} блокирует атаку ${attackerBaseStats.name}! Урон снижен (${initialDamage} -> ${damage}).`, if (attackBonusesLog.length > 0) {
configToUse.LOG_TYPE_BLOCK blockLogMsg += ` (${attackBonusesLog.join(', ')})`;
); }
addToLogCallback(blockLogMsg, configToUse.LOG_TYPE_BLOCK);
} }
} else { } else {
if (addToLogCallback) { if (addToLogCallback) {
addToLogCallback( let hitLogMsg = `${attackerBaseStats.name} атакует ${defenderBaseStats.name}! Наносит ${damage} урона.`;
`${attackerBaseStats.name} атакует ${defenderBaseStats.name}! Наносит ${damage} урона.`, if (attackBonusesLog.length > 0) {
configToUse.LOG_TYPE_DAMAGE hitLogMsg += ` (${attackBonusesLog.join(', ')})`;
); }
addToLogCallback(hitLogMsg, configToUse.LOG_TYPE_DAMAGE);
} }
} }
const actualDamageDealt = defenderState.currentHp - Math.max(0, Math.round(defenderState.currentHp - damage)); // Применяем урон, убеждаемся, что HP не ниже нуля
const actualDamageDealtToHp = defenderState.currentHp - Math.max(0, Math.round(defenderState.currentHp - damage)); // Сколько HP реально отнято
defenderState.currentHp = Math.max(0, Math.round(defenderState.currentHp - damage)); defenderState.currentHp = Math.max(0, Math.round(defenderState.currentHp - damage));
// --- Насмешка от защищающегося (defenderState) в ответ на атаку --- // --- Насмешка от защищающегося (defenderState) в ответ на атаку ---
@ -60,20 +120,20 @@ function performAttack(
let reactionTauntTrigger = null; let reactionTauntTrigger = null;
if (wasBlocked) { if (wasBlocked) {
reactionTauntTrigger = 'onOpponentAttackBlocked'; reactionTauntTrigger = 'onOpponentAttackBlocked';
} else if (actualDamageDealt > 0) { // Если урон прошел } else if (actualDamageDealtToHp > 0) {
reactionTauntTrigger = 'onOpponentAttackHit'; reactionTauntTrigger = 'onOpponentAttackHit';
} }
// Можно добавить 'onOpponentAttackMissed' если actualDamageDealt === 0 и !wasBlocked // Можно добавить еще условие для промаха, если урон = 0 и не было блока
if (reactionTauntTrigger) { if (reactionTauntTrigger) {
const attackerFullData = dataUtils.getCharacterData(attackerState.characterKey); const attackerFullDataForTaunt = dataUtils.getCharacterData(attackerState.characterKey);
if (attackerFullData) { // Убедимся, что данные атакующего есть if (attackerFullDataForTaunt) {
const reactionTaunt = getRandomTauntFunction( const reactionTaunt = getRandomTauntFunction(
defenderState.characterKey, // Кто говорит (защищающийся) defenderState.characterKey,
reactionTauntTrigger, // Триггер (onOpponentAttackBlocked или onOpponentAttackHit) reactionTauntTrigger,
{}, // Контекст (пока пустой для этих реакций) {},
configToUse, configToUse,
attackerFullData, // Оппонент для говорящего - это атакующий attackerFullDataForTaunt, // Оппонент для говорящего (защитника) - это атакующий
currentGameState currentGameState
); );
if (reactionTaunt && reactionTaunt !== "(Молчание)") { if (reactionTaunt && reactionTaunt !== "(Молчание)") {
@ -97,7 +157,7 @@ function performAttack(
* @param {object} configToUse - Конфигурация игры. * @param {object} configToUse - Конфигурация игры.
* @param {object} dataUtils - Утилиты для доступа к данным игры. * @param {object} dataUtils - Утилиты для доступа к данным игры.
* @param {function} getRandomTauntFunction - Функция gameLogic.getRandomTaunt. * @param {function} getRandomTauntFunction - Функция gameLogic.getRandomTaunt.
* @param {function} checkIfActionWasSuccessfulFunction - Функция для проверки успеха действия (для контекстных насмешек). * @param {function|null} checkIfActionWasSuccessfulFunction - (Опционально) Функция для проверки успеха действия для контекстных насмешек.
*/ */
function applyAbilityEffect( function applyAbilityEffect(
ability, ability,
@ -108,14 +168,13 @@ function applyAbilityEffect(
currentGameState, currentGameState,
addToLogCallback, addToLogCallback,
configToUse, configToUse,
dataUtils, // Добавлен dataUtils dataUtils,
getRandomTauntFunction, // Добавлена функция для насмешек getRandomTauntFunction,
checkIfActionWasSuccessfulFunction // Добавлена функция для проверки успеха checkIfActionWasSuccessfulFunction // Пока не используется активно, outcome определяется внутри
) { ) {
let abilityApplicationSucceeded = true; // Флаг для отслеживания, применилась ли способность успешно (для контекста насмешек) let abilityApplicationSucceeded = true;
let actionOutcomeForTaunt = null; // 'success' или 'fail' для способностей типа безмолвия let actionOutcomeForTaunt = null; // 'success' или 'fail'
// --- Основная логика применения способности ---
switch (ability.type) { switch (ability.type) {
case configToUse.ACTION_TYPE_HEAL: case configToUse.ACTION_TYPE_HEAL:
const healAmount = Math.floor(ability.power * (configToUse.HEAL_VARIATION_MIN + Math.random() * configToUse.HEAL_VARIATION_RANGE)); const healAmount = Math.floor(ability.power * (configToUse.HEAL_VARIATION_MIN + Math.random() * configToUse.HEAL_VARIATION_RANGE));
@ -125,7 +184,7 @@ function applyAbilityEffect(
if (addToLogCallback) addToLogCallback(`💚 ${casterBaseStats.name} применяет "${ability.name}" и восстанавливает ${actualHeal} HP!`, configToUse.LOG_TYPE_HEAL); if (addToLogCallback) addToLogCallback(`💚 ${casterBaseStats.name} применяет "${ability.name}" и восстанавливает ${actualHeal} HP!`, configToUse.LOG_TYPE_HEAL);
} else { } else {
if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} применяет "${ability.name}", но не получает лечения.`, configToUse.LOG_TYPE_INFO); if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} применяет "${ability.name}", но не получает лечения.`, configToUse.LOG_TYPE_INFO);
abilityApplicationSucceeded = false; // Можно считать это "неудачей" для реакции, если хотите abilityApplicationSucceeded = false;
} }
break; break;
@ -142,21 +201,21 @@ function applyAbilityEffect(
if (addToLogCallback && !wasAbilityBlocked) { if (addToLogCallback && !wasAbilityBlocked) {
addToLogCallback(`💥 ${casterBaseStats.name} применяет "${ability.name}" на ${targetBaseStats.name}, нанося ${damage} урона!`, configToUse.LOG_TYPE_DAMAGE); addToLogCallback(`💥 ${casterBaseStats.name} применяет "${ability.name}" на ${targetBaseStats.name}, нанося ${damage} урона!`, configToUse.LOG_TYPE_DAMAGE);
} }
if (damage <= 0 && !wasAbilityBlocked) abilityApplicationSucceeded = false; // Если урона не было (например, из-за эффектов) if (damage <= 0 && !wasAbilityBlocked) abilityApplicationSucceeded = false;
break; break;
case configToUse.ACTION_TYPE_BUFF: case configToUse.ACTION_TYPE_BUFF:
let effectDescriptionBuff = ability.description; let effectDescriptionBuff = ability.description;
if (typeof ability.descriptionFunction === 'function') { if (typeof ability.descriptionFunction === 'function') {
effectDescriptionBuff = ability.descriptionFunction(configToUse, targetBaseStats); // targetBaseStats здесь оппонент кастера effectDescriptionBuff = ability.descriptionFunction(configToUse, targetBaseStats);
} }
casterState.activeEffects.push({ casterState.activeEffects.push({
id: ability.id, name: ability.name, description: effectDescriptionBuff, id: ability.id, name: ability.name, description: effectDescriptionBuff,
type: ability.type, duration: ability.duration, type: ability.type, duration: ability.duration,
turnsLeft: ability.duration, turnsLeft: ability.duration, // Эффект начнет тикать в конце текущего хода кастера
grantsBlock: !!ability.grantsBlock, grantsBlock: !!ability.grantsBlock,
isDelayed: !!ability.isDelayed, isDelayed: !!ability.isDelayed, // Важно для "Силы Природы"
justCast: true justCast: true // Помечаем, что только что наложен
}); });
if (ability.grantsBlock) require('./effectsLogic').updateBlockingStatus(casterState); if (ability.grantsBlock) require('./effectsLogic').updateBlockingStatus(casterState);
if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} накладывает эффект "${ability.name}"!`, configToUse.LOG_TYPE_EFFECT); if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} накладывает эффект "${ability.name}"!`, configToUse.LOG_TYPE_EFFECT);
@ -164,8 +223,6 @@ function applyAbilityEffect(
case configToUse.ACTION_TYPE_DISABLE: case configToUse.ACTION_TYPE_DISABLE:
if (ability.id === configToUse.ABILITY_ID_HYPNOTIC_GAZE || ability.id === configToUse.ABILITY_ID_ALMAGEST_DISABLE) { if (ability.id === configToUse.ABILITY_ID_HYPNOTIC_GAZE || ability.id === configToUse.ABILITY_ID_ALMAGEST_DISABLE) {
// ... (логика полного безмолвия как у вас)
// Установите actionOutcomeForTaunt = 'success' или 'fail' если нужно
const effectIdFullSilence = ability.id === configToUse.ABILITY_ID_HYPNOTIC_GAZE ? 'fullSilenceByElena' : 'fullSilenceByAlmagest'; const effectIdFullSilence = ability.id === configToUse.ABILITY_ID_HYPNOTIC_GAZE ? 'fullSilenceByElena' : 'fullSilenceByAlmagest';
if (!targetState.activeEffects.some(e => e.id === effectIdFullSilence)) { if (!targetState.activeEffects.some(e => e.id === effectIdFullSilence)) {
targetState.activeEffects.push({ targetState.activeEffects.push({
@ -178,14 +235,13 @@ function applyAbilityEffect(
} else { } else {
if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} пытается применить "${ability.name}", но эффект уже активен на ${targetState.name}!`, configToUse.LOG_TYPE_INFO); if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} пытается применить "${ability.name}", но эффект уже активен на ${targetState.name}!`, configToUse.LOG_TYPE_INFO);
abilityApplicationSucceeded = false; abilityApplicationSucceeded = false;
actionOutcomeForTaunt = 'fail'; // Считаем провалом, если уже активен actionOutcomeForTaunt = 'fail';
} }
} }
else if (ability.id === configToUse.ABILITY_ID_BALARD_SILENCE && casterState.characterKey === 'balard') { else if (ability.id === configToUse.ABILITY_ID_BALARD_SILENCE && casterState.characterKey === 'balard') {
const success = Math.random() < configToUse.SILENCE_SUCCESS_RATE; const success = Math.random() < configToUse.SILENCE_SUCCESS_RATE;
actionOutcomeForTaunt = success ? 'success' : 'fail'; // Устанавливаем для контекста насмешки actionOutcomeForTaunt = success ? 'success' : 'fail';
if (success) { if (success) {
// ... (ваша логика наложения безмолвия на способность)
const targetAbilitiesList = dataUtils.getCharacterAbilities(targetState.characterKey); const targetAbilitiesList = dataUtils.getCharacterAbilities(targetState.characterKey);
const availableAbilitiesToSilence = targetAbilitiesList.filter(pa => const availableAbilitiesToSilence = targetAbilitiesList.filter(pa =>
!targetState.disabledAbilities?.some(d => d.abilityId === pa.id) && !targetState.disabledAbilities?.some(d => d.abilityId === pa.id) &&
@ -194,7 +250,7 @@ function applyAbilityEffect(
if (availableAbilitiesToSilence.length > 0) { if (availableAbilitiesToSilence.length > 0) {
const abilityToSilence = availableAbilitiesToSilence[Math.floor(Math.random() * availableAbilitiesToSilence.length)]; const abilityToSilence = availableAbilitiesToSilence[Math.floor(Math.random() * availableAbilitiesToSilence.length)];
const turns = configToUse.SILENCE_DURATION; const turns = configToUse.SILENCE_DURATION;
targetState.disabledAbilities.push({ abilityId: abilityToSilence.id, turnsLeft: turns + 1 }); targetState.disabledAbilities.push({ abilityId: abilityToSilence.id, turnsLeft: turns + 1 }); // +1 т.к. уменьшится в конце хода цели
targetState.activeEffects.push({ targetState.activeEffects.push({
id: `playerSilencedOn_${abilityToSilence.id}`, name: `Безмолвие: ${abilityToSilence.name}`, id: `playerSilencedOn_${abilityToSilence.id}`, name: `Безмолвие: ${abilityToSilence.name}`,
description: `Способность "${abilityToSilence.name}" временно недоступна.`, description: `Способность "${abilityToSilence.name}" временно недоступна.`,
@ -204,7 +260,7 @@ function applyAbilityEffect(
if (addToLogCallback) addToLogCallback(`🔇 Эхо Безмолвия! "${abilityToSilence.name}" у ${targetBaseStats.name} заблокировано на ${turns} хода!`, configToUse.LOG_TYPE_EFFECT); if (addToLogCallback) addToLogCallback(`🔇 Эхо Безмолвия! "${abilityToSilence.name}" у ${targetBaseStats.name} заблокировано на ${turns} хода!`, configToUse.LOG_TYPE_EFFECT);
} else { } else {
if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} пытается наложить Безмолвие, но у ${targetBaseStats.name} нечего глушить!`, configToUse.LOG_TYPE_INFO); if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} пытается наложить Безмолвие, но у ${targetBaseStats.name} нечего глушить!`, configToUse.LOG_TYPE_INFO);
actionOutcomeForTaunt = 'fail'; // Провал, если нечего глушить actionOutcomeForTaunt = 'fail';
} }
} else { } else {
if (addToLogCallback) addToLogCallback(`💨 Попытка ${casterBaseStats.name} наложить Безмолвие на ${targetBaseStats.name} провалилась!`, configToUse.LOG_TYPE_INFO); if (addToLogCallback) addToLogCallback(`💨 Попытка ${casterBaseStats.name} наложить Безмолвие на ${targetBaseStats.name} провалилась!`, configToUse.LOG_TYPE_INFO);
@ -213,8 +269,6 @@ function applyAbilityEffect(
break; break;
case configToUse.ACTION_TYPE_DEBUFF: case configToUse.ACTION_TYPE_DEBUFF:
// ... (логика дебаффа как у вас)
// Установите actionOutcomeForTaunt если нужно
const effectIdDebuff = 'effect_' + ability.id; const effectIdDebuff = 'effect_' + ability.id;
if (!targetState.activeEffects.some(e => e.id === effectIdDebuff)) { if (!targetState.activeEffects.some(e => e.id === effectIdDebuff)) {
let effectDescriptionDebuff = ability.description; let effectDescriptionDebuff = ability.description;
@ -237,7 +291,6 @@ function applyAbilityEffect(
break; break;
case configToUse.ACTION_TYPE_DRAIN: case configToUse.ACTION_TYPE_DRAIN:
// ... (логика дрейна как у вас)
if (casterState.characterKey === 'balard') { if (casterState.characterKey === 'balard') {
let manaDrained = 0; let healthGained = 0; let damageDealtDrain = 0; let manaDrained = 0; let healthGained = 0; let damageDealtDrain = 0;
if (ability.powerDamage > 0) { if (ability.powerDamage > 0) {
@ -262,7 +315,7 @@ function applyAbilityEffect(
else if (damageDealtDrain > 0) logMsgDrain += `У ${targetBaseStats.name} нет ${targetBaseStats.resourceName} для похищения.`; else if (damageDealtDrain > 0) logMsgDrain += `У ${targetBaseStats.name} нет ${targetBaseStats.resourceName} для похищения.`;
else logMsgDrain += `У ${targetBaseStats.name} нет ${targetBaseStats.resourceName} для похищения.`; else logMsgDrain += `У ${targetBaseStats.name} нет ${targetBaseStats.resourceName} для похищения.`;
if (addToLogCallback) addToLogCallback(logMsgDrain, (manaDrained > 0 || damageDealtDrain > 0) ? configToUse.LOG_TYPE_DAMAGE : configToUse.LOG_TYPE_INFO); if (addToLogCallback) addToLogCallback(logMsgDrain, (manaDrained > 0 || damageDealtDrain > 0) ? configToUse.LOG_TYPE_DAMAGE : configToUse.LOG_TYPE_INFO);
if (manaDrained <= 0 && damageDealtDrain <=0) abilityApplicationSucceeded = false; if (manaDrained <= 0 && damageDealtDrain <=0 && healthGained <=0) abilityApplicationSucceeded = false;
} }
break; break;
@ -273,32 +326,37 @@ function applyAbilityEffect(
} }
// --- Насмешка от цели (targetState) в ответ на применение способности --- // --- Насмешка от цели (targetState) в ответ на применение способности ---
// Вызываем только если способность не была нацелена на самого себя и успешно применилась (или как вы решите) // Вызываем только если способность не была нацелена на самого себя
if (getRandomTauntFunction && dataUtils && casterState.id !== targetState.id && abilityApplicationSucceeded) { if (getRandomTauntFunction && dataUtils && casterState.id !== targetState.id) {
const casterFullData = dataUtils.getCharacterData(casterState.characterKey); const casterFullDataForTaunt = dataUtils.getCharacterData(casterState.characterKey);
if (casterFullData) { // Убедимся, что данные кастера есть if (casterFullDataForTaunt) {
let tauntContext = { abilityId: ability.id }; let tauntContext = { abilityId: ability.id };
if (actionOutcomeForTaunt) { // Если для этой способности важен исход (success/fail) // Если для этой способности был определен исход (например, для безмолвия Баларда), используем его
if (actionOutcomeForTaunt) {
tauntContext.outcome = actionOutcomeForTaunt; tauntContext.outcome = actionOutcomeForTaunt;
} else if (checkIfActionWasSuccessfulFunction) {
// Если есть общая функция проверки успеха (менее специфично, чем actionOutcomeForTaunt)
// Это пример, вам нужно реализовать checkIfActionWasSuccessfulFunction
// const success = checkIfActionWasSuccessfulFunction(ability, casterState, targetState, currentGameState, configToUse);
// tauntContext.outcome = success ? 'success' : 'fail';
} }
// Здесь можно было бы вызвать checkIfActionWasSuccessfulFunction, если бы он был и нужен для других способностей
// else if (checkIfActionWasSuccessfulFunction) {
// const success = checkIfActionWasSuccessfulFunction(ability, casterState, targetState, currentGameState, configToUse);
// tauntContext.outcome = success ? 'success' : 'fail';
// }
// Вызываем насмешку, только если основное применение способности не считается полным провалом (опционально)
// Либо всегда вызываем, и пусть tauntLogic решает, есть ли реакция на "провальную" абилку
// if (abilityApplicationSucceeded || actionOutcomeForTaunt === 'fail') { // Например, реагируем даже на провал Эха Безмолвия
const reactionTaunt = getRandomTauntFunction( const reactionTaunt = getRandomTauntFunction(
targetState.characterKey, // Кто говорит (цель способности) targetState.characterKey, // Кто говорит (цель способности)
'onOpponentAction', // Триггер 'onOpponentAction', // Триггер
tauntContext, // Контекст: ID способности и исход (если нужен) tauntContext, // Контекст: ID способности и, возможно, outcome
configToUse, configToUse,
casterFullData, // Оппонент для говорящего - это кастер casterFullDataForTaunt, // Оппонент для говорящего - это кастер
currentGameState currentGameState
); );
if (reactionTaunt && reactionTaunt !== "(Молчание)") { if (reactionTaunt && reactionTaunt !== "(Молчание)") {
addToLogCallback(`${targetState.name}: "${reactionTaunt}"`, configToUse.LOG_TYPE_INFO); addToLogCallback(`${targetState.name}: "${reactionTaunt}"`, configToUse.LOG_TYPE_INFO);
} }
// }
} }
} }
} }
@ -306,16 +364,10 @@ function applyAbilityEffect(
/** /**
* Проверяет валидность использования способности. * Проверяет валидность использования способности.
* @param {object} ability - Объект способности.
* @param {object} casterState - Состояние кастера.
* @param {object} targetState - Состояние цели.
* @param {object} configToUse - Конфигурация игры.
* @returns {{isValid: boolean, reason: string|null}} Результат проверки.
*/ */
function checkAbilityValidity(ability, casterState, targetState, configToUse) { function checkAbilityValidity(ability, casterState, targetState, configToUse) {
// ... (существующий код checkAbilityValidity без изменений) ... // ... (код checkAbilityValidity без изменений, как вы предоставили) ...
if (!ability) return { isValid: false, reason: "Способность не найдена." }; if (!ability) return { isValid: false, reason: "Способность не найдена." };
if (casterState.currentResource < ability.cost) { if (casterState.currentResource < ability.cost) {
return { isValid: false, reason: `${casterState.name} пытается применить "${ability.name}", но не хватает ${casterState.resourceName}!` }; return { isValid: false, reason: `${casterState.name} пытается применить "${ability.name}", но не хватает ${casterState.resourceName}!` };
} }
@ -330,22 +382,21 @@ function checkAbilityValidity(ability, casterState, targetState, configToUse) {
return { isValid: false, reason: `"${ability.name}" (спец. КД) еще на перезарядке.` }; return { isValid: false, reason: `"${ability.name}" (спец. КД) еще на перезарядке.` };
} }
} }
const isCasterFullySilenced = casterState.activeEffects.some(eff => eff.isFullSilence && eff.turnsLeft > 0); const isCasterFullySilenced = casterState.activeEffects.some(eff => eff.isFullSilence && eff.turnsLeft > 0);
const isAbilitySpecificallySilenced = casterState.disabledAbilities?.some(dis => dis.abilityId === ability.id && dis.turnsLeft > 0); const isAbilitySpecificallySilenced = casterState.disabledAbilities?.some(dis => dis.abilityId === ability.id && dis.turnsLeft > 0);
if (isCasterFullySilenced || isAbilitySpecificallySilenced) { if (isCasterFullySilenced || isAbilitySpecificallySilenced) {
return { isValid: false, reason: `${casterState.name} не может использовать способности из-за безмолвия!` }; return { isValid: false, reason: `${casterState.name} не может использовать способности из-за безмолвия!` };
} }
if (ability.type === configToUse.ACTION_TYPE_BUFF && casterState.activeEffects.some(e => e.id === ability.id)) { if (ability.type === configToUse.ACTION_TYPE_BUFF && casterState.activeEffects.some(e => e.id === ability.id)) {
return { isValid: false, reason: `Эффект "${ability.name}" уже активен!` }; // Исключение для Силы Природы и Усиления Тьмой - их можно обновлять, если isDelayed
if (!ability.isDelayed) {
return { isValid: false, reason: `Эффект "${ability.name}" уже активен!` };
}
} }
const isTargetedDebuff = ability.id === configToUse.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === configToUse.ABILITY_ID_ALMAGEST_DEBUFF; const isTargetedDebuff = ability.id === configToUse.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === configToUse.ABILITY_ID_ALMAGEST_DEBUFF;
if (isTargetedDebuff && targetState.activeEffects.some(e => e.id === 'effect_' + ability.id)) { if (isTargetedDebuff && targetState.activeEffects.some(e => e.id === 'effect_' + ability.id)) {
return { isValid: false, reason: `Эффект "${ability.name}" уже наложен на ${targetState.name}!` }; return { isValid: false, reason: `Эффект "${ability.name}" уже наложен на ${targetState.name}!` };
} }
return { isValid: true, reason: null }; return { isValid: true, reason: null };
} }