Skip to main content

game_toolkit_platformer/sim/
grid.rs

1//! [`Grid`]: a tilemap of your own, one answer per tile, turned into the [`Terrain`]
2//! rules the movement relies on (solid ground, one-way platforms held only from exactly
3//! above, the surface under the feet or beside the body) by [`GridTerrain`]. [`Level`]
4//! answers by the same rules.
5//!
6//! ```
7//! use game_toolkit_platformer::sim::{CustomTile, Grid, GridTerrain, Rect, Surface, Terrain};
8//!
9//! /// The game's own map: `#` ground, `~` ice, `=` a one-way platform.
10//! struct Map(Vec<&'static [u8]>);
11//!
12//! impl Grid for Map {
13//!     fn tile_size(&self) -> i32 {
14//!         16
15//!     }
16//!     fn tile(&self, tx: i32, ty: i32) -> CustomTile {
17//!         let at = usize::try_from(ty).ok().and_then(|y| self.0.get(y));
18//!         match at.zip(usize::try_from(tx).ok()).and_then(|(row, x)| row.get(x)) {
19//!             Some(b'#') => CustomTile::solid(Surface::DEFAULT),
20//!             Some(b'~') => CustomTile::solid(Surface { friction: 0.1, ..Surface::DEFAULT }),
21//!             Some(b'=') => CustomTile::platform(Surface::DEFAULT),
22//!             _ => CustomTile::default(),
23//!         }
24//!     }
25//! }
26//!
27//! let map = Map(vec![b"..==", b"....", b"##~~"]);
28//! let view = GridTerrain(&map); // borrows: the game keeps its map
29//! let body = |x, y| Rect { x, y, w: 12, h: 24 };
30//! assert!(view.solid(body(0, 10)) && !view.solid(body(0, 8)));
31//! assert!(view.platform(body(34, -24)), "standing exactly on the platform");
32//! assert!(!view.platform(body(34, -20)), "not from inside it");
33//! let feet = Rect { x: 40, y: 32, w: 12, h: 1 }; // the row under a body standing on ice
34//! assert_eq!(view.surface(feet).friction, 0.1);
35//! ```
36
37use super::{CustomTile, Hold, Level, Rect, Surface, Terrain};
38
39/// A tilemap: its tile size and what each tile is to the movement. Write these two;
40/// [`GridTerrain`] turns them into a [`Terrain`], and the provided methods answer the
41/// way [`Level`] does.
42pub trait Grid {
43    /// Tile size in the terrain's space (pixels with a `Builder::scale`).
44    fn tile_size(&self) -> i32;
45
46    /// What the tile at `tx, ty` is to the movement: air (the default), solid, or a
47    /// one-way platform, with its [`Surface`]. Outside the map, air (or solid, to wall it
48    /// in).
49    fn tile(&self, tx: i32, ty: i32) -> CustomTile;
50
51    /// Whether `r` has fallen out of the map (a pit): never, unless you say so.
52    fn fell_out(&self, _r: Rect) -> bool {
53        false
54    }
55
56    /// The tiles `r` overlaps: the first and last column and row.
57    fn span(&self, r: Rect) -> (i32, i32, i32, i32) {
58        let t = self.tile_size();
59        (
60            r.x.div_euclid(t),
61            r.y.div_euclid(t),
62            (r.right() - 1).div_euclid(t),
63            (r.bottom() - 1).div_euclid(t),
64        )
65    }
66
67    /// Whether `r` overlaps a solid tile (platforms don't count).
68    fn overlaps_solid(&self, r: Rect) -> bool {
69        let (tx0, ty0, tx1, ty1) = self.span(r);
70        (ty0..=ty1).any(|ty| (tx0..=tx1).any(|tx| self.tile(tx, ty).hold == Hold::Solid))
71    }
72
73    /// Whether `r` stands exactly on top of a one-way platform: its bottom edge on the
74    /// tile row's top, a platform tile under any of its columns.
75    fn on_platform(&self, r: Rect) -> bool {
76        let t = self.tile_size();
77        if r.bottom().rem_euclid(t) != 0 {
78            return false;
79        }
80        let ty = r.bottom().div_euclid(t);
81        let (tx0, _, tx1, _) = self.span(r);
82        (tx0..=tx1).any(|tx| self.tile(tx, ty).hold == Hold::Platform)
83    }
84
85    /// The surface `r` touches (the row under the feet, the column beside a wall): of the
86    /// holding tile under `r`'s center if there is one, else of the first holding tile it
87    /// overlaps (row by row), else [`Surface::DEFAULT`].
88    fn surface_of(&self, r: Rect) -> Surface {
89        let t = self.tile_size();
90        let holding = |tx, ty| {
91            let c = self.tile(tx, ty);
92            (c.hold != Hold::Air).then_some(c.surface)
93        };
94        let (cx, cy) = (r.x + r.w / 2, r.y + r.h / 2);
95        if let Some(s) = holding(cx.div_euclid(t), cy.div_euclid(t)) {
96            return s;
97        }
98        let (tx0, ty0, tx1, ty1) = self.span(r);
99        (ty0..=ty1)
100            .flat_map(|ty| (tx0..=tx1).map(move |tx| (tx, ty)))
101            .find_map(|(tx, ty)| holding(tx, ty))
102            .unwrap_or_default()
103    }
104}
105
106impl<G: Grid + ?Sized> Grid for &G {
107    fn tile_size(&self) -> i32 {
108        (**self).tile_size()
109    }
110
111    fn tile(&self, tx: i32, ty: i32) -> CustomTile {
112        (**self).tile(tx, ty)
113    }
114
115    fn fell_out(&self, r: Rect) -> bool {
116        (**self).fell_out(r)
117    }
118}
119
120/// A [`Grid`] as a [`Terrain`]: solid tiles, one-way platforms, surfaces and pits by the
121/// grid's answers. Wrap a borrow of your map (`GridTerrain(&map)`) to step through it
122/// each frame, or own it.
123///
124/// It builds on nothing and isn't findable by `find_mut` (it may borrow). An ability
125/// that needs your map: write the [`Terrain`] yourself, forwarding to the grid's
126/// [`Grid::overlaps_solid`], [`Grid::on_platform`] and [`Grid::surface_of`].
127#[derive(Clone, Copy, Debug)]
128pub struct GridTerrain<G>(pub G);
129
130impl<G: Grid> Terrain for GridTerrain<G> {
131    fn solid(&self, r: Rect) -> bool {
132        self.0.overlaps_solid(r)
133    }
134
135    fn platform(&self, r: Rect) -> bool {
136        self.0.on_platform(r)
137    }
138
139    fn surface(&self, r: Rect) -> Surface {
140        self.0.surface_of(r)
141    }
142
143    fn fell_out(&self, r: Rect) -> bool {
144        self.0.fell_out(r)
145    }
146
147    crate::terrain_layer!(borrowed);
148}
149
150/// A [`Level`]'s tiles as a [`Grid`], so it answers by the same rules.
151pub(crate) struct LevelGrid<'a>(pub &'a Level);
152
153impl Grid for LevelGrid<'_> {
154    fn tile_size(&self) -> i32 {
155        self.0.tile_size()
156    }
157
158    fn tile(&self, tx: i32, ty: i32) -> CustomTile {
159        self.0.tile_kind(tx, ty)
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use crate::sim::Tile;
167
168    /// A level's tiles, through the public [`Grid`] only.
169    struct Mirror(Level);
170
171    impl Grid for Mirror {
172        fn tile_size(&self) -> i32 {
173            self.0.tile_size()
174        }
175
176        fn tile(&self, tx: i32, ty: i32) -> CustomTile {
177            match self.0.tile(tx, ty) {
178                Tile::Solid => CustomTile::solid(Surface::DEFAULT),
179                Tile::Platform => CustomTile::platform(Surface::DEFAULT),
180                Tile::Custom(id) => self.0.custom_tile(id),
181                Tile::Empty => CustomTile::default(),
182            }
183        }
184    }
185
186    #[test]
187    fn a_grid_answers_like_the_equivalent_level() {
188        let mut level =
189            Level::parse_with(
190                &["....==..", ".~~.....", "##ii##==", "........"],
191                |c| match c {
192                    '~' => Some(Tile::Custom(1)),
193                    'i' => Some(Tile::Custom(2)),
194                    _ => None,
195                },
196            );
197        let ice = Surface {
198            friction: 0.1,
199            ..Surface::DEFAULT
200        };
201        level.define(1, CustomTile::platform(Surface { bounce: 0.5, ..ice }));
202        level.define(2, CustomTile::solid(ice));
203        let grid = GridTerrain(Mirror(level.clone()));
204        for y in -14..34 {
205            for x in -8..66 {
206                for (w, h) in [(6, 12), (3, 3), (12, 2)] {
207                    let r = Rect { x, y, w, h };
208                    assert_eq!(grid.solid(r), level.solid(r), "{r:?}");
209                    assert_eq!(grid.platform(r), level.platform(r), "{r:?}");
210                    assert_eq!(grid.surface(r), level.surface(r), "{r:?}");
211                }
212            }
213        }
214    }
215}