diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index 8870e383..ce1d6d77 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -67,16 +67,13 @@ var _combat_seg = null # CombatSegment(③战斗段) var _decay_seg = null # DecaySegment(④衰减段) var _verdict_seg = null # VerdictSegment(⑤判定段——返回裁定,终局早退语义归本类) var _refresh_used_this_round := 0 # 每轮刷新计数(Active 相位合计,兜底上限 = ActionEconomyTable.refresh_cap_per_round) -var _mines_hit_total := 0 # 踩雷计数本场累计(触发条件事实面——负向 blast 守卫累加) -var _mines_hit_last := 0 # 上轮踩雷值(轮次真正结束后转存) -var _mines_hit_round := 0 # 当轮踩雷累计(轮内部分结算不更新) +var _mines_hits: Dictionary[Variant, Variant] = {"total": 0, "last": 0, "round": 0} # 踩雷计数(触发条件事实面——本场累计/上轮转存/当轮累计三读数合一账) +# 转存时点=_roll_round_actions 轮末(轮内部分结算不更新 last);开战清零归 begin_battle var _board_seq := 0 # 板序号(本战斗第几张板——快照归因用) var _board_from_midround_refresh := false # 装板 provenance:轮内刷新换来的图 = true(清图奖励守卫——刷新图不发清图奖励) var _seal_consumed := {} # 兑付资格以危险格实例为键(整图;换图重置) # 同调装配运行态(begin_battle 经 enc.attunement 注入;off=空集全部效果零扰动) var attunement: Array[Variant] = [] # 装备中同调件 item_id 集 -var loadout = null # 同调配装件(begin_battle 经 enc["loadout"] 持引用;直构 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=表值兜底未解析) @@ -98,6 +95,18 @@ var fx_counters_battle: Dictionary[Variant, Variant] = {} # battle 桶(无前 var counters_ctx: EffectContext = null # 四桶引用束(_init 建、begin_battle 挂 adventure/level 引用) +# 加算日记读面(begin_battle 经 enc["journal"] 派生——encounter.assign 后全生命期可读; +# null=调试/单场直构面,账面消费跳过。领域拆分批成员删除后的引用承接,读面即引用、无第二账) +func _journal() -> GainJournal: + return encounter.get("journal", null) if encounter.get("journal", null) is GainJournal else null + + +# 同调配装件读面(begin_battle 经 enc["loadout"] 派生——充能单源化的 reset_cycle/consume_charge +# 消费面;null=直构零装配,充能面跳过) +func _attune_loadout(): + return encounter.get("loadout", null) + + # 事件发射薄面(18 发射点统一入口——订阅空=零循环零扰动;确定性同步分发零 RNG) func _fx_emit(event: String, payload: Dictionary) -> Array: if _fx == null: @@ -237,9 +246,9 @@ func _attune_charge_safe_row() -> Dictionary: # (used handle 不回收=本轮已耗充能作废,补新 handle=「回满」;差额注入防双份=level_runner # set_attune_charges 同构)。reset ∈ {battle, level}——adventure 周期归冒险开始种子面) func _reseed_attune_charges(reset: String) -> void: - if loadout == null or treasure_inventory == null: + if _attune_loadout() == null or treasure_inventory == null: return # 直构零装配/库存未就位——充能回满跳过(空装配同语义) - loadout.reset_cycle(reset, treasure_inventory) # 写时种行/补行(按周期筛——语义逐字承原循环体) + _attune_loadout().reset_cycle(reset, treasure_inventory) # 写时种行/补行(按周期筛——语义逐字承原循环体) # 积点制件进度读数(自动兑付无玩家发起入口,读数口=C 区秘宝槽悬停浮窗进度行: @@ -311,10 +320,6 @@ func begin_battle(enc: Dictionary) -> bool: # 行有 atk_min 时按 [atk_min, atk_max] 骰 spawn 期整战持久值 # (每拍抖动否决——数字可读性锚;中点 = 行值)。encounter 封闭流恰一 scope(数量骰先、atk 骰后); # 挖掘决策域之外——P1 合规;敌人系统 零随机纪律保持(收 override 即确定值)。 - # 遭遇骰(难度曲线与Boss GDD Core Rule 8):行有 unit_count_max 时按 [unit_count, max] 均匀骰; - # 行有 atk_min 时按 [atk_min, atk_max] 骰 spawn 期整战持久值 - # (每拍抖动否决——数字可读性锚;中点 = 行值)。encounter 封闭流恰一 scope(数量骰先、atk 骰后); - # 挖掘决策域之外——P1 合规;敌人系统 零随机纪律保持(收 override 即确定值)。 # 逐行骰:混编行集**每行各掷自己的区间骰**—— # 先按行序逐行数量骰、再按行序逐行 atk 骰(单行行集 = 既有一次数量骰 + 一次 atk 骰,判定条件、 # 开流条件、消费序、消费次数逐位一致——零内容期随机流零变化;区间宽度为零的行不掷 = 「定值不骰」 @@ -358,14 +363,11 @@ func begin_battle(enc: Dictionary) -> bool: if not _install_new_board(): return false _reset_round_facts() # 首板 install 的初始展开事件不入轮 facts(resource 恒 false 的双保险) - # 同调装配运行态注入(enc.attunement 携带;off=空集零扰动。淬毒 N 注入 - # 伤害控制器、偏运 N 注入地图战斗资源——均须先于首板 install) + # 同调装配运行态注入(enc.attunement 携带;off=空集零扰动。淬毒 N 注入伤害控制器、偏运 N 注入地图战斗资源——均须先于首板 install) attunement = (enc.get("attunement", []) as Array).duplicate() _attune_safe_seen = 0 - # 淬毒/偏运=triggers 订阅行(淬毒=clamp_check - # modify 窗口、偏运=reward_roll bias_rewards;value 行字段 value_from 定影,begin_battle 不注入) - # 特性运行态(enc.feats 携带持有集:效果行为=数据行 triggers 经效果 - # 引擎分发;流派档位=FeatsEngine 纯函数求值两运行态注伤害控制器。魔矢每战首次开格计数同步归零) + # 淬毒/偏运=triggers 订阅行(淬毒=clamp_check modify 窗口、偏运=reward_roll bias_rewards;value 行字段 value_from 定影,begin_battle 不注入) + # 特性运行态(enc.feats 携带持有集:效果行为=数据行 triggers 经效果引擎分发;流派档位=FeatsEngine 纯函数求值两运行态注伤害控制器。魔矢每战首次开格计数同步归零) # 扩签运行态就位:AC 基线快照、累积器清零、adventure 计数器/加算区共享引用接收 _fx_hero_ac_base = int(enc.get("hero_armor_class", -1)) _fx_ac_round = 0 @@ -373,11 +375,7 @@ func begin_battle(enc: Dictionary) -> bool: _fx_new_resource_types = {} _fx_pending_reveals = [] _fx_bonus_delta = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} - _mines_hit_total = 0 # 踩雷计数开战清零(事实面生命周期同款) - _mines_hit_last = 0 - _mines_hit_round = 0 - loadout = enc.get("loadout", null) - _hero = enc.get("journal", null) if enc.get("journal", null) is GainJournal else null # null=调试直构——账面消费跳过 + _mines_hits = {"total": 0, "last": 0, "round": 0} # 踩雷计数开战清零(事实面生命周期同款;装备/日记读面经 encounter 派生——无成员接收行) 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: @@ -390,35 +388,29 @@ func begin_battle(enc: Dictionary) -> bool: _damage.rider_tier_bonus = 0 _damage.rider_tier_crit = false _feat_first_dig_used = false - # 树 rule 键消费(Fork 激活侧效果=效果引擎合成 - # 订阅(rebuild 注入——round_start 首轮 AP+1 / damage_taken 减伤+1)。其余 rule 键 - # =stat 面投影不入战斗(T8 表内容 provisional 口径披露)) + # 树 rule 键消费(Fork 激活侧效果=效果引擎合成订阅(rebuild 注入——round_start 首轮 AP+1 / damage_taken 减伤+1)。 + # 其余 rule 键=stat 面投影不入战斗(T8 表内容 provisional 口径披露)) var tree_rules: Array[Variant] = enc.get("tree_rules", []) as Array - # 效果引擎订阅重建(每战从持有集/装备集重建,中途获得下一战生效; - # 树规则合成订阅同面装载。装载序=feats 表序→treasure 表序→树规则,写死保种子复放。 - # 计数器四桶经 counters_ctx 引用宿主容器,无回灌时点(旧战首回灌时点删除)) + # 效果引擎订阅重建(每战从持有集/装备集重建,中途获得下一战生效;树规则合成订阅同面装载。 + # 装载序=feats 表序→treasure 表序→树规则,写死保种子复放。计数器四桶经 counters_ctx 引用宿主容器,无回灌时点(旧战首回灌时点删除)) _fx.rebuild(feat_ids, attunement, tree_rules) - # 秘宝常时参数注入(装配集 kind 分发:远程镐挖距放宽/信标网络封印可达豁免; - # 缺省 0/false=零装配零扰动。注入在首板 install 之后〔板在早段〕——板生成不读两参数, - # 参数只影响挖掘/插棋动作判据,时序安全) + # 秘宝常时参数注入(装配集 kind 分发:远程镐挖距放宽/信标网络封印可达豁免;缺省 0/false=零装配零扰动。 + # 注入在首板 install 之后〔板在早段〕——板生成不读两参数,参数只影响挖掘/插棋动作判据,时序安全) minesweeper.dig_range_bonus = attune_param_sum("dig_range_bonus") minesweeper.seal_range_free = attune_param_sum("seal_range_free") > 0 - # 充能重置周期(battle=每战回满/level=每关首战回满〔battle_index==1〕; - # adventure 周期维持冒险开始 _seed_attune_charges 单次) + # 充能重置周期(battle=每战回满/level=每关首战回满〔battle_index==1〕;adventure 周期维持冒险开始种行单次) _reseed_attune_charges("battle") if int(enc.get("battle_index", 1)) == 1: _reseed_attune_charges("level") - # board_installed 发射+施加(巢穴预告:装板后一次报本板危险格数量; - # 只报数量不标位置=信息红线,不碰分布=棋盘事实不可变契约 3。命中=读数口落值,呈现归 - # 呈现桥消费(无格来源不进结算明细行——行来源纪律);板已就绪〔早段 install〕、订阅行已在 rebuild 装载) + # board_installed 发射+施加(巢穴预告:装板后一次报本板危险格数量;只报数量不标位置=信息红线, + # 不碰分布=棋盘事实不可变契约 3。命中=读数口落值,呈现归呈现桥消费(无格来源不进结算明细行——行来源纪律);板已就绪〔早段 install〕、订阅行已在 rebuild 装载) nest_preview_count = -1 var bi_hits := _fx_emit("board_installed", {}) for h in bi_hits: if String(h.get("kind", "")) == "mine_count_preview": nest_preview_count = minesweeper.mine_total() - # 跨冒险行动点加成轴(统一冒险加算区第四轴):经排队修正通道兑现 - # 「每场战斗开始首轮 AP +加成值」(queue_ap_delta = 下轮开轮基线加算、notify_round_start 兑现后 - # 清零——首轮即消费,后续轮次不重复兑现;语义锚 = 行动经济 GDD「跨冒险行动点加成轴」节) + # 跨冒险行动点加成轴(统一冒险加算区第四轴):经排队修正通道兑现「每场战斗开始首轮 AP +加成值」 + #(queue_ap_delta = 下轮开轮基线加算、notify_round_start 兑现后清零——首轮即消费,后续轮次不重复兑现;语义锚 = 行动经济 GDD「跨冒险行动点加成轴」节) var ap_bonus: int = int(enc.get("hero_ap_bonus", 0)) + int(enc.get("voucher_time_sand", 0)) # 时沙同通道(首轮池增量,不改上限) if ap_bonus > 0: action_economy.queue_ap_delta(ap_bonus) @@ -426,11 +418,8 @@ func begin_battle(enc: Dictionary) -> bool: var voucher_rap: int = int(enc.get("voucher_rap", 0)) if voucher_rap > 0: action_economy.grant_reserve_ap(voucher_rap) - # 工兵晨令=round_start grant_ap(mode:queue) 订阅行承载, - # when round_index ≤ effective_rounds 行字段 value_from 定影;发射位先于轮切换=当轮兑现 - # round_start 发射+施加(发射位先于行动经济轮切换:树规则 grant_ap - # mode:queue 当轮入队、notify_round_start 即兑现(先手压制=首轮 AP+1 语义锚); - # 计数器 round_start 重置点同点生效) + # 工兵晨令=round_start grant_ap(mode:queue) 订阅行承载,when round_index ≤ effective_rounds 行字段 value_from 定影; + # 发射位先于轮切换=当轮兑现(树规则 grant_ap mode:queue 当轮入队、notify_round_start 即兑现——先手压制=首轮 AP+1 语义锚;计数器 round_start 重置点同点生效) round_index = 1 _emit_round_start() if not action_economy.notify_round_start(): @@ -752,13 +741,13 @@ func use_treasure(handle: int, target: Vector2i) -> Dictionary: return {"ok": false, "reason": "爆破工兵包:3×3 区域内无可移除危险格(须未揭示且未封印)"} var rec: Dictionary[Variant, Variant] = treasure_inventory.consume(handle) # 同调主动件充能消耗(单源化——行标 used 幂等承载,原字典扣减同点退役) - if loadout != null and not rec.is_empty() and String(rec.get("source_class", "")) == "attunement": - loadout.consume_charge(String(rec.get("item_id", "")), treasure_inventory) + if _attune_loadout() != null and not rec.is_empty() and String(rec.get("source_class", "")) == "attunement": + _attune_loadout().consume_charge(String(rec.get("item_id", "")), treasure_inventory) # 真视类工具使用计数(信标网络解锁行为条件:受理成功恰计一次; # 主角账单点记录,冒险收束聚合入档 save.add_vision_used) if kinds.has("reveal_tiles") or kinds.has("safe_dig") or kinds.has("mine_reveal"): - if _hero != null: - _hero.record_vision_use() # 受理成功恰计一次(未注入=调试直构不计——空账守卫同语义) + if _journal() != null: + _journal().record_vision_use() # 受理成功恰计一次(未注入=调试直构不计——空账守卫同语义) return {"ok": true, "item_id": v.item_id, "resolved": v.resolved, "record": rec} @@ -848,6 +837,14 @@ func _apply_gp_row(row: Dictionary, source_class: String, cell: Vector2i, ri: in # pierce/magic 正向 = 单目标克制直伤单据(attack_type 查克制矩阵,必中全额不掷点);负向 = 主角直伤单据。 # 返回 "redeemed" = 实际有效兑付 / "zero" = 零兑付(行已自记)——读数 = 控制器结账 outcome(A 模式提交即结)。 # sync = 同步开单(清图批——流程依赖完美第二份/高光份数读数,无视结账政策当场结账)。 +# 负向 blast(雷)踩雷计数入账(触发条件事实面——正向 spark_aoe/清图奖励同走 blast 分支不计入) +func _book_negative_blast(row: Dictionary) -> void: + if String(row.get("polarity", "positive")) != "negative": + return + _mines_hits["round"] += 1 + _mines_hits["total"] += 1 + + func _apply_flat_row(row: Dictionary, source_class: String, cell: Vector2i, ri: int, highlight: String = "", label_suffix: String = "", sync := false) -> String: var ctype := String(row.cell_type) var power := int(row.power) @@ -858,9 +855,7 @@ func _apply_flat_row(row: Dictionary, source_class: String, cell: Vector2i, ri: _emit_row("effect", source_class, cell, ri, "%s(下轮行动点 %+d)" % [display, delta], delta, {"effect_id": "ap_delta", "pool_value": action_economy.pending_ap_delta()}, highlight) return "redeemed" if ctype == "blast": - if String(row.get("polarity", "positive")) == "negative": - _mines_hit_round += 1 # 负向 blast(雷)踩雷计数——正向 spark_aoe/清图奖励同走此分支不计入 - _mines_hit_total += 1 + _book_negative_blast(row) # 负向 blast(雷)踩雷计数——正向 spark_aoe/清图奖励同走此分支不计入 var t := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, source_class, display, "board", power, \ DamageController.TARGET_ALL_ALIVE, ri, cell, "", 0, highlight, {"batch_key": String(row.reward_id)}) return _outcome_of(_submit(t, sync)) @@ -964,8 +959,8 @@ func _roll_round_actions() -> void: var facts := { "round_index": round_index, "last_round_end": "none" if (action_economy.last_round_end as Dictionary).is_empty() else String((action_economy.last_round_end as Dictionary).get("path_tag", "none")), - "mines_hit_last": _mines_hit_last, - "mines_hit_total": _mines_hit_total, + "mines_hit_last": int(_mines_hits["last"]), + "mines_hit_total": int(_mines_hits["total"]), } for u in _enemy_system.query_alive("enemy"): # Boss 半血解锁闩(迭代14④ T4——开轮摇号前逐敌检查;一次过门单向闩;解锁出一次归因明细行 @@ -981,7 +976,7 @@ func _roll_round_actions() -> void: # 非 Boss 专属——Boss 语义留给订阅者 when 判定。pct=本次解锁条目的 trigger 行值) var pct_v := 0 for pe in (u.get("pool", []) as Array): - var pe_d: Dictionary = pe + var pe_d: Dictionary[Variant, Variant] = pe if String(pe_d.get("skill_id", "")) == sid and (pe_d.get("trigger", {}) as Dictionary).has("value"): pct_v = int((pe_d.get("trigger", {}) as Dictionary).get("value", 0)) break @@ -1000,8 +995,8 @@ func _roll_round_actions() -> void: var stream: RandomNumberGenerator = rng.open_stream("behavior_roll") _enemy_system.set_pending_action(u, SkillLottery.draw(entries, stream)) _enemy_system.tick_cooldowns() # 递减在摇号读值之后——"施放后连续 N 拍不可摇中"的等价实现(单位容器 注记) - _mines_hit_last = _mines_hit_round # 轮次真正结束转存(轮内部分结算不更新) - _mines_hit_round = 0 + _mines_hits["last"] = _mines_hits["round"] # 轮次真正结束转存(轮内部分结算不更新) + _mines_hits["round"] = 0 # ---- 战斗段 ---- @@ -1133,10 +1128,10 @@ func _battle_over(result: String) -> void: if String(h.get("kind", "")) == "heal": var hu_w: Dictionary[Variant, Variant] = _enemy_system.hero_unit() hu_w["hp"] = mini(int(hu_w.get("hp", 0)) + int(h.get("value", 0)), maxi(1, int(hu_w.get("max_hp", 1)))) - # bonus_delta 战末汇入(血税双向账:本战累积经主角账单写原语汇入获得日记三层后清零) - if _hero != null: + # bonus_delta 战末汇入(血税双向账:本战累积经加算日记单写原语汇入获得日记三层后清零) + if _journal() != null: for axis in _fx_bonus_delta.keys(): - _hero.gain_bonus(String(axis), int(_fx_bonus_delta[axis]), "battle_export", String(axis)) + _journal().gain_bonus(String(axis), int(_fx_bonus_delta[axis]), "battle_export", String(axis)) _fx_bonus_delta = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} # 搬运链删除:adventure 桶经 ctx 引用直写宿主字典,无战末导出时点(旧导出/汇入搬运链删除) # 映射随战斗结束退役(地图战斗资源 义务④——下一场战斗 = 新板新 scope 批,roll_board 相位守卫的前提)