//! 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<Vec2>,
pub desks: Vec<Vec2>, // 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<Vec2> {
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<Vec2> = (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<Mesh>,
materials: &mut Assets<StandardMaterial>,
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<Font>, 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<CompanyCfg>,
font: Res<crate::UIFont>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
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,
});
}