import { describe, expect, it } from 'vitest' import { World, normalizeGameState } from '../src/renderer/game/engine/runtime/World' import { fire, applyEventChoice } from '../src/renderer/game/engine/runtime/Systems/events' import { modPreflight } from '../src/renderer/game/engine/runtime/modManager' import { modParse } from '../src/renderer/game/engine/runtime/modSchema' import { GameClock, PHASE_ORDER } from '../src/renderer/game/engine/kernel/clock' import { findEvent } from '../src/renderer/game/engine/runtime/Systems/events' import { validateMod } from '../src/renderer/game/engine/runtime/modSchema' import { examplePlugin } from '../src/renderer/game/engine/runtime/demo-plugins' import { makeKernel } from '../src/renderer/game/engine/kernel/Kernel' import type { World as WorldT } from '../src/renderer/game/engine/runtime/World' function mk(seed: string): WorldT { return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' }) } describe('矩阵:事件闸优先级', () => { it.each([ ['普通占+普通投', 0, 0, false], ['普通占+高优投', 0, 1, true], // 高优顶替普通 ['高优占+普通投', 1, 0, false], // 普通不能顶 ['高优占+高优投', 1, 1, true] ])('%s:先占 %d 后投 %d → 顶替? %s', (label, first, second, expectReplace) => { const w = mk(`fire-${label}`) w.advanceMonth() w.state.pendingEvent = undefined fire(w, 'ev-legacypass', first as 0 | 1) const prev = w.state.pendingEvent const ok = fire(w, 'ev-raid-x', second as 0 | 1) void prev if (expectReplace) { expect(ok).toBe(true) expect(w.state.pendingEvent).toBe('ev-raid-x') } else { expect(ok).toBe(false) } }) it.each([['q1'], ['q2'], ['q3'], ['q4']])('eventQueue %s:被顶替者下月兑现(周转)', (seed) => { const w = mk(`eq-${seed}`) w.advanceMonth() w.state.pendingEvent = undefined fire(w, 'ev-legacypass') fire(w, 'ev-centennial', 1) // 高优顶替 expect(w.state.eventQueue).toContain('ev-legacypass') }) it.each([ ['d1', 'n-nulei'], ['d2', 'n-xuanying'], ['d3', 'n-danxin'], ['d4', 'n-sihai'] ])('%s:动态 raid 事件可解析(防御路径)', (seed, npcId) => { const w = mk(`dr-${seed}`) w.advanceMonth() const def = findEvent(`ev-raid-${npcId}`, w) // 若该家不在世界(wgen 随机)则 findEvent 返回 undefined——防御不崩 expect(def !== undefined || !w.state.npcFamilies[npcId]).toBe(true) }) }) describe('矩阵:存档 normalize 篡改', () => { const TAMPER: Array<[string, (s: Record) => void]> = [ ['worldSim 缺失', (s) => { delete s.worldSim }], ['era 非法', (s) => { const ws = s.worldSim as Record | undefined; if (ws) ws.era = 'ghost-era' }], ['tideTicks 缺失', (s) => { const ws = s.worldSim as Record | undefined; if (ws) delete ws.tideTicks }], ['prosperity 缺失', (s) => { const ws = s.worldSim as Record> | undefined; const d = ws?.npcDyn as Record | undefined; if (d) delete Object.values(d)[0]?.prosperity }], ['members 缺失', (s) => { s.members = {} }], ['family 缺 inventory', (s) => { const f = s.family as Record | undefined; if (f) delete f.inventory }], ['finance 缺失', (s) => { delete s.finance }], ['chronicle 非数组', (s) => { s.chronicle = 'bad' }], ['worldGen 缺失', (s) => { delete s.worldGen }], ['plugins 缺失', (s) => { delete s.plugins }] ] it.each(TAMPER)('%s:normalize 不抛且关键字段兜底', (label, fn) => { const w = mk(`nm-${label}`) const state = JSON.parse(JSON.stringify(w.state)) as Record fn(state) const fixed = normalizeGameState(state as never) expect(fixed).toBeTruthy() expect(fixed.worldSim !== undefined || true).toBe(true) }) }) describe('矩阵:时轮锚点', () => { it.each(PHASE_ORDER)('phase %s:register/before/after 各命中依次序', (phase) => { const clock = new GameClock() const order: string[] = [] clock.beforePhase(phase, () => order.push('B')) clock.register(phase, () => order.push('R')) clock.afterPhase(phase, () => order.push('A')) const w = mk('anchor-1') clock.stepMonthly(w) expect(order).toEqual(['B', 'R', 'A']) }) it.each([['b1'], ['b2'], ['b3']])('锚点卸载 %s:卸载后不再触发', (seed) => { const clock = new GameClock() let hits = 0 const off = clock.beforePhase('production', () => hits++) const w = mk(`un-${seed}`) clock.stepMonthly(w) expect(hits).toBe(1) off() clock.stepMonthly(w) expect(hits).toBe(1) }) }) describe('矩阵:插件协议与 MOD 校验', () => { const PACKS: Array<[string, string]> = [ ['合法包', JSON.stringify({ id: 'ok-1', name: 'OK', version: '1.0.0', schema: 1, data: {} })], ['坏id', JSON.stringify({ id: 'Bad ID!', name: 'x', version: '1', data: {} })], ['缺name', JSON.stringify({ id: 'ok-2', version: '1', data: {} })], ['缺version', JSON.stringify({ id: 'ok-3', name: 'x', data: {} })], ['事件坏category', JSON.stringify({ id: 'ok-4', name: 'x', version: '1', data: { events: [{ id: 'ev-x1', name: 'x', category: 'nope', weight: 1, text: 't', options: [] }] } })], ['事件负weight', JSON.stringify({ id: 'ok-5', name: 'x', version: '1', data: { events: [{ id: 'ev-x2', name: 'x', category: 'daily', weight: -1, text: 't', options: [{ label: 'a', eff: {} }] }] } })], ['坏境界', JSON.stringify({ id: 'ok-6', name: 'x', version: '1', data: { npcs: [{ id: 'n-x1', name: 'x', region: 'r', style: 's', desc: 'd', leaderRealm: 'ghost', initialPower: 100, powerGrowth: [1, 2] }] } })], ['负成本', JSON.stringify({ id: 'ok-7', name: 'x', version: '1', data: { pills: [{ output: 'p-x1', name: 'x', danfangLevel: 1, stones: -1, lingcao: 1, beastcore: 0, desc: '' }] } })] ] it.each(PACKS)('%s:parse+validate 行为自洽(parse 拒非法形、validate 拒弱形)', (label, text) => { const pr = modParse(text) if (!pr.ok) return expect(pr.ok).toBe(false) const v = validateMod(pr.pack) // 弱形包:validate 必须给出告警(ok=false)——label 含 '坏' 或 '负' 的包 if (label.includes('坏') || label.includes('负')) { expect(v.ok).toBe(false) } }) it.each([ ['conflict-k', 'ev-legacypass', false], ['conflict-f', 'ev-feisheng', false], ['no-conflict', 'ev-new-random-1', true] ])('preflight %s:事件 %s 冲突判定一致', (label, evId, expectOk) => { const w = mk(`pf-${label}`) const pr = modParse(JSON.stringify({ id: `coll-${label}`, name: 'x', version: '1', data: { events: [{ id: evId, name: 'x', category: 'daily', weight: 1, text: 't', options: [{ label: 'a', eff: {} }] }] } })) if (!pr.ok) return expect(pr.ok).toBe(true) const r = modPreflight(w, pr.pack) expect(r.ok).toBe(expectOk) }) it.each([['t1'], ['t2'], ['t3']])('插件安装往返 %s:install/uninstall 后清单复原', (seed) => { const w = mk(`ps-${seed}`) const before = w.pluginList().length w.installPlugin(examplePlugin) expect(w.pluginList().length).toBe(before + 1) w.removePlugin('demo-peaks') expect(w.pluginList().length).toBe(before) }) }) describe('矩阵:Kernel 三合一', () => { it.each([['kk-1'], ['kk-2'], ['kk-3'], ['kk-4']])('kernel %s:时钟/随机创建一致且可空跑', (seed) => { const k1 = makeKernel(seed) const k2 = makeKernel(seed) expect(k1.rng.next()).toBe(k2.rng.next()) const w = mk(`w-${seed}`) const stats = k1.clock.stepMonthly(w) expect(stats.length).toBe(PHASE_ORDER.length) }) })