This commit is contained in:
2026-01-26 15:39:13 +03:00
parent a3b270fc62
commit f99a4940e2
5 changed files with 505 additions and 7 deletions

View File

@ -1012,11 +1012,11 @@ module.exports = {
cost: 3, cost: 3,
type: 'spell', type: 'spell',
faction: 'pirates', faction: 'pirates',
text: 'Возьми 2 карты.', text: 'Укради 2 карты из колоды противника.',
art: 'plunder', art: 'plunder',
spellEffect: 'draw_2', spellEffect: 'steal_cards',
spellTarget: 'none', spellTarget: 'enemy_player',
bio: 'Пираты грабят и получают добычу.', bio: 'Пираты грабят и крадут карты у противников.',
}, },
mandalorian_rage: { mandalorian_rage: {
name: 'Ярость мандалорца', name: 'Ярость мандалорца',

View File

@ -13,6 +13,7 @@
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 };
let stealCardsMode = { active: false, handIndex: -1, targetPlayerIndex: null, targetDeck: [], selectedIndices: [] };
const seenMinions = new Set(); const seenMinions = new Set();
let lastHandLength = 0; let lastHandLength = 0;
let prevGameState = null; let prevGameState = null;
@ -200,6 +201,40 @@
} }
renderGame(state); 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; return socket;
} }
@ -253,12 +288,14 @@
const minions = (p.board || []).map((m, j) => renderBoardMinion(m, i, j, state, true, false)); const minions = (p.board || []).map((m, j) => renderBoardMinion(m, i, j, state, true, false));
const heroBar = renderHeroTarget(i, state); const heroBar = renderHeroTarget(i, state);
const heroDrop = renderHeroDropZone(i, state); const heroDrop = renderHeroDropZone(i, state);
const canSteal = state.currentPlayerIndex === state.yourIndex && spellMode.active && spellMode.cardId && cardDb[spellMode.cardId]?.spellEffect === 'steal_cards';
return ` 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-name">${escapeHtml(name)}</div>
<div class="opponent-stats"> <div class="opponent-stats">
<span>❤ ${p.health}</span> <span>❤ ${p.health}</span>
<span>🔵 ${p.mana}/${p.maxMana}</span> <span>🔵 ${p.mana}/${p.maxMana}</span>
${p.deck && p.deck.length > 0 ? `<span>📚 ${p.deck.length}</span>` : ''}
</div> </div>
<div class="opponent-board">${heroDrop}${heroBar}${minions.join('')}</div> <div class="opponent-board">${heroDrop}${heroBar}${minions.join('')}</div>
</div>`; </div>`;
@ -888,14 +925,32 @@
return; return;
} }
if (meta.type === 'spell') { 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'; 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 (!needTarget) {
if (typeof window.Sounds !== 'undefined') window.Sounds.playCard(); if (typeof window.Sounds !== 'undefined') window.Sounds.playCard();
socket.emit('playSpell', { handIndex: handIndex }); socket.emit('playSpell', { handIndex: handIndex });
return; return;
} }
spellMode = { active: true, handIndex: handIndex, cardId: wrap.dataset.cardId, spellTarget: meta.spellTarget }; 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'); $('spell-mode')?.classList.remove('hidden');
renderGame(state); renderGame(state);
return; return;
@ -1184,6 +1239,34 @@
e.stopPropagation(); e.stopPropagation();
var tp = parseInt(el.dataset.dropPlayer ?? el.dataset.playerIndex, 10); var tp = parseInt(el.dataset.dropPlayer ?? el.dataset.playerIndex, 10);
var tb = parseInt(el.dataset.dropBoard ?? el.dataset.boardIndex, 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(); if (typeof window.Sounds !== 'undefined') window.Sounds.playCard();
socket.emit('playSpell', { handIndex: spellMode.handIndex, targetPlayerIndex: tp, targetBoardIndex: tb }); socket.emit('playSpell', { handIndex: spellMode.handIndex, targetPlayerIndex: tp, targetBoardIndex: tb });
spellMode = { active: false, handIndex: -1, cardId: '', spellTarget: '' }; spellMode = { active: false, handIndex: -1, cardId: '', spellTarget: '' };
@ -1199,6 +1282,33 @@
e.stopPropagation(); e.stopPropagation();
var tp = parseInt(el.dataset.dropPlayer ?? el.dataset.playerIndex, 10); var tp = parseInt(el.dataset.dropPlayer ?? el.dataset.playerIndex, 10);
var tb = parseInt(el.dataset.dropBoard ?? el.dataset.boardIndex, 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(); if (typeof window.Sounds !== 'undefined') window.Sounds.playCard();
socket.emit('playSpell', { handIndex: spellMode.handIndex, targetPlayerIndex: tp, targetBoardIndex: tb }); socket.emit('playSpell', { handIndex: spellMode.handIndex, targetPlayerIndex: tp, targetBoardIndex: tb });
spellMode = { active: false, handIndex: -1, cardId: '', spellTarget: '' }; spellMode = { active: false, handIndex: -1, cardId: '', spellTarget: '' };
@ -1237,6 +1347,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'); var heroAbilityBtn = $('btn-hero-ability');
if (heroAbilityBtn) heroAbilityBtn.onclick = function () { if (heroAbilityBtn) heroAbilityBtn.onclick = function () {
@ -1263,9 +1432,11 @@
spellMode = { active: false, handIndex: -1, cardId: '', spellTarget: '' }; spellMode = { active: false, handIndex: -1, cardId: '', spellTarget: '' };
attackMode = { active: false, attackerPlayer: -1, attackerBoard: -1 }; attackMode = { active: false, attackerPlayer: -1, attackerBoard: -1 };
heroAbilityMode = { active: false }; heroAbilityMode = { active: false };
stealCardsMode = { active: false, handIndex: -1, targetPlayerIndex: null, targetDeck: [], selectedIndices: [] };
$('spell-mode')?.classList.add('hidden'); $('spell-mode')?.classList.add('hidden');
$('attack-mode')?.classList.add('hidden'); $('attack-mode')?.classList.add('hidden');
$('hero-ability-mode')?.classList.add('hidden'); $('hero-ability-mode')?.classList.add('hidden');
$('steal-cards-overlay')?.classList.add('hidden');
renderGame(state); renderGame(state);
}; };
@ -1282,9 +1453,11 @@
attackMode = { active: false, attackerPlayer: -1, attackerBoard: -1 }; attackMode = { active: false, attackerPlayer: -1, attackerBoard: -1 };
spellMode = { active: false, handIndex: -1, cardId: '', spellTarget: '' }; spellMode = { active: false, handIndex: -1, cardId: '', spellTarget: '' };
heroAbilityMode = { active: false }; heroAbilityMode = { active: false };
stealCardsMode = { active: false, handIndex: -1, targetPlayerIndex: null, targetDeck: [], selectedIndices: [] };
$('attack-mode')?.classList.add('hidden'); $('attack-mode')?.classList.add('hidden');
$('spell-mode')?.classList.add('hidden'); $('spell-mode')?.classList.add('hidden');
$('hero-ability-mode')?.classList.add('hidden'); $('hero-ability-mode')?.classList.add('hidden');
$('steal-cards-overlay')?.classList.add('hidden');
renderGame(state); 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 = []; let forgeSelected = [];
function renderForgeDeck(state) { function renderForgeDeck(state) {
const you = state.players[state.yourIndex]; const you = state.players[state.yourIndex];

View File

@ -263,6 +263,19 @@
<button type="button" id="btn-card-info-close" class="btn btn-primary">Закрыть</button> <button type="button" id="btn-card-info-close" class="btn btn-primary">Закрыть</button>
</div> </div>
</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 id="settings-overlay" class="modal-overlay hidden">
<div class="modal settings-modal"> <div class="modal settings-modal">
<h2>Настройки</h2> <h2>Настройки</h2>

View File

@ -504,6 +504,21 @@ html, body {
border-color: rgba(255,180,0,0.6); border-color: rgba(255,180,0,0.6);
box-shadow: 0 0 24px rgba(255,180,0,0.2); 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-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-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; } .opponent-board { display: flex; flex-wrap: wrap; gap: 0.5rem; min-height: 80px; }
@ -1435,6 +1450,67 @@ html, body {
white-space: nowrap; white-space: nowrap;
} }
.settings-modal { max-width: 400px; text-align: left; } .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; } .settings-content { margin: 1.5rem 0; }
.setting-group { .setting-group {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;

View File

@ -777,8 +777,29 @@ function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardInde
const eff = card.spellEffect; const eff = card.spellEffect;
const needTarget = card.spellTarget && card.spellTarget !== 'none'; const needTarget = card.spellTarget && card.spellTarget !== 'none';
// Специальная обработка для 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; // Не тратим ману и не удаляем карту пока - это сделаем после выбора
}
if (needTarget && (targetPlayerIndex == null || targetBoardIndex == null)) 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); const enemies = gameState.players.filter((pl, i) => i !== pi && pl.health > 0);
if (eff === 'deal_2') { if (eff === 'deal_2') {
@ -927,6 +948,65 @@ function playSpell(room, socketId, handIndex, targetPlayerIndex, targetBoardInde
broadcastGameState(room); 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) { function heroAbility(room, socketId, targetPlayerIndex, targetBoardIndex) {
const gameState = room.gameState; const gameState = room.gameState;
const pi = findPlayerIndex(room, socketId); const pi = findPlayerIndex(room, socketId);
@ -1165,6 +1245,12 @@ io.on('connection', (socket) => {
forgeCard(room, socket.id, data.cardIds); 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', () => { socket.on('resetToLobby', () => {
const room = getRoomBySocket(socket.id); const room = getRoomBySocket(socket.id);
if (!room || !room.gameState || !room.gameState.players?.length) return; if (!room || !room.gameState || !room.gameState.players?.length) return;