219 lines
13 KiB
GDScript
219 lines
13 KiB
GDScript
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||
## 存档断点组装与回放唯一承载(AdventureRunner 拆件——单一变化原因=断点快照键面与回放策略):
|
||
## 断点快照拼盘、回放回填、读侧策略分发;BREAKPOINT_FIELDS 27 键登记单源随件(键名/策略标签/键序冻结)。
|
||
## 构造=引用束字典注入(db/rng/主角账三件/curio/stop/bank/loadout——件禁反向持宿主);
|
||
## 快照宿主输入(state/level_index/master_seed/runner/adventure_counters)逐调传参;
|
||
## resume_adventure 留宿主(壳+宿主侧回填语句——所有权缝分家,语句序逐步保形)。
|
||
class_name BreakpointStore
|
||
extends RefCounted
|
||
|
||
# ---- 断点快照(字段登记单源:写侧拼装与读侧回退策略同登记一表)----
|
||
# 键序即快照键序(27 顶层键——写侧/读侧/键集断言三面同源)。读侧回退策略词表:
|
||
# plain 无特殊回退面(缺键取代码内缺省)/clamp 数值夹 ≥0/keep_on_null 缺键或类型不符保持既有态
|
||
# dual_merge 双向合并(bonus_taken)/params_fallback 缺键回退参数面(attune_refresh_y)
|
||
# token_migration 旧版候选形态迁移(bonus_pending.candidates)
|
||
# 注入缺省 null = 整面静默(模拟/冒烟零扰动)。
|
||
const BREAKPOINT_FIELDS := {
|
||
"level_index": {"strategy": "plain"},
|
||
"node_id": {"strategy": "plain"},
|
||
"hero_hp": {"strategy": "clamp"},
|
||
"bonus": {"strategy": "clamp"},
|
||
"gp": {"strategy": "plain"},
|
||
"shop_purchased": {"strategy": "plain"},
|
||
"shop_shelf": {"strategy": "plain"},
|
||
"shop_visit_buys": {"strategy": "plain"},
|
||
"attunement_charges": {"strategy": "keep_on_null"},
|
||
"graph_gen_v2": {"strategy": "plain"},
|
||
"master_seed": {"strategy": "plain"},
|
||
"rng_counter": {"strategy": "plain"},
|
||
"keystone": {"strategy": "plain"},
|
||
"visited_ids": {"strategy": "plain"},
|
||
"aux_items": {"strategy": "plain"},
|
||
"bonus_taken": {"strategy": "dual_merge"},
|
||
"bonus_deck": {"strategy": "keep_on_null"},
|
||
"relics": {"strategy": "plain"},
|
||
"bonus_pending": {"strategy": "token_migration"},
|
||
"feat_ticket_pity": {"strategy": "plain"},
|
||
"feat_ticket_sold": {"strategy": "plain"},
|
||
"adventure_counters": {"strategy": "keep_on_null"},
|
||
"vision_counter_used": {"strategy": "clamp"},
|
||
"refresh_stock": {"strategy": "clamp"},
|
||
"attune_refresh_y": {"strategy": "params_fallback"},
|
||
"bonus_result": {"strategy": "plain"},
|
||
"keystone_drops": {"strategy": "plain"},
|
||
"gain_journal": {"strategy": "plain"}, # 获得日记(拆件批 T4 扩列——唯一声明的断点扩展;旧档缺键=汇总直载跳过比对)
|
||
}
|
||
|
||
# 引用束(宿主构造注入——成员名与宿主侧原名一致,迁移函数体逐字)
|
||
var db: ConfigDb
|
||
var rng: RngServiceCore
|
||
var _stats: HeroStats
|
||
var _wallet: GpWallet
|
||
var _journal: GainJournal
|
||
var _curio: CurioDeck = null # 令牌池件(池余库/获取计数随断点)
|
||
var _stop: CurioStop = null # 停点件(停点态/凭证计数片段)
|
||
var _refresh_bank: RefreshBank = null # 刷新账件(刷新存量/Y 池片段)
|
||
var _loadout: AttuneLoadout = null # 同调配装件(充能行集读侧)
|
||
# 断点域六成员(自宿主收编——宿主其余读写点经本件)
|
||
var _save_manager = null # 断点承载注入(null = 不写断点;模拟/冒烟缺省)
|
||
var _resume_bp := {} # 续玩快照暂存(resume_adventure → _start_current_level 单跳传递)
|
||
var _pending_next_level := false # 大奖励已兑付、等 continue_to_next_level 确认
|
||
var _last_level_payload := {} # 最近一次关卡结束载荷(失败结算的败退节点明细来源——数值验证取数面)
|
||
var _last_treasure_cleared := 0 # additive(最近一次关卡末清空计数,summary 取数面)
|
||
var _loadout_seeded := false # 同调行已种标记(每冒险一次——首关 bind 时点投行;行跨关存活后续关零动作)
|
||
|
||
|
||
func _init(refs: Dictionary) -> void:
|
||
db = refs["db"]
|
||
rng = refs["rng"]
|
||
_stats = refs["stats"]
|
||
_wallet = refs["wallet"]
|
||
_journal = refs["journal"]
|
||
_curio = refs["curio"]
|
||
_stop = refs["stop"]
|
||
_refresh_bank = refs["bank"]
|
||
_loadout = refs["loadout"]
|
||
|
||
|
||
# 断点快照拼盘落盘(宿主 _snapshot_breakpoint 委托目标——宿主输入逐调传参)
|
||
func snapshot(state: String, level_index: int, master_seed: int, runner: LevelRunner, counters: Dictionary[Variant, Variant]) -> void:
|
||
if _save_manager == null or state != "running":
|
||
return
|
||
var parts: Dictionary[Variant, Variant] = _bp_parts(level_index, master_seed, runner, counters)
|
||
var bp: Dictionary[Variant, Variant] = {}
|
||
for key in BREAKPOINT_FIELDS.keys():
|
||
bp[key] = parts[key]
|
||
_save_manager.set_breakpoint(bp)
|
||
_save_manager.write_to_disk()
|
||
|
||
|
||
# 断点写侧取值(键集以 BREAKPOINT_FIELDS 为准——未登记键即取值越界,fail-fast)
|
||
func _bp_parts(level_index: int, master_seed: int, runner: LevelRunner, counters: Dictionary[Variant, Variant]) -> Dictionary[Variant, Variant]:
|
||
var stats_frag: Dictionary[Variant, Variant] = _stats.breakpoint_fragment()
|
||
var wallet_frag: Dictionary[Variant, Variant] = _wallet.breakpoint_fragment()
|
||
var journal_frag: Dictionary[Variant, Variant] = _journal.breakpoint_fragment()
|
||
var bank_frag: Dictionary[Variant, Variant] = _refresh_bank.breakpoint_fragment()
|
||
var stop_frag: Dictionary[Variant, Variant] = _stop.breakpoint_fragment()
|
||
var level_frag: Dictionary[Variant, Variant] = runner.breakpoint_fragment() if runner != null else {
|
||
"node_id": -1, "visited_ids": [] as Array, "aux_items": [] as Array,
|
||
"shop_shelf": [] as Array, "shop_visit_buys": 0} # 三选一停点续玩分支 runner 为 null——节点域按缺省落档
|
||
# 该两态的续玩只消费 bonus_* 附随件(选择停点/已兑付停点快照)
|
||
var parts: Dictionary[Variant, Variant] = {
|
||
"level_index": level_index,
|
||
"node_id": int(level_frag.get("node_id", -1)),
|
||
"hero_hp": int(stats_frag.get("hero_hp", 0)),
|
||
"bonus": journal_frag.get("bonus", {}),
|
||
"gp": int(wallet_frag.get("gp", 0)),
|
||
"shop_purchased": journal_frag.get("shop_purchased", {}),
|
||
"shop_shelf": level_frag.get("shop_shelf", []), # 货架入断点(离开店/非店停点 = 空架合法态)
|
||
"shop_visit_buys": int(level_frag.get("shop_visit_buys", 0)), # 本次进店已购次数(A 链定价输入)
|
||
"attunement_charges": (_loadout.breakpoint_fragment(runner.treasure_inventory) if runner != null and runner.treasure_inventory != null else {"attunement_charges": {}}).get("attunement_charges", {}), # 同调主动件充能余量随断点(行派生——save v4 域键名策略不变)
|
||
"graph_gen_v2": 1, # 生成图换代代差标记(缺键 = 换代前旧断点 → resume 显式拒绝)
|
||
"master_seed": master_seed,
|
||
"rng_counter": rng.counter(),
|
||
"keystone": int(journal_frag.get("keystone", 0)),
|
||
"visited_ids": level_frag.get("visited_ids", []),
|
||
"aux_items": level_frag.get("aux_items", []),
|
||
"bonus_taken": _curio.taken.duplicate(true), # 三选一件获取次数(kind 维度派生账本——断点附随件与读数承载)
|
||
"bonus_deck": _curio.deck.duplicate(true), # 令牌池余库(TP0 v2.2——池消耗即上限,续玩余库逐枚还原)
|
||
"relics": stats_frag.get("relics", []), # 特性持有集(局内随机渠道获得态续玩防丢失)
|
||
"bonus_pending": stop_frag.get("bonus_pending", {}), # 三选一停点态(空 = 无停点——选择停点可中断契约)
|
||
"feat_ticket_pity": int(stop_frag.get("feat_ticket_pity", 0)), # T5 专长凭证保底计数(扩列向后兼容——旧档缺键缺省 0)
|
||
"feat_ticket_sold": int(stop_frag.get("feat_ticket_sold", 0)), # T5 专长凭证限购计数(同上)
|
||
"adventure_counters": counters.duplicate(true), # adventure 域计数器随断点(续玩跨战账不丢——桶归宿主字典)
|
||
"vision_counter_used": int(journal_frag.get("vision_counter_used", 0)), # 真视使用计数随断点(续玩跨战账不丢)
|
||
"refresh_stock": int(bank_frag.get("refresh_stock", 0)), # 刷新存量账随断点
|
||
"attune_refresh_y": int(bank_frag.get("attune_refresh_y", 0)), # Y 冒险周期池余量随断点(扩列向后兼容——旧档缺键回退参数面)
|
||
"bonus_result": stop_frag.get("bonus_result", {}), # 已兑付待推进态(空 = 无——中断续玩不重兑付)
|
||
"keystone_drops": journal_frag.get("keystone_drops", []), # 逐杀掉落明细(摘要归因续接)
|
||
"gain_journal": journal_frag.get("gain_journal", {}), # 获得日记(唯一声明的断点扩列——日记为现行事实源)
|
||
}
|
||
# 拼盘完整性自断言(fail-fast——片段并集与登记键集不符即中止落盘)
|
||
var stray: Array[Variant] = []
|
||
for key in parts.keys():
|
||
if not BREAKPOINT_FIELDS.has(key):
|
||
stray.append(key)
|
||
if not stray.is_empty():
|
||
push_error("[冒险结构] 断点拼盘含未登记键: %s(fail-fast——片段与登记单源失同步)" % str(stray))
|
||
return parts
|
||
|
||
|
||
# 续玩账面回填(生命/特性/钱包/加算区/已购档/钥石/逐杀明细/真视——主角账三件片段各还原各键;
|
||
# bonus 子键 clamp 类夹 ≥0;日记在档=推导比对载入、缺键=旧档汇总直载)
|
||
#(宿主侧账面语句——summary/levels_total/level_index/_pending_next_level——按语句序留宿主壳)
|
||
func restore_ledger(bp: Dictionary, params: Dictionary) -> void:
|
||
_stats.restore_fragment(bp, params)
|
||
_wallet.restore_fragment(bp, params)
|
||
_journal.restore_fragment(bp, params)
|
||
|
||
|
||
# 续玩三选一态回填(池与件计数归件、停点态归宿主;dual_merge/keep_on_null 策略见 BREAKPOINT_FIELDS)
|
||
#(前置清理 _reset_curio_state 留宿主——桶归宿主字典;本函数承接其后的件侧还原语句,序逐步保形)
|
||
func restore_curio_state(bp: Dictionary) -> void:
|
||
var bp_taken: Dictionary[Variant, Variant] = bp.get("bonus_taken", {}) as Dictionary
|
||
for id in _curio.taken.keys():
|
||
_curio.taken[id] = maxi(0, int(bp_taken.get(id, 0)))
|
||
for id in bp_taken.keys():
|
||
_curio.taken[String(id)] = maxi(0, int(bp_taken.get(id, 0))) # 身份维键(relic:<feat_id>)随快照还原
|
||
# 令牌池余库还原(TP0 v2.2——快照缺键 = 旧版断点,保持满库重建态:旧档只读一版周期的防御口径;
|
||
# relic 令牌 feat_id 随还原)
|
||
var bp_deck = _bp_read("bonus_deck", bp)
|
||
if bp_deck is Array:
|
||
var restored: Array[Variant] = []
|
||
for t in (bp_deck as Array):
|
||
var tok: Dictionary[Variant, Variant] = t as Dictionary
|
||
var rtok := {"kind": String(tok.get("kind", "")), "tier": String(tok.get("tier", ""))}
|
||
if String(rtok["kind"]) == "relic":
|
||
rtok["feat_id"] = String(tok.get("feat_id", "")) # relic 令牌 feat_id 随还原
|
||
restored.append(rtok)
|
||
_curio.deck = restored
|
||
# 特性持有集还原(缺键 = 旧版断点防御空集——主角数值账字段单点)
|
||
_stats.feats = []
|
||
for r in (bp.get("relics", []) as Array):
|
||
_stats.feats.append(String(r))
|
||
|
||
|
||
# 续玩参数面——数值账规范化与越界钳制(件侧;宿主侧四重绑定与预算注入按语句序夹在两件侧调用之间)
|
||
func apply_resume_params(params: Dictionary) -> void:
|
||
_stats.rebase_override(params.get("hero_override", {})) # 续玩参数面同 start_adventure(规范化归数值账)
|
||
_stats.attack_type = String(params.get("attack_type", "")) # 续玩参数面同 start_adventure(账号层选中态随 resume 重注入)
|
||
_stats.armor_type = String(params.get("armor_type", ""))
|
||
# 生命账本越界钳制(T17——spawn 守卫 EC16 免疫):断点可能带出高于生效上限的账本值
|
||
#(如调试浮窗调高期写出的值,实例:hero_hp=28 > 基础 26)——恢复后钳到生效上限,
|
||
# 否则首战 spawn「hero_hp 入参越界」拒收 → 「敌人系统 spawn 被拒」fatal。钳制式单点归
|
||
# 数值账 clamp_hp_to_resume_cap(保守口径不含加算区——与原钳制式逐字同)。
|
||
_stats.clamp_hp_to_resume_cap(int(db.combat.hero.max_hp))
|
||
|
||
|
||
# 停点件/刷新账片段回填(凭证计数;刷新存量/Y 池——Y 缺键回退参数面 params_fallback 类)
|
||
func restore_stop_bank_fragments(bp: Dictionary, params: Dictionary) -> void:
|
||
_stop.restore_fragment(bp, params)
|
||
_refresh_bank.restore_fragment(bp, params)
|
||
|
||
|
||
# 读侧单点(BREAKPOINT_FIELDS 策略标签分发;未登记键 fail-fast 不静默按 plain 兜)。
|
||
# 二次消费面(_start_current_level 经 _resume_bp 取 node_id/visited_ids/aux_items)同走此口。
|
||
func _bp_read(key: String, bp: Dictionary, fallback: Variant = null) -> Variant:
|
||
if not BREAKPOINT_FIELDS.has(key):
|
||
push_error("[冒险结构] 断点字段未登记(读侧): %s" % key)
|
||
var v: Variant = bp.get(key, fallback)
|
||
if _bp_strategy(key) == "keep_on_null" and v != null and not (v is Dictionary or v is Array):
|
||
return null # 类型不符——保持既有态(调用方跳过覆盖)
|
||
return v
|
||
|
||
|
||
# 读侧策略标签(BREAKPOINT_FIELDS 读侧消费面单点)
|
||
func _bp_strategy(key: String) -> String:
|
||
return String((BREAKPOINT_FIELDS.get(key, {}) as Dictionary).get("strategy", "plain"))
|
||
|
||
|
||
# 数值类读侧(clamp 夹 ≥0 / params_fallback 断点缺键回退参数面整额注入 / plain 直取)
|
||
func _bp_read_int(key: String, bp: Dictionary, fallback: int, params: Dictionary = {}) -> int:
|
||
match _bp_strategy(key):
|
||
"clamp":
|
||
return maxi(0, int(bp.get(key, fallback)))
|
||
"params_fallback":
|
||
return maxi(0, int(bp.get(key, int(params.get(key, fallback)))))
|
||
_:
|
||
return int(bp.get(key, fallback))
|