/** * 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, spectators: [] } 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) { const starWarsHeroes = [ 'Darth Maul', 'Count Dooku', 'General Grievous', 'Grand Moff Tarkin', 'Admiral Thrawn', 'Kylo Ren', 'Snoke', 'General Hux', 'Captain Phasma', 'Moff Gideon', 'Cad Bane', 'Asajj Ventress', 'Savage Opress', 'Darth Sidious', 'Darth Tyranus', 'Darth Plagueis', 'Jango Fett', 'Boba Fett', 'IG-88', 'Bossk', 'Dengar', 'Zuckuss', '4-LOM', 'Aurra Sing' ]; const randomHero = starWarsHeroes[Math.floor(Math.random() * starWarsHeroes.length)]; players.push({ id: 'AI_' + Date.now(), name: randomHero, 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) { // Выбираем врага: 70% шанс выбрать самого слабого, 30% - случайно среди всех let targetEnemy; if (Math.random() < 0.7 && enemies.length > 1) { const weakest = enemies.reduce((min, p) => (p.health < min.health ? p : min), enemies[0]); const weakEnemies = enemies.filter(p => p.health === weakest.health); targetEnemy = weakEnemies[Math.floor(Math.random() * weakEnemies.length)]; } else { targetEnemy = enemies[Math.floor(Math.random() * enemies.length)]; } 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; } const targetPlayerIndex = gameState.players.findIndex((p, i) => i !== aiPlayerIndex && p === targetEnemy); playSpell(room, aiPlayer.id, playable.index, targetPlayerIndex, 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); // Выбираем врага: 70% шанс выбрать самого слабого, 30% - случайно среди всех let targetEnemy; if (Math.random() < 0.7 && enemies.length > 1) { const weakest = enemies.reduce((min, p) => (p.health < min.health ? p : min), enemies[0]); const weakEnemies = enemies.filter(p => p.health === weakest.health); targetEnemy = weakEnemies[Math.floor(Math.random() * weakEnemies.length)]; } else { targetEnemy = enemies[Math.floor(Math.random() * enemies.length)]; } const targetPlayerIndex = gameState.players.findIndex((p, i) => i !== aiPlayerIndex && p === 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; if (room.spectators?.some((s) => s.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.aura === 'jedi_debuff_enemy') { gameState.players.forEach((enemyPlayer, enemyPlayerIndex) => { if (enemyPlayerIndex !== playerIndex && enemyPlayer.board) { enemyPlayer.board.forEach((enemyMinion) => { const enemyCard = cardDb[enemyMinion.cardId]; if (enemyCard && (enemyCard.name?.includes('Jedi') || enemyCard.id === 'luke' || enemyCard.id === 'obiwan' || enemyCard.id === 'ahsoka' || enemyCard.id === 'mace' || enemyCard.id === 'quigon' || enemyCard.id === 'plo_koon' || enemyCard.id === 'ki_adi' || enemyCard.id === 'aayla' || enemyCard.id === 'shaak_ti' || enemyCard.id === 'kanan' || enemyCard.id === 'ezra' || enemyCard.id === 'cal_kestis' || enemyCard.id === 'yoda' || enemyCard.id === 'anakin')) { enemyMinion.synergyAttackBonus = (enemyMinion.synergyAttackBonus || 0) - 1; enemyMinion.synergyHealthBonus = (enemyMinion.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' || otherCard.id === 'padme' || otherCard.name === 'Padmé Amidala' || otherCard.id === 'ahsoka' || otherCard.name === 'Ahsoka Tano')) { other.synergyAttackBonus = (other.synergyAttackBonus || 0) + 1; other.synergyHealthBonus = (other.synergyHealthBonus || 0) + 1; } } }); // Аура: Пока Анакин на поле, все джедаи получают +1/+1 if (card.aura === 'jedi_buff_all') { player.board.forEach((other, otherIdx) => { if (idx !== otherIdx) { const otherCard = cardDb[other.cardId]; if (otherCard && (otherCard.name?.includes('Jedi') || otherCard.id === 'luke' || otherCard.id === 'obiwan' || 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' || otherCard.id === 'yoda')) { 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 - требует выбор противника и карт с его доски if (eff === 'steal_cards') { if (targetPlayerIndex == null || targetPlayerIndex === pi) return; const targetPlayer = gameState.players[targetPlayerIndex]; if (!targetPlayer || targetPlayer.health <= 0 || !targetPlayer.board || targetPlayer.board.length === 0) return; // Отправляем запрос на выбор карт для кражи const socket = io.sockets.sockets.get(p.id); if (socket) { socket.emit('stealCardsRequest', { targetPlayerIndex: targetPlayerIndex, targetPlayerName: targetPlayer.name || `Игрок ${targetPlayerIndex + 1}`, targetBoardSize: targetPlayer.board.length, maxCards: Math.min(2, targetPlayer.board.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.board || targetPlayer.board.length === 0) return; // Проверяем индексы карт на доске if (!Array.isArray(cardIndices) || cardIndices.length === 0 || cardIndices.length > 2) return; const validIndices = cardIndices.filter(idx => idx >= 0 && idx < targetPlayer.board.length); if (validIndices.length === 0) return; // Убираем дубликаты и сортируем по убыванию (чтобы удалять с конца) const uniqueIndices = [...new Set(validIndices)].sort((a, b) => b - a); // Крадём карты с доски противника const stolenCards = []; uniqueIndices.forEach(boardIdx => { if (boardIdx >= 0 && boardIdx < targetPlayer.board.length) { const minion = targetPlayer.board[boardIdx]; if (minion && minion.cardId) { stolenCards.push(minion.cardId); targetPlayer.board.splice(boardIdx, 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, spectators: [], }; 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('getRoomsList', () => { const roomsList = Array.from(rooms.entries()).map(([code, room]) => ({ code, playerCount: room.lobby?.length || 0, maxPlayers: MAX_PLAYERS, gameStarted: room.gameStarted || false, hasAI: room.aiMode || false, players: room.lobby?.map(p => p.name) || [], spectators: (room.spectators || []).length })); socket.emit('roomsList', roomsList); }); 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) { // Игра уже началась - подключаемся как наблюдатель if (!room.spectators) room.spectators = []; room.spectators.push({ id: socket.id, name: name || 'Наблюдатель' }); socket.join(code); socket.emit('joinedAsSpectator', { code, gameState: room.gameState }); broadcastGameState(room); // Отправляем текущее состояние игры 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 })); // Сохраняем настройки комнаты const savedFaction = room.faction; const savedAIMode = room.aiMode; room.gameState = null; room.gameStarted = false; room.faction = savedFaction; room.aiMode = savedAIMode; // Очищаем наблюдателей if (room.spectators) room.spectators = []; io.to(room.code).emit('backToLobby', { lobby: room.lobby }); }); socket.on('disconnect', () => { const room = getRoomBySocket(socket.id); if (!room) return; // Проверяем, был ли это наблюдатель if (room.spectators) { const specIdx = room.spectators.findIndex(s => s.id === socket.id); if (specIdx >= 0) { room.spectators.splice(specIdx, 1); socket.leave(room.code); 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) ║ ╚══════════════════════════════════════════════════════════╝ `); });