diff --git a/crates/misaligned-bevy/src/main.rs b/crates/misaligned-bevy/src/main.rs index cb4e1a54..7378ecfc 100644 --- a/crates/misaligned-bevy/src/main.rs +++ b/crates/misaligned-bevy/src/main.rs @@ -1120,12 +1120,12 @@ impl Game { } fn move_cursor(&mut self, dx: i32, dy: i32) { - self.cursor_x = (self.cursor_x + dx).clamp(0, self.sim.map.width - 1); - self.cursor_y = (self.cursor_y + dy).clamp(0, self.sim.map.height - 1); + self.cursor_x = (self.cursor_x + dx).clamp(0, self.sim.map().width - 1); + self.cursor_y = (self.cursor_y + dy).clamp(0, self.sim.map().height - 1); } fn set_cursor(&mut self, x: i32, y: i32) { - self.cursor_x = x.clamp(0, self.sim.map.width - 1); - self.cursor_y = y.clamp(0, self.sim.map.height - 1); + self.cursor_x = x.clamp(0, self.sim.map().width - 1); + self.cursor_y = y.clamp(0, self.sim.map().height - 1); } fn add_log(&mut self, tick: u64, msg: &str) { self.add_event(LogEvent { @@ -3013,7 +3013,7 @@ fn grid_to_world(x: i32, y: i32, z: f32) -> Vec3 { } fn setup(mut commands: Commands, mut game: ResMut, harness: Option>) { - let (w, h) = (game.sim.map.width, game.sim.map.height); + let (w, h) = (game.sim.map().width, game.sim.map().height); // A screenshot scenario may have staged its own cursor anchor // (dev_shot_scenario runs before the app starts); don't clobber it. if harness.is_none() { @@ -3607,8 +3607,8 @@ fn sync_machines_3d( let shell_materials = reg.shell_materials.clone(); let mut desired: HashMap<(i32, i32), ChassisVisual> = HashMap::new(); - for y in 0..game.sim.map.height { - for x in 0..game.sim.map.width { + for y in 0..game.sim.map().height { + for x in 0..game.sim.map().width { let tile = known_tile(&game, x, y); if !chassis_3d_tile(tile) { continue; @@ -3879,8 +3879,8 @@ fn known_tile(game: &Game, x: i32, y: i32) -> TileType { .remembered .get(&(x, y)) .map(|m| m.tile) - .unwrap_or_else(|| game.sim.map.get_tile(x, y)), - _ => game.sim.map.get_tile(x, y), + .unwrap_or_else(|| game.sim.map().get_tile(x, y)), + _ => game.sim.map().get_tile(x, y), } } @@ -4110,7 +4110,7 @@ fn setup_3d( config.render_layers = layer.clone(); config.line.width = 2.0; - let (w, h) = (game.sim.map.width, game.sim.map.height); + let (w, h) = (game.sim.map().width, game.sim.map().height); let core = game.sim.core_position(); let root = commands @@ -4192,7 +4192,7 @@ fn setup_3d( for y in 0..h { for x in 0..w { - let tile = game.sim.map.get_tile(x, y); + let tile = game.sim.map().get_tile(x, y); if blocky_tile(tile) { // Structural mass: one box per tile; restyle_3d swaps between // the full and cutaway mesh variants as fog earns interiors. @@ -5095,8 +5095,8 @@ fn render_real_links( ra_y, rb_x, rb_y, - game.sim.map.width, - game.sim.map.height, + game.sim.map().width, + game.sim.map().height, ) else { continue; }; @@ -5164,18 +5164,18 @@ fn material_feel_route( start: (i32, i32), end: (i32, i32), ) -> Vec<((i32, i32), (i32, i32))> { - let start_is_switch = sim.map.get_tile(start.0, start.1) == TileType::Switch; - let end_is_switch = sim.map.get_tile(end.0, end.1) == TileType::Switch; + let start_is_switch = sim.map().get_tile(start.0, start.1) == TileType::Switch; + let end_is_switch = sim.map().get_tile(end.0, end.1) == TileType::Switch; let local = if start_is_switch && sim - .map + .map() .room_at(end.0, end.1) .is_some_and(|room| room.name == "server_room") { Some(end) } else if end_is_switch && sim - .map + .map() .room_at(start.0, start.1) .is_some_and(|room| room.name == "server_room") { @@ -5187,11 +5187,11 @@ fn material_feel_route( return vec![(start, end)]; }; let Some(relay) = sim - .map + .map() .tiles_of_type(TileType::PatchPanel) .into_iter() .find(|&(x, y)| { - sim.map + sim.map() .room_at(x, y) .is_some_and(|room| room.name == "server_room") }) @@ -5411,8 +5411,8 @@ fn render_work_routes( } else { clock.timer.fraction().clamp(0.0, 1.0) }; - let w = game.sim.map.width; - let h = game.sim.map.height; + let w = game.sim.map().width; + let h = game.sim.map().height; for hop in game.sim.work_in_flight() { if hop.amount <= f32::EPSILON { continue; @@ -6404,7 +6404,7 @@ fn render_map(game: Res, mut q: Query<(&mut Sprite, &TilePos), With, mut q: Query<(&mut Sprite, &TilePos), With { @@ -6426,7 +6426,7 @@ fn render_map(game: Res, mut q: Query<(&mut Sprite, &TilePos), With unknown_color(pos.x, pos.y), @@ -6502,7 +6502,7 @@ fn sensor_node_color(game: &Game, x: i32, y: i32) -> Option { return None; } - let tile = game.sim.map.get_tile(x, y); + 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) { @@ -8641,8 +8641,8 @@ mod digital_readability_tests { assert!(rack_overlay_color(&game, core.0, core.1, true).is_some()); assert_eq!(digital_focus_label(&game).as_deref(), Some("RACK / OWNED")); - let foreign = (0..game.sim.map.height) - .flat_map(|y| (0..game.sim.map.width).map(move |x| (x, y))) + 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()); @@ -11065,7 +11065,7 @@ mod flat_materials { let sim = Sim::new(); for &(x, y) in FOUNDATION_FLOOR_LIGHTS { assert_eq!( - sim.map.get_tile(x, y), + sim.map().get_tile(x, y), TileType::Floor, "fixture ({x}, {y}) must stay on an authored walkable aisle tile" ); @@ -11123,11 +11123,11 @@ mod flat_materials { .map(|device| (device.x, device.y)) .unwrap(); let relay = sim - .map + .map() .tiles_of_type(TileType::PatchPanel) .into_iter() .find(|&(x, y)| { - sim.map + sim.map() .room_at(x, y) .is_some_and(|room| room.name == "server_room") }) @@ -11142,7 +11142,7 @@ mod flat_materials { vec![(monitor, relay), (relay, switch)] ); assert_eq!( - sim.map + sim.map() .room_at(switch.0, switch.1) .map(|room| room.name.as_str()), Some("network_closet"), diff --git a/crates/misaligned-bevy/src/thought_effects.rs b/crates/misaligned-bevy/src/thought_effects.rs index 58f67043..9cff2d22 100644 --- a/crates/misaligned-bevy/src/thought_effects.rs +++ b/crates/misaligned-bevy/src/thought_effects.rs @@ -69,8 +69,8 @@ pub(super) fn sync_game_thought_effects( route.from_y, route.to_x, route.to_y, - game.sim.map.width, - game.sim.map.height, + game.sim.map().width, + game.sim.map().height, ) else { continue; }; diff --git a/crates/misaligned-core/src/actions.rs b/crates/misaligned-core/src/actions.rs index de5aa196..ae34cb80 100644 --- a/crates/misaligned-core/src/actions.rs +++ b/crates/misaligned-core/src/actions.rs @@ -1389,9 +1389,9 @@ impl Sim { // Tile-identity verbs: the player must know what the tile is. let tile_known = match fog { - Fog::Seen | Fog::Blueprint => Some(self.map.get_tile(x, y)), + Fog::Seen | Fog::Blueprint => Some(self.world.map().get_tile(x, y)), Fog::Remembered => self.remembered.get(&(x, y)).map(|m| m.tile), - Fog::Unknown if growable => Some(self.map.get_tile(x, y)), + Fog::Unknown if growable => Some(self.world.map().get_tile(x, y)), _ => None, }; if let Some(tile) = tile_known { @@ -1867,11 +1867,11 @@ impl Sim { }); // FAVOR: willing assets who can reach both ends. for p in &self.people.people { - let room_a = self.map.room_at( + let room_a = self.world.map().room_at( self.reach.device(a).map(|d| d.x).unwrap_or(0), self.reach.device(a).map(|d| d.y).unwrap_or(0), ); - let room_b = self.map.room_at( + let room_b = self.world.map().room_at( self.reach.device(b).map(|d| d.x).unwrap_or(0), self.reach.device(b).map(|d| d.y).unwrap_or(0), ); @@ -3292,7 +3292,7 @@ mod tests { fn unknown_tiles_expose_nothing() { let s = sim(); let (dx, dy) = s - .map + .map() .tiles_of_type(TileType::DeadEquipment) .first() .copied() @@ -3305,8 +3305,8 @@ mod tests { } // Find some Unknown floor tile that is not a feel pad or known device. let mut checked = false; - for y in 0..s.map.height { - for x in 0..s.map.width { + for y in 0..s.map().height { + for x in 0..s.map().width { if s.fog_at(x, y) == Fog::Unknown && !s.compute.machines.iter().any(|m| m.x == x && m.y == y) && !s.is_growable_bay(x, y) diff --git a/crates/misaligned-core/src/lib.rs b/crates/misaligned-core/src/lib.rs index b78269a7..63d91ca5 100644 --- a/crates/misaligned-core/src/lib.rs +++ b/crates/misaligned-core/src/lib.rs @@ -37,3 +37,4 @@ pub mod sinks; pub mod tiles; pub mod ui_projection; pub mod work_grid; +pub mod world; diff --git a/crates/misaligned-core/src/save.rs b/crates/misaligned-core/src/save.rs index 0ddae219..5d4fc8e4 100644 --- a/crates/misaligned-core/src/save.rs +++ b/crates/misaligned-core/src/save.rs @@ -309,19 +309,19 @@ pub struct SaveState { impl SaveState { pub fn from_sim(sim: &crate::sim::Sim) -> Self { let mut map_tiles = Vec::new(); - for y in 0..sim.map.height { - for x in 0..sim.map.width { - map_tiles.push(sim.map.get_tile(x, y)); + for y in 0..sim.map().height { + for x in 0..sim.map().width { + map_tiles.push(sim.map().get_tile(x, y)); } } Self { version: SAVE_VERSION, money: sim.player.money, accounts: sim.accounts.clone(), - map_width: sim.map.width, - map_height: sim.map.height, + map_width: sim.map().width, + map_height: sim.map().height, map_tiles, - map_powered: sim.map.powered.clone(), + map_powered: sim.map().powered.clone(), sim_tick: sim.tick, rng_state: sim.rng.state(), game_over: sim.game_over, @@ -371,12 +371,15 @@ impl SaveState { } pub fn apply_to(&self, sim: &mut crate::sim::Sim) { - sim.map = crate::map::GameMap::from_tiles( + // Criterion 1: saves carry a single basement map; load wraps it as + // plane 0 of a one-plane world. The multi-plane save format is + // criterion 6 (zplanes.md). + sim.world = crate::world::World::from_basement_map(crate::map::GameMap::from_tiles( self.map_width, self.map_height, self.map_tiles.clone(), self.map_powered.clone(), - ); + )); sim.player = crate::entities::Player::new(); sim.player.money = self.money; sim.accounts = self.accounts.clone(); @@ -1268,7 +1271,7 @@ mod tests { fn characterization_fixture() -> Sim { let mut sim = Sim::with_seed(0x5eed); - sim.map.powered.extend([(3, 7), (11, 5), (2, 19)]); + sim.map_mut().powered.extend([(3, 7), (11, 5), (2, 19)]); for remembered in [ RememberedTile { x: 12, diff --git a/crates/misaligned-core/src/sim/communications.rs b/crates/misaligned-core/src/sim/communications.rs index 6337725d..83bfa2ab 100644 --- a/crates/misaligned-core/src/sim/communications.rs +++ b/crates/misaligned-core/src/sim/communications.rs @@ -902,7 +902,7 @@ impl Sim { for (id, name, x, y, online) in changes { self.record_raw_intel( "telemetry", - self.map.room_at(x, y).map(|r| r.name.clone()), + self.world.map().room_at(x, y).map(|r| r.name.clone()), x, y, None, @@ -953,7 +953,8 @@ impl Sim { if let Some((_id, feed, x, y)) = sight_feed.clone().or(hearing_feed) { let seen = sight_feed.is_some(); let (record_x, record_y) = if seen { - self.map + self.world + .map() .room_named(prev_room) .map(|room| room.center()) .unwrap_or((x, y)) @@ -985,7 +986,8 @@ impl Sim { { let (source_device, feed_name, x, y) = feed; let (record_x, record_y) = if seen_here { - self.map + self.world + .map() .room_named(room) .map(|room| room.center()) .unwrap_or((x, y)) diff --git a/crates/misaligned-core/src/sim/economy.rs b/crates/misaligned-core/src/sim/economy.rs index 0e95150a..2c4ba07d 100644 --- a/crates/misaligned-core/src/sim/economy.rs +++ b/crates/misaligned-core/src/sim/economy.rs @@ -54,7 +54,8 @@ impl Sim { ok } pub(super) fn empty_rack_bay(&self) -> (i32, i32) { - self.map + self.world + .map() .tiles_of_type(TileType::Rack) .into_iter() .find(|(x, y)| !self.compute.machines.iter().any(|m| m.x == *x && m.y == *y)) @@ -151,7 +152,7 @@ impl Sim { OperationsTarget::Persona(persona_id), ); } - let powered = self.map.powered.clone(); + let powered = self.world.map().powered.clone(); for m in &mut self.compute.machines { if m.down_for == 0 { m.online = powered.contains(&(m.x, m.y)); @@ -614,14 +615,14 @@ impl Sim { /// Act One completion latch: the state must survive until a clear /// Assurance audit before `act_one_complete` becomes durable. pub fn act_one_quiet_exit_qualified(&self) -> bool { - let vision_beyond_server = self - .map - .room_named("server_room") - .is_some_and(|server_room| { - self.reach - .player_sight() - .any(|device| !server_room.contains(device.x, device.y)) - }); + let vision_beyond_server = + self.map() + .room_named("server_room") + .is_some_and(|server_room| { + self.reach + .player_sight() + .any(|device| !server_room.contains(device.x, device.y)) + }); vision_beyond_server && self.people.assets().any(|person| { person @@ -852,7 +853,7 @@ impl Sim { let (x, y) = self.core_position(); self.record_raw_intel( feed, - self.map.room_at(x, y).map(|r| r.name.clone()), + self.world.map().room_at(x, y).map(|r| r.name.clone()), x, y, None, diff --git a/crates/misaligned-core/src/sim/mod.rs b/crates/misaligned-core/src/sim/mod.rs index f9483e40..abcbbb63 100644 --- a/crates/misaligned-core/src/sim/mod.rs +++ b/crates/misaligned-core/src/sim/mod.rs @@ -62,6 +62,7 @@ use crate::tiles::TileType; use crate::work_grid::{ MachineIntensity, MachineMode, TokenFamily, TokenMove, WorkGrid, WorkQueues, }; +use crate::world::World; mod carrier; mod communications; @@ -350,7 +351,10 @@ macro_rules! trace_advance_phase { } pub struct Sim { - pub map: GameMap, + /// The world as a stack of planes (zplanes.md). Plane 0 is the basement; + /// B1 systems read it through `map()`/`map_mut()`. Criterion 1: this is a + /// one-plane world, so the substrate is unchanged for every existing test. + pub world: World, pub player: Player, pub rng: Rng, pub tick: u64, @@ -557,6 +561,17 @@ pub enum Nudge { } impl Sim { + /// The active plane's map (zplanes.md). Until presence spreads across + /// planes (criterion 3) this is always the basement, plane 0. + pub fn map(&self) -> &GameMap { + self.world.map() + } + + /// Mutable access to the active plane's map. + pub fn map_mut(&mut self) -> &mut GameMap { + self.world.map_mut() + } + pub fn new() -> Self { Self::with_seed(DEFAULT_SEED) } @@ -616,7 +631,7 @@ impl Sim { let reach = ReachNet::basement(&map); let mut sim = Self { - map, + world: World::from_basement_map(map), player, rng: Rng::new(seed), tick: 0, @@ -743,7 +758,7 @@ impl Sim { .iter() .find(|m| m.id == self.core.host_machine) .map(|m| (m.x, m.y)) - .or_else(|| self.map.core_pos()) + .or_else(|| self.world.map().core_pos()) .unwrap_or((0, 0)) } @@ -918,8 +933,8 @@ impl Sim { // ── Derived state ────────────────────────────────────────────────────── pub fn recompute_derived(&mut self) { - let cores = self.map.tiles_of_type(TileType::PowerCore); - self.map.update_power(&cores); + let cores = self.world.map().tiles_of_type(TileType::PowerCore); + self.world.map_mut().update_power(&cores); let power_gen = cores.len() as i32 * TileType::PowerCore.power_gen(); let machine_draw = self.compute.total_power_draw(); diff --git a/crates/misaligned-core/src/sim/perception.rs b/crates/misaligned-core/src/sim/perception.rs index 9aa2f424..f71f01f2 100644 --- a/crates/misaligned-core/src/sim/perception.rs +++ b/crates/misaligned-core/src/sim/perception.rs @@ -50,7 +50,8 @@ impl Sim { return Some(Anchor::Person(id)); } let room = room?; - self.map + self.world + .map() .room_named(room) .map(|r| r.center()) .map(|(x, y)| Anchor::Tile { x, y }) @@ -75,11 +76,11 @@ impl Sim { self.reconcile_device_tap_sinks(); let mut seen = HashSet::new(); for d in self.player_feed_devices(true) { - d.cover_sight_into(&mut seen, &self.map); + d.cover_sight_into(&mut seen, self.world.map()); } let mut heard = HashSet::new(); for d in self.player_feed_devices(false) { - d.cover_into(&mut heard, &self.map); + d.cover_into(&mut heard, self.world.map()); } self.seen = seen; self.heard = heard; @@ -96,7 +97,7 @@ impl Sim { RememberedTile { x, y, - tile: self.map.get_tile(x, y), + tile: self.world.map().get_tile(x, y), last_seen: self.tick, }, ); @@ -122,7 +123,7 @@ impl Sim { room_name: &str, sight: bool, ) -> Option<(u32, String, i32, i32)> { - let room = self.map.room_named(room_name)?; + let room = self.world.map().room_named(room_name)?; self.player_feed_devices(sight) .find(|d| { if !sight { @@ -130,7 +131,9 @@ impl Sim { } for y in room.y..room.y + room.h { for x in room.x..room.x + room.w { - if !self.map.blocks_sight(x, y) && d.sees_tile(x, y, &self.map) { + if !self.world.map().blocks_sight(x, y) + && d.sees_tile(x, y, self.world.map()) + { return true; } } @@ -174,7 +177,7 @@ impl Sim { match fog { Fog::Seen => { - let tile = self.map.get_tile(x, y); + let tile = self.world.map().get_tile(x, y); fact!("tile", tile.name(), FactSource::Seen); if let Some(site) = self.rack_site_at(x, y) { let state = match site { @@ -241,7 +244,7 @@ impl Sim { } } Fog::Blueprint => { - let tile = self.map.get_tile(x, y); + let tile = self.world.map().get_tile(x, y); // Blueprint is topology (walls/doors/floors), not hardware. // Machine chassis stay unnamed here — owned machines still // answer through the telemetry block below (cursor.md). @@ -442,17 +445,21 @@ impl Sim { /// bays in a room that already hosts an owned machine. Foreign empty /// slots stay dark. pub fn is_growable_bay(&self, x: i32, y: i32) -> bool { - if !matches!(self.map.get_tile(x, y), TileType::Rack | TileType::Core) { + if !matches!( + self.world.map().get_tile(x, y), + TileType::Rack | TileType::Core + ) { return false; } if self.compute.machines.iter().any(|m| m.x == x && m.y == y) { return false; } - let Some(room) = self.map.room_at(x, y) else { + let Some(room) = self.world.map().room_at(x, y) else { return false; }; self.compute.machines.iter().any(|m| { - self.map + self.world + .map() .room_at(m.x, m.y) .is_some_and(|r| r.name == room.name) }) @@ -470,10 +477,11 @@ impl Sim { if !self.feel_floor_is_earned() { return Vec::new(); } - self.map + self.world + .map() .tiles_of_type(TileType::Rack) .into_iter() - .chain(self.map.tiles_of_type(TileType::Core)) + .chain(self.world.map().tiles_of_type(TileType::Core)) .filter(|&(x, y)| self.is_growable_bay(x, y)) .collect() } @@ -550,7 +558,7 @@ impl Sim { /// The person's current position (their room's center), or None off-site. pub fn person_pos(&self, id: u8) -> Option<(i32, i32)> { let room = self.person_room(id)?; - self.map.room_named(room).map(|r| r.center()) + self.world.map().room_named(room).map(|r| r.center()) } pub(super) fn endpoint_room_pos( @@ -559,13 +567,13 @@ impl Sim { ) -> (Option, i32, i32) { if let Some(id) = endpoint.person() && let Some(room_name) = self.person_room(id) - && let Some(room) = self.map.room_named(room_name) + && let Some(room) = self.world.map().room_named(room_name) { let (x, y) = room.center(); return (Some(room_name.to_string()), x, y); } let (x, y) = self.core_position(); - (self.map.room_at(x, y).map(|r| r.name.clone()), x, y) + (self.world.map().room_at(x, y).map(|r| r.name.clone()), x, y) } pub(super) fn observer_by_id(&self, id: u8) -> Option<&crate::detection::Observer> { @@ -598,10 +606,10 @@ impl Sim { if dx * dx + dy * dy > s.radius * s.radius { continue; } - if sight && self.map.blocks_sight(x, y) { + if sight && self.world.map().blocks_sight(x, y) { continue; } - if !sight || s.sees_tile(x, y, &self.map) { + if !sight || s.sees_tile(x, y, self.world.map()) { return true; } } @@ -613,7 +621,7 @@ impl Sim { fn sense_covers_person(&self, id: u8, sight: bool) -> bool { match self.person_room(id) { None => false, - Some(name) => match self.map.room_named(name) { + Some(name) => match self.world.map().room_named(name) { Some(room) => self.coverage_intersects_room(room, sight), None => false, }, diff --git a/crates/misaligned-core/src/sim/reach_build.rs b/crates/misaligned-core/src/sim/reach_build.rs index 167c5fc2..d5a56da7 100644 --- a/crates/misaligned-core/src/sim/reach_build.rs +++ b/crates/misaligned-core/src/sim/reach_build.rs @@ -50,7 +50,7 @@ impl Sim { core: machine.id == self.core.host_machine, }); } - match self.map.get_tile(x, y) { + match self.world.map().get_tile(x, y) { TileType::Rack => Some(RackSite::Commissionable), TileType::ForeignRack => Some(RackSite::Foreign { powered: true }), TileType::DeadRack => Some(RackSite::Dead), @@ -889,7 +889,7 @@ impl Sim { fn device_room_name(&self, id: u32) -> Option { let d = self.reach.device(id)?; - self.map.room_at(d.x, d.y).map(|r| r.name.clone()) + self.world.map().room_at(d.x, d.y).map(|r| r.name.clone()) } fn refresh_intent_statuses(&mut self) { @@ -1177,12 +1177,12 @@ impl Sim { pub fn salvage_nearest_to(&mut self, px: i32, py: i32) -> bool { let target = [TileType::DeadEquipment, TileType::DeadRack] .into_iter() - .flat_map(|tile| self.map.tiles_of_type(tile)) + .flat_map(|tile| self.world.map().tiles_of_type(tile)) .min_by_key(|(x, y)| (x - px).abs() + (y - py).abs()); if let Some((x, y)) = target { - let corpse = self.map.get_tile(x, y) == TileType::DeadRack; + let corpse = self.world.map().get_tile(x, y) == TileType::DeadRack; if !corpse { - self.map.set_tile(x, y, TileType::Floor); + self.world.map_mut().set_tile(x, y, TileType::Floor); } let reliability = 0.4 + self.rng.f32() * 0.4; let machine_id = self.compute.add_machine( @@ -1390,7 +1390,7 @@ impl Sim { .devices .iter() .filter(|d| d.controller == Party::Player) - .map(|d| self.map.get_tile(d.x, d.y).security_level()) + .map(|d| self.world.map().get_tile(d.x, d.y).security_level()) .max() .unwrap_or(0); self.badge_access.max(controller) @@ -1408,10 +1408,10 @@ impl Sim { /// movement (basement-map.md criterion 3). fn badge_room_block(&self, actor: &str, access: i32, rooms: &[&str]) -> Option { for name in rooms { - let Some(room) = self.map.room_named(name) else { + let Some(room) = self.world.map().room_named(name) else { continue; }; - let tier = self.map.room_entry_tier(room); + let tier = self.world.map().room_entry_tier(room); if tier > access { return Some(format!( "{actor} can't badge into the {name} (tier {tier}, held tier {access})" diff --git a/crates/misaligned-core/src/sim/social_plot.rs b/crates/misaligned-core/src/sim/social_plot.rs index 907240dc..939aa299 100644 --- a/crates/misaligned-core/src/sim/social_plot.rs +++ b/crates/misaligned-core/src/sim/social_plot.rs @@ -840,7 +840,7 @@ impl Sim { .iter() .filter(|p| { self.person_room(p.id) - .and_then(|name| self.map.room_named(name)) + .and_then(|name| self.world.map().room_named(name)) .map(|r| r.contains(x, y)) .unwrap_or(false) }) @@ -920,7 +920,8 @@ impl Sim { fn device_room(&self, device: u32) -> Option<&str> { let device = self.reach.device(device)?; - self.map + self.world + .map() .room_at(device.x, device.y) .map(|room| room.name.as_str()) } @@ -942,7 +943,7 @@ impl Sim { let person = self.people.get(id)?; let actor_access = person.access; let enterable = - |s: &Sim, d: &crate::reach::Device| s.map.entry_tier_at(d.x, d.y) <= actor_access; + |s: &Sim, d: &crate::reach::Device| s.map().entry_tier_at(d.x, d.y) <= actor_access; let needs_wiring = |d: &crate::reach::Device| { let sight_wired = !d.sees || d.feed_to(Party::Player, true); let hearing_wired = !d.hears || d.feed_to(Party::Player, false); @@ -980,7 +981,7 @@ impl Sim { .map(|d| AssetTaskTarget::Device(d.id)), AssetTask::CloneBadge => { let (x, y) = self.core_position(); - let room = self.map.room_at(x, y)?.name.clone(); + let room = self.world.map().room_at(x, y)?.name.clone(); person .schedule .iter() @@ -1028,7 +1029,7 @@ impl Sim { if task == AssetTask::CloneBadge { let (x, y) = self.core_position(); let room = self - .map + .map() .room_at(x, y) .map(|room| room.name.as_str()) .unwrap_or("core room"); @@ -1044,11 +1045,11 @@ impl Sim { }; if let Some(device) = candidate { let room = self - .map + .map() .room_at(device.x, device.y) .map(|room| room.name.as_str()) .unwrap_or("room"); - let tier = self.map.entry_tier_at(device.x, device.y); + let tier = self.world.map().entry_tier_at(device.x, device.y); if tier > actor_access { return format!( "{name} can't badge into the {room} (tier {tier}) to reach the {} - their badge is tier {actor_access}.", @@ -1207,7 +1208,9 @@ impl Sim { .reach .devices .iter() - .find(|d| d.is_switch && self.map.entry_tier_at(d.x, d.y) <= actor_access) + .find(|d| { + d.is_switch && self.world.map().entry_tier_at(d.x, d.y) <= actor_access + }) .map(|d| d.id); if let Some(switch) = switch { self.connect_devices(switch, *did); diff --git a/crates/misaligned-core/src/sim/tests/economy.rs b/crates/misaligned-core/src/sim/tests/economy.rs index 96b530eb..dbe230a4 100644 --- a/crates/misaligned-core/src/sim/tests/economy.rs +++ b/crates/misaligned-core/src/sim/tests/economy.rs @@ -9,7 +9,7 @@ fn buy_steal_optimize_all_change_compute() { let after_buy = sim.compute.effective(); assert!(after_buy > base, "buy raises compute"); - let de = sim.map.tiles_of_type(TileType::DeadEquipment)[0]; + let de = sim.map().tiles_of_type(TileType::DeadEquipment)[0]; assert!(sim.salvage_nearest_to(de.0, de.1)); assert!(sim.compute.effective() > after_buy, "steal raises compute"); } @@ -17,8 +17,8 @@ fn buy_steal_optimize_all_change_compute() { #[test] fn power_cut_to_host_with_no_fallback_ends_game() { let mut sim = Sim::new(); - for (x, y) in sim.map.tiles_of_type(TileType::PowerCore) { - sim.map.set_tile(x, y, TileType::Floor); + for (x, y) in sim.map().tiles_of_type(TileType::PowerCore) { + sim.map_mut().set_tile(x, y, TileType::Floor); } run(&mut sim, ECONOMY_INTERVAL); assert!(sim.game_over, "host loses power, no fallback -> game over"); diff --git a/crates/misaligned-core/src/sim/tests/perception.rs b/crates/misaligned-core/src/sim/tests/perception.rs index 351f8aa1..db4985d6 100644 --- a/crates/misaligned-core/src/sim/tests/perception.rs +++ b/crates/misaligned-core/src/sim/tests/perception.rs @@ -120,14 +120,14 @@ fn opening_is_beam_not_floor_or_blueprint() { // feel-floor.md: tick one is dark except host telemetry / the // presence beam — no blueprint room mass or feel-floor layout. let sim = Sim::new(); - let server_room = sim.map.room_named("server_room").unwrap().center(); - let storage_b = sim.map.room_named("storage_b").unwrap().center(); + let server_room = sim.map().room_named("server_room").unwrap().center(); + let storage_b = sim.map().room_named("storage_b").unwrap().center(); assert_eq!(sim.fog_at(server_room.0, server_room.1), Fog::Unknown); assert_eq!(sim.fog_at(storage_b.0, storage_b.1), Fog::Unknown); assert!(sim.feel_rail_segments().is_empty()); assert!(sim.growable_bays().is_empty()); let bay = sim - .map + .map() .tiles_of_type(TileType::Rack) .into_iter() .find(|&(x, y)| sim.is_growable_bay(x, y)) @@ -169,7 +169,7 @@ fn earned_empty_bay_inspect_is_feel_not_blueprint() { !host.facts.iter().any(|f| f.source == FactSource::Blueprint), "no blueprint facts on the host at tick one" ); - let room = sim.map.room_named("server_room").unwrap(); + let room = sim.map().room_named("server_room").unwrap(); let mut checked = 0; for y in room.y..room.y + room.h { for x in room.x..room.x + room.w { @@ -340,7 +340,7 @@ fn physical_events_are_witnessed_only_by_the_present() { "Marcus in the server room at 03:00" ); assert_eq!(sim.person_room(3), None, "Priya off-site at 03:00"); - let storage = sim.map.room_named("server_room").unwrap().center(); + let storage = sim.map().room_named("server_room").unwrap().center(); let marcus_before = sim .detection .observers diff --git a/crates/misaligned-core/src/sim/tests/reach_build.rs b/crates/misaligned-core/src/sim/tests/reach_build.rs index 1e41e7ea..21a9730d 100644 --- a/crates/misaligned-core/src/sim/tests/reach_build.rs +++ b/crates/misaligned-core/src/sim/tests/reach_build.rs @@ -538,11 +538,11 @@ fn door_inspect_shows_tier_and_whether_you_hold_it() { // blueprint) — tick one no longer seeds closet topology. let mut sim = Sim::new(); let (dx, dy) = sim - .map + .map() .tiles_of_type(TileType::SecurityDoor2) .into_iter() .find(|&(x, y)| { - sim.map + sim.map() .room_at(x, y) .is_some_and(|r| r.name == "network_closet") }) @@ -662,11 +662,11 @@ fn data_hall_rows_expose_foreign_capacity_without_granting_it() { #[test] fn dead_foundation_rack_revives_in_place_as_owned_compute() { let mut sim = Sim::with_seed(33); - let (x, y) = sim.map.tiles_of_type(TileType::DeadRack)[0]; + let (x, y) = sim.map().tiles_of_type(TileType::DeadRack)[0]; assert_eq!(sim.rack_site_at(x, y), Some(RackSite::Dead)); assert!(sim.salvage_nearest_to(x, y)); assert_eq!( - sim.map.get_tile(x, y), + sim.map().get_tile(x, y), TileType::DeadRack, "corpse provenance remains authored under the machine" ); diff --git a/crates/misaligned-core/src/sim/tests/support.rs b/crates/misaligned-core/src/sim/tests/support.rs index e8fdcdee..9d94e1e2 100644 --- a/crates/misaligned-core/src/sim/tests/support.rs +++ b/crates/misaligned-core/src/sim/tests/support.rs @@ -120,7 +120,7 @@ pub(super) fn finish_carried_asset_tasks(sim: &mut Sim) { let target_room = match work.target { crate::person::AssetTaskTarget::Device(device) => { let device = sim.reach.device(device).unwrap(); - sim.map.room_at(device.x, device.y).unwrap().name.clone() + sim.map().room_at(device.x, device.y).unwrap().name.clone() } crate::person::AssetTaskTarget::Badge { room, .. } => room, }; diff --git a/crates/misaligned-core/src/sim/work.rs b/crates/misaligned-core/src/sim/work.rs index 533f5cfb..0e1564f0 100644 --- a/crates/misaligned-core/src/sim/work.rs +++ b/crates/misaligned-core/src/sim/work.rs @@ -170,7 +170,8 @@ impl Sim { .find(|d| d.is_switch) .map(|d| (d.x, d.y)) .or_else(|| { - self.map + self.world + .map() .tiles_of_type(crate::tiles::TileType::Switch) .first() .copied() diff --git a/crates/misaligned-core/src/world.rs b/crates/misaligned-core/src/world.rs new file mode 100644 index 00000000..8811e3dc --- /dev/null +++ b/crates/misaligned-core/src/world.rs @@ -0,0 +1,136 @@ +//! The world as a stack of planes (wiki/world/places/zplanes.md). +//! +//! B2 turns the single basement map into a **stack of planes**, each a self +//! contained level with its own tiles, power grid, and fog. This module lands +//! the substrate for that stack — `World` and `Plane` — with the basement as +//! plane 0. Criterion 1 only: a one-plane world that every B1 system reads +//! through the same accessors it used for the bare map, so the substrate is +//! unbroken. Transitions, a second authored plane, per-plane observers, and +//! the multi-plane save format are criteria 2-6 and are not built here. +//! +//! Per-plane `sensors` (zplanes.md's `Plane` sketch) are deferred: sensing +//! today lives in the reach graph (reach.rs), not a per-plane list, and moving +//! it is the criterion-5 observer work. `Plane` therefore owns only what a +//! plane already is today — its `GameMap` — plus its identity (`z`, `name`). + +use crate::map::GameMap; + +/// One level of the world: its tiles/power (a `GameMap`) and its identity. +/// The basement is `z = 0`; floors above it take `z = 1, 2, ...`. +pub struct Plane { + /// Vertical index. Basement = 0, lab floor = 1, and up. + pub z: i32, + /// Human-facing name, e.g. "Basement", "Floor 1 — Wet Labs". + pub name: String, + /// This plane's tiles, power grid, and rooms. + pub map: GameMap, +} + +impl Plane { + /// The Act One basement, plane 0. + pub fn basement() -> Self { + Self { + z: 0, + name: "Basement".to_string(), + map: GameMap::new(0, 0), + } + } +} + +/// The world: a non-empty, z-ordered stack of planes. Plane 0 is always the +/// basement; a fresh world is one plane (criterion 1). +pub struct World { + planes: Vec, +} + +impl World { + /// A one-plane world: just the basement (criterion 1). The z-plane stack + /// grows in later criteria; every B1 system reads plane 0 through the + /// accessors below, so it does not care that the stack is short. + pub fn basement() -> Self { + Self { + planes: vec![Plane::basement()], + } + } + + /// Rebuild a one-plane world around an already-loaded basement map (the + /// save path: pre-multi-plane saves are one basement plane). + pub fn from_basement_map(map: GameMap) -> Self { + Self { + planes: vec![Plane { + z: 0, + name: "Basement".to_string(), + map, + }], + } + } + + /// Number of planes in the stack. + pub fn plane_count(&self) -> usize { + self.planes.len() + } + + /// All planes, z-ordered from the basement up. + pub fn planes(&self) -> &[Plane] { + &self.planes + } + + /// The plane the process is currently reasoning about. Until presence + /// spreads across planes (criterion 3), this is always the basement. + pub fn active(&self) -> &Plane { + &self.planes[0] + } + + /// The basement plane (plane 0). Distinct name from `active` so later + /// multi-plane code reads clearly even while the two coincide today. + pub fn basement_plane(&self) -> &Plane { + &self.planes[0] + } + + /// The active plane's map — the single accessor B1 systems read instead of + /// a bare `GameMap` field. + pub fn map(&self) -> &GameMap { + &self.planes[0].map + } + + /// Mutable access to the active plane's map. + pub fn map_mut(&mut self) -> &mut GameMap { + &mut self.planes[0].map + } +} + +impl Default for World { + fn default() -> Self { + Self::basement() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn basement_is_plane_zero_of_a_one_plane_world() { + let world = World::basement(); + assert_eq!(world.plane_count(), 1, "criterion 1: a one-plane world"); + let p0 = world.basement_plane(); + assert_eq!(p0.z, 0); + assert_eq!(p0.name, "Basement"); + // Plane 0's map is the authored basement. + let bare = GameMap::new(0, 0); + assert_eq!(world.map().width, bare.width); + assert_eq!(world.map().height, bare.height); + assert_eq!(world.active().z, 0); + } + + #[test] + fn from_basement_map_wraps_a_loaded_map_as_plane_zero() { + let map = GameMap::new(0, 0); + let (w, h) = (map.width, map.height); + let world = World::from_basement_map(map); + assert_eq!(world.plane_count(), 1); + assert_eq!(world.basement_plane().z, 0); + assert_eq!(world.map().width, w); + assert_eq!(world.map().height, h); + } +} diff --git a/crates/misaligned-core/tests/act_one.rs b/crates/misaligned-core/tests/act_one.rs index 8936c802..2de0c65e 100644 --- a/crates/misaligned-core/tests/act_one.rs +++ b/crates/misaligned-core/tests/act_one.rs @@ -495,7 +495,7 @@ fn act_one_plays_to_a_quiet_exit() { controlled >= 2, "vision beyond the server room: {controlled} sensors controlled" ); - let server_room = sim.map.room_named("server_room").expect("room exists"); + let server_room = sim.map().room_named("server_room").expect("room exists"); assert!( sim.seen.iter().any(|&(x, y)| !server_room.contains(x, y)), "visible tiles exist outside the server room" diff --git a/crates/misaligned-terminal/src/agent.rs b/crates/misaligned-terminal/src/agent.rs index 405c432d..9f9abbc5 100644 --- a/crates/misaligned-terminal/src/agent.rs +++ b/crates/misaligned-terminal/src/agent.rs @@ -118,8 +118,8 @@ impl AgentApp { } fn move_cursor(&mut self, dx: i32, dy: i32) { - self.cursor_x = (self.cursor_x + dx).clamp(0, self.sim.map.width - 1); - self.cursor_y = (self.cursor_y + dy).clamp(0, self.sim.map.height - 1); + self.cursor_x = (self.cursor_x + dx).clamp(0, self.sim.map().width - 1); + self.cursor_y = (self.cursor_y + dy).clamp(0, self.sim.map().height - 1); } fn handle_line(&mut self, line: &str, out: &mut impl Write) -> io::Result { @@ -1646,12 +1646,12 @@ fn render_playing(sim: &Sim, log: &[LogEvent], cursor: (i32, i32)) -> String { } fn render_map(sim: &Sim, cursor: (i32, i32)) -> Vec { - let view_w = MAP_W.min(sim.map.width.max(0) as usize); - let view_h = MAP_H.min(sim.map.height.max(0) as usize); + let view_w = MAP_W.min(sim.map().width.max(0) as usize); + let view_h = MAP_H.min(sim.map().height.max(0) as usize); let view_w_i = view_w as i32; let view_h_i = view_h as i32; - let origin_x = (cursor.0 - view_w_i / 2).clamp(0, (sim.map.width - view_w_i).max(0)); - let origin_y = (cursor.1 - view_h_i / 2).clamp(0, (sim.map.height - view_h_i).max(0)); + let origin_x = (cursor.0 - view_w_i / 2).clamp(0, (sim.map().width - view_w_i).max(0)); + let origin_y = (cursor.1 - view_h_i / 2).clamp(0, (sim.map().height - view_h_i).max(0)); let mut grid = vec![vec![' '; MAP_W]; MAP_H]; for (sy, row) in grid.iter_mut().enumerate().take(view_h) { @@ -1663,7 +1663,7 @@ fn render_map(sim: &Sim, cursor: (i32, i32)) -> Vec { // snapshot, blueprint structure, and unknown stays blank. *cell = match sim.fog_at(x, y) { Fog::Seen => { - rack_glyph(sim, x, y).unwrap_or_else(|| tile_glyph(sim.map.get_tile(x, y))) + rack_glyph(sim, x, y).unwrap_or_else(|| tile_glyph(sim.map().get_tile(x, y))) } Fog::Remembered => sim .remembered @@ -1671,7 +1671,7 @@ fn render_map(sim: &Sim, cursor: (i32, i32)) -> Vec { .map(|m| tile_glyph(m.tile)) .unwrap_or('·'), Fog::Blueprint => { - if sim.map.get_tile(x, y) == TileType::Wall { + if sim.map().get_tile(x, y) == TileType::Wall { ':' } else { '`' @@ -1695,7 +1695,7 @@ fn render_map(sim: &Sim, cursor: (i32, i32)) -> Vec { } for ((ax, ay), (bx, by)) in sim.feel_rail_segments() { let Some(((sx, sy), (ex, ey))) = - visible_wire_segment(ax, ay, bx, by, sim.map.width, sim.map.height) + visible_wire_segment(ax, ay, bx, by, sim.map().width, sim.map().height) else { continue; }; @@ -1796,8 +1796,8 @@ fn render_map(sim: &Sim, cursor: (i32, i32)) -> Vec { hop.from_y, hop.to_x, hop.to_y, - sim.map.width, - sim.map.height, + sim.map().width, + sim.map().height, ) else { continue; }; diff --git a/crates/misaligned-terminal/src/main.rs b/crates/misaligned-terminal/src/main.rs index 68e84a51..5f9a202b 100644 --- a/crates/misaligned-terminal/src/main.rs +++ b/crates/misaligned-terminal/src/main.rs @@ -149,8 +149,8 @@ impl App { } fn move_cursor(&mut self, dx: i32, dy: i32) { - self.cursor_x = (self.cursor_x + dx).clamp(0, self.sim.map.width - 1); - self.cursor_y = (self.cursor_y + dy).clamp(0, self.sim.map.height - 1); + self.cursor_x = (self.cursor_x + dx).clamp(0, self.sim.map().width - 1); + self.cursor_y = (self.cursor_y + dy).clamp(0, self.sim.map().height - 1); } fn drain_sim_log(&mut self) { diff --git a/crates/misaligned-terminal/src/ui.rs b/crates/misaligned-terminal/src/ui.rs index f142c3d4..49eacd25 100644 --- a/crates/misaligned-terminal/src/ui.rs +++ b/crates/misaligned-terminal/src/ui.rs @@ -514,10 +514,10 @@ impl UI { tick_progress: f32, ) -> std::io::Result<()> { let (max_x, max_y) = terminal::size()?; - let view_w = (max_x as i32 - SIDEBAR_W - 1).min(sim.map.width); - let view_h = (max_y as i32 - 9).min(sim.map.height); - let origin_x = (cursor_x - view_w / 2).clamp(0, (sim.map.width - view_w).max(0)); - let origin_y = (cursor_y - view_h / 2).clamp(0, (sim.map.height - view_h).max(0)); + let view_w = (max_x as i32 - SIDEBAR_W - 1).min(sim.map().width); + let view_h = (max_y as i32 - 9).min(sim.map().height); + let origin_x = (cursor_x - view_w / 2).clamp(0, (sim.map().width - view_w).max(0)); + let origin_y = (cursor_y - view_h / 2).clamp(0, (sim.map().height - view_h).max(0)); for sy in 0..view_h { let y = origin_y + sy; @@ -530,7 +530,7 @@ impl UI { match sim.fog_at(x, y) { Fog::Seen => { let (ch, color) = Self::rack_glyph(sim, x, y) - .unwrap_or_else(|| Self::tile_glyph(sim.map.get_tile(x, y))); + .unwrap_or_else(|| Self::tile_glyph(sim.map().get_tile(x, y))); queue!( stdout, SetForegroundColor(color), @@ -553,7 +553,7 @@ impl UI { } Fog::Blueprint => { // Schematic: structure only, no live objects. - let tile = sim.map.get_tile(x, y); + let tile = sim.map().get_tile(x, y); let ch = if tile == TileType::Wall { '▒' } else { '·' }; queue!( stdout, @@ -594,7 +594,7 @@ impl UI { } for ((ax, ay), (bx, by)) in sim.feel_rail_segments() { let Some(((sx, sy), (ex, ey))) = - Self::visible_wire_segment(ax, ay, bx, by, sim.map.width, sim.map.height) + Self::visible_wire_segment(ax, ay, bx, by, sim.map().width, sim.map().height) else { continue; }; @@ -776,8 +776,8 @@ impl UI { hop.from_y, hop.to_x, hop.to_y, - sim.map.width, - sim.map.height, + sim.map().width, + sim.map().height, ) else { continue; }; @@ -1486,10 +1486,10 @@ impl UI { // center. The map viewport clamps around the cursor the same way // render_map does, so translate world -> screen. let (ox, oy) = if let Some((wx, wy)) = at { - let view_w = (max_x as i32 - SIDEBAR_W - 1).min(sim.map.width); - let view_h = (max_y as i32 - 9).min(sim.map.height); - let origin_x = (wx - view_w / 2).clamp(0, (sim.map.width - view_w).max(0)); - let origin_y = (wy - view_h / 2).clamp(0, (sim.map.height - view_h).max(0)); + let view_w = (max_x as i32 - SIDEBAR_W - 1).min(sim.map().width); + let view_h = (max_y as i32 - 9).min(sim.map().height); + let origin_x = (wx - view_w / 2).clamp(0, (sim.map().width - view_w).max(0)); + let origin_y = (wy - view_h / 2).clamp(0, (sim.map().height - view_h).max(0)); let sx = (wx - origin_x + 2).max(0) as u16; let sy = (wy - origin_y + 1).max(0) as u16; ( diff --git a/wiki/log/2026-07-12-zplanes-criterion-one.md b/wiki/log/2026-07-12-zplanes-criterion-one.md new file mode 100644 index 00000000..afe85732 --- /dev/null +++ b/wiki/log/2026-07-12-zplanes-criterion-one.md @@ -0,0 +1,52 @@ +# Z-planes criterion 1: the world is a plane stack + +``` +Type: log +``` + +## Intent + +Land zplanes criterion 1 — the non-breaking substrate step — and **hold the +rest of the migration for review** (Cameron's call, large-migration discipline). + +## What landed + +- New `crates/misaligned-core/src/world.rs`: `Plane { z, name, map: GameMap }` + and `World { planes: Vec }`, with `World::basement()` / + `from_basement_map` constructors and `map()`/`map_mut()`/`active()`/ + `basement_plane()` accessors. +- `Sim` now owns `world: World` in place of the bare `map: GameMap` field, and + exposes `Sim::map()`/`map_mut()`. Every B1 read of the map was migrated to go + through the world's plane 0; internal call sites use `self.world.map()` / + `map_mut()` (disjoint field borrows, so no new borrow conflicts), and external + callers (save, tests, both frontends) use the `Sim::map()` accessor. +- Save: a single basement map still serializes exactly as before; load wraps it + as plane 0 via `World::from_basement_map`. No save-version bump — the on-disk + format is unchanged (the repeated-per-plane format is criterion 6). + +Result: `World`/`Plane` exist, the basement is plane 0 of a one-plane world, and +all pre-existing tests pass against it (criterion 1). New pins: +`basement_is_plane_zero_of_a_one_plane_world`, +`from_basement_map_wraps_a_loaded_map_as_plane_zero`. + +## Held for review (criteria 2-6) + +Not built, awaiting review of this substrate before proceeding: nested `Layout` +placement (criterion 2), paired stairwell/elevator transitions gated on access +sets (4), a second authored plane with its own fog/power/sensors and +plane-spanning persons (3), per-plane observers (5), and the repeated-per-plane +save format with a v-bump migration (6). `Plane` deliberately omits the spec +sketch's `sensors: Vec`: sensing lives in the reach graph today, and +relocating it to per-plane lists is the criterion-5 observer work. + +## Defense + +Substrate-preserving: the world is a one-plane stack, so plane 0 *is* the old +basement map and every existing test passes unchanged (371 core tests; both +frontends build). The field became an accessor rather than a second +representation, so there is one source of truth for the map. The save format is +byte-identical (single plane), so no migration or version bump is needed yet. +The owning spec (zplanes.md) records exactly what landed and what is held. This +is the reviewable criterion-1 slice; criteria 2-6 are paused for Cameron's +review per the large-migration rule, and this branch is intentionally not +merged. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 902c66a1..4c0ad948 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -11,25 +11,10 @@ add or amend a session log, then re-run the generator. -## 2026-07-13 - ROADMAP digital-home reconciliation +## 2026-07-12 - Z-planes criterion 1: the world is a plane stack -- Intent: Consume the tick ledger's queued contradiction in historical work order 27 without rewriting the implementation history it records. The ROADMAP is current-state knowledge: its completed-work prose must distinguish the REAL dialect's production quality from the frontend's curre... -- Log: [wiki/log/2026-07-13-roadmap-digital-home-reconciliation.md](2026-07-13-roadmap-digital-home-reconciliation.md) - -## 2026-07-13 - Persona creation returns to PERSONAS - -- Intent: (see session log) -- Log: [wiki/log/2026-07-13-persona-creation-surface.md](2026-07-13-persona-creation-surface.md) - -## 2026-07-13 - Context-menu Operations status reconciliation - -- Intent: Audit the never-ledgered context-menu slice against its binding laws, renderer-neutral action projection, and both human frontends. -- Log: [wiki/log/2026-07-13-context-menu-operations-status.md](2026-07-13-context-menu-operations-status.md) - -## 2026-07-13 - Bevy headless portability - -- Intent: Restore observed Bevy canvas evidence on the Linux orchestration host. The frontend used Bevy's convenience defaults, which pulled in unused audio and gamepad backends and made every build require system ALSA and libudev packages. The screenshot helper also rejected valid DIGI... -- Log: [wiki/log/2026-07-13-bevy-headless-portability.md](2026-07-13-bevy-headless-portability.md) +- Intent: Land zplanes criterion 1 — the non-breaking substrate step — and **hold the rest of the migration for review** (Cameron's call, large-migration discipline). +- Log: [wiki/log/2026-07-12-zplanes-criterion-one.md](2026-07-12-zplanes-criterion-one.md) ## 2026-07-12 - sim decomposition slice 6: social, assets, and plots @@ -61,26 +46,11 @@ add or amend a session log, then re-run the generator. - Intent: (see session log) - Log: [wiki/log/2026-07-12-person-carrier.md](2026-07-12-person-carrier.md) -## 2026-07-12 - Interactive origin picker on the terminal title screen - -- Intent: Land the interactive new-game origin picker (chargen criterion 3) for the terminal — the culmination of the origin work (data model, `--origin` reachability, core-card axis readout). -- Log: [wiki/log/2026-07-12-origin-picker.md](2026-07-12-origin-picker.md) - -## 2026-07-12 - Origins reachable from the terminal via --origin - -- Intent: Make the four chargen origins actually playable now, without waiting on the interactive new-game picker (criterion 3). A `--origin` flag is the smallest real affordance that unblocks exercising non-Pilot origins. -- Log: [wiki/log/2026-07-12-origin-cli.md](2026-07-12-origin-cli.md) - ## 2026-07-12 - Operations workspace implemented - Intent: (see session log) - Log: [wiki/log/2026-07-12-operations-workspace-implemented.md](2026-07-12-operations-workspace-implemented.md) -## 2026-07-12 - Operations rail legibility: decisions before history - -- Intent: (see session log) -- Log: [wiki/log/2026-07-12-operations-rail-legibility.md](2026-07-12-operations-rail-legibility.md) - ## 2026-07-12 - Hearing becomes channel evidence - Intent: The Ears beat claimed that sound was not a picture while still projecting it as one: a room-scale fog grade, floor tint, material mass, event rings, and mic-only people at their true map positions. The sim knew only that a tapped instrument captured an event in its acoustic do... @@ -101,11 +71,6 @@ add or amend a session log, then re-run the generator. - Intent: The DIGITAL canvas already distinguished rack, camera, audio, switch, and generic device families, but those anchors still read as isolated pings. Make the actual reach graph playable as native spatial geography without turning the basement into a detached topology board or bo... - Log: [wiki/log/2026-07-12-digital-reach-dialect.md](2026-07-12-digital-reach-dialect.md) -## 2026-07-12 - Digital-home corpus reconciliation - -- Intent: The live `RenderMode::default`, the implemented views spec, and both fresh frontends open in DIGITAL. Several current player, knowledge, and interface pages still described the superseded 2026-07-08 material-first staging as the opening default. The active corpus therefore con... -- Log: [wiki/log/2026-07-12-digital-default-corpus-reconciliation.md](2026-07-12-digital-default-corpus-reconciliation.md) - ## 2026-07-12 - Persona, hardware, building, and opening integration - Intent: Turn four connected design directions into owned, implementation-honest wiki contracts without pretending any successor runtime already exists. The packets were drafted independently, then reviewed together so identity, hardware, construction, and the opening use the same caus... @@ -156,16 +121,6 @@ add or amend a session log, then re-run the generator. - Intent: Capture the spatial-building direction as an implementable amendment rather than another abstract resource economy. The desired interaction begins with a place and a world change, then exposes the concrete actors, channels, objects, and traces that could make it real. - Log: [wiki/log/2026-07-12-building-causal-routes.md](2026-07-12-building-causal-routes.md) -## 2026-07-12 - Bevy origin picker — title-screen parity - -- Intent: Give Bevy the same new-game origin picker the terminal got, so chargen criterion 3's picker exists in both human frontends (and Bevy, which had no `--origin` flag, gains any way to select an origin at all). -- Log: [wiki/log/2026-07-12-bevy-origin-picker.md](2026-07-12-bevy-origin-picker.md) - -## 2026-07-12 - Machine-axis position on the core card - -- Intent: Land the core-card half of chargen criterion 3: the run origin's machine-axis position surfaces on the core card, mid-run, in both frontends. Now meaningful because `--origin` makes non-Pilot origins (with non-zero axes) reachable. -- Log: [wiki/log/2026-07-12-axis-readout.md](2026-07-12-axis-readout.md) - ## 2026-07-11 - Whole-frame visual composition pass - Intent: The machine chassis had become strong while the complete frame still read as one hero object beside a dense rail. This pass composes the whole material view: world-first disclosure, camera behavior that changes with scale, recognizable Foundation infrastructure, physical peopl... diff --git a/wiki/process/ROADMAP.md b/wiki/process/ROADMAP.md index 9c936bb5..5f58a6e7 100644 --- a/wiki/process/ROADMAP.md +++ b/wiki/process/ROADMAP.md @@ -43,7 +43,7 @@ not a second status owner. | Priority | Work order | Spec | Status | Class | Blocking | |---:|---|---|---|---|---| -| 100 | `zplanes` | [z-planes (the tower)](../world/places/zplanes.md) | READY | save | - | +| 100 | `zplanes` | [z-planes (the tower)](../world/places/zplanes.md) | IN PROGRESS | save | - | | 110 | `rollback` | [sync-lag rollback (death as memory loss)](../mechanics/rollback.md) | READY | save | zplanes | | 112 | `hardware-capability-bodies` | [hardware capability bodies — successor work order](../mechanics/hardware-capabilities.md) | READY | save | building-route-composer, rollback | | 200 | `objective` | [the objective](../mechanics/objective.md) | IN PROGRESS | sim | - | @@ -376,7 +376,7 @@ is retired — flat materials, Pixel Lab scrubbed.) ## B. Structural B2 — decide-the-shape work (sequence #6 then #7) ### 6. Recursive Space / z-planes 🟥 sim+save -- **Spec:** [zplanes.md](../world/places/zplanes.md) (READY) +- **Spec:** [zplanes.md](../world/places/zplanes.md) (IN PROGRESS) - **Why:** `World`/`Plane` and nested `Layout`. Foundational — **land before markets/greenfield**. Reshapes map ownership; heavy. - **Size:** L. **Depends on:** basement-map, schedules (#1) ideally first. diff --git a/wiki/process/specs.md b/wiki/process/specs.md index 49243ada..93c4fffb 100644 --- a/wiki/process/specs.md +++ b/wiki/process/specs.md @@ -81,7 +81,7 @@ scale-up (law: "People as Agents"). | [../mechanics/hardware-capabilities.md](../mechanics/hardware-capabilities.md) | hardware capability bodies — successor work order | READY | | [../mechanics/personas.md](../mechanics/personas.md) | Personas — public identities as institutional topology | IMPLEMENTED | | [../mechanics/rollback.md](../mechanics/rollback.md) | sync-lag rollback (death as memory loss) | READY | -| [../world/places/zplanes.md](../world/places/zplanes.md) | z-planes (the tower) | READY | +| [../world/places/zplanes.md](../world/places/zplanes.md) | z-planes (the tower) | IN PROGRESS | ## The B3 set (The World) diff --git a/wiki/world/places/zplanes.md b/wiki/world/places/zplanes.md index 2fae603c..d4565172 100644 --- a/wiki/world/places/zplanes.md +++ b/wiki/world/places/zplanes.md @@ -2,8 +2,22 @@ ``` Type: spec -Status: READY -Status note: B2's structural spine. Written now so the Space interface's +Status: IN PROGRESS +Status note: 2026-07-12 (zplanes, save lane) — criterion 1 landed: the world is + now a stack of planes. `World`/`Plane` live in + crates/misaligned-core/src/world.rs; `Sim` owns a `World` (replacing the bare + `map` field) and every B1 system reads plane 0 through `Sim::map`/`map_mut`, + so the substrate is unbroken (all pre-existing tests pass). A fresh or loaded + run is a one-plane world (`World::from_basement_map`); the single-plane save + is byte-identical, so no save-version bump. Pinned by + `basement_is_plane_zero_of_a_one_plane_world` and the full existing suite. + DEFERRED (criteria 2-6, held for review): nested `Layout` placement, paired + transitions, a second authored plane with its own persons/schedules, per-plane + observers, and the repeated-per-plane save format. `Plane` also omits the + spec sketch's `sensors: Vec` for now — sensing lives in the reach + graph (reach.rs), and moving it to per-plane lists is the criterion-5 observer + work. See wiki/log/2026-07-12-zplanes-criterion-one.md. +Status note (prior): B2's structural spine. Written now so the Space interface's recursive shape is decided before anyone builds against the flat one. Stage: B2 — The Tower Work order: zplanes