Skip to main content

game_toolkit_platformer/
controls.rs

1//! [`Controls`]: game-toolkit's keyboard and gamepad input to an [`Intent`], through a
2//! [`Scheme`] of keys and buttons.
3//!
4//! Each feel preset comes with a control scheme ([`Scheme::of`]); any other [`Scheme`] can
5//! be built field by field. Movement is always the arrows, the left stick and the d-pad.
6//!
7//! | Action | `SNAPPY` (hybrid, snappy, drifter) | `PRECISE` | `MOMENTUM` |
8//! |---|---|---|---|
9//! | jump | Z, Space / A | C, Space / A, Y | Space, Z / A |
10//! | dash | C / RT | X / X, B | - |
11//! | super dash (hold) | S / LT | - | - |
12//! | grab (hold) | D / LB | Z / RT, LT, RB, LB | - |
13//! | dive | down + A / down + B, down + RB | - | - |
14//! | sprint (hold) | Shift / X | - | Shift, X / X, RT |
15//!
16//! The layouts follow the default controls of the games the presets are tuned toward:
17//! `SNAPPY` Hollow Knight's (dash on RT, the charged dash on LT, the dive as down + cast),
18//! with grab and sprint added where it has none; `PRECISE` Celeste's; `MOMENTUM` Super Meat
19//! Boy's, with sprint as its run button.
20//!
21//! Triggers count when the pad reports them as buttons or only as an analog axis (Z).
22//!
23//! Your game's own buttons (attack, interact, ...) go in [`Scheme::custom`]; they arrive as
24//! bits in [`Intent::custom`] and [`Intent::custom_pressed`].
25
26use std::borrow::Cow;
27
28use game_toolkit::prelude::*;
29
30use crate::sim::{Intent, Preset};
31
32/// Stick values below this are ignored.
33const DEADZONE: f32 = 0.25;
34
35/// Which keys and buttons do what. An action with no keys and no buttons can't be done.
36#[derive(Clone, Debug)]
37pub struct Scheme {
38    /// Jump keys.
39    pub jump: &'static [Key],
40    /// Jump buttons.
41    pub jump_pad: &'static [Button],
42    /// Dash keys.
43    pub dash: &'static [Key],
44    /// Dash buttons.
45    pub dash_pad: &'static [Button],
46    /// Grab keys (held).
47    pub grab: &'static [Key],
48    /// Grab buttons (held).
49    pub grab_pad: &'static [Button],
50    /// Cast keys: with down held, a dive.
51    pub cast: &'static [Key],
52    /// Cast buttons.
53    pub cast_pad: &'static [Button],
54    /// Sprint keys (held).
55    pub sprint: &'static [Key],
56    /// Sprint buttons (held).
57    pub sprint_pad: &'static [Button],
58    /// Super dash keys (held).
59    pub super_dash: &'static [Key],
60    /// Super dash buttons (held).
61    pub super_dash_pad: &'static [Button],
62    /// Your game's own buttons, read into [`Intent::custom`] and
63    /// [`Intent::custom_pressed`]. The built-in schemes have none; add yours with
64    /// `Scheme { custom: Cow::Borrowed(&[...]), ..Scheme::SNAPPY }`, or at runtime (a
65    /// key-rebinding menu) with [`Scheme::bind`].
66    pub custom: Cow<'static, [Custom]>,
67}
68
69/// A button of your game's own (attack, interact, ...): which [`Intent`] bit, and the keys
70/// and gamepad buttons that set it. Fixed ones can be `const`; rebound ones are built at
71/// runtime ([`Scheme::bind`]).
72///
73/// ```
74/// use std::borrow::Cow;
75/// use game_toolkit::prelude::{Button, Key};
76/// use game_toolkit_platformer::controls::{Custom, Scheme};
77///
78/// const ATTACK: u8 = 0;
79/// const SCHEME: Scheme = Scheme {
80///     custom: Cow::Borrowed(&[Custom::new(ATTACK, &[Key::KeyX], &[Button::West])]),
81///     ..Scheme::SNAPPY
82/// };
83/// assert_eq!(SCHEME.custom.len(), 1);
84///
85/// // A rebinding menu: attack on V from now on, the pad as it was.
86/// let mut scheme = SCHEME;
87/// scheme.bind(ATTACK, vec![Key::KeyV], &[Button::West][..]);
88/// assert_eq!(scheme.custom[0].keys[..], [Key::KeyV]);
89/// scheme.bind(1, &[Key::KeyB][..], vec![]); // a second button, added
90/// assert_eq!(scheme.custom.len(), 2);
91/// ```
92#[derive(Clone, Debug, PartialEq)]
93pub struct Custom {
94    /// The bit in [`Intent::custom`] and [`Intent::custom_pressed`], 0 to 31.
95    pub bit: u8,
96    /// Keys.
97    pub keys: Cow<'static, [Key]>,
98    /// Gamepad buttons.
99    pub pad: Cow<'static, [Button]>,
100}
101
102impl Custom {
103    /// Button `bit` on `keys` and `pad`, fixed (usable in a `const`).
104    pub const fn new(bit: u8, keys: &'static [Key], pad: &'static [Button]) -> Self {
105        Self {
106            bit,
107            keys: Cow::Borrowed(keys),
108            pad: Cow::Borrowed(pad),
109        }
110    }
111}
112
113// gilrs names: RightTrigger2 / LeftTrigger2 are the triggers, RightTrigger / LeftTrigger
114// the bumpers; South, East, West, North are A, B, X, Y.
115
116impl Scheme {
117    /// After Hollow Knight's default layout, plus grab and sprint. The hybrid, snappy and
118    /// drifter presets' scheme.
119    pub const SNAPPY: Scheme = Scheme {
120        jump: &[Key::KeyZ, Key::Space],
121        jump_pad: &[Button::South],
122        dash: &[Key::KeyC],
123        dash_pad: &[Button::RightTrigger2],
124        grab: &[Key::KeyD],
125        grab_pad: &[Button::LeftTrigger],
126        cast: &[Key::KeyA],
127        cast_pad: &[Button::East, Button::RightTrigger],
128        sprint: &[Key::ShiftLeft, Key::ShiftRight],
129        sprint_pad: &[Button::West],
130        super_dash: &[Key::KeyS],
131        super_dash_pad: &[Button::LeftTrigger2],
132        custom: Cow::Borrowed(&[]),
133    };
134
135    /// After Celeste's default layout. The precise preset's scheme.
136    pub const PRECISE: Scheme = Scheme {
137        jump: &[Key::KeyC, Key::Space],
138        jump_pad: &[Button::South, Button::North],
139        dash: &[Key::KeyX],
140        dash_pad: &[Button::West, Button::East],
141        grab: &[Key::KeyZ],
142        grab_pad: &[
143            Button::RightTrigger2,
144            Button::LeftTrigger2,
145            Button::RightTrigger,
146            Button::LeftTrigger,
147        ],
148        cast: &[],
149        cast_pad: &[],
150        sprint: &[],
151        sprint_pad: &[],
152        super_dash: &[],
153        super_dash_pad: &[],
154        custom: Cow::Borrowed(&[]),
155    };
156
157    /// After Super Meat Boy's: jump, and hold to run. The momentum preset's scheme.
158    pub const MOMENTUM: Scheme = Scheme {
159        jump: &[Key::Space, Key::KeyZ],
160        jump_pad: &[Button::South],
161        dash: &[],
162        dash_pad: &[],
163        grab: &[],
164        grab_pad: &[],
165        cast: &[],
166        cast_pad: &[],
167        sprint: &[Key::ShiftLeft, Key::ShiftRight, Key::KeyX],
168        sprint_pad: &[Button::West, Button::RightTrigger2],
169        super_dash: &[],
170        super_dash_pad: &[],
171        custom: Cow::Borrowed(&[]),
172    };
173
174    /// Bind custom button `bit` to `keys` and `pad` from now on (a key-rebinding menu),
175    /// replacing what it was bound to, or adding it if it wasn't.
176    pub fn bind(
177        &mut self,
178        bit: u8,
179        keys: impl Into<Cow<'static, [Key]>>,
180        pad: impl Into<Cow<'static, [Button]>>,
181    ) {
182        let button = Custom {
183            bit,
184            keys: keys.into(),
185            pad: pad.into(),
186        };
187        let custom = self.custom.to_mut();
188        match custom.iter_mut().find(|c| c.bit == bit) {
189            Some(c) => *c = button,
190            None => custom.push(button),
191        }
192    }
193
194    /// The control scheme that goes with a feel preset (`None`: Hollow Knight's).
195    pub fn of(preset: Option<Preset>) -> Self {
196        match preset {
197            Some(Preset::Precise) => Self::PRECISE,
198            Some(Preset::Momentum) => Self::MOMENTUM,
199            Some(Preset::Hybrid | Preset::Snappy | Preset::Drifter) | None => Self::SNAPPY,
200        }
201    }
202}
203
204/// Trigger state from the last frame, to see presses of triggers that only report an
205/// analog axis.
206#[derive(Clone, Copy, Debug, Default)]
207pub struct Triggers {
208    /// Left trigger was down.
209    left: bool,
210    /// Right trigger was down.
211    right: bool,
212}
213
214/// A trigger counts as down past this.
215const TRIGGER: f32 = 0.5;
216
217/// Read this frame's intent from the keyboard and the first gamepad, through `scheme`.
218pub fn read_intent(input: &Input, scheme: &Scheme, triggers: &mut Triggers) -> Intent {
219    let pressed = |ks: &[Key]| ks.iter().any(|&k| input.key_pressed(k));
220    let held = |ks: &[Key]| ks.iter().any(|&k| input.key_held(k));
221    let axis =
222        |neg: Key, pos: Key| (input.key_held(pos) as i32 - input.key_held(neg) as i32) as f32;
223    let mut i = Intent {
224        move_x: axis(Key::ArrowLeft, Key::ArrowRight),
225        move_y: axis(Key::ArrowUp, Key::ArrowDown),
226        jump_pressed: pressed(scheme.jump),
227        jump_held: held(scheme.jump),
228        dash_pressed: pressed(scheme.dash),
229        grab: held(scheme.grab),
230        dive_pressed: false,
231        sprint: held(scheme.sprint),
232        super_dash: held(scheme.super_dash),
233        custom: 0,
234        custom_pressed: 0,
235    };
236    for c in scheme.custom.iter() {
237        let bit = 1 << c.bit;
238        if held(&c.keys) {
239            i.custom |= bit;
240        }
241        if pressed(&c.keys) {
242            i.custom_pressed |= bit;
243        }
244    }
245    let mut cast = pressed(scheme.cast);
246    if let Some(g) = input.first_gamepad() {
247        // Triggers: as buttons, or as the Z axes some pads report instead.
248        let lt = g.button_held(Button::LeftTrigger2) || g.axis(Axis::LeftZ) > TRIGGER;
249        let rt = g.button_held(Button::RightTrigger2) || g.axis(Axis::RightZ) > TRIGGER;
250        let (lt_pressed, rt_pressed) = (lt && !triggers.left, rt && !triggers.right);
251        *triggers = Triggers {
252            left: lt,
253            right: rt,
254        };
255        let held_b = |b: Button| match b {
256            Button::LeftTrigger2 => lt,
257            Button::RightTrigger2 => rt,
258            _ => g.button_held(b),
259        };
260        let pressed_b = |b: Button| match b {
261            Button::LeftTrigger2 => lt_pressed,
262            Button::RightTrigger2 => rt_pressed,
263            _ => g.button_pressed(b),
264        };
265        let pressed = |bs: &[Button]| bs.iter().any(|&b| pressed_b(b));
266        let held = |bs: &[Button]| bs.iter().any(|&b| held_b(b));
267        let dz = |v: f32| if v.abs() < DEADZONE { 0.0 } else { v };
268        let dpad = |neg, pos| (g.button_held(pos) as i32 - g.button_held(neg) as i32) as f32;
269        let sx = dz(g.axis(Axis::LeftStickX)) + dpad(Button::DPadLeft, Button::DPadRight);
270        // gilrs: stick up is positive; the sim's y points down.
271        let sy = -dz(g.axis(Axis::LeftStickY)) + dpad(Button::DPadUp, Button::DPadDown);
272        if sx != 0.0 {
273            i.move_x = sx.clamp(-1.0, 1.0);
274        }
275        if sy != 0.0 {
276            i.move_y = sy.clamp(-1.0, 1.0);
277        }
278        i.jump_pressed |= pressed(scheme.jump_pad);
279        i.jump_held |= held(scheme.jump_pad);
280        i.dash_pressed |= pressed(scheme.dash_pad);
281        i.grab |= held(scheme.grab_pad);
282        i.sprint |= held(scheme.sprint_pad);
283        i.super_dash |= held(scheme.super_dash_pad);
284        cast |= pressed(scheme.cast_pad);
285        for c in scheme.custom.iter() {
286            let bit = 1 << c.bit;
287            if held(&c.pad) {
288                i.custom |= bit;
289            }
290            if pressed(&c.pad) {
291                i.custom_pressed |= bit;
292            }
293        }
294    }
295    i.dive_pressed = cast && i.down();
296    i
297}
298
299/// Reads a scheme's keys and buttons into an [`Intent`] each frame (remembering the
300/// gamepad triggers, which some pads only report as an axis).
301#[derive(Clone, Debug)]
302pub struct Controls {
303    /// Which keys and buttons do what.
304    pub scheme: Scheme,
305    /// The triggers last frame.
306    triggers: Triggers,
307}
308
309impl Controls {
310    /// Controls with `scheme`.
311    pub fn new(scheme: Scheme) -> Self {
312        Self {
313            scheme,
314            triggers: Triggers::default(),
315        }
316    }
317
318    /// The controls of `preset`'s game.
319    pub fn for_preset(preset: Preset) -> Self {
320        Self::new(Scheme::of(Some(preset)))
321    }
322
323    /// This frame's intent.
324    pub fn read(&mut self, input: &Input) -> Intent {
325        read_intent(input, &self.scheme, &mut self.triggers)
326    }
327}
328
329impl Default for Controls {
330    fn default() -> Self {
331        Self::new(Scheme::SNAPPY)
332    }
333}