490 lines
16 KiB
Rust
490 lines
16 KiB
Rust
use bevy::{
|
|
input::mouse::AccumulatedMouseMotion, pbr::VolumetricFog, prelude::*, render::view::RenderLayers, window::{PrimaryWindow, WindowResized}
|
|
};
|
|
use bevy_kira_audio::{Audio, AudioControl};
|
|
use bevy_rapier3d::prelude::*;
|
|
|
|
pub mod toolbar;
|
|
|
|
use crate::{
|
|
GameState,
|
|
asset_loading::{AudioAssets, FlashlightAssets},
|
|
};
|
|
|
|
#[derive(Debug, Component, Default)]
|
|
pub struct Player {
|
|
pub speed_factor: f32,
|
|
}
|
|
|
|
#[derive(Debug, Component, Deref, DerefMut)]
|
|
pub struct CameraSensitivity(Vec2);
|
|
impl Default for CameraSensitivity {
|
|
fn default() -> Self {
|
|
Self(Vec2::new(0.003, 0.002))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Component)]
|
|
pub struct WorldModelCamera;
|
|
|
|
#[derive(Debug, Component, Default)]
|
|
pub struct PlayerInput {
|
|
movement_direction: Vec3,
|
|
}
|
|
|
|
#[derive(Component, Debug)]
|
|
pub struct HeadBob {
|
|
pub enabled: bool,
|
|
pub intensity: f32,
|
|
pub speed: f32,
|
|
pub time_offset: f32,
|
|
}
|
|
|
|
impl Default for HeadBob {
|
|
fn default() -> Self {
|
|
Self {
|
|
enabled: true,
|
|
intensity: 0.05,
|
|
speed: 10.0,
|
|
time_offset: 0.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Component, Debug)]
|
|
pub struct BaseTransform(pub Transform);
|
|
|
|
#[derive(Debug, Component)]
|
|
pub struct Flashlight {
|
|
// 0 - 4
|
|
pub charge: f32,
|
|
pub is_on: bool,
|
|
}
|
|
|
|
#[derive(Debug, Component)]
|
|
pub struct SpotlightFlashlight {
|
|
}
|
|
|
|
#[derive(Component)]
|
|
pub struct FlashlightButtonAnimation {
|
|
pub timer: Timer,
|
|
pub is_pressed: bool,
|
|
}
|
|
|
|
impl Default for FlashlightButtonAnimation {
|
|
fn default() -> Self {
|
|
Self {
|
|
timer: Timer::from_seconds(0.20, TimerMode::Once),
|
|
is_pressed: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn plugin(app: &mut App) {
|
|
app.add_plugins(toolbar::plugin)
|
|
.add_systems(OnEnter(GameState::Playing), (init_player, hide_cursor))
|
|
.add_systems(
|
|
Update,
|
|
(
|
|
move_camera,
|
|
handle_input,
|
|
apply_head_bob,
|
|
on_resize_system,
|
|
handle_flashlight,
|
|
(update_flashlight_button_animation, update_flashlight_charge, update_flashlight_sprite).chain(),
|
|
)
|
|
.run_if(in_state(GameState::Playing)),
|
|
)
|
|
.add_systems(
|
|
FixedUpdate,
|
|
apply_player_movement.run_if(in_state(GameState::Playing)),
|
|
);
|
|
}
|
|
|
|
// used by the view model camera and the player's arm.
|
|
const STATIC_LAYER: usize = 1;
|
|
|
|
pub fn init_player(
|
|
mut commands: Commands,
|
|
flashlights: Res<FlashlightAssets>,
|
|
window: Query<&Window>,
|
|
) {
|
|
commands
|
|
.spawn((
|
|
Player::default(),
|
|
PlayerAction::default(),
|
|
CameraSensitivity::default(),
|
|
PlayerInput::default(),
|
|
toolbar::Item::none(),
|
|
HeadBob::default(),
|
|
// rapier
|
|
RigidBody::Dynamic,
|
|
Collider::capsule(Vec3::new(0.0, -0.5, 0.0), Vec3::new(0.0, 0.5, 0.0), 0.5),
|
|
Velocity::zero(),
|
|
LockedAxes::ROTATION_LOCKED,
|
|
Damping {
|
|
linear_damping: 6.0,
|
|
angular_damping: 1.0,
|
|
},
|
|
GravityScale(3.0),
|
|
Transform::from_xyz(0.0, 0.5, 0.0),
|
|
))
|
|
.with_children(|parent| {
|
|
parent.spawn((
|
|
WorldModelCamera,
|
|
Camera3d::default(),
|
|
Projection::from(PerspectiveProjection {
|
|
fov: 90.0_f32.to_radians(),
|
|
..default()
|
|
}),
|
|
DistanceFog {
|
|
color: Color::srgba(0.12, 0.08, 0.08, 0.65),
|
|
falloff: FogFalloff::Linear {
|
|
start: 3.0,
|
|
end: 12.0,
|
|
},
|
|
..default()
|
|
},
|
|
));
|
|
|
|
// camera voor pitslampke
|
|
parent.spawn((
|
|
Camera2d::default(),
|
|
Camera {
|
|
order: 1,
|
|
..default()
|
|
},
|
|
RenderLayers::layer(STATIC_LAYER),
|
|
));
|
|
let window = window.single();
|
|
let transform = flashlight_base_transform(window.width(), window.height());
|
|
parent.spawn((
|
|
Flashlight { charge: 4.0, is_on: false },
|
|
Sprite::from_image(flashlights.flash_hold_4.clone()),
|
|
transform.0.clone(),
|
|
transform,
|
|
RenderLayers::layer(STATIC_LAYER),
|
|
FlashlightButtonAnimation::default(),
|
|
));
|
|
|
|
// feitelijke pitslamp
|
|
parent.spawn((
|
|
SpotlightFlashlight{},
|
|
SpotLight {
|
|
intensity: 0.0,
|
|
color: Color::srgba(0.9, 0.628, 0.392, 1.0),
|
|
range: 5.0,
|
|
outer_angle: 28.0_f32.to_radians(), // wide cone for flashlight
|
|
inner_angle: 12.0_f32.to_radians(), // narrower inner cone
|
|
shadows_enabled: false, // (set to true for realism)
|
|
radius: 0.35, // low value for hard light
|
|
..default()
|
|
},
|
|
Transform::from_xyz(0.0, -0.15, 0.5),
|
|
GlobalTransform::default(),
|
|
));
|
|
});
|
|
}
|
|
|
|
fn on_resize_system(
|
|
mut resize_reader: EventReader<WindowResized>,
|
|
mut sprites: Query<(&mut Transform, &mut BaseTransform), With<Sprite>>,
|
|
) {
|
|
for e in resize_reader.read() {
|
|
for (mut transform, mut base_transform) in sprites.iter_mut() {
|
|
let new_pos = flashlight_base_transform(e.width, e.height);
|
|
*transform = new_pos.0.clone();
|
|
*base_transform = new_pos;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn flashlight_base_transform(window_width: f32, window_height: f32) -> BaseTransform {
|
|
let window_size = Vec2::new(window_width, window_height);
|
|
let sprite_size = Vec2::new(404.0, 404.0);
|
|
let scale = window_width / 2400.0;
|
|
let world_size = sprite_size * scale;
|
|
let xoffset = window_size.x / 4.0 - 40.0;
|
|
let yoffset = 15.0;
|
|
|
|
let mut transform = Transform::from_translation(Vec3::new(
|
|
window_size.x / 2.0 - world_size.x / 2.0 - xoffset,
|
|
-window_size.y / 2.0 + world_size.y / 2.0 - yoffset,
|
|
0.0,
|
|
));
|
|
transform.scale = Vec3::new(scale, scale, 1.0);
|
|
return BaseTransform(transform);
|
|
}
|
|
|
|
fn hide_cursor(mut windows: Query<&mut Window, With<PrimaryWindow>>) {
|
|
for mut window in windows.iter_mut() {
|
|
window.cursor_options.visible = false;
|
|
}
|
|
}
|
|
|
|
const PITCH_LIMIT: f32 = std::f32::consts::FRAC_PI_2 - 0.01;
|
|
|
|
pub fn move_camera(
|
|
accumulated_mouse_motion: Res<AccumulatedMouseMotion>,
|
|
mut player: Query<(&mut Transform, &CameraSensitivity), With<Player>>,
|
|
) {
|
|
let Ok((mut transform, camera_sensitivity)) = player.get_single_mut() else {
|
|
return;
|
|
};
|
|
let delta = accumulated_mouse_motion.delta;
|
|
|
|
if delta != Vec2::ZERO {
|
|
let delta_yaw = -delta.x * camera_sensitivity.x;
|
|
let delta_pitch = -delta.y * camera_sensitivity.y;
|
|
|
|
let (yaw, pitch, roll) = transform.rotation.to_euler(EulerRot::YXZ);
|
|
let yaw = yaw + delta_yaw;
|
|
let pitch = (pitch + delta_pitch).clamp(-PITCH_LIMIT, PITCH_LIMIT);
|
|
transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll);
|
|
}
|
|
}
|
|
|
|
#[derive(Component, Debug, Clone, Copy, Eq, PartialEq, Hash, Reflect, Default)]
|
|
pub(crate) enum PlayerAction {
|
|
#[default]
|
|
Move,
|
|
Sprint,
|
|
Jump,
|
|
ToggleFlashlight,
|
|
OpenDoor,
|
|
// Objects and stuff
|
|
PickUp,
|
|
Drop,
|
|
}
|
|
|
|
pub fn handle_input(
|
|
keyboard_input: Res<ButtonInput<KeyCode>>,
|
|
mut query: Query<(&Transform, &mut PlayerInput, &mut PlayerAction, &mut Player), With<Player>>,
|
|
) {
|
|
for (transform, mut input, mut action, mut player) in query.iter_mut() {
|
|
*action = PlayerAction::Move;
|
|
|
|
let forward = transform.forward();
|
|
let right = transform.right();
|
|
let mut movement_direction = Vec3::ZERO;
|
|
|
|
if keyboard_input.pressed(KeyCode::KeyW) {
|
|
movement_direction += Vec3::new(forward.x, 0.0, forward.z).normalize_or_zero();
|
|
}
|
|
if keyboard_input.pressed(KeyCode::KeyS) {
|
|
movement_direction -= Vec3::new(forward.x, 0.0, forward.z).normalize_or_zero();
|
|
}
|
|
if keyboard_input.pressed(KeyCode::KeyA) {
|
|
movement_direction -= Vec3::new(right.x, 0.0, right.z).normalize_or_zero();
|
|
}
|
|
if keyboard_input.pressed(KeyCode::KeyD) {
|
|
movement_direction += Vec3::new(right.x, 0.0, right.z).normalize_or_zero();
|
|
}
|
|
if keyboard_input.pressed(KeyCode::Space) {
|
|
movement_direction += Vec3::Y;
|
|
}
|
|
if keyboard_input.pressed(KeyCode::ShiftLeft) {
|
|
player.speed_factor = 1.35;
|
|
*action = PlayerAction::Sprint
|
|
} else if keyboard_input.pressed(KeyCode::ControlLeft) {
|
|
player.speed_factor = 0.65;
|
|
} else {
|
|
player.speed_factor = 1.0;
|
|
}
|
|
if keyboard_input.just_pressed(KeyCode::KeyE) {
|
|
*action = PlayerAction::PickUp;
|
|
}
|
|
if keyboard_input.just_pressed(KeyCode::KeyQ) {
|
|
*action = PlayerAction::Drop;
|
|
}
|
|
if keyboard_input.just_pressed(KeyCode::KeyF) {
|
|
*action = PlayerAction::ToggleFlashlight;
|
|
}
|
|
if keyboard_input.just_pressed(KeyCode::KeyK) {
|
|
*action = PlayerAction::OpenDoor;
|
|
}
|
|
|
|
input.movement_direction = movement_direction.normalize_or_zero();
|
|
}
|
|
}
|
|
|
|
pub fn apply_player_movement(
|
|
mut player_query: Query<(&PlayerInput, &mut Velocity, &Player), With<Player>>,
|
|
) {
|
|
const SPEED: f32 = 2.6;
|
|
const JUMP_FORCE: f32 = 4.0;
|
|
|
|
for (input, mut velocity, player) in player_query.iter_mut() {
|
|
let speed = SPEED * player.speed_factor;
|
|
let horizontal_movement = Vec3::new(
|
|
input.movement_direction.x * speed,
|
|
0.0,
|
|
input.movement_direction.z * speed,
|
|
);
|
|
|
|
velocity.linvel.x = horizontal_movement.x;
|
|
velocity.linvel.z = horizontal_movement.z;
|
|
|
|
if input.movement_direction.y > 0.0 {
|
|
velocity.linvel.y = JUMP_FORCE;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn apply_head_bob(
|
|
time: Res<Time>,
|
|
mut query: Query<(&PlayerInput, &mut HeadBob, &Player), With<Player>>,
|
|
mut camera_query: Query<&mut Transform, (With<WorldModelCamera>, Without<Player>)>,
|
|
mut sprite_query: Query<
|
|
(&mut Transform, &Sprite, &BaseTransform),
|
|
(With<Sprite>, Without<WorldModelCamera>, Without<Player>),
|
|
>,
|
|
) {
|
|
let Ok((input, mut head_bob, player)) = query.get_single_mut() else {
|
|
return;
|
|
};
|
|
|
|
// bob when moving horizontally
|
|
let horizontal_movement =
|
|
Vec3::new(input.movement_direction.x, 0.0, input.movement_direction.z);
|
|
let is_moving = horizontal_movement.length_squared() > 0.01;
|
|
let bobbing_speed = head_bob.speed * player.speed_factor;
|
|
|
|
let mut offset = bobbing_speed * time.delta_secs();
|
|
if !is_moving {
|
|
// decrease bobbing frequency when stationary
|
|
offset *= 0.2;
|
|
|
|
// bob returns to neutral position when stopped
|
|
if let Some(camera_transform) = camera_query.iter_mut().next() {
|
|
let current_offset = camera_transform.translation.y;
|
|
if current_offset.abs() < 0.005 {
|
|
// cancel out so head_bob.time_offset
|
|
offset = -head_bob.time_offset;
|
|
}
|
|
}
|
|
}
|
|
head_bob.time_offset += offset;
|
|
|
|
if head_bob.enabled {
|
|
// calculate vertical and horizontal offsets using sine and cosine
|
|
let vertical_offset = head_bob.intensity * f32::sin(head_bob.time_offset);
|
|
let horizontal_offset = (head_bob.intensity * 0.5) * f32::cos(head_bob.time_offset * 0.5);
|
|
|
|
// apply
|
|
for mut transform in camera_query.iter_mut() {
|
|
transform.translation.y = vertical_offset;
|
|
if is_moving {
|
|
transform.translation.x = horizontal_offset;
|
|
} else {
|
|
// decrease bobbing magnitude
|
|
transform.translation.x *= 0.8;
|
|
}
|
|
}
|
|
|
|
// apply offsets to flashlight
|
|
for (mut transform, _sprite, base_transform) in sprite_query.iter_mut() {
|
|
let scale_factor = 40.0 * player.speed_factor;
|
|
|
|
if is_moving {
|
|
transform.translation.x =
|
|
base_transform.0.translation.x + horizontal_offset * scale_factor;
|
|
transform.translation.y =
|
|
base_transform.0.translation.y + vertical_offset * scale_factor;
|
|
|
|
transform.rotation =
|
|
Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, horizontal_offset * 0.1);
|
|
} else {
|
|
transform.translation = transform
|
|
.translation
|
|
.lerp(base_transform.0.translation, 0.1);
|
|
transform.rotation = transform.rotation.slerp(Quat::IDENTITY, 0.1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
pub fn handle_flashlight(
|
|
player_query: Query<&PlayerAction, With<Player>>,
|
|
mut flashlight_query: Query<&mut SpotLight, With<SpotlightFlashlight>>,
|
|
mut flashlight_sprite_query: Query<&mut FlashlightButtonAnimation>,
|
|
audio_assets: Res<AudioAssets>,
|
|
audio: Res<Audio>,
|
|
) {
|
|
let Ok(action) = player_query.get_single() else {
|
|
return;
|
|
};
|
|
if *action != PlayerAction::ToggleFlashlight {
|
|
return;
|
|
}
|
|
if let Ok(mut animation) = flashlight_sprite_query.get_single_mut() {
|
|
animation.is_pressed = true;
|
|
animation.timer.reset();
|
|
}
|
|
if let Ok(mut spotlight) = flashlight_query.get_single_mut() {
|
|
audio.play(audio_assets.flash_click.clone());
|
|
|
|
spotlight.intensity = if spotlight.intensity > 0.0 {
|
|
0.0
|
|
} else {
|
|
320_000.0
|
|
};
|
|
}
|
|
}
|
|
|
|
pub fn update_flashlight_charge(
|
|
time: Res<Time>,
|
|
mut flashlight_query: Query<&mut Flashlight>,
|
|
) {
|
|
for mut flashlight in flashlight_query.iter_mut() {
|
|
if flashlight.is_on {
|
|
flashlight.charge = flashlight.charge - time.delta_secs() * 0.1;
|
|
} else {
|
|
flashlight.charge = flashlight.charge + time.delta_secs() * 0.1;
|
|
}
|
|
flashlight.charge = flashlight.charge.clamp(0.0, 4.0);
|
|
if flashlight.charge <= 0.0 {
|
|
flashlight.is_on = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn update_flashlight_button_animation(
|
|
time: Res<Time>,
|
|
mut query: Query<&mut FlashlightButtonAnimation>,
|
|
) {
|
|
for mut animation in query.iter_mut() {
|
|
if animation.is_pressed {
|
|
animation.timer.tick(time.delta());
|
|
if animation.timer.finished() {
|
|
animation.is_pressed = false;
|
|
} else {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn update_flashlight_sprite(
|
|
mut query: Query<(&FlashlightButtonAnimation, &mut Sprite, &Flashlight)>,
|
|
flashlights: Res<FlashlightAssets>,
|
|
){
|
|
for (animation, mut image, flashlight) in query.iter_mut() {
|
|
println!("charge: {}", flashlight.charge);
|
|
let charge = flashlight.charge.ceil() as i32;
|
|
println!("charge: {}", charge);
|
|
let sprite_image = match (charge, animation.is_pressed) {
|
|
(4, true) => flashlights.flash_hold_4_pressed.clone(),
|
|
(4, false) => flashlights.flash_hold_4.clone(),
|
|
(3, true) => flashlights.flash_hold_3_pressed.clone(),
|
|
(3, false) => flashlights.flash_hold_3.clone(),
|
|
(2, true) => flashlights.flash_hold_2_pressed.clone(),
|
|
(2, false) => flashlights.flash_hold_2.clone(),
|
|
(1, true) => flashlights.flash_hold_1_pressed.clone(),
|
|
(1, false) => flashlights.flash_hold_1.clone(),
|
|
(0, true) => flashlights.flash_hold_0_pressed.clone(),
|
|
(0, false) => flashlights.flash_hold_0.clone(),
|
|
_ => flashlights.flash_hold_0.clone()
|
|
};
|
|
*image = Sprite::from_image(sprite_image);
|
|
}
|
|
}
|