123
This commit is contained in:
171
server.js
171
server.js
@ -23,6 +23,10 @@ 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;
|
||||
@ -184,7 +188,7 @@ function makeAITurn(room) {
|
||||
// 1. Играем карты (приоритет дешёвым и эффективным)
|
||||
const playableCards = aiPlayer.hand
|
||||
.map((cardId, index) => {
|
||||
const card = cardDb[cardId];
|
||||
const card = getCard(room, cardId);
|
||||
if (!card) return null;
|
||||
const cost = card.cost || 0;
|
||||
if (cost > aiPlayer.mana) return null;
|
||||
@ -385,16 +389,15 @@ function broadcastGameState(room) {
|
||||
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],
|
||||
yourDeck: [...p.deck], // Передаем колоду для кузницы
|
||||
yourDeck: [...p.deck],
|
||||
yourDeckCount: p.deck.length,
|
||||
yourManualDrawUsed: !!p.manualDrawUsed,
|
||||
yourHeroAbilityUsed: !!p.heroAbilityUsed,
|
||||
cardDb: mergedCardDb,
|
||||
cardDb: cardDb,
|
||||
};
|
||||
socket.emit('gameState', yourView);
|
||||
}
|
||||
@ -430,7 +433,7 @@ function endTurn(room) {
|
||||
const prevPlayer = gameState.players[prev];
|
||||
if (prevPlayer) {
|
||||
prevPlayer.board.forEach((m) => {
|
||||
const card = cardDb[m.cardId];
|
||||
const card = getCard(room, m.cardId);
|
||||
if (!card) return;
|
||||
|
||||
if (card.name === 'Храм джедаев') {
|
||||
@ -461,7 +464,7 @@ function endTurn(room) {
|
||||
if (weak.length > 0) {
|
||||
const target = weak[Math.floor(Math.random() * weak.length)];
|
||||
const boardIdx = enemy.board.indexOf(target);
|
||||
const targetCard = cardDb[target.cardId];
|
||||
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) {
|
||||
@ -545,7 +548,7 @@ function endTurn(room) {
|
||||
|
||||
np.board.forEach((m) => {
|
||||
if (m.frozen) { m.frozen = false; return; }
|
||||
const card = cardDb[m.cardId];
|
||||
const card = getCard(room, m.cardId);
|
||||
if (card && card.attack > 0) {
|
||||
m.canAttack = true;
|
||||
if (card.canAttackTwice) {
|
||||
@ -639,7 +642,7 @@ function runBattlecry(room, card, playerIndex) {
|
||||
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);
|
||||
runDeathrattle(room, getCard(room, target.cardId), enemyIdx);
|
||||
}
|
||||
}
|
||||
} else if (id === 'destroy_weak_minion' && enemies.length) {
|
||||
@ -650,7 +653,7 @@ function runBattlecry(room, card, playerIndex) {
|
||||
if (weak.length > 0) {
|
||||
const target = weak[Math.floor(Math.random() * weak.length)];
|
||||
const boardIdx = enemy.board.indexOf(target);
|
||||
const targetCard = cardDb[target.cardId];
|
||||
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) {
|
||||
@ -685,7 +688,7 @@ function runBattlecry(room, card, playerIndex) {
|
||||
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);
|
||||
runDeathrattle(room, getCard(room, target.cardId), idx);
|
||||
}
|
||||
} else {
|
||||
t.health = Math.max(0, t.health - 3);
|
||||
@ -706,7 +709,7 @@ function runBattlecry(room, card, playerIndex) {
|
||||
if (weak.length > 0) {
|
||||
const target = weak[Math.floor(Math.random() * weak.length)];
|
||||
const boardIdx = enemy.board.indexOf(target);
|
||||
const targetCard = cardDb[target.cardId];
|
||||
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) {
|
||||
@ -758,7 +761,7 @@ function runBattlecry(room, card, playerIndex) {
|
||||
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];
|
||||
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) {
|
||||
@ -793,7 +796,7 @@ function runBattlecry(room, card, playerIndex) {
|
||||
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);
|
||||
runDeathrattle(room, getCard(room, target.cardId), enemyIdx);
|
||||
}
|
||||
} else {
|
||||
enemy.health = Math.max(0, enemy.health - 4);
|
||||
@ -805,8 +808,8 @@ function runBattlecry(room, card, playerIndex) {
|
||||
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));
|
||||
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);
|
||||
@ -832,7 +835,7 @@ function runBattlecry(room, card, playerIndex) {
|
||||
if (strong.length > 0) {
|
||||
const target = strong[Math.floor(Math.random() * strong.length)];
|
||||
const boardIdx = enemy.board.indexOf(target);
|
||||
const targetCard = cardDb[target.cardId];
|
||||
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) {
|
||||
@ -967,7 +970,7 @@ function playCard(room, socketId, handIndex, boardPos) {
|
||||
if (p.health <= 0 || p.isDead) return; // Мёртвый игрок не может играть
|
||||
const cid = p.hand[handIndex];
|
||||
if (!cid) return;
|
||||
const card = cardDb[cid];
|
||||
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;
|
||||
@ -1004,7 +1007,7 @@ function playCard(room, socketId, handIndex, boardPos) {
|
||||
playerIndex: enemyIdx,
|
||||
boardIndex: boardIdx,
|
||||
playerName: enemy.name || `Игрок ${enemyIdx + 1}`,
|
||||
minionName: cardDb[m.cardId]?.name || m.cardId
|
||||
minionName: getCard(room, m.cardId)?.name || m.cardId
|
||||
}));
|
||||
})
|
||||
});
|
||||
@ -1026,7 +1029,7 @@ function playCard(room, socketId, handIndex, boardPos) {
|
||||
// Функция для применения синергий между картами
|
||||
function applySynergies(room) {
|
||||
const gameState = room.gameState;
|
||||
const cardDb = require('./cards.js');
|
||||
const cardDb = require('./cards');
|
||||
|
||||
gameState.players.forEach((player, playerIndex) => {
|
||||
if (!player.board || player.board.length === 0) return;
|
||||
@ -1361,7 +1364,7 @@ function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardInde
|
||||
if (p.health <= 0 || p.isDead) return; // Мёртвый игрок не может играть
|
||||
const cid = p.hand[handIndex];
|
||||
if (!cid) return;
|
||||
const card = cardDb[cid];
|
||||
const card = getCard(room, cid);
|
||||
if (!card || card.type !== 'spell') return;
|
||||
const cost = card.cost || 0;
|
||||
if (p.mana < cost) return;
|
||||
@ -1406,8 +1409,8 @@ function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardInde
|
||||
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);
|
||||
const dc = getCard(room, m.cardId);
|
||||
if (dc && dc.deathrattleId) runDeathrattle(room, dc, targetPlayerIndex);
|
||||
}
|
||||
}
|
||||
} else if (eff === 'draw_2') {
|
||||
@ -1428,8 +1431,8 @@ function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardInde
|
||||
pl.board.forEach((m) => {
|
||||
m.health -= 1;
|
||||
if (m.health <= 0) {
|
||||
const card = cardDb[m.cardId];
|
||||
if (card && card.deathrattleId) runDeathrattle(room, card, i);
|
||||
const dc = getCard(room, m.cardId);
|
||||
if (dc && dc.deathrattleId) runDeathrattle(room, dc, i);
|
||||
}
|
||||
});
|
||||
pl.board = pl.board.filter((m) => m.health > 0);
|
||||
@ -1462,8 +1465,8 @@ function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardInde
|
||||
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);
|
||||
const dc = getCard(room, m.cardId);
|
||||
if (dc && dc.deathrattleId) runDeathrattle(room, dc, targetPlayerIndex);
|
||||
}
|
||||
}
|
||||
} else if (eff === 'heal_4') {
|
||||
@ -1487,8 +1490,8 @@ function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardInde
|
||||
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);
|
||||
const dc = getCard(room, m.cardId);
|
||||
if (dc && dc.deathrattleId) runDeathrattle(room, dc, targetPlayerIndex);
|
||||
}
|
||||
} else if (eff === 'freeze_damage') {
|
||||
const t = gameState.players[targetPlayerIndex];
|
||||
@ -1499,8 +1502,8 @@ function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardInde
|
||||
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);
|
||||
const dc = getCard(room, m.cardId);
|
||||
if (dc && dc.deathrattleId) runDeathrattle(room, dc, targetPlayerIndex);
|
||||
}
|
||||
} else if (eff === 'buff_all_friendly') {
|
||||
p.board.forEach((m) => {
|
||||
@ -1515,7 +1518,7 @@ function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardInde
|
||||
const weak = enemy.board.filter(m => m.attack <= 2);
|
||||
weak.forEach((target) => {
|
||||
const boardIdx = enemy.board.indexOf(target);
|
||||
const targetCard = cardDb[target.cardId];
|
||||
const targetCard = getCard(room, target.cardId);
|
||||
enemy.board.splice(boardIdx, 1);
|
||||
if (targetCard && targetCard.deathrattleId) {
|
||||
runDeathrattle(room, targetCard, gameState.players.indexOf(enemy));
|
||||
@ -1551,7 +1554,7 @@ function stealCardsFromDeck(room, socketId, handIndex, targetPlayerIndex, cardIn
|
||||
|
||||
const cid = p.hand[handIndex];
|
||||
if (!cid) return;
|
||||
const card = cardDb[cid];
|
||||
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;
|
||||
@ -1610,6 +1613,71 @@ function stealCardsFromDeck(room, socketId, handIndex, targetPlayerIndex, cardIn
|
||||
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);
|
||||
@ -1634,7 +1702,7 @@ function heroAbility(room, socketId, targetPlayerIndex, targetBoardIndex) {
|
||||
if (targetBoardIndex !== -1) {
|
||||
const m = targetPlayer.board[targetBoardIndex];
|
||||
if (m && m.health <= 0) {
|
||||
const card = cardDb[m.cardId];
|
||||
const card = getCard(room, m.cardId);
|
||||
if (card && card.deathrattleId) runDeathrattle(room, card, targetPlayerIndex);
|
||||
}
|
||||
}
|
||||
@ -1708,8 +1776,8 @@ function attack(room, socketId, attackerBoardIndex, targetPlayerIndex, targetBoa
|
||||
damage: attacker.attack,
|
||||
reverseDamage: target.attack,
|
||||
});
|
||||
const targetCard = cardDb[target.cardId];
|
||||
const attackerCard = cardDb[attacker.cardId];
|
||||
const targetCard = getCard(room, target.cardId);
|
||||
const attackerCard = getCard(room, attacker.cardId);
|
||||
if (targetDied && targetCard && targetCard.deathrattleId) {
|
||||
runDeathrattle(room, targetCard, targetPlayerIndex);
|
||||
}
|
||||
@ -1719,7 +1787,7 @@ function attack(room, socketId, attackerBoardIndex, targetPlayerIndex, targetBoa
|
||||
}
|
||||
|
||||
// Механика двойной атаки
|
||||
const attackerCard = cardDb[attacker.cardId];
|
||||
const attackerCard = getCard(room, attacker.cardId);
|
||||
if (attackerCard && attackerCard.canAttackTwice && !attacker.attacksUsed) {
|
||||
attacker.attacksUsed = 1; // Использована одна атака
|
||||
} else {
|
||||
@ -1868,6 +1936,37 @@ io.on('connection', (socket) => {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user