This commit is contained in:
2026-01-27 22:52:44 +03:00
parent dedc2635f2
commit 14d46962d4
5 changed files with 569 additions and 71 deletions

125
cards.js
View File

@ -2796,4 +2796,129 @@ module.exports = {
battlecryId: 'deal_2_minion', battlecryId: 'deal_2_minion',
bio: 'Висас Марр — мираллука, бывшая ученица Дарта Нихилуса. Была единственной выжившей после уничтожения её родной планеты Катамии Нихилусом. Была послана убить Изгнанника-джедая, но была спасена и вернулась к свету. Её слепота и связь с Силой делали её уникальной. Её искупление стало важной частью её истории. Её навыки боя на световых мечах были впечатляющими.', 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: 'Герой, который может нанести два удара, но каждый удар слабее.',
},
}; };

View File

@ -444,6 +444,7 @@
<div class="opponent-name ${isDead ? 'defeated-name' : ''}">${escapeHtml(name)}${isDead ? ' <span class="defeated-badge">✝</span>' : ''}</div> <div class="opponent-name ${isDead ? 'defeated-name' : ''}">${escapeHtml(name)}${isDead ? ' <span class="defeated-badge">✝</span>' : ''}</div>
<div class="opponent-stats ${isDead ? 'defeated-stats' : ''}"> <div class="opponent-stats ${isDead ? 'defeated-stats' : ''}">
<span class="opponent-health">❤ ${p.health ?? 30}</span> <span class="opponent-health">❤ ${p.health ?? 30}</span>
${(p.armor || 0) > 0 ? `<span class="opponent-armor">🛡 ${p.armor}</span>` : ''}
<span>🔵 ${p.mana ?? 0}/${p.maxMana ?? 0}</span> <span>🔵 ${p.mana ?? 0}/${p.maxMana ?? 0}</span>
${p.deck && p.deck.length > 0 ? `<span>📚 ${p.deck.length}</span>` : ''} ${p.deck && p.deck.length > 0 ? `<span>📚 ${p.deck.length}</span>` : ''}
</div> </div>
@ -779,6 +780,18 @@
setTimeout(() => healthEl.classList.remove('health-changed'), 500); 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; $('your-deck').textContent = state.yourDeckCount ?? you.deck?.length ?? 0;
const lastLog = state.log && state.log.length ? state.log[state.log.length - 1] : null; const lastLog = state.log && state.log.length ? state.log[state.log.length - 1] : null;

View File

@ -197,6 +197,10 @@
<i class="swg swg-deathstar health-icon" aria-hidden="true"></i> <i class="swg swg-deathstar health-icon" aria-hidden="true"></i>
<span id="your-health">30</span> <span id="your-health">30</span>
</span> </span>
<span class="armor-display" id="armor-display" style="display: none;">
<i class="swg swg-deathstar armor-icon" aria-hidden="true"></i>
<span id="your-armor">0</span>
</span>
<span class="deck-count"><i class="swg swg-galrep deck-icon" aria-hidden="true"></i><span id="your-deck">0</span> в колоде</span> <span class="deck-count"><i class="swg swg-galrep deck-icon" aria-hidden="true"></i><span id="your-deck">0</span> в колоде</span>
</div> </div>
<div class="header-center"> <div class="header-center">

View File

