123
This commit is contained in:
291
public/game.js
291
public/game.js
@ -18,9 +18,42 @@
|
||||
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');
|
||||
@ -761,6 +794,162 @@
|
||||
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) {
|
||||
@ -805,6 +994,43 @@
|
||||
$('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) => {
|
||||
@ -823,6 +1049,26 @@
|
||||
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) {
|
||||
@ -837,6 +1083,22 @@
|
||||
$('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);
|
||||
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) {
|
||||
@ -851,6 +1113,22 @@
|
||||
$('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);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
var heroAbilityBtn = $('btn-hero-ability');
|
||||
@ -927,9 +1205,22 @@
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user