feat: Add an EnhancedGridMap with advanced generation, randomization, and pathfinding capabilities, and introduce the Stop n Go game mode manager.

This commit is contained in:
Yogi Wiguna
2026-03-05 17:50:21 +08:00
parent 5c4764b082
commit 7d801f1156
2 changed files with 101 additions and 39 deletions
+6 -4
View File
@@ -14,8 +14,8 @@ signal grid_updated
@export var normal_items: Array[int] = [0] @export var normal_items: Array[int] = [0]
@export var non_walkable_items: Array[int] = [4] @export var non_walkable_items: Array[int] = [4]
@export var hover_item: int = 1 @export var hover_item: int = 1
@export var start_item: int = 2 @export var start_item: int = -1
@export var end_item: int = 3 @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) @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 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() clear_path_visualization()
# Always use Layer 2 for these temporary markers # Always use Layer 2 for these temporary markers
set_cell_item(Vector3i(start.x, 2, start.y), start_item) if start_item >= 0:
set_cell_item(Vector3i(end.x, 2, end.y), end_item) 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: for point in path:
if Vector2(point.x, point.y) != start and Vector2(point.x, point.y) != end: 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) set_cell_item(Vector3i(point.x, 2, point.y), hover_item)
+95 -35
View File
@@ -9,14 +9,14 @@ signal player_penalized(player_id: int)
enum Phase {GO, STOP} enum Phase {GO, STOP}
const GO_DURATION: float = 8.0 const GO_DURATION: float = 15.0
const STOP_DURATION: float = 4.0 const STOP_DURATION: float = 4.0
const REQUIRED_GOALS: int = 8 const REQUIRED_GOALS: int = 8
# Dynamic Safe Zone # Dynamic Safe Zone
const SAFE_ZONE_PRE_TIME: float = 5.0 # Seconds before STOP to spawn 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) 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 var safe_zone_spawned: bool = false
# Power-Up Tile Spawning # Power-Up Tile Spawning
@@ -465,38 +465,84 @@ func _spawn_safe_zone():
if valid_positions.is_empty(): if valid_positions.is_empty():
print("[StopNGo] WARNING: No valid position for safe zone!") print("[StopNGo] WARNING: No valid position for safe zone!")
return 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 safe_zone_spawned = true
print("[StopNGo] Safe Zones spawned at %s (radius %d)" % [safe_zone_centers, SAFE_ZONE_RADIUS])
print("[StopNGo] Safe Zone spawned at %s (radius %d)" % [safe_zone_center, SAFE_ZONE_RADIUS])
# Sync to all peers # Sync to all peers
if can_rpc(): 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(): func _clear_safe_zone():
"""Server: Clear the safe zone overlay and reset state.""" """Server: Clear the safe zone overlay and reset state."""
if not multiplayer.is_server(): return if not multiplayer.is_server(): return
if safe_zone_spawned: 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_spawned = false
safe_zone_center = Vector2i(-1, -1) safe_zone_centers = []
if can_rpc(): 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: func _is_in_safe_zone(pos: Vector2i) -> bool:
"""Check if a position is within the dynamic safe zone.""" """Check if a position is within ANY of the dynamic safe zones."""
if not safe_zone_spawned or safe_zone_center == Vector2i(-1, -1): if not safe_zone_spawned or safe_zone_centers.is_empty():
return false return false
# Chebyshev distance (square area)
var dx = abs(pos.x - safe_zone_center.x) for center in safe_zone_centers:
var dz = abs(pos.y - safe_zone_center.y) var dx = abs(pos.x - center.x)
return dx <= SAFE_ZONE_RADIUS and dz <= SAFE_ZONE_RADIUS 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): func _scatter_player_tiles(player_node: Node):
"""Server: Take all tiles from player's playerboard and scatter them onto nearby grid cells.""" """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]) print("[StopNGo] Scattered %d tiles from Player %d" % [tiles_to_scatter.size(), peer_id])
@rpc("authority", "call_local", "reliable") @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.""" """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 safe_zone_spawned = true
var gridmap = get_parent().get_node_or_null("EnhancedGridMap") 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") gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
if not gridmap: return if not gridmap: return
# Paint safe zone on Floor 2 (overlay layer) with TILE_SAFE visual # Paint safe zones on Floor 0 (Floor layer)
for dx in range(-radius, radius + 1): # This ensures items and walls on Floor 1 are visible ON TOP of the safe zone
for dz in range(-radius, radius + 1): for center in safe_zone_centers:
var x = center.x + dx for dx in range(-radius, radius + 1):
var z = center.y + dz for dz in range(-radius, radius + 1):
if x >= 0 and x < gridmap.columns and z >= 0 and z < gridmap.rows: var x = center.x + dx
gridmap.set_cell_item(Vector3i(x, 2, z), TILE_SAFE) 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 # Notify local player
var my_id = multiplayer.get_unique_id() 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) NotificationManager.send_message(player_node, "⚠ Safe Zone spawned! Get inside!", NotificationManager.MessageType.WARNING)
@rpc("authority", "call_local", "reliable") @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.""" """Client: Clear the safe zone overlay."""
var gridmap = get_parent().get_node_or_null("EnhancedGridMap") var gridmap = get_parent().get_node_or_null("EnhancedGridMap")
if not gridmap: if not gridmap:
gridmap = get_node_or_null("/root/Main/EnhancedGridMap") gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
if not gridmap: return if not gridmap: return
if safe_zone_center != Vector2i(-1, -1): if not centers_to_clear.is_empty():
for dx in range(-SAFE_ZONE_RADIUS, SAFE_ZONE_RADIUS + 1): for center in centers_to_clear:
for dz in range(-SAFE_ZONE_RADIUS, SAFE_ZONE_RADIUS + 1): # Radius 2 (5x5)
var x = safe_zone_center.x + dx for dx in range(-SAFE_ZONE_RADIUS, SAFE_ZONE_RADIUS + 1):
var z = safe_zone_center.y + dz for dz in range(-SAFE_ZONE_RADIUS, SAFE_ZONE_RADIUS + 1):
if x >= 0 and x < gridmap.columns and z >= 0 and z < gridmap.rows: var x = center.x + dx
gridmap.set_cell_item(Vector3i(x, 2, z), -1) 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 safe_zone_spawned = false
# ============================================================================= # =============================================================================