feat: Add right hand/foot editor options, Flintstones Water Buffalo Lodge hat, hide controls in world editor, and easy main menu sound toggle
checkpoint / checkpoint (push) Successful in 6m19s

This commit is contained in:
2026-09-20 11:40:41 +00:00
parent 034a6901a9
commit 62fa0b0067
18 changed files with 601 additions and 20 deletions
+40
View File
@@ -31,6 +31,14 @@
"display_name": "Ponytail", "display_name": "Ponytail",
"mount_bone": "HeadTop" "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": { "face_glasses_01": {
"type": "attachment", "type": "attachment",
"slot": "face", "slot": "face",
@@ -71,6 +79,14 @@
"display_name": "Left Glove", "display_name": "Left Glove",
"mount_bone": "LeftHand" "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": { "right_hand_glove_01": {
"type": "attachment", "type": "attachment",
"slot": "right_hand", "slot": "right_hand",
@@ -79,6 +95,14 @@
"display_name": "Right Glove", "display_name": "Right Glove",
"mount_bone": "RightHand" "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": { "left_foot_boot_01": {
"type": "attachment", "type": "attachment",
"slot": "left_foot", "slot": "left_foot",
@@ -87,6 +111,14 @@
"display_name": "Left Boot", "display_name": "Left Boot",
"mount_bone": "LeftFoot" "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": { "right_foot_boot_01": {
"type": "attachment", "type": "attachment",
"slot": "right_foot", "slot": "right_foot",
@@ -95,6 +127,14 @@
"display_name": "Right Boot", "display_name": "Right Boot",
"mount_bone": "RightFoot" "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": { "accessory_belt_01": {
"type": "attachment", "type": "attachment",
"slot": "accessory", "slot": "accessory",
Binary file not shown.
@@ -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
Binary file not shown.
@@ -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
Binary file not shown.
@@ -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
Binary file not shown.
@@ -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
Binary file not shown.
@@ -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
+2 -2
View File
@@ -42,7 +42,7 @@ signal loadout_changed(loadout: AvatarLoadout)
## Category tabs definition per Lead Architect blueprint: ## Category tabs definition per Lead Architect blueprint:
## Tab 0: "Body & Head" (slots: base_body, head, hair, face) ## 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 2: "Accessories" (slots: back, accessory)
## Tab 3: "Skin & Colors" (skin tone presets, shading slider, custom RGB sliders) ## Tab 3: "Skin & Colors" (skin tone presets, shading slider, custom RGB sliders)
const CATEGORIES: Array[Dictionary] = [ const CATEGORIES: Array[Dictionary] = [
@@ -52,7 +52,7 @@ const CATEGORIES: Array[Dictionary] = [
}, },
{ {
"name": "Clothing", "name": "Clothing",
"slots": ["chest", "pants", "torso", "left_foot", "left_hand"] "slots": ["chest", "pants", "torso", "left_hand", "right_hand", "left_foot", "right_foot"]
}, },
{ {
"name": "Accessories", "name": "Accessories",
+9
View File
@@ -45,6 +45,7 @@ var _friends_overlay: Control = null
var _report_modal: Control = null var _report_modal: Control = null
var _pause_menu: Control = null var _pause_menu: Control = null
var _controls_modal: Control = null var _controls_modal: Control = null
var _controls_pill: Control = null
var _hud_layer: CanvasLayer = null var _hud_layer: CanvasLayer = null
var _overlay_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)) hint_lbl.add_theme_color_override("font_color", Color(0.85, 0.9, 0.95, 0.85))
controls_pill.add_child(hint_lbl) controls_pill.add_child(hint_lbl)
_hud_layer.add_child(controls_pill) _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 editing in creator test mode, show status badge
if Session.editing and Session.world_id != null and int(Session.world_id) > 0: 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.closed.connect(_close_build_mode)
_build_editor.exit_browser_requested.connect(_exit_editor_to_browser) _build_editor.exit_browser_requested.connect(_exit_editor_to_browser)
add_child(_build_editor) 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 ## 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): if _build_editor != null and is_instance_valid(_build_editor):
_build_editor.queue_free() _build_editor.queue_free()
_build_editor = null _build_editor = null
if _controls_pill != null and is_instance_valid(_controls_pill):
_controls_pill.visible = true
for child in get_children(): for child in get_children():
if child is ChunkSaveService: if child is ChunkSaveService:
child.queue_free() child.queue_free()
@@ -760,6 +767,8 @@ func _open_editor_for_world() -> void:
_build_editor.closed.connect(_close_build_mode) _build_editor.closed.connect(_close_build_mode)
_build_editor.exit_browser_requested.connect(_exit_editor_to_browser) _build_editor.exit_browser_requested.connect(_exit_editor_to_browser)
add_child(_build_editor) 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 ## Close path for the non-joined editor (ESC, or the editor's own Close
+58 -13
View File
@@ -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 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 var enabled: bool = audio_mgr.is_sound_enabled() if audio_mgr and audio_mgr.has_method("is_sound_enabled") else true
if enabled: if enabled:
btn.text = "🔊 Sound: ON" btn.text = "🔊 Sound: ON [M]"
btn.tooltip_text = "Sound is enabled. Click or press [M] to mute."
else: 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: func _ready() -> void:
@@ -149,7 +151,7 @@ func _ready() -> void:
var sound_btn := Button.new() var sound_btn := Button.new()
sound_btn.name = "SoundButton" sound_btn.name = "SoundButton"
sound_btn.set_anchors_preset(Control.PRESET_TOP_RIGHT) 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_top = 10
sound_btn.offset_right = -310 sound_btn.offset_right = -310
sound_btn.offset_bottom = 38 sound_btn.offset_bottom = 38
@@ -161,16 +163,6 @@ func _ready() -> void:
audio_mgr.toggle_sound() audio_mgr.toggle_sound()
_update_sound_button(sound_btn) _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) add_child(sound_btn)
# Cache node references. # Cache node references.
@@ -178,6 +170,50 @@ func _ready() -> void:
_login_button = $CenterContainer/VBoxContainer/AuthButtonsRow/LoginButton _login_button = $CenterContainer/VBoxContainer/AuthButtonsRow/LoginButton
_register_button = $CenterContainer/VBoxContainer/AuthButtonsRow/RegisterButton _register_button = $CenterContainer/VBoxContainer/AuthButtonsRow/RegisterButton
_auth_panel = $CenterContainer/VBoxContainer/AuthPanel _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 _auth_mode_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthModeLabel
_username_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/UsernameEdit _username_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/UsernameEdit
_password_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/PasswordEdit _password_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/PasswordEdit
@@ -512,6 +548,15 @@ func _clear_status() -> void:
func _unhandled_input(event: InputEvent) -> 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 event.is_action_pressed("ui_cancel"):
if has_node("ControlsModal"): if has_node("ControlsModal"):
get_node("ControlsModal").queue_free() get_node("ControlsModal").queue_free()
+19 -3
View File
@@ -84,14 +84,20 @@ func _run() -> void:
# Check Category constants # Check Category constants
_check("Category 0 has slots: base_body, head, hair, face", _check("Category 0 has slots: base_body, head, hair, face",
ui.CATEGORIES[0]["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", _check("Category 1 has slots: chest, pants, torso, left_hand, right_hand, left_foot, right_foot",
ui.CATEGORIES[1]["slots"] == ["chest", "pants", "torso", "left_foot", "left_hand"]) ui.CATEGORIES[1]["slots"] == ["chest", "pants", "torso", "left_hand", "right_hand", "left_foot", "right_foot"])
_check("Category 2 has slots: back, accessory", _check("Category 2 has slots: back, accessory",
ui.CATEGORIES[2]["slots"] == ["back", "accessory"]) ui.CATEGORIES[2]["slots"] == ["back", "accessory"])
# 3. Friendly Item Names Verification # 3. Friendly Item Names Verification
var mohawk_name: String = ui.get_friendly_item_name("hair_mohawk_01") var mohawk_name: String = ui.get_friendly_item_name("hair_mohawk_01")
_check("hair_mohawk_01 friendly name is 'Mohawk'", mohawk_name == "Mohawk") _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") 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") _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") 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 ui._on_category_tab_changed(1) # Clothing
await process_frame await process_frame
var slot_list: HBoxContainer = ui.find_child("SlotList", true, false) as HBoxContainer 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 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) _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 # 6. Skin & Colors Tab Verification
ui._on_category_tab_changed(3) # Skin & Colors ui._on_category_tab_changed(3) # Skin & Colors
+3 -2
View File
@@ -52,10 +52,11 @@ func _run() -> void:
base_bodies.size() == 0 or not loadout.base_body.is_empty()) base_bodies.size() == 0 or not loadout.base_body.is_empty())
var hair_options: Array[String] = system.get_assets_for_slot("hair") 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: 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", _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. # 4. Loadout serializes and survives a round trip.
var json_str: String = loadout.to_json() var json_str: String = loadout.to_json()
@@ -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)
+113
View File
@@ -96,8 +96,12 @@ const ATTACHMENTS := {
"pants_cargo_01": ["hips", Vector3(0, -0.04, 0.0), Color(0.30, 0.35, 0.45)], "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)], "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)], "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)], "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)], "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_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)], "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] var spec: Array = ATTACHMENTS[id]
_gen_attachment(id, spec[0], spec[1], spec[2]) _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 print("\n%s" % ("DONE — all placeholder avatar GLBs written" if _fail == 0
else "FAILED (%d errors)" % _fail)) else "FAILED (%d errors)" % _fail))
quit(_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 var m := BoxMesh.new(); m.size = Vector3(0.30, 0.24, 0.34); return m
"boot": "boot":
var m := BoxMesh.new(); m.size = Vector3(0.34, 0.16, 0.42); return m 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": "belt":
var m := TorusMesh.new(); m.inner_radius = 0.22; m.outer_radius = 0.30; return m var m := TorusMesh.new(); m.inner_radius = 0.22; m.outer_radius = 0.30; return m
"necklace": "necklace":
@@ -290,6 +300,109 @@ func _attachment_mesh(kind: String) -> Mesh:
return BoxMesh.new() 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 # GLB export
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------