use bevy::prelude::*; use bevy::input::mouse::MouseMotion; use bevy::window::PrimaryWindow; use bevy::core_pipeline::bloom::{BloomCompositeMode, BloomSettings}; use bevy::core_pipeline::tonemapping::Tonemapping; use bevy_xpbd_3d::prelude::*; use bevy_xpbd_3d::PhysicsSchedule; use bevy_xpbd_3d::PhysicsStepSet; use std::f32::consts::*; use crate::{settings, audio, actor}; pub struct CameraControllerPlugin; impl Plugin for CameraControllerPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, setup_camera); app.add_systems(Update, handle_input); app.add_systems(Update, manage_player_actor); app.add_systems(PhysicsSchedule, sync_camera_to_player.in_set(PhysicsStepSet::BroadPhase)); app.add_systems(Update, update_fov); app.add_systems(Update, apply_input_to_player); } } // Based on Valorant's default sensitivity, not entirely sure why it is exactly 1.0 / 180.0, // but I'm guessing it is a misunderstanding between degrees/radians and then sticking with // it because it felt nice. pub const RADIANS_PER_DOT: f32 = 1.0 / 180.0; fn setup_camera( mut commands: Commands, ) { // Add player commands.spawn(( Camera3dBundle { camera: Camera { hdr: true, // HDR is required for bloom ..default() }, tonemapping: Tonemapping::TonyMcMapface, transform: Transform::from_xyz(0.0, 0.0, 8.0).looking_at(Vec3::ZERO, Vec3::Y), ..default() }, BloomSettings { composite_mode: BloomCompositeMode::EnergyConserving, ..default() }, )); } pub fn sync_camera_to_player( settings: Res, mut q_camera: Query<&mut Transform, With>, q_playercam: Query<(&actor::Actor, &Transform), (With, Without)>, ) { if q_camera.is_empty() || q_playercam.is_empty() { return; } let mut camera_transform = q_camera.get_single_mut().unwrap(); let (actor, player_transform) = q_playercam.get_single().unwrap(); // Rotation camera_transform.rotation = player_transform.rotation * Quat::from_array([0.0, -1.0, 0.0, 0.0]); // Translation if settings.third_person { camera_transform.translation = player_transform.translation + camera_transform.rotation * (actor.camdistance * Vec3::new(0.0, 0.2, 1.0)); } else { camera_transform.translation = player_transform.translation; } } pub fn update_fov( q_player: Query<&actor::LifeForm, With>, mut q_camera: Query<&mut Projection, With>, ) { if let (Ok(lifeform), Ok(mut projection)) = (q_player.get_single(), q_camera.get_single_mut()) { let fov = (lifeform.adrenaline.powf(3.0) * 45.0 + 45.0).to_radians(); *projection = Projection::Perspective(PerspectiveProjection { fov: fov, ..default() }); } } pub fn handle_input( keyboard_input: Res>, mut settings: ResMut, ) { if keyboard_input.just_pressed(settings.key_camera) { settings.third_person ^= true; } } fn manage_player_actor( settings: Res, mut q_playercam: Query<&mut Visibility, With>, ) { for mut vis in &mut q_playercam { if settings.third_person { *vis = Visibility::Inherited; } else { *vis = Visibility::Hidden; } } } #[allow(clippy::too_many_arguments)] fn apply_input_to_player( time: Res