feat(feats): 迭代20 T3 专长 27+2 件换代(专长实装批 B段一)

feats.tres 整表换代:旧 27 件删除(素材库表二退役面),重设计 29 行重建(基础 27+迁入
淬毒刃纹/封印爆裂——卡文 §10.4 推荐案件数口径);订阅行 on/when/do=效果引擎封闭词表;
rarity 四级直映射令牌档(普通 12/优秀 9/稀有 8/传说 0);cap 已拿上限字段(案②解耦);
desc 禁「雷/插旗」字面(契约 6——封印/陷阱风味系);dispatch 预评估口径「每第 N 次」按
N-1 落值(mod_eq ref/eq 阈值-1——effect_words_t1_test 钉住)。

feat_table schema 换代:RARITIES 四级;ROW_KEYS 摘 archetype 增 cap;流派扫雷铁律校验
随流派框架退役摘除(奇物.md v1「不设流派标签、不设档位机制」)。

battle_orchestrator:FeatsEngine 档位求值与 rider_tier 注入摘除(V14 旧实现删除覆盖——
散件单体效果=唯一形态);FACTS 补 pct(处刑台血量阈值沿)。

smoke 特性段整段换代:牌库构成 29 件四级断言(V2)/契约静态钉扎(V18 牌库 kind 全集/
V19 零棋盘改写订阅/V21 禁字初筛/V22 结构锚形态机检半)/临时表新 schema 注入/门票与
relic 兑付断点主语换代/V3 附表 A 逐件三列断言 29 件(命中面 dispatch 直测+counter 落账
施加面同构 helper);同调测试两处主语 venom_edge/flag_burst→nest_counter/dawn_order
(V14 换桩——T9 迁出 treasure_items 前置)。冒烟 863/0+run_all 20 套全绿。
This commit is contained in:
DKChan committed 2026-10-11 23:58:48 +08:00
1 parent 0640b0c06d
commit 54ab330beb
6 files changed
+1151 -830

No files matched your search

