feat : Refactored Game Over UI from programmatic to scene-based architecture and resolved critical multiplayer/spawn bugs
This commit is contained in:
+74
-192
@@ -318,19 +318,19 @@ func add_message_to_bar(player_name: String, message: String, type: int = Messag
|
||||
var color = Color.WHITE
|
||||
match type:
|
||||
MessageType.POWERUP:
|
||||
icon = "⚡ "
|
||||
icon = "âš¡ "
|
||||
color = Color(0.4, 1.0, 0.4) # Bright green
|
||||
MessageType.GOAL:
|
||||
icon = "🎯 "
|
||||
icon = "🎯 "
|
||||
color = Color(1.0, 0.85, 0.2) # Gold
|
||||
MessageType.CYCLE:
|
||||
icon = "⏱️ "
|
||||
icon = "â±ï¸ "
|
||||
color = Color(0.4, 0.8, 1.0) # Light blue
|
||||
MessageType.WARNING:
|
||||
icon = "⚠️ "
|
||||
icon = "âš ï¸ "
|
||||
color = Color(1.0, 0.5, 0.3) # Orange
|
||||
_:
|
||||
icon = "💬 "
|
||||
icon = "💬 "
|
||||
color = Color(0.9, 0.9, 0.9) # Light gray
|
||||
|
||||
# Include player name in message if provided
|
||||
@@ -966,7 +966,7 @@ func _assign_random_spawn_positions():
|
||||
print("Assigned spawn %s to player %s" % [assigned_pos, player.name])
|
||||
|
||||
func _assign_stop_n_go_spawn_positions(all_players: Array):
|
||||
"""Assigns spawns on column 0, starting from row 1 for Stop N Go mode."""
|
||||
"""Assigns spawns in the left columns of the Stop N Go arena, validating walkability."""
|
||||
# Sort players for deterministic assignment based on ID
|
||||
all_players.sort_custom(func(a, b): return a.name.to_int() < b.name.to_int())
|
||||
|
||||
@@ -974,12 +974,24 @@ func _assign_stop_n_go_spawn_positions(all_players: Array):
|
||||
if not enhanced_gridmap:
|
||||
return
|
||||
|
||||
var col = 0
|
||||
var current_row = 3
|
||||
# Collect valid walkable spawn positions from the leftmost columns
|
||||
var valid_spawns: Array[Vector2i] = []
|
||||
for col in range(0, min(5, enhanced_gridmap.columns)): # Check first 5 columns
|
||||
for row in range(enhanced_gridmap.rows):
|
||||
var tile = enhanced_gridmap.get_cell_item(Vector3i(col, 0, row))
|
||||
if tile == 0 or tile == 3: # Walkable or Start
|
||||
valid_spawns.append(Vector2i(col, row))
|
||||
if valid_spawns.size() >= all_players.size():
|
||||
break
|
||||
|
||||
for player in all_players:
|
||||
var assigned_pos = Vector2i(col, current_row)
|
||||
current_row += 1
|
||||
# Fallback: if somehow no valid spawns found, use old logic
|
||||
if valid_spawns.is_empty():
|
||||
for row in range(3, 3 + all_players.size()):
|
||||
valid_spawns.append(Vector2i(0, row))
|
||||
|
||||
for i in range(all_players.size()):
|
||||
var player = all_players[i]
|
||||
var assigned_pos = valid_spawns[i % valid_spawns.size()]
|
||||
|
||||
# Ensure immediate sync
|
||||
player.position = player.grid_to_world(assigned_pos)
|
||||
@@ -1903,6 +1915,13 @@ func sync_full_grid_data(data: PackedInt32Array):
|
||||
add_child(portal_mode_manager)
|
||||
portal_mode_manager.initialize(self , enhanced_gridmap)
|
||||
portal_mode_manager.setup_arena_locally()
|
||||
else:
|
||||
# Freemode: Ensure Floor 0 is entirely walkable (reset stale state from previous modes)
|
||||
for x in range(enhanced_gridmap.columns):
|
||||
for z in range(enhanced_gridmap.rows):
|
||||
var f0_item = enhanced_gridmap.get_cell_item(Vector3i(x, 0, z))
|
||||
if f0_item == -1 or (f0_item >= 7 and f0_item <= 20):
|
||||
enhanced_gridmap.set_cell_item(Vector3i(x, 0, z), 0)
|
||||
|
||||
# Apply the synced data to Floor 1
|
||||
enhanced_gridmap.set_floor_data(1, data)
|
||||
@@ -2089,13 +2108,13 @@ func _freeze_node_recursive(node: Node):
|
||||
_freeze_node_recursive(child)
|
||||
|
||||
func _show_game_over_panel():
|
||||
"""Create and display the game over panel with final leaderboard."""
|
||||
"""Instantiate and display the game over panel scene."""
|
||||
# Check if panel already exists
|
||||
var existing_panel = get_node_or_null("GameOverPanel")
|
||||
if existing_panel:
|
||||
existing_panel.show()
|
||||
var existing_layer = get_node_or_null("GameOverLayer")
|
||||
if existing_layer:
|
||||
existing_layer.show()
|
||||
return
|
||||
|
||||
|
||||
# Hide Gameplay UI
|
||||
var actions_btn = get_node_or_null("TouchControls/TouchControls/ActionsBtn")
|
||||
if actions_btn: actions_btn.hide()
|
||||
@@ -2108,209 +2127,72 @@ func _show_game_over_panel():
|
||||
|
||||
if stop_n_go_manager and stop_n_go_manager.hud_layer:
|
||||
stop_n_go_manager.hud_layer.hide()
|
||||
|
||||
|
||||
if portal_mode_manager and portal_mode_manager.hud_layer:
|
||||
portal_mode_manager.hud_layer.hide()
|
||||
|
||||
# Create game over panel
|
||||
var panel = PanelContainer.new()
|
||||
panel.name = "GameOverPanel"
|
||||
panel.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
|
||||
# Semi-transparent dark background
|
||||
var style = StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.0, 0.0, 0.0, 0.85)
|
||||
panel.add_theme_stylebox_override("panel", style)
|
||||
|
||||
# CONTENT VBOX
|
||||
var vbox = VBoxContainer.new()
|
||||
vbox.name = "VBox"
|
||||
vbox.set_anchors_preset(Control.PRESET_CENTER)
|
||||
vbox.add_theme_constant_override("separation", 20)
|
||||
vbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
panel.add_child(vbox)
|
||||
|
||||
# Center the vbox
|
||||
var margin = MarginContainer.new()
|
||||
margin.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
margin.add_theme_constant_override("margin_left", 200)
|
||||
margin.add_theme_constant_override("margin_right", 200)
|
||||
margin.add_theme_constant_override("margin_top", 100)
|
||||
margin.add_theme_constant_override("margin_bottom", 100)
|
||||
panel.add_child(margin)
|
||||
|
||||
var inner_vbox = VBoxContainer.new()
|
||||
inner_vbox.add_theme_constant_override("separation", 30)
|
||||
inner_vbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
margin.add_child(inner_vbox)
|
||||
|
||||
# Title
|
||||
var title = Label.new()
|
||||
title.text = "⏱️ TIME'S UP!"
|
||||
title.add_theme_font_size_override("font_size", 64)
|
||||
title.add_theme_color_override("font_color", Color(0.992, 0.796, 0.047))
|
||||
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
inner_vbox.add_child(title)
|
||||
|
||||
# Subtitle
|
||||
var subtitle = Label.new()
|
||||
subtitle.text = "FINAL STANDINGS"
|
||||
subtitle.add_theme_font_size_override("font_size", 24)
|
||||
subtitle.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7))
|
||||
subtitle.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
inner_vbox.add_child(subtitle)
|
||||
|
||||
# Leaderboard container
|
||||
var leaderboard_container = VBoxContainer.new()
|
||||
leaderboard_container.add_theme_constant_override("separation", 15)
|
||||
inner_vbox.add_child(leaderboard_container)
|
||||
|
||||
var player_scores = []
|
||||
# =========================================================================
|
||||
# Gather + sort player data
|
||||
# =========================================================================
|
||||
var all_player_scores = []
|
||||
for p in get_tree().get_nodes_in_group("Players"):
|
||||
player_scores.append({
|
||||
"peer_id": p.name.to_int(),
|
||||
var pid = p.name.to_int()
|
||||
all_player_scores.append({
|
||||
"peer_id": pid,
|
||||
"name": p.display_name if not p.display_name.is_empty() else str(p.name),
|
||||
"score": goals_cycle_manager.get_player_score(p.name.to_int()) if goals_cycle_manager else 0
|
||||
"score": goals_cycle_manager.get_player_score(pid) if goals_cycle_manager else 0,
|
||||
"goal_count": goals_cycle_manager.player_goal_counts.get(pid, 0) if goals_cycle_manager else 0,
|
||||
"character": p._selected_character if "_selected_character" in p else "Masbro"
|
||||
})
|
||||
|
||||
# Custom Sort for Stop n Go and Tekton Doors: Winner always first
|
||||
# Sort players by score (with winner priority for special modes)
|
||||
if LobbyManager.game_mode == "Stop n Go" and stop_n_go_winner_id != -1:
|
||||
player_scores.sort_custom(func(a, b):
|
||||
all_player_scores.sort_custom(func(a, b):
|
||||
if a.peer_id == stop_n_go_winner_id: return true
|
||||
if b.peer_id == stop_n_go_winner_id: return false
|
||||
return a.score > b.score
|
||||
)
|
||||
elif LobbyManager.game_mode == "Tekton Doors" and portal_mode_winner_id != -1:
|
||||
player_scores.sort_custom(func(a, b):
|
||||
all_player_scores.sort_custom(func(a, b):
|
||||
if a.peer_id == portal_mode_winner_id: return true
|
||||
if b.peer_id == portal_mode_winner_id: return false
|
||||
return a.score > b.score
|
||||
)
|
||||
else:
|
||||
player_scores.sort_custom(func(a, b): return a.score > b.score)
|
||||
all_player_scores.sort_custom(func(a, b): return a.score > b.score)
|
||||
|
||||
# Display each player
|
||||
# =========================================================================
|
||||
# Instantiate the scene
|
||||
# =========================================================================
|
||||
var panel_scene = load("res://scenes/ui/game_over_panel.tscn")
|
||||
if not panel_scene:
|
||||
push_error("[Main] Failed to load game_over_panel.tscn")
|
||||
return
|
||||
|
||||
# CanvasLayer for proper z-ordering
|
||||
var canvas_layer = CanvasLayer.new()
|
||||
canvas_layer.name = "GameOverLayer"
|
||||
canvas_layer.layer = 50
|
||||
add_child(canvas_layer)
|
||||
|
||||
var panel = panel_scene.instantiate()
|
||||
canvas_layer.add_child(panel)
|
||||
|
||||
# Populate data
|
||||
var local_peer_id = multiplayer.get_unique_id()
|
||||
var local_player_rank = -1
|
||||
panel.setup(all_player_scores, local_peer_id)
|
||||
|
||||
# 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()
|
||||
entry.add_theme_constant_override("separation", 20)
|
||||
|
||||
var score_data = player_scores[rank_idx]
|
||||
var is_local = score_data.peer_id == local_peer_id
|
||||
|
||||
var rank_label = Label.new()
|
||||
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)
|
||||
entry.add_child(rank_label)
|
||||
|
||||
var name_label = Label.new()
|
||||
name_label.text = score_data.name + (" (YOU)" if is_local else "")
|
||||
name_label.add_theme_font_size_override("font_size", 28)
|
||||
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
|
||||
entry.add_child(name_label)
|
||||
|
||||
var score_label = Label.new()
|
||||
score_label.text = str(score_data.score)
|
||||
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))
|
||||
entry.add_child(score_label)
|
||||
|
||||
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))
|
||||
|
||||
# 3. Rematch Option
|
||||
var rematch_container = HBoxContainer.new()
|
||||
rematch_container.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
rematch_container.add_theme_constant_override("separation", 20)
|
||||
inner_vbox.add_child(rematch_container)
|
||||
|
||||
var rematch_btn = Button.new()
|
||||
rematch_btn.name = "RematchBtn"
|
||||
rematch_btn.text = "REMATCH"
|
||||
rematch_btn.custom_minimum_size = Vector2(200, 60)
|
||||
rematch_btn.add_theme_font_size_override("font_size", 20)
|
||||
rematch_btn.pressed.connect(func():
|
||||
rematch_btn.disabled = true
|
||||
rematch_btn.text = "VOTED"
|
||||
# Connect signals
|
||||
panel.back_pressed.connect(_on_back_to_menu_pressed)
|
||||
panel.rematch_pressed.connect(func():
|
||||
LobbyManager.request_rematch.rpc(multiplayer.get_unique_id())
|
||||
)
|
||||
rematch_container.add_child(rematch_btn)
|
||||
|
||||
var rematch_label = Label.new()
|
||||
rematch_label.name = "RematchVoteLabel"
|
||||
rematch_label.text = "0/2"
|
||||
rematch_label.add_theme_font_size_override("font_size", 24)
|
||||
rematch_label.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7))
|
||||
rematch_container.add_child(rematch_label)
|
||||
|
||||
LobbyManager.rematch_votes_updated.connect(func(count, required):
|
||||
if is_instance_valid(rematch_label):
|
||||
rematch_label.text = "%d/%d" % [count, required]
|
||||
if count >= required:
|
||||
rematch_label.add_theme_color_override("font_color", Color.GREEN)
|
||||
if is_instance_valid(panel):
|
||||
panel.update_rematch_votes(count, required)
|
||||
)
|
||||
|
||||
# 4. Back to Menu button
|
||||
var back_btn = Button.new()
|
||||
back_btn.name = "BackToMenuBtn"
|
||||
back_btn.text = "BACK TO MAIN MENU"
|
||||
back_btn.custom_minimum_size = Vector2(300, 60)
|
||||
back_btn.add_theme_font_size_override("font_size", 20)
|
||||
back_btn.pressed.connect(_on_back_to_menu_pressed)
|
||||
|
||||
# Center the button in a container
|
||||
var btn_container = HBoxContainer.new()
|
||||
btn_container.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
btn_container.add_child(back_btn)
|
||||
inner_vbox.add_child(btn_container)
|
||||
|
||||
add_child(panel)
|
||||
|
||||
func _on_back_to_menu_pressed():
|
||||
"""Return to lobby/main menu and clean up game state."""
|
||||
print("[Main] Returning to lobby...")
|
||||
|
||||
Reference in New Issue
Block a user