/** * Star Wars Hearthstone - Game Server * PvP over LAN (Radmin VPN). 2-4 players. */ const express = require('express'); const http = require('http'); const { Server } = require('socket.io'); const path = require('path'); const app = express(); const server = http.createServer(app); const io = new Server(server); app.use(express.static(path.join(__dirname, 'public'))); app.get('/api/cards', (req, res) => { res.json(cardDb); }); const PORT = process.env.PORT || 3542; const MAX_PLAYERS = 4; const MIN_PLAYERS = 2; const cardDb = require('./cards'); function getCard(room, cardId) { return cardDb[cardId]; } const rooms = new Map(); // code -> { lobby, gameState, gameStarted, faction, turnTimerInterval, turnTimeLeft, turnTimerWarnFired } const TURN_SECONDS = 90; const TURN_WARN_AT = 15; function generateRoomCode() { const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; let code = ''; for (let i = 0; i < 5; i++) { code += chars[Math.floor(Math.random() * chars.length)]; } return code; } function cleanupEmptyRooms() { for (const [code, room] of rooms.entries()) { if (!room.gameStarted && (!room.lobby || room.lobby.length === 0)) { clearTurnTimer(room); rooms.delete(code); } } } // Периодическая очистка пустых комнат setInterval(cleanupEmptyRooms, 30000); // каждые 30 секунд function shuffle(a) { for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; } function createDeck(faction) { const minionIds = Object.keys(cardDb).filter((id) => { const c = cardDb[id]; if (c.type !== 'minion') return false; if (faction && c.faction !== faction && c.faction !== 'neutral' && c.faction !== 'animal') return false; return true; }); const spellIds = Object.keys(cardDb).filter((id) => { const c = cardDb[id]; if (c.type !== 'spell') return false; if (faction && c.faction !== faction && c.faction !== 'neutral') return false; return true; }); const deck = []; const legendaryUsed = new Set(); for (let i = 0; i < 15; i++) { const pool = minionIds.filter((id) => { const c = cardDb[id]; if (c.legendary && legendaryUsed.has(id)) return false; return true; }); if (!pool.length) break; const id = pool[Math.floor(Math.random() * pool.length)]; deck.push(id); if (cardDb[id].legendary) legendaryUsed.add(id); } for (let i = 0; i < 5 && spellIds.length; i++) { deck.push(spellIds[Math.floor(Math.random() * spellIds.length)]); } return shuffle(deck); } function drawCards(deck, n) { const drawn = []; for (let i = 0; i < n && deck.length; i++) { drawn.push(deck.pop()); } return drawn; } function initGame(room) { const lobby = room.lobby; const playerCount = lobby.length; const factionChoice = room.faction || null; const players = lobby.map((p, i) => ({ id: p.id, name: p.name || `Player ${i + 1}`, deck: createDeck(factionChoice), hand: [], board: [], mana: 0, maxMana: 0, health: 30, hero: (p.hero || ['luke', 'vader', 'yoda', 'leia', 'rey', 'kylo', 'mace'][i % 7]), manualDrawUsed: false, fatigueCounter: 0, heroAbilityUsed: false, isAI: false, })); // Если режим ИИ, добавляем ИИ игрока if (room.aiMode) { players.push({ id: 'AI_' + Date.now(), name: 'ИИ Противник', deck: createDeck(factionChoice), hand: [], board: [], mana: 0, maxMana: 0, health: 30, hero: 'vader', manualDrawUsed: false, fatigueCounter: 0, heroAbilityUsed: false, isAI: true, }); } players.forEach((p, i) => { p.hand = drawCards(p.deck, i < 2 ? 3 : 4); }); players[0].maxMana = 1; players[0].mana = 1; room.gameState = { phase: 'playing', turn: 1, players, currentPlayerIndex: 0, log: [], }; room.gameStarted = true; startTurnTimer(room); broadcastGameState(room); // Если первый ход ИИ, делаем его ход if (room.aiMode && players[0].isAI) { setTimeout(() => { if (room.gameState && room.gameState.phase === 'playing') { makeAITurn(room); } }, 1000); } } // Функция для хода ИИ function makeAITurn(room) { const gameState = room.gameState; if (!gameState || gameState.phase !== 'playing') return; const aiPlayerIndex = gameState.currentPlayerIndex; const aiPlayer = gameState.players[aiPlayerIndex]; if (!aiPlayer || !aiPlayer.isAI || aiPlayer.health <= 0) return; if (gameState.currentPlayerIndex !== aiPlayerIndex) return; // Применяем синергии перед ходом applySynergies(room); // Небольшая задержка для визуализации setTimeout(() => { let actionsDone = 0; const maxActions = 5; // Максимум действий за ход // 1. Играем карты (приоритет дешёвым и эффективным) const playableCards = aiPlayer.hand .map((cardId, index) => { const card = getCard(room, cardId); if (!card) return null; const cost = card.cost || 0; if (cost > aiPlayer.mana) return null; if (card.type === 'minion' && aiPlayer.board.length >= 7) return null; // Оценка карты (чем выше, тем лучше) let value = 0; if (card.type === 'minion') { value = (card.attack || 0) + (card.health || 0) - cost * 2; if (card.battlecryId) value += 2; if (card.legendary) value += 1; } else if (card.type === 'spell') { value = 3 - cost; if (card.spellEffect === 'deal_3_minion' || card.spellEffect === 'deal_3_any') value += 2; if (card.spellEffect === 'buff_3_2') value += 1; } return { index, card, cost, value }; }) .filter(Boolean) .sort((a, b) => b.value - a.value); // Играем лучшие карты let cardIndex = 0; const playNextCard = () => { if (cardIndex >= playableCards.length || actionsDone >= 3) { // Переходим к атакам setTimeout(() => { performAIAttacks(room, aiPlayerIndex); }, 500); return; } const playable = playableCards[cardIndex]; const currentPlayer = gameState.players[aiPlayerIndex]; if (!currentPlayer || playable.cost > currentPlayer.mana) { cardIndex++; playNextCard(); return; } if (playable.card.type === 'minion') { // Играем миньона (пропускаем карты с выбором цели для упрощения) if (playable.card.battlecryId === 'return_hand_enemy') { cardIndex++; playNextCard(); return; } const boardPos = currentPlayer.board.length; playCard(room, aiPlayer.id, playable.index, boardPos); actionsDone++; cardIndex++; setTimeout(playNextCard, 600); } else if (playable.card.type === 'spell') { // Используем заклинание const enemies = gameState.players.filter((p, i) => i !== aiPlayerIndex && p.health > 0); if (enemies.length > 0) { const targetEnemy = enemies[0]; let targetBoardIndex = -1; if (playable.card.spellTarget === 'enemy_minion' || playable.card.spellTarget === 'any_minion') { if (targetEnemy.board && targetEnemy.board.length > 0) { targetBoardIndex = 0; } else { targetBoardIndex = -1; } } else if (playable.card.spellTarget === 'friendly_minion') { if (currentPlayer.board && currentPlayer.board.length > 0) { targetBoardIndex = 0; } else { cardIndex++; playNextCard(); return; } } else if (playable.card.spellTarget === 'enemy_player') { // Для заклинаний типа "Грабеж" пропускаем (требует выбор карт) if (playable.card.spellEffect === 'steal_cards') { cardIndex++; playNextCard(); return; } targetBoardIndex = -1; } playSpell(room, aiPlayer.id, playable.index, gameState.players.indexOf(targetEnemy), targetBoardIndex); actionsDone++; cardIndex++; setTimeout(playNextCard, 600); } else { cardIndex++; playNextCard(); } } else { cardIndex++; playNextCard(); } }; playNextCard(); }, 800); } // Функция для атак ИИ function performAIAttacks(room, aiPlayerIndex) { const gameState = room.gameState; if (!gameState || gameState.phase !== 'playing') { endTurn(room); return; } const aiPlayer = gameState.players[aiPlayerIndex]; if (!aiPlayer || aiPlayer.health <= 0) { endTurn(room); return; } const attackableMinions = aiPlayer.board.filter(m => m.canAttack && !m.frozen); const enemies = gameState.players.filter((p, i) => i !== aiPlayerIndex && p.health > 0); if (attackableMinions.length === 0 || enemies.length === 0) { setTimeout(() => endTurn(room), 500); return; } let attackIndex = 0; const performNextAttack = () => { if (attackIndex >= attackableMinions.length) { setTimeout(() => endTurn(room), 500); return; } const minion = attackableMinions[attackIndex]; const boardIndex = aiPlayer.board.indexOf(minion); const targetEnemy = enemies[0]; const targetPlayerIndex = gameState.players.indexOf(targetEnemy); let targetBoardIndex = -1; // Выбираем цель: сначала слабые миньоны, потом герой if (targetEnemy.board && targetEnemy.board.length > 0) { const weakMinion = targetEnemy.board.find(m => m.health <= minion.attack) || targetEnemy.board[0]; targetBoardIndex = targetEnemy.board.indexOf(weakMinion); } else { targetBoardIndex = -1; // Атакуем героя } attack(room, aiPlayer.id, boardIndex, targetPlayerIndex, targetBoardIndex); attackIndex++; setTimeout(performNextAttack, 800); }; performNextAttack(); } function clearTurnTimer(room) { if (room.turnTimerInterval) { clearInterval(room.turnTimerInterval); room.turnTimerInterval = null; } room.turnTimeLeft = TURN_SECONDS; room.turnTimerWarnFired = false; } function startTurnTimer(room) { clearTurnTimer(room); if (!room.gameState || room.gameState.phase !== 'playing') return; room.turnTimeLeft = TURN_SECONDS; room.turnTimerWarnFired = false; io.to(room.code).emit('turnTime', { left: room.turnTimeLeft, currentPlayerIndex: room.gameState.currentPlayerIndex, warn: false, ended: false, }); room.turnTimerInterval = setInterval(() => { if (!room.gameState || room.gameState.phase !== 'playing') { clearTurnTimer(room); return; } room.turnTimeLeft--; io.to(room.code).emit('turnTime', { left: room.turnTimeLeft, currentPlayerIndex: room.gameState.currentPlayerIndex, warn: room.turnTimeLeft === TURN_WARN_AT, ended: room.turnTimeLeft <= 0, }); if (room.turnTimeLeft === TURN_WARN_AT) room.turnTimerWarnFired = true; if (room.turnTimeLeft <= 0) { clearTurnTimer(room); endTurn(room); } }, 1000); } function broadcastGameState(room) { if (!room.gameState) return; room.gameState.players.forEach((p, i) => { const socket = io.sockets.sockets.get(p.id); if (socket) { const yourView = { ...room.gameState, yourIndex: i, yourHand: [...p.hand], yourDeck: [...p.deck], yourDeckCount: p.deck.length, yourManualDrawUsed: !!p.manualDrawUsed, yourHeroAbilityUsed: !!p.heroAbilityUsed, cardDb: cardDb, }; socket.emit('gameState', yourView); } }); } function findPlayerIndex(room, socketId) { return room.gameState.players.findIndex((p) => p.id === socketId); } function getRoomByCode(code) { return rooms.get(code?.toUpperCase()); } function getRoomBySocket(socketId) { for (const room of rooms.values()) { if (room.lobby?.some((p) => p.id === socketId)) return room; if (room.gameState?.players?.some((p) => p.id === socketId)) return room; } return null; } function endTurn(room) { const gameState = room.gameState; const prev = gameState.currentPlayerIndex; gameState.currentPlayerIndex = (gameState.currentPlayerIndex + 1) % gameState.players.length; const next = gameState.currentPlayerIndex; const np = gameState.players[next]; np.maxMana = Math.min(10, np.maxMana + 1); np.mana = np.maxMana; // Эффекты структур в конце хода предыдущего игрока const prevPlayer = gameState.players[prev]; if (prevPlayer) { prevPlayer.board.forEach((m) => { const card = getCard(room, m.cardId); if (!card) return; if (card.name === 'Храм джедаев') { prevPlayer.board.forEach((minion) => { if (minion.id !== m.id && minion.health < (minion.maxHealth || minion.health)) { minion.health = Math.min(minion.maxHealth || minion.health, minion.health + 1); } }); } else if (card.name === 'Станция Звезды Смерти') { 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); gameState.log.push({ type: 'structure', effect: 'death_star_damage', fromPlayer: prev, toPlayer: gameState.players.indexOf(target), damage: 3 }); } } else if (card.name === 'Кантина Мос-Эйсли') { if (prevPlayer.deck.length > 0) { const drawn = drawCards(prevPlayer.deck, 1); if (drawn.length) prevPlayer.hand.push(...drawn); gameState.log.push({ type: 'structure', effect: 'cantina_draw', playerIndex: prev }); } } else if (card.name === 'Сарлак') { const enemies = gameState.players.filter((pl, i) => i !== prev && pl.health > 0); if (enemies.length) { const enemy = enemies[Math.floor(Math.random() * enemies.length)]; if (enemy.board && enemy.board.length > 0) { const weak = enemy.board.filter(min => min.attack <= 3); if (weak.length > 0) { const target = weak[Math.floor(Math.random() * weak.length)]; const boardIdx = enemy.board.indexOf(target); const targetCard = getCard(room, target.cardId); enemy.board.splice(boardIdx, 1); gameState.log.push({ type: 'structure', effect: 'sarlacc_consume', fromPlayer: prev, toPlayer: gameState.players.indexOf(enemy), toIdx: boardIdx }); if (targetCard && targetCard.deathrattleId) { runDeathrattle(room, targetCard, gameState.players.indexOf(enemy)); } } } } } // Обработка планет else if (card.name === 'Korriban') { 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); gameState.log.push({ type: 'planet', effect: 'korriban_damage', fromPlayer: prev, toPlayer: gameState.players.indexOf(target), damage: 2 }); } } else if (card.name === 'Tatooine') { if (prevPlayer.deck.length > 0) { const drawn = drawCards(prevPlayer.deck, 1); if (drawn.length) prevPlayer.hand.push(...drawn); gameState.log.push({ type: 'planet', effect: 'tatooine_draw', playerIndex: prev }); } } else if (card.name === 'Coruscant') { prevPlayer.board.forEach((minion) => { if (minion.id !== m.id && minion.health < (minion.maxHealth || minion.health)) { minion.health = Math.min(minion.maxHealth || minion.health, minion.health + 1); } }); gameState.log.push({ type: 'planet', effect: 'coruscant_heal', playerIndex: prev }); } 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); }); gameState.log.push({ type: 'planet', effect: 'zakuul_damage', fromPlayer: prev }); } else if (card.name === 'Kamino') { if (prevPlayer.hand.length < 10) { prevPlayer.hand.push('clone_trooper'); gameState.log.push({ type: 'planet', effect: 'kamino_summon', playerIndex: prev }); } } 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); if (enemy.board) { enemy.board.forEach((min) => { min.health = Math.max(0, min.health - 1); }); enemy.board = enemy.board.filter((m) => m.health > 0); } }); gameState.log.push({ type: 'planet', effect: 'mustafar_damage', fromPlayer: prev }); } else if (card.name === 'Naboo') { prevPlayer.health = Math.min(30, prevPlayer.health + 2); gameState.log.push({ type: 'planet', effect: 'naboo_heal', playerIndex: prev }); } else if (card.name === 'Endor') { if (prevPlayer.hand.length < 10) { prevPlayer.hand.push('ewok'); gameState.log.push({ type: 'planet', effect: 'endor_summon', playerIndex: prev }); } } else if (card.name === 'Hoth') { const enemies = gameState.players.filter((pl, i) => i !== prev && pl.health > 0); enemies.forEach((enemy) => { if (enemy.board) { enemy.board.forEach((min) => { min.frozen = true; }); } }); gameState.log.push({ type: 'planet', effect: 'hoth_freeze', fromPlayer: prev }); } else if (card.name === 'Dagobah') { if (prevPlayer.deck.length > 0) { const drawn = drawCards(prevPlayer.deck, 1); if (drawn.length) prevPlayer.hand.push(...drawn); gameState.log.push({ type: 'planet', effect: 'dagobah_draw', playerIndex: prev }); } } }); } np.board.forEach((m) => { if (m.frozen) { m.frozen = false; return; } const card = getCard(room, m.cardId); if (card && card.attack > 0) { m.canAttack = true; if (card.canAttackTwice) { m.attacksUsed = 0; // Сбрасываем счётчик атак для двойной атаки } } }); 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); gameState.log.push({ type: 'fatigue', playerIndex: next, damage: np.fatigueCounter }); } else { const drawn = drawCards(np.deck, 1); if (drawn.length) np.hand.push(...drawn); } gameState.turn++; gameState.log.push({ type: 'turn', from: prev, to: next }); // Применяем синергии в начале каждого хода applySynergies(room); checkGameOver(room); startTurnTimer(room); broadcastGameState(room); // Если следующий игрок - ИИ, делаем его ход if (np.isAI && gameState.phase === 'playing') { setTimeout(() => { if (room.gameState && room.gameState.phase === 'playing') { makeAITurn(room); } }, 1500); } } function manualDraw(room, socketId) { const gameState = room.gameState; const pi = findPlayerIndex(room, socketId); if (pi < 0 || gameState.currentPlayerIndex !== pi) return; const p = gameState.players[pi]; if (p.manualDrawUsed || p.hand.length >= 10) return; if (p.deck.length === 0) { p.manualDrawUsed = true; p.fatigueCounter = (p.fatigueCounter || 0) + 1; const dmg = p.fatigueCounter; p.health = Math.max(0, p.health - dmg); gameState.log.push({ type: 'fatigue', playerIndex: pi, damage: dmg }); checkGameOver(room); broadcastGameState(room); return; } p.manualDrawUsed = true; const drawn = drawCards(p.deck, 1); if (drawn.length) p.hand.push(...drawn); gameState.log.push({ type: 'draw', playerIndex: pi }); broadcastGameState(room); } function runBattlecry(room, card, playerIndex) { const gameState = room.gameState; const p = gameState.players[playerIndex]; const enemies = gameState.players.filter((pl, i) => i !== playerIndex && pl.health > 0); const id = card.battlecryId; 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); 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); 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); if (drawn.length) p.hand.push(...drawn); gameState.log.push({ type: 'battlecry', effect: 'draw_1', playerIndex }); } else if (id === 'draw_2') { const drawn = drawCards(p.deck, 2); if (drawn.length) p.hand.push(...drawn); gameState.log.push({ type: 'battlecry', effect: 'draw_2', playerIndex }); } else if (id === 'deal_2_minion' && enemies.length) { const enemy = enemies[Math.floor(Math.random() * enemies.length)]; const enemyIdx = gameState.players.indexOf(enemy); if (enemy.board && enemy.board.length > 0) { const target = enemy.board[Math.floor(Math.random() * enemy.board.length)]; target.health = Math.max(0, target.health - 2); const boardIdx = enemy.board.indexOf(target); gameState.log.push({ type: 'battlecry', effect: 'deal_2_minion', fromPlayer: playerIndex, toPlayer: enemyIdx, toIdx: boardIdx, damage: 2 }); if (target.health <= 0) { enemy.board.splice(boardIdx, 1); runDeathrattle(room, getCard(room, target.cardId), enemyIdx); } } } else if (id === 'destroy_weak_minion' && enemies.length) { const enemy = enemies[Math.floor(Math.random() * enemies.length)]; const enemyIdx = gameState.players.indexOf(enemy); if (enemy.board && enemy.board.length > 0) { const weak = enemy.board.filter(m => m.attack <= 3); if (weak.length > 0) { const target = weak[Math.floor(Math.random() * weak.length)]; const boardIdx = enemy.board.indexOf(target); const targetCard = getCard(room, target.cardId); enemy.board.splice(boardIdx, 1); gameState.log.push({ type: 'battlecry', effect: 'destroy_weak_minion', fromPlayer: playerIndex, toPlayer: enemyIdx, toIdx: boardIdx }); if (targetCard && targetCard.deathrattleId) { runDeathrattle(room, targetCard, enemyIdx); } } } } else if (id === 'heal_hero_2') { p.health = Math.min(30, p.health + 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); gameState.log.push({ type: 'battlecry', effect: 'heal_hero_3', playerIndex }); } else if (id === 'buff_all_friendly_attack') { p.board.forEach((m) => { m.attack += 1; }); 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); }); gameState.log.push({ type: 'battlecry', effect: 'deal_3_all_enemies', fromPlayer: playerIndex }); } else if (id === 'deal_3_any') { if (enemies.length) { const t = enemies[Math.floor(Math.random() * enemies.length)]; const idx = gameState.players.indexOf(t); if (t.board && t.board.length > 0) { const target = t.board[Math.floor(Math.random() * t.board.length)]; target.health = Math.max(0, target.health - 3); const boardIdx = t.board.indexOf(target); gameState.log.push({ type: 'battlecry', effect: 'deal_3_any', fromPlayer: playerIndex, toPlayer: idx, toIdx: boardIdx, damage: 3 }); if (target.health <= 0) { t.board.splice(boardIdx, 1); runDeathrattle(room, getCard(room, target.cardId), idx); } } else { t.health = Math.max(0, t.health - 3); gameState.log.push({ type: 'battlecry', effect: 'deal_3_any', fromPlayer: playerIndex, toPlayer: idx, damage: 3 }); } } } else if (id === 'draw_1_gain_mana') { const drawn = drawCards(p.deck, 1); if (drawn.length) p.hand.push(...drawn); p.maxMana = Math.min(10, p.maxMana + 1); p.mana = Math.min(10, p.mana + 1); gameState.log.push({ type: 'battlecry', effect: 'draw_1_gain_mana', playerIndex }); } else if (id === 'destroy_medium_minion' && enemies.length) { const enemy = enemies[Math.floor(Math.random() * enemies.length)]; const enemyIdx = gameState.players.indexOf(enemy); if (enemy.board && enemy.board.length > 0) { const weak = enemy.board.filter(m => m.attack <= 4); if (weak.length > 0) { const target = weak[Math.floor(Math.random() * weak.length)]; const boardIdx = enemy.board.indexOf(target); const targetCard = getCard(room, target.cardId); enemy.board.splice(boardIdx, 1); gameState.log.push({ type: 'battlecry', effect: 'destroy_medium_minion', fromPlayer: playerIndex, toPlayer: enemyIdx, toIdx: boardIdx }); if (targetCard && targetCard.deathrattleId) { runDeathrattle(room, targetCard, enemyIdx); } } } } else if (id === 'buff_friendly_2_1') { if (p.board && p.board.length > 0) { const target = p.board[Math.floor(Math.random() * p.board.length)]; target.attack += 2; target.health += 1; target.maxHealth = (target.maxHealth || target.health) + 1; const boardIdx = p.board.indexOf(target); gameState.log.push({ type: 'battlecry', effect: 'buff_friendly_2_1', playerIndex, toIdx: boardIdx }); } } else if (id === 'buff_2_2') { if (p.board && p.board.length > 0) { const target = p.board[Math.floor(Math.random() * p.board.length)]; target.attack += 2; target.health += 2; target.maxHealth = (target.maxHealth || target.health) + 2; const boardIdx = p.board.indexOf(target); 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); gameState.log.push({ type: 'battlecry', effect: 'heal_hero_5', playerIndex }); } else if (id === 'return_hand_enemy') { // Для Ezra Bridger требуется выбор конкретного игрока // Отправляем запрос клиенту для выбора цели const socket = io.sockets.sockets.get(p.id); if (socket && enemies.length > 0) { socket.emit('battlecryTargetRequest', { battlecryId: 'return_hand_enemy', cardId: card.id || Object.keys(cardDb).find(k => cardDb[k] === card), availableTargets: enemies.map((e, idx) => ({ playerIndex: gameState.players.indexOf(e), playerName: e.name || `Игрок ${gameState.players.indexOf(e) + 1}`, hasMinions: e.board && e.board.length > 0 })) }); } return; // Не выполняем сразу, ждём выбора цели } else if (id === 'destroy_strongest_enemy' && enemies.length) { const enemy = enemies[Math.floor(Math.random() * enemies.length)]; const enemyIdx = gameState.players.indexOf(enemy); if (enemy.board && enemy.board.length > 0) { const strongest = enemy.board.reduce((max, m) => (m.attack > (max?.attack || 0) ? m : max), null); if (strongest) { const boardIdx = enemy.board.indexOf(strongest); const targetCard = getCard(room, strongest.cardId); enemy.board.splice(boardIdx, 1); gameState.log.push({ type: 'battlecry', effect: 'destroy_strongest_enemy', fromPlayer: playerIndex, toPlayer: enemyIdx, toIdx: boardIdx }); if (targetCard && targetCard.deathrattleId) { runDeathrattle(room, targetCard, enemyIdx); } } } } else if (id === 'freeze_all_enemies') { enemies.forEach((enemy) => { if (enemy.board) { enemy.board.forEach((m) => { m.frozen = true; }); } }); gameState.log.push({ type: 'battlecry', effect: 'freeze_all_enemies', fromPlayer: 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 }); } ]; const randomEffect = effects[Math.floor(Math.random() * effects.length)]; randomEffect(); } else if (id === 'deal_2_or_4_hero' && enemies.length) { const enemy = enemies[Math.floor(Math.random() * enemies.length)]; const enemyIdx = gameState.players.indexOf(enemy); if (enemy.board && enemy.board.length > 0) { const target = enemy.board[Math.floor(Math.random() * enemy.board.length)]; target.health = Math.max(0, target.health - 2); const boardIdx = enemy.board.indexOf(target); 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); runDeathrattle(room, getCard(room, target.cardId), enemyIdx); } } else { enemy.health = Math.max(0, enemy.health - 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') { enemies.forEach((enemy) => { if (enemy.board) { enemy.board.forEach((m) => { m.health = Math.max(0, m.health - 2); if (m.health <= 0) { const dc = getCard(room, m.cardId); if (dc && dc.deathrattleId) runDeathrattle(room, dc, gameState.players.indexOf(enemy)); } }); enemy.board = enemy.board.filter((m) => m.health > 0); } }); gameState.log.push({ type: 'battlecry', effect: 'deal_2_all_enemy_minions', fromPlayer: playerIndex }); } else if (id === 'heal_all_friendly_2') { p.board.forEach((m) => { m.health = Math.min(m.maxHealth || m.health, m.health + 2); }); gameState.log.push({ type: 'battlecry', effect: 'heal_all_friendly_2', playerIndex }); } else if (id === 'heal_all_5') { p.health = Math.min(30, p.health + 5); p.board.forEach((m) => { m.health = Math.min(m.maxHealth || m.health, m.health + 5); }); gameState.log.push({ type: 'battlecry', effect: 'heal_all_5', playerIndex }); } else if (id === 'destroy_strong_minion' && enemies.length) { const enemy = enemies[Math.floor(Math.random() * enemies.length)]; const enemyIdx = gameState.players.indexOf(enemy); if (enemy.board && enemy.board.length > 0) { const strong = enemy.board.filter(m => m.attack <= 5); if (strong.length > 0) { const target = strong[Math.floor(Math.random() * strong.length)]; const boardIdx = enemy.board.indexOf(target); const targetCard = getCard(room, target.cardId); enemy.board.splice(boardIdx, 1); gameState.log.push({ type: 'battlecry', effect: 'destroy_strong_minion', fromPlayer: playerIndex, toPlayer: enemyIdx, toIdx: boardIdx }); if (targetCard && targetCard.deathrattleId) { runDeathrattle(room, targetCard, enemyIdx); } } } } else if (id === 'buff_all_friendly_1_1') { p.board.forEach((m) => { m.attack += 1; m.health += 1; m.maxHealth = (m.maxHealth || m.health) + 1; }); gameState.log.push({ type: 'battlecry', effect: 'buff_all_friendly_1_1', playerIndex }); } } function runDeathrattle(room, card, ownerIndex) { if (!card) return; // Проверка на превращение при смерти if (card.transformOnDeath) { handleTransformOnDeath(room, card, ownerIndex); } if (!card.deathrattleId) return; const gameState = room.gameState; const owner = gameState.players[ownerIndex]; const enemies = gameState.players.filter((pl, i) => i !== ownerIndex && pl.health > 0); const id = card.deathrattleId; if (id === 'draw_1') { const drawn = drawCards(owner.deck, 1); if (drawn.length) owner.hand.push(...drawn); 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); }); 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); 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); gameState.log.push({ type: 'deathrattle', effect: 'heal_hero_2', playerIndex: ownerIndex }); } else if (id === 'draw_3') { const drawn = drawCards(owner.deck, 3); if (drawn.length) owner.hand.push(...drawn); 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); 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); gameState.log.push({ type: 'deathrattle', effect: 'deal_3_random', fromPlayer: ownerIndex, toPlayer: gameState.players.indexOf(t), damage: 3 }); } } function handleTransformOnDeath(room, card, ownerIndex) { if (card.transformOnDeath) { const owner = room.gameState.players[ownerIndex]; const minionOnBoard = owner.board.find(m => { const mCard = cardDb[m.cardId]; return mCard && (mCard.id === card.id || mCard.name === card.name); }); if (minionOnBoard) { minionOnBoard.cardId = card.transformOnDeath; minionOnBoard.health = Math.max(1, minionOnBoard.health); const newCard = cardDb[card.transformOnDeath]; if (newCard) { minionOnBoard.maxHealth = newCard.health; minionOnBoard.attack = newCard.attack; room.gameState.log.push({ type: 'transform', fromCard: card.name, toCard: newCard.name, playerIndex: ownerIndex }); } } } } function handleTransformOnDeath(room, card, ownerIndex) { if (card.transformOnDeath) { const owner = room.gameState.players[ownerIndex]; const minionOnBoard = owner.board.find(m => m.cardId === card.id || m.cardId === Object.keys(cardDb).find(k => cardDb[k] === card)); if (minionOnBoard) { minionOnBoard.cardId = card.transformOnDeath; minionOnBoard.health = Math.max(1, minionOnBoard.health); const newCard = cardDb[card.transformOnDeath]; if (newCard) { minionOnBoard.maxHealth = newCard.health; minionOnBoard.attack = newCard.attack; room.gameState.log.push({ type: 'transform', fromCard: card.id || card.name, toCard: card.transformOnDeath, playerIndex: ownerIndex }); } } } } function checkGameOver(room) { const gameState = room.gameState; const alive = gameState.players.filter((pp) => pp.health > 0); const dead = gameState.players.filter((pp) => pp.health <= 0); // Помечаем мёртвых игроков dead.forEach((p) => { if (p.health <= 0 && !p.isDead) { p.isDead = true; gameState.log.push({ type: 'playerDefeated', playerIndex: gameState.players.indexOf(p), playerName: p.name }); } }); // Если остался один или меньше живых - игра окончена if (alive.length <= 1) { gameState.phase = 'ended'; gameState.winner = alive.length === 1 ? alive[0] : null; } // Если текущий игрок мёртв, переключаем ход const currentPlayer = gameState.players[gameState.currentPlayerIndex]; if (currentPlayer && currentPlayer.health <= 0 && alive.length > 1) { // Пропускаем ход мёртвого игрока gameState.log.push({ type: 'skipTurn', playerIndex: gameState.currentPlayerIndex, reason: 'defeated' }); endTurn(room); } } function playCard(room, socketId, handIndex, boardPos) { const gameState = room.gameState; const pi = findPlayerIndex(room, socketId); if (pi < 0 || gameState.currentPlayerIndex !== pi) return; const p = gameState.players[pi]; if (p.health <= 0 || p.isDead) return; // Мёртвый игрок не может играть const cid = p.hand[handIndex]; if (!cid) return; const card = getCard(room, cid); if (!card || card.type !== 'minion' || p.board.length >= 7) return; const cost = card.cost || 0; if (p.mana < cost) return; p.mana -= cost; p.hand.splice(handIndex, 1); const minion = { id: `${Date.now()}-${Math.random().toString(36).slice(2)}`, cardId: cid, attack: card.attack, health: card.health, maxHealth: card.health, canAttack: card.attack > 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, требующих выбора цели (Ezra Bridger), отправляем запрос клиенту if (card.battlecryId === 'return_hand_enemy') { const enemies = gameState.players.filter((pl, i) => i !== pi && pl.health > 0); if (enemies.length > 0) { const socket = io.sockets.sockets.get(p.id); if (socket) { // Сохраняем информацию о миньоне для последующего выполнения battlecry minion.pendingBattlecry = { battlecryId: card.battlecryId, cardId: cid }; socket.emit('battlecryTargetRequest', { battlecryId: 'return_hand_enemy', cardId: cid, minionId: minion.id, availableTargets: enemies.flatMap(enemy => { const enemyIdx = gameState.players.indexOf(enemy); if (!enemy.board || enemy.board.length === 0) return []; return enemy.board.map((m, boardIdx) => ({ playerIndex: enemyIdx, boardIndex: boardIdx, playerName: enemy.name || `Игрок ${enemyIdx + 1}`, minionName: getCard(room, m.cardId)?.name || m.cardId })); }) }); broadcastGameState(room); return; // Не применяем синергии пока, ждём выбора цели } } } else if (card.battlecryId) { runBattlecry(room, card, pi); } // Применяем синергии после размещения карты applySynergies(room); checkGameOver(room); broadcastGameState(room); } // Функция для применения синергий между картами function applySynergies(room) { const gameState = room.gameState; const cardDb = require('./cards'); gameState.players.forEach((player, playerIndex) => { if (!player.board || player.board.length === 0) return; // Сбрасываем все бонусы синергий перед пересчётом player.board.forEach(m => { m.synergyAttackBonus = 0; m.synergyHealthBonus = 0; }); player.board.forEach((minion, idx) => { const card = cardDb[minion.cardId]; if (!card) return; // Дарт Вейдер даёт +1/+1 всем штурмовикам и клонам if (card.id === 'vader' || card.name === 'Darth Vader') { player.board.forEach((other, otherIdx) => { if (idx !== otherIdx) { const otherCard = cardDb[other.cardId]; if (otherCard && (otherCard.id === 'stormtrooper' || otherCard.name === 'Stormtrooper' || otherCard.id === 'clone_trooper' || otherCard.name === 'Clone Trooper' || otherCard.id === 'clone_commando' || otherCard.name === 'Clone Commando' || otherCard.id === 'arc_trooper' || otherCard.name === 'ARC Trooper')) { other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1; other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1; } } }); } // Люк Скайуокер даёт +1/+1 всем повстанцам if (card.id === 'luke' || card.name === 'Luke Skywalker') { player.board.forEach((other, otherIdx) => { if (idx !== otherIdx) { const otherCard = cardDb[other.cardId]; if (otherCard && otherCard.faction === 'rebellion' && otherCard.type === 'minion') { other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1; other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1; } } }); } // Император Палпатин даёт +1/+1 всем ситхам и имперским картам if (card.id === 'palpatine' || card.name === 'Emperor Palpatine') { player.board.forEach((other, otherIdx) => { if (idx !== otherIdx) { const otherCard = cardDb[other.cardId]; if (otherCard && (otherCard.faction === 'empire' || otherCard.name?.includes('Darth') || otherCard.id === 'maul' || otherCard.id === 'vader' || otherCard.id === 'dooku' || otherCard.id === 'kylo')) { other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1; other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1; } } }); } // Хан Соло и Чубакка дают друг другу +1/+1 if (card.id === 'han' || card.name === 'Han Solo') { player.board.forEach((other, otherIdx) => { if (idx !== otherIdx) { const otherCard = cardDb[other.cardId]; if (otherCard && (otherCard.id === 'chewie' || otherCard.name === 'Chewbacca')) { other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1; other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1; } } }); } if (card.id === 'chewie' || card.name === 'Chewbacca') { player.board.forEach((other, otherIdx) => { if (idx !== otherIdx) { const otherCard = cardDb[other.cardId]; if (otherCard && (otherCard.id === 'han' || otherCard.name === 'Han Solo')) { other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1; other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1; } } }); } // R2-D2 и C-3PO дают друг другу +1/+1 if (card.id === 'r2d2' || card.name === 'R2-D2') { player.board.forEach((other, otherIdx) => { if (idx !== otherIdx) { const otherCard = cardDb[other.cardId]; if (otherCard && (otherCard.id === 'c3po' || otherCard.name === 'C-3PO')) { other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1; other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1; } } }); } if (card.id === 'c3po' || card.name === 'C-3PO') { player.board.forEach((other, otherIdx) => { if (idx !== otherIdx) { const otherCard = cardDb[other.cardId]; if (otherCard && (otherCard.id === 'r2d2' || otherCard.name === 'R2-D2')) { other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1; other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1; } } }); } // Лея даёт +1/+1 Люку if (card.id === 'leia' || card.name === 'Princess Leia') { player.board.forEach((other, otherIdx) => { if (idx !== otherIdx) { const otherCard = cardDb[other.cardId]; if (otherCard && (otherCard.id === 'luke' || otherCard.name === 'Luke Skywalker')) { other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1; other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1; } } }); } // Люк даёт +1/+1 Лее if (card.id === 'luke' || card.name === 'Luke Skywalker') { player.board.forEach((other, otherIdx) => { if (idx !== otherIdx) { const otherCard = cardDb[other.cardId]; if (otherCard && (otherCard.id === 'leia' || otherCard.name === 'Princess Leia')) { other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1; other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1; } } }); } // Оби-Ван даёт +1/+1 Энакину/Анакину if (card.id === 'obiwan' || card.name === 'Obi-Wan Kenobi') { player.board.forEach((other, otherIdx) => { if (idx !== otherIdx) { const otherCard = cardDb[other.cardId]; if (otherCard && (otherCard.id === 'anakin' || otherCard.name === 'Anakin Skywalker')) { other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1; other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1; } } }); } // Энакин даёт +1/+1 Оби-Вану if (card.id === 'anakin' || card.name === 'Anakin Skywalker') { player.board.forEach((other, otherIdx) => { if (idx !== otherIdx) { const otherCard = cardDb[other.cardId]; if (otherCard && (otherCard.id === 'obiwan' || otherCard.name === 'Obi-Wan Kenobi')) { other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1; other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1; } } }); } // Йода даёт +1/+1 всем джедаям if (card.id === 'yoda' || card.name === 'Yoda') { player.board.forEach((other, otherIdx) => { if (idx !== otherIdx) { const otherCard = cardDb[other.cardId]; if (otherCard && otherCard.faction === 'rebellion' && (otherCard.name?.includes('Jedi') || otherCard.id === 'luke' || otherCard.id === 'obiwan' || otherCard.id === 'anakin' || otherCard.id === 'ahsoka' || otherCard.id === 'mace' || otherCard.id === 'quigon' || otherCard.id === 'plo_koon' || otherCard.id === 'ki_adi' || otherCard.id === 'aayla' || otherCard.id === 'shaak_ti' || otherCard.id === 'kanan' || otherCard.id === 'ezra' || otherCard.id === 'cal_kestis')) { other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1; other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1; } } }); } // Боба Фетт даёт +1/+1 Джанго Фетту if (card.id === 'boba' || card.name === 'Boba Fett') { player.board.forEach((other, otherIdx) => { if (idx !== otherIdx) { const otherCard = cardDb[other.cardId]; if (otherCard && (otherCard.id === 'jango' || otherCard.name === 'Jango Fett')) { other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1; other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1; } } }); } // Джанго Фетт даёт +1/+1 Бобе Фетту if (card.id === 'jango' || card.name === 'Jango Fett') { player.board.forEach((other, otherIdx) => { if (idx !== otherIdx) { const otherCard = cardDb[other.cardId]; if (otherCard && (otherCard.id === 'boba' || otherCard.name === 'Boba Fett')) { other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1; other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1; } } }); } // Эйсока даёт +1/+1 Рексу if (card.id === 'ahsoka' || card.name === 'Ahsoka Tano') { player.board.forEach((other, otherIdx) => { if (idx !== otherIdx) { const otherCard = cardDb[other.cardId]; if (otherCard && (otherCard.id === 'rex' || otherCard.name === 'Captain Rex')) { other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1; other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1; } } }); } // Рекс даёт +1/+1 Эйсоке if (card.id === 'rex' || card.name === 'Captain Rex') { player.board.forEach((other, otherIdx) => { if (idx !== otherIdx) { const otherCard = cardDb[other.cardId]; if (otherCard && (otherCard.id === 'ahsoka' || otherCard.name === 'Ahsoka Tano')) { other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1; other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1; } } }); } // Мандалорец даёт +1/+1 Грогу if (card.id === 'mando' || card.name === 'Din Djarin') { player.board.forEach((other, otherIdx) => { if (idx !== otherIdx) { const otherCard = cardDb[other.cardId]; if (otherCard && (otherCard.id === 'grogu' || otherCard.name === 'Grogu')) { other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1; other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1; } } }); } // Грогу даёт +1/+1 Мандалорцу if (card.id === 'grogu' || card.name === 'Grogu') { player.board.forEach((other, otherIdx) => { if (idx !== otherIdx) { const otherCard = cardDb[other.cardId]; if (otherCard && (otherCard.id === 'mando' || otherCard.name === 'Din Djarin')) { other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1; other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1; } } }); } // Таркин даёт +1/+1 имперским картам if (card.id === 'tarkin' || card.name === 'Grand Moff Tarkin') { player.board.forEach((other, otherIdx) => { if (idx !== otherIdx) { const otherCard = cardDb[other.cardId]; if (otherCard && otherCard.faction === 'empire' && otherCard.type === 'minion') { other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1; other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1; } } }); } // Дарт Мол даёт +1/+1 Сэвиджу Оппрессу if (card.id === 'maul' || card.name === 'Darth Maul') { player.board.forEach((other, otherIdx) => { if (idx !== otherIdx) { const otherCard = cardDb[other.cardId]; if (otherCard && (otherCard.id === 'savage' || otherCard.name === 'Savage Opress')) { other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1; other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1; } } }); } // Сэвидж Оппресс даёт +1/+1 Дарту Молу if (card.id === 'savage' || card.name === 'Savage Opress') { player.board.forEach((other, otherIdx) => { if (idx !== otherIdx) { const otherCard = cardDb[other.cardId]; if (otherCard && (otherCard.id === 'maul' || otherCard.name === 'Darth Maul')) { other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1; other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1; } } }); } // Падме даёт +1/+1 Энакину if (card.id === 'padme' || card.name === 'Padmé Amidala') { player.board.forEach((other, otherIdx) => { if (idx !== otherIdx) { const otherCard = cardDb[other.cardId]; if (otherCard && (otherCard.id === 'anakin' || otherCard.name === 'Anakin Skywalker')) { other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1; other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1; } } }); } // Энакин даёт +1/+1 Падме if (card.id === 'anakin' || card.name === 'Anakin Skywalker') { player.board.forEach((other, otherIdx) => { if (idx !== otherIdx) { const otherCard = cardDb[other.cardId]; if (otherCard && (otherCard.id === 'padme' || otherCard.name === 'Padmé Amidala')) { other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1; other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1; } } }); } }); }); } function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardIndex) { const gameState = room.gameState; const pi = findPlayerIndex(room, socketId); if (pi < 0 || gameState.currentPlayerIndex !== pi) return; const p = gameState.players[pi]; if (p.health <= 0 || p.isDead) return; // Мёртвый игрок не может играть const cid = p.hand[handIndex]; if (!cid) return; const card = getCard(room, cid); if (!card || card.type !== 'spell') return; const cost = card.cost || 0; if (p.mana < cost) return; const eff = card.spellEffect; const needTarget = card.spellTarget && card.spellTarget !== 'none'; // Специальная обработка для steal_cards - требует только выбор противника (не требует targetBoardIndex) if (eff === 'steal_cards') { if (targetPlayerIndex == null || targetPlayerIndex === pi) return; const targetPlayer = gameState.players[targetPlayerIndex]; if (!targetPlayer || targetPlayer.health <= 0 || !targetPlayer.deck || targetPlayer.deck.length === 0) return; // Отправляем запрос на выбор карт для кражи const socket = io.sockets.sockets.get(p.id); if (socket) { socket.emit('stealCardsRequest', { targetPlayerIndex: targetPlayerIndex, targetPlayerName: targetPlayer.name || `Игрок ${targetPlayerIndex + 1}`, targetDeckSize: targetPlayer.deck.length, maxCards: Math.min(2, targetPlayer.deck.length) }); } return; // Не тратим ману и не удаляем карту пока - это сделаем после выбора } // Для других заклинаний проверяем targetBoardIndex только если это не выбор игрока if (needTarget && card.spellTarget !== 'enemy_player' && (targetPlayerIndex == null || targetBoardIndex == null)) return; if (needTarget && card.spellTarget === 'enemy_player' && targetPlayerIndex == null) return; const enemies = gameState.players.filter((pl, i) => i !== pi && pl.health > 0); if (eff === 'deal_2') { if (targetBoardIndex === -1) { const t = gameState.players[targetPlayerIndex]; if (!t || t.health <= 0) return; t.health = Math.max(0, t.health - 2); gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, toPlayer: targetPlayerIndex, effect: 'deal_2_hero', damage: 2 }); } else { const t = gameState.players[targetPlayerIndex]; const m = t?.board?.[targetBoardIndex]; if (!m) return; m.health -= 2; gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, toPlayer: targetPlayerIndex, toIdx: targetBoardIndex, effect: 'deal_2_minion', damage: 2 }); if (m.health <= 0) { const dc = getCard(room, m.cardId); if (dc && dc.deathrattleId) runDeathrattle(room, dc, targetPlayerIndex); } } } else if (eff === 'draw_2') { const drawn = drawCards(p.deck, 2); if (drawn.length) p.hand.push(...drawn); gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, effect: 'draw_2' }); } else if (eff === 'buff_2_2') { const m = p.board[targetBoardIndex]; if (!m) return; m.attack += 2; m.health += 2; m.maxHealth = (m.maxHealth || m.health) + 2; gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, toIdx: targetBoardIndex, effect: 'buff_2_2' }); } else if (eff === 'deal_1_all_enemies') { gameState.players.forEach((pl, i) => { if (i === pi) return; pl.health = Math.max(0, pl.health - 1); pl.board.forEach((m) => { m.health -= 1; if (m.health <= 0) { const dc = getCard(room, m.cardId); if (dc && dc.deathrattleId) runDeathrattle(room, dc, i); } }); pl.board = pl.board.filter((m) => m.health > 0); }); gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, effect: 'deal_1_all_enemies' }); } else if (eff === 'freeze') { const t = gameState.players[targetPlayerIndex]; const m = t?.board?.[targetBoardIndex]; if (!m) return; m.frozen = true; gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, toPlayer: targetPlayerIndex, toIdx: targetBoardIndex, effect: 'freeze' }); } else if (eff === 'return_hand') { const t = gameState.players[targetPlayerIndex]; const m = t?.board?.[targetBoardIndex]; if (!m) return; t.board.splice(targetBoardIndex, 1); t.hand.push(m.cardId); gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, toPlayer: targetPlayerIndex, toIdx: targetBoardIndex, effect: 'return_hand' }); } else if (eff === 'deal_4') { if (targetBoardIndex === -1) { const t = gameState.players[targetPlayerIndex]; if (!t || t.health <= 0) return; t.health = Math.max(0, t.health - 4); gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, toPlayer: targetPlayerIndex, effect: 'deal_4_hero', damage: 4 }); } else { const t = gameState.players[targetPlayerIndex]; const m = t?.board?.[targetBoardIndex]; if (!m) return; m.health = Math.max(0, m.health - 4); gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, toPlayer: targetPlayerIndex, toIdx: targetBoardIndex, effect: 'deal_4_minion', damage: 4 }); if (m.health <= 0) { t.board.splice(targetBoardIndex, 1); const dc = getCard(room, m.cardId); if (dc && dc.deathrattleId) runDeathrattle(room, dc, targetPlayerIndex); } } } else if (eff === 'heal_4') { if (targetBoardIndex === -1) { const t = gameState.players[targetPlayerIndex]; if (!t) return; t.health = Math.min(30, t.health + 4); gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, toPlayer: targetPlayerIndex, effect: 'heal_4_hero' }); } else { const t = gameState.players[targetPlayerIndex]; const m = t?.board?.[targetBoardIndex]; if (!m) return; m.health = Math.min(m.maxHealth || m.health, m.health + 4); gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, toPlayer: targetPlayerIndex, toIdx: targetBoardIndex, effect: 'heal_4_minion' }); } } else if (eff === 'deal_3_minion') { const t = gameState.players[targetPlayerIndex]; const m = t?.board?.[targetBoardIndex]; if (!m) return; m.health = Math.max(0, m.health - 3); gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, toPlayer: targetPlayerIndex, toIdx: targetBoardIndex, effect: 'deal_3_minion', damage: 3 }); if (m.health <= 0) { t.board.splice(targetBoardIndex, 1); const dc = getCard(room, m.cardId); if (dc && dc.deathrattleId) runDeathrattle(room, dc, targetPlayerIndex); } } else if (eff === 'freeze_damage') { const t = gameState.players[targetPlayerIndex]; const m = t?.board?.[targetBoardIndex]; if (!m) return; m.frozen = true; m.health = Math.max(0, m.health - 2); gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, toPlayer: targetPlayerIndex, toIdx: targetBoardIndex, effect: 'freeze_damage', damage: 2 }); if (m.health <= 0) { t.board.splice(targetBoardIndex, 1); const dc = getCard(room, m.cardId); if (dc && dc.deathrattleId) runDeathrattle(room, dc, targetPlayerIndex); } } else if (eff === 'buff_all_friendly') { p.board.forEach((m) => { m.attack += 1; m.health += 1; m.maxHealth = (m.maxHealth || m.health) + 1; }); gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, effect: 'buff_all_friendly' }); } else if (eff === 'destroy_weak_2') { enemies.forEach((enemy) => { if (enemy.board && enemy.board.length > 0) { const weak = enemy.board.filter(m => m.attack <= 2); weak.forEach((target) => { const boardIdx = enemy.board.indexOf(target); const targetCard = getCard(room, target.cardId); enemy.board.splice(boardIdx, 1); if (targetCard && targetCard.deathrattleId) { runDeathrattle(room, targetCard, gameState.players.indexOf(enemy)); } }); } }); gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, effect: 'destroy_weak_2' }); } else if (eff === 'buff_3_2') { const m = p.board[targetBoardIndex]; if (!m) return; m.attack += 3; m.health += 2; m.maxHealth = (m.maxHealth || m.health) + 2; gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, toIdx: targetBoardIndex, effect: 'buff_3_2' }); } else return; p.mana -= cost; p.hand.splice(handIndex, 1); // Применяем синергии после размещения карты applySynergies(room); checkGameOver(room); broadcastGameState(room); } function stealCardsFromDeck(room, socketId, handIndex, targetPlayerIndex, cardIndices) { const gameState = room.gameState; const pi = findPlayerIndex(room, socketId); if (pi < 0 || gameState.currentPlayerIndex !== pi) return; const p = gameState.players[pi]; if (p.health <= 0 || p.isDead) return; const cid = p.hand[handIndex]; if (!cid) return; const card = getCard(room, cid); if (!card || card.type !== 'spell' || card.spellEffect !== 'steal_cards') return; const cost = card.cost || 0; if (p.mana < cost) return; const targetPlayer = gameState.players[targetPlayerIndex]; if (!targetPlayer || targetPlayerIndex === pi || targetPlayer.health <= 0) return; if (!targetPlayer.deck || targetPlayer.deck.length === 0) return; // Проверяем индексы карт if (!Array.isArray(cardIndices) || cardIndices.length === 0 || cardIndices.length > 2) return; const validIndices = cardIndices.filter(idx => idx >= 0 && idx < targetPlayer.deck.length); if (validIndices.length === 0) return; // Убираем дубликаты и сортируем по убыванию (чтобы удалять с конца) const uniqueIndices = [...new Set(validIndices)].sort((a, b) => b - a); // Крадём карты const stolenCards = []; uniqueIndices.forEach(idx => { if (idx >= 0 && idx < targetPlayer.deck.length) { stolenCards.push(targetPlayer.deck[idx]); targetPlayer.deck.splice(idx, 1); } }); // Добавляем украденные карты в руку игрока stolenCards.forEach(cardId => { if (p.hand.length < 10) { p.hand.push(cardId); } }); // Тратим ману и удаляем заклинание p.mana -= cost; p.hand.splice(handIndex, 1); gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, toPlayer: targetPlayerIndex, effect: 'steal_cards', stolenCount: stolenCards.length }); // Применяем синергии после изменений на доске applySynergies(room); // Применяем синергии после боя applySynergies(room); // Применяем синергии после изменений на доске applySynergies(room); checkGameOver(room); broadcastGameState(room); } /** Выбор существующей карты из БД по правилу «лучше по характеристикам» (выше стоимость, та же фракция). */ function pickForgeResultCard(cardIds) { const ids = Array.isArray(cardIds) ? cardIds.slice() : []; if (ids.length < 2 || ids.length > 3) return null; const cards = ids.map((id) => cardDb[id]).filter(Boolean); if (cards.length !== ids.length || cards.some((c) => c.type !== 'minion')) return null; const base = cards.reduce((a, b) => ((a.cost || 0) >= (b.cost || 0) ? a : b)); const baseCost = base.cost || 0; const faction = base.faction || 'neutral'; const targetCost = Math.min(10, baseCost + 1); const exclude = new Set(ids); function pool(filter) { return Object.keys(cardDb).filter((id) => { const c = cardDb[id]; if (!c || c.type !== 'minion' || exclude.has(id)) return false; return filter(c, id); }); } let list = pool((c) => (c.faction || 'neutral') === faction && (c.cost || 0) === targetCost); if (list.length === 0) list = pool((c) => (c.faction || 'neutral') === faction && (c.cost || 0) >= targetCost); if (list.length === 0) list = pool((c) => (c.faction || 'neutral') === 'neutral' && (c.cost || 0) >= targetCost); if (list.length === 0) list = pool((c) => (c.cost || 0) >= targetCost); if (list.length === 0) return null; list.sort(); const resultCardId = list[0]; return { resultCardId }; } function forgeCard(room, socketId, cardIds) { const gameState = room.gameState; const pi = findPlayerIndex(room, socketId); if (pi < 0 || gameState.currentPlayerIndex !== pi) return; const p = gameState.players[pi]; if (p.health <= 0 || p.isDead) return; if ((p.mana || 0) < 2) return; const ids = Array.isArray(cardIds) ? cardIds : []; if (ids.length < 2 || ids.length > 3) return; const hand = p.hand || []; const toRemove = ids.filter((id) => hand.includes(id)); if (toRemove.length !== ids.length) return; const cards = toRemove.map((id) => cardDb[id]).filter(Boolean); if (cards.length !== toRemove.length || cards.some((c) => c.type !== 'minion')) return; const result = pickForgeResultCard(ids); if (!result) return; toRemove.forEach((id) => { const idx = p.hand.indexOf(id); if (idx >= 0) p.hand.splice(idx, 1); }); if (p.hand.length < 10) p.hand.push(result.resultCardId); p.mana = Math.max(0, (p.mana || 0) - 2); gameState.log.push({ type: 'forge', fromPlayer: pi, resultCardId: result.resultCardId }); applySynergies(room); checkGameOver(room); broadcastGameState(room); } function heroAbility(room, socketId, targetPlayerIndex, targetBoardIndex) { const gameState = room.gameState; const pi = findPlayerIndex(room, socketId); if (pi < 0 || gameState.currentPlayerIndex !== pi) return; const p = gameState.players[pi]; if (p.health <= 0 || p.isDead) return; // Мёртвый игрок не может играть if (p.heroAbilityUsed || p.mana < 2) return; if (targetPlayerIndex == null || targetBoardIndex == null) return; const targetPlayer = gameState.players[targetPlayerIndex]; if (!targetPlayer) return; if (targetBoardIndex === -1) { targetPlayer.health = Math.max(0, targetPlayer.health - 1); gameState.log.push({ type: 'heroAbility', fromPlayer: pi, toPlayer: targetPlayerIndex, effect: 'deal_1_hero' }); } else { const m = targetPlayer.board[targetBoardIndex]; if (!m) return; m.health -= 1; gameState.log.push({ type: 'heroAbility', fromPlayer: pi, toPlayer: targetPlayerIndex, toIdx: targetBoardIndex, effect: 'deal_1_minion' }); } p.mana -= 2; p.heroAbilityUsed = true; if (targetBoardIndex !== -1) { const m = targetPlayer.board[targetBoardIndex]; if (m && m.health <= 0) { const card = getCard(room, m.cardId); if (card && card.deathrattleId) runDeathrattle(room, card, targetPlayerIndex); } } gameState.players.forEach((pl) => { pl.board = pl.board.filter((min) => min.health > 0); }); // Применяем синергии после изменений на доске applySynergies(room); // Применяем синергии после изменений на доске applySynergies(room); checkGameOver(room); broadcastGameState(room); } function attack(room, socketId, attackerBoardIndex, targetPlayerIndex, targetBoardIndex) { const gameState = room.gameState; const pi = findPlayerIndex(room, socketId); if (pi < 0 || gameState.currentPlayerIndex !== pi) return; const p = gameState.players[pi]; if (p.health <= 0 || p.isDead) return; // Мёртвый игрок не может атаковать const attacker = p.board[attackerBoardIndex]; if (!attacker || !attacker.canAttack) return; const targetPlayer = gameState.players[targetPlayerIndex]; if (!targetPlayer) return; // Применяем синергии перед расчётом урона applySynergies(room); if (targetBoardIndex === -1) { const attackerAttack = attacker.attack + (attacker.synergyAttackBonus || 0); targetPlayer.health = Math.max(0, targetPlayer.health - attackerAttack); gameState.log.push({ type: 'attackHero', fromPlayer: pi, toPlayer: targetPlayerIndex, attackerMinionId: attacker.id, damage: attacker.attack, }); // Применяем синергии перед расчётом урона applySynergies(room); checkGameOver(room); // Проверяем после атаки по герою } else { const target = targetPlayer.board[targetBoardIndex]; if (!target) return; // Применяем синергии перед расчётом урона applySynergies(room); // Учитываем бонусы синергий при расчёте урона (бонусы уже применены в applySynergies) const attackerAttack = attacker.attack + (attacker.synergyAttackBonus || 0); const targetAttack = target.attack + (target.synergyAttackBonus || 0); target.health -= attackerAttack; attacker.health -= targetAttack; const attackerDied = attacker.health <= 0; const targetDied = target.health <= 0; gameState.log.push({ type: 'attack', fromPlayer: pi, toPlayer: targetPlayerIndex, fromIdx: attackerBoardIndex, toIdx: targetBoardIndex, attackerMinionId: attacker.id, targetMinionId: target.id, attackerDied, targetDied, damage: attacker.attack, reverseDamage: target.attack, }); const targetCard = getCard(room, target.cardId); const attackerCard = getCard(room, attacker.cardId); if (targetDied && targetCard && targetCard.deathrattleId) { runDeathrattle(room, targetCard, targetPlayerIndex); } if (attackerDied && attackerCard && attackerCard.deathrattleId) { runDeathrattle(room, attackerCard, pi); } } // Механика двойной атаки const attackerCard = getCard(room, attacker.cardId); if (attackerCard && attackerCard.canAttackTwice && !attacker.attacksUsed) { attacker.attacksUsed = 1; // Использована одна атака } else { attacker.canAttack = false; attacker.attacksUsed = 0; } [...gameState.players].forEach((pl) => { pl.board = pl.board.filter((m) => m.health > 0); }); checkGameOver(room); broadcastGameState(room); } io.on('connection', (socket) => { socket.on('createRoom', (data) => { const name = typeof data === 'string' ? data : (data?.name || 'Host'); const aiMode = typeof data === 'object' && data?.aiMode === true; const code = generateRoomCode(); const room = { code, lobby: [{ id: socket.id, name: name || 'Host' }], gameState: null, gameStarted: false, faction: null, aiMode: aiMode, turnTimerInterval: null, turnTimeLeft: TURN_SECONDS, turnTimerWarnFired: false, }; rooms.set(code, room); socket.join(code); socket.emit('roomCreated', { code, you: room.lobby[0], lobby: room.lobby, serverIP: serverIP, serverPort: serverPort, aiMode: aiMode, }); cleanupEmptyRooms(); }); socket.on('joinRoom', (data) => { const { code, name } = data || {}; if (!code) { socket.emit('error', 'Код комнаты не указан'); return; } const room = getRoomByCode(code); if (!room) { socket.emit('error', 'Комната не найдена'); return; } if (room.gameStarted) { socket.emit('error', 'Игра уже началась'); return; } if (room.lobby.length >= MAX_PLAYERS) { socket.emit('error', 'Комната заполнена'); return; } room.lobby.push({ id: socket.id, name: name || `Player ${room.lobby.length + 1}` }); socket.join(code); io.to(code).emit('lobbyUpdate', room.lobby); socket.emit('roomJoined', { code, you: room.lobby[room.lobby.length - 1], lobby: room.lobby }); cleanupEmptyRooms(); }); socket.on('leaveLobby', () => { const room = getRoomBySocket(socket.id); if (!room) return; room.lobby = room.lobby.filter((p) => p.id !== socket.id); socket.leave(room.code); if (room.lobby.length === 0 && !room.gameStarted) { clearTurnTimer(room); rooms.delete(room.code); } else { io.to(room.code).emit('lobbyUpdate', room.lobby); } cleanupEmptyRooms(); }); socket.on('startGame', (data) => { const room = getRoomBySocket(socket.id); if (!room) { socket.emit('error', 'Комната не найдена'); return; } if (room.lobby[0]?.id !== socket.id) { socket.emit('error', 'Только хост может начать игру'); return; } // В режиме ИИ можно начать игру с одним игроком if (!room.aiMode && room.lobby.length < MIN_PLAYERS) { socket.emit('error', `Нужно минимум ${MIN_PLAYERS} игроков`); return; } const validFactions = ['rebellion', 'empire', 'pirates', 'mandalorians']; const faction = data?.faction && validFactions.includes(data.faction) ? data.faction : null; room.faction = faction; initGame(room); }); socket.on('playCard', (data) => { const room = getRoomBySocket(socket.id); if (!room || !room.gameState || room.gameState.phase !== 'playing') return; playCard(room, socket.id, data.handIndex, data.boardPos); }); socket.on('playSpell', (data) => { const room = getRoomBySocket(socket.id); if (!room || !room.gameState || room.gameState.phase !== 'playing') return; playSpell(room, socket.id, data.handIndex, data.targetPlayerIndex, data.targetBoardIndex); }); socket.on('heroAbility', (data) => { const room = getRoomBySocket(socket.id); if (!room || !room.gameState || room.gameState.phase !== 'playing') return; heroAbility(room, socket.id, data.targetPlayerIndex, data.targetBoardIndex); }); socket.on('attack', (data) => { const room = getRoomBySocket(socket.id); if (!room || !room.gameState || room.gameState.phase !== 'playing') return; attack( room, socket.id, data.attackerBoardIndex, data.targetPlayerIndex, data.targetBoardIndex ?? -1 ); }); socket.on('endTurn', () => { const room = getRoomBySocket(socket.id); if (!room || !room.gameState || room.gameState.phase !== 'playing') return; if (findPlayerIndex(room, socket.id) !== room.gameState.currentPlayerIndex) return; endTurn(room); }); socket.on('drawCard', () => { const room = getRoomBySocket(socket.id); if (!room || !room.gameState || room.gameState.phase !== 'playing') return; manualDraw(room, socket.id); }); socket.on('forgePreview', (data) => { const room = getRoomBySocket(socket.id); if (!room || !room.gameState || room.gameState.phase !== 'playing') return; const pi = findPlayerIndex(room, socket.id); if (pi < 0) return; const p = room.gameState.players[pi]; const ids = Array.isArray(data?.cardIds) ? data.cardIds : []; const requestId = data?.requestId; if (ids.length < 2 || ids.length > 3) { socket.emit('forgePreviewResult', { error: true, requestId }); return; } const hand = p.hand || []; const ok = ids.every((id) => hand.includes(id)); if (!ok) { socket.emit('forgePreviewResult', { error: true, requestId }); return; } const cards = ids.map((id) => cardDb[id]).filter(Boolean); if (cards.length !== ids.length || cards.some((c) => c.type !== 'minion')) { socket.emit('forgePreviewResult', { error: true, requestId }); return; } const result = pickForgeResultCard(ids); if (!result) { socket.emit('forgePreviewResult', { error: true, requestId }); return; } socket.emit('forgePreviewResult', { resultCardId: result.resultCardId, requestId }); }); socket.on('forgeCard', (data) => { const room = getRoomBySocket(socket.id); if (!room || !room.gameState || room.gameState.phase !== 'playing') return; forgeCard(room, socket.id, data.cardIds); }); socket.on('stealCards', (data) => { const room = getRoomBySocket(socket.id); if (!room || !room.gameState || room.gameState.phase !== 'playing') return; stealCardsFromDeck(room, socket.id, data.handIndex, data.targetPlayerIndex, data.cardIndices); }); socket.on('chatMessage', (data) => { const room = getRoomBySocket(socket.id); if (!room) return; const message = (data?.message || '').trim(); if (message.length === 0 || message.length > 200) return; // Находим имя игрока let playerName = 'Игрок'; if (room.gameState && room.gameState.players) { const player = room.gameState.players.find(p => p.id === socket.id); if (player) { playerName = player.name || 'Игрок'; } } else if (room.lobby) { const lobbyPlayer = room.lobby.find(p => p.id === socket.id); if (lobbyPlayer) { playerName = lobbyPlayer.name || 'Игрок'; } } // Отправляем сообщение всем в комнате io.to(room.code).emit('chatMessage', { playerName: playerName, message: message }); }); socket.on('battlecryTarget', (data) => { const room = getRoomBySocket(socket.id); if (!room || !room.gameState || room.gameState.phase !== 'playing') return; const gameState = room.gameState; const pi = findPlayerIndex(room, socket.id); if (pi < 0) return; const p = gameState.players[pi]; if (p.health <= 0 || p.isDead) return; if (data.battlecryId === 'return_hand_enemy') { const targetPlayerIndex = data.targetPlayerIndex; const targetBoardIndex = data.targetBoardIndex; const targetPlayer = gameState.players[targetPlayerIndex]; if (!targetPlayer || targetPlayerIndex === pi || targetPlayer.health <= 0) return; if (!targetPlayer.board || targetPlayer.board.length === 0) return; if (targetBoardIndex == null || targetBoardIndex < 0 || targetBoardIndex >= targetPlayer.board.length) return; const target = targetPlayer.board[targetBoardIndex]; targetPlayer.board.splice(targetBoardIndex, 1); if (targetPlayer.hand.length < 10) { targetPlayer.hand.push(target.cardId); } // Удаляем pendingBattlecry с миньона const playedMinion = p.board?.find(m => m.pendingBattlecry && m.pendingBattlecry.battlecryId === 'return_hand_enemy'); if (playedMinion) { delete playedMinion.pendingBattlecry; } gameState.log.push({ type: 'battlecry', effect: 'return_hand_enemy', fromPlayer: pi, toPlayer: targetPlayerIndex, toIdx: targetBoardIndex }); // Применяем синергии после изменений на доске applySynergies(room); checkGameOver(room); broadcastGameState(room); } }); socket.on('resetToLobby', () => { const room = getRoomBySocket(socket.id); if (!room || !room.gameState || !room.gameState.players?.length) return; const hostId = room.gameState.players[0].id; if (socket.id !== hostId) return; clearTurnTimer(room); room.lobby = room.gameState.players.map((p) => ({ id: p.id, name: p.name })); room.gameState = null; room.gameStarted = false; io.to(room.code).emit('backToLobby'); }); socket.on('disconnect', () => { const room = getRoomBySocket(socket.id); if (!room) return; if (!room.gameStarted) { room.lobby = room.lobby.filter((p) => p.id !== socket.id); socket.leave(room.code); if (room.lobby.length === 0) { clearTurnTimer(room); rooms.delete(room.code); } else { io.to(room.code).emit('lobbyUpdate', room.lobby); } cleanupEmptyRooms(); return; } if (room.gameState) { const idx = findPlayerIndex(room, socket.id); if (idx >= 0) { room.gameState.players[idx].health = 0; const alive = room.gameState.players.filter((p) => p.health > 0); if (alive.length <= 1) { room.gameState.phase = 'ended'; room.gameState.winner = alive[0] || null; } broadcastGameState(room); } } }); }); let serverIP = 'localhost'; let serverPort = PORT; server.listen(PORT, '0.0.0.0', () => { const os = require('os'); const nets = os.networkInterfaces(); let lan = 'localhost'; for (const name of Object.keys(nets)) { for (const n of nets[name]) { if (n.family === 'IPv4' && !n.internal) { lan = n.address; break; } } } serverIP = lan; serverPort = PORT; console.log(` ╔══════════════════════════════════════════════════════════╗ ║ STAR WARS HEARTHSTONE - Server running ║ ║ Local: http://localhost:${PORT} ║ ║ LAN: http://${lan}:${PORT} (use for Radmin VPN) ║ ╚══════════════════════════════════════════════════════════╝ `); });