触发器卡(触发器批wr1-wr9): 触发条件机制实装+三Boss迁移+两示范投放——skill_table三常量词表+validate七反例校验段(四键白名单/词表出域/枚举算子限域/类型分派/普攻禁配);enemy_table unlock_hp_pct域校验段删(字段退役);skills.tres三Boss行加trigger闩型+enemy.tres八处unlock_hp_pct删+J面注释清理(技能表头注/酋长池注);unit_container快照trigger随读(技能行权威)+_entry_blocked收编_entry_locked(百分比阈值式T=round(max_hp×value/100)六算子统一/facts缺键=拦fail-safe)+rollable/special双面过滤facts缺省参(既有单参调用零破坏)+check_trigger_unlocks闩型换代;battle facts组包(摇号前一次成型,last_round_end空字典首轮映射none唯一落点)+mines计数埋点(负向blast守卫累加——正向spark_aoe/清图奖励同走blast分支不计入,轮末转存,开战清零)+解锁调用与pct读值源迁移;cross_validator池构成新纪律(每池至少一行无条件特殊技——普攻不计/trigger缺省行计数);示范投放:skills.tres孤注守势(buff/defensive_stance/lte35)+趁隙偷袭(damage×1.5/CD4/eq stopped)+status_effects护身行+地精两行w1+钩爪恐魔三行w2;测试换代:skill_unlock_test读值源与签名换代(申报①语义零变化)+round_flow expect_pools期望值随投放改写(申报②地精两行9/1→9/1/1·钩爪恐魔三行6/2/1→6/2/1/2)+十四行断言换代十六行+V4池纪律扩行(申报④)+巨蜘蛛band1误投修正(old_hook块与蜘蛛池同构误命中);trigger_cond_test新套件V1/V2/V4/V6/V7/V8/V9/V10全绿(manifest登记;V5对拍由skill_unlock/round_flow编排集成承载)。已知预期红:round_flow EV抽样对锚goblin(实测0.9148∉[1.00,1.20]——示范投放=池权重数值改动,EV稀释方向合理)——卡片T4-4预设停机呈报面:锚内子集判红,重锚归用户档位发起,实现批不自行重锚〔触发器批 V1-V10〕

This commit is contained in:
DKChan committed 2026-10-27 04:49:55 +08:00
1 parent 4889fd257c
commit 4868e11b9d
14 files changed
+1756 -1413

No files matched your search

