fix(ui): address PR #99 review — character creation editor fixes
13 fixes from PR #99 review on #705 (character editor): - #10/#11: Add _get_swatch_color() helper; fix stale closure in _make_color_swatch (reads current color at click time, not construction time); fix _on_modal_ok to use helper instead of wrong get_child(0) traversal - #12: Replace HSV palette with D-165 hardcoded hex values (6×9 rows, muted HSL per spec — reds/greens/blues/purples/browns/grays) - #13: Fix fallback asset IDs to match actual files — hair: bald/ bob/buzzed/long; heads: head_001–004; facial hair: beard/moustache/ mutton_chops (stubble removed — no asset) - #14: Remove invented "slot field" TODO comment; replace with name-prefix convention note - #15: Fix overhead camera pitch 0.0°→-80.0° (0° was horizontal, not top-down as intended per D-158) - #16: Make hair highlight swatch non-interactive (_make_display_swatch); remove _on_hair_highlight_changed and _hair_highlight_auto flag - #17: Remove no-op self-assignment in _on_hair_selected; remove redundant assignment inside _hair_highlight_auto block - #18: Store _clothing_dock_container/_accessory_dock_container as instance vars — eliminates fragile get_child(3).get_child(0) chain - #19: Fix test_r_key_triggers_randomise — replaces assert_bool(true) with a real assertion checking body_type or skin_tone changed - #20: Cache scan results in _cached_hair_ids/_cached_head_ids during tab build; _on_randomise() reads cache (no re-scan per keypress) - #21: Fix character_visual.gd docstring "21 segments" → "18 segments" - #22: Fix _get_accessory_ids_for_slot — apply slot name-prefix filtering instead of returning all accessories for every slot Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ extends Node3D
|
||||
## pre-authored asset files. No runtime deformation — selects and attaches meshes.
|
||||
##
|
||||
## All asset paths derived programmatically from BASE_PATH + descriptor fields:
|
||||
## bodies/{body_type}/seg_{name}.glb (D-160: 21 segments per body type)
|
||||
## bodies/{body_type}/seg_{name}.glb (D-160: 18 segments per body type)
|
||||
## heads/templates/{head_id}.glb (D-161: head as BoneAttachment3D)
|
||||
## hair/{hair_id}.glb (BoneAttachment3D to Head bone)
|
||||
## clothing/{item_id}/{body_type}.glb (D-162: pre-fitted per body type)
|
||||
|
||||
@@ -474,13 +474,24 @@ func test_q_key_rotates_left() -> void:
|
||||
func test_r_key_triggers_randomise() -> void:
|
||||
if _scene == null:
|
||||
return
|
||||
# R key should change SOMETHING in the descriptor (not all defaults)
|
||||
var event := InputEventKey.new()
|
||||
event.keycode = KEY_R
|
||||
event.pressed = true
|
||||
# Just verify it doesn't crash
|
||||
_scene._input(event)
|
||||
assert_bool(true).is_true() # reached here without error
|
||||
# Seed to a known state so change is detectable
|
||||
_scene._descriptor.body_type = CharacterVisualDescriptor.BodyType.AVERAGE_M
|
||||
_scene._descriptor.skin_tone = 0
|
||||
var body_before := _scene._descriptor.body_type
|
||||
var skin_before := _scene._descriptor.skin_tone
|
||||
# Press R up to 10 times — must change at least one observable descriptor field
|
||||
var changed := false
|
||||
for _i in 10:
|
||||
var event := InputEventKey.new()
|
||||
event.keycode = KEY_R
|
||||
event.pressed = true
|
||||
_scene._input(event)
|
||||
if _scene._descriptor.body_type != body_before or _scene._descriptor.skin_tone != skin_before:
|
||||
changed = true
|
||||
break
|
||||
assert_bool(changed).override_failure_message(
|
||||
"R key must trigger randomise and change body_type or skin_tone"
|
||||
).is_true()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
+130
-84
@@ -24,8 +24,9 @@ const ITEM_SELECTED_BG := Color(0.10, 0.13, 0.18, 1.0)
|
||||
const ITEM_SELECTED_BORDER := Color(0.906, 0.773, 0.278, 1.0) # INSERT_COLOR_HOVER — selection border
|
||||
|
||||
# --- Camera presets (D-158) ---
|
||||
# Pitch is degrees below horizontal: 0=top-down, 5=near-horizontal, 30=steeper
|
||||
const CAM_PITCHES: Array[float] = [-5.0, -30.0, 0.0] # frontal, dramatic, overhead
|
||||
# Pitch is degrees below horizontal: 5=near eye-level, 30=steeper, 80=near top-down.
|
||||
# Negative values = tilt downward (camera looks toward ground).
|
||||
const CAM_PITCHES: Array[float] = [-5.0, -30.0, -80.0] # frontal, dramatic, overhead
|
||||
const CAM_PITCH_NAMES := ["Frontal", "Dramatic", "Overhead"]
|
||||
const CAM_DISTANCE: float = 2.4
|
||||
const CAM_TARGET_HEIGHT: float = 0.9
|
||||
@@ -66,8 +67,8 @@ const BODY_TYPE_LABELS: Dictionary = {
|
||||
}
|
||||
|
||||
# --- Hair / facial hair / eyebrows ---
|
||||
const FACIAL_HAIR_OPTIONS := ["", "stubble", "beard", "moustache"]
|
||||
const FACIAL_HAIR_LABELS := ["None", "Stubble", "Beard", "Moustache"]
|
||||
const FACIAL_HAIR_OPTIONS := ["", "beard", "moustache", "mutton_chops"]
|
||||
const FACIAL_HAIR_LABELS := ["None", "Beard", "Moustache", "Mutton Chops"]
|
||||
const EYEBROW_OPTIONS := ["regular", "female", "teen", "thick"]
|
||||
const EYEBROW_LABELS := ["Regular", "Female", "Teen", "Thick"]
|
||||
|
||||
@@ -86,8 +87,21 @@ const ACCESSORY_SLOT_LABELS := [
|
||||
]
|
||||
|
||||
# --- D-165 palette (5 chromatic rows + 1 neutral row, 9 cols each) ---
|
||||
const PALETTE_HUES: Array[float] = [0.0, 150.0, 215.0, 275.0, 28.0]
|
||||
const PALETTE_SAT: Array[float] = [0.35, 0.28, 0.35, 0.28, 0.35]
|
||||
# Hardcoded hex per D-165 spec (HSL L=18→76% per chromatic row, S fixed per row).
|
||||
const PALETTE_COLORS: Array[Array] = [
|
||||
# Row 1: Reds H=0° S=35%
|
||||
["#3d1d1d", "#522727", "#673131", "#7f3d3d", "#974848", "#af5959", "#bc7575", "#c99090", "#d7acac"],
|
||||
# Row 2: Greens H=150° S=28%
|
||||
["#213a2d", "#2c4e3d", "#37614c", "#43785e", "#508f70", "#62a684", "#7cb599", "#96c4ad", "#b0d2c1"],
|
||||
# Row 3: Blues H=215° S=35%
|
||||
["#1d2b3d", "#273952", "#314867", "#3d587f", "#486997", "#597daf", "#7593bc", "#90a8c9", "#acbed7"],
|
||||
# Row 4: Purples H=275° S=28%
|
||||
["#30213a", "#402c4e", "#503761", "#624378", "#75508f", "#8a62a6", "#9d7cb5", "#b196c4", "#c4b0d2"],
|
||||
# Row 5: Browns H=28° S=35%
|
||||
["#3d2c1d", "#523b27", "#674a31", "#7f5c3d", "#976d48", "#af8159", "#bc9675", "#c9ab90", "#d7c0ac"],
|
||||
# Row 6: Neutral grays
|
||||
["#000000", "#1f1f1f", "#3f3f3f", "#5f5f5f", "#7f7f7f", "#9f9f9f", "#bfbfbf", "#dfdfdf", "#ffffff"],
|
||||
]
|
||||
const PALETTE_COLS := 9
|
||||
const RECENT_SLOTS := 9
|
||||
|
||||
@@ -145,7 +159,7 @@ var _accessory_primary_swatches: Dictionary = {} # slot -> Button
|
||||
var _accessory_secondary_swatches: Dictionary = {} # slot -> Button
|
||||
|
||||
# --- Auto-derive flags (true = auto-derive from hair/clothing primary) ---
|
||||
var _hair_highlight_auto: bool = true
|
||||
# Note: hair highlight is always auto-derived (display-only swatch, no override).
|
||||
var _eyebrow_tint_auto: bool = true
|
||||
var _facial_hair_tint_auto: bool = true
|
||||
var _clothing_secondary_auto: Dictionary = {} # slot -> bool
|
||||
@@ -161,6 +175,14 @@ var _modal_swatch_btns: Array[Button] = [] # 54 palette swatches
|
||||
var _recent_colors: Array[Color] = [] # persisted recent custom colors
|
||||
var _modal_recent_btns: Array[Button] = []
|
||||
|
||||
# --- Dock container references (set during tab build; avoid fragile child-index traversal) ---
|
||||
var _clothing_dock_container: Control = null
|
||||
var _accessory_dock_container: Control = null
|
||||
|
||||
# --- Cached asset scan results (populated once during tab build, reused by randomise) ---
|
||||
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).
|
||||
@@ -177,7 +199,7 @@ func _ready() -> void:
|
||||
_descriptor.body_type = CharacterVisualDescriptor.BodyType.AVERAGE_M
|
||||
_descriptor.skin_tone = 2 # light_olive — readable middle-ground default
|
||||
_descriptor.eyebrow_id = "regular"
|
||||
_descriptor.hair_id = "short_a"
|
||||
_descriptor.hair_id = "bob"
|
||||
_descriptor.hair_tint = Color(0.55, 0.35, 0.20) # warm brown default
|
||||
|
||||
_char_visual = CharacterVisual.new()
|
||||
@@ -361,9 +383,9 @@ func _build_head_tab(tab: Control) -> void:
|
||||
scroll.add_child(grid)
|
||||
_tab_grids[1] = grid
|
||||
|
||||
var head_ids := _scan_asset_ids("res://assets/characters/heads/templates/", ".glb",
|
||||
["starter_a", "starter_b", "starter_c", "starter_d"])
|
||||
for hid in head_ids:
|
||||
_cached_head_ids = _scan_asset_ids("res://assets/characters/heads/templates/", ".glb",
|
||||
["head_001", "head_002", "head_003", "head_004"])
|
||||
for hid in _cached_head_ids:
|
||||
var btn := _make_grid_item_btn(hid.replace("_", " ").capitalize(), Vector2(96, 80))
|
||||
btn.pressed.connect(_on_head_selected.bind(hid))
|
||||
grid.add_child(btn)
|
||||
@@ -416,9 +438,9 @@ func _build_hair_tab(tab: Control) -> void:
|
||||
hair_scroll.add_child(hair_grid)
|
||||
_tab_grids[2] = hair_grid
|
||||
|
||||
var hair_ids := _scan_asset_ids("res://assets/characters/hair/", ".glb",
|
||||
["short_a", "short_b", "medium_a", "medium_b", "long_a", "bald"])
|
||||
for hid in hair_ids:
|
||||
_cached_hair_ids = _scan_asset_ids("res://assets/characters/hair/", ".glb",
|
||||
["bald", "bob", "buzzed", "long"])
|
||||
for hid in _cached_hair_ids:
|
||||
var btn := _make_grid_item_btn(hid.replace("_", " ").capitalize(), Vector2(80, 64))
|
||||
btn.pressed.connect(_on_hair_selected.bind(hid))
|
||||
hair_grid.add_child(btn)
|
||||
@@ -460,8 +482,6 @@ func _build_hair_tab(tab: Control) -> void:
|
||||
|
||||
func _on_hair_selected(hair_id: String) -> void:
|
||||
_descriptor.hair_id = hair_id
|
||||
if _hair_highlight_auto:
|
||||
_descriptor.hair_tint = _descriptor.hair_tint # keep primary; highlight derived on refresh
|
||||
_update_hair_btns()
|
||||
_update_hair_color_dock()
|
||||
_refresh_preview()
|
||||
@@ -509,8 +529,9 @@ func _build_hair_color_dock() -> Control:
|
||||
func(c): _on_hair_primary_changed(c))
|
||||
row.add_child(_hair_primary_swatch)
|
||||
|
||||
_hair_highlight_swatch = _make_color_swatch(_derive_hair_highlight(_descriptor.hair_tint),
|
||||
"Highlight ●", func(c): _on_hair_highlight_changed(c))
|
||||
# Highlight is auto-derived from primary — display only, no click handler
|
||||
_hair_highlight_swatch = _make_display_swatch(_derive_hair_highlight(_descriptor.hair_tint),
|
||||
"Highlight ●")
|
||||
row.add_child(_hair_highlight_swatch)
|
||||
|
||||
_eyebrow_tint_swatch = _make_color_swatch(_descriptor.hair_tint, "Brows ●",
|
||||
@@ -528,8 +549,7 @@ func _update_hair_color_dock() -> void:
|
||||
if not _hair_primary_swatch:
|
||||
return
|
||||
_set_swatch_color(_hair_primary_swatch, _descriptor.hair_tint)
|
||||
var highlight := _derive_hair_highlight(_descriptor.hair_tint) if _hair_highlight_auto else _descriptor.hair_tint
|
||||
_set_swatch_color(_hair_highlight_swatch, highlight)
|
||||
_set_swatch_color(_hair_highlight_swatch, _derive_hair_highlight(_descriptor.hair_tint))
|
||||
var eb_tint := _descriptor.hair_tint if _eyebrow_tint_auto else _descriptor.eyebrow_tint
|
||||
_set_swatch_color(_eyebrow_tint_swatch, eb_tint)
|
||||
var fh_tint := _descriptor.hair_tint if _facial_hair_tint_auto else _descriptor.facial_hair_tint
|
||||
@@ -538,8 +558,6 @@ func _update_hair_color_dock() -> void:
|
||||
|
||||
func _on_hair_primary_changed(color: Color) -> void:
|
||||
_descriptor.hair_tint = color
|
||||
if _hair_highlight_auto:
|
||||
_descriptor.hair_tint = color # highlight derived at render time
|
||||
if _eyebrow_tint_auto:
|
||||
_descriptor.eyebrow_tint = color
|
||||
if _facial_hair_tint_auto:
|
||||
@@ -548,14 +566,6 @@ func _on_hair_primary_changed(color: Color) -> void:
|
||||
_refresh_preview()
|
||||
|
||||
|
||||
func _on_hair_highlight_changed(color: Color) -> void:
|
||||
_hair_highlight_auto = false
|
||||
# Hair has a single tint in the descriptor — highlight is auto-derived
|
||||
# When overriding, we store via hair_tint direction (compositor uses one tint)
|
||||
_update_hair_color_dock()
|
||||
_refresh_preview()
|
||||
|
||||
|
||||
func _on_eyebrow_tint_changed(color: Color) -> void:
|
||||
_eyebrow_tint_auto = false
|
||||
_descriptor.eyebrow_tint = color
|
||||
@@ -625,14 +635,14 @@ func _build_clothing_tab(tab: Control) -> void:
|
||||
_clothing_item_btns[slot][item_id] = btn
|
||||
|
||||
# Color dock (per slot, rebuilt on slot change)
|
||||
var dock_container := Control.new()
|
||||
dock_container.name = "ClothingColorDock"
|
||||
dock_container.custom_minimum_size = Vector2(0, 64)
|
||||
dock_container.size_flags_horizontal = Control.SIZE_FILL
|
||||
vbox.add_child(dock_container)
|
||||
_clothing_dock_container = Control.new()
|
||||
_clothing_dock_container.name = "ClothingColorDock"
|
||||
_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)
|
||||
|
||||
_rebuild_clothing_color_dock(dock_container)
|
||||
_rebuild_clothing_color_dock(_clothing_dock_container)
|
||||
_update_clothing_slot_btns()
|
||||
_update_clothing_item_btns()
|
||||
|
||||
@@ -644,9 +654,8 @@ func _on_clothing_slot_selected(slot: String) -> void:
|
||||
_update_clothing_slot_btns()
|
||||
|
||||
# Rebuild color dock for newly selected slot
|
||||
var dock_container: Control = _tab_container.get_child(3).get_child(0).get_node_or_null("ClothingColorDock")
|
||||
if dock_container:
|
||||
_rebuild_clothing_color_dock(dock_container)
|
||||
if _clothing_dock_container:
|
||||
_rebuild_clothing_color_dock(_clothing_dock_container)
|
||||
|
||||
|
||||
func _rebuild_clothing_color_dock(container: Control) -> void:
|
||||
@@ -689,9 +698,8 @@ func _on_clothing_item_selected(slot: String, item_id: String) -> void:
|
||||
|
||||
_update_clothing_item_btns()
|
||||
# Rebuild color dock to show new item's defaults
|
||||
var dock_container: Control = _tab_container.get_child(3).get_child(0).get_node_or_null("ClothingColorDock")
|
||||
if dock_container:
|
||||
_rebuild_clothing_color_dock(dock_container)
|
||||
if _clothing_dock_container:
|
||||
_rebuild_clothing_color_dock(_clothing_dock_container)
|
||||
_refresh_preview()
|
||||
|
||||
|
||||
@@ -799,14 +807,14 @@ func _build_accessories_tab(tab: Control) -> void:
|
||||
grid.add_child(btn)
|
||||
_accessory_item_btns[slot][item_id] = btn
|
||||
|
||||
var dock_container := Control.new()
|
||||
dock_container.name = "AccessoryColorDock"
|
||||
dock_container.custom_minimum_size = Vector2(0, 56)
|
||||
dock_container.size_flags_horizontal = Control.SIZE_FILL
|
||||
vbox.add_child(dock_container)
|
||||
_accessory_dock_container = Control.new()
|
||||
_accessory_dock_container.name = "AccessoryColorDock"
|
||||
_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)
|
||||
|
||||
_rebuild_accessory_color_dock(dock_container)
|
||||
_rebuild_accessory_color_dock(_accessory_dock_container)
|
||||
_update_accessory_slot_btns()
|
||||
_update_accessory_item_btns()
|
||||
|
||||
@@ -816,9 +824,8 @@ func _on_accessory_slot_selected(slot: String) -> void:
|
||||
for s in _accessory_grids:
|
||||
_accessory_grids[s].visible = (s == slot)
|
||||
_update_accessory_slot_btns()
|
||||
var dock_container: Control = _tab_container.get_child(4).get_child(0).get_node_or_null("AccessoryColorDock")
|
||||
if dock_container:
|
||||
_rebuild_accessory_color_dock(dock_container)
|
||||
if _accessory_dock_container:
|
||||
_rebuild_accessory_color_dock(_accessory_dock_container)
|
||||
|
||||
|
||||
func _rebuild_accessory_color_dock(container: Control) -> void:
|
||||
@@ -851,9 +858,8 @@ func _on_accessory_item_selected(slot: String, item_id: String) -> void:
|
||||
_descriptor.accessory_tints[item_id] = [Color.WHITE] # Array[Color]: Primary only to start
|
||||
_accessory_secondary_auto[slot] = true
|
||||
_update_accessory_item_btns()
|
||||
var dock_container: Control = _tab_container.get_child(4).get_child(0).get_node_or_null("AccessoryColorDock")
|
||||
if dock_container:
|
||||
_rebuild_accessory_color_dock(dock_container)
|
||||
if _accessory_dock_container:
|
||||
_rebuild_accessory_color_dock(_accessory_dock_container)
|
||||
_refresh_preview()
|
||||
|
||||
|
||||
@@ -934,26 +940,13 @@ func _build_color_picker_modal() -> void:
|
||||
box.add_child(palette_grid)
|
||||
_modal_swatch_btns.clear()
|
||||
|
||||
# 5 chromatic rows
|
||||
for row_i in PALETTE_HUES.size():
|
||||
var h_deg := PALETTE_HUES[row_i]
|
||||
var s := PALETTE_SAT[row_i]
|
||||
for col_i in PALETTE_COLS:
|
||||
# Vary lightness across the row (0.15 → 0.85)
|
||||
var v := 0.15 + col_i * (0.7 / (PALETTE_COLS - 1))
|
||||
var color := Color.from_hsv(h_deg / 360.0, s, v)
|
||||
var swatch := _make_palette_swatch(color)
|
||||
# 6 rows × 9 cols per D-165 (5 chromatic + 1 neutral gray)
|
||||
for row_hexes: Array in PALETTE_COLORS:
|
||||
for hex: String in row_hexes:
|
||||
var swatch := _make_palette_swatch(Color(hex))
|
||||
palette_grid.add_child(swatch)
|
||||
_modal_swatch_btns.append(swatch)
|
||||
|
||||
# Neutral gray row
|
||||
for col_i in PALETTE_COLS:
|
||||
var v := float(col_i) / (PALETTE_COLS - 1)
|
||||
var color := Color(v, v, v)
|
||||
var swatch := _make_palette_swatch(color)
|
||||
palette_grid.add_child(swatch)
|
||||
_modal_swatch_btns.append(swatch)
|
||||
|
||||
# Hex input row
|
||||
var hex_row := HBoxContainer.new()
|
||||
hex_row.add_theme_constant_override("separation", 4)
|
||||
@@ -1068,10 +1061,7 @@ func _on_modal_cancel() -> void:
|
||||
func _on_modal_ok() -> void:
|
||||
# Add current preview color to recent list
|
||||
if _modal_preview_swatch:
|
||||
var swatch_rect := _modal_preview_swatch.get_child(0) as ColorRect
|
||||
if swatch_rect:
|
||||
var final_color := swatch_rect.color
|
||||
_push_recent_color(final_color)
|
||||
_push_recent_color(_get_swatch_color(_modal_preview_swatch))
|
||||
_modal_root.visible = false
|
||||
|
||||
|
||||
@@ -1165,11 +1155,11 @@ func _on_randomise() -> void:
|
||||
_descriptor.body_type = all_types[randi() % all_types.size()]
|
||||
_descriptor.skin_tone = randi() % CharacterVisual.SKIN_TONES.size()
|
||||
|
||||
# Pick random hair from available
|
||||
var hair_ids := _scan_asset_ids("res://assets/characters/hair/", ".glb",
|
||||
["short_a", "short_b", "medium_a", "medium_b", "long_a", "bald"])
|
||||
if not hair_ids.is_empty():
|
||||
_descriptor.hair_id = hair_ids[randi() % hair_ids.size()]
|
||||
# Pick random head and hair from cached scan results (populated during tab build)
|
||||
if not _cached_head_ids.is_empty():
|
||||
_descriptor.head_id = _cached_head_ids[randi() % _cached_head_ids.size()]
|
||||
if not _cached_hair_ids.is_empty():
|
||||
_descriptor.hair_id = _cached_hair_ids[randi() % _cached_hair_ids.size()]
|
||||
_descriptor.hair_tint = Color.from_hsv(randf(), 0.4 + randf() * 0.3, 0.4 + randf() * 0.4)
|
||||
|
||||
# Random facial hair and eyebrows
|
||||
@@ -1177,7 +1167,6 @@ func _on_randomise() -> void:
|
||||
_descriptor.eyebrow_id = EYEBROW_OPTIONS[randi() % EYEBROW_OPTIONS.size()]
|
||||
|
||||
# Sync auto tints
|
||||
_hair_highlight_auto = true
|
||||
_eyebrow_tint_auto = true
|
||||
_facial_hair_tint_auto = true
|
||||
_descriptor.eyebrow_tint = _descriptor.hair_tint
|
||||
@@ -1240,6 +1229,18 @@ func _update_skin_tone_btns() -> void:
|
||||
# Shared helpers — color swatches
|
||||
# =============================================================================
|
||||
|
||||
## Get the current Color displayed in a swatch button (VBoxContainer → ColorRect).
|
||||
func _get_swatch_color(btn: Button) -> Color:
|
||||
for child in btn.get_children():
|
||||
if child is VBoxContainer:
|
||||
for sub in (child as VBoxContainer).get_children():
|
||||
if sub is ColorRect:
|
||||
return (sub as ColorRect).color
|
||||
if child is ColorRect:
|
||||
return (child as ColorRect).color
|
||||
return Color.WHITE
|
||||
|
||||
|
||||
func _make_color_swatch(color: Color, label_text: String, callback: Callable) -> Button:
|
||||
var btn := Button.new()
|
||||
btn.custom_minimum_size = Vector2(56, 44)
|
||||
@@ -1266,12 +1267,45 @@ func _make_color_swatch(color: Color, label_text: String, callback: Callable) ->
|
||||
lbl.clip_text = true
|
||||
vbox.add_child(lbl)
|
||||
|
||||
# Use _get_swatch_color to read current color at click time (avoids stale closure).
|
||||
btn.pressed.connect(func():
|
||||
_open_color_picker(color, btn, callback)
|
||||
_open_color_picker(_get_swatch_color(btn), btn, callback)
|
||||
)
|
||||
return btn
|
||||
|
||||
|
||||
## Build a non-interactive display swatch (auto-derived values — no click handler).
|
||||
func _make_display_swatch(color: Color, label_text: String) -> Button:
|
||||
var btn := Button.new()
|
||||
btn.custom_minimum_size = Vector2(56, 44)
|
||||
btn.flat = false
|
||||
btn.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
btn.focus_mode = Control.FOCUS_NONE
|
||||
|
||||
var vbox := VBoxContainer.new()
|
||||
vbox.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
vbox.add_theme_constant_override("separation", 2)
|
||||
btn.add_child(vbox)
|
||||
|
||||
var swatch_rect := ColorRect.new()
|
||||
swatch_rect.color = color
|
||||
swatch_rect.custom_minimum_size = Vector2(0, 24)
|
||||
swatch_rect.size_flags_horizontal = Control.SIZE_FILL
|
||||
swatch_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
vbox.add_child(swatch_rect)
|
||||
|
||||
if not label_text.is_empty():
|
||||
var lbl := Label.new()
|
||||
lbl.text = label_text.replace(" ●", "")
|
||||
lbl.add_theme_font_size_override("font_size", 9)
|
||||
lbl.add_theme_color_override("font_color", TEXT_DIM)
|
||||
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
lbl.clip_text = true
|
||||
vbox.add_child(lbl)
|
||||
|
||||
return btn
|
||||
|
||||
|
||||
func _set_swatch_color(btn: Button, color: Color) -> void:
|
||||
if not btn:
|
||||
return
|
||||
@@ -1424,9 +1458,9 @@ static func _scan_asset_ids(dir_path: String, ext: String, fallback: Array) -> A
|
||||
|
||||
func _get_clothing_ids_for_slot(slot: String) -> Array:
|
||||
# Clothing items are directories: clothing/{item_id}/{body_type}.glb
|
||||
# Slot assignment uses item name prefixes (convention, not a metadata field).
|
||||
var all_ids := _scan_subdirs("res://assets/characters/clothing/",
|
||||
["coveralls_basic", "jacket_basic", "shirt_basic", "pants_basic", "boots_basic", "gloves_basic"])
|
||||
# TODO: filter by slot once coverage.json includes a "slot" field. For now return all.
|
||||
match slot:
|
||||
"torso": return all_ids.filter(func(id: String) -> bool: return id.begins_with("coverall") or id.begins_with("jacket") or id.begins_with("shirt") or id.begins_with("dress") or id.begins_with("uniform"))
|
||||
"legs": return all_ids.filter(func(id: String) -> bool: return id.begins_with("pants") or id.begins_with("skirt"))
|
||||
@@ -1436,7 +1470,19 @@ func _get_clothing_ids_for_slot(slot: String) -> Array:
|
||||
|
||||
|
||||
func _get_accessory_ids_for_slot(slot: String) -> Array:
|
||||
return _scan_subdirs("res://assets/characters/accessories/", [])
|
||||
# Accessory items are directories: accessories/{item_id}/{body_type}.glb
|
||||
# Slot assignment uses item name prefixes (convention, not a metadata field).
|
||||
var all_ids := _scan_subdirs("res://assets/characters/accessories/", [])
|
||||
match slot:
|
||||
"hat": return all_ids.filter(func(id: String) -> bool: return id.begins_with("hat"))
|
||||
"goggles": return all_ids.filter(func(id: String) -> bool: return id.begins_with("goggle"))
|
||||
"mask": return all_ids.filter(func(id: String) -> bool: return id.begins_with("mask"))
|
||||
"backpack": return all_ids.filter(func(id: String) -> bool: return id.begins_with("backpack"))
|
||||
"belt": return all_ids.filter(func(id: String) -> bool: return id.begins_with("belt"))
|
||||
"wrist_l", "wrist_r": return all_ids.filter(func(id: String) -> bool: return id.begins_with("wrist"))
|
||||
"earring_l", "earring_r": return all_ids.filter(func(id: String) -> bool: return id.begins_with("earring"))
|
||||
"necklace": return all_ids.filter(func(id: String) -> bool: return id.begins_with("necklace"))
|
||||
_: return all_ids
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user