2123 lines
84 KiB
JavaScript
2123 lines
84 KiB
JavaScript
/**
|
||
* 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 weakest = enemies.reduce((min, p) => (p.health < min.health ? p : min), enemies[0]);
|
||
const weakEnemies = enemies.filter(p => p.health === weakest.health);
|
||
const targetEnemy = weakEnemies[Math.floor(Math.random() * weakEnemies.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;
|
||
}
|
||
|
||
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 weakest = enemies.reduce((min, p) => (p.health < min.health ? p : min), enemies[0]);
|
||
const weakEnemies = enemies.filter(p => p.health === weakest.health);
|
||
const targetEnemy = weakEnemies[Math.floor(Math.random() * weakEnemies.length)];
|
||
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) ║
|
||
╚══════════════════════════════════════════════════════════╝
|
||
`);
|
||
});
|