Skip to main content

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}