# VERTICAL SLICE - NOT FOR PRODUCTION # Date: 2026-09-24 # 伤害控制器:伤害单据唯一兑付管线——伤害管线单据化批(2026-09-24 用户三裁决,06 台账 148)。 # 背景:伤害此前无对象抽象——五族来源(主角攻击/敌方行动/棋盘克制直伤/棋盘群伤/反噬地形)在 # 战斗编排器内各自内联组装管线;类型轴(attack_type)与算术层(DamageMath)早已收口,缺的是 # 「一次伤害」这个对象与单点管线。本类补上:五调用点退化为开单点(流程决定何时开单、给谁开单), # 本类决定一张单经过哪几步(掷点 → 折扣 → 暴击 → 克制点数 → 池乘区 → 落地下限 → 交付)。 # 用户三裁决落位: # ① 兑现时机双模式(结账时机开关):POLICY_IMMEDIATE(A——开单即结,逐位等价 2026-09-24 前现状 # 行为,回切位)/ POLICY_SEGMENT(B——开单攒批、段界统一结账;流程在授予段尾与战斗段尾调 flush)。 # 切换=改 policy 一个值,管线/单据/数据表零改动。 # ② 目标 = 结账时刻照 target_rule 看现场解析(front_alive_enemy = 行主序最前存活敌 / hero / # all_alive_enemies)——开单时只写规则不冻结对象。 # ③ 死亡事件由单位自行宣布(UnitContainer.apply_damage 归零 → unit_died 信号):怪死 = 队列中 # 该敌为来源的未结单据作废(死亡即不动手——静默零行零 commit,同现行"不在存活序"口径); # 主角死 = _halted 终止(flush 余单作废;Game Over 仍归战斗编排器判定段既有 hp 读数路径)。 # 单据栏目(九栏):kind(兑付配方,唯一分派键)/ source_class+origin_cell+round_index(行协议归因三件)/ # display(行文本基名)/ channel(战斗段|棋盘兑付——声明栏,管线步序由 kind 承载)/ # attack_type(normal|pierce|magic|""=无类型——""恒不查克制矩阵)/ base_value(威力或攻击基线)/ # target_rule / rider(必中附加,后置于下限不进乘区)/ context(kind 专用上下文)。 # 可选:requires(前置单据 id——前置单 redeemed 才兑付,否则静默作废;清图完美第二份用)。 # 行协议归属战斗编排器(emit_row 以 Callable 注入——9 类行协议与来源断言不外泄本类)。 # RNG 纪律:掷点只在结账时刻消费 armor_roll 封闭流——A 模式提交即结=消费时点与迁移前逐位一致, # B 模式消费次序=开单次序(同种子同操作仍逐位复现;结果数值与 A 模式可不同——兑现时机语义差异)。 # 实现 GDD:design/gdd/systems/战斗循环/战斗结算.md(registry 追溯锚) class_name DamageController extends RefCounted const POLICY_IMMEDIATE := "immediate" # A:开单即结(现状行为——回切位) const POLICY_SEGMENT := "segment" # B:攒批到段界 flush 统一结(2026-09-24 用户拍板目标形态) # 兑付配方(kind → 步序): # hero_attack 战斗段·主角先手:掷点折扣暴击 → 克制点数(attack_type vs 敌护甲类型)→ # hero_offense×enemy_defense 池乘区 → 下限 → 交付;行载荷带 roll/ac/tier/killed。 # enemy_skill 战斗段·敌方伤害技(含普攻):基线 = round(atk×倍率)(开单侧算好入 base_value)→ # 掷点折扣暴击 → 克制恒中性(敌打主角无甲轴 delta 0)→ hero_defense×enemy_output # 池乘区 → 下限 → +rider(必中附加)→ 交付 → commit_action;来源敌已死 = 作废。 # board_typed 棋盘·单目标克制直伤(穿刺弹/魔法矢正向):必中全额,克制点数进加法核,下限保底。 # board_blast 棋盘·群伤(爆裂粉尘/清障炸药):无类型恒定表值,全体存活敌逐个交付。 # board_flat_hero 棋盘·主角直伤(反噬/治疗包负向/地形变异):无类型必中,下限保底。 const KIND_HERO_ATTACK := "hero_attack" const KIND_ENEMY_SKILL := "enemy_skill" const KIND_BOARD_TYPED := "board_typed" const KIND_BOARD_BLAST := "board_blast" const KIND_BOARD_FLAT_HERO := "board_flat_hero" const TARGET_FRONT_ALIVE := "front_alive_enemy" const TARGET_HERO := "hero" const TARGET_ALL_ALIVE := "all_alive_enemies" var db: ConfigDb var rng: RngServiceCore # 注入成员类型注(2026-10-02 修复批——单测经真实 RngService 构造,标注安全) var units: UnitContainer # UnitContainer——目标解析/伤害交付/存活查询(句柄即 Dictionary 引用) var effects: StatusContainer # StatusContainer——池值/AC 型效果读取(增益状态 只读消费面) var emit_row_cb: Callable # 行协议注入口:func(type, source_class, origin_cell, ri, display, value, extra, highlight) var policy := POLICY_SEGMENT # 结账时机开关(2026-09-24 用户拍板第二步拨 B=攒批段界统一结;回切 A 改回 POLICY_IMMEDIATE 即还原迁移前行为) var _queue: Array[Dictionary] = [] # 未结单据(B 模式攒批;A 模式恒空) var _outcomes := {} # ticket_id → "redeemed"/"zero"/"voided"(结账后可查——清图高光份数消费) var _seq := 0 # 流水号 var _halted := false # 主角倒下终止旗(死亡事件置位——flush 余单作废) var _hero_ac_override := -1 # 主角护甲等级合流入参(begin_battle 显式设定——-1 = 表值兜底,口径同战斗编排器) # 流派件求值旗(迭代 12 T2——begin_battle 编排器经 RelicsEngine 求值后注入;空字典=零扰动。 # rider_flat=英雄侧必中附加值〔奥术余烬族〕/rider_crit=档二 rider 可暴击/first_dig_spell=每战首次 # 开安全格追加魔法矢〔消费在编排器授予段〕/clamp_magic_light·clamp_magic_heavy=克制点直调) var relic_flags: Dictionary = {} func _init(db_ref, rng_ref, units_ref, effects_ref) -> void: db = db_ref rng = rng_ref units = units_ref effects = effects_ref units.unit_died.connect(_on_unit_died) # 主角护甲等级合流入参(战斗编排器 begin_battle 调用——encounter.hero_armor_class;缺参/-1 = 表值兜底) func set_hero_ac_override(v: int) -> void: _hero_ac_override = v # ---- 开单面(流程调用——编排器六调用点的唯一形态)---- # 单据构造(缺省填齐——调用点只写差异栏;context = kind 专用:enemy_skill 带 skill_id/baseline, # board_flat_hero 的键并入行载荷,board_blast/board_typed 带 batch_key 供未生效行归因) func open_ticket(kind: String, source_class: String, display: String, channel: String, base_value: int, target_rule: String, ri: int, origin_cell: Vector2i = Vector2i(-1, -1), attack_type: String = "", rider: int = 0, highlight: String = "", context: Dictionary = {}) -> Dictionary: return { "kind": kind, "source_class": source_class, "origin_cell": origin_cell, "round_index": ri, "display": display, "channel": channel, "attack_type": attack_type, "base_value": base_value, "target_rule": target_rule, "rider": rider, "highlight": highlight, "context": context, "requires": -1, # 前置单据 id(-1 = 无依赖) "source_unit": {}, # enemy_skill 专用——来源敌句柄(作废判定 + commit) } # 开单:A 模式当场结账;B 模式入队待 flush。返回流水号(outcome() 查询键)。 func submit(t: Dictionary) -> int: _seq += 1 t["ticket_id"] = _seq _outcomes[_seq] = "pending" if policy == POLICY_IMMEDIATE: _resolve(t) else: _queue.append(t) return _seq # 同步开单(清图批专用——完美第二份与高光份数的流程依赖需要同步读数,开单权归流程 用户裁决③): # 先按开单序结清段内排队的单据(保序),再无视结账时机政策当场兑现本单。 func flush_submit(t: Dictionary) -> int: flush() _seq += 1 t["ticket_id"] = _seq _outcomes[_seq] = "pending" _resolve(t) return _seq # 结账:B 模式按开单序逐张兑现队列(兑现中经 requires 追加的回声单按入队序自然续结); # A 模式恒空队 no-op(段界调用点两模式共用)。主角倒下(_halted)后余单全部静默作废。 func flush() -> void: if policy != POLICY_SEGMENT: return var i := 0 while i < _queue.size(): if _halted: break _resolve(_queue[i]) i += 1 for j in range(i, _queue.size()): _outcomes[int(_queue[j].ticket_id)] = "voided" _queue.clear() # 结账读数:"redeemed"=实际有效兑付 / "zero"=合法零兑付(未生效行已自记)/ "voided"=作废(静默)/ # "pending"=未结(B 模式排队中) func outcome(ticket_id: int) -> String: return String(_outcomes.get(ticket_id, "pending")) # ---- 死亡事件(用户裁决③——单位自行宣布,本类消费)---- func _on_unit_died(u: Dictionary) -> void: if bool(u.get("is_hero", false)): _halted = true # 主角倒下——flush 余单作废;Game Over 归判定段既有 hp 读数路径 return for t in _queue: if t.source_unit == u: t["cancelled"] = true # 死亡即不动手——该敌未结单据作废(静默零行零 commit) # ---- 兑付管线(单点——所有伤害经过同一道门)---- func _resolve(t: Dictionary) -> void: var id := int(t.ticket_id) if _halted or bool(t.get("cancelled", false)) \ or (int(t.get("requires", -1)) >= 0 and String(_outcomes.get(int(t.requires), "")) != "redeemed"): _outcomes[id] = "voided" # 终止后余单 / 来源敌已死 / 前置单未实际有效——静默作废(死亡即不动手口径) return match String(t.kind): KIND_HERO_ATTACK: _resolve_hero_attack(t) KIND_ENEMY_SKILL: _resolve_enemy_skill(t) KIND_BOARD_TYPED: _resolve_board_typed(t) KIND_BOARD_BLAST: _resolve_board_blast(t) KIND_BOARD_FLAT_HERO: _resolve_board_flat_hero(t) _: push_error("[伤害控制器/fatal] 未知兑付配方: %s" % String(t.kind)) _outcomes[id] = "zero" return # 主角倒下检查(每张单兑现后——敌方连拍致死即中止,同现行"hp=0 立即短路"口径) if int(units.hero_unit().get("hp", 0)) <= 0: _halted = true # 主角先手攻击:掷点 → 折扣/暴击 → 克制点数 → hero_offense×enemy_defense 池乘区 → 下限 → 交付 func _resolve_hero_attack(t: Dictionary) -> void: var target = _target_for(String(t.target_rule)) if target == null: _outcomes[int(t.ticket_id)] = "zero" # 不可达(流程开单前已有存活守卫)——防御位不发行 return var armor: String = String(target.get("armor_type", "light")) var atk_type: String = String(t.attack_type) var roll_read: Dictionary = _armor_roll() var threshold: int = defender_ac(target) + int(db.settlement.attack_bonus) # 矿工重锤·命中交换(迭代 12 T4 普攻流——正负双修原生形态:命中判定减成 trade_hit 点 # 〔更易命中〕换攻击增伤 trade_atk%——高 AC 负期望/低 AC 正期望) var trade_hit := int(relic_flags.get("hit_trade_hit", 0)) if trade_hit > 0: threshold = maxi(1, threshold - trade_hit) var crit: bool = bool(roll_read.crit) var discount: int = _roll_discount(int(roll_read.roll), threshold) # 专注(迭代 12 T4 普攻流——免掷骰恒定折扣:方差买断成期望——稳定器人格;roll 记 0 归因) var rewrite := int(relic_flags.get("armor_roll_rewrite", 0)) if rewrite > 0: discount = rewrite crit = false roll_read["roll"] = 0 # 厚积薄发满层必中(迭代 12 T5 蓄力流——spike ready 时 discount 恒全额、暴击面走倍乘不加掷) if int(relic_flags.get("spike_mult", 0)) > 0 and int(relic_flags.get("charge_layers", 0)) >= 3: discount = 100 # 猎弱本能(迭代 12 T4 穿刺流——对轻甲敌折扣档改善 value 档:对冲 pierce 列 L1 −1 开局罚; # 改善 = 折扣表向全额方向移档,nat 最大面/已全额不重复) var shift := int(relic_flags.get("discount_shift", 0)) if shift > 0 and armor == "light" and discount < 100: discount = _discount_shift_up(discount, shift) var cdelta: int = _counter_delta(atk_type, armor) + _relic_clamp_bonus(atk_type, armor) # 淬毒刃纹(迭代 11 同调批 T5——底稿 §3:对被克制敌人的穿刺/魔法直伤 +N;条件 = 克制成立 × # 攻击类型限定,主角攻击面专属——棋盘克制直伤走 _resolve_board_typed 不吃本加成) if attune_venom > 0 and (atk_type == "pierce" or atk_type == "magic") and cdelta > 0: cdelta += attune_venom var base_value := float(t.base_value) var trade_atk := int(relic_flags.get("hit_trade_atk", 0)) if trade_atk > 0: base_value *= 1.0 + float(trade_atk) / 100.0 # 重锤增伤面(交换的正向侧) # 蓄力流(迭代 12 T5):整备军备「每层下轮首段 +1」(层数×value 加算)+厚积薄发满层(≥3) # spike 倍乘+强制必中(discount 100/crit 关闭掷骰面——方差买断的爆发出口) var charge_layers := int(relic_flags.get("charge_layers", 0)) if charge_layers > 0: base_value += float(charge_layers * int(relic_flags.get("ap_carry_value", 0))) var spike_mult := int(relic_flags.get("spike_mult", 0)) var spike_ready := spike_mult > 0 and charge_layers >= 3 if spike_ready: base_value *= float(spike_mult) var rolled: int = DamageMath.rolled_component(base_value, discount, crit, int(db.settlement.crit_multiplier), 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["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) # 英雄侧 rider(迭代 12 T2 魔法流——enemy_skill rider 对称位补齐):后置必中附加,独立于掷点 # 与乘区(奥术余烬「普攻命中后恒定结算」语义);档二(magic 4 件)rider 可暴击=当次 crit 时 # rider 值乘暴击倍率。rider 不改变下限语义(landed 含 rider 后不再过 floor)。 var hero_rider := int(relic_flags.get("rider_flat", 0)) # 猎巢人(迭代 12 T3 标旗流——巢穴控制数转伤害光环):控制数 = 敌方 weaken(削弱)实例数 # 〔八型批 side-wide 状态行——标中旗控制巢穴的既有承载;原设计「对其中敌伤害」简化为全体 # 先手攻击附加值——台账披露待追认〕,每控制 1 巢 rider +Z。 var nest_aura := int(relic_flags.get("nest_aura_value", 0)) if nest_aura > 0: for eff in effects.query_active("enemy"): if String(eff.effect_id) == "weaken": hero_rider += nest_aura # 矿工的执拗(迭代 12 T4 普攻流——「排雷排得多好,打得就有多疼」):本轮安全开格 streak # 〔编排器授予段实时写入 relic_flags.dig_streak;踩雷清零〕×value,上限 cap。 var rdig := int(relic_flags.get("round_dig_value", 0)) if rdig > 0: hero_rider += mini(int(relic_flags.get("round_dig_cap", 5)), int(relic_flags.get("dig_streak", 0)) * rdig) if hero_rider > 0: if crit and bool(relic_flags.get("rider_crit", false)): hero_rider = int(round(float(hero_rider) * float(db.settlement.crit_multiplier))) landed += hero_rider payload["hero_rider"] = hero_rider units.apply_damage(target, landed) payload["killed"] = not target.alive # 击杀标记在扣血后取(原语义不变) # 碎甲震击(迭代 12 T4 普攻流——命中重甲附加「护甲裂解」):复用 off_guard(enemy percent # +25 受伤加深——零新状态行);apply_effect 域闭/重复授予由状态容器自行守卫。 var crack := int(relic_flags.get("crack_value", 0)) if crack > 0 and armor == "heavy": effects.apply_effect("off_guard", {"source_class": "relic_armor_crack"}, -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)) # 贯穿射击(迭代 12 T4 穿刺流——击杀溢出弹射):击杀时对下一存活敌以穿刺类型弹射 # 本体半额〔原设计「溢出伤害」追踪在单分量模型不可行——半额弹射 provisional 裁定披露待追认; # 无次敌=弹射落空〕。B 模式入队段尾 flush 兑现;A 模式递归 resolve(目标存活守卫内建)。 if bool(payload.get("killed", false)) and int(relic_flags.get("kill_overflow", 0)) > 0: var next_target = units.default_target() if next_target != null: var of_t := open_ticket(DamageController.KIND_BOARD_TYPED, "relic_pierce_shot", "贯穿射击", "board", maxi(int(db.settlement.damage_floor), int(landed / 2)), DamageController.TARGET_FRONT_ALIVE, int(t.round_index), t.origin_cell, "pierce") if policy == POLICY_SEGMENT: _queue.append(of_t) else: _resolve(of_t) _outcomes[int(t.ticket_id)] = "redeemed" # 敌方伤害技:掷点 → 折扣/暴击 → 克制恒中性 delta 0(敌打主角无甲轴)→ hero_defense×enemy_output # 池乘区 → 下限 → +rider(后置必中附加)→ 交付 → commit_action(作废单不 commit——死亡即不动手) func _resolve_enemy_skill(t: Dictionary) -> void: var hero: Dictionary = units.hero_unit() var roll_read: Dictionary = _armor_roll() var threshold: int = defender_ac(hero) + int(db.settlement.attack_bonus) var crit: bool = bool(roll_read.crit) var discount: int = _roll_discount(int(roll_read.roll), threshold) var rolled: int = DamageMath.rolled_component(float(t.base_value), discount, crit, int(db.settlement.crit_multiplier), 0) var pooled: int = DamageMath.pool_damage(rolled, clamped_pool("hero_defense"), clamped_pool("enemy_output")) var total: int = maxi(int(db.settlement.damage_floor), pooled) + int(t.rider) # rider 后置于伤害下限——独立必中加项,不进乘区 # 铁壁整备(迭代 12 T5 蓄力流——每层下轮开场 S 点减伤;减后过下限保底) var shield := int(relic_flags.get("shield_per_layer", 0)) * int(relic_flags.get("charge_layers", 0)) if shield > 0: total = maxi(int(db.settlement.damage_floor), total - shield) units.apply_damage(hero, total) var payload: Dictionary = {"target": "hero", "skill_id": String(t.context.get("skill_id", "")), "baseline": int(t.context.get("baseline", 0)), "roll": int(roll_read.roll), "ac": threshold, "tier": discount} if crit: payload["crit"] = true if int(t.rider) > 0: payload["rider"] = int(t.rider) var label := _attack_label(String(t.display), crit, discount) if int(t.rider) > 0: label += "+附加%d(必中)" % int(t.rider) emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), label, total, payload, String(t.highlight)) units.commit_action(t.source_unit) _outcomes[int(t.ticket_id)] = "redeemed" # 棋盘克制直伤:必中全额不掷点;目标 = 结账时刻最前存活敌;克制点数进加法核;下限保底 func _resolve_board_typed(t: Dictionary) -> void: var target = units.default_target() if target == null: _ineffective_row(t, "无存活敌人") _outcomes[int(t.ticket_id)] = "zero" return var delta: int = _counter_delta(String(t.attack_type), String(target.get("armor_type", "light"))) + _relic_clamp_bonus(String(t.attack_type), String(target.get("armor_type", "light"))) var dealt: int = DamageMath.counter_component(int(t.base_value), delta, int(db.settlement.damage_floor)) units.apply_damage(target, dealt) emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), "%s → %s" % [String(t.display), String(target.display_name)], dealt, {"target": String(target.display_name), "attack_type": String(t.attack_type), "effectiveness": delta}, String(t.highlight)) _outcomes[int(t.ticket_id)] = "redeemed" # 棋盘群伤:无类型恒定表值,结账时刻全体存活敌逐个交付(单行汇总——行内 count = 兑现时存活数) func _resolve_board_blast(t: Dictionary) -> void: var alive: Array = units.query_alive("enemy") if alive.is_empty(): _ineffective_row(t, "无存活敌人") _outcomes[int(t.ticket_id)] = "zero" return var blast_dmg: int = maxi(int(db.settlement.damage_floor), int(t.base_value)) # 伤害下限全来源(2026-09-20) for u in alive: units.apply_damage(u, blast_dmg) # 恒定表值(下限保底),逐个;受击即死——死亡即不动手 emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), "%s → 全体敌人 ×%d" % [String(t.display), alive.size()], blast_dmg, {"target": "all_enemies", "count": alive.size()}, String(t.highlight)) _outcomes[int(t.ticket_id)] = "redeemed" # 棋盘主角直伤(反噬/治疗包负向/地形变异):无类型必中,下限保底;context 键并入行载荷 func _resolve_board_flat_hero(t: Dictionary) -> void: var dealt: int = maxi(int(db.settlement.damage_floor), int(t.base_value)) # 伤害下限全来源(2026-09-20) units.apply_damage(units.hero_unit(), dealt) var extra: Dictionary = {"target": "hero"} for k in t.context.keys(): extra[k] = t.context[k] emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), "%s → 主角" % String(t.display), dealt, extra, String(t.highlight)) _outcomes[int(t.ticket_id)] = "redeemed" # ---- 管线算术读数(自战斗编排器迁入——管线单点;编排器留 _clamped_pool/_defender_ac 薄委托)---- # 护甲等级掷点(armor_roll 封闭流——逐次掷逐次取号、逐位可回放):返回 {roll, crit} func _armor_roll() -> Dictionary: var stream: RandomNumberGenerator = rng.open_stream(String(db.settlement.armor_roll_stream)) var roll: int = stream.randi_range(1, int(db.settlement.roll_faces)) return {"roll": roll, "crit": roll == int(db.settlement.crit_roll)} # 掷点折扣读数:nat 最大面恒全额(暴击分支前置),其余查档(缺口 = 阈值 − 掷点) func _roll_discount(roll: int, threshold: int) -> int: if roll == int(db.settlement.crit_roll): return 100 return DamageMath.roll_discount_percent(roll, threshold, db.settlement.roll_tier_bounds, db.settlement.roll_tier_discounts) # 防守方护甲等级读数(数值轴——与 armor_type 克制属性轴严格分离):敌人 = spawn 期行值快照; # 主角 = begin_battle 显式入参(encounter.hero_armor_class 合流值,缺参/-1 = 表值兜底); # ac 型状态效果叠其上(ac_up 层数×幅度——不入任何池,无 clamp) func defender_ac(unit: Dictionary) -> int: if bool(unit.get("is_hero", false)): var total: int = _hero_ac_override if total < 0: total = int(db.hero.armor_class) for eff in effects.query_active("hero"): var row: Dictionary = db.status_effect_row(eff.effect_id) if not row.is_empty() and String(row.unit) == "ac": total += int(eff.effective_value) return total var total_e: int = int(unit.get("armor_class", 0)) for eff_e in effects.query_active("enemy"): var erow_ac: Dictionary = db.status_effect_row(eff_e.effect_id) if not erow_ac.is_empty() and String(erow_ac.unit) == "ac": total_e += int(eff_e.effective_value) return total_e # 克制修正点数读值(SettlementTable.armor_effectiveness:{攻击类型: {护甲: 点数}}——缺项回退 0 中性; # attack_type "" = 无类型恒不查(群伤/反噬/地形/敌方技)) func _counter_delta(attack_type: String, armor: String) -> int: if attack_type == "": return 0 var tbl: Dictionary = db.settlement.armor_effectiveness var row: Dictionary = tbl.get(attack_type, {}) return int(row.get(armor, 0)) # 克制点直调(迭代 12 T2 魔法流/T4 穿刺流 clamp_modifier 件——克制矩阵查表值后的流派件调整; # 空旗=0 零扰动):魔网织者=magic vs magic(矩阵罚 −1 → 0/正);湛蓝誓约=magic vs heavy(+1 → +2); # 猎手刺剑=pierce vs light(+1——对轻甲敌穿刺提升,对重甲无增益=正负双修)。hero_attack 与 # board_typed 两消费面同享。 func _relic_clamp_bonus(attack_type: String, armor: String) -> int: var bonus := 0 if attack_type == "magic": if armor == "magic": bonus += int(relic_flags.get("clamp_magic_magic", 0)) elif armor == "heavy": bonus += int(relic_flags.get("clamp_magic_heavy", 0)) elif attack_type == "pierce": if armor == "light": bonus += int(relic_flags.get("clamp_pierce_light", 0)) return bonus # 折扣档改善(迭代 12 T4 猎弱本能——折扣表向全额方向移 value 档;查不到当前档 = 不动防御口径) func _discount_shift_up(discount: int, shift: int) -> int: var tbl: Array = db.settlement.roll_tier_discounts var idx := tbl.find(discount) if idx < 0: return discount return int(tbl[mini(tbl.size() - 1, idx + shift)]) # 淬毒刃纹 N(迭代 11 同调批 T5——战斗结算 begin_battle 注入;0 = 未装配零扰动) var attune_venom: int = 0 # 池内加算 → clamp [-100,+100](池间乘算归 DamageMath.pool_damage 调用侧) func clamped_pool(pool: String) -> int: var total := 0 for eff in effects.query_active("hero") + effects.query_active("enemy"): var row: Dictionary = db.status_effect_row(eff.effect_id) if row.is_empty(): continue if String(db.settlement.effect_pool_map.get(eff.effect_id, "")) == pool and String(row.unit) == "percent": total += eff.effective_value # 池内加算(L×m 原样——越域不 clamp 由本处收口) return clampi(total, int(db.settlement.pool_clamp_lo), int(db.settlement.pool_clamp_hi)) # 攻击行显示名(掷点档位随行文本可读;重档折扣 = "被压制") func _attack_label(base: String, crit: bool, discount: int) -> String: if crit: return base + "(暴击)" if discount < 100: return base + "(被压制)" return base # ---- 内部 ---- # 目标解析(裁决②——结账时刻照规则看现场;句柄即 Dictionary 引用) func _target_for(rule: String): match rule: TARGET_FRONT_ALIVE: return units.default_target() TARGET_HERO: return units.hero_unit() TARGET_ALL_ALIVE: return units.query_alive("enemy") push_error("[伤害控制器/fatal] 未知目标规则: %s" % rule) return null func _ineffective_row(t: Dictionary, reason: String) -> void: emit_row_cb.call("reward_ineffective", String(t.source_class), t.origin_cell, int(t.round_index), "%s 未生效(%s)" % [String(t.display), reason], 0, {"batch_key": String(t.context.get("batch_key", ""))}, String(t.highlight))