diff --git a/cards.js b/cards.js index 1fa8b42..0e35946 100644 --- a/cards.js +++ b/cards.js @@ -2796,4 +2796,129 @@ module.exports = { battlecryId: 'deal_2_minion', bio: 'Висас Марр — мираллука, бывшая ученица Дарта Нихилуса. Была единственной выжившей после уничтожения её родной планеты Катамии Нихилусом. Была послана убить Изгнанника-джедая, но была спасена и вернулась к свету. Её слепота и связь с Силой делали её уникальной. Её искупление стало важной частью её истории. Её навыки боя на световых мечах были впечатляющими.', }, + // Карты с хилом + field_heal: { + name: 'Полевой медик', + cost: 2, + attack: 1, + health: 3, + type: 'minion', + faction: 'neutral', + text: 'В конце каждого хода восстанавливает +1 HP герою.', + art: 'medic', + fieldEffect: 'heal_1_per_turn', + bio: 'Медик, который лечит раненых на поле боя.', + }, + instant_heal_5: { + name: 'Быстрое лечение', + cost: 2, + type: 'spell', + faction: 'neutral', + text: 'Восстанови 5 HP герою.', + art: 'heal', + spellEffect: 'heal_hero_5', + spellTarget: 'none', + bio: 'Мгновенное восстановление здоровья.', + }, + instant_heal_30pct: { + name: 'Регенерация', + cost: 3, + type: 'spell', + faction: 'neutral', + text: 'Восстанови 30% HP герою.', + art: 'regen', + spellEffect: 'heal_hero_30pct', + spellTarget: 'none', + bio: 'Восстановление части здоровья.', + }, + soul_heal: { + name: 'Душевное исцеление', + cost: 4, + attack: 2, + health: 4, + type: 'minion', + faction: 'neutral', + text: 'За каждую убитую карту противника восстанавливает +1 HP герою.', + art: 'soul', + onEnemyDeath: 'heal_1_per_kill', + bio: 'Черпает силу из смерти врагов.', + }, + // Карты с броней + shield_card: { + name: 'Щит', + cost: 3, + type: 'spell', + faction: 'neutral', + text: 'Даёт +10 брони игроку.', + art: 'shield', + spellEffect: 'add_armor_10', + spellTarget: 'none', + bio: 'Защитный щит для героя.', + }, + energy_shield: { + name: 'Энергетический щит', + cost: 4, + attack: 0, + health: 3, + type: 'minion', + faction: 'neutral', + text: 'Поглощает первый урон каждого хода.', + art: 'energy', + shieldEffect: 'absorb_first_damage', + bio: 'Щит, который поглощает первый удар.', + }, + jedi_barrier: { + name: 'Барьер джедая', + cost: 5, + attack: 2, + health: 5, + type: 'minion', + faction: 'rebellion', + text: 'Пока на поле джедай — броня восстанавливается на +2 за ход.', + art: 'barrier', + fieldEffect: 'armor_regen_2_if_jedi', + bio: 'Защита джедаев восстанавливается.', + }, + droid_armor: { + name: 'Броня дроида', + cost: 3, + attack: 1, + health: 4, + type: 'minion', + faction: 'neutral', + text: 'Мех-юниты имеют удвоенную броню.', + art: 'droid', + aura: 'double_armor_mechs', + bio: 'Усиленная защита для механических существ.', + }, + imperial_shield_generator: { + name: 'Имперский Щитогенератор', + cost: 6, + attack: 0, + health: 6, + type: 'minion', + faction: 'empire', + text: 'Даёт +15 брони. Каждые 2 хода +5 брони. Урон по герою -50%.', + art: 'generator', + battlecry: 'add_armor_15', + fieldEffect: 'armor_regen_5_every_2_turns', + damageReduction: 0.5, + bio: 'Мощный генератор щитов Империи.', + }, + // Карта героя с двойной атакой + double_strike_hero: { + name: 'Мастер двойного удара', + cost: 4, + attack: 6, + health: 2, + type: 'minion', + faction: 'neutral', + text: 'Может атаковать дважды, но урон делится. После двух ударов умирает.', + art: 'striker', + canAttackTwice: true, + divideDamage: true, + diesAfterAttacks: 2, + legendary: true, + bio: 'Герой, который может нанести два удара, но каждый удар слабее.', + }, }; diff --git a/public/game.js b/public/game.js index a3c7b29..fbc5ee7 100644 --- a/public/game.js +++ b/public/game.js @@ -444,6 +444,7 @@
${escapeHtml(name)}${isDead ? ' ' : ''}
❤ ${p.health ?? 30} + ${(p.armor || 0) > 0 ? `🛡 ${p.armor}` : ''} 🔵 ${p.mana ?? 0}/${p.maxMana ?? 0} ${p.deck && p.deck.length > 0 ? `📚 ${p.deck.length}` : ''}
@@ -779,6 +780,18 @@ setTimeout(() => healthEl.classList.remove('health-changed'), 500); } } + // Отображение брони + const armorEl = $('your-armor'); + const armorDisplayEl = $('armor-display'); + if (armorEl && armorDisplayEl) { + const armor = state.isSpectator ? 0 : (state.yourArmor || you?.armor || 0); + armorEl.textContent = armor; + if (armor > 0) { + armorDisplayEl.style.display = 'inline-flex'; + } else { + armorDisplayEl.style.display = 'none'; + } + } $('your-deck').textContent = state.yourDeckCount ?? you.deck?.length ?? 0; const lastLog = state.log && state.log.length ? state.log[state.log.length - 1] : null; diff --git a/public/index.html b/public/index.html index a5cdd52..9a7e1fc 100644 --- a/public/index.html +++ b/public/index.html @@ -197,6 +197,10 @@ 30 + 0 в колоде
diff --git a/public/styles.css b/public/styles.css index 2d7fadc..ef174e2 100644 --- a/public/styles.css +++ b/public/styles.css @@ -735,6 +735,9 @@ html, body { z-index: 11; } .opponent-name { font-weight: 700; color: var(--cyan); margin-bottom: 0.35rem; font-size: 0.95rem; } +.opponent-armor { color: #5eb3e8; font-weight: 600; } +.armor-display { color: #5eb3e8; font-weight: 600; margin-left: 0.5rem; } +.armor-icon { color: #5eb3e8; } .opponent-name.defeated-name { text-decoration: line-through; color: #666; @@ -2421,6 +2424,7 @@ html, body { width: 100%; justify-content: space-between; flex-wrap: wrap; + gap: 0.4rem; gap: 0.5rem; } @@ -2431,18 +2435,25 @@ html, body { } .btn { - min-height: 44px; - padding: 0.85rem 1.5rem; - font-size: 1rem; + min-height: 40px; + padding: 0.6rem 1.2rem; + font-size: 0.9rem; touch-action: manipulation; } .btn-end-turn { - min-height: 48px; - padding: 0.7rem 1.6rem; - font-size: 1.05rem; + min-height: 40px; + padding: 0.5rem 1rem; + font-size: 0.9rem; flex: 1; - min-width: 120px; + min-width: 100px; + } + + .btn-forge, .btn-hero-ability, .btn-draw-card { + min-height: 38px; + padding: 0.45rem 0.9rem; + font-size: 0.85rem; + min-width: 70px; } .btn-instructions, .btn-settings { @@ -2454,19 +2465,43 @@ html, body { } .card-wrap { - width: 140px; - height: 196px; + width: 120px; + height: 168px; touch-action: manipulation; } .board .card-wrap { - width: 100px; - height: 140px; + width: 85px; + height: 119px; } .opponents .card-wrap { - width: 100px; - height: 140px; + width: 85px; + height: 119px; + } + + /* Уменьшаем текст на картах для средних мобильных */ + .card-name { + font-size: 0.7rem !important; + } + + .card-cost { + font-size: 0.8rem !important; + width: 20px !important; + height: 20px !important; + } + + .card-stats { + font-size: 0.75rem !important; + } + + .card-stats .atk, .card-stats .hp { + font-size: 0.75rem !important; + } + + .card-text { + font-size: 0.65rem !important; + -webkit-line-clamp: 3 !important; } .opponents { @@ -2491,7 +2526,7 @@ html, body { } .hand .card-wrap { - margin-left: -30px; + margin-left: -25px; scroll-snap-align: start; flex-shrink: 0; } @@ -2511,19 +2546,19 @@ html, body { } .deck-fan { - width: 90px; - height: 126px; + width: 80px; + height: 112px; flex-shrink: 0; } .deck-stack { - width: 85px; - height: 120px; + width: 75px; + height: 105px; } .deck-stack .deck-card { - width: 85px; - height: 120px; + width: 75px; + height: 105px; } .board { @@ -2704,54 +2739,224 @@ html, body { padding: 1rem; } + /* Уменьшаем карты */ .card-wrap { - width: 120px; - height: 168px; + width: 100px; + height: 140px; } .board .card-wrap { - width: 85px; - height: 119px; + width: 70px; + height: 98px; } .opponents .card-wrap { - width: 85px; - height: 119px; + width: 70px; + height: 98px; } .hand .card-wrap { - margin-left: -35px; + margin-left: -25px; } .hand-container { - min-height: 160px; + min-height: 140px; + padding: 0.25rem; } + .hand { + gap: 0.2rem; + padding: 0.25rem; + } + + /* Уменьшаем колоду */ + .deck-fan { + width: 70px; + height: 98px; + } + + .deck-stack { + width: 65px; + height: 91px; + } + + .deck-stack .deck-card { + width: 65px; + height: 91px; + } + + /* Уменьшаем заголовок игры */ .game-header { - padding: 0.5rem; - font-size: 0.9rem; + padding: 0.4rem; + font-size: 0.75rem; + gap: 0.3rem; } + .header-left, .header-center, .header-right { + gap: 0.3rem; + } + + /* Маленькие кнопки */ .btn { - padding: 0.75rem 1.2rem; - font-size: 0.95rem; + padding: 0.4rem 0.7rem; + font-size: 0.75rem; + min-height: 36px; + border-radius: 6px; } + .btn-end-turn { + padding: 0.4rem 0.8rem; + font-size: 0.8rem; + min-height: 36px; + min-width: 80px; + } + + .btn-forge, .btn-hero-ability, .btn-draw-card { + padding: 0.35rem 0.6rem; + font-size: 0.7rem; + min-height: 32px; + min-width: 60px; + } + + .btn-instructions, .btn-settings { + width: 32px; + height: 32px; + font-size: 0.9rem; + min-width: 32px; + min-height: 32px; + } + + /* Уменьшаем текст на картах */ + .card-name { + font-size: 0.6rem !important; + line-height: 1.1 !important; + } + + .card-cost { + font-size: 0.75rem !important; + width: 18px !important; + height: 18px !important; + } + + .card-stats { + font-size: 0.7rem !important; + gap: 0.3rem !important; + } + + .card-stats .atk, .card-stats .hp { + font-size: 0.7rem !important; + } + + .card-text { + font-size: 0.55rem !important; + line-height: 1.2 !important; + -webkit-line-clamp: 3 !important; + margin-bottom: 0.2rem !important; + } + + .card-info { + padding: 0.3rem !important; + } + + /* Уменьшаем доски */ + .board { + min-height: 100px; + padding: 0.3rem; + } + + .your-board { + min-height: 90px; + padding: 0.3rem; + } + + .opponent-block { + padding: 0.3rem; + } + + .opponent-name { + font-size: 0.75rem; + } + + .opponent-stats { + font-size: 0.7rem; + } + + /* Уменьшаем статистику игрока */ + #your-mana, #your-max-mana, #your-health { + font-size: 1rem; + } + + #your-name { + font-size: 0.85rem; + } + + /* Уменьшаем анонсы */ .attack-announcement { - font-size: 1.5rem; - padding: 1.5rem 2rem; + font-size: 1.2rem; + padding: 1rem 1.5rem; } + /* Уменьшаем модальные окна */ .modal { - padding: 1rem; + padding: 0.75rem; + max-width: 95vw; } + .modal h2 { + font-size: 1.1rem; + } + + .modal p { + font-size: 0.85rem; + } + + /* Галерея карт */ .cards-gallery-grid { - grid-template-columns: repeat(auto-fill, minmax(85px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(70px, 1fr)); + gap: 0.5rem; } .gallery-card { - width: 85px !important; - height: 119px !important; + width: 70px !important; + height: 98px !important; + } + + /* Уменьшаем чат */ + .chat-container { + width: calc(100vw - 10px); + right: 5px; + bottom: 5px; + max-height: 250px; + } + + .chat-content { + height: 200px; + } + + .chat-messages { + font-size: 0.75rem; + padding: 0.5rem; + } + + /* Уменьшаем кузницу */ + .forge-sidebar { + padding: 0.5rem; + } + + .forge-cards-list .card-wrap { + width: 80px; + height: 112px; + } + + /* Уменьшаем лог игры */ + #game-log { + font-size: 0.7rem; + max-width: 150px; + } + + /* Уменьшаем таймер */ + .turn-timer { + font-size: 0.75rem; + padding: 0.3rem 0.6rem; } } diff --git a/server.js b/server.js index 6aa41e8..b998ee8 100644 --- a/server.js +++ b/server.js @@ -52,6 +52,32 @@ function cleanupEmptyRooms() { // Периодическая очистка пустых комнат setInterval(cleanupEmptyRooms, 30000); // каждые 30 секунд +// Функция для применения урона с учетом брони +function applyDamageToPlayer(player, damage) { + if (damage <= 0) return; + + // Проверка на Droid Armor - удваиваем броню для мех-юнитов + // (это обрабатывается при создании карты, но можно добавить проверку здесь) + + // Сначала урон идет на броню + if (player.armor > 0) { + const armorDamage = Math.min(player.armor, damage); + player.armor -= armorDamage; + damage -= armorDamage; + } + + // Оставшийся урон идет на HP + if (damage > 0) { + player.health = Math.max(0, player.health - damage); + } +} + +// Функция для лечения игрока +function healPlayer(player, amount) { + if (amount <= 0) return; + player.health = Math.min(30, player.health + amount); +} + function shuffle(a) { for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); @@ -113,6 +139,7 @@ function initGame(room) { mana: 0, maxMana: 0, health: 30, + armor: 0, // Система брони hero: (p.hero || ['luke', 'vader', 'yoda', 'leia', 'rey', 'kylo', 'mace'][i % 7]), manualDrawUsed: false, fatigueCounter: 0, @@ -140,6 +167,7 @@ function initGame(room) { mana: 0, maxMana: 0, health: 30, + armor: 0, // Система брони hero: 'vader', manualDrawUsed: false, fatigueCounter: 0, @@ -473,7 +501,7 @@ function endTurn(room) { const enemies = gameState.players.filter((pl, i) => i !== prev && pl.health > 0); if (enemies.length) { const target = enemies[Math.floor(Math.random() * enemies.length)]; - target.health = Math.max(0, target.health - 3); + applyDamageToPlayer(target, 3); gameState.log.push({ type: 'structure', effect: 'death_star_damage', fromPlayer: prev, toPlayer: gameState.players.indexOf(target), damage: 3 }); } } else if (card.name === 'Кантина Мос-Эйсли') { @@ -506,7 +534,7 @@ function endTurn(room) { const enemies = gameState.players.filter((pl, i) => i !== prev && pl.health > 0); if (enemies.length) { const target = enemies[Math.floor(Math.random() * enemies.length)]; - target.health = Math.max(0, target.health - 2); + applyDamageToPlayer(target, 2); gameState.log.push({ type: 'planet', effect: 'korriban_damage', fromPlayer: prev, toPlayer: gameState.players.indexOf(target), damage: 2 }); } } else if (card.name === 'Tatooine') { @@ -525,7 +553,7 @@ function endTurn(room) { } else if (card.name === 'Zakuul') { const enemies = gameState.players.filter((pl, i) => i !== prev && pl.health > 0); enemies.forEach((enemy) => { - enemy.health = Math.max(0, enemy.health - 3); + applyDamageToPlayer(enemy, 3); }); gameState.log.push({ type: 'planet', effect: 'zakuul_damage', fromPlayer: prev }); } else if (card.name === 'Kamino') { @@ -536,7 +564,7 @@ function endTurn(room) { } else if (card.name === 'Mustafar') { const enemies = gameState.players.filter((pl, i) => i !== prev && pl.health > 0); enemies.forEach((enemy) => { - enemy.health = Math.max(0, enemy.health - 1); + applyDamageToPlayer(enemy, 1); if (enemy.board) { enemy.board.forEach((min) => { min.health = Math.max(0, min.health - 1); @@ -546,8 +574,33 @@ function endTurn(room) { }); gameState.log.push({ type: 'planet', effect: 'mustafar_damage', fromPlayer: prev }); } else if (card.name === 'Naboo') { - prevPlayer.health = Math.min(30, prevPlayer.health + 2); + healPlayer(prevPlayer, 2); gameState.log.push({ type: 'planet', effect: 'naboo_heal', playerIndex: prev }); + } + // Обработка эффектов карт с хилом и броней + else if (card.fieldEffect === 'heal_1_per_turn') { + healPlayer(prevPlayer, 1); + gameState.log.push({ type: 'fieldEffect', effect: 'heal_1_per_turn', playerIndex: prev }); + } else if (card.fieldEffect === 'armor_regen_2_if_jedi') { + // Проверяем, есть ли джедай на поле + const hasJedi = prevPlayer.board.some(min => { + const minCard = getCard(room, min.cardId); + return minCard && (minCard.name?.includes('Jedi') || + minCard.id === 'luke' || minCard.id === 'obiwan' || + minCard.id === 'ahsoka' || minCard.id === 'mace' || + minCard.id === 'yoda' || minCard.id === 'anakin'); + }); + if (hasJedi) { + prevPlayer.armor = (prevPlayer.armor || 0) + 2; + gameState.log.push({ type: 'fieldEffect', effect: 'armor_regen_2_if_jedi', playerIndex: prev }); + } + } else if (card.fieldEffect === 'armor_regen_5_every_2_turns') { + // Каждые 2 хода восстанавливаем +5 брони + const turnCount = gameState.turn || 0; + if (turnCount % 2 === 0) { + prevPlayer.armor = (prevPlayer.armor || 0) + 5; + gameState.log.push({ type: 'fieldEffect', effect: 'armor_regen_5_every_2_turns', playerIndex: prev }); + } } else if (card.name === 'Endor') { if (prevPlayer.hand.length < 10) { prevPlayer.hand.push('ewok'); @@ -582,12 +635,16 @@ function endTurn(room) { m.attacksUsed = 0; // Сбрасываем счётчик атак для двойной атаки } } + // Сбрасываем флаг использования Energy Shield + if (card && card.shieldEffect === 'absorb_first_damage') { + m.shieldUsedThisTurn = false; + } }); np.manualDrawUsed = false; np.heroAbilityUsed = false; if (np.deck.length === 0) { np.fatigueCounter = (np.fatigueCounter || 0) + 1; - np.health = Math.max(0, np.health - np.fatigueCounter); + applyDamageToPlayer(np, np.fatigueCounter); gameState.log.push({ type: 'fatigue', playerIndex: next, damage: np.fatigueCounter }); } else { const drawn = drawCards(np.deck, 1); @@ -623,7 +680,7 @@ function manualDraw(room, socketId) { p.manualDrawUsed = true; p.fatigueCounter = (p.fatigueCounter || 0) + 1; const dmg = p.fatigueCounter; - p.health = Math.max(0, p.health - dmg); + applyDamageToPlayer(p, dmg); gameState.log.push({ type: 'fatigue', playerIndex: pi, damage: dmg }); checkGameOver(room); broadcastGameState(room); @@ -644,12 +701,12 @@ function runBattlecry(room, card, playerIndex) { if (id === 'deal_1_hero' && enemies.length) { const t = enemies[Math.floor(Math.random() * enemies.length)]; const idx = gameState.players.indexOf(t); - t.health = Math.max(0, t.health - 1); + applyDamageToPlayer(t, 1); gameState.log.push({ type: 'battlecry', effect: 'deal_1_hero', fromPlayer: playerIndex, toPlayer: idx, damage: 1 }); } else if (id === 'deal_2_hero' && enemies.length) { const t = enemies[Math.floor(Math.random() * enemies.length)]; const idx = gameState.players.indexOf(t); - t.health = Math.max(0, t.health - 2); + applyDamageToPlayer(t, 2); gameState.log.push({ type: 'battlecry', effect: 'deal_2_hero', fromPlayer: playerIndex, toPlayer: idx, damage: 2 }); } else if (id === 'draw_1') { const drawn = drawCards(p.deck, 1); @@ -689,10 +746,10 @@ function runBattlecry(room, card, playerIndex) { } } } else if (id === 'heal_hero_2') { - p.health = Math.min(30, p.health + 2); + healPlayer(p, 2); gameState.log.push({ type: 'battlecry', effect: 'heal_hero_2', playerIndex }); } else if (id === 'heal_hero_3') { - p.health = Math.min(30, p.health + 3); + healPlayer(p, 3); gameState.log.push({ type: 'battlecry', effect: 'heal_hero_3', playerIndex }); } else if (id === 'buff_all_friendly_attack') { p.board.forEach((m) => { @@ -701,7 +758,7 @@ function runBattlecry(room, card, playerIndex) { gameState.log.push({ type: 'battlecry', effect: 'buff_all_friendly_attack', playerIndex }); } else if (id === 'deal_3_all_enemies') { enemies.forEach((e) => { - e.health = Math.max(0, e.health - 3); + applyDamageToPlayer(e, 3); }); gameState.log.push({ type: 'battlecry', effect: 'deal_3_all_enemies', fromPlayer: playerIndex }); } else if (id === 'deal_3_any') { @@ -718,7 +775,7 @@ function runBattlecry(room, card, playerIndex) { runDeathrattle(room, getCard(room, target.cardId), idx); } } else { - t.health = Math.max(0, t.health - 3); + applyDamageToPlayer(t, 3); gameState.log.push({ type: 'battlecry', effect: 'deal_3_any', fromPlayer: playerIndex, toPlayer: idx, damage: 3 }); } } @@ -763,7 +820,7 @@ function runBattlecry(room, card, playerIndex) { gameState.log.push({ type: 'battlecry', effect: 'buff_2_2', playerIndex, toIdx: boardIdx }); } } else if (id === 'heal_hero_5') { - p.health = Math.min(30, p.health + 5); + healPlayer(p, 5); gameState.log.push({ type: 'battlecry', effect: 'heal_hero_5', playerIndex }); } else if (id === 'return_hand_enemy') { // Для Ezra Bridger требуется выбор конкретного игрока @@ -808,8 +865,8 @@ function runBattlecry(room, card, playerIndex) { } else if (id === 'random_effect') { const effects = [ () => { const drawn = drawCards(p.deck, 1); if (drawn.length) p.hand.push(...drawn); gameState.log.push({ type: 'battlecry', effect: 'random_draw', playerIndex }); }, - () => { if (enemies.length) { const t = enemies[Math.floor(Math.random() * enemies.length)]; t.health = Math.max(0, t.health - 1); gameState.log.push({ type: 'battlecry', effect: 'random_damage', fromPlayer: playerIndex, toPlayer: gameState.players.indexOf(t), damage: 1 }); } }, - () => { p.health = Math.min(30, p.health + 1); gameState.log.push({ type: 'battlecry', effect: 'random_heal', playerIndex }); } + () => { if (enemies.length) { const t = enemies[Math.floor(Math.random() * enemies.length)]; applyDamageToPlayer(t, 1); gameState.log.push({ type: 'battlecry', effect: 'random_damage', fromPlayer: playerIndex, toPlayer: gameState.players.indexOf(t), damage: 1 }); } }, + () => { healPlayer(p, 1); gameState.log.push({ type: 'battlecry', effect: 'random_heal', playerIndex }); } ]; const randomEffect = effects[Math.floor(Math.random() * effects.length)]; randomEffect(); @@ -820,6 +877,7 @@ function runBattlecry(room, card, playerIndex) { const target = enemy.board[Math.floor(Math.random() * enemy.board.length)]; target.health = Math.max(0, target.health - 2); const boardIdx = enemy.board.indexOf(target); + applyDamageToPlayer(enemy, 2); gameState.log.push({ type: 'battlecry', effect: 'deal_2_or_4_hero', fromPlayer: playerIndex, toPlayer: enemyIdx, toIdx: boardIdx, damage: 2 }); if (target.health <= 0) { enemy.board.splice(boardIdx, 1); @@ -827,6 +885,7 @@ function runBattlecry(room, card, playerIndex) { } } else { enemy.health = Math.max(0, enemy.health - 4); + applyDamageToPlayer(enemy, 4); gameState.log.push({ type: 'battlecry', effect: 'deal_2_or_4_hero', fromPlayer: playerIndex, toPlayer: enemyIdx, damage: 4 }); } } else if (id === 'deal_2_all_enemy_minions') { @@ -899,15 +958,15 @@ function runDeathrattle(room, card, ownerIndex) { gameState.log.push({ type: 'deathrattle', effect: 'draw_1', playerIndex: ownerIndex }); } else if (id === 'deal_2_all_enemies') { enemies.forEach((e) => { - e.health = Math.max(0, e.health - 2); + applyDamageToPlayer(e, 2); }); gameState.log.push({ type: 'deathrattle', effect: 'deal_2_all_enemies', fromPlayer: ownerIndex }); } else if (id === 'deal_1_random_enemy' && enemies.length) { const t = enemies[Math.floor(Math.random() * enemies.length)]; - t.health = Math.max(0, t.health - 1); + applyDamageToPlayer(t, 1); gameState.log.push({ type: 'deathrattle', effect: 'deal_1_random_enemy', fromPlayer: ownerIndex }); } else if (id === 'heal_hero_2') { - owner.health = Math.min(30, owner.health + 2); + healPlayer(owner, 2); gameState.log.push({ type: 'deathrattle', effect: 'heal_hero_2', playerIndex: ownerIndex }); } else if (id === 'draw_3') { const drawn = drawCards(owner.deck, 3); @@ -915,11 +974,11 @@ function runDeathrattle(room, card, ownerIndex) { gameState.log.push({ type: 'deathrattle', effect: 'draw_3', playerIndex: ownerIndex }); } else if (id === 'deal_4_random' && enemies.length) { const t = enemies[Math.floor(Math.random() * enemies.length)]; - t.health = Math.max(0, t.health - 4); + applyDamageToPlayer(t, 4); gameState.log.push({ type: 'deathrattle', effect: 'deal_4_random', fromPlayer: ownerIndex, toPlayer: gameState.players.indexOf(t), damage: 4 }); } else if (id === 'deal_3_random' && enemies.length) { const t = enemies[Math.floor(Math.random() * enemies.length)]; - t.health = Math.max(0, t.health - 3); + applyDamageToPlayer(t, 3); gameState.log.push({ type: 'deathrattle', effect: 'deal_3_random', fromPlayer: ownerIndex, toPlayer: gameState.players.indexOf(t), damage: 3 }); } } @@ -1011,10 +1070,17 @@ function playCard(room, socketId, handIndex, boardPos) { health: card.health, maxHealth: card.health, canAttack: card.attack > 0, + attacksUsed: 0, // Для двойной атаки }; p.board.splice(typeof boardPos === 'number' ? boardPos : p.board.length, 0, minion); gameState.log.push({ type: 'play', playerIndex: pi, cardId: cid, minionId: minion.id }); + // Обработка battlecry для карт с броней + if (card.battlecry === 'add_armor_15') { + p.armor = (p.armor || 0) + 15; + gameState.log.push({ type: 'battlecry', effect: 'add_armor_15', playerIndex: pi }); + } + // Для battlecry, требующих выбора цели (Ezra Bridger), отправляем запрос клиенту if (card.battlecryId === 'return_hand_enemy') { const enemies = gameState.players.filter((pl, i) => i !== pi && pl.health > 0); @@ -1261,6 +1327,21 @@ function applySynergies(room) { } } + // Аура: Droid Armor - мех-юниты имеют удвоенную броню + if (card.aura === 'double_armor_mechs') { + // Удваиваем броню игрока, если есть мех-юниты на поле + const hasMechs = player.board.some(other => { + const otherCard = cardDb[other.cardId]; + return otherCard && (otherCard.id === 'r2d2' || otherCard.id === 'c3po' || + otherCard.id === 'bb8' || otherCard.id === 'droid' || + otherCard.name?.includes('Droid') || otherCard.name?.includes('дроид')); + }); + if (hasMechs) { + // Броня уже применена, но можно добавить бонус + // Это будет обрабатываться при применении урона + } + } + // Йода даёт +1/+1 всем джедаям if (card.id === 'yoda' || card.name === 'Yoda') { player.board.forEach((other, otherIdx) => { @@ -1471,7 +1552,7 @@ function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardInde if (targetBoardIndex === -1) { const t = gameState.players[targetPlayerIndex]; if (!t || t.health <= 0) return; - t.health = Math.max(0, t.health - 2); + applyDamageToPlayer(t, 2); gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, toPlayer: targetPlayerIndex, effect: 'deal_2_hero', damage: 2 }); } else { const t = gameState.players[targetPlayerIndex]; @@ -1498,7 +1579,7 @@ function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardInde } else if (eff === 'deal_1_all_enemies') { gameState.players.forEach((pl, i) => { if (i === pi) return; - pl.health = Math.max(0, pl.health - 1); + applyDamageToPlayer(pl, 1); pl.board.forEach((m) => { m.health -= 1; if (m.health <= 0) { @@ -1526,7 +1607,7 @@ function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardInde if (targetBoardIndex === -1) { const t = gameState.players[targetPlayerIndex]; if (!t || t.health <= 0) return; - t.health = Math.max(0, t.health - 4); + applyDamageToPlayer(t, 4); gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, toPlayer: targetPlayerIndex, effect: 'deal_4_hero', damage: 4 }); } else { const t = gameState.players[targetPlayerIndex]; @@ -1544,7 +1625,7 @@ function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardInde if (targetBoardIndex === -1) { const t = gameState.players[targetPlayerIndex]; if (!t) return; - t.health = Math.min(30, t.health + 4); + healPlayer(t, 4); gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, toPlayer: targetPlayerIndex, effect: 'heal_4_hero' }); } else { const t = gameState.players[targetPlayerIndex]; @@ -1583,6 +1664,16 @@ function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardInde m.maxHealth = (m.maxHealth || m.health) + 1; }); gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, effect: 'buff_all_friendly' }); + } else if (eff === 'heal_hero_5') { + healPlayer(p, 5); + gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, effect: 'heal_hero_5' }); + } else if (eff === 'heal_hero_30pct') { + const healAmount = Math.floor(p.health * 0.3); + healPlayer(p, healAmount); + gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, effect: 'heal_hero_30pct', amount: healAmount }); + } else if (eff === 'add_armor_10') { + p.armor = (p.armor || 0) + 10; + gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, effect: 'add_armor_10' }); } else if (eff === 'destroy_weak_2') { enemies.forEach((enemy) => { if (enemy.board && enemy.board.length > 0) { @@ -1763,7 +1854,7 @@ function heroAbility(room, socketId, targetPlayerIndex, targetBoardIndex) { const targetPlayer = gameState.players[targetPlayerIndex]; if (!targetPlayer) return; if (targetBoardIndex === -1) { - targetPlayer.health = Math.max(0, targetPlayer.health - 1); + applyDamageToPlayer(targetPlayer, 1); gameState.log.push({ type: 'heroAbility', fromPlayer: pi, toPlayer: targetPlayerIndex, effect: 'deal_1_hero' }); } else { const m = targetPlayer.board[targetBoardIndex]; @@ -1809,14 +1900,50 @@ function attack(room, socketId, attackerBoardIndex, targetPlayerIndex, targetBoa applySynergies(room); if (targetBoardIndex === -1) { - const attackerAttack = attacker.attack + (attacker.synergyAttackBonus || 0); - targetPlayer.health = Math.max(0, targetPlayer.health - attackerAttack); + let attackerAttack = attacker.attack + (attacker.synergyAttackBonus || 0); + const attackerCard = getCard(room, attacker.cardId); + + // Если карта делит урон при двойной атаке + if (attackerCard && attackerCard.divideDamage && attackerCard.canAttackTwice) { + const attacksUsed = attacker.attacksUsed || 0; + if (attacksUsed > 0) { + // Второй удар - урон делится + attackerAttack = Math.floor(attackerAttack / 2); + } + } + + // Проверка на уменьшение урона (Имперский Щитогенератор) + const shieldGen = targetPlayer.board.find(m => { + const card = getCard(room, m.cardId); + return card && card.damageReduction; + }); + if (shieldGen) { + const card = getCard(room, shieldGen.cardId); + if (card && card.damageReduction) { + attackerAttack = Math.floor(attackerAttack * (1 - card.damageReduction)); + } + } + + // Проверка на Energy Shield - поглощает первый урон каждого хода + const energyShield = targetPlayer.board.find(m => { + const card = getCard(room, m.cardId); + return card && card.shieldEffect === 'absorb_first_damage'; + }); + if (energyShield && !energyShield.shieldUsedThisTurn) { + energyShield.shieldUsedThisTurn = true; + attackerAttack = 0; // Полностью поглощается + gameState.log.push({ type: 'shieldAbsorb', playerIndex: targetPlayerIndex, minionId: energyShield.id }); + } + + const oldArmor = targetPlayer.armor || 0; + applyDamageToPlayer(targetPlayer, attackerAttack); gameState.log.push({ type: 'attackHero', fromPlayer: pi, toPlayer: targetPlayerIndex, attackerMinionId: attacker.id, - damage: attacker.attack, + damage: attackerAttack, + armorBlocked: oldArmor - (targetPlayer.armor || 0), }); // Применяем синергии перед расчётом урона applySynergies(room); @@ -1852,8 +1979,21 @@ function attack(room, socketId, attackerBoardIndex, targetPlayerIndex, targetBoa }); const targetCard = getCard(room, target.cardId); const attackerCard = getCard(room, attacker.cardId); - if (targetDied && targetCard && targetCard.deathrattleId) { - runDeathrattle(room, targetCard, targetPlayerIndex); + + // Обработка Soul Heal - хил за каждую убитую карту противника + if (targetDied) { + // Проверяем, есть ли у атакующего игрока карта Soul Heal + p.board.forEach((minion) => { + const minionCard = getCard(room, minion.cardId); + if (minionCard && minionCard.onEnemyDeath === 'heal_1_per_kill') { + healPlayer(p, 1); + gameState.log.push({ type: 'soulHeal', playerIndex: pi, healed: 1 }); + } + }); + + if (targetCard && targetCard.deathrattleId) { + runDeathrattle(room, targetCard, targetPlayerIndex); + } } if (attackerDied && attackerCard && attackerCard.deathrattleId) { runDeathrattle(room, attackerCard, pi); @@ -1862,8 +2002,19 @@ function attack(room, socketId, attackerBoardIndex, targetPlayerIndex, targetBoa // Механика двойной атаки const attackerCard = getCard(room, attacker.cardId); - if (attackerCard && attackerCard.canAttackTwice && !attacker.attacksUsed) { - attacker.attacksUsed = 1; // Использована одна атака + if (attackerCard && attackerCard.canAttackTwice) { + attacker.attacksUsed = (attacker.attacksUsed || 0) + 1; + // Если карта умирает после определенного количества атак + if (attackerCard.diesAfterAttacks && attacker.attacksUsed >= attackerCard.diesAfterAttacks) { + attacker.health = 0; // Убиваем карту + gameState.log.push({ type: 'minionDeath', minionId: attacker.id, reason: 'diesAfterAttacks' }); + } else if (attacker.attacksUsed >= 2) { + attacker.canAttack = false; + attacker.attacksUsed = 0; + } else { + // Может атаковать еще раз + attacker.canAttack = true; + } } else { attacker.canAttack = false; attacker.attacksUsed = 0;