refactor(迭代27 T2): 波二 _settle 五段拆 Segment+_resolve_hero_attack 拆步——逐位一致对拍全同

P1-1 结算巨函数拆段(裁点③甲 审计五段):
- game/core/settlement/ 五段类: GrantSegment(授予段体+旗判随段迁入+counter_add 五位/
  grant_material 三位分支单点化)/AuraSegment/CombatSegment(原 _combat_segment 体)/
  DecaySegment/VerdictSegment(返回裁定,终局早退语义留宿主)
- 编排器 1725→1325 行退为段序编排+行发射/开单原语单点;_judge_flag/_combat_segment
  旧实现同批删除;smoke 白盒 rig 随段迁址(orch._grant_seg._judge_flag)

姊妹病灶(同批):
- damage_controller _resolve_hero_attack 148 行拆四步: _hero_attack_ctx(命中面/按序改写
  族四拍)→_hero_attack_base_value(加法族/计数器施加/倍乘)→_hero_attack_redeem(掷点/池乘/
  rider/落地/击杀)→_hero_attack_deliver(状态/交付行);ctx/redemption 字典穿线算序逐字保留

验收:
- V4 settle_segments_test 16 断言进 run_all(25→26 套)
- 逐位一致对拍三变体全同(RUNSUM+JSONL 双面零漂移);冒烟 1077/0+run_all 26 套
  +probe click+render golden 0.00%
This commit is contained in:
DKChan committed 2026-10-14 22:09:29 +08:00
1 parent 7a0d57ddb0
commit 26301baa9c
16 files changed
+646 -444

No files matched your search

