feat: Implement Stop n Go game mode with phase management, missions, dynamic safe zones, and associated UI and managers.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
Reference in New Issue
Block a user