931 lines
43 KiB
GDScript
931 lines
43 KiB
GDScript
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||
## 战斗屏 D 区棋盘视图:格态渲染、悬停预承诺光标与路径预览、奖励徽章与玩家意图识别(意图经信号外发)。
|
||
## 素材禁 preload——一律 UiKit.load_tex 装载(素材不入 git),任一缺位静默回退程序化平色/字形,不刷错误。
|
||
class_name BoardView
|
||
extends Control
|
||
|
||
signal action_requested(cell: Vector2i, kind: String) ## kind ∈ {dig, move, seal_place, seal_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 _TILE_PALETTE := preload("res://presentation/views/tile_palette.gd")
|
||
const _HERO_SPRITE := preload("res://presentation/views/hero_sprite.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 # 小人边框——小人蓝
|
||
const COL_SEAL := UiKit.SEAL_BLACK # 封印黑 #000000
|
||
const COL_GOLD := UiKit.GOLD # 奖励金 #F0A828
|
||
const COL_CONTACT := UiKit.CONTACT_GLOW # 接触带微光基色(蓝族浅一档·近白蓝——§3.2 v2.8)
|
||
const COL_HL_TARGET := UiKit.GOLD # 悬停目标格边框(金)
|
||
const REWARD_BADGE := { # 已开格格面资源身份 icon(cell_type 九型键)
|
||
"attack": "攻", "defense": "守", "heal": "医", "blast": "爆", "weaken": "弱", "pierce": "穿", "magic": "魔",
|
||
"action": "行", # 行 = 行动点型(ap_up/ap_drain 两行)
|
||
"gp": "金", # 金 = GP 型(gp_gain/gp_loss 两行;金色系 ~40° 相位,禁入带紫赤 300–345° 外,取色 UiKit.GOLD 徽标色单点)
|
||
}
|
||
const BADGE_PLACEHOLDER := "?" # 未知图标键占位纹样(HUD框架:禁静默空图标)
|
||
const CURSOR_DIG := Input.CURSOR_CROSS # 挖——镐形自绘纹理载体
|
||
const CURSOR_SEAL := Input.CURSOR_POINTING_HAND # 封印——封印形自绘纹理载体
|
||
const CURSOR_MOVE_SEM := Input.CURSOR_ARROW # 移动——箭头(命名避开 Control.CURSOR_MOVE 原生成员遮蔽)
|
||
const CHORD_WINDOW_MS := 350
|
||
const SEP := 2 # 格间距(格尺寸公式与命中换算共用)
|
||
|
||
# 棋盘立体参数
|
||
const DEPTH_PX := 8.0 # 悬崖厚度(像素)
|
||
const DEPTH_BEVEL := 0.6 # 顶面倒角强度
|
||
# 高墙感 D 投影档:地面投影带宽 = 格边比(研究件 64px 拼版实测 14/64≈0.22;
|
||
# 立面/投影色参数在 CellUnit 常量——绘制侧单点)
|
||
const WALL_PROJECTION_FRAC := 0.22
|
||
|
||
# 英雄单位演出参数单点 = HeroSprite 常量族(本处只保留探针直读面别名,数值禁二写)
|
||
const HERO_SPRITE_SCALE := _HERO_SPRITE.SPRITE_SCALE # 1.5× 档(探针 case⑨/⑩ 直读面)
|
||
const HERO_GROUND_FRAC := _HERO_SPRITE.GROUND_FRAC # 脚锚纵向落点 25%(探针 case⑦/⑩ 直读面)
|
||
|
||
# 瓦片纹理路径与 16 墙瓦片词表单点 = TilePalette 常量族(装载/查表同件;词表注释见该件)
|
||
|
||
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() # 地形变异 角标回调(只读面)——Vector2i → String("flat_damage"=伤害型变异未开格;""=无)
|
||
var _treasure_reveal := {} # 秘宝 真视集(Vector2i → true;组合根 set_treasure_overlays 注入投影)
|
||
var _treasure_preview := {} # 秘宝 预读集(同上——未开格徽章呈现挂点)
|
||
var _diggable := {} # 可挖域(8 向相邻未开格)——呼吸微光作用集
|
||
var _glow: Dictionary[Vector2i, int] = {} # 未开格类型微光作用集(Vector2i → 组号;=可挖域∩身份可查)
|
||
var _type_glow := true # 呈现态布尔:生产默认开;探针/夹具/调试浮窗可显式 off
|
||
var _glow_errors: Array[String] = [] # 清单外 cell_type 登记(禁静默空光)
|
||
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 格尺寸公式)
|
||
|
||
# ── 拆出件(状态单点在件内)──
|
||
var _palette = _TILE_PALETTE.new() # 瓦片件:装载缓存+查表+明度/墨族静态算法
|
||
var _hero_sprite = _HERO_SPRITE.new() # 英雄精灵件:_init 挂为本件子级(tween 需在树上)
|
||
|
||
# ── 件成员同名转发(探针直读面零改动:读 _palette.seal_tex、读写 _hero_sprite.tex_idle;其余为随迁状态单点再导出)──
|
||
# ---- 视野迷雾(纯呈现层;现行事实源=HUD框架「视野迷雾」节 v0.13,渲染规格=art-bible §6.5 v3.5)----
|
||
const FOG_RADIUS := 8 # 菱形灯光区基准半径(曼哈顿距离 ≤R——与微光环「周围 8 格」同圈起步;半径基准值改动=非白名单须用户)
|
||
var _fog_enabled := true # 迷雾开关(呈现态布尔——真实游戏恒 on;探针/夹具可 off=无迷雾基线,两态结算逐位一致断言面)
|
||
var _fog_lantern := 0 # 视野加成现值(缺省 0=无加成基线零扰动;生产写入面=组合根每战装配链显式重算——main._sync_treasure_glow)
|
||
|
||
# 视野加成接线:light_radius_bonus 装配参数(magnitude 4→半径 8+4=12 格 provisional);类型微光
|
||
# 不随视野扩〔微光锚=可挖域贴身 8 邻零改动〕。组合根 main._sync_treasure_glow 每战首轮装配链显式重算。
|
||
func set_fog_glow_bonus(bonus: int) -> void:
|
||
_fog_lantern = maxi(0, bonus)
|
||
refresh() # 灯光区半径变了——全板重算雾态(refresh/notify_hero_move 既有重绘驱动复用)
|
||
|
||
var cell_size: float: # 只读属性(棋盘格边长对外契约出口)
|
||
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 区底色
|
||
_svp_container.add_child(_viewport)
|
||
_world = _BOARD_WORLD.new()
|
||
_viewport.add_child(_world)
|
||
_hero_sprite.bind(_world, _svp_container)
|
||
_hero_sprite.set_cell_size(_cell, float(SEP))
|
||
add_child(_hero_sprite)
|
||
_desat_mat = ShaderMaterial.new()
|
||
_desat_mat.shader = load("res://presentation/views/desaturation.gdshader")
|
||
_desat_mat.set_shader_parameter("strength", 0.0)
|
||
_hero_sprite.tex_idle = UiKit.load_tex(UiKit.TEX_HERO_IDLE)
|
||
_hero_sprite.tex_walk = UiKit.load_tex(UiKit.TEX_HERO_WALK)
|
||
_palette.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
|
||
|
||
|
||
# 视野迷雾开关(呈现态布尔:真实游戏恒 on;探针/夹具 off=无迷雾基线,
|
||
# 迷雾开/关两态结算逐位一致断言面;切换即全板重算 fog_state)
|
||
func set_fog_enabled(on: bool) -> void:
|
||
_fog_enabled = on
|
||
refresh()
|
||
|
||
|
||
# 组合根注入 地图战斗资源 身份解析只读回调(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
|
||
|
||
|
||
# 组合根注入 地形变异 角标只读回调(flat_damage 未开格视觉标记;变异是策略纹理非数值墙:
|
||
# 玩家可据此绕行/承担的决策输入。空 Callable = 全板无标记(缺位回退态)
|
||
func set_variant_mark_resolver(cb: Callable) -> void:
|
||
_vmark_cb = cb
|
||
|
||
# 秘宝叠加层投影注入(组合根在 use 成功/三清后调;refresh 消费)
|
||
func set_treasure_overlays(reveal_cells: Array, preview_cells: Array) -> void:
|
||
_treasure_reveal.clear()
|
||
_treasure_preview.clear()
|
||
for c in reveal_cells:
|
||
_treasure_reveal[Vector2i(c)] = true
|
||
for c in preview_cells:
|
||
_treasure_preview[Vector2i(c)] = true
|
||
refresh()
|
||
|
||
|
||
# SubViewport 主题/字体显式注入挂点(SubViewport 不自动继承主场景 Control 主题;
|
||
# 本项目无自定义主题,缺省 = 引擎回退字体)
|
||
func set_world_font(f: Font) -> void:
|
||
_world.board_font = f
|
||
for c in _cells:
|
||
_cells[c].st.font = f
|
||
_cells[c].queue_redraw()
|
||
|
||
|
||
# D 区可用面积(AP 条之外)→ 格边长 = floor((min(宽, 高) − 预留 − (N−1)×格间距) / N)(battle.md 格尺寸公式)
|
||
# 公式须补算 (N−1)×2px 网格间距与垂直预留 24px,否则 12×12 板实高溢出可用区 ~6px,下缘被 clip_contents 裁掉。
|
||
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))
|
||
_hero_sprite.set_cell_size(_cell, float(SEP))
|
||
var stride := _cell + float(SEP)
|
||
for c in _cells:
|
||
var u: Node2D = _cells[c]
|
||
u.st.edge = _cell
|
||
u.position = Vector2(c) * stride
|
||
if _world.hero != null and _hero_sprite.last.x >= 0:
|
||
_place_hero(_hero_sprite.last, _hero_sprite.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: MinesweeperRules) -> void:
|
||
_board = board
|
||
_minesweeper = minesweeper
|
||
_palette.bind_board(_board, _minesweeper) # 查表族棋盘态随板同步(换板后首拍 refresh 前置)
|
||
for child in _world.get_children():
|
||
child.queue_free()
|
||
_world.hero = null # 英雄精灵随清场复位——下拍 refresh 经 ensure_hero 重建(首建即时落位)
|
||
_cells.clear()
|
||
_glow.clear()
|
||
_glow_errors.clear()
|
||
_contact_unit = null
|
||
_hero_sprite.last = Vector2i(-99, -99)
|
||
_r0.clear()
|
||
_hover = Vector2i(-9, -9)
|
||
for c in _board.compute_r0():
|
||
_r0[c] = true
|
||
# 瓦片套随板图型切换(board_gen 词表 v1.1 dungeon/underdark/deep_underdark;
|
||
# 同图型重复 rebuild 零重载,_palette.tiles_board_type 守卫)
|
||
var bt: String = _board.board_type if _board.board_type != "" else "dungeon"
|
||
if bt != _palette.tiles_board_type:
|
||
_palette.load_board_tiles(bt)
|
||
var stride := _cell + float(SEP)
|
||
for y in _board.height:
|
||
for x in _board.width:
|
||
var c := Vector2i(x, y)
|
||
_cells[c] = _build_cell_unit(c, stride)
|
||
set_cell_area(size)
|
||
_install_entrance_fx()
|
||
refresh()
|
||
|
||
|
||
# 逐格构建器(rebuild 建格循环唯一出函面):.new() 与属性装配逐字迁入,循环体只调构建器
|
||
func _build_cell_unit(c: Vector2i, stride: float) -> Node2D:
|
||
var u: Node2D = _CELL_UNIT.new()
|
||
u.st.cell = c
|
||
u.st.edge = _cell
|
||
u.position = Vector2(c) * stride
|
||
u.st.bevel = DEPTH_BEVEL # 新建格单元继承
|
||
u.use_parent_material = true # 去饱和材质挂 BoardWorld——单元随世界材质灰化
|
||
if _palette.tiles_ok:
|
||
# NEAREST 采样(32×32 源 × 45/36px 格边非整数缩放接受档,
|
||
# 与英雄小人 1.5× 豁免同级(圣经 §8.2);只在瓦片
|
||
# 模式启用——程序化回退路径渲染零扰动(含格面文字采样不回退不变化)
|
||
u.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||
u.st.font = _world.board_font
|
||
_world.add_child(u)
|
||
return u
|
||
|
||
|
||
# 逐格重绘(挖掘/结算后调用)
|
||
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) and not _minesweeper.is_removed(n):
|
||
_diggable[n] = true
|
||
_recompute_glow(hero)
|
||
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 驱动
|
||
|
||
|
||
# 未开格类型微光作用集:= 可挖域 ∩ 身份可查格 ∩ 灯光区内。
|
||
# 组号 = cell_type 经 UiKit.glow_group_for_cell_type 九值封闭判定得出;清单外值 = 数据错,
|
||
# 登记进 _glow_errors 由断言面显式暴露,禁静默空光(图标键错误处理同款纪律)。
|
||
# 纯派生:只读 query_identity 回调,零玩法数据写入(🔴 契约 1)。
|
||
# 相位门控与接触带同源(_dig_glow):结算/道具窗口态不显示——art-bible §2.1 挖掘期纪律面。
|
||
func _recompute_glow(hero: Vector2i) -> void:
|
||
_glow.clear()
|
||
if not _type_glow or not _dig_glow:
|
||
return
|
||
for n in _diggable:
|
||
if _r0.has(n):
|
||
continue # 出生展开区不入摇号清单 ⇒ query 恒 null,显式跳过(省回调、零诊断噪音)
|
||
var fdist: int = absi(n.x - hero.x) + absi(n.y - hero.y)
|
||
if fdist > FOG_RADIUS + _fog_lantern:
|
||
continue # 灯光区外未开格全黑不渲染(§6.5)——微光亦不画
|
||
var info: Variant = _identity_cb.call(n) if _identity_cb.is_valid() else null
|
||
if info == null:
|
||
continue
|
||
var ctype := String((info as Dictionary).get("cell_type", ""))
|
||
var g: int = UiKit.glow_group_for_cell_type(ctype)
|
||
if g == UiKit.GLOW_NONE:
|
||
# 登记去重:cell_type 随板摇定不可变,同一数据错在相位切换多次重算中只记一次
|
||
# (否则 set_dig_glow_enabled 每切一次 _sync_glow_to_units 就追加一条,数组无限增长)
|
||
if not _glow_errors.has(ctype):
|
||
_glow_errors.append(ctype)
|
||
continue
|
||
_glow[n] = g
|
||
|
||
|
||
# 未开格类型微光开关:呈现态布尔,生产写入面 = 组合根单点。
|
||
# 不新增设置项——工程有闸、界面无感;off 态为基线保护面(结算逐位一致断言承接)。
|
||
func set_type_glow_enabled(on: bool) -> void:
|
||
if _type_glow == on:
|
||
return
|
||
_type_glow = on
|
||
if _board != null and _minesweeper != null:
|
||
refresh()
|
||
|
||
|
||
# ---- 入场演出(battle.md Transitions「install 淡入 200ms + 初始 R0 展开涟漪」)----
|
||
# 全板 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)
|
||
|
||
|
||
# ---- 英雄单位精灵(件 = hero_sprite.gd;本视图保留调用面与探针直读转发) ----
|
||
|
||
# refresh diff 驱动(件内实现:位置变化 = 位移演出;零位移但受理移动回执 = 只播帧闪)
|
||
func _update_hero(hero: Vector2i) -> void:
|
||
_hero_sprite.update_hero(hero)
|
||
|
||
|
||
# 英雄落位(instant = 即位无演出——首建/格边长变更;否则 140ms 位移 + 走路帧起手、到位回站帧)
|
||
func _place_hero(from: Vector2i, to: Vector2i, instant: bool) -> void:
|
||
_hero_sprite.place_hero(from, to, instant)
|
||
|
||
|
||
# 受理移动指令回执(组合根 settle 受理后、refresh 前调用)——原地移动经此触发帧闪;
|
||
# 目标异格时本封印标被 diff 位移路径自然吞掉(置位即清)
|
||
func notify_hero_move() -> void:
|
||
_hero_sprite.notify_move()
|
||
|
||
|
||
# 挖掘角色弹跳(计划书 §3.C:scale.y 1→0.82→1,脚底锚收敛不悬空;旧弹跳先 kill)
|
||
func play_hero_dig_bounce() -> void:
|
||
_hero_sprite.play_dig_bounce()
|
||
|
||
|
||
# 格左上角(board_area 全幅坐标系)——fx 同层演出定位出口(本视图与 fx 同为 board_area 全幅子件,
|
||
# 局部坐标同系;cell_center_local 同公式族)
|
||
func cell_origin(c: Vector2i) -> Vector2:
|
||
return _svp_container.position + Vector2(c) * (_cell + float(SEP))
|
||
|
||
|
||
# 英雄持握手位(fx 同层坐标系 = 壳层坐标——件内换算含 _svp_container.position 居中偏移:
|
||
# 世界脚底锚 + 壳层偏移即得)
|
||
func hero_hand_point() -> Vector2:
|
||
return _hero_sprite.hand_point()
|
||
|
||
|
||
# 英雄精灵缩放档(fx 挥铲同档出口——两演出比例恒一致)
|
||
func hero_sprite_scale() -> float:
|
||
return _hero_sprite.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)
|
||
|
||
|
||
# 整图公式单点:格坐标 = floor(板内局部坐标 / 格步长)
|
||
#(格步长 = 格边长 + 间距;板外/负域返回 (-9,-9);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
|
||
|
||
|
||
# 真实输入路径(Node2D 迁移批 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_seal(c):
|
||
action_requested.emit(c, "seal_place")
|
||
# 封印 = 一次性承诺:右键对封印格零动作(无撤封印);
|
||
# 左键相邻封印格直接挖(规则引擎放行)。
|
||
|
||
|
||
# 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 seal: bool = _minesweeper.has_seal(c)
|
||
var adjacent: bool = _diggable.has(c)
|
||
if revealed:
|
||
u.st.hl_visible = true
|
||
u.st.hl_color = COL_HL_TARGET # 移动目标格边框(金)
|
||
_set_cursor_shape(c, CURSOR_MOVE_SEM)
|
||
_show_path_preview(c)
|
||
elif adjacent:
|
||
u.st.hl_visible = true
|
||
u.st.hl_color = COL_HL_TARGET # 挖掘目标格边框(相邻封印格同此——左键直接挖)
|
||
_set_cursor_shape(c, CURSOR_DIG)
|
||
elif seal:
|
||
u.st.hl_visible = true
|
||
u.st.hl_color = COL_SEAL # 已承诺封印标记(封印黑;无交互可供)
|
||
_set_cursor_shape(c, Input.CURSOR_FORBIDDEN)
|
||
else:
|
||
u.st.hl_visible = true
|
||
u.st.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].st.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:
|
||
# 合法性指示:悬停已开格 → BFS 路径蓝细线点迹(compute_move_path 只读面)
|
||
if _minesweeper == null:
|
||
return
|
||
var path: Array[Variant] = _minesweeper.compute_move_path(c)
|
||
for pc in path:
|
||
if _cells.has(pc):
|
||
_cells[pc].st.dot_visible = true
|
||
|
||
|
||
func _hide_path_preview() -> void:
|
||
for c in _cells:
|
||
_cells[c].st.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[Variant, Variant] = _minesweeper.reveal_record(c) # 已开记录经查询面(禁直读 排雷玩法 状态字典)
|
||
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 九值枚举)
|
||
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 冻结④):亮墨族 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;选族判据见 TilePalette.digit_ink_mode)
|
||
func _digit_ink(d: int) -> Color:
|
||
if _palette.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 seal: bool = _minesweeper.has_seal(c)
|
||
var removed: bool = _minesweeper.is_removed(c) # 中立移除格(退色降档)
|
||
var is_hero: bool = c == hero
|
||
# 封印槽统一维护:封印格=封印印记纹理;其余格清 null(封印格挖开后残留在 tex_seal
|
||
# 会让 _draw 无条件把封印印画在已开格上——tex_seal 绘制无 revealed 条件)。缺盘走字形回退。
|
||
u.st.tex_seal = _palette.seal_tex if (seal and not revealed and _palette.seal_tex != null) else null
|
||
if removed:
|
||
revealed = false # 移除格固定未开呈现基座(退色暗幕层承载;危险格本无数字,周边数字冻结)
|
||
seal = false
|
||
u.st.tex_seal = null
|
||
# 绘制层五段(字形与色牌→基底瓦片→立面投影→秘宝/变异叠加→迷雾)——段间交换经单元状态面
|
||
#(u.st/u.glyph/u.glyph_color),唯一例外=基底色 bg 经 ink 段返回值交 base 段(无单元场域承载)
|
||
var bg: Color = _paint_cell_ink(c, u, revealed, seal)
|
||
_paint_cell_base(c, u, revealed, is_hero, bg)
|
||
_paint_cell_facade(c, u, revealed)
|
||
_paint_cell_overlays(c, u, revealed)
|
||
_paint_cell_fog(c, u, revealed, hero)
|
||
u.st.removed_dim = removed
|
||
u.queue_redraw()
|
||
|
||
|
||
# 字形与色牌段:reveal 记录→字形/墨色判定+u.glyph/u.glyph_color 终写+双峰纹理板落牌;
|
||
# 返回基底色 bg(平色回退 sb 构建的唯一消费方在基底段)
|
||
func _paint_cell_ink(c: Vector2i, u: Node2D, revealed: bool, seal: bool) -> Color:
|
||
var glyph := ""
|
||
var bg: Color
|
||
var digit_col := COL_DIGIT
|
||
if revealed:
|
||
var rec: Dictionary[Variant, Variant] = _minesweeper.reveal_record(c) # 已开记录经查询面
|
||
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) # 数字墨三档色温(§4.1)
|
||
else:
|
||
if seal:
|
||
bg = COL_BRICK
|
||
if _palette.seal_tex == null:
|
||
glyph = "封"
|
||
digit_col = COL_SEAL
|
||
else:
|
||
bg = COL_BRICK
|
||
u.glyph = glyph
|
||
u.glyph_color = digit_col
|
||
# 色牌后备(数字直画退牌;「危」/「?」维持牌载保读性)+ v3.4:双峰纹理板
|
||
#(_palette.digit_plate——underdark 系亮白筋+近黑斑并存的直画不可救纹理)数字落牌,牌上深墨 12:1
|
||
var plate_digit: bool = _palette.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)
|
||
return bg
|
||
|
||
|
||
# 基底瓦片段:瓦片查表/平色回退 sb+英雄蓝框载体;英雄贴图缺位回退字形「人」在此段落
|
||
# u.glyph(先于叠加段消费,结果与原函数字形累计局部量逐字同值)
|
||
func _paint_cell_base(c: Vector2i, u: Node2D, revealed: bool, is_hero: bool, bg: Color) -> void:
|
||
# 瓦片模式:tex_base = 查表瓦片(未开=墙瓦片/已开=地面瓦片),
|
||
# bg_sb 降级为透明描边载体——只承载呼吸微光/主角蓝框(CellUnit 瓦片态画在瓦片之上),
|
||
# 瓦片分配=格坐标哈希,初始化落定后逐拍稳定(禁逐轮重掷——圣经原则三)
|
||
# draw calls 批优化:透明载体 sb 视觉贡献=零(透明底+零描边——不可见),但
|
||
# StyleBoxFlat 多边形命令不可合批,每格打断棋盘批——实测 100 格满负荷帧 ~402 DC(A/B 探针
|
||
# 阶梯:纹理统一 0 增益/去 glyph −23/去叠层 −23/去隐形 sb −402)。非英雄格 sb 直接置 null
|
||
#(绘制路径零像素变化),仅英雄格保留蓝框载体。
|
||
var sb: StyleBoxFlat = null
|
||
var tile: Texture2D = null
|
||
if _palette.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),UiKit.RADIUS_OBJECT)
|
||
if is_hero:
|
||
if sb == null:
|
||
sb = UiKit.panel_flat(Color.TRANSPARENT, Color.TRANSPARENT,UiKit.RADIUS_OBJECT, 0)
|
||
sb.set_border_width_all(3)
|
||
sb.border_color = COL_HERO
|
||
if u.glyph == "" and _hero_sprite.tex_idle == null:
|
||
# 贴图缺位回退字形「人」(项目纹理槽惯例——贴图在位=格内字形退役,单位本体由独立精灵承载)
|
||
u.glyph = "人"
|
||
u.glyph_color = COL_HERO
|
||
u.st.tex_base = tile
|
||
u.st.bg_sb = sb
|
||
|
||
|
||
# 立面投影段:未开墙格南缘立面/轮廓描边码;已开格北缘投影暗带/E 格高墙面——段内重置与赋值同序
|
||
func _paint_cell_facade(c: Vector2i, u: Node2D, revealed: bool) -> void:
|
||
u.st.outline_sides.assign([]) # typed Array[int](assign 保持引用语义)
|
||
u.st.facade_south = false
|
||
u.st.proj_band = 0.0
|
||
u.st.tex_facade = null
|
||
u.st.facade_strip = false
|
||
# 封印槽不在此重置:尾部重置会与此上方条件赋值构成双写踩踏(把封印纹理恒清 null=素材在位时封印不可见);
|
||
# 上方 else 分支已是全路径清残留,尾部重置系冗余且有害
|
||
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 投影档:南邻地面格 → 墙格南缘立「口沿高光+双段暗带」立面
|
||
#(邻地面才画——墙-墙内界保墙群内聚);板外=墙不画
|
||
u.st.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.st.facade_south:
|
||
continue # 南缘由立面件承载(高光+双段暗带)——2px 描边让位防三重叠黑
|
||
if code == 0 and _palette.tiles_ok:
|
||
continue # v3.5 冻结① (b)-(c) 交界无边框:墙面直贴接墙顶(E 格墙面件在邻格承载)——北向描边不画(平色回退态保留描边)
|
||
u.st.outline_sides.append(code)
|
||
else:
|
||
# 地面投影:北邻墙格在本已开格北缘投渐隐暗带(ITB 同款高度线索——带宽按格边比例常量)
|
||
if north_in and not _minesweeper.is_revealed(north_n):
|
||
u.st.proj_band = u.st.edge * WALL_PROJECTION_FRAC
|
||
# E 格高墙面(圣经 v3.5 冻结①——方向稿 is_facade_cell 口径):已开格南邻未开墙 → 南部 30%
|
||
# 承接墙面(材质=同板墙顶 center 瓦片,原亮度直贴);仅瓦片态启用——平色回退保持原语义
|
||
if south_in and not _minesweeper.is_revealed(south_n) and _palette.tiles_ok:
|
||
u.st.facade_strip = true
|
||
u.st.tex_facade = _palette.tiles.get("wall_center")
|
||
|
||
|
||
# 秘宝/变异叠加段:真视红框/预读徽章/变异角标/奖励徽章/类型微光组号——glyph/digit_col 局部镜像
|
||
# 自 u.glyph/u.glyph_color(原函数字形累计局部量的段内重建,预读徽章分支的无效局部写入逐字保留)
|
||
func _paint_cell_overlays(c: Vector2i, u: Node2D, revealed: bool) -> void:
|
||
# ②( 秘宝叠加层:真视格=红内缩框;预读格=奖励身份徽章(identity_resolver 既有
|
||
# 只读面取字形——零数据复制,badge 位复用);已开格标记自然退位(refresh 逐拍重算)
|
||
var glyph: String = u.glyph
|
||
var digit_col: Color = u.glyph_color
|
||
if not revealed:
|
||
u.st.treasure_mark = "mine_reveal" if _treasure_reveal.has(c) else ""
|
||
if _treasure_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.st.badge_glyph = glyph
|
||
# ②' 变异角标:未开格带伤害型变异 → 危险红角标(已开=威胁已兑现标记退位);
|
||
# 回调缺位 = 无标记(程序化角标,tex_mutation 纹带槽暂留)
|
||
if not revealed and _vmark_cb.is_valid():
|
||
u.st.mutation_mark = String(_vmark_cb.call(c))
|
||
else:
|
||
u.st.mutation_mark = ""
|
||
u.st.badge_glyph = _update_badge(c)
|
||
u.st.glow_group = int(_glow.get(c, UiKit.GLOW_NONE))
|
||
|
||
|
||
# 迷雾段:灯光区外未开格全黑/已开格记忆态灰显——纯呈现零逻辑(🔴 契约 1)
|
||
func _paint_cell_fog(c: Vector2i, u: Node2D, revealed: bool, hero: Vector2i) -> void:
|
||
# 视野迷雾呈态:曼哈顿距离 > 半径 → 区外——未开格全黑(现行事实源照抄)/
|
||
# 已开格记忆态(灰显可读);以英雄格为心逐格重算,refresh()/notify_hero_move() 既有重绘驱动=移动实时跟随。
|
||
# 纯呈现零逻辑——格态/数字真值/清图判定零触碰(🔴 契约 1);结算期去饱和叠加互斥由 CellUnit 记忆态终态档承载。
|
||
if _fog_enabled:
|
||
var fdist := absi(c.x - hero.x) + absi(c.y - hero.y)
|
||
var fogged := fdist > FOG_RADIUS + _fog_lantern
|
||
u.fog_state = (2 if revealed else 1) if fogged else 0
|
||
else:
|
||
u.fog_state = 0
|
||
|
||
|
||
# ---- 可挖域接触带呼吸微光(挖掘期唯一在动元素——§3.2 v2.8 画法:光画英雄格周界空间侧,
|
||
# 正交邻墙=整条边、斜向=共享角点+两侧各半条边) ----
|
||
|
||
func set_dig_glow_enabled(on: bool) -> void:
|
||
_dig_glow = on
|
||
if on:
|
||
_apply_contact_bands() # 相位重入(结算→挖掘):refresh 未跑也重挂带几何
|
||
else:
|
||
_reset_breath()
|
||
_sync_glow_to_units() # 相位切换 → 类型微光作用集随之增减(只重绘变动格)
|
||
|
||
|
||
# 类型微光作用集与格件同步:只重绘「上一拍有组/这一拍有组」的并集格,
|
||
# 其余格零触碰;与 refresh() 全板重绘互为补充(refresh 走全板,本函数走增量)。
|
||
func _sync_glow_to_units() -> void:
|
||
if _board == null or _minesweeper == null:
|
||
return
|
||
var prev: Dictionary[Vector2i, int] = _glow.duplicate()
|
||
_recompute_glow(_minesweeper.hero_pos())
|
||
var touched := {}
|
||
for c in prev:
|
||
touched[c] = true
|
||
for c in _glow:
|
||
touched[c] = true
|
||
for c in touched:
|
||
var u: Node2D = _cells.get(c)
|
||
if u != null:
|
||
u.st.glow_group = int(_glow.get(c, UiKit.GLOW_NONE))
|
||
|
||
|
||
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.st.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.st.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.st.contact_bands = bands
|
||
hu.st.contact_corners = corners
|
||
# 减动效:呼吸停格——refresh/enable 重放时施**静态**色(本件头注「已施静态色」的落実);
|
||
# 0.5 = 呼吸周期中值(正常态 k 的均值)。否则减动效下每次 refresh 都会按当前相位重施色,
|
||
# 带色在 refresh 间跳变=持续运动假象,且令 on/off 双帧取证无法隔离单一变量。
|
||
var k := 0.5 if UiKit.motion_scale < 1.0 else 0.5 + 0.5 * sin(_breath_t * 3.0)
|
||
hu.st.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.st.contact_col = Color(0, 0, 0, 0)
|
||
_contact_unit.queue_redraw()
|
||
|
||
|
||
# ---- 瓦片装载与查表(件 = tile_palette.gd;本视图仅保留格面渲染消费面) ----
|
||
|
||
# 墙瓦片查表(词表 §一 16 正交情形映射 + §三.2 凹角回落;查表用棋盘态由件持 bind_board 同步)
|
||
func _wall_tile_for(c: Vector2i) -> Texture2D:
|
||
return _palette.wall_tile_for(c)
|
||
|
||
|
||
# 地面瓦片五变体按格坐标哈希分配(词表 §三.3「同法哈希分配」)
|
||
func _floor_tile_for(c: Vector2i) -> Texture2D:
|
||
return _palette.floor_tile_for(c)
|
||
|
||
|
||
# ---- 自绘光标纹理(镐/封印——引擎内置光标无此语义;程序化占位,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)
|
||
|
||
|
||
# ---- 探针只读出口(形态仿 fx_probe 先例:只读快照) ----
|
||
|
||
# 格件映射只读快照(Vector2i → CellUnit;键面同构——断言语义零变化;破私有面的只是容器字典,
|
||
# 格件字段本为公有呈现面,快照值即原格件引用)
|
||
func cells_probe() -> Dictionary:
|
||
return _cells.duplicate()
|
||
|
||
|
||
# 可挖域只读快照(Vector2i → true;8 向相邻未开格=呼吸微光作用集)
|
||
func diggable_probe() -> Dictionary:
|
||
return _diggable.duplicate()
|
||
|
||
|
||
# 悬停格只读出口(case① 悬停到达断言消费面)
|
||
func hover_probe() -> Vector2i:
|
||
return _hover
|
||
|
||
|
||
# 迷雾参数只读快照(case⑮ 参数漂移断言消费面)
|
||
func fog_params_probe() -> Dictionary:
|
||
return {"lantern": _fog_lantern, "enabled": _fog_enabled}
|
||
|
||
|
||
# 类型微光只读快照(断言消费面——开关态/作用集与组号/清单外 cell_type 登记)
|
||
func type_glow_probe() -> Dictionary:
|
||
return {"enabled": _type_glow, "cells": _glow.duplicate(), "errors": _glow_errors.duplicate()}
|
||
|
||
|
||
# 格件全局矩形只读出口(像素侧取样坐标源——禁测试侧硬编码像素)
|
||
# 坐标系:格件居于 SubViewport 内(_world 于原点),global_position 是**子视口画布**坐标;
|
||
# 取证帧截的是窗口画面 ⇒ 必须经 SubViewportContainer 的全局矩形换算回窗口坐标
|
||
# (stretch 恒 1:1,仍按 host/vp 比例换算保稳健——容器中心化由 CenterContainer 驱动)。
|
||
func cell_rect_probe(c: Vector2i) -> Rect2:
|
||
var u: Node2D = _cells.get(c)
|
||
if u == null or not is_instance_valid(u):
|
||
return Rect2()
|
||
var host: Rect2 = _svp_container.get_global_rect()
|
||
var vp_size: Vector2 = _viewport.size
|
||
if host.size.x <= 0.0 or host.size.y <= 0.0 or vp_size.x <= 0.0 or vp_size.y <= 0.0:
|
||
return Rect2()
|
||
var k: Vector2 = host.size / vp_size
|
||
var local := Rect2(u.global_position, Vector2(u.st.edge, u.st.edge))
|
||
return Rect2(host.position + local.position * k, local.size * k)
|