Files
gplan-2/game/core/reward_grant.gd
T
DKChan b8b69c683d 合并 master(迭代39 拆件批+文档批) 进 iter40+主角账模型迁移适配——冲突两件按「保留本分支拆件结构+承接 master 语义」解:①battle_orchestrator 承 RunLedger 整类退役=新增 _journal()/_attune_loadout() 两读面(enc 派生、无成员接收)、_reseed_attune_charges 循环体换 AttuneLoadout.reset_cycle(写时种行/补行单源化)、冒险桶挂接改 enc["adv_bucket"](经 _fx_host.mount_buckets 下传)、战末 bonus 汇入换 GainJournal.gain_bonus(axis,value,"battle_export",axis);同时接回 master 的 B4 战斗指标面=_mines_hits 三读数账+开战清零+轮末转存、_roll_round_actions facts 组包四键(round_index/last_round_end/mines_hit_last/mines_hit_total)并传 check_trigger_unlocks/rollable_skills/special_skills 三参、enemy_hp_threshold 读数改 trigger.value。②RewardGrant 的 battle_ledger 引用改 battle_hooks 回调束三键(attune_consume/vision_add/negative_blast,bind_battle 缺键 assert)——授予件对账本零强引用、宿主侧单点留编排器,类变量封闭面 15 不减不增;use_treasure 充能消耗与真视计数改经回调,负向 blast 踩雷计数在 _flat_blast 判极点后回调宿主记账(等价承 master 的 _book_negative_blast 语义)。③treasure_effects_test 计数链断言取 master 口径(lg13.vision_used()==1)+本分支真实入口(reward_grant().use_treasure)。④trigger_cond_test V6 的两处 flat 行调用改指 reward_grant()._apply_flat_row(att 束形)——它原调编排器已迁函数触发 Invalid call,_init 在 quit 之前中断=进程挂死 124 超时(本轮排查实证:同件在 master rc=0 秒退,合并后挂死;修后 rc=0 全绿)。验收:run_all 65 套件 64 绿 1 红=已登记存量红(02 EV 锚 goblin 0.9148∉[1.00,1.20],迭代39 特批带红在册,重锚归后续数值批),零 SCRIPT ERROR;负向套件 run_time_fatal_test exit=1 三点绿;variant_placement 49 checks 绿(wr10 三条在位);gdstyle 待合并后复测〔结构小件批 V16〕
2026-10-27 22:25:53 +08:00

414 lines
26 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 奖励授予件(自 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.treasure_item_row(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.settlement.core.seal_hit_premium) != 100:
var prem_pct: int = int(db.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.status_effect_row(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)
# 主角直伤单据(反噬/治疗包负向——无类型必中,下限保底在控制器管线内)
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"