diff --git a/game/core/adventure_runner.gd b/game/core/adventure_runner.gd index ebdbd90d..0dfb6fb3 100644 --- a/game/core/adventure_runner.gd +++ b/game/core/adventure_runner.gd @@ -19,7 +19,7 @@ var state: String: # 外部契约面(只读——外部 return State.keys()[_state].to_lower() var _hero: HeroLedger # 主角实体账(生命/钱包/加算区/类型轴/特性集/钥石/真视——拆件批 T1 建件) var adventure_counters: Dictionary[Variant, Variant] = {} # 冒险桶(效果引擎 adv_ 计数器宿主字典——跨战持久) -var run_ledger := RunLedger.new() # 充能账瘦壳(attune_charges——整类退役归姊妹副卡) + var level_index := 0 # 当前关卡序号(1 起) var levels_total := 0 # 序列总关数(读 level 表行数) var master_seed := 0 # 本局主种子(摘要第五件事) @@ -32,9 +32,12 @@ var _treasure_unlocked := {} # aux 解锁门控集(params.tr var _attunement: Array[Variant] = [] # 同调装配集(params.attunement 接收面,start_level 外下传 LevelRunner) var _save_manager = null # 断点承载注入(null = 不写断点;模拟/冒烟缺省) var _resume_bp := {} # 续玩快照暂存(resume_adventure → _start_current_level 单跳传递) +var _loadout_seeded := false # 同调行已种标记(每冒险一次——首关 bind 时点投行;行跨关存活后续关零动作) # ---- 三选一停点(编排迁 CurioStop 件——停点三态/刷新三源/凭证出箱/信号面)---- var _curio: CurioDeck = null # 令牌池件(_init 建;纯池——多重集/抽签/cap/凭证判定唯一宿主) var _stop: CurioStop = null # 停点件(_init 建;每冒险一次——关卡换实例不换件) +var _loadout: AttuneLoadout = null # 同调配装件(_init 建;充能单源化——物化行唯一事实源) +var adventure_inventory: TreasureItems # 冒险级秘宝库存(单源化——同调未用行跨关存活的承载面;关卡域语义归 level 使用面) var _tree_rules: Array[Variant] = [] # 树 rule 键集(Fork 激活侧 effects 透传;下传 enc.tree_rules 消费面) var _shop_mode := "legacy" # 商店模式("legacy"=固定三件加定价〔off 对照承载,缺省零扰动〕/"pool"=共享池抽 3 买 1 重抽;真实入口 main.gd 显式投 pool) @@ -45,6 +48,8 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void: _hero = HeroLedger.new(db, {}) _curio = CurioDeck.new(db, rng) _stop = CurioStop.new(db, _curio, _hero, _snapshot_breakpoint) + _loadout = AttuneLoadout.new(db, _attunement) + adventure_inventory = TreasureItems.new(db) # 件出口(组件交出非委托壳——呈现/测量面取主角账单点;拆件批接口块) @@ -83,7 +88,9 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool: _attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive _stop.set_refresh_x_cap(_attune_refresh_x_total()) # 停点件 X 基数预算注入(装配集冒险期间锁死) _stop.set_refresh_y(maxi(0, int(params.get("attune_refresh_y", 0)))) # additive(Y 冒险周期池) - _seed_attune_charges() # 冒险开始充能账本按装备主动件重置(每冒险充能语义) + _loadout.set_attunement(_attunement) # 装配集注入(活引用——种行面) + adventure_inventory.reset() # 冒险级库存重置(新局空库存) + _loadout_seeded = false # 同调行投行延至首关 bind 时点(库存就位后) adventure_counters = {} # 冒险桶清零(效果引擎 adv_ 计数器宿主——冒险开始归零) # 刷新练习开局注入(refresh_stock_grant 装配即得一次性加仓: # 装配集 kind 分发扫描;只入存量账、临时账不动=V8 契约;resume 不重注入〔存量随断点已在〕) @@ -138,10 +145,10 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool: _stop.reemit_resolved() # 已兑付不重兑付只重发(信号面归停点件) return true _resume_bp = bp.duplicate(true) - # 充能账本随断点回局(缺键 = 旧档/无断点,保持 _seed_attune_charges 种子态) - var bp_charges = _bp_read("attunement_charges", bp) - if bp_charges is Dictionary: - run_ledger.attune_charges = (bp_charges as Dictionary).duplicate(true) + # 充能读侧以行集为源(键值仅校验读数——回填时点在首关 bind 行集就位后,经 _loadout_on_level_inventory) + _loadout_seeded = false + _loadout.set_attunement(_attunement) + adventure_inventory.reset() _state = State.RUNNING return _start_current_level() @@ -195,7 +202,6 @@ func _resume_params(bp: Dictionary, params: Dictionary) -> void: _shop_mode = "pool" if String(params.get("shop_mode", "legacy")) == "pool" else "legacy" # 续玩参数面同 start_adventure(缺省 legacy) _treasure_unlocked = params.get("treasure_unlocked", {}) if params.get("treasure_unlocked", {}) is Dictionary else {} _attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive - _seed_attune_charges() # 冒险开始充能账本按装备主动件重置(每冒险充能语义) _stop.set_refresh_x_cap(_attune_refresh_x_total()) # 停点件 X 基数预算注入(续玩参数面同 start) _stop.restore_fragment(bp, params) # 停点件片段回填(凭证计数/刷新存量/Y 池——Y 缺键回退参数面 params_fallback 类) _tree_rules = params.get("tree_rules", []) if params.get("tree_rules", []) is Array else [] # A/B 面 rule 键集(续玩参数面同 start_adventure 同式,消费面 set_tree_rules 下传) @@ -212,6 +218,7 @@ func _resume_counters(bp: Dictionary) -> void: func _reset_curio_state() -> void: _curio.reset() # 池与件获取计数(池半归件;停点态归停点件下行清理) _stop.reset_adventure_state() + _loadout_seeded = false _hero.feats = [] adventure_counters = {} @@ -303,13 +310,20 @@ func _attune_refresh_x_total() -> int: return total -# 冒险开始充能账本重置(T6——装备 active_charged 件各回 charges_per_adventure;被动件不入账) -func _seed_attune_charges() -> void: - run_ledger.attune_charges = {} - for iid in _attunement: - var arow: Dictionary[Variant, Variant] = db.treasure_item_row(String(iid)) - if String(arow.get("form", "")) == "active_charged": - run_ledger.attune_charges[String(iid)] = maxi(0, int(arow.get("charges_per_adventure", 0))) +# 同调行投行分派(关卡域库存就位时点——每关一次;首关=种行或旧档行集为源、后续关=快照补行) +func _loadout_on_level_inventory() -> void: + if _runner.treasure_inventory == null: + _runner.treasure_inventory = TreasureItems.new(db) # 惰性建(承原 sync_attune_inventory 同款——种行落点就位) + var inv = _runner.treasure_inventory + if inv == null: + return + if not _loadout_seeded: + _loadout_seeded = true + var bp_charges: Variant = _bp_read("attunement_charges", _resume_bp, null) if not _resume_bp.is_empty() else null + if bp_charges is Dictionary: + _loadout.restore_fragment({"attunement_charges": bp_charges}, inv) # 旧档读侧:行集为源+键值校验读数 + else: + _loadout.seed_for_adventure(inv) # 冒险开始全量种行(不筛周期——C 区投影行数不变) # ---- 三选一刷新与凭证(编排归停点件——本类留组合根委托读面)---- @@ -358,8 +372,9 @@ func _start_current_level() -> bool: "ticket": _curio.feat_ticket_consider, "cap": _curio.cap_ok} if _shop_mode == "pool" else {} var ctx: Dictionary[Variant, Variant] = { "hero": _hero, - "ledger": run_ledger, + "loadout": _loadout, "adv_bucket": adventure_counters, + "treasure_inventory": adventure_inventory, "attunement": _attunement, "tree_rules": _tree_rules, "shop_mode": _shop_mode, @@ -398,7 +413,7 @@ func _bind_level_runner() -> void: # core↔core 接线只许方法 Callable(S6——lambda 捕获 self 会延长 RefCounted 生命周期乃至成环) _runner.level_finished.connect(_on_level_finished) _runner.choices_presented.connect(_on_level_choices_presented) # 选路停点快照触发(原转发兼快照双职的核心侧受理) - _runner.sync_attune_inventory() # 能力件骑库存面注入(构造注入同拍) + _loadout_on_level_inventory() # 同调行投行分派(种行/旧档行集为源/每关快照补行) # 关卡开始广播(装配已随构造注入进 ctx——本函数只剩信号广播;信号顺序契约: @@ -551,7 +566,7 @@ func _bp_parts() -> Dictionary[Variant, Variant]: "shop_purchased": hero_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": run_ledger.attune_charges.duplicate(true), # 同调主动件充能余量随断点(save v4 域) + "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(), diff --git a/game/core/attune_loadout.gd b/game/core/attune_loadout.gd new file mode 100644 index 00000000..cf33b462 --- /dev/null +++ b/game/core/attune_loadout.gd @@ -0,0 +1,117 @@ +## 同调配装件:装备中的秘宝件对象集唯一所有者(每件内含充能单位与重置周期——冒险域拆分姊妹批自 RunLedger 充能字典轨迁移)。 +## 同调充能单源化:物化行(treasure_inventory 内 source_class="attunement" 行)为使用面唯一事实源—— +## 剩余次数由行的未用数派生(唯一读法,无影子账);字典轨与三处事后差额对账退役,改「写时种行/补行」。 +## 种行/补行写时同步:冒险开始全量种行(不筛周期——battle/level 周期件开局即有行,C 区投影行数不变); +## battle/level 周期回满走 reset_cycle(已耗 handle 不回收=作废、差额补新——语义逐字承原编排器重置); +## 同调未用行跨关存活(关卡域库存提为冒险级共享、关末清空改选择性保留——单源化的行承载前提)。 +## 旧档读侧以行集为源恢复使用面,attunement_charges 键读值仅作一致性校验读数(不符记日志不 fatal——键值本就派生自行)。 +## Core 层零 UI;零随机(充能域无 RNG 消费);随机无关、数值只读 db。 +class_name AttuneLoadout +extends RefCounted + +var db: ConfigDb +var _ids: Array[Variant] = [] # 装配集(活引用注入——宿主 start/resume 时点下传) + +func _init(db_ref: ConfigDb, attunement_ids: Array) -> void: + db = db_ref + set_attunement(attunement_ids) + + +# 装配集注入(活引用——宿主 start_adventure/resume 参数面锁定后下传) +func set_attunement(attunement_ids: Array) -> void: + _ids = attunement_ids + + +func item_ids() -> Array: + return _ids.duplicate() + + +# ---- 写时种行/补行(替代三处事后差额对账)---- + +# 冒险开始:全部 active_charged 件种行(**不筛周期**——承现 _seed_attune_charges 全量种账+sync 全量投行 +# 语义;battle/level 周期件开局即有行,C 区投影行数不变) +func seed_for_adventure(inventory: TreasureItems) -> void: + for iid_v in _ids: + var iid := String(iid_v) + var arow: Dictionary[Variant, Variant] = db.treasure_item_row(iid) + if String(arow.get("form", "")) != "active_charged": + continue + _ensure_rows(iid, maxi(0, int(arow.get("charges_per_adventure", 0))), inventory) + + +# battle/level 周期回满(**按周期筛**;已耗行作废、差额补新——语义逐字承原编排器 _reseed_attune_charges; +# charges_per_adventure=0=自动兑付件不入使用面〔试炼镐——不走骑库存面〕) +func reset_cycle(cycle: String, inventory: TreasureItems) -> void: + for iid_v in _ids: + var iid := String(iid_v) + var arow: Dictionary[Variant, Variant] = db.treasure_item_row(iid) + if String(arow.get("form", "")) != "active_charged": + continue + if String(arow.get("charge_reset", "adventure")) != cycle: + continue + var want := int(arow.get("charges_per_adventure", 0)) + if want < 1: + continue # charges=0=自动兑付件(不走骑库存面,兑付=积点段自动 grant_ap) + _ensure_rows(iid, want, inventory) + + +# 差额补行单点(未用行数不足 want 即补——「差额注入防续玩双份」现役语义逐字) +func _ensure_rows(iid: String, want: int, inventory: TreasureItems) -> void: + var existing := 0 + for r in inventory.snapshot_inventory(): + if String(r.item_id) == iid and String(r.source_class) == "attunement" and not bool(r.used): + existing += 1 + for i in range(maxi(0, want - existing)): + inventory.add_item(iid, "attunement", Vector2i(-1, -1), 0) + + +# ---- 使用面(战斗路径改指)---- + +# 充能消耗(承原 attune_charge_consume「扣账 -1」语义——单源化后=行标 used 幂等承载: +# 使用管线 consume 句柄已标行 used,本口兜底标未用行〔双入口同点防御——行 used 不可逆幂等〕) +func consume_charge(item_id: String, inventory: TreasureItems) -> void: + for r in inventory.snapshot_inventory(): + if String(r.item_id) == item_id and String(r.source_class) == "attunement" and not bool(r.used): + inventory.consume(int(r.handle)) # 行标 used(与使用管线 consume 同口——幂等兜底) + return + + +# ---- 读面(行派生——唯一读法)---- + +# 剩余次数(未用行数派生——原字典轨读数逐位等价) +func charges_left(item_id: String, inventory: TreasureItems) -> int: + var n := 0 + for r in inventory.snapshot_inventory(): + if String(r.item_id) == item_id and String(r.source_class) == "attunement" and not bool(r.used): + n += 1 + return n + + +# ---- 断点自交片段(应用层拼盘口收件——键名与策略不变)---- + +# attunement_charges 键(值形状=原字典轨 {item_id:剩余}——由行派生,键名/策略不变) +func breakpoint_fragment(inventory: TreasureItems) -> Dictionary[Variant, Variant]: + var charges: Dictionary[Variant, Variant] = {} + for iid_v in _ids: + var iid := String(iid_v) + var arow: Dictionary[Variant, Variant] = db.treasure_item_row(iid) + if String(arow.get("form", "")) == "active_charged": + charges[iid] = charges_left(iid, inventory) + return {"attunement_charges": charges} + + +# 旧档读侧(以行集为源恢复使用面——键值仅校验读数:不符记日志不 fatal;缺键保持种子态=原语义)。 +# 调用时点=行集已随 aux_items 回填(恢复链序:库存回填先于本口)。 +func restore_fragment(frag: Dictionary, inventory: TreasureItems) -> void: + var stored: Variant = frag.get("attunement_charges", null) + if not (stored is Dictionary) or (stored as Dictionary).is_empty(): + return # 缺键 = 旧档/无断点,保持种子态(原语义) + for iid in (stored as Dictionary).keys(): + var derived := charges_left(String(iid), inventory) + var recorded := int((stored as Dictionary)[iid]) + if derived != recorded: + # 跨版续玩兜底:改前档关卡完成停点的同调行已被关末清空(键在行缺)——按键值差额补行, + # 使用面与改前字典轨续玩逐位等价;新档行集为源时差额=0 零操作(行集仍为读数唯一来源) + push_warning("[同调配装] 充能校验读数与行派生不一致(%s 档=%d 行派生=%d——按键值差额补行)" + % [String(iid), recorded, derived]) + _ensure_rows(String(iid), recorded, inventory) diff --git a/game/core/attune_loadout.gd.uid b/game/core/attune_loadout.gd.uid new file mode 100644 index 00000000..50849b43 --- /dev/null +++ b/game/core/attune_loadout.gd.uid @@ -0,0 +1 @@ +uid://24ntqf46dfec diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index 8b3505ff..62eab234 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -72,7 +72,7 @@ var _board_from_midround_refresh := false # 装板 provenance:轮内刷新换 var _seal_consumed := {} # 兑付资格以危险格实例为键(整图;换图重置) # 同调装配运行态(begin_battle 经 enc.attunement 注入;off=空集全部效果零扰动) var attunement: Array[Variant] = [] # 装备中同调件 item_id 集 -var _ledger: RunLedger = null # 同调充能账瘦壳(begin_battle 经 enc["ledger"] 持引用;缺省新账=调试面) +var loadout = null # 同调配装件(begin_battle 经 enc["loadout"] 持引用;直构 null=零装配——充能面跳过) var _hero: HeroLedger = null # 主角实体账(begin_battle 经 enc["hero"] 持引用;null=调试/单场直构面——账面消费跳过) var _attune_safe_seen := 0 # 积点制件 battle 域安全开格计数(装配集 ap_charge_safe 行扫描;满 charge_n 自动兑付当轮 AP+1,无玩家发起入口) # ---- 效果引擎扩签运行态(新效果类型施加面;空订阅零扰动)---- @@ -234,24 +234,9 @@ func _attune_charge_safe_row() -> Dictionary: # (used handle 不回收=本轮已耗充能作废,补新 handle=「回满」;差额注入防双份=level_runner # set_attune_charges 同构)。reset ∈ {battle, level}——adventure 周期归冒险开始种子面) func _reseed_attune_charges(reset: String) -> void: - for iid_v in attunement: - var iid := String(iid_v) - var arow: Dictionary[Variant, Variant] = db.treasure_item_row(iid) - if String(arow.get("form", "")) != "active_charged": - continue - if String(arow.get("charge_reset", "adventure")) != reset: - continue - var want := int(arow.get("charges_per_adventure", 0)) - if want < 1: - continue # charges=0=自动兑付件(试炼镐——不走骑库存面,兑付=_on_cell_revealed 积点段自动 grant_ap) - _ledger.attune_charge_set(iid, want) - if treasure_inventory != null: - var existing := 0 - for r in treasure_inventory.snapshot_inventory(): - if String(r.get("item_id", "")) == iid and String(r.get("source_class", "")) == "attunement" and not bool(r.get("used", false)): - existing += 1 - for i in range(maxi(0, want - existing)): - treasure_inventory.add_item(iid, "attunement", Vector2i(-1, -1), 0) + if loadout == null or treasure_inventory == null: + return # 直构零装配/库存未就位——充能回满跳过(空装配同语义) + loadout.reset_cycle(reset, treasure_inventory) # 写时种行/补行(按周期筛——语义逐字承原循环体) # 积点制件进度读数(自动兑付无玩家发起入口,读数口=C 区秘宝槽悬停浮窗进度行: @@ -385,8 +370,7 @@ func begin_battle(enc: Dictionary) -> bool: _fx_new_resource_types = {} _fx_pending_reveals = [] _fx_bonus_delta = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} - var ledger_ref: Variant = enc.get("ledger", null) - _ledger = ledger_ref if ledger_ref is RunLedger else RunLedger.new() # 缺省新账=调试/单场零扰动面 + loadout = enc.get("loadout", null) _hero = enc.get("hero", null) if enc.get("hero", null) is HeroLedger else null # null=调试直构——账面消费跳过 counters_ctx.adventure_bucket = enc.get("adv_bucket", null) if enc.get("adv_bucket", null) is Dictionary else {} # 冒险桶宿主字典(直构兜底自有空字典) var level_ref: Variant = enc.get("level_counters") @@ -761,9 +745,9 @@ func use_treasure(handle: int, target: Vector2i) -> Dictionary: if blasted.is_empty(): return {"ok": false, "reason": "爆破工兵包:3×3 区域内无可移除危险格(须未揭示且未封印)"} var rec: Dictionary[Variant, Variant] = treasure_inventory.consume(handle) - # 同调主动件充能扣减(共享账本直改:adventure/level/settlement 同一字典引用) - if not rec.is_empty() and String(rec.get("source_class", "")) == "attunement": - _ledger.attune_charge_consume(String(rec.get("item_id", ""))) + # 同调主动件充能消耗(单源化——行标 used 幂等承载,原字典扣减同点退役) + if loadout != null and not rec.is_empty() and String(rec.get("source_class", "")) == "attunement": + loadout.consume_charge(String(rec.get("item_id", "")), treasure_inventory) # 真视类工具使用计数(信标网络解锁行为条件:受理成功恰计一次; # 主角账单点记录,冒险收束聚合入档 save.add_vision_used) if kinds.has("reveal_tiles") or kinds.has("safe_dig") or kinds.has("mine_reveal"): @@ -1134,7 +1118,7 @@ func _battle_over(result: String) -> void: for axis in _fx_bonus_delta.keys(): _hero.gain_bonus(String(axis), int(_fx_bonus_delta[axis]), "battle_export", String(axis)) _fx_bonus_delta = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} - # 搬运链删除:adventure 桶经 ctx 引用直写 RunLedger,无战末导出时点(旧导出/汇入搬运链删除) + # 搬运链删除:adventure 桶经 ctx 引用直写宿主字典,无战末导出时点(旧导出/汇入搬运链删除) # 映射随战斗结束退役(地图战斗资源 义务④——下一场战斗 = 新板新 scope 批,roll_board 相位守卫的前提) var hero_hp_left := int(_enemy_system.hero_unit().get("hp", 0)) # despawn 前捕获(信号载荷供呈现层"HP 带入下一层") var gp_dropped := _roll_gp_drop() # 战斗掉落(escrow 随此值上抛即弃) diff --git a/game/core/effect_engine.gd b/game/core/effect_engine.gd index ebff39b7..ed77b789 100644 --- a/game/core/effect_engine.gd +++ b/game/core/effect_engine.gd @@ -91,7 +91,7 @@ func _init(db_ref: ConfigDb = null) -> void: ## 订阅装载(每战 begin_battle 调用——重建时机 v1.1):feats 表序→treasure 表序(各按持有/装备 ## 集过滤)→树规则合成行。同一数据行的多个 trigger 按行内数组序。计数器四桶由宿主持有、随宿主生灭, ## 本方法只重建订阅集(运行时可变状态=零;battle/level 桶的清零由宿主 new 天然承载,round 桶清零 -## 在 dispatch 的 round_start 特判内整桶 clear,adventure 桶随 RunLedger 引用跨战持久——无副本搬运)。 +## 在 dispatch 的 round_start 特判内整桶 clear,adventure 桶随宿主字典引用跨战持久——无副本搬运)。 func rebuild(feat_ids: Array, item_ids: Array, tree_rules: Array = []) -> void: _triggers.clear() if db != null and db.feats != null: diff --git a/game/core/hero_ledger.gd b/game/core/hero_ledger.gd index 30a6d3d7..5bc1bbc3 100644 --- a/game/core/hero_ledger.gd +++ b/game/core/hero_ledger.gd @@ -9,7 +9,7 @@ class_name HeroLedger extends RefCounted -# 加算区四轴(键序即断点 bonus 键序——与现役 run_ledger.bonus 置账形态逐位同形) +# 加算区四轴(键序即断点 bonus 键序——与拆件前共享账置账形态逐位同形) const BONUS_AXES: Array[String] = ["atk", "max_hp", "armor_class", "ap"] var db: ConfigDb @@ -253,9 +253,6 @@ func restore_fragment(frag: Dictionary, _params: Dictionary) -> void: # 日记回填(记录列表与明细全量还原;汇总由明细推导——与留存汇总逐轴比对,不符即致命中止) func _restore_from_journal(journal: Dictionary, frag: Dictionary) -> void: _gain_log = (journal.get("records", []) as Array).duplicate(true) - _gain_seq = _gain_log.size() - for rec in _gain_log: - _gain_seq = maxi(_gain_seq, int((rec as Dictionary).get("id", 0))) _bonus_detail = (journal.get("details", []) as Array).duplicate(true) var derived: Dictionary[Variant, Variant] = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} for d in _bonus_detail: diff --git a/game/core/level_runner.gd b/game/core/level_runner.gd index 8e77c984..8cecdad1 100644 --- a/game/core/level_runner.gd +++ b/game/core/level_runner.gd @@ -30,6 +30,7 @@ var _external := false # 冒险模式标记(钱 # 主角账引用(拆件批 T1 过渡面:冒险模式经 set_hero 注入共享实体账、单关模式 _init 自建私有账; # 构造注入收口归 T3 ctx 面——后置 setter 随构造注入一并退役) var _hero: HeroLedger = HeroLedger.new(null, {}) # 直构缺省私有账(db 随 _init 补) +var loadout = null # 同调配装件(构造注入;直构=单关零件) var _adv_bucket: Dictionary[Variant, Variant] = {} # 冒险桶(效果引擎 adv_ 计数器宿主字典——冒险模式随 hero 注入;单关=本实例自有) # hero_hp / gp 属性化(既有读写点零改动——值类型经属性落主角账) var hero_hp: int: @@ -62,7 +63,7 @@ var _generator # 参数化长图生成器(惰性建——NodeMapGenerator # 构造注入一次给齐(拆件批 T3——后置 setter 群与商店重绑机制退役;_bind_shop 全仓仅本处构造调用)。 # ctx 键(冒险模式由 AdventureRunner._start_current_level 组包;缺省空字典 = 单关测试模式: # 私有主角账自建〔空 override〕、充能账瘦壳自建、冒险桶本实例自有、pool 模式零注入): -# hero:HeroLedger(共享主角账——活引用)/ledger:RunLedger(充能账瘦壳)/adv_bucket:Dictionary(冒险桶宿主字典) +# hero:HeroLedger(共享主角账——活引用)/loadout:AttuneLoadout(同调配装件)/adv_bucket:Dictionary(冒险桶宿主字典) # attunement:Array(同调装配集——活引用)/tree_rules:Array(树 rule 键集——活引用)/shop_mode:String("pool" 才注入池面) # treasure_unlocked:Dictionary/hero_attack_type:String/hero_armor_type:String # voucher_rap:int/voucher_time_sand:int(凭证出箱注入面) @@ -75,10 +76,12 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore, ctx: Dictionary = {}) -> v _feats = _hero.feats # 特性集活引用注入(关内兑付 relic 当关生效——契约 6) else: _hero.db = db # 直构私有账补表引用(单关模式——实例不重建,值重置走 start_level) - if ctx.get("ledger", null) is RunLedger: - run_ledger = ctx["ledger"] + var loadout_ref: Variant = ctx.get("loadout", null) + loadout = loadout_ref if loadout_ref is AttuneLoadout else null # 直构=单关零 loadout(充能面跳过) if ctx.get("adv_bucket", null) is Dictionary: _adv_bucket = ctx["adv_bucket"] + if ctx.get("treasure_inventory", null) is TreasureItems: + treasure_inventory = ctx["treasure_inventory"] # 冒险级库存共享(同调未用行跨关存活——单源化承载面) _treasure_unlocked = ctx.get("treasure_unlocked", {}) if ctx.get("treasure_unlocked", {}) is Dictionary else {} _attunement = ctx.get("attunement", []) if ctx.get("attunement", []) is Array else [] # 活引用注入(契约 6——禁快照) _tree_rules = ctx.get("tree_rules", []) if ctx.get("tree_rules", []) is Array else [] # 活引用注入 @@ -522,7 +525,7 @@ func _start_battle(node: Dictionary) -> bool: "hero_armor_class": effective_ac(), "hero_ap_bonus": effective_ap_bonus(), "attack_type": _hero_attack_type, "armor_type": _hero_armor_type, # 两根类型轴(spawn 英雄快照;空串=锻造配置默认值回退) "attunement": _attunement, - "ledger": run_ledger, # 充能账瘦壳下传(单引用键——bonus/真视/桶已迁主角账与冒险桶) + "loadout": loadout, # 同调配装件下传(充能单源化——reset_cycle/consume_charge 消费面;直构 null=零装配) "hero": _hero, # 主角实体账下传(战末 bonus 汇入/真视计数单写原语消费面) "adv_bucket": _adv_bucket, # 冒险桶下传(效果引擎 adv_ 计数器宿主字典——跨战持久同一引用) "level_counters": level_counters, # 关卡桶下传(同关跨战共享引用、随本类 new 天然空) @@ -580,7 +583,13 @@ func _on_battle_finished(payload: Dictionary) -> void: var treasure_cleared := 0 if treasure_inventory != null: treasure_cleared = treasure_inventory.inventory_count() # 快照计数(清空前——「剩余道具随关卡结束清空」可见反馈取数) - treasure_inventory.reset() # 关卡末库存清空(无论输赢,幂等) + # 关卡末库存清空(无论输赢,幂等)——单源化承载面:同调未用行跨关存活(充能唯一事实源, + # 原字典轨跨关记忆的行化等价——已耗行作废不保留=C 区投影与剩余读数同构) + var keep: Array[Variant] = [] + for r in treasure_inventory.snapshot_inventory(): + if String(r.get("source_class", "")) == "attunement" and not bool(r.get("used", false)): + keep.append(r) + treasure_inventory.restore_inventory(keep) level_finished.emit({"result": level_result, "hp": hp_left, "node_index": node_index, "node_id": _node_id(node), "node_type": String(node.get("type", "")), "aux_cleared": treasure_cleared}) # additive 扩签(不参与派生——node_map 扩签先例) @@ -641,10 +650,6 @@ func pick_next_node(node_id: int) -> bool: func current_battle() -> Variant: return _settlement -# 共享充能账瘦壳(RunLedger 瘦身后只承载 attune_charges;构造注入) -var run_ledger: RunLedger = RunLedger.new() - - # 特性持有集(begin_battle enc.feats 求值面;**共享引用不复制**——关内奖励节点兑付的 relic 须当关生效) var _feats: Array[Variant] = [] # 树 rule 键集(begin_battle enc.tree_rules 携带;Fork 激活侧 effects——构造注入活引用) @@ -666,21 +671,6 @@ var _voucher_rap := 0 var _voucher_time_sand := 0 -# 同调主动件充能账骑库存面注入(账本住 -# RunLedger;差额注入防续玩双份——resume 路径 restore_inventory -# 已含断点内 attune 记录) -func sync_attune_inventory() -> void: - var charges: Dictionary[Variant, Variant] = run_ledger.attune_charges - if treasure_inventory == null: - treasure_inventory = TreasureItems.new(db) - for iid in charges: - var existing := 0 - for r in treasure_inventory.snapshot_inventory(): - if String(r.item_id) == String(iid) and String(r.source_class) == "attunement" and not bool(r.used): - existing += 1 - for i in range(maxi(0, int(charges[iid]) - existing)): - treasure_inventory.add_item(String(iid), "attunement", Vector2i(-1, -1), 0) - # 三选一 GP 袋件入账(钱包收口的「奖励入账」入口;复用战斗完成同一入账原语, # wallet_changed 载荷带 source 供呈现归因) func grant_curio_gp(amount: int) -> void: diff --git a/game/core/run_ledger.gd b/game/core/run_ledger.gd deleted file mode 100644 index 3656bd2b..00000000 --- a/game/core/run_ledger.gd +++ /dev/null @@ -1,23 +0,0 @@ -class_name RunLedger -extends RefCounted -# 同调充能账瘦壳(冒险域拆件批瘦身半程——bonus 账/真视账/冒险桶已迁 HeroLedger 与冒险应用层)。 -# 所有权:AdventureRunner 持有(开局构造、冒险全程唯一宿主);LevelRunner 经 set_run_ledger -# 下传并装配 encounter;战斗侧(BattleOrchestrator)经 encounter["ledger"] 拿本类引用, -# 只走下方原语。整类文件退役归姊妹副卡(充能域封装批——单源化后零引用即删)。 -var attune_charges: Dictionary[Variant, Variant] = {} # 同调主动件充能账(use_treasure 扣减;断点随局回档) - - -# ---- 战斗侧原语(宿主侧冒险/关卡域直读字段域) ---- - -func attune_charge_has(item_id: String) -> bool: - return attune_charges.has(item_id) - - -func attune_charge_set(item_id: String, value: int) -> void: - attune_charges[item_id] = value - - -func attune_charge_consume(item_id: String) -> void: - # 原编排器 use_treasure 扣减语义逐字(has 闸 + maxi(0,·) 下限) - if attune_charges.has(item_id): - attune_charges[item_id] = maxi(0, int(attune_charges[item_id]) - 1) diff --git a/game/core/run_ledger.gd.uid b/game/core/run_ledger.gd.uid deleted file mode 100644 index e1e24a62..00000000 --- a/game/core/run_ledger.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://7afr7e1gtrj5 diff --git a/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd b/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd index ee4ae52e..0484a70f 100644 --- a/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd +++ b/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd @@ -965,16 +965,16 @@ func _smoke_debuff_rider() -> void: # 引擎直构 + 编排授予段直注(_grant_seg._judge_seal=结算授予段封印判定单口))---- func _smoke_counter_buckets() -> void: print("--- 效果引擎无状态化四桶正向验收(卡② §9 V1/V6)---") - # V1——adventure 桶跨战持久:同一 RunLedger 下两战各真实 request_dig 一次, + # V1——adventure 桶跨战持久:同一宿主字典下两战各真实 request_dig 一次, # 断付费计数经 ctx 就地写入宿主字典并跨战累积(副本搬运态:读不到上一战值→第 2 战仍 1,此断言会红)。 - var led = load("res://core/run_ledger.gd").new() + var led: Dictionary[Variant, Variant] = {} # 冒险桶宿主字典(拆件批——桶归宿主) var RigS = load("res://core/battle_orchestrator.gd") var RngS = load("res://core/rng_service.gd") var enc1 = {"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, "feats": ["twin_pick"], "tree_rules": [], "battle_index": 1, "bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "voucher_rap": 0, "voucher_time_sand": 0, - "node_type": "battle", "ledger": led} + "node_type": "battle", "adv_bucket": led} var o1 = RigS.new(_db, _fresh_rng(779301)) o1.begin_battle(enc1) var dig1: bool = o1.action_economy.request_dig(0, false) @@ -992,7 +992,7 @@ func _smoke_counter_buckets() -> void: "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, "feats": ["perfect_insurance"], "tree_rules": [], "battle_index": 1, "bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "voucher_rap": 0, "voucher_time_sand": 0, - "node_type": "battle", "ledger": load("res://core/run_ledger.gd").new()} + "node_type": "battle", "adv_bucket": {}} var oA = RigS.new(_db, _fresh_rng(779401)) oA.begin_battle(encA) var v6_rows: Array = [] @@ -1031,7 +1031,7 @@ func _smoke_extended_rows() -> void: "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, "feats": ["geologist"], "tree_rules": [], "battle_index": 1, "bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "voucher_rap": 0, "voucher_time_sand": 0, - "node_type": "battle", "ledger": load("res://core/run_ledger.gd").new()} + "node_type": "battle", "adv_bucket": {}} # ② 兑付行:有未揭示安全格 → 掷格揭示成功 → 扩展行带真实格与 fx_reveal 身份 var o_fx = RigS.new(_db, _fresh_rng(779501)) o_fx.begin_battle(enc_fx) diff --git a/game/tests/unit/core/feat_vouchers_test.gd b/game/tests/unit/core/feat_vouchers_test.gd index e5f5ac69..d1666fd8 100644 --- a/game/tests/unit/core/feat_vouchers_test.gd +++ b/game/tests/unit/core/feat_vouchers_test.gd @@ -68,12 +68,13 @@ func _init() -> void: "电池符号清零:runner 无点选函数与 getter(编译级孤儿面)〔卡② R1〕") Kit.check_quiet(int(db.adventure.curio_cap("attune_battery")) == 99, "出池后未知 kind 走分发表 default(cap 读数兜底 99——零崩溃面)〔卡② R1〕") - # 账本保留(契约 1):主动件充能账与每冒险重置入口零改动 + # 账本保留(契约 1):主动件充能与每冒险种行入口零改动(单源化后=差额种行幂等——不吞装配) a._attunement = ["incense_stick"] - a.run_ledger.attune_charges = {"incense_stick": 99} - a._seed_attune_charges() - Kit.check_quiet(int(a.run_ledger.attune_charges.get("incense_stick", -1)) != 99, - "账本保留:删件后 _seed_attune_charges 仍按装备重置充能账(attune_charges 语义零变化)〔卡② R1〕") + a._loadout.set_attunement(["incense_stick"]) + a._loadout_seeded = false # 模拟首关 bind 时点(分派走 seed_for_adventure 分支) + a._loadout_on_level_inventory() # 种行(首关 bind 时点语义) + Kit.check_quiet(a._loadout.charges_left("incense_stick", a.runner().treasure_inventory) == 1, + "账本保留:种行按装备表值投行(线香 battle 级 1——attune_charges 语义零变化)〔卡② R1〕") # V24 配额净删锚(R3——「净删」口径,rare 9→7/总量 104→102) var rare_sum := 0 var rare_kinds: Array[String] = [] diff --git a/game/tests/unit/core/meta_progression_forge_test.gd b/game/tests/unit/core/meta_progression_forge_test.gd index 4c933cef..dc1d22cb 100644 --- a/game/tests/unit/core/meta_progression_forge_test.gd +++ b/game/tests/unit/core/meta_progression_forge_test.gd @@ -287,8 +287,8 @@ func _meta_settle_and_workshop() -> void: var a12 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(12)) Kit.check("充能重置场景:装配冒险启动(seed 12)〔秘宝批 V8 换主语〕", a12.start_adventure(12, {"attunement": ["incense_stick", "sky_eye"]})) - Kit.check("充能账本按装备 active_charged 件重置(新目录表值:线香 battle 级 1 次/天眼 level 级 1 次——周期重置点归 V8 逐形态面)", # 充能账住 RunLedger - int(a12.run_ledger.attune_charges.get("incense_stick", -1)) == 1 and int(a12.run_ledger.attune_charges.get("sky_eye", -1)) == 1) + Kit.check("充能按装备 active_charged 件种行(新目录表值:线香 battle 级 1 次/天眼 level 级 1 次——周期重置点归 V8 逐形态面;读数=行派生)", + a12._loadout.charges_left("incense_stick", a12.runner().treasure_inventory) == 1 and a12._loadout.charges_left("sky_eye", a12.runner().treasure_inventory) == 1) a12._attunement = [] a12.stop()._refresh_x = 1 a12.stop()._refresh_y = 2 diff --git a/game/tests/unit/core/treasure_effects_test.gd b/game/tests/unit/core/treasure_effects_test.gd index 5adeae1c..a8baa525 100644 --- a/game/tests/unit/core/treasure_effects_test.gd +++ b/game/tests/unit/core/treasure_effects_test.gd @@ -306,7 +306,7 @@ func _init() -> void: "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, "feats": ["seal_smith"], "tree_rules": [], "battle_index": 1, "bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "voucher_rap": 0, "voucher_time_sand": 0, - "node_type": "battle", "ledger": load("res://core/run_ledger.gd").new()} + "node_type": "battle", "adv_bucket": {}} var rng15 = load("res://core/rng_service.gd").new(); rng15.set_master_seed(341) var ob15 = orch_s.new(db, rng15) ob15.begin_battle(enc_feats)