merge: 棋盘小人贴图替换批——英雄独立精灵+挥铲三帧+站桩接线(2026-09-25)

This commit is contained in:
DKChan committed 2026-09-25 03:05:13 +08:00
commit b0880919ee
7 files changed
+212 -3

No files matched your search

+8
View File
@@ -685,6 +685,14 @@ func _on_board_action(cell: Vector2i, kind: String) -> void:
# runner 置空,清图演出判定须先判 settlement(基线既有重入隐患,2026-09-13 加固)
if accepted and kind == "dig" and settlement != null and settlement.minesweeper.current_board() != null and settlement.minesweeper.clear_map_cleared():
board_zone.fx.play_clear_burst()
if accepted and kind == "dig" and settlement != null and settlement.minesweeper != null:
# 挖掘演出(棋盘小人贴图批 2026-09-25):挥铲三帧 + 角色弹跳——受理后一拍触发(挖掘不移动英雄,
# 出手方向 = 目标格−英雄格;refresh 同拍重绘零冲突)
var hero_cell: Vector2i = settlement.minesweeper.hero_pos()
board_zone.fx.play_shovel_swing(board_view.cell_origin(hero_cell), cell - hero_cell, board_view.cell_size)
board_view.play_hero_dig_bounce()
elif accepted and kind == "move":
board_view.notify_hero_move() # 原地移动(目标=当前格幂等受理)帧闪回执——refresh diff 前置告知
board_view.refresh()
_refresh_presentations()
@@ -35,6 +35,7 @@ func _init(db_ref) -> void:
vb.add_child(row)
hero_standee = StandeeView.new("hero")
hero_standee.set_portrait_texture(UiKit.load_tex(UiKit.TEX_HERO_IDLE)) # 棋盘小人贴图批 2026-09-25(挂点现成——null 回退字形「人」;NEAREST 归 set_portrait_texture 单点)
row.add_child(hero_standee)
var center := CenterContainer.new()
+45
View File
@@ -120,6 +120,51 @@ func play_flag_hit_projectile(heavy: bool) -> void:
tw.tween_callback(shot.queue_free)
# ---- 挖掘挥铲三帧(棋盘小人贴图批 2026-09-25——计划书 §3.C 解耦方案:铲归 fx 层、角色弹跳归
# BoardView.play_hero_dig_bounce,两演出零共享属性) ----
const SHOVEL_FRAME_PX := 32.0 # 铲帧画布边长(缩放比与英雄帧同源)
const SHOVEL_FRAME_S := 0.08 # ~80ms/帧(f0→f1→f2 共 ~240ms 即收)
const SHOVEL_GRIP := Vector2(18, 12) # 握把锚点(素材侧锁定的画布坐标——出手位对齐点)
var _shovel_texs: Array = [] # 惰性装载缓存(素材缺位 = 空表静默回退——素材不入 git)
# 挖掘挥铲:origin = 英雄格左上(board_area 坐标系——BoardView.cell_origin 出口,fx 与棋盘同层同系);
# dir = 挖掘方向单位格向量(素材铲头朝左——dir.x>0 翻转);edge = 格边长(缩放 = edge/32 与角色一致)。
# 握把锚对齐英雄格与目标格共享边中点(出手位),铲头弧线自然落进目标格。
func play_shovel_swing(origin: Vector2, dir: Vector2i, edge: float) -> void:
if _shovel_texs.is_empty():
for p in ["res://assets/art/fx/shovel_swing_f0.png",
"res://assets/art/fx/shovel_swing_f1.png",
"res://assets/art/fx/shovel_swing_f2.png"]:
var t := UiKit.load_tex(p)
if t == null:
return # 素材缺位——静默回退(fx 层占位演出不设断言面)
_shovel_texs.append(t)
var s := edge / SHOVEL_FRAME_PX
var flip := dir.x > 0
var grip := SHOVEL_GRIP
if flip:
grip.x = SHOVEL_FRAME_PX - SHOVEL_GRIP.x # flip_h 围绕纹理中心镜像——锚点 x 随镜
var deliver := origin + Vector2(edge, edge) * 0.5 \
+ Vector2(dir) * (edge * 0.5 + edge * 0.12) # 共享边中点再向目标格偏 12% 格宽
var sp := Sprite2D.new()
sp.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
sp.centered = true
sp.texture = _shovel_texs[0]
sp.flip_h = flip
sp.scale = Vector2.ONE * s
sp.position = deliver + (Vector2(SHOVEL_FRAME_PX, SHOVEL_FRAME_PX) * 0.5 - grip) * s
add_child(sp)
var tw := sp.create_tween()
for i in [1, 2]:
tw.tween_interval(UiKit.motion(SHOVEL_FRAME_S))
tw.tween_callback(func() -> void: sp.texture = _shovel_texs[i])
tw.tween_interval(UiKit.motion(SHOVEL_FRAME_S))
tw.tween_callback(sp.queue_free)
func _show_banner(text: String) -> void:
_banner.text = text
_banner.visible = true
+124 -3
View File
@@ -12,6 +12,9 @@
# 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 定长不增删,
@@ -52,6 +55,13 @@ const SEP := 2 # 格间距(旧 GridContainer v/
const DEPTH_PX := 8.0 # 悬崖厚度(像素)
const DEPTH_BEVEL := 0.6 # 顶面倒角强度
# 英雄单位演出参数(棋盘小人贴图批 2026-09-25——计划书 §3.B/C 时长档)
const HERO_FRAME_PX := 32.0 # 英雄帧画布边长(素材 32×32 锁定——缩放比分母)
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 脚底收敛)
var _wrap: CenterContainer
var _svp_container: SubViewportContainer
var _viewport: SubViewport
@@ -71,6 +81,16 @@ 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
var cell_size: float: # 只读属性(ui_layout_probe 断言面——旧私有名 _cell 的契约化出口)
get:
return _cell
@@ -95,6 +115,8 @@ func _init() -> void:
_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)
func board() -> BoardData:
@@ -155,6 +177,8 @@ func set_cell_area(avail: Vector2) -> void:
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()
@@ -176,7 +200,9 @@ func rebuild(board: BoardData, minesweeper) -> void:
_minesweeper = minesweeper
for child in _world.get_children():
child.queue_free()
_world.hero = null # 英雄精灵随清场退役——下拍 refresh 经 ensure_hero 重建(首建即时落位)
_cells.clear()
_hero_last = Vector2i(-99, -99)
_r0.clear()
_hover = Vector2i(-9, -9)
for c in _board.compute_r0():
@@ -213,6 +239,100 @@ func refresh() -> void:
for x in _board.width:
_paint_cell(Vector2i(x, y), hero)
_refresh_hover_after_paint()
_update_hero(hero) # 英雄精灵 diff 驱动(棋盘小人贴图批 2026-09-25)
# ---- 英雄单位精灵(棋盘小人贴图批 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)
sp.scale = Vector2.ONE * (_cell / HERO_FRAME_PX)
sp.position = Vector2(hero) * (_cell + float(SEP)) + Vector2(_cell * 0.5, float(_cell))
_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 := _cell / HERO_FRAME_PX
var feet := Vector2(to) * (_cell + float(SEP)) + Vector2(_cell * 0.5, float(_cell))
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 := _cell / HERO_FRAME_PX
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))
# 真实输入路径(行为探针复用):合成点击 → 与真实 gui_input 同一翻译出口
@@ -239,9 +359,9 @@ func _cell_at(shell_pos: Vector2) -> Vector2i:
return c
# 格中心(壳内局部坐标)——ui_input_probe 合成事件落点出口(与 _cell_at 同一公式族)
# 格中心(壳内局部坐标)——ui_input_probe 合成事件落点出口(与 _cell_at/cell_origin 同一公式族)
func cell_center_local(c: Vector2i) -> Vector2:
return _svp_container.position + Vector2(c) * (_cell + float(SEP)) + Vector2.ONE * (_cell * 0.5)
return cell_origin(c) + Vector2.ONE * (_cell * 0.5)
# 视口逻辑分辨率 = 板像素尺寸 + 悬崖外延(stretch=true 且容器 min 尺寸同步——缩放比恒 1:1)
@@ -445,7 +565,8 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
if is_hero:
sb.set_border_width_all(3)
sb.border_color = COL_HERO
if glyph == "":
if glyph == "" and _hero_tex_idle == null:
# 贴图缺位回退字形「人」(项目纹理槽惯例——贴图在位=格内字形退役,单位本体由独立精灵承载)
glyph = "人"
digit_col = COL_HERO
u.bg_sb = sb
+19
View File
@@ -11,6 +11,25 @@
extends Node2D
var board_font: Font = null # 字体注入槽位(null = ThemeDB.fallback_font——与旧 Button 缺省主题字体同源)
var hero: Sprite2D = null # 英雄单位精灵(棋盘小人贴图批 2026-09-25——移动实体独立于 CellUnit 格面
# 语义,计划书 §3.A;null = 未创建/随 rebuild 清场,BoardView.refresh 经
# ensure_hero 重建。texture 缺位时 BoardView 不调本方法=字形「人」回退路径)
# 英雄精灵惰性创建(幂等):脚底锚(offset 上移半高——scale.y 弹跳向脚底收敛不悬空)+ z_index=1
# 压格面之上(挥铲特效在 SubViewport 之外恒更上层,满足「格面之上、特效之下」叠序);NEAREST =
# SubViewport 缩放下像素清晰(默认 Linear 会糊)。返回值便于 BoardView 即时落位。
func ensure_hero(tex: Texture2D) -> Sprite2D:
if hero != null:
return hero
hero = Sprite2D.new()
hero.texture = tex
hero.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
hero.centered = true
hero.offset = Vector2(0.0, -16.0) # 纹理中点上移半高——position = 脚底中点
hero.z_index = 1
add_child(hero)
return hero
# ── 立体悬崖(BoardView.set_cell_area / _update_world_size 单点驱动;厚度定稿 8px=中档 2026-09-24)──
var depth_px := 0.0: # 悬崖厚度(像素;0 = 平面)
+1
View File
@@ -230,6 +230,7 @@ func set_portrait_texture(tex: Texture2D) -> void:
_portrait_tex = TextureRect.new()
_portrait_tex.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
_portrait_tex.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
_portrait_tex.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST # 32px 像素帧放大防糊(棋盘小人贴图批 2026-09-25)
_portrait_tex.set_anchors_preset(Control.PRESET_FULL_RECT)
_portrait_tex.offset_top = 44.0
_portrait_tex.offset_bottom = -26.0
+14
View File
@@ -74,6 +74,20 @@ static func motion(dur: float) -> float:
return dur * motion_scale
# ── 单位/特效纹理路径单点(棋盘小人贴图批 2026-09-25)──
# 素材 PNG 不入 git(用户裁定)——装载必须经 load_tex(exists 守卫),缺位返回 null 走各消费点
# 的字形/无演出回退路径;禁 preload(素材缺位树上直接断言炸脚本,冒烟安全红线)
const TEX_HERO_IDLE := "res://assets/art/character/character_hero_idle.png"
const TEX_HERO_WALK := "res://assets/art/character/character_hero_walk.png"
# 素材纹理守卫装载(exists 检查避开缺位树上 ResourceLoader 报错刷屏——冒烟/无素材检出静默回退)
static func load_tex(path: String) -> Texture2D:
if ResourceLoader.exists(path):
return load(path)
return null
# ── StyleBox 工厂 ──
# margin = (左, 上, 右, 下);border_w = 0 即无边框(根底/遮罩用)
static func panel_flat(bg: Color, border: Color, corner: int = 0, border_w: int = 1, margin: Vector4 = Vector4.ZERO) -> StyleBoxFlat: