ponko 0.0.1

A fast 2D game engine for simulating millions of things: ECS on all cores, hot reload, an in-game inspector, AI-agent control, and a C++ API.
Documentation
#![cfg(feature = "3d")]
use ponko::*;

#[test]
fn probe_weights_fade_inside_box_and_projection_hits_wall() {
    let probe = ReflectionProbe3D::default();
    assert_eq!(probe.weight(Vec3::ZERO, Vec3::ZERO), 1.0);
    assert_eq!(probe.weight(Vec3::ZERO, vec3(6.0, 0.0, 0.0)), 0.0);
    assert_eq!(
        probe.project(Vec3::ZERO, vec3(1.0, 0.0, 0.0), Vec3::Z),
        vec3(1.0, 0.0, 5.0)
    );
    let invalid = ReflectionProbe3D {
        size: Vec3::splat(f32::NAN),
        ..probe
    };
    assert_eq!(invalid.weight(Vec3::ZERO, Vec3::ZERO), 0.0);
}

#[test]
fn bake_requires_a_live_probe_and_allocates_content_once() {
    Game::new("probe", 1, 1).run_headless(0, |ctx| {
        let entity = ctx.world.spawn((Transform3D::default(), ReflectionProbe3D::default()));
        ctx.bake_reflection_probe(entity).unwrap();
        let content = ctx.world.get::<ReflectionProbe3D>(entity).unwrap().content;
        assert_ne!(content, 0);
        ctx.bake_reflection_probe(entity).unwrap();
        assert_eq!(ctx.world.get::<ReflectionProbe3D>(entity).unwrap().content, content);
        let empty = ctx.world.spawn((0u32,));
        assert!(ctx.bake_reflection_probe(empty).is_err());
    });
}

fn write_image(name: &str, width: u32, height: u32, value: f32) -> String {
    let path = std::env::temp_dir().join(format!("crabby-probe-{}-{name}.exr", std::process::id()));
    image::Rgb32FImage::from_pixel(width, height, image::Rgb([value; 3]))
        .save(&path)
        .unwrap();
    path.to_str().unwrap().to_string()
}

#[test]
fn loading_needs_a_two_to_one_finite_image() {
    let good = write_image("good", 64, 32, 1.0);
    let square = write_image("square", 32, 32, 1.0);
    let broken = write_image("nan", 64, 32, f32::NAN);
    Game::new("probe", 1, 1).run_headless(0, |ctx| {
        let entity = ctx.world.spawn((Transform3D::default(), ReflectionProbe3D::default()));
        let bad = ctx.load_reflection_probe(entity, &square).unwrap_err();
        assert!(bad.contains("2:1"), "{bad}");
        assert!(
            ctx.load_reflection_probe(entity, &broken)
                .unwrap_err()
                .contains("finite")
        );
        assert!(ctx.load_reflection_probe(entity, "missing-probe.exr").is_err());
        assert_eq!(ctx.world.get::<ReflectionProbe3D>(entity).unwrap().content, 0);
        ctx.load_reflection_probe(entity, &good).unwrap();
        assert_ne!(ctx.world.get::<ReflectionProbe3D>(entity).unwrap().content, 0);
    });
    for path in [good, square, broken] {
        std::fs::remove_file(path).ok();
    }
}

#[test]
fn a_fifth_environment_is_refused() {
    Game::new("probe", 1, 1).run_headless(0, |ctx| {
        for _ in 0..4 {
            let e = ctx.world.spawn((Transform3D::default(), ReflectionProbe3D::default()));
            ctx.bake_reflection_probe(e).unwrap();
        }
        let fifth = ctx.world.spawn((Transform3D::default(), ReflectionProbe3D::default()));
        assert!(ctx.bake_reflection_probe(fifth).unwrap_err().contains("four"));
    });
}