feat: implement obstacle manager for randomized static wall spawning, integrate into main scene, and add stop-n-go manager.

This commit is contained in:
Yogi Wiguna
2026-02-25 10:54:55 +08:00
parent e31973dfab
commit 040e6e53ce
3 changed files with 166 additions and 40 deletions
+16
View File
@@ -11,6 +11,7 @@ var screen_shake_manager
var touch_controls
var camera_context_manager
var stop_n_go_manager
var obstacle_manager
# Minimal local state
var _connection_check_timer: float = 0.0
@@ -117,6 +118,12 @@ func _init_managers():
add_child(camera_context_manager)
camera_context_manager.initialize($Camera3D200, screen_shake_manager)
# Obstacle manager for dynamic walls
obstacle_manager = load("res://scripts/managers/obstacle_manager.gd").new()
obstacle_manager.name = "ObstacleManager"
add_child(obstacle_manager)
obstacle_manager.initialize(self, $EnhancedGridMap)
# Connect signals for UI updates
goals_cycle_manager.timer_updated.connect(_on_timer_updated)
goals_cycle_manager.score_updated.connect(_on_score_updated)
@@ -591,6 +598,14 @@ func _start_game():
_assign_random_spawn_positions()
# PRE-GAME COUNTDOWN (3s)
# Spawn static obstacles before countdown starts
if obstacle_manager:
obstacle_manager.spawn_random_obstacles(15)
# Spawn mission tiles BEFORE countdown but AFTER walls (Stop n Go only)
if LobbyManager.game_mode == "Stop n Go" and stop_n_go_manager:
stop_n_go_manager.setup_mission_tiles()
await _start_pre_game_countdown()
GameStateManager.start_game()
@@ -624,6 +639,7 @@ func _start_game():
# Spawn Tekton NPC
spawn_tekton_npc()
func _assign_random_spawn_positions():
"""Assign spawn positions distributed to 4 corners (2 per corner for 8 players)."""
var enhanced_gridmap = $EnhancedGridMap
+146
View File
@@ -0,0 +1,146 @@
extends Node
class_name ObstacleManager
# ObstacleManager - Handles static spawning of walls/blocks on the arena
const TILE_OBSTACLE = 4 # Wall
const TILE_GROUND = 0 # Standard Floor
var main: Node
var gridmap: Node
func initialize(p_main: Node, p_gridmap: Node):
main = p_main
gridmap = p_gridmap
print("[ObstacleManager] Initialized")
func spawn_random_obstacles(count: int = 15):
if not multiplayer.is_server(): return
print("[ObstacleManager] Attempting to spawn %d obstacles (Guaranteed types first)" % count)
var shapes = _get_all_shapes()
var successful_spawns = 0
# 1. First, try to spawn each shape type at least once
for shape_idx in range(shapes.size()):
var shape_attempts = 0
var shape_placed = false
while not shape_placed and shape_attempts < 20: # Give each type a fair chance
shape_attempts += 1
if spawn_random_wall(shape_idx):
shape_placed = true
successful_spawns += 1
if successful_spawns >= count: break
# 2. Then, fill the remaining count with random shapes
var total_attempts = 0
var max_attempts = count * 5
while successful_spawns < count and total_attempts < max_attempts:
total_attempts += 1
if spawn_random_wall():
successful_spawns += 1
print("[ObstacleManager] Final: Spawned %d obstacles" % successful_spawns)
# Force AStar update after all spawns
if gridmap and gridmap.has_method("initialize_astar"):
gridmap.initialize_astar()
func _get_all_shapes() -> Array:
return [
# L (3 blocks) - 4 Rotations
[Vector2i(0, 0), Vector2i(1, 0), Vector2i(0, 1)], # L - Corner Bottom Left
[Vector2i(0, 0), Vector2i(-1, 0), Vector2i(0, 1)], # L - Corner Bottom Right
[Vector2i(0, 0), Vector2i(1, 0), Vector2i(0, -1)], # L - Corner Top Left
[Vector2i(0, 0), Vector2i(-1, 0), Vector2i(0, -1)], # L - Corner Top Right
# 2 Vertical
[Vector2i(0, 0), Vector2i(0, 1)],
[Vector2i(0, 0), Vector2i(0, -1)],
# 2 Horizontal
[Vector2i(0, 0), Vector2i(1, 0)],
[Vector2i(0, 0), Vector2i(-1, 0)],
# Single
[Vector2i(0, 0)]
]
func spawn_random_wall(forced_shape_idx: int = -1) -> bool:
if not gridmap or not main: return false
var cols = gridmap.get("columns") if "columns" in gridmap else 14
var rows = gridmap.get("rows") if "rows" in gridmap else 14
# User Request: "spawn on columns 3 and so on"
if cols <= 3: return false
var x = randi_range(3, cols - 1)
var z = randi_range(0, rows - 1)
var shapes = _get_all_shapes()
var shape_idx = forced_shape_idx if forced_shape_idx != -1 else randi() % shapes.size()
var shape = shapes[shape_idx]
var wall_positions: Array[Vector3i] = []
# Validate position and check for ground
for offset in shape:
var target_x = x + offset.x
var target_z = z + offset.y
# Bounds check
if target_x >= 3 and target_x < cols and target_z >= 0 and target_z < rows:
var current_item = gridmap.get_cell_item(Vector3i(target_x, 0, target_z))
# Only spawn on ground (0) to avoid replacing other obstacles or special tiles
if current_item == TILE_GROUND:
# PLAYER CHECK: Ensure no player is on this tile
var is_occupied = false
for player in get_tree().get_nodes_in_group("Players"):
if player.get("current_position") == Vector2i(target_x, target_z):
is_occupied = true
break
if is_occupied: return false
# ADJACENCY CHECK: Ensure no existing walls are nearby
if not _is_position_isolated(target_x, target_z, shape, x, z):
return false
wall_positions.append(Vector3i(target_x, 0, target_z))
else:
# If any block of the shape hits a non-ground tile, fail the whole shape for clean placement
return false
else:
return false
# Only proceed if we can place the full shape
if wall_positions.is_empty():
return false
# Create walls on all clients (Permanent, no despawn)
for pos in wall_positions:
main.rpc("sync_grid_item", pos.x, pos.y, pos.z, TILE_OBSTACLE)
return true
func _is_position_isolated(target_x: int, target_z: int, shape_offsets: Array, origin_x: int, origin_z: int) -> bool:
# Check 3x3 area
for dx in range(-1, 2):
for dz in range(-1, 2):
if dx == 0 and dz == 0: continue
var nx = target_x + dx
var nz = target_z + dz
# Check if this neighbor is part of the shape we are currently placing
var is_part_of_shape = false
for offset in shape_offsets:
if nx == origin_x + offset.x and nz == origin_z + offset.y:
is_part_of_shape = true
break
if is_part_of_shape: continue
# Check if gridmap has a wall at this neighbor
if gridmap.get_cell_item(Vector3i(nx, 0, nz)) == TILE_OBSTACLE:
return false
return true
+4 -40
View File
@@ -286,52 +286,16 @@ func _apply_arena_setup():
for z in range(gridmap.rows):
gridmap.set_cell_item(Vector3i(x, 0, z), tile_id)
# --- SPECIFIC OBSTACLES (Black Walls) ---
# Left Obstacles (Column 4)
# Top Vertical Bar
for z in range(0, 4): # z=0, 1, 2, 3
gridmap.set_cell_item(Vector3i(4, 0, z), TILE_OBSTACLE)
# Bottom Vertical Bar
for z in range(6, 10): # z=6, 7, 8, 9
gridmap.set_cell_item(Vector3i(4, 0, z), TILE_OBSTACLE)
# Center Obstacles (Column 11 area)
# Top Middle Vertical Bar (Offset slightly down)
for z in range(1, 5): # z=1, 2, 3, 4
gridmap.set_cell_item(Vector3i(11, 0, z), TILE_OBSTACLE)
# Bottom Middle L-Shape (Vertical + Horizontal hook)
for z in range(6, 9): # z=6, 7, 8 (Vertical part)
gridmap.set_cell_item(Vector3i(11, 0, z), TILE_OBSTACLE)
# Horizontal part of L (at z=6 to right?) - Image looks like inverted L or T?
# Let's assume right hook at top of bottom part
gridmap.set_cell_item(Vector3i(12, 0, 6), TILE_OBSTACLE)
# Right Obstacles (Column 18 area)
# Top Right L-Shape (Horizontal hook + Vertical down)
# Vertical
for z in range(0, 3): # z=0, 1, 2
gridmap.set_cell_item(Vector3i(18, 0, z), TILE_OBSTACLE)
# Horizontal hook to right
gridmap.set_cell_item(Vector3i(19, 0, 2), TILE_OBSTACLE)
gridmap.set_cell_item(Vector3i(20, 0, 2), TILE_OBSTACLE)
# Bottom Right Vertical Bar
for z in range(5, 9): # z=5, 6, 7, 8
gridmap.set_cell_item(Vector3i(18, 0, z), TILE_OBSTACLE)
# 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.
gridmap.update_grid_data()
gridmap.initialize_astar()
# Spawn tiles for missions
func setup_mission_tiles():
"""Public wrapper to trigger mission tile spawning before game start."""
if multiplayer.is_server():
_spawn_mission_tiles()
# Client already constructs the base arena locally via _apply_arena_setup()
# So no need to blast huge 5KB arrays across the network.
# For any specifically spawned tiles (like missions), they are sent individually
# by sync_grid_item inside _spawn_mission_tiles.
func _spawn_mission_tiles():
var gridmap = get_parent().get_node_or_null("EnhancedGridMap")