Trik: posledna karta sa prida bez blikania, kopka sa zmetie k vitazovi

Doteraz sa pri hodeni 4. karty cela kopka na frame stratila (displayedStash
padol na lingeredStash nastaveny az v useEffekte) a nasledne sa vsetky karty
znovu vlietli. Teraz sa previous_stash cita synchronne, takze posledna karta
len pribudne k trom uz leziacim, a potom sa cela kopka odsunie animaciou
smerom k sedadlu, ktore kopku vyhralo (stashWinner podla pravidiel enginu).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
tim
2026-07-07 10:59:42 +02:00
co-authored by Claude Opus 4.8
parent 1fbba5a7e1
commit fb90737944
3 changed files with 92 additions and 17 deletions
+44 -16
View File
@@ -1,9 +1,9 @@
import { useEffect, useRef, useState } from 'react';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useGameStore } from '../store/gameStore';
import { emit } from '../lib/socket';
import { leaveGame } from '../lib/leaveGame';
import { computePlayable } from '../lib/gameRules';
import { computePlayable, stashWinner } from '../lib/gameRules';
import { computeTotal } from '../lib/standings';
import { useIsDesktop } from '../lib/useIsDesktop';
import { useFitScale } from '../lib/useFitScale';
@@ -16,7 +16,12 @@ import FaceDownCards from '../components/FaceDownCards';
import GameOver from './GameOver';
import type { PlayerInfo, StashData } from '../types';
const TRICK_LINGER_MS = 3000;
// A completed trick stays face-up for SETTLE_MS (so the last card visibly joins
// the pile), then is swept towards the winner over COLLECT_MS.
const SETTLE_MS = 1100;
const COLLECT_MS = 550;
// Sweep direction by the winner's seat offset from me: 0=me(bottom) 1=left 2=top 3=right.
const COLLECT_BY_OFFSET = ['collect-bottom', 'collect-left', 'collect-top', 'collect-right'];
export default function GameTable() {
const navigate = useNavigate();
@@ -28,26 +33,31 @@ export default function GameTable() {
const gameStatus = useGameStore((s) => s.gameStatus);
const hand = useGameStore((s) => s.hand);
// Hold the last completed trick visible for TRICK_LINGER_MS after it finishes.
const [lingeredStash, setLingeredStash] = useState<StashData | null>(null);
const lingerTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
// Once a completed trick has been swept away, its key is remembered here so it
// is not shown again while we wait for the winner to lead the next trick.
const [dismissedKey, setDismissedKey] = useState<string | null>(null);
// Turns on for the collect (fly-to-winner) phase, after the settle pause.
const [collecting, setCollecting] = useState(false);
const previousStash = gameStatus?.status.previous_stash ?? null;
// Every game_status payload recreates the stash object, so identify the trick
// by content — the timer must restart only when a *different* trick completes.
// by content — the timers must restart only when a *different* trick completes.
const previousStashKey = previousStash
? `${previousStash.first_player}:${JSON.stringify(previousStash.cards)}`
: null;
useEffect(() => {
if (!previousStash) return;
setLingeredStash(previousStash);
if (lingerTimer.current) clearTimeout(lingerTimer.current);
lingerTimer.current = setTimeout(() => setLingeredStash(null), TRICK_LINGER_MS);
if (!previousStashKey) return;
setCollecting(false);
const settle = setTimeout(() => setCollecting(true), SETTLE_MS);
const done = setTimeout(() => {
setCollecting(false);
setDismissedKey(previousStashKey);
}, SETTLE_MS + COLLECT_MS);
return () => {
if (lingerTimer.current) clearTimeout(lingerTimer.current);
clearTimeout(settle);
clearTimeout(done);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [previousStashKey]);
if (!gameStatus || !myPlayer) {
@@ -73,8 +83,24 @@ export default function GameTable() {
const myTurnToPlay = isPlayPhase && active_player === myOrder;
const activeCards = active_stash ? Object.keys(active_stash.cards).length : 0;
const displayedStash: StashData | null =
activeCards > 0 && active_stash ? active_stash : lingeredStash ?? null;
// Once a player leads, that live trick always wins the centre. Otherwise, if a
// just-completed trick hasn't been swept away yet, keep it face-up. Reading
// `previousStash` synchronously (rather than a state set in an effect) means
// the pile never blinks to empty on the frame the 4th card lands — the last
// card simply joins the three already there, then the whole pile is collected.
const finishing =
previousStashKey !== null && previousStashKey !== dismissedKey && activeCards === 0;
const displayedStash: StashData | null = finishing
? previousStash
: activeCards > 0 && active_stash
? active_stash
: null;
// During the collect phase, sweep the pile towards whoever won it.
const collectAnim =
collecting && finishing && previousStash
? COLLECT_BY_OFFSET[(stashWinner(previousStash) - myOrder + 4) % 4]
: null;
const playableKeys = myTurnToPlay && active_stash
? computePlayable(hand, active_stash.cards[String(active_stash.first_player)]?.color ?? null)
@@ -160,7 +186,9 @@ export default function GameTable() {
// Center of the oval: trick during play, guess controls during bidding.
const ovalContent = isPlayPhase ? (
<Trick stash={displayedStash} players={players} myOrder={myOrder} />
<div style={collectAnim ? { animation: `${collectAnim} ${COLLECT_MS}ms ease-in both` } : undefined}>
<Trick stash={displayedStash} players={players} myOrder={myOrder} />
</div>
) : (
active_round_guesses !== undefined && active_player !== undefined ? (
<GuessControls