Files
gplan-2/game/smoke/ui_input_probe.gd
T
DKChan 442156bfb7 feat(t6): flat_damage 变异格视觉标记——危险红角标+resolver 注入面
①board_cell_unit 增 mutation_mark 程序化角标(flat_damage=左下角危险红三角 0.28 格边,危险红=「开此格有代价」语义位与雷族同源;tex_mutation 纹带槽留给变异纹带六型自产批——代拍板二选一择角标待追认)
②board_view 增 set_variant_mark_resolver 只读回调(同 mask_resolver 注入先例)+_paint_cell 未开格赋值(已开=威胁已兑现标记退位;回调缺位=无标记回退态)
③main 增 _variant_mark_lookup(flat_damage 效应解析,变异数据行动零特判)+接线
④ui_input_probe 扩 case⑭(角标落位+resolver 复位断言,十三→十四断言)
轻闸=冒烟 576/0+三探针 PASS;交叉对拍=master HEAD MC 与 worktree T0 锚 400/400 逐位一致(重锚验证闭合,主区在飞文件全非行为面)
2026-09-29 04:21:26 +08:00

390 lines
19 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
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
# ---- ⑫ 图上命中真实输入(关卡画面实装批 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]))
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⑤ 双击 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⑭ 无未开格可验(板面异常)")
if _problems.is_empty():
print("[ui_input_probe] PASS — 悬停金框/禁入信号/路径预览/flag_place/chord/dig/英雄落位/影子构成/朝向/缩格重排/缺位回退/变异角标 十四断言全绿(真实 push_input 路径——⑫图上命中⑬画布拟合 2026-09-28 增,⑭变异角标 2026-09-29 增)")
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)