feat: Implement Stop n Go game mode with phase management, missions, dynamic safe zones, and associated UI and managers.

This commit is contained in:
Yogi Wiguna
2026-03-06 16:01:12 +08:00
parent edd5051106
commit 0fb1397d11
8 changed files with 106 additions and 71 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2 -1
View File
@@ -633,9 +633,10 @@ func _start_game():
if obstacle_manager and LobbyManager.game_mode == "Stop n Go":
obstacle_manager.spawn_random_obstacles(15)
# Spawn mission tiles BEFORE countdown but AFTER walls (Stop n Go only)
# Spawn mission and power-up tiles BEFORE countdown but AFTER walls (Stop n Go only)
if LobbyManager.game_mode == "Stop n Go" and stop_n_go_manager:
stop_n_go_manager.setup_mission_tiles()
stop_n_go_manager.spawn_initial_powerups() # Ensure power-ups exist before 1,2,3 Go
# Spawn Static Tektons BEFORE countdown (Free Mode Only)
# Exclude for Stop n Go and Tekton Doors
+22 -11
View File
@@ -84,18 +84,29 @@ func handle_unhandled_input(event):
# --- Keyboard Shortcuts (Event-based) ---
if event is InputEventKey and event.pressed and not event.echo:
var mode = LobbyManager.get_game_mode()
var is_sng = mode == GameMode.Mode.STOP_N_GO
match event.keycode:
KEY_KP_1, KEY_1, KEY_KP_2, KEY_2, KEY_KP_3, KEY_3, KEY_KP_4, KEY_4:
# Unified Mapping for all modes: 1-Speed, 2-Wall, 3-Freeze, 4-Ghost
match event.keycode:
KEY_KP_1, KEY_1:
player.activate_powerup(0) # FASTER_SPEED
KEY_KP_2, KEY_2:
player.activate_powerup(2) # BLOCK_FLOOR
KEY_KP_3, KEY_3:
player.activate_powerup(1) # AREA_FREEZE
KEY_KP_4, KEY_4:
player.activate_powerup(3) # INVISIBLE_MODE
if is_sng:
# Stop n Go Mapping: 1-Speed, 2-Ghost
match event.keycode:
KEY_KP_1, KEY_1:
player.activate_powerup(0) # FASTER_SPEED
KEY_KP_2, KEY_2:
player.activate_powerup(3) # INVISIBLE_MODE
else:
# Unified Mapping for all modes: 1-Speed, 2-Wall, 3-Freeze, 4-Ghost
match event.keycode:
KEY_KP_1, KEY_1:
player.activate_powerup(0) # FASTER_SPEED
KEY_KP_2, KEY_2:
player.activate_powerup(2) # BLOCK_FLOOR
KEY_KP_3, KEY_3:
player.activate_powerup(1) # AREA_FREEZE
KEY_KP_4, KEY_4:
player.activate_powerup(3) # INVISIBLE_MODE
# KEY_R:
# player.auto_put_item()
KEY_Q:
@@ -117,7 +128,7 @@ func handle_unhandled_input(event):
KEY_G:
if not player.is_carrying_tekton and player.powerup_manager:
if player.powerup_manager.can_use_special():
player.grab_tekton()
player.grab_tekton()
# Handle spawn point selection if not yet selected
+10 -4
View File
@@ -307,8 +307,11 @@ func bot_try_grab_item() -> bool:
special_tiles_manager.add_powerup_from_item(item)
player.playerboard[empty_slot] = item
player.rpc("sync_grid_item", current_cell.x, current_cell.y, current_cell.z, -1)
player.rpc("sync_playerboard", player.playerboard)
var main = player.get_tree().get_root().get_node_or_null("Main")
if main:
main.rpc("sync_grid_item", current_cell.x, current_cell.y, current_cell.z, -1)
main.rpc("sync_playerboard", player.name.to_int(), player.playerboard)
player.has_performed_action = true
player.action_points -= 1
_check_goal_completion()
@@ -331,8 +334,11 @@ func bot_try_grab_item() -> bool:
if empty_slot != -1:
if player.is_multiplayer_authority():
player.playerboard[empty_slot] = item
player.rpc("sync_grid_item", cell.x, cell.y, cell.z, -1)
player.rpc("sync_playerboard", player.playerboard)
var main = player.get_tree().get_root().get_node_or_null("Main")
if main:
main.rpc("sync_grid_item", cell.x, cell.y, cell.z, -1)
main.rpc("sync_playerboard", player.name.to_int(), player.playerboard)
player.has_performed_action = true
player.action_points -= 1
return true
+32 -32
View File
@@ -22,6 +22,16 @@ 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 powerups_spawned: bool = false
var stop_phase_occurred: bool = false
const PERMANENT_POWERUP_LOCATIONS: Array[Vector2i] = [
Vector2i(4, 3), # Area 1
Vector2i(8, 7), # Area 2
Vector2i(11, 4), # Area 3
Vector2i(15, 8), # Area 4
Vector2i(18, 5) # Area 5
]
var current_phase: Phase = Phase.GO
var phase_timer: float = GO_DURATION
@@ -231,6 +241,7 @@ func _start_phase(phase: Phase):
rpc("sync_phase", phase_name, phase_timer)
if phase == Phase.STOP:
stop_phase_occurred = true
# --- DYNAMIC SAFE ZONE: Penalize players outside the zone ---
if safe_zone_spawned:
var all_players = get_tree().get_nodes_in_group("Players")
@@ -238,7 +249,7 @@ func _start_phase(phase: Phase):
if not _is_in_safe_zone(p.current_position):
_scatter_player_tiles(p)
# --- POWER-UP TILES: Spawn 5 Speed & Ghost tiles ---
# Refresh power-ups every STOP phase
_spawn_powerup_tiles()
# If GO phase starts, clear all STOP phase freezes and safe zone
@@ -323,6 +334,11 @@ func setup_mission_tiles():
if multiplayer.is_server():
_spawn_mission_tiles()
func spawn_initial_powerups():
"""Public wrapper to spawn powerups before game start."""
if multiplayer.is_server():
_spawn_powerup_tiles()
func _spawn_mission_tiles():
var gridmap = get_parent().get_node_or_null("EnhancedGridMap")
if not gridmap:
@@ -679,7 +695,7 @@ func sync_clear_safe_zone(centers_to_clear: Array):
# =============================================================================
func _spawn_powerup_tiles():
"""Server: Spawn 5 Speed & Ghost power-up tiles at random walkable positions."""
"""Server: Spawn 5 permanent power-up tiles at fixed positions."""
if not multiplayer.is_server(): return
var main = get_node_or_null("/root/Main")
@@ -688,40 +704,24 @@ func _spawn_powerup_tiles():
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))
print("[StopNGo] Spawning/Refreshing 5 static power-up tiles...")
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()]
for i in range(PERMANENT_POWERUP_LOCATIONS.size()):
var pos = PERMANENT_POWERUP_LOCATIONS[i]
# Set Floor 0 beneath power-up to ID 1 (Hover pattern) - Static PADS
gridmap.set_cell_item(Vector3i(pos.x, 0, pos.y), 1)
# Cycle through the available power-up types
var tile_id = POWERUP_TILES[i % POWERUP_TILES.size()]
# Place on Floor 1
gridmap.set_cell_item(Vector3i(pos.x, 1, pos.y), tile_id)
# Sync to all clients
# Sync both floor and tile to all clients and host
if can_rpc():
main.rpc("sync_grid_item", pos.x, 1, pos.y, tile_id)
main.rpc("sync_grid_item", pos.x, 0, pos.y, 1) # Sync floor change (Pad on)
main.rpc("sync_grid_item", pos.x, 1, pos.y, tile_id) # Sync power-up
print("[StopNGo] Spawned %d power-up tiles (Speed & Ghost)" % spawn_count)
powerups_spawned = true
print("[StopNGo] Static power-up refresh completed.")
+40 -23
View File
@@ -40,9 +40,16 @@ func _ready():
_setup_btn(2, wall_btn)
_setup_btn(3, ghost_btn)
# All skills available with new logic
if wall_btn: wall_btn.visible = true
if freeze_btn: freeze_btn.visible = true
# All skills available with new logic, but restricted in Stop n Go
if is_restricted:
if wall_btn: wall_btn.visible = false
if freeze_btn: freeze_btn.visible = false
# Re-setup shortcut labels for restricted mode
_update_shortcuts_for_mode(true)
else:
if wall_btn: wall_btn.visible = true
if freeze_btn: freeze_btn.visible = true
_update_shortcuts_for_mode(false)
print("[PowerUpUI] UI Initialization Complete. Mapped %d buttons." % icon_containers.size())
@@ -91,39 +98,49 @@ func _setup_btn(effect_id: int, btn: Button):
var sc_lbl = Label.new()
sc_lbl.name = "ShortcutLabel"
sc_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
# Position: Top Left of the button
sc_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
sc_lbl.vertical_alignment = VERTICAL_ALIGNMENT_TOP
# Anchor to top left
sc_lbl.set_anchors_preset(Control.PRESET_TOP_LEFT)
# Offset to be close to the corner
sc_lbl.offset_left = 0
sc_lbl.offset_top = -5 # Close to the button top
sc_lbl.offset_top = -5
sc_lbl.add_theme_font_size_override("font_size", 16)
sc_lbl.add_theme_color_override("font_outline_color", Color.BLACK)
sc_lbl.add_theme_constant_override("outline_size", 4)
# Add color override to make it distinct (optional, but good for visibility)
sc_lbl.add_theme_color_override("font_color", Color(0.9, 0.9, 0.9))
# Determine Label Text based on Effect ID
var key_text = ""
# Consistent mapping: 1, 2, 3, 4
btn.add_child(sc_lbl)
_update_btn_shortcut(effect_id, btn)
# Connect click
if not btn.pressed.is_connected(_on_btn_pressed):
btn.pressed.connect(_on_btn_pressed.bind(effect_id))
func _update_shortcuts_for_mode(is_restricted: bool):
for effect_id in icon_containers:
var btn = icon_containers[effect_id]
_update_btn_shortcut(effect_id, btn)
func _update_btn_shortcut(effect_id: int, btn: Button):
var sc_lbl = btn.get_node_or_null("ShortcutLabel")
if not sc_lbl: return
var mode = LobbyManager.get_game_mode()
var is_sng = mode == GameMode.Mode.STOP_N_GO
var key_text = ""
if is_sng:
match effect_id:
0: key_text = "1" # Speed
3: key_text = "2" # Ghost
_: key_text = "" # Others hidden/disabled
else:
match effect_id:
0: key_text = "1" # Speed
2: key_text = "2" # Wall
1: key_text = "3" # Freeze
3: key_text = "4" # Ghost
sc_lbl.text = key_text
btn.add_child(sc_lbl)
# Connect click
if not btn.pressed.is_connected(_on_btn_pressed):
btn.pressed.connect(_on_btn_pressed.bind(effect_id))
sc_lbl.text = key_text
func _on_btn_pressed(effect_id: int):
print("[PowerUpUI] Clicked Button %d" % effect_id)