merge: 迭代17卡⓪内容数据驱动扩展批落籍(EffectEngine事件发布订阅解释器+16发射点+19行23效果类型+组合规则6条+改写窗口;专长27行迁移+旧23挂点枚举消亡+伤害控制器13处消费点拆除+秘宝六件按id特判清零+令牌树收敛+文案表值化+断言面随迁净增17;闸=冒烟800/0+run_all 17套+probe_gate含click全绿+MC二档对拍40局逐位一致[seed0=83000 off/formed配对,判据两套全过]+doc三闸/熵增存量红零新增;active.md与并行迭代18两批union解+台账预配号381撞号改登383〔381/382系并行会话先占〕;台账383;遗留=掘进蓄能积点模判定归词汇表扩展批裁决+主动件kind词族改名不随批+实机目检归用户)

This commit is contained in:
DKChan committed 2026-10-07 00:45:05 +08:00
commit e535436695
25 files changed
+1727 -722

No files matched your search

+43 -21
View File
@@ -35,19 +35,19 @@ var last_round_end: Dictionary:
var _settlement_consumed := false # AC-14 门:notify_settlement_done 消费后才受理 round_start
var _next_round_mode := "" # 调试观测量(同板续轮 / 换新图)
var _pending_ap_delta := 0 # 下轮行动点修正排队(格面资源八型批 2026-09-20——action 型 ±1 排队,轮开始兑现后清零)
# 蓄力流运行态(迭代 12 T5——relic 件注入面;false=关着零扰动):
# feat_ap_carry=整备军备(轮末剩余 AP 转蓄力层替代 RAP 转存——cleared 作废口径改写的停手侧承载);
# feat_dig_extra=深挖钻头付费开掘对价(R4 甲非信息税形态;2026-10-04 试玩缺陷修复——对价挂付费
# 开掘侧:AP 富余时挖掘付全价 1+extra 积付费计数、AP 不足时降级只付基础 1 照常开格不积计数——
# 原「持有期所有挖掘无条件 +1」实现把对价错挂成普通挖掘税,AP=1 锁死开格);charge_layers=蓄力层账本。
var feat_ap_carry := false
var feat_dig_extra := 0
# 蓄力流运行态(迭代 12 T5 → 内容数据驱动扩展批 T3 换代:旧 feat_ap_carry/feat_dig_extra 注入旗
# 退役——转蓄力=round_end grant_ap(mode:transfer) 订阅(finish_round_stop 定夺)、深挖对价=dig_paid
# counter_spend 订阅(request_dig 施加面);charge_layers=蓄力层账本)
var _charge_layers := 0
var charge_layers: int:
get: return _charge_layers
var _deep_paid_count := 0
var deep_paid_count: int:
get: return _deep_paid_count
# 效果引擎(内容数据驱动扩展批 T1——dig_paid/charge_full 发射面;round_end 转存决策面)
var _fx: EffectEngine = null
var _last_stop_ap := 0 # 停手侧剩余 AP 记账(转存决策移至 round_end 发射点——grant_ap(transfer) 订阅定夺)
func set_fx(fx: EffectEngine) -> void:
_fx = fx
func _init(db_ref: ConfigDb) -> void:
@@ -87,17 +87,25 @@ func try_spend(action_kind: String) -> bool:
# 排雷玩法 挖掘专用:chain_size = 连锁附加格数(不含首格);clears_map = 本次动作后全图安全格开完。
# 深挖钻头付费开掘(2026-10-04 缺陷修复——对价挂付费侧):AP 富余(≥基础+extra)时本动作付全价
# 并积付费开掘计数(orchestrator 深层产出配额取数面);AP 不足全价时**降级只付基础 1 照常开格**、
# 不积计数——持有钻头永不锁死普通挖格(散件保底 B-2)。
# 深挖钻头付费开掘(内容数据驱动扩展批 T3——dig_paid counter_spend 订阅施加面,2026-10-04 缺陷
# 修复口径不变):按序消耗族——AP 富余(≥基础+对价)时本动作付全价并积本轮付费计数(效果引擎
# 计数器=授予段追加产出配额取数面);AP 不足全价时 fallback 降级只付基础 1 照常开格、不积计数
# ——持有钻头永不锁死普通挖格(散件保底 B-2)。
func request_dig(chain_size: int, clears_map: bool) -> bool:
if _phase != Phase.ACTIVE:
_reject("phase")
return false
var cost := _cost("dig")
if feat_dig_extra > 0 and _ap >= cost + feat_dig_extra:
cost += feat_dig_extra
_deep_paid_count += 1
# dig_paid 发射+施加(内容数据驱动扩展批 T3——付费开掘发起、动作经济闸收对价前:
# counter_spend 命中=尝试加收对价并积计数;资源不足走 fallback/耗尽即止=只付常规基础对价)
if _fx != null:
for h in _fx.dispatch("dig_paid", {"ap_cost": cost, "ap_surplus": _ap - cost}):
if String(h.get("kind", "")) != "counter_spend":
continue
var extra := int(h.get("value", 1))
if _ap >= cost + extra:
cost += extra
_fx.counter_add(String(h.get("counter_id", "")), 1)
if _ap < cost:
_reject("resource_face")
return false
@@ -137,21 +145,35 @@ func _spend_and_arbitrate(action_kind: String, clears_map: bool) -> bool:
# 停手:剩余 AP 1:1 转存 RAP;行动经济 直发 stopped 路径 round_end(双发射源)。
# 整备军备改写(迭代 12 T5——feat_ap_carry=true 时剩余 AP 转蓄力层〔上限 3〕替代 RAP 转存:
# 「不动也是行动」——cleared 作废口径改写的停手侧承载;cleared 分支 AP 恒清零维持既有作废语义)
# 「不动也是行动」——cleared 作废口径改写的停手侧承载;cleared 分支 AP 恒清零维持既有作废语义)。
# 内容数据驱动扩展批 T1:本方法改记账制——剩余 AP 存 _last_stop_ap 后清零,转存决策移至
# round_end 发射点 finish_round_stop(grant_ap(mode:transfer) 订阅定夺;双轨期旧旗 feat_ap_carry
# 并行生效,行为与迁移前逐位一致)。
func request_stop() -> bool:
if _phase != Phase.ACTIVE:
_reject("phase")
return false
if feat_ap_carry:
_charge_layers = mini(3, _charge_layers + _ap)
else:
_rap += _ap
_last_stop_ap = _ap
_ap = 0
_finish_round("stopped", "same_board")
round_end_stopped.emit({"path_tag": "stopped", "continuation_hint": "same_board"})
return true
# 轮末停手转存决策(round_end 发射点调用——编排器授予段尾):transfer=true=剩余 AP 转蓄力层
# (上限 3,整备军备面);false=1:1 转存 RAP(缺省)。非停手轮(cleared/耗尽)_last_stop_ap=0
# 两分支均零操作。转蓄后达上限的沿发 charge_full(R6 框架先行——现无订阅者)。
func finish_round_stop(transfer: bool) -> void:
if transfer:
var prev_layers := _charge_layers
_charge_layers = mini(3, _charge_layers + _last_stop_ap)
if _fx != null and _charge_layers >= 3 and prev_layers < 3:
_fx.dispatch("charge_full", {"layers": _charge_layers})
else:
_rap += _last_stop_ap
_last_stop_ap = 0
# 轮内刷新(2026-09-12 方案 A 用户拍板;等待期通道 2026-09-15 用户拍板整体移除):唯一入口 =
# Active 相位——仅扣 RAP,相位与 AP 均不动。刷新不打断轮次、不触发结算;换图编排归 战斗结算
# (retire + install 立即执行,玩家同轮继续挖新图)。决策主体 = 玩家(经 HUD框架发起)纪律不变。
+16 -13
View File
@@ -425,19 +425,15 @@ func _apply_curio_pick(kind: String, tier: String, value: int, feat_id: String =
_feats.append(feat_id)
_curio_taken["relic"] = int(_curio_taken.get("relic", 0)) + 1
return true
# 加算形态路由(内容数据驱动扩展批 T5——match 收敛进效果类型:属性值提升四类走
# adventure_table.CURIO_APPLY_ROUTES 声明路由(bonus 落点键),加新加算类令牌零代码)
var sink := db.adventure.curio_apply_route(kind)
if sink != "":
bonus[sink] = int(bonus.get(sink, 0)) + value
_curio_taken[kind] = int(_curio_taken.get(kind, 0)) + 1
return true
# 通道面(副作用形态——显式分支)
match kind:
"attack":
bonus["atk"] = int(bonus.get("atk", 0)) + value
_curio_taken["attack"] = int(_curio_taken.get("attack", 0)) + 1
"max_hp":
bonus["max_hp"] = int(bonus.get("max_hp", 0)) + value
_curio_taken["max_hp"] = int(_curio_taken.get("max_hp", 0)) + 1
"armor":
bonus["armor_class"] = int(bonus.get("armor_class", 0)) + value
_curio_taken["armor"] = int(_curio_taken.get("armor", 0)) + 1
"ap":
bonus["ap"] = int(bonus.get("ap", 0)) + value
_curio_taken["ap"] = int(_curio_taken.get("ap", 0)) + 1
"gp":
if _runner != null:
_runner.grant_curio_gp(value)
@@ -649,7 +645,14 @@ func _seed_attune_charges() -> void:
# 达上限剔牌/掷定前置直显;停点可中断)----
func _attune_refresh_x_total() -> int:
return int(db.treasure_item_row("pick_refresh").get("magnitude", 0)) if _attunement.has("pick_refresh") else 0
# 内容数据驱动扩展批 T4——按 id 特判拆除:kind 分发查询(装配集内 effect_kinds 含 bonus_refresh
# 的行 magnitude 合计;刷新件类加件零 id 分支)
var total := 0
for iid_v in _attunement:
var arow: Dictionary = db.treasure_item_row(String(iid_v))
if (arow.get("effect_kinds", []) as Array).has("bonus_refresh"):
total += int(arow.get("magnitude", 0))
return total
func _reset_refresh_x() -> void:
+289 -234
View File
@@ -75,113 +75,65 @@ var _flag_consumed := {} # 兑付资格以雷格实例为键
# 同调装配运行态(迭代 11 第①批 T5——begin_battle 经 enc.attunement 注入;off=空集全部效果零扰动)
var attunement: Array = [] # 装备中同调件 item_id 集
var _attune_charges_ref: Dictionary = {} # 充能账本共享引用(迭代 11 第①批 T6——use_treasure 消费直改)
var _attune_bank := 0 # 掘进蓄能积点(battle 域——每 charge_n 安全格 +1 点)
var _attune_safe_seen := 0 # 掘进蓄能 battle 域安全开格计数
# 特性运行态(迭代 12 T2——relic 持有集经 enc.feats 注入;求值旗注 damage_controller.feat_flags;
# 魔矢每战首次计数 battle 域。空集=零扰动)
var _attune_safe_seen := 0 # 掘进蓄能 battle 域安全开格计数(进度呈现取数面;积点账本=效果引擎计数器 dig_charge_bank——行 triggers counter_spend 订阅同账本)
# 特性运行态(内容数据驱动扩展批 T3——relic 持有集经 enc.feats 注入:效果行为=数据行 triggers 经
# 效果引擎分发;魔矢每战首次计数 battle 域。空集=零扰动)
var _feat_first_dig_used := false
var _feat_combo := 0 # 矿脉直觉连击计数(battle 域——safe +1/mine 清零)
var _feat_deep_given := 0 # 深挖钻头本轮已追加份数(battle 域——v2 本格双份制:safe 事件按付费数上限)
var _feat_paid_at_round_start := 0 # 轮初付费计数快照(v2——本轮付费次数=当前值−快照)
var _feat_magma_layers := 0 # 熔火之躯积层(battle 域——受地形伤 +1,轮末反打清零)
# 效果引擎(内容数据驱动扩展批 T1——发布订阅解释器;begin_battle 从持有集/装备集重建订阅。
# T3 起施加面全量切换:发射点 dispatch 返回命中,发射点按组合规则施加——旧 feat_flags 旗注入消亡)
var _fx: EffectEngine
# 事件发射薄面(16 发射点统一入口——订阅空=零循环零扰动;确定性同步分发零 RNG)
func _fx_emit(event: String, payload: Dictionary) -> Array:
if _fx == null:
return []
return _fx.dispatch(event, payload, Callable(self, "_fx_fact"))
# round_start 发射+施加(内容数据驱动扩展批 T3——grant_ap mode:queue=当轮入队,行动经济轮切换
# 即兑现;计数器 round_start 重置点在 dispatch 内生效。调用点两处:begin_battle 首轮 / 轮始段续轮)
func _emit_round_start() -> void:
for h in _fx_emit("round_start", {"round": round_index}):
if String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue":
action_economy.queue_ap_delta(int(h.get("value", 0)))
# 补充事实面(fact 取值第二优先源——payload 未携带的运行态读数)
func _fx_fact(fact: String):
match fact:
"charge_layers":
return action_economy.charge_layers
"nest_count":
var n := 0
for eff in _status_effects.query_active("enemy"):
if String(eff.effect_id) == "weaken":
n += 1
return n
"round_index":
return round_index
"flags_count":
var hits_n := 0
for fevt in _facts_flags:
if bool(fevt.get("hit", false)):
hits_n += 1
return hits_n
return null
func _attune_has(item_id: String) -> bool:
return attunement.has(item_id)
# 装备件表值读取(magnitude/effective_rounds 等——TreasureItemTable 行;未装配不读表)
# 装备件表值读取(charge_n 等——TreasureItemTable 行;未装配不读表。magnitude 读数面随 T4
# 秘宝订阅迁移退役——行值经 value_from 在 rebuild 定影,零运行期按件取值)
func _attune_row(item_id: String) -> Dictionary:
return db.treasure_item_row(item_id) if _attune_has(item_id) else {}
func _attune_mag(item_id: String) -> int:
return int(_attune_row(item_id).get("magnitude", 0))
# 特性求值(迭代 12 T2——持有集 × feat_table → damage_controller.feat_flags 注入面):
# rider_flat = Σ rider 件 value(奥术余烬 +1 / 魔能共鸣 +2);magic 档一(≥2 件)rider +1、
# 档二(≥4 件)rider 可暴击;clamp 两件直调键;first_dig_spell 旗(魔能共鸣第二效果——魔矢值
# = 件 effects.value)。空集/空表 = 零旗零扰动。
func _feat_flags_for(feat_ids: Array) -> void:
var flags: Dictionary = {}
if not feat_ids.is_empty() and db.feats != null and not db.feats.rows.is_empty():
var hooks := FeatsEngine.hooks_for(feat_ids, db.feats)
var tiers := FeatsEngine.tiers_for(feat_ids, db.feats)
for h in hooks:
var hook: String = String(h.get("hook", ""))
var fx: Dictionary = h.get("effects", {}) as Dictionary
match hook:
"hero_attack_rider":
flags["rider_flat"] = int(flags.get("rider_flat", 0)) + int(fx.get("value", 0))
"clamp_modifier":
# clamp 方向按件 note 词分流(魔网=magic-vs-magic 罚修正 / 湛蓝=magic-vs-heavy 加成)
if String(fx.get("polarity", "")) == "magic_vs_magic":
flags["clamp_magic_magic"] = int(flags.get("clamp_magic_magic", 0)) + int(fx.get("value", 0))
elif String(fx.get("polarity", "")) == "magic_vs_heavy":
flags["clamp_magic_heavy"] = int(flags.get("clamp_magic_heavy", 0)) + int(fx.get("value", 0))
elif String(fx.get("polarity", "")) == "pierce_vs_light":
flags["clamp_pierce_light"] = int(flags.get("clamp_pierce_light", 0)) + int(fx.get("value", 0))
"first_dig_spell":
flags["first_dig_spell"] = true
flags["spell_value"] = int(fx.get("value", 0))
"flag_resource":
flags["flag_resource_extra"] = int(fx.get("value", 0))
"nest_aura":
flags["nest_aura_value"] = int(fx.get("value", 0))
"flag_all_hit":
flags["flag_all_hit_mult"] = int(fx.get("value", 0))
"flag_strike":
flags["flag_strike_value"] = int(fx.get("value", 0))
"kill_overflow":
flags["kill_overflow"] = true
"dig_count_segments":
flags["chain_value"] = int(fx.get("value", 0))
flags["chain_cap"] = int(fx.get("cap", 2))
"roll_discount":
flags["discount_shift"] = int(fx.get("value", 0))
"round_dig_bonus":
flags["round_dig_value"] = int(fx.get("value", 0))
flags["round_dig_cap"] = int(fx.get("cap", 5))
"hit_trade":
flags["hit_trade_atk"] = int(fx.get("trade_atk", 0))
flags["hit_trade_hit"] = int(fx.get("trade_hit", 0))
"armor_roll_rewrite":
flags["armor_roll_rewrite"] = int(fx.get("value", 0))
"heavy_debuff":
flags["crack_value"] = int(fx.get("value", 0))
"dig_yield_mult":
# 开格产出乘区族(贪婪矿灯/矿脉直觉/赌徒的血签——正负双向,同 hook 累加)
flags["dig_yield_pos_mult"] = int(flags.get("dig_yield_pos_mult", 0)) + int(fx.get("mult", 0))
flags["dig_yield_neg_mult"] = int(flags.get("dig_yield_neg_mult", 0)) + int(fx.get("cost_mult", 0))
if int(fx.get("cap", 0)) > 0:
flags["combo_cap"] = int(fx.get("cap", 0)) # 矿脉直觉连击上限
"deep_dig":
flags["deep_dig_cost"] = int(fx.get("ap_cost", 0))
flags["deep_dig_cap"] = int(fx.get("cap", 4))
"ap_carry":
flags["ap_carry_value"] = int(fx.get("value", 0))
"charge_shield":
flags["shield_per_layer"] = int(fx.get("shield_per_layer", 0))
"charge_spike":
flags["spike_mult"] = int(fx.get("value", 0))
"terrain_refund":
flags["terrain_refund"] = int(fx.get("value", 0))
"terrain_dig_double":
flags["terrain_dig_double"] = int(fx.get("value", 0))
"flag_neutralize":
flags["flag_neutralize"] = int(fx.get("value", 0))
"variant_counter":
flags["variant_counter_value"] = int(fx.get("value", 0))
var mt := int(tiers.get("magic", 0))
if mt >= 1:
flags["rider_flat"] = int(flags.get("rider_flat", 0)) + 1 # 档一质变(≥2 件):rider +1(provisional)
if mt >= 2:
flags["rider_crit"] = true # 档二质变(≥4 件):rider 可暴击(crit 时乘暴击倍率)
_damage.feat_flags = flags
# 蓄力流/深挖钻头 action_economy 注入(求值即注入——冒烟直呼 _feat_flags_for 同面生效)
action_economy.feat_ap_carry = int(flags.get("ap_carry_value", 0)) > 0
action_economy.feat_dig_extra = int(flags.get("deep_dig_cost", 0))
# 特性求值(内容数据驱动扩展批 T3——_feat_flags_for 挂点求值退役:效果行为=数据行 triggers 订阅,
# 效果引擎 dispatch 分发;旧 23 hook 枚举与旗注入消亡。流派档位质变=FeatsEngine 纯函数求值
# (档位机制本身系引擎基建非内容效果),两运行态注伤害控制器:档一 rider+1 / 档二 rider 可暴击)
var _flag_miss_seen := {} # 标旗未中行去重(每安全格实例一次)
var _perfect_hit_true := 0 # 板级完美排雷计数(install 重置)
var _perfect_hit_false := 0
@@ -191,6 +143,7 @@ var _hero_atk := 0 # 主角攻击基值快照(encounte
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
db = db_ref
rng = rng_ref
_fx = EffectEngine.new(db_ref)
_board_generator = BoardGenerator.new()
action_economy = ActionEconomy.new(db)
minesweeper = MinesweeperRules.new(db, action_economy)
@@ -199,6 +152,9 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
_reward_economy = RewardEconomy.new(db, rng)
_damage = DamageController.new(db, rng, _enemy_system, _status_effects)
_damage.emit_row_cb = _emit_row # 行协议归属本类(9 类行协议 + 来源断言不外泄)
_damage.set_fx(_fx) # 效果引擎注入(attack_commit/clamp_check/armor_roll/kill/damage_taken 发射面)
action_economy.set_fx(_fx) # dig_paid/charge_full 发射面
_reward_economy.set_fx(_fx) # reward_roll 发射面
minesweeper.cell_revealed.connect(_on_cell_revealed)
minesweeper.flag_judged.connect(_on_flag_judged)
minesweeper.round_end_board.connect(_on_round_end)
@@ -248,38 +204,44 @@ func begin_battle(enc: Dictionary) -> bool:
# 伤害控制器、偏运 N 注入地图战斗资源——均须先于首板 install)
attunement = (enc.get("attunement", []) as Array).duplicate()
_attune_charges_ref = enc.get("attune_charges", {}) # 共享 dict 引用(level/adventure 同账本——use_treasure 扣减直改)
_attune_bank = 0
_attune_safe_seen = 0
_damage.attune_venom = _attune_mag("venom_edge") if _attune_has("venom_edge") else 0
_reward_economy.luck_bias = _attune_mag("luck_coin") if _attune_has("luck_coin") else 0
# 特性运行态注入(迭代 12 T2 魔法流——enc.feats 携带持有集;FeatsEngine 求值后注伤害控制器
# feat_flags;空集/空表=零扰动。魔矢每战首次开格计数同步归零)
_feat_flags_for((enc.get("feats", []) as Array))
# 淬毒/偏运注入拆除(内容数据驱动扩展批 T4——两件被动改 triggers 订阅行:淬毒=clamp_check
# modify 窗口、偏运=reward_roll bias_rewards;value 行字段 value_from 定影,零 begin_battle 注入)
# 特性运行态(内容数据驱动扩展批 T3——enc.feats 携带持有集:效果行为=数据行 triggers 经效果
# 引擎分发;流派档位=FeatsEngine 纯函数求值两运行态注伤害控制器。魔矢每战首次开格计数同步归零)
var feat_ids: Array = enc.get("feats", []) as Array
if not feat_ids.is_empty() and db.feats != null:
var tiers: Dictionary = FeatsEngine.tiers_for(feat_ids, db.feats)
var mt := int(tiers.get("magic", 0))
_damage.rider_tier_bonus = 1 if mt >= 1 else 0
_damage.rider_tier_crit = mt >= 2
else:
_damage.rider_tier_bonus = 0
_damage.rider_tier_crit = false
_feat_first_dig_used = false
_feat_combo = 0
_feat_deep_given = 0
_feat_paid_at_round_start = 0
_feat_magma_layers = 0
# 树 rule 键消费(迭代 12 T10——Fork 激活侧 effects;真实接线首期两键:先手压制=战斗首轮
# AP+1(queue_ap_delta——先手压制语义)/稳健拓荒=敌方攻击减 1(伤害控制器 flags bulwark);
# 其余 rule 键=stat 面投影不入战斗(T8 表内容 provisional 口径披露))
# 树 rule 键消费(迭代 12 T10 → 内容数据驱动扩展批 T3 换代:Fork 激活侧效果=效果引擎合成
# 订阅(rebuild 注入——round_start 首轮 AP+1 / damage_taken 减伤+1),旧直调拆除。其余 rule 键
# =stat 面投影不入战斗(T8 表内容 provisional 口径披露))
var tree_rules: Array = enc.get("tree_rules", []) as Array
if tree_rules.has("posture_first_strike"):
action_economy.queue_ap_delta(1)
_damage.feat_flags["bulwark"] = 1 if tree_rules.has("posture_bulwark") else 0
# 效果引擎订阅重建(内容数据驱动扩展批 T1——每战从持有集/装备集重建,中途获得下一战生效;
# 树规则合成订阅同面装载。装载序=feats 表序→treasure 表序→树规则,写死保种子复放)
_fx.rebuild(feat_ids, attunement, tree_rules)
# 迭代 10 奖励重构批——跨冒险行动点加成轴(统一冒险加算区第四轴):经排队修正通道兑现
# 「每场战斗开始首轮 AP +加成值」(queue_ap_delta = 下轮开轮基线加算、notify_round_start 兑现后
# 清零——首轮即消费,后续轮次不重复兑现;语义锚 = 行动经济 GDD「跨冒险行动点加成轴」节)
var ap_bonus: int = int(enc.get("hero_ap_bonus", 0))
if ap_bonus > 0:
action_economy.queue_ap_delta(ap_bonus)
# 工兵晨令(迭代 11 同调批 T5——前 effective_rounds 轮每轮 +1 AP;首轮在此入队,后续轮在轮始段续队)
if _attune_has("dawn_order"):
action_economy.queue_ap_delta(_attune_mag("dawn_order"))
# 工兵晨令直调拆除(内容数据驱动扩展批 T4——round_start grant_ap(mode:queue) 订阅行承载,
# when round_index ≤ effective_rounds 行字段 value_from 定影;发射位先于轮切换=当轮兑现)
# round_start 发射+施加(内容数据驱动扩展批 T3——发射位先于行动经济轮切换:树规则 grant_ap
# mode:queue 当轮入队、notify_round_start 即兑现(先手压制=首轮 AP+1 语义锚,与迁移前
# begin_battle 直调 queue_ap_delta 逐位一致);计数器 round_start 重置点同点生效)
round_index = 1
_emit_round_start()
if not action_economy.notify_round_start():
_fatal("行动经济 首轮 notify_round_start 被拒")
return false
round_index = 1
_roll_round_actions() # 开轮摇号(敌人技能池批——首轮同样摇;先于 round_started,挖掘期动画位可读)
round_started.emit(round_index)
return true
@@ -471,42 +433,71 @@ func _settle() -> void:
# 作废(归因行在案);标空判定已随旗擦除自然缺席(被挖的旗不在轮末在场基线)。
_emit_row("reward_ineffective", "safe_cell", evt.cell, ri, "奖励作废(插旗格直接挖开——零惩罚零奖励)", 0, {}, "")
continue
_grant_routed(_feat_yield_adjust(snap, true), "positive", "safe_cell", evt.cell, ri, "")
# 开格流连击/深层产出(迭代 12 T5):矿脉直觉连击 = safe +1(mine 清零在负向分支);
# 深挖钻头深层产出 = 每 cap 格额外 grant 一份(收益面补偿额外 AP 成本——R4 甲非信息税对价)
_feat_combo += 1
# 深挖钻头 v2(2026-10-04 二次修正——用户实测「付 2AP 无可见提升纯挨打」:v1 配额制
# 4 次付费 8AP 换 1 份随机资源=净负期望+收益埋明细带不可感知):改**本格双份**——
# 付费挖掘的本格资源直接双份(多花 1AP 买多一份同格产出,账目直白期望微正)。
# 本轮付费次数 N=deep_paid_count−轮初快照;safe 事件按 N 上限逐格追加第二份(「深挖」)。
var dd_cost := int(_damage.feat_flags.get("deep_dig_cost", 0))
if dd_cost > 0:
var paid_this_round := int(action_economy.deep_paid_count) - _feat_paid_at_round_start
if paid_this_round > 0 and _feat_deep_given < paid_this_round:
_feat_deep_given += 1
# dig_reveal 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价:
# 乘区收集 → 正向兑付 → 计数器 → 追加产出(深挖)→ 奥术矢)
var dr_hits := _fx_emit("dig_reveal", {"cell": evt.cell, "round": ri,
"is_first_dig_of_battle": not _feat_first_dig_used,
"terrain_kind": _harmful_variants(evt.get("variant_ids", []) as Array)})
var pos_m := 0
var neg_m := 0
var combo_cap := -1
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))
if int(h.get("cap", 0)) > 0:
combo_cap = int(h.cap) # 矿脉直觉连击上限
_grant_routed(_feat_yield_adjust(snap, true, pos_m, neg_m, combo_cap), "positive", "safe_cell", evt.cell, ri, "")
# 计数器执行(连击计数——加法族,乘区收集后执行=本格兑付用旧值,与迁移前逐位一致)
for h in dr_hits:
if String(h.get("kind", "")) == "counter_add":
var set_v = h.get("set", null)
_fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
# 追加产出(深挖钻头 v2——本格双份制:grant_material 命中=配额内追加第二份;标签随行)
for h in dr_hits:
if String(h.get("kind", "")) == "grant_material":
var dd_snap = _reward_economy.grant(evt.cell)
if dd_snap is Dictionary:
_grant_routed(dd_snap, "positive", "safe_cell", evt.cell, ri, "", "(深挖)")
# 魔能共鸣·每战首次开安全格追加魔法矢(迭代 12 T2——relic 件「每关首次开格必出魔法矢」
# 落地为每战首次〔战斗域单事务无跨战状态——「每关」字面改「每战」台账披露待追认〕;
# board_typed magic 必中,扫雷结算触发点=relic 挂 first_dig_spell 承载红线①)
if bool(_damage.feat_flags.get("first_dig_spell", false)) and not _feat_first_dig_used:
_grant_routed(dd_snap, "positive", "safe_cell", evt.cell, ri, "", String(h.get("label", "")))
# 奥术矢(魔能共鸣——每战首次开安全格追加魔法矢;任意真族单次生效)
var spell_hits: Array = _hits_of(dr_hits, "spawn_spell")
if not spell_hits.is_empty() and not _feat_first_dig_used:
_feat_first_dig_used = true
var spell_t := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_first_dig_spell",
"奥术矢", "board", int(_damage.feat_flags.get("spell_value", 0)),
"奥术矢", "board", int((spell_hits[0] as Dictionary).get("value", 0)),
DamageController.TARGET_FRONT_ALIVE, ri, evt.cell, "magic")
_submit(spell_t, false)
# 掘进蓄能计数(迭代 11 同调批 T6——battle 域安全开格计数,每 charge_n 格积 1 点)
if _attune_has("dig_charge"):
_attune_safe_seen += 1
var cn_b: int = int(db.treasure_item_row("dig_charge").get("charge_n", 999))
if cn_b > 0 and _attune_safe_seen % cn_b == 0:
_attune_bank += 1
# 掘进蓄能计数(迭代 11 同调批 T6 → 内容数据驱动扩展批 T4:积点账本迁效果引擎计数器
# dig_charge_bank——行 triggers counter_spend(treasure_used) 受理闸同账本。模判定留编排器侧:
# 「每 N 格积 1 点」在发射点施加架构下无法行内表达先增后判(when 于 dispatch 预评估),
# 逐位等价优先——词汇表边界注记随 commit 留痕;charge_n 读行零字面量)
if _attune_has("dig_charge"):
_attune_safe_seen += 1
var cn_b: int = int(db.treasure_item_row("dig_charge").get("charge_n", 999))
if cn_b > 0 and _attune_safe_seen % cn_b == 0:
_fx.counter_add("dig_charge_bank", 1)
elif evt.kind == "mine":
# 插旗不可逆四情况②:插雷格再挖开 = 只受挖雷一层惩罚(mine_hit 照常兑付——惊动巢穴),
# 标中不发(旗已随挖掘擦除、不入轮末判定基线)。
_grant_routed(_feat_yield_adjust(snap, false), "negative", "mine_hit", evt.cell, ri, "")
_feat_combo = 0 # 矿脉直觉连击清零(踩雷断连——「推理密度直接奖励」的对侧)
# mine_hit 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价:
# 减半收集 → 负向兑付〔带减半系数〕→ 反打开单 → 连击清零计数器)
var mh_hits := _fx_emit("mine_hit", {"cell": evt.cell, "round": ri,
"damage": int((snap as Dictionary).get("power", 0))})
var halve_pct := 0
for h in mh_hits:
if String(h.get("kind", "")) == "damage_mult":
halve_pct += int(h.get("value", 0))
for h in mh_hits:
if String(h.get("kind", "")) == "counter_add":
var set_v = h.get("set", null)
_fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
_grant_routed(_feat_yield_adjust(snap, false, 0, 0, -1), "negative", "mine_hit", evt.cell, ri, "", "", 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", "惊巢反制·反弹", "board",
int(h.get("value", 0)), DamageController.TARGET_ALL_ALIVE, ri, evt.cell, "", 0, "", {})
_submit(nct, false)
# 地形变异 兑付段(授予段尾——触发枚举 on_open:挖开即伤、chain 通道同口径;伤害单据案C 暂存到
# 战斗段末兑付〔2026-10-06 用户裁〕;信息型 hide_number 不进结算——呈现层读取,见 board_view 数字遮蔽;
# 雷格恒无变异——布置段排除,双层叠罚不存在;插旗格直挖照吃变异伤害——地形后果同 mine_hit 口径,
@@ -521,55 +512,73 @@ func _settle() -> void:
var vt := _damage.open_ticket(DamageController.KIND_BOARD_FLAT_HERO, "地形变异", String(vrow.get("display_name", vid)), "board", \
int(vrow.get("effect_value", 0)), DamageController.TARGET_HERO, ri, evt.cell, "", 0, "", {"variant_id": String(vid)})
_submit(vt, false)
# 吞噬流四面(迭代 12 T6——地形从纯税到第三资源带;伤害仍发生=乙稿判据 3 承载):
# ①荆棘编织者=挖开伤害型地形格额外掉落一次格面资源(主动吃地形换资源)
if int(_damage.feat_flags.get("terrain_dig_double", 0)) > 0:
var tw_snap = _reward_economy.grant(evt.cell)
if tw_snap is Dictionary:
_grant_routed(tw_snap, "positive", "safe_cell", evt.cell, ri, "", "(荆棘编织)")
# ②地脉采撷=受地形伤掉落小额资源补偿(薄矿化——每次受伤兑一份)
if int(_damage.feat_flags.get("terrain_refund", 0)) > 0:
var lh_snap = _reward_economy.grant(evt.cell)
if lh_snap is Dictionary:
_grant_routed(lh_snap, "positive", "safe_cell", evt.cell, ri, "", "(地脉补偿)")
# ③熔火之躯=受地形伤积层(触发键=「受地形伤」通用不按种类特化——定稿红线承载)
if int(_damage.feat_flags.get("variant_counter_value", 0)) > 0:
_feat_magma_layers += 1
# damage_taken(kind=terrain) 发射+施加(内容数据驱动扩展批 T3——荆棘/地脉 grant_material
# 即时执行 + 熔火积层 counter_add;行序随订阅声明序〔ley 表序在 thorn 前〕,兑付值逐位一致)
var dt_hits := _fx_emit("damage_taken", {"kind": "terrain", "amount": int(vrow.get("effect_value", 0)),
"source": String(vid), "cell": evt.cell, "round": ri, "terrain_kind": String(vid)})
for h in dt_hits:
match String(h.get("kind", "")):
"grant_material":
var tw_snap = _reward_economy.grant(evt.cell)
if tw_snap is Dictionary:
_grant_routed(tw_snap, "positive", "safe_cell", evt.cell, ri, "", String(h.get("label", "")))
"counter_add":
var set_v = h.get("set", null)
_fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
for evt in _facts_flags:
_judge_flag(evt, ri)
# 战旗军规开单段(迭代 12 T3 标旗流——flag_judged 批后统一开单:轮末每面标中旗对最前存活敌
# 结算 board_typed 必中伤害〔复用零新配方;「旗所在巢穴敌」简化为 default_target 最前存活敌——
# 台账披露待追认〕;完美布防=本轮旗批全中(hit>0 且 miss==0)时旗伤 ×mult——全对推理奖励)。
# 扫雷结算触发点=flag_strike/flag_all_hit/nest_aura 挂点承载红线①;伤害单据案C 暂存战斗段末兑付。
if int(_damage.feat_flags.get("flag_strike_value", 0)) > 0:
var hits := 0
var misses := 0
# 伤害单据案C 暂存战斗段末兑付。
# flag_settle 发射+施加(内容数据驱动扩展批 T3——战旗 open_ticket + 完美布防 modify_ticket
# 改写窗口:旗伤基值×完美倍率后逐旗开单,与迁移前算序一致)
var fs_hits := 0
var fs_misses := 0
for fevt in _facts_flags:
if bool(fevt.get("hit", false)):
fs_hits += 1
else:
fs_misses += 1
var fs_fx := _fx_emit("flag_settle", {"flags_hit_all": fs_hits > 0 and fs_misses == 0,
"flags_count": fs_hits, "round": ri})
var banner_hits: Array = []
for h in fs_fx:
if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "attack":
banner_hits.append(h)
if not banner_hits.is_empty():
var strike_value := int((banner_hits[0] as Dictionary).get("value", 0))
var flag_mult := 1
for h in fs_fx:
if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "flag_mult":
flag_mult *= maxi(1, int(h.get("value", 1)))
if fs_misses == 0 and fs_hits > 0 and flag_mult > 1:
strike_value *= flag_mult # 完美布防:同轮全中旗伤倍增(全对推理奖励)
for fevt in _facts_flags:
if bool(fevt.get("hit", false)):
hits += 1
else:
misses += 1
if hits > 0:
var mult := int(_damage.feat_flags.get("flag_all_hit_mult", 1))
var strike_value := int(_damage.feat_flags.get("flag_strike_value", 0))
if misses == 0 and mult > 1:
strike_value *= mult # 完美布防:同轮全中旗伤倍增(全对推理奖励)
for fevt in _facts_flags:
if not bool(fevt.get("hit", false)):
continue
var st_t := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_flag_strike",
"战旗军规", "board", strike_value, DamageController.TARGET_FRONT_ALIVE, ri,
fevt.cell, "normal")
_submit(st_t, false)
# 熔火之躯轮末反打(迭代 12 T6 吞噬流——受地形伤积层,对全体敌 ~1 必中反伤;
# 触发键=「受地形伤」通用不按地形种类特化——定稿红线承载;反打后清零;单据案C 暂存战斗段末)
var magma := int(_damage.feat_flags.get("variant_counter_value", 0))
if magma > 0 and _feat_magma_layers > 0:
var mg_t := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_magma_body",
"熔火反噬 ×%d" % _feat_magma_layers, "board", magma * _feat_magma_layers,
DamageController.TARGET_ALL_ALIVE, ri, minesweeper.hero_pos(), "", 0, "", {})
_submit(mg_t, false)
_feat_magma_layers = 0
if not bool(fevt.get("hit", false)):
continue
var st_t := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_flag_strike",
"战旗军规", "board", strike_value, DamageController.TARGET_FRONT_ALIVE, ri,
fevt.cell, "normal")
_submit(st_t, false)
# round_end 发射+施加(内容数据驱动扩展批 T3——每轮结算收口:熔火之躯反打单开立〔open_ticket
# 即时执行〕+积层清零〔counter_add set〕;整备转蓄力由 finish_round_stop 承载——停手侧 AP 已在
# request_stop 记账,转存决策=grant_ap(mode:transfer) 订阅定夺,无订阅走 RAP 转存缺省)
var re_fx := _fx_emit("round_end", {"round": ri})
for h in re_fx:
if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "blast":
var mg_layers := _fx.counter(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, "feat_magma_body",
"熔火反噬 ×%d" % mg_layers, "board", mg_total,
DamageController.TARGET_ALL_ALIVE, ri, minesweeper.hero_pos(), "", 0, "", {})
_submit(mg_t, false)
for h in re_fx:
if String(h.get("kind", "")) == "counter_add":
var set_v = h.get("set", null)
_fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
action_economy.finish_round_stop(_fx != null and _fx.has_effect_on("round_end", "grant_ap", "transfer"))
if _facts_path == "cleared":
_grant_clear_reward(ri)
_damage.flush() # 授予段界结账(案C 后本段伤害单据全数暂存,此处对空队 no-op——保留作防御性段界)
@@ -598,31 +607,62 @@ func _settle() -> void:
return
_emit_row("verdict", "", Vector2i(-1, -1), ri, "下一轮次", 0, {}, "")
_exit_distribution()
# 深挖钻头 v2 轮初快照滚动(本轮付费次数已消费——下一轮 N=当前值−新快照)
_feat_paid_at_round_start = int(action_economy.deep_paid_count)
_settling = false
# ---- 授予段 helpers(极性路由)----
# 开格产出乘区(迭代 12 T5 开格流——贪婪矿灯/矿脉直觉/赌徒的血签;duplicate 隔离不回写快照):
# 开格产出乘区(内容数据驱动扩展批 T3 参数化——贪婪矿灯/矿脉直觉/赌徒的血签事件收集器注入):
# positive=power ×(1+静态 mult% + 矿脉直觉连击加成 min(cap,combo)×10%);negative=power ×(1+cost%)。
# 零旗=原快照原样返回(零扰动)。
func _feat_yield_adjust(snap: Dictionary, positive: bool) -> Dictionary:
var pos_m := int(_damage.feat_flags.get("dig_yield_pos_mult", 0))
var neg_m := int(_damage.feat_flags.get("dig_yield_neg_mult", 0))
if positive and pos_m <= 0 and not _damage.feat_flags.has("combo_cap"):
# 全零收集器=原快照原样返回(零扰动)。combo 读数=效果引擎 dig_combo 计数器(safe+1/mine 清零,
# 跨轮累计——与迁移前 _feat_combo 逐位一致)。
func _feat_yield_adjust(snap: Dictionary, positive: bool, pos_m: int, neg_m: int, combo_cap: int) -> Dictionary:
var combo_val := _fx.counter("dig_combo") if _fx != null else 0
if positive and pos_m <= 0 and combo_cap <= 0:
return snap
if not positive and neg_m <= 0:
return snap
var out: Dictionary = snap.duplicate()
var power := int(out.get("power", int(_reward_economy.polarity_row(String(out.get("cell_type", "")), "positive" if positive else "negative").power)))
var pct := pos_m if positive else neg_m
if positive and _damage.feat_flags.has("combo_cap"):
pct += mini(int(_damage.feat_flags.get("combo_cap", 5)), _feat_combo) * 10
if positive and combo_cap > 0:
pct += mini(combo_cap, combo_val) * 10
out["power"] = maxi(1, int(round(float(power) * (1.0 + float(pct) / 100.0))))
return out
# 伤害型变异过滤(terrain_kind fact 取数面——variation 表 effect=flat_damage 判据;
# 🔴 R4 甲永不对冲边界=仅限伤害型,信息型(hide_number 族)永不出现在本集)
func _harmful_variants(vids: Array) -> Array:
var out: Array = []
for vid_v in vids:
var vid := String(vid_v)
var vrow: Dictionary = db.variation.row_by_id(vid)
if String(vrow.get("effect", "")) == "flat_damage":
out.append(vid)
return out
# 邻接伤害型变异收集(terrain_kind fact 取数面——flag_hit 发射点用:标中旗相邻八格的伤害型变异集;
# 🔴 R4 甲永不对冲边界=仅限伤害型,信息型〔hide_number 族〕不入集)
func _harmful_adjacent(cell: Vector2i) -> Array:
var out: Array = []
var board: BoardData = minesweeper.current_board()
if board == null:
return out
for n in board.neighbors8(cell):
out.append_array(_harmful_variants(board.variant_ids(n)))
return out
# 效果命中过滤(按效果类型取命中子集——发射点分桶施加面)
func _hits_of(hits: Array, kind: String) -> Array:
var out: Array = []
for h in hits:
if String((h as Dictionary).get("kind", "")) == kind:
out.append(h)
return out
# 「动作 × 格型」矩阵的执行面:snap = 缓冲期冻结身份快照(cell_type = 该格装板期摇定的类型身份),
# polarity 由动作决定(开安全格/标中旗 = positive,开雷格/标错旗 = negative),
# 兑付行 = 奖励字典(类型, 极性)行。返回 true = percent 行实际生效(flat 行恒 false——兑付结果由行协议承载)。
@@ -646,12 +686,14 @@ func use_treasure(handle: int, target: Vector2i) -> Dictionary:
func(c: Vector2i): return not board.is_revealed(c))
if not bool(v.ok):
return v # ② 拒绝零消耗
# 掘进蓄能受理闸(迭代 11 同调批 T6——bank>0 才可发起,一行一点;积点不足拒绝零消耗)
if String(v.item_id) == "dig_charge" and _attune_has("dig_charge"):
if _attune_bank <= 0:
var cn_info: int = int(db.treasure_item_row("dig_charge").get("charge_n", 0))
return {"ok": false, "reason": "掘进蓄能:安全格积点未达(%d/%d)" % [_attune_safe_seen % cn_info if cn_info > 0 else 0, cn_info]}
_attune_bank -= 1
# treasure_used 发射+施加(内容数据驱动扩展批 T4——使用主动秘宝受理面:counter_spend 订阅=受理闸
# (掘进蓄能积点账本),消耗成功才受理;积点不足拒绝零消耗。按 item_id 特判随本批拆除)
var tu_hits := _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 _fx.counter_spend(String(h.get("counter_id", "")), int(h.get("value", 1))):
var cn_info: int = int(db.treasure_item_row("dig_charge").get("charge_n", 0))
return {"ok": false, "reason": "掘进蓄能:安全格积点未达(%d/%d)" % [_attune_safe_seen % cn_info if cn_info > 0 else 0, cn_info]}
var kinds: Array = v.kinds
if kinds.has("mine_reveal"):
treasure_inventory.apply_overlay("mine_reveal", v.resolved)
@@ -685,7 +727,7 @@ func set_treasure_unlocked(ids: Dictionary) -> void:
_reward_economy.set_treasure_unlocked(ids)
func _grant_routed(snap: Dictionary, polarity: String, source_class: String, cell: Vector2i, ri: int, highlight: String = "", label_suffix: String = "") -> bool:
func _grant_routed(snap: Dictionary, polarity: String, source_class: String, cell: Vector2i, ri: int, highlight: String = "", label_suffix: String = "", halve_pct: int = 0) -> bool:
# 迭代 11 第①批 T7——aux 授予分支随掉落/停点渠道退役拆除(reward 词表第十型 treasure_item 保留但零投放:
# 无 aux 行入池、CURIO_KINDS 词表已除名);剩余类型走极性字典路由。
var row: Dictionary = _reward_economy.polarity_row(String(snap.get("cell_type", "")), polarity)
@@ -705,18 +747,14 @@ func _grant_routed(snap: Dictionary, polarity: String, source_class: String, cel
row = row.duplicate()
row["power"] = int(round(int(row.power) * prem_pct / 100.0))
row["premium_pct"] = prem_pct
# 惊巢反制(迭代 11 同调批 T5——底稿 §3:挖开雷格该次惩罚减半 + 反弹 N;duplicate 隔离——减半
# 不回写极性字典;仅 source_class=mine_hit 受理,标错旗/负向字典行走原通道)
if polarity == "negative" and source_class == "mine_hit" and _attune_has("nest_counter"):
# 惊巢反制(迭代 11 同调批 T5 → 内容数据驱动扩展批 T4:按 id 特判拆除——减半系数=mine_hit
# damage_mult 订阅命中(行 value 50=减 50%,取整 ceil 与迁移前逐位一致);反弹单=mine_hit
# open_ticket channel reflect 订阅施加(编排器 mine_hit 施加段);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)) / 2.0))
var half: int = int(ceil(float(int(row.power)) * float(100 - halve_pct) / 100.0))
_emit_row("effect", source_class, cell, ri, "惊巢反制——挖雷惩罚减半(%d→%d)" % [int(row.power), half], 0, {"effect_id": "nest_counter"}, highlight)
row.power = half
var reflect: int = _attune_mag("nest_counter")
if reflect > 0:
var nct := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "attune_nest_counter", "惊巢反制·反弹", "board",
reflect, DamageController.TARGET_ALL_ALIVE, ri, cell, "", 0, highlight, {})
_submit(nct, false)
return _dispatch_row(row, source_class, cell, ri, highlight, label_suffix)
@@ -840,15 +878,22 @@ func _judge_flag(evt: Dictionary, ri: int) -> void:
return # 去重跳过(资格以雷格实例为键——排雷玩法 诚实重复上报的对侧);零明细行
_flag_consumed[cell] = true
_perfect_hit_true += 1
# 标中殉爆(迭代 11 同调批 T5——底稿 §3:每标中一面旗,全体存活敌吃 N 群伤;无类型走 blast 通道)
if _attune_has("flag_burst"):
# flag_hit 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价:殉爆〔T4 秘宝面〕→
# 净化改写 → 图谱额外资源 → 基线兑付)
var fh_hits := _fx_emit("flag_hit", {"cell": cell, "round": ri,
"terrain_kind": _harmful_adjacent(cell)})
# 标中殉爆(迭代 11 同调批 T5 → 内容数据驱动扩展批 T4:订阅施加——行 triggers open_ticket
# channel blast value_from magnitude;每标中一面旗全体存活敌吃 N 群伤,无类型走 blast 通道)
for h in _hits_of(fh_hits, "open_ticket"):
if String(h.get("channel", "")) != "blast":
continue
var fbt := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "attune_flag_burst", "标中殉爆", "board",
_attune_mag("flag_burst"), DamageController.TARGET_ALL_ALIVE, ri, cell, "", 0, "highlight", {})
int(h.get("value", 0)), DamageController.TARGET_ALL_ALIVE, ri, cell, "", 0, "highlight", {})
_submit(fbt, false)
# 净化工兵(迭代 12 T6 吞噬流——标中旗中和相邻伤害型地形格):🔴 R4 甲永不对冲边界=
# **仅限伤害型**(thorn/magma/rockfall——variation 表 effect=flat_damage 判据),信息型
# (hide_number 族)永不清除(红线①看门税不可对冲);中和=变普通格(BoardData.clear_variants)。
if int(_damage.feat_flags.get("flag_neutralize", 0)) > 0:
if not _hits_of(fh_hits, "rewrite_board").is_empty():
var board: BoardData = minesweeper.current_board()
for n in board.neighbors8(cell):
var vids_n: Array = board.variant_ids(n)
@@ -863,10 +908,11 @@ func _judge_flag(evt: Dictionary, ri: int) -> void:
_emit_row("terrain_neutralized", "feat_purge_sapper", n, ri, "净化工兵·地形中和", 0, {}, "")
# 雷区图谱(迭代 12 T3 标旗流——标中雷额外获得一份格面资源:插旗开始产出对标开格;
# 复用 grant 单点,额外兑付走同一 _grant_routed 正向路由)
if int(_damage.feat_flags.get("flag_resource_extra", 0)) > 0:
var extra_snap = _reward_economy.grant(cell)
if extra_snap is Dictionary:
_grant_routed(extra_snap, "positive", "flag_hit", cell, ri, "", "(雷区图谱)")
for h in fh_hits:
if String(h.get("kind", "")) == "grant_material":
var extra_snap = _reward_economy.grant(cell)
if extra_snap is Dictionary:
_grant_routed(extra_snap, "positive", "flag_hit", cell, ri, "", String(h.get("label", "")))
var snap = _reward_economy.grant(cell)
if not (snap is Dictionary):
return
@@ -941,6 +987,15 @@ func _roll_round_actions() -> void:
_emit_row("highlight", "enemy_action", Vector2i(-1, -1), round_index,
"%s 解锁了 %s" % [String(u.display_name), String(srow_u.get("display_name", sid))],
0, {"skill_id": sid}, "skill_unlock")
# enemy_hp_threshold 发射点(内容数据驱动扩展批 T1——血量阈值沿;对所有带该字段敌人逐个发射,
# 非 Boss 专属——Boss 语义留给订阅者 when 判定。pct=本次解锁条目的 unlock_hp_pct 表值)
var pct_v := 0
for pe in (u.get("pool", []) as Array):
if String((pe as Dictionary).get("skill_id", "")) == sid and (pe as Dictionary).has("unlock_hp_pct"):
pct_v = int((pe as Dictionary).unlock_hp_pct)
break
_fx_emit("enemy_hp_threshold", {"unit": String(u.enemy_id), "pct": pct_v,
"is_boss": String(encounter.get("node_type", "")) == "boss"})
var entries: Array = _enemy_system.rollable_skills(u)
if _enemy_system.basic_streak(u) >= pity_m:
var specials: Array = _enemy_system.special_skills(u)
@@ -969,10 +1024,10 @@ func _combat_segment(ri: int) -> void:
round_safe += 1
elif evt.kind == "mine":
round_mine = true
_damage.feat_flags["dig_streak"] = 0 if round_mine else round_safe
# 蓄力层注入(迭代 12 T5——整备军备「每层下轮首段 +1」/厚积薄发满层 spike/铁壁整备减伤——
# 三件同读 action_economy.charge_layers;battle 域每轮刷新)
_damage.feat_flags["charge_layers"] = action_economy.charge_layers
_damage.dig_streak = 0 if round_mine else round_safe
# 蓄力层注入(内容数据驱动扩展批 T3——整备军备「每层下轮首段 +1」/厚积薄发满层 spike/铁壁整备
# 减伤——三件 fact charge_layers 同读 action_economy;battle 域每轮刷新)
_damage.charge_layers = action_economy.charge_layers
# ⓪ 主角先手攻击(基础攻击恒存在——2026-09-11 用户裁决;先于敌方行动。案C 后授予段不再产出伤害,
# 主角进入战斗段时恒存活——先手必兑,除非场上无敌)。
# 单分量伤害构成(2026-09-19 充能退役批·用户拍板):主角攻击 = 唯一基础分量(atk,吃掷点含暴击;
@@ -987,14 +1042,14 @@ func _combat_segment(ri: int) -> void:
var ht := _damage.open_ticket(DamageController.KIND_HERO_ATTACK, "hero_attack", "主角攻击", "combat", _hero_atk, \
DamageController.TARGET_FRONT_ALIVE, ri, Vector2i(-1, -1), String(_enemy_system.hero_unit().get("attack_type", "normal")))
_damage.submit(ht)
# 连珠矢(迭代 12 T4 穿刺流——开格节奏变攻击段数):本轮每开 ~3 安全格 +1 段穿刺附加
# (上限 cap;段值 provisional 固定值不乘 atk——台账披露)。扫描雷结算触发点=dig_count_segments。
var chain_value := int(_damage.feat_flags.get("chain_value", 0))
if chain_value > 0:
var segments := mini(int(_damage.feat_flags.get("chain_cap", 2)), int(_damage.feat_flags.get("dig_streak", 0)) / 3)
# 连珠矢(内容数据驱动扩展批 T3——attack_commit 订阅施加面:开格节奏变攻击段数,本轮每开
# ~3 安全格 +1 段穿刺附加(上限 cap;段值 provisional 固定值不乘 atk——台账披露)。施加位
# 紧随主角攻击单开立=迁移前开单位(先于敌方行动单入队——flush 兑现序逐位一致)
for h in _hits_of(ht.context.get("fx_attack_hits", []) as Array, "attack_segments"):
var segments := mini(int(h.get("cap", 2)), _damage.dig_streak / maxi(1, int(h.get("divisor", 3))))
for s in segments:
var ct := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_chain_arrows",
"连珠矢", "board", chain_value, DamageController.TARGET_FRONT_ALIVE, ri,
"连珠矢", "board", int(h.get("value", 0)), DamageController.TARGET_FRONT_ALIVE, ri,
minesweeper.hero_pos(), "pierce")
_damage.submit(ct)
# 敌方行动(敌人技能池批——本拍动作开轮已摇定,此处按技能行兑现;行序 = 生成序)。
@@ -1123,14 +1178,14 @@ func _exit_distribution() -> void:
if not _install_new_board():
_fatal("换图编排失败")
return
# 工兵晨令续队(迭代 11 同调批 T5——upcoming 轮号 = round_index+1 ≤ effective_rounds 才续,P4 甲表值化)
if _attune_has("dawn_order") and round_index + 1 <= int(db.treasure_item_row("dawn_order").get("effective_rounds", 0)):
action_economy.queue_ap_delta(_attune_mag("dawn_order"))
# 工兵晨令续队直调拆除(内容数据驱动扩展批 T4——round_start 订阅行承载,见 begin_battle 注)
action_economy.notify_settlement_done(mode)
# round_start 发射先于行动经济轮切换(queue 型 grant_ap 当轮入队当轮兑现——与首轮同口径)
round_index += 1
_emit_round_start()
action_economy.notify_round_start()
_reset_round_facts()
_refresh_used_this_round = 0
round_index += 1
_roll_round_actions() # 开轮摇号——每轮恰摇一次(轮内刷新换图不重摇:refresh 不走本通道)
round_started.emit(round_index)
+194 -113
View File
@@ -60,10 +60,16 @@ var _outcomes := {} # ticket_id → "redeemed"/"zero"/"voided"(
var _seq := 0 # 流水号
var _halted := false # 主角倒下终止旗(死亡事件置位——flush 余单作废)
var _hero_ac_override := -1 # 主角护甲等级合流入参(begin_battle 显式设定——-1 = 表值兜底,口径同战斗编排器)
# 特性求值旗(迭代 12 T2——begin_battle 编排器经 FeatsEngine 求值后注入;空字典=零扰动。
# rider_flat=英雄侧必中附加值〔奥术余烬族〕/rider_crit=档二 rider 可暴击/first_dig_spell=每战首次
# 开安全格追加魔法矢〔消费在编排器授予段〕/clamp_magic_light·clamp_magic_heavy=克制点直调)
var feat_flags: Dictionary = {}
# 效果引擎(内容数据驱动扩展批 T1/T3——attack_commit/clamp_check/armor_roll/kill/damage_taken
# 发射+施加面:dispatch 返回命中,本类按组合规则施加——T3 起旧 feat_flags 旗消费点全部拆除)
var _fx: EffectEngine = 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:
@@ -79,14 +85,78 @@ func set_hero_ac_override(v: int) -> void:
_hero_ac_override = v
# 效果引擎注入(战斗编排器 _init——事件发射面接线)
func set_fx(fx: EffectEngine) -> void:
_fx = fx
# 事件发射薄面(发射+命中收集统一入口——订阅空=零循环零扰动;命中由各施加面按组合规则施加)
func _fx_hits(event: String, payload: Dictionary) -> Array:
if _fx == null:
return []
return _fx.dispatch(event, payload, Callable(self, "_fx_fact"))
# 事件发射薄面(免施加观测位——enemy_hp_threshold 等现无订阅者的事件保留零扰动面)
func _fx_dispatch(event: String, payload: Dictionary) -> void:
_fx_hits(event, payload)
# 补充事实面(fact 取值第二优先源——payload 未携带的运行态读数:charge_layers 编排器每轮注入)
func _fx_fact(fact: String):
match fact:
"charge_layers":
return charge_layers
"dig_streak":
return dig_streak
return null
# 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 供未生效行归因)
# board_flat_hero 的键并入行载荷,board_blast/board_typed 带 batch_key 供未生效行归因)。
# attack_commit 发射点(内容数据驱动扩展批 T3——我方伤害单据开立入口,任务书 §3.1 定稿位;
# 命中挂单据 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:
return {
var t := {
"kind": kind,
"source_class": source_class,
"origin_cell": origin_cell,
@@ -102,18 +172,16 @@ func open_ticket(kind: String, source_class: String, display: String, channel: S
"requires": -1, # 前置单据 id(-1 = 无依赖)
"source_unit": {}, # enemy_skill 专用——来源敌句柄(作废判定 + commit)
}
# 开单: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
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
# 同步开单(清图批专用——完美第二份与高光份数的流程依赖需要同步读数,开单权归流程 用户裁决③):
@@ -189,6 +257,8 @@ func _resolve(t: Dictionary) -> void:
# 主角先手攻击:掷点 → 折扣/暴击 → 克制点数 → hero_offense×enemy_defense 池乘区 → 下限 → 交付
# (内容数据驱动扩展批 T3——attack_commit/clamp_check/armor_roll 订阅施加面:命中=开单入口随单
# context 携带,本函数按迁移前算序逐族施加——命中序=数据表声明序,确定性)
func _resolve_hero_attack(t: Dictionary) -> void:
var target = _target_for(String(t.target_rule))
if target == null:
@@ -196,47 +266,70 @@ func _resolve_hero_attack(t: Dictionary) -> void:
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 = (t.context.get("fx_attack_hits", []) as Array)
var ac_payload: Dictionary = (t.context.get("fx_attack_payload", {}) as Dictionary)
var roll_read: Dictionary = _armor_roll()
var threshold: int = defender_ac(target) + int(db.settlement.attack_bonus)
# 矿工重锤·命中交换(迭代 12 T4 普攻流——正负双修原生形态:命中判定减成 trade_hit 点
# 〔更易命中〕换攻击增伤 trade_atk%——高 AC 负期望/低 AC 正期望)
var trade_hit := int(feat_flags.get("hit_trade_hit", 0))
# armor_roll 发射点(内容数据驱动扩展批 T1——敌护甲掷骰判定;免掷恒折扣订阅面)
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)
# 专注(迭代 12 T4 普攻流——免掷骰恒定折扣:方差买断成期望——稳定器人格;roll 记 0 归因)
var rewrite := int(feat_flags.get("armor_roll_rewrite", 0))
if rewrite > 0:
discount = rewrite
crit = false
roll_read["roll"] = 0
# 厚积薄发满层必中(迭代 12 T5 蓄力流——spike ready 时 discount 恒全额、暴击面走倍乘不加掷)
if int(feat_flags.get("spike_mult", 0)) > 0 and int(feat_flags.get("charge_layers", 0)) >= 3:
discount = 100
# 猎弱本能(迭代 12 T4 穿刺流——对轻甲敌折扣档改善 value 档:对冲 pierce 列 L1 −1 开局罚;
# 按序改写族第二拍:专注免掷骰恒定折扣(方差买断成期望——稳定器人格;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 := int(feat_flags.get("discount_shift", 0))
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)
var cdelta: int = _counter_delta(atk_type, armor) + _feat_clamp_bonus(atk_type, armor)
# 淬毒刃纹(迭代 11 同调批 T5——底稿 §3:对被克制敌人的穿刺/魔法直伤 +N;条件 = 克制成立 ×
# 攻击类型限定,主角攻击面专属——棋盘克制直伤走 _resolve_board_typed 不吃本加成)
if attune_venom > 0 and (atk_type == "pierce" or atk_type == "magic") and cdelta > 0:
cdelta += attune_venom
# 克制点数读值+订阅改写(魔网织者/湛蓝誓约/猎手刺剑 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)
# 淬毒刃纹(迭代 11 同调批 T5 → 内容数据驱动扩展批 T4:特判拆除——行 triggers clamp_check
# modify(mode:clamp_bonus) 订阅命中已并入上方 delta 合计;mode 路由=主角攻击面专享,
# 棋盘克制直伤窗只消费无模式 delta——棋盘不吃毒面与迁移前一致)
var base_value := float(t.base_value)
var trade_atk := int(feat_flags.get("hit_trade_atk", 0))
if trade_atk > 0:
base_value *= 1.0 + float(trade_atk) / 100.0 # 重锤增伤面(交换的正向侧)
# 蓄力流(迭代 12 T5):整备军备「每层下轮首段 +1」(层数×value 加算)+厚积薄发满层(≥3)
# spike 倍乘+强制必中(discount 100/crit 关闭掷骰面——方差买断的爆发出口)
var charge_layers := int(feat_flags.get("charge_layers", 0))
if charge_layers > 0:
base_value += float(charge_layers * int(feat_flags.get("ap_carry_value", 0)))
var spike_mult := int(feat_flags.get("spike_mult", 0))
var spike_ready := spike_mult > 0 and charge_layers >= 3
# 按序改写族第五拍:矿工重锤增伤面(交换的正向侧)
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
for h in ac_hits:
if String(h.get("kind", "")) == "attack_bonus":
base_value += float(int(h.get("value", 0)) * _per_value(String(h.get("per", "")), ac_payload))
# 任意真族:厚积薄发满层 spike 倍乘(≥3 层——强制必中已在折扣拍承载)
if spike_ready:
base_value *= float(spike_mult)
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)))
var rolled: int = DamageMath.rolled_component(base_value, discount, crit, int(db.settlement.crit_multiplier), cdelta)
var payload: Dictionary = {"target": String(target.display_name)}
payload[atk_type] = rolled # 分量键 = 实际攻击类型(归因字段,单分量模型)
@@ -247,53 +340,49 @@ func _resolve_hero_attack(t: Dictionary) -> void:
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(迭代 12 T2 魔法流——enemy_skill rider 对称位补齐):后置必中附加,独立于掷点
# 与乘区(奥术余烬「普攻命中后恒定结算」语义);档二(magic 4 件)rider 可暴击=当次 crit 时
# rider 值乘暴击倍率。rider 不改变下限语义(landed 含 rider 后不再过 floor)。
var hero_rider := int(feat_flags.get("rider_flat", 0))
# 猎巢人(迭代 12 T3 标旗流——巢穴控制数转伤害光环):控制数 = 敌方 weaken(削弱)实例数
# 〔八型批 side-wide 状态行——标中旗控制巢穴的既有承载;原设计「对其中敌伤害」简化为全体
# 先手攻击附加值——台账披露待追认〕,每控制 1 巢 rider +Z。
var nest_aura := int(feat_flags.get("nest_aura_value", 0))
if nest_aura > 0:
for eff in effects.query_active("enemy"):
if String(eff.effect_id) == "weaken":
hero_rider += nest_aura
# 矿工的执拗(迭代 12 T4 普攻流——「排雷排得多好,打得就有多疼」):本轮安全开格 streak
# 〔编排器授予段实时写入 feat_flags.dig_streak;踩雷清零〕×value,上限 cap。
var rdig := int(feat_flags.get("round_dig_value", 0))
if rdig > 0:
hero_rider += mini(int(feat_flags.get("round_dig_cap", 5)), int(feat_flags.get("dig_streak", 0)) * rdig)
# 加法族:英雄侧 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", "")), ac_payload)
if h.has("cap"):
contrib = mini(int(h.cap), contrib)
hero_rider += contrib
if hero_rider > 0:
if crit and bool(feat_flags.get("rider_crit", false)):
if 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 # 击杀标记在扣血后取(原语义不变)
# 碎甲震击(迭代 12 T4 普攻流——命中重甲附加「护甲裂解」):复用 off_guard(enemy percent
# +25 受伤加深——零新状态行);apply_effect 域闭/重复授予由状态容器自行守卫。
var crack := int(feat_flags.get("crack_value", 0))
if crack > 0 and armor == "heavy":
effects.apply_effect("off_guard", {"source_class": "feat_armor_crack"}, -1)
payload["cracked"] = true
# kill 发射+施加(内容数据驱动扩展批 T3——敌人死亡确认;贯穿射击溢出弹射订阅面:对下一存活敌
# 以穿刺类型弹射本体 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", "")) != "overflow":
continue
var next_target = units.default_target()
if next_target != null:
var of_t := open_ticket(DamageController.KIND_BOARD_TYPED, "feat_pierce_shot",
"贯穿射击", "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)
# 即时执行族:碎甲震击打重甲附加「护甲裂解」(复用 off_guard——零新状态行;apply_effect 域闭/
# 重复授予由状态容器自行守卫)
for h in ac_hits:
if String(h.get("kind", "")) == "apply_status":
effects.apply_effect(String(h.get("status_id", "")), {"source_class": "feat_armor_crack"}, -1)
payload["cracked"] = true
var label := _attack_label(String(t.display), crit, discount)
if hero_rider > 0:
label += "+奥术%d(必中)" % hero_rider
emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), label, landed, payload, String(t.highlight))
# 贯穿射击(迭代 12 T4 穿刺流——击杀溢出弹射):击杀时对下一存活敌以穿刺类型弹射
# 本体半额〔原设计「溢出伤害」追踪在单分量模型不可行——半额弹射 provisional 裁定披露待追认;
# 无次敌=弹射落空〕。B 模式入队段尾 flush 兑现;A 模式递归 resolve(目标存活守卫内建)。
if bool(payload.get("killed", false)) and int(feat_flags.get("kill_overflow", 0)) > 0:
var next_target = units.default_target()
if next_target != null:
var of_t := open_ticket(DamageController.KIND_BOARD_TYPED, "feat_pierce_shot",
"贯穿射击", "board", maxi(int(db.settlement.damage_floor), int(landed / 2)),
DamageController.TARGET_FRONT_ALIVE, int(t.round_index), t.origin_cell, "pierce")
if policy == POLICY_SEGMENT:
_queue.append(of_t)
else:
_resolve(of_t)
_outcomes[int(t.ticket_id)] = "redeemed"
@@ -308,11 +397,14 @@ func _resolve_enemy_skill(t: Dictionary) -> void:
var rolled: int = DamageMath.rolled_component(float(t.base_value), discount, crit, int(db.settlement.crit_multiplier), 0)
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 后置于伤害下限——独立必中加项,不进乘区
# 铁壁整备(迭代 12 T5 蓄力流——每层下轮开场 S 点减伤;减后过下限保底)
var shield := int(feat_flags.get("shield_per_layer", 0)) * int(feat_flags.get("charge_layers", 0))
# 稳健拓荒(迭代 12 T10——Fork posture B 侧 rule 键:受击减 1;两侧对称弱值=「不同强非强弱」)
if int(feat_flags.get("bulwark", 0)) > 0:
shield += 1
# damage_taken 发射+施加(内容数据驱动扩展批 T3——kind=enemy_skill;铁壁整备减伤订阅面:
# shield 加法族合计 value×per(charge_layers),稳健拓荒树规则同面(rebuild 合成订阅);
# 减后过下限保底)
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)}):
if String(h.get("kind", "")) == "shield":
shield += int(h.get("value", 0)) * _per_value(String(h.get("per", "")), {})
if shield > 0:
total = maxi(int(db.settlement.damage_floor), total - shield)
units.apply_damage(hero, total)
@@ -337,11 +429,20 @@ func _resolve_board_typed(t: Dictionary) -> void:
_ineffective_row(t, "无存活敌人")
_outcomes[int(t.ticket_id)] = "zero"
return
var delta: int = _counter_delta(String(t.attack_type), String(target.get("armor_type", "light"))) + _feat_clamp_bonus(String(t.attack_type), String(target.get("armor_type", "light")))
var dealt: int = DamageMath.counter_component(int(t.base_value), delta, int(db.settlement.damage_floor))
var armor_b: String = String(target.get("armor_type", "light"))
var atk_b: String = String(t.attack_type)
# clamp_check 发射+施加(内容数据驱动扩展批 T3——克制判定修正点棋盘面;与 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": delta}, String(t.highlight))
{"target": String(target.display_name), "attack_type": String(t.attack_type), "effectiveness": cdelta}, String(t.highlight))
_outcomes[int(t.ticket_id)] = "redeemed"
@@ -418,23 +519,6 @@ func _counter_delta(attack_type: String, armor: String) -> int:
return int(row.get(armor, 0))
# 克制点直调(迭代 12 T2 魔法流/T4 穿刺流 clamp_modifier 件——克制矩阵查表值后的特性调整;
# 空旗=0 零扰动):魔网织者=magic vs magic(矩阵罚 −1 → 0/正);湛蓝誓约=magic vs heavy(+1 → +2);
# 猎手刺剑=pierce vs light(+1——对轻甲敌穿刺提升,对重甲无增益=正负双修)。hero_attack 与
# board_typed 两消费面同享。
func _feat_clamp_bonus(attack_type: String, armor: String) -> int:
var bonus := 0
if attack_type == "magic":
if armor == "magic":
bonus += int(feat_flags.get("clamp_magic_magic", 0))
elif armor == "heavy":
bonus += int(feat_flags.get("clamp_magic_heavy", 0))
elif attack_type == "pierce":
if armor == "light":
bonus += int(feat_flags.get("clamp_pierce_light", 0))
return bonus
# 折扣档改善(迭代 12 T4 猎弱本能——折扣表向全额方向移 value 档;查不到当前档 = 不动防御口径)
func _discount_shift_up(discount: int, shift: int) -> int:
var tbl: Array = db.settlement.roll_tier_discounts
@@ -444,9 +528,6 @@ func _discount_shift_up(discount: int, shift: int) -> int:
return int(tbl[mini(tbl.size() - 1, idx + shift)])
# 淬毒刃纹 N(迭代 11 同调批 T5——战斗结算 begin_battle 注入;0 = 未装配零扰动)
var attune_venom: int = 0
# 池内加算 → clamp [-100,+100](池间乘算归 DamageMath.pool_damage 调用侧)
func clamped_pool(pool: String) -> int:
var total := 0
+343
View File
@@ -0,0 +1,343 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Date: 2026-10-06(迭代 17 卡⓪ 内容数据驱动扩展批 T1——效果引擎框架)
# 内容数据驱动扩展(任务书 design/research/任务书-内容数据驱动扩展批-2026-10-06.md;设计权威
# design/research/研究-内容数据驱动扩展架构-2026-10-06.md):把「内容的效果行为」从散布在结算
# 代码里的硬编码分支搬进内容数据行三个字段——订阅哪个事件(on)/ 什么条件(when)/ 什么效果(do)
# ——由本解释器统一执行。完成后加一件「已有效果类型」的新专长/新秘宝=只改 .tres 数据行,零代码。
# 架构模型(R1 裁决):发布订阅——结算管线同步 emit 事件(无队列无异步,确定性优先)→ 本引擎遍历
# 持有订阅集匹配 → 返回按声明序排列的效果命中(do 列表),由发射点按组合规则施加。
# 多订阅与冲突(R3 裁决):一事件多订阅=常态;冲突在效果类型层用组合规则封死(§3.4 六族);遍历序
# =数据表声明序(feats 表序→treasure 表序→树规则合成行,装载时定死),写死保种子复放。
# 重建时机(v1.1 补):每战 begin_battle 从持有集/装备集重建 trigger 列表——冒险中途获得/换取的
# 专长下一战生效,同战内不热载,确定性优先。计数器账本随重建清零(battle 域)。
# 确定性契约:全同步调用、零 RNG、零 signal 时序依赖;同输入同命中序。
# 护栏保留:事件/效果/事实/算子四词表封闭(本文件常量),行内出域=加载期校验致命(schema 侧),
# 运行期 dispatch 对未知事件/效果类型 fail-fast(push_error+断言)。
class_name EffectEngine
extends RefCounted
## 事件词汇(16 个发射点——任务书 §3.1 定稿;4 个登记暂不做事件不入本表)
const EVENTS := ["dig_reveal", "dig_paid", "flag_hit", "mine_hit", "attack_commit", "clamp_check",
"armor_roll", "kill", "damage_taken", "enemy_hp_threshold", "round_start", "flag_settle",
"round_end", "reward_roll", "treasure_used", "charge_full"]
## 扫雷侧事件集(🔴 流派扫雷触发铁律新形态:每流派 ≥1 行订阅本集事件——schema 加载校验。
## 原 MINE_HOOKS 的同义迁移:挖掘/标旗/轮末结算面事件)
const MINE_EVENTS := ["dig_reveal", "dig_paid", "flag_hit", "mine_hit", "flag_settle", "round_end", "reward_roll"]
## 效果类型词汇(解释器 switch 闭集——任务书 §3.3 定稿 19 行;类型实数 24,含共享行拆型:
## grant_gp/bias_wallet、counter_add/counter_spend、reveal_tiles/remove_mines/refresh_pick、
## bias_rewards/preview。任务书标题计 23 系笔误,落码按行内容全收——commit 留痕)
const EFFECT_KINDS := ["add_ticket_rider", "modify_ticket", "damage_mult", "open_ticket",
"attack_bonus", "attack_segments", "yield_mult", "spawn_spell", "overflow", "grant_material",
"grant_ap", "grant_gp", "bias_wallet", "shield", "spike", "apply_status", "counter_add",
"counter_spend", "rewrite_board", "reveal_tiles", "remove_mines", "refresh_pick",
"bias_rewards", "preview"]
## fact 条件词汇(封闭枚举 9 项——任务书 §四定稿)
const FACTS := ["matchup", "is_first_dig_of_battle", "terrain_kind", "target_armor_class",
"flags_count", "charge_layers", "nest_count", "round_index", "counter"]
## when 算子词汇(封闭——fact 查询面落法随批登记:mod_zero=计数器取模判满〔掘进蓄能〕、
## advantageous=克制矩阵正面判定〔淬毒刃纹〕、gte_ref/lt_ref=与另一计数器比较〔深挖钻头配额〕、
## ends_with=后缀匹配〔对某甲族的泛条件〕。均系映射表现状条件装进 9 项 fact 的查询面落法,
## 代拍板白名单覆盖,commit 留痕)
const OPS := ["eq", "neq", "gt", "gte", "lt", "lte", "ends_with", "has", "mod_zero",
"advantageous", "gte_ref", "lt_ref"]
## when 条件键白名单(trigger 行 when 数组元素键)
const WHEN_KEYS := ["fact", "op", "value", "id", "ref", "value_from"]
## do 效果键白名单(trigger 行 do 字典键——零裸 flat 同族护栏:kind 外键全部参数引用面)
const DO_KEYS := ["kind", "value", "value_from", "delta", "delta_from", "mult", "mult_from",
"cap", "cap_from", "cost_mult", "per", "divisor", "dtype", "mode", "channel", "counter_id",
"reset_at", "set", "status_id", "label", "fallback", "target_kinds", "trade_atk", "trade_hit"]
## 主动件分发通道(不经事件订阅——刷新/显影/移除是玩家主动动作;use_treasure 按 kind 分发)
const ACTIVE_KINDS := ["reveal_tiles", "preview", "grant_ap", "remove_mines", "refresh_pick"]
# ---- 订阅集与计数器账本 ----
var _triggers: Array = [] # [{source, id, on, when, do}](装载序=声明序,重建时定死)
var _counters: Dictionary = {} # counter_id → int(battle 域账本;重建清零;重置点由订阅行声明)
var _reset_on_round_start: Array[String] = [] # round_start 重置点计数器名单(装载时收集)
var db: ConfigDb # 矩阵/表读取(advantageous 算子消费)——可空(纯计数器用法零依赖)
func _init(db_ref: ConfigDb = null) -> void:
db = db_ref
## 订阅装载(每战 begin_battle 调用——重建时机 v1.1):feats 表序→treasure 表序(各按持有/装备
## 集过滤)→树规则合成行。同一数据行的多个 trigger 按行内数组序。计数器账本同步清零并按订阅行
## 声明收集重置点(round_start)。
func rebuild(feat_ids: Array, item_ids: Array, tree_rules: Array = []) -> void:
_triggers.clear()
_counters.clear()
_reset_on_round_start.clear()
if db != null and db.feats != null:
for row in db.feats.rows:
var fid := String(row.get("feat_id", ""))
if not feat_ids.has(fid):
continue
for tr in (row.get("triggers", []) as Array):
_push_trigger("feat", fid, tr, row)
if db != null:
for row in db.treasure_attunement_rows():
var iid := String(row.get("item_id", ""))
if not item_ids.has(iid):
continue
for tr in (row.get("triggers", []) as Array):
_push_trigger("attune", iid, tr, row)
# 树规则合成订阅(T5——规则键效果走同面:非数据行,词表同闭集)
for key_v in tree_rules:
var rk := String(key_v)
match rk:
"posture_first_strike":
_push_trigger("tree", rk, {"on": "round_start",
"when": [{"fact": "round_index", "op": "eq", "value": 1}],
"do": {"kind": "grant_ap", "mode": "queue", "value": 1}}, {})
"posture_bulwark":
_push_trigger("tree", rk, {"on": "damage_taken",
"do": {"kind": "shield", "value": 1}}, {})
func _push_trigger(source: String, id: String, tr: Dictionary, row: Dictionary) -> void:
var on := String(tr.get("on", ""))
assert(EVENTS.has(on), "[效果引擎] 订阅事件出域: %s(%s %s)" % [on, source, id])
var do_d: Dictionary = (tr.get("do", {}) as Dictionary).duplicate(true)
# *_from 解析(rebuild 时把行字段引用定影为值——value_from:"magnitude" → value=行 magnitude)
for pair in [["value_from", "value"], ["delta_from", "delta"], ["mult_from", "mult"], ["cap_from", "cap"]]:
if do_d.has(pair[0]):
do_d[pair[1]] = int(row.get(String(do_d[pair[0]]), 0))
do_d.erase(pair[0])
# when value_from 同面解析(行字段引用条件——dawn_order effective_rounds 先例:字面量出行=数值
# 单一事实源破面,行引用随 rebuild 定影)
var when_d: Array = (tr.get("when", []) as Array).duplicate(true)
for c_v in when_d:
var c: Dictionary = c_v as Dictionary
if c.has("value_from"):
c["value"] = int(row.get(String(c["value_from"]), 0))
c.erase("value_from")
var t := {"source": source, "id": id, "on": on,
"when": when_d,
"do": do_d}
_triggers.append(t)
# 计数器重置点收集(counter_add/counter_spend 行声明 reset_at)
if do_d.has("counter_id") and String(do_d.get("reset_at", "")) == "round_start":
var cid := String(do_d.counter_id)
if not _reset_on_round_start.has(cid):
_reset_on_round_start.append(cid)
## 订阅查询面(发射点缺省行为仲裁用——如 round_end 无 transfer 订阅时走 RAP 转存)
func has_effect_on(event: String, kind: String, mode: String = "") -> bool:
for tr in _triggers:
if String(tr.on) != event:
continue
var d: Dictionary = tr.do
if String(d.get("kind", "")) == kind and (mode == "" or String(d.get("mode", "")) == mode):
# 有 when 时不能预判命中(发射点缺省行为保守按「可能命中」计——命中由 dispatch 定夺)
return true
return false
## 事件分发(发射点唯一入口):返回命中效果列表(装载序=声明序;零命中=空数组零扰动)。
## payload = 事件现场(fact 取值第一优先源);facts = 补充事实面(charge_layers/nest_count 等
## 运行态读数 Callable——payload 未带时回落)。
func dispatch(event: String, payload: Dictionary, facts: Callable = Callable()) -> Array:
assert(EVENTS.has(event), "[效果引擎] 未知事件: %s(发射点出域)" % event)
if event == "round_start":
for cid in _reset_on_round_start:
_counters[cid] = 0
var hits: Array = []
for tr in _triggers:
if String(tr.on) != event:
continue
if not _when_ok(tr.when, payload, facts):
continue
hits.append(tr.do)
return hits
## 计数器读数(fact counter 与发射点配额面共用)
func counter(counter_id: String) -> int:
return int(_counters.get(counter_id, 0))
func counter_add(counter_id: String, value: int, set_mode: bool = false) -> void:
if set_mode:
_counters[counter_id] = value
else:
_counters[counter_id] = int(_counters.get(counter_id, 0)) + value
## 计数器消耗(counter_spend 组合规则——按订阅序消耗共享资源,耗尽即止):
## 返回 true=消耗成功;false=资源不足(调用方按 reason 通道拒绝)。
func counter_spend(counter_id: String, value: int) -> bool:
var cur := int(_counters.get(counter_id, 0))
if cur < value:
return false
_counters[counter_id] = cur - value
return true
# ---- when 判定(条件谓词——零随机全确定) ----
func _when_ok(when: Array, payload: Dictionary, facts: Callable) -> bool:
for c_v in when:
if not _cond_ok(c_v as Dictionary, payload, facts):
return false
return true
func _cond_ok(c: Dictionary, payload: Dictionary, facts: Callable) -> bool:
var fact := String(c.get("fact", ""))
var op := String(c.get("op", "eq"))
var val = c.get("value", null)
var actual = _fact_value(fact, c, payload, facts)
match op:
"eq":
return str(actual) == str(val)
"neq":
return str(actual) != str(val)
"gt":
return int(actual) > int(val)
"gte":
return int(actual) >= int(val)
"lt":
return int(actual) < int(val)
"lte":
return int(actual) <= int(val)
"ends_with":
if val is Array:
for v in (val as Array):
if String(actual).ends_with(String(v)):
return true
return false
return String(actual).ends_with(String(val))
"has":
# 在场判定(fact 查询面落法——代拍板白名单,commit 留痕):value 缺省=非空判定
# (terrain_kind fact:伤害型变异集非空=挖开伤害型地形格/邻格有伤害型地形/地形伤种类在场);
# value 给定=数组包含判定(保留泛用面)。
if c.has("value"):
if actual is Array:
return (actual as Array).has(val)
return false
if actual is Array:
return not (actual as Array).is_empty()
return String(actual) != ""
"mod_zero":
var m := int(val)
return m > 0 and int(actual) % m == 0
"advantageous":
# 克制矩阵正面判定(matchup = "{attack_type}_vs_{armor}"——矩阵值 > 0 = 克制成立)
return _counter_delta_of(String(actual)) > 0
"gte_ref":
return int(actual) >= counter(String(c.get("ref", "")))
"lt_ref":
return int(actual) < counter(String(c.get("ref", "")))
assert(false, "[效果引擎] when 算子出域: %s" % op)
return false
func _fact_value(fact: String, c: Dictionary, payload: Dictionary, facts: Callable):
match fact:
"counter":
return counter(String(c.get("id", "")))
_:
# fact 取值序:payload 同名字段优先 → 补充事实面(发射点注入的运行态读数)
if payload.has(fact):
return payload[fact]
if facts.is_valid():
return facts.call(fact)
return null
## 克制矩阵读值(advantageous 算子消费——与伤害控制器 _counter_delta 同源同表)
func _counter_delta_of(matchup: String) -> int:
if db == null or matchup == "" or not matchup.contains("_vs_"):
return 0
var parts := matchup.split("_vs_")
if parts.size() != 2:
return 0
var tbl: Dictionary = db.settlement.armor_effectiveness
var row: Dictionary = tbl.get(String(parts[0]), {})
return int(row.get(String(parts[1]), 0))
## ---- 加载期校验(schema 侧共用——单一事实源;悬空引用/词表出域=致命) ----
## trigger 行校验:返回该行是否含扫雷侧事件订阅(流派铁律聚合面)。
## row 用于 value_from 类引用的字段存在性判定(悬空=加载致命)。
static func validate_trigger_row(rid: String, row: Dictionary, triggers: Array, errs: Array) -> bool:
var mine_covered := false
for tr_v in triggers:
var tr: Dictionary = tr_v as Dictionary
for k in tr.keys():
if not ["on", "when", "do"].has(String(k)):
errs.append({"category": "zero_flat", "message": "%s trigger 键出白名单: %s" % [rid, String(k)]})
var on := String(tr.get("on", ""))
if not EVENTS.has(on):
errs.append({"category": "enum", "message": "%s 订阅事件出域(事件词汇封闭): %s" % [rid, on]})
elif MINE_EVENTS.has(on):
mine_covered = true
for c_v in (tr.get("when", []) as Array):
var c: Dictionary = c_v as Dictionary
for k in c.keys():
if not WHEN_KEYS.has(String(k)):
errs.append({"category": "zero_flat", "message": "%s when 键出白名单: %s" % [rid, String(k)]})
var fact := String(c.get("fact", ""))
if not FACTS.has(fact):
errs.append({"category": "enum", "message": "%s fact 出域(事实词汇封闭 9 项): %s" % [rid, fact]})
if not OPS.has(String(c.get("op", "eq"))):
errs.append({"category": "enum", "message": "%s when 算子出域: %s" % [rid, String(c.get("op", ""))]})
var do_d: Dictionary = tr.get("do", {}) as Dictionary
for k in do_d.keys():
if not DO_KEYS.has(String(k)):
errs.append({"category": "zero_flat", "message": "%s do 键出白名单: %s(机制轴禁无条件属性加项)" % [rid, String(k)]})
var kind := String(do_d.get("kind", ""))
if not EFFECT_KINDS.has(kind):
errs.append({"category": "enum", "message": "%s 效果类型出域(解释器闭集): %s" % [rid, kind]})
# value_from/delta_from/mult_from/cap_from 引用面:行字段存在性(悬空=加载致命)——
# do 与 when 两侧同面(when value_from=dawn_order effective_rounds 先例)
for vf_k in ["value_from", "delta_from", "mult_from", "cap_from"]:
if do_d.has(vf_k):
var fld := String(do_d.get(vf_k, ""))
if fld != "" and not row.has(fld):
errs.append({"category": "dangling", "message": "%s do.%s 悬空(行无字段 %s)" % [rid, vf_k, fld]})
for c_v in (tr.get("when", []) as Array):
var c_w: Dictionary = c_v as Dictionary
if c_w.has("value_from"):
var wfld := String(c_w.get("value_from", ""))
if wfld != "" and not row.has(wfld):
errs.append({"category": "dangling", "message": "%s when.value_from 悬空(行无字段 %s)" % [rid, wfld]})
return mine_covered
## 组合规则(任务书 §3.4 六族——效果类型的必填属性,发射点施加面引用)
## 加法族(数值相加,可交换):add_ticket_rider、attack_bonus、grant_material、grant_ap(非 transfer)、
## counter_add、shield——发射点逐条累加收集器。
## 乘法族(收集后一次施加,正负分桶):yield_mult、damage_mult。
## 任意真族(任一命中即生效):spawn_spell、overflow、spike。
## 按序改写族(收集→单点按声明序施加):modify_ticket 及一切改写窗口效果。
## 按序消耗族(按订阅序消耗共享资源,耗尽即止;可选 fallback):counter_spend。
## 即时执行族(每条命中各自执行一次,执行序=声明序):open_ticket、grant_ap(mode:transfer)、
## rewrite_board、apply_status——发射点逐条执行。
static func combo_of(kind: String) -> String:
match kind:
"add_ticket_rider", "attack_bonus", "grant_material", "counter_add", "shield":
return "additive"
"grant_ap":
return "additive" # mode:transfer 由发射点即时执行族承载(转移非加法)
"yield_mult", "damage_mult":
return "multiplicative"
"spawn_spell", "overflow", "spike":
return "any_true"
"modify_ticket":
return "ordered_write"
"counter_spend":
return "ordered_spend"
"open_ticket", "rewrite_board", "apply_status":
return "immediate"
return "immediate" # 默认即时执行(每条命中各自执行一次)
+1
View File
@@ -0,0 +1 @@
uid://dgo4b5frip3e5
+2 -15
View File
@@ -30,21 +30,8 @@ static func tiers_for(feat_ids: Array, table: FeatTable) -> Dictionary:
return out
## 挂点投影:持有集 → [{feat_id, hook, archetype, tier, effects}](表序稳定——同集合同投影,
## MC 逐位可复放承载)
static func hooks_for(feat_ids: Array, table: FeatTable) -> Array:
var out: Array = []
for rid in feat_ids:
var row: Dictionary = table.row_by_id(String(rid)) if table != null else {}
if row.is_empty():
continue
out.append({
"feat_id": String(row.get("feat_id", "")),
"hook": String(row.get("hook", "")),
"archetype": String(row.get("archetype", "")),
"effects": (row.get("effects", {}) as Dictionary).duplicate(true),
})
return out
## 挂点投影随 T3 退役(内容数据驱动扩展批——hook 词族消亡:订阅投影=效果引擎 rebuild 装载序
## (feats 表序),投影稳定性由 EffectEngine._triggers 声明序承载)
## 成型判读(读数面口径):流派 tier ≥1 = 成型(档一即判——任务书完成定义「标签+档位激活引擎可判成型」)
+19 -6
View File
@@ -23,6 +23,12 @@ var _granted := {} # Vector2i → 快照(重复受理幂等返
var _clear_identity := ""
var _clear_granted := false
var _clear_snapshot := {}
# 效果引擎(内容数据驱动扩展批 T1——reward_roll 发射面;偏运硬币 bias_rewards 订阅面)
var _fx: EffectEngine = null
func set_fx(fx: EffectEngine) -> void:
_fx = fx
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
@@ -59,7 +65,14 @@ func roll_board(eligible: Array) -> bool:
_clear_granted = false
_clear_snapshot = {}
var stream: RandomNumberGenerator = rng.open_stream("reward_roll") # 恰一 scope 批(格批 + 清图批尾 + 区间行追加取号)
var cell_rows: Array = _biased_cell_rows() # cell 池正向行 weight>0(偏运硬币装配时稀有正向 kinds 权重 +N——池结构零触碰)
# reward_roll 发射+施加(内容数据驱动扩展批 T4——格面资源掷定、摇号前权重合成层:
# bias_rewards 订阅命中合计=偏运权重加算(行 value_from magnitude 定影);零命中=原行集零扰动)
var luck_bias := 0
if _fx != null:
for h in _fx.dispatch("reward_roll", {"polarity": "positive", "cell_count": list.size()}):
if String(h.get("kind", "")) == "bias_rewards":
luck_bias += int(h.get("value", 0))
var cell_rows: Array = _biased_cell_rows(luck_bias) # cell 池正向行 weight>0(偏运硬币装配时稀有正向 kinds 权重 +N——池结构零触碰)
var clear_rows: Array = _pool_rows("clear_default")
# ① 身份批:每非封锁格恰一类型身份(雷安同池——同一行集同一权重分布)
var draft := {}
@@ -174,8 +187,8 @@ func polarity_row(cell_type: String, polarity: String) -> Dictionary:
# ---- F1 选行算子纯函数 ----
# 偏运硬币 N(迭代 11 同调批 T5——战斗结算 begin_battle 注入;0 = 未装配零扰动)
var luck_bias: int = 0
# 偏运硬币 N(迭代 11 同调批 T5 → 内容数据驱动扩展批 T4:begin_battle 注入拆除——
# reward_roll bias_rewards 订阅命中合计经 _biased_cell_rows 入参;0 = 未装配零扰动)
# 稀有正向资源 kinds(底稿 §3——权重投影面,池结构零触碰)
const LUCK_BIAS_KINDS := ["heal", "gp", "pierce", "magic"]
@@ -192,15 +205,15 @@ const SKEW_UP_KINDS := ["attack", "blast", "pierce", "magic"]
# 偏运硬币权重投影(底稿 §3:只动池内权重、雷安仍同池同分布;luck_bias=0 逐位返回原行集——off 态零扰动)
# 基座换源(首发批 2026-10-04):先经图级偏斜投影再叠件级加法——合成序锚=图级先、件级后
#(环境事实先定型、玩家构筑叠其上;两投影均为池内权重面,雷安同池与取号次数零变化)
func _biased_cell_rows() -> Array:
func _biased_cell_rows(bias: int) -> Array:
var base: Array = _skewed_cell_rows()
if luck_bias <= 0:
if bias <= 0:
return base
var out: Array = []
for row in base:
if LUCK_BIAS_KINDS.has(String(row.get("cell_type", ""))):
var biased: Dictionary = row.duplicate()
biased["weight"] = int(row.weight) + luck_bias
biased["weight"] = int(row.weight) + bias
out.append(biased)
else:
out.append(row)
+17
View File
@@ -80,6 +80,23 @@ const TIER_QUOTAS := {"common": 70, "rare": 25, "rarest": 3}
@export var feat_ticket_pity_step: int = 5 # [1,99] knob(口径定死=5)——保底步数(连续未出展示次数)
## 令牌兑付路由表(内容数据驱动扩展批 T5——_apply_curio_pick match 收敛进效果类型:
## 属性值提升四类 = 「加算落点键」声明路由,加新加算类令牌 = 表行+配额/件值键,零代码;
## 通道面三类(gp 钱包/heal 台账/refresh 界面余量)+relic 持有集 = 副作用通道显式分支不进本表
## ——路由表只承载「纯加算落点」形态,与效果引擎组合规则加法族同构)
const CURIO_APPLY_ROUTES := {
"attack": "atk",
"max_hp": "max_hp",
"armor": "armor_class",
"ap": "ap",
}
## 令牌加算路由读数(空 = 非加算形态——调用方走通道面分支)
func curio_apply_route(kind: String) -> String:
return String(CURIO_APPLY_ROUTES.get(kind, ""))
## 令牌配额读数(未列 kind×tier 组合 = 0——heal 无普通档/ap 无普通稀有档按 v2.2 构成)
func curio_quota(kind: String, tier: String) -> int:
match [kind, tier]:
+31 -74
View File
@@ -1,7 +1,8 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 七核心流派各 ≥3 件入局内随机渠道后,标签+档位激活引擎能否机械判「成型」、
# 专长带能否挂进令牌池稀有/罕见档随机投放(P0——迭代 12 双层 BD 实施批 T1)
# Date: 2026-10-04(双层 BD 实施批 T1——任务书五裁定稿;设计权威 双层BD体系-整合定稿-2026-10-03 v3)
# Date: 2026-10-06(内容数据驱动扩展批 T3——行换代:hook/effects 挂点形态退役,triggers 事件订阅形态
# 唯一合法行形——旧 23 挂点枚举消亡:事件名+效果类型取代,任务书 R4 裁决)
# 概念指路:本表的专长,与 adventure.tres 的属性值提升、资源,同属一个对象——
# 「奇物」(一次冒险内各渠道给予玩家的增益总称)。定义见 design/gdd/systems/冒险循环/奇物.md。
# 它们是它分类轴上的枚举(专长 / 属性值提升 / 资源),不是三套系统;本表承载「专长」(专长 Feat)的逐项定义。
@@ -11,16 +12,15 @@
# 数据配置 FeatTable schema(奇物·专长定义表;行 = Dictionary 键 feat_id,Rule 4 豁免
# 定义表——TreasureItemTable 先例)。
# 行字段:feat_id 主键 / display_name / rarity ∈ {common, rare, rarest}(对齐令牌池三档命名——
# 投放档映射:common/rare→稀有令牌档、rarest→罕见令牌档;relic 令牌永不入普通档=「挂稀有/罕见档」
# 追认 b 接线口径)/ archetype ∈ ARCHETYPES 七流派标签 / hook ∈ HOOKS 挂点枚举(结算段槽——整合定稿
# §2.1 哈迪斯恩赐槽同构)/ effects Dictionary(hook 消费参数,provisional 投值)。
# 正负双修(整合定稿 B2 判据):effects 允许负向键(cost_* 族)——纯增益件须专项论证(反例 B-3),加载期
# 仅校验键域不禁纯增益(专项论证件以 positive_only 批注登记,人工清单呈报)。
# 投放档映射:common/rare→稀有令牌档、rarest→罕见令牌档;relic 令牌永不入普通档)/
# archetype ∈ ARCHETYPES 七流派标签 / desc 效果说明(文案表值化——呈现层读行,零硬编码)/
# triggers 订阅行数组(订阅哪个事件 on / 什么条件 when / 什么效果 do——效果引擎 EffectEngine
# 统一解释执行;四词表封闭校验=EffectEngine.validate_trigger_row 单一事实源)。
# 档位激活(整合定稿 设计工具 1——以撒套装/小骨铭文):同标签计数 ≥2→档一(流派核心规则)/≥4→档二
# (完全体);散件单体保底=每件效果独立生效,档位只做质变(反例 B-2)——引擎纯函数 FeatsEngine。
# 🔴 层级纪律(任务书红线 2):专长只走局内随机渠道(三选一/商店/格面)——本表行永不入同调位
# (treasure_items 表)/局外树(T8 节点表);扫雷触发点铁律:每流派 ≥1 行 hook ∈ MINE_HOOKS(加载校验,
# 违反=红线①触碰面)。
# (treasure_items 表)/局外树(T8 节点表);扫雷触发点铁律:每流派 ≥1 行订阅扫雷侧事件
# (EffectEngine.MINE_EVENTS——加载校验,违反=红线①触碰面)。
class_name FeatTable
extends Resource
@@ -32,58 +32,20 @@ const ARCHETYPES := ["magic", "pierce", "normal", "dig", "flag", "charge", "devo
## 稀有度词表(对齐令牌池三档;投放档映射 relic→rare / rarest→rarest)
const RARITIES := ["common", "rare", "rarest"]
## 挂点枚举(结算段槽——首批 23 点位;每 hook 恰一流派域消费,T2-T6 接线)
const HOOKS := [
# 魔法流
"hero_attack_rider", # 普攻命中后追加必中魔法伤(奥术余烬/魔能共鸣)
"clamp_modifier", # 克制点直调(魔网织者/湛蓝誓约/猎手刺剑)
"first_dig_spell", # 每关首次开格必出魔法矢(魔能共鸣第二效果)
# 穿刺流
"kill_overflow", # 击杀溢出弹射(贯穿射击)
"dig_count_segments", # 开格计数转攻击段数(连珠矢)
"roll_discount", # 折扣档改写(猎弱本能前移一档)
# 普攻流
"round_dig_bonus", # 轮内安全开格数转攻击加成(矿工的执拗;踩雷清零=mine_hit 监听同口)
"hit_trade", # 命中交换(矿工重锤——正负双修原生形态)
"armor_roll_rewrite", # 免掷骰恒折扣(专注)
"heavy_debuff", # 命中重甲附加护甲裂解(碎甲震击)
# 开格流
"dig_yield_mult", # 开格产出乘区(贪婪矿灯/矿脉直觉/赌徒的血签——正负双向)
"deep_dig", # 深层资源掘取(深挖钻头——R4 甲非信息税形态:开掘额外 AP 代价)
# 标旗流
"flag_resource", # 标中雷获格面资源(雷区图谱)
"nest_aura", # 控制巢穴数转伤害光环(猎巢人)
"flag_all_hit", # 同轮全中旗伤倍增(完美布防)
"flag_strike", # 轮末旗伤开单(战旗军规——复用 board_typed)
# 蓄力流
"ap_carry", # 轮末 AP 结转蓄力(整备军备——改写 cleared 作废口径)
"charge_shield", # 蓄力层转减伤(铁壁整备)
"charge_spike", # 满层 spike(厚积薄发)
# 吞噬流
"terrain_refund", # 受地形伤掉落资源补偿(地脉采撷——伤害仍发生)
"terrain_dig_double", # 挖开伤害型地形格双产出(荆棘编织者)
"flag_neutralize", # 标中旗中和相邻伤害型地形格(净化工兵——🔴 R4 甲仅限伤害型)
"variant_counter", # 受地形伤积层轮末反打(熔火之躯——触发键不按地形种类特化)
]
## 行键白名单(🔴 零裸 flat 同族护栏——键出域=加载致命;triggers/desc=T3 换代新形态)
const ROW_KEYS := ["feat_id", "display_name", "rarity", "archetype", "triggers", "desc", "note"]
## 扫雷结算触发点 hook 集(🔴 红线①承载:每流派 ≥1 行 hook 命中本集——加载校验)
const MINE_HOOKS := ["first_dig_spell", "dig_count_segments", "round_dig_bonus", "dig_yield_mult",
"deep_dig", "flag_resource", "nest_aura", "flag_all_hit", "flag_strike", "ap_carry",
"terrain_refund", "terrain_dig_double", "flag_neutralize", "variant_counter"]
## effects 键白名单(🔴 零裸 flat 同族护栏——机制轴禁无条件属性加项;键出域=加载致命)
## 全部条件触发/机制修正参数;cost_* 族=正负双修负向面
const EFFECT_KEYS := ["value", "value2", "tier_value", "per_stack", "cap", "ap_cost", "chance",
"mult", "cost_mult", "cost_flat", "polarity", "spell_kind", "segment_cap", "discount_shift",
"trade_atk", "trade_hit", "shield_per_layer", "spike_mult", "layers_to_shield",
"neutralize_range", "counter_damage", "resource_kind", "rider_kind", "note"]
## trigger 行键(订阅行三字段:on/when?/do)
const TRIGGER_KEYS := ["on", "when", "do"]
func validate() -> Array:
var errs: Array = []
if rows.is_empty():
return errs # 空表合法态(T1 基建先行——T2 起逐流派填行)
return errs # 空表合法态(T1 基建先行——逐流派填行)
var seen := {}
# 流派扫雷覆盖聚合(🔴 红线①铁律——每流派 ≥1 行订阅扫雷侧事件)
var arch_mine := {} # archetype → bool
for row in rows:
var rid: String = row.get("feat_id", "")
if rid == "":
@@ -100,32 +62,27 @@ func validate() -> Array:
var arch := String(row.get("archetype", ""))
if not ARCHETYPES.has(arch):
errs.append({"category": "enum", "message": "%s archetype 出域 %s" % [rid, str(ARCHETYPES)]})
var hook := String(row.get("hook", ""))
if not HOOKS.has(hook):
errs.append({"category": "enum", "message": "%s hook 出域(挂点枚举封闭): %s" % [rid, hook]})
if not row.has("triggers"):
errs.append({"category": "domain", "message": "%s 缺 triggers(订阅形态唯一合法行形)" % rid})
elif EffectEngine.validate_trigger_row(rid, row, row.get("triggers", []) as Array, errs):
arch_mine[arch] = true
for k in row.keys():
if not ["feat_id", "display_name", "rarity", "archetype", "hook", "effects", "note"].has(String(k)):
if not ROW_KEYS.has(String(k)):
errs.append({"category": "zero_flat", "message": "%s 行键出白名单: %s" % [rid, String(k)]})
var fx: Dictionary = row.get("effects", {}) as Dictionary
for ek in fx.keys():
if not EFFECT_KEYS.has(String(ek)):
errs.append({"category": "zero_flat", "message": "%s effects 键出白名单: %s(机制轴禁无条件属性加项)" % [rid, String(ek)]})
# 🔴 红线① 扫雷触发点铁律:**有行的流派**各 ≥1 行 hook ∈ MINE_HOOKS(部分填充合法——T2-T6
# 逐流派渐进;七流派全量覆盖完成度由批次一出口断言承载,加载闸不卡渐进填行)
var archs_with_rows := {}
for row in rows:
archs_with_rows[String(row.get("archetype", ""))] = true
for arch in archs_with_rows.keys():
var covered := false
for row in rows:
if String(row.get("archetype", "")) == String(arch) and MINE_HOOKS.has(String(row.get("hook", ""))):
covered = true
break
if not covered:
errs.append({"category": "load_bearing", "message": "流派 %s 无扫雷结算触发点行(🔴 红线①铁律——每流派 ≥1 件挂扫雷端)" % String(arch)})
# 🔴 红线① 扫雷触发点铁律:**有行的流派**各 ≥1 行订阅扫雷侧事件(MINE_EVENTS 同义校验)
for arch in archs_with_rows():
if not bool(arch_mine.get(arch, false)):
errs.append({"category": "load_bearing", "message": "流派 %s 无扫雷结算触发点行(🔴 红线①铁律——每流派 ≥1 件订阅扫雷侧事件)" % String(arch)})
return errs
func archs_with_rows() -> Array:
var out := {}
for row in rows:
out[String(row.get("archetype", ""))] = true
return out.keys()
func row_by_id(feat_id: String) -> Dictionary:
for row in rows:
if String(row.get("feat_id", "")) == feat_id:
+21 -10
View File
@@ -38,19 +38,23 @@ const DIMENSIONS := ["info", "risk", "action", "plan", "counter"]
# 同调批扩 enum(迭代 11 第①批 T1)——新增九 kind 全部条件触发/机制修正类;mine_reveal/ap_grant 语义不变。
# cell_removal = 爆破工兵包中立移除格(T7 原位换装时启用——T6 影响评估前置);mine_hit_halve 系固定
# 「减半」语义不消费 magnitude(反弹 N 由 reflect_damage 行 magnitude 承载)。
# 内容数据驱动扩展批 T2 挂账:主动件 kind 词族换代(reveal_tiles/preview/grant_ap/remove_mines/
# refresh_pick——效果引擎 ACTIVE_KINDS 同名闭集),被动件 effect_kinds 词族随 T4 秘宝迁移退役
# (passive 行改 triggers 事件订阅形态);旧词保留共存至迁移完成。
const EFFECT_KINDS := ["mine_reveal", "reward_preview", "ap_grant", "venom_rider", "flag_burst",
"mine_hit_halve", "reflect_damage", "ap_rounds_front", "ap_charge_safe", "cell_removal",
"reward_weight_bias", "bonus_refresh"]
"reward_weight_bias", "bonus_refresh",
"reveal_tiles", "preview", "grant_ap", "remove_mines", "refresh_pick"]
const TARGETS := ["none", "area_3x3", "board_wide"]
const USE_PHASES := ["active_dig"]
const FORMS := ["", "passive", "active_charged"]
# magnitude 消费面(≥1 校验):ap_grant 与同调 N 值 kind;其余 kind 活行 magnitude 恒 0
# magnitude 消费面(≥1 校验):grant_ap 与同调 N 值 kind;其余 kind 活行 magnitude 恒 0
const MAGNITUDE_KINDS := ["ap_grant", "venom_rider", "flag_burst", "reflect_damage", "ap_rounds_front",
"ap_charge_safe", "reward_weight_bias", "bonus_refresh"]
"ap_charge_safe", "reward_weight_bias", "bonus_refresh", "grant_ap"]
# 🔴 零裸 flat 结构护栏:行键白名单封闭(键出域=加载致命)——机制轴禁无条件属性加项的表结构承载
const KEY_WHITELIST := ["item_id", "display_name", "dimension_tags", "effect_kinds", "target",
"magnitude", "use_phase", "retired", "deferred", "form", "charges_per_adventure", "slot_cost",
"unlock_requires", "effective_rounds", "charge_n"]
"unlock_requires", "effective_rounds", "charge_n", "triggers", "desc"]
func validate() -> Array:
@@ -90,6 +94,13 @@ func validate() -> Array:
for k in row.keys():
if not KEY_WHITELIST.has(String(k)):
errs.append({"category": "zero_flat", "message": "%s 行键出白名单: %s(KEY_WHITELIST 外键禁入)" % [iid, String(k)]})
# triggers 事件订阅行(内容数据驱动扩展批 T2/T4——被动件换代形态;与 effect_kinds 互斥校验
# 面=passive 行(换代单形态——效果行为只走订阅);active_charged 行两形合法并存
# (effect_kinds=主动分发通道 + triggers=受理闸订阅,掘进蓄能先例))
if row.has("triggers"):
if (row.get("effect_kinds", []) as Array).size() > 0 and String(row.get("form", "")) != "active_charged" and not bool(row.get("retired", false)):
errs.append({"category": "domain", "message": "%s triggers 与 effect_kinds 并存(换代单形态——被动件迁移期禁双挂)" % iid})
EffectEngine.validate_trigger_row(iid, row, row.get("triggers", []) as Array, errs)
var form: String = String(row.get("form", ""))
if not FORMS.has(form):
errs.append({"category": "enum", "message": "%s form 出域 %s: %s" % [iid, str(FORMS), form]})
@@ -97,8 +108,8 @@ func validate() -> Array:
# 退役/失义行:免 effect_kinds 非空与承重护栏校验(登记面保留,不入投放与 F2)
continue
# ---- 活行校验 ----
if kinds.is_empty():
errs.append({"category": "domain", "message": "%s effect_kinds 空(活行;失义件应标 deferred)" % iid})
if kinds.is_empty() and not row.has("triggers"):
errs.append({"category": "domain", "message": "%s effect_kinds 空(活行;失义件应标 deferred,被动件应挂 triggers 订阅)" % iid})
var kind_seen := {}
for k in kinds:
var kind: String = String(k)
@@ -123,8 +134,8 @@ func validate() -> Array:
mag_consuming = true
if mag_consuming and mag < 1:
errs.append({"category": "cross_field", "message": "%s N 值 kind 行 magnitude 应 ≥1,实得 %d" % [iid, mag]})
if not mag_consuming and mag != 0:
errs.append({"category": "cross_field", "message": "%s 非 N 值 kind 活行 magnitude 应 0,实得 %d" % [iid, mag]})
if not mag_consuming and mag != 0 and not row.has("triggers"):
errs.append({"category": "cross_field", "message": "%s 非 N 值 kind 活行 magnitude 应 0(trigger 行 magnitude=value_from 取数源豁免),实得 %d" % [iid, mag]})
# ---- 同调行约束(form 非空——装配面;过渡行 form="" 不校验)----
if form != "":
if int(row.get("slot_cost", 1)) != 1:
@@ -139,8 +150,8 @@ func validate() -> Array:
errs.append({"category": "domain", "message": "%s effective_rounds 越域 [0,999]: %d" % [iid, eff_rounds]})
if kind_seen.has("ap_rounds_front") and eff_rounds < 1:
errs.append({"category": "cross_field", "message": "%s ap_rounds_front 行 effective_rounds 应 ≥1(P4 甲表值化),实得 %d" % [iid, eff_rounds]})
if not kind_seen.has("ap_rounds_front") and eff_rounds != 0:
errs.append({"category": "cross_field", "message": "%s 非 ap_rounds_front 行 effective_rounds 应 0,实得 %d" % [iid, eff_rounds]})
if not kind_seen.has("ap_rounds_front") and eff_rounds != 0 and not row.has("triggers"):
errs.append({"category": "cross_field", "message": "%s 非 ap_rounds_front 行 effective_rounds 应 0(trigger 行=when value_from 取数源豁免),实得 %d" % [iid, eff_rounds]})
var charge_n: int = int(row.get("charge_n", 0))
if charge_n < 0 or charge_n > 999:
errs.append({"category": "domain", "message": "%s charge_n 越域 [0,999]: %d" % [iid, charge_n]})
+390 -92
View File
@@ -9,266 +9,564 @@ rows = [{
"display_name": "奥术余烬",
"rarity": "common",
"archetype": "magic",
"hook": "hero_attack_rider",
"effects": {
"desc": "普攻附加必中伤害",
"triggers": [{
"on": "attack_commit",
"do": {
"kind": "add_ticket_rider",
"value": 1
}
}]
}, {
"feat_id": "mana_weaver",
"display_name": "魔网织者",
"rarity": "rare",
"archetype": "magic",
"hook": "clamp_modifier",
"effects": {
"value": 1,
"polarity": "magic_vs_magic"
"desc": "克制面强化",
"triggers": [{
"on": "clamp_check",
"when": [{
"fact": "matchup",
"op": "eq",
"value": "magic_vs_magic"
}],
"do": {
"kind": "modify_ticket",
"delta": 1
}
}]
}, {
"feat_id": "azure_oath",
"display_name": "湛蓝誓约",
"rarity": "rare",
"archetype": "magic",
"hook": "clamp_modifier",
"effects": {
"value": 1,
"polarity": "magic_vs_heavy"
"desc": "克制面强化",
"triggers": [{
"on": "clamp_check",
"when": [{
"fact": "matchup",
"op": "eq",
"value": "magic_vs_heavy"
}],
"do": {
"kind": "modify_ticket",
"delta": 1
}
}]
}, {
"feat_id": "mana_resonance",
"display_name": "魔能共鸣",
"rarity": "rarest",
"archetype": "magic",
"hook": "first_dig_spell",
"effects": {
"desc": "首格追加奥术矢",
"triggers": [{
"on": "dig_reveal",
"when": [{
"fact": "is_first_dig_of_battle",
"op": "eq",
"value": true
}],
"do": {
"kind": "spawn_spell",
"value": 2
}
}]
}, {
"feat_id": "mine_atlas",
"display_name": "雷区图谱",
"rarity": "common",
"archetype": "flag",
"hook": "flag_resource",
"effects": {
"value": 1
"desc": "标中雷额外得资源",
"triggers": [{
"on": "flag_hit",
"do": {
"kind": "grant_material",
"value": 1,
"label": "(雷区图谱)"
}
}]
}, {
"feat_id": "nest_hunter",
"display_name": "猎巢人",
"rarity": "rare",
"archetype": "flag",
"hook": "nest_aura",
"effects": {
"value": 1
"desc": "控巢攻击光环",
"triggers": [{
"on": "attack_commit",
"do": {
"kind": "add_ticket_rider",
"value": 1,
"per": "nest_count"
}
}]
}, {
"feat_id": "perfect_defense",
"display_name": "完美布防",
"rarity": "rare",
"archetype": "flag",
"hook": "flag_all_hit",
"effects": {
"value": 2
"desc": "全中标旗伤加倍",
"triggers": [{
"on": "flag_settle",
"when": [{
"fact": "flags_count",
"op": "gte",
"value": 1
}],
"do": {
"kind": "modify_ticket",
"value": 2,
"mode": "flag_mult"
}
}]
}, {
"feat_id": "war_banner",
"display_name": "战旗军规",
"rarity": "rarest",
"archetype": "flag",
"hook": "flag_strike",
"effects": {
"value": 2
"desc": "标中旗必中打敌",
"triggers": [{
"on": "flag_settle",
"when": [{
"fact": "flags_count",
"op": "gte",
"value": 1
}],
"do": {
"kind": "open_ticket",
"value": 2,
"channel": "attack"
}
}]
}, {
"feat_id": "pierce_shot",
"display_name": "贯穿射击",
"rarity": "rarest",
"archetype": "pierce",
"hook": "kill_overflow",
"effects": {
"value": 1
"desc": "击杀半额弹射",
"triggers": [{
"on": "kill",
"do": {
"kind": "overflow",
"value": 50
}
}]
}, {
"feat_id": "chain_arrows",
"display_name": "连珠矢",
"rarity": "rare",
"archetype": "pierce",
"hook": "dig_count_segments",
"effects": {
"value": 2,
"cap": 2
"desc": "开格积穿刺矢",
"triggers": [{
"on": "dig_reveal",
"do": {
"kind": "counter_add",
"counter_id": "chain_digs",
"value": 1,
"reset_at": "round_start"
}
}, {
"on": "attack_commit",
"do": {
"kind": "attack_segments",
"value": 2,
"cap": 2,
"per": "dig_streak",
"divisor": 3
}
}]
}, {
"feat_id": "weak_instinct",
"display_name": "猎弱本能",
"rarity": "rare",
"archetype": "pierce",
"hook": "roll_discount",
"effects": {
"value": 1
"desc": "打轻甲更易命中",
"triggers": [{
"on": "clamp_check",
"when": [{
"fact": "matchup",
"op": "ends_with",
"value": "vs_light"
}],
"do": {
"kind": "modify_ticket",
"value": 1,
"mode": "discount_shift"
}
}]
}, {
"feat_id": "hunter_rapier",
"display_name": "猎手刺剑",
"rarity": "common",
"archetype": "pierce",
"hook": "clamp_modifier",
"effects": {
"value": 1,
"polarity": "pierce_vs_light"
"desc": "克制面强化",
"triggers": [{
"on": "clamp_check",
"when": [{
"fact": "matchup",
"op": "eq",
"value": "pierce_vs_light"
}],
"do": {
"kind": "modify_ticket",
"delta": 1
}
}]
}, {
"feat_id": "miner_stubborn",
"display_name": "矿工的执拗",
"rarity": "common",
"archetype": "normal",
"hook": "round_dig_bonus",
"effects": {
"desc": "开格越多打得越疼",
"triggers": [{
"on": "dig_reveal",
"do": {
"kind": "counter_add",
"counter_id": "stubborn_digs",
"value": 1,
"cap": 5
"reset_at": "round_start"
}
}, {
"on": "mine_hit",
"do": {
"kind": "counter_add",
"counter_id": "stubborn_digs",
"set": 0
}
}, {
"on": "attack_commit",
"do": {
"kind": "add_ticket_rider",
"value": 1,
"cap": 5,
"per": "dig_streak"
}
}]
}, {
"feat_id": "miner_hammer",
"display_name": "矿工重锤",
"rarity": "common",
"archetype": "normal",
"hook": "hit_trade",
"effects": {
"desc": "加伤换命中",
"triggers": [{
"on": "attack_commit",
"do": {
"kind": "modify_ticket",
"trade_atk": 25,
"trade_hit": 2
}
}]
}, {
"feat_id": "focus_mind",
"display_name": "专注",
"rarity": "rare",
"archetype": "normal",
"hook": "armor_roll_rewrite",
"effects": {
"value": 75
"desc": "攻击恒75%折扣",
"triggers": [{
"on": "armor_roll",
"do": {
"kind": "modify_ticket",
"value": 75,
"mode": "armor_rewrite"
}
}]
}, {
"feat_id": "armor_crack",
"display_name": "碎甲震击",
"rarity": "rare",
"archetype": "normal",
"hook": "heavy_debuff",
"effects": {
"value": 25
"desc": "打重甲附加破防",
"triggers": [{
"on": "attack_commit",
"when": [{
"fact": "matchup",
"op": "ends_with",
"value": "vs_heavy"
}],
"do": {
"kind": "apply_status",
"status_id": "off_guard"
}
}]
}, {
"feat_id": "greedy_lantern",
"display_name": "贪婪矿灯",
"rarity": "common",
"archetype": "dig",
"hook": "dig_yield_mult",
"effects": {
"desc": "开格产出放大",
"triggers": [{
"on": "dig_reveal",
"do": {
"kind": "yield_mult",
"mult": 25,
"cost_mult": 25
}
}]
}, {
"feat_id": "vein_intuition",
"display_name": "矿脉直觉",
"rarity": "rare",
"archetype": "dig",
"hook": "dig_yield_mult",
"effects": {
"desc": "开格产出放大",
"triggers": [{
"on": "dig_reveal",
"do": {
"kind": "counter_add",
"counter_id": "dig_combo",
"value": 1
}
}, {
"on": "mine_hit",
"do": {
"kind": "counter_add",
"counter_id": "dig_combo",
"set": 0
}
}, {
"on": "dig_reveal",
"do": {
"kind": "yield_mult",
"mult": 10,
"cap": 5
}
}]
}, {
"feat_id": "gambler_pact",
"display_name": "赌徒的血签",
"rarity": "rarest",
"archetype": "dig",
"hook": "dig_yield_mult",
"effects": {
"desc": "开格产出放大",
"triggers": [{
"on": "dig_reveal",
"do": {
"kind": "yield_mult",
"mult": 50,
"cost_mult": 100
}
}]
}, {
"feat_id": "deep_drill",
"display_name": "深挖钻头",
"rarity": "rare",
"archetype": "dig",
"hook": "deep_dig",
"effects": {
"ap_cost": 1,
"cap": 4
"desc": "开格2AP·产出双份",
"triggers": [{
"on": "dig_paid",
"do": {
"kind": "counter_spend",
"counter_id": "deep_paid_round",
"value": 1,
"reset_at": "round_start",
"fallback": 1
}
}, {
"on": "dig_reveal",
"when": [{
"fact": "counter",
"id": "deep_paid_round",
"op": "gte",
"value": 1
}, {
"fact": "counter",
"id": "deep_given_round",
"op": "lt_ref",
"ref": "deep_paid_round"
}],
"do": {
"kind": "grant_material",
"label": "(深挖)"
}
}, {
"on": "dig_reveal",
"when": [{
"fact": "counter",
"id": "deep_paid_round",
"op": "gte",
"value": 1
}, {
"fact": "counter",
"id": "deep_given_round",
"op": "lt_ref",
"ref": "deep_paid_round"
}],
"do": {
"kind": "counter_add",
"counter_id": "deep_given_round",
"value": 1
}
}]
}, {
"feat_id": "armory_prep",
"display_name": "整备军备",
"rarity": "rarest",
"archetype": "charge",
"hook": "ap_carry",
"effects": {
"value": 1,
"cap": 3
"desc": "剩AP转下轮攻击",
"triggers": [{
"on": "round_end",
"do": {
"kind": "grant_ap",
"mode": "transfer"
}
}, {
"on": "attack_commit",
"do": {
"kind": "attack_bonus",
"value": 1,
"per": "charge_layers"
}
}]
}, {
"feat_id": "iron_wall",
"display_name": "铁壁整备",
"rarity": "rare",
"archetype": "charge",
"hook": "charge_shield",
"effects": {
"shield_per_layer": 1
"desc": "蓄力转减伤",
"triggers": [{
"on": "damage_taken",
"do": {
"kind": "shield",
"value": 1,
"per": "charge_layers"
}
}]
}, {
"feat_id": "patient_burst",
"display_name": "厚积薄发",
"rarity": "rare",
"archetype": "charge",
"hook": "charge_spike",
"effects": {
"value": 2
"desc": "蓄满下轮爆发",
"triggers": [{
"on": "attack_commit",
"when": [{
"fact": "charge_layers",
"op": "gte",
"value": 3
}],
"do": {
"kind": "modify_ticket",
"value": 2,
"mode": "spike"
}
}]
}, {
"feat_id": "ley_harvest",
"display_name": "地脉采撷",
"rarity": "common",
"archetype": "devour",
"hook": "terrain_refund",
"effects": {
"value": 1
"desc": "吃地形伤返资源",
"triggers": [{
"on": "damage_taken",
"when": [{
"fact": "terrain_kind",
"op": "has"
}],
"do": {
"kind": "grant_material",
"label": "(地脉补偿)"
}
}]
}, {
"feat_id": "thorn_weaver",
"display_name": "荆棘编织者",
"rarity": "rare",
"archetype": "devour",
"hook": "terrain_dig_double",
"effects": {
"value": 1
"desc": "挖伤害格双产出",
"triggers": [{
"on": "damage_taken",
"when": [{
"fact": "terrain_kind",
"op": "has"
}],
"do": {
"kind": "grant_material",
"label": "(荆棘编织)"
}
}]
}, {
"feat_id": "purge_sapper",
"display_name": "净化工兵",
"rarity": "rare",
"archetype": "devour",
"hook": "flag_neutralize",
"effects": {
"value": 1
"desc": "标旗中和伤害格",
"triggers": [{
"on": "flag_hit",
"when": [{
"fact": "terrain_kind",
"op": "has"
}],
"do": {
"kind": "rewrite_board"
}
}]
}, {
"feat_id": "magma_body",
"display_name": "熔火之躯",
"rarity": "rarest",
"archetype": "devour",
"hook": "variant_counter",
"effects": {
"desc": "吃地形伤反打全体",
"triggers": [{
"on": "damage_taken",
"when": [{
"fact": "terrain_kind",
"op": "has"
}],
"do": {
"kind": "counter_add",
"counter_id": "magma_layers",
"value": 1
}
}, {
"on": "round_end",
"when": [{
"fact": "counter",
"id": "magma_layers",
"op": "gte",
"value": 1
}],
"do": {
"kind": "open_ticket",
"value": 1,
"per": "magma_layers",
"channel": "blast"
}
}, {
"on": "round_end",
"when": [{
"fact": "counter",
"id": "magma_layers",
"op": "gte",
"value": 1
}],
"do": {
"kind": "counter_add",
"counter_id": "magma_layers",
"set": 0
}
}]
}]
; 双层 BD 实施批(2026-10-04):规则改写型特性表——行 = 特性定义
; (feat_id/display_name/rarity/archetype/hook/effects)。
; T2 魔法流四件(整合定稿 §2.3 S1):奥术余烬〔普通·rider+1 必中附加〕/魔网织者〔稀有·magic vs
; magic 克制罚修正〕/湛蓝誓约〔稀有·magic vs heavy 克制加成〕/魔能共鸣〔罕见锚点·rider+2+每战首次
; 开格奥术矢——「每关首次」字面落地为「每战首次」台账披露待追认〕;magic 档一(≥2 件)rider+1 /
; 档二(≥4 件)rider 可暴击——引擎 tiers_for 承载。T3-T6 逐流派续填(flag 4/pierce 4/normal 4/
; dig 4/charge 3/devour 4——整合定稿 §2.3 七流派的件面)。
; 稀有度→投放档映射:common/rare → 稀有令牌档、rarest → 罕见令牌档(relic 永不入普通令牌档
; ——「规则改写型特性带挂稀有/罕见档」追认 b 接线口径,普通档 70 张纯数值维持)。
; 🔴 层级纪律:本表行永不入同调位(treasure_items)/局外树(T8 节点表)——规则改写型特性只走局内随机渠道。
; 数值全 provisional 归 M-4 数值批;effects 键白名单封闭(零裸 flat 同族护栏)。
; 内容数据驱动扩展批(2026-10-06 T3):专长表换代——行 = triggers 事件订阅形态
; (feat_id/display_name/rarity/archetype/desc/triggers——订阅哪个事件 on / 什么条件 when / 什么效果 do)。
; 旧 hook/effects 挂点形态随本批退役(挂点枚举消亡:事件名+效果类型取代——任务书 R4 裁决)。
; 23 挂点逐行移植映射=任务书 §五(等价性对拍底单);desc=文案表值化(呈现层读行,零硬编码)。
; 效果参数值直接写 do 字典(数值单一事实源=本表;效果引擎 DO_KEYS 白名单封闭)。
; 🔴 护栏全部保留:事件/效果/fact/算子四词表封闭(EffectEngine 常量)、行键白名单、悬空引用
; 致命、零裸 flat 禁令、流派扫雷触发铁律(每流派 ≥1 行订阅扫雷侧事件——MINE_EVENTS 同义校验)。
; 映射表对拍注记(T3 落地语义修正,行为等价优先——commit 留痕):
; - terrain_dig_double(荆棘编织者):映射表标 yield_mult(×2),现行为=第二份同格产出兑付
; (grant_material),×2 乘区对 percent 行不等价——落 grant_material。
; - flag_all_hit(完美布防):映射表标 yield_mult(旗伤乘区),现行为=旗伤单值倍增(战旗单据
; 数值改写)——落 modify_ticket(mode:flag_mult)。
; - 连珠矢/矿工的执拗 streak 取数:映射表 on:dig_reveal counter_add 形态与现行为「轮内踩雷全轮
; 清零」不等价(逐格清零后重积≠全轮判定)——计数器行保留(扫雷铁律承载),等价执行读
; attack_commit 载荷 dig_streak(战斗段全轮推导,与迁移前逐位一致)。
; - 厚积薄发:映射表 on:charge_full(满层沿)→spike,现行为=满层持续状态(层数无消耗点,沿触发
; 一次≠持续增强)——落 on:attack_commit + when(charge_layers ≥3) → modify_ticket(mode:spike),
; charge_full 事件仍埋点(R6 框架先行)。
; - 完美布防:「全中」条件装不进 fact 封闭 9 项(需 hits>0 且 misses==0 双读数)——when 落
; flags_count ≥1(可命中保守判定),全中闸=flag_settle 发射点施加面承载(载荷 flags_hit_all
; 仅供观测)——禁私开 fact(停机点纪律),查询面落法=代拍板白名单。
; - 猎巢人/矿工的执拗:映射表标 modify_ticket/attack_bonus,现行为=必中附加通道(伤害控制器
; hero_rider 后置于乘区与下限)——落 add_ticket_rider(per 取数载荷),乘区外恒定兑付逐位一致。
; - 整备军备:现行为双效果(轮末 AP 转蓄力层 + 每层下轮首段攻 +1)——两行订阅:
; round_end grant_ap(mode:transfer) + attack_commit attack_bonus(per:charge_layers)。
; 数值全 provisional 归 M-4 数值批;desc 文案=旧 summaries 表迁移(措辞零变化)。
+94 -11
View File
@@ -10,6 +10,7 @@ rows = [{
"item_id": "probe_dust",
"display_name": "显影尘",
"dimension_tags": ["info", "counter"],
"desc": "效果失义待重定",
"effect_kinds": [],
"target": "none",
"magnitude": 0,
@@ -19,6 +20,7 @@ rows = [{
"item_id": "sapper_order",
"display_name": "工兵令",
"dimension_tags": ["action"],
"desc": "AP +2",
"effect_kinds": ["ap_grant"],
"target": "none",
"magnitude": 2,
@@ -28,6 +30,7 @@ rows = [{
"item_id": "loot_compass",
"display_name": "探宝罗盘",
"dimension_tags": ["plan"],
"desc": "预读 3×3",
"effect_kinds": ["reward_preview"],
"target": "area_3x3",
"magnitude": 0,
@@ -37,6 +40,7 @@ rows = [{
"item_id": "nest_pendulum",
"display_name": "探巢摆钟",
"dimension_tags": ["risk"],
"desc": "真视 3×3",
"effect_kinds": ["mine_reveal"],
"target": "area_3x3",
"magnitude": 0,
@@ -48,6 +52,7 @@ rows = [{
"item_id": "catacomb_scroll",
"display_name": "地穴全图卷",
"dimension_tags": ["info", "plan"],
"desc": "效果失义待重定",
"effect_kinds": [],
"target": "board_wide",
"magnitude": 0,
@@ -57,6 +62,7 @@ rows = [{
"item_id": "blast_kit",
"display_name": "爆破工兵包",
"dimension_tags": ["action", "risk"],
"desc": "AP +4 真视 3×3 移除雷格",
"effect_kinds": ["ap_grant", "mine_reveal", "cell_removal"],
"target": "area_3x3",
"magnitude": 4,
@@ -68,47 +74,108 @@ rows = [{
"item_id": "venom_edge",
"display_name": "淬毒刃纹",
"dimension_tags": ["counter"],
"effect_kinds": ["venom_rider"],
"desc": "克制正面直伤+2",
"target": "none",
"magnitude": 2,
"use_phase": "active_dig",
"form": "passive",
"slot_cost": 1
"slot_cost": 1,
"triggers": [{
"on": "clamp_check",
"when": [{
"fact": "matchup",
"op": "eq",
"value": "pierce_vs_ward"
}],
"do": {
"kind": "modify_ticket",
"delta_from": "magnitude",
"mode": "clamp_bonus"
}
}, {
"on": "clamp_check",
"when": [{
"fact": "matchup",
"op": "eq",
"value": "magic_vs_heavy"
}],
"do": {
"kind": "modify_ticket",
"delta_from": "magnitude",
"mode": "clamp_bonus"
}
}]
}, {
"item_id": "flag_burst",
"display_name": "标中殉爆",
"dimension_tags": ["action"],
"effect_kinds": ["flag_burst"],
"desc": "每标中一旗全体敌受2",
"target": "none",
"magnitude": 2,
"use_phase": "active_dig",
"form": "passive",
"slot_cost": 1
"slot_cost": 1,
"triggers": [{
"on": "flag_hit",
"do": {
"kind": "open_ticket",
"value_from": "magnitude",
"channel": "blast"
}
}]
}, {
"item_id": "nest_counter",
"display_name": "惊巢反制",
"dimension_tags": ["risk"],
"effect_kinds": ["mine_hit_halve", "reflect_damage"],
"desc": "踩雷惩罚减半+反弹2",
"target": "none",
"magnitude": 2,
"use_phase": "active_dig",
"form": "passive",
"slot_cost": 1
"slot_cost": 1,
"triggers": [{
"on": "mine_hit",
"do": {
"kind": "damage_mult",
"value": 50
}
}, {
"on": "mine_hit",
"do": {
"kind": "open_ticket",
"value_from": "magnitude",
"channel": "reflect"
}
}]
}, {
"item_id": "dawn_order",
"display_name": "工兵晨令",
"dimension_tags": ["action"],
"effect_kinds": ["ap_rounds_front"],
"desc": "前3轮每轮AP+1",
"target": "none",
"magnitude": 1,
"use_phase": "active_dig",
"form": "passive",
"slot_cost": 1,
"effective_rounds": 3
"effective_rounds": 3,
"triggers": [{
"on": "round_start",
"when": [{
"fact": "round_index",
"op": "lte",
"value_from": "effective_rounds"
}],
"do": {
"kind": "grant_ap",
"mode": "queue",
"value_from": "magnitude"
}
}]
}, {
"item_id": "dig_charge",
"display_name": "掘进蓄能",
"dimension_tags": ["action"],
"desc": "积点换AP+1",
"effect_kinds": ["ap_charge_safe"],
"target": "none",
"magnitude": 1,
@@ -116,21 +183,37 @@ rows = [{
"form": "active_charged",
"slot_cost": 1,
"charges_per_adventure": 2,
"charge_n": 3
"charge_n": 3,
"triggers": [{
"on": "treasure_used",
"do": {
"kind": "counter_spend",
"counter_id": "dig_charge_bank",
"value": 1
}
}]
}, {
"item_id": "luck_coin",
"display_name": "偏运硬币",
"dimension_tags": ["risk"],
"effect_kinds": ["reward_weight_bias"],
"desc": "稀有资源权重+2",
"target": "none",
"magnitude": 2,
"use_phase": "active_dig",
"form": "passive",
"slot_cost": 1
"slot_cost": 1,
"triggers": [{
"on": "reward_roll",
"do": {
"kind": "bias_rewards",
"value_from": "magnitude"
}
}]
}, {
"item_id": "pick_refresh",
"display_name": "三选一刷新件",
"dimension_tags": ["info"],
"desc": "三选一刷新+1",
"effect_kinds": ["bonus_refresh"],
"target": "none",
"magnitude": 1,
+4 -28
View File
@@ -131,40 +131,16 @@ static func bonus_result_text(result: Dictionary) -> String:
return "已选择:%s\n其余选项已丢弃(不可反悔)" % name
# 特性卡片文案(迭代 12 T1;2026-10-04 呈现补强——第三行=hook 效果摘要,用户反馈「不知道件是
# 干嘛的」缺口承载;摘要=静态映射表 provisional 文案归代拍板白名单「件文案微调」)
# 特性卡片文案(迭代 12 T1;2026-10-04 呈现补强——第三行=效果摘要,用户反馈「不知道件是
# 干嘛的」缺口承载。内容数据驱动扩展批 T3 文案表值化:摘要=数据行 desc 字段,零硬编码映射——
# 新件写行即有名有姓,措辞与旧 summaries 表逐字一致)
static func feat_card_text(row: Dictionary) -> String:
if row.is_empty() or String(row.get("feat_id", "")) == "":
return "特性\n???"
var arch_names := {"magic": "魔法", "pierce": "穿刺", "normal": "普攻", "dig": "开格",
"flag": "标旗", "charge": "蓄力", "devour": "吞噬"}
var arch := String(arch_names.get(String(row.get("archetype", "")), "流派"))
var summaries := {
"hero_attack_rider": "普攻附加必中伤害",
"clamp_modifier": "克制面强化",
"first_dig_spell": "首格追加奥术矢",
"kill_overflow": "击杀半额弹射",
"dig_count_segments": "开格积穿刺矢",
"roll_discount": "打轻甲更易命中",
"round_dig_bonus": "开格越多打得越疼",
"hit_trade": "加伤换命中",
"armor_roll_rewrite": "攻击恒75%折扣",
"heavy_debuff": "打重甲附加破防",
"dig_yield_mult": "开格产出放大",
"deep_dig": "开格2AP·产出双份",
"flag_resource": "标中雷额外得资源",
"nest_aura": "控巢攻击光环",
"flag_all_hit": "全中标旗伤加倍",
"flag_strike": "标中旗必中打敌",
"ap_carry": "剩AP转下轮攻击",
"charge_shield": "蓄力转减伤",
"charge_spike": "蓄满下轮爆发",
"terrain_refund": "吃地形伤返资源",
"terrain_dig_double": "挖伤害格双产出",
"flag_neutralize": "标旗中和伤害格",
"variant_counter": "吃地形伤反打全体",
}
var summary := String(summaries.get(String(row.get("hook", "")), ""))
var summary := String(row.get("desc", ""))
if summary != "":
return "%s · %s\n%s" % [String(row.get("display_name", row.get("feat_id", ""))), arch, summary]
return "特性 · %s\n%s" % [arch, String(row.get("display_name", row.get("feat_id", "")))]
+7 -15
View File
@@ -191,27 +191,19 @@ func _curio_card_texts(candidates: Array, treasure_detail: Array = []) -> Array:
return out
# 道具件卡片行(掷定前置——具体道具名 + 效果摘要;空 iid = 无预掷回退态显示通用文案)
# 道具件卡片行(掷定前置——具体道具名 + 效果摘要;空 iid = 无预掷回退态显示通用文案)。
# 内容数据驱动扩展批 T4 文案表值化:效果摘要=数据行 desc 字段直读(零 kind 硬编码映射——
# 新件写行即有名有姓)
func _treasure_card_line(iid: String) -> String:
if iid == "":
return ""
var row: Dictionary = db.treasure_item_row(iid)
if row.is_empty():
return ""
var parts: Array[String] = []
for k in (row.get("effect_kinds", []) as Array):
if String(k) == "mine_reveal":
parts.append("真视")
elif String(k) == "reward_preview":
parts.append("预读")
elif String(k) == "ap_grant":
parts.append("AP +%d" % int(row.get("magnitude", 0)))
var tgt := String(row.get("target", "none"))
if tgt == "area_3x3":
parts.append("3×3")
elif tgt == "board_wide":
parts.append("全图")
return "%s · %s" % [String(row.get("display_name", iid)), " ".join(parts)]
var desc := String(row.get("desc", ""))
if desc != "":
return "%s · %s" % [String(row.get("display_name", iid)), desc]
return String(row.get("display_name", iid))
# 三选一刷新(迭代 11 第①批 T5b X/Y 双源 + 2026-10-06 用户令对齐商店 MC-1——免费位用尽转 GP 付费档:
+163 -81
View File
@@ -619,17 +619,23 @@ func _smoke_feats() -> void:
rarest_tokens += 1
check(feat_tokens == 27 and rarest_tokens == 6 and a_r0._curio_deck.size() == 125,
"规则改写型特性带入池:27 件(rare 档 21 + rarest 锚点 6)——牌库 98+27=125")
check(_db.feats.validate().is_empty(), "feat_table.validate 零违例(有行流派扫雷端铁律——magic first_dig_spell 在案)")
# ② 档位激活引擎(纯函数直测——构造表 4 件 magic + 1 件 flag + 1 件 rarest 锚点)
check(_db.feats.validate().is_empty(), "feat_table.validate 零违例(有行流派扫雷端铁律——magic 魔能共鸣 dig_reveal 订阅在案)")
# ② 档位激活引擎(纯函数直测——构造表 4 件 magic + 1 件 flag;内容数据驱动扩展批 T3 断言面随迁:
# 行=triggers 事件订阅形态,挂点投影断言换代=效果引擎订阅装载序〔feats 表序〕)
var rt := FeatTable.new()
rt.rows = [
{"feat_id": "m1", "display_name": "魔一", "rarity": "common", "archetype": "magic", "hook": "hero_attack_rider", "effects": {"value": 1}},
{"feat_id": "m2", "display_name": "魔二", "rarity": "common", "archetype": "magic", "hook": "first_dig_spell", "effects": {"value": 1}},
{"feat_id": "m3", "display_name": "魔三", "rarity": "rare", "archetype": "magic", "hook": "clamp_modifier", "effects": {"value": 1}},
{"feat_id": "m4", "display_name": "魔四", "rarity": "rare", "archetype": "magic", "hook": "hero_attack_rider", "effects": {"tier_value": 2}},
{"feat_id": "f1", "display_name": "旗一", "rarity": "common", "archetype": "flag", "hook": "flag_resource", "effects": {"value": 1}},
{"feat_id": "m1", "display_name": "魔一", "rarity": "common", "archetype": "magic", "desc": "测试 rider",
"triggers": [{"on": "attack_commit", "do": {"kind": "add_ticket_rider", "value": 1}}]},
{"feat_id": "m2", "display_name": "魔二", "rarity": "common", "archetype": "magic", "desc": "测试奥术矢",
"triggers": [{"on": "dig_reveal", "do": {"kind": "spawn_spell", "value": 1}}]},
{"feat_id": "m3", "display_name": "魔三", "rarity": "rare", "archetype": "magic", "desc": "测试克制",
"triggers": [{"on": "clamp_check", "when": [{"fact": "matchup", "op": "eq", "value": "magic_vs_magic"}], "do": {"kind": "modify_ticket", "delta": 1}}]},
{"feat_id": "m4", "display_name": "魔四", "rarity": "rare", "archetype": "magic", "desc": "测试加值",
"triggers": [{"on": "attack_commit", "do": {"kind": "attack_bonus", "value": 2}}]},
{"feat_id": "f1", "display_name": "旗一", "rarity": "common", "archetype": "flag", "desc": "测试图谱",
"triggers": [{"on": "flag_hit", "do": {"kind": "grant_material", "value": 1}}]},
]
check(rt.validate().is_empty(), "构造 relic 表 validate 零违例(magic 4 件含 first_dig_spell 扫雷端——红线①铁律过)")
check(rt.validate().is_empty(), "构造 relic 表 validate 零违例(magic 4 件含 dig_reveal 扫雷端订阅——红线①铁律过)")
var tiers := FeatsEngine.tiers_for(["m1", "m2", "m3", "m4", "f1"], rt)
check(int(tiers.get("magic", -1)) == 2 and int(tiers.get("flag", -1)) == 0,
"档位激活:magic 4 件→档二 · flag 1 件→散件档 0(集 2 档一/4 档二——以撒套装/小骨铭文先例)")
@@ -637,9 +643,6 @@ func _smoke_feats() -> void:
"成型判读:magic 成型(tier≥1)· flag 未成型(散件单体保底——档位只质变不启动)")
var t2 := FeatsEngine.tiers_for(["m1", "m2"], rt)
check(int(t2.get("magic", -1)) == 1, "档位激活:2 件→档一(同集合同档位确定性——MC 逐位可复放承载)")
var hooks := FeatsEngine.hooks_for(["m1", "f1"], rt)
check(hooks.size() == 2 and String(hooks[0].get("hook", "")) == "hero_attack_rider",
"挂点投影:表序稳定(同集合→同投影)")
# ③ 规则改写型特性带接线(临时表注入——relic 令牌入牌库/兑付入持有集/踢池/断点往返;测试尾还原现行表)
var live_rows: FeatTable = _db.feats
_db.feats = rt
@@ -721,56 +724,121 @@ func _smoke_feats() -> void:
check(bp_deck_m3 == 0, "断点余库:已兑付 relic 令牌逐枚还原(m3 零残留——续玩不重复获得)")
sm_r.clear_breakpoint()
_db.feats = live_rows # 还原现行表(共享 _db 防污染——后续冒烟段零感知)
# ④ T2 魔法流求值面(现行表 magic 四件——flags 组装直测;rider 落地行为=伤害控制器消费面,
# 既有战斗冒烟全绿=feat_flags 空 dict 零扰动实证;全链分岔读数归 T7 MC)
# ④ 魔法流订阅面(内容数据驱动扩展批 T3 断言面随迁——_feat_flags_for 挂点求值退役:
# 求值断言=效果引擎 rebuild+dispatch 命中面;落地行为=发射点施加面(本段下方各事件直测),
# 全链分岔读数归 T6 MC 对拍)
var orch = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(73))
orch._feat_flags_for(["arcane_ember", "mana_weaver", "azure_oath", "mana_resonance"])
var rf: Dictionary = orch._damage.feat_flags
check(int(rf.get("rider_flat", -1)) == 2 and bool(rf.get("rider_crit", false)),
"魔法流求值:rider_flat=余烬1+档一(4件)+1=2 · 档二 rider 可暴击(共鸣 rider 提升以奥术矢+档位承载——锚点双效果 provisional 披露)")
check(int(rf.get("clamp_magic_magic", -1)) == 1 and int(rf.get("clamp_magic_heavy", -1)) == 1
and bool(rf.get("first_dig_spell", false)) and int(rf.get("spell_value", -1)) == 2,
"魔法流求值:魔网织者 magic-vs-magic 罚修正 +1 · 湛蓝誓约 magic-vs-heavy +1 · 魔能共鸣奥术矢 value 2")
# ⑤ T3 标旗流求值面(现行表 magic 4+flag 4——混合持有:flag 档一 4 件成型)
orch._feat_flags_for(["arcane_ember", "mana_weaver", "azure_oath", "mana_resonance",
"mine_atlas", "nest_hunter", "perfect_defense", "war_banner"])
var rf8: Dictionary = orch._damage.feat_flags
check(int(rf8.get("flag_resource_extra", -1)) == 1 and int(rf8.get("nest_aura_value", -1)) == 1
and int(rf8.get("flag_all_hit_mult", -1)) == 2 and int(rf8.get("flag_strike_value", -1)) == 2,
"标旗流求值:雷区图谱额外资源 1 · 猎巢人光环 +1/巢 · 完美布防 ×2 · 战旗军规旗伤 2(复用 board_typed 零新配方)")
# ⑥ T4 穿刺/普攻流求值面(全 16 件持有——四流派档位成型)
orch._feat_flags_for(["arcane_ember", "mana_weaver", "azure_oath", "mana_resonance",
"mine_atlas", "nest_hunter", "perfect_defense", "war_banner",
var magic4: Array = ["arcane_ember", "mana_weaver", "azure_oath", "mana_resonance"]
orch._fx.rebuild(magic4, [], [])
var ac4: Array = orch._fx_emit("attack_commit", {"atk": 10, "target": "敌", "matchup": "magic_vs_magic", "dig_streak": 0, "nest_count": 0})
check(orch._hits_of(ac4, "add_ticket_rider").size() == 1
and int((orch._hits_of(ac4, "add_ticket_rider")[0] as Dictionary).get("value", -1)) == 1,
"魔法流订阅:奥术余烬 attack_commit→add_ticket_rider value 1(必中附加通道)")
check(int((orch._hits_of(orch._fx_emit("clamp_check", {"matchup": "magic_vs_magic"}), "modify_ticket")[0] as Dictionary).get("delta", -1)) == 1
and int((orch._hits_of(orch._fx_emit("clamp_check", {"matchup": "magic_vs_heavy"}), "modify_ticket")[0] as Dictionary).get("delta", -1)) == 1,
"魔法流订阅:魔网织者 magic-vs-magic +1 · 湛蓝誓约 magic-vs-heavy +1(clamp_check modify 窗口)")
var spell4: Array = orch._fx_emit("dig_reveal", {"cell": Vector2i(1, 1), "round": 1, "is_first_dig_of_battle": true, "terrain_kind": []})
check(orch._hits_of(spell4, "spawn_spell").size() == 1
and int((orch._hits_of(spell4, "spawn_spell")[0] as Dictionary).get("value", -1)) == 2,
"魔法流订阅:魔能共鸣每战首次开格→spawn_spell value 2(奥术矢)")
check(int(FeatsEngine.tiers_for(magic4, _db.feats).get("magic", -1)) == 2,
"魔法流档位:magic 4 件→档二(rider 可暴击——begin_battle 注伤害控制器两运行态)")
# ⑤ 标旗流订阅面(magic 4+flag 4——混合持有:flag 档一成型)
orch._fx.rebuild(magic4 + ["mine_atlas", "nest_hunter", "perfect_defense", "war_banner"], [], [])
check(orch._hits_of(orch._fx_emit("flag_hit", {"cell": Vector2i(1, 1), "round": 1, "terrain_kind": []}), "grant_material").size() == 1,
"标旗流订阅:雷区图谱 flag_hit→grant_material(标中雷额外资源)")
var nh5: Array = orch._fx_emit("attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 3})
var nh_hits: Array = orch._hits_of(nh5, "add_ticket_rider")
check(nh_hits.size() == 2 and String((nh_hits[1] as Dictionary).get("per", "")) == "nest_count",
"标旗流订阅:猎巢人 attack_commit→add_ticket_rider per nest_count(控巢光环 rider 通道)")
var fs5: Array = orch._fx_emit("flag_settle", {"flags_hit_all": true, "flags_count": 2, "round": 1})
check(orch._hits_of(fs5, "open_ticket").size() == 1
and int((orch._hits_of(fs5, "open_ticket")[0] as Dictionary).get("value", -1)) == 2
and orch._hits_of(fs5, "modify_ticket").size() == 1,
"标旗流订阅:战旗军规 flag_settle→open_ticket value 2(attack 通道自开单)· 完美布防 flag_mult ×2")
check(orch._fx_emit("flag_settle", {"flags_hit_all": false, "flags_count": 0, "round": 1}).is_empty(),
"标旗流订阅:零标中旗→战旗/完美布防零命中(when flags_count 封闭判定)")
# ⑥ 穿刺/普攻流订阅面(16 件持有——四流派档位成型)
var ids16: Array = magic4 + ["mine_atlas", "nest_hunter", "perfect_defense", "war_banner",
"pierce_shot", "chain_arrows", "weak_instinct", "hunter_rapier",
"miner_stubborn", "miner_hammer", "focus_mind", "armor_crack"])
var rf16: Dictionary = orch._damage.feat_flags
check(bool(rf16.get("kill_overflow", false)) and int(rf16.get("chain_value", -1)) == 2
and int(rf16.get("discount_shift", -1)) == 1 and int(rf16.get("clamp_pierce_light", -1)) == 1,
"穿刺流求值:贯穿射击弹射旗 · 连珠矢段值 2 上限 2 · 猎弱本能折扣改善 1 档 · 猎手刺剑 pierce-vs-light +1")
check(int(rf16.get("round_dig_value", -1)) == 1 and int(rf16.get("hit_trade_atk", -1)) == 25
and int(rf16.get("hit_trade_hit", -1)) == 2 and int(rf16.get("armor_roll_rewrite", -1)) == 75
and int(rf16.get("crack_value", -1)) == 25,
"普攻流求值:执拗 +1/格上限 5 · 重锤交换 +25%/−2AC · 专注恒 75% 免掷骰 · 碎甲震击 off_guard 授予旗")
check(bool(rf16.get("rider_crit", false)) and bool(rf16.get("rider_crit", false)),
"miner_stubborn", "miner_hammer", "focus_mind", "armor_crack"]
orch._fx.rebuild(ids16, [], [])
check(orch._hits_of(orch._fx_emit("kill", {"target": "敌", "overflow": 10}), "overflow").size() == 1,
"穿刺流订阅:贯穿射击 kill→overflow(击杀弹射旗)")
var dr6: Array = orch._fx_emit("dig_reveal", {"cell": Vector2i(1, 1), "round": 1, "is_first_dig_of_battle": false, "terrain_kind": []})
check(orch._hits_of(dr6, "counter_add").size() == 2,
"穿刺/普攻流订阅:连珠矢+执拗 dig_reveal→counter_add(扫雷侧事件铁律承载——chain_digs/stubborn_digs)")
var ac6: Array = orch._fx_emit("attack_commit", {"atk": 10, "target": "敌", "matchup": "pierce_vs_light", "dig_streak": 9, "nest_count": 0})
var seg6: Array = orch._hits_of(ac6, "attack_segments")
check(seg6.size() == 1 and int((seg6[0] as Dictionary).get("value", -1)) == 2
and int((seg6[0] as Dictionary).get("cap", -1)) == 2 and int((seg6[0] as Dictionary).get("divisor", -1)) == 3,
"穿刺流订阅:连珠矢 attack_commit→attack_segments(段值 2 上限 2 每 3 格 +1 段)")
var clamp6: Array = orch._fx_emit("clamp_check", {"matchup": "pierce_vs_light"})
check(orch._hits_of(clamp6, "modify_ticket").size() == 2,
"穿刺流订阅:猎弱本能 ends_with vs_light→discount_shift · 猎手刺剑 pierce_vs_light→delta +1(同一事件双订阅按声明序)")
check(orch._hits_of(orch._fx_emit("armor_roll", {"target_ac": 5, "roll": 3}), "modify_ticket").size() == 1,
"普攻流订阅:专注 armor_roll→modify armor_rewrite 75(免掷恒折扣)")
var ac6n: Array = orch._fx_emit("attack_commit", {"atk": 10, "target": "敌", "matchup": "normal_vs_heavy", "dig_streak": 0, "nest_count": 0})
check(orch._hits_of(ac6n, "apply_status").size() == 1
and String((orch._hits_of(ac6n, "apply_status")[0] as Dictionary).get("status_id", "")) == "off_guard",
"普攻流订阅:碎甲震击 ends_with vs_heavy→apply_status off_guard(复用零新状态行)")
var ham6: Array = orch._hits_of(ac6, "modify_ticket")
var has_trade := false
for h in ham6:
if int((h as Dictionary).get("trade_atk", 0)) == 25 and int((h as Dictionary).get("trade_hit", 0)) == 2:
has_trade = true
check(has_trade, "普攻流订阅:矿工重锤 attack_commit→modify trade_atk 25/trade_hit 2(命中交换)")
check(int(FeatsEngine.tiers_for(ids16, _db.feats).get("pierce", -1)) == 2
and int(FeatsEngine.tiers_for(ids16, _db.feats).get("normal", -1)) == 2,
"档位质变:magic/pierce/normal/flag 四流派档二(16 件混合——同标签 ≥4 判档二确定性)")
# ⑦ T5 开格/蓄力流求值面(追加七件)
var ids23: Array = ["arcane_ember", "mana_weaver", "azure_oath", "mana_resonance",
"mine_atlas", "nest_hunter", "perfect_defense", "war_banner",
"pierce_shot", "chain_arrows", "weak_instinct", "hunter_rapier",
"miner_stubborn", "miner_hammer", "focus_mind", "armor_crack",
"greedy_lantern", "vein_intuition", "gambler_pact", "deep_drill",
"armory_prep", "iron_wall", "patient_burst"]
orch._feat_flags_for(ids23)
var rf23: Dictionary = orch._damage.feat_flags
check(int(rf23.get("dig_yield_pos_mult", -1)) == 85 and int(rf23.get("dig_yield_neg_mult", -1)) == 125
and int(rf23.get("combo_cap", -1)) == 5,
"开格流求值:正向乘区 25+10+50=85% · 负向加重 25+100=125% · 直觉连击上限 5(同 hook 累加——三件同持)")
check(int(rf23.get("deep_dig_cost", -1)) == 1 and int(rf23.get("deep_dig_cap", -1)) == 4
and int(rf23.get("ap_carry_value", -1)) == 1 and int(rf23.get("shield_per_layer", -1)) == 1
and int(rf23.get("spike_mult", -1)) == 2,
"深挖钻头=开掘 +1 AP/每 4 格深层产出(R4 甲非信息税对价)· 蓄力三件=层值 1/减伤 1/层/满层 ×2")
check(orch.action_economy.feat_ap_carry and orch.action_economy.feat_dig_extra == 1,
"蓄力/钻头 action_economy 注入(ap_carry 改写停手转存 + 付费开掘对价注入——关着 false/0 零扰动)")
# ⑦ 开格/蓄力流订阅面(追加七件)
var ids23: Array = ids16.duplicate()
ids23.append_array(["greedy_lantern", "vein_intuition", "gambler_pact", "deep_drill",
"armory_prep", "iron_wall", "patient_burst"])
orch._fx.rebuild(ids23, [], [])
var pos_m := 0
var neg_m := 0
var combo_cap := -1
for h in orch._fx_emit("dig_reveal", {"cell": Vector2i(1, 1), "round": 1, "is_first_dig_of_battle": false, "terrain_kind": []}):
if String(h.get("kind", "")) != "yield_mult":
continue
pos_m += int(h.get("mult", 0))
neg_m += int(h.get("cost_mult", 0))
if int(h.get("cap", 0)) > 0:
combo_cap = int(h.cap)
check(pos_m == 85 and neg_m == 125 and combo_cap == 5,
"开格流订阅:正向乘区 25+10+50=85% · 负向加重 25+100=125% · 直觉连击上限 5(dig_reveal yield_mult 乘法族三件同持收集)")
var dd7: Array = orch._fx_emit("dig_paid", {"ap_cost": 1, "ap_surplus": 1})
check(orch._hits_of(dd7, "counter_spend").size() == 1
and bool((orch._hits_of(dd7, "counter_spend")[0] as Dictionary).has("fallback")),
"开格流订阅:深挖钻头 dig_paid→counter_spend 对价(fallback 降级声明——AP 不足只付基础 1)")
var re7: Array = orch._fx_emit("round_end", {"round": 1})
check(orch._hits_of(re7, "grant_ap").size() == 1
and String((orch._hits_of(re7, "grant_ap")[0] as Dictionary).get("mode", "")) == "transfer",
"蓄力流订阅:整备军备 round_end→grant_ap transfer(剩 AP 转蓄力层——finish_round_stop 定夺)")
var ab7: Array = orch._hits_of(orch._fx_emit("attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}), "attack_bonus")
check(ab7.size() == 1 and String((ab7[0] as Dictionary).get("per", "")) == "charge_layers",
"蓄力流订阅:整备军备 attack_commit→attack_bonus per charge_layers(每层下轮首段 +1)")
orch.action_economy._charge_layers = 2
var sp7: Array = orch._fx_emit("attack_commit", {"atk": 10, "target": "敌", "matchup": "normal_vs_light", "dig_streak": 0, "nest_count": 0})
var spike7_off: Array = orch._hits_of(sp7, "modify_ticket")
var spike_off_n := 0
for h in spike7_off:
if String((h as Dictionary).get("mode", "")) == "spike":
spike_off_n += 1
check(spike_off_n == 0,
"蓄力流订阅:厚积薄发未满层(charge_layers 2<3)→零命中(when 封闭判定——满层持续状态口径)")
orch.action_economy._charge_layers = 3
var sp7b: Array = orch._fx_emit("attack_commit", {"atk": 10, "target": "敌", "matchup": "normal_vs_light", "dig_streak": 0, "nest_count": 0})
var spike7: Array = []
for h in orch._hits_of(sp7b, "modify_ticket"):
if String((h as Dictionary).get("mode", "")) == "spike":
spike7.append(h)
check(spike7.size() == 1 and int((spike7[0] as Dictionary).get("value", -1)) == 2,
"蓄力流订阅:厚积薄发满层→modify spike ×2(discount 恒全额+基值倍乘)")
orch.action_economy._charge_layers = 0
check(orch._hits_of(orch._fx_emit("damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1}), "shield").size() == 1,
"蓄力流订阅:铁壁整备 damage_taken→shield per charge_layers(蓄力转减伤)")
# ⑦c 特性持有呈现段(2026-10-04 用户反馈补齐——C 区「特性」节:聚合行+成型档位;空态隐藏)
var panel = load("res://presentation/views/hero_info_panel_view.gd").new()
panel.set_feat_holdings([])
@@ -778,30 +846,46 @@ func _smoke_feats() -> void:
panel.set_feat_holdings(["魔法 ×2 · 档一", "开格 ×1 · 散件"])
check(panel._feat_section.visible and panel._feat_list.get_child_count() == 2,
"特性段持有呈现(聚合行+成型档位常驻可读——「局内奖励选了什么看不到」缺口承载)")
# ⑦b 深挖钻头付费开掘回归(2026-10-04 试玩缺陷修复——对价挂付费侧):AP=1 降级照常开格
# (用户实测「AP=1 无法开格只能标旗」的回归闸);AP=2 付全价积付费计数(深层产出配额取数面)。
check(String(load("res://presentation/battle_narrator.gd").feat_card_text({"feat_id": "arcane_ember", "display_name": "奥术余烬", "archetype": "magic", "desc": "普攻附加必中伤害"})).contains("普攻附加必中伤害"),
"特性卡文案表值化:desc 行字段直读(T3 文案入表——零硬编码映射)")
# ⑦b 深挖钻头付费开掘回归(2026-10-04 试玩缺陷修复口径,T3 对价面换代=dig_paid 订阅施加):
# AP=1 降级照常开格(fallback 只付基础 1 不积计数);AP=2 付全价积付费计数(深层产出配额取数面)。
var ae_dd = load("res://core/action_economy.gd").new(_db)
ae_dd.feat_dig_extra = 1
var fx_dd = EffectEngine.new(_db)
fx_dd.rebuild(["deep_drill"], [], [])
ae_dd.set_fx(fx_dd)
ae_dd._phase = 1 if ae_dd.Phase.ACTIVE == 1 else ae_dd.Phase.ACTIVE # white-box 相位(冒烟专用)
ae_dd._ap = 1
check(ae_dd.request_dig(0, false) and ae_dd.deep_paid_count == 0,
"钻头降级:AP=1 照常开格(只付基础 1 不积付费——持有钻头永不锁死挖格,散件保底 B-2)")
check(ae_dd.request_dig(0, false) and fx_dd.counter("deep_paid_round") == 0,
"钻头降级:AP=1 照常开格(fallback 只付基础 1 不积付费——持有钻头永不锁死挖格,散件保底 B-2)")
ae_dd._ap = 2
ae_dd._phase = ae_dd.Phase.ACTIVE # 首挖 AP 归 0 已翻轮(ap_exhausted 仲裁)——白盒复位续测
check(ae_dd.request_dig(0, false) and ae_dd.deep_paid_count == 1,
"钻头付费开掘:AP=2 付全价(基础 1+对价 1——deep_paid_count 积数=深层产出配额取数面)")
# ⑧ T6 吞噬流求值面(27 件全量——七流派档位成型)
check(ae_dd.request_dig(0, false) and fx_dd.counter("deep_paid_round") == 1,
"钻头付费开掘:AP=2 付全价(基础 1+对价 1——deep_paid_round 计数器=授予段追加产出配额取数面)")
# ⑧ 吞噬流订阅面(27 件全量——七流派档位成型)
var ids27: Array = ids23.duplicate()
ids27.append_array(["ley_harvest", "thorn_weaver", "purge_sapper", "magma_body"])
orch._feat_flags_for(ids27)
var rf27: Dictionary = orch._damage.feat_flags
check(int(rf27.get("terrain_refund", -1)) == 1 and int(rf27.get("terrain_dig_double", -1)) == 1
and int(rf27.get("flag_neutralize", -1)) == 1 and int(rf27.get("variant_counter_value", -1)) == 1,
"吞噬流求值:地脉采撷补偿 1 · 荆棘编织双产出 · 净化工兵中和旗(🔴 仅伤害型——过滤在消费面)· 熔火反噬 1/层")
orch._fx.rebuild(ids27, [], [])
var dt8: Array = orch._fx_emit("damage_taken", {"kind": "terrain", "amount": 3, "source": "thorn_field", "round": 1, "terrain_kind": "thorn_field"})
check(orch._hits_of(dt8, "grant_material").size() == 2 and orch._hits_of(dt8, "counter_add").size() == 1,
"吞噬流订阅:地脉采撷+荆棘编织 damage_taken(地形伤)→grant_material ×2 · 熔火之躯→counter_add 积层")
check(orch._hits_of(orch._fx_emit("flag_hit", {"cell": Vector2i(1, 1), "round": 1, "terrain_kind": ["thorn_field"]}), "rewrite_board").size() == 1
and orch._hits_of(orch._fx_emit("flag_hit", {"cell": Vector2i(1, 1), "round": 1, "terrain_kind": []}), "rewrite_board").is_empty(),
"吞噬流订阅:净化工兵 flag_hit 邻伤害型地形→rewrite_board · 无伤害型邻格零命中(🔴 仅伤害型——terrain_kind has 在场判定)")
orch._fx.counter_add("magma_layers", 2, false)
var re8: Array = orch._fx_emit("round_end", {"round": 1})
check(orch._hits_of(re8, "open_ticket").size() == 1
and int((orch._hits_of(re8, "open_ticket")[0] as Dictionary).get("value", -1)) == 1
and String((orch._hits_of(re8, "open_ticket")[0] as Dictionary).get("per", "")) == "magma_layers",
"吞噬流订阅:熔火之躯 round_end→open_ticket per magma_layers(受地形伤积层轮末反打)")
check(orch._hits_of(re8, "counter_add").size() == 1
and (orch._hits_of(re8, "counter_add")[0] as Dictionary).has("set"),
"吞噬流订阅:熔火之躯反打后积层清零(counter_add set——即时执行族按声明序)")
var orch0 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(74))
orch0._feat_flags_for([])
check((orch0._damage.feat_flags as Dictionary).is_empty(),
"零持有求值=零旗(relic 关着零扰动——伤害管线既有行为逐位不变)")
orch0._fx.rebuild([], [], [])
check(orch0._fx_emit("attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}).is_empty()
and orch0._fx_emit("dig_reveal", {"cell": Vector2i(1, 1), "round": 1, "is_first_dig_of_battle": true, "terrain_kind": []}).is_empty(),
"零持有订阅=零命中(relic 关着零扰动——伤害管线既有行为逐位不变)")
# 手工场景驱动:L1 全程 + L2 推进到商店房(_drive_level 复用 + 兑付确认/选路/大奖励确认手工编排)
@@ -1458,10 +1542,10 @@ func _smoke_global_variant() -> void:
var base_sum: int = eco._pool_weight(base_rows)
check(base_sum == 77, "偏斜基线:正向池现行合计 77(挖掘风险批口径)")
eco.board_skew = 0
var off_rows: Array = eco._biased_cell_rows()
var off_rows: Array = eco._biased_cell_rows(0) # 内容数据驱动扩展批 T4:偏运权重改 reward_roll 订阅命中入参(luck_bias 注入位退役)
check(off_rows.size() == base_rows.size() and _rows_weights_equal(off_rows, base_rows), "off 态零扰动:board_skew=0 行集与基线逐位相同(不进投影域)")
eco.board_skew = 30
var on_rows: Array = eco._biased_cell_rows()
var on_rows: Array = eco._biased_cell_rows(0)
check(eco._pool_weight(on_rows) == 77, "合计守恒:偏斜后正向池合计 = 77(整数最大余数重归一)")
var wmap := _rows_weight_map(on_rows)
check(wmap.get("weaken", -1) == 8 and wmap.get("ap_up", -1) == 6 and wmap.get("gp_coin", -1) == 2,
@@ -1470,16 +1554,14 @@ func _smoke_global_variant() -> void:
check(wmap.get("attack_up", -1) == 15 and wmap.get("spark_aoe", -1) == 18 and wmap.get("pierce_strike", -1) == 9 and wmap.get("magic_bolt", -1) == 9,
"进攻族上浮:attack 14→15 / blast 16→18 / pierce 8→9 / magic 8→9(v=30 构造例,等比上浮守恒)")
eco.board_skew = 30
eco.luck_bias = 2
var combo_rows: Array = eco._biased_cell_rows()
var combo_rows: Array = eco._biased_cell_rows(2) # 件级加算 2=偏运硬币行 value(reward_roll bias_rewards 命中面同值)
var cw := _rows_weight_map(combo_rows)
check(eco._pool_weight(combo_rows) == 77 + 4 * 2, "合成序:图级偏斜先(乘法面)件级 luck_bias 后(加法 +2×四稀有类)——合计 77+8")
check(cw.get("heal_minor", -1) == 5 and cw.get("gp_coin", -1) == 4 and cw.get("pierce_strike", -1) == 11 and cw.get("magic_bolt", -1) == 11,
"合成序构造例:skew(heal 3)+2=5 / gp 2+2=4 / pierce 9+2=11 / magic 9+2=11(图级定型后玩家构筑叠加)")
eco.luck_bias = 0
eco.board_skew = 50
var v50: Dictionary = _rows_weight_map(eco._biased_cell_rows())
check(eco._pool_weight(eco._biased_cell_rows()) == 77 and int(v50.get("heal_minor", -1)) > 0, "域上界 v=50:守恒成立且逐行权重恒正(域 [0,50] 安全)")
var v50: Dictionary = _rows_weight_map(eco._biased_cell_rows(0))
check(eco._pool_weight(eco._biased_cell_rows(0)) == 77 and int(v50.get("heal_minor", -1)) > 0, "域上界 v=50:守恒成立且逐行权重恒正(域 [0,50] 安全)")
# 普通图摇号全链路确定性(off 路径)——同种子普通图资源映射逐位相同(golden 对拍另有批前快照)
var nrng = load("res://core/rng_service.gd").new()
nrng.set_master_seed(42)