| 1 | |
| 2 | |
| 3 | import { useEffect, useMemo, useRef, useState } from 'react'; |
| 4 | import type { GameView } from '../../../src/engine/types'; |
| 5 | import art from './art.json'; |
| 6 | |
| 7 | const CARDS = [...Object.keys(art.cards), '火杀', '雷杀']; |
| 8 | const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
| 9 | |
| 10 | export function Log({ view }: { view: GameView }) { |
| 11 | const [open, setOpen] = useState(false); |
| 12 | const ref = useRef<HTMLDivElement>(null); |
| 13 | const lines = view.log.slice(open ? -80 : -4); |
| 14 | const generals = view.players.map((p) => p.character).filter(Boolean).join('|'); |
| 15 | const re = useMemo(() => { |
| 16 | const words = [...generals.split('|').filter(Boolean), ...CARDS].sort((a, b) => b.length - a.length); |
| 17 | return new RegExp(`(${words.map(esc).join('|')})`, 'g'); |
| 18 | }, [generals]); |
| 19 | const cards = useMemo(() => new Set(CARDS), []); |
| 20 | useEffect(() => { |
| 21 | if (ref.current) ref.current.scrollTop = ref.current.scrollHeight; |
| 22 | }, [lines.length, open]); |
| 23 | return ( |
| 24 | <div className={`nm-log${open ? ' open' : ''}`}> |
| 25 | <button className="nm-log-toggle" onClick={() => setOpen(!open)} title={open ? '收起记录' : '展开记录'}> |
| 26 | 记录 |
| 27 | </button> |
| 28 | {lines.length > 0 && ( |
| 29 | <div className="nm-log-lines" ref={ref}> |
| 30 | {lines.map((e, i) => { |
| 31 | const p = view.players[e.player]; |
| 32 | return ( |
| 33 | <div key={view.log.length - lines.length + i} className="nm-log-line"> |
| 34 | {p && <span className="who">{p.character || p.name}</span>} |
| 35 | {e.text.split(re).map((t, k) => (k % 2 ? <span key={k} className={cards.has(t) ? 'card' : 'who'}>{t}</span> : t))} |
| 36 | </div> |
| 37 | ); |
| 38 | })} |
| 39 | </div> |
| 40 | )} |
| 41 | </div> |
| 42 | ); |
| 43 | } |