@ -735,6 +735,9 @@ html, body {
z-index: 11; z-index: 11;
} }
.opponent-name { font-weight: 700; color: var(--cyan); margin-bottom: 0.35rem; font-size: 0.95rem; } .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 { .opponent-name.defeated-name {
text-decoration: line-through; text-decoration: line-through;
color: #666; color: #666;
@ -2421,6 +2424,7 @@ html, body {
width: 100%; width: 100%;
justify-content: space-between; justify-content: space-between;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.4rem;
gap: 0.5rem; gap: 0.5rem;
} }
@ -2431,18 +2435,25 @@ html, body {
} }
.btn { .btn {
min-height: 44px; min-height: 40px;
padding: 0.85rem 1.5rem; padding: 0.6rem 1.2rem;
font-size: 1rem; font-size: 0.9rem;
touch-action: manipulation; touch-action: manipulation;
} }
.btn-end-turn { .btn-end-turn {
min-height: 48px; min-height: 40px;
padding: 0.7rem 1.6rem; padding: 0.5rem 1rem;
font-size: 1.05rem; font-size: 0.9rem;
flex: 1; 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 { .btn-instructions, .btn-settings {
@ -2454,19 +2465,43 @@ html, body {
} }
.card-wrap { .card-wrap {
width: 140px; width: 120px;
height: 196px; height: 168px;
touch-action: manipulation; touch-action: manipulation;
} }
.board .card-wrap { .board .card-wrap {
width: 100px; width: 85px;
height: 140px; height: 119px;
} }
.opponents .card-wrap { .opponents .card-wrap {
width: 100px; width: 85px;
height: 140px; 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 { .opponents {
@ -2491,7 +2526,7 @@ html, body {
} }
.hand .card-wrap { .hand .card-wrap {
margin-left: -30px; margin-left: -25px;
scroll-snap-align: start; scroll-snap-align: start;
flex-shrink: 0; flex-shrink: 0;
} }
@ -2511,19 +2546,19 @@ html, body {
} }
.deck-fan { .deck-fan {
width: 90px; width: 80px;
height: 126px; height: 112px;
flex-shrink: 0; flex-shrink: 0;
} }
.deck-stack { .deck-stack {
width: 85px; width: 75px;
height: 120px; height: 105px;
} }
.deck-stack .deck-card { .deck-stack .deck-card {
width: 85px; width: 75px;
height: 120px; height: 105px;
} }
.board { .board {
@ -2704,54 +2739,224 @@ html, body {
padding: 1rem; padding: 1rem;
} }
/* Уменьшаем карты */
.card-wrap { .card-wrap {
width: 120px; width: 100px;
height: 168px; height: 140px;
} }
.board .card-wrap { .board .card-wrap {
width: 85px; width: 70px;
height: 119px; height: 98px;
} }
.opponents .card-wrap { .opponents .card-wrap {
width: 85px; width: 70px;
height: 119px; height: 98px;
} }
.hand .card-wrap { .hand .card-wrap {
margin-left: -35px; margin-left: -25px;
} }
.hand-container { .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 { .game-header {
padding: 0.5rem; padding: 0.4rem;
font-size: 0.9rem; font-size: 0.75rem;
gap: 0.3rem;
} }
.header-left, .header-center, .header-right {
gap: 0.3rem;
}
/* Маленькие кнопки */
.btn { .btn {
padding: 0.75rem 1.2rem; padding: 0.4rem 0.7rem;
font-size: 0.95rem; 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 { .attack-announcement {
font-size: 1.5rem; font-size: 1.2rem;
padding: 1.5rem 2rem; padding: 1rem 1.5rem;
} }
/* Уменьшаем модальные окна */
.modal { .modal {
padding: 1rem; padding: 0.75rem;
max-width: 95vw;
} }
.modal h2 {
font-size: 1.1rem;
}
.modal p {
font-size: 0.85rem;
}
/* Галерея карт */
.cards-gallery-grid { .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 { .gallery-card {
width: 85px !important; width: 70px !important;
height: 119px !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;
} }
} }

215
server.js
View File

@ -52,6 +52,32 @@ function cleanupEmptyRooms() {
// Периодическая очистка пустых комнат // Периодическая очистка пустых комнат
setInterval(cleanupEmptyRooms, 30000); // каждые 30 секунд 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) { function shuffle(a) {
for (let i = a.length - 1; i > 0; i--) { for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1)); const j = Math.floor(Math.random() * (i + 1));
@ -113,6 +139,7 @@ function initGame(room) {
mana: 0, mana: 0,
maxMana: 0, maxMana: 0,
health: 30, health: 30,
armor: 0, // Система брони
hero: (p.hero || ['luke', 'vader', 'yoda', 'leia', 'rey', 'kylo', 'mace'][i % 7]), hero: (p.hero || ['luke', 'vader', 'yoda', 'leia', 'rey', 'kylo', 'mace'][i % 7]),
manualDrawUsed: false, manualDrawUsed: false,
fatigueCounter: 0, fatigueCounter: 0,
@ -140,6 +167,7 @@ function initGame(room) {
mana: 0, mana: 0,
maxMana: 0, maxMana: 0,
health: 30, health: 30,
armor: 0, // Система брони
hero: 'vader', hero: 'vader',
manualDrawUsed: false, manualDrawUsed: false,
fatigueCounter: 0, fatigueCounter: 0,
@ -473,7 +501,7 @@ function endTurn(room) {
const enemies = gameState.players.filter((pl, i) => i !== prev && pl.health > 0); const enemies = gameState.players.filter((pl, i) => i !== prev && pl.health > 0);
if (enemies.length) { if (enemies.length) {
const target = enemies[Math.floor(Math.random() * 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 }); gameState.log.push({ type: 'structure', effect: 'death_star_damage', fromPlayer: prev, toPlayer: gameState.players.indexOf(target), damage: 3 });
} }
} else if (card.name === 'Кантина Мос-Эйсли') { } else if (card.name === 'Кантина Мос-Эйсли') {
@ -506,7 +534,7 @@ function endTurn(room) {
const enemies = gameState.players.filter((pl, i) => i !== prev && pl.health > 0); const enemies = gameState.players.filter((pl, i) => i !== prev && pl.health > 0);
if (enemies.length) { if (enemies.length) {
const target = enemies[Math.floor(Math.random() * 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 }); gameState.log.push({ type: 'planet', effect: 'korriban_damage', fromPlayer: prev, toPlayer: gameState.players.indexOf(target), damage: 2 });
} }
} else if (card.name === 'Tatooine') { } else if (card.name === 'Tatooine') {
@ -525,7 +553,7 @@ function endTurn(room) {
} else if (card.name === 'Zakuul') { } else if (card.name === 'Zakuul') {
const enemies = gameState.players.filter((pl, i) => i !== prev && pl.health > 0); const enemies = gameState.players.filter((pl, i) => i !== prev && pl.health > 0);
enemies.forEach((enemy) => { enemies.forEach((enemy) => {
enemy.health = Math.max(0, enemy.health - 3); applyDamageToPlayer(enemy, 3);
}); });
gameState.log.push({ type: 'planet', effect: 'zakuul_damage', fromPlayer: prev }); gameState.log.push({ type: 'planet', effect: 'zakuul_damage', fromPlayer: prev });
} else if (card.name === 'Kamino') { } else if (card.name === 'Kamino') {
@ -536,7 +564,7 @@ function endTurn(room) {
} else if (card.name === 'Mustafar') { } else if (card.name === 'Mustafar') {
const enemies = gameState.players.filter((pl, i) => i !== prev && pl.health > 0); const enemies = gameState.players.filter((pl, i) => i !== prev && pl.health > 0);
enemies.forEach((enemy) => { enemies.forEach((enemy) => {
enemy.health = Math.max(0, enemy.health - 1); applyDamageToPlayer(enemy, 1);
if (enemy.board) { if (enemy.board) {
enemy.board.forEach((min) => { enemy.board.forEach((min) => {
min.health = Math.max(0, min.health - 1); 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 }); gameState.log.push({ type: 'planet', effect: 'mustafar_damage', fromPlayer: prev });
} else if (card.name === 'Naboo') { } 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 }); 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') { } else if (card.name === 'Endor') {
if (prevPlayer.hand.length < 10) { if (prevPlayer.hand.length < 10) {
prevPlayer.hand.push('ewok'); prevPlayer.hand.push('ewok');
@ -582,12 +635,16 @@ function endTurn(room) {
m.attacksUsed = 0; // Сбрасываем счётчик атак для двойной атаки m.attacksUsed = 0; // Сбрасываем счётчик атак для двойной атаки
} }
} }
// Сбрасываем флаг использования Energy Shield
if (card && card.shieldEffect === 'absorb_first_damage') {
m.shieldUsedThisTurn = false;
}
}); });
np.manualDrawUsed = false; np.manualDrawUsed = false;
np.heroAbilityUsed = false; np.heroAbilityUsed = false;
if (np.deck.length === 0) { if (np.deck.length === 0) {
np.fatigueCounter = (np.fatigueCounter || 0) + 1; 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 }); gameState.log.push({ type: 'fatigue', playerIndex: next, damage: np.fatigueCounter });
} else { } else {
const drawn = drawCards(np.deck, 1); const drawn = drawCards(np.deck, 1);
@ -623,7 +680,7 @@ function manualDraw(room, socketId) {
p.manualDrawUsed = true; p.manualDrawUsed = true;
p.fatigueCounter = (p.fatigueCounter || 0) + 1; p.fatigueCounter = (p.fatigueCounter || 0) + 1;
const dmg = p.fatigueCounter; const dmg = p.fatigueCounter;
p.health = Math.max(0, p.health - dmg); applyDamageToPlayer(p, dmg);
gameState.log.push({ type: 'fatigue', playerIndex: pi, damage: dmg }); gameState.log.push({ type: 'fatigue', playerIndex: pi, damage: dmg });
checkGameOver(room); checkGameOver(room);
broadcastGameState(room); broadcastGameState(room);
@ -644,12 +701,12 @@ function runBattlecry(room, card, playerIndex) {
if (id === 'deal_1_hero' && enemies.length) { if (id === 'deal_1_hero' && enemies.length) {
const t = enemies[Math.floor(Math.random() * enemies.length)]; const t = enemies[Math.floor(Math.random() * enemies.length)];
const idx = gameState.players.indexOf(t); 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 }); gameState.log.push({ type: 'battlecry', effect: 'deal_1_hero', fromPlayer: playerIndex, toPlayer: idx, damage: 1 });
} else if (id === 'deal_2_hero' && enemies.length) { } else if (id === 'deal_2_hero' && enemies.length) {
const t = enemies[Math.floor(Math.random() * enemies.length)]; const t = enemies[Math.floor(Math.random() * enemies.length)];
const idx = gameState.players.indexOf(t); 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 }); gameState.log.push({ type: 'battlecry', effect: 'deal_2_hero', fromPlayer: playerIndex, toPlayer: idx, damage: 2 });
} else if (id === 'draw_1') { } else if (id === 'draw_1') {
const drawn = drawCards(p.deck, 1); const drawn = drawCards(p.deck, 1);
@ -689,10 +746,10 @@ function runBattlecry(room, card, playerIndex) {
} }
} }
} else if (id === 'heal_hero_2') { } 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 }); gameState.log.push({ type: 'battlecry', effect: 'heal_hero_2', playerIndex });
} else if (id === 'heal_hero_3') { } 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 }); gameState.log.push({ type: 'battlecry', effect: 'heal_hero_3', playerIndex });
} else if (id === 'buff_all_friendly_attack') { } else if (id === 'buff_all_friendly_attack') {
p.board.forEach((m) => { p.board.forEach((m) => {
@ -701,7 +758,7 @@ function runBattlecry(room, card, playerIndex) {
gameState.log.push({ type: 'battlecry', effect: 'buff_all_friendly_attack', playerIndex }); gameState.log.push({ type: 'battlecry', effect: 'buff_all_friendly_attack', playerIndex });
} else if (id === 'deal_3_all_enemies') { } else if (id === 'deal_3_all_enemies') {
enemies.forEach((e) => { 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 }); gameState.log.push({ type: 'battlecry', effect: 'deal_3_all_enemies', fromPlayer: playerIndex });
} else if (id === 'deal_3_any') { } else if (id === 'deal_3_any') {
@ -718,7 +775,7 @@ function runBattlecry(room, card, playerIndex) {
runDeathrattle(room, getCard(room, target.cardId), idx); runDeathrattle(room, getCard(room, target.cardId), idx);
} }
} else { } 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 }); 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 }); gameState.log.push({ type: 'battlecry', effect: 'buff_2_2', playerIndex, toIdx: boardIdx });
} }
} else if (id === 'heal_hero_5') { } 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 }); gameState.log.push({ type: 'battlecry', effect: 'heal_hero_5', playerIndex });
} else if (id === 'return_hand_enemy') { } else if (id === 'return_hand_enemy') {
// Для Ezra Bridger требуется выбор конкретного игрока // Для Ezra Bridger требуется выбор конкретного игрока
@ -808,8 +865,8 @@ function runBattlecry(room, card, playerIndex) {
} else if (id === 'random_effect') { } else if (id === 'random_effect') {
const effects = [ const effects = [
() => { const drawn = drawCards(p.deck, 1); if (drawn.length) p.hand.push(...drawn); gameState.log.push({ type: 'battlecry', effect: 'random_draw', playerIndex }); }, () => { 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 }); } }, () => { 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 }); } },
() => { p.health = Math.min(30, p.health + 1); gameState.log.push({ type: 'battlecry', effect: 'random_heal', playerIndex }); } () => { healPlayer(p, 1); gameState.log.push({ type: 'battlecry', effect: 'random_heal', playerIndex }); }
]; ];
const randomEffect = effects[Math.floor(Math.random() * effects.length)]; const randomEffect = effects[Math.floor(Math.random() * effects.length)];
randomEffect(); randomEffect();
@ -820,6 +877,7 @@ function runBattlecry(room, card, playerIndex) {
const target = enemy.board[Math.floor(Math.random() * enemy.board.length)]; const target = enemy.board[Math.floor(Math.random() * enemy.board.length)];
target.health = Math.max(0, target.health - 2); target.health = Math.max(0, target.health - 2);
const boardIdx = enemy.board.indexOf(target); 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 }); gameState.log.push({ type: 'battlecry', effect: 'deal_2_or_4_hero', fromPlayer: playerIndex, toPlayer: enemyIdx, toIdx: boardIdx, damage: 2 });
if (target.health <= 0) { if (target.health <= 0) {
enemy.board.splice(boardIdx, 1); enemy.board.splice(boardIdx, 1);
@ -827,6 +885,7 @@ function runBattlecry(room, card, playerIndex) {
} }
} else { } else {
enemy.health = Math.max(0, enemy.health - 4); 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 }); 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') { } 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 }); gameState.log.push({ type: 'deathrattle', effect: 'draw_1', playerIndex: ownerIndex });
} else if (id === 'deal_2_all_enemies') { } else if (id === 'deal_2_all_enemies') {
enemies.forEach((e) => { 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 }); gameState.log.push({ type: 'deathrattle', effect: 'deal_2_all_enemies', fromPlayer: ownerIndex });
} else if (id === 'deal_1_random_enemy' && enemies.length) { } else if (id === 'deal_1_random_enemy' && enemies.length) {
const t = enemies[Math.floor(Math.random() * 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 }); gameState.log.push({ type: 'deathrattle', effect: 'deal_1_random_enemy', fromPlayer: ownerIndex });
} else if (id === 'heal_hero_2') { } 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 }); gameState.log.push({ type: 'deathrattle', effect: 'heal_hero_2', playerIndex: ownerIndex });
} else if (id === 'draw_3') { } else if (id === 'draw_3') {
const drawn = drawCards(owner.deck, 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 }); gameState.log.push({ type: 'deathrattle', effect: 'draw_3', playerIndex: ownerIndex });
} else if (id === 'deal_4_random' && enemies.length) { } else if (id === 'deal_4_random' && enemies.length) {
const t = enemies[Math.floor(Math.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 }); 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) { } else if (id === 'deal_3_random' && enemies.length) {
const t = enemies[Math.floor(Math.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 }); 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, health: card.health,
maxHealth: card.health, maxHealth: card.health,
canAttack: card.attack > 0, canAttack: card.attack > 0,
attacksUsed: 0, // Для двойной атаки
}; };
p.board.splice(typeof boardPos === 'number' ? boardPos : p.board.length, 0, minion); p.board.splice(typeof boardPos === 'number' ? boardPos : p.board.length, 0, minion);
gameState.log.push({ type: 'play', playerIndex: pi, cardId: cid, minionId: minion.id }); 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), отправляем запрос клиенту // Для battlecry, требующих выбора цели (Ezra Bridger), отправляем запрос клиенту
if (card.battlecryId === 'return_hand_enemy') { if (card.battlecryId === 'return_hand_enemy') {
const enemies = gameState.players.filter((pl, i) => i !== pi && pl.health > 0); 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 всем джедаям // Йода даёт +1/+1 всем джедаям
if (card.id === 'yoda' || card.name === 'Yoda') { if (card.id === 'yoda' || card.name === 'Yoda') {
player.board.forEach((other, otherIdx) => { player.board.forEach((other, otherIdx) => {
@ -1471,7 +1552,7 @@ function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardInde
if (targetBoardIndex === -1) { if (targetBoardIndex === -1) {
const t = gameState.players[targetPlayerIndex]; const t = gameState.players[targetPlayerIndex];
if (!t || t.health <= 0) return; 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 }); gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, toPlayer: targetPlayerIndex, effect: 'deal_2_hero', damage: 2 });
} else { } else {
const t = gameState.players[targetPlayerIndex]; const t = gameState.players[targetPlayerIndex];
@ -1498,7 +1579,7 @@ function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardInde
} else if (eff === 'deal_1_all_enemies') { } else if (eff === 'deal_1_all_enemies') {
gameState.players.forEach((pl, i) => { gameState.players.forEach((pl, i) => {
if (i === pi) return; if (i === pi) return;
pl.health = Math.max(0, pl.health - 1); applyDamageToPlayer(pl, 1);
pl.board.forEach((m) => { pl.board.forEach((m) => {
m.health -= 1; m.health -= 1;
if (m.health <= 0) { if (m.health <= 0) {
@ -1526,7 +1607,7 @@ function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardInde
if (targetBoardIndex === -1) { if (targetBoardIndex === -1) {
const t = gameState.players[targetPlayerIndex]; const t = gameState.players[targetPlayerIndex];
if (!t || t.health <= 0) return; 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 }); gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, toPlayer: targetPlayerIndex, effect: 'deal_4_hero', damage: 4 });
} else { } else {
const t = gameState.players[targetPlayerIndex]; const t = gameState.players[targetPlayerIndex];
@ -1544,7 +1625,7 @@ function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardInde
if (targetBoardIndex === -1) { if (targetBoardIndex === -1) {
const t = gameState.players[targetPlayerIndex]; const t = gameState.players[targetPlayerIndex];
if (!t) return; 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' }); gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, toPlayer: targetPlayerIndex, effect: 'heal_4_hero' });
} else { } else {
const t = gameState.players[targetPlayerIndex]; const t = gameState.players[targetPlayerIndex];
@ -1583,6 +1664,16 @@ function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardInde
m.maxHealth = (m.maxHealth || m.health) + 1; m.maxHealth = (m.maxHealth || m.health) + 1;
}); });
gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, effect: 'buff_all_friendly' }); 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') { } else if (eff === 'destroy_weak_2') {
enemies.forEach((enemy) => { enemies.forEach((enemy) => {
if (enemy.board && enemy.board.length > 0) { if (enemy.board && enemy.board.length > 0) {
@ -1763,7 +1854,7 @@ function heroAbility(room, socketId, targetPlayerIndex, targetBoardIndex) {
const targetPlayer = gameState.players[targetPlayerIndex]; const targetPlayer = gameState.players[targetPlayerIndex];
if (!targetPlayer) return; if (!targetPlayer) return;
if (targetBoardIndex === -1) { 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' }); gameState.log.push({ type: 'heroAbility', fromPlayer: pi, toPlayer: targetPlayerIndex, effect: 'deal_1_hero' });
} else { } else {
const m = targetPlayer.board[targetBoardIndex]; const m = targetPlayer.board[targetBoardIndex];
@ -1809,14 +1900,50 @@ function attack(room, socketId, attackerBoardIndex, targetPlayerIndex, targetBoa
applySynergies(room); applySynergies(room);
if (targetBoardIndex === -1) { if (targetBoardIndex === -1) {
const attackerAttack = attacker.attack + (attacker.synergyAttackBonus || 0); let attackerAttack = attacker.attack + (attacker.synergyAttackBonus || 0);
targetPlayer.health = Math.max(0, targetPlayer.health - attackerAttack); 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({ gameState.log.push({
type: 'attackHero', type: 'attackHero',
fromPlayer: pi, fromPlayer: pi,
toPlayer: targetPlayerIndex, toPlayer: targetPlayerIndex,
attackerMinionId: attacker.id, attackerMinionId: attacker.id,
damage: attacker.attack, damage: attackerAttack,
armorBlocked: oldArmor - (targetPlayer.armor || 0),
}); });
// Применяем синергии перед расчётом урона // Применяем синергии перед расчётом урона
applySynergies(room); applySynergies(room);
@ -1852,9 +1979,22 @@ function attack(room, socketId, attackerBoardIndex, targetPlayerIndex, targetBoa
}); });
const targetCard = getCard(room, target.cardId); const targetCard = getCard(room, target.cardId);
const attackerCard = getCard(room, attacker.cardId); const attackerCard = getCard(room, attacker.cardId);
if (targetDied && targetCard && targetCard.deathrattleId) {
// Обработка 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); runDeathrattle(room, targetCard, targetPlayerIndex);
} }
}
if (attackerDied && attackerCard && attackerCard.deathrattleId) { if (attackerDied && attackerCard && attackerCard.deathrattleId) {
runDeathrattle(room, attackerCard, pi); runDeathrattle(room, attackerCard, pi);
} }
@ -1862,8 +2002,19 @@ function attack(room, socketId, attackerBoardIndex, targetPlayerIndex, targetBoa
// Механика двойной атаки // Механика двойной атаки
const attackerCard = getCard(room, attacker.cardId); const attackerCard = getCard(room, attacker.cardId);
if (attackerCard && attackerCard.canAttackTwice && !attacker.attacksUsed) { if (attackerCard && attackerCard.canAttackTwice) {
attacker.attacksUsed = 1; // Использована одна атака 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 { } else {
attacker.canAttack = false; attacker.canAttack = false;
attacker.attacksUsed = 0; attacker.attacksUsed = 0;