123
This commit is contained in:
8
cards.js
8
cards.js
@ -1012,11 +1012,11 @@ module.exports = {
|
||||
cost: 3,
|
||||
type: 'spell',
|
||||
faction: 'pirates',
|
||||
text: 'Возьми 2 карты.',
|
||||
text: 'Укради 2 карты из колоды противника.',
|
||||
art: 'plunder',
|
||||
spellEffect: 'draw_2',
|
||||
spellTarget: 'none',
|
||||
bio: 'Пираты грабят и получают добычу.',
|
||||
spellEffect: 'steal_cards',
|
||||
spellTarget: 'enemy_player',
|
||||
bio: 'Пираты грабят и крадут карты у противников.',
|
||||
},
|
||||
mandalorian_rage: {
|
||||
name: 'Ярость мандалорца',
|
||||
|
||||
327
public/game.js
327
public/game.js
@ -13,6 +13,7 @@
|
||||
let attackMode = { active: false, attackerPlayer: -1, attackerBoard: -1 };
|
||||
let spellMode = { active: false, handIndex: -1, cardId: '', spellTarget: '' };
|
||||
let heroAbilityMode = { active: false };
|
||||
let stealCardsMode = { active: false, handIndex: -1, targetPlayerIndex: null, targetDeck: [], selectedIndices: [] };
|
||||
const seenMinions = new Set();
|
||||
let lastHandLength = 0;
|
||||
let prevGameState = null;
|
||||
@ -200,6 +201,40 @@
|
||||
}
|
||||
renderGame(state);
|
||||
});
|
||||
|
||||
socket.on('stealCardsRequest', (data) => {
|
||||
if (!gameState) return;
|
||||
// Находим handIndex заклинания "Грабеж" в текущей руке
|
||||
const you = gameState.players[gameState.yourIndex];
|
||||
if (!you || !you.hand) return;
|
||||
|
||||
// Ищем заклинание "Грабеж" в руке
|
||||
let handIndex = -1;
|
||||
for (let i = 0; i < you.hand.length; i++) {
|
||||
const card = cardDb[you.hand[i]];
|
||||
if (card && card.spellEffect === 'steal_cards') {
|
||||
handIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (handIndex < 0) return;
|
||||
|
||||
stealCardsMode.active = true;
|
||||
stealCardsMode.handIndex = handIndex;
|
||||
stealCardsMode.targetPlayerIndex = data.targetPlayerIndex;
|
||||
stealCardsMode.targetDeck = [];
|
||||
stealCardsMode.selectedIndices = [];
|
||||
|
||||
// Получаем колоду противника из gameState
|
||||
const targetPlayer = gameState.players[data.targetPlayerIndex];
|
||||
if (targetPlayer && targetPlayer.deck) {
|
||||
stealCardsMode.targetDeck = [...targetPlayer.deck];
|
||||
}
|
||||
|
||||
showStealCardsModal(gameState, data);
|
||||
});
|
||||
|
||||
return socket;
|
||||
}
|
||||
|
||||
@ -253,12 +288,14 @@
|
||||
const minions = (p.board || []).map((m, j) => renderBoardMinion(m, i, j, state, true, false));
|
||||
const heroBar = renderHeroTarget(i, state);
|
||||
const heroDrop = renderHeroDropZone(i, state);
|
||||
const canSteal = state.currentPlayerIndex === state.yourIndex && spellMode.active && spellMode.cardId && cardDb[spellMode.cardId]?.spellEffect === 'steal_cards';
|
||||
return `
|
||||
<div class="opponent-block ${isCurrent ? 'current-turn' : ''}" data-opponent-index="${i}">
|
||||
<div class="opponent-block ${isCurrent ? 'current-turn' : ''} ${canSteal ? 'steal-target' : ''}" data-opponent-index="${i}" data-player-index="${i}">
|
||||
<div class="opponent-name">${escapeHtml(name)}</div>
|
||||
<div class="opponent-stats">
|
||||
<span>❤ ${p.health}</span>
|
||||
<span>🔵 ${p.mana}/${p.maxMana}</span>
|
||||
${p.deck && p.deck.length > 0 ? `<span>📚 ${p.deck.length}</span>` : ''}
|
||||
</div>
|
||||
<div class="opponent-board">${heroDrop}${heroBar}${minions.join('')}</div>
|
||||
</div>`;
|
||||
@ -888,14 +925,32 @@
|
||||
return;
|
||||
}
|
||||
if (meta.type === 'spell') {
|
||||
if (spellMode.active || heroAbilityMode.active) return;
|
||||
if (spellMode.active || heroAbilityMode.active || stealCardsMode.active) return;
|
||||
var needTarget = meta.spellTarget && meta.spellTarget !== 'none';
|
||||
|
||||
// Специальная обработка для Грабежа
|
||||
if (meta.spellEffect === 'steal_cards') {
|
||||
if (meta.spellTarget === 'enemy_player') {
|
||||
// Открываем модальное окно для выбора противника
|
||||
spellMode = { active: true, handIndex: handIndex, cardId: wrap.dataset.cardId, spellTarget: meta.spellTarget };
|
||||
$('spell-mode')?.classList.remove('hidden');
|
||||
renderGame(state);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!needTarget) {
|
||||
if (typeof window.Sounds !== 'undefined') window.Sounds.playCard();
|
||||
socket.emit('playSpell', { handIndex: handIndex });
|
||||
return;
|
||||
}
|
||||
spellMode = { active: true, handIndex: handIndex, cardId: wrap.dataset.cardId, spellTarget: meta.spellTarget };
|
||||
const spellModeText = $('spell-mode')?.querySelector('p');
|
||||
if (spellModeText && meta.spellEffect === 'steal_cards') {
|
||||
spellModeText.textContent = 'Выберите противника для грабежа (кликните на блок противника)';
|
||||
} else if (spellModeText) {
|
||||
spellModeText.textContent = 'Выберите цель для заклинания';
|
||||
}
|
||||
$('spell-mode')?.classList.remove('hidden');
|
||||
renderGame(state);
|
||||
return;
|
||||
@ -1184,6 +1239,34 @@
|
||||
e.stopPropagation();
|
||||
var tp = parseInt(el.dataset.dropPlayer ?? el.dataset.playerIndex, 10);
|
||||
var tb = parseInt(el.dataset.dropBoard ?? el.dataset.boardIndex, 10);
|
||||
|
||||
// Специальная обработка для Грабежа - открываем модальное окно
|
||||
const spellCard = cardDb[spellMode.cardId];
|
||||
if (spellCard && spellCard.spellEffect === 'steal_cards' && spellCard.spellTarget === 'enemy_player') {
|
||||
// Выбираем противника (tp должен быть индексом противника, tb игнорируем для enemy_player)
|
||||
if (tp !== state.yourIndex && tp >= 0) {
|
||||
const targetPlayer = state.players[tp];
|
||||
if (targetPlayer && targetPlayer.deck && targetPlayer.deck.length > 0) {
|
||||
stealCardsMode.active = true;
|
||||
stealCardsMode.handIndex = spellMode.handIndex;
|
||||
stealCardsMode.targetPlayerIndex = tp;
|
||||
// Получаем актуальную колоду из gameState
|
||||
stealCardsMode.targetDeck = targetPlayer.deck ? [...targetPlayer.deck] : [];
|
||||
stealCardsMode.selectedIndices = [];
|
||||
showStealCardsModal(state, {
|
||||
targetPlayerIndex: tp,
|
||||
targetPlayerName: targetPlayer.name || `Игрок ${tp + 1}`,
|
||||
targetDeckSize: targetPlayer.deck.length,
|
||||
maxCards: Math.min(2, targetPlayer.deck.length)
|
||||
});
|
||||
spellMode = { active: false, handIndex: -1, cardId: '', spellTarget: '' };
|
||||
$('spell-mode')?.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window.Sounds !== 'undefined') window.Sounds.playCard();
|
||||
socket.emit('playSpell', { handIndex: spellMode.handIndex, targetPlayerIndex: tp, targetBoardIndex: tb });
|
||||
spellMode = { active: false, handIndex: -1, cardId: '', spellTarget: '' };
|
||||
@ -1199,6 +1282,33 @@
|
||||
e.stopPropagation();
|
||||
var tp = parseInt(el.dataset.dropPlayer ?? el.dataset.playerIndex, 10);
|
||||
var tb = parseInt(el.dataset.dropBoard ?? el.dataset.boardIndex, 10);
|
||||
|
||||
// Специальная обработка для Грабежа
|
||||
const spellCard = cardDb[spellMode.cardId];
|
||||
if (spellCard && spellCard.spellEffect === 'steal_cards' && spellCard.spellTarget === 'enemy_player') {
|
||||
if (tp !== state.yourIndex && tp >= 0) {
|
||||
const targetPlayer = state.players[tp];
|
||||
if (targetPlayer && targetPlayer.deck && targetPlayer.deck.length > 0) {
|
||||
stealCardsMode.active = true;
|
||||
stealCardsMode.handIndex = spellMode.handIndex;
|
||||
stealCardsMode.targetPlayerIndex = tp;
|
||||
// Получаем актуальную колоду из gameState
|
||||
stealCardsMode.targetDeck = targetPlayer.deck ? [...targetPlayer.deck] : [];
|
||||
stealCardsMode.selectedIndices = [];
|
||||
showStealCardsModal(state, {
|
||||
targetPlayerIndex: tp,
|
||||
targetPlayerName: targetPlayer.name || `Игрок ${tp + 1}`,
|
||||
targetDeckSize: targetPlayer.deck.length,
|
||||
maxCards: Math.min(2, targetPlayer.deck.length)
|
||||
});
|
||||
spellMode = { active: false, handIndex: -1, cardId: '', spellTarget: '' };
|
||||
$('spell-mode')?.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window.Sounds !== 'undefined') window.Sounds.playCard();
|
||||
socket.emit('playSpell', { handIndex: spellMode.handIndex, targetPlayerIndex: tp, targetBoardIndex: tb });
|
||||
spellMode = { active: false, handIndex: -1, cardId: '', spellTarget: '' };
|
||||
@ -1238,6 +1348,65 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Обработка выбора противника для Грабежа
|
||||
$all('.opponent-block.steal-target').forEach(function (el) {
|
||||
el.onclick = function (e) {
|
||||
if (!spellMode.active) return;
|
||||
const spellCard = cardDb[spellMode.cardId];
|
||||
if (!spellCard || spellCard.spellEffect !== 'steal_cards' || spellCard.spellTarget !== 'enemy_player') return;
|
||||
e.stopPropagation();
|
||||
var tp = parseInt(el.dataset.playerIndex ?? el.dataset.opponentIndex, 10);
|
||||
if (tp === state.yourIndex || tp < 0) return;
|
||||
const targetPlayer = state.players[tp];
|
||||
if (targetPlayer && targetPlayer.deck && targetPlayer.deck.length > 0) {
|
||||
stealCardsMode.active = true;
|
||||
stealCardsMode.handIndex = spellMode.handIndex;
|
||||
stealCardsMode.targetPlayerIndex = tp;
|
||||
// Получаем актуальную колоду из gameState
|
||||
stealCardsMode.targetDeck = targetPlayer.deck ? [...targetPlayer.deck] : [];
|
||||
stealCardsMode.selectedIndices = [];
|
||||
showStealCardsModal(state, {
|
||||
targetPlayerIndex: tp,
|
||||
targetPlayerName: targetPlayer.name || `Игрок ${tp + 1}`,
|
||||
targetDeckSize: targetPlayer.deck.length,
|
||||
maxCards: Math.min(2, targetPlayer.deck.length)
|
||||
});
|
||||
spellMode = { active: false, handIndex: -1, cardId: '', spellTarget: '' };
|
||||
$('spell-mode')?.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
// Touch support
|
||||
if (isTouchDevice()) {
|
||||
el.ontouchstart = function (e) {
|
||||
if (!spellMode.active) return;
|
||||
const spellCard = cardDb[spellMode.cardId];
|
||||
if (!spellCard || spellCard.spellEffect !== 'steal_cards' || spellCard.spellTarget !== 'enemy_player') return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
var tp = parseInt(el.dataset.playerIndex ?? el.dataset.opponentIndex, 10);
|
||||
if (tp === state.yourIndex || tp < 0) return;
|
||||
const targetPlayer = state.players[tp];
|
||||
if (targetPlayer && targetPlayer.deck && targetPlayer.deck.length > 0) {
|
||||
stealCardsMode.active = true;
|
||||
stealCardsMode.handIndex = spellMode.handIndex;
|
||||
stealCardsMode.targetPlayerIndex = tp;
|
||||
// Получаем актуальную колоду из gameState
|
||||
stealCardsMode.targetDeck = targetPlayer.deck ? [...targetPlayer.deck] : [];
|
||||
stealCardsMode.selectedIndices = [];
|
||||
showStealCardsModal(state, {
|
||||
targetPlayerIndex: tp,
|
||||
targetPlayerName: targetPlayer.name || `Игрок ${tp + 1}`,
|
||||
targetDeckSize: targetPlayer.deck.length,
|
||||
maxCards: Math.min(2, targetPlayer.deck.length)
|
||||
});
|
||||
spellMode = { active: false, handIndex: -1, cardId: '', spellTarget: '' };
|
||||
$('spell-mode')?.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
var heroAbilityBtn = $('btn-hero-ability');
|
||||
if (heroAbilityBtn) heroAbilityBtn.onclick = function () {
|
||||
if (heroAbilityBtn.disabled || heroAbilityMode.active) return;
|
||||
@ -1263,9 +1432,11 @@
|
||||
spellMode = { active: false, handIndex: -1, cardId: '', spellTarget: '' };
|
||||
attackMode = { active: false, attackerPlayer: -1, attackerBoard: -1 };
|
||||
heroAbilityMode = { active: false };
|
||||
stealCardsMode = { active: false, handIndex: -1, targetPlayerIndex: null, targetDeck: [], selectedIndices: [] };
|
||||
$('spell-mode')?.classList.add('hidden');
|
||||
$('attack-mode')?.classList.add('hidden');
|
||||
$('hero-ability-mode')?.classList.add('hidden');
|
||||
$('steal-cards-overlay')?.classList.add('hidden');
|
||||
renderGame(state);
|
||||
};
|
||||
|
||||
@ -1282,9 +1453,11 @@
|
||||
attackMode = { active: false, attackerPlayer: -1, attackerBoard: -1 };
|
||||
spellMode = { active: false, handIndex: -1, cardId: '', spellTarget: '' };
|
||||
heroAbilityMode = { active: false };
|
||||
stealCardsMode = { active: false, handIndex: -1, targetPlayerIndex: null, targetDeck: [], selectedIndices: [] };
|
||||
$('attack-mode')?.classList.add('hidden');
|
||||
$('spell-mode')?.classList.add('hidden');
|
||||
$('hero-ability-mode')?.classList.add('hidden');
|
||||
$('steal-cards-overlay')?.classList.add('hidden');
|
||||
renderGame(state);
|
||||
};
|
||||
}
|
||||
@ -1700,6 +1873,156 @@
|
||||
}
|
||||
});
|
||||
|
||||
$('btn-steal-close')?.addEventListener('click', () => {
|
||||
$('steal-cards-overlay')?.classList.add('hidden');
|
||||
stealCardsMode.active = false;
|
||||
stealCardsMode.handIndex = -1;
|
||||
stealCardsMode.targetPlayerIndex = null;
|
||||
stealCardsMode.targetDeck = [];
|
||||
stealCardsMode.selectedIndices = [];
|
||||
});
|
||||
|
||||
$('steal-cards-overlay')?.addEventListener('click', (e) => {
|
||||
if (e.target.id === 'steal-cards-overlay') {
|
||||
$('steal-cards-overlay')?.classList.add('hidden');
|
||||
stealCardsMode.active = false;
|
||||
stealCardsMode.handIndex = -1;
|
||||
stealCardsMode.targetPlayerIndex = null;
|
||||
stealCardsMode.targetDeck = [];
|
||||
stealCardsMode.selectedIndices = [];
|
||||
}
|
||||
});
|
||||
|
||||
function showStealCardsModal(state, data) {
|
||||
const deckList = $('steal-deck-list');
|
||||
const selectedEl = $('steal-selected');
|
||||
const confirmBtn = $('btn-steal-confirm');
|
||||
const hintEl = $('steal-cards-hint');
|
||||
const targetSelect = $('steal-target-select');
|
||||
|
||||
if (!deckList || !selectedEl || !confirmBtn) return;
|
||||
|
||||
// Если противник уже выбран, показываем его колоду
|
||||
if (stealCardsMode.targetPlayerIndex !== null) {
|
||||
// Обновляем колоду из актуального gameState
|
||||
const targetPlayer = state.players[stealCardsMode.targetPlayerIndex];
|
||||
if (targetPlayer && targetPlayer.deck) {
|
||||
stealCardsMode.targetDeck = [...targetPlayer.deck];
|
||||
}
|
||||
|
||||
if (stealCardsMode.targetDeck.length === 0) {
|
||||
// Колода пуста, закрываем модальное окно
|
||||
$('steal-cards-overlay')?.classList.add('hidden');
|
||||
stealCardsMode.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
targetSelect.classList.add('hidden');
|
||||
deckList.classList.remove('hidden');
|
||||
|
||||
if (hintEl) {
|
||||
hintEl.textContent = `Выберите до 2 карт из колоды ${targetPlayer?.name || `Игрока ${stealCardsMode.targetPlayerIndex + 1}`} (${stealCardsMode.targetDeck.length} карт)`;
|
||||
}
|
||||
|
||||
deckList.innerHTML = stealCardsMode.targetDeck.map((cardId, idx) => {
|
||||
const meta = cardDb[cardId];
|
||||
if (!meta) return '';
|
||||
const isSelected = stealCardsMode.selectedIndices.includes(idx);
|
||||
const cost = meta.cost || 0;
|
||||
const attack = meta.attack !== undefined ? meta.attack : '';
|
||||
const health = meta.health !== undefined ? meta.health : '';
|
||||
const stats = meta.type === 'minion' ? `<div class="card-stats"><span class="atk">${attack}</span><span class="hp">${health}</span></div>` : '';
|
||||
const costDisplay = `<div class="card-cost-wrap"><span class="card-cost">${cost}</span></div>`;
|
||||
const art = getCardArt ? getCardArt(meta) : '✦';
|
||||
|
||||
return `<div class="card-wrap steal-deck-card ${isSelected ? 'selected' : ''}" data-card-id="${cardId}" data-deck-index="${idx}" style="width: 100px; height: 140px; cursor: pointer;">
|
||||
<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.7rem;">${escapeHtml(meta.name)}</div>
|
||||
${stats}
|
||||
${costDisplay}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).filter(Boolean).join('');
|
||||
|
||||
$all('.steal-deck-card').forEach(card => {
|
||||
card.onclick = function() {
|
||||
const idx = parseInt(card.dataset.deckIndex, 10);
|
||||
const selectedIdx = stealCardsMode.selectedIndices.indexOf(idx);
|
||||
if (selectedIdx >= 0) {
|
||||
stealCardsMode.selectedIndices.splice(selectedIdx, 1);
|
||||
} else if (stealCardsMode.selectedIndices.length < 2) {
|
||||
stealCardsMode.selectedIndices.push(idx);
|
||||
}
|
||||
showStealCardsModal(state, data);
|
||||
};
|
||||
});
|
||||
|
||||
selectedEl.innerHTML = stealCardsMode.selectedIndices.length ? stealCardsMode.selectedIndices.map(idx => {
|
||||
const cardId = stealCardsMode.targetDeck[idx];
|
||||
const meta = cardDb[cardId];
|
||||
if (!meta) return '';
|
||||
return `<div class="card-wrap" style="width: 80px; height: 112px;"><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.6rem;">${escapeHtml(meta.name)}</div></div></div></div>`;
|
||||
}).join('') : '<div class="steal-selected-empty">Выберите до 2 карт</div>';
|
||||
|
||||
confirmBtn.disabled = stealCardsMode.selectedIndices.length === 0;
|
||||
confirmBtn.onclick = function() {
|
||||
if (confirmBtn.disabled || stealCardsMode.selectedIndices.length === 0) return;
|
||||
if (typeof window.Sounds !== 'undefined') window.Sounds.playCard();
|
||||
socket.emit('stealCards', {
|
||||
handIndex: stealCardsMode.handIndex,
|
||||
targetPlayerIndex: stealCardsMode.targetPlayerIndex,
|
||||
cardIndices: stealCardsMode.selectedIndices
|
||||
});
|
||||
stealCardsMode.active = false;
|
||||
stealCardsMode.handIndex = -1;
|
||||
stealCardsMode.targetPlayerIndex = null;
|
||||
stealCardsMode.targetDeck = [];
|
||||
stealCardsMode.selectedIndices = [];
|
||||
$('steal-cards-overlay')?.classList.add('hidden');
|
||||
};
|
||||
} else {
|
||||
// Показываем выбор противника (если еще не выбран)
|
||||
targetSelect.classList.remove('hidden');
|
||||
deckList.classList.add('hidden');
|
||||
|
||||
if (hintEl) {
|
||||
hintEl.textContent = 'Выберите противника для грабежа';
|
||||
}
|
||||
|
||||
const enemies = state.players.filter((p, i) => i !== state.yourIndex && p.health > 0 && p.deck && p.deck.length > 0);
|
||||
targetSelect.innerHTML = enemies.map((enemy, idx) => {
|
||||
const enemyIdx = state.players.indexOf(enemy);
|
||||
return `<div class="steal-target-option" data-player-index="${enemyIdx}" style="padding: 1rem; margin: 0.5rem 0; background: rgba(0,0,0,0.3); border: 2px solid rgba(0,180,255,0.3); border-radius: 8px; cursor: pointer; transition: all 0.2s;">
|
||||
<div style="font-weight: 700; color: var(--cyan);">${escapeHtml(enemy.name || `Игрок ${enemyIdx + 1}`)}</div>
|
||||
<div style="font-size: 0.85rem; color: #94a3b8; margin-top: 0.25rem;">Колода: ${enemy.deck.length} карт</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
$all('.steal-target-option').forEach(option => {
|
||||
option.onclick = function() {
|
||||
const playerIdx = parseInt(option.dataset.playerIndex, 10);
|
||||
const targetPlayer = state.players[playerIdx];
|
||||
if (targetPlayer && targetPlayer.deck && targetPlayer.deck.length > 0) {
|
||||
stealCardsMode.targetPlayerIndex = playerIdx;
|
||||
// Получаем актуальную колоду из gameState
|
||||
stealCardsMode.targetDeck = targetPlayer.deck ? [...targetPlayer.deck] : [];
|
||||
showStealCardsModal(state, {
|
||||
targetPlayerIndex: playerIdx,
|
||||
targetPlayerName: targetPlayer.name || `Игрок ${playerIdx + 1}`,
|
||||
targetDeckSize: targetPlayer.deck.length,
|
||||
maxCards: Math.min(2, targetPlayer.deck.length)
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
$('steal-cards-overlay')?.classList.remove('hidden');
|
||||
}
|
||||
|
||||
let forgeSelected = [];
|
||||
function renderForgeDeck(state) {
|
||||
const you = state.players[state.yourIndex];
|
||||
|
||||
@ -263,6 +263,19 @@
|
||||
<button type="button" id="btn-card-info-close" class="btn btn-primary">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="steal-cards-overlay" class="modal-overlay hidden">
|
||||
<div class="modal steal-cards-modal">
|
||||
<h2>⚔ Грабеж</h2>
|
||||
<p class="hint" id="steal-cards-hint" style="font-size: 0.85rem; margin-bottom: 1rem;">Выберите противника и до 2 карт из его колоды</p>
|
||||
<div id="steal-target-select" class="steal-target-select"></div>
|
||||
<div id="steal-deck-list" class="steal-deck-list hidden"></div>
|
||||
<div id="steal-selected" class="steal-selected"></div>
|
||||
<div class="steal-actions">
|
||||
<button type="button" id="btn-steal-confirm" class="btn btn-primary" disabled>Украсть карты</button>
|
||||
<button type="button" id="btn-steal-close" class="btn btn-ghost">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="settings-overlay" class="modal-overlay hidden">
|
||||
<div class="modal settings-modal">
|
||||
<h2>Настройки</h2>
|
||||
|
||||
@ -504,6 +504,21 @@ html, body {
|
||||
border-color: rgba(255,180,0,0.6);
|
||||
box-shadow: 0 0 24px rgba(255,180,0,0.2);
|
||||
}
|
||||
.opponent-block.steal-target {
|
||||
border-color: rgba(255,204,0,0.6);
|
||||
box-shadow: 0 0 20px rgba(255,204,0,0.4);
|
||||
cursor: pointer;
|
||||
animation: stealTargetPulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
.opponent-block.steal-target:hover {
|
||||
border-color: rgba(255,204,0,0.9);
|
||||
box-shadow: 0 0 30px rgba(255,204,0,0.6);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
@keyframes stealTargetPulse {
|
||||
0%, 100% { box-shadow: 0 0 20px rgba(255,204,0,0.4); }
|
||||
50% { box-shadow: 0 0 30px rgba(255,204,0,0.7); }
|
||||
}
|
||||
.opponent-name { font-weight: 700; color: var(--cyan); margin-bottom: 0.35rem; font-size: 0.95rem; }
|
||||
.opponent-stats { display: flex; gap: 0.75rem; font-size: 0.85rem; color: #94a3b8; margin-bottom: 0.5rem; }
|
||||
.opponent-board { display: flex; flex-wrap: wrap; gap: 0.5rem; min-height: 80px; }
|
||||
@ -1435,6 +1450,67 @@ html, body {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.settings-modal { max-width: 400px; text-align: left; }
|
||||
.steal-cards-modal {
|
||||
max-width: 800px;
|
||||
max-height: 90vh;
|
||||
text-align: left;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.steal-target-select {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.steal-target-option:hover {
|
||||
background: rgba(0,180,255,0.2) !important;
|
||||
border-color: var(--cyan) !important;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
.steal-deck-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
||||
gap: 0.75rem;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem;
|
||||
background: rgba(0,0,0,0.3);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.steal-deck-card {
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
.steal-deck-card:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 0 15px rgba(0,180,255,0.5);
|
||||
}
|
||||
.steal-deck-card.selected {
|
||||
border-color: var(--cyan);
|
||||
box-shadow: 0 0 20px rgba(0,180,255,0.7);
|
||||
}
|
||||
.steal-selected {
|
||||
min-height: 80px;
|
||||
padding: 0.5rem;
|
||||
background: rgba(255,204,0,0.1);
|
||||
border: 2px dashed rgba(255,204,0,0.3);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
.steal-selected-empty {
|
||||
color: #94a3b8;
|
||||
font-size: 0.9rem;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
.steal-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
}
|
||||
.settings-content { margin: 1.5rem 0; }
|
||||
.setting-group {
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
88
server.js
88
server.js
@ -778,7 +778,28 @@ function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardInde
|
||||
const eff = card.spellEffect;
|
||||
const needTarget = card.spellTarget && card.spellTarget !== 'none';
|
||||
|
||||
if (needTarget && (targetPlayerIndex == null || targetBoardIndex == null)) return;
|
||||
// Специальная обработка для steal_cards - требует только выбор противника (не требует targetBoardIndex)
|
||||
if (eff === 'steal_cards') {
|
||||
if (targetPlayerIndex == null || targetPlayerIndex === pi) return;
|
||||
const targetPlayer = gameState.players[targetPlayerIndex];
|
||||
if (!targetPlayer || targetPlayer.health <= 0 || !targetPlayer.deck || targetPlayer.deck.length === 0) return;
|
||||
|
||||
// Отправляем запрос на выбор карт для кражи
|
||||
const socket = io.sockets.sockets.get(p.id);
|
||||
if (socket) {
|
||||
socket.emit('stealCardsRequest', {
|
||||
targetPlayerIndex: targetPlayerIndex,
|
||||
targetPlayerName: targetPlayer.name || `Игрок ${targetPlayerIndex + 1}`,
|
||||
targetDeckSize: targetPlayer.deck.length,
|
||||
maxCards: Math.min(2, targetPlayer.deck.length)
|
||||
});
|
||||
}
|
||||
return; // Не тратим ману и не удаляем карту пока - это сделаем после выбора
|
||||
}
|
||||
|
||||
// Для других заклинаний проверяем targetBoardIndex только если это не выбор игрока
|
||||
if (needTarget && card.spellTarget !== 'enemy_player' && (targetPlayerIndex == null || targetBoardIndex == null)) return;
|
||||
if (needTarget && card.spellTarget === 'enemy_player' && targetPlayerIndex == null) return;
|
||||
const enemies = gameState.players.filter((pl, i) => i !== pi && pl.health > 0);
|
||||
|
||||
if (eff === 'deal_2') {
|
||||
@ -927,6 +948,65 @@ function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardInde
|
||||
broadcastGameState(room);
|
||||
}
|
||||
|
||||
function stealCardsFromDeck(room, socketId, handIndex, targetPlayerIndex, cardIndices) {
|
||||
const gameState = room.gameState;
|
||||
const pi = findPlayerIndex(room, socketId);
|
||||
if (pi < 0 || gameState.currentPlayerIndex !== pi) return;
|
||||
const p = gameState.players[pi];
|
||||
if (p.health <= 0 || p.isDead) return;
|
||||
|
||||
const cid = p.hand[handIndex];
|
||||
if (!cid) return;
|
||||
const card = cardDb[cid];
|
||||
if (!card || card.type !== 'spell' || card.spellEffect !== 'steal_cards') return;
|
||||
const cost = card.cost || 0;
|
||||
if (p.mana < cost) return;
|
||||
|
||||
const targetPlayer = gameState.players[targetPlayerIndex];
|
||||
if (!targetPlayer || targetPlayerIndex === pi || targetPlayer.health <= 0) return;
|
||||
if (!targetPlayer.deck || targetPlayer.deck.length === 0) return;
|
||||
|
||||
// Проверяем индексы карт
|
||||
if (!Array.isArray(cardIndices) || cardIndices.length === 0 || cardIndices.length > 2) return;
|
||||
const validIndices = cardIndices.filter(idx => idx >= 0 && idx < targetPlayer.deck.length);
|
||||
if (validIndices.length === 0) return;
|
||||
|
||||
// Убираем дубликаты и сортируем по убыванию (чтобы удалять с конца)
|
||||
const uniqueIndices = [...new Set(validIndices)].sort((a, b) => b - a);
|
||||
|
||||
// Крадём карты
|
||||
const stolenCards = [];
|
||||
uniqueIndices.forEach(idx => {
|
||||
if (idx >= 0 && idx < targetPlayer.deck.length) {
|
||||
stolenCards.push(targetPlayer.deck[idx]);
|
||||
targetPlayer.deck.splice(idx, 1);
|
||||
}
|
||||
});
|
||||
|
||||
// Добавляем украденные карты в руку игрока
|
||||
stolenCards.forEach(cardId => {
|
||||
if (p.hand.length < 10) {
|
||||
p.hand.push(cardId);
|
||||
}
|
||||
});
|
||||
|
||||
// Тратим ману и удаляем заклинание
|
||||
p.mana -= cost;
|
||||
p.hand.splice(handIndex, 1);
|
||||
|
||||
gameState.log.push({
|
||||
type: 'spell',
|
||||
spell: cid,
|
||||
fromPlayer: pi,
|
||||
toPlayer: targetPlayerIndex,
|
||||
effect: 'steal_cards',
|
||||
stolenCount: stolenCards.length
|
||||
});
|
||||
|
||||
checkGameOver(room);
|
||||
broadcastGameState(room);
|
||||
}
|
||||
|
||||
function heroAbility(room, socketId, targetPlayerIndex, targetBoardIndex) {
|
||||
const gameState = room.gameState;
|
||||
const pi = findPlayerIndex(room, socketId);
|
||||
@ -1165,6 +1245,12 @@ io.on('connection', (socket) => {
|
||||
forgeCard(room, socket.id, data.cardIds);
|
||||
});
|
||||
|
||||
socket.on('stealCards', (data) => {
|
||||
const room = getRoomBySocket(socket.id);
|
||||
if (!room || !room.gameState || room.gameState.phase !== 'playing') return;
|
||||
stealCardsFromDeck(room, socket.id, data.handIndex, data.targetPlayerIndex, data.cardIndices);
|
||||
});
|
||||
|
||||
socket.on('resetToLobby', () => {
|
||||
const room = getRoomBySocket(socket.id);
|
||||
if (!room || !room.gameState || !room.gameState.players?.length) return;
|
||||
|
||||
Reference in New Issue
Block a user