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:
DKChan committed 2026-10-02 10:42:52 +08:00
commit cd1ed95b55
14 files changed
+372 -32

No files matched your search

+11 -2
View File
@@ -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 必须已就位)
+6
View File
@@ -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():
+2 -2
View File
@@ -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,拍板点④):
+18 -2
View File
@@ -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 语义不冲突——选中期玩家仍可移动/插旗)
+5
View File
@@ -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
+8 -2
View File
@@ -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)
+13 -2
View File
@@ -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:
+5
View File
@@ -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
View File
@@ -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 全胜改多数胜 + 零踩雷恒要求,
+71 -12
View File
@@ -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():