1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
use super::time_helper::*;
use std::time::{Duration, Instant};

/// By default, the stickiness is set to 1/10th of a second.
/// This means that if player pushes a jump control less than
/// 1/10th of a second before it can actually jump, the jump
/// is still recorded as valid.
pub const DEFAULT_STICKINESS_SECS_F64: f64 = 0.1;

/// Used to handle jump input. This is to encapsulate all
/// the nitty-picky logic about considering a button is still
/// pressed a fraction of seconds after it has been pressed, etc.
/// It does not actually poll and/or listens to the real hardware,
/// it just keeps track of what's happening with push/pop operations.
#[derive(Debug)]
pub struct InputJump {
    /// How long a button should be considered pressed after
    /// it has actually been pressed. This is used to work
    /// around the fact that you might require a jump
    /// just before you actually land.
    pub stickiness: Option<Duration>,

    first_pop: bool,
    last_request: Option<Instant>,
}

impl InputJump {
    /// Create a new input jump manager.
    ///
    /// # Examples
    ///
    /// ```
    /// use babalcore::*;
    ///
    /// let _input_jump = InputJump::new();
    /// ```
    pub fn new() -> InputJump {
        let mut ret = InputJump {
            stickiness: None,
            first_pop: false,
            last_request: None,
        };
        ret.set_stickiness_secs_f64(Some(DEFAULT_STICKINESS_SECS_F64));
        ret
    }

    /// Set the stickiness value, in seconds.
    ///
    /// # Examples
    ///
    /// ```
    /// use babalcore::*;
    ///
    /// let mut input_jump = InputJump::new();
    /// input_jump.set_stickiness_secs_f64(Some(0.1));
    /// ```
    pub fn set_stickiness_secs_f64(&mut self, stickiness_secs_f64: Option<f64>) {
        self.stickiness = match stickiness_secs_f64 {
            Some(m) => {
                if m > 0.0 {
                    Some(Duration::from_secs_f64(m))
                } else {
                    None
                }
            }
            None => None,
        };
    }

    /// Push a jump request. It is possible to omit the current
    /// now timestamp, or you may provide your own. If omitted,
    /// the default is the current instant (AKA now). Pushing
    /// several jump requests will not queue them, only the
    /// last one is registered.
    ///
    /// # Examples
    ///
    /// ```
    /// use babalcore::*;
    ///
    /// let mut input_jump = InputJump::new();
    /// // Player pressed the jump key, clicked on mouse
    /// input_jump.push_jump(None);
    /// ```
    pub fn push_jump(&mut self, now: Option<Instant>) {
        let now = unwrap_now(now);
        self.last_request = Some(match self.last_request {
            Some(last_request) => {
                if last_request < now {
                    now
                } else {
                    last_request
                }
            }
            None => now,
        });
        self.last_request = Some(now);
        self.first_pop = true;
    }

    /// Pop a jump request. It is possible to omit the current
    /// now timestamp, or you may provide your own. If omitted,
    /// the default is the current instant (AKA now). Once a
    /// jump request is popped, it is cleared from the object.
    /// However, if stickiness is set, it will continue to
    /// return true for some time, defined by stickiness.
    /// This is to avoid the following "bug": a player would
    /// press jump just before it can actually jump (eg: the
    /// ball did not land yet). But a fraction of a second later
    /// it would be OK to jump... You'd want it to jump now.
    /// To work around this, pop will return true for some time,
    /// typically 1/10th of a second.
    ///
    /// # Examples
    ///
    /// ```
    /// use babalcore::*;
    ///
    /// let mut input_jump = InputJump::new();
    /// assert!(!input_jump.pop_jump(None));
    /// input_jump.push_jump(None);
    /// assert!(input_jump.pop_jump(None));
    /// ```
    pub fn pop_jump(&mut self, now: Option<Instant>) -> bool {
        match self.last_request {
            Some(last_request) => {
                let state = match self.stickiness {
                    Some(stickiness) => {
                        let now = unwrap_now(now);
                        if now >= last_request {
                            let delay: Duration = now - last_request;
                            delay <= stickiness
                        } else {
                            false
                        }
                    }
                    None => false,
                };
                if !state {
                    self.last_request = None
                }
                if self.first_pop {
                    self.first_pop = false;
                    true
                } else {
                    state
                }
            }
            None => false,
        }
    }
}

impl std::default::Default for InputJump {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use crate::*;
    use std::time::{Duration, Instant};

    #[test]
    fn test_default() {
        let input_jump = InputJump::default();
        assert_eq!(
            Some(Duration::from_secs_f64(DEFAULT_STICKINESS_SECS_F64)),
            input_jump.stickiness
        );
    }

    #[test]
    fn test_set_stickiness_secs_f64() {
        let mut input_jump = InputJump::new();
        assert_eq!(
            Some(Duration::from_secs_f64(DEFAULT_STICKINESS_SECS_F64)),
            input_jump.stickiness
        );
        input_jump.set_stickiness_secs_f64(None);
        assert_eq!(None, input_jump.stickiness);
        input_jump.set_stickiness_secs_f64(Some(0.5));
        assert_eq!(Some(Duration::from_secs_f64(0.5)), input_jump.stickiness);
    }

    #[test]
    fn test_push_pop_without_stickiness() {
        let mut input_jump = InputJump::new();
        let now = Instant::now();
        input_jump.set_stickiness_secs_f64(None);
        assert!(!input_jump.pop_jump(Some(now)));
        input_jump.push_jump(Some(now));
        assert!(input_jump.pop_jump(Some(now)));
        assert!(!input_jump.pop_jump(Some(now)));
        input_jump.push_jump(Some(now));
        input_jump.push_jump(Some(now));
        input_jump.push_jump(Some(now));
        assert!(input_jump.pop_jump(Some(now)));
        assert!(!input_jump.pop_jump(Some(now)));
    }

    #[test]
    fn test_push_pop_with_stickiness() {
        let mut input_jump = InputJump::new();
        let now = Instant::now();
        let short_enough = Duration::from_secs_f64(DEFAULT_STICKINESS_SECS_F64 - 0.001);
        let too_long = Duration::from_secs_f64(DEFAULT_STICKINESS_SECS_F64 + 0.001);
        assert!(!input_jump.pop_jump(Some(now)));
        input_jump.push_jump(Some(now));
        assert!(input_jump.pop_jump(Some(now)));
        assert!(input_jump.pop_jump(Some(now)));
        assert!(input_jump.pop_jump(Some(now + short_enough)));
        assert!(input_jump.pop_jump(Some(now + short_enough)));
        assert!(!input_jump.pop_jump(Some(now + too_long)));
        assert!(!input_jump.pop_jump(Some(now + short_enough)));
        assert!(!input_jump.pop_jump(Some(now)));
    }

    #[test]
    fn test_push_pop_rearm() {
        let mut input_jump = InputJump::new();
        let now = Instant::now();
        let extra_delay = Duration::from_secs_f64(DEFAULT_STICKINESS_SECS_F64 * 3.0);
        assert!(!input_jump.pop_jump(Some(now)));
        input_jump.push_jump(Some(now));
        input_jump.push_jump(Some(now + extra_delay));
        input_jump.push_jump(Some(now));
        assert!(input_jump.pop_jump(Some(now + extra_delay)));
    }
}