// 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;
};
}