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:
1 parent
0640b0c06d
commit
54ab330beb
6 files changed
+1151
-830
No files matched your search
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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=与另一计数器比较〔深挖钻头配额〕、
|
||||
|
||||
@@ -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:
|
||||
|
||||
+847
-566
File diff suppressed because it is too large.
Load diff
+272
-210
@@ -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←解锁集点亮(差额补偿=原样映射零亏损)")
|
||||
|
||||
Reference in new issue
Block a user