During an RMB path-follow the body yaw locks to the leg direction (rig
commit_body_to_motion, from interpolated velocity — immune to SetFacing
interleaving between throttled steps). The mouse instead drives a layered
look-at: LookAtModifier3D pair on Head (±70°) + spine_02 (±30° torso twist
for looking far lateral/behind — past their sum the character physically
cannot look further without turning). Forward axis measured from the
armature rest pose (+Z), not guessed. Influence fades in/out on follow
start/end. Pure client presentation per D-249; SetFacing still rides the
wire, so the server vision cone follows the mouse while walking — the
character looks where the player points.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live-session findings, three in one: (1) Gauntlet walls are 2 tiles thick —
the outer row's camera-far neighbor is the inner WALL, so its flag stayed 0
and it stood full-height behind the collapsed inner stub; the flag now scans
up to 3 tiles through consecutive KNOWN walls (unknown stops the scan — no
assumptions past the info boundary), and any newly-learned tile refreshes the
wall chain behind it. (2) Corner pillars at the screen-bottom junction never
collapsed — their interior floor sits DIAGONALLY behind, which no straight
scan line reaches; the combined diagonal joins the scan set. (3) Per-fragment
smoothstep falloff carved organic notches (the 'sphere' read) — the cut now
computes per-instance in the vertex shader from the box's tile center and
quantizes at 0.5: every wall box is either full or stub, X2-crisp.
Verified via locomotion_cutaway autopilot capture: south runs read as clean
stub rows, corners collapse, camera-far walls stand.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hover marker + optimal path line over the KNOWN tile store only — the
character plans through what they know; fog is unpathable (info boundary at
the planning layer; the follower additionally revalidates every remaining
tile per step). Pure static 8-dir A*: uniform cost 1 incl. diagonals (D-248
time-optimal, no sqrt2), no corner-cutting, terrain-cost provider seam for
Phase-4 terrain. Execution streams ordinary Move* steps through the existing
throttle — zero protocol change, server validates every step.
RMB vocabulary (live-session spec): click = walk there at current stance;
double-click = sprint there (ToggleStanceUp burst — server toggle handler
verified cooldown-free so bursts climb deterministically — with net-zero
restore on arrival; a double upgrades the active follow in place);
long-press >=400ms = go there then Crouch on arrival, no restore (input-
vocabulary prototype; real cover mechanics are future combat design).
Cancellation: WASD override (stance kept), invalidation, teleport,
suppression. Two additive InputMapper seams (queue_move_step,
queue_stance_toggle); mouse unproject shared with the facing provider
(ground_hit_local). 44 new gdUnit tests across finder + follower ladders.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Known interiors open at a glance (bottom walls low) AND the sightline
corridor punches through unexplored exterior walls; C still cycles modes.
sandbox_constants.gd also carries the path-preview/gesture knobs for the
click-to-move feature landing in the next commit (single tuning file).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live-session two-minds resolution: the trippy sightline corridor stays as a
mode (candidate signature look); mode 1 drops any wall with a KNOWN non-wall
tile on its camera-far side to the stub (X2 bottom-walls read — the player
sees the room without walking to it); mode 2 combines. Bottom-wall flag rides
MultiMesh INSTANCE_CUSTOM.r, updated on paint and when newly-learned floors
promote camera-ward walls; camera-far directions derived from the live
WorldRoot basis, never hand-derived. Default stays corridor pending the
feel verdict.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live-session finding: the half-disc cut bit nearby side walls while leaving
the wall actually occluding the character at full height, with dome-contour
scallops across the two-row walls. Rewritten as a sightline corridor: walls
within CUT_CORRIDOR_HALF_W of the character's x, camera-side, out to a
pitch-derived reach (WALL_H / tan(pitch) — how far a wall can still occlude
in ortho; ~4.3 m at -30, breathes with the T-cycle tilt) drop to the stub.
FollowCamera3D exposes pitch_deg(); reach clamped 0.5-16 m.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New SR_LIVE sandbox scene: CharacterVisual composited in a 3D greybox world
derived from server snapshots. Per-leg constant-velocity interpolation keyed
to the stance throttle, 'server feet / client eyes' facing (wire octant while
moving, client aim octant idle), cadence-synced gait state machine on
AnimationPlayer custom blends, D-148 orthographic follow camera (-30deg
default, T-cycle presets), sim-space grid shader, camera-side wall cutaway,
accumulating never-evict tile store with four-state visibility tint.
Additive seams only: InputMapper.facing_angle_provider (2D path unchanged),
CharacterVisual.play_animation blend_time param + get_animation_player().
Visual harness gains per-scenario scene field + SR_AUTOPILOT input scripting.
210 new gdUnit assertions across five suites; verified live (230/230 total,
clean smoke, screenshot at .cache/screenshots/locomotion_idle_live.png).
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>
Formatting-only cleanup of the #969/#960-A codec + bridge scripts to match
gdformat output (the pre-push gdformat check is advisory; these landed
un-formatted). No behavior change — 70/70 protocol tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Client transport half of the layer-stream protocol.
- SimBridge.request_atlas_layers(body_id) sends an AtlasLayerRequest frame
(live mode only; no-op in test mode); responses arrive via a new
atlas_layers_received signal.
- receive_bytes now decodes each frame ONCE via Protocol.decode_inbound and
branches by shape (snapshot vs atlas response) — avoids double-decoding the
20 Hz snapshot path. decode_snapshot is split into decode_raw +
_decode_snapshot_from_raw (public decode_snapshot unchanged, so the 70 protocol
tests stay the regression guard); decode_inbound returns {kind, value}.
70/70 protocol tests pass, including the new decode_inbound classifier test.
(Pre-existing client-suite failures in server-dependent e2e/roundtrip + unrelated
audio/fog/dialogue suites are unchanged — verified identical at baseline.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Client half of the layer-stream protocol (codec only; transport wiring next).
- protocol.gd: encode_atlas_layer_request (bare {body_id, up_to} map so the
server demux routes it to the proxy, not the PlayerInput array) and
decode_atlas_layer_response (-> {body_id, status, error, layer1}; returns null
for non-atlas frames, e.g. a snapshot, so receive_bytes can disambiguate).
- gen_fixtures.rs: cross-language fixtures (atlas_response_ready/pending/
not_found) from real rmp_serde output, matching the test_protocol.gd pattern.
- test_protocol.gd: 5 tests decode the fixtures + verify a snapshot is not
mistaken for a response + the request encodes to the right shape. 68/68 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <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>
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>
- Replace raw KEY_F4 check with Input.is_action_just_pressed("free_camera") to
consume the registered project action (matches F3/F12 dev toggle pattern)
- Reset camera.zoom to Vector2.ONE when toggling free camera off so zoom does
not bleed into normal gameplay
- Add free_camera_mode guard to InputMapper._unhandled_input() so discrete
actions (INTERACT, stance, pause) are suppressed alongside movement
- Add client/tests/test_free_camera.gd: flag default, movement suppression,
discrete action suppression, zoom constant contracts
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>
Code changes addressing PR #135 review (Tyre + Hoshe):
- **T3 (blocking):** test_merge_path_flows_sprint37.gd `_load_main_menu`
and `_load_char_create` now assert the scene loaded instead of silently
returning. Missing .tscn → red test, not falsely green.
- **T1:** sim_bridge.gd signal `handshake_complete(protocol_version: int)`
was D-192 residue with no listeners. Drop the int parameter entirely
and the literal-0 emit.
- **H4:** test_new_game_catalog_snapshot_resolves_loading_state now
asserts SimBridge.state == CONNECTED terminus, not just the loading
flag — guarantees full flow completion, not merely flag-clear.
- **H5:** test_protocol_bridge.gd file-level comment refreshed; drops
reference to removed protocol-version check tests.
- **H6:** test_p0_regressions.gd `_make_snapshot_bytes` comment refreshed
and version field removed from fixture dict (D-192: not required).
- **H7:** test_merge_path_flows_sprint37.gd `_make_catalog_snapshot`
drops version field from fixture dict (D-192).
Follow-up tickets filed for reviewer suggestions:
- **T2:** #889 — revive EntityRenderer sprite constants coverage
(D-044 ENTITY_WIDTH/HEIGHT, asserted by deleted test_sprite_integration).
- **T4:** #890 — UI timeout fallback for bookmark catalog wait in
main_menu (systemic 'catalog never arrives' class beyond #872's
TCP-batch race).
- **T5/T6:** #891 — scene-flow test tier docs + test-only reset
helpers (SimBridge.reset_for_test, MetaStack.reset_for_test) +
minimal public API on scenes so UI refactors don't break all four
flow tests simultaneously.
Verification:
- `make lint-client` — no script errors
- `gdlint client/scripts/ client/ui/` — no problems
- `make test-client` — 2428/2488 passing. 60 remaining failures are
pre-existing, unrelated to sprint 37 (test_dialogue_sprint20 #558
signals, test_input_roundtrip integration-sans-server, etc.).
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.
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>
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>
Adds client-side wire support for the bookmark catalog (#614) and the
two associated player actions. PROTOCOL_VERSION bumps from 21 to 23:
- v22 (server): RequestBookmarkCatalog + ConfirmBookmark player actions
- v23 (server): bookmark_catalog field on ObserverSnapshot
Decode:
- protocol.gd decode_snapshot extracts optional bookmark_catalog.
Defensive parse of BookmarkWire fields (id, title, subtitle, flavor,
default_location, allowed_locations, allowed_locations_cultures,
career, starting_capital_tractus). Missing or malformed → null.
- snapshot_handler.gd caches the catalog into GameState.bookmark_catalog
on each snapshot (server pushes on tick 0; re-fetchable via
RequestBookmarkCatalog).
- GameState gains bookmark_catalog: Array = [] (untyped per autoload
parse-order discipline; default empty so callers can iterate without
null checks).
Encode:
- encode_request_bookmark_catalog() — unit variant, sent to trigger a
re-push if the cached catalog is missing.
- encode_confirm_bookmark(bookmark_id, starting_location_id) — struct
variant matching server rmp_serde shape. Called from character
creation on Start (lands in Workstream 6).
Tests:
- 5 new cases in test_protocol.gd: hand-built bookmark_catalog decode
(all 9 fields asserted), fixture-based decode round-trip, missing-
field null behavior, RequestBookmarkCatalog encode roundtrip,
ConfirmBookmark encode roundtrip.
- All 12 existing snapshot fixtures regenerated from server via
`cargo test --test gen_fixtures -- --ignored`. The new
snapshot_with_bookmark_catalog.msgpack fixture was generated by the
same pass.
Verification:
- gdlint clean
- godot --headless --path client --quit — no SCRIPT ERROR
- test_protocol 62/62, test_client_p3 24/24, test_implant_nav_stack
52/52, test_implant_registry 42/42, test_implant_app_lifecycle 36/36
Workstream 4 (Option A sequencing via loading_screen + SimBridge
connect) 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>
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>
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>
4. AtlasPanel and AtlasViewer now compose their title + hint from an
ImplantHeader child rather than hand-rolling them via draw_string, so the
D-169 "theme swap changes the implant hardware appearance" invariant
holds end-to-end. _refresh_screen_header() drives content per level and
on system navigation.
5. AtlasViewer exposes city_canvas_pos(), get_hovered_city(),
get_selected_city(), and get_overlay_defs() as public API — the marker
overlay no longer reaches into underscore-prefixed state, which is
especially important because viewer is an untyped var in the overlay.
6. KEY_N now consumes unconditionally while the viewer is visible, and
main.gd's global economics-monitor toggle is gated on
!HudGroups.is_app_active("implant/map/atlas"). Previously pressing N
without a selected city fell through and closed the fullscreen atlas as
a side effect.
7. OVERLAY_DEFS lives in AtlasViewer as the single source of truth.
AtlasOverlayBar reads the list via viewer.get_overlay_defs(), and
AtlasViewer derives _overlay_visibility / _overlay_locked from the same
table at _ready() — no more hand-maintained parallel lists, so the bar
and the guard in set_overlay_visible can't drift.
8. AtlasOverlayBar drops `class_name`: it now loads via
load("res://ui/implant/atlas_overlay_bar.gd") from AtlasViewer, the same
pattern AtlasPanel uses for AtlasViewer. _init(viewer_ref = null) keeps
the required-arg footgun off the editor's introspection path.
9. `star-map-data` make target added to regenerate
client/data/star_map_data.json from systems.db + wiki, and
`check-star-map` wired into pre-pr-validate + pre-pr-client so any
commit that touches the generator (or any downstream systems.db change
like server #839) fails pre-pr until the JSON is regenerated. The
terrain_reference data-availability dependency is no longer tribal
knowledge.
Also addresses review #15 (push_warning on unknown overlay id in
set_overlay_visible) and #16 (disabled always-on buttons drop handler
churn) as part of the same refactor.
Adds Level.HEIGHTMAP_VIEWER to AtlasPanel. The viewer loads a body's
terrain_reference heightmap PNG, pairs it with markers.json (roads, rail, POIs,
cities, rivers/oceans/mountains), and renders markers in texture-space via an
AtlasMarkerOverlay Node2D child of a transformed canvas — pan = offset,
zoom = scale.
Pan/zoom is cursor-centred (wheel zooms under the mouse, drag pans), with a
fit-to-view reset on R. Empty markers.json state renders a bare heightmap;
missing terrain_reference shows a themed "terrain data pending (#839)" notice
instead of crashing.
City data sidebar rebuilds from the selected city: name, pop tier, function,
currency zone, Commission presence, shadow zone, gate distance. Pressing N on
a selected city emits economics_link_requested(system_id) — main.gd bridges
this to EconomicsPanel.select_system() + HudGroups.open_app("implant/economics")
as an insert overlay, satisfying the D-191 Phase 2/3 cross-panel integration.
The overlay renders all nine D-191 overlay layers off of per-overlay visibility
flags in AtlasViewer. Overlay toggling for the regional view (#836) plugs into
set_overlay_visible(); the five always-on layers (terrain, infrastructure,
named features, gate markers, political zones) draw by default, the four
toggleable layers draw from placeholder data, and the two locked layers
(stockpile_weeks, production_vs_baseline) remain off until unlocked.
Per D-191 criteria 1, 4, 5.
3-level atlas navigation as an implant/map/atlas FULLSCREEN app (D-170 z=20):
system picker (◄ ► cycle, Enter opens orbital) → orbital diagram (star centre,
bodies grouped by orbit_index with moons sub-orbiting parents, stations as
markers, click-to-open body or mini station panel) → body entry (data sheet,
Enter stub for #835 heightmap viewer, Esc back). Composed from the ImplantPanel
component library (D-169).
Wires the panel into hud.tscn, adds an A-key toggle in main.gd, propagates
insert_state via snapshot_consumers. Extends generate-star-map-data.py to emit
per-system orbit_bodies + stations arrays (with currency_zone, atmosphere,
population, terrain_reference, etc.) from systems.db; star_map_data.json
regenerated deterministically.
Per D-191 §6, Phase 3.
CursorRenderer now toggles Input.MOUSE_MODE_VISIBLE when gameplay is
occluded, restores MOUSE_MODE_HIDDEN when gameplay resumes. Without
this, fullscreen apps (star map, future atlas) had no cursor at all —
the custom diegetic cursor hid correctly but the system cursor was
never restored.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
was_occluded was computed AFTER _active_mode and _active_app were
updated to the new values, so it always matched now_occluded on the
first toggle (both TRUE). The signal condition (was != now) never
triggered. Moved the check before the state mutation.
This bug affected every GameplayRenderer (world, entities, fog,
cursor) and the stance indicator — none of them hid on first
fullscreen app open.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Bump client protocol version 20 → 21 to match server (#822)
- Fix render_priority parameter name (was _render_priority, unused prefix)
- Fix debug console type inference (var sub := → var sub: String =)
- Economics panel: add population row, improve key hint text
- Stance indicator: hide on gameplay_occluded (D-170 fullscreen apps)
- Remove 5 broken clothing items from manifest and delete their GLBs
(boots_work, coveralls_basic, jacket_utility, pants_cargo, shirt_henley)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
E is claimed by InputMap "interact" action — InputMapper consumes
it before _unhandled_key_input. N is free, adjacent to M (star map),
reads as "Numbers" for the economics monitor.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Economics panel: replace dead _gui_input LEFT/RIGHT with public
navigate() method, wire [ ] keys in main.gd (avoids movement
key conflict, fixes focus_mode=NONE issue)
- Debug console: add explicit effect guard in econ inject no-commodity
branch so invalid effects don't fall to commodity-form error
- Snapshot consumer: null-clear GameState.economy_snapshot after
consuming (matches one-shot consumer invariant)
- Star map: remove duplicate doc comment above set_insert_active()
- Generate script: remove stale comment, dead _WORKTREE_PARENT var,
dead field extraction in parse_wiki_index, add try/except around
DB queries
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New implant panel at implant/economics: system selector, 6-commodity
price table with trend indicators, GDP strip. Composed from D-169
component library. Ring buffer caches last 20 ticks per system.
Snapshot routing wired through snapshot_handler → GameState →
snapshot_consumers → economics_panel. Placeholder prices shown
until server ships EconomySnapshot (#822).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
HUD: merge TimeDisplay into ImplantPanel (time/health/perception in one
panel), remove dead tscn nodes, move HUD to InsertOverlay for D-051 bloom.
Interaction prompt: convert from PanelContainer+StyleBoxFlat to Control
with ImplantPanel+ImplantDataRow, fix panel height for 42px minimum.
Tests updated for new scene structure and public API.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New GameplayRenderer extends Node2D with built-in occlusion support.
Subclasses override _gameplay_process() and _gameplay_draw() instead
of _process() and _draw() — these are skipped when a fullscreen
implant app covers gameplay.
Migrated all 5 gameplay renderers:
- CursorRenderer: hover detection + bracket drawing paused
- EntityRenderer: position lerping paused
- FogEntities: fog dot rendering paused
- SoundIndicatorRenderer: directional arrows paused
- WorldRenderer: tile/fog/entity updates paused
No more manual occlusion wiring — the base class handles everything.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CursorRenderer z_index=100 so it renders above all HUD/implant layers
- Adjacent systems uses ImplantTextBlock (wraps) instead of ImplantDataRow (clips)
- Panel calls reset_size() before clamping to get accurate height on first frame
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CursorRenderer must always render on top — shouldn't have its z-index
managed by HudGroups. It's a world-space renderer, not a HUD element.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Moved signals above enum per gdlint class-definitions-order rule.
app_changed signal uses int instead of Mode type to avoid the
forward-reference that forced wrong ordering. Callers compare
against HudGroups.Mode.FULLSCREEN etc unchanged.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaced visibility toggling with z-index layer management. Nothing
gets hidden — fullscreen apps render on top, gameplay stays underneath.
Supports future hybrid layouts (insert-mode map alongside gameplay).
Added gameplay_occluded signal: WorldRenderer skips tile/fog/entity
updates when a fullscreen implant app covers it. Prevents wasted
render work behind opaque overlays. Other renderers can connect to
the same signal.
Modes: GAMEPLAY (z=0), INSERT (z=10), FULLSCREEN (z=20), MODAL (z=30).
Star map uses app_changed signal to toggle its own visibility based
on whether its app is active.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
gdformat collapsed enum + 3 inline comments into one 300-char line.
Split to multi-line enum with doc comments.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>