Выправление эффекта сила природы.
This commit is contained in:
parent
fb36c3d2e1
commit
12d85b8385
@ -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 };
|
||||||
|
@ -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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user