use crate::app::editor;
use crate::sim::{Level, LevelKind};
use bevy::prelude::*;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum CampaignKind {
TidePool,
BeachDay,
}
impl CampaignKind {
pub fn key(self) -> &'static str {
match self {
CampaignKind::TidePool => "tide",
CampaignKind::BeachDay => "beach",
}
}
}
#[derive(Resource)]
pub struct Campaign {
pub kind: CampaignKind,
pub levels: Vec<Level>,
pub index: usize,
pub builtins: usize,
}
impl Campaign {
pub fn current(&self) -> &Level {
&self.levels[self.index]
}
pub(crate) fn reset(&mut self, kind: CampaignKind, levels: Vec<Level>, builtins: usize) {
let builtins = builtins.min(levels.len());
*self = Campaign {
kind,
levels,
index: 0,
builtins,
};
}
}
pub(crate) fn tide_pool_levels() -> (Vec<Level>, usize) {
let mut levels = crate::sim::campaign_levels();
let builtins = levels.len();
levels.extend(custom_puzzles(load_custom_levels()));
disambiguate(&mut levels, builtins);
(levels, builtins)
}
pub(crate) fn disambiguate(levels: &mut [Level], builtins: usize) {
let mut taken: std::collections::HashSet<String> = levels[..builtins.min(levels.len())]
.iter()
.map(|level| level.name.clone())
.collect();
for level in levels.iter_mut().skip(builtins) {
if taken.insert(level.name.clone()) {
continue;
}
let renamed = (2..)
.map(|n| format!("{} ({n})", level.name))
.find(|candidate| !taken.contains(candidate))
.unwrap_or_else(|| level.name.clone());
taken.insert(renamed.clone());
level.name = renamed;
}
}
pub(crate) fn custom_puzzles(levels: Vec<Level>) -> Vec<Level> {
levels
.into_iter()
.filter(|level| level.kind == LevelKind::Puzzle && level.crab_count() > 0)
.collect()
}
pub(crate) fn custom_arenas(levels: Vec<Level>) -> Vec<Level> {
levels
.into_iter()
.filter(|level| level.kind == LevelKind::Arena)
.collect()
}
pub(crate) fn load_custom_levels() -> Vec<Level> {
let mut paths = vec![editor::legacy_save_path()];
if let Ok(dir) = std::fs::read_dir(editor::custom_dir()) {
let mut extra: Vec<_> = dir
.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| p.extension().is_some_and(|ext| ext == "txt"))
.collect();
extra.sort();
paths.extend(extra);
}
let mut levels = Vec::new();
for path in paths {
let Ok(text) = std::fs::read_to_string(&path) else {
continue; };
match Level::parse(&text) {
Ok(level) => {
info!(
"loaded custom {} {:?} from {}",
level.kind.token(),
level.name,
path.display()
);
if level.kind == LevelKind::Puzzle && level.crab_count() == 0 {
warn!("{}: a puzzle with no crabs to route", path.display());
}
if level.kind == LevelKind::Arena && level.seats() < 2 {
warn!(
"{}: a beach with castles for {} - no table can sit at it",
path.display(),
level.seats()
);
}
levels.push(level);
}
Err(e) => warn!("skipping {}: {e}", path.display()),
}
}
levels
}
#[cfg(test)]
mod tests {
use super::*;
fn level(name: &str, kind: &str, crabs: bool) -> Level {
let crab = if crabs { "crab: 0,0 R L common\n" } else { "" };
Level::parse(&format!(
"name: {name}\nposts: 2\nkind: {kind}\n{crab}map:\n+-+-+-+\n|0 . 1|\n+-+-+-+\n"
))
.expect("a level")
}
#[test]
fn the_shelf_splits_by_kind() {
let shelf = || {
vec![
level("Stage", "puzzle", true),
level("Beach", "arena", true),
level("Empty Beach", "arena", false),
level("Crabless", "puzzle", false),
]
};
let names = |levels: Vec<Level>| -> Vec<String> {
levels.into_iter().map(|level| level.name).collect()
};
assert_eq!(names(custom_puzzles(shelf())), ["Stage"]);
assert_eq!(names(custom_arenas(shelf())), ["Beach", "Empty Beach"]);
}
#[test]
fn shipped_level_names_are_unique() {
for levels in [
crate::sim::campaign_levels(),
crate::sim::challenge_levels(),
] {
let mut seen = std::collections::HashSet::new();
for level in &levels {
assert!(seen.insert(level.name.clone()), "{:?} twice", level.name);
}
}
}
#[test]
fn same_named_player_levels_are_told_apart() {
let mut levels = vec![
level("Welcome Ashore", "puzzle", true),
level("Gull Alley", "puzzle", true),
level("Welcome Ashore", "puzzle", true),
level("Mine", "puzzle", true),
level("Mine", "puzzle", true),
level("Mine", "puzzle", true),
level("Mine (2)", "puzzle", true),
];
disambiguate(&mut levels, 2);
let names: Vec<&str> = levels.iter().map(|level| level.name.as_str()).collect();
assert_eq!(
names,
[
"Welcome Ashore",
"Gull Alley",
"Welcome Ashore (2)",
"Mine",
"Mine (2)",
"Mine (3)",
"Mine (2) (2)",
]
);
}
}