mod achievements;
pub(crate) mod announce;
mod art;
mod audio;
mod awards;
mod binds;
mod board_render;
mod boot;
mod bot_seats;
pub mod campaign;
pub(crate) mod clock;
mod codes;
pub(crate) fn postcard_screen(screen: Screen) -> bool {
match screen {
Screen::Menu
| Screen::StageSelect
| Screen::Settings
| Screen::Controls
| Screen::MatchSetup
| Screen::Achievements
| Screen::Replays
| Screen::Lobby
| Screen::Language
| Screen::Interlude
| Screen::NewVersion => true,
Screen::Puzzle | Screen::Versus | Screen::Editor => false,
}
}
mod conditions;
mod controls;
mod countdown;
mod creatures;
mod cursor;
mod cycle;
mod daily;
mod dev;
mod editor;
mod effects;
mod embedded;
mod failsafe;
mod gamepad;
mod hint;
mod hud;
pub(crate) mod i18n;
mod keycaps;
mod keymap;
mod language;
pub mod layout;
mod lobby;
mod play_input;
pub mod match_setup;
mod menu_scene;
mod menu_ui;
pub mod net;
mod open;
pub mod palette;
pub(crate) mod paths;
mod pause;
pub mod progress;
pub mod replays;
mod results;
mod schedule;
mod session;
mod settings;
mod side_panels;
mod sim_events;
mod spectators;
mod stage_select;
mod suspend;
mod teams;
mod tournament;
mod typing;
pub mod update;
mod watch;
pub use campaign::{Campaign, CampaignKind, Coop};
pub use daily::Daily;
pub use schedule::run;
pub fn watch_file(replay: Replay) {
schedule::run_with(watch::file(replay));
}
pub fn watch_live(feed: std::sync::mpsc::Receiver<crate::bots::game::Feed>) {
schedule::run_with(watch::live(feed));
}
use crate::app::i18n::fill;
use crate::sim::{
Board, BotLevel, Level, MAX_PLAYERS, PlayerAction, PuzzleOutcome, Replay, bot_action,
castle_spots, classic_arena, classic_arena_seeded, generate_arena,
};
use bevy::app::{TaskPoolOptions, TaskPoolPlugin, TaskPoolThreadAssignmentPolicy};
use bevy::prelude::*;
use bevy::render::RenderPlugin;
use bevy::render::settings::{InstanceFlags, RenderCreation, WgpuSettings};
pub fn replay_path() -> std::path::PathBuf {
replays::library_dir().join("last.txt")
}
pub fn highlight_path() -> std::path::PathBuf {
replays::library_dir().join("highlight.gif")
}
#[derive(Resource, Default)]
pub struct Highlight(pub Option<String>);
#[derive(Resource)]
pub struct Sim(pub Board);
#[derive(Resource, Default)]
pub struct PendingActions(pub [PlayerAction; MAX_PLAYERS]);
#[derive(Resource, Default)]
pub struct Paused(pub bool);
#[derive(Resource, Default)]
pub struct Recorder(pub Option<Replay>);
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum SeatController {
#[default]
Local,
Remote,
Ai(BotLevel),
Bot,
}
impl SeatController {
pub fn ai(self) -> Option<BotLevel> {
match self {
SeatController::Ai(level) => Some(level),
SeatController::Local | SeatController::Remote | SeatController::Bot => None,
}
}
}
#[derive(Resource, Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct Controllers(pub [SeatController; MAX_PLAYERS]);
impl Controllers {
pub fn ai(&self, seat: usize) -> Option<BotLevel> {
self.0.get(seat).copied().and_then(SeatController::ai)
}
pub fn ai_count(&self) -> usize {
self.0.iter().filter(|c| c.ai().is_some()).count()
}
pub fn machine_count(&self) -> usize {
self.0
.iter()
.filter(|c| c.ai().is_some() || **c == SeatController::Bot)
.count()
}
pub fn levels(&self) -> [Option<BotLevel>; MAX_PLAYERS] {
self.0.map(|c| match c {
SeatController::Bot => Some(bot_seats::STAND_IN),
SeatController::Ai(level) => Some(level),
SeatController::Local | SeatController::Remote => None,
})
}
pub fn from_levels(levels: [Option<BotLevel>; MAX_PLAYERS]) -> Controllers {
Controllers(levels.map(|level| level.map_or(SeatController::Local, SeatController::Ai)))
}
}
#[derive(Resource, Default)]
pub struct Playback(pub Option<(Replay, usize)>);
#[derive(States, Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Screen {
#[default]
Menu,
Puzzle,
Versus,
Editor,
Lobby,
Settings,
Controls,
MatchSetup,
Achievements,
StageSelect,
Replays,
Interlude,
Language,
NewVersion,
}
#[derive(Clone, Copy)]
pub struct Chrome {
pub width: f32,
pub top: f32,
pub bottom: f32,
}
impl Chrome {
const fn of(width: f32, top: f32, bottom: f32) -> Chrome {
Chrome { width, top, bottom }
}
}
impl Screen {
pub const ALL: [Screen; 14] = [
Screen::Menu,
Screen::Puzzle,
Screen::Versus,
Screen::Editor,
Screen::Lobby,
Screen::Settings,
Screen::Controls,
Screen::MatchSetup,
Screen::Achievements,
Screen::StageSelect,
Screen::Replays,
Screen::Interlude,
Screen::Language,
Screen::NewVersion,
];
fn chrome(self) -> Chrome {
match self {
Screen::Menu => Chrome::of(0.0, 0.0, 0.0),
Screen::Versus => Chrome::of(2.0 * side_panels::SIDEBAR_W + 60.0, ABOVE_BOARD, 104.0),
Screen::Puzzle
| Screen::Editor
| Screen::Lobby
| Screen::Settings
| Screen::Controls
| Screen::MatchSetup
| Screen::Achievements
| Screen::StageSelect
| Screen::Replays
| Screen::Interlude
| Screen::Language
| Screen::NewVersion => Chrome::of(40.0, ABOVE_BOARD, 104.0),
}
}
}
const ABOVE_BOARD: f32 = menu_ui::HEADER_H + 18.0;
#[derive(States, Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Phase {
#[default]
Setup,
Running,
Won,
Lost,
}
#[derive(States, Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum VersusPhase {
#[default]
Countdown,
Running,
Over,
}
#[derive(Resource)]
pub struct Sandbox(pub bool);
#[derive(Message)]
pub struct LoadLevel {
pub keep_posts: bool,
}
#[derive(Message)]
pub struct PlacementDenied {
pub player: u8,
pub out_of_signposts: bool,
}
#[derive(Message)]
pub struct LevelSaved;
#[derive(Message)]
pub struct CodeShared;
#[derive(Message)]
pub struct CodeTaken;
#[derive(Resource, Default)]
pub struct Seats(pub u8);
#[derive(Resource, Default)]
pub struct SeatNames(pub [String; MAX_PLAYERS]);
#[derive(Resource, Default, Clone, Copy, PartialEq, Eq, Debug)]
pub struct SeatKinds(pub [crate::sim::SeatKind; MAX_PLAYERS]);
impl SeatKinds {
pub fn bot(&self, seat: u8) -> bool {
self.0.get(usize::from(seat)) == Some(&crate::sim::SeatKind::Bot)
}
pub fn of(controllers: &Controllers) -> SeatKinds {
SeatKinds(controllers.0.map(|c| match c {
SeatController::Local | SeatController::Remote => crate::sim::SeatKind::Human,
SeatController::Ai(_) => crate::sim::SeatKind::Ai,
SeatController::Bot => crate::sim::SeatKind::Bot,
}))
}
}
impl SeatNames {
pub fn label(&self, tr: &i18n::Tr, seat: u8) -> String {
name_or_label(&self.0, tr, seat)
}
}
pub fn seat_label(tr: &i18n::Tr, seat: u8) -> String {
fill(tr.player_label, &[("p", &(seat + 1).to_string())])
}
pub fn name_or_label(names: &[String], tr: &i18n::Tr, seat: u8) -> String {
match names.get(usize::from(seat)) {
Some(name) if !name.is_empty() => name.clone(),
_ => seat_label(tr, seat),
}
}
#[derive(Resource, Default)]
pub struct Resuming(pub Option<suspend::Suspended>);
#[derive(Resource, Default)]
pub struct RoundNotice(pub String);
#[cfg(test)]
mod chrome_tests {
use super::*;
#[test]
fn the_header_never_sits_on_the_board() {
assert_eq!(Screen::Menu.chrome().top, 0.0);
for screen in [
Screen::Versus,
Screen::Puzzle,
Screen::Editor,
Screen::Lobby,
Screen::Settings,
Screen::Controls,
Screen::MatchSetup,
Screen::Achievements,
Screen::StageSelect,
Screen::Replays,
Screen::Interlude,
Screen::Language,
Screen::NewVersion,
] {
assert!(
screen.chrome().top >= menu_ui::HEADER_H,
"{screen:?} reserves {} under a header of {}",
screen.chrome().top,
menu_ui::HEADER_H
);
}
}
}