- d1 伤害控制器来源敌作废判定 `==`→`is_same`(同内容异实例不再误废;夹具双敌两断言〔wd1〕) - d11 shop 表新增 relic_base_price/spend_discount 两 knob+域检+ConfigDB 访问器+level_runner 读表; level_shop_economy_test 三表值断言+relic 现价读表与迁移前字面量逐位等价〔wd11〕 - d16a HeroTable.display_name+域检+访问器+unit_container 读表;spawn 真实链断言〔wd16〕 - d14 board_generator 目标雷数公式单点 target_mine_count(两调用点改引)+legality 去 db 参数(两调用点同批改)〔wd14〕 - d15 蓄力层上限 CHARGE_LAYER_CAP 常量(两处字面量+注释随动) - d16b unit_container/status_container 六处 Dictionary 取值显式 int() 包裹(读侧包) - d10 minesweeper 两信号按 §2.1 三要素补契约(载荷键集按发射位并集实查)+全库 28 处信号注释载体 #→## 撤销证明:回退 d1 hunk → 同容异实例断言红(A 单据被 B 死亡误废),还原复绿; 回退 d10 契约 → 契约在位扫描 undocumented=2 红,还原归零。等价类申报(d11/d14/d15/d16)见实现报告。 代拍板:#1 target_mine_count 签名(照接口块)、#2 契约措辞与载荷键并集(实查发射位)、#4 int() 读侧包写法。
629 lines
33 KiB
GDScript
629 lines
33 KiB
GDScript
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||
## 伤害控制器:全部伤害的唯一兑付管线——调用点只开单,掷点/折扣/暴击/克制/池乘区/下限/交付全在本类(垂直切片验证:每笔伤害可归因)。
|
||
## 结账时机 policy 双模式:immediate=开单即结 / segment=攒批段界 flush——切换只改 policy 值,管线与表零改动。
|
||
## 掷点只在结账时刻消费 armor_roll 封闭流——segment 模式消费次序=开单次序,同种子同操作逐位复现。
|
||
## 目标在结账时刻按 target_rule 现场解析,开单不冻结对象;死亡由单位自行宣布(unit_died)触发作废/终止。
|
||
class_name DamageController
|
||
extends RefCounted
|
||
|
||
const POLICY_IMMEDIATE := "immediate" # A:开单即结(现状行为——回切位)
|
||
const POLICY_SEGMENT := "segment" # B:攒批到段界 flush 统一结(目标形态)
|
||
|
||
# 兑付配方(kind → 步序):
|
||
# hero_attack 战斗段·主角先手:掷点折扣暴击 → 克制点数(attack_type vs 敌护甲类型)→
|
||
# hero_offense×enemy_defense 池乘区 → 下限 → 交付;行载荷带 roll/ac/tier/killed。
|
||
# enemy_skill 战斗段·敌方伤害技(含普攻):基线 = round(atk×倍率)(开单侧算好入 base_value)→
|
||
# 掷点折扣暴击 → 克制点数(attack_type vs 主角护甲类型——
|
||
# 无类型恒中性 delta 0)→ hero_defense×enemy_output
|
||
# 池乘区 → 下限 → +rider(必中附加)→ 交付 → commit_action;来源敌已死 = 作废。
|
||
# board_typed 棋盘·单目标克制直伤(穿刺弹/魔法矢正向):必中全额,克制点数进加法核,下限保底。
|
||
# board_blast 棋盘·群伤(爆裂粉尘/清障炸药):无类型恒定表值,全体存活敌逐个交付。
|
||
# board_flat_hero 棋盘·主角直伤(反噬/治疗包负向/地形变异):无类型必中,下限保底。
|
||
const KIND_HERO_ATTACK := "hero_attack"
|
||
const KIND_ENEMY_SKILL := "enemy_skill"
|
||
const KIND_BOARD_TYPED := "board_typed"
|
||
const KIND_BOARD_BLAST := "board_blast"
|
||
const KIND_BOARD_FLAT_HERO := "board_flat_hero"
|
||
|
||
const TARGET_FRONT_ALIVE := "front_alive_enemy"
|
||
const TARGET_HERO := "hero"
|
||
const TARGET_ALL_ALIVE := "all_alive_enemies"
|
||
|
||
var db: ConfigDb
|
||
var rng: RngServiceCore # 注入成员类型注(单测经真实 RngService 构造,标注安全)
|
||
var units: UnitContainer # UnitContainer——目标解析/伤害交付/存活查询(句柄即 Dictionary 引用)
|
||
var effects: StatusContainer # StatusContainer——池值/AC 型效果读取(增益状态 只读消费面)
|
||
var emit_row_cb: Callable # 行协议注入口:func(type, source_class, origin_cell, ri, display, value, extra, highlight)
|
||
|
||
var policy := POLICY_SEGMENT # 结账时机开关(B=攒批段界统一结;回切 A 改 POLICY_IMMEDIATE 即开单即结)
|
||
var _queue: Array[Dictionary] = [] # 未结单据(B 模式攒批;A 模式恒空)
|
||
var _outcomes := {} # ticket_id → "redeemed"/"zero"/"voided"(结账后可查——清图高光份数消费)
|
||
var _seq := 0 # 流水号
|
||
var _halted := false # 主角倒下终止封印(死亡事件置位——flush 余单作废)
|
||
var _hero_ac_override := -1 # 主角护甲等级合流入参(begin_battle 显式设定——-1 = 表值兜底,口径同战斗编排器)
|
||
# 效果引擎(attack_commit/clamp_check/armor_roll/kill/damage_taken
|
||
# 发射+施加面: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
|
||
|
||
|
||
func _init(db_ref, rng_ref, units_ref, effects_ref) -> void:
|
||
db = db_ref
|
||
rng = rng_ref
|
||
units = units_ref
|
||
effects = effects_ref
|
||
units.unit_died.connect(_on_unit_died)
|
||
|
||
|
||
# 主角护甲等级合流入参(战斗编排器 begin_battle 调用——encounter.hero_armor_class;缺参/-1 = 表值兜底)
|
||
func set_hero_ac_override(v: int) -> void:
|
||
_hero_ac_override = v
|
||
|
||
|
||
# 效果引擎注入(战斗编排器 _init——事件发射面接线)
|
||
func set_fx(fx: EffectEngine, ctx: EffectContext) -> void:
|
||
_fx = fx
|
||
_ctx = ctx
|
||
|
||
|
||
# 事件发射薄面(发射+命中收集统一入口——订阅空=零循环零扰动;命中由各施加面按组合规则施加)
|
||
func _fx_hits(event: String, payload: Dictionary) -> Array:
|
||
if _fx == null:
|
||
return []
|
||
return _fx.dispatch(_ctx, event, payload, _fx_fact)
|
||
|
||
|
||
# 事件发射薄面(免施加观测位——enemy_hp_threshold 等现无订阅者的事件保留零扰动面)
|
||
# 补充事实面(fact 取值第二优先源——payload 未携带的运行态读数:charge_layers 编排器每轮注入。
|
||
# hp_ratio 补入〔背水 when 面,敌技 damage_taken 载荷不带血比例时回落〕;
|
||
# fx_reveal_request=揭示效果回调〔斩首学者 kill 订阅面——编排器注入,入队授予段末兑付〕)
|
||
func _fx_fact(fact: String):
|
||
match fact:
|
||
"charge_layers":
|
||
return charge_layers
|
||
"dig_streak":
|
||
return 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:
|
||
if name == "":
|
||
return 1
|
||
if payload.has(name):
|
||
return int(payload[name])
|
||
if _fx_fact(name) != null:
|
||
return int(_fx_fact(name))
|
||
return 0
|
||
|
||
|
||
# 巢穴控制数(fact nest_count 取数面——敌方 weaken 实例数,猎巢人订阅面)
|
||
func _nest_count() -> int:
|
||
var n := 0
|
||
for eff in effects.query_active("enemy"):
|
||
if String(eff.effect_id) == "weaken":
|
||
n += 1
|
||
return n
|
||
|
||
|
||
# ---- 开单面(流程调用——编排器六调用点的唯一形态)----
|
||
|
||
# 开单:A 模式当场结账;B 模式入队待 flush。返回流水号(outcome() 查询键)。
|
||
func submit(t: Dictionary) -> int:
|
||
_seq += 1
|
||
t["ticket_id"] = _seq
|
||
_outcomes[_seq] = "pending"
|
||
if policy == POLICY_IMMEDIATE:
|
||
_resolve(t)
|
||
else:
|
||
_queue.append(t)
|
||
return _seq
|
||
|
||
|
||
# 单据构造(缺省填齐——调用点只写差异栏;context = kind 专用:enemy_skill 带 skill_id/baseline,
|
||
# board_flat_hero 的键并入行载荷,board_blast/board_typed 带 batch_key 供未生效行归因)。
|
||
# attack_commit 发射点(我方伤害单据开立入口;
|
||
# 命中挂单据 context 随行——施加面在 _resolve_hero_attack 管线算术前逐族施加,开单时点=主角攻击
|
||
# 单恒为段内首单(结账现场与开单现场一致)。载荷 atk/target/matchup + 挖掘 streak 事实〔连珠矢/
|
||
# 执拗订阅面〕+巢穴控制数〔猎巢人〕)
|
||
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:
|
||
var t := {
|
||
"kind": kind,
|
||
"source_class": source_class,
|
||
"origin_cell": origin_cell,
|
||
"round_index": ri,
|
||
"display": display,
|
||
"channel": channel,
|
||
"attack_type": attack_type,
|
||
"base_value": base_value,
|
||
"target_rule": target_rule,
|
||
"rider": rider,
|
||
"highlight": highlight,
|
||
"context": context,
|
||
"requires": -1, # 前置单据 id(-1 = 无依赖)
|
||
"source_unit": {}, # enemy_skill 专用——来源敌句柄(作废判定 + commit)
|
||
}
|
||
if kind == KIND_HERO_ATTACK:
|
||
var tgt = _target_for(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),
|
||
"target": String(tgt.get("display_name", "")) if tgt != null else "",
|
||
"matchup": matchup_o, "dig_streak": 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
|
||
|
||
|
||
# 同步开单(清图批专用——完美第二份与高光份数的流程依赖需要同步读数,开单权归流程):
|
||
# 先按开单序结清段内排队的单据(保序),再无视结账时机政策当场兑现本单。
|
||
func flush_submit(t: Dictionary) -> int:
|
||
flush()
|
||
_seq += 1
|
||
t["ticket_id"] = _seq
|
||
_outcomes[_seq] = "pending"
|
||
_resolve(t)
|
||
return _seq
|
||
|
||
|
||
# 结账:B 模式按开单序逐张兑现队列(兑现中经 requires 追加的回声单按入队序自然续结);
|
||
# A 模式恒空队 no-op(段界调用点两模式共用)。主角倒下(_halted)后余单全部静默作废。
|
||
func flush() -> void:
|
||
if policy != POLICY_SEGMENT:
|
||
return
|
||
var i := 0
|
||
while i < _queue.size():
|
||
if _halted:
|
||
break
|
||
_resolve(_queue[i])
|
||
i += 1
|
||
for j in range(i, _queue.size()):
|
||
_outcomes[int(_queue[j].ticket_id)] = "voided"
|
||
_queue.clear()
|
||
|
||
|
||
# 结账读数:"redeemed"=实际有效兑付 / "zero"=合法零兑付(未生效行已自记)/ "voided"=作废(静默)/
|
||
# "pending"=未结(B 模式排队中)
|
||
func outcome(ticket_id: int) -> String:
|
||
return String(_outcomes.get(ticket_id, "pending"))
|
||
|
||
|
||
# ---- 死亡事件(单位自行宣布,本类消费)----
|
||
|
||
func _on_unit_died(u: Dictionary) -> void:
|
||
if bool(u.get("is_hero", false)):
|
||
_halted = true # 主角倒下——flush 余单作废;Game Over 归判定段既有 hp 读数路径
|
||
return
|
||
for t in _queue:
|
||
if is_same(t.source_unit, u):
|
||
t["cancelled"] = true # 死亡即不动手——该敌未结单据作废(静默零行零 commit)
|
||
|
||
|
||
# ---- 兑付管线(单点——所有伤害经过同一道门)----
|
||
|
||
func _resolve(t: Dictionary) -> void:
|
||
var id := int(t.ticket_id)
|
||
if _halted or bool(t.get("cancelled", false)) \
|
||
or (int(t.get("requires", -1)) >= 0 and String(_outcomes.get(int(t.requires), "")) != "redeemed"):
|
||
_outcomes[id] = "voided" # 终止后余单 / 来源敌已死 / 前置单未实际有效——静默作废(死亡即不动手口径)
|
||
return
|
||
match String(t.kind):
|
||
KIND_HERO_ATTACK:
|
||
_resolve_hero_attack(t)
|
||
KIND_ENEMY_SKILL:
|
||
_resolve_enemy_skill(t)
|
||
KIND_BOARD_TYPED:
|
||
_resolve_board_typed(t)
|
||
KIND_BOARD_BLAST:
|
||
_resolve_board_blast(t)
|
||
KIND_BOARD_FLAT_HERO:
|
||
_resolve_board_flat_hero(t)
|
||
_:
|
||
push_error("[伤害控制器/fatal] 未知兑付配方: %s" % String(t.kind))
|
||
_outcomes[id] = "zero"
|
||
return
|
||
# 主角倒下检查(每张单兑现后——敌方连拍致死即中止,同现行"hp=0 立即短路"口径)
|
||
if int(units.hero_unit().get("hp", 0)) <= 0:
|
||
_halted = true
|
||
|
||
|
||
# 主角先手攻击:掷点 → 折扣/暴击 → 克制点数 → hero_offense×enemy_defense 池乘区 → 下限 → 交付
|
||
# (attack_commit/clamp_check/armor_roll 订阅施加面:命中=开单入口随单
|
||
# context 携带,按固定算序逐族施加——命中序=数据表声明序,确定性)。
|
||
# 四步分段:命中面(_hero_attack_ctx,目标/护甲/
|
||
# 掷点/按序改写族四拍/克制点数) → 基值装配(_hero_attack_base_value,加法族/计数器施加/倍乘) →
|
||
# 掷点兑现(_hero_attack_redeem,rolled/池乘/下限/rider/落地/击杀发射) → 交付(_hero_attack_deliver,
|
||
# 状态施加/标签/明细行)。步骤间经 ctx/redemption 字典穿线——算序/取值固定(🔴 逐位一致契约)。
|
||
func _resolve_hero_attack(t: Dictionary) -> void:
|
||
var ctx := _hero_attack_ctx(t)
|
||
if ctx.is_empty():
|
||
return # 不可达(流程开单前已有存活守卫)——防御位不发行(outcome 已置 zero)
|
||
var base_value: float = _hero_attack_base_value(t, ctx)
|
||
var redemption := _hero_attack_redeem(t, ctx, base_value)
|
||
_hero_attack_deliver(t, ctx, redemption)
|
||
|
||
|
||
# 命中面(原 :273-328 逐字):目标/护甲克制 matchup/armor_roll 发射/按序改写族四拍(矿工重锤命中
|
||
# 交换→专注免掷→厚积薄发必中→猎弱折扣移档)/克制点数读值+订阅改写。返回穿线 ctx(空=目标不可达)。
|
||
func _hero_attack_ctx(t: Dictionary) -> Dictionary:
|
||
var target = _target_for(String(t.target_rule))
|
||
if target == null:
|
||
_outcomes[int(t.ticket_id)] = "zero" # 不可达(流程开单前已有存活守卫)——防御位不发行
|
||
return {}
|
||
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 roll_read: Dictionary[Variant, Variant] = _armor_roll()
|
||
var threshold: int = defender_ac(target) + int(db.settlement.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:
|
||
trade_hit += int(h.trade_hit)
|
||
if trade_hit > 0:
|
||
threshold = maxi(1, threshold - trade_hit)
|
||
var crit: bool = bool(roll_read.crit)
|
||
var discount: int = _roll_discount(int(roll_read.roll), threshold)
|
||
# 按序改写族第二拍:专注免掷骰恒定折扣(方差买断成期望——稳定器人格;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})
|
||
for h in clamp_hits:
|
||
if String(h.get("kind", "")) != "modify_ticket":
|
||
continue
|
||
if String(h.get("mode", "")) == "discount_shift":
|
||
shift += int(h.get("value", 0))
|
||
if shift > 0 and armor == "light" and discount < 100:
|
||
discount = _discount_shift_up(discount, shift)
|
||
# 克制点数读值+订阅改写(魔网织者/湛蓝誓约/猎手刺剑 clamp_check modify delta——克制矩阵查表值
|
||
# 后的直调;hero_attack 与 board_typed 两消费面同享)
|
||
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}
|
||
|
||
|
||
# 基值装配(原 :329-355 逐字):基值+单位标记加伤+矿工重锤增伤面+加法族(attack_bonus/
|
||
# value_counter 层值)+计数器施加(attack_commit 用后清零行——命中序=声明序)+spike 倍乘。
|
||
func _hero_attack_base_value(t: Dictionary, ctx: Dictionary) -> float:
|
||
var ac_hits: Array[Variant] = ctx["ac_hits"]
|
||
var ac_payload: Dictionary[Variant, Variant] = ctx["ac_payload"]
|
||
var base_value := float(t.base_value)
|
||
# 单位标记加伤(处刑台 target_attack_bonus 挂单位字典 fx_atk_bonus 键;
|
||
# 阈值沿发射点已写入,本处通用消费——对该敌伤害加算)
|
||
base_value += float(int((ctx["target"] as Dictionary).get("fx_atk_bonus", 0)))
|
||
# 按序改写族第五拍:矿工重锤增伤面(交换的正向侧)
|
||
for h in ac_hits:
|
||
if String(h.get("kind", "")) == "modify_ticket" and int(h.get("trade_atk", 0)) > 0:
|
||
base_value *= 1.0 + float(int(h.trade_atk)) / 100.0
|
||
# 加法族:整备军备「每层下轮首段 +1」(层数×value 加算)——attack_bonus per charge_layers。
|
||
# value_counter 扩展〔疼痛记忆/弃图兑换/陷阱本能——rider 值=计数器账面,
|
||
# 值读于施加时点=命中序先行 rider 后清零行〕
|
||
for h in ac_hits:
|
||
if String(h.get("kind", "")) == "attack_bonus":
|
||
var contrib := int(h.get("value", 0)) * _per_value(String(h.get("per", "")), ac_payload)
|
||
if h.has("value_counter") and String(h.get("value_counter", "")) != "":
|
||
contrib = EffectEngine.counter(_ctx, String(h.get("value_counter", "")))
|
||
base_value += float(contrib)
|
||
# 计数器施加(attack_commit 命中行内 counter_add〔用后清零行〕,命中序=声明序)
|
||
for h in ac_hits:
|
||
if String(h.get("kind", "")) == "counter_add":
|
||
var set_v = h.get("set", null)
|
||
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)
|
||
# 任意真族:厚积薄发满层 spike 倍乘(≥3 层——强制必中已在折扣拍承载)
|
||
if ctx["spike_ready"]:
|
||
for h in ac_hits:
|
||
if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "spike":
|
||
base_value *= float(int(h.get("value", 1)))
|
||
return base_value
|
||
|
||
|
||
# 掷点兑现(原 :356-402 逐字):rolled 分量/payload 归因/池乘区/下限/英雄侧 rider 族/落地扣血/
|
||
# 击杀标记/kill 发射+贯穿弹射。返回 {landed, payload, hero_rider}(交付步消费)。
|
||
func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> Dictionary:
|
||
var target = ctx["target"]
|
||
var ac_hits: Array[Variant] = ctx["ac_hits"]
|
||
var roll_read: Dictionary[Variant, Variant] = ctx["roll_read"]
|
||
var atk_type: String = ctx["atk_type"]
|
||
var rolled: int = DamageMath.rolled_component(base_value, int(ctx["discount"]), bool(ctx["crit"]), int(db.settlement.crit_multiplier), int(ctx["cdelta"]))
|
||
var payload: Dictionary[Variant, Variant] = {"target": String(target.display_name)}
|
||
payload[atk_type] = rolled # 分量键 = 实际攻击类型(归因字段,单分量模型)
|
||
payload["roll"] = int(roll_read.roll)
|
||
payload["ac"] = int(ctx["threshold"])
|
||
payload["tier"] = int(ctx["discount"])
|
||
if bool(ctx["crit"]):
|
||
payload["crit"] = true
|
||
var pooled: int = DamageMath.pool_damage(rolled, clamped_pool("hero_offense"), clamped_pool("enemy_defense"))
|
||
var landed: int = maxi(int(db.settlement.damage_floor), pooled)
|
||
# 加法族:英雄侧 rider(enemy_skill rider 对称位补齐口径)——后置必中附加,独立于掷点与乘区
|
||
# (奥术余烬「普攻命中后恒定结算」语义);猎巢人巢控光环(per nest_count)/矿工的执拗开格加成
|
||
# (per dig_streak、cap 封顶)同通道合计;档一质变 rider+1 / 档二(crit 时)rider 乘暴击倍率。
|
||
# rider 不改变下限语义(landed 含 rider 后不再过 floor)。
|
||
var hero_rider := 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"])
|
||
if h.has("cap"):
|
||
contrib = mini(int(h.cap), contrib)
|
||
hero_rider += contrib
|
||
if hero_rider > 0:
|
||
if bool(ctx["crit"]) and rider_tier_crit:
|
||
hero_rider = int(round(float(hero_rider) * float(db.settlement.crit_multiplier)))
|
||
landed += hero_rider
|
||
payload["hero_rider"] = hero_rider
|
||
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), "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_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)
|
||
return {"landed": landed, "payload": payload, "hero_rider": hero_rider}
|
||
|
||
|
||
# 交付(原 :403-414 逐字):即时执行族(碎甲震击护甲裂解)/标签装配/明细行发射/兑现登记。
|
||
func _hero_attack_deliver(t: Dictionary, ctx: Dictionary, redemption: Dictionary) -> void:
|
||
var payload: Dictionary[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 泛用通道,不挂件名)
|
||
effects.apply_effect(String(h.get("status_id", "")), {"source_class": "fx_apply_status"}, -1)
|
||
payload["cracked"] = true
|
||
var label := _attack_label(String(t.display), bool(ctx["crit"]), int(ctx["discount"]))
|
||
if int(redemption["hero_rider"]) > 0:
|
||
label += "+奥术%d(必中)" % int(redemption["hero_rider"])
|
||
emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), label, int(redemption["landed"]), payload, String(t.highlight))
|
||
_outcomes[int(t.ticket_id)] = "redeemed"
|
||
|
||
|
||
# 敌方伤害技:掷点 → 折扣/暴击 → 克制点数(attack_type vs 主角护甲类型——
|
||
# 无类型恒中性 delta 0,写法同 hero_attack 的 _counter_delta 消费位)→ hero_defense×enemy_output
|
||
# 池乘区 → 下限 → +rider(后置必中附加)→ 交付 → commit_action(作废单不 commit——死亡即不动手)
|
||
func _resolve_enemy_skill(t: Dictionary) -> void:
|
||
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.attack_bonus)
|
||
var crit: bool = bool(roll_read.crit)
|
||
var discount: int = _roll_discount(int(roll_read.roll), threshold)
|
||
var cdelta: int = _counter_delta(String(t.attack_type), String(hero.get("armor_type", "light")))
|
||
var rolled: int = DamageMath.rolled_component(float(t.base_value), discount, crit, int(db.settlement.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_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 补充事实面回落提供)
|
||
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)}):
|
||
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 消费面)
|
||
"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_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("damage", String(t.source_class), t.origin_cell, int(t.round_index), label, total, payload, String(t.highlight))
|
||
units.commit_action(t.source_unit)
|
||
_outcomes[int(t.ticket_id)] = "redeemed"
|
||
|
||
|
||
# 棋盘克制直伤:必中全额不掷点;目标 = 结账时刻最前存活敌;克制点数进加法核;下限保底
|
||
func _resolve_board_typed(t: Dictionary) -> void:
|
||
var target = units.default_target()
|
||
if target == null:
|
||
_ineffective_row(t, "无存活敌人")
|
||
_outcomes[int(t.ticket_id)] = "zero"
|
||
return
|
||
var armor_b: String = String(target.get("armor_type", "light"))
|
||
var atk_b: String = String(t.attack_type)
|
||
# clamp_check 发射+施加(克制判定修正点棋盘面;与 hero_attack 同表同点:
|
||
# modify delta 订阅改写合计进克制点数;discount_shift 档位面属主角攻击专享,本面不施加)
|
||
var cdelta: int = _counter_delta(atk_b, armor_b)
|
||
if atk_b != "":
|
||
for h in _fx_hits("clamp_check", {"matchup": atk_b + "_vs_" + armor_b}):
|
||
# 无模式 delta 才入棋盘窗(mode 路由:clamp_bonus=主角攻击面专享、discount_shift=折扣档面)
|
||
if String(h.get("kind", "")) == "modify_ticket" and h.has("delta") and String(h.get("mode", "")) == "":
|
||
cdelta += int(h.delta)
|
||
var dealt: int = DamageMath.counter_component(int(t.base_value), cdelta, int(db.settlement.damage_floor))
|
||
units.apply_damage(target, dealt)
|
||
emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), "%s → %s" % [String(t.display), String(target.display_name)], dealt,
|
||
{"target": String(target.display_name), "attack_type": String(t.attack_type), "effectiveness": cdelta}, String(t.highlight))
|
||
_outcomes[int(t.ticket_id)] = "redeemed"
|
||
|
||
|
||
# 棋盘群伤:无类型恒定表值,结账时刻全体存活敌逐个交付(单行汇总——行内 count = 兑现时存活数)
|
||
func _resolve_board_blast(t: Dictionary) -> void:
|
||
var alive: Array[Variant] = units.query_alive("enemy")
|
||
if alive.is_empty():
|
||
_ineffective_row(t, "无存活敌人")
|
||
_outcomes[int(t.ticket_id)] = "zero"
|
||
return
|
||
var blast_dmg: int = maxi(int(db.settlement.damage_floor), int(t.base_value)) # 伤害下限全来源
|
||
for u in alive:
|
||
units.apply_damage(u, blast_dmg) # 恒定表值(下限保底),逐个;受击即死——死亡即不动手
|
||
emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), "%s → 全体敌人 ×%d" % [String(t.display), alive.size()], blast_dmg,
|
||
{"target": "all_enemies", "count": alive.size()}, String(t.highlight))
|
||
_outcomes[int(t.ticket_id)] = "redeemed"
|
||
|
||
|
||
# 棋盘主角直伤(反噬/治疗包负向/地形变异):无类型必中,下限保底;context 键并入行载荷
|
||
func _resolve_board_flat_hero(t: Dictionary) -> void:
|
||
var dealt: int = maxi(int(db.settlement.damage_floor), int(t.base_value)) # 伤害下限全来源
|
||
units.apply_damage(units.hero_unit(), dealt)
|
||
var extra: Dictionary[Variant, Variant] = {"target": "hero"}
|
||
for k in t.context.keys():
|
||
extra[k] = t.context[k]
|
||
emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), "%s → 主角" % String(t.display), dealt, extra, String(t.highlight))
|
||
_outcomes[int(t.ticket_id)] = "redeemed"
|
||
|
||
|
||
# ---- 管线算术读数(自战斗编排器迁入——管线单点;编排器留 _clamped_pool/_defender_ac 薄委托)----
|
||
|
||
# 护甲等级掷点(armor_roll 封闭流——逐次掷逐次取号、逐位可回放):返回 {roll, crit}
|
||
func _armor_roll() -> Dictionary:
|
||
var stream: RandomNumberGenerator = rng.open_stream(String(db.settlement.armor_roll_stream))
|
||
var roll: int = stream.randi_range(1, int(db.settlement.roll_faces))
|
||
return {"roll": roll, "crit": roll == int(db.settlement.crit_roll)}
|
||
|
||
|
||
# 掷点折扣读数:nat 最大面恒全额(暴击分支前置),其余查档(缺口 = 阈值 − 掷点)
|
||
func _roll_discount(roll: int, threshold: int) -> int:
|
||
if roll == int(db.settlement.crit_roll):
|
||
return 100
|
||
return DamageMath.roll_discount_percent(roll, threshold, db.settlement.roll_tier_bounds, db.settlement.roll_tier_discounts)
|
||
|
||
|
||
# 防守方护甲等级读数(数值轴——与 armor_type 克制属性轴严格分离):敌人 = spawn 期行值快照;
|
||
# 主角 = begin_battle 显式入参(encounter.hero_armor_class 合流值,缺参/-1 = 表值兜底);
|
||
# ac 型状态效果叠其上(ac_up 层数×幅度——不入任何池,无 clamp)
|
||
func defender_ac(unit: Dictionary) -> int:
|
||
if bool(unit.get("is_hero", false)):
|
||
var total: int = _hero_ac_override
|
||
if total < 0:
|
||
total = int(db.hero.armor_class)
|
||
for eff in effects.query_active("hero"):
|
||
var row: Dictionary[Variant, Variant] = db.status_effect_row(eff.effect_id)
|
||
if not row.is_empty() and String(row.unit) == "ac":
|
||
total += int(eff.effective_value)
|
||
return total
|
||
var total_e: int = int(unit.get("armor_class", 0))
|
||
for eff_e in effects.query_active("enemy"):
|
||
var erow_ac: Dictionary[Variant, Variant] = db.status_effect_row(eff_e.effect_id)
|
||
if not erow_ac.is_empty() and String(erow_ac.unit) == "ac":
|
||
total_e += int(eff_e.effective_value)
|
||
return total_e
|
||
|
||
|
||
# 克制修正点数读值(SettlementTable.armor_effectiveness:{攻击类型: {护甲: 点数}}——缺项回退 0 中性;
|
||
# attack_type "" = 无类型恒不查(群伤/反噬/地形——敌方技随单轴查表))
|
||
func _counter_delta(attack_type: String, armor: String) -> int:
|
||
if attack_type == "":
|
||
return 0
|
||
var tbl: Dictionary[Variant, Variant] = db.settlement.armor_effectiveness
|
||
var row: Dictionary[Variant, Variant] = tbl.get(attack_type, {})
|
||
return int(row.get(armor, 0))
|
||
|
||
|
||
# 折扣档改善(猎弱本能——折扣表向全额方向移 value 档;查不到当前档 = 不动防御口径)
|
||
func _discount_shift_up(discount: int, shift: int) -> int:
|
||
var tbl: Array[Variant] = db.settlement.roll_tier_discounts
|
||
var idx := tbl.find(discount)
|
||
if idx < 0:
|
||
return discount
|
||
return int(tbl[mini(tbl.size() - 1, idx + shift)])
|
||
|
||
|
||
# 池内加算 → clamp [-100,+100](池间乘算归 DamageMath.pool_damage 调用侧)
|
||
func clamped_pool(pool: String) -> int:
|
||
var total := 0
|
||
for eff in effects.query_active("hero") + effects.query_active("enemy"):
|
||
var row: Dictionary[Variant, Variant] = db.status_effect_row(eff.effect_id)
|
||
if row.is_empty():
|
||
continue
|
||
if String(db.settlement.effect_pool_map.get(eff.effect_id, "")) == pool and String(row.unit) == "percent":
|
||
total += eff.effective_value # 池内加算(L×m 原样——越域不 clamp 由本处收口)
|
||
return clampi(total, int(db.settlement.pool_clamp_lo), int(db.settlement.pool_clamp_hi))
|
||
|
||
|
||
# 攻击行显示名(掷点档位随行文本可读;重档折扣 = "被压制")
|
||
func _attack_label(base: String, crit: bool, discount: int) -> String:
|
||
if crit:
|
||
return base + "(暴击)"
|
||
if discount < 100:
|
||
return base + "(被压制)"
|
||
return base
|
||
|
||
|
||
# ---- 内部 ----
|
||
|
||
# 目标解析(结账时刻照规则看现场;句柄即 Dictionary 引用)
|
||
func _target_for(rule: String):
|
||
match rule:
|
||
TARGET_FRONT_ALIVE:
|
||
return units.default_target()
|
||
TARGET_HERO:
|
||
return units.hero_unit()
|
||
TARGET_ALL_ALIVE:
|
||
return units.query_alive("enemy")
|
||
push_error("[伤害控制器/fatal] 未知目标规则: %s" % rule)
|
||
return null
|
||
|
||
|
||
func _ineffective_row(t: Dictionary, reason: String) -> void:
|
||
emit_row_cb.call("reward_ineffective", String(t.source_class), t.origin_cell, int(t.round_index),
|
||
"%s 未生效(%s)" % [String(t.display), reason], 0, {"batch_key": String(t.context.get("batch_key", ""))}, String(t.highlight))
|