【A+I】migrate.ts 迁移链(CURRENT_SCHEMA/MIGRATIONS/migrateIfNeeded/MigrationError)整链铲除→validateLoadedState 结构校验(seed/family/members 断言);信封统一 payload 单格式(去双格式兜底);APP_ID 保留 【F】schemaVersion 字段全删(domain/creation/save 8 处);SaveMeta.version 记游戏版本字符串 【C】chronicle 死表+saveChronicle 死方法(首行 return+unreachable legacy 僵尸)全删 【D/E】GameEngine __state globalThis 泄漏口+seed 三元(恒不可达)删除;engineFromSnapshot/noAutoCreate 保留(测试便利——非兼容机器) 【B/H】normalizeGameState 兼容批删:老档字段补(28行)/worldGen 重放(18行)/P0-2 占位 def(9行)/worldSim 字段批删;保留 npcs 重注册/headId 修复/d distress 防护;initSim 补 0.1.34 四键(唯一初始源) 【G】domain 收紧 5 兼容 optional(techniqueRank/Progress/tribBoost/worldGen 必填)+pcgen 必写 【测试】删 9 个兼容验证体(audit 0.1.0-era/全残缺矩阵/空 worldSim 兜底);5989 全绿——金钟罩零漂(新档结构全字段,删除不涉世界数值)
88 lines
3.3 KiB
TypeScript
88 lines
3.3 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
||
import { World, normalizeGameState } from '../src/renderer/game/engine/runtime/World'
|
||
import { EVENTS, EffectDef } from '../src/renderer/game/data/events'
|
||
import { applyEventChoice } from '../src/renderer/game/engine/runtime/Systems/events'
|
||
import { POSTS } from '../src/renderer/game/data/posts'
|
||
import { ASPIRATIONS } from '../src/renderer/game/data/aspirations'
|
||
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('事件选项应用矩阵(逐事件逐选项)', () => {
|
||
it.each(EVENTS.flatMap((e) => e.options.map((o, idx) => [e.id, idx, o.label] as const)))(
|
||
'事件 %s 选%i(%s)应用后世界不破',
|
||
(id, idx) => {
|
||
const w = baseWorld('ev-app-' + id)
|
||
applyEventChoice(w, id, idx)
|
||
expect(w.state.family.stones).toBeGreaterThanOrEqual(0)
|
||
for (const inv of Object.values(w.state.family.inventory)) {
|
||
if (typeof inv === 'number') expect(inv).toBeGreaterThanOrEqual(0)
|
||
}
|
||
}
|
||
)
|
||
})
|
||
|
||
describe('effect 结构合法矩阵', () => {
|
||
it.each(EVENTS.flatMap((e) => e.options.map((o) => [e.id, o.eff] as const)))('事件 %s 效果可回放', (id, eff) => {
|
||
expect(typeof eff).toBe('object')
|
||
expect(eff).not.toBeNull()
|
||
})
|
||
|
||
it.each(EVENTS.flatMap((e) => e.options.map((o, i) => [e.id, i, o.eff.res] as const)).filter(([, , r]) => !!r))(
|
||
'事件 %s 资源效果引用存在',
|
||
(_id, _i, res) => {
|
||
for (const [k] of Object.entries(res!)) {
|
||
expect(k === 'stones' || ['lingcao', 'lingkuang', 'beastcore'].includes(k) || k.startsWith('pill-') || k.startsWith('weapon-')).toBe(true)
|
||
}
|
||
}
|
||
)
|
||
})
|
||
|
||
describe('职事能力矩阵', () => {
|
||
// head 由继承链独占(assignPost 拒绝),跳过
|
||
const assignable = Object.entries(POSTS).filter(([id]) => id !== 'head')
|
||
it.each(assignable.map(([id, def]) => [id, def.max] as const))('职事 %s 上限可重复指派', (id, max) => {
|
||
const w = baseWorld('post-' + id)
|
||
let assigned = 0
|
||
for (const c of Object.values(w.state.members)) {
|
||
if (assigned >= max) break
|
||
if (w.assignPost(c.id, id)) assigned++
|
||
}
|
||
expect(assigned).toBe(max)
|
||
})
|
||
|
||
it.each(assignable.map(([id]) => [id] as const))('职事 %s 不可指派寄读者', (id) => {
|
||
const w = baseWorld('post-ap-' + id)
|
||
const c = w.state.members['x3']
|
||
c.state = 'apprentice'
|
||
expect(w.assignPost(c.id, id)).toBe(false)
|
||
})
|
||
})
|
||
|
||
describe('志向矩阵', () => {
|
||
it.each(Object.keys(ASPIRATIONS).map((id) => [id] as const))('志向 %s 生效于世界(无异常)', (id) => {
|
||
const w = baseWorld('asp-' + id)
|
||
for (const c of Object.values(w.state.members)) c.aspiration = id
|
||
w.advanceMonth()
|
||
expect(true).toBe(true)
|
||
})
|
||
})
|
||
|
||
describe('大比寄读渡劫动态事件矩阵', () => {
|
||
it.each([
|
||
['ev-tournament-10', '称病不出'],
|
||
['ev-recruit', '婉拒'],
|
||
['ev-centennial', '阖家简庆']
|
||
] as const)('%s 选「%s」无副作用', (id, _label) => {
|
||
const w = baseWorld('dyn-' + id.slice(0, 6))
|
||
const optIdx = 0 // 第一选项最简
|
||
applyEventChoice(w, id, optIdx)
|
||
expect(w.state.pendingEvent).toBeUndefined()
|
||
})
|
||
})
|