Files
Star-wars-card-game/server.js
2026-01-26 01:29:14 +03:00

1235 lines
49 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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');
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,
}));
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);
}
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 mergedCardDb = { ...cardDb, ...(room.gameState.forgedCards || {}) };
const yourView = {
...room.gameState,
yourIndex: i,
yourHand: [...p.hand],
yourDeckCount: p.deck.length,
yourManualDrawUsed: !!p.manualDrawUsed,
yourHeroAbilityUsed: !!p.heroAbilityUsed,
cardDb: mergedCardDb,
};
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 = cardDb[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 = cardDb[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 = cardDb[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 });
checkGameOver(room);
startTurnTimer(room);
broadcastGameState(room);
}
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, cardDb[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 = cardDb[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, cardDb[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 = cardDb[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' && 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)];
const boardIdx = enemy.board.indexOf(target);
enemy.board.splice(boardIdx, 1);
enemy.hand.push(target.cardId);
gameState.log.push({ type: 'battlecry', effect: 'return_hand_enemy', fromPlayer: playerIndex, toPlayer: enemyIdx, toIdx: boardIdx });
}
} 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 = cardDb[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, cardDb[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 card = cardDb[m.cardId];
if (card && card.deathrattleId) runDeathrattle(room, card, 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 = cardDb[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 = cardDb[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 });
if (card.battlecryId) runBattlecry(room, card, pi);
checkGameOver(room);
broadcastGameState(room);
}
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 = cardDb[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';
if (needTarget && (targetPlayerIndex == null || targetBoardIndex == 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 card = cardDb[m.cardId];
if (card && card.deathrattleId) runDeathrattle(room, card, 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 card = cardDb[m.cardId];
if (card && card.deathrattleId) runDeathrattle(room, card, 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 card = cardDb[m.cardId];
if (card && card.deathrattleId) runDeathrattle(room, card, 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 card = cardDb[m.cardId];
if (card && card.deathrattleId) runDeathrattle(room, card, 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 card = cardDb[m.cardId];
if (card && card.deathrattleId) runDeathrattle(room, card, 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 = cardDb[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);
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 = cardDb[m.cardId];
if (card && card.deathrattleId) runDeathrattle(room, card, targetPlayerIndex);
}
}
gameState.players.forEach((pl) => {
pl.board = pl.board.filter((min) => min.health > 0);
});
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;
if (targetBoardIndex === -1) {
targetPlayer.health = Math.max(0, targetPlayer.health - attacker.attack);
gameState.log.push({
type: 'attackHero',
fromPlayer: pi,
toPlayer: targetPlayerIndex,
attackerMinionId: attacker.id,
damage: attacker.attack,
});
checkGameOver(room); // Проверяем после атаки по герою
} else {
const target = targetPlayer.board[targetBoardIndex];
if (!target) return;
target.health -= attacker.attack;
attacker.health -= target.attack;
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 = cardDb[target.cardId];
const attackerCard = cardDb[attacker.cardId];
if (targetDied && targetCard && targetCard.deathrattleId) {
runDeathrattle(room, targetCard, targetPlayerIndex);
}
if (attackerDied && attackerCard && attackerCard.deathrattleId) {
runDeathrattle(room, attackerCard, pi);
}
}
// Механика двойной атаки
const attackerCard = cardDb[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', (name) => {
const code = generateRoomCode();
const room = {
code,
lobby: [{ id: socket.id, name: name || 'Host' }],
gameState: null,
gameStarted: false,
faction: null,
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,
});
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.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('forgeCard', (data) => {
const room = getRoomBySocket(socket.id);
if (!room || !room.gameState || room.gameState.phase !== 'playing') return;
forgeCard(room, socket.id, data.cardIds);
});
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) ║
╚══════════════════════════════════════════════════════════╝
`);
});