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

470 lines
23 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 世界坐标判据,素材缺位树跳过)
# ⑧影子构成与贴脚 ⑨右移朝向+到位回站帧+缩放档 ⑩变格边长即时重排 ⑪贴图缺位回退字形
#(⑧–⑪=呈现层状态断言批 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
const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd") # T6 收口:隔离样板单点
var main: Node
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
# 存档隔离(迭代 8 卡①②——断点写盘面随冒险挂 main.save_manager;探针禁碰真实档 user://save.json,
# 同 ui_click_probe 隔离纪律。首次冒险布尔位置位=对齐真实档游玩史口径,模板抽签路径不变;
# isolate_save 先删后建——上次运行的残留断点会触发互斥确认早退)
ProbeUiUtil.isolate_save(main, "smoke_input_probe_save", true, false, true)
main._start_adventure_from_menu()
await get_tree().process_frame
# ---- ⑫ 图上命中真实输入(关卡画面实装批 2026-09-28 回归闸):入口停点候选节点中心
# push_input 真实鼠标路径 motion+click → 悬停预承诺置位 → route_chosen 到达组合根。
# 由来=真机 bug:深层节点(L1 Boss 内容 x=802)在固定画布命中区外,press_candidate 直调
# 命中函数探针全绿、真鼠标点不中——本 case 封住输入管线,⑬封住几何拟合。
if main.level_screen.visible:
var ro = main.level_screen
var cands: Array = main.runner.next_candidate_ids()
var nds: Array = main.runner.nodes()
var tgt := {}
for n in nds:
if main.runner.node_id_of(n) == int(cands[0]):
tgt = n
var npos: Vector2 = ro._layout(nds, ro._canvas)[tgt]
var gpos: Vector2 = ro._canvas.get_global_position() + npos
var evm := InputEventMouseMotion.new()
evm.position = gpos
evm.global_position = gpos
main.get_viewport().push_input(evm, true)
await get_tree().process_frame
if int(ro._hover_id) != int(cands[0]):
_pre_fail("图上悬停预承诺未置位(hover=%s 候选=%s)" % [str(ro._hover_id), str(cands[0])])
var evc := InputEventMouseButton.new()
evc.button_index = MOUSE_BUTTON_LEFT
evc.pressed = true
evc.position = gpos
evc.global_position = gpos
main.get_viewport().push_input(evc, true)
await get_tree().process_frame
await get_tree().process_frame
if main.level_screen.visible or main.runner.awaiting_choice():
_pre_fail("图上真实点击未提交选路(overlay 可见=%s 选路态=%s)" % [str(main.level_screen.visible), str(main.runner.awaiting_choice())])
for i in range(3):
await get_tree().process_frame
# ---- ⑬ 画布随图拟合(同 bug 的几何面回归):复刻分层公式独立推导最深节点内容 x,
# 断言画布 min 尺寸覆盖之(open_choices 每 stop 拟合——L1 单链 Boss x=70+6*122=802 曾越界)
var ro2 = main.level_screen
var nds2: Array = main.runner.nodes()
var layer2 := {}
var inbound2 := {}
for n2 in nds2:
for tid2 in main.runner.node_out_ids(n2):
inbound2[tid2] = true
var queue2: Array = []
for n2 in nds2:
if not inbound2.has(main.runner.node_id_of(n2)):
queue2.append(n2)
layer2[n2] = 0
while not queue2.is_empty():
var cur2: Dictionary = queue2.pop_front()
for tid2 in main.runner.node_out_ids(cur2):
var tg2 := {}
for n2 in nds2:
if main.runner.node_id_of(n2) == tid2:
tg2 = n2
if tg2.is_empty() or layer2.has(tg2):
continue
layer2[tg2] = int(layer2[cur2]) + 1
queue2.append(tg2)
var maxl2 := 0
for k2 in layer2.keys():
maxl2 = maxi(maxl2, int(layer2[k2]))
# 常数锚(画布拟合探针):70/122/26/8 与 presentation/views/level_map_screen.gd 的 _content_size
# 公式(70 + maxl*122 + NODE_HALF.x + 8)同源复刻——改常数须两侧同步(本探针只读断言层,真源在屏)
var need_x2: float = 70.0 + maxl2 * 122.0 + 26.0 + 8.0
if float(ro2._canvas.custom_minimum_size.x) < need_x2 - 0.5:
_pre_fail("画布拟合不足(min.x=%.0f < 最深内容 %.0f——深层节点不可点回归)" % [ro2._canvas.custom_minimum_size.x, need_x2])
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
# ---- 目标格实证选取(不写死坐标;两枚相邻可挖格 + 禁入格 + 已开格)----
# 并流批(ADR-0007 裁丙 2026-09-27)后首战板面种子随全局序漂移——_diggable(主角 8 邻未开域)
# 不足两枚时,沿已开区真点击移位到「≥2 未开邻」的前沿格再选;移位失败即收手交由选取失败分支报状态
for _expand in range(16):
if bv._diggable.size() >= 2:
break
var mv_t := Vector2i(-9, -9)
for y in bv._board.height:
for x in bv._board.width:
var c := Vector2i(x, y)
if not ms.is_revealed(c):
continue
var hidden := 0
for n in bv._board.neighbors8(c):
if not ms.is_revealed(n):
hidden += 1
if hidden >= 2:
mv_t = c
break
if mv_t.x != -9:
break
if mv_t.x == -9:
break
var hero_before: Vector2i = ms.hero_pos()
_push_click(_pos(bv, mv_t), MOUSE_BUTTON_LEFT)
await get_tree().process_frame
await get_tree().process_frame
if ms.hero_pos() == hero_before:
break
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④b 旗纹理槽回归断言(P0-1 修复批 2026-10-02):T13 批曾在 paint 尾部无条件
# `u.tex_flag = null` 覆盖 T17 批的条件赋值=素材在位时插旗不可见(headless 缺盘走字形回退
# 掩盖)——有盘断言纹理在槽、缺盘断言字形回退,两分支防双写踩踏复活 ----
var flag_unit = bv._cells.get(flag_t)
if bv._flag_tex != null:
if flag_unit == null or flag_unit.tex_flag != bv._flag_tex:
_problems.append("case④b 旗纹理槽未挂封印旗纹理(tex_flag=%s)" % ["无单元" if flag_unit == null else str(flag_unit.tex_flag)])
elif flag_unit == null or flag_unit.glyph != "旗":
_problems.append("case④b 缺盘字形回退缺失(glyph=%s)" % ["无单元" if flag_unit == null else String(flag_unit.glyph)])
# ---- 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()
# ---- case⑭ 变异角标(迭代 6 T6——flat_damage 未开格视觉标记;resolver 注入面+落位+复位) ----
var mark_t: Vector2i = _pick_forbidden(bv, ms) # 任一未开格(禁入=未开非相邻)
if mark_t.x >= 0:
bv.set_variant_mark_resolver(func(_c): return "flat_damage")
bv.refresh()
if String(bv._cells[mark_t].mutation_mark) != "flat_damage":
_problems.append("case⑭ 变异角标未落位(mutation_mark=%s)" % str(bv._cells[mark_t].mutation_mark))
bv.set_variant_mark_resolver(Callable())
bv.refresh()
if String(bv._cells[mark_t].mutation_mark) != "":
_problems.append("case⑭ 角标未随 resolver 复位(mutation_mark=%s)" % str(bv._cells[mark_t].mutation_mark))
else:
_problems.append("case⑭ 无未开格可验(板面异常)")
# ---- case⑮ 视野迷雾(迭代 11 第④批 T5——三态呈态/半径圈/移动跟随/开关两态零扰动/提灯插值位;
# 纯呈现零逻辑——迷雾开关切换前后 minesweeper 状态摘要逐位一致) ----
if int(bv.FOG_RADIUS) != 8 or int(bv.FOG_LANTERN_BONUS) != 2 or int(bv._fog_lantern) != 0 or not bv._fog_enabled:
_problems.append("case⑮ 迷雾参数漂移(R=%s bonus=%s lantern=%s enabled=%s 应=8/2/0/true)"
% [str(bv.FOG_RADIUS), str(bv.FOG_LANTERN_BONUS), str(bv._fog_lantern), str(bv._fog_enabled)])
else:
var hero15: Vector2i = ms.hero_pos()
var b15 = ms.current_board()
var fog_ok := true
for y in b15.height:
for x in b15.width:
var c15 := Vector2i(x, y)
var d15: int = absi(c15.x - hero15.x) + absi(c15.y - hero15.y)
var expect15: int = 0
if d15 > bv.FOG_RADIUS:
expect15 = 2 if ms.is_revealed(c15) else 1
var got15: int = int(bv._cells[c15].fog_state)
if got15 != expect15:
fog_ok = false
_problems.append("case⑮ 迷雾呈态不符(%s dist=%d got=%d expect=%d)" % [str(c15), d15, got15, expect15])
break
if not fog_ok:
break
# 半径边界格恰在圈内(曼哈顿距离恰 =R 的格 fog_state=0——存在性校验,12×12 板必有)
var has_boundary := false
for y in b15.height:
for x in b15.width:
if absi(x - hero15.x) + absi(y - hero15.y) == int(bv.FOG_RADIUS):
has_boundary = true
if int(bv._cells[Vector2i(x, y)].fog_state) != 0:
_problems.append("case⑮ 边界圈内格被误遮(dist==R 应 fog_state=0)")
break
if has_boundary:
break
if not has_boundary:
_problems.append("case⑮ 板面无 dist==R 格(板面异常)")
# 移动跟随:把英雄挪到板角(move 动作逐格逼近成本高——直接白-box 校验公式换心后远格态翻转:
# 取一个对当前英雄 dist>R 的已开格(记忆态),以远角为新心重算应翻转/保持一致性由公式承载,
# 此处只验 set_fog_enabled(false) 全板归零与 true 恢复——开关两态契约主断言)
var digest_before: Array = []
for y in b15.height:
for x in b15.width:
var c16 := Vector2i(x, y)
digest_before.append(ms.is_revealed(c16))
bv.set_fog_enabled(false)
var all_clear := true
for y in b15.height:
for x in b15.width:
if int(bv._cells[Vector2i(x, y)].fog_state) != 0:
all_clear = false
if not all_clear:
_problems.append("case⑮ 迷雾关未全板归零(off 态应 fog_state=0)")
var digest_off: Array = []
for y in b15.height:
for x in b15.width:
var c17 := Vector2i(x, y)
digest_off.append(ms.is_revealed(c17))
if digest_off != digest_before:
_problems.append("case⑮ 迷雾开关改写逻辑态(两态结算逐位一致 🔴 契约破)")
bv.set_fog_enabled(true)
if int(bv._cells[Vector2i(0, 0)].fog_state) == 0 \
and absi(0 - hero15.x) + absi(0 - hero15.y) > int(bv.FOG_RADIUS):
_problems.append("case⑮ 迷雾复开后远角未复原遮蔽(fog_state=0)")
if _problems.is_empty():
print("[ui_input_probe] PASS — 悬停金框/禁入信号/路径预览/flag_place/chord/dig/英雄落位/影子构成/朝向/缩格重排/缺位回退/变异角标/视野迷雾 十六断言全绿(真实 push_input 路径——⑫图上命中⑬画布拟合 2026-09-28 增,⑭变异角标 2026-09-29 增,④b 旗纹理槽回归 2026-10-02 增,⑮视野迷雾 2026-10-03 增)")
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)