feat: Implement the main game scene with new player functionality, Stop n Go and Portal Mode managers, a dynamic message bar, and pre-game countdown logic.
This commit is contained in:
+32
-7
@@ -807,15 +807,41 @@ func _assign_random_spawn_positions():
|
||||
print("Assigned spawn %s to player %s" % [assigned_pos, player.name])
|
||||
|
||||
func _assign_stop_n_go_spawn_positions(all_players: Array):
|
||||
"""Assigns spawns to the far left columns (Start Line) for Stop N Go mode."""
|
||||
"""Assigns random spawns on walkable arena tiles for Stop N Go mode."""
|
||||
# Sort players for deterministic assignment based on ID
|
||||
all_players.sort_custom(func(a, b): return a.name.to_int() < b.name.to_int())
|
||||
|
||||
var spawn_index = 0
|
||||
var enhanced_gridmap = $EnhancedGridMap
|
||||
if not enhanced_gridmap:
|
||||
return
|
||||
|
||||
# Collect all valid walkable positions (not obstacle, not void)
|
||||
var valid_positions: Array[Vector2i] = []
|
||||
for x in range(enhanced_gridmap.columns):
|
||||
for z in range(enhanced_gridmap.rows):
|
||||
var tile = enhanced_gridmap.get_cell_item(Vector3i(x, 0, z))
|
||||
# Accept walkable (0), start (3), finish (3) — skip obstacles (4) and void (-1)
|
||||
if tile != -1 and tile != 4:
|
||||
valid_positions.append(Vector2i(x, z))
|
||||
|
||||
valid_positions.shuffle()
|
||||
|
||||
var used_positions: Array[Vector2i] = []
|
||||
|
||||
for player in all_players:
|
||||
# Use deterministic assignment from (0, 1) to (0, 8) to keep players separate
|
||||
# Start Line is Column 0. We use rows 1 to 8.
|
||||
var assigned_pos = Vector2i(0, spawn_index + 1)
|
||||
var assigned_pos = Vector2i(-1, -1)
|
||||
|
||||
# Find a random walkable position not occupied by another player
|
||||
for pos in valid_positions:
|
||||
if pos not in used_positions:
|
||||
assigned_pos = pos
|
||||
break
|
||||
|
||||
if assigned_pos == Vector2i(-1, -1):
|
||||
# Fallback: center of arena
|
||||
assigned_pos = Vector2i(int(enhanced_gridmap.columns / 2), int(enhanced_gridmap.rows / 2))
|
||||
|
||||
used_positions.append(assigned_pos)
|
||||
|
||||
# Ensure immediate sync
|
||||
player.position = player.grid_to_world(assigned_pos)
|
||||
@@ -825,8 +851,7 @@ func _assign_stop_n_go_spawn_positions(all_players: Array):
|
||||
if can_rpc():
|
||||
player.rpc("set_spawn_position", assigned_pos)
|
||||
|
||||
spawn_index += 1
|
||||
print("[StopNGo] Assigned fixed starting block %s to player %s" % [assigned_pos, player.name])
|
||||
print("[StopNGo] Assigned random spawn %s to player %s" % [assigned_pos, player.name])
|
||||
|
||||
func _assign_portal_mode_spawn_positions(all_players: Array):
|
||||
"""Assigns spawns to different quadrants for Tekton Doors mode."""
|
||||
|
||||
+19
-19
@@ -737,11 +737,11 @@ func apply_stagger(duration: float = 1.5):
|
||||
# If still immune, show immunity tint (Green?), otherwise White
|
||||
# UNLESS we are still stop-frozen (Cyan)
|
||||
if is_stop_frozen:
|
||||
_apply_tint_recursive(self, Color.CYAN)
|
||||
_apply_tint_recursive(self , Color.CYAN)
|
||||
elif immunity_timer > 0:
|
||||
_apply_tint_recursive(self, Color(0.5, 1.0, 0.5)) # Light Green for immunity
|
||||
_apply_tint_recursive(self , Color(0.5, 1.0, 0.5)) # Light Green for immunity
|
||||
else:
|
||||
_apply_tint_recursive(self, Color.WHITE) # Remove tint
|
||||
_apply_tint_recursive(self , Color.WHITE) # Remove tint
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func sync_stop_freeze(enabled: bool):
|
||||
@@ -753,16 +753,16 @@ func sync_stop_freeze(enabled: bool):
|
||||
is_stop_frozen = enabled
|
||||
|
||||
if enabled:
|
||||
_apply_tint_recursive(self, Color.CYAN)
|
||||
_apply_tint_recursive(self , Color.CYAN)
|
||||
print("[STOP n GO] Player %s FROZEN until GO phase" % name)
|
||||
else:
|
||||
# Restore appropriate tint
|
||||
if is_frozen:
|
||||
_apply_tint_recursive(self, Color.BLUE)
|
||||
_apply_tint_recursive(self , Color.BLUE)
|
||||
elif immunity_timer > 0:
|
||||
_apply_tint_recursive(self, Color(0.5, 1.0, 0.5))
|
||||
_apply_tint_recursive(self , Color(0.5, 1.0, 0.5))
|
||||
else:
|
||||
_apply_tint_recursive(self, Color.WHITE)
|
||||
_apply_tint_recursive(self , Color.WHITE)
|
||||
print("[STOP n GO] Player %s UNFROZEN" % name)
|
||||
|
||||
@rpc("any_peer", "call_local")
|
||||
@@ -905,7 +905,7 @@ func on_stop_phase_violation():
|
||||
var cell = Vector3i(pos.x, 1, pos.y)
|
||||
rpc("sync_grid_item", cell.x, cell.y, cell.z, item_id)
|
||||
|
||||
NotificationManager.send_message(self, "STOP VIOLATION! Tiles scattered!", NotificationManager.MessageType.WARNING)
|
||||
NotificationManager.send_message(self , "STOP VIOLATION! Tiles scattered!", NotificationManager.MessageType.WARNING)
|
||||
|
||||
func _find_multiple_drop_positions(count: int) -> Array:
|
||||
var positions = []
|
||||
@@ -1051,7 +1051,7 @@ func _process_remote_interpolation(_delta):
|
||||
# Fallback to simple lerp if not enough snapshots
|
||||
# Keep this very soft to smooth out transitions between tween and interpolation
|
||||
if global_position.distance_squared_to(target_visual_position) > 0.001:
|
||||
global_position = global_position.lerp(target_visual_position, _delta * 10.0)
|
||||
global_position = global_position.lerp(target_visual_position, _delta * 10.0)
|
||||
return
|
||||
|
||||
var render_time = Time.get_ticks_msec() - INTERPOLATION_OFFSET
|
||||
@@ -1063,7 +1063,7 @@ func _process_remote_interpolation(_delta):
|
||||
for i in range(1, snapshot_buffer.size()):
|
||||
if snapshot_buffer[i].time > render_time:
|
||||
newer = snapshot_buffer[i]
|
||||
older = snapshot_buffer[i-1]
|
||||
older = snapshot_buffer[i - 1]
|
||||
break
|
||||
|
||||
if newer:
|
||||
@@ -1257,7 +1257,7 @@ func _find_random_spawn_position() -> Vector2i:
|
||||
# We should check if it is NOT TILE_OBSTACLE.
|
||||
var item = enhanced_gridmap.get_cell_item(Vector3i(x, 0, z))
|
||||
# Assuming 4 is obstacle, and -1 is void. 0 is walkable, 2 is safe zone.
|
||||
if item != -1 and item != 4:
|
||||
if item != -1 and item != 4:
|
||||
if not is_position_occupied(pos):
|
||||
available_positions.append(pos)
|
||||
|
||||
@@ -2164,13 +2164,13 @@ func sync_throw_tekton(target_pos: Vector2i):
|
||||
# 1. Stun nearby players (Radius 2?)
|
||||
# "if there's a player around that floor they will got stunned" -> "around that floor" implies radius
|
||||
var impact_center = target_pos
|
||||
var stun_radius = 1.5
|
||||
var stun_radius = 1.5
|
||||
|
||||
var players = get_tree().get_nodes_in_group("Players")
|
||||
print("[Throw] Checking stun impact at %s. Found %d players." % [impact_center, players.size()])
|
||||
|
||||
for p in players:
|
||||
if p == self: continue
|
||||
if p == self: continue
|
||||
|
||||
# Check distance
|
||||
var dist = Vector2(p.current_position.x, p.current_position.y).distance_to(Vector2(impact_center.x, impact_center.y))
|
||||
@@ -2184,7 +2184,7 @@ func sync_throw_tekton(target_pos: Vector2i):
|
||||
|
||||
# 2. Tekton drops tiles (Spawn tiles around) AND shrinks
|
||||
if tekton.has_method("on_thrown_landing"):
|
||||
tekton.on_thrown_landing(self, 2.0)
|
||||
tekton.on_thrown_landing(self , 2.0)
|
||||
else:
|
||||
# Fallback
|
||||
tekton.on_hit(self , 1.0)
|
||||
@@ -2218,7 +2218,7 @@ func sync_drop_tekton():
|
||||
|
||||
# Trigger landing effects (minimal scale)
|
||||
if tekton.has_method("on_thrown_landing"):
|
||||
tekton.on_thrown_landing(self, 1.0) # Minimal scale impact
|
||||
tekton.on_thrown_landing(self , 1.0) # Minimal scale impact
|
||||
|
||||
print("[Player %s] Dropped Tekton at %s" % [name, current_position])
|
||||
|
||||
@@ -2231,7 +2231,7 @@ func enter_attack_mode():
|
||||
|
||||
is_attack_mode = true
|
||||
is_knock_mode = false # Mutually exclusive
|
||||
NotificationManager.send_message(self, "Attack Mode ACTIVATED (Red)", NotificationManager.MessageType.POWERUP)
|
||||
NotificationManager.send_message(self , "Attack Mode ACTIVATED (Red)", NotificationManager.MessageType.POWERUP)
|
||||
update_active_player_indicator()
|
||||
|
||||
func enter_knock_mode():
|
||||
@@ -2239,7 +2239,7 @@ func enter_knock_mode():
|
||||
|
||||
is_knock_mode = true
|
||||
is_attack_mode = false # Mutually exclusive
|
||||
NotificationManager.send_message(self, "Knock Mode ACTIVATED (Yellow)", NotificationManager.MessageType.POWERUP)
|
||||
NotificationManager.send_message(self , "Knock Mode ACTIVATED (Yellow)", NotificationManager.MessageType.POWERUP)
|
||||
update_active_player_indicator()
|
||||
|
||||
func update_active_player_indicator():
|
||||
@@ -2276,7 +2276,7 @@ func knock_tekton():
|
||||
|
||||
# Reset Knock Mode after successful hit
|
||||
is_knock_mode = false
|
||||
NotificationManager.send_message(self, "Knock Successful!", NotificationManager.MessageType.POWERUP)
|
||||
NotificationManager.send_message(self , "Knock Successful!", NotificationManager.MessageType.POWERUP)
|
||||
update_active_player_indicator()
|
||||
else:
|
||||
# If we called knock_tekton but nothing was nearby, maybe we just enter the mode?
|
||||
@@ -2289,7 +2289,7 @@ func sync_knock_tekton(tekton_path: NodePath):
|
||||
if tekton:
|
||||
# Intensity 2.0 for knock (drops 200% tiles) + Shrink/Recover
|
||||
# Use on_thrown_landing to trigger shrink animation and floor freeze
|
||||
tekton.on_thrown_landing(self, 2.0)
|
||||
tekton.on_thrown_landing(self , 2.0)
|
||||
print("[Player %s] Knocked Tekton %s" % [name, tekton.name])
|
||||
|
||||
# Visual feedback (Juice)
|
||||
|
||||
@@ -13,6 +13,16 @@ const GO_DURATION: float = 8.0
|
||||
const STOP_DURATION: float = 4.0
|
||||
const REQUIRED_GOALS: int = 8
|
||||
|
||||
# Dynamic Safe Zone
|
||||
const SAFE_ZONE_PRE_TIME: float = 5.0 # Seconds before STOP to spawn safe zone
|
||||
const SAFE_ZONE_RADIUS: int = 2 # 5x5 area (radius 2 from center)
|
||||
var safe_zone_center: Vector2i = Vector2i(-1, -1)
|
||||
var safe_zone_spawned: bool = false
|
||||
|
||||
# Power-Up Tile Spawning
|
||||
const POWERUP_TILES = [11, 14] # Speed (11) and Ghost (14)
|
||||
const POWERUP_SPAWN_COUNT: int = 5 # Number of power-up tiles to spawn
|
||||
|
||||
var current_phase: Phase = Phase.GO
|
||||
var phase_timer: float = GO_DURATION
|
||||
var is_active: bool = false
|
||||
@@ -82,6 +92,10 @@ func _process(delta):
|
||||
phase_timer -= delta
|
||||
|
||||
if multiplayer.is_server():
|
||||
# Spawn safe zone 5 seconds before STOP phase begins
|
||||
if current_phase == Phase.GO and not safe_zone_spawned and phase_timer <= SAFE_ZONE_PRE_TIME:
|
||||
_spawn_safe_zone()
|
||||
|
||||
if phase_timer <= 0:
|
||||
if current_phase == Phase.GO:
|
||||
_start_phase(Phase.STOP)
|
||||
@@ -198,7 +212,6 @@ func activate_client_side():
|
||||
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!
|
||||
@@ -217,8 +230,20 @@ func _start_phase(phase: Phase):
|
||||
if can_rpc():
|
||||
rpc("sync_phase", phase_name, phase_timer)
|
||||
|
||||
# If GO phase starts, clear all STOP phase freezes
|
||||
if phase == Phase.STOP:
|
||||
# --- DYNAMIC SAFE ZONE: Penalize players outside the zone ---
|
||||
if safe_zone_spawned:
|
||||
var all_players = get_tree().get_nodes_in_group("Players")
|
||||
for p in all_players:
|
||||
if not _is_in_safe_zone(p.current_position):
|
||||
_scatter_player_tiles(p)
|
||||
|
||||
# --- POWER-UP TILES: Spawn 5 Speed & Ghost tiles ---
|
||||
_spawn_powerup_tiles()
|
||||
|
||||
# If GO phase starts, clear all STOP phase freezes and safe zone
|
||||
if phase == Phase.GO:
|
||||
_clear_safe_zone()
|
||||
var all_players = get_tree().get_nodes_in_group("Players")
|
||||
for p in all_players:
|
||||
if p.has_method("sync_stop_freeze"):
|
||||
@@ -273,8 +298,8 @@ func _apply_arena_setup():
|
||||
# Clear existing items on all layers
|
||||
gridmap.clear()
|
||||
|
||||
# Safe Zones Columns: 6, 7, 8 (Only one band now)
|
||||
var safe_columns = [6, 7, 8]
|
||||
# Dynamic Safe Zone: No static safe columns anymore
|
||||
# Safe zone spawns randomly during gameplay
|
||||
|
||||
# Create bands based on X (Horizontal Progress)
|
||||
for x in range(gridmap.columns):
|
||||
@@ -283,8 +308,6 @@ func _apply_arena_setup():
|
||||
tile_id = TILE_START
|
||||
elif x == gridmap.columns - 1:
|
||||
tile_id = TILE_FINISH
|
||||
elif x in safe_columns:
|
||||
tile_id = TILE_SAFE
|
||||
|
||||
for z in range(gridmap.rows):
|
||||
gridmap.set_cell_item(Vector3i(x, 0, z), tile_id)
|
||||
@@ -306,8 +329,8 @@ func _spawn_mission_tiles():
|
||||
gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
|
||||
if not gridmap: return
|
||||
|
||||
# Forbidden Zones (Start, Safe, Finish) - No items here
|
||||
var forbidden_x = [0, 6, 7, 8, 21]
|
||||
# Forbidden Zones (Start, Finish) - No items here
|
||||
var forbidden_x = [0, 21]
|
||||
|
||||
# Goal items: Heart(7), Diamond(8), Star(9), Coin(10)
|
||||
var goal_items = [7, 8, 9, 10]
|
||||
@@ -350,18 +373,16 @@ func sync_missions(missions: Dictionary):
|
||||
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:
|
||||
var gridmap = get_parent().get_node_or_null("EnhancedGridMap")
|
||||
if not gridmap:
|
||||
gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
|
||||
if gridmap:
|
||||
# Check FROM position. If you were safe, you can move?
|
||||
# Rules: "If a player moves during this phase".
|
||||
# Usually implies checking if you ARE in a safe zone.
|
||||
var tile_from = gridmap.get_cell_item(Vector3i(from.x, 0, from.y))
|
||||
|
||||
if tile_from != TILE_SAFE and tile_from != TILE_START and tile_from != TILE_FINISH:
|
||||
_penalize_player(player_id)
|
||||
return true
|
||||
# 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):
|
||||
@@ -418,3 +439,229 @@ func check_win_condition(player_id: int, position: Vector2i) -> bool:
|
||||
|
||||
print("[StopNGo] Player %d reached finish but goals incomplete." % player_id)
|
||||
return false
|
||||
|
||||
# =============================================================================
|
||||
# Dynamic Safe Zone
|
||||
# =============================================================================
|
||||
|
||||
func _spawn_safe_zone():
|
||||
"""Server: Pick a random walkable position and spawn the 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
|
||||
|
||||
# Collect valid center positions (not too close to edges so the zone fits)
|
||||
var valid_positions: Array[Vector2i] = []
|
||||
for x in range(SAFE_ZONE_RADIUS, gridmap.columns - SAFE_ZONE_RADIUS):
|
||||
for z in range(SAFE_ZONE_RADIUS, gridmap.rows - SAFE_ZONE_RADIUS):
|
||||
var tile = gridmap.get_cell_item(Vector3i(x, 0, z))
|
||||
# Only walkable tiles (not start/finish)
|
||||
if tile == TILE_WALKABLE:
|
||||
valid_positions.append(Vector2i(x, z))
|
||||
|
||||
if valid_positions.is_empty():
|
||||
print("[StopNGo] WARNING: No valid position for safe zone!")
|
||||
return
|
||||
|
||||
# Pick random center
|
||||
var rng = RandomNumberGenerator.new()
|
||||
rng.randomize()
|
||||
safe_zone_center = valid_positions[rng.randi() % valid_positions.size()]
|
||||
safe_zone_spawned = true
|
||||
|
||||
print("[StopNGo] Safe Zone spawned at %s (radius %d)" % [safe_zone_center, SAFE_ZONE_RADIUS])
|
||||
|
||||
# Sync to all peers
|
||||
if can_rpc():
|
||||
rpc("sync_safe_zone", safe_zone_center, SAFE_ZONE_RADIUS)
|
||||
|
||||
func _clear_safe_zone():
|
||||
"""Server: Clear the safe zone overlay and reset state."""
|
||||
if not multiplayer.is_server(): return
|
||||
|
||||
if safe_zone_spawned:
|
||||
safe_zone_spawned = false
|
||||
safe_zone_center = Vector2i(-1, -1)
|
||||
|
||||
if can_rpc():
|
||||
rpc("sync_clear_safe_zone")
|
||||
|
||||
func _is_in_safe_zone(pos: Vector2i) -> bool:
|
||||
"""Check if a position is within the dynamic safe zone."""
|
||||
if not safe_zone_spawned or safe_zone_center == Vector2i(-1, -1):
|
||||
return false
|
||||
# Chebyshev distance (square area)
|
||||
var dx = abs(pos.x - safe_zone_center.x)
|
||||
var dz = abs(pos.y - safe_zone_center.y)
|
||||
return dx <= SAFE_ZONE_RADIUS and dz <= SAFE_ZONE_RADIUS
|
||||
|
||||
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
|
||||
var tiles_to_scatter: Array[int] = []
|
||||
|
||||
# Collect all non-empty tiles from playerboard
|
||||
for i in range(playerboard.size()):
|
||||
if playerboard[i] != -1:
|
||||
tiles_to_scatter.append(playerboard[i])
|
||||
playerboard[i] = -1
|
||||
|
||||
if tiles_to_scatter.is_empty():
|
||||
return # Nothing to scatter
|
||||
|
||||
# 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)
|
||||
var floor_tile = gridmap.get_cell_item(Vector3i(pos.x, 0, pos.y))
|
||||
if floor_tile == -1 or floor_tile == TILE_OBSTACLE:
|
||||
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
|
||||
var rng = RandomNumberGenerator.new()
|
||||
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
|
||||
main.rpc("sync_playerboard", peer_id, playerboard)
|
||||
|
||||
# Notify the player
|
||||
NotificationManager.send_message(player_node, "Not in Safe Zone! Tiles scattered!", NotificationManager.MessageType.WARNING)
|
||||
|
||||
# Screen shake
|
||||
if player_node.has_method("trigger_screen_shake") and can_rpc():
|
||||
player_node.rpc("trigger_screen_shake", "heavy")
|
||||
|
||||
print("[StopNGo] Scattered %d tiles from Player %d" % [tiles_to_scatter.size(), peer_id])
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_safe_zone(center: Vector2i, radius: int):
|
||||
"""Client: Show the safe zone overlay on the grid."""
|
||||
safe_zone_center = center
|
||||
safe_zone_spawned = true
|
||||
|
||||
var gridmap = get_parent().get_node_or_null("EnhancedGridMap")
|
||||
if not gridmap:
|
||||
gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
|
||||
if not gridmap: return
|
||||
|
||||
# Paint safe zone on Floor 2 (overlay layer) with TILE_SAFE visual
|
||||
for dx in range(-radius, radius + 1):
|
||||
for dz in range(-radius, radius + 1):
|
||||
var x = center.x + dx
|
||||
var z = center.y + dz
|
||||
if x >= 0 and x < gridmap.columns and z >= 0 and z < gridmap.rows:
|
||||
gridmap.set_cell_item(Vector3i(x, 2, z), TILE_SAFE)
|
||||
|
||||
# Notify local player
|
||||
var my_id = multiplayer.get_unique_id()
|
||||
var main = get_node_or_null("/root/Main")
|
||||
var player_node = main.get_node_or_null(str(my_id)) if main else null
|
||||
if player_node:
|
||||
NotificationManager.send_message(player_node, "⚠ Safe Zone spawned! Get inside!", NotificationManager.MessageType.WARNING)
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_clear_safe_zone():
|
||||
"""Client: Clear the safe zone overlay."""
|
||||
var gridmap = get_parent().get_node_or_null("EnhancedGridMap")
|
||||
if not gridmap:
|
||||
gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
|
||||
if not gridmap: return
|
||||
|
||||
if safe_zone_center != Vector2i(-1, -1):
|
||||
for dx in range(-SAFE_ZONE_RADIUS, SAFE_ZONE_RADIUS + 1):
|
||||
for dz in range(-SAFE_ZONE_RADIUS, SAFE_ZONE_RADIUS + 1):
|
||||
var x = safe_zone_center.x + dx
|
||||
var z = safe_zone_center.y + dz
|
||||
if x >= 0 and x < gridmap.columns and z >= 0 and z < gridmap.rows:
|
||||
gridmap.set_cell_item(Vector3i(x, 2, z), -1)
|
||||
|
||||
safe_zone_center = Vector2i(-1, -1)
|
||||
safe_zone_spawned = false
|
||||
|
||||
# =============================================================================
|
||||
# Power-Up Tile Spawning (Speed & Ghost)
|
||||
# =============================================================================
|
||||
|
||||
func _spawn_powerup_tiles():
|
||||
"""Server: Spawn 5 Speed & Ghost power-up tiles at random walkable 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
|
||||
|
||||
# Collect valid positions (walkable floor, no existing item on Floor 1)
|
||||
var valid_positions: Array[Vector2i] = []
|
||||
for x in range(gridmap.columns):
|
||||
for z in range(gridmap.rows):
|
||||
var floor_tile = gridmap.get_cell_item(Vector3i(x, 0, z))
|
||||
# Skip void, obstacles, start, finish
|
||||
if floor_tile == -1 or floor_tile == TILE_OBSTACLE:
|
||||
continue
|
||||
# Skip cells that already have items on Floor 1
|
||||
var existing_item = gridmap.get_cell_item(Vector3i(x, 1, z))
|
||||
if existing_item != -1:
|
||||
continue
|
||||
valid_positions.append(Vector2i(x, z))
|
||||
|
||||
if valid_positions.is_empty():
|
||||
print("[StopNGo] WARNING: No valid positions for power-up tiles!")
|
||||
return
|
||||
|
||||
# Shuffle and pick up to POWERUP_SPAWN_COUNT positions
|
||||
var rng = RandomNumberGenerator.new()
|
||||
rng.randomize()
|
||||
valid_positions.shuffle()
|
||||
|
||||
var spawn_count = min(POWERUP_SPAWN_COUNT, valid_positions.size())
|
||||
|
||||
for i in range(spawn_count):
|
||||
var pos = valid_positions[i]
|
||||
var tile_id = POWERUP_TILES[rng.randi() % POWERUP_TILES.size()]
|
||||
|
||||
# Place on Floor 1
|
||||
gridmap.set_cell_item(Vector3i(pos.x, 1, pos.y), tile_id)
|
||||
|
||||
# Sync to all clients
|
||||
if can_rpc():
|
||||
main.rpc("sync_grid_item", pos.x, 1, pos.y, tile_id)
|
||||
|
||||
print("[StopNGo] Spawned %d power-up tiles (Speed & Ghost)" % spawn_count)
|
||||
|
||||
Reference in New Issue
Block a user