# VERTICAL SLICE - NOT FOR PRODUCTION # 实现 GDD:design/ux/battle.md(战斗屏 D 区 BoardView——格态/悬停预承诺光标/可挖域呼吸微光/ # BFS 路径预览/奖励徽章/结算期去饱和锁输入)。 # Date: 2026-09-12(battle.md 战斗屏改造批;基础 = 2026-09-11 HUD框架-lite 版) # 棋盘 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 契约化出口)。 # 棋盘小人贴图批 2026-09-25:英雄=BoardWorld 独立 Sprite2D(移动实体不进格面语义——计划书 §3.A; # refresh diff last_hero_pos 驱动位移 Tween+站/走两帧+flip_h 朝向;素材缺位回退格内字形「人」); # 挖掘=挥铲三帧特效(BoardFxView)+角色弹跳 Tween;格面五 texture 槽零改动。 # 高内聚边界:只读 排雷玩法 查询面(+地图战斗资源 身份解析回调)做渲染与意图识别,动作经信号外发——不调用核心写接口 # (settle_batch 归组合根编排),不持久持有战斗态(minesweeper 引用随 refresh 更新,仅作只读查询)。 # 行为等价:格件常驻、逐格重绘在 refresh 内完成,无布局回流(缺陷①回归约束——CellUnit 定长不增删, # 重建仅在换板时发生)。 class_name BoardView extends Control signal action_requested(cell: Vector2i, kind: String) # kind ∈ {dig, move, flag_place, flag_pull} 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 硬指标) const COL_HERO := UiKit.HERO_BLUE # 小人边框——小人蓝 #2E6E8E const COL_FLAG := UiKit.SEAL_BLACK # 封印黑 #000000(旗朱→封印紫→封印黑,2026-09-28 用户拍板) const COL_GOLD := UiKit.GOLD # 奖励金 #F0A828 const COL_PATH := UiKit.HERO_BLUE # 路径预览点迹——小人蓝细线 const COL_CONTACT := UiKit.CONTACT_GLOW # 接触带微光基色(蓝族浅一档·近白蓝——§3.2 v2.8;旧绿相退役 2026-09-26) const COL_HL_TARGET := UiKit.GOLD # 悬停目标格边框(金) const REWARD_BADGE := { # 已开格格面资源身份 icon(cell_type 九型键;极性视觉归批 2) "attack": "攻", "defense": "守", "heal": "医", "blast": "爆", "weaken": "弱", "pierce": "穿", "magic": "魔", # 穿/魔 = 伤害道具批 2026-09-18 "action": "行", # 行 = 行动点型(八型批 2026-09-20——ap_up/ap_drain 两行;徽章键漏网补齐,ui_click_probe 91 断言修复) "gp": "金", # 金 = GP 型(GP+商店房实施批 2026-09-21——gp_gain/gp_loss 两行;金色系 ~40° 相位,禁入带紫赤 300–345° 外,取色 UiKit.GOLD 徽标色单点) } const BADGE_PLACEHOLDER := "?" # 未知图标键占位纹样(HUD框架:禁静默空图标) 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 同值——格尺寸公式与命中换算共用) # 棋盘立体参数(2026-09-24 用户实机三档对比拍板「中档」定稿——原型 F9/按钮入口已退役; # §1.4 注记待 art-bible 并行批落籍后补写) const DEPTH_PX := 8.0 # 悬崖厚度(像素) const DEPTH_BEVEL := 0.6 # 顶面倒角强度 # 高墙感 D 投影档(2026-09-28 挑档落码):地面投影带宽 = 格边比(研究件 64px 拼版实测 14/64≈0.22; # 立面/投影色参数在 CellUnit 常量——绘制侧单点) const WALL_PROJECTION_FRAC := 0.22 # 英雄单位演出参数(棋盘小人贴图批 2026-09-25——计划书 §3.B/C 时长档) const HERO_FRAME_PX := 32.0 # 英雄帧画布边长(素材 32×32 锁定——缩放比分母) const HERO_SPRITE_SCALE := 1.5 # 1.5× 档(2026-09-26 用户二裁:实机看 1× 偏小放大 1.5 倍——明示 # 裁决压圣经 §8.2「禁非整数缩放」一般规则,§8.2 豁免注记归 art-bible # 批另落;头可越格上沿不裁剪) const HERO_GROUND_FRAC := 0.25 # 脚锚纵向落点 = 格底往上 25%(用户口径「脚踩格子 1/4 处·从下往上数」; # 头越上格沿造立体感。⚠ 参照系纪律:本值一律自底边起算——上一批用 # 「自顶 75%」表述同线异名,措辞口径误导用户被纠,本批统一为自底边) const HERO_MOVE_S := 0.14 # 位移时长(~140ms) const HERO_STEP_S := 0.18 # 走路帧周期 = 原地撞墙帧闪时长(~180ms) const HERO_DIG_S := 0.15 # 挖掘弹跳总时长(~150ms,半程压半程回) const HERO_BOUNCE := 0.82 # 弹跳压缩比(scale.y 1→0.82→1 脚底收敛) const HERO_SHADOW_PX := Vector2(32, 12) # 脚下椭圆影屏幕尺寸(Godot 组件合成——径向渐变纹理压扁, # 贴图零改动,2026-09-26 用户裁决走引擎侧) # ── 高墙/地面瓦片纹理路径单点(棋盘高墙瓦片接线批 2026-09-26)── # 素材 PNG 不入 git(用户裁定)——一律 UiKit.load_tex(exists 守卫)装载,禁 preload; # 缺位返回 null,整批任一缺位 = 弃用瓦片走程序化平色回退(BRICK/FLOOR,fresh checkout 冒烟不炸)。 # 路径常量落本文件单点(ui_kit.gd 由并行批占用,任务书 §2.5 禁入)。词表 = # design/research/board-tile-vocab-plan-a-2026-09-26.md(16 唯一墙瓦片 + 5 地面变体; # 东/东北/东南/end_e/in_ne/in_se 六镜像件运行时 flip_x 合成,不出文件——词表 §二)。 # 换色批(T6 2026-09-28):路径按 board_type 后缀参数化——terrain_<件名>_.png; # 词表 §一 16 唯一墙瓦片名单单点在本常量数组。underdark/deep_underdark 两套 = # tools/recolor_terrain_tiles.py 程序换色产(台账 164 换色遗留清偿);任一套缺位 → 整批回落 # dungeon 套(禁同板混渲染);dungeon 套亦缺位 → 程序化平色回退(fresh checkout 冒烟不炸)。 const TILE_WALL_NAMES := ["wall_center", "wall_center_crack", "wall_center_speckle", "wall_n", "wall_s", "wall_w", "wall_nw", "wall_sw", "wall_hrun", "wall_vrun", "wall_end_n", "wall_end_s", "wall_end_w", "wall_lone", "wall_in_nw", "wall_in_sw"] const TILE_WALL_FMT := "res://assets/art/board/terrain_%s_%s.png" # %s=件名 %s=board_type const TILE_FLAG_SEAL := "res://assets/art/board/flag_seal.png" # 封印旗纹理(迭代 9 卡⑬——独立可选件) const TILE_FLOOR_FMT := "res://assets/art/board/terrain_floor_%s_%d.png" # %s=board_type 1..%d 变体 const TILE_FLOOR_VARIANTS := 5 # 地面瓦片变体数(词表 §一 floor_1..5) 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 var _mask_cb := Callable() # 地形变异 数字遮蔽回调(组合根注入只读面)——Vector2i → bool(true = 该格数字不显示) var _vmark_cb := Callable() # 地形变异 角标回调(迭代 6 T6 只读面)——Vector2i → String("flat_damage"=伤害型变异未开格;""=无) var _aux_reveal := {} # 魔法道具 真视集(迭代 7 T4——Vector2i → true;组合根 set_aux_overlays 注入投影) var _aux_preview := {} # 魔法道具 预读集(同上——未开格徽章呈现挂点) var _diggable := {} # 可挖域(8 向相邻未开格)——呼吸微光作用集 var _breath_t := 0.0 var _contact_unit: Node2D = null # 接触带承载单元(=英雄格 CellUnit;rebuild 随清场复位) var _install_tw: Tween # 入场淡入 tween(rebuild 重入先杀) var _ripple_tws: Array[Tween] = [] # R0 涟漪逐格 tween(rebuild 重入全杀) var _dig_glow := false # 挖掘期才允许唯一在动元素(art-bible §2.1) var _hover := Vector2i(-9, -9) var _last_click := {"cell": Vector2i(-9, -9), "ms": -100000} var _r0 := {} # 出生展开区 R₀(地图战斗资源 mapping 资格域 = S∖R₀——徽章查询跳过,防 E19 诊断噪音) var _desat_mat: ShaderMaterial var _desat_tween: Tween var _cell := 46.0 # 当前格边长(set_cell_area 按容器尺寸重算——battle.md 格尺寸公式) # ── 英雄单位精灵状态(棋盘小人贴图批 2026-09-25;素材 PNG 不入 git——load_tex 守卫装载,缺位=字形回退)── var _hero_tex_idle: Texture2D # 站立帧(null = 素材缺位——零精灵,字形「人」原路径) var _hero_tex_walk: Texture2D # 跨步帧 var _hero_last := Vector2i(-99, -99) # 上拍英雄格(refresh diff 基准;哨兵 = 未落位/随 rebuild 复位) var _hero_face_left := true # 朝向(素材只入库朝左帧——右向 = flip_h,计划书 §2 约定) var _hero_bump_pending := false # 受理零位移移动回执(refresh diff 只认位置变化,原地移动须外部告知) var _hero_move_tw: Tween var _hero_bump_tw: Tween var _hero_dig_tw: Tween # ── 瓦片状态(高墙瓦片接线批 2026-09-26;素材缺位 = 全 false/空——程序化平色回退)── var _tiles := {} # 词表键 → Texture2D(镜像件已预翻转;装载见 _load_board_tiles) var _floor_tiles: Array[Texture2D] = [] # 地面瓦片五变体(下标 0..4 = floor_1..5) var _tiles_ok := false # false = 素材缺位/未装载——bg_sb 程序化平色原路径 var _flag_tex: Texture2D = null # 封印旗纹理(缺盘=null=字形「旗」回退——独立可选件不连瓦片整批) var _tiles_board_type := "dungeon" # 当前装载瓦片套的 board_type(rebuild 按板切换——T6 换色批) var _digit_dark_ink := true # 数字墨族按板自适应(true=深墨三档)。缺盘平色回退态 #B39A78 裸土 # 为中明度底——深墨族 CR 4.96 达标、亮墨族仅 2.2,故缺盘默认深墨;瓦片装载后按实测分位数覆写 var _digit_plate := false # 数字色牌垫底模式(双峰纹理板——直画两族皆不过门槛时落牌, # 素牌+深墨 12:1 纹理免疫;2026-10-01 渲染帧取证 underdark 系深墨直画隐形后启用) 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 _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) _hero_tex_idle = UiKit.load_tex(UiKit.TEX_HERO_IDLE) _hero_tex_walk = UiKit.load_tex(UiKit.TEX_HERO_WALK) _load_board_tiles() # 高墙/地面瓦片整批守卫装载(缺位静默回退——英雄批同款纪律,禁刷屏) func board() -> BoardData: return _board func _ready() -> void: _apply_custom_cursors() mouse_exited.connect(_on_shell_mouse_exited) # 结算期/道具窗口态: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() _desat_tween = create_tween() 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)) _world.material = _desat_mat if on else null # 组合根注入 地图战斗资源 身份解析只读回调(query_identity)——奖励徽章数据源 func set_identity_resolver(cb: Callable) -> void: _identity_cb = cb # 组合根注入 地形变异 数字遮蔽只读回调(信息型 hide_number 触发面——触发枚举 on_number_shown: # 已开且显示数字时起作用;读取方按格子变异数据行动,规则本体零特判) func set_variant_mask_resolver(cb: Callable) -> void: _mask_cb = cb # 组合根注入 地形变异 角标只读回调(迭代 6 T6——flat_damage 未开格视觉标记;变异是策略纹理非数值墙: # 玩家可据此绕行/承担的决策输入。空 Callable = 全板无标记(缺位回退态) func set_variant_mark_resolver(cb: Callable) -> void: _vmark_cb = cb # 魔法道具叠加层投影注入(迭代 7 T4——组合根在 use 成功/三清后调;refresh 消费) func set_aux_overlays(reveal_cells: Array, preview_cells: Array) -> void: _aux_reveal.clear() _aux_preview.clear() for c in reveal_cells: _aux_reveal[Vector2i(c)] = true for c in preview_cells: _aux_preview[Vector2i(c)] = true refresh() # 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 格高。 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 := SEP # 格间距(_init 同值) var d := DEPTH_PX var reserve := 24.0 + d # 垂直总预留(上 12 + 下 12 + 悬崖外延——悬崖占预算防溢出) var cell := int(floor((minf(avail.x - 12.0 - d, avail.y - reserve) - float((n - 1) * sep)) / float(n))) _cell = float(clampi(cell, 26, 46)) var stride := _cell + float(SEP) for c in _cells: var u: Node2D = _cells[c] u.edge = _cell u.position = Vector2(c) * stride if _world.hero != null and _hero_last.x >= 0: _place_hero(_hero_last, _hero_last, true) # 格边长变更(窗口缩放)——英雄即时随格重置(杀位移 tween 防飞陈旧落点) _refresh_slab() _update_world_size() # 悬崖参数下发(网格像素尺寸/步距/列数/材质锚色——BoardWorld._draw 消费) func _refresh_slab() -> void: if _board == null or _board.width == 0: return _world.depth_px = DEPTH_PX _world.slab = Vector2(_board.width, _board.height) * (_cell + float(SEP)) - Vector2(SEP, SEP) _world.stride_px = _cell + float(SEP) _world.cols = _board.width _world.mat = COL_BRICK # 战斗启动/换板:按板尺寸重建格单元(minesweeper 引用随之更新) func rebuild(board: BoardData, minesweeper) -> void: _board = board _minesweeper = minesweeper for child in _world.get_children(): child.queue_free() _world.hero = null # 英雄精灵随清场退役——下拍 refresh 经 ensure_hero 重建(首建即时落位) _cells.clear() _contact_unit = null _hero_last = Vector2i(-99, -99) _r0.clear() _hover = Vector2i(-9, -9) for c in _board.compute_r0(): _r0[c] = true if _board == null: return # 瓦片套随板图型切换(T6 换色批——board_gen 词表 v1.1 dungeon/underdark/deep_underdark; # 同图型重复 rebuild 零重载,_tiles_board_type 守卫) var bt: String = _board.board_type if _board.board_type != "" else "dungeon" if bt != _tiles_board_type: _load_board_tiles(bt) var stride := _cell + float(SEP) for y in _board.height: for x in _board.width: var c := Vector2i(x, y) var u: Node2D = _CELL_UNIT.new() u.cell = c u.edge = _cell u.position = Vector2(c) * stride u.bevel = DEPTH_BEVEL # 定稿倒角——新建格单元继承 u.use_parent_material = true # 去饱和材质挂 BoardWorld——单元随世界材质灰化 if _tiles_ok: # NEAREST 采样(2026-09-26 用户拍板:32×32 源 × 45/36px 格边非整数缩放接受档, # 与英雄小人 1.5× 豁免同级——圣经 §8.2 豁免注记归 art-bible 批补写);只在瓦片 # 模式启用——程序化回退路径渲染零扰动(含格面文字采样不回退不变化) u.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST u.font = _world.board_font _world.add_child(u) _cells[c] = u set_cell_area(size) _install_entrance_fx() refresh() # 逐格重绘(挖掘/结算后调用) func refresh() -> void: if _board == null or _minesweeper == null: return var hero: Vector2i = _minesweeper.hero_pos() _diggable.clear() for n in _board.neighbors8(hero): if not _minesweeper.is_revealed(n): _diggable[n] = true for y in _board.height: for x in _board.width: _paint_cell(Vector2i(x, y), hero) _refresh_hover_after_paint() _apply_contact_bands() _update_hero(hero) # 英雄精灵 diff 驱动(棋盘小人贴图批 2026-09-25) # ---- 入场演出(battle.md Transitions「install 淡入 200ms + 初始 R0 展开涟漪」——2026-09-26 补实装)---- # 全板 modulate 淡入 200ms;R0 格按离板心距离逐格延迟显形(涟漪);纯视觉 alpha,零输入影响; # 减动效时长减半(motion);重入(弃图换图 rebuild)先杀旧 tween 防叠加 func _install_entrance_fx() -> void: if _install_tw != null and _install_tw.is_valid(): _install_tw.kill() for tw in _ripple_tws: if tw != null and tw.is_valid(): tw.kill() _ripple_tws.clear() modulate.a = 0.0 _install_tw = create_tween() _install_tw.tween_property(self, "modulate:a", 1.0, UiKit.motion(0.2)) var center := Vector2(_board.width, _board.height) * 0.5 for c in _r0: var u: Node2D = _cells.get(c) if u == null: continue u.modulate.a = 0.0 var tw := create_tween() tw.tween_interval(UiKit.motion(0.04 * (Vector2(c) + Vector2(0.5, 0.5) - center).length())) tw.tween_property(u, "modulate:a", 1.0, UiKit.motion(0.12)) _ripple_tws.append(tw) # ---- 英雄单位精灵(棋盘小人贴图批 2026-09-25——计划书 §3.A/B;格面五 texture 槽零改动) ---- # refresh diff 驱动:位置变化 = 位移演出(140ms Tween + 走路帧);零位移但有受理移动回执 = 只播帧 # 不位移(撞墙语义——排雷玩法「目标=当前格」幂等受理)。素材缺位 = 零精灵,字形「人」回退路径。 func _update_hero(hero: Vector2i) -> void: if _hero_tex_idle == null: return if _world.hero == null: var sp: Sprite2D = _world.ensure_hero(_hero_tex_idle, HERO_SHADOW_PX, HERO_SPRITE_SCALE) sp.scale = Vector2.ONE * HERO_SPRITE_SCALE sp.position = _hero_feet(hero) _hero_last = hero # 首建/随 rebuild 重建——即时落位无演出 return if hero == _hero_last and not _hero_bump_pending: return if hero != _hero_last and hero.x != _hero_last.x: _hero_face_left = hero.x < _hero_last.x _place_hero(_hero_last, hero, hero == _hero_last) if hero == _hero_last: _hero_frame_flash() _hero_bump_pending = false _hero_last = hero # 英雄落位(instant = 即位无演出——首建/格边长变更;否则 140ms 位移 + 走路帧起手、到位回站帧。 # 帧权威单一化:Sprite2D 换纹理路线(计划书 §3.B 二选一之后者)——换纹理+flip_h 同拍写定即生效, # 无 AnimatedSprite2D 的 play()/advance(0) 错向面) func _place_hero(from: Vector2i, to: Vector2i, instant: bool) -> void: var sp: Sprite2D = _world.hero if sp == null: return var s := HERO_SPRITE_SCALE var feet := _hero_feet(to) if _hero_move_tw != null and _hero_move_tw.is_valid(): _hero_move_tw.kill() if instant: sp.scale = Vector2(s, s) sp.position = feet sp.texture = _hero_tex_idle return sp.scale = Vector2(s, s) # 位移重置弹跳残形(scale.y 由 play_hero_dig_bounce 独立回收) sp.texture = _hero_tex_walk sp.flip_h = not _hero_face_left # 素材基准朝左 _hero_move_tw = create_tween() _hero_move_tw.tween_property(sp, "position", feet, UiKit.motion(HERO_MOVE_S)).set_trans(Tween.TRANS_SINE) _hero_move_tw.tween_callback(func() -> void: if is_instance_valid(sp): sp.texture = _hero_tex_idle) # 原地移动帧闪(撞墙语义):跨步帧 ~180ms 后回站帧,零位移 func _hero_frame_flash() -> void: var sp: Sprite2D = _world.hero if sp == null: return if _hero_bump_tw != null and _hero_bump_tw.is_valid(): _hero_bump_tw.kill() sp.texture = _hero_tex_walk sp.flip_h = not _hero_face_left _hero_bump_tw = create_tween() _hero_bump_tw.tween_interval(UiKit.motion(HERO_STEP_S)) _hero_bump_tw.tween_callback(func() -> void: if is_instance_valid(sp): sp.texture = _hero_tex_idle) # 受理移动指令回执(组合根 settle 受理后、refresh 前调用)——原地移动经此触发帧闪; # 目标异格时本旗标被 diff 位移路径自然吞掉(置位即清) func notify_hero_move() -> void: _hero_bump_pending = true # 挖掘角色弹跳(计划书 §3.C:scale.y 1→0.82→1,脚底锚收敛不悬空;与位移/帧闪 tween 分开管理 # 避免属性争抢——旧弹跳先 kill) func play_hero_dig_bounce() -> void: var sp: Sprite2D = _world.hero if sp == null or _hero_tex_idle == null: return if _hero_dig_tw != null and _hero_dig_tw.is_valid(): _hero_dig_tw.kill() var s := HERO_SPRITE_SCALE sp.scale = Vector2(s, s) _hero_dig_tw = create_tween() _hero_dig_tw.tween_property(sp, "scale:y", s * HERO_BOUNCE, UiKit.motion(HERO_DIG_S * 0.5)).set_trans(Tween.TRANS_SINE) _hero_dig_tw.tween_property(sp, "scale:y", s, UiKit.motion(HERO_DIG_S * 0.5)).set_trans(Tween.TRANS_SINE) # 格左上角(board_area 全幅坐标系)——fx 同层演出定位出口(本视图与 fx 同为 board_area 全幅子件, # 局部坐标同系;cell_center_local 同公式族) func cell_origin(c: Vector2i) -> Vector2: return _svp_container.position + Vector2(c) * (_cell + float(SEP)) # 英雄脚底锚点——**世界坐标**(精灵在 SubViewport 世界内:格原点 = 格×步距,禁带壳层居中偏移。 # 2026-09-26 修正:上批误用壳层 cell_origin(含 _svp_container.position 居中偏移)定位世界内精灵, # 实测整体右偏 ~7 格+偏离格心——实机截图用户报缺陷)。 # 纵向 = 格底往上 25% 线(脚踩格子 1/4 处——2026-09-26 用户裁决);横向格中线。身体向上延伸, # 头越上格沿不裁剪(立体感裁决);锚点与精灵缩放解耦(缩放只改身体向上长多少)。 func _hero_feet(c: Vector2i) -> Vector2: return Vector2(c) * (_cell + float(SEP)) + Vector2(_cell * 0.5, _cell * (1.0 - HERO_GROUND_FRAC)) # 英雄持握手位(fx 同层坐标系 = 壳层坐标——fx 在 SubViewport 之外,须含 _svp_container.position # 居中偏移:世界脚底锚 + 壳层偏移即得)。手 = 盒底往上 1/4 盒高、水平居中(2026-09-25 用户裁决; # 盒高随 HERO_SPRITE_SCALE——手位钉在身体上不随缩放漂移) func hero_hand_point() -> Vector2: return _svp_container.position + _hero_feet(_hero_last) \ + Vector2(0.0, -HERO_FRAME_PX * 0.25 * HERO_SPRITE_SCALE) # 英雄精灵缩放档(fx 挥铲同档出口——两演出比例恒一致) func hero_sprite_scale() -> float: return HERO_SPRITE_SCALE # 真实输入路径(行为探针复用):合成点击 → 与真实 gui_input 同一翻译出口 func click_cell(cell: Vector2i, button: int) -> void: var ev := InputEventMouseButton.new() ev.button_index = button ev.pressed = true _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/cell_origin 同一公式族) func cell_center_local(c: Vector2i) -> Vector2: return cell_origin(c) + Vector2.ONE * (_cell * 0.5) # 视口逻辑分辨率 = 板像素尺寸 + 悬崖外延(stretch=true 且容器 min 尺寸同步——缩放比恒 1:1) func _update_world_size() -> void: if _board == null or _board.width == 0: return var d := DEPTH_PX var px := Vector2(_board.width, _board.height) * (_cell + float(SEP)) - Vector2(SEP, SEP) + Vector2(d, d) _svp_container.custom_minimum_size = px # 树内 stretch=true 时 SubViewport 尺寸由容器接管(手设被引擎拒绝并刷警告);树外(冒烟直建)才需手设保 1:1 if not _svp_container.is_inside_tree(): _viewport.size = px # 真实输入路径(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 if ev.button_index == MOUSE_BUTTON_LEFT: if _minesweeper.is_revealed(c): action_requested.emit(c, "move") else: action_requested.emit(c, "dig") _detect_chord(c) elif ev.button_index == MOUSE_BUTTON_RIGHT: if not _minesweeper.has_flag(c): action_requested.emit(c, "flag_place") # 旗 = 一次性承诺(插旗不可逆批 2026-09-19):拔旗退役——旗格右键零动作; # 左键相邻旗格直接挖(四情况②④兑付改判,规则引擎放行)。 # chord 肌肉记忆首遇锚:同格双左击快开尝试(350ms 窗)——每 run 一次提示由组合根去重 func _detect_chord(c: Vector2i) -> void: var now := Time.get_ticks_msec() if _last_click.cell == c and now - int(_last_click.ms) <= CHORD_WINDOW_MS: chord_attempted.emit(c) _last_click.ms = -100000 return _last_click = {"cell": c, "ms": now} # ---- 悬停预承诺(pattern 4):三语义光标 + 目标格边框 + BFS 路径预览 ---- func _on_cell_hover(c: Vector2i) -> void: _hover = c _apply_hover_visuals(c) func _on_cell_unhover(c: Vector2i) -> void: if _hover == c: _hover = Vector2i(-9, -9) _clear_hover_visuals(c) func _refresh_hover_after_paint() -> void: # 状态随 refresh 变化后光标/边框语义需重算(悬停格四格态可能已变) if _hover.x >= 0 and _cells.has(_hover): _apply_hover_visuals(_hover) 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) if revealed: u.hl_visible = true u.hl_color = COL_HL_TARGET # 移动目标格边框(金) _set_cursor_shape(c, CURSOR_MOVE_SEM) _show_path_preview(c) elif adjacent: u.hl_visible = true u.hl_color = COL_HL_TARGET # 挖掘目标格边框(相邻旗格同此——左键直接挖,插旗不可逆批) _set_cursor_shape(c, CURSOR_DIG) elif flag: u.hl_visible = true u.hl_color = COL_FLAG # 已承诺封印标记(封印黑;无交互可供——拔旗已退役) _set_cursor_shape(c, Input.CURSOR_FORBIDDEN) else: 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 _cells.has(c): return _cells[c].hl_visible = false _hide_path_preview() func _set_cursor_shape(c: Vector2i, shape: int) -> void: mouse_default_cursor_shape = shape # 壳级光标(悬停格语义决定——单格悬停恒一,等价旧逐格光标) func _show_path_preview(c: Vector2i) -> void: # D3 合法性指示:悬停已开格 → BFS 路径蓝细线点迹(compute_move_path 只读面) if _minesweeper == null: return var path: Array = _minesweeper.compute_move_path(c) for pc in path: if _cells.has(pc): _cells[pc].dot_visible = true func _hide_path_preview() -> void: for c in _cells: _cells[c].dot_visible = false # ---- 奖励徽章(已开格身份 icon——旗之下、纹带之上、不进数字区) ---- func _update_badge(c: Vector2i) -> String: if _minesweeper == null or not _minesweeper.is_revealed(c) or not _identity_cb.is_valid(): return "" if _r0.has(c): return "" # R₀ 初始展开格无奖励身份(地图战斗资源 资格域 = S∖R₀) var rec: Dictionary = _minesweeper.reveal_record(c) # E2 收口:已开记录经查询面(禁直读 排雷玩法 状态字典) if rec.get("kind", "") != "safe": return "" var ident = _identity_cb.call(c) if not (ident is Dictionary): return "" return _badge_glyph(String(ident.get("cell_type", ""))) # 奖励图标键解析(HUD框架:查不到的键立即断言 + ? 占位纹样 + 诊断;键集 = 格面资源 cell_type 九值枚举——GP+商店房实施批 2026-09-21 八→九) func _badge_glyph(category: String) -> String: if REWARD_BADGE.has(category): return REWARD_BADGE[category] push_error("[棋盘] 未知奖励图标键(映射表外 category——立即断言): %s" % category) assert(false, "未知奖励图标键: " + category) return BADGE_PLACEHOLDER # ---- 格态渲染(灰度截图可辨:完整覆尘 / 碎裂裸土 / 已开数字) ---- # 数字三档色温(按板自适应双族——v3.5 冻结④ 修订 2026-10-01):亮墨族 1-3 冷亮/4-5 暖白/6-8 金亮 #(dungeon 瓦片实测达标维持);深墨族 1-3 青墨/4-5 赭墨/6-8 锈墨(色温语义同构——underdark/ # deep_underdark 亮白筋纹瓦片上亮墨对亮斑 CR 1.0–1.9 融字,深墨族 5.7–11.1;选族判据见 _digit_ink_mode) func _digit_ink(d: int) -> Color: if _digit_dark_ink: if d <= 3: return UiKit.INK_COLD if d <= 5: return UiKit.INK_OCHRE return UiKit.INK_RUST if d <= 3: return UiKit.INK_L_COLD if d <= 5: return UiKit.INK_L_WARM return UiKit.INK_L_GOLD func _paint_cell(c: Vector2i, hero: Vector2i) -> void: 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 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 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)): glyph = "?" # 数字遮蔽(地形变异 hide_number——信息型:该格真实数字不显示) else: glyph = str(d) digit_col = _digit_ink(d) # 数字墨三档色温(样张 v2 冻结批 T4——§4.1) else: if flag: bg = COL_BRICK # 封印旗纹理落位(迭代 9 卡⑬——tex_flag 槽接线,迭代 8 卡⑤降级债务清偿): # 纹理在位=字形「旗」退役(项目纹理槽惯例);缺盘=字形回退原语义(load_tex exists 守卫) if _flag_tex != null: u.tex_flag = _flag_tex else: glyph = "旗" digit_col = COL_FLAG else: bg = COL_BRICK # 瓦片模式(高墙接线批 2026-09-26):tex_base = 查表瓦片(未开=墙瓦片/已开=地面瓦片), # bg_sb 降级为透明描边载体——只承载呼吸微光/主角蓝框(CellUnit 瓦片态画在瓦片之上), # 程序化平色退役;瓦片分配=格坐标哈希,初始化落定后逐拍稳定(禁逐轮重掷——圣经原则三) # draw calls 批优化(2026-10-01):透明载体 sb 视觉贡献=零(透明底+零描边——不可见),但 # StyleBoxFlat 多边形命令不可合批,每格打断棋盘批——实测 100 格满负荷帧 ~402 DC(A/B 探针 # 阶梯:纹理统一 0 增益/去 glyph −23/去叠层 −23/去隐形 sb −402)。非英雄格 sb 直接置 null #(绘制路径零像素变化),仅英雄格保留蓝框载体。 var sb: StyleBoxFlat = null var tile: Texture2D = null if _tiles_ok: tile = _floor_tile_for(c) if revealed else _wall_tile_for(c) else: # 素材整批缺位回退 = 现行程序化平色路径(fresh checkout 冒烟不炸——任务书 §2.2) sb = UiKit.panel_flat(bg, bg.darkened(0.15), 3) if is_hero: if sb == null: sb = UiKit.panel_flat(Color.TRANSPARENT, Color.TRANSPARENT, 3, 0) sb.set_border_width_all(3) sb.border_color = COL_HERO if glyph == "" and _hero_tex_idle == null: # 贴图缺位回退字形「人」(项目纹理槽惯例——贴图在位=格内字形退役,单位本体由独立精灵承载) glyph = "人" digit_col = COL_HERO u.tex_base = tile u.bg_sb = sb u.glyph = glyph u.glyph_color = digit_col # 色牌后备(v3.5 批 B2——数字直画退牌;雷/「?」维持牌载保读性)+ 2026-10-01 v3.4:双峰纹理板 #(_digit_plate——underdark 系亮白筋+近黑斑并存的直画不可救纹理)数字落牌,牌上深墨 12:1 var plate_digit := _digit_plate and glyph != "" and glyph != "雷" and glyph != "?" and glyph != "旗" and glyph != "人" if plate_digit: digit_col = COL_DIGIT # 牌上深墨族(素牌 12:1——双峰纹理免疫) u.glyph_color = digit_col u.plate = revealed and (glyph == "雷" or glyph == "?" or plate_digit) u.outline_sides.assign([]) # typed Array[int](代码审查批 3 类型收紧——assign 保持引用语义) u.facade_south = false u.proj_band = 0.0 u.tex_facade = null u.facade_strip = false u.tex_flag = null var south_n: Vector2i = c + Vector2i(0, 1) var north_n: Vector2i = c + Vector2i(0, -1) var south_in: bool = south_n.x >= 0 and south_n.y >= 0 and south_n.x < _board.width and south_n.y < _board.height var north_in: bool = north_n.x >= 0 and north_n.y >= 0 and north_n.x < _board.width and north_n.y < _board.height if not revealed: # 高墙感 D 投影档(2026-09-28 挑档落码):南邻地面格 → 墙格南缘立「口沿高光+双段暗带」立面 #(邻地面才画——墙-墙内界保墙群内聚,轮廓加强批同口径);板外=墙不画 u.facade_south = south_in and _minesweeper.is_revealed(south_n) # 轮廓加强:墙格四邻中已开(地面)格朝向记描边码(板外=墙不描——保墙群外轮廓只在墙/地交界) for d in [Vector2i(0, -1), Vector2i(0, 1), Vector2i(-1, 0), Vector2i(1, 0)]: var n: Vector2i = c + d if n.x >= 0 and n.y >= 0 and n.x < _board.width and n.y < _board.height and _minesweeper.is_revealed(n): var code := 0 if d.y < 0 else (1 if d.y > 0 else (2 if d.x < 0 else 3)) if code == 1 and u.facade_south: continue # 南缘由立面件承载(高光+双段暗带)——2px 描边让位防三重叠黑 if code == 0 and _tiles_ok: continue # v3.5 冻结① (b)-(c) 交界无边框:墙面直贴接墙顶(E 格墙面件在邻格承载)——北向描边退役(平色回退态保留描边) u.outline_sides.append(code) else: # 地面投影:北邻墙格在本已开格北缘投渐隐暗带(ITB 同款高度线索——带宽按格边比例常量) if north_in and not _minesweeper.is_revealed(north_n): u.proj_band = u.edge * WALL_PROJECTION_FRAC # E 格高墙面(圣经 v3.5 冻结①——方向稿 is_facade_cell 口径):已开格南邻未开墙 → 南部 30% # 承接墙面(材质=同板墙顶 center 瓦片,原亮度直贴);仅瓦片态启用——平色回退保持原语义 if south_in and not _minesweeper.is_revealed(south_n) and _tiles_ok: u.facade_strip = true u.tex_facade = _tiles.get("wall_center") # ②( 魔法道具叠加层(迭代 7 T4):真视格=红内缩框;预读格=奖励身份徽章(identity_resolver 既有 # 只读面取字形——零数据复制,badge 位复用);已开格标记自然退位(refresh 逐拍重算) if not revealed: u.aux_mark = "mine_reveal" if _aux_reveal.has(c) else "" if _aux_preview.has(c) and glyph == "": var pid = _identity_cb.call(c) if _identity_cb.is_valid() else null if pid != null and REWARD_BADGE.has(String(pid.get("cell_type", ""))): glyph = String(REWARD_BADGE[String(pid.get("cell_type", ""))]) digit_col = COL_GOLD u.badge_glyph = glyph # ②' 变异角标(迭代 6 T6):未开格带伤害型变异 → 危险红角标(已开=威胁已兑现标记退位); # 回调缺位 = 无标记(程序化角标,tex_mutation 纹带槽留给变异纹带自产批) if not revealed and _vmark_cb.is_valid(): u.mutation_mark = String(_vmark_cb.call(c)) else: u.mutation_mark = "" u.badge_glyph = _update_badge(c) u.queue_redraw() # ---- 可挖域接触带呼吸微光(挖掘期唯一在动元素——§3.2 v2.8 画法:光画英雄格周界空间侧, # 正交邻墙=整条边、斜向=共享角点+两侧各半条边;旧"墙格整框绿相"退役 2026-09-26 批) ---- func set_dig_glow_enabled(on: bool) -> void: _dig_glow = on if on: _apply_contact_bands() # 相位重入(结算→挖掘):refresh 未跑也重挂带几何 else: _reset_breath() func _process(delta: float) -> void: if not _dig_glow or _diggable.is_empty(): return if _contact_unit == null or not is_instance_valid(_contact_unit): return _breath_t += delta if UiKit.motion_scale < 1.0: return # 减动效:呼吸停格(refresh/enable 时已施静态色——静态可辨、无持续运动) var k := 0.5 + 0.5 * sin(_breath_t * 3.0) # 3 rad/s≈0.48Hz——无 >3 次/秒频闪(AC 基线) _contact_unit.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k) _contact_unit.queue_redraw() # 带几何写入(refresh / set_dig_glow_enabled(true) 挂点):清旧承载件→按英雄格可挖域写边/角码→施色 func _apply_contact_bands() -> void: if _contact_unit != null and is_instance_valid(_contact_unit) and (_cells.get(_minesweeper.hero_pos()) != _contact_unit if _minesweeper != null else true): _contact_unit.contact_col = Color(0, 0, 0, 0) _contact_unit.queue_redraw() _contact_unit = null if _board == null or _minesweeper == null or not _dig_glow: return var hu: Node2D = _cells.get(_minesweeper.hero_pos()) if hu == null: return _contact_unit = hu var bands: Array[int] = [] var corners: Array[int] = [] for d in [Vector2i(0, -1), Vector2i(0, 1), Vector2i(-1, 0), Vector2i(1, 0)]: if _diggable.has(_minesweeper.hero_pos() + d): bands.append(0 if d.y < 0 else (1 if d.y > 0 else (2 if d.x < 0 else 3))) for d in [Vector2i(-1, -1), Vector2i(1, -1), Vector2i(-1, 1), Vector2i(1, 1)]: if _diggable.has(_minesweeper.hero_pos() + d): corners.append(0 if (d.x < 0 and d.y < 0) else (1 if d.x > 0 and d.y < 0 else (2 if d.x < 0 else 3))) hu.contact_bands = bands hu.contact_corners = corners var k := 0.5 + 0.5 * sin(_breath_t * 3.0) hu.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k) hu.queue_redraw() func _reset_breath() -> void: if _contact_unit != null and is_instance_valid(_contact_unit): _contact_unit.contact_col = Color(0, 0, 0, 0) _contact_unit.queue_redraw() # ---- 高墙/地面瓦片接线(2026-09-26 批;词表 = design/research/board-tile-vocab-plan-a-2026-09-26.md) ---- # 瓦片整批守卫装载(_init 单次调用):21 枚全成才启用——任一缺位按整批缺位处理(禁同板混渲染; # fresh checkout 无素材目录 → 静默全回退)。缺位不 push_error——可选素材缺位是预期路径非故障 # (英雄批 load_tex 纪律同款,防冒烟刷屏)。镜像件运行时 flip_x 合成,不出文件。 func _load_board_tiles(board_type: String = "dungeon") -> void: if board_type == "": board_type = "dungeon" var t := { "wall_center": UiKit.load_tex(TILE_WALL_FMT % ["wall_center", board_type]), "wall_center_crack": UiKit.load_tex(TILE_WALL_FMT % ["wall_center_crack", board_type]), "wall_center_speckle": UiKit.load_tex(TILE_WALL_FMT % ["wall_center_speckle", board_type]), "wall_n": UiKit.load_tex(TILE_WALL_FMT % ["wall_n", board_type]), "wall_s": UiKit.load_tex(TILE_WALL_FMT % ["wall_s", board_type]), "wall_w": UiKit.load_tex(TILE_WALL_FMT % ["wall_w", board_type]), "wall_nw": UiKit.load_tex(TILE_WALL_FMT % ["wall_nw", board_type]), "wall_sw": UiKit.load_tex(TILE_WALL_FMT % ["wall_sw", board_type]), "wall_hrun": UiKit.load_tex(TILE_WALL_FMT % ["wall_hrun", board_type]), "wall_vrun": UiKit.load_tex(TILE_WALL_FMT % ["wall_vrun", board_type]), "wall_end_n": UiKit.load_tex(TILE_WALL_FMT % ["wall_end_n", board_type]), "wall_end_s": UiKit.load_tex(TILE_WALL_FMT % ["wall_end_s", board_type]), "wall_end_w": UiKit.load_tex(TILE_WALL_FMT % ["wall_end_w", board_type]), "wall_lone": UiKit.load_tex(TILE_WALL_FMT % ["wall_lone", board_type]), "wall_in_nw": UiKit.load_tex(TILE_WALL_FMT % ["wall_in_nw", board_type]), "wall_in_sw": UiKit.load_tex(TILE_WALL_FMT % ["wall_in_sw", board_type]), } var floors: Array[Texture2D] = [] for i in range(1, TILE_FLOOR_VARIANTS + 1): floors.append(UiKit.load_tex(TILE_FLOOR_FMT % [board_type, i])) for k in t: if t[k] == null: _request_dungeon_fallback(board_type) return for tex in floors: if tex == null: _request_dungeon_fallback(board_type) return # 地板归一(圣经 v3.5 冻结①/§1.4 样张 v3 记录——已开格底归一世界底层档 L≈0.10,带 0.08–0.14): # 方向稿 _v3_darken_floors 同式移植;现行瓦片 L≈0.036 低于带下沿故实为提亮;素材重产归资产批, # 本归一为程序代役(逐批装载时对 freshly-loaded 原图测光,幂等)。归一后逐像素明度顺路采集, # 驱动数字墨族按板自适应(_digit_ink_mode——均值分位同源同像素,禁二次解码) var lumas: Array = [] for i in range(floors.size()): floors[i] = _normalize_floor_luma(floors[i], 0.10, lumas) var ink_mode := _digit_ink_mode(lumas) _digit_dark_ink = bool(ink_mode.dark) _digit_plate = bool(ink_mode.plate) # 六镜像件(词表 §二:wall_e←wall_w / wall_ne←wall_nw / wall_se←wall_sw / # wall_end_e←wall_end_w / wall_in_ne←wall_in_nw / wall_in_se←wall_in_sw;只水平翻转禁旋转 # ——北受光不受损) t["wall_e"] = _flip_tex(t["wall_w"] as Texture2D) t["wall_ne"] = _flip_tex(t["wall_nw"] as Texture2D) t["wall_se"] = _flip_tex(t["wall_sw"] as Texture2D) t["wall_end_e"] = _flip_tex(t["wall_end_w"] as Texture2D) t["wall_in_ne"] = _flip_tex(t["wall_in_nw"] as Texture2D) t["wall_in_se"] = _flip_tex(t["wall_in_sw"] as Texture2D) _tiles = t _floor_tiles = floors _tiles_board_type = board_type _tiles_ok = true # 封印旗纹理(迭代 9 卡⑬——独立可选件:缺盘=字形回退,不触发瓦片整批回落语义) _flag_tex = UiKit.load_tex(TILE_FLAG_SEAL) # 整批回落(T6 换色批):请求套缺位 → 回落 dungeon 套(禁同板混渲染);dungeon 本身缺位 = # 维持程序化平色回退原语义(_tiles_ok 恒 false,缺位不刷屏纪律不变) func _request_dungeon_fallback(requested: String) -> void: if requested != "dungeon": _load_board_tiles("dungeon") # 水平镜像贴图运行时合成(flip_h 语义):Image 级逐位翻转后重建 ImageTexture——CellUnit 零改动。 # VRAM 压缩源先解压(flip_x 不支持压缩格式);32×32 原尺寸逐位翻转,无 ±1px 取整问题 static func _flip_tex(tex: Texture2D) -> Texture2D: var img := tex.get_image() if img.is_compressed(): img.decompress() img.flip_x() return ImageTexture.create_from_image(img) # 地板瓦片明度归一(v3.5 批 B1——方向稿 _v3_darken_floors 同式移植):8×8 双线性重采样均值测光 # (sRGB→线性域,Rec.709 系数),增益 k=(目标/实测)^(1/2.2) 钳 [0.6,1.8],逐通道 sRGB 域线性拉伸。 # 输入恒为 freshly-loaded 原图(load_tex 每次盘读)——幂等可重入;32×32 五变体开销可忽略 static func _normalize_floor_luma(tex: Texture2D, target_l: float = 0.10, lumas_out = null) -> Texture2D: var img: Image = tex.get_image() if img.is_compressed(): img.decompress() img.convert(Image.FORMAT_RGBA8) var small: Image = img.duplicate() small.resize(8, 8, Image.INTERPOLATE_BILINEAR) var sum := 0.0 for y in range(8): for x in range(8): var p := small.get_pixel(x, y) sum += 0.2126 * _srgb_lin(p.r) + 0.7152 * _srgb_lin(p.g) + 0.0722 * _srgb_lin(p.b) var k := pow(target_l / maxf(sum / 64.0, 0.001), 1.0 / 2.2) k = clampf(k, 0.6, 1.8) for y in range(img.get_height()): for x in range(img.get_width()): var c := img.get_pixel(x, y) var nr := minf(c.r * k, 1.0) var ng := minf(c.g * k, 1.0) var nb := minf(c.b * k, 1.0) img.set_pixel(x, y, Color(nr, ng, nb, c.a)) if lumas_out != null: lumas_out.append(0.2126 * _srgb_lin(nr) + 0.7152 * _srgb_lin(ng) + 0.0722 * _srgb_lin(nb)) return ImageTexture.create_from_image(img) static func _srgb_lin(c: float) -> float: return c / 12.92 if c <= 0.04045 else pow((c + 0.055) / 1.055, 2.4) # ---- 数字墨族按板自适应(L2/L3 亮瓦融字缺陷修复 2026-10-01——甲案:墨色按关自适应+三关对比断言)---- const DIGIT_INK_CR_P90 := 4.5 # 墨族对地板 p90 亮斑对比度门槛(失效模式=亮白筋纹冲字,WCAG 数字体同源) const DIGIT_INK_CR_MEAN_BRIGHT := 2.5 # 亮族对地板均值门槛(中调防吃字) const DIGIT_INK_CR_MEAN_DARK := 3.0 # 深族对地板均值门槛(大字 AA 级——2.4 的实测观感=隐形,v3.4 收紧) # 归一后地板逐像素明度 → 选数字呈现模式。三态:亮族直画(暗纹理板)/深族直画(中暗板)/ # 色牌垫底(双峰纹理板——近黑斑与近白筋并存时任何墨族直画必融一峰,2026-10-01 渲染帧取证 # 实证 underdark 系深墨隐形,牌上深墨 12:1 纹理免疫)。家族最弱成员(亮族=金亮/深族=锈墨) # 对 p90 与均值双门槛;亮族过选亮族,否则深族(均值门 3.0),两族皆不过落牌。 # 实测(2026-10-01):dungeon p90=0.121 亮族 4.79/5.80 ✓ 直画;underdark p90=0.406 亮族 ✗、 # 深族 5.34/**2.41**(均值 <3.0 ✗)→ 落牌;deep_underdark 同→落牌 static func _digit_ink_mode(lumas: Array) -> Dictionary: var sorted := lumas.duplicate() sorted.sort() var n := sorted.size() if n == 0: return {"dark": false, "plate": false, "cr_p90_bright": 0.0, "cr_mean_bright": 0.0, "cr_p90_dark": 0.0, "cr_mean_dark": 0.0} var p90: float = sorted[int(n * 0.9)] var mean := 0.0 for v in sorted: mean += v mean /= n var cr_p90_b := _contrast_y(UiKit.INK_L_GOLD, p90) var cr_mean_b := _contrast_y(UiKit.INK_L_GOLD, mean) var cr_p90_d := _contrast_y(UiKit.INK_RUST, p90) var cr_mean_d := _contrast_y(UiKit.INK_RUST, mean) var bright_ok := cr_p90_b >= DIGIT_INK_CR_P90 and cr_mean_b >= DIGIT_INK_CR_MEAN_BRIGHT var dark_ok := cr_p90_d >= DIGIT_INK_CR_P90 and cr_mean_d >= DIGIT_INK_CR_MEAN_DARK var plate := false var dark := false if bright_ok: dark = false elif dark_ok: dark = true else: plate = true # 双峰纹理:直画不可救,色牌兜底(素牌+深墨 12:1) return {"dark": dark, "plate": plate, "cr_p90_bright": cr_p90_b, "cr_mean_bright": cr_mean_b, "cr_p90_dark": cr_p90_d, "cr_mean_dark": cr_mean_d} static func _contrast_y(ink: Color, bg: float) -> float: var y := _ink_y(ink) return (maxf(y, bg) + 0.05) / (minf(y, bg) + 0.05) static func _ink_y(c: Color) -> float: return 0.2126 * _srgb_lin(c.r) + 0.7152 * _srgb_lin(c.g) + 0.0722 * _srgb_lin(c.b) # 格坐标确定性散列(禁 RNG/禁 randomize 纪律——「初始化随机落定禁逐轮重掷」圣经原则三落点: # 同格坐标恒同值,refresh/重绘/逐轮不变)。乘加混合 + 移位雪崩;64 位整型乘法回绕即哈希语义 #(GDScript int 溢出静默回绕),板域 10×10/12×12 内 mod 10/mod 5 分布均匀 static func _cell_hash(c: Vector2i) -> int: var h := c.x * 374761393 + c.y * 668265263 h = (h ^ (h >> 13)) * 1274126177 return h ^ (h >> 16) # 四正交邻墙判定(词表 §三.1:板外视为墙;未开格=墙、已开格=地板——用户世界观口径「未开=墙、 # 已开=挖出的空间」)。唯一调用面 _paint_cell(refresh 对 _minesweeper 非空守卫在先) func _is_wall(c: Vector2i) -> bool: if c.x < 0 or c.y < 0 or c.x >= _board.width or c.y >= _board.height: return true return not _minesweeper.is_revealed(c) # 墙瓦片查表(词表 §一 16 正交情形映射 + §三.2 凹角回落):nf/ef/sf/wf = 北/东/南/西邻是地板。 # 16 情形 = 0 地板(center 族)+ 3 地板×4(端头)+ 2 地板×6(条带/凸角)+ 1 地板×4(单缘) # + 4 地板(lone),逐分支穷尽 func _wall_tile_for(c: Vector2i) -> Texture2D: var nf := not _is_wall(c + Vector2i(0, -1)) var ef := not _is_wall(c + Vector2i(1, 0)) var sf := not _is_wall(c + Vector2i(0, 1)) var wf := not _is_wall(c + Vector2i(-1, 0)) if nf and ef and sf and wf: return _wall_center_tile(c) # 四邻皆墙 → 对角凹角/中心三变体(下位函数) if nf and ef and sf: return _tiles["wall_end_w"] # 仅西墙(端头·东连墙) if ef and sf and wf: return _tiles["wall_end_s"] # 仅北墙(端头·南连地板、北连墙) if nf and sf and wf: return _tiles["wall_end_e"] # 仅东墙(= end_w 镜像) if nf and ef and wf: return _tiles["wall_end_n"] # 仅南墙(端头·北向死头、南连墙) if nf and sf: return _tiles["wall_hrun"] # 上下地板 = 横条带中段 if ef and wf: return _tiles["wall_vrun"] # 左右地板 = 竖条带中段 if nf and wf: return _tiles["wall_nw"] # 凸角(上+左地板) if nf and ef: return _tiles["wall_ne"] # 凸角镜像 if sf and wf: return _tiles["wall_sw"] # 凸角(下+左地板) if sf and ef: return _tiles["wall_se"] # 凸角镜像 if nf: return _tiles["wall_n"] # 北缘(上方地板) if ef: return _tiles["wall_e"] # 东缘(镜像件) if sf: return _tiles["wall_s"] # 南缘(下方地板) return _tiles["wall_w"] # 西缘(仅西地板——16 情形穷尽收尾) # 四邻皆墙格:对角凹角优先 nw > ne > sw > se(词表 §三.2——双凹角不做专件,多对角地板回落 # 首中单凹角件,残留 1px 缝为已裁决可接受限);无对角地板 → 中心三变体按格坐标哈希 60/20/20 func _wall_center_tile(c: Vector2i) -> Texture2D: if not _is_wall(c + Vector2i(-1, -1)): return _tiles["wall_in_nw"] # 西北对角是地板 if not _is_wall(c + Vector2i(1, -1)): return _tiles["wall_in_ne"] # 东北对角是地板(镜像件) if not _is_wall(c + Vector2i(-1, 1)): return _tiles["wall_in_sw"] # 西南对角是地板 if not _is_wall(c + Vector2i(1, 1)): return _tiles["wall_in_se"] # 东南对角是地板(镜像件) var m := posmod(_cell_hash(c), 10) if m < 6: return _tiles["wall_center"] # 6/10 = 60% if m < 8: return _tiles["wall_center_crack"] # 2/10 = 20% return _tiles["wall_center_speckle"] # 2/10 = 20% # 地面瓦片五变体按格坐标哈希均匀分配(词表 §一 floor_1..5 / §三.3「同法哈希分配」; # posmod 保非负下标) func _floor_tile_for(c: Vector2i) -> Texture2D: return _floor_tiles[posmod(_cell_hash(c), _floor_tiles.size())] # ---- 自绘光标纹理(镐/旗——引擎内置光标无此语义;程序化占位,headless 跳过) ---- func _apply_custom_cursors() -> void: if DisplayServer.get_name() == "headless": return Input.set_custom_mouse_cursor(_make_cursor_texture("⛏"), Input.CURSOR_CROSS, Vector2(4, 4)) Input.set_custom_mouse_cursor(_make_cursor_texture("⚑"), Input.CURSOR_POINTING_HAND, Vector2(2, 2)) func _make_cursor_texture(glyph: String) -> ImageTexture: var img := Image.create(24, 24, false, Image.FORMAT_RGBA8) img.fill(Color(0, 0, 0, 0)) var f := ThemeDB.fallback_font if f != null: f.draw_string(img, Vector2(2, 18), glyph, HORIZONTAL_ALIGNMENT_LEFT, 20, 18, COL_DIGIT) return ImageTexture.create_from_image(img)