feat(client): character creation screen with live 3D preview (#705)

Full character creation UI with 5-tab customisation panel (Body, Head,
Hair, Clothing, Accessories), live 3D compositor preview via SubViewport,
D-165 color picker modal, keyboard navigation, and game flow integration.

- CharacterCreation scene + script (~1440 lines) implementing wireframe spec
- SubViewport with Camera3D (frontal/dramatic/overhead presets per D-158)
- Cardinal rotation (Q/E) per D-155, randomise (R), Tab/Enter/Esc navigation
- Body type grid (11 types, D-159), skin tone dock (9 tones, shared state)
- Head template, hair, facial hair, eyebrow grids with DirAccess asset scanning
- Clothing slot selector with per-item Primary/Secondary/Accent tints
- Accessory slot selector with Primary/Secondary tints (Array[Color] descriptor)
- 54-swatch color picker modal (D-165) with hex input and recent colors
- Auto-derive flags for highlight, eyebrow tint, facial hair tint from hair primary
- Game flow: main_menu → archetype select → character creation → game start
- GameState.character_visual_descriptor field for gameplay consumption
- Updated accessory_tints from single Color to Array[Color] in descriptor + compositor
- 40+ GdUnit tests covering scene structure, signals, keyboard nav, color derivation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-22 17:16:47 +01:00
co-authored by Claude Opus 4.6
parent fa0af110f1
commit 56e8c70081
8 changed files with 2366 additions and 26 deletions
+3 -1
View File
@@ -518,7 +518,9 @@ func _load_accessories(desc: CharacterVisualDescriptor) -> void:
push_warning("CharacterVisual: unknown accessory slot '%s'" % slot)
continue
var path := BASE_PATH + "accessories/%s.glb" % item_id
var tint: Color = desc.accessory_tints.get(item_id, Color.WHITE)
# accessory_tints is now Array[Color] (Primary + Secondary). Pass Primary ([0]).
var tints: Array = desc.accessory_tints.get(item_id, [])
var tint: Color = tints[0] if not tints.is_empty() else Color.WHITE
var att := _attach_to_bone(path, bone_name, tint)
if att != null:
# Track accessory attachments separately for get_accessory_node_count()