feat: update vfx

This commit is contained in:
2026-06-18 14:04:06 +08:00
parent 5354d8b30f
commit 78f071b728
18 changed files with 704 additions and 62 deletions
+138
View File
@@ -1919,6 +1919,144 @@ func sync_grid_item(x: int, y: int, z: int, item: int):
if enhanced_gridmap.has_method("update_grid_data"):
enhanced_gridmap.update_grid_data()
@rpc("any_peer", "call_local", "reliable")
func play_freeze_floor_vfx(center_x: int, center_z: int, radius: int, duration: float) -> void:
# Spawns a flat freeze-floor AnimatedSprite3D that covers the (2*radius+1)^2
# frozen area, centered on the given grid cell and sitting just above the
# floor surface. The template under vfxController/AnimatedSprite3D (hidden) is
# laid flat and sized for ~1 tile; we duplicate it, then size it to the area
# from its measured footprint. Runs on every peer.
var vfx_root := get_node_or_null("vfxController")
if not vfx_root:
return
var template := vfx_root.get_node_or_null("AnimatedSprite3D") as AnimatedSprite3D
if not template:
return
var enhanced_gridmap = $EnhancedGridMap
if not enhanced_gridmap:
return
var cell: Vector3 = enhanced_gridmap.cell_size
var vfx := template.duplicate() as AnimatedSprite3D
# --- Scale to cover the whole area -------------------------------------
# The template's world footprint = its local sprite size (texture * pixel_size,
# from the AABB) times each basis axis length. Scale each flat axis (X = width,
# Y = depth, since the sprite is rotated to lie flat) so it spans the area.
# AREA_SIZE_FACTOR oversizes slightly so the visible art (which has transparent
# padding inside the frame) actually fills the (2*radius+1)-tile region — tune
# this if it reads a touch small or large.
const AREA_SIZE_FACTOR := 1.25
var aabb := template.get_aabb()
var b := template.transform.basis
var base_w: float = absf(aabb.size.x) * b.x.length()
var base_d: float = absf(aabb.size.y) * b.y.length()
var want_w: float = float(2 * radius + 1) * cell.x * AREA_SIZE_FACTOR
var want_d: float = float(2 * radius + 1) * cell.z * AREA_SIZE_FACTOR
if base_w > 0.0001:
b.x = b.x * (want_w / base_w)
if base_d > 0.0001:
b.y = b.y * (want_d / base_d)
# --- World center of the freeze area -----------------------------------
# X/Z use the same convention players use (grid_to_world): cell center =
# index*size + half a cell. Y is taken from the template's own position in the
# scene, so the height can be tuned in the editor (move vfxController/
# AnimatedSprite3D up/down) without touching code. vfxController/Main sit at
# the origin, so this local point is also the world point.
var world_center := Vector3(
center_x * cell.x + cell.x * 0.5,
template.position.y,
center_z * cell.z + cell.z * 0.5
)
# Assign basis + origin together in one shot so nothing overwrites the position.
vfx.transform = Transform3D(b, world_center)
vfx.visible = true
vfx_root.add_child(vfx)
vfx.play()
print("[FreezeVFX] center cell=(%d,%d) -> world=%s, scale=(%.2f x %.2f)" % [center_x, center_z, world_center, want_w, want_d])
await get_tree().create_timer(duration).timeout
if is_instance_valid(vfx):
vfx.queue_free()
@rpc("any_peer", "call_local", "reliable")
func play_block_floor_vfx(cells: PackedVector2Array, duration: float) -> void:
# Spawns one box_block VFX per blocked cell (each covers 1x1), autoplaying its
# wall animation. The template under vfxController/box_block (hidden) is sized
# for one cell; we duplicate it per cell, keeping its Y/scale from the scene so
# height can be tuned in the editor. Runs on every peer.
var vfx_root := get_node_or_null("vfxController")
if not vfx_root:
return
var template := vfx_root.get_node_or_null("box_block")
if not template:
return
var enhanced_gridmap = $EnhancedGridMap
if not enhanced_gridmap:
return
var cell: Vector3 = enhanced_gridmap.cell_size
var spawned: Array = []
for c in cells:
# DUPLICATE_USE_INSTANTIATION so the instanced glb subtree (incl. its
# AnimationPlayer) is recreated in the copy rather than dropped.
var box := template.duplicate(DUPLICATE_USE_INSTANTIATION)
# X/Z = cell center (same convention as players), Y/scale kept from template.
var pos := Vector3(
int(c.x) * cell.x + cell.x * 0.5,
template.position.y,
int(c.y) * cell.z + cell.z * 0.5
)
box.transform = Transform3D(template.transform.basis, pos)
box.visible = true
vfx_root.add_child(box)
_autoplay_vfx_animation(box)
spawned.append(box)
await get_tree().create_timer(duration).timeout
for box in spawned:
if is_instance_valid(box):
box.queue_free()
func _autoplay_vfx_animation(node: Node) -> void:
# Find the AnimationPlayer Godot creates for an imported glb/scene and play its
# first clip once (one-shot) when the VFX is spawned.
var anim_player := node.get_node_or_null("AnimationPlayer") as AnimationPlayer
if not anim_player:
for child in node.get_children():
if child is AnimationPlayer:
anim_player = child
break
if not anim_player:
return
var list := anim_player.get_animation_list()
if list.is_empty():
return
var anim_name: String = list[0]
var anim := anim_player.get_animation(anim_name)
if anim:
anim.loop_mode = Animation.LOOP_NONE
anim_player.play(anim_name)
func play_playerboard_scatter_vfx() -> void:
# One-shot "scatter" animation over the local player's board UI. Triggered for
# the player whose tiles were scattered (caught outside the safe zone in
# Stop n Go). The clip is already non-looping in the SpriteFrames.
var vfx := get_node_or_null("PlayerBoardUI/AnimatedSprite2D") as AnimatedSprite2D
if not vfx:
return
vfx.visible = true
vfx.play("scatter")
await vfx.animation_finished
vfx.visible = false
@rpc("any_peer", "call_local", "reliable")
func sync_grid_items_batch(data: Array):
# data is an array of dictionaries: [{x: int, y: int, z: int, item: int}, ...]