use crate::app::settings::GameSettings;
use crate::app::{VersusPhase, menu_ui, palette};
use bevy::prelude::*;
pub const COUNTDOWN_SECS: f32 = 3.0;
const GO_SECS: f32 = 0.7;
const COUNT_PX: f32 = 160.0;
const POP: f32 = 0.45;
#[derive(Resource, Default)]
pub struct Countdown {
left: f32,
}
impl Countdown {
fn shown(&self) -> u32 {
self.left.ceil().max(1.0) as u32
}
fn settled(&self) -> f32 {
(self.left.ceil() - self.left).clamp(0.0, 1.0)
}
}
#[derive(bevy::ecs::system::SystemParam)]
pub struct Opening<'w> {
countdown: ResMut<'w, Countdown>,
next_vphase: ResMut<'w, NextState<VersusPhase>>,
}
impl Opening<'_> {
pub fn open(&mut self, count_in: bool) {
self.countdown.left = match count_in {
true => COUNTDOWN_SECS,
false => 0.0,
};
self.next_vphase.set(match count_in {
true => VersusPhase::Countdown,
false => VersusPhase::Running,
});
}
}
#[derive(Component)]
pub struct CountdownText {
go: Option<f32>,
}
pub fn show_countdown(mut commands: Commands, countdown: Res<Countdown>) {
if countdown.left <= 0.0 {
return;
}
commands
.spawn((
CountdownText { go: None },
GlobalZIndex(menu_ui::layer::BANNER),
menu_ui::centred_overlay(),
))
.with_children(|wrap| {
wrap.spawn((
Text::new(countdown.shown().to_string()),
menu_ui::display_font(COUNT_PX),
TextColor(palette::GOLD),
TextShadow {
offset: Vec2::new(4.0, 5.0),
color: Color::BLACK.with_alpha(0.6),
},
UiTransform::IDENTITY,
));
});
}
pub fn run_countdown(
time: Res<Time>,
menu: Res<crate::app::pause::PauseMenu>,
mut countdown: ResMut<Countdown>,
mut next_vphase: ResMut<NextState<VersusPhase>>,
) {
if menu.stops_the_round() {
return;
}
countdown.left -= time.delta_secs();
if countdown.left <= 0.0 {
next_vphase.set(VersusPhase::Running);
}
}
pub fn draw_countdown(
mut commands: Commands,
time: Res<Time>,
countdown: Res<Countdown>,
phase: Res<State<VersusPhase>>,
settings: Res<GameSettings>,
mut overlays: Query<(Entity, &mut CountdownText, &Children)>,
mut texts: Query<(&mut Text, &mut TextColor, &mut UiTransform)>,
) {
let calm = settings.reduced_motion;
for (entity, mut overlay, children) in &mut overlays {
let (word, ink, settle) = match phase.get() {
VersusPhase::Countdown => (
countdown.shown().to_string(),
palette::GOLD,
countdown.settled(),
),
VersusPhase::Running => {
let up = overlay.go.unwrap_or(0.0) + time.delta_secs();
overlay.go = Some(up);
if up >= GO_SECS {
commands.entity(entity).despawn();
continue;
}
let fade = 1.0 - up / GO_SECS;
(
settings.tr().countdown_go.to_string(),
palette::PARCHMENT.with_alpha(fade),
up / GO_SECS,
)
}
VersusPhase::Over => {
commands.entity(entity).despawn();
continue;
}
};
let scale = match calm {
true => 1.0,
false => 1.0 + POP * (1.0 - settle).powi(3),
};
for child in children {
let Ok((mut text, mut color, mut transform)) = texts.get_mut(*child) else {
continue;
};
menu_ui::set_text(&mut text, &word);
menu_ui::set_color(&mut color, ink);
let pop = Vec2::splat(scale);
if transform.scale != pop {
transform.scale = pop;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_count_reads_three_two_one() {
let at = |left: f32| Countdown { left }.shown();
assert_eq!(at(3.0), 3);
assert_eq!(at(2.01), 3);
assert_eq!(at(2.0), 2);
assert_eq!(at(1.5), 2);
assert_eq!(at(0.5), 1);
assert_eq!(at(0.0), 1);
assert_eq!(at(-0.1), 1);
}
#[test]
fn the_round_starts_when_the_count_is_out() {
use bevy::state::app::StatesPlugin;
let mut app = App::new();
app.add_plugins(StatesPlugin);
app.init_state::<VersusPhase>();
app.insert_resource(State::new(VersusPhase::Countdown));
app.init_resource::<Countdown>();
app.init_resource::<crate::app::pause::PauseMenu>();
app.insert_resource(Time::<()>::default());
app.world_mut().resource_mut::<Countdown>().left = COUNTDOWN_SECS;
app.add_systems(Update, run_countdown);
let step = |app: &mut App, secs: f32| {
app.world_mut()
.resource_mut::<Time>()
.advance_by(std::time::Duration::from_secs_f32(secs));
app.update();
};
let phase = |app: &App| *app.world().resource::<State<VersusPhase>>().get();
step(&mut app, 1.0);
step(&mut app, 1.0);
assert_eq!(phase(&app), VersusPhase::Countdown, "two seconds in");
app.world_mut()
.resource_mut::<crate::app::pause::PauseMenu>()
.open = true;
step(&mut app, 5.0);
assert_eq!(phase(&app), VersusPhase::Countdown, "held by the card");
app.world_mut()
.resource_mut::<crate::app::pause::PauseMenu>()
.open = false;
step(&mut app, 1.1);
app.update();
assert_eq!(phase(&app), VersusPhase::Running, "and out");
}
}