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}