use serde::Serialize;
use serde_json::Value;
use crate::{Entity, JsonError, Light, LightKind, Resource, World};
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct Environment {
pub sun: Option<Entity>,
pub ambient: Option<Ambient>,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Ambient {
pub intensity: f32,
pub light: AmbientLight,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub enum AmbientLight {
Flat([f32; 3]),
Gradient {
sky: [f32; 3],
equator: [f32; 3],
ground: [f32; 3],
},
}
impl AmbientLight {
pub fn colours(&self) -> Vec<[f32; 3]> {
match self {
AmbientLight::Flat(c) => vec![*c],
AmbientLight::Gradient { sky, equator, ground } => vec![*sky, *equator, *ground],
}
}
}
impl Ambient {
fn verify(&self) -> Result<(), String> {
let ok = |x: f32| x.is_finite() && x >= 0.0;
if !ok(self.intensity) {
return Err(format!(
"ambient intensity {} is not finite and at least 0",
self.intensity
));
}
match self
.light
.colours()
.into_iter()
.find(|c| !c.iter().all(|&x| ok(x)))
{
Some(c) => Err(format!("ambient colour {c:?} is not finite and at least 0")),
None => Ok(()),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
pub struct TimeOfDay {
pub hour: f32,
}
impl Resource for TimeOfDay {
const NAME: &'static str = "core.time_of_day";
fn to_json(&self) -> Result<Option<Value>, JsonError> {
crate::json::to_value(self).map(Some)
}
fn verify(&self, _w: &World) -> Result<(), String> {
if !(self.hour.is_finite() && (0.0..=24.0).contains(&self.hour)) {
return Err(format!("hour {} is not a finite number from 0 to 24", self.hour));
}
Ok(())
}
}
impl Resource for Environment {
const NAME: &'static str = "core.environment";
const VERSION: u32 = 2;
fn to_json(&self) -> Result<Option<Value>, JsonError> {
crate::json::to_value(self).map(Some)
}
fn verify(&self, w: &World) -> Result<(), String> {
match self.sun {
Some(e) if !matches!(w.get::<Light>(e), Some(l) if l.kind == LightKind::Directional) => {
return Err(format!("sun {e:?} is not a directional core.light"));
}
_ => {}
}
self.ambient.as_ref().map_or(Ok(()), Ambient::verify)
}
fn references(&self, out: &mut Vec<Entity>) {
out.extend(self.sun);
}
fn retain_references(&mut self, keep: &dyn Fn(Entity) -> bool) {
if self.sun.is_some_and(|e| !keep(e)) {
self.sun = None;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Transform;
use std::collections::BTreeSet;
#[test]
fn the_time_of_day_is_an_hour_of_the_day() {
let w = World::new();
for good in [0.0, 12.5, 24.0] {
assert_eq!(TimeOfDay { hour: good }.verify(&w), Ok(()), "{good}");
}
for bad in [-0.5, 24.5, f32::NAN, f32::INFINITY] {
assert!(TimeOfDay { hour: bad }.verify(&w).is_err(), "{bad}");
}
}
fn world() -> (World, Entity, Entity) {
let mut w = World::new();
let sun = w.spawn();
w.insert(
sun,
Light {
kind: LightKind::Directional,
color: [1.0, 0.95, 0.9],
intensity: 3.0,
range: None,
cast_shadows: Some(true),
radius: None,
specular: None,
},
);
w.insert(sun, Transform::IDENTITY);
let lamp = w.spawn();
w.insert(
lamp,
Light {
kind: LightKind::Point,
color: [1.0; 3],
intensity: 10.0,
range: Some(5.0),
cast_shadows: None,
radius: None,
specular: None,
},
);
w.insert_resource(Environment {
sun: Some(sun),
ambient: None,
});
(w, sun, lamp)
}
fn sun_of(w: &World) -> Option<Entity> {
w.resource::<Environment>().unwrap().sun
}
#[test]
fn the_sun_survives_a_subset_that_keeps_it_and_is_forgotten_by_one_that_does_not() {
let (w, sun, lamp) = world();
assert!(w.verify_resources().is_empty());
let mut out = Vec::new();
Resource::references(w.resource::<Environment>().unwrap(), &mut out);
assert_eq!(out, [sun]);
let with = w.subset(&w.closure([sun]));
assert_eq!(sun_of(&with), Some(sun));
let without = w.subset(&BTreeSet::from([lamp]));
assert_eq!(sun_of(&without), None, "no reference to an entity the part lacks");
assert!(without.verify_resources().is_empty());
assert_eq!(sun_of(&w), Some(sun), "the source world keeps its sun");
let mut split = w.clone();
let part = split.split_off(&BTreeSet::from([lamp]));
assert_eq!((sun_of(&part), sun_of(&split)), (None, Some(sun)));
let part = split.split_off(&BTreeSet::from([sun]));
assert_eq!(sun_of(&part), Some(sun));
split.absorb(part).unwrap();
assert!(split.verify_resources().is_empty());
}
#[test]
fn despawning_or_dropping_the_sun_forgets_it() {
let (mut w, sun, lamp) = world();
w.retain(&BTreeSet::from([sun, lamp]));
assert_eq!(sun_of(&w), Some(sun));
w.retain(&BTreeSet::from([lamp]));
assert_eq!(sun_of(&w), None);
let (mut w, sun, _) = world();
w.despawn(sun);
assert_eq!(sun_of(&w), None);
assert!(w.verify_resources().is_empty());
}
#[test]
fn the_sun_must_be_a_directional_light() {
let (mut w, _, lamp) = world();
w.insert_resource(Environment {
sun: Some(lamp),
ambient: None,
});
assert_eq!(
w.verify_resources(),
[(
"core.environment",
format!("sun {lamp:?} is not a directional core.light")
)]
);
assert_eq!(
Environment {
sun: Some(lamp),
ambient: None
}
.to_json()
.unwrap(),
Some(serde_json::json!({"sun": lamp.index(), "ambient": null}))
);
}
#[test]
fn the_ambient_is_kept_by_every_part_and_must_be_finite_and_not_negative() {
let (mut w, sun, lamp) = world();
let gradient = Ambient {
intensity: 1.0,
light: AmbientLight::Gradient {
sky: [0.3, 0.3, 0.4],
equator: [0.5; 3],
ground: [0.25; 3],
},
};
w.insert_resource(Environment {
sun: Some(sun),
ambient: Some(gradient.clone()),
});
assert!(w.verify_resources().is_empty());
let part = w.subset(&BTreeSet::from([lamp]));
assert_eq!(
part.resource::<Environment>().unwrap().ambient,
Some(gradient.clone())
);
assert_eq!(gradient.light.colours(), [[0.3, 0.3, 0.4], [0.5; 3], [0.25; 3]]);
assert_eq!(AmbientLight::Flat([0.2; 3]).colours(), [[0.2; 3]]);
assert_eq!(
w.resource::<Environment>().unwrap().to_json().unwrap(),
Some(
serde_json::json!({"sun": sun.index(), "ambient": {"intensity": 1.0, "light": {"Gradient": {
"sky": [0.30000001192092896, 0.30000001192092896, 0.4000000059604645],
"equator": [0.5, 0.5, 0.5], "ground": [0.25, 0.25, 0.25]}}}})
)
);
for (intensity, colour, bad) in [
(
-1.0,
[0.5; 3],
"ambient intensity -1 is not finite and at least 0",
),
(
f32::NAN,
[0.5; 3],
"ambient intensity NaN is not finite and at least 0",
),
(
1.0,
[0.5, -0.1, 0.5],
"ambient colour [0.5, -0.1, 0.5] is not finite and at least 0",
),
(
1.0,
[f32::INFINITY; 3],
"ambient colour [inf, inf, inf] is not finite and at least 0",
),
] {
w.insert_resource(Environment {
sun: None,
ambient: Some(Ambient {
intensity,
light: AmbientLight::Flat(colour),
}),
});
assert_eq!(w.verify_resources(), [("core.environment", bad.to_owned())]);
}
}
}