feat(ui): location picker in Bookmark tab (Workstream 7, #680)
Adds the starting-location picker as a sub-component of the Bookmark tab detail view, per Araminta's spec and D-128's "culture implicit in location" constraint. Fills the space W6 reserved below the CAREER / CAPITAL data rows. Picker structure: - "STARTING LOCATION" section label (DIM_TEXT, 10px, all-caps). - ScrollContainer min_height=80 → VBoxContainer of selectable items. - Each item: Button with child VBox carrying the location name Label (PRIMARY_TEXT, 12px) and an optional culture tag Label (DIM_TEXT, 10px, mouse_filter IGNORE per D-128). Culture label is NOT rendered when `allowed_locations_cultures[i]` is empty — no "Unknown" placeholder, the row just shrinks. Behavior: - Clicking a bookmark card auto-selects its default_location in the picker (handled via _selected_location_id + _update_location_selection). - Clicking a location item updates _selected_location_id and re-gates the Start button (already checked in W6). - Switching bookmarks rebuilds the picker list for the new allowed_locations; prior selection cleared. - Parallel-array length mismatch defended: reads `cultures[i] if i < cultures.size() else ""` so a short cultures array won't crash rendering. D-128 compliance: - No culture dropdown or filter anywhere. - Culture tag Label is strictly display: MOUSE_FILTER_IGNORE, no signal handlers. - CharacterProfile carries only start_location_id; no culture_id. Other: removes W6's placeholder "Starting Location: X" ImplantDataRow since the picker supersedes it; separator before the picker preserved. Verification: - gdlint clean - Headless parse + scanner check (widened per hardened pr-push skill): no new errors. Pre-existing autoload class_name noise filtered per CLAUDE.md. - test_character_creation_sprint28 88/88, test_protocol 62/62, test_implant_* all green, test_client_p3 24/24, test_ui_framework_sprint15 54/54. Workstream 8 (Skills tab stub content) lands next — #618 closes then. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -273,12 +273,16 @@ var _appearance_active_idx: int = 0
|
||||
var _appearance_sub_btns: Array[Button] = []
|
||||
var _appearance_sub_sections: Array[Control] = []
|
||||
|
||||
# --- Bookmark tab state (WS6) ---
|
||||
# --- Bookmark tab state (WS6/WS7) ---
|
||||
var _selected_bookmark_id: String = ""
|
||||
var _selected_location_id: String = ""
|
||||
var _bookmark_cards: Array[Button] = []
|
||||
var _bookmark_detail_panel: ImplantPanel = null
|
||||
var _bookmark_cards_vbox: VBoxContainer = null
|
||||
var _location_items: Array[Button] = []
|
||||
var _location_item_ids: Array[String] = []
|
||||
var _location_item_labels: Array[Label] = []
|
||||
var _location_vbox: VBoxContainer = null
|
||||
|
||||
# --- Asset manifest (loaded once, replaces filesystem scanning) ---
|
||||
var _manifest: Dictionary = {}
|
||||
@@ -739,6 +743,11 @@ func _update_bookmark_card_selection() -> void:
|
||||
|
||||
func _refresh_bookmark_detail(bm: Dictionary) -> void:
|
||||
_bookmark_detail_panel.clear()
|
||||
_location_items.clear()
|
||||
_location_item_ids.clear()
|
||||
_location_item_labels.clear()
|
||||
_location_vbox = null
|
||||
|
||||
if bm.is_empty():
|
||||
_bookmark_detail_panel.add_component(
|
||||
ImplantHeader.new("Select a Bookmark", "Choose your starting conditions")
|
||||
@@ -757,9 +766,9 @@ func _refresh_bookmark_detail(bm: Dictionary) -> void:
|
||||
ImplantDataRow.new("Starting Capital: %d tr" % int(capital), TEXT_COLOR)
|
||||
)
|
||||
|
||||
var default_loc: String = bm.get("default_location", "")
|
||||
if not default_loc.is_empty():
|
||||
_bookmark_detail_panel.add_component(ImplantDataRow.new("Starting Location: " + default_loc, TEXT_DIM))
|
||||
# Location picker (WS7) — replaces the single "Starting Location: X" DataRow
|
||||
_bookmark_detail_panel.add_component(ImplantSeparator.new())
|
||||
_build_location_picker(bm)
|
||||
|
||||
var flavor: String = bm.get("flavor", "")
|
||||
if not flavor.is_empty():
|
||||
@@ -771,6 +780,85 @@ func _update_start_btn_state() -> void:
|
||||
_footer_start.disabled = _selected_bookmark_id.is_empty() or _selected_location_id.is_empty()
|
||||
|
||||
|
||||
func _build_location_picker(bm: Dictionary) -> void:
|
||||
var allowed: Array = bm.get("allowed_locations", [])
|
||||
var cultures: Array = bm.get("allowed_locations_cultures", [])
|
||||
|
||||
var section_lbl := Label.new()
|
||||
section_lbl.text = "STARTING LOCATION"
|
||||
section_lbl.add_theme_color_override("font_color", TEXT_DIM)
|
||||
section_lbl.add_theme_font_size_override("font_size", 10)
|
||||
section_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_bookmark_detail_panel.add_component(section_lbl)
|
||||
|
||||
if allowed.is_empty():
|
||||
return
|
||||
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.custom_minimum_size = Vector2(0, 80)
|
||||
scroll.size_flags_horizontal = Control.SIZE_FILL
|
||||
_bookmark_detail_panel.add_component(scroll)
|
||||
|
||||
_location_vbox = VBoxContainer.new()
|
||||
_location_vbox.size_flags_horizontal = Control.SIZE_FILL
|
||||
_location_vbox.add_theme_constant_override("separation", 2)
|
||||
scroll.add_child(_location_vbox)
|
||||
|
||||
for i in allowed.size():
|
||||
var loc_id: String = allowed[i]
|
||||
var culture: String = cultures[i] if i < cultures.size() else ""
|
||||
|
||||
var btn := Button.new()
|
||||
btn.size_flags_horizontal = Control.SIZE_FILL
|
||||
btn.flat = false
|
||||
btn.focus_mode = Control.FOCUS_NONE
|
||||
btn.pressed.connect(_on_location_selected.bind(loc_id))
|
||||
|
||||
var vbox := VBoxContainer.new()
|
||||
vbox.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
vbox.add_theme_constant_override("separation", 1)
|
||||
vbox.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
btn.add_child(vbox)
|
||||
|
||||
var name_lbl := Label.new()
|
||||
name_lbl.text = loc_id
|
||||
name_lbl.add_theme_color_override("font_color", TEXT_COLOR)
|
||||
name_lbl.add_theme_font_size_override("font_size", 12)
|
||||
name_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
vbox.add_child(name_lbl)
|
||||
|
||||
if not culture.is_empty():
|
||||
var culture_lbl := Label.new()
|
||||
culture_lbl.text = culture
|
||||
culture_lbl.add_theme_color_override("font_color", TEXT_DIM)
|
||||
culture_lbl.add_theme_font_size_override("font_size", 10)
|
||||
culture_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
vbox.add_child(culture_lbl)
|
||||
|
||||
_location_vbox.add_child(btn)
|
||||
_location_items.append(btn)
|
||||
_location_item_ids.append(loc_id)
|
||||
_location_item_labels.append(name_lbl)
|
||||
|
||||
_update_location_selection()
|
||||
|
||||
|
||||
func _on_location_selected(loc_id: String) -> void:
|
||||
_selected_location_id = loc_id
|
||||
_update_location_selection()
|
||||
_update_start_btn_state()
|
||||
|
||||
|
||||
func _update_location_selection() -> void:
|
||||
for i in _location_items.size():
|
||||
var selected: bool = (i < _location_item_ids.size() and _location_item_ids[i] == _selected_location_id)
|
||||
_set_item_selected(_location_items[i], selected)
|
||||
if i < _location_item_labels.size() and is_instance_valid(_location_item_labels[i]):
|
||||
_location_item_labels[i].add_theme_color_override(
|
||||
"font_color", HIGHLIGHT_COLOR if selected else TEXT_COLOR
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Tab: Body (Task #2)
|
||||
# =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user