diff --git a/src/renderer/game/engine/GameEngine.ts b/src/renderer/game/engine/GameEngine.ts index e678482..787922f 100644 --- a/src/renderer/game/engine/GameEngine.ts +++ b/src/renderer/game/engine/GameEngine.ts @@ -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) diff --git a/src/renderer/game/engine/runtime/World.ts b/src/renderer/game/engine/runtime/World.ts index a5b3fbd..9afa1d4 100644 --- a/src/renderer/game/engine/runtime/World.ts +++ b/src/renderer/game/engine/runtime/World.ts @@ -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) diff --git a/src/renderer/game/engine/runtime/creation.ts b/src/renderer/game/engine/runtime/creation.ts index aa58646..45bc227 100644 --- a/src/renderer/game/engine/runtime/creation.ts +++ b/src/renderer/game/engine/runtime/creation.ts @@ -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, diff --git a/src/renderer/game/engine/runtime/pcgen.ts b/src/renderer/game/engine/runtime/pcgen.ts index c5e5f21..bcbdc6d 100644 --- a/src/renderer/game/engine/runtime/pcgen.ts +++ b/src/renderer/game/engine/runtime/pcgen.ts @@ -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 } diff --git a/src/renderer/game/engine/sim/worldsim-data.ts b/src/renderer/game/engine/sim/worldsim-data.ts index 1ae05f4..44a091d 100644 --- a/src/renderer/game/engine/sim/worldsim-data.ts +++ b/src/renderer/game/engine/sim/worldsim-data.ts @@ -81,7 +81,11 @@ export function makeWorldSimState(): WorldSimState { era: 'pingshi', eraStartYear: 1, overfluxYear: -99, - worldAnnals: [] + worldAnnals: [], + warFatigue: 0, + shortage: {}, + shortageWarned: {}, + wonders: {} } } diff --git a/src/renderer/game/storage/migrate.ts b/src/renderer/game/storage/migrate.ts index d63ca85..2aebb38 100644 --- a/src/renderer/game/storage/migrate.ts +++ b/src/renderer/game/storage/migrate.ts @@ -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)→ v2(0.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)}` } } diff --git a/src/renderer/game/storage/slots.ts b/src/renderer/game/storage/slots.ts index 5d90b2e..0eac878 100644 --- a/src/renderer/game/storage/slots.ts +++ b/src/renderer/game/storage/slots.ts @@ -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 { 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 { - return - /* eslint-disable no-unreachable */ - try { - // legacy(0.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 { const rows = await this.driver.all(`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 { @@ -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 } } diff --git a/src/renderer/game/types/domain.ts b/src/renderer/game/types/domain.ts index ac7baab..63fd6c8 100644 --- a/src/renderer/game/types/domain.ts +++ b/src/renderer/game/types/domain.ts @@ -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> eras: 'shengshi' | 'pingshi' | 'luanshi' marketOffset: Record @@ -258,7 +257,7 @@ export interface SaveMeta { members: number reputation: number updatedAt: string - version: number + version: string // 0.1.35 记录游戏版本字符串(非 schema 戳) } export interface SnapshotMeta { diff --git a/src/renderer/ui/store.ts b/src/renderer/ui/store.ts index 35583ec..56e09dd 100644 --- a/src/renderer/ui/store.ts +++ b/src/renderer/ui/store.ts @@ -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) diff --git a/src/renderer/ui/storeHelper.ts b/src/renderer/ui/storeHelper.ts index 4681f7d..a634639 100644 --- a/src/renderer/ui/storeHelper.ts +++ b/src/renderer/ui/storeHelper.ts @@ -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' } } diff --git a/tests/audit.test.ts b/tests/audit.test.ts index 793f66d..0a44069 100644 --- a/tests/audit.test.ts +++ b/tests/audit.test.ts @@ -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)['finance'] - delete (legacy as Record)['yearStats'] - delete (legacy as Record)['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]! diff --git a/tests/event-state-matrix.test.ts b/tests/event-state-matrix.test.ts index e31e264..8c5caa2 100644 --- a/tests/event-state-matrix.test.ts +++ b/tests/event-state-matrix.test.ts @@ -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)[key] - const fixed = normalizeGameState(state) - expect((fixed as Record)[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', '称病不出'], diff --git a/tests/feature-0.1.9.test.ts b/tests/feature-0.1.9.test.ts index 2c85324..191dd76 100644 --- a/tests/feature-0.1.9.test.ts +++ b/tests/feature-0.1.9.test.ts @@ -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] diff --git a/tests/matrix-final-circle-0.1.30.test.ts b/tests/matrix-final-circle-0.1.30.test.ts index 84d5e24..ac9068c 100644 --- a/tests/matrix-final-circle-0.1.30.test.ts +++ b/tests/matrix-final-circle-0.1.30.test.ts @@ -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) }) diff --git a/tests/stance-world-0.1.19.test.ts b/tests/stance-world-0.1.19.test.ts index 03db78d..fdb4b61 100644 --- a/tests/stance-world-0.1.19.test.ts +++ b/tests/stance-world-0.1.19.test.ts @@ -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), - 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()