Hoshe's blocking find: district_pos_at divided the latitude axis by
tex_h where the server forward map uses ta.h.saturating_sub(1)
(district_profile.rs:1436; ground-truth inverse aliveness_probe.rs:511)
— every off-equator click descended into the wrong district, up to
~20km drift, live-repro'd at 10.2km. Fixed to /(tex_h - 1.0) with the
reference implementation's degenerate-texture guard; verified against
the repro (row 50: old formula recovered 41, fixed recovers 50 exact).
Columns were already correct (longitude wraps — the asymmetry the
docstring documented but the code didn't implement).
Regression tests transcribe the server forward formula (source cited)
and round-trip three off-equator rows — one per hemisphere plus
near-pole — asserting EXACT recovery; a dedicated drift guard is
framed as Tyre's C2 (a client-side twin of a server mapping owns its
own round-trip proof). Root-caused the coverage hole: the old
equator test sampled tex_h*0.5 believing it was the equator pixel —
the true equator is (tex_h-1)*0.5, so the test never verified what it
claimed; corrected with the why documented.
Araminta's find: the descend reticle drew unconditionally on hover —
over a city the marker flared white (city-click signal) while the
reticle+extent label promised descent, though the click correctly
resolved to city data. Extracted _should_draw_descend_reticle() with
the missing _hovered_city.is_empty() clause; three state tests pin
shows-over-map / hides-over-city / hides-when-idle.
test_atlas_descend_entry 32/32; adjacent clusters no ripple; gdlint
clean (atlas_viewer.gd at exactly the 1000-line cap).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entry per Jeroen's 2026-07-21 revision: planetary heightmap is now
FIXED — all drag-pan/wheel-zoom input removed (set_view/get_view_*
capture API survives for the golden harness); hover shows a
not-to-scale bracket reticle with the real extent labeled (a true
n=32 rectangle is sub-pixel on the planetary canvas — the honest
representation given the morph transition is deferred), and a click
that misses every city marker descends (city-click wins — one
gesture, two contextual reads, no modifier). Descent pushes a new
'district' nav screen centered on the click point's DistrictPos via
atlas_descend_geometry.district_pos_at (the pixel-to-district inverse
of the server mapping, verified against scale.rs).
Regional mode: atlas_window_viewer draws the composite (morphology x
elev_q lightness base; temp/moisture/veg toggles — temp reuses the
region-ramp colorizer exactly; Marine=6 transparent; glaciation
always-on tint matching apply_ice_tint's REAL gate, None|Light no-op,
over the amendment's looser prose — documented); pan-on-held-composite
with edge-crossing refetch + border-fade during the queue-based
derive wait; zoom never refetches. atlas_window_cache: LRU keyed
(body_id, center, n), touch-on-read, evict-only, no freshness (D-227).
atlas_window_request mirrors the generation-proxy pending-retry shape
for None-until-derived. Codec: window params omitted from the wire
when absent — byte-identical for every existing caller.
Live-verified against the T-1137 server in-worktree: real round-trip
on a GJ380c coastal district (6 fields x 1024 cells), echo staleness
guard, genuine ~1.4s background-derive wait, pan-edge refetch to an
adjacent window, cache-hit on re-descent with zero network. Full
client suite 3194/3194; gdlint clean on all 18 files.
Open follow-ups flagged in-code: header location label always falls
back to coordinates (nearest-settlement needs a join the district
window does not carry); atlas_standalone.gd's 'atlas_app.gd is never
modified' doc line is now imprecise.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hoshe's finding: the wrap fix gave the bar container the full
header-adjacent width, and with ALIGNMENT_END + the pre-existing
MOUSE_FILTER_STOP that left a ~700px dead strip (1920px screens) left
of the chips silently swallowing map clicks/drags near the top edge —
a regression the wrap change introduced by widening the rect without
revisiting the filter. Two edits: the container is now
MOUSE_FILTER_IGNORE (the legend-panel/header/empty-notice idiom; chip
Buttons STOP their own events so toggles and tooltips are unaffected),
and _is_over_ui() no longer checks the bar rect (with IGNORE, chip
events never reach the viewer — checking the wide rect would recreate
the dead strip). Regression test pins both: filter mode + an
empty-strip point not registering as UI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Jeroen's first make atlas hands-on: 17 overlay chips in a single
HBoxContainer row ran off the right screen edge (positioning also
guessed the bar's width with a 520px fallback). The bar is now an
HFlowContainer with ALIGNMENT_END; the viewer gives it everything
right of the header (OVERLAY_BAR_HEADER_RESERVE) and rows wrap
right-aligned — verified via live capture, all 18 chips visible.
OVERLAY_DEFS tooltips leaked dev provenance into player-facing text —
ticket ids (#960, T-960, T-1046, T-1118, T-1119) and decision ids
(D-181, D-226, D-243) mean nothing in-game. Stripped to plain
language; the visibility-ladder semantics (public signal / observable
/ needs corporate contact / needs insider access) stay.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
T-1118: gen_region_grid overlay (label TMP) — _draw_gen_region_grid
copies _draw_gen_district's self-contained mapping (dims from the
layer dict); mean-temp cold-to-hot ramp over -50..+50 C;
REGION_TEMP_NONE_DC airless sentinel = skip-cell (undrawn, never an
invented color); legend entry.
T-1119 (touch points 3-6): quarter_footprints protocol passthrough;
gen_l4_quarters overlay (label QTR) — density-scaled glyph anchored
on the L3 settlement dot joined by city_id, shape = dominant district
type (corner-tab/diamond marks for Commercial/Industrial/
Administrative), color = density ramp on the settlement-gold family,
zoom-gated at SETTLEMENT_LABEL_MIN_ZOOM; landmark/corridor counts
never drawn (D-226(d) tooltip-only ceiling); legend entry.
Structure: atlas_viewer.gd and protocol.gd were over gdlint's
1000-line cap before this batch; cleanly-separable responsibilities
extracted on existing precedent — atlas_generation_state.gd (per-layer
data + accessors), atlas_generation_proxy.gd (polling/retry/pending
machinery), atlas_overlay_colors.gd (pure ramp/shape lookups),
atlas_map_protocol.gd (atlas/starmap/citynames codec, the
browse_protocol.gd delegate pattern). Public APIs preserved exactly;
_gen_state stays a field default (RefCounted, pre-_ready safe) because
_ready()-construction breaks every bare AtlasViewer.new() test —
documented inline.
Tests: registration + round-trip for both overlays; pure-function
suites for the temp ramp (endpoints/midpoint/clamp/sentinel) and
quarter glyph (scaling, zoom gate, ramp, notch across all 9
DistrictTypes); Tier-2 replay asserts exact literals from the real
server-generated fixture incl. the airless sentinel. Color.lerp(a,b,
1.0) is not bit-exact to b — endpoint assertions use per-component
is_equal_approx. Full suite 3094/3094; gdlint zero warnings incl. the
two previously-over-cap files. Live capture: legend grows to 7
sections, TMP/QTR toggles clean against a live server.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
New atlas_standalone scene/script: attach-or-spawn boot (one REAL
connect_to_sim attempt at SR_PORT-or-9876 — a separate throwaway TCP
probe was proven by live run to kill a pre-accept-loop server via
broken handshake pipe; never abandon a connected socket), else spawn
--port 0 via new ServerProcess.start_with_pipe + LISTENING:{port}
stdout parse, retry against the resolved port. Reader role wired end
to end: protocol.encode_startup_message optional role param (empty
omits the wire key — byte-identical for all existing callers),
sim_bridge.connection_role suppresses the post-handshake
RequestAllSettings auto-send, hud_groups skips AutoPause/AutoResume
sends for readers (all three would otherwise burn Reader violation
strikes per the T-1130 matrix — endorsed by Oscar).
Generic implant host per D-254 SS3: the shell instantiates ALL
registered implant apps; implant_app_manifest gains
available_in_companion (opt-out, default true) and
implant_registry.instantiate_all a standalone filter param (default
preserves hud.gd behavior byte-identically). Boot order is
instantiate_all THEN open_app (reverse renders a permanently black
window — app_changed fires with no listener; matches hud.gd's order).
Owned-server lifecycle: _exit_tree stops a spawned child, attached
servers survive companion close. Known engine limitation documented:
raw SIGTERM bypasses all Godot notifications and orphans a spawned
server; WM close paths verified clean.
Live-verified: spawn-mode (301 systems rendered from systems.db over
the wire), attach-mode, two simultaneous readers, clean shutdown with
zero orphan processes. 14 new gdUnit tests (port/LISTENING parsing).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AtlasViewer gains public set_view(zoom, offset)/get_view_offset (clamped, review-flagged API gap) + gdUnit suite. visual_capture.gd gains an atlas_matrix scenario (one boot, tests/atlas_shots.json-driven: 7 verified bodies x zoom x overlay sets, signal-gated waits on atlas_layers_received/city_names_received with timeout fallback). 12 individually-addressable golden scenarios registered in tests/visual.json. Root-level settings.db* gitignored (capture-spawned servers write their settings store to cwd). Fixes found live: GDScript lambdas capture value-type locals BY VALUE (wait-flag moved to Array carrier); RegionalScreen.enter() no-ops when the screen id is unchanged (direct enter() per body after the first).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
H1 demux: ShapeProbe defensive multi-shape rejection (union frames now Err, not first-match; +2 tests) and doc claim made honest. H2/T1 SystemIndex.reset_test_state() folded into SimBridge.reset_test_state() (load() inline per autoload rule) + has_pending_request() accessor. H3 no-op tests now assert the replay flag both directions. H4 retry test actually ingests a failure and asserts the retry semantic. H5 error fixture uses the normalized status string. H6 bridge_tcp e2e sends all five frame shapes over real TCP (star-map + city-names buffers asserted). H7 positive replay-on-CONNECTED test via the test_local_bridge test-mode-flip precedent (stub bridge captures + decodes the request bytes). H8/T2 stale PLACEHOLDER doc replaced with the confirmed contract. H9 is_capital doc matches the COALESCE reality. T-r1 demux ceiling written down (next shape = tagged envelope). T-r2 AtlasLayerResponse governance ceiling comment. Lead item: the four cargo-fmt-formatted files from the gate round are now committed (layer_proxy/plugin/bridge-mod/main). H10 note for the record: the 13 snapshot_*.msgpack fixtures in commit 845737617 were regenerated because they were stale against their own generator (pre-existing version-key removal) — verified harmless, no client reads that key.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
T-960: gen_l2_roads (MaintenanceAuthority-colored polylines, rail styling, junction markers) + gen_l3_settlements (size-scaled markers, capital shape, name labels) overlays — cities render on generated bodies for the first time; left-side generation legend panel (D-226 item 3, data-driven per-overlay spec, implant component library); protocol.gd decodes road_graph/settlements + the two new response types. T-949: system_index/atlas_app/overview_screen migrated off the direct star_map_data.json read to StarMapRequest over the bridge (loading state + replay-on-connect, no silent file fallback); atlas_viewer _load_markers requests CityNamesResponse for non-Sol bodies; Sol keeps the legacy authored markers.json geometry read (D-236/T-1073, load-bearing guard). Lead fix: _send_star_map_request now carries the same guard as request_star_map — the autoload's _star_map_wanted leaked across gdUnit suites and the unguarded replay-on-CONNECTED crashed 8 pre-existing flow tests on a Nil bridge; reset_test_state clears the flag. Fixtures regenerated via gen_fixtures (road/settlement samples). Full suite 2946/2946.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Client half — the cascade's district tier is now visible in the Atlas map.
- protocol.gd: decode the district_grid field from AtlasLayerResponse.
- atlas_viewer.gd: a gen_district ('MRPH') toggle in OVERLAY_DEFS + the
district-grid generation state (set/get) + wire it from the Ready response.
- atlas_marker_overlay.gd: _draw_gen_district() paints the coarse cols×rows grid,
each cell coloured by its MorphologyZone discriminant (D-239 §6, 17-zone
palette), semi-transparent under the Layer-1 line overlays. Planetary map view
(not the 2km on-demand districts — those are Phase 5 in-world).
gdUnit4 test: overlay registered + district grid round-trips through the viewer
and the protocol decode. Visual tuning of the palette can follow once eyeballed
in the running Atlas.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Production fixes surfaced by honest test triage:
- hud_groups.gd: _set_group_z crashed on freed HUD nodes — the typed loop
variable errors before the is_instance_valid guard runs; prune first
- fog_state.gd: _resize cleared _prev_visible (world-space keys survive
resizes), so pre-resize tiles never decayed VISIBLE→EXPLORED (D-059)
Test debt (T-928/929/934/935/936/937/938/939, T-864, T-973): lambda
local-capture bugs rewritten with array captures (now assert exact
emission counts), e2e suites updated to the current handshake +
StartupMessage protocol and stream-aware reads against the live binary,
fog perf test measures steady state, chime test pins the shipped 800ms
catalog asset (D-067 amended separately), monologue gdUnit4 API typo,
battery-warning tests follow the MetaScreen on_open lifecycle. 3 sprint2
proof tests revived (corner_reveal had passed from the wrong tile — NPC3
blocks (18,14); route corrected). Soft-skips converted to real do_skip
reporting. T-1068: 7 orphan .gd.uid deleted, _format_pop/_format_radius
deduped into atlas_format.gd (preload, no class_name — headless cache).
Suite: 1264 cases/20 failures → 1268/0, independently re-verified
(2536/2536, exit 0).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Generation overlays map positions from Layer1Output.grid_w/grid_h onto the
displayed texture (not the markers.json grid), fixing the off-by-2 scale.
- AtlasViewer loads ImplantPending by path instead of referencing the new
class_name, so a stale global-class cache (a session that hasn't re-imported
after the class was added) can't fail to parse AtlasViewer and break the
atlas from opening.
- Rivers and mouths use the reliefmap's own water colour (sampled median ocean
blue) so river lines blend into surface water instead of reading as a
distinct-coloured line flowing onto the sea.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A reusable implant-layer busy indicator (Araminta's spec): an animated
bracket sweep `[ >>>>···· ]` with a caption, themed entirely via ImplantTheme.
The Atlas shows it centered over the canvas while the Layer-1 proxy is Pending
and hides it on Ready — so cache hits never flash it and serverless mode never
shows it.
Layer separation (per direction): ImplantPending belongs to the diegetic
implant UI only. The global, non-diegetic UI layer must use its own busy
indicator with its own visuals — sharing the sweep logic is fine, resting on
this theme/these glyphs is not. Documented in the component header.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
show_body() now requests the body's Layer-1 cascade output via
SimBridge.request_atlas_layers(); the response feeds set_generation_layer1().
The proxy returns Pending on a cache miss and generates in the background
(D-225), so the viewer re-requests every 0.5s (20-retry ceiling) until Ready,
guarding against stale responses by body_id. Serverless/test mode is a no-op
(overlays stay empty). Connected in _ready, torn down in _exit_tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Render the proxied Layer1Output (D-225) as toggleable Atlas overlays. The
viewer stores the decoded Layer1Output via set_generation_layer1() and the
marker overlay draws three new layers gated by their visibility flags:
RVR river_cells as dots, confluences as small circles, mouths as
double-ring sea-terminus markers
BAS drainage-basin boundaries as thin closed polylines + faint fill
ATR geographic attractors — shape by attractor_type (Araminta's 7-shape
vocabulary), color by sub_biome, size by strength; <0.15 culled
Layer-1 positions are [row, col] in the 512x256 working grid, which matches
the viewer's grid_to_canvas transform, so they project directly onto the
heightmap. OVERLAY_DEFS gains gen_l1_rivers/basins/attractors (toggle group)
so AtlasOverlayBar auto-exposes them as toggle buttons.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- heightmap.rs: read sea_level from the PNG tEXt chunk (bake writes it),
default-fallback param; new test reads_sea_level_from_text_chunk.
- client atlas_viewer.gd: load reliefmap.png (color display) instead of
heightmap.png (now 16-bit grayscale elevation, cascade-only).
- drop atlas_body_heightmaps: removed from systems-schema.sql; DROP TABLE in
import_economics MIGRATION_SQL (the PNG is the store now).
- D-202 amendment: implementation-status note (consumer + producer done),
resolving the review's 'reads done but producer pending' point.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Heightmap and markers load from wiki filesystem path via FileAccess
instead of res:// (wiki is outside the Godot project root). Overlay
bar buttons now receive mouse events — viewer skips input processing
when cursor is over UI elements. Header gets explicit minimum width
to prevent vertical text stacking. Shadow economy overlay removed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Body info panel with globe artwork renders on the right side of the
system map instead of navigating to a separate screen. Single-click
shows/swaps panel, double-click opens regional heightmap view, ESC
closes panel. Station panel also moved to right side for consistency.
Moon and station positioning clears parent body radius + label space.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Half-sun on left edge, planets left-to-right with radius-based sizing
(Earth = 24px reference, unclamped — gas giants fill the view). Globe
textures loaded from wiki at runtime. Double-click on reach map opens
system view. M closes atlas from any screen, ESC navigates back.
Fixes Godot 4.6 type inference parse errors in main.gd (var zoom/speed).
Uses physical_keycode throughout for layout-independent key handling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Export pre-computed drainage basin boundaries from atlas_province_boundaries
into markers.json during generate_atlas. AtlasMarkerOverlay draws province
polylines with semi-transparent fill under the political_zones overlay.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Protocol.gd is an autoload — replace Messagepack class_name refs with
inline load() calls via a static helper. main_menu.gd extends MetaScreen
by class_name which fails at parse time; switch to path-based extends.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix autoload parse-order violations: sim_bridge.gd, input_mapper.gd,
audio_manager.gd now use load() for class_name types instead of
direct references (LocalBridge, ServerProcess, Constants)
- Collapse redundant tween validity guard in dialogue_box.gd to
is_instance_valid(panel) only
- Add clarifying comments to fog test resize assertions (8-tile
padding trigger, 32x32 fixture assumption)
- Fix test_examine_display_sprint18 case 2: GameState.has() →
"field" in GameState (Node vs Dictionary API)
- Fix test_game_state_sprint20: rename before_each → before_test
(GdUnit4 lifecycle hook)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add free camera mode (F4 toggle): WASD pan, scroll zoom, decoupled
from player position (#898)
- Strip archetype-driven code: remove character_archetype, lattice_profile,
and lattice color palettes from client (#882)
- Fix confrontation_monologue signal not firing in headless test mode (#867)
- Revive fog state behavioral tests: EXP_EXPLORED persistence, grow-only
bounds, texture-resize copy, BoundaryWall handling (#879)
- Triage pre-existing test failures: fix examine_display dismiss timing,
fog test position fragility, rendering snapshot assertions,
time_display format (#871)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- character_creation.gd:1614: type-annotate dir_name to fix GDScript
inference parse error. CARDINAL_DIRS is an untyped const Array, so
`var dir_name := CARDINAL_DIRS[idx]` failed type inference and blocked
test_character_creation_sprint28.gd test discovery. Runtime was lenient
but gdUnit4's scanner is strict.
- test_merge_path_flows_sprint37.gd (_load_char_create): call add_child
before _update_start_btn_state so @onready vars (_footer_start) are
bound when the helper dereferences them. Unblocks 2 merge-path tests.
- test_anti_tedium.gd (test_bug_report_sends_unpause_on_close): call
dialog.close() instead of the old dialog._close() rename casualty.
`on_close()` is a lifecycle hook — it doesn't actually transition
state; `close()` is what MetaScreen exposes.
- test_anti_tedium.gd (before_test): clear MetaStack._stack. Prior tests
leave stale freed dialog refs on the stack; _any_pausing() iterates
the stack during close() and crashes with "previously freed".
Net: sprint-37 test files (test_merge_path_flows_sprint37.gd,
test_anti_tedium.gd) now pass. Overall suite: 2428/2488 passing
(60 remaining failures are pre-existing, unrelated to sprint 37).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes the version-mismatch guard from Protocol.decode_snapshot() and the
PROTOCOL_VERSION constant from the client (server side done in #874).
Core changes:
- protocol.gd: remove const PROTOCOL_VERSION, remove version mismatch guard,
remove "version" from return dict, add gauntlet_mode/room_id decode
- sim_bridge.gd: remove handshake version check; relax handshake guard to
require only a valid Dictionary (server no longer sends protocol_version);
emit handshake_complete(0) for API compat
- loading_screen.gd: drop "· protocol N" suffix from version label
- test_harness.gd: replace Protocol.PROTOCOL_VERSION with literal 23
Test updates (21 files): replace "version": Protocol.PROTOCOL_VERSION with
"version": 23 in all snapshot bytes dicts; remove snapshot.version == N
assertions; remove version-rejection tests (test_rejects_version_6,
test_decode_snapshot_rejects_missing_version, test_decode_snapshot_rejects_old_version,
test_protocol_rejects_version_mismatch, test_sim_bridge_test_snapshot_uses_current_protocol_version).
Also includes: #872 bookmark_catalog carry-forward regression test, and
#873 merge-path flow tests (test_merge_path_flows_sprint37.gd).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses Tyre's 9 architecture items from the sprint-36 client review.
- decisions/architecture.md (Tyre #1): D-192 now says "deprecate; removal
tracked in #868" instead of "remove". The branch does not remove the
version field or guard — that belongs in the coordinated server+client
PR. The decision text now matches the code on this branch.
- meta_stack.gd (#2): handle_escape() on a screen with
closable_by_escape=false now consumes the event unconditionally. Was
returning whatever on_escape() returned, which default-returned false
and leaked ESC into main.gd's implant/settings chain — opening the
settings dialog behind the loading screen.
- debug_console.gd (#4): drop the direct KEY_ESCAPE branch in
_unhandled_input. ESC now falls through to main.gd → MetaStack, which
finds the console on top of the stack and closes it via the normal
path. Other keys are still consumed so movement/action can't leak.
- main.gd (#6, #10): extract the ESC priority chain into
_handle_menu_key() so "MetaStack → implant → settings" is a named
thing. Add a comment near connect_to_sim explaining that
GameState.bookmark_catalog survives the Option A scene transition via
the autoload.
- main_menu.gd (#7): header comment documenting the double LoadingScreen
lifecycle — safe today because main_menu.tscn and main.tscn never
co-exist, noted for future promotion to autoload if that changes.
- meta_screen.gd (#8): apply captures_input symmetrically in open()/
close() — was set in open() only, so a screen changing the flag
between open+close kept the opened value forever.
- meta_screen.gd (#9): on_escape() docstring clarifies the tri-state
(consume-and-hold / consume-and-close / ignore) — and that
closable_by_escape=false is the screen-wide way to say
"consume-and-hold".
- bug_report_dialog.gd (#11): capture_cancelled now emits from
on_close() (covers any close path — ESC, MetaStack pop, programmatic
close) rather than only on_escape(). A new _completed flag
distinguishes completion from cancel so the two signals stay
mutually exclusive.
Addresses Hoshe's 3 code-quality items from the sprint-36 client review.
- character_creation: drop CARDINAL_NAMES (was [south, east, north,
west]) and use CARDINAL_DIRS ([south, west, north, east]) for both
facing and screenshot filename label. The two arrays indexed by the
same _screenshot_cardinal_idx produced swapped labels at indices 1
and 3 — screenshots at those positions had filenames that did not
match the character's actual facing.
- character_creation: Enter/KP_ENTER now honors _footer_start.disabled.
Without a bookmark selected the Start button disables, but the
keyboard path called _on_start() unconditionally — a player could
confirm creation with empty bookmark/location strings. Guard at the
top of _on_start.
- protocol.gd: raw_bm.get("career", "tycoon") hardcoded a content
default in the wire decoder — a missing server field silently became
"tycoon". Empty string is the correct protocol default;
_make_bookmark_card already skips the career label when empty.
- loading_screen: opaque BG (was 0.75 alpha) + mouse_filter STOP so the
loading state genuinely occludes the underlying screen
- main_menu: poll SimBridge.poll_snapshot in _process while waiting for
the bookmark catalog so the new-game flow doesn't stall on the
catalog round-trip introduced in Workstream 3 (#680)
Replaces the W5 placeholder inside the Skills tab with a properly
framed stub per Araminta's spec. Final workstream of the #618 + #680
+ MetaScreen implementation.
Content:
- MarginContainer (8 px sides, 4 px top — matches existing tab
padding)
- Single centered Label: "Skills allocation — coming soon."
- DIM_TEXT color, font_body size (11 px), horizontally and vertically
centered inside the tab content area
No inputs, no interactivity — real skill allocation lands in a future
sprint when the skills system exists server-side. Players selecting
a bookmark still proceed to Start regardless of what they see on this
tab.
Closes the implementation half of #618 (CK3-style character creation
screen) and #680 (location picker) — Hoshe's revised test plans for
MetaScreen pattern, #618, and #680 can now run end-to-end.
Verification:
- gdlint clean
- Headless parse + widened scanner check (per hardened pr-push skill)
clean; pre-existing autoload class_name noise filtered.
- test_character_creation_sprint28 88/88, test_protocol 62/62,
test_client_p3 24/24, test_ui_framework_sprint15 54/54
Next: Hoshe runs her full revised test gauntlet against the shipped
shape; if green, the PR pushes via /pr-push.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
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>
Consolidates the character creation TabContainer from 8 flat tabs
(Body / Head / Hair / Clothing / Accessories / Debug plus the two
being-added Skills / Bookmark) into 4 top-level tabs per Araminta's
revised spec: Bookmark, Appearance, Skills, Debug.
The existing five appearance sub-tabs (Body, Head, Hair, Clothing,
Accessories) now live inside the Appearance tab as a horizontal
segmented sub-navigation using the existing `_make_slot_btn()` pattern
— consistent with the Clothing/Accessories slot row vocabulary.
Selected sub-section uses existing ITEM_SELECTED_BG / BORDER styling.
Structural changes:
- New APPEARANCE_SUB_NAMES const lists the five sub-sections.
- Renamed _tab_search → _appearance_search, _tab_grids →
_appearance_grids. Scope changed from "top-level tabs" to
"Appearance sub-sections" but index 0..4 semantics preserved.
- Added _appearance_active_idx, _appearance_sub_btns,
_appearance_sub_sections state.
- _ready() builds exactly 4 top-level tabs; tab builders invoked
explicitly per index.
- New _build_bookmark_tab / _build_skills_tab render TEXT_DIM
placeholder labels ("Bookmark content lands in Workstream 6", etc.)
— actual content in W6/W8.
- _build_appearance_tab constructs the sub-nav strip and stacks all
5 sub-sections up front with visibility-toggle swap
(_on_appearance_sub_selected). Comment explains the up-front build
choice and the free-and-rebuild fallback if performance regresses.
- Existing _build_body_tab / _head / _hair / _clothing / _accessories /
_debug remain unchanged — they now receive Appearance sub-section
Controls as their tab argument instead of top-level tabs. _make_tab_vbox
anchors full-rect in both parent contexts, so layout is preserved.
Verification:
- gdlint clean
- godot --headless --path client --quit — no SCRIPT ERROR
- test_protocol 62/62, test_client_p3 24/24, test_ui_framework_sprint15
54/54, test_implant_nav_stack 52/52, test_implant_registry 42/42,
test_implant_app_lifecycle 36/36
Workstream 6 (Bookmark tab content: card list + detail view + location
picker per Araminta's spec) lands next. W7 (location picker as a
sub-component of Bookmark tab) follows. W8 (Skills stub content) is
last. Hoshe's parallel Task #21 (test hygiene triage) commits
separately.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
main_menu now connects SimBridge before character_creation opens, gating
the transition on first ObserverSnapshot carrying a bookmark_catalog.
Loading screen is shown during the connect; on cancel the SimBridge
subprocess is torn down and the player returns to main_menu. Catalog is
read straight from GameState.bookmark_catalog in W6.
Flow (Option A):
1. New Game → SessionManager.new_game() creates save dir
2. main_menu pushes loading_screen via MetaStack with "Connecting to
simulation..." message
3. SimBridge.connect_to_sim() spawned; main_menu listens on
connection_state_changed, then on snapshot_received for the catalog
4. On catalog arrival: loading_screen closed, scene-transition to
character_creation
5. character_creation Cancel → SimBridge.disconnect_from_sim() + scene
transition back to main_menu (Tyre's recommendation: clean state per
session over warm-start savings)
6. character_creation Start → ConfirmBookmark sent (stubbed for W4 with
first catalog entry; real bookmark + location from W6's UI)
ESC priority chain in main.gd OPEN_MENU handler:
- MetaStack.handle_escape() first — closes the topmost meta overlay
- HudGroups.is_implant_active() / close_app() — closes active implant
- Fallback: toggle settings_dialog (existing W2 behavior)
Files:
- sim_bridge.gd: send_named_action(action_name, action_data) helper.
Bridges named tag-enum PlayerActions (RequestBookmarkCatalog,
ConfirmBookmark) into the existing outbound buffer, parallel to
send_input's InputMapper.Action handling.
- loading_screen.gd: set_message(text) for the connecting/loading label.
- main_menu.gd: full Option A flow rewrite. Tracks _waiting_for_catalog
so re-clicking New Game during connect is a no-op.
- character_creation.gd: _on_back disconnect path + _on_start
ConfirmBookmark stub. MAIN_MENU_SCENE / GAME_SCENE constants.
- main.gd: connect_to_sim guard (don't reconnect when Option A leaves
it CONNECTED). ESC chain wiring.
Verification:
- gdlint clean
- godot --headless --path client --quit — no SCRIPT ERROR
- test_protocol 62/62, test_implant_nav_stack 52/52, test_client_p3
24/24, test_ui_framework_sprint15 54/54
Pre-existing failing suites unchanged: test_sprint2_proof,
test_dialogue_sprint18, test_client_p2 (camera-smoothing assertions
that pre-date W4 — main.gd has disabled position_smoothing_enabled
since #117 / #501 / #117 manual-lerp; tests were stale).
Workstream 5 (3-tab restructure of character_creation: Bookmark /
Appearance with sub-nav / Skills / Debug) lands next.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Relocates main_menu, character_creation, settings_dialog, debug_console,
bug_report_dialog, loading_screen from flat client/ui/ into structured
client/ui/meta/screens/<name>/. All six now extend MetaScreen instead
of Control; the base handles open/close lifecycle, visibility,
captures_input, and — for overlays — the sim-pause contract.
Screen policies set per Tyre's proposal:
- settings_dialog: pauses_sim=false, PUSHES onto MetaStack
- debug_console: pauses_sim=true, PUSHES (D-088 routing via base)
- bug_report_dialog: pauses_sim=true, PUSHES
- loading_screen: closable_by_escape=false, PUSHES
- main_menu, character_creation: scene-roots, extend MetaScreen for
the lifecycle contract only, do NOT push onto the stack
character_creation stays at its current surface (tabs, descriptor,
creation_confirmed signal unchanged). Tab consolidation and
CharacterProfile migration happen in Workstreams 5 and 6.
Knock-on changes:
- main.tscn ModalLayer CanvasLayer renamed to MetaLayer; main.gd
@onready refs updated; constants.gd comment updated; test_client_p3
and test_ui_framework_sprint15 assertions updated; test_monologue_display
and .tscn header comments updated.
- OPEN_MENU handler now pushes settings_dialog onto MetaStack before
calling open(). Full ESC priority chain lands in Workstream 4.
- atlas_app.gd: _unhandled_key_input signature widened from
InputEventKey to InputEvent with an is-check, per Godot 4 API. Pre-
existing narrowing was silently tolerated until main.tscn started
fully instantiating under the new pattern.
- test_client_p3: entity_renderer type annotations corrected from
ColorRect to Sprite2D (stale since a prior refactor); facing
indicator rotation assertion switched to angle_difference() for
modular-safe comparison.
Verification:
- gdlint client/scripts/ client/ui/ — zero problems
- godot --headless --path client --quit — no SCRIPT ERROR
- test_client_p3: 24/24 pass
- test_ui_framework_sprint15: 54/54 pass
- test_implant_nav_stack: 52/52 pass
- test_implant_registry: 42/42 pass
- test_implant_app_lifecycle: 36/36 pass
Workstream 1 foundation (84105916) remains unchanged. Workstreams 3-8
follow: protocol layer, Option A sequencing, 3-tab restructure,
Bookmark tab, location picker, Skills stub.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Establishes the faux-game-menu base pattern for non-diegetic UI,
analogous to ImplantApp but for pre-gameplay and meta-overlay
screens (main menu, character creation, settings, debug console,
bug report, loading screen). Workstream 1 of the MetaScreen
refactor — foundation only, no screen migrations yet.
- client/ui/meta/meta_screen.gd: base class (Control) with
HIDDEN/OPENING/OPEN/CLOSING phase tracking, three orthogonal
policy booleans (pauses_sim, closable_by_escape, captures_input),
open/close lifecycle, on_escape contract, closed + escape_pressed
signals, subclass hooks (on_open, on_close).
- client/ui/meta/meta_stack.gd: autoload coordinator. Overlay stack
with push/pop/top/is_active; handle_escape chain; sim-pause
coordination via SimBridge when pauses_sim=true; meta_active_changed
signal. All class references kept inside method bodies — no
top-level class_name refs, matching HudGroups / GameState
autoload parse-order discipline.
- client/scripts/character_profile.gd: Resource wrapping the
visual descriptor with bookmark_id and start_location_id.
Target of the creation_confirmed signal once the character
creation flow migrates.
- client/project.godot: MetaStack registered as autoload after
HudGroups, before ImplantRegistry.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents the ImplantApp lifecycle ordering and the nav-stack state
guarantee at each hook:
- Class-level docstring on implant_app.gd describes on_install,
on_open, on_close, and on_insert_deactivated: when each fires,
what nav state subclasses can rely on, and what is safe to do
(construct + register_screen in on_install; data refresh + read
nav.current() in on_open; pause timers in on_close; no close_app
manual call in on_insert_deactivated — call super or replicate
the guard).
- Arch doc gains a "Lifecycle hooks" subsection under ImplantApp
base class with a four-row contract table plus explanatory notes
on two load-bearing invariants: why on_install sees an empty
stack (bottom-up _ready order, no open signal yet); why on_close
must not push/pop (would destroy preserved position on reopen).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three architectural review comments implemented per Tyre's proposals:
#3 — scene_path consumption (full, Option A)
- ImplantRegistry.instantiate_all(parent) loads and instantiates all
registered apps with a declared scene_path. Manifests with empty
scene_path are treated as metadata-only (silently skipped).
- ImplantRegistry.get_app_instance(app_path) returns the live instance.
- hud.tscn no longer direct-instances AtlasApp or EconomicsApp — an
AppsContainer Control holds the registry-managed children.
- hud.gd._ready() calls ImplantRegistry.instantiate_all($AppsContainer).
- main.gd drops @onready vars for atlas_app/economics_app; looks up
both from the registry at the top of _ready().
#6c — schema_version on manifest
- ImplantAppManifest: @export var schema_version: int = 1 (first field).
- ImplantRegistry: const CURRENT_SCHEMA_VERSION := 1; tiered check in
_scan() — older-than-current emits print_verbose, newer-than-current
emits push_warning, both proceed best-effort.
- apps/atlas/app.tres + apps/economics/app.tres: schema_version = 1.
#10 — absorb _on_screen_changed boilerplate into ImplantApp
- Base class gains _screens: Dictionary, _current_screen_id: String,
register_screen(id, screen), current_screen_id(), and a real default
_on_screen_changed implementation that handles leave+hide+enter+show
with has_method guards and same-screen-replace detection.
- atlas_app: deletes _current_screen_id, _get_screen(),
_on_screen_changed() override; on_install() collapses to construct →
setup → wire → register_screen(id, screen) per screen. Preserves
direct screen refs for atlas-specific signal wiring and method calls.
- economics_app: deletes same scaffolding; on_install() reduces to three
lines (construct overview_screen, register_screen, nav.set_default).
Also: replaced Resource.get(name, default) dict-style calls with direct
property access on typed ImplantAppManifest reads (2-arg get() is
Dictionary-only; causes "Too many arguments" parse errors on Resources).
_is_valid_manifest gained a Resource type guard and a property-exists
check.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Seven nit-level fixes from PR review:
- Remove dead signal insert_deactivated from ImplantApp; the hook method
on_insert_deactivated() is the actual contract.
- Rewrite _on_atlas_economics_link comment in main.gd to reflect the
actual flow (AtlasApp closes as a consequence of HudGroups single-
active-app, not before emitting anything).
- Document the "pop never empties below default" invariant on
ImplantNavStack.pop() with a pointer to reset_to_default.
- Add _mutating re-entrancy guard on ImplantNavStack mutation methods.
push_error + early return if called during a screen_changed emission.
- main.gd registry loop now uses typed ImplantAppManifest property
access (manifest.app_path, manifest.default_key) instead of
dictionary-style .get() calls. Empty app_path triggers push_warning.
- Document the economics [/] hotkey exception in main.gd and reference
the planned handle_global_key lifecycle hook. Arch doc Follow-up
section gains a bullet for the new hook.
- Comment the independent-version-read rationale above client_ver and
proto_ver in loading_screen.gd.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Addresses 6 mechanical issues from PR review:
- ImplantAppManifest: drop dead display_name and icon_path fields.
default_key carries a TODO noting its future migration to a keybinds
manifest (input concern in app manifest is a layering violation,
tracked explicitly).
- default_mode wire format is now a String ("fullscreen" / "insert") for
mod-author discovery. ImplantRegistry parses via _MODE_MAP, caches the
resolved int in _resolved_modes, and exposes get_resolved_mode(app_path).
main.gd reads the resolved int directly instead of re-parsing.
- Extract client/ui/implant/widgets/system_index.gd (class_name SystemIndex,
static get_sorted_systems). Removes duplicated star_map_data.json loader +
sort lambda from atlas_app and economics overview_screen.
- ImplantApp.on_insert_deactivated() default auto-closes only when the app
is active in INSERT mode. FULLSCREEN apps no longer spuriously close on
insert state changes.
- tooling/db/sqlite-query and sqlite-exec: --help output goes to stderr
(exit 0). Keeps stdout reserved for JSON payloads so JSON-parsing
callers can't get silently corrupted.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Addresses blocker comments from PR review:
- Delete star_map.gd and star_map.tscn — dead implant/map/starchart
HudGroups registration that should have landed with the atlas
unification (D-191 criterion 1)
- Remove test_star_map_is_accessible_from_insert_ui and
test_star_map_scene_exists from test_sprint30.gd — D-191 supersedes
the insert-UI accessibility pattern
- ImplantRegistry._scan() now detects default_key collisions
(first-wins with push_warning) and validates default_mode against
HudGroups.Mode enum (skip + warn on invalid)
star_map_data.json remains — still used by atlas_app and
economics_app for system index lookups.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Loading screen reads the client version from project.yaml (root version
field) and displays it alongside Protocol.PROTOCOL_VERSION at the bottom
of the overlay: "v0.1.35 · protocol 21".
Falls back to "?.?.?" if project.yaml is missing or unreadable (e.g.
when run from an exported pck where the relative path is unavailable).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Per D-191, the atlas is the star map extended downward — not a separate
app. AtlasPanel now owns the full Reach → system → planet → heightmap
zoom hierarchy as a single HudGroups app (implant/map).
- Add REACH_MAP as Level 0 of AtlasPanel's zoom hierarchy; renumber the
enum so higher index = deeper zoom
- Port hop-ring rendering (pan/zoom, system markers, hover/info, sector
layout) from star_map.gd into AtlasPanel methods
- Change AtlasPanel.APP_PATH from "implant/map/atlas" to "implant/map"
- main.gd: KEY_M toggles unified atlas; KEY_A binding removed
- Symmetric nav: ORBITAL_DIAGRAM back goes to REACH_MAP (not
SYSTEM_PICKER), matching the forward skip
- hud_groups.gd docstring documents the unified path and flags the
legacy starchart path as kept-for-compat (retirement tracked in #852)
star_map.gd's HudGroups registration stays live but inert — no key
binding reaches it. Full retirement follows in #852 after a sprint of
soak on the unified panel.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>