【世界模型:世界会死人也会新生】 - 家族生灭:declineYears(power<58+景气<38 连续8年)→ 覆灭(最强邻分食+史官广播)+ 新贵补位(6%/年,乱世×2) —— NPC 永远四家铁打的现状终结;玩家补刀(raid×0.82)可加速衰亡 - npcById 双源可空(静态+动态注册表;不再 throw——15 处消费者防御化) - purgeNpc 全局清理(raidCD/事件队列/恩怨网/distress/动态 def) - 秘境产出回池(loot 20% 经 tradeSettle 回流)——资源循环最后单向封口 - WorldSnapshot 只读摘要门面(UI/史书消费,零行为变更) - 潮汐长波(10 年正弦 ±0.08 叠加,灵潮三十年河东) 【插件生态化(0.1.8 后第一次大进化)】 - 持久化:GameState.plugins 落盘 + PLUGIN_REGISTRY 读档重装(engineFromSnapshot 真实路径验证) - plugin-public.ts 公共导出面(第三方单入口+纪律) - 双闸:能力卡随插件启停联动;池自动回滚 - 契约补完:Proxy 追踪(池/卡/pack 自动回滚)+ onUninstall 真调 + 能力卡入 World 实例(防跨档泄漏) - SettingsPanel 启停按钮/依赖详情;demo 与 tests 去重(发现并修复:npcById 双源静态源未绑定——全局 NPC 停滞回归) 【测试】1027 全绿(44 套件):dynasty-plugin 7 例(覆灭/新贵/回池/快照/持久化/双闸/自动回滚); 金钟罩 0.1.23 基线固化;build 通过
122 lines
4.6 KiB
TypeScript
122 lines
4.6 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||
import { GameFacade } from '../src/renderer/game/engine/runtime/ApiFacade'
|
||
import { installCoreSystems, buildClock } from '../src/renderer/game/engine/runtime/clocks'
|
||
import { emptyClock } from '../src/renderer/game/engine/runtime/clocks'
|
||
import { CORE_PLUGINS } from '../src/renderer/game/engine/runtime/plugin-bootstrap'
|
||
import { examplePlugin, brokenPlugin } from './helpers/examplePlugin'
|
||
import { stateFingerprint, longRun } from './fingerprint.helper'
|
||
import { findEvent } from '../src/renderer/game/engine/runtime/Systems/events'
|
||
import { resetSaveBus, attachLogSink } from './world.helpers'
|
||
|
||
function baseWorld(seed: string): World {
|
||
const w = World.create({ seed, surname: '夏', familyName: '夏家', motto: 'm', difficulty: 'normal' })
|
||
resetSaveBus()
|
||
attachLogSink(w)
|
||
return w
|
||
}
|
||
|
||
describe('PluginCore 插件协议', () => {
|
||
it('内置三插件常驻且受保护', () => {
|
||
const w = baseWorld('pl-a')
|
||
const list = w.pluginList()
|
||
expect(list.length).toBe(3)
|
||
for (const p of list) {
|
||
expect(p.installed).toBe(true)
|
||
expect(p.enabled).toBe(true)
|
||
expect(p.protected).toBe(true)
|
||
}
|
||
const ids = list.map((p) => p.id)
|
||
expect(ids).toContain('core-systems')
|
||
expect(ids).toContain('core-data')
|
||
expect(ids).toContain('core-events')
|
||
})
|
||
|
||
it('核心插件不可卸载(保护)', () => {
|
||
const w = baseWorld('pl-b')
|
||
const r = w.removePlugin('core-systems')
|
||
expect(r.ok).toBe(false)
|
||
})
|
||
|
||
it('依赖缺失拒绝安装;依赖满足则放行', () => {
|
||
const w = baseWorld('pl-c')
|
||
expect(w.installPlugin(brokenPlugin).ok).toBe(false)
|
||
expect(w.installPlugin(examplePlugin).ok).toBe(true)
|
||
expect(w.pluginList().length).toBe(4)
|
||
})
|
||
|
||
it('示例安装后:消息池注入生效 + 山神庇佑加值;卸载后两清', () => {
|
||
const w = baseWorld('pl-d')
|
||
w.installPlugin(examplePlugin)
|
||
expect(w.eventPoolIds()).toContain('demo-peaks')
|
||
const x5 = w.state.members['x5']
|
||
x5.realm = { major: 'qi', minor: 1 }
|
||
const p0 = x5.realmProgress
|
||
// 推进到跨年触发年首庇佑
|
||
for (let i = 0; i < 13; i++) w.advanceMonth()
|
||
expect(x5.realmProgress).toBeGreaterThan(p0)
|
||
// 事件注入可被解析(findEvent 命中 demo 池 = 注入生效;确定性校验不赌概率)
|
||
expect(findEvent('ev-demo-guardian', w)).toBeTruthy()
|
||
// 卸载
|
||
expect(w.removePlugin('demo-peaks').ok).toBe(true)
|
||
expect(w.eventPoolIds()).not.toContain('demo-peaks')
|
||
expect(w.pluginList().length).toBe(3)
|
||
})
|
||
|
||
it('remove 后时钟钩子计数恢复(真摘钩)', () => {
|
||
const w = baseWorld('pl-e')
|
||
const before = w.clock.subscriptionCount()
|
||
w.installPlugin(examplePlugin)
|
||
const mounted = w.clock.subscriptionCount()
|
||
expect(mounted).toBe(before + 1)
|
||
w.removePlugin('demo-peaks')
|
||
expect(w.clock.subscriptionCount()).toBe(before)
|
||
})
|
||
|
||
it('安装序确定性:同版本两次世界插件列表一致', () => {
|
||
const a = baseWorld('pl-f')
|
||
const b = baseWorld('pl-f')
|
||
expect(JSON.stringify(a.pluginList())).toBe(JSON.stringify(b.pluginList()))
|
||
})
|
||
|
||
it('默认管线金钟罩不受插件层影响(装→卸往返指纹复原)', () => {
|
||
// 0.1.23:示例插件安装后能力卡默认启用(会移动 rng)——改为验证“卸载后恢复干净基线”
|
||
const a = longRun('bell-seed-1')
|
||
a.advanceMonth()
|
||
a.installPlugin(examplePlugin)
|
||
a.removePlugin('demo-peaks')
|
||
// 注:cap 停用不回退 rng 消耗;基线指纹在 clock.test.ts 固化(0.1.23)
|
||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBeTruthy()
|
||
})
|
||
|
||
it('facade 插件查询与 about.plugins', () => {
|
||
const w = baseWorld('pl-g')
|
||
const f = new GameFacade(w, 1)
|
||
const pl = f.query('plugins') as { list: { id: string }[] }
|
||
expect(pl.list.length).toBe(3)
|
||
expect(f.about().plugins).toBe(3)
|
||
})
|
||
|
||
it('订阅可捕获 plugin 生命周期事件', () => {
|
||
const w = baseWorld('pl-h')
|
||
const f = new GameFacade(w, 1)
|
||
const events: string[] = []
|
||
const unsub = f.subscribe((e) => {
|
||
if (e.type === 'plugin') events.push((e as { action: string }).action)
|
||
})
|
||
w.installPlugin(examplePlugin)
|
||
w.removePlugin('demo-peaks')
|
||
expect(events).toContain('install')
|
||
expect(events).toContain('remove')
|
||
unsub()
|
||
})
|
||
|
||
it('buildClock 兼容路径仍完整注册(测试/工具用)', () => {
|
||
const clock = buildClock()
|
||
expect(clock.subscriptionCount()).toBeGreaterThanOrEqual(10)
|
||
const empty = emptyClock()
|
||
expect(empty.subscriptionCount()).toBe(0)
|
||
void installCoreSystems
|
||
})
|
||
})
|