Files
HEDZReloaded/crates/hedz_reloaded/src/player.rs
2025-12-22 07:22:27 -05:00

249 lines
7.1 KiB
Rust

use crate::{
GameState,
abilities::PlayerTriggerState,
backpack::Backpack,
camera::{CameraArmRotation, CameraTarget},
cash::CashInventory,
character::{AnimatedCharacter, HedzCharacter},
control::{Inputs, LocalInputs, controller_common::PlayerCharacterController},
global_observer,
head::ActiveHead,
head_drop::HeadDrops,
heads::{ActiveHeads, HeadChanged, HeadState},
heads_database::HeadsDatabase,
hitpoints::{Hitpoints, Kill},
npc::SpawnCharacter,
protocol::{ClientHeadChanged, OwnedByClient, PlaySound, PlayerId},
tb_entities::SpawnPoint,
};
use bevy::{
input::common_conditions::input_just_pressed,
prelude::*,
window::{CursorGrabMode, CursorOptions, PrimaryWindow},
};
use bevy_replicon::prelude::{ClientId, Replicated, SendMode, ServerTriggerExt, ToClients};
use happy_feet::debug::DebugInput;
use rand::Rng;
use serde::{Deserialize, Serialize};
#[derive(Component, Default, Serialize, Deserialize, PartialEq)]
#[require(HedzCharacter, DebugInput = DebugInput, PlayerTriggerState)]
pub struct Player;
#[derive(Component, Debug, Reflect)]
#[reflect(Component)]
#[require(LocalInputs)]
pub struct LocalPlayer;
#[derive(Component, Default, Serialize, Deserialize, PartialEq)]
#[require(Transform, Visibility)]
pub struct PlayerBodyMesh;
/// Server-side only; inserted on each `client` (not the controller) to track player ids.
#[derive(Component, Clone, Copy)]
pub struct ClientPlayerId(pub PlayerId);
pub fn plugin(app: &mut App) {
app.add_systems(
OnEnter(GameState::Playing),
(toggle_cursor_system, cursor_recenter),
);
app.add_systems(
Update,
(
setup_animations_marker_for_player,
toggle_cursor_system.run_if(input_just_pressed(KeyCode::Escape)),
)
.run_if(in_state(GameState::Playing)),
);
global_observer!(app, on_update_head_mesh);
}
pub fn spawn(
mut commands: Commands,
owner: ClientId,
id: PlayerId,
query: Query<&Transform, With<SpawnPoint>>,
heads_db: Res<HeadsDatabase>,
) -> Option<Entity> {
let spawn = query.iter().next()?;
// This offset helps prevent players from getting stuck inside each other on spawn and causing a perpetual
// motion machine.
let random_offset = Vec3::new(
rand::thread_rng().gen_range(-0.01..0.01),
0.0,
rand::thread_rng().gen_range(-0.01..0.01),
);
let transform =
Transform::from_translation(spawn.translation + Vec3::new(0., 3., 0.) + random_offset);
let id = commands
.spawn((
(
Name::from("player"),
Player,
ActiveHead(0),
ActiveHeads::new([
Some(HeadState::new(0, heads_db.as_ref())),
Some(HeadState::new(3, heads_db.as_ref())),
Some(HeadState::new(6, heads_db.as_ref())),
Some(HeadState::new(10, heads_db.as_ref())),
Some(HeadState::new(9, heads_db.as_ref())),
]),
Hitpoints::new(100),
CashInventory::default(),
CameraTarget,
transform,
Visibility::default(),
PlayerCharacterController,
id,
),
Backpack::default(),
Inputs::default(),
Replicated,
))
.with_children(|c| {
c.spawn((
Name::new("player-rig"),
PlayerBodyMesh,
CameraArmRotation,
Replicated,
))
.with_child((
Name::new("player-animated-character"),
AnimatedCharacter::new(0),
Replicated,
));
})
.observe(on_kill)
.id();
if let Some(owner) = owner.entity() {
commands.entity(id).insert(OwnedByClient(owner));
}
commands.server_trigger(ToClients {
mode: SendMode::Broadcast,
message: PlaySound::Head("angry demonstrator".to_string()),
});
commands.trigger(SpawnCharacter(transform.translation));
Some(id)
}
fn on_kill(
trigger: On<Kill>,
mut commands: Commands,
mut query: Query<(
Entity,
&Transform,
&ActiveHead,
&mut ActiveHeads,
&mut Hitpoints,
)>,
) {
let Ok((player, transform, active, mut heads, mut hp)) = query.get_mut(trigger.event().entity)
else {
return;
};
commands.trigger(HeadDrops::new(transform.translation, active.0));
if let Some(new_head) = heads.loose_current() {
hp.set_health(heads.current().unwrap().health);
commands.trigger(HeadChanged {
entity: player,
head: new_head,
});
}
}
fn on_update_head_mesh(
trigger: On<HeadChanged>,
mut commands: Commands,
player_id: Query<&PlayerId, With<Player>>,
children: Query<&Children>,
player_body_mesh: Query<&PlayerBodyMesh>,
animated_characters: Query<&AnimatedCharacter>,
mut active_head: Query<&mut ActiveHead>,
) -> Result {
let player_id = *(player_id.get(trigger.entity)?);
let player_body_mesh = children
.get(trigger.entity)?
.iter()
.find(|child| player_body_mesh.get(*child).is_ok())
.unwrap();
let animated_character = children
.get(player_body_mesh)?
.iter()
.find(|child| animated_characters.get(*child).is_ok())
.unwrap();
{
let mut active_head = active_head.get_mut(trigger.entity)?;
active_head.0 = trigger.head;
}
commands
.entity(animated_character)
.insert(AnimatedCharacter::new(trigger.head));
commands.server_trigger(ToClients {
mode: SendMode::Broadcast,
message: ClientHeadChanged {
player: player_id,
head: trigger.head,
},
});
Ok(())
}
fn cursor_recenter(q_windows: Single<&mut Window, With<PrimaryWindow>>) {
let mut primary_window = q_windows;
let center = Vec2::new(
primary_window.resolution.width() / 2.,
primary_window.resolution.height() / 2.,
);
primary_window.set_cursor_position(Some(center));
}
fn toggle_grab_cursor(options: &mut CursorOptions) {
match options.grab_mode {
CursorGrabMode::None => {
options.grab_mode = CursorGrabMode::Confined;
options.visible = false;
}
_ => {
options.grab_mode = CursorGrabMode::None;
options.visible = true;
}
}
}
fn toggle_cursor_system(mut window: Single<&mut CursorOptions, With<PrimaryWindow>>) {
toggle_grab_cursor(&mut window);
}
fn setup_animations_marker_for_player(
mut commands: Commands,
animation_handles: Query<Entity, Added<AnimationGraphHandle>>,
child_of: Query<&ChildOf>,
player_rig: Query<&ChildOf, With<PlayerBodyMesh>>,
) {
for animation_rig in animation_handles.iter() {
for ancestor in child_of.iter_ancestors(animation_rig) {
if let Ok(rig_child_of) = player_rig.get(ancestor) {
commands.entity(rig_child_of.parent());
return;
}
}
}
}