babalcore 0.5.1

Babal core logic library, low-level things which are game-engine agnostic.
Documentation
use super::time_helper::*;
use std::time::{Duration, Instant};

/// By default, the stickiness is set to a bit more than 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.16;

/// 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)));
    }
}