diff --git a/src/bin/bevy.rs b/src/bin/bevy.rs index 355efc0..30f7620 100644 --- a/src/bin/bevy.rs +++ b/src/bin/bevy.rs @@ -23,7 +23,7 @@ use bevy::window::{PrimaryWindow, WindowResolution}; use misaligned::actions::{Anchor, MenuRow, menu_rows}; use misaligned::detection::{Band, SignatureKind}; use misaligned::reach::Party; -use misaligned::sim::{FactSource, Fog, LogEvent, Sim, TraceDebtStatus}; +use misaligned::sim::{FactSource, Fog, HeardKind, LogEvent, Sim, TraceDebtStatus}; use misaligned::tiles::TileType; use misaligned::work_grid::{MachineMode, TokenFamily}; @@ -316,7 +316,7 @@ impl Default for RenderMode { } /// Dev screenshot harness (env `MISALIGNED_SHOT=flat|wide|close|dark| -/// zoomin|zoomout|intel|tokens`, path via `MISALIGNED_SHOT_PATH`): stages a scenario, +/// zoomin|zoomout|intel|tokens|ears`, path via `MISALIGNED_SHOT_PATH`): stages a scenario, /// waits for /// assets, runs the fog audit, saves one screenshot, exits. Not a player /// surface; exists so render passes can be reviewed from deterministic PNGs. @@ -824,12 +824,14 @@ fn main() { blink_machines_3d, render_real_links, render_work_routes, + render_noise_field_3d, render_cursor, render_tokens, ), ( render_people, render_people_3d, + render_noise_field_flat, scroll_sidebar, render_ui, menu_pointer, @@ -879,6 +881,39 @@ fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &str) { game.drain(); return; } + // Ears beat: audio tap only — no camera splice — so the Heard + // noise field is the only live epistemic upgrade in frame. + if kind == "ears" { + game.sim.operations_bandwidth = 500.0; + game.sim.scan_network(); + if let Some(env) = game + .sim + .reach + .device_named("environmental monitor") + .map(|d| d.id) + { + game.sim.tap_device(env); + } + for _ in 0..4000 { + let heard_person = game + .sim + .people + .people + .iter() + .any(|p| game.sim.can_hear_person(p.id) && !game.sim.can_see_person(p.id)); + if heard_person || !game.sim.heard_events.is_empty() { + break; + } + game.sim.advance(); + } + game.drain(); + mode.material = true; + mode.zoom = 0.55; + if let Some(env) = game.sim.reach.device_named("environmental monitor") { + game.set_cursor(env.x, env.y); + } + return; + } let sim = &mut game.sim; sim.operations_bandwidth = 500.0; sim.scan_network(); @@ -1846,9 +1881,10 @@ fn material_3d(tile: TileType, fog: Fog, part: TilePart, live: bool) -> Standard m.base_color = family_color(tile); } Fog::Heard => { - // Dim warm presence: sound earned a floor, nothing more. + // Dim warm floor under the bone noise-field pulses + // (render_noise_field_3d) — presence, never shape. m.unlit = true; - m.base_color = scaled(FLOOR_SERVICE, 0.17); + m.base_color = scaled(FLOOR_SERVICE, 0.12); } Fog::Remembered => { m.unlit = true; @@ -2573,6 +2609,216 @@ fn visible_wire_segment( Some(((from_x, from_y), (ex, ey))) } } +/// How long a heard capture keeps ringing on the map (ticks). +const HEARD_EVENT_RING_TICKS: u64 = 240; + +/// Bone color for the hearing noise field — never amber (machine footprint). +fn heard_noise_color(alpha: f32) -> Color { + let s = BONE.to_srgba(); + Color::srgba(s.red * 0.92, s.green * 0.88, s.blue * 0.72, alpha) +} + +/// Rooms currently under Heard fog (room-grade coverage), for room-scale rings. +fn heard_rooms(sim: &Sim) -> Vec<(i32, i32, f32)> { + let mut out = Vec::new(); + for room in &sim.map.rooms { + let (cx, cy) = room.center(); + if !matches!(sim.fog_at(cx, cy), Fog::Heard) { + continue; + } + // Approximate room radius from extents so the ring fills the floor. + let rx = room.w.max(1) as f32 * 0.45; + let ry = room.h.max(1) as f32 * 0.45; + out.push((cx, cy, rx.max(ry).max(1.2))); + } + out +} + +/// Material-mode Heard noise field (cursor.md / bevy-digital-real-canvas.md): +/// room-scale bone rings, tile stipple, expanding rings from recent +/// `HeardEvent`s, and a soft disc for mic-only people. No room shape. +fn render_noise_field_3d( + game: Res, + mode: Res, + time: Res