strategem-hero 0.5.1

Simple CLI Game following Strategem Hero mini-game from Helldivers 2
use std::{cell::Cell, fmt::Display, time::Duration};

use chrono::{DateTime, TimeDelta, Utc};
use crossterm::{style::Stylize, ExecutableCommand};

use crate::strategem::{Strategem, StrategemClass, StrategemDifficulty};

pub struct GameTimer {
    initial_duration: Duration,
    game_over_time: DateTime<Utc>,
}

impl GameTimer {
    pub fn start_from(dur: Duration) -> Self {
        Self {
            initial_duration: dur,
            game_over_time: chrono::Utc::now() + dur,
        }
    }

    pub fn remaining(&self) -> TimeDelta {
        self.game_over_time - chrono::Utc::now()
    }

    pub fn is_over(&self) -> bool {
        self.game_over_time - chrono::Utc::now() <= TimeDelta::zero()
    }

    pub fn add(&mut self, dur: Duration) {
        self.game_over_time += dur;
    }

    pub fn reset(&mut self) {
        self.game_over_time = chrono::Utc::now() + self.initial_duration;
    }
}

impl Display for GameTimer {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let step = self.initial_duration.as_secs() / 10;
        let remaining_steps = self.remaining().num_seconds() / step as i64 + 1;
        let time_left = self.remaining();
        let steps_str = "#".repeat(remaining_steps as usize);

        write!(
            f,
            "[{}{}] {:02}.{:.1}s",
            match remaining_steps {
                1..=2 => steps_str.red(),
                3..=5 => steps_str.dark_yellow(),
                _ => steps_str.green(),
            },
            " ".repeat(10 - remaining_steps as usize),
            time_left.num_seconds(),
            (time_left.num_milliseconds() - time_left.num_seconds() * 1000) / 100
        )
    }
}

pub struct Penalty {
    counter: Cell<u32>,
    max_penalty: u32,
    step: u32,
}

impl Penalty {
    pub fn new(max_penalty: u32, step: u32) -> Self {
        Self {
            counter: Cell::new(0),
            max_penalty,
            step,
        }
    }

    pub fn apply(&self, on_done: impl FnOnce()) {
        if self.counter.get() < self.max_penalty {
            self.counter.set(self.counter.get() + self.step);
        } else {
            self.counter.set(0);
            on_done();
        }
    }
}

pub struct HideCursor;

impl HideCursor {
    pub fn hide() -> std::io::Result<HideCursorGuard> {
        std::io::stdout().execute(crossterm::cursor::Hide)?;
        Ok(HideCursorGuard)
    }
}

pub struct HideCursorGuard;

impl Drop for HideCursorGuard {
    fn drop(&mut self) {
        std::io::stdout().execute(crossterm::cursor::Show).unwrap();
    }
}

pub enum Multiplier {
    FirstTier,
    SecondTier,
    ThirdTier,
}

impl Multiplier {
    pub fn get(streak: usize) -> Multiplier {
        match streak {
            0..=5 => Multiplier::FirstTier,
            6..=20 => Multiplier::SecondTier,
            21.. => Multiplier::ThirdTier,
            _ => unreachable!(),
        }
    }
}

impl Display for Multiplier {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}",
            match self {
                Multiplier::FirstTier => "  ".black(),
                Multiplier::SecondTier => "x2".green(),
                Multiplier::ThirdTier => "x3".dark_magenta(),
            }
        )
    }
}

pub fn get_score_value(difficulty: &StrategemDifficulty, tier: Multiplier) -> usize {
    use Multiplier::*;
    use StrategemDifficulty::*;

    match (difficulty, tier) {
        (Easy, FirstTier) => 50,
        (Medium, FirstTier) => 75,
        (Hard, FirstTier) => 100,
        (Easy, SecondTier) => 100,
        (Medium, SecondTier) => 150,
        (Hard, SecondTier) => 200,
        (Easy, ThirdTier) => 125,
        (Medium, ThirdTier) => 190,
        (Hard, ThirdTier) => 250,
    }
}

pub fn format_strategem_name(strategem: &Strategem) -> String {
    match strategem.class() {
        StrategemClass::Supply => {
            format!(
                "|{}{}{}|",
                " ".on_cyan(),
                strategem.name().on_cyan().black(),
                " ".on_cyan(),
            )
        }
        StrategemClass::Mission => {
            format!(
                "|{}{}{}|",
                " ".on_yellow(),
                strategem.name().on_yellow().black(),
                " ".on_yellow(),
            )
        }
        StrategemClass::Defensive => {
            format!(
                "|{}{}{}|",
                " ".on_green(),
                strategem.name().on_green().white(),
                " ".on_green()
            )
        }
        StrategemClass::Offensive => {
            format!(
                "|{}{}{}|",
                " ".on_red(),
                strategem.name().on_red().white(),
                " ".on_red()
            )
        }
    }
}