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

216 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
# 棋盘 Node2D 迁移批 Phase A 2026-09-23:push_input 端到端输入探针(ADR-0003 登记项转义务、
# ADR-0006 Risks 缓解面)。现役探针全走 BoardView.click_cell 直调(合成鼠标事件零命中已实证)——
# 本探针是真实输入路径的唯一自动化覆盖:鼠标事件 → 壳层 _gui_input → 板级公式换算(_cell_at)→
# 悬停/点选/chord 既有出口 → 组合根信号。四 case:①悬停可挖域(金框+十字光标)②悬停禁入
# (hover_forbidden 信号+灰禁红框)③悬停已开格(路径预览点迹+移动光标)④右键插旗(flag_place
# 到达组合根+核心旗态)⑤双击 chord(禁入格双左击,核心拒挖零副作用)⑥左键挖(dig 到达组合根+核心揭格)
# ⑦英雄精灵世界落位(棋盘小人贴图批回归闸——SubViewport 世界坐标判据,素材缺位树跳过)。
# 事件路径:Viewport.push_input(ev, in_local_coords=true)——落点用 BoardView.cell_center_local
# (与 _cell_at 同一公式族)换算画布坐标,SubViewportContainer mouse_filter=IGNORE 不拦截。
# 运行:Godot_console.exe --headless --path . res://smoke/ui_input_probe.tscn
extends Node
var main
var _problems: Array = []
var _actions: Array = [] # action_requested 收集([cell, kind])
var _chords: Array = [] # chord_attempted 收集
var _forbids: Array = [] # hover_forbidden 收集
func _ready() -> void:
await get_tree().process_frame # autoload 就绪
main = load("res://presentation/main.tscn").instantiate()
add_child(main)
await get_tree().process_frame
main._start_adventure_from_menu()
await get_tree().process_frame
if main.route_overlay.visible:
main.route_overlay.press_candidate(0) # 入口选路停点 → 首战
for i in range(3):
await get_tree().process_frame
var bv = main.board_view
var ms = main.settlement.minesweeper if main.settlement != null else null
if ms == null or bv._cells.is_empty():
_pre_fail("战斗未就绪(cells=%d settlement=%s)" % [bv._cells.size(), str(main.settlement)])
return
bv.action_requested.connect(func(c: Vector2i, kind: String): _actions.append([c, kind]))
bv.chord_attempted.connect(func(c: Vector2i): _chords.append(c))
bv.hover_forbidden.connect(func(c: Vector2i): _forbids.append(c))
# ---- 先行移动(真实点击路径):R₀ 展开后主角邻域全已开(_diggable 空)——点击前沿已开格
# 迫近未开边界(与 ui_click_probe 行为同构:先 move 后 dig)----
var hero0: Vector2i = ms.hero_pos()
var mv := Vector2i(-9, -9)
for c in bv._board.neighbors8(hero0):
if ms.is_revealed(c) and _has_hidden_neighbor(bv, ms, c):
mv = c
break
if mv.x == -9:
mv = _pick_frontier(bv, ms)
if mv.x == -9:
_pre_fail("前沿已开格选取失败(板面无可迫近边界)")
return
_push_click(_pos(bv, mv), MOUSE_BUTTON_LEFT)
await get_tree().process_frame
await get_tree().process_frame
if not _has_action(mv, "move") or ms.hero_pos() == hero0:
_pre_fail("先导移动未到达(actions=%s hero=%s→%s)" % [str(_actions), hero0, ms.hero_pos()])
return
# ---- 目标格实证选取(不写死坐标;两枚相邻可挖格 + 禁入格 + 已开格)----
var flag_t := Vector2i(-9, -9)
var dig_t := Vector2i(-9, -9)
for c in bv._diggable:
if not ms.has_flag(c):
if flag_t.x == -9:
flag_t = c
elif dig_t.x == -9:
dig_t = c
break
var forbid_t := _pick_forbidden(bv, ms)
var reveal_t := _pick_revealed(ms)
if flag_t.x == -9 or dig_t.x == -9 or forbid_t.x == -9 or reveal_t.x == -9:
_pre_fail("目标格选取失败 flag=%s dig=%s forbid=%s reveal=%s" % [flag_t, dig_t, forbid_t, reveal_t])
return
# ---- case① 悬停可挖域:motion → 悬停态到达(金框 + 十字光标)----
_push_motion(_pos(bv, flag_t))
await get_tree().process_frame
if bv._hover != flag_t:
_problems.append("case① 悬停未到达(_hover=%s 应=%s)" % [bv._hover, flag_t])
var u1 = bv._cells[flag_t]
if not u1.hl_visible or u1.hl_color != UiKit.GOLD:
_problems.append("case① 悬停金框缺失(visible=%s color=%s)" % [str(u1.hl_visible), str(u1.hl_color)])
if bv.mouse_default_cursor_shape != Input.CURSOR_CROSS:
_problems.append("case① 挖掘光标未切十字(%d)" % bv.mouse_default_cursor_shape)
# ---- case② 悬停禁入:hover_forbidden 信号 + 灰禁红框 ----
_push_motion(_pos(bv, forbid_t))
await get_tree().process_frame
if _forbids.is_empty() or _forbids[_forbids.size() - 1] != forbid_t:
_problems.append("case② hover_forbidden 未达(%s)" % [str(_forbids)])
if bv._cells[forbid_t].hl_color != UiKit.RED_FORBID:
_problems.append("case② 禁入红框缺失(color=%s)" % str(bv._cells[forbid_t].hl_color))
# ---- case③ 悬停已开格:路径预览点迹 + 移动光标 ----
_push_motion(_pos(bv, reveal_t))
await get_tree().process_frame
var dots := 0
for c in bv._cells:
if bv._cells[c].dot_visible:
dots += 1
if dots < 1:
_problems.append("case③ 路径预览点迹未亮(dots=0)")
if bv.mouse_default_cursor_shape != Input.CURSOR_ARROW:
_problems.append("case③ 移动光标缺失(%d)" % bv.mouse_default_cursor_shape)
# ---- case④ 右键插旗:flag_place 到达组合根 + 核心旗态落位 ----
_push_click(_pos(bv, flag_t), MOUSE_BUTTON_RIGHT)
await get_tree().process_frame
if not _has_action(flag_t, "flag_place"):
_problems.append("case④ flag_place 未达组合根(actions=%s)" % [str(_actions)])
await get_tree().process_frame
if not ms.has_flag(flag_t):
_problems.append("case④ 核心旗态未落位")
# ---- case⑤ 双击 chord:禁入格双左击(核心拒挖零副作用)→ chord_attempted ----
_push_click(_pos(bv, forbid_t), MOUSE_BUTTON_LEFT)
_push_click(_pos(bv, forbid_t), MOUSE_BUTTON_LEFT)
await get_tree().process_frame
if _chords.is_empty() or _chords[_chords.size() - 1] != forbid_t:
_problems.append("case⑤ chord_attempted 未达(chords=%s)" % [str(_chords)])
# ---- case⑥ 左键挖:dig 到达组合根 + 核心揭格落位 ----
_push_click(_pos(bv, dig_t), MOUSE_BUTTON_LEFT)
await get_tree().process_frame
if not _has_action(dig_t, "dig"):
_problems.append("case⑥ dig 未达组合根(actions=%s)" % [str(_actions)])
await get_tree().process_frame
if not ms.is_revealed(dig_t):
_problems.append("case⑥ 核心揭格未落位(目标=%s)" % dig_t)
# ---- case⑦ 英雄精灵世界落位(棋盘小人贴图批回归闸 2026-09-26):精灵在 SubViewport 世界内,
# 落位 = 格×步距 + 脚底锚(横=格中线/纵=格高 75% 线——2026-09-26 二裁「脚踩格子 1/4 处」;
# 禁带壳层 _svp_container.position 居中偏移——上批误用壳层 cell_origin 实测右偏 ~7 格+偏离
# 格心,实机截图用户报缺陷)。素材缺位树(hero==null,字形「人」回退在役)跳过;断言前等
# 0.3s 墙钟——headless 帧率无上限,帧数等待 < 位移 Tween 140ms 时会采到半途位置(首跑实证)。 ----
if bv._world.hero != null:
await get_tree().create_timer(0.3).timeout
var expect: Vector2 = Vector2(ms.hero_pos()) * (bv.cell_size + float(bv.SEP)) \
+ Vector2(bv.cell_size * 0.5, bv.cell_size * bv.HERO_GROUND_FRAC)
if bv._world.hero.position.distance_to(expect) > 0.5:
_problems.append("case⑦ 英雄精灵世界落位偏移 pos=%s expect=%s" % [str(bv._world.hero.position), str(expect)])
if _problems.is_empty():
print("[ui_input_probe] PASS — 悬停金框/禁入信号/路径预览/flag_place/chord/dig/英雄落位 七断言全绿(真实 push_input 路径)")
get_tree().quit(0)
else:
for p in _problems:
push_error("[ui_input_probe] FAIL — " + p)
print("[ui_input_probe] FAIL — " + "; ".join(_problems))
get_tree().quit(1)
func _has_action(cell: Vector2i, kind: String) -> bool:
for a in _actions:
if a[0] == cell and a[1] == kind:
return true
return false
func _pick_forbidden(bv, ms) -> Vector2i:
var b = ms.current_board()
for y in b.height:
for x in b.width:
var c := Vector2i(x, y)
if not ms.is_revealed(c) and not bv._diggable.has(c):
return c
return Vector2i(-9, -9)
func _pick_revealed(ms) -> Vector2i:
var b = ms.current_board()
var hero: Vector2i = ms.hero_pos()
for y in b.height:
for x in b.width:
var c := Vector2i(x, y)
if ms.is_revealed(c) and c != hero:
return c
return Vector2i(-9, -9)
# 前沿已开格兜底选取(主角邻域无前沿时全板扫描)
func _pick_frontier(bv, ms) -> Vector2i:
var b = ms.current_board()
for y in b.height:
for x in b.width:
var c := Vector2i(x, y)
if ms.is_revealed(c) and _has_hidden_neighbor(bv, ms, c):
return c
return Vector2i(-9, -9)
func _has_hidden_neighbor(bv, ms, c: Vector2i) -> bool:
for n in bv._board.neighbors8(c):
if not ms.is_revealed(n):
return true
return false
# 合成事件落点 = BoardView 画布原点 + 格中心(cell_center_local 与 _cell_at 同一公式族)
func _pos(bv, c: Vector2i) -> Vector2:
return bv.global_position + bv.cell_center_local(c)
func _push_motion(pos: Vector2) -> void:
var ev := InputEventMouseMotion.new()
ev.position = pos
ev.global_position = pos
main.get_viewport().push_input(ev, true)
func _push_click(pos: Vector2, btn: int) -> void:
var ev := InputEventMouseButton.new()
ev.position = pos
ev.global_position = pos
ev.button_index = btn
ev.pressed = true
main.get_viewport().push_input(ev, true)
func _pre_fail(why: String) -> void:
push_error("[ui_input_probe] FAIL — " + why)
print("[ui_input_probe] FAIL — " + why)
get_tree().quit(1)