merge: 棋盘 Node2D 渲染迁移 Phase A——D 区棋盘本体迁 Node2D,公共面契约零改造(用户授权推进 2026-09-23,台账 144)

This commit is contained in:
DKChan committed 2026-09-24 01:35:02 +08:00
commit 1679d25bee
11 files changed
+500 -135

No files matched your search

@@ -251,3 +251,4 @@ HUD框架站在玩家与全部玩法系统之间:挖掘期它是输入的翻
| v0.6 | 2026-09-20 | **敌人卡形态裁决收口批(与 battle.md / 战斗表现 / 06 清单同批,台账 99)**:全站桩形态拍板——B 区主角与敌人均为无框立绘+底座、状态小图标挂血条正下方,英雄卡/敌人卡退役;B 区行、敌人呈现义务行(载体由卡改立绘,意图占位/共享层数各立绘同值/死亡灰化退场等义务面平移)、下游战斗表现交付行、就地更新示例措辞四处同步;攻击值等规划数据仍归 C 区数据框(C 区不动)。呈现快照数据面零改动(消费字段不变,只换呈现载体) |
| v0.7 | 2026-09-20 | **表现层三批落地 + 义务清单补行(代码同日三批:样式收敛 / 站桩改造 / 结算演出链)**:呈现义务来源清单补「关卡节点地图」行(节点链进度常显 + 选路覆盖层只读呈现——此前漏登记);新增结算演出覆盖层(跨区弹道/光带载体,兑现演出因果可视);减动效承诺落地(UiKit.motion 单点包装全部呈现层 tween 时长,开启时长减半、可挖域呼吸停格;切片期入口 = TunePanel 开关,正式设置层归设置批)。代码侧站桩载体(StandeeView)与卡片退役随 v0.6 裁决执行 |
| v0.8 | 2026-09-23 | **停点前移批(冒险入口流程倒置修复,用户 2026-09-23 拍板;上位裁决 `docs/architecture/adr-0006-board-node2d-rendering.md` 与 `docs/architecture/board-node2d-migration-plan.md` §1,代码实装另批并行)**:① 战斗屏布局四区 → 三区——删 A 区关卡进度条行(LevelStripView 退役删除,节点链进度呈现移出战斗屏、只留关卡画面);C/D 区纵向 520 → 548;装板校验变量与实算同步(棋盘区高 504、可用高度 488);② 呈现义务来源清单「关卡节点地图」行改承载口径——节点链进度由关卡画面选路覆盖层节点态承载(暗金已过 / 金当前 / 候选下一跳通道亮金 / Boss 旗朱描边),不另设进度条组件;③ 「四区」措辞全线改「三区」(负责表、节题、结构冻结、验收、下游战斗表现交付行),双舞台常驻语义不动。**批内代拍板三项待追认**:① B/C/D 字母保持不重编(字母是跨文档/代码注释的承载标识);② A 区 28px 纵向空间划归 C/D 底行(520 → 548;1920 档换算仍归表现层升级批);③ 关卡画面进度呈现由选路覆盖层节点态直接承载、不另设进度条组件 |
| v0.9 | 2026-09-23 | 渲染技术注记(棋盘 Node2D 迁移批 Phase A,代码实装同批;上位裁决 ADR-0006):棋盘本体 Node2D 化——BoardView 内部由 GridContainer+逐格控件改为 Control 壳 + SubViewport + Node2D 世界,类名/信号/公共方法契约不变、探针零改造;三区布局与性能预算(同屏 draw calls < 200)不变;程序化占位视觉等价平移,素材终稿按 art-bible §8.2 格面 32×32 暂定档槽位 1:1 换入 |
+1
View File
@@ -404,3 +404,4 @@
| 2026-09-16 | 格面资源批 UI 欠账收口(随 2026-09-15 可选类道具删除的代码收口同步文档):① 布局四区化——删 E 区道具面板行、布局决策 2(道具窗口形态=右栏原位激活)整段删除(编号留空防交叉引用失锚)、ASCII 线框图重绘为 A/B/C/D 四区(D 区横向加宽 ~1036px)、A 区节点链示例改文字签槽实际渲染(战/赏/疗/王);② States 删道具窗口态与零库存空态(「道包空空」);③ 结算口径五段化(授予→增益/减益→战斗→衰减→判定)与明细行 10 类封闭协议(删 15 类行、四类行旧口径;「奖励未生效」行保留);④ 交互/动效/事件表删道具窗口行、跳过按钮、`ux_window_choice`、`submit_window_selection`;⑤ 数据源登记删 `query_inventory` 与窗口载荷,奖励身份按现行 `query_identity` 只读回调口径校正;⑥ 验收清单删窗口断言、五区改四区、未生效类行改现行两型(溢出未生效/奖励未生效);⑦ 关卡末清库反馈义务改载魔法道具背包(随魔法道具 P0 落地,本批不设计具体呈现);⑧ OQ7(道具窗口原位激活)删除、OQ 重编号,「囤刀待机」等旧可选类措辞清理。本 spec 不装载被删除机制的呈现义务 |
| 2026-09-20 | **敌人卡形态裁决收口(2026-09-20 用户看对照样张拍板,06 待办「敌人卡形态」解决移除——台账 99)**:全站桩形态定稿——① B 区主角与敌人均为无框立绘+底座(站桩式),**英雄卡/敌人卡组件整体退役,卡片式不再作为候选**;主角血条悬浮头顶、状态效果小图标一排挂血条正下方,敌人血条+顺位徽标悬浮头顶、状态小图标同款挂血条下;攻击值等规划数据仍归 C 区数据框(C 区不动);② 布局表 B 区行、Component Inventory 英雄卡/敌人卡两行随裁决改写(敌方 side 池共享层数各立绘同值、意图动画占位等既有语义平移到站桩载体,不随形态改变);③ ASCII 线框图 B 区重绘(旧"意图:爪击/环点"示意早已随行为环退役,本次一并清掉);④ OQ4 改写为裁决已收口注记,对照样张存档 `production/mockups/`;呈现义务细节重述(受击/退场演出挂点等)随表现层升级批执行 |
| 2026-09-23 | **停点前移批(冒险入口流程倒置修复,用户 2026-09-23 拍板;上位裁决 `docs/architecture/adr-0006-board-node2d-rendering.md` 与 `docs/architecture/board-node2d-migration-plan.md` §1,代码实装另批并行)**:① 布局四区 → 三区——删布局表 A 区关卡进度条行与 Component Inventory LevelStripView 行(LevelStripView 退役删除,节点链进度只留关卡画面,战斗屏零进度承载);C/D 底行 520 → 548;ASCII 线框图重绘为 B/C/D 三区;② 新流程口径落文——关卡画面(选路覆盖层常驻形态)为关卡内常驻枢纽:Navigation Position 图、到达语境、Entry 表、信息层级第 5 层、Data Requirements 节点图行改「关卡开始即出图选路、战后返回关卡画面再选路」;③ AC 四区措辞改三区。**批内代拍板三项待追认**:① B/C/D 字母保持不重编;② 28px 纵向空间划归 C/D 底行(520 → 548);③ 关卡画面进度呈现由选路覆盖层节点态承载(暗金已过 / 金当前 / 亮金候选通道 / 旗朱 Boss)、不另设进度条组件 |
| 2026-09-23 | 实现注记(棋盘 Node2D 迁移批 Phase A,代码实装同批;上位裁决 ADR-0006):棋盘渲染 Node2D 化(BoardView = Control 壳 + SubViewportContainer + SubViewport + Node2D 世界,信号/click_cell/_cells 契约保持,新增 push_input 端到端输入探针);格尺寸公式锚点不变(set_cell_area = floor((min(宽,高) − 预留 − (N−1)×格间距) / N) 单点可调),布局与交互语义零改动 |
+3 -3
View File
@@ -2,8 +2,8 @@
## Document Status
- Version: 1.2
- Last Updated: 2026-09-16(格面资源批 UI 欠账收口:分层图与层表中 B4 旧体系变体名改现行规范名"地图战斗资源"——代号前缀按图内旧代号风格保留)
- Version: 1.3
- Last Updated: 2026-09-23(棋盘 Node2D 渲染迁移 Phase A:D1 层表 BoardView 形态注记——引 ADR-0006)
- Engine: Godot 4.7.2-stable(GDScript 单语言;2026-09-07 锁定)
- Review Mode: solo(`production/review-mode.txt`,2026-09-12 用户拍板——TD/LP 门禁按 solo 跳过,用户授权架构产出自主推进、迭代期回调)
- GDDs Covered: game-concept · game-pillars · systems-index · d2-data-config · a3-action-economy · a4-terrain-variation · b3-status-effect · a1-board-generation · a2-minesweeper-rules · b2-enemy-system · b4-reward-item-economy · b1-settlement(12 份;A5/C1–C4/B5/D3–D6 未设计,按 systems-index 依赖分层预留)
@@ -120,7 +120,7 @@
| 模块 | Owns | Exposes | Consumes | 引擎 API |
|---|---|---|---|---|
| **D1 HUD**(切片 lite 已拆 9 单元:BoardView / HUDView / HudPresenter / BattleLogView / ItemWindowOverlay / NodeOverlay / LevelStripView / TunePanel / UiKit + 组合根 main.gd) | 全部 Control/CanvasLayer、输入→意图翻译(**唯一提交路径 = settle_batch,不直调 A3**)、呈现义务台账(A2×6 / B2×4 / B3×4 / B4×5 / B1×3) | 视图单元接口(信号外发、setter 只读注入) | A2 compute_move_path / ActionOutcome;A3 ap/rap;B1 步骤流行协议(数据源);ConfigDB display_name | **MEDIUM**:Control / CanvasLayer / draw_* / InputEvent / RichTextLabel——对照 breaking-changes.md 4.5–4.7 条目(CanvasItem 线条无抗锯齿羽化、设备 ID 常量、RichTextLabel 图片参数两轮变化) |
| **D1 HUD**(切片 lite 已拆 9 单元:BoardView / HUDView / HudPresenter / BattleLogView / ItemWindowOverlay / NodeOverlay / LevelStripView / TunePanel / UiKit + 组合根 main.gd) | 全部 Control/CanvasLayer(例外:BoardView 棋盘本体 = Control 壳 + SubViewportContainer + SubViewport + Node2D 世界〔BoardWorld/CellUnit〕,探针契约面不变——ADR-0006 棋盘 Node2D 迁移批 Phase A 2026-09-23)、输入→意图翻译(**唯一提交路径 = settle_batch,不直调 A3**)、呈现义务台账(A2×6 / B2×4 / B3×4 / B4×5 / B1×3) | 视图单元接口(信号外发、setter 只读注入) | A2 compute_move_path / ActionOutcome;A3 ap/rap;B1 步骤流行协议(数据源);ConfigDB display_name | **MEDIUM**:Control / CanvasLayer / draw_* / InputEvent / RichTextLabel——对照 breaking-changes.md 4.5–4.7 条目(CanvasItem 线条无抗锯齿羽化、设备 ID 常量、RichTextLabel 图片参数两轮变化) |
| **battle_presentation**(B5,垂直切片后) | 结算动画 / 伤害数字 / 战斗舞台 | (未设计——前置"战斗表现美术测试"锁双舞台陷阱) | B1 步骤流(回放脚本源)、D1 | **MEDIUM**:AnimationPlayer(String→StringName 变更)/ Tween |
| **audio**(D4,Alpha) | 音频挂点 | (挂点 = B1 明细行类型流;负反馈聚合——单结算至多一次) | B1 流水 | AudioStreamPlayer(4.7 area_mask 默认 1→0 注意) |
| **tutorial**(D5,Alpha) | 教学条目 | (口径来源:A2/A3/B3/B1 各教学条目) | 各上游 | 无 |
@@ -0,0 +1,102 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 棋盘 Node2D 迁移批 Phase A 2026-09-23:CellUnit——BoardView SubViewport 世界内逐格渲染件(Node2D)。
# 固定叠层序(ADR-0006 Decision 4):砖面底 < 变异纹带 < 数字/徽章 < 旗 < 焦点高亮——本批全部程序化
# _draw 等价平移现行占位视觉;每层留 texture 槽位(art-bible §8.2 格面 32×32 暂定档——终稿 1:1 换入,
# 本批不实现纹理加载)。无 class_name(类名缓存面纪律——BoardView 经 preload 使用)。
# 字体:font 槽位缺省 = ThemeDB.fallback_font(与旧 Button 缺省主题字体同源);注入挂点归
# BoardView.set_world_font(SubViewport 内主题不自动继承主场景,Phase B 复用面)。
# 行为等价(缺陷①回归约束):格件常驻定长、逐格重绘由 BoardView 驱动,本类零游戏状态——渲染数据
# 全部由 BoardView 每次 paint 写入(bg_sb/glyph/badge),呼吸微光由 BoardView 就地改 bg_sb 边框。
extends Node2D
var cell := Vector2i.ZERO # 板坐标(诊断观测面)
var edge := 46.0: # 格边长(BoardView.set_cell_area 单点驱动)
set(v):
edge = v
queue_redraw()
var bg_sb: StyleBoxFlat = null # 格底 StyleBox(BoardView 每次 paint 重建;呼吸微光就地改边框)
var glyph := "": # 文字位——数字/雷/旗/人/?(现行视觉旗与数字互斥,同位承载)
set(v):
glyph = v
queue_redraw()
var glyph_color := UiKit.INK: # 文字色(墨族/雷朱/旗朱/小人蓝)
set(v):
glyph_color = v
queue_redraw()
var badge_glyph := "": # 奖励徽章文字(右下角金——旗之下、变异纹之上、不进数字区)
set(v):
badge_glyph = v
queue_redraw()
var dot_visible := false: # BFS 路径预览点迹(中心 6×6 小人蓝——旧 ColorRect 等价)
set(v):
dot_visible = v
queue_redraw()
var hl_visible := false: # 焦点高亮(悬停态边框覆盖层)
set(v):
hl_visible = v
queue_redraw()
var hl_color := UiKit.GOLD: # 高亮色(金目标/旗朱/禁入灰禁红)
set(v):
hl_color = v
queue_redraw()
var font: Font = null # 字体槽位(null = ThemeDB.fallback_font——BoardView.set_world_font 注入)
# ── 素材槽位(本批全 null = 程序化占位服役;art-bible §8.2 格面 32×32 暂定档)──
var tex_base: Texture2D = null # ① 砖面底
var tex_mutation: Texture2D = null # ② 变异纹带
var tex_glyph: Texture2D = null # ③ 数字
var tex_badge: Texture2D = null # ③' 徽章
var tex_flag: Texture2D = null # ④ 旗
func _draw() -> void:
var rect := Rect2(Vector2.ZERO, Vector2(edge, edge))
# ① 砖面底(占位 = StyleBoxFlat:格底色 + 暗化描边 + 3px 圆角——旧 Button normal 样式等价;
# 呼吸微光/主角蓝框由 BoardView 改 bg_sb 后触发重绘)
if tex_base != null:
draw_texture_rect(tex_base, rect, false)
elif bg_sb != null:
draw_style_box(bg_sb, rect)
# ② 变异纹带(占位批次无程序化纹带——槽位先行,终稿换入即用)
if tex_mutation != null:
draw_texture_rect(tex_mutation, rect, false)
# ③ 数字/徽章(数字 ≥ 格高 50% 墨族对格底 ≥4.5:1;徽章右下角金,遮蔽态 = "?")
if glyph != "":
if tex_glyph != null:
draw_texture_rect(tex_glyph, rect, false)
else:
_draw_glyph(glyph, glyph_color, maxi(14, int(edge * 0.5)))
if badge_glyph != "":
if tex_badge != null:
draw_texture_rect(tex_badge, rect, false)
else:
_draw_badge(badge_glyph)
# ④ 旗(现行视觉旗文字在 ③ 文字位承载——独立槽位留给终稿旗图)
if tex_flag != null:
draw_texture_rect(tex_flag, rect, false)
# ⑤ 路径点迹 + 焦点高亮(悬停态覆盖层,不参与命中)
if dot_visible:
var ds := 6.0
draw_rect(Rect2(Vector2((edge - ds) * 0.5, (edge - ds) * 0.5), Vector2(ds, ds)), UiKit.HERO_BLUE, true)
if hl_visible:
draw_rect(Rect2(1, 1, edge - 2.0, edge - 2.0), hl_color, false, 2.0) # 2px 内描边(旧 hl Panel 等价)
# 文字位绘制:水平居中 + 垂直居中(基线 = (格高 − 字高)/2 + 上伸部——旧 Button 居中对齐等价)
func _draw_glyph(g: String, col: Color, fs: int) -> void:
var f := font if font != null else ThemeDB.fallback_font
if f == null:
return
var asc := f.get_ascent(fs)
var desc := f.get_descent(fs)
draw_string(f, Vector2(0.0, (edge - (asc + desc)) * 0.5 + asc), g,
HORIZONTAL_ALIGNMENT_CENTER, edge, fs, col)
# 徽章绘制:右下角金(旧 Label BOTTOM_RIGHT 锚点等价——文字贴角内缩 2–3px)
func _draw_badge(g: String) -> void:
var f := font if font != null else ThemeDB.fallback_font
if f == null:
return
var fs := maxi(10, int(edge * 0.28))
var sz := f.get_string_size(g, HORIZONTAL_ALIGNMENT_LEFT, -1, fs)
draw_string(f, Vector2(edge - sz.x - 2.0, edge - 3.0), g, HORIZONTAL_ALIGNMENT_LEFT, -1, fs, UiKit.GOLD)
@@ -0,0 +1 @@
uid://c46ettm7ap332
@@ -2,12 +2,20 @@
# 实现 GDD:design/ux/battle.md(战斗屏 D 区 BoardView——格态/悬停预承诺光标/可挖域呼吸微光/
# BFS 路径预览/奖励徽章/结算期去饱和锁输入)。
# Date: 2026-09-12(battle.md 战斗屏改造批;基础 = 2026-09-11 HUD框架-lite 版)
# 棋盘视图:格子渲染(art-bible 明亮风、数字高对比、
# 绘制优先级 数字>小人>旗>奖励徽章>底色)+ 玩家输入意图翻译 + 悬停预承诺三态。
# 棋盘 Node2D 迁移批 Phase A 2026-09-23:渲染本体由「GridContainer + 逐格 Button」平移为
# 「Control 壳 + SubViewportContainer + SubViewport + BoardWorld(Node2D) + CellUnit(Node2D)」
# (ADR-0006 / docs/architecture/board-node2d-migration-plan.md §2)。对外契约逐项保持:
# 类名/extends/三信号/click_cell/_cells 键面(Vector2i→格单元,值 Button→CellUnit)/
# board()/set_desaturated()/set_identity_resolver()/set_variant_mask_resolver()/set_cell_area()/
# rebuild()/refresh()/set_dig_glow_enabled()——探针与组合根零改造。
# 命中 = 壳层 _gui_input + 板级公式单点换算(ADR-0006 Decision 3,_cell_at 为唯一公式函数;
# SubViewportContainer 逻辑分辨率恒等于板像素尺寸(stretch 1:1,无缩放比),换算只需减居中偏移)。
# 程序化占位视觉等价平移(数字三档/旗/徽章/危险红/变异遮蔽 "?"/呼吸微光/悬停/路径预览/R₀ 豁免);
# CellUnit 叠层 texture 槽位留好(本批不接纹理资产)。新增只读属性 cell_size(旧私有名 _cell 契约化出口)。
# 高内聚边界:只读 排雷玩法 查询面(+地图战斗资源 身份解析回调)做渲染与意图识别,动作经信号外发——不调用核心写接口
# (settle_batch 归组合根编排),不持久持有战斗态(minesweeper 引用随 refresh 更新,仅作只读查询)。
# 缺陷①回归约束:格子控件常驻、逐格重绘在 refresh 内完成,无布局回流(格控件/子标记定长,不增删)。
# ADR-0003 形态约束:GridContainer + 逐格常驻控件(探针耦合 _cells/click_cell——禁盲目迁移 _draw)。
# 行为等价:格件常驻、逐格重绘在 refresh 内完成,无布局回流(缺陷①回归约束——CellUnit 定长不增删,
# 重建仅在换板时发生)。
class_name BoardView
extends Control
@@ -15,6 +23,9 @@ signal action_requested(cell: Vector2i, kind: String) # kind ∈ {dig, move, f
signal hover_forbidden(cell: Vector2i) # 非相邻未开格悬停(首遇 toast 归组合根去重)
signal chord_attempted(cell: Vector2i) # 双击快开尝试(chord 肌肉记忆提示锚)
const _CELL_UNIT := preload("res://presentation/views/board_cell_unit.gd")
const _BOARD_WORLD := preload("res://presentation/views/board_world.gd")
const COL_BRICK := UiKit.BRICK # 未开——掩尘完整地砖(色板原语 #C9AF83)
const COL_FLOOR := UiKit.FLOOR # 已开——裸土 #B39A78(碎裂裸露地面)
const COL_DIGIT := UiKit.INK # 数字——墨族 #33261B(对格底 ≥4.5:1 硬指标)
@@ -34,11 +45,13 @@ const CURSOR_DIG := Input.CURSOR_CROSS # 挖——镐形自绘纹理
const CURSOR_FLAG := Input.CURSOR_POINTING_HAND # 旗——旗形自绘纹理载体
const CURSOR_MOVE_SEM := Input.CURSOR_ARROW # 移动——箭头(命名避开 Control.CURSOR_MOVE 原生成员遮蔽)
const CHORD_WINDOW_MS := 350
const SEP := 2 # 格间距(旧 GridContainer v/h_separation 同值——格尺寸公式与命中换算共用)
var _grid: GridContainer
var _cells := {} # Vector2i → Button
var _cell_sb := {} # Vector2i → StyleBoxFlat(呼吸微光就地改边框色)
var _marker := {} # Vector2i → {"dot": ColorRect, "badge": Label, "hl": Panel}
var _wrap: CenterContainer
var _svp_container: SubViewportContainer
var _viewport: SubViewport
var _world: Node2D # BoardWorld(preload 脚本实例——Node2D 世界根/材质挂点)
var _cells := {} # Vector2i → CellUnit(Node2D)——键面同构(探针断言面)
var _board: BoardData = null
var _minesweeper = null # 排雷玩法 只读查询引用(refresh 更新;null = 无战斗)
var _identity_cb := Callable() # 地图战斗资源 身份解析回调(组合根注入只读面)——Vector2i → Dictionary/null
@@ -53,18 +66,27 @@ var _desat_mat: ShaderMaterial
var _desat_tween: Tween
var _cell := 46.0 # 当前格边长(set_cell_area 按容器尺寸重算——battle.md 格尺寸公式)
var cell_size: float: # 只读属性(ui_layout_probe 断言面——旧私有名 _cell 的契约化出口)
get:
return _cell
func _init() -> void:
size_flags_horizontal = Control.SIZE_EXPAND_FILL
size_flags_vertical = Control.SIZE_EXPAND_FILL
var wrap := CenterContainer.new()
wrap.set_anchors_preset(Control.PRESET_FULL_RECT)
wrap.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(wrap)
_grid = GridContainer.new()
_grid.add_theme_constant_override("h_separation", 2)
_grid.add_theme_constant_override("v_separation", 2)
wrap.add_child(_grid)
_wrap = CenterContainer.new()
_wrap.set_anchors_preset(Control.PRESET_FULL_RECT)
_wrap.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_wrap)
_svp_container = SubViewportContainer.new()
_svp_container.stretch = true # 容器尺寸 = 视口逻辑分辨率(_update_world_size 同步)——恒 1:1 无缩放
_svp_container.mouse_filter = Control.MOUSE_FILTER_IGNORE # 输入归壳层 _gui_input(板级公式换算),不进 SubViewport
_wrap.add_child(_svp_container)
_viewport = SubViewport.new()
_viewport.transparent_bg = true # 格间距露出 D 区底色(与旧 Button 网格间隙同观感)
_svp_container.add_child(_viewport)
_world = _BOARD_WORLD.new()
_viewport.add_child(_world)
_desat_mat = ShaderMaterial.new()
_desat_mat.shader = load("res://presentation/views/desaturation.gdshader")
_desat_mat.set_shader_parameter("strength", 0.0)
@@ -76,9 +98,11 @@ func board() -> BoardData:
func _ready() -> void:
_apply_custom_cursors()
mouse_exited.connect(_on_shell_mouse_exited)
# 结算期/道具窗口态:D 区去饱和 + 金橙偏移锁输入(明度不变——art-bible §2;占位级逐格材质)
# 结算期/道具窗口态:D 区去饱和 + 金橙偏移锁输入(明度不变——art-bible §2);
# 材质挂 BoardWorld 世界级单点(CellUnit use_parent_material 链下随世界灰化——语义与旧逐格挂材质一致)
func set_desaturated(on: bool) -> void:
if _desat_tween != null and _desat_tween.is_valid():
_desat_tween.kill()
@@ -86,8 +110,7 @@ func set_desaturated(on: bool) -> void:
var target := 1.0 if on else 0.0
_desat_tween.tween_method(func(v: float): _desat_mat.set_shader_parameter("strength", v),
float(_desat_mat.get_shader_parameter("strength")), target, UiKit.motion(0.15))
for c in _cells:
(_cells[c] as Button).material = _desat_mat if on else null
_world.material = _desat_mat if on else null
# 组合根注入 地图战斗资源 身份解析只读回调(query_identity)——奖励徽章数据源
@@ -101,6 +124,15 @@ func set_variant_mask_resolver(cb: Callable) -> void:
_mask_cb = cb
# SubViewport 主题/字体显式注入挂点(Phase B 两舞台复用——SubViewport 不自动继承主场景 Control 主题;
# 本项目无自定义主题,缺省 = 引擎回退字体,与旧 Button 渲染同源)
func set_world_font(f: Font) -> void:
_world.board_font = f
for c in _cells:
_cells[c].font = f
_cells[c].queue_redraw()
# D 区可用面积(AP 条之外)→ 格边长 = floor((min(宽, 高) − 预留 − (N−1)×格间距) / N)(battle.md 格尺寸公式)
# 2026-09-19 修复:格边长公式补算 (N−1)×2px 网格间距 + 垂直预留 24px——旧公式只算 N×cell,
# L2 12×12 板网格实高溢出可用区 ~6px,下缘被 clip_contents 裁掉约 1/6 格高。
@@ -108,42 +140,43 @@ func set_cell_area(avail: Vector2) -> void:
if _board == null or _board.width == 0:
return
var n: int = maxi(_board.width, _board.height)
var sep := 2 # GridContainer v/h_separation(_init 同值)
var sep := SEP # 格间距(_init 同值)
var reserve := 24.0 # 垂直总预留(上 12 + 下 12:棋盘底部不贴 D 区边框)
var cell := int(floor((minf(avail.x - 12.0, avail.y - reserve) - float((n - 1) * sep)) / float(n)))
_cell = float(clampi(cell, 26, 46))
var stride := _cell + float(SEP)
for c in _cells:
(_cells[c] as Button).custom_minimum_size = Vector2(_cell, _cell)
var u: Node2D = _cells[c]
u.edge = _cell
u.position = Vector2(c) * stride
_update_world_size()
# 战斗启动/换板:按板尺寸重建格子控件(minesweeper 引用随之更新)
# 战斗启动/换板:按板尺寸重建格单元(minesweeper 引用随之更新)
func rebuild(board: BoardData, minesweeper) -> void:
_board = board
_minesweeper = minesweeper
for child in _grid.get_children():
for child in _world.get_children():
child.queue_free()
_cells.clear()
_cell_sb.clear()
_marker.clear()
_r0.clear()
_hover = Vector2i(-9, -9)
for c in _board.compute_r0():
_r0[c] = true
if _board == null:
return
_grid.columns = _board.width
var stride := _cell + float(SEP)
for y in _board.height:
for x in _board.width:
var c := Vector2i(x, y)
var btn := Button.new()
btn.custom_minimum_size = Vector2(_cell, _cell)
btn.focus_mode = Control.FOCUS_NONE
btn.gui_input.connect(func(ev: InputEvent): _on_cell_input(c, ev))
btn.mouse_entered.connect(func(): _on_cell_hover(c))
btn.mouse_exited.connect(func(): _on_cell_unhover(c))
_grid.add_child(btn)
_cells[c] = btn
_marker[c] = _build_markers(btn)
var u: Node2D = _CELL_UNIT.new()
u.cell = c
u.edge = _cell
u.position = Vector2(c) * stride
u.use_parent_material = true # 去饱和材质挂 BoardWorld——单元随世界材质灰化
u.font = _world.board_font
_world.add_child(u)
_cells[c] = u
set_cell_area(size)
refresh()
@@ -171,6 +204,64 @@ func click_cell(cell: Vector2i, button: int) -> void:
_on_cell_input(cell, ev)
# 板级公式单点(ADR-0006 Decision 3):格坐标 = floor(板内局部坐标 / 格步长)
#(格步长 = 格边长 + 间距;板外/负域返回 (-9,-9)。旧逐格 Button 命中被此函数取代;
# 2px 格间隙落入左/上邻格——旧间隙为零命中死区,等价性偏差已登记迁移方案汇报面)
func _cell_at(shell_pos: Vector2) -> Vector2i:
if _board == null or _board.width == 0:
return Vector2i(-9, -9)
var p := shell_pos - _svp_container.position # CenterContainer 居中偏移(wrap 恒在壳 (0,0)——容器位置即棋盘原点)
if p.x < 0.0 or p.y < 0.0:
return Vector2i(-9, -9)
var stride := _cell + float(SEP)
var c := Vector2i(int(floor(p.x / stride)), int(floor(p.y / stride)))
if c.x >= _board.width or c.y >= _board.height:
return Vector2i(-9, -9)
return c
# 格中心(壳内局部坐标)——ui_input_probe 合成事件落点出口(与 _cell_at 同一公式族)
func cell_center_local(c: Vector2i) -> Vector2:
return _svp_container.position + Vector2(c) * (_cell + float(SEP)) + Vector2.ONE * (_cell * 0.5)
# 视口逻辑分辨率 = 板像素尺寸(stretch=true 且容器 min 尺寸同步——缩放比恒 1:1)
func _update_world_size() -> void:
if _board == null or _board.width == 0:
return
var px := Vector2(_board.width, _board.height) * (_cell + float(SEP)) - Vector2(SEP, SEP)
_svp_container.custom_minimum_size = px
_viewport.size = px # 树内 stretch=true 由容器回写同值;树外(冒烟直建)此处保 1:1
# 真实输入路径(ADR-0006 Decision 3):壳层 _gui_input → 板级公式换算 → 复用既有 hover/点选/chord 出口
func _gui_input(ev: InputEvent) -> void:
if _minesweeper == null:
return
if ev is InputEventMouseMotion:
_hover_at(ev.position)
elif ev is InputEventMouseButton and ev.pressed:
var c := _cell_at(ev.position)
if c.x >= 0:
_on_cell_input(c, ev)
func _hover_at(pos: Vector2) -> void:
var c := _cell_at(pos)
if c.x < 0:
if _hover.x >= 0:
_on_cell_unhover(_hover)
elif c != _hover:
if _hover.x >= 0:
_on_cell_unhover(_hover)
_on_cell_hover(c)
func _on_shell_mouse_exited() -> void:
if _hover.x >= 0:
_on_cell_unhover(_hover)
func _on_cell_input(c: Vector2i, ev: InputEvent) -> void:
if _minesweeper == null or not (ev is InputEventMouseButton and ev.pressed):
return
@@ -220,40 +311,39 @@ func _apply_hover_visuals(c: Vector2i) -> void:
if _minesweeper == null or not _cells.has(c):
return
_clear_hover_visuals(c)
var u: Node2D = _cells[c]
var revealed: bool = _minesweeper.is_revealed(c)
var flag: bool = _minesweeper.has_flag(c)
var adjacent: bool = _diggable.has(c)
var hl: Panel = _marker[c]["hl"]
if revealed:
hl.visible = true
_set_hl_color(hl, COL_HL_TARGET) # 移动目标格边框(金)
u.hl_visible = true
u.hl_color = COL_HL_TARGET # 移动目标格边框(金)
_set_cursor_shape(c, CURSOR_MOVE_SEM)
_show_path_preview(c)
elif adjacent:
hl.visible = true
_set_hl_color(hl, COL_HL_TARGET) # 挖掘目标格边框(相邻旗格同此——左键直接挖,插旗不可逆批)
u.hl_visible = true
u.hl_color = COL_HL_TARGET # 挖掘目标格边框(相邻旗格同此——左键直接挖,插旗不可逆批)
_set_cursor_shape(c, CURSOR_DIG)
elif flag:
hl.visible = true
_set_hl_color(hl, COL_FLAG) # 已承诺旗标记(旗朱;无交互可供——拔旗已退役)
u.hl_visible = true
u.hl_color = COL_FLAG # 已承诺旗标记(旗朱;无交互可供——拔旗已退役)
_set_cursor_shape(c, Input.CURSOR_FORBIDDEN)
else:
hl.visible = true
_set_hl_color(hl, UiKit.RED_FORBID) # 不可及域灰禁框(禁止光标+首遇 toast 双通道)
u.hl_visible = true
u.hl_color = UiKit.RED_FORBID # 不可及域灰禁框(禁止光标+首遇 toast 双通道)
_set_cursor_shape(c, Input.CURSOR_FORBIDDEN)
hover_forbidden.emit(c)
func _clear_hover_visuals(c: Vector2i) -> void:
if not _marker.has(c):
if not _cells.has(c):
return
_marker[c]["hl"].visible = false
_cells[c].hl_visible = false
_hide_path_preview()
func _set_cursor_shape(c: Vector2i, shape: int) -> void:
if _cells.has(c):
(_cells[c] as Button).mouse_default_cursor_shape = shape
mouse_default_cursor_shape = shape # 壳级光标(悬停格语义决定——单格悬停恒一,等价旧逐格光标)
func _show_path_preview(c: Vector2i) -> void:
@@ -262,33 +352,29 @@ func _show_path_preview(c: Vector2i) -> void:
return
var path: Array = _minesweeper.compute_move_path(c)
for pc in path:
if _marker.has(pc):
_marker[pc]["dot"].visible = true
if _cells.has(pc):
_cells[pc].dot_visible = true
func _hide_path_preview() -> void:
for c in _marker:
_marker[c]["dot"].visible = false
for c in _cells:
_cells[c].dot_visible = false
# ---- 奖励徽章(已开格身份 icon——旗之下、纹带之上、不进数字区) ----
func _update_badge(c: Vector2i, btn: Button) -> void:
var badge: Label = _marker[c]["badge"]
badge.visible = false
func _update_badge(c: Vector2i) -> String:
if _minesweeper == null or not _minesweeper.is_revealed(c) or not _identity_cb.is_valid():
return
return ""
if _r0.has(c):
return # R₀ 初始展开格无奖励身份(地图战斗资源 资格域 = S∖R₀)
return "" # R₀ 初始展开格无奖励身份(地图战斗资源 资格域 = S∖R₀)
var rec: Dictionary = _minesweeper.reveal_record(c) # E2 收口:已开记录经查询面(禁直读 排雷玩法 状态字典)
if rec.get("kind", "") != "safe":
return
return ""
var ident = _identity_cb.call(c)
if not (ident is Dictionary):
return
badge.text = _badge_glyph(String(ident.get("cell_type", "")))
badge.add_theme_font_size_override("font_size", maxi(10, int(_cell * 0.28)))
badge.visible = true
return ""
return _badge_glyph(String(ident.get("cell_type", "")))
# 奖励图标键解析(HUD框架:查不到的键立即断言 + ? 占位纹样 + 诊断;键集 = 格面资源 cell_type 九值枚举——GP+商店房实施批 2026-09-21 八→九)
@@ -303,33 +389,33 @@ func _badge_glyph(category: String) -> String:
# ---- 格态渲染(灰度截图可辨:完整覆尘 / 碎裂裸土 / 已开数字) ----
func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
var btn: Button = _cells.get(c)
if btn == null:
var u: Node2D = _cells.get(c)
if u == null:
return
var revealed: bool = _minesweeper.is_revealed(c)
var flag: bool = _minesweeper.has_flag(c)
var is_hero: bool = c == hero
var text := ""
var glyph := ""
var bg: Color
var digit_col := COL_DIGIT
if revealed:
var rec: Dictionary = _minesweeper.reveal_record(c) # E2 收口:已开记录经查询面
if rec.get("kind", "") == "mine":
bg = COL_FLOOR
text = "雷"
glyph = "雷"
digit_col = UiKit.RED_MINE
else:
bg = COL_FLOOR
var d: int = rec.get("digit", 0)
if d > 0:
if _mask_cb.is_valid() and bool(_mask_cb.call(c)):
text = "?" # 数字遮蔽(地形变异 hide_number——信息型:该格真实数字不显示)
glyph = "?" # 数字遮蔽(地形变异 hide_number——信息型:该格真实数字不显示)
else:
text = str(d)
glyph = str(d)
else:
if flag:
bg = COL_BRICK
text = "旗"
glyph = "旗"
digit_col = COL_FLAG
else:
bg = COL_BRICK
@@ -337,28 +423,14 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
if is_hero:
sb.set_border_width_all(3)
sb.border_color = COL_HERO
if text == "":
text = "人"
if glyph == "":
glyph = "人"
digit_col = COL_HERO
_cell_sb[c] = sb
btn.add_theme_stylebox_override("normal", sb)
btn.add_theme_stylebox_override("hover", sb)
btn.add_theme_stylebox_override("pressed", sb)
btn.text = text
btn.add_theme_color_override("font_color", digit_col)
btn.add_theme_font_size_override("font_size", maxi(14, int(_cell * 0.5))) # 数字 ≥ 格高 50%
_update_badge(c, btn)
# 悬停光标三态(静态语义按四格态预置——hover 期再精调)
if is_hero:
_set_cursor_shape(c, Input.CURSOR_ARROW)
elif revealed:
_set_cursor_shape(c, CURSOR_MOVE_SEM)
elif flag:
_set_cursor_shape(c, CURSOR_FLAG)
elif _diggable.has(c):
_set_cursor_shape(c, CURSOR_DIG)
else:
_set_cursor_shape(c, Input.CURSOR_FORBIDDEN)
u.bg_sb = sb
u.glyph = glyph
u.glyph_color = digit_col
u.badge_glyph = _update_badge(c)
u.queue_redraw()
# ---- 可挖域 8 向常驻呼吸微光(挖掘期唯一在动元素) ----
@@ -378,53 +450,21 @@ func _process(delta: float) -> void:
var k := 0.5 + 0.5 * sin(_breath_t * 3.0) # 3 rad/s≈0.48Hz——无 >3 次/秒频闪(AC 基线)
var col := COL_DIG_EDGE.lerp(UiKit.DIG_EDGE_DEEP, 0.55 - 0.35 * k)
for c in _diggable:
if not _cell_sb.has(c):
var u: Node2D = _cells.get(c)
if u == null or u.bg_sb == null:
continue
var sb: StyleBoxFlat = _cell_sb[c]
sb.set_border_width_all(2)
sb.border_color = col
u.bg_sb.set_border_width_all(2)
u.bg_sb.border_color = col
u.queue_redraw()
func _reset_breath() -> void:
for c in _diggable:
if _cell_sb.has(c):
var sb: StyleBoxFlat = _cell_sb[c]
sb.set_border_width_all(1)
sb.border_color = sb.bg_color.darkened(0.15)
# ---- 子标记层(不参与命中测试) ----
func _build_markers(btn: Button) -> Dictionary:
var dot := ColorRect.new()
dot.color = COL_PATH
dot.custom_minimum_size = Vector2(6, 6)
dot.set_anchors_preset(Control.PRESET_CENTER)
dot.mouse_filter = Control.MOUSE_FILTER_IGNORE
dot.visible = false
btn.add_child(dot)
var badge := Label.new()
badge.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
badge.grow_horizontal = Control.GROW_DIRECTION_BEGIN
badge.grow_vertical = Control.GROW_DIRECTION_BEGIN
badge.add_theme_color_override("font_color", COL_GOLD)
badge.mouse_filter = Control.MOUSE_FILTER_IGNORE
badge.visible = false
btn.add_child(badge)
var hl := Panel.new()
hl.set_anchors_preset(Control.PRESET_FULL_RECT)
hl.mouse_filter = Control.MOUSE_FILTER_IGNORE
var hl_sb := UiKit.panel_flat(Color.TRANSPARENT, COL_HL_TARGET, 0, 2)
hl_sb.draw_center = false
hl.add_theme_stylebox_override("panel", hl_sb)
hl.visible = false
btn.add_child(hl)
return {"dot": dot, "badge": badge, "hl": hl, "hl_sb": hl_sb}
func _set_hl_color(hl: Panel, col: Color) -> void:
var sb: StyleBoxFlat = hl.get_theme_stylebox("panel")
sb.border_color = col
var u: Node2D = _cells.get(c)
if u != null and u.bg_sb != null:
u.bg_sb.set_border_width_all(1)
u.bg_sb.border_color = u.bg_sb.bg_color.darkened(0.15)
u.queue_redraw()
# ---- 自绘光标纹理(镐/旗——引擎内置光标无此语义;程序化占位,headless 跳过) ----
@@ -0,0 +1,9 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 棋盘 Node2D 迁移批 Phase A 2026-09-23:BoardWorld——棋盘 Node2D 世界根(SubViewport 内
# CellUnit 容器)。职责单点:世界级材质挂点(set_desaturated 去饱和挂此处,CellUnit
# use_parent_material 链下随世界灰化)+ 主题/字体显式注入槽位(SubViewport 不自动继承主场景
# Control 主题——ADR-0006 Negative 项;Phase B 两舞台共用骨架,ADR-0006 Decision 5)。
# 无 class_name(类名缓存面纪律——BoardView 经 preload 使用)。
extends Node2D
var board_font: Font = null # 字体注入槽位(null = ThemeDB.fallback_font——与旧 Button 缺省主题字体同源)
@@ -0,0 +1 @@
uid://ckpx51y78qqq1
@@ -0,0 +1,203 @@
# 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 到达组合根+核心揭格)。
# 事件路径: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)
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)
@@ -0,0 +1 @@
uid://6qb420weehdu
@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://smoke/ui_input_probe.gd" id="1"]
[node name="UIInputProbe" type="Node"]
script = ExtResource("1")