bevy_prank 0.4.1

Opinionated Unreal Engine inspired spectator camera for the Bevy game engine
Documentation
use super::{sync_active, Prank3d, Prank3dActive};
use bevy::{
    input::mouse::{MouseMotion, MouseWheel},
    prelude::*,
};

pub(super) struct Prank3dInputPlugin;

impl Plugin for Prank3dInputPlugin {
    fn build(&self, app: &mut App) {
        app.add_state::<Prank3dMode>()
            .add_event::<Prank3dMovement>()
            .add_event::<Prank3dRotation>()
            .add_systems(
                PreUpdate,
                (
                    mode,
                    speed_factor.run_if(in_state(Prank3dMode::Fly)),
                    movement.run_if(not(in_state(Prank3dMode::None))),
                    rotation.run_if(in_state(Prank3dMode::Fly)),
                )
                    .after(sync_active),
            );
    }
}

#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, States)]
pub(super) enum Prank3dMode {
    Fly,
    Offset,
    #[default]
    None,
}

#[derive(Event)]
pub(super) struct Prank3dMovement(pub(super) Vec3);

#[derive(Event)]
pub(super) struct Prank3dRotation(pub(super) Vec2);

fn mode(
    active: Res<Prank3dActive>,
    prev_mode: Res<State<Prank3dMode>>,
    mut mode: ResMut<NextState<Prank3dMode>>,
    mouse: Res<Input<MouseButton>>,
) {
    if active.0.is_none() {
        mode.set(Prank3dMode::None);
        return;
    }

    match **prev_mode {
        Prank3dMode::Fly => {
            if !mouse.pressed(MouseButton::Right) {
                mode.set(Prank3dMode::None);
            }
        }
        Prank3dMode::Offset => {
            if !mouse.pressed(MouseButton::Middle) {
                mode.set(Prank3dMode::None);
            }
        }
        Prank3dMode::None => {
            if mouse.pressed(MouseButton::Right) {
                mode.set(Prank3dMode::Fly);
            } else if mouse.pressed(MouseButton::Middle) {
                mode.set(Prank3dMode::Offset);
            }
        }
    }
}

fn speed_factor(
    active: Res<Prank3dActive>,
    mut pranks: Query<&mut Prank3d>,
    mut wheel: EventReader<MouseWheel>,
) {
    let Some(entity) = active.0 else {
        return;
    };
    let mut prank = pranks.get_mut(entity).expect("exists");

    prank.speed_factor =
        (prank.speed_factor + 0.1 * wheel.iter().fold(0.0, |acc, x| acc + x.y)).clamp(0.1, 10.0);
}

fn movement(
    mode: Res<State<Prank3dMode>>,
    active: Res<Prank3dActive>,
    pranks: Query<(&GlobalTransform, &Prank3d)>,
    mut movement: EventWriter<Prank3dMovement>,
    mut motion: EventReader<MouseMotion>,
    keyboard: Res<Input<KeyCode>>,
) {
    let Some(entity) = active.0 else {
        return;
    };
    let (transform, prank) = pranks.get(entity).expect("exists");

    movement.send(Prank3dMovement(match **mode {
        Prank3dMode::Fly => {
            let mut movement = Vec3::ZERO;

            if keyboard.pressed(KeyCode::W) {
                movement += transform.forward();
            }
            if keyboard.pressed(KeyCode::A) {
                movement += transform.left();
            }
            if keyboard.pressed(KeyCode::S) {
                movement += transform.back();
            }
            if keyboard.pressed(KeyCode::D) {
                movement += transform.right();
            }
            if keyboard.pressed(KeyCode::ShiftLeft) {
                movement = Vec3::new(movement.x, 0.0, movement.z);
            }
            if keyboard.pressed(KeyCode::E) {
                movement += Vec3::Y;
            }
            if keyboard.pressed(KeyCode::Q) {
                movement += Vec3::NEG_Y;
            }

            prank.speed_factor * movement.normalize_or_zero()
        }
        Prank3dMode::Offset => {
            let motion = motion
                .iter()
                .fold(Vec2::ZERO, |acc, motion| acc + motion.delta);

            transform.compute_transform().rotation
                * Vec3::new(
                    prank.sensitivity.x * motion.x,
                    -prank.sensitivity.y * motion.y,
                    0.0,
                )
        }
        Prank3dMode::None => Vec3::ZERO,
    }));
}

fn rotation(
    mode: Res<State<Prank3dMode>>,
    mut rotation: EventWriter<Prank3dRotation>,
    mut motion: EventReader<MouseMotion>,
) {
    rotation.send(Prank3dRotation(match **mode {
        Prank3dMode::Fly => motion.iter().fold(Vec2::ZERO, |acc, x| acc + x.delta),
        Prank3dMode::Offset => Vec2::ZERO,
        Prank3dMode::None => Vec2::ZERO,
    }));
}