chainmailer 0.2.1

If You Do Not Send This Letter To Ten Recipients You Will Regret It
Documentation
use std::time::Duration;

use bevy::{prelude::*, time::common_conditions::on_timer};
use ratatui::{
    buffer::Buffer,
    layout::{Constraint, Layout, Rect},
    style::Stylize,
    text::{Line, Span},
    widgets::{Block, StatefulWidget, Widget},
};

use crate::{
    constants::{
        CURSOR_BLINK_SPEED, CUSTOM_BORDERS_UNDER, MAC_RED_MUTED_COLOR, PLASTIC_EMPHASIS_COLOR,
        PLASTIC_MEDIUM_BACKGROUND_COLOR, PLASTIC_PRIMARY_COLOR, PLASTIC_SECONDARY_COLOR,
    },
    letters::CurrentLetter,
    states::{GameStates, LetterFailed, generate_current_letter_system},
};

pub(super) fn plugin(app: &mut App) {
    app.init_resource::<Prompt>()
        .init_resource::<PromptState>()
        .add_systems(
            OnEnter(GameStates::Printing),
            timer_setup_system.after(generate_current_letter_system),
        )
        .add_systems(
            Update,
            (
                tick_timer_system,
                blink_prompt_system.run_if(on_timer(Duration::from_millis(CURSOR_BLINK_SPEED))),
            )
                .run_if(in_state(GameStates::Playing)),
        );
}

#[derive(Resource, Default)]
pub struct Prompt {
    pub text: String,
    pub timer: Timer,
}

#[derive(Resource, Default)]
pub struct PromptState {
    cursor_visible: bool,
}

impl StatefulWidget for &Prompt {
    type State = PromptState;

    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
    where
        Self: Sized,
    {
        let block = Block::bordered()
            .border_set(CUSTOM_BORDERS_UNDER)
            .fg(PLASTIC_PRIMARY_COLOR);

        let inner_area = block.inner(area);

        let [text_area, timer_area] =
            *Layout::horizontal([Constraint::Fill(1), Constraint::Length(7)]).split(inner_area)
        else {
            unreachable!()
        };

        let [remaining_area, elapsed_area] = *Layout::horizontal([
            Constraint::Percentage((self.timer.fraction_remaining() * 100.0) as u16),
            Constraint::Fill(1),
        ])
        .split(area) else {
            unreachable!()
        };

        let cursor = if state.cursor_visible { "_" } else { "" };

        let text = Line::from(vec![
            Span::from(" > ").fg(PLASTIC_SECONDARY_COLOR).bold(),
            Span::from(self.text.clone()).fg(PLASTIC_EMPHASIS_COLOR),
            Span::from(cursor).fg(PLASTIC_SECONDARY_COLOR),
        ]);

        let timer_text = Line::from(format!(" {:.1}s ", self.timer.remaining_secs())).bold();

        Block::default()
            .bg(MAC_RED_MUTED_COLOR)
            .render(remaining_area, buf);
        Block::default()
            .bg(PLASTIC_MEDIUM_BACKGROUND_COLOR)
            .render(elapsed_area, buf);

        block.render(area, buf);
        text.render(text_area, buf);
        timer_text.render(timer_area, buf);
    }
}

fn timer_setup_system(mut prompt: ResMut<Prompt>, current_letter: Res<CurrentLetter>) {
    prompt.timer = Timer::from_seconds(current_letter.time_limit as f32, TimerMode::Once);
}

fn blink_prompt_system(mut prompt_state: ResMut<PromptState>) {
    prompt_state.cursor_visible = !prompt_state.cursor_visible;
}

fn tick_timer_system(mut commands: Commands, time: Res<Time>, mut prompt: ResMut<Prompt>) {
    prompt.timer.tick(time.delta());

    if prompt.timer.finished() {
        commands.trigger(LetterFailed);
    }
}