New implant app (app_path implant/browser, key B, fullscreen), sibling
to the Atlas per Jeroen's IA ruling: kind picker -> generic filterable
index (live search-mode typing) -> generic detail, parameterized per
kind, composed entirely from D-169 components. available_in_companion
left unset (default true) — the app appears in the companion shell
automatically via the generic-host seam, zero companion-side wiring.
browser_adapter.gd is the sole home of literal wire field names: maps
Oscar's BrowseResponse contract ({id, primary, secondary} index rows;
BrowseDetail enum-as-single-key-map) to view models for all six kinds,
folding join partners (system economy/factions/culture, corporation
presence, commodity production chains with nested Leontief inputs).
browse_protocol.gd split out of protocol.gd (max-file-lines);
sim_bridge gains browse_response_received + request_browse_index/detail.
Live-data catch: a present-but-NULL key (unnamed asteroid belt
proper_name) bypasses Dictionary.get fallbacks and rendered '<null>' —
_display_or() null-vs-absent helper applied across all six detail
mappers, 4 regression tests distinct from the absent-key cases.
43 gdUnit adapter cases; full suite 3074 green. Live-verified against
a real server + real systems.db: all six kinds Ready with real row
counts (301/3240/466/165/36/28), detail drill-down, NotFound on bogus
ids. Spawn-mode DB resolution issue found during verification is
pre-existing (cwd-relative data/systems.db) — server-side fix follows
separately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
153 lines
4.6 KiB
GDScript
153 lines
4.6 KiB
GDScript
class_name BrowserDetailScreen
|
|
extends Control
|
|
## Detail screen for one browser entity (T-1133, D-254 §4). An ImplantPanel of
|
|
## ImplantDataRows (every column the wire response carries for that entity)
|
|
## plus an ImplantTextBlock for free-text description/cultural_description
|
|
## fields where the kind has one. One instance handles all six kinds —
|
|
## parameterized by set_entity(), not six near-duplicate screens.
|
|
|
|
signal back_requested
|
|
|
|
const COLOR_BG: Color = Color("#0d1117")
|
|
const PANEL_WIDTH: float = 420.0
|
|
const PANEL_MARGIN: float = 16.0
|
|
|
|
var _kind: String = ""
|
|
var _entity_id: String = ""
|
|
var _view: Dictionary = {} # BrowserAdapter.detail_view() result
|
|
var _loading: bool = false
|
|
var _error: String = ""
|
|
var _status: String = ""
|
|
|
|
var _panel = null # ImplantPanel
|
|
var _implant_theme = null # ImplantTheme
|
|
var _pending: ImplantPending = null
|
|
|
|
|
|
func _ready() -> void:
|
|
mouse_filter = Control.MOUSE_FILTER_STOP
|
|
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
|
|
|
|
func _draw() -> void:
|
|
draw_rect(Rect2(Vector2.ZERO, get_rect().size), COLOR_BG)
|
|
|
|
|
|
func setup(implant_theme) -> void:
|
|
_implant_theme = implant_theme
|
|
_build_panel()
|
|
_build_pending(implant_theme)
|
|
|
|
|
|
func set_entity(kind: String, entity_id: String) -> void:
|
|
_kind = kind
|
|
_entity_id = entity_id
|
|
_view = {}
|
|
_loading = true
|
|
_error = ""
|
|
_status = ""
|
|
_rebuild_panel()
|
|
if _pending:
|
|
_pending.start("QUERYING " + BrowserAdapter.kind_label(kind))
|
|
SimBridge.request_browse_detail(kind, entity_id)
|
|
|
|
|
|
## Feed a decoded BrowseResponse into this screen. Ignored if the response
|
|
## doesn't match this screen's current kind (stale response after the player
|
|
## navigated elsewhere).
|
|
func receive_response(response: Dictionary) -> void:
|
|
if response.get("kind", "") != _kind:
|
|
return
|
|
_loading = false
|
|
if _pending:
|
|
_pending.stop()
|
|
_status = BrowserAdapter.response_status(response)
|
|
if _status == "Error":
|
|
_error = BrowserAdapter.response_error(response)
|
|
_view = {}
|
|
elif _status == "NotFound":
|
|
_error = ""
|
|
_view = {}
|
|
else:
|
|
_error = ""
|
|
var payload: Dictionary = BrowserAdapter.detail_payload(response)
|
|
_view = BrowserAdapter.detail_view(_kind, payload)
|
|
_rebuild_panel()
|
|
|
|
|
|
func enter(payload: Dictionary) -> void:
|
|
var kind: String = payload.get("kind", "")
|
|
var entity_id: String = payload.get("entity_id", "")
|
|
if kind != _kind or entity_id != _entity_id:
|
|
set_entity(kind, entity_id)
|
|
else:
|
|
_rebuild_panel()
|
|
|
|
|
|
func leave() -> void:
|
|
pass
|
|
|
|
|
|
# =============================================================================
|
|
# Panel
|
|
# =============================================================================
|
|
|
|
|
|
func _build_panel() -> void:
|
|
_panel = ImplantPanel.new()
|
|
_panel.name = "DetailPanel"
|
|
_panel.theme_resource = _implant_theme
|
|
_panel.custom_minimum_size.x = PANEL_WIDTH
|
|
_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
_panel.position = Vector2(PANEL_MARGIN, PANEL_MARGIN)
|
|
add_child(_panel)
|
|
|
|
|
|
func _build_pending(implant_theme) -> void:
|
|
_pending = ImplantPending.new()
|
|
_pending.apply_implant_theme(implant_theme)
|
|
_pending.position = Vector2(PANEL_MARGIN + PANEL_WIDTH + 24.0, PANEL_MARGIN)
|
|
add_child(_pending)
|
|
|
|
|
|
func _rebuild_panel() -> void:
|
|
if not _panel:
|
|
return
|
|
_panel.clear()
|
|
|
|
if _loading:
|
|
_panel.add_component(ImplantHeader.new(BrowserAdapter.kind_label(_kind), _entity_id))
|
|
_panel.add_component(ImplantSeparator.new())
|
|
_panel.add_component(ImplantTextBlock.new("querying registry…"))
|
|
elif not _error.is_empty():
|
|
_panel.add_component(ImplantHeader.new(BrowserAdapter.kind_label(_kind), _entity_id))
|
|
_panel.add_component(ImplantSeparator.new())
|
|
_panel.add_component(ImplantTextBlock.new("[ERROR] " + _error))
|
|
elif _status == "NotFound":
|
|
_panel.add_component(ImplantHeader.new(BrowserAdapter.kind_label(_kind), _entity_id))
|
|
_panel.add_component(ImplantSeparator.new())
|
|
_panel.add_component(ImplantTextBlock.new("entry not found: " + _entity_id))
|
|
else:
|
|
_rebuild_ready_panel()
|
|
|
|
_panel.add_component(ImplantSeparator.new())
|
|
_panel.add_component(ImplantTextBlock.new("esc back"))
|
|
|
|
|
|
func _rebuild_ready_panel() -> void:
|
|
var name_str: String = str(_view.get("name", _entity_id))
|
|
var subtitle: String = str(_view.get("subtitle", BrowserAdapter.kind_label(_kind)))
|
|
_panel.add_component(ImplantHeader.new(name_str, subtitle))
|
|
_panel.add_component(ImplantSeparator.new())
|
|
|
|
var rows: Array = _view.get("rows", [])
|
|
for row: Dictionary in rows:
|
|
var label: String = str(row.get("label", ""))
|
|
var value: String = str(row.get("value", "—"))
|
|
_panel.add_component(ImplantDataRow.new("%-22s %s" % [label, value]))
|
|
|
|
var text_block: String = str(_view.get("text", ""))
|
|
if not text_block.is_empty():
|
|
_panel.add_component(ImplantSeparator.new())
|
|
_panel.add_component(ImplantTextBlock.new(text_block))
|