1use self::{gizmo::Prank3dGizmoPlugin, hud::Prank3dHudPlugin};
4use bevy::{
5 input::mouse::{MouseMotion, MouseWheel},
6 prelude::*,
7 render::camera::NormalizedRenderTarget,
8 window::{CursorGrabMode, PrimaryWindow},
9};
10use std::f32::consts;
11
12pub mod gizmo;
13pub mod hud;
14
15pub(super) struct Prank3dPlugin;
16
17impl Plugin for Prank3dPlugin {
18 fn build(&self, app: &mut App) {
19 app.add_plugins((Prank3dGizmoPlugin, Prank3dHudPlugin))
20 .register_type::<Prank3d>()
21 .init_resource::<Prank3dActive>()
22 .add_state::<Prank3dMode>()
23 .add_systems(PreUpdate, (sync_active, mode.after(sync_active)))
24 .add_systems(
25 Update,
26 (
27 initialize,
28 sync_cursor.run_if(
29 |active: Res<Prank3dActive>, mode: Res<State<Prank3dMode>>| {
30 active.is_changed() || mode.is_changed()
31 },
32 ),
33 interpolation,
34 fly.run_if(in_state(Prank3dMode::Fly)),
35 offset.run_if(in_state(Prank3dMode::Offset)),
36 ),
37 );
38 }
39}
40
41#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, States)]
42enum Prank3dMode {
43 Fly,
44 Offset,
45 #[default]
46 None,
47}
48
49#[derive(Default, Resource)]
50struct Prank3dActive(Option<Entity>);
51
52#[derive(Reflect, Component)]
73#[reflect(Component)]
74pub struct Prank3d {
75 pub is_active: bool,
80
81 pub speed: f32,
83
84 pub speed_scalar: f32,
86
87 pub lerp_rate: f32,
96
97 pub sensitivity: Vec2,
99
100 pub translation: Vec3,
105}
106
107impl Default for Prank3d {
108 fn default() -> Self {
109 Self {
110 is_active: true,
111 speed: 25.0,
112 speed_scalar: 1.0,
113 lerp_rate: 0.001,
114 sensitivity: Vec2::splat(0.08),
115 translation: Vec3::ZERO,
116 }
117 }
118}
119
120fn sync_active(
121 primary_window: Query<(Entity, &Window), With<PrimaryWindow>>,
122 windows: Query<(Entity, &Window), Without<PrimaryWindow>>,
123 pranks: Query<(Entity, &Camera, &Prank3d)>,
124 mut active: ResMut<Prank3dActive>,
125) {
126 let primary_window = primary_window.get_single().ok();
127 let Some(focused_window) = windows
128 .iter()
129 .find(|(_, window)| window.focused)
130 .map(|(entity, _)| entity)
131 .or_else(|| primary_window.and_then(|(entity, window)| window.focused.then_some(entity)))
132 else {
133 return;
134 };
135
136 let active_entity = pranks
137 .iter()
138 .find(|(_, camera, prank)| {
139 if !prank.is_active {
140 return false;
141 }
142 let Some(NormalizedRenderTarget::Window(winref)) = camera
143 .target
144 .normalize(primary_window.map(|(entity, _)| entity))
145 else {
146 return false;
147 };
148
149 winref.entity() == focused_window
150 })
151 .map(|(entity, _, _)| entity);
152
153 if active_entity != active.0 {
154 *active = Prank3dActive(active_entity);
155 }
156}
157
158fn mode(
159 active: Res<Prank3dActive>,
160 prev_mode: Res<State<Prank3dMode>>,
161 mut mode: ResMut<NextState<Prank3dMode>>,
162 mouse: Res<Input<MouseButton>>,
163) {
164 if active.0.is_none() {
165 mode.set(Prank3dMode::None);
166 return;
167 }
168
169 match **prev_mode {
170 Prank3dMode::Fly => {
171 if !mouse.pressed(MouseButton::Right) {
172 mode.set(Prank3dMode::None);
173 }
174 }
175 Prank3dMode::Offset => {
176 if !mouse.pressed(MouseButton::Middle) {
177 mode.set(Prank3dMode::None);
178 }
179 }
180 Prank3dMode::None => {
181 if mouse.pressed(MouseButton::Right) {
182 mode.set(Prank3dMode::Fly);
183 } else if mouse.pressed(MouseButton::Middle) {
184 mode.set(Prank3dMode::Offset);
185 }
186 }
187 }
188}
189
190fn initialize(mut pranks: Query<(&mut Prank3d, &Transform), Added<Prank3d>>) {
191 for (mut prank, transform) in pranks.iter_mut() {
192 if !(0.0..1.0).contains(&prank.lerp_rate) {
193 panic!("`lerp_rate` field of `bevy_prank::three::Prank3d` must be in range [0.0, 1.0)");
194 }
195
196 prank.translation = transform.translation;
197 }
198}
199
200fn sync_cursor(
201 mut primary_window: Query<(Entity, &mut Window), With<PrimaryWindow>>,
202 mut windows: Query<&mut Window, Without<PrimaryWindow>>,
203 active: Res<Prank3dActive>,
204 pranks: Query<&Camera, With<Prank3d>>,
205 mode: Res<State<Prank3dMode>>,
206) {
207 let Some(entity) = active.0 else {
208 return;
209 };
210 let camera = pranks.get(entity).expect("exists");
211
212 let Some(NormalizedRenderTarget::Window(winref)) = camera
213 .target
214 .normalize(primary_window.get_single().ok().map(|(entity,_)| entity))
215 else {
216 return;
217 };
218
219 let mut window = match windows.get_mut(winref.entity()) {
220 Ok(window) => window,
221 Err(_) => {
222 let Ok((_, window)) = primary_window.get_single_mut() else {
223 return;
224 };
225
226 window
227 }
228 };
229
230 match **mode {
231 Prank3dMode::Fly => {
232 window.cursor.visible = false;
233 window.cursor.grab_mode = CursorGrabMode::Locked;
234 }
235 Prank3dMode::Offset => {
236 window.cursor.visible = false;
237 window.cursor.grab_mode = CursorGrabMode::Locked;
238 }
239 Prank3dMode::None => {
240 window.cursor.visible = true;
241 window.cursor.grab_mode = CursorGrabMode::None;
242 }
243 }
244}
245
246fn interpolation(
247 active: Res<Prank3dActive>,
248 mut pranks: Query<(&mut Transform, &Prank3d)>,
249 time: Res<Time>,
250) {
251 let Some(entity) = active.0 else {
252 return;
253 };
254 let (mut transform, prank) = pranks.get_mut(entity).expect("exists");
255
256 transform.translation = transform.translation.lerp(
257 prank.translation,
258 1.0 - prank.lerp_rate.powf(time.delta_seconds()),
259 );
260}
261
262fn fly(
263 active: Res<Prank3dActive>,
264 mut pranks: Query<(&mut Transform, &mut Prank3d)>,
265 time: Res<Time>,
266 mut motion: EventReader<MouseMotion>,
267 mut wheel: EventReader<MouseWheel>,
268 keyboard: Res<Input<KeyCode>>,
269) {
270 let Some(entity) = active.0 else {
271 return;
272 };
273 let (mut transform, mut prank) = pranks.get_mut(entity).expect("exists");
274 let motion = motion.iter().fold(Vec2::ZERO, |acc, m| acc + m.delta);
275 let wheel = wheel.iter().fold(0.0, |acc, w| acc + w.y);
276 let mut movement = Vec3::ZERO;
277 if keyboard.pressed(KeyCode::W) {
278 movement += transform.forward();
279 }
280 if keyboard.pressed(KeyCode::A) {
281 movement += transform.left();
282 }
283 if keyboard.pressed(KeyCode::S) {
284 movement += transform.back();
285 }
286 if keyboard.pressed(KeyCode::D) {
287 movement += transform.right();
288 }
289 if keyboard.pressed(KeyCode::ShiftLeft) {
290 movement = Vec3::new(movement.x, 0.0, movement.z);
291 }
292 if keyboard.pressed(KeyCode::E) {
293 movement += Vec3::Y;
294 }
295 if keyboard.pressed(KeyCode::Q) {
296 movement += Vec3::NEG_Y;
297 }
298
299 prank.speed_scalar = (prank.speed_scalar + 0.1 * wheel).clamp(0.1, 10.0);
300
301 let speed = prank.speed_scalar.powi(2) * prank.speed;
302 prank.translation += speed * movement.normalize_or_zero() * time.delta_seconds();
303
304 let (yaw, pitch, _) = transform.rotation.to_euler(EulerRot::YXZ);
305 transform.rotation = Quat::from_euler(
306 EulerRot::YXZ,
307 yaw - prank.sensitivity.x * motion.x * time.delta_seconds(),
308 (pitch - prank.sensitivity.y * motion.y * time.delta_seconds())
309 .clamp(-consts::FRAC_PI_3, consts::FRAC_PI_3),
310 0.0,
311 );
312}
313
314fn offset(
315 active: Res<Prank3dActive>,
316 mut pranks: Query<(&mut Transform, &mut Prank3d)>,
317 time: Res<Time>,
318 mut motion: EventReader<MouseMotion>,
319) {
320 let Some(entity) = active.0 else {
321 return;
322 };
323 let (mut transform, mut prank) = pranks.get_mut(entity).expect("exists");
324 let motion = motion.iter().fold(Vec2::ZERO, |acc, m| acc + m.delta);
325
326 let r = transform.rotation;
327 transform.translation += r * Vec3::new(motion.x, -motion.y, 0.0) * time.delta_seconds();
328 prank.translation = transform.translation;
329}