diff --git a/src/app/(frontend)/styles.css b/src/app/(frontend)/styles.css index f3f2762..74a2c4e 100644 --- a/src/app/(frontend)/styles.css +++ b/src/app/(frontend)/styles.css @@ -1414,6 +1414,78 @@ overflow: hidden; } +/* Poker chip flight: every chip starts stacked at one origin and launches + one after another (130ms stagger, fill-mode both holds unlaunched chips + in the pile). Wins drop from the pot plaque (~40% of the felt) down to + the viewer card spot (bottom 7rem, so about 14rem of travel); losses fly + the reverse leg from the card spot up to the pot. Chips are the shared + .bj-chip pieces; the geometry is poker-scoped because blackjack flies + between different anchors. */ +.poker-chip-flight { + position: absolute; + left: 50%; + z-index: 35; + width: 1.6rem; + height: 0.95rem; + transform: translateX(-50%); + pointer-events: none; +} + +.poker-chip-flight-win { + top: 40%; +} + +.poker-chip-flight-loss { + bottom: 7.6rem; +} + +.poker-chip-flight .bj-chip { + bottom: auto; + left: 0; +} + +.poker-chip-flight-win .bj-chip { + animation: poker-chip-fly-win 700ms ease-in both; +} + +.poker-chip-flight-loss .bj-chip { + animation: poker-chip-fly-loss 700ms ease-in both; +} + +@keyframes poker-chip-fly-win { + 0% { + opacity: 1; + transform: translateY(0) scale(1); + } + 70% { + opacity: 1; + } + 100% { + opacity: 0; + transform: translateY(14rem) scale(0.7); + } +} + +@keyframes poker-chip-fly-loss { + 0% { + opacity: 1; + transform: translateY(0) scale(1); + } + 70% { + opacity: 1; + } + 100% { + opacity: 0; + transform: translateY(-14rem) scale(0.7); + } +} + +@media (prefers-reduced-motion: reduce) { + .poker-chip-flight { + display: none; + } +} + /* Fullscreen contain-fit: absolute with zero insets + auto margins sizes the felt to the largest 4:3 rectangle inside the viewport area (the same rules browsers use for replaced elements), so it can never collapse to content diff --git a/src/components/frontend/minigames/poker/PokerFeltCanvas.tsx b/src/components/frontend/minigames/poker/PokerFeltCanvas.tsx index d56c1cc..ca052f9 100644 --- a/src/components/frontend/minigames/poker/PokerFeltCanvas.tsx +++ b/src/components/frontend/minigames/poker/PokerFeltCanvas.tsx @@ -1,7 +1,8 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { cn } from "@/lib/utils"; +import { chipFlightChips, type FlightChip } from "../blackjack/BlackjackChipStack"; import type { Card, PublicPlayerView } from "@/lib/minigames/poker"; import type { PokerSnapshot } from "@/app/(frontend)/minigames/poker/actions"; import { Badge } from "@/components/ui/badge"; @@ -122,6 +123,33 @@ export function PokerFeltCanvas({ return () => query.removeEventListener("change", update); }, []); + // Chip flight: on the settle transition the viewer's net chips fly between + // the pot plaque (felt center) and the viewer card spot: winnings drop + // from the pot, lost stakes climb to it. The hand number guards against + // refiring while the snapshot settles through showdown and hand-complete. + const previousSettleHandRef = useRef(null); + const [chipFlights, setChipFlights] = useState< + { key: number; direction: "win" | "loss"; chips: FlightChip[] }[] + >([]); + useEffect(() => { + const settling = view.phase === "showdown" || view.phase === "hand-complete"; + if (!settling || !result || result.netChange === 0) { + previousSettleHandRef.current = null; + return; + } + if (previousSettleHandRef.current === view.handNumber) return; + previousSettleHandRef.current = view.handNumber; + setChipFlights([ + { + key: Date.now(), + direction: result.netChange > 0 ? "win" : "loss", + chips: chipFlightChips(Math.abs(result.netChange)), + }, + ]); + const timer = setTimeout(() => setChipFlights([]), 2400); + return () => clearTimeout(timer); + }, [view.phase, view.handNumber, result]); + return (
)} + {chipFlights.map((flight) => ( + + ))} + {viewerPopups.length > 0 && (