feat: 2.3.2
This commit is contained in:
+135
-3
@@ -20,6 +20,9 @@ signal closed
|
||||
@onready var ban_btn := %BanBtn as Button
|
||||
@onready var unban_btn := %UnbanBtn as Button
|
||||
@onready var delete_btn := %DeleteBtn as Button
|
||||
@onready var history_btn := %HistoryBtn as Button
|
||||
@onready var history_dialog := %HistoryDialog as AcceptDialog
|
||||
@onready var history_text := %HistoryText as RichTextLabel
|
||||
|
||||
# Tab: Leaderboards
|
||||
@onready var lb_tree := %LeaderboardTree as Tree
|
||||
@@ -61,6 +64,11 @@ var _resolved_user_id: String = ""
|
||||
var _mail_root: TreeItem
|
||||
var _all_server_mails: Array = []
|
||||
|
||||
# Tab: Shop (Featured Banners)
|
||||
@onready var slots_vbox := %SlotsVBox as VBoxContainer
|
||||
@onready var load_banners_btn := %LoadBannersBtn as Button
|
||||
@onready var save_banners_btn := %SaveBannersBtn as Button
|
||||
|
||||
const MONTH_NAMES = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
|
||||
|
||||
# -- Data --
|
||||
@@ -178,6 +186,7 @@ func _connect_signals() -> void:
|
||||
ban_btn.pressed.connect(_on_ban)
|
||||
unban_btn.pressed.connect(_on_unban)
|
||||
delete_btn.pressed.connect(_on_delete)
|
||||
history_btn.pressed.connect(_on_history_pressed)
|
||||
user_tree.item_edited.connect(_on_user_tree_item_edited)
|
||||
user_tree.button_clicked.connect(_on_user_tree_button_clicked)
|
||||
|
||||
@@ -204,6 +213,10 @@ func _connect_signals() -> void:
|
||||
delete_mail_server_btn.pressed.connect(_on_delete_mail_server_pressed)
|
||||
_update_mail_action_btns(null)
|
||||
|
||||
# Shop actions
|
||||
load_banners_btn.pressed.connect(func(): await _load_featured_banners())
|
||||
save_banners_btn.pressed.connect(func(): await _save_featured_banners())
|
||||
|
||||
# =============================================================================
|
||||
# Core Panel Logic
|
||||
# =============================================================================
|
||||
@@ -228,6 +241,8 @@ func _on_tab_changed(tab_index: int) -> void:
|
||||
await _load_daily_rewards_config()
|
||||
elif tab_index == 4:
|
||||
await _load_mail()
|
||||
elif tab_index == 5:
|
||||
await _load_featured_banners()
|
||||
|
||||
# =============================================================================
|
||||
# RPC Helper
|
||||
@@ -262,7 +277,8 @@ func _load_users() -> void:
|
||||
_set_status("Failed: " + str(res.error), CLR_STATUS_ERR)
|
||||
return
|
||||
|
||||
all_users = res.get("users", [])
|
||||
var raw_users = res.get("users", [])
|
||||
all_users = raw_users if typeof(raw_users) == TYPE_ARRAY else []
|
||||
count_label.text = "%d users" % all_users.size()
|
||||
|
||||
for user in all_users:
|
||||
@@ -464,6 +480,63 @@ func _on_unban() -> void:
|
||||
_set_status("Unbanned %d/%d" % [ok, to_unban.size()], CLR_STATUS_OK)
|
||||
await _load_users()
|
||||
|
||||
func _on_history_pressed() -> void:
|
||||
var selected_data = _get_checked_user_data()
|
||||
if selected_data.size() != 1:
|
||||
_set_status("Please select exactly ONE user to view history.", CLR_STATUS_ERR)
|
||||
return
|
||||
|
||||
var uid = selected_data[0].get("user_id", "")
|
||||
_set_status("Fetching history for user...", CLR_STATUS_OK)
|
||||
var res = await _rpc("admin_get_user_history", {"user_id": uid})
|
||||
|
||||
if res.has("error"):
|
||||
_set_status("Failed to get history: " + str(res.error), CLR_STATUS_ERR)
|
||||
return
|
||||
|
||||
_set_status("History loaded.", CLR_STATUS_OK)
|
||||
|
||||
var h = res.get("history", {})
|
||||
var text = "[b]=== USER HISTORY ===[/b]\n"
|
||||
text += "User ID: " + uid + "\n\n"
|
||||
|
||||
# Logins
|
||||
text += "[b]-- Recent Logins --[/b]\n"
|
||||
var logins = h.get("logins", [])
|
||||
if logins.is_empty():
|
||||
text += "No recent logins found.\n"
|
||||
else:
|
||||
for l in logins:
|
||||
var time_str = Time.get_datetime_string_from_unix_time(int(l.get("time", 0)))
|
||||
text += "- %s (IP: %s)\n" % [time_str, l.get("ip", "unknown")]
|
||||
|
||||
text += "\n"
|
||||
|
||||
# Wallet Ledger
|
||||
text += "[b]-- Economy / Wallet Ledger --[/b]\n"
|
||||
var ledger = h.get("wallet_ledger", [])
|
||||
if ledger.is_empty():
|
||||
text += "No transactions found.\n"
|
||||
else:
|
||||
for item in ledger:
|
||||
var changeset = str(item.get("changeset", {}))
|
||||
var c_time = item.get("create_time", "")
|
||||
text += "- [%s] %s\n" % [c_time.left(19).replace("T", " "), changeset]
|
||||
|
||||
text += "\n"
|
||||
|
||||
# Matches
|
||||
text += "[b]-- Matches --[/b]\n"
|
||||
var matches = h.get("matches", [])
|
||||
if matches.is_empty():
|
||||
text += "No match history found.\n"
|
||||
else:
|
||||
for m in matches:
|
||||
text += "- " + str(m) + "\n"
|
||||
|
||||
history_text.text = text
|
||||
history_dialog.popup_centered()
|
||||
|
||||
func _on_delete() -> void:
|
||||
var users := _get_checked_user_data()
|
||||
if users.is_empty(): return
|
||||
@@ -498,7 +571,8 @@ func _load_leaderboard() -> void:
|
||||
_set_status("Failed to load scores", CLR_STATUS_ERR)
|
||||
return
|
||||
|
||||
lb_data = res.get("leaderboard", [])
|
||||
var raw_lb = res.get("leaderboard", [])
|
||||
lb_data = raw_lb if typeof(raw_lb) == TYPE_ARRAY else []
|
||||
count_label.text = "%d records" % lb_data.size()
|
||||
lb_data.sort_custom(func(a, b): return a.get("high_score", 0) > b.get("high_score", 0))
|
||||
|
||||
@@ -824,7 +898,8 @@ func _load_mail() -> void:
|
||||
_set_status("Failed: " + str(res.error), CLR_STATUS_ERR)
|
||||
return
|
||||
|
||||
_all_server_mails = res.get("mails", [])
|
||||
var raw_mails = res.get("mails", [])
|
||||
_all_server_mails = raw_mails if typeof(raw_mails) == TYPE_ARRAY else []
|
||||
count_label.text = "%d mails" % _all_server_mails.size()
|
||||
|
||||
var now_str = Time.get_datetime_string_from_system(true)
|
||||
@@ -1065,3 +1140,60 @@ func _on_delete_mail_server_pressed() -> void:
|
||||
await _load_mail()
|
||||
confirm.queue_free()
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# TAB 6: SHOP — FEATURED BANNERS
|
||||
# =============================================================================
|
||||
var _slot_nodes: Array = [] # cached references to the 3 slot HBoxContainers
|
||||
|
||||
func _get_slot_nodes() -> Array:
|
||||
if _slot_nodes.is_empty():
|
||||
for child in slots_vbox.get_children():
|
||||
if child is HBoxContainer:
|
||||
_slot_nodes.append(child)
|
||||
return _slot_nodes
|
||||
|
||||
func _load_featured_banners() -> void:
|
||||
_set_status("Loading banners...")
|
||||
var res := await _rpc("admin_get_featured_banners", {})
|
||||
if res.has("error"):
|
||||
_set_status("Failed: " + str(res.error), CLR_STATUS_ERR)
|
||||
return
|
||||
|
||||
var raw_banners = res.get("banners", [])
|
||||
var banners: Array = raw_banners if typeof(raw_banners) == TYPE_ARRAY else []
|
||||
var slots := _get_slot_nodes()
|
||||
|
||||
for i in range(slots.size()):
|
||||
var slot: HBoxContainer = slots[i]
|
||||
var id_edit: LineEdit = slot.get_node("ItemIdEdit") as LineEdit
|
||||
var lbl_edit: LineEdit = slot.get_node("LabelEdit") as LineEdit
|
||||
if i < banners.size():
|
||||
var b: Dictionary = banners[i] if banners[i] is Dictionary else {}
|
||||
id_edit.text = b.get("item_id", "")
|
||||
lbl_edit.text = b.get("label", "")
|
||||
else:
|
||||
id_edit.text = ""
|
||||
lbl_edit.text = ""
|
||||
|
||||
count_label.text = "%d banners configured" % banners.size()
|
||||
_set_status("Banners loaded", CLR_STATUS_OK)
|
||||
|
||||
func _save_featured_banners() -> void:
|
||||
var banners: Array = []
|
||||
var slots := _get_slot_nodes()
|
||||
|
||||
for slot in slots:
|
||||
var id_edit: LineEdit = slot.get_node("ItemIdEdit") as LineEdit
|
||||
var lbl_edit: LineEdit = slot.get_node("LabelEdit") as LineEdit
|
||||
var item_id: String = id_edit.text.strip_edges()
|
||||
var label: String = lbl_edit.text.strip_edges()
|
||||
if not item_id.is_empty():
|
||||
banners.append({"item_id": item_id, "label": label})
|
||||
|
||||
_set_status("Saving banners...")
|
||||
var res := await _rpc("admin_set_featured_banners", {"banners": banners})
|
||||
if res.has("error"):
|
||||
_set_status("Save failed: " + str(res.error), CLR_STATUS_ERR)
|
||||
elif res.has("success"):
|
||||
_set_status("Banners saved! (%d slots)" % banners.size(), CLR_STATUS_OK)
|
||||
|
||||
@@ -75,7 +75,7 @@ func _on_update_check_completed(has_update: bool, info: Dictionary) -> void:
|
||||
button_container.visible = true
|
||||
update_button.visible = true
|
||||
skip_button.visible = true
|
||||
skip_button_label.text = "Play without updating"
|
||||
skip_button_label.text = "Force Play"
|
||||
else:
|
||||
status_label.text = "Game up to date."
|
||||
button_container.visible = true
|
||||
|
||||
@@ -5,10 +5,12 @@ extends Control
|
||||
signal closed
|
||||
|
||||
# ─── Node refs ───────────────────────────────────────────────────────────────
|
||||
@onready var back_btn := %BackBtn as Button
|
||||
@onready var recipe_list := %RecipeList as VBoxContainer
|
||||
@onready var status_label := %StatusLabel as Label
|
||||
@onready var frag_balance := %FragBalance as Label
|
||||
@onready var back_btn := %BackBtn as Button
|
||||
@onready var recipe_list := %RecipeList as VBoxContainer
|
||||
@onready var status_label := %StatusLabel as Label
|
||||
@onready var common_label := %CommonLabel as Label
|
||||
@onready var uncommon_label := %UncommonLabel as Label
|
||||
@onready var rare_label := %RareLabel as Label
|
||||
|
||||
const FRAG_ICONS := {
|
||||
"frag_common": "⬜",
|
||||
@@ -31,12 +33,9 @@ func _refresh() -> void:
|
||||
# ─── Fragment balance header ──────────────────────────────────────────────────
|
||||
func _update_frag_balance() -> void:
|
||||
var frags: Dictionary = UserProfileManager.fragments
|
||||
var parts: Array = []
|
||||
for fid in ["frag_common", "frag_uncommon", "frag_rare"]:
|
||||
var icon: String = FRAG_ICONS.get(fid, "?")
|
||||
var count: int = frags.get(fid, 0)
|
||||
parts.append("%s ×%d" % [icon, count])
|
||||
frag_balance.text = " ".join(parts)
|
||||
common_label.text = str(frags.get("frag_common", 0))
|
||||
uncommon_label.text = str(frags.get("frag_uncommon", 0))
|
||||
rare_label.text = str(frags.get("frag_rare", 0))
|
||||
|
||||
# ─── Recipe cards ─────────────────────────────────────────────────────────────
|
||||
func _rebuild_recipe_list() -> void:
|
||||
@@ -55,6 +54,18 @@ func _make_recipe_card(recipe_id: String, recipe: Dictionary) -> PanelContainer:
|
||||
|
||||
var panel := PanelContainer.new()
|
||||
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
|
||||
# Apply Tekton panel style
|
||||
var panel_style := StyleBoxFlat.new()
|
||||
panel_style.bg_color = Color(0.14117648, 0.16862746, 0.19215687, 1)
|
||||
panel_style.corner_radius_top_left = 12
|
||||
panel_style.corner_radius_top_right = 12
|
||||
panel_style.corner_radius_bottom_right = 12
|
||||
panel_style.corner_radius_bottom_left = 12
|
||||
panel_style.shadow_color = Color(0, 0, 0, 0.3529412)
|
||||
panel_style.shadow_size = 4
|
||||
panel_style.shadow_offset = Vector2(-2, 2)
|
||||
panel.add_theme_stylebox_override("panel", panel_style)
|
||||
|
||||
var margin := MarginContainer.new()
|
||||
margin.add_theme_constant_override("margin_left", 14)
|
||||
@@ -103,11 +114,20 @@ func _make_recipe_card(recipe_id: String, recipe: Dictionary) -> PanelContainer:
|
||||
Color(0.4, 1.0, 0.5) if have >= needed else Color(1.0, 0.4, 0.4))
|
||||
cost_hbox.add_child(cost_lbl)
|
||||
|
||||
# Craft button
|
||||
# Craft button with Tekton dark style
|
||||
var craft_btn := Button.new()
|
||||
craft_btn.text = "🔨 Craft"
|
||||
craft_btn.custom_minimum_size = Vector2(100, 40)
|
||||
craft_btn.disabled = not can_craft
|
||||
|
||||
var btn_style := StyleBoxFlat.new()
|
||||
btn_style.bg_color = Color(0.15, 0.15, 0.15, 1)
|
||||
btn_style.corner_radius_top_left = 8
|
||||
btn_style.corner_radius_top_right = 8
|
||||
btn_style.corner_radius_bottom_right = 8
|
||||
btn_style.corner_radius_bottom_left = 8
|
||||
craft_btn.add_theme_stylebox_override("normal", btn_style)
|
||||
|
||||
if not can_craft:
|
||||
craft_btn.modulate = Color(0.5, 0.5, 0.5, 0.7)
|
||||
craft_btn.pressed.connect(_on_craft_pressed.bind(recipe_id, panel))
|
||||
|
||||
@@ -10,7 +10,8 @@ signal closed
|
||||
@onready var star_tab_btn := %StarTabBtn as Button
|
||||
@onready var gold_tab_btn := %GoldTabBtn as Button
|
||||
@onready var banner_label := %BannerLabel as Label
|
||||
@onready var balance_label := %BalanceLabel as Label
|
||||
@onready var gold_label := %GoldLabel as Label
|
||||
@onready var star_label := %StarLabel as Label
|
||||
@onready var pity_label := %PityLabel as Label
|
||||
@onready var pull_1_btn := %Pull1Btn as Button
|
||||
@onready var pull_10_btn := %Pull10Btn as Button
|
||||
@@ -88,8 +89,40 @@ func _ensure_dummy_wallet() -> void:
|
||||
# ─── Banner switching ─────────────────────────────────────────────────────────
|
||||
func _switch_banner(id: String) -> void:
|
||||
_current_banner = id
|
||||
star_tab_btn.modulate = Color(1.3, 1.1, 0.3) if id == "star" else Color.WHITE
|
||||
gold_tab_btn.modulate = Color(1.3, 1.1, 0.3) if id == "gold" else Color.WHITE
|
||||
|
||||
# Create active tab style (dark blue)
|
||||
var active_style := StyleBoxFlat.new()
|
||||
active_style.bg_color = Color(0.1, 0.19, 0.27, 1)
|
||||
active_style.content_margin_left = 16.0
|
||||
active_style.content_margin_top = 14.0
|
||||
active_style.content_margin_right = 16.0
|
||||
active_style.content_margin_bottom = 14.0
|
||||
active_style.corner_radius_top_left = 8
|
||||
active_style.corner_radius_top_right = 8
|
||||
active_style.corner_radius_bottom_right = 8
|
||||
active_style.corner_radius_bottom_left = 8
|
||||
|
||||
# Create inactive tab style (cyan)
|
||||
var inactive_style := StyleBoxFlat.new()
|
||||
inactive_style.bg_color = Color(0.33, 0.62, 0.78, 1)
|
||||
inactive_style.content_margin_left = 16.0
|
||||
inactive_style.content_margin_top = 14.0
|
||||
inactive_style.content_margin_right = 16.0
|
||||
inactive_style.content_margin_bottom = 14.0
|
||||
inactive_style.corner_radius_top_left = 8
|
||||
inactive_style.corner_radius_top_right = 8
|
||||
inactive_style.corner_radius_bottom_right = 8
|
||||
inactive_style.corner_radius_bottom_left = 8
|
||||
|
||||
# Apply styles
|
||||
star_tab_btn.add_theme_stylebox_override("normal", active_style if id == "star" else inactive_style)
|
||||
star_tab_btn.add_theme_stylebox_override("hover", active_style if id == "star" else inactive_style)
|
||||
star_tab_btn.add_theme_stylebox_override("pressed", active_style if id == "star" else inactive_style)
|
||||
|
||||
gold_tab_btn.add_theme_stylebox_override("normal", active_style if id == "gold" else inactive_style)
|
||||
gold_tab_btn.add_theme_stylebox_override("hover", active_style if id == "gold" else inactive_style)
|
||||
gold_tab_btn.add_theme_stylebox_override("pressed", active_style if id == "gold" else inactive_style)
|
||||
|
||||
_refresh_ui()
|
||||
|
||||
func _refresh_ui() -> void:
|
||||
@@ -107,7 +140,11 @@ func _refresh_ui() -> void:
|
||||
var rates: Dictionary = banner.get("rates", {})
|
||||
|
||||
banner_label.text = banner.get("name", "Banner")
|
||||
balance_label.text = "%s %d" % [icon, bal]
|
||||
|
||||
# Update both gold and star labels
|
||||
star_label.text = str(UserProfileManager.wallet.get("star", 0))
|
||||
gold_label.text = str(UserProfileManager.wallet.get("gold", 0))
|
||||
|
||||
pity_label.text = "Pity: %d / %d" % [pity, pity_at]
|
||||
cost_1_label.text = "%s %d" % [icon, c1]
|
||||
cost_10_label.text = "%s %d" % [icon, c10]
|
||||
|
||||
@@ -10,17 +10,19 @@ signal closed
|
||||
# -------------------------------------------------------------------------
|
||||
@onready var back_btn := %BackBtn as Button
|
||||
@onready var refresh_btn := %RefreshBtn as Button
|
||||
@onready var sync_btn := %SyncBtn as Button
|
||||
@onready var sort_score_btn := %SortScoreBtn as Button
|
||||
@onready var sort_win_rate_btn := %SortWinRateBtn as Button
|
||||
@onready var sort_games_btn := %SortGamesBtn as Button
|
||||
@onready var leaderboard_list := %LeaderboardList as VBoxContainer
|
||||
@onready var status_label := %StatusLabel as Label
|
||||
@onready var item_template := %ItemTemplate as PanelContainer
|
||||
|
||||
# 3D Preview
|
||||
@onready var character_root := %CharacterRoot as Node3D
|
||||
@onready var selected_name_label := %SelectedNameLabel as Label
|
||||
@onready var selected_rank_label := %SelectedRankLabel as Label
|
||||
@onready var selected_score_label := %SelectedScoreLabel as Label
|
||||
@onready var selected_avatar_rect := %SelectedAvatarRect as TextureRect
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# State
|
||||
@@ -43,13 +45,15 @@ const AVATAR_TO_CHAR: Array[String] = ["Pip", "Gatot", "Dabro", "Copper"]
|
||||
func _ready() -> void:
|
||||
back_btn.pressed.connect(_on_close_pressed)
|
||||
refresh_btn.pressed.connect(_fetch_leaderboard_data)
|
||||
sync_btn.pressed.connect(_on_sync_pressed)
|
||||
|
||||
sort_score_btn.pressed.connect(func(): _sort_by("high_score"))
|
||||
sort_win_rate_btn.pressed.connect(func(): _sort_by("win_rate"))
|
||||
sort_games_btn.pressed.connect(func(): _sort_by("games_played"))
|
||||
_update_tab_visuals()
|
||||
_setup_3d_preview()
|
||||
|
||||
if item_template:
|
||||
item_template.hide()
|
||||
|
||||
# Listen to profile and stats changes to keep the panel updated
|
||||
UserProfileManager.profile_updated.connect(_on_profile_or_stats_changed)
|
||||
@@ -79,17 +83,6 @@ func _on_close_pressed() -> void:
|
||||
hide()
|
||||
emit_signal("closed")
|
||||
|
||||
func _on_sync_pressed() -> void:
|
||||
"""Push the current player's stored stats up to the native Nakama leaderboard."""
|
||||
if not NakamaManager.session or AuthManager.is_guest:
|
||||
status_label.text = "Must be logged in to sync"
|
||||
return
|
||||
status_label.text = "Syncing your score..."
|
||||
await UserProfileManager.submit_to_leaderboard()
|
||||
status_label.text = "Synced! Refreshing..."
|
||||
await get_tree().create_timer(0.5).timeout
|
||||
_fetch_leaderboard_data()
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Data
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -99,8 +92,10 @@ func _fetch_leaderboard_data() -> void:
|
||||
return
|
||||
|
||||
status_label.text = "Fetching Leaderboard..."
|
||||
status_label.show()
|
||||
for child in leaderboard_list.get_children():
|
||||
child.queue_free()
|
||||
if child != item_template:
|
||||
child.queue_free()
|
||||
|
||||
# Try native Nakama leaderboard first (fastest, ranked already)
|
||||
var native_data = await _fetch_native_leaderboard()
|
||||
@@ -231,90 +226,76 @@ func _update_tab_visuals() -> void:
|
||||
|
||||
func _populate_list() -> void:
|
||||
for child in leaderboard_list.get_children():
|
||||
child.queue_free()
|
||||
if child != item_template:
|
||||
child.queue_free()
|
||||
|
||||
if leaderboard_data.size() == 0:
|
||||
status_label.text = "No players found.\nPlay a match to appear here!"
|
||||
status_label.show()
|
||||
return
|
||||
|
||||
status_label.hide()
|
||||
for i in range(leaderboard_data.size()):
|
||||
var entry = leaderboard_data[i]
|
||||
_create_leaderboard_item(i + 1, entry, i)
|
||||
|
||||
func _create_leaderboard_item(rank: int, entry: Dictionary, index: int) -> void:
|
||||
var item = PanelContainer.new()
|
||||
var style = StyleBoxFlat.new()
|
||||
var item = item_template.duplicate()
|
||||
item.show()
|
||||
|
||||
var style: StyleBoxFlat
|
||||
if item.has_theme_stylebox_override("panel"):
|
||||
style = item.get_theme_stylebox("panel").duplicate()
|
||||
else:
|
||||
style = StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.25, 0.3, 0.35, 1.0)
|
||||
style.set_corner_radius_all(8)
|
||||
|
||||
if index == current_selected_index:
|
||||
# Highlight color for the currently selected player row
|
||||
style.bg_color = Color(0.25, 0.35, 0.20, 1.0)
|
||||
elif rank <= 3:
|
||||
style.bg_color = Color(0.2, 0.2, 0.15, 1.0)
|
||||
style.bg_color = Color(0.35, 0.45, 0.30, 1.0)
|
||||
else:
|
||||
style.bg_color = Color(0.15, 0.15, 0.15, 1.0)
|
||||
style.bg_color = Color(0.25, 0.3, 0.35, 1.0)
|
||||
|
||||
style.set_corner_radius_all(4)
|
||||
style.content_margin_left = 10
|
||||
style.content_margin_right = 10
|
||||
style.content_margin_top = 8
|
||||
style.content_margin_bottom = 8
|
||||
item.add_theme_stylebox_override("panel", style)
|
||||
|
||||
var hbox = HBoxContainer.new()
|
||||
hbox.add_theme_constant_override("separation", 16)
|
||||
item.add_child(hbox)
|
||||
var rank_label = item.get_node("HBoxContainer/RankLabel") as Label
|
||||
if rank_label:
|
||||
var rank_suffix = "th"
|
||||
if rank % 10 == 1 and rank % 100 != 11: rank_suffix = "st"
|
||||
elif rank % 10 == 2 and rank % 100 != 12: rank_suffix = "nd"
|
||||
elif rank % 10 == 3 and rank % 100 != 13: rank_suffix = "rd"
|
||||
rank_label.text = str(rank) + rank_suffix
|
||||
|
||||
# Rank
|
||||
var rank_label = Label.new()
|
||||
rank_label.text = "#" + str(rank)
|
||||
rank_label.custom_minimum_size = Vector2(40, 0)
|
||||
match rank:
|
||||
1: rank_label.add_theme_color_override("font_color", Color.GOLD)
|
||||
2: rank_label.add_theme_color_override("font_color", Color.SILVER)
|
||||
3: rank_label.add_theme_color_override("font_color", Color.DARK_ORANGE)
|
||||
_: rank_label.add_theme_color_override("font_color", Color.LIGHT_GRAY)
|
||||
hbox.add_child(rank_label)
|
||||
match rank:
|
||||
1: rank_label.add_theme_color_override("font_color", Color.GOLD)
|
||||
2: rank_label.add_theme_color_override("font_color", Color.SILVER)
|
||||
3: rank_label.add_theme_color_override("font_color", Color.DARK_ORANGE)
|
||||
_: rank_label.add_theme_color_override("font_color", Color.WHITE)
|
||||
|
||||
# Avatar
|
||||
var avatar_rect = TextureRect.new()
|
||||
avatar_rect.custom_minimum_size = Vector2(32, 32)
|
||||
avatar_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
|
||||
var avatar_url = entry.get("avatar_url", "")
|
||||
if avatar_url.is_empty() or not ResourceLoader.exists(avatar_url):
|
||||
if not avatar_url.is_empty():
|
||||
print("[Leaderboard] Avatar URL not found or invalid: ", avatar_url)
|
||||
avatar_url = UserProfileManager.AVATARS[0]
|
||||
|
||||
avatar_rect.texture = load(avatar_url)
|
||||
hbox.add_child(avatar_rect)
|
||||
var avatar_rect = item.get_node("HBoxContainer/Margin/InnerHBox/AvatarRect") as TextureRect
|
||||
if avatar_rect:
|
||||
var avatar_url = entry.get("avatar_url", "")
|
||||
if avatar_url.is_empty() or not ResourceLoader.exists(avatar_url):
|
||||
avatar_url = UserProfileManager.AVATARS[0]
|
||||
avatar_rect.texture = load(avatar_url)
|
||||
|
||||
# Name
|
||||
var name_label = Label.new()
|
||||
name_label.text = entry.get("display_name", "Unknown")
|
||||
name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
name_label.add_theme_color_override("font_color", Color.WHITE)
|
||||
hbox.add_child(name_label)
|
||||
var name_label = item.get_node("HBoxContainer/Margin/InnerHBox/NameLabel") as Label
|
||||
if name_label:
|
||||
name_label.text = entry.get("display_name", "Unknown")
|
||||
|
||||
# Value
|
||||
var value_label = Label.new()
|
||||
var color = Color(0.647, 0.996, 0.224, 1)
|
||||
match current_sort_key:
|
||||
"high_score": value_label.text = str(entry.get("high_score", 0))
|
||||
"win_rate": value_label.text = "%.1f%%" % entry.get("win_rate", 0.0)
|
||||
"games_played": value_label.text = str(entry.get("games_played", 0))
|
||||
value_label.add_theme_color_override("font_color", color)
|
||||
value_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
value_label.custom_minimum_size = Vector2(80, 0)
|
||||
hbox.add_child(value_label)
|
||||
var value_label = item.get_node("HBoxContainer/ValueLabel") as Label
|
||||
if value_label:
|
||||
match current_sort_key:
|
||||
"high_score": value_label.text = str(entry.get("high_score", 0))
|
||||
"win_rate": value_label.text = "%.1f%%" % entry.get("win_rate", 0.0)
|
||||
"games_played": value_label.text = str(entry.get("games_played", 0))
|
||||
|
||||
leaderboard_list.add_child(item)
|
||||
|
||||
# Make row clickable to update 3D preview
|
||||
item.gui_input.connect(func(event: InputEvent):
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
current_selected_index = index
|
||||
_populate_list() # Re-draw list to apply the new active highlight colors visually
|
||||
_populate_list()
|
||||
_show_entry_preview(index)
|
||||
)
|
||||
item.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
@@ -347,8 +328,18 @@ func _show_entry_preview(index: int) -> void:
|
||||
|
||||
var display_name: String = entry.get("display_name", "Unknown")
|
||||
var rank := index + 1
|
||||
selected_name_label.text = display_name
|
||||
selected_rank_label.text = "#%d" % rank
|
||||
if selected_name_label: selected_name_label.text = display_name
|
||||
if selected_rank_label: selected_rank_label.text = "Rank #%d" % rank
|
||||
if selected_score_label:
|
||||
match current_sort_key:
|
||||
"high_score": selected_score_label.text = str(entry.get("high_score", 0))
|
||||
"win_rate": selected_score_label.text = "%.1f%%" % entry.get("win_rate", 0.0)
|
||||
"games_played": selected_score_label.text = str(entry.get("games_played", 0))
|
||||
if selected_avatar_rect:
|
||||
var avatar_url2 = entry.get("avatar_url", "")
|
||||
if avatar_url2.is_empty() or not ResourceLoader.exists(avatar_url2):
|
||||
avatar_url2 = UserProfileManager.AVATARS[0]
|
||||
selected_avatar_rect.texture = load(avatar_url2)
|
||||
|
||||
func _update_3d_preview(character_name: String) -> void:
|
||||
if not character_root:
|
||||
|
||||
@@ -247,6 +247,30 @@ func _on_category_tab_pressed(category: String) -> void:
|
||||
_highlight_active_tab()
|
||||
|
||||
func _highlight_active_tab() -> void:
|
||||
# Create active tab style (dark blue)
|
||||
var active_style := StyleBoxFlat.new()
|
||||
active_style.bg_color = Color(0.1, 0.19, 0.27, 1)
|
||||
active_style.content_margin_left = 12.0
|
||||
active_style.content_margin_top = 8.0
|
||||
active_style.content_margin_right = 12.0
|
||||
active_style.content_margin_bottom = 8.0
|
||||
active_style.corner_radius_top_left = 8
|
||||
active_style.corner_radius_top_right = 8
|
||||
active_style.corner_radius_bottom_right = 8
|
||||
active_style.corner_radius_bottom_left = 8
|
||||
|
||||
# Create inactive tab style (cyan)
|
||||
var inactive_style := StyleBoxFlat.new()
|
||||
inactive_style.bg_color = Color(0.33, 0.62, 0.78, 1)
|
||||
inactive_style.content_margin_left = 12.0
|
||||
inactive_style.content_margin_top = 8.0
|
||||
inactive_style.content_margin_right = 12.0
|
||||
inactive_style.content_margin_bottom = 8.0
|
||||
inactive_style.corner_radius_top_left = 8
|
||||
inactive_style.corner_radius_top_right = 8
|
||||
inactive_style.corner_radius_bottom_right = 8
|
||||
inactive_style.corner_radius_bottom_left = 8
|
||||
|
||||
var map := {
|
||||
"head": head_tab_btn,
|
||||
"costume": costume_tab_btn,
|
||||
@@ -255,7 +279,13 @@ func _highlight_active_tab() -> void:
|
||||
"fragment": frag_tab_btn
|
||||
}
|
||||
for cat: String in map:
|
||||
(map[cat] as Button).modulate = Color(1.3, 1.3, 0.4, 1) if cat == _current_category else Color.WHITE
|
||||
var btn: Button = map[cat]
|
||||
var is_active := (cat == _current_category)
|
||||
var style := active_style if is_active else inactive_style
|
||||
btn.add_theme_stylebox_override("normal", style)
|
||||
btn.add_theme_stylebox_override("hover", style)
|
||||
btn.add_theme_stylebox_override("pressed", style)
|
||||
btn.add_theme_color_override("font_color", Color.WHITE)
|
||||
|
||||
func _rebuild_category_items() -> void:
|
||||
_category_items.clear()
|
||||
@@ -306,6 +336,8 @@ func _populate_item_grid() -> void:
|
||||
prev_page_btn.disabled = (_current_page == 0)
|
||||
next_page_btn.disabled = ((_current_page + 1) >= total_pages)
|
||||
|
||||
var placeholder_tex = preload("res://assets/graphics/gui/inventory/item_placeholder.png")
|
||||
|
||||
var equipped: String = UserProfileManager.loadout.get(_current_category, "")
|
||||
for i in _item_slots.size():
|
||||
var slot: Button = _item_slots[i]
|
||||
@@ -313,11 +345,22 @@ func _populate_item_grid() -> void:
|
||||
if idx < total:
|
||||
var item_id: String = _category_items[idx]
|
||||
var info: Dictionary = ITEM_CATALOG.get(item_id, {})
|
||||
slot.text = info.get("name", item_id)
|
||||
slot.tooltip_text = item_id
|
||||
|
||||
var tex_path = "res://assets/graphics/gui/inventory/%s.png" % item_id
|
||||
if ResourceLoader.exists(tex_path):
|
||||
slot.icon = load(tex_path)
|
||||
else:
|
||||
slot.icon = placeholder_tex
|
||||
|
||||
slot.text = ""
|
||||
slot.tooltip_text = info.get("name", item_id)
|
||||
slot.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
slot.expand_icon = true
|
||||
|
||||
slot.modulate = Color(0.4, 1.0, 0.4, 1) if item_id == equipped else Color.WHITE
|
||||
else:
|
||||
slot.text = ""
|
||||
slot.icon = null
|
||||
slot.tooltip_text = ""
|
||||
slot.modulate = Color.WHITE
|
||||
|
||||
@@ -331,6 +374,12 @@ func _on_slot_pressed(slot_index: int) -> void:
|
||||
func _show_item_info(item_id: String) -> void:
|
||||
var info: Dictionary = ITEM_CATALOG.get(item_id, {})
|
||||
|
||||
var tex_path = "res://assets/graphics/gui/inventory/%s.png" % item_id
|
||||
if ResourceLoader.exists(tex_path):
|
||||
item_preview.texture = load(tex_path)
|
||||
else:
|
||||
item_preview.texture = preload("res://assets/graphics/gui/inventory/item_placeholder.png")
|
||||
|
||||
item_name_label.text = info.get("name", item_id)
|
||||
|
||||
var rarity: String = info.get("rarity", "Common")
|
||||
@@ -353,6 +402,7 @@ func _show_item_info(item_id: String) -> void:
|
||||
|
||||
func _clear_item_info() -> void:
|
||||
item_name_label.text = "Select an item"
|
||||
item_preview.texture = null
|
||||
item_rarity_label.text = ""
|
||||
item_price_label.text = ""
|
||||
equip_btn.text = "Equip"
|
||||
|
||||
+106
-22
@@ -8,6 +8,9 @@ signal closed
|
||||
@onready var item_grid: GridContainer = %ItemGrid
|
||||
@onready var back_btn: Button = %BackBtn
|
||||
@onready var status_label: Label = %StatusLabel
|
||||
@onready var banner1: Button = %Banner1
|
||||
@onready var banner2: Button = %Banner2
|
||||
@onready var banner3: Button = %Banner3
|
||||
|
||||
# Tabs
|
||||
@onready var tab_head: Button = %TabHead
|
||||
@@ -17,6 +20,9 @@ signal closed
|
||||
@onready var tab_gold: Button = %TabGold
|
||||
@onready var tab_star: Button = %TabStar
|
||||
|
||||
# Maps category -> tab button (populated in _ready)
|
||||
var _tab_map: Dictionary = {}
|
||||
|
||||
# 3D Preview
|
||||
@onready var character_root: Node3D = %CharacterRoot
|
||||
@onready var anim_player: AnimationPlayer = %AnimationPlayer
|
||||
@@ -72,6 +78,14 @@ const STAR_PACKS: Array = [
|
||||
# _ready
|
||||
# -----------------------------------------------------------------------
|
||||
func _ready() -> void:
|
||||
_tab_map = {
|
||||
"head": tab_head,
|
||||
"costume": tab_costume,
|
||||
"glove": tab_glove,
|
||||
"accessory": tab_acc,
|
||||
"gold_packs": tab_gold,
|
||||
"star_packs": tab_star,
|
||||
}
|
||||
back_btn.pressed.connect(_on_close)
|
||||
tab_head.pressed.connect(_on_tab_selected.bind("head"))
|
||||
tab_costume.pressed.connect(_on_tab_selected.bind("costume"))
|
||||
@@ -85,6 +99,7 @@ func _ready() -> void:
|
||||
if UserProfileManager.profile_updated.connect(_refresh_wallet) != OK:
|
||||
pass
|
||||
|
||||
_set_active_tab(current_category)
|
||||
_setup_3d_preview()
|
||||
|
||||
if UserProfileManager.shop_catalog.is_empty():
|
||||
@@ -175,12 +190,78 @@ func _fetch_and_build() -> void:
|
||||
|
||||
func _build_shop() -> void:
|
||||
_refresh_wallet()
|
||||
_populate_banners()
|
||||
_populate_current_tab()
|
||||
|
||||
func _on_tab_selected(category: String) -> void:
|
||||
current_category = category
|
||||
_set_active_tab(category)
|
||||
_populate_current_tab()
|
||||
|
||||
func _set_active_tab(active_category: String) -> void:
|
||||
var style_active := StyleBoxFlat.new()
|
||||
style_active.bg_color = Color(1, 1, 1, 1)
|
||||
style_active.content_margin_left = 16.0
|
||||
style_active.content_margin_top = 10.0
|
||||
style_active.content_margin_right = 16.0
|
||||
style_active.content_margin_bottom = 10.0
|
||||
style_active.corner_radius_top_left = 6
|
||||
style_active.corner_radius_top_right = 6
|
||||
style_active.corner_radius_bottom_right = 6
|
||||
style_active.corner_radius_bottom_left = 6
|
||||
|
||||
var style_inactive := StyleBoxFlat.new()
|
||||
style_inactive.bg_color = Color(0.15, 0.18, 0.22, 1)
|
||||
style_inactive.content_margin_left = 16.0
|
||||
style_inactive.content_margin_top = 10.0
|
||||
style_inactive.content_margin_right = 16.0
|
||||
style_inactive.content_margin_bottom = 10.0
|
||||
style_inactive.corner_radius_top_left = 6
|
||||
style_inactive.corner_radius_top_right = 6
|
||||
style_inactive.corner_radius_bottom_right = 6
|
||||
style_inactive.corner_radius_bottom_left = 6
|
||||
|
||||
var style_hover := StyleBoxFlat.new()
|
||||
style_hover.bg_color = Color(0.22, 0.26, 0.30, 1)
|
||||
style_hover.content_margin_left = 16.0
|
||||
style_hover.content_margin_top = 10.0
|
||||
style_hover.content_margin_right = 16.0
|
||||
style_hover.content_margin_bottom = 10.0
|
||||
style_hover.corner_radius_top_left = 6
|
||||
style_hover.corner_radius_top_right = 6
|
||||
style_hover.corner_radius_bottom_right = 6
|
||||
style_hover.corner_radius_bottom_left = 6
|
||||
|
||||
for cat in _tab_map:
|
||||
var btn: Button = _tab_map[cat]
|
||||
if cat == active_category:
|
||||
btn.add_theme_stylebox_override("normal", style_active)
|
||||
btn.add_theme_stylebox_override("hover", style_active)
|
||||
btn.add_theme_stylebox_override("pressed", style_active)
|
||||
btn.add_theme_color_override("font_color", Color(0.08, 0.09, 0.12))
|
||||
btn.add_theme_color_override("font_hover_color", Color(0.08, 0.09, 0.12))
|
||||
else:
|
||||
btn.add_theme_stylebox_override("normal", style_inactive)
|
||||
btn.add_theme_stylebox_override("hover", style_hover)
|
||||
btn.add_theme_stylebox_override("pressed", style_inactive)
|
||||
btn.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7))
|
||||
btn.add_theme_color_override("font_hover_color", Color(1, 1, 1))
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Banner population — promotional / featured items
|
||||
# -----------------------------------------------------------------------
|
||||
func _populate_banners() -> void:
|
||||
var banners: Array[Button] = [banner1, banner2, banner3]
|
||||
var promos: Array = UserProfileManager.shop_catalog.get("banners", [])
|
||||
for i in banners.size():
|
||||
var btn: Button = banners[i]
|
||||
if i < promos.size():
|
||||
btn.text = promos[i].get("label", "")
|
||||
btn.tooltip_text = promos[i].get("id", "")
|
||||
btn.show()
|
||||
else:
|
||||
btn.hide()
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Grid population — builds cards dynamically from localized templates
|
||||
# -----------------------------------------------------------------------
|
||||
@@ -214,8 +295,8 @@ func _make_gold_card(pack: Dictionary) -> Control:
|
||||
var card: Control = template_gold_card.duplicate()
|
||||
card.visible = true
|
||||
|
||||
var amount_lbl: Label = card.find_child("AmountLabel", true, false) as Label
|
||||
if amount_lbl: amount_lbl.text = "⭐ %d" % pack.amount
|
||||
var amount_lbl: RichTextLabel = card.find_child("AmountLabel", true, false) as RichTextLabel
|
||||
if amount_lbl: amount_lbl.text = "[right][img=24x24]res://assets/graphics/gui/lobby/gold.png[/img] %d[/right]" % pack.amount
|
||||
|
||||
var bonus_lbl: Label = card.find_child("BonusLabel", true, false) as Label
|
||||
if bonus_lbl:
|
||||
@@ -237,11 +318,11 @@ func _make_star_card(pack: Dictionary) -> Control:
|
||||
var card: Control = template_star_card.duplicate()
|
||||
card.visible = true
|
||||
|
||||
var amount_lbl: Label = card.find_child("AmountLabel", true, false) as Label
|
||||
if amount_lbl: amount_lbl.text = "✦ %d" % pack.amount
|
||||
var amount_lbl: RichTextLabel = card.find_child("AmountLabel", true, false) as RichTextLabel
|
||||
if amount_lbl: amount_lbl.text = "[right][img=24x24]res://assets/graphics/gui/lobby/star.png[/img] %d[/right]" % pack.amount
|
||||
|
||||
var cost_lbl: Label = card.find_child("CostLabel", true, false) as Label
|
||||
if cost_lbl: cost_lbl.text = "Cost: ⭐ %d Gold" % pack.gold_cost
|
||||
var cost_lbl: RichTextLabel = card.find_child("CostLabel", true, false) as RichTextLabel
|
||||
if cost_lbl: cost_lbl.text = "[center]Cost: [img=20x20]res://assets/graphics/gui/lobby/gold.png[/img] %d[/center]" % pack.gold_cost
|
||||
|
||||
var buy_btn: Button = card.find_child("BuyBtn", true, false) as Button
|
||||
if buy_btn: buy_btn.pressed.connect(_on_buy_star_pressed.bind(pack))
|
||||
@@ -270,13 +351,16 @@ func _make_cosmetic_card(item: Dictionary) -> Control:
|
||||
}.get(rarity, Color(0.50, 0.50, 0.50))
|
||||
rarity_lbl.add_theme_color_override("font_color", rarity_col)
|
||||
|
||||
var price_lbl: Label = card.find_child("PriceLabel", true, false) as Label
|
||||
var price_lbl: RichTextLabel = card.find_child("PriceLabel", true, false) as RichTextLabel
|
||||
if price_lbl:
|
||||
var g: int = int(item.get("gold", 0))
|
||||
var s: int = int(item.get("star", 0))
|
||||
if g > 0 and s > 0: price_lbl.text = "⭐ %d ✦ %d" % [g, s]
|
||||
elif g > 0: price_lbl.text = "⭐ %d" % g
|
||||
else: price_lbl.text = "✦ %d" % s
|
||||
if g > 0 and s > 0:
|
||||
price_lbl.text = "[center][img=20x20]res://assets/graphics/gui/lobby/gold.png[/img] %d [img=20x20]res://assets/graphics/gui/lobby/star.png[/img] %d[/center]" % [g, s]
|
||||
elif g > 0:
|
||||
price_lbl.text = "[center][img=20x20]res://assets/graphics/gui/lobby/gold.png[/img] %d[/center]" % g
|
||||
else:
|
||||
price_lbl.text = "[center][img=20x20]res://assets/graphics/gui/lobby/star.png[/img] %d[/center]" % s
|
||||
|
||||
var try_btn: Button = card.find_child("TryBtn", true, false) as Button
|
||||
if try_btn: try_btn.pressed.connect(_on_try_pressed.bind(item))
|
||||
@@ -300,8 +384,8 @@ func _make_cosmetic_card(item: Dictionary) -> Control:
|
||||
func _refresh_wallet() -> void:
|
||||
var g: int = UserProfileManager.wallet.get("gold", 0)
|
||||
var s: int = UserProfileManager.wallet.get("star", 0)
|
||||
gold_label.text = "⭐ %d" % g
|
||||
star_label.text = "✦ %d" % s
|
||||
gold_label.text = str(g)
|
||||
star_label.text = str(s)
|
||||
status_label.text = ""
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
@@ -351,23 +435,23 @@ func _on_buy_cosmetic_pressed(item: Dictionary) -> void:
|
||||
if UserProfileManager.inventory.has(item.id):
|
||||
status_label.text = "Already owned: " + item.get("name", item.id)
|
||||
return
|
||||
var price_gold: int = item.get("gold", 0)
|
||||
var price_star: int = item.get("star", 0)
|
||||
if UserProfileManager.wallet.get("gold", 0) < price_gold \
|
||||
or UserProfileManager.wallet.get("star", 0) < price_star:
|
||||
status_label.text = "Not enough currency."
|
||||
return
|
||||
|
||||
status_label.text = "Purchasing..."
|
||||
var success: bool = await UserProfileManager.purchase_item(
|
||||
item.id, price_gold, price_star, current_category)
|
||||
status_label.text = ("Purchased: " + item.get("name", item.id)) if success else "Purchase failed."
|
||||
if success:
|
||||
var err: String = await UserProfileManager.purchase_item(item.id)
|
||||
|
||||
if err == "":
|
||||
status_label.text = "Purchased: " + item.get("name", item.id)
|
||||
_refresh_wallet()
|
||||
# Refresh preview to show newly purchased skin's materials
|
||||
if _preview_revert.is_valid():
|
||||
_preview_revert.call()
|
||||
_preview_revert = Callable()
|
||||
SkinManager.apply_loadout(character_root, UserProfileManager.loadout)
|
||||
else:
|
||||
if "NotEnoughFunds" in err or "funds" in err.to_lower() or "wallet" in err.to_lower():
|
||||
status_label.text = "Not enough currency."
|
||||
else:
|
||||
status_label.text = "Purchase failed."
|
||||
|
||||
func _on_close() -> void:
|
||||
# Clean up any open preview when closing the shop
|
||||
|
||||
Reference in New Issue
Block a user