diff --git a/assets/avatars/catalog.json b/assets/avatars/catalog.json index 0a28f98..d20756d 100644 --- a/assets/avatars/catalog.json +++ b/assets/avatars/catalog.json @@ -31,6 +31,14 @@ "display_name": "Ponytail", "mount_bone": "HeadTop" }, + "hair_waterbuffalo_01": { + "type": "attachment", + "slot": "hair", + "path": "res://assets/avatars/hair_waterbuffalo_01.glb", + "version": "1.0.0", + "display_name": "Water Buffalo Lodge Hat", + "mount_bone": "HeadTop" + }, "face_glasses_01": { "type": "attachment", "slot": "face", @@ -71,6 +79,14 @@ "display_name": "Left Glove", "mount_bone": "LeftHand" }, + "left_hand_wristband_01": { + "type": "attachment", + "slot": "left_hand", + "path": "res://assets/avatars/left_hand_wristband_01.glb", + "version": "1.0.0", + "display_name": "Left Gold Band", + "mount_bone": "LeftHand" + }, "right_hand_glove_01": { "type": "attachment", "slot": "right_hand", @@ -79,6 +95,14 @@ "display_name": "Right Glove", "mount_bone": "RightHand" }, + "right_hand_wristband_01": { + "type": "attachment", + "slot": "right_hand", + "path": "res://assets/avatars/right_hand_wristband_01.glb", + "version": "1.0.0", + "display_name": "Right Gold Band", + "mount_bone": "RightHand" + }, "left_foot_boot_01": { "type": "attachment", "slot": "left_foot", @@ -87,6 +111,14 @@ "display_name": "Left Boot", "mount_bone": "LeftFoot" }, + "left_foot_sneaker_01": { + "type": "attachment", + "slot": "left_foot", + "path": "res://assets/avatars/left_foot_sneaker_01.glb", + "version": "1.0.0", + "display_name": "Left Sneaker", + "mount_bone": "LeftFoot" + }, "right_foot_boot_01": { "type": "attachment", "slot": "right_foot", @@ -95,6 +127,14 @@ "display_name": "Right Boot", "mount_bone": "RightFoot" }, + "right_foot_sneaker_01": { + "type": "attachment", + "slot": "right_foot", + "path": "res://assets/avatars/right_foot_sneaker_01.glb", + "version": "1.0.0", + "display_name": "Right Sneaker", + "mount_bone": "RightFoot" + }, "accessory_belt_01": { "type": "attachment", "slot": "accessory", diff --git a/assets/avatars/hair_waterbuffalo_01.glb b/assets/avatars/hair_waterbuffalo_01.glb new file mode 100644 index 0000000..869370e Binary files /dev/null and b/assets/avatars/hair_waterbuffalo_01.glb differ diff --git a/assets/avatars/hair_waterbuffalo_01.glb.import b/assets/avatars/hair_waterbuffalo_01.glb.import new file mode 100644 index 0000000..34344e0 --- /dev/null +++ b/assets/avatars/hair_waterbuffalo_01.glb.import @@ -0,0 +1,42 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://dbpg1j858253k" +path="res://.godot/imported/hair_waterbuffalo_01.glb-c029a4cc5c7f4da664b2a6ff40a4d5a5.scn" + +[deps] + +source_file="res://assets/avatars/hair_waterbuffalo_01.glb" +dest_files=["res://.godot/imported/hair_waterbuffalo_01.glb-c029a4cc5c7f4da664b2a6ff40a4d5a5.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 diff --git a/assets/avatars/left_foot_sneaker_01.glb b/assets/avatars/left_foot_sneaker_01.glb new file mode 100644 index 0000000..78f91e3 Binary files /dev/null and b/assets/avatars/left_foot_sneaker_01.glb differ diff --git a/assets/avatars/left_foot_sneaker_01.glb.import b/assets/avatars/left_foot_sneaker_01.glb.import new file mode 100644 index 0000000..dc9dd94 --- /dev/null +++ b/assets/avatars/left_foot_sneaker_01.glb.import @@ -0,0 +1,42 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://cdu5ncc6txi2a" +path="res://.godot/imported/left_foot_sneaker_01.glb-58178eceb7610b00fcdd19ec562d32ee.scn" + +[deps] + +source_file="res://assets/avatars/left_foot_sneaker_01.glb" +dest_files=["res://.godot/imported/left_foot_sneaker_01.glb-58178eceb7610b00fcdd19ec562d32ee.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 diff --git a/assets/avatars/left_hand_wristband_01.glb b/assets/avatars/left_hand_wristband_01.glb new file mode 100644 index 0000000..e32f2b1 Binary files /dev/null and b/assets/avatars/left_hand_wristband_01.glb differ diff --git a/assets/avatars/left_hand_wristband_01.glb.import b/assets/avatars/left_hand_wristband_01.glb.import new file mode 100644 index 0000000..35fe761 --- /dev/null +++ b/assets/avatars/left_hand_wristband_01.glb.import @@ -0,0 +1,42 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://bd2pj86s3jgw6" +path="res://.godot/imported/left_hand_wristband_01.glb-bbb8beaefb98cfa41b275a9c621f1f25.scn" + +[deps] + +source_file="res://assets/avatars/left_hand_wristband_01.glb" +dest_files=["res://.godot/imported/left_hand_wristband_01.glb-bbb8beaefb98cfa41b275a9c621f1f25.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 diff --git a/assets/avatars/right_foot_sneaker_01.glb b/assets/avatars/right_foot_sneaker_01.glb new file mode 100644 index 0000000..04dff01 Binary files /dev/null and b/assets/avatars/right_foot_sneaker_01.glb differ diff --git a/assets/avatars/right_foot_sneaker_01.glb.import b/assets/avatars/right_foot_sneaker_01.glb.import new file mode 100644 index 0000000..6b382e0 --- /dev/null +++ b/assets/avatars/right_foot_sneaker_01.glb.import @@ -0,0 +1,42 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://da2jib466th8w" +path="res://.godot/imported/right_foot_sneaker_01.glb-54204fa9ac3cba3033c3591ef48c54c7.scn" + +[deps] + +source_file="res://assets/avatars/right_foot_sneaker_01.glb" +dest_files=["res://.godot/imported/right_foot_sneaker_01.glb-54204fa9ac3cba3033c3591ef48c54c7.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 diff --git a/assets/avatars/right_hand_wristband_01.glb b/assets/avatars/right_hand_wristband_01.glb new file mode 100644 index 0000000..d9fd2f7 Binary files /dev/null and b/assets/avatars/right_hand_wristband_01.glb differ diff --git a/assets/avatars/right_hand_wristband_01.glb.import b/assets/avatars/right_hand_wristband_01.glb.import new file mode 100644 index 0000000..0bd5058 --- /dev/null +++ b/assets/avatars/right_hand_wristband_01.glb.import @@ -0,0 +1,42 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://b64ejiiluvql4" +path="res://.godot/imported/right_hand_wristband_01.glb-65f50dbbe9aaf5f2de1555fec5ca3385.scn" + +[deps] + +source_file="res://assets/avatars/right_hand_wristband_01.glb" +dest_files=["res://.godot/imported/right_hand_wristband_01.glb-65f50dbbe9aaf5f2de1555fec5ca3385.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 diff --git a/scripts/avatar/avatar_customization_ui.gd b/scripts/avatar/avatar_customization_ui.gd index 362d859..432ac76 100644 --- a/scripts/avatar/avatar_customization_ui.gd +++ b/scripts/avatar/avatar_customization_ui.gd @@ -42,7 +42,7 @@ signal loadout_changed(loadout: AvatarLoadout) ## Category tabs definition per Lead Architect blueprint: ## Tab 0: "Body & Head" (slots: base_body, head, hair, face) -## Tab 1: "Clothing" (slots: chest, pants, torso, left_foot, left_hand) +## Tab 1: "Clothing" (slots: chest, pants, torso, left_hand, right_hand, left_foot, right_foot) ## Tab 2: "Accessories" (slots: back, accessory) ## Tab 3: "Skin & Colors" (skin tone presets, shading slider, custom RGB sliders) const CATEGORIES: Array[Dictionary] = [ @@ -52,7 +52,7 @@ const CATEGORIES: Array[Dictionary] = [ }, { "name": "Clothing", - "slots": ["chest", "pants", "torso", "left_foot", "left_hand"] + "slots": ["chest", "pants", "torso", "left_hand", "right_hand", "left_foot", "right_foot"] }, { "name": "Accessories", diff --git a/scripts/game.gd b/scripts/game.gd index 7234e69..76dc0d1 100644 --- a/scripts/game.gd +++ b/scripts/game.gd @@ -45,6 +45,7 @@ var _friends_overlay: Control = null var _report_modal: Control = null var _pause_menu: Control = null var _controls_modal: Control = null +var _controls_pill: Control = null var _hud_layer: CanvasLayer = null var _overlay_layer: CanvasLayer = null @@ -335,6 +336,8 @@ func _setup_in_game_hud() -> void: hint_lbl.add_theme_color_override("font_color", Color(0.85, 0.9, 0.95, 0.85)) controls_pill.add_child(hint_lbl) _hud_layer.add_child(controls_pill) + _controls_pill = controls_pill + _controls_pill.visible = (_build_editor == null or not is_instance_valid(_build_editor)) # If editing in creator test mode, show status badge if Session.editing and Session.world_id != null and int(Session.world_id) > 0: @@ -687,6 +690,8 @@ func _open_build_mode() -> void: _build_editor.closed.connect(_close_build_mode) _build_editor.exit_browser_requested.connect(_exit_editor_to_browser) add_child(_build_editor) + if _controls_pill != null and is_instance_valid(_controls_pill): + _controls_pill.visible = false ## Shared teardown for both close paths (F2 again, or the editor's own @@ -697,6 +702,8 @@ func _close_build_mode() -> void: if _build_editor != null and is_instance_valid(_build_editor): _build_editor.queue_free() _build_editor = null + if _controls_pill != null and is_instance_valid(_controls_pill): + _controls_pill.visible = true for child in get_children(): if child is ChunkSaveService: child.queue_free() @@ -760,6 +767,8 @@ func _open_editor_for_world() -> void: _build_editor.closed.connect(_close_build_mode) _build_editor.exit_browser_requested.connect(_exit_editor_to_browser) add_child(_build_editor) + if _controls_pill != null and is_instance_valid(_controls_pill): + _controls_pill.visible = false ## Close path for the non-joined editor (ESC, or the editor's own Close diff --git a/scripts/main_menu.gd b/scripts/main_menu.gd index ff3008c..b0bc543 100644 --- a/scripts/main_menu.gd +++ b/scripts/main_menu.gd @@ -94,9 +94,11 @@ static func _update_sound_button(btn: Button) -> void: var audio_mgr: Node = main_loop.root.get_node_or_null("AudioManager") if main_loop is SceneTree else null var enabled: bool = audio_mgr.is_sound_enabled() if audio_mgr and audio_mgr.has_method("is_sound_enabled") else true if enabled: - btn.text = "🔊 Sound: ON" + btn.text = "🔊 Sound: ON [M]" + btn.tooltip_text = "Sound is enabled. Click or press [M] to mute." else: - btn.text = "🔇 Sound: OFF" + btn.text = "🔇 Sound: OFF [M]" + btn.tooltip_text = "Sound is muted. Click or press [M] to unmute." func _ready() -> void: @@ -149,7 +151,7 @@ func _ready() -> void: var sound_btn := Button.new() sound_btn.name = "SoundButton" sound_btn.set_anchors_preset(Control.PRESET_TOP_RIGHT) - sound_btn.offset_left = -440 + sound_btn.offset_left = -460 sound_btn.offset_top = 10 sound_btn.offset_right = -310 sound_btn.offset_bottom = 38 @@ -161,16 +163,6 @@ func _ready() -> void: audio_mgr.toggle_sound() _update_sound_button(sound_btn) ) - var audio_singleton = get_node_or_null("/root/AudioManager") - if audio_singleton and audio_singleton.has_signal("sound_toggled"): - var on_toggled = func(_enabled: bool): - if is_instance_valid(sound_btn): - _update_sound_button(sound_btn) - audio_singleton.sound_toggled.connect(on_toggled) - sound_btn.tree_exiting.connect(func(): - if is_instance_valid(audio_singleton) and audio_singleton.sound_toggled.is_connected(on_toggled): - audio_singleton.sound_toggled.disconnect(on_toggled) - ) add_child(sound_btn) # Cache node references. @@ -178,6 +170,50 @@ func _ready() -> void: _login_button = $CenterContainer/VBoxContainer/AuthButtonsRow/LoginButton _register_button = $CenterContainer/VBoxContainer/AuthButtonsRow/RegisterButton _auth_panel = $CenterContainer/VBoxContainer/AuthPanel + + # Prominent sound button in AuthButtonsRow on the logged-out start screen + var auth_sound_btn := Button.new() + auth_sound_btn.name = "AuthSoundButton" + auth_sound_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL + auth_sound_btn.add_theme_font_size_override("font_size", 13) + _update_sound_button(auth_sound_btn) + auth_sound_btn.pressed.connect(func(): + var audio_mgr = get_node_or_null("/root/AudioManager") + if audio_mgr and audio_mgr.has_method("toggle_sound"): + audio_mgr.toggle_sound() + _update_sound_button(auth_sound_btn) + ) + _auth_buttons_row.add_child(auth_sound_btn) + + # Sound toggle also inside AuthPanel so user can toggle sound without closing the auth form + var panel_sound_btn := Button.new() + panel_sound_btn.name = "PanelSoundButton" + panel_sound_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL + panel_sound_btn.add_theme_font_size_override("font_size", 12) + _update_sound_button(panel_sound_btn) + panel_sound_btn.pressed.connect(func(): + var audio_mgr = get_node_or_null("/root/AudioManager") + if audio_mgr and audio_mgr.has_method("toggle_sound"): + audio_mgr.toggle_sound() + _update_sound_button(panel_sound_btn) + ) + var auth_vbox = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox + auth_vbox.add_child(panel_sound_btn) + + var audio_singleton = get_node_or_null("/root/AudioManager") + if audio_singleton and audio_singleton.has_signal("sound_toggled"): + var on_toggled = func(_enabled: bool): + if is_instance_valid(sound_btn): + _update_sound_button(sound_btn) + if is_instance_valid(auth_sound_btn): + _update_sound_button(auth_sound_btn) + if is_instance_valid(panel_sound_btn): + _update_sound_button(panel_sound_btn) + audio_singleton.sound_toggled.connect(on_toggled) + tree_exiting.connect(func(): + if is_instance_valid(audio_singleton) and audio_singleton.sound_toggled.is_connected(on_toggled): + audio_singleton.sound_toggled.disconnect(on_toggled) + ) _auth_mode_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthModeLabel _username_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/UsernameEdit _password_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/PasswordEdit @@ -512,6 +548,15 @@ func _clear_status() -> void: func _unhandled_input(event: InputEvent) -> void: + if event is InputEventKey and event.pressed and not event.echo: + if event.keycode == KEY_M: + var focus_owner := get_viewport().gui_get_focus_owner() + if not (focus_owner is LineEdit or focus_owner is TextEdit): + var audio_mgr = get_node_or_null("/root/AudioManager") + if audio_mgr and audio_mgr.has_method("toggle_sound"): + audio_mgr.toggle_sound() + get_viewport().set_input_as_handled() + return if event.is_action_pressed("ui_cancel"): if has_node("ControlsModal"): get_node("ControlsModal").queue_free() diff --git a/scripts/smoke_test_avatar_studio_ui.gd b/scripts/smoke_test_avatar_studio_ui.gd index e70baa1..8d50f18 100644 --- a/scripts/smoke_test_avatar_studio_ui.gd +++ b/scripts/smoke_test_avatar_studio_ui.gd @@ -84,14 +84,20 @@ func _run() -> void: # Check Category constants _check("Category 0 has slots: base_body, head, hair, face", ui.CATEGORIES[0]["slots"] == ["base_body", "head", "hair", "face"]) - _check("Category 1 has slots: chest, pants, torso, left_foot, left_hand", - ui.CATEGORIES[1]["slots"] == ["chest", "pants", "torso", "left_foot", "left_hand"]) + _check("Category 1 has slots: chest, pants, torso, left_hand, right_hand, left_foot, right_foot", + ui.CATEGORIES[1]["slots"] == ["chest", "pants", "torso", "left_hand", "right_hand", "left_foot", "right_foot"]) _check("Category 2 has slots: back, accessory", ui.CATEGORIES[2]["slots"] == ["back", "accessory"]) # 3. Friendly Item Names Verification var mohawk_name: String = ui.get_friendly_item_name("hair_mohawk_01") _check("hair_mohawk_01 friendly name is 'Mohawk'", mohawk_name == "Mohawk") + var waterbuffalo_name: String = ui.get_friendly_item_name("hair_waterbuffalo_01") + _check("hair_waterbuffalo_01 friendly name is 'Water Buffalo Lodge Hat'", waterbuffalo_name == "Water Buffalo Lodge Hat") + var right_glove_name: String = ui.get_friendly_item_name("right_hand_glove_01") + _check("right_hand_glove_01 friendly name is 'Right Glove'", right_glove_name == "Right Glove") + var right_boot_name: String = ui.get_friendly_item_name("right_foot_boot_01") + _check("right_foot_boot_01 friendly name is 'Right Boot'", right_boot_name == "Right Boot") var cyber_name: String = ui.get_friendly_item_name("head_cyber_01") _check("head_cyber_01 friendly name is 'Cyber Head'", cyber_name == "Cyber Head") var cargo_name: String = ui.get_friendly_item_name("pants_cargo_01") @@ -152,9 +158,19 @@ func _run() -> void: ui._on_category_tab_changed(1) # Clothing await process_frame var slot_list: HBoxContainer = ui.find_child("SlotList", true, false) as HBoxContainer - _check("Switching to Clothing tab populates slot buttons", slot_list != null and slot_list.get_child_count() >= 3) + _check("Switching to Clothing tab populates slot buttons", slot_list != null and slot_list.get_child_count() >= 5) var chest_slot_btn: Button = ui.find_child("Slot_chest", true, false) as Button _check("Slot_chest button exists in Clothing tab", chest_slot_btn != null) + var right_hand_slot_btn: Button = ui.find_child("Slot_right_hand", true, false) as Button + _check("Slot_right_hand button exists in Clothing tab", right_hand_slot_btn != null) + var right_foot_slot_btn: Button = ui.find_child("Slot_right_foot", true, false) as Button + _check("Slot_right_foot button exists in Clothing tab", right_foot_slot_btn != null) + + # Verify selecting right_hand slot populates items + ui._select_slot("right_hand") + await process_frame + var right_glove_btn: Button = ui.find_child("Item_right_hand_glove_01", true, false) as Button + _check("Item_right_hand_glove_01 button exists under right_hand slot", right_glove_btn != null) # 6. Skin & Colors Tab Verification ui._on_category_tab_changed(3) # Skin & Colors diff --git a/scripts/smoke_test_avatar_system.gd b/scripts/smoke_test_avatar_system.gd index 7b7db06..1702025 100644 --- a/scripts/smoke_test_avatar_system.gd +++ b/scripts/smoke_test_avatar_system.gd @@ -52,10 +52,11 @@ func _run() -> void: base_bodies.size() == 0 or not loadout.base_body.is_empty()) var hair_options: Array[String] = system.get_assets_for_slot("hair") + _check("hair options include hair_waterbuffalo_01", hair_options.has("hair_waterbuffalo_01")) if hair_options.size() > 0: - loadout.set_equipment("hair", hair_options[0]) + loadout.set_equipment("hair", "hair_waterbuffalo_01" if hair_options.has("hair_waterbuffalo_01") else hair_options[0]) _check("equipping a real catalog hair item succeeds", - loadout.slots.get("hair") == hair_options[0]) + loadout.slots.get("hair") == ("hair_waterbuffalo_01" if hair_options.has("hair_waterbuffalo_01") else hair_options[0])) # 4. Loadout serializes and survives a round trip. var json_str: String = loadout.to_json() diff --git a/scripts/smoke_test_editor_controls_and_hat.gd b/scripts/smoke_test_editor_controls_and_hat.gd new file mode 100644 index 0000000..1edac92 --- /dev/null +++ b/scripts/smoke_test_editor_controls_and_hat.gd @@ -0,0 +1,147 @@ +extends SceneTree +## Smoke test verifying: +## 1. Character editor has right_hand and right_foot options. +## 2. Water Buffalo Lodge Hat (hair_waterbuffalo_01) exists, resolves, and loads. +## 3. In world editor / build mode, controls_pill is hidden. +## 4. Main menu sound toggle is accessible when logged out and responds to 'M' key. + +var passed := 0 +var failed := 0 + +func _initialize() -> void: + _run() + +func _check(desc: String, cond: bool) -> bool: + if cond: + print(" PASS: ", desc) + passed += 1 + else: + printerr(" FAIL: ", desc) + failed += 1 + return cond + +func _run() -> void: + await process_frame + print("SMOKE: Starting Editor Controls, Water Buffalo Hat & Sound verification...") + + # --- 1. Water Buffalo Hat & Character Editor Slots --- + var AvatarCatalogScript = preload("res://scripts/avatar/avatar_catalog.gd") + var catalog = AvatarCatalogScript.new() + var cat_err = catalog.load_catalog() + _check("Catalog loads successfully", cat_err == OK) + + var hat = catalog.resolve("hair_waterbuffalo_01") + _check("hair_waterbuffalo_01 exists in catalog", not hat.is_empty()) + _check("hair_waterbuffalo_01 slot is 'hair'", hat.get("slot", "") == "hair") + _check("hair_waterbuffalo_01 mount_bone is 'HeadTop'", hat.get("mount_bone", "") == "HeadTop") + _check("hair_waterbuffalo_01 display_name is 'Water Buffalo Lodge Hat'", hat.get("display_name", "") == "Water Buffalo Lodge Hat") + + var AvatarCustomizationUIScript = preload("res://scripts/avatar/avatar_customization_ui.gd") + var ui = AvatarCustomizationUIScript.new() + root.add_child(ui) + await process_frame + await process_frame + + var clothing_slots: Array = ui.CATEGORIES[1]["slots"] + _check("Clothing category contains 'right_hand'", clothing_slots.has("right_hand")) + _check("Clothing category contains 'right_foot'", clothing_slots.has("right_foot")) + + ui._select_slot("right_hand") + await process_frame + var right_hand_btn = ui.find_child("Item_right_hand_glove_01", true, false) + _check("Item_right_hand_glove_01 exists in ItemList", right_hand_btn != null) + + ui._select_slot("right_foot") + await process_frame + var right_foot_btn = ui.find_child("Item_right_foot_boot_01", true, false) + _check("Item_right_foot_boot_01 exists in ItemList", right_foot_btn != null) + + ui._select_slot("hair") + await process_frame + var hat_btn = ui.find_child("Item_hair_waterbuffalo_01", true, false) + _check("Item_hair_waterbuffalo_01 exists in ItemList", hat_btn != null) + + # Equip water buffalo hat + ui._on_equip_pressed("hair_waterbuffalo_01") + await process_frame + _check("Water Buffalo Hat equipped in loadout", ui.loadout.get_equipment("hair") == "hair_waterbuffalo_01") + + ui.queue_free() + await process_frame + + # --- 2. Main Menu Sound Controls & Keyboard 'M' --- + var audio_mgr = root.get_node_or_null("AudioManager") + _check("AudioManager autoload exists", audio_mgr != null) + if audio_mgr: + audio_mgr.set_sound_enabled(true, false) + + var main_menu_scene = load("res://scenes/main_menu.tscn") + var menu = main_menu_scene.instantiate() + root.add_child(menu) + await process_frame + + var sound_btn = menu.find_child("SoundButton", true, false) as Button + _check("SoundButton exists on MainMenu", sound_btn != null) + var auth_sound_btn = menu.find_child("AuthSoundButton", true, false) as Button + _check("AuthSoundButton exists in AuthButtonsRow", auth_sound_btn != null) + var panel_sound_btn = menu.find_child("PanelSoundButton", true, false) as Button + _check("PanelSoundButton exists in AuthPanel", panel_sound_btn != null) + + _check("SoundButton text contains 'Sound: ON'", sound_btn != null and sound_btn.text.contains("Sound: ON")) + _check("AuthSoundButton text contains 'Sound: ON'", auth_sound_btn != null and auth_sound_btn.text.contains("Sound: ON")) + + # Test 'M' key shortcut + var key_ev = InputEventKey.new() + key_ev.pressed = true + key_ev.keycode = KEY_M + menu._unhandled_input(key_ev) + await process_frame + + _check("Pressing 'M' toggles sound OFF", audio_mgr != null and not audio_mgr.is_sound_enabled()) + _check("SoundButton text updated to 'Sound: OFF'", sound_btn != null and sound_btn.text.contains("Sound: OFF")) + _check("AuthSoundButton text updated to 'Sound: OFF'", auth_sound_btn != null and auth_sound_btn.text.contains("Sound: OFF")) + + # Toggle back ON with AuthSoundButton + if auth_sound_btn != null: + auth_sound_btn.emit_signal("pressed") + await process_frame + _check("AuthSoundButton click toggles sound ON", audio_mgr != null and audio_mgr.is_sound_enabled()) + _check("SoundButton text restored to 'Sound: ON'", sound_btn != null and sound_btn.text.contains("Sound: ON")) + + menu.queue_free() + await process_frame + + # --- 3. World Editor Controls Banner Hiding --- + var game_scene = load("res://scenes/game.tscn") + _check("game.tscn loads successfully", game_scene != null) + if game_scene == null: + _finish() + return + var game = game_scene.instantiate() + root.add_child(game) + await process_frame + + var pill = game.find_child("ControlsPill", true, false) as Control + _check("ControlsPill exists in game HUD", pill != null) + _check("ControlsPill starts visible during regular play", pill != null and pill.visible) + + # Open build mode + game._open_build_mode() + await process_frame + _check("ControlsPill is HIDDEN when in build mode", pill != null and not pill.visible) + + # Close build mode + game._close_build_mode() + await process_frame + _check("ControlsPill is RESTORED when exiting build mode", pill != null and pill.visible) + + game.queue_free() + await process_frame + + print("\n=== SMOKE RESULT: %d/%d passed ===" % [passed, passed + failed]) + quit(0 if failed == 0 else 1) + + +func _finish() -> void: + print("\n=== SMOKE RESULT: %d/%d passed ===" % [passed, passed + failed]) + quit(0 if failed == 0 else 1) diff --git a/scripts/tools/gen_placeholder_avatars.gd b/scripts/tools/gen_placeholder_avatars.gd index b1ba89a..746e23c 100644 --- a/scripts/tools/gen_placeholder_avatars.gd +++ b/scripts/tools/gen_placeholder_avatars.gd @@ -96,8 +96,12 @@ const ATTACHMENTS := { "pants_cargo_01": ["hips", Vector3(0, -0.04, 0.0), Color(0.30, 0.35, 0.45)], "left_hand_glove_01": ["blob", Vector3(0, 0.0, 0.0), Color(0.15, 0.15, 0.18)], "right_hand_glove_01": ["blob", Vector3(0, 0.0, 0.0), Color(0.15, 0.15, 0.18)], + "left_hand_wristband_01": ["band", Vector3(0, 0.02, 0.0), Color(0.95, 0.80, 0.15)], + "right_hand_wristband_01": ["band", Vector3(0, 0.02, 0.0), Color(0.95, 0.80, 0.15)], "left_foot_boot_01": ["boot", Vector3(0, -0.02, -0.05), Color(0.12, 0.10, 0.09)], "right_foot_boot_01": ["boot", Vector3(0, -0.02, -0.05), Color(0.12, 0.10, 0.09)], + "left_foot_sneaker_01": ["sneaker", Vector3(0, -0.02, -0.05), Color(0.85, 0.25, 0.25)], + "right_foot_sneaker_01": ["sneaker", Vector3(0, -0.02, -0.05), Color(0.85, 0.25, 0.25)], "accessory_belt_01": ["belt", Vector3(0, 0.74, 0.0), Color(0.20, 0.18, 0.12)], "accessory_necklace_01": ["necklace", Vector3(0, 1.34, -0.10), Color(0.85, 0.75, 0.20)], } @@ -114,6 +118,8 @@ func _initialize() -> void: var spec: Array = ATTACHMENTS[id] _gen_attachment(id, spec[0], spec[1], spec[2]) + _gen_water_buffalo_hat() + print("\n%s" % ("DONE — all placeholder avatar GLBs written" if _fail == 0 else "FAILED (%d errors)" % _fail)) quit(_fail) @@ -282,6 +288,10 @@ func _attachment_mesh(kind: String) -> Mesh: var m := BoxMesh.new(); m.size = Vector3(0.30, 0.24, 0.34); return m "boot": var m := BoxMesh.new(); m.size = Vector3(0.34, 0.16, 0.42); return m + "sneaker": + var m := BoxMesh.new(); m.size = Vector3(0.32, 0.14, 0.40); return m + "band": + var m := TorusMesh.new(); m.inner_radius = 0.10; m.outer_radius = 0.16; return m "belt": var m := TorusMesh.new(); m.inner_radius = 0.22; m.outer_radius = 0.30; return m "necklace": @@ -290,6 +300,109 @@ func _attachment_mesh(kind: String) -> Mesh: return BoxMesh.new() +func _gen_water_buffalo_hat() -> void: + var root := Node3D.new() + root.name = "AttachmentRoot" + + # 1. Hat Cap (royal blue felt lodge hat, sitting on head crown) + var cap := MeshInstance3D.new() + cap.name = "HatCap" + var cap_mesh := CylinderMesh.new() + cap_mesh.top_radius = 0.28 + cap_mesh.bottom_radius = 0.32 + cap_mesh.height = 0.26 + cap.mesh = cap_mesh + cap.position = Vector3(0.0, 0.12, 0.0) + var cap_mat := StandardMaterial3D.new() + cap_mat.albedo_color = Color(0.13, 0.20, 0.58) # Flintstones Water Buffalo blue + cap_mat.roughness = 0.95 + cap.material_override = cap_mat + root.add_child(cap) + + # 2. Fur Brim / Rim at bottom edge of cap + var brim := MeshInstance3D.new() + brim.name = "FurBrim" + var brim_mesh := TorusMesh.new() + brim_mesh.inner_radius = 0.28 + brim_mesh.outer_radius = 0.36 + brim.mesh = brim_mesh + brim.position = Vector3(0.0, 0.01, 0.0) + var brim_mat := StandardMaterial3D.new() + brim_mat.albedo_color = Color(0.24, 0.18, 0.13) # Fur trim + brim_mat.roughness = 1.0 + brim.material_override = brim_mat + root.add_child(brim) + + # 3. Lodge Gold Medallion on front (-Z is forward) + var medal := MeshInstance3D.new() + medal.name = "LodgeBadge" + var medal_mesh := BoxMesh.new() + medal_mesh.size = Vector3(0.12, 0.10, 0.04) + medal.mesh = medal_mesh + medal.position = Vector3(0.0, 0.12, -0.32) + var medal_mat := StandardMaterial3D.new() + medal_mat.albedo_color = Color(0.96, 0.82, 0.18) # Gold lodge crest + medal_mat.metallic = 0.7 + medal_mat.roughness = 0.3 + medal.material_override = medal_mat + root.add_child(medal) + + # Horn material: ivory/bone + var horn_mat := StandardMaterial3D.new() + horn_mat.albedo_color = Color(0.92, 0.89, 0.82) # Bone/ivory + horn_mat.roughness = 0.5 + + var tip_mat := StandardMaterial3D.new() + tip_mat.albedo_color = Color(0.20, 0.17, 0.14) # Dark horn tip + tip_mat.roughness = 0.6 + + # 4. Sweeping Curved Water Buffalo Horns (curving outward and up) + for side in [-1.0, 1.0]: + var side_name := "Right" if side < 0.0 else "Left" + + # Base segment (projecting horizontally outwards from side of hat) + var h_base := MeshInstance3D.new() + h_base.name = "Horn_%s_Base" % side_name + var base_mesh := CylinderMesh.new() + base_mesh.bottom_radius = 0.08 + base_mesh.top_radius = 0.065 + base_mesh.height = 0.20 + h_base.mesh = base_mesh + h_base.position = Vector3(side * 0.36, 0.12, 0.02) + h_base.rotation_degrees = Vector3(10.0, side * -15.0, side * -70.0) + h_base.material_override = horn_mat + root.add_child(h_base) + + # Mid segment (sweeping upward and outward) + var h_mid := MeshInstance3D.new() + h_mid.name = "Horn_%s_Mid" % side_name + var mid_mesh := CylinderMesh.new() + mid_mesh.bottom_radius = 0.065 + mid_mesh.top_radius = 0.045 + mid_mesh.height = 0.22 + h_mid.mesh = mid_mesh + h_mid.position = Vector3(side * 0.52, 0.24, 0.0) + h_mid.rotation_degrees = Vector3(15.0, side * -10.0, side * -25.0) + h_mid.material_override = horn_mat + root.add_child(h_mid) + + # Tip segment (curving upward and inward to sharp tip) + var h_tip := MeshInstance3D.new() + h_tip.name = "Horn_%s_Tip" % side_name + var tip_mesh := CylinderMesh.new() + tip_mesh.bottom_radius = 0.045 + tip_mesh.top_radius = 0.005 + tip_mesh.height = 0.18 + h_tip.mesh = tip_mesh + h_tip.position = Vector3(side * 0.56, 0.40, -0.04) + h_tip.rotation_degrees = Vector3(20.0, 0.0, side * 20.0) + h_tip.material_override = tip_mat + root.add_child(h_tip) + + _write_glb(root, "hair_waterbuffalo_01") + root.free() + + # --------------------------------------------------------------------------- # GLB export # ---------------------------------------------------------------------------