feat: update some visual and fixing the bug

This commit is contained in:
2026-03-27 01:52:45 +08:00
parent 078ae4c966
commit 3a50d2d324
9 changed files with 310 additions and 31 deletions
@@ -14,7 +14,7 @@
[ext_resource type="Texture2D" uid="uid://dpkx1a780pvwv" path="res://assets/textures/tile_diamond.png" id="10_sx8rm"] [ext_resource type="Texture2D" uid="uid://dpkx1a780pvwv" path="res://assets/textures/tile_diamond.png" id="10_sx8rm"]
[ext_resource type="BoxMesh" uid="uid://fy4bhoeii40c" path="res://addons/enhanced_gridmap/meshlibrary/tile_safe_zone.tres" id="10_uwjsj"] [ext_resource type="BoxMesh" uid="uid://fy4bhoeii40c" path="res://addons/enhanced_gridmap/meshlibrary/tile_safe_zone.tres" id="10_uwjsj"]
[ext_resource type="BoxMesh" uid="uid://b5cc3prem52r6" path="res://addons/enhanced_gridmap/meshlibrary/tile_freeze.tres" id="11_pgnbl"] [ext_resource type="BoxMesh" uid="uid://b5cc3prem52r6" path="res://addons/enhanced_gridmap/meshlibrary/tile_freeze.tres" id="11_pgnbl"]
[ext_resource type="BoxMesh" uid="uid://dcjdwbffgtutt" path="res://addons/enhanced_gridmap/meshlibrary/tile_non_walkable.tres" id="11_uwjsj"] [ext_resource type="BoxMesh" path="res://addons/enhanced_gridmap/meshlibrary/tile_non_walkable.tres" id="11_uwjsj"]
[sub_resource type="CompressedTexture2D" id="CompressedTexture2D_5d0gc"] [sub_resource type="CompressedTexture2D" id="CompressedTexture2D_5d0gc"]
load_path = "res://.godot/imported/tile_heart.png-deeef50755ca225f028608dfd16900e6.s3tc.ctex" load_path = "res://.godot/imported/tile_heart.png-deeef50755ca225f028608dfd16900e6.s3tc.ctex"
@@ -2,7 +2,7 @@
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ou2ex"] [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ou2ex"]
transparency = 1 transparency = 1
albedo_color = Color(0.38039216, 1, 0.33333334, 1) albedo_color = Color(0.38039216, 1, 0.33333334, 0.65)
[resource] [resource]
material = SubResource("StandardMaterial3D_ou2ex") material = SubResource("StandardMaterial3D_ou2ex")
+4 -4
View File
@@ -720,6 +720,10 @@ func _auto_start_from_lobby():
func _start_game(): func _start_game():
if multiplayer.is_server(): if multiplayer.is_server():
# NOW assign spawn positions for EVERYONE (Host, Client, Bots)
# We do this BEFORE the stabilization delay so players are moved away from (0,0) immediately.
_assign_random_spawn_positions()
# Wait for Nakama websocket to actually be open, up to 5 seconds # Wait for Nakama websocket to actually be open, up to 5 seconds
# SKIP THIS FOR LAN MODE # SKIP THIS FOR LAN MODE
if not LobbyManager.is_lan_mode: if not LobbyManager.is_lan_mode:
@@ -735,10 +739,6 @@ func _start_game():
# before the countdown starts. # before the countdown starts.
await get_tree().create_timer(1.5).timeout await get_tree().create_timer(1.5).timeout
# NOW assign spawn positions for EVERYONE (Host, Client, Bots)
# This safely sends RPCs over the completed socket connection
_assign_random_spawn_positions()
# PRE-GAME COUNTDOWN (3s) # PRE-GAME COUNTDOWN (3s)
# Spawn static obstacles before countdown starts (Stop n Go only) # Spawn static obstacles before countdown starts (Stop n Go only)
if obstacle_manager and LobbyManager.game_mode == "Stop n Go": if obstacle_manager and LobbyManager.game_mode == "Stop n Go":
+6
View File
@@ -1096,6 +1096,9 @@ func _process(delta):
if LobbyManager.get_randomize_spawn() and spawn_point_selected and not visible: if LobbyManager.get_randomize_spawn() and spawn_point_selected and not visible:
visible = true visible = true
if not multiplayer.has_multiplayer_peer():
return
if is_multiplayer_authority(): if is_multiplayer_authority():
# Visual debugging - show display name with connection status # Visual debugging - show display name with connection status
$Name.text = display_name if not display_name.is_empty() else str(name) $Name.text = display_name if not display_name.is_empty() else str(name)
@@ -1184,6 +1187,9 @@ var last_sent_position: Vector3
func _physics_process(delta): func _physics_process(delta):
# Sync position periodically (Heartbeat / Smoothing) # Sync position periodically (Heartbeat / Smoothing)
if not multiplayer.has_multiplayer_peer():
return
if is_multiplayer_authority(): if is_multiplayer_authority():
# OPTIMIZATION: Only send smoothing updates if we ARE NOT currently mid-tween # OPTIMIZATION: Only send smoothing updates if we ARE NOT currently mid-tween
# The start/end of paths are already synced via start_movement_along_path. # The start/end of paths are already synced via start_movement_along_path.
+2 -2
View File
@@ -61,7 +61,7 @@ func _process(delta):
emit_signal("global_timer_updated", global_match_timer) emit_signal("global_timer_updated", global_match_timer)
# Server broadcasts global timer sync every second # Server broadcasts global timer sync every second
if multiplayer.is_server() and int(global_match_timer) != int(global_match_timer + delta): if multiplayer.has_multiplayer_peer() and multiplayer.is_server() and int(global_match_timer) != int(global_match_timer + delta):
rpc("sync_global_timer", global_match_timer) rpc("sync_global_timer", global_match_timer)
# Update cycle timer if cycle is active # Update cycle timer if cycle is active
@@ -87,7 +87,7 @@ func _process(delta):
emit_signal("timer_updated", current_cycle_timer) emit_signal("timer_updated", current_cycle_timer)
# Server broadcasts timer sync every second # Server broadcasts timer sync every second
if multiplayer.is_server() and int(current_cycle_timer) != int(current_cycle_timer + delta): if multiplayer.has_multiplayer_peer() and multiplayer.is_server() and int(current_cycle_timer) != int(current_cycle_timer + delta):
rpc("sync_timer", current_cycle_timer) rpc("sync_timer", current_cycle_timer)
# ============================================================================= # =============================================================================
+4 -3
View File
@@ -14,7 +14,7 @@ func initialize(p_player: Node3D, p_movement_manager: Node, p_race_manager: Node
func _process(delta): func _process(delta):
# Early return conditions # Early return conditions
if not is_instance_valid(player) or not player.is_multiplayer_authority() or player.is_bot or player.is_in_group("Bots") or player.is_frozen or player.is_stop_frozen: if not is_instance_valid(player) or not multiplayer.has_multiplayer_peer() or not player.is_multiplayer_authority() or player.is_bot or player.is_in_group("Bots") or player.is_frozen or player.is_stop_frozen:
return return
if TurnManager.turn_based_mode: if TurnManager.turn_based_mode:
@@ -59,8 +59,9 @@ func _process(delta):
func handle_unhandled_input(event): func handle_unhandled_input(event):
# Early return if not authorized human player # Early return if not authorized human player
if not player.is_multiplayer_authority() or player.is_bot or player.is_in_group("Bots"): if not multiplayer.has_multiplayer_peer() or not player.is_multiplayer_authority() or player.is_bot or player.is_in_group("Bots"):
player.set_process_unhandled_input(false) if multiplayer.has_multiplayer_peer():
player.set_process_unhandled_input(false)
return return
var main = player.get_node("/root/Main") var main = player.get_node("/root/Main")
@@ -166,6 +166,7 @@ func add_powerup_from_item(item_id: int):
for e in inventory: for e in inventory:
inventory[e] = false inventory[e] = false
powerup_levels[e] = 1 # Reset levels of discarded powerups powerup_levels[e] = 1 # Reset levels of discarded powerups
_exit_targeting_mode()
# Instant Level 8 on pickup (User Request) # Instant Level 8 on pickup (User Request)
inventory[effect] = true inventory[effect] = true
@@ -183,6 +184,8 @@ func sync_inventory_add(effect: int, level: int):
for e in inventory: for e in inventory:
inventory[e] = false inventory[e] = false
powerup_levels[e] = 1 # Reset levels of discarded powerups powerup_levels[e] = 1 # Reset levels of discarded powerups
_exit_targeting_mode()
inventory[effect] = true inventory[effect] = true
powerup_levels[effect] = level powerup_levels[effect] = level
+24 -15
View File
@@ -49,17 +49,8 @@ func calculate_spawn_points(count: int, gridmap: Node) -> Array[Vector2i]:
# Determine Position Type for Bias # Determine Position Type for Bias
# 0:TL, 1:TR, 2:BL, 3:BR, 4:Center # 0:TL, 1:TR, 2:BL, 3:BR, 4:Center
var pos_type = -1 # We pass zone_idx to _pick_spot_in_zone to snap corners
match zone_idx: var pos = _pick_spot_in_zone(zone, gridmap, zone_idx)
0: pos_type = 0 # TL
2: pos_type = 1 # TR
6: pos_type = 2 # BL
8: pos_type = 3 # BR
4: pos_type = 4 # Center
# Use PURE RANDOM spot in zone (User Request #2)
# instead of biased corner/center logic
var pos = _pick_spot_in_zone(zone, gridmap)
if pos != Vector2i(-1, -1): if pos != Vector2i(-1, -1):
spawn_points.append(pos) spawn_points.append(pos)
@@ -126,11 +117,29 @@ func _is_valid_3x3(center: Vector2i, gridmap: Node) -> bool:
return false return false
return true return true
func _pick_spot_in_zone(zone: Rect2i, gridmap: Node) -> Vector2i: func _pick_spot_in_zone(zone: Rect2i, gridmap: Node, zone_idx: int = -1) -> Vector2i:
# Find a valid 3x3 spot in the zone """
# The returned position is the CENTER of the 3x3 area Find a valid 3x3 spot in the zone.
The returned position is the CENTER of the 3x3 area.
If zone_idx is a corner (0, 2, 6, 8), we snap to the absolute map corner.
"""
# CORNER SNAPPING: If this is a corner zone, force it to the extreme corner
# to ensure the 3x3 Stand fills the corner completely (no 1-tile gaps).
if zone_idx == 0: # Top-Left
var center = Vector2i(1, 1)
if _is_valid_3x3(center, gridmap): return center
elif zone_idx == 2: # Top-Right
var center = Vector2i(gridmap.columns - 2, 1)
if _is_valid_3x3(center, gridmap): return center
elif zone_idx == 6: # Bottom-Left
var center = Vector2i(1, gridmap.rows - 2)
if _is_valid_3x3(center, gridmap): return center
elif zone_idx == 8: # Bottom-Right
var center = Vector2i(gridmap.columns - 2, gridmap.rows - 2)
if _is_valid_3x3(center, gridmap): return center
# Fallback/Random logic for non-corner zones or if preferred corner was invalid
var attempts = 0 var attempts = 0
while attempts < 30: while attempts < 30:
attempts += 1 attempts += 1
# Ensure center is at least 1 tile away from edges of the map to fit 3x3 # Ensure center is at least 1 tile away from edges of the map to fit 3x3
+265 -5
View File
@@ -12,6 +12,8 @@ enum Phase {GO, STOP}
# Dynamic Safe Zone # Dynamic Safe Zone
var active_safe_zone_rects: Array[Rect2i] = [] var active_safe_zone_rects: Array[Rect2i] = []
var spawned_safe_zones: int = 0 var spawned_safe_zones: int = 0
var _safe_zone_animating: bool = false
var _outline_nodes: Array[Node3D] = [] # Track perimeter outline containers for cleanup
# Power-Up Tile Spawning # Power-Up Tile Spawning
const POWERUP_TILES = [11, 14] # Speed, Ghost (Freeze and Wall excluded in this mode) const POWERUP_TILES = [11, 14] # Speed, Ghost (Freeze and Wall excluded in this mode)
@@ -97,6 +99,12 @@ func _process(delta):
print("[StopNGo] GO phase ending soon. Spawning 3 safe zones...") print("[StopNGo] GO phase ending soon. Spawning 3 safe zones...")
for i in range(3): for i in range(3):
_spawn_dynamic_safe_zone() _spawn_dynamic_safe_zone()
# Trigger global VFX and outline drawing on all clients
if can_rpc():
rpc("sync_all_safe_zones_vfx")
else:
sync_all_safe_zones_vfx()
if phase_timer <= 0: if phase_timer <= 0:
if current_phase == Phase.GO: if current_phase == Phase.GO:
@@ -607,19 +615,200 @@ func _spawn_dynamic_safe_zone():
gridmap.set_cell_item(Vector3i(px, 2, pz), TILE_SAFE) gridmap.set_cell_item(Vector3i(px, 2, pz), TILE_SAFE)
if can_rpc() and main: if can_rpc() and main:
main.rpc("sync_grid_item", px, 2, pz, TILE_SAFE) main.rpc("sync_grid_item", px, 2, pz, TILE_SAFE)
# Trigger safe zone appear visual effects on all clients
if can_rpc():
rpc("sync_safe_zone_vfx")
@rpc("authority", "call_local", "reliable") @rpc("authority", "call_local", "reliable")
func sync_safe_zone_vfx(): func sync_all_safe_zones_vfx():
var main = get_node_or_null("/root/Main") var main = get_node_or_null("/root/Main")
if main and main.get("vfx_manager"): if main and main.get("vfx_manager"):
if main.vfx_manager.has_method("play_safe_zone_appear"): if main.vfx_manager.has_method("play_safe_zone_appear"):
main.vfx_manager.play_safe_zone_appear() main.vfx_manager.play_safe_zone_appear()
elif main.vfx_manager.get("animation_player"): elif main.vfx_manager.get("animation_player"):
main.vfx_manager.animation_player.play("safe-zone-appear") main.vfx_manager.animation_player.play("safe-zone-appear")
# Build and animate the CONTINUOUS outer outline for ALL zones combined
var outline = _create_merged_safe_zone_outlines()
if outline:
_outline_nodes.append(outline)
_animate_outline_appear(outline)
# Animate the safe zone panels appearing (alpha 0 → 0.65)
_animate_safe_zone_appear()
func _animate_safe_zone_appear():
"""Two-phase appear: sharp bright flash then settle to semi-transparent.
Guarded so only ONE animation runs even when all 3 zones spawn at once."""
if _safe_zone_animating:
return
_safe_zone_animating = true
var gridmap = get_parent().get_node_or_null("EnhancedGridMap")
if not gridmap: gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
if not gridmap or not gridmap.mesh_library:
_safe_zone_animating = false
return
var original_mesh = gridmap.mesh_library.get_item_mesh(TILE_SAFE)
if not is_instance_valid(original_mesh):
_safe_zone_animating = false
return
var mat = original_mesh.material
if not is_instance_valid(mat):
_safe_zone_animating = false
return
# Duplicate mesh+material so we animate without touching the shared .tres on disk.
var anim_mat: StandardMaterial3D = mat.duplicate()
anim_mat.albedo_color = Color(mat.albedo_color.r, mat.albedo_color.g, mat.albedo_color.b, 0.0)
var anim_mesh = original_mesh.duplicate()
anim_mesh.material = anim_mat
gridmap.mesh_library.set_item_mesh(TILE_SAFE, anim_mesh)
const TARGET_ALPHA := 0.65
var tween = create_tween()
# Phase 1 — Sharp bright flash: alpha 0 → 1.0 in 0.2s (EXPO ease-out = instant pop)
tween.tween_method(
func(a: float):
if is_instance_valid(anim_mat): anim_mat.albedo_color.a = a,
0.0, 1.0, 0.2
).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_EXPO)
# Phase 2 — Settle: alpha 1.0 → 0.65 in 0.35s (CUBIC ease-in-out = soft land)
tween.tween_method(
func(a: float):
if is_instance_valid(anim_mat): anim_mat.albedo_color.a = a,
1.0, TARGET_ALPHA, 0.35
).set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_CUBIC)
# Done — anim_mesh stays in library at alpha=0.65 (no restore needed)
tween.tween_callback(func():
_safe_zone_animating = false
)
func _create_merged_safe_zone_outlines() -> Node3D:
"""Create thin edge BoxMesh strips ONLY on the outer perimeter of the merged safe zones.
Inner edges between adjacent safe zones are skipped."""
var gridmap = get_parent().get_node_or_null("EnhancedGridMap")
if not gridmap: gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
if not gridmap: return null
var cs: Vector3 = gridmap.cell_size
# Sit slightly above the safe panel (layer 2, panel top ~= 2*cs.y + 0.05)
var y := 2.0 * cs.y + 0.09
const T := 0.09 # outline thickness (world units)
const H := 0.045 # outline height
# One shared material (transparent, emissive lime-green) — starts invisible for animation
var mat := StandardMaterial3D.new()
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
mat.albedo_color = Color(0.15, 1.0, 0.3, 0.0) # start at alpha=0
mat.emission_enabled = true
mat.emission = Color(0.1, 0.9, 0.25)
mat.emission_energy_multiplier = 1.8
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
var container := Node3D.new()
container.add_to_group("SafeZoneOutlines")
gridmap.add_child(container)
var corner_points = {}
# Scan layer 2 for boundaries
for x in range(gridmap.columns):
for z in range(gridmap.rows):
if gridmap.get_cell_item(Vector3i(x, 2, z)) != TILE_SAFE:
continue
var cell_cx = x * cs.x + cs.x * 0.5
var cell_cz = z * cs.z + cs.z * 0.5
# Check North (z - 1)
if gridmap.get_cell_item(Vector3i(x, 2, z - 1)) != TILE_SAFE:
var mi := MeshInstance3D.new()
var box := BoxMesh.new()
box.size = Vector3(cs.x, H, T)
box.material = mat
mi.mesh = box
mi.position = Vector3(cell_cx, y, z * cs.z)
container.add_child(mi)
corner_points[Vector2i(x, z)] = true
corner_points[Vector2i(x + 1, z)] = true
# Check South (z + 1)
if gridmap.get_cell_item(Vector3i(x, 2, z + 1)) != TILE_SAFE:
var mi := MeshInstance3D.new()
var box := BoxMesh.new()
box.size = Vector3(cs.x, H, T)
box.material = mat
mi.mesh = box
mi.position = Vector3(cell_cx, y, z * cs.z + cs.z)
container.add_child(mi)
corner_points[Vector2i(x, z + 1)] = true
corner_points[Vector2i(x + 1, z + 1)] = true
# Check West (x - 1)
if gridmap.get_cell_item(Vector3i(x - 1, 2, z)) != TILE_SAFE:
var mi := MeshInstance3D.new()
var box := BoxMesh.new()
box.size = Vector3(T, H, cs.z)
box.material = mat
mi.mesh = box
mi.position = Vector3(x * cs.x, y, cell_cz)
container.add_child(mi)
corner_points[Vector2i(x, z)] = true
corner_points[Vector2i(x, z + 1)] = true
# Check East (x + 1)
if gridmap.get_cell_item(Vector3i(x + 1, 2, z)) != TILE_SAFE:
var mi := MeshInstance3D.new()
var box := BoxMesh.new()
box.size = Vector3(T, H, cs.z)
box.material = mat
mi.mesh = box
mi.position = Vector3(x * cs.x + cs.x, y, cell_cz)
container.add_child(mi)
corner_points[Vector2i(x + 1, z)] = true
corner_points[Vector2i(x + 1, z + 1)] = true
# Add a small cylinder at every exposed corner to connect lines and round the tips
for cp in corner_points:
var mi := MeshInstance3D.new()
var cyl := CylinderMesh.new()
cyl.top_radius = T * 0.5
cyl.bottom_radius = T * 0.5
cyl.height = H * 0.98 # Slightly shorter to prevent top-face Z-fighting with lines
cyl.radial_segments = 16
cyl.material = mat
mi.mesh = cyl
mi.position = Vector3(cp.x * cs.x, y, cp.y * cs.z)
container.add_child(mi)
return container
func _animate_outline_appear(container: Node3D):
"""Tween the shared outline material from transparent to full opacity,
mirroring the safe zone panel appear animation."""
if not is_instance_valid(container) or container.get_child_count() == 0: return
var mi := container.get_child(0) as MeshInstance3D
if not mi or not mi.mesh: return
var mat := mi.mesh.material as StandardMaterial3D
if not mat: return
var tween := create_tween()
# Flash in: alpha 0→1 in 0.2s
tween.tween_method(
func(a: float): if is_instance_valid(mat): mat.albedo_color.a = a,
0.0, 1.0, 0.2
).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_EXPO)
# Settle: alpha 1→0.9 in 0.35s
tween.tween_method(
func(a: float): if is_instance_valid(mat): mat.albedo_color.a = a,
1.0, 0.9, 0.35
).set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_CUBIC)
func _is_valid_safe_zone_area(gridmap: Node, start_x: int, start_z: int, width: int, height: int) -> bool: func _is_valid_safe_zone_area(gridmap: Node, start_x: int, start_z: int, width: int, height: int) -> bool:
# Avoid bounds or start/finish cols # Avoid bounds or start/finish cols
@@ -642,6 +831,64 @@ func _is_valid_safe_zone_area(gridmap: Node, start_x: int, start_z: int, width:
return true return true
@rpc("authority", "call_local", "reliable")
func sync_safe_zone_disappear_vfx():
_animate_safe_zone_disappear()
func _animate_safe_zone_disappear():
"""Two-phase disappear: quick flicker-brighten then fade to invisible.
Plays before cells are cleared so the panel smoothly vanishes."""
var gridmap = get_parent().get_node_or_null("EnhancedGridMap")
if not gridmap: gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
if not gridmap or not gridmap.mesh_library: return
var cur_mesh = gridmap.mesh_library.get_item_mesh(TILE_SAFE)
if not is_instance_valid(cur_mesh): return
var cur_mat = cur_mesh.material
if not is_instance_valid(cur_mat): return
var fade_mat: StandardMaterial3D = cur_mat.duplicate()
var fade_mesh = cur_mesh.duplicate()
fade_mesh.material = fade_mat
gridmap.mesh_library.set_item_mesh(TILE_SAFE, fade_mesh)
var start_alpha: float = cur_mat.albedo_color.a
var tween = create_tween()
# Phase 1 — Flicker brighten: alpha → 0.95 in 0.15s (warn the player)
tween.tween_method(
func(a: float):
if is_instance_valid(fade_mat): fade_mat.albedo_color.a = a,
start_alpha, 0.95, 0.15
).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_SINE)
# Phase 2 — Fade out: alpha 0.95 → 0 in 0.4s (EXPO ease-in = fast vanish)
tween.tween_method(
func(a: float):
if is_instance_valid(fade_mat): fade_mat.albedo_color.a = a,
0.95, 0.0, 0.4
).set_ease(Tween.EASE_IN).set_trans(Tween.TRANS_EXPO)
# Fade out all outline nodes in sync
for outline in _outline_nodes:
if not is_instance_valid(outline) or outline.get_child_count() == 0: continue
var mi := outline.get_child(0) as MeshInstance3D
if not mi or not mi.mesh: continue
var omat := mi.mesh.material as StandardMaterial3D
if not omat: continue
var fade_start := omat.albedo_color.a
var otween := create_tween()
# Flicker brighten
otween.tween_method(
func(a: float): if is_instance_valid(omat): omat.albedo_color.a = a,
fade_start, 1.0, 0.15
).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_SINE)
# Fade to zero
otween.tween_method(
func(a: float): if is_instance_valid(omat): omat.albedo_color.a = a,
1.0, 0.0, 0.4
).set_ease(Tween.EASE_IN).set_trans(Tween.TRANS_EXPO)
func _clear_dynamic_safe_zones(): func _clear_dynamic_safe_zones():
var gridmap = get_parent().get_node_or_null("EnhancedGridMap") var gridmap = get_parent().get_node_or_null("EnhancedGridMap")
if not gridmap: gridmap = get_node_or_null("/root/Main/EnhancedGridMap") if not gridmap: gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
@@ -650,6 +897,13 @@ func _clear_dynamic_safe_zones():
print("[StopNGo] Clearing %d active safe zones." % active_safe_zone_rects.size()) print("[StopNGo] Clearing %d active safe zones." % active_safe_zone_rects.size())
# Play disappear animation on all peers, then wait for it to finish (0.55s) before clearing.
if can_rpc():
rpc("sync_safe_zone_disappear_vfx")
else:
sync_safe_zone_disappear_vfx()
await get_tree().create_timer(0.55).timeout
for rect in active_safe_zone_rects: for rect in active_safe_zone_rects:
for rx in range(rect.size.x): for rx in range(rect.size.x):
for rz in range(rect.size.y): for rz in range(rect.size.y):
@@ -664,6 +918,12 @@ func _clear_dynamic_safe_zones():
active_safe_zone_rects.clear() active_safe_zone_rects.clear()
spawned_safe_zones = 0 spawned_safe_zones = 0
# Free all outline containers now that the animation has finished
for outline in _outline_nodes:
if is_instance_valid(outline):
outline.queue_free()
_outline_nodes.clear()
func _scatter_player_tiles(player_node: Node): func _scatter_player_tiles(player_node: Node):
"""Server: Take all tiles from player's playerboard and scatter them onto nearby grid cells.""" """Server: Take all tiles from player's playerboard and scatter them onto nearby grid cells."""