From 139cef098eb34037d0fff7e16d093caf27f6d0bf Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Tue, 27 Oct 2026 07:52:02 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=A1=E2=91=A0T5(=E6=8B=86=E4=BB=B6?= =?UTF-8?q?=E6=89=B9wr6/V13):=20damage=5Fcontroller=20=E6=8B=86=E5=88=86?= =?UTF-8?q?=EF=BC=8B=E5=8F=82=E6=95=B0=E6=9D=9F=E2=80=94=E2=80=94open=5Fti?= =?UTF-8?q?cket=2012=E5=8F=82=E2=86=92(kind,source=5Fclass,display,spec)?= =?UTF-8?q?=E5=9B=9B=E5=8F=82=20spec=20=E6=9D=9F=EF=BC=88=E5=BF=85?= =?UTF-8?q?=E8=BD=BD=20channel/base=5Fvalue/target=5Frule/ri=20=E7=BC=BA?= =?UTF-8?q?=E8=BD=BD=20assert=EF=BC=9B=E5=8F=AF=E9=80=89=20origin=5Fcell/a?= =?UTF-8?q?ttack=5Ftype/rider/highlight/context=20=E7=BC=BA=E7=9C=81?= =?UTF-8?q?=E5=90=8C=E5=8E=9F=E7=AD=BE=E5=90=8D=EF=BC=9B=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E5=8D=95=E6=8D=AE=E5=AD=97=E5=85=B8=E4=B9=9D=E6=A0=8F=E7=9B=AE?= =?UTF-8?q?+requires/source=5Funit=20=E9=80=90=E5=AD=97=E4=B8=8D=E5=8F=98?= =?UTF-8?q?=EF=BC=9D=E5=A5=91=E7=BA=A68=EF=BC=89=EF=BC=9B=E7=B1=BB?= =?UTF-8?q?=E5=8F=98=E9=87=8F=2019=E2=86=9215=EF=BC=88dig=5Fstreak/charge?= =?UTF-8?q?=5Flayers/rider=5Ftier=5Fbonus/rider=5Ftier=5Fcrit=E2=86=92inje?= =?UTF-8?q?ct=20=E6=9D=9F=EF=BC=9Bfx=5Freveal=5Frequest/fx=5Fac=5Frequest?= =?UTF-8?q?=E2=86=92fx=5Fcalls=20=E6=9D=9F=20{reveal,ac}=EF=BC=89=EF=BC=9B?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E7=BA=A7=E6=8B=86=E5=88=86=3D=5Fhero=5Fattac?= =?UTF-8?q?k=5Fctx=E2=86=92=5Fctx=5Frewrites(=E6=8C=89=E5=BA=8F=E6=94=B9?= =?UTF-8?q?=E5=86=99=E6=97=8F=E5=9B=9B=E6=8B=8D+armor=5Froll=20=E5=8F=91?= =?UTF-8?q?=E5=B0=84+clamp=5Fcheck=20=E5=94=AF=E4=B8=80=E5=8F=91=E5=B0=84?= =?UTF-8?q?=E7=82=B9)+=5Fctx=5Fclamp=5Fdelta(=E5=85=8B=E5=88=B6=E7=82=B9?= =?UTF-8?q?=E6=95=B0+=E8=AE=A2=E9=98=85=E6=94=B9=E5=86=99=EF=BC=8C?= =?UTF-8?q?=E6=B6=88=E8=B4=B9=E5=90=8C=E6=89=B9=E5=91=BD=E4=B8=AD=E7=A6=81?= =?UTF-8?q?=E9=87=8D=E5=A4=8D=E5=8F=91=E5=B0=84)=EF=BC=9B=5Fhero=5Fattack?= =?UTF-8?q?=5Fredeem=E2=86=92=5Fkill=5Foverflow(kill=20=E5=8F=91=E5=B0=84+?= =?UTF-8?q?=E8=B4=AF=E7=A9=BF=E5=BC=B9=E5=B0=84)=EF=BC=9B=5Fresolve=5Fenem?= =?UTF-8?q?y=5Fskill=E2=86=92=5Fenemy=5Froll(=E6=8E=B7=E7=82=B9/=E6=8A=98?= =?UTF-8?q?=E6=89=A3/=E5=85=8B=E5=88=B6/=E6=B1=A0=E4=B9=98/=E4=B8=8B?= =?UTF-8?q?=E9=99=90)+=5Fenemy=5Fshield=5Fapply(damage=5Ftaken=20=E6=96=BD?= =?UTF-8?q?=E5=8A=A0=E9=9D=A2=20{total,shield}=20=E7=A9=BF=E7=BA=BF?= =?UTF-8?q?=EF=BC=8C=E5=8F=91=E5=B0=84=E4=BD=8D=E7=95=99=E4=B8=BB=E5=87=BD?= =?UTF-8?q?=E6=95=B0=E4=BF=9D=E5=87=8F=E4=BC=A4=E8=90=BD=E5=BF=AB=E7=85=A7?= =?UTF-8?q?=E5=85=88=E5=90=8E=E5=BA=8F)=E3=80=82=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E7=82=B9=2030=20=E5=A4=84=E5=85=A8=E6=94=B9=20spec=20=E5=BD=A2?= =?UTF-8?q?=EF=BC=88core=2014=EF=BC=9Dreward=5Fgrant=203+grant=5Fsegment?= =?UTF-8?q?=207+combat=5Fsegment=203+=E6=8E=A7=E5=88=B6=E5=99=A8=E5=86=85?= =?UTF-8?q?=E9=83=A8=20overflow=201=EF=BC=9Bdamage=5Fcontroller=5Ftest=201?= =?UTF-8?q?5=EF=BC=9Bsystem=5Fcontracts=5Ftest=201=EF=BC=89=EF=BC=8Binject?= =?UTF-8?q?/fx=5Fcalls=20=E6=B3=A8=E5=85=A5=E7=82=B9=E6=94=B9=E9=94=AE?= =?UTF-8?q?=EF=BC=88combat=5Fsegment=20:34/:37/:62=E3=80=81begin=5Fbattle?= =?UTF-8?q?=20:341-347=EF=BC=89=E3=80=82=E9=AA=8C=E6=94=B6=EF=BC=9Agdstyle?= =?UTF-8?q?=20=E5=9F=BA=E7=BA=BF=E5=A4=96=200=20NEW=EF=BC=88damage=5Fcontr?= =?UTF-8?q?oller=209=20=E6=9D=A1=E5=AD=98=E9=87=8F=E9=94=AE=E5=85=A8?= =?UTF-8?q?=E6=B8=85=EF=BC=9Abranches/length/locals=C3=973/class-vars=2019?= =?UTF-8?q?=E2=86=9215/params=2012=E2=86=924=EF=BC=89+=20unit/integration?= =?UTF-8?q?=2062=20=E5=A5=97=E4=BB=B6=201698=20PASS=20=E9=9B=B6=20SCRIPT?= =?UTF-8?q?=20ERROR=EF=BC=88=E3=80=94dc1=E3=80=95damage=5Fcontroller=2044?= =?UTF-8?q?=20=E6=A3=80=E6=9F=A5=E5=85=A8=E7=BB=BF=E5=90=AB=20A/B=20?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F/=E6=AD=BB=E4=BA=A1=E4=BD=9C=E5=BA=9F/require?= =?UTF-8?q?s=20=E9=97=A8/flush=5Fsubmit/AC=20=E5=90=88=E6=B5=81/=E6=B1=A0?= =?UTF-8?q?=E4=B9=98=E5=8C=BA=EF=BC=9B=E3=80=94dc2=E3=80=95system=5Fcontra?= =?UTF-8?q?cts=20=E4=B9=9D=E6=A0=8F=E7=9B=AE=E5=A5=91=E7=BA=A6=E7=BB=BF?= =?UTF-8?q?=EF=BC=9B=E3=80=94dc3=E3=80=95round=5Fflow/seal=5Fpolarity/leve?= =?UTF-8?q?l=5Fshop=20=E5=85=A8=E7=BB=BF=EF=BC=89+=20system=5Ftest=2070=20?= =?UTF-8?q?=E9=A1=B9=200=20=E5=A4=B1=E8=B4=A5=E4=B8=89=E9=97=B8=E8=BF=87?= =?UTF-8?q?=E3=80=94=E6=8B=86=E4=BB=B6=E6=89=B9=20V13=E3=80=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/core/battle_fx_host.gd | 2 +- game/core/battle_orchestrator.gd | 8 +- game/core/damage_controller.gd | 234 +++++++++++------- game/core/reward_grant.gd | 21 +- game/core/settlement/combat_segment.gd | 28 ++- game/core/settlement/grant_segment.gd | 37 +-- .../damage_controller_test.gd | 69 +++--- .../unit/contracts/system_contracts_test.gd | 4 +- 8 files changed, 231 insertions(+), 172 deletions(-) diff --git a/game/core/battle_fx_host.gd b/game/core/battle_fx_host.gd index e9c164c6..2929dcc5 100644 --- a/game/core/battle_fx_host.gd +++ b/game/core/battle_fx_host.gd @@ -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"))}) diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index fc1a1258..c2a77825 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -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 键 diff --git a/game/core/damage_controller.gd b/game/core/damage_controller.gd index cd845b0c..68f51418 100644 --- a/game/core/damage_controller.gd +++ b/game/core/damage_controller.gd @@ -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} # 棋盘克制直伤:必中全额不掷点;目标 = 结账时刻最前存活敌;克制点数进加法核;下限保底 diff --git a/game/core/reward_grant.gd b/game/core/reward_grant.gd index 8a8fd10d..0c55ef54 100644 --- a/game/core/reward_grant.gd +++ b/game/core/reward_grant.gd @@ -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)) diff --git a/game/core/settlement/combat_segment.gd b/game/core/settlement/combat_segment.gd index c1907485..ccb739cd 100644 --- a/game/core/settlement/combat_segment.gd +++ b/game/core/settlement/combat_segment.gd @@ -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) diff --git a/game/core/settlement/grant_segment.gd b/game/core/settlement/grant_segment.gd index 62e761f5..162a1e3e 100644 --- a/game/core/settlement/grant_segment.gd +++ b/game/core/settlement/grant_segment.gd @@ -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) diff --git a/game/tests/unit/battle_settlement/damage_controller_test.gd b/game/tests/unit/battle_settlement/damage_controller_test.gd index cd1e9a4d..af439ce0 100644 --- a/game/tests/unit/battle_settlement/damage_controller_test.gd +++ b/game/tests/unit/battle_settlement/damage_controller_test.gd @@ -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") diff --git a/game/tests/unit/contracts/system_contracts_test.gd b/game/tests/unit/contracts/system_contracts_test.gd index 5634ce3f..aacd48cc 100644 --- a/game/tests/unit/contracts/system_contracts_test.gd +++ b/game/tests/unit/contracts/system_contracts_test.gd @@ -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"]