【引擎层 P0】 - GameEngine.restore 双重时钟注册(回档后整月双跑)→ restore 重建内核+回置 rng 状态 - engineFromSnapshot 必崩(globalThis.__state 从未设)→ 修正带 seed 重建 - DEV 性能分支二次执行整月管线 → 只读告警(绝不重跑) - 引擎 advance 遇待决事件死锁 → resolvePending(idx) 通道(自动化/测试推进) 【WorldSim 大修】 - NPC 换代文案垃圾(n-danxin行情降50%)→ 换代快讯专属文案 - secretQi 假闭环(纯写无读)→ 全秘境初始化+missions 统一 consumeSecretQi - 快讯节流自锁(lastNewsMonth 恒 1 月)→ totalTicks%24 - power 月度收敛坍塌(1400±3)→ 换代/年首化+年度成长 - relation 双重拉零(4 家全 0)→ 交还外交年度漂移 - 市场基准 5 处重复 → POOL_BASE 单一权威;灾年 stale 清理;drift 各池独立噪声 - worldsim 能力卡补全 + normalize 兜底(worldSim/stats 子字段) 【UI/动效/存储】 - ModalShell 三条 CSS 规则真正落地(close 定位/body 滚动/footer 独立底排)——0.1.12 声明未实施项 - fx-canvas:resize 监听移除/mistTimer 清零/reduced 联动/soft 档无雾 + drift 走 gate(过滤器) - 档位 density 脱钩修复;顶栏“止”钮补 onClick;版本号三处统一 0.1.15 - importAll 清 chronicle 表;自动推进季度落盘(手动逐次) - 性能:LogFeed memo / Genealogy computeGenealogy memo / Chronicle deps 修正;死码清除(lastPhaseStats/clockFromKernel/哨兵函数等 12 处) - 删除误入库的“p”游离文件 【测试】979 全绿(36 套件);金钟罩三档 0.1.15 基线固化; typecheck/build 通过
247 lines
10 KiB
TypeScript
247 lines
10 KiB
TypeScript
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'
|
||
|
||
|
||
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'>('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('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 === '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>
|
||
</>
|
||
)}
|
||
</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)
|
||
return (
|
||
<div key={c.id} className="bio-row">
|
||
<b>{c.name}</b>
|
||
<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 dimName(k: string): string {
|
||
const map: Record<string, string> = { renXing: '人兴', daoXing: '道兴', weiMing: '威名', xiangHuo: '香火' }
|
||
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)
|
||
}
|