lunar_render/
screen_shake.rs1use bevy_ecs::prelude::*;
23use lunar_math::Vec2;
24
25use crate::Camera;
26
27#[derive(Resource)]
29pub struct ScreenShake {
30 pub trauma: f32,
32 pub decay_rate: f32,
34 pub max_offset: Vec2,
36 elapsed: f32,
38}
39
40impl ScreenShake {
41 #[must_use]
43 pub fn new(max_offset: Vec2, decay_rate: f32) -> Self {
44 Self {
45 trauma: 0.0,
46 decay_rate,
47 max_offset,
48 elapsed: 0.0,
49 }
50 }
51
52 pub fn add_trauma(&mut self, amount: f32) {
54 self.trauma = (self.trauma + amount).min(1.0);
55 }
56}
57
58pub(crate) fn screen_shake_system(
59 mut shake: Option<ResMut<ScreenShake>>,
60 mut camera: Option<ResMut<Camera>>,
61 time: Res<lunar_core::Time>,
62) {
63 let (Some(shake), Some(camera)) = (shake.as_mut(), camera.as_mut()) else {
64 return;
65 };
66 if shake.trauma <= 0.0 {
67 return;
68 }
69
70 let delta = time.delta_seconds();
71 shake.elapsed += delta;
72 shake.trauma = (shake.trauma - shake.decay_rate * delta).max(0.0);
73
74 let intensity = shake.trauma * shake.trauma;
76 let t = shake.elapsed;
77
78 let noise_x = (t * 13.7).sin() * 0.6 + (t * 29.3).sin() * 0.3 + (t * 53.1).sin() * 0.1;
80 let noise_y = (t * 11.3).sin() * 0.6 + (t * 31.7).sin() * 0.3 + (t * 47.9).sin() * 0.1;
81
82 camera.position.x += noise_x * intensity * shake.max_offset.x;
83 camera.position.y += noise_y * intensity * shake.max_offset.y;
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use lunar_core::Time;
90
91 fn run_system(world: &mut World) {
92 let mut system = IntoSystem::into_system(screen_shake_system);
93 system.initialize(world);
94 let _ = system.run((), world);
95 }
96
97 #[test]
98 fn no_shake_when_trauma_zero() {
99 let mut world = World::new();
100 world.insert_resource(Camera::new());
101 world.insert_resource(Time::default());
102 world.insert_resource(ScreenShake::new(Vec2::new(20.0, 20.0), 1.0));
103
104 run_system(&mut world);
105
106 let camera = world.resource::<Camera>();
107 assert!((camera.position.x - 0.0).abs() < 0.001);
108 assert!((camera.position.y - 0.0).abs() < 0.001);
109 }
110
111 #[test]
112 fn trauma_decays_each_frame() {
113 let mut world = World::new();
114 world.insert_resource(Camera::new());
115
116 let mut time = Time::default();
117 time.set_delta_seconds(0.1);
118 world.insert_resource(time);
119
120 let mut shake = ScreenShake::new(Vec2::new(10.0, 10.0), 2.0);
121 shake.add_trauma(1.0);
122 world.insert_resource(shake);
123
124 run_system(&mut world);
125
126 let shake = world.resource::<ScreenShake>();
127 assert!((shake.trauma - 0.8).abs() < 0.01);
129 }
130
131 #[test]
132 fn trauma_clamps_to_one() {
133 let mut shake = ScreenShake::new(Vec2::ZERO, 1.0);
134 shake.add_trauma(0.7);
135 shake.add_trauma(0.7);
136 assert!((shake.trauma - 1.0).abs() < 0.001);
137 }
138
139 #[test]
140 fn trauma_does_not_go_negative() {
141 let mut world = World::new();
142 world.insert_resource(Camera::new());
143
144 let mut time = Time::default();
145 time.set_delta_seconds(10.0); world.insert_resource(time);
147
148 let mut shake = ScreenShake::new(Vec2::new(10.0, 10.0), 1.0);
149 shake.add_trauma(0.1);
150 world.insert_resource(shake);
151
152 run_system(&mut world);
153
154 let shake = world.resource::<ScreenShake>();
155 assert!(shake.trauma >= 0.0);
156 }
157}