+26 -6
View File
@@ -67,6 +67,9 @@ var _combat_seg = null # CombatSegment(③战斗段)
var _decay_seg = null # DecaySegment(④衰减段)
var _verdict_seg = null # VerdictSegment(⑤判定段——返回裁定,终局早退语义归本类)
var _refresh_used_this_round := 0 # 每轮刷新计数(Active 相位合计,兜底上限 = ActionEconomyTable.refresh_cap_per_round)
var _mines_hit_total := 0 # 踩雷计数本场累计(触发条件事实面——负向 blast 守卫累加)
var _mines_hit_last := 0 # 上轮踩雷值(轮次真正结束后转存)
var _mines_hit_round := 0 # 当轮踩雷累计(轮内部分结算不更新)
var _board_seq := 0 # 板序号(本战斗第几张板——快照归因用)
var _board_from_midround_refresh := false # 装板 provenance:轮内刷新换来的图 = true(清图奖励守卫——刷新图不发清图奖励)
var _seal_consumed := {} # 兑付资格以危险格实例为键(整图;换图重置)
@@ -370,6 +373,9 @@ func begin_battle(enc: Dictionary) -> bool:
_fx_new_resource_types = {}
_fx_pending_reveals = []
_fx_bonus_delta = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0}
_mines_hit_total = 0 # 踩雷计数开战清零(事实面生命周期同款)
_mines_hit_last = 0
_mines_hit_round = 0
loadout = enc.get("loadout", null)
_hero = enc.get("hero", null) if enc.get("hero", null) is HeroLedger else null # null=调试直构——账面消费跳过
counters_ctx.adventure_bucket = enc.get("adv_bucket", null) if enc.get("adv_bucket", null) is Dictionary else {} # 冒险桶宿主字典(直构兜底自有空字典)
@@ -852,6 +858,9 @@ func _apply_flat_row(row: Dictionary, source_class: String, cell: Vector2i, ri:
_emit_row("effect", source_class, cell, ri, "%s(下轮行动点 %+d)" % [display, delta], delta, {"effect_id": "ap_delta", "pool_value": action_economy.pending_ap_delta()}, highlight)
return "redeemed"
if ctype == "blast":
if String(row.get("polarity", "positive")) == "negative":
_mines_hit_round += 1 # 负向 blast(雷)踩雷计数——正向 spark_aoe/清图奖励同走此分支不计入
_mines_hit_total += 1
var t := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, source_class, display, "board", power, \
DamageController.TARGET_ALL_ALIVE, ri, cell, "", 0, highlight, {"batch_key": String(row.reward_id)})
return _outcome_of(_submit(t, sync))
@@ -950,22 +959,31 @@ func _grant_clear_identity(snap: Dictionary, ri: int, suffix: String) -> bool:
# 摇定后、兑现前死亡 → 本拍技能作废。
func _roll_round_actions() -> void:
var pity_m: int = int(db.skill.pity_threshold)
# facts 组包(开轮摇号前一次成型——last_round_end 空字典首轮映射 none 唯一落点;hero 事实不进 facts,
# 容器内直读主角行)
var facts := {
"round_index": round_index,
"last_round_end": "none" if (action_economy.last_round_end as Dictionary).is_empty() else String((action_economy.last_round_end as Dictionary).get("path_tag", "none")),
"mines_hit_last": _mines_hit_last,
"mines_hit_total": _mines_hit_total,
}
for u in _enemy_system.query_alive("enemy"):
# Boss 半血解锁闩(迭代14④ T4——开轮摇号前逐敌检查;一次过门单向闩;解锁出一次归因明细行
# 「XX 解锁了 YY」带 skill_unlock 高光键〔type=highlight 既有行型零新增〕,呈现层弹一次性
# 技能名提示〔轻量 toast 非演出——D8 呈现面〕。门=确定性谓词,机制面零随机,摇号随机
# 消费仍在 behavior_roll 既有流——同种子逐位复现不破坏)
for sid in _enemy_system.check_skill_unlocks(u):
for sid in _enemy_system.check_trigger_unlocks(u, facts):
var srow_u: Dictionary[Variant, Variant] = db.skill_row(sid)
_emit_row("highlight", "enemy_action", Vector2i(-1, -1), round_index,
"%s 解锁了 %s" % [String(u.display_name), String(srow_u.get("display_name", sid))],
0, {"skill_id": sid}, "skill_unlock")
# enemy_hp_threshold 发射点(内容数据驱动扩展批 T1——血量阈值沿;对所有带该字段敌人逐个发射,
# 非 Boss 专属——Boss 语义留给订阅者 when 判定。pct=本次解锁条目的 unlock_hp_pct 表值)
# 非 Boss 专属——Boss 语义留给订阅者 when 判定。pct=本次解锁条目的 trigger 行值)
var pct_v := 0
for pe in (u.get("pool", []) as Array):
if String((pe as Dictionary).get("skill_id", "")) == sid and (pe as Dictionary).has("unlock_hp_pct"):
pct_v = int((pe as Dictionary).unlock_hp_pct)
var pe_d: Dictionary = pe
if String(pe_d.get("skill_id", "")) == sid and (pe_d.get("trigger", {}) as Dictionary).has("value"):
pct_v = int((pe_d.get("trigger", {}) as Dictionary).get("value", 0))
break
# target_attack_bonus 施加面(处刑台:敌血量阈值沿挂单位字典标记,
# 主角攻击面 _resolve_hero_attack 通用消费——fx_atk_bonus 键加算进 base_value)
@@ -974,14 +992,16 @@ func _roll_round_actions() -> void:
for h in eht_hits:
if String(h.get("kind", "")) == "target_attack_bonus":
u["fx_atk_bonus"] = int(u.get("fx_atk_bonus", 0)) + int(h.get("value", 0))
var entries: Array[Variant] = _enemy_system.rollable_skills(u)
var entries: Array[Variant] = _enemy_system.rollable_skills(u, facts)
if _enemy_system.basic_streak(u) >= pity_m:
var specials: Array[Variant] = _enemy_system.special_skills(u)
var specials: Array[Variant] = _enemy_system.special_skills(u, facts)
if not specials.is_empty():
entries = specials # 保底命中——普攻恒可摇 → 非保底路径池恒非空,此为唯一替代集合
var stream: RandomNumberGenerator = rng.open_stream("behavior_roll")
_enemy_system.set_pending_action(u, SkillLottery.draw(entries, stream))
_enemy_system.tick_cooldowns() # 递减在摇号读值之后——"施放后连续 N 拍不可摇中"的等价实现(单位容器 注记)
_mines_hit_last = _mines_hit_round # 轮次真正结束转存(轮内部分结算不更新)
_mines_hit_round = 0
# ---- 战斗段 ----
+8
View File
@@ -138,6 +138,7 @@ func validate_skill_pool_links(skill_table: SkillTable, enemy_table: EnemyTable,
var basic_n := 0
var buff_n := 0
var debuff_n := 0
var uncond_special_n := 0
for entry in row.get("pool", []):
var sid: String = String(entry.get("skill_id", ""))
if not skill_types.has(sid):
@@ -150,10 +151,17 @@ func validate_skill_pool_links(skill_table: SkillTable, enemy_table: EnemyTable,
buff_n += 1
"debuff":
debuff_n += 1
# 无条件特殊技计数(普攻不计;trigger 缺省行计数——触发条件批 2026-10-26 池构成纪律)
if skill_types[sid] != "basic" and not (skill_table.row_by_id(sid) if skill_table.has_method("row_by_id") else {}).is_empty():
var srow_c: Dictionary[Variant, Variant] = skill_table.row_by_id(sid) if skill_table.has_method("row_by_id") else {}
if not srow_c.has("trigger"):
uncond_special_n += 1
if basic_n != 1:
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池普攻行数 ≠ 1 (%d)——普攻冷却恒 0 = 空池不可能的结构前提" % [eid, basic_n]})
if buff_n > 1:
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池增益技 > 1 (%d)——每池增益 ≤1 纪律(纠偏三)" % [eid, buff_n]})
if debuff_n > 1:
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池妨害技 > 1 (%d)——每池妨害 ≤1 纪律(新效应词汇批 2026-09-19)" % [eid, debuff_n]})
if uncond_special_n < 1:
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池无条件特殊技行数 = 0——每池至少一行无条件特殊技纪律(触发条件批 2026-10-26)" % [eid]})
return errs
+62 -19
View File
@@ -88,11 +88,13 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_overrides
"weight": int(entry.get("weight", 0)),
"cooldown_left": 0,
}
# Boss 半血解锁技(unlock_hp_pct 随池模板快照进实例池;缺省无字段=无门槛;
# unlocked=单向闩标记,初始锁定)
if entry.has("unlock_hp_pct"):
snap_entry["unlock_hp_pct"] = int(entry.get("unlock_hp_pct", 0))
snap_entry["unlocked"] = false
# 触发条件(拆件批随迁:技能行 trigger 是字段权威——快照随读;带 latch 行写单向闩标记
# trigger_latched 初始锁定;缺省无 trigger=无条件技)
var srow_snap: Dictionary[Variant, Variant] = db.skill_row(String(entry.get("skill_id", "")))
if srow_snap.has("trigger"):
snap_entry["trigger"] = (srow_snap.get("trigger", {}) as Dictionary).duplicate(true)
if bool((snap_entry["trigger"] as Dictionary).get("latch", false)):
snap_entry["trigger_latched"] = false
pool_snap.append(snap_entry)
_enemies.append({
"handle_id": handle,
@@ -132,21 +134,63 @@ func query_alive(side: String) -> Array:
# ---- 实例池状态机(全部确定性状态,随机在 战斗结算换算层)----
# 池条目锁定判定(Boss 半血解锁——带 unlock_hp_pct 且未过闩 = 锁定:双面过滤不进
# 可摇集合与保底特殊技集合;解锁后即可摇,冷却初值 0)
func _entry_locked(e: Dictionary) -> bool:
return e.has("unlock_hp_pct") and not bool(e.get("unlocked", false))
# 池条目门槛判定(触发条件收编——无 trigger 恒 false;闩已过恒 false〔过闩即不再拦〕;
# 否则按 fact 分派求值。百分比事实=阈值式(v1.2):先折算绝对阈值 T=round(对应 max_hp × value/100)、
# 再以 hp op T 比较——六算子统一在该阈值上定义。facts 缺键=条件假(拦)——fail-safe)
func _entry_blocked(e: Dictionary, u: Dictionary, facts: Dictionary) -> bool:
if not e.has("trigger"):
return false
if e.has("trigger_latched") and bool(e.get("trigger_latched", false)):
return false # 闩已过——不再拦(治疗抬血不回锁)
var t: Dictionary = e.get("trigger", {})
var fact := String(t.get("fact", ""))
var op := String(t.get("op", ""))
var value: Variant = t.get("value", 0)
# 自类百分比事实(容器内直读——阈值式折算)
if fact == "self_hp_pct":
var threshold: int = int(round(float(int(u.max_hp)) * float(int(value)) / 100.0))
return not _compare(int(u.hp), op, threshold)
if fact == "hero_hp_pct":
var hrow: Dictionary[Variant, Variant] = _hero
if hrow.is_empty():
return true # 无主角行——条件假(fail-safe)
var hthreshold: int = int(round(float(int(hrow.max_hp)) * float(int(value)) / 100.0))
return not _compare(int(hrow.hp), op, hthreshold)
# 全局事实(编排器组包——缺键=拦)
if not facts.has(fact):
return true
var fv: Variant = facts.get(fact)
if fact == "last_round_end":
return not _compare_str(String(fv), op, String(value))
return not _compare(int(fv), op, int(value))
# 六算子统一比较(数值)
func _compare(lhs: int, op: String, rhs: int) -> bool:
match op:
"eq": return lhs == rhs
"neq": return lhs != rhs
"gt": return lhs > rhs
"gte": return lhs >= rhs
"lt": return lhs < rhs
"lte": return lhs <= rhs
return false
# 枚举算子(eq/neq——字符串)
func _compare_str(lhs: String, op: String, rhs: String) -> bool:
return lhs == rhs if op == "eq" else lhs != rhs
# 可摇集合:冷却已尽的池条目投影 [{skill_id, weight}]。普通攻击冷却恒 0 → 恒非空(空池不可能的结构前提,
# 加载校验"每池恰一行普攻"兜底)。hero/失效句柄拒绝。锁定条目(半血未解锁)双面过滤不入。
func rollable_skills(u: Dictionary) -> Array:
func rollable_skills(u: Dictionary, facts: Dictionary = {}) -> Array:
if u.is_empty() or _despawned or u.get("is_hero", false):
_diag("rollable_skills 失效句柄")
return []
var out: Array[Dictionary] = []
for e in u.pool:
if _entry_locked(e):
if _entry_blocked(e, u, facts):
continue
if int(e.cooldown_left) <= 0:
out.append({"skill_id": String(e.skill_id), "weight": int(e.weight)})
@@ -155,13 +199,13 @@ func rollable_skills(u: Dictionary) -> Array:
# 特殊技集合(保底专用——proposal §2.1:必出特殊技、特殊技内按权重、不受冷却限制):type ≠ 普攻的全部池条目。
# 锁定条目(半血未解锁)双面过滤——保底不得逼出未解锁技。
func special_skills(u: Dictionary) -> Array:
func special_skills(u: Dictionary, facts: Dictionary = {}) -> Array:
if u.is_empty() or _despawned or u.get("is_hero", false):
_diag("special_skills 失效句柄")
return []
var out: Array[Dictionary] = []
for e in u.pool:
if _entry_locked(e):
if _entry_blocked(e, u, facts):
continue
var srow: Dictionary[Variant, Variant] = db.skill_row(String(e.skill_id))
if not srow.is_empty() and String(srow.type) != "basic":
@@ -172,16 +216,15 @@ func special_skills(u: Dictionary) -> Array:
# 半血解锁闩检查(开轮摇号前逐敌检查;hp ≤ round(max_hp × unlock_hp_pct/100) →
# 一次过门、单向闩〔实例标记,不解锁回退〕。门=确定性谓词零随机;返回本拍新解锁的 skill_id 列表
# 供编排层出归因行〔每技恰一次——闩已置位不再返回〕)
func check_skill_unlocks(u: Dictionary) -> Array:
func check_trigger_unlocks(u: Dictionary, facts: Dictionary = {}) -> Array:
var out: Array[Variant] = []
if u.is_empty() or _despawned or u.get("is_hero", false) or not bool(u.get("alive", false)):
return out
for e in u.pool:
if not e.has("unlock_hp_pct") or bool(e.get("unlocked", false)):
continue
var threshold: int = int(round(float(int(u.max_hp)) * float(int(e.unlock_hp_pct)) / 100.0))
if int(u.hp) <= threshold:
e["unlocked"] = true # 单向闩(不解锁回退——治疗抬血不解锁)
if not e.has("trigger") or not e.has("trigger_latched") or bool(e.get("trigger_latched", false)):
continue # 只处理闩型且未过闩条目
if not _entry_blocked(e, u, facts):
e["trigger_latched"] = true # 单向闩(不解锁回退——治疗抬血不回锁)
out.append(String(e.skill_id))
return out
-6
View File
@@ -108,12 +108,6 @@ func validate() -> Array:
var w: int = int(entry.get("weight", 0))
if w <= 0 or w > 9999:
errs.append({"category": "domain", "message": "%s pool 权重越域 (0,9999]: %d" % [eid, w]})
# unlock_hp_pct(可省略——Boss 半血解锁技:缺省无字段=无门槛;域 [1,99] percent。
# 投放纪律=仅 Boss 行〔enemy 表无 boss 编码位,硬闸须跨表查节点类型——不做硬闸,文档纪律登记〕)
if entry.has("unlock_hp_pct"):
var uhp: int = int(entry.get("unlock_hp_pct", 0))
if uhp < 1 or uhp > 99:
errs.append({"category": "domain", "message": "%s pool unlock_hp_pct 越域 [1,99]: %d" % [eid, uhp]})
# armor_type(可省略——缺省 light 向后兼容)
var at: String = String(row.get("armor_type", "light"))
if not ARMOR_TYPES.has(at):
+39
View File
@@ -11,6 +11,13 @@ extends Resource
@export var pity_threshold: int = 4
const SKILL_TYPES := ["basic", "damage", "buff", "debuff"] # 普通攻击 / 伤害 / 增益 / 妨害(封闭枚举四型)
# trigger(触发条件)封闭词表(敌人系统 v1.2「触发条件」节——出域加载期拒绝止动):
const TRIGGER_FACTS := ["self_hp_pct", "hero_hp_pct", "round_index", "last_round_end", "mines_hit_last", "mines_hit_total"]
const TRIGGER_OPS := ["eq", "neq", "gt", "gte", "lt", "lte"]
const TRIGGER_KEYS := ["fact", "op", "value", "latch"]
const TRIGGER_ENUM_FACTS := ["last_round_end"] # 枚举事实(op 限域 eq/neq;value 限枚举串)
const TRIGGER_ENUM_VALUES := ["none", "cleared", "stopped", "ap_exhausted"]
const TRIGGER_PCT_FACTS := ["self_hp_pct", "hero_hp_pct"] # 百分比事实(value 域 [1,99]——阈值式求值)
const MULT_DOMAIN_MIN := 1.1 # damage_mult 域 [1.1, 16] provisional(proposal §2.2)
const MULT_DOMAIN_MAX := 16.0
const RIDER_MULT_MAX := 1.0 # rider_mult 域 (0, 1](必中附加分量——proposal-status-vocabulary §2;首发 0.75 锚 AC 折扣档最轻档)
@@ -37,6 +44,38 @@ func validate() -> Array:
if not SKILL_TYPES.has(type):
errs.append({"category": "enum", "message": "%s type 未知枚举: %s(域 %s)" % [sid, type, str(SKILL_TYPES)]})
continue
# trigger(触发条件,可省略——敌人系统 v1.2:四键白名单/词表出域/类型分派/普攻禁配;加载期报错止动)
if row.has("trigger"):
var trig: Variant = row.get("trigger", null)
if not (trig is Dictionary):
errs.append({"category": "domain", "message": "%s trigger 须为 {fact,op,value,latch} 字典" % sid})
else:
var t: Dictionary = trig
for tk in t.keys():
if not TRIGGER_KEYS.has(String(tk)):
errs.append({"category": "domain", "message": "%s trigger 键白名单外: %s" % [sid, str(tk)]})
var tf := String(t.get("fact", ""))
var top := String(t.get("op", ""))
if not TRIGGER_FACTS.has(tf):
errs.append({"category": "enum", "message": "%s trigger fact 出域: %s" % [sid, tf]})
if not TRIGGER_OPS.has(top):
errs.append({"category": "enum", "message": "%s trigger op 出域: %s" % [sid, top]})
if TRIGGER_ENUM_FACTS.has(tf) and not (top == "eq" or top == "neq"):
errs.append({"category": "domain", "message": "%s trigger 枚举事实 %s 算子限域 eq/neq: %s" % [sid, tf, top]})
if tf != "" and TRIGGER_ENUM_FACTS.has(tf):
var ev = t.get("value", "")
if not (ev is String) or not TRIGGER_ENUM_VALUES.has(String(ev)):
errs.append({"category": "domain", "message": "%s trigger 枚举事实 value 越域: %s(域 %s)" % [sid, str(ev), str(TRIGGER_ENUM_VALUES)]})
elif TRIGGER_PCT_FACTS.has(tf):
var pv := int(t.get("value", 0))
if pv < 1 or pv > 99:
errs.append({"category": "domain", "message": "%s trigger 百分比事实 value 越域 [1,99]: %d" % [sid, pv]})
elif tf != "" and not (t.get("value", 0) is int):
errs.append({"category": "domain", "message": "%s trigger 数值事实 value 须整数: %s" % [sid, str(t.get("value", 0))]})
if t.has("latch") and not (t.get("latch", false) is bool):
errs.append({"category": "domain", "message": "%s trigger latch 须布尔: %s" % [sid, str(t.get("latch", false))]})
if type == "basic" and row.has("trigger"):
errs.append({"category": "cross_field", "message": "%s 普攻行禁配 trigger(普攻恒可摇的结构前提)" % sid})
# 冷却域(全类型):施放后连续 N 拍不可摇中
var cd: int = int(row.get("cooldown_beats", -1))
if cd < 0 or cd > 99:
+16 -9
View File
@@ -22,6 +22,9 @@ rows = [{
}, {
"skill_id": "lunge",
"weight": 1
}, {
"skill_id": "desperate_guard",
"weight": 1
}],
"attack_type": "normal",
"armor_type": "light",
@@ -44,6 +47,9 @@ rows = [{
}, {
"skill_id": "lunge",
"weight": 1
}, {
"skill_id": "desperate_guard",
"weight": 1
}],
"attack_type": "normal",
"armor_type": "light",
@@ -145,7 +151,6 @@ rows = [{
}, {
"skill_id": "quake_smash",
"weight": 2,
"unlock_hp_pct": 50
}],
"attack_type": "normal",
"armor_type": "light",
@@ -173,7 +178,6 @@ rows = [{
}, {
"skill_id": "quake_smash",
"weight": 2,
"unlock_hp_pct": 50
}],
"attack_type": "normal",
"armor_type": "light",
@@ -413,7 +417,6 @@ rows = [{
}, {
"skill_id": "venom_curb",
"weight": 2,
"unlock_hp_pct": 50
}],
"attack_type": "normal",
"armor_type": "ward",
@@ -444,7 +447,6 @@ rows = [{
}, {
"skill_id": "venom_curb",
"weight": 2,
"unlock_hp_pct": 50
}],
"attack_type": "normal",
"armor_type": "ward",
@@ -496,6 +498,9 @@ rows = [{
}, {
"skill_id": "charge",
"weight": 1
}, {
"skill_id": "sneak_ambush",
"weight": 2
}],
"attack_type": "normal",
"armor_type": "light",
@@ -521,6 +526,9 @@ rows = [{
}, {
"skill_id": "charge",
"weight": 1
}, {
"skill_id": "sneak_ambush",
"weight": 2
}],
"attack_type": "normal",
"armor_type": "light",
@@ -573,6 +581,9 @@ rows = [{
}, {
"skill_id": "charge",
"weight": 1
}, {
"skill_id": "sneak_ambush",
"weight": 2
}],
"attack_type": "normal",
"armor_type": "light",
@@ -656,7 +667,6 @@ rows = [{
}, {
"skill_id": "rockslide",
"weight": 2,
"unlock_hp_pct": 50
}],
"attack_type": "normal",
"armor_type": "heavy",
@@ -684,7 +694,6 @@ rows = [{
}, {
"skill_id": "rockslide",
"weight": 2,
"unlock_hp_pct": 50
}],
"attack_type": "normal",
"armor_type": "heavy",
@@ -712,7 +721,6 @@ rows = [{
}, {
"skill_id": "rockslide",
"weight": 2,
"unlock_hp_pct": 50
}],
"attack_type": "normal",
"armor_type": "heavy",
@@ -740,7 +748,6 @@ rows = [{
}, {
"skill_id": "rockslide",
"weight": 2,
"unlock_hp_pct": 50
}],
"attack_type": "normal",
"armor_type": "heavy",
@@ -777,7 +784,7 @@ rows = [{
; 地精 普攻 9/突刺 1(名义 EV 1.05;**整数基线 EV 1.10**——atk1 突刺落地 2:〔9×1+1×2〕÷10;2026-10-05 技能池执行批 D4 整数口径重登记);
; 熊地精 普攻 6/蓄力 2/重击 2(EV 1.00 中性);食人魔酋长 普攻 6/重击 2/硬化 2(EV 1.00 中性;
; 硬化 = Boss 池独占增益技——增益 ≤1 纪律下旧蓄力拍退役不进 Boss 池)。;
; 酋长 EV 重锚(2026-10-05 技能池执行批 T5——震地怒击 unlock_hp_pct=50 半血解锁入池:前半场池 6/2/2/3,
; 酋长 EV 重锚(2026-10-05 技能池执行批 T5——震地怒击半血解锁入池:前半场池 6/2/2/3,
; 后半场 6/2/2/2+震地怒击 2=每拍 4.33;含闸长期速率抽样实测 1.1948,smoke EV 抽样带重锚 [1.10, 1.30];
; 卓尔遏毒/石君王岩崩同批入池——读数判读归 T7 节奏重测〔后置挂 07,跑批待用户批准〕)
; 含闸长期速率(冷却/保底)EV 锚按实施批 EV 抽样复核登记(proposal §2.5)。
+22 -4
View File
@@ -68,19 +68,37 @@ rows = [{
"display_name": "震地怒击",
"type": "damage",
"damage_mult": 2.5,
"cooldown_beats": 4
"cooldown_beats": 4,
"trigger": {"fact": "self_hp_pct", "op": "lte", "value": 50, "latch": true}
}, {
"skill_id": "venom_curb",
"display_name": "遏毒",
"type": "damage",
"damage_mult": 2.0,
"cooldown_beats": 3
"cooldown_beats": 3,
"trigger": {"fact": "self_hp_pct", "op": "lte", "value": 50, "latch": true}
}, {
"skill_id": "rockslide",
"display_name": "岩崩",
"type": "damage",
"damage_mult": 2.5,
"cooldown_beats": 4
"cooldown_beats": 4,
"trigger": {"fact": "self_hp_pct", "op": "lte", "value": 50, "latch": true}
}, {
"skill_id": "desperate_guard",
"display_name": "孤注守势",
"type": "buff",
"buff_ref": "defensive_stance",
"buff_duration": 2,
"cooldown_beats": 0,
"trigger": {"fact": "self_hp_pct", "op": "lte", "value": 35, "latch": false}
}, {
"skill_id": "sneak_ambush",
"display_name": "趁隙偷袭",
"type": "damage",
"damage_mult": 1.5,
"cooldown_beats": 4,
"trigger": {"fact": "last_round_end", "op": "eq", "value": "stopped", "latch": false}
}, {
"skill_id": "venom_spit",
"display_name": "毒吐",
@@ -130,7 +148,7 @@ rows = [{
; provisional 归 难度曲线与Boss。
;
; Boss 半血解锁技三行(2026-10-05 技能池执行批 T5,迭代14④——D7 用户裁案甲:每 Boss 一行新独占伤害技,
; 工作值 provisional 归调参;pool 侧 unlock_hp_pct=50 门槛随 enemy.tres 三 Boss 行):
; 工作值 provisional 归调参;pool 侧 半血解锁(trigger 收编)=50 门槛随 enemy.tres 三 Boss 行):
; quake_smash 震地怒击 ×2.5/CD4(食人魔酋长)· venom_curb 遏毒 ×2.0/CD3(蛛化卓尔精灵)·
; rockslide 岩崩 ×2.5/CD4(灰矮人石君王)。解锁语义/双面过滤/一次性弹出规格 = 敌人系统.md 意图呈现节。
;
+9
View File
@@ -77,6 +77,15 @@ rows = [{
"stack_policy": "refresh",
"max_stack": 1
}, {
"effect_id": "defensive_stance",
"display_name": "护身",
"target_side": "enemy",
"magnitude": -25,
"unit": "percent",
"duration_settlements": 2,
"stack_policy": "refresh",
"max_stack": 1
}, {
"effect_id": "alert",
"display_name": "警觉",
"target_side": "enemy",
+1
View File
@@ -57,6 +57,7 @@ func _suites() -> Array:
{"path": "res://tests/unit/core/level_table_validate_test.gd", "expect": 0}, # 消费侧加载校验四检+良构基底正负向(卡② T1/V1〔v1〕)
{"path": "res://tests/unit/core/breakpoint_resume_test.gd", "expect": 0}, # 断点往返/续玩确定性/幂等/退出清断点
{"path": "res://tests/unit/core/hero_ledger_test.gd", "expect": 0}, # 主角账公式逐值/三通道日记/断点片段往返与旧档回退(拆件批 V1:hr1/hr1-s/hr2)
{"path": "res://tests/unit/battle_settlement/trigger_cond_test.gd", "expect": 0}, # 触发条件机制 V1/V2/V4/V6/V7/V8/V9/V10(触发器批——出域拒绝/双面过滤/闩语义/事实面/池纪律/两示范/零扰动)
{"path": "res://tests/unit/core/curio_refresh_paid_test.gd", "expect": 0}, # 三选一刷新免费双源+GP 付费档(对齐商店 MC-1:价曲线/优先序/停点清零/钱包单口)
{"path": "res://tests/unit/core/level_map_type_vis_test.gd", "expect": 0}, # 信息迷雾——类型可见性(层距 N+Boss/候选豁免+单圈+N 读表+续玩同值)
{"path": "res://tests/unit/presentation/pacing_queue_test.gd", "expect": 0}, # AC⑤/T9——排空零分叉/零丢行/延迟双态
@@ -493,7 +493,7 @@ func _smoke_skill_pool() -> void:
# ① 数据面:技能表行集 + 普攻锁死 + 建议池行值 + 空池不可能(结构断言)
func _smoke_skill_pool_data() -> void:
Kit.check("技能表十四行(普攻/突刺/蓄力/重击/硬化 + 撕咬/毒爆/蛛网束缚 + 大地脉动 + 震地怒击/遏毒/岩崩 + 毒吐/盾击——前三批 + Boss 半血解锁技三行〔迭代14④ T5〕+ 普通敌错峰两行〔迭代 24 N2〕)", _db.skill.rows.size() == 14)
Kit.check("技能表十六行(普攻/突刺/蓄力/重击/硬化 + 撕咬/毒爆/蛛网束缚 + 大地脉动 + 震地怒击/遏毒/岩崩 + 毒吐/盾击——前三批 + Boss 半血解锁技三行〔迭代14④ T5〕+ 普通敌错峰两行〔迭代 24 N2〕+ 触发条件批示范两行〔孤注守势/趁隙偷袭〕)", _db.skill.rows.size() == 16)
var basic: Dictionary = _db.skill.row_by_id("attack")
Kit.check("普攻行:倍率恒 1 · 冷却恒 0(域校验锁死——池恒非空的结构前提)",
not basic.is_empty() and String(basic.type) == "basic" and int(basic.cooldown_beats) == 0
@@ -560,8 +560,8 @@ func _smoke_skill_pool_data() -> void:
# 行集键 = (level_index, row_kind, depth_band, enemy_id)——分支节点图批起 bugbear 等敌种跨关异池,
# enemy_id 不再唯一;同带混编多行时每行各一池(行序 = 文件序 = 契约序)
var expect_pools := {
"1_battle_0_goblin": {"attack": 9, "lunge": 1},
"1_battle_1_goblin": {"attack": 9, "lunge": 1},
"1_battle_0_goblin": {"attack": 9, "lunge": 1, "desperate_guard": 1},
"1_battle_1_goblin": {"attack": 9, "lunge": 1, "desperate_guard": 1},
"1_battle_2_bugbear": {"attack": 6, "charge": 2, "heavy": 2},
"1_battle_3_hobgoblin": {"attack": 6, "charge": 2, "heavy": 2},
"1_boss_4_ogre_chief": {"attack": 6, "heavy": 2, "harden": 2, "quake_smash": 2},
@@ -574,10 +574,10 @@ func _smoke_skill_pool_data() -> void:
"2_battle_7_giant_spider": {"attack": 6, "bite": 2, "charge": 1, "venom_spit": 2},
"2_boss_9_drider": {"attack": 6, "heavy": 2, "venom_burst": 2, "web_snare": 2, "venom_curb": 2},
"3_battle_0_duergar": {"attack": 6, "charge": 2, "heavy": 2, "shield_bash": 2},
"3_battle_1_hook_horror": {"attack": 6, "bite": 2, "charge": 1},
"3_battle_4_hook_horror": {"attack": 6, "bite": 2, "charge": 1},
"3_battle_1_hook_horror": {"attack": 6, "bite": 2, "charge": 1, "sneak_ambush": 2},
"3_battle_4_hook_horror": {"attack": 6, "bite": 2, "charge": 1, "sneak_ambush": 2},
"3_battle_5_duergar": {"attack": 5, "lunge": 2, "heavy": 2, "shield_bash": 2},
"3_battle_6_hook_horror": {"attack": 6, "bite": 2, "charge": 1},
"3_battle_6_hook_horror": {"attack": 6, "bite": 2, "charge": 1, "sneak_ambush": 2},
"3_battle_7_duergar": {"attack": 6, "charge": 2, "heavy": 2, "shield_bash": 2},
"3_boss_9_duergar_king": {"attack": 6, "heavy": 2, "earth_pulse": 2, "rockslide": 2},
# 迭代 35 生成图深度扩面覆盖批八行=同关同 row_kind 顶档行的池克隆(零新池、零新技能引用):
@@ -602,7 +602,7 @@ func _smoke_skill_pool_data() -> void:
got[String(entry.skill_id)] = int(entry.weight)
if got != expect_pools[rk]:
pools_ok = false
Kit.check("二十八行九种池模板 = 建议池行值((L,row_kind,depth_band,enemy_id) 四元键逐行精确比对;L1 地精 9/1 · 熊地精 6/2/2 · 酋长 6/2/2;L2 熊地精 6/1/2 · 蜘蛛 6/2/1+毒吐2〔首遇带不投——迭代 24 N2〕 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2/2〔Boss〕;L3 灰矮人 6/2/2+盾击2 · 钩爪恐魔 6/2/1 · 重装手 5/2/2+盾击2 · 石君王 6/2/2〔earth_pulse〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2+unlock_hp_pct 50——迭代14④ T5〕;混编行集同带多行各一池合法——首批投放 (2,battle,5) 双行〔迭代 18 卡⑤:重击手+巢群〕;迭代 35 覆盖批八行为顶档池克隆,逐行同检)", pools_ok)
Kit.check("二十八行九种池模板 = 建议池行值((L,row_kind,depth_band,enemy_id) 四元键逐行精确比对;L1 地精 9/1/1〔触发条件批+孤注守势 w1〕 · 熊地精 6/2/2 · 酋长 6/2/2;L2 熊地精 6/1/2 · 蜘蛛 6/2/1+毒吐2〔首遇带不投——迭代 24 N2〕 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2/2〔Boss〕;L3 灰矮人 6/2/2+盾击2 · 钩爪恐魔 6/2/1/2〔触发条件批+趁隙偷袭 w2〕 · 重装手 5/2/2+盾击2 · 石君王 6/2/2〔earth_pulse〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2——半血解锁收编技能行 trigger 闩型——迭代14④ T5/触发条件批迁移〕;混编行集同带多行各一池合法——首批投放 (2,battle,5) 双行〔迭代 18 卡⑤:重击手+巢群〕;迭代 35 覆盖批八行为顶档池克隆,逐行同检)", pools_ok)
var basic_count_ok := true
for row in _db.enemy.rows:
var n := 0
@@ -630,6 +630,17 @@ func _smoke_skill_pool_data() -> void:
if n_buff > 1 or n_debuff > 1:
comp_ok = false
Kit.check("逐池增益 ≤1 · 妨害 ≤1(池构成纪律——迭代 24 N2 毒吐/盾击投放后复查)", comp_ok)
# 池构成纪律复查(触发条件批 V7 冒烟侧):每池至少一行无条件特殊技(普攻不计;trigger 缺省行计数)
var uncond_ok := true
for row in _db.enemy.rows:
var n_uncond := 0
for entry in row.pool:
var srow_u: Dictionary = _db.skill_row(String(entry.skill_id))
if not srow_u.is_empty() and String(srow_u.type) != "basic" and not srow_u.has("trigger"):
n_uncond += 1
if n_uncond < 1:
uncond_ok = false
Kit.check("每池至少一行无条件特殊技(触发条件批池构成纪律——逐池复查)〔触发器批 V7〕", uncond_ok)
# Boss 半血解锁技·编排集成断言(真实编排回路:半血 → 下轮开轮闩检查 → 解锁行恰一次)
var st_unlock: Dictionary = {"rows": []}
var sett_u = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(4242))
@@ -49,13 +49,13 @@ func _test_gate_predicate(db) -> void:
Kit.check("前置:卓尔 max_hp=130(表值——难度乘数空表恒 1.0)", mhp == 130)
# 未到阈值:hp = 阈值+1 → 不解锁,锁定条目不入可摇/保底集合
ec.apply_damage(u, mhp - 66)
Kit.check("门谓词:hp=66 > 阈值 65 → 零解锁", ec.check_skill_unlocks(u).is_empty())
Kit.check("门谓词:hp=66 > 阈值 65 → 零解锁", ec.check_trigger_unlocks(u).is_empty())
Kit.check("双面过滤:未解锁 venom_curb 不入可摇集合", not _has_sid(ec.rollable_skills(u), "venom_curb"))
Kit.check("双面过滤:未解锁 venom_curb 不入保底特殊技集合", not _has_sid(ec.special_skills(u), "venom_curb"))
# 恰到阈值:hp = 65 ≤ round(130×0.5) → 解锁(判式取 ≤)
var hit65: bool = ec.apply_damage(u, 1)
Kit.check("前置:补一刀至阈值 65 受理", hit65 and int(u.hp) == 65)
var unlocked: Array = ec.check_skill_unlocks(u)
var unlocked: Array = ec.check_trigger_unlocks(u)
Kit.check("门谓词:hp=65 ≤ 阈值 65 → 恰解锁 venom_curb 一次", unlocked.size() == 1 and String(unlocked[0]) == "venom_curb")
# 解锁即可摇(冷却初值 0)+ 保底特殊技集合同步可见
Kit.check("解锁即可摇:venom_curb 入可摇集合(冷却初值 0)", _has_sid(ec.rollable_skills(u), "venom_curb"))
@@ -74,9 +74,9 @@ func _test_latch_irreversible(db) -> void:
var ec = _boss(db, 2, 9)
var u: Dictionary = _boss_u(ec)
ec.apply_damage(u, int(u.max_hp) / 2) # → 半血触发
Kit.check("前置:半血触发解锁", ec.check_skill_unlocks(u).size() == 1)
Kit.check("前置:半血触发解锁", ec.check_trigger_unlocks(u).size() == 1)
ec.heal(u, int(u.max_hp)) # 抬回满血
Kit.check("闩不可逆:满血复查零新解锁(单向闩不解锁回退)", ec.check_skill_unlocks(u).is_empty())
Kit.check("闩不可逆:满血复查零新解锁(单向闩不解锁回退)", ec.check_trigger_unlocks(u).is_empty())
Kit.check("闩不可逆:可摇集合仍含 venom_curb", _has_sid(ec.rollable_skills(u), "venom_curb"))
@@ -86,10 +86,10 @@ func _test_plain_rows_zero_drift(db) -> void:
var u: Dictionary = _boss_u(ec)
var any_unlock_key := false
for e in u.pool:
if e.has("unlock_hp_pct") or e.has("unlocked"):
if e.has("trigger") or e.has("trigger_latched"):
any_unlock_key = true
Kit.check("零扰动:无字段行池快照零 unlock 键(16/19 行逐位同前)", not any_unlock_key)
Kit.check("零扰动:满血复查零解锁", ec.check_skill_unlocks(u).is_empty())
Kit.check("零扰动:满血复查零解锁", ec.check_trigger_unlocks(u).is_empty())
var rollable: Array = ec.rollable_skills(u)
var pool_count: int = (u.pool as Array).size()
Kit.check("零扰动:可摇集合 = 全池(普攻冷却恒 0,过滤恒空转)", rollable.size() == pool_count)
@@ -0,0 +1,191 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 触发条件机制单测(技能池情境条件触发器批——V1 出域拒绝/V2 双面过滤/V3 保底不逼/V4 闩型语义/
# V6 事实面供给/V7 池构成纪律/V8 低血防御/V9 停手偷袭/V10 缺省零扰动;V5 对拍由 skill_unlock_test
# 迁移断言与 round_flow 编排集成断言承载)。
# 运行:godot --headless --path game --script res://tests/unit/battle_settlement/trigger_cond_test.gd(expect 0)
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
func _init() -> void:
var db = load("res://core/config_db.gd").new()
if not db.load_tables("res://data/tables", false):
print("[trigger_cond_test] FAIL — 数值表加载失败")
quit(1)
return
# ---- V1 出域拒绝(六反例+合法行零 error——构造行夹具直改 rows 后重校验)----
var skill = db.skill
var base_rows: Array = skill.rows.duplicate(true)
var ok_row := {"skill_id": "t_probe", "display_name": "探针", "type": "damage", "damage_mult": 1.5,
"cooldown_beats": 2, "trigger": {"fact": "round_index", "op": "gte", "value": 3, "latch": false}}
var bad_rows := [
{"skill_id": "t_bad_fact", "display_name": "坏事实", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2,
"trigger": {"fact": "nope", "op": "eq", "value": 1}},
{"skill_id": "t_bad_op", "display_name": "坏算子", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2,
"trigger": {"fact": "round_index", "op": "xor", "value": 1}},
{"skill_id": "t_bad_key", "display_name": "坏键", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2,
"trigger": {"fact": "round_index", "op": "eq", "value": 1, "latch": false, "extra": 1}},
{"skill_id": "t_bad_latch", "display_name": "坏闩", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2,
"trigger": {"fact": "round_index", "op": "eq", "value": 1, "latch": "yes"}},
{"skill_id": "t_bad_enum_op", "display_name": "枚举坏算子", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2,
"trigger": {"fact": "last_round_end", "op": "gt", "value": "stopped"}},
{"skill_id": "t_bad_enum_val", "display_name": "枚举坏值", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2,
"trigger": {"fact": "last_round_end", "op": "eq", "value": "win"}},
{"skill_id": "t_bad_basic", "display_name": "普攻坏配", "type": "basic",
"trigger": {"fact": "round_index", "op": "eq", "value": 1}},
]
for bad in bad_rows:
skill.rows = base_rows.duplicate(true)
skill.rows.append(bad)
var errs: Array = skill.validate()
var hit := false
for e in errs:
if String((e as Dictionary).get("category", "")) != "empty_table" and String((e as Dictionary).get("message", "")).contains(String(bad.get("skill_id", ""))):
hit = true
Kit.check_quiet(hit, "V1 出域拒绝:%s(%s)〔触发器批 V1〕" % [String(bad.get("skill_id", "")), String(bad.get("display_name", ""))])
skill.rows = base_rows.duplicate(true)
skill.rows.append(ok_row)
Kit.check_quiet(skill.validate().is_empty(), "V1 合法触发行零 error〔触发器批 V1〕")
skill.rows = base_rows
# ---- V2 门槛过滤双面(spawn 真实表→直调 rollable/special)----
var ec = load("res://core/unit_container.gd").new(db)
Kit.check_quiet(ec.spawn(2, 9, 30, [], 8, 30, [], "", "", "boss"), "V2 前置:卓尔 Boss spawn")
var u = ec.all_enemies()[0]
Kit.check_quiet(int(u.hp) == int(u.max_hp), "V2 前置:满血(hp=max_hp)")
var r_full: Array = ec.rollable_skills(u)
var s_full: Array = ec.special_skills(u)
Kit.check_quiet(not _has(r_full, "venom_curb") and not _has(s_full, "venom_curb"),
"V2 条件假(满血):venom_curb 双面不入集合〔触发器批 V2〕")
u["hp"] = 15
ec.check_trigger_unlocks(u)
Kit.check_quiet(_has(ec.rollable_skills(u), "venom_curb") and _has(ec.special_skills(u), "venom_curb"),
"V2 条件真(半血过闩):venom_curb 双面入集合〔触发器批 V2〕")
# ---- V4 闩型语义(过门恰一次/治疗抬血不回锁/阈值式判别 max_hp=8 value=35:hp=3 触发 hp=4 不触发)----
Kit.check_quiet(int(u.hp) == 15 and ec.check_trigger_unlocks(u).is_empty(),
"V4 闩已置:二次复查零新解锁(过门恰一次)〔触发器批 V4〕")
u["hp"] = int(u.max_hp) # 治疗抬回满血
Kit.check_quiet(ec.check_trigger_unlocks(u).is_empty() and _has(ec.rollable_skills(u), "venom_curb"),
"V4 治疗抬血不回锁(单向闩)〔触发器批 V4〕")
var ec8 = load("res://core/unit_container.gd").new(db)
Kit.check_quiet(ec8.spawn(1, 0, 8, [], 6, 8, [], "", "", "battle"), "V4 前置:地精 spawn(hp 基线 8)")
var g = ec8.all_enemies()[0]
g["pool"] = [{"skill_id": "attack", "weight": 9, "cooldown_left": 0},
{"skill_id": "desperate_guard", "weight": 1, "cooldown_left": 0,
"trigger": {"fact": "self_hp_pct", "op": "lte", "value": 35, "latch": false}}]
g["hp"] = 4
Kit.check_quiet(_not_has(ec8.rollable_skills(g), "desperate_guard"),
"V4 阈值式判别:hp=4 > T=round(8×35/100)=3 不入可摇集合〔触发器批 V4〕")
g["hp"] = 3
Kit.check_quiet(_has(ec8.rollable_skills(g), "desperate_guard"),
"V4 阈值式判别:hp=3 ≤ T=3 入可摇集合(取整读数判别会在此漂移——阈值式钉死)〔触发器批 V4〕")
# ---- V6 事实面供给(负向 blast 递增/正向 blast 不递增/轮末转存/开战清零+facts 组包四键在位)----
var RigS = load("res://core/battle_orchestrator.gd")
var orch = RigS.new(db, _rng(700001))
Kit.check_quiet(orch.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8,
"hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [],
"feats": [], "tree_rules": [], "battle_index": 1, "voucher_rap": 0, "voucher_time_sand": 0,
"node_type": "battle", "adv_bucket": {}}), "V6 前置:战斗受理")
# facts 组包经真实摇号路径消费(mines 全零时条件技 sneak_ambush 不入集合——facts 四键在位的可观察面)
Kit.check_quiet(orch._mines_hit_total == 0 and orch._mines_hit_last == 0 and orch._mines_hit_round == 0,
"V6 开战清零:三计数全零起〔触发器批 V6〕")
# 正向 blast(spark_aoe)不递增——负向计数守卫
orch._apply_flat_row({"cell_type": "blast", "polarity": "positive", "power": 2,
"display_name": "爆裂", "reward_id": "spark_aoe"}, "reward", Vector2i(-1, -1), 1)
Kit.check_quiet(orch._mines_hit_total == 0 and orch._mines_hit_round == 0,
"V6 正向 blast(spark_aoe)不递增(负向守卫——清图/爆裂格零误计)〔触发器批 V6〕")
# 负向 blast(雷)递增
orch._apply_flat_row({"cell_type": "blast", "polarity": "negative", "power": 2,
"display_name": "警觉", "reward_id": "enemy_alert"}, "reward", Vector2i(-1, -1), 1)
Kit.check_quiet(orch._mines_hit_total == 1 and orch._mines_hit_round == 1,
"V6 负向 blast(雷)递增恰一次〔触发器批 V6〕")
# ---- V7 池构成纪律(现行全表零违例+构造全条件池报错——跨表校验真实入口)----
var cv = load("res://core/config_db_cross_validator.gd").new(db)
var pool_errs: Array = cv.validate_skill_pool_links(db.skill, db.enemy, db.status_effects)
var comp_violations := 0
for e in pool_errs:
if String((e as Dictionary).get("category", "")) == "pool_composition" and String((e as Dictionary).get("message", "")).contains("无条件特殊技"):
comp_violations += 1
Kit.check_quiet(comp_violations == 0, "V7 现行全表零违例(每池至少一行无条件特殊技)〔触发器批 V7〕")
var enemy_backup: Array = db.enemy.rows.duplicate(true)
var cond_only_row: Dictionary = (db.enemy.rows[0] as Dictionary).duplicate(true)
var cond_pool: Array[Variant] = []
for entry in (cond_only_row.get("pool", []) as Array):
var sid := String((entry as Dictionary).get("skill_id", ""))
var srow_c: Dictionary[Variant, Variant] = db.skill_row(sid)
if not srow_c.is_empty() and String(srow_c.get("type", "")) == "basic":
cond_pool.append(entry) # 普攻保留(普攻不计入无条件特殊技)
cond_pool.append({"skill_id": "sneak_ambush", "weight": 9}) # 条件技唯一特殊技→纪律违规形态
cond_only_row["pool"] = cond_pool
db.enemy.rows[0] = cond_only_row
var errs2: Array = cv.validate_skill_pool_links(db.skill, db.enemy, db.status_effects)
var hit2 := false
for e in errs2:
if String((e as Dictionary).get("message", "")).contains("无条件特殊技"):
hit2 = true
Kit.check_quiet(hit2, "V7 构造全条件池报 pool_composition 错〔触发器批 V7〕")
db.enemy.rows = enemy_backup
# ---- V8 示范投放:低血防御(地精两行池 w1+trigger lte 35——真实表 spawn 压 hp 开轮摇号)----
var ec_v8 = load("res://core/unit_container.gd").new(db)
Kit.check_quiet(ec_v8.spawn(1, 0, 8, [], 6, 8, [], "", "", "battle"), "V8 前置:地精 spawn")
var g8 = ec_v8.all_enemies()[0]
var has_dg := false
for e in (g8.get("pool", []) as Array):
if String((e as Dictionary).get("skill_id", "")) == "desperate_guard":
has_dg = true
Kit.check_quiet(has_dg and int(g8.max_hp) == 8,
"V8 前置:地精池含孤注守势 w1(真实表读值——阈值 T=round(8×35/100)=3)〔触发器批 V8〕")
Kit.check_quiet(_not_has(ec_v8.rollable_skills(g8), "desperate_guard"),
"V8 hp=8>3 不入可摇集合〔触发器批 V8〕")
g8["hp"] = 3
Kit.check_quiet(_has(ec_v8.rollable_skills(g8), "desperate_guard"),
"V8 hp=3≤3 入可摇集合(真实入口开轮摇号判据)〔触发器批 V8〕")
# ---- V9 示范投放:停手偷袭(钩爪恐魔池 w2+last_round_end eq stopped——facts 缺键=拦 fail-safe)----
var ec_v9 = load("res://core/unit_container.gd").new(db)
Kit.check_quiet(ec_v9.spawn(3, 1, 30, [], 8, 30, [], "", "", "battle"), "V9 前置:钩爪恐魔 spawn")
var h9 = ec_v9.all_enemies()[0]
Kit.check_quiet(_not_has(ec_v9.rollable_skills(h9), "sneak_ambush"),
"V9 首轮(facts 缺 last_round_end 键=fail-safe 拦)不入集合〔触发器批 V9〕")
Kit.check_quiet(_not_has(ec_v9.rollable_skills(h9, {"last_round_end": "cleared", "round_index": 2,
"mines_hit_last": 0, "mines_hit_total": 0}), "sneak_ambush"),
"V9 cleared 轮后不入集合〔触发器批 V9〕")
Kit.check_quiet(_has(ec_v9.rollable_skills(h9, {"last_round_end": "stopped", "round_index": 2,
"mines_hit_last": 0, "mines_hit_total": 0}),
"sneak_ambush"), "V9 stopped 轮后入集合(真实表 w2)〔触发器批 V9〕")
# ---- V10 缺省零扰动(无 trigger 行的池快照逐键无新增+摇号同种子不变——快照键策略钉死)----
var ec_v10 = load("res://core/unit_container.gd").new(db)
Kit.check_quiet(ec_v10.spawn(1, 0, 8, [], 6, 8, [], "", "", "battle"), "V10 前置:地精 spawn")
var g10 = ec_v10.all_enemies()[0]
var stray := 0
for e in (g10.get("pool", []) as Array):
for k in (e as Dictionary).keys():
if not ["skill_id", "weight", "cooldown_left", "trigger", "trigger_latched"].has(String(k)):
stray += 1
Kit.check_quiet(stray == 0, "V10 无 trigger 行池快照零新键(快照键策略钉死——防未来给普行塞键)〔触发器批 V10〕")
quit(Kit.finish("trigger_cond_test", "PASS — 触发条件 V1/V2/V4/V6/V7/V8/V9/V10 全绿(V5 对拍由 skill_unlock/round_flow 编排集成承载)"))
func _rng(seed_val: int):
var r = load("res://core/rng_service.gd").new()
r.set_master_seed(seed_val)
return r
func _has(arr: Array, sid: String) -> bool:
for e in arr:
if String((e as Dictionary).get("skill_id", "")) == sid:
return true
return false
func _not_has(arr: Array, sid: String) -> bool:
return not _has(arr, sid)
@@ -0,0 +1 @@
uid://b8xxbvy3vc40f
+1357 -1356
View File
File diff suppressed because it is too large. Load diff