feat(slice): 技能池新效应词汇批——妨害技+必中附加分量(L2 Boss 卓尔实装;多段命中退役;2026-09-19 用户逐项拍板,设计权威 docs/proposal-status-vocabulary.md v1.0 新建)——①三词收敛两词:妨害技=第四技能类型 debuff(0 伤害施放行,字段复用 buff_ref/buff_duration,落点由状态行 target_side 路由——容器零改动;池纪律+每池妨害≤1;施放类 side 守卫 buff→enemy/debuff→hero);固定值伤害改形必中附加分量 rider_mult(域 (0,1],分量值 max(floor, floor(atk×rider)) 随基线缩放——固定值不通用/跨关稀释析出此改形;价位锚 0.75=AC 折扣档最轻档"不掉档的稳定伤害",可掷分量期望 AC10≈0.80/AC12≈0.76 对表,×0.5 永久第三档案经用户质询否决);多段命中退役(无防御磨损体系无对照机制,P1 字段位保留);②实装面=仅 L2 Boss 蛛化卓尔精灵(用户拍板,其余敌种不动):毒爆+必中毒分量(atk3→毒2,atk 区间 2-4→1-3 随开战掷定缩放);新行蛛网束缚 web_snare(debuff→off_guard 失衡〔首发轴用户拍板〕,duration 2=1 有效拍,CD4,池 6/2/2→6/2/2/2 全 provisional);③结算面:三乘法链尾部+rider 后置加法(max(floor,主分量链)+rider,payload rider 键+行文本「+附加N(必中)」);意图徽标三档扩四档(+妨害系 ☠ 紫);④加载校验扩两族(pool_composition 妨害>1 / cast_side 施放类 side 守卫——validate_skill_pool_links 增 status_table 显式入参修装载序 Nil 崩);⑤验证:冒烟 306→317 项 0 失败(新探针 _smoke_debuff_rider 四断言:毒爆行逐拍精确复算/rider 键封闭/蛛网束缚落主角端到端/种子可达+校验拒绝组六断言)+四探针 PASS;顺手修并行 board_type 批漏网(smoke variant_pool Array→String 字面量,原断言被 SCRIPT ERROR 静默吞);800 局模拟试玩(两关×20 种子×双画像 4200-4219):L2 基线通关 90%→80%、剩余死因 100% 集中 Boss(设计方向生效)、台阶倒挂收窄 65vs90→65vs80,L1 65%/85% 与批前一致零行为变更回归;analyze_playtest.py 反事实重算+rider 识别(敌方招式成分,无奖励世界只剥伏击保留 rider);⑥文档:敌人系统 v0.10(枚举四类/词汇表 rider 行/池纪律/意图三族/P1 表核销)+战斗结算 v0.17(三乘法链尾 rider/公式行/必中家族/验收两条)+增益状态 v0.10(off_guard 第二来源=敌方技能施放)+registry skill 条目改写+off_guard 行+修订块+07 头注与速览行(重测变量+1)+README 批节+06 条目解决移除;数值全 provisional 归 难度曲线与Boss;本提交一并入库并行会话已获用户批准的完整批次:06 残留清理批(75→55 条,台账 81)+旗行为基线前置采集批(冒烟 11 断言,台账 83)+两处悬空指认连带(关卡节点地图/acceptance-ap6-ac)——e66d156 先例

This commit is contained in:
DKChan committed 2026-09-19 05:54:54 +08:00
1 parent e66d1561bc
commit d236d9f8e1
20 files changed
+449 -250

No files matched your search

