feat: implement new 'Stop N Go' arena scene and manager.

This commit is contained in:
Yogi Wiguna
2026-03-13 11:44:54 +08:00
parent 877a238a82
commit 84ae27c96a
5 changed files with 1083 additions and 217 deletions
+56 -199
View File
@@ -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