+23 -423
View File
@@ -70,6 +70,14 @@ var _facts_path := ""
var _settling := false
var _defer_board_damage := false # 案C 暂存闸(2026-10-06 用户裁:地图奖励伤害兑付迁战斗段末——怪物还手后)
var _deferred_board: Array[Dictionary] = [] # 授予段暂存伤害单据(保授予段事件序;战斗段怪物还手后按序补提交统一结账)
# 结算五段 Segment(迭代 27 T2——P1-1 拆段;段边界=审计五段〔裁点③甲〕,段序骨架冻结于
# SettlementTable.segment_order 语义:授予→增益/减益→战斗→衰减→判定。本类退为段序编排+
# 行发射/开单原语单点——段体逻辑归 game/core/settlement/ 五类)
var _grant_seg = null # GrantSegment(①授予段——含旗判与 counter/grant 助手单点)
var _aura_seg = null # AuraSegment(②增益/减益段)
var _combat_seg = null # CombatSegment(③战斗段——原 _combat_segment 体)
var _decay_seg = null # DecaySegment(④衰减段)
var _verdict_seg = null # VerdictSegment(⑤判定段——返回裁定,终局早退语义归本类)
var _refresh_used_this_round := 0 # 每轮刷新计数(Active 相位合计,兜底上限 = ActionEconomyTable.refresh_cap_per_round)
var _board_seq := 0 # 板序号(本战斗第几张板——快照归因用)
var _board_from_midround_refresh := false # 装板 provenance:轮内刷新换来的图 = true(清图奖励守卫——刷新图不发清图奖励)
@@ -303,6 +311,12 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
minesweeper.flag_judged.connect(_on_flag_judged)
minesweeper.round_end_board.connect(_on_round_end)
action_economy.round_end_stopped.connect(_on_round_end)
# 结算五段接线(迭代 27 T2——段持宿主引用;信号连接纪律不涉段〔段零信号连接,ADR-0001 同构〕)
_grant_seg = GrantSegment.new(self)
_aura_seg = AuraSegment.new(self)
_combat_seg = CombatSegment.new(self)
_decay_seg = DecaySegment.new(self)
_verdict_seg = VerdictSegment.new(self)
# ---- 战斗开始序列(编排专章①:门 → 增益状态 → 敌人系统 → install+roll → 行动经济)----
@@ -631,254 +645,16 @@ func _maybe_settle() -> void:
func _settle() -> void:
_settling = true
_defer_board_damage = true # 案C:授予段伤害单据只暂存不结账(2026-10-06 用户裁——兑付时点=战斗段末)
var ri := round_index
# ① 授予段:结算队列(开格/旗判定/清图)→ 极性路由逐批兑付(伤害形态单据暂存——见 _submit 案C 闸)。
# 判定基线 = 轮末在场旗(发射面已过滤——被挖开的旗已随挖掘擦除零兑付,插旗不可逆批 2026-09-19;
# 开格事实不可撤)。
for evt in _facts_reveals:
if not bool(evt.get("resource", false)):
continue # 封锁格(initial 通道)恒无资源——不入账(战斗结算 义务②)
var snap = evt.get("reward_snapshot")
if not (snap is Dictionary):
continue
if evt.kind == "safe":
if bool(evt.get("dug_under_flag", false)):
# 插旗不可逆四情况④(2026-09-19 用户拍板):插安全格再挖开 = 零惩罚零奖励——正向兑付
# 作废(归因行在案);标空判定已随旗擦除自然缺席(被挖的旗不在轮末在场基线)。
_emit_row("reward_ineffective", "safe_cell", evt.cell, ri, "奖励作废(插旗格直接挖开——零惩罚零奖励)", 0, {}, "")
continue
# dig_reveal 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价:
# 乘区收集 → 正向兑付 → 计数器 → 追加产出(深挖)→ 奥术矢。
# 迭代 20 T1——payload 扩三事实:is_edge_cell〔边陲矿工〕/is_blank_zero〔跳格采掘〕/
# new_resource_type〔地质学家——快照 cell_type 首见判定,见后集登记〕)
var board_b: BoardData = minesweeper.current_board()
var snap_pre = evt.get("reward_snapshot")
var ctype := String((snap_pre as Dictionary).get("cell_type", "")) if snap_pre is Dictionary else ""
var is_new_type := ctype != "" and not _fx_new_resource_types.has(ctype)
if is_new_type:
_fx_new_resource_types[ctype] = true
var dr_hits := _fx_emit("dig_reveal", {"cell": evt.cell, "round": ri,
"is_first_dig_of_battle": not _feat_first_dig_used,
"terrain_kind": _harmful_variants(evt.get("variant_ids", []) as Array),
"is_edge_cell": evt.cell.x == 0 or evt.cell.y == 0
or evt.cell.x == board_b.width - 1 or evt.cell.y == board_b.height - 1,
"is_blank_zero": board_b.digit_of(evt.cell) == 0,
"new_resource_type": is_new_type})
var pos_m := 0
var neg_m := 0
var combo_cap := -1
for h in dr_hits:
if String(h.get("kind", "")) != "yield_mult":
continue
pos_m += int(h.get("mult", 0))
neg_m += int(h.get("cost_mult", 0))
if int(h.get("cap", 0)) > 0:
combo_cap = int(h.cap) # 矿脉直觉连击上限
_grant_routed(_feat_yield_adjust(snap, true, pos_m, neg_m, combo_cap), "positive", "safe_cell", evt.cell, ri, "")
# 计数器执行(连击计数——加法族,乘区收集后执行=本格兑付用旧值,与迁移前逐位一致)
for h in dr_hits:
if String(h.get("kind", "")) == "counter_add":
var set_v = h.get("set", null)
_fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
# 揭示效果入队(迭代 20 T1——地质学家/跳格采掘 reveal_tiles 订阅面;授予段末
# _flush_pending_reveals 统一掷格兑付)
if not _hits_of(dr_hits, "reveal_tiles").is_empty():
_fx_enqueue_reveal()
# 追加产出(深挖钻头 v2——本格双份制:grant_material 命中=配额内追加第二份;标签随行)
for h in dr_hits:
if String(h.get("kind", "")) == "grant_material":
var dd_snap = _reward_economy.grant(evt.cell)
if dd_snap is Dictionary:
_grant_routed(dd_snap, "positive", "safe_cell", evt.cell, ri, "", String(h.get("label", "")))
# 奥术矢(魔能共鸣——每战首次开安全格追加魔法矢;任意真族单次生效)
var spell_hits: Array = _hits_of(dr_hits, "spawn_spell")
if not spell_hits.is_empty() and not _feat_first_dig_used:
_feat_first_dig_used = true
var spell_t := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_first_dig_spell",
"奥术矢", "board", int((spell_hits[0] as Dictionary).get("value", 0)),
DamageController.TARGET_FRONT_ALIVE, ri, evt.cell, "magic")
_submit(spell_t, false)
elif evt.kind == "mine":
# 插旗不可逆四情况②:插雷格再挖开 = 只受挖雷一层惩罚(mine_hit 照常兑付——惊动巢穴),
# 标中不发(旗已随挖掘擦除、不入轮末判定基线)。
# mine_hit 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价:
# 减半收集 → 负向兑付〔带减半系数〕→ 反打开单 → 连击清零计数器。
# 迭代 20 T1——发射前移+payload 扩 dug_under_flag〔破封一击〕;新增施加面:
# damage_reduce〔安全帽耗层减伤——value_counter 层值,一次清零〕/ heal〔血蜜〕/
# blast_scaled〔陷阱爆发——雷伤×倍率全体敌〕/ bonus_delta〔血税〕/ reveal_tiles〔见血开窍〕)
var mine_power := int((snap as Dictionary).get("power", 0))
var mh_hits := _fx_emit("mine_hit", {"cell": evt.cell, "round": ri,
"damage": mine_power, "dug_under_flag": bool(evt.get("dug_under_flag", false)),
"hp_ratio": int(round(100.0 * float(int(_enemy_system.hero_unit().get("hp", 0))) / float(maxi(1, int(_enemy_system.hero_unit().get("max_hp", 1))))))})
var halve_pct := 0
var mine_reduce := 0
for h in mh_hits:
match String(h.get("kind", "")):
"damage_mult":
halve_pct += int(h.get("value", 0))
"damage_reduce":
if h.has("value_counter") and String(h.get("value_counter", "")) != "":
mine_reduce += _fx.counter(String(h.get("value_counter", ""))) # 安全帽——层值减免
else:
mine_reduce += int(h.get("value", 0)) # 背水——定值减免
"heal":
var hu_heal: Dictionary = _enemy_system.hero_unit()
hu_heal["hp"] = mini(int(hu_heal.get("hp", 0)) + int(h.get("value", 0)), maxi(1, int(hu_heal.get("max_hp", 1))))
"bonus_delta":
var axis := String(h.get("axis", ""))
if _fx_bonus_delta.has(axis):
_fx_bonus_delta[axis] += int(h.get("delta", 0))
"blast_scaled":
var blast_v := maxi(int(db.settlement.damage_floor), int(float(mine_power) * float(h.get("mult", 1))))
var bs_t := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_blast_scaled", "陷阱爆发", "board",
blast_v, DamageController.TARGET_ALL_ALIVE, ri, evt.cell, "", 0, "", {})
_submit(bs_t, false)
if mine_reduce > 0:
snap["power"] = maxi(0, mine_power - mine_reduce) # 减伤落快照(负向兑付按减后值——安全帽每层减 1)
for h in mh_hits:
if String(h.get("kind", "")) == "counter_add":
var set_v = h.get("set", null)
_fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
if not _hits_of(mh_hits, "reveal_tiles").is_empty():
_fx_enqueue_reveal() # 见血开窍——每颗雷一次揭示(授予段末统一兑付)
_grant_routed(_feat_yield_adjust(snap, false, 0, 0, -1), "negative", "mine_hit", evt.cell, ri, "", "", halve_pct)
for h in mh_hits:
if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "reflect":
var nct := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "attune_nest_counter", "惊巢反制·反弹", "board",
int(h.get("value", 0)), DamageController.TARGET_ALL_ALIVE, ri, evt.cell, "", 0, "", {})
_submit(nct, false)
# 地形变异 兑付段(授予段尾——触发枚举 on_open:挖开即伤、chain 通道同口径;伤害单据案C 暂存到
# 战斗段末兑付〔2026-10-06 用户裁〕;信息型 hide_number 不进结算——呈现层读取,见 board_view 数字遮蔽;
# 雷格恒无变异——布置段排除,双层叠罚不存在;插旗格直挖照吃变异伤害——地形后果同 mine_hit 口径,
# 非奖励不作废;吞噬流资源兑付〔荆棘编织/地脉补偿〕中伤害形态行同样经案C闸暂存,非伤害形态照常当场)
for evt in _facts_reveals:
var vids: Array = evt.get("variant_ids", [])
for vid in vids:
var vrow: Dictionary = db.variation.row_by_id(String(vid))
if String(vrow.get("effect", "")) != "flat_damage":
continue # 触发词表按 effect 路由:flat_damage=on_open 兑现;hide_number/on_open 外组合 = 数据行新词表未接线(显式忽略面)
# 地形变异伤害单据(伤害管线单据化批 2026-09-24)——无类型必中,下限保底在控制器管线内
# 迭代 20 T1——damage_taken 发射前移(单据开立前):damage_reduce〔背水——hp_ratio when
# 面〕先收集再按减后值开单;counter_add〔保险丝/石肤〕+ ac_bonus〔石肤〕照常施加;
# grant_material〔荆棘/地脉〕移到单据提交后(与迁移前兑付值序一致——收益不因减伤变化)
var dt_amount := int(vrow.get("effect_value", 0))
var dt_hits := _fx_emit("damage_taken", {"kind": "terrain", "amount": dt_amount,
"source": String(vid), "cell": evt.cell, "round": ri, "terrain_kind": String(vid),
"hp_ratio": int(round(100.0 * float(int(_enemy_system.hero_unit().get("hp", 0))) / float(maxi(1, int(_enemy_system.hero_unit().get("max_hp", 1))))))})
for h in dt_hits:
match String(h.get("kind", "")):
"damage_reduce":
dt_amount = maxi(0, dt_amount - int(h.get("value", 0)))
"ac_bonus":
_apply_ac_bonus(h)
"counter_add":
var set_v = h.get("set", null)
_fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
var vt := _damage.open_ticket(DamageController.KIND_BOARD_FLAT_HERO, "地形变异", String(vrow.get("display_name", vid)), "board", \
dt_amount, DamageController.TARGET_HERO, ri, evt.cell, "", 0, "", {"variant_id": String(vid)})
_submit(vt, false)
for h in dt_hits:
if String(h.get("kind", "")) == "grant_material":
var tw_snap = _reward_economy.grant(evt.cell)
if tw_snap is Dictionary:
_grant_routed(tw_snap, "positive", "safe_cell", evt.cell, ri, "", String(h.get("label", "")))
for evt in _facts_flags:
_judge_flag(evt, ri)
# 战旗军规开单段(迭代 12 T3 标旗流——flag_judged 批后统一开单:轮末每面标中旗对最前存活敌
# 结算 board_typed 必中伤害〔复用零新配方;「旗所在巢穴敌」简化为 default_target 最前存活敌——
# 台账披露待追认〕;完美布防=本轮旗批全中(hit>0 且 miss==0)时旗伤 ×mult——全对推理奖励)。
# 伤害单据案C 暂存战斗段末兑付。
# flag_settle 发射+施加(内容数据驱动扩展批 T3——战旗 open_ticket + 完美布防 modify_ticket
# 改写窗口:旗伤基值×完美倍率后逐旗开单,与迁移前算序一致)
var fs_hits := 0
var fs_misses := 0
for fevt in _facts_flags:
if bool(fevt.get("hit", false)):
fs_hits += 1
else:
fs_misses += 1
var fs_fx := _fx_emit("flag_settle", {"flags_hit_all": fs_hits > 0 and fs_misses == 0,
"flags_count": fs_hits, "round": ri})
var banner_hits: Array = []
for h in fs_fx:
if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "attack":
banner_hits.append(h)
if not banner_hits.is_empty():
var strike_value := int((banner_hits[0] as Dictionary).get("value", 0))
var flag_mult := 1
for h in fs_fx:
if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "flag_mult":
flag_mult *= maxi(1, int(h.get("value", 1)))
if fs_misses == 0 and fs_hits > 0 and flag_mult > 1:
strike_value *= flag_mult # 完美布防:同轮全中旗伤倍增(全对推理奖励)
for fevt in _facts_flags:
if not bool(fevt.get("hit", false)):
continue
var st_t := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_flag_strike",
"战旗军规", "board", strike_value, DamageController.TARGET_FRONT_ALIVE, ri,
fevt.cell, "normal")
_submit(st_t, false)
# round_end 发射+施加(内容数据驱动扩展批 T3——每轮结算收口:熔火之躯反打单开立〔open_ticket
# 即时执行〕+积层清零〔counter_add set〕;整备转蓄力由 finish_round_stop 承载——停手侧 AP 已在
# request_stop 记账,转存决策=grant_ap(mode:transfer) 订阅定夺,无订阅走 RAP 转存缺省)
var re_fx := _fx_emit("round_end", {"round": ri})
for h in re_fx:
if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "blast":
var mg_layers := _fx.counter(String(h.get("per", "")))
var mg_total := int(h.get("value", 0)) * mg_layers
if mg_total > 0:
# 迭代 20 T9——source_class 中性名(旧件名 feat_magma_body 随熔火之躯退役;per-counter 反打通道保留泛用面)
var mg_t := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "fx_blast_counter",
"积层反噬 ×%d" % mg_layers, "board", mg_total,
DamageController.TARGET_ALL_ALIVE, ri, minesweeper.hero_pos(), "", 0, "", {})
_submit(mg_t, false)
for h in re_fx:
if String(h.get("kind", "")) == "counter_add":
var set_v = h.get("set", null)
_fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
# 迭代 20 T1——round_end 新施加面:grant_gp〔爆破会计/收盘——escrow 战斗域缓冲,随 gp_dropped 上抛〕/
# grant_ap mode:queue〔封印回响——返 1 AP 下轮,queue_ap_delta 通道与轮始加成同面〕
for h in re_fx:
if String(h.get("kind", "")) == "grant_gp":
escrow += int(h.get("value", 0))
_emit_row("effect", "fx_grant_gp", Vector2i(-1, -1), ri, "专长·GP 入账 +%d" % int(h.get("value", 0)), int(h.get("value", 0)), {"effect_id": "fx_grant_gp", "escrow": escrow}, "")
elif String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue":
action_economy.queue_ap_delta(int(h.get("value", 0)))
# 转存率入参(迭代 22 秘宝批 T3——储备罐 rap_transfer_mult magnitude=目标转存率百分比 125=1.25:1;
# 未装配=100 与旧 1:1 逐位一致)
var rap_pct: int = attune_param_sum("rap_transfer_mult")
action_economy.finish_round_stop(_fx != null and _fx.has_effect_on("round_end", "grant_ap", "transfer"),
rap_pct if rap_pct > 0 else 100)
if _facts_path == "cleared":
_grant_clear_reward(ri)
_flush_pending_reveals(ri) # 迭代 20 T1——揭示效果兑付(授予段末统一掷格,防轮 facts 迭代重入)
_damage.flush() # 授予段界结账(案C 后本段伤害单据全数暂存,此处对空队 no-op——保留作防御性段界)
# ② 增益/减益段(续存实例组合行;本结算新授予的已在授予段逐 grant 出行——增益状态 义务⑦)
_emit_composition_effects("hero", ri)
_emit_composition_effects("enemy", ri)
# ③ 战斗段(调用序:主角先手 → 逐敌还手 → 地图伤害账殿后〔案C 2026-10-06 用户裁〕;hero hp=0 短路)
_combat_segment(ri)
# ④ 衰减段(恰一次恒执行含末轮——先兑付后衰减)
var k: int = _status_effects.decay_round()
_emit_row("decay", "", Vector2i(-1, -1), ri, "状态衰减", k, {}, "")
# ⑤ 判定段(防御性优先级 hero 先判——双死结构不可达的纵深防御)
var verdict := "continue"
if int(_enemy_system.hero_unit().get("hp", 0)) <= 0:
verdict = "defeat"
elif _enemy_system.query_alive("enemy").is_empty():
verdict = "victory"
_emit_settlement_snapshot(verdict) # 全量状态快照(despawn 前组装——终局含 result)
if verdict == "defeat":
_emit_row("verdict", "", Vector2i(-1, -1), ri, "战斗失败", 0, {}, "")
_battle_over("defeat")
return
if verdict == "victory":
_emit_row("verdict", "", Vector2i(-1, -1), ri, "战斗胜利", 1, {}, "")
_battle_over("victory")
return
_emit_row("verdict", "", Vector2i(-1, -1), ri, "下一轮次", 0, {}, "")
_exit_distribution()
# 段序编排(迭代 27 T2——五段 Segment;段体归 game/core/settlement/,本类退为段序+
# 行发射/开单原语单点。旧 259 行巨函数体已逐字迁五段类,git 史留痕)
_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
@@ -1162,95 +938,6 @@ func _submit_flat_hero(source_class: String, display: String, power: int, cell:
# 旗判定批:标中 = 该雷格资源的正向兑付(每雷实例一次;高光 = 强制检查点);
# 标空 = "标旗未中"归因行 + 该安全格资源的负向兑付(同键去重——每格实例一次)。
func _judge_flag(evt: Dictionary, ri: int) -> void:
var cell: Vector2i = evt.cell
if evt.hit:
if _flag_consumed.has(cell):
return # 去重跳过(资格以雷格实例为键——排雷玩法 诚实重复上报的对侧);零明细行
_flag_consumed[cell] = true
_perfect_hit_true += 1
# flag_hit 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价:殉爆〔T4 秘宝面〕→
# 净化改写 → 图谱额外资源 → 基线兑付)
var fh_hits := _fx_emit("flag_hit", {"cell": cell, "round": ri,
"terrain_kind": _harmful_adjacent(cell)})
# 标中殉爆(迭代 11 同调批 T5 → 内容数据驱动扩展批 T4:订阅施加——行 triggers open_ticket
# channel blast value_from magnitude;每标中一面旗全体存活敌吃 N 群伤,无类型走 blast 通道。
# 迭代 20 T1——封印爆裂〔迁入专长〕同面消费:行 triggers open_ticket channel blast)
for h in _hits_of(fh_hits, "open_ticket"):
if String(h.get("channel", "")) != "blast":
continue
var fbt := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_flag_burst", "封印爆裂", "board",
int(h.get("value", 0)), DamageController.TARGET_ALL_ALIVE, ri, cell, "", 0, "highlight", {})
_submit(fbt, false)
# 迭代 20 T1——标中新施加面:counter_add〔血税 标中+1 攻——bonus_delta atk 轴〕/
# ac_bonus〔封印桩 本轮 AC+N〕/ preview_area〔示警烟火 3×3 资源类型预告〕/
# remove_mines〔封印匠 每第 3 次该危险格中立移除——mod_eq 闸在行 when〕。
# (净化工兵 rewrite_board 分支随老 27 件退役摘除——效果类型同批除名,T1 落法 commit 留痕)
for h in fh_hits:
match String(h.get("kind", "")):
"counter_add":
var set_v = h.get("set", null)
_fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
"ac_bonus":
_apply_ac_bonus(h)
"bonus_delta":
var axis_f := String(h.get("axis", ""))
if _fx_bonus_delta.has(axis_f):
_fx_bonus_delta[axis_f] += int(h.get("delta", 0))
"remove_mines":
var removed: Array = minesweeper.remove_mines_in([cell])
if not removed.is_empty():
_emit_row("mine_neutralized", "fx_seal_smith", cell, ri, "封印匠·危险格中立移除", 0, {}, "")
"preview_area":
var board_p: BoardData = minesweeper.current_board()
var types: Array = []
for n in board_p.neighbors8(cell):
var n_snap = _reward_economy.query_full_snapshot(n)
if n_snap is Dictionary and not types.has(String((n_snap as Dictionary).get("cell_type", ""))):
types.append(String((n_snap as Dictionary).get("cell_type", "")))
if treasure_inventory != null and not types.is_empty():
treasure_inventory.apply_overlay("reward_preview", board_p.neighbors8(cell))
_emit_row("fx_preview_area", "", cell, ri, "示警烟火·周围资源类型", 0, {"types": types}, "")
# 雷区图谱(迭代 12 T3 标旗流——标中雷额外获得一份格面资源:插旗开始产出对标开格;
# 复用 grant 单点,额外兑付走同一 _grant_routed 正向路由)
for h in fh_hits:
if String(h.get("kind", "")) == "grant_material":
var extra_snap = _reward_economy.grant(cell)
if extra_snap is Dictionary:
_grant_routed(extra_snap, "positive", "flag_hit", cell, ri, "", String(h.get("label", "")))
var snap = _reward_economy.grant(cell)
if not (snap is Dictionary):
return
_grant_routed(snap, "positive", "flag_hit", cell, ri, "flag_hit")
else:
_perfect_hit_false += 1
if _flag_miss_seen.has(cell):
return # 每格实例一次去重(归因行与负向兑付同键——防逐轮惩罚轰炸)
_flag_miss_seen[cell] = true
# 完美保险(迭代 20 T4——本战下一次标错不破坏完美资格:_perfect_hit_false 回退=本次标错
# 对完美谓词无痕;只保一次/件,节点级用完即止)
if _fx_insurance > 0:
_fx_insurance -= 1
_perfect_hit_false -= 1
_emit_row("insurance_used", "", cell, ri, "完美保险·标错豁免", 0, {"left": _fx_insurance}, "")
var hl := ""
if _perfect_hit_false == 1:
hl = "perfect_broken" # 本板完美谓词已破的归因锚
_emit_row("flag_miss", "flag_miss", cell, ri, "标旗未中", 0, {}, hl)
# 危险直觉·守挂点(迭代 21 T3——flag_missed 发射:契约 12 触发主语=「当回合发生的标错
# 动作」——去重闸=新旧标错判别器〔后续轮重判旧旗被闸挡回零触发〕;双条件②「结算时旗仍在
# 场」由发射面既有过滤承载〔被挖开的旗已随挖掘擦除零兑付〕;grant_ap mode:queue 下一轮
# 开轮兑现——末轮判出无下一轮=合法作废自动成立〔编排器每战新建,队列不跨战〕;限次=合成行
# adventure 域计数器闸)
for h in _fx_emit("flag_missed", {"cell": cell, "round": ri}):
if String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue":
action_economy.queue_ap_delta(int(h.get("value", 0)))
var snap = _reward_economy.grant(cell)
if not (snap is Dictionary):
return
_grant_routed(snap, "negative", "flag_miss", cell, ri, "")
# 清图批:清图奖励对象恰好发一次;完美排雷追加第二份同身份兑付(grant_clear 本身恰一次)。
# 高光行 value = 实际有效兑付份数(禁记名义值;首份零兑付 → 降级不发)。
func _grant_clear_reward(ri: int) -> void:
@@ -1336,93 +1023,6 @@ func _roll_round_actions() -> void:
# 开单守卫归流程(用户裁决③——流程决定给谁开单):主角存活+目标存在才开先手单;hero hp=0 立即
# 停开(B 模式下若 flush 中主角倒下,由控制器死亡事件终止余单——两模式同一结果)。
func _combat_segment(ri: int) -> void:
_defer_board_damage = false # 案C 闸复位——战斗段起开单即入队(主角/敌方/补提交的地图单按开单序统一结账)
# 本轮扫雷事实推导(迭代 12 T4——执拗 streak/连珠矢段数共用取数面):本轮 safe 事件数=
# streak;踩雷即清零(mine_hit 监听同口——执拗「踩雷清零」承载)。攻击在战斗段=授予段后
# 同一次 _settle 内,facts 已定。
var round_safe := 0
var round_mine := false
for evt in _facts_reveals:
if evt.kind == "safe":
round_safe += 1
elif evt.kind == "mine":
round_mine = true
_damage.dig_streak = 0 if round_mine else round_safe
# 蓄力层注入(内容数据驱动扩展批 T3——整备军备「每层下轮首段 +1」/厚积薄发满层 spike/铁壁整备
# 减伤——三件 fact charge_layers 同读 action_economy;battle 域每轮刷新)
_damage.charge_layers = action_economy.charge_layers
# ⓪ 主角先手攻击(基础攻击恒存在——2026-09-11 用户裁决;先于敌方行动。案C 后授予段不再产出伤害,
# 主角进入战斗段时恒存活——先手必兑,除非场上无敌)。
# 单分量伤害构成(2026-09-19 充能退役批·用户拍板):主角攻击 = 唯一基础分量(atk,吃掷点含暴击;
# 类型 = 主角 attack_type 表值——2026-09-18 营地扩展前置批:量与类型抽象分离;查甲效矩阵对应行取克制修正)。
# 原"基础 + 穿刺/魔法充能三分量"模型随充能系统整体退役——克制策略归冒险前配置(营地换型 / 未来冒险前参数面),
# 局内不再有挖掘攒弹药的第二伤害供给。attack_up 增益经 hero_offense 池乘攻击合计(见池乘区注记)。
# 护甲等级掷点批(2026-09-16):基础分量吃掷点判定(含暴击);棋盘来源伤害不掷不折;
# 任何伤害落地 ≥ damage_floor(全局 max(1,·)——2026-09-20 用户拍板扩全来源,下限在控制器管线单点收口)。
var hero_alive: bool = int(_enemy_system.hero_unit().get("hp", 0)) > 0
var hero_target = _enemy_system.default_target()
if hero_alive and hero_target != null:
var ht := _damage.open_ticket(DamageController.KIND_HERO_ATTACK, "hero_attack", "主角攻击", "combat", _hero_atk, \
DamageController.TARGET_FRONT_ALIVE, ri, Vector2i(-1, -1), String(_enemy_system.hero_unit().get("attack_type", "normal")))
_damage.submit(ht)
# 连珠矢(内容数据驱动扩展批 T3——attack_commit 订阅施加面:开格节奏变攻击段数,本轮每开
# ~3 安全格 +1 段穿刺附加(上限 cap;段值 provisional 固定值不乘 atk——台账披露)。施加位
# 紧随主角攻击单开立=迁移前开单位(先于敌方行动单入队——flush 兑现序逐位一致)
for h in _hits_of(ht.context.get("fx_attack_hits", []) as Array, "attack_segments"):
var segments := mini(int(h.get("cap", 2)), _damage.dig_streak / maxi(1, int(h.get("divisor", 3))))
for s in segments:
# 迭代 20 T9——source_class 中性名(旧件名 feat_chain_arrows 随连珠矢退役;attack_segments 兑付通道保留泛用面)
var ct := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "fx_streak_segments",
"连击分段", "board", int(h.get("value", 0)), DamageController.TARGET_FRONT_ALIVE, ri,
minesweeper.hero_pos(), "pierce")
_damage.submit(ct)
# 敌方行动(敌人技能池批——本拍动作开轮已摇定,此处按技能行兑现;行序 = 生成序)。
# 死亡即不动手:摇定后、兑现前死亡的敌不在存活序中——本拍技能作废,不 commit、不进冷却。
for u in _enemy_system.query_alive("enemy"):
if int(_enemy_system.hero_unit().hp) <= 0:
break # hero hp=0 立即短路(死尸挨打行是归因噪音——用户拍板)
var skill_id := _enemy_system.pending_action(u)
var srow: Dictionary = db.skill_row(skill_id)
if srow.is_empty():
_fatal("敌实例待发动作悬空: %s" % skill_id)
continue
if String(srow.type) == "buff" or String(srow.type) == "debuff":
# 增益/妨害技(新效应词汇批 2026-09-19)= 0 伤害施放行:不掷不折;状态授予即数值兑现,技能冷却与增益持续两计时器分账。
# (原"本轮喂的伏击逢施放拍不兑现"读怪奖励随伏击退役一并作废——2026-09-20 八型批)
# 落点由状态行 target_side 路由(buff → 敌方侧 / debuff → 主角侧——容器零改动,proposal-status-vocabulary §1)。
var dur: int = int(srow.get("buff_duration", 1))
_status_effects.apply_effect(String(srow.buff_ref), {"source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "round_index": ri}, dur)
_emit_row("cast", "enemy_action", Vector2i(-1, -1), ri, "%s 施放 %s" % [u.display_name, String(srow.display_name)], 0, {"skill_id": skill_id, "effect_id": String(srow.buff_ref), "duration": dur})
_enemy_system.commit_action(u)
continue
# 伤害技(含普攻——倍率恒 1)= 敌方伤害技单据:基线 = round(atk×倍率)、rider 预计算归开单侧
# (表值算术零随机),掷点/克制查表(第 9 参 = 敌方 attack_type——迭代 24 N3 轴开列前恒空串不查表)/
# hero_defense×enemy_output 池乘区/下限/rider 后置/commit 全归控制器管线(兑现序 = 开单序;
# 来源敌已死在兑现时作废——死亡即不动手)。
var baseline: int = int(round(float(u.atk) * float(srow.get("damage_mult", 1.0))))
var rider := 0
if srow.has("rider_mult"):
rider = maxi(int(db.settlement.damage_floor), int(floor(float(u.atk) * float(srow.rider_mult))))
var et := _damage.open_ticket(DamageController.KIND_ENEMY_SKILL, "enemy_action", "%s %s" % [u.display_name, String(srow.display_name)], "combat", \
baseline, DamageController.TARGET_HERO, ri, Vector2i(-1, -1), String(u.get("attack_type", "normal")), rider)
et["source_unit"] = u
et["context"] = {"skill_id": skill_id, "baseline": baseline}
_damage.submit(et)
# 地图伤害兑付(案C 2026-10-06 用户裁——战斗段末:怪物还手后按授予段事件序补提交统一结账;
# 主角死于怪物 → 地图账不兑,逐单留「未生效」行不作废静默——没生效也要留痕口径;
# 补提交后若地图直伤反杀主角,控制器 _halted 作废余单——战斗已败,胜负归判定段)
if int(_enemy_system.hero_unit().get("hp", 0)) <= 0:
for t in _deferred_board:
_emit_row("reward_ineffective", String(t.get("source_class", "")), t.get("origin_cell", Vector2i(-1, -1)), ri,
"%s 未生效(主角已倒下)" % String(t.get("display", "")), 0, {"forfeit": "hero_down"}, "")
else:
for t in _deferred_board:
_damage.submit(t)
_deferred_board.clear()
_damage.flush() # 战斗段界结账(B 模式——主角单+敌方单+地图单按开单序统一兑现;A 模式地图单补提交时即结)
# ---- 算术读数(伤害管线单据化批 2026-09-24 迁移注记)----
# 掷点/折扣/克制点数/池乘区/攻击行文本已全部迁入 伤害控制器(damage_controller.gd 管线单点);
# 本类仅保留一处薄委托——_defender_ac(快照面消费)。_clamped_pool 委托壳已随 2026-10-02 修复批删除
+56 -15
View File
@@ -268,12 +268,27 @@ func _resolve(t: Dictionary) -> void:
# 主角先手攻击:掷点 → 折扣/暴击 → 克制点数 → hero_offense×enemy_defense 池乘区 → 下限 → 交付
# (内容数据驱动扩展批 T3——attack_commit/clamp_check/armor_roll 订阅施加面:命中=开单入口随单
# context 携带,本函数按迁移前算序逐族施加——命中序=数据表声明序,确定性)
# context 携带,按迁移前算序逐族施加——命中序=数据表声明序,确定性)。
# 迭代 27 T2 姊妹病灶拆段(P1-1):原 148 行单体拆四步——命中面(_hero_attack_ctx,目标/护甲/
# 掷点/按序改写族四拍/克制点数) → 基值装配(_hero_attack_base_value,加法族/计数器施加/倍乘) →
# 掷点兑现(_hero_attack_redeem,rolled/池乘/下限/rider/落地/击杀发射) → 交付(_hero_attack_deliver,
# 状态施加/标签/明细行)。步骤间经 ctx/redemption 字典穿线——算序/取值逐字保留(🔴 逐位一致契约)。
func _resolve_hero_attack(t: Dictionary) -> void:
var ctx := _hero_attack_ctx(t)
if ctx.is_empty():
return # 不可达(流程开单前已有存活守卫)——防御位不发行(outcome 已置 zero)
var base_value: float = _hero_attack_base_value(t, ctx)
var redemption := _hero_attack_redeem(t, ctx, base_value)
_hero_attack_deliver(t, ctx, redemption)
# 命中面(原 :273-328 逐字):目标/护甲克制 matchup/armor_roll 发射/按序改写族四拍(矿工重锤命中
# 交换→专注免掷→厚积薄发必中→猎弱折扣移档)/克制点数读值+订阅改写。返回穿线 ctx(空=目标不可达)。
func _hero_attack_ctx(t: Dictionary) -> Dictionary:
var target = _target_for(String(t.target_rule))
if target == null:
_outcomes[int(t.ticket_id)] = "zero" # 不可达(流程开单前已有存活守卫)——防御位不发行
return
return {}
var armor: String = String(target.get("armor_type", "light"))
var atk_type: String = String(t.attack_type)
var matchup := (atk_type + "_vs_" + armor) if atk_type != "" else ""
@@ -326,10 +341,20 @@ func _resolve_hero_attack(t: Dictionary) -> void:
# 淬毒刃纹(迭代 11 同调批 T5 → 内容数据驱动扩展批 T4:特判拆除——行 triggers clamp_check
# modify(mode:clamp_bonus) 订阅命中已并入上方 delta 合计;mode 路由=主角攻击面专享,
# 棋盘克制直伤窗只消费无模式 delta——棋盘不吃毒面与迁移前一致)
return {"target": target, "armor": armor, "atk_type": atk_type, "ac_hits": ac_hits,
"ac_payload": ac_payload, "roll_read": roll_read, "threshold": threshold,
"crit": crit, "discount": discount, "spike_ready": spike_ready, "cdelta": cdelta}
# 基值装配(原 :329-355 逐字):基值+单位标记加伤+矿工重锤增伤面+加法族(attack_bonus/
# value_counter 层值)+计数器施加(attack_commit 用后清零行——命中序=声明序)+spike 倍乘。
func _hero_attack_base_value(t: Dictionary, ctx: Dictionary) -> float:
var ac_hits: Array = ctx["ac_hits"]
var ac_payload: Dictionary = ctx["ac_payload"]
var base_value := float(t.base_value)
# 单位标记加伤(迭代 20 T1——处刑台 target_attack_bonus 挂单位字典 fx_atk_bonus 键;
# 阈值沿发射点已写入,本处通用消费——对该敌伤害加算)
base_value += float(int(target.get("fx_atk_bonus", 0)))
base_value += float(int((ctx["target"] as Dictionary).get("fx_atk_bonus", 0)))
# 按序改写族第五拍:矿工重锤增伤面(交换的正向侧)
for h in ac_hits:
if String(h.get("kind", "")) == "modify_ticket" and int(h.get("trade_atk", 0)) > 0:
@@ -349,17 +374,27 @@ func _resolve_hero_attack(t: Dictionary) -> void:
var set_v = h.get("set", null)
_fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
# 任意真族:厚积薄发满层 spike 倍乘(≥3 层——强制必中已在折扣拍承载)
if spike_ready:
if ctx["spike_ready"]:
for h in ac_hits:
if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "spike":
base_value *= float(int(h.get("value", 1)))
var rolled: int = DamageMath.rolled_component(base_value, discount, crit, int(db.settlement.crit_multiplier), cdelta)
return base_value
# 掷点兑现(原 :356-402 逐字):rolled 分量/payload 归因/池乘区/下限/英雄侧 rider 族/落地扣血/
# 击杀标记/kill 发射+贯穿弹射。返回 {landed, payload, hero_rider}(交付步消费)。
func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> Dictionary:
var target = ctx["target"]
var ac_hits: Array = ctx["ac_hits"]
var roll_read: Dictionary = ctx["roll_read"]
var atk_type: String = ctx["atk_type"]
var rolled: int = DamageMath.rolled_component(base_value, int(ctx["discount"]), bool(ctx["crit"]), int(db.settlement.crit_multiplier), int(ctx["cdelta"]))
var payload: Dictionary = {"target": String(target.display_name)}
payload[atk_type] = rolled # 分量键 = 实际攻击类型(归因字段,单分量模型)
payload["roll"] = int(roll_read.roll)
payload["ac"] = threshold
payload["tier"] = discount
if crit:
payload["ac"] = int(ctx["threshold"])
payload["tier"] = int(ctx["discount"])
if bool(ctx["crit"]):
payload["crit"] = true
var pooled: int = DamageMath.pool_damage(rolled, clamped_pool("hero_offense"), clamped_pool("enemy_defense"))
var landed: int = maxi(int(db.settlement.damage_floor), pooled)
@@ -370,12 +405,12 @@ func _resolve_hero_attack(t: Dictionary) -> void:
var hero_rider := rider_tier_bonus
for h in ac_hits:
if String(h.get("kind", "")) == "add_ticket_rider":
var contrib := int(h.get("value", 0)) * _per_value(String(h.get("per", "")), ac_payload)
var contrib := int(h.get("value", 0)) * _per_value(String(h.get("per", "")), ctx["ac_payload"])
if h.has("cap"):
contrib = mini(int(h.cap), contrib)
hero_rider += contrib
if hero_rider > 0:
if crit and rider_tier_crit:
if bool(ctx["crit"]) and rider_tier_crit:
hero_rider = int(round(float(hero_rider) * float(db.settlement.crit_multiplier)))
landed += hero_rider
payload["hero_rider"] = hero_rider
@@ -400,17 +435,23 @@ func _resolve_hero_attack(t: Dictionary) -> void:
_queue.append(of_t)
else:
_resolve(of_t)
return {"landed": landed, "payload": payload, "hero_rider": hero_rider}
# 交付(原 :403-414 逐字):即时执行族(碎甲震击护甲裂解)/标签装配/明细行发射/兑现登记。
func _hero_attack_deliver(t: Dictionary, ctx: Dictionary, redemption: Dictionary) -> void:
var payload: Dictionary = redemption["payload"]
# 即时执行族:碎甲震击打重甲附加「护甲裂解」(复用 off_guard——零新状态行;apply_effect 域闭/
# 重复授予由状态容器自行守卫)
for h in ac_hits:
for h in (ctx["ac_hits"] as Array):
if String(h.get("kind", "")) == "apply_status":
# 迭代 20 T9——source_class 中性名(旧件名 feat_armor_crack 随碎甲震击退役;apply_status 通道保留泛用面)
effects.apply_effect(String(h.get("status_id", "")), {"source_class": "fx_apply_status"}, -1)
payload["cracked"] = true
var label := _attack_label(String(t.display), crit, discount)
if hero_rider > 0:
label += "+奥术%d(必中)" % hero_rider
emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), label, landed, payload, String(t.highlight))
var label := _attack_label(String(t.display), bool(ctx["crit"]), int(ctx["discount"]))
if int(redemption["hero_rider"]) > 0:
label += "+奥术%d(必中)" % int(redemption["hero_rider"])
emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), label, int(redemption["landed"]), payload, String(t.highlight))
_outcomes[int(t.ticket_id)] = "redeemed"
+16
View File
@@ -0,0 +1,16 @@
class_name AuraSegment
extends RefCounted
# ② 增益/减益段 Segment(迭代 27 T2——P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。
# 职责:续存实例组合行(本结算新授予的已在授予段逐 grant 出行——增益状态 义务⑦)。
# 行发射归宿主编排器(oc)——零算法改动(🔴 零行为变更契约)。
var oc: BattleOrchestrator # 宿主(类型标注=段内 := 推断前提;循环类名引用 Godot 4 全局类安全) 宿主
func _init(host) -> void:
oc = host
func run(ri: int) -> void:
oc._emit_composition_effects("hero", ri)
oc._emit_composition_effects("enemy", ri)
+1
View File
@@ -0,0 +1 @@
uid://buwfoafhp2wvk
+101
View File
@@ -0,0 +1,101 @@
class_name CombatSegment
extends RefCounted
# ③ 战斗段 Segment(迭代 27 T2——P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。
# 职责:案C 闸复位 + 本轮扫雷事实推导(执拗 streak/连珠矢段数)+ 蓄力层注入 + 主角先手
# (含连珠矢分段开单)+ 敌方行动兑现 + 地图伤害账殿后(补提交统一结账)。
# 调用序:主角先手 → 逐敌还手 → 地图伤害账殿后〔案C 2026-10-06 用户裁〕;hero hp=0 短路。
# 行发射/开单/兑付原语仍归宿主编排器(oc)——本函数体为原编排器 _combat_segment 逐字搬运
# (🔴 零行为变更契约)。
var oc: BattleOrchestrator # 宿主(类型标注=段内 := 推断前提;循环类名引用 Godot 4 全局类安全) 宿主
func _init(host) -> void:
oc = host
func run(ri: int) -> void:
oc._defer_board_damage = false # 案C 闸复位——战斗段起开单即入队(主角/敌方/补提交的地图单按开单序统一结账)
# 本轮扫雷事实推导(迭代 12 T4——执拗 streak/连珠矢段数共用取数面):本轮 safe 事件数=
# streak;踩雷即清零(mine_hit 监听同口——执拗「踩雷清零」承载)。攻击在战斗段=授予段后
# 同一次 _settle 内,facts 已定。
var round_safe := 0
var round_mine := false
for evt in oc._facts_reveals:
if evt.kind == "safe":
round_safe += 1
elif evt.kind == "mine":
round_mine = true
oc._damage.dig_streak = 0 if round_mine else round_safe
# 蓄力层注入(内容数据驱动扩展批 T3——整备军备「每层下轮首段 +1」/厚积薄发满层 spike/铁壁整备
# 减伤——三件 fact charge_layers 同读 action_economy;battle 域每轮刷新)
oc._damage.charge_layers = oc.action_economy.charge_layers
# ⓪ 主角先手攻击(基础攻击恒存在——2026-09-11 用户裁决;先于敌方行动。案C 后授予段不再产出伤害,
# 主角进入战斗段时恒存活——先手必兑,除非场上无敌)。
# 单分量伤害构成(2026-09-19 充能退役批·用户拍板):主角攻击 = 唯一基础分量(atk,吃掷点含暴击;
# 类型 = 主角 attack_type 表值——2026-09-18 营地扩展前置批:量与类型抽象分离;查甲效矩阵对应行取克制修正)。
# 原"基础 + 穿刺/魔法充能三分量"模型随充能系统整体退役——克制策略归冒险前配置(营地换型 / 未来冒险前参数面),
# 局内不再有挖掘攒弹药的第二伤害供给。attack_up 增益经 hero_offense 池乘攻击合计(见池乘区注记)。
# 护甲等级掷点批(2026-09-16):基础分量吃掷点判定(含暴击);棋盘来源伤害不掷不折;
# 任何伤害落地 ≥ damage_floor(全局 max(1,·)——2026-09-20 用户拍板扩全来源,下限在控制器管线单点收口)。
var hero_alive: bool = int(oc._enemy_system.hero_unit().get("hp", 0)) > 0
var hero_target = oc._enemy_system.default_target()
if hero_alive and hero_target != null:
var ht := oc._damage.open_ticket(DamageController.KIND_HERO_ATTACK, "hero_attack", "主角攻击", "combat", oc._hero_atk, \
DamageController.TARGET_FRONT_ALIVE, ri, Vector2i(-1, -1), String(oc._enemy_system.hero_unit().get("attack_type", "normal")))
oc._damage.submit(ht)
# 连珠矢(内容数据驱动扩展批 T3——attack_commit 订阅施加面:开格节奏变攻击段数,本轮每开
# ~3 安全格 +1 段穿刺附加(上限 cap;段值 provisional 固定值不乘 atk——台账披露)。施加位
# 紧随主角攻击单开立=迁移前开单位(先于敌方行动单入队——flush 兑现序逐位一致)
for h in oc._hits_of(ht.context.get("fx_attack_hits", []) as Array, "attack_segments"):
var segments := mini(int(h.get("cap", 2)), oc._damage.dig_streak / maxi(1, int(h.get("divisor", 3))))
for s in segments:
# 迭代 20 T9——source_class 中性名(旧件名 feat_chain_arrows 随连珠矢退役;attack_segments 兑付通道保留泛用面)
var ct := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "fx_streak_segments",
"连击分段", "board", int(h.get("value", 0)), DamageController.TARGET_FRONT_ALIVE, ri,
oc.minesweeper.hero_pos(), "pierce")
oc._damage.submit(ct)
# 敌方行动(敌人技能池批——本拍动作开轮已摇定,此处按技能行兑现;行序 = 生成序)。
# 死亡即不动手:摇定后、兑现前死亡的敌不在存活序中——本拍技能作废,不 commit、不进冷却。
for u in oc._enemy_system.query_alive("enemy"):
if int(oc._enemy_system.hero_unit().hp) <= 0:
break # hero hp=0 立即短路(死尸挨打行是归因噪音——用户拍板)
var skill_id := oc._enemy_system.pending_action(u)
var srow: Dictionary = oc.db.skill_row(skill_id)
if srow.is_empty():
oc._fatal("敌实例待发动作悬空: %s" % skill_id)
continue
if String(srow.type) == "buff" or String(srow.type) == "debuff":
# 增益/妨害技(新效应词汇批 2026-09-19)= 0 伤害施放行:不掷不折;状态授予即数值兑现,技能冷却与增益持续两计时器分账。
# (原"本轮喂的伏击逢施放拍不兑现"读怪奖励随伏击退役一并作废——2026-09-20 八型批)
# 落点由状态行 target_side 路由(buff → 敌方侧 / debuff → 主角侧——容器零改动,proposal-status-vocabulary §1)。
var dur: int = int(srow.get("buff_duration", 1))
oc._status_effects.apply_effect(String(srow.buff_ref), {"source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "round_index": ri}, dur)
oc._emit_row("cast", "enemy_action", Vector2i(-1, -1), ri, "%s 施放 %s" % [u.display_name, String(srow.display_name)], 0, {"skill_id": skill_id, "effect_id": String(srow.buff_ref), "duration": dur})
oc._enemy_system.commit_action(u)
continue
# 伤害技(含普攻——倍率恒 1)= 敌方伤害技单据:基线 = round(atk×倍率)、rider 预计算归开单侧
# (表值算术零随机),掷点/克制查表(第 9 参 = 敌方 attack_type——迭代 24 N3 轴开列前恒空串不查表)/
# hero_defense×enemy_output 池乘区/下限/rider 后置/commit 全归控制器管线(兑现序 = 开单序;
# 来源敌已死在兑现时作废——死亡即不动手)。
var baseline: int = int(round(float(u.atk) * float(srow.get("damage_mult", 1.0))))
var rider := 0
if srow.has("rider_mult"):
rider = maxi(int(oc.db.settlement.damage_floor), int(floor(float(u.atk) * float(srow.rider_mult))))
var et := oc._damage.open_ticket(DamageController.KIND_ENEMY_SKILL, "enemy_action", "%s %s" % [u.display_name, String(srow.display_name)], "combat", \
baseline, DamageController.TARGET_HERO, ri, Vector2i(-1, -1), String(u.get("attack_type", "normal")), rider)
et["source_unit"] = u
et["context"] = {"skill_id": skill_id, "baseline": baseline}
oc._damage.submit(et)
# 地图伤害兑付(案C 2026-10-06 用户裁——战斗段末:怪物还手后按授予段事件序补提交统一结账;
# 主角死于怪物 → 地图账不兑,逐单留「未生效」行不作废静默——没生效也要留痕口径;
# 补提交后若地图直伤反杀主角,控制器 _halted 作废余单——战斗已败,胜负归判定段)
if int(oc._enemy_system.hero_unit().get("hp", 0)) <= 0:
for t in oc._deferred_board:
oc._emit_row("reward_ineffective", String(t.get("source_class", "")), t.get("origin_cell", Vector2i(-1, -1)), ri,
"%s 未生效(主角已倒下)" % String(t.get("display", "")), 0, {"forfeit": "hero_down"}, "")
else:
for t in oc._deferred_board:
oc._damage.submit(t)
oc._deferred_board.clear()
oc._damage.flush() # 战斗段界结账(B 模式——主角单+敌方单+地图单按开单序统一兑现;A 模式地图单补提交时即结)
@@ -0,0 +1 @@
uid://cmr602bpitrxd
+16
View File
@@ -0,0 +1,16 @@
class_name DecaySegment
extends RefCounted
# ④ 衰减段 Segment(迭代 27 T2——P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。
# 职责:状态衰减恰一次恒执行含末轮——先兑付后衰减。行发射归宿主编排器(oc)——
# 零算法改动(🔴 零行为变更契约)。
var oc: BattleOrchestrator # 宿主(类型标注=段内 := 推断前提;循环类名引用 Godot 4 全局类安全) 宿主
func _init(host) -> void:
oc = host
func run(ri: int) -> void:
var k: int = oc._status_effects.decay_round()
oc._emit_row("decay", "", Vector2i(-1, -1), ri, "状态衰减", k, {}, "")
@@ -0,0 +1 @@
uid://c4oht2fyigigk
+333
View File
@@ -0,0 +1,333 @@
class_name GrantSegment
extends RefCounted
# ① 授予段 Segment(迭代 27 T2——P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。
# 职责:结算队列(开格/旗判定/清图)→ 极性路由逐批兑付 + 旗判/军规开单 + round_end 施加 +
# 转存收口 + 揭示兑付 + 段界结账。行发射/开单/兑付原语仍归宿主编排器(oc)——
# 本段只搬运时序,零算法改动(🔴 零行为变更契约:同种子完整局输出逐位一致)。
# kind 分派单点化(P1-1 同批):counter_add 五位三行组 / grant_material 三位兑付块
# 收 _apply_counter_hit / _apply_material_hit 两助手(原 :683/:742/:777/:839/:1193 与
# :690/:782/:1217 八处——分支本体逐字保留在助手内)。
var oc: BattleOrchestrator # 宿主(类型标注=段内 := 推断前提;循环类名引用 Godot 4 全局类安全) 宿主(状态/行发射/开单单点仍归其身——段只搬运时序)
func _init(host) -> void:
oc = host
func run(ri: int) -> void:
# ① 授予段:结算队列(开格/旗判定/清图)→ 极性路由逐批兑付(伤害形态单据暂存——见 _submit 案C 闸)。
# 判定基线 = 轮末在场旗(发射面已过滤——被挖开的旗已随挖掘擦除零兑付,插旗不可逆批 2026-09-19;
# 开格事实不可撤)。
oc._defer_board_damage = true # 案C:授予段伤害单据只暂存不结账(2026-10-06 用户裁——兑付时点=战斗段末)
for evt in oc._facts_reveals:
if not bool(evt.get("resource", false)):
continue # 封锁格(initial 通道)恒无资源——不入账(战斗结算 义务②)
var snap = evt.get("reward_snapshot")
if not (snap is Dictionary):
continue
if evt.kind == "safe":
if bool(evt.get("dug_under_flag", false)):
# 插旗不可逆四情况④(2026-09-19 用户拍板):插安全格再挖开 = 零惩罚零奖励——正向兑付
# 作废(归因行在案);标空判定已随旗擦除自然缺席(被挖的旗不在轮末在场基线)。
oc._emit_row("reward_ineffective", "safe_cell", evt.cell, ri, "奖励作废(插旗格直接挖开——零惩罚零奖励)", 0, {}, "")
continue
# dig_reveal 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价:
# 乘区收集 → 正向兑付 → 计数器 → 追加产出(深挖)→ 奥术矢。
# 迭代 20 T1——payload 扩三事实:is_edge_cell〔边陲矿工〕/is_blank_zero〔跳格采掘〕/
# new_resource_type〔地质学家——快照 cell_type 首见判定,见后集登记〕)
var board_b: BoardData = oc.minesweeper.current_board()
var snap_pre = evt.get("reward_snapshot")
var ctype := String((snap_pre as Dictionary).get("cell_type", "")) if snap_pre is Dictionary else ""
var is_new_type := ctype != "" and not oc._fx_new_resource_types.has(ctype)
if is_new_type:
oc._fx_new_resource_types[ctype] = true
var dr_hits := oc._fx_emit("dig_reveal", {"cell": evt.cell, "round": ri,
"is_first_dig_of_battle": not oc._feat_first_dig_used,
"terrain_kind": oc._harmful_variants(evt.get("variant_ids", []) as Array),
"is_edge_cell": evt.cell.x == 0 or evt.cell.y == 0
or evt.cell.x == board_b.width - 1 or evt.cell.y == board_b.height - 1,
"is_blank_zero": board_b.digit_of(evt.cell) == 0,
"new_resource_type": is_new_type})
var pos_m := 0
var neg_m := 0
var combo_cap := -1
for h in dr_hits:
if String(h.get("kind", "")) != "yield_mult":
continue
pos_m += int(h.get("mult", 0))
neg_m += int(h.get("cost_mult", 0))
if int(h.get("cap", 0)) > 0:
combo_cap = int(h.cap) # 矿脉直觉连击上限
oc._grant_routed(oc._feat_yield_adjust(snap, true, pos_m, neg_m, combo_cap), "positive", "safe_cell", evt.cell, ri, "")
# 计数器执行(连击计数——加法族,乘区收集后执行=本格兑付用旧值,与迁移前逐位一致)
for h in dr_hits:
if String(h.get("kind", "")) == "counter_add":
_apply_counter_hit(h)
# 揭示效果入队(迭代 20 T1——地质学家/跳格采掘 reveal_tiles 订阅面;授予段末
# _flush_pending_reveals 统一掷格兑付)
if not oc._hits_of(dr_hits, "reveal_tiles").is_empty():
oc._fx_enqueue_reveal()
# 追加产出(深挖钻头 v2——本格双份制:grant_material 命中=配额内追加第二份;标签随行)
for h in dr_hits:
if String(h.get("kind", "")) == "grant_material":
_apply_material_hit(h, evt.cell, ri)
# 奥术矢(魔能共鸣——每战首次开安全格追加魔法矢;任意真族单次生效)
var spell_hits: Array = oc._hits_of(dr_hits, "spawn_spell")
if not spell_hits.is_empty() and not oc._feat_first_dig_used:
oc._feat_first_dig_used = true
var spell_t := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_first_dig_spell",
"奥术矢", "board", int((spell_hits[0] as Dictionary).get("value", 0)),
DamageController.TARGET_FRONT_ALIVE, ri, evt.cell, "magic")
oc._submit(spell_t, false)
elif evt.kind == "mine":
# 插旗不可逆四情况②:插雷格再挖开 = 只受挖雷一层惩罚(mine_hit 照常兑付——惊动巢穴),
# 标中不发(旗已随挖掘擦除、不入轮末判定基线)。
# mine_hit 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价:
# 减半收集 → 负向兑付〔带减半系数〕→ 反打开单 → 连击清零计数器。
# 迭代 20 T1——发射前移+payload 扩 dug_under_flag〔破封一击〕;新增施加面:
# damage_reduce〔安全帽耗层减伤——value_counter 层值,一次清零〕/ heal〔血蜜〕/
# blast_scaled〔陷阱爆发——雷伤×倍率全体敌〕/ bonus_delta〔血税〕/ reveal_tiles〔见血开窍〕)
var mine_power := int((snap as Dictionary).get("power", 0))
var mh_hits := oc._fx_emit("mine_hit", {"cell": evt.cell, "round": ri,
"damage": mine_power, "dug_under_flag": bool(evt.get("dug_under_flag", false)),
"hp_ratio": int(round(100.0 * float(int(oc._enemy_system.hero_unit().get("hp", 0))) / float(maxi(1, int(oc._enemy_system.hero_unit().get("max_hp", 1))))))})
var halve_pct := 0
var mine_reduce := 0
for h in mh_hits:
match String(h.get("kind", "")):
"damage_mult":
halve_pct += int(h.get("value", 0))
"damage_reduce":
if h.has("value_counter") and String(h.get("value_counter", "")) != "":
mine_reduce += oc._fx.counter(String(h.get("value_counter", ""))) # 安全帽——层值减免
else:
mine_reduce += int(h.get("value", 0)) # 背水——定值减免
"heal":
var hu_heal: Dictionary = oc._enemy_system.hero_unit()
hu_heal["hp"] = mini(int(hu_heal.get("hp", 0)) + int(h.get("value", 0)), maxi(1, int(hu_heal.get("max_hp", 1))))
"bonus_delta":
var axis := String(h.get("axis", ""))
if oc._fx_bonus_delta.has(axis):
oc._fx_bonus_delta[axis] += int(h.get("delta", 0))
"blast_scaled":
var blast_v := maxi(int(oc.db.settlement.damage_floor), int(float(mine_power) * float(h.get("mult", 1))))
var bs_t := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_blast_scaled", "陷阱爆发", "board",
blast_v, DamageController.TARGET_ALL_ALIVE, ri, evt.cell, "", 0, "", {})
oc._submit(bs_t, false)
if mine_reduce > 0:
snap["power"] = maxi(0, mine_power - mine_reduce) # 减伤落快照(负向兑付按减后值——安全帽每层减 1)
for h in mh_hits:
if String(h.get("kind", "")) == "counter_add":
_apply_counter_hit(h)
if not oc._hits_of(mh_hits, "reveal_tiles").is_empty():
oc._fx_enqueue_reveal() # 见血开窍——每颗雷一次揭示(授予段末统一兑付)
oc._grant_routed(oc._feat_yield_adjust(snap, false, 0, 0, -1), "negative", "mine_hit", evt.cell, ri, "", "", halve_pct)
for h in mh_hits:
if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "reflect":
var nct := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "attune_nest_counter", "惊巢反制·反弹", "board",
int(h.get("value", 0)), DamageController.TARGET_ALL_ALIVE, ri, evt.cell, "", 0, "", {})
oc._submit(nct, false)
# 地形变异 兑付段(授予段尾——触发枚举 on_open:挖开即伤、chain 通道同口径;伤害单据案C 暂存到
# 战斗段末兑付〔2026-10-06 用户裁〕;信息型 hide_number 不进结算——呈现层读取,见 board_view 数字遮蔽;
# 雷格恒无变异——布置段排除,双层叠罚不存在;插旗格直挖照吃变异伤害——地形后果同 mine_hit 口径,
# 非奖励不作废;吞噬流资源兑付〔荆棘编织/地脉补偿〕中伤害形态行同样经案C闸暂存,非伤害形态照常当场)
for evt in oc._facts_reveals:
var vids: Array = evt.get("variant_ids", [])
for vid in vids:
var vrow: Dictionary = oc.db.variation.row_by_id(String(vid))
if String(vrow.get("effect", "")) != "flat_damage":
continue # 触发词表按 effect 路由:flat_damage=on_open 兑现;hide_number/on_open 外组合 = 数据行新词表未接线(显式忽略面)
# 地形变异伤害单据(伤害管线单据化批 2026-09-24)——无类型必中,下限保底在控制器管线内
# 迭代 20 T1——damage_taken 发射前移(单据开立前):damage_reduce〔背水——hp_ratio when
# 面〕先收集再按减后值开单;counter_add〔保险丝/石肤〕+ ac_bonus〔石肤〕照常施加;
# grant_material〔荆棘/地脉〕移到单据提交后(与迁移前兑付值序一致——收益不因减伤变化)
var dt_amount := int(vrow.get("effect_value", 0))
var dt_hits := oc._fx_emit("damage_taken", {"kind": "terrain", "amount": dt_amount,
"source": String(vid), "cell": evt.cell, "round": ri, "terrain_kind": String(vid),
"hp_ratio": int(round(100.0 * float(int(oc._enemy_system.hero_unit().get("hp", 0))) / float(maxi(1, int(oc._enemy_system.hero_unit().get("max_hp", 1))))))})
for h in dt_hits:
match String(h.get("kind", "")):
"damage_reduce":
dt_amount = maxi(0, dt_amount - int(h.get("value", 0)))
"ac_bonus":
oc._apply_ac_bonus(h)
"counter_add":
_apply_counter_hit(h)
var vt := oc._damage.open_ticket(DamageController.KIND_BOARD_FLAT_HERO, "地形变异", String(vrow.get("display_name", vid)), "board", \
dt_amount, DamageController.TARGET_HERO, ri, evt.cell, "", 0, "", {"variant_id": String(vid)})
oc._submit(vt, false)
for h in dt_hits:
if String(h.get("kind", "")) == "grant_material":
_apply_material_hit(h, evt.cell, ri)
for evt in oc._facts_flags:
_judge_flag(evt, ri)
# 战旗军规开单段(迭代 12 T3 标旗流——flag_judged 批后统一开单:轮末每面标中旗对最前存活敌
# 结算 board_typed 必中伤害〔复用零新配方;「旗所在巢穴敌」简化为 default_target 最前存活敌——
# 台账披露待追认〕;完美布防=本轮旗批全中(hit>0 且 miss==0)时旗伤 ×mult——全对推理奖励)。
# 伤害单据案C 暂存战斗段末兑付。
# flag_settle 发射+施加(内容数据驱动扩展批 T3——战旗 open_ticket + 完美布防 modify_ticket
# 改写窗口:旗伤基值×完美倍率后逐旗开单,与迁移前算序一致)
var fs_hits := 0
var fs_misses := 0
for fevt in oc._facts_flags:
if bool(fevt.get("hit", false)):
fs_hits += 1
else:
fs_misses += 1
var fs_fx := oc._fx_emit("flag_settle", {"flags_hit_all": fs_hits > 0 and fs_misses == 0,
"flags_count": fs_hits, "round": ri})
var banner_hits: Array = []
for h in fs_fx:
if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "attack":
banner_hits.append(h)
if not banner_hits.is_empty():
var strike_value := int((banner_hits[0] as Dictionary).get("value", 0))
var flag_mult := 1
for h in fs_fx:
if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "flag_mult":
flag_mult *= maxi(1, int(h.get("value", 1)))
if fs_misses == 0 and fs_hits > 0 and flag_mult > 1:
strike_value *= flag_mult # 完美布防:同轮全中旗伤倍增(全对推理奖励)
for fevt in oc._facts_flags:
if not bool(fevt.get("hit", false)):
continue
var st_t := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_flag_strike",
"战旗军规", "board", strike_value, DamageController.TARGET_FRONT_ALIVE, ri,
fevt.cell, "normal")
oc._submit(st_t, false)
# round_end 发射+施加(内容数据驱动扩展批 T3——每轮结算收口:熔火之躯反打单开立〔open_ticket
# 即时执行〕+积层清零〔counter_add set〕;整备转蓄力由 finish_round_stop 承载——停手侧 AP 已在
# request_stop 记账,转存决策=grant_ap(mode:transfer) 订阅定夺,无订阅走 RAP 转存缺省)
var re_fx := oc._fx_emit("round_end", {"round": ri})
for h in re_fx:
if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "blast":
var mg_layers := oc._fx.counter(String(h.get("per", "")))
var mg_total := int(h.get("value", 0)) * mg_layers
if mg_total > 0:
# 迭代 20 T9——source_class 中性名(旧件名 feat_magma_body 随熔火之躯退役;per-counter 反打通道保留泛用面)
var mg_t := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "fx_blast_counter",
"积层反噬 ×%d" % mg_layers, "board", mg_total,
DamageController.TARGET_ALL_ALIVE, ri, oc.minesweeper.hero_pos(), "", 0, "", {})
oc._submit(mg_t, false)
for h in re_fx:
if String(h.get("kind", "")) == "counter_add":
_apply_counter_hit(h)
# 迭代 20 T1——round_end 新施加面:grant_gp〔爆破会计/收盘——escrow 战斗域缓冲,随 gp_dropped 上抛〕/
# grant_ap mode:queue〔封印回响——返 1 AP 下轮,queue_ap_delta 通道与轮始加成同面〕
for h in re_fx:
if String(h.get("kind", "")) == "grant_gp":
oc.escrow += int(h.get("value", 0))
oc._emit_row("effect", "fx_grant_gp", Vector2i(-1, -1), ri, "专长·GP 入账 +%d" % int(h.get("value", 0)), int(h.get("value", 0)), {"effect_id": "fx_grant_gp", "escrow": oc.escrow}, "")
elif String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue":
oc.action_economy.queue_ap_delta(int(h.get("value", 0)))
# 转存率入参(迭代 22 秘宝批 T3——储备罐 rap_transfer_mult magnitude=目标转存率百分比 125=1.25:1;
# 未装配=100 与旧 1:1 逐位一致)
var rap_pct: int = oc.attune_param_sum("rap_transfer_mult")
oc.action_economy.finish_round_stop(oc._fx != null and oc._fx.has_effect_on("round_end", "grant_ap", "transfer"),
rap_pct if rap_pct > 0 else 100)
if oc._facts_path == "cleared":
oc._grant_clear_reward(ri)
oc._flush_pending_reveals(ri) # 迭代 20 T1——揭示效果兑付(授予段末统一掷格,防轮 facts 迭代重入)
oc._damage.flush() # 授予段界结账(案C 后本段伤害单据全数暂存,此处对空队 no-op——保留作防御性段界)
# 旗判定(迭代 12 T3 标旗流本体随段迁入——原编排器 _judge_flag 逐字搬运,仅宿主引用加前缀;
# 唯一调用面=本段 run 的 facts_flags 循环)。
func _judge_flag(evt: Dictionary, ri: int) -> void:
var cell: Vector2i = evt.cell
if evt.hit:
if oc._flag_consumed.has(cell):
return # 去重跳过(资格以雷格实例为键——排雷玩法 诚实重复上报的对侧);零明细行
oc._flag_consumed[cell] = true
oc._perfect_hit_true += 1
# flag_hit 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价:殉爆〔T4 秘宝面〕→
# 净化改写 → 图谱额外资源 → 基线兑付)
var fh_hits := oc._fx_emit("flag_hit", {"cell": cell, "round": ri,
"terrain_kind": oc._harmful_adjacent(cell)})
# 标中殉爆(迭代 11 同调批 T5 → 内容数据驱动扩展批 T4:订阅施加——行 triggers open_ticket
# channel blast value_from magnitude;每标中一面旗全体存活敌吃 N 群伤,无类型走 blast 通道。
# 迭代 20 T1——封印爆裂〔迁入专长〕同面消费:行 triggers open_ticket channel blast)
for h in oc._hits_of(fh_hits, "open_ticket"):
if String(h.get("channel", "")) != "blast":
continue
var fbt := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_flag_burst", "封印爆裂", "board",
int(h.get("value", 0)), DamageController.TARGET_ALL_ALIVE, ri, cell, "", 0, "highlight", {})
oc._submit(fbt, false)
# 迭代 20 T1——标中新施加面:counter_add〔血税 标中+1 攻——bonus_delta atk 轴〕/
# ac_bonus〔封印桩 本轮 AC+N〕/ preview_area〔示警烟火 3×3 资源类型预告〕/
# remove_mines〔封印匠 每第 3 次该危险格中立移除——mod_eq 闸在行 when〕。
# (净化工兵 rewrite_board 分支随老 27 件退役摘除——效果类型同批除名,T1 落法 commit 留痕)
for h in fh_hits:
match String(h.get("kind", "")):
"counter_add":
_apply_counter_hit(h)
"ac_bonus":
oc._apply_ac_bonus(h)
"bonus_delta":
var axis_f := String(h.get("axis", ""))
if oc._fx_bonus_delta.has(axis_f):
oc._fx_bonus_delta[axis_f] += int(h.get("delta", 0))
"remove_mines":
var removed: Array = oc.minesweeper.remove_mines_in([cell])
if not removed.is_empty():
oc._emit_row("mine_neutralized", "fx_seal_smith", cell, ri, "封印匠·危险格中立移除", 0, {}, "")
"preview_area":
var board_p: BoardData = oc.minesweeper.current_board()
var types: Array = []
for n in board_p.neighbors8(cell):
var n_snap = oc._reward_economy.query_full_snapshot(n)
if n_snap is Dictionary and not types.has(String((n_snap as Dictionary).get("cell_type", ""))):
types.append(String((n_snap as Dictionary).get("cell_type", "")))
if oc.treasure_inventory != null and not types.is_empty():
oc.treasure_inventory.apply_overlay("reward_preview", board_p.neighbors8(cell))
oc._emit_row("fx_preview_area", "", cell, ri, "示警烟火·周围资源类型", 0, {"types": types}, "")
# 雷区图谱(迭代 12 T3 标旗流——标中雷额外获得一份格面资源:插旗开始产出对标开格;
# 复用 grant 单点,额外兑付走同一 _grant_routed 正向路由)
for h in fh_hits:
if String(h.get("kind", "")) == "grant_material":
_apply_material_hit(h, cell, ri, "flag_hit")
var snap = oc._reward_economy.grant(cell)
if not (snap is Dictionary):
return
oc._grant_routed(snap, "positive", "flag_hit", cell, ri, "flag_hit")
else:
oc._perfect_hit_false += 1
if oc._flag_miss_seen.has(cell):
return # 每格实例一次去重(归因行与负向兑付同键——防逐轮惩罚轰炸)
oc._flag_miss_seen[cell] = true
# 完美保险(迭代 20 T4——本战下一次标错不破坏完美资格:_perfect_hit_false 回退=本次标错
# 对完美谓词无痕;只保一次/件,节点级用完即止)
if oc._fx_insurance > 0:
oc._fx_insurance -= 1
oc._perfect_hit_false -= 1
oc._emit_row("insurance_used", "", cell, ri, "完美保险·标错豁免", 0, {"left": oc._fx_insurance}, "")
var hl := ""
if oc._perfect_hit_false == 1:
hl = "perfect_broken" # 本板完美谓词已破的归因锚
oc._emit_row("flag_miss", "flag_miss", cell, ri, "标旗未中", 0, {}, hl)
# 危险直觉·守挂点(迭代 21 T3——flag_missed 发射:契约 12 触发主语=「当回合发生的标错
# 动作」——去重闸=新旧标错判别器〔后续轮重判旧旗被闸挡回零触发〕;双条件②「结算时旗仍在
# 场」由发射面既有过滤承载〔被挖开的旗已随挖掘擦除零兑付〕;grant_ap mode:queue 下一轮
# 开轮兑现——末轮判出无下一轮=合法作废自动成立〔编排器每战新建,队列不跨战〕;限次=合成行
# adventure 域计数器闸)
for h in oc._fx_emit("flag_missed", {"cell": cell, "round": ri}):
if String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue":
oc.action_economy.queue_ap_delta(int(h.get("value", 0)))
var snap = oc._reward_economy.grant(cell)
if not (snap is Dictionary):
return
oc._grant_routed(snap, "negative", "flag_miss", cell, ri, "")
# counter_add 三行组单点(原五位分支本体逐字收拢——kind 分派随段归位 P1-1)
func _apply_counter_hit(h: Dictionary) -> void:
var set_v = h.get("set", null)
oc._fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
# grant_material 兑付块单点(原三位分支本体逐字收拢——source_class 两态 safe_cell/flag_hit 由参承载)
func _apply_material_hit(h: Dictionary, cell: Vector2i, ri: int, source_class: String = "safe_cell") -> void:
var g_snap = oc._reward_economy.grant(cell)
if g_snap is Dictionary:
oc._grant_routed(g_snap, "positive", source_class, cell, ri, "", String(h.get("label", "")))
@@ -0,0 +1 @@
uid://b0f3h2g13d04g
+33
View File
@@ -0,0 +1,33 @@
class_name VerdictSegment
extends RefCounted
# ⑤ 判定段 Segment(迭代 27 T2——P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。
# 职责:防御性优先级 hero 先判(双死结构不可达的纵深防御)→ 全量状态快照(despawn 前
# 组装——终局含 result)→ 终局行+收束分派 / 下一轮出口分发。
# 返回值 = 裁定("victory"/"defeat"/"continue")——宿主 _settle 据此保持旧早退语义
#(终局不回置 _settling,_battle_over 接管);本段零算法改动(🔴 零行为变更契约)。
var oc: BattleOrchestrator # 宿主(类型标注=段内 := 推断前提;循环类名引用 Godot 4 全局类安全) 宿主
func _init(host) -> void:
oc = host
func run(ri: int) -> String:
var verdict := "continue"
if int(oc._enemy_system.hero_unit().get("hp", 0)) <= 0:
verdict = "defeat"
elif oc._enemy_system.query_alive("enemy").is_empty():
verdict = "victory"
oc._emit_settlement_snapshot(verdict) # 全量状态快照(despawn 前组装——终局含 result)
if verdict == "defeat":
oc._emit_row("verdict", "", Vector2i(-1, -1), ri, "战斗失败", 0, {}, "")
oc._battle_over("defeat")
return verdict
if verdict == "victory":
oc._emit_row("verdict", "", Vector2i(-1, -1), ri, "战斗胜利", 1, {}, "")
oc._battle_over("victory")
return verdict
oc._emit_row("verdict", "", Vector2i(-1, -1), ri, "下一轮次", 0, {}, "")
oc._exit_distribution()
return verdict
@@ -0,0 +1 @@
uid://i0dhvekibaam
+6 -6
View File
@@ -5329,7 +5329,7 @@ func _smoke_tree() -> void:
# ---- 天赋树六键行为冒烟(迭代 21 T3——V10:开局姿态窗/危险直觉限次/财富之道聚散/守侧失误补偿;
# 引擎直构 + 编排授予段直注(_judge_flag=结算授予段旗判定单口))----
# 引擎直构 + 编排授予段直注(_grant_seg._judge_flag=结算授予段旗判定单口——迭代27 T2 随段迁址))----
func _smoke_tree_rules() -> void:
print("--- 天赋树六键行为(V10:窗/限次/聚散/守侧)---")
# 开局姿态组:每关首轮窗(battle_index 事实——引擎直构)
@@ -5362,7 +5362,7 @@ func _smoke_tree_rules() -> void:
fz2.counters_seed(exported)
check((fz2.dispatch("mine_hit", {}, Callable()) as Array).is_empty(),
"锋:限次跨战持久(adventure 域 counters_export/seed——断点保留同源,不新增存档域不升 v7)")
# 守侧五场景(契约 12):编排 rig——begin_battle 携 sense_guard,授予段 _judge_flag 直注
# 守侧五场景(契约 12):编排 rig——begin_battle 携 sense_guard,授予段 _grant_seg._judge_flag 直注
var orch = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777001))
check(orch.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40,
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {},
@@ -5379,16 +5379,16 @@ func _smoke_tree_rules() -> void:
check(safe.x >= 0, "守侧 rig:安全格定位(非雷格=标错负样本)")
check(int(orch.action_economy.pending_ap_delta()) == 0, "守侧 rig:首轮后 queue 干净(开局 posture queue 已兑现)")
# 场景①:当回合新插旗、结算判标错且旗仍在场 → queue +1(本轮 AP 零变化=queue 承载、下轮 notify_round_start 兑现)
orch._judge_flag({"cell": safe, "hit": false}, 1)
orch._grant_seg._judge_flag({"cell": safe, "hit": false}, 1)
check(int(orch.action_economy.pending_ap_delta()) == 1,
"守①:当回合标错且旗在场 → 下轮 AP+1(queue 排队——本轮 AP 逐位零变化)")
# 场景③④:之前回合标错重判(旗在场状态)与同格重复——去重闸挡回,最多返还一次
orch._judge_flag({"cell": safe, "hit": false}, 2)
orch._judge_flag({"cell": safe, "hit": false}, 3)
orch._grant_seg._judge_flag({"cell": safe, "hit": false}, 2)
orch._grant_seg._judge_flag({"cell": safe, "hit": false}, 3)
check(int(orch.action_economy.pending_ap_delta()) == 1,
"守③④:旧旗重判零触发+同格最多一次(_flag_miss_seen 去重闸=「当回合标错动作」判别器)")
# 场景②负样本:标中旗(hit=true 正向兑付路径)零触发
orch._judge_flag({"cell": safe, "hit": true}, 4)
orch._grant_seg._judge_flag({"cell": safe, "hit": true}, 4)
check(int(orch.action_economy.pending_ap_delta()) == 1, "守②负样本:标中/被挖旗零触发(标错事件只在 miss 分支发射——发射面过滤承载)")
# 场景⑤:末轮判出→无下一轮=queue 不兑现(编排器每战新建——队列实例随战亡,合法作废态)
var orch2 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777002))
+1
View File
@@ -52,6 +52,7 @@ func _suites() -> Array:
{"path": "res://tests/unit/core/tree_requires_level_test.gd", "expect": 0}, # R1 机制位批 2026-10-13——requires_level 等级阈值前置(schema 四拒一过+判定三态+投影两态)
{"path": "res://tests/unit/core/pool_case_d_test.gd", "expect": 0}, # 迭代 20 T2——池结构案D 行为(V6 令牌三态/V7 曝光槽 2+1 回退/V8 概率上限解耦提前退役)
{"path": "res://tests/unit/core/feat_vouchers_test.gd", "expect": 0}, # 迭代 20 T4——凭证五件+留任二件(V9 路由逐件/F-1 存量账/V10 留任路由+AP 属性轴)
{"path": "res://tests/unit/core/settle_segments_test.gd", "expect": 0}, # 迭代 27 T2——结算五段 Segment(V4:直接 new/编排器接线/真实一轮走段序)
]
@@ -0,0 +1,55 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 结算五段 Segment 单测(迭代 27 T2——需求卡片 V4 验收条目:Segment 类直接 new() + 编排器段序
# 接线 + 真实一轮结算走段序出裁定/行)。段体逐位一致性由 parity_anchor 三变体对拍承载(V1 核心),
# 本测兜段类可独立实例化与编排器装配面。
# 运行:godot --headless --path game --script res://tests/unit/core/settle_segments_test.gd(expect 0)
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
func _init() -> void:
var db = load("res://core/config_db.gd").new()
if not db.load_tables("res://data/tables", false):
print("[settle_segments_test] FAIL — 数值表加载失败")
quit(1)
return
# V4-1 段类直接 new()(构造收宿主引用——五类同构)
var oc = load("res://core/battle_orchestrator.gd").new(db, load("res://core/rng_service.gd").new())
Kit.check_quiet(load("res://core/settlement/grant_segment.gd").new(oc) != null, "V4 GrantSegment 直接 new()〔迭代27 T2〕")
Kit.check_quiet(load("res://core/settlement/aura_segment.gd").new(oc) != null, "V4 AuraSegment 直接 new()〔迭代27 T2〕")
Kit.check_quiet(load("res://core/settlement/combat_segment.gd").new(oc) != null, "V4 CombatSegment 直接 new()〔迭代27 T2〕")
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〕")
Kit.check_quiet(oc._grant_seg._apply_counter_hit({"counter_id": "dig_combo", "value": 1}) == null
and oc._fx.counter("dig_combo") == 1, "V4 counter_add 助手单点(原五位三行组收拢后行为等价)〔迭代27 T2〕")
# V4-3 真实一轮结算走段序(begin_battle→停手→_maybe_settle→五段→裁定行)
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(4242)
var settlement = load("res://core/battle_orchestrator.gd").new(db, rng)
var rows: Array = []
settlement.step_row.connect(func(row): rows.append(row))
var result := {"r": ""}
settlement.battle_finished.connect(func(payload): result.r = payload.get("result", ""))
var enc := {"level_index": 1, "node_depth": 1, "hero_hp": int(db.hero.max_hp)}
Kit.check_quiet(settlement.begin_battle(enc), "V4 前置:begin_battle 受理")
# 停手触发轮末结算(stop 经批次提交——_maybe_settle 在 WAITING 相位放行 _settle 段序)
var outcomes: Array = settlement.minesweeper.settle_batch([{"action": "stop"}])
Kit.check_quiet(not outcomes.is_empty(), "V4 前置:停手受理")
Kit.check_quiet(rows.any(func(row): return String(row.get("type", "")) == "verdict"),
"V4 段序结算出判定行(stop→WAITING→五段→verdict)〔迭代27 T2〕")
Kit.check_quiet(rows.any(func(row): return String(row.get("type", "")) == "decay"),
"V4 衰减段行在流(段序④恒执行)〔迭代27 T2〕")
Kit.check_quiet(String(result.r) == "" or String(result.r) == "victory" or String(result.r) == "defeat",
"V4 裁定面合法(continue 静默/终局经 battle_finished)〔迭代27 T2〕")
quit(Kit.finish("settle_segments"))
@@ -0,0 +1 @@
uid://blthtn45ikah6