【A 经济闭环】 - 修为经济化:练气以上修行者月耗灵草(闭关2普通1),无草速修0.6~1.0衰减+缺草年告警 - 炼丹等级化:bonusOf 表达式引擎(白名单安全求值),craftChance 转正;丹方单源 PILL_RECIPES(聚气L1/凝元L2/破境L3)+ 失败折半退料 - 炼器筑宝:FORGE_RECIPES(法器/灵器/法宝,灵矿+兽核+灵石) - 秘境灵脉:secretQiOf→rollWarbooty 收益门槛(0.5~1.3折算);掉落按秘境 techGrades 过滤 - NPC 实力参战:raid 强度/风险随 npc.power 浮动(0.5~1.8) - 灾年落效:疫病/兽潮一次性袭击 + CALAMITY_FAMILY 月度减产乘子 【C 治理】 - C13 破境丹渡劫修正:大境界时药力→tribBoost 加成(+12%)入渡劫事件,不再跳过三选 - C14 存储防线:loadState 补 migrate;MigrationError(TOO_NEW/未知版本)提示升级 【UI 半批】 - 坊市新增 定制tab(丹方/铸器);ChroniclePanel 手写史书输入条;MemberModal 册立家主钮 - 年报摘要(要闻5条+辞世讣告);EventModal 选项收益预览 - 结盟引擎:setAlliance(relation≥40)/盟友岁差不降/年利15灵石
77 lines
2.7 KiB
TypeScript
77 lines
2.7 KiB
TypeScript
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 版本戳。
|
||
*/
|
||
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: 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 }
|
||
}
|
||
|
||
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)}` }
|
||
}
|
||
}
|