329 lines
9.7 KiB
Rust
329 lines
9.7 KiB
Rust
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::<PlayerSpawned>();
|
|
app.init_resource::<PlayerMovement>();
|
|
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<AssetServer>,
|
|
query: Query<&Transform, With<SpawnPoint>>,
|
|
mut player_spawned: ResMut<PlayerSpawned>,
|
|
) {
|
|
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<Controls>,
|
|
// todo: Put the player head as a child of the rig to avoid this mess:
|
|
mut player: Query<&mut Transform, With<PlayerRig>>,
|
|
) {
|
|
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<Controls>,
|
|
// todo: Put the player head as a child of the rig to avoid this mess:
|
|
mut player: Query<&mut Transform, With<PlayerRig>>,
|
|
) {
|
|
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<PrimaryWindow>>) {
|
|
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<PrimaryWindow>>) {
|
|
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<Controls>,
|
|
mut query: Query<&mut TnuaController>,
|
|
player: Query<&Transform, With<PlayerRig>>,
|
|
mut movement: ResMut<PlayerMovement>,
|
|
) {
|
|
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<CollisionStarted>,
|
|
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<Entity, Added<AnimationGraphHandle>>,
|
|
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<Animations>,
|
|
mut transitions: Query<
|
|
(&mut AnimationTransitions, &mut AnimationPlayer),
|
|
With<PlayerAnimations>,
|
|
>,
|
|
movement: Res<PlayerMovement>,
|
|
) {
|
|
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<HeadChanged>,
|
|
mut commands: Commands,
|
|
asset_server: Res<AssetServer>,
|
|
head: Query<Entity, With<PlayerHead>>,
|
|
) {
|
|
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));
|
|
}
|