①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 逐位一致(重锚验证闭合,主区在飞文件全非行为面)
958 lines
47 KiB
GDScript
958 lines
47 KiB
GDScript
# 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_<件名>_<board_type>.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_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 _diggable := {} # 可挖域(8 向相邻未开格)——呼吸微光作用集
|
||
var _breath_t := 0.0
|
||
var _contact_unit: Node2D = null # 接触带承载单元(=英雄格 CellUnit;rebuild 随清场复位)
|
||
var _install_tw: Tween # 入场淡入 tween(rebuild 重入先杀)
|
||
var _ripple_tws: Array = [] # 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 _tiles_board_type := "dungeon" # 当前装载瓦片套的 board_type(rebuild 按板切换——T6 换色批)
|
||
|
||
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
|
||
|
||
|
||
# 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
|
||
|
||
|
||
# ---- 格态渲染(灰度截图可辨:完整覆尘 / 碎裂裸土 / 已开数字) ----
|
||
|
||
# 数字墨三档色温(样张 v2 冻结批 T4——art-bible §4.1:1-3 青墨/4-5 赭墨/6-8 锈墨;
|
||
# 冷=稀疏、暖=密集的氛围档,字形为唯一数值载体——禁八色分色)
|
||
func _digit_ink(d: int) -> Color:
|
||
if d <= 3:
|
||
return UiKit.INK_COLD
|
||
if d <= 5:
|
||
return UiKit.INK_OCHRE
|
||
return UiKit.INK_RUST
|
||
|
||
|
||
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
|
||
glyph = "旗"
|
||
digit_col = COL_FLAG
|
||
else:
|
||
bg = COL_BRICK
|
||
# 瓦片模式(高墙接线批 2026-09-26):tex_base = 查表瓦片(未开=墙瓦片/已开=地面瓦片),
|
||
# bg_sb 降级为透明描边载体——只承载呼吸微光/主角蓝框(CellUnit 瓦片态画在瓦片之上),
|
||
# 程序化平色退役;瓦片分配=格坐标哈希,初始化落定后逐拍稳定(禁逐轮重掷——圣经原则三)
|
||
var sb: StyleBoxFlat
|
||
var tile: Texture2D = null
|
||
if _tiles_ok:
|
||
# Color.TRANSPARENT 载体(引擎常量——视图禁内联色字面量纪律;透明+零描边=瓦片上不画任何东西)
|
||
sb = UiKit.panel_flat(Color.TRANSPARENT, Color.TRANSPARENT, 3, 0)
|
||
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:
|
||
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
|
||
u.plate = revealed and glyph != "" # 数字色牌(原则二——已开格字形全垫牌;旗在未开格不垫)
|
||
u.outline_sides = []
|
||
u.facade_south = false
|
||
u.proj_band = 0.0
|
||
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 描边让位防三重叠黑
|
||
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
|
||
# ②' 变异角标(迭代 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 = []
|
||
var corners: Array = []
|
||
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
|
||
# 六镜像件(词表 §二: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
|
||
|
||
|
||
# 整批回落(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)
|
||
|
||
|
||
# 格坐标确定性散列(禁 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)
|