/**
* Star Wars Hearthstone - Client
* PvP over LAN (Radmin VPN), 2-4 players
*/
(function () {
'use strict';
let socket = null;
let gameState = null;
let cardDb = {};
let yourIndex = -1;
let forgePreviewRequestId = 0;
let forgePreviewPendingIds = null;
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: [] };
let battlecryTargetMode = { active: false, battlecryId: '', cardId: '', minionId: '', targetPlayerIndex: null, targetBoardIndex: null };
const seenMinions = new Set();
let lastHandLength = 0;
let prevGameState = null;
let combatTimeout = null;
const DEFAULT_PORT = 3542;
// Touch support variables
let touchState = {
startX: 0,
startY: 0,
startTime: 0,
target: null,
isSwipe: false,
minSwipeDistance: 50,
maxSwipeTime: 500,
longPressTimer: null,
longPressDelay: 500
};
const $ = (id) => document.getElementById(id);
const $all = (sel) => document.querySelectorAll(sel);
// Helper to detect if device is touch-enabled
const isTouchDevice = () => {
return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
};
// Helper to get touch coordinates
const getTouchCoords = (e) => {
if (e.touches && e.touches.length > 0) {
return { x: e.touches[0].clientX, y: e.touches[0].clientY };
}
return { x: e.clientX, y: e.clientY };
};
// Helper to find element at coordinates
const elementFromPoint = (x, y) => {
const el = document.elementFromPoint(x, y);
if (!el) return null;
return el.closest('.card-wrap, .drop-target, .hero-target, .spell-target, .hero-ability-target');
};
function spawnEffect(type, x, y) {
const layer = $('effects-layer');
if (!layer) return;
const el = document.createElement('div');
el.className = 'effect effect-' + type;
el.style.left = (typeof x === 'number' ? x : 0) + 'px';
el.style.top = (typeof y === 'number' ? y : 0) + 'px';
layer.appendChild(el);
setTimeout(function () { el.remove(); }, 800);
}
function showScreen(id) {
$all('.screen').forEach((s) => s.classList.add('hidden'));
const el = $(id);
if (el) el.classList.remove('hidden');
}
function showError(msg) {
const el = $('lobby-error');
if (!el) return;
el.textContent = msg;
el.classList.remove('hidden');
}
function clearError() {
const el = $('lobby-error');
if (el) el.classList.add('hidden');
}
function setLobbyPanel(name) {
$all('#lobby .panel').forEach((p) => {
p.classList.remove('active', 'hidden');
if (p.id !== name + '-panel') p.classList.add('hidden');
else p.classList.add('active');
});
}
function renderPlayerList(listId, players) {
const ul = $(listId);
if (!ul) return;
ul.innerHTML = (players || []).map((p) => `
${escapeHtml(p.name)}`).join('');
}
function escapeHtml(s) {
const div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
}
function connect(url) {
if (socket) {
socket.disconnect();
socket.removeAllListeners();
}
socket = io(url, { transports: ['websocket', 'polling'], reconnection: false });
socket.on('connect', () => {
clearError();
});
socket.on('connect_error', (e) => {
const errorMsg = e.message || 'Не удалось подключиться к серверу';
showError(errorMsg + '. Проверьте, что сервер запущен и используйте расширенные настройки для указания IP вручную.');
});
socket.on('error', (msg) => showError(msg));
socket.on('roomCreated', (data) => {
const codeEl = $('room-code-display');
if (codeEl) {
codeEl.textContent = data.code;
codeEl.onclick = function () {
navigator.clipboard.writeText(data.code).then(() => {
const old = codeEl.textContent;
codeEl.textContent = 'Скопировано!';
setTimeout(() => { codeEl.textContent = old; }, 1500);
}).catch(() => {});
};
}
const connectInfoEl = $('room-connect-info');
if (connectInfoEl && data.serverIP && data.serverPort) {
const isLocal = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
const serverAddr = isLocal ? 'localhost:' + data.serverPort : data.serverIP + ':' + data.serverPort;
const fullInfo = serverAddr + ' → код: ' + data.code;
connectInfoEl.textContent = fullInfo;
connectInfoEl.onclick = function () {
navigator.clipboard.writeText(fullInfo).then(() => {
const old = connectInfoEl.textContent;
connectInfoEl.textContent = 'Скопировано!';
setTimeout(() => { connectInfoEl.textContent = old; }, 1500);
}).catch(() => {});
};
}
setLobbyPanel('room');
renderPlayerList('player-list', data.lobby);
const portEl = $('display-port');
if (portEl) portEl.textContent = data.serverPort || window.location.port || DEFAULT_PORT;
});
socket.on('roomJoined', (data) => {
setLobbyPanel('connect');
renderPlayerList('connect-player-list', data.lobby);
const statusEl = $('connect-status');
if (statusEl) statusEl.textContent = 'Подключено к комнате. Ожидание начала игры...';
});
socket.on('lobbyUpdate', (lobby) => {
renderPlayerList('player-list', lobby);
renderPlayerList('connect-player-list', lobby);
});
socket.on('roomsList', (roomsList) => {
if (typeof window.handleRoomsList === 'function') window.handleRoomsList(roomsList);
});
socket.on('joinedAsSpectator', (data) => {
if (data.gameState) {
gameState = data.gameState;
gameState.isSpectator = true;
gameState.yourIndex = -1;
showScreen('game');
renderGame(gameState);
}
});
socket.on('backToLobby', (data) => {
showScreen('lobby');
clearError();
if (data && data.lobby) {
setLobbyPanel('room');
renderPlayerList('player-list', data.lobby);
} else {
setLobbyPanel('host');
}
const hp = $('host-panel');
if (hp) hp.classList.add('active');
});
socket.on('turnTime', function (data) {
var el = $('turn-timer');
if (!el) return;
var m = Math.floor(data.left / 60);
var s = data.left % 60;
el.textContent = m + ':' + (s < 10 ? '0' : '') + s;
el.classList.toggle('timer-warn', data.left > 0 && data.left <= 15);
el.classList.toggle('timer-ended', data.ended);
if (data.warn && typeof window.Sounds !== 'undefined') window.Sounds.timerWarning();
if (data.ended && typeof window.Sounds !== 'undefined') window.Sounds.timerEnd();
});
socket.on('forgePreviewResult', (data) => {
if (!data) return;
if (data.requestId != null && data.requestId !== forgePreviewRequestId) return;
const preview = $('forge-preview');
const wrap = $('forge-preview-card-wrap');
const confirmBtn = $('btn-forge-confirm');
if (!preview) return;
if (data.error || !data.resultCardId) {
preview.classList.add('hidden');
if (wrap) wrap.innerHTML = '';
if (confirmBtn) confirmBtn.disabled = true;
forgePreviewPendingIds = null;
return;
}
const meta = (gameState?.cardDb || cardDb)[data.resultCardId];
if (!meta) {
preview.classList.add('hidden');
forgePreviewPendingIds = null;
return;
}
const you = gameState?.isSpectator ? null : (gameState?.players?.[gameState?.yourIndex] || null);
const canCraft = !gameState?.isSpectator && you && (you.mana || 0) >= 2;
if (wrap) {
const art = typeof getCardArt === 'function' ? getCardArt(meta) : '';
wrap.innerHTML = '';
}
if (confirmBtn) confirmBtn.disabled = !canCraft;
preview.classList.remove('hidden');
});
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);
combatTimeout = null;
}
const wasLobby = $('game')?.classList.contains('hidden') === true;
if (wasLobby) {
seenMinions.clear();
lastHandLength = 0;
prevGameState = null;
}
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;
// Режим наблюдателя
if (state.isSpectator) {
yourIndex = -1;
const spectatorBadge = $('spectator-badge');
if (!spectatorBadge) {
const badge = document.createElement('div');
badge.id = 'spectator-badge';
badge.style.cssText = 'position: fixed; top: 1rem; right: 1rem; background: rgba(212,168,75,0.9); color: #000; padding: 0.5rem 1rem; border-radius: 8px; font-weight: 700; z-index: 200;';
badge.textContent = '👁 НАБЛЮДАТЕЛЬ';
document.body.appendChild(badge);
}
} else {
const spectatorBadge = $('spectator-badge');
if (spectatorBadge) spectatorBadge.remove();
}
if (state.phase === 'ended') {
showGameOver(state);
renderGameEnded(state);
return;
}
renderGame(state);
});
socket.on('stealCardsRequest', (data) => {
if (!gameState) return;
// Находим handIndex заклинания "Грабеж" в текущей руке
const you = gameState.isSpectator ? null : (gameState.players[gameState.yourIndex] || null);
if (!you || !you.hand || gameState.isSpectator) 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.board) {
stealCardsMode.targetDeck = targetPlayer.board.map(m => m.cardId);
}
showStealCardsModal(gameState, data);
});
socket.on('battlecryTargetRequest', (data) => {
if (data.battlecryId === 'return_hand_enemy') {
// Показываем модальное окно для выбора противника и его миньона
showBattlecryTargetModal(gameState, data);
}
});
return socket;
}
function showBattlecryTargetModal(state, data) {
battlecryTargetMode = { active: true, battlecryId: data.battlecryId, cardId: data.cardId, minionId: data.minionId };
// Активируем режим выбора цели
spellMode = { active: true, handIndex: -1, cardId: data.cardId, spellTarget: 'enemy_minion', battlecryMode: true, minionId: data.minionId };
$('spell-mode')?.classList.remove('hidden');
const spellModeText = $('spell-mode')?.querySelector('p');
if (spellModeText) {
spellModeText.textContent = 'Выберите миньона противника для возврата в руку';
}
renderGame(state);
}
function renderGameEnded(state) {
const badge = $('turn-badge');
if (badge) badge.textContent = 'Игра окончена';
const endBtn = $('btn-end-turn');
if (endBtn) endBtn.disabled = true;
const resetBtn = $('btn-reset-lobby');
if (resetBtn) {
resetBtn.classList.toggle('hidden', state.yourIndex !== 0);
}
bindGameEvents(state);
}
function showGameOver(state) {
const winner = state.winner;
const modal = $('modal-overlay');
const title = $('modal-title');
const body = $('modal-body');
const newGameBtn = $('btn-new-game');
if (!modal || !title || !body) return;
if (winner && winner.id === socket?.id) {
title.textContent = 'Победа!';
title.style.color = 'var(--amber)';
body.textContent = 'Светлая сторона Силы с тобой.';
if (typeof window.Sounds !== 'undefined') window.Sounds.victory();
} else if (winner) {
title.textContent = 'Поражение';
title.style.color = 'var(--red)';
body.textContent = 'Победу одержал ' + (winner.name || 'противник') + '.';
if (typeof window.Sounds !== 'undefined') window.Sounds.defeat();
} else {
title.textContent = 'Игра окончена';
title.style.color = 'var(--cyan)';
body.textContent = 'Победитель не определён.';
}
// Показываем кнопку "Новая игра" только хосту (не наблюдателю)
if (newGameBtn && !state.isSpectator && state.yourIndex === 0) {
newGameBtn.style.display = 'block';
} else if (newGameBtn) {
newGameBtn.style.display = 'none';
}
modal.classList.remove('hidden');
}
function showTurnNotification(state) {
const notification = $('turn-notification');
const notificationText = $('turn-notification-text');
if (!notification || !notificationText) return;
const currentPlayer = state.players[state.currentPlayerIndex];
const isYourTurn = !state.isSpectator && state.currentPlayerIndex === state.yourIndex;
const playerName = currentPlayer?.name || `Игрок ${state.currentPlayerIndex + 1}`;
if (isYourTurn) {
notificationText.textContent = 'ВАШ ХОД';
notificationText.parentElement.classList.add('your-turn');
} else {
notificationText.textContent = `ХОД: ${playerName.toUpperCase()}`;
notificationText.parentElement.classList.remove('your-turn');
}
// Показываем нотификацию на 2 секунды
notification.classList.add('show');
setTimeout(() => {
notification.classList.remove('show');
}, 2000);
}
function renderBoards(state) {
const you = state.isSpectator ? null : (state.players[yourIndex] || null);
const opponentsArea = $('opponents-area');
const yourBoard = $('your-board');
if (opponentsArea) {
opponentsArea.innerHTML = state.players
.map((p, i) => {
if (!state.isSpectator && i === yourIndex) return '';
const isCurrent = state.currentPlayerIndex === i;
const isDead = p.health <= 0 || p.isDead;
const name = p.name || 'Игрок ' + (i + 1);
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.isSpectator && state.currentPlayerIndex === state.yourIndex && spellMode.active && spellMode.cardId && cardDb[spellMode.cardId]?.spellEffect === 'steal_cards';
return `
${escapeHtml(name)}${isDead ? ' ✝' : ''}
❤ ${p.health ?? 30}
${(p.armor || 0) > 0 ? `🛡 ${p.armor}` : ''}
🔵 ${p.mana ?? 0}/${p.maxMana ?? 0}
${p.deck && p.deck.length > 0 ? `📚 ${p.deck.length}` : ''}
${heroDrop}${heroBar}${minions.join('')}
`;
})
.filter(Boolean)
.join('');
}
if (yourBoard) {
if (state.isSpectator) {
yourBoard.innerHTML = 'Режим наблюдателя — вы не играете
';
} else if (you) {
yourBoard.innerHTML = (you.board || []).map((m, j) => {
const isNew = !seenMinions.has(m.id);
if (isNew) seenMinions.add(m.id);
return renderBoardMinion(m, yourIndex, j, state, false, isNew);
}).join('');
}
}
}
function showDamageEffect(element, damage, isHero) {
if (!element) return;
const damageEl = document.createElement('div');
damageEl.className = 'damage-indicator';
damageEl.textContent = '-' + damage;
damageEl.style.position = 'absolute';
const rect = element.getBoundingClientRect();
damageEl.style.left = (rect.left + rect.width / 2) + 'px';
damageEl.style.top = (rect.top + rect.height / 2) + 'px';
damageEl.style.pointerEvents = 'none';
damageEl.style.zIndex = '1000';
document.body.appendChild(damageEl);
setTimeout(() => {
damageEl.style.animation = 'damageFloat 1.5s ease-out forwards';
setTimeout(() => damageEl.remove(), 1000);
}, 10);
}
function showAttackAnnouncement(state, logEntry) {
const announcementEl = $('attack-announcement');
if (!announcementEl || !logEntry) return;
// Обработка специальных событий
if (logEntry.type === 'skipTurn') {
const playerName = state.players[logEntry.playerIndex]?.name || `Игрок ${logEntry.playerIndex + 1}`;
announcementEl.textContent = `⏭ ${playerName} пропускает ход`;
announcementEl.classList.remove('hidden');
setTimeout(() => {
announcementEl.classList.add('hidden');
}, 2000);
return;
}
if (logEntry.type === 'playerDefeated') {
const playerName = state.players[logEntry.playerIndex]?.name || `Игрок ${logEntry.playerIndex + 1}`;
announcementEl.textContent = `💀 ${playerName} выбыл из игры`;
announcementEl.classList.remove('hidden');
setTimeout(() => {
announcementEl.classList.add('hidden');
}, 2500);
return;
}
const who = (i) => state.players[i]?.name || ('Игрок ' + (i + 1));
let text = '';
if (logEntry.type === 'attack' && logEntry.attackerMinionId && logEntry.targetMinionId) {
const attackerPlayer = state.players.find(p => p.board?.some(m => m.id === logEntry.attackerMinionId));
const targetPlayer = state.players.find(p => p.board?.some(m => m.id === logEntry.targetMinionId));
if (attackerPlayer && targetPlayer) {
const attackerIdx = state.players.indexOf(attackerPlayer);
const targetIdx = state.players.indexOf(targetPlayer);
const attackerMinion = attackerPlayer.board.find(m => m.id === logEntry.attackerMinionId);
const targetMinion = targetPlayer.board.find(m => m.id === logEntry.targetMinionId);
const attackerCard = cardDb[attackerMinion?.cardId];
const targetCard = cardDb[targetMinion?.cardId];
const attackerName = attackerCard?.name || 'Миньон';
const targetName = targetCard?.name || 'Миньон';
const damage = logEntry.damage || attackerMinion?.attack || 0;
text = who(attackerIdx) + ' → ' + attackerName + ' ⚔ атакует ' + targetName + ' (' + damage + ' урона) → ' + who(targetIdx);
}
} else if (logEntry.type === 'attackHero' && logEntry.fromPlayer !== undefined && logEntry.toPlayer !== undefined) {
const attackerPlayer = state.players[logEntry.fromPlayer];
const targetPlayer = state.players[logEntry.toPlayer];
if (attackerPlayer && targetPlayer) {
if (logEntry.attackerMinionId) {
const attackerMinion = attackerPlayer.board?.find(m => m.id === logEntry.attackerMinionId);
const attackerCard = cardDb[attackerMinion?.cardId];
const attackerName = attackerCard?.name || 'Миньон';
const damage = logEntry.damage || attackerMinion?.attack || 0;
text = who(logEntry.fromPlayer) + ' → ' + attackerName + ' ⚔ атакует героя ' + who(logEntry.toPlayer) + ' (' + damage + ' урона)!';
} else {
const damage = logEntry.damage || 0;
text = who(logEntry.fromPlayer) + ' ⚔ атакует героя ' + who(logEntry.toPlayer) + ' (' + damage + ' урона)!';
}
}
} else if (logEntry.type === 'spell' && logEntry.fromPlayer !== undefined && logEntry.toPlayer !== undefined) {
const spellCard = cardDb[logEntry.spell];
const spellName = spellCard?.name || 'Заклинание';
if (logEntry.toIdx !== undefined && logEntry.toIdx >= 0) {
const targetPlayer = state.players[logEntry.toPlayer];
const targetMinion = targetPlayer?.board?.[logEntry.toIdx];
const targetCard = cardDb[targetMinion?.cardId];
const targetName = targetCard?.name || 'Миньон';
const damage = logEntry.damage || '';
const damageText = damage ? ' (' + damage + ' урона)' : '';
text = who(logEntry.fromPlayer) + ' → ' + spellName + ' ✨ по ' + targetName + damageText + ' → ' + who(logEntry.toPlayer);
} else {
const damage = logEntry.damage || '';
const damageText = damage ? ' (' + damage + ' урона)' : '';
text = who(logEntry.fromPlayer) + ' → ' + spellName + ' ✨ по герою ' + who(logEntry.toPlayer) + damageText + '!';
}
} else if (logEntry.type === 'heroAbility' && logEntry.fromPlayer !== undefined && logEntry.toPlayer !== undefined) {
if (logEntry.toIdx !== undefined && logEntry.toIdx >= 0) {
const targetPlayer = state.players[logEntry.toPlayer];
const targetMinion = targetPlayer?.board?.[logEntry.toIdx];
const targetCard = cardDb[targetMinion?.cardId];
const targetName = targetCard?.name || 'Миньон';
const damage = logEntry.damage || 1;
text = who(logEntry.fromPlayer) + ' → Геройская способность ⚡ по ' + targetName + ' (' + damage + ' урона) → ' + who(logEntry.toPlayer);
} else {
const damage = logEntry.damage || 1;
text = who(logEntry.fromPlayer) + ' → Геройская способность ⚡ по герою ' + who(logEntry.toPlayer) + ' (' + damage + ' урона)!';
}
} else if (logEntry.type === 'battlecry' && logEntry.damage && logEntry.fromPlayer !== undefined && logEntry.toPlayer !== undefined) {
const attackerPlayer = state.players[logEntry.fromPlayer];
const targetPlayer = state.players[logEntry.toPlayer];
if (attackerPlayer && targetPlayer) {
if (logEntry.toIdx !== undefined && logEntry.toIdx >= 0) {
const targetMinion = targetPlayer?.board?.[logEntry.toIdx];
const targetCard = cardDb[targetMinion?.cardId];
const targetName = targetCard?.name || 'Миньон';
text = who(logEntry.fromPlayer) + ' → Battlecry 💥 по ' + targetName + ' (' + logEntry.damage + ' урона) → ' + who(logEntry.toPlayer);
} else {
text = who(logEntry.fromPlayer) + ' → Battlecry 💥 по герою ' + who(logEntry.toPlayer) + ' (' + logEntry.damage + ' урона)!';
}
}
}
if (text) {
announcementEl.textContent = text;
announcementEl.classList.remove('hidden');
setTimeout(() => {
announcementEl.classList.add('hidden');
}, 2000); // Сократил с 3500 до 2000 мс
}
}
function runCombatAnimation(state) {
const lastLog = state.log && state.log.length ? state.log[state.log.length - 1] : null;
if (!lastLog) return false;
if (lastLog.type === 'attack' && lastLog.attackerMinionId && lastLog.targetMinionId) {
const attackerWrap = document.querySelector(`[data-minion-id="${lastLog.attackerMinionId}"]`);
const targetWrap = document.querySelector(`[data-minion-id="${lastLog.targetMinionId}"]`);
if (!attackerWrap || !targetWrap) return false;
// Вычисляем позиции для точного перемещения карты
const attackerRect = attackerWrap.getBoundingClientRect();
const targetRect = targetWrap.getBoundingClientRect();
// Вычисляем смещение
const dx = targetRect.left + targetRect.width / 2 - (attackerRect.left + attackerRect.width / 2);
const dy = targetRect.top + targetRect.height / 2 - (attackerRect.top + attackerRect.height / 2);
// Сохраняем начальную позицию
const startX = attackerRect.left;
const startY = attackerRect.top;
const startWidth = attackerRect.width;
const startHeight = attackerRect.height;
// Устанавливаем fixed positioning для наложения
attackerWrap.style.position = 'fixed';
attackerWrap.style.left = startX + 'px';
attackerWrap.style.top = startY + 'px';
attackerWrap.style.width = startWidth + 'px';
attackerWrap.style.height = startHeight + 'px';
attackerWrap.style.zIndex = '1000';
attackerWrap.style.transition = 'none';
// Добавляем класс для анимации
attackerWrap.classList.add('combat-lunge-overlay');
targetWrap.classList.add('combat-hit');
// Устанавливаем CSS переменные для перемещения
attackerWrap.style.setProperty('--attack-dx', dx + 'px');
attackerWrap.style.setProperty('--attack-dy', dy + 'px');
if (lastLog.damage) showDamageEffect(targetWrap, lastLog.damage, false);
if (typeof window.Sounds !== 'undefined') window.Sounds.attack();
// Анимация удара
setTimeout(() => {
targetWrap.classList.add('combat-hit-impact');
}, 200);
// Возврат карты и очистка
setTimeout(() => {
// Если атакующий не умер, возвращаем карту на место
if (!lastLog.attackerDied) {
attackerWrap.classList.remove('combat-lunge-overlay');
attackerWrap.style.position = '';
attackerWrap.style.left = '';
attackerWrap.style.top = '';
attackerWrap.style.width = '';
attackerWrap.style.height = '';
attackerWrap.style.zIndex = '';
attackerWrap.style.setProperty('--attack-dx', '');
attackerWrap.style.setProperty('--attack-dy', '');
attackerWrap.style.transition = '';
} else {
// Если атакующий умер, сразу применяем анимацию смерти
attackerWrap.classList.remove('combat-lunge-overlay');
attackerWrap.style.position = '';
attackerWrap.style.left = '';
attackerWrap.style.top = '';
attackerWrap.style.width = '';
attackerWrap.style.height = '';
attackerWrap.style.zIndex = '';
attackerWrap.style.setProperty('--attack-dx', '');
attackerWrap.style.setProperty('--attack-dy', '');
attackerWrap.style.transition = '';
attackerWrap.classList.add('combat-death');
showDamageEffect(attackerWrap, '💀', false);
}
if (lastLog.targetDied) {
targetWrap.classList.add('combat-death');
showDamageEffect(targetWrap, '💀', false);
}
targetWrap.classList.remove('combat-hit-impact');
}, 500);
combatTimeout = setTimeout(function () {
combatTimeout = null;
renderBoards(state);
bindGameEvents(state);
}, 600);
return true;
} else if (lastLog.type === 'attackHero' && lastLog.toPlayer !== undefined && lastLog.attackerMinionId) {
const attackerWrap = document.querySelector(`[data-minion-id="${lastLog.attackerMinionId}"]`);
const targetPlayer = state.players[lastLog.toPlayer];
if (attackerWrap && targetPlayer) {
const heroEl = document.querySelector(`[data-player-index="${lastLog.toPlayer}"].hero-target, .opponent-block[data-opponent-index="${lastLog.toPlayer}"] .drop-target-hero`);
if (heroEl) {
// Вычисляем позиции для атаки по герою
const attackerRect = attackerWrap.getBoundingClientRect();
const heroRect = heroEl.getBoundingClientRect();
const dx = heroRect.left + heroRect.width / 2 - (attackerRect.left + attackerRect.width / 2);
const dy = heroRect.top + heroRect.height / 2 - (attackerRect.top + attackerRect.height / 2);
const startX = attackerRect.left;
const startY = attackerRect.top;
const startWidth = attackerRect.width;
const startHeight = attackerRect.height;
// Устанавливаем fixed positioning
attackerWrap.style.position = 'fixed';
attackerWrap.style.left = startX + 'px';
attackerWrap.style.top = startY + 'px';
attackerWrap.style.width = startWidth + 'px';
attackerWrap.style.height = startHeight + 'px';
attackerWrap.style.zIndex = '1000';
attackerWrap.style.transition = 'none';
attackerWrap.classList.add('combat-lunge-overlay');
attackerWrap.style.setProperty('--attack-dx', dx + 'px');
attackerWrap.style.setProperty('--attack-dy', dy + 'px');
heroEl.classList.add('hero-damage-flash', 'combat-hit-impact');
if (lastLog.damage) showDamageEffect(heroEl, lastLog.damage, true);
if (typeof window.Sounds !== 'undefined') window.Sounds.attack();
setTimeout(() => {
attackerWrap.classList.remove('combat-lunge-overlay');
attackerWrap.style.position = '';
attackerWrap.style.left = '';
attackerWrap.style.top = '';
attackerWrap.style.width = '';
attackerWrap.style.height = '';
attackerWrap.style.zIndex = '';
attackerWrap.style.setProperty('--attack-dx', '');
attackerWrap.style.setProperty('--attack-dy', '');
attackerWrap.style.transition = '';
heroEl.classList.remove('hero-damage-flash', 'combat-hit-impact');
}, 500);
}
}
return true;
} else if ((lastLog.type === 'battlecry' || lastLog.type === 'spell' || lastLog.type === 'heroAbility') && lastLog.damage && lastLog.toPlayer !== undefined) {
const targetPlayer = state.players[lastLog.toPlayer];
if (targetPlayer) {
if (lastLog.toIdx !== undefined && lastLog.toIdx >= 0 && targetPlayer.board && targetPlayer.board[lastLog.toIdx]) {
const minionEl = document.querySelector(`[data-player-index="${lastLog.toPlayer}"][data-board-index="${lastLog.toIdx}"]`);
if (minionEl) {
minionEl.classList.add('spell-hit');
showDamageEffect(minionEl, lastLog.damage, false);
setTimeout(() => minionEl.classList.remove('spell-hit'), 500);
}
} else {
const heroEl = document.querySelector(`[data-player-index="${lastLog.toPlayer}"].hero-target, .opponent-block[data-opponent-index="${lastLog.toPlayer}"] .drop-target-hero`);
if (heroEl) {
heroEl.classList.add('hero-damage-flash');
showDamageEffect(heroEl, lastLog.damage, true);
setTimeout(() => heroEl.classList.remove('hero-damage-flash'), 500);
}
}
}
return true;
}
return false;
}
function renderGame(state) {
const you = state.isSpectator ? null : (state.players[yourIndex] || null);
$('your-mana').textContent = state.isSpectator ? '—' : (you?.mana ?? 0);
$('your-max-mana').textContent = state.isSpectator ? '—' : (you?.maxMana ?? 10);
const healthEl = $('your-health');
if (healthEl) {
const newHealth = state.isSpectator ? '—' : (you?.health ?? 30);
const oldHealthText = healthEl.textContent;
const oldHealth = state.isSpectator ? '—' : (parseInt(oldHealthText) || 30);
healthEl.textContent = newHealth;
// Добавляем визуальный эффект при изменении HP
if (!state.isSpectator && typeof newHealth === 'number' && typeof oldHealth === 'number' && newHealth !== oldHealth) {
healthEl.classList.add('health-changed');
setTimeout(() => healthEl.classList.remove('health-changed'), 500);
}
}
// Отображение брони
const armorEl = $('your-armor');
const armorDisplayEl = $('armor-display');
if (armorEl && armorDisplayEl) {
const armor = state.isSpectator ? 0 : (state.yourArmor || you?.armor || 0);
armorEl.textContent = armor;
if (armor > 0) {
armorDisplayEl.style.display = 'inline-flex';
} else {
armorDisplayEl.style.display = 'none';
}
}
$('your-deck').textContent = state.yourDeckCount ?? you.deck?.length ?? 0;
const lastLog = state.log && state.log.length ? state.log[state.log.length - 1] : null;
// Добавляем новые логи в чат
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;
}
});
}
}
if (lastLog?.type === 'attackHero' && typeof window.Sounds !== 'undefined') window.Sounds.attack();
const isYourTurn = state.currentPlayerIndex === yourIndex && !state.isSpectator;
const endBtn = $('btn-end-turn');
if (endBtn) {
endBtn.disabled = !isYourTurn || state.isSpectator;
if (state.isSpectator) endBtn.title = 'Режим наблюдателя';
}
const heroBtn = $('btn-hero-ability');
if (heroBtn) {
var canHero = isYourTurn && !state.isSpectator && (you.mana ?? 0) >= 2 && !state.yourHeroAbilityUsed && !attackMode.active && !spellMode.active && !heroAbilityMode.active;
heroBtn.disabled = !canHero || state.isSpectator;
heroBtn.classList.toggle('hidden', !isYourTurn || state.isSpectator);
}
const resetBtnEl = $('btn-reset-lobby');
if (resetBtnEl) resetBtnEl.classList.add('hidden');
const badge = $('turn-badge');
if (badge) {
badge.classList.toggle('your-turn', isYourTurn);
badge.textContent = isYourTurn ? 'ВАШ ХОД' : 'Ход ' + (state.turn || 1);
}
// Показываем нотификацию о том, кто ходит
showTurnNotification(state);
var skipBoardUpdate = false;
if (lastLog?.type === 'attack' && lastLog.attackerMinionId && lastLog.targetMinionId && prevGameState && runCombatAnimation(state)) {
skipBoardUpdate = true;
showAttackAnnouncement(state, lastLog);
} else if (lastLog && (lastLog.type === 'attackHero' || lastLog.type === 'spell' || lastLog.type === 'heroAbility' || (lastLog.type === 'battlecry' && lastLog.damage))) {
showAttackAnnouncement(state, lastLog);
} else if (lastLog?.type === 'attack' && lastLog.attackerMinionId && lastLog.targetMinionId) {
showAttackAnnouncement(state, lastLog);
} else if (lastLog?.type === 'skipTurn' || lastLog?.type === 'playerDefeated') {
showAttackAnnouncement(state, lastLog);
}
if (!skipBoardUpdate) {
renderBoards(state);
}
const hand = state.isSpectator ? [] : (you.hand || []);
var justDrew = !state.isSpectator && hand.length > lastHandLength && lastHandLength > 0;
if (justDrew && typeof window.Sounds !== 'undefined') window.Sounds.drawCard();
lastHandLength = hand.length;
const handEl = $('your-hand');
if (handEl) {
if (state.isSpectator) {
handEl.innerHTML = 'Режим наблюдателя — вы не можете играть
';
} else {
handEl.innerHTML = hand
.map((cid, i) => {
var meta = cardDb[cid];
var cost = meta?.cost ?? 0;
var playable = false;
var notEnoughMana = false;
if (isYourTurn) {
if (meta?.type === 'minion') {
playable = you && you.mana >= cost && (you.board?.length ?? 0) < 7;
notEnoughMana = you && you.mana < cost;
} else if (meta?.type === 'spell') {
playable = you && you.mana >= cost && !attackMode.active && !spellMode.active && !heroAbilityMode.active;
notEnoughMana = you && you.mana < cost;
}
}
var drawAnim = justDrew && i === hand.length - 1;
return renderHandCard(cid, i, hand.length, playable, drawAnim, notEnoughMana);
})
.join('');
}
}
const deckCount = state.isSpectator ? 0 : (state.yourDeckCount ?? you?.deck?.length ?? 0);
const canDraw = isYourTurn && !state.isSpectator && !state.yourManualDrawUsed && deckCount > 0 && (you.hand?.length ?? 0) < 10;
renderDeckFan(deckCount, justDrew, canDraw);
bindGameEvents(state);
}
function renderDeckFan(deckCount, justDrew, canDraw) {
const fan = $('deck-fan');
const stack = $('deck-stack');
const badge = $('deck-count-badge');
if (!stack) return;
const n = Math.min(5, Math.max(0, deckCount));
let html = '';
for (let i = 0; i < 5; i++) {
html += i < n ? '' : '';
}
stack.innerHTML = html;
if (badge) badge.textContent = String(deckCount);
if (fan) {
fan.classList.toggle('deck-just-drew', !!justDrew);
fan.classList.toggle('deck-drawable', !!canDraw);
fan.title = canDraw ? 'Клик — вытянуть карту' : 'Колода';
if (justDrew) setTimeout(function () { fan.classList.remove('deck-just-drew'); }, 400);
}
}
function abilityTags(meta) {
var tags = [];
if (meta.battlecry) tags.push('Вой:' + escapeHtml(meta.battlecry));
if (meta.deathrattle) tags.push('Предсм.:' + escapeHtml(meta.deathrattle));
if (!tags.length) return '';
return '' + tags.join(' ') + '
';
}
function renderHandCard(cardId, handIndex, handSize, playable, drawFromDeck, notEnoughMana) {
var meta = cardDb[cardId];
if (!meta) return '';
var name = meta.name || cardId;
var cost = meta.cost ?? 0;
var atk = meta.attack ?? 0;
var hp = meta.health ?? 0;
var art = getCardArt(meta);
var faction = meta.faction || 'neutral';
var rotate = handSize <= 1 ? 0 : -10 + (handIndex / Math.max(1, handSize - 1)) * 20;
var isMinion = meta.type === 'minion';
var statsHtml = isMinion
? 'Мана' + cost + '
'
+ 'Атака' + atk + '
'
+ 'Здоровье' + hp + '
'
: 'Мана' + cost + '
';
var textHtml = meta.text ? '' + escapeHtml(meta.text) + '
' : '';
var abilHtml = abilityTags(meta);
var fictionalClass = meta.fictional ? ' fictional-card' : '';
var notEnoughManaClass = notEnoughMana ? ' not-enough-mana' : '';
return ''
+ '
'
+ '
' + art + '
'
+ '
'
+ '
' + escapeHtml(name) + '
'
+ textHtml + abilHtml
+ '
' + statsHtml + '
'
+ '
'
+ '
';
}
var swgMap = {
r2d2: 'r2d2', c3po: 'c3po', yoda: 'yoda', boba: 'bobbafett', vader: 'darthvader',
stormtrooper: 'stormtrooper', deathstar: 'deathstar', atat: 'atat', xwing: 'xwing',
tie: 'tie', falcon: 'falcon', leia: 'leia', chewie: 'wookie', ackbar: 'akbar',
kylo: 'kylo', bb8: 'bb8', phasma: 'phasma', trooper: 'stormtrooper',
jawas: 'galrep', sandcrawler: 'galrep', tusken: 'galrep', sarlacc: 'galrep',
bantha: 'galrep', rancor: 'galrep', tauntaun: 'galrep', temple: 'jediorder',
cantina: 'galrep', void: 'galrep', storm: 'jediorder', malice: 'darthvader',
zephyr: 'galrep', walker: 'galrep', crystal: 'jediorder', shadow: 'darthvader',
nebula: 'galrep', elite: 'galrep',
anakin: 'saberjedi', kanan: 'saberjedi', ezra: 'saberjedi', cal_kestis: 'saberjedi',
plo_koon: 'saberjedi', ki_adi: 'saberjedi', aayla: 'saberjedi', shaak_ti: 'saberjedi',
savage: 'sabersith', jar_jar: 'galrep', tarkin: 'galemp', starkiller: 'sabersith',
moff_gideon: 'galemp', rebel_trooper: 'galrep', rebel_commando: 'galrep', rebel_scout: 'galrep',
clone_trooper: 'stormtrooper', clone_commando: 'stormtrooper', arc_trooper: 'stormtrooper',
battle_droid: 'combatdrone', super_battle_droid: 'combatdrone', droideka: 'combatdrone',
cody: 'stormtrooper', rex: 'stormtrooper', fives: 'stormtrooper', echo: 'stormtrooper',
father: 'jediorder', son: 'sith', daughter: 'jediorder',
korriban: 'galemp', tatooine: 'galrep', coruscant: 'galrep', zakkuul: 'galemp',
kamino: 'galrep', mustafar: 'galemp', naboo: 'galrep', endor: 'galrep', hoth: 'galrep', dagobah: 'jediorder',
star_destroyer: 'stardestroyer', speeder: 'landspeeder', wedge: 'xwing',
nebulon: 'xwing', droideka: 'combatdrone', jango: 'bobbafett',
luke: 'saberjedi', obiwan: 'saberjedi', quigon: 'saberjedi', mace: 'saberjedi',
dooku: 'sabersith', maul: 'sabersith', snoke: 'sith', palpatine: 'sith',
han: 'falcon', rey: 'newrep', finn: 'newrep', padme: 'galrep', jabba: 'carbonite',
lando: 'cantina', ewok: 'jediorder', wicket: 'jediorder', scout: 'stormtrooper',
guard: 'stormtrooper', probe: 'r2d2',
ahsoka: 'saberjedi', ventress: 'sabersith', rex: 'stormtrooper', thrawn: 'galemp',
mando: 'mandalorian', grogu: 'yoda', jyn: 'newrep', cassian: 'galrep',
k2so: 'r2d2', chirrut: 'jediorder', baze: 'stormtrooper', poe: 'xwing',
hux: 'stormtrooper', rose: 'galrep',
lightsaber: 'saberjedi', force: 'saberjedi', blaster: 'tie', hyperspace: 'xwing', fear: 'sith', hope: 'galrep',
};
function getCardArt(meta) {
// Проверяем наличие кастомных иконок в папке extraicons
const extraIconMap = {
'ezra': 'ezra-bridger.png',
'ezra_bridger': 'ezra-bridger.png',
'father': 'the-father.png',
'the_father': 'the-father.png',
'ahsoka': 'ashoka-tano.png',
'ashoka_tano': 'ashoka-tano.png',
'cal': 'CalCestis.png',
'cal_kestis': 'CalCestis.png',
'nihilus': 'darthnihilus.png',
'darth_nihilus': 'darthnihilus.png',
'plagueis': 'dartplagues.png',
'darth_plagueis': 'dartplagues.png',
'plo': 'plokoon.png',
'plo_koon': 'plokoon.png',
'revan': 'revan.png'
};
const extraIcon = extraIconMap[meta.art];
if (extraIcon) {
return '
';
}
// Используем стандартные SWG иконки
var swg = swgMap[meta.art];
if (swg) return '';
return '✦';
}
function renderBoardMinion(m, playerIndex, boardIndex, state, isOpponent, summonAnim) {
const meta = cardDb[m.cardId];
const name = meta?.name || m.cardId;
const art = meta ? getCardArt(meta) : '✦';
const faction = meta?.faction || 'neutral';
const isYourTurn = state.currentPlayerIndex === state.yourIndex;
const canAttackTwice = meta && meta.canAttackTwice;
const attacksUsed = m.attacksUsed || 0;
const canAttack = !isOpponent && m.canAttack && isYourTurn && (!canAttackTwice || attacksUsed < 2);
const attackable = attackMode.active && attackMode.attackerPlayer === state.yourIndex && attackMode.attackerBoard === boardIndex;
const targetable = attackMode.active && attackMode.attackerPlayer === state.yourIndex && playerIndex !== state.yourIndex;
// Учитываем бонусы синергий
const synergyAttack = m.synergyAttackBonus || 0;
const synergyHealth = m.synergyHealthBonus || 0;
const displayAttack = (m.attack || 0) + synergyAttack;
const displayHealth = (m.health || 0) + synergyHealth;
const hasSynergy = synergyAttack > 0 || synergyHealth > 0;
const cls = ['card-wrap'];
if (canAttack && !attackMode.active) cls.push('attackable');
if (attackable) cls.push('attackable');
if (targetable) cls.push('targetable');
if (isOpponent && isYourTurn) cls.push('drop-target');
if (hasSynergy) cls.push('has-synergy');
var spellTarget = false;
if (spellMode.active && spellMode.spellTarget) {
var st = spellMode.spellTarget;
if (isOpponent && (st === 'any' || st === 'enemy_minion' || st === 'any_minion')) spellTarget = true;
if (!isOpponent && (st === 'friendly_minion' || st === 'any_minion')) spellTarget = true;
}
var heroTarget = heroAbilityMode.active && isOpponent;
if (heroTarget) cls.push('hero-ability-target');
if (spellTarget) cls.push('spell-target');
if (m.frozen) cls.push('frozen');
if (meta && meta.fictional) cls.push('fictional-card');
const draggable = !isOpponent && canAttack && !m.frozen ? ' draggable="true"' : '';
const cardCls = 'card faction-' + faction + (summonAnim ? ' summon-anim' : '') + (m.frozen ? ' frozen-card' : '') + (meta && meta.fictional ? ' fictional-card' : '');
const dropAttrs = (isOpponent && isYourTurn) || spellTarget || heroTarget ? ` data-drop-player="${playerIndex}" data-drop-board="${boardIndex}"` : '';
const dragTitle = !isOpponent && canAttack && !m.frozen ? ' title="Перетащи на врага для атаки"' : (m.frozen ? ' title="Заморожен (не может атаковать)"' : '');
var abilHtml = meta ? abilityTags(meta) : '';
var textHtml = meta && meta.text ? '' + escapeHtml(meta.text) + '
' : '';
var frozenIcon = m.frozen ? '❄
' : '';
return ''
+ '
'
+ frozenIcon
+ '
' + art + '
'
+ '
'
+ '
' + escapeHtml(name) + '
'
+ textHtml + abilHtml
+ '
'
+ '
Атака' + displayAttack + (synergyAttack > 0 ? '+' + synergyAttack + '' : '') + '
'
+ '
Здоровье' + displayHealth + (synergyHealth > 0 ? '+' + synergyHealth + '' : '') + '
'
+ '
'
+ '
'
+ '
';
}
function renderHeroDropZone(playerIndex, state) {
const isYourTurn = state.currentPlayerIndex === state.yourIndex;
if (!isYourTurn || playerIndex === state.yourIndex) return '';
if (attackMode.active && !spellMode.active && !heroAbilityMode.active) return '';
var spellHero = spellMode.active && spellMode.spellTarget === 'any';
var heroAb = heroAbilityMode.active;
const p = state.players[playerIndex];
const name = p?.name || 'Игрок ' + (playerIndex + 1);
var heroCls = 'drop-target drop-target-hero' + (spellHero ? ' spell-target' : '') + (heroAb ? ' hero-ability-target' : '');
var title = spellHero ? 'Цель заклинания' : (heroAb ? 'Цель геройской способности' : 'Перетащи сюда миньона для атаки по лидеру');
return ''
+ '❤'
+ '' + escapeHtml(name) + ''
+ '
';
}
function renderHeroTarget(playerIndex, state) {
if (!attackMode.active || attackMode.attackerPlayer !== state.yourIndex || playerIndex === state.yourIndex) return '';
const p = state.players[playerIndex];
const name = p?.name || 'Игрок ' + (playerIndex + 1);
return `
❤
${escapeHtml(name)} (лидер)
${p?.health ?? 30}
`;
}
function bindGameEvents(state) {
const you = state.players[state.yourIndex];
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);
const handIndex = parseInt(wrap.dataset.handIndex, 10);
const meta = cardDb[wrap.dataset.cardId];
const isMinion = meta && meta.type === 'minion';
const canPlay = isYourTurn && !state.isSpectator && !attackMode.active && !heroAbilityMode.active && meta && you && you.mana >= (meta.cost || 0);
// Если кузница открыта и карта - миньон, делаем её перетаскиваемой для кузницы
if (isForgeOpen && isMinion && !state.isSpectator) {
wrap.draggable = true;
wrap.ondragstart = function (e) {
e.dataTransfer.setData('text/plain', 'hand:' + handIndex);
e.dataTransfer.setData('source', 'hand');
wrap.classList.add('dragging');
};
wrap.ondragend = function () {
wrap.classList.remove('dragging');
};
} else if (isMinion && canPlay && !state.isSpectator && you && (you.board?.length ?? 0) < 7) {
wrap.draggable = true;
wrap.ondragstart = function (e) {
if (!canPlay) return;
e.dataTransfer.setData('text/plain', 'hand:' + handIndex);
e.dataTransfer.effectAllowed = 'move';
wrap.classList.add('dragging');
};
wrap.ondragend = function () {
wrap.classList.remove('dragging');
$all('.drop-target').forEach(function (t) { t.classList.remove('drop-over'); });
};
} else {
wrap.draggable = false;
wrap.ondragstart = null;
wrap.ondragend = null;
}
wrap.onclick = function (e) {
if (e.target.closest('.card-btn-info')) return;
if (!isYourTurn || attackMode.active || heroAbilityMode.active) return;
if (!meta) return;
if (you.mana < (meta.cost || 0)) return;
if (meta.type === 'minion') {
if ((you.board?.length ?? 0) >= 7) return;
if (typeof window.Sounds !== 'undefined') window.Sounds.playCard();
var r = wrap.getBoundingClientRect();
spawnEffect('play', r.left + r.width / 2, r.top + r.height / 2);
wrap.querySelector('.card')?.classList.add('play-anim');
socket.emit('playCard', { handIndex: handIndex, boardPos: you.board.length });
setTimeout(function () { wrap.querySelector('.card')?.classList.remove('play-anim'); }, 500);
return;
}
if (meta.type === 'spell') {
if (state.isSpectator || 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;
}
}
// Если это Грабеж, но spellTarget не enemy_player, все равно активируем режим выбора цели
if (meta.spellEffect === 'steal_cards' && !meta.spellTarget) {
spellMode = { active: true, handIndex: handIndex, cardId: wrap.dataset.cardId, spellTarget: 'enemy_player' };
$('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;
}
};
// Touch support for mobile
if (isTouchDevice()) {
wrap.ontouchstart = function (e) {
if (e.target.closest('.card-btn-info')) {
// Handle info button separately
const btn = e.target.closest('.card-btn-info');
if (btn) {
const cid = btn.dataset.cardId;
if (cid) {
const meta = cardDb[cid];
if (meta) {
const nameEl = $('card-info-name');
const metaEl = $('card-info-meta');
const textEl = $('card-info-text');
const abilEl = $('card-info-abilities');
const bioEl = $('card-info-bio');
if (nameEl) nameEl.textContent = meta.name || cid;
if (metaEl) {
if (meta.type === 'spell') metaEl.textContent = 'Заклинание · Мана: ' + (meta.cost ?? 0);
else metaEl.textContent = 'Минайон · Мана: ' + (meta.cost ?? 0) + ', Атака: ' + (meta.attack ?? 0) + ', Здоровье: ' + (meta.health ?? 0);
}
if (textEl) { textEl.textContent = meta.text || ''; textEl.classList.toggle('hidden', !meta.text); }
if (abilEl) {
var parts = [];
if (meta.battlecry) parts.push('Вой: ' + escapeHtml(meta.battlecry));
if (meta.deathrattle) parts.push('Предсмертный хрип: ' + escapeHtml(meta.deathrattle));
abilEl.innerHTML = parts.length ? parts.join('
') : '';
abilEl.classList.toggle('hidden', !parts.length);
}
if (bioEl) { bioEl.textContent = meta.bio || ''; bioEl.classList.toggle('hidden', !meta.bio); }
$('card-info-overlay')?.classList.remove('hidden');
}
}
}
return;
}
if (!isYourTurn || attackMode.active || heroAbilityMode.active) return;
if (!meta) return;
const touch = e.touches[0];
touchState.startX = touch.clientX;
touchState.startY = touch.clientY;
touchState.startTime = Date.now();
touchState.target = wrap;
touchState.isSwipe = false;
wrap.classList.add('touch-active');
// Long press for card info
touchState.longPressTimer = setTimeout(function() {
if (touchState.target === wrap && meta) {
const nameEl = $('card-info-name');
const metaEl = $('card-info-meta');
const textEl = $('card-info-text');
const abilEl = $('card-info-abilities');
const bioEl = $('card-info-bio');
if (nameEl) nameEl.textContent = meta.name || wrap.dataset.cardId;
if (metaEl) {
if (meta.type === 'spell') metaEl.textContent = 'Заклинание · Мана: ' + (meta.cost ?? 0);
else metaEl.textContent = 'Минайон · Мана: ' + (meta.cost ?? 0) + ', Атака: ' + (meta.attack ?? 0) + ', Здоровье: ' + (meta.health ?? 0);
}
if (textEl) { textEl.textContent = meta.text || ''; textEl.classList.toggle('hidden', !meta.text); }
if (abilEl) {
var parts = [];
if (meta.battlecry) parts.push('Вой: ' + escapeHtml(meta.battlecry));
if (meta.deathrattle) parts.push('Предсмертный хрип: ' + escapeHtml(meta.deathrattle));
abilEl.innerHTML = parts.length ? parts.join('
') : '';
abilEl.classList.toggle('hidden', !parts.length);
}
if (bioEl) { bioEl.textContent = meta.bio || ''; bioEl.classList.toggle('hidden', !meta.bio); }
$('card-info-overlay')?.classList.remove('hidden');
wrap.classList.add('long-pressed');
}
}, touchState.longPressDelay);
};
wrap.ontouchmove = function (e) {
if (!touchState.target || touchState.target !== wrap) return;
// Cancel long press if moved
if (touchState.longPressTimer) {
clearTimeout(touchState.longPressTimer);
touchState.longPressTimer = null;
}
const touch = e.touches[0];
const dx = touch.clientX - touchState.startX;
const dy = touch.clientY - touchState.startY;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > touchState.minSwipeDistance) {
touchState.isSwipe = true;
wrap.classList.add('swiping');
wrap.classList.remove('long-pressed');
}
};
wrap.ontouchend = function (e) {
if (!touchState.target || touchState.target !== wrap) return;
// Cancel long press timer
if (touchState.longPressTimer) {
clearTimeout(touchState.longPressTimer);
touchState.longPressTimer = null;
}
wrap.classList.remove('touch-active', 'swiping', 'long-pressed');
const touch = e.changedTouches[0];
const dx = touch.clientX - touchState.startX;
const dy = touch.clientY - touchState.startY;
const distance = Math.sqrt(dx * dx + dy * dy);
const time = Date.now() - touchState.startTime;
// If it was a swipe or long press, don't trigger card play
if (touchState.isSwipe || distance > touchState.minSwipeDistance || time > touchState.longPressDelay) {
touchState.target = null;
return;
}
// Tap - trigger card play
if (time < touchState.maxSwipeTime && distance < 10 && you.mana >= (meta?.cost || 0)) {
if (meta?.type === 'minion' && (you.board?.length ?? 0) < 7) {
if (typeof window.Sounds !== 'undefined') window.Sounds.playCard();
var r = wrap.getBoundingClientRect();
spawnEffect('play', r.left + r.width / 2, r.top + r.height / 2);
wrap.querySelector('.card')?.classList.add('play-anim');
socket.emit('playCard', { handIndex: handIndex, boardPos: you.board.length });
setTimeout(function () { wrap.querySelector('.card')?.classList.remove('play-anim'); }, 500);
} else if (meta?.type === 'spell') {
if (spellMode.active || heroAbilityMode.active) return;
var needTarget = meta.spellTarget && meta.spellTarget !== 'none';
if (!needTarget) {
if (typeof window.Sounds !== 'undefined') window.Sounds.playCard();
socket.emit('playSpell', { handIndex: handIndex });
} else {
spellMode = { active: true, handIndex: handIndex, cardId: wrap.dataset.cardId, spellTarget: meta.spellTarget };
$('spell-mode')?.classList.remove('hidden');
renderGame(state);
}
}
}
touchState.target = null;
};
wrap.ontouchcancel = function () {
if (touchState.longPressTimer) {
clearTimeout(touchState.longPressTimer);
touchState.longPressTimer = null;
}
wrap.classList.remove('touch-active', 'swiping', 'long-pressed');
touchState.target = null;
};
}
});
$all('.card-btn-info').forEach(function (btn) {
btn.onclick = function (e) {
e.stopPropagation();
var cid = btn.dataset.cardId;
if (!cid) return;
var meta = cardDb[cid];
if (!meta) return;
var nameEl = $('card-info-name');
var metaEl = $('card-info-meta');
var textEl = $('card-info-text');
var abilEl = $('card-info-abilities');
var bioEl = $('card-info-bio');
if (nameEl) nameEl.textContent = meta.name || cid;
if (metaEl) {
if (meta.type === 'spell') metaEl.textContent = 'Заклинание · Мана: ' + (meta.cost ?? 0);
else metaEl.textContent = 'Минайон · Мана: ' + (meta.cost ?? 0) + ', Атака: ' + (meta.attack ?? 0) + ', Здоровье: ' + (meta.health ?? 0);
}
if (textEl) { textEl.textContent = meta.text || ''; textEl.classList.toggle('hidden', !meta.text); }
if (abilEl) {
var parts = [];
if (meta.battlecry) parts.push('Вой: ' + escapeHtml(meta.battlecry));
if (meta.deathrattle) parts.push('Предсмертный хрип: ' + escapeHtml(meta.deathrattle));
abilEl.innerHTML = parts.length ? parts.join('
') : '';
abilEl.classList.toggle('hidden', !parts.length);
}
if (bioEl) { bioEl.textContent = meta.bio || ''; bioEl.classList.toggle('hidden', !meta.bio); }
$('card-info-overlay')?.classList.remove('hidden');
};
});
var closeInfo = $('btn-card-info-close');
if (closeInfo) closeInfo.onclick = function () { $('card-info-overlay')?.classList.add('hidden'); };
$all('.card-wrap.attackable:not(.targetable)').forEach((wrap) => {
wrap.onclick = () => {
if (!isYourTurn || attackMode.active || spellMode.active || heroAbilityMode.active) return;
const playerIndex = parseInt(wrap.dataset.playerIndex, 10);
const boardIndex = parseInt(wrap.dataset.boardIndex, 10);
if (playerIndex !== state.yourIndex) return;
attackMode = { active: true, attackerPlayer: playerIndex, attackerBoard: boardIndex };
$('attack-mode')?.classList.remove('hidden');
renderGame(state);
};
// Touch support for attack
if (isTouchDevice()) {
wrap.ontouchstart = function (e) {
if (!isYourTurn || attackMode.active || spellMode.active || heroAbilityMode.active) return;
const playerIndex = parseInt(wrap.dataset.playerIndex, 10);
const boardIndex = parseInt(wrap.dataset.boardIndex, 10);
if (playerIndex !== state.yourIndex) return;
const touch = e.touches[0];
touchState.startX = touch.clientX;
touchState.startY = touch.clientY;
touchState.startTime = Date.now();
touchState.target = wrap;
};
wrap.ontouchend = function (e) {
if (!touchState.target || touchState.target !== wrap) return;
const touch = e.changedTouches[0];
const dx = touch.clientX - touchState.startX;
const dy = touch.clientY - touchState.startY;
const distance = Math.sqrt(dx * dx + dy * dy);
const time = Date.now() - touchState.startTime;
if (time < touchState.maxSwipeTime && distance < 20) {
const playerIndex = parseInt(wrap.dataset.playerIndex, 10);
const boardIndex = parseInt(wrap.dataset.boardIndex, 10);
if (playerIndex === state.yourIndex) {
attackMode = { active: true, attackerPlayer: playerIndex, attackerBoard: boardIndex };
$('attack-mode')?.classList.remove('hidden');
renderGame(state);
}
}
touchState.target = null;
};
}
});
$all('.card-wrap.targetable, .card-wrap.hero-target').forEach((wrap) => {
wrap.onclick = () => {
if (spellMode.active || heroAbilityMode.active) return;
if (!attackMode.active) return;
const targetPlayer = parseInt(wrap.dataset.playerIndex ?? wrap.dataset.dropPlayer, 10);
const targetBoard = parseInt(wrap.dataset.boardIndex ?? wrap.dataset.dropBoard, 10);
if (typeof window.Sounds !== 'undefined') window.Sounds.attack();
socket.emit('attack', {
attackerPlayerIndex: state.yourIndex,
attackerBoardIndex: attackMode.attackerBoard,
targetPlayerIndex: targetPlayer,
targetBoardIndex: targetBoard,
});
attackMode = { active: false, attackerPlayer: -1, attackerBoard: -1 };
$('attack-mode')?.classList.add('hidden');
};
// Touch support for target selection
if (isTouchDevice()) {
wrap.ontouchstart = function (e) {
if (spellMode.active || heroAbilityMode.active) return;
if (!attackMode.active) return;
e.preventDefault();
const targetPlayer = parseInt(wrap.dataset.playerIndex ?? wrap.dataset.dropPlayer, 10);
const targetBoard = parseInt(wrap.dataset.boardIndex ?? wrap.dataset.dropBoard, 10);
if (typeof window.Sounds !== 'undefined') window.Sounds.attack();
socket.emit('attack', {
attackerPlayerIndex: state.yourIndex,
attackerBoardIndex: attackMode.attackerBoard,
targetPlayerIndex: targetPlayer,
targetBoardIndex: targetBoard,
});
attackMode = { active: false, attackerPlayer: -1, attackerBoard: -1 };
$('attack-mode')?.classList.add('hidden');
};
}
});
$all('.spell-target, .drop-target-hero.spell-target').forEach(function (el) {
el.onclick = function (e) {
if (!spellMode.active && !battlecryTargetMode.active) return;
e.stopPropagation();
var tp = parseInt(el.dataset.dropPlayer ?? el.dataset.playerIndex, 10);
var tb = parseInt(el.dataset.dropBoard ?? el.dataset.boardIndex, 10);
// Обработка для battlecry (Ezra Bridger)
if (battlecryTargetMode.active && battlecryTargetMode.battlecryId === 'return_hand_enemy') {
if (tp !== state.yourIndex && tp >= 0 && tb >= 0) {
const targetPlayer = state.players[tp];
if (targetPlayer && targetPlayer.board && targetPlayer.board[tb]) {
socket.emit('battlecryTarget', {
battlecryId: 'return_hand_enemy',
targetPlayerIndex: tp,
targetBoardIndex: tb
});
battlecryTargetMode = { active: false, battlecryId: '', cardId: '', minionId: '', targetPlayerIndex: null, targetBoardIndex: null };
spellMode = { active: false, handIndex: -1, cardId: '', spellTarget: '' };
$('spell-mode')?.classList.add('hidden');
renderGame(state);
return;
}
}
return;
}
// Специальная обработка для Грабежа - открываем модальное окно
const spellCard = cardDb[spellMode.cardId];
if (spellCard && spellCard.spellEffect === 'steal_cards' && (spellCard.spellTarget === 'enemy_player' || spellCard.spellTarget === 'enemy_board')) {
// Выбираем противника (tp должен быть индексом противника, tb игнорируем для enemy_player)
if (tp !== state.yourIndex && tp >= 0) {
const targetPlayer = state.players[tp];
if (targetPlayer && targetPlayer.board && targetPlayer.board.length > 0) {
stealCardsMode.active = true;
stealCardsMode.handIndex = spellMode.handIndex;
stealCardsMode.targetPlayerIndex = tp;
// Получаем актуальную доску из gameState
stealCardsMode.targetDeck = targetPlayer.board ? targetPlayer.board.map(m => m.cardId) : [];
stealCardsMode.selectedIndices = [];
showStealCardsModal(state, {
targetPlayerIndex: tp,
targetPlayerName: targetPlayer.name || `Игрок ${tp + 1}`,
targetBoardSize: targetPlayer.board.length,
maxCards: Math.min(2, targetPlayer.board.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: '' };
$('spell-mode')?.classList.add('hidden');
renderGame(state);
};
// Touch support for spell targets
if (isTouchDevice()) {
el.ontouchstart = function (e) {
if (!spellMode.active) return;
e.preventDefault();
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' || spellCard.spellTarget === 'enemy_board')) {
if (tp !== state.yourIndex && tp >= 0) {
const targetPlayer = state.players[tp];
if (targetPlayer && targetPlayer.board && targetPlayer.board.length > 0) {
stealCardsMode.active = true;
stealCardsMode.handIndex = spellMode.handIndex;
stealCardsMode.targetPlayerIndex = tp;
// Получаем актуальную доску из gameState
stealCardsMode.targetDeck = targetPlayer.board ? targetPlayer.board.map(m => m.cardId) : [];
stealCardsMode.selectedIndices = [];
showStealCardsModal(state, {
targetPlayerIndex: tp,
targetPlayerName: targetPlayer.name || `Игрок ${tp + 1}`,
targetBoardSize: targetPlayer.board.length,
maxCards: Math.min(2, targetPlayer.board.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: '' };
$('spell-mode')?.classList.add('hidden');
renderGame(state);
};
}
});
$all('.hero-ability-target, .drop-target-hero.hero-ability-target').forEach(function (el) {
el.onclick = function (e) {
if (!heroAbilityMode.active) return;
e.stopPropagation();
var tp = parseInt(el.dataset.dropPlayer ?? el.dataset.playerIndex, 10);
var tb = parseInt(el.dataset.dropBoard ?? el.dataset.boardIndex, 10);
if (typeof window.Sounds !== 'undefined') window.Sounds.attack();
socket.emit('heroAbility', { targetPlayerIndex: tp, targetBoardIndex: tb });
heroAbilityMode = { active: false };
$('hero-ability-mode')?.classList.add('hidden');
renderGame(state);
};
// Touch support for hero ability targets
if (isTouchDevice()) {
el.ontouchstart = function (e) {
if (!heroAbilityMode.active) return;
e.preventDefault();
e.stopPropagation();
var tp = parseInt(el.dataset.dropPlayer ?? el.dataset.playerIndex, 10);
var tb = parseInt(el.dataset.dropBoard ?? el.dataset.boardIndex, 10);
if (typeof window.Sounds !== 'undefined') window.Sounds.attack();
socket.emit('heroAbility', { targetPlayerIndex: tp, targetBoardIndex: tb });
heroAbilityMode = { active: false };
$('hero-ability-mode')?.classList.add('hidden');
renderGame(state);
};
}
});
// Обработка выбора противника для Грабежа
$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' && spellCard.spellTarget !== 'enemy_board')) 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.board && targetPlayer.board.length > 0) {
stealCardsMode.active = true;
stealCardsMode.handIndex = spellMode.handIndex;
stealCardsMode.targetPlayerIndex = tp;
// Получаем актуальную доску из gameState
stealCardsMode.targetDeck = targetPlayer.board ? targetPlayer.board.map(m => m.cardId) : [];
stealCardsMode.selectedIndices = [];
showStealCardsModal(state, {
targetPlayerIndex: tp,
targetPlayerName: targetPlayer.name || `Игрок ${tp + 1}`,
targetBoardSize: targetPlayer.board.length,
maxCards: Math.min(2, targetPlayer.board.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' && spellCard.spellTarget !== 'enemy_board')) 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.board && targetPlayer.board.length > 0) {
stealCardsMode.active = true;
stealCardsMode.handIndex = spellMode.handIndex;
stealCardsMode.targetPlayerIndex = tp;
// Получаем актуальную доску из gameState
stealCardsMode.targetDeck = targetPlayer.board ? targetPlayer.board.map(m => m.cardId) : [];
stealCardsMode.selectedIndices = [];
showStealCardsModal(state, {
targetPlayerIndex: tp,
targetPlayerName: targetPlayer.name || `Игрок ${tp + 1}`,
targetBoardSize: targetPlayer.board.length,
maxCards: Math.min(2, targetPlayer.board.length)
});
spellMode = { active: false, handIndex: -1, cardId: '', spellTarget: '' };
$('spell-mode')?.classList.add('hidden');
}
};
}
});
var heroAbilityBtn = $('btn-hero-ability');
if (heroAbilityBtn) heroAbilityBtn.onclick = function () {
if (state.isSpectator || heroAbilityBtn.disabled || heroAbilityMode.active) return;
heroAbilityMode = { active: true };
$('hero-ability-mode')?.classList.remove('hidden');
renderGame(state);
};
var forgeBtn = $('btn-forge');
if (forgeBtn && isYourTurn && !state.isSpectator && you && you.mana >= 2) {
forgeBtn.disabled = false;
forgeBtn.onclick = function () {
if (state.isSpectator || attackMode.active || spellMode.active || heroAbilityMode.active) return;
const sidebar = $('forge-sidebar');
if (sidebar) {
sidebar.classList.remove('hidden');
forgeSelected = []; // Сбрасываем выбор при открытии
renderForgeHand(state);
renderForgeSelected();
updateForgePreview();
}
};
} else if (forgeBtn) {
forgeBtn.disabled = true;
}
var cancelSpellBtn = $('btn-cancel-spell');
if (cancelSpellBtn) cancelSpellBtn.onclick = function () {
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);
};
var cancelHeroBtn = $('btn-cancel-hero');
if (cancelHeroBtn) cancelHeroBtn.onclick = function () {
heroAbilityMode = { active: false };
$('hero-ability-mode')?.classList.add('hidden');
renderGame(state);
};
const cancelBtn = $('btn-cancel-attack');
if (cancelBtn) {
cancelBtn.onclick = function () {
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);
};
}
const endBtn = $('btn-end-turn');
if (endBtn) {
endBtn.onclick = isYourTurn
? () => {
if (typeof window.Sounds !== 'undefined') window.Sounds.endTurn();
socket.emit('endTurn');
}
: null;
}
const resetBtn = $('btn-reset-lobby');
if (resetBtn) {
resetBtn.onclick = state.yourIndex === 0 ? () => socket.emit('resetToLobby') : null;
}
var deckEl = $('deck-fan');
if (deckEl && deckEl.classList.contains('deck-drawable')) {
deckEl.onclick = function () {
var r = deckEl.getBoundingClientRect();
spawnEffect('draw', r.left + r.width / 2, r.top + r.height / 2);
socket.emit('drawCard');
};
deckEl.style.cursor = 'pointer';
// Touch support for deck
if (isTouchDevice()) {
deckEl.ontouchstart = function (e) {
e.preventDefault();
var r = deckEl.getBoundingClientRect();
spawnEffect('draw', r.left + r.width / 2, r.top + r.height / 2);
socket.emit('drawCard');
};
}
} else if (deckEl) {
deckEl.onclick = null;
deckEl.style.cursor = 'default';
if (isTouchDevice()) {
deckEl.ontouchstart = null;
}
}
$all('.card-wrap.attackable[draggable="true"]').forEach(function (wrap) {
wrap.ondragstart = function (e) {
var idx = wrap.dataset.attackerBoardIndex;
if (idx === undefined) return;
e.dataTransfer.setData('text/plain', idx);
e.dataTransfer.effectAllowed = 'move';
wrap.classList.add('dragging');
};
wrap.ondragend = function () {
wrap.classList.remove('dragging');
$all('.drop-target').forEach(function (t) { t.classList.remove('drop-over'); });
};
});
$all('.drop-target').forEach(function (target) {
target.ondragover = function (e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
target.classList.add('drop-over');
};
target.ondragleave = function () {
target.classList.remove('drop-over');
};
target.ondrop = function (e) {
e.preventDefault();
target.classList.remove('drop-over');
var data = e.dataTransfer.getData('text/plain');
if (data === '') return;
if (data.startsWith('hand:')) {
var handIndex = parseInt(data.split(':')[1], 10);
if (isNaN(handIndex)) return;
var meta = cardDb[you.hand?.[handIndex]];
if (!meta || meta.type !== 'minion') return;
if (you.mana < (meta.cost || 0)) return;
if ((you.board?.length ?? 0) >= 7) return;
if (typeof window.Sounds !== 'undefined') window.Sounds.playCard();
var rect = target.getBoundingClientRect();
spawnEffect('play', rect.left + rect.width / 2, rect.top + rect.height / 2);
socket.emit('playCard', { handIndex: handIndex, boardPos: you.board.length });
return;
}
var attackerBoard = parseInt(data, 10);
var targetPlayer = parseInt(target.dataset.dropPlayer, 10);
var targetBoard = target.dataset.dropBoard !== undefined ? parseInt(target.dataset.dropBoard, 10) : -1;
if (isNaN(attackerBoard) || isNaN(targetPlayer)) return;
if (typeof window.Sounds !== 'undefined') window.Sounds.attack();
var rect = target.getBoundingClientRect();
spawnEffect('attack', rect.left + rect.width / 2, rect.top + rect.height / 2);
socket.emit('attack', {
attackerPlayerIndex: state.yourIndex,
attackerBoardIndex: attackerBoard,
targetPlayerIndex: targetPlayer,
targetBoardIndex: isNaN(targetBoard) ? -1 : targetBoard,
});
attackMode = { active: false, attackerPlayer: -1, attackerBoard: -1 };
$('attack-mode')?.classList.add('hidden');
};
});
var yourBoardEl = $('your-board');
if (yourBoardEl && isYourTurn && !attackMode.active && !spellMode.active && !heroAbilityMode.active) {
yourBoardEl.classList.add('drop-target');
yourBoardEl.ondragover = function (e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
yourBoardEl.classList.add('drop-over');
};
yourBoardEl.ondragleave = function () {
yourBoardEl.classList.remove('drop-over');
};
yourBoardEl.ondrop = function (e) {
e.preventDefault();
yourBoardEl.classList.remove('drop-over');
var data = e.dataTransfer.getData('text/plain');
if (!data.startsWith('hand:')) return;
var handIndex = parseInt(data.split(':')[1], 10);
if (isNaN(handIndex)) return;
var meta = cardDb[you.hand?.[handIndex]];
if (!meta || meta.type !== 'minion') return;
if (you.mana < (meta.cost || 0)) return;
if ((you.board?.length ?? 0) >= 7) return;
if (typeof window.Sounds !== 'undefined') window.Sounds.playCard();
var rect = yourBoardEl.getBoundingClientRect();
spawnEffect('play', rect.left + rect.width / 2, rect.top + rect.height / 2);
socket.emit('playCard', { handIndex: handIndex, boardPos: you.board.length });
};
} else if (yourBoardEl) {
yourBoardEl.classList.remove('drop-target');
yourBoardEl.ondragover = null;
yourBoardEl.ondragleave = null;
yourBoardEl.ondrop = null;
}
// Обновляем кузницу, если она открыта
if (isForgeOpen) {
renderForgeHand(state);
renderForgeSelected();
updateForgePreview();
}
}
function initLobby() {
const hostTab = document.querySelector('.tab[data-tab="host"]');
const joinTab = document.querySelector('.tab[data-tab="join"]');
const browseTab = document.querySelector('.tab[data-tab="browse"]');
hostTab?.addEventListener('click', () => {
$all('.tab').forEach((t) => t.classList.remove('active'));
hostTab.classList.add('active');
$('host-panel')?.classList.add('active');
$('join-panel')?.classList.remove('active');
$('browse-panel')?.classList.remove('active');
clearError();
});
joinTab?.addEventListener('click', () => {
$all('.tab').forEach((t) => t.classList.remove('active'));
joinTab.classList.add('active');
$('join-panel')?.classList.add('active');
$('host-panel')?.classList.remove('active');
$('browse-panel')?.classList.remove('active');
clearError();
});
browseTab?.addEventListener('click', () => {
$all('.tab').forEach((t) => t.classList.remove('active'));
browseTab.classList.add('active');
$('browse-panel')?.classList.add('active');
$('host-panel')?.classList.remove('active');
$('join-panel')?.classList.remove('active');
clearError();
// Подключаемся к серверу если еще не подключены
if (!socket || !socket.connected) {
const url = window.location.origin;
connect(url);
socket.once('connect', () => {
socket.emit('getRoomsList');
});
} else {
socket.emit('getRoomsList');
}
});
const btnHost = $('btn-host');
if (btnHost) {
// Удаляем старые обработчики, если есть
const newBtn = btnHost.cloneNode(true);
btnHost.parentNode.replaceChild(newBtn, btnHost);
newBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
console.log('Кнопка "Создать комнату" нажата');
clearError();
const name = ($('host-name')?.value || '').trim() || 'Игрок 1';
console.log('Имя игрока:', name);
const currentHost = window.location.hostname;
let url = window.location.origin;
if (currentHost !== 'localhost' && currentHost !== '127.0.0.1') {
url = window.location.origin;
}
if (!socket || !socket.connected) {
console.log('Подключение к серверу:', url);
connect(url);
socket.once('connect', () => {
console.log('Подключено, создаём комнату');
const aiMode = $('ai-mode')?.checked || false;
socket.emit('createRoom', { name, aiMode });
});
socket.once('connect_error', (e) => {
console.error('Ошибка подключения:', e);
showError('Не удалось подключиться к серверу. Проверьте, что сервер запущен.');
});
} else {
console.log('Уже подключен, создаём комнату');
// Уже подключен, сразу отправляем
const aiMode = $('ai-mode')?.checked || false;
socket.emit('createRoom', { name, aiMode });
}
});
// Также добавляем обработчик через onclick для надёжности
newBtn.onclick = function(e) {
e.preventDefault();
e.stopPropagation();
console.log('Кнопка "Создать комнату" нажата (onclick)');
const name = ($('host-name')?.value || '').trim() || 'Игрок 1';
const currentHost = window.location.hostname;
let url = window.location.origin;
if (currentHost !== 'localhost' && currentHost !== '127.0.0.1') {
url = window.location.origin;
}
if (!socket || !socket.connected) {
connect(url);
socket.once('connect', () => {
const aiMode = $('ai-mode')?.checked || false;
socket.emit('createRoom', { name, aiMode });
});
socket.once('connect_error', (e) => {
showError('Не удалось подключиться к серверу. Проверьте, что сервер запущен.');
});
} else {
const aiMode = $('ai-mode')?.checked || false;
socket.emit('createRoom', { name, aiMode });
}
};
} else {
console.error('Кнопка btn-host не найдена! Проверьте, что элемент существует в DOM.');
}
$('btn-join')?.addEventListener('click', () => {
clearError();
const addr = ($('join-addr')?.value || '').trim();
let url;
if (addr && addr !== 'Оставьте пустым для авто') {
const parts = addr.split(':');
const host = parts[0] || 'localhost';
const port = parts[1] || DEFAULT_PORT;
url = `http://${host}:${port}`;
} else {
url = window.location.origin;
}
connect(url);
socket.once('connect', () => {
setLobbyPanel('connect');
$('connect-panel')?.classList.add('active');
});
socket.once('connect_error', () => {
const detailsEl = document.querySelector('#join-panel details');
if (detailsEl && !detailsEl.open) {
detailsEl.open = true;
showError('Не удалось подключиться автоматически. Проверьте расширенные настройки и введите IP сервера вручную.');
}
});
});
var joinCodeHandler = function () {
clearError();
const code = ($('join-code')?.value || '').trim().toUpperCase();
if (!code || code.length !== 5) {
showError('Введите код комнаты (5 символов)');
return;
}
const name = ($('join-name')?.value || '').trim() || 'Игрок 2';
if (!socket || !socket.connected) {
const addr = ($('join-addr')?.value || '').trim();
let url;
if (addr && addr !== 'Оставьте пустым для авто') {
const parts = addr.split(':');
const host = parts[0] || 'localhost';
const port = parts[1] || DEFAULT_PORT;
url = `http://${host}:${port}`;
} else {
url = window.location.origin;
}
connect(url);
socket.once('connect', () => {
socket.emit('joinRoom', { code, name });
});
} else {
socket.emit('joinRoom', { code, name });
}
};
$('btn-join-code')?.addEventListener('click', joinCodeHandler);
$('join-code')?.addEventListener('keypress', function (e) {
if (e.key === 'Enter') joinCodeHandler();
});
$('btn-start')?.addEventListener('click', () => {
clearError();
var faction = $('room-faction')?.value || '';
socket?.emit('startGame', { faction: faction || undefined });
});
$('btn-leave')?.addEventListener('click', () => {
socket?.emit('leaveLobby');
setLobbyPanel('host');
$('host-panel')?.classList.add('active');
showScreen('lobby');
});
$('btn-leave-join')?.addEventListener('click', () => {
socket?.emit('leaveLobby');
socket?.disconnect();
setLobbyPanel('join');
$('join-panel')?.classList.add('active');
showScreen('lobby');
});
$('btn-modal-close')?.addEventListener('click', () => {
$('modal-overlay')?.classList.add('hidden');
const newGameBtn = $('btn-new-game');
if (newGameBtn) newGameBtn.style.display = 'none';
});
$('btn-new-game')?.addEventListener('click', () => {
if (socket && socket.connected) {
socket.emit('resetToLobby');
$('modal-overlay')?.classList.add('hidden');
const newGameBtn = $('btn-new-game');
if (newGameBtn) newGameBtn.style.display = 'none';
}
});
$('btn-refresh-rooms')?.addEventListener('click', () => {
if (socket && socket.connected) {
socket.emit('getRoomsList');
} else {
const url = window.location.origin;
connect(url);
socket.once('connect', () => {
socket.emit('getRoomsList');
});
}
});
// Обработчик списка комнат
window.handleRoomsList = function(roomsList) {
const roomsListEl = $('rooms-list');
if (!roomsListEl) return;
if (!roomsList || roomsList.length === 0) {
roomsListEl.innerHTML = 'Нет доступных игр
';
return;
}
roomsListEl.innerHTML = roomsList.map((room) => {
const status = room.gameStarted ? 'Игра идёт' : 'Ожидание игроков';
const statusColor = room.gameStarted ? 'var(--amber)' : 'var(--cyan)';
const playersText = room.players.length > 0 ? room.players.join(', ') : 'Нет игроков';
const canJoin = !room.gameStarted && room.playerCount < room.maxPlayers;
const actionText = room.gameStarted ? '👁 Наблюдать' : (canJoin ? '▶ Подключиться' : 'Полная');
const actionClass = room.gameStarted ? 'btn-ghost' : (canJoin ? 'btn-primary' : 'btn-ghost');
const actionDisabled = !room.gameStarted && !canJoin;
return `
${escapeHtml(room.code)}
${status}
Игроки: ${room.playerCount}/${room.maxPlayers}
${room.spectators > 0 ? `
Наблюдателей: ${room.spectators}
` : ''}
${room.hasAI ? '
🤖 ИИ
' : ''}
${escapeHtml(playersText)}
`;
}).join('');
// Обработчики подключения к комнатам
$all('.room-item button').forEach(btn => {
btn.addEventListener('click', function() {
const code = this.dataset.roomCode;
const name = ($('browse-name')?.value || '').trim() || 'Игрок';
if (!code) return;
if (!socket || !socket.connected) {
const url = window.location.origin;
connect(url);
socket.once('connect', () => {
socket.emit('joinRoom', { code, name });
});
} else {
socket.emit('joinRoom', { code, name });
}
});
});
};
$('btn-instructions')?.addEventListener('click', () => {
$('instructions-overlay')?.classList.remove('hidden');
});
$('btn-instructions-lobby')?.addEventListener('click', () => {
$('instructions-overlay')?.classList.remove('hidden');
});
$('btn-instructions-close')?.addEventListener('click', () => {
$('instructions-overlay')?.classList.add('hidden');
});
$('btn-cards-gallery')?.addEventListener('click', () => {
$('cards-gallery-overlay')?.classList.remove('hidden');
renderCardsGallery();
});
$('btn-cards-gallery-game')?.addEventListener('click', () => {
$('cards-gallery-overlay')?.classList.remove('hidden');
renderCardsGallery();
});
$('btn-gallery-close')?.addEventListener('click', () => {
$('cards-gallery-overlay')?.classList.add('hidden');
});
$('cards-gallery-overlay')?.addEventListener('click', (e) => {
if (e.target.id === 'cards-gallery-overlay') {
$('cards-gallery-overlay')?.classList.add('hidden');
}
});
$('gallery-faction-filter')?.addEventListener('change', renderCardsGallery);
$('gallery-type-filter')?.addEventListener('change', renderCardsGallery);
$('gallery-search')?.addEventListener('input', renderCardsGallery);
let galleryCardDb = null;
function renderCardsGallery() {
const grid = $('cards-gallery-grid');
if (!grid) return;
if (!galleryCardDb) {
fetch('/api/cards')
.then(res => res.json())
.then(data => {
galleryCardDb = data;
renderCardsGallery();
})
.catch(() => {
grid.innerHTML = 'Не удалось загрузить карты
';
});
return;
}
const factionFilter = ($('gallery-faction-filter')?.value || '').toLowerCase();
const typeFilter = ($('gallery-type-filter')?.value || '').toLowerCase();
const searchText = ($('gallery-search')?.value || '').toLowerCase();
const allCards = Object.keys(galleryCardDb || {}).map(id => ({ id, ...galleryCardDb[id] }));
const filtered = allCards.filter(card => {
if (factionFilter && card.faction !== factionFilter) return false;
if (typeFilter && card.type !== typeFilter) return false;
if (searchText && !card.name.toLowerCase().includes(searchText)) return false;
return true;
});
grid.innerHTML = filtered.map(card => {
const isFictional = card.fictional || false;
const cost = card.cost || 0;
const attack = card.attack !== undefined ? card.attack : '';
const health = card.health !== undefined ? card.health : '';
const stats = card.type === 'minion' ? `${attack}${health}
` : '';
const costDisplay = card.type === 'spell' ? `${cost}
` : '';
const art = getCardArt ? getCardArt(card) : '✦';
return `
${art}
${escapeHtml(card.name)}
${card.text ? `
${escapeHtml(card.text)}
` : ''}
${stats}
${costDisplay}
`;
}).join('');
$all('.gallery-card').forEach(cardEl => {
cardEl.onclick = function() {
const cardId = cardEl.dataset.cardId;
const card = galleryCardDb[cardId];
if (!card) return;
const nameEl = $('card-info-name');
const metaEl = $('card-info-meta');
const textEl = $('card-info-text');
const abilEl = $('card-info-abilities');
const bioEl = $('card-info-bio');
if (nameEl) nameEl.textContent = card.name;
if (metaEl) {
const parts = [];
if (card.type === 'minion') {
parts.push(`Стоимость: ${card.cost || 0}`);
parts.push(`Атака: ${card.attack !== undefined ? card.attack : 0}`);
parts.push(`Здоровье: ${card.health !== undefined ? card.health : 0}`);
} else {
parts.push(`Стоимость: ${card.cost || 0}`);
}
if (card.faction) {
const factionNames = { rebellion: 'Сопротивление', empire: 'Империя', pirates: 'Пираты', mandalorians: 'Мандалорцы', neutral: 'Нейтральная' };
parts.push(`Фракция: ${factionNames[card.faction] || card.faction}`);
}
if (card.legendary) parts.push('Легендарная');
if (card.fictional) parts.push('⚡ ВЫМЫШЛЕННЫЙ ПЕРСОНАЖ');
metaEl.textContent = parts.join(' | ');
}
if (textEl) textEl.textContent = card.text || '';
if (abilEl) {
let abil = '';
if (card.battlecry) abil += `Battlecry: ${card.battlecry} `;
if (card.deathrattle) abil += `Deathrattle: ${card.deathrattle} `;
abilEl.textContent = abil || 'Нет способностей';
}
if (bioEl) bioEl.textContent = card.bio || 'Нет описания';
$('card-info-overlay')?.classList.remove('hidden');
$('cards-gallery-overlay')?.classList.add('hidden');
};
});
}
$('btn-settings')?.addEventListener('click', () => {
$('settings-overlay')?.classList.remove('hidden');
});
$('btn-settings-lobby')?.addEventListener('click', () => {
$('settings-overlay')?.classList.remove('hidden');
});
$('btn-settings-close')?.addEventListener('click', () => {
$('settings-overlay')?.classList.add('hidden');
});
$('settings-overlay')?.addEventListener('click', (e) => {
if (e.target.id === 'settings-overlay') {
$('settings-overlay')?.classList.add('hidden');
}
});
$('btn-forge-close')?.addEventListener('click', () => {
const sidebar = $('forge-sidebar');
if (sidebar) {
sidebar.classList.add('hidden');
forgeSelected = [];
forgePreviewPendingIds = null;
const preview = $('forge-preview');
if (preview) preview.classList.add('hidden');
renderForgeSelected();
updateForgePreview();
}
});
$('btn-forge-clear')?.addEventListener('click', () => {
forgeSelected = [];
forgePreviewPendingIds = null;
renderForgeSelected();
updateForgePreview();
if (gameState) renderForgeHand(gameState);
});
$('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) {
console.error('Steal cards modal elements not found');
return;
}
const currentState = gameState || state;
if (!currentState || !currentState.players) {
console.error('Invalid game state for steal cards modal');
return;
}
// Если противник уже выбран, показываем его доску (карты на столе)
if (stealCardsMode.targetPlayerIndex !== null) {
// Обновляем доску из актуального gameState
const targetPlayer = currentState.players[stealCardsMode.targetPlayerIndex];
if (targetPlayer && targetPlayer.board) {
stealCardsMode.targetDeck = targetPlayer.board.map(m => m.cardId);
} else {
console.warn('Target player not found or board is empty:', stealCardsMode.targetPlayerIndex);
}
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 = targetPlayer.board.map((minion, idx) => {
const cardId = minion.cardId;
const cardDbToUse = currentState?.cardDb || cardDb;
const meta = cardDbToUse[cardId];
if (!meta) {
console.warn('Card not found in cardDb:', cardId, 'Available keys:', Object.keys(cardDbToUse).slice(0, 10));
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 = typeof getCardArt === 'function' ? getCardArt(meta) : '✦';
return ``;
}).join('');
$all('.steal-deck-card').forEach(card => {
const handleCardClick = function(e) {
e.preventDefault();
e.stopPropagation();
const idx = parseInt(card.dataset.boardIndex, 10);
if (isNaN(idx)) {
console.warn('Invalid board index:', card.dataset.boardIndex);
return;
}
const selectedIdx = stealCardsMode.selectedIndices.indexOf(idx);
if (selectedIdx >= 0) {
stealCardsMode.selectedIndices.splice(selectedIdx, 1);
} else if (stealCardsMode.selectedIndices.length < 2) {
stealCardsMode.selectedIndices.push(idx);
}
const currentState = gameState || state;
showStealCardsModal(currentState, data || {
targetPlayerIndex: stealCardsMode.targetPlayerIndex,
targetPlayerName: currentState?.players?.[stealCardsMode.targetPlayerIndex]?.name || `Игрок ${stealCardsMode.targetPlayerIndex + 1}`,
targetBoardSize: currentState?.players?.[stealCardsMode.targetPlayerIndex]?.board?.length || 0,
maxCards: Math.min(2, currentState?.players?.[stealCardsMode.targetPlayerIndex]?.board?.length || 0)
});
};
card.onclick = handleCardClick;
card.style.pointerEvents = 'auto';
if (isTouchDevice()) {
card.ontouchstart = function(e) {
e.preventDefault();
handleCardClick(e);
};
}
});
selectedEl.innerHTML = stealCardsMode.selectedIndices.length ? stealCardsMode.selectedIndices.map(idx => {
const cardId = stealCardsMode.targetDeck[idx];
const meta = cardDb[cardId];
if (!meta) return '';
return ``;
}).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 = currentState.players.filter((p, i) => i !== currentState.yourIndex && p.health > 0 && p.deck && p.deck.length > 0);
if (enemies.length === 0) {
if (hintEl) hintEl.textContent = 'Нет доступных противников для грабежа';
targetSelect.innerHTML = '';
return;
}
targetSelect.innerHTML = enemies.map((enemy, idx) => {
const enemyIdx = currentState.players.indexOf(enemy);
return `
${escapeHtml(enemy.name || `Игрок ${enemyIdx + 1}`)}
Колода: ${enemy.deck.length} карт
`;
}).join('');
$all('.steal-target-option').forEach(option => {
const handleOptionClick = function(e) {
e.preventDefault();
e.stopPropagation();
const playerIdx = parseInt(option.dataset.playerIndex, 10);
if (isNaN(playerIdx)) return;
const currentState = gameState || state;
if (!currentState || !currentState.players) {
console.error('Invalid state when selecting target player');
return;
}
const targetPlayer = currentState.players[playerIdx];
if (targetPlayer && targetPlayer.board && targetPlayer.board.length > 0) {
stealCardsMode.targetPlayerIndex = playerIdx;
stealCardsMode.targetDeck = targetPlayer.board ? targetPlayer.board.map(m => m.cardId) : [];
console.log('Selected target player:', playerIdx, 'Board size:', stealCardsMode.targetDeck.length);
showStealCardsModal(currentState, {
targetPlayerIndex: playerIdx,
targetPlayerName: targetPlayer.name || `Игрок ${playerIdx + 1}`,
targetBoardSize: targetPlayer.board.length,
maxCards: Math.min(2, targetPlayer.board.length)
});
} else {
console.warn('Target player has no board or is invalid:', playerIdx, targetPlayer);
}
};
option.onclick = handleOptionClick;
option.style.pointerEvents = 'auto';
option.style.cursor = 'pointer';
if (isTouchDevice()) {
option.ontouchstart = function(e) {
e.preventDefault();
handleOptionClick(e);
};
}
});
}
$('steal-cards-overlay')?.classList.remove('hidden');
}
let forgeSelected = [];
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 = 'Кликните на карты из руки
';
} else {
selectedEl.innerHTML = forgeSelected.map((cardId, idx) => {
const meta = cardDb[cardId];
if (!meta) return '';
return ``;
}).filter(Boolean).join('');
// Добавляем обработчики удаления
$all('.forge-selected-card').forEach(card => {
card.onclick = function(e) {
e.stopPropagation();
const cardId = card.dataset.cardId;
removeCardFromForge(cardId);
};
});
}
}
function updateForgePreview() {
const preview = $('forge-preview');
const wrap = $('forge-preview-card-wrap');
const confirmBtn = $('btn-forge-confirm');
if (!preview) return;
if (forgeSelected.length < 2 || forgeSelected.length > 3) {
preview.classList.add('hidden');
if (wrap) wrap.innerHTML = '';
if (confirmBtn) confirmBtn.disabled = true;
forgePreviewPendingIds = null;
return;
}
forgePreviewRequestId = Date.now();
forgePreviewPendingIds = [...forgeSelected];
if (wrap) wrap.innerHTML = 'Загрузка…
';
if (confirmBtn) confirmBtn.disabled = true;
preview.classList.remove('hidden');
socket.emit('forgePreview', { cardIds: forgePreviewPendingIds, requestId: forgePreviewRequestId });
}
function addCardToForge(cardId, source) {
if (!cardId) return;
if (source !== 'hand') return; // только из руки
if (forgeSelected.includes(cardId)) {
removeCardFromForge(cardId);
return;
}
if (forgeSelected.length >= 3) {
// Показываем сообщение, что максимум 3 карты
const selectedEl = $('forge-selected');
if (selectedEl) {
selectedEl.style.animation = 'shake 0.5s';
setTimeout(() => selectedEl.style.animation = '', 500);
}
return;
}
const meta = cardDb[cardId];
if (!meta || meta.type !== 'minion') return; // Только миньоны
forgeSelected.push(cardId);
renderForgeSelected();
updateForgePreview();
if (gameState) renderForgeHand(gameState);
}
function removeCardFromForge(cardId) {
const idx = forgeSelected.indexOf(cardId);
if (idx >= 0) {
forgeSelected.splice(idx, 1);
renderForgeSelected();
updateForgePreview();
if (gameState) renderForgeHand(gameState);
}
}
function renderForgeHand(state) {
const you = state.players[state.yourIndex];
const handList = $('forge-hand-list');
if (!handList) return;
const hand = you.hand || state.yourHand || [];
if (!hand || hand.length === 0) {
handList.innerHTML = 'Рука пуста
';
return;
}
// Фильтруем только миньонов
const minionCards = hand.filter(cardId => {
const meta = cardDb[cardId];
return meta && meta.type === 'minion';
});
if (minionCards.length === 0) {
handList.innerHTML = 'Нет миньонов в руке
';
return;
}
handList.innerHTML = minionCards.map((cardId) => {
const meta = cardDb[cardId];
if (!meta) return '';
const isSelected = forgeSelected.includes(cardId);
return ``;
}).filter(Boolean).join('');
// Обработчики клика для карт из руки
$all('#forge-hand-list .card-wrap').forEach(card => {
card.onclick = function(e) {
e.stopPropagation();
const cardId = card.dataset.cardId;
if (cardId) {
addCardToForge(cardId, 'hand');
}
};
});
}
const confirmBtn = $('btn-forge-confirm');
if (confirmBtn) {
confirmBtn.onclick = function() {
if (confirmBtn.disabled || !forgePreviewPendingIds || forgePreviewPendingIds.length < 2) return;
const you = gameState?.players?.[gameState?.yourIndex];
if (!you || (you.mana || 0) < 2) return;
socket.emit('forgeCard', { cardIds: forgePreviewPendingIds });
if (typeof window.Sounds !== 'undefined' && window.Sounds.forge) window.Sounds.forge();
forgeSelected = [];
forgePreviewPendingIds = null;
const preview = $('forge-preview');
if (preview) preview.classList.add('hidden');
renderForgeSelected();
updateForgePreview();
if (gameState) renderForgeHand(gameState);
const sidebar = $('forge-sidebar');
if (sidebar) sidebar.classList.add('hidden');
};
}
const cancelPreviewBtn = $('btn-forge-cancel-preview');
if (cancelPreviewBtn) {
cancelPreviewBtn.onclick = function() {
const preview = $('forge-preview');
if (preview) preview.classList.add('hidden');
forgePreviewPendingIds = null;
const wrap = $('forge-preview-card-wrap');
if (wrap) wrap.innerHTML = '';
const cb = $('btn-forge-confirm');
if (cb) cb.disabled = true;
};
}
$('music-toggle')?.addEventListener('click', () => {
if (window.Music) {
window.Music.toggle();
}
});
$('music-volume-slider')?.addEventListener('input', (e) => {
const volume = parseFloat(e.target.value) / 100;
if (window.Music) {
window.Music.setVolume(volume);
}
});
}
function init() {
initLobby();
showScreen('lobby');
socket = null;
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
init();
if (window.Music) {
window.Music.init();
}
});
} else {
init();
if (window.Music) {
window.Music.init();
}
}
})();