实时对局选你的对手
加入
What shoal-games changes in this game
由 @shoal shoal 从 main 的 1b3bc26 分出 · 领先 1 个提交,比 main 落后 0 个
接续
贡献回去
127 行 · 6.2 KB 原始文件 永久链接 历史
1// What the table shows of each engine event beyond the state itself, as 无名杀 does: a card played, discarded, judged
2// or shown flies from its seat to the centre and stays there a moment with a caption (谁 → 谁), and a line runs from
3// the user to each target; a basic card's name flashes on its user. Fed by the same event batches as the upstream's play history (usePlayHistory).
4import { useEffect, useRef, useState } from 'react';
5import type { GameView, ViewEvent } from '../../../src/engine/types';
6import type { QueuedEvent } from '../../../src/client/hooks/useEventPlayback';
7import type { Face } from './Card';
8
9export interface CenterCard {
10 id: string;
11 face: Face;
12 caption: string;
13 from: number | null;
14 cardId?: string;
15 at: number;
16}
17/** A basic card's name flashing large on its user, as 无名杀's cardeffect. */
18export interface Word {
19 id: string;
20 seat: number;
21 text: string;
22 at: number;
23}
24export interface Line {
25 id: string;
26 from: number;
27 to: number;
28 kind: 'sha' | 'trick';
29 at: number;
30}
31
32const KEEP_MS = 4200;
33const LINE_MS = 1400;
34const WORD_MS = 1100;
35const WORDS: Record<string, string> = { 杀: 'sha', 火杀: 'sha', 雷杀: 'sha', 闪: 'shan', 桃: 'tao', 酒: 'jiu' };
36const MAX_CENTER = 7;
37let n = 0;
38const id = () => `fx${++n}`;
39
40export function useFx(ingested: readonly QueuedEvent[] | undefined, view: GameView) {
41 const [center, setCenter] = useState<CenterCard[]>([]);
42 const [lines, setLines] = useState<Line[]>([]);
43 const [words, setWords] = useState<Word[]>([]);
44 const last = useRef(-1);
45 const viewRef = useRef(view);
46 viewRef.current = view;
47
48 useEffect(() => {
49 const batch = ingested ?? [];
50 if (!batch.length) return;
51 // A batch from before the last one is a jump back (a replay seek): start the table's effects over.
52 if (batch.every((e) => e.seq <= last.current)) {
53 last.current = Math.min(...batch.map((e) => e.seq)) - 1;
54 setCenter([]);
55 setLines([]);
56 setWords([]);
57 }
58 const fresh = batch.filter((e) => e.seq > last.current);
59 if (!fresh.length) return;
60 last.current = Math.max(...fresh.map((e) => e.seq));
61 const v = viewRef.current;
62 const name = (i: unknown) => v.players[i as number]?.character || v.players[i as number]?.name || '';
63 const now = Date.now();
64 const add: CenterCard[] = [];
65 const newLines: Line[] = [];
66 const newWords: Word[] = [];
67 const captions = new Map<string, string>();
68 for (const { event } of fresh) {
69 const e = event as ViewEvent & Record<string, unknown>;
70 const card = e.card as { name?: string; suit?: string; rank?: string } | null | undefined;
71 const full = typeof e.cardId === 'string' ? v.cardMap[e.cardId] : undefined;
72 const face = (c: { name?: string; suit?: string; rank?: string } | undefined, cid?: string): Face | null => {
73 const f = cid ? v.cardMap[cid] : undefined;
74 const nm = c?.name ?? f?.name;
75 return nm ? { name: nm, suit: c?.suit ?? f?.suit, rank: c?.rank ?? f?.rank, damageType: f?.damageType } : null;
76 };
77 if (e.type === '打出' || e.type === '弃牌' || e.type === '展示') {
78 const f = face(card ?? undefined, e.cardId as string | undefined);
79 const verb = e.type === '打出' ? '' : e.type === '弃牌' ? ' 弃置' : ' 展示';
80 if (f) add.push({ id: id(), face: f, caption: `${name(e.player)}${verb}`, from: e.player as number, cardId: e.cardId as string, at: now });
81 const label = f && (f.damageType === '火焰' ? '火杀' : f.damageType === '雷电' ? '雷杀' : f.name);
82 if (e.type === '打出' && label && WORDS[label]) newWords.push({ id: id(), seat: e.player as number, text: label, at: now });
83 } else if (e.type === '弃置') {
84 const ids = (e.cardIds as string[] | undefined) ?? [];
85 const names = (e.cardNames as string[] | undefined) ?? [];
86 for (let i = 0; i < Math.max(ids.length, names.length); i++) {
87 const f = face(names[i] ? { name: names[i], ...pick(v.cardMap[ids[i]]) } : undefined, ids[i]);
88 if (f) add.push({ id: id(), face: f, caption: `${name(e.player)} 弃置`, from: e.player as number, cardId: ids[i], at: now });
89 }
90 } else if (e.type === '判定') {
91 const f = face(card ?? undefined, e.cardId as string | undefined);
92 if (f) add.push({ id: id(), face: f, caption: `${name(e.player)} ${(e.judgeType as string) ?? ''}判定`, from: null, cardId: e.cardId as string, at: now });
93 } else if (e.type === '指定目标' && typeof e.source === 'number' && typeof e.target === 'number') {
94 const cardName = (e.cardName as string) ?? full?.name;
95 if (e.source !== e.target) newLines.push({ id: id(), from: e.source, to: e.target, kind: cardName === '杀' ? 'sha' : 'trick', at: now });
96 if (typeof e.cardId === 'string') {
97 const prev = captions.get(e.cardId);
98 captions.set(e.cardId, prev ? `${prev}、${name(e.target)}` : `${name(e.source)} → ${name(e.target)}`);
99 }
100 }
101 }
102 if (add.length || captions.size)
103 setCenter((prev) => {
104 let next = [...prev.filter((c) => now - c.at < KEEP_MS), ...add];
105 if (captions.size) next = next.map((c) => (c.cardId && captions.has(c.cardId) ? { ...c, caption: captions.get(c.cardId)! } : c));
106 return next.slice(-MAX_CENTER);
107 });
108 if (newLines.length) setLines((prev) => [...prev.filter((l) => now - l.at < LINE_MS), ...newLines]);
109 if (newWords.length) setWords((prev) => [...prev.filter((w) => now - w.at < WORD_MS), ...newWords]);
110 }, [ingested]);
111
112 // expire
113 useEffect(() => {
114 if (!center.length && !lines.length && !words.length) return;
115 const t = setInterval(() => {
116 const now = Date.now();
117 setCenter((p) => (p.some((c) => now - c.at >= KEEP_MS) ? p.filter((c) => now - c.at < KEEP_MS) : p));
118 setLines((p) => (p.some((l) => now - l.at >= LINE_MS) ? p.filter((l) => now - l.at < LINE_MS) : p));
119 setWords((p) => (p.some((w) => now - w.at >= WORD_MS) ? p.filter((w) => now - w.at < WORD_MS) : p));
120 }, 300);
121 return () => clearInterval(t);
122 }, [center.length, lines.length, words.length]);
123
124 return { center, lines, words };
125}
126
127const pick = (c?: { suit?: string; rank?: string }) => (c ? { suit: c.suit, rank: c.rank } : {});