diff --git a/public/game.js b/public/game.js index 6acf712..87b2a1a 100644 --- a/public/game.js +++ b/public/game.js @@ -179,6 +179,20 @@ if (data.warn && typeof window.Sounds !== 'undefined') window.Sounds.timerWarning(); if (data.ended && typeof window.Sounds !== 'undefined') window.Sounds.timerEnd(); }); + socket.on('chatMessage', (data) => { + const chatMessagesEl = $('chat-messages'); + if (chatMessagesEl && data) { + const time = new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }); + const messageEl = document.createElement('div'); + messageEl.className = 'chat-message user'; + messageEl.innerHTML = '' + escapeHtml(data.playerName || 'Игрок') + ': ' + + escapeHtml(data.message || '') + + ' ' + escapeHtml(time) + ''; + chatMessagesEl.appendChild(messageEl); + chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight; + } + }); + socket.on('gameState', (state) => { if (combatTimeout) { clearTimeout(combatTimeout); @@ -192,6 +206,19 @@ } prevGameState = gameState && state.phase !== 'ended' ? { ...gameState, players: gameState.players.map((p) => ({ ...p, board: (p.board || []).map((m) => ({ ...m })) })) } : null; showScreen('game'); + + // Инициализация чата при первом запуске игры + if (wasLobby) { + const chatMessagesEl = $('chat-messages'); + if (chatMessagesEl) { + chatMessagesEl.innerHTML = ''; + const welcomeMsg = document.createElement('div'); + welcomeMsg.className = 'chat-message system'; + welcomeMsg.textContent = 'Игра началась!'; + chatMessagesEl.appendChild(welcomeMsg); + } + } + gameState = state; cardDb = state.cardDb || {}; yourIndex = state.yourIndex ?? 0; @@ -673,30 +700,53 @@ } $('your-deck').textContent = state.yourDeckCount ?? you.deck?.length ?? 0; const lastLog = state.log && state.log.length ? state.log[state.log.length - 1] : null; - const logEl = $('game-log'); - if (logEl) { - function logLine(entry) { - if (!entry) return ''; - var who = function (i) { return state.players[i]?.name || ('Игрок ' + (i + 1)); }; - if (entry.type === 'play') { - var c = cardDb[entry.cardId]; - return who(entry.playerIndex) + ' разыграл ' + (c?.name || entry.cardId); - } - if (entry.type === 'turn') return 'Ход: ' + who(entry.to); - if (entry.type === 'skipTurn') return who(entry.playerIndex) + ' пропускает ход'; - if (entry.type === 'playerDefeated') return who(entry.playerIndex) + ' выбыл из игры'; - if (entry.type === 'attackHero') return 'Атака по герою!'; - if (entry.type === 'attack') return 'Бой!'; - if (entry.type === 'draw') return who(entry.playerIndex) + ' вытянул карту'; - if (entry.type === 'fatigue') return who(entry.playerIndex) + ' усталость (' + (entry.damage || 0) + ' урона)'; - if (entry.type === 'spell') return who(entry.fromPlayer) + ' заклинание'; - if (entry.type === 'heroAbility') return who(entry.fromPlayer) + ' — геройская способность'; - if (entry.type === 'battlecry' || entry.type === 'deathrattle') return ''; - return ''; + + // Добавляем новые логи в чат + if (state.log && prevGameState) { + const chatMessagesEl = $('chat-messages'); + if (chatMessagesEl) { + const prevLogLength = prevGameState.log ? prevGameState.log.length : 0; + const newLogs = state.log.slice(prevLogLength); + newLogs.forEach(entry => { + if (!entry) return; + const who = (i) => state.players[i]?.name || ('Игрок ' + (i + 1)); + let message = ''; + + if (entry.type === 'play') { + const c = cardDb[entry.cardId]; + message = who(entry.playerIndex) + ' разыграл ' + (c?.name || entry.cardId); + } else if (entry.type === 'turn') { + message = 'Ход: ' + who(entry.to); + } else if (entry.type === 'skipTurn') { + message = who(entry.playerIndex) + ' пропускает ход'; + } else if (entry.type === 'playerDefeated') { + message = who(entry.playerIndex) + ' выбыл из игры'; + } else if (entry.type === 'attackHero') { + message = 'Атака по герою!'; + } else if (entry.type === 'attack') { + message = 'Бой!'; + } else if (entry.type === 'draw') { + message = who(entry.playerIndex) + ' вытянул карту'; + } else if (entry.type === 'fatigue') { + message = who(entry.playerIndex) + ' усталость (' + (entry.damage || 0) + ' урона)'; + } else if (entry.type === 'spell') { + message = who(entry.fromPlayer) + ' заклинание'; + } else if (entry.type === 'heroAbility') { + message = who(entry.fromPlayer) + ' — геройская способность'; + } else if (entry.type === 'battlecry' || entry.type === 'deathrattle') { + return; // Пропускаем эти типы + } + + if (message) { + const time = new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }); + const messageEl = document.createElement('div'); + messageEl.className = 'chat-message system'; + messageEl.innerHTML = '' + escapeHtml(time) + ' ' + escapeHtml(message); + chatMessagesEl.appendChild(messageEl); + chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight; + } + }); } - var lines = (state.log || []).slice(-10).map(logLine).filter(Boolean); - logEl.innerHTML = lines.map(function (s) { return '
' + escapeHtml(s) + '
'; }).join(''); - logEl.scrollTop = logEl.scrollHeight; } if (lastLog?.type === 'attackHero' && typeof window.Sounds !== 'undefined') window.Sounds.attack(); @@ -747,10 +797,18 @@ var meta = cardDb[cid]; var cost = meta?.cost ?? 0; var playable = false; - if (meta?.type === 'minion') playable = you.mana >= cost && (you.board?.length ?? 0) < 7; - else if (meta?.type === 'spell') playable = you.mana >= cost && !attackMode.active && !spellMode.active && !heroAbilityMode.active; + var notEnoughMana = false; + if (isYourTurn) { + if (meta?.type === 'minion') { + playable = you.mana >= cost && (you.board?.length ?? 0) < 7; + notEnoughMana = you.mana < cost; + } else if (meta?.type === 'spell') { + playable = you.mana >= cost && !attackMode.active && !spellMode.active && !heroAbilityMode.active; + notEnoughMana = you.mana < cost; + } + } var drawAnim = justDrew && i === hand.length - 1; - return renderHandCard(cid, i, hand.length, playable, drawAnim); + return renderHandCard(cid, i, hand.length, playable, drawAnim, notEnoughMana); }) .join(''); } @@ -789,7 +847,7 @@ return '
' + tags.join(' ') + '
'; } - function renderHandCard(cardId, handIndex, handSize, playable, drawFromDeck) { + function renderHandCard(cardId, handIndex, handSize, playable, drawFromDeck, notEnoughMana) { var meta = cardDb[cardId]; if (!meta) return ''; var name = meta.name || cardId; @@ -808,7 +866,8 @@ var textHtml = meta.text ? '
' + escapeHtml(meta.text) + '
' : ''; var abilHtml = abilityTags(meta); var fictionalClass = meta.fictional ? ' fictional-card' : ''; - return '
' + var notEnoughManaClass = notEnoughMana ? ' not-enough-mana' : ''; + return '
' + '
' + '
' + art + '
' + '
' @@ -978,6 +1037,54 @@ const isYourTurn = state.currentPlayerIndex === state.yourIndex; const sidebar = $('forge-sidebar'); const isForgeOpen = sidebar && !sidebar.classList.contains('hidden'); + + // Chat handlers + const chatContainer = $('chat-container'); + const chatToggle = $('btn-chat-toggle'); + const chatHeader = $('chat-header'); + const chatInput = $('chat-input'); + const chatSendBtn = $('btn-chat-send'); + + if (chatToggle) { + chatToggle.onclick = function() { + if (chatContainer) { + chatContainer.classList.toggle('collapsed'); + chatToggle.textContent = chatContainer.classList.contains('collapsed') ? '▲' : '▼'; + } + }; + } + + if (chatHeader) { + chatHeader.onclick = function() { + if (chatContainer) { + chatContainer.classList.toggle('collapsed'); + if (chatToggle) { + chatToggle.textContent = chatContainer.classList.contains('collapsed') ? '▲' : '▼'; + } + } + }; + } + + function sendChatMessage() { + if (!chatInput || !socket) return; + const message = chatInput.value.trim(); + if (message.length === 0) return; + + socket.emit('chatMessage', { message: message }); + chatInput.value = ''; + } + + if (chatSendBtn) { + chatSendBtn.onclick = sendChatMessage; + } + + if (chatInput) { + chatInput.onkeypress = function(e) { + if (e.key === 'Enter') { + sendChatMessage(); + } + }; + } $all('.card-wrap.in-hand').forEach((wrap) => { wrap.classList.toggle('disabled', !isYourTurn); @@ -1549,8 +1656,11 @@ const sidebar = $('forge-sidebar'); if (sidebar) { sidebar.classList.remove('hidden'); + forgeSelected = []; // Сбрасываем выбор при открытии + renderForgeHand(state); renderForgeDeck(state); - setupForgeDragAndDrop(state); + renderForgeSelected(); + updateForgeCraftButton(); } }; } else if (forgeBtn) { @@ -1728,9 +1838,12 @@ yourBoardEl.ondrop = null; } - // Настраиваем drag-and-drop для кузницы, если она открыта + // Обновляем кузницу, если она открыта if (isForgeOpen) { - setTimeout(() => setupForgeDragAndDrop(state), 100); + renderForgeHand(state); + renderForgeDeck(state); + renderForgeSelected(); + updateForgeCraftButton(); } } @@ -2050,10 +2163,16 @@ sidebar.classList.add('hidden'); forgeSelected = []; renderForgeSelected(); - // Обновляем отображение руки и колоды - if (gameState) { - renderHand(gameState); - } + } + }); + + $('btn-forge-clear')?.addEventListener('click', () => { + forgeSelected = []; + renderForgeSelected(); + updateForgeCraftButton(); + if (gameState) { + renderForgeHand(gameState); + renderForgeDeck(gameState); } }); @@ -2211,20 +2330,24 @@ function renderForgeSelected() { const selectedEl = $('forge-selected'); + const countEl = $('forge-selected-count'); if (!selectedEl) return; + if (countEl) { + countEl.textContent = forgeSelected.length; + } + if (forgeSelected.length === 0) { - selectedEl.innerHTML = '
Перетащите карты сюда
'; - selectedEl.classList.remove('drag-over'); + selectedEl.innerHTML = '
Кликните на карты чтобы выбрать
'; } else { selectedEl.innerHTML = forgeSelected.map((cardId, idx) => { const meta = cardDb[cardId]; if (!meta) return ''; - return `
+ return `
${getCardArt(meta)}
-
${escapeHtml(meta.name)}
+
${escapeHtml(meta.name)}
${meta.type === 'minion' ? `
${meta.attack || 0}${meta.health || 0}
` : ''}
@@ -2236,17 +2359,7 @@ card.onclick = function(e) { e.stopPropagation(); const cardId = card.dataset.cardId; - const idx = forgeSelected.indexOf(cardId); - if (idx >= 0) { - forgeSelected.splice(idx, 1); - renderForgeSelected(); - updateForgeCraftButton(); - // Обновляем отображение в источнике - if (gameState) { - renderForgeDeck(gameState); - renderHand(gameState); - } - } + removeCardFromForge(cardId); }; }); } @@ -2256,13 +2369,18 @@ const craftBtn = $('btn-forge-craft'); const you = gameState?.players?.[gameState?.yourIndex]; if (craftBtn && you) { - craftBtn.disabled = forgeSelected.length < 2 || forgeSelected.length > 3 || (you.mana || 0) < 2; + const canCraft = forgeSelected.length >= 2 && forgeSelected.length <= 3 && (you.mana || 0) >= 2; + craftBtn.disabled = !canCraft; } } function addCardToForge(cardId, source) { if (!cardId) return; - if (forgeSelected.includes(cardId)) return; // Уже добавлена + if (forgeSelected.includes(cardId)) { + // Если карта уже выбрана, убираем её + removeCardFromForge(cardId); + return; + } if (forgeSelected.length >= 3) { // Показываем сообщение, что максимум 3 карты const selectedEl = $('forge-selected'); @@ -2270,7 +2388,7 @@ selectedEl.style.animation = 'shake 0.5s'; setTimeout(() => selectedEl.style.animation = '', 500); } - return; // Максимум 3 карты + return; } const meta = cardDb[cardId]; @@ -2280,272 +2398,59 @@ renderForgeSelected(); updateForgeCraftButton(); - // Обновляем отображение в источнике (рука или колода) + // Обновляем отображение в источниках if (gameState) { - if (source === 'hand') { - renderHand(gameState); - } else if (source === 'deck') { + renderForgeHand(gameState); + renderForgeDeck(gameState); + } + } + + function removeCardFromForge(cardId) { + const idx = forgeSelected.indexOf(cardId); + if (idx >= 0) { + forgeSelected.splice(idx, 1); + renderForgeSelected(); + updateForgeCraftButton(); + // Обновляем отображение в источниках + if (gameState) { + renderForgeHand(gameState); renderForgeDeck(gameState); } } - - // Настраиваем drag-and-drop заново после добавления - setTimeout(() => setupForgeDragAndDrop(gameState), 100); } - function setupForgeDragAndDrop(state) { - const selectedEl = $('forge-selected'); - const sidebar = $('forge-sidebar'); - if (!selectedEl || !sidebar || sidebar.classList.contains('hidden')) return; + function renderForgeHand(state) { + const you = state.players[state.yourIndex]; + const handList = $('forge-hand-list'); + if (!handList) return; - // Обработка drop в область выбранных карт - const handleDragOver = (e) => { - e.preventDefault(); - e.stopPropagation(); - selectedEl.classList.add('drag-over'); - }; + const hand = you.hand || state.yourHand || []; - const handleDragLeave = (e) => { - if (!selectedEl.contains(e.relatedTarget)) { - selectedEl.classList.remove('drag-over'); - } - }; - - const handleDrop = (e) => { - e.preventDefault(); - e.stopPropagation(); - selectedEl.classList.remove('drag-over'); - - let cardId = null; - let source = null; - - // Проверяем данные из dataTransfer - const data = e.dataTransfer.getData('text/plain'); - if (data) { - if (data.startsWith('hand:')) { - const handIndex = parseInt(data.split(':')[1], 10); - const you = state.players[state.yourIndex]; - if (you.hand && you.hand[handIndex]) { - cardId = you.hand[handIndex]; - source = 'hand'; - } - } else { - cardId = data; - source = e.dataTransfer.getData('source') || 'deck'; - } - } - - if (cardId) { - addCardToForge(cardId, source); - } - }; - - // Удаляем старые обработчики, если есть - selectedEl.removeEventListener('dragover', handleDragOver); - selectedEl.removeEventListener('dragleave', handleDragLeave); - selectedEl.removeEventListener('drop', handleDrop); - - // Добавляем новые обработчики - selectedEl.addEventListener('dragover', handleDragOver); - selectedEl.addEventListener('dragleave', handleDragLeave); - selectedEl.addEventListener('drop', handleDrop); - - // Поддержка touch для мобильных устройств - if (isTouchDevice()) { - let touchStartCard = null; - let touchStartSource = null; - let touchStartElement = null; - - // Обработка touch для карт в руке - упрощённая версия: просто тап добавляет карту - $all('#your-hand .card-wrap').forEach(cardWrap => { - const cardId = cardWrap.dataset.cardId; - const meta = cardDb[cardId]; - if (cardId && meta && meta.type === 'minion') { - // Удаляем старые обработчики - cardWrap.ontouchstart = null; - cardWrap.ontouchend = null; - - cardWrap.ontouchstart = function(e) { - if (sidebar && !sidebar.classList.contains('hidden')) { - touchStartCard = cardId; - touchStartSource = 'hand'; - touchStartElement = cardWrap; - cardWrap._touchStartTime = Date.now(); - cardWrap.classList.add('touch-active'); - e.preventDefault(); - } - }; - - cardWrap.ontouchend = function(e) { - if (touchStartCard === cardId && sidebar && !sidebar.classList.contains('hidden')) { - const touch = e.changedTouches[0]; - const timeDiff = Date.now() - (touchStartElement?._touchStartTime || Date.now()); - - // Если это быстрый тап (менее 300мс), просто добавляем карту - if (timeDiff < 300) { - addCardToForge(cardId, 'hand'); - } else { - // Иначе проверяем, куда перетащили - const dropEl = document.elementFromPoint(touch.clientX, touch.clientY); - if (dropEl && (dropEl === selectedEl || selectedEl.contains(dropEl) || - dropEl.closest('#forge-selected'))) { - addCardToForge(cardId, 'hand'); - } - } - - cardWrap.classList.remove('touch-active'); - touchStartCard = null; - touchStartSource = null; - touchStartElement = null; - e.preventDefault(); - } - }; - - cardWrap.ontouchcancel = function() { - if (touchStartCard === cardId) { - cardWrap.classList.remove('touch-active'); - touchStartCard = null; - touchStartSource = null; - touchStartElement = null; - } - }; - } - }); - - // Обработка touch для карт в колоде - упрощённая версия - $all('.forge-deck-card').forEach(cardWrap => { - const cardId = cardWrap.dataset.cardId; - if (cardId) { - // Удаляем старые обработчики - cardWrap.ontouchstart = null; - cardWrap.ontouchend = null; - - cardWrap.ontouchstart = function(e) { - touchStartCard = cardId; - touchStartSource = 'deck'; - touchStartElement = cardWrap; - cardWrap._touchStartTime = Date.now(); - cardWrap.classList.add('touch-active'); - e.preventDefault(); - }; - - cardWrap.ontouchend = function(e) { - if (touchStartCard === cardId) { - const touch = e.changedTouches[0]; - const timeDiff = Date.now() - (touchStartElement?._touchStartTime || Date.now()); - - // Если это быстрый тап (менее 300мс), просто добавляем карту - if (timeDiff < 300) { - addCardToForge(cardId, 'deck'); - } else { - // Иначе проверяем, куда перетащили - const dropEl = document.elementFromPoint(touch.clientX, touch.clientY); - if (dropEl && (dropEl === selectedEl || selectedEl.contains(dropEl) || - dropEl.closest('#forge-selected'))) { - addCardToForge(cardId, 'deck'); - } - } - - cardWrap.classList.remove('touch-active'); - touchStartCard = null; - touchStartSource = null; - touchStartElement = null; - e.preventDefault(); - } - }; - - cardWrap.ontouchcancel = function() { - if (touchStartCard === cardId) { - cardWrap.classList.remove('touch-active'); - touchStartCard = null; - touchStartSource = null; - touchStartElement = null; - } - }; - } - }); - - // Также добавляем обработчик на саму область выбранных карт для визуальной обратной связи - selectedEl.ontouchstart = function(e) { - selectedEl.classList.add('drag-over'); - }; - - selectedEl.ontouchend = function(e) { - selectedEl.classList.remove('drag-over'); - }; + if (!hand || hand.length === 0) { + handList.innerHTML = '
Рука пуста
'; + return; } - // Делаем карты в руке перетаскиваемыми в кузницу - $all('#your-hand .card-wrap').forEach(cardWrap => { - const cardId = cardWrap.dataset.cardId; + // Фильтруем только миньонов + const minionCards = hand.filter(cardId => { const meta = cardDb[cardId]; - if (cardId && meta && meta.type === 'minion') { - // Сохраняем оригинальный draggable, если он был - const originalDraggable = cardWrap.draggable; - const originalOndragstart = cardWrap.ondragstart; - - // Добавляем дополнительный обработчик для кузницы - const forgeDragStart = (e) => { - // Проверяем, открыта ли кузница - if (sidebar && !sidebar.classList.contains('hidden')) { - e.dataTransfer.setData('text/plain', cardId); - e.dataTransfer.setData('source', 'hand'); - cardWrap.classList.add('dragging'); - } - }; - - cardWrap.addEventListener('dragstart', forgeDragStart); - - // Сохраняем ссылку для последующего удаления - cardWrap._forgeDragStart = forgeDragStart; - } + return meta && meta.type === 'minion'; }); - // Делаем карты в колоде перетаскиваемыми - $all('.forge-deck-card').forEach(cardWrap => { - const cardId = cardWrap.dataset.cardId; - if (cardId) { - cardWrap.draggable = true; - - const deckDragStart = (e) => { - e.dataTransfer.setData('text/plain', cardId); - e.dataTransfer.setData('source', 'deck'); - cardWrap.classList.add('dragging'); - }; - - const deckDragEnd = () => { - cardWrap.classList.remove('dragging'); - }; - - // Удаляем старые обработчики - cardWrap.removeEventListener('dragstart', cardWrap._deckDragStart); - cardWrap.removeEventListener('dragend', cardWrap._deckDragEnd); - - // Добавляем новые - cardWrap.addEventListener('dragstart', deckDragStart); - cardWrap.addEventListener('dragend', deckDragEnd); - - // Сохраняем ссылки - cardWrap._deckDragStart = deckDragStart; - cardWrap._deckDragEnd = deckDragEnd; - } - }); - } - - function renderForgeDeck(state) { - const you = state.players[state.yourIndex]; - const deckList = $('forge-deck-list'); - if (!deckList) return; + if (minionCards.length === 0) { + handList.innerHTML = '
Нет миньонов в руке
'; + return; + } - deckList.innerHTML = you.deck.map((cardId, idx) => { + handList.innerHTML = minionCards.map((cardId) => { const meta = cardDb[cardId]; - if (!meta || meta.type !== 'minion') return ''; + if (!meta) return ''; const isSelected = forgeSelected.includes(cardId); - return `
+ return `
${getCardArt(meta)}
-
${escapeHtml(meta.name)}
+
${escapeHtml(meta.name)}
${meta.attack || 0} ${meta.health || 0} @@ -2555,22 +2460,67 @@
`; }).filter(Boolean).join(''); - // Клик по карте в колоде тоже добавляет её (для десктопа) - // На мобильных это обрабатывается через touch события - if (!isTouchDevice()) { - $all('.forge-deck-card').forEach(card => { - card.onclick = function() { - const cardId = card.dataset.cardId; - addCardToForge(cardId, 'deck'); - }; - }); + // Обработчики клика для карт из руки + $all('#forge-hand-list .card-wrap').forEach(card => { + card.onclick = function(e) { + e.stopPropagation(); + const cardId = card.dataset.cardId; + if (cardId) { + addCardToForge(cardId, 'hand'); + } + }; + }); + } + + function renderForgeDeck(state) { + const deck = state.yourDeck || []; + const deckList = $('forge-deck-list'); + if (!deckList) return; + + if (!deck || deck.length === 0) { + deckList.innerHTML = '
Колода пуста
'; + return; } - renderForgeSelected(); - updateForgeCraftButton(); + // Фильтруем только миньонов + const minionCards = deck.filter(cardId => { + const meta = cardDb[cardId]; + return meta && meta.type === 'minion'; + }); - // Настраиваем drag-and-drop после рендеринга - setTimeout(() => setupForgeDragAndDrop(state), 100); + if (minionCards.length === 0) { + deckList.innerHTML = '
Нет миньонов в колоде
'; + return; + } + + deckList.innerHTML = minionCards.map((cardId) => { + const meta = cardDb[cardId]; + if (!meta) return ''; + const isSelected = forgeSelected.includes(cardId); + return `
+
+
${getCardArt(meta)}
+
+
${escapeHtml(meta.name)}
+
+ ${meta.attack || 0} + ${meta.health || 0} +
+
+
+
`; + }).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 при каждом обновлении руки, если кузница открыта @@ -2587,6 +2537,7 @@ socket.emit('forgeCard', { cardIds: forgeSelected }); forgeSelected = []; renderForgeSelected(); + updateForgeCraftButton(); const sidebar = $('forge-sidebar'); if (sidebar) { sidebar.classList.add('hidden'); diff --git a/public/index.html b/public/index.html index 3c6caad..660f480 100644 --- a/public/index.html +++ b/public/index.html @@ -198,14 +198,26 @@
-
-
-
+ + +
+
+ 💬 Чат + +
+
+
+
+ + +
+
+
@@ -257,16 +269,34 @@

⚒ Звёздная кузница

-

Перетащите 2-3 карты из руки или колоды сюда

-
-
Перетащите карты сюда
-
-
-

Карты из колоды:

-
+ +
+
+ +
+

📋 Из руки

+
+
+ + +
+

✨ Выбрано (0/3)

+
+
Кликните на карты чтобы выбрать
+
+
+ + +
+

📚 Из колоды

+
+
+
+
- + +