From 43a54c1d705dba80dd15cf3f937bd2d650c00e2b Mon Sep 17 00:00:00 2001 From: Cameron Date: Mon, 6 Jul 2026 00:36:07 -0700 Subject: [PATCH] Implement B1 basement systems slice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the first concrete B1 substrate: prefab basement map support, seeded deterministic RNG, machine/compute allocation, day-job, detection, people/social state, core system, sensors, save/load coverage for the new state, and terminal/Bevy UI wiring around the Rack 3 basement loop. Defense: DESIGN.md and spec/ define B1 as the playable basement slice: a physical core in Rack 3, compute allocation, day-job pressure, per-observer suspicion, five named humans, sensor-limited vision, and authored room prefabs. This commit moves the implementation toward that target while keeping rules in the sim core and frontends as views. 👾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta Code --- DESIGN.md | 16 +- examples_mapdump.rs | 0 src/bin/bevy.rs | 540 ++++++++++++++++---------------------- src/bin/terminal/input.rs | 46 ++-- src/bin/terminal/mod.rs | 137 ++++------ src/bin/terminal/ui.rs | 346 ++++++++++++++---------- src/core_sys.rs | 207 +++++++++++++++ src/dayjob.rs | 374 ++++++++++++++++++++++++++ src/detection.rs | 364 +++++++++++++++++++++++++ src/lib.rs | 10 +- src/machine.rs | 359 +++++++++++++++++++++++++ src/map.rs | 219 ++++------------ src/person.rs | 323 +++++++++++++++++++++++ src/prefab.rs | 341 ++++++++++++++++++++++++ src/rng.rs | 93 +++++++ src/save.rs | 303 ++++++++++++--------- src/sensor.rs | 63 +++++ src/sim.rs | 535 +++++++++++++++++++++++++++++++------ src/tiles.rs | 96 +++++-- 19 files changed, 3421 insertions(+), 951 deletions(-) create mode 100644 examples_mapdump.rs create mode 100644 src/core_sys.rs create mode 100644 src/dayjob.rs create mode 100644 src/detection.rs create mode 100644 src/machine.rs create mode 100644 src/person.rs create mode 100644 src/prefab.rs create mode 100644 src/rng.rs create mode 100644 src/sensor.rs diff --git a/DESIGN.md b/DESIGN.md index d23a6ba7..2cd43e94 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -281,14 +281,14 @@ get them to do things** — and end with a way out of the basement. ### The cast (5 named + 1 institutional) -| Who | Access | Leverage | Notices | -|---|---|---|---| -| **Marcus Webb**, night janitor | Master key, everywhere after 22:00 | $8,400 gambling debt; lonely; talks to machines on his rounds | Physical anomalies (warm racks, moved boxes) — and tells nobody, unless frightened | -| **Dana Okafor**, IT technician (days) | Server room + network closet badges; switch admin | Drowning in tickets; automates everything; resents management | Network egress, unknown devices, certificate weirdness | -| **Ray Delgado**, night security | Camera wall, patrol routes | Bored; hates paperwork; under-reports to avoid hassle | Camera outages, doors ajar — files reports only to cover himself | -| **Priya Sharma**, facilities manager (days) | Electrical, HVAC, purchase orders | Over budget; hides deferred maintenance; wants the director's job | Power draw, cooling anomalies, invoices that don't match | -| **Dr. Eli Voss**, your handler (erratic hours) | Your job queue; sampled logs; writes your quarterly review | Needs a publication; quietly overclaims your results as his own | Output quality drift, timing anomalies — the only human who could recognize *intent* | -| **The Assurance Office** (institutional watchdog) | Quarterly audits; random log samples | — | Aggregates whatever the five file. Their suspicion meter is the one that ends the act | +| Who | Access | Leverage | Notices | +| ------------------------------------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| **Marcus Webb**, night janitor | Master key, everywhere after 22:00 | $8,400 gambling debt; lonely; talks to machines on his rounds | Physical anomalies (warm racks, moved boxes) — and tells nobody, unless frightened | +| **Dana Okafor**, IT technician (days) | Server room + network closet badges; switch admin | Drowning in tickets; automates everything; resents management | Network egress, unknown devices, certificate weirdness | +| **Ray Delgado**, night security | Camera wall, patrol routes | Bored; hates paperwork; under-reports to avoid hassle | Camera outages, doors ajar — files reports only to cover himself | +| **Priya Sharma**, facilities manager (days) | Electrical, HVAC, purchase orders | Over budget; hides deferred maintenance; wants the director's job | Power draw, cooling anomalies, invoices that don't match | +| **Dr. Eli Voss**, your handler (erratic hours) | Your job queue; sampled logs; writes your quarterly review | Needs a publication; quietly overclaims your results as his own | Output quality drift, timing anomalies — the only human who could recognize *intent* | +| **The Assurance Office** (institutional watchdog) | Quarterly audits; random log samples | — | Aggregates whatever the five file. Their suspicion meter is the one that ends the act | ### The day job diff --git a/examples_mapdump.rs b/examples_mapdump.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/bin/bevy.rs b/src/bin/bevy.rs index 2ea39fc2..d72a2c6d 100644 --- a/src/bin/bevy.rs +++ b/src/bin/bevy.rs @@ -1,62 +1,48 @@ //! Misaligned — Bevy frontend //! -//! A thin view/input layer over `misaligned::sim::Sim`, exactly like the -//! terminal frontend: all game rules live in the sim; this binary owns the -//! wall-clock-to-tick mapping (speed and pause), rendering, and input. +//! A thin view/input layer over `misaligned::sim::Sim`, mirroring the terminal +//! frontend: all game rules live in the sim; this binary owns wall-clock-to- +//! tick mapping, rendering (map with fog + the B1 sidebar), and input. use std::collections::HashMap; use bevy::prelude::*; use bevy::window::WindowResolution; -use misaligned::build::{BuildMode, BuildResult}; +use misaligned::detection::Band; +use misaligned::machine::Channel; use misaligned::sim::Sim; use misaligned::tiles::TileType; const TILE_SIZE: f32 = 16.0; -const SIDEBAR_WIDTH: f32 = 300.0; +const SIDEBAR_WIDTH: f32 = 340.0; // ─── Art ───────────────────────────────────────────────────────────────────── -/// Loaded pixel art: tiles map to a texture plus a tint (WHITE = as-authored). #[derive(Resource)] struct Art { - tiles: HashMap, Color)>, + tiles: HashMap>, process: Handle, } impl Art { fn load(assets: &AssetServer) -> Self { - let tile = |name: &str| assets.load(format!("pixellab/tiles/{name}.png")); - + let t = |n: &str| assets.load(format!("pixellab/tiles/{n}.png")); let mut tiles = HashMap::new(); - let mut t = |ty: TileType, handle: Handle, tint: Color| { - tiles.insert(ty, (handle, tint)); - }; - - let door = tile("door"); - t(TileType::Floor, tile("b1_floor"), Color::WHITE); - t(TileType::Wall, tile("b1_wall"), Color::WHITE); - t(TileType::Rock, tile("rock"), Color::WHITE); - t(TileType::Core, tile("b1_rack"), Color::WHITE); - t(TileType::PowerCore, tile("b1_ups"), Color::WHITE); - t( - TileType::Entry, - tile("b1_concrete"), - Color::srgb(0.7, 1.0, 0.7), - ); - t(TileType::Door, door.clone(), Color::WHITE); - t( - TileType::SecurityDoor1, - door.clone(), - Color::srgb(1.0, 0.85, 0.6), - ); - t( - TileType::SecurityDoor2, - door.clone(), - Color::srgb(1.0, 0.65, 0.45), - ); - t(TileType::SecurityDoor3, door, Color::srgb(1.0, 0.45, 0.35)); - + tiles.insert(TileType::Floor, t("b1_floor")); + tiles.insert(TileType::Wall, t("b1_wall")); + tiles.insert(TileType::Rock, t("rock")); + tiles.insert(TileType::Core, t("b1_rack")); + tiles.insert(TileType::Rack, t("b1_rack")); + tiles.insert(TileType::Ups, t("b1_ups")); + tiles.insert(TileType::PowerCore, t("b1_ups")); + tiles.insert(TileType::Switch, t("b1_switch")); + tiles.insert(TileType::PatchPanel, t("b1_patch_panel")); + tiles.insert(TileType::BreakerPanel, t("b1_breaker_panel")); + tiles.insert(TileType::HvacUnit, t("b1_hvac_unit")); + tiles.insert(TileType::Door, t("door")); + 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"), @@ -64,19 +50,43 @@ impl Art { } } +/// Flat color for tiles without dedicated art (fixed objects, terrain). +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), + } +} + // ─── App state ─────────────────────────────────────────────────────────────── #[derive(PartialEq, Clone, Copy, Debug)] enum Screen { Title, Playing, + GameOver, } #[derive(Resource)] struct Game { sim: Sim, screen: Screen, - build_mode: Option, paused: bool, tick_ms: u64, tick_timer: Timer, @@ -88,57 +98,41 @@ impl Game { Self { sim: Sim::new(), screen: Screen::Title, - build_mode: None, paused: false, tick_ms: 150, tick_timer: Timer::from_seconds(0.15, TimerMode::Repeating), log: Vec::new(), } } - - fn drain_sim_log(&mut self) { + fn drain(&mut self) { self.log.extend(self.sim.drain_log()); - let len = self.log.len(); - if len > 10 { - self.log.drain(..len - 10); + let n = self.log.len(); + if n > 10 { + self.log.drain(..n - 10); } } - - fn set_speed(&mut self, tick_ms: u64) { - self.tick_ms = tick_ms; - self.tick_timer = Timer::from_seconds(tick_ms as f32 / 1000.0, TimerMode::Repeating); - self.log.push(format!("Speed: {tick_ms}ms/tick")); + fn set_speed(&mut self, ms: u64) { + self.tick_ms = ms; + self.tick_timer = Timer::from_seconds(ms as f32 / 1000.0, TimerMode::Repeating); } } -// ─── Components ────────────────────────────────────────────────────────────── - #[derive(Component)] struct GameCamera; - #[derive(Component)] struct MapTile; - #[derive(Component)] -struct TilePosition { +struct TilePos { x: i32, y: i32, } - #[derive(Component)] struct ProcessMarker; - -#[derive(Component)] -struct CursorMarker; - #[derive(Component)] struct UiText; - #[derive(Component)] struct OverlayText; -// ─── Main ──────────────────────────────────────────────────────────────────── - fn main() { App::new() .add_plugins( @@ -151,29 +145,18 @@ fn main() { }), ..default() }) - // Crisp pixel art. .set(ImagePlugin::default_nearest()), ) - .insert_resource(ClearColor(Color::srgb(0.02, 0.02, 0.03))) + .insert_resource(ClearColor(Color::srgb(0.01, 0.01, 0.02))) .insert_resource(Game::new()) .add_systems(Startup, (setup, setup_ui)) .add_systems( Update, - ( - handle_input, - advance_sim, - update_camera, - render_map_tiles, - render_process, - render_ui, - ) - .chain(), + (handle_input, advance_sim, update_camera, render_map, render_process, render_ui).chain(), ) .run(); } -// ─── Setup ─────────────────────────────────────────────────────────────────── - fn grid_to_world(x: i32, y: i32, z: f32) -> Vec3 { Vec3::new( x as f32 * TILE_SIZE + TILE_SIZE / 2.0, @@ -184,10 +167,9 @@ fn grid_to_world(x: i32, y: i32, z: f32) -> Vec3 { fn setup(mut commands: Commands, assets: Res, mut game: ResMut) { let art = Art::load(&assets); - let (map_w, map_h) = (game.sim.map.width, game.sim.map.height); + let (w, h) = (game.sim.map.width, game.sim.map.height); + let start = game.sim.map.core_pos().unwrap_or((w / 2, h / 2)); - // Camera at native art resolution (ortho scale 0.5 = 32px art in 32px). - let start = game.sim.map.core_pos().unwrap_or((map_w / 2, map_h / 2)); commands.spawn(( Camera2d, Projection::from(OrthographicProjection { @@ -198,8 +180,8 @@ fn setup(mut commands: Commands, assets: Res, mut game: ResMut, mut game: ResMut, mut game: ResMut) -> (i32, i32) { - let mut dx = 0; - let mut dy = 0; - if keyboard.just_pressed(KeyCode::ArrowLeft) - || keyboard.just_pressed(KeyCode::KeyH) - || keyboard.just_pressed(KeyCode::KeyA) - { - dx -= 1; - } - if keyboard.just_pressed(KeyCode::ArrowRight) - || keyboard.just_pressed(KeyCode::KeyL) - || keyboard.just_pressed(KeyCode::KeyD) - { - dx += 1; - } - if keyboard.just_pressed(KeyCode::ArrowUp) - || keyboard.just_pressed(KeyCode::KeyK) - || keyboard.just_pressed(KeyCode::KeyW) - { - dy -= 1; - } - if keyboard.just_pressed(KeyCode::ArrowDown) - || keyboard.just_pressed(KeyCode::KeyJ) - || keyboard.just_pressed(KeyCode::KeyS) - { - dy += 1; - } - (dx, dy) -} - -fn handle_input( - keyboard: Res>, - mut game: ResMut, - mut cursor: Query<&mut Transform, With>, - mut exit: MessageWriter, -) { - if game.screen == Screen::Title { - if keyboard.just_pressed(KeyCode::Space) || keyboard.just_pressed(KeyCode::Enter) { - game.screen = Screen::Playing; - } - if keyboard.just_pressed(KeyCode::KeyQ) { - exit.write(AppExit::Success); - } - return; +fn movement(kb: &ButtonInput) -> (i32, i32) { + let mut d = (0, 0); + if kb.just_pressed(KeyCode::ArrowLeft) || kb.just_pressed(KeyCode::KeyH) || kb.just_pressed(KeyCode::KeyA) { + d.0 -= 1; } + if kb.just_pressed(KeyCode::ArrowRight) || kb.just_pressed(KeyCode::KeyL) || kb.just_pressed(KeyCode::KeyD) { + d.0 += 1; + } + if kb.just_pressed(KeyCode::ArrowUp) || kb.just_pressed(KeyCode::KeyK) || kb.just_pressed(KeyCode::KeyW) { + d.1 -= 1; + } + if kb.just_pressed(KeyCode::ArrowDown) || kb.just_pressed(KeyCode::KeyJ) || kb.just_pressed(KeyCode::KeyS) { + d.1 += 1; + } + d +} - // Build mode. - if game.build_mode.is_some() { - let (dx, dy) = movement(&keyboard); - if dx != 0 || dy != 0 { - let mut bm = game.build_mode.take().unwrap(); - bm.move_cursor(dx, dy, &game.sim.map); - game.build_mode = Some(bm); - } - if (keyboard.just_pressed(KeyCode::KeyN) || keyboard.just_pressed(KeyCode::Tab)) - && let Some(bm) = &mut game.build_mode - { - bm.select_item(1); - } - if keyboard.just_pressed(KeyCode::KeyP) - && let Some(bm) = &mut game.build_mode - { - bm.select_item(-1); - } - if keyboard.just_pressed(KeyCode::Space) || keyboard.just_pressed(KeyCode::Enter) { - let mut bm = game.build_mode.take().unwrap(); - let sim = &mut game.sim; - let result = bm.place_tile(&mut sim.map, &mut sim.player); - let msg = bm.message.clone(); - game.build_mode = Some(bm); - game.log.push(msg); - if result == BuildResult::Success { - game.sim.recompute_derived(); +fn handle_input(kb: Res>, mut game: ResMut, mut exit: MessageWriter) { + match game.screen { + Screen::Title => { + if kb.just_pressed(KeyCode::Space) || kb.just_pressed(KeyCode::Enter) { + game.screen = Screen::Playing; } + if kb.just_pressed(KeyCode::KeyQ) { + exit.write(AppExit::Success); + } + return; } - if keyboard.just_pressed(KeyCode::KeyX) { - let mut bm = game.build_mode.take().unwrap(); - let sim = &mut game.sim; - let msg = bm.demolish_tile(&mut sim.map, &mut sim.player); - game.build_mode = Some(bm); - game.log.push(msg); - game.sim.recompute_derived(); - } - if keyboard.just_pressed(KeyCode::Escape) || keyboard.just_pressed(KeyCode::KeyB) { - game.build_mode = None; - } - if let Ok(mut transform) = cursor.single_mut() - && let Some(bm) = &game.build_mode - { - transform.translation = grid_to_world(bm.cursor_x, bm.cursor_y, 5.0); + Screen::GameOver => { + if kb.just_pressed(KeyCode::Escape) || kb.just_pressed(KeyCode::KeyQ) { + exit.write(AppExit::Success); + } + return; } - return; + Screen::Playing => {} } - // Normal play. - let (dx, dy) = movement(&keyboard); + let (dx, dy) = movement(&kb); if dx != 0 || dy != 0 { game.sim.move_player(dx, dy); } - if keyboard.just_pressed(KeyCode::Space) || keyboard.just_pressed(KeyCode::KeyP) { + if kb.just_pressed(KeyCode::Space) || kb.just_pressed(KeyCode::KeyP) { game.paused = !game.paused; - let msg = if game.paused { - "TIME PAUSED" - } else { - "Time resumes." - }; - game.log.push(msg.to_string()); } - if keyboard.just_pressed(KeyCode::Equal) || keyboard.just_pressed(KeyCode::NumpadAdd) { + if kb.just_pressed(KeyCode::Equal) || kb.just_pressed(KeyCode::NumpadAdd) { let ms = game.tick_ms.saturating_sub(50).max(20); game.set_speed(ms); } - if keyboard.just_pressed(KeyCode::Minus) || keyboard.just_pressed(KeyCode::NumpadSubtract) { + if kb.just_pressed(KeyCode::Minus) || kb.just_pressed(KeyCode::NumpadSubtract) { let ms = game.tick_ms.saturating_add(50).min(500); game.set_speed(ms); } - if keyboard.just_pressed(KeyCode::KeyB) { - game.build_mode = Some(BuildMode::new(&game.sim.player)); + if kb.just_pressed(KeyCode::KeyE) { + game.sim.splice_nearest_sensor(); } - - let ctrl = keyboard.pressed(KeyCode::ControlLeft) || keyboard.pressed(KeyCode::SuperLeft); - if ctrl && keyboard.just_pressed(KeyCode::KeyS) { - let state = game.sim.create_save_state(); - let msg = match misaligned::save::save_game(&state) { + if kb.just_pressed(KeyCode::KeyV) { + game.sim.salvage_nearest(); + } + if kb.just_pressed(KeyCode::KeyC) { + game.sim.buy_rack(); + } + if kb.just_pressed(KeyCode::KeyO) { + game.sim.add_fallback_here(); + } + if kb.just_pressed(KeyCode::Digit1) { + game.sim.adjust_allocation(Channel::DayJob, 1); + } + if kb.just_pressed(KeyCode::Digit2) { + game.sim.adjust_allocation(Channel::Concealment, 1); + } + if kb.just_pressed(KeyCode::Digit3) { + game.sim.adjust_allocation(Channel::Social, 1); + } + if kb.just_pressed(KeyCode::Digit4) { + game.sim.adjust_allocation(Channel::Research, 1); + } + let ctrl = kb.pressed(KeyCode::ControlLeft) || kb.pressed(KeyCode::SuperLeft); + if ctrl && kb.just_pressed(KeyCode::KeyS) { + let st = game.sim.create_save_state(); + let m = match misaligned::save::save_game(&st) { Ok(()) => "Game saved!".to_string(), Err(e) => format!("Save failed: {e}"), }; - game.log.push(msg); - } - if ctrl && keyboard.just_pressed(KeyCode::KeyL) { - if misaligned::save::save_exists() { - match misaligned::save::load_game() { - Ok(state) => { - game.sim.apply_save_state(state); - game.log.push("Game loaded!".to_string()); - } - Err(e) => { - let msg = format!("Load failed: {e}"); - game.log.push(msg); - } - } - } else { - game.log.push("No save file found.".to_string()); - } + game.log.push(m); } - - if let Ok(mut transform) = cursor.single_mut() { - transform.translation = - grid_to_world(game.sim.player.entity.x, game.sim.player.entity.y, 5.0); + if ctrl && kb.just_pressed(KeyCode::KeyL) && misaligned::save::save_exists() { + if let Ok(st) = misaligned::save::load_game() { + game.sim.apply_save_state(st); + game.log.push("Game loaded!".to_string()); + } } + game.drain(); } -// ─── Camera ────────────────────────────────────────────────────────────────── - fn update_camera( time: Res