use alloc::vec::Vec;
use crate::components::{SkyRotation, Transform};
use crate::ecs::{Entity, MenuActive, PipelineContext, SimTiming, StepResult, System};
use crate::sky::SkyOrientation;
const FULL_TURN_DEG: f32 = 360.0;
#[derive(Debug)]
pub struct SkyRotationSystem {
axis: [f32; 3],
degrees_per_second: f32,
angle_deg: f32,
pivots: Vec<Entity>,
}
impl SkyRotationSystem {
pub fn new(rotation: &SkyRotation) -> Self {
Self {
axis: rotation.axis,
degrees_per_second: rotation.degrees_per_second,
angle_deg: rotation.angle_deg,
pivots: Vec::new(),
}
}
fn publish(&self, ctx: &mut PipelineContext) {
let sky = SkyOrientation::new(self.axis, self.angle_deg);
let rotation_deg = sky.euler_deg();
for &pivot in &self.pivots {
match ctx.get_mut::<Transform>(pivot) {
Some(transform) => transform.rotation_deg = rotation_deg,
None => ctx.insert(
pivot,
Transform {
rotation_deg,
..Default::default()
},
),
}
}
ctx.insert_resource(sky);
}
}
impl System for SkyRotationSystem {
fn init(&mut self, ctx: &mut PipelineContext) {
self.pivots = ctx
.query_with_entity::<SkyRotation>()
.map(|(entity, _)| entity)
.collect();
self.publish(ctx);
}
fn step(&mut self, ctx: &mut PipelineContext) -> StepResult {
if ctx.resource::<MenuActive>().is_some_and(|m| m.0) {
return StepResult::Continue;
}
let timing = ctx.resource::<SimTiming>().copied().unwrap_or_default();
let turned = self.degrees_per_second * timing.ticks as f32 * timing.tick_dt;
let wrapped = (self.angle_deg + turned) % FULL_TURN_DEG;
self.angle_deg = if wrapped < 0.0 {
wrapped + FULL_TURN_DEG
} else {
wrapped
};
self.publish(ctx);
StepResult::Continue
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ecs::World;
fn world_with(rotation: SkyRotation) -> (World, SkyRotationSystem) {
let system = SkyRotationSystem::new(&rotation);
let mut world = World::new();
world.add_component(rotation);
(world, system)
}
#[test]
fn the_angle_integrates_on_the_fixed_timestep() {
let (mut world, mut system) = world_with(SkyRotation {
degrees_per_second: 90.0,
..Default::default()
});
system.init(&mut world.context());
for _ in 0..60 {
system.step(&mut world.context());
}
let sky = world
.resource::<SkyOrientation>()
.copied()
.expect("published");
assert!((sky.angle_deg - 90.0).abs() < 1e-3, "{}", sky.angle_deg);
let up = sky.rotate([0.0, 0.0, 1.0]);
assert!(up[1] > 0.99, "a quarter turn puts +Z overhead: {up:?}");
}
#[test]
fn the_authored_angle_is_live_from_init() {
let (mut world, mut system) = world_with(SkyRotation {
degrees_per_second: 0.0,
angle_deg: 180.0,
..Default::default()
});
system.init(&mut world.context());
let sky = world
.resource::<SkyOrientation>()
.copied()
.expect("published");
assert_eq!(sky.angle_deg, 180.0);
}
#[test]
fn the_pivot_entity_carries_the_rotation() {
let (mut world, mut system) = world_with(SkyRotation {
degrees_per_second: 60.0,
..Default::default()
});
system.init(&mut world.context());
{
let ctx = world.context();
let mut transforms = ctx.query::<Transform>();
let t = transforms.next().expect("the pivot gained a transform");
assert_eq!(t.rotation_deg, [0.0, 0.0, 0.0]);
assert!(transforms.next().is_none(), "one pivot, one transform");
}
for _ in 0..30 {
system.step(&mut world.context());
}
let ctx = world.context();
let t = ctx
.query::<Transform>()
.next()
.copied()
.expect("still there");
assert!(
(t.rotation_deg[0] + 30.0).abs() < 1e-2,
"{:?}",
t.rotation_deg
);
}
#[test]
fn the_angle_stays_within_one_turn() {
let (mut world, mut system) = world_with(SkyRotation {
degrees_per_second: -90.0,
angle_deg: 30.0,
..Default::default()
});
system.init(&mut world.context());
for _ in 0..60 {
system.step(&mut world.context());
}
let angle = world
.resource::<SkyOrientation>()
.expect("published")
.angle_deg;
assert!((angle - 300.0).abs() < 1e-2, "{angle}");
let orientation = SkyOrientation::new([1.0, 0.0, 0.0], -60.0).rotate([0.0, 0.0, 1.0]);
let published = world
.resource::<SkyOrientation>()
.expect("published")
.rotate([0.0, 0.0, 1.0]);
assert!(
(0..3).all(|i| (orientation[i] - published[i]).abs() < 1e-3),
"{orientation:?} {published:?}"
);
}
#[test]
fn an_open_menu_freezes_the_sky() {
let (mut world, mut system) = world_with(SkyRotation {
degrees_per_second: 90.0,
..Default::default()
});
system.init(&mut world.context());
world.insert_resource(MenuActive(true));
for _ in 0..60 {
system.step(&mut world.context());
}
assert_eq!(
world
.resource::<SkyOrientation>()
.expect("published")
.angle_deg,
0.0
);
}
}