mod scenery;
pub use scenery::{menu_ambience, refit_shore};
use crate::app::effects::VisualRng;
use crate::app::i18n::fill;
use crate::app::menu_ui;
use crate::app::menu_ui::card_shadow;
use crate::app::palette;
use crate::app::settings::GameSettings;
use crate::app::{Campaign, CampaignKind, Screen};
use crate::sim::challenge_levels;
use bevy::prelude::*;
const TITLE: &str = "P\u{2007}I\u{2007}N\u{2007}C\u{2007}H\u{2007}\u{2007}\u{2007}\u{2007}P\u{2007}O\u{2007}I\u{2007}N\u{2007}T\u{2007}S";
const TITLE_INK: Color = Color::srgb(0.99, 0.85, 0.36);
const TITLE_SHADOW: Color = Color::srgba(0.05, 0.10, 0.20, 0.55);
const SIGN_FILL: Color = Color::srgba(0.05, 0.09, 0.14, 0.96);
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum MenuEntry {
TidePool,
TurfWar,
Driftwood,
Replay,
BeachLobby,
Settings,
BeachDay,
Achievements,
DailyChallenge,
Resume,
}
impl MenuEntry {
pub const ALL: [MenuEntry; 10] = [
MenuEntry::TidePool,
MenuEntry::TurfWar,
MenuEntry::Driftwood,
MenuEntry::Replay,
MenuEntry::BeachLobby,
MenuEntry::Settings,
MenuEntry::BeachDay,
MenuEntry::Achievements,
MenuEntry::DailyChallenge,
MenuEntry::Resume,
];
}
pub const MENU_ENTRY_COUNT: usize = MenuEntry::ALL.len();
fn live_rows() -> [bool; MENU_ENTRY_COUNT] {
let waiting = crate::app::suspend::waiting();
std::array::from_fn(|row| MenuEntry::ALL[row] != MenuEntry::Resume || waiting)
}
#[derive(Resource, Default)]
pub struct MenuList {
pub selected: usize,
}
#[derive(Component)]
pub struct MenuRow(pub usize);
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum MenuCol {
Key,
Name,
Blurb,
}
#[derive(Component)]
pub struct MenuCell(pub usize, pub MenuCol);
#[derive(Component)]
pub struct MenuArt;
fn spawn_title(commands: &mut Commands, settings: &GameSettings) {
commands
.spawn((
MenuArt,
Node {
position_type: PositionType::Absolute,
top: Val::Px(44.0),
left: Val::Px(0.0),
width: Val::Percent(100.0),
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
..default()
},
))
.with_children(|wrap| {
wrap.spawn((
Node {
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
row_gap: Val::Px(6.0),
padding: UiRect::axes(Val::Px(34.0), Val::Px(16.0)),
border: UiRect::all(Val::Px(1.0)),
border_radius: BorderRadius::all(Val::Px(18.0)),
..default()
},
BackgroundColor(SIGN_FILL),
BorderColor::all(palette::CARD_EDGE),
card_shadow(),
))
.with_children(|card| {
card.spawn((
Text::new(TITLE),
TextFont {
font_size: FontSize::Px(54.0),
..default()
},
TextColor(TITLE_INK),
TextShadow {
offset: Vec2::new(2.0, 3.0),
color: TITLE_SHADOW,
},
));
card.spawn((
Text::new(settings.tr().tagline),
TextFont {
font_size: FontSize::Px(18.0),
..default()
},
TextColor(palette::PARCHMENT),
));
});
});
}
fn spawn_mode_list(commands: &mut Commands) {
commands
.spawn((
MenuArt,
Node {
position_type: PositionType::Absolute,
top: Val::Px(196.0),
width: Val::Percent(100.0),
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
..default()
},
))
.with_children(|wrap| {
wrap.spawn((
Node {
flex_direction: FlexDirection::Column,
row_gap: Val::Px(2.0),
padding: UiRect::axes(Val::Px(20.0), Val::Px(16.0)),
border: UiRect::all(Val::Px(1.0)),
border_radius: BorderRadius::all(Val::Px(18.0)),
..default()
},
BackgroundColor(palette::CARD_FILL),
BorderColor::all(palette::CARD_EDGE),
card_shadow(),
))
.with_children(|panel| {
for row in 0..MENU_ENTRY_COUNT {
panel
.spawn((
MenuRow(row),
Node {
align_items: AlignItems::Center,
column_gap: Val::Px(14.0),
padding: UiRect::axes(Val::Px(12.0), Val::Px(4.0)),
border_radius: BorderRadius::all(Val::Px(8.0)),
..default()
},
BackgroundColor(Color::NONE),
))
.with_children(|line| {
for (col, width, size) in [
(MenuCol::Key, 16.0, 17.0),
(MenuCol::Name, 216.0, 21.0),
(MenuCol::Blurb, 384.0, 17.0),
] {
line.spawn((
Node {
width: Val::Px(width),
flex_shrink: 0.0,
overflow: Overflow::clip_x(),
..default()
},
children![(
MenuCell(row, col),
Text::new(""),
TextFont {
font_size: FontSize::Px(size),
..default()
},
TextLayout::no_wrap(),
TextColor(palette::IDLE_ROW),
)],
));
}
});
}
});
});
}
pub fn enter_menu(mut commands: Commands, settings: Res<GameSettings>) {
spawn_title(&mut commands, &settings);
spawn_mode_list(&mut commands);
spawn_version(&mut commands);
}
fn spawn_version(commands: &mut Commands) {
commands.spawn((
MenuArt,
Text::new(crate::app::update::Version::current().to_string()),
TextFont {
font_size: FontSize::Px(15.0),
..default()
},
TextLayout::no_wrap(),
TextColor(palette::PARCHMENT.with_alpha(0.75)),
Node {
position_type: PositionType::Absolute,
bottom: Val::Px(8.0),
right: Val::Px(12.0),
padding: UiRect::axes(Val::Px(12.0), Val::Px(7.0)),
border_radius: BorderRadius::all(Val::Px(11.0)),
..default()
},
BackgroundColor(Color::srgba(0.05, 0.07, 0.11, 0.72)),
));
}
pub fn update_menu_rows(
list: Res<MenuList>,
settings: Res<GameSettings>,
mut cells: Query<(&MenuCell, &mut Text, &mut TextColor)>,
mut rows: Query<(&MenuRow, &mut BackgroundColor)>,
) {
let tr = settings.tr();
let live = live_rows();
for (cell, mut text, mut color) in &mut cells {
let picked = cell.0 == list.selected;
let line = if live[cell.0] {
match cell.1 {
MenuCol::Key => if cell.0 + 1 < 10 { cell.0 + 1 } else { 0 }.to_string(),
MenuCol::Name => tr.menu_names[cell.0].to_string(),
MenuCol::Blurb if MenuEntry::ALL[cell.0] == MenuEntry::DailyChallenge => {
let (day, month) = crate::app::clock::civil_date(crate::app::Daily::today());
fill(
tr.menu_daily_blurb,
&[("d", &format!("{day:02}")), ("m", &format!("{month:02}"))],
)
}
MenuCol::Blurb => tr.menu_blurbs[cell.0].to_string(),
}
} else {
String::new()
};
let target = match (cell.1, picked) {
(MenuCol::Key, true) => palette::GOLD,
(MenuCol::Key, false) => palette::PARCHMENT.with_alpha(0.30),
(MenuCol::Name, true) => Color::WHITE,
(MenuCol::Name, false) => palette::PARCHMENT.with_alpha(0.92),
(MenuCol::Blurb, true) => palette::PARCHMENT.with_alpha(0.85),
(MenuCol::Blurb, false) => palette::PARCHMENT.with_alpha(0.45),
};
menu_ui::set_text(&mut text, &line);
menu_ui::set_color(&mut color, target);
}
for (row, mut fill) in &mut rows {
let ground = if row.0 == list.selected && live[row.0] {
palette::GOLD.with_alpha(0.16)
} else {
Color::NONE
};
if fill.0 != ground {
fill.0 = ground;
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn menu_input(
keys: Res<ButtonInput<KeyCode>>,
mut exit: MessageWriter<AppExit>,
mut list: ResMut<MenuList>,
settings: Res<GameSettings>,
mut campaign: ResMut<Campaign>,
mut config: ResMut<crate::app::match_setup::MatchConfig>,
mut daily: ResMut<crate::app::Daily>,
mut resuming: ResMut<crate::app::Resuming>,
mut notice: ResMut<crate::app::RoundNotice>,
mut clipboard: ResMut<Clipboard>,
mut next_screen: ResMut<NextState<Screen>>,
) {
if keys.just_pressed(KeyCode::Escape) {
exit.write(AppExit::Success);
return;
}
if keys.just_pressed(KeyCode::KeyV) {
match crate::app::suspend::round_from(
crate::app::codes::paste(&mut clipboard),
settings.tr(),
) {
Ok(round) => {
notice.0.clear();
resuming.0 = Some(round);
next_screen.set(Screen::Versus);
}
Err(complaint) => notice.0 = complaint,
}
return;
}
let live = live_rows();
list.selected = menu_ui::nav_live(&keys, list.selected, &live);
const HOTKEYS: [KeyCode; MENU_ENTRY_COUNT] = [
KeyCode::Digit1,
KeyCode::Digit2,
KeyCode::Digit3,
KeyCode::Digit4,
KeyCode::Digit5,
KeyCode::Digit6,
KeyCode::Digit7,
KeyCode::Digit8,
KeyCode::Digit9,
KeyCode::Digit0,
];
let choice = if keys.just_pressed(KeyCode::Enter) {
Some(list.selected)
} else {
HOTKEYS.iter().position(|&key| keys.just_pressed(key))
};
let Some(choice) = choice.filter(|&row| live[row]) else {
return;
};
list.selected = choice;
match MenuEntry::ALL[choice] {
MenuEntry::TidePool => {
let (levels, builtins) = crate::app::campaign::tide_pool_levels();
campaign.reset(CampaignKind::TidePool, levels, builtins);
next_screen.set(Screen::StageSelect);
}
MenuEntry::TurfWar => next_screen.set(Screen::MatchSetup),
MenuEntry::Driftwood => next_screen.set(Screen::Editor),
MenuEntry::Replay => next_screen.set(Screen::Replays),
MenuEntry::BeachLobby => next_screen.set(Screen::Lobby),
MenuEntry::Settings => next_screen.set(Screen::Settings),
MenuEntry::BeachDay => {
let levels = challenge_levels();
let builtins = levels.len();
campaign.reset(CampaignKind::BeachDay, levels, builtins);
next_screen.set(Screen::StageSelect);
}
MenuEntry::Achievements => next_screen.set(Screen::Achievements),
MenuEntry::Resume => match crate::app::suspend::pick_up() {
Ok(round) => {
notice.0.clear();
resuming.0 = Some(round);
next_screen.set(Screen::Versus);
}
Err(e) => notice.0 = e,
},
MenuEntry::DailyChallenge => {
config.seats = 4;
config.bots = 3;
config.bot_levels = [crate::sim::BotLevel::Hard; crate::sim::MAX_PLAYERS];
config.map = crate::app::match_setup::MapChoice::GenClassic;
config.gulls = crate::app::match_setup::GullPressure::Normal;
config.round = crate::app::match_setup::RoundLength::Standard;
config.armed = true;
daily.active = true;
next_screen.set(Screen::Versus);
}
}
}