swarm / office/src/scene.rs

raw file 10738 bytes
//! 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,
    });
}