// From DeskPark apps/web/src/games/shared/trick.tsx (MIT, see viewer/NOTICE), without the turn cue's sound. import { useEffect, useRef, useState } from 'react'; import { CardRow } from './Hand'; import type { Card } from './PlayingCard'; export function CountChip({ n, warnAt = 2 }: { n: number; warnAt?: number }) { return ( 0 && n <= warnAt ? 'is-warn' : ''}`}> {n} ); } export type TrickMove = { type: 'play'; cards: Card[] } | { type: 'pass' } | null; export function MoveZone({ move, from, passText = '不出', w, }: { move: TrickMove; from: 'left' | 'right' | 'top' | 'bottom'; passText?: string; w?: string; }) { if (!move) return null; if (move.type === 'pass') return {passText}; return c.id).join(',')} cards={move.cards} from={from} w={w} />; } /** 手牌选择状态:手牌变化时自动剔除已不存在的牌,resetKey 变化时清空。 */ export function useSelection(hand: Card[], resetKey: unknown) { const [sel, setSel] = useState>(new Set()); const ids = hand.map((c) => c.id).join(','); useEffect(() => { setSel((prev) => { const alive = new Set(hand.map((c) => c.id)); const next = new Set([...prev].filter((id) => alive.has(id))); return next.size === prev.size ? prev : next; }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [ids]); useEffect(() => { setSel(new Set()); }, [resetKey]); return [sel, setSel] as const; } /** 依次循环的提示 */ export function useHintCycle(candidates: T[], resetKey: unknown) { const idx = useRef(0); useEffect(() => { idx.current = 0; }, [resetKey]); return () => { if (candidates.length === 0) return null; const c = candidates[idx.current % candidates.length]; idx.current++; return c; }; }