diff --git a/scenes/main.gd b/scenes/main.gd index 95404f0..3b059d6 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -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...") diff --git a/scenes/ui/game_over_panel.tscn b/scenes/ui/game_over_panel.tscn new file mode 100644 index 0000000..d32d7bd --- /dev/null +++ b/scenes/ui/game_over_panel.tscn @@ -0,0 +1,504 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="res://scripts/ui/game_over_panel.gd" id="1_script"] +[ext_resource type="FontFile" uid="uid://xnjx058n4tsw" path="res://assets/fonts/Nougat-ExtraBlack.ttf" id="2_font"] +[ext_resource type="PackedScene" uid="uid://ejeamn0pyey4" path="res://assets/characters/Bob.glb" id="3_bob"] +[ext_resource type="PackedScene" uid="uid://d4cul3w3wem5w" path="res://assets/characters/Gatot.glb" id="4_gatot"] +[ext_resource type="PackedScene" uid="uid://1vk0mjnwkngi" path="res://assets/characters/Masbro.glb" id="5_masbro"] +[ext_resource type="PackedScene" uid="uid://bmln7v6v5kvxg" path="res://assets/characters/Oldpop.glb" id="6_oldpop"] +[ext_resource type="AnimationLibrary" uid="uid://c3pyopnwibckj" path="res://assets/characters/animations/animation-pack.res" id="7_animlib"] + +[sub_resource type="Environment" id="Environment_preview"] +background_mode = 1 +background_color = Color(0.06, 0.07, 0.1, 0) +ambient_light_source = 2 +ambient_light_color = Color(0.5, 0.55, 0.75, 1) +ambient_light_energy = 0.7 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_header"] +bg_color = Color(0.95, 0.75, 0.1, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +content_margin_left = 16.0 +content_margin_top = 8.0 +content_margin_right = 16.0 +content_margin_bottom = 8.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_content"] +bg_color = Color(0.08, 0.1, 0.2, 0.95) +corner_radius_bottom_left = 8 +content_margin_left = 20.0 +content_margin_top = 16.0 +content_margin_right = 20.0 +content_margin_bottom = 16.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_back_normal"] +bg_color = Color(0.75, 0.15, 0.15, 1) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 +content_margin_left = 16.0 +content_margin_top = 8.0 +content_margin_right = 16.0 +content_margin_bottom = 8.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_back_hover"] +bg_color = Color(0.9, 0.2, 0.2, 1) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 +content_margin_left = 16.0 +content_margin_top = 8.0 +content_margin_right = 16.0 +content_margin_bottom = 8.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_back_pressed"] +bg_color = Color(0.6, 0.1, 0.1, 1) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 +content_margin_left = 16.0 +content_margin_top = 8.0 +content_margin_right = 16.0 +content_margin_bottom = 8.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_rematch_normal"] +bg_color = Color(0.15, 0.7, 0.15, 1) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 +content_margin_left = 16.0 +content_margin_top = 8.0 +content_margin_right = 16.0 +content_margin_bottom = 8.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_rematch_hover"] +bg_color = Color(0.2, 0.85, 0.2, 1) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 +content_margin_left = 16.0 +content_margin_top = 8.0 +content_margin_right = 16.0 +content_margin_bottom = 8.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_rematch_pressed"] +bg_color = Color(0.1, 0.55, 0.1, 1) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 +content_margin_left = 16.0 +content_margin_top = 8.0 +content_margin_right = 16.0 +content_margin_bottom = 8.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_xp_bg"] +bg_color = Color(0.15, 0.15, 0.15, 1) +corner_radius_top_left = 4 +corner_radius_top_right = 4 +corner_radius_bottom_right = 4 +corner_radius_bottom_left = 4 + +[node name="GameOverPanel" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_script") + +[node name="Background" type="ColorRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +color = Color(0.22, 0.22, 0.22, 0.95) + +[node name="MainMargin" type="MarginContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 30 +theme_override_constants/margin_top = 20 +theme_override_constants/margin_right = 30 +theme_override_constants/margin_bottom = 20 + +[node name="MainHBox" type="HBoxContainer" parent="MainMargin"] +layout_mode = 2 +theme_override_constants/separation = 20 + +[node name="LeftSide" type="VBoxContainer" parent="MainMargin/MainHBox"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_stretch_ratio = 1.4 +theme_override_constants/separation = 0 + +[node name="HeaderPanel" type="PanelContainer" parent="MainMargin/MainHBox/LeftSide"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_styles/panel = SubResource("StyleBoxFlat_header") + +[node name="HeaderHBox" type="HBoxContainer" parent="MainMargin/MainHBox/LeftSide/HeaderPanel"] +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="TabTitleLabel" type="Label" parent="MainMargin/MainHBox/LeftSide/HeaderPanel/HeaderHBox"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_colors/font_color = Color(0.15, 0.1, 0, 1) +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 28 +text = "RACE RESULT" + +[node name="BodyHBox" type="HBoxContainer" parent="MainMargin/MainHBox/LeftSide"] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/separation = 8 + +[node name="ContentPanel" type="PanelContainer" parent="MainMargin/MainHBox/LeftSide/BodyHBox"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_styles/panel = SubResource("StyleBoxFlat_content") + +[node name="ContentStack" type="Control" parent="MainMargin/MainHBox/LeftSide/BodyHBox/ContentPanel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="RaceResultContent" type="VBoxContainer" parent="MainMargin/MainHBox/LeftSide/BodyHBox/ContentPanel/ContentStack"] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/separation = 12 + +[node name="PlayerInfoHBox" type="HBoxContainer" parent="MainMargin/MainHBox/LeftSide/BodyHBox/ContentPanel/ContentStack/RaceResultContent"] +layout_mode = 2 +theme_override_constants/separation = 12 + +[node name="PlayerRankLabel" type="Label" parent="MainMargin/MainHBox/LeftSide/BodyHBox/ContentPanel/ContentStack/RaceResultContent/PlayerInfoHBox"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_colors/font_color = Color(0.95, 0.75, 0.1, 1) +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 32 +text = "1st" + +[node name="PlayerNameLabel" type="Label" parent="MainMargin/MainHBox/LeftSide/BodyHBox/ContentPanel/ContentStack/RaceResultContent/PlayerInfoHBox"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_colors/font_color = Color(1, 1, 1, 1) +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 32 +text = "Player" + +[node name="PlayerScoreLabel" type="Label" parent="MainMargin/MainHBox/LeftSide/BodyHBox/ContentPanel/ContentStack/RaceResultContent/PlayerInfoHBox"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_colors/font_color = Color(1, 1, 1, 1) +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 40 +text = "0" +horizontal_alignment = 2 + +[node name="Sep1" type="HSeparator" parent="MainMargin/MainHBox/LeftSide/BodyHBox/ContentPanel/ContentStack/RaceResultContent"] +layout_mode = 2 +theme_override_constants/separation = 4 + +[node name="CompletionRow" type="HBoxContainer" parent="MainMargin/MainHBox/LeftSide/BodyHBox/ContentPanel/ContentStack/RaceResultContent"] +layout_mode = 2 +theme_override_constants/separation = 20 + +[node name="CompletionLabel" type="Label" parent="MainMargin/MainHBox/LeftSide/BodyHBox/ContentPanel/ContentStack/RaceResultContent/CompletionRow"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_colors/font_color = Color(0.8, 0.8, 0.8, 1) +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 24 +text = "COMPLETION" + +[node name="CompletionValue" type="Label" parent="MainMargin/MainHBox/LeftSide/BodyHBox/ContentPanel/ContentStack/RaceResultContent/CompletionRow"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_colors/font_color = Color(1, 1, 1, 1) +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 24 +text = "0x" +horizontal_alignment = 2 + +[node name="ScoreRow" type="HBoxContainer" parent="MainMargin/MainHBox/LeftSide/BodyHBox/ContentPanel/ContentStack/RaceResultContent"] +layout_mode = 2 +theme_override_constants/separation = 20 + +[node name="ScoreLabel" type="Label" parent="MainMargin/MainHBox/LeftSide/BodyHBox/ContentPanel/ContentStack/RaceResultContent/ScoreRow"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_colors/font_color = Color(0.8, 0.8, 0.8, 1) +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 24 +text = "SCORE" + +[node name="ScoreValue" type="Label" parent="MainMargin/MainHBox/LeftSide/BodyHBox/ContentPanel/ContentStack/RaceResultContent/ScoreRow"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_colors/font_color = Color(1, 1, 1, 1) +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 24 +text = "0" +horizontal_alignment = 2 + +[node name="RankRow" type="HBoxContainer" parent="MainMargin/MainHBox/LeftSide/BodyHBox/ContentPanel/ContentStack/RaceResultContent"] +layout_mode = 2 +theme_override_constants/separation = 20 + +[node name="RankLabel" type="Label" parent="MainMargin/MainHBox/LeftSide/BodyHBox/ContentPanel/ContentStack/RaceResultContent/RankRow"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_colors/font_color = Color(0.8, 0.8, 0.8, 1) +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 24 +text = "RANK" + +[node name="RankValue" type="Label" parent="MainMargin/MainHBox/LeftSide/BodyHBox/ContentPanel/ContentStack/RaceResultContent/RankRow"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_colors/font_color = Color(0.95, 0.75, 0.1, 1) +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 52 +text = "1st" +horizontal_alignment = 2 + +[node name="RankListContent" type="VBoxContainer" parent="MainMargin/MainHBox/LeftSide/BodyHBox/ContentPanel/ContentStack"] +unique_name_in_owner = true +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/separation = 6 + +[node name="WinnerHBox" type="HBoxContainer" parent="MainMargin/MainHBox/LeftSide/BodyHBox/ContentPanel/ContentStack/RankListContent"] +layout_mode = 2 +theme_override_constants/separation = 12 + +[node name="WinnerRankLabel" type="Label" parent="MainMargin/MainHBox/LeftSide/BodyHBox/ContentPanel/ContentStack/RankListContent/WinnerHBox"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_colors/font_color = Color(0.95, 0.75, 0.1, 1) +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 30 +text = "1st" + +[node name="WinnerNameLabel" type="Label" parent="MainMargin/MainHBox/LeftSide/BodyHBox/ContentPanel/ContentStack/RankListContent/WinnerHBox"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_colors/font_color = Color(1, 1, 1, 1) +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 30 +text = "Player" + +[node name="WinnerScoreLabel" type="Label" parent="MainMargin/MainHBox/LeftSide/BodyHBox/ContentPanel/ContentStack/RankListContent/WinnerHBox"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_colors/font_color = Color(1, 1, 1, 1) +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 38 +text = "0" +horizontal_alignment = 2 + +[node name="Sep2" type="HSeparator" parent="MainMargin/MainHBox/LeftSide/BodyHBox/ContentPanel/ContentStack/RankListContent"] +layout_mode = 2 +theme_override_constants/separation = 4 + +[node name="RankScroll" type="ScrollContainer" parent="MainMargin/MainHBox/LeftSide/BodyHBox/ContentPanel/ContentStack/RankListContent"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="RankListVBox" type="VBoxContainer" parent="MainMargin/MainHBox/LeftSide/BodyHBox/ContentPanel/ContentStack/RankListContent/RankScroll"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 4 + +[node name="TabBtnVBox" type="VBoxContainer" parent="MainMargin/MainHBox/LeftSide/BodyHBox"] +custom_minimum_size = Vector2(90, 0) +layout_mode = 2 +theme_override_constants/separation = 8 + +[node name="TabSpacer" type="Control" parent="MainMargin/MainHBox/LeftSide/BodyHBox/TabBtnVBox"] +custom_minimum_size = Vector2(0, 30) +layout_mode = 2 + +[node name="RaceResultTabBtn" type="Button" parent="MainMargin/MainHBox/LeftSide/BodyHBox/TabBtnVBox"] +unique_name_in_owner = true +custom_minimum_size = Vector2(85, 50) +layout_mode = 2 +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 12 +text = "RACE +RESULT" + +[node name="RankListTabBtn" type="Button" parent="MainMargin/MainHBox/LeftSide/BodyHBox/TabBtnVBox"] +unique_name_in_owner = true +custom_minimum_size = Vector2(85, 50) +layout_mode = 2 +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 12 +text = "RANK +LIST" + +[node name="BottomBar" type="HBoxContainer" parent="MainMargin/MainHBox/LeftSide"] +custom_minimum_size = Vector2(0, 50) +layout_mode = 2 +theme_override_constants/separation = 20 + +[node name="BackBtn" type="Button" parent="MainMargin/MainHBox/LeftSide/BottomBar"] +unique_name_in_owner = true +custom_minimum_size = Vector2(120, 48) +layout_mode = 2 +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 20 +theme_override_styles/normal = SubResource("StyleBoxFlat_back_normal") +theme_override_styles/hover = SubResource("StyleBoxFlat_back_hover") +theme_override_styles/pressed = SubResource("StyleBoxFlat_back_pressed") +text = "BACK" + +[node name="BottomSpacer" type="Control" parent="MainMargin/MainHBox/LeftSide/BottomBar"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="RightSide" type="VBoxContainer" parent="MainMargin/MainHBox"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_stretch_ratio = 1.0 +theme_override_constants/separation = 8 +alignment = 1 + +[node name="ViewportContainer" type="SubViewportContainer" parent="MainMargin/MainHBox/RightSide"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +stretch = true + +[node name="PreviewViewport" type="SubViewport" parent="MainMargin/MainHBox/RightSide/ViewportContainer"] +transparent_bg = true +handle_input_locally = false +size = Vector2i(400, 500) +render_target_update_mode = 4 + +[node name="WorldEnvironment" type="WorldEnvironment" parent="MainMargin/MainHBox/RightSide/ViewportContainer/PreviewViewport"] +environment = SubResource("Environment_preview") + +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="MainMargin/MainHBox/RightSide/ViewportContainer/PreviewViewport"] +transform = Transform3D(0.866025, -0.25, 0.433013, 0, 0.866025, 0.5, -0.5, -0.433013, 0.75, 0, 4, 0) +light_energy = 1.4 + +[node name="FillLight" type="OmniLight3D" parent="MainMargin/MainHBox/RightSide/ViewportContainer/PreviewViewport"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2, 2, 2) +light_color = Color(0.4, 0.5, 1, 1) +light_energy = 0.5 +omni_range = 8.0 + +[node name="PreviewCamera" type="Camera3D" parent="MainMargin/MainHBox/RightSide/ViewportContainer/PreviewViewport"] +transform = Transform3D(1, 0, 0, 0, 0.9659259, 0.25881898, 0, -0.25881898, 0.9659259, 0, 0.8380414, 3.2) +current = true +fov = 40.0 + +[node name="CharacterRoot" type="Node3D" parent="MainMargin/MainHBox/RightSide/ViewportContainer/PreviewViewport"] +unique_name_in_owner = true + +[node name="Masbro" parent="MainMargin/MainHBox/RightSide/ViewportContainer/PreviewViewport/CharacterRoot" instance=ExtResource("5_masbro")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.485, 0) +visible = false + +[node name="Bob" parent="MainMargin/MainHBox/RightSide/ViewportContainer/PreviewViewport/CharacterRoot" instance=ExtResource("3_bob")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.485, 0) +visible = false + +[node name="Gatot" parent="MainMargin/MainHBox/RightSide/ViewportContainer/PreviewViewport/CharacterRoot" instance=ExtResource("4_gatot")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.485, 0) +visible = false + +[node name="Oldpop" parent="MainMargin/MainHBox/RightSide/ViewportContainer/PreviewViewport/CharacterRoot" instance=ExtResource("6_oldpop")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.485, 0) + +[node name="AnimationPlayer" type="AnimationPlayer" parent="MainMargin/MainHBox/RightSide/ViewportContainer/PreviewViewport/CharacterRoot"] +root_node = NodePath("../Oldpop") +libraries/animation-pack = ExtResource("7_animlib") +autoplay = &"animation-pack/idle" + +[node name="CharNameLabel" type="Label" parent="MainMargin/MainHBox/RightSide"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_colors/font_color = Color(1, 1, 1, 1) +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 26 +text = "PLAYER" +horizontal_alignment = 1 + +[node name="XPBarHBox" type="HBoxContainer" parent="MainMargin/MainHBox/RightSide"] +layout_mode = 2 +theme_override_constants/separation = 8 +alignment = 1 + +[node name="XPIcon" type="ColorRect" parent="MainMargin/MainHBox/RightSide/XPBarHBox"] +custom_minimum_size = Vector2(24, 24) +layout_mode = 2 +color = Color(0.85, 0.65, 0.1, 1) + +[node name="XPBarBg" type="PanelContainer" parent="MainMargin/MainHBox/RightSide/XPBarHBox"] +custom_minimum_size = Vector2(180, 20) +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_xp_bg") + +[node name="XPFill" type="ColorRect" parent="MainMargin/MainHBox/RightSide/XPBarHBox/XPBarBg"] +custom_minimum_size = Vector2(120, 16) +layout_mode = 2 +color = Color(0.85, 0.65, 0.1, 1) + +[node name="RematchHBox" type="HBoxContainer" parent="MainMargin/MainHBox/RightSide"] +layout_mode = 2 +theme_override_constants/separation = 10 +alignment = 2 + +[node name="RematchBtn" type="Button" parent="MainMargin/MainHBox/RightSide/RematchHBox"] +unique_name_in_owner = true +custom_minimum_size = Vector2(160, 48) +layout_mode = 2 +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 20 +theme_override_styles/normal = SubResource("StyleBoxFlat_rematch_normal") +theme_override_styles/hover = SubResource("StyleBoxFlat_rematch_hover") +theme_override_styles/pressed = SubResource("StyleBoxFlat_rematch_pressed") +text = "REMATCH" + +[node name="RematchVoteLabel" type="Label" parent="MainMargin/MainHBox/RightSide/RematchHBox"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_colors/font_color = Color(0.7, 0.7, 0.7, 1) +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 18 diff --git a/scripts/managers/lobby_manager.gd b/scripts/managers/lobby_manager.gd index c6e24f5..13ce74e 100644 --- a/scripts/managers/lobby_manager.gd +++ b/scripts/managers/lobby_manager.gd @@ -844,20 +844,26 @@ func _on_server_disconnected() -> void: # Rematch Logic # ============================================================================= +func _get_connected_human_count() -> int: + """Returns the number of connected real human players (peers + host, excluding bots).""" + if multiplayer.has_multiplayer_peer(): + return multiplayer.get_peers().size() + 1 # peers + self (host) + return max(1, players_in_room.size()) # Fallback to lobby list + func reset_rematch_votes() -> void: rematch_votes.clear() - var required = max(1, ceili(players_in_room.size() / 2.0)) + var required = max(1, ceili(_get_connected_human_count() / 2.0)) emit_signal("rematch_votes_updated", 0, required) @rpc("any_peer", "call_local", "reliable") func request_rematch(player_id: int) -> void: - """Client requests a rematch. Votes needed depend on connected real players.""" + """Client requests a rematch. Votes needed depend on connected real players (excluding bots).""" if not multiplayer.is_server(): return if player_id not in rematch_votes: rematch_votes.append(player_id) - var required = max(1, ceili(players_in_room.size() / 2.0)) + var required = max(1, ceili(_get_connected_human_count() / 2.0)) print("[LobbyManager] Rematch vote from %d. Total: %d/%d" % [player_id, rematch_votes.size(), required]) # Sync vote count to all clients diff --git a/scripts/ui/game_over_panel.gd b/scripts/ui/game_over_panel.gd new file mode 100644 index 0000000..968138d --- /dev/null +++ b/scripts/ui/game_over_panel.gd @@ -0,0 +1,228 @@ +extends Control +## End-of-game modal panel with tabbed Race Result / Rank List and 3D character preview. +## Instantiated by main.gd at match end. Call setup() to populate data. + +signal back_pressed +signal rematch_pressed + +# ------------------------------------------------------------------------- +# UI References (unique names from .tscn) +# ------------------------------------------------------------------------- + +# Header +@onready var tab_title_label := %TabTitleLabel as Label + +# Race Result tab +@onready var race_result_content := %RaceResultContent as VBoxContainer +@onready var player_rank_label := %PlayerRankLabel as Label +@onready var player_name_label := %PlayerNameLabel as Label +@onready var player_score_label := %PlayerScoreLabel as Label +@onready var completion_value := %CompletionValue as Label +@onready var score_value := %ScoreValue as Label +@onready var rank_value := %RankValue as Label + +# Rank List tab +@onready var rank_list_content := %RankListContent as VBoxContainer +@onready var winner_rank_label := %WinnerRankLabel as Label +@onready var winner_name_label := %WinnerNameLabel as Label +@onready var winner_score_label := %WinnerScoreLabel as Label +@onready var rank_list_vbox := %RankListVBox as VBoxContainer + +# Tab Buttons +@onready var race_result_tab_btn := %RaceResultTabBtn as Button +@onready var rank_list_tab_btn := %RankListTabBtn as Button + +# Action Buttons +@onready var back_btn := %BackBtn as Button +@onready var rematch_btn := %RematchBtn as Button +@onready var rematch_vote_label := %RematchVoteLabel as Label + +# 3D Preview +@onready var character_root := %CharacterRoot as Node3D +@onready var char_name_label := %CharNameLabel as Label + +# ------------------------------------------------------------------------- +# State +# ------------------------------------------------------------------------- +var _anim_player: AnimationPlayer +var _nougat_font: Font + +# Maps lobby character name -> GLB node name in the SubViewport +const CHAR_NODE_MAP: Dictionary = { + "Copper": "Oldpop", + "Dabro": "Masbro", + "Gatot": "Gatot", + "Pip": "Bob" +} + +# ------------------------------------------------------------------------- +# Lifecycle +# ------------------------------------------------------------------------- + +func _ready() -> void: + _nougat_font = load("res://assets/fonts/Nougat-ExtraBlack.ttf") + + # Tab button connections + race_result_tab_btn.pressed.connect(func(): _switch_tab(true)) + rank_list_tab_btn.pressed.connect(func(): _switch_tab(false)) + + # Action button connections + back_btn.pressed.connect(func(): emit_signal("back_pressed")) + rematch_btn.pressed.connect(func(): + rematch_btn.disabled = true + rematch_btn.text = "VOTED" + emit_signal("rematch_pressed") + ) + + # Setup 3D preview + _setup_3d_preview() + + # Default tab: Race Result + _switch_tab(true) + +# ------------------------------------------------------------------------- +# Tab Switching +# ------------------------------------------------------------------------- + +func _switch_tab(show_race_result: bool) -> void: + race_result_content.visible = show_race_result + rank_list_content.visible = not show_race_result + tab_title_label.text = "RACE RESULT" if show_race_result else "RANK LIST" + +# ------------------------------------------------------------------------- +# Data Population (called by main.gd) +# ------------------------------------------------------------------------- + +func setup(player_scores: Array, local_peer_id: int) -> void: + """Populate all UI data. player_scores is a sorted Array of Dicts: + {peer_id, name, score, goal_count, character}""" + + # Find local player in sorted list + var local_rank := 0 + var local_score := 0 + var local_goal_count := 0 + var local_name := "Player" + var local_character := "Masbro" + + for i in range(player_scores.size()): + if player_scores[i].peer_id == local_peer_id: + local_rank = i + local_score = player_scores[i].score + local_goal_count = player_scores[i].get("goal_count", 0) + local_name = player_scores[i].name + local_character = player_scores[i].get("character", "Masbro") + break + + # --- Race Result tab --- + player_rank_label.text = _get_ordinal(local_rank + 1) + player_name_label.text = local_name + player_score_label.text = str(local_score) + completion_value.text = "%dx" % local_goal_count + score_value.text = str(local_score) + rank_value.text = _get_ordinal(local_rank + 1) + + # Rank color + var rank_color := Color(0.95, 0.75, 0.1) # Gold + if local_rank == 1: rank_color = Color(0.75, 0.75, 0.75) # Silver + elif local_rank == 2: rank_color = Color(0.8, 0.5, 0.2) # Bronze + elif local_rank > 2: rank_color = Color(0.6, 0.6, 0.6) + rank_value.add_theme_color_override("font_color", rank_color) + player_rank_label.add_theme_color_override("font_color", rank_color) + + # --- Rank List tab --- + if player_scores.size() > 0: + winner_rank_label.text = "1st" + winner_name_label.text = player_scores[0].name + winner_score_label.text = str(player_scores[0].score) + + # Populate rank list rows + for child in rank_list_vbox.get_children(): + child.queue_free() + + for i in range(player_scores.size()): + var pdata = player_scores[i] + var is_local = pdata.peer_id == local_peer_id + _add_rank_list_entry(i, pdata, is_local) + + # --- 3D Character Preview --- + _update_3d_preview(local_character) + char_name_label.text = local_name.to_upper() + +func _add_rank_list_entry(index: int, pdata: Dictionary, is_local: bool) -> void: + var row = HBoxContainer.new() + row.add_theme_constant_override("separation", 16) + + var num_lbl = Label.new() + num_lbl.text = str(index + 1) + num_lbl.custom_minimum_size = Vector2(30, 0) + num_lbl.add_theme_font_size_override("font_size", 20) + num_lbl.add_theme_color_override("font_color", Color(0.8, 0.8, 0.8)) + if _nougat_font: num_lbl.add_theme_font_override("font", _nougat_font) + row.add_child(num_lbl) + + var pname_lbl = Label.new() + pname_lbl.text = pdata.name + pname_lbl.add_theme_font_size_override("font_size", 20) + if is_local: + pname_lbl.add_theme_color_override("font_color", Color(0.4, 0.8, 1.0)) + else: + pname_lbl.add_theme_color_override("font_color", Color.WHITE) + if _nougat_font: pname_lbl.add_theme_font_override("font", _nougat_font) + pname_lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL + row.add_child(pname_lbl) + + var pscore_lbl = Label.new() + pscore_lbl.text = str(pdata.score) + pscore_lbl.add_theme_font_size_override("font_size", 20) + pscore_lbl.add_theme_color_override("font_color", Color.WHITE) + if _nougat_font: pscore_lbl.add_theme_font_override("font", _nougat_font) + pscore_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT + row.add_child(pscore_lbl) + + rank_list_vbox.add_child(row) + +# ------------------------------------------------------------------------- +# Rematch Vote Update (called by main.gd) +# ------------------------------------------------------------------------- + +func update_rematch_votes(count: int, required: int) -> void: + if is_instance_valid(rematch_vote_label): + rematch_vote_label.text = "%d/%d" % [count, required] + if count >= required: + rematch_vote_label.add_theme_color_override("font_color", Color.GREEN) + +# ------------------------------------------------------------------------- +# 3D Preview +# ------------------------------------------------------------------------- + +func _setup_3d_preview() -> void: + if not character_root: + return + _anim_player = character_root.get_node_or_null("AnimationPlayer") + +func _update_3d_preview(character_name: String) -> void: + if not character_root: + return + var node_name: String = CHAR_NODE_MAP.get(character_name, character_name) + for child in character_root.get_children(): + if child is Node3D: + child.visible = (child.name == node_name) + if _anim_player: + var new_root := character_root.get_node_or_null(node_name) + if new_root: + _anim_player.root_node = _anim_player.get_path_to(new_root) + if _anim_player.has_animation("animation-pack/idle"): + _anim_player.play("animation-pack/idle") + elif _anim_player.get_animation_list().size() > 0: + _anim_player.play(_anim_player.get_animation_list()[0]) + +# ------------------------------------------------------------------------- +# Helpers +# ------------------------------------------------------------------------- + +func _get_ordinal(n: int) -> String: + match n: + 1: return "1st" + 2: return "2nd" + 3: return "3rd" + _: return str(n) + "th" diff --git a/scripts/ui/game_over_panel.gd.uid b/scripts/ui/game_over_panel.gd.uid new file mode 100644 index 0000000..44ca733 --- /dev/null +++ b/scripts/ui/game_over_panel.gd.uid @@ -0,0 +1 @@ +uid://b100tjl67tbh7