From 7d801f115686176b5cb171dad2608d3182a87fd8 Mon Sep 17 00:00:00 2001 From: Yogi Wiguna Date: Thu, 5 Mar 2026 17:50:21 +0800 Subject: [PATCH] feat: Add an `EnhancedGridMap` with advanced generation, randomization, and pathfinding capabilities, and introduce the `Stop n Go` game mode manager. --- addons/enhanced_gridmap/enhanced_gridmap.gd | 10 +- scripts/managers/stop_n_go_manager.gd | 130 ++++++++++++++------ 2 files changed, 101 insertions(+), 39 deletions(-) diff --git a/addons/enhanced_gridmap/enhanced_gridmap.gd b/addons/enhanced_gridmap/enhanced_gridmap.gd index 894130f..85c7da4 100644 --- a/addons/enhanced_gridmap/enhanced_gridmap.gd +++ b/addons/enhanced_gridmap/enhanced_gridmap.gd @@ -14,8 +14,8 @@ signal grid_updated @export var normal_items: Array[int] = [0] @export var non_walkable_items: Array[int] = [4] @export var hover_item: int = 1 -@export var start_item: int = 2 -@export var end_item: int = 3 +@export var start_item: int = -1 +@export var end_item: int = -1 @export var immutable_items: Array[int] = [1, 2, 3, 4] # Items that cannot be randomized/reset (Start, Safe, Finish, Wall) var current_mesh_library: MeshLibrary @@ -583,8 +583,10 @@ func find_path(start: Vector2, end: Vector2, floor_index: int = 0, clear_path_vi clear_path_visualization() # Always use Layer 2 for these temporary markers - set_cell_item(Vector3i(start.x, 2, start.y), start_item) - set_cell_item(Vector3i(end.x, 2, end.y), end_item) + if start_item >= 0: + set_cell_item(Vector3i(start.x, 2, start.y), start_item) + if end_item >= 0: + set_cell_item(Vector3i(end.x, 2, end.y), end_item) for point in path: if Vector2(point.x, point.y) != start and Vector2(point.x, point.y) != end: set_cell_item(Vector3i(point.x, 2, point.y), hover_item) diff --git a/scripts/managers/stop_n_go_manager.gd b/scripts/managers/stop_n_go_manager.gd index d8affc2..95fb87d 100644 --- a/scripts/managers/stop_n_go_manager.gd +++ b/scripts/managers/stop_n_go_manager.gd @@ -9,14 +9,14 @@ signal player_penalized(player_id: int) enum Phase {GO, STOP} -const GO_DURATION: float = 8.0 +const GO_DURATION: float = 15.0 const STOP_DURATION: float = 4.0 const REQUIRED_GOALS: int = 8 # Dynamic Safe Zone const SAFE_ZONE_PRE_TIME: float = 5.0 # Seconds before STOP to spawn safe zone const SAFE_ZONE_RADIUS: int = 2 # 5x5 area (radius 2 from center) -var safe_zone_center: Vector2i = Vector2i(-1, -1) +var safe_zone_centers: Array[Vector2i] = [] var safe_zone_spawned: bool = false # Power-Up Tile Spawning @@ -465,38 +465,84 @@ func _spawn_safe_zone(): if valid_positions.is_empty(): print("[StopNGo] WARNING: No valid position for safe zone!") return + + # 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) + ) + + # 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) - # Pick random center - var rng = RandomNumberGenerator.new() - rng.randomize() - safe_zone_center = valid_positions[rng.randi() % valid_positions.size()] safe_zone_spawned = true - - print("[StopNGo] Safe Zone spawned at %s (radius %d)" % [safe_zone_center, SAFE_ZONE_RADIUS]) + 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_center, SAFE_ZONE_RADIUS) + 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_center = Vector2i(-1, -1) + safe_zone_centers = [] if can_rpc(): - rpc("sync_clear_safe_zone") + rpc("sync_clear_safe_zone", centers_to_clear) + else: + sync_clear_safe_zone(centers_to_clear) func _is_in_safe_zone(pos: Vector2i) -> bool: - """Check if a position is within the dynamic safe zone.""" - if not safe_zone_spawned or safe_zone_center == Vector2i(-1, -1): + """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 - # Chebyshev distance (square area) - var dx = abs(pos.x - safe_zone_center.x) - var dz = abs(pos.y - safe_zone_center.y) - return dx <= SAFE_ZONE_RADIUS and dz <= SAFE_ZONE_RADIUS + + 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 + return false func _scatter_player_tiles(player_node: Node): """Server: Take all tiles from player's playerboard and scatter them onto nearby grid cells.""" @@ -570,9 +616,13 @@ 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(center: Vector2i, radius: int): +func sync_safe_zone(centers: Array, radius: int): """Client: Show the safe zone overlay on the grid.""" - safe_zone_center = center + # 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") @@ -580,13 +630,17 @@ func sync_safe_zone(center: Vector2i, radius: int): gridmap = get_node_or_null("/root/Main/EnhancedGridMap") if not gridmap: return - # Paint safe zone on Floor 2 (overlay layer) with TILE_SAFE visual - 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: - gridmap.set_cell_item(Vector3i(x, 2, z), TILE_SAFE) + # Paint safe zones on Floor 0 (Floor layer) + # This ensures items and walls on Floor 1 are visible ON TOP of the safe zone + for center in safe_zone_centers: + 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: + # ONLY replace standard walkable floor (don't overwrite Start/Finish/Walls on Layer 0) + if gridmap.get_cell_item(Vector3i(x, 0, z)) == TILE_WALKABLE: + gridmap.set_cell_item(Vector3i(x, 0, z), TILE_SAFE) # Notify local player var my_id = multiplayer.get_unique_id() @@ -596,22 +650,28 @@ func sync_safe_zone(center: Vector2i, radius: int): NotificationManager.send_message(player_node, "⚠ Safe Zone spawned! Get inside!", NotificationManager.MessageType.WARNING) @rpc("authority", "call_local", "reliable") -func sync_clear_safe_zone(): +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 safe_zone_center != Vector2i(-1, -1): - for dx in range(-SAFE_ZONE_RADIUS, SAFE_ZONE_RADIUS + 1): - for dz in range(-SAFE_ZONE_RADIUS, SAFE_ZONE_RADIUS + 1): - var x = safe_zone_center.x + dx - var z = safe_zone_center.y + dz - if x >= 0 and x < gridmap.columns and z >= 0 and z < gridmap.rows: - gridmap.set_cell_item(Vector3i(x, 2, z), -1) + 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 + if gridmap.get_cell_item(Vector3i(x, 0, z)) == TILE_SAFE: + gridmap.set_cell_item(Vector3i(x, 0, z), TILE_WALKABLE) - safe_zone_center = Vector2i(-1, -1) + 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 # =============================================================================