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
Binary file not shown.
File diff suppressed because one or more lines are too long
+20 -9
View File
@@ -92,8 +92,6 @@ func _setup_multiplayer_spawners():
func _apply_arena_background(): func _apply_arena_background():
var arena_bg = get_node_or_null("ArenaBG") var arena_bg = get_node_or_null("ArenaBG")
if not arena_bg:
return
var selected_area = LobbyManager.get_selected_area() var selected_area = LobbyManager.get_selected_area()
var texture_path = "" var texture_path = ""
@@ -103,17 +101,29 @@ func _apply_arena_background():
texture_path = "res://assets/graphics/level_bg/level_bg_colloseum.jpg" texture_path = "res://assets/graphics/level_bg/level_bg_colloseum.jpg"
"Stop N Go Arena": "Stop N Go Arena":
texture_path = "res://assets/graphics/level_bg/placeholder_stop_n_go.jpg" texture_path = "res://assets/graphics/level_bg/placeholder_stop_n_go.jpg"
_instantiate_3d_arena("res://scenes/arena/stop_n_go.scn")
"Tekton Doors Arena": "Tekton Doors Arena":
texture_path = "res://assets/graphics/level_bg/placeholder_tekton_doors.jpg" texture_path = "res://assets/graphics/level_bg/placeholder_tekton_doors.jpg"
"Classic", _: "Classic", _:
texture_path = "res://assets/graphics/level_bg/placeholder_classic.jpg" texture_path = "res://assets/graphics/level_bg/placeholder_classic.jpg"
if ResourceLoader.exists(texture_path): if arena_bg and texture_path != "":
var tex = load(texture_path) if ResourceLoader.exists(texture_path):
if tex: var tex = load(texture_path)
arena_bg.texture = tex if tex:
else: arena_bg.texture = tex
print("Arena bg texture not found: ", texture_path) else:
print("Arena bg texture not found: ", texture_path)
func _instantiate_3d_arena(scene_path: String):
if ResourceLoader.exists(scene_path):
var arena_scene = load(scene_path)
if arena_scene:
var arena_instance = arena_scene.instantiate()
arena_instance.name = "ArenaEnvironment3D"
add_child(arena_instance)
move_child(arena_instance, 0)
print("Instantiated 3D Arena: ", scene_path)
@rpc("any_peer", "call_local", "reliable") @rpc("any_peer", "call_local", "reliable")
func sync_portal_configs(configs: Array): func sync_portal_configs(configs: Array):
@@ -665,7 +675,8 @@ func _start_game():
# PRE-GAME COUNTDOWN (3s) # PRE-GAME COUNTDOWN (3s)
# Spawn static obstacles before countdown starts (Stop n Go only) # Spawn static obstacles before countdown starts (Stop n Go only)
if obstacle_manager and LobbyManager.game_mode == "Stop n Go": if obstacle_manager and LobbyManager.game_mode == "Stop n Go":
obstacle_manager.spawn_random_obstacles(15) # obstacle_manager.spawn_random_obstacles(15) # Disabled: Using fixed obstacles now
pass
# Spawn mission and power-up tiles BEFORE countdown but AFTER walls (Stop n Go only) # Spawn mission and power-up tiles BEFORE countdown but AFTER walls (Stop n Go only)
if LobbyManager.game_mode == "Stop n Go" and stop_n_go_manager: if LobbyManager.game_mode == "Stop n Go" and stop_n_go_manager:
+3 -9
View File
File diff suppressed because one or more lines are too long
+55 -198
View File
@@ -24,11 +24,11 @@ var stop_phase_occurred: bool = false
var safe_zone_wall_scene = preload("res://scenes/wall_3d.tscn") var safe_zone_wall_scene = preload("res://scenes/wall_3d.tscn")
const PERMANENT_POWERUP_LOCATIONS: Array[Vector2i] = [ const PERMANENT_POWERUP_LOCATIONS: Array[Vector2i] = [
Vector2i(4, 3), # Area 1 Vector2i(4, 1), # Power up 1
Vector2i(8, 7), # Area 2 Vector2i(3, 9), # Power up 2
Vector2i(11, 4), # Area 3 Vector2i(9, 6), # Power up 3
Vector2i(15, 8), # Area 4 Vector2i(16, 2), # Power up 4
Vector2i(18, 5) # Area 5 Vector2i(18, 8) # Power up 5
] ]
var current_phase: Phase = Phase.GO var current_phase: Phase = Phase.GO
@@ -101,10 +101,6 @@ func _process(delta):
phase_timer -= delta phase_timer -= delta
if multiplayer.is_server(): 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 phase_timer <= 0:
if current_phase == Phase.GO: if current_phase == Phase.GO:
_start_phase(Phase.STOP) _start_phase(Phase.STOP)
@@ -242,19 +238,17 @@ func _start_phase(phase: Phase):
if phase == Phase.STOP: if phase == Phase.STOP:
stop_phase_occurred = true stop_phase_occurred = true
# --- DYNAMIC SAFE ZONE: Penalize players outside the zone --- # --- STATIC SAFE ZONE: Penalize players outside the zone ---
if safe_zone_spawned: var all_players = get_tree().get_nodes_in_group("Players")
var all_players = get_tree().get_nodes_in_group("Players") for p in all_players:
for p in all_players: if not _is_in_safe_zone(p.current_position):
if not _is_in_safe_zone(p.current_position): _scatter_player_tiles(p)
_scatter_player_tiles(p)
# Refresh power-ups every STOP phase # Refresh power-ups every STOP phase
_spawn_powerup_tiles() _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: if phase == Phase.GO:
_clear_safe_zone()
var all_players = get_tree().get_nodes_in_group("Players") var all_players = get_tree().get_nodes_in_group("Players")
for p in all_players: for p in all_players:
if p.has_method("sync_stop_freeze"): 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(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(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(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) # 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, 0, z), tile_id)
gridmap.set_cell_item(Vector3i(x, 1, z), -1) 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. # 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. # 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 return false
# ============================================================================= # =============================================================================
# Dynamic Safe Zone # Static Safe Zone
# ============================================================================= # =============================================================================
func _spawn_safe_zone(): func _paint_static_safe_zone(gridmap: Node, min_x: int, max_x: int, min_z: int, max_z: int):
"""Server: Pick a random walkable position and spawn the safe zone.""" # Paint safe floor
if not multiplayer.is_server(): return 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)
var gridmap = get_parent().get_node_or_null("EnhancedGridMap") # Get center opening for horizontal walls
if not gridmap: var center_x = int(float(min_x + max_x) / 2.0)
gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
if not gridmap: return
# Collect valid center positions (account for wall footprint: radius + 1) # Instantiate Top and Bottom horizontal walls
var spawn_buffer = SAFE_ZONE_RADIUS + 1 for x in range(min_x, max_x + 1):
var valid_positions: Array[Vector2i] = [] if x == center_x: continue # Opening
for x in range(spawn_buffer, gridmap.columns - spawn_buffer): _instantiate_safe_zone_wall(Vector3(x + 0.5, 0.0, min_z), 0) # Bottom/North
for z in range(spawn_buffer, gridmap.rows - spawn_buffer): _instantiate_safe_zone_wall(Vector3(x + 0.5, 0.0, max_z + 1), 0) # Top/South
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(): # Get center opening for vertical walls
print("[StopNGo] WARNING: No valid position for safe zone!") var center_z = int(float(min_z + max_z) / 2.0)
return
# Rank positions by how "open" they are (more walkable tiles in 5x5 area) for z in range(min_z, max_z + 1):
valid_positions.sort_custom(func(a, b): if z == center_z: continue # Opening
return _count_walkable_neighbors(gridmap, a) > _count_walkable_neighbors(gridmap, b) _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
# 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)
func _is_in_safe_zone(pos: Vector2i) -> bool: func _is_in_safe_zone(pos: Vector2i) -> bool:
"""Check if a position is within ANY of the dynamic safe zones.""" """Check if a position is within ANY of the static safe zones."""
if not safe_zone_spawned or safe_zone_centers.is_empty(): # Safe zone 1: [7,6] - [11,9]
return false 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]
for center in safe_zone_centers: if pos.x >= 15 and pos.x <= 19 and pos.y >= 1 and pos.y <= 5: return true
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
return false return false
func _scatter_player_tiles(player_node: Node): 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]) print("[StopNGo] Scattered %d tiles from Player %d" % [tiles_to_scatter.size(), peer_id])
@rpc("authority", "call_local", "reliable") # Removed dynamic sync methods.
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
func _instantiate_safe_zone_wall(pos: Vector3, rotation_deg: float): func _instantiate_safe_zone_wall(pos: Vector3, rotation_deg: float):
if not safe_zone_wall_scene: return if not safe_zone_wall_scene: return