123
This commit is contained in:
563
public/game.js
563
public/game.js
@ -179,6 +179,20 @@
|
||||
if (data.warn && typeof window.Sounds !== 'undefined') window.Sounds.timerWarning();
|
||||
if (data.ended && typeof window.Sounds !== 'undefined') window.Sounds.timerEnd();
|
||||
});
|
||||
socket.on('chatMessage', (data) => {
|
||||
const chatMessagesEl = $('chat-messages');
|
||||
if (chatMessagesEl && data) {
|
||||
const time = new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
|
||||
const messageEl = document.createElement('div');
|
||||
messageEl.className = 'chat-message user';
|
||||
messageEl.innerHTML = '<span class="chat-username">' + escapeHtml(data.playerName || 'Игрок') + ':</span> ' +
|
||||
escapeHtml(data.message || '') +
|
||||
' <span class="chat-time">' + escapeHtml(time) + '</span>';
|
||||
chatMessagesEl.appendChild(messageEl);
|
||||
chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight;
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('gameState', (state) => {
|
||||
if (combatTimeout) {
|
||||
clearTimeout(combatTimeout);
|
||||
@ -192,6 +206,19 @@
|
||||
}
|
||||
prevGameState = gameState && state.phase !== 'ended' ? { ...gameState, players: gameState.players.map((p) => ({ ...p, board: (p.board || []).map((m) => ({ ...m })) })) } : null;
|
||||
showScreen('game');
|
||||
|
||||
// Инициализация чата при первом запуске игры
|
||||
if (wasLobby) {
|
||||
const chatMessagesEl = $('chat-messages');
|
||||
if (chatMessagesEl) {
|
||||
chatMessagesEl.innerHTML = '';
|
||||
const welcomeMsg = document.createElement('div');
|
||||
welcomeMsg.className = 'chat-message system';
|
||||
welcomeMsg.textContent = 'Игра началась!';
|
||||
chatMessagesEl.appendChild(welcomeMsg);
|
||||
}
|
||||
}
|
||||
|
||||
gameState = state;
|
||||
cardDb = state.cardDb || {};
|
||||
yourIndex = state.yourIndex ?? 0;
|
||||
@ -673,30 +700,53 @@
|
||||
}
|
||||
$('your-deck').textContent = state.yourDeckCount ?? you.deck?.length ?? 0;
|
||||
const lastLog = state.log && state.log.length ? state.log[state.log.length - 1] : null;
|
||||
const logEl = $('game-log');
|
||||
if (logEl) {
|
||||
function logLine(entry) {
|
||||
if (!entry) return '';
|
||||
var who = function (i) { return state.players[i]?.name || ('Игрок ' + (i + 1)); };
|
||||
|
||||
// Добавляем новые логи в чат
|
||||
if (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') {
|
||||
var c = cardDb[entry.cardId];
|
||||
return who(entry.playerIndex) + ' разыграл ' + (c?.name || entry.cardId);
|
||||
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 (entry.type === 'turn') return 'Ход: ' + who(entry.to);
|
||||
if (entry.type === 'skipTurn') return who(entry.playerIndex) + ' пропускает ход';
|
||||
if (entry.type === 'playerDefeated') return who(entry.playerIndex) + ' выбыл из игры';
|
||||
if (entry.type === 'attackHero') return 'Атака по герою!';
|
||||
if (entry.type === 'attack') return 'Бой!';
|
||||
if (entry.type === 'draw') return who(entry.playerIndex) + ' вытянул карту';
|
||||
if (entry.type === 'fatigue') return who(entry.playerIndex) + ' усталость (' + (entry.damage || 0) + ' урона)';
|
||||
if (entry.type === 'spell') return who(entry.fromPlayer) + ' заклинание';
|
||||
if (entry.type === 'heroAbility') return who(entry.fromPlayer) + ' — геройская способность';
|
||||
if (entry.type === 'battlecry' || entry.type === 'deathrattle') return '';
|
||||
return '';
|
||||
|
||||
if (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 = '<span class="chat-time">' + escapeHtml(time) + '</span> ' + escapeHtml(message);
|
||||
chatMessagesEl.appendChild(messageEl);
|
||||
chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
var lines = (state.log || []).slice(-10).map(logLine).filter(Boolean);
|
||||
logEl.innerHTML = lines.map(function (s) { return '<div class="game-log-line">' + escapeHtml(s) + '</div>'; }).join('');
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
if (lastLog?.type === 'attackHero' && typeof window.Sounds !== 'undefined') window.Sounds.attack();
|
||||
|
||||
@ -747,10 +797,18 @@
|
||||
var meta = cardDb[cid];
|
||||
var cost = meta?.cost ?? 0;
|
||||
var playable = false;
|
||||
if (meta?.type === 'minion') playable = you.mana >= cost && (you.board?.length ?? 0) < 7;
|
||||
else if (meta?.type === 'spell') playable = you.mana >= cost && !attackMode.active && !spellMode.active && !heroAbilityMode.active;
|
||||
var notEnoughMana = false;
|
||||
if (isYourTurn) {
|
||||
if (meta?.type === 'minion') {
|
||||
playable = you.mana >= cost && (you.board?.length ?? 0) < 7;
|
||||
notEnoughMana = you.mana < cost;
|
||||
} else if (meta?.type === 'spell') {
|
||||
playable = you.mana >= cost && !attackMode.active && !spellMode.active && !heroAbilityMode.active;
|
||||
notEnoughMana = you.mana < cost;
|
||||
}
|
||||
}
|
||||
var drawAnim = justDrew && i === hand.length - 1;
|
||||
return renderHandCard(cid, i, hand.length, playable, drawAnim);
|
||||
return renderHandCard(cid, i, hand.length, playable, drawAnim, notEnoughMana);
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
@ -789,7 +847,7 @@
|
||||
return '<div class="card-abilities">' + tags.join(' ') + '</div>';
|
||||
}
|
||||
|
||||
function renderHandCard(cardId, handIndex, handSize, playable, drawFromDeck) {
|
||||
function renderHandCard(cardId, handIndex, handSize, playable, drawFromDeck, notEnoughMana) {
|
||||
var meta = cardDb[cardId];
|
||||
if (!meta) return '';
|
||||
var name = meta.name || cardId;
|
||||
@ -808,7 +866,8 @@
|
||||
var textHtml = meta.text ? '<div class="card-text">' + escapeHtml(meta.text) + '</div>' : '';
|
||||
var abilHtml = abilityTags(meta);
|
||||
var fictionalClass = meta.fictional ? ' fictional-card' : '';
|
||||
return '<div class="card-wrap in-hand ' + (playable ? 'playable' : '') + ' ' + (drawFromDeck ? 'draw-from-deck' : '') + fictionalClass + '" data-hand-index="' + handIndex + '" data-card-id="' + cardId + '" style="--hand-rotate: ' + rotate + 'deg" title="' + escapeHtml(name) + (meta.text ? ': ' + meta.text : '') + (meta.fictional ? ' [ВЫМЫШЛЕННЫЙ]' : '') + '">'
|
||||
var notEnoughManaClass = notEnoughMana ? ' not-enough-mana' : '';
|
||||
return '<div class="card-wrap in-hand ' + (playable ? 'playable' : '') + ' ' + (drawFromDeck ? 'draw-from-deck' : '') + fictionalClass + notEnoughManaClass + '" data-hand-index="' + handIndex + '" data-card-id="' + cardId + '" style="--hand-rotate: ' + rotate + 'deg" title="' + escapeHtml(name) + (meta.text ? ': ' + meta.text : '') + (meta.fictional ? ' [ВЫМЫШЛЕННЫЙ]' : '') + (notEnoughMana ? ' (Недостаточно маны)' : '') + '">'
|
||||
+ '<div class="card faction-' + faction + fictionalClass + '">'
|
||||
+ '<div class="card-art">' + art + '</div>'
|
||||
+ '<div class="card-info">'
|
||||
@ -979,6 +1038,54 @@
|
||||
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);
|
||||
@ -1549,8 +1656,11 @@
|
||||
const sidebar = $('forge-sidebar');
|
||||
if (sidebar) {
|
||||
sidebar.classList.remove('hidden');
|
||||
forgeSelected = []; // Сбрасываем выбор при открытии
|
||||
renderForgeHand(state);
|
||||
renderForgeDeck(state);
|
||||
setupForgeDragAndDrop(state);
|
||||
renderForgeSelected();
|
||||
updateForgeCraftButton();
|
||||
}
|
||||
};
|
||||
} else if (forgeBtn) {
|
||||
@ -1728,9 +1838,12 @@
|
||||
yourBoardEl.ondrop = null;
|
||||
}
|
||||
|
||||
// Настраиваем drag-and-drop для кузницы, если она открыта
|
||||
// Обновляем кузницу, если она открыта
|
||||
if (isForgeOpen) {
|
||||
setTimeout(() => setupForgeDragAndDrop(state), 100);
|
||||
renderForgeHand(state);
|
||||
renderForgeDeck(state);
|
||||
renderForgeSelected();
|
||||
updateForgeCraftButton();
|
||||
}
|
||||
}
|
||||
|
||||
@ -2050,10 +2163,16 @@
|
||||
sidebar.classList.add('hidden');
|
||||
forgeSelected = [];
|
||||
renderForgeSelected();
|
||||
// Обновляем отображение руки и колоды
|
||||
if (gameState) {
|
||||
renderHand(gameState);
|
||||
}
|
||||
});
|
||||
|
||||
$('btn-forge-clear')?.addEventListener('click', () => {
|
||||
forgeSelected = [];
|
||||
renderForgeSelected();
|
||||
updateForgeCraftButton();
|
||||
if (gameState) {
|
||||
renderForgeHand(gameState);
|
||||
renderForgeDeck(gameState);
|
||||
}
|
||||
});
|
||||
|
||||
@ -2211,20 +2330,24 @@
|
||||
|
||||
function renderForgeSelected() {
|
||||
const selectedEl = $('forge-selected');
|
||||
const countEl = $('forge-selected-count');
|
||||
if (!selectedEl) return;
|
||||
|
||||
if (countEl) {
|
||||
countEl.textContent = forgeSelected.length;
|
||||
}
|
||||
|
||||
if (forgeSelected.length === 0) {
|
||||
selectedEl.innerHTML = '<div class="forge-selected-empty">Перетащите карты сюда</div>';
|
||||
selectedEl.classList.remove('drag-over');
|
||||
selectedEl.innerHTML = '<div class="forge-selected-empty">Кликните на карты чтобы выбрать</div>';
|
||||
} else {
|
||||
selectedEl.innerHTML = forgeSelected.map((cardId, idx) => {
|
||||
const meta = cardDb[cardId];
|
||||
if (!meta) return '';
|
||||
return `<div class="card-wrap forge-selected-card" data-card-id="${cardId}" data-forge-index="${idx}" style="width: 80px; height: 112px; position: relative;">
|
||||
return `<div class="card-wrap forge-selected-card" data-card-id="${cardId}" data-forge-index="${idx}" style="width: 90px; height: 126px; position: relative;">
|
||||
<div class="card faction-${meta.faction || 'neutral'}">
|
||||
<div class="card-art">${getCardArt(meta)}</div>
|
||||
<div class="card-info">
|
||||
<div class="card-name" style="font-size: 0.6rem;">${escapeHtml(meta.name)}</div>
|
||||
<div class="card-name" style="font-size: 0.7rem;">${escapeHtml(meta.name)}</div>
|
||||
${meta.type === 'minion' ? `<div class="card-stats"><span class="atk">${meta.attack || 0}</span><span class="hp">${meta.health || 0}</span></div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
@ -2236,17 +2359,7 @@
|
||||
card.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
const cardId = card.dataset.cardId;
|
||||
const idx = forgeSelected.indexOf(cardId);
|
||||
if (idx >= 0) {
|
||||
forgeSelected.splice(idx, 1);
|
||||
renderForgeSelected();
|
||||
updateForgeCraftButton();
|
||||
// Обновляем отображение в источнике
|
||||
if (gameState) {
|
||||
renderForgeDeck(gameState);
|
||||
renderHand(gameState);
|
||||
}
|
||||
}
|
||||
removeCardFromForge(cardId);
|
||||
};
|
||||
});
|
||||
}
|
||||
@ -2256,13 +2369,18 @@
|
||||
const craftBtn = $('btn-forge-craft');
|
||||
const you = gameState?.players?.[gameState?.yourIndex];
|
||||
if (craftBtn && you) {
|
||||
craftBtn.disabled = forgeSelected.length < 2 || forgeSelected.length > 3 || (you.mana || 0) < 2;
|
||||
const canCraft = forgeSelected.length >= 2 && forgeSelected.length <= 3 && (you.mana || 0) >= 2;
|
||||
craftBtn.disabled = !canCraft;
|
||||
}
|
||||
}
|
||||
|
||||
function addCardToForge(cardId, source) {
|
||||
if (!cardId) return;
|
||||
if (forgeSelected.includes(cardId)) return; // Уже добавлена
|
||||
if (forgeSelected.includes(cardId)) {
|
||||
// Если карта уже выбрана, убираем её
|
||||
removeCardFromForge(cardId);
|
||||
return;
|
||||
}
|
||||
if (forgeSelected.length >= 3) {
|
||||
// Показываем сообщение, что максимум 3 карты
|
||||
const selectedEl = $('forge-selected');
|
||||
@ -2270,7 +2388,7 @@
|
||||
selectedEl.style.animation = 'shake 0.5s';
|
||||
setTimeout(() => selectedEl.style.animation = '', 500);
|
||||
}
|
||||
return; // Максимум 3 карты
|
||||
return;
|
||||
}
|
||||
|
||||
const meta = cardDb[cardId];
|
||||
@ -2280,272 +2398,59 @@
|
||||
renderForgeSelected();
|
||||
updateForgeCraftButton();
|
||||
|
||||
// Обновляем отображение в источнике (рука или колода)
|
||||
// Обновляем отображение в источниках
|
||||
if (gameState) {
|
||||
if (source === 'hand') {
|
||||
renderHand(gameState);
|
||||
} else if (source === 'deck') {
|
||||
renderForgeHand(gameState);
|
||||
renderForgeDeck(gameState);
|
||||
}
|
||||
}
|
||||
|
||||
// Настраиваем drag-and-drop заново после добавления
|
||||
setTimeout(() => setupForgeDragAndDrop(gameState), 100);
|
||||
function removeCardFromForge(cardId) {
|
||||
const idx = forgeSelected.indexOf(cardId);
|
||||
if (idx >= 0) {
|
||||
forgeSelected.splice(idx, 1);
|
||||
renderForgeSelected();
|
||||
updateForgeCraftButton();
|
||||
// Обновляем отображение в источниках
|
||||
if (gameState) {
|
||||
renderForgeHand(gameState);
|
||||
renderForgeDeck(gameState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setupForgeDragAndDrop(state) {
|
||||
const selectedEl = $('forge-selected');
|
||||
const sidebar = $('forge-sidebar');
|
||||
if (!selectedEl || !sidebar || sidebar.classList.contains('hidden')) return;
|
||||
|
||||
// Обработка drop в область выбранных карт
|
||||
const handleDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
selectedEl.classList.add('drag-over');
|
||||
};
|
||||
|
||||
const handleDragLeave = (e) => {
|
||||
if (!selectedEl.contains(e.relatedTarget)) {
|
||||
selectedEl.classList.remove('drag-over');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
selectedEl.classList.remove('drag-over');
|
||||
|
||||
let cardId = null;
|
||||
let source = null;
|
||||
|
||||
// Проверяем данные из dataTransfer
|
||||
const data = e.dataTransfer.getData('text/plain');
|
||||
if (data) {
|
||||
if (data.startsWith('hand:')) {
|
||||
const handIndex = parseInt(data.split(':')[1], 10);
|
||||
function renderForgeHand(state) {
|
||||
const you = state.players[state.yourIndex];
|
||||
if (you.hand && you.hand[handIndex]) {
|
||||
cardId = you.hand[handIndex];
|
||||
source = 'hand';
|
||||
}
|
||||
} else {
|
||||
cardId = data;
|
||||
source = e.dataTransfer.getData('source') || 'deck';
|
||||
}
|
||||
const handList = $('forge-hand-list');
|
||||
if (!handList) return;
|
||||
|
||||
const hand = you.hand || state.yourHand || [];
|
||||
|
||||
if (!hand || hand.length === 0) {
|
||||
handList.innerHTML = '<div style="color: #94a3b8; text-align: center; padding: 2rem; font-size: 0.9rem;">Рука пуста</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (cardId) {
|
||||
addCardToForge(cardId, source);
|
||||
}
|
||||
};
|
||||
|
||||
// Удаляем старые обработчики, если есть
|
||||
selectedEl.removeEventListener('dragover', handleDragOver);
|
||||
selectedEl.removeEventListener('dragleave', handleDragLeave);
|
||||
selectedEl.removeEventListener('drop', handleDrop);
|
||||
|
||||
// Добавляем новые обработчики
|
||||
selectedEl.addEventListener('dragover', handleDragOver);
|
||||
selectedEl.addEventListener('dragleave', handleDragLeave);
|
||||
selectedEl.addEventListener('drop', handleDrop);
|
||||
|
||||
// Поддержка touch для мобильных устройств
|
||||
if (isTouchDevice()) {
|
||||
let touchStartCard = null;
|
||||
let touchStartSource = null;
|
||||
let touchStartElement = null;
|
||||
|
||||
// Обработка touch для карт в руке - упрощённая версия: просто тап добавляет карту
|
||||
$all('#your-hand .card-wrap').forEach(cardWrap => {
|
||||
const cardId = cardWrap.dataset.cardId;
|
||||
// Фильтруем только миньонов
|
||||
const minionCards = hand.filter(cardId => {
|
||||
const meta = cardDb[cardId];
|
||||
if (cardId && meta && meta.type === 'minion') {
|
||||
// Удаляем старые обработчики
|
||||
cardWrap.ontouchstart = null;
|
||||
cardWrap.ontouchend = null;
|
||||
|
||||
cardWrap.ontouchstart = function(e) {
|
||||
if (sidebar && !sidebar.classList.contains('hidden')) {
|
||||
touchStartCard = cardId;
|
||||
touchStartSource = 'hand';
|
||||
touchStartElement = cardWrap;
|
||||
cardWrap._touchStartTime = Date.now();
|
||||
cardWrap.classList.add('touch-active');
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
cardWrap.ontouchend = function(e) {
|
||||
if (touchStartCard === cardId && sidebar && !sidebar.classList.contains('hidden')) {
|
||||
const touch = e.changedTouches[0];
|
||||
const timeDiff = Date.now() - (touchStartElement?._touchStartTime || Date.now());
|
||||
|
||||
// Если это быстрый тап (менее 300мс), просто добавляем карту
|
||||
if (timeDiff < 300) {
|
||||
addCardToForge(cardId, 'hand');
|
||||
} else {
|
||||
// Иначе проверяем, куда перетащили
|
||||
const dropEl = document.elementFromPoint(touch.clientX, touch.clientY);
|
||||
if (dropEl && (dropEl === selectedEl || selectedEl.contains(dropEl) ||
|
||||
dropEl.closest('#forge-selected'))) {
|
||||
addCardToForge(cardId, 'hand');
|
||||
}
|
||||
}
|
||||
|
||||
cardWrap.classList.remove('touch-active');
|
||||
touchStartCard = null;
|
||||
touchStartSource = null;
|
||||
touchStartElement = null;
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
cardWrap.ontouchcancel = function() {
|
||||
if (touchStartCard === cardId) {
|
||||
cardWrap.classList.remove('touch-active');
|
||||
touchStartCard = null;
|
||||
touchStartSource = null;
|
||||
touchStartElement = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
return meta && meta.type === 'minion';
|
||||
});
|
||||
|
||||
// Обработка touch для карт в колоде - упрощённая версия
|
||||
$all('.forge-deck-card').forEach(cardWrap => {
|
||||
const cardId = cardWrap.dataset.cardId;
|
||||
if (cardId) {
|
||||
// Удаляем старые обработчики
|
||||
cardWrap.ontouchstart = null;
|
||||
cardWrap.ontouchend = null;
|
||||
|
||||
cardWrap.ontouchstart = function(e) {
|
||||
touchStartCard = cardId;
|
||||
touchStartSource = 'deck';
|
||||
touchStartElement = cardWrap;
|
||||
cardWrap._touchStartTime = Date.now();
|
||||
cardWrap.classList.add('touch-active');
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
cardWrap.ontouchend = function(e) {
|
||||
if (touchStartCard === cardId) {
|
||||
const touch = e.changedTouches[0];
|
||||
const timeDiff = Date.now() - (touchStartElement?._touchStartTime || Date.now());
|
||||
|
||||
// Если это быстрый тап (менее 300мс), просто добавляем карту
|
||||
if (timeDiff < 300) {
|
||||
addCardToForge(cardId, 'deck');
|
||||
} else {
|
||||
// Иначе проверяем, куда перетащили
|
||||
const dropEl = document.elementFromPoint(touch.clientX, touch.clientY);
|
||||
if (dropEl && (dropEl === selectedEl || selectedEl.contains(dropEl) ||
|
||||
dropEl.closest('#forge-selected'))) {
|
||||
addCardToForge(cardId, 'deck');
|
||||
}
|
||||
if (minionCards.length === 0) {
|
||||
handList.innerHTML = '<div style="color: #94a3b8; text-align: center; padding: 2rem; font-size: 0.9rem;">Нет миньонов в руке</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
cardWrap.classList.remove('touch-active');
|
||||
touchStartCard = null;
|
||||
touchStartSource = null;
|
||||
touchStartElement = null;
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
cardWrap.ontouchcancel = function() {
|
||||
if (touchStartCard === cardId) {
|
||||
cardWrap.classList.remove('touch-active');
|
||||
touchStartCard = null;
|
||||
touchStartSource = null;
|
||||
touchStartElement = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Также добавляем обработчик на саму область выбранных карт для визуальной обратной связи
|
||||
selectedEl.ontouchstart = function(e) {
|
||||
selectedEl.classList.add('drag-over');
|
||||
};
|
||||
|
||||
selectedEl.ontouchend = function(e) {
|
||||
selectedEl.classList.remove('drag-over');
|
||||
};
|
||||
}
|
||||
|
||||
// Делаем карты в руке перетаскиваемыми в кузницу
|
||||
$all('#your-hand .card-wrap').forEach(cardWrap => {
|
||||
const cardId = cardWrap.dataset.cardId;
|
||||
handList.innerHTML = minionCards.map((cardId) => {
|
||||
const meta = cardDb[cardId];
|
||||
if (cardId && meta && meta.type === 'minion') {
|
||||
// Сохраняем оригинальный draggable, если он был
|
||||
const originalDraggable = cardWrap.draggable;
|
||||
const originalOndragstart = cardWrap.ondragstart;
|
||||
|
||||
// Добавляем дополнительный обработчик для кузницы
|
||||
const forgeDragStart = (e) => {
|
||||
// Проверяем, открыта ли кузница
|
||||
if (sidebar && !sidebar.classList.contains('hidden')) {
|
||||
e.dataTransfer.setData('text/plain', cardId);
|
||||
e.dataTransfer.setData('source', 'hand');
|
||||
cardWrap.classList.add('dragging');
|
||||
}
|
||||
};
|
||||
|
||||
cardWrap.addEventListener('dragstart', forgeDragStart);
|
||||
|
||||
// Сохраняем ссылку для последующего удаления
|
||||
cardWrap._forgeDragStart = forgeDragStart;
|
||||
}
|
||||
});
|
||||
|
||||
// Делаем карты в колоде перетаскиваемыми
|
||||
$all('.forge-deck-card').forEach(cardWrap => {
|
||||
const cardId = cardWrap.dataset.cardId;
|
||||
if (cardId) {
|
||||
cardWrap.draggable = true;
|
||||
|
||||
const deckDragStart = (e) => {
|
||||
e.dataTransfer.setData('text/plain', cardId);
|
||||
e.dataTransfer.setData('source', 'deck');
|
||||
cardWrap.classList.add('dragging');
|
||||
};
|
||||
|
||||
const deckDragEnd = () => {
|
||||
cardWrap.classList.remove('dragging');
|
||||
};
|
||||
|
||||
// Удаляем старые обработчики
|
||||
cardWrap.removeEventListener('dragstart', cardWrap._deckDragStart);
|
||||
cardWrap.removeEventListener('dragend', cardWrap._deckDragEnd);
|
||||
|
||||
// Добавляем новые
|
||||
cardWrap.addEventListener('dragstart', deckDragStart);
|
||||
cardWrap.addEventListener('dragend', deckDragEnd);
|
||||
|
||||
// Сохраняем ссылки
|
||||
cardWrap._deckDragStart = deckDragStart;
|
||||
cardWrap._deckDragEnd = deckDragEnd;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderForgeDeck(state) {
|
||||
const you = state.players[state.yourIndex];
|
||||
const deckList = $('forge-deck-list');
|
||||
if (!deckList) return;
|
||||
|
||||
deckList.innerHTML = you.deck.map((cardId, idx) => {
|
||||
const meta = cardDb[cardId];
|
||||
if (!meta || meta.type !== 'minion') return '';
|
||||
if (!meta) return '';
|
||||
const isSelected = forgeSelected.includes(cardId);
|
||||
return `<div class="card-wrap forge-deck-card ${isSelected ? 'selected' : ''}" data-card-id="${cardId}" data-deck-index="${idx}" draggable="true" style="width: 90px; height: 126px; cursor: grab;">
|
||||
return `<div class="card-wrap ${isSelected ? 'selected' : ''}" data-card-id="${cardId}" style="width: 100px; height: 140px; cursor: pointer;">
|
||||
<div class="card faction-${meta.faction || 'neutral'}">
|
||||
<div class="card-art">${getCardArt(meta)}</div>
|
||||
<div class="card-info">
|
||||
<div class="card-name" style="font-size: 0.65rem;">${escapeHtml(meta.name)}</div>
|
||||
<div class="card-name" style="font-size: 0.7rem;">${escapeHtml(meta.name)}</div>
|
||||
<div class="card-stats">
|
||||
<span class="atk">${meta.attack || 0}</span>
|
||||
<span class="hp">${meta.health || 0}</span>
|
||||
@ -2555,22 +2460,67 @@
|
||||
</div>`;
|
||||
}).filter(Boolean).join('');
|
||||
|
||||
// Клик по карте в колоде тоже добавляет её (для десктопа)
|
||||
// На мобильных это обрабатывается через touch события
|
||||
if (!isTouchDevice()) {
|
||||
$all('.forge-deck-card').forEach(card => {
|
||||
card.onclick = function() {
|
||||
// Обработчики клика для карт из руки
|
||||
$all('#forge-hand-list .card-wrap').forEach(card => {
|
||||
card.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
const cardId = card.dataset.cardId;
|
||||
addCardToForge(cardId, 'deck');
|
||||
if (cardId) {
|
||||
addCardToForge(cardId, 'hand');
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
renderForgeSelected();
|
||||
updateForgeCraftButton();
|
||||
function renderForgeDeck(state) {
|
||||
const deck = state.yourDeck || [];
|
||||
const deckList = $('forge-deck-list');
|
||||
if (!deckList) return;
|
||||
|
||||
// Настраиваем drag-and-drop после рендеринга
|
||||
setTimeout(() => setupForgeDragAndDrop(state), 100);
|
||||
if (!deck || deck.length === 0) {
|
||||
deckList.innerHTML = '<div style="color: #94a3b8; text-align: center; padding: 2rem; font-size: 0.9rem;">Колода пуста</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Фильтруем только миньонов
|
||||
const minionCards = deck.filter(cardId => {
|
||||
const meta = cardDb[cardId];
|
||||
return meta && meta.type === 'minion';
|
||||
});
|
||||
|
||||
if (minionCards.length === 0) {
|
||||
deckList.innerHTML = '<div style="color: #94a3b8; text-align: center; padding: 2rem; font-size: 0.9rem;">Нет миньонов в колоде</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
deckList.innerHTML = minionCards.map((cardId) => {
|
||||
const meta = cardDb[cardId];
|
||||
if (!meta) return '';
|
||||
const isSelected = forgeSelected.includes(cardId);
|
||||
return `<div class="card-wrap ${isSelected ? 'selected' : ''}" data-card-id="${cardId}" style="width: 100px; height: 140px; cursor: pointer;">
|
||||
<div class="card faction-${meta.faction || 'neutral'}">
|
||||
<div class="card-art">${getCardArt(meta)}</div>
|
||||
<div class="card-info">
|
||||
<div class="card-name" style="font-size: 0.7rem;">${escapeHtml(meta.name)}</div>
|
||||
<div class="card-stats">
|
||||
<span class="atk">${meta.attack || 0}</span>
|
||||
<span class="hp">${meta.health || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).filter(Boolean).join('');
|
||||
|
||||
// Обработчики клика для карт из колоды
|
||||
$all('#forge-deck-list .card-wrap').forEach(card => {
|
||||
card.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
const cardId = card.dataset.cardId;
|
||||
if (cardId) {
|
||||
addCardToForge(cardId, 'deck');
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Обновляем drag-and-drop при каждом обновлении руки, если кузница открыта
|
||||
@ -2587,6 +2537,7 @@
|
||||
socket.emit('forgeCard', { cardIds: forgeSelected });
|
||||
forgeSelected = [];
|
||||
renderForgeSelected();
|
||||
updateForgeCraftButton();
|
||||
const sidebar = $('forge-sidebar');
|
||||
if (sidebar) {
|
||||
sidebar.classList.add('hidden');
|
||||
|
||||
@ -198,15 +198,27 @@
|
||||
<button type="button" id="btn-reset-lobby" class="btn btn-ghost hidden" title="Сбросить игру и вернуться в лобби">Вернуться в лобби</button>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="game-log-wrap">
|
||||
<div id="game-log" class="game-log"></div>
|
||||
</div>
|
||||
<button type="button" id="btn-cards-gallery-game" class="btn-instructions" title="Галерея карт" style="right: 3.5rem;">📚</button>
|
||||
<button type="button" id="btn-instructions" class="btn-instructions" title="Инструкция">?</button>
|
||||
<button type="button" id="btn-settings" class="btn-settings" title="Настройки">⚙</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Chat -->
|
||||
<div id="chat-container" class="chat-container">
|
||||
<div class="chat-header" id="chat-header">
|
||||
<span class="chat-title">💬 Чат</span>
|
||||
<button type="button" id="btn-chat-toggle" class="btn-chat-toggle" title="Свернуть/Развернуть">▼</button>
|
||||
</div>
|
||||
<div id="chat-content" class="chat-content">
|
||||
<div id="chat-messages" class="chat-messages"></div>
|
||||
<div class="chat-input-container">
|
||||
<input type="text" id="chat-input" class="chat-input" placeholder="Введите сообщение..." maxlength="200" />
|
||||
<button type="button" id="btn-chat-send" class="btn-chat-send">Отправить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="board">
|
||||
<div class="opponents" id="opponents-area">
|
||||
<!-- Opponent boards filled by JS -->
|
||||
@ -257,16 +269,34 @@
|
||||
<h2>⚒ Звёздная кузница</h2>
|
||||
<button type="button" id="btn-forge-close" class="btn-forge-close" title="Закрыть">×</button>
|
||||
</div>
|
||||
<p class="forge-hint">Перетащите 2-3 карты из руки или колоды сюда</p>
|
||||
|
||||
<div class="forge-main-content">
|
||||
<div class="forge-columns">
|
||||
<!-- Карты из руки -->
|
||||
<div class="forge-column">
|
||||
<h3 class="forge-column-title">📋 Из руки</h3>
|
||||
<div id="forge-hand-list" class="forge-cards-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Выбранные карты -->
|
||||
<div class="forge-column forge-selected-column">
|
||||
<h3 class="forge-column-title">✨ Выбрано (<span id="forge-selected-count">0</span>/3)</h3>
|
||||
<div id="forge-selected" class="forge-selected">
|
||||
<div class="forge-selected-empty">Перетащите карты сюда</div>
|
||||
<div class="forge-selected-empty">Кликните на карты чтобы выбрать</div>
|
||||
</div>
|
||||
<div class="forge-deck-section">
|
||||
<h3>Карты из колоды:</h3>
|
||||
<div id="forge-deck-list" class="forge-deck-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Карты из колоды -->
|
||||
<div class="forge-column">
|
||||
<h3 class="forge-column-title">📚 Из колоды</h3>
|
||||
<div id="forge-deck-list" class="forge-cards-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="forge-actions">
|
||||
<button type="button" id="btn-forge-craft" class="btn btn-primary" disabled>Создать улучшенную карту</button>
|
||||
<button type="button" id="btn-forge-clear" class="btn btn-ghost">Очистить</button>
|
||||
<button type="button" id="btn-forge-craft" class="btn btn-primary" disabled>⚒ Создать улучшенную карту (2 маны)</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="card-info-overlay" class="modal-overlay hidden">
|
||||
|
||||
@ -403,15 +403,177 @@ html, body {
|
||||
.turn-timer.timer-warn { color: var(--amber); border-color: var(--amber); animation: timerPulse 1s ease-in-out infinite; }
|
||||
.turn-timer.timer-ended { color: var(--red); }
|
||||
@keyframes timerPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
|
||||
.game-log-wrap { max-width: 200px; max-height: 80px; overflow: hidden; }
|
||||
.game-log {
|
||||
max-height: 80px;
|
||||
overflow-y: auto;
|
||||
font-size: 0.8rem;
|
||||
color: #94a3b8;
|
||||
line-height: 1.35;
|
||||
/* Chat */
|
||||
.chat-container {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 320px;
|
||||
max-width: calc(100vw - 40px);
|
||||
background: linear-gradient(145deg, rgba(13,18,25,0.95) 0%, rgba(5,10,18,0.98) 100%);
|
||||
border: 2px solid rgba(0,180,255,0.3);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.6);
|
||||
z-index: 150;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 400px;
|
||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.chat-container.collapsed {
|
||||
max-height: 50px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-container.collapsed .chat-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
background: rgba(0,0,0,0.3);
|
||||
border-bottom: 1px solid rgba(0,180,255,0.2);
|
||||
border-radius: 12px 12px 0 0;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
font-weight: 600;
|
||||
color: var(--cyan);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.btn-chat-toggle {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--cyan);
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem 0.5rem;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.chat-container.collapsed .btn-chat-toggle {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 350px;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.chat-message {
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
border-radius: 6px;
|
||||
background: rgba(0,0,0,0.3);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.chat-message.system {
|
||||
color: #94a3b8;
|
||||
font-style: italic;
|
||||
background: rgba(148,163,184,0.1);
|
||||
}
|
||||
|
||||
.chat-message.user {
|
||||
color: var(--cyan);
|
||||
background: rgba(0,180,255,0.1);
|
||||
}
|
||||
|
||||
.chat-message .chat-username {
|
||||
font-weight: 600;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.chat-message .chat-time {
|
||||
font-size: 0.7rem;
|
||||
color: #64748b;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
border-top: 1px solid rgba(0,180,255,0.2);
|
||||
background: rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
flex: 1;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: rgba(0,0,0,0.4);
|
||||
border: 1px solid rgba(0,180,255,0.3);
|
||||
border-radius: 6px;
|
||||
color: #e2e8f0;
|
||||
font-size: 0.85rem;
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
.chat-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--cyan);
|
||||
box-shadow: 0 0 8px rgba(0,180,255,0.3);
|
||||
}
|
||||
|
||||
.btn-chat-send {
|
||||
padding: 0.5rem 1rem;
|
||||
background: linear-gradient(135deg, var(--cyan) 0%, var(--cyan-dim) 100%);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: #0d1219;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-chat-send:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, var(--cyan-dim) 0%, var(--cyan) 100%);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.btn-chat-send:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.chat-container {
|
||||
width: calc(100vw - 20px);
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-chat-send {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.game-log-line { margin-bottom: 0.2rem; }
|
||||
@keyframes yourTurnGlow {
|
||||
0%, 100% { box-shadow: 0 0 12px rgba(94,179,232,0.5); transform: scale(1); }
|
||||
50% { box-shadow: 0 0 24px rgba(94,179,232,0.8); transform: scale(1.04); }
|
||||
@ -610,11 +772,17 @@ html, body {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.card-wrap.disabled { cursor: not-allowed; opacity: 0.6; }
|
||||
.card-wrap.not-enough-mana {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
filter: grayscale(0.3) brightness(0.7);
|
||||
pointer-events: none;
|
||||
}
|
||||
.card-wrap.in-hand {
|
||||
transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
transform: rotate(var(--hand-rotate, 0deg));
|
||||
}
|
||||
.card-wrap.in-hand:hover:not(.disabled) {
|
||||
.card-wrap.in-hand:hover:not(.disabled):not(.not-enough-mana) {
|
||||
transform: rotate(var(--hand-rotate, 0deg)) translateY(-20px) scale(1.08);
|
||||
z-index: 20;
|
||||
}
|
||||
@ -1401,17 +1569,17 @@ html, body {
|
||||
.card-info-modal .card-info-abilities { font-size: 0.85rem; color: var(--amber); margin-bottom: 0.5rem; }
|
||||
.card-info-modal .card-info-bio { font-size: 0.8rem; color: #94a3b8; line-height: 1.4; margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid rgba(148,163,184,0.3); }
|
||||
.card-info-modal .hidden { display: none !important; }
|
||||
/* Forge Sidebar */
|
||||
/* Forge Sidebar - Новая версия */
|
||||
.forge-sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 400px;
|
||||
max-width: 90vw;
|
||||
width: 90vw;
|
||||
max-width: 1200px;
|
||||
height: 100vh;
|
||||
background: linear-gradient(145deg, #2d2a22 0%, #1a1814 100%);
|
||||
background: linear-gradient(145deg, #1a1814 0%, #0d0a08 100%);
|
||||
border-left: 3px solid var(--gold);
|
||||
box-shadow: -5px 0 30px rgba(0,0,0,0.8);
|
||||
box-shadow: -5px 0 40px rgba(0,0,0,0.9);
|
||||
z-index: 200;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -1429,24 +1597,26 @@ html, body {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-bottom: 2px solid var(--gold);
|
||||
background: rgba(0,0,0,0.3);
|
||||
background: rgba(0,0,0,0.4);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.forge-sidebar-header h2 {
|
||||
margin: 0;
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.5rem;
|
||||
color: var(--gold);
|
||||
text-shadow: 0 0 10px rgba(212,168,75,0.5);
|
||||
}
|
||||
.btn-forge-close {
|
||||
background: transparent;
|
||||
border: 2px solid var(--gold);
|
||||
color: var(--gold);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.8rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
@ -1459,101 +1629,113 @@ html, body {
|
||||
background: var(--gold);
|
||||
color: #1a1814;
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 0 15px rgba(212,168,75,0.6);
|
||||
}
|
||||
.forge-hint {
|
||||
padding: 1rem 1.5rem;
|
||||
font-size: 0.85rem;
|
||||
color: #94a3b8;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
background: rgba(0,0,0,0.2);
|
||||
}
|
||||
.forge-selected {
|
||||
min-height: 150px;
|
||||
padding: 1rem;
|
||||
background: rgba(0,180,255,0.1);
|
||||
border: 2px dashed rgba(0,180,255,0.3);
|
||||
border-radius: 8px;
|
||||
margin: 1rem 1.5rem;
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.forge-selected.drag-over {
|
||||
background: rgba(0,180,255,0.2);
|
||||
border-color: var(--cyan);
|
||||
border-style: solid;
|
||||
box-shadow: 0 0 20px rgba(0,180,255,0.5);
|
||||
}
|
||||
.forge-selected-empty {
|
||||
color: #94a3b8;
|
||||
font-size: 0.9rem;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
.forge-deck-section {
|
||||
|
||||
.forge-main-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 1.5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
.forge-deck-section h3 {
|
||||
font-size: 1rem;
|
||||
color: var(--gold);
|
||||
margin: 0 0 0.75rem 0;
|
||||
font-family: var(--font-display);
|
||||
}
|
||||
.forge-deck-list {
|
||||
|
||||
.forge-columns {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
|
||||
gap: 0.75rem;
|
||||
overflow-y: auto;
|
||||
padding: 0.75rem;
|
||||
grid-template-columns: 1fr 1.2fr 1fr;
|
||||
gap: 1rem;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.forge-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: rgba(0,0,0,0.3);
|
||||
border-radius: 8px;
|
||||
flex: 1;
|
||||
border: 1px solid rgba(212,168,75,0.2);
|
||||
}
|
||||
.forge-deck-card {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
border: 2px solid transparent;
|
||||
user-select: none;
|
||||
}
|
||||
.forge-deck-card:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 0 15px rgba(0,180,255,0.5);
|
||||
}
|
||||
.forge-deck-card.selected {
|
||||
border-color: var(--cyan);
|
||||
box-shadow: 0 0 20px rgba(0,180,255,0.7);
|
||||
}
|
||||
.forge-deck-card.dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.forge-actions {
|
||||
padding: 1.5rem;
|
||||
border-top: 2px solid var(--gold);
|
||||
|
||||
.forge-column-title {
|
||||
font-size: 0.95rem;
|
||||
color: var(--gold);
|
||||
margin: 0;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid rgba(212,168,75,0.2);
|
||||
background: rgba(0,0,0,0.3);
|
||||
font-family: var(--font-display);
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.forge-actions .btn {
|
||||
|
||||
.forge-cards-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.75rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
gap: 0.75rem;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.forge-cards-list .card-wrap {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.forge-cards-list .card-wrap:hover {
|
||||
transform: translateY(-5px) scale(1.05);
|
||||
border-color: var(--cyan);
|
||||
box-shadow: 0 0 20px rgba(0,180,255,0.5);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.forge-cards-list .card-wrap.selected {
|
||||
border-color: var(--cyan);
|
||||
box-shadow: 0 0 25px rgba(0,180,255,0.7);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.forge-selected-column {
|
||||
background: rgba(0,180,255,0.05);
|
||||
border-color: rgba(0,180,255,0.3);
|
||||
}
|
||||
|
||||
.forge-selected {
|
||||
flex: 1;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.forge-selected-empty {
|
||||
width: 100%;
|
||||
color: #94a3b8;
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
font-style: italic;
|
||||
}
|
||||
.forge-selected .card-wrap {
|
||||
|
||||
.forge-selected-card {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.forge-selected .card-wrap:hover {
|
||||
|
||||
.forge-selected-card:hover {
|
||||
transform: scale(1.1);
|
||||
z-index: 5;
|
||||
}
|
||||
.forge-selected .card-wrap::before {
|
||||
content: '';
|
||||
|
||||
.forge-selected-card::before {
|
||||
content: '×';
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
@ -1562,36 +1744,51 @@ html, body {
|
||||
background: rgba(255,70,70,0.9);
|
||||
border: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
z-index: 10;
|
||||
transition: all 0.2s;
|
||||
opacity: 0;
|
||||
}
|
||||
.forge-selected .card-wrap:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
.forge-selected .card-wrap::after {
|
||||
content: '×';
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
z-index: 11;
|
||||
transition: all 0.2s;
|
||||
font-weight: 800;
|
||||
z-index: 10;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
pointer-events: none;
|
||||
}
|
||||
.forge-selected .card-wrap:hover::after {
|
||||
|
||||
.forge-selected-card:hover::before {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.forge-actions {
|
||||
padding: 1rem 1.5rem;
|
||||
border-top: 2px solid var(--gold);
|
||||
background: rgba(0,0,0,0.4);
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.forge-actions .btn {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.forge-actions .btn-primary {
|
||||
background: linear-gradient(135deg, var(--gold) 0%, #b8860b 100%);
|
||||
color: #1a0f00;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.forge-actions .btn-primary:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #b8860b 0%, var(--gold) 100%);
|
||||
box-shadow: 0 0 20px rgba(212,168,75,0.6);
|
||||
}
|
||||
|
||||
.forge-actions .btn-primary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-10px); }
|
||||
@ -2279,32 +2476,37 @@ html, body {
|
||||
right: 0;
|
||||
left: 0;
|
||||
border-radius: 0;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.forge-columns {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto auto auto;
|
||||
gap: 0.75rem;
|
||||
max-height: calc(100vh - 200px);
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.forge-deck-list {
|
||||
.forge-column {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.forge-cards-list {
|
||||
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
|
||||
gap: 0.5rem;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.forge-deck-card {
|
||||
width: 80px !important;
|
||||
height: 112px !important;
|
||||
touch-action: manipulation;
|
||||
max-height: 250px;
|
||||
}
|
||||
|
||||
.forge-selected {
|
||||
min-height: 120px;
|
||||
touch-action: manipulation;
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.forge-selected-card {
|
||||
touch-action: manipulation;
|
||||
.forge-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.forge-actions .btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mana-display, .health-display {
|
||||
@ -2320,11 +2522,23 @@ html, body {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.game-log {
|
||||
font-size: 0.75rem;
|
||||
max-height: 60px;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
.chat-container {
|
||||
width: calc(100vw - 20px);
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-chat-send {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-name {
|
||||
|
||||
29
server.js
29
server.js
@ -390,6 +390,7 @@ function broadcastGameState(room) {
|
||||
...room.gameState,
|
||||
yourIndex: i,
|
||||
yourHand: [...p.hand],
|
||||
yourDeck: [...p.deck], // Передаем колоду для кузницы
|
||||
yourDeckCount: p.deck.length,
|
||||
yourManualDrawUsed: !!p.manualDrawUsed,
|
||||
yourHeroAbilityUsed: !!p.heroAbilityUsed,
|
||||
@ -1879,6 +1880,34 @@ io.on('connection', (socket) => {
|
||||
stealCardsFromDeck(room, socket.id, data.handIndex, data.targetPlayerIndex, data.cardIndices);
|
||||
});
|
||||
|
||||
socket.on('chatMessage', (data) => {
|
||||
const room = getRoomBySocket(socket.id);
|
||||
if (!room) return;
|
||||
|
||||
const message = (data?.message || '').trim();
|
||||
if (message.length === 0 || message.length > 200) return;
|
||||
|
||||
// Находим имя игрока
|
||||
let playerName = 'Игрок';
|
||||
if (room.gameState && room.gameState.players) {
|
||||
const player = room.gameState.players.find(p => p.id === socket.id);
|
||||
if (player) {
|
||||
playerName = player.name || 'Игрок';
|
||||
}
|
||||
} else if (room.lobby) {
|
||||
const lobbyPlayer = room.lobby.find(p => p.id === socket.id);
|
||||
if (lobbyPlayer) {
|
||||
playerName = lobbyPlayer.name || 'Игрок';
|
||||
}
|
||||
}
|
||||
|
||||
// Отправляем сообщение всем в комнате
|
||||
io.to(room.code).emit('chatMessage', {
|
||||
playerName: playerName,
|
||||
message: message
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('battlecryTarget', (data) => {
|
||||
const room = getRoomBySocket(socket.id);
|
||||
if (!room || !room.gameState || room.gameState.phase !== 'playing') return;
|
||||
|
||||
Reference in New Issue
Block a user