use crate::{ alien::{ALIEN_ASSET_PATH, Animations}, camera::{CameraArmRotation, CameraTarget}, cash::{Cash, CashCollectEvent}, controls::Controls, heads_ui::HeadChanged, physics_layers::GameLayer, tb_entities::SpawnPoint, }; use avian3d::prelude::*; use bevy::{ prelude::*, window::{CursorGrabMode, PrimaryWindow}, }; use bevy_tnua::{TnuaUserControlsSystemSet, prelude::*}; use bevy_tnua_avian3d::TnuaAvian3dSensorShape; use std::time::Duration; #[derive(Component, Default)] pub struct Player; #[derive(Component, Default)] struct PlayerAnimations; #[derive(Component, Default)] struct PlayerHead; #[derive(Component, Default)] pub struct PlayerRig; #[derive(Resource, Default)] struct PlayerSpawned { spawned: bool, } #[derive(Resource, Default)] pub struct PlayerMovement { pub any_direction: bool, } pub fn plugin(app: &mut App) { app.init_resource::(); app.init_resource::(); app.add_systems(Startup, (initial_grab_cursor, cursor_recenter)); app.add_systems( Update, ( spawn, collect_cash, toggle_animation, setup_animations_marker_for_player, ), ); app.add_systems( FixedUpdate, apply_controls.in_set(TnuaUserControlsSystemSet), ); app.add_systems(Update, (rotate_view_keyboard, rotate_view_gamepad)); app.add_observer(update_head); } fn spawn( mut commands: Commands, asset_server: Res, query: Query<&Transform, With>, mut player_spawned: ResMut, ) { if player_spawned.spawned { return; } let Some(spawn) = query.iter().next() else { return; }; let transform = Transform::from_translation(spawn.translation + Vec3::new(0., 3., 0.)); let mesh = asset_server .load(GltfAssetLabel::Scene(0).from_asset("models/heads/angry demonstrator.glb")); commands .spawn(( Name::from("player"), Player, CameraTarget, transform, TransformInterpolation, TransformExtrapolation, Visibility::default(), RigidBody::Dynamic, Collider::capsule(1.2, 1.5), CollisionLayers::new(LayerMask(GameLayer::Player.to_bits()), LayerMask::ALL), LockedAxes::ROTATION_LOCKED, TnuaController::default(), TnuaAvian3dSensorShape(Collider::cylinder(0.8, 0.0)), )) .with_children(|parent| { parent .spawn(( Name::from("body rig"), PlayerRig, CameraArmRotation, Transform::from_translation(Vec3::new(0., -3., 0.)) .with_rotation(Quat::from_rotation_y(std::f32::consts::PI)) .with_scale(Vec3::splat(1.4)), SceneRoot( asset_server.load(GltfAssetLabel::Scene(0).from_asset(ALIEN_ASSET_PATH)), ), )) .with_child(( Name::from("head"), PlayerHead, Transform::from_translation(Vec3::new(0., 1.6, 0.)) .with_scale(Vec3::splat(0.7)), SceneRoot(mesh), )); }); commands.spawn(( AudioPlayer::new(asset_server.load("sfx/heads/angry demonstrator.ogg")), PlaybackSettings::DESPAWN, )); player_spawned.spawned = true; } fn rotate_view_gamepad( controls: Res, // todo: Put the player head as a child of the rig to avoid this mess: mut player: Query<&mut Transform, With>, ) { let Some(gamepad) = controls.gamepad_state else { return; }; if gamepad.view_mode { return; } for mut tr in &mut player { tr.rotate_y(gamepad.look_dir.x * -0.001); } } fn rotate_view_keyboard( mut controls: ResMut, // todo: Put the player head as a child of the rig to avoid this mess: mut player: Query<&mut Transform, With>, ) { if controls.keyboard_state.view_mode { return; } for mut tr in &mut player { tr.rotate_y(controls.keyboard_state.look_dir.x * -0.001); } controls.keyboard_state.look_dir = Vec2::ZERO; } fn cursor_recenter(mut q_windows: Query<&mut Window, With>) { let mut primary_window = q_windows.single_mut(); let center = Vec2::new(primary_window.width() / 2.0, primary_window.height() / 2.0); primary_window.set_cursor_position(Some(center)); } fn toggle_grab_cursor(window: &mut Window) { match window.cursor_options.grab_mode { CursorGrabMode::None => { window.cursor_options.grab_mode = CursorGrabMode::Confined; window.cursor_options.visible = false; } _ => { window.cursor_options.grab_mode = CursorGrabMode::None; window.cursor_options.visible = true; } } } fn initial_grab_cursor(mut primary_window: Query<&mut Window, With>) { if let Ok(mut window) = primary_window.get_single_mut() { toggle_grab_cursor(&mut window); } else { warn!("Primary window not found for `initial_grab_cursor`!"); } } fn apply_controls( controls: Res, mut query: Query<&mut TnuaController>, player: Query<&Transform, With>, mut movement: ResMut, ) { let Ok(mut controller) = query.get_single_mut() else { return; }; let Ok(player) = player.get_single() else { return; }; let mut direction = player.forward().as_vec3() * -controls.keyboard_state.move_dir.y; direction += player.right().as_vec3() * -controls.keyboard_state.move_dir.x; if let Some(gamepad) = controls.gamepad_state { direction += player.forward().as_vec3() * -gamepad.move_dir.y; direction += player.right().as_vec3() * -gamepad.move_dir.x; } if movement.any_direction != (direction != Vec3::ZERO) { movement.any_direction = direction != Vec3::ZERO; } controller.basis(TnuaBuiltinWalk { // The `desired_velocity` determines how the character will move. desired_velocity: direction.normalize_or_zero() * 15.0, spring_strengh: 1000., spring_dampening: 0.5, // The `float_height` must be greater (even if by little) from the distance between the // character's center and the lowest point of its collider. float_height: 3.0, // `TnuaBuiltinWalk` has many other fields for customizing the movement - but they have // sensible defaults. Refer to the `TnuaBuiltinWalk`'s documentation to learn what they do. ..Default::default() }); // Feed the jump action every frame as long as the player holds the jump button. If the player // stops holding the jump button, simply stop feeding the action. if controls.keyboard_state.jump || controls.gamepad_state.map(|gp| gp.jump).unwrap_or(false) { controller.action(TnuaBuiltinJump { // The height is the only mandatory field of the jump button. height: 4.0, // `TnuaBuiltinJump` also has customization fields with sensible defaults. ..Default::default() }); } } fn collect_cash( mut commands: Commands, mut collision_event_reader: EventReader, query_player: Query<&Player>, query_cash: Query<&Cash>, ) { for CollisionStarted(e1, e2) in collision_event_reader.read() { let collect = if query_player.contains(*e1) && query_cash.contains(*e2) { Some(*e2) } else if query_player.contains(*e2) && query_cash.contains(*e1) { Some(*e1) } else { None }; if let Some(cash) = collect { commands.trigger(CashCollectEvent); commands.entity(cash).despawn(); } } } fn setup_animations_marker_for_player( mut commands: Commands, animation_handles: Query>, parent_query: Query<&Parent>, player: Query<&Player>, ) { for entity in animation_handles.iter() { for ancestor in parent_query.iter_ancestors(entity) { if player.contains(ancestor) { commands.entity(entity).insert(PlayerAnimations); } } } } fn toggle_animation( animations: Res, mut transitions: Query< (&mut AnimationTransitions, &mut AnimationPlayer), With, >, movement: Res, ) { if movement.is_changed() { let index = if movement.any_direction { 0 } else { 1 }; for (mut transition, mut player) in &mut transitions { transition .play( &mut player, animations.animations[index], Duration::from_millis(100), ) .repeat(); } } } fn update_head( trigger: Trigger, mut commands: Commands, asset_server: Res, head: Query>, ) { let Ok(head) = head.get_single() else { return; }; let head_str = match trigger.0 { 0 => "angry demonstrator", 1 => "commando", 2 => "goblin", 3 => "highland hammer thrower", _ => "legionnaire", }; commands.spawn(( AudioPlayer::new(asset_server.load(format!("sfx/heads/{}.ogg", head_str))), PlaybackSettings::DESPAWN, )); let mesh = asset_server .load(GltfAssetLabel::Scene(0).from_asset(format!("models/heads/{}.glb", head_str))); commands.entity(head).insert(SceneRoot(mesh)); }