Play liveChoose your opponent
Join
32fe633
118 lines · 5.2 KB Raw Permalink History
1// The replay's controls in the top bar (step, play, speed, whose view), the live seat switch, and the end of a game as
2// 无名杀's result dialog.
3import type { GameView } from '../../../src/engine/types';
4import type { UseReplayResult, ReplaySpeed } from '../../../src/client/hooks/useReplay';
5import { ID_SHORT, ID_KEY } from './Seat';
6import { portrait } from './art';
7
8export function ReplayBar({ r }: { r: UseReplayResult }) {
9 return (
10 <div className="nm-replay">
11 <button className="nm-mini" onClick={() => r.goTo(0)} title="回到开局" disabled={r.step <= 0}>
12 ⏮
13 </button>
14 <button className="nm-mini" onClick={r.prev} title="上一步" disabled={r.step <= 0}>
15 ◀
16 </button>
17 <button className="nm-mini play" onClick={r.togglePlay} title={r.playing ? '暂停' : '播放'}>
18 {r.playing ? '❚❚' : '▶'}
19 </button>
20 <button className="nm-mini" onClick={r.next} title="下一步" disabled={r.step >= r.total}>
21 ▶▏
22 </button>
23 <input type="range" min={0} max={r.total} value={r.step} onChange={(e) => r.goTo(Number(e.target.value))} aria-label="进度" />
24 <span className="nm-step">
25 {r.step}/{r.total}
26 </span>
27 </div>
28 );
29}
30
31/** The replay's speed and whose view it shows (a seat's, or the spectator's). */
32export function ReplayView({ r, names }: { r: UseReplayResult; names: string[] }) {
33 return (
34 <div className="nm-replay">
35 <select value={r.speed} onChange={(e) => r.setSpeed(Number(e.target.value) as ReplaySpeed)} title="速度">
36 {[0.5, 1, 2, 4].map((x) => (
37 <option key={x} value={x}>
38 {x}×
39 </option>
40 ))}
41 </select>
42 <select value={r.seat} onChange={(e) => r.setSeat(Number(e.target.value))} title="视角">
43 {r.seats.map((s) => (
44 <option key={s} value={s}>
45 {s < 0 ? '旁观' : `${s + 1}号位 ${names[s] ?? ''}`}
46 </option>
47 ))}
48 </select>
49 </div>
50 );
51}
52
53/** The live seat switch, when this viewer controls more than one seat. */
54export function SeatSwitch({ seats, seat, view, onPick }: { seats: string[]; seat?: string; view: GameView; onPick: (s: string) => void }) {
55 if (seats.length < 2) return null;
56 return (
57 <div className="nm-replay">
58 {seats.map((x) => (
59 <button key={x} className={`nm-mini${x === seat ? ' on' : ''}`} onClick={() => onPick(x)}>
60 {view.players[Number(x)]?.character || `${Number(x) + 1}号位`}
61 </button>
62 ))}
63 </div>
64 );
65}
66
67const CAMP: Record<string, string> = { 主公: '主公', 忠臣: '主公', 反贼: '反贼', 内奸: '内奸' };
68const COUNT: Record<number, [number, number]> = { 2: [0, 1], 3: [0, 1], 4: [1, 1], 5: [1, 2], 6: [1, 3], 7: [2, 3], 8: [2, 4] };
69
70/** Who won, from what the view shows at the end: the lord alive means the lord's side; the lord dead, the rebels, unless
71 * the one left alive is the renegade (every loyalist and rebel is dead and shown). */
72export function inferWinner(view: GameView): string {
73 const lord = view.players.find((p) => p.identity === '主公');
74 if (!lord) return '';
75 if (lord.alive) return '主公';
76 const alive = view.players.filter((p) => p.alive);
77 const [zhong, fan] = COUNT[view.players.length] ?? [1, 2];
78 const dead = (id: string) => view.players.filter((p) => !p.alive && p.identity === id).length;
79 return alive.length === 1 && dead('忠臣') === zhong && dead('反贼') === fan ? '内奸' : '反贼';
80}
81
82export function Result({ view, camp, identities, me, labels }: { view: GameView; camp: string; identities?: Record<string, string>; me: number; labels?: string[] }) {
83 const idOf = (i: number) => identities?.[String(i)] || view.players[i]?.identity || '';
84 const mine = me >= 0 ? CAMP[idOf(me)] : '';
85 const title = !camp ? '游戏结束' : mine ? (mine === camp ? '游戏胜利' : '游戏失败') : `${camp === '主公' ? '主公、忠臣' : camp}获胜`;
86 return (
87 <div className="nm-dialog-mask result">
88 <div className={`nm-dialog result ${mine && camp ? (mine === camp ? 'win' : 'lose') : ''}`}>
89 <div className="nm-result-title">{title}</div>
90 <table className="nm-result">
91 <tbody>
92 {view.players.map((p, i) => {
93 const id = idOf(i);
94 const img = portrait(p.character);
95 return (
96 <tr key={i} className={`${CAMP[id] === camp ? 'won' : ''}${p.alive ? '' : ' dead'}`}>
97 <td>
98 <span className="nm-result-face" style={img ? { backgroundImage: `url(${img})` } : undefined} />
99 </td>
100 <td className="nm-result-name">{p.character}</td>
101 <td>{labels?.[i] ?? (p.name !== p.character ? p.name : '')}</td>
102 <td>
103 <span className={`nm-identity inline ${id ? ID_KEY[id] : 'unknown'}`}>
104 {id ? ID_SHORT[id] : ''}
105 </span>
106 {id || '未知'}
107 </td>
108 <td>{p.alive ? '存活' : '阵亡'}</td>
109 <td>{CAMP[id] === camp ? '胜' : id ? '负' : ''}</td>
110 </tr>
111 );
112 })}
113 </tbody>
114 </table>
115 </div>
116 </div>
117 );
118}