123
This commit is contained in:
190
public/game.js
190
public/game.js
@ -10,6 +10,8 @@
|
|||||||
let gameState = null;
|
let gameState = null;
|
||||||
let cardDb = {};
|
let cardDb = {};
|
||||||
let yourIndex = -1;
|
let yourIndex = -1;
|
||||||
|
let forgePreviewRequestId = 0;
|
||||||
|
let forgePreviewPendingIds = null;
|
||||||
let attackMode = { active: false, attackerPlayer: -1, attackerBoard: -1 };
|
let attackMode = { active: false, attackerPlayer: -1, attackerBoard: -1 };
|
||||||
let spellMode = { active: false, handIndex: -1, cardId: '', spellTarget: '' };
|
let spellMode = { active: false, handIndex: -1, cardId: '', spellTarget: '' };
|
||||||
let heroAbilityMode = { active: false };
|
let heroAbilityMode = { active: false };
|
||||||
@ -179,6 +181,39 @@
|
|||||||
if (data.warn && typeof window.Sounds !== 'undefined') window.Sounds.timerWarning();
|
if (data.warn && typeof window.Sounds !== 'undefined') window.Sounds.timerWarning();
|
||||||
if (data.ended && typeof window.Sounds !== 'undefined') window.Sounds.timerEnd();
|
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 = '<div class="card-wrap forge-preview-card" style="width:110px;height:154px;"><div class="card faction-' + (meta.faction || 'neutral') + '">' +
|
||||||
|
'<div class="card-art">' + art + '</div>' +
|
||||||
|
'<div class="card-info"><div class="card-name" style="font-size:0.75rem">' + escapeHtml(meta.name || '') + '</div>' +
|
||||||
|
'<div class="card-stats"><span class="atk">' + (meta.attack || 0) + '</span><span class="hp">' + (meta.health || 0) + '</span></div></div></div></div>';
|
||||||
|
}
|
||||||
|
if (confirmBtn) confirmBtn.disabled = !canCraft;
|
||||||
|
preview.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
socket.on('chatMessage', (data) => {
|
socket.on('chatMessage', (data) => {
|
||||||
const chatMessagesEl = $('chat-messages');
|
const chatMessagesEl = $('chat-messages');
|
||||||
if (chatMessagesEl && data) {
|
if (chatMessagesEl && data) {
|
||||||
@ -399,7 +434,7 @@
|
|||||||
document.body.appendChild(damageEl);
|
document.body.appendChild(damageEl);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
damageEl.style.animation = 'damageFloat 1.5s ease-out forwards';
|
damageEl.style.animation = 'damageFloat 1.5s ease-out forwards';
|
||||||
setTimeout(() => damageEl.remove(), 1000); // Сократил с 1500 до 1000 мс
|
setTimeout(() => damageEl.remove(), 1000);
|
||||||
}, 10);
|
}, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1658,9 +1693,8 @@
|
|||||||
sidebar.classList.remove('hidden');
|
sidebar.classList.remove('hidden');
|
||||||
forgeSelected = []; // Сбрасываем выбор при открытии
|
forgeSelected = []; // Сбрасываем выбор при открытии
|
||||||
renderForgeHand(state);
|
renderForgeHand(state);
|
||||||
renderForgeDeck(state);
|
|
||||||
renderForgeSelected();
|
renderForgeSelected();
|
||||||
updateForgeCraftButton();
|
updateForgePreview();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else if (forgeBtn) {
|
} else if (forgeBtn) {
|
||||||
@ -1841,9 +1875,8 @@
|
|||||||
// Обновляем кузницу, если она открыта
|
// Обновляем кузницу, если она открыта
|
||||||
if (isForgeOpen) {
|
if (isForgeOpen) {
|
||||||
renderForgeHand(state);
|
renderForgeHand(state);
|
||||||
renderForgeDeck(state);
|
|
||||||
renderForgeSelected();
|
renderForgeSelected();
|
||||||
updateForgeCraftButton();
|
updateForgePreview();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2162,18 +2195,20 @@
|
|||||||
if (sidebar) {
|
if (sidebar) {
|
||||||
sidebar.classList.add('hidden');
|
sidebar.classList.add('hidden');
|
||||||
forgeSelected = [];
|
forgeSelected = [];
|
||||||
|
forgePreviewPendingIds = null;
|
||||||
|
const preview = $('forge-preview');
|
||||||
|
if (preview) preview.classList.add('hidden');
|
||||||
renderForgeSelected();
|
renderForgeSelected();
|
||||||
|
updateForgePreview();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$('btn-forge-clear')?.addEventListener('click', () => {
|
$('btn-forge-clear')?.addEventListener('click', () => {
|
||||||
forgeSelected = [];
|
forgeSelected = [];
|
||||||
|
forgePreviewPendingIds = null;
|
||||||
renderForgeSelected();
|
renderForgeSelected();
|
||||||
updateForgeCraftButton();
|
updateForgePreview();
|
||||||
if (gameState) {
|
if (gameState) renderForgeHand(gameState);
|
||||||
renderForgeHand(gameState);
|
|
||||||
renderForgeDeck(gameState);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$('btn-steal-close')?.addEventListener('click', () => {
|
$('btn-steal-close')?.addEventListener('click', () => {
|
||||||
@ -2338,7 +2373,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (forgeSelected.length === 0) {
|
if (forgeSelected.length === 0) {
|
||||||
selectedEl.innerHTML = '<div class="forge-selected-empty">Кликните на карты чтобы выбрать</div>';
|
selectedEl.innerHTML = '<div class="forge-selected-empty">Кликните на карты из руки</div>';
|
||||||
} else {
|
} else {
|
||||||
selectedEl.innerHTML = forgeSelected.map((cardId, idx) => {
|
selectedEl.innerHTML = forgeSelected.map((cardId, idx) => {
|
||||||
const meta = cardDb[cardId];
|
const meta = cardDb[cardId];
|
||||||
@ -2365,19 +2400,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateForgeCraftButton() {
|
function updateForgePreview() {
|
||||||
const craftBtn = $('btn-forge-craft');
|
const preview = $('forge-preview');
|
||||||
const you = gameState?.players?.[gameState?.yourIndex];
|
const wrap = $('forge-preview-card-wrap');
|
||||||
if (craftBtn && you) {
|
const confirmBtn = $('btn-forge-confirm');
|
||||||
const canCraft = forgeSelected.length >= 2 && forgeSelected.length <= 3 && (you.mana || 0) >= 2;
|
if (!preview) return;
|
||||||
craftBtn.disabled = !canCraft;
|
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 = '<p class="forge-preview-loading">Загрузка…</p>';
|
||||||
|
if (confirmBtn) confirmBtn.disabled = true;
|
||||||
|
preview.classList.remove('hidden');
|
||||||
|
socket.emit('forgePreview', { cardIds: forgePreviewPendingIds, requestId: forgePreviewRequestId });
|
||||||
}
|
}
|
||||||
|
|
||||||
function addCardToForge(cardId, source) {
|
function addCardToForge(cardId, source) {
|
||||||
if (!cardId) return;
|
if (!cardId) return;
|
||||||
|
if (source !== 'hand') return; // только из руки
|
||||||
if (forgeSelected.includes(cardId)) {
|
if (forgeSelected.includes(cardId)) {
|
||||||
// Если карта уже выбрана, убираем её
|
|
||||||
removeCardFromForge(cardId);
|
removeCardFromForge(cardId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -2396,13 +2442,8 @@
|
|||||||
|
|
||||||
forgeSelected.push(cardId);
|
forgeSelected.push(cardId);
|
||||||
renderForgeSelected();
|
renderForgeSelected();
|
||||||
updateForgeCraftButton();
|
updateForgePreview();
|
||||||
|
if (gameState) renderForgeHand(gameState);
|
||||||
// Обновляем отображение в источниках
|
|
||||||
if (gameState) {
|
|
||||||
renderForgeHand(gameState);
|
|
||||||
renderForgeDeck(gameState);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeCardFromForge(cardId) {
|
function removeCardFromForge(cardId) {
|
||||||
@ -2410,12 +2451,8 @@
|
|||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
forgeSelected.splice(idx, 1);
|
forgeSelected.splice(idx, 1);
|
||||||
renderForgeSelected();
|
renderForgeSelected();
|
||||||
updateForgeCraftButton();
|
updateForgePreview();
|
||||||
// Обновляем отображение в источниках
|
if (gameState) renderForgeHand(gameState);
|
||||||
if (gameState) {
|
|
||||||
renderForgeHand(gameState);
|
|
||||||
renderForgeDeck(gameState);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2472,76 +2509,35 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderForgeDeck(state) {
|
const confirmBtn = $('btn-forge-confirm');
|
||||||
const deck = state.yourDeck || [];
|
if (confirmBtn) {
|
||||||
const deckList = $('forge-deck-list');
|
confirmBtn.onclick = function() {
|
||||||
if (!deckList) return;
|
if (confirmBtn.disabled || !forgePreviewPendingIds || forgePreviewPendingIds.length < 2) return;
|
||||||
|
|
||||||
if (!deck || deck.length === 0) {
|
|
||||||
deckList.innerHTML = '<div style="color: #94a3b8; text-align: center; padding: 2rem; font-size: 0.9rem;">Колода пуста</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Фильтруем только миньонов
|
|
||||||
const minionCards = deck.filter(cardId => {
|
|
||||||
const meta = cardDb[cardId];
|
|
||||||
return meta && meta.type === 'minion';
|
|
||||||
});
|
|
||||||
|
|
||||||
if (minionCards.length === 0) {
|
|
||||||
deckList.innerHTML = '<div style="color: #94a3b8; text-align: center; padding: 2rem; font-size: 0.9rem;">Нет миньонов в колоде</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
deckList.innerHTML = minionCards.map((cardId) => {
|
|
||||||
const meta = cardDb[cardId];
|
|
||||||
if (!meta) return '';
|
|
||||||
const isSelected = forgeSelected.includes(cardId);
|
|
||||||
return `<div class="card-wrap ${isSelected ? 'selected' : ''}" data-card-id="${cardId}" style="width: 100px; height: 140px; cursor: pointer;">
|
|
||||||
<div class="card faction-${meta.faction || 'neutral'}">
|
|
||||||
<div class="card-art">${getCardArt(meta)}</div>
|
|
||||||
<div class="card-info">
|
|
||||||
<div class="card-name" style="font-size: 0.7rem;">${escapeHtml(meta.name)}</div>
|
|
||||||
<div class="card-stats">
|
|
||||||
<span class="atk">${meta.attack || 0}</span>
|
|
||||||
<span class="hp">${meta.health || 0}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
}).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 you = gameState?.players?.[gameState?.yourIndex];
|
const you = gameState?.players?.[gameState?.yourIndex];
|
||||||
if (!you || forgeSelected.length < 2 || forgeSelected.length > 3 || (you.mana || 0) < 2) return;
|
if (!you || (you.mana || 0) < 2) return;
|
||||||
|
socket.emit('forgeCard', { cardIds: forgePreviewPendingIds });
|
||||||
socket.emit('forgeCard', { cardIds: forgeSelected });
|
if (typeof window.Sounds !== 'undefined' && window.Sounds.forge) window.Sounds.forge();
|
||||||
forgeSelected = [];
|
forgeSelected = [];
|
||||||
|
forgePreviewPendingIds = null;
|
||||||
|
const preview = $('forge-preview');
|
||||||
|
if (preview) preview.classList.add('hidden');
|
||||||
renderForgeSelected();
|
renderForgeSelected();
|
||||||
updateForgeCraftButton();
|
updateForgePreview();
|
||||||
|
if (gameState) renderForgeHand(gameState);
|
||||||
const sidebar = $('forge-sidebar');
|
const sidebar = $('forge-sidebar');
|
||||||
if (sidebar) {
|
if (sidebar) sidebar.classList.add('hidden');
|
||||||
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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -271,10 +271,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="forge-main-content">
|
<div class="forge-main-content">
|
||||||
<div class="forge-columns">
|
<div class="forge-columns forge-two-cols">
|
||||||
<!-- Карты из руки -->
|
<!-- Карты из руки -->
|
||||||
<div class="forge-column">
|
<div class="forge-column">
|
||||||
<h3 class="forge-column-title">📋 Из руки</h3>
|
<h3 class="forge-column-title">📋 Из руки</h3>
|
||||||
|
<p class="forge-hint-inline">Только миньоны из руки. Выберите 2–3 карты.</p>
|
||||||
<div id="forge-hand-list" class="forge-cards-list"></div>
|
<div id="forge-hand-list" class="forge-cards-list"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -282,21 +283,22 @@
|
|||||||
<div class="forge-column forge-selected-column">
|
<div class="forge-column forge-selected-column">
|
||||||
<h3 class="forge-column-title">✨ Выбрано (<span id="forge-selected-count">0</span>/3)</h3>
|
<h3 class="forge-column-title">✨ Выбрано (<span id="forge-selected-count">0</span>/3)</h3>
|
||||||
<div id="forge-selected" class="forge-selected">
|
<div id="forge-selected" class="forge-selected">
|
||||||
<div class="forge-selected-empty">Кликните на карты чтобы выбрать</div>
|
<div class="forge-selected-empty">Кликните на карты из руки</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Карты из колоды -->
|
|
||||||
<div class="forge-column">
|
|
||||||
<h3 class="forge-column-title">📚 Из колоды</h3>
|
|
||||||
<div id="forge-deck-list" class="forge-cards-list"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="forge-preview" class="forge-preview hidden">
|
||||||
|
<p class="forge-preview-title">При слиянии вы получите:</p>
|
||||||
|
<div id="forge-preview-card-wrap" class="forge-preview-card-wrap"></div>
|
||||||
|
<div class="forge-preview-actions">
|
||||||
|
<button type="button" id="btn-forge-confirm" class="btn btn-primary" disabled>⚒ Крафтить (2 маны)</button>
|
||||||
|
<button type="button" id="btn-forge-cancel-preview" class="btn btn-ghost">Отмена</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="forge-actions">
|
<div class="forge-actions">
|
||||||
<button type="button" id="btn-forge-clear" class="btn btn-ghost">Очистить</button>
|
<button type="button" id="btn-forge-clear" class="btn btn-ghost">Очистить</button>
|
||||||
<button type="button" id="btn-forge-craft" class="btn btn-primary" disabled>⚒ Создать улучшенную карту (2 маны)</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="card-info-overlay" class="modal-overlay hidden">
|
<div id="card-info-overlay" class="modal-overlay hidden">
|
||||||
|
|||||||
@ -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 = {
|
global.Sounds = {
|
||||||
init,
|
init,
|
||||||
playCard,
|
playCard,
|
||||||
@ -126,5 +135,6 @@
|
|||||||
hoverCard,
|
hoverCard,
|
||||||
timerWarning,
|
timerWarning,
|
||||||
timerEnd,
|
timerEnd,
|
||||||
|
forge,
|
||||||
};
|
};
|
||||||
})(typeof window !== 'undefined' ? window : globalThis);
|
})(typeof window !== 'undefined' ? window : globalThis);
|
||||||
|
|||||||
@ -1648,6 +1648,17 @@ html, body {
|
|||||||
overflow: hidden;
|
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 {
|
.forge-column {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -1789,6 +1800,56 @@ html, body {
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
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 {
|
@keyframes shake {
|
||||||
0%, 100% { transform: translateX(0); }
|
0%, 100% { transform: translateX(0); }
|
||||||
25% { transform: translateX(-10px); }
|
25% { transform: translateX(-10px); }
|
||||||
|
|||||||
171
server.js
171
server.js
@ -23,6 +23,10 @@ const MIN_PLAYERS = 2;
|
|||||||
|
|
||||||
const cardDb = require('./cards');
|
const cardDb = require('./cards');
|
||||||
|
|
||||||
|
function getCard(room, cardId) {
|
||||||
|
return cardDb[cardId];
|
||||||
|
}
|
||||||
|
|
||||||
const rooms = new Map(); // code -> { lobby, gameState, gameStarted, faction, turnTimerInterval, turnTimeLeft, turnTimerWarnFired }
|
const rooms = new Map(); // code -> { lobby, gameState, gameStarted, faction, turnTimerInterval, turnTimeLeft, turnTimerWarnFired }
|
||||||
const TURN_SECONDS = 90;
|
const TURN_SECONDS = 90;
|
||||||
const TURN_WARN_AT = 15;
|
const TURN_WARN_AT = 15;
|
||||||
@ -184,7 +188,7 @@ function makeAITurn(room) {
|
|||||||
// 1. Играем карты (приоритет дешёвым и эффективным)
|
// 1. Играем карты (приоритет дешёвым и эффективным)
|
||||||
const playableCards = aiPlayer.hand
|
const playableCards = aiPlayer.hand
|
||||||
.map((cardId, index) => {
|
.map((cardId, index) => {
|
||||||
const card = cardDb[cardId];
|
const card = getCard(room, cardId);
|
||||||
if (!card) return null;
|
if (!card) return null;
|
||||||
const cost = card.cost || 0;
|
const cost = card.cost || 0;
|
||||||
if (cost > aiPlayer.mana) return null;
|
if (cost > aiPlayer.mana) return null;
|
||||||
@ -385,16 +389,15 @@ function broadcastGameState(room) {
|
|||||||
room.gameState.players.forEach((p, i) => {
|
room.gameState.players.forEach((p, i) => {
|
||||||
const socket = io.sockets.sockets.get(p.id);
|
const socket = io.sockets.sockets.get(p.id);
|
||||||
if (socket) {
|
if (socket) {
|
||||||
const mergedCardDb = { ...cardDb, ...(room.gameState.forgedCards || {}) };
|
|
||||||
const yourView = {
|
const yourView = {
|
||||||
...room.gameState,
|
...room.gameState,
|
||||||
yourIndex: i,
|
yourIndex: i,
|
||||||
yourHand: [...p.hand],
|
yourHand: [...p.hand],
|
||||||
yourDeck: [...p.deck], // Передаем колоду для кузницы
|
yourDeck: [...p.deck],
|
||||||
yourDeckCount: p.deck.length,
|
yourDeckCount: p.deck.length,
|
||||||
yourManualDrawUsed: !!p.manualDrawUsed,
|
yourManualDrawUsed: !!p.manualDrawUsed,
|
||||||
yourHeroAbilityUsed: !!p.heroAbilityUsed,
|
yourHeroAbilityUsed: !!p.heroAbilityUsed,
|
||||||
cardDb: mergedCardDb,
|
cardDb: cardDb,
|
||||||
};
|
};
|
||||||
socket.emit('gameState', yourView);
|
socket.emit('gameState', yourView);
|
||||||
}
|
}
|
||||||
@ -430,7 +433,7 @@ function endTurn(room) {
|
|||||||
const prevPlayer = gameState.players[prev];
|
const prevPlayer = gameState.players[prev];
|
||||||
if (prevPlayer) {
|
if (prevPlayer) {
|
||||||
prevPlayer.board.forEach((m) => {
|
prevPlayer.board.forEach((m) => {
|
||||||
const card = cardDb[m.cardId];
|
const card = getCard(room, m.cardId);
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
|
|
||||||
if (card.name === 'Храм джедаев') {
|
if (card.name === 'Храм джедаев') {
|
||||||
@ -461,7 +464,7 @@ function endTurn(room) {
|
|||||||
if (weak.length > 0) {
|
if (weak.length > 0) {
|
||||||
const target = weak[Math.floor(Math.random() * weak.length)];
|
const target = weak[Math.floor(Math.random() * weak.length)];
|
||||||
const boardIdx = enemy.board.indexOf(target);
|
const boardIdx = enemy.board.indexOf(target);
|
||||||
const targetCard = cardDb[target.cardId];
|
const targetCard = getCard(room, target.cardId);
|
||||||
enemy.board.splice(boardIdx, 1);
|
enemy.board.splice(boardIdx, 1);
|
||||||
gameState.log.push({ type: 'structure', effect: 'sarlacc_consume', fromPlayer: prev, toPlayer: gameState.players.indexOf(enemy), toIdx: boardIdx });
|
gameState.log.push({ type: 'structure', effect: 'sarlacc_consume', fromPlayer: prev, toPlayer: gameState.players.indexOf(enemy), toIdx: boardIdx });
|
||||||
if (targetCard && targetCard.deathrattleId) {
|
if (targetCard && targetCard.deathrattleId) {
|
||||||
@ -545,7 +548,7 @@ function endTurn(room) {
|
|||||||
|
|
||||||
np.board.forEach((m) => {
|
np.board.forEach((m) => {
|
||||||
if (m.frozen) { m.frozen = false; return; }
|
if (m.frozen) { m.frozen = false; return; }
|
||||||
const card = cardDb[m.cardId];
|
const card = getCard(room, m.cardId);
|
||||||
if (card && card.attack > 0) {
|
if (card && card.attack > 0) {
|
||||||
m.canAttack = true;
|
m.canAttack = true;
|
||||||
if (card.canAttackTwice) {
|
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 });
|
gameState.log.push({ type: 'battlecry', effect: 'deal_2_minion', fromPlayer: playerIndex, toPlayer: enemyIdx, toIdx: boardIdx, damage: 2 });
|
||||||
if (target.health <= 0) {
|
if (target.health <= 0) {
|
||||||
enemy.board.splice(boardIdx, 1);
|
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) {
|
} else if (id === 'destroy_weak_minion' && enemies.length) {
|
||||||
@ -650,7 +653,7 @@ function runBattlecry(room, card, playerIndex) {
|
|||||||
if (weak.length > 0) {
|
if (weak.length > 0) {
|
||||||
const target = weak[Math.floor(Math.random() * weak.length)];
|
const target = weak[Math.floor(Math.random() * weak.length)];
|
||||||
const boardIdx = enemy.board.indexOf(target);
|
const boardIdx = enemy.board.indexOf(target);
|
||||||
const targetCard = cardDb[target.cardId];
|
const targetCard = getCard(room, target.cardId);
|
||||||
enemy.board.splice(boardIdx, 1);
|
enemy.board.splice(boardIdx, 1);
|
||||||
gameState.log.push({ type: 'battlecry', effect: 'destroy_weak_minion', fromPlayer: playerIndex, toPlayer: enemyIdx, toIdx: boardIdx });
|
gameState.log.push({ type: 'battlecry', effect: 'destroy_weak_minion', fromPlayer: playerIndex, toPlayer: enemyIdx, toIdx: boardIdx });
|
||||||
if (targetCard && targetCard.deathrattleId) {
|
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 });
|
gameState.log.push({ type: 'battlecry', effect: 'deal_3_any', fromPlayer: playerIndex, toPlayer: idx, toIdx: boardIdx, damage: 3 });
|
||||||
if (target.health <= 0) {
|
if (target.health <= 0) {
|
||||||
t.board.splice(boardIdx, 1);
|
t.board.splice(boardIdx, 1);
|
||||||
runDeathrattle(room, cardDb[target.cardId], idx);
|
runDeathrattle(room, getCard(room, target.cardId), idx);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
t.health = Math.max(0, t.health - 3);
|
t.health = Math.max(0, t.health - 3);
|
||||||
@ -706,7 +709,7 @@ function runBattlecry(room, card, playerIndex) {
|
|||||||
if (weak.length > 0) {
|
if (weak.length > 0) {
|
||||||
const target = weak[Math.floor(Math.random() * weak.length)];
|
const target = weak[Math.floor(Math.random() * weak.length)];
|
||||||
const boardIdx = enemy.board.indexOf(target);
|
const boardIdx = enemy.board.indexOf(target);
|
||||||
const targetCard = cardDb[target.cardId];
|
const targetCard = getCard(room, target.cardId);
|
||||||
enemy.board.splice(boardIdx, 1);
|
enemy.board.splice(boardIdx, 1);
|
||||||
gameState.log.push({ type: 'battlecry', effect: 'destroy_medium_minion', fromPlayer: playerIndex, toPlayer: enemyIdx, toIdx: boardIdx });
|
gameState.log.push({ type: 'battlecry', effect: 'destroy_medium_minion', fromPlayer: playerIndex, toPlayer: enemyIdx, toIdx: boardIdx });
|
||||||
if (targetCard && targetCard.deathrattleId) {
|
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);
|
const strongest = enemy.board.reduce((max, m) => (m.attack > (max?.attack || 0) ? m : max), null);
|
||||||
if (strongest) {
|
if (strongest) {
|
||||||
const boardIdx = enemy.board.indexOf(strongest);
|
const boardIdx = enemy.board.indexOf(strongest);
|
||||||
const targetCard = cardDb[strongest.cardId];
|
const targetCard = getCard(room, strongest.cardId);
|
||||||
enemy.board.splice(boardIdx, 1);
|
enemy.board.splice(boardIdx, 1);
|
||||||
gameState.log.push({ type: 'battlecry', effect: 'destroy_strongest_enemy', fromPlayer: playerIndex, toPlayer: enemyIdx, toIdx: boardIdx });
|
gameState.log.push({ type: 'battlecry', effect: 'destroy_strongest_enemy', fromPlayer: playerIndex, toPlayer: enemyIdx, toIdx: boardIdx });
|
||||||
if (targetCard && targetCard.deathrattleId) {
|
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 });
|
gameState.log.push({ type: 'battlecry', effect: 'deal_2_or_4_hero', fromPlayer: playerIndex, toPlayer: enemyIdx, toIdx: boardIdx, damage: 2 });
|
||||||
if (target.health <= 0) {
|
if (target.health <= 0) {
|
||||||
enemy.board.splice(boardIdx, 1);
|
enemy.board.splice(boardIdx, 1);
|
||||||
runDeathrattle(room, cardDb[target.cardId], enemyIdx);
|
runDeathrattle(room, getCard(room, target.cardId), enemyIdx);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
enemy.health = Math.max(0, enemy.health - 4);
|
enemy.health = Math.max(0, enemy.health - 4);
|
||||||
@ -805,8 +808,8 @@ function runBattlecry(room, card, playerIndex) {
|
|||||||
enemy.board.forEach((m) => {
|
enemy.board.forEach((m) => {
|
||||||
m.health = Math.max(0, m.health - 2);
|
m.health = Math.max(0, m.health - 2);
|
||||||
if (m.health <= 0) {
|
if (m.health <= 0) {
|
||||||
const card = cardDb[m.cardId];
|
const dc = getCard(room, m.cardId);
|
||||||
if (card && card.deathrattleId) runDeathrattle(room, card, gameState.players.indexOf(enemy));
|
if (dc && dc.deathrattleId) runDeathrattle(room, dc, gameState.players.indexOf(enemy));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
enemy.board = enemy.board.filter((m) => m.health > 0);
|
enemy.board = enemy.board.filter((m) => m.health > 0);
|
||||||
@ -832,7 +835,7 @@ function runBattlecry(room, card, playerIndex) {
|
|||||||
if (strong.length > 0) {
|
if (strong.length > 0) {
|
||||||
const target = strong[Math.floor(Math.random() * strong.length)];
|
const target = strong[Math.floor(Math.random() * strong.length)];
|
||||||
const boardIdx = enemy.board.indexOf(target);
|
const boardIdx = enemy.board.indexOf(target);
|
||||||
const targetCard = cardDb[target.cardId];
|
const targetCard = getCard(room, target.cardId);
|
||||||
enemy.board.splice(boardIdx, 1);
|
enemy.board.splice(boardIdx, 1);
|
||||||
gameState.log.push({ type: 'battlecry', effect: 'destroy_strong_minion', fromPlayer: playerIndex, toPlayer: enemyIdx, toIdx: boardIdx });
|
gameState.log.push({ type: 'battlecry', effect: 'destroy_strong_minion', fromPlayer: playerIndex, toPlayer: enemyIdx, toIdx: boardIdx });
|
||||||
if (targetCard && targetCard.deathrattleId) {
|
if (targetCard && targetCard.deathrattleId) {
|
||||||
@ -967,7 +970,7 @@ function playCard(room, socketId, handIndex, boardPos) {
|
|||||||
if (p.health <= 0 || p.isDead) return; // Мёртвый игрок не может играть
|
if (p.health <= 0 || p.isDead) return; // Мёртвый игрок не может играть
|
||||||
const cid = p.hand[handIndex];
|
const cid = p.hand[handIndex];
|
||||||
if (!cid) return;
|
if (!cid) return;
|
||||||
const card = cardDb[cid];
|
const card = getCard(room, cid);
|
||||||
if (!card || card.type !== 'minion' || p.board.length >= 7) return;
|
if (!card || card.type !== 'minion' || p.board.length >= 7) return;
|
||||||
const cost = card.cost || 0;
|
const cost = card.cost || 0;
|
||||||
if (p.mana < cost) return;
|
if (p.mana < cost) return;
|
||||||
@ -1004,7 +1007,7 @@ function playCard(room, socketId, handIndex, boardPos) {
|
|||||||
playerIndex: enemyIdx,
|
playerIndex: enemyIdx,
|
||||||
boardIndex: boardIdx,
|
boardIndex: boardIdx,
|
||||||
playerName: enemy.name || `Игрок ${enemyIdx + 1}`,
|
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) {
|
function applySynergies(room) {
|
||||||
const gameState = room.gameState;
|
const gameState = room.gameState;
|
||||||
const cardDb = require('./cards.js');
|
const cardDb = require('./cards');
|
||||||
|
|
||||||
gameState.players.forEach((player, playerIndex) => {
|
gameState.players.forEach((player, playerIndex) => {
|
||||||
if (!player.board || player.board.length === 0) return;
|
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; // Мёртвый игрок не может играть
|
if (p.health <= 0 || p.isDead) return; // Мёртвый игрок не может играть
|
||||||
const cid = p.hand[handIndex];
|
const cid = p.hand[handIndex];
|
||||||
if (!cid) return;
|
if (!cid) return;
|
||||||
const card = cardDb[cid];
|
const card = getCard(room, cid);
|
||||||
if (!card || card.type !== 'spell') return;
|
if (!card || card.type !== 'spell') return;
|
||||||
const cost = card.cost || 0;
|
const cost = card.cost || 0;
|
||||||
if (p.mana < cost) return;
|
if (p.mana < cost) return;
|
||||||
@ -1406,8 +1409,8 @@ function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardInde
|
|||||||
m.health -= 2;
|
m.health -= 2;
|
||||||
gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, toPlayer: targetPlayerIndex, toIdx: targetBoardIndex, effect: 'deal_2_minion', damage: 2 });
|
gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, toPlayer: targetPlayerIndex, toIdx: targetBoardIndex, effect: 'deal_2_minion', damage: 2 });
|
||||||
if (m.health <= 0) {
|
if (m.health <= 0) {
|
||||||
const card = cardDb[m.cardId];
|
const dc = getCard(room, m.cardId);
|
||||||
if (card && card.deathrattleId) runDeathrattle(room, card, targetPlayerIndex);
|
if (dc && dc.deathrattleId) runDeathrattle(room, dc, targetPlayerIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (eff === 'draw_2') {
|
} else if (eff === 'draw_2') {
|
||||||
@ -1428,8 +1431,8 @@ function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardInde
|
|||||||
pl.board.forEach((m) => {
|
pl.board.forEach((m) => {
|
||||||
m.health -= 1;
|
m.health -= 1;
|
||||||
if (m.health <= 0) {
|
if (m.health <= 0) {
|
||||||
const card = cardDb[m.cardId];
|
const dc = getCard(room, m.cardId);
|
||||||
if (card && card.deathrattleId) runDeathrattle(room, card, i);
|
if (dc && dc.deathrattleId) runDeathrattle(room, dc, i);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
pl.board = pl.board.filter((m) => m.health > 0);
|
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 });
|
gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, toPlayer: targetPlayerIndex, toIdx: targetBoardIndex, effect: 'deal_4_minion', damage: 4 });
|
||||||
if (m.health <= 0) {
|
if (m.health <= 0) {
|
||||||
t.board.splice(targetBoardIndex, 1);
|
t.board.splice(targetBoardIndex, 1);
|
||||||
const card = cardDb[m.cardId];
|
const dc = getCard(room, m.cardId);
|
||||||
if (card && card.deathrattleId) runDeathrattle(room, card, targetPlayerIndex);
|
if (dc && dc.deathrattleId) runDeathrattle(room, dc, targetPlayerIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (eff === 'heal_4') {
|
} 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 });
|
gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, toPlayer: targetPlayerIndex, toIdx: targetBoardIndex, effect: 'deal_3_minion', damage: 3 });
|
||||||
if (m.health <= 0) {
|
if (m.health <= 0) {
|
||||||
t.board.splice(targetBoardIndex, 1);
|
t.board.splice(targetBoardIndex, 1);
|
||||||
const card = cardDb[m.cardId];
|
const dc = getCard(room, m.cardId);
|
||||||
if (card && card.deathrattleId) runDeathrattle(room, card, targetPlayerIndex);
|
if (dc && dc.deathrattleId) runDeathrattle(room, dc, targetPlayerIndex);
|
||||||
}
|
}
|
||||||
} else if (eff === 'freeze_damage') {
|
} else if (eff === 'freeze_damage') {
|
||||||
const t = gameState.players[targetPlayerIndex];
|
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 });
|
gameState.log.push({ type: 'spell', spell: cid, fromPlayer: pi, toPlayer: targetPlayerIndex, toIdx: targetBoardIndex, effect: 'freeze_damage', damage: 2 });
|
||||||
if (m.health <= 0) {
|
if (m.health <= 0) {
|
||||||
t.board.splice(targetBoardIndex, 1);
|
t.board.splice(targetBoardIndex, 1);
|
||||||
const card = cardDb[m.cardId];
|
const dc = getCard(room, m.cardId);
|
||||||
if (card && card.deathrattleId) runDeathrattle(room, card, targetPlayerIndex);
|
if (dc && dc.deathrattleId) runDeathrattle(room, dc, targetPlayerIndex);
|
||||||
}
|
}
|
||||||
} else if (eff === 'buff_all_friendly') {
|
} else if (eff === 'buff_all_friendly') {
|
||||||
p.board.forEach((m) => {
|
p.board.forEach((m) => {
|
||||||
@ -1515,7 +1518,7 @@ function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardInde
|
|||||||
const weak = enemy.board.filter(m => m.attack <= 2);
|
const weak = enemy.board.filter(m => m.attack <= 2);
|
||||||
weak.forEach((target) => {
|
weak.forEach((target) => {
|
||||||
const boardIdx = enemy.board.indexOf(target);
|
const boardIdx = enemy.board.indexOf(target);
|
||||||
const targetCard = cardDb[target.cardId];
|
const targetCard = getCard(room, target.cardId);
|
||||||
enemy.board.splice(boardIdx, 1);
|
enemy.board.splice(boardIdx, 1);
|
||||||
if (targetCard && targetCard.deathrattleId) {
|
if (targetCard && targetCard.deathrattleId) {
|
||||||
runDeathrattle(room, targetCard, gameState.players.indexOf(enemy));
|
runDeathrattle(room, targetCard, gameState.players.indexOf(enemy));
|
||||||
@ -1551,7 +1554,7 @@ function stealCardsFromDeck(room, socketId, handIndex, targetPlayerIndex, cardIn
|
|||||||
|
|
||||||
const cid = p.hand[handIndex];
|
const cid = p.hand[handIndex];
|
||||||
if (!cid) return;
|
if (!cid) return;
|
||||||
const card = cardDb[cid];
|
const card = getCard(room, cid);
|
||||||
if (!card || card.type !== 'spell' || card.spellEffect !== 'steal_cards') return;
|
if (!card || card.type !== 'spell' || card.spellEffect !== 'steal_cards') return;
|
||||||
const cost = card.cost || 0;
|
const cost = card.cost || 0;
|
||||||
if (p.mana < cost) return;
|
if (p.mana < cost) return;
|
||||||
@ -1610,6 +1613,71 @@ function stealCardsFromDeck(room, socketId, handIndex, targetPlayerIndex, cardIn
|
|||||||
broadcastGameState(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) {
|
function heroAbility(room, socketId, targetPlayerIndex, targetBoardIndex) {
|
||||||
const gameState = room.gameState;
|
const gameState = room.gameState;
|
||||||
const pi = findPlayerIndex(room, socketId);
|
const pi = findPlayerIndex(room, socketId);
|
||||||
@ -1634,7 +1702,7 @@ function heroAbility(room, socketId, targetPlayerIndex, targetBoardIndex) {
|
|||||||
if (targetBoardIndex !== -1) {
|
if (targetBoardIndex !== -1) {
|
||||||
const m = targetPlayer.board[targetBoardIndex];
|
const m = targetPlayer.board[targetBoardIndex];
|
||||||
if (m && m.health <= 0) {
|
if (m && m.health <= 0) {
|
||||||
const card = cardDb[m.cardId];
|
const card = getCard(room, m.cardId);
|
||||||
if (card && card.deathrattleId) runDeathrattle(room, card, targetPlayerIndex);
|
if (card && card.deathrattleId) runDeathrattle(room, card, targetPlayerIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1708,8 +1776,8 @@ function attack(room, socketId, attackerBoardIndex, targetPlayerIndex, targetBoa
|
|||||||
damage: attacker.attack,
|
damage: attacker.attack,
|
||||||
reverseDamage: target.attack,
|
reverseDamage: target.attack,
|
||||||
});
|
});
|
||||||
const targetCard = cardDb[target.cardId];
|
const targetCard = getCard(room, target.cardId);
|
||||||
const attackerCard = cardDb[attacker.cardId];
|
const attackerCard = getCard(room, attacker.cardId);
|
||||||
if (targetDied && targetCard && targetCard.deathrattleId) {
|
if (targetDied && targetCard && targetCard.deathrattleId) {
|
||||||
runDeathrattle(room, targetCard, targetPlayerIndex);
|
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) {
|
if (attackerCard && attackerCard.canAttackTwice && !attacker.attacksUsed) {
|
||||||
attacker.attacksUsed = 1; // Использована одна атака
|
attacker.attacksUsed = 1; // Использована одна атака
|
||||||
} else {
|
} else {
|
||||||
@ -1868,6 +1936,37 @@ io.on('connection', (socket) => {
|
|||||||
manualDraw(room, socket.id);
|
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) => {
|
socket.on('forgeCard', (data) => {
|
||||||
const room = getRoomBySocket(socket.id);
|
const room = getRoomBySocket(socket.id);
|
||||||
if (!room || !room.gameState || room.gameState.phase !== 'playing') return;
|
if (!room || !room.gameState || room.gameState.phase !== 'playing') return;
|
||||||
|
|||||||
Reference in New Issue
Block a user