feat(perception): ground vision cone in human sensory physiology

- Visible half-angle 150° → 100° (200° arc), matching average human
  binocular field. Blind spot widens from 60° to 160° — genuine
  vulnerability behind the character.
- Forward half-angle stays at 60° (120° arc) — binocular overlap zone.
- Peripheral zone represents combined sensory awareness: visual
  periphery + subconscious sound/motion tracking, not just eyes.
- Per-character VisionConeConfig allows implants and perception modes
  (D-017) to widen beyond the unaugmented baseline.
- Fog shader: smooth back-edge gradient via smoothstep blend between
  Layer 2 (peripheral) and Layer 3 (deep fog), eliminating blocky
  stair-stepped tiles at the rear of the vision cone.
- D-015 updated with physiological basis and per-character config.
- D-017 updated with vision cone modification by perception modes.
- D-020 updated with protocol versioning policy: PROTOCOL_VERSION
  gates wire format, not gameplay parameters.
- DB backup after sprint 21 close.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 22:31:29 +01:00
co-authored by Claude Opus 4.6
parent dd0de2ff04
commit a69e69e663
5 changed files with 67 additions and 39 deletions
+27 -24
View File
@@ -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);
+1
View File
@@ -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)
+9 -4
View File
@@ -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
Binary file not shown.
+30 -11
View File
@@ -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,