use std::collections::BTreeMap;
use super::grid::LevelGrid;
use super::{Crystal, CustomTile, Grid, Hold, Rect, Spot, Surface, Terrain};
pub const TILE: i32 = 8;
const REFILL_BACK: f32 = 2.5;
pub const DEMO: [&str; 27] = [
"#..............................................#",
"#..............................................#",
"#..............................................#",
"#..........................*...................#",
"#..............................................#",
"#.......................=======................#",
"#.......................#.....#................#",
"#.......................#.....#....##..........#",
"#.............=====.....#.....#....##..........#",
"#.............#...#.....#.....#....##.......*..#",
"#.............#...#.....#.....#....##..........#",
"#.............#...#.....#.....#....##..........#",
"#.............#...#.....#.....#....##..........#",
"#####.........#...#..*..#.....#....##..........#",
"#.............#...#.....#.....#...........B....#",
"#.............#...#.....#.....#.........########",
"#....====.....#...#.....#.....#................#",
"#.............#...#.....#.....#................#",
"#.............#...#.....#.....#........===.....#",
"#####.........#...#.....#.....#................#",
"#.............#...#.....#.....#................#",
"#.............#...#..............######........#",
"#....====......................................#",
"#..............................................#",
"#..........P.........B.........................#",
"##########################################...###",
"##########################################...###",
];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Tile {
Empty,
Solid,
Platform,
Custom(u8),
}
#[derive(Clone, Debug, PartialEq)]
pub struct Level {
w: i32,
h: i32,
tiles: Vec<Tile>,
spawn: (i32, i32),
crystals: Vec<Crystal>,
refill_back: f32,
tile_size: i32,
custom: BTreeMap<u8, CustomTile>,
}
impl Level {
pub fn parse(rows: &[&str]) -> Self {
Self::parse_with(rows, |_| None)
}
pub fn parse_with(rows: &[&str], tile: impl Fn(char) -> Option<Tile>) -> Self {
let h = rows.len() as i32;
let w = rows.first().map_or(0, |r| r.chars().count()) as i32;
for (y, row) in rows.iter().enumerate() {
let n = row.chars().count() as i32;
assert_eq!(n, w, "level row {y} is {n} wide, not {w}");
}
let grid: Vec<Vec<char>> = rows.iter().map(|r| r.chars().collect()).collect();
let mut level = Self::from_fn(w, h, |x, y| {
let c = grid[y as usize][x as usize];
tile(c).unwrap_or(match c {
'#' => Tile::Solid,
'=' => Tile::Platform,
_ => Tile::Empty,
})
});
for (y, row) in grid.iter().enumerate() {
for (x, &c) in row.iter().enumerate() {
if tile(c).is_some() {
continue;
}
let at = (x as i32, y as i32);
match c {
'P' => level.spawn = at,
'*' => level.add_refill(at.0, at.1),
_ => {}
}
}
}
level
}
pub fn from_fn(w: i32, h: i32, tile: impl Fn(i32, i32) -> Tile) -> Self {
let tiles = (0..h)
.flat_map(|y| (0..w).map(move |x| (x, y)))
.map(|(x, y)| tile(x, y))
.collect();
Self {
w,
h,
tiles,
spawn: (1, 1),
crystals: Vec::new(),
refill_back: REFILL_BACK,
tile_size: TILE,
custom: BTreeMap::new(),
}
}
pub fn with_tile_size(mut self, size: i32) -> Self {
assert!(size >= 1, "a tile of {size} units");
let old = self.tile_size;
for c in &mut self.crystals {
let a = &mut c.area;
(a.x, a.y) = (a.x.div_euclid(old) * size, a.y.div_euclid(old) * size);
(a.w, a.h) = (size, size);
}
self.tile_size = size;
self
}
pub fn define(&mut self, id: u8, tile: CustomTile) {
self.custom.insert(id, tile);
}
pub fn custom_tile(&self, id: u8) -> CustomTile {
self.custom.get(&id).copied().unwrap_or_default()
}
pub fn width(&self) -> i32 {
self.w
}
pub fn height(&self) -> i32 {
self.h
}
pub fn tile_size(&self) -> i32 {
self.tile_size
}
pub fn spawn_tile(&self) -> (i32, i32) {
self.spawn
}
pub fn set_spawn_tile(&mut self, tx: i32, ty: i32) {
self.spawn = (tx, ty);
}
pub fn refills(&self) -> &[Crystal] {
&self.crystals
}
pub fn add_refill(&mut self, tx: i32, ty: i32) {
self.crystals.push(Crystal::at((tx, ty), self.tile_size));
}
pub fn refill_back(&self) -> f32 {
self.refill_back
}
pub fn set_refill_back(&mut self, seconds: f32) {
self.refill_back = seconds;
}
pub fn find(&self, tile: Tile) -> Option<(i32, i32)> {
let i = self.tiles.iter().position(|&t| t == tile)? as i32;
Some((i % self.w, i / self.w))
}
pub fn demo() -> Self {
Self::parse(&DEMO)
}
pub fn start(&self) -> Spot {
let (sx, sy) = self.spawn;
let t = self.tile_size;
Spot::Feet(sx * t + t / 2, (sy + 1) * t)
}
fn tile_span(&self, r: Rect) -> (i32, i32, i32, i32) {
let t = self.tile_size;
(
r.x.div_euclid(t),
r.y.div_euclid(t),
(r.right() - 1).div_euclid(t),
(r.bottom() - 1).div_euclid(t),
)
}
pub fn tile(&self, tx: i32, ty: i32) -> Tile {
if (0..self.w).contains(&tx) && (0..self.h).contains(&ty) {
self.tiles[(ty * self.w + tx) as usize]
} else {
Tile::Empty
}
}
pub fn set(&mut self, tx: i32, ty: i32, tile: Tile) {
if (0..self.w).contains(&tx) && (0..self.h).contains(&ty) {
self.tiles[(ty * self.w + tx) as usize] = tile;
}
}
pub fn tiles_touching(&self, r: Rect) -> impl Iterator<Item = (i32, i32, Tile)> + '_ {
let (tx0, ty0, tx1, ty1) = self.tile_span(r);
(ty0..=ty1).flat_map(move |ty| (tx0..=tx1).map(move |tx| (tx, ty, self.tile(tx, ty))))
}
pub fn touches(&self, hitboxes: &[Rect], tile: Tile) -> Option<Rect> {
hitboxes
.iter()
.copied()
.find(|&r| self.tiles_touching(r).any(|(_, _, t)| t == tile))
}
pub(crate) fn tile_kind(&self, tx: i32, ty: i32) -> CustomTile {
match self.tile(tx, ty) {
Tile::Solid => CustomTile::solid(Surface::DEFAULT),
Tile::Platform => CustomTile::platform(Surface::DEFAULT),
Tile::Custom(id) => self.custom_tile(id),
Tile::Empty => CustomTile::default(),
}
}
pub fn solid_tile(&self, tx: i32, ty: i32) -> bool {
self.tile_kind(tx, ty).hold == Hold::Solid
}
pub fn platform_tile(&self, tx: i32, ty: i32) -> bool {
self.tile_kind(tx, ty).hold == Hold::Platform
}
pub fn surface_at(&self, r: Rect) -> Surface {
LevelGrid(self).surface_of(r)
}
pub fn overlaps(&self, r: Rect) -> bool {
LevelGrid(self).overlaps_solid(r)
}
pub fn on_platform(&self, r: Rect) -> bool {
LevelGrid(self).on_platform(r)
}
pub fn floor_y(&self) -> i32 {
self.h * self.tile_size
}
}
impl Terrain for Level {
fn solid(&self, r: Rect) -> bool {
self.overlaps(r)
}
fn platform(&self, r: Rect) -> bool {
self.on_platform(r)
}
fn refill(&mut self, r: Rect) -> bool {
let back = self.refill_back;
match self
.crystals
.iter_mut()
.find(|c| c.ready() && c.area.intersects(r))
{
Some(c) => {
c.cooldown = back;
true
}
None => false,
}
}
fn fell_out(&self, r: Rect) -> bool {
r.y > self.floor_y() + 4 * self.tile_size
}
fn surface(&self, r: Rect) -> Surface {
self.surface_at(r)
}
fn tick(&mut self, dt: f32, _bodies: &[Rect]) {
for c in &mut self.crystals {
c.cooldown = (c.cooldown - dt).max(0.0);
}
}
crate::terrain_layer!();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn demo_parses_with_spawn_on_open_ground() {
let l = Level::demo();
assert_eq!((l.w, l.h), (48, 27));
let (sx, sy) = l.spawn;
assert_eq!(l.tile(sx, sy), Tile::Empty);
assert!(l.solid_tile(sx, sy + 1));
assert_eq!(l.crystals.len(), 3);
assert_eq!(l.spawn, (11, 24));
assert_eq!(l.tile(21, 24), Tile::Empty, "box places are air");
}
#[test]
fn overlaps_rounds_negative_coordinates_down() {
let l = Level::parse(&["#.", ".."]);
assert!(l.overlaps(Rect {
x: -3,
y: 0,
w: 4,
h: 4
}));
assert!(!l.overlaps(Rect {
x: 8,
y: 0,
w: 8,
h: 8
}));
assert!(!l.overlaps(Rect {
x: -9,
y: 0,
w: 4,
h: 4
}));
}
#[test]
fn custom_tiles_are_air_and_can_be_found() {
let l = Level::parse_with(&["~~P.", "~##*"], |c| (c == '~').then_some(Tile::Custom(7)));
assert_eq!(l.tile(0, 0), Tile::Custom(7));
assert_eq!(l.spawn, (2, 0), "built-in characters still work");
assert_eq!(l.crystals.len(), 1);
let r = Rect {
x: 0,
y: 0,
w: 8,
h: 16,
};
assert!(!l.overlaps(r), "custom tiles are air");
let found: Vec<_> = l.tiles_touching(r).collect();
assert_eq!(
found,
vec![(0, 0, Tile::Custom(7)), (0, 1, Tile::Custom(7))]
);
let l = Level::parse_with(&["*"], |c| (c == '*').then_some(Tile::Empty));
assert!(l.crystals.is_empty());
}
#[test]
fn refills_added_after_a_new_tile_size_fill_their_tile() {
let mut l = Level::parse(&["*...", "...."]).with_tile_size(16);
l.add_refill(2, 1);
let areas: Vec<Rect> = l.refills().iter().map(|c| c.area).collect();
let tile = |x, y| Rect { x, y, w: 16, h: 16 };
assert_eq!(areas, vec![tile(0, 0), tile(32, 16)]);
assert_eq!((l.width(), l.height(), l.tile_size()), (4, 2, 16));
}
#[test]
fn a_custom_tile_can_be_a_one_way_platform_and_the_spawn() {
let mut l = Level::parse_with(&["..S.", "~~~~"], |c| match c {
'~' => Some(Tile::Custom(3)),
'S' => Some(Tile::Custom(4)),
_ => None,
});
let bouncy = Surface {
bounce: 0.5,
..Surface::DEFAULT
};
l.define(3, CustomTile::platform(bouncy));
let feet = Rect {
x: 2,
y: -4,
w: 4,
h: 12,
};
assert!(l.on_platform(feet) && l.platform_tile(0, 1));
assert!(!l.overlaps(feet.shifted(0, 4)), "not solid");
assert_eq!(l.surface_at(feet.shifted(0, 8)).bounce, 0.5);
assert_eq!(l.find(Tile::Custom(4)), Some((2, 0)));
assert_eq!(l.find(Tile::Solid), None);
l.set_spawn_tile(2, 0);
assert_eq!(l.start(), Spot::Feet(20, 8));
}
#[test]
fn platforms_hold_only_from_exactly_above() {
let l = Level::parse(&["..", "=="]);
let feet_on = Rect {
x: 2,
y: -4,
w: 4,
h: 12,
};
assert!(l.on_platform(feet_on));
assert!(!l.on_platform(feet_on.shifted(0, 1)));
assert!(!l.overlaps(feet_on.shifted(0, 4)));
}
}