按 code-conventions §1.4.1 第三轮定稿执行:core 22+schemas 18+presentation 根 14+views 28,全部头注 #→##、删日期/变更叙事/GDD 与架构反向链接/字段与域清单/机制描述,保留职责摘要+真实使用约束(防事故禁令/单一入口/信号契约每信号一行)。main.gd 31→5 行、save_manager 45→5 行、adventure_runner 37→5 行为最大单件。子代理分模块执行(波一 core+schemas、波二 presentation+views,并发≤2)。验收=tools/verify_comment_only_change.py 逐字节校验(82 文件头注区外逐行一致=零行为变更)+audit_file_headers 全绿;纯注释批免冒烟(先例 5843270)
172 lines
6.8 KiB
GDScript
172 lines
6.8 KiB
GDScript
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||
## D 区演出层:清图喷泉/Boss 影子/完美排雷光带/标中旗弹道/挥铲三帧等一次性占位演出。
|
||
## 组合根按核心信号直调 play_*;素材缺位静默回退(演出层不设断言面)。
|
||
class_name BoardFxView
|
||
extends Control
|
||
|
||
const COL_GOLD := UiKit.GOLD
|
||
const COL_BOSS := UiKit.BOSS_PURPLE # 紫赤 #8F2F6A
|
||
const COL_FLAG := UiKit.SEAL_BLACK # 封印黑(旗化暴击弹道——旗朱→封印紫→封印黑 2026-09-28)
|
||
const CONFETTI_COLS := [UiKit.GOLD, UiKit.GOLD_LIT, UiKit.HERO_BLUE, UiKit.WARM] # 装饰彩带不涉封印语义——原紫位改亮金(紫赤系 Boss 独占不入装饰)
|
||
|
||
var _banner: Label
|
||
|
||
|
||
func _init() -> void:
|
||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
_banner = UiKit.label("",UiKit.FS_TITLE, true, COL_GOLD)
|
||
_banner.set_anchors_preset(Control.PRESET_CENTER_TOP)
|
||
_banner.grow_horizontal = Control.GROW_DIRECTION_BOTH
|
||
_banner.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
_banner.visible = false
|
||
_banner.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
add_child(_banner)
|
||
|
||
|
||
# 清图瞬间:金粒喷泉 + 彩纸 3–5s 即收(§2.3 占位)
|
||
func play_clear_burst() -> void:
|
||
var fountain := CPUParticles2D.new()
|
||
fountain.position = size * 0.5
|
||
fountain.amount = 90
|
||
fountain.lifetime = UiKit.motion(1.6)
|
||
fountain.one_shot = true
|
||
fountain.explosiveness = 0.2
|
||
fountain.direction = Vector2(0, -1)
|
||
fountain.spread = 55.0
|
||
fountain.initial_velocity_min = 320.0
|
||
fountain.initial_velocity_max = 560.0
|
||
fountain.gravity = Vector2(0, 700)
|
||
fountain.scale_amount_min = 2.0
|
||
fountain.scale_amount_max = 4.5
|
||
fountain.color = COL_GOLD
|
||
fountain.emitting = true
|
||
add_child(fountain)
|
||
var confetti := CPUParticles2D.new()
|
||
confetti.position = Vector2(size.x * 0.5, -8.0)
|
||
confetti.amount = 60
|
||
confetti.lifetime = UiKit.motion(3.2)
|
||
confetti.one_shot = true
|
||
confetti.explosiveness = 0.9
|
||
confetti.direction = Vector2(0, 1)
|
||
confetti.spread = 75.0
|
||
confetti.initial_velocity_min = 90.0
|
||
confetti.initial_velocity_max = 200.0
|
||
confetti.gravity = Vector2(0, 140)
|
||
confetti.scale_amount_min = 3.0
|
||
confetti.scale_amount_max = 5.0
|
||
confetti.color_ramp = _confetti_ramp()
|
||
confetti.emitting = true
|
||
add_child(confetti)
|
||
var tw := create_tween()
|
||
tw.tween_interval(UiKit.motion(3.5))
|
||
tw.tween_callback(func():
|
||
for n in [fountain, confetti]:
|
||
if is_instance_valid(n):
|
||
n.queue_free())
|
||
|
||
|
||
func _confetti_ramp() -> Gradient:
|
||
var g := Gradient.new()
|
||
g.set_color(0, CONFETTI_COLS[0])
|
||
g.set_color(1, CONFETTI_COLS[1])
|
||
return g
|
||
|
||
|
||
# Boss 遭遇:紫赤影子爬过棋盘一次性占位演出(§2.6)
|
||
func play_boss_shadow() -> void:
|
||
var shadow := ColorRect.new()
|
||
shadow.color = Color(COL_BOSS, 0.55)
|
||
shadow.size = Vector2(size.x, 56.0)
|
||
shadow.position = Vector2(0, -60.0)
|
||
shadow.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
add_child(shadow)
|
||
var tw := shadow.create_tween()
|
||
tw.tween_property(shadow, "position:y", size.y, UiKit.motion(1.1)).set_trans(Tween.TRANS_SINE)
|
||
tw.tween_property(shadow, "modulate:a", 0.0, UiKit.motion(0.5))
|
||
tw.tween_callback(shadow.queue_free)
|
||
|
||
|
||
# 完美排雷:金光带加倍扫射 + 横幅占位(§2.2 最高档)
|
||
func play_perfect_sweep() -> void:
|
||
for i in 2:
|
||
var band := ColorRect.new()
|
||
band.color = Color(COL_GOLD, 0.35)
|
||
band.size = Vector2(size.x, 14.0 + 10.0 * i)
|
||
band.position = Vector2(-size.x, size.y * 0.3 + 40.0 * i)
|
||
band.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
add_child(band)
|
||
var tw := band.create_tween()
|
||
tw.tween_property(band, "position:x", size.x + 8.0, UiKit.motion(0.5 + 0.15 * i))
|
||
tw.tween_callback(band.queue_free)
|
||
_show_banner("完美排雷 ×2")
|
||
|
||
|
||
# 标中旗:旗化暴击弹道加重占位(自棋盘上缘飞向 B 区敌人卡方向——越界即收)
|
||
func play_flag_hit_projectile(heavy: bool) -> void:
|
||
var shot := ColorRect.new()
|
||
var w := 8.0 if heavy else 4.0
|
||
shot.color = COL_FLAG
|
||
shot.size = Vector2(w, w * 2.2)
|
||
shot.position = Vector2(size.x * 0.5, size.y * 0.7)
|
||
shot.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
add_child(shot)
|
||
var tw := shot.create_tween()
|
||
tw.tween_property(shot, "position", Vector2(size.x * 0.75, -30.0), UiKit.motion(0.35))
|
||
tw.parallel().tween_property(shot, "modulate:a", 0.0, UiKit.motion(0.35))
|
||
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[Texture2D] = [] # 惰性装载缓存(素材缺位 = 空表静默回退——素材不入 git)
|
||
|
||
|
||
# 挖掘挥铲:hand = 英雄持握手位(BoardView.hero_hand_point 出口,fx 与棋盘同层同系)——
|
||
# 以手为圆心、铲长为半径旋转(2026-09-25 用户裁决;三帧系素材侧预烤的旋转相位,握把锚 (18,12)
|
||
# 三帧同位,锚点钉死手位即成弧);dir = 挖掘方向单位格向量(素材铲头朝左——dir.x>0 翻转);
|
||
# sprite_scale 与英雄精灵同档(整数 1×——原「共享边中点+12% 偏置」定位与 edge/32 缩放作废)。
|
||
func play_shovel_swing(hand: Vector2, dir: Vector2i, sprite_scale: 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 flip := dir.x > 0
|
||
var grip := SHOVEL_GRIP
|
||
if flip:
|
||
grip.x = SHOVEL_FRAME_PX - SHOVEL_GRIP.x # flip_h 围绕纹理中心镜像——锚点 x 随镜
|
||
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 * sprite_scale
|
||
sp.position = hand + (Vector2(SHOVEL_FRAME_PX, SHOVEL_FRAME_PX) * 0.5 - grip) * sprite_scale
|
||
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
|
||
_banner.modulate.a = 0.0
|
||
var tw := create_tween()
|
||
tw.tween_property(_banner, "modulate:a", 1.0, UiKit.motion(0.2))
|
||
tw.tween_interval(UiKit.motion(1.6))
|
||
tw.tween_property(_banner, "modulate:a", 0.0, UiKit.motion(0.4))
|
||
tw.tween_callback(func(): _banner.visible = false)
|