+7 -12
View File
@@ -190,8 +190,7 @@ func _fx_fact(fact: String):
return hits_n
"hp_ratio":
var hu: Dictionary = _enemy_system.hero_unit()
var hmax := maxi(1, int(hu.get("max_hp", 1)))
return float(int(hu.get("hp", 0))) / float(hmax)
return int(round(100.0 * float(int(hu.get("hp", 0))) / float(maxi(1, int(hu.get("max_hp", 1)))))) # 百分比 int(0-100——when lte 算子 int 截断比较口径)
return null
@@ -326,14 +325,10 @@ func begin_battle(enc: Dictionary) -> bool:
_damage.fx_reveal_request = Callable(self, "_fx_enqueue_reveal")
_damage.fx_ac_request = Callable(self, "_apply_ac_bonus")
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
# 迭代 20 T3 换代(V14 旧实现摘除)——流派档位质变随重设计退役(奇物.md v1「不设流派标签、
# 不设档位机制」):FeatsEngine 求值与 rider_tier 注入拆除,散件单体效果=唯一形态
_damage.rider_tier_bonus = 0
_damage.rider_tier_crit = false
_feat_first_dig_used = false
# 树 rule 键消费(迭代 12 T10 → 内容数据驱动扩展批 T3 换代:Fork 激活侧效果=效果引擎合成
# 订阅(rebuild 注入——round_start 首轮 AP+1 / damage_taken 减伤+1),旧直调拆除。其余 rule 键
@@ -624,7 +619,7 @@ func _settle() -> void:
var mine_power := int((snap as Dictionary).get("power", 0))
var mh_hits := _fx_emit("mine_hit", {"cell": evt.cell, "round": ri,
"damage": mine_power, "dug_under_flag": bool(evt.get("dug_under_flag", false)),
"hp_ratio": float(int(_enemy_system.hero_unit().get("hp", 0))) / float(maxi(1, int(_enemy_system.hero_unit().get("max_hp", 1))))})
"hp_ratio": int(round(100.0 * float(int(_enemy_system.hero_unit().get("hp", 0))) / float(maxi(1, int(_enemy_system.hero_unit().get("max_hp", 1))))))})
var halve_pct := 0
var mine_reduce := 0
for h in mh_hits:
@@ -679,7 +674,7 @@ func _settle() -> void:
var dt_amount := int(vrow.get("effect_value", 0))
var dt_hits := _fx_emit("damage_taken", {"kind": "terrain", "amount": dt_amount,
"source": String(vid), "cell": evt.cell, "round": ri, "terrain_kind": String(vid),
"hp_ratio": float(int(_enemy_system.hero_unit().get("hp", 0))) / float(maxi(1, int(_enemy_system.hero_unit().get("max_hp", 1))))})
"hp_ratio": int(round(100.0 * float(int(_enemy_system.hero_unit().get("hp", 0))) / float(maxi(1, int(_enemy_system.hero_unit().get("max_hp", 1))))))})
for h in dt_hits:
match String(h.get("kind", "")):
"damage_reduce":
+1 -1
View File
@@ -113,7 +113,7 @@ func _fx_fact(fact: String):
return dig_streak
"hp_ratio":
var hu: Dictionary = units.hero_unit()
return float(int(hu.get("hp", 0))) / float(maxi(1, int(hu.get("max_hp", 1))))
return int(round(100.0 * float(int(hu.get("hp", 0))) / float(maxi(1, int(hu.get("max_hp", 1)))))) # 百分比 int(when lte int 截断口径)
return null
+1 -1
View File
@@ -53,7 +53,7 @@ const EFFECT_KINDS := ["add_ticket_rider", "modify_ticket", "damage_mult", "open
const FACTS := ["matchup", "is_first_dig_of_battle", "terrain_kind", "target_armor_class",
"flags_count", "charge_layers", "nest_count", "round_index", "counter",
"new_resource_type", "is_edge_cell", "is_blank_zero", "dug_under_flag",
"ap_exhausted_last", "hp_ratio", "amount", "ap_left"]
"ap_exhausted_last", "hp_ratio", "amount", "ap_left", "pct"]
## when 算子词汇(封闭——fact 查询面落法随批登记:mod_zero=计数器取模判满〔掘进蓄能〕、
## advantageous=克制矩阵正面判定〔淬毒刃纹〕、gte_ref/lt_ref=与另一计数器比较〔深挖钻头配额〕、
+23 -40
View File
@@ -1,8 +1,9 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 七核心流派各 ≥3 件入局内随机渠道后,标签+档位激活引擎能否机械判「成型」、
# 专长带能否挂进令牌池稀有/罕见档随机投放(P0——迭代 12 双层 BD 实施批 T1)
# Date: 2026-10-06(内容数据驱动扩展批 T3——行换代:hook/effects 挂点形态退役,triggers 事件订阅形态
# 唯一合法行形——旧 23 挂点枚举消亡:事件名+效果类型取代,需求卡片 R4 裁决)
# Validation Question: 专长 27 件订阅行能否经效果引擎统一解释执行、逐件行为与声明一致(P0——
# 迭代 20 专长实装批 T3;前史:迭代 12 双层 BD 实施批 T1 建表/迭代 17 内容数据驱动扩展批 T3 行换代)
# Date: 2026-10-11(迭代 20 专长实装批 T3——重设计换代:流派标签/档位激活机制退役〔奇物.md v1
# 设计原则「不设流派标签、不设档位机制」〕;rarity 四级〔跨层框架〕;cap 已拿上限字段〔池结构案D
# 概率与上限解耦——每件标注两个独立属性:概率位=档+rarity、上限=cap(拿了才计、刷出不计)〕)
# 概念指路:本表的专长,与 adventure.tres 的属性值提升、资源,同属一个对象——
# 「奇物」(一次冒险内各渠道给予玩家的增益总称)。定义见 design/gdd/systems/冒险循环/奇物.md。
# 它们是它分类轴上的枚举(专长 / 属性值提升 / 资源),不是三套系统;本表承载「专长」(专长 Feat)的逐项定义。
@@ -11,29 +12,25 @@
# 目标渠道:专长走关底 Boss 击败后专属三选一(不新增节点类型),见奇物文档 2.1 节。)
# 数据配置 FeatTable schema(奇物·专长定义表;行 = Dictionary 键 feat_id,Rule 4 豁免
# 定义表——TreasureItemTable 先例)。
# 行字段:feat_id 主键 / display_name / rarity ∈ {common, rare, rarest}(对齐令牌池三档命名——
# 投放档映射: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 行订阅扫雷侧事件
# (EffectEngine.MINE_EVENTS——加载校验,违反=红线①触碰面)。
# 行字段:feat_id 主键 / display_name / rarity ∈ {common, excellent, rare, legendary}(四级稀有度,
# 与令牌池档位词表同名词族——令牌档 = rarity 直映射)/ cap 已拿上限(正整数,缺省 1——「同一件
# 最多拿 N 次」;概率位与上限解耦见奇物.md 池结构节)/ desc 效果说明(文案表值化——呈现层读行,
# 零硬编码)/ triggers 订阅行数组(订阅哪个事件 on / 什么条件 when / 什么效果 do——效果引擎
# EffectEngine 统一解释执行;四词表封闭校验=EffectEngine.validate_trigger_row 单一事实源)。
# 🔴 层级纪律(需求卡片红线 2):专长只走局内随机渠道(Boss 三选一/凭证开启)——本表行永不入同调位
# (treasure_items 表)/局外树(T8 节点表)。
# 退役注记(V14 旧实现摘除):ARCHETYPES 流派标签/档位激活(FeatsEngine 档位质变)随重设计退役——
# 事件族(开格/插旗/踩雷/节奏/战斗系)仅系设计侧分组工具不上数据行不上卡面(命名铁律)。
class_name FeatTable
extends Resource
@export var rows: Array = [] # Array[Dictionary] 键 feat_id
## 流派标签词表(七核心——喂雷/淘金不在本批,R5 甲/缓议)
const ARCHETYPES := ["magic", "pierce", "normal", "dig", "flag", "charge", "devour"]
## 稀有度词表(四级——与令牌池 TIERS 同名词族;令牌档 = rarity 直映射)
const RARITIES := ["common", "excellent", "rare", "legendary"]
## 稀有度词表(对齐令牌池三档;投放档映射 relic→rare / rarest→rarest)
const RARITIES := ["common", "rare", "rarest"]
## 行键白名单(🔴 零裸 flat 同族护栏——键出域=加载致命;triggers/desc=T3 换代新形态)
const ROW_KEYS := ["feat_id", "display_name", "rarity", "archetype", "triggers", "desc", "note"]
## 行键白名单(🔴 零裸 flat 同族护栏——键出域=加载致命)
const ROW_KEYS := ["feat_id", "display_name", "rarity", "cap", "triggers", "desc", "note"]
## trigger 行键(订阅行三字段:on/when?/do)
const TRIGGER_KEYS := ["on", "when", "do"]
@@ -42,10 +39,8 @@ const TRIGGER_KEYS := ["on", "when", "do"]
func validate() -> Array:
var errs: Array = []
if rows.is_empty():
return errs # 空表合法态(T1 基建先行——逐流派填行)
return errs # 空表合法态(T1 基建先行——逐件填行)
var seen := {}
# 流派扫雷覆盖聚合(🔴 红线①铁律——每流派 ≥1 行订阅扫雷侧事件)
var arch_mine := {} # archetype → bool
for row in rows:
var rid: String = row.get("feat_id", "")
if rid == "":
@@ -59,30 +54,18 @@ func validate() -> Array:
errs.append({"category": "domain", "message": "%s display_name 空" % rid})
if not RARITIES.has(String(row.get("rarity", ""))):
errs.append({"category": "enum", "message": "%s rarity 出域 %s" % [rid, str(RARITIES)]})
var arch := String(row.get("archetype", ""))
if not ARCHETYPES.has(arch):
errs.append({"category": "enum", "message": "%s archetype 出域 %s" % [rid, str(ARCHETYPES)]})
if row.has("cap") and int(row.get("cap", 1)) < 1:
errs.append({"category": "domain", "message": "%s cap 出域 [1,∞): %d" % [rid, int(row.get("cap", 1))]})
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
else:
EffectEngine.validate_trigger_row(rid, row, row.get("triggers", []) as Array, errs)
for k in row.keys():
if not ROW_KEYS.has(String(k)):
errs.append({"category": "zero_flat", "message": "%s 行键出白名单: %s" % [rid, String(k)]})
# 🔴 红线① 扫雷触发点铁律:**有行的流派**各 ≥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:
File diff suppressed because it is too large. Load diff
+272 -210
View File
@@ -633,58 +633,76 @@ func _smoke_adventure() -> void:
"放弃摘要:abandoned · 未打穿(已获大奖励不追回——同失败型结算)")
# ---- 双层 BD 规则改写型特性基建冒烟(迭代 12 T1——feat_table/档位激活引擎/规则改写型特性带池接线)----
# ---- 专长重设计实装冒烟(迭代 20 T3——feats 四级换代 · 案D 池接线 · V3 订阅面逐件三列)----
func _smoke_feats() -> void:
print("--- 规则改写型特性基建(feat_table · 档位激活引擎 · 规则改写型特性带接线)---")
# ① 现行表牌库构成:七流派 27 件全量入池——98 数值令牌+27 relic 令牌(批次一铺量完成)
check(_db.feats != null and (_db.feats.rows as Array).size() == 27, "feat_table 已加载(七流派 27 件全量——批次一 T2-T6 铺量完成)")
print("--- 专长重设计实装(feat_table 四级 · 专长带池接线 · 订阅面逐件)---")
# ① 现行表牌库构成:重设计 29 件全量入池——98 数值令牌+29 relic 令牌(四级 rarity 直映射)
check(_db.feats != null and (_db.feats.rows as Array).size() == 29, "feat_table 已加载(重设计 29 件=基础 27+迁入 2——卡文 §10.4 推荐案口径)")
check(_db.feats.validate().is_empty(), "feat_table.validate 零违例(四级 rarity/cap 域/trigger 词表/行键白名单全过)")
var a_r0 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(71))
check(a_r0.start_adventure(71, {"first_script": true}), "规则改写型特性带场景:冒险启动(seed 71)")
check(a_r0.start_adventure(71, {"first_script": true}), "专长带场景:冒险启动(seed 71)")
var feat_tokens := 0
var rare_tier_tokens := 0
var tier_counts := {}
for t in a_r0._curio_deck:
if String((t as Dictionary).get("kind", "")) == "relic":
feat_tokens += 1
if String((t as Dictionary).get("tier", "")) == "rare":
rare_tier_tokens += 1
check(feat_tokens == 27 and rare_tier_tokens == 20 and a_r0._curio_deck.size() == 125,
"规则改写型特性带入池:27 件(transitional 直映射——旧 rare 名直通 14+旧 rarest 回退 6=稀有档 20、旧 common 7=普通档)——牌库 98+27=125〔专长批 V14 换代〕")
check(_db.feats.validate().is_empty(), "feat_table.validate 零违例(有行流派扫雷端铁律——magic 魔能共鸣 dig_reveal 订阅在案)")
# ② 档位激活引擎(纯函数直测——构造表 4 件 magic + 1 件 flag;内容数据驱动扩展批 T3 断言面随迁:
# 行=triggers 事件订阅形态,挂点投影断言换代=效果引擎订阅装载序〔feats 表序〕)
var tk := String((t as Dictionary).get("tier", ""))
tier_counts[tk] = int(tier_counts.get(tk, 0)) + 1
check(feat_tokens == 29 and int(tier_counts.get("common", 0)) == 12 and int(tier_counts.get("excellent", 0)) == 9 and int(tier_counts.get("rare", 0)) == 8,
"专长带入池:29 件四级直映射(普通 12/优秀 9/稀有 8/传说 0)——牌库 98+29=127〔专长批 V2〕")
# ② 旧 27 件退役钉面(V2——arcane_ember 系旧件表首行;全仓 grep 零残留归批末静态核查)
check(_db.feats.row_by_id("arcane_ember").is_empty() and _db.feats.row_by_id("magma_body").is_empty(),
"旧 27 件已退役:arcane_ember/magma_body 表内零残留〔专长批 V2〕")
# ③ 契约面静态钉扎(V18/V19/V21/V22——人工核对 V20/V21 裁决面以外的可机检半)
var kinds_ok := true
for t in a_r0._curio_deck:
var kk := String((t as Dictionary).get("kind", ""))
if kk != "relic" and not _db.adventure.CURIO_KINDS.has(kk):
kinds_ok = false
check(kinds_ok, "层级纪律:牌库 kind 全集=CURIO_KINDS+relic(秘宝 item_id 零入池)〔专长批 V18〕")
var rewrite_rows := 0
for row_v in (_db.feats.rows as Array):
for tv in ((row_v as Dictionary).get("triggers", []) as Array):
if String(((tv as Dictionary).get("do", {}) as Dictionary).get("kind", "")) == "rewrite_board":
rewrite_rows += 1
check(rewrite_rows == 0, "棋盘事实不可变:新 29 行零棋盘改写类订阅(rewrite_board 已除名消费面)〔专长批 V19〕")
var bad_words := 0
for row_v in (_db.feats.rows as Array):
var r_d: Dictionary = row_v as Dictionary
for txt_v in [String(r_d.get("display_name", "")), String(r_d.get("desc", ""))]:
if String(txt_v).contains("雷") or String(txt_v).contains("插旗"):
bad_words += 1
check(bad_words == 0, "对外命名纪律:29 行 display_name/desc 零「雷/插旗」字面(封印/陷阱风味系)〔专长批 V21 初筛〕")
check(float((_db.feats.row_by_id("trap_burst").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("mult", 0)) > 1.0
and int((_db.feats.row_by_id("blood_honey").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("value", 99)) < 5
and int((_db.feats.row_by_id("stone_skin").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("cap", 0)) == 3,
"数值纪律:陷阱爆发倍率>1 · 血蜜回复<典型陷阱伤 · 石肤上限 3 在册(结构锚形态内 provisional)〔专长批 V22 机检半〕")
# ④ 临时表注入(新 schema 形态——零 archetype/cap 直用):5 行 relic 入牌库/兑付/踢池/断点往返
var rt := FeatTable.new()
rt.rows = [
{"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}}]},
{"feat_id": "x1", "display_name": "试一", "rarity": "common", "desc": "测试揭示", "cap": 1,
"triggers": [{"on": "mine_hit", "do": {"kind": "reveal_tiles", "value": 1}}]},
{"feat_id": "x2", "display_name": "试二", "rarity": "excellent", "desc": "测试回复", "cap": 1,
"triggers": [{"on": "mine_hit", "do": {"kind": "heal", "value": 2}}]},
{"feat_id": "x3", "display_name": "试三", "rarity": "rare", "desc": "测试群伤", "cap": 1,
"triggers": [{"on": "flag_hit", "do": {"kind": "open_ticket", "channel": "blast", "value": 3}}]},
{"feat_id": "x4", "display_name": "试四", "rarity": "rare", "desc": "测试减伤", "cap": 2,
"triggers": [{"on": "damage_taken", "when": [{"fact": "hp_ratio", "op": "lte", "value": 50}], "do": {"kind": "damage_reduce", "value": 2}}]},
{"feat_id": "x5", "display_name": "试五", "rarity": "legendary", "desc": "测试AC", "cap": 1,
"triggers": [{"on": "flag_hit", "do": {"kind": "ac_bonus", "value": 1, "mode": "round"}}]},
]
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 档二——以撒套装/小骨铭文先例)")
check(FeatsEngine.is_formed(tiers, "magic") and not FeatsEngine.is_formed(tiers, "flag"),
"成型判读:magic 成型(tier≥1)· flag 未成型(散件单体保底——档位只质变不启动)")
var t2 := FeatsEngine.tiers_for(["m1", "m2"], rt)
check(int(t2.get("magic", -1)) == 1, "档位激活:2 件→档一(同集合同档位确定性——MC 逐位可复放承载)")
# ③ 规则改写型特性带接线(临时表注入——relic 令牌入牌库/兑付入持有集/踢池/断点往返;测试尾还原现行表)
check(rt.validate().is_empty(), "新 schema 构造表 validate 零违例(四级 rarity/cap 字段/词表全过——流派标签已退役零 archetype)")
var live_rows: FeatTable = _db.feats
_db.feats = rt
var a_r = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(72))
check(a_r.start_adventure(72, {"first_script": true}), "规则改写型特性带场景:冒险启动(seed 72——注入 5 行临时表)")
check(a_r.start_adventure(72, {"first_script": true}), "专长带场景:冒险启动(seed 72——注入 5 行临时表)")
var deck_relic := 0
for t in a_r._curio_deck:
if String((t as Dictionary).get("kind", "")) == "relic":
deck_relic += 1
check(deck_relic == 5 and a_r._curio_deck.size() == 103,
"规则改写型特性带入池:5 行 relic 每件 1 张(transitional——旧 common/rare→优秀档)——牌库 98+5=103")
"专长带入池:5 行 relic 每件 1 张(四级 rarity 直映射)——牌库 98+5=103")
# T4 渠道白名单断言(迭代 15 卡②——渠道轴定位:通关渠道只出专长/节点渠道零专长)
var feat_seen: Dictionary = {}
var node_relic_seen := false
@@ -696,15 +714,15 @@ func _smoke_feats() -> void:
if String((t as Dictionary).get("kind", "")) == "relic":
node_relic_seen = true
check(feat_seen.size() >= 2, "通关渠道只出专长(60 次重抽 ≥2 种不同 relic——身份键 feat_id 同档可多件)")
check(not node_relic_seen, "节点渠道零专长(60 次重抽零 relic——专长在非 Boss 渠道只以门票形态出现,T4 渠道白名单)")
# T5 门票行为断言(A5 拿到即开 / A6 保底计数——契约④四口径;独立 runner 防污染 a_r 牌库——后续 relic 兑付断言按 5 枚锚定)
check(not node_relic_seen, "节点渠道零专长(60 次重抽零 relic——专长在非 Boss 渠道只以门票形态出现)")
# T5 门票行为断言(A5 拿到即开 / A6 保底计数——契约④四口径;独立 runner 防污染 a_r 牌库)
var a_t = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(91))
a_t.start_adventure(91, {"first_script": true})
var stock_before := {}
for kk in ["attack", "gp", "heal"]:
stock_before[kk] = a_t.pool_stock(kk)
a_t._node_curio_pending = {"candidates": [{"kind": "feat_ticket", "tier": "rare"}, {"kind": "attack", "tier": "common"}], "aux_detail": []}
check(a_t.apply_node_curio("feat_ticket", "rare"), "A5 门票兑付受理(节点渠道——拿到即开)")
a_t._node_curio_pending = {"candidates": [{"kind": "feat_ticket", "tier": "excellent"}, {"kind": "attack", "tier": "common"}], "aux_detail": []}
check(a_t.apply_node_curio("feat_ticket", "excellent"), "A5 门票兑付受理(节点渠道——拿到即开)")
var tkt_cands: Array = (a_t.node_curio_pending_state().get("candidates", []) as Array)
var tkt_all_relic := tkt_cands.size() > 0
for t in tkt_cands:
@@ -734,12 +752,8 @@ func _smoke_feats() -> void:
offered = true
break
check(offered, "A6 概率面存在(chance=20% 200 展示内必出现——零触发=概率面失守)")
a_r._curio_pending = {"candidates": [{"kind": "relic", "tier": "rare", "feat_id": "m3"}, {"kind": "gp", "tier": "common"}], "payload": {}}
var deck_m3 := 0
for t in a_r._curio_deck:
if String((t as Dictionary).get("feat_id", "")) == "m3":
deck_m3 += 1
check(a_r.pick_curio("relic", "rare", "m3") and (a_r.feats_held() as Array).has("m3") and a_r.pool_stock("relic") == deck_relic - 1,
a_r._curio_pending = {"candidates": [{"kind": "relic", "tier": "rare", "feat_id": "x3"}, {"kind": "gp", "tier": "common"}], "payload": {}}
check(a_r.pick_curio("relic", "rare", "x3") and (a_r.feats_held() as Array).has("x3") and a_r.pool_stock("relic") == deck_relic - 1,
"relic 兑付:入持有集 + 牌库摘牌(选中踢池——池库存 %d→%d)" % [deck_relic, deck_relic - 1])
# 断点往返(持有集+余库 feat_id 随行)
var sm_r = load("res://core/save_manager.gd").new("user://_smoke_feats_bp.json")
@@ -747,176 +761,224 @@ func _smoke_feats() -> void:
a_r.set_save_manager(sm_r)
a_r._on_level_finished({"result": "victory", "hp": int(a_r.ledger.get("hero_hp", 26)), "node_index": 1, "node_id": 1, "node_type": "shop"})
var bp_r: Dictionary = sm_r.breakpoint_snapshot()
check((bp_r.get("relics", []) as Array).has("m3"), "断点附随件:relics 持有集入快照(迭代 12 T1 扩列)")
var bp_deck_m3 := 0
check((bp_r.get("relics", []) as Array).has("x3"), "断点附随件:relics 持有集入快照(迭代 12 T1 扩列)")
check(int((bp_r.get("adventure_counters", {}) as Dictionary).size()) >= 0, "断点附随件:adventure 计数器账本键在案(T6/T7 扩列)")
var bp_deck_x3 := 0
for t in (bp_r.get("bonus_deck", []) as Array):
if String((t as Dictionary).get("feat_id", "")) == "m3":
bp_deck_m3 += 1
check(bp_deck_m3 == 0, "断点余库:已兑付 relic 令牌逐枚还原(m3 零残留——续玩不重复获得)")
if String((t as Dictionary).get("feat_id", "")) == "x3":
bp_deck_x3 += 1
check(bp_deck_x3 == 0, "断点余库:已兑付 relic 令牌逐枚还原(x3 零残留——续玩不重复获得)")
sm_r.clear_breakpoint()
_db.feats = live_rows # 还原现行表(共享 _db 防污染——后续冒烟段零感知)
# ④ 魔法流订阅面(内容数据驱动扩展批 T3 断言面随迁——_feat_flags_for 挂点求值退役:
# 求值断言=效果引擎 rebuild+dispatch 命中面;落地行为=发射点施加面(本段下方各事件直测),
# 全链分岔读数归 T6 MC 对拍)
# ⑤ V3 订阅面逐件(附表 A 三列——命中面=EffectEngine dispatch 直测;账本面=counter 直读;
# 落地行为=发射点施加面〔T1 施加段承载——本段按声明逐件钉命中与边界/反例〕)
var orch = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(73))
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"]
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 判档二确定性)")
# ⑦ 开格/蓄力流订阅面(追加七件)
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([])
check(not panel._feat_section.visible, "特性段空态隐藏(视图空态纪律同特性段)")
panel.set_feat_holdings(["魔法 ×2 · 档一", "开格 ×1 · 散件"])
check(panel._feat_section.visible and panel._feat_list.get_child_count() == 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)
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 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 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._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 fx = orch._fx
# counter 落账 helper(dispatch 只返回命中不落账——施加面同构:counter_add 逐条落账本 helper 承载)
var apply_fx := func(hits: Array) -> void:
for h in hits:
if String(h.get("kind", "")) == "counter_add":
var sv_c = h.get("set", null)
fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if sv_c == null else int(sv_c), sv_c != null)
# 开格系
fx.rebuild(["twin_pick"], [], [])
for i in 4:
var hits_dp: Array = fx.dispatch("dig_paid", {"ap_cost": 1})
var refund := false
for h in hits_dp:
if String(h.get("kind", "")) == "grant_ap":
refund = true
fx.counter_add("twin_digs", 1)
if i == 1 or i == 3:
check(refund, "双管镐:第 %d 次付费挖格返 1 AP(每第 2 次——mod_eq 预评估口径)" % (i + 1))
else:
check(not refund, "双管镐反例:第 %d 次不返(未满 2 次)" % (i + 1))
fx.rebuild(["geologist"], [], [])
var geo_hits: Array = fx.dispatch("dig_reveal", {"new_resource_type": true})
apply_fx.call(geo_hits)
check(orch._hits_of(geo_hits, "counter_add").size() == 1, "地质学家:首开新类型集 1 点〔专长批 V3〕")
fx.counter_add("geo_types", 2) # 手动基数补齐(row1 已落账 1+2=3——满 4 种的预评估现值)
var geo_hits2: Array = fx.dispatch("dig_reveal", {"new_resource_type": true})
apply_fx.call(geo_hits2)
check(orch._hits_of(geo_hits2, "reveal_tiles").size() == 1 and fx.counter("geo_types") == 0,
"地质学家:满 4 种(预评估现值 3+本格)→揭示 1 格·计数清零〔专长批 V3〕")
var geo_hits3: Array = fx.dispatch("dig_reveal", {"new_resource_type": false})
check(orch._hits_of(geo_hits3, "counter_add").is_empty(), "地质学家反例:同类型重复首开不叠点〔专长批 V3〕")
fx.rebuild(["hard_hat"], [], [])
apply_fx.call(fx.dispatch("dig_reveal", {}))
apply_fx.call(fx.dispatch("dig_reveal", {}))
check(fx.counter("helm_layers") == 2, "安全帽:开安全格攒层 ×2〔专长批 V3〕")
var hh_hits: Array = fx.dispatch("mine_hit", {"damage": 6})
var hh_reduce := 0
for h in hh_hits:
if String(h.get("kind", "")) == "damage_reduce":
hh_reduce = fx.counter(String(h.get("value_counter", "")))
check(hh_reduce == 2, "安全帽:踩雷耗层减伤=层值 2(一次清零随施加面 set 行)〔专长批 V3〕")
fx.rebuild(["skip_mining"], [], [])
var sk_zero: Array = fx.dispatch("dig_reveal", {"is_blank_zero": true})
apply_fx.call(sk_zero)
check(orch._hits_of(sk_zero, "counter_add").size() == 1, "跳格采掘:空白 0 格攒 1 点〔专长批 V3〕")
fx.counter_add("skip_pts", 1) # 手动基数补齐(已落账 1+1=2——满 3 点的预评估现值)
var sk_full: Array = fx.dispatch("dig_reveal", {"is_blank_zero": true})
apply_fx.call(sk_full)
check(orch._hits_of(sk_full, "reveal_tiles").size() == 1,
"跳格采掘:满 3 点→揭示 1 格〔专长批 V3〕")
check(orch._hits_of(fx.dispatch("dig_reveal", {"is_blank_zero": false}), "counter_add").is_empty(),
"跳格采掘反例:普通开格不计点〔专长批 V3〕")
fx.rebuild(["blast_accounting"], [], [])
for i in 5:
var ba_hits: Array = fx.dispatch("dig_paid", {"ap_cost": 1})
for h in ba_hits:
if String(h.get("kind", "")) == "counter_add":
fx.counter_add(String(h.get("counter_id", "")), 1)
check(orch._hits_of(fx.dispatch("round_end", {}), "grant_gp").size() == 1, "爆破会计:本轮挖格花费≥5→+1 GP〔专长批 V3〕")
fx.rebuild(["frontier_miner"], [], [])
check(orch._hits_of(fx.dispatch("dig_reveal", {"is_edge_cell": true}), "grant_material").size() == 1
and orch._hits_of(fx.dispatch("dig_reveal", {"is_edge_cell": false}), "grant_material").is_empty(),
"边陲矿工:边缘格产出 +1·中心格反例零产出〔专长批 V3〕")
# 插旗系
fx.rebuild(["seal_echo"], [], [])
apply_fx.call(fx.dispatch("flag_hit", {}))
apply_fx.call(fx.dispatch("flag_hit", {}))
var se_hits: Array = fx.dispatch("round_end", {})
check(orch._hits_of(se_hits, "grant_ap").size() == 1 and String((orch._hits_of(se_hits, "grant_ap")[0] as Dictionary).get("mode", "")) == "queue",
"封印回响:标中计数≥2→轮末返 1 AP(下轮 queue)〔专长批 V3〕")
fx.rebuild(["seal_smith"], [], [])
var sm_remove := 0
for i in 3:
var sm_dispatch: Array = fx.dispatch("flag_hit", {})
apply_fx.call(sm_dispatch)
for h in sm_dispatch:
if String(h.get("kind", "")) == "remove_mines":
sm_remove += 1
check(sm_remove == 1, "封印匠:标中每第 3 次→中立移除恰一次(频率闸死)〔专长批 V3〕")
fx.rebuild(["seal_stake"], [], [])
check(orch._hits_of(fx.dispatch("flag_hit", {}), "ac_bonus").size() == 1
and String((orch._hits_of(fx.dispatch("flag_hit", {}), "ac_bonus")[0] as Dictionary).get("mode", "")) == "round",
"封印桩:标中本轮 AC +N(round 域——round_start 清零)〔专长批 V3〕")
fx.rebuild(["warning_beacon"], [], [])
check(orch._hits_of(fx.dispatch("flag_hit", {}), "preview_area").size() == 1, "示警烟火:标中揭示 3×3 资源类型(不揭安危位置)〔专长批 V3〕")
# 踩雷/受伤系
fx.rebuild(["pain_memory"], [], [])
apply_fx.call(fx.dispatch("mine_hit", {}))
apply_fx.call(fx.dispatch("mine_hit", {}))
var pm_hits: Array = fx.dispatch("attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0})
var pm_bonus := 0
for h in orch._hits_of(pm_hits, "attack_bonus"):
pm_bonus = fx.counter(String(h.get("value_counter", "")))
check(pm_bonus == 2, "疼痛记忆:攻击附加层值伤害=层值 2·用后清零(set 行随施加面)〔专长批 V3〕")
fx.rebuild(["fuse_wire"], [], [])
var fw_hits: Array = fx.dispatch("damage_taken", {"kind": "terrain", "amount": 6})
var fw_shield := 0
for h in fw_hits:
if String(h.get("kind", "")) == "shield":
fw_shield += int(h.get("value", 0))
check(orch._hits_of(fx.dispatch("damage_taken", {"kind": "terrain", "amount": 3}), "shield").is_empty(),
"保险丝反例:单次 <5 不攒点不触发〔专长批 V3〕")
for h in fx.dispatch("damage_taken", {"kind": "terrain", "amount": 6}):
if String(h.get("kind", "")) == "counter_add":
fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)))
check(fw_shield == 0, "保险丝:首点不出盾(2 点才出——预评估口径)〔专长批 V3〕")
fx.rebuild(["blood_insight"], [], [])
check(orch._hits_of(fx.dispatch("mine_hit", {}), "reveal_tiles").size() == 1, "见血开窍:踩雷揭示 1 格〔专长批 V3〕")
fx.rebuild(["trap_instinct"], [], [])
apply_fx.call(fx.dispatch("mine_hit", {}))
var ti_hits: Array = fx.dispatch("attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0})
var ti_val := 0
for h in orch._hits_of(ti_hits, "attack_bonus"):
ti_val = fx.counter(String(h.get("value_counter", "")))
check(ti_val == 2, "陷阱本能:踩雷本轮攻击 +2(attack_bonus 通道——面板加算)〔专长批 V3〕")
fx.rebuild(["chain_traps", "pain_memory"], [], [])
apply_fx.call(fx.dispatch("mine_hit", {}))
var ct_before: int = fx.counter("pain_layers")
apply_fx.call(fx.dispatch("mine_hit", {}))
check(fx.counter("pain_layers") == ct_before + 2, "连环陷阱:同轮第 2 颗起每颗额外 +1 层(疼痛记忆同账)〔专长批 V3〕")
fx.rebuild(["trap_burst"], [], [])
var tb_hits: Array = fx.dispatch("mine_hit", {"damage": 8})
check(orch._hits_of(tb_hits, "blast_scaled").size() == 1
and float((orch._hits_of(tb_hits, "blast_scaled")[0] as Dictionary).get("mult", 0)) > 1.0,
"陷阱爆发:全体敌雷伤×倍率直伤(blast_scaled——施加面值=damage×mult)〔专长批 V3〕")
fx.rebuild(["blood_honey"], [], [])
check(orch._hits_of(fx.dispatch("mine_hit", {}), "heal").size() == 1, "血蜜:踩雷回复 N 生命〔专长批 V3〕")
fx.rebuild(["stone_skin"], [], [])
var ss_hits: Array = fx.dispatch("damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1})
check(orch._hits_of(ss_hits, "ac_bonus").size() == 1
and int((orch._hits_of(ss_hits, "ac_bonus")[0] as Dictionary).get("cap", 0)) == 3,
"石肤:受伤 AC+1(本场域 cap 3——达上限不再涨由累积器封顶)〔专长批 V3〕")
fx.rebuild(["last_stand"], [], [])
check(orch._hits_of(fx.dispatch("damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1, "hp_ratio": 40}), "damage_reduce").size() == 1
and orch._hits_of(fx.dispatch("damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 2, "hp_ratio": 80}), "damage_reduce").is_empty(),
"背水:生命≤50% 减伤·过半血反例零减伤〔专长批 V3〕")
# 跨族桥
fx.rebuild(["breaker_strike"], [], [])
check(orch._hits_of(fx.dispatch("mine_hit", {"dug_under_flag": true}), "counter_add").size() == 1
and orch._hits_of(fx.dispatch("mine_hit", {"dug_under_flag": false}), "counter_add").is_empty(),
"破封一击:挖开带封印危险格 +1 层·无旗反例零触发〔专长批 V3〕")
# 节奏系
fx.rebuild(["map_exchange"], [], [])
var me_hits: Array = fx.dispatch("dig_stop", {"ap_left": 4})
apply_fx.call(me_hits)
fx.counter_add("stop_swap", 4) # value_event="ap_left" 解析归施加面(action_economy 载荷取值)——冒烟手动落账
check(orch._hits_of(me_hits, "counter_add").size() == 1, "弃图兑换:停手剩余 AP≥3 入账〔专长批 V3〕")
var me_atk := 0
for h in orch._hits_of(fx.dispatch("attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}), "attack_bonus"):
me_atk = fx.counter(String(h.get("value_counter", "")))
check(me_atk == 4, "弃图兑换:折为攻击伤害=剩余 AP 全额·用后清零〔专长批 V3〕")
check(orch._hits_of(fx.dispatch("dig_stop", {"ap_left": 2}), "counter_add").is_empty(),
"弃图兑换反例:剩余 AP<3 不触发〔专长批 V3〕")
fx.rebuild(["last_drop"], [], [])
var ld_hits: Array = fx.dispatch("round_start", {"round": 2, "ap_exhausted_last": true})
check(orch._hits_of(ld_hits, "grant_ap").size() == 1, "一滴不剩:上轮 AP 耗尽→+1 AP〔专长批 V3〕")
check(orch._hits_of(fx.dispatch("round_start", {"round": 3, "ap_exhausted_last": false}), "grant_ap").is_empty(),
"一滴不剩反例:上轮有余不发〔专长批 V3〕")
fx.rebuild(["market_close"], [], [])
for i in 8:
for h in fx.dispatch("dig_reveal", {}):
if String(h.get("kind", "")) == "counter_add":
fx.counter_add(String(h.get("counter_id", "")), 1)
check(orch._hits_of(fx.dispatch("round_end", {}), "grant_gp").size() == 1, "收盘:本轮开格≥8→+1 GP〔专长批 V3〕")
# 战斗系
fx.rebuild(["execution_scholar"], [], [])
check(orch._hits_of(fx.dispatch("kill", {"target": "敌", "overflow": 0}), "reveal_tiles").size() == 1,
"斩首学者:击杀揭示 1 格〔专长批 V3〕")
fx.rebuild(["executioner"], [], [])
var ex_hits: Array = fx.dispatch("enemy_hp_threshold", {"unit": "e1", "pct": 30, "is_boss": false})
check(orch._hits_of(ex_hits, "target_attack_bonus").size() == 1
and orch._hits_of(fx.dispatch("enemy_hp_threshold", {"unit": "e1", "pct": 60, "is_boss": false}), "target_attack_bonus").is_empty(),
"处刑台:敌血量≤30% 挂单位标记加伤·阈值外反例零命中〔专长批 V3〕")
fx.rebuild(["field_dressing"], [], [])
check(orch._hits_of(fx.dispatch("battle_won", {"round": 2}), "heal").size() == 1,
"战地包扎:战斗胜利回复 N 生命(battle_won 新挂点)〔专长批 V3〕")
# 契约尖点
fx.rebuild(["blood_tax"], [], [])
check(orch._hits_of(fx.dispatch("mine_hit", {}), "bonus_delta").size() == 1
and String((orch._hits_of(fx.dispatch("flag_hit", {}), "bonus_delta")[0] as Dictionary).get("axis", "")) == "atk",
"血税:双向同局账本(踩陷阱−2max_hp/封印命中+1atk——bonus_delta 战末汇入)〔专长批 V3〕")
# 迁入 2 件
fx.rebuild(["venom_edge"], [], [])
check(orch._hits_of(fx.dispatch("clamp_check", {"matchup": "magic_vs_heavy"}), "modify_ticket").size() == 1
and orch._hits_of(fx.dispatch("clamp_check", {"matchup": "normal_vs_light"}), "modify_ticket").is_empty(),
"淬毒刃纹(迁入):克制正面(magic_vs_heavy 矩阵值>0)追加·中性面反例零触发〔专长批 V3〕")
fx.rebuild(["flag_burst"], [], [])
check(orch._hits_of(fx.dispatch("flag_hit", {}), "open_ticket").size() == 1,
"封印爆裂(迁入):标中全体敌群伤〔专长批 V3〕")
# 零持有
var orch0 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(74))
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(),
and orch0._fx_emit("dig_reveal", {"cell": Vector2i(1, 1), "round": 1, "is_first_dig_of_battle": true, "terrain_kind": []}).is_empty()
and orch0._fx_emit("mine_hit", {"damage": 5}).is_empty()
and orch0._fx_emit("battle_won", {"round": 1}).is_empty(),
"零持有订阅=零命中(relic 关着零扰动——伤害管线既有行为逐位不变)")
# ⑦c 特性持有呈现段(2026-10-04 用户反馈补齐——C 区「特性」节;呈现换代面=06 在册不随批)
var panel = load("res://presentation/views/hero_info_panel_view.gd").new()
panel.set_feat_holdings([])
check(not panel._feat_section.visible, "特性段空态隐藏(视图空态纪律同特性段)")
panel.set_feat_holdings(["疼痛记忆 ×1", "双管镐 ×1"])
check(panel._feat_section.visible and panel._feat_list.get_child_count() == 2,
"特性段持有呈现(逐件行常驻可读——「局内奖励选了什么看不到」缺口承载)")
check(String(load("res://presentation/battle_narrator.gd").feat_card_text({"feat_id": "pain_memory", "display_name": "疼痛记忆", "desc": "触陷阱攒 1 层"})).contains("触陷阱攒 1 层"),
"特性卡文案表值化:desc 行字段直读(零硬编码映射)")
# 手工场景驱动:L1 全程 + L2 推进到商店房(_drive_level 复用 + 兑付确认/选路/大奖励确认手工编排)
@@ -4645,7 +4707,7 @@ func _meta_settle_and_workshop() -> void:
var mp_a = load("res://core/meta_progression.gd").new(_db, sv_a)
check(Attunement.slots(_db, sv_a) == 0 and int(mp_a.attune_camp_status()["refresh"]["charges"]) == 0,
"白板档同调位求值 = 0(0 起步裁决)且 Y=0(未购无刷新能力——P1 甲初始 0 同构)")
check(mp_a.toggle_attunement("venom_edge") == false,
check(mp_a.toggle_attunement("nest_counter") == false,
"满位禁选(Slots 0 时装配拒绝)")
sv_a.add_keystone(200)
check(mp_a.buy_attune_refresh() and sv_a.attune_refresh_level() == 1
@@ -4655,9 +4717,9 @@ func _meta_settle_and_workshop() -> void:
"stopped_at": 2, "keystone_total": 0, "master_seed": 21})
check(Attunement.slots(_db, sv_a) == 1 and sv_a.boss_kill_count() == 1,
"通关 1 关 → m1 达成 Slots=1 且 Boss 击杀总数=1(计数源收束入账)")
check(mp_a.toggle_attunement("venom_edge") and mp_a.toggle_attunement("flag_burst") == false,
check(mp_a.toggle_attunement("nest_counter") and mp_a.toggle_attunement("dawn_order") == false,
"装配持久写入 + 满位禁选(Slots 1 装第二件拒绝)")
check(mp_a.toggle_attunement("venom_edge") and mp_a.toggle_attunement("flag_burst"),
check(mp_a.toggle_attunement("nest_counter") and mp_a.toggle_attunement("dawn_order"),
"卸下恒可再装配(换装语义——装配序数组承载)")
var sv_a2 = _meta_new_save(_META_SMOKE_PATHS[7])
sv_a2.set_attunement_equipped(["luck_coin"])
@@ -4763,7 +4825,7 @@ func _smoke_tree() -> void:
# 迁移幂等(forge_levels→锻造脉原样映射;重复迁移零变化)
var mp2 = load("res://core/meta_progression.gd").new(_db, sv)
sv.set_forge_level("atk", 2)
sv.unlock_item("venom_edge")
sv.unlock_item("nest_counter")
var m1: Dictionary = mp2.migrate_tree()
check(int(m1.get("forge", 0)) == 1 and int(m1.get("unlock", 0)) == 1,
"三体系迁移:forge_atk←2 级点亮 + ks_workshop←解锁集点亮(差额补偿=原样映射零亏损)")