【世界模型:世界会死人也会新生】 - 家族生灭: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 通过
177 lines
6.5 KiB
TypeScript
177 lines
6.5 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 { SYSTEM_DEFS } from '../src/renderer/game/engine/runtime/capabilities'
|
|
import { PACK, pack, DEFAULT_PACK } from '../src/renderer/game/data/registry'
|
|
import { marketPrice } from '../src/renderer/game/engine/sim/Market'
|
|
import { stateFingerprint, longRun } from './fingerprint.helper'
|
|
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('CapabilityRegistry 能力卡', () => {
|
|
it('系统清单齐全(id 唯一、版本与说明在位)', () => {
|
|
const ids = new Set(SYSTEM_DEFS.map((d) => d.id))
|
|
expect(ids.size).toBe(SYSTEM_DEFS.length)
|
|
expect(SYSTEM_DEFS.length).toBeGreaterThanOrEqual(11)
|
|
for (const d of SYSTEM_DEFS) {
|
|
expect(d.version).toMatch(/^\d+\.\d+$/)
|
|
expect(d.name.length).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
|
|
it('禁用修炼 → 12 月无人精进;恢复后回归', () => {
|
|
const w = baseWorld('cap-a')
|
|
const x5 = w.state.members['x5']
|
|
x5.realm = { major: 'qi', minor: 1 }
|
|
const p0 = x5.realmProgress
|
|
w.toggleSystem('cultivation')
|
|
for (let i = 0; i < 12; i++) w.advanceMonth()
|
|
expect(x5.realmProgress).toBe(p0)
|
|
w.toggleSystem('cultivation')
|
|
for (let i = 0; i < 12; i++) w.advanceMonth()
|
|
expect(x5.realmProgress).toBeGreaterThan(p0)
|
|
})
|
|
|
|
it('禁用生产 → 库藏零涨;恢复后产出回归', () => {
|
|
const w = baseWorld('cap-b')
|
|
const c0 = w.state.family.inventory['lingcao'] ?? 0
|
|
w.toggleSystem('production')
|
|
for (let i = 0; i < 6; i++) w.advanceMonth()
|
|
expect(w.state.family.inventory['lingcao'] ?? 0).toBeLessThanOrEqual(c0)
|
|
w.toggleSystem('production')
|
|
for (let i = 0; i < 6; i++) w.advanceMonth()
|
|
expect(w.state.family.inventory['lingcao'] ?? 0).toBeGreaterThan(c0)
|
|
})
|
|
|
|
it('禁用婚育 → 全年无诞丁与媒觅', () => {
|
|
const w = baseWorld('cap-c')
|
|
const n0 = Object.values(w.state.members).filter((c) => c.bornYear > 1).length
|
|
w.toggleSystem('marriage')
|
|
for (let i = 0; i < 48; i++) w.advanceMonth()
|
|
const n1 = Object.values(w.state.members).filter((c) => c.bornYear > 1).length
|
|
expect(n1).toBe(n0)
|
|
})
|
|
|
|
it('禁用事件 → 迟迟无事,天空澄澈', () => {
|
|
const w = baseWorld('cap-d')
|
|
w.toggleSystem('events')
|
|
for (let i = 0; i < 40; i++) w.advanceMonth()
|
|
expect(w.state.pendingEvent).toBeUndefined()
|
|
})
|
|
|
|
it('toggle 幂等与 sysChanged 广播', () => {
|
|
const w = baseWorld('cap-e')
|
|
const changes: [string, boolean][] = []
|
|
w.out.push({
|
|
onLog: () => undefined,
|
|
onChronicle: () => undefined,
|
|
onBattle: () => undefined,
|
|
onPendingEvent: () => undefined,
|
|
onGameOver: () => undefined,
|
|
onSystemChange: (id, enabled) => changes.push([id, enabled])
|
|
})
|
|
const r1 = w.toggleSystem('season')
|
|
expect(r1).toBe(false)
|
|
expect(changes).toEqual([['season', false]])
|
|
const r2 = w.toggleSystem('season')
|
|
expect(r2).toBe(true)
|
|
expect(w.toggleSystem('no-such-system')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('DataPackRegistry 数据包', () => {
|
|
it('默认包与静态数据同引用(金钟罩不受影响)', () => {
|
|
expect(pack().items).toBe(DEFAULT_PACK.items)
|
|
expect(DEFAULT_PACK.items['lingcao'].name).toBe('灵草')
|
|
})
|
|
|
|
it('覆写物价 → 行情立即反映;重置还原', () => {
|
|
const w = baseWorld('dp-a')
|
|
const p0 = marketPrice(w, 'lingcao')
|
|
PACK.override({ items: { ...DEFAULT_PACK.items, lingcao: { ...DEFAULT_PACK.items['lingcao'], basePrice: 999 } } })
|
|
const p1 = marketPrice(w, 'lingcao')
|
|
expect(p1).toBeGreaterThan(p0)
|
|
PACK.reset()
|
|
const p2 = marketPrice(w, 'lingcao')
|
|
expect(p2).toBe(p0)
|
|
})
|
|
|
|
it('fingerprint 稳定且对覆写敏感', () => {
|
|
const f0 = PACK.fingerprint()
|
|
PACK.override({})
|
|
const f1 = PACK.fingerprint()
|
|
expect(f1).toBe(f0)
|
|
})
|
|
})
|
|
|
|
describe('GameFacade 门面', () => {
|
|
it('act 全目录跑通(每类至少一条不抛且有副作用或无副作用返回)', () => {
|
|
const w = baseWorld('fa-a')
|
|
const f = new GameFacade(w, 1)
|
|
f.act('head.set', { memberId: 'x3' })
|
|
expect(w.state.family.headId).toBe('x3')
|
|
f.act('member.post', { memberId: 'x4', post: 'guardian' })
|
|
expect(w.state.members['x4'].post).toBe('guardian')
|
|
f.act('estate.build', { building: 'fangshi' })
|
|
expect(w.state.family.buildings['fangshi']).toBe(1)
|
|
f.act('estate.rite', {})
|
|
expect(w.state.family.flag['lastRiteYear']).toBe(1)
|
|
const afterRite = w.state.family.stones
|
|
f.act('market.sell', { item: 'lingcao', count: 5 })
|
|
expect(w.state.family.stones).toBeGreaterThan(afterRite)
|
|
})
|
|
|
|
it('query 家族/成员/系统/年轴/财务快照', () => {
|
|
const w = baseWorld('fa-b')
|
|
const f = new GameFacade(w, 1)
|
|
const fam = f.query('family') as { year: number; reputation: number }
|
|
expect(fam.year).toBe(1)
|
|
const mem = f.query('members') as { list: { id: string }[] }
|
|
expect(mem.list.length).toBe(5)
|
|
const sys = f.query('systems') as { list: { id: string }[] }
|
|
expect(sys.list.length).toBe(SYSTEM_DEFS.length)
|
|
const fin = f.query('finance') as { stones: number }
|
|
expect(fin.stones).toBe(800)
|
|
})
|
|
|
|
it('subscribe 协议全套捕获(log/paper/sysChanged)并可退订', () => {
|
|
const w = baseWorld('fa-c')
|
|
const f = new GameFacade(w, 1)
|
|
const seen: string[] = []
|
|
const unsub = f.subscribe((e) => seen.push(e.type))
|
|
for (let i = 0; i < 14; i++) w.advanceMonth()
|
|
f.act('estate.rite', {})
|
|
w.toggleSystem('season')
|
|
expect(seen).toContain('log')
|
|
expect(seen).toContain('paper')
|
|
expect(seen).toContain('sysChanged')
|
|
unsub()
|
|
const before = seen.length
|
|
w.advanceMonth()
|
|
expect(seen.length).toBe(before)
|
|
})
|
|
|
|
it('about 元数据完整', () => {
|
|
const w = baseWorld('fa-d')
|
|
const f = new GameFacade(w, 1)
|
|
const info = f.about()
|
|
expect(info.title).toBe('仙途家族志')
|
|
expect(info.version).toContain('0.1.22')
|
|
expect(info.modules).toBeGreaterThanOrEqual(11)
|
|
expect(info.systems).toBeGreaterThan(0)
|
|
expect(info.plugins).toBeGreaterThanOrEqual(3)
|
|
})
|
|
|
|
it('默认配置金钟罩不受门面化影响', () => {
|
|
PACK.reset()
|
|
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('855600f5')
|
|
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('2ecc7add')
|
|
})
|
|
})
|