## 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_SEAL := UiKit.SEAL_BLACK # 封印黑(封印化暴击弹道) 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_seal_hit_projectile(heavy: bool) -> void: var shot := ColorRect.new() var w := 8.0 if heavy else 4.0 shot.color = COL_SEAL 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) # ---- 挖掘挥铲三帧(计划书 §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 与棋盘同层同系)—— # 以手为圆心、铲长为半径旋转(三帧系素材侧预烤的旋转相位,握把锚 (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)