287 lines
11 KiB
GDScript
287 lines
11 KiB
GDScript
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"
|