diff --git a/game/core/adventure_runner.gd b/game/core/adventure_runner.gd index 0dfb6fb3..a9c1f120 100644 --- a/game/core/adventure_runner.gd +++ b/game/core/adventure_runner.gd @@ -17,7 +17,9 @@ var _state: State = State.IDLE var state: String: # 外部契约面(只读——外部零写点实证;词表 "idle"/"running"/"settling") get: return State.keys()[_state].to_lower() -var _hero: HeroLedger # 主角实体账(生命/钱包/加算区/类型轴/特性集/钥石/真视——拆件批 T1 建件) +var _stats: HeroStats # 主角数值账(生命/类型轴/特性集——公式单点,拆件批 T1 建件、领域拆分批三件化) +var _wallet: GpWallet # GP 钱包件(单写原语——LevelRunner 与 ShopSession 读写单点) +var _journal: GainJournal # 加算日记件(获得记录三层/钥石/真视/已购档单写原语) var adventure_counters: Dictionary[Variant, Variant] = {} # 冒险桶(效果引擎 adv_ 计数器宿主字典——跨战持久) var level_index := 0 # 当前关卡序号(1 起) @@ -45,16 +47,30 @@ var _shop_mode := "legacy" # 商店模式("legacy"=固定三件 func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void: db = db_ref rng = rng_ref - _hero = HeroLedger.new(db, {}) + _journal = GainJournal.new() + _wallet = GpWallet.new() + _stats = HeroStats.new(db, {}, _journal) # 数值账消费日记汇总(同源单件注入) _curio = CurioDeck.new(db, rng) - _stop = CurioStop.new(db, _curio, _hero, _snapshot_breakpoint) + _stop = CurioStop.new(db, _curio, _stats, _journal, _snapshot_breakpoint) _loadout = AttuneLoadout.new(db, _attunement) adventure_inventory = TreasureItems.new(db) -# 件出口(组件交出非委托壳——呈现/测量面取主角账单点;拆件批接口块) -func hero() -> HeroLedger: - return _hero +# 件出口(组件交出非委托壳——呈现/测量面取主角账三件与令牌池单点;领域拆分批接口块) +func stats() -> HeroStats: + return _stats + + +func wallet() -> GpWallet: + return _wallet + + +func journal() -> GainJournal: + return _journal + + +func deck() -> CurioDeck: + return _curio # 件出口(停点件——呈现/测量面取停点编排单点;拆件批接口块) @@ -74,10 +90,12 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool: return false master_seed = seed_val rng.new_adventure(seed_val) - _hero.rebase_override(params.get("hero_override", {})) # 永久强化 flat 注入(规范化归主角账) - _hero.reset_adventure_accounts() # 账面重置(钱包/加算区/已购档/钥石/日记/特性清零+生命置满基础上限) - _hero.attack_type = String(params.get("attack_type", "")) # 两根类型轴快照(契约 8 锁死:开局后不随存档选中变化) - _hero.armor_type = String(params.get("armor_type", "")) + _stats.rebase_override(params.get("hero_override", {})) # 永久强化 flat 注入(规范化归数值账) + _journal.reset_adventure_accounts() # 日记域账面清零(汇总/明细/已购档/钥石/真视——先于置满) + _wallet.clear_wallet() # 钱包清零 + _stats.reset_adventure_accounts() # 生命置满基础上限+特性持有集清零(口径不含加算区——日记先清) + _stats.attack_type = String(params.get("attack_type", "")) # 两根类型轴快照(契约 8 锁死:开局后不随存档选中变化) + _stats.armor_type = String(params.get("armor_type", "")) summary = {} _reset_curio_state() levels_total = db.level_count() # 表访问收口(关数读 level 表行数) @@ -104,7 +122,7 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool: _tree_rules = params.get("tree_rules", []) if params.get("tree_rules", []) is Array else [] # A/B 面 rule 键集 if not feat_seed.is_empty(): for r in feat_seed: - _hero.feats.append(String(r)) + _stats.feats.append(String(r)) _state = State.RUNNING return _start_current_level() @@ -153,10 +171,12 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool: return _start_current_level() -# 续玩账面回填(生命/钱包/加算区/已购档/钥石/逐杀明细/特性/真视——主角账片段单点; +# 续玩账面回填(生命/特性/钱包/加算区/已购档/钥石/逐杀明细/真视——主角账三件片段各还原各键; # bonus 子键 clamp 类夹 ≥0;日记在档=推导比对载入、缺键=旧档汇总直载) func _resume_ledger(bp: Dictionary, params: Dictionary) -> void: - _hero.restore_fragment(bp, params) + _stats.restore_fragment(bp, params) + _wallet.restore_fragment(bp, params) + _journal.restore_fragment(bp, params) summary = {} levels_total = db.level_count() # 表访问收口(关数读 level 表行数) level_index = int(bp.get("level_index", 1)) @@ -183,22 +203,21 @@ func _resume_curio_state(bp: Dictionary) -> void: rtok["feat_id"] = String(tok.get("feat_id", "")) # relic 令牌 feat_id 随还原 restored.append(rtok) _curio.deck = restored - # 特性持有集还原(缺键 = 旧版断点防御空集——主角账字段单点) - _hero.feats = [] + # 特性持有集还原(缺键 = 旧版断点防御空集——主角数值账字段单点) + _stats.feats = [] for r in (bp.get("relics", []) as Array): - _hero.feats.append(String(r)) + _stats.feats.append(String(r)) # 续玩参数面回填(同 start_adventure 参数面;生命账本越界钳制在此落——clamp 类) func _resume_params(bp: Dictionary, params: Dictionary) -> void: - _hero.rebase_override(params.get("hero_override", {})) # 续玩参数面同 start_adventure(规范化归主角账) - _hero.attack_type = String(params.get("attack_type", "")) # 续玩参数面同 start_adventure(账号层选中态随 resume 重注入) - _hero.armor_type = String(params.get("armor_type", "")) + _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。上限 = 表值+永久 - # 强化 flat 后乘调试浮窗 pct(保守口径不含加算区——与原钳制式逐字同,加算区恢复后其余战斗照常)。 - _hero.hero_hp = mini(_hero.hero_hp, maxi(1, - int(round(float(int(db.hero.max_hp) + int(_hero.base_override_flat().get("max_hp", 0))) * (100.0 + float(_hero.debug_hp_pct())) / 100.0)))) + #(如调试浮窗调高期写出的值,实例:hero_hp=28 > 基础 26)——恢复后钳到生效上限, + # 否则首战 spawn「hero_hp 入参越界」拒收 → 「敌人系统 spawn 被拒」fatal。钳制式单点归 + # 数值账 clamp_hp_to_resume_cap(保守口径不含加算区——与原钳制式逐字同)。 + _stats.clamp_hp_to_resume_cap(int(db.hero.max_hp)) _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 @@ -219,20 +238,20 @@ func _reset_curio_state() -> void: _curio.reset() # 池与件获取计数(池半归件;停点态归停点件下行清理) _stop.reset_adventure_state() _loadout_seeded = false - _hero.feats = [] + _stats.feats = [] adventure_counters = {} -# 调试浮窗百分比通道(测试工具覆盖——每关新 runner 都要应用;正式侧归元进度。单点归主角账) +# 调试浮窗百分比通道(测试工具覆盖——每关新 runner 都要应用;正式侧归元进度。单点归数值账) func set_hero_override(atk_pct: int, hp_pct: int) -> void: - _hero.set_debug_override(atk_pct, hp_pct) + _stats.set_debug_override(atk_pct, hp_pct) if _runner != null: _runner.set_hero_override(atk_pct, hp_pct) -# 冒险账本侧生效上限(公式单点归主角账——置满/回血封顶与战斗内上限同式同源) +# 冒险账本侧生效上限(公式单点归数值账——置满/回血封顶与战斗内上限同式同源) func effective_max_hp() -> int: - return _hero.effective_max_hp() + return _stats.effective_max_hp() # 断点承载注入(组合根冒险开始时调用;模拟/冒烟不注入 = 断点面整体静默) @@ -245,9 +264,9 @@ func runner() -> LevelRunner: return _runner -# 接口③治疗封顶回报执行体(恢复点节点兑付——按比例回血并封顶)。算术宿主 = HeroLedger.rest_heal(公式单点) +# 接口③治疗封顶回报执行体(恢复点节点兑付——按比例回血并封顶)。算术宿主 = HeroStats.rest_heal(公式单点) func apply_rest(pct: int, hp: int, max_hp: int) -> int: - return HeroLedger.rest_heal(pct, hp, max_hp) + return HeroStats.rest_heal(pct, hp, max_hp) # 大奖励确认 → 进下一关(仅兑付后受理——零自动推进的冒险级停点;三选一未兑付拒绝推进) @@ -275,7 +294,7 @@ func _find_token(candidates: Array, kind: String, tier: String, feat_id: String # 玩家点选兑付(state 守卫留应用层——编排归停点件;呈现层直调面 T5 改 stop()) # 特性持有集读面(效果接线/求值/摘要唯一取数口——副本防外部改写) func feats_held() -> Array: - return _hero.feats_held() + return _stats.feats_held() # ---- 商店共享池消费面(纯池——库存=牌库剩余令牌)---- @@ -346,7 +365,7 @@ func _boss_keystone_drop(li: int) -> void: first = not _save_manager.boss_first_kill_marked(li) var row: Dictionary[Variant, Variant] = db.meta_boss_keystone_row(li) var amount: int = int(row.get("first_kill", 0)) if first else int(row.get("repeat_kill", 0)) - _hero.record_keystone_drop({"level": li, "first": first, "amount": amount}) # 逐杀明细+累计单写原语 + _journal.record_keystone_drop({"level": li, "first": first, "amount": amount}) # 逐杀明细+累计单写原语 # 主动放弃(仅非战斗期受理——战斗域单事务在飞;与失败同型结算,已获大奖励不追回。 @@ -371,7 +390,9 @@ func _start_current_level() -> bool: var pool_wiring: Dictionary[Variant, Variant] = {"deck": _curio.deck, "consume": _stop.consume_pool_purchase, "ticket": _curio.feat_ticket_consider, "cap": _curio.cap_ok} if _shop_mode == "pool" else {} var ctx: Dictionary[Variant, Variant] = { - "hero": _hero, + "stats": _stats, + "wallet": _wallet, + "journal": _journal, "loadout": _loadout, "adv_bucket": adventure_counters, "treasure_inventory": adventure_inventory, @@ -379,8 +400,8 @@ func _start_current_level() -> bool: "tree_rules": _tree_rules, "shop_mode": _shop_mode, "treasure_unlocked": _treasure_unlocked, - "hero_attack_type": _hero.attack_type, - "hero_armor_type": _hero.armor_type, + "hero_attack_type": _stats.attack_type, + "hero_armor_type": _stats.armor_type, "voucher_rap": int(outbox.get("rap", 0)), "voucher_time_sand": int(outbox.get("time_sand", 0)), "pool_wiring": pool_wiring, @@ -395,7 +416,7 @@ func _start_current_level() -> bool: resume_state = {"node_id": int(_bp_read("node_id", _resume_bp, -1)), "visited_ids": _bp_read("visited_ids", _resume_bp, []) as Array} # 接口①开始关卡:生命经主角账共享交接(账面当前值即开局生命——冒险开始置满 / 大奖励回满已写好); - # 主角账与冒险桶注入(set_hero——T1 过渡面,T3 收构造注入)。 + # 主角账三件与冒险桶注入(构造注入 ctx——拆件批 T3 收口形态)。 # defer_entry=true 起每关(含首关)——先 emit level_started(呈现层在 # 该回调重取 runner 实例),后入口候选呈现;顺序反了选路口会被呈现层的 runner 空防御静默丢弃。 # 呈现零 RNG 消耗、pick 走同一 _enter_node——测量面逐位零漂移。 @@ -439,7 +460,7 @@ func _on_level_finished(payload: Dictionary) -> void: _last_level_payload = payload.duplicate(true) var result := String(payload.get("result", "")) if result == "defeat": - _hero.hero_hp = 0 # 倒下记 0(生命账本战后回写口径) + _stats.hero_hp = 0 # 倒下记 0(生命账本战后回写口径) _settle("failed") return if result != "victory": @@ -452,9 +473,11 @@ func _on_level_finished(payload: Dictionary) -> void: # 关卡完成兑付(非末关恰一次;三层奖励框架):编排归停点件——推进位先于 emit 置位 #(信号同步派发——处理器内立即确认时 _pending_next_level 必须已就位); # 停点件内部分回血先行 → 加权洗牌掷 3 → 三选一停点(顺序即契约——回血按当刻账本与上限结算, - # 选择停点零自动推进) + # 选择停点零自动推进)。载荷_base 随附钱包读数(停点件零钱包依赖——GP 件入账回调仍走 grant_curio_gp) _pending_next_level = true - _stop.open_level_clear(payload) + var stop_base: Dictionary[Variant, Variant] = payload.duplicate(true) + stop_base["gp"] = _wallet.gp + _stop.open_level_clear(stop_base) return # 末关的关卡完成:不发三选一与部分回血(没有下一战可受益)、Boss 钥石照常入账——冒险完成结算 _settle("completed") @@ -467,13 +490,13 @@ func _settle(result: String) -> void: "cleared": result == "completed", "levels_cleared": levels_total if result == "completed" else level_index - 1, "stopped_at": 0 if result == "completed" else level_index, - "keystone_total": _hero.keystone_gained(), + "keystone_total": _journal.keystone_gained(), "master_seed": master_seed, "aux_cleared_last": _last_treasure_cleared, # additive 扩签(末关清空反馈;非末关走 bonus 载荷) - "keystone_drops": _hero.keystone_drop_log(), # additive 扩签(逐杀掉落明细;settle 入账明细/复盘归因数据源) - "vision_used_delta": _hero.vision_used(), # additive 扩签(真视使用本局计数;settle 聚合入档) + "keystone_drops": _journal.keystone_drop_log(), # additive 扩签(逐杀掉落明细;settle 入账明细/复盘归因数据源) + "vision_used_delta": _journal.vision_used(), # additive 扩签(真视使用本局计数;settle 聚合入档) "pool_stock": _pool_stock_snapshot(), # additive 扩签(终局池读数:清账前快照,商店买走 vs 奖励领走 vs 枯竭的终局面) - "relics": _hero.feats_held(), # additive 扩签(特性终局持有集:清账前快照,成型判读取数面) + "relics": _stats.feats_held(), # additive 扩签(特性终局持有集:清账前快照,成型判读取数面) } # 败退明细(数值验证工具取数面——tools/balance_verifier.py 止步节点分布数据源; # 仅失败局附加,键缺省 = 未败退,既有五件事消费方零感知) @@ -551,7 +574,9 @@ func _snapshot_breakpoint() -> void: # 断点写侧取值(键集以 BREAKPOINT_FIELDS 为准——未登记键即取值越界,fail-fast) func _bp_parts() -> Dictionary[Variant, Variant]: - var hero_frag: Dictionary[Variant, Variant] = _hero.breakpoint_fragment() + 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 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, @@ -560,32 +585,32 @@ func _bp_parts() -> Dictionary[Variant, Variant]: var parts: Dictionary[Variant, Variant] = { "level_index": level_index, "node_id": int(level_frag.get("node_id", -1)), - "hero_hp": int(hero_frag.get("hero_hp", 0)), - "bonus": hero_frag.get("bonus", {}), - "gp": int(hero_frag.get("gp", 0)), - "shop_purchased": hero_frag.get("shop_purchased", {}), + "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(hero_frag.get("keystone", 0)), + "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": hero_frag.get("relics", []), # 特性持有集(局内随机渠道获得态续玩防丢失) + "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": adventure_counters.duplicate(true), # adventure 域计数器随断点(续玩跨战账不丢——桶归宿主字典) - "vision_counter_used": int(hero_frag.get("vision_counter_used", 0)), # 真视使用计数随断点(续玩跨战账不丢) + "vision_counter_used": int(journal_frag.get("vision_counter_used", 0)), # 真视使用计数随断点(续玩跨战账不丢) "refresh_stock": int(stop_frag.get("refresh_stock", 0)), # 刷新存量账随断点 "attune_refresh_y": int(stop_frag.get("attune_refresh_y", 0)), # Y 冒险周期池余量随断点(扩列向后兼容——旧档缺键回退参数面) "bonus_result": stop_frag.get("bonus_result", {}), # 已兑付待推进态(空 = 无——中断续玩不重兑付) - "keystone_drops": hero_frag.get("keystone_drops", []), # 逐杀掉落明细(摘要归因续接) - "gain_journal": hero_frag.get("gain_journal", {}), # 获得日记(唯一声明的断点扩列——日记为正主) + "keystone_drops": journal_frag.get("keystone_drops", []), # 逐杀掉落明细(摘要归因续接) + "gain_journal": journal_frag.get("gain_journal", {}), # 获得日记(唯一声明的断点扩列——日记为现行事实源) } # 拼盘完整性自断言(fail-fast——片段并集与登记键集不符即中止落盘) var stray: Array[Variant] = [] diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index 578affac..8870e383 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -76,7 +76,7 @@ var _seal_consumed := {} # 兑付资格以危险格实例为 # 同调装配运行态(begin_battle 经 enc.attunement 注入;off=空集全部效果零扰动) var attunement: Array[Variant] = [] # 装备中同调件 item_id 集 var loadout = null # 同调配装件(begin_battle 经 enc["loadout"] 持引用;直构 null=零装配——充能面跳过) -var _hero: HeroLedger = null # 主角实体账(begin_battle 经 enc["hero"] 持引用;null=调试/单场直构面——账面消费跳过) +var _hero: GainJournal = null # 加算日记(begin_battle 经 enc["journal"] 持引用;null=调试/单场直构面——账面消费跳过) var _attune_safe_seen := 0 # 积点制件 battle 域安全开格计数(装配集 ap_charge_safe 行扫描;满 charge_n 自动兑付当轮 AP+1,无玩家发起入口) # ---- 效果引擎扩签运行态(新效果类型施加面;空订阅零扰动)---- var _fx_hero_ac_base := -1 # 主角 AC 基线快照(enc.hero_armor_class;-1=表值兜底未解析) @@ -377,7 +377,7 @@ func begin_battle(enc: Dictionary) -> bool: _mines_hit_last = 0 _mines_hit_round = 0 loadout = enc.get("loadout", null) - _hero = enc.get("hero", null) if enc.get("hero", null) is HeroLedger else null # null=调试直构——账面消费跳过 + _hero = enc.get("journal", null) if enc.get("journal", null) is GainJournal 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") if level_ref is Dictionary: diff --git a/game/core/curio_stop.gd b/game/core/curio_stop.gd index bebdbcb9..b1d7d072 100644 --- a/game/core/curio_stop.gd +++ b/game/core/curio_stop.gd @@ -1,11 +1,12 @@ -## 三选一停点件:停点编排唯一所有者(冒险域拆件批自 AdventureRunner 迁入——架构原则 9 本域承载)。 -## 停点三态(关卡完成三选一在途/已兑付待推进/奖励节点三选一在途);刷新三源账(X 刷新件每碰三选一 -## 重给/优先消耗不结转、Y 营地升级行冒险周期池/用完即无、刷新券存量账/冒险级跨停点持久);兑付方法族 -## (pick/refresh/redeem/skip);凭证出箱(RAP/时沙待注入——战首取走清零);三选一信号面自带。 +## 三选一停点件:停点编排唯一所有者(架构原则 9 本域承载)。 +## 停点三态(关卡完成三选一在途/已兑付待推进/奖励节点三选一在途);刷新账三源(X 刷新件每碰三选一 +## 重给/优先消耗不结转、Y 营地升级行冒险周期池/用完即无、刷新券存量账/冒险级跨停点持久——账面归 +## RefreshBank 件、编排留本件);兑付方法族(pick/refresh/redeem/skip);凭证出箱(RAP/时沙待注入 +## ——战首取走清零);三选一信号面自带。 ## 零自动推进停点语义承载:兑付只经 pick/apply 显式入口,停点在途时推进被拒(宿主关序受理读放行谓词)。 ## 生命周期=每冒险一次(AdventureRunner 冒险开始建;关卡换实例不换件——每关注入时点经 rebind_level_runner)。 -## 随机只经注入 CurioDeck 的 reward_pick 封闭流;兑付落账经注入 HeroLedger 单写原语;落盘经注入方法 -## Callable(core↔core 接线禁 lambda 捕获 self)。Core 层零 UI。 +## 随机只经注入 CurioDeck 的 reward_pick 封闭流;兑付落账经注入主角账件(HeroStats/GainJournal) +## 单写原语;落盘经注入方法 Callable(core↔core 接线禁 lambda 捕获 self)。Core 层零 UI。 class_name CurioStop extends RefCounted @@ -14,7 +15,8 @@ signal curio_resolved(result: Dictionary) ## 三选一兑付完成( var db: ConfigDb var _deck: CurioDeck # 令牌池件(抽签/摘牌/cap/凭证判定——纯池) -var _hero: HeroLedger # 主角账(兑付落账单写原语——加算区/生命/特性) +var _stats: HeroStats # 主角数值账(生命/特性——兑付回血与 relic 入集单写原语) +var _journal: GainJournal # 加算日记件(兑付落账单写原语——加算区/钥石读面) var _snapshot_call := Callable() # 落盘回调(宿主断点快照受理——发射点后触发) var _grant_gp := Callable() # 当前关卡对象 grant_curio_gp 方法 Callable(GP 件入账维持现路径与 wallet_changed 发射序——每关注入时点更新,防调死对象) # ---- 停点三态 ---- @@ -32,10 +34,11 @@ var _time_sand_queue := 0 # 时沙待注入(下一战首轮 AP var _level_index := 0 # 兑付载荷关卡序号(宿主每关注入——与 AdventureRunner.level_index 同源单写) -func _init(db_ref: ConfigDb, deck_ref: CurioDeck, hero_ref: HeroLedger, snapshot_call: Callable) -> void: +func _init(db_ref: ConfigDb, deck_ref: CurioDeck, stats_ref: HeroStats, journal_ref: GainJournal, snapshot_call: Callable) -> void: db = db_ref _deck = deck_ref - _hero = hero_ref + _stats = stats_ref + _journal = journal_ref _snapshot_call = snapshot_call @@ -53,10 +56,10 @@ func rebind_level_runner(runner: LevelRunner, level_index: int) -> void: #(顺序即契约——回血按当刻账本与上限结算,选择停点零自动推进)。调用方(宿主关序)须先置 # 「已兑付待推进」关序位(信号同步派发——处理器内立即确认时推进位必须已就位)。 func open_level_clear(payload_base: Dictionary) -> void: - var hp_before := _hero.hero_hp - var max_eff := _hero.effective_max_hp() # 回血封顶口径含调试浮窗 pct(同战斗内上限) + var hp_before := _stats.hero_hp + var max_eff := _stats.effective_max_hp() # 回血封顶口径含调试浮窗 pct(同战斗内上限) var heal := mini(int(floor(float(max_eff) * float(int(db.adventure.clear_heal_pct)) / 100.0)), max_eff - hp_before) - _hero.hero_hp = hp_before + heal + _stats.hero_hp = hp_before + heal _reset_refresh_x() # 碰三选一重置 X 余量(不结转) _paid_refreshes = 0 # 付费刷计数随停点清零(对齐商店进店清零) var pick_payload: Dictionary[Variant, Variant] = { @@ -64,10 +67,10 @@ func open_level_clear(payload_base: Dictionary) -> void: "candidates": draw_candidates("level_clear"), # T4 渠道白名单——关底 Boss 击败=关卡完成渠道只出专长 "heal_gained": heal, "hp_before": hp_before, - "hp_after": _hero.hero_hp, - "keystone_gained": int(_hero.keystone_drop_log().back().get("amount", 0)) if not _hero.keystone_drop_log().is_empty() else 0, - "keystone_total": _hero.keystone_gained(), - "gp": _hero.gp, + "hp_after": _stats.hero_hp, + "keystone_gained": int(_journal.keystone_drop_log().back().get("amount", 0)) if not _journal.keystone_drop_log().is_empty() else 0, + "keystone_total": _journal.keystone_gained(), + "gp": int(payload_base.get("gp", 0)), # 钱包读数由宿主随载荷_base 注入(停点件构造参只收 stats+journal——零钱包依赖) "aux_cleared": int(payload_base.get("aux_cleared", 0)), # additive 扩签(关卡末清空反馈取数) } _pending = {"candidates": (pick_payload["candidates"] as Array).duplicate(true), @@ -109,7 +112,7 @@ func pick_curio(kind: String, tier: String = "", feat_id: String = "") -> bool: if kind == "feat_ticket": return _redeem_feat_ticket() # T5 拿到即开——停点候选替换为专长三选一(零摘牌/不进持有集) var value: int = int(db.adventure.curio_value(kind, tier)) - var hp_before := _hero.hero_hp + var hp_before := _stats.hero_hp if not _apply_curio_pick(kind, tier, value, feat_id): push_error("[停点件] pick_curio 拒绝——未知件: %s|%s|%s" % [kind, tier, feat_id]) return false @@ -150,8 +153,8 @@ func _apply_curio_pick(kind: String, tier: String, value: int, feat_id: String = _bump_taken("time_sand") return true if kind == "relic": - if not _hero.feats.has(feat_id): - _hero.feats.append(feat_id) + if not _stats.feats.has(feat_id): + _stats.feats.append(feat_id) _bump_taken("relic") _bump_taken("relic:" + feat_id) # 身份维已拿计数(cap 消费面) return true @@ -159,7 +162,7 @@ func _apply_curio_pick(kind: String, tier: String, value: int, feat_id: String = # adventure_table.CURIO_APPLY_ROUTES 声明路由(bonus 落点键),加新加算类令牌零代码) var sink := db.adventure.curio_apply_route(kind) if sink != "": - _hero.gain_bonus(sink, value, "curio_pick", kind) # 加算区单写原语(获得日记明细+汇总同拍) + _journal.gain_bonus(sink, value, "curio_pick", kind) # 加算区单写原语(获得日记明细+汇总同拍) _bump_taken(kind) return true # 通道面(副作用形态——显式分支) @@ -169,7 +172,7 @@ func _apply_curio_pick(kind: String, tier: String, value: int, feat_id: String = _grant_gp.call(value) _bump_taken("gp") "heal": - _hero.hero_hp = mini(_hero.hero_hp + value, _hero.effective_max_hp()) + _stats.hero_hp = mini(_stats.hero_hp + value, _stats.effective_max_hp()) _bump_taken("heal") _: return false @@ -190,7 +193,7 @@ func _finish_curio_pick(picked: String, value: int, hp_before: int, tier: String "tier": tier, "value": value, "hp_before": hp_before, - "hp_after": _hero.hero_hp, + "hp_after": _stats.hero_hp, "level_index": _level_index, } _resolved.merge(_feat_payload_keys(feat_id)) diff --git a/game/core/gain_journal.gd b/game/core/gain_journal.gd new file mode 100644 index 00000000..b8c98c08 --- /dev/null +++ b/game/core/gain_journal.gd @@ -0,0 +1,150 @@ +## 加算日记件:统一冒险加算区与获得记录唯一所有者。获得记录三层结构=获得记录列表 +## (_record_gain 追加、返回记录 id)→ 属性增益明细(带来源指回)→ 四轴汇总(由明细同步维护; +## summary 与断点读汇总,日记为现行事实源)。三写通道统一走 gain_bonus 单写原语:三选一属性件 +## (source_class="curio_pick")/商店属性商品("shop_product"、kind=product_id)/战末导出 +## ("battle_export"、kind=轴名)。本局钥石增量与逐杀明细、真视使用记录、商店已购档数记账同域。 +## 生命与钱包各归 HeroStats/GpWallet——主角账三件各交各的断点片段,拼盘落盘归冒险应用层。 +## Core 层零 UI;随机零消费。 +class_name GainJournal +extends RefCounted + +# 加算区四轴(键序即断点 bonus 键序——与拆件前共享账置账形态逐位同形) +const BONUS_AXES: Array[String] = ["atk", "max_hp", "armor_class", "ap"] + +var _gain_log: Array[Variant] = [] # 获得记录列表(三选一件/商店购买/战末奖励各一条) +var _bonus_detail: Array[Variant] = [] # 属性增益明细(带来源指回 gain_id) +var _bonus_total: Dictionary[Variant, Variant] = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} +var _keystone := 0 # 钥石入账累计(摘要第三件事的账面) +var _keystone_drops: Array[Variant] = [] # Boss 逐杀掉落明细(additive 摘要键 keystone_drops) +var _vision_used := 0 # 真视使用记录(本局计数——冒险收束聚合入档) +var _purchased: Dictionary[Variant, Variant] = {} # product_id → 已购档数(递增定价输入;冒险开始清零) + + +# ---- 获得记录三层 ---- + +# 获得记录列表追加(三通道各一条——source_class 词表:curio_pick/shop_product/battle_export; +# 返回记录 id 供明细来源指回) +func _record_gain(source_class: String, kind: String, detail: Dictionary) -> int: + var rec_id := _gain_log.size() + 1 # 记录 id = 列表序派生(1 起——恢复面按 max(id) 校正) + var rec: Dictionary[Variant, Variant] = {"id": rec_id, "source_class": source_class, "kind": kind} + rec.merge(detail.duplicate(true)) + _gain_log.append(rec) + return rec_id + + +# 统一冒险加算区单写原语(三通道统一入口。明细+汇总同步维护——写时同拍,无事后对账) +func gain_bonus(axis: String, value: int, source_class: String, kind: String) -> void: + var gain_id := _record_gain(source_class, kind, {"axis": axis, "value": value}) + _bonus_detail.append({"gain_id": gain_id, "axis": axis, "value": value, + "source_class": source_class, "kind": kind}) + _bonus_total[axis] = int(_bonus_total.get(axis, 0)) + value + + +# 汇总读面(生效公式/断点/摘要唯一取数口) +func bonus_total(axis: String) -> int: + return int(_bonus_total.get(axis, 0)) + + +func bonus_snapshot() -> Dictionary[Variant, Variant]: + return _bonus_total.duplicate(true) + + +# 商店已购档数记账(购买条目——递增定价输入;定价读数走 purchase_tier) +func record_purchase(product_id: String) -> void: + _purchased[product_id] = purchase_tier(product_id) + 1 + + +func purchase_tier(product_id: String) -> int: + return int(_purchased.get(product_id, 0)) + + +# 本局钥石增量(Boss 逐杀掉落渠道——首杀/复杀金额由调用方按存档标记判读后入行) +func record_keystone_drop(drop: Dictionary) -> void: + _keystone_drops.append(drop.duplicate(true)) + _keystone += int(drop.get("amount", 0)) + + +func keystone_gained() -> int: + return _keystone + + +func keystone_drop_log() -> Array: + return _keystone_drops.duplicate(true) + + +# 真视使用记录(真视类工具受理成功恰计一次;冒险收束聚合入档) +func record_vision_use() -> void: + _vision_used += 1 + + +func vision_used() -> int: + return _vision_used + + +# 日记域账面重置(冒险开始/单关重开共用值面——宿主须先调本件再调 HeroStats 置满,生命口径 +# 才不含加算区) +func reset_adventure_accounts() -> void: + _bonus_total = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} + _gain_log = [] + _bonus_detail = [] + _purchased = {} + _keystone = 0 + _keystone_drops = [] + _vision_used = 0 + + +# ---- 断点自交片段(应用层拼盘口收件——本件只交自己的账)---- +# 片段键:bonus(汇总快照)/shop_purchased/keystone/keystone_drops/vision_counter_used +# +获得日记键(唯一声明的断点扩列;旧档缺日记→汇总直载 clamp、在档→明细推导比对不符 +# push_error 拒载) + +func breakpoint_fragment() -> Dictionary[Variant, Variant]: + return { + "bonus": bonus_snapshot(), + "shop_purchased": _purchased.duplicate(true), + "keystone": _keystone, + "keystone_drops": keystone_drop_log(), + "vision_counter_used": _vision_used, + "gain_journal": {"records": _gain_log.duplicate(true), "details": _bonus_detail.duplicate(true)}, + } + + +# 片段回填(日记缺键 = 旧档——汇总直载跳过比对;日记在档 = 由明细推导汇总、与留存汇总不符 +# 即报错中止——日记为现行事实源,汇总仅作一致性校验读数) +func restore_fragment(frag: Dictionary, _params: Dictionary) -> void: + _purchased = (frag.get("shop_purchased", {}) as Dictionary).duplicate(true) + _keystone = int(frag.get("keystone", 0)) + _keystone_drops = (frag.get("keystone_drops", []) as Array).duplicate(true) + _vision_used = maxi(0, int(frag.get("vision_counter_used", 0))) # clamp 类(现役读侧策略同款) + var journal: Variant = frag.get("gain_journal", null) + if journal is Dictionary and not (journal as Dictionary).is_empty(): + _restore_from_journal(journal as Dictionary, frag) + return + # 旧档回退:汇总键直载(clamp 类——四轴子键夹 ≥0) + _bonus_total = {} + var bp_bonus: Dictionary[Variant, Variant] = frag.get("bonus", {}) as Dictionary + for axis in BONUS_AXES: + _bonus_total[axis] = maxi(0, int(bp_bonus.get(axis, 0))) + + +# 日记回填(记录列表与明细全量还原;汇总由明细推导——与留存汇总逐轴比对,不符即致命中止) +func _restore_from_journal(journal: Dictionary, frag: Dictionary) -> void: + _gain_log = (journal.get("records", []) as Array).duplicate(true) + _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: + var axis := String((d as Dictionary).get("axis", "")) + if not derived.has(axis): + push_error("[加算日记] 断点日记明细含未知加算轴: %s(拒绝载入)" % axis) + derived.clear() + break + derived[axis] = int(derived[axis]) + int((d as Dictionary).get("value", 0)) + if derived.is_empty(): + return # 已报错中止(fail-fast——明细非法整面拒绝) + var stored: Dictionary[Variant, Variant] = frag.get("bonus", {}) as Dictionary + for axis in derived.keys(): + if int(stored.get(axis, -1)) != int(derived[axis]): + push_error("[加算日记] 断点汇总与日记推导不一致(axis=%s 汇总=%d 推导=%d)——拒绝载入" + % [axis, int(stored.get(axis, -1)), int(derived[axis])]) + return + _bonus_total = derived diff --git a/game/core/gain_journal.gd.uid b/game/core/gain_journal.gd.uid new file mode 100644 index 00000000..ef8e9175 --- /dev/null +++ b/game/core/gain_journal.gd.uid @@ -0,0 +1 @@ +uid://chssg25j1e75a diff --git a/game/core/gp_wallet.gd b/game/core/gp_wallet.gd new file mode 100644 index 00000000..7a9f35e4 --- /dev/null +++ b/game/core/gp_wallet.gd @@ -0,0 +1,36 @@ +## GP 钱包件:冒险局内货币唯一所有者(gain/spend/clear 单写原语;LevelRunner 与 ShopSession +## 读写全指此单点)。生命与加算日记各归 HeroStats/GainJournal——主角账三件各交各的断点片段, +## 拼盘落盘归冒险应用层。Core 层零 UI;随机零消费。 +class_name GpWallet +extends RefCounted + +var gp: int = 0 # GP 钱包(冒险开始 = 0、收束清零;不入存档不入摘要) + + +func gain_gp(amount: int) -> void: + gp = maxi(0, gp + amount) + + +# 余额不足 false 零副作用(出账前置校验由调用方守卫时本方法不重复拒绝) +func spend_gp(amount: int) -> bool: + if amount <= 0 or gp < amount: + return false + gp = gp - amount + return true + + +func clear_wallet() -> void: + gp = 0 + + +# ---- 断点自交片段(应用层拼盘口收件——本件只交自己的账)---- +# 片段键:gp + +func breakpoint_fragment() -> Dictionary[Variant, Variant]: + return { + "gp": gp, + } + + +func restore_fragment(frag: Dictionary, _params: Dictionary) -> void: + gp = int(frag.get("gp", 0)) diff --git a/game/core/gp_wallet.gd.uid b/game/core/gp_wallet.gd.uid new file mode 100644 index 00000000..48da71b5 --- /dev/null +++ b/game/core/gp_wallet.gd.uid @@ -0,0 +1 @@ +uid://byi84vj4wyg1y diff --git a/game/core/hero_ledger.gd b/game/core/hero_ledger.gd deleted file mode 100644 index 5bc1bbc3..00000000 --- a/game/core/hero_ledger.gd +++ /dev/null @@ -1,273 +0,0 @@ -## 主角实体账:冒险生命周期主角数值与获得记录唯一所有者(冒险域拆件批自 AdventureRunner 迁入)。 -## 生命与 GP 钱包(gain/spend/clear 单写原语);锻造强化快照(永久强化 flat)与调试浮窗百分比通道; -## 攻击/甲类型轴快照;特性持有集(活引用注入——关内兑付当关生效);本局钥石增量与逐杀明细; -## 真视使用记录;统一冒险加算区(商店购买/三选一属性件/战末导出三通道唯一落点)。 -## 获得记录三层结构=获得记录列表(record_gain 追加、返回记录 id)→ 属性增益明细(带来源指回)→ -## 四轴汇总(由明细同步维护;summary 与断点读汇总,日记为正主)。 -## 生效上限/攻击/护甲公式与回血封顶公式单点归本件(与战斗内上限同式同源,pct=0 时乘法恒等——MC 锚零漂移)。 -## Core 层零 UI;随机零消费;断点自交片段(账本键+获得日记键)——拼盘落盘归冒险应用层。 -class_name HeroLedger -extends RefCounted - -# 加算区四轴(键序即断点 bonus 键序——与拆件前共享账置账形态逐位同形) -const BONUS_AXES: Array[String] = ["atk", "max_hp", "armor_class", "ap"] - -var db: ConfigDb -var hero_hp: int = 0 # 生命账(原冒险账本 ledger["hero_hp"] 单值化) -var gp: int = 0 # GP 钱包(原 ledger["gp"]——LevelRunner 与 ShopSession 读写全改指此单点) -var feats: Array[String] = [] # 特性持有集(活引用注入——关内兑付 relic 须当关生效;收束清零) -var attack_type := "" # 攻击类型轴快照(空串=未注入→锻造配置默认值回退) -var armor_type := "" # 甲类型轴快照(同上) -var _base_override: Dictionary[Variant, Variant] = {"atk": 0, "max_hp": 0, "armor_class": 0} -var _override: Array[int] = [0, 0] # 调试浮窗百分比通道 [atk_pct, hp_pct](每关新 runner 应用面) -var _gain_log: Array[Variant] = [] # 获得记录列表(三选一件/商店购买/战末奖励各一条) -var _bonus_detail: Array[Variant] = [] # 属性增益明细(带来源指回 gain_id) -var _bonus_total: Dictionary[Variant, Variant] = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} -var _keystone := 0 # 钥石入账累计(摘要第三件事的账面) -var _keystone_drops: Array[Variant] = [] # Boss 逐杀掉落明细(additive 摘要键 keystone_drops) -var _vision_used := 0 # 真视使用记录(本局计数——冒险收束聚合入档) -var _purchased: Dictionary[Variant, Variant] = {} # product_id → 已购档数(递增定价输入;冒险开始清零) - - -func _init(db_ref: ConfigDb, base_override_ref: Dictionary) -> void: - db = db_ref - _base_override = _normalize_override(base_override_ref) - - -# 永久强化 flat 重注(单关测试模式 start_level 收 base_override_in 时调用——实例不重建,值重注) -func rebase_override(raw: Dictionary) -> void: - _base_override = _normalize_override(raw) - - -# 调试浮窗百分比通道(测试工具覆盖——每关新 runner 都要应用;正式侧归元进度) -func set_debug_override(atk_pct: int, hp_pct: int) -> void: - _override = [atk_pct, hp_pct] - - -func debug_atk_pct() -> int: - return int(_override[0]) - - -func debug_hp_pct() -> int: - return int(_override[1]) - - -# 永久强化 flat 快照读面(续玩生命钳制保守上限用——不含加算区) -func base_override_flat() -> Dictionary[Variant, Variant]: - return _base_override.duplicate(true) - - -# 注入参数规范化:三字段缺键/-1 = 表值兜底(→ 0 flat);负值一律 clamp 0(白板 = 全零合法态) -func _normalize_override(raw: Dictionary) -> Dictionary[Variant, Variant]: - var out: Dictionary[Variant, Variant] = {"atk": 0, "max_hp": 0, "armor_class": 0} - for k in out.keys(): - out[k] = maxi(0, int(raw.get(k, 0))) - return out - - -# ---- 生效公式(单点——叠加次序钉死:表值 → 永久强化 flat → 加算区 flat 先加, -# 调试浮窗百分比后乘;pct=0 时乘法恒等——MC 锚零漂移)---- - -func effective_max_hp() -> int: - var base: int = int(db.hero.max_hp) + int(_base_override.get("max_hp", 0)) + bonus_total("max_hp") - return maxi(1, int(round(float(base) * (100.0 + float(_override[1])) / 100.0))) - - -func effective_atk() -> int: - var base: int = int(db.hero.atk) + int(_base_override.get("atk", 0)) + bonus_total("atk") - return maxi(1, int(round(float(base) * (100.0 + float(_override[0])) / 100.0))) - - -# 护甲等级生效值(入战 AC 三层可加:甲型基础 AC + 甲路线等级加值 + 加算区 armor_class 轴; -# 无调试浮窗百分比通道,纯 flat) -func effective_ac() -> int: - return maxi(0, _armor_base_ac() + int(_base_override.get("armor_class", 0)) + bonus_total("armor_class")) - - -# 甲型基础 AC(三层之第一层——选中甲路线行 base_ac;未注入/缺行=锻造配置默认值路线, -# 与 hero 表两轴删除前 armor_class=10 行为连续) -func _armor_base_ac() -> int: - var at: String = armor_type if armor_type != "" else db.forge_default_armor_type() - var row: Dictionary[Variant, Variant] = db.forge_armor_route_by_type(at) - if row.is_empty(): - return int(db.hero.armor_class) # 表缺失防御兜底(现役基准) - return int(row.get("base_ac", 0)) - - -# 回血公式单点(恢复点节点兑付/账本置满封顶同式;算术宿主——原 LevelRunner.rest_heal 迁入) -static func rest_heal(pct: int, hp: int, max_hp: int) -> int: - return mini(int(floor(float(max_hp) * float(pct) / 100.0)), max_hp - hp) - - -# ---- GP 钱包单写原语 ---- - -func gain_gp(amount: int) -> void: - gp = maxi(0, gp + amount) - - -# 余额不足 false 零副作用(出账前置校验由调用方守卫时本方法不重复拒绝) -func spend_gp(amount: int) -> bool: - if amount <= 0 or gp < amount: - return false - gp = gp - amount - return true - - -func clear_wallet() -> void: - gp = 0 - - -# 单关模式重开重置(值重置非实例重建——shop 绑 hero 对象引用不换代;语义=原单关 start_level 重置面) -func reset_for_single_level() -> void: - _base_override = {"atk": 0, "max_hp": 0, "armor_class": 0} - reset_adventure_accounts() - - -# 冒险账面重置(冒险开始/单关重开共用值面——生命置满基础上限、钱包与账目清零) -func reset_adventure_accounts() -> void: - gp = 0 - _bonus_total = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} - _gain_log = [] - _bonus_detail = [] - _purchased = {} - _keystone = 0 - _keystone_drops = [] - _vision_used = 0 - feats = [] - hero_hp = effective_max_hp() - - -# ---- 获得记录三层 ---- - -# 获得记录列表追加(三通道各一条——source_class 词表:curio_pick/shop_product/battle_export; -# 返回记录 id 供明细来源指回) -func record_gain(source_class: String, kind: String, detail: Dictionary) -> int: - var rec_id := _gain_log.size() + 1 # 记录 id = 列表序派生(1 起——恢复面按 max(id) 校正) - var rec: Dictionary[Variant, Variant] = {"id": rec_id, "source_class": source_class, "kind": kind} - rec.merge(detail.duplicate(true)) - _gain_log.append(rec) - return rec_id - - -# 统一冒险加算区单写原语(三通道统一入口:三选一属性件 source_class="curio_pick"; -# 商店属性商品 source_class="shop_product"、kind=product_id;战末导出 source_class="battle_export"、 -# kind=轴名。明细+汇总同步维护——写时同拍,无事后对账) -func gain_bonus(axis: String, value: int, source_class: String, kind: String) -> void: - var gain_id := record_gain(source_class, kind, {"axis": axis, "value": value}) - _bonus_detail.append({"gain_id": gain_id, "axis": axis, "value": value, - "source_class": source_class, "kind": kind}) - _bonus_total[axis] = int(_bonus_total.get(axis, 0)) + value - - -# 汇总读面(生效公式/断点/摘要唯一取数口) -func bonus_total(axis: String) -> int: - return int(_bonus_total.get(axis, 0)) - - -func bonus_snapshot() -> Dictionary[Variant, Variant]: - return _bonus_total.duplicate(true) - - -# 商店已购档数记账(购买条目——递增定价输入;定价读数走 purchase_tier) -func record_purchase(product_id: String) -> void: - _purchased[product_id] = purchase_tier(product_id) + 1 - - -func purchase_tier(product_id: String) -> int: - return int(_purchased.get(product_id, 0)) - - -func purchased_snapshot() -> Dictionary[Variant, Variant]: - return _purchased.duplicate(true) - - -# 本局钥石增量(Boss 逐杀掉落渠道——首杀/复杀金额由调用方按存档标记判读后入行) -func record_keystone_drop(drop: Dictionary) -> void: - _keystone_drops.append(drop.duplicate(true)) - _keystone += int(drop.get("amount", 0)) - - -func keystone_gained() -> int: - return _keystone - - -func keystone_drop_log() -> Array: - return _keystone_drops.duplicate(true) - - -# 真视使用记录(真视类工具受理成功恰计一次;冒险收束聚合入档) -func record_vision_use() -> void: - _vision_used += 1 - - -func vision_used() -> int: - return _vision_used - - -# 特性持有集读面(效果接线/求值/摘要唯一取数口——副本防外部改写) -func feats_held() -> Array: - return feats.duplicate() - - -# ---- 断点自交片段(应用层拼盘口收件——本件只交自己的账)---- -# 片段键:hero_hp/bonus(汇总快照)/gp/shop_purchased/keystone/keystone_drops/relics/vision_counter_used -# +获得日记键(唯一声明的断点扩列;旧档缺日记→汇总直载跳过比对;日记在档→由日记推导汇总比对) - -func breakpoint_fragment() -> Dictionary[Variant, Variant]: - return { - "hero_hp": hero_hp, - "bonus": bonus_snapshot(), - "gp": gp, - "shop_purchased": purchased_snapshot(), - "keystone": _keystone, - "keystone_drops": keystone_drop_log(), - "relics": feats.duplicate(), - "vision_counter_used": _vision_used, - "gain_journal": {"records": _gain_log.duplicate(true), "details": _bonus_detail.duplicate(true)}, - } - - -# 片段回填(日记缺键 = 旧档——汇总直载跳过比对;日记在档 = 由明细推导汇总、与留存汇总不符 -# 即报错中止——日记为正主,汇总仅作一致性校验读数) -func restore_fragment(frag: Dictionary, _params: Dictionary) -> void: - hero_hp = int(frag.get("hero_hp", 0)) - gp = int(frag.get("gp", 0)) - _purchased = (frag.get("shop_purchased", {}) as Dictionary).duplicate(true) - _keystone = int(frag.get("keystone", 0)) - _keystone_drops = (frag.get("keystone_drops", []) as Array).duplicate(true) - feats = [] - for r in (frag.get("relics", []) as Array): - feats.append(String(r)) - _vision_used = maxi(0, int(frag.get("vision_counter_used", 0))) # clamp 类(现役读侧策略同款) - var journal: Variant = frag.get("gain_journal", null) - if journal is Dictionary and not (journal as Dictionary).is_empty(): - _restore_from_journal(journal as Dictionary, frag) - return - # 旧档回退:汇总键直载(clamp 类——四轴子键夹 ≥0) - _bonus_total = {} - var bp_bonus: Dictionary[Variant, Variant] = frag.get("bonus", {}) as Dictionary - for axis in BONUS_AXES: - _bonus_total[axis] = maxi(0, int(bp_bonus.get(axis, 0))) - - -# 日记回填(记录列表与明细全量还原;汇总由明细推导——与留存汇总逐轴比对,不符即致命中止) -func _restore_from_journal(journal: Dictionary, frag: Dictionary) -> void: - _gain_log = (journal.get("records", []) as Array).duplicate(true) - _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: - var axis := String((d as Dictionary).get("axis", "")) - if not derived.has(axis): - push_error("[主角账] 断点日记明细含未知加算轴: %s(拒绝载入)" % axis) - derived.clear() - break - derived[axis] = int(derived[axis]) + int((d as Dictionary).get("value", 0)) - if derived.is_empty(): - return # 已报错中止(fail-fast——明细非法整面拒绝) - var stored: Dictionary[Variant, Variant] = frag.get("bonus", {}) as Dictionary - for axis in derived.keys(): - if int(stored.get(axis, -1)) != int(derived[axis]): - push_error("[主角账] 断点汇总与日记推导不一致(axis=%s 汇总=%d 推导=%d)——拒绝载入" - % [axis, int(stored.get(axis, -1)), int(derived[axis])]) - return - _bonus_total = derived diff --git a/game/core/hero_ledger.gd.uid b/game/core/hero_ledger.gd.uid deleted file mode 100644 index ceb39ca8..00000000 --- a/game/core/hero_ledger.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cweny8ulalase diff --git a/game/core/hero_stats.gd b/game/core/hero_stats.gd new file mode 100644 index 00000000..aad9753d --- /dev/null +++ b/game/core/hero_stats.gd @@ -0,0 +1,118 @@ +## 主角数值账:冒险生命周期主角数值域唯一所有者(生命账与特性持有集、锻造强化快照与调试浮窗 +## 百分比通道、攻击/甲类型轴快照、生效上限/攻击/护甲公式与回血封顶公式单点、续玩生命钳制收口)。 +## 生效公式消费加算日记域汇总(注入 GainJournal 只读引用——无反向引用不成环);GP 钱包归 +## GpWallet、加算日记归 GainJournal——主角账三件各交各的断点片段,拼盘落盘归冒险应用层。 +## Core 层零 UI;随机零消费。 +class_name HeroStats +extends RefCounted + +var db: ConfigDb +var journal: GainJournal # 加算日记只读引用(生效公式的加算区取数口——构造注入,宿主同源单件) +var hero_hp: int = 0 # 生命账 +var feats: Array[String] = [] # 特性持有集(活引用注入——关内兑付 relic 须当关生效;收束清零) +var attack_type := "" # 攻击类型轴快照(空串=未注入→锻造配置默认值回退) +var armor_type := "" # 甲类型轴快照(同上) +var _base_override: Dictionary[Variant, Variant] = {"atk": 0, "max_hp": 0, "armor_class": 0} +var _override: Array[int] = [0, 0] # 调试浮窗百分比通道 [atk_pct, hp_pct](每关新 runner 应用面) + + +func _init(db_ref: ConfigDb, base_override_ref: Dictionary, journal_ref: GainJournal) -> void: + db = db_ref + journal = journal_ref + _base_override = _normalize_override(base_override_ref) + + +# 永久强化 flat 重注(单关测试模式 start_level 收 base_override_in 时调用——实例不重建,值重注) +func rebase_override(raw: Dictionary) -> void: + _base_override = _normalize_override(raw) + + +# 调试浮窗百分比通道(测试工具覆盖——每关新 runner 都要应用;正式侧归元进度) +func set_debug_override(atk_pct: int, hp_pct: int) -> void: + _override = [atk_pct, hp_pct] + + +# 续玩生命钳制(断点可能带出高于生效上限的账本值——恢复后钳到保守上限:表值+永久强化 flat +# 后乘调试 pct、下限 1;口径不含加算区,加算区恢复后其余战斗照常。原宿主 _resume_params 的 +# mini 式逐字收进) +func clamp_hp_to_resume_cap(db_max_hp: int) -> void: + hero_hp = mini(hero_hp, maxi(1, + int(round(float(int(db_max_hp) + int(_base_override.get("max_hp", 0))) * (100.0 + float(_override[1])) / 100.0)))) + + +# 注入参数规范化:三字段缺键/-1 = 表值兜底(→ 0 flat);负值一律 clamp 0(白板 = 全零合法态) +func _normalize_override(raw: Dictionary) -> Dictionary[Variant, Variant]: + var out: Dictionary[Variant, Variant] = {"atk": 0, "max_hp": 0, "armor_class": 0} + for k in out.keys(): + out[k] = maxi(0, int(raw.get(k, 0))) + return out + + +# ---- 生效公式(单点——叠加次序钉死:表值 → 永久强化 flat → 加算区 flat 先加, +# 调试浮窗百分比后乘;pct=0 时乘法恒等——MC 锚零漂移)---- + +func effective_max_hp() -> int: + var base: int = int(db.hero.max_hp) + int(_base_override.get("max_hp", 0)) + journal.bonus_total("max_hp") + return maxi(1, int(round(float(base) * (100.0 + float(_override[1])) / 100.0))) + + +func effective_atk() -> int: + var base: int = int(db.hero.atk) + int(_base_override.get("atk", 0)) + journal.bonus_total("atk") + return maxi(1, int(round(float(base) * (100.0 + float(_override[0])) / 100.0))) + + +# 护甲等级生效值(入战 AC 三层可加:甲型基础 AC + 甲路线等级加值 + 加算区 armor_class 轴; +# 无调试浮窗百分比通道,纯 flat) +func effective_ac() -> int: + return maxi(0, _armor_base_ac() + int(_base_override.get("armor_class", 0)) + journal.bonus_total("armor_class")) + + +# 甲型基础 AC(三层之第一层——选中甲路线行 base_ac;未注入/缺行=锻造配置默认值路线, +# 与 hero 表两轴删除前 armor_class=10 行为连续) +func _armor_base_ac() -> int: + var at: String = armor_type if armor_type != "" else db.forge_default_armor_type() + var row: Dictionary[Variant, Variant] = db.forge_armor_route_by_type(at) + if row.is_empty(): + return int(db.hero.armor_class) # 表缺失防御兜底(现役基准) + return int(row.get("base_ac", 0)) + + +# 回血公式单点(恢复点节点兑付/账本置满封顶同式;算术宿主——原 LevelRunner.rest_heal 迁入) +static func rest_heal(pct: int, hp: int, max_hp: int) -> int: + return mini(int(floor(float(max_hp) * float(pct) / 100.0)), max_hp - hp) + + +# 单关模式重开重置(值重置非实例重建——shop 绑定引用不换代;语义=原单关 start_level 重置面。 +# 钱包/日记清零各归各件,由宿主在本调用前完成——日记先清再置满,生命口径才不含加算区) +func reset_for_single_level() -> void: + _base_override = {"atk": 0, "max_hp": 0, "armor_class": 0} + reset_adventure_accounts() + + +# 冒险账面重置(冒险开始/单关重开共用值面——生命置满基础上限、特性持有集清零) +func reset_adventure_accounts() -> void: + feats = [] + hero_hp = effective_max_hp() + + +# 特性持有集读面(效果接线/求值/摘要唯一取数口——副本防外部改写) +func feats_held() -> Array: + return feats.duplicate() + + +# ---- 断点自交片段(应用层拼盘口收件——本件只交自己的账)---- +# 片段键:hero_hp/relics(其余账面键归 GpWallet/GainJournal 各自片段) + +func breakpoint_fragment() -> Dictionary[Variant, Variant]: + return { + "hero_hp": hero_hp, + "relics": feats.duplicate(), + } + + +# 片段回填(生命直载;特性持有集逐项归一 String——旧档防御同款) +func restore_fragment(frag: Dictionary, _params: Dictionary) -> void: + hero_hp = int(frag.get("hero_hp", 0)) + feats = [] + for r in (frag.get("relics", []) as Array): + feats.append(String(r)) diff --git a/game/core/hero_stats.gd.uid b/game/core/hero_stats.gd.uid new file mode 100644 index 00000000..6029a635 --- /dev/null +++ b/game/core/hero_stats.gd.uid @@ -0,0 +1 @@ +uid://boeklf03gti5b diff --git a/game/core/level_runner.gd b/game/core/level_runner.gd index 8cecdad1..cb860f5f 100644 --- a/game/core/level_runner.gd +++ b/game/core/level_runner.gd @@ -5,7 +5,7 @@ ## 调试浮窗百分比通道单点归主角账(set_hero_override 转调——公式消费面同源)。 ## 信号顺序契约:呈现层须在 level_started 回调重取 runner 实例,choices_presented 才不会被空防御丢弃。 ## 关卡开始信号前零发射:本件全部信号在 level_started 发射前保持静默(构造与 start 收参零信号—— -## defer_entry 两步式以 level_started 为界,先广播后呈现入口候选;配断言面见冒烟源码断言〔拆件批 V5〕)。 +## defer_entry 两步式以 level_started 为界,先广播后呈现入口候选;配断言面见冒烟源码断言)。 class_name LevelRunner extends RefCounted @@ -27,18 +27,20 @@ var node_index := -1 var level_counters: Dictionary[Variant, Variant] = {} # 关卡桶(lvl_ 前缀计数器;每关随 LevelRunner new 天然空=按关清零,替代旧硬编码清除名单) var _rest_handler: Callable # 治疗封顶回报执行体(默认内置 rest_heal_amount;冒险模式 = AdventureRunner.apply_rest——接口③) var _external := false # 冒险模式标记(钱包收束时机与播种语义分叉点) -# 主角账引用(拆件批 T1 过渡面:冒险模式经 set_hero 注入共享实体账、单关模式 _init 自建私有账; -# 构造注入收口归 T3 ctx 面——后置 setter 随构造注入一并退役) -var _hero: HeroLedger = HeroLedger.new(null, {}) # 直构缺省私有账(db 随 _init 补) +# 主角账三件引用(领域拆分批三件化:冒险模式经 ctx 注入共享三件、单关模式 _init 自建私有件; +# 数值账消费日记汇总——私有默认件同源成对建) +var _journal: GainJournal +var _wallet: GpWallet +var _stats: HeroStats var loadout = null # 同调配装件(构造注入;直构=单关零件) -var _adv_bucket: Dictionary[Variant, Variant] = {} # 冒险桶(效果引擎 adv_ 计数器宿主字典——冒险模式随 hero 注入;单关=本实例自有) -# hero_hp / gp 属性化(既有读写点零改动——值类型经属性落主角账) +var _adv_bucket: Dictionary[Variant, Variant] = {} # 冒险桶(效果引擎 adv_ 计数器宿主字典——冒险模式随 ctx 注入;单关=本实例自有) +# hero_hp / gp 属性化(既有读写点零改动——值类型经属性落主角账三件) var hero_hp: int: - get: return _hero.hero_hp - set(v): _hero.hero_hp = v + get: return _stats.hero_hp + set(v): _stats.hero_hp = v var gp: int: # GP 冒险钱包(局内货币——冒险开始 = 0、收束清零;不入存档不入摘要) - get: return _hero.gp - set(v): _hero.gp = v + get: return _wallet.gp + set(v): _wallet.gp = v var _shop: ShopSession = null # 商店件(_init 建;双模式货架/定价/购买/刷新唯一宿主) # 协作对象私有(E1/E3 封装收口):settlement 读取经 current_battle() 成文出口 var _settlement: BattleOrchestrator = null # 当前战斗编排器(关卡结束置 null) @@ -60,10 +62,10 @@ var visited: Array[Dictionary] = [] # 已兑付/到达节点 var _generator # 参数化长图生成器(惰性建——NodeMapGenerator 持注入的 RngService) -# 构造注入一次给齐(拆件批 T3——后置 setter 群与商店重绑机制退役;_bind_shop 全仓仅本处构造调用)。 +# 构造注入一次给齐(后置 setter 群与商店重绑机制退役;_bind_shop 全仓仅本处构造调用)。 # ctx 键(冒险模式由 AdventureRunner._start_current_level 组包;缺省空字典 = 单关测试模式: -# 私有主角账自建〔空 override〕、充能账瘦壳自建、冒险桶本实例自有、pool 模式零注入): -# hero:HeroLedger(共享主角账——活引用)/loadout:AttuneLoadout(同调配装件)/adv_bucket:Dictionary(冒险桶宿主字典) +# 私有主角账三件自建〔空 override、数值账同源消费私有日记〕、冒险桶本实例自有、pool 模式零注入): +# stats:HeroStats/wallet:GpWallet/journal:GainJournal(共享主角账三件——活引用)/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(凭证出箱注入面) @@ -71,11 +73,16 @@ var _generator # 参数化长图生成器(惰性建——NodeMapGenerator func _init(db_ref: ConfigDb, rng_ref: RngServiceCore, ctx: Dictionary = {}) -> void: db = db_ref rng = rng_ref - if ctx.get("hero", null) is HeroLedger: - _hero = ctx["hero"] - _feats = _hero.feats # 特性集活引用注入(关内兑付 relic 当关生效——契约 6) + if ctx.get("stats", null) is HeroStats: + _stats = ctx["stats"] + _wallet = ctx["wallet"] if ctx.get("wallet", null) is GpWallet else GpWallet.new() + _journal = ctx["journal"] if ctx.get("journal", null) is GainJournal else GainJournal.new() + _feats = _stats.feats # 特性集活引用注入(关内兑付 relic 当关生效——契约 6) else: - _hero.db = db # 直构私有账补表引用(单关模式——实例不重建,值重置走 start_level) + _journal = GainJournal.new() # 直构私有三件(单关模式——实例不重建,值重置走 start_level) + _wallet = GpWallet.new() + _stats = HeroStats.new(null, {}, _journal) + _stats.db = db # 直构私有账补表引用(单关模式——实例不重建,值重置走 start_level) 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: @@ -105,31 +112,31 @@ func _is_running() -> bool: # ---- 测试面板覆盖(模拟天赋树属性提升——仅测试用,正式侧归 元进度;经 encounter 传入战斗)---- func set_hero_override(atk_pct: int, hp_pct: int) -> void: - _hero.set_debug_override(atk_pct, hp_pct) + _stats.set_debug_override(atk_pct, hp_pct) -# 生效公式单点归主角账(拆件批 T1——本类删公式本体,读面转调;叠加次序注释随公式迁主角账) +# 生效公式单点归数值账(本类删公式本体,读面转调;叠加次序注释随公式迁数值账) func effective_atk() -> int: - return _hero.effective_atk() + return _stats.effective_atk() func effective_max_hp() -> int: - return _hero.effective_max_hp() + return _stats.effective_max_hp() func effective_ac() -> int: - return _hero.effective_ac() + return _stats.effective_ac() -# 行动点加成生效值(统一冒险加算区第四轴 ap;经战斗开始入参 hero_ap_bonus +# 行动点加成生效值(统一冒险加算区第四轴;经战斗开始入参 hero_ap_bonus # 传入战斗域,兑现 = 每场战斗开始首轮行动点基线 +加成值〔行动经济 GDD 跨冒险行动点加成轴节〕) func effective_ap_bonus() -> int: - return maxi(0, _hero.bonus_total("ap")) + return maxi(0, _journal.bonus_total("ap")) # 覆盖复位(关卡结束归零——表值不可变,仅清百分比;调用方兼容保留) func restore_hero_base() -> void: - _hero.set_debug_override(0, 0) + _stats.set_debug_override(0, 0) # ---- 关卡生命周期 ---- @@ -152,8 +159,10 @@ func start_level(level_index: int, seed_val: int, base_override_in: Dictionary = _phase = Phase.IDLE _candidates = [] visited = [] - _hero.rebase_override(base_override_in) # 永久强化 flat 重注(规范化归主角账——契约同冒险开始参数面) - _hero.reset_for_single_level() # 账面值重置(钱包/加算区/已购档/钥石/日记清零+生命置满——实例不重建,shop 绑定不换代) + _stats.rebase_override(base_override_in) # 永久强化 flat 重注(规范化归数值账——契约同冒险开始参数面) + _journal.reset_adventure_accounts() # 日记域账面清零(先于置满——生命口径不含加算区) + _wallet.clear_wallet() # 钱包清零 + _stats.reset_for_single_level() # 数值账面重置(override 清零+生命置满+特性清零——实例不重建,shop 绑定不换代) rng.new_adventure(seed_val) # 关卡起点 = 冒险起点(切片口径;跨关接续归 冒险结构) if not _assign_generated_graph(row, seed_val): _phase = Phase.ENDED @@ -171,7 +180,7 @@ func start_level(level_index: int, seed_val: int, base_override_in: Dictionary = # 种子域归冒险结构:本路径不重播种;账本归冒险结构:本路径不重置 gp/加算区/已购档, # 也不重置 hero_hp——账本当前值即开局生命,AdventureRunner 已在主角账写好)。 # 流程与 start_level 同款(_find_level_row → 生成图装配 → 状态初始化 → _enter_node(入口)), -# 差异仅在恢复执行体的注入与零重置(主角账/冒险桶经 set_hero 注入——T1 过渡面,T3 收构造)。 +# 差异仅在恢复执行体的注入与零重置(主角账三件/冒险桶经 ctx 构造注入)。 # defer_entry 尾参(缺省 false = 直接进节点)——true 时不自动 # _enter_node(entry),改置选路态(awaiting_choice + 单候选 = 入口节点),由 AdventureRunner 在 # level_started 之后调 present_entry_choice() 呈现、玩家 pick_next_node 显式进首战。拆两步的 @@ -302,7 +311,7 @@ func breakpoint_fragment() -> Dictionary[Variant, Variant]: # "visited_ids": Array[int], "aux_items": Array};空字典 = 既有路径逐位不变)。 # 返回 resume_state 形态供 start_level_external 分派(_restore_to_stop / restore_treasure_items)。 func restore_fragment(resume_state: Dictionary) -> Dictionary: - var aux: Array = resume_state.get("aux_items", []) as Array + var aux: Array[Variant] = resume_state.get("aux_items", []) as Array if not aux.is_empty(): restore_treasure_items(aux) return resume_state @@ -526,7 +535,7 @@ func _start_battle(node: Dictionary) -> bool: "attack_type": _hero_attack_type, "armor_type": _hero_armor_type, # 两根类型轴(spawn 英雄快照;空串=锻造配置默认值回退) "attunement": _attunement, "loadout": loadout, # 同调配装件下传(充能单源化——reset_cycle/consume_charge 消费面;直构 null=零装配) - "hero": _hero, # 主角实体账下传(战末 bonus 汇入/真视计数单写原语消费面) + "journal": _journal, # 加算日记下传(战末 bonus 汇入/真视计数单写原语消费面) "adv_bucket": _adv_bucket, # 冒险桶下传(效果引擎 adv_ 计数器宿主字典——跨战持久同一引用) "level_counters": level_counters, # 关卡桶下传(同关跨战共享引用、随本类 new 天然空) "feats": _feats, @@ -661,9 +670,14 @@ func feats_held() -> Array: return _feats.duplicate() -# 主角账只读出口(加算区生产读面承接—— AdventureRunner.hero() 同款组件交出) -func hero() -> HeroLedger: - return _hero +# 主角数值账只读出口(组件交出—— AdventureRunner.stats() 同款;v1.3 hero() 换代) +func stats() -> HeroStats: + return _stats + + +# 加算日记只读出口(bonus 生产读面承接——呈现层选路屏/结算行经此读加算区) +func journal() -> GainJournal: + return _journal # 凭证出箱注入面(储备金袋 RAP/时沙 首轮 AP;节点级一次性,battle begin 消费后归零;缺省 0=零扰动) @@ -710,10 +724,11 @@ func close_wallet() -> void: # 实装归 ShopSession 件(双模式货架/定价/购买/刷新唯一宿主);本类保留同签名委托与件注入/ # 白盒入口——外部调用面与断点取数面零变化。 -# 件注入(建件与宿主引用换代时调用——钱包/主角账/图行/装配集为共享引用,出账直接落同一引用) +# 件注入(建件与宿主引用换代时调用——钱包/主角账三件/图行/装配集为共享引用,出账直接落同一引用) func _bind_shop() -> void: _shop.bind({ - "hero": _hero, + "wallet": _wallet, + "journal": _journal, "level_row": level_row, "tree_rules": _tree_rules, "attunement": _attunement, @@ -730,7 +745,7 @@ func _emit_wallet_event(event: String, gp_now: int, extra: Dictionary) -> void: # 购血(商店 max_hp 商品——当前生命 +Δ 并封顶新上限,与账本置满/关卡完成回血同式) func _gain_max_hp_flat(flat: int) -> void: - hero_hp = mini(hero_hp + flat, effective_max_hp()) + _stats.hero_hp = mini(_stats.hero_hp + flat, _stats.effective_max_hp()) func shop_products() -> Array: diff --git a/game/core/shop_session.gd b/game/core/shop_session.gd index 55c966fa..028a98c6 100644 --- a/game/core/shop_session.gd +++ b/game/core/shop_session.gd @@ -1,6 +1,6 @@ ## VERTICAL SLICE - NOT FOR PRODUCTION ## 商店会话件:关卡商店双模式(legacy 固定三件加定价/pool 共享令牌池)的货架、定价、购买与刷新唯一宿主。 -## 钱包/主角账/装配集归宿主(HeroLedger 共享引用经 bind 注入——GP 读写与购买加算走主角账单点); +## 钱包/主角账/装配集归宿主(GpWallet/GainJournal 共享引用经 bind 注入——GP 读写走钱包、购买加算走日记单点); ## Core 层零 UI 依赖;公开面由 LevelRunner 同签名委托承载(外部调用面零变化); ## legacy 模式零 RNG 零池消费(off 对照承载)。 class_name ShopSession @@ -63,7 +63,7 @@ func _spend_discounted(price: int) -> int: func purchased_tier(product_id: String) -> int: - return _hero().purchase_tier(product_id) + return _journal().purchase_tier(product_id) # 购买(未知商品/属性键出域/余额不足拒绝——返回 false 零副作用): @@ -74,7 +74,7 @@ func buy_product(product_id: String) -> bool: push_error("[关卡节点地图] 商店房未知商品: %s" % product_id) return false var attr := String(prow.get("attr", "")) - if not HeroLedger.BONUS_AXES.has(attr): + if not GainJournal.BONUS_AXES.has(attr): push_error("[关卡节点地图] 商品属性键出域: %s(attr=%s)" % [product_id, attr]) return false var price := current_price(prow) @@ -83,10 +83,10 @@ func buy_product(product_id: String) -> bool: return false # 余额不足(覆盖层按钮禁用为常态防线——此处兜底拒绝+静默留痕) _set_gp(_gp() - price) var flat := int(prow.get("flat_value", 0)) - _hero().gain_bonus(attr, flat, "shop_product", product_id) # 加算区单写原语(明细+汇总同拍——wr1-s 同值承载) + _journal().gain_bonus(attr, flat, "shop_product", product_id) # 加算区单写原语(明细+汇总同拍——wr1-s 同值承载) if attr == "max_hp": (_hooks["max_hp_gain"] as Callable).call(flat) # 购血:上限与当前生命同步 +Δ(封顶新上限) - _hero().record_purchase(product_id) + _journal().record_purchase(product_id) _emit("buy", {"product": product_id, "price": price}) return true @@ -307,18 +307,22 @@ func shop_bp_visit_buys() -> int: return visit_buys -# ---- 宿主引用读取(共享引用绑定面——钱包/账类/图行/装配集)---- +# ---- 宿主引用读取(共享引用绑定面——钱包/加算日记/图行/装配集)---- func _gp() -> int: - return _hero().gp + return _wallet().gp func _set_gp(v: int) -> void: - _hero().gp = v + _wallet().gp = v -func _hero() -> HeroLedger: - return _refs["hero"] as HeroLedger +func _wallet() -> GpWallet: + return _refs["wallet"] as GpWallet + + +func _journal() -> GainJournal: + return _refs["journal"] as GainJournal func _level_index() -> int: diff --git a/game/presentation/hud_presenter.gd b/game/presentation/hud_presenter.gd index eb11f049..408bc94f 100644 --- a/game/presentation/hud_presenter.gd +++ b/game/presentation/hud_presenter.gd @@ -17,7 +17,7 @@ static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApB # 共账本〔level_runner bonus{atk,max_hp,armor_class}〕;空加算=整节隐藏〔视图内空态纪律〕。 # 呈现只读投影零核心触碰;行文格式=白名单项) if runner != null and level_active: - var b: Dictionary[Variant, Variant] = runner.hero().bonus_snapshot() # 加算区住主角账 + var b: Dictionary[Variant, Variant] = runner.journal().bonus_snapshot() # 加算区住加算日记 var lines: Array[String] = [] if int(b.get("atk", 0)) != 0: lines.append("攻击 %+d" % int(b.get("atk", 0))) @@ -310,7 +310,7 @@ static func gp_summary_text(payload: Dictionary, runner: Variant, save_manager: static func bonus_summary_text(runner: Variant) -> String: if runner == null: return "" - var b: Dictionary[Variant, Variant] = runner.hero().bonus_snapshot() # 加算区住主角账 + var b: Dictionary[Variant, Variant] = runner.journal().bonus_snapshot() # 加算区住加算日记 var parts: Array[String] = [] if int(b.get("atk", 0)) != 0: parts.append("攻 %+d" % int(b.get("atk", 0))) diff --git a/game/presentation/main_adventure_flow.gd b/game/presentation/main_adventure_flow.gd index 4d2db8f4..f0c2a4ea 100644 --- a/game/presentation/main_adventure_flow.gd +++ b/game/presentation/main_adventure_flow.gd @@ -31,8 +31,8 @@ static func _on_adventure_level_started(root: Variant, level_index: int, display var ov: Dictionary[Variant, Variant] = root.meta.hero_override() var boosted: bool = int(ov.get("atk", 0)) > 0 or int(ov.get("max_hp", 0)) > 0 \ or root.debug_overlay.atk_pct() > 0 or root.debug_overlay.hp_pct() > 0 \ - or root.run_scope.runner.hero().bonus_total("atk") > 0 \ - or root.run_scope.runner.hero().bonus_total("max_hp") > 0 + or root.run_scope.runner.journal().bonus_total("atk") > 0 \ + or root.run_scope.runner.journal().bonus_total("max_hp") > 0 root.log_view.append_bb(root.Narrator.level_open_line(display_name, int(root.run_scope.adventure.master_seed), root.run_scope.runner.effective_atk(), root.run_scope.runner.effective_max_hp(), boosted)) @@ -217,9 +217,9 @@ static func _exit_level_screen(root: Variant) -> void: # 选路屏状态栏装配(视图零核心态拼装——architecture §1.3:读核心态→拼展示文案集中于装配器)。 # 打开本屏/兑付确认回执后由组合根取成品喂入 apply_status(刷新时点与原视图内自刷一致)。 static func level_map_status_payload(runner: LevelRunner) -> Dictionary: - var atk_bonus := runner.hero().bonus_total("atk") # 加算区住主角账(读面经 hero() 出口承接) - var ac_bonus := runner.hero().bonus_total("armor_class") - var hp_bonus := runner.hero().bonus_total("max_hp") + var atk_bonus := runner.journal().bonus_total("atk") # 加算区住加算日记(读面经 journal() 出口承接) + var ac_bonus := runner.journal().bonus_total("armor_class") + var hp_bonus := runner.journal().bonus_total("max_hp") var atk_txt := "%d" % runner.effective_atk() if atk_bonus > 0: atk_txt += " +%d" % atk_bonus # 加成角标(ASCII 加号——防像素字体缺 ⁺ 字形出豆腐块) diff --git a/game/presentation/main_debug_flow.gd b/game/presentation/main_debug_flow.gd index db041a85..ae2b77a4 100644 --- a/game/presentation/main_debug_flow.gd +++ b/game/presentation/main_debug_flow.gd @@ -58,7 +58,7 @@ static func _debug_run_data(root: Variant) -> Dictionary: if r == null: return {"seed": seed_txt, "level": "—", "node": "—", "hp": "—", "gp": "—", "keystone": "—"} return {"seed": seed_txt, "level": int(root.run_scope.adventure.level_index), "node": int(r.node_index), - "hp": "%d/%d" % [int(r.hero_hp), int(r.effective_max_hp())], "gp": int(r.gp), "keystone": root.run_scope.adventure.hero().keystone_gained()} + "hp": "%d/%d" % [int(r.hero_hp), int(r.effective_max_hp())], "gp": int(r.gp), "keystone": root.run_scope.adventure.journal().keystone_gained()} diff --git a/game/probe/ui_click_probe.gd b/game/probe/ui_click_probe.gd index 9b3a5166..c0069530 100644 --- a/game/probe/ui_click_probe.gd +++ b/game/probe/ui_click_probe.gd @@ -713,7 +713,7 @@ func _ready() -> void: while ol_r_wait < 24 and String(main.log_view._log.get_parsed_text()).count("· 种子 ") < 1: ol_r_wait += 1 await get_tree().process_frame - if main.run_scope.runner == null or main.run_scope.runner.hero().bonus_total("atk") != 5: + if main.run_scope.runner == null or main.run_scope.runner.journal().bonus_total("atk") != 5: _problems.append("开场行路径②③前提失守:续玩重建后加算区 atk!=5(断点回填链断)") if ol_r_wait >= 24: _problems.append("开场行路径②③失守:续玩重建未发开场行(选路停点形应重发——形态/驱动复核)") diff --git a/game/probe/ui_tune_probe.gd b/game/probe/ui_tune_probe.gd index 6e698c61..51c39b7e 100644 --- a/game/probe/ui_tune_probe.gd +++ b/game/probe/ui_tune_probe.gd @@ -144,8 +144,8 @@ func _ready() -> void: problems.append("当局数据 HP 失对(%s)" % str(rd.get("hp"))) if int(rd.get("gp", -1)) != int(r.gp): problems.append("当局数据 GP 失对(%s ≠ %s)" % [str(rd.get("gp")), str(r.gp)]) - if int(rd.get("keystone", -1)) != main.run_scope.adventure.hero().keystone_gained(): - problems.append("当局数据钥石失对(%s ≠ %s)" % [str(rd.get("keystone")), str(main.run_scope.adventure.hero().keystone_gained())]) + if int(rd.get("keystone", -1)) != main.run_scope.adventure.journal().keystone_gained(): + problems.append("当局数据钥石失对(%s ≠ %s)" % [str(rd.get("keystone")), str(main.run_scope.adventure.journal().keystone_gained())]) var close_btn: Button = ProbeUiUtil.find_button(main.debug_overlay, "关闭", "prefix") if close_btn == null: problems.append("浮窗关闭钮缺失(真实收窗路径中断)") diff --git a/game/sim/level_autoplay.gd b/game/sim/level_autoplay.gd index d7cfb9ce..6bf19d7a 100644 --- a/game/sim/level_autoplay.gd +++ b/game/sim/level_autoplay.gd @@ -224,7 +224,7 @@ static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr, logger.attach_battle(r.current_battle()) if t == "boss": st.boss_depth = int(node.get("depth", -1)) - st.hp_at_boss = adr.hero().hero_hp + st.hp_at_boss = adr.stats().hero_hp elif t == "reward": # 奖励节点三选一(共享池确定性策略;TP0 起 316-f 判读:1 级最稀有、 # 同档并列取最左;替换放弃新件;兑付后 continue_after_node 回图选路) @@ -274,7 +274,7 @@ static func _on_adventure_wallet(event: String, gp_val: int, extra: Dictionary, # 奖励事件随 reward_events 留档) static func _on_adventure_bonus(payload: Dictionary, st: Dictionary, adr) -> void: var pl := payload.duplicate(true) - pl["bonus_max_hp"] = adr.hero().bonus_total("max_hp") # 观测面:停点时刻加算区快照(回血精确断言用) + pl["bonus_max_hp"] = adr.journal().bonus_total("max_hp") # 观测面:停点时刻加算区快照(回血精确断言用) st.reward_events.append({"curio_pick": pl}) var candidates: Array = payload.get("candidates", []) as Array if candidates.is_empty(): diff --git a/game/tests/integration/cross_system_flow_test.gd b/game/tests/integration/cross_system_flow_test.gd index dc5dc282..89b7bc25 100644 --- a/game/tests/integration/cross_system_flow_test.gd +++ b/game/tests/integration/cross_system_flow_test.gd @@ -86,7 +86,7 @@ func _feats_breakpoint_roundtrip() -> void: var sm_r = load("res://core/save_manager.gd").new(bp_path) sm_r.load_from_disk() a_r.set_save_manager(sm_r) - a_r._on_level_finished({"result": "victory", "hp": int(a_r.hero().hero_hp), "node_index": 1, "node_id": 1, "node_type": "shop"}) + a_r._on_level_finished({"result": "victory", "hp": int(a_r.stats().hero_hp), "node_index": 1, "node_id": 1, "node_type": "shop"}) var bp_r: Dictionary = sm_r.breakpoint_snapshot() Kit.check("断点附随件:relics 持有集入快照(迭代 12 T1 扩列)", (bp_r.get("relics", []) as Array).has("x3")) Kit.check("断点附随件:adventure 计数器账本键在案(T6/T7 扩列)", bp_r.has("adventure_counters")) diff --git a/game/tests/unit/core/adventure_feats_test.gd b/game/tests/unit/core/adventure_feats_test.gd index 90fd57cd..c88a601c 100644 --- a/game/tests/unit/core/adventure_feats_test.gd +++ b/game/tests/unit/core/adventure_feats_test.gd @@ -353,7 +353,7 @@ func _smoke_adventure() -> void: l2_price_bonus = int(pb.get("bonus", 0)) Kit.check("进店余额 ≥ hp_up L2 首价(%d = 基价 %d + 关卡加成 %d——配置推导)" % [hp_price + l2_price_bonus, hp_price, l2_price_bonus], gp0 >= hp_price + l2_price_bonus) Kit.check("购买 hp_up 受理(出账 + 加算区)", r8.buy_product("hp_up")) - var acc_shop: int = a8.hero().bonus_total("max_hp") # L1 三选一后 + 商店 3(首件随种子) + var acc_shop: int = a8.journal().bonus_total("max_hp") # L1 三选一后 + 商店 3(首件随种子) Kit.check("统一冒险加算区含商店值(max_hp=%d ≥ 商店 3——L1 三选一件值另计)" % acc_shop, acc_shop >= 3) a8._on_level_finished({"result": "victory", "hp": int(r8.hero_hp), "node_index": r8.node_index, "node_id": r8.current_node_id(), "node_type": "shop"}) # 模拟 L2 关卡完成(victory 载荷直调——非末关) @@ -364,9 +364,9 @@ func _smoke_adventure() -> void: Kit.check("三选一点选兑付受理(候选取首件)", a8.stop().pick_curio(String(l2_pick.get("kind", "")), String(l2_pick.get("tier", "")), String(l2_pick.get("feat_id", "")))) var picked2: String = String(a8.stop()._resolved.get("picked", "")) var expect_acc: int = acc_shop + (int(a8.stop()._resolved.get("value", 0)) if picked2 == "max_hp" else 0) - Kit.check("三选一属性件与商店购买同账累加(max_hp=%d = %d + 三选一%s)" % [a8.hero().bonus_total("max_hp"), acc_shop, picked2], a8.hero().bonus_total("max_hp") == expect_acc) + Kit.check("三选一属性件与商店购买同账累加(max_hp=%d = %d + 三选一%s)" % [a8.journal().bonus_total("max_hp"), acc_shop, picked2], a8.journal().bonus_total("max_hp") == expect_acc) var max_eff8: int = a8.effective_max_hp() # 账本生效上限含调试浮窗 pct(a8 已 set 400) - Kit.check("关卡完成部分回血封顶(hp %d ≤ 生效上限 %d——血回满语义退役)" % [a8.hero().hero_hp, max_eff8], a8.hero().hero_hp <= max_eff8) + Kit.check("关卡完成部分回血封顶(hp %d ≤ 生效上限 %d——血回满语义退役)" % [a8.stats().hero_hp, max_eff8], a8.stats().hero_hp <= max_eff8) Kit.check("兑付后推进受理(下一关开始)", a8.continue_to_next_level()) # ⑨ 恢复点数学(接口③封顶公式三读数 + 同式两落点一致) var a9 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(99)) @@ -416,8 +416,8 @@ func _smoke_adventure() -> void: a12.start_adventure(12) var cap := {"payload": {}} a12.stop().curio_choice_presented.connect(func(p): cap["payload"] = p) # 停点件信号面(拆件批 T3 直连) - var max_full: int = int(_db.hero.max_hp) + int(a12.hero().base_override_flat().get("max_hp", 0)) - a12.hero().hero_hp = max_full + var max_full: int = a12.stats().effective_max_hp() # 白板局无 override 无加算——置满口径=生效上限 + a12.stats().hero_hp = max_full a12._on_level_finished({"result": "victory", "hp": max_full, "node_index": 1, "node_id": 1, "node_type": "shop"}) Kit.check("满血关卡完成回血 0 合法(封顶口径——heal_gained=0 非负溢出)", int(cap["payload"].get("heal_gained", -1)) == 0) # ⑬ 奖励节点共享池(同库同池同摘牌;TP0:attack 令牌耗尽后节点域同样零出现) @@ -436,8 +436,8 @@ func _smoke_adventure() -> void: Kit.check("奖励节点共享池:令牌耗尽同样不出候选(同库同池——%s)" % str(ncands13), not has_attack13) a13.stop().present_node_curio(ncands13) a13.stop().present_node_curio([{"kind": "gp", "tier": "common"}]) - var gp13: int = a13.hero().gp - Kit.check("奖励节点 GP 件入账(钱包「奖励入账」收口——+%d)" % int(_db.adventure.curio_value("gp", "common")), a13.stop().apply_node_curio("gp", "common") and a13.hero().gp == gp13 + int(_db.adventure.curio_value("gp", "common"))) + var gp13: int = a13.wallet().gp + Kit.check("奖励节点 GP 件入账(钱包「奖励入账」收口——+%d)" % int(_db.adventure.curio_value("gp", "common")), a13.stop().apply_node_curio("gp", "common") and a13.wallet().gp == gp13 + int(_db.adventure.curio_value("gp", "common"))) Kit.check("节点停点态兑付后清零(恰一次语义同构)", a13.stop()._node_pending.is_empty()) var s10: Dictionary = a10.summary Kit.check("放弃摘要:abandoned · 未打穿(已获大奖励不追回——同失败型结算)", String(s10.get("result", "")) == "abandoned" and not bool(s10.get("cleared", true))) diff --git a/game/tests/unit/core/feat_vouchers_test.gd b/game/tests/unit/core/feat_vouchers_test.gd index 1b2c6bac..3b0d4927 100644 --- a/game/tests/unit/core/feat_vouchers_test.gd +++ b/game/tests/unit/core/feat_vouchers_test.gd @@ -101,15 +101,15 @@ func _init() -> void: var gp_taken_before := int(a._curio.taken.get("gp", 0)) Kit.check_quiet(a.stop().pick_curio("gp", "common") and int(a._curio.taken.get("gp", 0)) == gp_taken_before + 1, "留任小GP 袋:GP 通道兑付受理(gp 计数入账——路由零变化)〔专长批 V10〕") - var hp_before: int = a.hero().hero_hp + var hp_before: int = a.stats().hero_hp a.stop()._pending = {"candidates": [{"kind": "heal", "tier": "excellent"}]} - Kit.check_quiet(a.stop().pick_curio("heal", "excellent") and a.hero().hero_hp == mini(hp_before + int(db.adventure.curio_value("heal", "excellent")), a.effective_max_hp()), + Kit.check_quiet(a.stop().pick_curio("heal", "excellent") and a.stats().hero_hp == mini(hp_before + int(db.adventure.curio_value("heal", "excellent")), a.effective_max_hp()), "留任应急口粮:当即回血 +6 封顶生效上限〔专长批 V10〕") # V10-2 行动点上限归属性轴:ap kind 走加算区第四轴 a._curio.deck = [{"kind": "ap", "tier": "rare"}] a.stop()._pending = {"candidates": a._curio.deck.duplicate()} - var ap_bonus_before: int = a.hero().bonus_total("ap") - Kit.check_quiet(a.stop().pick_curio("ap", "rare") and a.hero().bonus_total("ap") == ap_bonus_before + int(db.adventure.curio_value("ap", "rare")), + var ap_bonus_before: int = a.journal().bonus_total("ap") + Kit.check_quiet(a.stop().pick_curio("ap", "rare") and a.journal().bonus_total("ap") == ap_bonus_before + int(db.adventure.curio_value("ap", "rare")), "行动点上限归属性轴:ap 兑付写加算区第四轴(bonus.ap 承载)〔专长批 V10〕") # V16——断点扩容往返:新账本随断点快照/续玩回填(breakpoint_resume_test P5 同构) diff --git a/game/tests/unit/core/hero_ledger_test.gd b/game/tests/unit/core/hero_ledger_test.gd index fa2eb678..de96b9d1 100644 --- a/game/tests/unit/core/hero_ledger_test.gd +++ b/game/tests/unit/core/hero_ledger_test.gd @@ -1,11 +1,14 @@ # VERTICAL SLICE - NOT FOR PRODUCTION -# 主角实体账单测(拆件批 T1——hr1 公式逐值/hr1-s 商店加算同值/hr2 三通道日记与断点片段)。 +# 主角账三件单测(领域拆分批——hr1 公式逐值/hr1-s 商店加算同值/hr2 三通道日记与断点片段; +# HeroStats 数值域+GpWallet 钱包域+GainJournal 加算日记域逐件直构,另附源码断言面)。 # 公式判据值全部由表读值推导(禁数值字面量断言);真链段走 start_adventure 与 runner 接口③。 # 运行:godot --headless --path game --script res://tests/unit/core/hero_ledger_test.gd(expect 0) extends SceneTree const Kit := preload("res://tests/unit/test_kit.gd") -const HeroLedgerScript := preload("res://core/hero_ledger.gd") +const HeroStatsScript := preload("res://core/hero_stats.gd") +const GpWalletScript := preload("res://core/gp_wallet.gd") +const GainJournalScript := preload("res://core/gain_journal.gd") const AdventureRunnerScript := preload("res://core/adventure_runner.gd") const LevelRunnerScript := preload("res://core/level_runner.gd") @@ -17,8 +20,9 @@ func _init() -> void: quit(1) return - # ---- hr1:生效公式逐值(表值+永久强化 flat 后乘调试 pct——与改前同式)---- - var hero = HeroLedgerScript.new(db, {"atk": 3, "max_hp": 5, "armor_class": 2}) + # ---- hr1:生效公式逐值(表值+永久强化 flat+加算区后乘调试 pct——与改前同式)---- + var journal_a = GainJournalScript.new() + var hero = HeroStatsScript.new(db, {"atk": 3, "max_hp": 5, "armor_class": 2}, journal_a) Kit.check_quiet(hero.effective_max_hp() == int(db.hero.max_hp) + 5, "hr1 生效上限 = 表值+永久强化 flat(pct=0 乘法恒等)") Kit.check_quiet(hero.effective_atk() == int(db.hero.atk) + 3, @@ -29,47 +33,47 @@ func _init() -> void: Kit.check_quiet(hero.effective_max_hp() == int(round(float(int(db.hero.max_hp) + 5) * 1.5)), "hr1 调试 pct=50 后乘——上限 ×1.5(叠加次序:flat 先加 pct 后乘)") hero.set_debug_override(0, 0) - hero.gain_bonus("max_hp", 4, "battle_export", "max_hp") + journal_a.gain_bonus("max_hp", 4, "battle_export", "max_hp") Kit.check_quiet(hero.effective_max_hp() == int(db.hero.max_hp) + 5 + 4, - "hr1 加算区 flat 与永久强化 flat 同列基数(三段叠加)") + "hr1 加算区 flat 与永久强化 flat 同列基数(三段叠加——公式消费日记汇总)") - # hr1 回血封顶公式逐值(算术宿主=HeroLedger.rest_heal 单点——改前 LevelRunner.rest_heal 同式) - Kit.check_quiet(HeroLedgerScript.rest_heal(30, 25, 26) == mini(int(floor(26.0 * 30.0 / 100.0)), 26 - 25), + # hr1 回血封顶公式逐值(算术宿主=HeroStats.rest_heal 单点——改前 LevelRunner.rest_heal 同式) + Kit.check_quiet(HeroStatsScript.rest_heal(30, 25, 26) == mini(int(floor(26.0 * 30.0 / 100.0)), 26 - 25), "hr1 回血 = floor(上限×比例÷100) 后封顶缺口(比例部分 7 > 缺口 1 → 1)") - Kit.check_quiet(HeroLedgerScript.rest_heal(30, 10, 26) == int(floor(26.0 * 30.0 / 100.0)), + Kit.check_quiet(HeroStatsScript.rest_heal(30, 10, 26) == int(floor(26.0 * 30.0 / 100.0)), "hr1 回血比例部分未触封顶时按比例整值") - # ---- hr1 真链段:start_adventure 置满+接口③治疗封顶回报(runner._rest_handler → 主角账公式)---- + # ---- hr1 真链段:start_adventure 置满+接口③治疗封顶回报(runner._rest_handler → 数值账公式)---- var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(1234) var adr = AdventureRunnerScript.new(db, rng) Kit.check_quiet(adr.start_adventure(1234, {}), "hr1 真链冒险启动(seed 1234)") var runner = adr.runner() - Kit.check_quiet(runner.hero_hp == adr.hero().effective_max_hp(), + Kit.check_quiet(runner.hero_hp == adr.stats().effective_max_hp(), "hr1 冒险开始生命账本置满生效上限(含 pct 口径——与战斗内同源)") var healed: int = adr.apply_rest(30, runner.hero_hp, runner.effective_max_hp()) # 接口③真链(关卡恢复点兑付的同一 handler) - Kit.check_quiet(healed == HeroLedgerScript.rest_heal(30, runner.hero_hp - healed, runner.effective_max_hp()), - "hr1 接口③真链回报 = 主角账回血公式(恢复点兑付同链)") + Kit.check_quiet(healed == HeroStatsScript.rest_heal(30, runner.hero_hp - healed, runner.effective_max_hp()), + "hr1 接口③真链回报 = 数值账回血公式(恢复点兑付同链)") - # ---- hr1-s:商店属性商品加算走主角账单写原语(购买后 bonus_total 与旧径同值)---- + # ---- hr1-s:商店属性商品加算走日记单写原语(购买后 bonus_total 与旧径同值)---- runner._gain_wallet(1000) # 钱包注入(测试直调写口——测试构造惯例) var products: Array = runner.shop_products() var bought := false for p in products: if int(p.get("price", 1 << 30)) <= 1000 and String(p.get("attr", "")) != "": var attr := String(p.get("attr", "")) - var before: int = runner.hero().bonus_total(attr) + var before: int = runner.journal().bonus_total(attr) bought = runner.buy_product(String(p.get("product_id", ""))) if bought: - Kit.check_quiet(runner.hero().bonus_total(attr) == before + int(p.get("flat_value", 0)), + Kit.check_quiet(runner.journal().bonus_total(attr) == before + int(p.get("flat_value", 0)), "hr1-s 商店购买后 bonus_total 增量 = 商品 flat(加算走 gain_bonus 单写原语——同值承载)") - Kit.check_quiet(runner.hero().purchase_tier(String(p.get("product_id", ""))) == 1, - "hr1-s 购买档数记主角账(递增定价输入单点)") + Kit.check_quiet(runner.journal().purchase_tier(String(p.get("product_id", ""))) == 1, + "hr1-s 购买档数记日记(递增定价输入单点)") break Kit.check_quiet(bought, "hr1-s 真链购买受理(GP 充足路径)") # ---- hr2:三通道日记(明细来源指回+汇总同步维护)---- - var h2 = HeroLedgerScript.new(db, {}) + var h2 = GainJournalScript.new() h2.gain_bonus("atk", 5, "curio_pick", "attack") h2.gain_bonus("atk", 2, "shop_product", "p_atk_1") h2.gain_bonus("armor_class", 1, "battle_export", "armor_class") @@ -94,32 +98,45 @@ func _init() -> void: h2.record_vision_use() Kit.check_quiet(h2.vision_used() == 1, "hr2 真视使用恰计一次") - # hr2 断点片段往返(日记在档=正主) - var frag: Dictionary = h2.breakpoint_fragment() - var h3 = HeroLedgerScript.new(db, {}) - h3.restore_fragment(frag, {}) - Kit.check_quiet(h3.bonus_total("atk") == 7 and h3.bonus_total("armor_class") == 1, + # hr2 断点片段往返(日记在档=现行事实源)——三件各交各键、并集拼装 + var frag: Dictionary = {} + frag.merge(h2.breakpoint_fragment()) + var w2 = GpWalletScript.new() + w2.gain_gp(37) + frag.merge(w2.breakpoint_fragment()) + var s2 = HeroStatsScript.new(db, {}, GainJournalScript.new()) + s2.hero_hp = 21 + s2.feats.append("twin_pick") + frag.merge(s2.breakpoint_fragment()) + var j3 = GainJournalScript.new() + var w3 = GpWalletScript.new() + var s3 = HeroStatsScript.new(db, {}, j3) + j3.restore_fragment(frag, {}) + w3.restore_fragment(frag, {}) + s3.restore_fragment(frag, {}) + Kit.check_quiet(j3.bonus_total("atk") == 7 and j3.bonus_total("armor_class") == 1, "hr2 片段往返:日记在档由明细推导汇总(逐轴相等)") - Kit.check_quiet(h3.hero_hp == h2.hero_hp and h3.gp == h2.gp and h3.keystone_gained() == 3 and h3.vision_used() == 1, - "hr2 片段往返:账面键逐值(生命/钱包/钥石/真视)") - Kit.check_quiet((h3._gain_log as Array).size() == 3 and (h3._bonus_detail as Array).size() == 3, + Kit.check_quiet(w3.gp == 37 and j3.keystone_gained() == 3 and j3.vision_used() == 1 + and s3.hero_hp == 21 and (s3.feats_held() as Array).has("twin_pick"), + "hr2 片段往返:三件账面键逐值(生命/特性/钱包/钥石/真视)") + Kit.check_quiet((j3._gain_log as Array).size() == 3 and (j3._bonus_detail as Array).size() == 3, "hr2 片段往返:日记全量还原") # hr2 旧档回退(无日记键→汇总直载跳过比对) var legacy: Dictionary = frag.duplicate(true) legacy.erase("gain_journal") - var h4 = HeroLedgerScript.new(db, {}) - h4.restore_fragment(legacy, {}) - Kit.check_quiet(h4.bonus_total("atk") == 7 and h4.bonus_total("armor_class") == 1 and (h4._gain_log as Array).is_empty(), + var j4 = GainJournalScript.new() + j4.restore_fragment(legacy, {}) + Kit.check_quiet(j4.bonus_total("atk") == 7 and j4.bonus_total("armor_class") == 1 and (j4._gain_log as Array).is_empty(), "hr2 旧档缺日记→汇总键直载(跳过比对——旧档行为零变化)") # hr2 日记在档推导比对不符→拒绝载入(fail-fast——汇总不回填) var tampered: Dictionary = frag.duplicate(true) (tampered["bonus"] as Dictionary)["atk"] = 99 - var h5 = HeroLedgerScript.new(db, {}) - h5.restore_fragment(tampered, {}) - Kit.check_quiet(h5.bonus_total("atk") == 0, - "hr2 汇总与日记推导不符→拒载(汇总不回填——日记为正主)") + var j5 = GainJournalScript.new() + j5.restore_fragment(tampered, {}) + Kit.check_quiet(j5.bonus_total("atk") == 0, + "hr2 汇总与日记推导不符→拒载(汇总不回填——日记为现行事实源)") # ---- sc3 源码断言(拆件批 T3——构造注入收口与转发族退役的静态面)---- var adv_src := FileAccess.get_file_as_string("res://core/adventure_runner.gd") @@ -127,8 +144,7 @@ func _init() -> void: var bind_shop_adv: int = adv_src.count("func _bind_shop") var bind_shop_lvl: int = lvl_src.count("func _bind_shop") var bind_calls := 0 # 调用行计数(排除 func 定义行——行首剥缩进后以 _bind_shop() 起始) - for line in (adv_src + lvl_src).split(" -"): + for line in (adv_src + lvl_src).split("\n"): var t := line.strip_edges() if t.begins_with("_bind_shop()"): bind_calls += 1 @@ -145,5 +161,12 @@ func _init() -> void: runledger_hits += FileAccess.get_file_as_string(f).count("RunLedger") + FileAccess.get_file_as_string(f).count("run_ledger") Kit.check_quiet(runledger_hits == 0 and not FileAccess.file_exists("res://core/run_ledger.gd"), "al4 RunLedger 全仓零引用+类文件已删(充能单源化退役收口)〔配装批 V4〕") + # hs1 源码断言(领域拆分批——hero_ledger 整类退役全仓零引用) + var heroledger_hits := 0 + for f in ["res://core/adventure_runner.gd", "res://core/level_runner.gd", "res://core/battle_orchestrator.gd", + "res://core/shop_session.gd", "res://core/curio_stop.gd"]: + heroledger_hits += FileAccess.get_file_as_string(f).count("HeroLedger") + FileAccess.get_file_as_string(f).count("hero_ledger") + Kit.check_quiet(heroledger_hits == 0 and not FileAccess.file_exists("res://core/hero_ledger.gd"), + "hs1 hero_ledger 全仓零引用+类文件已删(主角账三件拆分退役收口)〔领域拆分批 V1〕") - quit(Kit.finish("hero_ledger_test", "PASS — 主角账公式/三通道日记/断点片段往返与旧档回退/源码断言 全绿")) + quit(Kit.finish("hero_ledger_test", "PASS — 主角账三件公式/三通道日记/断点片段往返与旧档回退/源码断言 全绿")) diff --git a/game/tests/unit/core/level_shop_economy_test.gd b/game/tests/unit/core/level_shop_economy_test.gd index d5bf009a..d69add19 100644 --- a/game/tests/unit/core/level_shop_economy_test.gd +++ b/game/tests/unit/core/level_shop_economy_test.gd @@ -1032,8 +1032,8 @@ func _smoke_shop_room() -> void: var gp0: int = runner.gp Kit.check("购买 atk_up 受理(现价 6)", runner.buy_product("atk_up")) Kit.check("钱包扣现价 6(%d → %d)" % [gp0, runner.gp], runner.gp == gp0 - 6) - # 加算区住主角账 - Kit.check("统一冒险加算区 atk +1(加算区住主角账)", runner.hero().bonus_total("atk") == 1 and runner.hero().bonus_total("max_hp") == 0 and runner.hero().bonus_total("armor_class") == 0) + # 加算区住加算日记 + Kit.check("统一冒险加算区 atk +1(加算区住加算日记)", runner.journal().bonus_total("atk") == 1 and runner.journal().bonus_total("max_hp") == 0 and runner.journal().bonus_total("armor_class") == 0) by_id = _shop_products_by_id(runner.shop_products()) Kit.check("atk_up 二购现价 = 4+2+2=8(步长 2 × 已购 1 档,现价现算)", int(by_id["atk_up"]["price"]) == 8 and int(by_id["atk_up"]["purchased"]) == 1) # 行进段百分比归零(购买/生效断言按加算区裸值口径——调试浮窗百分比后乘;hp 快照钳到新口径上限防 spawn 越界,测试通道) @@ -1318,10 +1318,10 @@ func _smoke_shop_pool() -> void: var val_map := {"attack": "atk", "max_hp": "max_hp", "armor": "armor_class", "ap": "ap"} var gp0: int = int(r8.gp) var taken0: int = int(a8._curio.taken.get(kind, 0)) - var val0: int = a8.hero().bonus_total(String(val_map[kind])) + var val0: int = a8.journal().bonus_total(String(val_map[kind])) Kit.check("购买受理(pool 口——%s|%s)" % [kind, tier], r8.buy_pool_item(kind)) Kit.check("钱包扣现价(%d → %d)" % [gp0, int(r8.gp)], int(r8.gp) == gp0 - int(buy_c.get("price", 0))) - Kit.check("加算区 +令牌档位件值(池件=奖励件本体——%s|%s %+d)" % [kind, tier, int(_db.adventure.curio_value(kind, tier))], a8.hero().bonus_total(String(val_map[kind])) == val0 + int(_db.adventure.curio_value(kind, tier))) + Kit.check("加算区 +令牌档位件值(池件=奖励件本体——%s|%s %+d)" % [kind, tier, int(_db.adventure.curio_value(kind, tier))], a8.journal().bonus_total(String(val_map[kind])) == val0 + int(_db.adventure.curio_value(kind, tier))) Kit.check("共享池账本 -1(跨渠道单点——taken %d → %d)" % [taken0, int(a8._curio.taken.get(kind, 0))], int(a8._curio.taken.get(kind, 0)) == taken0 + 1) # A 链:同visit 第二购现价 = ceil(base×b^(l2−1)×A^1)(货架已全重抽——新卡新价同公式) # A 链:白盒抬 GP 保证第二购场景确定构造(直改测试基建——非生产行为) diff --git a/game/tests/unit/core/meta_progression_forge_test.gd b/game/tests/unit/core/meta_progression_forge_test.gd index dc1d22cb..39960be5 100644 --- a/game/tests/unit/core/meta_progression_forge_test.gd +++ b/game/tests/unit/core/meta_progression_forge_test.gd @@ -365,7 +365,7 @@ func _meta_injection() -> void: int(r.effective_atk()) == int(_db.hero.atk) + int(ov.get("atk", 0))) Kit.check("首战 hero_max_hp 与开局生命账本 = 表值 + 注入档", int(r.effective_max_hp()) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0)) - and int(adr.hero().hero_hp) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0))) + and int(adr.stats().hero_hp) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0))) # 白板:同种子下 空参数 = 零强化参数 = 纯表值(逐位一致——注入通道默认值漏改在此转红) var adr_w = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(920251)) Kit.check("白板冒险启动(无 params 实参——B1 空参数路径)", adr_w.start_adventure(920251)) @@ -511,10 +511,10 @@ func _smoke_forge_axes() -> void: Kit.check("入战 AC 三层:甲型基础 12(重甲)+路线加值 1+局内 0〔锻造工坊批 V7〕", int(r5.effective_ac()) == 12 + 1) Kit.check("参数面快照入 runner(attack_type=normal/armor_type=heavy)〔锻造工坊批 V6〕", - adr5.hero().attack_type == "normal" and adr5.hero().armor_type == "heavy") + adr5.stats().attack_type == "normal" and adr5.stats().armor_type == "heavy") # V6 锁死:开冒险后改存档选中→参数面/注入值不变(快照冻结;light=初始持有已解锁可切) Kit.check("冒险开始后锁死:改存档选中不影响参数面快照与生效值(契约 8)〔锻造工坊批 V6〕", - mp5.forge_select_armor("light") and adr5.hero().armor_type == "heavy" + mp5.forge_select_armor("light") and adr5.stats().armor_type == "heavy" and int(r5.effective_ac()) == 12 + 1) # V5/V7 末端:enc 两轴直达 spawn 英雄快照(hero_attack 行携带选中类型键) var run_ax = _run_battle(1, 0, {"attack_type": "pierce", "armor_type": "ward"}) diff --git a/game/tests/unit/core/treasure_effects_test.gd b/game/tests/unit/core/treasure_effects_test.gd index a8baa525..c7614fc2 100644 --- a/game/tests/unit/core/treasure_effects_test.gd +++ b/game/tests/unit/core/treasure_effects_test.gd @@ -273,9 +273,9 @@ func _init() -> void: # ⑬ 真视使用计数链(受理成功恰计一次——主角账单点记录) var rng13 = load("res://core/rng_service.gd").new(); rng13.set_master_seed(321) var ob13 = orch_s.new(db, rng13) - var lg13 = load("res://core/hero_ledger.gd").new(db, {}) - Kit.check_quiet(ob13.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["incense_stick"], "hero": lg13}), - "计数链:装配起战(HeroLedger 携带)") + var lg13 = load("res://core/gain_journal.gd").new() + Kit.check_quiet(ob13.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["incense_stick"], "journal": lg13}), + "计数链:装配起战(GainJournal 携带)") var inv13 = load("res://core/treasure_items.gd").new(db) var rec13: Dictionary = inv13.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0) ob13.treasure_inventory = inv13 diff --git a/reports/需求卡片-冒险域拆件与账本实体化批-2026-10-26.md b/reports/需求卡片-冒险域拆件与账本实体化批-2026-10-26.md index f08d5733..d4a7c9e1 100644 --- a/reports/需求卡片-冒险域拆件与账本实体化批-2026-10-26.md +++ b/reports/需求卡片-冒险域拆件与账本实体化批-2026-10-26.md @@ -1,6 +1,6 @@ # 需求卡片:冒险域拆件与账本实体化批(AdventureRunner 真拆分·主卡) -- 版本:**v1.2**(v1.0→grilling 一轮「回炉」13 阻断全折入→v1.1→二轮「通过附残留 8 条」全折入→v1.2 定稿,复核记录见 §12) +- 版本:**v1.3**(v1.0→grilling 一轮「回炉」13 阻断全折入→v1.1→二轮「通过附残留 8 条」全折入→v1.2 定稿→v1.3 领域拆分修订,复核记录见 §12。v1.3=2026-10-27 领域拆分修订——用户裁结构指标=缺陷,闸 09「标准是尺」口径下的还债实现,用户令本会话讨论后修订) - 来源:[待办清单](../待办清单.md)「AdventureRunner 职责真拆分」条目(2026-10-25 登记,P0,状态=文档设计已闭合+待写卡)(唯一事实源:[architecture](../design/engineering/architecture/architecture.md) v1.16 核心层 HeroLedger/CurioStop 新件行+「冒险域拆分定稿」节) - 归属:`game/core/` 新二件+adventure_runner/level_runner/curio_deck/shop_session/run_ledger/battle_orchestrator 改造+呈现接线面+测量面改址 - 排期:未编入(迭代批卡片;估时 ≈5–5.5 个工作日;姊妹副卡另 ≈1.5–2) @@ -32,7 +32,7 @@ | u9 | RunLedger 瘦身 | bonus 账/vision 账/adventure_counters 桶全部退役迁出(桶归宿主+encounter `"adv_bucket"` 键);attune_charges 账保留(副卡退役) | **完成定义**: -1. u1–u9 落地;全套闸按本批改动域选档全绿;S13 计数读数改善(adventure_runner 类变量 42→约 20、level_runner 38→约 30、board_view 45→32——读数记录进实现报告,基线禁改)。 +1. u1–u9 落地;全套闸按本批改动域选档全绿;S13 计数读数改善(adventure_runner 类变量 42→约 20、level_runner 38→约 30、board_view 45→32——读数记录进实现报告,基线禁改)。**v1.3 增**:拆分后新件逐件落 gdstyle 默认线内——HeroStats 公开方法 12/GpWallet 5/GainJournal 13/RefreshBank 13(类变量各 ≤7)、CurioStop 收口公开方法恰 20、adventure_runner 公开方法 ≤23 基线内;battle_orchestrator 指标精确回位=类变量 50/begin_battle 函数长 ≤141/文件长恰 1314(闸 09 存量 FAIL 行「1314>1302」文本逐字保持,指纹 91d45583cf6d 不变);hero_ledger.gd 删除、grep `HeroLedger|hero_ledger|\.hero()` 代码引用零残留(manifest 文件名与历史性注释除外)。 2. 同种子改前改后:断点快照 27 键逐键逐位一致(含日记键前形态);冒烟/系统/单测/探针全绿。 3. 撤销证明逐 wr 行落地;三份产出物写齐(T6);独立验收审计过。 @@ -109,14 +109,18 @@ | 文件 | 职责 | 行为改动 | |---|---|---| -| `game/core/hero_ledger.gd`(**新增**) | 主角实体账:数值面+获得日记三层+gp 单写原语+公式单点+断点片段 | 新件(承载迁移态,行为不变)→ wr1/wr6 | -| `game/core/curio_stop.gd`(**新增**) | 三选一停点件:停点三态/刷新三源账/兑付族/凭证出箱/信号面/断点片段 | 新件(承载迁移态)→ wr2/wr3/wr6 | -| `game/core/adventure_runner.gd`(修改) | 薄应用层:删数值面/停点族/六转发信号族;建件接线;断点拼盘口;件出口 hero()/stop() | 结构改动,行为不变 → wr4/wr6 | -| `game/core/level_runner.gd`(修改) | 构造注入收参;8 白盒属性删;重绑机制删;战斗→关卡信号族保留;断点关卡域片段方法 | 结构改动 → wr5/wr6 | +| `game/core/hero_stats.gd`(**新增**,v1.3 拆分) | 主角数值账:生命与特性持有集+永久强化快照+类型轴+生效公式单点+回血封顶+续玩生命钳制+断点片段(hero_hp/relics 两键) | 新件(hero_ledger 领域拆分——行为不变)→ wr1/wr6 | +| `game/core/gp_wallet.gd`(**新增**,v1.3 拆分) | GP 钱包:gain/spend/clear 单写原语+断点片段(gp 一键) | 新件(同上)→ wr1/wr6 | +| `game/core/gain_journal.gd`(**新增**,v1.3 拆分) | 加算日记:获得记录三层+统一加算区三通道+购买档数+钥石+真视+断点片段(六键) | 新件(同上)→ wr1/wr6 | +| `game/core/hero_ledger.gd`(**删除**,v1.3 拆分) | 原主角实体账整件退役——按领域拆入上三件(grep 零残留) | 结构改动(行为不变)→ wr1/wr6 | +| `game/core/refresh_bank.gd`(**新增**,v1.3 拆分) | 刷新账件:X 基数与余量/Y 池/刷新券存量账/付费刷计数与报价公式/免费位消耗序+断点片段(refresh_stock/attune_refresh_y 两键) | 新件(curio_stop 领域拆分——行为不变)→ wr2/wr6 | +| `game/core/curio_stop.gd`(**新增**) | 三选一停点件:停点三态/兑付族/谓词/凭证出箱/信号面/断点片段;刷新编排留件(查停点态→bank 支出→重抽重发);deck 纯委托删除(调用方直连 CurioDeck) | 新件(承载迁移态)→ wr2/wr3/wr6 | +| `game/core/adventure_runner.gd`(修改) | 薄应用层:删数值面/停点族/六转发信号族;建件接线(stats/wallet/journal/bank 四引用);断点拼盘口(三片段并集);件出口 stats()/wallet()/journal()/deck()/refresh_bank()/stop() | 结构改动,行为不变 → wr4/wr6 | +| `game/core/level_runner.gd`(修改) | 构造注入收参(ctx 三键 stats/wallet/journal);8 白盒属性删;重绑机制删;战斗→关卡信号族保留;断点关卡域片段方法 | 结构改动 → wr5/wr6 | | `game/core/curio_deck.gd`(修改) | 纯池化:bind_handlers 删;consume_pool_purchase 改经注入 Callable | 结构改动 → wr3 | -| `game/core/shop_session.gd`(修改) | 账本引用换代(run_ledger→hero);商店属性加算改经 hero 单写原语 | 结构改动,写通道语义不变 → wr1 | +| `game/core/shop_session.gd`(修改) | 账本引用换代(run_ledger→wallet+journal);商店属性加算改经 journal 单写原语 | 结构改动,写通道语义不变 → wr1 | | `game/core/run_ledger.gd`(修改) | 瘦身:bonus 账/vision 账/adventure_counters 桶全部退役迁出(桶归宿主字典+encounter `"adv_bucket"` 键);attune_charges 账+其三原语保留 | 结构改动 → wr1 | -| `game/core/battle_orchestrator.gd`(修改) | encounter 注入面:+`"hero"` 键;bonus_add_axis/vision_count_add 调用改指 hero;attune_charge_* 仍走 ledger 瘦壳 | 结构改动 → wr1 | +| `game/core/battle_orchestrator.gd`(修改) | encounter 注入面:+`"journal"` 键(v1.3——原 `"hero"` 键随拆分换代);bonus_add_axis/vision_count_add 调用改指 journal;attune_charge_* 仍走 ledger 瘦壳;结构指标回位(类变量 50/函数长/文件长——闸 09 口径) | 结构改动 → wr1 | | `game/presentation/main_adventure_flow.gd`(修改) | 信号重连(level_started 重取点 is_connected 守卫);控制器件注入换代;bonus 读面改指 hero(:37-38) | 结构改动 → wr4/wr1 | | `game/presentation/views/level_map_screen.gd`(修改) | bonus 读面改指 hero(:334/335/343) | 结构改动 → wr1 | | `game/presentation/hud_presenter.gd`(修改) | bonus 读面改指 hero(:20/313) | 结构改动 → wr1 | @@ -134,73 +138,107 @@ ### 接口块 -**HeroLedger(`game/core/hero_ledger.gd`)** +**HeroStats(`game/core/hero_stats.gd`,v1.3 拆分——数值域)** ```gdscript -class_name HeroLedger extends RefCounted -func _init(db_ref: ConfigDb, base_override_ref: Dictionary) -> void # 锻造强化快照构造注入(原 _base_override 迁入) +class_name HeroStats extends RefCounted +func _init(db_ref: ConfigDb, base_override_ref: Dictionary) -> void # 锻造强化快照构造注入 func set_debug_override(atk_pct: int, hp_pct: int) -> void # 调试百分比通道(adventure.set_hero_override 转发件内) -# ---- 数值面(自 adventure_runner 迁入,语义逐字不变)---- -var hero_hp: int # 生命账(原 ledger["hero_hp"] 单值化——共享字典退役) -var gp: int # GP 钱包(原 ledger["gp"]——LevelRunner.gp 属性与 ShopSession 钱包读写全改指此单点) -func gain_gp(amount: int) -> void # gp 单写原语(clamp ≥0) -func spend_gp(amount: int) -> bool # gp 单写原语(不足 false 零副作用) -func clear_wallet() -> void # 收束清零 -func reset_for_single_level() -> void # 单关模式重开重置(值重置非实例重建——shop 绑 hero 对象引用不换代,重绑因此消失;语义=现 start_level :151-164 重置面) -func effective_max_hp() -> int # 公式单点(与现 :305-307 逐字同式:表值+base_override+汇总,后乘本件 pct) -func effective_atk() -> int # 公式单点(与现 level :101-103 同式,迁入) -func effective_ac() -> int # 同上(level :114-115) -static func rest_heal(pct: int, hp: int, max_hp: int) -> int # 回血公式单点(自 LevelRunner.rest_heal 迁入) -# ---- 获得日记三层 ---- -func record_gain(source_class: String, kind: String, detail: Dictionary) -> int # 获得记录列表追加,返回记录 id -func gain_bonus(axis: String, value: int, source_class: String, kind: String) -> void # 明细+汇总同步维护(三通道统一入口;战末通道 source_class="battle_export"、kind=轴名——battle :1125-1127 按轴逐条调) -func bonus_total(axis: String) -> int # 汇总读面 -func record_purchase(product_id: String) -> void # 购买条目(定价读数走 purchase_tier) -func purchase_tier(product_id: String) -> int -func record_keystone_drop(drop: Dictionary) -> void # 本局增量(keystone 更名 keystone_gained 语义)+逐杀明细 -func keystone_gained() -> int -func record_vision_use() -> void; func vision_used() -> int -var feats: Array[String] # 特性持有集(活引用注入——G12 语义) +func clamp_hp_to_resume_cap(db_max_hp: int) -> void # 续玩生命钳制(原 _resume_params mini 式逐字收进——上限=表值+永久强化 flat 后乘调试 pct) +func rebase_override(raw: Dictionary) -> void +func reset_for_single_level() -> void # 单关重开:override 清零+本域账面重置(钱包/日记清零各归各件) +func reset_adventure_accounts() -> void # 生命置满生效上限+特性持有集清零 +func effective_max_hp() -> int; func effective_atk() -> int; func effective_ac() -> int # 公式单点 +static func rest_heal(pct: int, hp: int, max_hp: int) -> int # 回血公式单点 +var hero_hp: int # 生命账 +var feats: Array[String] # 特性持有集(活引用注入) func feats_held() -> Array -# ---- 断点片段 ---- -func breakpoint_fragment() -> Dictionary # hero_hp/bonus(汇总快照)/获得日记键/purchased/keystone 相关/relics/vision_counter_used -func restore_fragment(frag: Dictionary, params: Dictionary) -> void # 日记缺键→汇总直载跳过比对;在档→推导比对不符 push_error+拒载 +var attack_type := ""; var armor_type := "" # 两根类型轴快照 +func breakpoint_fragment() -> Dictionary # hero_hp/relics 两键 +func restore_fragment(frag: Dictionary, params: Dictionary) -> void ``` -**CurioStop(`game/core/curio_stop.gd`)** +**GpWallet(`game/core/gp_wallet.gd`,v1.3 拆分——钱包域)** + +```gdscript +class_name GpWallet extends RefCounted +var gp: int +func gain_gp(amount: int) -> void; func spend_gp(amount: int) -> bool; func clear_wallet() -> void +func breakpoint_fragment() -> Dictionary # gp 一键 +func restore_fragment(frag: Dictionary, params: Dictionary) -> void +``` + +**GainJournal(`game/core/gain_journal.gd`,v1.3 拆分——加算日记域)** + +```gdscript +class_name GainJournal extends RefCounted +const BONUS_AXES: Array[String] = ["atk", "max_hp", "armor_class", "ap"] +func gain_bonus(axis: String, value: int, source_class: String, kind: String) -> void # 三通道统一入口(明细+汇总同拍) +func bonus_total(axis: String) -> int; func bonus_snapshot() -> Dictionary +func record_purchase(product_id: String) -> void; func purchase_tier(product_id: String) -> int +func record_keystone_drop(drop: Dictionary) -> void; func keystone_gained() -> int; func keystone_drop_log() -> Array +func record_vision_use() -> void; func vision_used() -> int +func reset_adventure_accounts() -> void # 日记域账面清零(汇总/明细/记录/已购档/钥石/真视) +func breakpoint_fragment() -> Dictionary # bonus/shop_purchased/keystone/keystone_drops/vision_counter_used/gain_journal 六键 +func restore_fragment(frag: Dictionary, params: Dictionary) -> void # 缺日记→汇总直载 clamp;在档→明细推导比对不符 push_error 拒载 +``` + +**RefreshBank(`game/core/refresh_bank.gd`,v1.3 拆分——刷新账域,零停点态依赖)** + +```gdscript +class_name RefreshBank extends RefCounted +func _init(db_ref: ConfigDb) -> void +func set_refresh_x_cap(total: int) -> void # X 基数注入(装配集 bonus_refresh magnitude 合计——宿主 start/resume 时点) +func set_refresh_y(v: int) -> void; func refresh_y() -> int # Y 冒险周期池 +func add_refresh_stock(n: int) -> void; func refresh_stock() -> int # 刷新券存量账(冒险级) +func free_left() -> int # 合计读面(X+Y+存量——原 attune_refresh_left 迁入) +func spend_free() -> bool # 免费位消耗(X→Y→存量序逐字;全空 false 零副作用) +func charge_paid() -> void; func gp_cost() -> int # 付费刷计数递增/报价公式(ceil 基价×曲线^已付费刷——原式迁入) +func reset_x() -> void # X 每碰三选一重置回基数(不结转) +func reset_paid() -> void # 付费刷计数随停点清零(对齐商店进店清零) +func breakpoint_fragment() -> Dictionary # refresh_stock/attune_refresh_y 两键 +func restore_fragment(frag: Dictionary, params: Dictionary) -> void # Y 缺键回退参数面(params_fallback 逐字) +``` + +**CurioStop(`game/core/curio_stop.gd`,v1.3 收口后保留面)** ```gdscript class_name CurioStop extends RefCounted # 生命周期=每冒险一次(AdventureRunner 冒险开始建;关卡换实例不换件) -signal curio_choice_presented(payload: Dictionary) # 自 adventure_runner 迁入,载荷逐字不变 +signal curio_choice_presented(payload: Dictionary) # 载荷逐字不变 signal curio_resolved(result: Dictionary) -func _init(db_ref: ConfigDb, deck_ref: CurioDeck, hero_ref: HeroLedger, snapshot_call: Callable) -> void -func rebind_level_runner(runner: LevelRunner) -> void # 每关注入时点=AdventureRunner._start_current_level 建件接线处:存当前关卡对象的 grant_curio_gp 方法 Callable(GP 件入账维持现路径与 wallet_changed 发射序——第二关起绑新 runner,防调死对象) -# ---- 五组入口 ---- -func open_level_clear(payload_base: Dictionary) -> void # 收编 _on_level_finished 停点段:抽签(level_clear)/置在途态/发信号/触发落盘回调 -func present_node_curio(candidates: Array) -> void +func _init(db_ref: ConfigDb, deps: Dictionary, snapshot_call: Callable) -> void +# deps = {deck:CurioDeck, stats:HeroStats, journal:GainJournal, bank:RefreshBank}——构造注入一次给齐 +func rebind_level_runner(runner: LevelRunner, level_index: int) -> void # 每关注入时点(GP 件入账回调换代) +# ---- 停点编排保留面(公开方法恰 20——gdstyle 默认线)---- +func open_level_clear(payload_base: Dictionary) -> void +func present_node_curio(candidates: Array) -> void; func node_curio_candidates() -> Dictionary func pick_curio(kind: String, tier: String = "", feat_id: String = "") -> bool func apply_node_curio(kind: String, tier: String = "", feat_id: String = "") -> bool -func skip_empty_curio() -> bool -func refresh_curio() -> bool; func refresh_curio_paid() -> bool -func curio_refresh_gp_cost() -> int; func attune_refresh_left() -> int; func refresh_stock() -> int -func curio_stop_pending() -> bool # 名继承现役 :584 谓词 -func stop_resolved() -> bool # 放行谓词(已兑付待推进且无在途停点) -func node_curio_pending_state() -> Dictionary +func skip_empty_curio() -> bool; func node_curio_pending_state() -> Dictionary +func refresh_curio() -> bool; func refresh_curio_paid() -> bool # 编排=查停点态→bank 支出→重抽重发 +func curio_stop_pending() -> bool; func stop_resolved() -> bool +func consume_pool_purchase(kind: String, tier: String) -> bool +func reset_adventure_state() -> void +func restore_pending(bp_pending: Dictionary) -> void +func restore_resolved(payload: Dictionary) -> void # v1.3 合并 reemit——置态后重发(resume 只重发不重兑付) +func clear_resolved() -> void func voucher_outbox() -> Dictionary # 凭证出箱 {rap:int, time_sand:int}——战首取走清零 -func draw_candidates(channel: String) -> Array # 经 deck(reward_pick 流不变) -# ---- 断点片段 ---- -func breakpoint_fragment() -> Dictionary # bonus_pending/bonus_result/feat_ticket_*/refresh_stock/attune_refresh_y(读侧 params 回退经参传入) +func breakpoint_fragment() -> Dictionary # bonus_pending/bonus_result/feat_ticket_* func restore_fragment(frag: Dictionary, params: Dictionary) -> void +# v1.3 删除:feat_ticket_consider/curio_cap_ok/pool_stock(deck 纯委托——调用方直连 CurioDeck)、 +# pool_stock_snapshot/draw_candidates(同上)、resolved_payload(零消费死面)、reemit_resolved(并入 restore_resolved)、 +# 刷新族七方法(set_refresh_x_cap/set_refresh_y/refresh_y/add_refresh_stock/refresh_stock/attune_refresh_left/curio_refresh_gp_cost→RefreshBank) ``` **adventure_runner 应用层(保留/新增面)** ```gdscript func _init(db_ref: ConfigDb, rng_ref: RngServiceCore, save_manager_ref = null) -> void # 存档承载改构造注入(缺省 null=断点静默,原语义) -func hero() -> HeroLedger # 件出口(组件交出,非委托壳) +func stats() -> HeroStats; func wallet() -> GpWallet; func journal() -> GainJournal # 件出口(v1.3——hero() 随三件拆分换代) +func deck() -> CurioDeck; func refresh_bank() -> RefreshBank # 件出口(v1.3——停点件 deck 纯委托删除后调用方直连面) func stop() -> CurioStop # 保留:start_adventure/resume_adventure/continue_to_next_level/abandon/state/summary/runner()/set_hero_override -# 删除:数值面方法族/停点族方法/六转发信号与 _forward_*/_bp_parts 五连挖(改收片段) +# 删除:数值面方法族/停点族方法/六转发信号与 _forward_*/_bp_parts 五连挖(改收片段)/hero() # BREAKPOINT_FIELDS 常量保留本件=键序契约(拼盘口按此断言完整性) ``` @@ -208,13 +246,15 @@ func stop() -> CurioStop ```gdscript func _init(db_ref: ConfigDb, rng_ref: RngServiceCore, ctx: Dictionary) -> void -# ctx 键(一次给齐,后置 setter 群删除——grilling 一轮补全 12 键): -# hero:HeroLedger/ledger:RunLedger(瘦壳——本卡仅传,副卡删)/ -# attunement:Array/tree_rules:Array/shop_mode:String/treasure_unlocked:Dictionary/ +# ctx 键(一次给齐,后置 setter 群删除——grilling 一轮补全 12 键;v1.3 hero 键换代三键): +# stats:HeroStats/wallet:GpWallet/journal:GainJournal(共享主角账三件——活引用)/loadout:AttuneLoadout(同调配装件)/ +# adv_bucket:Dictionary(冒险桶宿主字典)/attunement:Array/tree_rules:Array/shop_mode:String/treasure_unlocked:Dictionary/ # hero_attack_type:String/hero_armor_type:String/voucher_rap:int/voucher_time_sand:int/ # pool_wiring:Dictionary(池模式装配 4 参收编:{deck:Array, consume:Callable, ticket:Callable, cap:Callable}——缺省空字典=legacy 零注入) -# start_level(单关测试模式)件内自建私有 HeroLedger(_init 即建、空 override 注入——shop 绑定因此仍仅构造一次;start_level 收 base_override_in 经 hero 重注+reset_for_single_level() 值重置,实例不重建) -# 新增:func hero() -> HeroLedger # 只读出口(bonus 生产读面 5 文件经现 run_ledger 访问链改此承接) +# start_level(单关测试模式)件内自建私有三件(_init 即建、空 override 注入——shop 绑定因此仍仅构造一次; +# start_level 收 base_override_in 经 stats 重注+三件各归各件清零,实例不重建) +# 新增:func stats() -> HeroStats # 只读出口(v1.3——hero() 换代;bonus 生产读面经 journal() 承接) +# func journal() -> GainJournal # start_level_external 收参收窄为 (level_index:int, defer_entry:bool, resume_state:Dictionary) # 新增:func breakpoint_fragment() -> Dictionary # 关卡域五键(node_id/visited_ids/aux_items/shop_shelf/shop_visit_buys) # func restore_fragment(resume_state: Dictionary) -> void # 断点重建参数面收口(原 _resume_bp 直读面收编) @@ -327,6 +367,8 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore, ctx: Dictionary) -> void **二轮闭合复检(2026-10-26,同一独立子代理)**:判「通过(附残留 8 条,无新阻断)」——13 阻断逐条核闭合(含三处点名矛盾检查:桶迁移与契约 8 一致、rebind 时点先于该关全部信号发射无冲突、副卡键值校验与零行为变化自洽)。残留 8 条已折入 v1.2:R1 数字统一(u8/契约 9/wr7/§6 四处 47→49+B 类 68→78);R2 B 类补 run_ledger 账本断言族(约 10 处/3 文件改指);R3 bonus 读面经 LevelRunner `hero()` 只读出口承接写明;R4 单关模式 `_init` 即建私有 hero(空 override)+start_level 经 reset 重注——绑定仍仅构造一次;R5 结构图 run_ledger 行同步桶退役+battle 直构空字典兜底;R6/R7/R8 归副卡折入。估时分项和 5.35d 对齐。 +**领域拆分修订动因(2026-10-27,v1.3——用户令本会话讨论后修订)**:实现批收口呈报的结构指标超标(hero_ledger 公开方法 29、curio_stop 公开方法 34/类变量 18/_apply_curio_pick 分支 10 返回 9——五条 NEW;宿主 battle_orchestrator 类变量/函数长/文件长三条恶化)经用户裁「结构指标=缺陷、按领域切分」定案为还债实现,随本卡迭代收口批执行:①hero_ledger 按领域拆三件(HeroStats 数值与公式/GpWallet 钱包/GainJournal 加算日记——断点片段三件各交各账、应用层并集拼装,28 键对拍基线逐位一致为硬验收);②curio_stop 拆出 RefreshBank 刷新账件(停点态零依赖)+删五条 deck 纯委托(调用方经 adventure.deck() 直连)+resolved_payload 死面删除+reemit_resolved 并入 restore_resolved;③battle_orchestrator 指标精确回位(类变量 50、begin_battle ≤141、文件长恰 1314——闸 09「标准是尺」存量红两行的行文本与指纹逐字保持,多一行少一行都改指纹)。行为零变化契约不变;机检口径仍按 §13——禁为计数保留无效成员,本修订的全部公开面收敛都来自真实调用面。 + ## 13. 禁为机检文本保留无效成员(重构类必填条款) 禁止为迎合机检消息文本(含计数)而保留或新增无效成员/别名;本批撤除的 25 条转发属性即该债务的清偿——不得以「基线计数」「探针便利」等理由保留任何一条或新增同种转发;机检键与卡片契约互斥时实现段一律停卡呈报,不得自造第三条路(code-conventions §1.1 同款)。 diff --git a/reports/需求卡片-同调配装充能域封装批-2026-10-26.md b/reports/需求卡片-同调配装充能域封装批-2026-10-26.md index c3175232..1b9e7ad8 100644 --- a/reports/需求卡片-同调配装充能域封装批-2026-10-26.md +++ b/reports/需求卡片-同调配装充能域封装批-2026-10-26.md @@ -1,6 +1,6 @@ # 需求卡片:同调配装充能域封装批(AdventureRunner 真拆分·副卡) -- 版本:**v1.2**(v1.0→grilling 一轮「回炉」5 阻断全折入→v1.1→二轮「通过附残留」R6/R7/R8 折入→v1.2 定稿,复核记录见 §12) +- 版本:**v1.3**(v1.0→grilling 一轮「回炉」5 阻断全折入→v1.1→二轮「通过附残留」R6/R7/R8 折入→v1.2 定稿→v1.3 领域拆分修订,复核记录见 §12。v1.3=2026-10-27 领域拆分修订——用户裁结构指标=缺陷,闸 09「标准是尺」口径下的还债实现,用户令本会话讨论后修订) - 来源:[待办清单](../待办清单.md)「AdventureRunner 职责真拆分」条目(2026-10-25 登记,P0,状态=文档设计已闭合+待写卡)(唯一事实源:[architecture](../design/engineering/architecture/architecture.md) v1.16 AttuneLoadout 新件行+「冒险域拆分定稿」节充能单源化句) - 归属:`game/core/` 新一件(attune_loadout)+run_ledger 整类退役+battle_orchestrator 使用/重置路径+断点旧档迁移 - 排期:未编入(迭代批卡片;估时 ≈1.5–2 个工作日) @@ -10,7 +10,7 @@ ## 0. 前置依赖与并行性 - **第〇步核对(2026-10-26 写卡会话)**:来源条目=设计线条目(同主卡),闭合凭据=architecture v1.16,放行。 -- **前置卡接口取用**:主卡 T1 产出的 HeroLedger 与 encounter `"hero"` 键、T3 产出的 ctx 注入面与「关卡开始信号前零发射」头注契约、T4 产出的片段三接口形态——本卡在同形接口上扩展充能域。 +- **前置卡接口取用**:主卡 T1 产出的主角账件(v1.3 拆分后=HeroStats/GpWallet/GainJournal 三件)与 encounter `"journal"` 键、T3 产出的 ctx 注入面与「关卡开始信号前零发射」头注契约、T4 产出的片段三接口形态——本卡在同形接口上扩展充能域。 - 并行冲突:与主卡串行(硬前置);与「通关口径换代批」(在册 v1.1)真实交叠=**adventure_runner+level_runner 两文件**(该批 C 注释面 adventure_runner 14 处/level_runner 3 处——本卡改其建件接线与 ctx;该批六件清单无 battle_orchestrator)——先后做者 T0 行号复核吸收;其余在册卡零交叠。 ## 1. 目标与完成定义 @@ -122,9 +122,9 @@ func restore_fragment(frag: Dictionary, inventory: TreasureItems) -> void # 旧 **battle_orchestrator 改指**:`begin_battle` encounter 删 `"ledger"` 键——桶挂链走主卡已建的 `"adv_bucket"` 键(:382 改指已完成于主卡 T1,本卡零动作)、`"hero"` 键沿用;encounter 增 `"loadout": AttuneLoadout` 键;`_reseed_attune_charges(reset)` 改调 `loadout.reset_cycle(reset, treasure_inventory)`(:228-246 语义逐字承);使用点 `:757` 改 `loadout.consume_charge(...)`;`:239` 充能设定改种行路径;`:381` ledger 类型检查与 `RunLedger.new()` 兜底删除。 -**测试直构面改址(grilling 一轮补查)**:treasure_effects_test:275、round_flow_skill_pool_test:969/994(`load("res://core/run_ledger.gd").new()` 直构+encounter "ledger" 注入)→ 改直构 HeroLedger+"hero"/"adv_bucket" 键注入;meta_progression_forge_test:290-291 同款;effect_engine.gd:94/effect_context.gd:3 注释中 RunLedger 表述随批清(注释面,非行为)。 +**测试直构面改址(grilling 一轮补查)**:treasure_effects_test:275、round_flow_skill_pool_test:969/994(`load("res://core/run_ledger.gd").new()` 直构+encounter "ledger" 注入)→ 改直构主角账件+`"journal"`/`"adv_bucket"` 键注入(v1.3——原 `"hero"` 键随主角账三件拆分换代);meta_progression_forge_test:290-291 同款;effect_engine.gd:94/effect_context.gd:3 注释中 RunLedger 表述随批清(注释面,非行为)。 -**主卡接口取用**:HeroLedger(无充能交互)、ctx 注入面(本卡扩两键)、片段三接口形态(本卡同形扩展)。 +**主卡接口取用**:主角账件(v1.3 拆分后=HeroStats/GpWallet/GainJournal 三件——充能交互零涉及)、ctx 注入面(本卡扩两键)、片段三接口形态(本卡同形扩展)。 ### 落位与可复用性预检 @@ -202,6 +202,8 @@ func restore_fragment(frag: Dictionary, inventory: TreasureItems) -> void # 旧 **二轮闭合复检(2026-10-26,同一独立子代理)**:判「通过(附残留,无新阻断)」——5 阻断全核闭合、「键值仅校验读数与零行为变化」边界判自洽。残留 R6/R7/R8 已折入 v1.2:R6 契约 2 旧句(两读法兜底)删、wr1 列改单源对拍、§1 目标句旧措辞(同调行迁移)改「旧档读侧以行集为源」;R7 结构图补 feat_vouchers_test 充能直写 8 处改指行;R8 并行注记事实订正(通关批 C 面六件无 battle_orchestrator,真实交叠=adventure_runner+level_runner)。 +**领域拆分修订动因(2026-10-27,v1.3——用户令本会话讨论后修订)**:主卡 v1.3 领域拆分(用户裁「结构指标=缺陷」还债实现)把主角账 HeroLedger 拆为 HeroStats/GpWallet/GainJournal 三件、battle encounter `"hero"` 键换代 `"journal"`——本卡对主卡产物的取用面(前置卡接口取用句、测试直构改址行、接口取用行)随换代同步改写;本卡自身范围(AttuneLoadout 建件、RunLedger 退役、使用管线逐位不变)零改动。 + ## 13. 禁为机检文本保留无效成员(重构类必填条款) 同主卡:禁止为迎合机检消息文本保留或新增无效成员/别名;RunLedger 的删除不得以任何「基线便利」理由拖延或保留空壳;机检键与契约互斥时停卡呈报(code-conventions §1.1 同款)。