@@ -513,3 +513,12 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/s
**文档/registry 同批(批 3)**:敌人系统 v0.5(技能池章节重写)/ 战斗结算 v0.11(随机口径三处/三乘法链/施放行/敌方防御池/AC 措辞四处)/ 增益状态 v0.5(九行 + 敌方防御池;顺手清偿 v0.4 挂起的 sapped 文档同步)/ 难度曲线与Boss v0.6(曲线纪律/Boss 专属池/技能池投值表 + EV 实测登记)/ 战斗表现 v0.3 / HUD框架 v0.4 / 数据配置(敌人表措辞 + 技能表行)/ 03-内容规模 / 02-系统清单 / 关卡节点地图 / ux battle.md(改型注记)/ architecture.md(单位容器 API 面)/ registry(rng/skill 新条目/enemy/status_effect/settlement/术语七词条/修订记录)/ 06-待办清单(改型"单拍行为环体验验证"、收编关闭"行为环废除与蓄力拍退役"、新增 P1 五项 + P2 技能图鉴)。
**实现决策登记(提案未定处的自行拍板,按封装纪律报告)**:① 保底 M 载体 = 技能表全局单字段 pity_threshold(不分敌种,难度曲线单旋钮);② 冷却"施放后连续 N 拍不可摇中"的实现 = 摇号按当前冷却值排除、递减在其后(与提案"递减→排除"的可摇集合逐拍恒等);③ 增益持续与技能冷却两计时器 = apply_effect duration_override 通道;④ 敌方防御池乘区落点 = 主角攻击 Σ三分量后(hero_offense 现行走普通分量直乘的切片现状不动——登记遗留);⑤ 摇号器类名 SkillLottery(core/skill_lottery.gd)。
## 2026-09-19 技能池新效应词汇批(用户拍板——设计权威 docs/proposal-status-vocabulary.md v1.0)
**三词收敛两词**:妨害技(debuff 新类型)+ 必中附加分量(rider_mult,"固定值伤害"改形);多段命中经用户拍板退役(无防御磨损体系,无对照机制)。**实装面 = 仅 L2 Boss 蛛化卓尔精灵,其余敌种不动**:
- 毒爆 venom_burst + `rider_mult = 0.75`——必中毒分量 = max(1, floor(atk × 0.75))(atk 开战掷定 2–4 → 毒 1–3 点),后置加法、必中全额、不进乘区不出暴击;伤害行 payload 增 `rider` 键 + 行文本「+附加N(必中)」。
- 新技能行**蛛网束缚 web_snare**(debuff → off_guard 失衡,duration 2 = 1 有效结算,冷却 4,池权重 2 → 卓尔池 6/2/2/2 全 provisional)——0 伤害施放行,状态按 target_side 落主角(掷点阈值 10 → 8)。
- 结构面:SkillTable type 枚举 +debuff;池纪律 +每池妨害 ≤1;施放类 side 守卫(buff→enemy / debuff→hero,`validate_skill_pool_links` 增 status_table 显式入参——装载序修正);意图徽标 +妨害系 ☠(紫)。
- 验证:冒烟 306 项 0 失败(+新探针 `_smoke_debuff_rider`:毒爆行逐拍精确复算 / rider 键封闭 / 蛛网束缚落主角端到端 / 六项加载校验拒绝)+ 四探针 PASS;800 局模拟试玩(两关 × 20 种子 × 双画像)——L2 基线通关 90%→80%、死因全部集中 Boss(设计方向生效),L1 零行为变更回归一致(65%/85%)。
- 顺手收口:smoke_all `variant_pool(1, ["dungeon"])` → `"dungeon"`(并行 board_type 批签名 Array→String 的漏网同步——此前该断言被 SCRIPT ERROR 静默吞掉);analyze_playtest.py 反事实重算 +rider 识别。
@@ -481,9 +481,10 @@ func _combat_segment(ri: int) -> void:
if srow.is_empty():
_fatal("敌实例待发动作悬空: %s" % skill_id)
continue
if String(srow.type) == "buff":
# 增益技 = 0 伤害施放行:不兑付伏击(旧蓄力拍"零兑付"语义由施放行继承——本轮喂的伏击
# 逢施放拍不兑现的读怪奖励保留);状态授予即数值兑现,技能冷却与增益持续两计时器分账。
if String(srow.type) == "buff" or String(srow.type) == "debuff":
# 增益/妨害技(新效应词汇批 2026-09-19)= 0 伤害施放行:不兑付伏击(旧蓄力拍"零兑付"语义由施放行
# 继承——本轮喂的伏击逢施放拍不兑现的读怪奖励保留);状态授予即数值兑现,技能冷却与增益持续两计时器分账。
# 落点由状态行 target_side 路由(buff → 敌方侧 / debuff → 主角侧——容器零改动,proposal-status-vocabulary §1)。
var dur: int = int(srow.get("buff_duration", 1))
_status_effects.apply_effect(String(srow.buff_ref), {"source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "round_index": ri}, dur)
_emit_row("cast", "enemy_action", Vector2i(-1, -1), ri, "%s 施放 %s" % [u.display_name, String(srow.display_name)], 0, {"skill_id": skill_id, "effect_id": String(srow.buff_ref), "duration": dur})
@@ -491,21 +492,31 @@ func _combat_segment(ri: int) -> void:
continue
# 伤害技(含普攻——倍率恒 1):三乘法链 ① A = 攻击基线 × 技能倍率(判定对象 = 技能基础伤害分量)
# → ② d20 vs 主角 AC(nat20 暴击 ≠ 技能重击,两层严格区分)→ ③ +克制修正点数(敌打主角无甲轴恒中性 delta 0——加法核 2026-09-18 换装)
# → 伏击附加(后置加法、必中全额、不吃 AC)→ 增益池乘区 → 伤害下限 max(1,·)。
# → 伏击附加(后置加法、必中全额、不吃 AC)→ 增益池乘区 → 伤害下限 max(1,·)
# → 必中附加分量 rider(新效应词汇批 2026-09-19:后置加法、必中全额、不进乘区不出暴击——
# proposal-status-vocabulary §2;价位锚 rider_mult=0.75 = AC 折扣档最轻档"不掉档的稳定伤害")。
var baseline: int = int(round(float(u.atk) * float(srow.get("damage_mult", 1.0))))
var rider := 0
if srow.has("rider_mult"):
rider = maxi(int(db.settlement.damage_floor), int(floor(float(u.atk) * float(srow.rider_mult))))
var roll_read_e: Dictionary = _armor_roll()
var threshold_e: int = _defender_ac(_enemy_system.hero_unit()) + int(db.settlement.attack_bonus)
var crit_e: bool = bool(roll_read_e.crit)
var discount_e: int = _roll_discount(int(roll_read_e.roll), threshold_e)
var rolled_base: int = DamageMath.rolled_component(float(baseline), discount_e, crit_e, int(db.settlement.crit_multiplier), 0) # 敌打主角无甲轴恒中性——加法核中性 delta = 0(原 percent 100 退役)
var d_tick := _resolve_damage(rolled_base + L * _m_amb)
d_tick = maxi(int(db.settlement.damage_floor), d_tick)
d_tick = maxi(int(db.settlement.damage_floor), d_tick) + rider # rider 后置于伤害下限——独立必中加项,不进乘区
_enemy_system.apply_damage(_enemy_system.hero_unit(), d_tick)
var payload_e: Dictionary = {"target": "hero", "skill_id": skill_id, "baseline": baseline, "attach": L * _m_amb,
"roll": int(roll_read_e.roll), "ac": threshold_e, "tier": discount_e}
if crit_e:
payload_e["crit"] = true
_emit_row("damage", "enemy_action", Vector2i(-1, -1), ri, _attack_label("%s %s" % [u.display_name, String(srow.display_name)], crit_e, discount_e), d_tick, payload_e)
if rider > 0:
payload_e["rider"] = rider
var label_e := _attack_label("%s %s" % [u.display_name, String(srow.display_name)], crit_e, discount_e)
if rider > 0:
label_e += "+附加%d(必中)" % rider
_emit_row("damage", "enemy_action", Vector2i(-1, -1), ri, label_e, d_tick, payload_e)
_enemy_system.commit_action(u) # 调用序③执行上报——技能进冷却 + 保底计数(每结算每存活敌恰一次)
@@ -167,21 +167,35 @@ func _cross_validate(loaded: Dictionary) -> void:
# ⑤ 技能池引用完整性(敌人技能池批——敌人表池 × 技能表;独立入口 validate_skill_pool_links 供冒烟直验)
var skt = loaded.get("skill")
if skt != null and en != null:
for e in validate_skill_pool_links(skt, en):
for e in validate_skill_pool_links(skt, en, loaded.get("status_effects")):
push_diag("error", e.category, e.message)
# 池模板 × 技能行跨表校验(加载期致命;返回错误列表——ConfigDB 诊断通道与冒烟直验共用):
# skill_id 悬空致命 + 池构成纪律(每池恰一行普攻、增益技 ≤1——proposal §2.1 结构保证)。
func validate_skill_pool_links(skill_table, enemy_table) -> Array:
# skill_id 悬空致命 + 池构成纪律(每池恰一行普攻、增益技 ≤1、妨害技 ≤1——proposal §2.1 结构保证 +
# proposal-status-vocabulary §1.2)+ 施放类 side 守卫(增益引用 enemy 侧 / 妨害引用 hero 侧状态行)。
# status_table 显式入参:装载序上本校验先于成员赋值执行(self.status_effects 尚为 Nil)——装载通道传 loaded 表,
# 冒烟直验传 _db.status_effects;传 null 时 side 守卫跳过(其他校验不受影响)。
func validate_skill_pool_links(skill_table, enemy_table, status_table = null) -> Array:
var errs: Array = []
var skill_types := {}
for row in skill_table.rows:
skill_types[String(row.get("skill_id", ""))] = String(row.get("type", ""))
# 施放类 side 守卫(新效应词汇批 2026-09-19):buff → 敌方侧状态 / debuff → 主角侧状态,防反向挂接
for row in skill_table.rows:
var st: String = String(row.get("type", ""))
if st != "buff" and st != "debuff":
continue
var want_side: String = "enemy" if st == "buff" else "hero"
var eff: String = String(row.get("buff_ref", ""))
var srow: Dictionary = status_table.row_by_id(eff) if status_table != null else {}
if not srow.is_empty() and String(srow.get("target_side", "")) != want_side:
errs.append({"category": "cast_side", "message": "SkillTable %s(%s)引用 %s 的 target_side=%s ≠ %s(施放类 side 守卫)" % [String(row.get("skill_id", "")), st, eff, String(srow.get("target_side", "")), want_side]})
for row in enemy_table.rows:
var eid: String = String(row.get("enemy_id", ""))
var basic_n := 0
var buff_n := 0
var debuff_n := 0
for entry in row.get("pool", []):
var sid: String = String(entry.get("skill_id", ""))
if not skill_types.has(sid):
@@ -192,10 +206,14 @@ func validate_skill_pool_links(skill_table, enemy_table) -> Array:
basic_n += 1
"buff":
buff_n += 1
"debuff":
debuff_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]})
return errs
@@ -13,9 +13,10 @@ extends Resource
# (特殊技内按权重、不受冷却限制——proposal §2.1 两道闸之二)。全局单旋钮(不分敌种)。
@export var pity_threshold: int = 4
const SKILL_TYPES := ["basic", "damage", "buff"] # 普通攻击 / 伤害 / 增益(封闭枚举——新类型走文档修订)
const SKILL_TYPES := ["basic", "damage", "buff", "debuff"] # 普通攻击 / 伤害 / 增益 / 妨害(封闭枚举——新效应词汇批 2026-09-19 由三扩四)
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 折扣档最轻档)
func validate() -> Array:
@@ -52,7 +53,10 @@ func validate() -> Array:
errs.append({"category": "cross_field", "message": "%s 普攻 cooldown_beats ≠ 0(禁配——池恒非空的结构前提): %d" % [sid, cd]})
if row.has("buff_ref") or row.has("buff_duration"):
errs.append({"category": "cross_field", "message": "%s 普攻不带增益字段" % sid})
# 伤害技:存倍率不存固定值——伤害 = 施放者攻击基线 × 技能倍率(三乘法链第一乘)
if row.has("rider_mult"):
errs.append({"category": "cross_field", "message": "%s 普攻不带必中附加分量(rider_mult 仅伤害技)" % sid})
# 伤害技:存倍率不存固定值——伤害 = 施放者攻击基线 × 技能倍率(三乘法链第一乘);
# 可选 rider_mult = 必中附加分量(后置加法、不吃骰不出暴击——proposal-status-vocabulary §2)
if type == "damage":
if not row.has("damage_mult"):
errs.append({"category": "cross_field", "message": "%s 伤害技缺 damage_mult(存倍率不存固定值)" % sid})
@@ -62,15 +66,33 @@ func validate() -> Array:
errs.append({"category": "domain", "message": "%s damage_mult 越域 [%.1f, %.1f]: %s" % [sid, MULT_DOMAIN_MIN, MULT_DOMAIN_MAX, str(m)]})
if row.has("buff_ref") or row.has("buff_duration"):
errs.append({"category": "cross_field", "message": "%s 伤害技不带增益字段" % sid})
if row.has("rider_mult"):
var rm: float = float(row.rider_mult)
if rm <= 0.0 or rm > RIDER_MULT_MAX:
errs.append({"category": "domain", "message": "%s rider_mult 越域 (0, %.1f]: %s" % [sid, RIDER_MULT_MAX, str(rm)]})
# 增益技:0 伤害动作(无倍率),效果引用 + 持续回合;悬空引用由 ConfigDB 跨表校验兜底
if type == "buff":
if row.has("damage_mult"):
errs.append({"category": "cross_field", "message": "%s 增益技为 0 伤害动作,不带 damage_mult" % sid})
if row.has("rider_mult"):
errs.append({"category": "cross_field", "message": "%s 增益技不带必中附加分量(rider_mult 仅伤害技)" % sid})
if String(row.get("buff_ref", "")) == "":
errs.append({"category": "cross_field", "message": "%s 增益技缺 buff_ref(效果引用)" % sid})
var bd: int = int(row.get("buff_duration", 0))
if bd < 1 or bd > 99:
errs.append({"category": "domain", "message": "%s buff_duration 越域 [1,99]: %d" % [sid, bd]})
# 妨害技(新效应词汇批 2026-09-19):0 伤害施放动作,对主角施加负面状态——
# 字段复用 buff_ref/buff_duration(落点由状态行 target_side 路由,ConfigDB side 守卫兜底);池纪律妨害 ≤1 在跨表通道
if type == "debuff":
if row.has("damage_mult"):
errs.append({"category": "cross_field", "message": "%s 妨害技为 0 伤害动作,不带 damage_mult" % sid})
if row.has("rider_mult"):
errs.append({"category": "cross_field", "message": "%s 妨害技不带必中附加分量(rider_mult 仅伤害技)" % sid})
if String(row.get("buff_ref", "")) == "":
errs.append({"category": "cross_field", "message": "%s 妨害技缺 buff_ref(效果引用)" % sid})
var dbd: int = int(row.get("buff_duration", 0))
if dbd < 1 or dbd > 99:
errs.append({"category": "domain", "message": "%s buff_duration 越域 [1,99]: %d" % [sid, dbd]})
if not has_basic:
errs.append({"category": "structural", "message": "SkillTable 无普攻行(basic)——池构成纪律的结构前提"})
if pity_threshold < 1 or pity_threshold > 99:
@@ -276,6 +276,9 @@ rows = [{
}, {
"skill_id": "venom_burst",
"weight": 2
}, {
"skill_id": "web_snare",
"weight": 2
}],
"advantage_per_round_cap": 4,
"armor_type": "ward",
@@ -342,6 +345,11 @@ rows = [{
;
; 甲效矩阵加法核换装批(2026-09-18 用户拍板——settlement.tres armor_effectiveness 量纲 percent→整数点数,
; 三档 克制 +1 / 中性 0 / 被克 −1,五档百分比词表随批退役)。
;
; 新效应词汇批(2026-09-19 用户拍板——设计权威 docs/proposal-status-vocabulary.md v1.0):卓尔池
; 6/2/2 → 6/2/2/2(+蛛网束缚 web_snare 2——妨害技,对主角施 off_guard;每池妨害 ≤1 纪律入跨表校验)。
; 主分量权重期望 1.67 → 1.25(零伤害拍占 2/12),加毒爆必中附加分量(max(1,floor(atk×0.75)),atk 区间
; 2–4 → 1–3 点)后净期望待抽样复核归 难度曲线与Boss;实装面 = 仅卓尔,其余敌种不动。
; 现行读数效果:L1/L2 小怪全 light → 穿刺类(开格充能/穿刺弹)被克 −1(穿刺弹 2→1、充能 3 点→2)、普通/魔法中性;
; L2 仅 Boss 战非中性(魔法甲)——基础攻击与穿刺弹克制 +1、魔法矢被克 −1(原威力 + delta 自行核算后落笔)。
; 原重配批注「魔法矢克 Boss ×150、穿刺弹被克 ×50」与当批矩阵正反互倒(系笔误),随本批订正为上行读数。
@@ -47,6 +47,14 @@ rows = [{
"display_name": "毒爆",
"type": "damage",
"damage_mult": 2.5,
"rider_mult": 0.75,
"cooldown_beats": 4
}, {
"skill_id": "web_snare",
"display_name": "蛛网束缚",
"type": "debuff",
"buff_ref": "off_guard",
"buff_duration": 2,
"cooldown_beats": 4
}]
@@ -67,3 +75,14 @@ rows = [{
; 毒爆 venom_burst = 伤害 ×2.5 冷却 4(蛛化卓尔精灵 drider 专属池独占技——"Boss=专属池独占技+权重轮廓"
; 纪律的本批兑现;池引用唯一性 = 唯一 enemy_id: drider)。
; 两行均为 damage 型(存倍率不存固定值——伤害 = 施放者攻击基线 × 技能倍率,三乘法链第一乘)。
;
; 新效应词汇批(2026-09-19 用户拍板——设计权威 docs/proposal-status-vocabulary.md v1.0,全值 provisional
; 投值归 难度曲线与Boss):三词收敛两词——多段命中退役(无防御磨损体系,无对照机制);固定值伤害改形
; 必中附加分量(rider_mult,域 (0,1]):后置加法、必中全额、不进乘区不出暴击,分量值 =
; max(damage_floor, floor(攻击基线 × rider_mult))——随基线缩放保持共享技能表前提;
; ×0.75 价位锚 = AC 折扣档最轻档"不掉档的稳定伤害"(可掷分量期望 AC10≈0.80 / AC12≈0.76 对表)。
; ① 毒爆 + rider_mult 0.75(必中毒分量:atk3 → 毒 2 点,atk 区间 2–4 → 1–3 点随开战掷定值缩放)。
; ② 蛛网束缚 web_snare(新行,妨害型 debuff——对主角施加 off_guard 失衡):0 伤害施放行(不掷不折不兑付
; 伏击),字段复用 buff_ref/buff_duration;duration 2 = 1 个有效结算(empower/fortify 先兑付后衰减含
; 施放当轮口径);冷却 4;首发载荷 = 失衡(用户拍板),乏力 sapped 为第二词汇暂不实装;
; 池纪律 +:每池妨害 ≤1(ConfigDB 跨表校验);实装面 = 仅卓尔池,其余敌种不动。
@@ -11,13 +11,15 @@ class_name EnemyCardView
extends PanelContainer
const COL_INK := Color8(51, 38, 27)
# 摇定动作呈现映射(敌人技能池批 §2.4 三档可见性):键 = pending_type(快照投影)。
# 摇定动作呈现映射(敌人技能池批 §2.4 三档可见性;新效应词汇批 2026-09-19 扩四档):键 = pending_type(快照投影)。
# 普通攻击零表示("—" 中性色——完全不可预测);伤害特殊技 = 攻击系 ⚔(朱红);
# 增益特殊技 = 防御/增益系 ✦(蓝)。nat20 暴击无前置表示(掷点层,与技能重击严格区分)。
# 增益特殊技 = 防御/增益系 ✦(蓝);妨害特殊技 = 妨害系 ☠(紫——对主角施减益,proposal-status-vocabulary §1.4)。
# nat20 暴击无前置表示(掷点层,与技能重击严格区分)。
const PENDING_STYLE := {
"basic": ["—", Color8(120, 106, 88)],
"damage": ["⚔", Color8(198, 61, 34)],
"buff": ["✦", Color8(46, 110, 142)],
"debuff": ["☠", Color8(122, 66, 148)],
}
# 蓄力档技能集(2026-09-18 用户拍板:重击类独立蓄力提示——伤害技 ⚔ / 增益技 ✦ / 重击类 蓄力 三档占位)
const CHARGE_SKILL_IDS := ["heavy"]
@@ -105,7 +107,7 @@ func _set_order(i: int) -> void:
# 摇定动作渲染(敌人技能池批——presenter 不翻译,渲染映射唯一定义处归本类〔code-review 2026-09-12 先例〕):
# pending_type = 战斗结算 快照投影(basic/damage/buff;"" = 未摇定/死亡残位)。
# pending_type = 战斗结算 快照投影(basic/damage/buff/debuff;"" = 未摇定/死亡残位)。
# 信号/入参契约:set_data 每结算行刷新、set_pending 每轮开轮刷新(开轮摇号时点)。
func set_pending(ptype: String, pid: String) -> void:
if _dead:
@@ -0,0 +1 @@
uid://cg2dgdnbkd7op
@@ -82,6 +82,7 @@ func _run_core_smoke(db, rng) -> void:
_smoke_clear_reward_chain()
_smoke_refresh_provenance_guard()
_smoke_polarity_routing()
_smoke_flag_misfire_dig_after_pull()
_smoke_negative_dictionary()
_smoke_resource_surface()
_smoke_settlement_segments()
@@ -489,7 +490,7 @@ func _mk_stream(stream_name: String, scope: int) -> RandomNumberGenerator:
func _smoke_variation_framework() -> void:
print("--- 地形变异 框架面(空清单合法 + 候选池门控通道)---")
check(_db.variation.rows.is_empty(), "变异清单为空(P0 合法态——零变体格)")
check(_db.variant_pool(1, ["dungeon"]).is_empty(), "候选池为空——棋盘生成静默产出无变异图(不报错、不警告)")
check(_db.variant_pool(1, "dungeon").is_empty(), "候选池为空——棋盘生成静默产出无变异图(不报错、不警告)")
check(_db.variation.row_by_id("nonexistent").is_empty(), "清单外 variant_id 返回空(读取通道在位)")
@@ -863,6 +864,77 @@ func _find_flag_target(minesweeper, want_mine: bool) -> Dictionary:
return {}
# ---- 旗奖励语义实证(2026-09-19 观测项⑧语义锚——用户质询「误旗判定后拔旗再挖是否仍有奖励」)----
# 场景 A(跨轮):标安全格 → 收轮判定(flag_miss 负向兑付)→ 拔旗 → 挖开 → 收轮。
# 语义锚 = 惩罚与奖励并行:判定走消费型 grant、挖开走查询型 query_full_snapshot,两通道互不吞并。
# 场景 B(同轮插撤):标安全格 → 同轮拔旗 → 挖开 → 收轮。被拔不判(抵消过滤)——零惩罚零影响。
func _smoke_flag_misfire_dig_after_pull() -> void:
print("--- 旗奖励语义(误旗判定后拔旗再挖)---")
# 场景 A:跨轮(判定已走)
var rng_a = load("res://core/rng_service.gd").new()
rng_a.set_master_seed(20260919)
var settle_a = load("res://core/battle_orchestrator.gd").new(_db, rng_a)
var rows_a: Array = []
settle_a.step_row.connect(func(row): rows_a.append(row))
check(settle_a.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}), "旗语义 A:开局受理")
var ms_a = settle_a.minesweeper
var t_a := _find_flag_target(ms_a, false)
check(not t_a.is_empty(), "旗语义 A:找到安全格旗目标")
if not t_a.is_empty():
if t_a.hero_cell != ms_a.hero_pos():
ms_a.settle_batch([{"action": "move", "cell": t_a.hero_cell}])
ms_a.settle_batch([{"action": "flag_place", "cell": t_a.flag_cell}])
ms_a.settle_batch([{"action": "stop"}]) # 收轮 → 判定批:flag_miss + 负向兑付
ms_a.settle_batch([{"action": "flag_pull", "cell": t_a.flag_cell}])
var dig_a: Array = ms_a.settle_batch([{"action": "dig", "cell": t_a.flag_cell}])
check(not dig_a.is_empty() and bool(dig_a[0].get("accepted", false)), "旗语义 A:拔旗后挖开被受理")
ms_a.settle_batch([{"action": "stop"}])
var target_a: Array = [t_a.flag_cell.x, t_a.flag_cell.y]
var miss_attr := 0
var miss_neg := 0
var dig_reward := 0
for row in rows_a:
if row.get("origin_cell", [0, 0]) != target_a:
continue
if row.get("type", "") == "flag_miss":
miss_attr += 1
if String(row.get("source_class", "")) == "flag_miss":
miss_neg += 1
if String(row.get("source_class", "")) == "safe_cell":
dig_reward += 1
check(miss_attr == 1, "旗语义 A:标空归因行恰一次(×%d——每格实例一次去重)" % miss_attr)
check(miss_neg >= 1, "旗语义 A:标空负向兑付行在案(×%d)" % miss_neg)
check(dig_reward >= 1, "旗语义 A:判定后拔旗再挖仍获正向奖励(×%d——惩罚与奖励并行)" % dig_reward)
# 场景 B:同轮插撤(被拔不判)
var rng_b = load("res://core/rng_service.gd").new()
rng_b.set_master_seed(20260919)
var settle_b = load("res://core/battle_orchestrator.gd").new(_db, rng_b)
var rows_b: Array = []
settle_b.step_row.connect(func(row): rows_b.append(row))
check(settle_b.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}), "旗语义 B:开局受理")
var ms_b = settle_b.minesweeper
var t_b := _find_flag_target(ms_b, false)
check(not t_b.is_empty(), "旗语义 B:找到安全格旗目标")
if not t_b.is_empty():
if t_b.hero_cell != ms_b.hero_pos():
ms_b.settle_batch([{"action": "move", "cell": t_b.hero_cell}])
ms_b.settle_batch([{"action": "flag_place", "cell": t_b.flag_cell}])
ms_b.settle_batch([{"action": "flag_pull", "cell": t_b.flag_cell}]) # 同轮拔——发射面已过滤
var dig_b: Array = ms_b.settle_batch([{"action": "dig", "cell": t_b.flag_cell}])
check(not dig_b.is_empty() and bool(dig_b[0].get("accepted", false)), "旗语义 B:同轮插撤后挖开被受理")
ms_b.settle_batch([{"action": "stop"}])
var target_b: Array = [t_b.flag_cell.x, t_b.flag_cell.y]
var miss_b := 0
var reward_b := 0
for row in rows_b:
if row.get("type", "") == "flag_miss" or String(row.get("source_class", "")) == "flag_miss":
miss_b += 1
if String(row.get("source_class", "")) == "safe_cell" and row.get("origin_cell", [0, 0]) == target_b:
reward_b += 1
check(miss_b == 0, "旗语义 B:同轮插撤零判定零惩罚(×%d——被拔不判)" % miss_b)
check(reward_b >= 1, "旗语义 B:同轮插撤后挖开奖励不受影响(×%d)" % reward_b)
# ---- 试玩日志器冒烟(结算快照 JSONL + 诊断转储)----
func _smoke_playtest_logger() -> void:
print("--- PlaytestLogger(结算快照 JSONL + 诊断转储)---")
@@ -1538,11 +1610,12 @@ func _smoke_skill_pool() -> void:
_smoke_skill_pool_ev_anchor()
_smoke_skill_pool_death_void()
_smoke_skill_pool_validation_reject()
_smoke_debuff_rider()
# ① 数据面:技能表行集 + 普攻锁死 + 建议池行值 + 空池不可能(结构断言)
func _smoke_skill_pool_data() -> void:
check(_db.skill.rows.size() == 7, "技能表七行(普攻/突刺/蓄力/重击/硬化 + 撕咬/毒爆——分支节点图批 L2 新行)")
check(_db.skill.rows.size() == 8, "技能表八行(普攻/突刺/蓄力/重击/硬化 + 撕咬/毒爆/蛛网束缚——分支节点图批 + 新效应词汇批)")
var basic: Dictionary = _db.skill.row_by_id("attack")
check(not basic.is_empty() and String(basic.type) == "basic" and int(basic.cooldown_beats) == 0
and (not basic.has("damage_mult") or is_equal_approx(float(basic.damage_mult), 1.0)),
@@ -1557,14 +1630,29 @@ func _smoke_skill_pool_data() -> void:
check(not bite.is_empty() and is_equal_approx(float(bite.damage_mult), 1.5) and int(bite.cooldown_beats) == 1,
"撕咬行(伤害 ×1.5 · 冷却 1——巨蜘蛛池载体,与突刺同倍率错开冷却轴)")
var venom: Dictionary = _db.skill.row_by_id("venom_burst")
check(not venom.is_empty() and is_equal_approx(float(venom.damage_mult), 2.5) and int(venom.cooldown_beats) == 4,
"毒爆行(伤害 ×2.5 · 冷却 4——蛛化卓尔精灵专属池独占技)")
check(not venom.is_empty() and is_equal_approx(float(venom.damage_mult), 2.5) and int(venom.cooldown_beats) == 4
and is_equal_approx(float(venom.rider_mult), 0.75),
"毒爆行(伤害 ×2.5 · 冷却 4 · 必中附加 ×0.75——蛛化卓尔精灵专属池独占技,新效应词汇批)")
var venom_sole_ok := true
for row in _db.enemy.rows:
for entry in row.pool:
if String(entry.skill_id) == "venom_burst" and String(row.enemy_id) != "drider":
venom_sole_ok = false
check(venom_sole_ok, "毒爆独占性(仅蛛化卓尔精灵池引用——Boss 专属池独占技纪律)")
# 新效应词汇批(2026-09-19):妨害技首发行 + off_guard 落点 + 实装面收窄
var snare: Dictionary = _db.skill.row_by_id("web_snare")
check(not snare.is_empty() and String(snare.type) == "debuff" and String(snare.buff_ref) == "off_guard"
and int(snare.buff_duration) == 2 and int(snare.cooldown_beats) == 4,
"蛛网束缚行(妨害型 · buff_ref → off_guard 失衡 · duration 2 = 1 有效拍 · 冷却 4——新效应词汇批首发)")
var snare_sole_ok := true
for row in _db.enemy.rows:
for entry in row.pool:
if String(entry.skill_id) == "web_snare" and String(row.enemy_id) != "drider":
snare_sole_ok = false
check(snare_sole_ok, "蛛网束缚独占性(仅蛛化卓尔精灵池引用——实装面收窄,其余敌种不动)")
var off_guard_row: Dictionary = _db.status_effect_row("off_guard")
check(not off_guard_row.is_empty() and String(off_guard_row.target_side) == "hero" and String(off_guard_row.unit) == "ac",
"失衡状态行(hero · ac 轴——妨害技落点,ConfigDB side 守卫正例)")
# 行集键 = (level_index, node_depth)——分支节点图批起 bugbear 等敌种跨关异池,enemy_id 不再唯一
var expect_pools := {
"1_0": {"attack": 9, "lunge": 1},
@@ -1578,7 +1666,7 @@ func _smoke_skill_pool_data() -> void:
"2_5": {"attack": 5, "lunge": 2, "heavy": 2},
"2_6": {"attack": 6, "bite": 2, "charge": 1},
"2_7": {"attack": 6, "bite": 2, "charge": 1},
"2_8": {"attack": 6, "heavy": 2, "venom_burst": 2},
"2_8": {"attack": 6, "heavy": 2, "venom_burst": 2, "web_snare": 2},
}
var pools_ok := true
for row in _db.enemy.rows:
@@ -1591,7 +1679,7 @@ func _smoke_skill_pool_data() -> void:
got[String(entry.skill_id)] = int(entry.weight)
if got != expect_pools[rk]:
pools_ok = false
check(pools_ok, "十二行池模板 = 建议池行值(L1 地精 9/1 · 熊地精 6/2/2 · 酋长 6/2/2;L2 兄弟精 6/1/2 · 蜘蛛 6/2/1 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2——全部 provisional)")
check(pools_ok, "十二行池模板 = 建议池行值(L1 地精 9/1 · 熊地精 6/2/2 · 酋长 6/2/2;L2 兄弟精 6/1/2 · 蜘蛛 6/2/1 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2/2——全部 provisional)")
var basic_count_ok := true
for row in _db.enemy.rows:
var n := 0
@@ -1830,6 +1918,26 @@ func _smoke_skill_pool_validation_reject() -> void:
check(not et2.validate().is_empty(), "加载校验拒绝:池权重 ≤ 0")
et2.rows = [{"enemy_id": "x", "display_name": "x", "pool": []}]
check(not et2.validate().is_empty(), "加载校验拒绝:空池(禁)")
# 新效应词汇批(2026-09-19):妨害 / 必中附加分量校验组
skill_table.rows = [
{"skill_id": "w1", "display_name": "w1", "type": "debuff", "buff_ref": "off_guard", "buff_duration": 2, "cooldown_beats": 4},
{"skill_id": "w2", "display_name": "w2", "type": "debuff", "buff_ref": "off_guard", "buff_duration": 2, "cooldown_beats": 4}]
enemy_table.rows = [{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "w1", "weight": 1}, {"skill_id": "w2", "weight": 1}]}]
check(_has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "pool_composition"),
"加载校验拒绝:池妨害技 > 1(每池妨害 ≤1 纪律)")
skill_table.rows = [{"skill_id": "w1", "display_name": "w1", "type": "debuff", "cooldown_beats": 4}]
check(not skill_table.validate().is_empty(), "加载校验拒绝:妨害技缺 buff_ref(效果引用)")
skill_table.rows = [{"skill_id": "d", "display_name": "d", "type": "damage", "damage_mult": 1.5, "rider_mult": 1.5, "cooldown_beats": 1}]
check(not skill_table.validate().is_empty(), "加载校验拒绝:rider_mult 越域 (0,1]")
skill_table.rows = [{"skill_id": "b", "display_name": "b", "type": "buff", "buff_ref": "empower", "buff_duration": 2, "rider_mult": 0.5, "cooldown_beats": 1}]
check(not skill_table.validate().is_empty(), "加载校验拒绝:非伤害技带必中附加分量")
# 施放类 side 守卫:妨害技引用敌方侧状态 → 拒绝(增益技引用主角侧同罪,本组以妨害反例覆盖)
skill_table.rows = [
{"skill_id": "atk", "display_name": "a", "type": "basic", "cooldown_beats": 0},
{"skill_id": "w1", "display_name": "w1", "type": "debuff", "buff_ref": "empower", "buff_duration": 2, "cooldown_beats": 4}]
enemy_table.rows = [{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "atk", "weight": 1}, {"skill_id": "w1", "weight": 1}]}]
check(_has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table, _db.status_effects), "cast_side"),
"加载校验拒绝:妨害技引用敌方侧状态(施放类 side 守卫)")
func _has_err_category(errs: Array, category: String) -> bool:
@@ -1839,6 +1947,74 @@ func _has_err_category(errs: Array, category: String) -> bool:
return false
# ⑦ 新效应词汇批实装探针(2026-09-19——妨害技蛛网束缚 + 毒爆必中附加分量,实装面 = L2 Boss 卓尔):
# 停手轮驱动 (2,8)——零挖掘零旗 → 零池零伏击,伤害行可逐拍精确复算;种子 hunt 至少各命一粒毒爆伤害行
# 与蛛网束缚施放行;毒分量随开战掷定的攻击基线缩放(atk 区间 2–4 → rider 1–3)。
func _smoke_debuff_rider() -> void:
var venom_seed := -1
var venom_check_ok := true
var venom_rows_seen := 0
var snare_seed := -1
var base_ac: int = int(_db.hero.armor_class)
var off_m: int = int(_db.status_effect_row("off_guard").magnitude)
var floor_v: int = int(_db.settlement.damage_floor)
for s in range(1, 401):
if venom_seed > 0 and snare_seed > 0:
break
var rg = load("res://core/rng_service.gd").new()
rg.set_master_seed(s)
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rg)
var rows: Array = []
settlement.step_row.connect(func(row): rows.append(row))
if not settlement.begin_battle({"level_index": 2, "node_depth": 8, "hero_hp": int(_db.hero.max_hp)}):
continue
var boss_atk: int = int(settlement._enemy_system.all_enemies()[0].atk) # 开战掷定攻击基线(毒分量随其缩放)
for i in range(6):
if settlement.battle_result != "":
break
settlement.minesweeper.settle_batch([{"action": "stop"}])
var has_venom := false
var has_snare := false
var snare_round := -1
for row in rows:
if String(row.get("type", "")) != "damage" or String(row.get("source_class", "")) != "enemy_action":
if String(row.get("type", "")) == "cast" and String(row.get("skill_id", "")) == "web_snare":
has_snare = true
snare_round = int(row.get("round_index", -1))
continue
if String(row.get("skill_id", "")) == "venom_burst":
has_venom = true
venom_rows_seen += 1
var rider_v: int = int(row.get("rider", -1))
if rider_v != maxi(floor_v, int(floor(float(boss_atk) * 0.75))):
venom_check_ok = false
# 终值精确复算(停手轮池恒空):主分量链结果 + 必中附加后置
var base_factor: float = float(int(row.tier)) * (float(_db.settlement.crit_multiplier) / 100.0 if bool(row.get("crit", false)) else 1.0)
var rolled_main: int = int(floor(float(int(row.baseline)) * base_factor / 100.0))
var expected: int = maxi(floor_v, rolled_main + int(row.attach)) + rider_v
if int(row.value) != expected:
venom_check_ok = false
elif row.has("rider"):
venom_check_ok = false # rider 键只允许出现在带 rider_mult 的技能行
if has_snare:
# 施放后主角掷点阈值下调(off_guard ac 轴):同轮起的伤害行阈值读数 = 基准 AC + 失衡幅度。
# 只取"可观测"种子(施放轮后仍有敌方伤害行——施放在末轮随战斗终局属合法不可观测,不作失败)。
var seen_lowered := false
for row in rows:
if String(row.get("type", "")) != "damage" or String(row.get("source_class", "")) != "enemy_action":
continue
if int(row.get("round_index", -1)) >= snare_round and int(row.get("ac", -999)) == base_ac + off_m:
seen_lowered = true
break
if seen_lowered and snare_seed < 0:
snare_seed = s
if has_venom and venom_seed < 0:
venom_seed = s
check(venom_seed > 0 and venom_rows_seen >= 1, "毒爆必中附加分量种子可达(seed=%d,毒爆伤害行 ×%d)" % [venom_seed, venom_rows_seen])
check(venom_check_ok, "毒爆伤害行逐拍精确复算:终值 = max(1, 掷点档位主分量 + 伏击附加) + 必中附加(rider 只随 rider_mult 行出现)")
check(snare_seed > 0, "蛛网束缚落主角:施放后掷点阈值 = 基准 AC + 失衡幅度(off_guard ac 轴端到端,seed=%d)" % snare_seed)
# ---- 分支节点图批冒烟:图合法性校验 + L2 幽暗地域图版行进(2026-09-17——关卡节点地图 v1.0 图版落地)----
func _smoke_level_graph() -> void: