diff --git a/scenes/main.gd b/scenes/main.gd index 1725614..d774206 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -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() @@ -623,6 +638,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).""" diff --git a/scripts/managers/obstacle_manager.gd b/scripts/managers/obstacle_manager.gd new file mode 100644 index 0000000..432e18c --- /dev/null +++ b/scripts/managers/obstacle_manager.gd @@ -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 diff --git a/scripts/managers/stop_n_go_manager.gd b/scripts/managers/stop_n_go_manager.gd index 691a315..5d15579 100644 --- a/scripts/managers/stop_n_go_manager.gd +++ b/scripts/managers/stop_n_go_manager.gd @@ -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) --- + # 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. - # 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) - 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")