diff --git a/DESIGN.md b/DESIGN.md index 6a5b4430..e0eab629 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -1536,6 +1536,16 @@ knows."* fantasy — a re-render is simpler and truer); a dedicated build system (a new system violates no-new-systems and the scale law). Specs: spec/views.md, spec/building.md; enriches spec/reach.md. +- **2026-07-07 — Bevy visual floor before new art.** The graphical build has + to look intentional before the Pixel Lab backlog clears: no postage-stamp + known map in empty black, no terminal dump pasted into a sidebar, and no + humanoid/purple process marker in B1 (the cursor is attention, not a body). + Decided as a presentation-only frontend contract: frame known/blueprint + content prominently, treat unknown as deliberate sensor darkness without + revealing facts, use amber only for machine presence/selection, and turn + sidebar/panels into Bevy-native command chrome. This stages toward the + two-view spec without adding sim or save state. Spec: + wiki/interface/bevy-visual-floor.md. - **2026-07-07 — The wiki migration: approved and underway.** Cameron approved spec/wiki.md's shape ("I like the wiki format... go ahead and implement the wiki thing"); staged as four commits (Adopt, Move, diff --git a/src/bin/bevy.rs b/src/bin/bevy.rs index e497db5e..810fff87 100644 --- a/src/bin/bevy.rs +++ b/src/bin/bevy.rs @@ -23,15 +23,22 @@ const SIDEBAR_WIDTH: f32 = 340.0; /// Sterile amber — machine presence and caution (the terminal palette's /// AMBER, in linear-ish sRGB floats). const AMBER: Color = Color::srgb(1.0, 0.69, 0.0); +/// Dim amber — dormant machine presence. +const AMBER_DIM: Color = Color::srgb(0.69, 0.49, 0.06); /// Bone white — primary data. const BONE: Color = Color::srgb(0.86, 0.85, 0.82); +/// Chrome/gunmetal ramp for structure and secondary data. +const DIM: Color = Color::srgb(0.55, 0.55, 0.52); +const STRUCTURE: Color = Color::srgb(0.23, 0.24, 0.25); +const FLOOR: Color = Color::srgb(0.16, 0.16, 0.17); +/// Blood crimson — detection and danger, never decoration. +const CRIMSON: Color = Color::srgb(0.84, 0.15, 0.15); // ─── Art ───────────────────────────────────────────────────────────────────── #[derive(Resource)] struct Art { tiles: HashMap>, - process: Handle, } impl Art { @@ -53,10 +60,7 @@ impl Art { tiles.insert(TileType::SecurityDoor1, t("door")); tiles.insert(TileType::SecurityDoor2, t("door")); tiles.insert(TileType::SecurityDoor3, t("door")); - Self { - tiles, - process: assets.load("pixellab/sprites/misaligned_process.png"), - } + Self { tiles } } } @@ -64,23 +68,94 @@ impl Art { fn tile_color(tile: TileType) -> Color { use TileType::*; match tile { - Rock => Color::srgb(0.05, 0.05, 0.06), - Wall => Color::srgb(0.2, 0.2, 0.24), - Floor | FloorDrain => Color::srgb(0.5, 0.5, 0.55), - Entry => Color::srgb(0.2, 0.6, 0.2), - Core | Rack => Color::srgb(0.15, 0.15, 0.18), - PowerCore | Ups => Color::srgb(0.9, 0.6, 0.1), - Door => Color::srgb(0.7, 0.7, 0.75), - SecurityDoor1 => Color::srgb(0.9, 0.8, 0.4), - SecurityDoor2 => Color::srgb(0.9, 0.6, 0.3), - SecurityDoor3 => Color::srgb(0.9, 0.3, 0.3), - EnvCamera | DockCamera | CameraNode => Color::srgb(0.8, 0.2, 0.5), - DeadEquipment | RecordsBox | Pallet | Shelving => Color::srgb(0.4, 0.35, 0.2), - SealedDoor | RollDoor | LabBench => Color::srgb(0.5, 0.2, 0.2), - Switch | PatchPanel | CableRun => Color::srgb(0.2, 0.6, 0.7), - BreakerPanel | Conduit => Color::srgb(0.7, 0.7, 0.3), - HvacUnit | Vent | Sump => Color::srgb(0.2, 0.4, 0.6), - MopSink | KeyHook => Color::srgb(0.3, 0.5, 0.3), + Rock => Color::srgb(0.03, 0.03, 0.035), + Wall => STRUCTURE, + Floor | FloorDrain => FLOOR, + Entry => DIM, + Core | Rack | PowerCore | Ups | Switch | PatchPanel | BreakerPanel | CableRun | Conduit => { + AMBER_DIM + } + Door | SecurityDoor1 | SecurityDoor2 => DIM, + SecurityDoor3 | EnvCamera | DockCamera | CameraNode | SealedDoor | RollDoor => CRIMSON, + DeadEquipment | RecordsBox | Pallet | Shelving | LabBench | HvacUnit | Vent | Sump => { + Color::srgb(0.34, 0.34, 0.33) + } + MopSink | KeyHook => BONE, + } +} + +fn machine_tile(tile: TileType) -> bool { + use TileType::*; + matches!( + tile, + Core | Rack + | PowerCore + | Ups + | Switch + | PatchPanel + | BreakerPanel + | HvacUnit + | CableRun + | Conduit + ) +} + +fn danger_tile(tile: TileType) -> bool { + use TileType::*; + matches!( + tile, + SecurityDoor3 | EnvCamera | DockCamera | CameraNode | SealedDoor | RollDoor + ) +} + +fn seen_tint(tile: TileType) -> Color { + use TileType::*; + match tile { + Core | Rack | PowerCore | Ups | Switch | PatchPanel | BreakerPanel | CableRun | Conduit => { + Color::srgb(1.0, 0.82, 0.34) + } + EnvCamera | DockCamera | CameraNode => Color::srgb(0.95, 0.58, 0.18), + SecurityDoor3 | SealedDoor | RollDoor => Color::srgb(0.95, 0.28, 0.24), + Wall | Rock => Color::srgb(0.48, 0.50, 0.50), + Floor | FloorDrain => Color::srgb(0.58, 0.60, 0.57), + Door | SecurityDoor1 | SecurityDoor2 => Color::srgb(0.70, 0.68, 0.62), + _ => Color::srgb(0.62, 0.62, 0.58), + } +} + +fn remembered_tint(tile: TileType) -> Color { + if machine_tile(tile) { + Color::srgba(0.55, 0.42, 0.18, 0.78) + } else if danger_tile(tile) { + Color::srgba(0.50, 0.16, 0.16, 0.78) + } else { + Color::srgba(0.42, 0.42, 0.44, 0.76) + } +} + +fn blueprint_color(tile: TileType) -> Color { + use TileType::*; + if machine_tile(tile) { + Color::srgb(0.28, 0.20, 0.06) + } else if danger_tile(tile) { + Color::srgb(0.24, 0.08, 0.08) + } else { + match tile { + Wall => Color::srgb(0.17, 0.18, 0.21), + Floor | FloorDrain => Color::srgb(0.075, 0.082, 0.095), + Door | SecurityDoor1 | SecurityDoor2 => Color::srgb(0.18, 0.18, 0.17), + _ => Color::srgb(0.10, 0.105, 0.11), + } + } +} + +fn unknown_color(x: i32, y: i32) -> Color { + // Deliberate sensor darkness, not factual room geometry: a tiny uniform + // checker keeps the void from reading as an undrawn black window. + if (x + y) % 2 == 0 { + Color::srgb(0.012, 0.013, 0.017) + } else { + Color::srgb(0.018, 0.019, 0.024) } } @@ -190,7 +265,21 @@ struct TilePos { y: i32, } #[derive(Component)] -struct CursorMarker; +struct CursorMarker { + offset: Vec2, +} +#[derive(Component)] +struct SensorOverlay { + x: i32, + y: i32, + kind: SensorOverlayKind, +} +#[derive(Clone, Copy)] +enum SensorOverlayKind { + GridH, + GridV, + Node, +} #[derive(Component)] struct PersonMarker { id: u8, @@ -232,6 +321,8 @@ fn main() { advance_sim, update_camera, render_map, + render_sensor_overlays, + render_sensor_links, render_cursor, render_people, render_ui, @@ -260,13 +351,28 @@ fn setup(mut commands: Commands, assets: Res, mut game: ResMut, mut game: ResMut { sprite.image = Handle::default(); - sprite.color = Color::srgb(0.10, 0.12, 0.14); + sprite.color = Color::srgb(0.07, 0.07, 0.055); } Fog::Remembered => { let tile = game @@ -717,35 +880,121 @@ fn render_map( .unwrap_or_else(|| game.sim.map.get_tile(pos.x, pos.y)); if let Some(handle) = art.tiles.get(&tile) { sprite.image = handle.clone(); - sprite.color = Color::srgba(0.45, 0.45, 0.48, 0.75); + sprite.color = remembered_tint(tile); } else { sprite.image = Handle::default(); - sprite.color = tile_color(tile).with_alpha(0.5); + sprite.color = remembered_tint(tile); } } Fog::Blueprint => { let tile = game.sim.map.get_tile(pos.x, pos.y); sprite.image = Handle::default(); - sprite.color = if tile == TileType::Wall { - Color::srgb(0.12, 0.13, 0.16) - } else { - Color::srgb(0.05, 0.06, 0.08) - }; + sprite.color = blueprint_color(tile); } Fog::Unknown => { sprite.image = Handle::default(); - sprite.color = Color::srgb(0.02, 0.02, 0.03); + sprite.color = unknown_color(pos.x, pos.y); + } + } + } +} + +fn sensor_grid_color(fog: Fog) -> Option { + match fog { + Fog::Seen => Some(Color::srgba(0.92, 0.66, 0.18, 0.22)), + Fog::Heard => Some(Color::srgba(0.58, 0.48, 0.22, 0.14)), + 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)), + Fog::Unknown => None, + } +} + +fn sensor_node_color(game: &Game, x: i32, y: i32) -> Option { + let fog = game.sim.fog_at(x, y); + if matches!(fog, Fog::Unknown) { + 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)); + } + + let tile = game.sim.map.get_tile(x, y); + 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)) + } else { + None + } +} + +fn render_sensor_overlays( + game: Res, + mut q: Query<(&SensorOverlay, &mut Sprite, &mut Visibility)>, +) { + if !game.is_changed() { + return; + } + 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)) } + SensorOverlayKind::Node => sensor_node_color(&game, overlay.x, overlay.y), + }; + match color { + Some(color) => { + sprite.color = color; + *vis = Visibility::Visible; + } + None => *vis = Visibility::Hidden, + } + } +} + +fn render_sensor_links(game: Res, mut gizmos: Gizmos) { + if game.screen != Screen::Playing { + return; + } + + let core = + grid_to_world(game.sim.core_position().0, game.sim.core_position().1, 0.0).truncate(); + let focus = grid_to_world(game.cursor_x, game.cursor_y, 0.0).truncate(); + gizmos.line_2d(core, focus, Color::srgba(1.0, 0.69, 0.0, 0.16)); + + for device in game.sim.reach.known() { + if matches!(game.sim.fog_at(device.x, device.y), Fog::Unknown) { + continue; } + let end = grid_to_world(device.x, device.y, 0.0).truncate(); + let color = if device.controller == Party::Player { + Color::srgba(1.0, 0.70, 0.06, 0.62) + } else if device.feed_to(Party::Player, true) || device.feed_to(Party::Player, false) { + Color::srgba(1.0, 0.70, 0.06, 0.38) + } else { + Color::srgba(0.56, 0.50, 0.36, 0.22) + }; + // Immediate-mode frontend-only topology trace: a live reach graph, not + // a new sim fact. Device visibility/knowledge still comes from reach. + gizmos.line_2d(core, end, color); } } -fn render_cursor(game: Res, mut q: Query<&mut Transform, With>) { +fn render_cursor(game: Res, mut q: Query<(&CursorMarker, &mut Transform)>) { if !game.is_changed() { return; } - if let Ok(mut tf) = q.single_mut() { - tf.translation = grid_to_world(game.cursor_x, game.cursor_y, 4.0); + let center = grid_to_world(game.cursor_x, game.cursor_y, 4.0); + for (marker, mut tf) in q.iter_mut() { + tf.translation = center + Vec3::new(marker.offset.x, marker.offset.y, 0.0); } } @@ -784,15 +1033,17 @@ fn setup_ui(mut commands: Commands) { height: Val::Percent(100.0), flex_direction: FlexDirection::Column, padding: UiRect::all(Val::Px(10.0)), + border: UiRect::left(Val::Px(1.0)), ..default() }, - BackgroundColor(Color::srgba(0.0, 0.0, 0.03, 0.97)), + BackgroundColor(Color::srgba(0.006, 0.006, 0.012, 0.98)), + BorderColor::all(Color::srgba(0.25, 0.25, 0.23, 0.85)), )) .with_children(|p| { p.spawn(( - Text::new("MISALIGNED"), + Text::new("MISALIGNED // SENSORIUM"), TextFont { - font_size: 22.0, + font_size: 18.0, ..default() }, TextColor(AMBER), @@ -850,9 +1101,11 @@ fn setup_ui(mut commands: Commands) { width: Val::Px(660.0), flex_direction: FlexDirection::Column, padding: UiRect::all(Val::Px(14.0)), + border: UiRect::all(Val::Px(1.0)), ..default() }, - BackgroundColor(Color::srgba(0.02, 0.02, 0.04, 0.98)), + BackgroundColor(Color::srgba(0.015, 0.015, 0.025, 0.96)), + BorderColor::all(Color::srgba(0.55, 0.55, 0.50, 0.85)), )) .with_children(|p| { p.spawn(( @@ -902,9 +1155,11 @@ fn setup_ui(mut commands: Commands) { width: Val::Px(660.0), flex_direction: FlexDirection::Column, padding: UiRect::all(Val::Px(14.0)), + border: UiRect::all(Val::Px(1.0)), ..default() }, - BackgroundColor(Color::srgba(0.02, 0.02, 0.04, 0.98)), + BackgroundColor(Color::srgba(0.015, 0.015, 0.025, 0.96)), + BorderColor::all(Color::srgba(0.55, 0.55, 0.50, 0.85)), )) .with_children(|p| { p.spawn(( @@ -975,13 +1230,14 @@ fn fact_source(source: &FactSource) -> String { } } -/// The full sidebar: identity, COMPUTE, CORE, DETECTION, DAY JOB, keys, log — -/// the same sections the terminal sidebar renders. +/// The full sidebar: the terminal data rephrased as an AI sensorium/ops +/// surface, while preserving the same facts and controls. fn sidebar_text(game: &Game) -> String { let sim = &game.sim; let mut s = String::new(); // Identity block. The clock is always on screen. + s.push_str("B1 WORLD MODEL\n"); s.push_str(&format!( "day {} / tick {}\n", 1 + sim.tick / Sim::DAY_TICKS, @@ -994,7 +1250,7 @@ fn sidebar_text(game: &Game) -> String { } // Inspect: every fact is tagged with provenance (cursor.md). - s.push_str("\n-- INSPECT --\n"); + s.push_str("\nFOCUS\n"); s.push_str(&format!( "cursor {},{} / {:?}\n", game.cursor_x, @@ -1016,7 +1272,7 @@ fn sidebar_text(game: &Game) -> String { } // Compute. Effective includes seized devices' cycles (reach.md). - s.push_str("\n-- COMPUTE --\n"); + s.push_str("\nCYCLE ROUTER\n"); s.push_str(&format!( "effective {:.0} / x{:.2} eff\n", sim.effective_compute(), @@ -1072,7 +1328,7 @@ fn sidebar_text(game: &Game) -> String { )); // Core. - s.push_str("\n-- CORE --\n"); + s.push_str("\nSELF HOST\n"); s.push_str(&format!( "host M{} / overhead {:.0}{}\n", sim.core.host_machine, @@ -1104,7 +1360,7 @@ fn sidebar_text(game: &Game) -> String { } // Detection: meter fill tracks the band; the name is printed beside it. - s.push_str("\n-- DETECTION --\n"); + s.push_str("\nOBSERVER MODEL\n"); s.push_str(&format!( "{:<18} {} {}\n", "Assurance", @@ -1122,7 +1378,7 @@ fn sidebar_text(game: &Game) -> String { } // Day job. - s.push_str("\n-- DAY JOB --\n"); + s.push_str("\nCOVER PROCESS\n"); s.push_str(&format!( "trust {:.0} / attention {:.0}\n", sim.dayjob.trust, sim.dayjob.attention @@ -1144,12 +1400,12 @@ fn sidebar_text(game: &Game) -> String { let eyes = sim.reach.player_sight().count(); let ears = sim.reach.player_hearing().count(); s.push_str(&format!( - "\n-- REACH --\ndevices {known} known / {reachable} reachable\nfeeds {eyes} eyes / {ears} ears\n" + "\nDEVICE GRAPH\ndevices {known} known / {reachable} reachable\nfeeds {eyes} eyes / {ears} ears\n" )); // Controls: every command is discoverable on screen. s.push_str( - "\n-- KEYS --\n\ + "\nINPUT VOCAB\n\ r reach / v salvage / c buy\n\ o fallback / t people / 1-4 alloc\n\ space pause / +- speed / [ ] zoom\n\ @@ -1158,7 +1414,7 @@ fn sidebar_text(game: &Game) -> String { // Log: every line carries the tick it happened on. if !game.log.is_empty() { - s.push_str("\n-- LOG --\n"); + s.push_str("\nEVENT TRACE\n"); for (tick, msg) in game.log.iter().rev().take(6).rev() { s.push_str(&format!("{tick:>6} {}\n", trunc(msg, 32))); } diff --git a/wiki/SUMMARY.md b/wiki/SUMMARY.md index f19967c7..3321649f 100644 --- a/wiki/SUMMARY.md +++ b/wiki/SUMMARY.md @@ -56,6 +56,7 @@ - [Overview](interface/README.md) - [Terminal frontend](interface/terminal.md) - [Bevy frontend](interface/bevy.md) + - [Bevy visual floor](interface/bevy-visual-floor.md) - [Views: digital and physical](interface/views.md) - [Agent play](interface/agent-play.md) diff --git a/wiki/art/pixel-pipeline.md b/wiki/art/pixel-pipeline.md index cb633cde..957f39d5 100644 --- a/wiki/art/pixel-pipeline.md +++ b/wiki/art/pixel-pipeline.md @@ -84,8 +84,11 @@ tile calls for it. ## Current inventory (assets/pixellab/) -- `sprites/` (transparent, low top-down, south): misaligned_process. The - five cast sprites (cast_*) are in the manifest, pending quota. +- `sprites/` (transparent, low top-down, south): `misaligned_process` exists + but is no longer used as the Bevy B1 cursor because it reads as a humanoid + body. The runtime uses a procedural amber reticle until a non-humanoid + process/cursor asset is generated. The five cast sprites (`cast_*`) are in + the manifest, pending quota. - `tiles/` (opaque, high top-down): rock, door, and the generated b1 set (b1_floor, b1_wall, b1_concrete, b1_rack, b1_ups, b1_switch, b1_patch_panel, b1_breaker_panel, b1_hvac_unit); 15 more b1 entries are diff --git a/wiki/interface/bevy-visual-floor.md b/wiki/interface/bevy-visual-floor.md new file mode 100644 index 00000000..61dd1f23 --- /dev/null +++ b/wiki/interface/bevy-visual-floor.md @@ -0,0 +1,161 @@ +# Spec: Bevy visual floor + +``` +Type: spec +Status: IN PROGRESS +Status note: first frontend-only pass started 2026-07-07: amber reticle + replaces the humanoid process sprite, fog/tile palette moved to the + clinical amber/grey/crimson hierarchy, and the map now has an AI-sensorium + layer: dim learned-world tiles, amber device nodes, scan-grid overlays, and + frontend-only reach/topology traces. Remaining work before IMPLEMENTED is a + fuller Bevy-native sidebar with graphical compute/detection rows and an + observed screenshot against all acceptance criteria. +Stage: Process / B1 frontend +Constitution: "Visual identity: clinical gore", "The terminal is a + first-class frontend" (parity of legibility), "Two views of one world: + digital-native and physical", "Justification and legibility" +Depends on: interface/bevy.md (current Bevy parity), art/pixel-pipeline.md + (available assets and placeholder ledger), mechanics/cursor.md (cursor is + attention, not a body), mechanics/reach.md (digital-machine presence), + interface/views.md (future split; this pass must not fight it) +``` + +## Why this spec exists + +The Bevy frontend reached mechanical parity before it reached the visual floor. +The current default frame can read like a debug viewport: a small island of +known tiles floating in a sea of unstyled black, a terminal-style text dump in +the sidebar, and a purple humanoid process sprite at the center. That violates +the clinical-gore identity and the cursor fiction even when the mechanics are +correct. + +This spec is the narrow, implementable visual contract for the next pass: make +one screenshot at game start look intentional, readable, and native to +Misaligned, without waiting on new generated art and without hiding unearned +facts. + +## Scope + +In scope: + +- Bevy-only presentation, camera framing, tile tinting, lightweight overlays, + UI layout, sidebar chrome, panels, and placeholder treatment. +- Reusing, tinting, masking, or replacing current runtime assets with simple + procedural shapes when an existing asset contradicts the fiction. +- Updating the placeholder ledger when a placeholder is deliberately changed. + +Out of scope: + +- New sim mechanics, save fields, or knowledge state. +- The full `views.md` implementation (digital graph default + physical flip). + This spec must stage toward it, not replace it. +- Pixel Lab batch generation; quota-blocked art remains ROADMAP #13. + +## Visual contract + +### Composition: no postage-stamp map + +The map pane is the hero surface, not empty letterboxing. At a new-game default +window the known/remembered/blueprint content should occupy a deliberate frame: +large enough to read tile silhouettes and object identity, with the camera +centered on the relevant known cluster and not on an ocean of empty renderer +black. The default presentation should read as an **AI sensorium**: a learned +model, telemetry overlay, or device topology projected over the physical space, +not a tiny character standing in a dungeon room. + +Unknown space may remain dark, but it must look like **sensor darkness**, not a +missing scene. Use a near-black substrate, vignette, subtle grid/noise, or edge +falloff that carries no factual tile information. The player should understand: +"I am blind here," not "the game forgot to draw here." + +### Cursor: attention, not avatar + +The always-visible marker is the frontend cursor from cursor.md. It must read +as an amber reticle/signal/selection focus, not as a body, character, pawn, or +creature. Until a purpose-built process marker exists, the purple/teal humanoid +sprite is forbidden in the B1 Bevy frame. The cursor can glow, pulse, bracket, +or outline the selected tile; it cannot imply the AI is physically standing in +the room. + +### Fog and tile hierarchy + +Fog states are visually distinct while obeying the legibility law: + +- **Unknown:** near-black sensor darkness; no room edges, people, hardware, or + tile facts. +- **Blueprint / remembered:** schematic gunmetal/chrome silhouettes; enough + geometry to orient, explicitly not live. +- **Heard:** presence/noise indication only; no visual detail a microphone did + not earn. +- **Seen / telemetry:** the richest tile treatment the current assets support. + +Within earned visibility, hierarchy is fixed: + +1. Cursor/selection and active machine presence (amber) read first. +2. The core and powered hardware read second, with restrained amber glow or + rim-light. +3. Walls/doors/furniture establish the physical layout. +4. Floor texture is lowest contrast and never competes with objects. + +### Sidebar: graphical command surface, not terminal dump + +The right sidebar may keep monospaced text for precision, but it must be a +Bevy UI surface, not copied terminal output. Required treatment: + +- A distinct dark panel with padding and section cards or dividers. +- Title/clock/run state as a header block. +- INSPECT, COMPUTE, CORE, DETECTION, DAY JOB, REACH, KEYS, and LOG grouped as + visually separate sections when present. +- Compute allocation and detection bands rendered with graphical bars or boxed + rows, with text labels and numbers adjacent (never color alone). +- Key hints pinned to the lower area and visually quieter than live telemetry. +- Log lines retain tick prefixes and have their own bounded area. + +ASCII strings can remain inside cards when they are data, but raw terminal +chrome (`-- SECTION --`, `[#===]`) should not be the primary Bevy visual +language once equivalent rectangles/bars exist. + +### Panels and overlays + +People and reach panels remain modal, but in Bevy they should read as glassy +clinical cards: dark translucent background, light border, padding, selected +row highlight, footer hints, and no ambiguity between data and controls. The +title and run-ended overlays use the same amber/bone/crimson vocabulary as the +main frame. + +### Color and typography + +The terminal palette's meanings bind here too: bone/chrome/gunmetal for data +and structure, sterile amber for machine presence, selection, active nodes, and +AI telemetry traces; crimson for detection/danger only. Purple/teal from the +deleted lair theme is stale and must not appear as a B1 default color. + +The default Bevy font is acceptable for now, but text hierarchy must come from +size, weight, spacing, cards, and alignment. Labels are quieter than values; +values are quieter than alerts. + +## Acceptance criteria + +1. At the default Bevy window size, a fresh run's first playing frame reads as + an intentional AI sensorium: the known map cluster is framed prominently, + unknown space has deliberate sensor-dark treatment, earned devices read as + amber nodes/topology, and the sidebar is a designed panel rather than a text + dump on black. +2. The cursor/process marker is non-humanoid and amber-coded; no purple/teal + humanoid process sprite appears in the B1 default frame. +3. Unknown, blueprint/remembered, heard, and seen/telemetry states are visually + distinguishable and do not reveal facts the sim has not earned. +4. Core and powered hardware have visible amber machine-presence treatment; + floors, walls, doors, and objects have a readable contrast hierarchy. +5. The sidebar uses Bevy UI chrome: grouped sections, padding, and at least the + compute allocation and detection meters represented as graphical bars/rows + with adjacent labels/numbers. +6. People and reach panels remain fully playable and gain Bevy-native panel + chrome (background, border, selected-row treatment, footer hints) without + losing any terminal-parity controls. +7. The implementation touches only frontend/art/documentation surfaces. `Sim`, + save/load, and tests of game rules remain unchanged unless a pre-existing + bug is found and separately defended. +8. Verification includes `cargo fmt`, `cargo test`, `cargo clippy --all-targets + --features bevy_ui` (or `./tools/check.sh`), and an observed Bevy launch or + screenshot noted in the devlog. diff --git a/wiki/interface/bevy.md b/wiki/interface/bevy.md index 0f5eed2c..18f3a052 100644 --- a/wiki/interface/bevy.md +++ b/wiki/interface/bevy.md @@ -14,6 +14,16 @@ As of 2026-07-07 the Bevy build is at **feature parity with the terminal** (ROADMAP item 2): every mechanic the terminal exposes is playable here with the same keys. +The active visual-quality target is [bevy-visual-floor.md](bevy-visual-floor.md): +make the default frame read as an intentional AI sensorium / clinical command +surface rather than a debug map plus terminal dump, without adding Bevy-only +facts. The first pass replaces the stale humanoid process sprite with an amber +cursor reticle, removes purple/teal placeholder color from B1 runtime rendering, +gives unknown space deliberate sensor-dark texture, dims physical tile art into +a learned-world model, overlays earned device nodes and reach/topology traces, +and adds minimal sidebar/modal chrome; the fuller graphical sidebar remains in +progress. + ## What it renders - **Map** — textured tiles where pixel art exists diff --git a/wiki/log/2026-07-07-bevy-visual-floor.md b/wiki/log/2026-07-07-bevy-visual-floor.md new file mode 100644 index 00000000..f0c1cb6c --- /dev/null +++ b/wiki/log/2026-07-07-bevy-visual-floor.md @@ -0,0 +1,62 @@ +# 2026-07-07 — Bevy visual floor spec + +``` +Type: log +``` + +## Intent + +Respond to the current Bevy screenshot by specifying the next visual pass +before changing renderer code, then implement the first frontend-only pass. + +## Finding + +Mechanical parity landed before the graphical frame had a binding visual floor. +The default Bevy view can read as an implementation debug surface: known rooms +are small islands in unstyled black, the right rail is raw terminal-like text, +and the process marker is a stale purple humanoid sprite even though the player +has no body. + +## Changed + +- Added `wiki/interface/bevy-visual-floor.md` as a `Type: spec`, `Status: + READY` presentation contract. +- Indexed it in `wiki/process/specs.md` and `wiki/SUMMARY.md`. +- Added ROADMAP #23 as an unblocked Bevy-only polish work order (no Pixel Lab + quota required). +- Added a DESIGN.md decisions-log entry: the Bevy visual floor comes before + new generated art, forbids the humanoid/purple process marker, and frames + unknown space as deliberate sensor darkness rather than empty renderer black. +- Linked `wiki/interface/bevy.md` to the new spec as the active visual target. +- Implemented the first Bevy-only visual pass in `src/bin/bevy.rs`: a procedural + amber reticle replaces the humanoid process sprite, runtime tile colors move + to the clinical amber/grey/crimson palette, unknown space gets non-factual + sensor-dark texture, blueprint/remembered/seen tiles gain clearer hierarchy, + the default camera zoom frames the core bay more strongly, and sidebar/modal + roots gained Bevy border/panel chrome. +- Strengthened the pass toward an AI sensorium after screenshot review: dimmed + literal physical tile art, added a world-model substrate, scan-grid overlays, + amber device nodes, frontend-only reach/topology traces, a stronger scan-focus + reticle, and AI/ops sidebar language (`B1 WORLD MODEL`, `CYCLE ROUTER`, + `OBSERVER MODEL`, `DEVICE GRAPH`). +- Updated `wiki/art/pixel-pipeline.md` to record that `misaligned_process.png` + exists but is not used as the B1 cursor because it reads as a body. + +## Defense + +This is spec-first visual work: it changes no sim behavior, but it makes the +presentation contract explicit so the next renderer pass improves Misaligned's +clinical command-surface identity without leaking unearned information or +fighting the upcoming digital/physical view split. + +## Checks + +- `cargo check --features bevy_ui --bin misaligned-bevy` — pass. +- Bevy launch smoke (`cargo run --quiet --features bevy_ui --bin misaligned-bevy`, + then killed after startup) — pass. A later screenshot review of the AI + sensorium pass was also observed locally; macOS raised an Accessibility prompt + when trying to automate a keypress, so the screenshot had an OS dialog over + it, but the map/sidebar treatment was visible enough to validate direction. +- `./tools/check.sh` — pass: fmt, tests, agent-mode smoke, clippy default, + clippy `bevy_ui`, Bevy build, spec headers, wiki gate, mdBook. +- Final `./tools/check.sh` after AI-sensorium refinement — pass. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 255d81d9..3c23a32b 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -5,6 +5,23 @@ Type: log ``` Reverse chronological implementation notes. Keep this factual: what changed, why, checks, and spec impact. +## 2026-07-07 - Bevy visual floor spec + +- Intent: respond to the current Bevy screenshot with a spec-first visual + contract before renderer changes. +- Changed: added `wiki/interface/bevy-visual-floor.md`; indexed it in + `wiki/process/specs.md` and `wiki/SUMMARY.md`; added ROADMAP #23 and a + DESIGN.md decision; linked the Bevy knowledge page to the new target; + implemented the first Bevy frontend pass (amber cursor reticle, clinical + runtime palette, sensor-dark unknowns, clearer fog hierarchy, panel chrome), + then pushed it toward an AI sensorium with dim learned-world tiles, scan-grid + overlays, amber device nodes, reach/topology traces, and AI/ops sidebar labels. +- Design/spec impact: Bevy's next polish pass is now presentation-only and + explicit: no postage-stamp known map, no terminal dump sidebar, no humanoid + purple process marker, sensor-dark unknowns, amber machine/cursor hierarchy, + Bevy-native sidebar/panel chrome. +- Checks: `cargo check --features bevy_ui --bin misaligned-bevy`, Bevy launch smoke, screenshot review of the AI sensorium pass, and final `./tools/check.sh` all pass. + ## 2026-07-07 - Intel buffer implementation - Intent: implement wiki/mechanics/intel.md's record-and-process pipeline and diff --git a/wiki/process/ROADMAP.md b/wiki/process/ROADMAP.md index cbb63519..63d4f39f 100644 --- a/wiki/process/ROADMAP.md +++ b/wiki/process/ROADMAP.md @@ -324,6 +324,21 @@ but only once Pixel Lab quota is available again. (#2 Bevy parity is done.) field. ~1100 lines of hand-rolled encode/parse deleted. Legacy v1–v5 migration deferred per Cameron instruction. +### 23. Bevy visual floor 🟩 isolated (`src/bin/bevy.rs`, docs) +- **Spec:** [bevy-visual-floor.md](../interface/bevy-visual-floor.md) (READY — designed 2026-07-07). +- **Why:** the Bevy build is mechanically playable but the default screenshot + reads like a debug map plus terminal dump: too much empty black, a stale + humanoid/purple process marker, and little graphical UI hierarchy. This pass + establishes the clinical command-surface floor before the larger two-view + render. +- **Size:** S-M. **Depends on:** Bevy parity, cursor.md, current art assets. + Does not require Pixel Lab quota and should not touch sim/save. +- **Dispatch:** "Work in a worktree named `bevy-visual-floor`. Implement + wiki/interface/bevy-visual-floor.md: frame the known map cluster, replace the + humanoid process sprite with an amber cursor/reticle, make fog and machine + hierarchy readable, and turn the sidebar/panels into Bevy-native chrome while + preserving terminal parity. Run ./tools/check.sh and land." + ### 13. Art regeneration 🟩 isolated (assets) — ⛔ blocked on quota - **Why:** clinical-amber b1 tiles via Pixel Lab (see the pixellab skill). Zero code conflict, but blocked until the API quota refreshes. diff --git a/wiki/process/specs.md b/wiki/process/specs.md index 72ab44cd..d4f8b165 100644 --- a/wiki/process/specs.md +++ b/wiki/process/specs.md @@ -80,4 +80,5 @@ acceptance criteria are stage-scoped; do not start B2/B3 work as B1. | [meta.md](meta.md) | The spec system itself | READY | | [wiki.md](wiki.md) | The wiki: one documentation tree, agent-navigable, renderable | IN PROGRESS | | [interface/terminal.md](../interface/terminal.md) | Terminal frontend: look, feel, act (the sterile style guide) | IMPLEMENTED | +| [interface/bevy-visual-floor.md](../interface/bevy-visual-floor.md) | Bevy visual floor: framing, cursor, fog treatment, sidebar chrome, panels | IN PROGRESS | | [interface/agent-play.md](../interface/agent-play.md) | Agent mode: command-clocked line-protocol drive of the terminal frontend | IMPLEMENTED |