This commit is contained in:
2026-01-26 23:33:33 +03:00
parent 937c39b73f
commit a597cb9de7
5 changed files with 732 additions and 57 deletions

View File

@ -9,7 +9,7 @@ module.exports = {
health: 5,
type: 'minion',
faction: 'rebellion',
text: 'Jedi. Hope of the Rebellion.',
text: 'Jedi. Hope of the Rebellion. Даёт +1/+1 повстанцам и Лее.',
art: 'luke',
legendary: true,
battlecry: 'Нанеси 1 урона вражескому герою.',
@ -23,7 +23,7 @@ module.exports = {
health: 8,
type: 'minion',
faction: 'empire',
text: 'Sith Lord. Fear is his weapon.',
text: 'Sith Lord. Fear is his weapon. Даёт +1/+1 штурмовикам и клонам.',
art: 'vader',
legendary: true,
deathrattle: 'Нанеси 2 урона всем врагам.',
@ -37,7 +37,7 @@ module.exports = {
health: 7,
type: 'minion',
faction: 'rebellion',
text: 'Jedi Master. Size matters not.',
text: 'Jedi Master. Size matters not. Даёт +1/+1 джедаям.',
art: 'yoda',
legendary: true,
battlecry: 'Дайте союзному миньону +1/+1.',
@ -51,7 +51,7 @@ module.exports = {
health: 4,
type: 'minion',
faction: 'rebellion',
text: 'Leader. Diplomat. Hero.',
text: 'Leader. Diplomat. Hero. Даёт +1/+1 Люку.',
art: 'leia',
legendary: true,
bio: 'Лея Органа — принцесса Алдераана, сенатор Галактической Республики, дочь Энакина Скайуокера и Падме Амидалы, сестра-близнец Люка. Воспитана Бейлом и Брехой Органа. Лидер Альянса повстанцев, участвовала в уничтожении обеих «Звёзд Смерти». Обладала чувствительностью к Силе, обучалась у Люка. Генерал Сопротивления, мать Бена Соло (Кайло Рена). Погибла, использовав последние силы Силы для связи с сыном через галактику.',
@ -63,7 +63,7 @@ module.exports = {
health: 4,
type: 'minion',
faction: 'rebellion',
text: 'Smuggler. Shoot first.',
text: 'Smuggler. Shoot first. Даёт +1/+1 Чубакке.',
art: 'han',
legendary: true,
bio: 'Хан Соло — контрабандист и пилот, владелец корабля «Тысячелетний сокол». Родился на Кореллии, служил в Имперском флоте, затем стал контрабандистом. Встретил Люка и Лею, присоединился к Альянсу повстанцев. Участвовал в уничтожении обеих «Звёзд Смерти». Муж Леи, отец Бена Соло. Был заморожен в карбоните Джаббой Хаттом, но выжил. Позже убит собственным сыном Кайло Реном, пытаясь вернуть его к свету. Его знаменитая фраза: «Стреляй первым».',
@ -75,7 +75,7 @@ module.exports = {
health: 5,
type: 'minion',
faction: 'rebellion',
text: 'Wookiee. Loyal friend.',
text: 'Wookiee. Loyal friend. Даёт +1/+1 Хану Соло.',
art: 'chewie',
bio: 'Чубакка — вуки с планеты Кашиик, первый помощник и лучший друг Хана Соло. Пережил рабство и Имперскую оккупацию родной планеты. Сражался в Войнах клонов и Гражданской войне. Долг жизни Люку Скайуокеру за спасение от рабства. Пилот «Тысячелетнего сокола», мастер-механик. Один из немногих, кто понимал язык вуки. Пережил все три трилогии, стал генералом Сопротивления.',
},
@ -86,7 +86,7 @@ module.exports = {
health: 3,
type: 'minion',
faction: 'neutral',
text: 'Astromech. Beep boop.',
text: 'Astromech. Beep boop. Даёт +1/+1 C-3PO.',
art: 'r2d2',
legendary: true,
deathrattle: 'Возьми карту.',
@ -100,7 +100,7 @@ module.exports = {
health: 4,
type: 'minion',
faction: 'neutral',
text: 'Protocol droid. Terrible at odds.',
text: 'Protocol droid. Terrible at odds. Даёт +1/+1 R2-D2.',
art: 'c3po',
bio: 'C-3PO — протокольный дроид, созданный Энакином Скайуокером в детстве на Татуине из запчастей. Владеет более чем шестью миллионами форм общения. Служил Падме Амидале, затем Лее Органе. Вечный спутник и лучший друг R2-D2, хотя часто жалуется на его безрассудство. Труслив, но предан друзьям. Участвовал во всех важных событиях галактики. Его золотая обшивка стала символом надежды для повстанцев.',
},
@ -111,7 +111,7 @@ module.exports = {
health: 3,
type: 'minion',
faction: 'empire',
text: 'Bounty Hunter. No disintegrations.',
text: 'Bounty Hunter. No disintegrations. Даёт +1/+1 Джанго Фетту.',
art: 'boba',
legendary: true,
bio: 'Боба Фетт — охотник за головами, генетическая копия (не клон) Джанго Фетта. Воспитан как сын Джанго, унаследовал его доспехи и корабль «Раб I». Один из самых опасных охотников за головами в галактике. Работал на Джаббу Хатта и Дарта Вейдера. Был проглочен сарлаком, но выжил благодаря доспехам. Позже стал лидером мандалорцев, приняв тёмный меч. Мастер тактики и выживания, уважаемый во всей галактике.',
@ -123,7 +123,7 @@ module.exports = {
health: 6,
type: 'minion',
faction: 'empire',
text: 'Sith. Unlimited power!',
text: 'Sith. Unlimited power! Даёт +1/+1 ситхам и имперцам.',
art: 'palpatine',
legendary: true,
battlecry: 'Нанеси 2 урона вражескому герою.',
@ -167,7 +167,7 @@ module.exports = {
health: 6,
type: 'minion',
faction: 'rebellion',
text: 'Jedi. Hello there.',
text: 'Jedi. Hello there. Даёт +1/+1 Энакину.',
art: 'obiwan',
legendary: true,
bio: 'Оби-Ван Кеноби — мастер-джедай, наставник Энакина Скайуокера и Люка Скайуокера. Ученик Квай-Гона Джинна, затем мастер Энакина. Участвовал в Войнах клонов, сражался с Дартом Молом, графом Дуку и генералом Гривусом. Победил Энакина на Мустафаре, но не смог убить его. Скрывался на Татуине, наблюдая за Люком. Погиб на «Звезде Смерти», став духом Силы и продолжив наставлять Люка. Его знаменитая фраза: «Привет там!» Его мудрость и преданность кодексу джедаев сделали его легендой.',
@ -179,7 +179,7 @@ module.exports = {
health: 3,
type: 'minion',
faction: 'empire',
text: 'Sith. Double-bladed saber.',
text: 'Sith. Double-bladed saber. Даёт +1/+1 Сэвиджу Оппрессу.',
art: 'maul',
legendary: true,
bio: 'Дарт Мол — тёмный лорд ситхов, зачак, ученик Дарта Сидиуса. Был известен своим двойным световым мечом и агрессивным стилем боя. Убил Квай-Гона Джинна на Набу, но был побеждён Оби-Ваном Кеноби и считался мёртвым. Выжил благодаря ненависти и был спасён братом Сэвиджем Оппрессом. Получил кибернетические ноги. Стал лидером преступного синдиката «Теней». Был убит Оби-Ваном на Татуине, защищая Люка. Его ненависть к Оби-Вану длилась десятилетия.',
@ -366,7 +366,7 @@ module.exports = {
health: 4,
type: 'minion',
faction: 'rebellion',
text: 'Senator. Fighter for peace.',
text: 'Senator. Fighter for peace. Даёт +1/+1 Энакину.',
art: 'padme',
legendary: true,
bio: 'Падме Амидала — королева, затем сенатор Набу. Была избрана королевой в 14 лет. Встретила Энакина Скайуокера, когда он был мальчиком, затем вышла за него замуж тайно. Мать Люка и Леи. Была известна своей дипломатией и борьбой за мир. Участвовала в создании Альянса повстанцев. Умерла от разбитого сердца после того, как Энакин пал на тёмную сторону. Её смерть стала ключевым моментом в превращении Энакина в Вейдера. Её преданность демократии вдохновила многих.',
@ -415,7 +415,7 @@ module.exports = {
health: 3,
type: 'minion',
faction: 'empire',
text: 'Bounty hunter. Clone template.',
text: 'Bounty hunter. Clone template. Даёт +1/+1 Бобе Фетту.',
art: 'jango',
legendary: true,
bio: 'Джанго Фетт — мандалорец, охотник за головами, один из лучших в галактике. Был выбран как генетический шаблон для армии клонов Республики. Получил в награду невидоизменённого клона, которого назвал Бобой и воспитал как сына. Участвовал в попытке убийства сенатора Падме Амидалы. Был убит Мейсом Винду на Джеонозисе, обезглавлен световым мечом. Его доспехи и корабль унаследовал Боба. Его навыки боя и тактики сделали его идеальным шаблоном для клонов.',
@ -578,7 +578,7 @@ module.exports = {
health: 5,
type: 'minion',
faction: 'rebellion',
text: 'Бывшая падаван Энакина. Белые клинки.',
text: 'Бывшая падаван Энакина. Белые клинки. Даёт +1/+1 Рексу.',
art: 'ahsoka',
legendary: true,
battlecry: 'Нанеси 1 урона вражескому миньону.',
@ -603,7 +603,7 @@ module.exports = {
health: 4,
type: 'minion',
faction: 'rebellion',
text: 'Клон-командир 501-го. Верен своим.',
text: 'Клон-командир 501-го. Верен своим. Даёт +1/+1 Эйсоке.',
art: 'rex',
bio: 'Капитан Рекс (CT-7567) — клон-командир, лидер 501-го легиона. Служил под командованием Энакина Скайуокера и Эйсоки Тано. Снял чип контроля после того, как Файвс раскрыл заговор. Выжил после Приказа 66 благодаря помощи Эйсоки. Присоединился к повстанцам и сражался на Эндоре. Его независимость и преданность товарищам делали его уникальным клоном. Дожил до старости, став символом того, что клоны были больше, чем просто солдаты. Его дружба с Эйсокой длилась десятилетия.',
},
@ -628,7 +628,7 @@ module.exports = {
health: 5,
type: 'minion',
faction: 'neutral',
text: 'Мандалорец. Это путь.',
text: 'Мандалорец. Это путь. Даёт +1/+1 Грогу.',
art: 'mando',
legendary: true,
bio: 'Дин Джарин (Мандалорец) — охотник за головами, мандалорец из клана Джаринов. Воспитал Грогу (Малыша Йоду), став его приёмным отцом. Владеет тёмным мечом, легендарным оружием мандалорцев. Следовал кодексу мандалорцев: «Это путь». Его доспехи из бескара были семейной реликвией. Стал лидером мандалорцев, объединив различные кланы. Его преданность Грогу и кодексу мандалорцев сделали его легендой. Один из величайших мандалорцев своего времени.',
@ -640,7 +640,7 @@ module.exports = {
health: 3,
type: 'minion',
faction: 'neutral',
text: 'Малыш Йода. Сила сильна в нём.',
text: 'Малыш Йода. Сила сильна в нём. Даёт +1/+1 Мандалорцу.',
art: 'grogu',
legendary: true,
deathrattle: 'Восстанови 2 здоровья своему герою.',
@ -1308,7 +1308,7 @@ module.exports = {
health: 6,
type: 'minion',
faction: 'rebellion',
text: 'Избранный. Может пасть на тёмную сторону.',
text: 'Избранный. Может пасть на тёмную сторону. Даёт +1/+1 Оби-Вану и Падме.',
art: 'anakin',
legendary: true,
battlecry: 'Уничтожь вражеского миньона с атакой 4 или меньше.',
@ -1423,7 +1423,7 @@ module.exports = {
health: 5,
type: 'minion',
faction: 'empire',
text: 'Брат Мола. Сильный, но хрупкий.',
text: 'Брат Мола. Сильный, но хрупкий. Даёт +1/+1 Дарту Молу.',
art: 'savage',
legendary: true,
battlecry: 'Уничтожь вражеского миньона с атакой 2 или меньше.',
@ -1453,7 +1453,7 @@ module.exports = {
health: 6,
type: 'minion',
faction: 'empire',
text: 'Стратег. Тактик Империи.',
text: 'Стратег. Тактик Империи. Даёт +1/+1 имперцам.',
art: 'tarkin',
legendary: true,
battlecry: 'Уничтожь вражеского миньона с наибольшей атакой.',

View File

@ -14,6 +14,7 @@
let spellMode = { active: false, handIndex: -1, cardId: '', spellTarget: '' };
let heroAbilityMode = { active: false };
let stealCardsMode = { active: false, handIndex: -1, targetPlayerIndex: null, targetDeck: [], selectedIndices: [] };
let battlecryTargetMode = { active: false, battlecryId: '', cardId: '', minionId: '', targetPlayerIndex: null, targetBoardIndex: null };
const seenMinions = new Set();
let lastHandLength = 0;
let prevGameState = null;
@ -235,8 +236,28 @@
showStealCardsModal(gameState, data);
});
socket.on('battlecryTargetRequest', (data) => {
if (data.battlecryId === 'return_hand_enemy') {
// Показываем модальное окно для выбора противника и его миньона
showBattlecryTargetModal(gameState, data);
}
});
return socket;
}
function showBattlecryTargetModal(state, data) {
battlecryTargetMode = { active: true, battlecryId: data.battlecryId, cardId: data.cardId, minionId: data.minionId };
// Активируем режим выбора цели
spellMode = { active: true, handIndex: -1, cardId: data.cardId, spellTarget: 'enemy_minion', battlecryMode: true, minionId: data.minionId };
$('spell-mode')?.classList.remove('hidden');
const spellModeText = $('spell-mode')?.querySelector('p');
if (spellModeText) {
spellModeText.textContent = 'Выберите миньона противника для возврата в руку';
}
renderGame(state);
}
function renderGameEnded(state) {
const badge = $('turn-badge');
@ -274,6 +295,30 @@
modal.classList.remove('hidden');
}
function showTurnNotification(state) {
const notification = $('turn-notification');
const notificationText = $('turn-notification-text');
if (!notification || !notificationText) return;
const currentPlayer = state.players[state.currentPlayerIndex];
const isYourTurn = state.currentPlayerIndex === state.yourIndex;
const playerName = currentPlayer?.name || `Игрок ${state.currentPlayerIndex + 1}`;
if (isYourTurn) {
notificationText.textContent = 'ВАШ ХОД';
notificationText.parentElement.classList.add('your-turn');
} else {
notificationText.textContent = `ХОД: ${playerName.toUpperCase()}`;
notificationText.parentElement.classList.remove('your-turn');
}
// Показываем нотификацию на 2 секунды
notification.classList.add('show');
setTimeout(() => {
notification.classList.remove('show');
}, 2000);
}
function renderBoards(state) {
const you = state.players[yourIndex];
if (!you) return;
@ -284,17 +329,18 @@
.map((p, i) => {
if (i === yourIndex) return '';
const isCurrent = state.currentPlayerIndex === i;
const isDead = p.health <= 0 || p.isDead;
const name = p.name || 'Игрок ' + (i + 1);
const minions = (p.board || []).map((m, j) => renderBoardMinion(m, i, j, state, true, false));
const heroBar = renderHeroTarget(i, state);
const heroDrop = renderHeroDropZone(i, state);
const canSteal = state.currentPlayerIndex === state.yourIndex && spellMode.active && spellMode.cardId && cardDb[spellMode.cardId]?.spellEffect === 'steal_cards';
return `
<div class="opponent-block ${isCurrent ? 'current-turn' : ''} ${canSteal ? 'steal-target' : ''}" data-opponent-index="${i}" data-player-index="${i}">
<div class="opponent-name">${escapeHtml(name)}</div>
<div class="opponent-stats">
<span>❤ ${p.health}</span>
<span>🔵 ${p.mana}/${p.maxMana}</span>
<div class="opponent-block ${isCurrent ? 'current-turn' : ''} ${canSteal ? 'steal-target' : ''} ${isDead ? 'defeated' : ''}" data-opponent-index="${i}" data-player-index="${i}">
<div class="opponent-name ${isDead ? 'defeated-name' : ''}">${escapeHtml(name)}${isDead ? ' <span class="defeated-badge">✝</span>' : ''}</div>
<div class="opponent-stats ${isDead ? 'defeated-stats' : ''}">
<span class="opponent-health">❤ ${p.health ?? 30}</span>
<span>🔵 ${p.mana ?? 0}/${p.maxMana ?? 0}</span>
${p.deck && p.deck.length > 0 ? `<span>📚 ${p.deck.length}</span>` : ''}
</div>
<div class="opponent-board">${heroDrop}${heroBar}${minions.join('')}</div>
@ -612,9 +658,19 @@
const you = state.players[yourIndex];
if (!you) return;
$('your-mana').textContent = you.mana;
$('your-max-mana').textContent = you.maxMana;
$('your-health').textContent = you.health;
$('your-mana').textContent = you.mana ?? 0;
$('your-max-mana').textContent = you.maxMana ?? 0;
const healthEl = $('your-health');
if (healthEl) {
const newHealth = you.health ?? 30;
const oldHealth = parseInt(healthEl.textContent) || 30;
healthEl.textContent = newHealth;
// Добавляем визуальный эффект при изменении HP
if (newHealth !== oldHealth) {
healthEl.classList.add('health-changed');
setTimeout(() => healthEl.classList.remove('health-changed'), 500);
}
}
$('your-deck').textContent = state.yourDeckCount ?? you.deck?.length ?? 0;
const lastLog = state.log && state.log.length ? state.log[state.log.length - 1] : null;
const logEl = $('game-log');
@ -660,6 +716,9 @@
badge.classList.toggle('your-turn', isYourTurn);
badge.textContent = isYourTurn ? 'ВАШ ХОД' : 'Ход ' + (state.turn || 1);
}
// Показываем нотификацию о том, кто ходит
showTurnNotification(state);
var skipBoardUpdate = false;
if (lastLog?.type === 'attack' && lastLog.attackerMinionId && lastLog.targetMinionId && prevGameState && runCombatAnimation(state)) {
@ -810,11 +869,20 @@
const canAttack = !isOpponent && m.canAttack && isYourTurn && (!canAttackTwice || attacksUsed < 2);
const attackable = attackMode.active && attackMode.attackerPlayer === state.yourIndex && attackMode.attackerBoard === boardIndex;
const targetable = attackMode.active && attackMode.attackerPlayer === state.yourIndex && playerIndex !== state.yourIndex;
// Учитываем бонусы синергий
const synergyAttack = m.synergyAttackBonus || 0;
const synergyHealth = m.synergyHealthBonus || 0;
const displayAttack = (m.attack || 0) + synergyAttack;
const displayHealth = (m.health || 0) + synergyHealth;
const hasSynergy = synergyAttack > 0 || synergyHealth > 0;
const cls = ['card-wrap'];
if (canAttack && !attackMode.active) cls.push('attackable');
if (attackable) cls.push('attackable');
if (targetable) cls.push('targetable');
if (isOpponent && isYourTurn) cls.push('drop-target');
if (hasSynergy) cls.push('has-synergy');
var spellTarget = false;
if (spellMode.active && spellMode.spellTarget) {
var st = spellMode.spellTarget;
@ -841,8 +909,8 @@
+ '<div class="card-name">' + escapeHtml(name) + '</div>'
+ textHtml + abilHtml
+ '<div class="card-stats">'
+ '<div class="card-atk-wrap"><span class="card-stat-label">Атака</span><span class="atk">' + m.attack + '</span></div>'
+ '<div class="card-hp-wrap"><span class="card-stat-label">Здоровье</span><span class="hp">' + m.health + '</span></div>'
+ '<div class="card-atk-wrap"><span class="card-stat-label">Атака</span><span class="atk">' + displayAttack + (synergyAttack > 0 ? '<span class="synergy-bonus">+' + synergyAttack + '</span>' : '') + '</span></div>'
+ '<div class="card-hp-wrap"><span class="card-stat-label">Здоровье</span><span class="hp">' + displayHealth + (synergyHealth > 0 ? '<span class="synergy-bonus">+' + synergyHealth + '</span>' : '') + '</span></div>'
+ '</div>'
+ '<div class="card-info-row"><span></span><button type="button" class="card-btn-info" data-card-id="' + escapeHtml(m.cardId) + '" title="Описание">i</button></div>'
+ '</div></div></div>';
@ -1248,11 +1316,31 @@
$all('.spell-target, .drop-target-hero.spell-target').forEach(function (el) {
el.onclick = function (e) {
if (!spellMode.active) return;
if (!spellMode.active && !battlecryTargetMode.active) return;
e.stopPropagation();
var tp = parseInt(el.dataset.dropPlayer ?? el.dataset.playerIndex, 10);
var tb = parseInt(el.dataset.dropBoard ?? el.dataset.boardIndex, 10);
// Обработка для battlecry (Ezra Bridger)
if (battlecryTargetMode.active && battlecryTargetMode.battlecryId === 'return_hand_enemy') {
if (tp !== state.yourIndex && tp >= 0 && tb >= 0) {
const targetPlayer = state.players[tp];
if (targetPlayer && targetPlayer.board && targetPlayer.board[tb]) {
socket.emit('battlecryTarget', {
battlecryId: 'return_hand_enemy',
targetPlayerIndex: tp,
targetBoardIndex: tb
});
battlecryTargetMode = { active: false, battlecryId: '', cardId: '', minionId: '', targetPlayerIndex: null, targetBoardIndex: null };
spellMode = { active: false, handIndex: -1, cardId: '', spellTarget: '' };
$('spell-mode')?.classList.add('hidden');
renderGame(state);
return;
}
}
return;
}
// Специальная обработка для Грабежа - открываем модальное окно
const spellCard = cardDb[spellMode.cardId];
if (spellCard && spellCard.spellEffect === 'steal_cards' && spellCard.spellTarget === 'enemy_player') {

View File

@ -229,6 +229,13 @@
</div>
</div>
<!-- Turn Notification -->
<div id="turn-notification" class="turn-notification hidden">
<div class="turn-notification-content">
<h2 id="turn-notification-text">Ход игрока</h2>
</div>
</div>
<div id="attack-mode" class="attack-mode hidden">
<p>Выберите цель для атаки (враг или его существо)</p>
<button type="button" id="btn-cancel-attack" class="btn btn-ghost">Отмена</button>

View File

@ -345,6 +345,26 @@ html, body {
color: #ff6b6b; display: inline-flex; align-items: center;
}
.health-display #your-health.health-changed {
animation: healthChange 0.5s ease-out;
display: inline-block;
}
@keyframes healthChange {
0% {
transform: scale(1);
color: var(--red);
}
50% {
transform: scale(1.3);
color: var(--amber-bright);
}
100% {
transform: scale(1);
color: var(--red);
}
}
.deck-count { font-size: 0.9rem; color: #94a3b8; }
.deck-count .swg.deck-icon { font-size: 1rem; margin-right: 0.2rem; color: var(--gold); }
@ -519,8 +539,50 @@ html, body {
0%, 100% { box-shadow: 0 0 20px rgba(255,204,0,0.4); }
50% { box-shadow: 0 0 30px rgba(255,204,0,0.7); }
}
.opponent-block.defeated {
opacity: 0.5;
position: relative;
}
.opponent-block.defeated::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, transparent 45%, rgba(255,70,70,0.3) 48%, rgba(255,70,70,0.3) 52%, transparent 55%);
pointer-events: none;
z-index: 10;
}
.opponent-block.defeated::after {
content: '';
position: absolute;
top: 50%;
left: -10px;
right: -10px;
height: 3px;
background: rgba(255,70,70,0.8);
transform: translateY(-50%) rotate(-5deg);
box-shadow: 0 0 10px rgba(255,70,70,0.6);
pointer-events: none;
z-index: 11;
}
.opponent-name { font-weight: 700; color: var(--cyan); margin-bottom: 0.35rem; font-size: 0.95rem; }
.opponent-name.defeated-name {
text-decoration: line-through;
color: #666;
opacity: 0.6;
}
.defeated-badge {
color: var(--red);
font-size: 1.2em;
margin-left: 0.3rem;
}
.opponent-stats { display: flex; gap: 0.75rem; font-size: 0.85rem; color: #94a3b8; margin-bottom: 0.5rem; }
.opponent-stats.defeated-stats {
opacity: 0.5;
text-decoration: line-through;
}
.opponent-board { display: flex; flex-wrap: wrap; gap: 0.5rem; min-height: 80px; }
.battlefield { flex: 1; display: flex; flex-direction: column; justify-content: center; position: relative; z-index: 1; }
@ -1739,31 +1801,102 @@ html, body {
/* Frozen effect */
.frozen, .frozen-card {
position: relative;
filter: brightness(0.85) saturate(0.7);
border: 2px solid rgba(135,206,250,0.6) !important;
box-shadow: 0 0 15px rgba(135,206,250,0.4), inset 0 0 20px rgba(135,206,250,0.2) !important;
filter: brightness(0.75) saturate(0.6);
border: 3px solid rgba(135,206,250,0.8) !important;
box-shadow: 0 0 20px rgba(135,206,250,0.6),
0 0 40px rgba(135,206,250,0.4),
inset 0 0 30px rgba(135,206,250,0.3) !important;
animation: frozenShimmer 2s ease-in-out infinite;
}
@keyframes frozenShimmer {
0%, 100% {
filter: brightness(0.75) saturate(0.6);
box-shadow: 0 0 20px rgba(135,206,250,0.6),
0 0 40px rgba(135,206,250,0.4),
inset 0 0 30px rgba(135,206,250,0.3);
}
50% {
filter: brightness(0.85) saturate(0.7);
box-shadow: 0 0 30px rgba(135,206,250,0.8),
0 0 60px rgba(135,206,250,0.6),
inset 0 0 40px rgba(135,206,250,0.4);
}
}
.frozen-icon {
position: absolute;
top: 5px;
right: 5px;
font-size: 1.5rem;
font-size: 1.8rem;
z-index: 10;
animation: frozenPulse 2s ease-in-out infinite;
text-shadow: 0 0 10px rgba(135,206,250,0.8);
text-shadow: 0 0 10px rgba(135,206,250,0.8),
0 0 20px rgba(135,206,250,0.6);
}
@keyframes frozenPulse {
0%, 100% { opacity: 0.8; transform: scale(1); }
50% { opacity: 1; transform: scale(1.1); }
0%, 100% { opacity: 0.9; transform: scale(1); }
50% { opacity: 1; transform: scale(1.3); }
}
.frozen-card::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(135,206,250,0.1) 0%, rgba(70,130,180,0.1) 100%);
border-radius: 10px;
inset: -3px;
background: linear-gradient(135deg,
rgba(135,206,250,0.2) 0%,
rgba(70,130,180,0.3) 25%,
rgba(135,206,250,0.2) 50%,
rgba(70,130,180,0.3) 75%,
rgba(135,206,250,0.2) 100%);
background-size: 200% 200%;
border-radius: 12px;
animation: frozenGradient 3s linear infinite;
z-index: -1;
pointer-events: none;
z-index: 1;
}
@keyframes frozenGradient {
0% {
background-position: 0% 0%;
}
100% {
background-position: 200% 200%;
}
}
/* Synergy bonuses */
.has-synergy {
position: relative;
}
.has-synergy::after {
content: '✨';
position: absolute;
top: -5px;
left: -5px;
font-size: 1.2rem;
z-index: 15;
animation: synergyGlow 2s ease-in-out infinite;
filter: drop-shadow(0 0 5px rgba(255, 204, 0, 0.8));
}
@keyframes synergyGlow {
0%, 100% {
opacity: 0.7;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.2);
}
}
.synergy-bonus {
color: var(--amber-bright);
font-size: 0.7em;
font-weight: bold;
margin-left: 2px;
text-shadow: 0 0 5px rgba(255, 204, 0, 0.8);
}
/* Fictional card marker */

475
server.js
View File

@ -357,6 +357,10 @@ function endTurn(room) {
}
gameState.turn++;
gameState.log.push({ type: 'turn', from: prev, to: next });
// Применяем синергии в начале каждого хода
applySynergies(room);
checkGameOver(room);
startTurnTimer(room);
broadcastGameState(room);
@ -514,16 +518,22 @@ function runBattlecry(room, card, playerIndex) {
} else if (id === 'heal_hero_5') {
p.health = Math.min(30, p.health + 5);
gameState.log.push({ type: 'battlecry', effect: 'heal_hero_5', playerIndex });
} else if (id === 'return_hand_enemy' && enemies.length) {
const enemy = enemies[Math.floor(Math.random() * enemies.length)];
const enemyIdx = gameState.players.indexOf(enemy);
if (enemy.board && enemy.board.length > 0) {
const target = enemy.board[Math.floor(Math.random() * enemy.board.length)];
const boardIdx = enemy.board.indexOf(target);
enemy.board.splice(boardIdx, 1);
enemy.hand.push(target.cardId);
gameState.log.push({ type: 'battlecry', effect: 'return_hand_enemy', fromPlayer: playerIndex, toPlayer: enemyIdx, toIdx: boardIdx });
} else if (id === 'return_hand_enemy') {
// Для Ezra Bridger требуется выбор конкретного игрока
// Отправляем запрос клиенту для выбора цели
const socket = io.sockets.sockets.get(p.id);
if (socket && enemies.length > 0) {
socket.emit('battlecryTargetRequest', {
battlecryId: 'return_hand_enemy',
cardId: card.id || Object.keys(cardDb).find(k => cardDb[k] === card),
availableTargets: enemies.map((e, idx) => ({
playerIndex: gameState.players.indexOf(e),
playerName: e.name || `Игрок ${gameState.players.indexOf(e) + 1}`,
hasMinions: e.board && e.board.length > 0
}))
});
}
return; // Не выполняем сразу, ждём выбора цели
} else if (id === 'destroy_strongest_enemy' && enemies.length) {
const enemy = enemies[Math.floor(Math.random() * enemies.length)];
const enemyIdx = gameState.players.indexOf(enemy);
@ -757,11 +767,375 @@ function playCard(room, socketId, handIndex, boardPos) {
};
p.board.splice(typeof boardPos === 'number' ? boardPos : p.board.length, 0, minion);
gameState.log.push({ type: 'play', playerIndex: pi, cardId: cid, minionId: minion.id });
if (card.battlecryId) runBattlecry(room, card, pi);
// Для battlecry, требующих выбора цели (Ezra Bridger), отправляем запрос клиенту
if (card.battlecryId === 'return_hand_enemy') {
const enemies = gameState.players.filter((pl, i) => i !== pi && pl.health > 0);
if (enemies.length > 0) {
const socket = io.sockets.sockets.get(p.id);
if (socket) {
// Сохраняем информацию о миньоне для последующего выполнения battlecry
minion.pendingBattlecry = { battlecryId: card.battlecryId, cardId: cid };
socket.emit('battlecryTargetRequest', {
battlecryId: 'return_hand_enemy',
cardId: cid,
minionId: minion.id,
availableTargets: enemies.flatMap(enemy => {
const enemyIdx = gameState.players.indexOf(enemy);
if (!enemy.board || enemy.board.length === 0) return [];
return enemy.board.map((m, boardIdx) => ({
playerIndex: enemyIdx,
boardIndex: boardIdx,
playerName: enemy.name || `Игрок ${enemyIdx + 1}`,
minionName: cardDb[m.cardId]?.name || m.cardId
}));
})
});
broadcastGameState(room);
return; // Не применяем синергии пока, ждём выбора цели
}
}
} else if (card.battlecryId) {
runBattlecry(room, card, pi);
}
// Применяем синергии после размещения карты
applySynergies(room);
checkGameOver(room);
broadcastGameState(room);
}
// Функция для применения синергий между картами
function applySynergies(room) {
const gameState = room.gameState;
const cardDb = require('./cards.js');
gameState.players.forEach((player, playerIndex) => {
if (!player.board || player.board.length === 0) return;
// Сбрасываем все бонусы синергий перед пересчётом
player.board.forEach(m => {
m.synergyAttackBonus = 0;
m.synergyHealthBonus = 0;
});
player.board.forEach((minion, idx) => {
const card = cardDb[minion.cardId];
if (!card) return;
// Дарт Вейдер даёт +1/+1 всем штурмовикам и клонам
if (card.id === 'vader' || card.name === 'Darth Vader') {
player.board.forEach((other, otherIdx) => {
if (idx !== otherIdx) {
const otherCard = cardDb[other.cardId];
if (otherCard && (otherCard.id === 'stormtrooper' || otherCard.name === 'Stormtrooper' ||
otherCard.id === 'clone_trooper' || otherCard.name === 'Clone Trooper' ||
otherCard.id === 'clone_commando' || otherCard.name === 'Clone Commando' ||
otherCard.id === 'arc_trooper' || otherCard.name === 'ARC Trooper')) {
other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1;
other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1;
}
}
});
}
// Люк Скайуокер даёт +1/+1 всем повстанцам
if (card.id === 'luke' || card.name === 'Luke Skywalker') {
player.board.forEach((other, otherIdx) => {
if (idx !== otherIdx) {
const otherCard = cardDb[other.cardId];
if (otherCard && otherCard.faction === 'rebellion' && otherCard.type === 'minion') {
other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1;
other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1;
}
}
});
}
// Император Палпатин даёт +1/+1 всем ситхам и имперским картам
if (card.id === 'palpatine' || card.name === 'Emperor Palpatine') {
player.board.forEach((other, otherIdx) => {
if (idx !== otherIdx) {
const otherCard = cardDb[other.cardId];
if (otherCard && (otherCard.faction === 'empire' ||
otherCard.name?.includes('Darth') || otherCard.id === 'maul' ||
otherCard.id === 'vader' || otherCard.id === 'dooku' || otherCard.id === 'kylo')) {
other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1;
other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1;
}
}
});
}
// Хан Соло и Чубакка дают друг другу +1/+1
if (card.id === 'han' || card.name === 'Han Solo') {
player.board.forEach((other, otherIdx) => {
if (idx !== otherIdx) {
const otherCard = cardDb[other.cardId];
if (otherCard && (otherCard.id === 'chewie' || otherCard.name === 'Chewbacca')) {
other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1;
other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1;
}
}
});
}
if (card.id === 'chewie' || card.name === 'Chewbacca') {
player.board.forEach((other, otherIdx) => {
if (idx !== otherIdx) {
const otherCard = cardDb[other.cardId];
if (otherCard && (otherCard.id === 'han' || otherCard.name === 'Han Solo')) {
other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1;
other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1;
}
}
});
}
// R2-D2 и C-3PO дают друг другу +1/+1
if (card.id === 'r2d2' || card.name === 'R2-D2') {
player.board.forEach((other, otherIdx) => {
if (idx !== otherIdx) {
const otherCard = cardDb[other.cardId];
if (otherCard && (otherCard.id === 'c3po' || otherCard.name === 'C-3PO')) {
other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1;
other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1;
}
}
});
}
if (card.id === 'c3po' || card.name === 'C-3PO') {
player.board.forEach((other, otherIdx) => {
if (idx !== otherIdx) {
const otherCard = cardDb[other.cardId];
if (otherCard && (otherCard.id === 'r2d2' || otherCard.name === 'R2-D2')) {
other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1;
other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1;
}
}
});
}
// Лея даёт +1/+1 Люку
if (card.id === 'leia' || card.name === 'Princess Leia') {
player.board.forEach((other, otherIdx) => {
if (idx !== otherIdx) {
const otherCard = cardDb[other.cardId];
if (otherCard && (otherCard.id === 'luke' || otherCard.name === 'Luke Skywalker')) {
other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1;
other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1;
}
}
});
}
// Люк даёт +1/+1 Лее
if (card.id === 'luke' || card.name === 'Luke Skywalker') {
player.board.forEach((other, otherIdx) => {
if (idx !== otherIdx) {
const otherCard = cardDb[other.cardId];
if (otherCard && (otherCard.id === 'leia' || otherCard.name === 'Princess Leia')) {
other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1;
other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1;
}
}
});
}
// Оби-Ван даёт +1/+1 Энакину/Анакину
if (card.id === 'obiwan' || card.name === 'Obi-Wan Kenobi') {
player.board.forEach((other, otherIdx) => {
if (idx !== otherIdx) {
const otherCard = cardDb[other.cardId];
if (otherCard && (otherCard.id === 'anakin' || otherCard.name === 'Anakin Skywalker')) {
other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1;
other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1;
}
}
});
}
// Энакин даёт +1/+1 Оби-Вану
if (card.id === 'anakin' || card.name === 'Anakin Skywalker') {
player.board.forEach((other, otherIdx) => {
if (idx !== otherIdx) {
const otherCard = cardDb[other.cardId];
if (otherCard && (otherCard.id === 'obiwan' || otherCard.name === 'Obi-Wan Kenobi')) {
other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1;
other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1;
}
}
});
}
// Йода даёт +1/+1 всем джедаям
if (card.id === 'yoda' || card.name === 'Yoda') {
player.board.forEach((other, otherIdx) => {
if (idx !== otherIdx) {
const otherCard = cardDb[other.cardId];
if (otherCard && otherCard.faction === 'rebellion' &&
(otherCard.name?.includes('Jedi') || otherCard.id === 'luke' ||
otherCard.id === 'obiwan' || otherCard.id === 'anakin' ||
otherCard.id === 'ahsoka' || otherCard.id === 'mace' ||
otherCard.id === 'quigon' || otherCard.id === 'plo_koon' ||
otherCard.id === 'ki_adi' || otherCard.id === 'aayla' ||
otherCard.id === 'shaak_ti' || otherCard.id === 'kanan' ||
otherCard.id === 'ezra' || otherCard.id === 'cal_kestis')) {
other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1;
other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1;
}
}
});
}
// Боба Фетт даёт +1/+1 Джанго Фетту
if (card.id === 'boba' || card.name === 'Boba Fett') {
player.board.forEach((other, otherIdx) => {
if (idx !== otherIdx) {
const otherCard = cardDb[other.cardId];
if (otherCard && (otherCard.id === 'jango' || otherCard.name === 'Jango Fett')) {
other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1;
other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1;
}
}
});
}
// Джанго Фетт даёт +1/+1 Бобе Фетту
if (card.id === 'jango' || card.name === 'Jango Fett') {
player.board.forEach((other, otherIdx) => {
if (idx !== otherIdx) {
const otherCard = cardDb[other.cardId];
if (otherCard && (otherCard.id === 'boba' || otherCard.name === 'Boba Fett')) {
other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1;
other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1;
}
}
});
}
// Эйсока даёт +1/+1 Рексу
if (card.id === 'ahsoka' || card.name === 'Ahsoka Tano') {
player.board.forEach((other, otherIdx) => {
if (idx !== otherIdx) {
const otherCard = cardDb[other.cardId];
if (otherCard && (otherCard.id === 'rex' || otherCard.name === 'Captain Rex')) {
other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1;
other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1;
}
}
});
}
// Рекс даёт +1/+1 Эйсоке
if (card.id === 'rex' || card.name === 'Captain Rex') {
player.board.forEach((other, otherIdx) => {
if (idx !== otherIdx) {
const otherCard = cardDb[other.cardId];
if (otherCard && (otherCard.id === 'ahsoka' || otherCard.name === 'Ahsoka Tano')) {
other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1;
other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1;
}
}
});
}
// Мандалорец даёт +1/+1 Грогу
if (card.id === 'mando' || card.name === 'Din Djarin') {
player.board.forEach((other, otherIdx) => {
if (idx !== otherIdx) {
const otherCard = cardDb[other.cardId];
if (otherCard && (otherCard.id === 'grogu' || otherCard.name === 'Grogu')) {
other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1;
other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1;
}
}
});
}
// Грогу даёт +1/+1 Мандалорцу
if (card.id === 'grogu' || card.name === 'Grogu') {
player.board.forEach((other, otherIdx) => {
if (idx !== otherIdx) {
const otherCard = cardDb[other.cardId];
if (otherCard && (otherCard.id === 'mando' || otherCard.name === 'Din Djarin')) {
other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1;
other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1;
}
}
});
}
// Таркин даёт +1/+1 имперским картам
if (card.id === 'tarkin' || card.name === 'Grand Moff Tarkin') {
player.board.forEach((other, otherIdx) => {
if (idx !== otherIdx) {
const otherCard = cardDb[other.cardId];
if (otherCard && otherCard.faction === 'empire' && otherCard.type === 'minion') {
other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1;
other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1;
}
}
});
}
// Дарт Мол даёт +1/+1 Сэвиджу Оппрессу
if (card.id === 'maul' || card.name === 'Darth Maul') {
player.board.forEach((other, otherIdx) => {
if (idx !== otherIdx) {
const otherCard = cardDb[other.cardId];
if (otherCard && (otherCard.id === 'savage' || otherCard.name === 'Savage Opress')) {
other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1;
other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1;
}
}
});
}
// Сэвидж Оппресс даёт +1/+1 Дарту Молу
if (card.id === 'savage' || card.name === 'Savage Opress') {
player.board.forEach((other, otherIdx) => {
if (idx !== otherIdx) {
const otherCard = cardDb[other.cardId];
if (otherCard && (otherCard.id === 'maul' || otherCard.name === 'Darth Maul')) {
other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1;
other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1;
}
}
});
}
// Падме даёт +1/+1 Энакину
if (card.id === 'padme' || card.name === 'Padmé Amidala') {
player.board.forEach((other, otherIdx) => {
if (idx !== otherIdx) {
const otherCard = cardDb[other.cardId];
if (otherCard && (otherCard.id === 'anakin' || otherCard.name === 'Anakin Skywalker')) {
other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1;
other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1;
}
}
});
}
// Энакин даёт +1/+1 Падме
if (card.id === 'anakin' || card.name === 'Anakin Skywalker') {
player.board.forEach((other, otherIdx) => {
if (idx !== otherIdx) {
const otherCard = cardDb[other.cardId];
if (otherCard && (otherCard.id === 'padme' || otherCard.name === 'Padmé Amidala')) {
other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1;
other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1;
}
}
});
}
});
});
}
function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardIndex) {
const gameState = room.gameState;
const pi = findPlayerIndex(room, socketId);
@ -944,6 +1318,9 @@ function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardInde
p.mana -= cost;
p.hand.splice(handIndex, 1);
// Применяем синергии после размещения карты
applySynergies(room);
checkGameOver(room);
broadcastGameState(room);
}
@ -1003,6 +1380,15 @@ function stealCardsFromDeck(room, socketId, handIndex, targetPlayerIndex, cardIn
stolenCount: stolenCards.length
});
// Применяем синергии после изменений на доске
applySynergies(room);
// Применяем синергии после боя
applySynergies(room);
// Применяем синергии после изменений на доске
applySynergies(room);
checkGameOver(room);
broadcastGameState(room);
}
@ -1038,6 +1424,12 @@ function heroAbility(room, socketId, targetPlayerIndex, targetBoardIndex) {
gameState.players.forEach((pl) => {
pl.board = pl.board.filter((min) => min.health > 0);
});
// Применяем синергии после изменений на доске
applySynergies(room);
// Применяем синергии после изменений на доске
applySynergies(room);
checkGameOver(room);
broadcastGameState(room);
}
@ -1054,8 +1446,12 @@ function attack(room, socketId, attackerBoardIndex, targetPlayerIndex, targetBoa
const targetPlayer = gameState.players[targetPlayerIndex];
if (!targetPlayer) return;
// Применяем синергии перед расчётом урона
applySynergies(room);
if (targetBoardIndex === -1) {
targetPlayer.health = Math.max(0, targetPlayer.health - attacker.attack);
const attackerAttack = attacker.attack + (attacker.synergyAttackBonus || 0);
targetPlayer.health = Math.max(0, targetPlayer.health - attackerAttack);
gameState.log.push({
type: 'attackHero',
fromPlayer: pi,
@ -1063,12 +1459,23 @@ function attack(room, socketId, attackerBoardIndex, targetPlayerIndex, targetBoa
attackerMinionId: attacker.id,
damage: attacker.attack,
});
checkGameOver(room); // Проверяем после атаки по герою
// Применяем синергии перед расчётом урона
applySynergies(room);
checkGameOver(room); // Проверяем после атаки по герою
} else {
const target = targetPlayer.board[targetBoardIndex];
if (!target) return;
target.health -= attacker.attack;
attacker.health -= target.attack;
// Применяем синергии перед расчётом урона
applySynergies(room);
// Учитываем бонусы синергий при расчёте урона (бонусы уже применены в applySynergies)
const attackerAttack = attacker.attack + (attacker.synergyAttackBonus || 0);
const targetAttack = target.attack + (target.synergyAttackBonus || 0);
target.health -= attackerAttack;
attacker.health -= targetAttack;
const attackerDied = attacker.health <= 0;
const targetDied = target.health <= 0;
gameState.log.push({
@ -1250,6 +1657,46 @@ io.on('connection', (socket) => {
if (!room || !room.gameState || room.gameState.phase !== 'playing') return;
stealCardsFromDeck(room, socket.id, data.handIndex, data.targetPlayerIndex, data.cardIndices);
});
socket.on('battlecryTarget', (data) => {
const room = getRoomBySocket(socket.id);
if (!room || !room.gameState || room.gameState.phase !== 'playing') return;
const gameState = room.gameState;
const pi = findPlayerIndex(room, socket.id);
if (pi < 0) return;
const p = gameState.players[pi];
if (p.health <= 0 || p.isDead) return;
if (data.battlecryId === 'return_hand_enemy') {
const targetPlayerIndex = data.targetPlayerIndex;
const targetBoardIndex = data.targetBoardIndex;
const targetPlayer = gameState.players[targetPlayerIndex];
if (!targetPlayer || targetPlayerIndex === pi || targetPlayer.health <= 0) return;
if (!targetPlayer.board || targetPlayer.board.length === 0) return;
if (targetBoardIndex == null || targetBoardIndex < 0 || targetBoardIndex >= targetPlayer.board.length) return;
const target = targetPlayer.board[targetBoardIndex];
targetPlayer.board.splice(targetBoardIndex, 1);
if (targetPlayer.hand.length < 10) {
targetPlayer.hand.push(target.cardId);
}
// Удаляем pendingBattlecry с миньона
const playedMinion = p.board?.find(m => m.pendingBattlecry && m.pendingBattlecry.battlecryId === 'return_hand_enemy');
if (playedMinion) {
delete playedMinion.pendingBattlecry;
}
gameState.log.push({ type: 'battlecry', effect: 'return_hand_enemy', fromPlayer: pi, toPlayer: targetPlayerIndex, toIdx: targetBoardIndex });
// Применяем синергии после изменений на доске
applySynergies(room);
checkGameOver(room);
broadcastGameState(room);
}
});
socket.on('resetToLobby', () => {
const room = getRoomBySocket(socket.id);