卡①T5(拆件批wr6/V13): damage_controller 拆分+参数束——open_ticket 12参→(kind,source_class,display,spec)四参 spec 束(必载 channel/base_value/target_rule/ri 缺载 assert;可选 origin_cell/attack_type/rider/highlight/context 缺省同原签名;返回单据字典九栏目+requires/source_unit 逐字不变=契约8);类变量 19→15(dig_streak/charge_layers/rider_tier_bonus/rider_tier_crit→inject 束;fx_reveal_request/fx_ac_request→fx_calls 束 {reveal,ac});函数级拆分=_hero_attack_ctx→_ctx_rewrites(按序改写族四拍+armor_roll 发射+clamp_check 唯一发射点)+_ctx_clamp_delta(克制点数+订阅改写,消费同批命中禁重复发射);_hero_attack_redeem→_kill_overflow(kill 发射+贯穿弹射);_resolve_enemy_skill→_enemy_roll(掷点/折扣/克制/池乘/下限)+_enemy_shield_apply(damage_taken 施加面 {total,shield} 穿线,发射位留主函数保减伤落快照先后序)。调用点 30 处全改 spec 形(core 14=reward_grant 3+grant_segment 7+combat_segment 3+控制器内部 overflow 1;damage_controller_test 15;system_contracts_test 1)+inject/fx_calls 注入点改键(combat_segment :34/:37/:62、begin_battle :341-347)。验收:gdstyle 基线外 0 NEW(damage_controller 9 条存量键全清:branches/length/locals×3/class-vars 19→15/params 12→4)+ unit/integration 62 套件 1698 PASS 零 SCRIPT ERROR(〔dc1〕damage_controller 44 检查全绿含 A/B 模式/死亡作废/requires 门/flush_submit/AC 合流/池乘区;〔dc2〕system_contracts 九栏目契约绿;〔dc3〕round_flow/seal_polarity/level_shop 全绿)+ system_test 70 项 0 失败三闸过〔拆件批 V13〕

This commit is contained in:
DKChan committed 2026-10-27 07:52:02 +08:00
1 parent e2a2fbf3ee
commit 139cef098e
8 files changed
+231 -172

No files matched your search

