diff --git a/cards.js b/cards.js index 2e9bd58..389b557 100644 --- a/cards.js +++ b/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: 'Ярость мандалорца', diff --git a/public/game.js b/public/game.js index 4b939d1..2e30b1d 100644 --- a/public/game.js +++ b/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 ` -
+
${escapeHtml(name)}
❤ ${p.health} 🔵 ${p.mana}/${p.maxMana} + ${p.deck && p.deck.length > 0 ? `📚 ${p.deck.length}` : ''}
${heroDrop}${heroBar}${minions.join('')}
`; @@ -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: '' }; @@ -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'); if (heroAbilityBtn) heroAbilityBtn.onclick = function () { @@ -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' ? `
${attack}${health}
` : ''; + const costDisplay = `
${cost}
`; + const art = getCardArt ? getCardArt(meta) : '✦'; + + return `
+
+
${art}
+
+
${escapeHtml(meta.name)}
+ ${stats} + ${costDisplay} +
+
+
`; + }).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 `
${getCardArt(meta)}
${escapeHtml(meta.name)}
`; + }).join('') : '
Выберите до 2 карт
'; + + 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 `
+
${escapeHtml(enemy.name || `Игрок ${enemyIdx + 1}`)}
+
Колода: ${enemy.deck.length} карт
+
`; + }).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]; diff --git a/public/index.html b/public/index.html index 18df195..ad5881b 100644 --- a/public/index.html +++ b/public/index.html @@ -263,6 +263,19 @@
+