1use serde::Serialize;
4use serde_json::Value;
5
6use crate::{Entity, JsonError, Light, LightKind, Resource, World};
7
8#[derive(Clone, Debug, Default, PartialEq, Serialize)]
14pub struct Environment {
15 pub sun: Option<Entity>,
16 pub ambient: Option<Ambient>,
17}
18
19#[derive(Clone, Debug, PartialEq, Serialize)]
24pub struct Ambient {
25 pub intensity: f32,
26 pub light: AmbientLight,
27}
28
29#[derive(Clone, Debug, PartialEq, Serialize)]
31pub enum AmbientLight {
32 Flat([f32; 3]),
34 Gradient {
37 sky: [f32; 3],
38 equator: [f32; 3],
39 ground: [f32; 3],
40 },
41}
42
43impl AmbientLight {
44 pub fn colours(&self) -> Vec<[f32; 3]> {
46 match self {
47 AmbientLight::Flat(c) => vec![*c],
48 AmbientLight::Gradient { sky, equator, ground } => vec![*sky, *equator, *ground],
49 }
50 }
51}
52
53impl Ambient {
54 fn verify(&self) -> Result<(), String> {
56 let ok = |x: f32| x.is_finite() && x >= 0.0;
57 if !ok(self.intensity) {
58 return Err(format!(
59 "ambient intensity {} is not finite and at least 0",
60 self.intensity
61 ));
62 }
63 match self
64 .light
65 .colours()
66 .into_iter()
67 .find(|c| !c.iter().all(|&x| ok(x)))
68 {
69 Some(c) => Err(format!("ambient colour {c:?} is not finite and at least 0")),
70 None => Ok(()),
71 }
72 }
73}
74
75#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
81pub struct TimeOfDay {
82 pub hour: f32,
84}
85
86impl Resource for TimeOfDay {
87 const NAME: &'static str = "core.time_of_day";
88 fn to_json(&self) -> Result<Option<Value>, JsonError> {
89 crate::json::to_value(self).map(Some)
90 }
91 fn verify(&self, _w: &World) -> Result<(), String> {
93 if !(self.hour.is_finite() && (0.0..=24.0).contains(&self.hour)) {
94 return Err(format!("hour {} is not a finite number from 0 to 24", self.hour));
95 }
96 Ok(())
97 }
98}
99
100impl Resource for Environment {
101 const NAME: &'static str = "core.environment";
102 const VERSION: u32 = 2;
104 fn to_json(&self) -> Result<Option<Value>, JsonError> {
105 crate::json::to_value(self).map(Some)
106 }
107 fn verify(&self, w: &World) -> Result<(), String> {
109 match self.sun {
110 Some(e) if !matches!(w.get::<Light>(e), Some(l) if l.kind == LightKind::Directional) => {
111 return Err(format!("sun {e:?} is not a directional core.light"));
112 }
113 _ => {}
114 }
115 self.ambient.as_ref().map_or(Ok(()), Ambient::verify)
116 }
117 fn references(&self, out: &mut Vec<Entity>) {
118 out.extend(self.sun);
119 }
120 fn retain_references(&mut self, keep: &dyn Fn(Entity) -> bool) {
121 if self.sun.is_some_and(|e| !keep(e)) {
122 self.sun = None;
123 }
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::Transform;
131 use std::collections::BTreeSet;
132
133 #[test]
135 fn the_time_of_day_is_an_hour_of_the_day() {
136 let w = World::new();
137 for good in [0.0, 12.5, 24.0] {
138 assert_eq!(TimeOfDay { hour: good }.verify(&w), Ok(()), "{good}");
139 }
140 for bad in [-0.5, 24.5, f32::NAN, f32::INFINITY] {
141 assert!(TimeOfDay { hour: bad }.verify(&w).is_err(), "{bad}");
142 }
143 }
144
145 fn world() -> (World, Entity, Entity) {
147 let mut w = World::new();
148 let sun = w.spawn();
149 w.insert(
150 sun,
151 Light {
152 kind: LightKind::Directional,
153 color: [1.0, 0.95, 0.9],
154 intensity: 3.0,
155 range: None,
156 cast_shadows: Some(true),
157 radius: None,
158 specular: None,
159 },
160 );
161 w.insert(sun, Transform::IDENTITY);
162 let lamp = w.spawn();
163 w.insert(
164 lamp,
165 Light {
166 kind: LightKind::Point,
167 color: [1.0; 3],
168 intensity: 10.0,
169 range: Some(5.0),
170 cast_shadows: None,
171 radius: None,
172 specular: None,
173 },
174 );
175 w.insert_resource(Environment {
176 sun: Some(sun),
177 ambient: None,
178 });
179 (w, sun, lamp)
180 }
181
182 fn sun_of(w: &World) -> Option<Entity> {
183 w.resource::<Environment>().unwrap().sun
184 }
185
186 #[test]
187 fn the_sun_survives_a_subset_that_keeps_it_and_is_forgotten_by_one_that_does_not() {
188 let (w, sun, lamp) = world();
189 assert!(w.verify_resources().is_empty());
190 let mut out = Vec::new();
191 Resource::references(w.resource::<Environment>().unwrap(), &mut out);
192 assert_eq!(out, [sun]);
193 let with = w.subset(&w.closure([sun]));
194 assert_eq!(sun_of(&with), Some(sun));
195 let without = w.subset(&BTreeSet::from([lamp]));
196 assert_eq!(sun_of(&without), None, "no reference to an entity the part lacks");
197 assert!(without.verify_resources().is_empty());
198 assert_eq!(sun_of(&w), Some(sun), "the source world keeps its sun");
199 let mut split = w.clone();
201 let part = split.split_off(&BTreeSet::from([lamp]));
202 assert_eq!((sun_of(&part), sun_of(&split)), (None, Some(sun)));
203 let part = split.split_off(&BTreeSet::from([sun]));
204 assert_eq!(sun_of(&part), Some(sun));
205 split.absorb(part).unwrap();
206 assert!(split.verify_resources().is_empty());
207 }
208
209 #[test]
210 fn despawning_or_dropping_the_sun_forgets_it() {
211 let (mut w, sun, lamp) = world();
212 w.retain(&BTreeSet::from([sun, lamp]));
213 assert_eq!(sun_of(&w), Some(sun));
214 w.retain(&BTreeSet::from([lamp]));
215 assert_eq!(sun_of(&w), None);
216 let (mut w, sun, _) = world();
217 w.despawn(sun);
218 assert_eq!(sun_of(&w), None);
219 assert!(w.verify_resources().is_empty());
220 }
221
222 #[test]
223 fn the_sun_must_be_a_directional_light() {
224 let (mut w, _, lamp) = world();
225 w.insert_resource(Environment {
226 sun: Some(lamp),
227 ambient: None,
228 });
229 assert_eq!(
230 w.verify_resources(),
231 [(
232 "core.environment",
233 format!("sun {lamp:?} is not a directional core.light")
234 )]
235 );
236 assert_eq!(
237 Environment {
238 sun: Some(lamp),
239 ambient: None
240 }
241 .to_json()
242 .unwrap(),
243 Some(serde_json::json!({"sun": lamp.index(), "ambient": null}))
244 );
245 }
246
247 #[test]
248 fn the_ambient_is_kept_by_every_part_and_must_be_finite_and_not_negative() {
249 let (mut w, sun, lamp) = world();
250 let gradient = Ambient {
251 intensity: 1.0,
252 light: AmbientLight::Gradient {
253 sky: [0.3, 0.3, 0.4],
254 equator: [0.5; 3],
255 ground: [0.25; 3],
256 },
257 };
258 w.insert_resource(Environment {
259 sun: Some(sun),
260 ambient: Some(gradient.clone()),
261 });
262 assert!(w.verify_resources().is_empty());
263 let part = w.subset(&BTreeSet::from([lamp]));
265 assert_eq!(
266 part.resource::<Environment>().unwrap().ambient,
267 Some(gradient.clone())
268 );
269 assert_eq!(gradient.light.colours(), [[0.3, 0.3, 0.4], [0.5; 3], [0.25; 3]]);
270 assert_eq!(AmbientLight::Flat([0.2; 3]).colours(), [[0.2; 3]]);
271 assert_eq!(
272 w.resource::<Environment>().unwrap().to_json().unwrap(),
273 Some(
274 serde_json::json!({"sun": sun.index(), "ambient": {"intensity": 1.0, "light": {"Gradient": {
275 "sky": [0.30000001192092896, 0.30000001192092896, 0.4000000059604645],
276 "equator": [0.5, 0.5, 0.5], "ground": [0.25, 0.25, 0.25]}}}})
277 )
278 );
279 for (intensity, colour, bad) in [
280 (
281 -1.0,
282 [0.5; 3],
283 "ambient intensity -1 is not finite and at least 0",
284 ),
285 (
286 f32::NAN,
287 [0.5; 3],
288 "ambient intensity NaN is not finite and at least 0",
289 ),
290 (
291 1.0,
292 [0.5, -0.1, 0.5],
293 "ambient colour [0.5, -0.1, 0.5] is not finite and at least 0",
294 ),
295 (
296 1.0,
297 [f32::INFINITY; 3],
298 "ambient colour [inf, inf, inf] is not finite and at least 0",
299 ),
300 ] {
301 w.insert_resource(Environment {
302 sun: None,
303 ambient: Some(Ambient {
304 intensity,
305 light: AmbientLight::Flat(colour),
306 }),
307 });
308 assert_eq!(w.verify_resources(), [("core.environment", bad.to_owned())]);
309 }
310 }
311}