//! 2.5D office: pitched camera at 45° with ZERO yaw — the world's square //! footprint stays a rectangle on screen (no diamond), while walls, desks and //! people have real height. XZ is the ground plane (screen-top = -Z). use bevy::prelude::*; use crate::company::CompanyCfg; pub const OFFICE_W: f32 = 1280.0; // X extent pub const OFFICE_H: f32 = 800.0; // Z extent pub const AGENT_Y: f32 = 22.0; // capsule center height /// Ground-plane helper: world Vec3 → (x, z). #[inline] pub fn flat(v: Vec3) -> Vec2 { Vec2::new(v.x, v.z) } /// Named spots agents can go to. Vec2 = (x, z). #[derive(Resource, Clone)] pub struct Spots { pub meeting: Vec2, pub meeting_seats: Vec, pub desks: Vec, // one per person, index = person index pub break_room: Vec2, pub entrance: Vec2, } /// A desk monitor — clicking it selects the person who owns the machine. #[derive(Component)] pub struct DeskOwner(pub usize); impl Spots { pub fn lookup(&self, place: &str, person: usize) -> Option { match place.to_lowercase().trim() { "meeting" | "conference" | "standup" | "table" => Some(self.meeting), "break" | "kitchen" | "coffee" | "lunch" => Some(self.break_room), "desk" | "workstation" | "office" | "manager's office" | "" => { Some(self.desks.get(person).copied().unwrap_or(self.entrance)) } "entrance" | "door" => Some(self.entrance), _ => None, } } } /// Synthetic floor for headless runs: consistent positions, nothing renders. pub fn headless_spots(cfg: &CompanyCfg) -> Spots { let cols = 5usize; let desks: Vec = (0..cfg.people.len()) .map(|i| { Vec2::new( -480.0 + (i % cols) as f32 * 240.0, 60.0 + (i / cols) as f32 * 200.0, ) }) .collect(); Spots { meeting: Vec2::new(0.0, -320.0), meeting_seats: desks.clone(), desks, break_room: Vec2::new(560.0, -320.0), entrance: Vec2::new(0.0, 360.0), } } fn spawn_box( commands: &mut Commands, meshes: &mut Assets, materials: &mut Assets, size: Vec3, center: Vec3, color: Color, ) { let alpha = color.alpha(); let material = StandardMaterial { base_color: color, perceptual_roughness: 0.9, alpha_mode: if alpha < 1.0 { AlphaMode::Blend } else { AlphaMode::Opaque }, ..default() }; commands.spawn(( Mesh3d(meshes.add(Cuboid::from_size(size))), MeshMaterial3d(materials.add(material)), Transform::from_translation(center), )); } /// Floating billboard label. fn label(commands: &mut Commands, font: &Handle, text: &str, pos: Vec3, size: f32, color: Color) { commands.spawn(( Text2d::new(text.to_string()), TextFont { font: font.clone().into(), font_size: size.into(), ..default() }, TextColor(color), Transform::from_translation(pos), )); } pub fn spawn_office( mut commands: Commands, cfg: Res, font: Res, mut meshes: ResMut>, mut materials: ResMut>, ) { let hw = OFFICE_W / 2.0; let hh = OFFICE_H / 2.0; // --- Floor slab --- spawn_box( &mut commands, &mut meshes, &mut materials, Vec3::new(OFFICE_W, 2.0, OFFICE_H), Vec3::new(0.0, -1.0, 0.0), Color::srgb(0.90, 0.90, 0.86), ); // --- Perimeter walls (height 36) --- let wall = Color::srgb(0.30, 0.30, 0.34); let wt = 10.0; let wh = 36.0; spawn_box( &mut commands, &mut meshes, &mut materials, Vec3::new(OFFICE_W + wt * 2.0, wh, wt), Vec3::new(0.0, wh / 2.0, -hh - wt / 2.0), wall, ); // far / top spawn_box( &mut commands, &mut meshes, &mut materials, Vec3::new(OFFICE_W + wt * 2.0, wh, wt), Vec3::new(0.0, wh / 2.0, hh + wt / 2.0), wall, ); // near / bottom spawn_box( &mut commands, &mut meshes, &mut materials, Vec3::new(wt, wh, OFFICE_H), Vec3::new(-hw - wt / 2.0, wh / 2.0, 0.0), wall, ); // left spawn_box( &mut commands, &mut meshes, &mut materials, Vec3::new(wt, wh, OFFICE_H), Vec3::new(hw + wt / 2.0, wh / 2.0, 0.0), wall, ); // right // --- Meeting room: left third, glass partition with doorway (z −80..40) --- let meeting_w = 380.0; let meeting_x = -hw + meeting_w / 2.0; let partition_x = -hw + meeting_w; spawn_box( &mut commands, &mut meshes, &mut materials, Vec3::new(meeting_w, 0.5, OFFICE_H), Vec3::new(meeting_x, 0.3, 0.0), Color::srgb(0.82, 0.86, 0.92), ); let glass = Color::srgba(0.55, 0.70, 0.85, 0.5); spawn_box( &mut commands, &mut meshes, &mut materials, Vec3::new(6.0, wh, 320.0), Vec3::new(partition_x, wh / 2.0, -240.0), glass, ); spawn_box( &mut commands, &mut meshes, &mut materials, Vec3::new(6.0, wh, 360.0), Vec3::new(partition_x, wh / 2.0, 220.0), glass, ); // Meeting table let table_z = -40.0; spawn_box( &mut commands, &mut meshes, &mut materials, Vec3::new(240.0, 16.0, 110.0), Vec3::new(meeting_x, 8.0, table_z), Color::srgb(0.45, 0.32, 0.20), ); label( &mut commands, &font.0, "MEETING ROOM", Vec3::new(meeting_x, 58.0, -320.0), 20.0, Color::srgb(0.25, 0.32, 0.45), ); // --- Break room: bottom-right --- let break_room = Vec2::new(hw - 130.0, hh - 110.0); spawn_box( &mut commands, &mut meshes, &mut materials, Vec3::new(240.0, 0.5, 200.0), Vec3::new(break_room.x, 0.3, break_room.y), Color::srgb(0.88, 0.85, 0.78), ); spawn_box( &mut commands, &mut meshes, &mut materials, Vec3::new(90.0, 14.0, 50.0), Vec3::new(break_room.x, 7.0, break_room.y), Color::srgb(0.50, 0.40, 0.30), ); label( &mut commands, &font.0, "BREAK ROOM", Vec3::new(break_room.x, 46.0, break_room.y - 60.0), 17.0, Color::srgb(0.35, 0.30, 0.22), ); // --- Desks: right side, 2 rows × 5, each labeled with owner + role --- let mut desks = Vec::new(); let start_x = -hw + meeting_w + 140.0; for (i, person) in cfg.people.iter().enumerate() { let row = i / 5; let col = i % 5; let x = start_x + col as f32 * 155.0; let z = -210.0 + row as f32 * 250.0; spawn_box( &mut commands, &mut meshes, &mut materials, Vec3::new(110.0, 18.0, 56.0), Vec3::new(x, 9.0, z), Color::srgb(0.72, 0.66, 0.55), ); // monitor (far edge of desk) — clickable: selects its person so the // work browser's MY COMPUTER folder shows that machine's files. commands .spawn(( Mesh3d(meshes.add(Cuboid::from_size(Vec3::new(44.0, 26.0, 6.0)))), MeshMaterial3d(materials.add(StandardMaterial { base_color: if person.is_manager { Color::srgb(0.45, 0.15, 0.15) } else { Color::srgb(0.15, 0.15, 0.18) }, emissive: LinearRgba::rgb(0.05, 0.06, 0.08), ..default() })), Transform::from_translation(Vec3::new(x, 31.0, z - 22.0)), DeskOwner(i), )); // (screen glow face is implied by emissive tint) // chair (near side) spawn_box( &mut commands, &mut meshes, &mut materials, Vec3::new(30.0, 12.0, 30.0), Vec3::new(x, 6.0, z + 50.0), Color::srgb(0.25, 0.25, 0.28), ); let name_color = if person.is_manager { Color::srgb(0.75, 0.15, 0.15) } else { Color::srgb(0.18, 0.22, 0.30) }; label( &mut commands, &font.0, &format!("{} — {}", person.name, person.role), Vec3::new(x, 64.0, z), 14.0, name_color, ); desks.push(Vec2::new(x, z + 52.0)); // chair = sitting spot } let entrance = Vec2::new(0.0, hh - 40.0); label( &mut commands, &font.0, "ENTRANCE", Vec3::new(entrance.x, 40.0, entrance.y + 12.0), 16.0, Color::srgb(0.30, 0.30, 0.30), ); // Company sign above the far wall label( &mut commands, &font.0, &cfg.company.name.to_uppercase(), Vec3::new(0.0, 76.0, -hh - 16.0), 26.0, Color::srgb(0.85, 0.75, 0.40), ); // Meeting seats: two banks flanking the table let mut seats = Vec::new(); for i in 0..10 { let side = i % 2; let rank = i / 2; let x = meeting_x - 100.0 + rank as f32 * 50.0; let z = table_z + if side == 0 { -75.0 } else { 75.0 }; seats.push(Vec2::new(x, z)); } // --- Lights: key above-front, fill behind-left --- commands.spawn(( DirectionalLight { illuminance: 12_000.0, ..default() }, Transform::from_xyz(300.0, 900.0, 700.0).looking_at(Vec3::ZERO, Vec3::Y), )); commands.spawn(( DirectionalLight { illuminance: 4_000.0, ..default() }, Transform::from_xyz(-400.0, 600.0, -500.0).looking_at(Vec3::ZERO, Vec3::Y), )); // --- Camera: 45° pitch, ZERO yaw → rectangle stays rectangle, real depth --- // Positioned so nothing clips through the near/far planes: the look-at // point is the room center, ortho box covers the whole floor. commands.spawn(( Camera3d::default(), Projection::Orthographic(OrthographicProjection { scale: 1.0, near: -2000.0, far: 2000.0, ..OrthographicProjection::default_3d() }), Transform::from_xyz(0.0, 700.0, 700.0).looking_at(Vec3::ZERO, Vec3::Y), )); commands.insert_resource(Spots { meeting: Vec2::new(meeting_x, table_z), meeting_seats: seats, desks, break_room, entrance, }); }