123
This commit is contained in:
291
public/game.js
291
public/game.js
@ -18,9 +18,42 @@
|
|||||||
let prevGameState = null;
|
let prevGameState = null;
|
||||||
let combatTimeout = null;
|
let combatTimeout = null;
|
||||||
const DEFAULT_PORT = 3542;
|
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 $ = (id) => document.getElementById(id);
|
||||||
const $all = (sel) => document.querySelectorAll(sel);
|
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) {
|
function spawnEffect(type, x, y) {
|
||||||
const layer = $('effects-layer');
|
const layer = $('effects-layer');
|
||||||
@ -761,6 +794,162 @@
|
|||||||
return;
|
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('<strong>Вой:</strong> ' + escapeHtml(meta.battlecry));
|
||||||
|
if (meta.deathrattle) parts.push('<strong>Предсмертный хрип:</strong> ' + escapeHtml(meta.deathrattle));
|
||||||
|
abilEl.innerHTML = parts.length ? parts.join('<br>') : '';
|
||||||
|
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('<strong>Вой:</strong> ' + escapeHtml(meta.battlecry));
|
||||||
|
if (meta.deathrattle) parts.push('<strong>Предсмертный хрип:</strong> ' + escapeHtml(meta.deathrattle));
|
||||||
|
abilEl.innerHTML = parts.length ? parts.join('<br>') : '';
|
||||||
|
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) {
|
$all('.card-btn-info').forEach(function (btn) {
|
||||||
@ -805,6 +994,43 @@
|
|||||||
$('attack-mode')?.classList.remove('hidden');
|
$('attack-mode')?.classList.remove('hidden');
|
||||||
renderGame(state);
|
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) => {
|
$all('.card-wrap.targetable, .card-wrap.hero-target').forEach((wrap) => {
|
||||||
@ -823,6 +1049,26 @@
|
|||||||
attackMode = { active: false, attackerPlayer: -1, attackerBoard: -1 };
|
attackMode = { active: false, attackerPlayer: -1, attackerBoard: -1 };
|
||||||
$('attack-mode')?.classList.add('hidden');
|
$('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) {
|
$all('.spell-target, .drop-target-hero.spell-target').forEach(function (el) {
|
||||||
@ -837,6 +1083,22 @@
|
|||||||
$('spell-mode')?.classList.add('hidden');
|
$('spell-mode')?.classList.add('hidden');
|
||||||
renderGame(state);
|
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);
|
||||||
|
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) {
|
$all('.hero-ability-target, .drop-target-hero.hero-ability-target').forEach(function (el) {
|
||||||
@ -851,6 +1113,22 @@
|
|||||||
$('hero-ability-mode')?.classList.add('hidden');
|
$('hero-ability-mode')?.classList.add('hidden');
|
||||||
renderGame(state);
|
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);
|
||||||
|
};
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var heroAbilityBtn = $('btn-hero-ability');
|
var heroAbilityBtn = $('btn-hero-ability');
|
||||||
@ -927,9 +1205,22 @@
|
|||||||
socket.emit('drawCard');
|
socket.emit('drawCard');
|
||||||
};
|
};
|
||||||
deckEl.style.cursor = 'pointer';
|
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) {
|
} else if (deckEl) {
|
||||||
deckEl.onclick = null;
|
deckEl.onclick = null;
|
||||||
deckEl.style.cursor = 'default';
|
deckEl.style.cursor = 'default';
|
||||||
|
if (isTouchDevice()) {
|
||||||
|
deckEl.ontouchstart = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$all('.card-wrap.attackable[draggable="true"]').forEach(function (wrap) {
|
$all('.card-wrap.attackable[draggable="true"]').forEach(function (wrap) {
|
||||||
|
|||||||
@ -2,7 +2,10 @@
|
|||||||
<html lang="ru">
|
<html lang="ru">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<title>Star Wars Hearthstone — PvP по сети</title>
|
<title>Star Wars Hearthstone — PvP по сети</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
|||||||
@ -29,6 +29,7 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
@ -37,6 +38,10 @@ html, body {
|
|||||||
font-family: var(--font-body);
|
font-family: var(--font-body);
|
||||||
background: var(--space);
|
background: var(--space);
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
|
touch-action: manipulation;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
@ -532,6 +537,47 @@ html, body {
|
|||||||
z-index: 20;
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Touch feedback for mobile */
|
||||||
|
.card-wrap.touch-active {
|
||||||
|
transform: rotate(var(--hand-rotate, 0deg)) translateY(-15px) scale(1.05);
|
||||||
|
z-index: 20;
|
||||||
|
transition: transform 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-wrap.swiping {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: rotate(var(--hand-rotate, 0deg)) translateY(-10px) scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-wrap.long-pressed {
|
||||||
|
transform: rotate(var(--hand-rotate, 0deg)) translateY(-25px) scale(1.12);
|
||||||
|
z-index: 25;
|
||||||
|
box-shadow: 0 0 30px rgba(0,212,255,0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent text selection on touch devices */
|
||||||
|
.card-wrap, .btn, .tab, .deck-fan {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Better touch targets */
|
||||||
|
@media (hover: none) and (pointer: coarse) {
|
||||||
|
.card-wrap.in-hand:active:not(.disabled) {
|
||||||
|
transform: rotate(var(--hand-rotate, 0deg)) translateY(-15px) scale(1.05);
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-wrap.attackable:active {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -1551,6 +1597,7 @@ html, body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile optimizations */
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.hand .card-wrap { margin-left: -48px; }
|
.hand .card-wrap { margin-left: -48px; }
|
||||||
.card-wrap { width: 154px; height: 214px; }
|
.card-wrap { width: 154px; height: 214px; }
|
||||||
@ -1564,3 +1611,254 @@ html, body {
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Touch device optimizations */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
html, body {
|
||||||
|
touch-action: manipulation;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lobby-card {
|
||||||
|
padding: 1.5rem;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left, .header-center, .header-right {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0.85rem 1.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-end-turn {
|
||||||
|
min-height: 48px;
|
||||||
|
padding: 0.7rem 1.6rem;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-instructions, .btn-settings {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-wrap {
|
||||||
|
width: 140px;
|
||||||
|
height: 196px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board .card-wrap {
|
||||||
|
width: 100px;
|
||||||
|
height: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opponents .card-wrap {
|
||||||
|
width: 100px;
|
||||||
|
height: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hand .card-wrap {
|
||||||
|
margin-left: -40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hand .card-wrap:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hand-container {
|
||||||
|
min-height: 180px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-fan {
|
||||||
|
width: 90px;
|
||||||
|
height: 126px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-stack {
|
||||||
|
width: 85px;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-stack .deck-card {
|
||||||
|
width: 85px;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board {
|
||||||
|
min-height: 150px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.your-board {
|
||||||
|
min-height: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opponent-block {
|
||||||
|
min-width: 140px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attack-mode {
|
||||||
|
bottom: 200px;
|
||||||
|
padding: 0.7rem 1.2rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attack-mode p {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
max-width: 95vw;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards-gallery-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-card {
|
||||||
|
width: 100px !important;
|
||||||
|
height: 140px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forge-deck-list {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forge-deck-card {
|
||||||
|
width: 80px !important;
|
||||||
|
height: 112px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mana-display, .health-display {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.turn-badge {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 0.35rem 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.turn-timer {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-log {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
max-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-name {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-text {
|
||||||
|
font-size: 0.68rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-cost, .card-atk-hp {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-btn-info {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Touch feedback */
|
||||||
|
.card-wrap.touch-active {
|
||||||
|
transform: scale(1.05);
|
||||||
|
transition: transform 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-wrap.swiping {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent text selection on touch */
|
||||||
|
.card-wrap, .btn, .tab {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small mobile devices */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.lobby-card {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-wrap {
|
||||||
|
width: 120px;
|
||||||
|
height: 168px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board .card-wrap {
|
||||||
|
width: 85px;
|
||||||
|
height: 119px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opponents .card-wrap {
|
||||||
|
width: 85px;
|
||||||
|
height: 119px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hand .card-wrap {
|
||||||
|
margin-left: -35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hand-container {
|
||||||
|
min-height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-header {
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.75rem 1.2rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attack-announcement {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards-gallery-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(85px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-card {
|
||||||
|
width: 85px !important;
|
||||||
|
height: 119px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user