- Fix stale 21→18 segment count in architecture doc, script docstrings,
and disk budget table (critical — both reviewers)
- Add average_m/README.md noting role as clothing reference body
- Add __main__ guards to diagnostic scripts
- Preflight-check all source paths in blender_process_bodies.py
- Document solid-white hair masks as v0.2 placeholder for multi-region
- Add fallback size comment for head mask generation
- Add bald_mask.png (1×1 black) for sidecar convention consistency
- Document child non-uniform scale rationale (Y=0.72 vs XZ=0.75)
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>
Port proven shaders (toon, toon_masked, outline) and skin tone textures
from quaternius-aesthetic spike to production client. Import 65-bone
armature and Universal Animation Libraries (UAL1 + UAL2). Set
gltf/embedded_image_handling=3 in project.godot (critical import setting).
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>
Player autonomy wins: AI Dialogue toggle is only greyed out for RAM fail
(< 1.6 GB free). Battery suspend shows a warning label instead of
disabling the toggle — player can override the auto-suspend at any time.
Changes:
- settings_dialog: toggle disabled = hw_status == "fail" only
- settings_dialog: add _ai_battery_warning_label (STATUS_YELLOW) shown
when _ai_inference_suspended; hidden when plugged back in via
set_ai_inference_suspended()
- settings_dialog: is_ai_dialogue_toggle_enabled() no longer checks
_ai_inference_suspended
- settings_dialog: set_ai_inference_suspended() updates warning label
visibility, not toggle disabled state
- ui-strings: add settings.ai_battery_warning "High battery usage"
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>
Per-player settings via rusqlite (bundled, zero runtime dep). Server
owns the settings DB; client sends ChangeSettings commands over IPC.
Extensible key-value with typed columns (String/Int/Float/Bool).
Protocol bumped to v20 with settings_response in ObserverSnapshot.
Co-Authored-By: Claude Opus 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>
16 tests covering character select, triangle activation consumer,
news ticker, and protocol v19 bridge. Includes show/hide behavior
for ticker on null current_ticker.
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 live server lifecycle to tests/run-visual (start/stop server per
scenario, parse LISTENING:{port}). Add MessagePack snapshot replay to
visual_capture.gd via Protocol.decode_snapshot() — exercises the full
client pipeline from wire bytes to rendered fog. Three replay scenarios
(hub_spawn, fog_theater, hub_after_movement) plus one live scenario
(fog_live_hub). Add gen_gauntlet_fixtures.rs to produce .msgpack fixtures
from the Gauntlet test world. Add max_diff_pct threshold to visual-diff.
Makefile: add fixtures-gauntlet target, fix build-client double-import,
preserve .godot cache in clean.
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>
Same Sprite2D correction applied to test_client_p2.gd entity color tests
(Terrain, Player) — ColorRect was replaced with Sprite2D in entity_renderer.gd.
Minor comment clarification in test_rendering.gd rotation test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reset _npc_entity_colors/_npc_entity_id/_next_npc_color in _end_player_conversation()
to prevent palette exhaustion across long sessions with many unique NPCs
- Re-enforce contrast floor after passive desaturation (_enforce_contrast after _desaturate)
- Add TestDialogueSpeakerColors suite: palette allocation, entity reuse, reset, fallback
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>
Fixes 7 pre-existing failures in test_entity_renderer tests:
- ColorRect → Sprite2D cast; .color → .self_modulate for D-033 color checks
- Position offset: (TILE_SIZE-24)/2 → EntityRenderer.ENTITY_OFFSET_{X,Y} (0.0)
- Rotation accuracy: expected values updated for raw un-normalised Godot rotation
Also adds SoundIndicatorRenderer to global_script_class_cache.cfg so test_rendering.gd parses.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add debug_response: None to all ObserverSnapshot constructors in
integration tests
- Bump PROTOCOL_VERSION assertion 17 → 18 in serialization tests
- Regenerate golden proof_room_tick_10.json (BoundaryWall tiles)
- Regenerate client msgpack fixtures for new snapshot fields
- Fix debug.rs resource optionality (Option<ResMut> for
ContaminationActive/EventQueue)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Godot 4.6 OpenGL3 compatibility mode does not support 'return' in
fragment(). The early returns on lines 68 and 82 caused silent shader
compilation failure, making the fog overlay render as a no-op — the
root cause of the Sprint 22 fog regression.
Restructured to if/else-if/else chain preserving identical logic.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The 5-layer fog model's deep fog sub-zone (alpha 0.55-0.70) was designed
for the old peripheral sector. After #569 removed peripheral, tiles behind
the player dropped straight to deep fog — indistinguishable from unexplored
black over dark scene tiles.
D-015 is explicit: behind = light fog overlay, art preserved, just "not
fresh." There is no deep fog sub-zone.
Changes:
- Remove dual light/deep fog alpha — all explored tiles use alpha 0.25-0.35
- Add 5x5 Gaussian blur on exploration texture to soften staircase boundary
- Use blurred exploration value for the visual transition (raw for anti-bleed)
- Remove DARK_OVERLAY constant (zone_tint handles all explored color)
Co-Authored-By: Claude Opus 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>
Remove the db/connectors → tooling/db/ symlink added in Sprint 21
(#274) and migrate all references to use tooling/db/ directly.
- Delete tracked symlink from db/connectors
- Remove duplicate db/connectors/* permission patterns from settings
- Update project-structure.md to reflect removal
- Move whatsinagame/static/db/connectors/ to whatsinagame/static/tooling/db/
- Update 20 whatsinagame template, skill, and test files
- Update comment references in client/tests/test_anti_tedium.gd
- Historical docs (old sprint briefings, changelog, discussions) left as-is
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The 7x7 Gaussian kernel (sigma 2.0) produces a 3-4 tile radius
gradient, not 6-8 tiles. Header comment now matches the function
comment and actual math.
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>
Sync fog-shader-spec.md with actual shader values after #563 tuning:
light fog 0.25-0.35 (was 0.26-0.34), deep fog 0.55-0.70 (was 0.54-0.70).
Pseudocode now uses symmetric noise remapping (noise*2-1)*amp to match
the shader. Added first-call guard comment on _tint_bytes in fog_state.gd.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements the StartupMessage protocol: client generates world_seed in
SessionManager.new_game(), sends it after handshake, server uses it to
seed SimRng and sample EntanglementConfig.
EntanglementConfig samples flat ∈ [25,35]%, intrigue ∈ [15,25]%, mundane
as remainder (D-029). Same seed produces identical config (D-010
determinism). Different seeds produce distinct configs in ≥90% of pairs.
Protocol flow: HandshakeMessage (server→client) → StartupMessage with
world_seed (client→server) → SimRng initialization → tick loop.
10 Rust tests (determinism, variation, bounds, sum invariant).
9 GDScript test stubs + 2 encode tests for client-side pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause: update_from_state() used visible_tiles (always empty in live
server mode) instead of visible_positions for bounds calculation. Bounds
never grew beyond 64x64, so tiles outside that area rendered as solid
unexplored black.
Fix: new _grow_bounds_from_positions() method reads visible_positions
(always populated from server snapshots). Shader fix: removed the
(explored < 0.01 && vis_raw < 0.01) guard that cut off the Gaussian
gradient at unexplored tile boundaries. Doubled blur step size for
D-066 compliant 6-8 tile soft gradient. Added debug_exploration mode
for diagnostic rendering of the exploration texture.
19 acceptance tests covering exploration persistence, bounds grow-only
invariant, gradient margin, Forward-only visibility writes, and
exploration data surviving texture resize.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ticket #563. Light fog alpha tuned to 0.25-0.35 range (was 0.25-0.55),
deep fog alpha set to 0.55-0.70 with zone temperature tint from
zone_tint_tex (bar=warm #2a1f15, hub=cool #1a1f2e, corridor=neutral
#1a1a1a). Two Perlin noise cycles: 8-10s light, 15-20s deep.
Zone tint texture now populated per-tile from server zone_id in
fog_state.gd with preservation across texture resizes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>