diff --git a/public/game.js b/public/game.js
index 87b2a1a..7d35e48 100644
--- a/public/game.js
+++ b/public/game.js
@@ -10,6 +10,8 @@
let gameState = null;
let cardDb = {};
let yourIndex = -1;
+ let forgePreviewRequestId = 0;
+ let forgePreviewPendingIds = null;
let attackMode = { active: false, attackerPlayer: -1, attackerBoard: -1 };
let spellMode = { active: false, handIndex: -1, cardId: '', spellTarget: '' };
let heroAbilityMode = { active: false };
@@ -179,6 +181,39 @@
if (data.warn && typeof window.Sounds !== 'undefined') window.Sounds.timerWarning();
if (data.ended && typeof window.Sounds !== 'undefined') window.Sounds.timerEnd();
});
+ socket.on('forgePreviewResult', (data) => {
+ if (!data) return;
+ if (data.requestId != null && data.requestId !== forgePreviewRequestId) return;
+ const preview = $('forge-preview');
+ const wrap = $('forge-preview-card-wrap');
+ const confirmBtn = $('btn-forge-confirm');
+ if (!preview) return;
+ if (data.error || !data.resultCardId) {
+ preview.classList.add('hidden');
+ if (wrap) wrap.innerHTML = '';
+ if (confirmBtn) confirmBtn.disabled = true;
+ forgePreviewPendingIds = null;
+ return;
+ }
+ const meta = (gameState?.cardDb || cardDb)[data.resultCardId];
+ if (!meta) {
+ preview.classList.add('hidden');
+ forgePreviewPendingIds = null;
+ return;
+ }
+ const you = gameState?.players?.[gameState?.yourIndex];
+ const canCraft = you && (you.mana || 0) >= 2;
+ if (wrap) {
+ const art = typeof getCardArt === 'function' ? getCardArt(meta) : '';
+ wrap.innerHTML = '
';
+ }
+ if (confirmBtn) confirmBtn.disabled = !canCraft;
+ preview.classList.remove('hidden');
+ });
+
socket.on('chatMessage', (data) => {
const chatMessagesEl = $('chat-messages');
if (chatMessagesEl && data) {
@@ -399,7 +434,7 @@
document.body.appendChild(damageEl);
setTimeout(() => {
damageEl.style.animation = 'damageFloat 1.5s ease-out forwards';
- setTimeout(() => damageEl.remove(), 1000); // Сократил с 1500 до 1000 мс
+ setTimeout(() => damageEl.remove(), 1000);
}, 10);
}
@@ -1658,9 +1693,8 @@
sidebar.classList.remove('hidden');
forgeSelected = []; // Сбрасываем выбор при открытии
renderForgeHand(state);
- renderForgeDeck(state);
renderForgeSelected();
- updateForgeCraftButton();
+ updateForgePreview();
}
};
} else if (forgeBtn) {
@@ -1841,9 +1875,8 @@
// Обновляем кузницу, если она открыта
if (isForgeOpen) {
renderForgeHand(state);
- renderForgeDeck(state);
renderForgeSelected();
- updateForgeCraftButton();
+ updateForgePreview();
}
}
@@ -2162,18 +2195,20 @@
if (sidebar) {
sidebar.classList.add('hidden');
forgeSelected = [];
+ forgePreviewPendingIds = null;
+ const preview = $('forge-preview');
+ if (preview) preview.classList.add('hidden');
renderForgeSelected();
+ updateForgePreview();
}
});
$('btn-forge-clear')?.addEventListener('click', () => {
forgeSelected = [];
+ forgePreviewPendingIds = null;
renderForgeSelected();
- updateForgeCraftButton();
- if (gameState) {
- renderForgeHand(gameState);
- renderForgeDeck(gameState);
- }
+ updateForgePreview();
+ if (gameState) renderForgeHand(gameState);
});
$('btn-steal-close')?.addEventListener('click', () => {
@@ -2338,7 +2373,7 @@
}
if (forgeSelected.length === 0) {
- selectedEl.innerHTML = 'Кликните на карты чтобы выбрать
';
+ selectedEl.innerHTML = 'Кликните на карты из руки
';
} else {
selectedEl.innerHTML = forgeSelected.map((cardId, idx) => {
const meta = cardDb[cardId];
@@ -2365,19 +2400,30 @@
}
}
- function updateForgeCraftButton() {
- const craftBtn = $('btn-forge-craft');
- const you = gameState?.players?.[gameState?.yourIndex];
- if (craftBtn && you) {
- const canCraft = forgeSelected.length >= 2 && forgeSelected.length <= 3 && (you.mana || 0) >= 2;
- craftBtn.disabled = !canCraft;
+ function updateForgePreview() {
+ const preview = $('forge-preview');
+ const wrap = $('forge-preview-card-wrap');
+ const confirmBtn = $('btn-forge-confirm');
+ if (!preview) return;
+ if (forgeSelected.length < 2 || forgeSelected.length > 3) {
+ preview.classList.add('hidden');
+ if (wrap) wrap.innerHTML = '';
+ if (confirmBtn) confirmBtn.disabled = true;
+ forgePreviewPendingIds = null;
+ return;
}
+ forgePreviewRequestId = Date.now();
+ forgePreviewPendingIds = [...forgeSelected];
+ if (wrap) wrap.innerHTML = 'Загрузка…
';
+ if (confirmBtn) confirmBtn.disabled = true;
+ preview.classList.remove('hidden');
+ socket.emit('forgePreview', { cardIds: forgePreviewPendingIds, requestId: forgePreviewRequestId });
}
-
+
function addCardToForge(cardId, source) {
if (!cardId) return;
+ if (source !== 'hand') return; // только из руки
if (forgeSelected.includes(cardId)) {
- // Если карта уже выбрана, убираем её
removeCardFromForge(cardId);
return;
}
@@ -2396,26 +2442,17 @@
forgeSelected.push(cardId);
renderForgeSelected();
- updateForgeCraftButton();
-
- // Обновляем отображение в источниках
- if (gameState) {
- renderForgeHand(gameState);
- renderForgeDeck(gameState);
- }
+ updateForgePreview();
+ if (gameState) renderForgeHand(gameState);
}
-
+
function removeCardFromForge(cardId) {
const idx = forgeSelected.indexOf(cardId);
if (idx >= 0) {
forgeSelected.splice(idx, 1);
renderForgeSelected();
- updateForgeCraftButton();
- // Обновляем отображение в источниках
- if (gameState) {
- renderForgeHand(gameState);
- renderForgeDeck(gameState);
- }
+ updateForgePreview();
+ if (gameState) renderForgeHand(gameState);
}
}
@@ -2472,76 +2509,35 @@
});
}
- function renderForgeDeck(state) {
- const deck = state.yourDeck || [];
- const deckList = $('forge-deck-list');
- if (!deckList) return;
-
- if (!deck || deck.length === 0) {
- deckList.innerHTML = 'Колода пуста
';
- return;
- }
-
- // Фильтруем только миньонов
- const minionCards = deck.filter(cardId => {
- const meta = cardDb[cardId];
- return meta && meta.type === 'minion';
- });
-
- if (minionCards.length === 0) {
- deckList.innerHTML = 'Нет миньонов в колоде
';
- return;
- }
-
- deckList.innerHTML = minionCards.map((cardId) => {
- const meta = cardDb[cardId];
- if (!meta) return '';
- const isSelected = forgeSelected.includes(cardId);
- return ``;
- }).filter(Boolean).join('');
-
- // Обработчики клика для карт из колоды
- $all('#forge-deck-list .card-wrap').forEach(card => {
- card.onclick = function(e) {
- e.stopPropagation();
- const cardId = card.dataset.cardId;
- if (cardId) {
- addCardToForge(cardId, 'deck');
- }
- };
- });
- }
-
- // Обновляем drag-and-drop при каждом обновлении руки, если кузница открыта
- // Это делается в bindGameEvents
-
- // Обработчик создания улучшенной карты
- const craftBtn = $('btn-forge-craft');
- if (craftBtn) {
- craftBtn.onclick = function() {
- if (craftBtn.disabled) return;
+ const confirmBtn = $('btn-forge-confirm');
+ if (confirmBtn) {
+ confirmBtn.onclick = function() {
+ if (confirmBtn.disabled || !forgePreviewPendingIds || forgePreviewPendingIds.length < 2) return;
const you = gameState?.players?.[gameState?.yourIndex];
- if (!you || forgeSelected.length < 2 || forgeSelected.length > 3 || (you.mana || 0) < 2) return;
-
- socket.emit('forgeCard', { cardIds: forgeSelected });
+ if (!you || (you.mana || 0) < 2) return;
+ socket.emit('forgeCard', { cardIds: forgePreviewPendingIds });
+ if (typeof window.Sounds !== 'undefined' && window.Sounds.forge) window.Sounds.forge();
forgeSelected = [];
+ forgePreviewPendingIds = null;
+ const preview = $('forge-preview');
+ if (preview) preview.classList.add('hidden');
renderForgeSelected();
- updateForgeCraftButton();
+ updateForgePreview();
+ if (gameState) renderForgeHand(gameState);
const sidebar = $('forge-sidebar');
- if (sidebar) {
- sidebar.classList.add('hidden');
- }
+ if (sidebar) sidebar.classList.add('hidden');
+ };
+ }
+ const cancelPreviewBtn = $('btn-forge-cancel-preview');
+ if (cancelPreviewBtn) {
+ cancelPreviewBtn.onclick = function() {
+ const preview = $('forge-preview');
+ if (preview) preview.classList.add('hidden');
+ forgePreviewPendingIds = null;
+ const wrap = $('forge-preview-card-wrap');
+ if (wrap) wrap.innerHTML = '';
+ const cb = $('btn-forge-confirm');
+ if (cb) cb.disabled = true;
};
}
diff --git a/public/index.html b/public/index.html
index 660f480..8f0d1ed 100644
--- a/public/index.html
+++ b/public/index.html
@@ -271,10 +271,11 @@
-
+
📋 Из руки
+
Только миньоны из руки. Выберите 2–3 карты.
@@ -282,21 +283,22 @@
✨ Выбрано (0/3)
-
Кликните на карты чтобы выбрать
+
Кликните на карты из руки
-
-
-
+
+
При слиянии вы получите:
+
+
+
+
+
+
-
diff --git a/public/sounds.js b/public/sounds.js
index 1af0758..d96bcad 100644
--- a/public/sounds.js
+++ b/public/sounds.js
@@ -115,6 +115,15 @@
});
}
+ function forge() {
+ const c = init();
+ if (!c) return;
+ const notes = [392, 523, 659, 784];
+ notes.forEach((f, i) => {
+ setTimeout(() => beep(f, 0.2, 'sine', 0.12), i * 90);
+ });
+ }
+
global.Sounds = {
init,
playCard,
@@ -126,5 +135,6 @@
hoverCard,
timerWarning,
timerEnd,
+ forge,
};
})(typeof window !== 'undefined' ? window : globalThis);
diff --git a/public/styles.css b/public/styles.css
index 5299bfe..5746a22 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -1648,6 +1648,17 @@ html, body {
overflow: hidden;
}
+.forge-columns.forge-two-cols {
+ grid-template-columns: 1fr 1.2fr;
+}
+
+.forge-hint-inline {
+ font-size: 0.85rem;
+ color: #94a3b8;
+ margin: 0 0 0.5rem 0;
+ padding: 0 1rem;
+}
+
.forge-column {
display: flex;
flex-direction: column;
@@ -1789,6 +1800,56 @@ html, body {
opacity: 0.5;
cursor: not-allowed;
}
+
+/* Forge preview (предпросмотр результата крафта) */
+.forge-preview {
+ padding: 1rem;
+ border-top: 1px solid rgba(212,168,75,0.2);
+ background: rgba(0,0,0,0.25);
+ flex-shrink: 0;
+}
+
+.forge-preview.hidden {
+ display: none !important;
+}
+
+.forge-preview-title {
+ margin: 0 0 0.75rem 0;
+ font-size: 0.95rem;
+ font-weight: 600;
+ color: var(--gold);
+}
+
+.forge-preview-card-wrap {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 120px;
+ margin-bottom: 0.75rem;
+}
+
+.forge-preview-loading {
+ margin: 0;
+ color: #94a3b8;
+ font-size: 0.9rem;
+}
+
+.forge-preview-card {
+ flex-shrink: 0;
+}
+
+.forge-preview-card .card {
+ border-radius: 8px;
+ border: 2px solid rgba(212,168,75,0.5);
+ box-shadow: 0 0 20px rgba(212,168,75,0.3);
+}
+
+.forge-preview-actions {
+ display: flex;
+ gap: 0.5rem;
+ justify-content: center;
+ flex-wrap: wrap;
+}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-10px); }
diff --git a/server.js b/server.js
index 064035c..5688284 100644
--- a/server.js
+++ b/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;