feat: 2.3.2

This commit is contained in:
2026-05-19 17:30:29 +08:00
parent 7ca11c6534
commit 8430d1054e
39 changed files with 6581 additions and 738 deletions
+32 -17
View File
@@ -25,6 +25,7 @@ var fragments: Dictionary = {} # frag_common, frag_uncommon, frag_rare
var inventory: Array = []
var loadout: Dictionary = {"head": "", "costume": "", "glove": "", "accessory": ""}
var shop_catalog: Dictionary = {}
var featured_banners: Array = []
var is_profile_loaded: bool = false
# Nakama storage collection names
@@ -311,14 +312,13 @@ func update_loadout(category: String, item_id: String) -> bool:
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
func purchase_item(item_id: String) -> String:
if not NakamaManager.session: return "Not authenticated"
var payload = JSON.stringify({
"item_id": item_id,
"price_gold": price_gold,
"price_star": price_star,
"category": category
"quantity": 1,
"idempotency_key": str(randi()) + "_" + str(Time.get_ticks_usec())
})
var result = await NakamaManager.client.rpc_async(
@@ -328,17 +328,20 @@ func purchase_item(item_id: String, price_gold: int, price_star: int, category:
)
if result.is_exception():
push_error("[UserProfileManager] Purchase failed: ", result.get_exception().message)
return false
var msg = result.get_exception().message
push_error("[UserProfileManager] Purchase failed: ", msg)
return msg
# 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
var response = JSON.parse_string(result.payload)
if typeof(response) == TYPE_DICTIONARY and response.has("success") and response.success == true:
await _reload_wallet()
if not inventory.has(item_id):
inventory.append(item_id)
emit_signal("profile_updated")
return ""
return "Unknown error"
func fetch_shop_catalog() -> void:
if not NakamaManager.session: return
@@ -356,6 +359,8 @@ func fetch_shop_catalog() -> void:
var payload: Dictionary = JSON.parse_string(result.payload)
if payload and payload.has("catalog"):
shop_catalog = payload.catalog
if payload.has("featured_banners"):
featured_banners = payload.get("featured_banners", [])
emit_signal("profile_updated")
## Admin-only: grants a large amount of gold via a server-authoritative RPC.
@@ -377,7 +382,9 @@ func buy_currency(package_id: String) -> bool:
if not NakamaManager.session: return false
var payload = JSON.stringify({
"package_id": package_id
"package_id": package_id,
"idempotency_key": str(randi()) + "_" + str(Time.get_ticks_usec()),
"receipt": "mock_receipt_for_now"
})
var result = await NakamaManager.client.rpc_async(
@@ -387,9 +394,17 @@ func buy_currency(package_id: String) -> bool:
)
if result.is_exception():
push_error("[UserProfileManager] Failed to buy currency: ", result.get_exception().message)
var msg = result.get_exception().message
if "NotEnoughFunds" in msg:
push_error("[UserProfileManager] Failed to buy currency: Not enough funds.")
else:
push_error("[UserProfileManager] Failed to buy currency: ", msg)
return false
var response = JSON.parse_string(result.payload)
if typeof(response) == TYPE_DICTIONARY and response.has("status") and response.status == "pending":
print("[UserProfileManager] Currency purchase pending verification.")
await _reload_wallet()
return true
+67 -67
View File
@@ -6,43 +6,43 @@ extends Control
## Open scenes/tools/skin_catalog_editor.tscn in the editor, then press F6 (Run Current Scene).
## Edit skins in the form, then click "💾 Save & Generate" to rewrite:
## • scripts/managers/skin_manager.gd (SKIN_CATALOG block)
## • server/nakama/tekton_admin.js (SHOP_CATALOG_DEFS block)
## • server/nakama/economy.js (SHOP_CATALOG_DEFS block)
const DATA_PATH := "res://assets/data/skin_catalog_data.json"
const DATA_PATH := "res://assets/data/skin_catalog_data.json"
const SKIN_MANAGER_PATH := "res://scripts/managers/skin_manager.gd"
const ADMIN_JS_PATH := "res://server/nakama/tekton_admin.js"
const ADMIN_JS_PATH := "res://server/nakama/economy.js"
const CATEGORIES := ["head", "costume", "glove", "accessory"]
const RARITIES := ["Common", "Rare", "Epic", "Legendary"]
const MODES := ["override", "overlay"]
const RARITIES := ["Common", "Rare", "Epic", "Legendary"]
const MODES := ["override", "overlay"]
# Sentinel markers — must match what's in the target files
const BEGIN_SKIN := "## [BEGIN_SKIN_CATALOG]"
const END_SKIN := "## [END_SKIN_CATALOG]"
const END_SKIN := "## [END_SKIN_CATALOG]"
const BEGIN_SHOP := "// [BEGIN_SHOP_CATALOG_DEFS]"
const END_SHOP := "// [END_SHOP_CATALOG_DEFS]"
const END_SHOP := "// [END_SHOP_CATALOG_DEFS]"
# ─── State ───────────────────────────────────────────────────────────────────
var _data: Array = []
var _selected_idx: int = -1
var _dirty: bool = false
var _data: Array = []
var _selected_idx: int = -1
var _dirty: bool = false
# ─── UI refs (built in code) ─────────────────────────────────────────────────
var _skin_list_vbox: VBoxContainer
var _status_label: Label
var _form_panel: PanelContainer
var _no_sel_label: Label
var _form_item_id: LineEdit
var _form_name: LineEdit
var _form_character: LineEdit
var _form_gold: SpinBox
var _form_star: SpinBox
var _form_category: OptionButton
var _form_rarity: OptionButton
var _slots_vbox: VBoxContainer
var _duplicate_btn: Button
var _delete_btn: Button
var _save_btn: Button
var _skin_list_vbox: VBoxContainer
var _status_label: Label
var _form_panel: PanelContainer
var _no_sel_label: Label
var _form_item_id: LineEdit
var _form_name: LineEdit
var _form_character: LineEdit
var _form_gold: SpinBox
var _form_star: SpinBox
var _form_category: OptionButton
var _form_rarity: OptionButton
var _slots_vbox: VBoxContainer
var _duplicate_btn: Button
var _delete_btn: Button
var _save_btn: Button
# ─────────────────────────────────────────────────────────────────────────────
func _ready() -> void:
@@ -54,7 +54,7 @@ func _ready() -> void:
# UI Construction
# ─────────────────────────────────────────────────────────────────────────────
func _build_ui() -> void:
anchor_right = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
# Root VBox
@@ -137,13 +137,13 @@ func _build_ui() -> void:
# Form fields
form_vbox.add_child(_section_label("── Item Info ───────────────────────────"))
_form_item_id = _field(form_vbox, "Item ID", "e.g. oldpop_hat1")
_form_name = _field(form_vbox, "Display Name", "e.g. Oldpop Hat I")
_form_character = _field(form_vbox, "Character (node)", "e.g. Oldpop, Masbro, Bob")
_form_gold = _spinbox(form_vbox, "Gold Price", 0, 99999)
_form_star = _spinbox(form_vbox, "Star Price", 0, 99999)
_form_category = _option(form_vbox, "Category", CATEGORIES)
_form_rarity = _option(form_vbox, "Rarity", RARITIES)
_form_item_id = _field(form_vbox, "Item ID", "e.g. oldpop_hat1")
_form_name = _field(form_vbox, "Display Name", "e.g. Oldpop Hat I")
_form_character = _field(form_vbox, "Character (node)", "e.g. Oldpop, Masbro, Bob")
_form_gold = _spinbox(form_vbox, "Gold Price", 0, 99999)
_form_star = _spinbox(form_vbox, "Star Price", 0, 99999)
_form_category = _option(form_vbox, "Category", CATEGORIES)
_form_rarity = _option(form_vbox, "Rarity", RARITIES)
# ── Slots section ─────────────────────────────────────────────────────────
var slots_hdr := HBoxContainer.new()
@@ -280,23 +280,23 @@ func _on_list_item_pressed(idx: int) -> void:
# ─────────────────────────────────────────────────────────────────────────────
func _populate_form() -> void:
if _selected_idx < 0 or _selected_idx >= _data.size():
_form_panel.visible = false
_form_panel.visible = false
_no_sel_label.visible = true
_duplicate_btn.disabled = true
_delete_btn.disabled = true
_delete_btn.disabled = true
return
_form_panel.visible = true
_form_panel.visible = true
_no_sel_label.visible = false
_duplicate_btn.disabled = false
_delete_btn.disabled = false
_delete_btn.disabled = false
var e: Dictionary = _data[_selected_idx]
_form_item_id.text = e.get("item_id", "")
_form_name.text = e.get("name", "")
_form_character.text = e.get("character", "")
_form_gold.value = e.get("gold", 0)
_form_star.value = e.get("star", 0)
_form_item_id.text = e.get("item_id", "")
_form_name.text = e.get("name", "")
_form_character.text = e.get("character", "")
_form_gold.value = e.get("gold", 0)
_form_star.value = e.get("star", 0)
var cat_idx := CATEGORIES.find(e.get("category", "head"))
_form_category.selected = max(0, cat_idx)
@@ -355,13 +355,13 @@ func _commit_form() -> void:
if _selected_idx < 0 or _selected_idx >= _data.size():
return
var e: Dictionary = _data[_selected_idx]
e["item_id"] = _form_item_id.text.strip_edges()
e["name"] = _form_name.text.strip_edges()
e["item_id"] = _form_item_id.text.strip_edges()
e["name"] = _form_name.text.strip_edges()
e["character"] = _form_character.text.strip_edges()
e["gold"] = int(_form_gold.value)
e["star"] = int(_form_star.value)
e["category"] = CATEGORIES[_form_category.selected]
e["rarity"] = RARITIES[_form_rarity.selected]
e["gold"] = int(_form_gold.value)
e["star"] = int(_form_star.value)
e["category"] = CATEGORIES[_form_category.selected]
e["rarity"] = RARITIES[_form_rarity.selected]
# Read slots
var slots: Array = []
for row in _slots_vbox.get_children():
@@ -371,8 +371,8 @@ func _commit_form() -> void:
if ch.size() < 3:
continue
slots.append({
"mesh": (ch[0] as LineEdit).text.strip_edges(),
"mode": MODES[(ch[1] as OptionButton).selected],
"mesh": (ch[0] as LineEdit).text.strip_edges(),
"mode": MODES[(ch[1] as OptionButton).selected],
"material": (ch[2] as LineEdit).text.strip_edges(),
})
e["slots"] = slots
@@ -384,14 +384,14 @@ func _on_add_pressed() -> void:
if _selected_idx >= 0:
_commit_form()
_data.append({
"item_id": "new_skin_%d" % _data.size(),
"name": "New Skin",
"category": "head",
"item_id": "new_skin_%d" % _data.size(),
"name": "New Skin",
"category": "head",
"character": "",
"gold": 0,
"star": 0,
"rarity": "Common",
"slots": [],
"gold": 0,
"star": 0,
"rarity": "Common",
"slots": [],
})
_selected_idx = _data.size() - 1
_refresh_list()
@@ -454,7 +454,7 @@ func _on_save_pressed() -> void:
var err_gd := _generate_skin_manager()
var err_js := _generate_admin_js()
if err_gd == OK and err_js == OK:
_set_status("✓ skin_manager.gd and tekton_admin.js updated successfully!", Color(0.4, 1.0, 0.4))
_set_status("✓ skin_manager.gd and economy.js updated successfully!", Color(0.4, 1.0, 0.4))
else:
_set_status("⚠ Some files could not be updated — check the Output log.", Color.YELLOW)
@@ -465,7 +465,7 @@ func _generate_skin_manager() -> int:
if not FileAccess.file_exists(SKIN_MANAGER_PATH):
push_error("[SkinCatalogEditor] File not found: " + SKIN_MANAGER_PATH)
return ERR_FILE_NOT_FOUND
var f := FileAccess.open(SKIN_MANAGER_PATH, FileAccess.READ)
var f := FileAccess.open(SKIN_MANAGER_PATH, FileAccess.READ)
var src: String = f.get_as_text()
f.close()
@@ -516,20 +516,20 @@ func _generate_skin_manager() -> int:
return OK
# ─────────────────────────────────────────────────────────────────────────────
# Code Generation — tekton_admin.js
# Code Generation — economy.js
# ─────────────────────────────────────────────────────────────────────────────
func _generate_admin_js() -> int:
if not FileAccess.file_exists(ADMIN_JS_PATH):
push_error("[SkinCatalogEditor] File not found: " + ADMIN_JS_PATH)
return ERR_FILE_NOT_FOUND
var f := FileAccess.open(ADMIN_JS_PATH, FileAccess.READ)
var f := FileAccess.open(ADMIN_JS_PATH, FileAccess.READ)
var src: String = f.get_as_text()
f.close()
var b := src.find(BEGIN_SHOP)
var e := src.find(END_SHOP)
if b == -1 or e == -1:
push_error("[SkinCatalogEditor] Sentinel markers not found in tekton_admin.js")
push_error("[SkinCatalogEditor] Sentinel markers not found in economy.js")
return ERR_INVALID_DATA
var lines: PackedStringArray = []
@@ -545,12 +545,12 @@ func _generate_admin_js() -> int:
var char_val: String = entry.get("character", "")
var char_part: String = (", character: \"%s\"" % char_val) if not char_val.is_empty() else ""
lines.append(" { id: \"%s\", name: \"%s\", category: \"%s\", gold: %d, star: %d, rarity: \"%s\"%s }," % [
entry.get("item_id", ""),
entry.get("name", ""),
entry.get("item_id", ""),
entry.get("name", ""),
cat,
entry.get("gold", 0),
entry.get("star", 0),
entry.get("rarity", "Common"),
entry.get("gold", 0),
entry.get("star", 0),
entry.get("rarity", "Common"),
char_part,
])
+135 -3
View File
@@ -20,6 +20,9 @@ signal closed
@onready var ban_btn := %BanBtn as Button
@onready var unban_btn := %UnbanBtn as Button
@onready var delete_btn := %DeleteBtn as Button
@onready var history_btn := %HistoryBtn as Button
@onready var history_dialog := %HistoryDialog as AcceptDialog
@onready var history_text := %HistoryText as RichTextLabel
# Tab: Leaderboards
@onready var lb_tree := %LeaderboardTree as Tree
@@ -61,6 +64,11 @@ var _resolved_user_id: String = ""
var _mail_root: TreeItem
var _all_server_mails: Array = []
# Tab: Shop (Featured Banners)
@onready var slots_vbox := %SlotsVBox as VBoxContainer
@onready var load_banners_btn := %LoadBannersBtn as Button
@onready var save_banners_btn := %SaveBannersBtn as Button
const MONTH_NAMES = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
# -- Data --
@@ -178,6 +186,7 @@ func _connect_signals() -> void:
ban_btn.pressed.connect(_on_ban)
unban_btn.pressed.connect(_on_unban)
delete_btn.pressed.connect(_on_delete)
history_btn.pressed.connect(_on_history_pressed)
user_tree.item_edited.connect(_on_user_tree_item_edited)
user_tree.button_clicked.connect(_on_user_tree_button_clicked)
@@ -204,6 +213,10 @@ func _connect_signals() -> void:
delete_mail_server_btn.pressed.connect(_on_delete_mail_server_pressed)
_update_mail_action_btns(null)
# Shop actions
load_banners_btn.pressed.connect(func(): await _load_featured_banners())
save_banners_btn.pressed.connect(func(): await _save_featured_banners())
# =============================================================================
# Core Panel Logic
# =============================================================================
@@ -228,6 +241,8 @@ func _on_tab_changed(tab_index: int) -> void:
await _load_daily_rewards_config()
elif tab_index == 4:
await _load_mail()
elif tab_index == 5:
await _load_featured_banners()
# =============================================================================
# RPC Helper
@@ -262,7 +277,8 @@ func _load_users() -> void:
_set_status("Failed: " + str(res.error), CLR_STATUS_ERR)
return
all_users = res.get("users", [])
var raw_users = res.get("users", [])
all_users = raw_users if typeof(raw_users) == TYPE_ARRAY else []
count_label.text = "%d users" % all_users.size()
for user in all_users:
@@ -464,6 +480,63 @@ func _on_unban() -> void:
_set_status("Unbanned %d/%d" % [ok, to_unban.size()], CLR_STATUS_OK)
await _load_users()
func _on_history_pressed() -> void:
var selected_data = _get_checked_user_data()
if selected_data.size() != 1:
_set_status("Please select exactly ONE user to view history.", CLR_STATUS_ERR)
return
var uid = selected_data[0].get("user_id", "")
_set_status("Fetching history for user...", CLR_STATUS_OK)
var res = await _rpc("admin_get_user_history", {"user_id": uid})
if res.has("error"):
_set_status("Failed to get history: " + str(res.error), CLR_STATUS_ERR)
return
_set_status("History loaded.", CLR_STATUS_OK)
var h = res.get("history", {})
var text = "[b]=== USER HISTORY ===[/b]\n"
text += "User ID: " + uid + "\n\n"
# Logins
text += "[b]-- Recent Logins --[/b]\n"
var logins = h.get("logins", [])
if logins.is_empty():
text += "No recent logins found.\n"
else:
for l in logins:
var time_str = Time.get_datetime_string_from_unix_time(int(l.get("time", 0)))
text += "- %s (IP: %s)\n" % [time_str, l.get("ip", "unknown")]
text += "\n"
# Wallet Ledger
text += "[b]-- Economy / Wallet Ledger --[/b]\n"
var ledger = h.get("wallet_ledger", [])
if ledger.is_empty():
text += "No transactions found.\n"
else:
for item in ledger:
var changeset = str(item.get("changeset", {}))
var c_time = item.get("create_time", "")
text += "- [%s] %s\n" % [c_time.left(19).replace("T", " "), changeset]
text += "\n"
# Matches
text += "[b]-- Matches --[/b]\n"
var matches = h.get("matches", [])
if matches.is_empty():
text += "No match history found.\n"
else:
for m in matches:
text += "- " + str(m) + "\n"
history_text.text = text
history_dialog.popup_centered()
func _on_delete() -> void:
var users := _get_checked_user_data()
if users.is_empty(): return
@@ -498,7 +571,8 @@ func _load_leaderboard() -> void:
_set_status("Failed to load scores", CLR_STATUS_ERR)
return
lb_data = res.get("leaderboard", [])
var raw_lb = res.get("leaderboard", [])
lb_data = raw_lb if typeof(raw_lb) == TYPE_ARRAY else []
count_label.text = "%d records" % lb_data.size()
lb_data.sort_custom(func(a, b): return a.get("high_score", 0) > b.get("high_score", 0))
@@ -824,7 +898,8 @@ func _load_mail() -> void:
_set_status("Failed: " + str(res.error), CLR_STATUS_ERR)
return
_all_server_mails = res.get("mails", [])
var raw_mails = res.get("mails", [])
_all_server_mails = raw_mails if typeof(raw_mails) == TYPE_ARRAY else []
count_label.text = "%d mails" % _all_server_mails.size()
var now_str = Time.get_datetime_string_from_system(true)
@@ -1065,3 +1140,60 @@ func _on_delete_mail_server_pressed() -> void:
await _load_mail()
confirm.queue_free()
)
# =============================================================================
# TAB 6: SHOP — FEATURED BANNERS
# =============================================================================
var _slot_nodes: Array = [] # cached references to the 3 slot HBoxContainers
func _get_slot_nodes() -> Array:
if _slot_nodes.is_empty():
for child in slots_vbox.get_children():
if child is HBoxContainer:
_slot_nodes.append(child)
return _slot_nodes
func _load_featured_banners() -> void:
_set_status("Loading banners...")
var res := await _rpc("admin_get_featured_banners", {})
if res.has("error"):
_set_status("Failed: " + str(res.error), CLR_STATUS_ERR)
return
var raw_banners = res.get("banners", [])
var banners: Array = raw_banners if typeof(raw_banners) == TYPE_ARRAY else []
var slots := _get_slot_nodes()
for i in range(slots.size()):
var slot: HBoxContainer = slots[i]
var id_edit: LineEdit = slot.get_node("ItemIdEdit") as LineEdit
var lbl_edit: LineEdit = slot.get_node("LabelEdit") as LineEdit
if i < banners.size():
var b: Dictionary = banners[i] if banners[i] is Dictionary else {}
id_edit.text = b.get("item_id", "")
lbl_edit.text = b.get("label", "")
else:
id_edit.text = ""
lbl_edit.text = ""
count_label.text = "%d banners configured" % banners.size()
_set_status("Banners loaded", CLR_STATUS_OK)
func _save_featured_banners() -> void:
var banners: Array = []
var slots := _get_slot_nodes()
for slot in slots:
var id_edit: LineEdit = slot.get_node("ItemIdEdit") as LineEdit
var lbl_edit: LineEdit = slot.get_node("LabelEdit") as LineEdit
var item_id: String = id_edit.text.strip_edges()
var label: String = lbl_edit.text.strip_edges()
if not item_id.is_empty():
banners.append({"item_id": item_id, "label": label})
_set_status("Saving banners...")
var res := await _rpc("admin_set_featured_banners", {"banners": banners})
if res.has("error"):
_set_status("Save failed: " + str(res.error), CLR_STATUS_ERR)
elif res.has("success"):
_set_status("Banners saved! (%d slots)" % banners.size(), CLR_STATUS_OK)
+1 -1
View File
@@ -75,7 +75,7 @@ func _on_update_check_completed(has_update: bool, info: Dictionary) -> void:
button_container.visible = true
update_button.visible = true
skip_button.visible = true
skip_button_label.text = "Play without updating"
skip_button_label.text = "Force Play"
else:
status_label.text = "Game up to date."
button_container.visible = true
+31 -11
View File
@@ -5,10 +5,12 @@ extends Control
signal closed
# ─── Node refs ───────────────────────────────────────────────────────────────
@onready var back_btn := %BackBtn as Button
@onready var recipe_list := %RecipeList as VBoxContainer
@onready var status_label := %StatusLabel as Label
@onready var frag_balance := %FragBalance as Label
@onready var back_btn := %BackBtn as Button
@onready var recipe_list := %RecipeList as VBoxContainer
@onready var status_label := %StatusLabel as Label
@onready var common_label := %CommonLabel as Label
@onready var uncommon_label := %UncommonLabel as Label
@onready var rare_label := %RareLabel as Label
const FRAG_ICONS := {
"frag_common": "",
@@ -31,12 +33,9 @@ func _refresh() -> void:
# ─── Fragment balance header ──────────────────────────────────────────────────
func _update_frag_balance() -> void:
var frags: Dictionary = UserProfileManager.fragments
var parts: Array = []
for fid in ["frag_common", "frag_uncommon", "frag_rare"]:
var icon: String = FRAG_ICONS.get(fid, "?")
var count: int = frags.get(fid, 0)
parts.append("%s ×%d" % [icon, count])
frag_balance.text = " ".join(parts)
common_label.text = str(frags.get("frag_common", 0))
uncommon_label.text = str(frags.get("frag_uncommon", 0))
rare_label.text = str(frags.get("frag_rare", 0))
# ─── Recipe cards ─────────────────────────────────────────────────────────────
func _rebuild_recipe_list() -> void:
@@ -55,6 +54,18 @@ func _make_recipe_card(recipe_id: String, recipe: Dictionary) -> PanelContainer:
var panel := PanelContainer.new()
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
# Apply Tekton panel style
var panel_style := StyleBoxFlat.new()
panel_style.bg_color = Color(0.14117648, 0.16862746, 0.19215687, 1)
panel_style.corner_radius_top_left = 12
panel_style.corner_radius_top_right = 12
panel_style.corner_radius_bottom_right = 12
panel_style.corner_radius_bottom_left = 12
panel_style.shadow_color = Color(0, 0, 0, 0.3529412)
panel_style.shadow_size = 4
panel_style.shadow_offset = Vector2(-2, 2)
panel.add_theme_stylebox_override("panel", panel_style)
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 14)
@@ -103,11 +114,20 @@ func _make_recipe_card(recipe_id: String, recipe: Dictionary) -> PanelContainer:
Color(0.4, 1.0, 0.5) if have >= needed else Color(1.0, 0.4, 0.4))
cost_hbox.add_child(cost_lbl)
# Craft button
# Craft button with Tekton dark style
var craft_btn := Button.new()
craft_btn.text = "🔨 Craft"
craft_btn.custom_minimum_size = Vector2(100, 40)
craft_btn.disabled = not can_craft
var btn_style := StyleBoxFlat.new()
btn_style.bg_color = Color(0.15, 0.15, 0.15, 1)
btn_style.corner_radius_top_left = 8
btn_style.corner_radius_top_right = 8
btn_style.corner_radius_bottom_right = 8
btn_style.corner_radius_bottom_left = 8
craft_btn.add_theme_stylebox_override("normal", btn_style)
if not can_craft:
craft_btn.modulate = Color(0.5, 0.5, 0.5, 0.7)
craft_btn.pressed.connect(_on_craft_pressed.bind(recipe_id, panel))
+41 -4
View File
@@ -10,7 +10,8 @@ signal closed
@onready var star_tab_btn := %StarTabBtn as Button
@onready var gold_tab_btn := %GoldTabBtn as Button
@onready var banner_label := %BannerLabel as Label
@onready var balance_label := %BalanceLabel as Label
@onready var gold_label := %GoldLabel as Label
@onready var star_label := %StarLabel as Label
@onready var pity_label := %PityLabel as Label
@onready var pull_1_btn := %Pull1Btn as Button
@onready var pull_10_btn := %Pull10Btn as Button
@@ -88,8 +89,40 @@ func _ensure_dummy_wallet() -> void:
# ─── Banner switching ─────────────────────────────────────────────────────────
func _switch_banner(id: String) -> void:
_current_banner = id
star_tab_btn.modulate = Color(1.3, 1.1, 0.3) if id == "star" else Color.WHITE
gold_tab_btn.modulate = Color(1.3, 1.1, 0.3) if id == "gold" else Color.WHITE
# Create active tab style (dark blue)
var active_style := StyleBoxFlat.new()
active_style.bg_color = Color(0.1, 0.19, 0.27, 1)
active_style.content_margin_left = 16.0
active_style.content_margin_top = 14.0
active_style.content_margin_right = 16.0
active_style.content_margin_bottom = 14.0
active_style.corner_radius_top_left = 8
active_style.corner_radius_top_right = 8
active_style.corner_radius_bottom_right = 8
active_style.corner_radius_bottom_left = 8
# Create inactive tab style (cyan)
var inactive_style := StyleBoxFlat.new()
inactive_style.bg_color = Color(0.33, 0.62, 0.78, 1)
inactive_style.content_margin_left = 16.0
inactive_style.content_margin_top = 14.0
inactive_style.content_margin_right = 16.0
inactive_style.content_margin_bottom = 14.0
inactive_style.corner_radius_top_left = 8
inactive_style.corner_radius_top_right = 8
inactive_style.corner_radius_bottom_right = 8
inactive_style.corner_radius_bottom_left = 8
# Apply styles
star_tab_btn.add_theme_stylebox_override("normal", active_style if id == "star" else inactive_style)
star_tab_btn.add_theme_stylebox_override("hover", active_style if id == "star" else inactive_style)
star_tab_btn.add_theme_stylebox_override("pressed", active_style if id == "star" else inactive_style)
gold_tab_btn.add_theme_stylebox_override("normal", active_style if id == "gold" else inactive_style)
gold_tab_btn.add_theme_stylebox_override("hover", active_style if id == "gold" else inactive_style)
gold_tab_btn.add_theme_stylebox_override("pressed", active_style if id == "gold" else inactive_style)
_refresh_ui()
func _refresh_ui() -> void:
@@ -107,7 +140,11 @@ func _refresh_ui() -> void:
var rates: Dictionary = banner.get("rates", {})
banner_label.text = banner.get("name", "Banner")
balance_label.text = "%s %d" % [icon, bal]
# Update both gold and star labels
star_label.text = str(UserProfileManager.wallet.get("star", 0))
gold_label.text = str(UserProfileManager.wallet.get("gold", 0))
pity_label.text = "Pity: %d / %d" % [pity, pity_at]
cost_1_label.text = "%s %d" % [icon, c1]
cost_10_label.text = "%s %d" % [icon, c10]
+65 -74
View File
@@ -10,17 +10,19 @@ signal closed
# -------------------------------------------------------------------------
@onready var back_btn := %BackBtn as Button
@onready var refresh_btn := %RefreshBtn as Button
@onready var sync_btn := %SyncBtn as Button
@onready var sort_score_btn := %SortScoreBtn as Button
@onready var sort_win_rate_btn := %SortWinRateBtn as Button
@onready var sort_games_btn := %SortGamesBtn as Button
@onready var leaderboard_list := %LeaderboardList as VBoxContainer
@onready var status_label := %StatusLabel as Label
@onready var item_template := %ItemTemplate as PanelContainer
# 3D Preview
@onready var character_root := %CharacterRoot as Node3D
@onready var selected_name_label := %SelectedNameLabel as Label
@onready var selected_rank_label := %SelectedRankLabel as Label
@onready var selected_score_label := %SelectedScoreLabel as Label
@onready var selected_avatar_rect := %SelectedAvatarRect as TextureRect
# -------------------------------------------------------------------------
# State
@@ -43,13 +45,15 @@ const AVATAR_TO_CHAR: Array[String] = ["Pip", "Gatot", "Dabro", "Copper"]
func _ready() -> void:
back_btn.pressed.connect(_on_close_pressed)
refresh_btn.pressed.connect(_fetch_leaderboard_data)
sync_btn.pressed.connect(_on_sync_pressed)
sort_score_btn.pressed.connect(func(): _sort_by("high_score"))
sort_win_rate_btn.pressed.connect(func(): _sort_by("win_rate"))
sort_games_btn.pressed.connect(func(): _sort_by("games_played"))
_update_tab_visuals()
_setup_3d_preview()
if item_template:
item_template.hide()
# Listen to profile and stats changes to keep the panel updated
UserProfileManager.profile_updated.connect(_on_profile_or_stats_changed)
@@ -79,17 +83,6 @@ func _on_close_pressed() -> void:
hide()
emit_signal("closed")
func _on_sync_pressed() -> void:
"""Push the current player's stored stats up to the native Nakama leaderboard."""
if not NakamaManager.session or AuthManager.is_guest:
status_label.text = "Must be logged in to sync"
return
status_label.text = "Syncing your score..."
await UserProfileManager.submit_to_leaderboard()
status_label.text = "Synced! Refreshing..."
await get_tree().create_timer(0.5).timeout
_fetch_leaderboard_data()
# -------------------------------------------------------------------------
# Data
# -------------------------------------------------------------------------
@@ -99,8 +92,10 @@ func _fetch_leaderboard_data() -> void:
return
status_label.text = "Fetching Leaderboard..."
status_label.show()
for child in leaderboard_list.get_children():
child.queue_free()
if child != item_template:
child.queue_free()
# Try native Nakama leaderboard first (fastest, ranked already)
var native_data = await _fetch_native_leaderboard()
@@ -231,90 +226,76 @@ func _update_tab_visuals() -> void:
func _populate_list() -> void:
for child in leaderboard_list.get_children():
child.queue_free()
if child != item_template:
child.queue_free()
if leaderboard_data.size() == 0:
status_label.text = "No players found.\nPlay a match to appear here!"
status_label.show()
return
status_label.hide()
for i in range(leaderboard_data.size()):
var entry = leaderboard_data[i]
_create_leaderboard_item(i + 1, entry, i)
func _create_leaderboard_item(rank: int, entry: Dictionary, index: int) -> void:
var item = PanelContainer.new()
var style = StyleBoxFlat.new()
var item = item_template.duplicate()
item.show()
var style: StyleBoxFlat
if item.has_theme_stylebox_override("panel"):
style = item.get_theme_stylebox("panel").duplicate()
else:
style = StyleBoxFlat.new()
style.bg_color = Color(0.25, 0.3, 0.35, 1.0)
style.set_corner_radius_all(8)
if index == current_selected_index:
# Highlight color for the currently selected player row
style.bg_color = Color(0.25, 0.35, 0.20, 1.0)
elif rank <= 3:
style.bg_color = Color(0.2, 0.2, 0.15, 1.0)
style.bg_color = Color(0.35, 0.45, 0.30, 1.0)
else:
style.bg_color = Color(0.15, 0.15, 0.15, 1.0)
style.bg_color = Color(0.25, 0.3, 0.35, 1.0)
style.set_corner_radius_all(4)
style.content_margin_left = 10
style.content_margin_right = 10
style.content_margin_top = 8
style.content_margin_bottom = 8
item.add_theme_stylebox_override("panel", style)
var hbox = HBoxContainer.new()
hbox.add_theme_constant_override("separation", 16)
item.add_child(hbox)
var rank_label = item.get_node("HBoxContainer/RankLabel") as Label
if rank_label:
var rank_suffix = "th"
if rank % 10 == 1 and rank % 100 != 11: rank_suffix = "st"
elif rank % 10 == 2 and rank % 100 != 12: rank_suffix = "nd"
elif rank % 10 == 3 and rank % 100 != 13: rank_suffix = "rd"
rank_label.text = str(rank) + rank_suffix
# Rank
var rank_label = Label.new()
rank_label.text = "#" + str(rank)
rank_label.custom_minimum_size = Vector2(40, 0)
match rank:
1: rank_label.add_theme_color_override("font_color", Color.GOLD)
2: rank_label.add_theme_color_override("font_color", Color.SILVER)
3: rank_label.add_theme_color_override("font_color", Color.DARK_ORANGE)
_: rank_label.add_theme_color_override("font_color", Color.LIGHT_GRAY)
hbox.add_child(rank_label)
match rank:
1: rank_label.add_theme_color_override("font_color", Color.GOLD)
2: rank_label.add_theme_color_override("font_color", Color.SILVER)
3: rank_label.add_theme_color_override("font_color", Color.DARK_ORANGE)
_: rank_label.add_theme_color_override("font_color", Color.WHITE)
# Avatar
var avatar_rect = TextureRect.new()
avatar_rect.custom_minimum_size = Vector2(32, 32)
avatar_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
var avatar_url = entry.get("avatar_url", "")
if avatar_url.is_empty() or not ResourceLoader.exists(avatar_url):
if not avatar_url.is_empty():
print("[Leaderboard] Avatar URL not found or invalid: ", avatar_url)
avatar_url = UserProfileManager.AVATARS[0]
avatar_rect.texture = load(avatar_url)
hbox.add_child(avatar_rect)
var avatar_rect = item.get_node("HBoxContainer/Margin/InnerHBox/AvatarRect") as TextureRect
if avatar_rect:
var avatar_url = entry.get("avatar_url", "")
if avatar_url.is_empty() or not ResourceLoader.exists(avatar_url):
avatar_url = UserProfileManager.AVATARS[0]
avatar_rect.texture = load(avatar_url)
# Name
var name_label = Label.new()
name_label.text = entry.get("display_name", "Unknown")
name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
name_label.add_theme_color_override("font_color", Color.WHITE)
hbox.add_child(name_label)
var name_label = item.get_node("HBoxContainer/Margin/InnerHBox/NameLabel") as Label
if name_label:
name_label.text = entry.get("display_name", "Unknown")
# Value
var value_label = Label.new()
var color = Color(0.647, 0.996, 0.224, 1)
match current_sort_key:
"high_score": value_label.text = str(entry.get("high_score", 0))
"win_rate": value_label.text = "%.1f%%" % entry.get("win_rate", 0.0)
"games_played": value_label.text = str(entry.get("games_played", 0))
value_label.add_theme_color_override("font_color", color)
value_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
value_label.custom_minimum_size = Vector2(80, 0)
hbox.add_child(value_label)
var value_label = item.get_node("HBoxContainer/ValueLabel") as Label
if value_label:
match current_sort_key:
"high_score": value_label.text = str(entry.get("high_score", 0))
"win_rate": value_label.text = "%.1f%%" % entry.get("win_rate", 0.0)
"games_played": value_label.text = str(entry.get("games_played", 0))
leaderboard_list.add_child(item)
# Make row clickable to update 3D preview
item.gui_input.connect(func(event: InputEvent):
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
current_selected_index = index
_populate_list() # Re-draw list to apply the new active highlight colors visually
_populate_list()
_show_entry_preview(index)
)
item.mouse_filter = Control.MOUSE_FILTER_STOP
@@ -347,8 +328,18 @@ func _show_entry_preview(index: int) -> void:
var display_name: String = entry.get("display_name", "Unknown")
var rank := index + 1
selected_name_label.text = display_name
selected_rank_label.text = "#%d" % rank
if selected_name_label: selected_name_label.text = display_name
if selected_rank_label: selected_rank_label.text = "Rank #%d" % rank
if selected_score_label:
match current_sort_key:
"high_score": selected_score_label.text = str(entry.get("high_score", 0))
"win_rate": selected_score_label.text = "%.1f%%" % entry.get("win_rate", 0.0)
"games_played": selected_score_label.text = str(entry.get("games_played", 0))
if selected_avatar_rect:
var avatar_url2 = entry.get("avatar_url", "")
if avatar_url2.is_empty() or not ResourceLoader.exists(avatar_url2):
avatar_url2 = UserProfileManager.AVATARS[0]
selected_avatar_rect.texture = load(avatar_url2)
func _update_3d_preview(character_name: String) -> void:
if not character_root:
+53 -3
View File
@@ -247,6 +247,30 @@ func _on_category_tab_pressed(category: String) -> void:
_highlight_active_tab()
func _highlight_active_tab() -> void:
# Create active tab style (dark blue)
var active_style := StyleBoxFlat.new()
active_style.bg_color = Color(0.1, 0.19, 0.27, 1)
active_style.content_margin_left = 12.0
active_style.content_margin_top = 8.0
active_style.content_margin_right = 12.0
active_style.content_margin_bottom = 8.0
active_style.corner_radius_top_left = 8
active_style.corner_radius_top_right = 8
active_style.corner_radius_bottom_right = 8
active_style.corner_radius_bottom_left = 8
# Create inactive tab style (cyan)
var inactive_style := StyleBoxFlat.new()
inactive_style.bg_color = Color(0.33, 0.62, 0.78, 1)
inactive_style.content_margin_left = 12.0
inactive_style.content_margin_top = 8.0
inactive_style.content_margin_right = 12.0
inactive_style.content_margin_bottom = 8.0
inactive_style.corner_radius_top_left = 8
inactive_style.corner_radius_top_right = 8
inactive_style.corner_radius_bottom_right = 8
inactive_style.corner_radius_bottom_left = 8
var map := {
"head": head_tab_btn,
"costume": costume_tab_btn,
@@ -255,7 +279,13 @@ func _highlight_active_tab() -> void:
"fragment": frag_tab_btn
}
for cat: String in map:
(map[cat] as Button).modulate = Color(1.3, 1.3, 0.4, 1) if cat == _current_category else Color.WHITE
var btn: Button = map[cat]
var is_active := (cat == _current_category)
var style := active_style if is_active else inactive_style
btn.add_theme_stylebox_override("normal", style)
btn.add_theme_stylebox_override("hover", style)
btn.add_theme_stylebox_override("pressed", style)
btn.add_theme_color_override("font_color", Color.WHITE)
func _rebuild_category_items() -> void:
_category_items.clear()
@@ -306,6 +336,8 @@ func _populate_item_grid() -> void:
prev_page_btn.disabled = (_current_page == 0)
next_page_btn.disabled = ((_current_page + 1) >= total_pages)
var placeholder_tex = preload("res://assets/graphics/gui/inventory/item_placeholder.png")
var equipped: String = UserProfileManager.loadout.get(_current_category, "")
for i in _item_slots.size():
var slot: Button = _item_slots[i]
@@ -313,11 +345,22 @@ func _populate_item_grid() -> void:
if idx < total:
var item_id: String = _category_items[idx]
var info: Dictionary = ITEM_CATALOG.get(item_id, {})
slot.text = info.get("name", item_id)
slot.tooltip_text = item_id
var tex_path = "res://assets/graphics/gui/inventory/%s.png" % item_id
if ResourceLoader.exists(tex_path):
slot.icon = load(tex_path)
else:
slot.icon = placeholder_tex
slot.text = ""
slot.tooltip_text = info.get("name", item_id)
slot.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
slot.expand_icon = true
slot.modulate = Color(0.4, 1.0, 0.4, 1) if item_id == equipped else Color.WHITE
else:
slot.text = ""
slot.icon = null
slot.tooltip_text = ""
slot.modulate = Color.WHITE
@@ -331,6 +374,12 @@ func _on_slot_pressed(slot_index: int) -> void:
func _show_item_info(item_id: String) -> void:
var info: Dictionary = ITEM_CATALOG.get(item_id, {})
var tex_path = "res://assets/graphics/gui/inventory/%s.png" % item_id
if ResourceLoader.exists(tex_path):
item_preview.texture = load(tex_path)
else:
item_preview.texture = preload("res://assets/graphics/gui/inventory/item_placeholder.png")
item_name_label.text = info.get("name", item_id)
var rarity: String = info.get("rarity", "Common")
@@ -353,6 +402,7 @@ func _show_item_info(item_id: String) -> void:
func _clear_item_info() -> void:
item_name_label.text = "Select an item"
item_preview.texture = null
item_rarity_label.text = ""
item_price_label.text = ""
equip_btn.text = "Equip"
+106 -22
View File
@@ -8,6 +8,9 @@ signal closed
@onready var item_grid: GridContainer = %ItemGrid
@onready var back_btn: Button = %BackBtn
@onready var status_label: Label = %StatusLabel
@onready var banner1: Button = %Banner1
@onready var banner2: Button = %Banner2
@onready var banner3: Button = %Banner3
# Tabs
@onready var tab_head: Button = %TabHead
@@ -17,6 +20,9 @@ signal closed
@onready var tab_gold: Button = %TabGold
@onready var tab_star: Button = %TabStar
# Maps category -> tab button (populated in _ready)
var _tab_map: Dictionary = {}
# 3D Preview
@onready var character_root: Node3D = %CharacterRoot
@onready var anim_player: AnimationPlayer = %AnimationPlayer
@@ -72,6 +78,14 @@ const STAR_PACKS: Array = [
# _ready
# -----------------------------------------------------------------------
func _ready() -> void:
_tab_map = {
"head": tab_head,
"costume": tab_costume,
"glove": tab_glove,
"accessory": tab_acc,
"gold_packs": tab_gold,
"star_packs": tab_star,
}
back_btn.pressed.connect(_on_close)
tab_head.pressed.connect(_on_tab_selected.bind("head"))
tab_costume.pressed.connect(_on_tab_selected.bind("costume"))
@@ -85,6 +99,7 @@ func _ready() -> void:
if UserProfileManager.profile_updated.connect(_refresh_wallet) != OK:
pass
_set_active_tab(current_category)
_setup_3d_preview()
if UserProfileManager.shop_catalog.is_empty():
@@ -175,12 +190,78 @@ func _fetch_and_build() -> void:
func _build_shop() -> void:
_refresh_wallet()
_populate_banners()
_populate_current_tab()
func _on_tab_selected(category: String) -> void:
current_category = category
_set_active_tab(category)
_populate_current_tab()
func _set_active_tab(active_category: String) -> void:
var style_active := StyleBoxFlat.new()
style_active.bg_color = Color(1, 1, 1, 1)
style_active.content_margin_left = 16.0
style_active.content_margin_top = 10.0
style_active.content_margin_right = 16.0
style_active.content_margin_bottom = 10.0
style_active.corner_radius_top_left = 6
style_active.corner_radius_top_right = 6
style_active.corner_radius_bottom_right = 6
style_active.corner_radius_bottom_left = 6
var style_inactive := StyleBoxFlat.new()
style_inactive.bg_color = Color(0.15, 0.18, 0.22, 1)
style_inactive.content_margin_left = 16.0
style_inactive.content_margin_top = 10.0
style_inactive.content_margin_right = 16.0
style_inactive.content_margin_bottom = 10.0
style_inactive.corner_radius_top_left = 6
style_inactive.corner_radius_top_right = 6
style_inactive.corner_radius_bottom_right = 6
style_inactive.corner_radius_bottom_left = 6
var style_hover := StyleBoxFlat.new()
style_hover.bg_color = Color(0.22, 0.26, 0.30, 1)
style_hover.content_margin_left = 16.0
style_hover.content_margin_top = 10.0
style_hover.content_margin_right = 16.0
style_hover.content_margin_bottom = 10.0
style_hover.corner_radius_top_left = 6
style_hover.corner_radius_top_right = 6
style_hover.corner_radius_bottom_right = 6
style_hover.corner_radius_bottom_left = 6
for cat in _tab_map:
var btn: Button = _tab_map[cat]
if cat == active_category:
btn.add_theme_stylebox_override("normal", style_active)
btn.add_theme_stylebox_override("hover", style_active)
btn.add_theme_stylebox_override("pressed", style_active)
btn.add_theme_color_override("font_color", Color(0.08, 0.09, 0.12))
btn.add_theme_color_override("font_hover_color", Color(0.08, 0.09, 0.12))
else:
btn.add_theme_stylebox_override("normal", style_inactive)
btn.add_theme_stylebox_override("hover", style_hover)
btn.add_theme_stylebox_override("pressed", style_inactive)
btn.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7))
btn.add_theme_color_override("font_hover_color", Color(1, 1, 1))
# -----------------------------------------------------------------------
# Banner population — promotional / featured items
# -----------------------------------------------------------------------
func _populate_banners() -> void:
var banners: Array[Button] = [banner1, banner2, banner3]
var promos: Array = UserProfileManager.shop_catalog.get("banners", [])
for i in banners.size():
var btn: Button = banners[i]
if i < promos.size():
btn.text = promos[i].get("label", "")
btn.tooltip_text = promos[i].get("id", "")
btn.show()
else:
btn.hide()
# -----------------------------------------------------------------------
# Grid population — builds cards dynamically from localized templates
# -----------------------------------------------------------------------
@@ -214,8 +295,8 @@ func _make_gold_card(pack: Dictionary) -> Control:
var card: Control = template_gold_card.duplicate()
card.visible = true
var amount_lbl: Label = card.find_child("AmountLabel", true, false) as Label
if amount_lbl: amount_lbl.text = "%d" % pack.amount
var amount_lbl: RichTextLabel = card.find_child("AmountLabel", true, false) as RichTextLabel
if amount_lbl: amount_lbl.text = "[right][img=24x24]res://assets/graphics/gui/lobby/gold.png[/img] %d[/right]" % pack.amount
var bonus_lbl: Label = card.find_child("BonusLabel", true, false) as Label
if bonus_lbl:
@@ -237,11 +318,11 @@ func _make_star_card(pack: Dictionary) -> Control:
var card: Control = template_star_card.duplicate()
card.visible = true
var amount_lbl: Label = card.find_child("AmountLabel", true, false) as Label
if amount_lbl: amount_lbl.text = "%d" % pack.amount
var amount_lbl: RichTextLabel = card.find_child("AmountLabel", true, false) as RichTextLabel
if amount_lbl: amount_lbl.text = "[right][img=24x24]res://assets/graphics/gui/lobby/star.png[/img] %d[/right]" % pack.amount
var cost_lbl: Label = card.find_child("CostLabel", true, false) as Label
if cost_lbl: cost_lbl.text = "Cost: ⭐ %d Gold" % pack.gold_cost
var cost_lbl: RichTextLabel = card.find_child("CostLabel", true, false) as RichTextLabel
if cost_lbl: cost_lbl.text = "[center]Cost: [img=20x20]res://assets/graphics/gui/lobby/gold.png[/img] %d[/center]" % pack.gold_cost
var buy_btn: Button = card.find_child("BuyBtn", true, false) as Button
if buy_btn: buy_btn.pressed.connect(_on_buy_star_pressed.bind(pack))
@@ -270,13 +351,16 @@ func _make_cosmetic_card(item: Dictionary) -> Control:
}.get(rarity, Color(0.50, 0.50, 0.50))
rarity_lbl.add_theme_color_override("font_color", rarity_col)
var price_lbl: Label = card.find_child("PriceLabel", true, false) as Label
var price_lbl: RichTextLabel = card.find_child("PriceLabel", true, false) as RichTextLabel
if price_lbl:
var g: int = int(item.get("gold", 0))
var s: int = int(item.get("star", 0))
if g > 0 and s > 0: price_lbl.text = "%d%d" % [g, s]
elif g > 0: price_lbl.text = "%d" % g
else: price_lbl.text = "%d" % s
if g > 0 and s > 0:
price_lbl.text = "[center][img=20x20]res://assets/graphics/gui/lobby/gold.png[/img] %d [img=20x20]res://assets/graphics/gui/lobby/star.png[/img] %d[/center]" % [g, s]
elif g > 0:
price_lbl.text = "[center][img=20x20]res://assets/graphics/gui/lobby/gold.png[/img] %d[/center]" % g
else:
price_lbl.text = "[center][img=20x20]res://assets/graphics/gui/lobby/star.png[/img] %d[/center]" % s
var try_btn: Button = card.find_child("TryBtn", true, false) as Button
if try_btn: try_btn.pressed.connect(_on_try_pressed.bind(item))
@@ -300,8 +384,8 @@ func _make_cosmetic_card(item: Dictionary) -> Control:
func _refresh_wallet() -> void:
var g: int = UserProfileManager.wallet.get("gold", 0)
var s: int = UserProfileManager.wallet.get("star", 0)
gold_label.text = "%d" % g
star_label.text = "%d" % s
gold_label.text = str(g)
star_label.text = str(s)
status_label.text = ""
# -----------------------------------------------------------------------
@@ -351,23 +435,23 @@ func _on_buy_cosmetic_pressed(item: Dictionary) -> void:
if UserProfileManager.inventory.has(item.id):
status_label.text = "Already owned: " + item.get("name", item.id)
return
var price_gold: int = item.get("gold", 0)
var price_star: int = item.get("star", 0)
if UserProfileManager.wallet.get("gold", 0) < price_gold \
or UserProfileManager.wallet.get("star", 0) < price_star:
status_label.text = "Not enough currency."
return
status_label.text = "Purchasing..."
var success: bool = await UserProfileManager.purchase_item(
item.id, price_gold, price_star, current_category)
status_label.text = ("Purchased: " + item.get("name", item.id)) if success else "Purchase failed."
if success:
var err: String = await UserProfileManager.purchase_item(item.id)
if err == "":
status_label.text = "Purchased: " + item.get("name", item.id)
_refresh_wallet()
# Refresh preview to show newly purchased skin's materials
if _preview_revert.is_valid():
_preview_revert.call()
_preview_revert = Callable()
SkinManager.apply_loadout(character_root, UserProfileManager.loadout)
else:
if "NotEnoughFunds" in err or "funds" in err.to_lower() or "wallet" in err.to_lower():
status_label.text = "Not enough currency."
else:
status_label.text = "Purchase failed."
func _on_close() -> void:
# Clean up any open preview when closing the shop