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" AuthManager.AuthMode.STEAM: return "Steam" _: 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 = ""