289 lines
14 KiB
GDScript
289 lines
14 KiB
GDScript
# 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 世界坐标判据,素材缺位树跳过)
|
||
# ⑧影子构成与贴脚 ⑨右移朝向+到位回站帧+缩放档 ⑩变格边长即时重排 ⑪贴图缺位回退字形
|
||
#(⑧–⑪=呈现层状态断言批 2026-09-26——Node2D 展示几何/状态面单元化,A 档方案)。
|
||
# 事件路径: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 世界内,
|
||
# 落位 = 格×步距 + 脚底锚(横=格中线/纵=格底往上 25% 线=HERO_GROUND_FRAC 自底边口径——「脚踩
|
||
# 格子 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 * (1.0 - 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)])
|
||
# ---- case⑧ 影子构成与贴脚(小人立体感批回归闸 2026-09-26):椭圆影=英雄子件、中心贴脚锚
|
||
#(上批 (0,2) 下坠脱脚用户报缺陷)、z 相对回落格面之上英雄之下、黑色半透明、渐变纹理组件
|
||
# 合成;父子 use_parent_material 双挂(结算期随世界去饱和与格面同口径)。 ----
|
||
if bv._world.hero != null:
|
||
var kids: Array = bv._world.hero.get_children()
|
||
if kids.size() != 1 or not (kids[0] is Sprite2D):
|
||
_problems.append("case⑧ 影子子件缺失或类型不对(children=%d)" % kids.size())
|
||
else:
|
||
var sh: Sprite2D = kids[0]
|
||
if sh.position != Vector2.ZERO:
|
||
_problems.append("case⑧ 影子未贴脚(pos=%s 应=ZERO)" % str(sh.position))
|
||
if sh.z_index != -1:
|
||
_problems.append("case⑧ 影子 z 相对档不对(%d 应=-1)" % sh.z_index)
|
||
if absf(sh.modulate.a - 0.35) > 0.01:
|
||
_problems.append("case⑧ 影子透明度漂移(%s 应=0.35)" % str(sh.modulate.a))
|
||
if not (sh.texture is GradientTexture2D):
|
||
_problems.append("case⑧ 影子纹理非径向渐变组件(%s)" % str(sh.texture))
|
||
if not sh.use_parent_material or not bv._world.hero.use_parent_material:
|
||
_problems.append("case⑧ 去饱和材质链漏挂(shadow=%s hero=%s)" % [str(sh.use_parent_material), str(bv._world.hero.use_parent_material)])
|
||
# ---- case⑨ 右移朝向+到位回站帧+缩放档(贴图批回归闸):素材基准朝左——向右移动 flip_h=true;
|
||
# 到位(0.3s 墙钟 > 位移 Tween 140ms)纹理回站帧、scale 恒 1.5 均匀档。无可达右邻已开格跳过。 ----
|
||
if bv._world.hero != null:
|
||
var hp9: Vector2i = ms.hero_pos()
|
||
var mv9 := Vector2i(-9, -9)
|
||
for n in bv._board.neighbors8(hp9):
|
||
if n.x == hp9.x + 1 and ms.is_revealed(n):
|
||
mv9 = n
|
||
break
|
||
if mv9.x == -9:
|
||
print("[ui_input_probe] SKIP case⑨(无可达右邻已开格)")
|
||
else:
|
||
_push_click(_pos(bv, mv9), MOUSE_BUTTON_LEFT)
|
||
await get_tree().create_timer(0.3).timeout
|
||
var sp9: Sprite2D = bv._world.hero
|
||
if not sp9.flip_h:
|
||
_problems.append("case⑨ 右移后未翻转朝向(flip_h=false,素材基准朝左)")
|
||
if sp9.texture != bv._hero_tex_idle:
|
||
_problems.append("case⑨ 到位未回站帧(texture=%s)" % str(sp9.texture))
|
||
if sp9.scale != Vector2.ONE * bv.HERO_SPRITE_SCALE:
|
||
_problems.append("case⑨ 缩放档漂移(%s 应=%s 均匀)" % [str(sp9.scale), str(bv.HERO_SPRITE_SCALE)])
|
||
# ---- case⑩ 变格边长即时重排(窗口缩放路径回归闸):set_cell_area 换小格 → 英雄即时落新脚锚、
|
||
# 缩放均匀不残弹跳形;还原 → 46px 档复验。 ----
|
||
if bv._world.hero != null:
|
||
var hp10: Vector2i = ms.hero_pos()
|
||
bv.set_cell_area(Vector2(300, 300))
|
||
var expect10: Vector2 = Vector2(hp10) * (bv.cell_size + float(bv.SEP)) \
|
||
+ Vector2(bv.cell_size * 0.5, bv.cell_size * (1.0 - bv.HERO_GROUND_FRAC))
|
||
if bv._world.hero.position.distance_to(expect10) > 0.5 or bv._world.hero.scale != Vector2.ONE * bv.HERO_SPRITE_SCALE:
|
||
_problems.append("case⑩ 缩格后英雄未即时重排(pos=%s expect=%s scale=%s)" % [str(bv._world.hero.position), str(expect10), str(bv._world.hero.scale)])
|
||
bv.set_cell_area(bv.size)
|
||
var expect10b: Vector2 = Vector2(hp10) * (bv.cell_size + float(bv.SEP)) \
|
||
+ Vector2(bv.cell_size * 0.5, bv.cell_size * (1.0 - bv.HERO_GROUND_FRAC))
|
||
if bv._world.hero.position.distance_to(expect10b) > 0.5:
|
||
_problems.append("case⑩ 还原后英雄未复位(pos=%s expect=%s)" % [str(bv._world.hero.position), str(expect10b)])
|
||
# ---- case⑪ 贴图缺位回退(字形「人」回归闸):texture 置 null + refresh → 英雄格数字 0 时字形
|
||
# 必须回退「人」(数字格仍显数字——回退判据与抑制判据同源);验毕还原纹理重绘。 ----
|
||
if bv._world.hero != null:
|
||
var hp11: Vector2i = ms.hero_pos()
|
||
var u11 = bv._cells[hp11]
|
||
var tex_saved: Texture2D = bv._hero_tex_idle
|
||
bv._hero_tex_idle = null
|
||
bv.refresh()
|
||
var digit11: int = int(ms.reveal_record(hp11).get("digit", 0))
|
||
if digit11 == 0:
|
||
if u11.glyph != "人":
|
||
_problems.append("case⑪ 贴图缺位未回退字形「人」(glyph=%s)" % u11.glyph)
|
||
elif u11.glyph != str(digit11):
|
||
_problems.append("case⑪ 缺位态数字格字形被破坏(glyph=%s 应=%d)" % [u11.glyph, digit11])
|
||
bv._hero_tex_idle = tex_saved
|
||
bv.refresh()
|
||
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)
|