use crate::app::art::Art;
use crate::app::cycle::Cycle;
use crate::app::i18n::{ALL_LANGS, Lang};
use crate::app::settings::GameSettings;
use crate::app::{Screen, menu_ui, palette};
use bevy::prelude::*;
#[derive(Component)]
pub struct LanguageUi;
#[derive(Component)]
pub struct LanguageRow(pub usize);
#[derive(Component)]
pub struct LanguageNote;
#[derive(Component)]
pub struct LanguageCritter {
gull: bool,
rate: f32,
travel: f32,
phase: f32,
frame: f32,
}
const FLAG_W: f32 = 33.0;
const FLAG_H: f32 = 22.0;
const ROW_FONT: f32 = 22.0;
const NAME_W: f32 = 190.0;
const CRITTER: f32 = 84.0;
const CRITTER_GAP: f32 = 40.0;
pub fn enter_language(mut commands: Commands, settings: Res<GameSettings>, art: Res<Art>) {
commands
.spawn((LanguageUi, menu_ui::between_bars()))
.with_children(|wrap| {
for (index, (x, y, size, gull)) in FLOCK.into_iter().enumerate() {
spawn_critter(wrap, &art, gull, size, index as f32 * 0.9, Some((x, y)));
}
wrap.spawn(Node {
align_items: AlignItems::Center,
column_gap: Val::Px(CRITTER_GAP),
..default()
})
.with_children(|line| {
spawn_critter(line, &art, false, CRITTER, 0.0, None);
line.spawn(menu_ui::screen_card()).with_children(|card| {
for (index, lang) in ALL_LANGS.iter().enumerate() {
card.spawn((LanguageRow(index), menu_ui::card_row()))
.with_children(|row| {
row.spawn((
ImageNode::new(art.flag(*lang)),
Node {
width: Val::Px(FLAG_W),
height: Val::Px(FLAG_H),
flex_shrink: 0.0,
margin: UiRect::right(Val::Px(12.0)),
..default()
},
));
row.spawn((
Text::new(lang.native_name()),
TextFont {
font_size: FontSize::Px(ROW_FONT),
..default()
},
TextLayout::no_wrap(),
TextColor(palette::IDLE_ROW),
Node {
width: Val::Px(NAME_W),
..default()
},
));
});
}
});
spawn_critter(line, &art, true, CRITTER, 1.7, None);
});
wrap.spawn((
LanguageNote,
Text::new(settings.tr().pick_language_later),
TextFont {
font_size: FontSize::Px(15.0),
..default()
},
TextLayout::no_wrap(),
TextColor(palette::PARCHMENT.with_alpha(0.40)),
));
});
}
const FLOCK: [(f32, f32, f32, bool); 7] = [
(0.09, 0.13, 46.0, true),
(0.25, 0.04, 30.0, true),
(0.88, 0.09, 40.0, true),
(0.70, 0.02, 26.0, true),
(0.12, 0.80, 42.0, false),
(0.35, 0.92, 28.0, false),
(0.86, 0.84, 48.0, false),
];
const FLOCK_ALPHA: f32 = 0.30;
fn spawn_critter(
parent: &mut ChildSpawnerCommands,
art: &Art,
gull: bool,
size: f32,
phase: f32,
at: Option<(f32, f32)>,
) {
let (image, tint) = if gull {
(art.gull.clone(), Color::WHITE)
} else {
(
art.crab.clone(),
crate::app::creatures::body_color(crate::sim::CrabKind::Common),
)
};
let node = match at {
Some((x, y)) => Node {
position_type: PositionType::Absolute,
left: Val::Percent(x * 100.0),
top: Val::Percent(y * 100.0),
width: Val::Px(size),
height: Val::Px(size),
..default()
},
None => Node {
width: Val::Px(size),
height: Val::Px(size),
flex_shrink: 0.0,
..default()
},
};
parent.spawn((
LanguageCritter {
gull,
rate: if gull { 1.5 } else { 3.4 },
travel: if gull { 7.0 } else { 3.0 },
phase,
frame: if gull { 0.30 } else { 0.16 },
},
ImageNode {
image,
color: tint.with_alpha(if at.is_some() { FLOCK_ALPHA } else { 1.0 }),
flip_x: at.map_or(gull, |(x, _)| x > 0.5),
..default()
},
node,
));
}
pub fn animate_language_art(
time: Res<Time>,
art: Res<Art>,
mut critters: Query<(&LanguageCritter, &mut Node, &mut ImageNode)>,
) {
let now = time.elapsed_secs();
for (critter, mut node, mut image) in &mut critters {
let bob = Val::Px((now * critter.rate + critter.phase).sin() * critter.travel);
if node.margin.top != bob {
node.margin.top = bob;
}
let beat = ((now + critter.phase) / critter.frame) as u32 % 2 == 1;
let want = match (critter.gull, beat) {
(true, true) => &art.gull_fly,
(true, false) => &art.gull,
(false, true) => &art.crab_b,
(false, false) => &art.crab,
};
if image.image != *want {
image.image = want.clone();
}
}
}
pub fn opening_screen(saved: bool) -> Screen {
if saved {
Screen::default()
} else {
Screen::Language
}
}
fn step(keys: &ButtonInput<KeyCode>, current: Lang) -> (Lang, bool) {
let at = menu_ui::nav(keys, current.index(), ALL_LANGS.len());
let taken = keys.just_pressed(KeyCode::Enter) || keys.just_pressed(KeyCode::NumpadEnter);
(ALL_LANGS[at], taken)
}
pub fn language_input(
keys: Res<ButtonInput<KeyCode>>,
mut settings: ResMut<GameSettings>,
mut next_screen: ResMut<NextState<Screen>>,
) {
let (lang, taken) = step(&keys, settings.language);
if lang != settings.language {
settings.language = lang;
}
if taken {
settings.save();
next_screen.set(Screen::Menu);
}
}
pub fn update_language_ui(
settings: Res<GameSettings>,
mut rows: Query<(&LanguageRow, &mut BackgroundColor, &Children)>,
mut text: Query<(&mut TextColor, &mut Node)>,
mut note: Query<&mut Text, With<LanguageNote>>,
) {
for mut line in &mut note {
menu_ui::set_text(&mut line, settings.tr().pick_language_later);
}
let picked = settings.language.index();
for (row, mut fill, children) in &mut rows {
let on = row.0 == picked;
menu_ui::set_bg(&mut fill, menu_ui::band(on));
for child in children {
let Ok((mut colour, _)) = text.get_mut(*child) else {
continue;
};
menu_ui::set_color(
&mut colour,
if on {
palette::SELECTED_ROW
} else {
palette::IDLE_ROW
},
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pressing(key: KeyCode) -> ButtonInput<KeyCode> {
let mut keys = ButtonInput::default();
keys.press(key);
keys
}
#[test]
fn walking_down_reaches_every_language() {
let mut at = Lang::default();
let mut seen = vec![at];
for _ in 1..ALL_LANGS.len() {
at = step(&pressing(KeyCode::KeyS), at).0;
seen.push(at);
}
for lang in ALL_LANGS {
assert!(seen.contains(&lang), "{lang:?} cannot be walked to");
}
assert_eq!(step(&pressing(KeyCode::KeyS), at).0, Lang::default());
}
#[test]
fn walking_up_wraps_to_the_end() {
let up = step(&pressing(KeyCode::KeyW), Lang::default()).0;
assert_eq!(up, *ALL_LANGS.last().unwrap());
}
#[test]
fn only_enter_takes_it() {
for key in [
KeyCode::KeyW,
KeyCode::KeyS,
KeyCode::ArrowUp,
KeyCode::Escape,
] {
assert!(!step(&pressing(key), Lang::default()).1, "{key:?} took it");
}
for key in [KeyCode::Enter, KeyCode::NumpadEnter] {
let (lang, taken) = step(&pressing(key), Lang::Es);
assert!(taken, "{key:?} did not take it");
assert_eq!(lang, Lang::Es);
}
}
#[test]
fn only_a_fresh_install_is_asked() {
assert_eq!(opening_screen(false), Screen::Language);
assert_eq!(opening_screen(true), Screen::default());
assert_ne!(Screen::default(), Screen::Language);
}
#[test]
fn the_flock_leaves_the_card_alone() {
for (x, y, size, gull) in FLOCK {
assert!((0.0..=0.95).contains(&x), "{x} is off the frame");
assert!((0.0..=0.95).contains(&y), "{y} is off the frame");
assert!(size > 0.0, "a critter with no size is not drawn");
assert_eq!(gull, y < 0.5, "a gull on the sand, or a crab in the air");
assert!(
!((0.30..0.70).contains(&x) && (0.15..0.80).contains(&y)),
"the critter at {x},{y} sits over the card"
);
}
}
#[test]
fn the_default_language_is_on_the_list() {
assert!(ALL_LANGS.contains(&Lang::default()));
assert_eq!(ALL_LANGS[Lang::default().index()], Lang::default());
}
}