## 奖励授予件(自 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"