diff --git a/scenes/player.gd b/scenes/player.gd index a47aae4..50ed0dd 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -521,21 +521,93 @@ 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: diff --git a/scenes/ui/shop_panel.tscn b/scenes/ui/shop_panel.tscn index dc02389..4572a20 100644 --- a/scenes/ui/shop_panel.tscn +++ b/scenes/ui/shop_panel.tscn @@ -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" diff --git a/scripts/managers/user_profile_manager.gd b/scripts/managers/user_profile_manager.gd index d8719e1..347bc70 100644 --- a/scripts/managers/user_profile_manager.gd +++ b/scripts/managers/user_profile_manager.gd @@ -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 # ============================================================================= diff --git a/scripts/ui/shop_panel.gd b/scripts/ui/shop_panel.gd index c8d3a27..d9cbfec 100644 --- a/scripts/ui/shop_panel.gd +++ b/scripts/ui/shop_panel.gd @@ -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) -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!" + var catalog := UserProfileManager.shop_catalog -func _on_buy_pressed(item: Dictionary): - if UserProfileManager.inventory.has(item.id): - status_label.text = "Already owned: " + item.name - return + 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)) + +# ----------------------------------------------------------------------- +# Card builders — instantiated from scene templates +# ----------------------------------------------------------------------- + +@onready var template_gold_card: Control = %GoldCard +@onready var template_star_card: Control = %StarCard +@onready var template_cosmetic_card: Control = %CosmeticCard + +func _make_gold_card(pack: Dictionary) -> Control: + var card: Control = template_gold_card.duplicate() + card.visible = true - var price_gold = item.gold - var price_star = item.star - - if UserProfileManager.wallet.get("gold", 0) < price_gold or UserProfileManager.wallet.get("star", 0) < price_star: - status_label.text = "Not enough currency for " + item.name - return + var amount_lbl: Label = card.find_child("AmountLabel", true, false) as Label + if amount_lbl: amount_lbl.text = "⭐ %d" % pack.amount - status_label.text = "Purchasing " + item.name + "..." + 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 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 - # 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 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 - var success = await UserProfileManager.purchase_item(item.id, price_gold, price_star, category) +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") diff --git a/server/nakama/tekton_admin.js b/server/nakama/tekton_admin.js index b18c456..f42edee 100644 --- a/server/nakama/tekton_admin.js +++ b/server/nakama/tekton_admin.js @@ -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");