use crate::app::editor::EditorState;
use crate::app::lobby::LobbyState;
use crate::app::net::Online;
use crate::app::settings::GameSettings;
use crate::app::{Bots, Campaign, Phase, Playback, Screen, Seats, Sim, VersusPhase};
use crate::app::{menu_ui, palette};
use bevy::prelude::*;
mod text;
use text::CLOCK_CALM;
pub(crate) use text::{clock_color, clock_text, event_name};
#[derive(Component)]
pub struct LevelLabel;
#[derive(Component)]
pub struct PostsLabel;
#[derive(Component)]
pub struct PromptLabel;
#[derive(Component)]
pub struct HeaderBar;
pub fn spawn_hud(
mut commands: Commands,
settings: Res<crate::app::settings::GameSettings>,
art: Res<crate::app::art::Art>,
) {
let font = TextFont {
font_size: FontSize::Px(22.0),
..default()
};
commands
.spawn((
HeaderBar,
Node {
position_type: PositionType::Absolute,
top: Val::Px(0.0),
left: Val::Px(0.0),
width: Val::Percent(100.0),
height: Val::Px(42.0),
padding: UiRect::horizontal(Val::Px(12.0)),
align_items: AlignItems::Center,
justify_content: JustifyContent::SpaceBetween,
column_gap: Val::Px(18.0),
..default()
},
BackgroundColor(Color::srgb(0.08, 0.09, 0.12)),
))
.with_children(|bar| {
bar.spawn((
LevelLabel,
Text::new(""),
font.clone(),
TextColor(Color::WHITE),
));
bar.spawn((
PostsLabel,
Text::new(""),
font.clone(),
TextColor(Color::WHITE),
));
});
commands.spawn((
PromptLabel,
Text::new(""),
font,
TextColor(palette::SELECTED_ROW),
Node {
position_type: PositionType::Absolute,
bottom: Val::Px(8.0),
left: Val::Px(12.0),
max_width: Val::Percent(88.0),
padding: UiRect::axes(Val::Px(12.0), Val::Px(5.0)),
border_radius: BorderRadius::all(Val::Px(11.0)),
..default()
},
BackgroundColor(Color::srgba(0.05, 0.07, 0.11, 0.72)),
));
commands
.spawn((Node {
position_type: PositionType::Absolute,
top: Val::Px(46.0),
left: Val::Px(0.0),
width: Val::Percent(100.0),
justify_content: JustifyContent::Center,
..default()
},))
.with_children(|wrap| {
wrap.spawn((
HintLabel,
Text::new(""),
TextFont {
font_size: FontSize::Px(19.0),
..default()
},
TextColor(palette::SELECTED_ROW.with_alpha(0.85)),
));
});
commands
.spawn((
Node {
position_type: PositionType::Absolute,
top: Val::Px(2.0),
left: Val::Px(0.0),
width: Val::Percent(100.0),
justify_content: JustifyContent::Center,
..default()
},
ZIndex(1),
))
.with_children(|wrap| {
wrap.spawn((
TideClock,
Text::new(""),
TextFont {
font_size: FontSize::Px(34.0),
..default()
},
TextColor(CLOCK_CALM),
));
});
spawn_field_guide(&mut commands, settings.tr(), &art);
}
#[derive(Component)]
pub struct HintLabel;
#[derive(Component)]
pub struct TideClock;
const KEY_LESSON_LEVEL: &str = "Welcome Ashore";
pub fn update_hint(
campaign: Res<Campaign>,
screen: Res<State<Screen>>,
phase: Res<State<Phase>>,
settings: Res<GameSettings>,
stuck: Res<crate::app::hint::Hints>,
mut hints: Query<&mut Text, With<HintLabel>>,
) {
let line = if *screen.get() != Screen::Puzzle {
String::new()
} else {
crate::app::hint::hint_line(settings.tr(), &stuck, phase.get()).unwrap_or_else(|| {
let name = &campaign.current().name;
let honest = name != KEY_LESSON_LEVEL || settings.stock_legend();
if *phase.get() == Phase::Setup && honest {
settings.language.level_hint(name).unwrap_or("").to_string()
} else {
String::new()
}
})
};
for mut text in &mut hints {
menu_ui::set_text(&mut text, &line);
}
}
pub fn update_tide_clock(
sim: Res<Sim>,
screen: Res<State<Screen>>,
phase: Res<State<Phase>>,
time: Res<Time>,
settings: Res<GameSettings>,
mut clocks: Query<(&mut Text, &mut TextColor), With<TideClock>>,
) {
let remaining = match screen.get() {
Screen::Versus => None,
Screen::Puzzle if *phase.get() == Phase::Running => sim.0.remaining_ticks().or(Some(
crate::sim::PUZZLE_TICK_LIMIT.saturating_sub(sim.0.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;
};
menu_ui::set_text(&mut text, &clock_text(ticks));
let target = clock_color(ticks, time.elapsed_secs(), !settings.reduced_motion);
menu_ui::set_color(&mut color, target);
}
}
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
pub fn update_hud(
sim: Res<Sim>,
campaign: Res<Campaign>,
screen: Res<State<Screen>>,
phase: Res<State<Phase>>,
vphase: Res<State<VersusPhase>>,
editor: Res<EditorState>,
online: Res<Online>,
playback: Res<Playback>,
lobby: Res<LobbyState>,
seats: Res<Seats>,
(settings, names): (Res<GameSettings>, Res<crate::app::SeatNames>),
bots: Res<Bots>,
library: Res<crate::app::replays::Library>,
(notice, match_menu): (
Res<crate::app::RoundNotice>,
Res<crate::app::match_setup::MatchMenu>,
),
speed: Res<crate::app::replays::PlaybackSpeed>,
mut labels: ParamSet<(
Query<&mut Text, With<LevelLabel>>,
Query<&mut Text, With<PostsLabel>>,
Query<&mut Text, With<PromptLabel>>,
)>,
) {
let tr = settings.tr();
let lang = settings.language;
let said = text::screen_text(
*screen.get(),
&text::Readout {
tr,
lang,
sim: &sim,
campaign: &campaign,
phase: &phase,
vphase: &vphase,
editor: &editor,
online: &online,
playback: &playback,
lobby: &lobby,
seats: &seats,
settings: &settings,
names: &names,
bots: &bots,
library: &library,
notice: ¬ice,
match_menu: &match_menu,
speed: speed.0,
},
);
if let Ok(mut text) = labels.p0().single_mut() {
menu_ui::set_text(&mut text, &said.title);
}
if let Ok(mut text) = labels.p1().single_mut() {
menu_ui::set_text(&mut text, &said.status);
}
if let Ok(mut text) = labels.p2().single_mut() {
menu_ui::set_text(&mut text, &said.prompt);
}
}
#[derive(Component)]
pub struct FieldGuide;
#[derive(Component)]
pub struct FieldGuideNote(usize);
const KINDS: [crate::sim::CrabKind; 6] = [
crate::sim::CrabKind::Common,
crate::sim::CrabKind::Juvenile,
crate::sim::CrabKind::Giant,
crate::sim::CrabKind::Molting,
crate::sim::CrabKind::Golden,
crate::sim::CrabKind::Sparkling,
];
fn field_guide_note(tr: &crate::app::i18n::Tr, i: usize) -> String {
let kind = KINDS[i];
let note = tr.crab_notes[i];
if note.is_empty() {
kind.value().to_string()
} else {
format!("{} {note}", kind.value())
}
}
pub fn update_field_guide(
settings: Res<GameSettings>,
mut notes: Query<(&FieldGuideNote, &mut Text)>,
) {
if !settings.is_changed() {
return;
}
let tr = settings.tr();
for (note, mut text) in &mut notes {
menu_ui::set_text(&mut text, &field_guide_note(tr, note.0));
}
}
fn spawn_field_guide(
commands: &mut Commands,
tr: &crate::app::i18n::Tr,
art: &crate::app::art::Art,
) {
commands
.spawn((
FieldGuide,
Node {
position_type: PositionType::Absolute,
bottom: Val::Px(60.0),
left: Val::Px(0.0),
width: Val::Percent(100.0),
justify_content: JustifyContent::Center,
..default()
},
))
.with_children(|row| {
row.spawn((
Node {
column_gap: Val::Px(10.0),
align_items: AlignItems::Center,
padding: UiRect::axes(Val::Px(12.0), Val::Px(4.0)),
border_radius: BorderRadius::all(Val::Px(11.0)),
..default()
},
BackgroundColor(Color::srgba(0.05, 0.07, 0.11, 0.72)),
))
.with_children(|strip| {
for (i, kind) in KINDS.iter().enumerate() {
strip.spawn((
ImageNode::new(art.crab.clone())
.with_color(crate::app::creatures::body_color(*kind)),
Node {
width: Val::Px(20.0),
height: Val::Px(20.0),
..default()
},
));
strip.spawn((
FieldGuideNote(i),
Text::new(field_guide_note(tr, i)),
TextFont {
font_size: FontSize::Px(13.0),
..default()
},
TextLayout::no_wrap(),
TextColor(palette::PARCHMENT.with_alpha(0.85)),
));
}
});
});
}
pub fn field_guide_visibility(
screen: Res<State<Screen>>,
mut guides: Query<&mut Node, With<FieldGuide>>,
) {
let wanted = match screen.get() {
Screen::Versus | Screen::Puzzle => Display::Flex,
Screen::Menu
| Screen::Editor
| Screen::Lobby
| Screen::Settings
| Screen::Controls
| Screen::MatchSetup
| Screen::Achievements
| Screen::StageSelect
| Screen::Replays
| Screen::Interlude
| Screen::Language
| Screen::NewVersion => Display::None,
};
for mut node in &mut guides {
if node.display != wanted {
node.display = wanted;
}
}
}
pub fn header_backdrop(
screen: Res<State<Screen>>,
mut bars: Query<&mut BackgroundColor, With<HeaderBar>>,
) {
let target = if *screen.get() == Screen::Menu {
Color::NONE
} else {
Color::srgb(0.08, 0.09, 0.12)
};
for mut bg in &mut bars {
menu_ui::set_bg(&mut bg, target);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::i18n::Lang;
#[test]
fn only_the_key_lesson_names_keys() {
let names_keys = |hint: &str| hint.contains("WASD") || hint.contains("arrow");
assert!(names_keys(Lang::En.level_hint(KEY_LESSON_LEVEL).unwrap()));
for level in crate::sim::campaign_levels() {
if level.name != KEY_LESSON_LEVEL
&& let Some(hint) = Lang::En.level_hint(&level.name)
{
assert!(!names_keys(hint), "{:?} names keys too", level.name);
}
}
}
}