插件协议:
- CotycPlugin Manifest(dependencies/conflicts/protected/install/uninstall)
- PluginManager:安装序确定性/依赖拒绝/异常回滚/追踪型上下文(卸载全摘钩子)
- 三核心插件常驻(Systems/Data/Events);插件清单/安装移除/启停广播
- EventPoolRegistry 多池合并;facade about/query('plugins')/plugin 订阅
- 设置页插件清单(含卸载按钮、核心保护);examplePlugin 开发范本入仓
全应用审计修复(引擎 39 + UI/主进程 40 双路清单,P0→P2 歼灭):
- P0:单亲 rollRoots 崩/插件事件软锁(findEvent 查池)/fire 覆盖幸存事件/
facade 未赋值(act 目录 6/24 生效→全量接通)/packOverride 死实现真接管/
4 能力卡无接线(trib/apprentice/tournament/season)/读档 rowid 方言崩溃/
buildApprentice 空候选崩/插件安装无回滚/trait 加成从未消费/DEV 双跑破坏时序
- P1:定时器幸存/advance in-flight 闸/battle 暂停/tournament 点将真传/
史书 memo 依赖/面板 hooks 顺序违规/远征 squad 残留 id/跨档状态残留/
toast 自清除/读档错误提示/导入后重载世界/pendingEvent 读档回填
- P2:版本三处统一/NewGame 随机收编/学费错误文案/姓氏池去重/
功法品阶错位(凡阶哨兵)/功法定价对齐/market 白名单校验/
战利功法去重/回声漏封缄/clock 迭代快照/core 事件池保护/
渡劫陨落走真突破/寄读 desc 诚实化/doas 死代码清理等 30 项
测试 239→256;金钟罩复验(防御批零漂移)
106 lines
4.3 KiB
TypeScript
106 lines
4.3 KiB
TypeScript
import { useGameStore } from '../store'
|
|
import { useMemo, useState } from 'react'
|
|
import { ChronicleEntry } from '../../game/types/domain'
|
|
|
|
const CAT_NAME: Record<string, string> = {
|
|
birth: '诞庆',
|
|
marriage: '姻缘',
|
|
death: '祭丧',
|
|
breakthrough: '突破',
|
|
battle: '战事',
|
|
trade: '商贸',
|
|
diplomacy: '外交',
|
|
exploration: '寻宝',
|
|
building: '营造',
|
|
event: '奇遇',
|
|
misc: '家常'
|
|
}
|
|
|
|
export default function ChroniclePanel() {
|
|
const world = useGameStore((s) => s.world)
|
|
const revision = useGameStore((s) => s.revision)
|
|
void revision
|
|
const [filter, setFilter] = useState<string>('all')
|
|
const [battleId, setBattleId] = useState<string | null>(null)
|
|
const entries = useMemo(() => {
|
|
if (!world) return []
|
|
const all = [...world.state.chronicle]
|
|
const filtered = filter === 'all' ? all : all.filter((e) => e.category === filter)
|
|
return filtered.slice().sort((a, b) => (b.year - a.year) || (b.month - a.month))
|
|
}, [world, filter, revision])
|
|
if (!world) return null
|
|
const battles = world.state.battles.slice().reverse()
|
|
const battle = battles.find((b) => b.id === battleId)
|
|
|
|
return (
|
|
<div>
|
|
<div className="help-text">
|
|
族史一卷:由系统自动记下的家族大事。倒序排列,可回看任意一年的兴衰细节。
|
|
</div>
|
|
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 10 }}>
|
|
<button className={`btn btn-sm ${filter === 'all' ? 'btn-primary' : ''}`} onClick={() => setFilter('all')}>全部</button>
|
|
{Object.entries(CAT_NAME).map(([k, v]) => (
|
|
<button key={k} className={`btn btn-sm ${filter === k ? 'btn-primary' : ''}`} onClick={() => setFilter(k)}>
|
|
{v}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<div className="chronicle-list">
|
|
{entries.map((e) => (
|
|
<div key={e.id} className={`ch-item ${e.important ? 'gold' : ''}`}>
|
|
<span className="ch-year">{e.year}年{e.month}月</span>
|
|
<span>
|
|
<span className="tag" style={{ marginRight: 8 }}>{CAT_NAME[e.category] ?? e.category}</span>
|
|
{e.text}
|
|
</span>
|
|
</div>
|
|
))}
|
|
{entries.length === 0 && <div className="dim2">暂无记载。</div>}
|
|
</div>
|
|
|
|
<div className="card-title" style={{ fontSize: '1.02rem', marginTop: 22 }}>战报匣(全文留存)</div>
|
|
{battle ? (
|
|
<div className="mission-card" style={{ maxWidth: 720 }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
<b>{battle.title}</b>
|
|
<span className="dim2" style={{ fontSize: '0.85rem' }}>
|
|
{battle.year}年{battle.month}月 ·{' '}
|
|
<span className={battle.winner === 'player' ? 'good' : battle.winner === 'enemy' ? 'bad' : 'warn'}>
|
|
{battle.winner === 'player' ? '我方得胜' : battle.winner === 'enemy' ? '我方败北' : '平分秋色'}
|
|
</span>
|
|
</span>
|
|
<button className="btn btn-sm" style={{ marginLeft: 'auto' }} onClick={() => setBattleId(null)}>
|
|
收起
|
|
</button>
|
|
</div>
|
|
<div className="dim" style={{ fontSize: '0.92rem', lineHeight: 1.8, marginTop: 8 }}>
|
|
{battle.lines.slice(1).map((l, i) => (
|
|
<div key={i}>· {l}</div>
|
|
))}
|
|
{battle.losses.length > 0 && (
|
|
<div className="bad">我方折损:{battle.losses.join('、')}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : battles.length === 0 ? (
|
|
<div className="dim2" style={{ marginBottom: 6 }}>尚无战事可录。</div>
|
|
) : (
|
|
<div style={{ maxWidth: 720 }}>
|
|
{battles.map((b) => (
|
|
<div key={b.id} className="set-row" style={{ maxWidth: 720 }}>
|
|
<span>
|
|
<span className="ch-year" style={{ marginRight: 10 }}>{b.year}年{b.month}月</span>
|
|
{b.title}
|
|
<span className={`tag ${b.winner === 'player' ? 'good' : b.winner === 'enemy' ? 'bad' : 'warn'}`} style={{ marginLeft: 8 }}>
|
|
{b.winner === 'player' ? '胜' : b.winner === 'enemy' ? '负' : '平'}
|
|
</span>
|
|
</span>
|
|
<button className="btn btn-sm" onClick={() => setBattleId(b.id)}>阅</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|