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 Panel # Content Panel @onready var content_panel := %ContentPanel as PanelContainer # 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 if tab_title_label: tab_title_label.text = "RACE RESULT" if show_race_result else "RANK LIST" # Tab button styles var tex_avail_normal = preload("res://assets/graphics/gui/game_over_panel/Button_avail.png") var tex_avail_click = preload("res://assets/graphics/gui/game_over_panel/Button_avail_click.png") var tex_avail_hover = preload("res://assets/graphics/gui/game_over_panel/Button_avail_expand.png") var tex_unavail_normal = preload("res://assets/graphics/gui/game_over_panel/Button_unavail.png") var tex_unavail_click = preload("res://assets/graphics/gui/game_over_panel/Button_unavail_click.png") if show_race_result: race_result_tab_btn.add_theme_stylebox_override("normal", _get_stylebox(tex_avail_normal)) race_result_tab_btn.add_theme_stylebox_override("pressed", _get_stylebox(tex_avail_click)) race_result_tab_btn.add_theme_stylebox_override("hover", _get_stylebox(tex_avail_hover)) rank_list_tab_btn.add_theme_stylebox_override("normal", _get_stylebox(tex_unavail_normal)) rank_list_tab_btn.add_theme_stylebox_override("pressed", _get_stylebox(tex_unavail_click)) rank_list_tab_btn.add_theme_stylebox_override("hover", _get_stylebox(tex_unavail_normal)) else: race_result_tab_btn.add_theme_stylebox_override("normal", _get_stylebox(tex_unavail_normal)) race_result_tab_btn.add_theme_stylebox_override("pressed", _get_stylebox(tex_unavail_click)) race_result_tab_btn.add_theme_stylebox_override("hover", _get_stylebox(tex_unavail_normal)) rank_list_tab_btn.add_theme_stylebox_override("normal", _get_stylebox(tex_avail_normal)) rank_list_tab_btn.add_theme_stylebox_override("pressed", _get_stylebox(tex_avail_click)) rank_list_tab_btn.add_theme_stylebox_override("hover", _get_stylebox(tex_avail_hover)) # Content Panel Background var bg_tex = preload("res://assets/graphics/gui/game_over_panel/raceresult.png") if show_race_result else preload("res://assets/graphics/gui/game_over_panel/ranklist.png") var bg_style = StyleBoxTexture.new() bg_style.texture = bg_tex # Must match margins from tscn: left 50, top 180, right 80, bottom 20 bg_style.content_margin_left = 50 bg_style.content_margin_top = 180 bg_style.content_margin_right = 80 bg_style.content_margin_bottom = 20 content_panel.add_theme_stylebox_override("panel", bg_style) func _get_stylebox(tex: Texture2D) -> StyleBoxTexture: var sb = StyleBoxTexture.new() sb.texture = tex return sb # ------------------------------------------------------------------------- # 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) # Update RankValue Panel texture based on rank var rank_str = _get_ordinal(local_rank + 1) var rank_tex_path = "res://assets/graphics/gui/game_over_panel/%s.png" % rank_str if ResourceLoader.exists(rank_tex_path): var rank_tex = load(rank_tex_path) var rank_style = StyleBoxTexture.new() rank_style.texture = rank_tex rank_value.add_theme_stylebox_override("panel", rank_style) # 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) 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") _set_layers_recursive(character_root, 512) func _set_layers_recursive(node: Node, layer_mask: int) -> void: if node is VisualInstance3D: node.layers = layer_mask for child in node.get_children(): _set_layers_recursive(child, layer_mask) 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"