1097 lines
39 KiB
GDScript
1097 lines
39 KiB
GDScript
extends Node
|
|
class_name StopNGoManager
|
|
|
|
# StopNGoManager - Handles phase transitions, missions, and movement penalties
|
|
|
|
signal phase_changed(new_phase: String, remaining_time: float)
|
|
signal mission_status_updated(player_id: int, completed: bool)
|
|
signal player_penalized(player_id: int)
|
|
|
|
enum Phase {GO, STOP}
|
|
|
|
# Dynamic Safe Zone
|
|
var active_safe_zone_rects: Array[Rect2i] = []
|
|
var spawned_safe_zones: int = 0
|
|
var _safe_zone_animating: bool = false
|
|
var _outline_nodes: Array[Node3D] = [] # Track perimeter outline containers for cleanup
|
|
var _cached_safe_zone_mesh: Mesh = null
|
|
|
|
# Power-Up Tile Spawning
|
|
const POWERUP_TILES = [11, 14] # Speed, Ghost (Freeze and Wall excluded in this mode)
|
|
const POWERUP_SPAWN_COUNT: int = 5 # Number of power-up tiles to spawn
|
|
var powerups_spawned: bool = false
|
|
var stop_phase_occurred: bool = false
|
|
|
|
# Safe zone walls removed for fully open dynamic zones
|
|
const PERMANENT_POWERUP_LOCATIONS: Array[Vector2i] = [
|
|
Vector2i(4, 1), # Power up 1
|
|
Vector2i(3, 9), # Power up 2
|
|
Vector2i(9, 6), # Power up 3
|
|
Vector2i(16, 2), # Power up 4
|
|
Vector2i(18, 8) # Power up 5
|
|
]
|
|
|
|
var current_phase: Phase = Phase.GO
|
|
var phase_timer: float = 20.0 # Initialized dynamically later
|
|
var is_active: bool = false
|
|
|
|
var player_missions: Dictionary = {} # player_id -> {target_tile: int, required: int, current: int}
|
|
var finish_line_x: int = 22 # Right side of the map for win condition
|
|
|
|
# Tile IDs
|
|
const TILE_WALKABLE = 0
|
|
const TILE_START = 3 # Start Line
|
|
const TILE_FINISH = 3 # Finish Line
|
|
const TILE_SAFE = 2 # Green Safe Zone
|
|
const TILE_OBSTACLE = 4 # Wall
|
|
const TILE_LIGHTNING_STONE = 15 # Ancient Rock with Lightning Symbol
|
|
const TILE_SAFE_WALL = 16 # Safe Zone Wall
|
|
|
|
var hud_layer: CanvasLayer
|
|
var mission_label: Label
|
|
var red_tint_overlay: ColorRect
|
|
|
|
# Traffic Light / StopTimer Visuals
|
|
var stop_timer_node: PanelContainer
|
|
var stop_segments: Array[Panel] = []
|
|
var empty_styles: Array[StyleBoxTexture] = []
|
|
var filled_styles: Array[StyleBoxTexture] = []
|
|
|
|
func _ready():
|
|
set_process(false)
|
|
_setup_hud()
|
|
|
|
func _setup_hud():
|
|
hud_layer = CanvasLayer.new()
|
|
hud_layer.layer = 5 # Ensure it's above normal UI but below Pause Menu (10)
|
|
hud_layer.visible = false
|
|
add_child(hud_layer)
|
|
|
|
# New container for bottom-mid label
|
|
var bottom_container = CenterContainer.new()
|
|
bottom_container.set_anchors_preset(Control.PRESET_CENTER_BOTTOM)
|
|
bottom_container.grow_horizontal = Control.GROW_DIRECTION_BOTH
|
|
bottom_container.grow_vertical = Control.GROW_DIRECTION_BEGIN
|
|
bottom_container.offset_bottom = -50
|
|
hud_layer.add_child(bottom_container)
|
|
|
|
var custom_font = load("res://assets/fonts/Nougat-ExtraBlack.ttf")
|
|
|
|
mission_label = Label.new()
|
|
mission_label.text = "MISSION: Collect Goals"
|
|
mission_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
if custom_font: mission_label.add_theme_font_override("font", custom_font)
|
|
mission_label.add_theme_font_size_override("font_size", 28) # Slightly larger since it's centered
|
|
mission_label.add_theme_color_override("font_outline_color", Color.BLACK)
|
|
mission_label.add_theme_constant_override("outline_size", 8)
|
|
bottom_container.add_child(mission_label)
|
|
|
|
func _process(delta):
|
|
if not is_active:
|
|
return
|
|
|
|
# Decrement timer locally for all peers (smoother HUD than waiting for RPC)
|
|
phase_timer -= delta
|
|
|
|
if multiplayer.is_server():
|
|
if current_phase == Phase.GO:
|
|
# Spawn all 3 safe zones at once, 6 seconds before STOP
|
|
if phase_timer <= 6.0 and spawned_safe_zones == 0:
|
|
print("[StopNGo] GO phase ending soon. Spawning 3 safe zones...")
|
|
for i in range(3):
|
|
_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 current_phase == Phase.GO:
|
|
print("[StopNGo] GO Timer reached 0. Starting STOP Phase.")
|
|
_start_phase(Phase.STOP)
|
|
else:
|
|
print("[StopNGo] STOP Timer reached 0. Starting GO Phase.")
|
|
_start_phase(Phase.GO)
|
|
|
|
# Update HUD locally
|
|
_update_hud_visuals()
|
|
|
|
func _on_goal_count_updated(_peer_id: int, _count: int):
|
|
# Refresh visuals whenever points change
|
|
_update_hud_visuals()
|
|
|
|
var _has_notified_mission_complete: bool = false
|
|
|
|
func _update_hud_visuals():
|
|
var my_id = multiplayer.get_unique_id()
|
|
if mission_label:
|
|
var main = get_node_or_null("/root/Main")
|
|
var goals_cycle_manager = main.get_node_or_null("GoalsCycleManager") if main else null
|
|
|
|
# Get count from GoalsCycleManager (Source of truth for PlayerBoardLabel)
|
|
var completed_count = goals_cycle_manager.player_goal_counts.get(my_id, 0) if goals_cycle_manager else 0
|
|
var required_goals = LobbyManager.sng_required_goals
|
|
|
|
mission_label.text = "GOALS (%d/%d)" % [completed_count, required_goals]
|
|
|
|
if completed_count >= required_goals:
|
|
mission_label.text = "ALL GOALS COMPLETE!\nREACH THE FINISH!"
|
|
mission_label.add_theme_color_override("font_color", Color.GOLD)
|
|
|
|
# Notify player once
|
|
if my_id == multiplayer.get_unique_id() and not _has_notified_mission_complete:
|
|
_has_notified_mission_complete = true
|
|
var player_node = main.get_node_or_null(str(my_id))
|
|
if player_node:
|
|
NotificationManager.send_message(player_node, "ALL GOALS COMPLETE!", NotificationManager.MessageType.GOAL)
|
|
|
|
# Play looping "GoFinish" animation to signals the Go phase for this player
|
|
if main and main.get("vfx_manager"):
|
|
if main.vfx_manager.has_method("play_go_finish_animation"):
|
|
main.vfx_manager.play_go_finish_animation()
|
|
elif main.vfx_manager.has_method("play_go_animation"):
|
|
main.vfx_manager.play_go_animation()
|
|
else:
|
|
mission_label.add_theme_color_override("font_color", Color.WHITE)
|
|
_has_notified_mission_complete = false
|
|
|
|
# Update StopTimer (Traffic Light)
|
|
_update_stop_timer_visuals()
|
|
|
|
func _update_stop_timer_visuals():
|
|
if not stop_timer_node:
|
|
# Try to find it once
|
|
var main = get_node_or_null("/root/Main")
|
|
if main:
|
|
stop_timer_node = main.get_node_or_null("StopTimer")
|
|
if stop_timer_node:
|
|
var hbox = stop_timer_node.get_node_or_null("HBox")
|
|
if hbox:
|
|
stop_segments.clear()
|
|
empty_styles.clear()
|
|
filled_styles.clear()
|
|
for i in range(3):
|
|
var seg = hbox.get_node_or_null("Segment%d" % i)
|
|
if seg: stop_segments.append(seg)
|
|
|
|
var e_style = StyleBoxTexture.new()
|
|
e_style.texture = load("res://assets/graphics/gui/stop_timer/Segment%d_empty.png" % i)
|
|
empty_styles.append(e_style)
|
|
|
|
var f_style = StyleBoxTexture.new()
|
|
f_style.texture = load("res://assets/graphics/gui/stop_timer/Segment%d_filled.png" % i)
|
|
filled_styles.append(f_style)
|
|
|
|
if not stop_timer_node: return
|
|
|
|
# ALWAYS VISIBLE in Stop n Go mode
|
|
stop_timer_node.visible = true
|
|
|
|
if current_phase == Phase.GO:
|
|
# GO Phase: All empty unless in last 3 seconds (lights up 3, 2, 1s)
|
|
for i in range(stop_segments.size()):
|
|
var threshold = 3.0 - (i * 1.0)
|
|
if phase_timer <= threshold:
|
|
stop_segments[i].add_theme_stylebox_override("panel", filled_styles[i])
|
|
else:
|
|
stop_segments[i].add_theme_stylebox_override("panel", empty_styles[i])
|
|
else:
|
|
# STOP Phase: All Red
|
|
for i in range(stop_segments.size()):
|
|
stop_segments[i].add_theme_stylebox_override("panel", filled_styles[i])
|
|
|
|
func activate_client_side():
|
|
is_active = true
|
|
if hud_layer:
|
|
hud_layer.visible = true
|
|
|
|
# Connect to GoalsCycleManager for immediate HUD updates
|
|
var main = get_node_or_null("/root/Main")
|
|
if main:
|
|
var gcm = main.get_node_or_null("GoalsCycleManager")
|
|
if gcm and not gcm.goal_count_updated.is_connected(_on_goal_count_updated):
|
|
gcm.goal_count_updated.connect(_on_goal_count_updated)
|
|
|
|
set_process(true)
|
|
|
|
func start_game_mode():
|
|
# This should primarily be called by the Server
|
|
# Clients get activated via RPCs (sync_phase, etc)
|
|
if multiplayer.is_server():
|
|
activate_client_side() # Server also needs local processing
|
|
# _setup_arena() # REMOVED: Already explicitly called in main.gd _setup_host_game to prepare floor before spawns!
|
|
_assign_missions()
|
|
_start_phase(Phase.GO)
|
|
else:
|
|
# Clients just wait for updates, but can enable HUD if needed
|
|
# activate_client_side() can be called when first sync arrives
|
|
pass
|
|
|
|
func rotate_players_to_start():
|
|
"""Force all players to face East (towards finish line). Called before countdown."""
|
|
if not multiplayer.is_server(): return
|
|
|
|
var all_players = get_tree().get_nodes_in_group("Players")
|
|
for p in all_players:
|
|
if p.has_method("rpc"):
|
|
p.rpc("sync_rotation", PI/2)
|
|
|
|
func _start_phase(phase: Phase):
|
|
current_phase = phase
|
|
phase_timer = float(LobbyManager.sng_go_duration) if phase == Phase.GO else float(LobbyManager.sng_stop_duration)
|
|
|
|
var phase_name = "GO" if phase == Phase.GO else "STOP"
|
|
if can_rpc():
|
|
rpc("sync_phase", phase_name, phase_timer)
|
|
|
|
if phase == Phase.STOP:
|
|
stop_phase_occurred = true
|
|
# --- STATIC SAFE ZONE: Penalize players outside the zone ---
|
|
var all_players = get_tree().get_nodes_in_group("Players")
|
|
for p in all_players:
|
|
# POSITION SYNC FIX: If player is currently moving on their client,
|
|
# the server's 'current_position' might still be the old one.
|
|
# Use 'target_position' if they are moving.
|
|
var check_pos = p.current_position
|
|
if p.get("is_player_moving") and p.get("target_position") != Vector2i(-1, -1):
|
|
check_pos = p.target_position
|
|
|
|
if not _is_in_safe_zone(check_pos):
|
|
_scatter_player_tiles(p)
|
|
|
|
# Refresh power-ups every STOP phase
|
|
_spawn_powerup_tiles()
|
|
|
|
# If GO phase starts, clear all STOP phase freezes and dynamic safe zones
|
|
if phase == Phase.GO:
|
|
_clear_dynamic_safe_zones()
|
|
var all_players = get_tree().get_nodes_in_group("Players")
|
|
for p in all_players:
|
|
if p.has_method("sync_stop_freeze"):
|
|
p.rpc("sync_stop_freeze", false)
|
|
|
|
emit_signal("phase_changed", phase_name, phase_timer)
|
|
|
|
func can_rpc() -> bool:
|
|
if not multiplayer.has_multiplayer_peer() or multiplayer.multiplayer_peer.get_connection_status() != MultiplayerPeer.CONNECTION_CONNECTED:
|
|
return false
|
|
return true
|
|
|
|
@rpc("authority", "call_local", "reliable")
|
|
func sync_phase(phase_name: String, duration: float):
|
|
if not is_active:
|
|
activate_client_side()
|
|
current_phase = Phase.GO if phase_name == "GO" else Phase.STOP
|
|
phase_timer = duration
|
|
|
|
# Trigger Phase Animations
|
|
var main = get_node_or_null("/root/Main")
|
|
if main and main.get("vfx_manager"):
|
|
if phase_name == "STOP":
|
|
if main.vfx_manager.has_method("play_stop_phase"):
|
|
main.vfx_manager.play_stop_phase()
|
|
elif main.vfx_manager.get("animation_player"):
|
|
main.vfx_manager.animation_player.play("stop-phase")
|
|
|
|
func _setup_arena():
|
|
var gridmap = get_parent().get_node_or_null("EnhancedGridMap")
|
|
if not gridmap:
|
|
gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
|
|
if not gridmap: return
|
|
|
|
print("[StopNGo] Setting up 22x10 Arena with Randomized Obstacles...")
|
|
|
|
# Explicitly sync dimensions and clear grid on all clients
|
|
if can_rpc():
|
|
rpc("sync_arena_setup")
|
|
|
|
# Apply locally for Server (RPC is call_remote)
|
|
_apply_arena_setup()
|
|
|
|
@rpc("authority", "call_remote", "reliable")
|
|
func sync_arena_setup():
|
|
print("[StopNGo] Client: Syncing Arena Setup (23x12)...")
|
|
_apply_arena_setup()
|
|
|
|
func _apply_arena_setup():
|
|
# Shared logic for resizing and clearing
|
|
var gridmap = get_parent().get_node_or_null("EnhancedGridMap")
|
|
if not gridmap:
|
|
# Fallback just in case
|
|
gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
|
|
if not gridmap: return
|
|
|
|
# Set Size for Stop n Go explicitly, bypassing setters that wipe the map
|
|
gridmap.set("columns", 23)
|
|
gridmap.set("rows", 12)
|
|
|
|
# Clear existing items on all layers
|
|
gridmap.clear()
|
|
|
|
# Dynamic Safe Zone: No static safe columns anymore
|
|
# Safe zone spawns randomly during gameplay
|
|
|
|
var non_walkable_coords = [
|
|
Vector2i(0,0), Vector2i(1,0), Vector2i(2,0), Vector2i(3,0), Vector2i(4,0), Vector2i(5,0), Vector2i(6,0), Vector2i(7,0), Vector2i(8,0), Vector2i(9,0), Vector2i(10,0), Vector2i(13,0), Vector2i(19,0), Vector2i(20,0), Vector2i(21,0), Vector2i(22,0),
|
|
Vector2i(0,1), Vector2i(1,1), Vector2i(2,1), Vector2i(3,1), Vector2i(6,1),
|
|
Vector2i(0,2), Vector2i(1,2), Vector2i(2,2), Vector2i(3,2),
|
|
Vector2i(17,9), Vector2i(18,9), Vector2i(19,9), Vector2i(20,9), Vector2i(21,9), Vector2i(22,9),
|
|
Vector2i(11,10), Vector2i(12,10), Vector2i(13,10), Vector2i(15,10), Vector2i(16,10), Vector2i(17,10), Vector2i(18,10), Vector2i(19,10), Vector2i(20,10), Vector2i(21,10), Vector2i(22,10),
|
|
Vector2i(0,11), Vector2i(4,11), Vector2i(5,11), Vector2i(6,11), Vector2i(9,11), Vector2i(10,11), Vector2i(11,11), Vector2i(12,11), Vector2i(13,11), Vector2i(14,11), Vector2i(15,11), Vector2i(16,11), Vector2i(17,11), Vector2i(18,11), Vector2i(19,11), Vector2i(20,11), Vector2i(21,11), Vector2i(22,11),
|
|
# Fixed User Obstacles Below:
|
|
Vector2i(2,8), # Cactus 1
|
|
Vector2i(4,4), # Cactus 2
|
|
Vector2i(12,6), # Cactus 3
|
|
Vector2i(12,9), # Cactus 4
|
|
Vector2i(5,9), Vector2i(5,10), # Wall rock 1
|
|
Vector2i(10,1), Vector2i(10,2), Vector2i(11,1), # Wall rock 2
|
|
Vector2i(17,6), # Wall rock 3
|
|
Vector2i(7,4), Vector2i(7,5), # Tree 1
|
|
Vector2i(10,4), # Tree 2
|
|
Vector2i(13,3), Vector2i(14,3), Vector2i(14,4), # Tree 3
|
|
Vector2i(20,4), Vector2i(20,5), # Tree 4
|
|
Vector2i(9,8), # Statue 1
|
|
Vector2i(17,3) # Statue 2
|
|
]
|
|
|
|
# Create bands based on X (Horizontal Progress)
|
|
for x in range(gridmap.columns):
|
|
for z in range(gridmap.rows):
|
|
var current_pos = Vector2i(x, z)
|
|
if current_pos in non_walkable_coords:
|
|
gridmap.set_cell_item(Vector3i(x, 0, z), TILE_OBSTACLE) # wall block on Floor 0
|
|
gridmap.set_cell_item(Vector3i(x, 1, z), -1)
|
|
continue
|
|
|
|
var tile_id = TILE_WALKABLE
|
|
if x == 0:
|
|
tile_id = TILE_START
|
|
elif x == gridmap.columns - 1:
|
|
tile_id = TILE_FINISH
|
|
|
|
gridmap.set_cell_item(Vector3i(x, 0, z), tile_id)
|
|
gridmap.set_cell_item(Vector3i(x, 1, z), -1)
|
|
|
|
# Create rooms (Option B: hollow inside, walls around edges)
|
|
# Room 1: (7,6) to (11,9) - 5x4 Area. Boundaries: X[7..11], Z[6..9]
|
|
var r1_entrances = [Vector2i(7, 8), Vector2i(9, 6), Vector2i(9, 9), Vector2i(11, 8)]
|
|
_create_room_with_edge_walls(gridmap, 7, 6, 11, 9, r1_entrances)
|
|
|
|
# Non-walkable obstacle inside Room 1
|
|
gridmap.set_cell_item(Vector3i(9, 0, 8), TILE_OBSTACLE)
|
|
|
|
# Room 2: (15,1) to (19,5) - 5x5 Area. Boundaries: X[15..19], Z[1..5]
|
|
var r2_entrances = [Vector2i(15, 2), Vector2i(17, 1), Vector2i(19, 2), Vector2i(18, 5)]
|
|
_create_room_with_edge_walls(gridmap, 15, 1, 19, 5, r2_entrances)
|
|
|
|
gridmap.diagonal_movement = true
|
|
gridmap.update_grid_data()
|
|
gridmap.initialize_astar()
|
|
|
|
func _create_room_with_edge_walls(gridmap: GridMap, x_start: int, z_start: int, x_end: int, z_end: int, entrances: Array):
|
|
for x in range(x_start, x_end + 1):
|
|
for z in range(z_start, z_end + 1):
|
|
var is_north = (z == z_start)
|
|
var is_south = (z == z_end)
|
|
var is_west = (x == x_start)
|
|
var is_east = (x == x_end)
|
|
|
|
if is_north or is_south or is_west or is_east:
|
|
if not Vector2i(x, z) in entrances:
|
|
# Determine placement SIDE and rotation
|
|
if is_north: _instantiate_safe_zone_wall(gridmap, x, z, "NORTH")
|
|
if is_south: _instantiate_safe_zone_wall(gridmap, x, z, "SOUTH")
|
|
if is_west: _instantiate_safe_zone_wall(gridmap, x, z, "WEST")
|
|
if is_east: _instantiate_safe_zone_wall(gridmap, x, z, "EAST")
|
|
|
|
# Ensure ground is floor
|
|
var current_f0 = gridmap.get_cell_item(Vector3i(x, 0, z))
|
|
if current_f0 == -1:
|
|
gridmap.set_cell_item(Vector3i(x, 0, z), TILE_WALKABLE)
|
|
|
|
func _instantiate_safe_zone_wall(gridmap: GridMap, x: int, z: int, side: String):
|
|
var wall_scene = load("res://scenes/safe_zone_wall.tscn")
|
|
if not wall_scene: return
|
|
|
|
var wall = wall_scene.instantiate()
|
|
gridmap.add_child(wall)
|
|
|
|
# Slightly shorten the wall (from 1.0 to 0.95) to allow diagonal raycasts to pass corners
|
|
wall.scale.x = 0.95
|
|
|
|
var pos = Vector3(
|
|
x * gridmap.cell_size.x + gridmap.cell_size.x/2,
|
|
0.5,
|
|
z * gridmap.cell_size.z + gridmap.cell_size.z/2
|
|
)
|
|
|
|
# Adjust position to the LINE between tiles based on side
|
|
match side:
|
|
"NORTH":
|
|
pos.z = z * gridmap.cell_size.z # Top edge of cell
|
|
wall.rotation_degrees.y = 0
|
|
"SOUTH":
|
|
pos.z = (z + 1) * gridmap.cell_size.z # Bottom edge
|
|
wall.rotation_degrees.y = 0
|
|
"WEST":
|
|
pos.x = x * gridmap.cell_size.x # Left edge
|
|
wall.rotation_degrees.y = 90
|
|
"EAST":
|
|
pos.x = (x + 1) * gridmap.cell_size.x # Right edge
|
|
wall.rotation_degrees.y = 90
|
|
|
|
wall.add_to_group("SafeZoneWalls")
|
|
wall.global_position = pos
|
|
|
|
func setup_mission_tiles():
|
|
"""Public wrapper to trigger mission tile spawning before game start."""
|
|
if multiplayer.is_server():
|
|
_spawn_mission_tiles()
|
|
|
|
func spawn_initial_powerups():
|
|
"""Public wrapper to spawn powerups before game start."""
|
|
if multiplayer.is_server():
|
|
_spawn_powerup_tiles()
|
|
|
|
func _spawn_mission_tiles():
|
|
var gridmap = get_parent().get_node_or_null("EnhancedGridMap")
|
|
if not gridmap:
|
|
gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
|
|
if not gridmap: return
|
|
|
|
# Forbidden Zones (Start, Finish) - No items here
|
|
var forbidden_x = [0, gridmap.columns - 1]
|
|
|
|
# Goal items: Heart(7), Diamond(8), Star(9), Coin(10)
|
|
var goal_items = [7, 8, 9, 10]
|
|
|
|
for x in range(gridmap.columns):
|
|
if x in forbidden_x:
|
|
continue # Clear zone
|
|
|
|
for z in range(gridmap.rows):
|
|
# Ensure we don't spawn on obstacles
|
|
var base_tile = gridmap.get_cell_item(Vector3i(x, 0, z))
|
|
var current_item = gridmap.get_cell_item(Vector3i(x, 1, z))
|
|
|
|
# PROTECTED FLOOR CHECK: Don't spawn on walls, void, or static powerup pods
|
|
if base_tile in [TILE_OBSTACLE, -1, TILE_LIGHTNING_STONE] or current_item == TILE_OBSTACLE:
|
|
continue
|
|
|
|
# Spawn tiles with 60% density
|
|
if randf() > 0.6:
|
|
gridmap.set_cell_item(Vector3i(x, 1, z), -1)
|
|
continue
|
|
|
|
var tile_type = goal_items[randi() % goal_items.size()]
|
|
gridmap.set_cell_item(Vector3i(x, 1, z), tile_type)
|
|
|
|
# Sync to clients
|
|
var main = get_node("/root/Main")
|
|
if main:
|
|
main.rpc("sync_grid_item", x, 1, z, tile_type)
|
|
|
|
func _assign_missions():
|
|
# NO-OP: Missions are now achievement-based (Complete 3 Goals)
|
|
# which is tracked natively by GoalsCycleManager.
|
|
pass
|
|
|
|
@rpc("authority", "call_local", "reliable")
|
|
func sync_missions(missions: Dictionary):
|
|
player_missions = missions
|
|
|
|
func check_movement_violation(player_id: int, from: Vector2i, to: Vector2i) -> bool:
|
|
"""Check if movement is illegal (during STOP phase and not in safe zone)."""
|
|
if current_phase == Phase.STOP:
|
|
# Use dynamic safe zone position check instead of static tile check
|
|
if not _is_in_safe_zone(from):
|
|
var gridmap = get_parent().get_node_or_null("EnhancedGridMap")
|
|
if not gridmap:
|
|
gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
|
|
if gridmap:
|
|
var tile_from = gridmap.get_cell_item(Vector3i(from.x, 0, from.y))
|
|
if tile_from != TILE_START and tile_from != TILE_FINISH:
|
|
_penalize_player(player_id)
|
|
return true
|
|
return false
|
|
|
|
func _penalize_player(player_id: int):
|
|
if not multiplayer.is_server(): return
|
|
|
|
var main = get_node("/root/Main")
|
|
if not main: return
|
|
|
|
var player_node = main.get_node_or_null(str(player_id))
|
|
if player_node:
|
|
# We now use the 'on_stop_phase_violation' RPC which scatters tiles around the player
|
|
# like they got attacked, instead of just dropping them in one pile.
|
|
if can_rpc():
|
|
player_node.rpc("on_stop_phase_violation")
|
|
|
|
# Notification is also handled inside on_stop_phase_violation on the player node
|
|
emit_signal("player_penalized", player_id)
|
|
|
|
func update_mission_progress(_player_id: int, _tile_id: int):
|
|
# Redundant in Board-based mode, but kept for compatibility.
|
|
# The board is synced separately via sync_playerboard in playerboard_manager.gd.
|
|
pass
|
|
|
|
@rpc("any_peer", "call_local", "reliable")
|
|
func sync_mission_progress(_player_id: int, _mission_index: int, _current: int):
|
|
# Deprecated
|
|
pass
|
|
|
|
func is_mission_complete(player_id: int) -> bool:
|
|
var main = get_node_or_null("/root/Main")
|
|
if not main: return false
|
|
|
|
var goals_cycle_manager = main.get_node_or_null("GoalsCycleManager")
|
|
if not goals_cycle_manager: return false
|
|
|
|
var completed_count = goals_cycle_manager.player_goal_counts.get(player_id, 0)
|
|
return completed_count >= LobbyManager.sng_required_goals
|
|
|
|
func check_win_condition(player_id: int, position: Vector2i) -> bool:
|
|
# 1. Must reach the finish line (Column 21)
|
|
if position.x < finish_line_x:
|
|
return false
|
|
|
|
# 2. Must have enough Goal Completions
|
|
if is_mission_complete(player_id):
|
|
print("[StopNGo] Player %d REACHED FINISH with goals complete!" % player_id)
|
|
return true
|
|
else:
|
|
# Inform the player locally if they reach the end without goals
|
|
var main = get_node_or_null("/root/Main")
|
|
var player_node = main.get_node_or_null(str(player_id)) if main else null
|
|
if player_node:
|
|
NotificationManager.send_message(player_node, "Incomplete! Achieve %d goals to win!" % LobbyManager.sng_required_goals, NotificationManager.MessageType.WARNING)
|
|
|
|
print("[StopNGo] Player %d reached finish but goals incomplete." % player_id)
|
|
return false
|
|
|
|
# =============================================================================
|
|
# Static Safe Zone
|
|
# =============================================================================
|
|
|
|
func _is_in_safe_zone(pos: Vector2i) -> bool:
|
|
var gridmap = get_parent().get_node_or_null("EnhancedGridMap")
|
|
if not gridmap:
|
|
gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
|
|
if not gridmap: return false
|
|
|
|
# 1. Check Floor 0 for Start (3) or Finish (3) lines
|
|
var floor_tile = gridmap.get_cell_item(Vector3i(pos.x, 0, pos.y))
|
|
if floor_tile == TILE_START or floor_tile == TILE_FINISH:
|
|
return true
|
|
|
|
# 2. Check Layer 2 (Overlay layer) for Dynamic Safe Zone tile
|
|
var overlay_tile = gridmap.get_cell_item(Vector3i(pos.x, 2, pos.y))
|
|
return overlay_tile == TILE_SAFE
|
|
|
|
func _spawn_dynamic_safe_zone():
|
|
if not multiplayer.is_server(): return
|
|
var gridmap = get_parent().get_node_or_null("EnhancedGridMap")
|
|
if not gridmap: gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
|
|
if not gridmap: return
|
|
|
|
var main = get_node("/root/Main")
|
|
var possible_rects = []
|
|
|
|
# Check all possible 3x2 and 2x3 areas
|
|
for x in range(1, gridmap.columns - 3):
|
|
for z in range(1, gridmap.rows - 2):
|
|
if _is_valid_safe_zone_area(gridmap, x, z, 3, 2):
|
|
possible_rects.append(Rect2i(x, z, 3, 2))
|
|
if _is_valid_safe_zone_area(gridmap, x, z, 2, 3):
|
|
possible_rects.append(Rect2i(x, z, 2, 3))
|
|
|
|
if possible_rects.size() > 0:
|
|
var rect = possible_rects.pick_random()
|
|
active_safe_zone_rects.append(rect)
|
|
spawned_safe_zones += 1
|
|
print("[StopNGo] Spawning Safe Zone %d at %s" % [spawned_safe_zones, rect])
|
|
|
|
# Paint Floor 2 (Overlay layer) to TILE_SAFE
|
|
# This allows seeing items below on Floor 1
|
|
for rx in range(rect.size.x):
|
|
for rz in range(rect.size.y):
|
|
var px = rect.position.x + rx
|
|
var pz = rect.position.y + rz
|
|
gridmap.set_cell_item(Vector3i(px, 2, pz), TILE_SAFE)
|
|
if can_rpc() and main:
|
|
main.rpc("sync_grid_item", px, 2, pz, TILE_SAFE)
|
|
|
|
@rpc("authority", "call_local", "reliable")
|
|
func sync_all_safe_zones_vfx():
|
|
var main = get_node_or_null("/root/Main")
|
|
if main and main.get("vfx_manager"):
|
|
if main.vfx_manager.has_method("play_safe_zone_appear"):
|
|
main.vfx_manager.play_safe_zone_appear()
|
|
elif main.vfx_manager.get("animation_player"):
|
|
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
|
|
|
|
if not _cached_safe_zone_mesh:
|
|
_cached_safe_zone_mesh = gridmap.mesh_library.get_item_mesh(TILE_SAFE)
|
|
|
|
var original_mesh = _cached_safe_zone_mesh
|
|
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:
|
|
# Avoid bounds or start/finish cols
|
|
if start_x < 2 or start_x + width > gridmap.columns - 2: return false
|
|
if start_z < 1 or start_z + height > gridmap.rows - 1: return false
|
|
|
|
var test_rect = Rect2i(start_x, start_z, width, height)
|
|
for existing in active_safe_zone_rects:
|
|
if test_rect.intersects(existing):
|
|
return false
|
|
|
|
for x in range(start_x, start_x + width):
|
|
for z in range(start_z, start_z + height):
|
|
var floor_0 = gridmap.get_cell_item(Vector3i(x, 0, z))
|
|
|
|
# Floor must be purely TILE_WALKABLE (0)
|
|
# We no longer check Floor 1 (items) so safe zones can spawn ON TOP of tiles
|
|
if floor_0 != TILE_WALKABLE:
|
|
return false
|
|
|
|
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
|
|
|
|
# Instead of continuously duplicating, use the current duplicated material for tweening.
|
|
var fade_mat: StandardMaterial3D = cur_mat
|
|
|
|
var start_alpha: float = fade_mat.albedo_color.a
|
|
var tween = create_tween()
|
|
|
|
# Phase 1 — Hold alpha steady (warn the player via outlines only to prevent panel z-fighting glitch)
|
|
tween.tween_method(
|
|
func(a: float):
|
|
if is_instance_valid(fade_mat): fade_mat.albedo_color.a = a,
|
|
start_alpha, start_alpha, 0.15
|
|
).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_SINE)
|
|
|
|
# Phase 2 — Fade out: alpha start_alpha → 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,
|
|
start_alpha, 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)
|
|
|
|
# Wait for animation to finish, then free outlines locally
|
|
await get_tree().create_timer(0.6).timeout
|
|
for outline in _outline_nodes:
|
|
if is_instance_valid(outline):
|
|
outline.queue_free()
|
|
_outline_nodes.clear()
|
|
|
|
# Restore the original safe zone mesh to prevent material/alpha leakage
|
|
if _cached_safe_zone_mesh and gridmap and gridmap.mesh_library:
|
|
gridmap.mesh_library.set_item_mesh(TILE_SAFE, _cached_safe_zone_mesh)
|
|
|
|
func _clear_dynamic_safe_zones():
|
|
var gridmap = get_parent().get_node_or_null("EnhancedGridMap")
|
|
if not gridmap: gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
|
|
if not gridmap: return
|
|
var main = get_node_or_null("/root/Main")
|
|
|
|
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 rx in range(rect.size.x):
|
|
for rz in range(rect.size.y):
|
|
var px = rect.position.x + rx
|
|
var pz = rect.position.y + rz
|
|
|
|
# Only clear layer 2
|
|
if gridmap.get_cell_item(Vector3i(px, 2, pz)) == TILE_SAFE:
|
|
gridmap.set_cell_item(Vector3i(px, 2, pz), -1)
|
|
if can_rpc() and main:
|
|
main.rpc("sync_grid_item", px, 2, pz, -1)
|
|
|
|
active_safe_zone_rects.clear()
|
|
spawned_safe_zones = 0
|
|
|
|
# Outline deletion and mesh restoration is now handled locally by each client
|
|
# at the end of _animate_safe_zone_disappear()
|
|
|
|
func _scatter_player_tiles(player_node: Node):
|
|
"""Server: Take all tiles from player's playerboard and scatter them onto nearby grid cells."""
|
|
if not multiplayer.is_server(): return
|
|
|
|
var main = get_node_or_null("/root/Main")
|
|
if not main: return
|
|
|
|
var gridmap = main.get_node_or_null("EnhancedGridMap")
|
|
if not gridmap: return
|
|
|
|
var peer_id = player_node.name.to_int()
|
|
var playerboard = player_node.playerboard
|
|
|
|
# Collect all non-empty tile indices from playerboard
|
|
var occupied_indices: Array[int] = []
|
|
for i in range(playerboard.size()):
|
|
if playerboard[i] != -1:
|
|
occupied_indices.append(i)
|
|
|
|
if occupied_indices.is_empty():
|
|
return # Nothing to scatter
|
|
|
|
# Select up to 3 random tiles to scatter
|
|
var rng = RandomNumberGenerator.new()
|
|
rng.randomize()
|
|
occupied_indices.shuffle()
|
|
|
|
var tiles_to_scatter: Array[int] = []
|
|
var scatter_count = min(occupied_indices.size(), 3)
|
|
|
|
for i in range(scatter_count):
|
|
var board_idx = occupied_indices[i]
|
|
tiles_to_scatter.append(playerboard[board_idx])
|
|
playerboard[board_idx] = -1 # Remove only the scattered tiles from board
|
|
|
|
# Find valid nearby positions to drop tiles (within radius 3 of player)
|
|
var center = player_node.current_position
|
|
var valid_drop_positions: Array[Vector2i] = []
|
|
for dx in range(-3, 4):
|
|
for dz in range(-3, 4):
|
|
var pos = Vector2i(center.x + dx, center.y + dz)
|
|
# Bounds check
|
|
if pos.x < 0 or pos.x >= gridmap.columns or pos.y < 0 or pos.y >= gridmap.rows:
|
|
continue
|
|
# Check floor is walkable (not void, not obstacle, not static powerup spawn)
|
|
var floor_tile = gridmap.get_cell_item(Vector3i(pos.x, 0, pos.y))
|
|
if floor_tile == -1 or floor_tile == TILE_OBSTACLE or floor_tile == TILE_LIGHTNING_STONE:
|
|
continue
|
|
# Check floor 1 is empty (no existing item)
|
|
var existing_item = gridmap.get_cell_item(Vector3i(pos.x, 1, pos.y))
|
|
if existing_item != -1:
|
|
continue
|
|
valid_drop_positions.append(pos)
|
|
|
|
# Scatter tiles onto valid positions
|
|
rng.randomize()
|
|
|
|
for tile in tiles_to_scatter:
|
|
if valid_drop_positions.is_empty():
|
|
break # No more space
|
|
|
|
var idx = rng.randi() % valid_drop_positions.size()
|
|
var drop_pos = valid_drop_positions[idx]
|
|
valid_drop_positions.remove_at(idx)
|
|
|
|
# Place tile on grid Floor 1
|
|
gridmap.set_cell_item(Vector3i(drop_pos.x, 1, drop_pos.y), tile)
|
|
# Sync to all clients
|
|
main.rpc("sync_grid_item", drop_pos.x, 1, drop_pos.y, tile)
|
|
|
|
# Sync cleared playerboard to all clients
|
|
# Sync cleared playerboard via the player's own RPC (more robust than Main lookup)
|
|
player_node.rpc("sync_playerboard", playerboard)
|
|
|
|
# FREEZE FIX: If they were caught outside, they should also be frozen like a movement violation
|
|
player_node.rpc("sync_stop_freeze", true)
|
|
|
|
# Notify the player
|
|
SfxManager.rpc("play_rpc", "tile_scatter")
|
|
NotificationManager.send_message(player_node, "Not in Safe Zone! Tiles scattered!", NotificationManager.MessageType.WARNING)
|
|
|
|
# Screen shake and animation
|
|
if can_rpc():
|
|
if player_node.has_method("trigger_screen_shake"):
|
|
player_node.rpc("trigger_screen_shake", "heavy")
|
|
if player_node.has_method("play_scatter_knock"):
|
|
player_node.rpc("play_scatter_knock")
|
|
|
|
print("[StopNGo] Caught outside: Scattered %d tiles from Player %d" % [tiles_to_scatter.size(), peer_id])
|
|
|
|
# =============================================================================
|
|
# OLD STATIC SAFE ZONE LOGIC (Retained for Reference)
|
|
# =============================================================================
|
|
# var safe_zone_columns: Array[int] = [5, 10, 15]
|
|
|
|
# func _is_in_safe_zone_old(pos: Vector2i) -> bool:
|
|
# return pos.x in safe_zone_columns
|
|
|
|
# func _paint_static_safe_zones(gridmap):
|
|
# for x in safe_zone_columns:
|
|
# for z in range(gridmap.rows):
|
|
# gridmap.set_cell_item(Vector3i(x, 0, z), TILE_SAFE)
|
|
# # Optional: instantiate walls
|
|
# # _instantiate_safe_zone_wall(gridmap, x, z)
|
|
|
|
# func _instantiate_safe_zone_wall(gridmap, x: int, z: int):
|
|
# var wall_scene = load("res://scenes/environment/safe_zone_wall.tscn")
|
|
# if wall_scene:
|
|
# var wall = wall_scene.instantiate()
|
|
# gridmap.add_child(wall)
|
|
# wall.global_position = Vector3(
|
|
# x * gridmap.cell_size.x + gridmap.cell_size.x/2,
|
|
# 0,
|
|
# z * gridmap.cell_size.z + gridmap.cell_size.z/2
|
|
# )
|
|
# =============================================================================
|
|
|
|
# =============================================================================
|
|
# Power-Up Tile Spawning (Speed & Ghost)
|
|
# =============================================================================
|
|
|
|
func _spawn_powerup_tiles():
|
|
"""Server: Spawn 5 permanent power-up tiles at fixed positions."""
|
|
if not multiplayer.is_server(): return
|
|
|
|
var main = get_node_or_null("/root/Main")
|
|
if not main: return
|
|
|
|
var gridmap = main.get_node_or_null("EnhancedGridMap")
|
|
if not gridmap: return
|
|
|
|
print("[StopNGo] Spawning/Refreshing 5 static power-up tiles...")
|
|
|
|
for i in range(PERMANENT_POWERUP_LOCATIONS.size()):
|
|
var pos = PERMANENT_POWERUP_LOCATIONS[i]
|
|
|
|
# Set Floor 0 beneath power-up to ID 15 (Ancient Lightning Stone)
|
|
gridmap.set_cell_item(Vector3i(pos.x, 0, pos.y), TILE_LIGHTNING_STONE)
|
|
|
|
# Select a random power-up type (User Request: ensure all types can spawn)
|
|
var tile_id = POWERUP_TILES.pick_random()
|
|
|
|
# Place on Floor 1
|
|
gridmap.set_cell_item(Vector3i(pos.x, 1, pos.y), tile_id)
|
|
|
|
# Sync both floor and tile to all clients and host
|
|
if can_rpc():
|
|
main.rpc("sync_grid_item", pos.x, 0, pos.y, TILE_LIGHTNING_STONE) # Sync floor change (Stone on)
|
|
main.rpc("sync_grid_item", pos.x, 1, pos.y, tile_id) # Sync power-up
|
|
|
|
powerups_spawned = true
|
|
print("[StopNGo] Static power-up refresh completed.")
|