diff --git a/client/shaders/fog.gdshader b/client/shaders/fog.gdshader index c64bb31f9..dbcda4abd 100644 --- a/client/shaders/fog.gdshader +++ b/client/shaders/fog.gdshader @@ -73,33 +73,36 @@ void fragment() { vis = 0.0; } - if (vis > PERIPHERAL_LOW) { - // In or near vision cone - if (vis > CLEAR_THRESHOLD) { - // Layer 1: Clear — soft edge gradient - // Alpha matches Layer 2 at boundary (coverage=1.0 → 0.25) for continuity - float edge = smoothstep(CLEAR_THRESHOLD, 1.0, vis); - COLOR = vec4(DARK_OVERLAY, 0.25 * (1.0 - edge)); - } else { - // Layer 2: Light fog (peripheral + forward edge) - // D-059: animated Perlin noise, 8-10s cycle - float noise_val = texture(noise_tex, tile * 0.03 + vec2(time * 0.11, time * 0.07)).r; - float coverage = smoothstep(PERIPHERAL_LOW, CLEAR_THRESHOLD, vis); - // Blend from heavy fog (alpha ~0.38) to lighter fog near clear edge - // Taper noise to zero near the clear boundary — preserves breathing in deep - // peripheral but keeps the Layer 1/2 transition clean (no curly edge) - float noise_weight = 1.0 - smoothstep(0.5, 1.0, coverage); - float alpha = mix(0.38, 0.25, coverage) + noise_val * 0.05 * noise_weight; - COLOR = vec4(DARK_OVERLAY, alpha); - } - } else if (explored > 0.3) { - // Layer 3: Deep fog (previously explored, no longer in LOS) + if (vis > CLEAR_THRESHOLD) { + // Layer 1: Clear — soft edge gradient + // Alpha matches Layer 2 at boundary (coverage=1.0 → 0.25) for continuity + float edge = smoothstep(CLEAR_THRESHOLD, 1.0, vis); + COLOR = vec4(DARK_OVERLAY, 0.25 * (1.0 - edge)); + } else if (vis > PERIPHERAL_LOW || explored > 0.3) { + // Layers 2-3: smooth blend between peripheral fog and deep fog. + // Same gradient approach as the forward cone — smoothstep over the + // blur radius eliminates stair-stepped tile edges at the back of + // the vision cone. + + // Layer 2: peripheral fog + float noise_val = texture(noise_tex, tile * 0.03 + vec2(time * 0.11, time * 0.07)).r; + float coverage = smoothstep(PERIPHERAL_LOW, CLEAR_THRESHOLD, vis); + float noise_weight = 1.0 - smoothstep(0.5, 1.0, coverage); + float l2_alpha = mix(0.38, 0.25, coverage) + noise_val * 0.05 * noise_weight; + + // Layer 3: deep fog (explored, no longer in LOS) // D-059: near-monochrome, ~10% zone temperature tint, 15-20s breathing cycle vec3 zone_tint = texture(zone_tint_tex, tex_uv).rgb; - float noise_val = texture(noise_tex, tile * 0.015 + vec2(time * 0.06, time * 0.045)).r; + float noise_deep = texture(noise_tex, tile * 0.015 + vec2(time * 0.06, time * 0.045)).r; vec3 tint_color = mix(vec3(0.04), zone_tint, 0.1); - float alpha = mix(0.62, 0.76, noise_val); // Fog breathes - COLOR = vec4(tint_color, alpha); + float l3_alpha = mix(0.62, 0.76, noise_deep); + + // Back-edge gradient: blend Layer 2 → Layer 3 over the blur radius. + // PERIPHERAL_LOW * 3.0 ≈ 0.24 — matches the 3-4 tile Gaussian tail. + float back_edge = smoothstep(0.0, PERIPHERAL_LOW * 3.0, vis); + float alpha = mix(l3_alpha, l2_alpha, back_edge); + vec3 color = mix(tint_color, DARK_OVERLAY, back_edge); + COLOR = vec4(color, alpha); } else { // Layer 5: Unexplored, no maps — information zero COLOR = vec4(UNEXPLORED_COLOR, 1.0); diff --git a/decisions/architecture.md b/decisions/architecture.md index eb9040693..693920e1f 100644 --- a/decisions/architecture.md +++ b/decisions/architecture.md @@ -69,6 +69,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser - `ObserverSnapshot`: the only data structure crossing the boundary. Contains visible entities, fog state, sound events, monologue triggers, HUD widget data. Variable shape per character build. - `PlayerInput`: semantic actions (MoveNorth, Interact, UsePerceptionMode), not raw key events. Timestamped for deterministic processing. - `SimBridge` trait: abstracts transport. `LocalBridge` (subprocess, channels) and `NetworkBridge` (TCP, MessagePack) implement the same interface. +- **Protocol versioning policy:** `PROTOCOL_VERSION` gates **wire format compatibility** — field names, types, message structure. Bump when deserialization would fail (added/removed/renamed fields, new enum variants, changed types). Do NOT bump for gameplay parameter changes that affect what data flows through the same format (vision cone angles, NPC behavior, map layout, balance tuning). Per-character variation (D-015 cone config, D-017 perception modes) means these parameters differ between entities on the same server simultaneously — they are game state, not protocol. The client renders whatever `ObserverSnapshot` the server sends; it has no awareness of cone angles or perception mode configuration. - **Kill switch:** If no working prototype (character + fog + one NPC) exists by week 8 of development, pivot to pure Godot. If bridge/sync code exceeds game logic for 3 consecutive sprints, the architecture tax is too high. - **Development sequence:** 1. Build Rust simulation as standalone binary (testable via terminal/logs) diff --git a/decisions/perception.md b/decisions/perception.md index aab16a5de..fe54c84a4 100644 --- a/decisions/perception.md +++ b/decisions/perception.md @@ -19,13 +19,16 @@ How the player observes and interacts with the world: camera, fog, line-of-sight - **Date:** 2026-02-09 - **Decision:** Camera is locked to the character at all times. No panning. Optionally rotates to character facing direction (player option, later version). - **Rationale:** Pannable camera breaks the information model - you become a surveillance drone, not a character. Locked camera reinforces "you ARE this person." Rotation with facing direction restores directional audio mapping (binaural becomes viable again) and creates a natural vision cone (front = detailed, peripheral = reduced, behind = blind). -- **Vision cone model:** - - Forward: full LOS, full detail - - Peripheral: reduced range, dimmer - - Behind: fog / blind spot. You can be snuck up on. +- **Vision cone model (physiological basis):** + - Forward: 120° arc (60° half-angle) — binocular overlap zone where both eyes converge, providing stereoscopic depth and high acuity. Full LOS range, full detail. + - Peripheral: extends to 200° arc (100° half-angle) — combined sensory awareness envelope. Beyond pure visual acuity (~120°), this represents the character's subconscious tracking of sound cues, movement in the corner of the eye, and ambient sensory input. Nothing supernatural — just the human awareness envelope. Reduced range, dimmer. The fog shader gradient (D-059) is aligned to this outer edge. + - Behind: ~160° blind arc — no sensory awareness without explicit cues. Fog / blind spot. You can be snuck up on. The monologue system (D-016) bridges this gap when the character's senses fire ("Footsteps behind me"). + - Physiological reference: human binocular field ~200° horizontal, binocular overlap ~120°, far peripheral limit ~100-105° per eye (NCBI Visual Fields NBK220). +- **Per-character cone configuration:** The physiological baseline (120° forward / 200° total / 160° blind) is the unaugmented human floor. Bionic implants, perception modes (D-017), and equipment modify these angles upward. A back-of-head optical implant might widen to 320° visible (tiny blind spot); an ANA-touched character might approach 360°. The `VisionConeConfig` struct is per-entity, not global — different characters can have different awareness envelopes simultaneously. This is gameplay parameter variation, not a protocol change: the wire format (D-020) carries the *result* of cone classification (which tiles are Forward/Peripheral), not the cone angles themselves. - **Reference:** Hotline Miami's camera made that game terrifying with the same principle. - **v0.1:** Locked camera, no rotation. Vision cone still works on fixed-north map. Rotation deferred as player option. - **Raised by:** Team Leader (Jeroen). +- **Updated:** 2026-02-27 — cone angles grounded in human sensory physiology; per-character config documented. ### D-016: Internal monologue as core perception/atmosphere system - **Date:** 2026-02-09 @@ -54,9 +57,11 @@ How the player observes and interacts with the world: camera, fog, line-of-sight - Higher: biononic thermal, enhanced spectrum, passive scanning - ANA-touched: pattern recognition across all feeds, predictive awareness - **Playstyle implications (Nigel):** Low-tech Guardian playthrough = survival horror (blind, relying on contacts and paranoia). Senator playthrough = information overload (cameras and tracking but drowning in data). Perception modes are playstyle selectors. +- **Vision cone modification:** Perception modes and bionic equipment can widen the vision cone beyond the unaugmented human baseline (D-015: 120° forward / 200° total). Examples: back-of-head optical implant → 320° visible arc; thermal overlay → omnidirectional heat signatures through walls; ANA-touched awareness → near-360° pattern recognition. Each mode modifies `VisionConeConfig` per-entity. The fog shader renders whatever the server classifies — no client-side awareness of cone angles. - **v0.1:** Natural vision cone + basic audio indicators + insert minimap only. Additional modes are milestone features, each self-contained and modular. - **Engine implication:** Each perception mode is an observer query against the information boundary system ([D-010](architecture.md#d-010-multiplayer-ready-architectural-baseline) principle 2). Engine doesn't distinguish between eyes/thermal/camera - all are "given this sensor, what state is visible?" - **Raised by:** Team Leader (Jeroen) proposed thermal and camera hacking. Full team developed into perception mode framework. +- **Updated:** 2026-02-27 — vision cone modification by perception modes documented. ### D-018: Three-range sound model - **Date:** 2026-02-09 diff --git a/docs/backups/settledreach.db.backup b/docs/backups/settledreach.db.backup index 6a567e64a..4602899e2 100644 Binary files a/docs/backups/settledreach.db.backup and b/docs/backups/settledreach.db.backup differ diff --git a/server/src/perception/vision_cone.rs b/server/src/perception/vision_cone.rs index 283b9a5ac..d05559581 100644 --- a/server/src/perception/vision_cone.rs +++ b/server/src/perception/vision_cone.rs @@ -1,9 +1,21 @@ //! Vision cone system (D-015) //! -//! Modulates raw shadowcast output with direction-dependent sectors: -//! - Forward: full LOS range, full detail (~120 degree arc) -//! - Peripheral: reduced range, dimmer (~90 degrees each side) -//! - Behind: blind (excluded from output) +//! Modulates raw shadowcast output with direction-dependent sectors +//! grounded in human sensory physiology: +//! +//! - Forward: 120° arc (60° half-angle) — binocular overlap zone where +//! both eyes converge. Stereoscopic depth, high acuity. Full LOS range. +//! - Peripheral: extends to 200° arc (100° half-angle) — combined sensory +//! awareness envelope. Beyond pure visual acuity (~120°), this includes +//! subconscious tracking of sound cues, movement in the corner of the +//! eye, and ambient sensory input. Reduced range, dimmer rendering. +//! The fog shader gradient (D-059) is aligned to this outer edge. +//! - Behind: 160° blind arc — no sensory awareness without explicit cues. +//! You can be snuck up on. Monologue system (D-016) bridges this gap +//! when the character's other senses fire. +//! +//! Reference: binocular field ~200° horizontal, overlap ~120°, +//! far peripheral limit ~100-105° per eye (Wikipedia, NCBI NBK220). //! //! Y-down convention: North = (0, -1) @@ -22,16 +34,23 @@ impl Default for Facing { } } -/// Vision cone configuration per D-015 +/// Vision cone configuration per D-015. +/// +/// Angles grounded in human visual field physiology: +/// - forward_half_angle (60°): binocular overlap — where both eyes converge, +/// providing stereoscopic depth and high acuity. +/// - visible_half_angle (100°): average human binocular field limit — the +/// outermost boundary of peripheral vision. The fog shader's transparency +/// gradient is aligned to this edge. pub struct VisionConeConfig { /// Maximum vision range for forward sector (in tiles) pub forward_range: i32, /// Maximum vision range for peripheral sector (shorter than forward) pub peripheral_range: i32, - /// Half-angle of forward cone in radians (~60 degrees = 120 degree arc) + /// Half-angle of forward cone in radians (60° = 120° arc, binocular overlap) pub forward_half_angle: f32, - /// Half-angle of total visible cone in radians (~150 degrees = 300 degree arc) - /// Tiles beyond this are in the blind spot + /// Half-angle of total visible cone in radians (100° = 200° arc, peripheral limit) + /// Tiles beyond this are in the blind spot (~160° behind) pub visible_half_angle: f32, } @@ -40,8 +59,8 @@ impl Default for VisionConeConfig { Self { forward_range: 20, peripheral_range: 12, - forward_half_angle: std::f32::consts::FRAC_PI_3, // 60 degrees = 120 degree arc - visible_half_angle: 5.0 * std::f32::consts::FRAC_PI_6, // 150 degrees = 300 degree arc + forward_half_angle: std::f32::consts::FRAC_PI_3, // 60° = 120° arc (binocular overlap) + visible_half_angle: 100.0_f32.to_radians(), // 100° = 200° arc (peripheral limit) } } } @@ -258,7 +277,7 @@ mod tests { assert!(!cone.is_empty()); // Tile directly south (same x, far behind) should be in blind spot - // The blind spot is the 60 degrees directly behind + // The blind spot is ~160 degrees behind (beyond 100° peripheral limit) let has_direct_south_far = cone.iter().any(|&(x, y, _)| x == 5 && y >= 10); assert!( !has_direct_south_far,