Files
gplan-2/game/smoke/ui_struct_probe.gd
T

271 lines
9.5 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# VERTICAL SLICE - NOT FOR PRODUCTION
# UI 结构探针(ui-struct-probe mini-loop 2026-10-05——计划书 .zcode/plans/ui-struct-probe-miniloop-plan-2026-10-05.md T0/T1)
# 全 UI 屏结构断言一次跑完:逐屏实例化(菜单簇真驱动 + 冒险簇真流推进 + 兑付屏真实 open 面合成喂入),
# 断言族:
# ①overlap 同屏内可见未禁用按钮两两 global_rect 不相交(互压=红;层间模态盖底层系设计内——各屏采样以屏根为界天然排除)
# ②equal_width 同一直接父容器内 ≥2 可见按钮宽度差 >1px=红(容器纪律:组内等宽)
# ③offscreen 可见件 global_rect 完全落在视口外=红
# ④zero_size 可见件宽或高 ≤0=红
# ⑤start 专项 标题屏左列(start_box)与右列(start_right)矩形不相交(M2 双列契约——叠放缺陷回归闸)
# 豁免:WL 常量表按「屏名/规则名/件名」登记合法例外(登记须注理由于行尾注释)。
# 运行:Godot_console.exe --headless --path . res://smoke/ui_struct_probe.tscn
extends Node
const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd")
# 豁免清单(空 = 零豁免起步;红灯逐条核对后合法者入表并注理由)
const WL := {}
var problems: Array = []
func _ready() -> void:
await get_tree().process_frame # autoload 就绪
var main: Node = load("res://presentation/main.tscn").instantiate()
add_child(main)
await get_tree().process_frame
# ---- 菜单簇(开始层可见态;逐屏开→采样→关,模态恢复走组合根真实路径)----
ProbeUiUtil.isolate_save(main, "smoke_ui_struct_probe_menu", true, false, true)
_settle()
await _audit("start", [main._start_overlay])
_start_columns_contract(main)
main._open_settings()
_settle()
await _audit("settings", [main.settings_overlay])
main._on_settings_closed()
_settle()
var campbtn: Button = ProbeUiUtil.find_button(main._start_overlay, "营地", "prefix")
if campbtn != null:
campbtn.pressed.emit()
_settle()
await _audit("camp", [main.camp_overlay])
main._on_camp_closed()
_settle()
else:
problems.append("[camp] 营地入口按钮缺失(驱动中断)")
main.confirm_dialog.open("结构探针采样", "确认")
_settle()
await _audit("confirm", [main.confirm_dialog])
main.confirm_dialog.close()
_settle()
main._show_debug_overlay()
_settle()
await _audit("debug", [main.debug_overlay])
main._close_debug_overlay()
_settle()
# ---- 冒险簇(真流推进:开冒险→地图选路态→选战斗节点→战斗 HUD)----
ProbeUiUtil.isolate_save(main, "smoke_ui_struct_probe_adv", true, false, true)
main._start_adventure_from_menu()
_settle(4)
await _audit("level_map", [main.level_screen])
var battle_id := _pick_battle_node(main)
if battle_id >= 0:
main.level_screen.route_chosen.emit(battle_id)
_settle(6)
_audit("battle_hud", [main.board_zone, main.info_panel, main.stage])
else:
problems.append("[battle_hud] 候选中无战斗节点(驱动中断——候选=%s)" % str(main.level_screen._cand_ids))
# ---- 兑付屏(真实 open 面 + 合成最小数据——结构断言只看矩形/宽度/可见性)----
main._end_overlay.open_victory("结构探针·胜利演算", "返回", Rect2(200, 200, 400, 300), "GP 0")
_settle()
await _audit("battle_end", [main._end_overlay])
main._end_overlay.close()
_settle()
main.curio_overlay.open_pick("结构探针·三选一", [
{"id": "probe_heal", "text": "回血\n恢复 3 点"},
{"id": "probe_magic", "text": "爆裂轰击\n下一击 +5"},
{"id": "probe_pierce", "text": "穿甲\n无视护甲"},
])
_settle()
await _audit("curio_pick", [main.curio_overlay])
main.curio_overlay.visible = false
_settle()
main.shop_overlay.open_shop([
{"product_id": "probe_p1", "display_name": "探针商品甲", "attr": "atk", "flat_value": 1, "price": 10, "purchased": 0},
{"product_id": "probe_p2", "display_name": "探针商品乙", "attr": "hp", "flat_value": 2, "price": 20, "purchased": 0},
], 100)
_settle()
await _audit("shop", [main.shop_overlay])
# ---- 判定 ----
if problems.is_empty():
print("[ui_struct_probe] PASS — 全屏结构断言零红")
get_tree().quit(0)
else:
for p in problems:
print("[ui_struct_probe] FAIL — %s" % p)
get_tree().quit(1)
func _pick_battle_node(main: Node) -> int:
# 入口/兑付停点走 level_screen.open_choices 直传(不写 _pending_candidates)——
# 候选从视图态取:_cand_ids × runner.nodes() 的 type(编号解析=node_id_of 单一出口)
var ls: Node = main.level_screen
if ls._runner == null:
return -1
for nd in ls._runner.nodes():
if String(nd.get("type", "")) in ["battle", "boss"]:
var nid: int = ls._runner.node_id_of(nd)
if nid in ls._cand_ids:
return nid
return -1
func _settle(frames := 4) -> void:
for i in range(frames):
await get_tree().process_frame
await get_tree().process_frame # 布局排序系帧尾 call_deferred——末帧后再保一帧
# ---- 采样与断言 ----
func _visible_children(root: Control) -> Array:
var out: Array = []
var stack: Array = [root]
while not stack.is_empty():
var c: Control = stack.pop_back()
if c == null:
continue
if c.is_visible_in_tree():
out.append(c)
for ch in c.get_children():
if ch is Control:
stack.append(ch)
return out
func _buttons(root: Control) -> Array:
var out: Array = []
for c in _visible_children(root):
if c is Button and not c.disabled:
out.append(c)
return out
func _wl(screen: String, rule: String, ctl: Control) -> bool:
var s: Dictionary = WL.get(screen, {})
if s.is_empty():
return false
var names: Array = s.get(rule, [])
if names.is_empty():
return false
return String(ctl.name) in names
func _force_layout(root: Control) -> void:
# modal 屏首次 visible 后容器排序不自动传播(size 已算而 position 停 0——4.7 行为)——
# 采样前对子树全部 Container 强制 queue_sort
var stack: Array = [root]
while not stack.is_empty():
var c: Control = stack.pop_back()
if c is Container:
c.queue_sort()
for ch in c.get_children():
if ch is Control:
stack.append(ch)
func _in_scroll(c: Control) -> bool:
var p := c.get_parent()
while p is Control:
if p is ScrollContainer:
return true
p = p.get_parent()
return false
func _audit(screen: String, roots: Array) -> void:
for r in roots:
if r != null and is_instance_valid(r):
_force_layout(r)
await _settle(1)
if OS.get_environment("STRUCT_PROBE_DEBUG") == "1":
for r in roots:
if r != null and is_instance_valid(r):
_dump(r, 0)
var all_ctls: Array = []
var all_buttons: Array = []
for r in roots:
if r == null or not is_instance_valid(r):
problems.append("[%s] 采样根缺失" % screen)
return
all_ctls += _visible_children(r)
all_buttons += _buttons(r)
var vp := get_viewport().get_visible_rect()
# ①overlap:操作钮两两不相交
for i in range(all_buttons.size()):
for j in range(i + 1, all_buttons.size()):
var a: Control = all_buttons[i]
var b: Control = all_buttons[j]
if _wl(screen, "overlap", a) or _wl(screen, "overlap", b):
continue
if a.get_global_rect().intersects(b.get_global_rect()):
problems.append("[%s][overlap] %s 与 %s 矩形相交(%s ∩ %s)" % [
screen, a.name, b.name, a.get_global_rect(), b.get_global_rect()])
# ②equal_width:同一直接父容器内 ≥2 可见按钮宽度差 >1px
var groups := {}
for b in all_buttons:
var k: Node = b.get_parent()
if not groups.has(k):
groups[k] = []
groups[k].append(b)
for k in groups:
var g: Array = groups[k]
if g.size() < 2:
continue
var w0: float = (g[0] as Control).size.x
var diff := false
for b in g:
if _wl(screen, "equal_width", b):
diff = false
break
if absf((b as Control).size.x - w0) > 1.0:
diff = true
if diff:
var names: Array = []
var widths: Array = []
for x in g:
names.append(String((x as Control).name))
widths.append((x as Control).size.x)
problems.append("[%s][equal_width] 同容器按钮宽度不一(%s——%s)" % [screen, str(names), str(widths)])
# ③offscreen + ④zero_size(ScrollContainer 内容坐标系超出=可滚动设计内——豁免)
for c in all_ctls:
if _in_scroll(c):
continue
var gr: Rect2 = c.get_global_rect()
if not _wl(screen, "offscreen", c) and not gr.intersects(vp):
problems.append("[%s][offscreen] %s 完全出视口(%s)" % [screen, c.name, gr])
if not _wl(screen, "zero_size", c) and (c.size.x <= 0.0 or c.size.y <= 0.0):
problems.append("[%s][zero_size] %s 零尺寸(%s)" % [screen, c.name, c.size])
func _dump(c: Control, depth: int) -> void:
var ind := " ".repeat(depth)
print("[dump] %s%s(%s) pos=%s gpos=%s size=%s vis=%s" % [ind, c.name, c.get_class(),
c.position, c.get_global_position(), c.size, c.is_visible_in_tree()])
for ch in c.get_children():
if ch is Control:
_dump(ch, depth + 1)
# ⑤start 专项:左列/右列矩形不相交(M2 双列契约——装配件命名 start_box/start_right 为探针锚点)
func _start_columns_contract(main: Node) -> void:
var box: Control = main._start_overlay.find_child("start_box", true, false)
var right: Control = main._start_overlay.find_child("start_right", true, false)
if box == null or right == null:
problems.append("[start][columns] 左列/右列节点缺失(start_box/start_right——装配命名失守)")
return
if box.get_global_rect().intersects(right.get_global_rect()):
problems.append("[start][columns] 左列 %s 与右列 %s 相交(双列叠放回归——M2 缺陷闸)" % [
box.get_global_rect(), right.get_global_rect()])