feat: Implement new Stop 'n' Go game mode with dedicated manager, player logic, and control scripts.

This commit is contained in:
2026-03-23 20:42:55 +08:00
parent 75a636c97c
commit 878e331b6e
6 changed files with 225 additions and 87 deletions
+124 -52
View File
@@ -10,10 +10,8 @@ signal player_penalized(player_id: int)
enum Phase {GO, STOP}
# 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_centers: Array[Vector2i] = []
var safe_zone_spawned: bool = false
var active_safe_zone_rects: Array[Rect2i] = []
var spawned_safe_zones: int = 0
# Power-Up Tile Spawning
const POWERUP_TILES = [11, 14] # Speed, Ghost (Freeze and Wall excluded in this mode)
@@ -21,8 +19,7 @@ const POWERUP_SPAWN_COUNT: int = 5 # Number of power-up tiles to spawn
var powerups_spawned: bool = false
var stop_phase_occurred: bool = false
var safe_zone_wall_scene = preload("res://scenes/safe_zone_wall.tscn")
# Safe zone walls removed for fully open dynamic zones
const PERMANENT_POWERUP_LOCATIONS: Array[Vector2i] = [
Vector2i(4, 1), # Power up 1
Vector2i(3, 9), # Power up 2
@@ -101,6 +98,15 @@ func _process(delta):
phase_timer -= delta
if multiplayer.is_server():
if current_phase == Phase.GO:
var int_timer = int(ceil(phase_timer))
if int_timer == 3 and spawned_safe_zones == 0 and phase_timer <= 3.0:
_spawn_dynamic_safe_zone()
elif int_timer == 2 and spawned_safe_zones == 1 and phase_timer <= 2.0:
_spawn_dynamic_safe_zone()
elif int_timer == 1 and spawned_safe_zones == 2 and phase_timer <= 1.0:
_spawn_dynamic_safe_zone()
if phase_timer <= 0:
if current_phase == Phase.GO:
_start_phase(Phase.STOP)
@@ -247,8 +253,9 @@ func _start_phase(phase: Phase):
# Refresh power-ups every STOP phase
_spawn_powerup_tiles()
# If GO phase starts, clear all STOP phase freezes
# If GO phase starts, clear all STOP phase freezes and dynamic safe zones
if phase == Phase.GO:
_clear_dynamic_safe_zones()
var all_players = get_tree().get_nodes_in_group("Players")
for p in all_players:
if p.has_method("sync_stop_freeze"):
@@ -347,9 +354,7 @@ func _apply_arena_setup():
gridmap.set_cell_item(Vector3i(x, 0, z), tile_id)
gridmap.set_cell_item(Vector3i(x, 1, z), -1)
# Paint Static Safe Zones
_paint_static_safe_zone(gridmap, 7, 11, 6, 9, -1, -1, 8, 8)
_paint_static_safe_zone(gridmap, 15, 19, 1, 5, -1, 18, 2, 2)
# Dynamic Safe Zones are procedural and spawn during GO phase
# 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.
@@ -488,40 +493,91 @@ func check_win_condition(player_id: int, position: Vector2i) -> bool:
# Static Safe Zone
# =============================================================================
func _paint_static_safe_zone(gridmap: Node, min_x: int, max_x: int, min_z: int, max_z: int, north_door_x: int = -1, south_door_x: int = -1, west_door_z: int = -1, east_door_z: int = -1):
# Paint safe floor
for x in range(min_x, max_x + 1):
for z in range(min_z, max_z + 1):
gridmap.set_cell_item(Vector3i(x, 0, z), TILE_SAFE)
# Get center opening for horizontal walls
var center_n_x = north_door_x if north_door_x != -1 else int(float(min_x + max_x) / 2.0)
var center_s_x = south_door_x if south_door_x != -1 else int(float(min_x + max_x) / 2.0)
# Instantiate Top and Bottom horizontal walls
for x in range(min_x, max_x + 1):
if x != center_n_x:
_instantiate_safe_zone_wall(Vector3(x + 0.5, 0.0, min_z), 0) # Bottom/North
if x != center_s_x:
_instantiate_safe_zone_wall(Vector3(x + 0.5, 0.0, max_z + 1), 0) # Top/South
# Get center opening for vertical walls
var center_w_z = west_door_z if west_door_z != -1 else int(float(min_z + max_z) / 2.0)
var center_e_z = east_door_z if east_door_z != -1 else int(float(min_z + max_z) / 2.0)
for z in range(min_z, max_z + 1):
if z != center_w_z:
_instantiate_safe_zone_wall(Vector3(min_x, 0.0, z + 0.5), 90) # Left/West
if z != center_e_z:
_instantiate_safe_zone_wall(Vector3(max_x + 1, 0.0, z + 0.5), 90) # Right/East
func _is_in_safe_zone(pos: Vector2i) -> bool:
"""Check if a position is within ANY of the static safe zones."""
# Safe zone 1: [7,6] - [11,9]
if pos.x >= 7 and pos.x <= 11 and pos.y >= 6 and pos.y <= 9: return true
# Safe zone 2: [15,1] - [19,5]
if pos.x >= 15 and pos.x <= 19 and pos.y >= 1 and pos.y <= 5: return true
return false
var gridmap = get_parent().get_node_or_null("EnhancedGridMap")
if not gridmap:
gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
if not gridmap: return false
var floor_tile = gridmap.get_cell_item(Vector3i(pos.x, 0, pos.y))
return floor_tile == TILE_SAFE
func _spawn_dynamic_safe_zone():
if not multiplayer.is_server(): return
var gridmap = get_parent().get_node_or_null("EnhancedGridMap")
if not gridmap: gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
if not gridmap: return
var main = get_node("/root/Main")
var possible_rects = []
# Check all possible 3x2 and 2x3 areas
for x in range(1, gridmap.columns - 3):
for z in range(1, gridmap.rows - 2):
if _is_valid_safe_zone_area(gridmap, x, z, 3, 2):
possible_rects.append(Rect2i(x, z, 3, 2))
if _is_valid_safe_zone_area(gridmap, x, z, 2, 3):
possible_rects.append(Rect2i(x, z, 2, 3))
if possible_rects.size() > 0:
var rect = possible_rects.pick_random()
active_safe_zone_rects.append(rect)
spawned_safe_zones += 1
# Paint floor to TILE_SAFE
for rx in range(rect.size.x):
for rz in range(rect.size.y):
var px = rect.position.x + rx
var pz = rect.position.y + rz
gridmap.set_cell_item(Vector3i(px, 0, pz), TILE_SAFE)
if can_rpc() and main:
main.rpc("sync_grid_item", px, 0, pz, TILE_SAFE)
func _is_valid_safe_zone_area(gridmap: Node, start_x: int, start_z: int, width: int, height: int) -> bool:
# Avoid bounds or start/finish cols
if start_x < 2 or start_x + width > gridmap.columns - 2: return false
if start_z < 1 or start_z + height > gridmap.rows - 1: return false
var test_rect = Rect2i(start_x, start_z, width, height)
for existing in active_safe_zone_rects:
if test_rect.intersects(existing):
return false
for x in range(start_x, start_x + width):
for z in range(start_z, start_z + height):
var floor_0 = gridmap.get_cell_item(Vector3i(x, 0, z))
var floor_1 = gridmap.get_cell_item(Vector3i(x, 1, z))
# Floor must be purely TILE_WALKABLE (0)
if floor_0 != TILE_WALKABLE:
return false
# Floor 1 must be empty (-1) - no items or obstacles
if floor_1 != -1:
return false
return true
func _clear_dynamic_safe_zones():
var gridmap = get_parent().get_node_or_null("EnhancedGridMap")
if not gridmap: gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
if not gridmap: return
var main = get_node_or_null("/root/Main")
for rect in active_safe_zone_rects:
for rx in range(rect.size.x):
for rz in range(rect.size.y):
var px = rect.position.x + rx
var pz = rect.position.y + rz
# Only clear if it is actually still a safe zone
if gridmap.get_cell_item(Vector3i(px, 0, pz)) == TILE_SAFE:
gridmap.set_cell_item(Vector3i(px, 0, pz), TILE_WALKABLE)
if can_rpc() and main:
main.rpc("sync_grid_item", px, 0, pz, TILE_WALKABLE)
active_safe_zone_rects.clear()
spawned_safe_zones = 0
func _scatter_player_tiles(player_node: Node):
"""Server: Take all tiles from player's playerboard and scatter them onto nearby grid cells."""
@@ -595,16 +651,32 @@ func _scatter_player_tiles(player_node: Node):
print("[StopNGo] Scattered %d tiles from Player %d" % [tiles_to_scatter.size(), peer_id])
# Removed dynamic sync methods.
# =============================================================================
# OLD STATIC SAFE ZONE LOGIC (Retained for Reference)
# =============================================================================
# var safe_zone_columns: Array[int] = [5, 10, 15]
func _instantiate_safe_zone_wall(pos: Vector3, rotation_deg: float):
if not safe_zone_wall_scene: return
var wall = safe_zone_wall_scene.instantiate()
add_child(wall)
wall.add_to_group("SafeZoneWalls")
wall.position = pos
wall.rotation_degrees.y = rotation_deg
# func _is_in_safe_zone_old(pos: Vector2i) -> bool:
# return pos.x in safe_zone_columns
# func _paint_static_safe_zones(gridmap):
# for x in safe_zone_columns:
# for z in range(gridmap.rows):
# gridmap.set_cell_item(Vector3i(x, 0, z), TILE_SAFE)
# # Optional: instantiate walls
# # _instantiate_safe_zone_wall(gridmap, x, z)
# func _instantiate_safe_zone_wall(gridmap, x: int, z: int):
# var wall_scene = load("res://scenes/environment/safe_zone_wall.tscn")
# if wall_scene:
# var wall = wall_scene.instantiate()
# gridmap.add_child(wall)
# wall.global_position = Vector3(
# x * gridmap.cell_size.x + gridmap.cell_size.x/2,
# 0,
# z * gridmap.cell_size.z + gridmap.cell_size.z/2
# )
# =============================================================================
# =============================================================================
# Power-Up Tile Spawning (Speed & Ghost)