diff --git a/client/ui/meta/screens/character_creation/character_creation.gd b/client/ui/meta/screens/character_creation/character_creation.gd index 9d7746eaf..35a27807f 100644 --- a/client/ui/meta/screens/character_creation/character_creation.gd +++ b/client/ui/meta/screens/character_creation/character_creation.gd @@ -190,6 +190,7 @@ const MAIN_MENU_SCENE := "res://scenes/main_menu.tscn" const GAME_SCENE := "res://scenes/main.tscn" const MANIFEST_PATH := "res://assets/characters/manifest.json" +const APPEARANCE_SUB_NAMES := ["Body", "Head", "Hair", "Clothing", "Accessories"] const SCREENSHOT_DIR := "user://screenshots/" const CARDINAL_NAMES: Array[String] = ["south", "east", "north", "west"] @@ -261,11 +262,16 @@ var _accessory_dock_container: Control = null var _cached_hair_ids: Array = [] var _cached_head_ids: Array = [] -# --- Per-tab search text --- -var _tab_search: Array[String] = ["", "", "", "", ""] # one per tab index -## Per-tab grid container for search filtering (index = tab index 0–4). -## Null for tabs without a primary grid (Body uses HBoxRows; Clothing/Accessories use slot grids). -var _tab_grids: Array[GridContainer] = [null, null, null, null, null] +# --- Appearance sub-section search text and grid references --- +var _appearance_search: Array[String] = ["", "", "", "", ""] # one per sub-section (Body=0 … Accessories=4) +## Per-sub-section grid container for search filtering (index = sub-section 0–4). +## Null for sub-sections without a primary grid (Body uses HBoxRows; Clothing/Accessories use slot grids). +var _appearance_grids: Array[GridContainer] = [null, null, null, null, null] + +# --- Appearance sub-navigation state (WS5) --- +var _appearance_active_idx: int = 0 +var _appearance_sub_btns: Array[Button] = [] +var _appearance_sub_sections: Array[Control] = [] # --- Asset manifest (loaded once, replaces filesystem scanning) --- var _manifest: Dictionary = {} @@ -352,31 +358,10 @@ func _ready() -> void: _tab_container.add_theme_color_override("font_color", Color(0.784, 0.816, 0.878, 1.0)) tab_panel.add_child(_tab_container) - # Build tabs dynamically — only show tabs that have content in the manifest - var tab_builders: Array[Dictionary] = [] - tab_builders.append({"name": "Body", "build": _build_body_tab, "always": true}) - var has_heads := not _manifest_array("heads").is_empty() - if has_heads: - tab_builders.append({"name": "Head", "build": _build_head_tab, "always": false}) - var has_hair := not _manifest_array("hair").is_empty() - if has_hair: - tab_builders.append({"name": "Hair", "build": _build_hair_tab, "always": false}) - var clothing_data: Variant = _manifest.get("clothing", {}) - var has_clothing: bool = ( - clothing_data is Dictionary and not (clothing_data as Dictionary).is_empty() - ) - if has_clothing: - tab_builders.append({"name": "Clothing", "build": _build_clothing_tab, "always": false}) - var has_accessories := not _manifest_array("accessories").is_empty() - if has_accessories: - tab_builders.append( - {"name": "Accessories", "build": _build_accessories_tab, "always": false} - ) - tab_builders.append({"name": "Debug", "build": _build_debug_tab, "always": true}) - - for tb in tab_builders: + # Fixed 4-tab top-level structure: Bookmark / Appearance / Skills / Debug + for tab_name: String in ["Bookmark", "Appearance", "Skills", "Debug"]: var tab := Control.new() - tab.name = tb["name"] + tab.name = tab_name _tab_container.add_child(tab) _descriptor = CharacterVisualDescriptor.new() @@ -412,9 +397,10 @@ func _ready() -> void: _rotate_left_btn.text = UIStrings.get_text("character_creation.btn_rotate_left") _rotate_right_btn.text = UIStrings.get_text("character_creation.btn_rotate_right") - for i in tab_builders.size(): - var builder: Callable = tab_builders[i]["build"] - builder.call(_tab_container.get_child(i)) + _build_bookmark_tab(_tab_container.get_child(0)) + _build_appearance_tab(_tab_container.get_child(1)) + _build_skills_tab(_tab_container.get_child(2)) + _build_debug_tab(_tab_container.get_child(3)) _build_color_picker_modal() _modal_root.visible = false @@ -498,6 +484,98 @@ func _refresh_preview() -> void: _char_visual.set_facing(CARDINAL_DIRS[_facing_idx]) +# ============================================================================= +# Tabs: Bookmark / Appearance / Skills — top-level structure (WS5) +# ============================================================================= + + +func _build_bookmark_tab(tab: Control) -> void: + var lbl := Label.new() + lbl.text = "Bookmark content lands in Workstream 6." + lbl.add_theme_color_override("font_color", TEXT_DIM) + lbl.add_theme_font_size_override("font_size", 12) + lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + lbl.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE + tab.add_child(lbl) + + +func _build_appearance_tab(tab: Control) -> void: + var margin := MarginContainer.new() + margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + margin.add_theme_constant_override("margin_left", 8) + margin.add_theme_constant_override("margin_right", 8) + margin.add_theme_constant_override("margin_top", 4) + tab.add_child(margin) + + var vbox := VBoxContainer.new() + vbox.add_theme_constant_override("separation", 4) + margin.add_child(vbox) + + # Segmented control — one button per sub-section, using _make_slot_btn style. + var subnav := HBoxContainer.new() + subnav.add_theme_constant_override("separation", 4) + vbox.add_child(subnav) + + _appearance_sub_btns.clear() + for i in APPEARANCE_SUB_NAMES.size(): + var btn := _make_slot_btn(APPEARANCE_SUB_NAMES[i]) + btn.pressed.connect(_on_appearance_sub_selected.bind(i)) + subnav.add_child(btn) + _appearance_sub_btns.append(btn) + + # Sub-section area — stacked Controls, only one visible at a time. + # Build all 5 up front to avoid rebuild cost on switch. + # If initial build is slow on low-end hardware, switch to free-and-rebuild on switch. + var sub_area := Control.new() + sub_area.size_flags_vertical = Control.SIZE_FILL | Control.SIZE_EXPAND + sub_area.size_flags_horizontal = Control.SIZE_FILL + vbox.add_child(sub_area) + + _appearance_sub_sections.clear() + var builders: Array[Callable] = [ + _build_body_tab, + _build_head_tab, + _build_hair_tab, + _build_clothing_tab, + _build_accessories_tab, + ] + for i in builders.size(): + var section := Control.new() + section.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + section.visible = (i == 0) + sub_area.add_child(section) + builders[i].call(section) + _appearance_sub_sections.append(section) + + _update_appearance_sub_btns() + + +func _on_appearance_sub_selected(idx: int) -> void: + _appearance_active_idx = idx + for i in _appearance_sub_sections.size(): + _appearance_sub_sections[i].visible = (i == idx) + _update_appearance_sub_btns() + + +func _update_appearance_sub_btns() -> void: + for i in _appearance_sub_btns.size(): + _set_item_selected(_appearance_sub_btns[i], i == _appearance_active_idx) + + +func _build_skills_tab(tab: Control) -> void: + var lbl := Label.new() + lbl.text = "Skills content lands in Workstream 8." + lbl.add_theme_color_override("font_color", TEXT_DIM) + lbl.add_theme_font_size_override("font_size", 12) + lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + lbl.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE + tab.add_child(lbl) + + # ============================================================================= # Tab: Body (Task #2) # ============================================================================= @@ -641,7 +719,7 @@ func _build_head_tab(tab: Control) -> void: grid.add_theme_constant_override("h_separation", 4) grid.add_theme_constant_override("v_separation", 4) scroll.add_child(grid) - _tab_grids[1] = grid + _appearance_grids[1] = grid _cached_head_ids = _manifest_array("heads") for hid in _cached_head_ids: @@ -917,7 +995,7 @@ func _build_clothing_tab(tab: Control) -> void: _clothing_dock_container.custom_minimum_size = Vector2(0, 64) _clothing_dock_container.size_flags_horizontal = Control.SIZE_FILL vbox.add_child(_clothing_dock_container) - # _tab_grids[3] left null: clothing uses per-slot grids (search filters active slot via _clothing_grids) + # _appearance_grids[3] left null: clothing uses per-slot grids (search filters active slot via _clothing_grids) _rebuild_clothing_color_dock(_clothing_dock_container) _update_clothing_slot_btns() @@ -1121,7 +1199,7 @@ func _build_accessories_tab(tab: Control) -> void: _accessory_dock_container.custom_minimum_size = Vector2(0, 56) _accessory_dock_container.size_flags_horizontal = Control.SIZE_FILL vbox.add_child(_accessory_dock_container) - # _tab_grids[4] left null: accessories uses per-slot grids (search filters active slot via _accessory_grids) + # _appearance_grids[4] left null: accessories uses per-slot grids (search filters active slot via _accessory_grids) _rebuild_accessory_color_dock(_accessory_dock_container) _update_accessory_slot_btns() @@ -1917,14 +1995,14 @@ func _make_search_bar(tab_idx: int) -> LineEdit: search.add_theme_font_size_override("font_size", 12) search.text_changed.connect( func(q: String): - _tab_search[tab_idx] = q - # Tabs 3/4 filter the active slot's grid; others use _tab_grids by index + _appearance_search[tab_idx] = q + # Sub-sections 3/4 filter the active slot's grid; others use _appearance_grids by index if tab_idx == 3 and _clothing_grids.has(_active_clothing_slot): _apply_search_filter(_clothing_grids[_active_clothing_slot], q) elif tab_idx == 4 and _accessory_grids.has(_active_accessory_slot): _apply_search_filter(_accessory_grids[_active_accessory_slot], q) - elif tab_idx < _tab_grids.size() and _tab_grids[tab_idx] != null: - _apply_search_filter(_tab_grids[tab_idx], q) + elif tab_idx < _appearance_grids.size() and _appearance_grids[tab_idx] != null: + _apply_search_filter(_appearance_grids[tab_idx], q) ) return search