Files
gplan-2/game/presentation/views/board_fx_view.gd
T

175 lines
7.0 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# VERTICAL SLICE - NOT FOR PRODUCTION
# 实现 GDD:design/ux/battle.md(D 区演出层——清图金粒喷泉+彩纸 3–5s 占位、Boss 紫赤影子爬棋盘
# 一次性占位、完美排雷金光带+横幅占位、标中旗加重弹道占位;全部程序化色板原语,真实美术归 战斗表现)。
# Date: 2026-09-12
# 高内聚边界:纯演出——组合根按核心信号调用 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("", 26, true)
_banner.add_theme_color_override("font_color", 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 = [] # 惰性装载缓存(素材缺位 = 空表静默回退——素材不入 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)