merge master(d3fa887 技能池④+⑤提灯)入分支:07清单⑭双登冲突解(技能池⑭保留+秘宝目检改⑮);四文件auto-merge带旧词族零带入(全查仅存档键合法例外)
This commit is contained in:
commit
a9a2e25a0a
17 files changed
+342
-28
No files matched your search
@@ -916,6 +916,15 @@ 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)
|
||||
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):
|
||||
var srow_u: Dictionary = 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")
|
||||
var entries: Array = _enemy_system.rollable_skills(u)
|
||||
if _enemy_system.basic_streak(u) >= pity_m:
|
||||
var specials: Array = _enemy_system.special_skills(u)
|
||||
|
||||
@@ -72,11 +72,17 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_override:
|
||||
# 实例池快照(proposal §2.2:开战把模板快照一份,冷却全零——与"生成拿到当时的表值快照"同一条快照纪律)
|
||||
var pool_snap: Array[Dictionary] = []
|
||||
for entry in row.get("pool", []):
|
||||
pool_snap.append({
|
||||
var snap_entry := {
|
||||
"skill_id": String(entry.get("skill_id", "")),
|
||||
"weight": int(entry.get("weight", 0)),
|
||||
"cooldown_left": 0,
|
||||
})
|
||||
}
|
||||
# Boss 半血解锁技(迭代14④ T3——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
|
||||
pool_snap.append(snap_entry)
|
||||
_enemies.append({
|
||||
"handle_id": i,
|
||||
"enemy_id": String(row.enemy_id),
|
||||
@@ -113,32 +119,60 @@ func query_alive(side: String) -> Array:
|
||||
|
||||
# ---- 实例池状态机(敌人技能池批——全部确定性状态,随机在 战斗结算换算层)----
|
||||
|
||||
# 池条目锁定判定(迭代14④ Boss 半血解锁——带 unlock_hp_pct 且未过闩 = 锁定:双面过滤不进
|
||||
# 可摇集合与保底特殊技集合;解锁后即可摇,冷却初值 0)
|
||||
func _entry_locked(e: Dictionary) -> bool:
|
||||
return e.has("unlock_hp_pct") and not bool(e.get("unlocked", false))
|
||||
|
||||
|
||||
# 可摇集合:冷却已尽的池条目投影 [{skill_id, weight}]。普通攻击冷却恒 0 → 恒非空(空池不可能的结构前提,
|
||||
# 加载校验"每池恰一行普攻"兜底)。hero/失效句柄拒绝。
|
||||
# 加载校验"每池恰一行普攻"兜底)。hero/失效句柄拒绝。锁定条目(半血未解锁)双面过滤不入。
|
||||
func rollable_skills(u: 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):
|
||||
continue
|
||||
if int(e.cooldown_left) <= 0:
|
||||
out.append({"skill_id": String(e.skill_id), "weight": int(e.weight)})
|
||||
return out
|
||||
|
||||
|
||||
# 特殊技集合(保底专用——proposal §2.1:必出特殊技、特殊技内按权重、不受冷却限制):type ≠ 普攻的全部池条目。
|
||||
# 锁定条目(半血未解锁)双面过滤——保底不得逼出未解锁技(迭代14④ T3)。
|
||||
func special_skills(u: 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):
|
||||
continue
|
||||
var srow: Dictionary = db.skill_row(String(e.skill_id))
|
||||
if not srow.is_empty() and String(srow.type) != "basic":
|
||||
out.append({"skill_id": String(e.skill_id), "weight": int(e.weight)})
|
||||
return out
|
||||
|
||||
|
||||
# 半血解锁闩检查(迭代14④ T3——开轮摇号前逐敌检查;hp ≤ round(max_hp × unlock_hp_pct/100) →
|
||||
# 一次过门、单向闩〔实例标记,不解锁回退〕。门=确定性谓词零随机;返回本拍新解锁的 skill_id 列表
|
||||
# 供编排层出归因行〔每技恰一次——闩已置位不再返回〕)
|
||||
func check_skill_unlocks(u: Dictionary) -> Array:
|
||||
var out: Array = []
|
||||
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 # 单向闩(不解锁回退——治疗抬血不解锁)
|
||||
out.append(String(e.skill_id))
|
||||
return out
|
||||
|
||||
|
||||
# 摇定结果写入(战斗结算换算层调用——确定值;roll_seq 递增 = 单敌第几拍诊断标签,不参与派生)
|
||||
func set_pending_action(u: Dictionary, skill_id: String) -> void:
|
||||
if u.is_empty() or _despawned or u.get("is_hero", false):
|
||||
|
||||
@@ -91,6 +91,12 @@ 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(可省略——迭代14④ 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):
|
||||
|
||||
@@ -108,6 +108,10 @@ rows = [{
|
||||
}, {
|
||||
"skill_id": "harden",
|
||||
"weight": 2
|
||||
}, {
|
||||
"skill_id": "quake_smash",
|
||||
"weight": 2,
|
||||
"unlock_hp_pct": 50
|
||||
}],
|
||||
"armor_type": "light",
|
||||
"armor_class": 8
|
||||
@@ -268,6 +272,10 @@ rows = [{
|
||||
}, {
|
||||
"skill_id": "web_snare",
|
||||
"weight": 2
|
||||
}, {
|
||||
"skill_id": "venom_curb",
|
||||
"weight": 2,
|
||||
"unlock_hp_pct": 50
|
||||
}],
|
||||
"armor_type": "ward",
|
||||
"armor_class": 10
|
||||
@@ -425,6 +433,10 @@ rows = [{
|
||||
}, {
|
||||
"skill_id": "earth_pulse",
|
||||
"weight": 2
|
||||
}, {
|
||||
"skill_id": "rockslide",
|
||||
"weight": 2,
|
||||
"unlock_hp_pct": 50
|
||||
}],
|
||||
"armor_type": "heavy",
|
||||
"armor_class": 12
|
||||
@@ -457,20 +469,23 @@ rows = [{
|
||||
; 敌人技能池批(2026-09-16 用户拍板——设计权威 docs/proposal-skill-pool.md v1.0):
|
||||
; pattern 行为环数组 + heavy_multiplier 整体退役,改池模板 pool = [{skill_id, weight}](技能行本体在 skills.tres;
|
||||
; 实例池快照与冷却状态在 单位容器 spawn)。建议池(全部 provisional,投值归 难度曲线与Boss;EV 锚 = proposal §2.5):
|
||||
; 地精 普攻 9/突刺 1(每拍 EV 1.05——突刺倍率 >1 必然抬 EV,+5% 偏移显式登记不硬凑);
|
||||
; 地精 普攻 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 池)。
|
||||
; 硬化 = Boss 池独占增益技——增益 ≤1 纪律下旧蓄力拍退役不进 Boss 池)。;
|
||||
; 酋长 EV 重锚(2026-10-05 技能池执行批 T5——震地怒击 unlock_hp_pct=50 半血解锁入池:前半场池 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)。
|
||||
;
|
||||
; 分支节点图批(2026-09-17——L2 幽暗地域内容批,行值全部 provisional,投值归 难度曲线与Boss):
|
||||
; 每战斗/Boss 节点 (2, node_depth) 精确行(跨表校验强制,违例加载致命——ConfigDB ④);锚行 (2,0) 保留。
|
||||
; L2 熊地精 = 熊地精 bugbear 复用(幽暗地域部族设定——第二关三行):
|
||||
; 池差异化改造 = 蓄力拍退役换突刺(复用共享技能行 lunge——共享技能表验证面),display_name 沿用
|
||||
; 熊地精系译名(熊地精/熊地精猎群/熊地精重击手——行值与池与 L1 错开)。节点 5 重击手轮廓:普攻 5/突刺 2/重击 2(EV ≈1.28,权重期望
|
||||
; 熊地精系译名(熊地精/熊地精猎群/熊地精重击手——行值与池与 L1 错开)。节点 5 重击手轮廓:普攻 5/突刺 2/重击 2(名义 EV 1.333;atk1 整数口径 1.333、atk3 每拍 ≈4.11——2026-10-05 整数口径重登记;权重期望
|
||||
——冷却闸实际拉低,抽样复核归实施批口径)。
|
||||
; 巨蜘蛛 giant_spider(新敌种——关间台阶纪律的"新敌种"半边):群行倾向(数量区间 [2,3],首遇位节点 4
|
||||
; 固定 2 不加骰——新敌种首遇教学位纪律同 L1 D2);池 = 普攻 6/撕咬 2/蓄力 1(增益 ≤1 纪律——charge 复用
|
||||
; empower;EV ≈1.11 权重期望);护甲 light(与 L2 熊地精 ward 错开克制面;L1 三甲 goblin=light/
|
||||
; empower;EV 名义 1.00——批注旧值 1.11 = **atk1 整数口径**〔6+2×2〕÷9=1.111,非混用(2026-10-05 T2 消歧登记);护甲 light(与 L2 熊地精 ward 错开克制面;L1 三甲 goblin=light/
|
||||
; bugbear=ward/ogre_chief=heavy 全覆盖,蜘蛛取 light 与同关敌种错开)。
|
||||
; 蛛化卓尔精灵 drider(Boss——用户 2026-09-17 拍板定名,蜘蛛主题呼应):
|
||||
; 专属池 = 普攻 6/重击 2/毒爆 2(独占技 venom_burst 权重轮廓;EV ≈1.67 权重期望,冷却闸拉低后待抽样),
|
||||
|
||||
@@ -63,6 +63,24 @@ rows = [{
|
||||
"buff_ref": "sapped",
|
||||
"buff_duration": 2,
|
||||
"cooldown_beats": 4
|
||||
}, {
|
||||
"skill_id": "quake_smash",
|
||||
"display_name": "震地怒击",
|
||||
"type": "damage",
|
||||
"damage_mult": 2.5,
|
||||
"cooldown_beats": 4
|
||||
}, {
|
||||
"skill_id": "venom_curb",
|
||||
"display_name": "遏毒",
|
||||
"type": "damage",
|
||||
"damage_mult": 2.0,
|
||||
"cooldown_beats": 3
|
||||
}, {
|
||||
"skill_id": "rockslide",
|
||||
"display_name": "岩崩",
|
||||
"type": "damage",
|
||||
"damage_mult": 2.5,
|
||||
"cooldown_beats": 4
|
||||
}]
|
||||
|
||||
; 敌人技能池批(2026-09-16 用户拍板——设计权威 docs/proposal-skill-pool.md v1.0,全值 provisional 投值归 难度曲线与Boss):
|
||||
@@ -98,3 +116,18 @@ rows = [{
|
||||
; (0 伤害施放行,对主角施加 sapped 乏力——第二妨害词汇首发,状态行 status_effects.tres 既有);
|
||||
; 字段口径同 web_snare(duration 2 = 1 个有效结算);每池妨害 ≤1 纪律由跨表校验覆盖。
|
||||
; provisional 归 难度曲线与Boss。
|
||||
;
|
||||
; Boss 半血解锁技三行(2026-10-05 技能池执行批 T5,迭代14④——D7 用户裁案甲:每 Boss 一行新独占伤害技,
|
||||
; 工作值 provisional 归调参;pool 侧 unlock_hp_pct=50 门槛随 enemy.tres 三 Boss 行):
|
||||
; quake_smash 震地怒击 ×2.5/CD4(食人魔酋长)· venom_curb 遏毒 ×2.0/CD3(蛛化卓尔精灵)·
|
||||
; rockslide 岩崩 ×2.5/CD4(灰矮人石君王)。解锁语义/双面过滤/一次性弹出规格 = 敌人系统.md 意图呈现节。
|
||||
;
|
||||
; 有效倍率表(2026-10-05 技能池执行批 T2——D4 整数基线登记:敌技基线=int(round(atk×mult)) 四舍五入半值上取,
|
||||
; 全伤害管线唯一非 floor 取整点〔豁免登记见 registry skill_table〕;半点恒上取保住 atk=1 特殊技辨识度):
|
||||
; atk=0(区间下界):全技塌缩下限 1(突刺==普攻==重击==1)
|
||||
; atk=1(L1 全系+L2 四行):attack 1 / lunge·bite 2(实际 ×2.0 +33%)/ heavy 2(==突刺)/
|
||||
; venom_burst 3(×3.0 +20%)/ venom_curb 2(×2.0 =名义)/ quake_smash 3 / rockslide 3(×3.0 +20%)
|
||||
; atk=2:attack 2 / lunge·bite 3 / heavy 4 / venom_burst 5 / venom_curb 4 / quake_smash 5 / rockslide 5(全精确)
|
||||
; atk=3(L2 精英/L3 全系):attack 3 / lunge·bite 5(×1.667 +11pp)/ heavy 6 / venom_burst 8(×2.667 +6.7pp)/
|
||||
; venom_curb 6(=名义)/ quake_smash 8 / rockslide 8
|
||||
; atk=4(区间上界):全精确(4/6/6/8/10/8/10)
|
||||
@@ -93,10 +93,14 @@ func _consume_row(row: Dictionary) -> void:
|
||||
_play_cross_zone_fx(row) # 跨区因果演出(结算演出链批):开格之因 → 站桩之果
|
||||
_play_settlement_mark(row) # 三轮 #3——逐条标出:Buff/Debuff 色环脉冲 → 伤害/治疗数字浮现
|
||||
# 高光演出(战斗结算×3①② 占位):完美排雷横幅 / 标中旗加重弹道
|
||||
if String(row.get("type", "")) == "highlight":
|
||||
if String(row.get("type", "")) == "highlight" and String(row.get("highlight", "")) != "skill_unlock":
|
||||
board_zone.fx.play_perfect_sweep()
|
||||
elif String(row.get("highlight", "")) == "flag_hit":
|
||||
board_zone.fx.play_flag_hit_projectile(true)
|
||||
# Boss 半血解锁技提示(迭代14④ T4——D8 呈现面:skill_unlock 行弹一次性技能名 toast,
|
||||
# 轻量非演出零动画资产;行文本 =「XX 解锁了 YY」,只报技能名不报数值——规格=敌人系统 意图呈现节)
|
||||
if String(row.get("highlight", "")) == "skill_unlock":
|
||||
board_zone.toast.show_toast(String(row.get("display_name", "")), "info")
|
||||
refresh_request.call()
|
||||
|
||||
|
||||
|
||||
@@ -140,6 +140,13 @@ const FOG_LANTERN_BONUS := 2 # 提灯视野外扩插值位(+2 预留
|
||||
var _fog_enabled := true # 迷雾开关(呈现态布尔——真实游戏恒 on;探针/夹具可 off=无迷雾基线,两态结算逐位一致断言面)
|
||||
var _fog_lantern := 0 # 提灯加成现值(缺省 0=未接效果——预留插值位,半径算式已接入位)
|
||||
|
||||
# 提灯视野外扩接线(迭代14⑤——2026-09-30 拍板④:灯甲视野外扩 +2、类型微光不随视野扩〔微光锚=可挖域
|
||||
# 贴身 8 邻零改动〕。FOG_LANTERN_BONUS 常量与半径算式迷雾批已在位,本 setter=公共接线面:载体道具
|
||||
# 〔aux_items 提灯新行——归 Alpha 信息件重定批〕到位后由组合根/库存挂接调用;缺省 0=无提灯基线零扰动。
|
||||
func set_fog_lantern(enabled: bool) -> void:
|
||||
_fog_lantern = FOG_LANTERN_BONUS if enabled else 0
|
||||
refresh() # 灯光区半径变了——全板重算雾态(refresh/notify_hero_move 既有重绘驱动复用)
|
||||
|
||||
var cell_size: float: # 只读属性(ui_layout_probe 断言面——旧私有名 _cell 的契约化出口)
|
||||
get:
|
||||
return _cell
|
||||
|
||||
+24
-6
@@ -2859,7 +2859,7 @@ func _smoke_skill_pool() -> void:
|
||||
|
||||
# ① 数据面:技能表行集 + 普攻锁死 + 建议池行值 + 空池不可能(结构断言)
|
||||
func _smoke_skill_pool_data() -> void:
|
||||
check(_db.skill.rows.size() == 9, "技能表九行(普攻/突刺/蓄力/重击/硬化 + 撕咬/毒爆/蛛网束缚 + 大地脉动——分支节点图批 + 新效应词汇批 + 第三关内容批)")
|
||||
check(_db.skill.rows.size() == 12, "技能表十二行(普攻/突刺/蓄力/重击/硬化 + 撕咬/毒爆/蛛网束缚 + 大地脉动 + 震地怒击/遏毒/岩崩——前三批 + Boss 半血解锁技三行〔迭代14④ T5〕)")
|
||||
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)),
|
||||
@@ -2903,21 +2903,21 @@ func _smoke_skill_pool_data() -> void:
|
||||
"1_1": {"attack": 9, "lunge": 1},
|
||||
"1_2": {"attack": 6, "charge": 2, "heavy": 2},
|
||||
"1_3": {"attack": 6, "charge": 2, "heavy": 2},
|
||||
"1_4": {"attack": 6, "heavy": 2, "harden": 2},
|
||||
"1_4": {"attack": 6, "heavy": 2, "harden": 2, "quake_smash": 2},
|
||||
"2_0": {"attack": 6, "lunge": 1, "heavy": 2},
|
||||
"2_1": {"attack": 6, "lunge": 1, "heavy": 2},
|
||||
"2_4": {"attack": 6, "bite": 2, "charge": 1},
|
||||
"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_9": {"attack": 6, "heavy": 2, "venom_burst": 2, "web_snare": 2},
|
||||
"2_9": {"attack": 6, "heavy": 2, "venom_burst": 2, "web_snare": 2, "venom_curb": 2},
|
||||
"3_0": {"attack": 6, "charge": 2, "heavy": 2},
|
||||
"3_1": {"attack": 6, "bite": 2, "charge": 1},
|
||||
"3_4": {"attack": 6, "bite": 2, "charge": 1},
|
||||
"3_5": {"attack": 5, "lunge": 2, "heavy": 2},
|
||||
"3_6": {"attack": 6, "bite": 2, "charge": 1},
|
||||
"3_7": {"attack": 6, "charge": 2, "heavy": 2},
|
||||
"3_9": {"attack": 6, "heavy": 2, "earth_pulse": 2},
|
||||
"3_9": {"attack": 6, "heavy": 2, "earth_pulse": 2, "rockslide": 2},
|
||||
}
|
||||
var pools_ok := true
|
||||
for row in _db.enemy.rows:
|
||||
@@ -2930,7 +2930,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/2〔(2,9)——商店房批 Boss 重编号〕;L3 灰矮人 6/2/2 · 钩爪恐魔 6/2/1 · 重装手 5/2/2 · 石君王 6/2/2〔earth_pulse·(3,9)〕——全部 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〔(2,9)——商店房批 Boss 重编号〕;L3 灰矮人 6/2/2 · 钩爪恐魔 6/2/1 · 重装手 5/2/2 · 石君王 6/2/2〔earth_pulse·(3,9)〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2+unlock_hp_pct 50——迭代14④ T5〕)")
|
||||
var basic_count_ok := true
|
||||
for row in _db.enemy.rows:
|
||||
var n := 0
|
||||
@@ -2941,6 +2941,24 @@ func _smoke_skill_pool_data() -> void:
|
||||
if n != 1:
|
||||
basic_count_ok = false
|
||||
check(basic_count_ok, "每池恰一行普攻(空池不可能的加载校验同款)")
|
||||
# Boss 半血解锁技·编排集成断言(迭代14④ T6——真实编排回路:半血 → 下轮开轮闩检查 → 解锁行恰一次)
|
||||
var st_unlock: Dictionary = {"rows": []}
|
||||
var sett_u = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(4242))
|
||||
sett_u.step_row.connect(func(row): st_unlock["rows"].append(row))
|
||||
check(sett_u.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp)}), "前置:卓尔 Boss 战受理(2,9——解锁技 venom_curb 门槛 50)")
|
||||
var boss_u = sett_u._enemy_system.all_enemies()[0] # 白盒夹具(smoke 惯例——autoplay 同族私有面)
|
||||
sett_u._enemy_system.apply_damage(boss_u, (int(boss_u.max_hp) - 65))
|
||||
check(int(boss_u.hp) == 65, "前置:Boss 压到阈值 65(=round(130×0.5))")
|
||||
sett_u.minesweeper.settle_batch([{"action": "stop"}]) # 收轮 → 结算 → 次轮开轮闩检查
|
||||
var unlock_rows: Array = []
|
||||
for r in st_unlock["rows"]:
|
||||
if String(r.get("highlight", "")) == "skill_unlock":
|
||||
unlock_rows.append(r)
|
||||
check(unlock_rows.size() == 1, "解锁归因行恰一次(×%d——闩去重,二次检查零行)" % unlock_rows.size())
|
||||
if unlock_rows.size() == 1:
|
||||
check(String(unlock_rows[0].get("display_name", "")) == "蛛化卓尔精灵 解锁了 遏毒",
|
||||
"解锁行文本 =「蛛化卓尔精灵 解锁了 遏毒」(高光键 skill_unlock——呈现层一次性技能名 toast 路由键)")
|
||||
check(String(unlock_rows[0].get("type", "")) == "highlight", "解锁行型 = highlight 既有行型(零行型新增——行协议封闭)")
|
||||
var rollable_ok := true
|
||||
for depth in [0, 1, 2, 3, 4]:
|
||||
var es = load("res://core/unit_container.gd").new(_db)
|
||||
@@ -3011,7 +3029,7 @@ func _record_pool_trajectories(seed_val: int, rounds: int) -> Dictionary:
|
||||
# 带宽口径:熊地精/酋长 EV 结构性恒 1(普攻 1 + 重击 2 与蓄力/硬化 0 加权守恒)→ 窄带;
|
||||
# 地精突刺池受保底闸抬升(唯一特殊技 w=1/10,保底 M=4 把突刺率从自然 10% 抬到 ~20%+)→ 宽带登记。
|
||||
func _smoke_skill_pool_ev_anchor() -> void:
|
||||
var bands := {"goblin": [1.00, 1.20], "bugbear": [0.97, 1.03], "ogre_chief": [0.97, 1.03]}
|
||||
var bands := {"goblin": [1.00, 1.20], "bugbear": [0.97, 1.03], "ogre_chief": [1.10, 1.30]} # 酋长重锚〔迭代14④ T5——震地怒击半血解锁入池,含闸长期速率上移:实测 1.1948;EV 锚批注 enemy.tres 同批换代〕
|
||||
var species := ["goblin", "bugbear", "ogre_chief"]
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(20260921)
|
||||
|
||||
@@ -391,8 +391,32 @@ func _ready() -> void:
|
||||
if int(bv.cells_probe()[Vector2i(0, 0)].fog_state) == 0 \
|
||||
and absi(0 - hero15.x) + absi(0 - hero15.y) > int(bv.FOG_RADIUS):
|
||||
_problems.append("case⑮ 迷雾复开后远角未复原遮蔽(fog_state=0)")
|
||||
# 提灯 +2 接线断言(迭代14⑤——set_fog_lantern(true) → 半径 8+2:dist==R+1 格由雾态转可见,
|
||||
# 关闭复原 lantern=0;纯接线零逻辑——半径算式迷雾批已在位)
|
||||
var ring_cell := Vector2i(-99, -99)
|
||||
for y in b15.height:
|
||||
for x in b15.width:
|
||||
if absi(x - hero15.x) + absi(y - hero15.y) == int(bv.FOG_RADIUS) + 1:
|
||||
ring_cell = Vector2i(x, y)
|
||||
break
|
||||
if ring_cell.x != -99:
|
||||
break
|
||||
if ring_cell.x == -99:
|
||||
_problems.append("case⑮ 板面无 dist==R+1 格(提灯断言取数异常)")
|
||||
else:
|
||||
var pre_lantern: int = int(bv.cells_probe()[ring_cell].fog_state)
|
||||
bv.set_fog_lantern(true)
|
||||
if int(bv.fog_params_probe()["lantern"]) != 2:
|
||||
_problems.append("case⑮ 提灯接雾参数未变(lantern 应=2)")
|
||||
if int(bv.cells_probe()[ring_cell].fog_state) != 0:
|
||||
_problems.append("case⑮ 提灯 +2 后 dist==R+1 格仍被遮(应转可见 fog_state=0,改前=%d)" % pre_lantern)
|
||||
bv.set_fog_lantern(false)
|
||||
if int(bv.fog_params_probe()["lantern"]) != 0:
|
||||
_problems.append("case⑮ 提灯关闭后雾参数未复原(lantern 应=0)")
|
||||
if int(bv.cells_probe()[ring_cell].fog_state) == 0:
|
||||
_problems.append("case⑮ 提灯关闭后 dist==R+1 格未复原遮蔽")
|
||||
if _problems.is_empty():
|
||||
print("[ui_input_probe] PASS — 悬停金框/禁入信号/路径预览/flag_place/chord/dig/英雄落位/影子构成/朝向/缩格重排/缺位回退/变异角标/视野迷雾 十六断言全绿(真实 push_input 路径——⑫图上命中⑬画布拟合 2026-09-28 增,⑭变异角标 2026-09-29 增,④b 旗纹理槽回归 2026-10-02 增,⑮视野迷雾 2026-10-03 增)")
|
||||
print("[ui_input_probe] PASS — 悬停金框/禁入信号/路径预览/flag_place/chord/dig/英雄落位/影子构成/朝向/缩格重排/缺位回退/变异角标/视野迷雾 十六断言全绿(真实 push_input 路径——⑫图上命中⑬画布拟合 2026-09-28 增,⑭变异角标 2026-09-29 增,④b 旗纹理槽回归 2026-10-02 增,⑮视野迷雾 2026-10-03 增〔+提灯接线断言 2026-10-05 迭代14⑤〕)")
|
||||
get_tree().quit(0)
|
||||
else:
|
||||
for p in _problems:
|
||||
|
||||
@@ -38,6 +38,7 @@ func _suites() -> Array:
|
||||
{"path": "res://tests/unit/presentation/reward_overlay_test.gd", "expect": 0}, # 迭代 7 hotfix——奖励节点 UI 直验(盲区补漏)
|
||||
{"path": "res://tests/unit/battle_settlement/damage_math_test.gd", "expect": 0},
|
||||
{"path": "res://tests/unit/battle_settlement/damage_controller_test.gd", "expect": 0},
|
||||
{"path": "res://tests/unit/battle_settlement/skill_unlock_test.gd", "expect": 0}, # 迭代14④ T6——Boss 半血解锁技·容器机制(门谓词/闩不可逆/双面过滤/保底不逼锁定/零扰动)
|
||||
{"path": "res://tests/unit/contracts/system_contracts_test.gd", "expect": 0},
|
||||
{"path": "res://tests/unit/presentation/ui_font_asset_test.gd", "expect": 0},
|
||||
{"path": "res://tests/unit/presentation/terrain_tile_sets_test.gd", "expect": 0},
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
# tests/unit/battle_settlement/skill_unlock_test.gd — Boss 半血解锁技·单位容器机制单元测试
|
||||
# 运行:godot --headless --path game --script res://tests/unit/battle_settlement/skill_unlock_test.gd
|
||||
# 覆盖(迭代14④ 技能池执行批 T6):门谓词(hp ≤ round(max_hp×pct/100))/闩不可逆(治疗抬血不解锁)/
|
||||
# 双面过滤(rollable+special 不入锁定)/保底不逼锁定(保底集合排除锁定)/解锁即可摇(冷却初值 0)/
|
||||
# 普通行无字段逐位零扰动(goblin 池快照零 unlock 键、过滤恒空转)/同种子确定性(容器零随机)。
|
||||
# 红线:期望值由表读值推导(零玩法数值字面量);零自建 RNG。
|
||||
extends SceneTree
|
||||
|
||||
const ConfigDBScript := preload("res://core/config_db.gd")
|
||||
const UnitContainerScript := preload("res://core/unit_container.gd")
|
||||
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var db = ConfigDBScript.new()
|
||||
var load_ok: bool = db.load_tables("res://data/tables", true)
|
||||
Kit.check("前置:表加载零 error", load_ok and db.diagnostics_count("error") == 0)
|
||||
if load_ok:
|
||||
_test_gate_predicate(db)
|
||||
_test_latch_irreversible(db)
|
||||
_test_plain_rows_zero_drift(db)
|
||||
quit(Kit.finish("skill_unlock_test"))
|
||||
|
||||
|
||||
# 门谓词:卓尔 (2,9) hp 130(×mult 1.0)→ venom_curb unlock 50 → 阈值 round(130×0.5)=65
|
||||
func _boss(db, L: int, D: int):
|
||||
var ec = UnitContainerScript.new(db)
|
||||
var ok: bool = ec.spawn(L, D, int(db.hero.max_hp))
|
||||
Kit.check("前置:spawn (%d,%d) 受理" % [L, D], ok)
|
||||
return ec
|
||||
|
||||
|
||||
func _boss_u(ec) -> Dictionary:
|
||||
return ec.all_enemies()[0]
|
||||
|
||||
|
||||
func _has_sid(arr: Array, sid: String) -> bool:
|
||||
for e in arr:
|
||||
if String(e.skill_id) == sid:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _test_gate_predicate(db) -> void:
|
||||
var ec = _boss(db, 2, 9)
|
||||
var u: Dictionary = _boss_u(ec)
|
||||
var mhp: int = int(u.max_hp)
|
||||
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("双面过滤:未解锁 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)
|
||||
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"))
|
||||
Kit.check("解锁即可摇:venom_curb 入保底特殊技集合", _has_sid(ec.special_skills(u), "venom_curb"))
|
||||
# 保底不逼锁定:锁定期保底集合 = web_snare 唯一(venom_burst 被过滤——保底不得逼出未解锁技)
|
||||
var ec2 = _boss(db, 2, 9)
|
||||
var u2: Dictionary = _boss_u(ec2)
|
||||
ec2.apply_damage(u2, mhp - 66)
|
||||
var specials: Array = ec2.special_skills(u2)
|
||||
# 特殊技集合语义 = 全部非普攻池条目(含伤害技 heavy——保底「必出特殊技」口径;proposal §2.1)
|
||||
Kit.check("保底不逼锁定:锁定期保底集合 = heavy+venom_burst+web_snare(venom_curb 过滤不出)", specials.size() == 3 and _has_sid(specials, "heavy") and _has_sid(specials, "web_snare") and _has_sid(specials, "venom_burst") and not _has_sid(specials, "venom_curb"))
|
||||
|
||||
|
||||
# 闩不可逆:解锁后治疗抬回满血 → 复查零新解锁、可摇集合不回退
|
||||
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)
|
||||
ec.heal(u, int(u.max_hp)) # 抬回满血
|
||||
Kit.check("闩不可逆:满血复查零新解锁(单向闩不解锁回退)", ec.check_skill_unlocks(u).is_empty())
|
||||
Kit.check("闩不可逆:可摇集合仍含 venom_curb", _has_sid(ec.rollable_skills(u), "venom_curb"))
|
||||
|
||||
|
||||
# 普通行无字段逐位零扰动:地精 (1,0) 池快照零 unlock 键、过滤恒空转、可摇=全池
|
||||
func _test_plain_rows_zero_drift(db) -> void:
|
||||
var ec = _boss(db, 1, 0)
|
||||
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"):
|
||||
any_unlock_key = true
|
||||
Kit.check("零扰动:无字段行池快照零 unlock 键(16/19 行逐位同前)", not any_unlock_key)
|
||||
Kit.check("零扰动:满血复查零解锁", ec.check_skill_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)
|
||||
# 同种子确定性:容器零随机——两实例同输入查询逐位同
|
||||
var ec2 = _boss(db, 1, 0)
|
||||
var u2: Dictionary = _boss_u(ec2)
|
||||
ec.apply_damage(u, 3)
|
||||
ec2.apply_damage(u2, 3)
|
||||
var a := JSON.stringify(ec.rollable_skills(u))
|
||||
var b := JSON.stringify(ec2.rollable_skills(u2))
|
||||
Kit.check("确定性:两实例同输入可摇集合逐位同(容器零随机纪律)", a == b)
|
||||
Reference in new issue
Block a user