seldom_state/trigger/
input.rs

1use std::{any::type_name, ops::Range};
2
3use bevy_math::InvalidDirectionError;
4use leafwing_input_manager::action_state::ActionData;
5
6use crate::prelude::*;
7
8/// Trigger that transitions if the given [`Actionlike`]'s value is within the given bounds.
9/// Consider using `f32::NEG_INFINITY`/`f32::INFINITY` in the bounds.
10pub fn value<A: Actionlike>(
11    action: A,
12    bounds: Range<f32>,
13) -> impl EntityTrigger<Out = Result<f32, f32>> {
14    (move |In(entity): In<Entity>, actors: Query<&ActionState<A>>| {
15        let value = actors
16            .get(entity)
17            .unwrap_or_else(|_| {
18                panic!(
19                    "entity {entity:?} with `ValueTrigger<{0}>` is missing `ActionState<{0}>`",
20                    type_name::<A>()
21                )
22            })
23            .value(&action);
24
25        if bounds.contains(&value) {
26            Ok(value)
27        } else {
28            Err(value)
29        }
30    })
31    .into_trigger()
32}
33
34/// Unbounded [`value`]
35pub fn value_unbounded(action: impl Actionlike) -> impl EntityTrigger<Out = Result<f32, f32>> {
36    value(action, f32::NEG_INFINITY..f32::INFINITY)
37}
38
39/// [`value`] with only a minimum bound
40pub fn value_min(action: impl Actionlike, min: f32) -> impl EntityTrigger<Out = Result<f32, f32>> {
41    value(action, min..f32::INFINITY)
42}
43
44/// [`value`] with only a maximum bound
45pub fn value_max(action: impl Actionlike, max: f32) -> impl EntityTrigger<Out = Result<f32, f32>> {
46    value(action, f32::NEG_INFINITY..max)
47}
48
49/// [`value`] clamped to [-1, 1]
50pub fn clamped_value<A: Actionlike>(
51    action: A,
52    bounds: Range<f32>,
53) -> impl EntityTrigger<Out = Result<f32, f32>> {
54    (move |In(entity): In<Entity>, actors: Query<&ActionState<A>>| {
55        let value = actors
56            .get(entity)
57            .unwrap_or_else(|_| {
58                panic!(
59                    "entity {entity:?} with `ClampedValueTrigger<{0}>` is missing `ActionState<{0}>`",
60                    type_name::<A>()
61                )
62            })
63            .clamped_value(&action);
64
65        if bounds.contains(&value) {
66            Ok(value)
67        } else {
68            Err(value)
69        }
70    })
71    .into_trigger()
72}
73
74/// Unbounded [`clamped_value`]
75pub fn clamped_value_unbounded(
76    action: impl Actionlike,
77) -> impl EntityTrigger<Out = Result<f32, f32>> {
78    clamped_value(action, f32::NEG_INFINITY..f32::INFINITY)
79}
80
81/// [`clamped_value`] with only a minimum bound
82pub fn clamped_value_min(
83    action: impl Actionlike,
84    min: f32,
85) -> impl EntityTrigger<Out = Result<f32, f32>> {
86    clamped_value(action, min..f32::INFINITY)
87}
88
89/// [`clamped_value`] with only a maximum bound
90pub fn clamped_value_max(
91    action: impl Actionlike,
92    max: f32,
93) -> impl EntityTrigger<Out = Result<f32, f32>> {
94    clamped_value(action, f32::NEG_INFINITY..max)
95}
96
97/// Trigger that transitions if the given [`Actionlike`]'s [`DualAxisData`] is within the given
98/// bounds.
99///
100/// If no minimum length is necessary, use `0.`. To exclude specifically neutral axis pairs,
101/// use a small positive value. If no maximum length is necessary, use `f32::INFINITY`, or similar.
102/// If rotation bounds are not necessary, use the same value for the minimum and maximum ex.
103/// `Dir2::Y..Dir2::Y`.
104pub fn axis_pair<A: Actionlike>(
105    action: A,
106    length_bounds: Range<f32>,
107    rotation_bounds: Range<Dir2>,
108) -> impl EntityTrigger<Out = Result<Vec2, Vec2>> {
109    (move |In(entity): In<Entity>, actors: Query<&ActionState<A>>| {
110        let axis_pair = actors
111            .get(entity)
112            .unwrap_or_else(|_| {
113                panic!(
114                    "entity {entity:?} with `AxisPairTrigger<{0}>` is missing `ActionState<{0}>`",
115                    type_name::<A>()
116                )
117            })
118            .axis_pair(&action);
119
120        match Dir2::new_and_length(axis_pair) {
121            Ok((rotation, length)) => {
122                length_bounds.contains(&length)
123                    && (rotation_bounds.start == rotation_bounds.end
124                        || ({
125                            let start = rotation_bounds.start.to_angle();
126                            let end = rotation_bounds.end.to_angle();
127                            let rotation = rotation.to_angle();
128
129                            if start < end {
130                                rotation >= start && rotation <= end
131                            } else {
132                                rotation >= start || rotation <= end
133                            }
134                        }))
135            }
136            Err(InvalidDirectionError::Zero) => length_bounds.contains(&0.),
137            Err(InvalidDirectionError::Infinite | InvalidDirectionError::NaN) => false,
138        }
139        .then_some(axis_pair)
140        .ok_or(axis_pair)
141    })
142    .into_trigger()
143}
144
145/// Unbounded [`axis_pair`]
146pub fn axis_pair_unbounded(
147    action: impl Actionlike,
148) -> impl EntityTrigger<Out = Result<Vec2, Vec2>> {
149    axis_pair(action, 0.0..f32::INFINITY, Dir2::Y..Dir2::Y)
150}
151
152/// [`axis_pair`] with only a minimum length bound
153pub fn axis_pair_min_length(
154    action: impl Actionlike,
155    min_length: f32,
156) -> impl EntityTrigger<Out = Result<Vec2, Vec2>> {
157    axis_pair(action, min_length..f32::INFINITY, Dir2::Y..Dir2::Y)
158}
159
160/// [`axis_pair`] with only a maximum length bound
161pub fn axis_pair_max_length(
162    action: impl Actionlike,
163    max_length: f32,
164) -> impl EntityTrigger<Out = Result<Vec2, Vec2>> {
165    axis_pair(action, 0.0..max_length, Dir2::Y..Dir2::Y)
166}
167
168/// [`axis_pair`] with only length bounds
169pub fn axis_pair_length_bounds(
170    action: impl Actionlike,
171    length_bounds: Range<f32>,
172) -> impl EntityTrigger<Out = Result<Vec2, Vec2>> {
173    axis_pair(action, length_bounds, Dir2::Y..Dir2::Y)
174}
175
176/// [`axis_pair`] with only rotation bounds
177pub fn axis_pair_rotation_bounds(
178    action: impl Actionlike,
179    rotation_bounds: Range<Dir2>,
180) -> impl EntityTrigger<Out = Result<Vec2, Vec2>> {
181    axis_pair(action, 0.0..f32::INFINITY, rotation_bounds)
182}
183
184/// [`axis_pair`] with axes clamped to [-1, 1]
185pub fn clamped_axis_pair<A: Actionlike>(
186    action: A,
187    length_bounds: Range<f32>,
188    rotation_bounds: Range<Dir2>,
189) -> impl EntityTrigger<Out = Result<Vec2, Vec2>> {
190    (move |In(entity): In<Entity>, actors: Query<&ActionState<A>>| {
191        let axis_pair = actors
192            .get(entity)
193            .unwrap_or_else(|_| {
194                panic!(
195                    "entity {entity:?} with `AxisPairTrigger<{0}>` is missing `ActionState<{0}>`",
196                    type_name::<A>()
197                )
198            })
199            .clamped_axis_pair(&action);
200
201        match Dir2::new_and_length(axis_pair) {
202            Ok((rotation, length)) => {
203                length_bounds.contains(&length)
204                    && (rotation_bounds.start == rotation_bounds.end
205                        || ({
206                            let start = rotation_bounds.start.to_angle();
207                            let end = rotation_bounds.end.to_angle();
208                            let rotation = rotation.to_angle();
209
210                            if start < end {
211                                rotation >= start && rotation <= end
212                            } else {
213                                rotation >= start || rotation <= end
214                            }
215                        }))
216            }
217            Err(InvalidDirectionError::Zero) => length_bounds.contains(&0.),
218            Err(InvalidDirectionError::Infinite | InvalidDirectionError::NaN) => false,
219        }
220        .then_some(axis_pair)
221        .ok_or(axis_pair)
222    })
223    .into_trigger()
224}
225
226/// Unbounded [`clamped_axis_pair`]
227pub fn clamped_axis_pair_unbounded(
228    action: impl Actionlike,
229) -> impl EntityTrigger<Out = Result<Vec2, Vec2>> {
230    clamped_axis_pair(action, 0.0..f32::INFINITY, Dir2::Y..Dir2::Y)
231}
232
233/// [`clamped_axis_pair`] with only a minimum length bound
234pub fn clamped_axis_pair_min_length(
235    action: impl Actionlike,
236    min_length: f32,
237) -> impl EntityTrigger<Out = Result<Vec2, Vec2>> {
238    clamped_axis_pair(action, min_length..f32::INFINITY, Dir2::Y..Dir2::Y)
239}
240
241/// [`clamped_axis_pair`] with only a maximum length bound
242pub fn clamped_axis_pair_max_length(
243    action: impl Actionlike,
244    max_length: f32,
245) -> impl EntityTrigger<Out = Result<Vec2, Vec2>> {
246    clamped_axis_pair(action, 0.0..max_length, Dir2::Y..Dir2::Y)
247}
248
249/// [`clamped_axis_pair`] with only length bounds
250pub fn clamped_axis_pair_length_bounds(
251    action: impl Actionlike,
252    length_bounds: Range<f32>,
253) -> impl EntityTrigger<Out = Result<Vec2, Vec2>> {
254    clamped_axis_pair(action, length_bounds, Dir2::Y..Dir2::Y)
255}
256
257/// [`clamped_axis_pair`] with only rotation bounds
258pub fn clamped_axis_pair_rotation_bounds(
259    action: impl Actionlike,
260    rotation_bounds: Range<Dir2>,
261) -> impl EntityTrigger<Out = Result<Vec2, Vec2>> {
262    clamped_axis_pair(action, 0.0..f32::INFINITY, rotation_bounds)
263}
264
265/// Trigger that transitions upon pressing the given [`Actionlike`]
266pub fn just_pressed<A: Actionlike>(action: A) -> impl EntityTrigger<Out = bool> {
267    (move |In(entity): In<Entity>, actors: Query<&ActionState<A>>| {
268        actors
269            .get(entity)
270            .unwrap_or_else(|_| {
271                panic!(
272                    "entity {entity:?} with `JustPressedTrigger<{0}>` is missing `ActionState<{0}>`",
273                    type_name::<A>()
274                )
275            })
276            .just_pressed(&action)
277    })
278    .into_trigger()
279}
280
281/// Trigger that transitions while pressing the given [`Actionlike`]
282pub fn pressed<A: Actionlike>(action: A) -> impl EntityTrigger<Out = bool> {
283    (move |In(entity): In<Entity>, actors: Query<&ActionState<A>>| {
284        actors
285            .get(entity)
286            .unwrap_or_else(|_| {
287                panic!(
288                    "entity {entity:?} with `JustPressedTrigger<{0}>` is missing `ActionState<{0}>`",
289                    type_name::<A>()
290                )
291            })
292            .pressed(&action)
293    })
294    .into_trigger()
295}
296
297/// Trigger that transitions upon releasing the given [`Actionlike`]
298pub fn just_released<A: Actionlike>(action: A) -> impl EntityTrigger<Out = bool> {
299    (move |In(entity): In<Entity>, actors: Query<&ActionState<A>>| {
300        actors
301            .get(entity)
302            .unwrap_or_else(|_| {
303                panic!(
304                    "entity {entity:?} with `JustPressedTrigger<{0}>` is missing `ActionState<{0}>`",
305                    type_name::<A>()
306                )
307            })
308            .just_released(&action)
309    })
310    .into_trigger()
311}
312
313/// Provides the given [`Actionlike`]'s [`ActionData`]
314pub fn action_data<A: Actionlike>(action: A) -> impl EntityTrigger<Out = Option<ActionData>> {
315    (move |In(entity): In<Entity>, actors: Query<&ActionState<A>>| {
316        actors
317            .get(entity)
318            .unwrap_or_else(|_| {
319                panic!(
320                    "entity {entity:?} with `ActionDataTrigger<{0}>` is missing `ActionState<{0}>`",
321                    type_name::<A>()
322                )
323            })
324            .action_data(&action)
325            .cloned()
326    })
327    .into_trigger()
328}