Commit 8a856349 authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix: piece click offset + redesign home screen

- Canvas coords: scale CSS pixels to logical canvas coordinates properly
  (fixes the offset between where you tap and what gets selected)
- Canvas touchAction:none prevents browser gestures interfering
- Canvas width/height explicitly set in style to match logical size

Home screen redesign:
- Games as 2x2 grid of large colorful tiles (center stage)
- Tap game → bottom sheet menu slides up with:
  - Single Player (bots + local play)
  - Multiplayer (online ranked)
  - Feature chips: Leaderboard, My Matches, Puzzles (chess only)
- No more carousel that gets cut off
- Game-feel: tiles scale down on press, smooth spring animation
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent e84212cf
...@@ -51,8 +51,12 @@ export function drawCircle(ctx, x, y, radius) { ...@@ -51,8 +51,12 @@ export function drawCircle(ctx, x, y, radius) {
export function getCanvasCoords(canvas, e) { export function getCanvasCoords(canvas, e) {
const rect = canvas.getBoundingClientRect(); const rect = canvas.getBoundingClientRect();
const touch = e.touches ? e.touches[0] : e; const touch = e.touches ? e.touches[0] : e;
const cssX = touch.clientX - rect.left;
const cssY = touch.clientY - rect.top;
const scaleX = canvas.width / (window.devicePixelRatio || 1) / rect.width;
const scaleY = canvas.height / (window.devicePixelRatio || 1) / rect.height;
return { return {
x: touch.clientX - rect.left, x: cssX * scaleX,
y: touch.clientY - rect.top y: cssY * scaleY
}; };
} }
...@@ -238,7 +238,12 @@ export class ChessBoard { ...@@ -238,7 +238,12 @@ export class ChessBoard {
this.canvas = canvas; this.canvas = canvas;
this.ctx = ctx; this.ctx = ctx;
this.size = size; this.size = size;
this.canvas.style.cssText = 'display:block;border-radius:4px;box-shadow:0 4px 20px rgba(0,0,0,0.5);'; this.canvas.style.width = size + 'px';
this.canvas.style.height = size + 'px';
this.canvas.style.display = 'block';
this.canvas.style.borderRadius = '4px';
this.canvas.style.boxShadow = '0 4px 20px rgba(0,0,0,0.5)';
this.canvas.style.touchAction = 'none';
} }
bindEvents() { bindEvents() {
......
...@@ -5,104 +5,271 @@ import * as audio from '../../../core/audio.js'; ...@@ -5,104 +5,271 @@ import * as audio from '../../../core/audio.js';
import { t } from '../../../core/i18n.js'; import { t } from '../../../core/i18n.js';
const games = [ const games = [
{ key: 'chess', name: 'game.chess', color: 'var(--chess-primary)', secondary: 'var(--chess-secondary)', icon: '♟' }, { key: 'chess', name: 'شطرنج', nameEn: 'Chess', color: '#2563EB', secondary: '#F5B731', icon: '♟', gradient: 'linear-gradient(135deg, #1e40af, #3b82f6)' },
{ key: 'domino', name: 'game.domino', color: 'var(--domino-primary)', secondary: 'var(--domino-secondary)', icon: '⬚' }, { key: 'domino', name: 'دومينو', nameEn: 'Domino', color: '#10B981', secondary: '#06B6D4', icon: '🁣', gradient: 'linear-gradient(135deg, #065f46, #10b981)' },
{ key: 'ludo', name: 'game.ludo', color: 'var(--ludo-primary)', secondary: 'var(--ludo-secondary)', icon: '⬡' }, { key: 'ludo', name: 'لودو', nameEn: 'Ludo', color: '#8B5CF6', secondary: '#EC4899', icon: '🎲', gradient: 'linear-gradient(135deg, #5b21b6, #8b5cf6)' },
{ key: 'backgammon', name: 'game.backgammon', color: 'var(--backgammon-primary)', secondary: 'var(--backgammon-secondary)', icon: '◎' } { key: 'backgammon', name: 'طاولة', nameEn: 'Backgammon', color: '#F59E0B', secondary: '#EF4444', icon: '◎', gradient: 'linear-gradient(135deg, #92400e, #f59e0b)' }
]; ];
let selectedGame = 0;
export function mountTable(el) { export function mountTable(el) {
const player = store.get('player');
el.innerHTML = ` el.innerHTML = `
<div style="padding:var(--s-4);display:flex;flex-direction:column;gap:var(--s-4);height:100%;"> <div class="play-home">
<!-- Game Carousel --> <div class="games-grid">
<div id="game-carousel" style="display:flex;gap:12px;overflow-x:auto;scroll-snap-type:x mandatory;padding:8px 16px;-webkit-overflow-scrolling:touch;scroll-padding:16px;"> ${games.map(g => `
${games.map((g, i) => ` <div class="game-tile" data-game="${g.key}" style="--game-color:${g.color};--game-gradient:${g.gradient};">
<div class="card game-card ${i === selectedGame ? 'selected' : ''}" data-idx="${i}" style="min-width:240px;flex-shrink:0;scroll-snap-align:start;cursor:pointer;border-color:${i === selectedGame ? g.color : 'var(--border)'};border-width:2px;"> <div class="game-tile-bg" style="background:${g.gradient};"></div>
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;"> <div class="game-tile-content">
<div style="width:44px;height:44px;border-radius:10px;background:linear-gradient(135deg,${g.color},${g.secondary});display:flex;align-items:center;justify-content:center;font-size:22px;">${g.icon}</div> <div class="game-tile-icon">${g.icon}</div>
<div> <div class="game-tile-name">${g.name}</div>
<div style="font-size:15px;font-weight:700;">${t(g.name)}</div>
<div style="font-size:12px;color:var(--text-secondary);font-family:var(--font-lat);">⭐ ${getRating(g.key, player)}</div>
</div>
</div>
<div style="height:60px;background:linear-gradient(135deg,${g.color}22,${g.secondary}22);border-radius:8px;display:flex;align-items:center;justify-content:center;color:${g.color};font-size:28px;">
${g.icon}
</div> </div>
</div> </div>
`).join('')} `).join('')}
</div> </div>
<!-- Mode Picker -->
<div style="display:flex;flex-direction:column;gap:var(--s-3);flex:1;justify-content:center;align-items:center;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--s-3);width:100%;max-width:320px;">
<button class="btn btn-secondary mode-btn" data-mode="bot" style="flex-direction:column;padding:var(--s-4);height:80px;">
<span style="font-size:20px;">🤖</span>
<span style="font-size:13px;">${t('play.vs_bot')}</span>
</button>
<button class="btn btn-secondary mode-btn" data-mode="human" style="flex-direction:column;padding:var(--s-4);height:80px;">
<span style="font-size:20px;">⚔️</span>
<span style="font-size:13px;">${t('play.vs_human')}</span>
</button>
<button class="btn btn-secondary mode-btn" data-mode="friend" style="flex-direction:column;padding:var(--s-4);height:80px;">
<span style="font-size:20px;">👥</span>
<span style="font-size:13px;">${t('play.vs_friend')}</span>
</button>
<button class="btn btn-secondary mode-btn" data-mode="local" style="flex-direction:column;padding:var(--s-4);height:80px;">
<span style="font-size:20px;">🏠</span>
<span style="font-size:13px;">${t('play.local')}</span>
</button>
</div>
</div>
</div> </div>
<div id="game-menu" class="game-menu hidden"></div>
<style>
.play-home {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding: 24px 16px;
}
.games-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
width: 100%;
max-width: 340px;
}
.game-tile {
position: relative;
aspect-ratio: 1;
border-radius: 20px;
overflow: hidden;
cursor: pointer;
transition: transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.15s;
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
}
.game-tile:active {
transform: scale(0.93);
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
}
.game-tile-bg {
position: absolute;
inset: 0;
opacity: 0.9;
}
.game-tile-content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 8px;
}
.game-tile-icon {
font-size: 42px;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3));
}
.game-tile-name {
font-size: 16px;
font-weight: 800;
color: white;
text-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.game-menu {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #0f0f1e;
border-top-left-radius: 24px;
border-top-right-radius: 24px;
padding: 24px 20px;
padding-bottom: calc(24px + var(--tab-height, 60px) + var(--safe-bottom, 0px));
z-index: 50;
transform: translateY(0);
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
box-shadow: 0 -8px 40px rgba(0,0,0,0.6);
}
.game-menu.hidden {
transform: translateY(100%);
pointer-events: none;
}
.game-menu-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.game-menu-title {
font-size: 20px;
font-weight: 800;
color: #f8fafc;
}
.game-menu-close {
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(255,255,255,0.08);
border: none;
color: #94a3b8;
font-size: 18px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.menu-btn {
display: flex;
align-items: center;
gap: 14px;
width: 100%;
padding: 16px;
border-radius: 14px;
border: none;
cursor: pointer;
margin-bottom: 10px;
transition: transform 0.1s, background 0.15s;
text-align: right;
}
.menu-btn:active { transform: scale(0.97); }
.menu-btn-primary {
background: var(--game-gradient, linear-gradient(135deg, #2563eb, #3b82f6));
color: white;
}
.menu-btn-secondary {
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.08);
color: #f8fafc;
}
.menu-btn-icon {
width: 40px;
height: 40px;
border-radius: 10px;
background: rgba(255,255,255,0.15);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
}
.menu-btn-text {
flex: 1;
}
.menu-btn-label {
font-size: 15px;
font-weight: 700;
}
.menu-btn-desc {
font-size: 11px;
opacity: 0.7;
margin-top: 2px;
}
.menu-features {
display: flex;
gap: 8px;
margin-top: 6px;
}
.feature-chip {
padding: 6px 12px;
border-radius: 8px;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.06);
color: #94a3b8;
font-size: 11px;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
}
.feature-chip:active { background: rgba(255,255,255,0.1); }
</style>
`; `;
el.querySelectorAll('.game-card').forEach(card => { const menu = el.querySelector('#game-menu');
card.addEventListener('click', () => {
selectedGame = parseInt(card.dataset.idx); el.querySelectorAll('.game-tile').forEach(tile => {
tile.addEventListener('click', () => {
audio.play('click'); audio.play('click');
el.querySelectorAll('.game-card').forEach((c, i) => { const gameKey = tile.dataset.game;
c.classList.toggle('selected', i === selectedGame); const game = games.find(g => g.key === gameKey);
c.style.borderColor = i === selectedGame ? games[i].color : 'var(--border)'; showGameMenu(menu, game);
});
}); });
}); });
}
el.querySelectorAll('.mode-btn').forEach(btn => { function showGameMenu(menu, game) {
btn.addEventListener('click', () => { menu.classList.remove('hidden');
audio.play('click'); menu.innerHTML = `
const mode = btn.dataset.mode; <div class="game-menu-header">
const game = games[selectedGame]; <div class="game-menu-title">${game.icon} ${game.name}</div>
<button class="game-menu-close" id="menu-close">✕</button>
</div>
if (mode === 'bot') { <button class="menu-btn menu-btn-primary" id="btn-single" style="--game-gradient:${game.gradient};">
if (game.key === 'chess') { <div class="menu-btn-icon">🎮</div>
scene.push('play-bot-select', { game: game.key }); <div class="menu-btn-text">
} else { <div class="menu-btn-label">لاعب واحد</div>
startLocalGame(game.key, 'bot'); <div class="menu-btn-desc">العب ضد الكمبيوتر أو محلياً</div>
} </div>
} else if (mode === 'human') { </button>
scene.push('play-time-select', { game: game.key, mode: 'human' });
} else if (mode === 'local') { <button class="menu-btn menu-btn-primary" id="btn-multi" style="--game-gradient:${game.gradient};opacity:0.85;">
startLocalGame(game.key, 'local'); <div class="menu-btn-icon">⚔️</div>
} else if (mode === 'friend') { <div class="menu-btn-text">
scene.push('play-time-select', { game: game.key, mode: 'friend' }); <div class="menu-btn-label">متعدد اللاعبين</div>
} <div class="menu-btn-desc">نافس لاعبين حقيقيين أونلاين</div>
}); </div>
</button>
<div class="menu-features">
<div class="feature-chip" id="btn-leaderboard">🏆 الترتيب</div>
<div class="feature-chip" id="btn-history">📋 مبارياتي</div>
${game.key === 'chess' ? '<div class="feature-chip" id="btn-puzzles">🧩 أحجيات</div>' : ''}
</div>
`;
menu.querySelector('#menu-close').addEventListener('click', () => {
audio.play('click');
menu.classList.add('hidden');
}); });
}
function getRating(gameKey, player) { menu.querySelector('#btn-single').addEventListener('click', () => {
if (!player) return '1200'; audio.play('click');
if (gameKey === 'chess') return `${player.elo_rapid || player.elo_blitz || 1200}`; menu.classList.add('hidden');
const r = player.ratings?.find(r => r.game_key === gameKey); if (game.key === 'chess') {
return r ? `${r.rating}` : '1200'; scene.push('play-bot-select', { game: game.key });
} } else {
const gameScene = game.key + '-game';
scene.push(gameScene, { mode: 'bot', game: game.key });
}
});
menu.querySelector('#btn-multi').addEventListener('click', () => {
audio.play('click');
menu.classList.add('hidden');
scene.push('play-time-select', { game: game.key, mode: 'human' });
});
menu.querySelector('#btn-leaderboard')?.addEventListener('click', () => {
audio.play('click');
menu.classList.add('hidden');
bus.emit('navigate', { world: 'rank', scene: 'leaderboard' });
});
function startLocalGame(gameKey, mode) { menu.querySelector('#btn-history')?.addEventListener('click', () => {
const sceneMap = { chess: 'chess-game', domino: 'domino-game', ludo: 'ludo-game', backgammon: 'chess-game' }; audio.play('click');
const target = sceneMap[gameKey] || 'chess-game'; menu.classList.add('hidden');
bus.emit('navigate', { scene: target, params: { mode, game: gameKey } }); scene.push('chess-history');
});
menu.querySelector('#btn-puzzles')?.addEventListener('click', () => {
audio.play('click');
menu.classList.add('hidden');
scene.push('puzzles');
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment