From a832965a35a383609573afba2cbb59f00516743d Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Fri, 30 Oct 2026 05:14:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=A1=E2=91=A1T2(=E6=88=98=E6=96=97?= =?UTF-8?q?=E7=BC=96=E6=8E=92=E5=BD=92=E7=BA=A6=E6=89=B9):=20RoundCycle?= =?UTF-8?q?=E5=BB=BA=E4=BB=B6(8=E7=B1=BB=E5=8F=98=E9=87=8F=C2=B7=E4=B8=89?= =?UTF-8?q?=E5=8F=82=E7=9B=B4=E6=9E=84=C2=B7=E4=BA=94=E6=AE=B5=E6=9E=84?= =?UTF-8?q?=E9=80=A0=E9=80=90=E5=AD=97=E8=BF=81=E5=85=A5)=E2=80=94?= =?UTF-8?q?=E2=80=94=E5=AE=BF=E4=B8=BB=E6=AE=B5=E9=A9=B1=E5=8A=A8=E8=BF=81?= =?UTF-8?q?=E7=A7=BB:=20=E4=BA=94=E6=AE=B5=E6=88=90=E5=91=98+settling+mine?= =?UTF-8?q?s=5Fhits=E4=B8=83=E5=A3=B0=E6=98=8E=E5=88=A0=C2=B7=5Fmaybe=5Fse?= =?UTF-8?q?ttle/=5Fsettle/=5Froll=5Fround=5Factions=E4=B8=89=E5=87=BD?= =?UTF-8?q?=E6=95=B0=E5=88=A0=C2=B7settle=5Fif=5Fdue=E5=90=88=E4=BD=93(?= =?UTF-8?q?=E9=97=B8=E5=BA=8F=E9=80=90=E5=AD=97=C2=B7=E7=BB=88=E5=B1=80?= =?UTF-8?q?=E6=97=A9=E9=80=80=E4=B8=8D=E5=9B=9E=E7=BD=AE)=C2=B7roll=5Froun?= =?UTF-8?q?d=5Factions=E9=80=90=E5=AD=97(oc=E6=94=B9=E6=8C=87=C2=B7behavio?= =?UTF-8?q?r=5Froll=E6=B5=81=E5=90=8D=E5=BC=80=E6=B5=81=E6=97=B6=E7=82=B9?= =?UTF-8?q?=E5=8F=96=E5=8F=B7=E5=BA=8F=E9=80=90=E5=AD=97);=E5=AE=BF?= =?UTF-8?q?=E4=B8=BB916=E2=86=92817=E8=A1=8C=C2=B7=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E7=82=B9=E6=94=B9=E5=9D=80=E4=BA=94=E5=A4=84;=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E6=94=B9=E6=8C=87(trigger=5Fcond=203/round=5Fflow=201?= =?UTF-8?q?/feat=5Fvouchers=205/meta=203)+V1=E6=96=AD=E8=A8=80=E5=86=85?= =?UTF-8?q?=E5=B5=8C(=5Frounds=E5=9C=A8=E5=BA=A7=E5=9B=9E=E6=8C=87+facts?= =?UTF-8?q?=E5=9C=A8=E5=BA=A7+=E4=BA=94=E6=AE=B5=E9=BD=90);static=5Fscan?= =?UTF-8?q?=20S8/S7=E4=B8=A4=E6=B8=85=E5=8D=95=E5=A2=9E=E8=A1=8Cround=5Fcy?= =?UTF-8?q?cle.gd;=E4=B8=83=E5=A5=97=E4=BB=B6+=E5=86=92=E7=83=9F=E7=BB=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/core/battle_orchestrator.gd | 110 ++---------------- game/core/settlement/round_cycle.gd | 106 +++++++++++++++++ game/core/settlement/round_cycle.gd.uid | 1 + game/tests/static_scan.sh | 4 +- .../round_flow_skill_pool_test.gd | 2 +- .../battle_settlement/trigger_cond_test.gd | 6 +- game/tests/unit/core/feat_vouchers_test.gd | 10 +- .../unit/core/meta_progression_forge_test.gd | 8 +- game/tests/unit/core/settle_segments_test.gd | 15 ++- 9 files changed, 143 insertions(+), 119 deletions(-) create mode 100644 game/core/settlement/round_cycle.gd create mode 100644 game/core/settlement/round_cycle.gd.uid diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index 57b3d0c8..06842c9c 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -35,6 +35,7 @@ const SOURCE_EXEMPT_TYPES := ["decay", "verdict"] const SOURCE_EXEMPT_CLASSES := ["hero_attack", "enemy_action", "clear_reward", "fx_reveal", "fx_grant_gp"] var _facts: BattleFacts # 战斗事实件(battle_facts.gd——事实族 15 成员单点) +var _rounds: RoundCycle # 轮循环驱动件(settlement/round_cycle.gd——五段持有/段序/重入闸/摇号) var db: ConfigDb var rng: RngServiceCore # E1 封装收口:协作对象 enemy_system/status_effects/reward_economy 私有化——跨层读取一律经 presentation_snapshot()/diagnostic_snapshot() 成文出口。 @@ -58,18 +59,11 @@ var _reward_grant: RewardGrant # 奖励授予件(授予管线与秘宝使用 var round_index := 0 var battle_result := "" # "" / "victory" / "defeat" / "fatal_error" -var _settling := false # 结算五段 Segment(段边界=审计五段,段序骨架冻结于 # SettlementTable.segment_order 语义:授予→增益/减益→战斗→衰减→判定。本类做段序编排+ # 行发射/开单原语单点——段体逻辑归 game/core/settlement/ 五类) -var _grant_seg = null # GrantSegment(①授予段——含封印判与 counter/grant 助手单点) -var _aura_seg = null # AuraSegment(②增益/减益段) -var _combat_seg = null # CombatSegment(③战斗段) -var _decay_seg = null # DecaySegment(④衰减段) -var _verdict_seg = null # VerdictSegment(⑤判定段——返回裁定,终局早退语义归本类) # 同调装配运行态(begin_battle 经 enc 携带 attunement 键注入 facts;off=空集全部效果零扰动) -var _mines_hits: Dictionary[Variant, Variant] = {"total": 0, "last": 0, "round": 0} # 踩雷计数(触发条件事实面——本场累计/上轮转存/当轮累计三读数合一账) -# 转存时点=_roll_round_actions 轮末(轮内部分结算不更新 last);开战清零归 begin_battle +# 转存时点=roll_round_actions 轮末(轮内部分结算不更新 last);开战清零归 begin_battle var nest_preview_count := -1 # 巢穴预告读数口(board_installed 命中落值;-1=未装配/未发射;呈现桥消费) # 特性运行态(relic 持有集经 enc.feats 注入:效果行为=数据行 triggers 经 # 效果引擎分发;魔矢每战首次计数 battle 域。空集=零扰动) @@ -94,7 +88,7 @@ func _attune_loadout() -> AttuneLoadout: # 授予件战斗域回调束(bind_battle 注入——键集封闭三键:充能消耗/真视计数/负向踩雷计数; -# 宿主侧各转 _attune_loadout/_journal/_mines_hits 单点,授予件对账本零强引用) +# 宿主侧各转 _attune_loadout/_journal/_rounds.mines_hits 单点,授予件对账本零强引用) func _battle_hooks() -> Dictionary[Variant, Variant]: return { "attune_consume": _hook_attune_consume, @@ -116,8 +110,8 @@ func _hook_vision_add() -> void: # 负向 blast(雷)踩雷计数入账(触发条件事实面——正向 spark_aoe/清图奖励不计入; # 极性判定在授予件 _flat_blast 传点,本口只记账) func _book_negative_blast() -> void: - _mines_hits["round"] += 1 - _mines_hits["total"] += 1 + _rounds.mines_hits["round"] += 1 + _rounds.mines_hits["total"] += 1 # 效果引擎宿主侧事实单点(fx_host facts_cb 注入——round_index/battle_index/seals_count/nest_count/hp_ratio 五事实;charge_layers 归 fx_host 自答;未知事实回落 null) @@ -244,11 +238,8 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void: minesweeper.round_end_board.connect(_on_round_end) action_economy.round_end_stopped.connect(_on_round_end) # 结算五段接线(授予/战斗两段持三引用=宿主+两协作件;信号连接纪律不涉段〔段零信号连接〕) - _grant_seg = GrantSegment.new(self, _fx_host, _reward_grant) - _aura_seg = AuraSegment.new(self) - _combat_seg = CombatSegment.new(self, _fx_host, _reward_grant) - _decay_seg = DecaySegment.new(self) - _verdict_seg = VerdictSegment.new(self) + # 结算五段接线随轮循环驱动件迁驻(round_cycle.gd——段件持有/段序/重入闸/摇号单点;构造序禁环不变) + _rounds = RoundCycle.new(self, _fx_host, _reward_grant) # ---- 战斗开始序列(编排专章①:门 → 增益状态 → 敌人系统 → install+roll → 行动经济)---- @@ -273,7 +264,7 @@ func begin_battle(enc: Dictionary) -> bool: if not action_economy.notify_round_start(): _fatal("行动经济 首轮 notify_round_start 被拒") return false - _roll_round_actions() # 开轮摇号(首轮同样摇;先于 round_started,挖掘期动画位可读) + _rounds.roll_round_actions() # 开轮摇号(首轮同样摇;先于 round_started,挖掘期动画位可读) round_started.emit(round_index) return true @@ -349,7 +340,7 @@ func _begin_battle_fx_state(enc: Dictionary) -> void: _facts.attunement = (enc.get("attunement", []) as Array).duplicate() _facts.attune_safe_seen = 0 _fx_host.reset_battle_state(int(enc.get("hero_armor_class", -1))) - _mines_hits = {"total": 0, "last": 0, "round": 0} # 踩雷计数开战清零(事实面生命周期同款) + _rounds.mines_hits = {"total": 0, "last": 0, "round": 0} # 踩雷计数开战清零(事实面生命周期同款) var adv_bucket: Variant = enc.get("adv_bucket", null) _fx_host.mount_buckets(adv_bucket if adv_bucket is Dictionary else {}, enc.get("level_counters")) # 冒险桶宿主字典(直构兜底自有空字典);关卡桶 LevelRunner 每关 new 下传、非 Dictionary 即不挂 _reward_grant.bind_battle(_battle_hooks()) # 授予件战斗域回调束(充能消耗/真视计数/踩雷计数) @@ -460,38 +451,11 @@ func _on_seal_judged(evt: Dictionary) -> void: func _on_round_end(_payload: Dictionary) -> void: _facts.path = String(_payload.get("path_tag", "")) - _maybe_settle() + _rounds.settle_if_due() -func _maybe_settle() -> void: - if _settling or battle_result != "": - return - if action_economy.phase != action_economy.PHASE_WAITING: - return - _settle() -# ---- 每轮结算序列(五段——段序骨架冻结于 SettlementTable.segment_order)---- - -func _settle() -> void: - _settling = true - var ri := round_index - # 段序编排(五段——段体归 game/core/settlement/,本类只做段序+行发射/开单原语单点) - _grant_seg.run(ri) # ① 授予段(案C 闸随段装载) - _aura_seg.run(ri) # ② 增益/减益段(续存实例组合行) - _combat_seg.run(ri) # ③ 战斗段(主角先手 → 逐敌还手 → 地图伤害账殿后〔案C〕) - _decay_seg.run(ri) # ④ 衰减段(恰一次恒执行含末轮——先兑付后衰减) - var verdict: String = _verdict_seg.run(ri) # ⑤ 判定段(防御性优先级 hero 先判) - if verdict != "continue": - return # 终局:_settling 不回置——_battle_over 已接管 - _settling = false - - -# ---- 授予段 helpers(极性路由)---- - -# 开格产出乘区(贪婪矿灯/矿脉直觉/赌徒的血签事件收集器注入): -# positive=power ×(1+静态 mult%);negative=power ×(1+cost%)。 -# 全零收集器=原快照原样返回(零扰动)。 func _feat_yield_adjust(snap: Dictionary, positive: bool, pos_m: int, neg_m: int) -> Dictionary: if positive and pos_m <= 0: return snap @@ -548,57 +512,7 @@ func set_treasure_unlocked(ids: Dictionary) -> void: # 随机消费只在换算层(本方法 + SkillLottery);单位容器收确定值(零随机纪律——armor_roll 先例同构)。 # 统一流:behavior_roll 无子流派生向量(位置交错,单点定位由事件日志承载)。 # 摇定后、兑现前死亡 → 本拍技能作废。 -func _roll_round_actions() -> void: - var pity_m: int = int(db.combat.skill.pity_threshold) - # facts 组包(开轮摇号前一次成型——last_round_end 空字典首轮映射 none 唯一落点;hero 事实不进 facts, - # 容器内直读主角行) - var facts := { - "round_index": round_index, - "last_round_end": "none" if action_economy.last_round_end().is_empty() else String(action_economy.last_round_end().get("path_tag", "none")), - "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——开轮摇号前逐敌检查;一次过门单向闩;解锁出一次归因明细行 - # 「XX 解锁了 YY」带 skill_unlock 高光键〔type=highlight 既有行型零新增〕,呈现层弹一次性 - # 技能名提示〔轻量 toast 非演出——D8 呈现面〕。门=确定性谓词,机制面零随机,摇号随机 - # 消费仍在 behavior_roll 既有流——同种子逐位复现不破坏) - for sid in _enemy_system.check_trigger_unlocks(u, facts): - var srow_u: Dictionary[Variant, Variant] = db.combat.skill.row_by_id(sid) - _emit_row({"type": "highlight", "source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "highlight": "skill_unlock"}, - round_index, "%s 解锁了 %s" % [String(u.display_name), String(srow_u.get("display_name", sid))], - 0, {"skill_id": sid}) - # enemy_hp_threshold 发射点(内容数据驱动扩展批 T1——血量阈值沿;对所有带该字段敌人逐个发射, - # 非 Boss 专属——Boss 语义留给订阅者 when 判定。pct=本次解锁条目的 trigger 行值) - var pct_v := 0 - for pe in (u.get("pool", []) as Array): - 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 - # target_attack_bonus 施加面(处刑台:敌血量阈值沿挂单位字典标记, - # 主角攻击面 _resolve_hero_attack 通用消费——fx_atk_bonus 键加算进 base_value) - var eht_hits := _fx_host.fx_emit("enemy_hp_threshold", {"unit": String(u.enemy_id), "pct": pct_v, - "is_boss": String(_facts.encounter.get("node_type", "")) == "boss"}) - for h in eht_hits: - if String(h.get("kind", "")) == "target_attack_bonus": - u["fx_atk_bonus"] = int(u.get("fx_atk_bonus", 0)) + int(h.get("value", 0)) - var entries: Array[Variant] = _enemy_system.rollable_skills(u, facts) - if _enemy_system.basic_streak(u) >= pity_m: - var specials: Array[Variant] = _enemy_system.special_skills(u, facts) - if not specials.is_empty(): - entries = specials # 保底命中——普攻恒可摇 → 非保底路径池恒非空,此为唯一替代集合 - var stream: RandomNumberGenerator = rng.open_stream("behavior_roll") - _enemy_system.set_pending_action(u, SkillLottery.draw(entries, stream)) - _enemy_system.tick_cooldowns() # 递减在摇号读值之后——"施放后连续 N 拍不可摇中"的等价实现(单位容器 注记) - _mines_hits["last"] = _mines_hits["round"] # 轮次真正结束转存(轮内部分结算不更新) - _mines_hits["round"] = 0 - -# ---- 战斗段/算术读数(段体与掷点/折扣/暴击/克制/池乘区/下限/交付管线归 damage_controller.gd -# 与 settlement/combat_segment.gd;本类只保留薄委托——_defender_ac 供结算/呈现快照消费)---- - -# 防守方护甲等级读数——实现归 伤害控制器 defender_ac(本委托供结算快照/呈现快照) func _defender_ac(unit: Dictionary) -> int: return _damage.defender_ac(unit) @@ -683,7 +597,7 @@ func _exit_distribution() -> void: action_economy.notify_round_start() _facts.reset_round() _facts.refresh_used = 0 - _roll_round_actions() # 开轮摇号——每轮恰摇一次(轮内刷新换图不重摇:refresh 不走本通道) + _rounds.roll_round_actions() # 开轮摇号——每轮恰摇一次(轮内刷新换图不重摇:refresh 不走本通道) round_started.emit(round_index) @@ -831,7 +745,7 @@ func diagnostic_snapshot() -> Dictionary: return { "battle_result": battle_result, "round": round_index, - "settling": _settling, + "settling": _rounds.is_settling(), "refresh_used_round": _facts.refresh_used, "board_seq": _facts.board_seq, "facts_path": _facts.path, diff --git a/game/core/settlement/round_cycle.gd b/game/core/settlement/round_cycle.gd new file mode 100644 index 00000000..370e6c77 --- /dev/null +++ b/game/core/settlement/round_cycle.gd @@ -0,0 +1,106 @@ +## 轮循环驱动件:每轮结算周期的唯一驱动——结算重入门闸(settling)、五段段序执行 +## (段序骨架冻结于 SettlementTable.segment_order)、开轮摇号(共识点 B:技能选择前置到轮次开始, +## 摇定即本拍结算的选技)、踩雷计数账(转存时点=摇号轮末)。 +## 单一变化原因:轮结算周期的推进结构(段序、重入闸、摇号时序)。 +## 耦合边界:oc=宿主引用(五段段类既定同款直调形态)——经 oc 取 facts/协作件/行发射/出口分发; +## 五段构造参数与持有形态不变(grant/combat 三引用、aura/decay/verdict 单引用);判定段零改动 +## (_exit_distribution 留宿主);随机只经 oc.rng 的 behavior_roll 封闭流(流名、开流时点、取号序逐字); +## 零信号连接、零 await;随编排器每战新建同灭。 +class_name RoundCycle +extends RefCounted + +var oc: BattleOrchestrator # 宿主引用(段类同款直调——facts/协作件/行发射/出口分发取数单点) +var mines_hits: Dictionary[Variant, Variant] = {"total": 0, "last": 0, "round": 0} # 踩雷计数(本场累计/上轮转存/当轮累计三读数合一账) +var _grant_seg = null # GrantSegment(①授予段——含封印判与 counter/grant 助手单点) +var _aura_seg = null # AuraSegment(②增益/减益段) +var _combat_seg = null # CombatSegment(③战斗段) +var _decay_seg = null # DecaySegment(④衰减段) +var _verdict_seg = null # VerdictSegment(⑤判定段——返回裁定,终局早退语义归宿主) +var _settling := false + + +func _init(host: BattleOrchestrator, fx_host_ref: BattleFxHost, reward_grant_ref: RewardGrant) -> void: + oc = host + # 结算五段接线(授予/战斗两段持三引用=宿主+两协作件;信号连接纪律不涉段〔段零信号连接〕——五行自宿主 _init 逐字迁入) + _grant_seg = GrantSegment.new(oc, fx_host_ref, reward_grant_ref) + _aura_seg = AuraSegment.new(oc) + _combat_seg = CombatSegment.new(oc, fx_host_ref, reward_grant_ref) + _decay_seg = DecaySegment.new(oc) + _verdict_seg = VerdictSegment.new(oc) + + +# 结算重入门闸+段序体(原宿主 _maybe_settle 三闸与 _settle 合体——闸序 settling→battle_result→phase +# 逐字;终局早退不回置 _settling,_battle_over 接管) +func settle_if_due() -> void: + if _settling or oc.battle_result != "": + return + if oc.action_economy.phase != oc.action_economy.PHASE_WAITING: + return + _settling = true + var ri := oc.round_index + # 段序编排(五段——段体归 game/core/settlement/,宿主只做行发射/开单原语单点) + _grant_seg.run(ri) # ① 授予段(案C 闸随段装载) + _aura_seg.run(ri) # ② 增益/减益段(续存实例组合行) + _combat_seg.run(ri) # ③ 战斗段(主角先手 → 逐敌还手 → 地图伤害账殿后〔案C〕) + _decay_seg.run(ri) # ④ 衰减段(恰一次恒执行含末轮——先兑付后衰减) + var verdict: String = _verdict_seg.run(ri) # ⑤ 判定段(防御性优先级 hero 先判) + if verdict != "continue": + return # 终局:_settling 不回置——_battle_over 已接管 + _settling = false + + +func is_settling() -> bool: + return _settling + + +# ---- 开轮摇号(共识点 B:技能选择前置到轮次开始,数值计算留结算段——原宿主函数逐字迁入)---- +# 每实例:保底判定(连续执行普攻 ≥ M → 必出特殊技、特殊技内按权重、不受冷却限制)→ 排除冷却中技能 +# (中签率 = 权重 ÷ 可摇权重合计——份额归一)→ 按行序逐敌从统一流顺序取号摇定本拍动作。 +# 随机消费只在换算层(本方法 + SkillLottery);单位容器收确定值(零随机纪律——armor_roll 先例同构)。 +# 统一流:behavior_roll 无子流派生向量(位置交错,单点定位由事件日志承载)。 +# 摇定后、兑现前死亡 → 本拍技能作废。 +func roll_round_actions() -> void: + var pity_m: int = int(oc.db.combat.skill.pity_threshold) + # facts 组包(开轮摇号前一次成型——last_round_end 空字典首轮映射 none 唯一落点;hero 事实不进 facts, + # 容器内直读主角行) + var facts := { + "round_index": oc.round_index, + "last_round_end": "none" if oc.action_economy.last_round_end().is_empty() else String(oc.action_economy.last_round_end().get("path_tag", "none")), + "mines_hit_last": int(mines_hits["last"]), + "mines_hit_total": int(mines_hits["total"]), + } + for u in oc._enemy_system.query_alive("enemy"): + # Boss 半血解锁闩(迭代14④ T4——开轮摇号前逐敌检查;一次过门单向闩;解锁出一次归因明细行 + # 「XX 解锁了 YY」带 skill_unlock 高光键〔type=highlight 既有行型零新增〕,呈现层弹一次性 + # 技能名提示〔轻量 toast 非演出——D8 呈现面〕。门=确定性谓词,机制面零随机,摇号随机 + # 消费仍在 behavior_roll 既有流——同种子逐位复现不破坏) + for sid in oc._enemy_system.check_trigger_unlocks(u, facts): + var srow_u: Dictionary[Variant, Variant] = oc.db.combat.skill.row_by_id(sid) + oc._emit_row({"type": "highlight", "source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "highlight": "skill_unlock"}, + oc.round_index, "%s 解锁了 %s" % [String(u.display_name), String(srow_u.get("display_name", sid))], + 0, {"skill_id": sid}) + # enemy_hp_threshold 发射点(内容数据驱动扩展批 T1——血量阈值沿;对所有带该字段敌人逐个发射, + # 非 Boss 专属——Boss 语义留给订阅者 when 判定。pct=本次解锁条目的 trigger 行值) + var pct_v := 0 + for pe in (u.get("pool", []) as Array): + 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 + # target_attack_bonus 施加面(处刑台:敌血量阈值沿挂单位字典标记, + # 主角攻击面 _resolve_hero_attack 通用消费——fx_atk_bonus 键加算进 base_value) + var eht_hits := oc._fx_host.fx_emit("enemy_hp_threshold", {"unit": String(u.enemy_id), "pct": pct_v, + "is_boss": String(oc._facts.encounter.get("node_type", "")) == "boss"}) + for h in eht_hits: + if String(h.get("kind", "")) == "target_attack_bonus": + u["fx_atk_bonus"] = int(u.get("fx_atk_bonus", 0)) + int(h.get("value", 0)) + var entries: Array[Variant] = oc._enemy_system.rollable_skills(u, facts) + if oc._enemy_system.basic_streak(u) >= pity_m: + var specials: Array[Variant] = oc._enemy_system.special_skills(u, facts) + if not specials.is_empty(): + entries = specials # 保底命中——普攻恒可摇 → 非保底路径池恒非空,此为唯一替代集合 + var stream: RandomNumberGenerator = oc.rng.open_stream("behavior_roll") + oc._enemy_system.set_pending_action(u, SkillLottery.draw(entries, stream)) + oc._enemy_system.tick_cooldowns() # 递减在摇号读值之后——"施放后连续 N 拍不可摇中"的等价实现(单位容器 注记) + mines_hits["last"] = mines_hits["round"] # 轮次真正结束转存(轮内部分结算不更新) + mines_hits["round"] = 0 diff --git a/game/core/settlement/round_cycle.gd.uid b/game/core/settlement/round_cycle.gd.uid new file mode 100644 index 00000000..f7d726e5 --- /dev/null +++ b/game/core/settlement/round_cycle.gd.uid @@ -0,0 +1 @@ +uid://ce07smbypdcyj diff --git a/game/tests/static_scan.sh b/game/tests/static_scan.sh index df9951d6..c76494db 100644 --- a/game/tests/static_scan.sh +++ b/game/tests/static_scan.sh @@ -314,7 +314,7 @@ fi # get_tree());排除注释行(行首 #/##)与字符串字面量(双引号包裹段内的 await 非代码), # 口径仿 S1/S2 的 grep 排除手法。 # 自检:清单内文件缺位(路径写错/被改名)即判红——防扫面为空静默绿。 -S8_FILES="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/damage_controller.gd" +S8_FILES="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd" S8_MISS=0 for f in $S8_FILES; do if [ ! -f "$f" ]; then @@ -356,7 +356,7 @@ fi # 自检:扫描面为空即判红(require_scan_face);B1 文件集与三族自家文件缺位即判红 # (文件被改名=豁免名单失效,宁可红不可静默放行)。 if require_scan_face "S7" "$DIR_CORE"; then -S7_B1_SET="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/damage_controller.gd" +S7_B1_SET="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd" S7_MISS=0 for f in $S7_B1_SET game/core/unit_container.gd game/core/status_container.gd game/core/reward_economy.gd; do if [ ! -f "$f" ]; then diff --git a/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd b/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd index f8450315..677a9f25 100644 --- a/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd +++ b/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd @@ -1024,7 +1024,7 @@ func _smoke_counter_buckets() -> void: if not oA.minesweeper.current_board().is_mine(ca): safe_a = ca break - oA._grant_seg._judge_seal({"cell": safe_a, "hit": false}, 1) + oA._rounds._grant_seg._judge_seal({"cell": safe_a, "hit": false}, 1) Kit.check("V6 第1战标错落 battle 桶 insurance_used=1(_apply_counter_hit 静态写口经 ctx)〔无状态化 V6〕", EffectEngine.counter(oA.fx_host().counters_ctx, "insurance_used") == 1) # 卡④ wr2①——完美保险标错行=单一扩展行型(原 insurance_used 实名行型收敛) var v6_ins := {} diff --git a/game/tests/unit/battle_settlement/trigger_cond_test.gd b/game/tests/unit/battle_settlement/trigger_cond_test.gd index cd7c0709..026b88d6 100644 --- a/game/tests/unit/battle_settlement/trigger_cond_test.gd +++ b/game/tests/unit/battle_settlement/trigger_cond_test.gd @@ -116,19 +116,19 @@ func _init() -> void: "feats": [], "tree_rules": [], "battle_index": 1, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle", "adv_bucket": {}}), "V6 前置:战斗受理") # facts 组包经真实摇号路径消费(mines 全零时条件技 sneak_ambush 不入集合——facts 四键在位的可观察面) - Kit.check_quiet(int(orch._mines_hits["total"]) == 0 and int(orch._mines_hits["last"]) == 0 and int(orch._mines_hits["round"]) == 0, + Kit.check_quiet(int(orch._rounds.mines_hits["total"]) == 0 and int(orch._rounds.mines_hits["last"]) == 0 and int(orch._rounds.mines_hits["round"]) == 0, "V6 开战清零:三计数全零起〔触发器批 V6〕") # 正向 blast(spark_aoe)不递增——负向计数守卫(flat 行应用面自编排器迁至奖励授予件 # ——迭代40 卡① 拆件;计数经授予件 battle_hooks.negative_blast 回宿主 _mines_hits) var att_v6 := {"source_class": "reward", "cell": Vector2i(-1, -1), "ri": 1} orch.reward_grant()._apply_flat_row({"cell_type": "blast", "polarity": "positive", "power": 2, "display_name": "爆裂", "reward_id": "spark_aoe"}, att_v6) - Kit.check_quiet(int(orch._mines_hits["total"]) == 0 and int(orch._mines_hits["round"]) == 0, + Kit.check_quiet(int(orch._rounds.mines_hits["total"]) == 0 and int(orch._rounds.mines_hits["round"]) == 0, "V6 正向 blast(spark_aoe)不递增(负向守卫——清图/爆裂格零误计)〔触发器批 V6〕") # 负向 blast(雷)递增 orch.reward_grant()._apply_flat_row({"cell_type": "blast", "polarity": "negative", "power": 2, "display_name": "警觉", "reward_id": "enemy_alert"}, att_v6) - Kit.check_quiet(int(orch._mines_hits["total"]) == 1 and int(orch._mines_hits["round"]) == 1, + Kit.check_quiet(int(orch._rounds.mines_hits["total"]) == 1 and int(orch._rounds.mines_hits["round"]) == 1, "V6 负向 blast(雷)递增恰一次〔触发器批 V6〕") # ---- V7 池构成纪律(现行全表零违例+构造全条件池报错——跨表校验真实入口)---- diff --git a/game/tests/unit/core/feat_vouchers_test.gd b/game/tests/unit/core/feat_vouchers_test.gd index 7d7a8414..33631f57 100644 --- a/game/tests/unit/core/feat_vouchers_test.gd +++ b/game/tests/unit/core/feat_vouchers_test.gd @@ -176,7 +176,7 @@ func _init() -> void: orch.step_row.connect(func(row): rows_orch.append(row)) # step_row 公开信号面(insurance_used 载荷/seal_miss highlight=豁免账与误标账观测面) Kit.check_quiet(int(orch._facts.perfect_hit_false) == 0 and int(orch._facts.perfect_forgiven) == 0, "保险前置:误标账与豁免账双零起〔卡② R2〕") - orch._grant_seg._judge_seal({"cell": safe_cells[0], "hit": false}, 1) + orch._rounds._grant_seg._judge_seal({"cell": safe_cells[0], "hit": false}, 1) Kit.check_quiet(int(orch._facts.perfect_hit_false) == 1, "计数纯净:首次标错 _perfect_hit_false 落 1 未被回退改写(契约 2)〔卡② R2〕") var ins_row := {} @@ -187,7 +187,7 @@ func _init() -> void: "豁免一次:首次标错 _perfect_forgiven=1(forgive_seal_miss 派发)〔卡② R2〕") Kit.check_quiet(int(orch.action_economy.pending_ap_delta()) == 1, "事件不吞:同一标错的 seal_missed 仍被危险直觉·守收到(queue AP+1)〔卡② R2〕") - orch._grant_seg._judge_seal({"cell": safe_cells[1], "hit": false}, 2) + orch._rounds._grant_seg._judge_seal({"cell": safe_cells[1], "hit": false}, 2) var ins_count := 0 var miss_hl_last := "" for r in rows_orch: @@ -218,7 +218,7 @@ func _init() -> void: var rows_orch2: Array[Dictionary] = [] orch2.step_row.connect(func(row): rows_orch2.append(row)) # step_row 公开信号面(insurance_used 载荷=豁免账观测面) var sc2 = safe_cells[0] - orch2._grant_seg._judge_seal({"cell": sc2, "hit": false}, 1) + orch2._rounds._grant_seg._judge_seal({"cell": sc2, "hit": false}, 1) var ins_row2 := {} for r in rows_orch2: if String(r.get("effect_id", "")) == "insurance_used": @@ -245,7 +245,7 @@ func _init() -> void: if not o10.minesweeper.current_board().is_mine(c10): sc10 = c10 break - o10._grant_seg._judge_seal({"cell": sc10, "hit": false}, 1) + o10._rounds._grant_seg._judge_seal({"cell": sc10, "hit": false}, 1) var sm10: Dictionary = {} for r10 in rows10: if String(r10.get("type", "")) == "seal_miss": @@ -272,7 +272,7 @@ func _init() -> void: if not o11.minesweeper.current_board().is_mine(c11): sc11 = c11 break - o11._grant_seg._judge_seal({"cell": sc11, "hit": false}, 1) + o11._rounds._grant_seg._judge_seal({"cell": sc11, "hit": false}, 1) var sm11: Dictionary = {} for r11 in rows11: if String(r11.get("type", "")) == "seal_miss": diff --git a/game/tests/unit/core/meta_progression_forge_test.gd b/game/tests/unit/core/meta_progression_forge_test.gd index 0b3360c8..508396dd 100644 --- a/game/tests/unit/core/meta_progression_forge_test.gd +++ b/game/tests/unit/core/meta_progression_forge_test.gd @@ -705,16 +705,16 @@ func _smoke_tree_rules() -> void: Kit.check("守侧 rig:首轮后 queue 干净(开局 posture queue 已兑现)", int(orch.action_economy.pending_ap_delta()) == 0) # 场景①:当回合新封印、结算判标错且封印仍在场 → queue +1(本轮 AP 零变化=queue 承载、下轮 notify_round_start 兑现) - orch._grant_seg._judge_seal({"cell": safe, "hit": false}, 1) + orch._rounds._grant_seg._judge_seal({"cell": safe, "hit": false}, 1) Kit.check("守①:当回合标错且封印在场 → 下轮 AP+1(queue 排队——本轮 AP 逐位零变化)", int(orch.action_economy.pending_ap_delta()) == 1) # 场景③④:之前回合标错重判(封印在场状态)与同格重复——去重闸挡回,最多返还一次 - orch._grant_seg._judge_seal({"cell": safe, "hit": false}, 2) - orch._grant_seg._judge_seal({"cell": safe, "hit": false}, 3) + orch._rounds._grant_seg._judge_seal({"cell": safe, "hit": false}, 2) + orch._rounds._grant_seg._judge_seal({"cell": safe, "hit": false}, 3) Kit.check("守③④:旧封印重判零触发+同格最多一次(_seal_miss_seen 去重闸=「当回合标错动作」判别器)", int(orch.action_economy.pending_ap_delta()) == 1) # 场景②负样本:封印命中(hit=true 正向兑付路径)零触发 - orch._grant_seg._judge_seal({"cell": safe, "hit": true}, 4) + orch._rounds._grant_seg._judge_seal({"cell": safe, "hit": true}, 4) Kit.check("守②负样本:标中/被挖封印零触发(标错事件只在 miss 分支发射——发射面过滤承载)", int(orch.action_economy.pending_ap_delta()) == 1) # 场景⑤:末轮判出→无下一轮=queue 不兑现(编排器每战新建——队列实例随战亡,合法作废态) diff --git a/game/tests/unit/core/settle_segments_test.gd b/game/tests/unit/core/settle_segments_test.gd index e7f23209..1714b14e 100644 --- a/game/tests/unit/core/settle_segments_test.gd +++ b/game/tests/unit/core/settle_segments_test.gd @@ -23,12 +23,15 @@ func _init() -> void: Kit.check_quiet(load("res://core/settlement/decay_segment.gd").new(oc) != null, "V4 DecaySegment 直接 new()〔迭代27 T2〕") Kit.check_quiet(load("res://core/settlement/verdict_segment.gd").new(oc) != null, "V4 VerdictSegment 直接 new()〔迭代27 T2〕") - # V4-2 编排器 _init 接线(段持宿主引用;段序编排面在位) - Kit.check_quiet(oc._grant_seg != null and oc._grant_seg.oc == oc, "V4 编排器接线·授予段(宿主回指)〔迭代27 T2〕") - Kit.check_quiet(oc._aura_seg != null and oc._aura_seg.oc == oc, "V4 编排器接线·增益/减益段〔迭代27 T2〕") - Kit.check_quiet(oc._combat_seg != null and oc._combat_seg.oc == oc, "V4 编排器接线·战斗段〔迭代27 T2〕") - Kit.check_quiet(oc._decay_seg != null and oc._decay_seg.oc == oc, "V4 编排器接线·衰减段〔迭代27 T2〕") - Kit.check_quiet(oc._verdict_seg != null and oc._verdict_seg.oc == oc, "V4 编排器接线·判定段〔迭代27 T2〕") + # V4-2 编排器 _init 接线(段持宿主引用;段序编排面在位)——注册面批同代归约 V1: + # 段件宿主直持改经 _rounds(轮循环驱动件)承载,接线断言改指 _rounds 段族 + Kit.check_quiet(oc._rounds != null and oc._rounds.oc == oc, "V1 编排器接线·轮循环驱动件在座且宿主回指〔编排归约 V1〕") + Kit.check_quiet(oc._facts != null, "V1 编排器接线·战斗事实件在座〔编排归约 V1〕") + Kit.check_quiet(oc._rounds._grant_seg != null and oc._rounds._grant_seg.oc == oc, "V4 编排器接线·授予段(宿主回指)〔迭代27 T2〕") + Kit.check_quiet(oc._rounds._aura_seg != null and oc._rounds._aura_seg.oc == oc, "V4 编排器接线·增益/减益段〔迭代27 T2〕") + Kit.check_quiet(oc._rounds._combat_seg != null and oc._rounds._combat_seg.oc == oc, "V4 编排器接线·战斗段〔迭代27 T2〕") + Kit.check_quiet(oc._rounds._decay_seg != null and oc._rounds._decay_seg.oc == oc, "V4 编排器接线·衰减段〔迭代27 T2〕") + Kit.check_quiet(oc._rounds._verdict_seg != null and oc._rounds._verdict_seg.oc == oc, "V4 编排器接线·判定段〔迭代27 T2〕") # counter_add 写口真实路径(geologist 订阅行承载:公共命令链 begin_battle→move→dig→stop # 经授予段 counter_add 单点落 counters_ctx battle 桶——受理载荷+公开读数口 fx_host().counters_ctx) var rng_c = load("res://core/rng_service.gd").new()