mod marker; mod target_ui; use crate::{ GameState, head::ActiveHead, heads_database::HeadsDatabase, physics_layers::GameLayer, player::{Player, PlayerBodyMesh}, tb_entities::EnemySpawn, }; use avian3d::prelude::*; use bevy::prelude::*; use marker::MarkerEvent; use std::f32::consts::PI; #[derive(Component, Reflect, Default, Deref)] #[reflect(Component)] pub struct AimTarget(pub Option); #[derive(Component, Reflect)] #[reflect(Component)] #[require(AimTarget)] pub struct AimState { pub range: f32, pub max_angle: f32, pub spawn_marker: bool, } impl Default for AimState { fn default() -> Self { Self { range: 80., max_angle: PI / 8., spawn_marker: true, } } } pub fn plugin(app: &mut App) { app.add_plugins(target_ui::plugin); app.add_plugins(marker::plugin); app.add_systems( Update, (update_player_aim, update_npc_aim, head_change).run_if(in_state(GameState::Playing)), ); app.add_systems(Update, add_aim); } fn add_aim(mut commands: Commands, query: Query>) { for e in query.iter() { commands.entity(e).insert(AimState::default()); } } 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>, player_rot: Query<(&Transform, &GlobalTransform), With>, mut player_aim: Query<(&AimState, &mut AimTarget), With>, spatial_query: SpatialQuery, ) { let Some((state, mut aim_target)) = player_aim.iter_mut().next() else { return; }; let Some((player_pos, player_forward)) = player_rot .iter() .next() .map(|(t, global)| (global.translation(), t.forward())) else { return; }; let mut new_target = None; let mut target_distance = f32::MAX; for (e, t) in potential_targets.iter() { let delta = player_pos - t.translation; 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 { if commands.get_entity(*e).is_none() { aim_target.0 = None; return; } } if new_target != aim_target.0 { if state.spawn_marker { 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 = pos - t.translation; 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 { if commands.get_entity(*e).is_none() { 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 }