feat: overhaul UI main and profile
This commit is contained in:
@@ -9,6 +9,9 @@ signal avatar_changed(url: String)
|
||||
# Profile data
|
||||
var profile: Dictionary = {}
|
||||
var stats: Dictionary = {}
|
||||
var wallet: Dictionary = {"gold": 0, "star": 0}
|
||||
var inventory: Array = []
|
||||
var loadout: Dictionary = {"head": "", "costume": "", "glove": "", "accessory": ""}
|
||||
var is_profile_loaded: bool = false
|
||||
|
||||
# Nakama storage collection names
|
||||
@@ -47,6 +50,8 @@ func load_profile() -> Dictionary:
|
||||
# Reset state first to ensure no old account data carries over
|
||||
profile = {}
|
||||
stats = {}
|
||||
wallet = {"gold": 0, "star": 0}
|
||||
inventory = []
|
||||
is_profile_loaded = false
|
||||
|
||||
if not NakamaManager.session:
|
||||
@@ -84,6 +89,18 @@ func load_profile() -> Dictionary:
|
||||
var stored_data = JSON.parse_string(storage_result.objects[0].value)
|
||||
if stored_data:
|
||||
profile.merge(stored_data, true)
|
||||
if stored_data.has("loadout"):
|
||||
loadout = stored_data["loadout"]
|
||||
|
||||
# Parse Wallet
|
||||
if account.wallet:
|
||||
var w_data = JSON.parse_string(account.wallet)
|
||||
if w_data:
|
||||
wallet["gold"] = w_data.get("gold", 0)
|
||||
wallet["star"] = w_data.get("star", 0)
|
||||
|
||||
# Load Inventory
|
||||
await load_inventory()
|
||||
|
||||
# Load stats
|
||||
await load_stats()
|
||||
@@ -98,6 +115,21 @@ func load_profile() -> Dictionary:
|
||||
|
||||
return profile
|
||||
|
||||
func load_inventory() -> void:
|
||||
if not NakamaManager.session: return
|
||||
|
||||
inventory.clear()
|
||||
var result = await NakamaManager.client.list_storage_objects_async(
|
||||
NakamaManager.session,
|
||||
"inventory",
|
||||
NakamaManager.session.user_id,
|
||||
100
|
||||
)
|
||||
|
||||
if not result.is_exception() and result.objects:
|
||||
for obj in result.objects:
|
||||
inventory.append(obj.key)
|
||||
|
||||
func load_stats() -> Dictionary:
|
||||
# Reset stats first to ensure fresh data for new logins
|
||||
stats = {}
|
||||
@@ -223,7 +255,8 @@ func _save_profile_data() -> bool:
|
||||
"avatar_index": profile.get("avatar_index", 0),
|
||||
"bio": profile.get("bio", ""),
|
||||
"country": profile.get("country", ""),
|
||||
"language": profile.get("language", "en")
|
||||
"language": profile.get("language", "en"),
|
||||
"loadout": loadout
|
||||
}
|
||||
|
||||
var write_obj := NakamaWriteStorageObject.new(
|
||||
@@ -247,6 +280,41 @@ func _save_profile_data() -> bool:
|
||||
emit_signal("profile_updated")
|
||||
return true
|
||||
|
||||
func update_loadout(category: String, item_id: String) -> bool:
|
||||
if not loadout.has(category):
|
||||
return false
|
||||
loadout[category] = item_id
|
||||
return await _save_profile_data()
|
||||
|
||||
func purchase_item(item_id: String, price_gold: int, price_star: int, category: String) -> bool:
|
||||
if not NakamaManager.session: return false
|
||||
|
||||
var payload = JSON.stringify({
|
||||
"item_id": item_id,
|
||||
"price_gold": price_gold,
|
||||
"price_star": price_star,
|
||||
"category": category
|
||||
})
|
||||
|
||||
var result = await NakamaManager.client.rpc_async(
|
||||
NakamaManager.session,
|
||||
"purchase_item",
|
||||
payload
|
||||
)
|
||||
|
||||
if result.is_exception():
|
||||
push_error("[UserProfileManager] Purchase failed: ", result.get_exception().message)
|
||||
return false
|
||||
|
||||
# Update local cache
|
||||
if price_gold > 0: wallet["gold"] -= price_gold
|
||||
if price_star > 0: wallet["star"] -= price_star
|
||||
if not inventory.has(item_id):
|
||||
inventory.append(item_id)
|
||||
|
||||
emit_signal("profile_updated")
|
||||
return true
|
||||
|
||||
# =============================================================================
|
||||
# Stats Management
|
||||
# =============================================================================
|
||||
|
||||
+443
-248
@@ -1,163 +1,369 @@
|
||||
extends Control
|
||||
## Profile panel controller — full-scene loadout screen.
|
||||
## Left: profile info + default character selector.
|
||||
## Right: 3D SubViewport character preview.
|
||||
## 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
|
||||
# -------------------------------------------------------------------------
|
||||
@onready var back_btn := %BackBtn as Button
|
||||
@onready var avatar_display := %AvatarDisplay as TextureRect
|
||||
@onready var change_avatar_btn := %ChangeAvatarBtn as Button
|
||||
@onready var display_name_input := %DisplayNameInput as LineEdit
|
||||
@onready var save_name_btn := %SaveNameBtn as Button
|
||||
@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 account_type_label := %AccountType as Label
|
||||
@onready var link_account_btn := %LinkAccountBtn as Button
|
||||
@onready var admin_panel_btn := %AdminPanelBtn as Button
|
||||
@onready var logout_btn := %LogoutBtn as Button
|
||||
@onready var status_label := %StatusLabel as Label
|
||||
@onready var avatar_popup := %AvatarSelectionPopup as PopupPanel
|
||||
@onready var avatar_grid := %GridContainer as GridContainer
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# UI references (all resolved via %UniqueName — must match .tscn)
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
||||
# Account Settings refs
|
||||
@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
|
||||
# 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
|
||||
|
||||
# Loadout refs
|
||||
@onready var char_left_btn := %CharLeftBtn as Button
|
||||
@onready var char_right_btn := %CharRightBtn as Button
|
||||
# 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 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 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
|
||||
|
||||
# 3D Preview refs
|
||||
@onready var character_root := %CharacterRoot as Node3D
|
||||
@onready var anim_player: AnimationPlayer
|
||||
# Back button + popups
|
||||
@onready var back_btn := %BackBtn 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
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# State
|
||||
# -------------------------------------------------------------------------
|
||||
# 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"]
|
||||
# Maps game character name -> GLB node name in the SubViewport
|
||||
# Must match the mapping in player.gd's set_character()
|
||||
const CHAR_NODE_MAP: Dictionary = {
|
||||
"Copper": "Oldpop",
|
||||
"Dabro": "Masbro",
|
||||
"Gatot": "Gatot",
|
||||
"Pip": "Bob"
|
||||
"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 }
|
||||
const ITEM_CATALOG: Dictionary = {
|
||||
"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},
|
||||
}
|
||||
const ITEMS_PER_PAGE: int = 9
|
||||
|
||||
var _loadout_index: int = 0 # Index into CHARACTERS
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 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()
|
||||
_load_profile_data()
|
||||
_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)
|
||||
|
||||
UserProfileManager.profile_loaded.connect(func(p): _on_profile_updated())
|
||||
# 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"))
|
||||
|
||||
# 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)
|
||||
|
||||
# Dynamically inject Account Settings button
|
||||
var acc_settings_btn = Button.new()
|
||||
acc_settings_btn.text = "Account Settings"
|
||||
acc_settings_btn.custom_minimum_size = Vector2(0, 44)
|
||||
acc_settings_btn.add_theme_font_override("font", load("res://assets/fonts/Nougat-ExtraBlack.ttf"))
|
||||
acc_settings_btn.pressed.connect(_open_account_settings)
|
||||
# Insert it before Logout button
|
||||
var logout_idx = logout_btn.get_index()
|
||||
logout_btn.get_parent().add_child(acc_settings_btn)
|
||||
logout_btn.get_parent().move_child(acc_settings_btn, logout_idx)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Profile
|
||||
# -------------------------------------------------------------------------
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Profile data
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
func _load_profile_data() -> void:
|
||||
var profile := UserProfileManager.profile
|
||||
var prof := UserProfileManager.profile
|
||||
var stats := UserProfileManager.stats
|
||||
|
||||
display_name_input.text = profile.get("display_name", "Guest")
|
||||
display_name_input.text = prof.get("display_name", "")
|
||||
display_name_input.max_length = 6
|
||||
|
||||
var avatar_url: String = UserProfileManager.get_avatar_url()
|
||||
if ResourceLoader.exists(avatar_url):
|
||||
avatar_display.texture = load(avatar_url)
|
||||
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)
|
||||
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"
|
||||
account_type_label.text = "Account : Guest"
|
||||
link_account_btn.visible = true
|
||||
link_account_btn.text = "Link Email (Keep Progress)"
|
||||
else:
|
||||
var mode_name := _get_auth_mode_name(AuthManager.auth_mode)
|
||||
account_type_label.text = "Account: %s" % mode_name
|
||||
account_type_label.text = "Account : %s" % _auth_mode_name(AuthManager.auth_mode)
|
||||
link_account_btn.visible = false
|
||||
|
||||
status_label.text = ""
|
||||
|
||||
func _get_auth_mode_name(mode: int) -> String:
|
||||
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"
|
||||
AuthManager.AuthMode.EMAIL: return "Email"
|
||||
AuthManager.AuthMode.GOOGLE: return "Google"
|
||||
AuthManager.AuthMode.APPLE: return "Apple"
|
||||
AuthManager.AuthMode.FACEBOOK: return "Facebook"
|
||||
_: return "Guest"
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Loadout
|
||||
# -------------------------------------------------------------------------
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 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
|
||||
}
|
||||
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()
|
||||
var prefix := _current_category + "_"
|
||||
for item_id: String in UserProfileManager.inventory:
|
||||
if item_id.begins_with(prefix):
|
||||
_category_items.append(item_id)
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Item grid
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
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, "")
|
||||
equip_btn.text = "✓ Equipped" if equipped == item_id else "Equip"
|
||||
equip_btn.disabled = (equipped == item_id)
|
||||
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 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))
|
||||
_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:
|
||||
"""Load the saved default character from profile storage."""
|
||||
var saved = UserProfileManager.profile.get("loadout_character", "Copper")
|
||||
var idx = CHARACTERS.find(saved)
|
||||
_loadout_index = max(idx, 0)
|
||||
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()
|
||||
_update_3d_preview(CHARACTERS[_loadout_index])
|
||||
|
||||
func _refresh_loadout_ui() -> void:
|
||||
var char_name := CHARACTERS[_loadout_index]
|
||||
loadout_char_name.text = char_name
|
||||
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.text = "✓ DEFAULT" if is_default else "Set as Default"
|
||||
set_default_btn.disabled = is_default
|
||||
_update_3d_preview(char_name)
|
||||
|
||||
@@ -165,50 +371,42 @@ func _on_set_default_pressed() -> void:
|
||||
var char_name := CHARACTERS[_loadout_index]
|
||||
_default_character = char_name
|
||||
UserProfileManager.profile["loadout_character"] = char_name
|
||||
# Also apply immediately to LobbyManager
|
||||
if LobbyManager.available_characters.has(char_name):
|
||||
LobbyManager.local_character_index = LobbyManager.available_characters.find(char_name)
|
||||
status_label.text = "Loadout set to: " + char_name
|
||||
status_label.add_theme_color_override("font_color", Color(0.4, 0.8, 0.4))
|
||||
_set_status("Default set to: " + char_name, Color(0.4, 0.9, 0.4))
|
||||
_refresh_loadout_ui()
|
||||
# Persist to storage
|
||||
_save_loadout_to_profile()
|
||||
# Sync to leaderboard immediately
|
||||
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:
|
||||
"""Save loadout_character field to Nakama profile storage."""
|
||||
if not NakamaManager.session:
|
||||
return
|
||||
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
|
||||
"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), ""
|
||||
)
|
||||
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:
|
||||
anim_player = character_root.get_node_or_null("AnimationPlayer")
|
||||
_update_3d_preview(_default_character)
|
||||
|
||||
func _update_3d_preview(character_name: String) -> void:
|
||||
if not character_root:
|
||||
return
|
||||
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)
|
||||
# Update AnimationPlayer root
|
||||
if anim_player:
|
||||
var new_root := character_root.get_node_or_null(node_name)
|
||||
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"):
|
||||
@@ -216,149 +414,153 @@ func _update_3d_preview(character_name: String) -> void:
|
||||
elif anim_player.get_animation_list().size() > 0:
|
||||
anim_player.play(anim_player.get_animation_list()[0])
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 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:
|
||||
for child in avatar_grid.get_children():
|
||||
child.queue_free()
|
||||
for i in range(UserProfileManager.AVATARS.size()):
|
||||
var avatar_path: String = UserProfileManager.AVATARS[i]
|
||||
var btn := Button.new()
|
||||
btn.custom_minimum_size = Vector2(64, 64)
|
||||
if ResourceLoader.exists(avatar_path):
|
||||
var tex := load(avatar_path) as Texture2D
|
||||
btn.icon = tex
|
||||
# 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))
|
||||
avatar_grid.add_child(btn)
|
||||
|
||||
func _on_change_avatar_pressed() -> void:
|
||||
avatar_popup.popup_centered()
|
||||
|
||||
func _on_avatar_selected(index: int) -> void:
|
||||
avatar_popup.hide()
|
||||
status_label.text = "Saving avatar..."
|
||||
var success := await UserProfileManager.update_avatar(index)
|
||||
if success:
|
||||
var avatar_url: String = UserProfileManager.get_avatar_url()
|
||||
if ResourceLoader.exists(avatar_url):
|
||||
avatar_display.texture = load(avatar_url)
|
||||
status_label.text = "Avatar updated!"
|
||||
_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:
|
||||
status_label.text = "Failed to update avatar"
|
||||
_set_status("Failed to update avatar.", Color.RED)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Name
|
||||
# -------------------------------------------------------------------------
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Display name
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
func _on_save_name_pressed() -> void:
|
||||
var new_name := display_name_input.text.strip_edges()
|
||||
if new_name.is_empty():
|
||||
status_label.text = "Name cannot be empty"
|
||||
_set_status("Name cannot be empty.", Color.RED)
|
||||
return
|
||||
status_label.text = "Saving..."
|
||||
_set_status("Saving...", Color.WHITE)
|
||||
save_name_btn.disabled = true
|
||||
var success := await UserProfileManager.update_display_name(new_name)
|
||||
var ok: bool = await UserProfileManager.update_display_name(new_name)
|
||||
save_name_btn.disabled = false
|
||||
if success:
|
||||
status_label.add_theme_color_override("font_color", Color.GREEN)
|
||||
status_label.text = "Name updated!"
|
||||
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:
|
||||
var dialog := AcceptDialog.new()
|
||||
dialog.title = "Link Email"
|
||||
dialog.dialog_text = "Enter your email and password to link this guest account.\nYour progress will be preserved!"
|
||||
var vbox := VBoxContainer.new()
|
||||
var email_input := LineEdit.new()
|
||||
email_input.placeholder_text = "Email"
|
||||
var password_input := LineEdit.new()
|
||||
password_input.placeholder_text = "Password"
|
||||
password_input.secret = true
|
||||
vbox.add_child(email_input)
|
||||
vbox.add_child(password_input)
|
||||
dialog.add_child(vbox)
|
||||
add_child(dialog)
|
||||
dialog.popup_centered()
|
||||
dialog.confirmed.connect(func():
|
||||
var email := email_input.text.strip_edges()
|
||||
var password := password_input.text
|
||||
if email.is_empty() or password.is_empty():
|
||||
status_label.text = "Please fill in all fields"
|
||||
return
|
||||
status_label.text = "Linking account..."
|
||||
var success := await AuthManager.link_email(email, password)
|
||||
if success:
|
||||
status_label.text = "Account linked successfully!"
|
||||
link_account_btn.visible = false
|
||||
account_type_label.text = "Account: Email"
|
||||
else:
|
||||
status_label.text = "Failed to link account"
|
||||
dialog.queue_free()
|
||||
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:
|
||||
# Populate Timezone dropdown
|
||||
tz_dropdown.clear()
|
||||
for i in range(-12, 15):
|
||||
var prefix = "+" if i >= 0 else ""
|
||||
tz_dropdown.add_item("GMT " + prefix + str(i))
|
||||
|
||||
# Connect buttons
|
||||
tz_dropdown.add_item("GMT %s%d" % ["+" if i >= 0 else "", i])
|
||||
|
||||
submit_cred_btn.pressed.connect(func():
|
||||
status_label.text = "Updating credentials..."
|
||||
var payload = {
|
||||
_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
|
||||
"new_email": new_email_input.text,
|
||||
"new_password": new_pass_input.text
|
||||
}
|
||||
var result = await NakamaManager.client.rpc_async(NakamaManager.session, "change_credentials", JSON.stringify(payload))
|
||||
if result.is_exception():
|
||||
status_label.text = "Failed: " + result.get_exception().message
|
||||
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:
|
||||
status_label.text = "Credentials updated successfully!"
|
||||
_set_status("Credentials updated!", Color(0.4, 1.0, 0.4))
|
||||
acc_settings_dialog.hide()
|
||||
)
|
||||
|
||||
|
||||
save_tz_btn.pressed.connect(func():
|
||||
var selected_text = tz_dropdown.get_item_text(tz_dropdown.selected)
|
||||
var res = await NakamaManager.client.update_account_async(NakamaManager.session, null, null, null, null, null, selected_text)
|
||||
if res.is_exception():
|
||||
status_label.text = "TZ Failed: " + res.get_exception().message
|
||||
else:
|
||||
status_label.text = "Timezone saved!"
|
||||
)
|
||||
|
||||
reset_stats_btn.pressed.connect(func():
|
||||
var conf = ConfirmationDialog.new()
|
||||
conf.dialog_text = "Are you SURE you want to irreversibly wipe all your stats to 0?"
|
||||
acc_settings_dialog.add_child(conf)
|
||||
conf.popup_centered()
|
||||
conf.confirmed.connect(func():
|
||||
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()
|
||||
status_label.text = "Stats wiped completely."
|
||||
conf.queue_free()
|
||||
acc_settings_dialog.hide()
|
||||
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 = ""
|
||||
old_pass_input.text = ""; new_email_input.text = ""; new_pass_input.text = ""
|
||||
acc_settings_dialog.popup_centered()
|
||||
|
||||
func _on_logout_pressed() -> void:
|
||||
@@ -366,58 +568,51 @@ func _on_logout_pressed() -> void:
|
||||
get_tree().change_scene_to_file("res://scenes/ui/login_screen.tscn")
|
||||
|
||||
func _on_admin_panel_pressed() -> void:
|
||||
if has_node("/root/AdminManager"):
|
||||
get_node("/root/AdminManager").toggle_admin_panel()
|
||||
else:
|
||||
AdminManager.toggle_admin_panel()
|
||||
AdminManager.toggle_admin_panel()
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Close / Show
|
||||
# -------------------------------------------------------------------------
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Show / Close
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
func _on_close_pressed() -> void:
|
||||
hide()
|
||||
emit_signal("closed")
|
||||
|
||||
func show_panel() -> void:
|
||||
if not UserProfileManager.is_profile_loaded:
|
||||
status_label.text = "Loading profile from server..."
|
||||
status_label.add_theme_color_override("font_color", Color.YELLOW)
|
||||
|
||||
_set_status("Loading profile...", Color.YELLOW)
|
||||
_load_profile_data()
|
||||
_load_loadout()
|
||||
_rebuild_category_items()
|
||||
_populate_item_grid()
|
||||
_check_admin_visibility()
|
||||
show()
|
||||
|
||||
if AuthManager.is_guest:
|
||||
_on_link_account_pressed()
|
||||
status_label.text = "Please link an email to save your progress permanently!"
|
||||
_set_status("Link an email to save progress permanently!", Color.YELLOW)
|
||||
|
||||
func _check_admin_visibility() -> void:
|
||||
admin_panel_btn.hide()
|
||||
if not NakamaManager.client or not NakamaManager.session:
|
||||
return
|
||||
|
||||
# Use native Nakama account API to check user metadata for admin role
|
||||
if not NakamaManager.client or not NakamaManager.session: return
|
||||
var account = await NakamaManager.client.get_account_async(NakamaManager.session)
|
||||
if account.is_exception():
|
||||
return
|
||||
|
||||
# Check account metadata for role
|
||||
var metadata_str: String = account.user.metadata if account.user.metadata else "{}"
|
||||
var metadata = JSON.parse_string(metadata_str)
|
||||
if metadata and metadata is Dictionary:
|
||||
var role = metadata.get("role", "")
|
||||
if role in ["owner", "admin"]:
|
||||
admin_panel_btn.show()
|
||||
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()
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Helpers
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
func _set_status(msg: String, color: Color = Color.WHITE) -> void:
|
||||
status_label.add_theme_color_override("font_color", color)
|
||||
status_label.text = msg
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Signal handlers
|
||||
# -------------------------------------------------------------------------
|
||||
func _on_profile_updated() -> void:
|
||||
_load_profile_data()
|
||||
_rebuild_category_items()
|
||||
_populate_item_grid()
|
||||
|
||||
func _on_profile_update_failed(error: String) -> void:
|
||||
status_label.add_theme_color_override("font_color", Color.RED)
|
||||
status_label.text = error
|
||||
_set_status(error, Color.RED)
|
||||
await get_tree().create_timer(3.0).timeout
|
||||
status_label.text = ""
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
extends Control
|
||||
|
||||
signal closed
|
||||
|
||||
@onready var tab_container = $Panel/VBoxContainer/TabContainer
|
||||
@onready var head_grid = $Panel/VBoxContainer/TabContainer/Head/GridContainer
|
||||
@onready var costume_grid = $Panel/VBoxContainer/TabContainer/Costume/GridContainer
|
||||
@onready var glove_grid = $Panel/VBoxContainer/TabContainer/Glove/GridContainer
|
||||
@onready var accessory_grid = $Panel/VBoxContainer/TabContainer/Accessory/GridContainer
|
||||
@onready var status_label = $Panel/VBoxContainer/StatusLabel
|
||||
@onready var back_btn = $Panel/VBoxContainer/Header/BackBtn
|
||||
|
||||
var shop_items = {
|
||||
"head": [
|
||||
{"id": "head_hat1", "name": "Cap", "gold": 100, "star": 0},
|
||||
{"id": "head_crown", "name": "Crown", "gold": 0, "star": 50}
|
||||
],
|
||||
"costume": [
|
||||
{"id": "costume_red", "name": "Red Suit", "gold": 200, "star": 0},
|
||||
{"id": "costume_gold", "name": "Gold Suit", "gold": 0, "star": 100}
|
||||
],
|
||||
"glove": [
|
||||
{"id": "glove_leather", "name": "Leather Gloves", "gold": 50, "star": 0}
|
||||
],
|
||||
"accessory": [
|
||||
{"id": "acc_glasses", "name": "Sunglasses", "gold": 80, "star": 0}
|
||||
]
|
||||
}
|
||||
|
||||
func _ready():
|
||||
back_btn.pressed.connect(_on_close)
|
||||
_build_shop()
|
||||
|
||||
func show_panel():
|
||||
show()
|
||||
_refresh_shop()
|
||||
|
||||
func _build_shop():
|
||||
_populate_grid(head_grid, shop_items["head"])
|
||||
_populate_grid(costume_grid, shop_items["costume"])
|
||||
_populate_grid(glove_grid, shop_items["glove"])
|
||||
_populate_grid(accessory_grid, shop_items["accessory"])
|
||||
|
||||
func _populate_grid(grid, items):
|
||||
for child in grid.get_children():
|
||||
child.queue_free()
|
||||
|
||||
for item in items:
|
||||
var btn = Button.new()
|
||||
btn.custom_minimum_size = Vector2(120, 80)
|
||||
btn.add_theme_font_size_override("font_size", 14)
|
||||
btn.text = "%s\nGold: %d\nStar: %d" % [item.name, item.gold, item.star]
|
||||
btn.pressed.connect(_on_buy_pressed.bind(item))
|
||||
grid.add_child(btn)
|
||||
|
||||
func _refresh_shop():
|
||||
# Visual update to show which items are owned
|
||||
# (For simplicity, not disabling buttons directly, relying on backend check)
|
||||
status_label.text = "Welcome to the Shop!"
|
||||
|
||||
func _on_buy_pressed(item: Dictionary):
|
||||
if UserProfileManager.inventory.has(item.id):
|
||||
status_label.text = "Already owned: " + item.name
|
||||
return
|
||||
|
||||
var price_gold = item.gold
|
||||
var price_star = item.star
|
||||
|
||||
if UserProfileManager.wallet.get("gold", 0) < price_gold or UserProfileManager.wallet.get("star", 0) < price_star:
|
||||
status_label.text = "Not enough currency for " + item.name
|
||||
return
|
||||
|
||||
status_label.text = "Purchasing " + item.name + "..."
|
||||
|
||||
# Determine category
|
||||
var category = ""
|
||||
if shop_items["head"].has(item): category = "head"
|
||||
elif shop_items["costume"].has(item): category = "costume"
|
||||
elif shop_items["glove"].has(item): category = "glove"
|
||||
elif shop_items["accessory"].has(item): category = "accessory"
|
||||
|
||||
var success = await UserProfileManager.purchase_item(item.id, price_gold, price_star, category)
|
||||
if success:
|
||||
status_label.text = "Successfully purchased: " + item.name
|
||||
_refresh_shop()
|
||||
else:
|
||||
status_label.text = "Failed to purchase. Backend error."
|
||||
|
||||
func _on_close():
|
||||
hide()
|
||||
emit_signal("closed")
|
||||
@@ -0,0 +1 @@
|
||||
uid://w0ddjofws4ib
|
||||
Reference in New Issue
Block a user