Files
tekton/scripts/ui/profile_panel.gd
T

720 lines
33 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends Control
## Profile panel — 3-column layout.
## Left : profile card, currencies, stats, account buttons.
## Center: category tabs overlay, 3D rotatable preview, char-selector overlay.
## Right : item info card, 3×3 owned-item grid, pagination.
signal closed
signal profile_updated
# ─────────────────────────────────────────────────────────────
# UI references (all resolved via %UniqueName — must match .tscn)
# ─────────────────────────────────────────────────────────────
# Left column
@onready var avatar_display := %AvatarDisplay as TextureRect
@onready var display_name_input := %DisplayNameInput as LineEdit
@onready var save_name_btn := %SaveNameBtn as Button
@onready var change_avatar_btn := %ChangeAvatarBtn as Button
@onready var account_type_label := %AccountType as Label
@onready var link_account_btn := %LinkAccountBtn as Button
@onready var gold_label := %GoldLabel as Label
@onready var star_label := %StarLabel as Label
@onready var games_played_label := %GamesPlayed as Label
@onready var win_rate_label := %WinRate as Label
@onready var high_score_label := %HighScore as Label
@onready var acc_settings_btn := %AccSettingsBtn as Button
@onready var admin_panel_btn := %AdminPanelBtn as Button
@onready var logout_btn := %LogoutBtn as Button
@onready var status_label := %StatusLabel as Label
# Center column (overlays inside CenterWrapper)
@onready var head_tab_btn := %HeadTabBtn as Button
@onready var costume_tab_btn := %CostumeTabBtn as Button
@onready var glove_tab_btn := %GloveTabBtn as Button
@onready var acc_tab_btn := %AccTabBtn as Button
@onready var frag_tab_btn := %FragTabBtn as Button
@onready var drag_zone := %DragZone as Control
@onready var character_root := %CharacterRoot as Node3D
@onready var char_left_btn := %CharLeftBtn as Button
@onready var char_right_btn := %CharRightBtn as Button
@onready var loadout_char_name := %LoadoutCharName as Label
@onready var set_default_btn := %SetDefaultBtn as Button
@onready var anim_player := %AnimationPlayer as AnimationPlayer
# Right column
@onready var item_preview := %ItemPreview as TextureRect
@onready var item_name_label := %ItemNameLabel as Label
@onready var item_rarity_label := %ItemRarityLabel as Label
@onready var item_price_label := %ItemPriceLabel as Label
@onready var equip_btn := %EquipBtn as Button
@onready var dismantle_btn := %DismantleBtn as Button
@onready var item_grid := %ItemGrid as GridContainer
@onready var prev_page_btn := %PrevPageBtn as Button
@onready var page_label := %PageLabel as Label
@onready var next_page_btn := %NextPageBtn as Button
# Back button + popups
@onready var back_btn := %BackBtn as Button
@onready var topup_gold_btn := %TopupGoldBtn as Button
@onready var avatar_popup := %AvatarSelectionPopup as PopupPanel
@onready var avatar_grid := %GridContainer as GridContainer
@onready var acc_settings_dialog := %AccountSettingsDialog as AcceptDialog
@onready var old_pass_input := %OldPassInput as LineEdit
@onready var new_email_input := %NewEmailInput as LineEdit
@onready var new_pass_input := %NewPassInput as LineEdit
@onready var submit_cred_btn := %SubmitCredBtn as Button
@onready var tz_dropdown := %TzDropdown as OptionButton
@onready var save_tz_btn := %SaveTzBtn as Button
@onready var reset_stats_btn := %ResetStatsBtn as Button
# New static popups
@onready var dismantle_dialog := %DismantleConfirmDialog as ConfirmationDialog
@onready var link_email_dialog := %LinkEmailDialog as AcceptDialog
@onready var link_email_input := %LinkEmailInput as LineEdit
@onready var link_pass_input := %LinkPassInput as LineEdit
@onready var reset_stats_dialog := %ResetStatsConfirmDialog as ConfirmationDialog
# ─────────────────────────────────────────────────────────────
# Constants
# ─────────────────────────────────────────────────────────────
const CHARACTERS: Array[String] = ["Copper", "Dabro", "Gatot", "Pip"]
const CHAR_NODE_MAP: Dictionary = {
"Copper": "Oldpop",
"Dabro": "Masbro",
"Gatot": "Gatot",
"Pip": "Bob"
}
const RARITY_COLORS: Dictionary = {
"Common": Color(0.80, 0.80, 0.80, 1),
"Rare": Color(0.30, 0.60, 1.00, 1),
"Epic": Color(0.70, 0.30, 1.00, 1),
"Legendary": Color(1.00, 0.70, 0.10, 1)
}
## Client-side item catalog: item_id -> { name, rarity, star_value }
## Add new entries here whenever a new item is added to the shop.
const ITEM_CATALOG: Dictionary = {
# ── Generic items ──────────────────────────────────────────────────────
"head_hat1": {"name": "Cap", "rarity": "Common", "star_value": 50},
"head_crown": {"name": "Crown", "rarity": "Epic", "star_value": 1500},
"costume_red": {"name": "Red Suit", "rarity": "Rare", "star_value": 200},
"costume_gold": {"name": "Gold Suit", "rarity": "Epic", "star_value": 1000},
"glove_leather": {"name": "Leather Gloves", "rarity": "Common", "star_value": 50},
"acc_glasses": {"name": "Sunglasses", "rarity": "Rare", "star_value": 300},
# ── Oldpop (Copper) — Hat ─────────────────────────────────────────────
"oldpop_hat1": {"name": "Oldpop Hat I", "rarity": "Common", "star_value": 100},
"oldpop_hat2": {"name": "Oldpop Hat II", "rarity": "Rare", "star_value": 250},
"oldpop_hat3": {"name": "Oldpop Hat III", "rarity": "Epic", "star_value": 800},
# ── Oldpop (Copper) — Costume ─────────────────────────────────────────
"oldpop_body": {"name": "Oldpop Body", "rarity": "Rare", "star_value": 200},
"oldpop_arms": {"name": "Oldpop Arms", "rarity": "Common", "star_value": 100},
"oldpop-grey-pant": {"name": "Grey Pants", "rarity": "Common", "star_value": 80},
"oldpop-clothing-original": {"name": "Original Pants", "rarity": "Common", "star_value": 80},
# kept for backward compat with older purchases
"oldpop_clothing_original": {"name": "Original Pants", "rarity": "Common", "star_value": 80},
# ── Oldpop (Copper) — Gloves ──────────────────────────────────────────
"oldpop_gloves": {"name": "Oldpop Gloves", "rarity": "Common", "star_value": 60},
}
const ITEMS_PER_PAGE: int = 9
# ─────────────────────────────────────────────────────────────
# State
# ─────────────────────────────────────────────────────────────
var _loadout_index: int = 0
var _default_character: String = "Copper"
var _current_category: String = "head"
var _category_items: Array = []
var _current_page: int = 0
var _selected_item_id: String = ""
var _item_slots: Array[Button] = []
# Drag-to-rotate
var _drag_active: bool = false
var _drag_start_x: float = 0.0
var _model_yaw: float = 0.0
# ─────────────────────────────────────────────────────────────
# Lifecycle
# ─────────────────────────────────────────────────────────────
func _ready() -> void:
_build_slot_refs()
_connect_signals()
_setup_avatar_grid()
_setup_account_settings_ui()
_load_profile_data()
_load_loadout()
_setup_3d_preview()
_on_category_tab_pressed("head")
func _build_slot_refs() -> void:
_item_slots.clear()
for child in item_grid.get_children():
if child is Button:
_item_slots.append(child as Button)
# ─────────────────────────────────────────────────────────────
# Signal wiring
# ─────────────────────────────────────────────────────────────
func _connect_signals() -> void:
back_btn.pressed.connect(_on_close_pressed)
change_avatar_btn.pressed.connect(_on_change_avatar_pressed)
save_name_btn.pressed.connect(_on_save_name_pressed)
link_account_btn.pressed.connect(_on_link_account_pressed)
acc_settings_btn.pressed.connect(_open_account_settings)
admin_panel_btn.pressed.connect(_on_admin_panel_pressed)
logout_btn.pressed.connect(_on_logout_pressed)
char_left_btn.pressed.connect(func(): _cycle_loadout_char(-1))
char_right_btn.pressed.connect(func(): _cycle_loadout_char(1))
set_default_btn.pressed.connect(_on_set_default_pressed)
topup_gold_btn.pressed.connect(_on_topup_gold_pressed)
# Category tabs
head_tab_btn.pressed.connect(func(): _on_category_tab_pressed("head"))
costume_tab_btn.pressed.connect(func(): _on_category_tab_pressed("costume"))
glove_tab_btn.pressed.connect(func(): _on_category_tab_pressed("glove"))
acc_tab_btn.pressed.connect(func(): _on_category_tab_pressed("accessory"))
frag_tab_btn.pressed.connect(func(): _on_category_tab_pressed("fragment"))
# Item grid slot buttons
for i in _item_slots.size():
_item_slots[i].pressed.connect(_on_slot_pressed.bind(i))
equip_btn.pressed.connect(_on_equip_pressed)
dismantle_btn.pressed.connect(_on_dismantle_pressed)
dismantle_dialog.confirmed.connect(_on_dismantle_confirmed)
link_email_dialog.confirmed.connect(_on_link_account_confirmed)
reset_stats_dialog.confirmed.connect(_on_reset_stats_confirmed)
prev_page_btn.pressed.connect(_on_prev_page)
next_page_btn.pressed.connect(_on_next_page)
drag_zone.gui_input.connect(_on_drag_input)
if UserProfileManager.profile_loaded.connect(func(_p): _on_profile_updated()) != OK:
push_warning("[ProfilePanel] Could not connect profile_loaded")
UserProfileManager.profile_updated.connect(_on_profile_updated)
UserProfileManager.profile_update_failed.connect(_on_profile_update_failed)
# ─────────────────────────────────────────────────────────────
# Profile data
# ─────────────────────────────────────────────────────────────
func _load_profile_data() -> void:
var prof := UserProfileManager.profile
var stats := UserProfileManager.stats
display_name_input.text = prof.get("display_name", "")
display_name_input.max_length = 6
var url: String = UserProfileManager.get_avatar_url()
if ResourceLoader.exists(url):
avatar_display.texture = load(url) as Texture2D
games_played_label.text = "Games Played: %d" % stats.get("games_played", 0)
win_rate_label.text = "Win Rate: %.1f%%" % UserProfileManager.get_win_rate()
high_score_label.text = "High Score: %d" % stats.get("high_score", 0)
gold_label.text = str(UserProfileManager.wallet.get("gold", 0))
star_label.text = str(UserProfileManager.wallet.get("star", 0))
if AuthManager.is_guest:
account_type_label.text = "Account : Guest"
link_account_btn.visible = true
else:
account_type_label.text = "Account : %s" % _auth_mode_name(AuthManager.auth_mode)
link_account_btn.visible = false
status_label.text = ""
func _auth_mode_name(mode: int) -> String:
match mode:
AuthManager.AuthMode.EMAIL: return "Email"
AuthManager.AuthMode.GOOGLE: return "Google"
AuthManager.AuthMode.APPLE: return "Apple"
AuthManager.AuthMode.FACEBOOK: return "Facebook"
_: return "Guest"
# ─────────────────────────────────────────────────────────────
# Category tabs
# ─────────────────────────────────────────────────────────────
func _on_category_tab_pressed(category: String) -> void:
_current_category = category
_current_page = 0
_selected_item_id = ""
_clear_item_info()
_rebuild_category_items()
_populate_item_grid()
_highlight_active_tab()
func _highlight_active_tab() -> void:
var map := {
"head": head_tab_btn,
"costume": costume_tab_btn,
"glove": glove_tab_btn,
"accessory": acc_tab_btn,
"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
func _rebuild_category_items() -> void:
_category_items.clear()
# Fragment tab handled separately
if _current_category == "fragment":
_rebuild_fragment_items()
return
var prefix := _current_category + "_"
# Resolve the current character's node name (e.g. "Copper" → "Oldpop")
var current_char_display: String = CHARACTERS[_loadout_index]
var current_char_node: String = CHAR_NODE_MAP.get(current_char_display, current_char_display)
for item_id: String in UserProfileManager.inventory:
# Look up the skin data from SkinManager first (handles all id formats)
var skin_data: Dictionary = SkinManager.SKIN_CATALOG.get(item_id, {})
if not skin_data.is_empty():
# Only show items that match the current category
if skin_data.get("category", "") != _current_category:
continue
# Only show items that belong to this character (or have no character restriction)
var item_char: String = skin_data.get("character", "")
if item_char.is_empty() or item_char == current_char_node:
if not _category_items.has(item_id):
_category_items.append(item_id)
else:
# Fallback: generic prefix-based match (e.g. "head_hat1" under "head" tab)
if item_id.begins_with(prefix):
if not _category_items.has(item_id):
_category_items.append(item_id)
# ─────────────────────────────────────────────────────────────
# Item grid
# ─────────────────────────────────────────────────────────────
func _rebuild_fragment_items() -> void:
var frags: Dictionary = UserProfileManager.fragments
for fid in ["frag_common", "frag_uncommon", "frag_rare"]:
var count: int = frags.get(fid, 0)
if count > 0: _category_items.append(fid)
func _populate_item_grid() -> void:
var start: int = _current_page * ITEMS_PER_PAGE
var total: int = _category_items.size()
var total_pages: int = max(1, ceili(float(total) / float(ITEMS_PER_PAGE)))
page_label.text = "%d / %d" % [_current_page + 1, total_pages]
prev_page_btn.disabled = (_current_page == 0)
next_page_btn.disabled = ((_current_page + 1) >= total_pages)
var equipped: String = UserProfileManager.loadout.get(_current_category, "")
for i in _item_slots.size():
var slot: Button = _item_slots[i]
var idx: int = start + i
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
slot.modulate = Color(0.4, 1.0, 0.4, 1) if item_id == equipped else Color.WHITE
else:
slot.text = ""
slot.tooltip_text = ""
slot.modulate = Color.WHITE
func _on_slot_pressed(slot_index: int) -> void:
var idx: int = _current_page * ITEMS_PER_PAGE + slot_index
if idx >= _category_items.size():
return
_selected_item_id = _category_items[idx]
_show_item_info(_selected_item_id)
func _show_item_info(item_id: String) -> void:
var info: Dictionary = ITEM_CATALOG.get(item_id, {})
item_name_label.text = info.get("name", item_id)
var rarity: String = info.get("rarity", "Common")
item_rarity_label.text = rarity
item_rarity_label.add_theme_color_override(
"font_color", RARITY_COLORS.get(rarity, Color.WHITE)
)
var sv: int = info.get("star_value", 0)
item_price_label.text = str(sv) if sv > 0 else ""
var equipped: String = UserProfileManager.loadout.get(_current_category, "")
if equipped == item_id:
equip_btn.text = "Unequip"
equip_btn.disabled = false
else:
equip_btn.text = "Equip"
equip_btn.disabled = false
dismantle_btn.disabled = false
func _clear_item_info() -> void:
item_name_label.text = "Select an item"
item_rarity_label.text = ""
item_price_label.text = ""
equip_btn.text = "Equip"
equip_btn.disabled = true
dismantle_btn.disabled = true
# ─────────────────────────────────────────────────────────────
# Item actions
# ─────────────────────────────────────────────────────────────
func _on_equip_pressed() -> void:
if _selected_item_id.is_empty(): return
var equipped: String = UserProfileManager.loadout.get(_current_category, "")
if equipped == _selected_item_id:
# ── UNEQUIP ──────────────────────────────────────────────
var ok: bool = await UserProfileManager.update_loadout(_current_category, "")
if ok:
_set_status("Unequipped: " + _selected_item_id, Color(1.0, 0.7, 0.3))
SkinManager.apply_loadout(character_root, UserProfileManager.loadout)
_populate_item_grid()
_show_item_info(_selected_item_id)
else:
_set_status("Failed to unequip.", Color.RED)
else:
# ── EQUIP ────────────────────────────────────────────────
var ok: bool = await UserProfileManager.update_loadout(_current_category, _selected_item_id)
if ok:
_set_status("Equipped: " + _selected_item_id, Color(0.4, 1.0, 0.4))
SkinManager.apply_loadout(character_root, UserProfileManager.loadout)
_populate_item_grid()
_show_item_info(_selected_item_id)
else:
_set_status("Failed to equip item.", Color.RED)
func _on_dismantle_pressed() -> void:
if _selected_item_id.is_empty(): return
dismantle_dialog.dialog_text = "Dismantle '%s'?\nThis will remove it from your inventory." % _selected_item_id
dismantle_dialog.popup_centered()
func _on_dismantle_confirmed() -> void:
UserProfileManager.inventory.erase(_selected_item_id)
if UserProfileManager.loadout.get(_current_category, "") == _selected_item_id:
UserProfileManager.update_loadout(_current_category, "")
_selected_item_id = ""
_clear_item_info()
_rebuild_category_items()
_populate_item_grid()
_set_status("Item dismantled.", Color(1, 0.5, 0.3))
# ─────────────────────────────────────────────────────────────
# Pagination
# ─────────────────────────────────────────────────────────────
func _on_prev_page() -> void:
if _current_page > 0:
_current_page -= 1
_populate_item_grid()
func _on_next_page() -> void:
var total_pages: int = max(1, ceili(float(_category_items.size()) / float(ITEMS_PER_PAGE)))
if _current_page + 1 < total_pages:
_current_page += 1
_populate_item_grid()
# ─────────────────────────────────────────────────────────────
# Character loadout
# ─────────────────────────────────────────────────────────────
func _load_loadout() -> void:
var saved: String = UserProfileManager.profile.get("loadout_character", "Copper")
var idx: int = CHARACTERS.find(saved)
_loadout_index = max(idx, 0)
_default_character = CHARACTERS[_loadout_index]
_refresh_loadout_ui()
func _cycle_loadout_char(direction: int) -> void:
_loadout_index = wrapi(_loadout_index + direction, 0, CHARACTERS.size())
_refresh_loadout_ui()
func _refresh_loadout_ui() -> void:
var char_name := CHARACTERS[_loadout_index]
loadout_char_name.text = char_name
var is_default := char_name == _default_character
set_default_btn.text = "✓ DEFAULT" if is_default else "Set as Default"
set_default_btn.disabled = is_default
_update_3d_preview(char_name)
func _on_set_default_pressed() -> void:
var char_name := CHARACTERS[_loadout_index]
_default_character = char_name
UserProfileManager.profile["loadout_character"] = char_name
if LobbyManager.available_characters.has(char_name):
LobbyManager.local_character_index = LobbyManager.available_characters.find(char_name)
_set_status("Default set to: " + char_name, Color(0.4, 0.9, 0.4))
_refresh_loadout_ui()
_save_loadout_to_profile()
UserProfileManager.submit_to_leaderboard()
# Notify lobby (and any other listeners) to refresh their 3D preview
UserProfileManager.profile_updated.emit()
func _save_loadout_to_profile() -> void:
if not NakamaManager.session: return
var data := {
"avatar_index": UserProfileManager.profile.get("avatar_index", 0),
"bio": UserProfileManager.profile.get("bio", ""),
"country": UserProfileManager.profile.get("country", ""),
"language": UserProfileManager.profile.get("language", "en"),
"loadout_character": _default_character,
"loadout": UserProfileManager.loadout
}
var write_obj := NakamaWriteStorageObject.new("profiles", "profile", 2, 1, JSON.stringify(data), "")
await NakamaManager.client.write_storage_objects_async(NakamaManager.session, [write_obj])
# ─────────────────────────────────────────────────────────────
# 3D Preview
# ─────────────────────────────────────────────────────────────
func _setup_3d_preview() -> void:
_update_3d_preview(_default_character)
func _update_3d_preview(character_name: String) -> void:
if not character_root: return
var node_name: String = CHAR_NODE_MAP.get(character_name, "Masbro")
for child in character_root.get_children():
if child is Node3D:
child.visible = (child.name == node_name)
if anim_player:
var new_root: Node3D = character_root.get_node_or_null(node_name) as Node3D
if new_root:
anim_player.root_node = new_root.get_path()
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])
# Apply equipped skins on the newly-visible character
SkinManager.apply_loadout(character_root, UserProfileManager.loadout)
# ─────────────────────────────────────────────────────────────
# Drag-to-rotate
# ─────────────────────────────────────────────────────────────
func _on_drag_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
var mb := event as InputEventMouseButton
if mb.button_index == MOUSE_BUTTON_LEFT:
_drag_active = mb.pressed
_drag_start_x = mb.position.x
elif event is InputEventMouseMotion and _drag_active:
var mm := event as InputEventMouseMotion
var delta: float = mm.position.x - _drag_start_x
_drag_start_x = mm.position.x
_model_yaw += delta * 0.8
if character_root:
character_root.rotation_degrees.y = _model_yaw
# ─────────────────────────────────────────────────────────────
# Avatar
# ─────────────────────────────────────────────────────────────
func _setup_avatar_grid() -> void:
# Assume .tscn has precisely instantiated Button nodes in AvatarGrid
var children = avatar_grid.get_children()
for i in range(min(children.size(), UserProfileManager.AVATARS.size())):
var btn = children[i] as Button
var path: String = UserProfileManager.AVATARS[i]
if ResourceLoader.exists(path):
btn.icon = load(path) as Texture2D
btn.expand_icon = true
else:
btn.text = str(i + 1)
# Prevent double connection if re-initializing
if btn.pressed.is_connected(_on_avatar_selected):
btn.pressed.disconnect(_on_avatar_selected)
btn.pressed.connect(_on_avatar_selected.bind(i))
func _on_change_avatar_pressed() -> void:
avatar_popup.popup_centered()
func _on_avatar_selected(index: int) -> void:
avatar_popup.hide()
_set_status("Saving avatar...", Color.WHITE)
var ok: bool = await UserProfileManager.update_avatar(index)
if ok:
var url: String = UserProfileManager.get_avatar_url()
if ResourceLoader.exists(url):
avatar_display.texture = load(url) as Texture2D
_set_status("Avatar updated!", Color(0.4, 1.0, 0.4))
else:
_set_status("Failed to update avatar.", Color.RED)
# ─────────────────────────────────────────────────────────────
# Display name
# ─────────────────────────────────────────────────────────────
func _on_save_name_pressed() -> void:
var new_name := display_name_input.text.strip_edges()
if new_name.is_empty():
_set_status("Name cannot be empty.", Color.RED)
return
_set_status("Saving...", Color.WHITE)
save_name_btn.disabled = true
var ok: bool = await UserProfileManager.update_display_name(new_name)
save_name_btn.disabled = false
if ok:
_set_status("Name updated!", Color(0.4, 1.0, 0.4))
emit_signal("profile_updated")
await get_tree().create_timer(3.0).timeout
status_label.text = ""
else:
_set_status("Failed to update name.", Color.RED)
# ─────────────────────────────────────────────────────────────
# Account
# ─────────────────────────────────────────────────────────────
func _on_link_account_pressed() -> void:
link_email_input.text = ""
link_pass_input.text = ""
link_email_dialog.popup_centered()
func _on_link_account_confirmed() -> void:
var e := link_email_input.text.strip_edges()
var p := link_pass_input.text
if e.is_empty() or p.is_empty():
_set_status("Fill in all fields.", Color.RED); return
_set_status("Linking account...", Color.WHITE)
var ok: bool = await AuthManager.link_email(e, p)
_set_status(
"Account linked!" if ok else "Failed to link account.",
Color(0.4, 1.0, 0.4) if ok else Color.RED
)
if ok:
link_account_btn.visible = false
account_type_label.text = "Account: Email"
func _setup_account_settings_ui() -> void:
tz_dropdown.clear()
for i in range(-12, 15):
tz_dropdown.add_item("GMT %s%d" % ["+" if i >= 0 else "", i])
submit_cred_btn.pressed.connect(func():
_set_status("Updating credentials...", Color.WHITE)
var payload := {
"current_password": old_pass_input.text,
"new_email": new_email_input.text,
"new_password": new_pass_input.text
}
var r = await NakamaManager.client.rpc_async(
NakamaManager.session, "change_credentials", JSON.stringify(payload)
)
if r.is_exception():
_set_status("Error: " + r.get_exception().message, Color.RED)
else:
_set_status("Credentials updated!", Color(0.4, 1.0, 0.4))
acc_settings_dialog.hide()
)
save_tz_btn.pressed.connect(func():
var sel := tz_dropdown.get_item_text(tz_dropdown.selected)
var r = await NakamaManager.client.update_account_async(
NakamaManager.session, null, null, null, null, null, sel
)
_set_status(
"Timezone saved!" if not r.is_exception() else "TZ error: " + r.get_exception().message,
Color(0.4, 1.0, 0.4) if not r.is_exception() else Color.RED
)
)
reset_stats_btn.pressed.connect(func():
reset_stats_dialog.popup_centered()
)
func _on_reset_stats_confirmed() -> void:
var r = await NakamaManager.client.rpc_async(NakamaManager.session, "reset_stats", "{}")
if not r.is_exception():
UserProfileManager.stats = {
"games_played": 0, "games_won": 0, "games_lost": 0,
"total_score": 0, "high_score": 0, "play_time_minutes": 0
}
_load_profile_data()
_set_status("Stats wiped.", Color(1, 0.5, 0.3))
acc_settings_dialog.hide()
func _open_account_settings() -> void:
old_pass_input.visible = not AuthManager.is_guest
old_pass_input.text = ""; new_email_input.text = ""; new_pass_input.text = ""
acc_settings_dialog.popup_centered()
func _on_logout_pressed() -> void:
AuthManager.logout()
get_tree().change_scene_to_file("res://scenes/ui/login_screen.tscn")
func _on_admin_panel_pressed() -> void:
AdminManager.toggle_admin_panel()
func _on_topup_gold_pressed() -> void:
_set_status("Topping up gold...", Color.WHITE)
topup_gold_btn.disabled = true
var ok: bool = await UserProfileManager.admin_topup_gold()
topup_gold_btn.disabled = false
if ok:
gold_label.text = str(UserProfileManager.wallet.get("gold", 0))
_set_status("Top-up successful!", Color(0.4, 1.0, 0.4))
else:
_set_status("Top-up failed.", Color.RED)
# ─────────────────────────────────────────────────────────────
# Show / Close
# ─────────────────────────────────────────────────────────────
func _on_close_pressed() -> void:
hide()
emit_signal("closed")
func show_panel() -> void:
if not UserProfileManager.is_profile_loaded:
_set_status("Loading profile...", Color.YELLOW)
_load_profile_data()
_load_loadout()
_check_admin_visibility()
show()
if AuthManager.is_guest:
_on_link_account_pressed()
_set_status("Link an email to save progress permanently!", Color.YELLOW)
# Reload inventory from server to guarantee fresh data, then refresh the grid
_set_status("Refreshing inventory...", Color.WHITE)
await UserProfileManager.load_inventory()
# Auto-select the first tab that actually has items (avoids confusing empty grid)
var found_tab := false
for cat in ["head", "costume", "glove", "accessory"]:
_current_category = cat
_rebuild_category_items()
if not _category_items.is_empty():
found_tab = true
break
if not found_tab:
_current_category = "head"
_rebuild_category_items()
_current_page = 0
_populate_item_grid()
_highlight_active_tab()
_set_status("", Color.WHITE)
func _check_admin_visibility() -> void:
admin_panel_btn.hide()
topup_gold_btn.hide()
if not NakamaManager.client or not NakamaManager.session: return
var account = await NakamaManager.client.get_account_async(NakamaManager.session)
if account.is_exception(): return
var raw: String = account.user.metadata if account.user.metadata else "{}"
var meta = JSON.parse_string(raw)
if meta is Dictionary and meta.get("role", "") in ["owner", "admin"]:
admin_panel_btn.show()
topup_gold_btn.show()
# ─────────────────────────────────────────────────────────────
# Helpers
# ─────────────────────────────────────────────────────────────
func _set_status(msg: String, color: Color = Color.WHITE) -> void:
status_label.add_theme_color_override("font_color", color)
status_label.text = msg
func _on_profile_updated() -> void:
_load_profile_data()
_rebuild_category_items()
_populate_item_grid()
func _on_profile_update_failed(error: String) -> void:
_set_status(error, Color.RED)
await get_tree().create_timer(3.0).timeout
status_label.text = ""