Skip to main content

aeon_fighter/
input.rs

1//! Per-tick input, a short ring buffer, and motion detection.
2//!
3//! Directions are numpad notation relative to the fighter's facing
4//! (6 = toward opponent, 4 = away). The client is responsible for
5//! converting raw keys / stick into a facing-relative [`InputFrame`].
6//!
7//! Buttons are the six of the law: P K S HS FL ST. Chords are adjacent
8//! pairs on the authorized stick layout (`P S HS` over `K FL ST`):
9//! P+K throw, S+FL Roman Cancel, S+HS EX, FL+ST feint, HS+ST overhead.
10
11pub const BUFFER_LEN: usize = 16;
12pub const MOTION_WINDOW: usize = 12;
13pub const HCB_WINDOW: usize = 16;
14pub const CHARGE_FRAMES: u16 = 45;
15pub const CHARGE_RELEASE_WINDOW: u8 = 5;
16/// A chord tolerates its two buttons landing this many frames apart.
17pub const CHORD_WINDOW: u8 = 3;
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
20pub enum Btn {
21    P,
22    K,
23    S,
24    HS,
25    FL,
26    ST,
27}
28
29impl Btn {
30    pub const ALL: [Btn; 6] = [Btn::P, Btn::K, Btn::S, Btn::HS, Btn::FL, Btn::ST];
31
32    pub fn label(self) -> &'static str {
33        match self {
34            Btn::P => "P",
35            Btn::K => "K",
36            Btn::S => "S",
37            Btn::HS => "HS",
38            Btn::FL => "FL",
39            Btn::ST => "ST",
40        }
41    }
42
43    /// Slash family carries specials, uppercuts, projectiles.
44    pub fn is_slash(self) -> bool {
45        matches!(self, Btn::S | Btn::HS)
46    }
47
48    /// Flash / Style family carries grabs, dashes, charge, overheads.
49    pub fn is_flash(self) -> bool {
50        matches!(self, Btn::FL | Btn::ST)
51    }
52}
53
54#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
55pub enum Chord {
56    /// P+K
57    Throw,
58    /// S+FL, 250 bar
59    RomanCancel,
60    /// S+HS, character gauge
61    Ex,
62    /// FL+ST
63    Feint,
64    /// HS+ST, universal standing overhead
65    Overhead,
66}
67
68impl Chord {
69    pub fn pair(self) -> (Btn, Btn) {
70        match self {
71            Chord::Throw => (Btn::P, Btn::K),
72            Chord::RomanCancel => (Btn::S, Btn::FL),
73            Chord::Ex => (Btn::S, Btn::HS),
74            Chord::Feint => (Btn::FL, Btn::ST),
75            Chord::Overhead => (Btn::HS, Btn::ST),
76        }
77    }
78
79    pub const ALL: [Chord; 5] = [
80        Chord::Throw,
81        Chord::RomanCancel,
82        Chord::Ex,
83        Chord::Feint,
84        Chord::Overhead,
85    ];
86}
87
88#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
89pub struct Buttons {
90    pub p: bool,
91    pub k: bool,
92    pub s: bool,
93    pub hs: bool,
94    pub fl: bool,
95    pub st: bool,
96}
97
98impl Buttons {
99    pub const NONE: Buttons = Buttons {
100        p: false,
101        k: false,
102        s: false,
103        hs: false,
104        fl: false,
105        st: false,
106    };
107
108    pub fn one(b: Btn) -> Self {
109        let mut out = Self::default();
110        out.set(b, true);
111        out
112    }
113
114    pub fn two(a: Btn, b: Btn) -> Self {
115        let mut out = Self::one(a);
116        out.set(b, true);
117        out
118    }
119
120    pub fn chord(c: Chord) -> Self {
121        let (a, b) = c.pair();
122        Self::two(a, b)
123    }
124
125    pub fn get(self, b: Btn) -> bool {
126        match b {
127            Btn::P => self.p,
128            Btn::K => self.k,
129            Btn::S => self.s,
130            Btn::HS => self.hs,
131            Btn::FL => self.fl,
132            Btn::ST => self.st,
133        }
134    }
135
136    pub fn set(&mut self, b: Btn, v: bool) {
137        match b {
138            Btn::P => self.p = v,
139            Btn::K => self.k = v,
140            Btn::S => self.s = v,
141            Btn::HS => self.hs = v,
142            Btn::FL => self.fl = v,
143            Btn::ST => self.st = v,
144        }
145    }
146
147    pub fn any(self) -> bool {
148        self.p || self.k || self.s || self.hs || self.fl || self.st
149    }
150
151    pub fn count(self) -> u8 {
152        Btn::ALL.iter().filter(|b| self.get(**b)).count() as u8
153    }
154
155    pub fn iter(self) -> impl Iterator<Item = Btn> {
156        Btn::ALL.into_iter().filter(move |b| self.get(*b))
157    }
158
159    /// The single button, if exactly one is set.
160    pub fn single(self) -> Option<Btn> {
161        let mut it = self.iter();
162        let first = it.next()?;
163        if it.next().is_some() {
164            None
165        } else {
166            Some(first)
167        }
168    }
169
170    pub fn has_chord(self, c: Chord) -> bool {
171        let (a, b) = c.pair();
172        self.get(a) && self.get(b)
173    }
174
175    pub fn just_pressed(self, prev: Self) -> Self {
176        Self {
177            p: self.p && !prev.p,
178            k: self.k && !prev.k,
179            s: self.s && !prev.s,
180            hs: self.hs && !prev.hs,
181            fl: self.fl && !prev.fl,
182            st: self.st && !prev.st,
183        }
184    }
185
186    pub fn or(self, other: Self) -> Self {
187        Self {
188            p: self.p || other.p,
189            k: self.k || other.k,
190            s: self.s || other.s,
191            hs: self.hs || other.hs,
192            fl: self.fl || other.fl,
193            st: self.st || other.st,
194        }
195    }
196}
197
198/// One tick of facing-relative input.
199/// `dir` is numpad 1..=9 (5 = neutral).
200#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
201pub struct InputFrame {
202    pub dir: u8,
203    pub buttons: Buttons,
204}
205
206impl Default for InputFrame {
207    fn default() -> Self {
208        Self {
209            dir: 5,
210            buttons: Buttons::default(),
211        }
212    }
213}
214
215impl InputFrame {
216    pub fn dir(d: u8) -> Self {
217        Self {
218            dir: d,
219            buttons: Buttons::default(),
220        }
221    }
222
223    pub fn press(b: Btn) -> Self {
224        Self {
225            dir: 5,
226            buttons: Buttons::one(b),
227        }
228    }
229
230    pub fn dir_press(d: u8, b: Btn) -> Self {
231        Self {
232            dir: d,
233            buttons: Buttons::one(b),
234        }
235    }
236
237    pub fn chord(c: Chord) -> Self {
238        Self {
239            dir: 5,
240            buttons: Buttons::chord(c),
241        }
242    }
243
244    pub fn down(self) -> bool {
245        matches!(self.dir, 1..=3)
246    }
247    pub fn up(self) -> bool {
248        matches!(self.dir, 7..=9)
249    }
250    pub fn forward(self) -> bool {
251        matches!(self.dir, 3 | 6 | 9)
252    }
253    pub fn back(self) -> bool {
254        matches!(self.dir, 1 | 4 | 7)
255    }
256    pub fn down_back(self) -> bool {
257        self.dir == 1
258    }
259}
260
261/// Convert a world-space stick (x: -1/0/1, y: -1/0/1, y+ = up) into a
262/// facing-relative numpad direction.
263pub fn stick_to_dir(sx: i32, sy: i32, facing_right: bool) -> u8 {
264    let fx = if facing_right { sx } else { -sx };
265    match (fx, sy) {
266        (-1, -1) => 1,
267        (0, -1) => 2,
268        (1, -1) => 3,
269        (-1, 0) => 4,
270        (0, 0) => 5,
271        (1, 0) => 6,
272        (-1, 1) => 7,
273        (0, 1) => 8,
274        (1, 1) => 9,
275        _ => 5,
276    }
277}
278
279#[derive(Clone, Debug, PartialEq, Eq, Hash)]
280pub struct InputBuffer {
281    frames: [InputFrame; BUFFER_LEN],
282    head: usize,
283    filled: usize,
284    back_charge: u16,
285    down_charge: u16,
286    back_release: u8,
287    down_release: u8,
288    /// Frames since each button was pressed (edge), saturating. Used for
289    /// the chord tolerance window.
290    press_age: [u8; 6],
291}
292
293impl Default for InputBuffer {
294    fn default() -> Self {
295        Self {
296            frames: [InputFrame::default(); BUFFER_LEN],
297            head: 0,
298            filled: 0,
299            back_charge: 0,
300            down_charge: 0,
301            back_release: 0,
302            down_release: 0,
303            press_age: [u8::MAX; 6],
304        }
305    }
306}
307
308impl InputBuffer {
309    pub fn push(&mut self, frame: InputFrame) {
310        let prev = self.latest();
311        let was_back = prev.back();
312        let was_down = prev.down();
313
314        if frame.back() {
315            self.back_charge = self.back_charge.saturating_add(1);
316            self.back_release = 0;
317        } else {
318            if was_back && self.back_charge >= CHARGE_FRAMES {
319                self.back_release = CHARGE_RELEASE_WINDOW;
320            } else {
321                self.back_release = self.back_release.saturating_sub(1);
322            }
323            self.back_charge = 0;
324        }
325
326        if frame.down() {
327            self.down_charge = self.down_charge.saturating_add(1);
328            self.down_release = 0;
329        } else {
330            if was_down && self.down_charge >= CHARGE_FRAMES {
331                self.down_release = CHARGE_RELEASE_WINDOW;
332            } else {
333                self.down_release = self.down_release.saturating_sub(1);
334            }
335            self.down_charge = 0;
336        }
337
338        let edges = frame.buttons.just_pressed(prev.buttons);
339        for (i, b) in Btn::ALL.iter().enumerate() {
340            if edges.get(*b) {
341                self.press_age[i] = 0;
342            } else {
343                self.press_age[i] = self.press_age[i].saturating_add(1);
344            }
345        }
346
347        self.frames[self.head] = frame;
348        self.head = (self.head + 1) % BUFFER_LEN;
349        self.filled = (self.filled + 1).min(BUFFER_LEN);
350    }
351
352    pub fn latest(&self) -> InputFrame {
353        if self.filled == 0 {
354            return InputFrame::default();
355        }
356        let i = (self.head + BUFFER_LEN - 1) % BUFFER_LEN;
357        self.frames[i]
358    }
359
360    pub fn prev(&self) -> InputFrame {
361        if self.filled < 2 {
362            return InputFrame::default();
363        }
364        let i = (self.head + BUFFER_LEN - 2) % BUFFER_LEN;
365        self.frames[i]
366    }
367
368    /// Buttons whose press edge happened this frame.
369    pub fn pressed(&self) -> Buttons {
370        self.latest().buttons.just_pressed(self.prev().buttons)
371    }
372
373    /// Buttons pressed within the last `n` frames (inclusive of this one)
374    /// and still held.
375    pub fn pressed_within(&self, n: u8) -> Buttons {
376        let held = self.latest().buttons;
377        let mut out = Buttons::default();
378        for (i, b) in Btn::ALL.iter().enumerate() {
379            if held.get(*b) && self.press_age[i] < n {
380                out.set(*b, true);
381            }
382        }
383        out
384    }
385
386    /// Buttons whose press edge happened within the last `n` frames, held or
387    /// not. Used for the throw-tech window.
388    pub fn pressed_recently(&self, n: u8) -> Buttons {
389        let mut out = Buttons::default();
390        for (i, b) in Btn::ALL.iter().enumerate() {
391            if self.press_age[i] < n {
392                out.set(*b, true);
393            }
394        }
395        out
396    }
397
398    /// A chord fires the frame its second button lands, provided the first
399    /// landed within [`CHORD_WINDOW`]. Returns at most one chord; if two
400    /// are simultaneously completable the law's priority is RC, EX, feint,
401    /// overhead, throw.
402    pub fn chord(&self) -> Option<Chord> {
403        let edge = self.pressed();
404        if !edge.any() {
405            return None;
406        }
407        let recent = self.pressed_within(CHORD_WINDOW);
408        for c in [
409            Chord::RomanCancel,
410            Chord::Ex,
411            Chord::Feint,
412            Chord::Overhead,
413            Chord::Throw,
414        ] {
415            let (a, b) = c.pair();
416            let completes_now = (edge.get(a) && recent.get(b)) || (edge.get(b) && recent.get(a));
417            if completes_now {
418                return Some(c);
419            }
420        }
421        None
422    }
423
424    /// Iterate from oldest-in-window to newest.
425    pub fn window(&self, n: usize) -> impl Iterator<Item = InputFrame> + '_ {
426        let n = n.min(self.filled);
427        (0..n).map(move |k| {
428            let i = (self.head + BUFFER_LEN - n + k) % BUFFER_LEN;
429            self.frames[i]
430        })
431    }
432
433    pub fn motion(&self, motion: Motion) -> bool {
434        match motion {
435            Motion::None => true,
436            Motion::Qcf => self.sequence(&[2, 3, 6], MOTION_WINDOW),
437            Motion::Qcb => self.sequence(&[2, 1, 4], MOTION_WINDOW),
438            Motion::Dp => self.sequence(&[6, 2, 3], MOTION_WINDOW),
439            Motion::Hcb => self.sequence(&[6, 2, 4], HCB_WINDOW) && self.hcb_passes_down(),
440            Motion::SuperQcf => self.sequence(&[2, 3, 6, 2, 3, 6], BUFFER_LEN),
441            Motion::ForwardDash => self.dash(6),
442            Motion::BackDash => self.dash(4),
443            Motion::ChargeBackForward => self.back_release > 0 && self.latest().forward(),
444            Motion::ChargeDownUp => self.down_release > 0 && self.latest().up(),
445        }
446    }
447
448    pub fn back_charge(&self) -> u16 {
449        self.back_charge
450    }
451
452    pub fn down_charge(&self) -> u16 {
453        self.down_charge
454    }
455
456    /// 63214 must actually pass through 3 or 1 as well as 2, so a sloppy
457    /// 6-2-4 (down, then back) still reads but a 6-4 does not.
458    fn hcb_passes_down(&self) -> bool {
459        self.window(HCB_WINDOW).any(|f| matches!(f.dir, 1..=3))
460    }
461
462    /// Loose sequential match: each required direction (or a diagonal that
463    /// contains it) must appear in order within the window.
464    fn sequence(&self, parts: &[u8], window: usize) -> bool {
465        let frames: Vec<u8> = self.window(window).map(|f| f.dir).collect();
466        let mut at = 0;
467        for &need in parts {
468            let mut found = false;
469            while at < frames.len() {
470                if dir_matches(frames[at], need) {
471                    found = true;
472                    at += 1;
473                    break;
474                }
475                at += 1;
476            }
477            if !found {
478                return false;
479            }
480        }
481        true
482    }
483
484    /// Two taps of `dir` (6 or 4) within 10f, released to neutral, up, or the
485    /// opposite side in between. Passing through down does not count as a
486    /// release, so 214 214 does not backdash and 236 236 does not run.
487    fn dash(&self, dir: u8) -> bool {
488        let opposite = if dir == 6 { 4 } else { 6 };
489        let frames: Vec<InputFrame> = self.window(10).collect();
490        let mut taps = 0;
491        let mut released = true;
492        for f in frames {
493            let match_dir = dir_matches(f.dir, dir);
494            if match_dir && released {
495                taps += 1;
496                released = false;
497            } else if f.dir == 5 || f.dir == 8 || dir_matches(f.dir, opposite) {
498                released = true;
499            }
500        }
501        taps >= 2
502    }
503}
504
505fn dir_matches(got: u8, need: u8) -> bool {
506    if got == need {
507        return true;
508    }
509    match need {
510        2 => matches!(got, 1..=3),
511        4 => matches!(got, 1 | 4 | 7),
512        6 => matches!(got, 3 | 6 | 9),
513        8 => matches!(got, 7..=9),
514        _ => false,
515    }
516}
517
518#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
519pub enum Motion {
520    /// Plain button (used for chord-only routes such as the overhead).
521    None,
522    Qcf,      // 236
523    Qcb,      // 214
524    Dp,       // 623
525    Hcb,      // 63214
526    SuperQcf, // 236236
527    ForwardDash,
528    BackDash,
529    ChargeBackForward,
530    ChargeDownUp,
531}
532
533impl Motion {
534    /// Longer motions win when two routes are simultaneously satisfied so
535    /// that 236236+S doesn't degrade to 236+S and 63214+FL to 214+FL.
536    pub fn rank(self) -> u8 {
537        match self {
538            Motion::SuperQcf => 4,
539            Motion::Hcb => 3,
540            Motion::Dp | Motion::ChargeBackForward | Motion::ChargeDownUp => 2,
541            Motion::Qcf | Motion::Qcb | Motion::ForwardDash | Motion::BackDash => 1,
542            Motion::None => 0,
543        }
544    }
545
546    pub fn notation(self) -> &'static str {
547        match self {
548            Motion::None => "",
549            Motion::Qcf => "236",
550            Motion::Qcb => "214",
551            Motion::Dp => "623",
552            Motion::Hcb => "63214",
553            Motion::SuperQcf => "236236",
554            Motion::ForwardDash => "66",
555            Motion::BackDash => "44",
556            Motion::ChargeBackForward => "[4]6",
557            Motion::ChargeDownUp => "[2]8",
558        }
559    }
560}
561
562#[cfg(test)]
563mod tests {
564    use super::*;
565
566    fn dir_only(d: u8) -> InputFrame {
567        InputFrame::dir(d)
568    }
569
570    #[test]
571    fn qcf_reads_236() {
572        let mut b = InputBuffer::default();
573        for d in [5, 2, 3, 6, 5] {
574            b.push(dir_only(d));
575        }
576        assert!(b.motion(Motion::Qcf));
577        assert!(!b.motion(Motion::Dp));
578    }
579
580    #[test]
581    fn qcf_accepts_diagonals() {
582        let mut b = InputBuffer::default();
583        for d in [2, 3, 6] {
584            b.push(dir_only(d));
585        }
586        assert!(b.motion(Motion::Qcf));
587    }
588
589    #[test]
590    fn hcb_reads_63214_and_not_64() {
591        let mut b = InputBuffer::default();
592        for d in [6, 3, 2, 1, 4] {
593            b.push(dir_only(d));
594        }
595        assert!(b.motion(Motion::Hcb));
596        let mut b = InputBuffer::default();
597        for d in [6, 5, 4] {
598            b.push(dir_only(d));
599        }
600        assert!(!b.motion(Motion::Hcb));
601    }
602
603    #[test]
604    fn charge_release_is_first_class() {
605        let mut b = InputBuffer::default();
606        for _ in 0..CHARGE_FRAMES {
607            b.push(dir_only(4));
608        }
609        assert_eq!(b.back_charge(), CHARGE_FRAMES);
610        b.push(dir_only(6));
611        assert!(b.motion(Motion::ChargeBackForward));
612    }
613
614    #[test]
615    fn short_charge_does_not_release() {
616        let mut b = InputBuffer::default();
617        for _ in 0..(CHARGE_FRAMES - 1) {
618            b.push(dir_only(4));
619        }
620        b.push(dir_only(6));
621        assert!(!b.motion(Motion::ChargeBackForward));
622    }
623
624    #[test]
625    fn chord_same_frame_and_within_window() {
626        let mut b = InputBuffer::default();
627        b.push(InputFrame::chord(Chord::RomanCancel));
628        assert_eq!(b.chord(), Some(Chord::RomanCancel));
629        b.push(InputFrame::chord(Chord::RomanCancel));
630        assert_eq!(b.chord(), None, "held chord does not re-fire");
631
632        let mut b = InputBuffer::default();
633        b.push(InputFrame::press(Btn::FL));
634        b.push(InputFrame::press(Btn::FL));
635        b.push(InputFrame {
636            dir: 5,
637            buttons: Buttons::two(Btn::FL, Btn::ST),
638        });
639        assert_eq!(b.chord(), Some(Chord::Feint));
640
641        let mut b = InputBuffer::default();
642        for _ in 0..CHORD_WINDOW + 1 {
643            b.push(InputFrame::press(Btn::HS));
644        }
645        b.push(InputFrame {
646            dir: 5,
647            buttons: Buttons::two(Btn::HS, Btn::ST),
648        });
649        assert_eq!(b.chord(), None, "too late to be a chord");
650    }
651}