mecha10-behavior-runtime 0.7.0

Behavior tree runtime for Mecha10 - unified AI and logic composition system
Documentation
//! Shared helper for leaf action nodes that gate on wall-clock time elapsed since their
//! first tick after construction/reset.
//!
//! `TimerNode` and `PauseNode` (`timing.rs`) and `MoveNode` (`movement.rs`) all start a clock
//! on first tick, compare elapsed time against a configured duration, and clear the clock on
//! reset - cargo-dupes flagged their near-identical `tick`/`reset` bodies as duplicates of each
//! other. Centralizing the clock logic here, plus the `reset_timer_field!` macro below for the
//! one-line `reset_self` override every one of them needs, removes the duplication instead of
//! just relocating it.

use std::time::{Duration, Instant};

#[derive(Debug, Default)]
pub(crate) struct ElapsedTimer {
    start_time: Option<Instant>,
}

impl ElapsedTimer {
    /// Returns `true` if the clock has already been started (i.e. `elapsed_since_start` has
    /// been called at least once since construction/reset).
    pub(crate) fn is_running(&self) -> bool {
        self.start_time.is_some()
    }

    /// Returns `true` once `duration` has elapsed since the first call after
    /// construction/reset; starts the clock on the first call.
    pub(crate) fn elapsed_since_start(&mut self, duration: Duration) -> bool {
        let now = Instant::now();
        let start = *self.start_time.get_or_insert(now);
        now.duration_since(start) >= duration
    }

    /// Clear the tracked start time, so the next `elapsed_since_start` call restarts the clock.
    pub(crate) fn reset(&mut self) {
        self.start_time = None;
    }
}

macro_rules! reset_timer_field {
    ($field:ident) => {
        fn reset_self(&mut self) {
            self.$field.reset();
        }
    };
}

pub(crate) use reset_timer_field;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn new_timer_is_not_running() {
        let timer = ElapsedTimer::default();
        assert!(!timer.is_running());
    }

    #[test]
    fn elapsed_since_start_starts_the_clock_on_first_call() {
        let mut timer = ElapsedTimer::default();
        assert!(!timer.elapsed_since_start(Duration::from_secs(60)));
        assert!(timer.is_running());
    }

    #[test]
    fn elapsed_since_start_reports_elapsed_once_duration_is_zero() {
        let mut timer = ElapsedTimer::default();
        assert!(timer.elapsed_since_start(Duration::ZERO));
    }

    #[test]
    fn elapsed_since_start_keeps_the_original_start_time_across_calls() {
        let mut timer = ElapsedTimer::default();
        assert!(!timer.elapsed_since_start(Duration::from_millis(50)));
        std::thread::sleep(Duration::from_millis(75));
        assert!(timer.elapsed_since_start(Duration::from_millis(50)));
    }

    #[test]
    fn reset_clears_the_clock() {
        let mut timer = ElapsedTimer::default();
        timer.elapsed_since_start(Duration::from_secs(60));
        assert!(timer.is_running());

        timer.reset();
        assert!(!timer.is_running());
    }
}