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

217
server.js
View File

@ -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;