Commit 873b7000 authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix: backgammon gameplay — working buttons, correct canvas rendering, no DPR bugs

- Remove DPR ctx.scale() that compounded every frame and broke all rendering
- Use logical pixel dimensions directly (canvas.width = logical size)
- Fix isSel/isValid closures that caused undefined reference errors in checker glow
- Simplify game scene: roll button is now a normal element in .bgg-controls (not overlay)
- Event handlers correctly scale click coords via rect ratio
- Remove unused getCheckerScreenPos import from move-animator
- All buttons (roll, double, emote, quit) bind via addEventListener properly
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 4cf9baa5
...@@ -150,13 +150,15 @@ export function drawBoard(ctx, w, h, state, options = {}) { ...@@ -150,13 +150,15 @@ export function drawBoard(ctx, w, h, state, options = {}) {
const vis = Math.min(count, maxVis); const vis = Math.min(count, maxVis);
const spacing = Math.min(checkerR * 2, pointH / vis); const spacing = Math.min(checkerR * 2, pointH / vis);
const pointIsSelected = selectedPoint === i;
const pointIsValid = highlights.includes(i);
for (let j = 0; j < vis; j++) { for (let j = 0; j < vis; j++) {
const cy = isTop ? y + checkerR + j * spacing : y - checkerR - j * spacing; const cy = isTop ? y + checkerR + j * spacing : y - checkerR - j * spacing;
const isTopChecker = j === vis - 1; const isTopChecker = j === vis - 1;
const glowing = isTopChecker && isSel === i;
drawChecker(ctx, cx, cy, pieces[j].type, checkerR, { drawChecker(ctx, cx, cy, pieces[j].type, checkerR, {
glow: isSel && isTopChecker, glow: pointIsSelected && isTopChecker,
pulse: isValid(i) && isTopChecker pulse: pointIsValid && isTopChecker
}); });
} }
if (count > maxVis) { if (count > maxVis) {
...@@ -194,9 +196,6 @@ export function drawBoard(ctx, w, h, state, options = {}) { ...@@ -194,9 +196,6 @@ export function drawBoard(ctx, w, h, state, options = {}) {
// ── Particles ── // ── Particles ──
if (particles.length) drawParticles(ctx, particles); if (particles.length) drawParticles(ctx, particles);
function isValid(idx) { return highlights.includes(idx); }
function isSel(idx) { return selectedPoint === idx; }
return layout; return layout;
} }
......
// Arc-based move animator with overshoot bounce + particle burst // Arc-based move animator with overshoot bounce + particle burst
import { getPointCoords, getCheckerScreenPos } from './board-renderer.js'; import { getPointCoords } from './board-renderer.js';
const MOVE_DURATION = 320; const MOVE_DURATION = 320;
const HIT_DURATION = 420; const HIT_DURATION = 420;
......
...@@ -13,25 +13,22 @@ import { drawBoard, hitTest } from '../canvas/board-renderer.js'; ...@@ -13,25 +13,22 @@ import { drawBoard, hitTest } from '../canvas/board-renderer.js';
import { drawDice, createRollAnimation } from '../canvas/dice-renderer.js'; import { drawDice, createRollAnimation } from '../canvas/dice-renderer.js';
import { createMoveAnimation } from '../canvas/move-animator.js'; import { createMoveAnimation } from '../canvas/move-animator.js';
// ═══ State ═══
let canvas, ctx, layout; let canvas, ctx, layout;
let game, match; let game, match;
let selectedPiece = null; let selectedPiece = null;
let validMoves = [];
let highlights = []; let highlights = [];
let isRolling = false; let isRolling = false;
let isAnimating = false; let isAnimating = false;
let myColor = WHITE; let myColor = WHITE;
let params = {}; let params = {};
let turnTimer = null;
let turnTimeLeft = 30;
let rafId = null; let rafId = null;
let diceRollFrame = 0; let diceRollFrame = 0;
let diceRollTotal = 24; let diceRollTotal = 24;
let particles = []; let particles = [];
let animatingChecker = null; // {x, y, type, scale} let animatingChecker = null;
let lastFrameTime = 0; let lastFrameTime = 0;
let container = null; let container = null;
let logicalW = 360, logicalH = 400;
export function mountGame(el, p) { export function mountGame(el, p) {
params = p || {}; params = p || {};
...@@ -44,61 +41,50 @@ export function mountGame(el, p) { ...@@ -44,61 +41,50 @@ export function mountGame(el, p) {
el.innerHTML = ` el.innerHTML = `
<div class="bgg-container"> <div class="bgg-container">
<div class="bgg-top-bar"> <div class="bgg-top-bar">
<div class="bgg-player-card bgg-player-top"> <div class="bgg-player-card">
<div class="bgg-avatar-ring bgg-opp-ring"><div class="bgg-avatar">🎯</div></div> <div class="bgg-avatar">🎯</div>
<div class="bgg-player-info"> <div class="bgg-player-info">
<div class="bgg-name" id="name-top">${mode === 'bot' ? `بوت (${getDiffLabel(difficulty)})` : params.opponentName || 'خصم'}</div> <div class="bgg-name" id="name-top">${mode === 'bot' ? 'بوت' : 'خصم'}</div>
<div class="bgg-pips" id="pips-top">167 نقطة</div> <div class="bgg-pips" id="pips-top">167 نقطة</div>
</div> </div>
<div class="bgg-score-badge" id="score-top">0</div> <div class="bgg-score" id="score-top">0</div>
</div> </div>
</div> </div>
<div class="bgg-board-area"> <div class="bgg-board-area">
<canvas id="bg-canvas"></canvas> <canvas id="bg-canvas"></canvas>
<div class="bgg-dice-overlay" id="dice-overlay"> </div>
<button class="bgg-roll-btn" id="btn-roll">${emoji('game_die', '🎲', 26)} ارمي النرد</button>
</div> <div class="bgg-controls">
<div class="bgg-cube-area" id="cube-area" style="display:none;"> <button class="bgg-roll-btn" id="btn-roll">${emoji('game_die', '🎲', 22)} ارمي النرد</button>
<button class="bgg-cube-btn" id="btn-double"> <button class="bgg-cube-btn" id="btn-double" style="display:none;">×2 ضاعف</button>
<span class="bgg-cube-icon">⬡</span> </div>
<span class="bgg-cube-label">×2 ضاعف</span>
</button> <div class="bgg-double-offer" id="double-offer" style="display:none;">
</div> <p>الخصم يضاعف! ×<strong id="double-val">2</strong></p>
<div class="bgg-double-offer" id="double-offer" style="display:none;"> <button class="bgg-accept-btn" id="btn-accept-double">قبول</button>
<div class="bgg-offer-title">مضاعفة!</div> <button class="bgg-decline-btn" id="btn-decline-double">رفض</button>
<p class="bgg-offer-desc">الخصم يريد مضاعفة الرهان إلى <strong id="double-val">2</strong></p>
<div class="bgg-offer-btns">
<button class="bgg-accept-btn" id="btn-accept-double">${emoji('check', '✓', 16)} قبول</button>
<button class="bgg-decline-btn" id="btn-decline-double">${emoji('cross', '✕', 16)} رفض</button>
</div>
</div>
<div class="bgg-no-moves-toast" id="no-moves" style="display:none;">لا حركات متاحة!</div>
</div> </div>
<div class="bgg-bottom-bar"> <div class="bgg-bottom-bar">
<div class="bgg-player-card bgg-player-bottom"> <div class="bgg-player-card">
<div class="bgg-avatar-ring bgg-my-ring"><div class="bgg-avatar">👤</div></div> <div class="bgg-avatar">👤</div>
<div class="bgg-player-info"> <div class="bgg-player-info">
<div class="bgg-name" id="name-bottom">أنت</div> <div class="bgg-name" id="name-bottom">أنت</div>
<div class="bgg-pips" id="pips-bottom">167 نقطة</div> <div class="bgg-pips" id="pips-bottom">167 نقطة</div>
</div> </div>
<div class="bgg-score-badge bgg-my-score" id="score-bottom">0</div> <div class="bgg-score bgg-my-score" id="score-bottom">0</div>
</div> </div>
<div class="bgg-actions"> <div class="bgg-actions">
<button class="bgg-action-btn" id="btn-emote">${emoji('sparkles', '✨', 18)}</button> <button class="bgg-action-btn" id="btn-emote">${emoji('sparkles', '✨', 18)}</button>
<button class="bgg-action-btn bgg-action-quit" id="btn-quit">✕</button> <button class="bgg-action-btn bgg-quit" id="btn-quit">✕</button>
</div> </div>
</div> </div>
<div class="bgg-turn-badge" id="turn-indicator">دورك</div> <div class="bgg-turn-badge" id="turn-indicator">دورك</div>
<div class="bgg-emote-panel" id="emote-panel" style="display:none;"></div> <div class="bgg-emote-panel" id="emote-panel" style="display:none;"></div>
<div class="bgg-match-info" id="match-info">
<span class="bgg-match-label">ماتش ${matchLength}</span>
<span class="bgg-variant-label">${getVariantLabel(variant)}</span>
</div>
</div> </div>
${getGameStyles()} ${getStyles()}
`; `;
container = el.querySelector('.bgg-container'); container = el.querySelector('.bgg-container');
...@@ -108,18 +94,16 @@ export function mountGame(el, p) { ...@@ -108,18 +94,16 @@ export function mountGame(el, p) {
resizeCanvas(); resizeCanvas();
startNewGameRound(); startNewGameRound();
// Event bindings
canvas.addEventListener('click', onCanvasClick); canvas.addEventListener('click', onCanvasClick);
canvas.addEventListener('touchstart', onCanvasTouch, { passive: false }); canvas.addEventListener('touchstart', onCanvasTouch, { passive: false });
el.querySelector('#btn-roll').onclick = onRollClick; el.querySelector('#btn-roll').addEventListener('click', onRollClick);
el.querySelector('#btn-double').onclick = onDoubleClick; el.querySelector('#btn-double').addEventListener('click', onDoubleClick);
el.querySelector('#btn-accept-double').onclick = onAcceptDouble; el.querySelector('#btn-accept-double').addEventListener('click', onAcceptDouble);
el.querySelector('#btn-decline-double').onclick = onDeclineDouble; el.querySelector('#btn-decline-double').addEventListener('click', onDeclineDouble);
el.querySelector('#btn-emote').onclick = toggleEmotePanel; el.querySelector('#btn-emote').addEventListener('click', toggleEmotePanel);
el.querySelector('#btn-quit').onclick = onQuit; el.querySelector('#btn-quit').addEventListener('click', onQuit);
window.addEventListener('resize', resizeCanvas); window.addEventListener('resize', resizeCanvas);
// Multiplayer
if (mode === 'live') { if (mode === 'live') {
matchSession.create(params.matchId, 'backgammon', { matchSession.create(params.matchId, 'backgammon', {
onOpponentMove: handleServerState, onOpponentMove: handleServerState,
...@@ -129,261 +113,167 @@ export function mountGame(el, p) { ...@@ -129,261 +113,167 @@ export function mountGame(el, p) {
} }
setupEmotePanel(el); setupEmotePanel(el);
startGameLoop(); startLoop();
} }
export function unmountGame() { export function unmountGame() {
if (rafId) cancelAnimationFrame(rafId); if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
rafId = null;
window.removeEventListener('resize', resizeCanvas); window.removeEventListener('resize', resizeCanvas);
matchSession.destroy(); matchSession.destroy();
clearInterval(turnTimer); container = null; canvas = null; ctx = null;
container = null;
} }
// ═══════════════════════════════════════ // ── Resize ──
// 60 FPS GAME LOOP function resizeCanvas() {
// ═══════════════════════════════════════ if (!canvas) return;
const area = canvas.parentElement;
const w = Math.min(area.clientWidth - 8, 400);
const h = Math.round(w * 1.1);
logicalW = w;
logicalH = h;
canvas.width = w;
canvas.height = h;
canvas.style.width = w + 'px';
canvas.style.height = h + 'px';
}
function startGameLoop() { // ── Game Loop ──
function startLoop() {
lastFrameTime = performance.now(); lastFrameTime = performance.now();
function loop(now) { function loop(now) {
const dt = (now - lastFrameTime) / 1000; const dt = Math.min((now - lastFrameTime) / 1000, 0.05);
lastFrameTime = now; lastFrameTime = now;
update(dt); updateParticles(dt);
render(); draw();
rafId = requestAnimationFrame(loop); rafId = requestAnimationFrame(loop);
} }
rafId = requestAnimationFrame(loop); rafId = requestAnimationFrame(loop);
} }
function update(dt) { function updateParticles(dt) {
// Update particles
for (let i = particles.length - 1; i >= 0; i--) { for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i]; const p = particles[i];
p.x += p.vx * dt; p.x += p.vx * dt;
p.y += p.vy * dt; p.y += p.vy * dt;
p.vy += (p.gravity || 200) * dt; p.vy += 250 * dt;
p.alpha -= (p.decay || 1.5) * dt; p.alpha -= 2 * dt;
if (p.rot !== undefined) p.rot += p.vr * dt;
if (p.alpha <= 0) particles.splice(i, 1); if (p.alpha <= 0) particles.splice(i, 1);
} }
} }
function render() { function draw() {
if (!ctx || !game) return; if (!ctx || !game) return;
const w = canvas.width;
const h = canvas.height;
layout = drawBoard(ctx, w, h, game.state, { layout = drawBoard(ctx, logicalW, logicalH, game.state, {
highlights, highlights,
selectedPoint: selectedPiece?.from ?? null, selectedPoint: selectedPiece?.from ?? null,
particles particles
}); });
// Draw dice // Dice on board
if (game.dice && !isRolling) { if (game.dice && !isRolling) {
const diceCount = game.dice[0] === game.dice[1] ? 4 : 2; const allDice = game.dice[0] === game.dice[1]
const allDice = game.dice[0] === game.dice[1] ? [game.dice[0], game.dice[0], game.dice[0], game.dice[0]] : game.dice; ? [game.dice[0], game.dice[0], game.dice[0], game.dice[0]]
const totalDiceW = diceCount * 38 + (diceCount - 1) * 12; : game.dice;
const diceX = w / 2 - totalDiceW / 2; const totalW = allDice.length * 38 + (allDice.length - 1) * 10;
const diceY = h / 2 - 19; const diceX = logicalW / 2 - totalW / 2;
const diceY = logicalH / 2 - 19;
const usedIndices = getUsedDiceIndices(); drawDice(ctx, diceX, diceY, allDice, { used: getUsedDiceIndices() });
drawDice(ctx, diceX, diceY, allDice, { used: usedIndices });
} }
// Rolling dice animation
if (isRolling) { if (isRolling) {
const fakeVals = [Math.ceil(Math.random() * 6), Math.ceil(Math.random() * 6)]; const fake = [Math.ceil(Math.random() * 6), Math.ceil(Math.random() * 6)];
const diceX = w / 2 - 44; drawDice(ctx, logicalW / 2 - 44, logicalH / 2 - 19, fake, {
const diceY = h / 2 - 19; rolling: true, frame: diceRollFrame, totalFrames: diceRollTotal
drawDice(ctx, diceX, diceY, fakeVals, { rolling: true, frame: diceRollFrame, totalFrames: diceRollTotal }); });
} }
// Animating checker // Animated checker in flight
if (animatingChecker) { if (animatingChecker) {
const { x, y, type, scale = 1 } = animatingChecker; const { x, y, type } = animatingChecker;
const r = layout?.checkerR || 14;
ctx.save(); ctx.save();
ctx.translate(x, y); // shadow
ctx.scale(scale, scale);
const r = layout.checkerR;
const isW = type === WHITE;
// Shadow
ctx.beginPath(); ctx.beginPath();
ctx.arc(1, 3, r, 0, Math.PI * 2); ctx.arc(x + 1, y + 3, r, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fill(); ctx.fill();
// Checker // checker
ctx.beginPath(); ctx.beginPath();
ctx.arc(0, 0, r, 0, Math.PI * 2); ctx.arc(x, y, r, 0, Math.PI * 2);
const grad = ctx.createRadialGradient(-r * 0.3, -r * 0.3, r * 0.1, 0, 0, r); ctx.fillStyle = type === WHITE ? '#F5E6D3' : '#2A2A2A';
if (isW) { grad.addColorStop(0, '#FFF'); grad.addColorStop(1, '#C8A882'); }
else { grad.addColorStop(0, '#555'); grad.addColorStop(1, '#0A0A0A'); }
ctx.fillStyle = grad;
ctx.fill(); ctx.fill();
ctx.strokeStyle = isW ? 'rgba(160,120,60,0.5)' : 'rgba(100,100,100,0.5)'; ctx.strokeStyle = type === WHITE ? '#A08060' : '#555';
ctx.lineWidth = 1.5; ctx.lineWidth = 2;
ctx.stroke(); ctx.stroke();
// Glow // glow
ctx.beginPath(); ctx.beginPath();
ctx.arc(0, 0, r + 4, 0, Math.PI * 2); ctx.arc(x, y, r + 3, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(255,215,0,0.6)'; ctx.strokeStyle = 'rgba(255,215,0,0.6)';
ctx.lineWidth = 2; ctx.lineWidth = 2;
ctx.shadowColor = '#FFD700';
ctx.shadowBlur = 12;
ctx.stroke(); ctx.stroke();
ctx.shadowBlur = 0;
ctx.restore(); ctx.restore();
} }
} }
// ═══════════════════════════════════════ // ── Game Logic ──
// PARTICLES
// ═══════════════════════════════════════
function spawnHitParticles(x, y) {
const colors = ['#EF4444', '#F97316', '#FBBF24', '#FFF'];
for (let i = 0; i < 12; i++) {
const angle = (Math.PI * 2 * i) / 12 + Math.random() * 0.3;
const speed = 80 + Math.random() * 120;
particles.push({
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed - 60,
alpha: 1,
size: 2 + Math.random() * 3,
color: colors[Math.floor(Math.random() * colors.length)],
gravity: 300,
decay: 2
});
}
}
function spawnBearOffParticles(x, y) {
const colors = ['#10B981', '#34D399', '#6EE7B7', '#FFD700'];
for (let i = 0; i < 8; i++) {
const angle = -Math.PI / 2 + (Math.random() - 0.5) * Math.PI * 0.8;
const speed = 60 + Math.random() * 80;
particles.push({
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
alpha: 1,
size: 2 + Math.random() * 2.5,
color: colors[Math.floor(Math.random() * colors.length)],
shape: Math.random() > 0.5 ? 'star' : 'circle',
rot: 0, vr: (Math.random() - 0.5) * 8,
gravity: 150,
decay: 1.8
});
}
}
function spawnWinParticles() {
const colors = ['#FFD700', '#FFA500', '#FF6347', '#10B981', '#8B5CF6', '#06B6D4'];
for (let i = 0; i < 30; i++) {
const x = Math.random() * canvas.width;
particles.push({
x, y: canvas.height + 10,
vx: (Math.random() - 0.5) * 60,
vy: -(200 + Math.random() * 200),
alpha: 1,
size: 3 + Math.random() * 4,
color: colors[Math.floor(Math.random() * colors.length)],
shape: 'star',
rot: 0, vr: (Math.random() - 0.5) * 10,
gravity: 80,
decay: 0.7
});
}
}
// ═══════════════════════════════════════
// GAME LOGIC
// ═══════════════════════════════════════
function startNewGameRound() { function startNewGameRound() {
game = startNewGame(match); game = startNewGame(match);
selectedPiece = null; selectedPiece = null;
validMoves = [];
highlights = []; highlights = [];
isRolling = false; isRolling = false;
isAnimating = false; isAnimating = false;
animatingChecker = null; animatingChecker = null;
particles = []; particles = [];
updateUI(); updateUI();
updateTurnIndicator();
if (game.turn !== myColor && params.mode === 'bot') { if (game.turn !== myColor && params.mode === 'bot') {
setTimeout(doBotTurn, 1000); setTimeout(doBotTurn, 800);
} }
} }
function getUsedDiceIndices() { function getUsedDiceIndices() {
const usedIndices = []; if (!game.dice) return [];
const isDoubles = game.dice[0] === game.dice[1]; const used = [];
const allDice = isDoubles ? [game.dice[0], game.dice[0], game.dice[0], game.dice[0]] : [...game.dice]; const allDice = game.dice[0] === game.dice[1]
? [game.dice[0], game.dice[0], game.dice[0], game.dice[0]]
for (const played of game.movesPlayed) { : [...game.dice];
for (const played of (game.movesPlayed || [])) {
for (let j = 0; j < allDice.length; j++) { for (let j = 0; j < allDice.length; j++) {
if (!usedIndices.includes(j) && allDice[j] === played) { if (!used.includes(j) && allDice[j] === played) { used.push(j); break; }
usedIndices.push(j);
break;
}
} }
} }
return usedIndices; return used;
}
// ═══════════════════════════════════════
// UI UPDATES
// ═══════════════════════════════════════
function resizeCanvas() {
if (!canvas) return;
const area = canvas.parentElement;
const w = Math.min(area.clientWidth - 8, 420);
const h = Math.min(w * 1.12, 470);
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = w * dpr;
canvas.height = h * dpr;
canvas.style.width = w + 'px';
canvas.style.height = h + 'px';
ctx.scale(dpr, dpr);
// Reset scale on next render
canvas._logicalW = w;
canvas._logicalH = h;
} }
function updateUI() { function updateUI() {
if (!game || !container) return; if (!game || !container) return;
const topColor = myColor === WHITE ? BLACK : WHITE; const topColor = myColor === WHITE ? BLACK : WHITE;
const isMyTurn = game.turn === myColor;
setText('#pips-top', getPipCount(game.state, topColor) + ' نقطة'); setText('#pips-top', getPipCount(game.state, topColor) + ' نقطة');
setText('#pips-bottom', getPipCount(game.state, myColor) + ' نقطة'); setText('#pips-bottom', getPipCount(game.state, myColor) + ' نقطة');
setText('#score-top', String(match.scores[topColor])); setText('#score-top', String(match.scores[topColor]));
setText('#score-bottom', String(match.scores[myColor])); setText('#score-bottom', String(match.scores[myColor]));
// Roll button
const rollBtn = container.querySelector('#btn-roll'); const rollBtn = container.querySelector('#btn-roll');
const isMyTurn = game.turn === myColor; rollBtn.style.display = (isMyTurn && !game.dice && !isAnimating && !isRolling) ? '' : 'none';
const diceOverlay = container.querySelector('#dice-overlay');
if (diceOverlay) diceOverlay.style.display = (isMyTurn && !game.dice && !isAnimating && !isRolling) ? '' : 'none';
const cubeArea = container.querySelector('#cube-area'); // Double button
const dblBtn = container.querySelector('#btn-double');
if (match.cube && canUseCube(match) && isMyTurn && !game.dice && canDouble(match.cube, myColor)) { if (match.cube && canUseCube(match) && isMyTurn && !game.dice && canDouble(match.cube, myColor)) {
cubeArea.style.display = ''; dblBtn.style.display = '';
} else { } else {
cubeArea.style.display = 'none'; dblBtn.style.display = 'none';
} }
}
function updateTurnIndicator() { // Turn indicator
const el = container?.querySelector('#turn-indicator'); const ti = container.querySelector('#turn-indicator');
if (!el) return; if (ti) {
const isMyTurn = game.turn === myColor; ti.textContent = isMyTurn ? 'دورك' : 'دور الخصم';
el.textContent = isMyTurn ? 'دورك' : 'دور الخصم'; ti.className = `bgg-turn-badge ${isMyTurn ? 'bgg-turn-mine' : 'bgg-turn-opp'}`;
el.className = `bgg-turn-badge ${isMyTurn ? 'bgg-turn-mine' : 'bgg-turn-opp'}`; }
} }
function setText(sel, text) { function setText(sel, text) {
...@@ -391,114 +281,96 @@ function setText(sel, text) { ...@@ -391,114 +281,96 @@ function setText(sel, text) {
if (el) el.textContent = text; if (el) el.textContent = text;
} }
// ═══════════════════════════════════════ // ── Input ──
// INPUT
// ═══════════════════════════════════════
function onCanvasClick(e) { function onCanvasClick(e) {
if (isAnimating || isRolling) return; if (isAnimating || isRolling) return;
if (game.turn !== myColor && params.mode !== 'local-multi') return; if (game.turn !== myColor) return;
if (!game.dice || game.movesLeft.length === 0) return; if (!game.dice) return;
const rect = canvas.getBoundingClientRect(); const rect = canvas.getBoundingClientRect();
const scaleX = (canvas._logicalW || canvas.width) / rect.width; const x = (e.clientX - rect.left) * (logicalW / rect.width);
const scaleY = (canvas._logicalH || canvas.height) / rect.height; const y = (e.clientY - rect.top) * (logicalH / rect.height);
const x = (e.clientX - rect.left) * scaleX; handleTap(x, y);
const y = (e.clientY - rect.top) * scaleY;
handleBoardTap(x, y);
} }
function onCanvasTouch(e) { function onCanvasTouch(e) {
if (isAnimating || isRolling) return; if (isAnimating || isRolling) return;
if (game.turn !== myColor && params.mode !== 'local-multi') return; if (game.turn !== myColor) return;
if (!game.dice || game.movesLeft.length === 0) return; if (!game.dice) return;
e.preventDefault(); e.preventDefault();
const touch = e.touches[0]; const t = e.touches[0];
const rect = canvas.getBoundingClientRect(); const rect = canvas.getBoundingClientRect();
const scaleX = (canvas._logicalW || canvas.width) / rect.width; const x = (t.clientX - rect.left) * (logicalW / rect.width);
const scaleY = (canvas._logicalH || canvas.height) / rect.height; const y = (t.clientY - rect.top) * (logicalH / rect.height);
const x = (touch.clientX - rect.left) * scaleX; handleTap(x, y);
const y = (touch.clientY - rect.top) * scaleY;
handleBoardTap(x, y);
} }
function handleBoardTap(x, y) { function handleTap(x, y) {
const hit = hitTest(x, y, layout, game.state); const hit = hitTest(x, y, layout, game.state);
if (!hit) { clearSelection(); return; } if (!hit) { clearSelection(); return; }
// Tap destination
if (selectedPiece && hit.type === 'point' && highlights.includes(hit.index)) { if (selectedPiece && hit.type === 'point' && highlights.includes(hit.index)) {
executeMove(hit.index); executeMove(hit.index); return;
return;
} }
if (selectedPiece && hit.type === 'bearoff' && highlights.includes('off')) { if (selectedPiece && hit.type === 'bearoff' && highlights.includes('off')) {
executeMove('off'); executeMove('off'); return;
return;
} }
// Select piece
if (hit.type === 'point') selectPoint(hit.index); if (hit.type === 'point') selectPoint(hit.index);
else if (hit.type === 'bar' && hit.player === game.turn) selectBar(); else if (hit.type === 'bar' && hit.player === game.turn) selectBar();
else clearSelection(); else clearSelection();
} }
function selectPoint(pointIndex) { function selectPoint(idx) {
const pieces = game.state.points[pointIndex]; const pieces = game.state.points[idx];
if (pieces.length === 0 || pieces[pieces.length - 1].type !== game.turn) { if (!pieces.length || pieces[pieces.length - 1].type !== game.turn) { clearSelection(); return; }
clearSelection();
return;
}
const piece = pieces[pieces.length - 1]; const piece = pieces[pieces.length - 1];
const moves = getValidMoves(game).filter(m => m.piece.id === piece.id); const moves = getValidMoves(game).filter(m => m.piece.id === piece.id);
if (moves.length === 0) { clearSelection(); return; } if (!moves.length) { clearSelection(); return; }
selectedPiece = { id: piece.id, from: pointIndex, moves }; selectedPiece = { id: piece.id, from: idx, moves };
highlights = moves.map(m => m.to === 'off' ? 'off' : m.to); highlights = moves.map(m => m.to === 'off' ? 'off' : m.to);
audio.play('click'); audio.play('click');
} }
function selectBar() { function selectBar() {
const moves = getValidMoves(game).filter(m => m.from === 'bar' || m.from === null); const moves = getValidMoves(game).filter(m => m.from === 'bar' || m.from === null);
if (moves.length === 0) return; if (!moves.length) return;
selectedPiece = { id: moves[0].piece.id, from: 'bar', moves }; selectedPiece = { id: moves[0].piece.id, from: 'bar', moves };
highlights = moves.map(m => m.to); highlights = moves.map(m => m.to);
audio.play('click'); audio.play('click');
} }
function clearSelection() { function clearSelection() { selectedPiece = null; highlights = []; }
selectedPiece = null;
highlights = [];
}
function executeMove(destination) { function executeMove(dest) {
if (!selectedPiece) return; if (!selectedPiece) return;
const move = selectedPiece.moves.find(m => m.to === destination); const move = selectedPiece.moves.find(m => m.to === dest);
if (!move) return; if (!move) return;
isAnimating = true; isAnimating = true;
const fromPos = selectedPiece.from; const fromPt = selectedPiece.from;
const toPos = destination;
const pieceType = game.turn; const pieceType = game.turn;
audio.play('sfx_bg_piece_move', 'game'); audio.play('sfx_bg_piece_move', 'game');
// Start arc animation createMoveAnimation(fromPt, dest, layout,
createMoveAnimation(fromPos, toPos, layout, (frame) => { animatingChecker = { x: frame.x, y: frame.y, type: pieceType }; },
(frame) => {
animatingChecker = { x: frame.x, y: frame.y, type: pieceType, scale: frame.scale };
},
() => { () => {
animatingChecker = null; animatingChecker = null;
const actions = applyMove(game, move.piece.id, move.steps); const actions = applyMove(game, move.piece.id, move.steps);
if (!actions) { isAnimating = false; return; }
if (actions.some(a => a.type === 'hit')) { if (actions?.some(a => a.type === 'hit')) {
audio.play('sfx_bg_piece_hit', 'game'); audio.play('sfx_bg_piece_hit', 'game');
const hitPos = layout ? { x: canvas._logicalW / 2, y: canvas._logicalH / 2 } : { x: 180, y: 200 }; spawnParticles(logicalW / 2, logicalH / 2, ['#EF4444', '#F97316', '#FBBF24']);
spawnHitParticles(hitPos.x, hitPos.y);
} }
if (actions.some(a => a.type === 'bear')) { if (actions?.some(a => a.type === 'bear')) {
audio.play('sfx_bg_bear_off', 'game'); audio.play('sfx_bg_bear_off', 'game');
spawnBearOffParticles(canvas._logicalW - 20, canvas._logicalH / 2); spawnParticles(logicalW - 20, logicalH / 2, ['#10B981', '#34D399', '#FFD700']);
} }
clearSelection(); clearSelection();
...@@ -507,7 +379,6 @@ function executeMove(destination) { ...@@ -507,7 +379,6 @@ function executeMove(destination) {
if (game.isOver) { handleGameOver(); return; } if (game.isOver) { handleGameOver(); return; }
if (game.movesLeft.length === 0) { endTurn(); return; } if (game.movesLeft.length === 0) { endTurn(); return; }
const remaining = getValidMoves(game); const remaining = getValidMoves(game);
if (remaining.length === 0) endTurn(); if (remaining.length === 0) endTurn();
} }
...@@ -517,37 +388,29 @@ function executeMove(destination) { ...@@ -517,37 +388,29 @@ function executeMove(destination) {
function endTurn() { function endTurn() {
nextTurn(game); nextTurn(game);
updateUI(); updateUI();
updateTurnIndicator(); if (params.mode === 'bot' && game.turn !== myColor) setTimeout(doBotTurn, 600);
else if (params.mode === 'live') syncToServer();
if (params.mode === 'bot' && game.turn !== myColor) {
setTimeout(doBotTurn, 700);
} else if (params.mode === 'live') {
syncToServer();
}
} }
// ═══════════════════════════════════════ // ── Dice ──
// DICE
// ═══════════════════════════════════════
function onRollClick() { function onRollClick() {
if (game.dice || isRolling || isAnimating) return; if (game.dice || isRolling || isAnimating) return;
if (game.turn !== myColor && params.mode !== 'local-multi') return; if (game.turn !== myColor) return;
isRolling = true; isRolling = true;
diceRollFrame = 0; diceRollFrame = 0;
audio.play('sfx_bg_dice_roll', 'game'); audio.play('sfx_bg_dice_roll', 'game');
updateUI();
createRollAnimation( createRollAnimation(
(frame, total) => { diceRollFrame = frame; diceRollTotal = total; }, (frame, total) => { diceRollFrame = frame; diceRollTotal = total; },
() => { () => {
isRolling = false; isRolling = false;
diceRollFrame = 0;
rollDice(game); rollDice(game);
if (game.movesLeft.length === 0) { if (game.movesLeft.length === 0) {
showNoMoves(); bus.emit('toast', { text: 'لا حركات متاحة!' });
setTimeout(endTurn, 1200); setTimeout(endTurn, 1000);
return; return;
} }
updateUI(); updateUI();
...@@ -564,36 +427,22 @@ function onRollClick() { ...@@ -564,36 +427,22 @@ function onRollClick() {
); );
} }
function showNoMoves() { // ── Doubling ──
const el = container?.querySelector('#no-moves');
if (!el) return;
el.style.display = '';
el.classList.add('bgg-toast-show');
setTimeout(() => { el.style.display = 'none'; el.classList.remove('bgg-toast-show'); }, 1200);
}
// ═══════════════════════════════════════
// DOUBLING CUBE
// ═══════════════════════════════════════
function onDoubleClick() { function onDoubleClick() {
if (!match.cube || !canDouble(match.cube, myColor)) return; if (!match.cube || !canDouble(match.cube, myColor) || game.dice) return;
if (game.dice) return;
audio.play('sfx_bg_double', 'game'); audio.play('sfx_bg_double', 'game');
if (params.mode === 'bot') { if (params.mode === 'bot') {
offerDouble(match.cube, myColor); offerDouble(match.cube, myColor);
const myPips = getPipCount(game.state, myColor); const myPips = getPipCount(game.state, myColor);
const oppPips = getPipCount(game.state, myColor === WHITE ? BLACK : WHITE); const oppPips = getPipCount(game.state, myColor === WHITE ? BLACK : WHITE);
if (shouldBotAccept(match.cube, oppPips, myPips, params.difficulty)) { if (shouldBotAccept(match.cube, oppPips, myPips, params.difficulty)) {
acceptDouble(match.cube); acceptDouble(match.cube);
bus.emit('toast', { text: `البوت قبل المضاعفة! ×${match.cube.value}` }); bus.emit('toast', { text: `البوت قبل! ×${match.cube.value}` });
} else { } else {
declineDouble(match.cube); declineDouble(match.cube);
bus.emit('toast', { text: 'البوت رفض! فزت بالجولة' }); bus.emit('toast', { text: 'البوت رفض! فزت بالجولة' });
const result = endGame(match, myColor, 1); showGameResult(endGame(match, myColor, 1));
showGameResult(result);
return; return;
} }
updateUI(); updateUI();
...@@ -606,7 +455,7 @@ function onDoubleClick() { ...@@ -606,7 +455,7 @@ function onDoubleClick() {
function onAcceptDouble() { function onAcceptDouble() {
if (!match.cube) return; if (!match.cube) return;
acceptDouble(match.cube); acceptDouble(match.cube);
audio.play('sfx_bg_double', 'game'); audio.play('click');
container.querySelector('#double-offer').style.display = 'none'; container.querySelector('#double-offer').style.display = 'none';
updateUI(); updateUI();
if (params.mode === 'live') syncToServer(); if (params.mode === 'live') syncToServer();
...@@ -616,127 +465,92 @@ function onDeclineDouble() { ...@@ -616,127 +465,92 @@ function onDeclineDouble() {
if (!match.cube) return; if (!match.cube) return;
declineDouble(match.cube); declineDouble(match.cube);
container.querySelector('#double-offer').style.display = 'none'; container.querySelector('#double-offer').style.display = 'none';
const result = endGame(match, game.turn, 1); showGameResult(endGame(match, game.turn, 1));
showGameResult(result);
} }
// ═══════════════════════════════════════ // ── Bot ──
// BOT AI
// ═══════════════════════════════════════
function doBotTurn() { function doBotTurn() {
if (game.turn === myColor || game.isOver) return; if (game.turn === myColor || game.isOver) return;
// Bot considers doubling // Doubling consideration
if (match.cube && canUseCube(match) && canDouble(match.cube, game.turn) && !game.dice) { if (match.cube && canUseCube(match) && canDouble(match.cube, game.turn) && !game.dice) {
const botPips = getPipCount(game.state, game.turn); const botPips = getPipCount(game.state, game.turn);
const myPips = getPipCount(game.state, myColor); const myPips = getPipCount(game.state, myColor);
if (shouldBotDouble(match.cube, botPips, myPips, params.difficulty)) { if (shouldBotDouble(match.cube, botPips, myPips, params.difficulty)) {
offerDouble(match.cube, game.turn); offerDouble(match.cube, game.turn);
audio.play('sfx_bg_double', 'game'); audio.play('sfx_bg_double', 'game');
const offerUI = container.querySelector('#double-offer'); container.querySelector('#double-offer').style.display = '';
const valEl = container.querySelector('#double-val'); container.querySelector('#double-val').textContent = match.cube.value * 2;
if (offerUI) offerUI.style.display = '';
if (valEl) valEl.textContent = match.cube.value * 2;
return; return;
} }
} }
// Roll // Roll
audio.play('sfx_bg_dice_roll', 'game');
isRolling = true; isRolling = true;
diceRollFrame = 0; diceRollFrame = 0;
audio.play('sfx_bg_dice_roll', 'game');
createRollAnimation( createRollAnimation(
(frame, total) => { diceRollFrame = frame; diceRollTotal = total; }, (frame, total) => { diceRollFrame = frame; diceRollTotal = total; },
() => { () => {
isRolling = false; isRolling = false;
rollDice(game); rollDice(game);
if (game.movesLeft.length === 0) { setTimeout(endTurn, 500); return; }
if (game.movesLeft.length === 0) { botPlayMoves();
setTimeout(endTurn, 700);
return;
}
makeBotMoves();
} }
); );
} }
function makeBotMoves() { function botPlayMoves() {
if (game.movesLeft.length === 0 || game.isOver) { if (game.movesLeft.length === 0 || game.isOver) {
if (!game.isOver) endTurn(); if (!game.isOver) endTurn(); else handleGameOver();
else handleGameOver();
return; return;
} }
const move = getBotMove(game, params.difficulty); const move = getBotMove(game, params.difficulty);
if (!move) { endTurn(); return; } if (!move) { endTurn(); return; }
const fromPos = move.from;
const toPos = move.to;
const pieceType = game.turn;
isAnimating = true; isAnimating = true;
createMoveAnimation(fromPos, toPos, layout, createMoveAnimation(move.from, move.to, layout,
(frame) => { animatingChecker = { x: frame.x, y: frame.y, type: pieceType, scale: frame.scale }; }, (frame) => { animatingChecker = { x: frame.x, y: frame.y, type: game.turn }; },
() => { () => {
animatingChecker = null; animatingChecker = null;
audio.play('sfx_bg_piece_move', 'game'); audio.play('sfx_bg_piece_move', 'game');
const actions = applyMove(game, move.piece.id, move.steps); const actions = applyMove(game, move.piece.id, move.steps);
if (actions?.some(a => a.type === 'hit')) { if (actions?.some(a => a.type === 'hit')) {
audio.play('sfx_bg_piece_hit', 'game'); audio.play('sfx_bg_piece_hit', 'game');
spawnHitParticles(canvas._logicalW / 2, canvas._logicalH / 2); spawnParticles(logicalW / 2, logicalH / 2, ['#EF4444', '#F97316']);
} }
if (actions?.some(a => a.type === 'bear')) {
audio.play('sfx_bg_bear_off', 'game');
spawnBearOffParticles(canvas._logicalW - 20, canvas._logicalH / 2);
}
isAnimating = false; isAnimating = false;
updateUI(); updateUI();
if (game.isOver) { handleGameOver(); return; } if (game.isOver) { handleGameOver(); return; }
setTimeout(makeBotMoves, 350); setTimeout(botPlayMoves, 300);
} }
); );
} }
// ═══════════════════════════════════════ // ── Game Over ──
// GAME OVER
// ═══════════════════════════════════════
function handleGameOver() { function handleGameOver() {
const winner = game.winner; const isMyWin = game.winner === myColor;
const isMyWin = winner === myColor;
const result = endGame(match, winner, game.score || 1);
audio.play(isMyWin ? 'sfx_bg_win_game' : 'lose', 'game'); audio.play(isMyWin ? 'sfx_bg_win_game' : 'lose', 'game');
if (isMyWin) spawnWinParticles(); if (isMyWin) spawnParticles(logicalW / 2, logicalH * 0.3, ['#FFD700', '#10B981', '#8B5CF6'], 20);
showGameResult(endGame(match, game.winner, game.score || 1));
showGameResult(result);
} }
function showGameResult(result) { function showGameResult(result) {
updateUI(); updateUI();
if (result.matchOver) { if (result.matchOver) {
audio.play(result.winner === myColor ? 'sfx_bg_win_match' : 'lose', 'game'); audio.play(result.winner === myColor ? 'sfx_bg_win_match' : 'lose', 'game');
if (result.winner === myColor) spawnWinParticles();
setTimeout(() => { setTimeout(() => {
if (params.mode === 'live') syncComplete(result); if (params.mode === 'live') syncComplete(result);
scene.replace('backgammon-result', { scene.replace('backgammon-result', {
winner: result.winner === myColor ? 'you' : 'opponent', winner: result.winner === myColor ? 'you' : 'opponent',
scores: result.scores, scores: result.scores, matchLength: match.length, mode: params.mode
matchLength: match.length,
mode: params.mode
}); });
}, 2000); }, 1800);
} else { } else {
const who = result.winner === myColor ? 'فزت' : 'البوت فاز'; const who = result.winner === myColor ? 'فزت' : 'البوت فاز';
const scoreLabel = result.gameScore === 3 ? 'باكگمون!' : result.gameScore === 2 ? 'گامون!' : ''; bus.emit('toast', { text: `${who} بالجولة (+${result.stake})` });
bus.emit('toast', { text: `${who} بالجولة ${scoreLabel} (+${result.stake})` }); setTimeout(startNewGameRound, 2000);
setTimeout(startNewGameRound, 2500);
} }
} }
...@@ -747,25 +561,35 @@ function endMatchWithWin() { ...@@ -747,25 +561,35 @@ function endMatchWithWin() {
}); });
} }
// ═══════════════════════════════════════ // ── Particles ──
// MULTIPLAYER SYNC function spawnParticles(x, y, colors, count = 10) {
// ═══════════════════════════════════════ for (let i = 0; i < count; i++) {
const angle = (Math.PI * 2 * i) / count;
const speed = 60 + Math.random() * 100;
particles.push({
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed - 80,
alpha: 1,
size: 2 + Math.random() * 3,
color: colors[i % colors.length]
});
}
}
// ── Multiplayer ──
function syncToServer() { function syncToServer() {
if (params.mode !== 'live') return; if (params.mode !== 'live') return;
net.post('backgammon-match.php', { net.post('backgammon-match.php', {
action: 'move', match_id: params.matchId, action: 'move', match_id: params.matchId,
game_state: serializeState(game), game_state: serializeState(game), match_state: getMatchState(match), cube: match.cube
match_state: getMatchState(match),
cube: match.cube
}).catch(() => {}); }).catch(() => {});
} }
function syncComplete(result) { function syncComplete(result) {
net.post('backgammon-match.php', { net.post('backgammon-match.php', {
action: 'complete', match_id: params.matchId, action: 'complete', match_id: params.matchId,
winner: result.winner === myColor ? store.get('auth.userId') : 'opponent', winner: result.winner === myColor ? store.get('auth.userId') : 'opponent', reason: 'normal'
reason: 'normal'
}).catch(() => {}); }).catch(() => {});
} }
...@@ -777,60 +601,50 @@ function handleServerState(data) { ...@@ -777,60 +601,50 @@ function handleServerState(data) {
if (data.match_state) match.scores = data.match_state.scores || match.scores; if (data.match_state) match.scores = data.match_state.scores || match.scores;
if (data.cube) match.cube = data.cube; if (data.cube) match.cube = data.cube;
updateUI(); updateUI();
updateTurnIndicator();
} }
} }
// ═══════════════════════════════════════ // ── Emotes ──
// EMOTES
// ═══════════════════════════════════════
function setupEmotePanel(el) { function setupEmotePanel(el) {
const panel = el.querySelector('#emote-panel'); const panel = el.querySelector('#emote-panel');
const emotes = ['🎲', '😤', '🤔', '👏', '😂', '🔥', '💀', '🫡']; const emotes = ['🎲', '😤', '🤔', '👏', '😂', '🔥', '💀', '🫡'];
const phrases = ['!لعبة حلوة', '!حركة ممتازة', '!حظ', '...فكّر أسرع', '?ريماتش', 'gg wp']; const phrases = ['!لعبة حلوة', '!حركة ممتازة', '!حظ', '...فكّر أسرع', '?ريماتش', 'gg wp'];
panel.innerHTML = ` panel.innerHTML = `
<div class="bgg-emotes">${emotes.map(e => `<button class="bgg-emote-btn">${e}</button>`).join('')}</div> <div class="bgg-emotes">${emotes.map(e => `<button class="bgg-emote-btn">${e}</button>`).join('')}</div>
<div class="bgg-phrases">${phrases.map(p => `<button class="bgg-phrase-btn">${p}</button>`).join('')}</div> <div class="bgg-phrases">${phrases.map(p => `<button class="bgg-phrase-btn">${p}</button>`).join('')}</div>
`; `;
let lastT = 0;
let lastEmote = 0;
panel.querySelectorAll('.bgg-emote-btn').forEach(btn => { panel.querySelectorAll('.bgg-emote-btn').forEach(btn => {
btn.onclick = () => { btn.onclick = () => {
if (Date.now() - lastEmote < 3000) return; if (Date.now() - lastT < 3000) return; lastT = Date.now();
lastEmote = Date.now();
audio.play('sfx_emote'); audio.play('sfx_emote');
showBubble(btn.textContent, 'emote'); showBubble(btn.textContent);
panel.style.display = 'none'; panel.style.display = 'none';
if (params.mode === 'live') { if (params.mode === 'live') net.post('backgammon-match.php', { action: 'emote', match_id: params.matchId, emote: btn.textContent }).catch(() => {});
net.post('backgammon-match.php', { action: 'emote', match_id: params.matchId, emote: btn.textContent }).catch(() => {});
}
}; };
}); });
panel.querySelectorAll('.bgg-phrase-btn').forEach(btn => { panel.querySelectorAll('.bgg-phrase-btn').forEach(btn => {
btn.onclick = () => { btn.onclick = () => {
if (Date.now() - lastEmote < 3000) return; if (Date.now() - lastT < 3000) return; lastT = Date.now();
lastEmote = Date.now();
audio.play('sfx_emote'); audio.play('sfx_emote');
showBubble(btn.textContent, 'phrase'); showBubble(btn.textContent);
panel.style.display = 'none'; panel.style.display = 'none';
}; };
}); });
} }
function toggleEmotePanel() { function toggleEmotePanel() {
const panel = container?.querySelector('#emote-panel'); const p = container?.querySelector('#emote-panel');
if (panel) panel.style.display = panel.style.display === 'none' ? '' : 'none'; if (p) p.style.display = p.style.display === 'none' ? '' : 'none';
} }
function showBubble(text, type) { function showBubble(text) {
if (!container) return; if (!container) return;
const bubble = document.createElement('div'); const b = document.createElement('div');
bubble.className = `bgg-bubble bgg-bubble-${type}`; b.className = 'bgg-bubble';
bubble.textContent = text; b.textContent = text;
container.appendChild(bubble); container.appendChild(b);
setTimeout(() => bubble.remove(), 2500); setTimeout(() => b.remove(), 2200);
} }
function onQuit() { function onQuit() {
...@@ -843,189 +657,114 @@ function onQuit() { ...@@ -843,189 +657,114 @@ function onQuit() {
scene.replace('backgammon-room', { mode: 'menu' }); scene.replace('backgammon-room', { mode: 'menu' });
} }
// ═══════════════════════════════════════ // ── Styles ──
// HELPERS function getStyles() {
// ═══════════════════════════════════════
function getDiffLabel(d) {
if (d === 'easy') return 'سهل';
if (d === 'hard') return 'صعب';
return 'متوسط';
}
function getVariantLabel(v) {
if (v === 'mahbousa') return 'محبوسة';
if (v === 'thirtyone') return '٣١';
return 'شيش بيش';
}
function getGameStyles() {
return `<style> return `<style>
.bgg-container { .bgg-container {
position:relative;display:flex;flex-direction:column; display:flex;flex-direction:column;height:100%;
height:100%;background:linear-gradient(180deg,#060A10 0%,#0A1020 100%); background:#0a0f14;color:#f8fafc;overflow:hidden;
color:#f8fafc;overflow:hidden;font-family:var(--font-ar,'IBM Plex Sans Arabic',sans-serif); font-family:var(--font-ar,'IBM Plex Sans Arabic',sans-serif);
position:relative;
} }
.bgg-top-bar,.bgg-bottom-bar { .bgg-top-bar,.bgg-bottom-bar {
display:flex;align-items:center;padding:10px 14px;gap:10px; display:flex;align-items:center;padding:8px 12px;gap:8px;
background:rgba(0,0,0,0.4);backdrop-filter:blur(8px); background:rgba(0,0,0,0.4);
border-bottom:1px solid rgba(255,255,255,0.04);
}
.bgg-bottom-bar { border-bottom:none;border-top:1px solid rgba(255,255,255,0.04);justify-content:space-between; }
.bgg-player-card { display:flex;align-items:center;gap:10px;flex:1; }
.bgg-avatar-ring {
width:40px;height:40px;border-radius:50%;padding:2px;
background:conic-gradient(from 0deg,rgba(148,163,184,0.3),rgba(148,163,184,0.1));
} }
.bgg-my-ring { background:conic-gradient(from 0deg,rgba(16,185,129,0.5),rgba(16,185,129,0.2)); } .bgg-bottom-bar { justify-content:space-between; }
.bgg-opp-ring { background:conic-gradient(from 0deg,rgba(239,68,68,0.4),rgba(239,68,68,0.15)); } .bgg-player-card { display:flex;align-items:center;gap:8px;flex:1; }
.bgg-avatar { .bgg-avatar {
width:100%;height:100%;border-radius:50%; width:34px;height:34px;border-radius:50%;
display:flex;align-items:center;justify-content:center; display:flex;align-items:center;justify-content:center;
background:rgba(255,255,255,0.04);font-size:18px; background:rgba(255,255,255,0.06);font-size:16px;
border:2px solid rgba(255,255,255,0.08);
} }
.bgg-player-info { flex:1; } .bgg-player-info { flex:1; }
.bgg-name { font-size:13px;font-weight:700;color:#f8fafc; } .bgg-name { font-size:13px;font-weight:700; }
.bgg-pips { font-size:11px;color:#64748b; } .bgg-pips { font-size:11px;color:#64748b; }
.bgg-score-badge { .bgg-score { font-size:18px;font-weight:800;color:#94a3b8;min-width:20px;text-align:center; }
font-size:18px;font-weight:800;color:#94a3b8; .bgg-my-score { color:#d4940a; }
min-width:28px;height:28px;border-radius:8px;
display:flex;align-items:center;justify-content:center;
background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.06);
}
.bgg-my-score { color:#d4940a;border-color:rgba(212,148,10,0.2);background:rgba(212,148,10,0.06); }
.bgg-board-area { .bgg-board-area {
flex:1;display:flex;align-items:center;justify-content:center; flex:1;display:flex;align-items:center;justify-content:center;padding:4px;
position:relative;padding:4px;
} }
#bg-canvas { border-radius:8px;touch-action:none;cursor:pointer; } #bg-canvas { border-radius:6px;touch-action:none;cursor:pointer; }
.bgg-dice-overlay { .bgg-controls {
position:absolute;bottom:16px;left:50%;transform:translateX(-50%);z-index:5; display:flex;align-items:center;justify-content:center;gap:10px;padding:8px;
min-height:52px;
} }
.bgg-roll-btn { .bgg-roll-btn {
display:flex;align-items:center;gap:8px; display:flex;align-items:center;gap:6px;
padding:14px 28px;border-radius:16px;border:none; padding:12px 28px;border-radius:14px;border:none;
background:linear-gradient(135deg,#d4940a 0%,#8B4513 100%); background:linear-gradient(135deg,#d4940a,#8B4513);
color:#fff;font-size:16px;font-weight:800;cursor:pointer; color:#fff;font-size:15px;font-weight:700;cursor:pointer;
box-shadow:0 6px 24px rgba(212,148,10,0.5),inset 0 1px 0 rgba(255,255,255,0.2); box-shadow:0 4px 16px rgba(212,148,10,0.4);
animation:rollPulse 2.5s ease-in-out infinite; transition:transform 0.12s;
transition:transform 0.15s var(--ease-spring);
}
.bgg-roll-btn:active { transform:scale(0.92);animation:none; }
@keyframes rollPulse {
0%,100%{box-shadow:0 6px 24px rgba(212,148,10,0.4)}
50%{box-shadow:0 6px 32px rgba(212,148,10,0.7),0 0 60px rgba(212,148,10,0.15)}
} }
.bgg-roll-btn:active { transform:scale(0.92); }
.bgg-cube-area { position:absolute;top:12px;right:12px;z-index:5; }
.bgg-cube-btn { .bgg-cube-btn {
display:flex;align-items:center;gap:6px; padding:8px 16px;border-radius:10px;border:none;
padding:8px 14px;border-radius:10px;border:none;
background:rgba(139,92,246,0.15);border:1.5px solid rgba(139,92,246,0.4); background:rgba(139,92,246,0.15);border:1.5px solid rgba(139,92,246,0.4);
color:#a78bfa;font-size:12px;font-weight:700;cursor:pointer; color:#a78bfa;font-size:13px;font-weight:700;cursor:pointer;
transition:all 0.2s;
} }
.bgg-cube-btn:active { transform:scale(0.9);background:rgba(139,92,246,0.3); } .bgg-cube-btn:active { transform:scale(0.9); }
.bgg-cube-icon { font-size:16px; }
.bgg-double-offer { .bgg-double-offer {
position:absolute;top:50%;left:50%;transform:translate(-50%,-50%); position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
background:rgba(10,16,32,0.95);border:1.5px solid rgba(212,148,10,0.5); background:rgba(10,15,20,0.95);border:1.5px solid #d4940a;
border-radius:20px;padding:24px 28px;text-align:center;z-index:20; border-radius:16px;padding:20px 24px;text-align:center;z-index:20;
backdrop-filter:blur(12px);box-shadow:0 12px 40px rgba(0,0,0,0.6);
animation:offerIn 0.3s var(--ease-spring);
} }
@keyframes offerIn { from{opacity:0;transform:translate(-50%,-50%) scale(0.85)} to{opacity:1;transform:translate(-50%,-50%) scale(1)} } .bgg-double-offer p { margin:0 0 12px;font-size:14px;color:#e2e8f0; }
.bgg-offer-title { font-size:18px;font-weight:800;color:#d4940a;margin-bottom:8px; }
.bgg-offer-desc { font-size:13px;color:#94a3b8;margin:0 0 16px; }
.bgg-offer-btns { display:flex;gap:10px;justify-content:center; }
.bgg-accept-btn,.bgg-decline-btn { .bgg-accept-btn,.bgg-decline-btn {
padding:10px 22px;border-radius:12px;border:none;font-weight:700;font-size:13px;cursor:pointer; padding:10px 20px;border-radius:10px;border:none;
transition:transform 0.15s var(--ease-spring); font-weight:700;font-size:13px;cursor:pointer;margin:0 4px;
} }
.bgg-accept-btn { background:linear-gradient(135deg,#10B981,#059669);color:#fff;box-shadow:0 4px 12px rgba(16,185,129,0.3); } .bgg-accept-btn { background:#10b981;color:#fff; }
.bgg-decline-btn { background:linear-gradient(135deg,#EF4444,#DC2626);color:#fff;box-shadow:0 4px 12px rgba(239,68,68,0.3); } .bgg-decline-btn { background:#ef4444;color:#fff; }
.bgg-accept-btn:active,.bgg-decline-btn:active { transform:scale(0.9); }
.bgg-no-moves-toast { .bgg-actions { display:flex;gap:6px; }
position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
padding:12px 24px;border-radius:12px;
background:rgba(239,68,68,0.15);border:1px solid rgba(239,68,68,0.3);
color:#f87171;font-size:14px;font-weight:700;
animation:toastIn 0.3s var(--ease-spring);
}
@keyframes toastIn { from{opacity:0;transform:translate(-50%,-50%) scale(0.8)} }
.bgg-actions { display:flex;gap:8px; }
.bgg-action-btn { .bgg-action-btn {
width:38px;height:38px;border-radius:12px;border:none; width:36px;height:36px;border-radius:50%;border:none;
background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.06); background:rgba(255,255,255,0.05);color:#94a3b8;
color:#94a3b8;font-size:16px;cursor:pointer; font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;
display:flex;align-items:center;justify-content:center;
transition:all 0.15s;
} }
.bgg-action-btn:active { transform:scale(0.85);background:rgba(255,255,255,0.08); } .bgg-action-btn:active { background:rgba(255,255,255,0.1); }
.bgg-action-quit { color:#ef4444;border-color:rgba(239,68,68,0.2); } .bgg-quit { color:#ef4444; }
.bgg-turn-badge { .bgg-turn-badge {
position:absolute;top:50%;left:6px;transform:translateY(-50%); position:absolute;top:50%;left:4px;transform:translateY(-50%);
writing-mode:vertical-rl;font-size:10px;font-weight:800;letter-spacing:0.5px; writing-mode:vertical-rl;font-size:10px;font-weight:700;
padding:8px 5px;border-radius:8px; padding:6px 4px;border-radius:6px;
transition:all 0.3s var(--ease-spring);
}
.bgg-turn-mine { background:rgba(16,185,129,0.12);color:#34D399;border:1px solid rgba(16,185,129,0.2); }
.bgg-turn-opp { background:rgba(239,68,68,0.1);color:#f87171;border:1px solid rgba(239,68,68,0.15); }
.bgg-match-info {
position:absolute;top:50%;right:6px;transform:translateY(-50%);
display:flex;flex-direction:column;align-items:center;gap:4px;
}
.bgg-match-label,.bgg-variant-label {
writing-mode:vertical-rl;font-size:9px;font-weight:600;color:#475569;
padding:4px 3px;border-radius:4px;background:rgba(255,255,255,0.02);
} }
.bgg-turn-mine { background:rgba(16,185,129,0.15);color:#34D399; }
.bgg-turn-opp { background:rgba(239,68,68,0.1);color:#f87171; }
.bgg-emote-panel { .bgg-emote-panel {
position:absolute;bottom:64px;right:14px; position:absolute;bottom:56px;right:12px;
background:rgba(10,16,32,0.95);border:1px solid rgba(255,255,255,0.08); background:rgba(10,15,20,0.95);border:1px solid rgba(255,255,255,0.08);
border-radius:16px;padding:14px;z-index:25;max-width:260px; border-radius:14px;padding:12px;z-index:20;max-width:240px;
backdrop-filter:blur(12px);box-shadow:0 8px 32px rgba(0,0,0,0.5);
animation:panelIn 0.2s var(--ease-spring);
} }
@keyframes panelIn { from{opacity:0;transform:translateY(8px) scale(0.95)} } .bgg-emotes { display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px; }
.bgg-emotes { display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px; }
.bgg-emote-btn { .bgg-emote-btn {
width:38px;height:38px;border-radius:10px;border:none; width:36px;height:36px;border-radius:8px;border:none;
background:rgba(255,255,255,0.05);font-size:20px;cursor:pointer; background:rgba(255,255,255,0.05);font-size:20px;cursor:pointer;
transition:transform 0.15s var(--ease-spring);
} }
.bgg-emote-btn:active { transform:scale(0.85);background:rgba(255,255,255,0.1); } .bgg-emote-btn:active { background:rgba(255,255,255,0.12); }
.bgg-phrases { display:flex;flex-direction:column;gap:4px; } .bgg-phrases { display:flex;flex-direction:column;gap:4px; }
.bgg-phrase-btn { .bgg-phrase-btn {
padding:7px 12px;border-radius:8px;border:none; padding:6px 10px;border-radius:6px;border:none;
background:rgba(255,255,255,0.03);color:#94a3b8; background:rgba(255,255,255,0.03);color:#94a3b8;
font-size:12px;cursor:pointer;text-align:right; font-size:12px;cursor:pointer;text-align:right;
transition:background 0.15s;
} }
.bgg-phrase-btn:active { background:rgba(255,255,255,0.08); } .bgg-phrase-btn:active { background:rgba(255,255,255,0.08); }
.bgg-bubble { .bgg-bubble {
position:absolute;pointer-events:none;z-index:30; position:absolute;bottom:80px;right:50px;font-size:36px;
animation:bubbleFloat 2.5s ease-out forwards; pointer-events:none;animation:bubblePop 2.2s ease-out forwards;
}
.bgg-bubble-emote { bottom:80px;right:60px;font-size:44px; }
.bgg-bubble-phrase {
bottom:80px;right:20px;
padding:10px 16px;border-radius:14px;
background:rgba(212,148,10,0.12);border:1px solid rgba(212,148,10,0.25);
color:#d4940a;font-size:13px;font-weight:700;
} }
@keyframes bubbleFloat { @keyframes bubblePop {
0%{opacity:1;transform:translateY(0) scale(1)} 0%{opacity:1;transform:translateY(0) scale(1)}
70%{opacity:1} 100%{opacity:0;transform:translateY(-40px) scale(1.2)}
100%{opacity:0;transform:translateY(-50px) scale(1.15)}
} }
</style>`; </style>`;
} }
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