feat: Implement Stop N Go game mode with phase management, mission objectives, and visual HUD elements.
This commit is contained in:
@@ -545,6 +545,9 @@ func _on_profile_updated() -> void:
|
|||||||
|
|
||||||
func _update_player_slots() -> void:
|
func _update_player_slots() -> void:
|
||||||
"""Update all player slot visuals based on current player list."""
|
"""Update all player slot visuals based on current player list."""
|
||||||
|
if not multiplayer.has_multiplayer_peer():
|
||||||
|
return
|
||||||
|
|
||||||
var players = LobbyManager.get_players()
|
var players = LobbyManager.get_players()
|
||||||
var my_id = multiplayer.get_unique_id()
|
var my_id = multiplayer.get_unique_id()
|
||||||
|
|
||||||
|
|||||||
+65
-29
@@ -1766,6 +1766,13 @@ func _show_game_over_panel():
|
|||||||
if existing_panel:
|
if existing_panel:
|
||||||
existing_panel.show()
|
existing_panel.show()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Hide Gameplay UI
|
||||||
|
var actions_btn = get_node_or_null("TouchControls/TouchControls/ActionsBtn")
|
||||||
|
if actions_btn: actions_btn.hide()
|
||||||
|
|
||||||
|
if stop_n_go_manager and stop_n_go_manager.hud_layer:
|
||||||
|
stop_n_go_manager.hud_layer.hide()
|
||||||
|
|
||||||
# Create game over panel
|
# Create game over panel
|
||||||
var panel = PanelContainer.new()
|
var panel = PanelContainer.new()
|
||||||
@@ -1839,41 +1846,78 @@ func _show_game_over_panel():
|
|||||||
player_scores.sort_custom(func(a, b): return a.score > b.score)
|
player_scores.sort_custom(func(a, b): return a.score > b.score)
|
||||||
|
|
||||||
# Display each player
|
# Display each player
|
||||||
for i in range(min(player_scores.size(), 8)):
|
var local_peer_id = multiplayer.get_unique_id()
|
||||||
|
var local_player_rank = -1
|
||||||
|
|
||||||
|
# Find local player rank in sorted list
|
||||||
|
for i in range(player_scores.size()):
|
||||||
|
if player_scores[i].peer_id == local_peer_id:
|
||||||
|
local_player_rank = i
|
||||||
|
break
|
||||||
|
|
||||||
|
var rank_colors = [
|
||||||
|
Color(1.0, 0.84, 0.0), # Gold
|
||||||
|
Color(0.75, 0.75, 0.75), # Silver
|
||||||
|
Color(0.8, 0.5, 0.2) # Bronze
|
||||||
|
]
|
||||||
|
var rank_emojis = ["🥇", "🥈", "🥉"]
|
||||||
|
|
||||||
|
# Helper to create a leaderboard entry HBox
|
||||||
|
var create_entry = func(rank_idx: int):
|
||||||
var entry = HBoxContainer.new()
|
var entry = HBoxContainer.new()
|
||||||
entry.add_theme_constant_override("separation", 20)
|
entry.add_theme_constant_override("separation", 20)
|
||||||
|
|
||||||
var rank_colors = [
|
var score_data = player_scores[rank_idx]
|
||||||
Color(1.0, 0.84, 0.0), # Gold
|
var is_local = score_data.peer_id == local_peer_id
|
||||||
Color(0.75, 0.75, 0.75), # Silver
|
|
||||||
Color(0.8, 0.5, 0.2), # Bronze
|
|
||||||
Color(0.5, 0.5, 0.5), # 4th
|
|
||||||
Color(0.5, 0.5, 0.5), # 5th
|
|
||||||
Color(0.5, 0.5, 0.5), # 6th
|
|
||||||
Color(0.5, 0.5, 0.5), # 7th
|
|
||||||
Color(0.5, 0.5, 0.5) # 8th
|
|
||||||
]
|
|
||||||
var rank_emojis = ["🥇", "🥈", "🥉", "4th", "5th", "6th", "7th", "8th"]
|
|
||||||
|
|
||||||
var rank_label = Label.new()
|
var rank_label = Label.new()
|
||||||
rank_label.text = rank_emojis[i]
|
if rank_idx < 3:
|
||||||
|
rank_label.text = rank_emojis[rank_idx]
|
||||||
|
else:
|
||||||
|
# Ordinal rank (4th, 5th, etc.)
|
||||||
|
var n = rank_idx + 1
|
||||||
|
var suffix = "th"
|
||||||
|
if n % 10 == 1 and n % 100 != 11: suffix = "st"
|
||||||
|
elif n % 10 == 2 and n % 100 != 12: suffix = "nd"
|
||||||
|
elif n % 10 == 3 and n % 100 != 13: suffix = "rd"
|
||||||
|
rank_label.text = str(n) + suffix
|
||||||
|
|
||||||
rank_label.add_theme_font_size_override("font_size", 32)
|
rank_label.add_theme_font_size_override("font_size", 32)
|
||||||
entry.add_child(rank_label)
|
entry.add_child(rank_label)
|
||||||
|
|
||||||
var name_label = Label.new()
|
var name_label = Label.new()
|
||||||
name_label.text = player_scores[i].name
|
name_label.text = score_data.name + (" (YOU)" if is_local else "")
|
||||||
name_label.add_theme_font_size_override("font_size", 28)
|
name_label.add_theme_font_size_override("font_size", 28)
|
||||||
name_label.add_theme_color_override("font_color", rank_colors[i])
|
if rank_idx < 3:
|
||||||
|
name_label.add_theme_color_override("font_color", rank_colors[rank_idx])
|
||||||
|
elif is_local:
|
||||||
|
name_label.add_theme_color_override("font_color", Color(0.4, 0.7, 1.0)) # Blue for local player
|
||||||
|
|
||||||
name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
entry.add_child(name_label)
|
entry.add_child(name_label)
|
||||||
|
|
||||||
var score_label = Label.new()
|
var score_label = Label.new()
|
||||||
score_label.text = str(player_scores[i].score)
|
score_label.text = str(score_data.score)
|
||||||
score_label.add_theme_font_size_override("font_size", 28)
|
score_label.add_theme_font_size_override("font_size", 28)
|
||||||
score_label.add_theme_color_override("font_color", Color(0.4, 1.0, 0.4))
|
score_label.add_theme_color_override("font_color", Color(0.4, 1.0, 0.4))
|
||||||
entry.add_child(score_label)
|
entry.add_child(score_label)
|
||||||
|
|
||||||
leaderboard_container.add_child(entry)
|
return entry
|
||||||
|
|
||||||
|
# 1. Show Top 3
|
||||||
|
for i in range(min(player_scores.size(), 3)):
|
||||||
|
leaderboard_container.add_child(create_entry.call(i))
|
||||||
|
|
||||||
|
# 2. Show Local Player if NOT in Top 3
|
||||||
|
if local_player_rank >= 3:
|
||||||
|
# Add a separator
|
||||||
|
var separator = ColorRect.new()
|
||||||
|
separator.custom_minimum_size = Vector2(0, 2)
|
||||||
|
separator.color = Color(1, 1, 1, 0.2)
|
||||||
|
leaderboard_container.add_child(separator)
|
||||||
|
|
||||||
|
# Add local player entry
|
||||||
|
leaderboard_container.add_child(create_entry.call(local_player_rank))
|
||||||
|
|
||||||
# Back to Menu button
|
# Back to Menu button
|
||||||
var back_btn = Button.new()
|
var back_btn = Button.new()
|
||||||
@@ -1901,24 +1945,16 @@ func _on_back_to_menu_pressed():
|
|||||||
# Properly disconnect from Nakama match
|
# Properly disconnect from Nakama match
|
||||||
_cleanup_multiplayer()
|
_cleanup_multiplayer()
|
||||||
|
|
||||||
|
# Small delay to let cleanup settle
|
||||||
|
await get_tree().create_timer(0.2).timeout
|
||||||
|
|
||||||
# Go back to lobby
|
# Go back to lobby
|
||||||
if get_tree():
|
if get_tree():
|
||||||
get_tree().change_scene_to_file("res://scenes/lobby.tscn")
|
get_tree().change_scene_to_file("res://scenes/lobby.tscn")
|
||||||
|
|
||||||
func _cleanup_multiplayer():
|
func _cleanup_multiplayer():
|
||||||
"""Properly leave Nakama match and cleanup multiplayer state."""
|
"""Properly leave Nakama match and cleanup multiplayer state."""
|
||||||
print("[Main] Cleaning up multiplayer connection...")
|
NakamaManager.cleanup()
|
||||||
|
|
||||||
# Leave the Nakama match through the bridge
|
|
||||||
if NakamaManager.bridge:
|
|
||||||
NakamaManager.bridge.leave()
|
|
||||||
|
|
||||||
# Clear the current match ID
|
|
||||||
NakamaManager.current_match_id = ""
|
|
||||||
|
|
||||||
# Reset multiplayer peer to disconnect cleanly
|
|
||||||
if multiplayer.get_multiplayer_peer():
|
|
||||||
multiplayer.set_multiplayer_peer(null)
|
|
||||||
|
|
||||||
func _deferred_init_leaderboard():
|
func _deferred_init_leaderboard():
|
||||||
"""Initialize leaderboard after a delay to ensure all players are loaded."""
|
"""Initialize leaderboard after a delay to ensure all players are loaded."""
|
||||||
|
|||||||
+18
-12
@@ -195,6 +195,9 @@ func _run_ai_tick():
|
|||||||
print("[BotController] Action Taken: Put")
|
print("[BotController] Action Taken: Put")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not is_instance_valid(actor):
|
||||||
|
return
|
||||||
|
|
||||||
var goals_achv = _is_goals_achieved()
|
var goals_achv = _is_goals_achieved()
|
||||||
|
|
||||||
if actor.action_points > 1: # Only print if they have multi-AP
|
if actor.action_points > 1: # Only print if they have multi-AP
|
||||||
@@ -256,7 +259,7 @@ func _try_use_powerup() -> bool:
|
|||||||
NotificationManager.send_message(actor, NotificationManager.MESSAGES.USED_SPECIAL_POWER, NotificationManager.MessageType.POWERUP)
|
NotificationManager.send_message(actor, NotificationManager.MESSAGES.USED_SPECIAL_POWER, NotificationManager.MessageType.POWERUP)
|
||||||
|
|
||||||
await _wait_with_variance(action_delay)
|
await _wait_with_variance(action_delay)
|
||||||
if not is_instance_valid(self): return true # Early exit if deleted
|
if not is_instance_valid(actor) or not is_instance_valid(self): return true # Early exit if deleted
|
||||||
|
|
||||||
_is_processing_action = false
|
_is_processing_action = false
|
||||||
_current_action = "idle"
|
_current_action = "idle"
|
||||||
@@ -301,7 +304,7 @@ func _try_attack_chase() -> bool:
|
|||||||
_is_processing_action = true
|
_is_processing_action = true
|
||||||
_current_action = "attacking"
|
_current_action = "attacking"
|
||||||
await _wait_with_variance(action_delay) # Shorter delay for attacks? perhaos
|
await _wait_with_variance(action_delay) # Shorter delay for attacks? perhaos
|
||||||
if not is_instance_valid(self): return true
|
if not is_instance_valid(self) or not is_instance_valid(actor): return true
|
||||||
_is_processing_action = false
|
_is_processing_action = false
|
||||||
_current_action = "idle"
|
_current_action = "idle"
|
||||||
return true
|
return true
|
||||||
@@ -365,7 +368,7 @@ func _try_grab() -> bool:
|
|||||||
|
|
||||||
# Wait for animation
|
# Wait for animation
|
||||||
await _wait_with_variance(action_delay)
|
await _wait_with_variance(action_delay)
|
||||||
if not is_instance_valid(self): return true
|
if not is_instance_valid(self) or not is_instance_valid(actor): return true
|
||||||
_is_processing_action = false
|
_is_processing_action = false
|
||||||
_current_action = "idle"
|
_current_action = "idle"
|
||||||
return true
|
return true
|
||||||
@@ -466,17 +469,19 @@ func _try_move() -> bool:
|
|||||||
var max_wait_time = 2.0
|
var max_wait_time = 2.0
|
||||||
var elapsed = 0.0
|
var elapsed = 0.0
|
||||||
|
|
||||||
while actor.is_player_moving and is_instance_valid(self):
|
while is_instance_valid(actor) and actor.is_player_moving and is_instance_valid(self):
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
elapsed += get_process_delta_time()
|
elapsed += get_process_delta_time()
|
||||||
if elapsed > max_wait_time:
|
if elapsed > max_wait_time:
|
||||||
print("[BotController] %s movement TIMEOUT after %.1fs" % [actor.name, elapsed])
|
if is_instance_valid(actor):
|
||||||
|
print("[BotController] %s movement TIMEOUT after %.1fs" % [actor.name, elapsed])
|
||||||
break
|
break
|
||||||
|
|
||||||
if not is_instance_valid(self): return true
|
if not is_instance_valid(self) or not is_instance_valid(actor): return true
|
||||||
_is_processing_action = false
|
_is_processing_action = false
|
||||||
_current_action = "idle"
|
_current_action = "idle"
|
||||||
print("[BotController] %s move finished. New Pos: %s" % [actor.name, actor.current_position])
|
if is_instance_valid(actor):
|
||||||
|
print("[BotController] %s move finished. New Pos: %s" % [actor.name, actor.current_position])
|
||||||
return true
|
return true
|
||||||
else:
|
else:
|
||||||
print("[BotController] %s simple_move_to BLOCKED (others). Trying unstuck move." % actor.name)
|
print("[BotController] %s simple_move_to BLOCKED (others). Trying unstuck move." % actor.name)
|
||||||
@@ -515,15 +520,16 @@ func _try_unstuck_move() -> bool:
|
|||||||
# Proper wait for movement completion
|
# Proper wait for movement completion
|
||||||
var max_wait = 1.5
|
var max_wait = 1.5
|
||||||
var elapsed = 0.0
|
var elapsed = 0.0
|
||||||
while actor.is_player_moving and is_instance_valid(self):
|
while is_instance_valid(actor) and actor.is_player_moving and is_instance_valid(self):
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
elapsed += get_process_delta_time()
|
elapsed += get_process_delta_time()
|
||||||
if elapsed > max_wait: break
|
if elapsed > max_wait: break
|
||||||
|
|
||||||
if not is_instance_valid(self): return true
|
if not is_instance_valid(self) or not is_instance_valid(actor): return true
|
||||||
_is_processing_action = false
|
_is_processing_action = false
|
||||||
_current_action = "idle"
|
_current_action = "idle"
|
||||||
print("[BotController] %s Unstuck move finished at %s" % [actor.name, actor.current_position])
|
if is_instance_valid(actor):
|
||||||
|
print("[BotController] %s Unstuck move finished at %s" % [actor.name, actor.current_position])
|
||||||
return true
|
return true
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -580,7 +586,7 @@ func _try_put(high_priority: bool = false) -> bool:
|
|||||||
print("[BotController] %s put unneeded tile %d at %s (Panic: %s)" % [actor.name, item, put_position, is_panic])
|
print("[BotController] %s put unneeded tile %d at %s (Panic: %s)" % [actor.name, item, put_position, is_panic])
|
||||||
|
|
||||||
await _wait_with_variance(action_delay)
|
await _wait_with_variance(action_delay)
|
||||||
if not is_instance_valid(self): return true
|
if not is_instance_valid(actor) or not is_instance_valid(self): return true
|
||||||
_is_processing_action = false
|
_is_processing_action = false
|
||||||
_current_action = "idle"
|
_current_action = "idle"
|
||||||
return true
|
return true
|
||||||
@@ -631,7 +637,7 @@ func _try_arrange() -> bool:
|
|||||||
print("[BotController] %s arranged slot %d -> %d" % [actor.name, arrangement.source_slot, arrangement.target_slot])
|
print("[BotController] %s arranged slot %d -> %d" % [actor.name, arrangement.source_slot, arrangement.target_slot])
|
||||||
|
|
||||||
await _wait_with_variance(action_delay)
|
await _wait_with_variance(action_delay)
|
||||||
if not is_instance_valid(self): return true
|
if not is_instance_valid(actor) or not is_instance_valid(self): return true
|
||||||
_is_processing_action = false
|
_is_processing_action = false
|
||||||
_current_action = "idle"
|
_current_action = "idle"
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -120,6 +120,9 @@ func refresh_room_list() -> void:
|
|||||||
|
|
||||||
func set_ready(is_ready: bool) -> void:
|
func set_ready(is_ready: bool) -> void:
|
||||||
"""Set local player's ready state."""
|
"""Set local player's ready state."""
|
||||||
|
if not multiplayer.has_multiplayer_peer():
|
||||||
|
return
|
||||||
|
|
||||||
var my_id = multiplayer.get_unique_id()
|
var my_id = multiplayer.get_unique_id()
|
||||||
|
|
||||||
# Update local state
|
# Update local state
|
||||||
@@ -248,6 +251,10 @@ func set_character(character_name: String) -> void:
|
|||||||
return
|
return
|
||||||
|
|
||||||
local_character_index = idx
|
local_character_index = idx
|
||||||
|
|
||||||
|
if not multiplayer.has_multiplayer_peer():
|
||||||
|
return
|
||||||
|
|
||||||
var my_id = multiplayer.get_unique_id()
|
var my_id = multiplayer.get_unique_id()
|
||||||
|
|
||||||
# Update local player data
|
# Update local player data
|
||||||
@@ -289,6 +296,11 @@ func sync_character(player_id: int, character_name: String) -> void:
|
|||||||
func set_player_name(new_name: String) -> void:
|
func set_player_name(new_name: String) -> void:
|
||||||
"""Set local player's name. Syncs to all peers."""
|
"""Set local player's name. Syncs to all peers."""
|
||||||
local_player_name = new_name
|
local_player_name = new_name
|
||||||
|
|
||||||
|
if not multiplayer.has_multiplayer_peer():
|
||||||
|
emit_signal("player_list_changed")
|
||||||
|
return
|
||||||
|
|
||||||
var my_id = multiplayer.get_unique_id()
|
var my_id = multiplayer.get_unique_id()
|
||||||
|
|
||||||
# Update local player data
|
# Update local player data
|
||||||
@@ -412,6 +424,9 @@ func _on_match_joined(match_id: String) -> void:
|
|||||||
var short_id = match_id.substr(0, 8) if match_id.length() > 8 else match_id
|
var short_id = match_id.substr(0, 8) if match_id.length() > 8 else match_id
|
||||||
current_room["room_name"] = short_id
|
current_room["room_name"] = short_id
|
||||||
|
|
||||||
|
if not multiplayer.has_multiplayer_peer():
|
||||||
|
return
|
||||||
|
|
||||||
# Add self to player list
|
# Add self to player list
|
||||||
var my_id = multiplayer.get_unique_id()
|
var my_id = multiplayer.get_unique_id()
|
||||||
var my_data = {
|
var my_data = {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ enum Phase {GO, STOP}
|
|||||||
|
|
||||||
const GO_DURATION: float = 8.0
|
const GO_DURATION: float = 8.0
|
||||||
const STOP_DURATION: float = 4.0
|
const STOP_DURATION: float = 4.0
|
||||||
const REQUIRED_GOALS: int = 8
|
const REQUIRED_GOALS: int = 1
|
||||||
|
|
||||||
var current_phase: Phase = Phase.GO
|
var current_phase: Phase = Phase.GO
|
||||||
var phase_timer: float = GO_DURATION
|
var phase_timer: float = GO_DURATION
|
||||||
|
|||||||
@@ -75,6 +75,10 @@ func connect_to_nakama_async(email: String = "", password: String = "") -> bool:
|
|||||||
|
|
||||||
# 3. Initialize Multiplayer Bridge
|
# 3. Initialize Multiplayer Bridge
|
||||||
# This links Nakama's socket to Godot's High-Level Multiplayer API
|
# This links Nakama's socket to Godot's High-Level Multiplayer API
|
||||||
|
if bridge:
|
||||||
|
bridge.leave()
|
||||||
|
bridge = null
|
||||||
|
|
||||||
bridge = NakamaMultiplayerBridge.new(socket)
|
bridge = NakamaMultiplayerBridge.new(socket)
|
||||||
|
|
||||||
# Connect bridge signals
|
# Connect bridge signals
|
||||||
@@ -89,6 +93,26 @@ func connect_to_nakama_async(email: String = "", password: String = "") -> bool:
|
|||||||
emit_signal("connected_to_nakama")
|
emit_signal("connected_to_nakama")
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
func cleanup():
|
||||||
|
"""Properly shutdown the Nakama connection and reset the multiplayer peer."""
|
||||||
|
print("[NakamaManager] Full cleanup starting...")
|
||||||
|
|
||||||
|
if bridge:
|
||||||
|
bridge.leave()
|
||||||
|
bridge = null
|
||||||
|
|
||||||
|
if socket:
|
||||||
|
socket.close()
|
||||||
|
socket = null
|
||||||
|
|
||||||
|
current_match_id = ""
|
||||||
|
|
||||||
|
# Reset Godot's multiplayer peer
|
||||||
|
if multiplayer.get_multiplayer_peer():
|
||||||
|
multiplayer.set_multiplayer_peer(null)
|
||||||
|
|
||||||
|
print("[NakamaManager] Cleanup complete.")
|
||||||
|
|
||||||
# --- Match Management ---
|
# --- Match Management ---
|
||||||
|
|
||||||
func host_game():
|
func host_game():
|
||||||
@@ -172,5 +196,4 @@ func list_matches_async() -> Array:
|
|||||||
return rooms
|
return rooms
|
||||||
|
|
||||||
func _exit_tree():
|
func _exit_tree():
|
||||||
if socket:
|
cleanup()
|
||||||
socket.close()
|
|
||||||
|
|||||||
Reference in New Issue
Block a user