Commit 189ce786 authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix: domino — wrong tile placement bug, forced boneyard draw, fly animation, visual pile, win VFX

- Validate tile exists in player's hand before executing placement
- Draw continuously from boneyard until playable tile found (with 400ms delay per draw)
- Tile flies from hand area to board position instead of instant scale-in
- Visual boneyard pile on left side with live counter
- Multi-wave confetti + starBurst + screen flash on match win
- Proper tile aspect ratio (34x68, 1:2) with 8px hand spacing
- Flush tile contact on board (GAP=1)
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 31cf6666
......@@ -170,15 +170,20 @@ export class DominoBoard {
h = dbl ? TILE_W : TILE_H;
}
const duration = 200;
const startX = this.width / 2;
const startY = this.height + 40;
const duration = 350;
const startTime = performance.now();
const animate = (now) => {
const t = Math.min((now - startTime) / duration, 1);
const ease = 1 - Math.pow(1 - t, 3);
const scale = 1.3 + (1 - 1.3) * ease;
this.animatingTile = { tile, x: ep.x, y: ep.y, w, h, rotation: 0, scale };
const x = startX + (ep.x - startX) * ease;
const y = startY + (ep.y - startY) * ease;
const scale = 1.4 + (1 - 1.4) * ease;
this.animatingTile = { tile, x, y, w, h, rotation: 0, scale };
this.draw();
if (t < 1) {
......
......@@ -12,7 +12,7 @@ export class DominoHand {
this.disabled = false;
container.style.cssText = `
display:flex;gap:2px;padding:10px 6px;overflow-x:auto;
display:flex;gap:8px;padding:10px 12px;overflow-x:auto;
background:#0f0f1e;
border-top:1px solid rgba(255,255,255,0.06);
min-height:96px;align-items:center;justify-content:center;
......@@ -47,12 +47,11 @@ export class DominoHand {
this.container.innerHTML = hand.map((tile, i) => {
const playable = this.validTileIds.has(tile.id);
const selected = this.selectedTileId === tile.id;
const overlap = i > 0 ? 'margin-left:-6px;' : '';
return `
<div class="dh-tile ${playable ? 'dh-playable' : ''} ${selected ? 'dh-selected' : ''}"
data-id="${tile.id}" data-idx="${i}"
style="${overlap}scroll-snap-align:center;${this.disabled ? 'pointer-events:none;' : ''}">
style="scroll-snap-align:center;${this.disabled ? 'pointer-events:none;' : ''}">
<div class="dh-top">${this._renderPipGrid(tile.left)}</div>
<div class="dh-divider"></div>
<div class="dh-bottom">${this._renderPipGrid(tile.right)}</div>
......@@ -119,7 +118,8 @@ export class DominoHand {
getStyle() {
return `
.dh-tile {
width:36px;height:72px;
width:34px;height:68px;
aspect-ratio:1/2;
background:#F5F0E8;
border:2px solid rgba(255,255,255,0.15);
border-radius:6px;
......
......@@ -5,7 +5,7 @@
export const TILE_W = 24; // narrow dimension
export const TILE_H = 48; // long dimension
export const GAP = 6;
export const GAP = 1; // flush contact between connected tiles on board
const MARGIN = 30;
......
......@@ -125,7 +125,17 @@ function buildLayout(mode) {
<div id="emote-display" style="position:absolute;top:56px;left:50%;transform:translateX(-50%);z-index:50;pointer-events:none;"></div>
<!-- Canvas board -->
<div id="domino-board" style="flex:1;min-height:0;padding:4px;"></div>
<div id="domino-board" style="flex:1;min-height:0;padding:4px;position:relative;">
<!-- Boneyard visual pile -->
<div id="boneyard-pile" style="position:absolute;left:8px;top:50%;transform:translateY(-50%);z-index:10;display:flex;flex-direction:column;align-items:center;gap:4px;pointer-events:none;">
<div id="boneyard-stack" style="position:relative;width:28px;height:44px;">
<div style="position:absolute;top:0;left:0;width:26px;height:42px;background:#d4c9a8;border:2px solid rgba(255,255,255,0.15);border-radius:4px;box-shadow:0 2px 6px rgba(0,0,0,0.4);"></div>
<div style="position:absolute;top:2px;left:2px;width:26px;height:42px;background:#e8dfc4;border:2px solid rgba(255,255,255,0.12);border-radius:4px;box-shadow:0 2px 4px rgba(0,0,0,0.3);"></div>
<div style="position:absolute;top:4px;left:4px;width:26px;height:42px;background:#F5F0E8;border:2px solid rgba(255,255,255,0.1);border-radius:4px;box-shadow:0 2px 8px rgba(0,0,0,0.3);"></div>
</div>
<div id="boneyard-pile-count" style="font-size:11px;font-weight:700;color:#E4AC38;text-shadow:0 1px 3px rgba(0,0,0,0.6);"></div>
</div>
</div>
<!-- Score bar -->
<div id="score-bar" style="display:flex;align-items:center;justify-content:center;gap:12px;padding:6px 12px;background:#0f0f1e;border-top:1px solid rgba(255,255,255,0.04);">
......@@ -597,13 +607,19 @@ function handleDragStart(el, tile, info) {
function executePlacement(el, tile, end) {
if (state.gameOver) return;
// Validate tile actually exists in the player's hand
const myHand = state.hands[state.myPlayerIndex];
const handTile = myHand.find(t => t.id === tile.id);
if (!handTile) return;
// Use the hand's copy to prevent stale reference bugs
const actualEnd = state.chain.length === 0 ? 'right' : end;
const result = rules.placeTile(state.chain, tile, actualEnd);
const result = rules.placeTile(state.chain, handTile, actualEnd);
state.chain = result.chain;
state.leftEnd = result.leftEnd;
state.rightEnd = result.rightEnd;
state.hands[state.myPlayerIndex] = state.hands[state.myPlayerIndex].filter(t => t.id !== tile.id);
state.hands[state.myPlayerIndex] = myHand.filter(t => t.id !== handTile.id);
state.selectedTile = null;
state.moveCount++;
......@@ -628,20 +644,31 @@ function executePlacement(el, tile, end) {
nextTurn(el);
}
function drawFromBoneyard(el) {
async function drawFromBoneyard(el) {
if (state.drawing) return;
if (state.currentPlayer !== state.myPlayerIndex) return;
if (state.boneyard.length === 0) return;
if (state.gameOver) return;
if (rules.hasValidMove(state.hands[state.myPlayerIndex], state.leftEnd, state.rightEnd)) return;
const tile = state.boneyard.pop();
state.hands[state.myPlayerIndex].push(tile);
audio.play('click');
juice.hapticLight?.();
state.drawing = true;
syncDrawToServer();
updateUI(el);
refreshHand();
while (state.boneyard.length > 0 && !rules.hasValidMove(state.hands[state.myPlayerIndex], state.leftEnd, state.rightEnd)) {
const tile = state.boneyard.pop();
state.hands[state.myPlayerIndex].push(tile);
audio.play('click');
juice.hapticLight?.();
syncDrawToServer();
updateUI(el);
refreshHand();
await new Promise(r => setTimeout(r, 400));
}
state.drawing = false;
if (!rules.hasValidMove(state.hands[state.myPlayerIndex], state.leftEnd, state.rightEnd) && state.boneyard.length === 0) {
passTurn(el);
}
}
function passTurn(el) {
......@@ -952,6 +979,11 @@ function updateUI(el) {
const boneEl = el.querySelector('#boneyard-count');
if (boneEl) boneEl.textContent = `المخزن: ${state.boneyard.length}`;
const pileCountEl = el.querySelector('#boneyard-pile-count');
const pileEl = el.querySelector('#boneyard-pile');
if (pileCountEl) pileCountEl.textContent = state.boneyard.length;
if (pileEl) pileEl.style.display = state.boneyard.length > 0 ? '' : 'none';
const myScoreEl = el.querySelector('#my-score');
const oppScoreEl = el.querySelector('#opp-score');
if (myScoreEl) myScoreEl.textContent = state.matchScores[state.myPlayerIndex];
......
......@@ -66,19 +66,38 @@ export function mountResult(el, params) {
animateCounter(el.querySelector('#score-opp'), 0, oppScore, 800);
if (isWin) {
audio.play('win', 'reward');
juice.hapticSuccess?.();
juice.screenFlash?.('#4ade80', 300);
setTimeout(() => {
const iconEl = el.querySelector('#result-icon');
if (iconEl) {
const rect = iconEl.getBoundingClientRect();
juice.confetti?.(rect.left + rect.width / 2, rect.top + rect.height / 2, 50);
juice.starBurst?.(rect.left + rect.width / 2, rect.top - 20, 10);
juice.confetti?.(rect.left + rect.width / 2, rect.top + rect.height / 2, 60);
juice.starBurst?.(rect.left + rect.width / 2, rect.top - 20, 15);
}
}, 400);
// Gold glow on score
}, 300);
setTimeout(() => {
const cx = window.innerWidth / 2;
juice.confetti?.(cx - 60, 80, 30);
juice.confetti?.(cx + 60, 80, 30);
}, 900);
setTimeout(() => {
juice.confetti?.(window.innerWidth / 2, window.innerHeight / 3, 40);
juice.starBurst?.(window.innerWidth / 2, window.innerHeight / 4, 12);
juice.hapticMedium?.();
}, 1500);
setTimeout(() => {
const scoreEl = el.querySelector('#score-me');
if (scoreEl) juice.pulseElement?.(scoreEl, '#fbbf24', 800);
}, 800);
} else if (!isDraw) {
audio.play('lose', 'game');
juice.shake?.(el.querySelector('#result-wrap'), 4);
}
completeOnServer(el, matchId, result, mode);
......
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