+1 -1
View File
@@ -80,7 +80,7 @@ func apply_ac_bonus(h: Dictionary) -> void:
# 揭示效果入队(fx_reveal 掷格兑付延后到授予段末——防轮 facts 迭代中追加重入;见血开窍/
# 斩首学者〔经伤害控制器 fx_reveal_request 回调〕/地质学家/跳格采掘共用)
# 斩首学者〔经伤害控制器 fx_calls["reveal"] 回调〕/地质学家/跳格采掘共用)
func enqueue_reveal() -> void:
reveal.queue.append({"round": int(facts_cb.call("round_index"))})
+4 -4
View File
@@ -338,13 +338,13 @@ func _begin_battle_fx_state(enc: Dictionary) -> void:
_ledger = ledger_ref if ledger_ref is RunLedger else RunLedger.new() # 缺省新账=调试/单场零扰动面
_fx_host.mount_buckets(_ledger.adventure_counters, enc.get("level_counters")) # 挂冒险桶引用(跨战持久=同一字典引用,非副本搬运);关卡桶 LevelRunner 每关 new 下传、非 Dictionary 即不挂
_reward_grant.bind_battle(_ledger)
_damage.fx_reveal_request = _fx_host.enqueue_reveal
_damage.fx_ac_request = _fx_host.apply_ac_bonus
_damage.fx_calls["reveal"] = _fx_host.enqueue_reveal
_damage.fx_calls["ac"] = _fx_host.apply_ac_bonus
var feat_ids: Array[Variant] = enc.get("feats", []) as Array
# 流派档位质变不设(奇物.md v1「不设流派标签、
# 不设档位机制」):rider_tier 两注入恒零,散件单体效果=唯一形态
_damage.rider_tier_bonus = 0
_damage.rider_tier_crit = false
_damage.inject["rider_tier_bonus"] = 0
_damage.inject["rider_tier_crit"] = false
_feat_first_dig_used = false
# 树 rule 键消费(Fork 激活侧效果=效果引擎合成
# 订阅(rebuild 注入——round_start 首轮 AP+1 / damage_taken 减伤+1)。其余 rule 键
+138 -96
View File
@@ -45,13 +45,15 @@ var _hero_ac_override := -1 # 主角护甲等级合流入参(begin_battle
# 发射+施加面:dispatch 返回命中,本类按组合规则施加)
var _fx: EffectEngine = null
var _ctx: EffectContext = null
# 战斗段运行态事实(编排器每轮刷新——fact 取数面:attack_commit 载荷 dig_streak /
# charge_layers facts〔猎巢人 per/整备 attack_bonus per/厚积薄发 when/铁壁 shield per〕;
# 流派档位两运行态〔magic 档一 rider+1 / 档二 rider 可暴击〕)
var dig_streak := 0
var charge_layers := 0
var rider_tier_bonus := 0
var rider_tier_crit := false
# 战斗段运行态事实束(编排器每轮/每战注入——键集封闭,缺键读缺省即零扰动):
# dig_streak/charge_layers——facts 取数面(attack_commit 载荷 dig_streak;猎巢人 per/整备
# attack_bonus per/厚积薄发 when/铁壁 shield per 同读 charge_layers)——combat_segment 每轮写;
# rider_tier_bonus/rider_tier_crit——流派档位两运行态(magic 档一 rider+1/档二 rider 可暴击)
# ——begin_battle 每战写
var inject := {"dig_streak": 0, "charge_layers": 0, "rider_tier_bonus": 0, "rider_tier_crit": false}
# 效果引擎入站回调束(编排器 begin_battle 同点注入):reveal=揭示效果回调(kill 发射点 reveal_tiles
# 订阅面,入队授予段末兑付)/ac=ac_bonus 回调(石肤 AC 累积单点同步)
var fx_calls := {"reveal": Callable(), "ac": Callable()}
func _init(db_ref, rng_ref, units_ref, effects_ref) -> void:
@@ -83,23 +85,19 @@ func _fx_hits(event: String, payload: Dictionary) -> Array:
# 事件发射薄面(免施加观测位——enemy_hp_threshold 等现无订阅者的事件保留零扰动面)
# 补充事实面(fact 取值第二优先源——payload 未携带的运行态读数:charge_layers 编排器每轮注入。
# hp_ratio 补入〔背水 when 面,敌技 damage_taken 载荷不带血比例时回落〕;
# fx_reveal_request=揭示效果回调〔斩首学者 kill 订阅面——编排器注入,入队授予段末兑付〕)
# fx_calls.reveal=揭示效果回调〔斩首学者 kill 订阅面——编排器注入,入队授予段末兑付〕)
func _fx_fact(fact: String) -> Variant:
match fact:
"charge_layers":
return charge_layers
return int(inject["charge_layers"])
"dig_streak":
return dig_streak
return int(inject["dig_streak"])
"hp_ratio":
var hu: Dictionary[Variant, Variant] = units.hero_unit()
return int(round(100.0 * float(int(hu.get("hp", 0))) / float(maxi(1, int(hu.get("max_hp", 1)))))) # 百分比 int(when lte int 截断口径)
return null
var fx_reveal_request: Callable = Callable() # 揭示效果回调(编排器注入——kill 发射点 reveal_tiles 订阅面)
var fx_ac_request: Callable = Callable() # ac_bonus 回调(编排器注入——石肤 AC 累积单点同步)
# per 取数(per 字段解析——载荷同名字段优先,回落补充事实面;add_ticket_rider/attack_bonus/
# shield/open_ticket 的倍数承载:贡献值 = value × per 读数,cap 对乘积封顶)
func _per_value(name: String, payload: Dictionary) -> int:
@@ -141,33 +139,37 @@ func submit(t: Dictionary) -> int:
# 命中挂单据 context 随行——施加面在 _resolve_hero_attack 管线算术前逐族施加,开单时点=主角攻击
# 单恒为段内首单(结账现场与开单现场一致)。载荷 atk/target/matchup + 挖掘 streak 事实〔连珠矢/
# 执拗订阅面〕+巢穴控制数〔猎巢人〕)
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:
# 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: Dictionary = spec.get("context", {})
var t := {
"kind": kind,
"source_class": source_class,
"origin_cell": origin_cell,
"round_index": ri,
"origin_cell": spec.get("origin_cell", Vector2i(-1, -1)),
"round_index": int(spec.get("ri", 0)),
"display": display,
"channel": channel,
"attack_type": attack_type,
"base_value": base_value,
"target_rule": target_rule,
"rider": rider,
"highlight": highlight,
"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(target_rule)
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 = (attack_type + "_vs_" + armor_b) if attack_type != "" else ""
var ac_payload := {"atk": int(base_value),
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": dig_streak, "nest_count": _nest_count()}
"matchup": matchup_o, "dig_streak": int(inject["dig_streak"]), "nest_count": _nest_count()}
t.context["fx_attack_hits"] = _fx_hits("attack_commit", ac_payload)
t.context["fx_attack_payload"] = ac_payload
return t
@@ -261,8 +263,9 @@ func _resolve_hero_attack(t: Dictionary) -> void:
_hero_attack_deliver(t, ctx, redemption)
# 命中面(原 :273-328 逐字):目标/护甲克制 matchup/armor_roll 发射/按序改写族四拍(矿工重锤命中
# 交换→专注免掷→厚积薄发必中→猎弱折扣移档)/克制点数读值+订阅改写。返回穿线 ctx(空=目标不可达)。
# 命中面(目标/护甲克制 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:
@@ -271,14 +274,29 @@ func _hero_attack_ctx(t: Dictionary) -> Dictionary:
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_hits: Array[Variant] = (t.context.get("fx_attack_hits", []) as Array)
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.settlement.damage_roll.attack_bonus)
# armor_roll 发射点(敌护甲掷骰判定;免掷恒折扣订阅面)
var ar_hits := _fx_hits("armor_roll", {"target_ac": threshold, "roll": int(roll_read.roll)})
# 按序改写族第一拍:矿工重锤命中交换(正负双修原生形态:命中判定减成 trade_hit 点〔更易命中〕
# 换攻击增伤 trade_atk%——高 AC 负期望/低 AC 正期望;改写位先于掷点折扣读数)
var trade_hit := 0
for h in ac_hits:
if String(h.get("kind", "")) == "modify_ticket" and int(h.get("trade_hit", 0)) > 0:
@@ -287,23 +305,18 @@ func _hero_attack_ctx(t: Dictionary) -> Dictionary:
threshold = maxi(1, threshold - trade_hit)
var crit: bool = bool(roll_read.crit)
var discount: int = _roll_discount(int(roll_read.roll), threshold)
# 按序改写族第二拍:专注免掷骰恒定折扣(方差买断成期望——稳定器人格;roll 记 0 归因)
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
# 按序改写族第三拍:厚积薄发满层必中(spike ready 时 discount 恒全额、暴击面走倍乘不加掷;
# when charge_layers≥3 在 dispatch 判定——满层持续状态口径)
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
# 按序改写族第四拍:猎弱本能对轻甲敌折扣档改善 value 档(对冲 pierce 列 L1 −1 开局罚;
# 改善 = 折扣表向全额方向移档,nat 最大面/已全额不重复)
var shift := 0
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
@@ -311,21 +324,24 @@ func _hero_attack_ctx(t: Dictionary) -> Dictionary:
shift += int(h.get("value", 0))
if shift > 0 and armor == "light" and discount < 100:
discount = _discount_shift_up(discount, shift)
# 克制点数读值+订阅改写(魔网织者/湛蓝誓约/猎手刺剑 clamp_check modify delta——克制矩阵查表值
# 后的直调;hero_attack 与 board_typed 两消费面同享)
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)
# 淬毒刃纹(行 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}
return cdelta
# 基值装配(原 :329-355 逐字):基值+单位标记加伤+矿工重锤增伤面+加法族(attack_bonus/
# 基值装配(基值+单位标记加伤+矿工重锤增伤面+加法族(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"]
@@ -360,8 +376,8 @@ func _hero_attack_base_value(t: Dictionary, ctx: Dictionary) -> float:
return base_value
# 掷点兑现(原 :356-402 逐字):rolled 分量/payload 归因/池乘区/下限/英雄侧 rider 族/落地扣血/
# 击杀标记/kill 发射+贯穿弹射。返回 {landed, payload, hero_rider}(交付步消费)。
# 掷点兑现(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"]
@@ -381,7 +397,7 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D
# (奥术余烬「普攻命中后恒定结算」语义);猎巢人巢控光环(per nest_count)/矿工的执拗开格加成
# (per dig_streak、cap 封顶)同通道合计;档一质变 rider+1 / 档二(crit 时)rider 乘暴击倍率。
# rider 不改变下限语义(landed 含 rider 后不再过 floor)。
var hero_rider := rider_tier_bonus
var hero_rider := int(inject["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", "")), ctx["ac_payload"])
@@ -389,39 +405,46 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D
contrib = mini(int(h.cap), contrib)
hero_rider += contrib
if hero_rider > 0:
if bool(ctx["crit"]) and rider_tier_crit:
if bool(ctx["crit"]) and bool(inject["rider_tier_crit"]):
hero_rider = int(round(float(hero_rider) * float(db.settlement.damage_roll.crit_multiplier)))
landed += hero_rider
payload["hero_rider"] = hero_rider
units.apply_damage(target, landed)
payload["killed"] = not target.alive # 击杀标记在扣血后取
# kill 发射+施加(敌人死亡确认;贯穿射击溢出弹射订阅面:对下一存活敌
# 以穿刺类型弹射本体 value%〔无次敌=弹射落空〕。B 模式入队段尾 flush 兑现;A 模式递归 resolve)
if bool(payload.get("killed", false)):
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" and fx_reveal_request.is_valid():
fx_reveal_request.call() # 斩首学者——击杀揭示 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",
"贯穿弹射", "board", maxi(int(db.settlement.damage_roll.damage_floor), int(landed * int(h.get("value", 50)) / 100)),
DamageController.TARGET_FRONT_ALIVE, int(t.round_index), t.origin_cell, "pierce")
if policy == POLICY_SEGMENT:
_queue.append(of_t)
else:
_resolve(of_t)
_kill_overflow(t, target, landed)
return {"landed": landed, "payload": payload, "hero_rider": hero_rider}
# 交付(原 :403-414 逐字):即时执行族(碎甲震击护甲裂解)/标签装配/明细行发射/兑现登记。
# 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":
var reveal_cb: Callable = fx_calls["reveal"]
if reveal_cb.is_valid():
reveal_cb.call() # 斩首学者——击杀揭示 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.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"]
# 即时执行族:碎甲震击打重甲附加「护甲裂解」(复用 off_guard——零新状态行;apply_effect 域闭/
# 重复授予由状态容器自行守卫)
for h in (ctx["ac_hits"] as Array):
if String(h.get("kind", "")) == "apply_status":
# source_class 用中性名(fx_apply_status 泛用通道,不挂件名)
@@ -436,8 +459,37 @@ func _hero_attack_deliver(t: Dictionary, ctx: Dictionary, redemption: Dictionary
# 敌方伤害技:掷点 → 折扣/暴击 → 克制点数(attack_type vs 主角护甲类型——
# 无类型恒中性 delta 0,写法同 hero_attack 的 _counter_delta 消费位)→ hero_defense×enemy_output
# 池乘区 → 下限 → +rider(后置必中附加)→ 交付 → commit_action(作废单不 commit——死亡即不动手)
# 池乘区 → 下限 → +rider(后置必中附加)→ 交付 → commit_action(作废单不 commit——死亡即不动手)。
# 掷点算术归 _enemy_roll;damage_taken 施加面归 _enemy_shield_apply(发射在本函数——
# 与减伤落快照的先后序固定)。
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 由 _fx_fact 补充事实面回落提供)
var dt_hits := _fx_hits("damage_taken", {"kind": "enemy_skill", "amount": total,
"source": String(t.context.get("skill_id", "")), "round": int(t.round_index)})
total = int(_enemy_shield_apply(dt_hits, total)["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.settlement.damage_roll.attack_bonus)
@@ -447,39 +499,29 @@ func _resolve_enemy_skill(t: Dictionary) -> void:
var rolled: int = DamageMath.rolled_component(float(t.base_value), discount, crit, int(db.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.settlement.damage_roll.damage_floor), pooled) + int(t.rider) # rider 后置于伤害下限——独立必中加项,不进乘区
# damage_taken 发射+施加(kind=enemy_skill;铁壁整备减伤订阅面:
# shield 加法族合计 value×per(charge_layers),稳健拓荒树规则同面(rebuild 合成订阅);
# 减后过下限保底。damage_reduce〔背水 hp_ratio when〕/counter_add〔保险丝/
# 石肤〕/ac_bonus〔石肤〕同面施加;hp_ratio fact 由 _fx_fact 补充事实面回落提供)
return {"roll": int(roll_read.roll), "threshold": threshold, "crit": crit, "discount": discount, "total": total}
# damage_taken 施加面(shield 加法族合计;damage_reduce 逐条减量〔背水〕;ac_bonus 转编排器单点
# 〔石肤 AC 累积归 set_hero_ac_override 消费面〕;counter_add 施加)。返回 {total, shield}。
func _enemy_shield_apply(hits: Array, total: int) -> Dictionary:
var shield := 0
for h in _fx_hits("damage_taken", {"kind": "enemy_skill", "amount": total,
"source": String(t.context.get("skill_id", "")), "round": int(t.round_index)}):
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":
if fx_ac_request.is_valid():
fx_ac_request.call(h) # 石肤 AC 累积归编排器单点(set_hero_ac_override 消费面)
var ac_cb: Callable = fx_calls["ac"]
if ac_cb.is_valid():
ac_cb.call(h)
"counter_add":
var set_v = h.get("set", null)
EffectEngine.counter_add(_ctx, 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.settlement.damage_roll.damage_floor), total - shield)
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_read.roll), "ac": threshold, "tier": discount, "attack_type": String(t.attack_type)}
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({"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"
return {"total": total, "shield": shield}
# 棋盘克制直伤:必中全额不掷点;目标 = 结账时刻最前存活敌;克制点数进加法核;下限保底
+12 -9
View File
@@ -302,9 +302,10 @@ func _flat_action(row: Dictionary, display: String, att: Dictionary) -> String:
# 群伤单据(无类型恒定表值——打全体存活敌)
func _flat_blast(row: Dictionary, display: String, att: Dictionary, sync: bool) -> String:
var t := damage.open_ticket(DamageController.KIND_BOARD_BLAST, String(att.get("source_class", "")), display, "board", int(row.power),
DamageController.TARGET_ALL_ALIVE, int(att.get("ri", 0)), att.get("cell", Vector2i(-1, -1)), "", 0,
String(att.get("highlight", "")), {"batch_key": String(row.reward_id)})
var t := damage.open_ticket(DamageController.KIND_BOARD_BLAST, String(att.get("source_class", "")), display,
{"channel": "board", "base_value": int(row.power), "target_rule": DamageController.TARGET_ALL_ALIVE,
"ri": int(att.get("ri", 0)), "origin_cell": att.get("cell", Vector2i(-1, -1)),
"highlight": String(att.get("highlight", "")), "context": {"batch_key": String(row.reward_id)}})
return outcome_of(submit(t, sync))
@@ -313,9 +314,10 @@ func _flat_typed(row: Dictionary, display: String, att: Dictionary, sync: bool)
if String(row.get("polarity", "positive")) == "negative":
return _submit_flat_hero(display, int(row.power), att, sync)
var atk_type: String = String(row.get("attack_type", String(row.cell_type)))
var t2 := damage.open_ticket(DamageController.KIND_BOARD_TYPED, String(att.get("source_class", "")), display, "board", int(row.power),
DamageController.TARGET_FRONT_ALIVE, int(att.get("ri", 0)), att.get("cell", Vector2i(-1, -1)), atk_type, 0,
String(att.get("highlight", "")), {"batch_key": String(row.reward_id)})
var t2 := damage.open_ticket(DamageController.KIND_BOARD_TYPED, String(att.get("source_class", "")), display,
{"channel": "board", "base_value": int(row.power), "target_rule": DamageController.TARGET_FRONT_ALIVE,
"ri": int(att.get("ri", 0)), "origin_cell": att.get("cell", Vector2i(-1, -1)), "attack_type": atk_type,
"highlight": String(att.get("highlight", "")), "context": {"batch_key": String(row.reward_id)}})
return outcome_of(submit(t2, sync))
@@ -358,9 +360,10 @@ func outcome_of(ticket_id: int) -> String:
# 主角直伤单据(反噬/治疗包负向——无类型必中,下限保底在控制器管线内)
func _submit_flat_hero(display: String, power: int, att: Dictionary, sync: bool) -> String:
var t := damage.open_ticket(DamageController.KIND_BOARD_FLAT_HERO, String(att.get("source_class", "")), display, "board", power,
DamageController.TARGET_HERO, int(att.get("ri", 0)), att.get("cell", Vector2i(-1, -1)), "", 0,
String(att.get("highlight", "")), {})
var t := damage.open_ticket(DamageController.KIND_BOARD_FLAT_HERO, String(att.get("source_class", "")), display,
{"channel": "board", "base_value": power, "target_rule": DamageController.TARGET_HERO,
"ri": int(att.get("ri", 0)), "origin_cell": att.get("cell", Vector2i(-1, -1)),
"highlight": String(att.get("highlight", "")), "context": {}})
return outcome_of(submit(t, sync))
+17 -11
View File
@@ -31,10 +31,10 @@ func run(ri: int) -> void:
round_safe += 1
elif evt.kind == "mine":
round_mine = true
oc._damage.dig_streak = 0 if round_mine else round_safe
oc._damage.inject["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
oc._damage.inject["charge_layers"] = oc.action_economy.charge_layers
_hero_strike(ri)
_enemy_actions(ri)
_board_deferred_settle(ri)
@@ -52,19 +52,22 @@ func _hero_strike(ri: int) -> void:
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")))
var ht := oc._damage.open_ticket(DamageController.KIND_HERO_ATTACK, "hero_attack", "主角攻击",
{"channel": "combat", "base_value": oc._hero_atk, "target_rule": DamageController.TARGET_FRONT_ALIVE,
"ri": ri, "origin_cell": Vector2i(-1, -1),
"attack_type": 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))))
var segments := mini(int(h.get("cap", 2)), int(oc._damage.inject["dig_streak"]) / maxi(1, int(h.get("divisor", 3))))
for s in segments:
# source_class 用中性名;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")
var ct := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "fx_streak_segments", "连击分段",
{"channel": "board", "base_value": int(h.get("value", 0)),
"target_rule": DamageController.TARGET_FRONT_ALIVE, "ri": ri,
"origin_cell": oc.minesweeper.hero_pos(), "attack_type": "pierce"})
oc._damage.submit(ct)
@@ -88,15 +91,18 @@ func _enemy_actions(ri: int) -> void:
oc._enemy_system.commit_action(u)
continue
# 伤害技(含普攻——倍率恒 1)= 敌方伤害技单据:基线 = round(atk×倍率)、rider 预计算归开单侧
# (表值算术零随机),掷点/克制查表(第 9 参 = 敌方 attack_type——迭代 24 N3 轴开列前恒空串不查表)/
# (表值算术零随机),掷点/克制查表(spec 束 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_roll.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)
var et := oc._damage.open_ticket(DamageController.KIND_ENEMY_SKILL, "enemy_action",
"%s %s" % [u.display_name, String(srow.display_name)],
{"channel": "combat", "base_value": baseline, "target_rule": DamageController.TARGET_HERO,
"ri": ri, "origin_cell": Vector2i(-1, -1),
"attack_type": String(u.get("attack_type", "normal")), "rider": rider})
et["source_unit"] = u
et["context"] = {"skill_id": skill_id, "baseline": baseline}
oc._damage.submit(et)
+21 -16
View File
@@ -93,9 +93,9 @@ func _reveal_safe_apply(dr_hits: Array, evt: Dictionary, ri: int) -> void:
var spell_hits: Array[Variant] = 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")
var spell_t := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_first_dig_spell", "奥术矢",
{"channel": "board", "base_value": int((spell_hits[0] as Dictionary).get("value", 0)),
"target_rule": DamageController.TARGET_FRONT_ALIVE, "ri": ri, "origin_cell": evt.cell, "attack_type": "magic"})
reward_grant.submit(spell_t, false)
@@ -118,8 +118,9 @@ func _reveal_mine(evt: Dictionary, snap: Dictionary, ri: int) -> void:
reward_grant.grant_routed(oc._feat_yield_adjust(snap, false, 0, 0), "negative", {"source_class": "mine_hit", "cell": evt.cell, "ri": ri, "halve_pct": int(applied["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, "", {})
var nct := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "attune_nest_counter", "惊巢反制·反弹",
{"channel": "board", "base_value": int(h.get("value", 0)), "target_rule": DamageController.TARGET_ALL_ALIVE,
"ri": ri, "origin_cell": evt.cell})
reward_grant.submit(nct, false)
@@ -147,8 +148,9 @@ func _reveal_mine_apply(mh_hits: Array, snap: Dictionary, cell: Vector2i, ri: in
fx_host._fx_bonus_delta[axis] += int(h.get("delta", 0))
"blast_scaled":
var blast_v := maxi(int(oc.db.settlement.damage_roll.damage_floor), int(float(int(snap.get("power", 0))) * 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, cell, "", 0, "", {})
var bs_t := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_blast_scaled", "陷阱爆发",
{"channel": "board", "base_value": blast_v, "target_rule": DamageController.TARGET_ALL_ALIVE,
"ri": ri, "origin_cell": cell})
reward_grant.submit(bs_t, false)
return {"halve_pct": halve_pct, "mine_reduce": mine_reduce}
@@ -173,8 +175,9 @@ func _terrain_batch(ri: int) -> void:
"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))))))})
dt_amount = _terrain_damage_reduce(dt_hits, dt_amount)
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)})
var vt := oc._damage.open_ticket(DamageController.KIND_BOARD_FLAT_HERO, "地形变异", String(vrow.get("display_name", vid)),
{"channel": "board", "base_value": dt_amount, "target_rule": DamageController.TARGET_HERO,
"ri": ri, "origin_cell": evt.cell, "context": {"variant_id": String(vid)}})
reward_grant.submit(vt, false)
for h in dt_hits:
if String(h.get("kind", "")) == "grant_material":
@@ -223,9 +226,9 @@ func _seal_strike_batch(ri: int) -> void:
for fevt in oc._facts_seals:
if not bool(fevt.get("hit", false)):
continue
var st_t := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_seal_strike",
"战封印军规", "board", int(strike["value"]), DamageController.TARGET_FRONT_ALIVE, ri,
fevt.cell, "normal")
var st_t := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_seal_strike", "战封印军规",
{"channel": "board", "base_value": int(strike["value"]), "target_rule": DamageController.TARGET_FRONT_ALIVE,
"ri": ri, "origin_cell": fevt.cell, "attack_type": "normal"})
reward_grant.submit(st_t, false)
@@ -284,8 +287,9 @@ func _round_end_blast_tickets(re_fx: Array, ri: int) -> void:
var mg_total := int(h.get("value", 0)) * mg_layers
if mg_total > 0:
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, "", {})
"积层反噬 ×%d" % mg_layers,
{"channel": "board", "base_value": mg_total, "target_rule": DamageController.TARGET_ALL_ALIVE,
"ri": ri, "origin_cell": oc.minesweeper.hero_pos()})
reward_grant.submit(mg_t, false)
@@ -326,8 +330,9 @@ func _seal_burst_tickets(fh_hits: Array, cell: Vector2i, ri: int) -> void:
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_seal_burst", "封印爆裂", "board",
int(h.get("value", 0)), DamageController.TARGET_ALL_ALIVE, ri, cell, "", 0, "highlight", {})
var fbt := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_seal_burst", "封印爆裂",
{"channel": "board", "base_value": int(h.get("value", 0)), "target_rule": DamageController.TARGET_ALL_ALIVE,
"ri": ri, "origin_cell": cell, "highlight": "highlight"})
reward_grant.submit(fbt, false)
@@ -86,24 +86,25 @@ func _run_recipe_sequence(db) -> Array:
atk_type = String(eff_keys[0])
var floor_v: int = int(h.db.settlement.damage_roll.damage_floor)
var t1: int = ctrl.submit(ctrl.open_ticket(DamageControllerScript.KIND_HERO_ATTACK, "hero_attack",
"T1主角攻击", "战斗段", int(h.db.hero.atk), DamageControllerScript.TARGET_FRONT_ALIVE, 1))
var t1: int = ctrl.submit(ctrl.open_ticket(DamageControllerScript.KIND_HERO_ATTACK, "hero_attack", "T1主角攻击",
{"channel": "战斗段", "base_value": int(h.db.hero.atk),
"target_rule": DamageControllerScript.TARGET_FRONT_ALIVE, "ri": 1}))
# 敌方技来源取后位敌(前位是攻击/直伤/群伤的靶位——来源与靶位分离,免死置干扰配方 redeemed 断言)
var alive_list: Array = units.query_alive("enemy")
var src0: Dictionary = alive_list[alive_list.size() - 1]
var ticket2: Dictionary = ctrl.open_ticket(DamageControllerScript.KIND_ENEMY_SKILL, "enemy_action",
"T2敌方技", "战斗段", int(src0.atk), DamageControllerScript.TARGET_HERO, 1,
Vector2i(-1, -1), "", floor_v, "", {"skill_id": "fixture_skill", "baseline": int(src0.atk)})
var ticket2: Dictionary = ctrl.open_ticket(DamageControllerScript.KIND_ENEMY_SKILL, "enemy_action", "T2敌方技",
{"channel": "战斗段", "base_value": int(src0.atk), "target_rule": DamageControllerScript.TARGET_HERO,
"ri": 1, "rider": floor_v, "context": {"skill_id": "fixture_skill", "baseline": int(src0.atk)}})
ticket2["source_unit"] = src0
var t2: int = ctrl.submit(ticket2)
var t3: int = ctrl.submit(ctrl.open_ticket(DamageControllerScript.KIND_BOARD_TYPED, "clear_reward",
"T3克制直伤", "棋盘兑付", floor_v, DamageControllerScript.TARGET_FRONT_ALIVE, 1,
Vector2i(-1, -1), atk_type))
var t4: int = ctrl.submit(ctrl.open_ticket(DamageControllerScript.KIND_BOARD_BLAST, "clear_reward",
"T4群伤", "棋盘兑付", floor_v, DamageControllerScript.TARGET_ALL_ALIVE, 1))
var t5: int = ctrl.submit(ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit",
"T5反噬", "棋盘兑付", floor_v, DamageControllerScript.TARGET_HERO, 1,
Vector2i(-1, -1), "", 0, "", {"terrain": "fixture"}))
var t3: int = ctrl.submit(ctrl.open_ticket(DamageControllerScript.KIND_BOARD_TYPED, "clear_reward", "T3克制直伤",
{"channel": "棋盘兑付", "base_value": floor_v, "target_rule": DamageControllerScript.TARGET_FRONT_ALIVE,
"ri": 1, "attack_type": atk_type}))
var t4: int = ctrl.submit(ctrl.open_ticket(DamageControllerScript.KIND_BOARD_BLAST, "clear_reward", "T4群伤",
{"channel": "棋盘兑付", "base_value": floor_v, "target_rule": DamageControllerScript.TARGET_ALL_ALIVE, "ri": 1}))
var t5: int = ctrl.submit(ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit", "T5反噬",
{"channel": "棋盘兑付", "base_value": floor_v, "target_rule": DamageControllerScript.TARGET_HERO,
"ri": 1, "context": {"terrain": "fixture"}}))
ctrl.flush()
var outcomes_redeemed := true
@@ -187,8 +188,8 @@ func _test_policy_immediate_mode(db) -> void:
return
h.ctrl.policy = DamageControllerScript.POLICY_IMMEDIATE
var floor_v: int = int(h.db.settlement.damage_roll.damage_floor)
var t: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit",
"A模式反噬", "棋盘兑付", floor_v, DamageControllerScript.TARGET_HERO, 1))
var t: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit", "A模式反噬",
{"channel": "棋盘兑付", "base_value": floor_v, "target_rule": DamageControllerScript.TARGET_HERO, "ri": 1}))
Kit.check("A 模式开单即结(无 flush 即 redeemed)", h.ctrl.outcome(t) == "redeemed")
Kit.check("A 模式主角即时扣血", int(h.units.hero_unit().hp) == int(h.db.hero.max_hp) - floor_v)
@@ -203,8 +204,8 @@ func _test_source_death_void(db) -> void:
var alive_list: Array = h.units.query_alive("enemy")
var src: Dictionary = alive_list[alive_list.size() - 1]
var hero_before: int = int(h.units.hero_unit().hp)
var ticket: Dictionary = h.ctrl.open_ticket(DamageControllerScript.KIND_ENEMY_SKILL, "enemy_action",
"亡敌未结单", "战斗段", int(src.atk), DamageControllerScript.TARGET_HERO, 1)
var ticket: Dictionary = h.ctrl.open_ticket(DamageControllerScript.KIND_ENEMY_SKILL, "enemy_action", "亡敌未结单",
{"channel": "战斗段", "base_value": int(src.atk), "target_rule": DamageControllerScript.TARGET_HERO, "ri": 1})
ticket["source_unit"] = src
var t: int = h.ctrl.submit(ticket)
h.units.apply_damage(src, int(src.max_hp)) # 敌当场倒下 → unit_died → 该敌未结单作废
@@ -234,8 +235,8 @@ func _test_identical_content_dual_enemy(db: ConfigDb) -> void:
var h = _harness(db)
if not h.spawn_ok:
return
var ticket: Dictionary = h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit",
"同容双敌单", "棋盘兑付", 1, DamageControllerScript.TARGET_HERO, 1)
var ticket: Dictionary = h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit", "同容双敌单",
{"channel": "棋盘兑付", "base_value": 1, "target_rule": DamageControllerScript.TARGET_HERO, "ri": 1})
ticket["source_unit"] = a
var t: int = h.ctrl.submit(ticket)
h.units.unit_died.emit(b) # 真实死亡事件(单位自行宣布)——同容异实例
@@ -244,8 +245,8 @@ func _test_identical_content_dual_enemy(db: ConfigDb) -> void:
var h2 = _harness(db)
if not h2.spawn_ok:
return
var ticket2: Dictionary = h2.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit",
"同容双敌单·自亡", "棋盘兑付", 1, DamageControllerScript.TARGET_HERO, 1)
var ticket2: Dictionary = h2.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit", "同容双敌单·自亡",
{"channel": "棋盘兑付", "base_value": 1, "target_rule": DamageControllerScript.TARGET_HERO, "ri": 1})
ticket2["source_unit"] = a
var t2: int = h2.ctrl.submit(ticket2)
h2.units.unit_died.emit(a)
@@ -265,8 +266,9 @@ func _test_target_id_payload(db: ConfigDb) -> void:
var target = h.units.default_target()
var expect_id := String(target.get("enemy_id", ""))
Kit.check("前置:目标敌携非空 enemy_id", expect_id != "")
var ticket: Dictionary = h.ctrl.open_ticket(DamageControllerScript.KIND_HERO_ATTACK, "hero_attack",
"T6目标id", "战斗段", int(db.hero.atk), DamageControllerScript.TARGET_FRONT_ALIVE, 1)
var ticket: Dictionary = h.ctrl.open_ticket(DamageControllerScript.KIND_HERO_ATTACK, "hero_attack", "T6目标id",
{"channel": "战斗段", "base_value": int(db.hero.atk),
"target_rule": DamageControllerScript.TARGET_FRONT_ALIVE, "ri": 1})
var fx_payload: Dictionary = ticket.context.get("fx_attack_payload", {})
Kit.check("attack_commit fx 载荷携 target_id〔wd12〕",
String(fx_payload.get("target_id", "")) == expect_id and String(fx_payload.get("target", "")) != "")
@@ -286,13 +288,14 @@ func _test_requires_gate_and_zero(db) -> void:
# 先清场(flush_submit 同步开单——清图批专用通道的契约面同时被覆盖)
var front_hp: int = int(h.units.query_alive("enemy")[0].max_hp)
var big: int = front_hp * 10
var t_clear: int = h.ctrl.flush_submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_BLAST, "clear_reward",
"清场群伤", "棋盘兑付", big, DamageControllerScript.TARGET_ALL_ALIVE, 1))
var t_clear: int = h.ctrl.flush_submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_BLAST, "clear_reward", "清场群伤",
{"channel": "棋盘兑付", "base_value": big, "target_rule": DamageControllerScript.TARGET_ALL_ALIVE, "ri": 1}))
Kit.check("flush_submit 同步开单即结(清图批契约)", h.ctrl.outcome(t_clear) == "redeemed" and h.units.query_alive("enemy").is_empty())
var t_zero: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_BLAST, "clear_reward",
"空场群伤", "棋盘兑付", floor_v, DamageControllerScript.TARGET_ALL_ALIVE, 1))
var gated_ticket: Dictionary = h.ctrl.open_ticket(DamageControllerScript.KIND_HERO_ATTACK, "hero_attack",
"前置门攻击", "战斗段", int(h.db.hero.atk), DamageControllerScript.TARGET_FRONT_ALIVE, 1)
var t_zero: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_BLAST, "clear_reward", "空场群伤",
{"channel": "棋盘兑付", "base_value": floor_v, "target_rule": DamageControllerScript.TARGET_ALL_ALIVE, "ri": 1}))
var gated_ticket: Dictionary = h.ctrl.open_ticket(DamageControllerScript.KIND_HERO_ATTACK, "hero_attack", "前置门攻击",
{"channel": "战斗段", "base_value": int(h.db.hero.atk),
"target_rule": DamageControllerScript.TARGET_FRONT_ALIVE, "ri": 1})
gated_ticket["requires"] = t_zero
var t_gated: int = h.ctrl.submit(gated_ticket)
h.ctrl.flush()
@@ -308,10 +311,10 @@ func _test_hero_death_halt(db) -> void:
if not h.spawn_ok:
return
var max_hp: int = int(h.db.hero.max_hp)
var t_kill: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit",
"致死反噬", "棋盘兑付", max_hp, DamageControllerScript.TARGET_HERO, 1))
var t_after: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit",
"余单", "棋盘兑付", 1, DamageControllerScript.TARGET_HERO, 2))
var t_kill: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit", "致死反噬",
{"channel": "棋盘兑付", "base_value": max_hp, "target_rule": DamageControllerScript.TARGET_HERO, "ri": 1}))
var t_after: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit", "余单",
{"channel": "棋盘兑付", "base_value": 1, "target_rule": DamageControllerScript.TARGET_HERO, "ri": 2}))
h.ctrl.flush()
Kit.check("致死单 redeemed 且主角 hp 归零", h.ctrl.outcome(t_kill) == "redeemed" and int(h.units.hero_unit().hp) == 0)
Kit.check("主角倒下后余单 voided(flush 终止)", h.ctrl.outcome(t_after) == "voided")
@@ -71,8 +71,8 @@ func _test_rng_tag_closure(db) -> void:
func _test_ticket_nine_columns() -> void:
var ctrl = _bare_controller()
var t: Dictionary = ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit",
"契约探针", "棋盘兑付", 1, DamageControllerScript.TARGET_HERO, 0)
var t: Dictionary = ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit", "契约探针",
{"channel": "棋盘兑付", "base_value": 1, "target_rule": DamageControllerScript.TARGET_HERO, "ri": 0})
var columns := ["kind", "source_class", "origin_cell", "round_index", "display",
"channel", "attack_type", "base_value", "target_rule", "rider", "highlight", "context",
"requires", "source_unit"]