0.1.40 P2-1~P2-3: 天下大事记叙事层 + WorldPanel展示区 + LegacyPanel引用 (saga.ts + buildSaga + 8类标签色系)
This commit is contained in:
@@ -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<string>()
|
||||
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<string, SagaEntry>()
|
||||
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'
|
||||
}
|
||||
@@ -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() {
|
||||
<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>
|
||||
|
||||
@@ -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<string, string> = { lingcao: '灵草', lingkuang: '灵矿', beastcore: '兽核' }
|
||||
const REALM_IDX = ['mortal', 'qi', 'foundation', 'core', 'nascent', 'spirit'] as const
|
||||
@@ -239,6 +240,23 @@ export default function WorldPanel() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 0.1.40 天下大事记展示区 */}
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<h4 className="dim" style={{ margin: '4px 0 6px' }}>天下大事记</h4>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, maxHeight: 300, overflowY: 'auto' }}>
|
||||
{(() => {
|
||||
const saga = buildSaga(ws)
|
||||
if (saga.length === 0) return <div className="dim2">天下初定,尚无大事可录。</div>
|
||||
return saga.slice(0, 30).map((e, i) => (
|
||||
<div key={i} className="ch-item" style={{ padding: '5px 8px' }}>
|
||||
<span className="ch-month">{e.year}年</span>
|
||||
<span className={`tag ${sagaKindClass(e.kind)}`} style={{ marginRight: 6 }}>{sagaKindName(e.kind)}</span>
|
||||
<span>{e.text}</span>
|
||||
</div>
|
||||
))
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user