codecraft 0.1.1

A minimalist 3D game engine built on parts of Bevy (ECS, color) with wgpu and winit: OpenPBR materials, clustered lighting, an immediate-mode UI, audio and gamepad haptics
Documentation
//! The orbit rig reaching the screen.
//!
//! The frame is drawn from [`Views`], and the renderer sets the [`Camera`]
//! resource back to the first view's before it draws. A rig that only wrote
//! the resource would be undone every frame -- the camera would sit at its
//! default however far the mouse was dragged -- so the rig has to point the
//! view itself. These are the tests that would have caught that.
use codecraft::dev::DevPlugin;
use codecraft::ecs::Application;
use codecraft::gizmos::GizmosPlugin;
use codecraft::glam::Vec3;
use codecraft::render3d::Render3dPlugin;
use codecraft::ui::{CursorPosition, MouseInput, ScreenSize, UiPlugin};
use codecraft::{Camera, OrbitCamera, Views};

/// The plugins an `AppState` is built from, minus the ones that need
/// hardware.
fn app() -> Application {
    let mut app = Application::new();
    app.add_plugin(UiPlugin);
    app.add_plugin(Render3dPlugin);
    app.add_plugin(GizmosPlugin);
    app.add_plugin(DevPlugin);
    app.world.insert_resource(ScreenSize {
        width: 800.0,
        height: 600.0,
    });
    app.world.spawn(OrbitCamera::new(Vec3::ZERO, 10.0));
    app
}

/// What the renderer does with the views before drawing: fits them, and sets
/// the camera resource back from the first.
fn render(app: &mut Application) {
    let mut views = app.world.resource::<Views>().clone();
    views.fit(800.0, 600.0);
    let camera = views.camera();
    app.world.insert_resource(views);
    app.world.insert_resource(camera);
}

/// Holds the right button and moves the cursor across two frames, rendering
/// between them the way the app does.
fn drag(app: &mut Application) {
    app.world.insert_resource(CursorPosition { x: 100.0, y: 100.0 });
    app.world.resource_mut::<MouseInput>().right_down = true;
    app.update();
    render(app);
    app.world.insert_resource(CursorPosition { x: 300.0, y: 150.0 });
    app.update();
    render(app);
}

#[test]
fn the_rig_points_the_view_it_is_drawn_from() {
    let mut app = app();
    let before = app.world.resource::<Views>().camera();
    drag(&mut app);
    let views = app.world.resource::<Views>();
    assert_ne!(views.camera(), before, "the drag never reached the view");
    assert_eq!(
        views.camera(),
        *app.world.resource::<Camera>(),
        "the view and the camera resource disagree",
    );
}

#[test]
fn the_rig_survives_the_scene_handing_it_a_fresh_view_every_frame() {
    // What a scene in dev mode does: `set_views(Views::one(app.camera()))`
    // before every frame. With the renderer writing the camera back from the
    // view, that used to pin the rig at the default camera for good.
    let mut app = app();
    let mut seen = Vec::new();
    for x in [100.0, 200.0, 300.0, 400.0] {
        let camera = *app.world.resource::<Camera>();
        app.world.insert_resource(Views::one(camera));
        app.world.insert_resource(CursorPosition { x, y: 100.0 });
        app.world.resource_mut::<MouseInput>().right_down = true;
        app.update();
        render(&mut app);
        seen.push(app.world.resource::<Views>().camera().eye);
    }
    assert!(
        seen.windows(2).skip(1).all(|pair| pair[0] != pair[1]),
        "the eye stopped moving: {seen:?}",
    );
}

#[test]
fn a_split_screen_is_left_to_the_scene() {
    let mut app = app();
    let (left, right) = (
        Camera::looking_at(Vec3::new(-5.0, 2.0, 5.0), Vec3::ZERO),
        Camera::looking_at(Vec3::new(5.0, 2.0, 5.0), Vec3::ZERO),
    );
    app.world
        .insert_resource(Views::split(left, right, 800.0, 600.0));
    drag(&mut app);
    let views = app.world.resource::<Views>();
    let cameras: Vec<Camera> = views.iter().map(|view| view.camera).collect();
    assert_eq!(cameras, [left, right], "the rig took over a split screen");
}