## VERTICAL SLICE - NOT FOR PRODUCTION ## 伤害控制器:全部伤害的唯一兑付管线——调用点只开单,掷点/折扣/暴击/克制/池乘区/下限/交付全在本类(垂直切片验证:每笔伤害可归因)。 ## 结账时机 policy 双模式:immediate=开单即结 / segment=攒批段界 flush——切换只改 policy 值,管线与表零改动。 ## 掷点只在结账时刻消费 armor_roll 封闭流——segment 模式消费次序=开单次序,同种子同操作逐位复现。 ## 目标在结账时刻按 target_rule 现场解析,开单不冻结对象;死亡由单位自行宣布(unit_died)触发作废/终止。 class_name DamageController extends RefCounted const POLICY_IMMEDIATE := "immediate" # A:开单即结(现状行为——回切位) const POLICY_SEGMENT := "segment" # B:攒批到段界 flush 统一结(目标形态) # 兑付配方(kind → 步序): # hero_attack 战斗段·主角先手:掷点折扣暴击 → 克制点数(attack_type vs 敌护甲类型)→ # hero_offense×enemy_defense 池乘区 → 下限 → 交付;行载荷带 roll/ac/tier/killed。 # enemy_skill 战斗段·敌方伤害技(含普攻):基线 = round(atk×倍率)(开单侧算好入 base_value)→ # 掷点折扣暴击 → 克制点数(attack_type vs 主角护甲类型—— # 无类型恒中性 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 # 注入成员类型注(单测经真实 RngService 构造,标注安全) var units: UnitContainer # UnitContainer——目标解析/伤害交付/存活查询(句柄即 Dictionary 引用) var effects: StatusContainer # StatusContainer——池值/AC 型效果读取(增益状态 只读消费面) var emit_row_cb: Callable # 行协议注入口:func(head, ri, display, value, extra)——head 归因束键集 type/source_class/origin_cell/highlight(缺省"") var policy := POLICY_SEGMENT # 结账时机开关(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 = 表值兜底,口径同战斗编排器) # 效果端口件(组装期单点注入——host _init bind_port):发射/计数器/事实/揭示/AC 全经端口路由, # 缺省 null=未绑(与原 _fx==null 零扰动守卫同语义——单测直构不接端口的夹具零扰动照跑) var _port: EffectPort = null 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 # 效果端口件注入(host _init 组装期单点——先于 begin_battle 任何发射) func bind_port(port: EffectPort) -> void: _port = port # 事件发射薄面(发射+命中收集统一入口——订阅空=零循环零扰动;命中由各施加面按组合规则施加; # 路由经效果端口件,事实回调=biz_fact 闭面五键宿主侧) func _fx_hits(event: String, payload: Dictionary) -> Array: if _port == null: return [] return _port.emit(event, payload) # 未绑事实回落(缺省 null——int()/bool() 包裹后与原 inject 缺省值逐位同:int(null)=0/bool(null)=false) func _fact_or(name: String) -> Variant: return null if _port == null else _port.fact(name) # per 取数(per 字段解析——载荷同名字段优先,回落端口事实面;add_ticket_rider/attack_bonus/ # shield/open_ticket 的倍数承载:贡献值 = value × per 读数,cap 对乘积封顶) func _per_value(name: String, payload: Dictionary) -> int: if name == "": return 1 if payload.has(name): return int(payload[name]) if _fact_or(name) != null: return int(_fact_or(name)) return 0 # 巢穴控制数(fact nest_count 取数面——敌方 weaken 实例数,猎巢人订阅面) func _nest_count() -> int: var n := 0 for eff in effects.query_active("enemy"): if String(eff.effect_id) == "weaken": n += 1 return n # ---- 开单面(流程调用——编排器六调用点的唯一形态)---- # 开单: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 # 单据构造(缺省填齐——调用点只写差异栏;context = kind 专用:enemy_skill 带 skill_id/baseline, # board_flat_hero 的键并入行载荷,board_blast/board_typed 带 batch_key 供未生效行归因)。 # attack_commit 发射点(我方伤害单据开立入口; # 命中挂单据 context 随行——施加面在 _resolve_hero_attack 管线算术前逐族施加,开单时点=主角攻击 # 单恒为段内首单(结账现场与开单现场一致)。载荷 atk/target/matchup + 挖掘 streak 事实〔连珠矢/ # 执拗订阅面〕+巢穴控制数〔猎巢人〕) # spec 束必载键(缺载 assert——fail-fast):channel/base_value/target_rule/ri; # 可选键缺省=原形缺省:origin_cell=(-1,-1)/attack_type=""/rider=0/highlight=""/context={}。 # 返回单据字典九栏目+requires/source_unit 逐字不变(契约 8)。 func open_ticket(kind: String, source_class: String, display: String, spec: Dictionary = {}) -> Dictionary: for req in ["channel", "base_value", "target_rule", "ri"]: assert(spec.has(req), "open_ticket spec 束缺载必载键: %s" % req) var context := spec.get("context", {}) as Dictionary var t := { "kind": kind, "source_class": source_class, "origin_cell": spec.get("origin_cell", Vector2i(-1, -1)), "round_index": int(spec.get("ri", 0)), "display": display, "channel": String(spec.get("channel", "")), "attack_type": String(spec.get("attack_type", "")), "base_value": int(spec.get("base_value", 0)), "target_rule": String(spec.get("target_rule", "")), "rider": int(spec.get("rider", 0)), "highlight": String(spec.get("highlight", "")), "context": context, "requires": -1, # 前置单据 id(-1 = 无依赖) "source_unit": {}, # enemy_skill 专用——来源敌句柄(作废判定 + commit) } if kind == KIND_HERO_ATTACK: var tgt = _target_for(String(t.target_rule)) var armor_b: String = String(tgt.get("armor_type", "light")) if tgt != null else "light" var matchup_o: String = (String(t.attack_type) + "_vs_" + armor_b) if String(t.attack_type) != "" else "" var ac_payload := {"atk": int(t.base_value), "target": String(tgt.get("display_name", "")) if tgt != null else "", "target_id": String(tgt.get("enemy_id", "")) if tgt != null else "", "matchup": matchup_o, "dig_streak": int(_fact_or("dig_streak") or 0), "nest_count": _nest_count()} t.context["fx_attack_hits"] = _fx_hits("attack_commit", ac_payload) t.context["fx_attack_payload"] = ac_payload return t # 同步开单(清图批专用——完美第二份与高光份数的流程依赖需要同步读数,开单权归流程): # 先按开单序结清段内排队的单据(保序),再无视结账时机政策当场兑现本单。 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 is_same(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 池乘区 → 下限 → 交付 # (attack_commit/clamp_check/armor_roll 订阅施加面:命中=开单入口随单 # context 携带,按固定算序逐族施加——命中序=数据表声明序,确定性)。 # 四步分段:命中面(_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) # 命中面(目标/护甲克制 matchup/armor_roll 发射→按序改写族四拍→克制点数读值+订阅改写; # 后两步各由 _ctx_rewrites/_ctx_clamp_delta 承载)。返回穿线 ctx(空=目标不可达)。 # clamp_check 只在 _ctx_rewrites 内发射一次——_ctx_clamp_delta 消费同批命中(禁重复发射)。 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 {} 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 "" var ac_payload: Dictionary[Variant, Variant] = (t.context.get("fx_attack_payload", {}) as Dictionary) var rewrites := _ctx_rewrites(t, target, armor, matchup) var cdelta := _ctx_clamp_delta(atk_type, armor, rewrites["clamp_hits"]) return {"target": target, "armor": armor, "atk_type": atk_type, "ac_hits": (t.context.get("fx_attack_hits", []) as Array), "ac_payload": ac_payload, "roll_read": rewrites["roll_read"], "threshold": rewrites["threshold"], "crit": rewrites["crit"], "discount": rewrites["discount"], "spike_ready": rewrites["spike_ready"], "cdelta": cdelta} # 按序改写族四拍+掷点快照(armor_roll 发射点在此):第一拍矿工重锤命中交换(正负双修原生形态: # 命中判定减成 trade_hit 点〔更易命中〕换攻击增伤 trade_atk%——高 AC 负期望/低 AC 正期望; # 改写位先于掷点折扣读数)→ 第二拍专注免掷骰恒定折扣(方差买断成期望——稳定器人格;roll 记 0 归因) # → 第三拍厚积薄发满层必中(spike ready 时 discount 恒全额、暴击面走倍乘不加掷;when charge_layers≥3 # 在 dispatch 判定——满层持续状态口径)→ 第四拍猎弱本能对轻甲敌折扣档改善 value 档 # (对冲 pierce 列 L1 −1 开局罚;改善 = 折扣表向全额方向移档,nat 最大面/已全额不重复)。 # clamp_check 同在此发射(唯一发射点——命中随返回值穿给 _ctx_clamp_delta)。 func _ctx_rewrites(t: Dictionary, target: Dictionary, armor: String, matchup: String) -> Dictionary: var ac_hits: Array[Variant] = (t.context.get("fx_attack_hits", []) as Array) var roll_read: Dictionary[Variant, Variant] = _armor_roll() var threshold: int = defender_ac(target) + int(db.combat.settlement.damage_roll.attack_bonus) # armor_roll 发射点(敌护甲掷骰判定;免掷恒折扣订阅面) var ar_hits := _fx_hits("armor_roll", {"target_ac": threshold, "roll": int(roll_read.roll)}) var trade_hit := 0 for h in ac_hits: if String(h.get("kind", "")) == "modify_ticket" and int(h.get("trade_hit", 0)) > 0: trade_hit += int(h.trade_hit) 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) for h in ar_hits: if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "armor_rewrite": discount = int(h.get("value", 0)) crit = false roll_read["roll"] = 0 var spike_ready := false for h in ac_hits: if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "spike": spike_ready = true discount = 100 var clamp_hits := _fx_hits("clamp_check", {"matchup": matchup}) var shift := 0 for h in clamp_hits: if String(h.get("kind", "")) != "modify_ticket": continue if String(h.get("mode", "")) == "discount_shift": shift += int(h.get("value", 0)) if shift > 0 and armor == "light" and discount < 100: discount = _discount_shift_up(discount, shift) return {"roll_read": roll_read, "threshold": threshold, "crit": crit, "discount": discount, "spike_ready": spike_ready, "clamp_hits": clamp_hits} # 克制点数读值+订阅改写(魔网织者/湛蓝誓约/猎手刺剑 clamp_check modify delta——克制矩阵查表值 # 后的直调;hero_attack 与 board_typed 两消费面同享)。命中批由 _ctx_rewrites 单次发射后传入。 # 淬毒刃纹(行 triggers clamp_check # modify(mode:clamp_bonus) 订阅命中并入本处 delta 合计;mode 路由=主角攻击面专享, # 棋盘克制直伤窗只消费无模式 delta——棋盘不吃毒面) func _ctx_clamp_delta(atk_type: String, armor: String, clamp_hits: Array) -> int: var cdelta: int = _counter_delta(atk_type, armor) for h in clamp_hits: if String(h.get("kind", "")) == "modify_ticket" and h.has("delta"): cdelta += int(h.delta) return cdelta # 基值装配(基值+单位标记加伤+矿工重锤增伤面+加法族(attack_bonus/ # value_counter 层值)+计数器施加(attack_commit 用后清零行——命中序=声明序)+spike 倍乘。 func _hero_attack_base_value(t: Dictionary, ctx: Dictionary) -> float: var ac_hits: Array[Variant] = ctx["ac_hits"] var ac_payload: Dictionary[Variant, Variant] = ctx["ac_payload"] var base_value := float(t.base_value) # 单位标记加伤(处刑台 target_attack_bonus 挂单位字典 fx_atk_bonus 键; # 阈值沿发射点已写入,本处通用消费——对该敌伤害加算) 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: base_value *= 1.0 + float(int(h.trade_atk)) / 100.0 # 加法族:整备军备「每层下轮首段 +1」(层数×value 加算)——attack_bonus per charge_layers。 # value_counter 扩展〔疼痛记忆/弃图兑换/陷阱本能——rider 值=计数器账面, # 值读于施加时点=命中序先行 rider 后清零行〕 for h in ac_hits: if String(h.get("kind", "")) == "attack_bonus": var contrib := int(h.get("value", 0)) * _per_value(String(h.get("per", "")), ac_payload) if h.has("value_counter") and String(h.get("value_counter", "")) != "": contrib = _port.counter_read(String(h.get("value_counter", ""))) base_value += float(contrib) # 计数器施加(attack_commit 命中行内 counter_add〔用后清零行〕,命中序=声明序;端口路由账面) for h in ac_hits: if String(h.get("kind", "")) == "counter_add": var set_v = h.get("set", null) _port.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 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))) return base_value # 掷点兑现(rolled 分量/payload 归因/池乘区/下限/英雄侧 rider 族/落地扣血/击杀标记; # 击杀发射面拆 _kill_overflow)。返回 {landed, payload, hero_rider}(交付步消费)。 func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> Dictionary: var target = ctx["target"] var ac_hits: Array[Variant] = ctx["ac_hits"] var roll_read: Dictionary[Variant, Variant] = 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.combat.settlement.damage_roll.crit_multiplier), int(ctx["cdelta"])) var payload: Dictionary[Variant, Variant] = {"target": String(target.display_name), "target_id": String(target.get("enemy_id", ""))} payload[atk_type] = rolled # 分量键 = 实际攻击类型(归因字段,单分量模型) payload["roll"] = int(roll_read.roll) 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.combat.settlement.damage_roll.damage_floor), pooled) # 加法族:英雄侧 rider(enemy_skill rider 对称位补齐口径)——后置必中附加,独立于掷点与乘区 # (奥术余烬「普攻命中后恒定结算」语义);猎巢人巢控光环(per nest_count)/矿工的执拗开格加成 # (per dig_streak、cap 封顶)同通道合计;档一质变 rider+1 / 档二(crit 时)rider 乘暴击倍率。 # rider 不改变下限语义(landed 含 rider 后不再过 floor)。 var hero_rider := int(_fact_or("rider_tier_bonus") or 0) # or 0=未绑缺省与 int(null) 缺省同值(int() 构造拒 null——端口缺省 null 面的空值合并) 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", "")), ctx["ac_payload"]) if h.has("cap"): contrib = mini(int(h.cap), contrib) hero_rider += contrib if hero_rider > 0: if bool(ctx["crit"]) and bool(_fact_or("rider_tier_crit") or false): hero_rider = int(round(float(hero_rider) * float(db.combat.settlement.damage_roll.crit_multiplier))) landed += hero_rider payload["hero_rider"] = hero_rider units.apply_damage(target, landed) payload["killed"] = not target.alive # 击杀标记在扣血后取 if bool(payload.get("killed", false)): _kill_overflow(t, target, landed) return {"landed": landed, "payload": payload, "hero_rider": hero_rider} # kill 发射+施加(敌人死亡确认;贯穿射击溢出弹射订阅面:对下一存活敌 # 以穿刺类型弹射本体 value%〔无次敌=弹射落空〕。B 模式入队段尾 flush 兑现;A 模式递归 resolve) func _kill_overflow(t: Dictionary, target: Dictionary, landed: int) -> void: for h in _fx_hits("kill", {"target": String(target.display_name), "target_id": String(target.get("enemy_id", "")), "overflow": landed}): if String(h.get("kind", "")) == "reveal_tiles": _port.reveal_effect() # 斩首学者——击杀揭示 1 格(端口路由宿主入队,授予段末兑付) continue if String(h.get("kind", "")) != "overflow": continue var next_target = units.default_target() if next_target != null: # source_class 用中性名(fx_overflow 泛用通道,不挂件名) var of_t := open_ticket(DamageController.KIND_BOARD_TYPED, "fx_overflow", "贯穿弹射", {"channel": "board", "base_value": maxi(int(db.combat.settlement.damage_roll.damage_floor), int(landed * int(h.get("value", 50)) / 100)), "target_rule": DamageController.TARGET_FRONT_ALIVE, "ri": int(t.round_index), "origin_cell": t.origin_cell, "attack_type": "pierce"}) if policy == POLICY_SEGMENT: _queue.append(of_t) else: _resolve(of_t) # 交付:即时执行族(碎甲震击打重甲附加「护甲裂解」——复用 off_guard,零新状态行; # apply_effect 域闭/重复授予由状态容器自行守卫)/标签装配/明细行发射/兑现登记。 func _hero_attack_deliver(t: Dictionary, ctx: Dictionary, redemption: Dictionary) -> void: var payload: Dictionary[Variant, Variant] = redemption["payload"] for h in (ctx["ac_hits"] as Array): if String(h.get("kind", "")) == "apply_status": # source_class 用中性名(fx_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), bool(ctx["crit"]), int(ctx["discount"])) if int(redemption["hero_rider"]) > 0: label += "+奥术%d(必中)" % int(redemption["hero_rider"]) emit_row_cb.call({"type": "damage", "source_class": String(t.source_class), "origin_cell": t.origin_cell, "highlight": String(t.highlight)}, int(t.round_index), label, int(redemption["landed"]), payload) _outcomes[int(t.ticket_id)] = "redeemed" # 敌方伤害技:掷点 → 折扣/暴击 → 克制点数(attack_type vs 主角护甲类型—— # 无类型恒中性 delta 0,写法同 hero_attack 的 _counter_delta 消费位)→ hero_defense×enemy_output # 池乘区 → 下限 → +rider(后置必中附加)→ 交付 → commit_action(作废单不 commit——死亡即不动手)。 # 掷点算术归 _enemy_roll;damage_taken 统一发射+消费归 _emit_hero_damage_taken(发射在本函数—— # 与减伤落快照的先后序固定;「掷点后、交付前」消费时点不变)。 func _resolve_enemy_skill(t: Dictionary) -> void: var hero: Dictionary[Variant, Variant] = units.hero_unit() var roll := _enemy_roll(t) var total: int = int(roll["total"]) # damage_taken 发射(kind=enemy_skill;铁壁整备减伤订阅面:shield 加法族合计 value×per(charge_layers), # 稳健拓荒树规则同面(rebuild 合成订阅);减后过下限保底。damage_reduce〔背水 hp_ratio when〕/ # counter_add〔保险丝/石肤〕/ac_bonus〔石肤〕同面施加;hp_ratio fact 由 damage_taken 载荷携带) var dt_res := _emit_hero_damage_taken(total, "enemy_skill", String(t.context.get("skill_id", "")), t.origin_cell, int(t.round_index)) total = int(dt_res["total"]) units.apply_damage(hero, total) var payload: Dictionary[Variant, Variant] = {"target": "hero", "skill_id": String(t.context.get("skill_id", "")), "baseline": int(t.context.get("baseline", 0)), "roll": int(roll["roll"]), "ac": int(roll["threshold"]), "tier": int(roll["discount"]), "attack_type": String(t.attack_type)} if bool(roll["crit"]): payload["crit"] = true if int(t.rider) > 0: payload["rider"] = int(t.rider) var label := _attack_label(String(t.display), bool(roll["crit"]), int(roll["discount"])) if int(t.rider) > 0: label += "+附加%d(必中)" % int(t.rider) emit_row_cb.call({"type": "damage", "source_class": String(t.source_class), "origin_cell": t.origin_cell, "highlight": String(t.highlight)}, int(t.round_index), label, total, payload) units.commit_action(t.source_unit) _outcomes[int(t.ticket_id)] = "redeemed" # 敌方技掷点算术(掷点/折扣/暴击/克制点数/池乘区/下限/rider 后置——取值序逐位同原单函数)。 # 返回 {roll, threshold, crit, discount, total}。 func _enemy_roll(t: Dictionary) -> Dictionary: var hero: Dictionary[Variant, Variant] = units.hero_unit() var roll_read: Dictionary[Variant, Variant] = _armor_roll() var threshold: int = defender_ac(hero) + int(db.combat.settlement.damage_roll.attack_bonus) var crit: bool = bool(roll_read.crit) var discount: int = _roll_discount(int(roll_read.roll), threshold) var cdelta: int = _counter_delta(String(t.attack_type), String(hero.get("armor_type", "light"))) var rolled: int = DamageMath.rolled_component(float(t.base_value), discount, crit, int(db.combat.settlement.damage_roll.crit_multiplier), cdelta) var pooled: int = DamageMath.pool_damage(rolled, clamped_pool("hero_defense"), clamped_pool("enemy_output")) var total: int = maxi(int(db.combat.settlement.damage_roll.damage_floor), pooled) + int(t.rider) # rider 后置于伤害下限——独立必中加项,不进乘区 return {"roll": int(roll_read.roll), "threshold": threshold, "crit": crit, "discount": discount, "total": total} # 全局受伤发射单点(迭代 46 卡A T2——所有主角承伤路径唯一发射口:敌方攻击/地形/危险格/错误封印 # 资源直伤(后三者经 board_flat_hero 单据进入)都经此 dispatch 一次 damage_taken 并消费一次; # 同一伤害单据重复发射由 context.hero_damage_consumed 标记闸拦(地形路径在开单点标记)。 # payload 必含 kind/amount/source/cell/round/hp_ratio;返回 {total, hits} 交调用侧按减后值开单/交付)。 func _emit_hero_damage_taken(amount: int, damage_kind: String, source: String, cell: Vector2i, ri: int) -> Dictionary: var hu: Dictionary[Variant, Variant] = units.hero_unit() var payload: Dictionary[Variant, Variant] = {"kind": damage_kind, "amount": amount, "source": source, "cell": cell, "round": ri, "hp_ratio": int(round(100.0 * float(int(hu.get("hp", 0))) / float(maxi(1, int(hu.get("max_hp", 1))))))} var hits := _fx_hits("damage_taken", payload) return {"total": int(_apply_hero_damage_effects(hits, amount)["total"]), "hits": hits} # damage_taken 唯一消费族(组合次序冻结:shield 加法族合计→damage_reduce 逐条减量〔背水/石层缓冲〕→ # ac_bonus 转编排器单点〔石肤 AC 累积归 set_hero_ac_override 消费面〕→counter_add 施加〔保险丝/石肤/ # 换图系临时计数夹具〕。返回 {total, shield})。 func _apply_hero_damage_effects(hits: Array, total: int) -> Dictionary: var shield := 0 for h in hits: match String(h.get("kind", "")): "shield": shield += int(h.get("value", 0)) * _per_value(String(h.get("per", "")), {}) "damage_reduce": total = maxi(0, total - int(h.get("value", 0))) "ac_bonus": _port.apply_ac(h) "counter_add": var set_v = h.get("set", null) _port.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) if shield > 0: total = maxi(int(db.combat.settlement.damage_roll.damage_floor), total - shield) return {"total": total, "shield": shield} # 棋盘克制直伤:必中全额不掷点;目标 = 结账时刻最前存活敌;克制点数进加法核;下限保底 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 armor_b: String = String(target.get("armor_type", "light")) var atk_b: String = String(t.attack_type) # clamp_check 发射+施加(克制判定修正点棋盘面;与 hero_attack 同表同点: # modify delta 订阅改写合计进克制点数;discount_shift 档位面属主角攻击专享,本面不施加) var cdelta: int = _counter_delta(atk_b, armor_b) if atk_b != "": for h in _fx_hits("clamp_check", {"matchup": atk_b + "_vs_" + armor_b}): # 无模式 delta 才入棋盘窗(mode 路由:clamp_bonus=主角攻击面专享、discount_shift=折扣档面) if String(h.get("kind", "")) == "modify_ticket" and h.has("delta") and String(h.get("mode", "")) == "": cdelta += int(h.delta) var dealt: int = DamageMath.counter_component(int(t.base_value), cdelta, int(db.combat.settlement.damage_roll.damage_floor)) units.apply_damage(target, dealt) emit_row_cb.call({"type": "damage", "source_class": String(t.source_class), "origin_cell": t.origin_cell, "highlight": String(t.highlight)}, int(t.round_index), "%s → %s" % [String(t.display), String(target.display_name)], dealt, {"target": String(target.display_name), "target_id": String(target.get("enemy_id", "")), "attack_type": String(t.attack_type), "effectiveness": cdelta}) _outcomes[int(t.ticket_id)] = "redeemed" # 棋盘群伤:无类型恒定表值,结账时刻全体存活敌逐个交付(单行汇总——行内 count = 兑现时存活数) func _resolve_board_blast(t: Dictionary) -> void: var alive: Array[Variant] = 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.combat.settlement.damage_roll.damage_floor), int(t.base_value)) # 伤害下限全来源 for u in alive: units.apply_damage(u, blast_dmg) # 恒定表值(下限保底),逐个;受击即死——死亡即不动手 emit_row_cb.call({"type": "damage", "source_class": String(t.source_class), "origin_cell": t.origin_cell, "highlight": String(t.highlight)}, int(t.round_index), "%s → 全体敌人 ×%d" % [String(t.display), alive.size()], blast_dmg, {"target": "all_enemies", "count": alive.size()}) _outcomes[int(t.ticket_id)] = "redeemed" # 棋盘主角直伤(反噬/治疗包负向/危险格/错误封印资源直伤):无类型必中,下限保底;context 键并入行载荷。 # damage_taken 全局发射点(迭代 46 卡A T2——单据未标 hero_damage_consumed 时在下限前发射并消费一次; # 地形路径已在授予段开单点发射并标记,此处跳过防重复) func _resolve_board_flat_hero(t: Dictionary) -> void: var dealt: int = int(t.base_value) if not bool((t.get("context", {}) as Dictionary).get("hero_damage_consumed", false)): var res_flat := _emit_hero_damage_taken(dealt, "board_flat_hero", String(t.get("source_class", "")), t.origin_cell, int(t.round_index)) dealt = int(res_flat["total"]) dealt = maxi(int(db.combat.settlement.damage_roll.damage_floor), dealt) # 伤害下限全来源(减后过下限) units.apply_damage(units.hero_unit(), dealt) var extra: Dictionary[Variant, Variant] = {"target": "hero"} for k in t.context.keys(): extra[k] = t.context[k] emit_row_cb.call({"type": "damage", "source_class": String(t.source_class), "origin_cell": t.origin_cell, "highlight": String(t.highlight)}, int(t.round_index), "%s → 主角" % String(t.display), dealt, extra) _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.combat.settlement.damage_roll.armor_roll_stream)) var roll: int = stream.randi_range(1, int(db.combat.settlement.damage_roll.roll_faces)) return {"roll": roll, "crit": roll == int(db.combat.settlement.damage_roll.crit_roll)} # 掷点折扣读数:nat 最大面恒全额(暴击分支前置),其余查档(缺口 = 阈值 − 掷点) func _roll_discount(roll: int, threshold: int) -> int: if roll == int(db.combat.settlement.damage_roll.crit_roll): return 100 return DamageMath.roll_discount_percent(roll, threshold, db.combat.settlement.damage_roll.roll_tier_bounds, db.combat.settlement.damage_roll.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.combat.hero.armor_class) for eff in effects.query_active("hero"): var row: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(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[Variant, Variant] = db.combat.status_effects.row_by_id(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[Variant, Variant] = db.combat.settlement.damage_roll.armor_effectiveness var row: Dictionary[Variant, Variant] = tbl.get(attack_type, {}) return int(row.get(armor, 0)) # 折扣档改善(猎弱本能——折扣表向全额方向移 value 档;查不到当前档 = 不动防御口径) func _discount_shift_up(discount: int, shift: int) -> int: var tbl: Array[Variant] = db.combat.settlement.damage_roll.roll_tier_discounts var idx := tbl.find(discount) if idx < 0: return discount return int(tbl[mini(tbl.size() - 1, idx + shift)]) # 池内加算 → 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[Variant, Variant] = db.combat.status_effects.row_by_id(eff.effect_id) if row.is_empty(): continue if String(db.combat.settlement.core.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.combat.settlement.core.pool_clamp_lo), int(db.combat.settlement.core.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) -> Variant: 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({"type": "reward_ineffective", "source_class": String(t.source_class), "origin_cell": t.origin_cell, "highlight": String(t.highlight)}, int(t.round_index), "%s 未生效(%s)" % [String(t.display), reason], 0, {"batch_key": String(t.context.get("batch_key", "")), "deferred": true})