use crate::{ GameState, control::Inputs, head::ActiveHead, heads_database::HeadsDatabase, hitpoints::Hitpoints, physics_layers::GameLayer, player::{LocalPlayer, Player}, tb_entities::EnemySpawn, }; use avian3d::prelude::*; use bevy::prelude::*; use serde::{Deserialize, Serialize}; use std::f32::consts::PI; #[derive(Component, Reflect, Default, Deref, PartialEq, Serialize, Deserialize)] #[reflect(Component)] pub struct AimTarget(pub Option); #[derive(Component, Reflect, PartialEq, Serialize, Deserialize)] #[reflect(Component)] #[require(AimTarget)] pub struct AimState { pub range: f32, pub max_angle: f32, } #[derive(Event)] pub enum MarkerEvent { Spawn(Entity), Despawn, } impl Default for AimState { fn default() -> Self { Self { range: 80., max_angle: PI / 8., } } } pub fn plugin(app: &mut App) { app.register_type::(); app.register_type::(); app.register_required_components::(); app.add_systems( Update, (update_player_aim, update_npc_aim, head_change).run_if(in_state(GameState::Playing)), ); } fn head_change( mut query: Query<(&ActiveHead, &mut AimState), Changed>, heads_db: Res, ) { for (head, mut state) in query.iter_mut() { // info!("head changed: {}", head.0); // state.max_angle = if head.0 == 0 { PI / 8. } else { PI / 2. } let stats = heads_db.head_stats(head.0); state.range = stats.range; } } fn update_player_aim( mut commands: Commands, potential_targets: Query<(Entity, &Transform), With>, mut player_aim: Query< ( Entity, &AimState, &mut AimTarget, &GlobalTransform, &Inputs, Has, ), With, >, spatial_query: SpatialQuery, ) { for (player, state, mut aim_target, global_tf, inputs, is_local) in player_aim.iter_mut() { let (player_pos, player_forward) = (global_tf.translation(), inputs.look_dir); let mut new_target = None; let mut target_distance = f32::MAX; for (e, t) in potential_targets.iter() { if e == player { continue; } let delta = t.translation - player_pos; let distance = delta.length(); if distance > state.range { continue; } let angle = player_forward.angle_between(delta.normalize()); if angle < state.max_angle && distance < target_distance { if !line_of_sight(&spatial_query, player_pos, delta, distance) { continue; } new_target = Some(e); target_distance = distance; } } if let Some(e) = &aim_target.0 && commands.get_entity(*e).is_err() { aim_target.0 = None; return; } if new_target != aim_target.0 { if is_local { if let Some(target) = new_target { commands.trigger(MarkerEvent::Spawn(target)); } else { commands.trigger(MarkerEvent::Despawn); } } aim_target.0 = new_target; } } } fn update_npc_aim( mut commands: Commands, mut subject: Query<(&AimState, &Transform, &mut AimTarget), With>, potential_targets: Query<(Entity, &Transform), With>, spatial_query: SpatialQuery, ) { for (state, t, mut aim_target) in subject.iter_mut() { let (pos, forward) = (t.translation, t.forward()); let mut new_target = None; let mut target_distance = f32::MAX; for (e, t) in potential_targets.iter() { let delta = t.translation - pos; let distance = delta.length(); if distance > state.range { continue; } let angle = forward.angle_between(delta.normalize()); if angle < state.max_angle && distance < target_distance { if !line_of_sight(&spatial_query, pos, delta, distance) { continue; } new_target = Some(e); target_distance = distance; } } if let Some(e) = &aim_target.0 && commands.get_entity(*e).is_err() { aim_target.0 = None; return; } if new_target != aim_target.0 { aim_target.0 = new_target; } } } fn line_of_sight( spatial_query: &SpatialQuery<'_, '_>, player_pos: Vec3, delta: Vec3, distance: f32, ) -> bool { if let Some(_hit) = spatial_query.cast_shape( &Collider::sphere(0.1), player_pos + delta.normalize() + (Vec3::Y * 2.), Quat::default(), Dir3::new(delta).unwrap(), &ShapeCastConfig { max_distance: distance * 0.98, compute_contact_on_penetration: false, ignore_origin_penetration: true, ..Default::default() }, &SpatialQueryFilter::default().with_mask(LayerMask(GameLayer::Level.to_bits())), ) { // info!("no line of sight"); return false; }; true }