Files
ChronicleOfTheImmortalClan/src/renderer/ui/panels/LegacyPanel.tsx
T

315 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react'
import { useGameStore } from '../store'
import { computeLegacy, resolveLegacy } from '../../game/engine/narrative/legacy'
import { ResolveModal } from '../components/ResolveModal'
import { buildBiography } from '../../game/engine/narrative/biography'
import { yearAxis, decadeLabel } from '../../game/engine/narrative/yearaxis'
import { buildReport, verdictLine } from '../../game/engine/narrative/legacyreport'
import { buildSaga, sagaKindName, sagaKindClass } from '../../game/engine/narrative/saga'
import { buildChronicleScroll, scrollKindName, scrollKindClass } from '../../game/engine/narrative/chronicle-scroll'
import type { WorldSimState } from '../../game/engine/sim/worldsim-data'
export default function LegacyPanel() {
const world = useGameStore((s) => s.world)
const revision = useGameStore((s) => s.revision)
void revision
const [showResolve, setShowResolve] = useState(false)
const [view, setView] = useState<'dims' | 'chronicle' | 'report' | 'scroll' | 'axis' | 'legacy' | 'cscroll'>('dims')
if (!world) return null
const w = world
const s = w.state
const dims = computeLegacy(s)
const arch = resolveLegacy(s)
const resolved = !!s.family.flag['resolved']
const yearly = s.yearlyReports.slice().sort((a, b) => a.year - b.year)
const births = Object.values(s.members).filter((c) => c.bornYear >= 0)
const genMin = Math.min(...Object.values(s.members).map((c) => c.bornYear))
const genMax = Math.max(...Object.values(s.members).map((c) => c.deathYear ?? s.year))
return (
<div>
<div className="help-text">
春秋原:家族百年气数之图。望气可预演四维,落印后方入青册(不阻继续行旅)。
</div>
<div className="card" style={{ display: 'flex', alignItems: 'center', gap: 18, marginBottom: 12 }}>
<div className="legacy-peak" style={{ width: 86, height: 110 }}>
<div className="legacy-glyph"></div>
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '1.04rem', marginBottom: 4 }}>
当前气数 · 预演称号 <b className="gold" style={{ fontSize: '1.2rem' }}>{arch.title}</b>
{resolved && <span className="tag head-t" style={{ marginLeft: 8 }}>已定局</span>}
</div>
<div className="dim2" style={{ fontSize: '0.85rem' }}>
存续{s.year} · {s.family.generation} · 声望峰值{s.stats.repPeak} · 丁口峰值{s.stats.popPeak}
{s.stats.tourneyBest ? ` · 大比最佳第${s.stats.tourneyBest}名` : ''}
</div>
</div>
<div style={{ display: 'flex', gap: 8 }}>
{!resolved && (
<button className="btn btn-primary" onClick={() => setShowResolve(true)}>
望气定鼎
</button>
)}
<button className="btn" onClick={() => setView('dims')}>四维</button>
<button className="btn" onClick={() => setView('report')}>年报</button>
<button className="btn" onClick={() => setView('chronicle')}>年表</button>
<button className="btn" onClick={() => setView('scroll')}>长卷</button>
<button className="btn" onClick={() => setView('cscroll')}>族史卷</button>
<button className="btn" onClick={() => setView('axis')}>年轴</button>
<button className="btn" onClick={() => setView('legacy')}>百年报告</button>
</div>
</div>
{view === 'dims' && (
<div className="card">
<div className="card-title">气数四维</div>
{Object.entries(dims)
.filter(([k]) => k !== 'total')
.map(([k, v]) => (
<div key={k} className="resolve-dim" style={{ maxWidth: 620 }}>
<span className="dim">{dimName(k)}</span>
<div className="bar" style={{ flex: 1 }}>
<div style={{ width: `${Math.min(100, (v / 160) * 100)}%` }} />
</div>
<b>{v}</b>
</div>
))}
<div className="resolve-dim" style={{ maxWidth: 620 }}>
<span className="dim">总评</span>
<div className="bar" style={{ flex: 1 }}>
<div style={{ width: `${Math.min(100, (dims.total / 400) * 100)}%`, background: 'linear-gradient(90deg,#8e2f24,#c05a41,#e0c15e)' }} />
</div>
<b className="gold">{dims.total}</b>
</div>
<div className="dim2" style={{ marginTop: 8 }}>
人兴=代数·丁口峰值 · 道兴=最高境界·功法大成 · 威名=声望峰·大比最佳 · 香火=存续·飞升 · 因果=功德善恶
</div>
</div>
)}
{view === 'report' && (
<div className="card">
<div className="card-title">岁末族簿汇编(逐年)</div>
<table className="market-table" style={{ width: '100%' }}>
<thead>
<tr>
<th></th><th>灵石盈亏</th><th>诞辰</th><th>辞世</th><th>声望</th><th>宗族战力</th>
</tr>
</thead>
<tbody>
{yearly.slice(-20).map((r) => (
<tr key={r.year}>
<td>{r.year}</td>
<td className={r.nets >= 0 ? 'good' : 'bad'}>{r.nets >= 0 ? `+${r.nets}` : r.nets}</td>
<td>+{r.births}</td>
<td className="bad">-{r.deaths}</td>
<td>{r.rep}</td>
<td>{r.power}</td>
</tr>
))}
</tbody>
</table>
<div className="dim2" style={{ marginTop: 6 }}> {yearly.length} 年度账册。</div>
</div>
)}
{view === 'legacy' && <LegacyReportView />}
{view === 'axis' && <AxisView />}
{view === 'scroll' && <ScrollScroll />}
{view === 'cscroll' && <ChronicleScrollView />}
{view === 'chronicle' && (
<div className="card">
<div className="card-title">生卒与突破年表</div>
<div className="dim" style={{ marginBottom: 6 }}>
族人生卒带(首倡 {genMin + s.year - s.year} ~ {genMax} 年,区间 {genMax - genMin} 载)
</div>
<div className="dim2" style={{ marginBottom: 10 }}>
大比记录:{s.stats.tourneyHistory.length === 0 ? '尚无参赛记录' : s.stats.tourneyHistory.map((t) => `${t.year}年第${t.rank}名`).join('、')}
</div>
<div className="dim2">
突破年表(按年份计数):
{breakdown(s).length === 0 ? '暂无' : breakdown(s).map((b) => `${b.year}年×${b.n}`).join(' · ')}
</div>
</div>
)}
{showResolve && <ResolveModal arch={arch} onClose={() => setShowResolve(false)} />}
</div>
)
}
function LegacyReportView() {
const world = useGameStore((s) => s.world)
if (!world) return null
const r = buildReport(world.state)
const span = r.years.length
return (
<div className="card">
<div className="card-title">百年族史报告</div>
{span < 4 ? (
<div className="dim">开卷尚浅,待岁月沉淀后再启此卷。</div>
) : (
<>
<Sparklines r={r} />
<div className="dim" style={{ marginTop: 10 }}>死因榜:{Object.entries(r.deathCauses).filter(([k]) => k !== '').sort((a, b) => b[1] - a[1]).slice(0, 4).map(([k, v]) => `${k}×${v}`).join(' · ') || '尚无记录'}</div>
<div className="dim" style={{ marginTop: 6 }}>突破之劲:{r.breakthroughs.slice(-6).map((b) => `${b.year}年×${b.count}`).join(' · ')}</div>
<div className="resolve-poem" style={{ marginTop: 14 }}>{verdictLine(r)}</div>
{/* 0.1.40 天下大事记摘要 */}
{(() => {
const ws = world.state.worldSim as WorldSimState | undefined
if (!ws) return null
const saga = buildSaga(ws).slice(0, 12)
if (saga.length === 0) return null
return (
<div style={{ marginTop: 14 }}>
<div className="dim" style={{ marginBottom: 4 }}>天下大事记摘要:</div>
{saga.map((e, i) => (
<div key={i} className="ch-item" style={{ padding: '4px 8px', fontSize: '0.84rem' }}>
<span className="ch-month">{e.year}</span>
<span className={`tag ${sagaKindClass(e.kind)}`} style={{ marginRight: 6, fontSize: '0.72rem' }}>{sagaKindName(e.kind)}</span>
<span className="dim">{e.text}</span>
</div>
))}
</div>
)
})()}
</>
)}
</div>
)
}
function Sparklines({ r }: { r: ReturnType<typeof buildReport> }) {
const width = 640
const height = 150
const pts = (seq: number[], color: string) => {
const max = Math.max(...seq, 1)
const step = seq.length > 1 ? width / (seq.length - 1) : width
return seq.map((v, i) => `${(i * step).toFixed(1)},${(height - 20 - (v / max) * (height - 40)).toFixed(1)}`).join(' ')
}
return (
<svg width="100%" viewBox={`0 0 ${width} ${height}`} style={{ maxHeight: 160, background: 'rgba(20,15,9,0.5)', borderRadius: 4 }}>
<polyline fill="none" stroke="#8fae6a" strokeWidth="2" points={pts(r.population, 'green')} />
<polyline fill="none" stroke="#c9a227" strokeWidth="2" points={pts(r.reputation, 'gold')} />
<polyline fill="none" stroke="#a9a9a9" strokeWidth="2" points={pts(r.power, 'gray')} />
<text x="4" y="14" fill="#8fae6a" fontSize="10">人口</text>
<text x="64" y="14" fill="#c9a227" fontSize="10">声望</text>
<text x="124" y="14" fill="#aaa" fontSize="10">战力</text>
</svg>
)
}
function AxisView() {
const world = useGameStore((s) => s.world)
if (!world) return null
const cells = yearAxis(world.state)
return (
<div className="card">
<div className="card-title">横轴年表(十年一格)</div>
{cells.map((c) => (
<div key={c.from} className="axis-cell">
<div className="axis-decade">{decadeLabel(c.from, c.to)}</div>
<div className="axis-events">
<span className="tag good-t-bg"> {c.births}</span>
<span className="tag bad-t-bg"> {c.deaths}</span>
{c.events.slice(0, 6).map((e, i) => (
<span key={i} className={`tag ${e.kind === '战' || e.kind === '渡劫' ? 'bad-t-bg' : ''}`}>
{e.kind}·{e.year} {e.label}
</span>
))}
{c.events.length > 6 && <span className="dim2">…还有{c.events.length - 6}</span>}
</div>
</div>
))}
</div>
)
}
function ScrollScroll() {
const world = useGameStore((s) => s.world)
if (!world) return null
const mem = Object.values(world.state.members).sort((a, b) => b.generation - a.generation || b.bornYear - a.bornYear)
return (
<div className="card">
<div className="card-title">列传长卷</div>
{mem.slice(0, 40).map((c) => {
const bio = buildBiography(world.state, c.id)
const isAscended = c.deathCause === '飞升太虚' || c.deathCause === '云游飞升'
const isAscensionFall = c.deathCause === '飞升劫陨'
const isFengxian = c.traits.includes('fengxian') && c.alive
return (
<div key={c.id} className="bio-row" style={
isAscended ? { borderLeft: '3px solid #c9a227', paddingLeft: 8 }
: isAscensionFall ? { borderLeft: '3px solid #c05a41', paddingLeft: 8 }
: isFengxian ? { borderLeft: '3px solid #8fae6a', paddingLeft: 8 }
: undefined
}>
<b>{c.name}</b>
{isAscended && <span className="tag head-t" style={{ marginLeft: 6 }}>飞升</span>}
{isAscensionFall && <span className="tag bad-t-bg" style={{ marginLeft: 6 }}>劫陨</span>}
{isFengxian && <span className="tag good-t-bg" style={{ marginLeft: 6 }}>留仙</span>}
<span className="dim" style={{ marginLeft: 8 }}>
{bio.map((l) => l.text).join(' ').slice(0, 90)}{bio.map((l) => l.text).join(' ').length > 90 ? '……' : ''}
</span>
</div>
)
})}
</div>
)
}
function ChronicleScrollView() {
const world = useGameStore((s) => s.world)
if (!world) return null
const ws = world.state.worldSim as WorldSimState | undefined
const scroll = buildChronicleScroll(world.state, ws)
return (
<div className="card">
<div className="card-title">族史卷(天命·族事·天下 串联)</div>
{scroll.length === 0 ? (
<div className="dim">开卷尚浅,待岁月沉淀后再启此卷。</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, maxHeight: 500, overflowY: 'auto' }}>
{scroll.map((e, i) => (
<div key={i} className="ch-item" style={{
padding: '5px 8px',
borderLeft: e.important ? '3px solid #c9a227' : undefined,
paddingLeft: e.important ? 10 : 8
}}>
<span className="ch-month">{e.year}</span>
<span className={`tag ${scrollKindClass(e.kind)}`} style={{ marginRight: 6, fontSize: '0.72rem' }}>{scrollKindName(e.kind)}</span>
<span className={e.important ? 'gold-t' : ''}>{e.text}</span>
</div>
))}
</div>
)}
<div className="dim2" style={{ marginTop: 8 }}>
族史卷将天命决策、族中大事与天下大事记串联展示——以时间为经、以事件为纬。
</div>
</div>
)
}
function dimName(k: string): string {
const map: Record<string, string> = { renXing: '人兴', daoXing: '道兴', weiMing: '威名', xiangHuo: '香火', yinGuo: '因果' }
return map[k] ?? k
}
function breakdown(s: { chronicle: { category: string; year: number }[] }): { year: number; n: number }[] {
const counts = new Map<number, number>()
for (const e of s.chronicle) {
if (e.category === 'breakthrough') {
const cur = counts.get(e.year) ?? 0
counts.set(e.year, cur + 1)
}
}
return [...counts.entries()].map(([year, n]) => ({ year, n })).sort((a, b) => a.year - b.year)
}