feat: Implement the main game scene with new player functionality, Stop n Go and Portal Mode managers, a dynamic message bar, and pre-game countdown logic.

This commit is contained in:
2026-03-05 02:07:14 +08:00
parent cd7881bc3f
commit ebfa8f99a7
3 changed files with 318 additions and 46 deletions
+267 -20
View File
@@ -13,6 +13,16 @@ const GO_DURATION: float = 8.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_spawned: bool = false
# Power-Up Tile Spawning
const POWERUP_TILES = [11, 14] # Speed (11) and Ghost (14)
const POWERUP_SPAWN_COUNT: int = 5 # Number of power-up tiles to spawn
var current_phase: Phase = Phase.GO
var phase_timer: float = GO_DURATION
var is_active: bool = false
@@ -82,6 +92,10 @@ func _process(delta):
phase_timer -= delta
if multiplayer.is_server():
# Spawn safe zone 5 seconds before STOP phase begins
if current_phase == Phase.GO and not safe_zone_spawned and phase_timer <= SAFE_ZONE_PRE_TIME:
_spawn_safe_zone()
if phase_timer <= 0:
if current_phase == Phase.GO:
_start_phase(Phase.STOP)
@@ -198,7 +212,6 @@ func activate_client_side():
func start_game_mode():
# This should primarily be called by the Server
# Clients get activated via RPCs (sync_phase, etc)
if multiplayer.is_server():
activate_client_side() # Server also needs local processing
# _setup_arena() # REMOVED: Already explicitly called in main.gd _setup_host_game to prepare floor before spawns!
@@ -217,8 +230,20 @@ func _start_phase(phase: Phase):
if can_rpc():
rpc("sync_phase", phase_name, phase_timer)
# If GO phase starts, clear all STOP phase freezes
if phase == Phase.STOP:
# --- DYNAMIC SAFE ZONE: Penalize players outside the zone ---
if safe_zone_spawned:
var all_players = get_tree().get_nodes_in_group("Players")
for p in all_players:
if not _is_in_safe_zone(p.current_position):
_scatter_player_tiles(p)
# --- POWER-UP TILES: Spawn 5 Speed & Ghost tiles ---
_spawn_powerup_tiles()
# If GO phase starts, clear all STOP phase freezes and safe zone
if phase == Phase.GO:
_clear_safe_zone()
var all_players = get_tree().get_nodes_in_group("Players")
for p in all_players:
if p.has_method("sync_stop_freeze"):
@@ -273,8 +298,8 @@ func _apply_arena_setup():
# Clear existing items on all layers
gridmap.clear()
# Safe Zones Columns: 6, 7, 8 (Only one band now)
var safe_columns = [6, 7, 8]
# Dynamic Safe Zone: No static safe columns anymore
# Safe zone spawns randomly during gameplay
# Create bands based on X (Horizontal Progress)
for x in range(gridmap.columns):
@@ -283,8 +308,6 @@ func _apply_arena_setup():
tile_id = TILE_START
elif x == gridmap.columns - 1:
tile_id = TILE_FINISH
elif x in safe_columns:
tile_id = TILE_SAFE
for z in range(gridmap.rows):
gridmap.set_cell_item(Vector3i(x, 0, z), tile_id)
@@ -306,8 +329,8 @@ func _spawn_mission_tiles():
gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
if not gridmap: return
# Forbidden Zones (Start, Safe, Finish) - No items here
var forbidden_x = [0, 6, 7, 8, 21]
# Forbidden Zones (Start, Finish) - No items here
var forbidden_x = [0, 21]
# Goal items: Heart(7), Diamond(8), Star(9), Coin(10)
var goal_items = [7, 8, 9, 10]
@@ -350,18 +373,16 @@ func sync_missions(missions: Dictionary):
func check_movement_violation(player_id: int, from: Vector2i, to: Vector2i) -> bool:
"""Check if movement is illegal (during STOP phase and not in safe zone)."""
if current_phase == Phase.STOP:
var gridmap = get_parent().get_node_or_null("EnhancedGridMap")
if not gridmap:
gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
if gridmap:
# Check FROM position. If you were safe, you can move?
# Rules: "If a player moves during this phase".
# Usually implies checking if you ARE in a safe zone.
var tile_from = gridmap.get_cell_item(Vector3i(from.x, 0, from.y))
if tile_from != TILE_SAFE and tile_from != TILE_START and tile_from != TILE_FINISH:
_penalize_player(player_id)
return true
# Use dynamic safe zone position check instead of static tile check
if not _is_in_safe_zone(from):
var gridmap = get_parent().get_node_or_null("EnhancedGridMap")
if not gridmap:
gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
if gridmap:
var tile_from = gridmap.get_cell_item(Vector3i(from.x, 0, from.y))
if tile_from != TILE_START and tile_from != TILE_FINISH:
_penalize_player(player_id)
return true
return false
func _penalize_player(player_id: int):
@@ -418,3 +439,229 @@ func check_win_condition(player_id: int, position: Vector2i) -> bool:
print("[StopNGo] Player %d reached finish but goals incomplete." % player_id)
return false
# =============================================================================
# Dynamic Safe Zone
# =============================================================================
func _spawn_safe_zone():
"""Server: Pick a random walkable position and spawn the 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
# Collect valid center positions (not too close to edges so the zone fits)
var valid_positions: Array[Vector2i] = []
for x in range(SAFE_ZONE_RADIUS, gridmap.columns - SAFE_ZONE_RADIUS):
for z in range(SAFE_ZONE_RADIUS, gridmap.rows - SAFE_ZONE_RADIUS):
var tile = gridmap.get_cell_item(Vector3i(x, 0, z))
# Only walkable tiles (not start/finish)
if tile == TILE_WALKABLE:
valid_positions.append(Vector2i(x, z))
if valid_positions.is_empty():
print("[StopNGo] WARNING: No valid position for safe zone!")
return
# 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])
# Sync to all peers
if can_rpc():
rpc("sync_safe_zone", safe_zone_center, SAFE_ZONE_RADIUS)
func _clear_safe_zone():
"""Server: Clear the safe zone overlay and reset state."""
if not multiplayer.is_server(): return
if safe_zone_spawned:
safe_zone_spawned = false
safe_zone_center = Vector2i(-1, -1)
if can_rpc():
rpc("sync_clear_safe_zone")
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):
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
func _scatter_player_tiles(player_node: Node):
"""Server: Take all tiles from player's playerboard and scatter them onto nearby grid cells."""
if not multiplayer.is_server(): return
var main = get_node_or_null("/root/Main")
if not main: return
var gridmap = main.get_node_or_null("EnhancedGridMap")
if not gridmap: return
var peer_id = player_node.name.to_int()
var playerboard = player_node.playerboard
var tiles_to_scatter: Array[int] = []
# Collect all non-empty tiles from playerboard
for i in range(playerboard.size()):
if playerboard[i] != -1:
tiles_to_scatter.append(playerboard[i])
playerboard[i] = -1
if tiles_to_scatter.is_empty():
return # Nothing to scatter
# Find valid nearby positions to drop tiles (within radius 3 of player)
var center = player_node.current_position
var valid_drop_positions: Array[Vector2i] = []
for dx in range(-3, 4):
for dz in range(-3, 4):
var pos = Vector2i(center.x + dx, center.y + dz)
# Bounds check
if pos.x < 0 or pos.x >= gridmap.columns or pos.y < 0 or pos.y >= gridmap.rows:
continue
# Check floor is walkable (not void, not obstacle)
var floor_tile = gridmap.get_cell_item(Vector3i(pos.x, 0, pos.y))
if floor_tile == -1 or floor_tile == TILE_OBSTACLE:
continue
# Check floor 1 is empty (no existing item)
var existing_item = gridmap.get_cell_item(Vector3i(pos.x, 1, pos.y))
if existing_item != -1:
continue
valid_drop_positions.append(pos)
# Scatter tiles onto valid positions
var rng = RandomNumberGenerator.new()
rng.randomize()
for tile in tiles_to_scatter:
if valid_drop_positions.is_empty():
break # No more space
var idx = rng.randi() % valid_drop_positions.size()
var drop_pos = valid_drop_positions[idx]
valid_drop_positions.remove_at(idx)
# Place tile on grid Floor 1
gridmap.set_cell_item(Vector3i(drop_pos.x, 1, drop_pos.y), tile)
# Sync to all clients
main.rpc("sync_grid_item", drop_pos.x, 1, drop_pos.y, tile)
# Sync cleared playerboard to all clients
main.rpc("sync_playerboard", peer_id, playerboard)
# Notify the player
NotificationManager.send_message(player_node, "Not in Safe Zone! Tiles scattered!", NotificationManager.MessageType.WARNING)
# Screen shake
if player_node.has_method("trigger_screen_shake") and can_rpc():
player_node.rpc("trigger_screen_shake", "heavy")
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):
"""Client: Show the safe zone overlay on the grid."""
safe_zone_center = center
safe_zone_spawned = true
var gridmap = get_parent().get_node_or_null("EnhancedGridMap")
if not gridmap:
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)
# Notify local player
var my_id = multiplayer.get_unique_id()
var main = get_node_or_null("/root/Main")
var player_node = main.get_node_or_null(str(my_id)) if main else null
if player_node:
NotificationManager.send_message(player_node, "⚠ Safe Zone spawned! Get inside!", NotificationManager.MessageType.WARNING)
@rpc("authority", "call_local", "reliable")
func sync_clear_safe_zone():
"""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)
safe_zone_center = Vector2i(-1, -1)
safe_zone_spawned = false
# =============================================================================
# Power-Up Tile Spawning (Speed & Ghost)
# =============================================================================
func _spawn_powerup_tiles():
"""Server: Spawn 5 Speed & Ghost power-up tiles at random walkable positions."""
if not multiplayer.is_server(): return
var main = get_node_or_null("/root/Main")
if not main: return
var gridmap = main.get_node_or_null("EnhancedGridMap")
if not gridmap: return
# Collect valid positions (walkable floor, no existing item on Floor 1)
var valid_positions: Array[Vector2i] = []
for x in range(gridmap.columns):
for z in range(gridmap.rows):
var floor_tile = gridmap.get_cell_item(Vector3i(x, 0, z))
# Skip void, obstacles, start, finish
if floor_tile == -1 or floor_tile == TILE_OBSTACLE:
continue
# Skip cells that already have items on Floor 1
var existing_item = gridmap.get_cell_item(Vector3i(x, 1, z))
if existing_item != -1:
continue
valid_positions.append(Vector2i(x, z))
if valid_positions.is_empty():
print("[StopNGo] WARNING: No valid positions for power-up tiles!")
return
# Shuffle and pick up to POWERUP_SPAWN_COUNT positions
var rng = RandomNumberGenerator.new()
rng.randomize()
valid_positions.shuffle()
var spawn_count = min(POWERUP_SPAWN_COUNT, valid_positions.size())
for i in range(spawn_count):
var pos = valid_positions[i]
var tile_id = POWERUP_TILES[rng.randi() % POWERUP_TILES.size()]
# Place on Floor 1
gridmap.set_cell_item(Vector3i(pos.x, 1, pos.y), tile_id)
# Sync to all clients
if can_rpc():
main.rpc("sync_grid_item", pos.x, 1, pos.y, tile_id)
print("[StopNGo] Spawned %d power-up tiles (Speed & Ghost)" % spawn_count)