bevy_tnua/control_helpers/
crouch_enforcer.rs

1use std::any::Any;
2
3use bevy::ecs::schedule::{InternedScheduleLabel, ScheduleLabel};
4use bevy::ecs::system::EntityCommands;
5use bevy::prelude::*;
6use bevy_tnua_physics_integration_layer::math::{AdjustPrecision, Float, Quaternion, Vector3};
7
8use crate::controller::TnuaController;
9use crate::subservient_sensors::TnuaSubservientSensor;
10use crate::{TnuaAction, TnuaPipelineStages, TnuaProximitySensor};
11
12pub struct TnuaCrouchEnforcerPlugin {
13    schedule: InternedScheduleLabel,
14}
15
16/// A plugin required for making [`TnuaCrouchEnforcer`] work.
17///
18/// Must run in the same schedule as
19/// ['TnuaControllerPlugin'](crate::prelude::TnuaControllerPlugin).
20impl TnuaCrouchEnforcerPlugin {
21    pub fn new(schedule: impl ScheduleLabel) -> Self {
22        Self {
23            schedule: schedule.intern(),
24        }
25    }
26}
27
28impl Plugin for TnuaCrouchEnforcerPlugin {
29    fn build(&self, app: &mut App) {
30        app.add_systems(
31            self.schedule,
32            update_crouch_enforcer.in_set(TnuaPipelineStages::SubservientSensors),
33        );
34    }
35}
36
37/// Prevents the character from standing up if the player stops feeding a crouch action (like
38/// [`TnuaBuiltinCrouch`](crate::builtins::TnuaBuiltinCrouch)) while under an obstacle.
39///
40/// This will create a child entity with a proximity sensor pointed upward. When that sensor senses
41/// a ceiling, it will force feed the action even if it is no longer fed by the game code into the
42/// controller (which would happen if the player releases the crouch button)
43///
44/// Using it requires three things:
45///
46/// 1. Adding the plugin [`TnuaCrouchEnforcerPlugin`].
47/// 2. Adding [`TnuaCrouchEnforcer`] as a component to the character entity.
48/// 2. Passing the crouch action through the component's
49///    [`enforcing`](TnuaCrouchEnforcer::enforcing) method:
50///     ```no_run
51///     # use bevy_tnua::prelude::*;
52///     # use bevy_tnua::builtins::TnuaBuiltinCrouch;
53///     # use bevy_tnua::control_helpers::TnuaCrouchEnforcer;
54///     # let mut controller = TnuaController::default();
55///     # let mut crouch_enforcer = TnuaCrouchEnforcer::new(Default::default(), |_| {});
56///     controller.action(crouch_enforcer.enforcing(TnuaBuiltinCrouch {
57///         float_offset: -0.9,
58///         ..Default::default()
59///     }));
60///     ```
61#[derive(Component)]
62pub struct TnuaCrouchEnforcer {
63    sensor_entity: Option<Entity>,
64    offset: Vector3,
65    modify_sensor: Box<dyn Send + Sync + Fn(&mut EntityCommands)>,
66    enforced_action: Option<(Box<dyn DynamicCrouchEnforcedAction>, bool)>,
67    currently_enforcing: bool,
68}
69
70impl TnuaCrouchEnforcer {
71    /// Create a new crouch enforcer, to be added as a component to the entity that the crouch
72    /// action will be fed to.
73    ///
74    /// # Arguments:
75    ///
76    /// * `offset` - the origin of the proximity sensor used to determine if the character needs to
77    ///   crouch. Should be placed at the top of the collider. The sensor is always pointed
78    ///   upwards.
79    /// * `modify_sensor` - a function called with the command that creates the sensor. This
80    ///   function has the opportunity to add things to the sensor entity - mostly cast-shape
81    ///   components.
82    pub fn new(
83        offset: Vector3,
84        modify_sensor: impl 'static + Send + Sync + Fn(&mut EntityCommands),
85    ) -> Self {
86        Self {
87            sensor_entity: None,
88            offset,
89            modify_sensor: Box::new(modify_sensor),
90            enforced_action: None,
91            currently_enforcing: false,
92        }
93    }
94
95    pub fn enforcing<A: TnuaCrouchEnforcedAction>(&mut self, mut crouch_action: A) -> A {
96        if let Some((enforced_action, fed_this_frame)) = self.enforced_action.as_mut() {
97            if enforced_action.overwrite(&crouch_action).is_ok() {
98                *fed_this_frame = true;
99                if self.currently_enforcing {
100                    crouch_action.prevent_cancellation();
101                }
102                return crouch_action;
103            }
104        }
105        self.enforced_action = Some((
106            Box::new(BoxableCrouchEnforcedAction(crouch_action.clone())),
107            true,
108        ));
109        if self.currently_enforcing {
110            crouch_action.prevent_cancellation();
111        }
112        crouch_action
113    }
114}
115
116/// An action that can be enforced by [`TnuaCrouchEnforcer`].
117pub trait TnuaCrouchEnforcedAction: TnuaAction + Clone {
118    /// The range, from the sensor's offset (as set by [`TnuaCrouchEnforcer::new`]), to check for a
119    /// ceiling. If the sensor finds anything within that range - the crouch will be enforced.
120    fn range_to_cast_up(&self, state: &Self::State) -> Float;
121
122    /// Modify the action so that it won't be cancellable by another action.
123    fn prevent_cancellation(&mut self);
124}
125
126trait DynamicCrouchEnforcedAction: Send + Sync {
127    fn overwrite(&mut self, value: &dyn Any) -> Result<(), ()>;
128    fn feed_to_controller(&mut self, controller: &mut TnuaController);
129    fn range_to_cast_up(&self, controller: &TnuaController) -> Option<Float>;
130}
131
132struct BoxableCrouchEnforcedAction<A: TnuaCrouchEnforcedAction>(A);
133
134impl<A: TnuaCrouchEnforcedAction> DynamicCrouchEnforcedAction for BoxableCrouchEnforcedAction<A> {
135    fn overwrite(&mut self, value: &dyn Any) -> Result<(), ()> {
136        if let Some(concrete) = value.downcast_ref::<A>() {
137            self.0 = concrete.clone();
138            Ok(())
139        } else {
140            Err(())
141        }
142    }
143
144    fn feed_to_controller(&mut self, controller: &mut TnuaController) {
145        let mut action = self.0.clone();
146        action.prevent_cancellation();
147        controller.action(action);
148    }
149
150    fn range_to_cast_up(&self, controller: &TnuaController) -> Option<Float> {
151        if let Some((action, state)) = controller.concrete_action::<A>() {
152            Some(action.range_to_cast_up(state))
153        } else {
154            None
155        }
156    }
157}
158
159fn update_crouch_enforcer(
160    mut query: Query<(Entity, &mut TnuaController, &mut TnuaCrouchEnforcer)>,
161    mut sensors_query: Query<(&mut TnuaProximitySensor, Has<TnuaSubservientSensor>)>,
162    mut commands: Commands,
163) {
164    for (owner_entity, mut controller, mut crouch_enforcer) in query.iter_mut() {
165        struct SetSensor {
166            cast_direction: Dir3,
167            cast_range: Float,
168        }
169        let set_sensor: Option<SetSensor>;
170        if let Some((enforced_action, fed_this_frame)) = crouch_enforcer.enforced_action.as_mut() {
171            if *fed_this_frame {
172                set_sensor = enforced_action
173                    .range_to_cast_up(controller.as_mut())
174                    .and_then(|cast_range| {
175                        let (main_sensor, _) = sensors_query.get(owner_entity).ok()?;
176                        Some(SetSensor {
177                            cast_direction: -main_sensor.cast_direction,
178                            cast_range,
179                        })
180                    });
181                *fed_this_frame = false;
182            } else {
183                set_sensor = None;
184                crouch_enforcer.enforced_action = None;
185            }
186        } else {
187            set_sensor = None;
188        }
189
190        if let Some(SetSensor {
191            cast_direction,
192            cast_range,
193        }) = set_sensor
194        {
195            if let Some((mut subservient_sensor, true)) = crouch_enforcer
196                .sensor_entity
197                .and_then(|entity| sensors_query.get_mut(entity).ok())
198            {
199                // TODO: Maybe add the horizontal rotation as well somehow?
200                subservient_sensor.cast_shape_rotation = Quaternion::from_rotation_arc(
201                    Vector3::Y,
202                    subservient_sensor.cast_direction.adjust_precision(),
203                );
204                subservient_sensor.cast_origin = subservient_sensor
205                    .cast_shape_rotation
206                    .mul_vec3(crouch_enforcer.offset);
207                subservient_sensor.cast_direction = cast_direction;
208                subservient_sensor.cast_range = cast_range;
209            } else {
210                let mut cmd = commands.spawn((
211                    Transform::default(),
212                    TnuaSubservientSensor { owner_entity },
213                    TnuaProximitySensor {
214                        cast_origin: crouch_enforcer.offset,
215                        cast_direction,
216                        cast_range,
217                        ..Default::default()
218                    },
219                ));
220                cmd.insert(ChildOf(owner_entity));
221                (crouch_enforcer.modify_sensor)(&mut cmd);
222                let sensor_entity = cmd.id();
223                crouch_enforcer.sensor_entity = Some(sensor_entity);
224            }
225        } else if let Some((mut subservient_sensor, true)) = crouch_enforcer
226            .sensor_entity
227            .and_then(|entity| sensors_query.get_mut(entity).ok())
228        {
229            // Turn it off
230            subservient_sensor.cast_range = 0.0;
231        }
232        if let Some((enforced_action, fed_this_frame)) =
233            crouch_enforcer.sensor_entity.and_then(|entity| {
234                let Ok((sensor, true)) = sensors_query.get_mut(entity) else {
235                    return None;
236                };
237                if sensor.output.is_some() {
238                    crouch_enforcer.enforced_action.as_mut()
239                } else {
240                    None
241                }
242            })
243        {
244            enforced_action.feed_to_controller(controller.as_mut());
245            *fed_this_frame = true;
246            crouch_enforcer.currently_enforcing = true;
247        } else {
248            crouch_enforcer.currently_enforcing = false;
249        }
250    }
251}