| 1 | |
| 2 | |
| 3 | import type { GameView } from '../../../src/engine/types'; |
| 4 | import type { UseReplayResult, ReplaySpeed } from '../../../src/client/hooks/useReplay'; |
| 5 | import { ID_SHORT, ID_KEY } from './Seat'; |
| 6 | import { portrait } from './art'; |
| 7 | |
| 8 | export 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 | |
| 32 | export 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 | |
| 54 | export 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 | |
| 67 | const CAMP: Record<string, string> = { 主公: '主公', 忠臣: '主公', 反贼: '反贼', 内奸: '内奸' }; |
| 68 | const 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 | |
| 71 | |
| 72 | export 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 | |
| 82 | export 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 | } |