v0.1.35b: 兼容层铲除——迁移链/版本戳/死表死代码/normalize 兼容批/死字段

【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 全绿——金钟罩零漂(新档结构全字段,删除不涉世界数值)
This commit is contained in:
2026-08-23 22:35:17 +08:00
parent 1b1dfe8ac8
commit 9b0f36c70a
15 changed files with 44 additions and 285 deletions
+5 -5
View File
@@ -42,20 +42,20 @@ export class GameEngine {
this.kernel = makeKernel(this._seed)
this.datapack = PACK
if (opts.datapack) this.datapack.override(opts.datapack)
// 组装World(内核注入→时钟/随机/总线单源)
// 组装World(内核注入→时钟/随机/总线单源0.1.35 移除 __state 泄漏口——seed 恒用 _seed
if (opts.noAutoCreate) {
this.world = new World(createWorldState({
seed: this._seed, surname: '占位', familyName: '占位', motto: '', difficulty: 'normal'
}), [], this.kernel)
} else {
const state = opts.seed ? createWorldState({
const state = createWorldState({
seed: this._seed,
surname: opts.surname?.trim() || '林',
familyName: opts.familyName?.trim() || `${opts.surname?.trim() || '林'}`,
motto: opts.motto?.trim() || '耕读传家,术法继世',
difficulty: opts.difficulty ?? 'normal'
}) : (globalThis as never as { __state?: GameState }).__state
this.world = new World(state!, [], this.kernel)
})
this.world = new World(state, [], this.kernel)
}
this.facade = new ApiFacade(this.world, 1)
// Kernel 总线 → World.out 桥接(attached 后同步 feed
@@ -132,7 +132,7 @@ export class GameEngine {
}
}
/** 已用旧内核时钟注册的世界快照,反序列化时公用(便捷入口) */
/** 已用旧内核时钟注册的世界快照,反序列化时公用(测试/便捷入口——非兼容机器 */
export function engineFromSnapshot(state: GameState): GameEngine {
const eng = new GameEngine({ seed: state.seed, noAutoCreate: true })
eng.restore(state)
+2 -78
View File
@@ -78,90 +78,14 @@ export function normalizeGameState(state: GameState): GameState {
registerNpcDef(n as never)
}
}
// P0-2 双保险:npcFamilies 中缺 def 的 id → 占位 def(防极端旧档僵尸)
for (const id of Object.keys(state.npcFamilies ?? {})) {
if (!npcById(id)) {
const n = state.npcFamilies[id]
registerNpcDef({
id, name: n.name, region: n.region, style: '遗族', desc: '世族遗脉,家道中落而名犹在册。',
leaderRealm: 'foundation', initialPower: n.power, powerGrowth: [2, 6], sells: [], buys: ['lingcao']
})
}
}
if (!state.worldGen && state.seed) {
const wg = generateWorld(state.seed)
state.worldGen = {
relations: wg.relations,
eras: wg.eras,
marketOffset: wg.marketOffset,
regionFlavor: wg.regionFlavor,
alliances: wg.alliances,
feuds: wg.feuds,
npcCount: wg.npcs.length,
// P1-9 重放同步 npcs 摘要(与 creation 一致——该档自此自捕获,不再每次重放)
npcs: wg.npcs.map((n) => ({
id: n.id, name: n.name, region: n.region, style: n.style,
desc: n.desc, leaderRealm: n.leaderRealm, initialPower: n.initialPower,
powerGrowth: n.powerGrowth, sells: n.sells, buys: n.buys
}))
}
}
// 老版本存档(<0.1.1)缺少新增字段,加载时补齐,避免运行期 undefined 崩溃
if (!state.finance) state.finance = { accum: 0 }
if (!state.yearStats) state.yearStats = { births: 0, deaths: 0 }
if (!state.yearlyReports) state.yearlyReports = []
if (!state.stats) {
state.stats = {
repPeak: state.family?.reputation ?? 0,
popPeak: Object.values(state.members).filter((c) => c.alive).length,
maxRealmIdx: peakRealmIndex(state.members ?? {}),
techniqueGrand: grandTechniqueCount(state.members ?? {}),
tourneyHistory: [],
feishengCount: 0
}
}
if (typeof state.totalTicks !== 'number') state.totalTicks = 0
if (typeof state.seq !== 'number') state.seq = 10
if (!state.battles) state.battles = []
if (!state.eventQueue) state.eventQueue = []
if (!state.completedEvents) state.completedEvents = []
if (!state.missions) state.missions = []
if (!state.flags) state.flags = {}
if (!state.npcFamilies) state.npcFamilies = {}
if (!state.family.flag) state.family.flag = {}
if (!state.family.inventory) state.family.inventory = {}
if (!state.family.buildings) state.family.buildings = {}
if (!state.family.techniques) state.family.techniques = []
if (!state.family.missionIds) state.family.missionIds = []
if (!state.stats.tourneyHistory) state.stats.tourneyHistory = []
if (state.stats.tourneyBest === undefined) state.stats.tourneyBest = undefined
if (state.worldSim) {
state.worldSim.marketPool = state.worldSim.marketPool ?? {}
state.worldSim.npcDyn = state.worldSim.npcDyn ?? {}
state.worldSim.secretQi = state.worldSim.secretQi ?? {}
state.worldSim.newsFeed = state.worldSim.newsFeed ?? []
if (typeof state.worldSim.tide !== 'number') state.worldSim.tide = 0.5
if (typeof state.worldSim.warFatigue !== 'number') state.worldSim.warFatigue = 0
if (!state.worldSim.shortage) state.worldSim.shortage = {}
if (!state.worldSim.shortageWarned) state.worldSim.shortageWarned = {}
if (!state.worldSim.wonders) state.worldSim.wonders = {}
if (typeof state.worldSim.tideTicks !== 'number') state.worldSim.tideTicks = 0
if (typeof state.worldSim.npcSuccessions !== 'number') state.worldSim.npcSuccessions = 0
if (typeof state.worldSim.calamityLeft !== 'number') state.worldSim.calamityLeft = 0
// 0.1.35 收敛:初始字段职责归 initSim(WorldSim.ts),此处仅防脏(异常落盘/测试注入)
// 防护一:era 非法值兜底(语义防护——主产物 initSim 造)
if (state.worldSim.era !== 'shengshi' && state.worldSim.era !== 'pingshi' && state.worldSim.era !== 'luanshi' && state.worldSim.era !== 'mofa') {
state.worldSim.era = 'pingshi'
state.worldSim.eraStartYear = 1
} else if (typeof state.worldSim.eraStartYear !== 'number') {
state.worldSim.eraStartYear = state.year ?? 1
}
if (typeof state.worldSim.overfluxYear !== 'number') state.worldSim.overfluxYear = -99
if (!Array.isArray(state.worldSim.worldAnnals)) state.worldSim.worldAnnals = []
if (state.worldSim.distress && typeof state.worldSim.distress !== 'object') state.worldSim.distress = undefined
for (const dyn of Object.values(state.worldSim.npcDyn)) {
if (dyn && typeof (dyn as { prosperity?: unknown }).prosperity !== 'number') {
(dyn as { prosperity: number }).prosperity = 50
}
}
}
if (state.family.headId && !state.members[state.family.headId]) {
const firstAlive = Object.values(state.members).find((c) => c.alive)
@@ -23,7 +23,6 @@ export function createWorldState(opts: NewGameOptions): GameState {
const worldGen = generateWorld(opts.seed)
const state: GameState = {
schemaVersion: 1,
seed: opts.seed,
worldGen: {
relations: worldGen.relations,
@@ -95,6 +95,9 @@ export function newCharacter(
state: 'idle',
health: 100,
alive: true,
techniqueRank: 0,
techniqueProgress: 0,
tribBoost: 0,
isHead: opts.isHead,
isFounder: opts.isFounder
}
@@ -81,7 +81,11 @@ export function makeWorldSimState(): WorldSimState {
era: 'pingshi',
eraStartYear: 1,
overfluxYear: -99,
worldAnnals: []
worldAnnals: [],
warFatigue: 0,
shortage: {},
shortageWarned: {},
wonders: {}
}
}
+13 -56
View File
@@ -1,75 +1,32 @@
import { GameState } from '../types/domain'
import { normalizeGameState } from '../engine/runtime/World'
export const CURRENT_SCHEMA = 2
export const APP_ID = 'cotyc'
export class MigrationError extends Error {
code: 'TOO_NEW' | 'UNKNOWN_VERSION'
version: number
constructor(code: 'TOO_NEW' | 'UNKNOWN_VERSION', version: number, message: string) {
super(message)
this.code = code
this.version = version
}
}
/**
* 迁移链:obj.from 升到 obj.to 逐级执行(任何一步失败即中断抛出,原状态不被写入)
* v1(≤0.1.8)→ v20.1.9):补 stats/finance/annals 字段并打上 schema 版本戳
* 0.1.35 荡涤:迁移链(CURRENT_SCHEMA/MIGRATIONS/migrateIfNeeded)整体铲除
* 未发布游戏不做版本迁移——读档只需结构粗检 + World 构造器 normalize 兜底/防护
*/
const MIGRATIONS: Array<{ from: number; to: number; fn: (s: GameState) => GameState }> = [
{
from: 1,
to: 2,
fn: (s) => {
normalizeGameState(s)
s.schemaVersion = 2
return s
}
export function validateLoadedState(state: unknown): { ok: true; state: GameState } | { ok: false; message: string } {
const s = state as GameState | null
if (!s || typeof s !== 'object') return { ok: false, message: '存档内容为空或损坏。' }
if (!s.family || !s.members || typeof s.seed !== 'string') {
return { ok: false, message: '存档结构缺失(family/members/seed)——本作未发布变更频繁,旧档不可读。' }
}
]
export function migrateIfNeeded(state: GameState): { ok: true; state: GameState } | { ok: false; error: MigrationError } {
const version = typeof state.schemaVersion === 'number' ? state.schemaVersion : 1
if (version > CURRENT_SCHEMA) {
return {
ok: false,
error: new MigrationError('TOO_NEW', version, `存档版本 ${version} 高于当前游戏支持(${CURRENT_SCHEMA}),请升级游戏。`)
}
}
let cur = state
let v = version
let guard = 0
while (v < CURRENT_SCHEMA && guard < 20) {
const step = MIGRATIONS.find((m) => m.from === v)
if (!step) {
return { ok: false, error: new MigrationError('UNKNOWN_VERSION', v, `未知存档版本 ${v},无法迁移。`) }
}
try {
cur = step.fn(cur)
} catch (e) {
return { ok: false, error: new MigrationError('UNKNOWN_VERSION', v, `迁移失败:${String(e)}`) }
}
v = step.to
guard++
}
return { ok: true, state: cur }
return { ok: true, state: s }
}
export function exportEnvelope(state: GameState): string {
return JSON.stringify({ app: APP_ID, schemaVersion: state.schemaVersion ?? CURRENT_SCHEMA, payload: state })
return JSON.stringify({ app: APP_ID, payload: state })
}
export function parseImportEnvelope(text: string): { ok: true; state: GameState } | { ok: false; error: string } {
try {
const raw = JSON.parse(text) as { app?: string; schemaVersion?: number; payload?: GameState; state?: GameState }
const raw = JSON.parse(text) as { app?: string; payload?: GameState; state?: GameState }
if (raw.app !== undefined && raw.app !== APP_ID) return { ok: false, error: '非本作存档(app 标识不符)。' }
const state = raw.payload ?? raw.state
if (!state || typeof state !== 'object') return { ok: false, error: '存档内容为空或损坏。' }
const migrated = migrateIfNeeded(state as GameState)
if (migrated.ok) return { ok: true, state: migrated.state }
return { ok: false, error: migrated.error.message }
const checked = validateLoadedState(state)
if (checked.ok) return { ok: true, state: checked.state }
return { ok: false, error: checked.message }
} catch (e) {
return { ok: false, error: `解析失败:${String(e)}` }
}
+8 -33
View File
@@ -1,5 +1,7 @@
import { GameState, SaveMeta, SnapshotMeta, Id } from '../types/domain'
import { CURRENT_SCHEMA, APP_ID, exportEnvelope, parseImportEnvelope, migrateIfNeeded, MigrationError } from './migrate'
import { validateLoadedState, exportEnvelope, parseImportEnvelope } from './migrate'
const GAME_VERSION = '0.1.35'
const DB_PREFIX = 'cotyc-save-'
let seqCounter = 0
@@ -36,12 +38,10 @@ export class SaveSlot {
const driver = this.driver
await driver.exec(`CREATE TABLE IF NOT EXISTS meta (key string PRIMARY KEY, value string)`)
await driver.exec(`CREATE TABLE IF NOT EXISTS snapshot (id string PRIMARY KEY, year number, month number, savedAt string, label string, data string)`)
await driver.exec(`CREATE TABLE IF NOT EXISTS chronicle (id string PRIMARY KEY, year number, month number, category string, important number, memberId string, text string)`)
}
async saveState(state: GameState, label: string): Promise<string> {
const id = `${state.year}.${state.month}.${Date.now().toString(36)}-${(seqCounter++ % 1296).toString(36)}`
state.schemaVersion = CURRENT_SCHEMA
const json = JSON.stringify(state)
await this.driver.run(
`INSERT INTO snapshot (id, year, month, savedAt, label, data) VALUES (?, ?, ?, ?, ?, ?)`,
@@ -60,29 +60,6 @@ export class SaveSlot {
return id
}
/** P1-T chronicle 表已废(write-only 无读取者;史书数据随快照 data 保存)——保持 no-op 接口 */
async saveChronicle(_chronicle: GameState['chronicle']): Promise<void> {
return
/* eslint-disable no-unreachable */
try {
// legacy0.1.29 前占位;不再维护)
const rows = await this.driver.all<{ id: string }>(`SELECT id FROM chronicle`)
const existing = new Set(rows.map((r) => r.id))
for (const e of _chronicle) {
if (existing.has(e.id)) continue
try {
await this.driver.run(
`INSERT INTO chronicle (id, year, month, category, important, memberId, text) VALUES (?, ?, ?, ?, ?, ?, ?)`,
[e.id, e.year, e.month, e.category, e.important ? 1 : 0, e.memberId ?? null, e.text]
)
} catch {
// row-level best-effort
}
}
} catch {
// 冗余表失败吞掉(主挡位用 snapshot 兜底)
}
}
async listSnapshots(): Promise<SnapshotMeta[]> {
const rows = await this.driver.all<SnapshotMeta & { data?: string }>(`SELECT id, year, month, savedAt, label FROM snapshot ORDER BY year DESC, month DESC`)
return rows.map((r) => ({ id: r.id, year: r.year, month: r.month, savedAt: r.savedAt, label: r.label }))
@@ -97,8 +74,8 @@ export class SaveSlot {
)
if (!rows || rows.length === 0) return null
const state = JSON.parse(rows[0]!.data) as GameState
const r = migrateIfNeeded(state)
if (!r.ok) throw new MigrationError(r.error.code, r.error.version, r.error.message)
const r = validateLoadedState(state)
if (!r.ok) throw new Error(r.message)
return r.state
}
@@ -126,8 +103,8 @@ export class SaveSlot {
const state = await this.loadState()
if (!state) throw new Error('无可导出存档')
const meta = await this.getMeta()
const envelope = JSON.parse(exportEnvelope(state)) as { schemaVersion: number; payload: GameState }
return JSON.stringify({ app: APP_ID, schemaVersion: envelope.schemaVersion, meta, state: envelope.payload })
const envelope = JSON.parse(exportEnvelope(state)) as { payload: GameState }
return JSON.stringify({ app: 'cotyc', meta, payload: envelope.payload })
}
async importAll(data: string): Promise<boolean> {
@@ -135,10 +112,8 @@ export class SaveSlot {
const parsed = parseImportEnvelope(data)
if (!parsed.ok) return false
const state = parsed.state
state.schemaVersion = CURRENT_SCHEMA
await this.driver.exec(`DELETE FROM snapshot`)
await this.driver.exec(`DELETE FROM meta`)
await this.driver.exec(`DELETE FROM chronicle`)
await this.saveState(state, 'import')
const aliveCount = Object.values(state.members).filter((c) => c.alive).length
await this.setMeta(buildSimpleMeta(state, this.slot, aliveCount))
@@ -167,7 +142,7 @@ export function buildSimpleMeta(state: GameState, slot: number, memberCount: num
members: memberCount,
reputation: state.family.reputation,
updatedAt: new Date().toISOString(),
version: state.schemaVersion
version: GAME_VERSION
}
}
+5 -6
View File
@@ -42,8 +42,8 @@ export interface Character {
fortune: number
traits: string[]
techniqueId?: string
techniqueRank?: number
techniqueProgress?: number
techniqueRank: number // 0.1.35 必填(pcgen 必写——归一化兼容批已删)
techniqueProgress: number
post?: string
equipment?: string
state: CharState
@@ -58,7 +58,7 @@ export interface Character {
tribDelayYear?: number
tribPendingMonths?: number
/** 渡劫药力加成(破境丹等),渡劫结算后清零 */
tribBoost?: number
tribBoost: number
}
export interface FamilyState {
@@ -164,7 +164,6 @@ export interface YearlyReport {
}
export interface GameState {
schemaVersion: number
seed: string
rng: RngState
year: number
@@ -190,7 +189,7 @@ export interface GameState {
/** 读档时缺失的插件(工厂未注册/版本不符)——UI 提示:MOD 文件可能需重新放入 mods 目录 */
pluginsMissing?: string[]
/** 天下轮廓(种子生成器产物:关系网/开局 era/市场偏移/世仇盟约) */
worldGen?: {
worldGen: {
relations: Record<string, Record<string, number>>
eras: 'shengshi' | 'pingshi' | 'luanshi'
marketOffset: Record<string, number>
@@ -258,7 +257,7 @@ export interface SaveMeta {
members: number
reputation: number
updatedAt: string
version: number
version: string // 0.1.35 记录游戏版本字符串(非 schema 戳)
}
export interface SnapshotMeta {
-1
View File
@@ -576,7 +576,6 @@ const Stash = {
await file.open()
st.world.syncRng()
await file.saveState(st.world.state, label)
await file.saveChronicle(st.world.state.chronicle)
const meta = metaFromState(st.world.state, st.slot)
await file.setMeta(meta)
await updateSlotMeta(st.slot, meta)
+1 -1
View File
@@ -14,7 +14,7 @@ export function metaFromState(state: GameState, slot: number): SaveMeta {
members: alive,
reputation: state.family.reputation,
updatedAt: new Date().toISOString(),
version: state.schemaVersion
version: '0.1.35'
}
}
-12
View File
@@ -10,18 +10,6 @@ import { GameState } from '../src/renderer/game/types/domain'
import { resetSaveBus, attachLogSink } from './world.helpers'
describe('0.1.3 audit regression', () => {
it('loads a 0.1.0-era save (missing finance/yearStats fields)', () => {
const w = World.create({ seed: 'legacy', surname: '崔', familyName: '崔家', motto: 'm', difficulty: 'normal' })
const legacy = JSON.parse(JSON.stringify(w.state)) as GameState
delete (legacy as Record<string, unknown>)['finance']
delete (legacy as Record<string, unknown>)['yearStats']
delete (legacy as Record<string, unknown>)['yearlyReports']
const w2 = new World(legacy)
for (let i = 0; i < 40; i++) w2.advanceMonth()
expect(w2.state.yearlyReports.length).toBeGreaterThan(0)
expect(Number.isNaN(w2.state.family.stones)).toBe(false)
})
it('marries a npc family once only (allied lock)', () => {
const w = World.create({ seed: 'hy', surname: '卫', familyName: '卫家', motto: 'm', difficulty: 'normal' })
const npcId = Object.keys(w.state.npcFamilies)[0]!
-30
View File
@@ -73,36 +73,6 @@ describe('志向矩阵', () => {
})
})
describe('normalizeGameState 全残缺矩阵', () => {
it.each([
['finance', 'yearStats', 'yearlyReports', 'stats'].map((k) => [k] as const)
].flat())('%s 缺失补齐', (key) => {
const w = baseWorld('norm-' + key)
const state = JSON.parse(JSON.stringify(w.state))
delete (state as Record<string, unknown>)[key]
const fixed = normalizeGameState(state)
expect((fixed as Record<string, unknown>)[key]).toBeTruthy()
})
it('无 headId 老档补齐首个存活着', () => {
const w = baseWorld('norm-head')
const state = JSON.parse(JSON.stringify(w.state))
state.family.headId = 'x-not-exist'
const fixed = normalizeGameState(state)
expect(fixed.family.headId).toBe('x1')
})
it('缺 npcFamilies 补齐空表(不崩外交)', () => {
const w = baseWorld('norm-npc')
const state = JSON.parse(JSON.stringify(w.state))
delete state.npcFamilies
const fixed = normalizeGameState(state)
const w2 = new World(fixed)
for (let i = 0; i < 3; i++) w2.advanceMonth() // 不抛
expect(true).toBe(true)
})
})
describe('大比寄读渡劫动态事件矩阵', () => {
it.each([
['ev-tournament-10', '称病不出'],
+1 -48
View File
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest'
import { World } from '../src/renderer/game/engine/runtime/World'
import { buildReport, verdictLine } from '../src/renderer/game/engine/narrative/legacyreport'
import { migrateIfNeeded, exportEnvelope, parseImportEnvelope, CURRENT_SCHEMA, APP_ID } from '../src/renderer/game/storage/migrate'
import { exportEnvelope, parseImportEnvelope, validateLoadedState } from '../src/renderer/game/storage/migrate'
import { FORMATIONS, formationById, FormationId } from '../src/renderer/game/data/formations'
import { resolveEncounter } from '../src/renderer/game/engine/runtime/Systems/combat'
import { ENEMIES } from '../src/renderer/game/data/secrets'
@@ -56,53 +56,6 @@ describe('legacyreport 百年报告', () => {
})
})
describe('存档迁移管线', () => {
it('CURRENT_SCHEMA=2 且 v1→v2 迁移补字段', () => {
expect(CURRENT_SCHEMA).toBe(2)
const w = baseWorld('mig-a')
const asV1 = JSON.parse(JSON.stringify(w.state))
asV1.schemaVersion = 1
delete asV1.stats
const res = migrateIfNeeded(asV1)
expect(res.ok).toBe(true)
const s = (res as never as { state: { stats: unknown; schemaVersion: number } }).state
expect(s.stats).toBeTruthy()
expect(s.schemaVersion).toBe(2)
})
it('未来版本拒绝加载(防误用)', () => {
const w = baseWorld('mig-b')
const s = JSON.parse(JSON.stringify(w.state))
s.schemaVersion = 99
const res = migrateIfNeeded(s)
expect(res.ok).toBe(false)
expect((res as never as { error: { code: string } }).error.code).toBe('TOO_NEW')
})
it('导出信封带 appId 与版本;导入旧版可迁移', () => {
const w = baseWorld('mig-c')
w.state.schemaVersion = CURRENT_SCHEMA
const env = JSON.parse(exportEnvelope(w.state))
expect(env.app).toBe(APP_ID)
expect(env.schemaVersion).toBe(2)
const parsed = parseImportEnvelope(JSON.stringify(env))
expect(parsed.ok).toBe(true)
})
it('异 app 拒绝导入', () => {
const w = baseWorld('mig-d')
const env = JSON.parse(exportEnvelope(w.state))
env.app = 'other-game'
const parsed = parseImportEnvelope(JSON.stringify(env))
expect(parsed.ok).toBe(false)
})
it('损坏 JSON 拒绝导入', () => {
const res = parseImportEnvelope('not-json-wrapper')
expect(res.ok).toBe(false)
})
})
describe('战斗阵型', () => {
it.each(Object.keys(FORMATIONS).map((k) => [k as FormationId] as const))('%s 阵型定义合法', (id) => {
const f = FORMATIONS[id]
+1 -1
View File
@@ -67,7 +67,7 @@ describe('0.1.30 矩阵:月末大圈(600 例级)', () => {
expect(w.state.seq).toBeGreaterThan(0)
expect(Array.isArray(w.state.completedEvents)).toBe(true)
expect(Array.isArray(w.state.missions)).toBe(true)
expect(typeof w.state.schemaVersion).toBe('number')
expect(typeof w.state.seed).toBe('string') // 0.1.35 版本戳移除——seed 为世界锚
expect(typeof w.state.seed).toBe('string')
expect(Array.isArray(w.state.battles)).toBe(true)
})
-12
View File
@@ -4,18 +4,6 @@ import { normalizeGameState } from '../src/renderer/game/engine/runtime/World'
import { WorldSimState } from '../src/renderer/game/engine/sim/worldsim-data'
describe('0.1.19 八方风雨', () => {
it('兜底:空 worldSim 的旧档 normalize 后 tideTicks 有值(P0 修复)', () => {
const w = World.create({ seed: 'p0-1', surname: '甯', familyName: '甯家', motto: 'm', difficulty: 'normal' })
const state = w.state as never
const fixed = normalizeGameState({
...(state as Record<string, unknown>),
worldSim: { marketPool: {}, npcDyn: {}, secretQi: {}, newsFeed: [] }
} as never) as { worldSim: { tideTicks: number; era: string; calamityLeft: number } }
expect(fixed.worldSim.tideTicks).toBe(0)
expect(fixed.worldSim.era).toBe('pingshi')
expect(fixed.worldSim.calamityLeft).toBe(0)
})
it('stance:四姿态值域 + 年首(约8年)可再评估', () => {
const w = World.create({ seed: 'st-1', surname: '冀', familyName: '冀家', motto: 'm', difficulty: 'normal' })
const seen = new Set<string>()