Загрузить файлы в «server_modules»
Новая версия
This commit is contained in:
parent
dd35157869
commit
cf3c4705ce
@ -65,7 +65,7 @@ const playerAbilities = [
|
|||||||
type: GAME_CONFIG.ACTION_TYPE_BUFF,
|
type: GAME_CONFIG.ACTION_TYPE_BUFF,
|
||||||
duration: 4,
|
duration: 4,
|
||||||
descriptionFunction: (config) => `Восст. ${config.NATURE_STRENGTH_MANA_REGEN} маны при след. атаке (${4 - 1} хода)`,
|
descriptionFunction: (config) => `Восст. ${config.NATURE_STRENGTH_MANA_REGEN} маны при след. атаке (${4 - 1} хода)`,
|
||||||
isDelayed: true
|
isDelayed: true // Этот эффект применяется ПОСЛЕ следующей атаки, а не сразу
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: GAME_CONFIG.ABILITY_ID_DEFENSE_AURA,
|
id: GAME_CONFIG.ABILITY_ID_DEFENSE_AURA,
|
||||||
@ -73,7 +73,7 @@ const playerAbilities = [
|
|||||||
cost: 15,
|
cost: 15,
|
||||||
type: GAME_CONFIG.ACTION_TYPE_BUFF,
|
type: GAME_CONFIG.ACTION_TYPE_BUFF,
|
||||||
duration: 3,
|
duration: 3,
|
||||||
grantsBlock: true,
|
grantsBlock: true, // Дает эффект блока на время действия
|
||||||
descriptionFunction: (config) => `Снижает урон на ${config.BLOCK_DAMAGE_REDUCTION * 100}% (${3} хода)`
|
descriptionFunction: (config) => `Снижает урон на ${config.BLOCK_DAMAGE_REDUCTION * 100}% (${3} хода)`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -81,7 +81,7 @@ const playerAbilities = [
|
|||||||
name: 'Гипнотический взгляд',
|
name: 'Гипнотический взгляд',
|
||||||
cost: 30,
|
cost: 30,
|
||||||
type: GAME_CONFIG.ACTION_TYPE_DISABLE,
|
type: GAME_CONFIG.ACTION_TYPE_DISABLE,
|
||||||
effectDuration: 2, // Длительность безмолвия
|
effectDuration: 2, // Длительность безмолвия в ходах противника
|
||||||
cooldown: 6,
|
cooldown: 6,
|
||||||
power: 5, // Урон в ход от взгляда
|
power: 5, // Урон в ход от взгляда
|
||||||
description: 'Накладывает на противника полное безмолвие на 2 хода и наносит 5 урона каждый его ход. КД: 6 х.'
|
description: 'Накладывает на противника полное безмолвие на 2 хода и наносит 5 урона каждый его ход. КД: 6 х.'
|
||||||
@ -91,11 +91,11 @@ const playerAbilities = [
|
|||||||
name: 'Печать Слабости',
|
name: 'Печать Слабости',
|
||||||
cost: 30,
|
cost: 30,
|
||||||
type: GAME_CONFIG.ACTION_TYPE_DEBUFF,
|
type: GAME_CONFIG.ACTION_TYPE_DEBUFF,
|
||||||
effectDuration: 3,
|
effectDuration: 3, // Длительность дебаффа
|
||||||
power: 10, // Количество ресурса противника, сжигаемое каждый ход
|
power: 10, // Количество ресурса противника, сжигаемое каждый ход
|
||||||
cooldown: 5,
|
cooldown: 5,
|
||||||
// Описание теперь может адаптироваться к ресурсу оппонента
|
// Описание теперь может адаптироваться к ресурсу оппонента
|
||||||
descriptionFunction: (config, oppStats) => `Накладывает печать, сжигающую 10 ${oppStats.resourceName} противника каждый его ход в течение 3 ходов. КД: 5 х.`
|
descriptionFunction: (config, oppStats) => `Накладывает печать, сжигающую 10 ${oppStats.resourceName} противника каждый его ход в течение 3 ходов. КД: 5 х.'`
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -107,8 +107,9 @@ const opponentAbilities = [
|
|||||||
cost: 20,
|
cost: 20,
|
||||||
type: GAME_CONFIG.ACTION_TYPE_HEAL,
|
type: GAME_CONFIG.ACTION_TYPE_HEAL,
|
||||||
power: 25,
|
power: 25,
|
||||||
successRate: 0.60, // Можно вынести в GAME_CONFIG
|
successRate: 0.60, // Шанс успеха
|
||||||
description: 'Исцеляет ~25 HP с 60% шансом',
|
description: 'Исцеляет ~25 HP с 60% шансом',
|
||||||
|
// Условие для AI: HP ниже порога
|
||||||
condition: (opSt, plSt, currentGameState, config) => {
|
condition: (opSt, plSt, currentGameState, config) => {
|
||||||
return (opSt.currentHp / opSt.maxHp) * 100 < config.OPPONENT_HEAL_THRESHOLD_PERCENT;
|
return (opSt.currentHp / opSt.maxHp) * 100 < config.OPPONENT_HEAL_THRESHOLD_PERCENT;
|
||||||
}
|
}
|
||||||
@ -118,34 +119,35 @@ const opponentAbilities = [
|
|||||||
name: 'Эхо Безмолвия',
|
name: 'Эхо Безмолвия',
|
||||||
cost: GAME_CONFIG.BALARD_SILENCE_ABILITY_COST,
|
cost: GAME_CONFIG.BALARD_SILENCE_ABILITY_COST,
|
||||||
type: GAME_CONFIG.ACTION_TYPE_DISABLE,
|
type: GAME_CONFIG.ACTION_TYPE_DISABLE,
|
||||||
|
// Описание с адаптацией
|
||||||
descriptionFunction: (config) => `Шанс ${config.SILENCE_SUCCESS_RATE * 100}% заглушить случайное заклинание Елены на ${config.SILENCE_DURATION} х.`,
|
descriptionFunction: (config) => `Шанс ${config.SILENCE_SUCCESS_RATE * 100}% заглушить случайное заклинание Елены на ${config.SILENCE_DURATION} х.`,
|
||||||
|
// Условие для AI: HP выше порога лечения, Елена не заглушена, не на спец. КД
|
||||||
condition: (opSt, plSt, currentGameState, config) => {
|
condition: (opSt, plSt, currentGameState, config) => {
|
||||||
const hpPercent = (opSt.currentHp / opSt.maxHp) * 100;
|
const hpPercent = (opSt.currentHp / opSt.maxHp) * 100;
|
||||||
const isElenaAlreadySilenced = currentGameState?.player.disabledAbilities?.length > 0 ||
|
const isElenaAlreadySilenced = currentGameState?.player.disabledAbilities?.length > 0 ||
|
||||||
currentGameState?.player.activeEffects?.some(eff => eff.id.startsWith('playerSilencedOn_'));
|
currentGameState?.player.activeEffects?.some(eff => eff.id.startsWith('playerSilencedOn_'));
|
||||||
// Условие для Баларда использовать безмолвие (только против Елены)
|
return hpPercent >= config.OPPONENT_HEAL_THRESHOLD_PERCENT && !isElenaAlreadySilenced && opSt.silenceCooldownTurns <= 0;
|
||||||
return hpPercent >= config.OPPONENT_HEAL_THRESHOLD_PERCENT && !isElenaAlreadySilenced && currentGameState.opponent.silenceCooldownTurns <= 0;
|
|
||||||
},
|
},
|
||||||
successRateFromConfig: 'SILENCE_SUCCESS_RATE',
|
successRateFromConfig: 'SILENCE_SUCCESS_RATE', // Шанс берется из конфига
|
||||||
durationFromConfig: 'SILENCE_DURATION',
|
durationFromConfig: 'SILENCE_DURATION', // Длительность берется из конфига
|
||||||
internalCooldownFromConfig: 'BALARD_SILENCE_INTERNAL_COOLDOWN'
|
internalCooldownFromConfig: 'BALARD_SILENCE_INTERNAL_COOLDOWN' // Спец. КД берется из конфига
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN,
|
id: GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN,
|
||||||
name: 'Похищение Света',
|
name: 'Похищение Света',
|
||||||
cost: 10,
|
cost: 10,
|
||||||
type: GAME_CONFIG.ACTION_TYPE_DRAIN,
|
type: GAME_CONFIG.ACTION_TYPE_DRAIN,
|
||||||
powerManaDrain: 5,
|
powerManaDrain: 5, // Сколько маны вытягивает
|
||||||
powerDamage: 5,
|
powerDamage: 5, // Сколько урона наносит дополнительно
|
||||||
powerHealthGainFactor: 1.0,
|
powerHealthGainFactor: 1.0, // Множитель для расчета лечения от вытянутой маны
|
||||||
description: `Вытягивает 5 Маны у Елены, наносит 5 урона и восстанавливает себе здоровье (100% от украденного).`,
|
description: `Вытягивает 5 Маны у Елены, наносит 5 урона и восстанавливает себе здоровье (100% от украденного).`,
|
||||||
|
// Условие для AI: У Елены достаточно маны, не на спец. КД
|
||||||
condition: (opSt, plSt, currentGameState, config) => {
|
condition: (opSt, plSt, currentGameState, config) => {
|
||||||
const playerManaPercent = (plSt.currentResource / plSt.maxResource) * 100;
|
const playerManaPercent = (plSt.currentResource / plSt.maxResource) * 100;
|
||||||
const playerHasHighMana = playerManaPercent > (config.BALARD_MANA_DRAIN_HIGH_MANA_THRESHOLD || 60);
|
const playerHasHighMana = playerManaPercent > (config.BALARD_MANA_DRAIN_HIGH_MANA_THRESHOLD || 60);
|
||||||
// Условие для Баларда использовать (только против Елены)
|
return playerHasHighMana && opSt.manaDrainCooldownTurns <= 0;
|
||||||
return playerHasHighMana && currentGameState.opponent.manaDrainCooldownTurns <= 0;
|
|
||||||
},
|
},
|
||||||
internalCooldownValue: 3
|
internalCooldownValue: 3 // Спец. КД задается здесь
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -175,7 +177,7 @@ const almagestAbilities = [
|
|||||||
duration: 4,
|
duration: 4,
|
||||||
// Аналогично Силе Природы, но использует Темную Энергию
|
// Аналогично Силе Природы, но использует Темную Энергию
|
||||||
descriptionFunction: (config) => `Восст. ${config.NATURE_STRENGTH_MANA_REGEN} Темной Энергии при след. атаке (${4 - 1} хода)`,
|
descriptionFunction: (config) => `Восст. ${config.NATURE_STRENGTH_MANA_REGEN} Темной Энергии при след. атаке (${4 - 1} хода)`,
|
||||||
isDelayed: true
|
isDelayed: true // Этот эффект применяется ПОСЛЕ следующей атаки
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_DEFENSE,
|
id: GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_DEFENSE,
|
||||||
@ -183,7 +185,7 @@ const almagestAbilities = [
|
|||||||
cost: 15,
|
cost: 15,
|
||||||
type: GAME_CONFIG.ACTION_TYPE_BUFF,
|
type: GAME_CONFIG.ACTION_TYPE_BUFF,
|
||||||
duration: 3,
|
duration: 3,
|
||||||
grantsBlock: true,
|
grantsBlock: true, // Дает эффект блока на время действия
|
||||||
descriptionFunction: (config) => `Создает щит, снижающий урон на ${config.BLOCK_DAMAGE_REDUCTION * 100}% (${3} хода)`
|
descriptionFunction: (config) => `Создает щит, снижающий урон на ${config.BLOCK_DAMAGE_REDUCTION * 100}% (${3} хода)`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -209,154 +211,113 @@ const almagestAbilities = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
// --- Система Насмешек Елены (Переработанная) ---
|
// --- Система Насмешек ---
|
||||||
// Ключи верхнего уровня: 'aiBalard' для игры против Баларда (AI)
|
// Ключи верхнего уровня: characterKey того, кто произносит насмешку ('elena', 'almagest')
|
||||||
// 'pvpAlmagest' для игры против Альмагест (PvP)
|
// Внутри каждого characterKey: ключи characterKey противника ('balard', 'almagest', 'elena')
|
||||||
const elenaTauntSystem = {
|
// Внутри каждого противника: секции по триггерам (battleStart, selfCastAbility, onOpponentAction, etc.)
|
||||||
aiBalard: { // Насмешки против Баларда (AI) - существующая логика
|
|
||||||
base: {
|
const tauntSystem = {
|
||||||
mercifulAttack: [ "Балард, прошу, остановись. Еще не поздно.", /* ... другие ... */ ],
|
elena: { // Насмешки Елены
|
||||||
mercifulCast: [ "Даже сейчас, я пытаюсь исцелить не только тело...", /* ... другие ... */ ],
|
balard: { // Против Баларда (AI)
|
||||||
dominating: {
|
// Триггер: Елена использует СВОЮ способность
|
||||||
creatorVsCreation: [ "Глина не спорит с гончаром, Балард! Прекрати жалкое сопротивление!", /* ... другие ... */ ],
|
selfCastAbility: {
|
||||||
betrayalOfLight: [ "Ты мог ходить в сиянии Света! Ты ИЗБРАЛ эту гниль! Получай возмездие!", /* ... другие ... */ ],
|
[GAME_CONFIG.ABILITY_ID_HEAL]: [ "Свет лечит, Балард. Но не искаженную завистью искру.", "Я черпаю силы в Истине." ],
|
||||||
ingratitudeContempt: [ "Самый страшный грех - грех неблагодарности!", /* ... другие ... */ ],
|
[GAME_CONFIG.ABILITY_ID_FIREBALL]: [ "Прими очищающее пламя Света!", "Пусть твой мрак сгорит!" ],
|
||||||
unmakingThreats: [ "Я сотру тебя с лика этой земли, как досадную ошибку!", /* ... другие ... */ ]
|
[GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH]: [ "Сама земля отвергает тебя, я черпаю её силу!", "Гармония природы со мной." ],
|
||||||
|
[GAME_CONFIG.ABILITY_ID_DEFENSE_AURA]: [ "Порядок восторжествует над твоим хаосом.", "Моя вера - моя защита." ],
|
||||||
|
[GAME_CONFIG.ABILITY_ID_HYPNOTIC_GAZE]: [ "Смотри мне в глаза, Балард. И слушай тишину.", "Твой разум - в моей власти." ],
|
||||||
|
[GAME_CONFIG.ABILITY_ID_SEAL_OF_WEAKNESS]: [ "Твоя ярость иссякнет, как вода в песке, Балард!", "Твоя сила угасает." ]
|
||||||
|
},
|
||||||
|
// Триггер: Противник (Балард) совершает действие
|
||||||
|
onOpponentAction: {
|
||||||
|
[GAME_CONFIG.ABILITY_ID_BALARD_HEAL]: [ "Пытаешься отсрочить неизбежное жалкой темной силой?" ],
|
||||||
|
[GAME_CONFIG.ABILITY_ID_BALARD_SILENCE]: {
|
||||||
|
success: [ "(Сдавленный вздох)... Ничтожная попытка заглушить Слово!" ],
|
||||||
|
fail: [ "Твой шепот Тьмы слаб против Света Истины!" ]
|
||||||
|
},
|
||||||
|
[GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN]: [ "Ты питаешься Светом, как паразит?!" ],
|
||||||
|
attackBlocked: [ "Твои удары тщетны перед щитом Порядка." ], // При блоке атаки Баларда
|
||||||
|
attackHits: [ "(Шипение боли)... Боль – лишь напоминание о твоем предательстве." ] // При попадании атаки Баларда
|
||||||
|
},
|
||||||
|
// Триггер: Базовая атака Елены
|
||||||
|
basicAttack: {
|
||||||
|
merciful: [ "Балард, прошу, остановись. Еще не поздно.", "Подумай о том, что потерял." ],
|
||||||
|
dominating: [ // Когда HP Баларда ниже порога PLAYER_MERCY_TAUNT_THRESHOLD_PERCENT
|
||||||
|
"Глина не спорит с гончаром, Балард!",
|
||||||
|
"Ты ИЗБРАЛ эту гниль! Получай возмездие!",
|
||||||
|
"Самый страшный грех - грех неблагодарности!",
|
||||||
|
"Я сотру тебя с лика этой земли!"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// Триггер: Изменение состояния боя (например, начало, оппонент почти побежден)
|
||||||
|
onBattleState: {
|
||||||
|
start: [ "Балард, есть ли еще путь назад?" ], // Начало AI боя с Балардом
|
||||||
|
opponentNearDefeat: [ "Конец близок, Балард. Прими свою судьбу." ] // Балард почти побежден
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPlayerCast: {
|
almagest: { // Против Альмагест (PvP)
|
||||||
[GAME_CONFIG.ABILITY_ID_HEAL]: [ "Свет лечит, Балард. Но не искаженную завистью искру.", /* ... другие ... */ ],
|
// Триггер: Елена использует СВОЮ способность
|
||||||
[GAME_CONFIG.ABILITY_ID_FIREBALL]: [ "Прими очищающее пламя Света!", /* ... другие ... */ ],
|
selfCastAbility: {
|
||||||
[GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH]: [ "Сама земля отвергает тебя, я черпаю её силу!", /* ... другие ... */ ],
|
[GAME_CONFIG.ABILITY_ID_HEAL]: [ "Я исцеляюсь Светом, который ты отвергла.", "Жизнь восторжествует над твоей некромантией!", "Мое сияние не померкнет." ],
|
||||||
[GAME_CONFIG.ABILITY_ID_DEFENSE_AURA]: [ "Порядок восторжествует над твоим хаосом.", /* ... другие ... */ ],
|
[GAME_CONFIG.ABILITY_ID_FIREBALL]: [ "Очищающий огонь для твоей тьмы!", "Почувствуй гнев праведного Света!", "Это пламя ярче твоих теней!" ],
|
||||||
[GAME_CONFIG.ABILITY_ID_HYPNOTIC_GAZE]: [ "Смотри мне в глаза, Балард. И слушай тишину.", /* ... другие ... */ ],
|
[GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH]: [ "Природа дает мне силу, а тебе - лишь презрение.", "Я черпаю из источника жизни, ты - из могилы." ],
|
||||||
[GAME_CONFIG.ABILITY_ID_SEAL_OF_WEAKNESS]: [ "Твоя ярость иссякнет, как вода в песке, Балард!", /* ... другие ... */ ]
|
[GAME_CONFIG.ABILITY_ID_DEFENSE_AURA]: [ "Мой щит отразит твою злобу.", "Свет - лучшая защита.", "Твои темные чары не пройдут!" ],
|
||||||
},
|
[GAME_CONFIG.ABILITY_ID_HYPNOTIC_GAZE]: [ "Смотри в глаза Истине, колдунья!", "Твои лживые речи умолкнут!", "Хватит прятаться за иллюзиями!" ],
|
||||||
onOpponentAction: { // Действия Баларда
|
[GAME_CONFIG.ABILITY_ID_SEAL_OF_WEAKNESS]: [ "Твоя темная сила иссякнет!", "Я ослабляю твою связь с бездной!", "Почувствуй, как тает твоя энергия!" ]
|
||||||
[GAME_CONFIG.ABILITY_ID_BALARD_HEAL]: [ "Пытаешься отсрочить неизбежное жалкой темной силой?", /* ... другие ... */ ],
|
|
||||||
[GAME_CONFIG.ABILITY_ID_BALARD_SILENCE]: {
|
|
||||||
success: [ "(Сдавленный вздох)... Ничтожная попытка заглушить Слово!", /* ... другие ... */ ],
|
|
||||||
fail: [ "Твой шепот Тьмы слаб против Света Истины!", /* ... другие ... */ ]
|
|
||||||
},
|
},
|
||||||
[GAME_CONFIG.ABILITY_ID_BALARD_MANA_DRAIN]: [ "Ты питаешься Светом, как паразит?!", /* ... другие ... */ ],
|
// Триггер: Противник (Альмагест) совершает действие
|
||||||
attackBlocked: [ "Твои удары тщетны перед щитом Порядка.", /* ... другие ... */ ],
|
onOpponentAction: {
|
||||||
attackHits: [ "(Шипение боли)... Боль – лишь напоминание о твоем предательстве.", /* ... другие ... */ ]
|
[GAME_CONFIG.ABILITY_ID_ALMAGEST_HEAL]: [ "Лечишь раны тьмой? Она лишь глубже проникнет в тебя.", "Твоя магия несет лишь порчу, даже исцеляя." ],
|
||||||
},
|
[GAME_CONFIG.ABILITY_ID_ALMAGEST_DAMAGE]: [ "Твоя тень лишь царапает, не ранит.", "Слабый удар! Тьма делает тебя немощной." ],
|
||||||
onBattleState: {
|
[GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_ATTACK]: [ "Черпаешь силы из бездны? Она поглотит и тебя.", "Твое усиление - лишь агония искаженной энергии." ],
|
||||||
startMerciful: [ "Балард, есть ли еще путь назад?", /* ... другие ... */ ],
|
[GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_DEFENSE]: [ "Щит из теней? Он рассыпется прахом!", "Твоя защита иллюзорна, как и твоя сила." ],
|
||||||
opponentNearDefeat: [ "Конец близок, Балард. Прими свою судьбу.", /* ... другие ... */ ]
|
[GAME_CONFIG.ABILITY_ID_ALMAGEST_DISABLE]: [ "(Сдавленно) Твои ментальные атаки отвратительны!", "Тьма в моей голове... я вырвусь!" ], // Если Елена попадает под Раскол Разума Альмагест
|
||||||
|
[GAME_CONFIG.ABILITY_ID_ALMAGEST_DEBUFF]: [ "Истощаешь мою силу? Я восстановлю ее Светом!", "Твое проклятие слабо." ], // Если Елена попадает под Проклятие Увядания Альмагест
|
||||||
|
attackBlocked: [ "Твоя атака разбилась о мой щит Света!", "Предсказуемо и слабо, Альмагест." ], // При блоке атаки Альмагест
|
||||||
|
attackHits: [ "(Резкий вздох) Коснулась... Но Свет исцелит рану.", "Эта царапина - ничто!", "Ты заплатишь за это!" ] // При попадании атаки Альмагест
|
||||||
|
},
|
||||||
|
// Триггер: Базовая атака Елены (PvP)
|
||||||
|
basicAttack: {
|
||||||
|
general: [ "Тьма не победит, Альмагест!", "Твои иллюзии рассеются перед Светом!", "Пока я стою, порядок будет восстановлен!" ]
|
||||||
|
},
|
||||||
|
// Триггер: Изменение состояния боя
|
||||||
|
onBattleState: {
|
||||||
|
start: [ "Альмагест! Твоим темным делам пришел конец!", "Во имя Света, я остановлю тебя!", "Приготовься к битве, служительница тьмы!" ], // Начало PvP боя с Альмагест
|
||||||
|
opponentNearDefeat: [ "Твоя тьма иссякает, колдунья!", "Сдавайся, пока Свет не испепелил тебя!", "Конец твоим злодеяниям близок!" ] // Альмагест почти побеждена
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pvpAlmagest: { // Насмешки против Альмагест (PvP) - НОВАЯ ЛОГИКА
|
almagest: { // Насмешки Альмагест
|
||||||
base: {
|
elena: { // Против Елены (PvP)
|
||||||
generalAttack: [ // Общие фразы при атаке
|
// Триггер: Альмагест использует СВОЮ способность
|
||||||
"Тьма не победит, Альмагест!",
|
selfCastAbility: {
|
||||||
"Твои иллюзии рассеются перед Светом!",
|
[GAME_CONFIG.ABILITY_ID_ALMAGEST_HEAL]: [ "Я питаюсь слабостью, Елена!", "Тьма дает мне силу!" ],
|
||||||
"Пока я стою, порядок будет восстановлен!",
|
[GAME_CONFIG.ABILITY_ID_ALMAGEST_DAMAGE]: [ "Почувствуй холод бездны!", "Твой Свет померкнет перед моей тенью!" ],
|
||||||
"Не думай, что твои трюки остановят меня.",
|
[GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_ATTACK]: [ "Силы Бездны со мной!", "Моя тень становится гуще!" ],
|
||||||
"За каждую искаженную душу - получишь сторицей!",
|
[GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_DEFENSE]: [ "Мой щит выкован из самой тьмы!", "Попробуй пробить это, служительница Света!" ],
|
||||||
"Сражайся честно, если осмелишься!",
|
[GAME_CONFIG.ABILITY_ID_ALMAGEST_DISABLE]: [ "Твой разум сломлен!", "Умолкни, Светлая!", "Я владею твоими мыслями!" ],
|
||||||
"Даже тень боится истинного Света."
|
[GAME_CONFIG.ABILITY_ID_ALMAGEST_DEBUFF]: [ "Твоя сила тает!", "Почувствуй гниль!", "Я истощаю твой Свет!" ]
|
||||||
],
|
},
|
||||||
generalCast: [ // Общие фразы при касте Еленой
|
// Триггер: Противник (Елена) совершает действие
|
||||||
"Сила Света на моей стороне!",
|
onOpponentAction: {
|
||||||
"Гармония мира укрепит меня.",
|
[GAME_CONFIG.ABILITY_ID_HEAL]: [ "Исцеляешься? Твои раны слишком глубоки!" ],
|
||||||
"Твоя магия хаоса бессильна здесь.",
|
[GAME_CONFIG.ABILITY_ID_FIREBALL]: [ "Жалкое пламя! Мои тени поглотят его!" ],
|
||||||
"Истинная сила - в созидании, не в разрушении.",
|
[GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH]: [ "Сила земли? Смешно! Бездну ничто не остановит." ],
|
||||||
"Я призываю чистую энергию!",
|
[GAME_CONFIG.ABILITY_ID_DEFENSE_AURA]: [ "Твой щит из Света не спасет тебя от Тьмы!" ],
|
||||||
"Свет очистит эту землю от твоей скверны."
|
[GAME_CONFIG.ABILITY_ID_HYPNOTIC_GAZE]: [ "(Сдавленно, затем смех) Попытка управлять моим разумом? Жалко!", "Ты пытаешься заглянуть в Бездну?!" ], // Если Альмагест попадает под Гипнотический взгляд Елены
|
||||||
]
|
[GAME_CONFIG.ABILITY_ID_SEAL_OF_WEAKNESS]: [ "Моя энергия вечна, дура!", "Это лишь раздражение!" ], // Если Альмагест попадает под Печать Слабости Елены
|
||||||
},
|
attackBlocked: [ "Твой блок не спасет тебя вечно, Елена!", "Это лишь задержка." ], // При блоке атаки Елены
|
||||||
onPlayerCast: { // Реакция на конкретные касты Елены
|
attackHits: [ "Ха! Чувствуешь силу Тьмы?", "Это только начало!", "Слабость!" ] // При попадании атаки Елены
|
||||||
[GAME_CONFIG.ABILITY_ID_HEAL]: [
|
},
|
||||||
"Я исцеляюсь Светом, который ты отвергла.",
|
// Триггер: Базовая атака Альмагест
|
||||||
"Жизнь восторжествует над твоей некромантией!",
|
basicAttack: {
|
||||||
"Мое сияние не померкнет."
|
general: [ "Почувствуй мою силу!", "Тени атакуют!", "Я наношу удар!" ]
|
||||||
],
|
},
|
||||||
[GAME_CONFIG.ABILITY_ID_FIREBALL]: [
|
// Триггер: Изменение состояния боя
|
||||||
"Очищающий огонь для твоей тьмы!",
|
onBattleState: {
|
||||||
"Почувствуй гнев праведного Света!",
|
start: [ "Тысяча лет в заточении лишь усилили меня, Елена!", "Твой Свет скоро погаснет!", "Пора положить конец твоему господству!" ], // Начало PvP боя с Еленой
|
||||||
"Это пламя ярче твоих теней!"
|
opponentNearDefeat: [ "Твой Свет гаснет!", "Ты побеждена!", "Бездне нужен твой дух!" ] // Елена почти побеждена
|
||||||
],
|
}
|
||||||
[GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH]: [
|
|
||||||
"Природа дает мне силу, а тебе - лишь презрение.",
|
|
||||||
"Я черпаю из источника жизни, ты - из могилы.",
|
|
||||||
"Естественный порядок против твоего искажения."
|
|
||||||
],
|
|
||||||
[GAME_CONFIG.ABILITY_ID_DEFENSE_AURA]: [
|
|
||||||
"Мой щит отразит твою злобу.",
|
|
||||||
"Свет - лучшая защита.",
|
|
||||||
"Твои темные чары не пройдут!"
|
|
||||||
],
|
|
||||||
[GAME_CONFIG.ABILITY_ID_HYPNOTIC_GAZE]: [
|
|
||||||
"Смотри в глаза Истине, колдунья!",
|
|
||||||
"Твои лживые речи умолкнут!",
|
|
||||||
"Хватит прятаться за иллюзиями!",
|
|
||||||
"Я вижу твою истинную, уродливую суть."
|
|
||||||
],
|
|
||||||
[GAME_CONFIG.ABILITY_ID_SEAL_OF_WEAKNESS]: [
|
|
||||||
"Твоя темная сила иссякнет!",
|
|
||||||
"Я ослабляю твою связь с бездной!",
|
|
||||||
"Почувствуй, как тает твоя энергия!"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
onOpponentAction: { // Реакция на действия Альмагест
|
|
||||||
[GAME_CONFIG.ABILITY_ID_ALMAGEST_HEAL]: [
|
|
||||||
"Лечишь раны тьмой? Она лишь глубже проникнет в тебя.",
|
|
||||||
"Твоя магия несет лишь порчу, даже исцеляя.",
|
|
||||||
"Жалкие попытки отсрочить возмездие."
|
|
||||||
],
|
|
||||||
[GAME_CONFIG.ABILITY_ID_ALMAGEST_DAMAGE]: [
|
|
||||||
"Твоя тень лишь царапает, не ранит.",
|
|
||||||
"Слабый удар! Тьма делает тебя немощной.",
|
|
||||||
"Думала, это причинит мне боль?",
|
|
||||||
"Свет поглотит твой мрак!"
|
|
||||||
],
|
|
||||||
[GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_ATTACK]: [
|
|
||||||
"Черпаешь силы из бездны? Она поглотит и тебя.",
|
|
||||||
"Твое усиление - лишь агония искаженной энергии.",
|
|
||||||
"Сколько бы ты ни копила тьму, Свет сильнее."
|
|
||||||
],
|
|
||||||
[GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_DEFENSE]: [
|
|
||||||
"Щит из теней? Он рассыплется прахом!",
|
|
||||||
"Твоя защита иллюзорна, как и твоя сила.",
|
|
||||||
"Не спрячешься от Света за этой ширмой!"
|
|
||||||
],
|
|
||||||
[GAME_CONFIG.ABILITY_ID_ALMAGEST_DISABLE]: [ // Если попала под Раскол Разума
|
|
||||||
"(Сдавленно) Твои ментальные атаки отвратительны!",
|
|
||||||
"Тьма в моей голове... я вырвусь!",
|
|
||||||
"Не сломить мой дух твоими фокусами!"
|
|
||||||
],
|
|
||||||
[GAME_CONFIG.ABILITY_ID_ALMAGEST_DEBUFF]: [ // Если попала под Проклятие Увядания
|
|
||||||
"Истощаешь мою силу? Я восстановлю ее Светом!",
|
|
||||||
"Твое проклятие слабо.",
|
|
||||||
"Чувствую холод... но он лишь закаляет волю!"
|
|
||||||
],
|
|
||||||
attackBlocked: [
|
|
||||||
"Твоя атака разбилась о мой щит Света!",
|
|
||||||
"Предсказуемо и слабо, Альмагест.",
|
|
||||||
"Моя защита безупречна."
|
|
||||||
],
|
|
||||||
attackHits: [
|
|
||||||
"(Резкий вздох) Коснулась... Но Свет исцелит рану.",
|
|
||||||
"Эта царапина - ничто!",
|
|
||||||
"Ты заплатишь за это!",
|
|
||||||
"Боль лишь укрепляет мою решимость!"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
onBattleState: {
|
|
||||||
start: [ // Начало PvP боя
|
|
||||||
"Альмагест! Твоим темным делам пришел конец!",
|
|
||||||
"Во имя Света, я остановлю тебя!",
|
|
||||||
"Приготовься к битве, служительница тьмы!",
|
|
||||||
"Эта дуэль решит судьбу многих."
|
|
||||||
],
|
|
||||||
opponentNearDefeat: [ // Альмагест почти побеждена
|
|
||||||
"Твоя тьма иссякает, колдунья!",
|
|
||||||
"Сдавайся, пока Свет не испепелил тебя!",
|
|
||||||
"Конец твоим злодеяниям близок!",
|
|
||||||
"Прими свое поражение!"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -374,10 +335,35 @@ const gameData = {
|
|||||||
opponentAbilities, // Балард
|
opponentAbilities, // Балард
|
||||||
almagestAbilities, // Альмагест
|
almagestAbilities, // Альмагест
|
||||||
|
|
||||||
// Система насмешек (с разделением)
|
// Система насмешек (с разделением по персонажам и противникам)
|
||||||
elenaTauntSystem
|
tauntSystem // Переименовали для лучшей структуры
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Вспомогательные функции для использования ВНУТРИ data.js или модулей, которые его используют ---
|
||||||
|
// Эти функции НЕ являются частью экспортируемого объекта gameData,
|
||||||
|
// но могут быть вызваны внутри этого файла, например, в descriptionFunction
|
||||||
|
// Или их можно было бы экспортировать отдельно, если они нужны другим модулям, не имеющим gameData в аргументах.
|
||||||
|
// Но в текущей структуре gameLogic они не нужны, так как gameLogic получает gameData и имеет свои локальные хелперы.
|
||||||
|
|
||||||
|
// function _getCharacterData(key) {
|
||||||
|
// if (!key) return null;
|
||||||
|
// switch (key) {
|
||||||
|
// case 'elena': return { baseStats: playerBaseStats, abilities: playerAbilities };
|
||||||
|
// case 'balard': return { baseStats: opponentBaseStats, abilities: opponentAbilities };
|
||||||
|
// case 'almagest': return { baseStats: almagestBaseStats, abilities: almagestAbilities };
|
||||||
|
// default: console.error(`data.js::_getCharacterData: Unknown character key "${key}"`); return null;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// function _getCharacterBaseData(key) {
|
||||||
|
// const charData = _getCharacterData(key);
|
||||||
|
// return charData ? charData.baseStats : null;
|
||||||
|
// }
|
||||||
|
// function _getCharacterAbilities(key) {
|
||||||
|
// const charData = _getCharacterData(key);
|
||||||
|
// return charData ? charData.abilities : null;
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
||||||
module.exports = gameData;
|
module.exports = gameData;
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -2,10 +2,36 @@
|
|||||||
const GAME_CONFIG = require('./config');
|
const GAME_CONFIG = require('./config');
|
||||||
const gameData = require('./data'); // Загружаем один раз на уровне модуля
|
const gameData = require('./data'); // Загружаем один раз на уровне модуля
|
||||||
|
|
||||||
// --- Вспомогательные Функции ---
|
// --- Вспомогательные Функции для gameLogic ---
|
||||||
|
|
||||||
|
// Вспомогательная функция для получения данных персонажа (baseStats и abilities)
|
||||||
|
// Нужна здесь, так как объект gameData сам по себе не имеет этих методов.
|
||||||
|
// Принимает gameDataForLogic как аргумент для гибкости, по умолчанию использует глобальный gameData.
|
||||||
|
function _getCharacterDataForLogic(key, gameDataForLogic = gameData) {
|
||||||
|
if (!key) return null;
|
||||||
|
switch (key) {
|
||||||
|
case 'elena': return { baseStats: gameDataForLogic.playerBaseStats, abilities: gameDataForLogic.playerAbilities };
|
||||||
|
case 'balard': return { baseStats: gameDataForLogic.opponentBaseStats, abilities: gameDataForLogic.opponentAbilities }; // Балард использует opponentAbilities
|
||||||
|
case 'almagest': return { baseStats: gameDataForLogic.almagestBaseStats, abilities: gameDataForLogic.almagestAbilities }; // Альмагест использует almagestAbilities
|
||||||
|
default: console.error(`_getCharacterDataForLogic: Неизвестный ключ персонажа "${key}"`); return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Вспомогательная функция для получения только базовых статов персонажа
|
||||||
|
function _getCharacterBaseDataForLogic(key, gameDataForLogic = gameData) {
|
||||||
|
const charData = _getCharacterDataForLogic(key, gameDataForLogic);
|
||||||
|
return charData ? charData.baseStats : null;
|
||||||
|
}
|
||||||
|
// Вспомогательная функция для получения только способностей персонажа
|
||||||
|
function _getCharacterAbilitiesForLogic(key, gameDataForLogic = gameData) {
|
||||||
|
const charData = _getCharacterDataForLogic(key, gameDataForLogic);
|
||||||
|
return charData ? charData.abilities : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Обрабатывает активные эффекты (баффы/дебаффы) для бойца в конце его хода.
|
* Обрабатывает активные эффекты (баффы/дебаффы) для бойца в конце его хода.
|
||||||
|
* Длительность эффекта уменьшается на 1.
|
||||||
|
* Периодические эффекты (DoT, ресурсный дебафф и т.п.) срабатывают, если эффект не "justCast" в этом ходу.
|
||||||
* @param {Array} effectsArray - Массив активных эффектов бойца.
|
* @param {Array} effectsArray - Массив активных эффектов бойца.
|
||||||
* @param {Object} ownerState - Состояние бойца (currentHp, currentResource и т.д.).
|
* @param {Object} ownerState - Состояние бойца (currentHp, currentResource и т.д.).
|
||||||
* @param {Object} ownerBaseStats - Базовые статы бойца (включая characterKey).
|
* @param {Object} ownerBaseStats - Базовые статы бойца (включая characterKey).
|
||||||
@ -22,51 +48,41 @@ function processEffects(effectsArray, ownerState, ownerBaseStats, ownerId, curre
|
|||||||
|
|
||||||
let effectsToRemoveIndexes = [];
|
let effectsToRemoveIndexes = [];
|
||||||
|
|
||||||
|
// Важно: Сначала обрабатываем эффекты, затем уменьшаем длительность, затем удаляем.
|
||||||
for (let i = 0; i < effectsArray.length; i++) {
|
for (let i = 0; i < effectsArray.length; i++) {
|
||||||
const eff = effectsArray[i];
|
const eff = effectsArray[i];
|
||||||
const isNatureStrengthEffect = eff.id === GAME_CONFIG.ABILITY_ID_NATURE_STRENGTH || eff.id === GAME_CONFIG.ABILITY_ID_ALMAGEST_BUFF_ATTACK;
|
|
||||||
|
|
||||||
// if (isNatureStrengthEffect) { // Отладочный лог
|
// --- Применяем эффект (DoT, сжигание ресурса и т.п.), если он не только что наложен в этом ходу ---
|
||||||
// console.log(`[NATURE_STRENGTH_DEBUG] processEffects for ${ownerState.name}: Effect: ${eff.name}, justCast (before): ${eff.justCast}, turnsLeft (before): ${eff.turnsLeft}`);
|
if (!eff.justCast) {
|
||||||
// }
|
// Обработка урона от эффектов полного безмолвия (Гипнотический Взгляд, Раскол Разума)
|
||||||
|
// Эти эффекты наносят урон цели В КОНЦЕ ее хода
|
||||||
// --- Обработка эффектов с действием каждый ход (ДО уменьшения turnsLeft, если justCast === false) ---
|
if (eff.isFullSilence && typeof eff.power === 'number' && eff.power > 0) {
|
||||||
if (!eff.justCast) { // Эффекты, которые тикают, не должны тикать в ход наложения
|
|
||||||
if (eff.isFullSilence && eff.power && typeof eff.power === 'number' && eff.power > 0) {
|
|
||||||
const damage = eff.power;
|
const damage = eff.power;
|
||||||
ownerState.currentHp = Math.max(0, ownerState.currentHp - damage);
|
// ИСПРАВЛЕНО: Округляем результат вычитания HP
|
||||||
|
ownerState.currentHp = Math.max(0, Math.round(ownerState.currentHp - damage));
|
||||||
if (addToLogCallback) addToLogCallback(`😵 Эффект "${eff.name}" наносит ${damage} урона ${ownerName}!`, configToUse.LOG_TYPE_DAMAGE);
|
if (addToLogCallback) addToLogCallback(`😵 Эффект "${eff.name}" наносит ${damage} урона ${ownerName}!`, configToUse.LOG_TYPE_DAMAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обработка сжигания ресурса (Печать Слабости, Проклятие Увядания)
|
||||||
|
// Эти эффекты сжигают ресурс цели В КОНЦЕ ее хода
|
||||||
if ((eff.id === 'effect_' + configToUse.ABILITY_ID_SEAL_OF_WEAKNESS || eff.id === 'effect_' + configToUse.ABILITY_ID_ALMAGEST_DEBUFF) && eff.power > 0) {
|
if ((eff.id === 'effect_' + configToUse.ABILITY_ID_SEAL_OF_WEAKNESS || eff.id === 'effect_' + configToUse.ABILITY_ID_ALMAGEST_DEBUFF) && eff.power > 0) {
|
||||||
const resourceToBurn = eff.power;
|
const resourceToBurn = eff.power;
|
||||||
if (ownerState.currentResource > 0) {
|
if (ownerState.currentResource > 0) {
|
||||||
const actualBurn = Math.min(ownerState.currentResource, resourceToBurn);
|
const actualBurn = Math.min(ownerState.currentResource, resourceToBurn);
|
||||||
ownerState.currentResource = Math.max(0, ownerState.currentResource - actualBurn);
|
// ИСПРАВЛЕНО: Округляем результат вычитания ресурса
|
||||||
|
ownerState.currentResource = Math.max(0, Math.round(ownerState.currentResource - actualBurn));
|
||||||
if (addToLogCallback) addToLogCallback(`🔥 Эффект "${eff.name}" сжигает ${actualBurn} ${ownerBaseStats.resourceName} у ${ownerName}!`, configToUse.LOG_TYPE_EFFECT);
|
if (addToLogCallback) addToLogCallback(`🔥 Эффект "${eff.name}" сжигает ${actualBurn} ${ownerBaseStats.resourceName} у ${ownerName}!`, configToUse.LOG_TYPE_EFFECT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Примечание: Отложенные эффекты (например, Сила Природы) применяют свою силу в gameInstance.processPlayerAction (после атаки), а не здесь.
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Уменьшение длительности эффекта ---
|
// --- Уменьшаем длительность ---
|
||||||
if (eff.justCast) {
|
eff.turnsLeft--;
|
||||||
eff.justCast = false;
|
eff.justCast = false; // Эффект больше не считается "just cast" после обработки этого хода
|
||||||
} else {
|
|
||||||
// Не уменьшаем turnsLeft для эффектов, которые должны длиться до следующей атаки
|
|
||||||
// и не имеют фиксированного числа ходов (таких как Сила Природы, если бы она так работала).
|
|
||||||
// В нашем случае Сила Природы имеет duration, поэтому turnsLeft уменьшается.
|
|
||||||
eff.turnsLeft--;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (isNatureStrengthEffect) { // Отладочный лог
|
// --- Отмечаем для удаления, если длительность закончилась ---
|
||||||
// console.log(`[NATURE_STRENGTH_DEBUG] processEffects for ${ownerState.name}: Effect: ${eff.name}, justCast (after): ${eff.justCast}, turnsLeft (after): ${eff.turnsLeft}`);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// --- Удаление закончившихся эффектов ---
|
|
||||||
if (eff.turnsLeft <= 0) {
|
if (eff.turnsLeft <= 0) {
|
||||||
// if (isNatureStrengthEffect) { // Отладочный лог
|
|
||||||
// console.log(`[NATURE_STRENGTH_DEBUG] processEffects for ${ownerState.name}: Effect ${eff.name} REMOVED because turnsLeft is 0.`);
|
|
||||||
// }
|
|
||||||
effectsToRemoveIndexes.push(i);
|
effectsToRemoveIndexes.push(i);
|
||||||
if (addToLogCallback) {
|
if (addToLogCallback) {
|
||||||
addToLogCallback(`Эффект "${eff.name}" на ${ownerName} закончился.`, configToUse.LOG_TYPE_EFFECT);
|
addToLogCallback(`Эффект "${eff.name}" на ${ownerName} закончился.`, configToUse.LOG_TYPE_EFFECT);
|
||||||
@ -74,37 +90,57 @@ function processEffects(effectsArray, ownerState, ownerBaseStats, ownerId, curre
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Удаляем эффекты с конца, чтобы не нарушить индексы
|
||||||
for (let i = effectsToRemoveIndexes.length - 1; i >= 0; i--) {
|
for (let i = effectsToRemoveIndexes.length - 1; i >= 0; i--) {
|
||||||
effectsArray.splice(effectsToRemoveIndexes[i], 1);
|
effectsArray.splice(effectsToRemoveIndexes[i], 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Обрабатывает отсчет для отключенных (заглушенных) способностей игрока. */
|
/**
|
||||||
|
* Обрабатывает отсчет для отключенных (заглушенных) способностей игрока в конце его хода.
|
||||||
|
* Длительность заглушения уменьшается на 1.
|
||||||
|
* @param {Array<object>} disabledAbilitiesArray - Массив объектов заглушенных способностей.
|
||||||
|
* @param {Array<object>} characterAbilities - Полный список способностей персонажа (для получения имени).
|
||||||
|
* @param {string} characterName - Имя персонажа (для лога).
|
||||||
|
* @param {function} addToLogCallback - Функция для добавления лога.
|
||||||
|
* @returns {void} - Модифицирует disabledAbilitiesArray напрямую.
|
||||||
|
*/
|
||||||
function processDisabledAbilities(disabledAbilitiesArray, characterAbilities, characterName, addToLogCallback) {
|
function processDisabledAbilities(disabledAbilitiesArray, characterAbilities, characterName, addToLogCallback) {
|
||||||
if (!disabledAbilitiesArray || disabledAbilitiesArray.length === 0) return;
|
if (!disabledAbilitiesArray || disabledAbilitiesArray.length === 0) return;
|
||||||
const stillDisabled = [];
|
const stillDisabled = [];
|
||||||
disabledAbilitiesArray.forEach(dis => {
|
disabledAbilitiesArray.forEach(dis => {
|
||||||
dis.turnsLeft--;
|
dis.turnsLeft--; // Уменьшаем длительность заглушения
|
||||||
if (dis.turnsLeft > 0) {
|
if (dis.turnsLeft > 0) {
|
||||||
stillDisabled.push(dis);
|
stillDisabled.push(dis);
|
||||||
} else {
|
} else {
|
||||||
if (addToLogCallback) {
|
if (addToLogCallback) {
|
||||||
const ability = characterAbilities.find(ab => ab.id === dis.abilityId);
|
const ability = characterAbilities.find(ab => ab.id === dis.abilityId);
|
||||||
|
// Проверка на заглушающий эффект тоже должна быть удалена из activeEffects в processEffects
|
||||||
|
// Здесь мы только обрабатываем список disabledAbilities, удаляя запись
|
||||||
if (ability) addToLogCallback(`Способность ${characterName} "${ability.name}" больше не заглушена!`, GAME_CONFIG.LOG_TYPE_INFO);
|
if (ability) addToLogCallback(`Способность ${characterName} "${ability.name}" больше не заглушена!`, GAME_CONFIG.LOG_TYPE_INFO);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// Очищаем исходный массив и добавляем только те, что еще активны
|
||||||
disabledAbilitiesArray.length = 0;
|
disabledAbilitiesArray.length = 0;
|
||||||
disabledAbilitiesArray.push(...stillDisabled);
|
disabledAbilitiesArray.push(...stillDisabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Обрабатывает отсчет кулдаунов для способностей. */
|
/**
|
||||||
|
* Обрабатывает отсчет общих кулдаунов для способностей в конце хода.
|
||||||
|
* Длительность кулдауна уменьшается на 1.
|
||||||
|
* @param {object} cooldownsObject - Объект с кулдаунами способностей ({ abilityId: turnsLeft }).
|
||||||
|
* @param {Array<object>} abilitiesList - Полный список способностей персонажа (для получения имени).
|
||||||
|
* @param {string} ownerName - Имя персонажа (для лога).
|
||||||
|
* @param {function} addToLogCallback - Функция для добавления лога.
|
||||||
|
* @returns {void} - Модифицирует cooldownsObject напрямую.
|
||||||
|
*/
|
||||||
function processPlayerAbilityCooldowns(cooldownsObject, abilitiesList, ownerName, addToLogCallback) {
|
function processPlayerAbilityCooldowns(cooldownsObject, abilitiesList, ownerName, addToLogCallback) {
|
||||||
if (!cooldownsObject || !abilitiesList) return;
|
if (!cooldownsObject || !abilitiesList) return;
|
||||||
for (const abilityId in cooldownsObject) {
|
for (const abilityId in cooldownsObject) {
|
||||||
if (cooldownsObject.hasOwnProperty(abilityId) && cooldownsObject[abilityId] > 0) {
|
if (cooldownsObject.hasOwnProperty(abilityId) && cooldownsObject[abilityId] > 0) {
|
||||||
cooldownsObject[abilityId]--;
|
cooldownsObject[abilityId]--; // Уменьшаем кулдаун
|
||||||
if (cooldownsObject[abilityId] === 0) {
|
if (cooldownsObject[abilityId] === 0) {
|
||||||
const ability = abilitiesList.find(ab => ab.id === abilityId);
|
const ability = abilitiesList.find(ab => ab.id === abilityId);
|
||||||
if (ability && addToLogCallback) {
|
if (ability && addToLogCallback) {
|
||||||
@ -115,215 +151,334 @@ function processPlayerAbilityCooldowns(cooldownsObject, abilitiesList, ownerName
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Обновляет статус 'isBlocking' на основе активных эффектов. */
|
/**
|
||||||
|
* Обновляет статус 'isBlocking' на основе активных эффектов.
|
||||||
|
* @param {object} fighterState - Состояние бойца.
|
||||||
|
* @returns {void} - Модифицирует fighterState.isBlocking.
|
||||||
|
*/
|
||||||
function updateBlockingStatus(fighterState) {
|
function updateBlockingStatus(fighterState) {
|
||||||
if (!fighterState) return;
|
if (!fighterState) return;
|
||||||
|
// Боец считается блокирующим, если у него есть активный эффект, дающий блок (grantsBlock: true) с turnsLeft > 0
|
||||||
fighterState.isBlocking = fighterState.activeEffects.some(eff => eff.grantsBlock && eff.turnsLeft > 0);
|
fighterState.isBlocking = fighterState.activeEffects.some(eff => eff.grantsBlock && eff.turnsLeft > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Выбирает подходящую насмешку для Елены.
|
* Получает случайную насмешку из системы насмешек для определенного персонажа.
|
||||||
|
* Ищет фразу в gameData.tauntSystem[speakerCharacterKey][opponentCharacterKey][trigger][context].
|
||||||
|
* @param {string} speakerCharacterKey - Ключ персонажа, который произносит насмешку ('elena' или 'almagest' или 'balard').
|
||||||
|
* @param {string} trigger - Тип события, вызвавшего насмешку (например, 'selfCastAbility', 'onOpponentAction', 'battleStart', 'basicAttack', 'opponentNearDefeatCheck').
|
||||||
|
* @param {object} context - Дополнительный контекст (например, { abilityId: 'fireball' }, { outcome: 'success' }).
|
||||||
|
* @param {object} configToUse - Конфигурационный объект игры.
|
||||||
|
* @param {object} gameDataForLogic - Полный объект gameData.
|
||||||
|
* @param {object} currentGameState - Текущее состояние игры.
|
||||||
|
* @returns {string} Текст насмешки или "(Молчание)".
|
||||||
*/
|
*/
|
||||||
function getElenaTaunt(trigger, context = {}, configToUse, gameDataForLogic = gameData, currentGameState) {
|
function getRandomTaunt(speakerCharacterKey, trigger, context = {}, configToUse, gameDataForLogic = gameData, currentGameState) {
|
||||||
if (!currentGameState || !currentGameState.player || currentGameState.player.characterKey !== 'elena') {
|
// Проверяем наличие системы насмешек для говорящего персонажа
|
||||||
|
const speakerTauntSystem = gameDataForLogic?.tauntSystem?.[speakerCharacterKey];
|
||||||
|
if (!speakerTauntSystem) return "(Молчание)"; // Нет насмешек для этого персонажа
|
||||||
|
|
||||||
|
// Определяем противника, чтобы выбрать соответствующую ветку насмешек
|
||||||
|
// Для этого нужно найти в gameState, кто из player/opponent имеет characterKey говорящего,
|
||||||
|
// и взять characterKey другого.
|
||||||
|
const speakerRole = currentGameState?.player?.characterKey === speakerCharacterKey ?
|
||||||
|
GAME_CONFIG.PLAYER_ID :
|
||||||
|
(currentGameState?.opponent?.characterKey === speakerCharacterKey ?
|
||||||
|
GAME_CONFIG.OPPONENT_ID : null);
|
||||||
|
|
||||||
|
if (speakerRole === null) {
|
||||||
|
console.warn(`getRandomTaunt: Speaker character key "${speakerCharacterKey}" not found in current game state roles.`);
|
||||||
return "(Молчание)";
|
return "(Молчание)";
|
||||||
}
|
}
|
||||||
const opponentKey = currentGameState.opponent.characterKey;
|
|
||||||
const tauntSystem = gameDataForLogic?.elenaTauntSystem;
|
const opponentRole = speakerRole === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||||||
const tauntBranch = opponentKey === 'balard' ? tauntSystem?.aiBalard : tauntSystem?.pvpAlmagest;
|
const opponentCharacterKey = currentGameState?.[opponentRole]?.characterKey;
|
||||||
if (!tauntBranch) return "(Молчание)";
|
|
||||||
|
const tauntBranch = speakerTauntSystem[opponentCharacterKey];
|
||||||
|
if (!tauntBranch) {
|
||||||
|
// console.warn(`getRandomTaunt: No taunt branch found for speaker "${speakerCharacterKey}" against opponent "${opponentCharacterKey}".`);
|
||||||
|
return "(Молчание)"; // Нет насмешек против этого оппонента
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
let potentialTaunts = [];
|
let potentialTaunts = [];
|
||||||
const opponentHpPerc = (currentGameState.opponent.currentHp / currentGameState.opponent.maxHp) * 100;
|
|
||||||
const isOpponentLowHpForDomination = opponentKey === 'balard' && opponentHpPerc <= configToUse.PLAYER_MERCY_TAUNT_THRESHOLD_PERCENT;
|
|
||||||
const isOpponentNearDefeat = opponentHpPerc < 20;
|
|
||||||
|
|
||||||
if (trigger === 'opponentNearDefeatCheck' && isOpponentNearDefeat && tauntBranch.onBattleState?.opponentNearDefeat) {
|
// Навигация по структуре tauntSystem в зависимости от триггера и контекста
|
||||||
potentialTaunts = tauntBranch.onBattleState.opponentNearDefeat;
|
if (trigger === 'battleStart') {
|
||||||
|
potentialTaunts = tauntBranch.onBattleState?.start;
|
||||||
}
|
}
|
||||||
else if (trigger === 'opponentAction' && context.abilityId) {
|
else if (trigger === 'opponentNearDefeatCheck') { // Проверка на низкое HP противника для специальных фраз
|
||||||
|
const opponentState = currentGameState?.[opponentRole]; // Состояние противника для проверки HP
|
||||||
|
// Проверяем, что состояние оппонента существует и его HP ниже порога (например, 20%)
|
||||||
|
if (opponentState && opponentState.maxHp > 0 && opponentState.currentHp / opponentState.maxHp < 0.20) {
|
||||||
|
potentialTaunts = tauntBranch.onBattleState?.opponentNearDefeat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (trigger === 'selfCastAbility' && context.abilityId) {
|
||||||
|
potentialTaunts = tauntBranch.selfCastAbility?.[context.abilityId];
|
||||||
|
}
|
||||||
|
else if (trigger === 'basicAttack' && tauntBranch.basicAttack) {
|
||||||
|
const opponentState = currentGameState?.[opponentRole]; // Состояние противника
|
||||||
|
// Специальная логика для базовой атаки Елены против Баларда (милосердие/доминирование)
|
||||||
|
if (speakerCharacterKey === 'elena' && opponentCharacterKey === 'balard' && opponentState) {
|
||||||
|
const opponentHpPerc = (opponentState.currentHp / opponentState.maxHp) * 100;
|
||||||
|
if (opponentHpPerc <= configToUse.PLAYER_MERCY_TAUNT_THRESHOLD_PERCENT) {
|
||||||
|
potentialTaunts = tauntBranch.basicAttack.dominating;
|
||||||
|
} else {
|
||||||
|
potentialTaunts = tauntBranch.basicAttack.merciful;
|
||||||
|
}
|
||||||
|
} else { // Общая логика для PvP или Елена/Балард вне порога
|
||||||
|
potentialTaunts = tauntBranch.basicAttack.general;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Реакция на действие противника
|
||||||
|
else if (trigger === 'onOpponentAction' && context.abilityId) {
|
||||||
const actionResponses = tauntBranch.onOpponentAction?.[context.abilityId];
|
const actionResponses = tauntBranch.onOpponentAction?.[context.abilityId];
|
||||||
if (actionResponses) {
|
if (actionResponses) {
|
||||||
|
// Если структура содержит вложенные результаты (например, успех/провал Безмолвия)
|
||||||
if (typeof actionResponses === 'object' && !Array.isArray(actionResponses) && context.outcome && context.outcome in actionResponses) {
|
if (typeof actionResponses === 'object' && !Array.isArray(actionResponses) && context.outcome && context.outcome in actionResponses) {
|
||||||
potentialTaunts = actionResponses[context.outcome];
|
potentialTaunts = actionResponses[context.outcome]; // Например, onOpponentAction.silence.success
|
||||||
} else if (Array.isArray(actionResponses)) {
|
} else if (Array.isArray(actionResponses)) {
|
||||||
potentialTaunts = actionResponses;
|
potentialTaunts = actionResponses; // Прямой массив фраз для способности
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (trigger === 'opponentAttackBlocked' && tauntBranch.onOpponentAction?.attackBlocked) {
|
// Реакция на попадание/блок атаки противника
|
||||||
|
// Примечание: Эти триггеры срабатывают, когда по ГОВОРЯЩЕМУ попала атака или он ее заблокировал.
|
||||||
|
// Вызываются из performAttack, где известно, кто атакует и кто защищается.
|
||||||
|
else if (trigger === 'onOpponentAttackBlocked' && tauntBranch.onOpponentAction?.attackBlocked) {
|
||||||
potentialTaunts = tauntBranch.onOpponentAction.attackBlocked;
|
potentialTaunts = tauntBranch.onOpponentAction.attackBlocked;
|
||||||
}
|
}
|
||||||
else if (trigger === 'opponentAttackHit' && tauntBranch.onOpponentAction?.attackHits) {
|
else if (trigger === 'onOpponentAttackHit' && tauntBranch.onOpponentAction?.attackHits) {
|
||||||
potentialTaunts = tauntBranch.onOpponentAction.attackHits;
|
potentialTaunts = tauntBranch.onOpponentAction.attackHits;
|
||||||
}
|
}
|
||||||
else if (trigger === 'playerActionCast' && context.abilityId && tauntBranch.onPlayerCast?.[context.abilityId]) {
|
|
||||||
potentialTaunts = tauntBranch.onPlayerCast[context.abilityId];
|
// Если по прямому триггеру не найдено, возвращаем "(Молчание)".
|
||||||
}
|
// Можно добавить фоллбэк на общие фразы, если требуется более разговорчивый персонаж.
|
||||||
else if (trigger === 'playerBasicAttack') {
|
// Например: if ((!potentialTaunts || potentialTaunts.length === 0) && tauntBranch.basicAttack?.general) { potentialTaunts = tauntBranch.basicAttack.general; }
|
||||||
if (isOpponentLowHpForDomination) {
|
|
||||||
const pools = tauntBranch.base?.dominating || {};
|
|
||||||
potentialTaunts = [ ...(pools.creatorVsCreation || []), ...(pools.betrayalOfLight || []), ...(pools.ingratitudeContempt || []), ...(pools.unmakingThreats || []) ];
|
|
||||||
} else if (opponentKey === 'balard' && !isOpponentLowHpForDomination) {
|
|
||||||
potentialTaunts = tauntBranch.base?.mercifulAttack || [];
|
|
||||||
} else {
|
|
||||||
potentialTaunts = tauntBranch.base?.generalAttack || tauntBranch.base?.mercifulAttack || [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (trigger === 'playerActionGeneral') {
|
|
||||||
if (isOpponentLowHpForDomination) {
|
|
||||||
const pools = tauntBranch.base?.dominating || {};
|
|
||||||
potentialTaunts = [ ...(pools.creatorVsCreation || []), ...(pools.betrayalOfLight || []), ...(pools.ingratitudeContempt || []), ...(pools.unmakingThreats || []) ];
|
|
||||||
} else if (opponentKey === 'balard' && !isOpponentLowHpForDomination) {
|
|
||||||
potentialTaunts = tauntBranch.base?.mercifulCast || [];
|
|
||||||
} else {
|
|
||||||
potentialTaunts = tauntBranch.base?.generalCast || tauntBranch.base?.mercifulCast || [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (trigger === 'battleStart') {
|
|
||||||
const startTaunts = (opponentKey === 'balard' ? tauntBranch.onBattleState?.startMerciful : tauntBranch.onBattleState?.start);
|
|
||||||
if (startTaunts) potentialTaunts = startTaunts;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(potentialTaunts) || potentialTaunts.length === 0) {
|
if (!Array.isArray(potentialTaunts) || potentialTaunts.length === 0) {
|
||||||
if (opponentKey === 'balard') {
|
return "(Молчание)"; // Возвращаем молчание, если ничего не найдено
|
||||||
if (isOpponentLowHpForDomination) {
|
|
||||||
const pools = tauntBranch.base?.dominating || {};
|
|
||||||
potentialTaunts = [ ...(pools.creatorVsCreation || []), ...(pools.betrayalOfLight || []), ...(pools.ingratitudeContempt || []), ...(pools.unmakingThreats || []) ];
|
|
||||||
} else {
|
|
||||||
potentialTaunts = [...(tauntBranch.base?.mercifulAttack || []), ...(tauntBranch.base?.mercifulCast || [])];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
potentialTaunts = [...(tauntBranch.base?.generalAttack || []), ...(tauntBranch.base?.generalCast || [])];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!Array.isArray(potentialTaunts) || potentialTaunts.length === 0) return "(Молчание)";
|
|
||||||
return potentialTaunts[Math.floor(Math.random() * potentialTaunts.length)] || "(Молчание)";
|
// Возвращаем случайную фразу из найденного массива
|
||||||
|
const selectedTaunt = potentialTaunts[Math.floor(Math.random() * potentialTaunts.length)];
|
||||||
|
return selectedTaunt || "(Молчание)"; // Фоллбэк на "(Молчание)" если массив был пуст после всех проверок
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Основные Игровые Функции ---
|
// --- Основные Игровые Функции ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обрабатывает базовую атаку одного бойца по другому.
|
||||||
|
* @param {object} attackerState - Состояние атакующего бойца.
|
||||||
|
* @param {object} defenderState - Состояние защищающегося бойца.
|
||||||
|
* @param {object} attackerBaseStats - Базовые статы атакующего.
|
||||||
|
* @param {object} defenderBaseStats - Базовые статы защищающегося.
|
||||||
|
* @param {object} currentGameState - Текущее состояние игры (для насмешек).
|
||||||
|
* @param {function} addToLogCallback - Функция для добавления лога.
|
||||||
|
* @param {object} configToUse - Конфигурация игры.
|
||||||
|
* @param {object} gameDataForLogic - Данные игры (для насмешек).
|
||||||
|
*/
|
||||||
function performAttack(attackerState, defenderState, attackerBaseStats, defenderBaseStats, currentGameState, addToLogCallback, configToUse, gameDataForLogic = gameData) {
|
function performAttack(attackerState, defenderState, attackerBaseStats, defenderBaseStats, currentGameState, addToLogCallback, configToUse, gameDataForLogic = gameData) {
|
||||||
|
// Расчет базового урона с вариацией
|
||||||
let damage = Math.floor(attackerBaseStats.attackPower * (configToUse.DAMAGE_VARIATION_MIN + Math.random() * configToUse.DAMAGE_VARIATION_RANGE));
|
let damage = Math.floor(attackerBaseStats.attackPower * (configToUse.DAMAGE_VARIATION_MIN + Math.random() * configToUse.DAMAGE_VARIATION_RANGE));
|
||||||
let tauntMessagePart = "";
|
let tauntMessagePart = ""; // Переменная для насмешки защищающегося
|
||||||
|
|
||||||
|
// Проверка на блок
|
||||||
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);
|
||||||
if (defenderState.characterKey === 'elena') {
|
// Проверка на насмешку ОТ защищающегося (Елены или Альмагест) при блокировании атаки
|
||||||
const blockTaunt = getElenaTaunt('opponentAttackBlocked', {}, configToUse, gameDataForLogic, currentGameState);
|
if (defenderState.characterKey === 'elena' || defenderState.characterKey === 'almagest') {
|
||||||
|
// getRandomTaunt принимает speaker (защищающийся), trigger, context, config, gameData, gameState
|
||||||
|
const blockTaunt = getRandomTaunt(defenderState.characterKey, 'onOpponentAttackBlocked', {}, configToUse, gameDataForLogic, currentGameState);
|
||||||
if (blockTaunt !== "(Молчание)") tauntMessagePart = ` (${blockTaunt})`;
|
if (blockTaunt !== "(Молчание)") tauntMessagePart = ` (${blockTaunt})`;
|
||||||
}
|
}
|
||||||
if (addToLogCallback) addToLogCallback(`🛡️ ${defenderBaseStats.name} блокирует атаку! Урон снижен (${initialDamage} -> ${damage}).${tauntMessagePart}`, configToUse.LOG_TYPE_BLOCK);
|
|
||||||
|
if (addToLogCallback) addToLogCallback(`🛡️ ${defenderBaseStats.name} блокирует атаку ${attackerBaseStats.name}! Урон снижен (${initialDamage} -> ${damage}).${tauntMessagePart}`, configToUse.LOG_TYPE_BLOCK);
|
||||||
} else {
|
} else {
|
||||||
let hitMessage = `${attackerBaseStats.name} атакует ${defenderBaseStats.name}! Наносит ${damage} урона.`;
|
let hitMessage = `${attackerBaseStats.name} атакует ${defenderBaseStats.name}! Наносит ${damage} урона.`;
|
||||||
if (defenderState.characterKey === 'elena') {
|
// Проверка на насмешку ОТ защищающегося (Елены или Альмагест) при попадании атаки
|
||||||
const hitTaunt = getElenaTaunt('opponentAttackHit', {}, configToUse, gameDataForLogic, currentGameState);
|
if (defenderState.characterKey === 'elena' || defenderState.characterKey === 'almagest') {
|
||||||
|
// getRandomTaunt принимает speaker (защищающийся), trigger, context, config, gameData, gameState
|
||||||
|
const hitTaunt = getRandomTaunt(defenderState.characterKey, 'onOpponentAttackHit', {}, configToUse, gameDataForLogic, currentGameState);
|
||||||
if (hitTaunt !== "(Молчание)") hitMessage += ` (${hitTaunt})`;
|
if (hitTaunt !== "(Молчание)") hitMessage += ` (${hitTaunt})`;
|
||||||
}
|
}
|
||||||
if (addToLogCallback) addToLogCallback(hitMessage, configToUse.LOG_TYPE_DAMAGE);
|
if (addToLogCallback) addToLogCallback(hitMessage, configToUse.LOG_TYPE_DAMAGE);
|
||||||
}
|
}
|
||||||
defenderState.currentHp = Math.max(0, defenderState.currentHp - damage);
|
// Применяем урон, убеждаемся, что HP не ниже нуля
|
||||||
|
// ИСПРАВЛЕНО: Округляем результат вычитания HP
|
||||||
|
defenderState.currentHp = Math.max(0, Math.round(defenderState.currentHp - damage));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Применяет эффект способности.
|
||||||
|
* @param {object} ability - Объект способности.
|
||||||
|
* @param {object} casterState - Состояние бойца, применившего способность.
|
||||||
|
* @param {object} targetState - Состояние цели способности.
|
||||||
|
* @param {object} casterBaseStats - Базовые статы кастера.
|
||||||
|
* @param {object} targetBaseStats - Базовые статы цели.
|
||||||
|
* @param {object} currentGameState - Текущее состояние игры (для насмешек).
|
||||||
|
* @param {function} addToLogCallback - Функция для добавления лога.
|
||||||
|
* @param {object} configToUse - Конфигурация игры.
|
||||||
|
* @param {object} gameDataForLogic - Данные игры (для насмешек).
|
||||||
|
*/
|
||||||
function applyAbilityEffect(ability, casterState, targetState, casterBaseStats, targetBaseStats, currentGameState, addToLogCallback, configToUse, gameDataForLogic = gameData) {
|
function applyAbilityEffect(ability, casterState, targetState, casterBaseStats, targetBaseStats, currentGameState, addToLogCallback, configToUse, gameDataForLogic = gameData) {
|
||||||
let tauntMessagePart = "";
|
let tauntMessagePart = ""; // Переменная для насмешки, если она связана с результатом эффекта или реакцией цели
|
||||||
|
|
||||||
|
// Проверка на насмешку ОТ цели (Елены или Альмагест), если она попадает под способность противника
|
||||||
|
if (targetState.characterKey === 'elena' || targetState.characterKey === 'almagest') {
|
||||||
|
// Триггер 'onOpponentAction' с abilityId противника
|
||||||
|
const reactionTaunt = getRandomTaunt(targetState.characterKey, 'onOpponentAction', { abilityId: ability.id }, configToUse, gameDataForLogic, currentGameState);
|
||||||
|
if (reactionTaunt !== "(Молчание)") tauntMessagePart = ` (${reactionTaunt})`;
|
||||||
|
} else {
|
||||||
|
tauntMessagePart = ""; // Другие персонажи (Балард) не имеют реакционных насмешек такого типа
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
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));
|
||||||
const actualHeal = Math.min(healAmount, casterBaseStats.maxHp - casterState.currentHp);
|
const actualHeal = Math.min(healAmount, casterBaseStats.maxHp - casterState.currentHp);
|
||||||
if (actualHeal > 0) {
|
if (actualHeal > 0) {
|
||||||
casterState.currentHp += actualHeal;
|
// ИСПРАВЛЕНО: Округляем результат прибавления HP
|
||||||
if (addToLogCallback) addToLogCallback(`💚 ${casterBaseStats.name} восстанавливает ${actualHeal} HP!`, configToUse.LOG_TYPE_HEAL);
|
casterState.currentHp = Math.round(casterState.currentHp + actualHeal);
|
||||||
|
// --- ИЗМЕНЕНИЕ: Добавляем название способности в лог лечения ---
|
||||||
|
if (addToLogCallback) addToLogCallback(`💚 ${casterBaseStats.name} применяет "${ability.name}" и восстанавливает ${actualHeal} HP!${tauntMessagePart}`, configToUse.LOG_TYPE_HEAL);
|
||||||
|
// --- КОНЕЦ ИЗМЕНЕНИЯ ---
|
||||||
} else {
|
} else {
|
||||||
if (addToLogCallback) addToLogCallback(`✨ ${casterBaseStats.name} уже имеет полное здоровье или эффект не дал лечения.`, configToUse.LOG_TYPE_INFO);
|
if (addToLogCallback) addToLogCallback(`✨ ${casterBaseStats.name} применяет "${ability.name}", но не получает лечения.${tauntMessagePart}`, configToUse.LOG_TYPE_INFO);
|
||||||
}
|
}
|
||||||
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));
|
||||||
|
// Проверка на блок цели
|
||||||
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);
|
||||||
if (targetState.characterKey === 'elena') {
|
// Проверка на насмешку ОТ цели (Елены или Альмагест), если она заблокировала урон от способности - перенесено наверх
|
||||||
const blockTaunt = getElenaTaunt('opponentAttackBlocked', {abilityId: ability.id} , configToUse, gameDataForLogic, currentGameState);
|
// if (targetState.characterKey === 'elena' || targetState.characterKey === 'almagest') {
|
||||||
if (blockTaunt !== "(Молчание)") tauntMessagePart = ` (${blockTaunt})`;
|
// const blockTaunt = getRandomTaunt(targetState.characterKey, 'onOpponentAttackBlocked', {abilityId: ability.id} , configToUse, gameDataForLogic, currentGameState);
|
||||||
}
|
// if (blockTaunt !== "(Молчание)") tauntMessagePart = ` (${blockTaunt})`;
|
||||||
if (addToLogCallback) addToLogCallback(`🛡️ ${targetBaseStats.name} блокирует "${ability.name}"! Урон снижен (${initialDamage} -> ${damage}).${tauntMessagePart}`, configToUse.LOG_TYPE_BLOCK);
|
// }
|
||||||
|
if (addToLogCallback) addToLogCallback(`🛡️ ${targetBaseStats.name} блокирует "${ability.name}" от ${casterBaseStats.name}! Урон снижен (${initialDamage} -> ${damage}).${tauntMessagePart}`, configToUse.LOG_TYPE_BLOCK);
|
||||||
}
|
}
|
||||||
targetState.currentHp = Math.max(0, targetState.currentHp - damage);
|
// Применяем урон, убеждаемся, что HP не ниже нуля
|
||||||
|
// ИСПРАВЛЕНО: Округляем результат вычитания HP
|
||||||
|
targetState.currentHp = Math.max(0, Math.round(targetState.currentHp - damage));
|
||||||
if (addToLogCallback && !targetState.isBlocking) {
|
if (addToLogCallback && !targetState.isBlocking) {
|
||||||
let hitMessage = `💥 ${casterBaseStats.name} применяет "${ability.name}" на ${targetBaseStats.name}, нанося ${damage} урона!`;
|
let hitMessage = `💥 ${casterBaseStats.name} применяет "${ability.name}" на ${targetBaseStats.name}, нанося ${damage} урона!${tauntMessagePart}`;
|
||||||
if (targetState.characterKey === 'elena') {
|
// Проверка на насмешку ОТ цели (Елены или Альмагест), если по ней попала способность - перенесено наверх
|
||||||
const hitTaunt = getElenaTaunt('opponentAction', {abilityId: ability.id}, configToUse, gameDataForLogic, currentGameState);
|
// if (targetState.characterKey === 'elena' || targetState.characterKey === 'almagest') {
|
||||||
if (hitTaunt !== "(Молчание)") hitMessage += ` (${hitTaunt})`;
|
// const hitTaunt = getRandomTaunt(targetState.characterKey, 'onOpponentAction', {abilityId: ability.id}, configToUse, gameDataForLogic, currentGameState);
|
||||||
}
|
// if (hitTaunt !== "(Молчание)") hitMessage += ` (${hitTaunt})`;
|
||||||
|
// }
|
||||||
addToLogCallback(hitMessage, configToUse.LOG_TYPE_DAMAGE);
|
addToLogCallback(hitMessage, configToUse.LOG_TYPE_DAMAGE);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case configToUse.ACTION_TYPE_BUFF:
|
case configToUse.ACTION_TYPE_BUFF:
|
||||||
|
// Если бафф уже активен, не применяем его повторно (эта проверка уже есть в gameInstance)
|
||||||
|
// Проверка на .some здесь опциональна, т.к. вызывающий код должен гарантировать уникальность
|
||||||
if (!casterState.activeEffects.some(e => e.id === ability.id)) {
|
if (!casterState.activeEffects.some(e => e.id === ability.id)) {
|
||||||
let effectDescription = ability.description;
|
let effectDescription = ability.description;
|
||||||
if (typeof ability.descriptionFunction === 'function') {
|
if (typeof ability.descriptionFunction === 'function') {
|
||||||
|
// Для описания баффа может потребоваться информация о противнике
|
||||||
const opponentRole = casterState.id === configToUse.PLAYER_ID ? configToUse.OPPONENT_ID : configToUse.PLAYER_ID;
|
const opponentRole = casterState.id === configToUse.PLAYER_ID ? configToUse.OPPONENT_ID : configToUse.PLAYER_ID;
|
||||||
const opponentCurrentState = currentGameState[opponentRole];
|
const opponentCurrentState = currentGameState[opponentRole];
|
||||||
const opponentDataForDesc = opponentCurrentState ? gameDataForLogic[opponentCurrentState.characterKey + 'BaseStats'] : gameDataForLogic.playerBaseStats; // Фоллбэк
|
// Получаем базовые статы противника, если он определен, для функции описания
|
||||||
|
const opponentDataForDesc = opponentCurrentState?.characterKey ? _getCharacterBaseDataForLogic(opponentCurrentState.characterKey, gameDataForLogic) : null; // ИСПОЛЬЗУЕМ _getCharacterBaseDataForLogic
|
||||||
effectDescription = ability.descriptionFunction(configToUse, opponentDataForDesc);
|
effectDescription = ability.descriptionFunction(configToUse, opponentDataForDesc);
|
||||||
}
|
}
|
||||||
|
// isDelayed: true используется для эффектов, которые срабатывают ПОСЛЕ следующего действия (например, Сила Природы).
|
||||||
|
// duration: исходная длительность из данных, turnsLeft: сколько ходов осталось (включая текущий, если !justCast)
|
||||||
casterState.activeEffects.push({
|
casterState.activeEffects.push({
|
||||||
id: ability.id, name: ability.name, description: effectDescription,
|
id: ability.id, name: ability.name, description: effectDescription,
|
||||||
type: ability.type, turnsLeft: ability.duration,
|
type: ability.type, duration: ability.duration, // Сохраняем исходную длительность для отображения в UI или логики
|
||||||
grantsBlock: !!ability.grantsBlock, justCast: !!ability.isDelayed
|
turnsLeft: ability.duration, // Длительность жизни эффекта в ходах владельца
|
||||||
|
grantsBlock: !!ability.grantsBlock,
|
||||||
|
isDelayed: !!ability.isDelayed, // Флаг, что эффект отложенный (срабатывает после действия)
|
||||||
|
justCast: true // Флаг, что эффект только что наложен (для логики processEffects)
|
||||||
});
|
});
|
||||||
if (ability.grantsBlock) updateBlockingStatus(casterState);
|
if (ability.grantsBlock) updateBlockingStatus(casterState); // Обновляем статус блока кастера, если бафф его дает
|
||||||
|
// Насмешки при применении баффа (selfCastAbility) добавляются в GameInstance перед вызовом applyAbilityEffect
|
||||||
|
if (addToLogCallback) addToLogCallback(`✨ ${casterBaseStats.name} накладывает эффект "${ability.name}"!${tauntMessagePart}`, configToUse.LOG_TYPE_EFFECT);
|
||||||
} else {
|
} else {
|
||||||
if (addToLogCallback) addToLogCallback(`Эффект "${ability.name}" уже активен на ${casterBaseStats.name}!`, configToUse.LOG_TYPE_INFO);
|
// Сообщение "уже активен" отправляется из gameInstance перед вызовом applyAbilityEffect
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case configToUse.ACTION_TYPE_DISABLE:
|
case configToUse.ACTION_TYPE_DISABLE: // Безмолвие, Стан и т.п.
|
||||||
tauntMessagePart = ""; // Сбрасываем перед каждым дизейблом
|
// Проверка на насмешку ОТ цели (Елены или Альмагест), если она попадает под дизейбл противника - перенесено наверх
|
||||||
if (targetState.characterKey === 'elena') {
|
// if (targetState.characterKey === 'elena' || targetState.characterKey === 'almagest') {
|
||||||
const disableTaunt = getElenaTaunt('opponentAction', {abilityId: ability.id}, configToUse, gameDataForLogic, currentGameState);
|
// const disableTaunt = getRandomTaunt(targetState.characterKey, 'onOpponentAction', {abilityId: ability.id}, configToUse, gameDataForLogic, currentGameState);
|
||||||
if (disableTaunt !== "(Молчание)") tauntMessagePart = ` (${disableTaunt})`;
|
// if (disableTaunt !== "(Молчание)") tauntMessagePart = ` (${disableTaunt})`;
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (ability.id === configToUse.ABILITY_ID_HYPNOTIC_GAZE && casterState.characterKey === 'elena') {
|
// Гипнотический взгляд Елены / Раскол Разума Альмагест (полное безмолвие)
|
||||||
const effectId = 'fullSilenceByElena';
|
if (ability.id === configToUse.ABILITY_ID_HYPNOTIC_GAZE || ability.id === configToUse.ABILITY_ID_ALMAGEST_DISABLE) {
|
||||||
|
const effectId = ability.id === configToUse.ABILITY_ID_HYPNOTIC_GAZE ? 'fullSilenceByElena' : 'fullSilenceByAlmagest';
|
||||||
|
// Не накладываем повторно, если эффект уже есть на цели
|
||||||
if (!targetState.activeEffects.some(e => e.id === effectId)) {
|
if (!targetState.activeEffects.some(e => e.id === effectId)) {
|
||||||
targetState.activeEffects.push({
|
targetState.activeEffects.push({
|
||||||
id: effectId, name: ability.name, description: ability.description,
|
id: effectId, name: ability.name, description: ability.description,
|
||||||
type: ability.type, turnsLeft: ability.effectDuration, power: ability.power, isFullSilence: true
|
type: ability.type, duration: ability.effectDuration, turnsLeft: ability.effectDuration, // Длительность в ходах цели
|
||||||
|
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}! Способности ${targetBaseStats.name} заблокированы на ${ability.effectDuration} хода, и он(а) получает урон!${tauntMessagePart}`, configToUse.LOG_TYPE_EFFECT);
|
||||||
|
} else {
|
||||||
|
if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} пытается применить "${ability.name}", но эффект уже активен на ${targetState.name}!${tauntMessagePart}`, configToUse.LOG_TYPE_INFO);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Эхо Безмолвия Баларда (заглушает случайную абилку)
|
||||||
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;
|
||||||
const silenceOutcome = success ? 'success' : 'fail';
|
const silenceOutcome = success ? 'success' : 'fail';
|
||||||
const specificSilenceTaunt = getElenaTaunt('opponentAction', { abilityId: ability.id, outcome: silenceOutcome }, configToUse, gameDataForLogic, currentGameState);
|
// Реакция цели (Елены) на успех/провал безмолвия Баларда - перенесено наверх, но с context.outcome
|
||||||
tauntMessagePart = (specificSilenceTaunt !== "(Молчание)") ? ` (${specificSilenceTaunt})` : "";
|
// if (targetState.characterKey === 'elena') { // Балард применяет это только на Елену
|
||||||
|
// const specificSilenceTaunt = getRandomTaunt(targetState.characterKey, 'onOpponentAction', { abilityId: ability.id, outcome: silenceOutcome }, configToUse, gameDataForLogic, currentGameState);
|
||||||
|
// tauntMessagePart = (specificSilenceTaunt !== "(Молчание)") ? ` (${specificSilenceTaunt})` : "";
|
||||||
|
// } else {
|
||||||
|
// tauntMessagePart = ""; // Другие персонажи не реагируют на Безмолвие Баларда
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Нужно получить насмешку с outcome здесь, так как она зависит от результата броска шанса
|
||||||
|
// Временно сохраняем общую насмешку и получаем специфичную
|
||||||
|
let specificSilenceTaunt = "(Молчание)";
|
||||||
|
if (targetState.characterKey === 'elena') { // Балард применяет это только на Елену
|
||||||
|
specificSilenceTaunt = getRandomTaunt(targetState.characterKey, 'onOpponentAction', { abilityId: ability.id, outcome: silenceOutcome }, configToUse, gameDataForLogic, currentGameState);
|
||||||
|
}
|
||||||
|
tauntMessagePart = (specificSilenceTaunt !== "(Молчание)") ? ` (${specificSilenceTaunt})` : tauntMessagePart; // Используем специфичную, если найдена, иначе общую (хотя общая для этого абилки вряд ли есть)
|
||||||
|
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
const targetAbilities = gameDataForLogic.playerAbilities;
|
const targetAbilities = _getCharacterAbilitiesForLogic(targetState.characterKey, gameDataForLogic); // Глушим абилки цели
|
||||||
|
// Фильтруем способности, которые еще не заглушены этим типом безмолвия
|
||||||
const availableAbilities = targetAbilities.filter(pa =>
|
const availableAbilities = targetAbilities.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}`) // Проверка на эффект заглушения для UI/ProcessEffects
|
||||||
);
|
);
|
||||||
|
|
||||||
if (availableAbilities.length > 0) {
|
if (availableAbilities.length > 0) {
|
||||||
const abilityToSilence = availableAbilities[Math.floor(Math.random() * availableAbilities.length)];
|
const abilityToSilence = availableAbilities[Math.floor(Math.random() * availableAbilities.length)];
|
||||||
const turns = configToUse.SILENCE_DURATION;
|
const turns = configToUse.SILENCE_DURATION; // Длительность из конфига (в ходах цели)
|
||||||
targetState.disabledAbilities.push({ abilityId: abilityToSilence.id, turnsLeft: turns + 1 });
|
// Добавляем запись о заглушенной способности в disabledAbilities цели
|
||||||
|
targetState.disabledAbilities.push({ abilityId: abilityToSilence.id, turnsLeft: turns + 1 }); // +1, т.к. длительность уменьшается в конце хода цели
|
||||||
|
// Добавляем эффект заглушения в activeEffects цели (для UI и ProcessEffects)
|
||||||
const silenceEffectIdOnPlayer = `playerSilencedOn_${abilityToSilence.id}`;
|
const silenceEffectIdOnPlayer = `playerSilencedOn_${abilityToSilence.id}`;
|
||||||
targetState.activeEffects.push({
|
targetState.activeEffects.push({
|
||||||
id: silenceEffectIdOnPlayer, name: `Безмолвие: ${abilityToSilence.name}`,
|
id: silenceEffectIdOnPlayer, name: `Безмолвие: ${abilityToSilence.name}`,
|
||||||
description: `Способность "${abilityToSilence.name}" временно недоступна.`,
|
description: `Способность "${abilityToSilence.name}" временно недоступна.`,
|
||||||
type: configToUse.ACTION_TYPE_DISABLE, turnsLeft: turns + 1
|
type: configToUse.ACTION_TYPE_DISABLE, sourceAbilityId: ability.id, // Добавлено sourceAbilityId
|
||||||
|
duration: turns, turnsLeft: turns + 1,
|
||||||
|
justCast: true // Эффект только что наложен
|
||||||
});
|
});
|
||||||
if (addToLogCallback) addToLogCallback(`🔇 Эхо Безмолвия! "${abilityToSilence.name}" Елены заблокировано!${tauntMessagePart}`, configToUse.LOG_TYPE_EFFECT);
|
if (addToLogCallback) addToLogCallback(`🔇 Эхо Безмолвия! "${abilityToSilence.name}" ${targetBaseStats.name} заблокировано на ${turns} хода!${tauntMessagePart}`, configToUse.LOG_TYPE_EFFECT);
|
||||||
} else {
|
} else {
|
||||||
if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} пытается наложить Безмолвие, но у ${targetBaseStats.name} нечего глушить!${tauntMessagePart}`, configToUse.LOG_TYPE_INFO);
|
if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} пытается наложить Безмолвие, но у ${targetBaseStats.name} нечего глушить!${tauntMessagePart}`, configToUse.LOG_TYPE_INFO);
|
||||||
}
|
}
|
||||||
@ -331,74 +486,74 @@ function applyAbilityEffect(ability, casterState, targetState, casterBaseStats,
|
|||||||
if (addToLogCallback) addToLogCallback(`💨 Попытка ${casterBaseStats.name} наложить Безмолвие на ${targetBaseStats.name} провалилась!${tauntMessagePart}`, configToUse.LOG_TYPE_INFO);
|
if (addToLogCallback) addToLogCallback(`💨 Попытка ${casterBaseStats.name} наложить Безмолвие на ${targetBaseStats.name} провалилась!${tauntMessagePart}`, configToUse.LOG_TYPE_INFO);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (ability.id === configToUse.ABILITY_ID_ALMAGEST_DISABLE && casterState.characterKey === 'almagest') {
|
|
||||||
const effectId = 'fullSilenceByAlmagest';
|
|
||||||
if (!targetState.activeEffects.some(e => e.id === effectId)) {
|
|
||||||
targetState.activeEffects.push({
|
|
||||||
id: effectId, name: ability.name, description: ability.description,
|
|
||||||
type: ability.type, turnsLeft: ability.effectDuration, power: ability.power, isFullSilence: true
|
|
||||||
});
|
|
||||||
if (addToLogCallback) addToLogCallback(`🧠 ${casterBaseStats.name} применяет "${ability.name}"! Способности ${targetBaseStats.name} заблокированы на ${ability.effectDuration} хода, и он(а) получает урон!${tauntMessagePart}`, configToUse.LOG_TYPE_EFFECT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case configToUse.ACTION_TYPE_DEBUFF:
|
case configToUse.ACTION_TYPE_DEBUFF: // Ослабления, DoT и т.п.
|
||||||
tauntMessagePart = "";
|
// Проверка на насмешку ОТ цели (Елены или Альмагест), если она попадает под дебафф противника - перенесено наверх
|
||||||
if (targetState.characterKey === 'elena') {
|
// if (targetState.characterKey === 'elena' || targetState.characterKey === 'almagest') {
|
||||||
const debuffTaunt = getElenaTaunt('opponentAction', {abilityId: ability.id}, configToUse, gameDataForLogic, currentGameState);
|
// const debuffTaunt = getRandomTaunt(targetState.characterKey, 'onOpponentAction', {abilityId: ability.id}, configToUse, gameDataForLogic, currentGameState);
|
||||||
if (debuffTaunt !== "(Молчание)") tauntMessagePart = ` (${debuffTaunt})`;
|
// if (debuffTaunt !== "(Молчание)") tauntMessagePart = ` (${debuffTaunt})`;
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
// Печать Слабости Елены / Проклятие Увядания Альмагест (сжигание ресурса)
|
||||||
if (ability.id === configToUse.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === configToUse.ABILITY_ID_ALMAGEST_DEBUFF) {
|
if (ability.id === configToUse.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === configToUse.ABILITY_ID_ALMAGEST_DEBUFF) {
|
||||||
const effectIdForDebuff = 'effect_' + ability.id;
|
const effectIdForDebuff = 'effect_' + ability.id; // Уникальный ID эффекта на цели
|
||||||
|
// Не накладываем повторно, если эффект уже есть на цели
|
||||||
if (!targetState.activeEffects.some(e => e.id === effectIdForDebuff)) {
|
if (!targetState.activeEffects.some(e => e.id === effectIdForDebuff)) {
|
||||||
let effectDescription = ability.description;
|
let effectDescription = ability.description;
|
||||||
if (typeof ability.descriptionFunction === 'function') {
|
if (typeof ability.descriptionFunction === 'function') {
|
||||||
effectDescription = ability.descriptionFunction(configToUse, targetBaseStats);
|
effectDescription = ability.descriptionFunction(configToUse, targetBaseStats); // Описание может зависеть от цели
|
||||||
}
|
}
|
||||||
targetState.activeEffects.push({
|
targetState.activeEffects.push({
|
||||||
id: effectIdForDebuff, name: ability.name, description: effectDescription,
|
id: effectIdForDebuff, name: ability.name, description: effectDescription,
|
||||||
type: configToUse.ACTION_TYPE_DEBUFF, sourceAbilityId: ability.id,
|
type: configToUse.ACTION_TYPE_DEBUFF, sourceAbilityId: ability.id,
|
||||||
turnsLeft: ability.effectDuration, power: ability.power,
|
duration: ability.effectDuration, turnsLeft: ability.effectDuration, // Длительность в ходах цели
|
||||||
|
power: ability.power, // Количество сжигаемого ресурса в ход (применяется в конце хода цели)
|
||||||
|
justCast: true // Эффект только что наложен
|
||||||
});
|
});
|
||||||
if (addToLogCallback) addToLogCallback(`📉 ${casterBaseStats.name} накладывает "${ability.name}" на ${targetBaseStats.name}! Ресурс будет сжигаться.${tauntMessagePart}`, configToUse.LOG_TYPE_EFFECT);
|
if (addToLogCallback) addToLogCallback(`📉 ${casterBaseStats.name} накладывает "${ability.name}" на ${targetBaseStats.name}! Ресурс будет сжигаться.${tauntMessagePart}`, configToUse.LOG_TYPE_EFFECT);
|
||||||
|
} else {
|
||||||
|
if (addToLogCallback) addToLogCallback(`${casterBaseStats.name} пытается применить "${ability.name}", но эффект уже активен на ${targetState.name}!${tauntMessagePart}`, configToUse.LOG_TYPE_INFO);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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;
|
||||||
tauntMessagePart = "";
|
// tauntMessagePart уже получена в начале функции
|
||||||
if (targetState.characterKey === 'elena') {
|
|
||||||
const drainTaunt = getElenaTaunt('opponentAction', { abilityId: ability.id }, configToUse, gameDataForLogic, currentGameState);
|
|
||||||
if (drainTaunt !== "(Молчание)") tauntMessagePart = ` (${drainTaunt})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Сначала урон от способности
|
||||||
if (ability.powerDamage > 0) {
|
if (ability.powerDamage > 0) {
|
||||||
let baseDamageDrain = ability.powerDamage;
|
let baseDamageDrain = ability.powerDamage;
|
||||||
|
// Проверка на блок цели
|
||||||
if (targetState.isBlocking) {
|
if (targetState.isBlocking) {
|
||||||
baseDamageDrain = Math.floor(baseDamageDrain * configToUse.BLOCK_DAMAGE_REDUCTION);
|
baseDamageDrain = Math.floor(baseDamageDrain * configToUse.BLOCK_DAMAGE_REDUCTION);
|
||||||
let blockDrainTaunt = "";
|
let blockDrainTaunt = "";
|
||||||
if (targetState.characterKey === 'elena') {
|
// Реакция цели (Елены/Альмагест) на блок урона от дрейна
|
||||||
blockDrainTaunt = getElenaTaunt('opponentAttackBlocked', {}, configToUse, gameDataForLogic, currentGameState);
|
if (targetState.characterKey === 'elena' || targetState.characterKey === 'almagest') {
|
||||||
|
blockDrainTaunt = getRandomTaunt(targetState.characterKey, 'onOpponentAttackBlocked', {}, configToUse, gameDataForLogic, currentGameState);
|
||||||
if (blockDrainTaunt !== "(Молчание)") blockDrainTaunt = ` (${blockDrainTaunt})`;
|
if (blockDrainTaunt !== "(Молчание)") blockDrainTaunt = ` (${blockDrainTaunt})`;
|
||||||
}
|
}
|
||||||
if (addToLogCallback) addToLogCallback(`🛡️ ${targetBaseStats.name} блокирует часть урона от "${ability.name}"! Урон снижен до ${baseDamageDrain}.${blockDrainTaunt}`, configToUse.LOG_TYPE_BLOCK);
|
if (addToLogCallback) addToLogCallback(`🛡️ ${targetBaseStats.name} блокирует часть урона от "${ability.name}" от ${casterBaseStats.name}! Урон снижен до ${baseDamageDrain}.${blockDrainTaunt}`, configToUse.LOG_TYPE_BLOCK);
|
||||||
}
|
}
|
||||||
damageDealtDrain = Math.max(0, baseDamageDrain);
|
damageDealtDrain = Math.max(0, baseDamageDrain);
|
||||||
targetState.currentHp = Math.max(0, targetState.currentHp - damageDealtDrain);
|
// ИСПРАВЛЕНО: Округляем результат вычитания HP
|
||||||
|
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 -= 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);
|
||||||
const actualHealGain = Math.min(potentialHeal, casterBaseStats.maxHp - casterState.currentHp);
|
const actualHealGain = Math.min(potentialHeal, casterBaseStats.maxHp - casterState.currentHp);
|
||||||
casterState.currentHp += actualHealGain;
|
// ИСПРАВЛЕНО: Округляем результат прибавления HP
|
||||||
|
casterState.currentHp = Math.round(casterState.currentHp + actualHealGain);
|
||||||
healthGained = actualHealGain;
|
healthGained = actualHealGain;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -407,12 +562,17 @@ function applyAbilityEffect(ability, casterState, targetState, casterBaseStats,
|
|||||||
if (manaDrained > 0) {
|
if (manaDrained > 0) {
|
||||||
logMsgDrain += `Вытягивает ${manaDrained} ${targetBaseStats.resourceName} у ${targetBaseStats.name} и исцеляется на ${healthGained} HP!`;
|
logMsgDrain += `Вытягивает ${manaDrained} ${targetBaseStats.resourceName} у ${targetBaseStats.name} и исцеляется на ${healthGained} HP!`;
|
||||||
} else if (damageDealtDrain > 0) {
|
} else if (damageDealtDrain > 0) {
|
||||||
logMsgDrain += `У ${targetBaseStats.name} нет ${targetBaseStats.resourceName} для похищения.`;
|
logMsgDrain += `У ${targetBaseStats.name} нет ${targetBaseStats.resourceName} для похищения.`; // Урон прошел, но ресурс не вытянулся
|
||||||
} else {
|
} else { // Ни урона, ни вытягивания ресурса
|
||||||
logMsgDrain += `У ${targetBaseStats.name} нет ${targetBaseStats.resourceName} для похищения, эффект не сработал!`;
|
// ИСПРАВЛЕНО: targetBaseStats.resourceName -> targetState.resourceName (или defenderBaseStats.resourceName, если он передается)
|
||||||
|
logMsgDrain += `У ${targetBaseStats.name} нет ${targetBaseStats.resourceName} для похищения.`; // Оставляем targetBaseStats.resourceName, т.к. он точнее для лога
|
||||||
|
// Если урон был 0, и ресурса нет, можно уточнить лог
|
||||||
|
if(damageDealtDrain === 0 && potentialDrain > 0) logMsgDrain += ` Урон не прошел или равен нулю, ресурс не похищен.`;
|
||||||
}
|
}
|
||||||
logMsgDrain += tauntMessagePart;
|
logMsgDrain += tauntMessagePart; // Добавляем насмешку цели, если была
|
||||||
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);
|
||||||
|
} else {
|
||||||
|
console.warn(`applyAbilityEffect: Drain type ability ${ability?.name} used by non-Balard character ${casterState.characterKey}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -421,88 +581,145 @@ function applyAbilityEffect(ability, casterState, targetState, casterBaseStats,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Логика принятия решения для AI (Балард).
|
||||||
|
* @param {object} currentGameState - Текущее состояние игры.
|
||||||
|
* @param {object} gameDataForLogic - Данные игры.
|
||||||
|
* @param {object} configToUse - Конфигурация игры.
|
||||||
|
* @param {function} addToLogCallback - Функция для добавления лога.
|
||||||
|
* @returns {object} Объект с действием AI ({ actionType: 'attack' | 'ability', ability?: object }).
|
||||||
|
*/
|
||||||
function decideAiAction(currentGameState, gameDataForLogic = gameData, configToUse, addToLogCallback) {
|
function decideAiAction(currentGameState, gameDataForLogic = gameData, configToUse, addToLogCallback) {
|
||||||
const opponentState = currentGameState.opponent;
|
const opponentState = currentGameState.opponent; // AI Балард всегда в слоте opponent
|
||||||
const playerState = currentGameState.player;
|
const playerState = currentGameState.player; // Игрок всегда в слоте player (в AI режиме)
|
||||||
|
|
||||||
|
// Убеждаемся, что это AI Балард
|
||||||
if (opponentState.characterKey !== 'balard') {
|
if (opponentState.characterKey !== 'balard') {
|
||||||
console.warn("[AI DEBUG] decideAiAction called for non-Balard opponent. This should not happen.");
|
console.warn("[AI DEBUG] decideAiAction called for non-Balard opponent. This should not happen.");
|
||||||
return { actionType: 'pass', logMessage: { message: `${opponentState.name} (не AI) пропускает ход.`, type: configToUse.LOG_TYPE_INFO } };
|
return { actionType: 'pass', logMessage: { message: `${opponentState.name} (не AI) пропускает ход.`, type: configToUse.LOG_TYPE_INFO } };
|
||||||
}
|
}
|
||||||
|
|
||||||
const isBalardFullySilencedByElena = opponentState.activeEffects.some(
|
// Проверка полного безмолвия Баларда (от Гипнотического Взгляда Елены или Раскола Разума Альмагест)
|
||||||
eff => eff.id === 'fullSilenceByElena' && eff.turnsLeft > 0
|
const isBalardFullySilenced = opponentState.activeEffects.some(
|
||||||
|
eff => eff.isFullSilence && eff.turnsLeft > 0
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isBalardFullySilencedByElena) {
|
if (isBalardFullySilenced) {
|
||||||
if (addToLogCallback) addToLogCallback(`😵 ${opponentState.name} под действием "Гипнотического взгляда"! Атакует в смятении.`, configToUse.LOG_TYPE_EFFECT);
|
// AI под полным безмолвием просто атакует
|
||||||
|
// Лог о безмолвии и атаке в смятении добавляется в processAiTurn перед вызовом performAttack.
|
||||||
|
// decideAiAction просто возвращает действие.
|
||||||
return { actionType: 'attack' };
|
return { actionType: 'attack' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableActions = [];
|
const availableActions = [];
|
||||||
const opponentAbilities = gameDataForLogic.opponentAbilities;
|
const opponentAbilities = gameDataForLogic.opponentAbilities; // Способности Баларда
|
||||||
|
|
||||||
|
// Проверяем доступность способностей AI и добавляем их в список возможных действий с весом
|
||||||
|
// Вес определяет приоритет: выше вес -> выше шанс выбора (после сортировки)
|
||||||
|
|
||||||
const healAbility = opponentAbilities.find(a => a.id === configToUse.ABILITY_ID_BALARD_HEAL);
|
const healAbility = opponentAbilities.find(a => a.id === configToUse.ABILITY_ID_BALARD_HEAL);
|
||||||
if (healAbility && opponentState.currentResource >= healAbility.cost &&
|
if (healAbility && opponentState.currentResource >= healAbility.cost &&
|
||||||
|
(opponentState.abilityCooldowns?.[healAbility.id] || 0) <= 0 && // Проверка общего КД (хотя у Баларда могут быть только спец. КД)
|
||||||
healAbility.condition(opponentState, playerState, currentGameState, configToUse)) {
|
healAbility.condition(opponentState, playerState, currentGameState, configToUse)) {
|
||||||
availableActions.push({ weight: 80, type: 'ability', ability: healAbility, requiresSuccessCheck: true, successRate: healAbility.successRate });
|
availableActions.push({ weight: 80, type: 'ability', ability: healAbility, requiresSuccessCheck: true, successRate: healAbility.successRate });
|
||||||
}
|
}
|
||||||
|
|
||||||
const silenceAbility = opponentAbilities.find(a => a.id === configToUse.ABILITY_ID_BALARD_SILENCE);
|
const silenceAbility = opponentAbilities.find(a => a.id === configToUse.ABILITY_ID_BALARD_SILENCE);
|
||||||
if (silenceAbility && opponentState.currentResource >= silenceAbility.cost &&
|
if (silenceAbility && opponentState.currentResource >= silenceAbility.cost &&
|
||||||
(opponentState.silenceCooldownTurns === undefined || opponentState.silenceCooldownTurns <= 0) &&
|
(opponentState.silenceCooldownTurns === undefined || opponentState.silenceCooldownTurns <= 0) && // Проверка спец. КД безмолвия
|
||||||
(!opponentState.abilityCooldowns || opponentState.abilityCooldowns[silenceAbility.id] === undefined || opponentState.abilityCooldowns[silenceAbility.id] <=0) &&
|
(opponentState.abilityCooldowns?.[silenceAbility.id] || 0) <= 0 && // Проверка общего КД
|
||||||
silenceAbility.condition(opponentState, playerState, currentGameState, configToUse)) {
|
silenceAbility.condition(opponentState, playerState, currentGameState, configToUse)) {
|
||||||
const playerHpPercent = (playerState.currentHp / playerState.maxHp) * 100;
|
const playerHpPercent = (playerState.currentHp / playerState.maxHp) * 100;
|
||||||
if (playerHpPercent > (configToUse.PLAYER_HP_BLEED_THRESHOLD_PERCENT || 40)) {
|
// Балард предпочитает безмолвие, если HP Елены не слишком низкое (позволяет ей лечиться, чтобы игра длилась дольше)
|
||||||
|
if (playerHpPercent > (configToUse.PLAYER_HP_BLEED_THRESHOLD_PERCENT || 60)) { // Используем порог для текстов Елены как пример
|
||||||
availableActions.push({ weight: 60, type: 'ability', ability: silenceAbility, requiresSuccessCheck: true, successRate: configToUse.SILENCE_SUCCESS_RATE });
|
availableActions.push({ weight: 60, type: 'ability', ability: silenceAbility, requiresSuccessCheck: true, successRate: configToUse.SILENCE_SUCCESS_RATE });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const drainAbility = opponentAbilities.find(a => a.id === configToUse.ABILITY_ID_BALARD_MANA_DRAIN);
|
const drainAbility = opponentAbilities.find(a => a.id === configToUse.ABILITY_ID_BALARD_MANA_DRAIN);
|
||||||
if (drainAbility && opponentState.currentResource >= drainAbility.cost &&
|
if (drainAbility && opponentState.currentResource >= drainAbility.cost &&
|
||||||
(opponentState.manaDrainCooldownTurns === undefined || opponentState.manaDrainCooldownTurns <= 0) &&
|
(opponentState.manaDrainCooldownTurns === undefined || opponentState.manaDrainCooldownTurns <= 0) && // Проверка спец. КД дрейна
|
||||||
(!opponentState.abilityCooldowns || opponentState.abilityCooldowns[drainAbility.id] === undefined || opponentState.abilityCooldowns[drainAbility.id] <=0) &&
|
(opponentState.abilityCooldowns?.[drainAbility.id] || 0) <= 0 && // Проверка общего КД
|
||||||
drainAbility.condition(opponentState, playerState, currentGameState, configToUse)) {
|
drainAbility.condition(opponentState, playerState, currentGameState, configToUse)) {
|
||||||
availableActions.push({ weight: 50, type: 'ability', ability: drainAbility });
|
availableActions.push({ weight: 50, type: 'ability', ability: drainAbility });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Базовая атака - всегда доступна как запасной вариант с низким весом
|
||||||
availableActions.push({ weight: 30, type: 'attack' });
|
availableActions.push({ weight: 30, type: 'attack' });
|
||||||
|
|
||||||
|
// Если по какой-то причине список доступных действий пуст (не должно быть, т.к. атака всегда есть)
|
||||||
if (availableActions.length === 0) {
|
if (availableActions.length === 0) {
|
||||||
return { actionType: 'pass', logMessage: { message: `${opponentState.name} не может совершить действие.`, type: configToUse.LOG_TYPE_INFO } };
|
return { actionType: 'pass', logMessage: { message: `${opponentState.name} не может совершить действие.`, type: configToUse.LOG_TYPE_INFO } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Сортируем действия по весу в порядке убывания
|
||||||
availableActions.sort((a, b) => b.weight - a.weight);
|
availableActions.sort((a, b) => b.weight - a.weight);
|
||||||
|
|
||||||
|
// Перебираем действия в порядке приоритета и выбираем первое возможное
|
||||||
for (const action of availableActions) {
|
for (const action of availableActions) {
|
||||||
if (action.requiresSuccessCheck) {
|
if (action.type === 'ability') {
|
||||||
if (Math.random() < action.successRate) {
|
// Если способность требует проверки успеха (например, Безмолвие Баларда)
|
||||||
return { actionType: action.type, ability: action.ability };
|
if (action.requiresSuccessCheck) {
|
||||||
|
if (Math.random() < action.successRate) {
|
||||||
|
// Успех, добавляем лог о попытке (чтобы было видно, что AI пытался)
|
||||||
|
if (addToLogCallback) addToLogCallback(`⭐ ${opponentState.name} пытается использовать "${action.ability.name}"...`, configToUse.LOG_TYPE_INFO);
|
||||||
|
return { actionType: action.type, ability: action.ability }; // Успех, выбираем эту способность
|
||||||
|
} else {
|
||||||
|
// Провал, добавляем лог о провале и переходим к следующему возможному действию в цикле
|
||||||
|
if (addToLogCallback) addToLogCallback(`💨 Попытка ${opponentState.name} использовать "${action.ability.name}" провалилась!`, configToUse.LOG_TYPE_INFO);
|
||||||
|
continue; // Пробуем следующее действие в списке
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (addToLogCallback) addToLogCallback(`💨 ${opponentState.name} пытается использовать "${action.ability.name}", но терпит неудачу!`, configToUse.LOG_TYPE_INFO);
|
// Нет проверки успеха, добавляем лог о попытке и выбираем способность
|
||||||
continue;
|
if (addToLogCallback) addToLogCallback(`⭐ ${opponentState.name} использует "${action.ability.name}"...`, configToUse.LOG_TYPE_INFO);
|
||||||
|
return { actionType: action.type, ability: action.ability };
|
||||||
}
|
}
|
||||||
} else {
|
} else if (action.type === 'attack') {
|
||||||
return { actionType: action.type, ability: action.ability };
|
// Атака - всегда возможна (если нет полного безмолвия, проверено выше)
|
||||||
|
if (addToLogCallback) addToLogCallback(`🦶 ${opponentState.name} готовится к атаке...`, configToUse.LOG_TYPE_INFO);
|
||||||
|
return { actionType: 'attack' };
|
||||||
}
|
}
|
||||||
|
// 'pass' не должен быть в доступных действиях, если атака всегда доступна
|
||||||
}
|
}
|
||||||
return { actionType: 'attack' };
|
|
||||||
|
// Если все попытки выбрать способность или атаку провалились (очень маловероятно, если атака всегда в списке), пропуск хода
|
||||||
|
console.warn("[AI DEBUG] AI failed to select any action. Defaulting to pass.");
|
||||||
|
return { actionType: 'pass', logMessage: { message: `${opponentState.name} не смог выбрать подходящее действие. Пропускает ход.`, type: configToUse.LOG_TYPE_INFO } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Внутренняя проверка условий конца игры (основано на HP).
|
||||||
|
* @param {object} currentGameState - Текущее состояние игры.
|
||||||
|
* @param {object} configToUse - Конфигурация игры.
|
||||||
|
* @param {object} gameDataForLogic - Данные игры.
|
||||||
|
* @returns {boolean} true, если игра окончена, иначе false.
|
||||||
|
*/
|
||||||
function checkGameOverInternal(currentGameState, configToUse, gameDataForLogic = gameData) {
|
function checkGameOverInternal(currentGameState, configToUse, gameDataForLogic = gameData) {
|
||||||
|
// Проверка на конец игры происходит только если gameState существует и игра еще не помечена как оконченная
|
||||||
if (!currentGameState || currentGameState.isGameOver) return currentGameState ? currentGameState.isGameOver : true;
|
if (!currentGameState || currentGameState.isGameOver) return currentGameState ? currentGameState.isGameOver : true;
|
||||||
|
|
||||||
|
// Убеждаемся, что оба бойца определены в gameState и не являются плейсхолдерами
|
||||||
|
// Проверка maxHp > 0 в gameState.opponent гарантирует, что оппонент не плейсхолдер
|
||||||
|
if (!currentGameState.player || !currentGameState.opponent || currentGameState.opponent.maxHp <= 0) {
|
||||||
|
// Если один из бойцов не готов (например, PvP игра ожидает второго игрока), игра не может закончиться по HP
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const playerDead = currentGameState.player.currentHp <= 0;
|
const playerDead = currentGameState.player.currentHp <= 0;
|
||||||
const opponentDead = currentGameState.opponent.currentHp <= 0;
|
const opponentDead = currentGameState.opponent.currentHp <= 0;
|
||||||
|
|
||||||
|
// Игра окончена, если один или оба бойца мертвы
|
||||||
return playerDead || opponentDead;
|
return playerDead || opponentDead;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Экспортируем все функции, которые используются в других модулях
|
||||||
module.exports = {
|
module.exports = {
|
||||||
processEffects,
|
processEffects,
|
||||||
processDisabledAbilities,
|
processDisabledAbilities,
|
||||||
processPlayerAbilityCooldowns,
|
processPlayerAbilityCooldowns,
|
||||||
updateBlockingStatus,
|
updateBlockingStatus,
|
||||||
getElenaTaunt,
|
getRandomTaunt, // Экспортируем переименованную функцию
|
||||||
performAttack,
|
performAttack,
|
||||||
applyAbilityEffect,
|
applyAbilityEffect,
|
||||||
decideAiAction,
|
decideAiAction,
|
||||||
|
@ -1,270 +1,520 @@
|
|||||||
// /server_modules/gameManager.js
|
// /server_modules/gameManager.js
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid'); // Убедитесь, что uuidv4 установлен: npm install uuid
|
||||||
const GameInstance = require('./gameInstance');
|
const GameInstance = require('./gameInstance'); // Убедитесь, что GameInstance экспортируется из gameInstance.js
|
||||||
const gameData = require('./data'); // Нужен для getAvailablePvPGamesListForClient
|
const gameData = require('./data'); // Нужен для getAvailablePvPGamesListForClient и данных персонажей
|
||||||
const GAME_CONFIG = require('./config'); // Нужен для GAME_CONFIG.PLAYER_ID и других констант
|
const GAME_CONFIG = require('./config'); // Нужен для GAME_CONFIG.PLAYER_ID и других констант
|
||||||
|
|
||||||
class GameManager {
|
class GameManager {
|
||||||
constructor(io) {
|
constructor(io) {
|
||||||
this.io = io;
|
this.io = io; // Ссылка на Socket.IO сервер для широковещательных рассылок
|
||||||
this.games = {}; // { gameId: GameInstance }
|
this.games = {}; // { gameId: GameInstance } - Все активные или ожидающие игры
|
||||||
this.socketToGame = {}; // { socket.id: gameId }
|
this.userIdentifierToGameId = {}; // { userId|socketId: gameId } - Какому пользователю какая игра соответствует (более стабильно, чем socket.id)
|
||||||
this.pendingPvPGames = []; // [gameId] - ID игр, ожидающих второго игрока в PvP
|
this.pendingPvPGames = []; // [gameId] - ID PvP игр, ожидающих второго игрока
|
||||||
this.userToPendingGame = {}; // { userId: gameId } или { socketId: gameId } - для отслеживания созданных ожидающих игр
|
|
||||||
|
// Навешиваем обработчик события 'gameOver' на Socket.IO сервер
|
||||||
|
// Это событие исходит от экземпляра GameInstance при завершении игры (по HP или дисконнекту)
|
||||||
|
// Мы слушаем его здесь, чтобы GameManager мог очистить ссылки.
|
||||||
|
// Примечание: Это событие отправляется всем в комнате игры. GameManager слушает его через io.sockets.sockets.on,
|
||||||
|
// но удобнее слушать его на уровне io, если возможно, или добавить специальный emit из GameInstance.
|
||||||
|
// Текущая архитектура (GameInstance напрямую вызывает io.to(...).emit('gameOver', ...)) уже рабочая.
|
||||||
|
// GameManager сам должен отреагировать на завершение, проверяя gameState.isGameOver после каждого действия/хода.
|
||||||
|
// Или GameInstance должен вызвать специальный метод GameManager при gameOver.
|
||||||
|
// Давайте сделаем GameInstance вызывать метод GameManager при gameOver.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Удаляет предыдущую ожидающую игру пользователя, если таковая существует.
|
||||||
|
* Это предотвращает создание множества пустых игр одним пользователем.
|
||||||
|
* @param {string} currentSocketId - ID текущего сокета.
|
||||||
|
* @param {string|number} identifier - userId или socketId пользователя.
|
||||||
|
* @param {string|null} excludeGameId - ID игры, которую НЕ нужно удалять (например, если пользователь присоединяется к своей же игре).
|
||||||
|
*/
|
||||||
_removePreviousPendingGames(currentSocketId, identifier, excludeGameId = null) {
|
_removePreviousPendingGames(currentSocketId, identifier, excludeGameId = null) {
|
||||||
const keyToUse = identifier || currentSocketId;
|
// Ищем игру по идентификатору пользователя
|
||||||
const oldPendingGameId = this.userToPendingGame[keyToUse];
|
const oldPendingGameId = this.userIdentifierToGameId[identifier];
|
||||||
|
|
||||||
if (oldPendingGameId && oldPendingGameId !== excludeGameId) {
|
// Проверяем, что нашли игру, она не исключена, и она все еще существует в списке игр
|
||||||
|
if (oldPendingGameId && oldPendingGameId !== excludeGameId && this.games[oldPendingGameId]) {
|
||||||
const gameToRemove = this.games[oldPendingGameId];
|
const gameToRemove = this.games[oldPendingGameId];
|
||||||
if (gameToRemove && gameToRemove.mode === 'pvp' && gameToRemove.playerCount === 1 && this.pendingPvPGames.includes(oldPendingGameId)) {
|
// Проверяем, что игра является ожидающей PvP игрой с одним игроком
|
||||||
const playersInOldGame = Object.values(gameToRemove.players);
|
if (gameToRemove.mode === 'pvp' && gameToRemove.playerCount === 1 && this.pendingPvPGames.includes(oldPendingGameId)) {
|
||||||
const isOwnerBySocket = playersInOldGame.length === 1 && playersInOldGame[0].socket.id === currentSocketId;
|
// Проверяем, что этот пользователь является владельцем этой ожидающей игры
|
||||||
const isOwnerByUserId = identifier && gameToRemove.ownerUserId === identifier;
|
// Владелец в pendingPvPGames - это всегда тот, кто ее создал (первый игрок в слоте PLAYER_ID)
|
||||||
|
const oldOwnerInfo = Object.values(gameToRemove.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||||||
|
|
||||||
if (isOwnerBySocket || isOwnerByUserId) {
|
// Проверяем, что владелец игры существует и его идентификатор совпадает
|
||||||
console.log(`[GameManager] Пользователь ${keyToUse} (сокет: ${currentSocketId}) создал/присоединился к новой игре. Удаляем его предыдущую ожидающую игру: ${oldPendingGameId}`);
|
if (oldOwnerInfo && (oldOwnerInfo.identifier === identifier)) {
|
||||||
|
console.log(`[GameManager] Пользователь ${identifier} (сокет: ${currentSocketId}) создал/присоединился к новой игре. Удаляем его предыдущую ожидающую игру: ${oldPendingGameId}`);
|
||||||
|
|
||||||
delete this.games[oldPendingGameId];
|
// Используем централизованную функцию очистки
|
||||||
const pendingIndex = this.pendingPvPGames.indexOf(oldPendingGameId);
|
this._cleanupGame(oldPendingGameId, 'replaced_by_new_game');
|
||||||
if (pendingIndex > -1) this.pendingPvPGames.splice(pendingIndex, 1);
|
|
||||||
|
|
||||||
if (playersInOldGame.length === 1 && this.socketToGame[playersInOldGame[0].socket.id] === oldPendingGameId) {
|
// Оповещаем клиентов об обновленном списке игр (уже внутри _cleanupGame)
|
||||||
delete this.socketToGame[playersInOldGame[0].socket.id];
|
// this.broadcastAvailablePvPGames();
|
||||||
}
|
|
||||||
delete this.userToPendingGame[keyToUse];
|
|
||||||
|
|
||||||
this.broadcastAvailablePvPGames();
|
|
||||||
}
|
}
|
||||||
} else if (oldPendingGameId === excludeGameId) {
|
|
||||||
// Это та же игра, к которой игрок присоединяется, ничего не делаем
|
|
||||||
} else {
|
} else {
|
||||||
delete this.userToPendingGame[keyToUse];
|
// Если игра не соответствует критериям ожидающей игры, но идентификатор был связан с ней,
|
||||||
|
// это может означать, что игра уже началась или была завершена.
|
||||||
|
// Просто очищаем ссылку, если она не ведет в исключенную игру.
|
||||||
|
// Идентификатор должен был быть очищен из userIdentifierToGameId при старте или завершении игры.
|
||||||
|
// На всякий случай убеждаемся, что мы не удаляем ссылку на игру, к которой только что присоединились.
|
||||||
|
if (this.userIdentifierToGameId[identifier] !== excludeGameId) {
|
||||||
|
console.warn(`[GameManager] Удаление потенциально некорректной ссылки userIdentifierToGameId[${identifier}] на игру ${oldPendingGameId}.`);
|
||||||
|
delete this.userIdentifierToGameId[identifier];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Если oldPendingGameId не найдена, или она равна excludeGameId, ничего не делаем.
|
||||||
}
|
}
|
||||||
|
|
||||||
createGame(socket, mode = 'ai', chosenCharacterKey = 'elena', userId = null) {
|
|
||||||
const identifier = userId || socket.id;
|
/**
|
||||||
|
* Создает новую игру.
|
||||||
|
* @param {object} socket - Сокет игрока, создающего игру.
|
||||||
|
* @param {string} [mode='ai'] - Режим игры ('ai' или 'pvp').
|
||||||
|
* @param {string} [chosenCharacterKey='elena'] - Выбранный персонаж для первого игрока в PvP.
|
||||||
|
* @param {string|number} identifier - ID пользователя (userId или socketId).
|
||||||
|
*/
|
||||||
|
createGame(socket, mode = 'ai', chosenCharacterKey = 'elena', identifier) {
|
||||||
|
// Удаляем старые ожидающие игры этого пользователя, прежде чем создавать новую
|
||||||
this._removePreviousPendingGames(socket.id, identifier);
|
this._removePreviousPendingGames(socket.id, identifier);
|
||||||
|
|
||||||
const gameId = uuidv4();
|
// Проверяем, не находится ли пользователь уже в какой-то игре (активной или ожидающей)
|
||||||
const game = new GameInstance(gameId, this.io, mode);
|
// Проверяем наличие ссылки на игру по идентификатору пользователя
|
||||||
if (userId) game.ownerUserId = userId;
|
if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]]) {
|
||||||
this.games[gameId] = game;
|
console.warn(`[GameManager] Пользователь ${identifier} (сокет: ${socket.id}) уже в игре ${this.userIdentifierToGameId[identifier]}. Игнорируем запрос на создание.`);
|
||||||
|
socket.emit('gameError', { message: 'Вы уже находитесь в активной или ожидающей игре.' });
|
||||||
|
// Можно попробовать отправить состояние текущей игры пользователю
|
||||||
|
this.handleRequestGameState(socket, identifier);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// В AI режиме игрок всегда Елена, в PvP - тот, кого выбрали
|
|
||||||
|
const gameId = uuidv4(); // Генерируем уникальный ID для игры
|
||||||
|
// Передаем ссылку на GameManager в GameInstance, чтобы он мог вызвать _notifyGameEnded
|
||||||
|
const game = new GameInstance(gameId, this.io, mode, this); // <-- ПЕРЕДАЕМ GameManager
|
||||||
|
game.ownerIdentifier = identifier; // Сохраняем идентификатор создателя
|
||||||
|
this.games[gameId] = game; // Добавляем игру в список активных игр
|
||||||
|
|
||||||
|
// В AI режиме игрок всегда Елена, в PvP - тот, кого выбрали при создании
|
||||||
const charKeyForInstance = (mode === 'pvp') ? chosenCharacterKey : 'elena';
|
const charKeyForInstance = (mode === 'pvp') ? chosenCharacterKey : 'elena';
|
||||||
|
|
||||||
if (game.addPlayer(socket, charKeyForInstance)) {
|
// Добавляем игрока в созданный экземпляр игры, передавая идентификатор
|
||||||
this.socketToGame[socket.id] = gameId;
|
// GameInstance.addPlayer принимает socket, chosenCharacterKey, identifier
|
||||||
console.log(`[GameManager] Игра создана: ${gameId} (режим: ${mode}) игроком ${socket.userData?.username || socket.id} (userId: ${userId}, выбран: ${charKeyForInstance})`);
|
if (game.addPlayer(socket, charKeyForInstance, identifier)) {
|
||||||
|
this.userIdentifierToGameId[identifier] = gameId; // Связываем идентификатор пользователя с этой игрой
|
||||||
|
console.log(`[GameManager] Игра создана: ${gameId} (режим: ${mode}) игроком ${identifier} (сокет: ${socket.id}, выбран: ${charKeyForInstance})`);
|
||||||
|
|
||||||
const assignedPlayerId = game.players[socket.id]?.id;
|
// Уведомляем игрока, что игра создана, и передаем его технический ID слота
|
||||||
|
const assignedPlayerId = game.players[socket.id]?.id; // ID слота все еще берем из playerInfo по socket.id
|
||||||
if (!assignedPlayerId) {
|
if (!assignedPlayerId) {
|
||||||
delete this.games[gameId]; if(this.socketToGame[socket.id] === gameId) delete this.socketToGame[socket.id];
|
// Если по какой-то причине не удалось назначить ID игрока, удаляем игру и отправляем ошибку
|
||||||
socket.emit('gameError', { message: 'Ошибка сервера при создании игры (не удалось назначить ID игрока).' }); return;
|
// Используем централизованную функцию очистки
|
||||||
|
this._cleanupGame(gameId, 'player_add_failed');
|
||||||
|
console.error(`[GameManager] Ошибка при создании игры ${gameId}: Не удалось назначить ID игрока сокету ${socket.id} (идентификатор ${identifier}).`);
|
||||||
|
socket.emit('gameError', { message: 'Ошибка сервера при создании игры.' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
socket.emit('gameCreated', { gameId: gameId, mode: mode, yourPlayerId: assignedPlayerId });
|
socket.emit('gameCreated', { gameId: gameId, mode: mode, yourPlayerId: assignedPlayerId });
|
||||||
|
|
||||||
if (mode === 'pvp') {
|
|
||||||
if (!this.pendingPvPGames.includes(gameId)) this.pendingPvPGames.push(gameId);
|
// --- Логика старта игры ---
|
||||||
this.userToPendingGame[identifier] = gameId;
|
// Если игра AI и теперь с 1 игроком, или PvP и теперь с 2 игроками, запускаем ее немедленно
|
||||||
this.broadcastAvailablePvPGames();
|
if ((game.mode === 'ai' && game.playerCount === 1) || (game.mode === 'pvp' && game.playerCount === 2)) {
|
||||||
|
console.log(`[GameManager] Игра ${gameId} готова к старту. Инициализация и запуск.`);
|
||||||
|
// Инициализируем состояние игры. initializeGame вернет true, если оба бойца определены.
|
||||||
|
const isInitialized = game.initializeGame();
|
||||||
|
if (isInitialized) { // Проверяем, успешно ли инициализировалось состояние
|
||||||
|
game.startGame(); // Запускаем игру
|
||||||
|
} else {
|
||||||
|
console.error(`[GameManager] Не удалось запустить игру ${gameId}: initializeGame вернул false или gameState некорректен после инициализации.`);
|
||||||
|
// initializeGame уже должен был добавить ошибку в лог игры и отправить gameError клиентам
|
||||||
|
// Возможно, стоит вызвать cleanupGame здесь при ошибке инициализации
|
||||||
|
this._cleanupGame(gameId, 'initialization_failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если игра PvP и только что заполнилась, удаляем ее из списка ожидающих
|
||||||
|
// Идентификаторы игроков остаются связанными с игрой в userIdentifierToGameId до ее завершения.
|
||||||
|
if (game.mode === 'pvp' && game.playerCount === 2) {
|
||||||
|
const gameIndex = this.pendingPvPGames.indexOf(gameId);
|
||||||
|
if (gameIndex > -1) this.pendingPvPGames.splice(gameIndex, 1);
|
||||||
|
// Связи userIdentifierToGameId[identifier] НЕ УДАЛЯЕМ! Они нужны для активной игры.
|
||||||
|
this.broadcastAvailablePvPGames(); // Обновляем список у всех клиентов
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} else if (mode === 'pvp' && game.playerCount === 1) {
|
||||||
|
// Если игра PvP и ожидает второго игрока, добавляем ее в список ожидающих
|
||||||
|
if (!this.pendingPvPGames.includes(gameId)) {
|
||||||
|
this.pendingPvPGames.push(gameId); // Добавляем ID игры в список ожидающих
|
||||||
|
}
|
||||||
|
// userIdentifierToGameId для создателя уже установлен выше
|
||||||
|
|
||||||
|
// Частичная инициализация gameState для отображения Player 1 на UI ожидания
|
||||||
|
// initializeGame вызывается при playerCount === 1 в GameInstance
|
||||||
|
game.initializeGame();
|
||||||
|
|
||||||
|
this.broadcastAvailablePvPGames(); // Обновляем список у всех
|
||||||
}
|
}
|
||||||
|
// --- КОНЕЦ Логики старта игры ---
|
||||||
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
delete this.games[gameId];
|
// Если не удалось добавить игрока в GameInstance (например, уже 2 игрока - хотя проверили выше), удаляем игру
|
||||||
if (this.socketToGame[socket.id] === gameId) delete this.socketToGame[socket.id];
|
// Используем централизованную функцию очистки
|
||||||
// Сообщение об ошибке отправляется из game.addPlayer
|
this._cleanupGame(gameId, 'player_add_failed');
|
||||||
|
// GameInstance.addPlayer уже отправил ошибку клиенту
|
||||||
|
console.warn(`[GameManager] Не удалось добавить игрока ${socket.id} (идентификатор ${identifier}) в игру ${gameId}. Игра удалена.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
joinGame(socket, gameId, userId = null) {
|
/**
|
||||||
const identifier = userId || socket.id;
|
* Присоединяет игрока к существующей игре по ID.
|
||||||
const game = this.games[gameId];
|
* @param {object} socket - Сокет игрока.
|
||||||
|
* @param {string} gameId - ID игры, к которой нужно присоединиться.
|
||||||
|
* @param {string|number} identifier - ID пользователя (userId).
|
||||||
|
*/
|
||||||
|
joinGame(socket, gameId, identifier) { // В joinGame всегда передается userId, т.к. PvP требует логина
|
||||||
|
const game = this.games[gameId]; // Находим игру по ID
|
||||||
|
|
||||||
|
// Проверки перед присоединением
|
||||||
if (!game) { socket.emit('gameError', { message: 'Игра с таким ID не найдена.' }); return; }
|
if (!game) { socket.emit('gameError', { message: 'Игра с таким ID не найдена.' }); return; }
|
||||||
if (game.mode !== 'pvp') { socket.emit('gameError', { message: 'Эта игра не является PvP игрой.' }); return; }
|
if (game.mode !== 'pvp') { socket.emit('gameError', { message: 'К этой игре нельзя присоединиться как к PvP.' }); return; }
|
||||||
if (game.playerCount >= 2) { socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return; }
|
if (game.playerCount >= 2) { socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return; }
|
||||||
if (game.players[socket.id]) { socket.emit('gameError', { message: 'Вы уже в этой игре.' }); return;}
|
// Проверка, не находится ли пользователь уже в какой-то игре
|
||||||
|
if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]] && this.userIdentifierToGameId[identifier] !== gameId) {
|
||||||
|
console.warn(`[GameManager] Пользователь ${identifier} (сокет: ${socket.id}) уже в игре ${this.userIdentifierToGameId[identifier]}. Игнорируем запрос на присоединение.`);
|
||||||
|
socket.emit('gameError', { message: 'Вы уже находитесь в активной или ожидающей игре.' });
|
||||||
|
this.handleRequestGameState(socket, identifier); // Попробуем отправить состояние текущей игры
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (game.players[socket.id]) { socket.emit('gameError', { message: 'Вы уже в этой игре.' }); return;} // Проверка на повторное присоединение по текущему сокету (хотя userIdentifierToGameId должен это предотвратить)
|
||||||
|
|
||||||
|
// Удаляем старые ожидающие игры этого пользователя, исключая текущую игру, к которой присоединяемся
|
||||||
this._removePreviousPendingGames(socket.id, identifier, gameId);
|
this._removePreviousPendingGames(socket.id, identifier, gameId);
|
||||||
|
|
||||||
// addPlayer в GameInstance сам определит персонажа для второго игрока на основе первого
|
// addPlayer в GameInstance сам определит персонажа для второго игрока на основе первого
|
||||||
if (game.addPlayer(socket)) {
|
// GameInstance.addPlayer принимает socket, chosenCharacterKey (null для присоединения), identifier
|
||||||
this.socketToGame[socket.id] = gameId;
|
if (game.addPlayer(socket, null, identifier)) { // chosenCharacterKey для присоединяющегося игрока не нужен, передаем null
|
||||||
console.log(`[GameManager] Игрок ${socket.userData?.username || socket.id} (userId: ${userId}) присоединился к PvP игре ${gameId}`);
|
this.userIdentifierToGameId[identifier] = gameId; // Связываем идентификатор пользователя с этой игрой
|
||||||
|
console.log(`[GameManager] Игрок ${identifier} (сокет: ${socket.id}) присоединился к PvP игре ${gameId}`);
|
||||||
|
|
||||||
const gameIndex = this.pendingPvPGames.indexOf(gameId);
|
// --- Логика старта игры ---
|
||||||
if (gameIndex > -1) this.pendingPvPGames.splice(gameIndex, 1);
|
// Если игра PvP и теперь с 2 игроками, запускаем ее немедленно
|
||||||
|
if (game.mode === 'pvp' && game.playerCount === 2) {
|
||||||
if (game.ownerUserId && this.userToPendingGame[game.ownerUserId] === gameId) {
|
console.log(`[GameManager] Игра ${gameId} готова к старту. Инициализация и запуск.`);
|
||||||
delete this.userToPendingGame[game.ownerUserId];
|
// Инициализируем состояние игры. initializeGame вернет true, если оба бойца определены.
|
||||||
} else {
|
const isInitialized = game.initializeGame();
|
||||||
const firstPlayerSocketId = Object.keys(game.players).find(sId => game.players[sId].id === GAME_CONFIG.PLAYER_ID && game.players[sId].socket.id !== socket.id);
|
if (isInitialized) { // Проверяем, успешно ли инициализировалось состояние
|
||||||
if (firstPlayerSocketId && this.userToPendingGame[firstPlayerSocketId] === gameId) {
|
game.startGame(); // Запускаем игру
|
||||||
delete this.userToPendingGame[firstPlayerSocketId];
|
} else {
|
||||||
|
console.error(`[GameManager] Не удалось запустить игру ${gameId}: initializeGame вернул false или gameState некорректен после инициализации.`);
|
||||||
|
// initializeGame уже должен был добавить ошибку в лог игры и отправить gameError клиентам
|
||||||
|
// Возможно, стоит вызвать cleanupGame здесь при ошибке инициализации
|
||||||
|
this._cleanupGame(gameId, 'initialization_failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Если игра PvP и только что заполнилась, удаляем ее из списка ожидающих
|
||||||
|
const gameIndex = this.pendingPvPGames.indexOf(gameId);
|
||||||
|
if (gameIndex > -1) this.pendingPvPGames.splice(gameIndex, 1);
|
||||||
|
|
||||||
|
// Связи userIdentifierToGameId[identifier] НЕ УДАЛЯЕМ! Они нужны для активной игры.
|
||||||
|
// ownerIdentifier игры (идентификатор создателя) также остается.
|
||||||
|
|
||||||
|
this.broadcastAvailablePvPGames(); // Обновляем список у всех клиентов
|
||||||
}
|
}
|
||||||
this.broadcastAvailablePvPGames();
|
// --- КОНЕЦ Логики старта игры ---
|
||||||
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Сообщение об ошибке отправляется из game.addPlayer
|
// Сообщение об ошибке отправляется из game.addPlayer
|
||||||
|
console.warn(`[GameManager] Не удалось добавить игрока ${socket.id} (идентификатор ${identifier}) в игру ${gameId}.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', userId = null) {
|
/**
|
||||||
const identifier = userId || socket.id;
|
* Ищет случайную ожидающую PvP игру и присоединяет игрока к ней.
|
||||||
|
* Если подходящих игр нет, создает новую ожидающую игру.
|
||||||
|
* @param {object} socket - Сокет игрока.
|
||||||
|
* @param {string} [chosenCharacterKeyForCreation='elena'] - Выбранный персонаж, если придется создавать новую игру.
|
||||||
|
* @param {string|number} identifier - ID пользователя (userId).
|
||||||
|
*/
|
||||||
|
findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', identifier) { // В findRandomGame всегда передается userId
|
||||||
|
// Удаляем старые ожидающие игры этого пользователя
|
||||||
this._removePreviousPendingGames(socket.id, identifier);
|
this._removePreviousPendingGames(socket.id, identifier);
|
||||||
|
|
||||||
|
// Проверяем, не находится ли пользователь уже в какой-то игре
|
||||||
|
if (this.userIdentifierToGameId[identifier] && this.games[this.userIdentifierToGameId[identifier]]) {
|
||||||
|
console.warn(`[GameManager] Пользователь ${identifier} (сокет: ${socket.id}) уже в игре ${this.userIdentifierToGameId[identifier]}. Игнорируем запрос на поиск.`);
|
||||||
|
socket.emit('gameError', { message: 'Вы уже находитесь в активной или ожидающей игре.' });
|
||||||
|
this.handleRequestGameState(socket, identifier); // Попробуем отправить состояние текущей игры
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let gameIdToJoin = null;
|
let gameIdToJoin = null;
|
||||||
// Персонаж, которого мы бы хотели видеть у оппонента (зеркальный нашему выбору)
|
// Персонаж, которого мы бы хотели видеть у оппонента (зеркальный нашему выбору для создания)
|
||||||
const preferredOpponentKey = chosenCharacterKeyForCreation === 'elena' ? 'almagest' : 'elena';
|
const preferredOpponentKey = chosenCharacterKeyForCreation === 'elena' ? 'almagest' : 'elena';
|
||||||
|
|
||||||
// Сначала ищем игру, где первый игрок выбрал "зеркального" персонажа
|
// Ищем свободную игру в списке ожидающих
|
||||||
for (const id of this.pendingPvPGames) {
|
for (const id of this.pendingPvPGames) {
|
||||||
const pendingGame = this.games[id];
|
const pendingGame = this.games[id];
|
||||||
if (pendingGame && pendingGame.playerCount === 1 && pendingGame.mode === 'pvp') {
|
// Проверяем, что игра существует, PvP, в ней только 1 игрок и это НЕ игра, которую создал сам текущий пользователь
|
||||||
const firstPlayerInfo = Object.values(pendingGame.players)[0];
|
// Игрок не должен присоединяться к игре, которую создал сам.
|
||||||
const isMyOwnGame = (userId && pendingGame.ownerUserId === userId) || (firstPlayerInfo.socket.id === socket.id);
|
if (pendingGame && pendingGame.mode === 'pvp' && pendingGame.playerCount === 1 && pendingGame.ownerIdentifier !== identifier) {
|
||||||
if (isMyOwnGame) continue;
|
// Нашли потенциальную игру. Проверяем предпочтительного оппонента.
|
||||||
|
const firstPlayerInfo = Object.values(pendingGame.players).find(p => p.id === GAME_CONFIG.PLAYER_ID); // В ожидающей игре всегда 1 игрок, он и есть players[0]
|
||||||
if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey === preferredOpponentKey) {
|
if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey === preferredOpponentKey) {
|
||||||
gameIdToJoin = id; break;
|
gameIdToJoin = id; // Нашли игру с предпочтительным оппонентом
|
||||||
}
|
break; // Выходим из цикла, т.к. нашли лучший вариант
|
||||||
}
|
|
||||||
}
|
|
||||||
// Если не нашли с предпочтительным оппонентом, ищем любую свободную (не нашу)
|
|
||||||
if (!gameIdToJoin && this.pendingPvPGames.length > 0) {
|
|
||||||
for (const id of this.pendingPvPGames) {
|
|
||||||
const pendingGame = this.games[id];
|
|
||||||
if (pendingGame && pendingGame.playerCount === 1 && pendingGame.mode === 'pvp') {
|
|
||||||
const firstPlayerInfo = Object.values(pendingGame.players)[0];
|
|
||||||
const isMyOwnGame = (userId && pendingGame.ownerUserId === userId) || (firstPlayerInfo.socket.id === socket.id);
|
|
||||||
if (isMyOwnGame) continue;
|
|
||||||
gameIdToJoin = id; break;
|
|
||||||
}
|
}
|
||||||
|
// Если предпочтительного не нашли в этом цикле, сохраняем ID первой попавшейся (не своей) игры
|
||||||
|
if (!gameIdToJoin) gameIdToJoin = id; // Сохраняем, но продолжаем искать предпочтительную
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gameIdToJoin) {
|
if (gameIdToJoin) {
|
||||||
// Присоединяемся к найденной игре. GameInstance.addPlayer сам назначит нужного персонажа второму игроку.
|
// Присоединяемся к найденной игре. GameInstance.addPlayer сам назначит нужного персонажа второму игроку.
|
||||||
this.joinGame(socket, gameIdToJoin, userId);
|
console.log(`[GameManager] Игрок ${identifier} (сокет: ${socket.id}) нашел игру ${gameIdToJoin} и присоединяется.`);
|
||||||
|
this.joinGame(socket, gameIdToJoin, identifier); // Используем joinGame, т.к. логика присоединения одинакова
|
||||||
} else {
|
} else {
|
||||||
// Если свободных игр нет, создаем новую с выбранным персонажем
|
// Если свободных игр нет, создаем новую с выбранным персонажем
|
||||||
this.createGame(socket, 'pvp', chosenCharacterKeyForCreation, userId);
|
console.log(`[GameManager] Игрок ${identifier} (сокет: ${socket.id}) не нашел свободных игр. Создает новую.`);
|
||||||
|
this.createGame(socket, 'pvp', chosenCharacterKeyForCreation, identifier); // Используем createGame
|
||||||
// Клиент получит 'gameCreated', а 'noPendingGamesFound' используется для информационного сообщения
|
// Клиент получит 'gameCreated', а 'noPendingGamesFound' используется для информационного сообщения
|
||||||
|
// userIdentifierToGameId уже обновлен в createGame
|
||||||
socket.emit('noPendingGamesFound', {
|
socket.emit('noPendingGamesFound', {
|
||||||
message: 'Свободных PvP игр не найдено. Создана новая игра для вас. Ожидайте противника.',
|
message: 'Свободных PvP игр не найдено. Создана новая игра для вас. Ожидайте противника.',
|
||||||
gameId: this.userToPendingGame[identifier], // ID только что созданной игры
|
gameId: this.userIdentifierToGameId[identifier], // ID только что созданной игры
|
||||||
yourPlayerId: GAME_CONFIG.PLAYER_ID // При создании всегда PLAYER_ID
|
yourPlayerId: GAME_CONFIG.PLAYER_ID // При создании всегда PLAYER_ID
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePlayerAction(socketId, actionData) {
|
/**
|
||||||
const gameIdFromSocket = this.socketToGame[socketId];
|
* Перенаправляет действие игрока соответствующему экземпляру игры.
|
||||||
const game = this.games[gameIdFromSocket];
|
* @param {string|number} identifier - ID пользователя (userId или socketId).
|
||||||
if (game) {
|
* @param {object} actionData - Данные о действии.
|
||||||
game.processPlayerAction(socketId, actionData);
|
*/
|
||||||
|
handlePlayerAction(identifier, actionData) { // Теперь принимаем identifier
|
||||||
|
const gameId = this.userIdentifierToGameId[identifier]; // Находим ID игры по идентификатору пользователя
|
||||||
|
const game = this.games[gameId]; // Находим экземпляр игры
|
||||||
|
|
||||||
|
if (game && game.players) {
|
||||||
|
// Находим текущий сокет ID пользователя в списке игроков этой игры
|
||||||
|
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
||||||
|
const currentSocketId = playerInfo?.socket?.id;
|
||||||
|
|
||||||
|
if (playerInfo && currentSocketId) {
|
||||||
|
// Проверяем, что сокет с этим ID еще подключен.
|
||||||
|
// Это дополнительная проверка, чтобы не обрабатывать действия от "зомби"-сокетов
|
||||||
|
const actualSocket = this.io.sockets.sockets.get(currentSocketId);
|
||||||
|
|
||||||
|
if (actualSocket && actualSocket.connected) {
|
||||||
|
// Передаем действие экземпляру игры, используя ТЕКУЩИЙ Socket ID
|
||||||
|
game.processPlayerAction(currentSocketId, actionData); // processPlayerAction в GameInstance использует socketId
|
||||||
|
} else {
|
||||||
|
// Если сокет не найден или не подключен, это может быть старое действие от отключившегося сокета
|
||||||
|
console.warn(`[GameManager] Игрок ${identifier} отправил действие (${actionData?.actionType}), но его текущий сокет (${currentSocketId}) не найден или отключен.`);
|
||||||
|
// Не отправляем ошибку клиенту, так как он, вероятно, уже отключен или переподключается
|
||||||
|
// Клиент получит gameNotFound при следующем запросе состояния или gameError, если игра еще активна
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Игрок не найден в списке players этой игры по идентификатору
|
||||||
|
console.warn(`[GameManager] Игрок ${identifier} отправил действие (${actionData?.actionType}) для игры ${gameId}, но его запись не найдена в game.players.`);
|
||||||
|
// В таком случае, возможно, состояние userIdentifierToGameId некорректно.
|
||||||
|
// Удаляем некорректную ссылку.
|
||||||
|
delete this.userIdentifierToGameId[identifier];
|
||||||
|
// Оповещаем клиента, что игра не найдена (он должен будет запросить состояние)
|
||||||
|
const playerSocket = this.io.sockets.sockets.get(identifier); // Попробуем найти сокет по идентификатору (если он был socket.id)
|
||||||
|
if (!playerSocket && playerInfo?.socket) { // Если не нашли по identifier, попробуем по сокету из playerInfo
|
||||||
|
playerSocket = playerInfo.socket;
|
||||||
|
}
|
||||||
|
if (playerSocket) {
|
||||||
|
playerSocket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена или завершена.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const playerSocket = this.io.sockets.sockets.get(socketId);
|
// Если игра не найдена по userIdentifierToGameId[identifier]
|
||||||
if (playerSocket) playerSocket.emit('gameError', { message: 'Ошибка: игровая сессия потеряна для этого действия.' });
|
console.warn(`[GameManager] Игрок ${identifier} отправил действие (${actionData?.actionType}), но его игра (ID: ${gameId}) не найдена в GameManager.`);
|
||||||
|
// Удаляем некорректную ссылку
|
||||||
|
delete this.userIdentifierToGameId[identifier];
|
||||||
|
// Отправляем gameNotFound клиенту, если можем его найти (по identifier, если это socket.id)
|
||||||
|
const playerSocket = this.io.sockets.sockets.get(identifier);
|
||||||
|
if (playerSocket) {
|
||||||
|
playerSocket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена или завершена.' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDisconnect(socketId, userId = null) {
|
/**
|
||||||
const identifier = userId || socketId;
|
* Обрабатывает отключение сокета игрока.
|
||||||
const gameId = this.socketToGame[socketId];
|
* Вызывается из bc.js при событии 'disconnect'.
|
||||||
|
* @param {string} socketId - ID отключившегося сокета.
|
||||||
|
* @param {string|number} identifier - ID пользователя (userId или socketId).
|
||||||
|
*/
|
||||||
|
handleDisconnect(socketId, identifier) { // Принимаем и socketId, и identifier
|
||||||
|
// Ищем игру по идентификатору пользователя (более надежный способ после переподключения)
|
||||||
|
const gameId = this.userIdentifierToGameId[identifier];
|
||||||
|
const game = this.games[gameId];
|
||||||
|
|
||||||
if (gameId && this.games[gameId]) {
|
// Если игра найдена и в ней есть игрок с этим идентификатором (или сокетом)
|
||||||
const game = this.games[gameId];
|
if (game && game.players) {
|
||||||
const playerInfo = game.players[socketId];
|
// Находим информацию об игроке по идентификатору
|
||||||
const username = playerInfo?.socket?.userData?.username || socketId;
|
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
||||||
console.log(`[GameManager] Игрок ${username} (socket: ${socketId}, userId: ${userId}) отключился от игры ${gameId}.`);
|
|
||||||
game.removePlayer(socketId);
|
|
||||||
|
|
||||||
if (game.playerCount === 0) {
|
if (playerInfo) {
|
||||||
console.log(`[GameManager] Игра ${gameId} пуста и будет удалена (после дисконнекта).`);
|
console.log(`[GameManager] Игрок ${identifier} (сокет: ${socketId}) отключился. В игре ${gameId}.`);
|
||||||
delete this.games[gameId];
|
|
||||||
const gameIndexPending = this.pendingPvPGames.indexOf(gameId);
|
// Удаляем игрока из экземпляра игры, передавая Socket ID, который отключился
|
||||||
if (gameIndexPending > -1) this.pendingPvPGames.splice(gameIndexPending, 1);
|
// GameInstance.removePlayer принимает socketId
|
||||||
for (const key in this.userToPendingGame) {
|
game.removePlayer(socketId); // Передаем socketId для удаления конкретного сокета
|
||||||
if (this.userToPendingGame[key] === gameId) delete this.userToPendingGame[key];
|
|
||||||
|
// После удаления игрока из GameInstance, проверяем состояние игры и GameManager
|
||||||
|
if (game.playerCount === 0) {
|
||||||
|
// Если в игре больше нет игроков, удаляем ее из GameManager
|
||||||
|
console.log(`[GameManager] Игра ${gameId} пуста после дисконнекта ${socketId} (идентификатор ${identifier}). Удаляем.`);
|
||||||
|
// Используем централизованную функцию очистки
|
||||||
|
this._cleanupGame(gameId, 'player_count_zero_on_disconnect');
|
||||||
|
|
||||||
|
} else if (game.mode === 'pvp' && game.playerCount === 1 && (!game.gameState || !game.gameState.isGameOver)) {
|
||||||
|
// Если игра PvP, остался 1 игрок, и она еще не окончена (из-за дисконнекта),
|
||||||
|
// возвращаем ее в список ожидающих.
|
||||||
|
console.log(`[GameManager] Игра ${gameId} (PvP) теперь с 1 игроком после дисконнекта ${socketId} (идентификатор ${identifier}). Возвращаем в список ожидания.`);
|
||||||
|
if (!this.pendingPvPGames.includes(gameId)) {
|
||||||
|
this.pendingPvPGames.push(gameId);
|
||||||
|
}
|
||||||
|
// Удаляем ссылку на игру только для отключившегося идентификатора
|
||||||
|
delete this.userIdentifierToGameId[identifier];
|
||||||
|
|
||||||
|
// ownerIdentifier игры (если был userId) останется тем же, даже если отключился владелец.
|
||||||
|
// Это OK, ownerIdentifier используется для _removePreviousPendingGames.
|
||||||
|
|
||||||
|
this.broadcastAvailablePvPGames(); // Обновляем список у всех
|
||||||
|
} else if (game.gameState?.isGameOver) {
|
||||||
|
// Если игра была окончена (например, дисконнект приводил к gameOver),
|
||||||
|
// просто удаляем ссылку на игру для отключившегося идентификатора.
|
||||||
|
console.log(`[GameManager] Игрок ${identifier} отключился из завершенной игры ${gameId}. Удаляем ссылку.`);
|
||||||
|
delete this.userIdentifierToGameId[identifier];
|
||||||
|
} else {
|
||||||
|
// Игра не пуста и не вернулась в ожидание (например, AI игра, где остался игрок,
|
||||||
|
// или PvP игра с 2 игроками, где один отключился, а второй остался)
|
||||||
|
// Ссылка userIdentifierToGameId[identifier] для отключившегося игрока должна быть удалена.
|
||||||
|
console.log(`[GameManager] Игрок ${identifier} отключился из активной игры ${gameId} (mode: ${game.mode}, players: ${game.playerCount}). Удаляем ссылку.`);
|
||||||
|
delete this.userIdentifierToGameId[identifier];
|
||||||
}
|
}
|
||||||
this.broadcastAvailablePvPGames();
|
} else {
|
||||||
} else if (game.mode === 'pvp' && game.playerCount === 1 && (!game.gameState || !game.gameState.isGameOver)) {
|
// Игра найдена, но игрока с этим идентификатором или сокетом в game.players нет.
|
||||||
if (!this.pendingPvPGames.includes(gameId)) {
|
// Это может означать, что сокет отключился, но запись игрока была удалена раньше,
|
||||||
this.pendingPvPGames.push(gameId);
|
// или identifier некорректен.
|
||||||
}
|
console.warn(`[GameManager] Игрок с идентификатором ${identifier} (сокет: ${socketId}) не найден в game.players для игры ${gameId}.`);
|
||||||
const remainingPlayerSocketId = Object.keys(game.players)[0];
|
// Удаляем ссылку на игру для этого идентификатора, если она есть.
|
||||||
const remainingPlayerSocket = game.players[remainingPlayerSocketId]?.socket;
|
delete this.userIdentifierToGameId[identifier];
|
||||||
const remainingUserId = remainingPlayerSocket?.userData?.userId;
|
// Проверяем, возможно, этот сокет был в другой игре по старой ссылке socketToGame (удалено),
|
||||||
const newIdentifier = remainingUserId || remainingPlayerSocketId;
|
// или это просто отключившийся сокет без активной игры.
|
||||||
|
|
||||||
game.ownerUserId = remainingUserId;
|
|
||||||
this.userToPendingGame[newIdentifier] = gameId;
|
|
||||||
|
|
||||||
if (identifier !== newIdentifier && this.userToPendingGame[identifier] === gameId) {
|
|
||||||
delete this.userToPendingGame[identifier];
|
|
||||||
}
|
|
||||||
console.log(`[GameManager] Игра ${gameId} возвращена в список ожидания PvP. Новый владелец: ${newIdentifier}`);
|
|
||||||
this.broadcastAvailablePvPGames();
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const pendingGameIdToRemove = this.userToPendingGame[identifier];
|
// Если игра не найдена по userIdentifierToGameId[identifier]
|
||||||
if (pendingGameIdToRemove && this.games[pendingGameIdToRemove] && this.games[pendingGameIdToRemove].playerCount === 1) {
|
console.log(`[GameManager] Отключился сокет ${socketId} (идентификатор ${identifier}). Игровая сессия по этому идентификатору не найдена.`);
|
||||||
console.log(`[GameManager] Игрок ${socketId} (identifier: ${identifier}) отключился, удаляем его ожидающую игру ${pendingGameIdToRemove}`);
|
// Убеждаемся, что ссылка userIdentifierToGameId[identifier] удалена
|
||||||
delete this.games[pendingGameIdToRemove];
|
delete this.userIdentifierToGameId[identifier];
|
||||||
const idx = this.pendingPvPGames.indexOf(pendingGameIdToRemove);
|
|
||||||
if (idx > -1) this.pendingPvPGames.splice(idx, 1);
|
|
||||||
delete this.userToPendingGame[identifier];
|
|
||||||
this.broadcastAvailablePvPGames();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
delete this.socketToGame[socketId];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Централизованная функция для очистки игры после ее завершения.
|
||||||
|
* Удаляет экземпляр игры и все связанные с ней ссылки.
|
||||||
|
* Вызывается из GameInstance при gameOver (по HP или дисконнекту).
|
||||||
|
* @param {string} gameId - ID завершенной игры.
|
||||||
|
* @param {string} reason - Причина завершения (для логирования).
|
||||||
|
* @returns {boolean} true, если игра найдена и очищена, иначе false.
|
||||||
|
*/
|
||||||
|
_cleanupGame(gameId, reason = 'unknown_reason') { // <-- НОВЫЙ ПРИВАТНЫЙ МЕТОД
|
||||||
|
const game = this.games[gameId];
|
||||||
|
if (!game) {
|
||||||
|
console.warn(`[GameManager] _cleanupGame called for unknown game ID: ${gameId}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[GameManager] Cleaning up game ${gameId} (Mode: ${game.mode}, Reason: ${reason})...`);
|
||||||
|
|
||||||
|
// Удаляем ссылку userIdentifierToGameId для всех игроков, которые были в этой игре
|
||||||
|
// Перебираем players в GameInstance, чтобы получить идентификаторы
|
||||||
|
Object.values(game.players).forEach(playerInfo => {
|
||||||
|
if (playerInfo && playerInfo.identifier && this.userIdentifierToGameId[playerInfo.identifier] === gameId) {
|
||||||
|
delete this.userIdentifierToGameId[playerInfo.identifier];
|
||||||
|
console.log(`[GameManager] Removed userIdentifierToGameId for ${playerInfo.identifier}.`);
|
||||||
|
} else if (playerInfo && playerInfo.identifier) {
|
||||||
|
console.warn(`[GameManager] User ${playerInfo.identifier} in game ${gameId} has incorrect userIdentifierToGameId reference.`);
|
||||||
|
// Если ссылка некорректна, ничего не удаляем.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Удаляем ID игры из списка ожидающих, если она там была
|
||||||
|
const pendingIndex = this.pendingPvPGames.indexOf(gameId);
|
||||||
|
if (pendingIndex > -1) {
|
||||||
|
this.pendingPvPGames.splice(pendingIndex, 1);
|
||||||
|
console.log(`[GameManager] Removed game ${gameId} from pendingPvPGames.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаляем сам экземпляр игры
|
||||||
|
delete this.games[gameId];
|
||||||
|
console.log(`[GameManager] Deleted GameInstance for game ${gameId}.`);
|
||||||
|
|
||||||
|
// Оповещаем клиентов об обновленном списке игр (может понадобиться, если удалена ожидающая игра)
|
||||||
|
// Или если активная игра была удалена, и игроки вернутся в лобби.
|
||||||
|
this.broadcastAvailablePvPGames();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Формирует список доступных для присоединения PvP игр для клиента.
|
||||||
|
* @returns {Array<object>} Массив объектов с информацией об играх.
|
||||||
|
*/
|
||||||
getAvailablePvPGamesListForClient() {
|
getAvailablePvPGamesListForClient() {
|
||||||
return this.pendingPvPGames
|
return this.pendingPvPGames
|
||||||
.map(gameId => {
|
.map(gameId => {
|
||||||
const game = this.games[gameId];
|
const game = this.games[gameId];
|
||||||
if (game && game.mode === 'pvp' && game.playerCount === 1 && (!game.gameState || !game.gameState.isGameOver)) {
|
// Проверяем, что игра существует, это PvP, в ней 1 игрок, и она не окончена
|
||||||
|
// gameState.isGameOver проверяется, чтобы исключить игры, которые могли завершиться сразу (очень маловероятно)
|
||||||
|
if (game && game.mode === 'pvp' && game.playerCount === 1 && game.gameState && !game.gameState.isGameOver) {
|
||||||
let firstPlayerUsername = 'Игрок';
|
let firstPlayerUsername = 'Игрок';
|
||||||
let firstPlayerCharacterName = '';
|
let firstPlayerCharacterName = '';
|
||||||
|
|
||||||
if (game.players && Object.keys(game.players).length > 0) {
|
// Находим информацию о первом игроке (он всегда в слоте GAME_CONFIG.PLAYER_ID в ожидающей игре)
|
||||||
const firstPlayerSocketId = Object.keys(game.players)[0];
|
const firstPlayerInfo = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||||||
const firstPlayerInfo = game.players[firstPlayerSocketId];
|
|
||||||
|
|
||||||
if (firstPlayerInfo) {
|
if (firstPlayerInfo) {
|
||||||
if (firstPlayerInfo.socket?.userData?.username) {
|
// Получаем имя пользователя из userData, если залогинен
|
||||||
firstPlayerUsername = firstPlayerInfo.socket.userData.username;
|
if (firstPlayerInfo.socket?.userData?.username) {
|
||||||
}
|
firstPlayerUsername = firstPlayerInfo.socket.userData.username;
|
||||||
|
} else {
|
||||||
const charKey = firstPlayerInfo.chosenCharacterKey;
|
// Если нет userData.username, используем часть identifier
|
||||||
if (charKey) {
|
firstPlayerUsername = `User#${String(firstPlayerInfo.identifier).substring(0,6)}`; // Приводим identifier к строке
|
||||||
let charBaseStats;
|
|
||||||
if (charKey === 'elena') {
|
|
||||||
charBaseStats = gameData.playerBaseStats;
|
|
||||||
} else if (charKey === 'almagest') {
|
|
||||||
charBaseStats = gameData.almagestBaseStats;
|
|
||||||
}
|
|
||||||
// Баларда не должно быть в pending PvP как создателя
|
|
||||||
|
|
||||||
if (charBaseStats && charBaseStats.name) {
|
|
||||||
firstPlayerCharacterName = charBaseStats.name;
|
|
||||||
} else {
|
|
||||||
console.warn(`[GameManager] getAvailablePvPGamesList: Не удалось найти имя для charKey '${charKey}' в gameData.`);
|
|
||||||
firstPlayerCharacterName = charKey; // В крайнем случае ключ
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn(`[GameManager] getAvailablePvPGamesList: firstPlayerInfo.chosenCharacterKey отсутствует для игры ${gameId}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Получаем имя персонажа из chosenCharacterKey
|
||||||
|
const charKey = firstPlayerInfo.chosenCharacterKey;
|
||||||
|
if (charKey) {
|
||||||
|
// Используем _getCharacterBaseData напрямую, т.к. gameData доступен
|
||||||
|
const charBaseStats = this._getCharacterBaseData(charKey);
|
||||||
|
if (charBaseStats && charBaseStats.name) {
|
||||||
|
firstPlayerCharacterName = charBaseStats.name;
|
||||||
|
} else {
|
||||||
|
//console.warn(`[GameManager] getAvailablePvPGamesList: Не удалось найти имя для charKey '${charKey}' в gameData.`);
|
||||||
|
firstPlayerCharacterName = charKey; // В крайнем случае используем ключ
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//console.warn(`[GameManager] getAvailablePvPGamesList: firstPlayerInfo.chosenCharacterKey отсутствует для игры ${gameId}.`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`[GameManager] getAvailablePvPGamesList: firstPlayerInfo (Player 1) не найдена для ожидающей игры ${gameId}.`);
|
||||||
|
firstPlayerUsername = 'Неизвестный игрок'; // Если даже игрока не нашли в players
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Формируем строку статуса для отображения в списке
|
||||||
let statusString = `Ожидает 1 игрока (Создал: ${firstPlayerUsername}`;
|
let statusString = `Ожидает 1 игрока (Создал: ${firstPlayerUsername}`;
|
||||||
if (firstPlayerCharacterName) {
|
if (firstPlayerCharacterName) {
|
||||||
statusString += ` за ${firstPlayerCharacterName}`;
|
statusString += ` за ${firstPlayerCharacterName}`;
|
||||||
@ -272,40 +522,271 @@ class GameManager {
|
|||||||
statusString += `)`;
|
statusString += `)`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: gameId,
|
id: gameId, // Отправляем полный ID, но в списке UI показываем обрезанный
|
||||||
status: statusString
|
status: statusString
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return null;
|
// Если игра не соответствует критериям ожидающей (например, пуста, заполнена, окончена), не включаем ее
|
||||||
|
if (game && !this.pendingPvPGames.includes(gameId)) {
|
||||||
|
// Если игра есть, но не в pendingPvPGames, она не должна тут обрабатываться.
|
||||||
|
} else if (game && game.playerCount === 1 && (game.gameState?.isGameOver || !game.gameState)) {
|
||||||
|
// Игра с 1 игроком, но окончена или не инициализирована - не показывать
|
||||||
|
} else if (game && game.playerCount === 2) {
|
||||||
|
// Игра заполнена - не показывать
|
||||||
|
} else if (game && game.playerCount === 0) {
|
||||||
|
// Игра пуста - ее надо было удалить при дисконнекте последнего игрока.
|
||||||
|
// Возможно, тут нужна очистка таких "потерянных" игр.
|
||||||
|
console.warn(`[GameManager] getAvailablePvPGamesList: Найдена пустая игра ${gameId} в games. Удаляем.`);
|
||||||
|
delete this.games[gameId]; // Удаляем потерянную игру
|
||||||
|
// Очистка из pendingPvPGames не нужна, т.к. она удаляется при playerCount === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // Исключаем игры, не соответствующие критериям или удаленные
|
||||||
})
|
})
|
||||||
.filter(info => info !== null);
|
.filter(info => info !== null); // Удаляем null из результатов map
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправляет обновленный список доступных PvP игр всем подключенным клиентам.
|
||||||
|
*/
|
||||||
broadcastAvailablePvPGames() {
|
broadcastAvailablePvPGames() {
|
||||||
this.io.emit('availablePvPGamesList', this.getAvailablePvPGamesListForClient());
|
const availableGames = this.getAvailablePvPGamesListForClient();
|
||||||
|
this.io.emit('availablePvPGamesList', availableGames);
|
||||||
|
console.log(`[GameManager] Обновлен список доступных PvP игр. Всего: ${availableGames.length}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает список активных игр для отладки на сервере.
|
||||||
|
* @returns {Array<object>} Список объектов с краткой информацией об играх.
|
||||||
|
*/
|
||||||
getActiveGamesList() { // Для отладки на сервере
|
getActiveGamesList() { // Для отладки на сервере
|
||||||
return Object.values(this.games).map(game => {
|
return Object.values(this.games).map(game => {
|
||||||
let playerSlotChar = game.gameState?.player?.name || (game.playerCharacterKey ? gameData[game.playerCharacterKey === 'elena' ? 'playerBaseStats' : (game.playerCharacterKey === 'almagest' ? 'almagestBaseStats' : null)]?.name : 'N/A');
|
// Получаем имена персонажей из gameState, если игра инициализирована, иначе из chosenCharacterKey/default
|
||||||
let opponentSlotChar = game.gameState?.opponent?.name || (game.opponentCharacterKey ? gameData[game.opponentCharacterKey === 'elena' ? 'playerBaseStats' : (game.opponentCharacterKey === 'almagest' ? 'almagestBaseStats' : (game.opponentCharacterKey === 'balard' ? 'opponentBaseStats' : null))]?.name : 'N/A');
|
let playerSlotCharName = game.gameState?.player?.name || (game.playerCharacterKey ? this._getCharacterBaseData(game.playerCharacterKey)?.name : 'N/A (ожидание)');
|
||||||
|
let opponentSlotCharName = game.gameState?.opponent?.name || (game.opponentCharacterKey ? this._getCharacterBaseData(game.opponentCharacterKey)?.name : 'N/A (ожидание)');
|
||||||
|
|
||||||
if (game.mode === 'pvp' && game.playerCount === 1 && !game.opponentCharacterKey && game.gameState && !game.gameState.isGameOver) {
|
// Проверяем наличие игроков в слотах, чтобы уточнить статус
|
||||||
opponentSlotChar = 'Ожидание...';
|
const playerInSlot1 = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID);
|
||||||
}
|
const playerInSlot2 = Object.values(game.players).find(p => p.id === GAME_CONFIG.OPPONENT_ID);
|
||||||
|
|
||||||
|
if (!playerInSlot1) playerSlotCharName = 'Пусто';
|
||||||
|
if (!playerInSlot2 && game.mode === 'pvp') opponentSlotCharName = 'Ожидание...'; // В PvP слоты могут быть пустыми
|
||||||
|
if (!playerInSlot2 && game.mode === 'ai' && game.aiOpponent) opponentSlotCharName = 'Балард (AI)'; // В AI слоте оппонента всегда AI
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: game.id.substring(0,8),
|
id: game.id.substring(0,8), // Обрезанный ID для удобства
|
||||||
mode: game.mode,
|
mode: game.mode,
|
||||||
playerCount: game.playerCount,
|
playerCount: game.playerCount,
|
||||||
isGameOver: game.gameState ? game.gameState.isGameOver : 'N/A',
|
isGameOver: game.gameState ? game.gameState.isGameOver : 'N/A (Не инициализирована)',
|
||||||
playerSlot: playerSlotChar,
|
playerSlot: playerSlotCharName,
|
||||||
opponentSlot: opponentSlotChar,
|
opponentSlot: opponentSlotCharName,
|
||||||
ownerUserId: game.ownerUserId || 'N/A',
|
ownerIdentifier: game.ownerIdentifier || 'N/A',
|
||||||
pending: this.pendingPvPGames.includes(game.id)
|
pending: this.pendingPvPGames.includes(game.id),
|
||||||
|
turn: game.gameState ? `Ход ${game.gameState.turnNumber}, ${game.gameState.isPlayerTurn ? (playerInSlot1?.identifier || 'Player Slot') : (playerInSlot2?.identifier || 'Opponent Slot')}` : 'N/A'
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обрабатывает запрос клиента на gameState (например, при переподключении).
|
||||||
|
* Находит игру пользователя по его идентификатору и отправляет ему актуальное состояние.
|
||||||
|
* Также обновляет ссылку на сокет в GameInstance.
|
||||||
|
* @param {object} socket - Сокет клиента, запросившего состояние.
|
||||||
|
* @param {string|number} identifier - ID пользователя (userId или socketId).
|
||||||
|
*/
|
||||||
|
handleRequestGameState(socket, identifier) { // Принимаем socket и identifier
|
||||||
|
// Ищем игру пользователя по его идентификатору
|
||||||
|
const gameId = this.userIdentifierToGameId[identifier];
|
||||||
|
let game = null;
|
||||||
|
|
||||||
|
if (gameId) {
|
||||||
|
game = this.games[gameId];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если игра найдена и она существует, и в ней есть игрок с этим идентификатором
|
||||||
|
if (game && game.players) {
|
||||||
|
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
|
||||||
|
|
||||||
|
if (playerInfo) {
|
||||||
|
// Проверяем, если игра окончена, не восстанавливаем состояние, а информируем
|
||||||
|
if (game.gameState?.isGameOver) {
|
||||||
|
console.log(`[GameManager] Reconnected user ${identifier} to game ${gameId} which is already over. Sending gameNotFound.`);
|
||||||
|
// Удаляем ссылку на оконченную игру для этого пользователя
|
||||||
|
delete this.userIdentifierToGameId[identifier];
|
||||||
|
// Отправляем gameNotFound, чтобы клиент вернулся в меню
|
||||||
|
socket.emit('gameNotFound', { message: 'Ваша предыдущая игровая сессия уже завершена.' });
|
||||||
|
return; // Прекращаем обработку
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
console.log(`[GameManager] Found game ${gameId} for identifier ${identifier} (role ${playerInfo.id}). Reconnecting socket ${socket.id}.`);
|
||||||
|
|
||||||
|
// --- Обновляем GameInstance: заменяем старый сокет на новый для этого игрока ---
|
||||||
|
// Удаляем старую запись игрока по старому socket.id, если она есть и отличается
|
||||||
|
const oldSocketId = playerInfo.socket?.id;
|
||||||
|
if (oldSocketId && oldSocketId !== socket.id && game.players[oldSocketId]) {
|
||||||
|
console.log(`[GameManager] Updating socket ID for player ${identifier} from ${oldSocketId} to ${socket.id} in game ${gameId}.`);
|
||||||
|
delete game.players[oldSocketId]; // Удаляем запись по старому socketId
|
||||||
|
// playerCount не уменьшаем/увеличиваем, т.к. это тот же игрок, просто сменил сокет
|
||||||
|
// Удаляем ссылку на старый сокет по роли
|
||||||
|
if (game.playerSockets[playerInfo.id]?.id === oldSocketId) {
|
||||||
|
delete game.playerSockets[playerInfo.id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем или обновляем запись для нового сокета, связывая его с существующим идентификатором игрока
|
||||||
|
game.players[socket.id] = playerInfo; // Переиспользуем существующий объект playerInfo
|
||||||
|
game.players[socket.id].socket = socket; // Обновляем объект сокета
|
||||||
|
// Ensure the identifier and role are correct on the new socket entry
|
||||||
|
game.players[socket.id].identifier = identifier; // Make sure identifier is set (уже должно быть, но на всякий случай)
|
||||||
|
// playerInfo.id should already be correct (player/opponent role)
|
||||||
|
|
||||||
|
game.playerSockets[playerInfo.id] = socket; // Обновляем ссылку на сокет по роли
|
||||||
|
|
||||||
|
// Убеждаемся, что новый socket.id теперь связан с этой игрой в GameManager - НЕ НУЖНО, socketToGame удален
|
||||||
|
// this.socketToGame[socket.id] = game.id;
|
||||||
|
|
||||||
|
|
||||||
|
// Присоединяем новый сокет к комнате Socket.IO
|
||||||
|
socket.join(game.id);
|
||||||
|
// --- КОНЕЦ Обновления сокета ---
|
||||||
|
|
||||||
|
|
||||||
|
// Получаем данные персонажей с точки зрения этого клиента
|
||||||
|
// playerInfo.chosenCharacterKey - это персонаж этого клиента
|
||||||
|
const playerCharDataForClient = this._getCharacterData(playerInfo.chosenCharacterKey);
|
||||||
|
// Определяем ключ персонажа оппонента с точки зрения этого клиента
|
||||||
|
const opponentActualSlotId = playerInfo.id === GAME_CONFIG.PLAYER_ID ? GAME_CONFIG.OPPONENT_ID : GAME_CONFIG.PLAYER_ID;
|
||||||
|
const opponentCharacterKeyForClient = game.gameState?.[opponentActualSlotId]?.characterKey || null; // Берем из gameState, т.к. там актуальное состояние слотов
|
||||||
|
// Если оппонент еще не определен в gameState (PvP ожидание), используем playerCharacterKey/opponentCharacterKey из gameInstance
|
||||||
|
// ВАЖНО: при переподключении к *активной* игре, gameState.opponent.characterKey ДОЛЖЕН БЫТЬ определен.
|
||||||
|
// Если он null, это может быть PvP ожидание или некорректное состояние.
|
||||||
|
if (!opponentCharacterKeyForClient) {
|
||||||
|
// Попробуем найти ключ из GameInstance properties (они устанавливаются при инициализации)
|
||||||
|
const opponentSlotKeyInInstance = playerInfo.id === GAME_CONFIG.PLAYER_ID ? game.playerCharacterKey : game.opponentCharacterKey; // ИСПРАВЛЕНО: Логика получения ключа оппонента
|
||||||
|
opponentCharacterKeyForClient = opponentSlotKeyInInstance;
|
||||||
|
// Если даже из GameInstance properties ключ null, это точно PvP ожидание или критическая ошибка
|
||||||
|
}
|
||||||
|
const opponentCharDataForClient = this._getCharacterData(opponentCharacterKeyForClient); // Данные оппонента с т.з. клиента
|
||||||
|
|
||||||
|
|
||||||
|
if (playerCharDataForClient && opponentCharDataForClient && game.gameState) {
|
||||||
|
// Проверяем, готово ли gameState к игре (определены оба бойца)
|
||||||
|
const isGameReadyForPlay = (game.mode === 'ai' && game.playerCount === 1) || (game.mode === 'pvp' && game.playerCount === 2);
|
||||||
|
const isOpponentDefinedInState = game.gameState.opponent?.characterKey && game.gameState.opponent?.name !== 'Ожидание игрока...';
|
||||||
|
|
||||||
|
|
||||||
|
socket.emit('gameState', {
|
||||||
|
gameId: game.id,
|
||||||
|
yourPlayerId: playerInfo.id, // ID слота этого клиента в игре
|
||||||
|
gameState: game.gameState,
|
||||||
|
playerBaseStats: playerCharDataForClient.baseStats, // Статы "моего" персонажа для клиента
|
||||||
|
opponentBaseStats: opponentCharDataForClient.baseStats, // Статы "моего" оппонента для клиента
|
||||||
|
playerAbilities: playerCharDataForClient.abilities, // Абилки "моего" персонажа для клиента
|
||||||
|
opponentAbilities: opponentCharDataForClient.abilities, // Абилки "моего" оппонента для клиента
|
||||||
|
log: game.consumeLogBuffer(), // Отправляем текущий лог и очищаем буфер игры
|
||||||
|
clientConfig: { ...GAME_CONFIG } // Отправляем копию конфига
|
||||||
|
});
|
||||||
|
console.log(`[GameManager] Sent gameState to socket ${socket.id} (identifier: ${identifier}) for game ${game.id}.`);
|
||||||
|
|
||||||
|
// Логика старта игры при переподключении (если она еще не началась)
|
||||||
|
// Эта логика должна быть только для случая, когда переподключившийся игрок ЗАВЕРШАЕТ состав игры
|
||||||
|
// (например, второй игрок в PvP переподключился к ожидающей игре).
|
||||||
|
// Если игра уже началась, startGame не должен вызываться повторно.
|
||||||
|
// Проверяем: игра не окончена, готова к игре (2 игрока или AI), и состояние оппонента НЕ БЫЛО определено до этого запроса (признак не полностью стартовавшей игры)
|
||||||
|
if (!game.gameState.isGameOver && isGameReadyForPlay && !isOpponentDefinedInState) {
|
||||||
|
console.log(`[GameManager] Game ${game.id} found ready but not fully started on reconnect (Opponent state missing). Initializing/Starting.`);
|
||||||
|
// Инициализируем состояние игры. initializeGame вернет true, если оба бойца определены.
|
||||||
|
const isInitialized = game.initializeGame(); // Переинициализируем state полностью с обоими персонажами
|
||||||
|
if (isInitialized) { // Проверяем, успешно ли инициализировалось состояние
|
||||||
|
game.startGame(); // Запускаем игру (это отправит gameStarted всем, включая этого клиента)
|
||||||
|
} else {
|
||||||
|
console.error(`[GameManager] Failed to initialize game ${game.id} on reconnect. Cannot start.`);
|
||||||
|
// Дополнительная обработка ошибки, возможно, уведомить игроков
|
||||||
|
this.io.to(game.id).emit('gameError', { message: 'Ошибка сервера при старте игры после переподключения. Не удалось инициализировать игру.' });
|
||||||
|
// Если инициализация провалилась, игра в некорректном состоянии, нужно ее удалить
|
||||||
|
this._cleanupGame(gameId, 'reconnect_initialization_failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Если игра уже активно идет (не окончена, не ожидание) и состояние оппонента БЫЛО определено,
|
||||||
|
// то startGame не вызывается повторно. Клиент получит gameStateUpdate от обычного хода игры.
|
||||||
|
// Если игра PvP ожидающая (1 игрок), startGame не вызывается, isGameReadyForPlay будет false.
|
||||||
|
else if (!isGameReadyForPlay) {
|
||||||
|
console.log(`[GameManager] Reconnected user ${identifier} to pending game ${gameId}. Sending gameState and waiting status.`);
|
||||||
|
// Если это ожидающая игра, убедимся, что клиент получает статус ожидания
|
||||||
|
socket.emit('waitingForOpponent');
|
||||||
|
} else if (game.gameState.isGameOver) {
|
||||||
|
console.log(`[GameManager] Reconnected to game ${gameId} which is already over. Sending gameNotFound.`);
|
||||||
|
// Если игра окончена, client.js должен по gameState.isGameOver показать модалку.
|
||||||
|
// Но чтобы гарантировать возврат в меню при последующих запросах, лучше отправить gameNotFound.
|
||||||
|
// Удаляем ссылку на оконченную игру для этого пользователя
|
||||||
|
delete this.userIdentifierToGameId[identifier];
|
||||||
|
// Отправляем gameNotFound
|
||||||
|
socket.emit('gameNotFound', { message: 'Ваша предыдущая игровая сессия уже завершена.' });
|
||||||
|
} else {
|
||||||
|
// Переподключение к активной игре, которая уже полностью стартовала.
|
||||||
|
console.log(`[GameManager] Reconnected user ${identifier} to active game ${gameId}. gameState sent.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.error(`[GameManager] Failed to send gameState to ${socket.id} (identifier ${identifier}) for game ${gameId}: missing character data or gameState.`);
|
||||||
|
socket.emit('gameError', { message: 'Ошибка сервера при восстановлении состояния игры.' });
|
||||||
|
// Если данные для отправки некорректны, игра в некорректном состоянии, нужно ее удалить
|
||||||
|
this._cleanupGame(gameId, 'reconnect_send_failed');
|
||||||
|
socket.emit('gameNotFound', { message: 'Ваша игровая сессия в некорректном состоянии и была завершена.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Игра найдена по идентификатору пользователя, но игрока с этим идентификатором нет в players этой игры.
|
||||||
|
// Это очень странная ситуация, возможно, state userIdentifierToGameId некорректен.
|
||||||
|
console.warn(`[GameManager] Found game ${gameId} by identifier ${identifier}, but player with this identifier not found in game.players.`);
|
||||||
|
// Удаляем некорректную ссылку и отправляем gameNotFound
|
||||||
|
delete this.userIdentifierToGameId[identifier];
|
||||||
|
socket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена. Возможно, идентификатор пользователя некорректен.' });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Игра не найдена по userIdentifierToGameId[identifier]
|
||||||
|
console.log(`[GameManager] No active or pending game found for identifier ${identifier}.`);
|
||||||
|
socket.emit('gameNotFound', { message: 'Игровая сессия не найдена.' }); // Уведомляем клиента, что игра не найдена
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Вспомогательные функции для получения данных персонажа из data.js ---
|
||||||
|
// Скопировано из gameInstance.js, т.к. gameManager тоже использует gameData напрямую
|
||||||
|
/**
|
||||||
|
* Получает базовые статы и список способностей для персонажа по ключу.
|
||||||
|
* Эти функции предназначены для использования ВНУТРИ GameManager или GameInstance.
|
||||||
|
* @param {string} key - Ключ персонажа ('elena', 'balard', 'almagest').
|
||||||
|
* @returns {{baseStats: object, abilities: array}|null} Объект с базовыми статами и способностями, или null.
|
||||||
|
*/
|
||||||
|
_getCharacterData(key) {
|
||||||
|
if (!key) { console.warn("GameManager::_getCharacterData called with null/undefined key."); return null; }
|
||||||
|
switch (key) {
|
||||||
|
case 'elena': return { baseStats: gameData.playerBaseStats, abilities: gameData.playerAbilities };
|
||||||
|
case 'balard': return { baseStats: gameData.opponentBaseStats, abilities: gameData.opponentAbilities }; // Балард использует opponentAbilities из data.js
|
||||||
|
case 'almagest': return { baseStats: gameData.almagestBaseStats, abilities: gameData.almagestAbilities }; // Альмагест использует almagestAbilities из data.js
|
||||||
|
default: console.error(`GameManager::_getCharacterData: Unknown character key "${key}"`); return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Получает только базовые статы для персонажа по ключу.
|
||||||
|
* @param {string} key - Ключ персонажа.
|
||||||
|
* @returns {object|null} Базовые статы или null.
|
||||||
|
*/
|
||||||
|
_getCharacterBaseData(key) {
|
||||||
|
const charData = this._getCharacterData(key);
|
||||||
|
return charData ? charData.baseStats : null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Получает только список способностей для персонажа по ключу.
|
||||||
|
* @param {string} key - Ключ персонажа.
|
||||||
|
* @returns {array|null} Список способностей или null.
|
||||||
|
*/
|
||||||
|
_getCharacterAbilities(key) {
|
||||||
|
const charData = this._getCharacterData(key);
|
||||||
|
return charData ? charData.abilities : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = GameManager;
|
module.exports = GameManager;
|
Loading…
x
Reference in New Issue
Block a user