420 lines
26 KiB
GDScript
420 lines
26 KiB
GDScript
## 奖励授予件(自 battle_orchestrator 收编 grant 族):极性字典路由兑付(grant_routed——含封印溢价
|
||
## 与惊巢反制减半)、三种行应用(flat 四分支/percent/gp)、伤害单据提交面(submit——案C 暂存闸承载)、
|
||
## 清图奖励与身份授予(完美封印第二份)、战斗内 GP 缓冲 escrow、秘宝使用四步编排(use_treasure——
|
||
## 相位闸→校验→受理闸→逐 kind 应用→消耗)、揭示奖励兑付(reveal_redeem——fx_host flush 的兑付回调面)。
|
||
## 生命周期=随编排器(编排器 _init 同建同灭,每战新建)。
|
||
## 跨件单向接线(🔴 契约红线):持 BattleFxHost 引用(treasure_used 发射与 counters_ctx 消费;
|
||
## minesweeper 同源经 fx_host 引用——两引用束同源零行为差);行发射经 emit_cb 回宿主(协议面单点
|
||
## 留宿主);fatal 经 fatal_cb 回宿主 _fatal(battle_result 状态机唯一写点留宿主);attune 积点读数经
|
||
## attune_safe_seen_cb、percent 行池值读经 pool_effective_cb——禁回调宿主私有族、禁转发壳。
|
||
## 随机:零随机零 await——授予期零随机口径(roll 值授予前已定)。
|
||
## att 归因束(全 grant 族共用,键集封闭):source_class:String/cell:Vector2i/ri:int/
|
||
## highlight:String(缺省"")/label_suffix:String(缺省"")/halve_pct:int(缺省 0)。
|
||
## 类变量封闭面(15≤15):bundle 11 项(db/reward_economy/damage/fx_host/action_economy/status_effects/
|
||
## units/emit_cb/fatal_cb/attune_safe_seen_cb/pool_effective_cb,缺一键 assert)+escrow+defer
|
||
## (键集封闭 on/queue——原 _defer_board_damage/_deferred_board 二合一)+treasure_inventory
|
||
## (经宿主 setter 转发注入——库存注入时序在宿主构造之后)+battle_hooks(bind_battle 挂接回调束——
|
||
## use_treasure 充能消耗/真视计数与负向踩雷计数消费;宿主侧账本单点留编排器)。禁实现期增删键。
|
||
class_name RewardGrant
|
||
extends RefCounted
|
||
|
||
var db: ConfigDb
|
||
var reward_economy: RewardEconomy
|
||
var damage: DamageController
|
||
var fx_host: BattleFxHost
|
||
var action_economy: ActionEconomy
|
||
var status_effects: StatusContainer
|
||
var units: UnitContainer
|
||
var emit_cb: Callable # 宿主 _emit_row——行协议面单点(束形改形后随束形)
|
||
var fatal_cb: Callable # 宿主 _fatal——battle_result 唯一写点留宿主
|
||
var attune_safe_seen_cb: Callable # 宿主 attune 读投影——use_treasure 拒绝文案读数(_attune_safe_seen)
|
||
var pool_effective_cb: Callable # 宿主 _pool_effective——percent 行池值读
|
||
var escrow := 0 # 战斗内 GP 缓冲(战斗域中间态——reset_battle 清零;gp_gain/gp_loss 兑付累记;随 gp_dropped 上抛即弃,不入钱包)
|
||
var defer := {"on": false, "queue": []} # 案C 暂存闸(授予段伤害单据只暂存不结账——战斗段怪物还手后按事件序补提交统一结账)
|
||
var treasure_inventory: TreasureItems = null # 经宿主 setter 转发注入(库存注入时序在宿主构造之后)
|
||
var battle_hooks: Dictionary[Variant, Variant] = {} # begin_battle 战斗域回调束(bind_battle 挂接——键集封闭三键:attune_consume/vision_add/negative_blast;宿主转账本三单点)
|
||
|
||
|
||
func _init(bundle: Dictionary) -> void:
|
||
for k in ["db", "reward_economy", "damage", "fx_host", "action_economy", "status_effects", "units",
|
||
"emit_cb", "fatal_cb", "attune_safe_seen_cb", "pool_effective_cb"]:
|
||
assert(bundle.has(k), "[奖励授予] 引用束缺键: %s" % k)
|
||
db = bundle.db
|
||
reward_economy = bundle.reward_economy
|
||
damage = bundle.damage
|
||
fx_host = bundle.fx_host
|
||
action_economy = bundle.action_economy
|
||
status_effects = bundle.status_effects
|
||
units = bundle.units
|
||
emit_cb = bundle.emit_cb
|
||
fatal_cb = bundle.fatal_cb
|
||
attune_safe_seen_cb = bundle.attune_safe_seen_cb
|
||
pool_effective_cb = bundle.pool_effective_cb
|
||
|
||
|
||
# begin_battle 战斗域回调束挂接(编排器传三键 Callable——充能消耗/真视计数/负向踩雷计数;
|
||
# 宿主侧各转 _attune_loadout/_journal/_mines_hits 单点,本件对账本零强引用)
|
||
func bind_battle(hooks: Dictionary) -> void:
|
||
for k in ["attune_consume", "vision_add", "negative_blast"]:
|
||
assert(hooks.has(k), "[奖励授予] 战斗域回调束缺键: %s" % k)
|
||
battle_hooks = hooks.duplicate()
|
||
|
||
|
||
# begin_battle 运行态复位(escrow 清零+案C 闸复位——每战编排器新建本件天然全新,复位为防御性收口)
|
||
func reset_battle() -> void:
|
||
escrow = 0
|
||
defer.on = false
|
||
defer.queue = []
|
||
|
||
|
||
# 揭示奖励兑付(BattleFxHost.flush_pending_reveals 的兑付回调面——授予面收编:
|
||
# snapshot 查询与极性路由同在本件,fx_host 对授予面零强引用)
|
||
func reveal_redeem(cell: Vector2i, ri: int) -> void:
|
||
var snap = reward_economy.query_full_snapshot(cell)
|
||
if snap is Dictionary:
|
||
grant_routed(snap, "positive", {"source_class": "fx_reveal", "cell": cell, "ri": ri, "highlight": "fx_reveal"})
|
||
|
||
|
||
# use_treasure 四步受理(registry use_orchestration 规范序——序即原子:
|
||
# ① 相位 gate(Active——行动经济 phase getter)→ ② validate_use(目标校验读排雷玩法只读面,零消耗)→
|
||
# ②半 treasure_used 受理闸(counter_spend 订阅——消耗成功才受理)→ ③ 逐 kind 应用(顺序钉死:空间先、
|
||
# ap_grant 后——effect_kinds 列表序非运行时语义)→ ④ consume。
|
||
# 拒绝全部发生在 ①②③(④合法输入下无失败路径);使用零 AP 成本(不经 try_spend)。
|
||
# 结算明细无道具行(GDD——效果即时可见:overlay 呈现+AP 条刷新;AP 过账本 grant_ap 走 _reject 面)。
|
||
func use_treasure(handle: int, target: Vector2i) -> Dictionary:
|
||
if treasure_inventory == null:
|
||
return {"ok": false, "reason": "库存未接线"}
|
||
if action_economy.phase != action_economy.PHASE_ACTIVE:
|
||
return {"ok": false, "reason": "非挖掘期(使用窗口=Active)"}
|
||
var board: MinesweeperRules = fx_host.minesweeper # 直引宿主件同源引用(无包装层)
|
||
# 板维度读数(Rules 无 width 代理,读 BoardData 本体)
|
||
var board_b: BoardData = board.current_board()
|
||
var v: Dictionary[Variant, Variant] = treasure_inventory.validate_use(handle, target,
|
||
Vector2(board_b.width, board_b.height),
|
||
func(c: Vector2i): return not board.is_revealed(c))
|
||
if not bool(v.ok):
|
||
return v # ② 拒绝零消耗
|
||
var gate_reject := _charge_gate(v, target)
|
||
if not gate_reject.is_empty():
|
||
return gate_reject # ②半 受理闸拒绝零消耗
|
||
var reject := _apply_use_kinds(v, target)
|
||
if not reject.is_empty():
|
||
return reject # ③ 拒绝零消耗(④ consume 之前 return——reason 可呈现)
|
||
var rec: Dictionary[Variant, Variant] = treasure_inventory.consume(handle)
|
||
# 同调主动件充能消耗(单源化——行标 used 幂等承载,经宿主回调束转 AttuneLoadout.consume_charge)
|
||
if not rec.is_empty() and String(rec.get("source_class", "")) == "attunement" and battle_hooks.has("attune_consume"):
|
||
(battle_hooks["attune_consume"] as Callable).call(String(rec.get("item_id", "")))
|
||
# 真视类工具使用计数(信标网络解锁行为条件:受理成功恰计一次;
|
||
# 经宿主回调束转 GainJournal.record_vision_use,冒险收束聚合入档 save.add_vision_used)
|
||
var kinds: Array[Variant] = v.kinds
|
||
if kinds.has("reveal_tiles") or kinds.has("safe_dig") or kinds.has("mine_reveal"):
|
||
if battle_hooks.has("vision_add"):
|
||
(battle_hooks["vision_add"] as Callable).call() # 受理成功恰计一次(未注入=调试直构不计——空账守卫同语义)
|
||
return {"ok": true, "item_id": v.item_id, "resolved": v.resolved, "record": rec}
|
||
|
||
|
||
# 受理闸(treasure_used 发射+施加:counter_spend 订阅=掘进蓄能积点账本,积点不足拒绝零消耗。
|
||
# 零 item_id 特判;拒绝文案读装配集积点进度——宿主 attune 读投影)
|
||
func _charge_gate(v: Dictionary, target: Vector2i) -> Dictionary:
|
||
var tu_hits := fx_host.fx_emit("treasure_used", {"item_id": String(v.item_id), "target": target})
|
||
for h in tu_hits:
|
||
if String(h.get("kind", "")) == "counter_spend":
|
||
if not EffectEngine.counter_spend(fx_host.counters_ctx, String(h.get("counter_id", "")), int(h.get("value", 1))):
|
||
var vrow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(v.item_id))
|
||
var cn_info: int = int(vrow.get("charge_n", 0)) # 读受理行自身
|
||
var seen := int(attune_safe_seen_cb.call())
|
||
return {"ok": false, "reason": "%s:安全格积点未达(%d/%d)" % [String(vrow.get("display_name", "")), seen % cn_info if cn_info > 0 else 0, cn_info]}
|
||
return {}
|
||
|
||
|
||
# ③ 逐 kind 应用(顺序钉死:空间先、ap_grant 后;拒收字典/空字典=受理——④ consume 前零消耗分支)
|
||
func _apply_use_kinds(v: Dictionary, _target: Vector2i) -> Dictionary:
|
||
var kinds: Array[Variant] = v.kinds
|
||
if kinds.has("mine_reveal") or kinds.has("reveal_tiles"):
|
||
# 真视写入(reveal_tiles 这类真视 kind 与 mine_reveal 同叠加层通道:
|
||
# 危险格显示=信息呈现零回写,棋盘事实不可变契约)
|
||
treasure_inventory.apply_overlay("mine_reveal", v.resolved)
|
||
if kinds.has("reward_preview"):
|
||
treasure_inventory.apply_overlay("reward_preview", v.resolved)
|
||
if kinds.has("ap_grant"):
|
||
if not action_economy.grant_ap(int(v.magnitude)):
|
||
return {"ok": false, "reason": "行动点加量被拒(相位防御)"}
|
||
if kinds.has("safe_dig"):
|
||
var dig_reject := _safe_dig_open(v)
|
||
if not dig_reject.is_empty():
|
||
return dig_reject
|
||
if kinds.has("cell_removal"):
|
||
var blast_reject := _cell_removal_open(v)
|
||
if not blast_reject.is_empty():
|
||
return blast_reject
|
||
return {} # 空字典=受理
|
||
|
||
|
||
# 天眼安全格直开(C5 全额兑付:fx_reveal 通道复用=免 AP 零相位、
|
||
# 连锁与格面资源与普通挖格同链;危险格仅真视显示不移除=棋盘事实契约;有封印格跳过——封印=玩家承诺不代开;
|
||
# 拒收字典/空字典=有开格)
|
||
func _safe_dig_open(v: Dictionary) -> Dictionary:
|
||
var opened_any := false
|
||
for c_v in v.resolved:
|
||
var c := Vector2i(c_v)
|
||
if fx_host.minesweeper.has_seal(c):
|
||
continue
|
||
if fx_host.minesweeper.fx_reveal(c):
|
||
opened_any = true
|
||
if not opened_any:
|
||
return {"ok": false, "reason": "天眼:窗口内安全格均已开(无可开格)"}
|
||
return {}
|
||
|
||
|
||
# 爆破工兵包兑付(中立移除危险格:未揭示/无封印/是雷过滤归排雷玩法;
|
||
# 邻接数字冻结零重算、清图分母零涉(危险格不入分母);空集拒绝零消耗——
|
||
# consume 之前 return(拒绝面注记扩「③内零消耗分支」),拒绝 reason 可呈现)
|
||
func _cell_removal_open(v: Dictionary) -> Dictionary:
|
||
var blasted: Array[Variant] = fx_host.minesweeper.remove_mines_in(v.resolved)
|
||
if blasted.is_empty():
|
||
return {"ok": false, "reason": "爆破工兵包:3×3 区域内无可移除危险格(须未揭示且未封印)"}
|
||
return {}
|
||
|
||
|
||
# 极性路由兑付(「动作 × 格型」矩阵的执行面:snap = 缓冲期冻结身份快照〔cell_type = 该格装板期
|
||
# 摇定的类型身份〕,polarity 由动作决定(开安全格/封印命中 = positive,开危险格/标错封印 = negative),
|
||
# 兑付行 = 奖励字典(类型, 极性)行。返回 true = percent 行实际生效(flat 行恒 false——兑付结果由行协议承载)。
|
||
# treasure_item 型零投放(无 aux 行入池、CURIO_KINDS 词表无此型),本口不处理;剩余类型走极性字典路由。
|
||
func grant_routed(snap: Dictionary, polarity: String, att: Dictionary) -> bool:
|
||
var source_class := String(att.get("source_class", ""))
|
||
var cell: Vector2i = att.get("cell", Vector2i(-1, -1))
|
||
var ri := int(att.get("ri", 0))
|
||
var highlight := String(att.get("highlight", ""))
|
||
var label_suffix := String(att.get("label_suffix", ""))
|
||
var halve_pct := int(att.get("halve_pct", 0))
|
||
var row: Dictionary[Variant, Variant] = reward_economy.polarity_row(String(snap.get("cell_type", "")), polarity)
|
||
if row.is_empty():
|
||
fatal_cb.call("极性字典缺行: %s %s" % [String(snap.get("cell_type", "")), polarity])
|
||
return false
|
||
if polarity == "positive":
|
||
row["power"] = int(snap.get("power", int(row.power))) # 区间行快照 power(身份批 roll 值)覆盖表值——授予期零随机口径
|
||
# 标中兑付溢价(条款3 条件二封印溢价形态 a):
|
||
# δ 读 settlement.seal_hit_premium(percent 域 125=×1.25 provisional);100=无溢价直通零扰动。
|
||
# 兑付公式一处=本块:只乘封印命中正向(flat power 乘后入单据/治疗/排队;percent 幅度经
|
||
# premium_pct 贯穿 _apply_percent_row 应用与呈现同乘)——标空/挖格/清图不乘,雷安同池冻结不动。
|
||
# 取整=四舍五入(与伤害管线 round_direction=floor 分域——registry clause3_inequality 登记;
|
||
# 1 点量纲行 ac_up/action 取整后不变=整数粒度地板)。
|
||
if source_class == "seal_hit" and int(db.combat.settlement.core.seal_hit_premium) != 100:
|
||
var prem_pct: int = int(db.combat.settlement.core.seal_hit_premium)
|
||
row = row.duplicate()
|
||
row["power"] = int(round(int(row.power) * prem_pct / 100.0))
|
||
row["premium_pct"] = prem_pct
|
||
# 惊巢反制(减半系数=mine_hit damage_mult 订阅命中〔行 value 50=减 50%,取整 ceil〕;
|
||
# duplicate 隔离——减半不回写极性字典)
|
||
if halve_pct > 0 and polarity == "negative" and source_class == "mine_hit":
|
||
row = row.duplicate()
|
||
var half: int = int(ceil(float(int(row.power)) * float(100 - halve_pct) / 100.0))
|
||
emit_cb.call({"type": "effect", "source_class": source_class, "origin_cell": cell}, ri, "惊巢反制——危险格挖掘惩罚减半(%d→%d)" % [int(row.power), half], 0, {"effect_id": "nest_counter"})
|
||
row.power = half
|
||
return _dispatch_row(row, att)
|
||
|
||
|
||
# 兑付形态分派:effect_id 非空 = percent 行(进增益状态容器);空 = flat 行(当场兑付)
|
||
func _dispatch_row(row: Dictionary, att: Dictionary) -> bool:
|
||
var eff := String(row.get("effect_id", ""))
|
||
if eff != "":
|
||
return _apply_percent_row(row, att)
|
||
_apply_flat_row(row, att, false)
|
||
return false
|
||
|
||
|
||
# percent 行兑付:GP 型行走战斗内缓冲记账;其余 apply_effect 入容器;
|
||
# 满层拒收 = "奖励未生效"行(零静默)
|
||
func _apply_percent_row(row: Dictionary, att: Dictionary) -> bool:
|
||
var eff := String(row.effect_id)
|
||
if eff == "gp_gain" or eff == "gp_loss":
|
||
return _apply_gp_row(row, att)
|
||
var srow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(eff)
|
||
# 封印溢价贯穿(仅封印命中正向行携带 premium_pct;其余授予路径缺省 100 零扰动):
|
||
# 应用幅度与呈现行值同乘,容器经 magnitude_override 拿同一快照(抗表重载口径保持)。
|
||
var prem_pct: int = int(row.get("premium_pct", 100))
|
||
var eff_mag: int = int(srow.magnitude)
|
||
if prem_pct != 100:
|
||
eff_mag = int(round(eff_mag * prem_pct / 100.0))
|
||
var ok: bool = status_effects.apply_effect(eff, _grant_src(att, prem_pct, eff_mag))
|
||
if ok:
|
||
emit_cb.call({"type": "effect", "source_class": String(att.get("source_class", "")), "origin_cell": att.get("cell", Vector2i(-1, -1))},
|
||
int(att.get("ri", 0)), String(srow.display_name) + String(att.get("label_suffix", "")), eff_mag,
|
||
{"effect_id": eff, "pool_value": int(pool_effective_cb.call(eff))})
|
||
return true
|
||
emit_cb.call({"type": "reward_ineffective", "source_class": String(att.get("source_class", "")), "origin_cell": att.get("cell", Vector2i(-1, -1)), "highlight": String(att.get("highlight", ""))},
|
||
int(att.get("ri", 0)), "奖励未生效(满层)%s" % String(att.get("label_suffix", "")), 0,
|
||
{"batch_key": eff})
|
||
return false
|
||
|
||
|
||
# percent 行施加源束(source_class/origin_cell/round_index 三键;溢价行补 magnitude_override——
|
||
# 容器拿同一快照,与呈现行值同乘口径一致)
|
||
func _grant_src(att: Dictionary, prem_pct: int, eff_mag: int) -> Dictionary:
|
||
var src := {"source_class": String(att.get("source_class", "")), "origin_cell": att.get("cell", Vector2i(-1, -1)), "round_index": int(att.get("ri", 0))}
|
||
if prem_pct != 100:
|
||
src["magnitude_override"] = eff_mag
|
||
return src
|
||
|
||
|
||
# GP 型行兑付:战斗内缓冲 escrow 记账——不入钱包不入增益容器。
|
||
# 正向(gp_gain)= 挖开GP安全格/封印命中;负向(gp_loss)= 挖危险格/标错封印。兑付行经既有 effect 行
|
||
# 通道外发(escrow 随行——归因可读),战斗结束随 gp_dropped 一次上抛后即弃。
|
||
func _apply_gp_row(row: Dictionary, att: Dictionary) -> bool:
|
||
var eff := String(row.effect_id)
|
||
var delta: int = int(row.power) if eff == "gp_gain" else -int(row.power)
|
||
escrow += delta
|
||
emit_cb.call({"type": "effect", "source_class": String(att.get("source_class", "")), "origin_cell": att.get("cell", Vector2i(-1, -1)), "highlight": String(att.get("highlight", ""))},
|
||
int(att.get("ri", 0)), "%s %+d%s" % [String(row.display_name), delta, String(att.get("label_suffix", ""))], delta,
|
||
{"effect_id": eff, "escrow": escrow})
|
||
return true
|
||
|
||
|
||
# flat 行兑付分派(负向恒即时,无库存语义):伤害型分支走开单据(
|
||
# 管线/下限/目标解析归 伤害控制器,本件只保留非伤害分支与单据装配):
|
||
# action = 行动点排队(非伤害);blast 正向 = 群伤单据(无类型恒定表值打全体存活敌;负向 = 警觉 percent 行走 _apply_percent_row);
|
||
# heal 正向 = 主角回复 clamp(满血 → 奖励未生效行);heal 负向 = 主角直伤单据(不经增益状态);
|
||
# pierce/magic 正向 = 单目标克制直伤单据(attack_type 查克制矩阵,必中全额不掷点);负向 = 主角直伤单据。
|
||
# 返回 "redeemed" = 实际有效兑付 / "zero" = 零兑付(行已自记)——读数 = 控制器结账 outcome(A 模式提交即结)。
|
||
# sync = 同步开单(清图批——流程依赖完美第二份/高光份数读数,无视结账政策当场结账)。
|
||
func _apply_flat_row(row: Dictionary, att: Dictionary, sync := false) -> String:
|
||
var ctype := String(row.cell_type)
|
||
var display := String(row.display_name) + String(att.get("label_suffix", ""))
|
||
if ctype == "action":
|
||
return _flat_action(row, display, att)
|
||
if ctype == "blast":
|
||
return _flat_blast(row, display, att, sync)
|
||
if ctype == "pierce" or ctype == "magic":
|
||
return _flat_typed(row, display, att, sync)
|
||
if ctype == "heal":
|
||
return _flat_heal(row, display, att, sync)
|
||
fatal_cb.call("flat 行未知类型: %s" % ctype)
|
||
return "zero"
|
||
|
||
|
||
# 行动点排队行(非伤害——下轮行动点带极性入队)
|
||
func _flat_action(row: Dictionary, display: String, att: Dictionary) -> String:
|
||
var power := int(row.power)
|
||
var delta: int = power if String(row.get("polarity", "positive")) == "positive" else -power
|
||
action_economy.queue_ap_delta(delta)
|
||
emit_cb.call({"type": "effect", "source_class": String(att.get("source_class", "")), "origin_cell": att.get("cell", Vector2i(-1, -1)), "highlight": String(att.get("highlight", ""))},
|
||
int(att.get("ri", 0)), "%s(下轮行动点 %+d)" % [display, delta], delta,
|
||
{"effect_id": "ap_delta", "pool_value": action_economy.pending_ap_delta()})
|
||
return "redeemed"
|
||
|
||
|
||
# 群伤单据(无类型恒定表值——打全体存活敌)
|
||
func _flat_blast(row: Dictionary, display: String, att: Dictionary, sync: bool) -> String:
|
||
# 负向 blast(雷)踩雷计数入账(触发条件事实面——正向 spark_aoe/清图奖励不计入;
|
||
# 极性判定在此传点,宿主只记账)
|
||
if String(row.get("polarity", "positive")) == "negative" and battle_hooks.has("negative_blast"):
|
||
(battle_hooks["negative_blast"] as Callable).call()
|
||
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))
|
||
|
||
|
||
# 单目标克制直伤单据(正向 = pierce/magic 必中全额不掷点;负向 = 主角直伤〔复用反噬通道〕)
|
||
func _flat_typed(row: Dictionary, display: String, att: Dictionary, sync: bool) -> String:
|
||
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,
|
||
{"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))
|
||
|
||
|
||
# 主角回复行(正向 = clamp 回复,满血/已倒下 → 奖励未生效行;负向 = 主角直伤单据)
|
||
func _flat_heal(row: Dictionary, display: String, att: Dictionary, sync: bool) -> String:
|
||
var power := int(row.power)
|
||
if String(row.get("polarity", "positive")) == "negative":
|
||
return _submit_flat_hero(display, power, att, sync)
|
||
if int(units.hero_unit().get("hp", 0)) <= 0:
|
||
# 主角已倒下(同轮更早的负向直伤致死)——治疗不再合法作用,按"奖励未生效"留痕
|
||
emit_cb.call({"type": "reward_ineffective", "source_class": String(att.get("source_class", "")), "origin_cell": att.get("cell", Vector2i(-1, -1)), "highlight": String(att.get("highlight", ""))},
|
||
int(att.get("ri", 0)), "%s 未生效(主角已倒下)" % display, 0,
|
||
{"batch_key": String(row.reward_id)})
|
||
return "zero"
|
||
if int(units.hero_unit().hp) >= int(units.hero_unit().max_hp):
|
||
emit_cb.call({"type": "reward_ineffective", "source_class": String(att.get("source_class", "")), "origin_cell": att.get("cell", Vector2i(-1, -1)), "highlight": String(att.get("highlight", ""))},
|
||
int(att.get("ri", 0)), "%s 未生效(满血)" % display, 0,
|
||
{"batch_key": String(row.reward_id)})
|
||
return "zero"
|
||
var healed: int = units.heal(units.hero_unit(), power) # clamp 归 战斗结算
|
||
emit_cb.call({"type": "heal", "source_class": String(att.get("source_class", "")), "origin_cell": att.get("cell", Vector2i(-1, -1)), "highlight": String(att.get("highlight", ""))},
|
||
int(att.get("ri", 0)), "%s 回复" % display, healed, {"target": "hero"})
|
||
return "redeemed"
|
||
|
||
|
||
# 开单分流:sync(清图批)走控制器同步开单——先按序结清段内排队单据、再当场兑现本单;
|
||
# 常规开单按结账时机政策(A 即结 / B 入队)。案C 闸开启期间一律暂存(战斗段怪物还手后按事件序补提交)。
|
||
func submit(t: Dictionary, sync: bool) -> int:
|
||
if bool(defer.on):
|
||
(defer.queue as Array).append(t) # 案C:授予段伤害单据暂存(sync 同闸,清图批一致延后)
|
||
return -1
|
||
return damage.flush_submit(t) if sync else damage.submit(t)
|
||
|
||
|
||
# 案C 结账读数:暂存单(submit 返回 -1)以「将兑付」回报——授予段兑付决策(清图完美第二份等)只能
|
||
# 依据开单时点事实(该时点敌人存活有保证);战斗段末实际结账若目标已灭,控制器自记未生效行。
|
||
func outcome_of(ticket_id: int) -> String:
|
||
return "redeemed" if ticket_id < 0 else damage.outcome(ticket_id)
|
||
|
||
|
||
# 延迟单据入队(迭代 46 卡A T4——成功刷新的崩壁冲击群伤单走案C 同队列:授予段闸外唯一入口,
|
||
# 战斗段末 _board_deferred_settle 按开单序补提交统一结账;类变量零增员——复用 defer.queue)
|
||
func defer_ticket(t: Dictionary) -> void:
|
||
(defer.queue as Array).append(t)
|
||
|
||
|
||
# 主角直伤单据(反噬/治疗包负向——无类型必中,下限保底在控制器管线内)
|
||
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,
|
||
{"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))
|
||
|
||
|
||
# 封印判定批清图奖励(清图奖励对象恰好发一次;完美封印追加第二份同身份兑付——grant_clear 本身恰一次)。
|
||
# 高光行 value = 实际有效兑付份数(禁记名义值;首份零兑付 → 降级不发)。
|
||
# skip_on_refresh/perfect 为宿主态读经参传入(轮内刷新 provenance 与完美封印谓词留宿主)。
|
||
func grant_clear_reward(ri: int, skip_on_refresh: bool, perfect: bool) -> void:
|
||
if skip_on_refresh:
|
||
return # 轮内刷新换来的新图不发清图奖励(行动经济/战斗结算 明文——刷新图不是清图挣来的)
|
||
var snap = reward_economy.grant_clear()
|
||
if not (snap is Dictionary):
|
||
return
|
||
var first_ok := _grant_clear_identity(snap, ri, "(清图奖励)")
|
||
var second_ok := false
|
||
if perfect and first_ok:
|
||
second_ok = _grant_clear_identity(snap, ri, "(完美第二份)")
|
||
if perfect and first_ok:
|
||
emit_cb.call({"type": "highlight", "source_class": "clear_reward", "origin_cell": Vector2i(-1, -1), "highlight": "perfect_sweep"},
|
||
ri, "完美封印!清图奖励 ×2", (2 if second_ok else 1), {})
|
||
|
||
|
||
# 清图奖励 = 整图独立对象(不属于任何一格):兑付形态按其 cell_type 的正向字典形态走,
|
||
# 行值全部取快照(身份批 roll 值)。返回实际有效兑付。
|
||
# 同步开单(sync)——完美第二份与高光份数的流程依赖需要同步读数(开单权归流程);
|
||
# 同步路径内先按序结清段内排队单据(B 模式保序),随后本批无视结账政策当场兑现。
|
||
func _grant_clear_identity(snap: Dictionary, ri: int, suffix: String) -> bool:
|
||
var row := {
|
||
"reward_id": String(snap.get("reward_id", "")),
|
||
"cell_type": String(snap.get("cell_type", "")),
|
||
"polarity": "positive",
|
||
"display_name": String(snap.get("display_name", "")),
|
||
"power": int(snap.get("power", 0)),
|
||
"effect_id": String(snap.get("effect_id", "")),
|
||
"stacks": int(snap.get("stacks", 1)),
|
||
}
|
||
var att := {"source_class": "clear_reward", "cell": Vector2i(-1, -1), "ri": ri,
|
||
"highlight": "clear_reward_h2", "label_suffix": suffix}
|
||
if String(row.effect_id) != "":
|
||
return _apply_percent_row(row, att)
|
||
return _apply_flat_row(row, att, true) == "redeemed"
|