use avian3d::sync::ancestor_marker::AncestorMarker;
use super::{HoldSystem, prelude::*};
use crate::{
math::{GetBestGlobalTransform as _, rigid_body_compound_collider},
prelude::*,
prop::PrePickupRotation,
verb::{Holding, SetVerb, Verb},
};
pub(super) fn plugin(app: &mut App) {
app.add_systems(PhysicsSchedule, set_targets.in_set(HoldSystem::SetTargets));
}
fn set_targets(
mut commands: Commands,
spatial_query: SpatialQuery,
mut q_actor: Query<(
Entity,
&AvianPickupActor,
&HoldError,
&mut ShadowParams,
&Holding,
)>,
q_actor_transform: Query<(&GlobalTransform, Option<&Position>, Option<&Rotation>)>,
mut q_prop: Query<(
&Rotation,
Option<&PrePickupRotation>,
Option<&PreferredPickupRotation>,
Option<&PreferredPickupDistanceOverride>,
Option<&PitchRangeOverride>,
)>,
q_collider_ancestor: Query<&Children, With<AncestorMarker<ColliderMarker>>>,
q_collider_parent: Query<&ColliderParent>,
q_collider: Query<(&Transform, &Collider, Option<&CollisionLayers>)>,
) {
let max_error = 0.3048; for (actor, config, hold_error, mut shadow, holding) in q_actor.iter_mut() {
let prop = holding.0;
if hold_error.error > max_error {
commands
.entity(actor)
.queue(SetVerb::new(Verb::Drop { prop, forced: true }));
continue;
}
let actor_transform = q_actor_transform.get_best_global_transform(actor);
let Ok((
prop_rotation,
pre_pickup_rotation,
preferred_rotation,
preferred_distance,
clamp_pitch,
)) = q_prop.get_mut(prop)
else {
error!("Prop entity was deleted or in an invalid state. Ignoring.");
continue;
};
let pitch_range = clamp_pitch
.map(|c| &c.0)
.unwrap_or(&config.hold.pitch_range);
let (actor_yaw, actor_pitch, actor_roll) = actor_transform.rotation.to_euler(EulerRot::YXZ);
let actor_to_prop_pitch = actor_pitch.clamp(*pitch_range.start(), *pitch_range.end());
let clamped_rotation =
Quat::from_euler(EulerRot::YXZ, actor_yaw, actor_to_prop_pitch, actor_roll);
let forward = Transform::from_rotation(clamped_rotation).forward();
let prop_collider = rigid_body_compound_collider(
prop,
&q_collider_ancestor,
&q_collider,
&config.prop_filter,
);
let Some(prop_collider) = prop_collider else {
error!("Held prop does not have a collider in its hierarchy. Ignoring.");
continue;
};
let prop_radius_wrt_direction =
collide_get_extent(&prop_collider, Vec3::ZERO, prop_rotation.0, -forward);
let min_non_penetrating_distance = prop_radius_wrt_direction;
let min_distance = min_non_penetrating_distance + config.hold.min_distance;
let preferred_distance = preferred_distance
.map(|d| d.0)
.unwrap_or(config.hold.preferred_distance)
+ min_non_penetrating_distance;
let max_distance = preferred_distance.max(min_distance);
let Some(actor_space_rotation) = preferred_rotation
.map(|preferred| preferred.0)
.or_else(|| pre_pickup_rotation.map(|pre| pre.0))
else {
error!("Held prop does not have a preferred or pre-pickup rotation. Ignoring.");
continue;
};
let clamped_actor_transform = actor_transform.with_rotation(clamped_rotation);
let target_rotation =
prop_rotation_from_actor_space(actor_space_rotation, clamped_actor_transform);
shadow.target_rotation = target_rotation;
let max_cast_toi = max_distance + min_distance + 0.5;
let is_terrain = |entity: Entity| {
q_collider_parent
.get(entity)
.is_ok_and(|parent| parent.get() != prop)
};
let terrain_hit = spatial_query.cast_shape_predicate(
&prop_collider,
actor_transform.translation,
target_rotation,
forward,
&ShapeCastConfig {
max_distance: max_cast_toi,
ignore_origin_penetration: true,
..default()
},
&config.obstacle_filter,
&is_terrain,
);
let distance = if let Some(terrain_hit) = terrain_hit {
let toi = terrain_hit.distance;
let fraction = toi / max_distance;
if fraction < 0.5 {
min_distance
} else {
max_distance.min(toi)
}
} else {
max_distance
};
let target_position = actor_transform.translation + forward * distance;
shadow.target_position = target_position;
}
}
fn collide_get_extent(collider: &Collider, origin: Vec3, rotation: Quat, dir: Dir3) -> f32 {
const TRANSLATION: Vec3 = Vec3::ZERO;
const MAX_TOI: f32 = f32::INFINITY;
const SOLID: bool = false;
const ARBITRARY_ROTATION: f32 = 5e-3;
for offset in [
Quat::IDENTITY,
Quat::from_rotation_x(ARBITRARY_ROTATION),
Quat::from_rotation_x(-ARBITRARY_ROTATION),
Quat::from_rotation_y(ARBITRARY_ROTATION),
Quat::from_rotation_y(-ARBITRARY_ROTATION),
Quat::from_rotation_z(ARBITRARY_ROTATION),
Quat::from_rotation_z(-ARBITRARY_ROTATION),
] {
let rotation = rotation * offset;
let hit = collider.cast_ray(TRANSLATION, rotation, origin, dir.into(), MAX_TOI, SOLID);
if let Some((toi, _normal)) = hit {
return toi;
}
}
let aabb = collider.aabb(origin, rotation);
(aabb.max / 2.).length()
}
fn prop_rotation_from_actor_space(rot: Quat, actor: Transform) -> Quat {
let actor_matrix = actor.compute_affine();
let rot_to_actor = Transform::from_rotation(rot).compute_affine();
let out_affine = actor_matrix * rot_to_actor;
Quat::from_affine3(&out_affine)
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_collide_get_extent() {
let collider = Collider::capsule(0.3, 1.2);
let rot = Quat::from_euler(EulerRot::YXZ, -0.014999974, -0.07314853, 0.);
let dir = Vec3::new(0.014959301, -0.073083326, -0.9972137)
.try_into()
.unwrap();
let extent = collide_get_extent(&collider, Vec3::ZERO, rot, dir);
assert!(extent > 0.0);
assert!(extent < 0.6);
}
#[test]
#[ignore = "Parry bug"]
fn test_collider_get_extent_manual() {
let collider = Collider::capsule(0.3, 1.2);
let rotation = Quat::from_euler(EulerRot::YXZ, -0.014999974, -0.07314853, 0.);
let dir = Vec3::new(0.014959301, -0.073083326, -0.9972137);
const TRANSLATION: Vec3 = Vec3::ZERO;
const ORIGIN: Vec3 = Vec3::ZERO;
const MAX_TOI: f32 = f32::INFINITY;
const SOLID: bool = false;
let hit = collider.cast_ray(TRANSLATION, rotation, ORIGIN, dir, MAX_TOI, SOLID);
assert!(hit.is_some());
}
}