game_toolkit_platformer/sim/terrain.rs
1//! [`Terrain`]: what the movement asks about the world around the character. Your game
2//! implements it for its own level (a tilemap, a collision grid, anything): write
3//! [`Terrain::solid`] and say what it builds on with [`terrain_layer!`](crate::terrain_layer)
4//! (`terrain_layer!()` for nothing, `terrain_layer!(level)` for a field it wraps).
5//!
6//! Positions and sizes are in the controller's units: the body is 6x12 unless
7//! [`super::Tuning::size`] says otherwise, the tuning is made for 8-unit tiles. If your
8//! tiles are 16 pixels, give the [`crate::Builder`] a scale of 2
9//! ([`crate::Builder::scale`]) and answer in your pixels, or use [`super::Level`], which
10//! already speaks units.
11//!
12//! Hazards, pickups and checkpoints aren't the terrain's business: test the character's
13//! hitboxes against them after each update (`docs/hazards.md` has the recipe). Dash
14//! refills are the one exception, because the movement decides when one is used (only
15//! when a dash has been spent): [`Terrain::refill`].
16//!
17//! To start the character standing on your ground, give it the feet's position:
18//! `Platformer::at(Spot::Feet(x, floor_y), preset)` (see [`super::Spot`]).
19//!
20//! The smallest terrain is a closure (it builds on nothing):
21//!
22//! ```
23//! use game_toolkit_platformer::sim::{Rect, Terrain};
24//!
25//! // Solid below y = 100, nothing else.
26//! let mut floor = |r: Rect| r.bottom() > 100;
27//! assert!(floor.solid(Rect { x: 0, y: 95, w: 6, h: 12 }));
28//! ```
29//!
30//! # Building on another terrain
31//!
32//! To add state of your own to a terrain (doors, switches, moving platforms, boxes),
33//! wrap it and name it as the [`Terrain::base`] with [`terrain_layer!`](crate::terrain_layer):
34//! every method you don't write forwards to it, those added later included, and
35//! [`find_mut`](trait.Terrain.html#method.find_mut) finds your type. Write only what
36//! changes; your own state moves in [`Terrain::tick`], which runs on every layer by
37//! itself, so the base's refills come back whatever you write:
38//!
39//! ```
40//! use game_toolkit_platformer::sim::{Level, Rect, Terrain};
41//!
42//! /// The demo level with a door that opens and shuts every second.
43//! struct Stage {
44//! level: Level,
45//! door: Rect,
46//! clock: f32,
47//! }
48//!
49//! impl Terrain for Stage {
50//! fn solid(&self, r: Rect) -> bool {
51//! let shut = self.clock % 2.0 < 1.0;
52//! self.level.solid(r) || (shut && self.door.intersects(r))
53//! }
54//! fn tick(&mut self, dt: f32, _bodies: &[Rect]) {
55//! self.clock += dt; // the level's refills come back on their own
56//! }
57//! // Platforms, refills, surfaces, falling out: the level's.
58//! game_toolkit_platformer::terrain_layer!(level);
59//! }
60//! # let mut s = Stage { level: Level::demo(), door: Rect { x: 0, y: 0, w: 8, h: 8 }, clock: 0.0 };
61//! # assert!(s.solid(Rect { x: 1, y: 1, w: 2, h: 2 }) && !s.fell_out(Rect { x: 9, y: 9, w: 2, h: 2 }));
62//! # let t: &mut dyn Terrain = &mut s;
63//! # assert!(t.find_mut::<Stage>().is_some() && t.find_mut::<Level>().is_some());
64//! ```
65//!
66//! A terrain that borrows (a view of your game's map, `View<'a>`) writes
67//! `terrain_layer!(borrowed)` or `terrain_layer!(borrowed field)`: the plain forms make it
68//! findable by [`find_mut`](trait.Terrain.html#method.find_mut), which needs `'static`, and
69//! fail with "lifetime may not live long enough" otherwise. See
70//! [`terrain_layer!`](crate::terrain_layer).
71//!
72//! [`Terrain::base`] has no default, so a wrapper can't forget it: without it, the level's
73//! platforms, refills, surfaces and pits would silently be gone. This doesn't compile:
74//!
75//! ```compile_fail,E0046
76//! use game_toolkit_platformer::sim::{Level, Rect, Terrain};
77//!
78//! struct Doors { level: Level }
79//! impl Terrain for Doors {
80//! fn solid(&self, r: Rect) -> bool { self.level.solid(r) }
81//! // missing: game_toolkit_platformer::terrain_layer!(level);
82//! }
83//! ```
84
85mod base_ref;
86
87use std::any::Any;
88
89#[doc(hidden)]
90pub use base_ref::BaseRef;
91
92use super::{Rect, Surface};
93
94/// The world around the character, as the movement sees it.
95///
96/// Write [`Terrain::solid`] and [`Terrain::base`] (with [`terrain_layer!`](crate::terrain_layer));
97/// every other method has a default: forward to the base if there is one, otherwise there
98/// is nothing of the kind (no platforms, no refills, ...).
99pub trait Terrain {
100 /// Whether `r` overlaps anything solid (walls, floors, ceilings, moving solids).
101 fn solid(&self, r: Rect) -> bool;
102
103 /// Whether `r` stands exactly on top of a one-way platform (its bottom edge on the
104 /// platform's top). One-way platforms hold from above, let the character jump up
105 /// through them, and drop it on down + jump.
106 fn platform(&self, r: Rect) -> bool {
107 self.base().is_some_and(|b| b.platform(r))
108 }
109
110 /// The character's body `r` touches a dash refill: use it up and return `true` (it's
111 /// only asked when a dash is spent).
112 fn refill(&mut self, r: Rect) -> bool {
113 self.base_mut().is_some_and(|b| b.refill(r))
114 }
115
116 /// Whether `r` is out of the world (fell into a pit).
117 fn fell_out(&self, r: Rect) -> bool {
118 self.base().is_some_and(|b| b.fell_out(r))
119 }
120
121 /// What kind of ground or wall `r` touches: the row under the feet while standing, the
122 /// column beside the body at a wall. Ice, conveyor belts, walls that can't be climbed,
123 /// bouncy pads (see [`Surface`]). Default: the base's, or [`Surface::DEFAULT`].
124 fn surface(&self, r: Rect) -> Surface {
125 self.base().map_or(Surface::DEFAULT, |b| b.surface(r))
126 }
127
128 /// This layer's own update, once per step after the characters moved, with their
129 /// `bodies` (one, unless several share the terrain through a [`crate::Party`]): let
130 /// things fall onto them, refills come back, platforms move. Only this layer's state:
131 /// [`Terrain::update`] runs the bases' ticks by itself. Default: nothing.
132 fn tick(&mut self, _dt: f32, _bodies: &[Rect]) {}
133
134 /// The whole terrain's update: [`Terrain::tick`] on this layer, then on every base
135 /// down the chain. Don't write it; write [`Terrain::tick`]. Only an adapter that hands
136 /// its base something else than it got (converted bodies, say) writes it, and then
137 /// calls the base's `update`.
138 fn update(&mut self, dt: f32, bodies: &[Rect]) {
139 debug_assert!(
140 same_base(self),
141 "Terrain::base and Terrain::base_mut must return the same terrain"
142 );
143 self.tick(dt, bodies);
144 if let Some(b) = self.base_mut() {
145 b.update(dt, bodies);
146 }
147 }
148
149 /// How far the solid (or one-way platform) overlapping `r` moved in the last
150 /// [`Terrain::update`], in units: moving platforms, elevators.
151 ///
152 /// Asked right after `update` with what the character rode before it: the row just
153 /// under its feet while standing, the column beside it while holding on to a wall.
154 /// The world then carries it along by as much, stopping at walls and ceilings, and a
155 /// jump off keeps that speed ([`super::Tuning::lift_boost`]). Moving solids don't push
156 /// the character otherwise: keep them from moving into it from the side or above
157 /// (`docs/moving-platforms.md` has a whole one).
158 fn carry(&self, r: Rect) -> (f32, f32) {
159 self.base().map_or((0.0, 0.0), |b| b.carry(r))
160 }
161
162 /// The terrain this one builds on, which the defaults forward to, or `None`.
163 /// [`terrain_layer!`](crate::terrain_layer) writes it with [`Terrain::base_mut`] and
164 /// [`Terrain::as_any_mut`]. Required, so a wrapper can't lose its base's platforms,
165 /// refills and pits by forgetting it.
166 fn base(&self) -> Option<&dyn Terrain>;
167
168 /// [`Terrain::base`], to change. Return the same terrain from both (checked in debug
169 /// builds).
170 fn base_mut(&mut self) -> Option<&mut dyn Terrain> {
171 None
172 }
173
174 /// This terrain as itself, so an [`Ability`](super::Ability) can reach your type
175 /// through [`find_mut`](trait.Terrain.html#method.find_mut) (break a block, flip a
176 /// switch): `Some(self)`, which [`terrain_layer!`](crate::terrain_layer) writes.
177 /// Default: `None`, it isn't found (its [`Terrain::base`] still is).
178 fn as_any_mut(&mut self) -> Option<&mut dyn Any> {
179 None
180 }
181
182 /// [`Terrain::as_any_mut`], to read: what [`find`](trait.Terrain.html#method.find)
183 /// looks at. [`terrain_layer!`](crate::terrain_layer) writes both. Default: `None`.
184 fn as_any(&self) -> Option<&dyn Any> {
185 None
186 }
187}
188
189/// Whether `t`'s [`Terrain::base`] and [`Terrain::base_mut`] are the same terrain.
190fn same_base<T: Terrain + ?Sized>(t: &mut T) -> bool {
191 let shared = t.base().map(|b| b as *const dyn Terrain as *const ());
192 let unique = t.base_mut().map(|b| b as *const dyn Terrain as *const ());
193 shared == unique
194}
195
196/// Writes the boilerplate of a [`Terrain`] layer inside its `impl Terrain` block:
197/// [`Terrain::base`] and [`Terrain::base_mut`] returning the field named (a name or a
198/// tuple index; left out: no base), and [`Terrain::as_any_mut`] and [`Terrain::as_any`] returning the
199/// terrain itself, so [`find_mut`](trait.Terrain.html#method.find_mut) finds it.
200///
201/// A terrain that borrows (has a lifetime) can't be found, since `find_mut` needs
202/// `'static`; the plain forms then fail with "lifetime may not live long enough". Write
203/// `terrain_layer!(borrowed)` (no base) or `terrain_layer!(borrowed field)` instead; its
204/// base is still found. In every form the base field may be the terrain itself, a `&mut`
205/// to one, or a `Box<dyn Terrain>`:
206///
207/// ```
208/// use game_toolkit_platformer::sim::{Level, Rect, Terrain};
209///
210/// struct Map { floor: i32 }
211/// struct View<'a> { map: &'a Map }
212/// impl Terrain for View<'_> {
213/// fn solid(&self, r: Rect) -> bool { r.bottom() > self.map.floor }
214/// game_toolkit_platformer::terrain_layer!(borrowed);
215/// }
216///
217/// struct Doors<'a> { level: &'a mut Level, shut: bool }
218/// impl Terrain for Doors<'_> {
219/// fn solid(&self, r: Rect) -> bool { self.level.solid(r) || (self.shut && r.x < 0) }
220/// game_toolkit_platformer::terrain_layer!(borrowed level);
221/// }
222///
223/// struct Lit<'a> { level: Level, map: &'a Map }
224/// impl Terrain for Lit<'_> {
225/// fn solid(&self, r: Rect) -> bool { self.level.solid(r) || r.bottom() > self.map.floor }
226/// game_toolkit_platformer::terrain_layer!(borrowed level);
227/// }
228///
229/// struct Any { inner: Box<dyn Terrain> }
230/// impl Terrain for Any {
231/// fn solid(&self, r: Rect) -> bool { self.inner.solid(r) }
232/// game_toolkit_platformer::terrain_layer!(inner);
233/// }
234/// # let map = Map { floor: 100 };
235/// # let mut lit = Lit { level: Level::demo(), map: &map };
236/// # assert!((&mut lit as &mut dyn Terrain).find_mut::<Level>().is_some());
237/// # let mut any = Any { inner: Box::new(Level::demo()) };
238/// # assert!((&mut any as &mut dyn Terrain).find_mut::<Level>().is_some());
239/// # assert!(View { map: &map }.solid(Rect { x: 0, y: 95, w: 6, h: 12 }));
240/// # let mut level = Level::demo();
241/// # let mut d = Doors { level: &mut level, shut: true };
242/// # assert!(!d.fell_out(Rect { x: 9, y: 9, w: 2, h: 2 }));
243/// # let t: &mut dyn Terrain = &mut d;
244/// # assert!(t.find_mut::<Level>().is_some());
245/// ```
246///
247/// ```
248/// use game_toolkit_platformer::sim::{Level, Rect, Terrain};
249///
250/// struct Doors { level: Level }
251/// impl Terrain for Doors {
252/// fn solid(&self, r: Rect) -> bool { self.level.solid(r) }
253/// game_toolkit_platformer::terrain_layer!(level);
254/// }
255///
256/// struct Room;
257/// impl Terrain for Room {
258/// fn solid(&self, r: Rect) -> bool { r.bottom() > 100 }
259/// game_toolkit_platformer::terrain_layer!(); // no base; still found by find_mut
260/// }
261/// # let t: &mut dyn Terrain = &mut Room;
262/// # assert!(t.find_mut::<Room>().is_some());
263/// ```
264#[macro_export]
265macro_rules! terrain_layer {
266 () => {
267 fn base(&self) -> ::core::option::Option<&dyn $crate::sim::Terrain> {
268 ::core::option::Option::None
269 }
270 $crate::terrain_layer!(@any);
271 };
272 (@any) => {
273 fn as_any_mut(&mut self) -> ::core::option::Option<&mut dyn ::core::any::Any> {
274 ::core::option::Option::Some(self)
275 }
276 fn as_any(&self) -> ::core::option::Option<&dyn ::core::any::Any> {
277 ::core::option::Option::Some(self)
278 }
279 };
280 (borrowed) => {
281 fn base(&self) -> ::core::option::Option<&dyn $crate::sim::Terrain> {
282 ::core::option::Option::None
283 }
284 };
285 (borrowed $base:tt) => {
286 fn base(&self) -> ::core::option::Option<&dyn $crate::sim::Terrain> {
287 ::core::option::Option::Some({ use $crate::sim::terrain::BaseRef as _; self.$base.__terrain_base() })
288 }
289 fn base_mut(&mut self) -> ::core::option::Option<&mut dyn $crate::sim::Terrain> {
290 ::core::option::Option::Some({ use $crate::sim::terrain::BaseRef as _; self.$base.__terrain_base_mut() })
291 }
292 };
293 ($base:tt) => {
294 fn base(&self) -> ::core::option::Option<&dyn $crate::sim::Terrain> {
295 ::core::option::Option::Some({ use $crate::sim::terrain::BaseRef as _; self.$base.__terrain_base() })
296 }
297 fn base_mut(&mut self) -> ::core::option::Option<&mut dyn $crate::sim::Terrain> {
298 ::core::option::Option::Some({ use $crate::sim::terrain::BaseRef as _; self.$base.__terrain_base_mut() })
299 }
300 $crate::terrain_layer!(@any);
301 };
302}
303
304impl dyn Terrain + '_ {
305 /// The terrain of type `T` in this one: itself, or the first of its bases (see
306 /// [`Terrain::base`]) that is a `T` and says so in [`Terrain::as_any_mut`].
307 ///
308 /// ```
309 /// use game_toolkit_platformer::sim::{Level, Terrain, Tile};
310 ///
311 /// let mut level = Level::parse(&["....", "####"]);
312 /// let terrain: &mut dyn Terrain = &mut level;
313 /// terrain.find_mut::<Level>().unwrap().set(1, 1, Tile::Empty);
314 /// assert_eq!(level.tile(1, 1), Tile::Empty);
315 /// ```
316 pub fn find_mut<T: 'static>(&mut self) -> Option<&mut T> {
317 if self.as_any_mut().is_some_and(|a| a.is::<T>()) {
318 return self.as_any_mut()?.downcast_mut();
319 }
320 self.base_mut()?.find_mut()
321 }
322
323 /// [`find_mut`](trait.Terrain.html#method.find_mut), to read: the terrain of type `T`
324 /// in this one, through a shared `&dyn Terrain` (a debug overlay, a minimap).
325 ///
326 /// ```
327 /// use game_toolkit_platformer::sim::{Level, Terrain};
328 ///
329 /// let level = Level::parse(&["....", "####"]);
330 /// let terrain: &dyn Terrain = &level;
331 /// assert_eq!(terrain.find::<Level>().map(Level::width), Some(4));
332 /// ```
333 pub fn find<T: 'static>(&self) -> Option<&T> {
334 match self.as_any().and_then(|a| a.downcast_ref()) {
335 Some(t) => Some(t),
336 None => self.base()?.find(),
337 }
338 }
339}
340
341/// Any `Fn(Rect) -> bool` is a terrain of solid ground, built on nothing.
342impl<F: Fn(Rect) -> bool> Terrain for F {
343 fn solid(&self, r: Rect) -> bool {
344 self(r)
345 }
346 fn base(&self) -> Option<&dyn Terrain> {
347 None
348 }
349}