use crate::app::i18n::{Tr, fill};
use crate::app::settings::GameSettings;
use crate::app::{Sim, menu_ui, palette};
use crate::sim::{Board, EVENT_TICKS, TICKS_PER_SECOND};
use bevy::prelude::*;
#[derive(Component)]
pub struct EventTimer;
#[derive(Component)]
pub struct EventTimerName;
#[derive(Component)]
pub struct EventTimerLeft;
#[derive(Component)]
pub struct EventTimerFill;
pub(super) const FEED_PAD_X: f32 = 12.0;
#[cfg(test)]
const FEED_TEXT_W: f32 =
super::SIDEBAR_W - 2.0 * 10.0 - 2.0 * super::CARD_BORDER - 2.0 * FEED_PAD_X;
const NAME_PX: f32 = menu_ui::type_scale::ROW;
const LEFT_PX: f32 = menu_ui::type_scale::BODY;
const BAR_MIN_W: f32 = 40.0;
const BAR_H: f32 = 6.0;
pub fn timer_lines<'a>(tr: &'a Tr, board: &Board, stepped: bool) -> Option<(&'a str, String, f32)> {
if board.round_over() {
return None;
}
let (event, ticks) = board.running_event()?;
let secs = ticks.div_ceil(TICKS_PER_SECOND);
let shown = if stepped {
secs * TICKS_PER_SECOND
} else {
ticks
};
let share = (shown as f32 / EVENT_TICKS as f32).clamp(0.0, 1.0);
Some((
crate::app::hud::event_name(tr, event),
fill(tr.event_left, &[("s", &secs.to_string())]),
share,
))
}
#[cfg(test)]
fn timed_events() -> impl Iterator<Item = crate::sim::TideEvent> {
crate::sim::TideEvent::ALL
.into_iter()
.filter(|event| event.lasts())
}
pub(super) fn spawn_event_timer(card: &mut ChildSpawnerCommands) {
card.spawn((
EventTimer,
Node {
flex_direction: FlexDirection::Column,
row_gap: Val::Px(2.0),
margin: UiRect::bottom(Val::Px(10.0)),
flex_shrink: 0.0,
display: Display::None,
..default()
},
))
.with_children(|block| {
block.spawn((
EventTimerName,
Text::new(""),
menu_ui::display_font(NAME_PX),
TextColor(palette::INK_TIDE),
TextLayout::no_wrap(),
));
block
.spawn(Node {
align_items: AlignItems::Center,
column_gap: Val::Px(8.0),
..default()
})
.with_children(|row| {
row.spawn((
EventTimerLeft,
Text::new(""),
TextFont {
font_size: FontSize::Px(LEFT_PX),
..default()
},
TextColor(palette::PARCHMENT),
TextLayout::no_wrap(),
));
row.spawn((
Node {
flex_grow: 1.0,
min_width: Val::Px(BAR_MIN_W),
height: Val::Px(BAR_H),
border_radius: BorderRadius::all(Val::Px(BAR_H / 2.0)),
..default()
},
BackgroundColor(palette::INK_TIDE.with_alpha(0.25)),
))
.with_children(|track| {
track.spawn((
EventTimerFill,
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
border_radius: BorderRadius::all(Val::Px(BAR_H / 2.0)),
..default()
},
BackgroundColor(palette::INK_TIDE),
));
});
});
});
}
type TimerParts<'w, 's> = (
Query<'w, 's, &'static mut Node, (With<EventTimer>, Without<EventTimerFill>)>,
Query<'w, 's, &'static mut Text, (With<EventTimerName>, Without<EventTimerLeft>)>,
Query<'w, 's, &'static mut Text, (With<EventTimerLeft>, Without<EventTimerName>)>,
Query<'w, 's, &'static mut Node, (With<EventTimerFill>, Without<EventTimer>)>,
);
pub fn update_event_timer(
sim: Res<Sim>,
settings: Res<GameSettings>,
phase: Res<State<crate::app::VersusPhase>>,
mut parts: ParamSet<TimerParts>,
) {
let lines = match phase.get() {
crate::app::VersusPhase::Over => None,
crate::app::VersusPhase::Countdown | crate::app::VersusPhase::Running => {
timer_lines(settings.tr(), &sim.0, settings.reduced_motion)
}
};
let display = match lines {
Some(_) => Display::Flex,
None => Display::None,
};
for mut node in &mut parts.p0() {
if node.display != display {
node.display = display;
}
}
let Some((name, left, share)) = lines else {
return;
};
for mut text in &mut parts.p1() {
menu_ui::set_text(&mut text, name);
}
for mut text in &mut parts.p2() {
menu_ui::set_text(&mut text, &left);
}
let width = Val::Percent(share * 100.0);
for mut node in &mut parts.p3() {
if node.width != width {
node.width = width;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::i18n::{ALL_LANGS, EN, metrics};
use crate::sim::TideEvent;
#[test]
fn the_timer_counts_the_events_own_ticks() {
let mut board = Board::new(7, 5, 3);
board.set_events_enabled(true);
board.rules_mut().round_length = Some(10 * EVENT_TICKS);
assert!(timer_lines(&EN, &board, false).is_none(), "nothing runs");
board.force_tide_event(TideEvent::SpeedUp, 0);
let (name, left, share) = timer_lines(&EN, &board, false).expect("it runs");
assert_eq!(name, EN.events[TideEvent::SpeedUp.index()]);
let full = (EVENT_TICKS / TICKS_PER_SECOND).to_string();
assert_eq!(left, fill(EN.event_left, &[("s", &full)]));
assert_eq!(share, 1.0);
for _ in 0..TICKS_PER_SECOND / 3 {
board.tick_idle();
}
let (_, left, smooth) = timer_lines(&EN, &board, false).expect("it runs");
assert_eq!(left, fill(EN.event_left, &[("s", &full)]));
assert!(smooth < 1.0);
let (_, _, stepped) = timer_lines(&EN, &board, true).expect("it runs");
assert_eq!(stepped, 1.0);
while board.running_event().is_some() {
let (_, left, _) = timer_lines(&EN, &board, false).expect("it runs");
assert_ne!(left, fill(EN.event_left, &[("s", "0")]), "zero while on");
board.tick_idle();
}
assert!(timer_lines(&EN, &board, false).is_none(), "gone with it");
board.force_tide_event(TideEvent::RightClaws, 0);
board.rules_mut().round_length = Some(board.ticks() as u32);
assert!(board.round_over() && board.running_event().is_some());
assert!(
timer_lines(&EN, &board, false).is_none(),
"the round is over"
);
}
#[test]
fn the_timer_fits_the_feed_card_in_every_language() {
let most = (EVENT_TICKS / TICKS_PER_SECOND).to_string();
for lang in ALL_LANGS {
let tr = lang.tr();
for event in timed_events() {
let name = crate::app::hud::event_name(tr, event);
let wide = metrics::display_px(name, NAME_PX);
assert!(
wide <= FEED_TEXT_W,
"{lang:?} {event:?}: {name} is {wide} px of {FEED_TEXT_W}"
);
}
let left = fill(tr.event_left, &[("s", &most)]);
let wide = metrics::text_px(&left, LEFT_PX);
assert!(
wide + BAR_MIN_W + 8.0 <= FEED_TEXT_W,
"{lang:?}: {left} is {wide} px beside the bar"
);
}
}
}