feat: update shop
This commit is contained in:
+75
-3
@@ -521,22 +521,94 @@ func _apply_outline_recursive(node: Node):
|
||||
for child in node.get_children():
|
||||
_apply_outline_recursive(child)
|
||||
|
||||
const COSMETIC_MAPPING = {
|
||||
"head_hat1": {
|
||||
"hide": ["helm_default", "helm1", "helm2", "helm3"],
|
||||
"show": ["head_hat1"],
|
||||
"materials": {}
|
||||
},
|
||||
"head_crown": {
|
||||
"hide": ["helm_default", "helm1", "helm2", "helm3"],
|
||||
"show": ["head_crown"],
|
||||
"materials": {}
|
||||
},
|
||||
"costume_red": {
|
||||
"hide": [],
|
||||
"show": [],
|
||||
"materials": {
|
||||
"body": "res://assets/materials/body_red.tres",
|
||||
"arm": "res://assets/materials/body_red.tres"
|
||||
}
|
||||
},
|
||||
"costume_gold": {
|
||||
"hide": [],
|
||||
"show": [],
|
||||
"materials": {
|
||||
"body": "res://assets/materials/body_gold.tres",
|
||||
"arm": "res://assets/materials/body_gold.tres"
|
||||
}
|
||||
},
|
||||
"glove_leather": {
|
||||
"hide": ["hands", "glove_default"],
|
||||
"show": ["glove_leather"],
|
||||
"materials": {}
|
||||
},
|
||||
"acc_glasses": {
|
||||
"hide": [],
|
||||
"show": ["acc_glasses"],
|
||||
"materials": {}
|
||||
}
|
||||
}
|
||||
|
||||
func apply_loadout(character_node: Node3D) -> void:
|
||||
"""Apply equipped cosmetics from UserProfileManager.loadout onto the active character model.
|
||||
Items are expected as child nodes named matching the item ID (e.g. 'head_hat1', 'acc_glasses').
|
||||
All nodes in the cosmetic category groups are hidden first, then the equipped one is shown."""
|
||||
It uses COSMETIC_MAPPING to dynamically swap visibility and materials of internal meshes."""
|
||||
if not has_node("/root/UserProfileManager"):
|
||||
return
|
||||
|
||||
var loadout: Dictionary = UserProfileManager.loadout
|
||||
var all_meshes = _get_all_mesh_instances(character_node)
|
||||
|
||||
for category in ["head", "costume", "glove", "accessory"]:
|
||||
var equipped: String = loadout.get(category, "")
|
||||
|
||||
# Fallback basic logic for direct children
|
||||
for child in character_node.get_children():
|
||||
# Only manage nodes that start with the category prefix
|
||||
if child.name.begins_with(category):
|
||||
child.visible = (child.name == equipped)
|
||||
|
||||
# Advanced mapping logic for deep node part swapping and materials
|
||||
if equipped and COSMETIC_MAPPING.has(equipped):
|
||||
var mapping = COSMETIC_MAPPING[equipped]
|
||||
|
||||
# Visibility overrides
|
||||
var to_hide = mapping.get("hide", [])
|
||||
var to_show = mapping.get("show", [])
|
||||
|
||||
for mesh in all_meshes:
|
||||
if mesh.name in to_hide:
|
||||
mesh.visible = false
|
||||
if mesh.name in to_show:
|
||||
mesh.visible = true
|
||||
|
||||
# Material overrides
|
||||
var mats = mapping.get("materials", {})
|
||||
for mesh_name in mats:
|
||||
var mat_path = mats[mesh_name]
|
||||
for mesh in all_meshes:
|
||||
if mesh.name == mesh_name:
|
||||
if ResourceLoader.exists(mat_path):
|
||||
var new_mat = load(mat_path)
|
||||
mesh.material_override = new_mat
|
||||
|
||||
func _get_all_mesh_instances(node: Node) -> Array:
|
||||
var result = []
|
||||
if node is MeshInstance3D or node.get_class() == "SkinnedMeshInstance3D":
|
||||
result.append(node)
|
||||
for child in node.get_children():
|
||||
result.append_array(_get_all_mesh_instances(child))
|
||||
return result
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func sync_character(character_name: String) -> void:
|
||||
"""Sync character selection across all clients."""
|
||||
|
||||
+419
-57
@@ -1,93 +1,455 @@
|
||||
[gd_scene format=3 uid="uid://c018oue81jm44"]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/ui/shop_panel.gd" id="1"]
|
||||
[ext_resource type="Script" uid="uid://w0ddjofws4ib" path="res://scripts/ui/shop_panel.gd" id="1"]
|
||||
[ext_resource type="Theme" uid="uid://da337sh5qxi0s" path="res://assets/themes/ui_theme.tres" id="1_jr3vq"]
|
||||
[ext_resource type="Texture2D" uid="uid://2d1ks5pmblc7" path="res://assets/graphics/main_menu/bg_back.png" id="3_qjhny"]
|
||||
[ext_resource type="FontFile" uid="uid://xnjx058n4tsw" path="res://assets/fonts/Nougat-ExtraBlack.ttf" id="3_udh10"]
|
||||
[ext_resource type="PackedScene" uid="uid://ejeamn0pyey4" path="res://assets/characters/Bob.glb" id="4_bob"]
|
||||
[ext_resource type="PackedScene" uid="uid://d4cul3w3wem5w" path="res://assets/characters/Gatot.glb" id="4_gatot"]
|
||||
[ext_resource type="PackedScene" uid="uid://1vk0mjnwkngi" path="res://assets/characters/Masbro.glb" id="4_masbro"]
|
||||
[ext_resource type="PackedScene" uid="uid://bmln7v6v5kvxg" path="res://assets/characters/Oldpop.glb" id="4_oldpop"]
|
||||
[ext_resource type="AnimationLibrary" uid="uid://c3pyopnwibckj" path="res://assets/characters/animations/animation-pack.res" id="5_animlib"]
|
||||
|
||||
[node name="ShopPanel" type="Control"]
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bg"]
|
||||
bg_color = Color(0.08, 0.09, 0.12, 1)
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_card"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 12.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 12.0
|
||||
bg_color = Color(0.91, 0.86, 0.61, 1)
|
||||
border_width_left = 4
|
||||
border_width_top = 4
|
||||
border_width_right = 4
|
||||
border_width_bottom = 4
|
||||
border_color = Color(0.72, 0.52, 0.1, 1)
|
||||
corner_radius_top_left = 10
|
||||
corner_radius_top_right = 10
|
||||
corner_radius_bottom_right = 10
|
||||
corner_radius_bottom_left = 10
|
||||
|
||||
[node name="ShopPanel" type="Control" unique_id=1967851868]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme = ExtResource("1_jr3vq")
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="ColorRect" type="ColorRect" parent="."]
|
||||
[node name="Background" type="Panel" parent="." unique_id=2035775033]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
color = Color(0.12, 0.12, 0.12, 0.95)
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_bg")
|
||||
|
||||
[node name="Panel" type="PanelContainer" parent="."]
|
||||
[node name="Background2" type="TextureRect" parent="." unique_id=1682487151]
|
||||
modulate = Color(1, 1, 1, 0.28235295)
|
||||
layout_mode = 1
|
||||
anchors_preset = 8
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -300.0
|
||||
offset_top = -250.0
|
||||
offset_right = 300.0
|
||||
offset_bottom = 250.0
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
texture = ExtResource("3_qjhny")
|
||||
expand_mode = 2
|
||||
|
||||
[node name="MainMargin" type="MarginContainer" parent="." unique_id=1416392345]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/margin_left = 30
|
||||
theme_override_constants/margin_top = 30
|
||||
theme_override_constants/margin_right = 30
|
||||
theme_override_constants/margin_bottom = 30
|
||||
|
||||
[node name="MainVBox" type="VBoxContainer" parent="MainMargin" unique_id=298123419]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 20
|
||||
|
||||
[node name="TopBar" type="HBoxContainer" parent="MainMargin/MainVBox" unique_id=1421665563]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Wallet" type="HBoxContainer" parent="MainMargin/MainVBox/TopBar" unique_id=382704727]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 15
|
||||
|
||||
[node name="StarPanel" type="PanelContainer" parent="MainMargin/MainVBox/TopBar/Wallet" unique_id=1049443997]
|
||||
custom_minimum_size = Vector2(150, 50)
|
||||
layout_mode = 2
|
||||
theme = ExtResource("1_jr3vq")
|
||||
|
||||
[node name="StarLabel" type="Label" parent="MainMargin/MainVBox/TopBar/Wallet/StarPanel" unique_id=593135182]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0, 0, 0, 1)
|
||||
theme_override_fonts/font = ExtResource("3_udh10")
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "✦ 0"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="GoldPanel" type="PanelContainer" parent="MainMargin/MainVBox/TopBar/Wallet" unique_id=132289]
|
||||
custom_minimum_size = Vector2(150, 50)
|
||||
layout_mode = 2
|
||||
theme = ExtResource("1_jr3vq")
|
||||
|
||||
[node name="GoldLabel" type="Label" parent="MainMargin/MainVBox/TopBar/Wallet/GoldPanel" unique_id=1638818024]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0, 0, 0, 1)
|
||||
theme_override_fonts/font = ExtResource("3_udh10")
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "⭐ 0"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Spacer" type="Control" parent="MainMargin/MainVBox/TopBar" unique_id=1578035202]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="TabsHBox" type="HBoxContainer" parent="MainMargin/MainVBox/TopBar" unique_id=805731541]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="TabHead" type="Button" parent="MainMargin/MainVBox/TopBar/TabsHBox" unique_id=212253926]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(60, 50)
|
||||
layout_mode = 2
|
||||
theme_override_fonts/font = ExtResource("3_udh10")
|
||||
text = "HAT"
|
||||
|
||||
[node name="TabCostume" type="Button" parent="MainMargin/MainVBox/TopBar/TabsHBox" unique_id=1953478397]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(60, 50)
|
||||
layout_mode = 2
|
||||
theme_override_fonts/font = ExtResource("3_udh10")
|
||||
text = "CLOTHING"
|
||||
|
||||
[node name="TabGlove" type="Button" parent="MainMargin/MainVBox/TopBar/TabsHBox" unique_id=1900195629]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(60, 50)
|
||||
layout_mode = 2
|
||||
theme_override_fonts/font = ExtResource("3_udh10")
|
||||
text = "GLOVES"
|
||||
|
||||
[node name="TabAccessory" type="Button" parent="MainMargin/MainVBox/TopBar/TabsHBox" unique_id=228390814]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(60, 50)
|
||||
layout_mode = 2
|
||||
theme_override_fonts/font = ExtResource("3_udh10")
|
||||
text = "COSTUME"
|
||||
|
||||
[node name="TabGold" type="Button" parent="MainMargin/MainVBox/TopBar/TabsHBox" unique_id=1246468211]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(60, 50)
|
||||
layout_mode = 2
|
||||
theme_override_fonts/font = ExtResource("3_udh10")
|
||||
text = "GET GOLD"
|
||||
|
||||
[node name="TabStar" type="Button" parent="MainMargin/MainVBox/TopBar/TabsHBox" unique_id=378819506]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(60, 50)
|
||||
layout_mode = 2
|
||||
theme_override_fonts/font = ExtResource("3_udh10")
|
||||
text = "GET STAR"
|
||||
|
||||
[node name="ContentHBox" type="HBoxContainer" parent="MainMargin/MainVBox" unique_id=908636948]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/separation = 30
|
||||
|
||||
[node name="LeftSidebar" type="VBoxContainer" parent="MainMargin/MainVBox/ContentHBox" unique_id=1308912288]
|
||||
custom_minimum_size = Vector2(200, 0)
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 20
|
||||
|
||||
[node name="Banner1" type="Button" parent="MainMargin/MainVBox/ContentHBox/LeftSidebar" unique_id=785866623]
|
||||
custom_minimum_size = Vector2(0, 100)
|
||||
layout_mode = 2
|
||||
theme_override_fonts/font = ExtResource("3_udh10")
|
||||
disabled = true
|
||||
text = "Banner Slot"
|
||||
|
||||
[node name="Banner2" type="Button" parent="MainMargin/MainVBox/ContentHBox/LeftSidebar" unique_id=1108522673]
|
||||
custom_minimum_size = Vector2(0, 100)
|
||||
layout_mode = 2
|
||||
theme_override_fonts/font = ExtResource("3_udh10")
|
||||
disabled = true
|
||||
text = "Banner Slot"
|
||||
|
||||
[node name="Banner3" type="Button" parent="MainMargin/MainVBox/ContentHBox/LeftSidebar" unique_id=1479545458]
|
||||
custom_minimum_size = Vector2(0, 100)
|
||||
layout_mode = 2
|
||||
theme_override_fonts/font = ExtResource("3_udh10")
|
||||
disabled = true
|
||||
text = "Banner Slot"
|
||||
|
||||
[node name="Spacer" type="Control" parent="MainMargin/MainVBox/ContentHBox/LeftSidebar" unique_id=1808260178]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="BackBtn" type="Button" parent="MainMargin/MainVBox/ContentHBox/LeftSidebar" unique_id=1287390655]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(80, 60)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
text = "←"
|
||||
|
||||
[node name="CenterScroll" type="ScrollContainer" parent="MainMargin/MainVBox/ContentHBox" unique_id=953911053]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
horizontal_scroll_mode = 0
|
||||
|
||||
[node name="ItemGrid" type="GridContainer" parent="MainMargin/MainVBox/ContentHBox/CenterScroll" unique_id=1867424471]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/h_separation = 20
|
||||
theme_override_constants/v_separation = 20
|
||||
columns = 2
|
||||
|
||||
[node name="Divider" type="ColorRect" parent="MainMargin/MainVBox/ContentHBox" unique_id=221156668]
|
||||
custom_minimum_size = Vector2(8, 0)
|
||||
layout_direction = 2
|
||||
layout_mode = 2
|
||||
color = Color(0.85, 0.72, 0.35, 1)
|
||||
|
||||
[node name="RightPanel" type="VBoxContainer" parent="MainMargin/MainVBox/ContentHBox" unique_id=472452762]
|
||||
custom_minimum_size = Vector2(350, 0)
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 15
|
||||
|
||||
[node name="SubViewportContainer" type="SubViewportContainer" parent="MainMargin/MainVBox/ContentHBox/RightPanel" unique_id=916953792]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
stretch = true
|
||||
|
||||
[node name="SubViewport" type="SubViewport" parent="MainMargin/MainVBox/ContentHBox/RightPanel/SubViewportContainer" unique_id=1223822678]
|
||||
transparent_bg = true
|
||||
handle_input_locally = false
|
||||
size = Vector2i(350, 450)
|
||||
render_target_update_mode = 4
|
||||
|
||||
[node name="Camera3D" type="Camera3D" parent="MainMargin/MainVBox/ContentHBox/RightPanel/SubViewportContainer/SubViewport" unique_id=24602145]
|
||||
transform = Transform3D(1, 0, 0, 0, 0.9659259, 0.25881898, 0, -0.25881898, 0.9659259, 0, 0.5795684, 1.7936413)
|
||||
fov = 55.0
|
||||
|
||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="MainMargin/MainVBox/ContentHBox/RightPanel/SubViewportContainer/SubViewport" unique_id=72453875]
|
||||
transform = Transform3D(0.866025, -0.433013, 0.25, 0, 0.5, 0.866025, -0.5, -0.75, 0.433013, 0, 0, 0)
|
||||
|
||||
[node name="CharacterRoot" type="Node3D" parent="MainMargin/MainVBox/ContentHBox/RightPanel/SubViewportContainer/SubViewport" unique_id=222752607]
|
||||
unique_name_in_owner = true
|
||||
|
||||
[node name="Bob" parent="MainMargin/MainVBox/ContentHBox/RightPanel/SubViewportContainer/SubViewport/CharacterRoot" unique_id=1096946192 instance=ExtResource("4_bob")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.484, 0)
|
||||
visible = false
|
||||
|
||||
[node name="Gatot" parent="MainMargin/MainVBox/ContentHBox/RightPanel/SubViewportContainer/SubViewport/CharacterRoot" unique_id=49313654 instance=ExtResource("4_gatot")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.485, 0)
|
||||
visible = false
|
||||
|
||||
[node name="Masbro" parent="MainMargin/MainVBox/ContentHBox/RightPanel/SubViewportContainer/SubViewport/CharacterRoot" unique_id=1131149145 instance=ExtResource("4_masbro")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.485, 0)
|
||||
visible = false
|
||||
|
||||
[node name="Oldpop" parent="MainMargin/MainVBox/ContentHBox/RightPanel/SubViewportContainer/SubViewport/CharacterRoot" unique_id=1364644723 instance=ExtResource("4_oldpop")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.485, 0)
|
||||
|
||||
[node name="AnimationPlayer" type="AnimationPlayer" parent="MainMargin/MainVBox/ContentHBox/RightPanel/SubViewportContainer/SubViewport/CharacterRoot" unique_id=245373446]
|
||||
unique_name_in_owner = true
|
||||
root_node = NodePath("../Oldpop")
|
||||
libraries/animation-pack = ExtResource("5_animlib")
|
||||
autoplay = &"animation-pack/idle"
|
||||
|
||||
[node name="CharSelector" type="HBoxContainer" parent="MainMargin/MainVBox/ContentHBox/RightPanel" unique_id=1613160498]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 15
|
||||
alignment = 1
|
||||
|
||||
[node name="PrevBtn" type="Button" parent="MainMargin/MainVBox/ContentHBox/RightPanel/CharSelector" unique_id=754085432]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(40, 40)
|
||||
layout_mode = 2
|
||||
text = "<"
|
||||
|
||||
[node name="CharName" type="Label" parent="MainMargin/MainVBox/ContentHBox/RightPanel/CharSelector" unique_id=1410054360]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(100, 0)
|
||||
layout_mode = 2
|
||||
theme_override_fonts/font = ExtResource("3_udh10")
|
||||
text = "Masbro"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="NextBtn" type="Button" parent="MainMargin/MainVBox/ContentHBox/RightPanel/CharSelector" unique_id=1382801465]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(40, 40)
|
||||
layout_mode = 2
|
||||
text = ">"
|
||||
|
||||
[node name="Label" type="Label" parent="MainMargin/MainVBox/ContentHBox/RightPanel" unique_id=2080778318]
|
||||
layout_mode = 2
|
||||
theme_override_fonts/font = ExtResource("3_udh10")
|
||||
text = "Drag to rotate"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="StatusLabel" type="Label" parent="MainMargin/MainVBox" unique_id=1749562194]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Templates" type="Control" parent="." unique_id=842512015]
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="Panel"]
|
||||
layout_mode = 2
|
||||
[node name="GoldCard" type="PanelContainer" parent="Templates" unique_id=376422212]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(319, 150)
|
||||
layout_mode = 0
|
||||
offset_right = 319.0
|
||||
offset_bottom = 150.0
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_card")
|
||||
|
||||
[node name="Header" type="HBoxContainer" parent="Panel/VBoxContainer"]
|
||||
[node name="HBox" type="HBoxContainer" parent="Templates/GoldCard" unique_id=2018445885]
|
||||
layout_mode = 2
|
||||
alignment = 1
|
||||
theme_override_constants/separation = 14
|
||||
|
||||
[node name="BackBtn" type="Button" parent="Panel/VBoxContainer/Header"]
|
||||
[node name="Icon" type="ColorRect" parent="Templates/GoldCard/HBox" unique_id=1987319256]
|
||||
custom_minimum_size = Vector2(125, 125)
|
||||
layout_mode = 2
|
||||
text = "Back"
|
||||
color = Color(0.8, 0.7, 0.4, 1)
|
||||
|
||||
[node name="Title" type="Label" parent="Panel/VBoxContainer/Header"]
|
||||
[node name="VBox" type="VBoxContainer" parent="Templates/GoldCard/HBox" unique_id=1510531971]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = "SHOP"
|
||||
alignment = 1
|
||||
|
||||
[node name="AmountLabel" type="Label" parent="Templates/GoldCard/HBox/VBox" unique_id=581128094]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.15, 0.1, 0.05, 1)
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "⭐ 100"
|
||||
horizontal_alignment = 2
|
||||
|
||||
[node name="BonusLabel" type="Label" parent="Templates/GoldCard/HBox/VBox" unique_id=31159053]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.9, 0.45, 0.1, 1)
|
||||
text = "+50"
|
||||
horizontal_alignment = 2
|
||||
|
||||
[node name="PriceLabel" type="Label" parent="Templates/GoldCard/HBox/VBox" unique_id=2125761085]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.15, 0.55, 0.2, 1)
|
||||
text = "$ 0.99"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="TabContainer" type="TabContainer" parent="Panel/VBoxContainer"]
|
||||
[node name="BuyBtn" type="Button" parent="Templates/GoldCard/HBox/VBox" unique_id=417610366]
|
||||
layout_mode = 2
|
||||
text = "Buy"
|
||||
|
||||
[node name="StarCard" type="PanelContainer" parent="Templates" unique_id=1034862492]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(319, 150)
|
||||
layout_mode = 0
|
||||
offset_right = 319.0
|
||||
offset_bottom = 150.0
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_card")
|
||||
|
||||
[node name="HBox" type="HBoxContainer" parent="Templates/StarCard" unique_id=1343953464]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 14
|
||||
|
||||
[node name="Icon" type="ColorRect" parent="Templates/StarCard/HBox" unique_id=914113873]
|
||||
custom_minimum_size = Vector2(125, 125)
|
||||
layout_mode = 2
|
||||
color = Color(0.65, 0.5, 0.9, 1)
|
||||
|
||||
[node name="VBox" type="VBoxContainer" parent="Templates/StarCard/HBox" unique_id=1883407781]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
alignment = 1
|
||||
|
||||
[node name="AmountLabel" type="Label" parent="Templates/StarCard/HBox/VBox" unique_id=1276779551]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.15, 0.1, 0.05, 1)
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "✦ 100"
|
||||
horizontal_alignment = 2
|
||||
|
||||
[node name="HSeparator" type="Control" parent="Templates/StarCard/HBox/VBox" unique_id=1627096153]
|
||||
clip_contents = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
current_tab = 0
|
||||
|
||||
[node name="Head" type="MarginContainer" parent="Panel/VBoxContainer/TabContainer"]
|
||||
[node name="CostLabel" type="Label" parent="Templates/StarCard/HBox/VBox" unique_id=1937011106]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="GridContainer" type="GridContainer" parent="Panel/VBoxContainer/TabContainer/Head"]
|
||||
layout_mode = 2
|
||||
columns = 3
|
||||
|
||||
[node name="Costume" type="MarginContainer" parent="Panel/VBoxContainer/TabContainer"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
|
||||
[node name="GridContainer" type="GridContainer" parent="Panel/VBoxContainer/TabContainer/Costume"]
|
||||
layout_mode = 2
|
||||
columns = 3
|
||||
|
||||
[node name="Glove" type="MarginContainer" parent="Panel/VBoxContainer/TabContainer"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
|
||||
[node name="GridContainer" type="GridContainer" parent="Panel/VBoxContainer/TabContainer/Glove"]
|
||||
layout_mode = 2
|
||||
columns = 3
|
||||
|
||||
[node name="Accessory" type="MarginContainer" parent="Panel/VBoxContainer/TabContainer"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
|
||||
[node name="GridContainer" type="GridContainer" parent="Panel/VBoxContainer/TabContainer/Accessory"]
|
||||
layout_mode = 2
|
||||
columns = 3
|
||||
|
||||
[node name="StatusLabel" type="Label" parent="Panel/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "Welcome to the Store!"
|
||||
theme_override_colors/font_color = Color(0.55, 0.35, 0.05, 1)
|
||||
text = "⭐ 500 Gold"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="BuyBtn" type="Button" parent="Templates/StarCard/HBox/VBox" unique_id=1523983544]
|
||||
layout_mode = 2
|
||||
text = "Convert"
|
||||
|
||||
[node name="CosmeticCard" type="PanelContainer" parent="Templates" unique_id=1937229989]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(319, 150)
|
||||
layout_mode = 0
|
||||
offset_right = 319.0
|
||||
offset_bottom = 150.0
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_card")
|
||||
|
||||
[node name="HBox" type="HBoxContainer" parent="Templates/CosmeticCard" unique_id=1527720461]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 14
|
||||
|
||||
[node name="Icon" type="ColorRect" parent="Templates/CosmeticCard/HBox" unique_id=1136185378]
|
||||
custom_minimum_size = Vector2(125, 125)
|
||||
layout_mode = 2
|
||||
color = Color(0.8, 0.7, 0.4, 1)
|
||||
|
||||
[node name="VBox" type="VBoxContainer" parent="Templates/CosmeticCard/HBox" unique_id=1048501562]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
alignment = 1
|
||||
|
||||
[node name="NameLabel" type="Label" parent="Templates/CosmeticCard/HBox/VBox" unique_id=958903842]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.1, 0.08, 0.04, 1)
|
||||
text = "Item Name"
|
||||
|
||||
[node name="RarityLabel" type="Label" parent="Templates/CosmeticCard/HBox/VBox" unique_id=882234879]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.5, 0.5, 0.5, 1)
|
||||
text = "Common"
|
||||
|
||||
[node name="PriceLabel" type="Label" parent="Templates/CosmeticCard/HBox/VBox" unique_id=489381936]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 6
|
||||
theme_override_colors/font_color = Color(0.15, 0.1, 0.05, 1)
|
||||
text = "⭐ 0 ✦ 0"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="BtnRow" type="HBoxContainer" parent="Templates/CosmeticCard/HBox/VBox" unique_id=1035177281]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="TryBtn" type="Button" parent="Templates/CosmeticCard/HBox/VBox/BtnRow" unique_id=273726845]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = "Try"
|
||||
|
||||
[node name="BuyBtn" type="Button" parent="Templates/CosmeticCard/HBox/VBox/BtnRow" unique_id=1486650007]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = "Buy"
|
||||
|
||||
@@ -12,6 +12,7 @@ var stats: Dictionary = {}
|
||||
var wallet: Dictionary = {"gold": 0, "star": 0}
|
||||
var inventory: Array = []
|
||||
var loadout: Dictionary = {"head": "", "costume": "", "glove": "", "accessory": ""}
|
||||
var shop_catalog: Dictionary = {}
|
||||
var is_profile_loaded: bool = false
|
||||
|
||||
# Nakama storage collection names
|
||||
@@ -107,7 +108,7 @@ func load_profile() -> Dictionary:
|
||||
|
||||
is_profile_loaded = true
|
||||
emit_signal("profile_loaded", profile)
|
||||
print("[UserProfileManager] Profile loaded: ", profile.display_name)
|
||||
print("[UserProfileManager] Profile loaded: ", profile.get("display_name", "Unknown"))
|
||||
|
||||
# Auto-sync existing score to native Nakama leaderboard in background
|
||||
if stats.get("high_score", 0) > 0 and NakamaManager.session and not AuthManager.is_guest:
|
||||
@@ -315,6 +316,55 @@ func purchase_item(item_id: String, price_gold: int, price_star: int, category:
|
||||
emit_signal("profile_updated")
|
||||
return true
|
||||
|
||||
func fetch_shop_catalog() -> void:
|
||||
if not NakamaManager.session: return
|
||||
|
||||
var result = await NakamaManager.client.rpc_async(
|
||||
NakamaManager.session,
|
||||
"get_shop_catalog",
|
||||
"{}"
|
||||
)
|
||||
|
||||
if result.is_exception():
|
||||
push_error("[UserProfileManager] Failed to fetch shop catalog: ", result.get_exception().message)
|
||||
return
|
||||
|
||||
var payload: Dictionary = JSON.parse_string(result.payload)
|
||||
if payload and payload.has("catalog"):
|
||||
shop_catalog = payload.catalog
|
||||
emit_signal("profile_updated")
|
||||
|
||||
func buy_currency(package_id: String) -> bool:
|
||||
if not NakamaManager.session: return false
|
||||
|
||||
var payload = JSON.stringify({
|
||||
"package_id": package_id
|
||||
})
|
||||
|
||||
var result = await NakamaManager.client.rpc_async(
|
||||
NakamaManager.session,
|
||||
"buy_currency",
|
||||
payload
|
||||
)
|
||||
|
||||
if result.is_exception():
|
||||
push_error("[UserProfileManager] Failed to buy currency: ", result.get_exception().message)
|
||||
return false
|
||||
|
||||
await _reload_wallet()
|
||||
return true
|
||||
|
||||
func _reload_wallet() -> void:
|
||||
if not NakamaManager.session: return
|
||||
var account = await NakamaManager.client.get_account_async(NakamaManager.session)
|
||||
if account.is_exception(): return
|
||||
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)
|
||||
emit_signal("profile_updated")
|
||||
|
||||
# =============================================================================
|
||||
# Stats Management
|
||||
# =============================================================================
|
||||
|
||||
+317
-67
@@ -2,90 +2,340 @@ 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
|
||||
# --- Node References (% = unique name, path-independent) ---
|
||||
@onready var star_label: Label = %StarLabel
|
||||
@onready var gold_label: Label = %GoldLabel
|
||||
@onready var item_grid: GridContainer = %ItemGrid
|
||||
@onready var back_btn: Button = %BackBtn
|
||||
@onready var status_label: Label = %StatusLabel
|
||||
|
||||
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}
|
||||
]
|
||||
}
|
||||
# Tabs
|
||||
@onready var tab_head: Button = %TabHead
|
||||
@onready var tab_costume: Button = %TabCostume
|
||||
@onready var tab_glove: Button = %TabGlove
|
||||
@onready var tab_acc: Button = %TabAccessory
|
||||
@onready var tab_gold: Button = %TabGold
|
||||
@onready var tab_star: Button = %TabStar
|
||||
|
||||
func _ready():
|
||||
# 3D Preview
|
||||
@onready var character_root: Node3D = %CharacterRoot
|
||||
@onready var anim_player: AnimationPlayer = %AnimationPlayer
|
||||
@onready var prev_btn: Button = %PrevBtn
|
||||
@onready var next_btn: Button = %NextBtn
|
||||
@onready var char_name_label: Label = %CharName
|
||||
|
||||
# --- State ---
|
||||
var current_category: String = "head"
|
||||
var current_char_idx: int = 0
|
||||
var available_chars: Array[String] = ["Bob", "Masbro", "Gatot", "Oldpop"]
|
||||
|
||||
# Drag tracking
|
||||
var _is_dragging: bool = false
|
||||
var _last_mouse_x: float = 0.0
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Gold-pack catalog: { id, label, amount, bonus, usd }
|
||||
# Matches the mockup tiers: 100 / 500+50 / 1000+150 / 2000+400 / 5000+1250 / 10000+3000
|
||||
# -----------------------------------------------------------------------
|
||||
const GOLD_PACKS: Array = [
|
||||
{"id": "gold_100", "label": "100", "amount": 100, "bonus": 0, "usd": 0.99},
|
||||
{"id": "gold_500", "label": "500", "amount": 500, "bonus": 50, "usd": 4.99},
|
||||
{"id": "gold_1000", "label": "1000", "amount": 1000, "bonus": 150, "usd": 9.99},
|
||||
{"id": "gold_2000", "label": "2000", "amount": 2000, "bonus": 400, "usd": 19.99},
|
||||
{"id": "gold_5000", "label": "5000", "amount": 5000, "bonus": 1250, "usd": 49.99},
|
||||
{"id": "gold_10000", "label": "10000", "amount": 10000, "bonus": 3000, "usd": 99.99},
|
||||
]
|
||||
|
||||
# Star-to-gold conversion rates (spent in gold, received in star)
|
||||
const STAR_PACKS: Array = [
|
||||
{"id": "star_100", "label": "100 Star", "amount": 100, "gold_cost": 500},
|
||||
{"id": "star_250", "label": "250 Star", "amount": 250, "gold_cost": 1100},
|
||||
{"id": "star_600", "label": "600 Star", "amount": 600, "gold_cost": 2500},
|
||||
]
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# _ready
|
||||
# -----------------------------------------------------------------------
|
||||
func _ready() -> void:
|
||||
back_btn.pressed.connect(_on_close)
|
||||
tab_head.pressed.connect(_on_tab_selected.bind("head"))
|
||||
tab_costume.pressed.connect(_on_tab_selected.bind("costume"))
|
||||
tab_glove.pressed.connect(_on_tab_selected.bind("glove"))
|
||||
tab_acc.pressed.connect(_on_tab_selected.bind("accessory"))
|
||||
tab_gold.pressed.connect(_on_tab_selected.bind("gold_packs"))
|
||||
tab_star.pressed.connect(_on_tab_selected.bind("star_packs"))
|
||||
prev_btn.pressed.connect(_on_prev_char)
|
||||
next_btn.pressed.connect(_on_next_char)
|
||||
|
||||
if UserProfileManager.profile_updated.connect(_refresh_wallet) != OK:
|
||||
pass
|
||||
|
||||
_setup_3d_preview()
|
||||
|
||||
if UserProfileManager.shop_catalog.is_empty():
|
||||
_fetch_and_build()
|
||||
else:
|
||||
_build_shop()
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Local 3D preview
|
||||
# -----------------------------------------------------------------------
|
||||
func _setup_3d_preview() -> void:
|
||||
# Attempt to match the user's currently saved loadout character
|
||||
var def_char: String = UserProfileManager.profile.get("loadout_character", "Bob")
|
||||
var idx = available_chars.find(def_char)
|
||||
if idx != -1:
|
||||
current_char_idx = idx
|
||||
|
||||
_update_char_name_label()
|
||||
_update_preview_char()
|
||||
|
||||
func _on_prev_char() -> void:
|
||||
current_char_idx = (current_char_idx - 1 + available_chars.size()) % available_chars.size()
|
||||
_update_char_name_label()
|
||||
_update_preview_char()
|
||||
|
||||
func _on_next_char() -> void:
|
||||
current_char_idx = (current_char_idx + 1) % available_chars.size()
|
||||
_update_char_name_label()
|
||||
_update_preview_char()
|
||||
|
||||
func _update_char_name_label() -> void:
|
||||
char_name_label.text = available_chars[current_char_idx]
|
||||
|
||||
func _update_preview_char() -> void:
|
||||
if not character_root: return
|
||||
|
||||
var target_node_name = available_chars[current_char_idx]
|
||||
var active_char_node: Node3D = null
|
||||
|
||||
for child in character_root.get_children():
|
||||
if child is Node3D:
|
||||
child.visible = (child.name == target_node_name)
|
||||
if child.name == target_node_name:
|
||||
active_char_node = child
|
||||
|
||||
if active_char_node and anim_player:
|
||||
anim_player.root_node = active_char_node.get_path()
|
||||
if anim_player.has_animation("animation-pack/idle"):
|
||||
anim_player.play("animation-pack/idle")
|
||||
elif anim_player.get_animation_list().size() > 0:
|
||||
anim_player.play(anim_player.get_animation_list()[0])
|
||||
|
||||
if active_char_node:
|
||||
var p = preload("res://scenes/player.gd").new()
|
||||
p.apply_loadout(active_char_node)
|
||||
p.free()
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Drag-to-rotate
|
||||
# -----------------------------------------------------------------------
|
||||
func _input(event: InputEvent) -> void:
|
||||
if not visible:
|
||||
return
|
||||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
_is_dragging = event.pressed
|
||||
if event.pressed:
|
||||
_last_mouse_x = event.position.x
|
||||
elif event is InputEventMouseMotion and _is_dragging and character_root:
|
||||
var delta: float = event.position.x - _last_mouse_x
|
||||
character_root.rotation_degrees.y += delta * 0.5
|
||||
_last_mouse_x = event.position.x
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Public entry point called by the parent scene
|
||||
# -----------------------------------------------------------------------
|
||||
func show_panel() -> void:
|
||||
show()
|
||||
_setup_3d_preview()
|
||||
if UserProfileManager.shop_catalog.is_empty():
|
||||
_fetch_and_build()
|
||||
else:
|
||||
_refresh_wallet()
|
||||
|
||||
func _fetch_and_build() -> void:
|
||||
status_label.text = "Loading catalog..."
|
||||
await UserProfileManager.fetch_shop_catalog()
|
||||
_build_shop()
|
||||
|
||||
func show_panel():
|
||||
show()
|
||||
_refresh_shop()
|
||||
func _build_shop() -> void:
|
||||
_refresh_wallet()
|
||||
_populate_current_tab()
|
||||
|
||||
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 _on_tab_selected(category: String) -> void:
|
||||
current_category = category
|
||||
_populate_current_tab()
|
||||
|
||||
func _populate_grid(grid, items):
|
||||
for child in grid.get_children():
|
||||
# -----------------------------------------------------------------------
|
||||
# Grid population — builds cards dynamically from localized templates
|
||||
# -----------------------------------------------------------------------
|
||||
func _populate_current_tab() -> void:
|
||||
for child in item_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)
|
||||
var catalog := UserProfileManager.shop_catalog
|
||||
|
||||
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!"
|
||||
match current_category:
|
||||
"gold_packs":
|
||||
for pack in GOLD_PACKS:
|
||||
item_grid.add_child(_make_gold_card(pack))
|
||||
"star_packs":
|
||||
for pack in STAR_PACKS:
|
||||
item_grid.add_child(_make_star_card(pack))
|
||||
_:
|
||||
var items: Array = catalog.get(current_category, [])
|
||||
for item in items:
|
||||
item_grid.add_child(_make_cosmetic_card(item))
|
||||
|
||||
func _on_buy_pressed(item: Dictionary):
|
||||
if UserProfileManager.inventory.has(item.id):
|
||||
status_label.text = "Already owned: " + item.name
|
||||
return
|
||||
# -----------------------------------------------------------------------
|
||||
# Card builders — instantiated from scene templates
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
var price_gold = item.gold
|
||||
var price_star = item.star
|
||||
@onready var template_gold_card: Control = %GoldCard
|
||||
@onready var template_star_card: Control = %StarCard
|
||||
@onready var template_cosmetic_card: Control = %CosmeticCard
|
||||
|
||||
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
|
||||
func _make_gold_card(pack: Dictionary) -> Control:
|
||||
var card: Control = template_gold_card.duplicate()
|
||||
card.visible = true
|
||||
|
||||
status_label.text = "Purchasing " + item.name + "..."
|
||||
var amount_lbl: Label = card.find_child("AmountLabel", true, false) as Label
|
||||
if amount_lbl: amount_lbl.text = "⭐ %d" % pack.amount
|
||||
|
||||
# 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 bonus_lbl: Label = card.find_child("BonusLabel", true, false) as Label
|
||||
if bonus_lbl:
|
||||
if pack.bonus > 0:
|
||||
bonus_lbl.text = "+%d" % pack.bonus
|
||||
bonus_lbl.show()
|
||||
else:
|
||||
bonus_lbl.hide()
|
||||
|
||||
var success = await UserProfileManager.purchase_item(item.id, price_gold, price_star, category)
|
||||
var price_lbl: Label = card.find_child("PriceLabel", true, false) as Label
|
||||
if price_lbl: price_lbl.text = "$ %.2f" % pack.usd
|
||||
|
||||
var buy_btn: Button = card.find_child("BuyBtn", true, false) as Button
|
||||
if buy_btn: buy_btn.pressed.connect(_on_buy_gold_pressed.bind(pack))
|
||||
|
||||
return card
|
||||
|
||||
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 cost_lbl: Label = card.find_child("CostLabel", true, false) as Label
|
||||
if cost_lbl: cost_lbl.text = "Cost: ⭐ %d Gold" % 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))
|
||||
|
||||
return card
|
||||
|
||||
func _make_cosmetic_card(item: Dictionary) -> Control:
|
||||
var card: Control = template_cosmetic_card.duplicate()
|
||||
card.visible = true
|
||||
|
||||
var name_lbl: Label = card.find_child("NameLabel", true, false) as Label
|
||||
if name_lbl: name_lbl.text = item.get("name", item.get("id", "?"))
|
||||
|
||||
var rarity: String = item.get("rarity", "Common")
|
||||
var rarity_lbl: Label = card.find_child("RarityLabel", true, false) as Label
|
||||
if rarity_lbl:
|
||||
rarity_lbl.text = rarity
|
||||
var rarity_col: Color = {
|
||||
"Common": Color(0.50, 0.50, 0.50),
|
||||
"Rare": Color(0.20, 0.50, 0.90),
|
||||
"Epic": Color(0.60, 0.20, 0.80),
|
||||
"Legendary": Color(0.90, 0.60, 0.10),
|
||||
}.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
|
||||
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
|
||||
|
||||
var try_btn: Button = card.find_child("TryBtn", true, false) as Button
|
||||
if try_btn: try_btn.pressed.connect(_on_try_pressed.bind(item))
|
||||
|
||||
var buy_btn: Button = card.find_child("BuyBtn", true, false) as Button
|
||||
if buy_btn: buy_btn.pressed.connect(_on_buy_cosmetic_pressed.bind(item))
|
||||
|
||||
return card
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Wallet refresh
|
||||
# -----------------------------------------------------------------------
|
||||
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
|
||||
status_label.text = ""
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Button callbacks
|
||||
# -----------------------------------------------------------------------
|
||||
func _on_try_pressed(item: Dictionary) -> void:
|
||||
status_label.text = "Previewing: " + item.get("name", item.get("id", "?"))
|
||||
|
||||
# Auto-switch character if the catalog item targets a specific one.
|
||||
if item.has("character"):
|
||||
var char_name: String = item.get("character")
|
||||
var idx: int = available_chars.find(char_name)
|
||||
if idx != -1 and current_char_idx != idx:
|
||||
current_char_idx = idx
|
||||
_update_char_name_label()
|
||||
|
||||
# Inject into loadout temporarily to preview it without saving
|
||||
var prev: String = UserProfileManager.loadout.get(current_category, "")
|
||||
UserProfileManager.loadout[current_category] = item.id
|
||||
|
||||
_update_preview_char()
|
||||
|
||||
# Revert immediately, so jumping to next character drops preview.
|
||||
UserProfileManager.loadout[current_category] = prev
|
||||
|
||||
func _on_buy_gold_pressed(pack: Dictionary) -> void:
|
||||
status_label.text = "Processing purchase..."
|
||||
var success: bool = await UserProfileManager.buy_currency(pack.id)
|
||||
status_label.text = "Purchased %s Gold!" % pack.label if success else "Purchase failed."
|
||||
if success:
|
||||
status_label.text = "Successfully purchased: " + item.name
|
||||
_refresh_shop()
|
||||
else:
|
||||
status_label.text = "Failed to purchase. Backend error."
|
||||
_refresh_wallet()
|
||||
|
||||
func _on_close():
|
||||
func _on_buy_star_pressed(pack: Dictionary) -> void:
|
||||
var cost: int = pack.gold_cost
|
||||
if UserProfileManager.wallet.get("gold", 0) < cost:
|
||||
status_label.text = "Not enough Gold. Need ⭐ %d." % cost
|
||||
return
|
||||
status_label.text = "Converting..."
|
||||
var success: bool = await UserProfileManager.buy_currency(pack.id)
|
||||
status_label.text = "Received ✦ %d Star!" % pack.amount if success else "Conversion failed."
|
||||
if success:
|
||||
_refresh_wallet()
|
||||
|
||||
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:
|
||||
_refresh_wallet()
|
||||
|
||||
func _on_close() -> void:
|
||||
hide()
|
||||
emit_signal("closed")
|
||||
|
||||
@@ -25,6 +25,8 @@ function InitModule(ctx, logger, nk, initializer) {
|
||||
|
||||
// Store RPCs
|
||||
initializer.registerRpc("purchase_item", rpcPurchaseItem);
|
||||
initializer.registerRpc("get_shop_catalog", rpcGetShopCatalog);
|
||||
initializer.registerRpc("buy_currency", rpcBuyCurrency);
|
||||
|
||||
// Leaderboard RPCs
|
||||
initializer.registerRpc("get_leaderboard_stats", rpcGetLeaderboardStats);
|
||||
@@ -384,6 +386,54 @@ function rpcAdminSetUserRole(ctx, logger, nk, payload) {
|
||||
// Store / Economy RPCs
|
||||
// =============================================================================
|
||||
|
||||
function rpcGetShopCatalog(ctx, logger, nk, payload) {
|
||||
if (!ctx.userId) throw new Error("Not authenticated");
|
||||
|
||||
// We could read this from storage, but for now we hardcode it to keep it simple and authoritative
|
||||
var catalog = {
|
||||
"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}
|
||||
]
|
||||
};
|
||||
|
||||
return JSON.stringify({ catalog: catalog });
|
||||
}
|
||||
|
||||
function rpcBuyCurrency(ctx, logger, nk, payload) {
|
||||
if (!ctx.userId) throw new Error("Not authenticated");
|
||||
|
||||
var request = JSON.parse(payload);
|
||||
var packageId = request.package_id;
|
||||
|
||||
var changeset = { "gold": 0, "star": 0 };
|
||||
|
||||
if (packageId === "gold_pack_1") changeset["gold"] = 1000;
|
||||
else if (packageId === "gold_pack_2") changeset["gold"] = 5000;
|
||||
else if (packageId === "star_pack_1") changeset["star"] = 100;
|
||||
else if (packageId === "star_pack_2") changeset["star"] = 500;
|
||||
else throw new Error("Invalid package ID");
|
||||
|
||||
try {
|
||||
nk.walletUpdate(ctx.userId, changeset, {}, true);
|
||||
logger.info("User " + ctx.userId + " bought currency package " + packageId);
|
||||
return JSON.stringify({ success: true, package_id: packageId });
|
||||
} catch (e) {
|
||||
logger.error("Currency purchase failed: " + e.message);
|
||||
throw new Error("Currency purchase failed: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function rpcPurchaseItem(ctx, logger, nk, payload) {
|
||||
if (!ctx.userId) throw new Error("Not authenticated");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user