feat(ui): Bookmark tab content + CharacterProfile signal payload (Workstream 6)
Fills in the Bookmark tab stubbed in W5 with the full spec from
Araminta: card list + detail view + start-button gating. Also changes
the creation_confirmed signal to carry a CharacterProfile instead of
bare CharacterVisualDescriptor, consolidating bookmark + location
selection into one payload object.
Bookmark tab (left pane, 35%):
- ScrollContainer over VBoxContainer of card Buttons, one per entry in
GameState.bookmark_catalog. Each card: title (PRIMARY_TEXT,
font_header 15px) / subtitle (DIM_TEXT, font_small 10px, clipped) /
career badge (ACCENT_ACTIVE, all-caps). Selected state uses existing
ITEM_SELECTED_BG + ITEM_SELECTED_BORDER. custom_minimum_size
Vector2(180, 64).
Detail view (right pane, 65%):
- ImplantPanel composed via add_component:
- ImplantHeader (bookmark.title, bookmark.subtitle)
- ImplantSeparator
- ImplantTextBlock (flavor, autowrap, PRIMARY_TEXT)
- ImplantSeparator
- ImplantDataRow CAREER (accent_active) / CAPITAL (accent_positive,
format "%d Tractus") / STARTING LOCATION
- ImplantSeparator
- [location picker space reserved — W7 fills it]
Selection:
- Card click stores _selected_bookmark_id, auto-assigns
_selected_location_id from bookmark.default_location, rebuilds
detail view.
- Start button (footer) gated on both _selected_bookmark_id and
_selected_location_id non-empty.
- Randomize while Bookmark tab is active picks a random bookmark +
one of its allowed_locations and skips appearance randomization.
Signal contract change:
- creation_confirmed(profile: CharacterProfile) replaces
creation_confirmed(descriptor: CharacterVisualDescriptor).
- CharacterProfile now extends RefCounted (was Resource) with
non-exported fields — it's a one-shot signal payload, never
persisted. This also sidesteps the scanner error that the prior
@export var descriptor: CharacterVisualDescriptor on a Resource
caused (RefCounted types cannot be @export-ed).
- _on_start emits a CharacterProfile built from _descriptor +
_selected_bookmark_id + _selected_location_id, then sends
ConfirmBookmark via SimBridge.send_named_action before scene
transition.
Test updates:
- test_character_creation_sprint28.gd signal receivers switched to
untyped to accept CharacterProfile without hitting class_name
parse-order at test-suite scan time. 88/88 pass.
Verification:
- gdlint clean
- godot --headless --path client --quit — no SCRIPT ERROR (prior
character_profile.gd scanner noise now gone after the RefCounted
conversion)
- test_character_creation_sprint28 88/88, test_protocol 62/62,
test_implant_nav_stack 52/52
Workstream 7 (location picker as sub-component of the Bookmark detail
view) follows. W8 fills the Skills tab.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -145,13 +145,13 @@ func test_creation_confirmed_emits_on_start() -> void:
|
||||
func test_creation_confirmed_carries_descriptor() -> void:
|
||||
if _scene == null:
|
||||
return
|
||||
var received_descriptor: CharacterVisualDescriptor = null
|
||||
_scene.creation_confirmed.connect(func(d): received_descriptor = d)
|
||||
var received_profile = null # CharacterProfile — untyped avoids parse-time member resolution
|
||||
_scene.creation_confirmed.connect(func(p): received_profile = p)
|
||||
_scene._on_start()
|
||||
assert_bool(received_descriptor != null).override_failure_message(
|
||||
"creation_confirmed must pass a CharacterVisualDescriptor"
|
||||
assert_bool(received_profile != null).override_failure_message(
|
||||
"creation_confirmed must pass a CharacterProfile"
|
||||
).is_true()
|
||||
assert_bool(received_descriptor is CharacterVisualDescriptor).is_true()
|
||||
assert_bool(received_profile is CharacterProfile).is_true()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -161,9 +161,13 @@ func test_creation_confirmed_carries_descriptor() -> void:
|
||||
func test_descriptor_initialized_on_ready() -> void:
|
||||
if _scene == null:
|
||||
return
|
||||
var desc: CharacterVisualDescriptor = null
|
||||
_scene.creation_confirmed.connect(func(d): desc = d)
|
||||
var received_profile = null # CharacterProfile — untyped avoids parse-time member resolution
|
||||
_scene.creation_confirmed.connect(func(p): received_profile = p)
|
||||
_scene._on_start()
|
||||
assert_bool(received_profile != null).is_true()
|
||||
if received_profile == null:
|
||||
return
|
||||
var desc = received_profile.descriptor
|
||||
assert_bool(desc != null).is_true()
|
||||
if desc == null:
|
||||
return
|
||||
@@ -247,12 +251,12 @@ func test_body_type_selection_updates_descriptor() -> void:
|
||||
if _scene == null:
|
||||
return
|
||||
_scene._on_body_type_selected(CharacterVisualDescriptor.BodyType.THIN_F)
|
||||
var desc: CharacterVisualDescriptor = null
|
||||
_scene.creation_confirmed.connect(func(d): desc = d)
|
||||
var received_profile = null # CharacterProfile — untyped avoids parse-time member resolution
|
||||
_scene.creation_confirmed.connect(func(p): received_profile = p)
|
||||
_scene._on_start()
|
||||
if desc == null:
|
||||
if received_profile == null:
|
||||
return
|
||||
assert_int(desc.body_type as int).override_failure_message(
|
||||
assert_int(received_profile.descriptor.body_type as int).override_failure_message(
|
||||
"Selecting THIN_F must update descriptor.body_type"
|
||||
).is_equal(CharacterVisualDescriptor.BodyType.THIN_F)
|
||||
|
||||
@@ -280,12 +284,12 @@ func test_skin_tone_selection_updates_descriptor() -> void:
|
||||
if _scene == null:
|
||||
return
|
||||
_scene._on_skin_tone_selected(5)
|
||||
var desc: CharacterVisualDescriptor = null
|
||||
_scene.creation_confirmed.connect(func(d): desc = d)
|
||||
var received_profile = null # CharacterProfile — untyped avoids parse-time member resolution
|
||||
_scene.creation_confirmed.connect(func(p): received_profile = p)
|
||||
_scene._on_start()
|
||||
if desc == null:
|
||||
if received_profile == null:
|
||||
return
|
||||
assert_int(desc.skin_tone).override_failure_message(
|
||||
assert_int(received_profile.descriptor.skin_tone).override_failure_message(
|
||||
"Selecting skin tone index 5 must update descriptor.skin_tone"
|
||||
).is_equal(5)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user