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:
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user