use crate::app::palette::{CLOCK_CALM, CLOCK_RED, CLOCK_RED_BRIGHT};
use crate::app::settings::GameSettings;
use crate::app::{Phase, Screen, Sim, menu_ui};
use crate::sim::TICKS_PER_SECOND;
use bevy::prelude::*;
#[derive(Component)]
pub struct TideClock;
pub(crate) fn clock_into(out: &mut String, ticks: u64) {
use std::fmt::Write;
let secs = ticks.div_ceil(u64::from(TICKS_PER_SECOND));
out.clear();
let _ = write!(out, "{}:{:02}", secs / 60, secs % 60);
}
#[cfg(test)]
pub(crate) fn clock_text(ticks: u64) -> String {
let mut out = String::new();
clock_into(&mut out, ticks);
out
}
pub(crate) fn urgency_band(round: Option<u32>, band: u32) -> u64 {
match round {
Some(round) if round <= band => u64::from(round) / 3,
_ => u64::from(band),
}
}
pub(crate) fn clock_color(ticks: u64, round: Option<u32>, elapsed: f32, blink: bool) -> Color {
const BLINK_TICKS: u64 = 10 * TICKS_PER_SECOND as u64;
let red = urgency_band(round, crate::sim::SURGE_TICKS);
if ticks <= BLINK_TICKS.min(red / 3) {
if !blink || ((elapsed * 2.0) as u32).is_multiple_of(2) {
CLOCK_RED
} else {
CLOCK_RED_BRIGHT
}
} else if ticks <= red {
CLOCK_RED
} else {
CLOCK_CALM
}
}
#[derive(bevy::ecs::system::SystemParam)]
pub struct Blink<'w> {
time: Res<'w, Time>,
settings: Res<'w, GameSettings>,
round: crate::app::round_state::RoundState<'w>,
}
impl Blink<'_> {
pub fn read(&self, board: &crate::sim::Board) -> (f32, bool) {
let blink = !self.settings.reduced_motion && self.round.moving(board);
(self.time.elapsed_secs(), blink)
}
}
pub fn update_tide_clock(
sim: Res<Sim>,
screen: Res<State<Screen>>,
phase: Res<State<Phase>>,
blink: Blink,
mut clocks: Query<(&mut Text, &mut TextColor), With<TideClock>>,
mut line: Local<String>,
) {
let remaining = match screen.get() {
Screen::Versus => None,
Screen::Puzzle if *phase.get() == Phase::Running => {
crate::app::round_state::countdown(&sim.0, true).map(|c| c.remaining)
}
Screen::Puzzle if *phase.get() == Phase::Setup => sim.0.remaining_ticks(),
Screen::Puzzle
| Screen::Menu
| Screen::Editor
| Screen::Lobby
| Screen::Settings
| Screen::Controls
| Screen::MatchSetup
| Screen::Achievements
| Screen::StageSelect
| Screen::Replays
| Screen::Interlude
| Screen::Language
| Screen::NewVersion => None,
};
for (mut text, mut color) in &mut clocks {
let Some(ticks) = remaining else {
if !text.0.is_empty() {
text.0.clear();
}
continue;
};
clock_into(&mut line, ticks);
menu_ui::set_text(&mut text, &line);
let (elapsed, blinking) = blink.read(&sim.0);
let target = clock_color(ticks, sim.0.rules().round_length, elapsed, blinking);
menu_ui::set_color(&mut color, target);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clock_formats_minutes_and_seconds() {
let tps = u64::from(TICKS_PER_SECOND);
assert_eq!(clock_text(0), "0:00");
assert_eq!(clock_text(29 * tps), "0:29");
assert_eq!(clock_text(90 * tps), "1:30");
assert_eq!(clock_text(600 * tps), "10:00");
assert_eq!(clock_text(1), "0:01");
assert_eq!(clock_text(60 * tps - 1), "1:00");
assert_eq!(clock_text(29 * tps + 1), "0:30");
}
#[test]
fn the_clock_reddens_then_blinks() {
let tps = u64::from(TICKS_PER_SECOND);
let long = Some(3 * 60 * TICKS_PER_SECOND);
for round in [None, long] {
assert_eq!(clock_color(60 * tps, round, 0.0, true), CLOCK_CALM);
assert_eq!(clock_color(20 * tps, round, 0.0, true), CLOCK_RED);
assert_eq!(clock_color(5 * tps, round, 0.0, true), CLOCK_RED);
assert_eq!(clock_color(5 * tps, round, 0.5, true), CLOCK_RED_BRIGHT);
assert_eq!(clock_color(5 * tps, round, 0.5, false), CLOCK_RED);
}
}
#[test]
fn a_short_round_is_not_one_long_emergency() {
let dry_feet = Some(240);
assert_eq!(clock_color(200, dry_feet, 0.0, true), CLOCK_CALM);
assert_eq!(clock_color(100, dry_feet, 0.0, true), CLOCK_CALM);
assert_eq!(clock_color(60, dry_feet, 0.0, true), CLOCK_RED);
assert_eq!(clock_color(20, dry_feet, 0.5, true), CLOCK_RED_BRIGHT);
for round in [240u32, 300, 900, 1800, 3600] {
let red = urgency_band(Some(round), crate::sim::SURGE_TICKS);
let blink = (0..=round)
.rev()
.map(u64::from)
.find(|&t| clock_color(t, Some(round), 0.5, true) == CLOCK_RED_BRIGHT);
assert!(
blink.is_some_and(|blink| blink < red),
"round {round}: reddens at {red}, blinks at {blink:?}"
);
}
}
#[test]
fn the_urgency_band_shrinks_only_for_short_rounds() {
assert_eq!(urgency_band(None, 900), 900);
assert_eq!(urgency_band(Some(3600), 900), 900);
assert_eq!(urgency_band(Some(901), 900), 900);
assert_eq!(urgency_band(Some(900), 900), 300);
assert_eq!(urgency_band(Some(240), 900), 80);
}
}