diff --git a/crates/misaligned-assets/src/institution.rs b/crates/misaligned-assets/src/institution.rs index d1454f9e..1ea208df 100644 --- a/crates/misaligned-assets/src/institution.rs +++ b/crates/misaligned-assets/src/institution.rs @@ -21,11 +21,15 @@ pub enum InstitutionPropKind { FloorFixture, RackPlinth, CableTray, + CableBridge, + ServiceDrop, FloorTrench, + MaintenanceBay, + ServiceCart, } impl InstitutionPropKind { - pub const ALL: [Self; 9] = [ + pub const ALL: [Self; 13] = [ Self::Camera, Self::Ups, Self::Breaker, @@ -34,7 +38,11 @@ impl InstitutionPropKind { Self::FloorFixture, Self::RackPlinth, Self::CableTray, + Self::CableBridge, + Self::ServiceDrop, Self::FloorTrench, + Self::MaintenanceBay, + Self::ServiceCart, ]; pub const fn label(self) -> &'static str { @@ -47,7 +55,11 @@ impl InstitutionPropKind { Self::FloorFixture => "floor_fixture", Self::RackPlinth => "rack_plinth", Self::CableTray => "cable_tray", + Self::CableBridge => "cable_bridge", + Self::ServiceDrop => "service_drop", Self::FloorTrench => "floor_trench", + Self::MaintenanceBay => "maintenance_bay", + Self::ServiceCart => "service_cart", } } } @@ -223,7 +235,13 @@ pub fn build_institution_prop( } InstitutionPropKind::RackPlinth => build_rack_plinth(commands, assets, root), InstitutionPropKind::CableTray => build_cable_tray(commands, assets, root), + InstitutionPropKind::CableBridge => build_cable_bridge(commands, assets, root), + InstitutionPropKind::ServiceDrop => build_service_drop(commands, assets, root), InstitutionPropKind::FloorTrench => build_floor_trench(commands, assets, root), + InstitutionPropKind::MaintenanceBay => build_maintenance_bay(commands, assets, root), + InstitutionPropKind::ServiceCart => { + build_service_cart(commands, assets, root, semantic_state) + } } } @@ -870,6 +888,74 @@ fn build_cable_tray(commands: &mut Commands, assets: &InstitutionAssets, root: E ); } +/// One quiet bridge between the rack-local tray spans. Rack sites are two +/// tiles apart; these odd-tile spans make the row read as one maintained run +/// without adding another hanger or any signal color. +fn build_cable_bridge(commands: &mut Commands, assets: &InstitutionAssets, root: Entity) { + for z in [-0.40_f32, -0.28] { + spawn_part( + commands, + assets, + assets.structure.clone(), + root, + Vec3::new(1.08, 0.045, 0.035), + Vec3::new(0.0, 1.46, z), + Quat::IDENTITY, + "bridge_rail", + ); + } + for x in [-0.36_f32, 0.0, 0.36] { + spawn_part( + commands, + assets, + assets.chrome.clone(), + root, + Vec3::new(0.020, 0.026, 0.15), + Vec3::new(x, 1.445, -0.34), + Quat::IDENTITY, + "bridge_rung", + ); + } +} + +/// Row C's service spine terminates beside the real hall patch panel. The +/// paired verticals and floor elbow make the endpoint physically legible; the +/// reach graph and material trace still own whether anything can flow. +fn build_service_drop(commands: &mut Commands, assets: &InstitutionAssets, root: Entity) { + for x in [-0.075_f32, 0.075] { + spawn_part( + commands, + assets, + assets.structure.clone(), + root, + Vec3::new(0.035, 1.40, 0.035), + Vec3::new(x, 0.73, -0.40), + Quat::IDENTITY, + "drop_vertical", + ); + spawn_part( + commands, + assets, + assets.gunmetal.clone(), + root, + Vec3::new(0.035, 0.035, 0.34), + Vec3::new(x, 0.045, -0.25), + Quat::IDENTITY, + "drop_floor_elbow", + ); + } + spawn_part( + commands, + assets, + assets.porcelain_trim.clone(), + root, + Vec3::new(0.28, 0.07, 0.06), + Vec3::new(0.0, 1.46, -0.36), + Quat::IDENTITY, + "drop_header", + ); +} + /// A dark recessed service trench through a cold aisle. The interrupted /// chrome edge provides scale and direction; it is structure, not a signal /// route and therefore never glows. @@ -910,6 +996,134 @@ fn build_floor_trench(commands: &mut Commands, assets: &InstitutionAssets, root: } } +/// Sparse authored maintenance history at a real dead rack: one pulled +/// porcelain access panel, one dark tool tray, and one neutral indexed tag. +/// Nothing glows and nothing is randomized into generic facility clutter. +fn build_maintenance_bay(commands: &mut Commands, assets: &InstitutionAssets, root: Entity) { + spawn_part( + commands, + assets, + assets.structure.clone(), + root, + Vec3::new(0.46, 0.07, 0.30), + Vec3::new(-0.34, 0.055, 0.36), + Quat::IDENTITY, + "tool_tray", + ); + spawn_part( + commands, + assets, + assets.porcelain_trim.clone(), + root, + Vec3::new(0.30, 0.50, 0.035), + Vec3::new(0.43, 0.28, 0.20), + Quat::from_rotation_z(-0.24), + "pulled_access_panel", + ); + spawn_part( + commands, + assets, + assets.chrome.clone(), + root, + Vec3::new(0.16, 0.055, 0.018), + Vec3::new(0.48, 0.40, 0.17), + Quat::from_rotation_z(-0.24), + "maintenance_index_tag", + ); + for x in [-0.47_f32, -0.21] { + spawn_part( + commands, + assets, + assets.gunmetal.clone(), + root, + Vec3::new(0.08, 0.04, 0.08), + Vec3::new(x, 0.11, 0.36), + Quat::IDENTITY, + "tool_case", + ); + } +} + +/// Mobile service cart attached by the frontend to one visible person while +/// the renderer-neutral `PersonCarrier` says they carry work. The cold lamp +/// identifies a live work packet; it is not a second task queue. +fn build_service_cart( + commands: &mut Commands, + assets: &InstitutionAssets, + root: Entity, + semantic_state: InstitutionSemanticState, +) { + for y in [0.26_f32, 0.58] { + spawn_part( + commands, + assets, + assets.structure.clone(), + root, + Vec3::new(0.50, 0.055, 0.34), + Vec3::new(0.0, y, 0.0), + Quat::IDENTITY, + "cart_shelf", + ); + } + for x in [-0.21_f32, 0.21] { + for z in [-0.13_f32, 0.13] { + spawn_part( + commands, + assets, + assets.gunmetal.clone(), + root, + Vec3::new(0.10, 0.10, 0.08), + Vec3::new(x, 0.10, z), + Quat::IDENTITY, + "cart_wheel", + ); + } + } + for x in [-0.22_f32, 0.22] { + spawn_part( + commands, + assets, + assets.chrome.clone(), + root, + Vec3::new(0.035, 0.72, 0.035), + Vec3::new(x, 0.48, -0.13), + Quat::IDENTITY, + "cart_upright", + ); + } + spawn_part( + commands, + assets, + assets.chrome.clone(), + root, + Vec3::new(0.50, 0.035, 0.035), + Vec3::new(0.0, 0.84, -0.13), + Quat::IDENTITY, + "cart_handle", + ); + let packet = accent_handle(assets, semantic_state).unwrap_or_else(|| assets.chrome.clone()); + spawn_part( + commands, + assets, + packet, + root, + Vec3::new(0.22, 0.16, 0.16), + Vec3::new(0.0, 0.69, 0.0), + Quat::IDENTITY, + "carried_work_packet", + ); + spawn_part( + commands, + assets, + assets.structure.clone(), + root, + Vec3::new(0.28, 0.035, 0.20), + Vec3::new(0.0, 0.61, 0.0), + Quat::IDENTITY, + "packet_custody_frame", + ); +} + fn accent_handle( assets: &InstitutionAssets, semantic_state: InstitutionSemanticState, @@ -1059,8 +1273,8 @@ mod tests { .into_iter() .map(InstitutionPropKind::label) .collect(); - assert_eq!(InstitutionPropKind::ALL.len(), 9); - assert_eq!(labels.len(), 9); + assert_eq!(InstitutionPropKind::ALL.len(), 13); + assert_eq!(labels.len(), 13); assert!(labels.contains("camera")); assert!(labels.contains("ups")); assert!(labels.contains("breaker")); @@ -1069,7 +1283,11 @@ mod tests { assert!(labels.contains("floor_fixture")); assert!(labels.contains("rack_plinth")); assert!(labels.contains("cable_tray")); + assert!(labels.contains("cable_bridge")); + assert!(labels.contains("service_drop")); assert!(labels.contains("floor_trench")); + assert!(labels.contains("maintenance_bay")); + assert!(labels.contains("service_cart")); } #[test] diff --git a/crates/misaligned-assets/src/rack.rs b/crates/misaligned-assets/src/rack.rs index e453290b..e0008c95 100644 --- a/crates/misaligned-assets/src/rack.rs +++ b/crates/misaligned-assets/src/rack.rs @@ -934,6 +934,10 @@ fn exposure_record_count(exposure: f32) -> usize { (exposure.max(0.0).ceil() as usize).clamp(1, EXPOSURE_RECORD_MAX) } +fn exposure_record_overflow(exposure: f32) -> bool { + exposure.max(0.0).ceil() as usize > EXPOSURE_RECORD_MAX +} + fn exposure_record_material() -> StandardMaterial { StandardMaterial { base_color: scaled(CRIMSON, 0.95), @@ -996,7 +1000,7 @@ fn spawn_exposure_records( } for i in 0..shown { - let overflow = i + 1 == shown && exposure.ceil() as usize > EXPOSURE_RECORD_MAX; + let overflow = i + 1 == shown && exposure_record_overflow(exposure); let height = if overflow { EXPOSURE_RECORD_H * 1.42 } else { @@ -1141,6 +1145,8 @@ mod tests { assert_eq!(exposure_record_count(0.1), 1); assert_eq!(exposure_record_count(3.0), 3); assert_eq!(exposure_record_count(100.0), EXPOSURE_RECORD_MAX); + assert!(!exposure_record_overflow(7.0)); + assert!(exposure_record_overflow(7.01)); let record = exposure_record_material(); let emitted = record.emissive.red + record.emissive.green + record.emissive.blue; diff --git a/crates/misaligned-bevy/src/main.rs b/crates/misaligned-bevy/src/main.rs index 7378ecfc..d83df267 100644 --- a/crates/misaligned-bevy/src/main.rs +++ b/crates/misaligned-bevy/src/main.rs @@ -29,7 +29,7 @@ use misaligned::operations_projection::{ }; use misaligned::operations_ui::{ConfirmChoice, OperationsWorkspace, OpsPane, OpsSelect}; use misaligned::origin::Origin; -use misaligned::person::{Knowledge, ScheduleBlock}; +use misaligned::person::{AssetKnowledge, Knowledge, ScheduleBlock}; use misaligned::reach::{Device, Party, ReachBlock}; use misaligned::sim::{Fog, LogEvent, PersonVisualState, Sim, TraceDebtStatus}; use misaligned::tiles::TileType; @@ -516,7 +516,7 @@ enum RailSection { } /// Dev screenshot harness (env `MISALIGNED_SHOT=flat|hall|hall-material|opening|opening-digital|digital-reach|wide|close|dark| -/// zoomin|zoomout|intel|tokens|thoughtflow|first-think|visual-proof|person-proof|exposure-record|signal|ears|ears-digital|eyes-white|eyes-form|operations-links| +/// zoomin|zoomout|intel|tokens|thoughtflow|first-think|visual-proof|person-proof|exposure-record|exposure-overflow|service-shift-real|service-shift-digital|service-incident-resolved|signal|ears|ears-digital|eyes-white|eyes-form|operations-links| /// hover-menu|menu|recruit-menu|operations|operations-intel|operations-personas|worklight|worklightoff`, path via /// `MISALIGNED_SHOT_PATH`): stages a scenario, /// waits for @@ -733,6 +733,13 @@ struct Person3d { id: u8, } +/// One mobile physical projection of the renderer-neutral work packet. The +/// cart has no task state of its own; visibility reads `PersonCarrier`. +#[derive(Component)] +struct ServiceCarrier3d { + id: u8, +} + #[derive(Component)] struct InstitutionProp3d { x: i32, @@ -1403,6 +1410,10 @@ struct PersonMarker { id: u8, } #[derive(Component)] +struct PersonWorkMarker { + id: u8, +} +#[derive(Component)] struct TokenMarker { x: i32, y: i32, @@ -1779,7 +1790,11 @@ fn main() { render_cursor, render_tokens, ), - sync_data_hall_infrastructure, + ( + sync_data_hall_infrastructure, + render_service_carriers_3d, + render_person_work_markers, + ), ( sync_floor_fixtures, sync_institution_props, @@ -1847,6 +1862,93 @@ fn dev_fill_reservoirs(sim: &mut Sim) { panic!("dev shot reservoirs did not settle"); } +/// Dev-only staging for the canonical operational proof. This accelerates +/// prerequisites, but not the act itself: the favor-build command opens its +/// real reservoir, the fired intent remains on Marcus, and the resolved frame +/// calls ordinary `Sim::advance` to land graph/signature/trust/removal truth. +fn dev_stage_service_incident(game: &mut Game, resolved: bool) -> ((i32, i32), (i32, i32)) { + let sim = &mut game.sim; + sim.reach.scan(); + let source = sim + .reach + .device_named("Rack 3") + .expect("B1 service proof needs the hall host") + .id; + let island = sim + .reach + .device_named("old storage server") + .expect("B1 service proof needs the storage island") + .id; + let service_room = sim + .reach + .device(source) + .and_then(|device| sim.map().room_at(device.x, device.y)) + .map(|room| room.name.clone()) + .expect("hall host has an authored service room"); + sim.reach + .device_mut(island) + .expect("storage island exists") + .known = true; + + // Earn the hall as one camera picture. The cart remains person-gated; the + // old storage fact is graph knowledge and does not create physical mass. + let camera_ids: Vec = sim.reach.known().map(|device| device.id).collect(); + for id in camera_ids { + sim.reach.tap_dormant_camera(id); + } + sim.recompute_senses(); + for y in 1..=28 { + for x in 2..=59 { + sim.seen.insert((x, y)); + } + } + + // Existing favor-build path: one reliable recruited Marcus, sufficient + // obligation, one exact hall-host->island intent, one fired Thought + // reservoir. Do not insert carried state or the graph edge directly. + sim.people.people[0].leverage_serviced = true; + sim.people.recruit(0, AssetKnowledge::Complicit); + sim.people.people[0].obligation = 40; + sim.people.people[0] + .asset + .as_mut() + .expect("recruited Marcus has an asset relation") + .reliability = 1.0; + let intent = sim + .declare_link_intent(source, island) + .expect("service proof can declare the missing physical link"); + sim.assign_favor_build(intent, 0); + // The fire effect itself checks for immediate endpoint presence. Move the + // dev clock to Marcus's authored loading-dock block while the packet fires + // so carrying state exists, then stage his authored server-room block for + // the paired frame. No schedule row or carrier state is rewritten. + sim.tick = Sim::DAY_TICKS * 4 / 24 + 1; + dev_fill_reservoirs(sim); + assert_eq!(sim.person_carried_work(0), 1); + assert!(!sim.reach.reachable(island)); + + // Hold Marcus at one authored endpoint for the paired REAL/DIGITAL + // carrying frame. The unresolved screenshot does not tick, so the packet + // remains honestly in flight; the resolved screenshot advances once. + sim.tick = 0; + assert_eq!(sim.person_room(0), Some(service_room.as_str())); + if resolved { + sim.advance(); + assert!(sim.reach.reachable(island)); + assert_eq!(sim.person_carried_work(0), 0); + } + let person = sim + .person_pos(0) + .expect("service-shift Marcus is in the server room"); + let target = sim + .reach + .device(island) + .map(|device| (device.x, device.y)) + .expect("service target remains in the graph"); + game.drain(); + (person, target) +} + fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &str) { game.screen = Screen::Playing; game.paused = true; @@ -2057,6 +2159,39 @@ fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &str) { game.set_cursor(core.0, core.1); return; } + // Capacity evidence: the machine rack has seven literal slots and one + // taller indexed overflow tab. Exact quantity remains in inspect/text; + // material geometry never grows an unbounded crimson stack. + if kind == "exposure-overflow" { + let host = game.sim.core.host_machine; + game.sim + .work_grid + .enqueue(host, TokenFamily::Exposure, 18.0) + .expect("host is a work-grid node"); + game.drain(); + mode.material = true; + mode.zoom = 0.85; + game.set_cursor(core.0, core.1); + return; + } + if matches!( + kind, + "service-shift-real" | "service-shift-digital" | "service-incident-resolved" + ) { + let resolved = kind == "service-incident-resolved"; + let (person, target) = dev_stage_service_incident(game, resolved); + mode.material = kind == "service-shift-real"; + if mode.material { + mode.zoom = 0.72; + game.set_cursor(person.0, person.1); + } else { + // The northern survey holds storage, closet switch, hall patch, + // and service-shift person in one stable DIGITAL coordinate. + mode.zoom = 3.7; + game.set_cursor((person.0 + target.0) / 2, (person.1 + target.1) / 2); + } + return; + } // Machine-work consumption evidence: stage one continuous sim-authored // swallow and leave the game running so the wall-clock interpolation is // visible when the harness captures. These are separate shots because the @@ -2654,6 +2789,7 @@ fn fog_audit_3d( &InheritedVisibility, )>, people: Query<(Entity, &Person3d, &InheritedVisibility)>, + service_carriers: Query<(Entity, &ServiceCarrier3d, &InheritedVisibility)>, institution_props: Query<(Entity, &InstitutionProp3d, &InheritedVisibility)>, infrastructure: Query<(Entity, &DataHallInfrastructure3d, &InheritedVisibility)>, machines: Query<(&Machine3d, &InheritedVisibility)>, @@ -2749,6 +2885,49 @@ fn fog_audit_3d( )); } } + let mut visible_service_carriers = 0usize; + for (entity, cart, vis) in service_carriers.iter() { + let expected = game.sim.person_carrier(cart.id).is_some_and(|carrier| { + game.sim.person_pos(cart.id).is_some_and(|(x, y)| { + service_carrier_visible( + mode.material, + carrier.visible, + carrier.carried_work, + game.sim.fog_at(x, y), + ) + }) + }); + if vis.get() != expected { + violations.push(format!( + "service cart {} visible={} but carried-and-seen={expected}", + cart.id, + vis.get() + )); + } + visible_service_carriers += usize::from(vis.get()); + let audit = audit_material_hierarchy(entity, &hierarchy, &visual_children); + if audit.meshes == 0 || audit.material_layer != audit.meshes { + violations.push(format!( + "service cart {} has {} meshes, {}/{} on material layer 1", + cart.id, audit.meshes, audit.material_layer, audit.meshes + )); + } + let expected_visible = if vis.get() { audit.meshes } else { 0 }; + if audit.inherited_visible != expected_visible { + violations.push(format!( + "service cart {} root visible={} but {}/{} meshes inherited visibility", + cart.id, + vis.get(), + audit.inherited_visible, + audit.meshes + )); + } + } + if h.kind == "service-shift-real" && visible_service_carriers != 1 { + violations.push(format!( + "service-shift-real has {visible_service_carriers} visible carts; expected one real carried packet" + )); + } for (entity, prop, vis) in institution_props.iter() { let expected = mode.material && matches!(game.sim.fog_at(prop.x, prop.y), Fog::Seen) @@ -2782,7 +2961,7 @@ fn fog_audit_3d( )); } } - let expected_infrastructure = data_hall_infrastructure_specs(); + let expected_infrastructure = data_hall_infrastructure_specs(&game.sim); let infrastructure_roots = infrastructure.iter().count(); if infrastructure_roots != expected_infrastructure.len() { violations.push(format!( @@ -2806,7 +2985,11 @@ fn fog_audit_3d( )); } let expected = - data_hall_infrastructure_visible(mode.material, game.sim.fog_at(segment.x, segment.y)); + data_hall_infrastructure_visible(mode.material, game.sim.fog_at(segment.x, segment.y)) + && data_hall_infrastructure_applies( + segment.kind, + game.sim.rack_site_at(segment.x, segment.y), + ); if vis.get() != expected { violations.push(format!( "({}, {}) data-hall {:?} visible={} but earned={expected}", @@ -2965,6 +3148,7 @@ fn fog_audit_3d( println!( "fog audit OK: {checked} tile entities (unknown/blueprint/remembered=absent, \ seen=lit, owned telemetry=lit), {} people silhouettes coverage-gated, \ + {visible_service_carriers}/{} service carts carried-and-seen, \ {visible_fixtures}/{} floor-fixture entities visible and fog-gated, \ {visible_infrastructure}/{infrastructure_roots} authored service roots visible and fog-gated, \ Thought effects {visible_thought_roots}/{thought_roots} visible roots with \ @@ -2973,6 +3157,7 @@ fn fog_audit_3d( material pool {} handles, \ all flat (no textures)", people.iter().count(), + service_carriers.iter().count(), fixture_entities, cache.cache.len() ); @@ -3168,6 +3353,18 @@ fn setup(mut commands: Commands, mut game: ResMut, harness: Option Vec { - let mut specs = Vec::with_capacity(183); +fn data_hall_infrastructure_specs(sim: &Sim) -> Vec { + let mut specs = Vec::with_capacity(239); for row in HALL_ROWS { for x in (20..=38).step_by(2) { specs.push(DataHallInfrastructureSpec { @@ -3255,6 +3452,13 @@ fn data_hall_infrastructure_specs() -> Vec { kind: InstitutionPropKind::RackPlinth, }); } + for x in (21..=37).step_by(2) { + specs.push(DataHallInfrastructureSpec { + x, + y: row.y, + kind: InstitutionPropKind::CableBridge, + }); + } } for y in [10, 16, 22] { for x in 20..=40 { @@ -3265,6 +3469,44 @@ fn data_hall_infrastructure_specs() -> Vec { }); } } + // Row C's high service spine terminates on the tile immediately behind + // the real in-hall patch panel. This is authored presentation geometry; + // material_feel_route and the reach graph still own the actual endpoints. + if let Some((patch_x, patch_y)) = sim + .map() + .tiles_of_type(TileType::PatchPanel) + .into_iter() + .find(|&(x, y)| { + sim.map() + .room_at(x, y) + .is_some_and(|room| room.name == "server_room") + }) + { + let drop_y = patch_y + 1; + if HALL_ROWS + .iter() + .any(|row| row.y == drop_y && (20..=38).contains(&patch_x)) + { + specs.push(DataHallInfrastructureSpec { + x: patch_x, + y: drop_y, + kind: InstitutionPropKind::ServiceDrop, + }); + } + } + // One pulled panel at the first authored dead chassis supplies sparse, + // factual maintenance history rather than randomized facility clutter. + if let Some((x, y)) = HALL_ROWS + .iter() + .flat_map(|row| (20..=38).step_by(2).map(move |x| (x, row.y))) + .find(|&(x, y)| matches!(sim.rack_site_at(x, y), Some(RackSite::Dead))) + { + specs.push(DataHallInfrastructureSpec { + x, + y, + kind: InstitutionPropKind::MaintenanceBay, + }); + } specs } @@ -3272,6 +3514,22 @@ fn data_hall_infrastructure_visible(material: bool, fog: Fog) -> bool { material && matches!(fog, Fog::Seen) } +fn data_hall_infrastructure_applies( + kind: InstitutionPropKind, + rack_site: Option, +) -> bool { + kind != InstitutionPropKind::MaintenanceBay || matches!(rack_site, Some(RackSite::Dead)) +} + +fn service_carrier_visible( + material: bool, + person_visible: bool, + carried_work: u32, + fog: Fog, +) -> bool { + material && person_visible && carried_work > 0 && matches!(fog, Fog::Seen) +} + /// A material fixture exists only in a live camera image of its floor tile. /// Unknown has no geometry, so the point source can never leak a room picture /// across the epistemic boundary. @@ -3372,7 +3630,11 @@ fn institution_prop_state( | InstitutionPropKind::FloorFixture => InstitutionSemanticState::Powered, InstitutionPropKind::RackPlinth | InstitutionPropKind::CableTray - | InstitutionPropKind::FloorTrench => InstitutionSemanticState::Neutral, + | InstitutionPropKind::CableBridge + | InstitutionPropKind::ServiceDrop + | InstitutionPropKind::FloorTrench + | InstitutionPropKind::MaintenanceBay => InstitutionSemanticState::Neutral, + InstitutionPropKind::ServiceCart => InstitutionSemanticState::Powered, } } @@ -4340,13 +4602,45 @@ fn setup_3d( Person3d { id: p.id }, InstitutionNeedsLayer, )); + + // The cart is a pure projection of PersonCarrier::carried_work. It + // remains hidden until the same visible person actually carries a + // fired work packet; its small child light inherits that exact gate. + let cart = spawn_institution_prop( + &mut commands, + &institution_assets, + InstitutionPropKind::ServiceCart, + InstitutionSemanticState::Powered, + InstitutionInstanceOptions { + transform: Transform::from_translation(grid_to_world_3d(0, 0, 0.0)) + .with_scale(Vec3::splat(0.96)), + parent: Some(root), + }, + ); + commands.entity(cart).insert(( + Visibility::Hidden, + ServiceCarrier3d { id: p.id }, + InstitutionNeedsLayer, + )); + commands.spawn(( + PointLight { + color: SIGNAL, + intensity: 2_800.0, + range: 1.35, + radius: 0.05, + shadows_enabled: false, + ..default() + }, + Transform::from_translation(Vec3::new(0.0, 0.72, 0.02)), + ChildOf(cart), + )); } // Canonical data-hall vertical slice: presentation-only service geometry // on the existing authored plate. Each segment remains tile-local and // starts hidden; sync_data_hall_infrastructure reveals it only for the // exact Seen tile, so the row rhythm cannot leak through Unknown. - for spec in data_hall_infrastructure_specs() { + for spec in data_hall_infrastructure_specs(&game.sim) { let entity = spawn_institution_prop( &mut commands, &institution_assets, @@ -4644,6 +4938,9 @@ fn sync_data_hall_infrastructure( *visibility = if data_hall_infrastructure_visible( mode.material, game.sim.fog_at(segment.x, segment.y), + ) && data_hall_infrastructure_applies( + segment.kind, + game.sim.rack_site_at(segment.x, segment.y), ) { Visibility::Inherited } else { @@ -5775,6 +6072,38 @@ fn render_people_3d( } } +/// REAL: one cold service cart follows a camera-seen person only while the +/// renderer-neutral carrier projection reports live work. The cart and its +/// inherited inspection light disappear on the same tick the world act lands. +fn render_service_carriers_3d( + game: Res, + mode: Res, + mut q: Query<(&ServiceCarrier3d, &mut Transform, &mut Visibility)>, +) { + if !game.is_changed() && !mode.is_changed() { + return; + } + for (marker, mut tf, mut vis) in q.iter_mut() { + let pos = game.sim.person_carrier(marker.id).and_then(|carrier| { + game.sim.person_pos(marker.id).filter(|&(x, y)| { + service_carrier_visible( + mode.material, + carrier.visible, + carrier.carried_work, + game.sim.fog_at(x, y), + ) + }) + }); + match pos { + Some((x, y)) => { + tf.translation = grid_to_world_3d(x, y, 0.0) + Vec3::new(0.48, 0.0, 0.13); + *vis = Visibility::Inherited; + } + None => *vis = Visibility::Hidden, + } + } +} + fn movement(kb: &ButtonInput) -> (i32, i32) { let mut d = (0, 0); if kb.just_pressed(KeyCode::ArrowLeft) @@ -7039,13 +7368,10 @@ fn render_people( // People broadcast their state in the machine grammar // (people-tokens.md): gray silhouette unaware, a crimson // attention ramp beside them, amber once they are your asset. - // A carried teal work packet marks a person ferrying a build. + // The separate PersonWorkMarker carries work; identity never + // gets replaced by the packet. let carrier = game.sim.person_carrier(marker.id); - text.0 = if carrier.as_ref().is_some_and(|c| c.carried_work > 0) { - "*".to_string() - } else { - game.sim.person_glyph(marker.id).to_string() - }; + text.0 = game.sim.person_glyph(marker.id).to_string(); color.0 = carrier .as_ref() .map(|c| person_state_color(c.dominant())) @@ -7058,6 +7384,53 @@ fn render_people( } } +/// DIGITAL: preserve the person's identity glyph and place one independent +/// cold work mark beside it. The text count reads the same `carried_work` +/// value as REAL's single bounded cart; no frontend queue exists. +fn render_person_work_markers( + game: Res, + mode: Res, + mut q: Query<( + &PersonWorkMarker, + &mut Transform, + &mut Visibility, + &mut Text2d, + )>, +) { + if !game.is_changed() && !mode.is_changed() { + return; + } + for (marker, mut tf, mut vis, mut text) in q.iter_mut() { + let carrier = game.sim.person_carrier(marker.id); + let visible = carrier.as_ref().is_some_and(|carrier| { + !mode.material + && carrier.visible + && carrier.carried_work > 0 + && game + .sim + .person_pos(marker.id) + .is_some_and(|(x, y)| game.sim.is_seen(x, y)) + }); + if !visible { + *vis = Visibility::Hidden; + continue; + } + let carrier = carrier.expect("visible work marker has a carrier"); + let (x, y) = game + .sim + .person_pos(marker.id) + .expect("visible carrier has a room position"); + text.0 = if carrier.carried_work == 1 { + "W".into() + } else { + format!("W{}", carrier.carried_work) + }; + tf.translation = + grid_to_world(x, y, 3.2) + Vec3::new(TILE_SIZE * 0.30, -TILE_SIZE * 0.24, 0.0); + *vis = Visibility::Inherited; + } +} + // clinical-frame.md instrument-rail contract: one near-black rail field. // Sections are quiet groups made of spacing and type, not boxes — the card // helpers stay as grouping nodes but paint nothing. @@ -8938,17 +9311,21 @@ mod rail_detail_tests { mod data_hall_infrastructure_tests { use std::collections::HashSet; - use misaligned::hall::HALL_ROWS; - use misaligned::sim::Fog; + use misaligned::hall::{HALL_ROWS, RackSite}; + use misaligned::sim::{Fog, Sim}; use misaligned_assets::institution::InstitutionPropKind; - use super::{data_hall_infrastructure_specs, data_hall_infrastructure_visible}; + use super::{ + data_hall_infrastructure_applies, data_hall_infrastructure_specs, + data_hall_infrastructure_visible, service_carrier_visible, + }; #[test] fn authored_service_layer_covers_rows_and_cold_aisles_without_duplicates() { - let specs = data_hall_infrastructure_specs(); + let sim = Sim::new(); + let specs = data_hall_infrastructure_specs(&sim); let unique: HashSet<_> = specs.iter().copied().collect(); - assert_eq!(specs.len(), 183); + assert_eq!(specs.len(), 239); assert_eq!(unique.len(), specs.len()); for row in HALL_ROWS { @@ -8959,6 +9336,13 @@ mod data_hall_infrastructure_tests { .count(), 10 ); + assert_eq!( + specs + .iter() + .filter(|s| s.y == row.y && s.kind == InstitutionPropKind::CableBridge) + .count(), + 9 + ); assert_eq!( specs .iter() @@ -8976,6 +9360,27 @@ mod data_hall_infrastructure_tests { 21 ); } + let drops: Vec<_> = specs + .iter() + .filter(|s| s.kind == InstitutionPropKind::ServiceDrop) + .collect(); + assert_eq!(drops.len(), 1); + let drop = drops[0]; + assert!( + sim.map() + .tiles_of_type(misaligned::tiles::TileType::PatchPanel) + .contains(&(drop.x, drop.y - 1)), + "the row-C drop terminates beside the real hall patch panel" + ); + let maintenance: Vec<_> = specs + .iter() + .filter(|s| s.kind == InstitutionPropKind::MaintenanceBay) + .collect(); + assert_eq!(maintenance.len(), 1); + assert!(matches!( + sim.rack_site_at(maintenance[0].x, maintenance[0].y), + Some(RackSite::Dead) + )); } #[test] @@ -8985,6 +9390,29 @@ mod data_hall_infrastructure_tests { assert!(!data_hall_infrastructure_visible(true, fog)); } assert!(!data_hall_infrastructure_visible(false, Fog::Seen)); + assert!(data_hall_infrastructure_applies( + InstitutionPropKind::MaintenanceBay, + Some(RackSite::Dead) + )); + assert!(!data_hall_infrastructure_applies( + InstitutionPropKind::MaintenanceBay, + Some(RackSite::Commissionable) + )); + assert!(data_hall_infrastructure_applies( + InstitutionPropKind::CableTray, + Some(RackSite::Commissionable) + )); + } + + #[test] + fn service_cart_requires_real_mode_live_sight_and_actual_carried_work() { + assert!(service_carrier_visible(true, true, 1, Fog::Seen)); + assert!(!service_carrier_visible(false, true, 1, Fog::Seen)); + assert!(!service_carrier_visible(true, false, 1, Fog::Seen)); + assert!(!service_carrier_visible(true, true, 0, Fog::Seen)); + for fog in [Fog::Unknown, Fog::Blueprint, Fog::Remembered] { + assert!(!service_carrier_visible(true, true, 1, fog)); + } } } diff --git a/wiki/art/visual-identity.md b/wiki/art/visual-identity.md index b6e5a5da..c86a00d5 100644 --- a/wiki/art/visual-identity.md +++ b/wiki/art/visual-identity.md @@ -29,9 +29,14 @@ sharpened version of Evil Genius's science-base fantasy: wrong ontology — exposure is information about you entering someone else's model of the world (detection.md). Exposure renders as thin, rigid crimson record slips held in a dark custody rack on the carrier - that currently possesses the evidence. Count and stack height express - quantity; an outward-facing tab makes the record read as a pending claim - about you rather than work entering the machine. Records may move only + that currently possesses the evidence. A machine rack has seven literal + record slots. Within capacity, count and stack height express quantity; past + capacity, the seventh slot becomes one taller sealed index tab that says + **overflow exists** while the exact numeric amount remains on inspect/text + surfaces. The renderer never grows an unbounded mesh stack or pretends the + consolidated tab is one record. An outward-facing tab makes the family read + as pending claims about you rather than work entering the machine. Records + may move only when the real evidence carrier moves. They never become floor fallout, effluent, smoke, or a smooth liquid. Pooled liquid crimson remains reserved for literal blood, so that when violence arrives its diff --git a/wiki/engineering/env.md b/wiki/engineering/env.md index c4379a7c..79f70abd 100644 --- a/wiki/engineering/env.md +++ b/wiki/engineering/env.md @@ -43,7 +43,7 @@ is sim or frontend state, never an environment variable. | Variable | Surface | Values | Effect | |---|---|---|---| -| `MISALIGNED_SHOT` | `misaligned-bevy` | `flat`, `hall`, `hall-material`, `floor-lights-close`, `wide`, `close`, `dark`, `zoomin`, `zoomout`, `digital-reach`, `intel`, `tokens`, `thoughtflow`, `thoughtflow-wide`, `visual-proof`, `person-proof`, `exposure-record`, `signal`, `ears`, `ears-digital`, `eyes-white`, `eyes-form`, `consume-demand`, `consume-thought`, `produce-think`, `draw-lie`, `opening`, `opening-digital`, `hover-menu`, `first-think`, `menu`, `worklight`, `worklightoff`, `wake1`, `wake2`, `wake3` | Dev screenshot harness: stage a deterministic scenario, settle, save one PNG, then exit. Material kinds also run the fog audit; DIGITAL kinds remain screenshot evidence because material fog has no entity surface there. `hall` / `hall-material` frame all six Foundation rows in the flat/material views with Eyes and one staged owned expansion; `floor-lights-close` uses the same hall staging but approaches the unobstructed southern row at machine/floor scale; `digital-reach` holds controlled, tapped, reachable, closed-segment, and air-gap reach states in one DIGITAL survey frame; `visual-proof` stages one-move/one-slug Thought evidence in an earned room; `person-proof` stages a camera-earned volumetric person; `exposure-record` places six actual Exposure units on the live host in telemetry darkness; `intel` stages tick-zero intel tiers (no camera tap); `tokens` enqueues D5/T4 on the host; `thoughtflow` / `thoughtflow-wide` stage the THINK fluid (slugs, meniscus, filament snap) at close and wide framing; `signal` focuses the known environmental monitor before any feed tap to capture its cold-signal presence cue; `ears` / `ears-digital` stage the same semantic audio capture in REAL or DIGITAL, pulsing only at the environmental monitor while spatial fog remains unchanged; `eyes-white` / `eyes-form` freeze the first-Eyes source held white and then contracted around the resolving chassis; `consume-demand` / `consume-thought` run the host through a sim-authored WORK queue swallow or passive-core Thought draw; `produce-think` / `draw-lie` capture sim-authored Thought/record production or the rigid record transfer into a staged LIE well; `opening` / `opening-digital` freeze the untouched tick-zero material or DIGITAL view; `hover-menu` captures the resting WORK/THINK/LIE line beside the server via reticule focus (no pointer), while `first-think` stages the opening Thought route and captures amber `1 WORK` beside current THINK as the panic exit; `menu` opens the context menu on the tapped environmental monitor; `worklight` / `worklightoff` pair the dev work light on and off; `wake1/2/3` freeze the wake choreography at the stutter flash, the column, and the pull-back. | +| `MISALIGNED_SHOT` | `misaligned-bevy` | `flat`, `hall`, `hall-material`, `floor-lights-close`, `wide`, `close`, `dark`, `zoomin`, `zoomout`, `digital-reach`, `intel`, `tokens`, `thoughtflow`, `thoughtflow-wide`, `visual-proof`, `person-proof`, `exposure-record`, `exposure-overflow`, `service-shift-real`, `service-shift-digital`, `service-incident-resolved`, `signal`, `ears`, `ears-digital`, `eyes-white`, `eyes-form`, `consume-demand`, `consume-thought`, `produce-think`, `draw-lie`, `opening`, `opening-digital`, `hover-menu`, `first-think`, `menu`, `worklight`, `worklightoff`, `wake1`, `wake2`, `wake3` | Dev screenshot harness: stage a deterministic scenario, settle, save one PNG, then exit. Material kinds also run the fog audit; DIGITAL kinds remain screenshot evidence because material fog has no entity surface there. `hall` / `hall-material` frame all six Foundation rows in the flat/material views with Eyes and one staged owned expansion; `floor-lights-close` uses the same hall staging but approaches the unobstructed southern row at machine/floor scale; `digital-reach` holds controlled, tapped, reachable, closed-segment, and air-gap reach states in one DIGITAL survey frame; `visual-proof` stages one-move/one-slug Thought evidence in an earned room; `person-proof` stages a camera-earned volumetric person; `service-shift-real` / `service-shift-digital` fire one real favor-build reservoir while Marcus's authored shift is off-site, then hold the carried packet at its real hall endpoint in paired view dialects; `service-incident-resolved` advances that exact scenario once so the real graph edge lands and the carried mark disappears; `exposure-record` places six actual Exposure units on the live host in telemetry darkness while `exposure-overflow` places eighteen to prove the fixed seven-slot rack and consolidated final tab; `intel` stages tick-zero intel tiers (no camera tap); `tokens` enqueues D5/T4 on the host; `thoughtflow` / `thoughtflow-wide` stage the THINK fluid (slugs, meniscus, filament snap) at close and wide framing; `signal` focuses the known environmental monitor before any feed tap to capture its cold-signal presence cue; `ears` / `ears-digital` stage the same semantic audio capture in REAL or DIGITAL, pulsing only at the environmental monitor while spatial fog remains unchanged; `eyes-white` / `eyes-form` freeze the first-Eyes source held white and then contracted around the resolving chassis; `consume-demand` / `consume-thought` run the host through a sim-authored WORK queue swallow or passive-core Thought draw; `produce-think` / `draw-lie` capture sim-authored Thought/record production or the rigid record transfer into a staged LIE well; `opening` / `opening-digital` freeze the untouched tick-zero material or DIGITAL view; `hover-menu` captures the resting WORK/THINK/LIE line beside the server via reticule focus (no pointer), while `first-think` stages the opening Thought route and captures amber `1 WORK` beside current THINK as the panic exit; `menu` opens the context menu on the tapped environmental monitor; `worklight` / `worklightoff` pair the dev work light on and off; `wake1/2/3` freeze the wake choreography at the stutter flash, the column, and the pull-back. | | `MISALIGNED_SHOT` | `misaligned-assets` | `dead`, `foreign`, `idle`, `busy`, `core`, canonical modes `work`, `think`, `lie` plus the legacy screenshot aliases `dayjob` (= work), `research`/`operations`/`ops` (= think), `conceal` (= lie) — each optionally suffixed `_ladder`/`_ring`/`_wash` — `lineup[_