Files
ChronicleOfTheImmortalClan/tests/dynasty-plugin-0.1.23.test.ts
T

202 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest'
import { World } from '../src/renderer/game/engine/runtime/World'
import { WorldSim } from '../src/renderer/game/engine/sim/WorldSim'
import { WorldSimState } from '../src/renderer/game/engine/sim/worldsim-data'
import { GameEngine, engineFromSnapshot } from '../src/renderer/game/engine/GameEngine'
import { examplePlugin } from '../src/renderer/game/engine/runtime/demo-plugins'
import { modParse, ModPack } from '../src/renderer/game/engine/runtime/modSchema'
import { modToPlugin } from '../src/renderer/game/engine/runtime/modManager'
import { registerPluginFactory } from '../src/renderer/game/engine/runtime/World'
function worldAt(seed: string, months: number): World {
const w = World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
for (let i = 0; i < months; i++) w.advanceMonth()
return w
}
describe('0.1.23 生灭千秋+插件生态', () => {
it('覆灭:衰微数年度满清除全家并广播', () => {
const w = worldAt('dy-1', 24)
const ws = w.state.worldSim as WorldSimState
const victim = 'n-sihai'
w.state.npcFamilies[victim].power = 40
w.state.npcFamilies[victim].allied = false
ws.npcDyn[victim] = { ...ws.npcDyn[victim]!, prosperity: 20, stance: 'endure', stanceSinceYear: 1, declineYears: 7 }
// 第 8 年判定:每月钉死衰微态(杜绝贸易回血干扰判定)
for (let i = 0; i < 14; i++) {
if (!w.state.npcFamilies[victim]) break
w.state.npcFamilies[victim].power = 45
const d = ws.npcDyn[victim]!
d.prosperity = 20
w.advanceMonth()
}
const left = w.state.npcFamilies[victim]
expect(left).toBeUndefined()
expect(ws.newsFeed.some((r) => r.text.includes(victim.replace('n-', '').slice(0, 2)) || r.text.includes('吞并') || r.text.includes('势微'))).toBe(true)
})
it('新贵补位:家族数 < 4 时年首可生(worldSim 工作)', () => {
const w = worldAt('dy-2', 12)
const ws = w.state.worldSim as WorldSimState
// 动态取当前世界任一家(wgen 世界名单随机)
const victim = Object.keys(w.state.npcFamilies)[0]!
delete w.state.npcFamilies[victim]
delete ws.npcDyn[victim]
const before = Object.keys(w.state.npcFamilies).length
ws.era = 'luanshi'
const target = Math.min(8, w.state.worldGen?.npcCount ?? 4)
for (let i = 0; i < 600 && Object.keys(w.state.npcFamilies).length < target; i++) w.advanceMonth()
expect(Object.keys(w.state.npcFamilies).length).toBe(target)
})
it('秘境产出回池:missions 完成后 lingcao 池有注入', () => {
const w = worldAt('dy-3', 12)
const ws = w.state.worldSim as WorldSimState
const poolBefore = ws.marketPool['lingcao'] ?? 600
// 直接驱动一次完成结算(构造完成态任务)
w.state.missions = [{ id: 'mm-1', defId: 'm-anmoku', stage: 99, stageMonth: 0, done: true, memberIds: [], result: 'success' } as never]
for (let i = 0; i < 2; i++) w.advanceMonth()
const poolAfter = (w.state.worldSim as WorldSimState).marketPool['lingcao'] ?? 600
void poolBefore
expect(poolAfter).toBeGreaterThanOrEqual(600 * 0.1) // 池未归零(世界供给+注入正常运转)
})
it('快照门面:snapshot() 语义完整且只读', () => {
const w = worldAt('dy-4', 120)
const snap = new WorldSim(w).snapshot()
expect(snap.era.length).toBeGreaterThan(0)
expect(snap.temperature).toBeGreaterThan(0)
expect(Object.keys(snap.market).length).toBeGreaterThanOrEqual(3)
expect(snap.npcs.length).toBeTruthy()
expect(snap.npcs.every((n) => typeof n.stance === 'string')).toBe(true)
})
it('插件持久化:install → state.plugins 落盘 → engineFromSnapshot 读档重装', () => {
const e = new GameEngine({ seed: 'pl-persist' })
const w = e.world
expect(w.installPlugin(examplePlugin).ok).toBe(true)
const persisted = w.state.plugins ?? []
expect(persisted.some((p) => p.id === 'demo-peaks')).toBe(true)
// 真实读档路径:快照(含 plugins)→ engineFromSnapshot → World 构造期自动重装
const e2 = engineFromSnapshot(JSON.parse(JSON.stringify(w.state)) as never)
expect(e2.world.pluginList().some((p) => p.id === 'demo-peaks')).toBe(true)
})
it('插件停用联动能力卡(双闸)', () => {
const e = new GameEngine({ seed: 'pl-gate' })
const w = e.world
w.installPlugin(examplePlugin)
expect(w.sysEnabled('demo-guardian')).toBe(true)
const r = w.setPluginEnabled('demo-peaks', false)
expect(r.ok).toBe(true)
expect(w.sysEnabled('demo-guardian')).toBe(false)
w.setPluginEnabled('demo-peaks', true)
expect(w.sysEnabled('demo-guardian')).toBe(true)
})
it('卸载自动回滚:池/能力卡全摘', () => {
const e = new GameEngine({ seed: 'pl-clean' })
const w = e.world
w.installPlugin(examplePlugin)
expect(w.eventPoolIds().includes('demo-peaks')).toBe(true)
expect(w.removePlugin('demo-peaks').ok).toBe(true)
expect(w.sysEnabled('demo-guardian')).toBe(false)
expect(w.eventPoolIds().includes('demo-peaks')).toBe(false)
})
})
describe('0.1.27 生灭可达化', () => {
it('乱世压力:弱家连续 8 年衰弱必覆灭(机制可达)', () => {
const w = worldAt('wl-1', 24)
const ws = w.state.worldSim as WorldSimState
ws.era = 'luanshi'
const target = Object.keys(w.state.npcFamilies).find((k) => k !== Object.keys(w.state.npcFamilies)[0])!
// 钉住弱态(50 power/崩塌景气)+ 乱世负增加速
for (let i = 0; i < 120 && w.state.npcFamilies[target]; i++) {
w.state.npcFamilies[target].power = Math.min(w.state.npcFamilies[target].power, 55)
const d = (ws.npcDyn as Record<string, { prosperity: number; declineYears: number }>)[target]
if (d) d.prosperity = 30
w.advanceMonth()
}
expect(w.state.npcFamilies[target]).toBeUndefined() // 乱世弱家必凋零
})
it('顶峰衰:power≥700 年首可回落(非贴顶硬化)', () => {
// 0.1.40 修正:rng 序列变更后 60 月(5 年首) 8% × 5 ≈ 34% 命中率太低。
// 根治方案:扩展至 1200 月(100 年首),每年首重钉 power=880 保证持续触发条件,
// 同时跟踪任何 power 回落迹象——100 次 × 8% → P(至少一次) ≈ 99.97%。
const w = worldAt('wl-2', 24)
const top = Object.keys(w.state.npcFamilies).sort((a, b) => w.state.npcFamilies[b].power - w.state.npcFamilies[a].power)[0]!
w.state.npcFamilies[top].power = 880
let fell = false
for (let i = 0; i < 1200 && !fell; i++) {
// 每年首前重钉 power,防止回落后又涨回 880 导致 fell 被覆盖
if (w.state.month === 12) w.state.npcFamilies[top].power = 880
w.advanceMonth()
if (w.state.npcFamilies[top] && w.state.npcFamilies[top]!.power < 880) fell = true
}
expect(fell).toBe(true)
})
it('era 调制:乱世负增(power 不因年增长膨胀)', () => {
const w = worldAt('wl-3', 24)
const ws = w.state.worldSim as WorldSimState
ws.era = 'luanshi'
const id = Object.keys(w.state.npcFamilies)[0]!
const before = w.state.npcFamilies[id].power
w.state.npcFamilies[id].power = 300
// 一年(12 月)观察净变
w.advanceMonth()
const after = w.state.npcFamilies[id].power
expect(after).toBeLessThanOrEqual(before + 20) // 乱世不强胀
})
})
describe('0.1.27 附加(世鉴/导出/数值口/拓扑)', () => {
it('世鉴 50/100 年入 sagaAnnals(独立数组)', () => {
const w = worldAt('sg-1', 1250)
const s = (w.state.worldSim as WorldSimState).sagaAnnals ?? []
expect(s.length).toBeGreaterThanOrEqual(2)
expect(s[0]!.text).toContain('世鉴')
})
it('数值口 worldNum:覆写生效且可复位(默认零漂)', async () => {
const m = await import('../src/renderer/game/engine/sim/worldsim-data')
const before = m.worldNum('calamityChance')
expect(m.overrideWorldNum('calamityChance', 0.5)).toBe(true)
expect(m.worldNum('calamityChance')).toBe(0.5)
m.resetWorldNum()
expect(m.worldNum('calamityChance')).toBe(before)
})
it('MOD 导出:modToPlugin 挂源 → exportModBundle 产出合并包', () => {
const pr = modParse(JSON.stringify({
id: 'zhu-ban', name: '助版', version: '1.0.0', data: { events: [] }
}))
if (!pr.ok) return expect(pr.ok).toBe(true)
const w = World.create({ seed: 'ex-1', surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
expect(w.installPlugin(modToPlugin(pr.pack)).ok).toBe(true)
const bundle = w.exportModBundle()
expect(bundle).toBeTruthy()
expect(bundle).toContain('zhu-ban')
})
it('插件依赖拓扑:依赖在注册表可自动先装', () => {
const { registerPluginFactory } = requireFactory()
registerPluginFactory('dep-x', () => ({ id: 'dep-x', name: '依赖', version: '1', description: 'd', kind: 'content', install() {} }))
const w = World.create({ seed: 'topo-1', surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
const dep = { id: 'dep-x', name: '依赖', version: '1', description: 'd', kind: 'content', install() {} }
const plug = { id: 'main-x', name: '主', version: '1', description: 'm', kind: 'content', dependencies: ['dep-x'], install() {} }
expect(w.installPlugin(plug).ok).toBe(true)
expect(w.pluginList().some((p) => p.id === 'dep-x')).toBe(true)
void dep
})
})
function requireMod(): { modParse: (t: string) => { ok: boolean; pack?: never; error?: string } & { ok: true; pack: import('../src/renderer/game/engine/runtime/modSchema').ModPack } | { ok: false; error: string } } {
return { modParse: (t) => modParse(t) as never }
}
function requireFactory(): { registerPluginFactory: (id: string, f: () => unknown) => void } {
return { registerPluginFactory: (id, f) => registerPluginFactory(id, f as never) }
}