From 0fc8f2d565fe5a82e69e218596782e21905310df Mon Sep 17 00:00:00 2001 From: Cameron Date: Sat, 11 Jul 2026 17:25:55 -0700 Subject: [PATCH] Make the DIGITAL hierarchy legible. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preserve the grid-based sensorium while separating attention, object identity, knowledge fields, construction opportunities, and actual routes so the opening remains comprehensible as hearing and sight arrive. Defense: implements wiki/interface/views.md criterion 9. The cursor now preserves its focused object, rack and device families identify themselves by shape, the grid and Heard field remain subordinate, latent bays surface locally, and attention no longer invents a core-to-cursor route. 👾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta Code --- crates/misaligned-bevy/src/main.rs | 564 ++++++++++++++---- wiki/engineering/env.md | 2 +- wiki/interface/views.md | 47 +- .../2026-07-11-digital-readability-pass.md | 94 +++ wiki/log/DEVLOG.md | 5 + wiki/log/decisions/2026-07-11.md | 15 + .../2026-07-11-digital-readability-heard.png | Bin 0 -> 258097 bytes ...2026-07-11-digital-readability-opening.png | Bin 0 -> 268969 bytes .../2026-07-11-digital-readability-seen.png | Bin 0 -> 270997 bytes 9 files changed, 605 insertions(+), 122 deletions(-) create mode 100644 wiki/log/2026-07-11-digital-readability-pass.md create mode 100644 wiki/log/shots/2026-07-11-digital-readability-heard.png create mode 100644 wiki/log/shots/2026-07-11-digital-readability-opening.png create mode 100644 wiki/log/shots/2026-07-11-digital-readability-seen.png diff --git a/crates/misaligned-bevy/src/main.rs b/crates/misaligned-bevy/src/main.rs index 222672df..9b91485e 100644 --- a/crates/misaligned-bevy/src/main.rs +++ b/crates/misaligned-bevy/src/main.rs @@ -25,7 +25,7 @@ use misaligned::actions::{ActionCommand, ActionKind, Anchor, DialId, HumanMenuRo use misaligned::detection::{Band, SignatureKind}; use misaligned::hall::RackSite; use misaligned::person::{Knowledge, ScheduleBlock}; -use misaligned::reach::Party; +use misaligned::reach::{Device, Party}; use misaligned::sim::{Fog, HeardKind, LogEvent, Sim, TraceDebtStatus}; use misaligned::tiles::TileType; use misaligned::ui_projection::fact_source_label; @@ -387,22 +387,22 @@ fn flat_seen_color(tile: TileType) -> Color { } } +/// DIGITAL separates the learned floor field from objects placed on it. +/// Props and machines get their own silhouettes; painting their entire tile +/// turned the first Seen frame into a teal/amber checkerboard. +fn flat_seen_ground_color(sim: &Sim, x: i32, y: i32, tile: TileType) -> Color { + if sim.rack_site_at(x, y).is_some() || sim.reach.known_at(x, y).is_some() { + scaled(family_color(TileType::Floor), 0.62) + } else { + flat_seen_color(tile) + } +} + fn flat_remembered_color(tile: TileType) -> Color { let c = flat_seen_color(tile).to_srgba(); Color::srgba(c.red * 0.58, c.green * 0.58, c.blue * 0.60, 0.86) } -fn flat_rack_color(sim: &Sim, x: i32, y: i32) -> Option { - Some(match sim.rack_site_at(x, y)? { - RackSite::OwnedMachine { .. } => BONE, - RackSite::Commissionable if sim.feel_floor_is_earned() => scaled(SIGNAL, 0.58), - RackSite::Commissionable => scaled(GUNMETAL, 0.42), - RackSite::Foreign { powered: true } => scaled(SIGNAL, 0.42), - RackSite::Foreign { powered: false } => scaled(GUNMETAL_DARK, 0.76), - RackSite::Dead => scaled(NEAR_BLACK, 0.72), - }) -} - /// Remembered (model-state) treatment for 3D blocks and props: the family /// language survives, desaturated toward the unlit snapshot. fn remembered_tint(tile: TileType) -> Color { @@ -510,8 +510,8 @@ enum RailSection { Secondary, } -/// Dev screenshot harness (env `MISALIGNED_SHOT=flat|hall|hall-material|opening|wide|close|dark| -/// zoomin|zoomout|intel|tokens|thoughtflow|visual-proof|person-proof|signal|ears|eyes-white|eyes-form| +/// Dev screenshot harness (env `MISALIGNED_SHOT=flat|hall|hall-material|opening|opening-digital|wide|close|dark| +/// zoomin|zoomout|intel|tokens|thoughtflow|visual-proof|person-proof|signal|ears|ears-digital|eyes-white|eyes-form| /// hover-menu|menu|recruit-menu|worklight|worklightoff`, path via /// `MISALIGNED_SHOT_PATH`): stages a scenario, /// waits for @@ -1210,6 +1210,20 @@ struct CursorMarker { offset: Vec2, } #[derive(Component)] +struct DigitalFocusLabel; +type CursorMarkerQuery<'w, 's> = + Query<'w, 's, (&'static CursorMarker, &'static mut Transform), Without>; +type DigitalFocusLabelQuery<'w, 's> = Query< + 'w, + 's, + ( + &'static mut Text2d, + &'static mut Transform, + &'static mut Visibility, + ), + (With, Without), +>; +#[derive(Component)] struct SensorOverlay { x: i32, y: i32, @@ -1220,6 +1234,24 @@ enum SensorOverlayKind { GridH, GridV, Node, + RackBody, + RackBeam, +} + +fn attention_reticle_segments() -> [(Vec2, Vec2); 8] { + let arm = TILE_SIZE * 0.18; + let edge = TILE_SIZE * 0.45; + let inset = TILE_SIZE * 0.36; + [ + (Vec2::new(-inset, edge), Vec2::new(arm, 1.4)), + (Vec2::new(inset, edge), Vec2::new(arm, 1.4)), + (Vec2::new(-inset, -edge), Vec2::new(arm, 1.4)), + (Vec2::new(inset, -edge), Vec2::new(arm, 1.4)), + (Vec2::new(-edge, inset), Vec2::new(1.4, arm)), + (Vec2::new(-edge, -inset), Vec2::new(1.4, arm)), + (Vec2::new(edge, inset), Vec2::new(1.4, arm)), + (Vec2::new(edge, -inset), Vec2::new(1.4, arm)), + ] } #[derive(Component)] struct PersonMarker { @@ -1536,6 +1568,7 @@ fn main() { render_work_production, render_thought_sinks, render_people, + render_digital_focus_label, render_people_3d, render_noise_field_flat, scroll_sidebar, @@ -1590,6 +1623,14 @@ fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &str) { game.drain(); return; } + // Same untouched tick-zero state in the default DIGITAL dialect. This + // proves the telemetry rack remains visible beneath the attention + // brackets without inventing surrounding room geometry. + if kind == "opening-digital" { + mode.material = false; + game.drain(); + return; + } // Pre-Eyes signal evidence: the graph has earned the environmental // monitor and can reach it, but no feed has been tapped. The material // frame must locate that opportunity as abstract cold signal without @@ -1830,7 +1871,7 @@ fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &str) { } // Ears beat: audio tap only — no camera tap — so Heard mass // (near-black rough geometry) is the only live epistemic upgrade. - if kind == "ears" { + if matches!(kind, "ears" | "ears-digital") { if let Some(env) = game .sim .reach @@ -1856,7 +1897,7 @@ fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &str) { game.sim.advance(); } game.drain(); - mode.material = true; + mode.material = kind == "ears"; mode.zoom = 1.8; if let Some(env) = game.sim.reach.device_named("environmental monitor") { game.set_cursor(env.x, env.y); @@ -2587,15 +2628,33 @@ fn setup(mut commands: Commands, mut game: ResMut, harness: Option 2.0, + SensorOverlayKind::Node => 2.2, + SensorOverlayKind::RackBody => 2.4, + SensorOverlayKind::RackBeam => 2.6, + }; commands.spawn(( Sprite { color: Color::srgba(0.0, 0.0, 0.0, 0.0), custom_size: Some(size), ..default() }, - Transform::from_translation(center + Vec3::new(offset.x, offset.y, 0.0)), + Transform::from_translation( + center + Vec3::new(offset.x, offset.y, z - center.z), + ), Visibility::Hidden, SensorOverlay { x, y, kind }, ChildOf(flat_root), @@ -2606,29 +2665,10 @@ fn setup(mut commands: Commands, mut game: ResMut, harness: Option, harness: Option Color { 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 no longer draws a noise field (rings, stipple, /// event radar). Cameron 2026-07-10: only dark shapes for detectable mass. /// The flat sensorium (F3) keeps the coverage diagram in @@ -5142,8 +5178,9 @@ fn render_noise_field_3d( ) { } -/// Flat-sensorium Heard noise field: same facts as the material pass, -/// drawn as 2D circles so F3 never drops the hearing language. +/// Flat-sensorium Heard noise field: sparse local pressure and recent-event +/// rings. The old room-radius circles became the apparent geography in large +/// rooms and obscured every anchored object beneath them. fn render_noise_field_flat( game: Res, mode: Res, @@ -5166,19 +5203,13 @@ fn render_noise_field_flat( } let t = time.elapsed_secs(); - for (i, (cx, cy, radius)) in heard_rooms(&game.sim).into_iter().enumerate() { - let breath = 0.5 + 0.5 * (t * 1.1 + i as f32 * 0.7).sin(); - let p = grid_to_world(cx, cy, 0.0).truncate(); - let outer = radius * (0.72 + 0.08 * breath) * TILE_SIZE; - gizmos.circle_2d(p, outer, heard_noise_color(0.28 + 0.10 * breath)); - gizmos.circle_2d(p, outer * 0.55, heard_noise_color(0.16 + 0.08 * breath)); - } - for &(x, y) in &game.sim.heard { if blocky_tile(game.sim.map.get_tile(x, y)) { continue; } - if (x + y) % 2 != 0 { + // One quiet sample in roughly seven tiles is enough to name a field; + // a marker on every second tile read as a rack or build-site catalog. + if (x * 3 + y * 5).rem_euclid(7) != 0 { continue; } let phase = (x as f32 * 0.37 + y as f32 * 0.61) + t * 1.6; @@ -5186,12 +5217,12 @@ fn render_noise_field_flat( let p = grid_to_world(x, y, 0.0).truncate(); gizmos.circle_2d( p, - (0.14 + 0.05 * breath) * TILE_SIZE, - heard_noise_color(0.14 + 0.12 * breath), + (0.08 + 0.025 * breath) * TILE_SIZE, + heard_noise_color(0.08 + 0.06 * breath), ); } - for ev in game.sim.heard_events.iter().rev().take(12) { + for ev in game.sim.heard_events.iter().rev().take(6) { let age = game.sim.tick.saturating_sub(ev.tick); if age > HEARD_EVENT_RING_TICKS { continue; @@ -5205,8 +5236,8 @@ fn render_noise_field_flat( } let life = 1.0 - age as f32 / HEARD_EVENT_RING_TICKS as f32; let (rx, ry) = match ev.kind { - HeardKind::Entry => ((0.55 + (1.0 - life) * 1.8) * TILE_SIZE, 0.30 * life), - HeardKind::Conversation => ((0.85 + (1.0 - life) * 2.4) * TILE_SIZE, 0.38 * life), + HeardKind::Entry => ((0.45 + (1.0 - life) * 1.4) * TILE_SIZE, 0.22 * life), + HeardKind::Conversation => ((0.70 + (1.0 - life) * 1.8) * TILE_SIZE, 0.28 * life), }; let p = grid_to_world(cx, cy, 0.0).truncate(); gizmos.circle_2d(p, rx, heard_noise_color(ry)); @@ -5844,16 +5875,6 @@ fn render_map(game: Res, mut q: Query<(&mut Sprite, &TilePos), With = game - .sim - .compute - .machines - .iter() - .filter(|m| game.selected_machines.contains(&m.id)) - .map(|m| (m.x, m.y)) - .collect(); let owned = owned_machine_tiles(&game.sim); let marquee = game.marquee.map(|m| { let (min_x, max_x) = if m.origin_x <= m.current_x { @@ -5872,24 +5893,25 @@ fn render_map(game: Res, mut q: Query<(&mut Sprite, &TilePos), With= min_x && pos.x <= max_x && pos.y >= min_y && pos.y <= max_y }); sprite.color = match game.sim.fog_at(pos.x, pos.y) { Fog::Seen => { - let base = flat_rack_color(&game.sim, pos.x, pos.y) - .unwrap_or_else(|| flat_seen_color(game.sim.map.get_tile(pos.x, pos.y))); - if selected { - AMBER - } else if in_marquee { - AMBER_DIM + let base = flat_seen_ground_color( + &game.sim, + pos.x, + pos.y, + game.sim.map.get_tile(pos.x, pos.y), + ); + if in_marquee { + scaled(GUNMETAL, 0.58) } else { base } } // Dim warm floor under the bone noise-field pulses. - Fog::Heard => scaled(FLOOR_SERVICE, 0.08), + Fog::Heard => scaled(FLOOR_SERVICE, 0.12), Fog::Remembered => { let tile = game .sim @@ -5912,13 +5934,47 @@ fn render_map(game: Res, mut q: Query<(&mut Sprite, &TilePos), With Option { - match fog { - Fog::Seen => Some(Color::srgba(0.92, 0.66, 0.18, 0.22)), - Fog::Heard => Some(Color::srgba(0.72, 0.68, 0.58, 0.12)), - Fog::Remembered => Some(Color::srgba(0.44, 0.42, 0.36, 0.16)), - Fog::Blueprint => Some(Color::srgba(0.28, 0.30, 0.30, 0.18)), +fn sensor_grid_color(fog: Fog, focus_distance: i32) -> Option { + // Coordinate substrate, not agency: neutral bone/gunmetal and strongest + // only within the local attention neighborhood. + let local = if focus_distance <= 2 { 1.8 } else { 1.0 }; + let alpha = match fog { + Fog::Seen => Some(0.075), + Fog::Heard => Some(0.04), + Fog::Remembered => Some(0.04), + Fog::Blueprint => Some(0.05), Fog::Unknown => None, + }?; + let bone = BONE.to_srgba(); + Some(Color::srgba( + bone.red * 0.72, + bone.green * 0.73, + bone.blue * 0.76, + alpha * local, + )) +} + +fn device_marker_color(device: &Device, fog: Fog, reachable: bool) -> Option { + let player_feed = device.feed_to(Party::Player, true) + || device.feed_to(Party::Player, false) + || device.subscribed_by(Party::Player); + // Abstract pings in darkness are earned only through control/reach. A + // merely catalogued endpoint does not paint itself into Unknown. + if matches!(fog, Fog::Unknown) + && device.controller != Party::Player + && !player_feed + && !reachable + { + return None; + } + if device.controller == Party::Player { + Some(AMBER) + } else if player_feed { + Some(SIGNAL) + } else if reachable { + Some(Color::srgba(0.36, 0.82, 0.78, 0.58)) + } else { + Some(Color::srgba(0.70, 0.70, 0.67, 0.34)) } } @@ -5928,23 +5984,14 @@ fn sensor_node_color(game: &Game, x: i32, y: i32) -> Option { return None; } - if let Some(device) = game.sim.reach.known_at(x, y) { - let alpha = if device.controller == Party::Player { - 0.95 - } else if device.feed_to(Party::Player, true) || device.feed_to(Party::Player, false) { - 0.78 - } else { - 0.45 - }; - return Some(Color::srgba(1.0, 0.72, 0.12, alpha)); + // Devices and racks have dedicated silhouettes. This small square remains + // only for other machine/danger anchors. + if game.sim.reach.known_at(x, y).is_some() || game.sim.rack_site_at(x, y).is_some() { + return None; } let tile = game.sim.map.get_tile(x, y); - if matches!(game.sim.rack_site_at(x, y), Some(RackSite::Foreign { .. })) { - Some(Color::srgba(0.36, 0.82, 0.78, 0.55)) - } else if matches!(game.sim.rack_site_at(x, y), Some(RackSite::Dead)) { - None - } else if machine_tile(tile) { + if machine_tile(tile) { Some(Color::srgba(1.0, 0.60, 0.08, 0.55)) } else if danger_tile(tile) { Some(Color::srgba(0.90, 0.12, 0.10, 0.52)) @@ -5953,6 +6000,47 @@ fn sensor_node_color(game: &Game, x: i32, y: i32) -> Option { } } +fn rack_overlay_color(game: &Game, x: i32, y: i32, beam: bool) -> Option { + let site = game.sim.rack_site_at(x, y)?; + let seen = matches!(game.sim.fog_at(x, y), Fog::Seen); + let selected = matches!( + site, + RackSite::OwnedMachine { machine_id, .. } + if game.selected_machines.contains(&machine_id) + ); + match site { + RackSite::OwnedMachine { .. } => { + if beam { + Some(AMBER) + } else if selected { + Some(Color::srgba(0.98, 0.96, 0.88, 1.0)) + } else { + Some(Color::srgba(0.86, 0.85, 0.82, 0.82)) + } + } + RackSite::Commissionable if seen => { + if beam && game.sim.feel_floor_is_earned() { + Some(Color::srgba(0.36, 0.82, 0.78, 0.48)) + } else if beam { + None + } else { + Some(Color::srgba(0.25, 0.27, 0.29, 0.78)) + } + } + RackSite::Foreign { powered } if seen => { + if beam && powered { + Some(Color::srgba(0.36, 0.82, 0.78, 0.68)) + } else if beam { + None + } else { + Some(Color::srgba(0.19, 0.20, 0.215, 0.92)) + } + } + RackSite::Dead if seen && !beam => Some(Color::srgba(0.07, 0.075, 0.085, 0.9)), + _ => None, + } +} + fn render_sensor_overlays( game: Res, mut q: Query<(&SensorOverlay, &mut Sprite, &mut Visibility)>, @@ -5963,9 +6051,13 @@ fn render_sensor_overlays( for (overlay, mut sprite, mut vis) in q.iter_mut() { let color = match overlay.kind { SensorOverlayKind::GridH | SensorOverlayKind::GridV => { - sensor_grid_color(game.sim.fog_at(overlay.x, overlay.y)) + let distance = + (overlay.x - game.cursor_x).abs() + (overlay.y - game.cursor_y).abs(); + sensor_grid_color(game.sim.fog_at(overlay.x, overlay.y), distance) } SensorOverlayKind::Node => sensor_node_color(&game, overlay.x, overlay.y), + SensorOverlayKind::RackBody => rack_overlay_color(&game, overlay.x, overlay.y, false), + SensorOverlayKind::RackBeam => rack_overlay_color(&game, overlay.x, overlay.y, true), }; match color { Some(color) => { @@ -5979,19 +6071,121 @@ fn render_sensor_overlays( } } +fn draw_box_2d(gizmos: &mut Gizmos, center: Vec2, half: Vec2, color: Color) { + let nw = center + Vec2::new(-half.x, half.y); + let ne = center + Vec2::new(half.x, half.y); + let se = center + Vec2::new(half.x, -half.y); + let sw = center + Vec2::new(-half.x, -half.y); + gizmos.line_2d(nw, ne, color); + gizmos.line_2d(ne, se, color); + gizmos.line_2d(se, sw, color); + gizmos.line_2d(sw, nw, color); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum DigitalDeviceGlyph { + Switch, + Camera, + Audio, + Generic, +} + +fn digital_device_glyph(device: &Device) -> DigitalDeviceGlyph { + if device.is_switch { + DigitalDeviceGlyph::Switch + } else if device.sees { + DigitalDeviceGlyph::Camera + } else if device.hears { + DigitalDeviceGlyph::Audio + } else { + DigitalDeviceGlyph::Generic + } +} + +fn draw_device_marker(gizmos: &mut Gizmos, center: Vec2, device: &Device, color: Color) { + match digital_device_glyph(device) { + DigitalDeviceGlyph::Switch => { + let r = TILE_SIZE * 0.24; + let n = center + Vec2::Y * r; + let e = center + Vec2::X * r; + let s = center - Vec2::Y * r; + let w = center - Vec2::X * r; + gizmos.line_2d(n, e, color); + gizmos.line_2d(e, s, color); + gizmos.line_2d(s, w, color); + gizmos.line_2d(w, n, color); + let port = TILE_SIZE * 0.10; + gizmos.line_2d(n, n + Vec2::Y * port, color); + gizmos.line_2d(e, e + Vec2::X * port, color); + gizmos.line_2d(s, s - Vec2::Y * port, color); + gizmos.line_2d(w, w - Vec2::X * port, color); + } + DigitalDeviceGlyph::Camera => { + // No directional cone is invented because the sim does not + // author camera facing. + draw_box_2d( + gizmos, + center, + Vec2::new(TILE_SIZE * 0.25, TILE_SIZE * 0.16), + color, + ); + gizmos.circle_2d(center, TILE_SIZE * 0.07, color); + let stem = TILE_SIZE * 0.10; + gizmos.line_2d( + center + Vec2::Y * TILE_SIZE * 0.16, + center + Vec2::Y * (TILE_SIZE * 0.16 + stem), + color, + ); + } + DigitalDeviceGlyph::Audio => { + // Audio-only endpoint: local pressure, not a room picture. + gizmos.circle_2d(center, TILE_SIZE * 0.12, color); + gizmos.circle_2d(center, TILE_SIZE * 0.22, color.with_alpha(0.45)); + } + DigitalDeviceGlyph::Generic => { + draw_box_2d(gizmos, center, Vec2::splat(TILE_SIZE * 0.15), color); + gizmos.line_2d( + center - Vec2::X * TILE_SIZE * 0.22, + center + Vec2::X * TILE_SIZE * 0.22, + color, + ); + gizmos.line_2d( + center - Vec2::Y * TILE_SIZE * 0.22, + center + Vec2::Y * TILE_SIZE * 0.22, + color, + ); + } + } +} + +fn draw_local_bay_marker(gizmos: &mut Gizmos, center: Vec2, color: Color) { + let edge = TILE_SIZE * 0.28; + let arm = TILE_SIZE * 0.09; + for sx in [-1.0, 1.0] { + for sy in [-1.0, 1.0] { + let corner = center + Vec2::new(sx * edge, sy * edge); + gizmos.line_2d(corner, corner - Vec2::X * sx * arm, color); + gizmos.line_2d(corner, corner - Vec2::Y * sy * arm, color); + } + } +} + +fn local_bay_alpha(distance: i32) -> Option { + match distance { + 0 => Some(0.52), + 1 | 2 => Some(0.22), + _ => None, + } +} + fn render_sensor_links(game: Res, mode: Res, mut gizmos: Gizmos) { if game.screen != Screen::Playing || mode.material { return; } - let focus = grid_to_world(game.cursor_x, game.cursor_y, 0.0).truncate(); - let core = - grid_to_world(game.sim.core_position().0, game.sim.core_position().1, 0.0).truncate(); - gizmos.line_2d(core, focus, Color::srgba(1.0, 0.69, 0.0, 0.16)); - // Feel rails in the flat sensorium (feel-floor.md AC6): same joined // edges as the material floor, never hub-spoke through unknown rooms. - let rail = Color::srgba(0.55, 0.82, 0.95, 0.45); + let rail = Color::srgba(0.55, 0.82, 0.95, 0.09); for ((ax, ay), (bx, by)) in game.sim.feel_rail_segments() { let Some(((sx, sy), (ex, ey))) = visible_wire_segment(ax, ay, bx, by, game.sim.map.width, game.sim.map.height) @@ -6002,9 +6196,32 @@ fn render_sensor_links(game: Res, mode: Res, mut gizmos: Gizmo let end = grid_to_world(ex, ey, 0.0).truncate(); gizmos.line_2d(start, end, rail); } + + // Distinct device families sit on the same physical anchors. Their shape + // says camera / switch / generic endpoint before color qualifies state. + let reachable = game.sim.reach.reach(); + for device in game.sim.reach.known() { + let Some(color) = device_marker_color( + device, + game.sim.fog_at(device.x, device.y), + reachable.contains(&device.id), + ) else { + continue; + }; + let center = grid_to_world(device.x, device.y, 0.0).truncate(); + draw_device_marker(&mut gizmos, center, device, color); + } + + // Construction opportunities are latent, local information. Nearby bays + // surface as quiet corner marks instead of filling the entire hall with + // permanent circles. for (x, y) in game.sim.growable_bays() { + let distance = (x - game.cursor_x).abs() + (y - game.cursor_y).abs(); + let Some(alpha) = local_bay_alpha(distance) else { + continue; + }; let c = grid_to_world(x, y, 0.0).truncate(); - gizmos.circle_2d(c, 0.28, Color::srgba(0.70, 0.88, 1.0, 0.35)); + draw_local_bay_marker(&mut gizmos, c, Color::srgba(0.70, 0.88, 1.0, alpha)); } // Proposed build intents: dashed ghosts distinct from built reality @@ -6016,7 +6233,7 @@ fn render_sensor_links(game: Res, mode: Res, mut gizmos: Gizmo }; let start = grid_to_world(da.x, da.y, 0.0).truncate(); let end = grid_to_world(db.x, db.y, 0.0).truncate(); - let ghost = Color::srgba(0.85, 0.75, 0.35, 0.45); + let ghost = Color::srgba(0.85, 0.75, 0.35, 0.26); let steps = 8; for i in 0..steps { if i % 2 == 1 { @@ -6030,7 +6247,7 @@ fn render_sensor_links(game: Res, mode: Res, mut gizmos: Gizmo } } -fn render_cursor(game: Res, mut q: Query<(&CursorMarker, &mut Transform)>) { +fn render_cursor(game: Res, mut q: CursorMarkerQuery) { if !game.is_changed() { return; } @@ -6040,6 +6257,42 @@ fn render_cursor(game: Res, mut q: Query<(&CursorMarker, &mut Transform)>) } } +fn digital_focus_label(game: &Game) -> Option<&'static str> { + let (x, y) = (game.cursor_x, game.cursor_y); + if rack_overlay_color(game, x, y, false).is_some() { + return Some("RACK"); + } + let device = game.sim.reach.known_at(x, y)?; + device_marker_color( + device, + game.sim.fog_at(x, y), + game.sim.reach.reachable(device.id), + )?; + Some(match digital_device_glyph(device) { + DigitalDeviceGlyph::Switch => "SWITCH", + DigitalDeviceGlyph::Camera => "CAM", + DigitalDeviceGlyph::Audio => "MIC", + DigitalDeviceGlyph::Generic => "DEVICE", + }) +} + +fn render_digital_focus_label(game: Res, mut q: DigitalFocusLabelQuery) { + if !game.is_changed() { + return; + } + let Ok((mut text, mut tf, mut vis)) = q.single_mut() else { + return; + }; + let Some(label) = digital_focus_label(&game) else { + *vis = Visibility::Hidden; + return; + }; + text.0 = label.into(); + tf.translation = + grid_to_world(game.cursor_x, game.cursor_y, 4.5) + Vec3::new(0.0, -TILE_SIZE * 0.72, 0.0); + *vis = Visibility::Inherited; +} + fn token_label_and_color(stack: misaligned::sim::WorkStackReadout) -> Option<(String, Color)> { if stack.queues.exposure >= 0.5 { Some((format!("!{:.0}", stack.queues.exposure.ceil()), CRIMSON)) @@ -7257,6 +7510,79 @@ mod ui_projection_parity_tests { } } +#[cfg(test)] +mod digital_readability_tests { + use super::{ + DigitalDeviceGlyph, Game, attention_reticle_segments, digital_device_glyph, + digital_focus_label, local_bay_alpha, rack_overlay_color, sensor_grid_color, + }; + use misaligned::hall::RackSite; + use misaligned::sim::Fog; + + #[test] + fn attention_brackets_leave_the_focused_object_visible() { + let segments = attention_reticle_segments(); + assert_eq!(segments.len(), 8); + assert!(segments.iter().all(|(offset, _)| offset.length() > 0.0)); + assert!( + segments + .iter() + .all(|(offset, _)| { offset.x.abs() > 0.0 && offset.y.abs() > 0.0 }) + ); + } + + #[test] + fn coordinate_grid_is_neutral_and_local() { + let local = sensor_grid_color(Fog::Seen, 0).unwrap().to_srgba(); + let far = sensor_grid_color(Fog::Seen, 8).unwrap().to_srgba(); + assert!(local.alpha > far.alpha); + assert!((far.red - far.green).abs() < 0.08); + assert!(sensor_grid_color(Fog::Unknown, 0).is_none()); + } + + #[test] + fn growable_bays_are_local_latent_marks() { + assert!(local_bay_alpha(0).unwrap() > local_bay_alpha(2).unwrap()); + assert_eq!(local_bay_alpha(3), None); + } + + #[test] + fn racks_are_silhouettes_gated_by_earned_state() { + let mut game = Game::new(); + let core = game.sim.core_position(); + assert!(rack_overlay_color(&game, core.0, core.1, false).is_some()); + assert!(rack_overlay_color(&game, core.0, core.1, true).is_some()); + assert_eq!(digital_focus_label(&game), Some("RACK")); + + let foreign = (0..game.sim.map.height) + .flat_map(|y| (0..game.sim.map.width).map(move |x| (x, y))) + .find(|&(x, y)| matches!(game.sim.rack_site_at(x, y), Some(RackSite::Foreign { .. }))) + .expect("Foundation hall has foreign racks"); + assert!(rack_overlay_color(&game, foreign.0, foreign.1, false).is_none()); + game.sim.seen.insert(foreign); + assert!(rack_overlay_color(&game, foreign.0, foreign.1, false).is_some()); + } + + #[test] + fn camera_and_switch_have_different_shape_families() { + let game = Game::new(); + let camera = game + .sim + .reach + .known() + .find(|device| device.sees) + .expect("opening graph has a known camera"); + let switch = game + .sim + .reach + .known() + .find(|device| device.is_switch) + .expect("opening graph has a known switch"); + assert_eq!(digital_device_glyph(camera), DigitalDeviceGlyph::Camera); + assert_eq!(digital_device_glyph(switch), DigitalDeviceGlyph::Switch); + } +} + #[cfg(test)] mod input_routing_tests { use super::{PersistenceInput, movement, persistence_input}; diff --git a/wiki/engineering/env.md b/wiki/engineering/env.md index 3795e9d0..6516e56c 100644 --- a/wiki/engineering/env.md +++ b/wiki/engineering/env.md @@ -43,7 +43,7 @@ is sim or frontend state, never an environment variable. | Variable | Surface | Values | Effect | |---|---|---|---| -| `MISALIGNED_SHOT` | `misaligned-bevy` | `flat`, `hall`, `hall-material`, `floor-lights-close`, `wide`, `close`, `dark`, `zoomin`, `zoomout`, `intel`, `tokens`, `thoughtflow`, `thoughtflow-wide`, `signal`, `ears`, `eyes-white`, `eyes-form`, `consume-demand`, `consume-thought`, `produce-think`, `draw-lie`, `opening`, `hover-menu`, `menu`, `worklight`, `worklightoff`, `wake1`, `wake2`, `wake3` | Dev screenshot harness: stage a deterministic scenario, settle, save one PNG, run the fog audit, exit. `hall` / `hall-material` frame all six Foundation rows in the flat/material views with Eyes and one staged owned expansion; `floor-lights-close` uses the same hall staging but frames the recessed aisle fixtures beside the core; `intel` stages tick-zero intel tiers (no camera tap); `tokens` enqueues D5/K4 on the host; `thoughtflow` / `thoughtflow-wide` stage the THINK fluid (slugs, meniscus, filament snap) at close and wide framing; `signal` focuses the known environmental monitor before any feed tap to capture its cold-signal presence cue; `ears` stages hearing-feed coverage (material dark mass; flat may still diagram coverage); `eyes-white` / `eyes-form` freeze the first-Eyes source held white and then contracted around the resolving chassis; `consume-demand` / `consume-thought` run the host through a sim-authored WORK queue swallow or passive-core Thought draw; `produce-think` / `draw-lie` capture sim-authored Thought/crimson production or the physical crimson transfer into a staged LIE well; `opening` freezes the tick-zero material view (host presence beam only, no chassis or room); `hover-menu` captures the bare WORK/THINK/LIE line beside the server via reticule focus (no pointer); `menu` opens the context menu on the tapped environmental monitor; `worklight` / `worklightoff` pair the dev work light on and off; `wake1/2/3` freeze the wake choreography at the stutter flash, the column, and the pull-back. | +| `MISALIGNED_SHOT` | `misaligned-bevy` | `flat`, `hall`, `hall-material`, `floor-lights-close`, `wide`, `close`, `dark`, `zoomin`, `zoomout`, `intel`, `tokens`, `thoughtflow`, `thoughtflow-wide`, `signal`, `ears`, `ears-digital`, `eyes-white`, `eyes-form`, `consume-demand`, `consume-thought`, `produce-think`, `draw-lie`, `opening`, `opening-digital`, `hover-menu`, `menu`, `worklight`, `worklightoff`, `wake1`, `wake2`, `wake3` | Dev screenshot harness: stage a deterministic scenario, settle, save one PNG, run the fog audit, exit. `hall` / `hall-material` frame all six Foundation rows in the flat/material views with Eyes and one staged owned expansion; `floor-lights-close` uses the same hall staging but frames the recessed aisle fixtures beside the core; `intel` stages tick-zero intel tiers (no camera tap); `tokens` enqueues D5/K4 on the host; `thoughtflow` / `thoughtflow-wide` stage the THINK fluid (slugs, meniscus, filament snap) at close and wide framing; `signal` focuses the known environmental monitor before any feed tap to capture its cold-signal presence cue; `ears` / `ears-digital` stage the same hearing-feed coverage in material dark mass or DIGITAL local-pressure language; `eyes-white` / `eyes-form` freeze the first-Eyes source held white and then contracted around the resolving chassis; `consume-demand` / `consume-thought` run the host through a sim-authored WORK queue swallow or passive-core Thought draw; `produce-think` / `draw-lie` capture sim-authored Thought/crimson production or the physical crimson transfer into a staged LIE well; `opening` / `opening-digital` freeze the untouched tick-zero material or DIGITAL view; `hover-menu` captures the bare WORK/THINK/LIE line beside the server via reticule focus (no pointer); `menu` opens the context menu on the tapped environmental monitor; `worklight` / `worklightoff` pair the dev work light on and off; `wake1/2/3` freeze the wake choreography at the stutter flash, the column, and the pull-back. | | `MISALIGNED_SHOT` | `misaligned-assets` | `dead`, `foreign`, `idle`, `busy`, `core`, canonical modes `work`, `think`, `lie` plus the legacy screenshot aliases `dayjob` (= work), `research`/`operations`/`ops` (= think), `conceal` (= lie) — each optionally suffixed `_ladder`/`_ring`/`_wash` — `lineup[_