Stage 17 (#9): animated shuffle — tiles hop along a low parabola to new slots
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 28s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 53s
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 28s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 53s
Give each rack slot a stable id permuted with the letters on shuffle, so the keyed rack reorders (rather than relabelling in place) and Svelte's animate directive fires. hop flies each tile along a parabola (apogee ~half a tile height) with a duration that scales with the horizontal distance (arc length): the longest 1<->7 swap takes ~0.5s, shorter swaps land sooner. Ordinary reflow (place/recall) stays instant via a guard. e2e locks that a shuffle preserves the rack's tile multiset.
This commit is contained in:
+18
-5
@@ -43,6 +43,10 @@
|
||||
let zoomed = $state(false);
|
||||
let selected = $state<number | null>(null);
|
||||
let focus = $state<{ row: number; col: number } | null>(null);
|
||||
// A stable id per rack slot, permuted together with the letters on shuffle, so the rack
|
||||
// tiles fly to their new positions (Rack's hop animation) instead of relabelling in place.
|
||||
let rackIds = $state<number[]>([]);
|
||||
let shuffling = $state(false);
|
||||
let panel = $state<'none' | 'chat'>('none');
|
||||
let historyOpen = $state(false);
|
||||
let blankPrompt = $state<{ rackIndex: number; row: number; col: number } | null>(null);
|
||||
@@ -81,6 +85,7 @@
|
||||
view.game.toMove === view.seat,
|
||||
);
|
||||
const slots = $derived(rackView(placement));
|
||||
const rackSlots = $derived(slots.map((s) => ({ ...s, id: rackIds[s.index] ?? s.index })));
|
||||
const isMyTurn = $derived(!!view && view.game.status === 'active' && view.game.toMove === view.seat);
|
||||
const gameOver = $derived(!!view && view.game.status !== 'active');
|
||||
const bagEmpty = $derived((view?.bagLen ?? 0) === 0);
|
||||
@@ -98,6 +103,7 @@
|
||||
moves = hist.moves;
|
||||
setCachedGame(id, st, hist.moves);
|
||||
placement = newPlacement(st.rack);
|
||||
rackIds = st.rack.map((_, i) => i);
|
||||
preview = null;
|
||||
selected = null;
|
||||
dirOverride = undefined;
|
||||
@@ -122,6 +128,7 @@
|
||||
view = cached.view;
|
||||
moves = cached.moves;
|
||||
placement = newPlacement(cached.view.rack);
|
||||
rackIds = cached.view.rack.map((_, i) => i);
|
||||
}
|
||||
void load();
|
||||
});
|
||||
@@ -373,12 +380,18 @@
|
||||
}
|
||||
function shuffle() {
|
||||
if (placement.pending.length > 0) return;
|
||||
const r = [...placement.rack];
|
||||
for (let i = r.length - 1; i > 0; i--) {
|
||||
// Shuffle an index permutation, then apply it to both the letters and the slot ids so
|
||||
// each tile keeps its id as it flies to a new position (driving Rack's hop animation).
|
||||
const order = placement.rack.map((_, i) => i);
|
||||
for (let i = order.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[r[i], r[j]] = [r[j], r[i]];
|
||||
[order[i], order[j]] = [order[j], order[i]];
|
||||
}
|
||||
placement = newPlacement(r);
|
||||
rackIds = order.map((i) => rackIds[i] ?? i);
|
||||
placement = newPlacement(order.map((i) => placement.rack[i]));
|
||||
selected = null;
|
||||
shuffling = true;
|
||||
setTimeout(() => (shuffling = false), 600);
|
||||
// A short "shake": a few quick light taps rather than one.
|
||||
for (let i = 0; i < 4; i++) setTimeout(() => telegramHaptic('light'), i * 55);
|
||||
}
|
||||
@@ -589,7 +602,7 @@
|
||||
a finished game shows the final rack greyed out and the controls disabled. -->
|
||||
<div class="rack-row" class:inert={gameOver}>
|
||||
<div class="rack-wrap">
|
||||
<Rack {slots} {variant} {selected} ondown={onRackDown} />
|
||||
<Rack slots={rackSlots} {variant} {selected} {shuffling} ondown={onRackDown} />
|
||||
</div>
|
||||
{#if !gameOver && placement.pending.length > 0}
|
||||
<button class="make" onclick={commit} disabled={busy} aria-label={t('game.makeMove')}>✅</button>
|
||||
|
||||
Reference in New Issue
Block a user