Исправление логики насмешек и видимости списка приглашений на игру.
This commit is contained in:
parent
63134d4d27
commit
ec459f65d1
66
server/bc.js
66
server/bc.js
@ -11,7 +11,7 @@ const cors = require('cors');
|
|||||||
|
|
||||||
const authService = require('./auth/authService');
|
const authService = require('./auth/authService');
|
||||||
const GameManager = require('./game/GameManager');
|
const GameManager = require('./game/GameManager');
|
||||||
const db = require('./core/db');
|
const db = require('./core/db'); // Используется для auth, не для игр в этом варианте
|
||||||
const GAME_CONFIG = require('./core/config');
|
const GAME_CONFIG = require('./core/config');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
@ -84,7 +84,7 @@ if (!socketCorsOrigin && process.env.NODE_ENV !== 'development' && process.env.N
|
|||||||
}
|
}
|
||||||
|
|
||||||
const io = new Server(server, {
|
const io = new Server(server, {
|
||||||
path: '/socket.io/', // Убедитесь, что это соответствует клиенту и прокси (stripPrefix: false для /socket.io)
|
path: '/socket.io/',
|
||||||
cors: {
|
cors: {
|
||||||
origin: socketCorsOrigin,
|
origin: socketCorsOrigin,
|
||||||
methods: ["GET", "POST"],
|
methods: ["GET", "POST"],
|
||||||
@ -94,7 +94,7 @@ const io = new Server(server, {
|
|||||||
console.log(`[BC.JS CONFIG] Socket.IO server configured with path: ${io.path()} and effective CORS origin: ${io.opts.cors.origin === '*' ? "'*'" : io.opts.cors.origin || 'NOT SET'}`);
|
console.log(`[BC.JS CONFIG] Socket.IO server configured with path: ${io.path()} and effective CORS origin: ${io.opts.cors.origin === '*' ? "'*'" : io.opts.cors.origin || 'NOT SET'}`);
|
||||||
|
|
||||||
const gameManager = new GameManager(io);
|
const gameManager = new GameManager(io);
|
||||||
const loggedInUsersBySocketId = {}; // Хранилище для данных пользователя по ID сокета
|
const loggedInUsersBySocketId = {};
|
||||||
|
|
||||||
// --- MIDDLEWARE АУТЕНТИФИКАЦИИ SOCKET.IO ---
|
// --- MIDDLEWARE АУТЕНТИФИКАЦИИ SOCKET.IO ---
|
||||||
io.use(async (socket, next) => {
|
io.use(async (socket, next) => {
|
||||||
@ -108,17 +108,16 @@ io.use(async (socket, next) => {
|
|||||||
if (token) {
|
if (token) {
|
||||||
try {
|
try {
|
||||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||||
socket.userData = { userId: decoded.userId, username: decoded.username }; // Прикрепляем данные к объекту сокета
|
socket.userData = { userId: decoded.userId, username: decoded.username };
|
||||||
console.log(`[BC Socket.IO Middleware] Socket ${socket.id} authenticated for user ${decoded.username} (ID: ${decoded.userId}).`);
|
console.log(`[BC Socket.IO Middleware] Socket ${socket.id} authenticated for user ${decoded.username} (ID: ${decoded.userId}).`);
|
||||||
return next();
|
return next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(`[BC Socket.IO Middleware] Socket ${socket.id} auth failed: Invalid token. Error: ${err.message}. Proceeding as unauthenticated.`);
|
console.warn(`[BC Socket.IO Middleware] Socket ${socket.id} auth failed: Invalid token. Error: ${err.message}. Proceeding as unauthenticated.`);
|
||||||
// Не прерываем соединение, но userData не будет установлен
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(`[BC Socket.IO Middleware] Socket ${socket.id} has no token. Proceeding as unauthenticated.`);
|
console.log(`[BC Socket.IO Middleware] Socket ${socket.id} has no token. Proceeding as unauthenticated.`);
|
||||||
}
|
}
|
||||||
next(); // Разрешаем подключение даже неаутентифицированным
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- ОБРАБОТЧИКИ СОБЫТИЙ SOCKET.IO ---
|
// --- ОБРАБОТЧИКИ СОБЫТИЙ SOCKET.IO ---
|
||||||
@ -129,7 +128,19 @@ io.on('connection', (socket) => {
|
|||||||
|
|
||||||
if (socket.userData && socket.userData.userId) {
|
if (socket.userData && socket.userData.userId) {
|
||||||
console.log(`[BC Socket.IO Connection] Authenticated user ${socket.userData.username} (ID: ${socket.userData.userId}) connected. Socket: ${socket.id}, IP: ${clientIp}, Origin: ${originHeader}, Path: ${socketPath}`);
|
console.log(`[BC Socket.IO Connection] Authenticated user ${socket.userData.username} (ID: ${socket.userData.userId}) connected. Socket: ${socket.id}, IP: ${clientIp}, Origin: ${originHeader}, Path: ${socketPath}`);
|
||||||
loggedInUsersBySocketId[socket.id] = socket.userData; // Сохраняем данные пользователя, связанные с этим сокетом
|
loggedInUsersBySocketId[socket.id] = socket.userData;
|
||||||
|
|
||||||
|
// --- НАЧАЛО ИЗМЕНЕНИЯ ---
|
||||||
|
// Отправляем текущий список доступных PvP игр этому конкретному сокету
|
||||||
|
// после успешной аутентификации.
|
||||||
|
if (gameManager && typeof gameManager.getAvailablePvPGamesListForClient === 'function') {
|
||||||
|
console.log(`[BC Socket.IO Connection] Sending initial available PvP games list to authenticated user ${socket.userData.username} (Socket: ${socket.id})`);
|
||||||
|
const availableGames = gameManager.getAvailablePvPGamesListForClient();
|
||||||
|
socket.emit('availablePvPGamesList', availableGames);
|
||||||
|
} else {
|
||||||
|
console.error("[BC Socket.IO Connection] CRITICAL: gameManager or getAvailablePvPGamesListForClient not found for sending initial list!");
|
||||||
|
}
|
||||||
|
// --- КОНЕЦ ИЗМЕНЕНИЯ ---
|
||||||
|
|
||||||
if (gameManager && typeof gameManager.handleRequestGameState === 'function') {
|
if (gameManager && typeof gameManager.handleRequestGameState === 'function') {
|
||||||
gameManager.handleRequestGameState(socket, socket.userData.userId);
|
gameManager.handleRequestGameState(socket, socket.userData.userId);
|
||||||
@ -138,19 +149,28 @@ io.on('connection', (socket) => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(`[BC Socket.IO Connection] Unauthenticated user connected. Socket: ${socket.id}, IP: ${clientIp}, Origin: ${originHeader}, Path: ${socketPath}.`);
|
console.log(`[BC Socket.IO Connection] Unauthenticated user connected. Socket: ${socket.id}, IP: ${clientIp}, Origin: ${originHeader}, Path: ${socketPath}.`);
|
||||||
|
// --- НАЧАЛО ИЗМЕНЕНИЯ (опционально, если неаутентифицированные тоже видят список) ---
|
||||||
|
// Если неаутентифицированные пользователи тоже должны видеть список игр
|
||||||
|
/*
|
||||||
|
if (gameManager && typeof gameManager.getAvailablePvPGamesListForClient === 'function') {
|
||||||
|
console.log(`[BC Socket.IO Connection] Sending initial available PvP games list to unauthenticated socket ${socket.id}`);
|
||||||
|
const availableGames = gameManager.getAvailablePvPGamesListForClient();
|
||||||
|
socket.emit('availablePvPGamesList', availableGames);
|
||||||
|
} else {
|
||||||
|
console.error("[BC Socket.IO Connection] CRITICAL: gameManager or getAvailablePvPGamesListForClient not found for sending initial list to unauth user!");
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
// --- КОНЕЦ ИЗМЕНЕНИЯ ---
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.on('logout', () => { // Инициируется клиентом ПЕРЕД разрывом соединения и удалением токена
|
socket.on('logout', () => {
|
||||||
const username = socket.userData?.username || 'UnknownUserOnLogout';
|
const username = socket.userData?.username || 'UnknownUserOnLogout';
|
||||||
const userId = socket.userData?.userId;
|
const userId = socket.userData?.userId;
|
||||||
console.log(`[BC Socket.IO 'logout' event] User: ${username} (ID: ${userId || 'N/A'}, Socket: ${socket.id}).`);
|
console.log(`[BC Socket.IO 'logout' event] User: ${username} (ID: ${userId || 'N/A'}, Socket: ${socket.id}).`);
|
||||||
// Логика GameManager.handleDisconnect будет вызвана при фактическом 'disconnect' событии.
|
|
||||||
// Здесь мы просто очищаем данные, связанные с этим сокетом на сервере,
|
|
||||||
// так как клиент собирается разорвать соединение или переподключиться без токена.
|
|
||||||
if (loggedInUsersBySocketId[socket.id]) {
|
if (loggedInUsersBySocketId[socket.id]) {
|
||||||
delete loggedInUsersBySocketId[socket.id];
|
delete loggedInUsersBySocketId[socket.id];
|
||||||
}
|
}
|
||||||
socket.userData = null; // Очищаем данные на самом объекте сокета
|
socket.userData = null;
|
||||||
console.log(`[BC Socket.IO 'logout' event] Session data for socket ${socket.id} cleared on server.`);
|
console.log(`[BC Socket.IO 'logout' event] Session data for socket ${socket.id} cleared on server.`);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -171,7 +191,6 @@ io.on('connection', (socket) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- НАЧАЛО ИЗМЕНЕНИЯ: ОБРАБОТЧИК ДЛЯ ВЫХОДА ИЗ AI-ИГРЫ ---
|
|
||||||
socket.on('leaveAiGame', () => {
|
socket.on('leaveAiGame', () => {
|
||||||
if (!socket.userData?.userId) {
|
if (!socket.userData?.userId) {
|
||||||
console.warn(`[BC Socket.IO 'leaveAiGame'] Denied for unauthenticated socket ${socket.id}.`);
|
console.warn(`[BC Socket.IO 'leaveAiGame'] Denied for unauthenticated socket ${socket.id}.`);
|
||||||
@ -184,14 +203,11 @@ io.on('connection', (socket) => {
|
|||||||
|
|
||||||
if (gameManager && typeof gameManager.handleLeaveAiGame === 'function') {
|
if (gameManager && typeof gameManager.handleLeaveAiGame === 'function') {
|
||||||
gameManager.handleLeaveAiGame(identifier);
|
gameManager.handleLeaveAiGame(identifier);
|
||||||
// Ответ клиенту не требуется, т.к. он и так выходит и переходит на другой экран.
|
|
||||||
// GameManager._cleanupGame будет вызван изнутри handleLeaveAiGame через GameInstance.
|
|
||||||
} else {
|
} else {
|
||||||
console.error("[BC Socket.IO 'leaveAiGame'] CRITICAL: gameManager or handleLeaveAiGame method not found!");
|
console.error("[BC Socket.IO 'leaveAiGame'] CRITICAL: gameManager or handleLeaveAiGame method not found!");
|
||||||
socket.emit('gameError', { message: 'Ошибка сервера при выходе из AI игры.' });
|
socket.emit('gameError', { message: 'Ошибка сервера при выходе из AI игры.' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// --- КОНЕЦ ИЗМЕНЕНИЯ ---
|
|
||||||
|
|
||||||
socket.on('createGame', (data) => {
|
socket.on('createGame', (data) => {
|
||||||
if (!socket.userData?.userId) {
|
if (!socket.userData?.userId) {
|
||||||
@ -207,7 +223,6 @@ io.on('connection', (socket) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on('joinGame', (data) => {
|
socket.on('joinGame', (data) => {
|
||||||
// ... (код без изменений)
|
|
||||||
if (!socket.userData?.userId) {
|
if (!socket.userData?.userId) {
|
||||||
console.warn(`[BC Socket.IO 'joinGame'] Denied for unauthenticated socket ${socket.id}.`);
|
console.warn(`[BC Socket.IO 'joinGame'] Denied for unauthenticated socket ${socket.id}.`);
|
||||||
socket.emit('gameError', { message: 'Необходимо войти для присоединения к PvP игре.' });
|
socket.emit('gameError', { message: 'Необходимо войти для присоединения к PvP игре.' });
|
||||||
@ -220,7 +235,6 @@ io.on('connection', (socket) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on('findRandomGame', (data) => {
|
socket.on('findRandomGame', (data) => {
|
||||||
// ... (код без изменений)
|
|
||||||
if (!socket.userData?.userId) {
|
if (!socket.userData?.userId) {
|
||||||
console.warn(`[BC Socket.IO 'findRandomGame'] Denied for unauthenticated socket ${socket.id}.`);
|
console.warn(`[BC Socket.IO 'findRandomGame'] Denied for unauthenticated socket ${socket.id}.`);
|
||||||
socket.emit('gameError', { message: 'Необходимо войти для поиска случайной PvP игры.' });
|
socket.emit('gameError', { message: 'Необходимо войти для поиска случайной PvP игры.' });
|
||||||
@ -233,14 +247,17 @@ io.on('connection', (socket) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on('requestPvPGameList', () => {
|
socket.on('requestPvPGameList', () => {
|
||||||
// ... (код без изменений)
|
|
||||||
console.log(`[BC Socket.IO 'requestPvPGameList'] Request from socket ${socket.id} (User: ${socket.userData?.username || 'Unauth'}).`);
|
console.log(`[BC Socket.IO 'requestPvPGameList'] Request from socket ${socket.id} (User: ${socket.userData?.username || 'Unauth'}).`);
|
||||||
const availableGames = gameManager.getAvailablePvPGamesListForClient();
|
if (gameManager && typeof gameManager.getAvailablePvPGamesListForClient === 'function') {
|
||||||
socket.emit('availablePvPGamesList', availableGames);
|
const availableGames = gameManager.getAvailablePvPGamesListForClient();
|
||||||
|
socket.emit('availablePvPGamesList', availableGames);
|
||||||
|
} else {
|
||||||
|
console.error("[BC Socket.IO 'requestPvPGameList'] CRITICAL: gameManager or getAvailablePvPGamesListForClient not found!");
|
||||||
|
socket.emit('availablePvPGamesList', []); // Отправляем пустой список в случае ошибки
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('requestGameState', () => {
|
socket.on('requestGameState', () => {
|
||||||
// ... (код без изменений)
|
|
||||||
if (!socket.userData?.userId) {
|
if (!socket.userData?.userId) {
|
||||||
console.warn(`[BC Socket.IO 'requestGameState'] Denied for unauthenticated socket ${socket.id}.`);
|
console.warn(`[BC Socket.IO 'requestGameState'] Denied for unauthenticated socket ${socket.id}.`);
|
||||||
socket.emit('gameNotFound', { message: 'Необходимо войти для восстановления игры.' });
|
socket.emit('gameNotFound', { message: 'Необходимо войти для восстановления игры.' });
|
||||||
@ -252,7 +269,6 @@ io.on('connection', (socket) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on('playerAction', (actionData) => {
|
socket.on('playerAction', (actionData) => {
|
||||||
// ... (код без изменений)
|
|
||||||
if (!socket.userData?.userId) {
|
if (!socket.userData?.userId) {
|
||||||
console.warn(`[BC Socket.IO 'playerAction'] Denied for unauthenticated socket ${socket.id}. Action: ${actionData?.actionType}`);
|
console.warn(`[BC Socket.IO 'playerAction'] Denied for unauthenticated socket ${socket.id}. Action: ${actionData?.actionType}`);
|
||||||
socket.emit('gameError', { message: 'Действие не разрешено: пользователь не аутентифицирован.' });
|
socket.emit('gameError', { message: 'Действие не разрешено: пользователь не аутентифицирован.' });
|
||||||
@ -264,7 +280,7 @@ io.on('connection', (socket) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on('disconnect', (reason) => {
|
socket.on('disconnect', (reason) => {
|
||||||
const identifier = socket.userData?.userId; // Берем из userData, если был аутентифицирован
|
const identifier = socket.userData?.userId;
|
||||||
const username = socket.userData?.username || loggedInUsersBySocketId[socket.id]?.username || 'UnauthenticatedOrUnknown';
|
const username = socket.userData?.username || loggedInUsersBySocketId[socket.id]?.username || 'UnauthenticatedOrUnknown';
|
||||||
|
|
||||||
console.log(`[BC Socket.IO Disconnect] User ${username} (ID: ${identifier || 'N/A'}, Socket: ${socket.id}) disconnected. Reason: ${reason}.`);
|
console.log(`[BC Socket.IO Disconnect] User ${username} (ID: ${identifier || 'N/A'}, Socket: ${socket.id}) disconnected. Reason: ${reason}.`);
|
||||||
@ -274,7 +290,6 @@ io.on('connection', (socket) => {
|
|||||||
if (loggedInUsersBySocketId[socket.id]) {
|
if (loggedInUsersBySocketId[socket.id]) {
|
||||||
delete loggedInUsersBySocketId[socket.id];
|
delete loggedInUsersBySocketId[socket.id];
|
||||||
}
|
}
|
||||||
// socket.userData автоматически очистится при уничтожении объекта сокета
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -308,6 +323,5 @@ process.on('unhandledRejection', (reason, promise) => {
|
|||||||
|
|
||||||
process.on('uncaughtException', (err) => {
|
process.on('uncaughtException', (err) => {
|
||||||
console.error('[BC Server FATAL UncaughtException] Error:', err);
|
console.error('[BC Server FATAL UncaughtException] Error:', err);
|
||||||
// В продакшене PM2 или другой менеджер процессов должен перезапустить приложение
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
@ -10,7 +10,7 @@ const tauntSystem = {
|
|||||||
balard: { // Против Баларда (AI)
|
balard: { // Против Баларда (AI)
|
||||||
// Триггер: Елена использует СВОЮ способность
|
// Триггер: Елена использует СВОЮ способность
|
||||||
selfCastAbility: {
|
selfCastAbility: {
|
||||||
[GAME_CONFIG.ABILITY_ID_HEAL]: [ "Свет лечит, Балард. Но не искаженную завистью искру.", "Я черпаю силы в Истине." ],
|
[GAME_CONFIG.ABILITY_ID_HEAL]: [ "Свет лечит, Балард. Но не искаженную завистью искру.", "Я черпаю силы в Истине."],
|
||||||
[GAME_CONFIG.ABILITY_ID_FIREBALL]: [ "Прими очищающее пламя Света!", "Пусть твой мрак сгорит!" ],
|
[GAME_CONFIG.ABILITY_ID_FIREBALL]: [ "Прими очищающее пламя Света!", "Пусть твой мрак сгорит!" ],
|
||||||
[GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH]: [ "Сама земля отвергает тебя, я черпаю её силу!", "Гармония природы со мной." ],
|
[GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH]: [ "Сама земля отвергает тебя, я черпаю её силу!", "Гармония природы со мной." ],
|
||||||
[GAME_CONFIG.ABILITY_ID_DEFENSE_AURA]: [ "Порядок восторжествует над твоим хаосом.", "Моя вера - моя защита." ],
|
[GAME_CONFIG.ABILITY_ID_DEFENSE_AURA]: [ "Порядок восторжествует над твоим хаосом.", "Моя вера - моя защита." ],
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
// /server/game/logic/combatLogic.js
|
// /server/game/logic/combatLogic.js
|
||||||
|
|
||||||
// GAME_CONFIG и dataUtils будут передаваться в функции как параметры.
|
// GAME_CONFIG и dataUtils будут передаваться в функции как параметры.
|
||||||
|
// effectsLogic может потребоваться для импорта, если updateBlockingStatus используется здесь напрямую,
|
||||||
|
// но в вашем GameInstance.js он вызывается отдельно.
|
||||||
|
// const effectsLogic = require('./effectsLogic'); // Если нужно
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Обрабатывает базовую атаку одного бойца по другому.
|
* Обрабатывает базовую атаку одного бойца по другому.
|
||||||
@ -34,7 +37,6 @@ function performAttack(
|
|||||||
let attackBonusesLog = []; // Для сбора информации о бонусах к атаке
|
let attackBonusesLog = []; // Для сбора информации о бонусах к атаке
|
||||||
|
|
||||||
// --- ПРОВЕРКА И ПРИМЕНЕНИЕ БОНУСА ОТ ОТЛОЖЕННОГО БАФФА АТАКИ ---
|
// --- ПРОВЕРКА И ПРИМЕНЕНИЕ БОНУСА ОТ ОТЛОЖЕННОГО БАФФА АТАКИ ---
|
||||||
// Ищем активный бафф, который должен сработать ПРИ атаке
|
|
||||||
const delayedAttackBuff = attackerState.activeEffects.find(eff =>
|
const delayedAttackBuff = attackerState.activeEffects.find(eff =>
|
||||||
eff.isDelayed &&
|
eff.isDelayed &&
|
||||||
(eff.id === configToUse.ABILITY_ID_NATURE_STRENGTH || eff.id === configToUse.ABILITY_ID_ALMAGEST_BUFF_ATTACK) &&
|
(eff.id === configToUse.ABILITY_ID_NATURE_STRENGTH || eff.id === configToUse.ABILITY_ID_ALMAGEST_BUFF_ATTACK) &&
|
||||||
@ -45,28 +47,25 @@ function performAttack(
|
|||||||
if (delayedAttackBuff) {
|
if (delayedAttackBuff) {
|
||||||
console.log(`[CombatLogic performAttack] Found active delayed buff: ${delayedAttackBuff.name} for ${attackerState.name}`);
|
console.log(`[CombatLogic performAttack] Found active delayed buff: ${delayedAttackBuff.name} for ${attackerState.name}`);
|
||||||
|
|
||||||
// 1. Применяем бонус к урону (если он есть в конфиге/данных эффекта)
|
|
||||||
let damageBonus = 0;
|
let damageBonus = 0;
|
||||||
if (delayedAttackBuff.id === configToUse.ABILITY_ID_NATURE_STRENGTH) {
|
// Если бы были прямые бонусы к урону атаки от этих баффов, они бы рассчитывались здесь
|
||||||
// Предположим, что Сила Природы НЕ дает прямого бонуса к урону атаки, а только реген маны.
|
// Например:
|
||||||
// Если бы давала, то: damageBonus = configToUse.NATURE_STRENGTH_ATTACK_DAMAGE_BONUS || 0;
|
// if (delayedAttackBuff.id === configToUse.ABILITY_ID_NATURE_STRENGTH && configToUse.NATURE_STRENGTH_ATTACK_DAMAGE_BONUS) {
|
||||||
} else if (delayedAttackBuff.id === configToUse.ABILITY_ID_ALMAGEST_BUFF_ATTACK) {
|
// damageBonus = configToUse.NATURE_STRENGTH_ATTACK_DAMAGE_BONUS;
|
||||||
// Аналогично для Альмагест
|
// } else if (delayedAttackBuff.id === configToUse.ABILITY_ID_ALMAGEST_BUFF_ATTACK && configToUse.ALMAGEST_ATTACK_BUFF_DAMAGE_BONUS) {
|
||||||
// damageBonus = configToUse.ALMAGEST_ATTACK_BUFF_DAMAGE_BONUS || 0;
|
// damageBonus = configToUse.ALMAGEST_ATTACK_BUFF_DAMAGE_BONUS;
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (damageBonus > 0) {
|
if (damageBonus > 0) {
|
||||||
damage += damageBonus;
|
damage += damageBonus;
|
||||||
attackBonusesLog.push(`урон +${damageBonus} от "${delayedAttackBuff.name}"`);
|
attackBonusesLog.push(`урон +${damageBonus} от "${delayedAttackBuff.name}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Восстановление ресурса (для Силы Природы / Усиления Тьмой)
|
|
||||||
// Этот бонус (восстановление ресурса) срабатывает при каждой атаке, пока эффект активен
|
|
||||||
let resourceRegenConfigKey = null;
|
let resourceRegenConfigKey = null;
|
||||||
if (delayedAttackBuff.id === configToUse.ABILITY_ID_NATURE_STRENGTH) {
|
if (delayedAttackBuff.id === configToUse.ABILITY_ID_NATURE_STRENGTH) {
|
||||||
resourceRegenConfigKey = 'NATURE_STRENGTH_MANA_REGEN';
|
resourceRegenConfigKey = 'NATURE_STRENGTH_MANA_REGEN';
|
||||||
} else if (delayedAttackBuff.id === configToUse.ABILITY_ID_ALMAGEST_BUFF_ATTACK) {
|
} else if (delayedAttackBuff.id === configToUse.ABILITY_ID_ALMAGEST_BUFF_ATTACK) {
|
||||||
// Предположим, аналогичный конфиг для Альмагест, если она тоже регенит ресурс при атаке под баффом
|
resourceRegenConfigKey = 'ALMAGEST_DARK_ENERGY_REGEN'; // Предположительный ключ
|
||||||
resourceRegenConfigKey = 'ALMAGEST_DARK_ENERGY_REGEN';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resourceRegenConfigKey && configToUse[resourceRegenConfigKey]) {
|
if (resourceRegenConfigKey && configToUse[resourceRegenConfigKey]) {
|
||||||
@ -80,12 +79,8 @@ function performAttack(
|
|||||||
configToUse.LOG_TYPE_HEAL
|
configToUse.LOG_TYPE_HEAL
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Не добавляем в attackBonusesLog, т.к. это отдельное событие, уже залогированное
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Важно: НЕ МЕНЯЕМ здесь delayedAttackBuff.turnsLeft и НЕ УДАЛЯЕМ эффект.
|
|
||||||
// Его длительность будет уменьшаться в effectsLogic.processEffects каждый ход владельца эффекта.
|
|
||||||
}
|
}
|
||||||
// --- КОНЕЦ ПРОВЕРКИ И ПРИМЕНЕНИЯ ОТЛОЖЕННОГО БАФФА АТАКИ ---
|
// --- КОНЕЦ ПРОВЕРКИ И ПРИМЕНЕНИЯ ОТЛОЖЕННОГО БАФФА АТАКИ ---
|
||||||
|
|
||||||
@ -112,28 +107,29 @@ function performAttack(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Применяем урон, убеждаемся, что HP не ниже нуля
|
// Применяем урон, убеждаемся, что HP не ниже нуля
|
||||||
const actualDamageDealtToHp = defenderState.currentHp - Math.max(0, Math.round(defenderState.currentHp - damage)); // Сколько HP реально отнято
|
const actualDamageDealtToHp = Math.min(defenderState.currentHp, damage); // Сколько HP реально отнято (не может быть больше текущего HP)
|
||||||
defenderState.currentHp = Math.max(0, Math.round(defenderState.currentHp - damage));
|
defenderState.currentHp = Math.max(0, Math.round(defenderState.currentHp - damage));
|
||||||
|
|
||||||
// --- Насмешка от защищающегося (defenderState) в ответ на атаку ---
|
// --- Насмешка от защищающегося (defenderState) в ответ на атаку ---
|
||||||
if (getRandomTauntFunction && dataUtils) {
|
if (getRandomTauntFunction && dataUtils) {
|
||||||
let reactionTauntTrigger = null;
|
let subTriggerForTaunt = null;
|
||||||
if (wasBlocked) {
|
if (wasBlocked) {
|
||||||
reactionTauntTrigger = 'onOpponentAttackBlocked';
|
subTriggerForTaunt = 'attackBlocked';
|
||||||
} else if (actualDamageDealtToHp > 0) {
|
} else if (actualDamageDealtToHp > 0) { // Если не было блока, но был нанесен урон
|
||||||
reactionTauntTrigger = 'onOpponentAttackHit';
|
subTriggerForTaunt = 'attackHits';
|
||||||
}
|
}
|
||||||
// Можно добавить еще условие для промаха, если урон = 0 и не было блока
|
// Можно добавить еще условие для промаха, если урон = 0 и не было блока (и actualDamageDealtToHp === 0)
|
||||||
|
// else if (damage <= 0 && !wasBlocked) { subTriggerForTaunt = 'attackMissed'; } // Если есть такой триггер
|
||||||
|
|
||||||
if (reactionTauntTrigger) {
|
if (subTriggerForTaunt) {
|
||||||
const attackerFullDataForTaunt = dataUtils.getCharacterData(attackerState.characterKey);
|
const attackerFullDataForTaunt = dataUtils.getCharacterData(attackerState.characterKey);
|
||||||
if (attackerFullDataForTaunt) {
|
if (attackerFullDataForTaunt) {
|
||||||
const reactionTaunt = getRandomTauntFunction(
|
const reactionTaunt = getRandomTauntFunction(
|
||||||
defenderState.characterKey,
|
defenderState.characterKey, // Говорящий (защитник)
|
||||||
reactionTauntTrigger,
|
'onOpponentAction', // Главный триггер
|
||||||
{},
|
subTriggerForTaunt, // Подтриггер: 'attackBlocked' или 'attackHits'
|
||||||
configToUse,
|
configToUse,
|
||||||
attackerFullDataForTaunt, // Оппонент для говорящего (защитника) - это атакующий
|
attackerFullDataForTaunt, // Оппонент (атакующий) для говорящего
|
||||||
currentGameState
|
currentGameState
|
||||||
);
|
);
|
||||||
if (reactionTaunt && reactionTaunt !== "(Молчание)") {
|
if (reactionTaunt && reactionTaunt !== "(Молчание)") {
|
||||||
@ -172,8 +168,8 @@ function applyAbilityEffect(
|
|||||||
getRandomTauntFunction,
|
getRandomTauntFunction,
|
||||||
checkIfActionWasSuccessfulFunction // Пока не используется активно, outcome определяется внутри
|
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:
|
||||||
@ -182,46 +178,69 @@ function applyAbilityEffect(
|
|||||||
if (actualHeal > 0) {
|
if (actualHeal > 0) {
|
||||||
casterState.currentHp = Math.round(casterState.currentHp + actualHeal);
|
casterState.currentHp = Math.round(casterState.currentHp + actualHeal);
|
||||||
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);
|
||||||
|
actionOutcomeForTaunt = 'success'; // Для реакции оппонента, если таковая есть на хил
|
||||||
} else {
|
} else {
|
||||||
if (addToLogCallback) addToLogCallback(`✨ ${casterBaseStats.name} применяет "${ability.name}", но не получает лечения.`, configToUse.LOG_TYPE_INFO);
|
if (addToLogCallback) addToLogCallback(`✨ ${casterBaseStats.name} применяет "${ability.name}", но не получает лечения (HP уже полное или хил = 0).`, configToUse.LOG_TYPE_INFO);
|
||||||
abilityApplicationSucceeded = false;
|
abilityApplicationSucceeded = false;
|
||||||
|
actionOutcomeForTaunt = 'fail';
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case configToUse.ACTION_TYPE_DAMAGE:
|
case configToUse.ACTION_TYPE_DAMAGE:
|
||||||
let damage = Math.floor(ability.power * (configToUse.DAMAGE_VARIATION_MIN + Math.random() * configToUse.DAMAGE_VARIATION_RANGE));
|
let damage = Math.floor(ability.power * (configToUse.DAMAGE_VARIATION_MIN + Math.random() * configToUse.DAMAGE_VARIATION_RANGE));
|
||||||
let wasAbilityBlocked = false;
|
let wasAbilityBlocked = false;
|
||||||
|
let actualDamageDealtByAbility = 0;
|
||||||
|
|
||||||
if (targetState.isBlocking) {
|
if (targetState.isBlocking) {
|
||||||
const initialDamage = damage;
|
const initialDamage = damage;
|
||||||
damage = Math.floor(damage * configToUse.BLOCK_DAMAGE_REDUCTION);
|
damage = Math.floor(damage * configToUse.BLOCK_DAMAGE_REDUCTION);
|
||||||
wasAbilityBlocked = true;
|
wasAbilityBlocked = true;
|
||||||
if (addToLogCallback) addToLogCallback(`🛡️ ${targetBaseStats.name} блокирует "${ability.name}" от ${casterBaseStats.name}! Урон снижен (${initialDamage} -> ${damage}).`, configToUse.LOG_TYPE_BLOCK);
|
if (addToLogCallback) addToLogCallback(`🛡️ ${targetBaseStats.name} блокирует "${ability.name}" от ${casterBaseStats.name}! Урон снижен (${initialDamage} -> ${damage}).`, configToUse.LOG_TYPE_BLOCK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actualDamageDealtByAbility = Math.min(targetState.currentHp, damage);
|
||||||
targetState.currentHp = Math.max(0, Math.round(targetState.currentHp - damage));
|
targetState.currentHp = Math.max(0, Math.round(targetState.currentHp - damage));
|
||||||
|
|
||||||
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;
|
||||||
|
actionOutcomeForTaunt = 'fail';
|
||||||
|
} else if (wasAbilityBlocked) {
|
||||||
|
actionOutcomeForTaunt = 'blocked'; // Специальный исход для реакции на блок способности
|
||||||
|
} else if (actualDamageDealtByAbility > 0) {
|
||||||
|
actionOutcomeForTaunt = 'hit'; // Специальный исход для реакции на попадание способностью
|
||||||
|
} else {
|
||||||
|
actionOutcomeForTaunt = 'fail'; // Если урон 0 и не было блока (например цель уже мертва и 0 хп)
|
||||||
|
}
|
||||||
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);
|
effectDescriptionBuff = ability.descriptionFunction(configToUse, targetBaseStats); // targetBaseStats здесь может быть casterBaseStats, если бафф на себя
|
||||||
}
|
}
|
||||||
|
// Обычно баффы накладываются на кастера
|
||||||
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 && casterState.activeEffects.find(e => e.id === ability.id && e.grantsBlock)) {
|
||||||
|
// Требуется effectsLogic.updateBlockingStatus(casterState);
|
||||||
|
// но GameInstance вызывает его в switchTurn, так что здесь можно не дублировать, если эффект не мгновенный
|
||||||
|
}
|
||||||
if (addToLogCallback) addToLogCallback(`✨ ${casterBaseStats.name} накладывает эффект "${ability.name}"!`, configToUse.LOG_TYPE_EFFECT);
|
if (addToLogCallback) addToLogCallback(`✨ ${casterBaseStats.name} накладывает эффект "${ability.name}"!`, configToUse.LOG_TYPE_EFFECT);
|
||||||
|
actionOutcomeForTaunt = 'success'; // Для реакции оппонента, если бафф на себя
|
||||||
break;
|
break;
|
||||||
|
|
||||||
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) {
|
||||||
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)) {
|
||||||
@ -230,7 +249,7 @@ function applyAbilityEffect(
|
|||||||
type: ability.type, duration: ability.effectDuration, turnsLeft: ability.effectDuration,
|
type: ability.type, duration: ability.effectDuration, turnsLeft: ability.effectDuration,
|
||||||
power: ability.power, isFullSilence: true, justCast: true
|
power: ability.power, isFullSilence: true, justCast: true
|
||||||
});
|
});
|
||||||
if (addToLogCallback) addToLogCallback(`🌀 ${casterBaseStats.name} применяет "${ability.name}" на ${targetBaseStats.name}! Способности заблокированы на ${ability.effectDuration} хода и наносится урон!`, configToUse.LOG_TYPE_EFFECT);
|
if (addToLogCallback) addToLogCallback(`🌀 ${casterBaseStats.name} применяет "${ability.name}" на ${targetBaseStats.name}! Способности цели заблокированы на ${ability.effectDuration} хода!`, configToUse.LOG_TYPE_EFFECT);
|
||||||
actionOutcomeForTaunt = 'success';
|
actionOutcomeForTaunt = 'success';
|
||||||
} 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);
|
||||||
@ -238,19 +257,21 @@ function applyAbilityEffect(
|
|||||||
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'; // Этот outcome используется в tauntLogic
|
||||||
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) &&
|
||||||
!targetState.activeEffects?.some(eff => eff.id === `playerSilencedOn_${pa.id}`)
|
!targetState.activeEffects?.some(eff => eff.id === `playerSilencedOn_${pa.id}`) &&
|
||||||
|
pa.id !== configToUse.ABILITY_ID_NONE // Исключаем "пустую" абилку, если она есть
|
||||||
);
|
);
|
||||||
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 }); // +1 т.к. уменьшится в конце хода цели
|
targetState.disabledAbilities.push({ abilityId: abilityToSilence.id, turnsLeft: turns + 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}" временно недоступна.`,
|
||||||
@ -260,7 +281,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);
|
||||||
@ -269,7 +290,7 @@ function applyAbilityEffect(
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case configToUse.ACTION_TYPE_DEBUFF:
|
case configToUse.ACTION_TYPE_DEBUFF:
|
||||||
const effectIdDebuff = 'effect_' + ability.id;
|
const effectIdDebuff = 'effect_' + ability.id; // Уникальный 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;
|
||||||
if (typeof ability.descriptionFunction === 'function') {
|
if (typeof ability.descriptionFunction === 'function') {
|
||||||
@ -281,7 +302,7 @@ function applyAbilityEffect(
|
|||||||
duration: ability.effectDuration, turnsLeft: ability.effectDuration,
|
duration: ability.effectDuration, turnsLeft: ability.effectDuration,
|
||||||
power: ability.power, justCast: true
|
power: ability.power, justCast: true
|
||||||
});
|
});
|
||||||
if (addToLogCallback) addToLogCallback(`📉 ${casterBaseStats.name} накладывает "${ability.name}" на ${targetBaseStats.name}! Ресурс будет сжигаться.`, configToUse.LOG_TYPE_EFFECT);
|
if (addToLogCallback) addToLogCallback(`📉 ${casterBaseStats.name} накладывает "${ability.name}" на ${targetBaseStats.name}! Эффект продлится ${ability.effectDuration} хода.`, configToUse.LOG_TYPE_EFFECT);
|
||||||
actionOutcomeForTaunt = 'success';
|
actionOutcomeForTaunt = 'success';
|
||||||
} 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);
|
||||||
@ -290,65 +311,81 @@ function applyAbilityEffect(
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case configToUse.ACTION_TYPE_DRAIN:
|
case configToUse.ACTION_TYPE_DRAIN: // Пример для Манадрейна Баларда
|
||||||
if (casterState.characterKey === 'balard') {
|
if (casterState.characterKey === 'balard' && ability.id === configToUse.ABILITY_ID_BALARD_MANA_DRAIN) {
|
||||||
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) {
|
||||||
let baseDamageDrain = ability.powerDamage;
|
let baseDamageDrain = ability.powerDamage;
|
||||||
if (targetState.isBlocking) baseDamageDrain = Math.floor(baseDamageDrain * configToUse.BLOCK_DAMAGE_REDUCTION);
|
if (targetState.isBlocking) { // Маловероятно, что дрейны блокируются, но для полноты
|
||||||
|
baseDamageDrain = Math.floor(baseDamageDrain * configToUse.BLOCK_DAMAGE_REDUCTION);
|
||||||
|
}
|
||||||
damageDealtDrain = Math.max(0, baseDamageDrain);
|
damageDealtDrain = Math.max(0, baseDamageDrain);
|
||||||
targetState.currentHp = Math.max(0, Math.round(targetState.currentHp - damageDealtDrain));
|
targetState.currentHp = Math.max(0, Math.round(targetState.currentHp - damageDealtDrain));
|
||||||
}
|
}
|
||||||
const potentialDrain = ability.powerManaDrain;
|
const potentialDrain = ability.powerManaDrain;
|
||||||
const actualDrain = Math.min(potentialDrain, targetState.currentResource);
|
const actualDrain = Math.min(potentialDrain, targetState.currentResource);
|
||||||
|
|
||||||
if (actualDrain > 0) {
|
if (actualDrain > 0) {
|
||||||
targetState.currentResource = Math.max(0, Math.round(targetState.currentResource - actualDrain));
|
targetState.currentResource = Math.max(0, Math.round(targetState.currentResource - actualDrain));
|
||||||
manaDrained = actualDrain;
|
manaDrained = actualDrain;
|
||||||
const potentialHeal = Math.floor(manaDrained * ability.powerHealthGainFactor);
|
const potentialHeal = Math.floor(manaDrained * (ability.powerHealthGainFactor || 0)); // Убедимся, что фактор есть
|
||||||
const actualHealGain = Math.min(potentialHeal, casterBaseStats.maxHp - casterState.currentHp);
|
const actualHealGain = Math.min(potentialHeal, casterBaseStats.maxHp - casterState.currentHp);
|
||||||
casterState.currentHp = Math.round(casterState.currentHp + actualHealGain);
|
if (actualHealGain > 0) {
|
||||||
healthGained = actualHealGain;
|
casterState.currentHp = Math.round(casterState.currentHp + actualHealGain);
|
||||||
|
healthGained = actualHealGain;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let logMsgDrain = `⚡ ${casterBaseStats.name} применяет "${ability.name}"! `;
|
let logMsgDrain = `⚡ ${casterBaseStats.name} применяет "${ability.name}"! `;
|
||||||
if (damageDealtDrain > 0) logMsgDrain += `Наносит ${damageDealtDrain} урона. `;
|
if (damageDealtDrain > 0) logMsgDrain += `Наносит ${damageDealtDrain} урона ${targetBaseStats.name}. `;
|
||||||
if (manaDrained > 0) logMsgDrain += `Вытягивает ${manaDrained} ${targetBaseStats.resourceName} у ${targetBaseStats.name} и исцеляется на ${healthGained} HP!`;
|
if (manaDrained > 0) {
|
||||||
else if (damageDealtDrain > 0) logMsgDrain += `У ${targetBaseStats.name} нет ${targetBaseStats.resourceName} для похищения.`;
|
logMsgDrain += `Вытягивает ${manaDrained} ${targetBaseStats.resourceName} у ${targetBaseStats.name}`;
|
||||||
else logMsgDrain += `У ${targetBaseStats.name} нет ${targetBaseStats.resourceName} для похищения.`;
|
if(healthGained > 0) logMsgDrain += ` и исцеляется на ${healthGained} HP!`; else logMsgDrain += `!`;
|
||||||
|
} else if (damageDealtDrain > 0) {
|
||||||
|
logMsgDrain += `${targetBaseStats.name} не имеет ${targetBaseStats.resourceName} для похищения.`;
|
||||||
|
} else {
|
||||||
|
logMsgDrain += `Не удалось ничего похитить у ${targetBaseStats.name}.`;
|
||||||
|
}
|
||||||
|
|
||||||
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 && healthGained <=0) abilityApplicationSucceeded = false;
|
|
||||||
|
if (manaDrained <= 0 && damageDealtDrain <= 0 && healthGained <= 0) {
|
||||||
|
abilityApplicationSucceeded = false;
|
||||||
|
actionOutcomeForTaunt = 'fail';
|
||||||
|
} else {
|
||||||
|
actionOutcomeForTaunt = 'success';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
if (addToLogCallback) addToLogCallback(`Неизвестный тип способности: ${ability?.type} для "${ability?.name}"`, configToUse.LOG_TYPE_SYSTEM);
|
if (addToLogCallback) addToLogCallback(`Неизвестный тип способности: ${ability?.type} для "${ability?.name}"`, configToUse.LOG_TYPE_SYSTEM);
|
||||||
console.warn(`applyAbilityEffect: Неизвестный тип способности: ${ability?.type}`);
|
console.warn(`applyAbilityEffect: Неизвестный тип способности: ${ability?.type} для способности ${ability?.id}`);
|
||||||
abilityApplicationSucceeded = false;
|
abilityApplicationSucceeded = false;
|
||||||
|
actionOutcomeForTaunt = 'fail';
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Насмешка от цели (targetState) в ответ на применение способности ---
|
// --- Насмешка от цели (targetState) в ответ на применение способности оппонентом (casterState) ---
|
||||||
// Вызываем только если способность не была нацелена на самого себя
|
// Вызываем только если способность не была нацелена на самого себя И есть функция насмешек
|
||||||
if (getRandomTauntFunction && dataUtils && casterState.id !== targetState.id) {
|
if (getRandomTauntFunction && dataUtils && casterState.id !== targetState.id) {
|
||||||
const casterFullDataForTaunt = dataUtils.getCharacterData(casterState.characterKey);
|
const casterFullDataForTaunt = dataUtils.getCharacterData(casterState.characterKey);
|
||||||
if (casterFullDataForTaunt) {
|
if (casterFullDataForTaunt) {
|
||||||
let tauntContext = { abilityId: ability.id };
|
let tauntContext = { abilityId: ability.id };
|
||||||
// Если для этой способности был определен исход (например, для безмолвия Баларда), используем его
|
|
||||||
if (actionOutcomeForTaunt) {
|
// Если для этой способности был определен исход (например, для безмолвия Баларда, или попадание/блок урона)
|
||||||
|
// Используем actionOutcomeForTaunt, который мы установили в switch-case выше
|
||||||
|
if (actionOutcomeForTaunt === 'success' || actionOutcomeForTaunt === 'fail' || actionOutcomeForTaunt === 'blocked' || actionOutcomeForTaunt === 'hit') {
|
||||||
tauntContext.outcome = actionOutcomeForTaunt;
|
tauntContext.outcome = actionOutcomeForTaunt;
|
||||||
}
|
}
|
||||||
// Здесь можно было бы вызвать checkIfActionWasSuccessfulFunction, если бы он был и нужен для других способностей
|
// Для способностей типа DAMAGE, 'blocked' и 'hit' будут ключами в taunts.js (например, Elena onOpponentAction -> ABILITY_ID_ALMAGEST_DAMAGE -> blocked: [...])
|
||||||
// else if (checkIfActionWasSuccessfulFunction) {
|
// Это не стандартные 'attackBlocked' и 'attackHits', а специфичные для реакции на *способность*
|
||||||
// const success = checkIfActionWasSuccessfulFunction(ability, casterState, targetState, currentGameState, configToUse);
|
// Если вы хотите использовать общие 'attackBlocked'/'attackHits' и для способностей, вам нужно будет изменить логику в taunts.js
|
||||||
// tauntContext.outcome = success ? 'success' : 'fail';
|
// или передавать здесь другие subTrigger'ы, если способность заблокирована/попала.
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
// Вызываем насмешку, только если основное применение способности не считается полным провалом (опционально)
|
|
||||||
// Либо всегда вызываем, и пусть tauntLogic решает, есть ли реакция на "провальную" абилку
|
|
||||||
// if (abilityApplicationSucceeded || actionOutcomeForTaunt === 'fail') { // Например, реагируем даже на провал Эха Безмолвия
|
|
||||||
const reactionTaunt = getRandomTauntFunction(
|
const reactionTaunt = getRandomTauntFunction(
|
||||||
targetState.characterKey, // Кто говорит (цель способности)
|
targetState.characterKey, // Кто говорит (цель способности)
|
||||||
'onOpponentAction', // Триггер
|
'onOpponentAction', // Триггер
|
||||||
tauntContext, // Контекст: ID способности и, возможно, outcome
|
tauntContext, // Контекст: ID способности кастера (оппонента) и, возможно, outcome
|
||||||
configToUse,
|
configToUse,
|
||||||
casterFullDataForTaunt, // Оппонент для говорящего - это кастер
|
casterFullDataForTaunt, // Оппонент для говорящего - это кастер
|
||||||
currentGameState
|
currentGameState
|
||||||
@ -356,7 +393,6 @@ function applyAbilityEffect(
|
|||||||
if (reactionTaunt && reactionTaunt !== "(Молчание)") {
|
if (reactionTaunt && reactionTaunt !== "(Молчание)") {
|
||||||
addToLogCallback(`${targetState.name}: "${reactionTaunt}"`, configToUse.LOG_TYPE_INFO);
|
addToLogCallback(`${targetState.name}: "${reactionTaunt}"`, configToUse.LOG_TYPE_INFO);
|
||||||
}
|
}
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -364,39 +400,67 @@ function applyAbilityEffect(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Проверяет валидность использования способности.
|
* Проверяет валидность использования способности.
|
||||||
|
* @param {object} ability - Объект способности.
|
||||||
|
* @param {object} casterState - Состояние бойца, который пытается применить способность.
|
||||||
|
* @param {object} targetState - Состояние цели (может быть тем же, что и casterState).
|
||||||
|
* @param {object} configToUse - Конфигурационный объект игры (GAME_CONFIG).
|
||||||
|
* @returns {object} - { isValid: boolean, reason: string|null }
|
||||||
*/
|
*/
|
||||||
function checkAbilityValidity(ability, casterState, targetState, configToUse) {
|
function checkAbilityValidity(ability, casterState, targetState, configToUse) {
|
||||||
// ... (код 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} (${casterState.currentResource}/${ability.cost})!` };
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((casterState.abilityCooldowns?.[ability.id] || 0) > 0) {
|
if ((casterState.abilityCooldowns?.[ability.id] || 0) > 0) {
|
||||||
return { isValid: false, reason: `"${ability.name}" еще на перезарядке.` };
|
return { isValid: false, reason: `"${ability.name}" еще на перезарядке (${casterState.abilityCooldowns[ability.id]} х.).` };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Специальные кулдауны для Баларда
|
||||||
if (casterState.characterKey === 'balard') {
|
if (casterState.characterKey === 'balard') {
|
||||||
if (ability.id === configToUse.ABILITY_ID_BALARD_SILENCE && (casterState.silenceCooldownTurns || 0) > 0) {
|
if (ability.id === configToUse.ABILITY_ID_BALARD_SILENCE && (casterState.silenceCooldownTurns || 0) > 0) {
|
||||||
return { isValid: false, reason: `"${ability.name}" (спец. КД) еще на перезарядке.` };
|
return { isValid: false, reason: `"${ability.name}" (спец. КД) еще на перезарядке (${casterState.silenceCooldownTurns} х.).` };
|
||||||
}
|
}
|
||||||
if (ability.id === configToUse.ABILITY_ID_BALARD_MANA_DRAIN && (casterState.manaDrainCooldownTurns || 0) > 0) {
|
if (ability.id === configToUse.ABILITY_ID_BALARD_MANA_DRAIN && (casterState.manaDrainCooldownTurns || 0) > 0) {
|
||||||
return { isValid: false, reason: `"${ability.name}" (спец. КД) еще на перезарядке.` };
|
return { isValid: false, reason: `"${ability.name}" (спец. КД) еще на перезарядке (${casterState.manaDrainCooldownTurns} х.).` };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Проверка на безмолвие
|
||||||
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) {
|
||||||
return { isValid: false, reason: `${casterState.name} не может использовать способности из-за безмолвия!` };
|
return { isValid: false, reason: `${casterState.name} не может использовать способности из-за полного безмолвия!` };
|
||||||
}
|
}
|
||||||
|
if (isAbilitySpecificallySilenced) {
|
||||||
|
const specificSilenceEffect = casterState.disabledAbilities.find(dis => dis.abilityId === ability.id);
|
||||||
|
return { isValid: false, reason: `Способность "${ability.name}" у ${casterState.name} временно заблокирована (${specificSilenceEffect.turnsLeft} х.)!` };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Проверка наложения баффа, который уже активен (кроме обновляемых)
|
||||||
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)) {
|
||||||
// Исключение для Силы Природы и Усиления Тьмой - их можно обновлять, если isDelayed
|
// Исключение для "отложенных" баффов, которые можно обновлять (например, Сила Природы)
|
||||||
if (!ability.isDelayed) {
|
if (!ability.isDelayed) { // Если isDelayed не true, то нельзя обновлять.
|
||||||
return { isValid: false, reason: `Эффект "${ability.name}" уже активен!` };
|
return { isValid: false, reason: `Эффект "${ability.name}" уже активен у ${casterState.name}!` };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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)) {
|
// Проверка наложения дебаффа, который уже активен на цели
|
||||||
return { isValid: false, reason: `Эффект "${ability.name}" уже наложен на ${targetState.name}!` };
|
const isTargetedDebuff = ability.type === configToUse.ACTION_TYPE_DEBUFF ||
|
||||||
|
(ability.type === configToUse.ACTION_TYPE_DISABLE && ability.id !== configToUse.ABILITY_ID_BALARD_SILENCE); // Безмолвие Баларда может пытаться наложиться повторно (и провалиться)
|
||||||
|
|
||||||
|
if (isTargetedDebuff && targetState.id !== casterState.id) { // Убедимся, что это не бафф на себя, проверяемый как дебафф
|
||||||
|
const effectIdToCheck = (ability.type === configToUse.ACTION_TYPE_DISABLE && ability.id !== configToUse.ABILITY_ID_BALARD_SILENCE) ?
|
||||||
|
(ability.id === configToUse.ABILITY_ID_HYPNOTIC_GAZE ? 'fullSilenceByElena' : 'fullSilenceByAlmagest') :
|
||||||
|
('effect_' + ability.id);
|
||||||
|
|
||||||
|
if (targetState.activeEffects.some(e => e.id === effectIdToCheck)) {
|
||||||
|
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