Files
gplan-2/game/core/settlement/grant_segment.gd
T
2026-11-04 05:08:31 +08:00

462 lines
28 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.
class_name GrantSegment
extends RefCounted
# ① 授予段 Segment(P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。
# 职责:结算队列(开格/封印判定/清图)→ 极性路由逐批兑付 + 封印判/军规开单 + round_end 施加 +
# 转存收口 + 揭示兑付 + 段界结账。行发射经 emit_row_cb 回宿主——兑付管线归 RewardGrant、
# 效果施加归 BattleFxHost(段持注入引用直调,零转发壳)——
# 本段只搬运时序,零算法改动(🔴 零行为变更契约:同种子完整局输出逐位一致)。
# 耦合边界(5.1 封装修复换代):引用束注入——构造收编排器组装的 20 键封闭束,本段断言 12 键
# (facts/units/damage/reward_economy/fx_host/minesweeper/db/action_economy/reward_grant/
# emit_row_cb/attune_param_sum_cb/perfect_sweep_cb)解包同名成员;对编排器零引用(跨件单向接线,
# 注入 Callable 亦属接线面);变值标量(round_index)走调用参数现值交付。
# run 编排骨架+批私有族(_reveal_batch/_terrain_batch/_seal_batch/_round_end_apply,段内施加面
# 再拆 _reveal_safe_apply/_reveal_mine_apply/_seal_hit_effects 等单面函数);地形 damage_taken
# 消费随迭代 46 卡A T2 并入伤害控制器全局消费族(本段零本地减伤函数);
# kind 分派单点化(P1-1 同批):counter_add 五位三行组 / grant_material 三位兑付块
# 收 _apply_counter_hit / _apply_material_hit 两助手(分支本体逐字保留在助手内)。
var facts: BattleFacts # 战斗事实件(轮缓冲/封印账/溯源)
var units: UnitContainer # 单位容器(hero_unit 读面)
var damage: DamageController # 伤害控制器(开单/承伤发射面)
var reward_economy: RewardEconomy # 地图战斗资源(grant/query_full_snapshot 消费面)
var fx_host: BattleFxHost # 效果引擎宿主件(fx 发射/施加/揭示队列/计数器桶)
var minesweeper: MinesweeperRules # 排雷玩法(板读取/变异过滤/危险格移除面)
var db: ConfigDb
var action_economy: ActionEconomy
var reward_grant: RewardGrant # 奖励授予件(极性路由/行应用/提交面/清图奖励/escrow)
var emit_row_cb: Callable # 宿主 _emit_row——行协议面单点(5 参 head 束形)
var attune_param_sum_cb: Callable # 宿主 attune_param_sum 读投影——转存率合计读数
var perfect_sweep_cb: Callable # 宿主 perfect_sweep_earned 读投影——清图奖励第二份判定
func _init(bundle: Dictionary) -> void:
for k in ["facts", "units", "damage", "reward_economy", "fx_host", "minesweeper", "db",
"action_economy", "reward_grant", "emit_row_cb", "attune_param_sum_cb", "perfect_sweep_cb"]:
assert(bundle.has(k), "[授予段] 引用束缺键: %s" % k)
facts = bundle.facts
units = bundle.units
damage = bundle.damage
reward_economy = bundle.reward_economy
fx_host = bundle.fx_host
minesweeper = bundle.minesweeper
db = bundle.db
action_economy = bundle.action_economy
reward_grant = bundle.reward_grant
emit_row_cb = bundle.emit_row_cb
attune_param_sum_cb = bundle.attune_param_sum_cb
perfect_sweep_cb = bundle.perfect_sweep_cb
func run(ri: int) -> void:
# ① 授予段:结算队列(开格/封印判定/清图)→ 极性路由逐批兑付(伤害形态单据暂存——见 submit 案C 闸)。
# 判定基线 = 轮末在场封印(发射面已过滤——被挖开的封印已随挖掘擦除零兑付;
# 开格事实不可撤)。
reward_grant.defer["on"] = true # 案C:授予段伤害单据只暂存不结账(兑付时点=战斗段末)
_reveal_batch(ri)
_terrain_batch(ri)
_seal_batch(ri)
_round_end_apply(ri)
# 成功刷新事实发射+施加(迭代 46 卡A T4——BattleRefreshFlow 装新板后单点调用,每成功刷新恰一次;
# 拒绝刷新零事实〔C2〕。命中消费三面:counter_add→既有单点落账〔蓄力周转/石层缓冲/坑道小憩 计数〕;
# grant_gp→战斗内缓冲 escrow 入账+fx_grant_gp 行〔矿道红利——不绑清图奖励,C7〕;
# open_ticket channel=blast→崩壁冲击群伤单延迟入队〔案C 同队列,战斗段末按存活目标结账〕)
func apply_board_refreshed(ri: int) -> void:
var br_hits := fx_host.fx_emit("board_refreshed", {"round": ri})
for h in br_hits:
if String(h.get("kind", "")) == "counter_add":
_apply_counter_hit(h)
for h in br_hits:
if String(h.get("kind", "")) == "grant_gp":
reward_grant.escrow += int(h.get("value", 0))
emit_row_cb.call({"type": "effect", "source_class": "fx_grant_gp", "origin_cell": Vector2i(-1, -1)}, ri, "专长·GP 入账 +%d" % int(h.get("value", 0)), int(h.get("value", 0)), {"effect_id": "fx_grant_gp", "escrow": reward_grant.escrow})
for h in br_hits:
if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "blast":
var bb_t := damage.open_ticket(DamageController.KIND_BOARD_BLAST, "fx_board_refresh", "崩壁冲击",
{"channel": "board", "base_value": int(h.get("value", 0)), "target_rule": DamageController.TARGET_ALL_ALIVE,
"ri": ri, "origin_cell": Vector2i(-1, -1)})
reward_grant.defer_ticket(bb_t)
# 开格/踩雷兑付批(轮 facts_reveals 资源事件序:safe 正向乘区兑付 / mine_hit 减半负向兑付)
func _reveal_batch(ri: int) -> void:
for evt in facts.reveals:
if not bool(evt.get("resource", false)):
continue # 封锁格(initial 通道)恒无资源——不入账(战斗结算 义务②)
var snap = evt.get("reward_snapshot")
if not (snap is Dictionary):
continue
if evt.kind == "safe":
_reveal_safe(evt, snap, ri)
elif evt.kind == "mine":
_reveal_mine(evt, snap, ri)
# safe 事件兑付(封印不可逆四情况④:插安全格再挖开 = 零惩罚零奖励——正向兑付作废〔归因行在案〕;
# 标空判定已随封印擦除自然缺席——被挖的封印不在轮末在场基线。dig_reveal 发射+施加序:
# 乘区收集 → 正向兑付 → 施加面〔计数器/揭示入队/追加产出/奥术矢〕。
# payload 携 is_edge_cell〔边陲矿工〕/is_blank_zero〔跳格采掘〕/
# new_resource_type〔地质学家——快照 cell_type 首见判定,见后集登记〕)
func _reveal_safe(evt: Dictionary, snap: Dictionary, ri: int) -> void:
if bool(evt.get("dug_under_seal", false)):
emit_row_cb.call({"type": "reward_ineffective", "source_class": "safe_cell", "origin_cell": evt.cell}, ri, "奖励作废(封印格直接挖开——零惩罚零奖励)", 0, {})
return
var board_b: BoardData = minesweeper.current_board()
var ctype := String(snap.get("cell_type", ""))
var is_new_type := fx_host.mark_new_resource_type(ctype)
var dr_hits := fx_host.fx_emit("dig_reveal", {"cell": evt.cell, "round": ri,
"is_first_dig_of_battle": not facts.feat_first_dig_used,
"terrain_kind": minesweeper.harmful_variants(evt.get("variant_ids", []) as Array),
"is_edge_cell": evt.cell.x == 0 or evt.cell.y == 0
or evt.cell.x == board_b.width - 1 or evt.cell.y == board_b.height - 1,
"is_blank_zero": board_b.digit_of(evt.cell) == 0,
"new_resource_type": is_new_type})
var pos_m := 0
var neg_m := 0
for h in dr_hits:
if String(h.get("kind", "")) != "yield_mult":
continue
pos_m += int(h.get("mult", 0))
neg_m += int(h.get("cost_mult", 0))
reward_grant.grant_routed(reward_grant.feat_yield_adjust(snap, true, pos_m, neg_m), "positive", {"source_class": "safe_cell", "cell": evt.cell, "ri": ri})
_reveal_safe_apply(dr_hits, evt, ri)
# dig_reveal 施加面(序=计数器〔连击计数——乘区收集后执行=本格兑付用旧值〕→ 揭示入队〔地质学家/
# 跳格采掘 reveal_tiles 订阅面,授予段末统一掷格兑付〕→ 追加产出〔深挖钻头 v2 双份制:grant_material
# 命中=配额内追加第二份〕→ 奥术矢〔魔能共鸣——每战首次开安全格追加魔法矢;任意真族单次生效〕)
func _reveal_safe_apply(dr_hits: Array, evt: Dictionary, ri: int) -> void:
for h in dr_hits:
if String(h.get("kind", "")) == "counter_add":
_apply_counter_hit(h)
if not DamageController.hits_of(dr_hits, "reveal_tiles").is_empty():
fx_host.enqueue_reveal()
for h in dr_hits:
if String(h.get("kind", "")) == "grant_material":
_apply_material_hit(h, evt.cell, ri)
var spell_hits: Array[Variant] = DamageController.hits_of(dr_hits, "spawn_spell")
if not spell_hits.is_empty() and not facts.feat_first_dig_used:
facts.feat_first_dig_used = true
var spell_t := damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_first_dig_spell", "奥术矢",
{"channel": "board", "base_value": int((spell_hits[0] as Dictionary).get("value", 0)),
"target_rule": DamageController.TARGET_FRONT_ALIVE, "ri": ri, "origin_cell": evt.cell, "attack_type": "magic"})
reward_grant.submit(spell_t, false)
# mine 事件兑付(封印不可逆四情况②:插危险格再挖开 = 只受挖雷一层惩罚——mine_hit 照常兑付惊动巢穴,
# 标中不发〔封印已随挖掘擦除、不入轮末判定基线〕。mine_hit 发射+施加序:施加面收集 → 减伤落快照 →
# 计数器 → 揭示入队 → 负向兑付〔带减半系数〕→ 反打开单)
func _reveal_mine(evt: Dictionary, snap: Dictionary, ri: int) -> void:
var mine_power := int(snap.get("power", 0))
var mh_hits := fx_host.fx_emit("mine_hit", {"cell": evt.cell, "round": ri,
"damage": mine_power, "dug_under_seal": bool(evt.get("dug_under_seal", false)),
"hp_ratio": int(round(100.0 * float(int(units.hero_unit().get("hp", 0))) / float(maxi(1, int(units.hero_unit().get("max_hp", 1))))))})
var applied := _reveal_mine_apply(mh_hits, snap, evt.cell, ri)
if int(applied["mine_reduce"]) > 0:
snap["power"] = maxi(0, mine_power - int(applied["mine_reduce"])) # 减伤落快照(负向兑付按减后值——安全帽每层减 1)
for h in mh_hits:
if String(h.get("kind", "")) == "counter_add":
_apply_counter_hit(h)
if not DamageController.hits_of(mh_hits, "reveal_tiles").is_empty():
fx_host.enqueue_reveal() # 见血开窍——每颗雷一次揭示(授予段末统一兑付)
reward_grant.grant_routed(reward_grant.feat_yield_adjust(snap, false, 0, 0), "negative", {"source_class": "mine_hit", "cell": evt.cell, "ri": ri, "halve_pct": int(applied["halve_pct"])})
for h in mh_hits:
if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "reflect":
var nct := damage.open_ticket(DamageController.KIND_BOARD_BLAST, "attune_nest_counter", "惊巢反制·反弹",
{"channel": "board", "base_value": int(h.get("value", 0)), "target_rule": DamageController.TARGET_ALL_ALIVE,
"ri": ri, "origin_cell": evt.cell})
reward_grant.submit(nct, false)
# mine_hit 施加面(damage_reduce〔安全帽耗层减伤——value_counter 层值一次清零/背水——定值减免〕/
# heal〔血蜜〕/ bonus_delta〔血税——fx 件四轴累积〕/ blast_scaled〔陷阱爆发——雷伤×倍率全体敌,案C 暂存〕/
# damage_mult 减半系数收集;返回 {halve_pct, mine_reduce} 交兑付与快照减伤消费)
func _reveal_mine_apply(mh_hits: Array, snap: Dictionary, cell: Vector2i, ri: int) -> Dictionary:
var halve_pct := 0
var mine_reduce := 0
for h in mh_hits:
match String(h.get("kind", "")):
"damage_mult":
halve_pct += int(h.get("value", 0))
"damage_reduce":
if h.has("value_counter") and String(h.get("value_counter", "")) != "":
mine_reduce += EffectEngine.counter(fx_host.counters_ctx, String(h.get("value_counter", ""))) # 安全帽——层值减免
else:
mine_reduce += int(h.get("value", 0)) # 背水——定值减免
"heal":
var hu_heal: Dictionary[Variant, Variant] = units.hero_unit()
hu_heal["hp"] = mini(int(hu_heal.get("hp", 0)) + int(h.get("value", 0)), maxi(1, int(hu_heal.get("max_hp", 1))))
"bonus_delta":
var axis := String(h.get("axis", ""))
fx_host.add_bonus_delta(axis, int(h.get("delta", 0)))
"blast_scaled":
var blast_v := maxi(int(db.combat.settlement.damage_roll.damage_floor), int(float(int(snap.get("power", 0))) * float(h.get("mult", 1))))
var bs_t := damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_blast_scaled", "陷阱爆发",
{"channel": "board", "base_value": blast_v, "target_rule": DamageController.TARGET_ALL_ALIVE,
"ri": ri, "origin_cell": cell})
reward_grant.submit(bs_t, false)
return {"halve_pct": halve_pct, "mine_reduce": mine_reduce}
# 地形变异 兑付批(授予段尾——触发枚举 on_open:挖开即伤、chain 通道同口径;伤害单据案C 暂存到
# 战斗段末兑付〔案C〕;信息型 hide_number 不进结算——呈现层读取,见 board_view 数字遮蔽;
# 危险格恒无变异——布置段排除,双层叠罚不存在;封印格直挖照吃变异伤害——地形后果同 mine_hit 口径,
# 非奖励不作废;吞噬流资源兑付〔荆棘编织/地脉补偿〕中伤害形态行同样经案C闸暂存,非伤害形态照常当场)
func _terrain_batch(ri: int) -> void:
for evt in facts.reveals:
var vids: Array[Variant] = evt.get("variant_ids", [])
for vid in vids:
var vrow: Dictionary[Variant, Variant] = db.combat.variation.row_by_id(String(vid))
if String(vrow.get("effect", "")) != "flat_damage":
continue # 触发词表按 effect 路由:flat_damage=on_open 兑现;hide_number/on_open 外组合 = 数据行新词表未接线(显式忽略面)
# 地形变异伤害单据——无类型必中,下限保底在控制器管线内
# damage_taken 统一发射(迭代 46 卡A T2——地形路径并入全局消费族 emit_hero_damage_taken:
# 发射在单据开立前、减后值开单,组合消费含 shield〔全局受伤语义〕;
# hero_damage_consumed 标记入单据 context——战斗段末 _resolve_board_flat_hero 跳过重复发射;
# grant_material〔荆棘/地脉〕移到单据提交后(兑付值序一致——收益不因减伤变化))
var dt_amount := int(vrow.get("effect_value", 0))
var dt_res := damage.emit_hero_damage_taken(dt_amount, "terrain", String(vid), evt.cell, ri)
dt_amount = int(dt_res["total"])
var vt := damage.open_ticket(DamageController.KIND_BOARD_FLAT_HERO, "地形变异", String(vrow.get("display_name", vid)),
{"channel": "board", "base_value": dt_amount, "target_rule": DamageController.TARGET_HERO,
"ri": ri, "origin_cell": evt.cell, "context": {"variant_id": String(vid), "hero_damage_consumed": true}})
reward_grant.submit(vt, false)
for h in (dt_res["hits"] as Array):
if String(h.get("kind", "")) == "grant_material":
_apply_material_hit(h, evt.cell, ri)
# 封印判定批+战封印军规开单段(封印流:seal_judged 批后统一开单:轮末每面封印命中对最前存活敌
# 结算 board_typed 必中伤害〔复用零新配方;「封印所在巢穴敌」简化为 default_target 最前存活敌——
# 台账披露待追认〕;完美布防=本轮封印批全中(hit>0 且 miss==0)时封印伤 ×mult——全对推理奖励)。
# 伤害单据案C 暂存战斗段末兑付。
func _seal_batch(ri: int) -> void:
for evt in facts.seals:
_judge_seal(evt, ri)
_seal_strike_batch(ri)
# seal_settle 发射+施加(战封印 open_ticket + 完美布防 modify_ticket 改写窗口:封印伤基值×完美倍率后
# 逐封印开单)
func _seal_strike_batch(ri: int) -> void:
var fs_hits := 0
var fs_misses := 0
for fevt in facts.seals:
if bool(fevt.get("hit", false)):
fs_hits += 1
else:
fs_misses += 1
var fs_fx := fx_host.fx_emit("seal_settle", {"seals_hit_all": fs_hits > 0 and fs_misses == 0,
"seals_count": fs_hits, "round": ri})
var strike := _seal_strike_value(fs_fx, fs_hits, fs_misses)
if strike.is_empty():
return
for fevt in facts.seals:
if not bool(fevt.get("hit", false)):
continue
var st_t := damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_seal_strike", "战封印军规",
{"channel": "board", "base_value": int(strike["value"]), "target_rule": DamageController.TARGET_FRONT_ALIVE,
"ri": ri, "origin_cell": fevt.cell, "attack_type": "normal"})
reward_grant.submit(st_t, false)
# 战封印军规基值决议(attack 通道 open_ticket 首命中取基值;seal_mult modify_ticket 连乘;
# 完美布防=miss==0 且 hit>0 时基值×倍率。空旗集=空字典〔不开单合法态〕)
func _seal_strike_value(fs_fx: Array, fs_hits: int, fs_misses: int) -> Dictionary:
var banner_hits: Array[Variant] = []
for h in fs_fx:
if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "attack":
banner_hits.append(h)
if banner_hits.is_empty():
return {}
var strike_value := int((banner_hits[0] as Dictionary).get("value", 0))
var seal_mult := 1
for h in fs_fx:
if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "seal_mult":
seal_mult *= maxi(1, int(h.get("value", 1)))
if fs_misses == 0 and fs_hits > 0 and seal_mult > 1:
strike_value *= seal_mult # 完美布防:同轮全中封印伤倍增(全对推理奖励)
return {"value": strike_value}
# round_end 发射+施加(每轮结算收口:熔火之躯反打单开立〔open_ticket 即时执行〕+积层清零〔counter_add
# set〕;停手侧 AP 已在 request_stop 记账,finish_round_stop 按转存率转存 RAP)+施加面:grant_gp〔爆破会计/收盘——escrow 战斗域
# 缓冲,随 gp_dropped 上抛〕/grant_ap mode:queue〔封印回响——返 1 AP 下轮,queue_ap_delta 通道与轮始
# 加成同面〕+段界收口(转存率入参、清图奖励、揭示兑付、段界结账)
func _round_end_apply(ri: int) -> void:
var re_fx := fx_host.fx_emit("round_end", {"round": ri})
_round_end_blast_tickets(re_fx, ri)
_round_end_heal_value_counter(re_fx, ri) # 迭代 46 卡A T5——坑道小憩恢复兑付(先于清零行消费层值,声明序=heal 行在 counter set 行前)
for h in re_fx:
if String(h.get("kind", "")) == "counter_add":
_apply_counter_hit(h)
for h in re_fx:
if String(h.get("kind", "")) == "grant_gp":
reward_grant.escrow += int(h.get("value", 0))
emit_row_cb.call({"type": "effect", "source_class": "fx_grant_gp", "origin_cell": Vector2i(-1, -1)}, ri, "专长·GP 入账 +%d" % int(h.get("value", 0)), int(h.get("value", 0)), {"effect_id": "fx_grant_gp", "escrow": reward_grant.escrow})
elif String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue":
action_economy.queue_ap_delta(int(h.get("value", 0)))
# 转存率入参(储备罐 rap_transfer_mult magnitude=目标转存率百分比 125=1.25:1;未装配=100 与 1:1 逐位一致)
var rap_pct: int = int(attune_param_sum_cb.call("rap_transfer_mult"))
action_economy.finish_round_stop(rap_pct if rap_pct > 0 else 100)
if facts.path == "cleared":
reward_grant.grant_clear_reward(ri, facts.board_from_midround, bool(perfect_sweep_cb.call()))
fx_host.flush_pending_reveals(ri) # 揭示效果兑付(授予段末统一掷格,防轮 facts 迭代重入)
damage.flush() # 授予段界结账(案C 后本段伤害单据全数暂存,此处对空队 no-op——保留作防御性段界)
# round_end heal value_counter 兑付(迭代 46 卡A T5——坑道小憩:层值=本轮成功刷新累计恢复点,
# units.heal 封顶、满血发未生效行;层清零由同事件 counter_add set 行承载〔声明序在后〕——本面
# 先于 counter_add 遍历消费层值即保序。行源=成功刷新事实族(无棋盘格——fx_board_refresh 豁免类))
func _round_end_heal_value_counter(re_fx: Array, ri: int) -> void:
for h in re_fx:
if String(h.get("kind", "")) != "heal" or String(h.get("value_counter", "")) == "":
continue
var layers := EffectEngine.counter(fx_host.counters_ctx, String(h.get("value_counter", "")))
if layers <= 0:
continue
var hu: Dictionary[Variant, Variant] = units.hero_unit()
if int(hu.get("hp", 0)) >= int(hu.get("max_hp", 1)):
emit_row_cb.call({"type": "reward_ineffective", "source_class": "fx_board_refresh", "origin_cell": Vector2i(-1, -1)}, ri, "坑道小憩 未生效(满血)", 0, {"batch_key": String(h.get("value_counter", ""))})
continue
var healed: int = units.heal(hu, layers)
emit_row_cb.call({"type": "heal", "source_class": "fx_board_refresh", "origin_cell": Vector2i(-1, -1)}, ri, "坑道小憩 回复", healed, {"target": "hero"})
# round_end blast 通道反打单(per-counter 积层读数×value=反打总量;source_class 用中性名,
# per-counter 反打通道保留泛用面;案C 暂存战斗段末兑付)
func _round_end_blast_tickets(re_fx: Array, ri: int) -> void:
for h in re_fx:
if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "blast":
var mg_layers := EffectEngine.counter(fx_host.counters_ctx, String(h.get("per", "")))
var mg_total := int(h.get("value", 0)) * mg_layers
if mg_total > 0:
var mg_t := damage.open_ticket(DamageController.KIND_BOARD_BLAST, "fx_blast_counter",
"积层反噬 ×%d" % mg_layers,
{"channel": "board", "base_value": mg_total, "target_rule": DamageController.TARGET_ALL_ALIVE,
"ri": ri, "origin_cell": minesweeper.hero_pos()})
reward_grant.submit(mg_t, false)
# 封印判定(封印流本体;唯一调用面=本段 _seal_batch 的 facts_seals 循环——命中/未中两分)
func _judge_seal(evt: Dictionary, ri: int) -> void:
if evt.hit:
_judge_seal_hit(evt, ri)
else:
_judge_seal_miss(evt, ri)
# 封印命中(资格以危险格实例为键去重——排雷玩法 诚实重复上报的对侧;seal_hit 发射序:
# 殉爆票据 → 施加面 → 雷区图谱额外兑付 → 基线正向兑付)
func _judge_seal_hit(evt: Dictionary, ri: int) -> void:
var cell: Vector2i = evt.cell
if facts.seal_consumed.has(cell):
return # 去重跳过;零明细行
facts.seal_consumed[cell] = true
facts.mark_perfect_hit()
var fh_hits := fx_host.fx_emit("seal_hit", {"cell": cell, "round": ri,
"terrain_kind": minesweeper.harmful_adjacent(cell)})
_seal_burst_tickets(fh_hits, cell, ri)
_seal_hit_effects(fh_hits, cell, ri)
# 雷区图谱(封印流——标中雷额外获得一份格面资源:封印开始产出对标开格;
# 复用 grant 单点,额外兑付走同一 grant_routed 正向路由)
for h in fh_hits:
if String(h.get("kind", "")) == "grant_material":
_apply_material_hit(h, cell, ri, "seal_hit")
var snap = reward_economy.grant(cell)
if not (snap is Dictionary):
return
reward_grant.grant_routed(snap, "positive", {"source_class": "seal_hit", "cell": cell, "ri": ri, "highlight": "seal_hit"})
# 标中殉爆票据(行 triggers open_ticket channel blast value_from magnitude——每标中一面封印全体存活敌
# 吃 N 群伤,无类型走 blast 通道;封印爆裂〔专长〕同面消费)
func _seal_burst_tickets(fh_hits: Array, cell: Vector2i, ri: int) -> void:
for h in DamageController.hits_of(fh_hits, "open_ticket"):
if String(h.get("channel", "")) != "blast":
continue
var fbt := damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_seal_burst", "封印爆裂",
{"channel": "board", "base_value": int(h.get("value", 0)), "target_rule": DamageController.TARGET_ALL_ALIVE,
"ri": ri, "origin_cell": cell, "highlight": "highlight"})
reward_grant.submit(fbt, false)
# 标中施加面:counter_add〔血税 标中+1 攻——bonus_delta atk 轴〕/
# ac_bonus〔封印桩 本轮 AC+N〕/ bonus_delta〔血税——fx 件四轴累积〕/
# remove_mines〔封印匠 每第 3 次该危险格中立移除——mod_eq 闸在行 when〕/
# preview_area〔示警烟火 3×3 资源类型预告〕
func _seal_hit_effects(fh_hits: Array, cell: Vector2i, ri: int) -> void:
for h in fh_hits:
match String(h.get("kind", "")):
"counter_add":
_apply_counter_hit(h)
"ac_bonus":
fx_host.apply_ac_bonus(h)
"bonus_delta":
var axis_f := String(h.get("axis", ""))
fx_host.add_bonus_delta(axis_f, int(h.get("delta", 0)))
"remove_mines":
var removed: Array[Variant] = minesweeper.remove_mines_in([cell])
if not removed.is_empty():
emit_row_cb.call({"type": "extended", "source_class": "fx_seal_smith", "origin_cell": cell}, ri, "封印匠·危险格中立移除", 0, {"effect_id": "mine_neutralized"})
"preview_area":
_seal_preview_area(cell, ri)
# 示警烟火施加面(3×3 邻格资源类型预告——类型去重集入 overlay;信息呈现零回写)
func _seal_preview_area(cell: Vector2i, ri: int) -> void:
var board_p: BoardData = minesweeper.current_board()
var types: Array[Variant] = []
for n in board_p.neighbors8(cell):
var n_snap = reward_economy.query_full_snapshot(n)
if n_snap is Dictionary and not types.has(String((n_snap as Dictionary).get("cell_type", ""))):
types.append(String((n_snap as Dictionary).get("cell_type", "")))
if reward_grant.treasure_inventory != null and not types.is_empty():
reward_grant.treasure_inventory.apply_overlay("reward_preview", board_p.neighbors8(cell))
emit_row_cb.call({"type": "extended", "source_class": "fx_warning_beacon", "origin_cell": cell}, ri, "示警烟火·周围资源类型", 0, {"effect_id": "fx_preview_area", "types": types})
# 封印未中(每格实例一次去重——归因行与负向兑付同键,防逐轮惩罚轰炸;seal_missed 发射序:
# grant_ap mode:queue 下一轮兑现 / forgive_seal_miss 完美保险豁免 → 归因行 → 负向兑付)
func _judge_seal_miss(evt: Dictionary, ri: int) -> void:
var cell: Vector2i = evt.cell
facts.mark_perfect_miss()
if facts.seal_miss_seen.has(cell):
return # 每格实例一次去重
facts.seal_miss_seen[cell] = true
# 危险直觉·守挂点(seal_missed 发射:契约 12 触发主语=「当回合发生的标错
# 动作」——去重闸=新旧标错判别器〔后续轮重判旧封印被闸挡回零触发〕;双条件②「结算时封印仍在
# 场」由发射面既有过滤承载〔被挖开的封印已随挖掘擦除零兑付〕;grant_ap mode:queue 下一轮
# 开轮兑现——末轮判出无下一轮=合法作废自动成立〔编排器每战新建,队列不跨战〕;限次=合成行
# adventure 域计数器闸)
for h in fx_host.fx_emit("seal_missed", {"cell": cell, "round": ri}):
if String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue":
action_economy.queue_ap_delta(int(h.get("value", 0)))
elif String(h.get("kind", "")) == "forgive_seal_miss":
# 完美保险(订阅式豁免:本战首次标错计一次豁免,只累加自有账
# _perfect_forgiven,绝不回写 _perfect_hit_false=契约 2 计数纯净;发射时机与消费点零改动=契约 3)
facts.mark_perfect_forgiven()
_apply_counter_hit(h) # 豁免同时记账(insurance_used battle 域——下次标错 when lt 1 不再命中)
emit_row_cb.call({"type": "extended", "source_class": "seal_miss", "origin_cell": cell}, ri, "完美保险·标错豁免", 0,
{"effect_id": "insurance_used", "forgiven": facts.perfect_forgiven})
var hl := "perfect_broken" if facts.perfect_hit_false - facts.perfect_forgiven == 1 else ""
emit_row_cb.call({"type": "seal_miss", "source_class": "seal_miss", "origin_cell": cell, "highlight": hl}, ri, "封印未中", 0, {})
var snap = reward_economy.grant(cell)
if not (snap is Dictionary):
return
reward_grant.grant_routed(snap, "negative", {"source_class": "seal_miss", "cell": cell, "ri": ri})
# counter_add 三行组单点(五位分支本体逐字收拢——kind 分派随段归位 P1-1)
func _apply_counter_hit(h: Dictionary) -> void:
var set_v = h.get("set", null)
EffectEngine.counter_add(fx_host.counters_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
# grant_material 兑付块单点(三位分支本体逐字收拢——source_class 两态 safe_cell/seal_hit 由参承载)
func _apply_material_hit(h: Dictionary, cell: Vector2i, ri: int, source_class: String = "safe_cell") -> void:
var g_snap = reward_economy.grant(cell)
if g_snap is Dictionary:
reward_grant.grant_routed(g_snap, "positive", {"source_class": source_class, "cell": cell, "ri": ri, "label_suffix": String(h.get("label", ""))})