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>
CursorRenderer extends Node2D, not Control — can't be registered in
HudGroups. Removed from gameplay group. Also dropped typed array
annotation to avoid runtime type mismatch errors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implant apps use hierarchical groups: implant/map, implant/wiki,
implant/journal, etc. Apps are mutually exclusive with each other
and with gameplay. Opening implant/map hides gameplay + any other
implant app. Closing returns to gameplay.
API: open_app(), close_app(), toggle_app(), is_app_active().
Star map uses implant/map. Future apps register their own group.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New HudGroups autoload manages show/hide of related HUD elements as
groups. Exclusive groups (gameplay, implant) are mutually exclusive —
opening the star map hides stance indicator, minimap, interaction
prompts, etc. Closing it restores them.
Gameplay nodes registered in main.gd _ready(). Star map registers as
implant group and uses toggle_group() instead of direct visibility.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add _unhandled_key_input to main.gd: M toggles star_map.toggle_visible()
- Remove topology line from info panel (internal data, not player-facing)
- Cast aperture_count to int (was showing decimal point)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Star map (W1-W3, S3):
- _process visibility guard + dirty flag (no redraw when hidden/unchanged)
- _system_hash masked to 31-bit positive range
- Extracted _find_nearest_system() shared helper
game_state.gd (W4):
- Inline load() in apply_snapshot() replaces per-tick overhead; safe at
runtime because script is already in resource cache
Data pipeline (W5-W6):
- Script-relative path resolution via __file__
- --check mode + make check-star-map staleness target
Minor (S1-S2, S5):
- Removed redundant bone_idx assignment
- Simplified double-negative test assertion
- Documented autoload parse-order convention in CLAUDE.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Autoload scripts are parsed before regular scripts, so class_name
types (CharacterVisualDescriptor, TestHarness, YamlParser) are not
available at parse time. Replace type annotations with untyped vars
and use load() for in-body class references. Fixes all 14 headless
parse errors (9 pre-existing + 5 from Sprint 30 changes).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New StarMapRenderer: 301 systems in concentric hop-rings from player
location, sector-colored, click-to-select with info panel, pan/zoom.
Integrated into HUD (hidden by default), insert state propagation
wired in main.gd. Data enriched from systems.db + star-map.json via
tooling/generate-star-map-data.py regeneration script.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
encode_startup_message() now accepts optional CharacterVisualDescriptor
as third param, serialized via to_dict(). sim_bridge passes the
descriptor from GameState on new game start. apply_snapshot() restores
descriptor from server snapshot on save/load cycle.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Infrastructure for floating UI elements (status indicators, thought
bubbles, alert markers). Marker3D at Vector3(0, 0.3, 0) offset from
Head bone, exposed via get_overhead_anchor() public API.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Major changes from runtime testing and iteration:
- Asset manifest (manifest.json) controls all available content —
replaces filesystem scanning, solves DirAccess/PCK export issue
- Dynamic tabs: only show tabs with manifest content
- Body segmentation: face-based exclusive assignment, hips split from
torso, torso_upper independent (not a variant)
- Eye color with iris-only mask from T_Eye_Split.png green channel
- Eyebrows tinted with hair color (from body segment, not separate GLB)
- Hair/clothing loaded as skinned meshes on shared skeleton
- Segment hiding disabled for clothing (solidify handles coverage)
- Gender-aware randomizer (no facial hair on female/teen)
- Clothing slots hidden when empty in manifest
- UI: color docks docked to bottom on all tabs, padding, flow layout
- Debug tab with per-segment visibility toggles
- Screenshot automation with test config JSON
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Load UAL animation library onto the skeleton and play idle animation
after character assembly. Adds play_animation() and stop_animation()
public API. Searches all animation libraries for matching animation
name with common idle variant fallbacks.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GDScript strict mode rejects type inference from Variant-returning
functions. JSON.parse_string() returns Variant — use explicit type
annotation to satisfy the parser.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Stig's review fix caught the first reference (line 9) but missed the
class docstring at line 27. Both now say 18 segments per D-160.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
13 fixes from PR #99 review on #705 (character editor):
- #10/#11: Add _get_swatch_color() helper; fix stale closure in
_make_color_swatch (reads current color at click time, not
construction time); fix _on_modal_ok to use helper instead of
wrong get_child(0) traversal
- #12: Replace HSV palette with D-165 hardcoded hex values (6×9
rows, muted HSL per spec — reds/greens/blues/purples/browns/grays)
- #13: Fix fallback asset IDs to match actual files — hair: bald/
bob/buzzed/long; heads: head_001–004; facial hair: beard/moustache/
mutton_chops (stubble removed — no asset)
- #14: Remove invented "slot field" TODO comment; replace with
name-prefix convention note
- #15: Fix overhead camera pitch 0.0°→-80.0° (0° was horizontal,
not top-down as intended per D-158)
- #16: Make hair highlight swatch non-interactive (_make_display_swatch);
remove _on_hair_highlight_changed and _hair_highlight_auto flag
- #17: Remove no-op self-assignment in _on_hair_selected; remove
redundant assignment inside _hair_highlight_auto block
- #18: Store _clothing_dock_container/_accessory_dock_container as
instance vars — eliminates fragile get_child(3).get_child(0) chain
- #19: Fix test_r_key_triggers_randomise — replaces assert_bool(true)
with a real assertion checking body_type or skin_tone changed
- #20: Cache scan results in _cached_hair_ids/_cached_head_ids during
tab build; _on_randomise() reads cache (no re-scan per keypress)
- #21: Fix character_visual.gd docstring "21 segments" → "18 segments"
- #22: Fix _get_accessory_ids_for_slot — apply slot name-prefix
filtering instead of returning all accessories for every slot
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Full character creation UI with 5-tab customisation panel (Body, Head,
Hair, Clothing, Accessories), live 3D compositor preview via SubViewport,
D-165 color picker modal, keyboard navigation, and game flow integration.
- CharacterCreation scene + script (~1440 lines) implementing wireframe spec
- SubViewport with Camera3D (frontal/dramatic/overhead presets per D-158)
- Cardinal rotation (Q/E) per D-155, randomise (R), Tab/Enter/Esc navigation
- Body type grid (11 types, D-159), skin tone dock (9 tones, shared state)
- Head template, hair, facial hair, eyebrow grids with DirAccess asset scanning
- Clothing slot selector with per-item Primary/Secondary/Accent tints
- Accessory slot selector with Primary/Secondary tints (Array[Color] descriptor)
- 54-swatch color picker modal (D-165) with hex input and recent colors
- Auto-derive flags for highlight, eyebrow tint, facial hair tint from hair primary
- Game flow: main_menu → archetype select → character creation → game start
- GameState.character_visual_descriptor field for gameplay consumption
- Updated accessory_tints from single Color to Array[Color] in descriptor + compositor
- 40+ GdUnit tests covering scene structure, signals, keyboard nav, color derivation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add CharacterVisualDescriptor (11-variant BodyType enum, wire format
encode/decode, body_type_key mapping) and CharacterVisual compositor
(runtime 3D character assembler with slot architecture, BoneAttachment3D,
clothing coverage, recolor mask loading, skin tone tinting, bone
validation). Includes 64 unit tests and Blender utility scripts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
W1/A2: add SimBridge.CONNECTED guard before send_input in toggle handler
W2/A1: assert PlatformInfo != null in HardwareDetector._ready();
add ordering comment in project.godot [autoload] section
W3: replace FileAccess.open() with get_file_as_string() in
read_benchmark_cache() — auto-closes, no leak
W4/A3: add _extract_bool_setting() helper in game_state.gd that handles
both {"Bool": true} and plain bool; push_warning on type mismatch
S5: sync _pre_battery_pref from GameState after load_ai_pref() in
HardwareDetector._ready() — closes race if system starts on battery
S6: separate "red" TPT status into its own arm with STATUS_RED dot and
label colour — three-colour mapping: green/yellow/red now distinct
A4: replace DebugConsole.PREFS_PATH compile-time dependency in
_save_ai_pref() with local SETTINGS_CFG_PATH constant
S7 (battery CI guard) already applied by Hoshe in test file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
config_dir, benchmark_cache_path, executable_path, model_dir were
populated in _init_paths() but missing from the diagnostics dict.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Platform abstraction:
- New PlatformInfo autoload (extends Node, registered first in project.godot).
Owns all OS queries: power state monitoring (30s poll), memory (on demand),
file path resolution (user_data_dir, config_dir, benchmark_cache_path,
install_dir, executable_path, cache_dir, model_dir).
- PowerProfile enum (FULL/BATTERY/POWER_SAVER), power_profile_changed signal.
Uses OS.callv("get_power_info") to defer resolution to runtime — avoids
compile errors on Godot 4.6 headless builds without power API.
HardwareDetector refactor:
- check_ram() now delegates to PlatformInfo.refresh_memory() + free_memory_mb.
All direct OS.get_memory_info() calls removed.
- classify_power_state(int) and should_suspend_inference(int) delegate to
PlatformInfo for single source of truth.
- check_power_state() → {power_state, classification, should_suspend}.
- Battery suspend/resume: on PowerProfile.BATTERY, stores pre-battery pref and
sends ChangeSettings(false) to server. On FULL restore, sends ChangeSettings
with saved pref if it was enabled. inference_suspended member tracks state.
Settings dialog:
- is_ai_inference_suspended() / set_ai_inference_suspended(bool) — testable API
per Hoshe's test contract. Reads HardwareDetector.inference_suspended on open.
- Toggle now disabled when battery-suspended OR hardware fails (not only fail).
- get_ai_dialogue_label_text() wired to UIStrings ("settings.ai_dialogue_toggle")
instead of hardcoded string.
Cleanup:
- Deleted ai_dialogue_detector.gd and .uid (orphaned duplicate, dead code).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements the full AI-Enhanced Dialogue feature for Sprint 26:
- HardwareDetector autoload (extends Node): three-layer detection —
Layer 1 RAM classification (pass/marginal/fail, thresholds 2GB/1.6GB),
Layer 2 TPT benchmark cache (green/yellow/red, thresholds 6/3 t/s),
Layer 3 degradation monitoring (>40% drop → yellow). load_ai_pref()
restores toggle state from user://settings.cfg on startup.
- Settings dialog AI section: toggle, colored status dot, status message
label, testable API (get_ai_dialogue_label_text, set_ai_dialogue_
hardware_status, is_ai_dialogue_toggle_enabled). Only RAM "fail" greys
out toggle — player always overrides yellow/red recommendations (D-138).
- GameState.ai_enhanced_dialogue_enabled (default true, opt-out model).
GameState.settings_response (v20 one-shot settings dump from server).
apply_snapshot() hydrates ai_enhanced_dialogue_enabled from full dump.
- Protocol v19→v20, settings_response decoding in decode_snapshot().
Protocol converted to extends Node autoload (enables test has_method).
encode_change_settings() helper for test inspection.
- InputMapper: CHANGE_SETTINGS, REQUEST_ALL_SETTINGS, DELETE_SETTING.
SimBridge: wire mappings for all three. RequestAllSettings queued after
handshake to hydrate client state from server SQLite on connect.
settings_response carry-forward in receive_bytes().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- protocol.gd: replace capitalize() with explicit match for archetype
string mapping, push_error on unknown input with Detective fallback
- main.gd: clear _known_triangle_ids in _teleport_transition() alongside
_known_recognition_ids so chime re-fires after room change
- news_ticker.gd: defer get_minimum_size() via call_deferred to run
after layout pass, fixing first-frame scroll distance
- 3 new tests: unknown archetype fallback, triangle dedup per-id,
independent triangle ID firing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sprint 24 Signal — three client tickets delivering the player-facing
storyteller feedback loop:
- #588: Character archetype select screen between New Game and session
start. Two-card UI (Smuggler/Detective), keyboard+mouse, ESC cancels.
GameState.character_archetype persisted and sent in StartupMessage.
PROTOCOL_VERSION bumped to 19.
- #590: Triangle crisis event consumer. Decodes triangle_crisis_events
from snapshot, fires sfx_monologue_chime_urgent once per triangle per
session via AudioManager.CHIME_ACTIVATION.
- #592: News ticker HUD element. Scrolling marquee on UILayer, visible
only when current_ticker is present in snapshot (Last Shift zone).
Zero-arg update_from_state reads from GameState.current_snapshot.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
TestHarness: remove deprecated tiles/visible_positions keys, add tile
type (floor/wall/door) to visible_tiles, expand radius to 5. Bug report
dialog: capture viewport screenshot before showing overlay, save as
screenshot.png in report bundle. time_display: use maxf() instead of
max() to match float argument types.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add D-088 Overlay pause/unpause signals to DebugConsole, wire in main.gd
so sim does not advance while typing debug commands
- Settings dialog reads live DebugConsole.is_enabled() instead of ConfigFile
directly, preventing checkbox/state divergence
- append_response respects disabled state — no auto-open when user disabled
console via settings
- tp command warns on invalid z value instead of silently defaulting to 0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Server #580 bumped to 18 for debug_response field. Client was still
at 17, causing every snapshot to be rejected — game unplayable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
4 new tests in test_fog_sprint22.gd verify BoundaryWall tiles populate
boundary_positions (not visible_positions), get VIS_FORWARD without
EXP_VISIBLE, stay EXP_UNEXPLORED after leaving LOS, and clear on new
snapshot. Comment in tile_renderer.gd documents implicit rendering path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- game_state.gd: add boundary_positions Dictionary field; BoundaryWall tiles from
visible_tiles go to boundary_positions instead of visible_positions — rendered by
tile_renderer but not tracked as explored fog memory
- fog_state.gd: update_from_state() writes VIS_FORWARD for boundary_positions so fog
lifts over margin wall content; boundary tiles excluded from exploration step so they
don't persist as EXP_EXPLORED when player turns away
- tile_renderer.gd: no changes needed — renders all visible_tiles by type, sector-agnostic
- test_fog_shader.gd: 4 new tests — boundary excluded from visible_positions, tracked in
boundary_positions, cleared each snapshot, fog lifts to VIS_FORWARD
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Maintains Dict[entity_id → Color] in dialogue_box for player conversations.
On first encounter, assigns a round-robin palette color; reuses on subsequent lines.
Eliminates position-based name-hash coloring for player dialogue.
Changes:
- Add _npc_entity_colors dict, _npc_entity_id, _next_npc_color fields
- Add _assign_npc_color(entity_id) — registers palette color on first encounter
- show_dialogue: accept npc_entity_id param, register entity color
- append_line: optional speaker_entity_id/target_entity_id stored in log entries
- append_player_line: pass _npc_entity_id as target_entity_id
- append_dialogue_response: accept entity_id, register, pass to append_line
- _format_entry else branch: look up _npc_entity_colors before name-hash fallback
- main.gd: pass _last_dialogue_npc_id to show_dialogue, speaker_entity_id to append_dialogue_response
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The merge of visual and client branches dropped the `tiles` variable
declaration. The client branch refactored bounds calculation to use
`visible_positions`, but the visual branch's zone tint loop still
iterates over `visible_tiles` for zone_id data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Round 4 review: mark VIS_PERIPHERAL as deprecated (peripheral sector
removed in #569, constant retained for test compatibility). Fix spec
status table row from "peripheral sector" to "cone gradient".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Round 3 review fixes — thorough spec cleanup:
- Remove visibility_sectors from data flow (peripheral removed in #569)
- Remove player_pos uniform (cone center implicit in visibility_tex)
- Update FogState pseudocode: remove sector step, add zone tint step
- Update lifecycle diagram to match single update_from_state() call
- Fix "5-layer fog" → "3-state fog" in Files to Create and impl notes
- Mark zone tint open question as resolved (Sprint 22, D-077)
- Document filter_nearest rationale on zone_tint_tex (D-073 hard zones)
- Note low-saturation tint is intentional per D-046 Hopper test
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GDScript int is i64 — when randi() returns a value with bit 31 set,
left-shifting by 32 sets bit 63, producing a negative i64. MessagePack
encodes this as a negative integer, which Rust rmp_serde rejects when
deserializing as u64, causing ~50% startup failure rate.
Fix: mask bit 31 before shifting in new_game() to cap entropy at 63
bits. Also mask the sign bit in _read_seed_file() to handle save files
written before this fix.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Widen world_seed entropy from u32 to full u64 by combining two randi()
calls (Hoshe warning #1)
- Persist world_seed to save directory and restore on resume_game() so
loaded sessions maintain D-010 deterministic replay (Tyre warning #2)
- Constrain EntanglementConfig intrigue range based on flat value so
mundane_ratio stays within D-029 spec [45,55]% (both reviewers)
- Remove dead VIS_PERIPHERAL constant and _grow_bounds() method
- Update test_client_p1 peripheral test for forward-only simplification
- Fix misleading exp_fade shader comment (filter_nearest = hard step)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>