- 百年族史报告:buildReport 纯函数(人口/声望/战力三线、死因榜、突破密度、 大比战绩、代际长度带)+ 春秋原 SVG 折线视图 + 自动措辞结语 - 存档迁移管线 v2:migrate 链(v1→v2)+ 未来版本拒载(TOO_NEW)+ 导出信封 带 appId/schemaVersion + 导入全链路迁移校验;saveState 自动写 CURRENT_SCHEMA - 战斗阵型:锋阵(+10%攻)/雁阵(御5%+溃伤-15%)/蛇阵(溃伤-30%),三处出战接入 (遣队/大比/劫掠)点将面板下拉 + 战报首行标阵 - 冬至岁祷:每年十一月三选(观星+3声望/祈福全族修为/问卜吉凶) - 发布准备:scripts/verify-release.mjs 验收哨兵(npm run verify/verify:package) + README 全面重写(架构/守护/版本沿革);版本号三处归一 - 测试 861→881(报告四例/迁移五链/阵型六则含胜率统计/冬祷四则); 金钟罩三指纹零漂移;typecheck/build 通过
73 lines
2.6 KiB
TypeScript
73 lines
2.6 KiB
TypeScript
import { GameState } from '../types/domain'
|
||
import { normalizeGameState } from '../engine/world'
|
||
|
||
export const CURRENT_SCHEMA = 2
|
||
export const APP_ID = 'cotyc'
|
||
|
||
export interface MigrationError {
|
||
code: 'TOO_NEW' | 'UNKNOWN_VERSION'
|
||
version: number
|
||
message: string
|
||
}
|
||
|
||
/**
|
||
* 迁移链:obj.from 升到 obj.to 逐级执行(任何一步失败即中断抛出,原状态不被写入)。
|
||
* v1(≤0.1.8)→ v2(0.1.9):补 stats/finance/annals 字段并打上 schema 版本戳。
|
||
*/
|
||
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 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: { code: 'TOO_NEW', version, message: `存档版本 ${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: { code: 'UNKNOWN_VERSION', version: v, message: `未知存档版本 ${v},无法迁移。` } }
|
||
}
|
||
try {
|
||
cur = step.fn(cur)
|
||
} catch (e) {
|
||
return { ok: false, error: { code: 'UNKNOWN_VERSION', version: v, message: `迁移失败:${String(e)}` } }
|
||
}
|
||
v = step.to
|
||
guard++
|
||
}
|
||
return { ok: true, state: cur }
|
||
}
|
||
|
||
export function exportEnvelope(state: GameState): string {
|
||
return JSON.stringify({ app: APP_ID, schemaVersion: state.schemaVersion ?? CURRENT_SCHEMA, 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 }
|
||
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 }
|
||
} catch (e) {
|
||
return { ok: false, error: `解析失败:${String(e)}` }
|
||
}
|
||
}
|