diff --git a/src/renderer/game/engine/narrative/saga.ts b/src/renderer/game/engine/narrative/saga.ts new file mode 100644 index 0000000..b0f8f6e --- /dev/null +++ b/src/renderer/game/engine/narrative/saga.ts @@ -0,0 +1,105 @@ +/** + * 0.1.40 天下大事记叙事层 + * + * 聚合 WorldSim 的 sagaAnnals(世鉴 50/100 年)+ worldAnnals(十年一鉴)+ + * newsFeed 中的里程碑事件(宗祧更替/势力分裂/新贵崛起/灾年/世道更替), + * 输出一个按年份排序的"天下大事记"序列,供 WorldPanel 展示区和 LegacyPanel 百年报告引用。 + * + * 设计原则: + * - 纯函数(不修改 state) + * - 去重(同一年同类事件只保留最新一条) + * - 裁剪(上限 60 条——百年内的重要事件) + */ + +import type { WorldSimState } from '../sim/worldsim-data' + +/** 大事记条目 */ +export interface SagaEntry { + year: number + /** 事件类型标签 */ + kind: 'era' | 'calamity' | 'succession' | 'split' | 'newborn' | 'annal' | 'war' | 'wonder' + /** 叙事文本 */ + text: string +} + +/** 从 WorldSimState 中提取天下大事记 + * + * 聚合来源: + * 1. sagaAnnals — 50/100 年世鉴(宏观视角) + * 2. worldAnnals — 十年一鉴(史官综述) + * 3. newsFeed 中 kind=annal/npc/calamity 的里程碑事件 + */ +export function buildSaga(ws: WorldSimState | undefined): SagaEntry[] { + if (!ws) return [] + const entries: SagaEntry[] = [] + + // 1. 世鉴 + for (const s of ws.sagaAnnals ?? []) { + entries.push({ year: s.year, kind: 'annal', text: s.text }) + } + + // 2. 十年一鉴 + for (const a of ws.worldAnnals ?? []) { + entries.push({ year: a.year, kind: 'annal', text: a.text }) + } + + // 3. newsFeed 里程碑事件(只取 annal/npc/calamity 类,去重) + const seen = new Set() + for (const n of ws.newsFeed ?? []) { + if (n.kind === 'annal' || n.kind === 'calamity') { + const key = `${n.year}:${n.text.slice(0, 20)}` + if (seen.has(key)) continue + seen.add(key) + let kind: SagaEntry['kind'] = 'annal' + if (n.kind === 'calamity') kind = 'calamity' + else if (n.text.includes('宗主') || n.text.includes('更替') || n.text.includes('退隐') || n.text.includes('遇害') || n.text.includes('篡权')) kind = 'succession' + else if (n.text.includes('分裂')) kind = 'split' + else if (n.text.includes('新贵') || n.text.includes('崛起') || n.text.includes('灰烬重生')) kind = 'newborn' + else if (n.text.includes('世道更替')) kind = 'era' + else if (n.text.includes('击破') || n.text.includes('起衅')) kind = 'war' + else if (n.text.includes('异变') || n.text.includes('出世') || n.text.includes('祥瑞')) kind = 'wonder' + entries.push({ year: n.year, kind, text: n.text }) + } + } + + // 按年份排序,同年按 kind 分组 + entries.sort((a, b) => a.year - b.year) + + // 去重:同年同 kind 只保留最新一条 + const dedupMap = new Map() + for (const e of entries) { + const key = `${e.year}:${e.kind}` + dedupMap.set(key, e) // 后来的覆盖前面的(但已排序,同年同 kind 只有一条) + } + + const result = [...dedupMap.values()].sort((a, b) => b.year - a.year) + + // 裁剪:上限 60 条 + if (result.length > 60) result.length = 60 + + return result +} + +/** 大事记条目类型中文名 */ +export function sagaKindName(kind: SagaEntry['kind']): string { + return kind === 'era' ? '世道' + : kind === 'calamity' ? '灾年' + : kind === 'succession' ? '宗祧' + : kind === 'split' ? '分裂' + : kind === 'newborn' ? '新贵' + : kind === 'war' ? '战事' + : kind === 'wonder' ? '奇观' + : '史鉴' +} + +/** 大事记条目 CSS 类名(供 UI 着色) */ +export function sagaKindClass(kind: SagaEntry['kind']): string { + return kind === 'era' ? 'saga-era' + : kind === 'calamity' ? 'saga-calamity' + : kind === 'succession' ? 'saga-succession' + : kind === 'split' ? 'saga-split' + : kind === 'newborn' ? 'saga-newborn' + : kind === 'war' ? 'saga-war' + : kind === 'wonder' ? 'saga-wonder' + : 'saga-annal' +} diff --git a/src/renderer/ui/panels/LegacyPanel.tsx b/src/renderer/ui/panels/LegacyPanel.tsx index 858549a..a0d0b0a 100644 --- a/src/renderer/ui/panels/LegacyPanel.tsx +++ b/src/renderer/ui/panels/LegacyPanel.tsx @@ -5,6 +5,8 @@ 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 type { WorldSimState } from '../../game/engine/sim/worldsim-data' export default function LegacyPanel() { @@ -155,6 +157,25 @@ function LegacyReportView() {
死因榜:{Object.entries(r.deathCauses).filter(([k]) => k !== '').sort((a, b) => b[1] - a[1]).slice(0, 4).map(([k, v]) => `${k}×${v}`).join(' · ') || '尚无记录'}
突破之劲:{r.breakthroughs.slice(-6).map((b) => `${b.year}年×${b.count}`).join(' · ')}
{verdictLine(r)}
+ {/* 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 ( +
+
天下大事记摘要:
+ {saga.map((e, i) => ( +
+ {e.year}年 + {sagaKindName(e.kind)} + {e.text} +
+ ))} +
+ ) + })()} )} diff --git a/src/renderer/ui/panels/WorldPanel.tsx b/src/renderer/ui/panels/WorldPanel.tsx index e441a20..35a40d3 100644 --- a/src/renderer/ui/panels/WorldPanel.tsx +++ b/src/renderer/ui/panels/WorldPanel.tsx @@ -8,6 +8,7 @@ import { combatPowerOf } from '../../game/engine/runtime/Systems/combat' import { sellItem, buyItem, marketPrice } from '../../game/engine/sim/Market' import { useState } from 'react' import type { World } from '../../game/engine/runtime/World' +import { buildSaga, sagaKindName, sagaKindClass } from '../../game/engine/narrative/saga' const RES_LABEL: Record = { lingcao: '灵草', lingkuang: '灵矿', beastcore: '兽核' } const REALM_IDX = ['mortal', 'qi', 'foundation', 'core', 'nascent', 'spirit'] as const @@ -239,6 +240,23 @@ export default function WorldPanel() { + {/* 0.1.40 天下大事记展示区 */} +
+

天下大事记

+
+ {(() => { + const saga = buildSaga(ws) + if (saga.length === 0) return
天下初定,尚无大事可录。
+ return saga.slice(0, 30).map((e, i) => ( +
+ {e.year}年 + {sagaKindName(e.kind)} + {e.text} +
+ )) + })()} +
+
) } diff --git a/src/renderer/ui/styles.css b/src/renderer/ui/styles.css index 0bdc03b..2f2e656 100644 --- a/src/renderer/ui/styles.css +++ b/src/renderer/ui/styles.css @@ -1978,3 +1978,13 @@ html[data-blade] .battle-lines { background: linear-gradient(180deg, #2a1e3d, #1a1228); border-color: #6b4c9a; } + +/* ---------- 0.1.40 天下大事记标签色系 ---------- */ +.saga-era { color: #c9a227; border-color: #c9a227; } +.saga-calamity { color: #c05a41; border-color: #a33; } +.saga-succession { color: #6b8a9a; border-color: #4a6a7a; } +.saga-split { color: #8a5a8a; border-color: #6a4a6a; } +.saga-newborn { color: #5a8a44; border-color: #4a6a34; } +.saga-war { color: #c08a4a; border-color: #a06a3a; } +.saga-wonder { color: #7a5aa8; border-color: #6b4c9a; } +.saga-annal { color: #8a7a5a; border-color: #6a5a3a; }