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>, heads_db: Res, ) -> Option { 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, 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, mut commands: Commands, player_id: Query<&PlayerId, With>, 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>) { 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>) { toggle_grab_cursor(&mut window); } fn setup_animations_marker_for_player( mut commands: Commands, animation_handles: Query>, child_of: Query<&ChildOf>, player_rig: Query<&ChildOf, With>, ) { 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; } } } }