feat: implement new 'Stop N Go' arena scene and manager.
This commit is contained in:
@@ -24,11 +24,11 @@ var stop_phase_occurred: bool = false
|
||||
var safe_zone_wall_scene = preload("res://scenes/wall_3d.tscn")
|
||||
|
||||
const PERMANENT_POWERUP_LOCATIONS: Array[Vector2i] = [
|
||||
Vector2i(4, 3), # Area 1
|
||||
Vector2i(8, 7), # Area 2
|
||||
Vector2i(11, 4), # Area 3
|
||||
Vector2i(15, 8), # Area 4
|
||||
Vector2i(18, 5) # Area 5
|
||||
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
|
||||
@@ -101,10 +101,6 @@ 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)
|
||||
@@ -242,19 +238,17 @@ func _start_phase(phase: Phase):
|
||||
|
||||
if phase == Phase.STOP:
|
||||
stop_phase_occurred = true
|
||||
# --- 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)
|
||||
# --- STATIC SAFE ZONE: Penalize players outside the zone ---
|
||||
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)
|
||||
|
||||
# Refresh power-ups every STOP phase
|
||||
_spawn_powerup_tiles()
|
||||
|
||||
# If GO phase starts, clear all STOP phase freezes and safe zone
|
||||
# If GO phase starts, clear all STOP phase freezes
|
||||
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"):
|
||||
@@ -318,7 +312,21 @@ func _apply_arena_setup():
|
||||
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)
|
||||
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)
|
||||
@@ -339,6 +347,10 @@ func _apply_arena_setup():
|
||||
gridmap.set_cell_item(Vector3i(x, 0, z), tile_id)
|
||||
gridmap.set_cell_item(Vector3i(x, 1, z), -1)
|
||||
|
||||
# Paint Static Safe Zones
|
||||
_paint_static_safe_zone(gridmap, 7, 11, 6, 9)
|
||||
_paint_static_safe_zone(gridmap, 15, 19, 1, 5)
|
||||
|
||||
# Note: Specific obstacles removed as per user request to replace with random ones.
|
||||
# MISSION TILES: Moved to start_game_mode() to ensure they spawn AFTER walls.
|
||||
|
||||
@@ -473,108 +485,38 @@ func check_win_condition(player_id: int, position: Vector2i) -> bool:
|
||||
return false
|
||||
|
||||
# =============================================================================
|
||||
# Dynamic Safe Zone
|
||||
# Static Safe Zone
|
||||
# =============================================================================
|
||||
|
||||
func _spawn_safe_zone():
|
||||
"""Server: Pick a random walkable position and spawn the safe zone."""
|
||||
if not multiplayer.is_server(): return
|
||||
func _paint_static_safe_zone(gridmap: Node, min_x: int, max_x: int, min_z: int, max_z: int):
|
||||
# Paint safe floor
|
||||
for x in range(min_x, max_x + 1):
|
||||
for z in range(min_z, max_z + 1):
|
||||
gridmap.set_cell_item(Vector3i(x, 0, z), TILE_SAFE)
|
||||
|
||||
# Get center opening for horizontal walls
|
||||
var center_x = int(float(min_x + max_x) / 2.0)
|
||||
|
||||
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 (account for wall footprint: radius + 1)
|
||||
var spawn_buffer = SAFE_ZONE_RADIUS + 1
|
||||
var valid_positions: Array[Vector2i] = []
|
||||
for x in range(spawn_buffer, gridmap.columns - spawn_buffer):
|
||||
for z in range(spawn_buffer, gridmap.rows - spawn_buffer):
|
||||
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
|
||||
# Instantiate Top and Bottom horizontal walls
|
||||
for x in range(min_x, max_x + 1):
|
||||
if x == center_x: continue # Opening
|
||||
_instantiate_safe_zone_wall(Vector3(x + 0.5, 0.0, min_z), 0) # Bottom/North
|
||||
_instantiate_safe_zone_wall(Vector3(x + 0.5, 0.0, max_z + 1), 0) # Top/South
|
||||
|
||||
# Rank positions by how "open" they are (more walkable tiles in 5x5 area)
|
||||
valid_positions.sort_custom(func(a, b):
|
||||
return _count_walkable_neighbors(gridmap, a) > _count_walkable_neighbors(gridmap, b)
|
||||
)
|
||||
# Get center opening for vertical walls
|
||||
var center_z = int(float(min_z + max_z) / 2.0)
|
||||
|
||||
# Take the top 20 most open positions to pick from (adds randomness while ensuring good UX)
|
||||
var top_picks = valid_positions.slice(0, min(20, valid_positions.size()))
|
||||
top_picks.shuffle()
|
||||
|
||||
safe_zone_centers = []
|
||||
|
||||
# Pick 1st one
|
||||
var first_center = top_picks.pop_front()
|
||||
safe_zone_centers.append(first_center)
|
||||
|
||||
# Pick 2nd one (Must not overlap 5x5 area)
|
||||
for pos in top_picks:
|
||||
var dx = abs(pos.x - first_center.x)
|
||||
var dz = abs(pos.y - first_center.y)
|
||||
if dx > 5 or dz > 5:
|
||||
safe_zone_centers.append(pos)
|
||||
break
|
||||
|
||||
if safe_zone_centers.size() < 2:
|
||||
# Fallback to shuffled valid_positions if top_picks was too restrictive
|
||||
valid_positions.shuffle()
|
||||
for pos in valid_positions:
|
||||
if safe_zone_centers.size() >= 2: break
|
||||
var dx = abs(pos.x - first_center.x)
|
||||
var dz = abs(pos.y - first_center.y)
|
||||
if dx > 5 or dz > 5:
|
||||
safe_zone_centers.append(pos)
|
||||
|
||||
safe_zone_spawned = true
|
||||
print("[StopNGo] Safe Zones spawned at %s (radius %d)" % [safe_zone_centers, SAFE_ZONE_RADIUS])
|
||||
|
||||
# Sync to all peers
|
||||
if can_rpc():
|
||||
rpc("sync_safe_zone", safe_zone_centers, SAFE_ZONE_RADIUS)
|
||||
|
||||
func _count_walkable_neighbors(gridmap: Node, center: Vector2i) -> int:
|
||||
var count = 0
|
||||
for dx in range(-SAFE_ZONE_RADIUS, SAFE_ZONE_RADIUS + 1):
|
||||
for dz in range(-SAFE_ZONE_RADIUS, SAFE_ZONE_RADIUS + 1):
|
||||
var tile = gridmap.get_cell_item(Vector3i(center.x + dx, 0, center.y + dz))
|
||||
if tile == TILE_WALKABLE:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
func _clear_safe_zone():
|
||||
"""Server: Clear the safe zone overlay and reset state."""
|
||||
if not multiplayer.is_server(): return
|
||||
|
||||
if safe_zone_spawned:
|
||||
# Copy the centers before we reset them so the RPC knows what to clear
|
||||
var centers_to_clear = safe_zone_centers.duplicate()
|
||||
|
||||
# Reset internal state
|
||||
safe_zone_spawned = false
|
||||
safe_zone_centers = []
|
||||
|
||||
if can_rpc():
|
||||
rpc("sync_clear_safe_zone", centers_to_clear)
|
||||
else:
|
||||
sync_clear_safe_zone(centers_to_clear)
|
||||
for z in range(min_z, max_z + 1):
|
||||
if z == center_z: continue # Opening
|
||||
_instantiate_safe_zone_wall(Vector3(min_x, 0.0, z + 0.5), 90) # Left/West
|
||||
_instantiate_safe_zone_wall(Vector3(max_x + 1, 0.0, z + 0.5), 90) # Right/East
|
||||
|
||||
func _is_in_safe_zone(pos: Vector2i) -> bool:
|
||||
"""Check if a position is within ANY of the dynamic safe zones."""
|
||||
if not safe_zone_spawned or safe_zone_centers.is_empty():
|
||||
return false
|
||||
|
||||
for center in safe_zone_centers:
|
||||
var dx = abs(pos.x - center.x)
|
||||
var dz = abs(pos.y - center.y)
|
||||
if dx <= SAFE_ZONE_RADIUS and dz <= SAFE_ZONE_RADIUS:
|
||||
return true
|
||||
"""Check if a position is within ANY of the static safe zones."""
|
||||
# Safe zone 1: [7,6] - [11,9]
|
||||
if pos.x >= 7 and pos.x <= 11 and pos.y >= 6 and pos.y <= 9: return true
|
||||
# Safe zone 2: [15,1] - [19,5]
|
||||
if pos.x >= 15 and pos.x <= 19 and pos.y >= 1 and pos.y <= 5: return true
|
||||
return false
|
||||
|
||||
func _scatter_player_tiles(player_node: Node):
|
||||
@@ -648,92 +590,7 @@ func _scatter_player_tiles(player_node: Node):
|
||||
|
||||
print("[StopNGo] Scattered %d tiles from Player %d" % [tiles_to_scatter.size(), peer_id])
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_safe_zone(centers: Array, radius: int):
|
||||
"""Client: Show the safe zone overlay on the grid."""
|
||||
# GDScript 2.0 type conversion check for RPC arrays
|
||||
safe_zone_centers = []
|
||||
for c in centers:
|
||||
safe_zone_centers.append(Vector2i(c.x, c.y))
|
||||
|
||||
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 zones on Floor 0 (Floor layer)
|
||||
# Also paint walls (ID 16) around the zone with 1 door on each side
|
||||
for center in safe_zone_centers:
|
||||
# 1. Paint the safe floor
|
||||
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:
|
||||
if gridmap.get_cell_item(Vector3i(x, 0, z)) == TILE_WALKABLE:
|
||||
gridmap.set_cell_item(Vector3i(x, 0, z), TILE_SAFE)
|
||||
|
||||
# 2. Instantiate North and South walls (Horizontal)
|
||||
for dx in range(-radius, radius + 1):
|
||||
if dx == 0: continue # Opening
|
||||
_instantiate_safe_zone_wall(Vector3(center.x + dx + 0.5, 0.0, center.y - radius), 0)
|
||||
_instantiate_safe_zone_wall(Vector3(center.x + dx + 0.5, 0.0, center.y + radius + 1), 0)
|
||||
|
||||
# 3. Instantiate East and West walls (Vertical - 4 walls each)
|
||||
# From -2 to 2 (radius), with center opening = 4 walls per side
|
||||
for dz in range(-radius, radius + 1):
|
||||
if dz == 0: continue # Opening
|
||||
_instantiate_safe_zone_wall(Vector3(center.x - radius, 0.0, center.y + dz + 0.5), 90)
|
||||
_instantiate_safe_zone_wall(Vector3(center.x + radius + 1, 0.0, center.y + dz + 0.5), 90)
|
||||
|
||||
# Update pathfinding for bots and movement checks
|
||||
gridmap.initialize_astar()
|
||||
|
||||
# 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(centers_to_clear: Array):
|
||||
"""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 not centers_to_clear.is_empty():
|
||||
for center in centers_to_clear:
|
||||
# Radius 2 (5x5)
|
||||
for dx in range(-SAFE_ZONE_RADIUS, SAFE_ZONE_RADIUS + 1):
|
||||
for dz in range(-SAFE_ZONE_RADIUS, SAFE_ZONE_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:
|
||||
# Restore Floor 0 back to standard walkable floor
|
||||
var current = gridmap.get_cell_item(Vector3i(x, 0, z))
|
||||
if current == TILE_SAFE:
|
||||
gridmap.set_cell_item(Vector3i(x, 0, z), TILE_WALKABLE)
|
||||
|
||||
# Also clean up walls
|
||||
for wall in get_tree().get_nodes_in_group("SafeZoneWalls"):
|
||||
wall.queue_free()
|
||||
|
||||
# Clear local state
|
||||
safe_zone_centers = []
|
||||
safe_zone_spawned = false
|
||||
|
||||
# Restore navigation
|
||||
gridmap.initialize_astar()
|
||||
|
||||
print("[StopNGo] Safe Zones cleared.")
|
||||
# Ensure local state is also updated in case this was just an RPC call
|
||||
safe_zone_centers = []
|
||||
safe_zone_spawned = false
|
||||
# Removed dynamic sync methods.
|
||||
|
||||
func _instantiate_safe_zone_wall(pos: Vector3, rotation_deg: float):
|
||||
if not safe_zone_wall_scene: return
|
||||
|
||||
Reference in New Issue
Block a user