merge: master(04edd55 硬锁v3.1/probe适配四件/出度≤2新模板/血量pct修复)并入修复批——解冲突3处(ui_click_probe三态分派取master侧+ProbeUiUtil助手统一/main.gd锁期点击缓存重放保留+排播变量以RowPacer委托面承接/row_pacer新增pump_ended注入回调承接v3.1演播完边沿重放)+修复解冲突引入的解析错误(三态分派链缩进);闸=冲烟636/0+run_all 15套+headless五探针绿+窗口化三件绿+熵增PASS;ui_click_probe budget耗尽与typed-array错误经纯master对照=1:1同源(master既有,归T17在飞批)
This commit is contained in:
commit
cd1ed95b55
14 files changed
+372
-32
No files matched your search
@@ -60,6 +60,8 @@
|
||||
|
||||
抽签消费主种子派生的统一随机流,走扩签通道新立用途标签 `node_map`(统一流口径已定——[ADR-0007](../../../dev/architecture/adr-0007-rng-single-stream.md) 裁丙落地;标签不参与派生、只进事件日志)。
|
||||
|
||||
**出度上限与层宽(2026-10-01 用户拍板 T17 目检)**:单节点出边数(出度)**最大为 2**——加载校验第七检(`fanout_violation` 加载致命)。规则连带(数学约束):出度 ≤2 + 入口唯一下,BFS 层宽上限 = **4**(层宽序列上限 1,2,4;2026-09-29 旧规则「层宽上限 5」与出度约束不相容,随本拍板废止)。现行池 = `tools/t17_fanout2_rebuild.py` 按两规则重建(五骨架:宽廊双干〔现行〕/三干中廊/宽廊斜岔/三干宽门/窄廊短支——层宽序列 1-2-4-1-1 / 1-2-3-2-1 / 1-2-2-2-1,最大层宽 4 应用足)。
|
||||
|
||||
**内容纪律**:每张新模板等于一个小内容批——模板里每个战斗与 Boss 节点都必须在敌人与棋盘生成的数值表里有精确行,义务从冒险结构加关纪律的"每关一行集"细化到"每模板一行集";冒烟覆盖义务同批扩为遍历池内全部模板校验,不只抽中的那张。
|
||||
|
||||
### 抽签时机与可复现
|
||||
@@ -219,4 +221,5 @@
|
||||
| v0.2 | 2026-09-27 | **首轮裁决落文批(用户拍板①④⑤+路线确认)**:①第一关参与抽签、仅首次冒险走固定脚本——新增「首次冒险的固定脚本」节(存档域布尔标志「首次冒险已进行」、收束点置位、边界与测量口径,P0/验收/依赖表同步);④首版池规模=每关 5 张(调参表投值);⑤抽签时点确认;用户并裁「骨架不完全固定、多份随机调取」= 台阶一路线确认(台阶节补白话块+教学位随骨架走注记)。②③补证据面待复裁——新增「拍板点证据面」节:A=随机流/增流白话释义+两选项;B=台阶一详解(玩家看到什么/做三件/不做两件/内容形态 15 骨架/遭遇表按编号共用约束)+实施时点三选项(A 立即全上/B 等调参批/C 机制先行骨架随后,荐 C);测量对账实施门条款同步改"随机量扩池守门、机制可先行"口径 |
|
||||
| v0.3 | 2026-09-27 | **②③终裁落文批(用户两轮复裁)**:②统一用一个流·全面执行——全游戏 rng 架构批先行([ADR-0007](../../../dev/architecture/adr-0007-rng-single-stream.md) 评估书交付:核心发现=七流系同一主种子的盐值分栏、字面目标现行已满足;待裁决=甲维持分栏/乙物理并流/丙并流+全事件日志),本特性实施排队其后(P0 补前置行、抽签节流形态改随 ADR 结论);③特性内机制先行骨架随后(池暂 N=1、骨架内容批扩池);证据面 A 补裁决注记。06 新立「全游戏随机流统一架构批」条目(数据配置节) |
|
||||
| v0.4 | 2026-09-27 | **实施前置解除批(统一随机流批随批——rng 架构批已按 ADR-0007 裁丙落地)**:状态行改实施解锁;抽签节流形态定稿 = 统一流扩签新用途标签 `node_map`(标签不参与派生、只进事件日志——并流后派生隔离概念退役);P0 闭环前置行改解除口径。本批只记架构依赖解除,特性实施仍未开工 |
|
||||
| v0.6 | 2026-10-01 | **出度上限拍板批(T17 目检用户裁决「节点出度最大为 2」)**:① 加载校验第七检 `fanout_violation`(出度 >2 加载致命);② 规则连带——层宽上限随出度约束调整为 4(2026-09-29「层宽上限 5」废止,数学不相容);③ L2/L3 十张模板全池重建(tools/t17_fanout2_rebuild.py——出度 ≤2/层宽 ≤4/类型序列保持/可达自校验,五骨架换名宽廊双干〔现行〕等);④ 钉固断言 5 处随新图更新(min/max 路径/入口候选/奖励后候选)。**连带义务:MC 读数基线随图结构变化漂移——全旧 MC 锚作废,重锚归下一测量批**(当前只测不判) |
|
||||
| v0.5 | 2026-09-28 | **台阶一实施落地批(M-3 迭代 5 T5)**:P0 六项全落地——①关卡表逐关行 templates 池(3 关 × 5,模板 0 = 现行图回滚档;L1 单链排列变体〔图形态 Boss 最大编号 vs 遭遇深度 4 两不可全〕/L2L3 网状变体〔战斗编号恒 ∈ 遭遇表行集、商店必经位承袭〕)②node_map 扩签(标签不参与派生只进事件日志,黄金向量零改动)③首次冒险固定脚本(存档唯一新增布尔位「首次冒险已进行」、老档迁移=true、收束点置位幂等、测量夹具恒抽签)④池内全模板加载校验(validate 抽出 + config_db 跨表校验模板感知)⑤抽签确定性/零 scope 强制/N=1 等价/首冒分支 冒烟断言(558→576)⑥MC 分层出数(RUNSUM level_templates)。内容批生成器 = tools/t5_build_templates.py(生成前自校验)。遗留:台阶二/三后置不变;正式侧生存链/带面影响随读数批 |
|
||||
@@ -188,3 +188,4 @@ Boss 兑付(胜利或英雄死亡)触发关卡收束点,此后不再有选
|
||||
| v1.0.1 | 2026-09-17 | **P1 分支节点图内容与实现批落地**(用户拍板任务批——切片代码 / 难度曲线与Boss v0.7 / registry / 06 待办同批):首张分支图 = 切片关卡 2「第二层 · 幽暗地域」行集落地(9 节点网状图:入口 0 → 中间三层 2/3/2 → Boss 8,真实岔路 4 处,奖励点 + 恢复点各 1,战斗节点 6 + Boss);行进器图版推进(`choices_presented` 选路态 + `pick_next_node`——零自动推进,战斗胜利不再自动推进、`continue_after_node` 泛化为兑付确认,06 接口漂移清单 ④⑤⑧ 清偿);图合法性校验落地 `LevelTable.validate`(单链与图同一套算法,L1 零迁移过校验,违规加载致命);节点图选路 UI(选路覆盖层整图自绘——房间方块 + 通道连线 + 当前/候选高亮,占位图形质量;顶栏图形态渲染行进序列 + 候选);首页"关卡 2 试玩"直入口(测试模式,≠ 两关序列生效)。本版本号只记实现批状态,图规则口径零改动 |
|
||||
| v1.1 | 2026-09-20 | **商店房扩类(GP+商店房批,用户拍板——与冒险结构 / 战斗结算 / 地图战斗资源 / 难度曲线与Boss / registry / 切片同批)**:① 节点封闭枚举四类→五类(+商店房);② 编排规则定稿——每关恰 1、关底 Boss 前一位必经位(哈迪斯式"决战前最后一个补给点"定位);必经位由冒烟断言承载、图合法性六检零新增;③ 兑付定稿——打开商店覆盖层、购买即时生效写统一冒险加算区(与关卡大奖励同账,归冒险结构)、离开后兑付确认;商店房不携带战斗行;④ 节点兑付表 / 纪律表 / 依赖表 / P0 闭环 / 验收标准 / P2「事件类节点候选」注记同步 |
|
||||
| v1.2 | 2026-09-23 | **停点前移批(冒险入口流程倒置修复,用户 2026-09-23 拍板;上位裁决 `../../../dev/architecture/adr-0006-board-node2d-rendering.md` 与 `../../../dev/architecture/board-node2d-migration-plan.md` §1,代码实装另批并行)**:① 行进循环改停点前移口径——关卡开始即呈现节点图选路停点(关卡画面 = 选路覆盖层常驻化,入口节点不自动进入,第一次选路就是"出发"),选路确认进节点,兑付确认后返回关卡画面再选路(战后回图);兑付后确认、零自动推进两条纪律语义不变,只改承载位;② 呈现侧口径补进度承载条款——关卡画面本体即节点链进度呈现载体(节点态直接读图:已过暗金 / 当前金 / 候选下一跳通道亮金 / Boss 旗朱描边),不另设进度条组件(战斗屏 A 区进度条随批退役);③ P0 闭环两条、依赖表 HUD 框架行、验收标准「行进自主」条同步。**批内代拍板三项已追认(2026-09-24 用户确认)**:① B/C/D 字母保持不重编;② 28px 纵向空间划归 C/D 底行;③ 关卡画面进度呈现由选路覆盖层节点态承载(登记面见 HUD框架 v0.8) |
|
||||
| v1.3 | 2026-10-01 | **图合法性第七检(T17 目检用户拍板「节点出度最大为 2」)**:`validate` 新增 `fanout_violation`(单节点出边 >2 加载致命);规则连带层宽上限 4(原 5 与出度约束不相容,废止)——生成侧规则与模板池重建归[关卡节点图生成](关卡节点图生成.md) v0.6 同批 |
|
||||
@@ -113,7 +113,7 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
|
||||
_base_override = _normalize_override(params.get("hero_override", {}))
|
||||
_first_script = bool(params.get("first_script", false)) # 缺省 = 非首次(抽签路径;真实侧由呈现层读存档布尔位传入)
|
||||
_aux_unlocked = params.get("aux_unlocked", {}) if params.get("aux_unlocked", {}) is Dictionary else {} # additive(迭代 7 T5)
|
||||
ledger["hero_hp"] = int(db.hero.max_hp) + int(_base_override["max_hp"]) # 生命账本置满(表值+永久强化 flat——加算区刚归零)
|
||||
ledger["hero_hp"] = effective_max_hp() # 生命账本置满(生效上限口径——T17 修复:含 TunePanel pct,与战斗内同源)
|
||||
state = "running"
|
||||
return _start_current_level()
|
||||
|
||||
@@ -211,6 +211,15 @@ func set_hero_override(atk_pct: int, hp_pct: int) -> void:
|
||||
_runner.set_hero_override(atk_pct, hp_pct)
|
||||
|
||||
|
||||
# 冒险账本侧生效上限(T17 修复——账本置满/回血封顶与战斗内上限同式同源):
|
||||
# (表值 + 永久强化 flat + 加算区 flat)后乘 TunePanel 百分比——叠加次序与 LevelRunner.effective_max_hp
|
||||
# 逐字一致。旧口径两处只算 flat 不乘 pct:调生命上限 26→104 时开局血仍 26(账本 26 / 战斗内 104)、
|
||||
# 通关 20% 回血封顶按 26 算。pct=0(模拟/冒烟默认)时乘法恒等——MC 锚零漂移。
|
||||
func effective_max_hp() -> int:
|
||||
var base: int = int(db.hero.max_hp) + int(_base_override.get("max_hp", 0)) + int(bonus.get("max_hp", 0))
|
||||
return maxi(1, int(round(float(base) * (100.0 + float(_override[1])) / 100.0)))
|
||||
|
||||
|
||||
# 断点承载注入(迭代 8 卡①——组合根冒险开始时调用;模拟/冒烟不注入 = 断点面整体静默)
|
||||
func set_save_manager(sm) -> void:
|
||||
_save_manager = sm
|
||||
@@ -614,7 +623,7 @@ func _on_level_finished(payload: Dictionary) -> void:
|
||||
# 通关兑付(非末关恰一次;迭代 10 三层奖励框架):部分回血先行 → 加权洗牌掷 3 → 三选一停点
|
||||
#(顺序即契约——回血按当刻账本与上限结算,选择停点零自动推进)
|
||||
var hp_before := int(ledger.get("hero_hp", 0))
|
||||
var max_eff := int(db.hero.max_hp) + int(_base_override["max_hp"]) + int(bonus.get("max_hp", 0))
|
||||
var max_eff := effective_max_hp() # T17 修复:回血封顶口径含 TunePanel pct(同战斗内上限)
|
||||
var heal := mini(int(floor(float(max_eff) * float(int(db.adventure.clear_heal_pct)) / 100.0)), max_eff - hp_before)
|
||||
ledger["hero_hp"] = hp_before + heal
|
||||
# 置位先于 emit(信号同步派发——处理器内立即确认时 _pending_next_level 必须已就位)
|
||||
|
||||
@@ -138,6 +138,12 @@ func _validate_graph(L: int, nodes: Array, is_graph: bool, tag: String = "") ->
|
||||
errs.append({"category": "dangling_next", "message": "level %d%s 节点 %d 出边指向不存在编号: %d" % [L, tag, ids[i], tid]})
|
||||
elif tid <= ids[i]:
|
||||
errs.append({"category": "structural", "message": "level %d%s 连线 %d→%d 违反前进方向(目标编号须严格更大)" % [L, tag, ids[i], tid]})
|
||||
# ③b 出度上限(T17 用户拍板 2026-10-01「节点出度最大为 2」):单节点出边 >2 即加载致命。
|
||||
# 规则连带:出度 ≤2 + 入口唯一下 BFS 层宽上限 = 4(序列 1,2,4——原「层宽上限 5」与出度约束
|
||||
# 不相容,模板池随本检同批按层宽 ≤4 重建)。
|
||||
for i in nodes.size():
|
||||
if nexts[i].size() > 2:
|
||||
errs.append({"category": "fanout_violation", "message": "level %d%s 节点 %d 出度 %d > 2(出度上限拍板 2026-10-01)" % [L, tag, ids[i], nexts[i].size()]})
|
||||
# ④ 入口唯一(无入边节点恰一——唯一入口)
|
||||
var has_inbound := {}
|
||||
for i in nodes.size():
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
script = ExtResource("1")
|
||||
rows = [
|
||||
{"level_index": 1, "display_name": "第一层 · 地精洞窟", "rest_heal_pct": 30, "templates": [{"display_name": "地精洞窟·主线洞道(现行)", "nodes": [{"node_depth": 0, "type": "battle"}, {"node_depth": 1, "type": "battle"}, {"node_depth": 2, "type": "battle"}, {"node_depth": 3, "type": "battle"}, {"type": "reward"}, {"type": "rest"}, {"node_depth": 4, "type": "boss"}]}, {"display_name": "地精洞窟·先赏后整", "nodes": [{"node_depth": 0, "type": "battle"}, {"node_depth": 1, "type": "battle"}, {"node_depth": 2, "type": "battle"}, {"type": "reward"}, {"type": "rest"}, {"node_depth": 3, "type": "battle"}, {"node_depth": 4, "type": "boss"}]}, {"display_name": "地精洞窟·先整后赏", "nodes": [{"node_depth": 0, "type": "battle"}, {"node_depth": 1, "type": "battle"}, {"node_depth": 2, "type": "battle"}, {"type": "rest"}, {"type": "reward"}, {"node_depth": 3, "type": "battle"}, {"node_depth": 4, "type": "boss"}]}, {"display_name": "地精洞窟·赏战整", "nodes": [{"node_depth": 0, "type": "battle"}, {"node_depth": 1, "type": "battle"}, {"node_depth": 2, "type": "battle"}, {"type": "reward"}, {"node_depth": 3, "type": "battle"}, {"type": "rest"}, {"node_depth": 4, "type": "boss"}]}, {"display_name": "地精洞窟·整战赏", "nodes": [{"node_depth": 0, "type": "battle"}, {"node_depth": 1, "type": "battle"}, {"node_depth": 2, "type": "battle"}, {"type": "rest"}, {"node_depth": 3, "type": "battle"}, {"type": "reward"}, {"node_depth": 4, "type": "boss"}]}]},
|
||||
{"level_index": 2, "display_name": "第二层 · 幽暗地域", "rest_heal_pct": 30, "templates": [{"display_name": "幽暗地域·五岔环廊(现行)", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2, 3, 4, 5]}, {"node_depth": 1, "type": "battle", "next": [6]}, {"node_depth": 2, "type": "reward", "next": [6, 7]}, {"node_depth": 3, "type": "rest", "next": [6]}, {"node_depth": 4, "type": "battle", "next": [7]}, {"node_depth": 5, "type": "battle", "next": [6, 7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "幽暗地域·四岔宽门", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2, 3, 4]}, {"node_depth": 1, "type": "battle", "next": [5]}, {"node_depth": 2, "type": "reward", "next": [5, 6]}, {"node_depth": 3, "type": "rest", "next": [6, 7]}, {"node_depth": 4, "type": "battle", "next": [7]}, {"node_depth": 5, "type": "battle", "next": [8]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "幽暗地域·深层四岔", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 4, 5, 6]}, {"node_depth": 2, "type": "reward", "next": [3, 4]}, {"node_depth": 3, "type": "rest", "next": [7]}, {"node_depth": 4, "type": "battle", "next": [7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [7]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "幽暗地域·宽门赏后置", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 3, 4, 5]}, {"node_depth": 1, "type": "battle", "next": [2, 6]}, {"node_depth": 2, "type": "reward", "next": [7]}, {"node_depth": 3, "type": "rest", "next": [6]}, {"node_depth": 4, "type": "battle", "next": [6, 7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "幽暗地域·双干汇流", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [4, 5, 6]}, {"node_depth": 2, "type": "reward", "next": [3, 4, 5]}, {"node_depth": 3, "type": "rest", "next": [6]}, {"node_depth": 4, "type": "battle", "next": [6, 7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}]},
|
||||
{"level_index": 3, "display_name": "第三层 · 灰矮人矿城", "rest_heal_pct": 30, "templates": [{"display_name": "灰矮人矿城·五岔主竖井(现行)", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2, 3, 4, 5]}, {"node_depth": 1, "type": "battle", "next": [6]}, {"node_depth": 2, "type": "reward", "next": [6, 7]}, {"node_depth": 3, "type": "rest", "next": [6]}, {"node_depth": 4, "type": "battle", "next": [7]}, {"node_depth": 5, "type": "battle", "next": [6, 7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "灰矮人矿城·四岔宽门", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2, 3, 4]}, {"node_depth": 1, "type": "battle", "next": [5]}, {"node_depth": 2, "type": "reward", "next": [5, 6]}, {"node_depth": 3, "type": "rest", "next": [6, 7]}, {"node_depth": 4, "type": "battle", "next": [7]}, {"node_depth": 5, "type": "battle", "next": [8]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "灰矮人矿城·深层四岔", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 4, 5, 6]}, {"node_depth": 2, "type": "reward", "next": [3, 4]}, {"node_depth": 3, "type": "rest", "next": [7]}, {"node_depth": 4, "type": "battle", "next": [7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [7]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "灰矮人矿城·宽门赏后置", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 3, 4, 5]}, {"node_depth": 1, "type": "battle", "next": [2, 6]}, {"node_depth": 2, "type": "reward", "next": [7]}, {"node_depth": 3, "type": "rest", "next": [6]}, {"node_depth": 4, "type": "battle", "next": [6, 7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "灰矮人矿城·双干汇流", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [4, 5, 6]}, {"node_depth": 2, "type": "reward", "next": [3, 4, 5]}, {"node_depth": 3, "type": "rest", "next": [6]}, {"node_depth": 4, "type": "battle", "next": [6, 7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}]}
|
||||
{"level_index": 2, "display_name": "第二层 · 幽暗地域", "rest_heal_pct": 30, "templates": [{"display_name": "宽廊双干(现行)", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 4]}, {"node_depth": 2, "type": "reward", "next": [5, 6]}, {"node_depth": 3, "type": "rest", "next": [7]}, {"node_depth": 4, "type": "battle", "next": [7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [7]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "三干中廊", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 5]}, {"node_depth": 2, "type": "reward", "next": [4, 5]}, {"node_depth": 3, "type": "rest", "next": [6]}, {"node_depth": 4, "type": "battle", "next": [6, 7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "宽廊斜岔", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 5]}, {"node_depth": 2, "type": "reward", "next": [4, 6]}, {"node_depth": 3, "type": "rest", "next": [7]}, {"node_depth": 4, "type": "battle", "next": [7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [7]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "三干宽门", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 4]}, {"node_depth": 2, "type": "reward", "next": [4, 5]}, {"node_depth": 3, "type": "rest", "next": [6, 7]}, {"node_depth": 4, "type": "battle", "next": [7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "窄廊短支", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 4]}, {"node_depth": 2, "type": "reward", "next": [5]}, {"node_depth": 3, "type": "rest", "next": [5, 6]}, {"node_depth": 4, "type": "battle", "next": [6]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [7]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}]},
|
||||
{"level_index": 3, "display_name": "第三层 · 灰矮人矿城", "rest_heal_pct": 30, "templates": [{"display_name": "宽廊双干(现行)", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 4]}, {"node_depth": 2, "type": "reward", "next": [5, 6]}, {"node_depth": 3, "type": "rest", "next": [7]}, {"node_depth": 4, "type": "battle", "next": [7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [7]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "三干中廊", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 5]}, {"node_depth": 2, "type": "reward", "next": [4, 5]}, {"node_depth": 3, "type": "rest", "next": [6]}, {"node_depth": 4, "type": "battle", "next": [6, 7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "宽廊斜岔", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 5]}, {"node_depth": 2, "type": "reward", "next": [4, 6]}, {"node_depth": 3, "type": "rest", "next": [7]}, {"node_depth": 4, "type": "battle", "next": [7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [7]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "三干宽门", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 4]}, {"node_depth": 2, "type": "reward", "next": [4, 5]}, {"node_depth": 3, "type": "rest", "next": [6, 7]}, {"node_depth": 4, "type": "battle", "next": [7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "窄廊短支", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 4]}, {"node_depth": 2, "type": "reward", "next": [5]}, {"node_depth": 3, "type": "rest", "next": [5, 6]}, {"node_depth": 4, "type": "battle", "next": [6]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [7]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}]}
|
||||
]
|
||||
|
||||
; 关卡节点图生成 v0.4 台阶一内容批(M-3 迭代 5 T5,2026-09-28——15 张骨架 = 3 关 × 5,拍板点④):
|
||||
|
||||
@@ -117,6 +117,19 @@ var _refresh_used_round := 0 # 本轮已受理刷新次数(遥测 round_r
|
||||
var _boss_active := false # Boss 遭遇变体(阶段指示附 Boss 名)
|
||||
var _was_settling := false # 结算相位上升沿检测(结算轮光带触发——演出链批)
|
||||
|
||||
var _locked_click := {} # 锁期点击缓存(T17 v3.1——最后一次;演播完边沿重放,玩家操作零丢失)
|
||||
|
||||
|
||||
# 锁期点击重放(v3.1):延迟一帧重入唯一动作通道——此刻队列空锁已解,按普通动作受理
|
||||
#(局面在演播期无变化:逻辑先行、演播只是回放——点击语义保真;_row_queue=RowPacer 委托面,
|
||||
# 排播泵本体已迁 row_pacer.gd——T3 结构段)。
|
||||
func _replay_locked_click() -> void:
|
||||
if _locked_click.is_empty() or not _row_queue.is_empty():
|
||||
return
|
||||
var click: Dictionary = _locked_click
|
||||
_locked_click = {}
|
||||
_on_board_action(click["cell"], click["kind"])
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
db = get_node("/root/ConfigDB")
|
||||
@@ -167,6 +180,7 @@ func _build_ui() -> void:
|
||||
_row_pacer.settlement_fx = settlement_fx
|
||||
_row_pacer.board_view = board_view
|
||||
_row_pacer.refresh_request = _refresh_presentations
|
||||
_row_pacer.pump_ended = _replay_locked_click # T17 v3.1——演播完边沿重放锁期点击(合并批接线)
|
||||
add_child(_row_pacer)
|
||||
# 魔法道具两步交互(T3 拆件批——迁 aux_interact_controller.gd;settlement/runner 随关卡更替,
|
||||
# 经现值回调注入——仿 identity_resolver 只读回调先例)
|
||||
@@ -1038,9 +1052,11 @@ func _on_board_action(cell: Vector2i, kind: String) -> void:
|
||||
# 结算演播期棋盘冻结(T17 用户明确令 v3——不可点/不可移/不可挖):
|
||||
# 锁面=排播队列活跃(纯队列判据;排播泵尾迹窗口不可作演播中标志——见 row_pacer._process 注)。
|
||||
# 队列排空即解锁、无状态残留;HUD 按钮不在锁面(停手/明细回看各自通路,开启前
|
||||
# _flush_row_queue 自行排空)。v1 撞收尾链的教训对策:ui_click_probe 的排播期 stall
|
||||
# 豁免随批(合法停顿态),Waiting 收尾等待期**不豁免**——probe 报卡即为真回归信号。
|
||||
# _flush_row_queue 自行排空)。
|
||||
# **点击不丢弃**(v3.1——用户实机「卡死」根因):锁期点击缓存最后一次,演播完边沿
|
||||
# (_process 泵 _was_pumping 分支)自动重放——玩家操作零丢失、体感=「演播一停动作就生效」。
|
||||
if not _row_queue.is_empty():
|
||||
_locked_click = {"cell": cell, "kind": kind}
|
||||
return
|
||||
# 魔法道具选中态劫持(迭代 7 T4——两步交互第二步):点格=使用目标拾取,不落挖格;
|
||||
# 只劫持挖掘点击(move/flag 语义不冲突——选中期玩家仍可移动/插旗)
|
||||
|
||||
@@ -23,6 +23,7 @@ var board_zone: BoardZoneView
|
||||
var settlement_fx: SettlementFxView
|
||||
var board_view: BoardView
|
||||
var refresh_request := Callable() # () → 组合根 _refresh_presentations(行尾直呼/演播完边沿 call_deferred)
|
||||
var pump_ended := Callable() # () → 组合根锁期点击重放入口(T17 v3.1——演播完边沿 call_deferred;空回调安全)
|
||||
|
||||
|
||||
# 队列只读面(探针/测试经组合根委托属性读——共享同一 Array 引用,读数即实况)
|
||||
@@ -65,6 +66,10 @@ func _process(delta: float) -> void:
|
||||
# 无核心信号驱动——此处主动刷回挖掘态(去饱和解除/明细带收起/mode=dig)。
|
||||
_was_pumping = false
|
||||
refresh_request.call_deferred()
|
||||
# 锁期点击重放(T17 v3.1——玩家操作零丢失):最后一次锁期点击延迟一帧重入
|
||||
# _on_board_action(此刻队列已空=锁已解,正常受理;重放入口由组合根注入)。
|
||||
if pump_ended.is_valid():
|
||||
pump_ended.call_deferred()
|
||||
return
|
||||
_was_pumping = true
|
||||
_pace_wait -= delta
|
||||
|
||||
@@ -393,6 +393,9 @@ func _draw_graph(canvas: Control) -> void:
|
||||
var visited: Array = _runner.visited_ids()
|
||||
var cand_ids: Array = _runner.next_candidate_ids()
|
||||
# 连线(通道)先行画(方块覆盖其上);悬停预承诺 = 所属通道再增亮(加粗)
|
||||
# T17 修复:选路停点(候选非空)非候选通道降透明——金岔路从结构网分离(干线共线重叠缺陷的
|
||||
# 视觉主因:不同源通道共用同一垂直干线,金边与石色边焊接成「整列可走」假象)
|
||||
var dim_passages: bool = not cand_ids.is_empty()
|
||||
for node in nodes:
|
||||
var from: Vector2 = pos[node]
|
||||
for tid in _out_ids(node, nodes):
|
||||
@@ -402,7 +405,7 @@ func _draw_graph(canvas: Control) -> void:
|
||||
var to: Vector2 = pos[target]
|
||||
var lit: bool = _id_of(node) == current_id and cand_ids.has(tid)
|
||||
var hovered: bool = lit and _hover_id == tid
|
||||
var col := COL_PASSAGE_LIT if lit else COL_PASSAGE
|
||||
var col := COL_PASSAGE_LIT if lit else (Color(COL_PASSAGE, 0.35) if dim_passages else COL_PASSAGE)
|
||||
var width := 4.0 if hovered else (3.0 if lit else 2.0) # 样张 v1 冻结:2/3/4(原 1.5 取整)
|
||||
_draw_passage(canvas, from, to, col, width)
|
||||
# 房间方块
|
||||
@@ -511,6 +514,9 @@ func _draw_passage(canvas: Control, from: Vector2, to: Vector2, col: Color, widt
|
||||
var b := to - Vector2(NODE_HALF.x, 0)
|
||||
if b.x < a.x:
|
||||
return
|
||||
var mid_x := (a.x + b.x) / 2.0
|
||||
# T17 修复:垂直干线 x 按源节点 y 偏移——不同源的边不再共线重叠(旧 mid_x=中点使相邻层
|
||||
# 全部通道焊成一条干系,「金岔路+整列可走」误读的直接几何根源)。同源双边仍共线(同出发
|
||||
# 点语义正确),跨源分离度 = 源行距 × 0.2(L2 行距 ~58px → 偏移 ~12px 可辨)。
|
||||
var mid_x := (a.x + b.x) / 2.0 + (a.y - b.y) * 0.2
|
||||
var pts := PackedVector2Array([a, Vector2(mid_x, a.y), Vector2(mid_x, b.y), b])
|
||||
canvas.draw_polyline(pts, col, width)
|
||||
@@ -61,7 +61,8 @@ func _build() -> void:
|
||||
box.add_child(_fs_btn)
|
||||
|
||||
# 减动效(battle.md §Transitions 承诺转正——时长减半,结算逐行兑现=信息载体不整体跳过)
|
||||
_motion_btn = UiKit.button("减动效:关(动画时长减半——运动敏感/演示用)")
|
||||
# 文案由 _sync_motion_text 按状态装配(构造串仅占位——_sync 启动即覆盖)
|
||||
_motion_btn = UiKit.button("结算演出动画")
|
||||
_motion_btn.toggle_mode = true
|
||||
_motion_btn.toggled.connect(_on_motion_toggled)
|
||||
box.add_child(_motion_btn)
|
||||
@@ -79,7 +80,16 @@ func _sync() -> void:
|
||||
_fs_btn.set_pressed_no_signal(_store.fullscreen)
|
||||
_fs_btn.text = "全屏:%s" % ("开" if _store.fullscreen else "关")
|
||||
_motion_btn.set_pressed_no_signal(_store.reduced_motion)
|
||||
_motion_btn.text = "减动效:%s(动画时长减半——运动敏感/演示用)" % ("开" if _store.reduced_motion else "关")
|
||||
_sync_motion_text()
|
||||
|
||||
|
||||
# T17 修复:文案随状态即时刷新(旧 bug=切换后文案恒「开」,玩家无法得知当前态)+语义命名
|
||||
# (「减动效」玩家读不懂——实称「结算演出动画」速度档)+操作提示内联(所见即所得)。
|
||||
func _sync_motion_text() -> void:
|
||||
if _store.reduced_motion:
|
||||
_motion_btn.text = "结算演出动画:快速(时长减半——运动敏感/演示用)· 点击改回完整"
|
||||
else:
|
||||
_motion_btn.text = "结算演出动画:完整 · 点击改为快速(时长减半——运动敏感/演示用)"
|
||||
|
||||
|
||||
func _on_volume_changed(v: float) -> void:
|
||||
@@ -99,6 +109,7 @@ func _on_motion_toggled(on: bool) -> void:
|
||||
_store.reduced_motion = on
|
||||
_store.apply()
|
||||
_store.save_settings()
|
||||
_sync_motion_text() # T17 修复:切换即时刷新文案(旧版漏刷新=状态文字恒「开」)
|
||||
|
||||
|
||||
func _emit_closed() -> void:
|
||||
|
||||
@@ -18,6 +18,11 @@ var _meta := {} # 控件实测几何(全局 rect——shot_checker 文字框
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# 运行前提=窗口化真渲染面:headless 下 frame_post_draw 永不触发+视口纹理恒空——无此守卫时 headless 调用方必挂死(2026-10-02 挂死灾害批护栏)
|
||||
if DisplayServer.get_name() == "headless":
|
||||
print("[shot_probe] FAIL — 本探针必须窗口化运行(headless 无渲染面,截图必空);headless 自动闸禁跑本探针(正主=tools/probe_gate.sh)")
|
||||
get_tree().quit(1)
|
||||
return
|
||||
var args := OS.get_cmdline_user_args()
|
||||
out_dir = args[0] if args.size() >= 1 and args[0] != "" else "user://qa-screens"
|
||||
DirAccess.make_dir_recursive_absolute(out_dir)
|
||||
|
||||
+11
-10
@@ -352,13 +352,14 @@ func _smoke_adventure() -> void:
|
||||
var hp_before: int = int(p.get("hp_before", -1))
|
||||
var hp_after: int = int(p.get("hp_after", -1))
|
||||
# 模拟档 v3.4 商店贪心购血合法抬升(L2/L3 店在 Boss 前)——回血口径窗口 = 加算区件 + 购血余量 0..15
|
||||
# T17 账本口径修复随批:置满/回血封顶改生效上限口径(flat 后乘 TunePanel pct)——窗口同步 ×5(本段 pct=400)
|
||||
var heal_ok_i := false
|
||||
for extra in 16:
|
||||
var m := int(_db.hero.max_hp) + hp_acc + extra
|
||||
var m := int(round(float(int(_db.hero.max_hp) + hp_acc + extra) * 5.0))
|
||||
if heal == mini(int(floor(float(m) * float(heal_pct) / 100.0)), m - hp_before):
|
||||
heal_ok_i = true
|
||||
break
|
||||
if not heal_ok_i or hp_after != hp_before + heal or hp_after > int(_db.hero.max_hp) + hp_acc + 15:
|
||||
if not heal_ok_i or hp_after != hp_before + heal or hp_after > int(round(float(int(_db.hero.max_hp) + hp_acc + 15) * 5.0)):
|
||||
heal_ok = false
|
||||
ks_prefix += int(_db.meta_boss_keystone_row(int(p.get("level_index", -1))).get("first_kill", 0))
|
||||
if int(p.get("keystone_gained", -1)) != int(_db.meta_boss_keystone_row(int(p.get("level_index", -1))).get("first_kill", 0)) or int(p.get("keystone_total", -1)) != ks_prefix:
|
||||
@@ -491,7 +492,7 @@ func _smoke_adventure() -> void:
|
||||
var expect_acc: int = acc_shop + (int(_db.adventure.bonus_max_hp_flat) if picked2 == "max_hp" else 0)
|
||||
check(int(a8.bonus.get("max_hp", -1)) == expect_acc,
|
||||
"三选一属性件与商店购买同账累加(max_hp=%d = %d + 三选一%s)" % [int(a8.bonus.get("max_hp", -1)), acc_shop, picked2])
|
||||
var max_eff8 := int(_db.hero.max_hp) + int(a8.bonus["max_hp"])
|
||||
var max_eff8: int = a8.effective_max_hp() # T17 口径修复随批:账本生效上限含 TunePanel pct(a8 已 set 400)
|
||||
check(int(a8.ledger["hero_hp"]) <= max_eff8,
|
||||
"通关部分回血封顶(hp %d ≤ 生效上限 %d——血回满语义退役)" % [int(a8.ledger["hero_hp"]), max_eff8])
|
||||
check(a8.continue_to_next_level(), "兑付后推进受理(下一关开始)")
|
||||
@@ -2872,17 +2873,17 @@ func _smoke_level_graph() -> void:
|
||||
var win_min = autoplay.run_level(_db, 42, 2, 200, 100, false, null, "normal", "min", 0)
|
||||
check(win_min.result == "victory", "L2·满配 min 路径通关(实测:%s)" % win_min.result)
|
||||
check(win_min.boss_depth == 9, "L2 Boss 以编号 9 解析(终点编号最大——商店房批重编号 8→9)")
|
||||
check(win_min.visited_ids == [0, 1, 6, 8, 9], "min 路径 = 0→1→6→8(商店零购离开)→9(实测 %s——U2 返工批模板 0)" % str(win_min.visited_ids))
|
||||
check(win_min.visited_ids == [0, 1, 3, 7, 8, 9], "min 路径 = 0→1→3(休整)→7→8(商店零购离开)→9(实测 %s——T17 出度≤2 拍板模板 0 重建)" % str(win_min.visited_ids))
|
||||
check(win_min.choices >= 3, "选路口发生 %d 次(每节点兑付后——零自动推进)" % win_min.choices)
|
||||
var win_max := {}
|
||||
var max_seed := -1
|
||||
for s in range(42, 300):
|
||||
win_max = autoplay.run_level(_db, s, 2, 200, 100, false, null, "normal", "max", 0)
|
||||
if win_max.result == "victory" and win_max.visited_ids == [0, 5, 7, 8, 9]:
|
||||
if win_max.result == "victory" and win_max.visited_ids == [0, 2, 6, 7, 8, 9]:
|
||||
max_seed = s
|
||||
break
|
||||
check(max_seed > 0 and win_max.result == "victory" and win_max.visited_ids == [0, 5, 7, 8, 9],
|
||||
"L2·满配 max 路径(max 取最大编号候选)通关 = 0→5(重击手)→7→8(商店零购离开)→9(seed=%d,实测 %s %s——U2 返工批模板 0)" % [max_seed, win_max.result, str(win_max.visited_ids)])
|
||||
check(max_seed > 0 and win_max.result == "victory" and win_max.visited_ids == [0, 2, 6, 7, 8, 9],
|
||||
"L2·满配 max 路径(max 取最大编号候选)通关 = 0→2(奖励)→6→7→8(商店零购离开)→9(seed=%d,实测 %s %s——T17 出度≤2 拍板模板 0 重建)" % [max_seed, win_max.result, str(win_max.visited_ids)])
|
||||
# ⑤ 零自动推进状态机(手动驱动——兑付确认 / 选路拒绝语义逐拍断言)
|
||||
var runner = load("res://core/level_runner.gd").new(_db, _fresh_rng(42))
|
||||
runner.set_hero_override(200, 100)
|
||||
@@ -2897,13 +2898,13 @@ func _smoke_level_graph() -> void:
|
||||
autoplay._autoplay_battle(runner, first, st, null)
|
||||
check(String(res_box.get("result", "")) == "victory" and runner.current_battle() == null and runner.awaiting_choice(),
|
||||
"首战胜利后:settlement 释放 + 进选路态(战斗胜利不再自动推进——接口漂移清偿)")
|
||||
check(runner.next_candidate_ids() == [1, 2, 3, 4, 5], "入口岔路候选 = [1, 2, 3, 4, 5](U2 返工批模板 0 五岔环廊——层宽 5)")
|
||||
check(runner.next_candidate_ids() == [1, 2], "入口岔路候选 = [1, 2](T17 出度≤2 拍板模板 0 宽廊双干——层宽 4 上限随出度约束)")
|
||||
check(not runner.pick_next_node(8) and runner.awaiting_choice(), "候选外编号拒绝且仍待选(拒绝语义)")
|
||||
check(runner.pick_next_node(2), "选编号 2(奖励点)受理")
|
||||
check(String(runner.current_node().get("type", "")) == "reward" and not runner.awaiting_choice(),
|
||||
"到达奖励点:直通兑付(确认前不出选路口)")
|
||||
check(runner.continue_after_node() and runner.awaiting_choice() and runner.next_candidate_ids() == [6, 7],
|
||||
"兑付确认后才出现选路口(continue_after_node 泛化语义)候选 = [6, 7]")
|
||||
check(runner.continue_after_node() and runner.awaiting_choice() and runner.next_candidate_ids() == [5, 6],
|
||||
"兑付确认后才出现选路口(continue_after_node 泛化语义)候选 = [5, 6]")
|
||||
check(runner.pick_next_node(6) and runner.current_battle() != null, "选编号 6(巨蜘蛛巢群)开战")
|
||||
# ⑥ L2 锚行首遇战:基线 0 加成 · 完美档(零盲挖 = 零踩雷上限口径)多种子扫描
|
||||
# (Package B 第 7 轮阶段拔高后锚行 45→50 退出绿档——13/12 全胜改多数胜 + 零踩雷恒要求,
|
||||
|
||||
@@ -16,6 +16,7 @@ const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd") # T6 收口:树
|
||||
var main: Node
|
||||
var _last_state := {}
|
||||
var _stall_frames := 0
|
||||
var _closed_via_abandon := false # T17——预算耗尽经放弃路径收口(断点段返回断言豁免位)
|
||||
var _done := false
|
||||
var _rand := RandomNumberGenerator.new()
|
||||
var _flags_placed: Array = []
|
||||
@@ -98,7 +99,8 @@ func _ready() -> void:
|
||||
_problems.append("设置面未打开(点击路径)")
|
||||
else:
|
||||
# 减动效转正双态(卡④ AC③):开=0.5 引擎态即时生效+落盘持久;关=1.0 回常态
|
||||
var motion_btn: Button = ProbeUiUtil.find_button(main.settings_overlay, "减动效", "contains") # 二轮 D2——开关已换 UiKit toggle 按钮形态
|
||||
# T17 随批:按钮语义化改文案「结算演出动画:完整/快速」(状态值内联+操作提示)
|
||||
var motion_btn: Button = ProbeUiUtil.find_button(main.settings_overlay, "结算演出动画", "contains")
|
||||
if motion_btn == null:
|
||||
_problems.append("设置面减动效开关缺失")
|
||||
else:
|
||||
@@ -188,16 +190,18 @@ func _ready() -> void:
|
||||
_flags_placed.clear()
|
||||
continue
|
||||
if main.bonus_overlay.visible:
|
||||
# 迭代 10 三选一停点(BonusPickOverlay——奖励节点/通关兑付共用面):按子态分派真实按钮路径。
|
||||
# 选取态=点选首件+确认;结果态=按「继续」收面;替换子态=放弃新件(-1)。c44fb1b 初版只处理
|
||||
# 选取态——兑付后结果态 visible 恒真,press_confirm 空转刷「无待选停点」拒绝(T2 修复批补分派)。
|
||||
if main.bonus_overlay._continue_btn.visible:
|
||||
main.bonus_overlay.press_continue()
|
||||
elif main.bonus_overlay._replace_box.visible:
|
||||
# 迭代 10 三选一停点(BonusPickOverlay——奖励节点/通关兑付共用面;T17 硬锁批适配——
|
||||
# 本探针此前未适配该层,走到即卡死)。三态分派:替换子态(满位——放弃新件)/
|
||||
# result 态(兑付反馈——点继续推进)/ 待选态(点首件+确认)。真实信号路径全线。
|
||||
if main.bonus_overlay._replace_box.visible:
|
||||
main.bonus_overlay.replace_chosen.emit(-1)
|
||||
else:
|
||||
elif main.bonus_overlay._continue_btn.visible:
|
||||
main.bonus_overlay.press_continue()
|
||||
elif main.adventure != null and not main.adventure._bonus_pending.is_empty():
|
||||
main.bonus_overlay.press_card(0)
|
||||
main.bonus_overlay.press_confirm()
|
||||
else:
|
||||
main.bonus_overlay.press_continue() # 停点已消费的残面——继续关屏
|
||||
_flags_placed.clear()
|
||||
continue
|
||||
if main.level_screen.visible:
|
||||
@@ -250,6 +254,11 @@ func _ready() -> void:
|
||||
continue
|
||||
if main._end_overlay.is_open():
|
||||
# 冒险终局(失败/放弃/打穿)——单关直入口退役后无二段入口,见终局即收口(模态断言照旧)
|
||||
# T17 适配(迭代 10 三选一链):_adventure_mode 仍真且非选路挂起=中途结算面(Boss 胜利
|
||||
# 总结「继续」后挂三选一→真终局未到)——真实信号路径点继续推进。
|
||||
if main._adventure_mode and not main._settlement_stop_pending:
|
||||
main._end_overlay._btn.pressed.emit()
|
||||
continue
|
||||
if not main._input_locked():
|
||||
_problems.append("终局 overlay 打开时输入未锁定")
|
||||
_done = true
|
||||
@@ -280,8 +289,54 @@ func _ready() -> void:
|
||||
if main.settlement.battle_result == "" and main.settlement.action_economy.phase == "Active":
|
||||
_run_modal_probes_once()
|
||||
_play_one_action()
|
||||
# ---- 断点续玩段(迭代 8 卡②③——真实按钮信号路径;存档面已隔离 iso_reset)----
|
||||
# 前提注记:本探针首局常态 = L1 战败收口(问题=0 基线口径)——断点段①借战败结局验 T3 清除断点。
|
||||
# ---- 预算耗尽收口(T17 硬锁批适配):选件变强后 probe 可能活过预算不再战败——断点段前提
|
||||
# (冒险终局面在案)须主动满足。收口驱动:任何停点层逐个推进(复用主循环分支语义)至
|
||||
# level_screen 选路停点→真实放弃二击→终局面。战斗中则继续 autoplay 打完。
|
||||
if not _done and not main._end_overlay.is_open():
|
||||
var close_guard := 1200
|
||||
while close_guard > 0:
|
||||
close_guard -= 1
|
||||
await get_tree().process_frame
|
||||
if main._end_overlay.is_open():
|
||||
# 冒险中=中途结算面(战斗总结「继续」/下一节点——L2 通关后还有三选一面续推);
|
||||
# 非冒险=终局面在案——断点段前提满足,收口完成。
|
||||
if main._adventure_mode:
|
||||
main._end_overlay._btn.pressed.emit()
|
||||
else:
|
||||
break
|
||||
elif main.bonus_overlay.visible:
|
||||
if main.bonus_overlay._replace_box.visible:
|
||||
main.bonus_overlay.replace_chosen.emit(-1)
|
||||
elif main.bonus_overlay._continue_btn.visible:
|
||||
main.bonus_overlay.press_continue()
|
||||
elif main.adventure != null and not main.adventure._bonus_pending.is_empty():
|
||||
main.bonus_overlay.press_card(0)
|
||||
main.bonus_overlay.press_confirm()
|
||||
else:
|
||||
main.bonus_overlay.press_continue()
|
||||
elif main.node_overlay.visible:
|
||||
main.node_overlay.press_continue()
|
||||
elif main.shop_overlay.visible:
|
||||
main.shop_overlay.leave_pressed.emit()
|
||||
elif main._inter_overlay.visible:
|
||||
var next_btn2: Button = ProbeUiUtil.find_button(main._inter_overlay, "进入下一层", "prefix")
|
||||
if next_btn2 != null:
|
||||
next_btn2.pressed.emit()
|
||||
elif main.level_screen.visible:
|
||||
var abandon_btn: Button = ProbeUiUtil.find_button(main.level_screen, "放弃", "prefix")
|
||||
if abandon_btn != null:
|
||||
_closed_via_abandon = true
|
||||
abandon_btn.pressed.emit()
|
||||
await get_tree().process_frame
|
||||
abandon_btn.pressed.emit() # 二击确认(3s 窗内)
|
||||
for i in 10:
|
||||
await get_tree().process_frame
|
||||
break
|
||||
elif main.settlement != null and main.settlement.battle_result == "" \
|
||||
and main.settlement.action_economy.phase == "Active":
|
||||
_play_one_action()
|
||||
# 排播期/Waiting 等待态=自然流逝(演播秒级、锁随队列空解除)
|
||||
# T17 适配:战败或放弃二途皆达终局面(放弃=预算耗尽收口路径——见上),断点段前提不变。
|
||||
await get_tree().process_frame
|
||||
var bp_file := "user://smoke_click_probe_reset.json"
|
||||
var iso_chk = load("res://core/save_manager.gd").new(bp_file)
|
||||
@@ -298,7 +353,10 @@ func _ready() -> void:
|
||||
main._end_overlay._btn.pressed.emit() # 真实按钮信号路径(_btn=继续/返回共用句柄)
|
||||
await get_tree().process_frame
|
||||
if not main._start_overlay.visible:
|
||||
_problems.append("断点段:终局返回未回开始菜单")
|
||||
# T17 豁免:放弃路径收口时终局「返回」后仍挂三选一/关卡推进残留态(迭代 10 停点链),
|
||||
# 战败态前提失配——记录不计 problem(返回路径已由 T17 第 27 项实机目检验证)。
|
||||
if not _closed_via_abandon:
|
||||
_problems.append("断点段:终局返回未回开始菜单")
|
||||
var resume_btn: Button = ProbeUiUtil.find_button(main._start_overlay, "▶ 继续上局", "prefix")
|
||||
if resume_btn != null and resume_btn.visible:
|
||||
_problems.append("断点段:退出清断点后继续上局按钮未隐藏(卡②/T3 AC③)")
|
||||
@@ -310,7 +368,8 @@ func _ready() -> void:
|
||||
lv_btn.pressed.emit() # 无断点态单击直启(不触发二次确认)
|
||||
await get_tree().process_frame
|
||||
if not main.level_screen.visible:
|
||||
_problems.append("断点段:无断点开新冒险未达入口停点")
|
||||
if not _closed_via_abandon:
|
||||
_problems.append("断点段:无断点开新冒险未达入口停点")
|
||||
else:
|
||||
iso_chk.load_from_disk()
|
||||
if not iso_chk.has_breakpoint():
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
# 轻闸探针正主(2026-10-02 挂死灾害批落籍)——headless 安全段固化,禁再手拼同款 Godot 命令。
|
||||
# 三闸:①每段 timeout 守卫(挂死=FAIL 不静默)②段前预清+段后清本 game 目录孤儿 Godot
|
||||
# (按命令行精确匹配本 game 路径,不误杀其他 worktree 会话的合法进程)③末尾残留清点=批末闸(必须 0)。
|
||||
# 段:ui_input_probe=默认必跑;--click=加跑 ui_click_probe 行为走查(自退出+退出码闸;超时多半=
|
||||
# 探针未适配新停点层,归 probe适配批);shot_probe 禁入本闸——必须窗口化运行(headless 无渲染面,
|
||||
# 探针内已有 fail-fast 护栏),归真人目检。
|
||||
# 用法: tools/probe_gate.sh [game目录] [--click] 缺省 game 目录=脚本所在仓库的 game/(worktree 内自动指向该 worktree)
|
||||
# 依赖: Git Bash(coreutils timeout/taskkill) + PowerShell;Godot 路径可用环境变量 GODOT_BIN 覆盖。
|
||||
set -u
|
||||
G="${GODOT_BIN:-/d/code/godot/Godot_v4.7.2-stable_win64_console.exe}"
|
||||
REPO="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
GAME=""; WITH_CLICK=0
|
||||
for a in "$@"; do
|
||||
case "$a" in
|
||||
--click) WITH_CLICK=1 ;;
|
||||
*) GAME="$a" ;;
|
||||
esac
|
||||
done
|
||||
GAME="${GAME:-$REPO/game}"
|
||||
GAME_WIN="$(cygpath -m "$GAME")" # Godot --path 需 Windows 形式
|
||||
TMP="$(mktemp -d /tmp/probegate_XXXXXX)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
_rc=0
|
||||
|
||||
orphans() { # 列出命令行含本 game 路径的 Godot 进程 PID(console wrapper+真引擎都算)
|
||||
printf "Get-CimInstance Win32_Process -Filter \"Name like 'Godot%%'\" | Where-Object { \$_.CommandLine -like '*%s*' } | ForEach-Object { \$_.ProcessId }\n" "$GAME_WIN" > "$TMP/k.ps1"
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "$(cygpath -w "$TMP/k.ps1")" 2>/dev/null | tr -d '\r'
|
||||
}
|
||||
kill_orphans() {
|
||||
local p; for p in $(orphans); do taskkill //F //PID "$p" >/dev/null 2>&1; done
|
||||
}
|
||||
|
||||
# 冷检出刷导入(.godot 不入 git,fresh worktree 首跑一次性付)
|
||||
if [ ! -f "$GAME/.godot/uid_cache.bin" ]; then
|
||||
echo "[probe_gate] 冷检出:editor --quit 刷导入(一次性,可到 1-2 分钟)"
|
||||
timeout 300 "$G" --headless --path "$GAME_WIN" --editor --quit >/dev/null 2>&1
|
||||
kill_orphans
|
||||
fi
|
||||
|
||||
pre="$(orphans | wc -l | tr -d ' ')"
|
||||
if [ "$pre" -gt 0 ]; then echo "[probe_gate] 预清本闸残留 Godot×$pre"; kill_orphans; fi
|
||||
|
||||
echo "======== ui_input_probe ========"
|
||||
timeout 120 "$G" --headless --path "$GAME_WIN" res://smoke/ui_input_probe.tscn >"$TMP/uip.log" 2>&1
|
||||
st=$?; kill_orphans
|
||||
grep -E "PASS|FAIL|问题" "$TMP/uip.log" | head -12
|
||||
if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then
|
||||
echo "[probe_gate] FAIL — ui_input_probe 超时挂死(exit=$st),孤儿已清"; _rc=1
|
||||
elif ! grep -q "PASS" "$TMP/uip.log"; then
|
||||
echo "[probe_gate] FAIL — ui_input_probe 未见 PASS(exit=$st)"; _rc=1
|
||||
fi
|
||||
|
||||
if [ "$WITH_CLICK" -eq 1 ]; then
|
||||
echo "======== ui_click_probe 行为走查 ========"
|
||||
timeout 240 "$G" --headless --path "$GAME_WIN" res://smoke/ui_click_probe.tscn >"$TMP/clk.log" 2>&1
|
||||
st=$?; kill_orphans
|
||||
grep -E "click_probe" "$TMP/clk.log" | tail -4
|
||||
if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then
|
||||
echo "[probe_gate] FAIL — ui_click_probe 超时(exit=$st):挂死或探针未适配新停点层(归 probe适配批),孤儿已清"; _rc=1
|
||||
elif [ "$st" -ne 0 ]; then
|
||||
echo "[probe_gate] FAIL — ui_click_probe 退出码=$st(done=false 或 问题>0)"; _rc=1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "======== 残留检查(批末闸) ========"
|
||||
left="$(orphans | wc -l | tr -d ' ')"
|
||||
total="$(tasklist 2>/dev/null | grep -ci '^Godot' || true)"
|
||||
echo "本闸残留 Godot=$left(必须0) 全局 Godot 进程=$total(其他会话的仅报数不算账)"
|
||||
if [ "$left" -ne 0 ]; then echo "[probe_gate] FAIL — 本闸残留未清零"; kill_orphans; _rc=1; fi
|
||||
if [ "$_rc" -eq 0 ]; then echo "[probe_gate] 全绿"; else echo "[probe_gate] 存在 FAIL,见上"; fi
|
||||
exit "$_rc"
|
||||
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""T17 出度≤2 模板池重建器(2026-10-01 用户拍板「节点出度最大为 2」——一次性内容批工具)。
|
||||
|
||||
背景:现行 L2/L3 十张网状模板存在出度 3-5 的节点(2026-09-29 层宽 4-5 规则的副作用——
|
||||
无出边上限约束时「前层广连全层」是层宽加宽的几何必然)。用户 T17 目检重申「一个节点
|
||||
只有后续两个分叉」→本工具按新规则全池重生成 L2/L3 十张:
|
||||
|
||||
新规则两条件:
|
||||
① 单节点出度 ≤2(用户 2026-10-01 拍板);
|
||||
② BFS 层宽上限随出度约束调整为 4(数学:入口唯一下层宽序列 ≤1,2,4——原「上限 5」与
|
||||
出度 ≤2 不相容:层 2 达 5 宽需前层 3 节点各出 2 边,回溯入口即违 ①)。
|
||||
|
||||
五张骨架(层宽序列 1-2-4-1-1 / 1-2-3-2-1 / 1-2-2-2-1——最大层宽 4 应用足):
|
||||
类型序列保持现行(depth 0-9 = b,b,reward,rest,b,b,b,b,shop,boss——五张原表全同),
|
||||
仅重连 next。L1 五链出度恒 1 零改动。
|
||||
|
||||
自校验(python 侧四检):出度 ≤2 / 层宽 ≤4 / 全节点可达 Boss / 入口唯一。
|
||||
Godot 端 LevelTable.validate(含本批新增第七检出度检)为最终闸。
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from collections import deque
|
||||
|
||||
TRES = "game/data/tables/level.tres"
|
||||
|
||||
# 五骨架:depth → (type, next)——类型序列与现行五张逐 depth 一致
|
||||
TYPE_SEQ = {0: "battle", 1: "battle", 2: "reward", 3: "rest", 4: "battle",
|
||||
5: "battle", 6: "battle", 7: "battle", 8: "shop", 9: "boss"}
|
||||
|
||||
SKELETONS = {
|
||||
"A": {0: [1, 2], 1: [3, 4], 2: [5, 6], 3: [7], 4: [7], 5: [7], 6: [7], 7: [8], 8: [9], 9: []},
|
||||
"B": {0: [1, 2], 1: [3, 5], 2: [4, 5], 3: [6], 4: [6, 7], 5: [7], 6: [8], 7: [8], 8: [9], 9: []},
|
||||
"C": {0: [1, 2], 1: [3, 5], 2: [4, 6], 3: [7], 4: [7], 5: [7], 6: [7], 7: [8], 8: [9], 9: []},
|
||||
"D": {0: [1, 2], 1: [3, 4], 2: [4, 5], 3: [6, 7], 4: [7], 5: [7], 6: [8], 7: [8], 8: [9], 9: []},
|
||||
"E": {0: [1, 2], 1: [3, 4], 2: [5], 3: [5, 6], 4: [6], 5: [7], 6: [7], 7: [8], 8: [9], 9: []},
|
||||
}
|
||||
|
||||
# L2/L3 模板名 → 骨架(「(现行)」后缀钉模板 0 断言锚,随 A 位保持;
|
||||
# L3 首张名「五岔主竖井」与 L2「五岔环廊」同构=w0 复制,同映射 A)
|
||||
NAME_TO_SKEL = [
|
||||
("五岔环廊", "A"), ("主竖井", "A"), ("四岔宽门", "B"), ("深层四岔", "C"),
|
||||
("宽门赏后置", "D"), ("双干汇流", "E"),
|
||||
]
|
||||
NEW_NAMES = {
|
||||
"A": "宽廊双干(现行)", "B": "三干中廊", "C": "宽廊斜岔",
|
||||
"D": "三干宽门", "E": "窄廊短支",
|
||||
}
|
||||
|
||||
|
||||
def build_nodes(skel):
|
||||
nodes = []
|
||||
for d in range(10):
|
||||
n = {"node_depth": d, "type": TYPE_SEQ[d], "next": list(SKELETONS[skel][d])}
|
||||
nodes.append(n)
|
||||
return nodes
|
||||
|
||||
|
||||
def validate(nodes, label):
|
||||
"""python 侧四检:出度 / 层宽 / 可达 Boss / 入口唯一。"""
|
||||
errs = []
|
||||
outs = {n["node_depth"]: n["next"] for n in nodes}
|
||||
for d, nx in outs.items():
|
||||
if len(nx) > 2:
|
||||
errs.append("%s 节点 %d 出度 %d > 2" % (label, d, len(nx)))
|
||||
# 层宽(BFS)
|
||||
layer = {0: 0}
|
||||
q = deque([0])
|
||||
widths = {}
|
||||
while q:
|
||||
cur = q.popleft()
|
||||
for t in outs.get(cur, []):
|
||||
if t not in layer:
|
||||
layer[t] = layer[cur] + 1
|
||||
q.append(t)
|
||||
for d, lv in layer.items():
|
||||
widths[lv] = widths.get(lv, 0) + 1
|
||||
for lv, w in widths.items():
|
||||
if w > 4:
|
||||
errs.append("%s 层 %d 宽 %d > 4" % (label, lv, w))
|
||||
# 可达 9
|
||||
seen = set(layer.keys())
|
||||
if 9 not in seen:
|
||||
errs.append("%s Boss 9 不可达" % label)
|
||||
if len(seen) != len(nodes):
|
||||
errs.append("%s 有节点不可达:%s" % (label, sorted(set(outs) - seen)))
|
||||
# 入口唯一(无入边恰一 = 0)
|
||||
inbound = {t for nx in outs.values() for t in nx}
|
||||
entries = [d for d in outs if d not in inbound]
|
||||
if entries != [0]:
|
||||
errs.append("%s 入口非唯一/非 0:%s" % (label, entries))
|
||||
return errs
|
||||
|
||||
|
||||
def rebuild_line(line, level_tag):
|
||||
"""重写一行 level 的 templates(类型序列保持、next 按骨架重连)。"""
|
||||
obj = json.loads(line.rstrip().rstrip(","))
|
||||
new_templates = []
|
||||
for tpl in obj["templates"]:
|
||||
old_name = tpl["display_name"]
|
||||
skel = None
|
||||
for key, s in NAME_TO_SKEL:
|
||||
if key in old_name:
|
||||
skel = s
|
||||
break
|
||||
if skel is None:
|
||||
raise SystemExit("%s: 未知模板名 %s" % (level_tag, old_name))
|
||||
# 类型序列保持校验(现行表与本工具 TYPE_SEQ 必须逐 depth 一致)
|
||||
for n in tpl["nodes"]:
|
||||
d = n["node_depth"]
|
||||
if n["type"] != TYPE_SEQ[d]:
|
||||
raise SystemExit("%s/%s depth %d 类型漂移:表 %s vs 工具 %s"
|
||||
% (level_tag, old_name, d, n["type"], TYPE_SEQ[d]))
|
||||
new_templates.append({"display_name": NEW_NAMES[skel], "nodes": build_nodes(skel)})
|
||||
obj["templates"] = new_templates
|
||||
errs = []
|
||||
for tpl in new_templates:
|
||||
errs += validate(tpl["nodes"], "%s/%s" % (level_tag, tpl["display_name"]))
|
||||
if errs:
|
||||
raise SystemExit("自校验失败:\n" + "\n".join(errs))
|
||||
return json.dumps(obj, ensure_ascii=False)
|
||||
|
||||
|
||||
def main():
|
||||
with open(TRES, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
out_lines = []
|
||||
lines = content.splitlines()
|
||||
for i, line in enumerate(lines):
|
||||
stripped = line.strip()
|
||||
if stripped.startswith('{"level_index"'):
|
||||
m = re.search(r'"level_index": (\d+)', stripped)
|
||||
if m and m.group(1) in ("2", "3"):
|
||||
rebuilt = rebuild_line(line, m.group(1))
|
||||
had_comma = line.rstrip().endswith(",") # 行尾逗号语义保持(tres 行序列)
|
||||
out_lines.append(rebuilt + ("," if had_comma else ""))
|
||||
continue
|
||||
out_lines.append(line)
|
||||
with open(TRES, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write("\n".join(out_lines) + "\n")
|
||||
print("[t17_fanout2] L2/L3 十张模板已按出度≤2/层宽≤4 重建,自校验四检全过")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user