Files
ChronicleOfTheImmortalClan/src/renderer/ui/panels/LegacyPanel.tsx
T
thzxx c5fa2a3357 v0.1.15: 深度审计加固(7 P0 + 20+ P1 歼灭)
【引擎层 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 通过
2026-08-23 13:52:51 +08:00

247 lines
10 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'
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)
}