Skip to main content

game_toolkit_platformer/
sim.rs

1//! The deterministic simulation: a level of solid tiles and the hero moving through it.
2//!
3//! No toolkit types. [`World::step`] advances one fixed step of [`DT`]; the same
4//! [`Intent`]s give the same results, so a cloned world plays out identically (the tests
5//! rely on that, and so will rewinds and replays).
6//!
7//! Movement follows the Celeste school: integer positions (units) with a fractional
8//! remainder, moved one unit at a time against the tile grid, so collisions are exact and
9//! nothing tunnels. All feel numbers live in [`Tuning`].
10//!
11//! On its own: an [`Intent`] in each fixed step, [`Event`]s out.
12//!
13//! ```
14//! use game_toolkit_platformer::sim::{Event, Intent, JumpKind, Level, Preset, World};
15//!
16//! let mut level = Level::demo();
17//! let mut world = World::on(&level, Preset::Precise.tuning());
18//! world.step(&mut level, &Intent::default());
19//! world.step(&mut level, &Intent { jump_pressed: true, jump_held: true, ..Intent::default() });
20//! assert!(world.events.iter().any(|e| matches!(e, Event::Jumped { kind: JumpKind::Ground, .. })));
21//! ```
22
23pub mod ability;
24pub mod contacts;
25pub mod crystal;
26pub mod custom_id;
27pub mod custom_tile;
28pub mod event;
29pub mod grid;
30pub mod hold;
31pub mod intent;
32pub mod level;
33pub mod moves;
34pub mod player;
35pub mod rect;
36pub mod spot;
37pub mod surface;
38pub mod terrain;
39pub mod tuning;
40
41pub use ability::{Ability, Step};
42pub use contacts::Contacts;
43pub use crystal::Crystal;
44pub use custom_id::CustomId;
45pub use custom_tile::CustomTile;
46pub use event::{Event, JumpKind};
47pub use grid::{Grid, GridTerrain};
48pub use hold::Hold;
49pub use intent::Intent;
50pub use level::{Level, TILE, Tile};
51pub use moves::Moves;
52pub use player::{Mode, Player};
53pub use rect::Rect;
54pub use spot::Spot;
55pub use surface::Surface;
56pub use terrain::Terrain;
57pub use tuning::{Preset, Tuning};
58
59/// The fixed simulation step in seconds (120 Hz).
60pub const DT: f32 = 1.0 / 120.0;
61
62/// The character's movement: its body, its feel, and the bookkeeping between steps. The
63/// world around it is a [`Terrain`] passed to every [`World::step`], so it stays yours.
64#[derive(Clone, Debug)]
65pub struct World {
66    /// The character's body.
67    pub player: Player,
68    /// The movement feel.
69    pub tuning: Tuning,
70    /// Where it starts, and comes back to after falling out (units).
71    pub spawn: Spot,
72    /// Put it back at [`World::spawn`] when it falls out (an [`Event::FellOut`] is
73    /// reported either way).
74    pub respawn: bool,
75    /// What it rode when it finished moving this step (see [`Player::riding`]), for
76    /// [`World::settle`].
77    riding: Option<Rect>,
78    /// The conveyor speed under its feet when it finished moving this step, units/s.
79    belt: f32,
80    /// Steps taken so far.
81    pub tick: u64,
82    /// What happened in the last step.
83    pub events: Vec<Event>,
84    /// Moves of your own, run in every step in this order (see [`Ability`]).
85    pub abilities: Vec<Box<dyn Ability>>,
86    /// Pixels of the game's terrain per unit (see [`World::scale`]).
87    scale: i32,
88}
89
90impl World {
91    /// A character standing at `spawn` (units).
92    pub fn new(tuning: Tuning, spawn: Spot) -> Self {
93        Self {
94            player: Player::at(spawn.top_left(tuning.size), &tuning),
95            tuning,
96            spawn,
97            respawn: true,
98            riding: None,
99            belt: 0.0,
100            tick: 0,
101            events: Vec::new(),
102            abilities: Vec::new(),
103            scale: 1,
104        }
105    }
106
107    /// Pixels of the game's terrain per unit: 1 for a world stepped on its own, `k` under a
108    /// [`crate::Platformer`] with a `Builder::scale(k)`. Abilities get it as
109    /// [`Step::scale`], to ask a terrain they [`find`](trait.Terrain.html#method.find) in its pixels.
110    pub fn scale(&self) -> i32 {
111        self.scale
112    }
113
114    /// Set [`World::scale`]: the facade does, for the terrain it converts.
115    pub(crate) fn set_scale(&mut self, k: i32) {
116        self.scale = k;
117    }
118
119    /// A character at `level`'s spawn tile.
120    pub fn on(level: &Level, tuning: Tuning) -> Self {
121        Self::new(tuning, level.start())
122    }
123
124    /// The first ability of type `T`, to read its state (a charge level, a phase): for a
125    /// driver of its animation, or the game.
126    pub fn ability<T: Ability + 'static>(&self) -> Option<&T> {
127        self.abilities
128            .iter()
129            .find_map(|a| a.as_any().downcast_ref::<T>())
130    }
131
132    /// [`World::ability`], to change.
133    pub fn ability_mut<T: Ability + 'static>(&mut self) -> Option<&mut T> {
134        self.abilities
135            .iter_mut()
136            .find_map(|a| a.as_any_mut().downcast_mut::<T>())
137    }
138
139    /// Advance one step of [`DT`] with `intent` in `terrain`. [`World::events`] then holds
140    /// what happened in this step.
141    ///
142    /// It runs [`World::act`], the terrain's [`Terrain::update`] and [`World::settle`];
143    /// several characters on one terrain run those apart, so the terrain updates once per
144    /// step (see [`crate::Party`]).
145    pub fn step(&mut self, terrain: &mut dyn Terrain, intent: &Intent) {
146        self.act(terrain, intent);
147        terrain.update(DT, &[self.player.rect()]);
148        self.settle(terrain, None);
149    }
150
151    /// The first part of a step: the character moves (your abilities, the built-in moves,
152    /// refills, bouncy ground), the terrain doesn't update yet.
153    pub fn act(&mut self, terrain: &mut dyn Terrain, intent: &Intent) {
154        self.act_among(terrain, intent, &[]);
155    }
156
157    /// [`World::act`] beside other characters, whose bodies its abilities see in
158    /// [`Step::others`] (a [`crate::Party`] passes them).
159    pub fn act_among(&mut self, terrain: &mut dyn Terrain, intent: &Intent, others: &[Rect]) {
160        self.events.clear();
161        let mut moves = Moves::ALL;
162        self.abilities(terrain, intent, &mut moves, others, |a, s| a.before(s));
163        self.player.step(
164            &*terrain,
165            intent,
166            &moves,
167            &self.tuning,
168            DT,
169            &mut self.events,
170        );
171        self.bounce(&*terrain);
172        self.abilities(terrain, intent, &mut moves, others, |a, s| a.after(s));
173        let body = self.player.rect();
174        if self.player.dashes < self.tuning.dashes && terrain.refill(body) {
175            self.player.dashes = self.tuning.dashes;
176            self.events.push(Event::Recharged { crystal: true });
177        }
178        self.riding = self.player.riding();
179        self.belt = if self.player.grounded {
180            terrain.surface(self.player.feet()).belt
181        } else {
182            0.0
183        };
184    }
185
186    /// A landing on bouncy ground ([`Surface::bounce`]) this step bounces back up.
187    fn bounce(&mut self, terrain: &dyn Terrain) {
188        let impact = self.events.iter().find_map(|e| match e {
189            Event::Landed { impact, .. } => Some(*impact),
190            _ => None,
191        });
192        let Some(impact) = impact else { return };
193        let bounce = terrain.surface(self.player.feet()).bounce;
194        if bounce > 0.0 && impact > Surface::BOUNCE_MIN {
195            self.player.launch(self.player.vx, -impact * bounce);
196        }
197    }
198
199    /// The last part of a step, after the terrain's update: moving ground (and conveyor
200    /// belts) carry the character (`extra` adds to how far, for riding on other
201    /// characters), and it may be squished or fall out.
202    pub fn settle(&mut self, terrain: &mut dyn Terrain, extra: Option<(f32, f32)>) {
203        let belt = (self.belt != 0.0).then_some((self.belt * DT, 0.0));
204        let carry = [self.riding.map(|r| terrain.carry(r)), extra, belt]
205            .into_iter()
206            .flatten()
207            .reduce(|(x, y), (ex, ey)| (x + ex, y + ey));
208        self.player.ride(&*terrain, carry, &self.tuning, DT);
209        let body = self.player.rect();
210        if terrain.solid(body) {
211            self.events.push(Event::Squished);
212        }
213        if terrain.fell_out(body) {
214            self.events.push(Event::FellOut);
215            if self.respawn {
216                self.respawn_at(self.spawn);
217            }
218        }
219        self.tick += 1;
220    }
221
222    /// Start over at `at` (units): a fresh body, standing, with everything refilled, and
223    /// an [`Event::Respawned`] in [`World::events`]. For hazards and checkpoints;
224    /// [`World::spawn`] stays as it is.
225    pub fn respawn_at(&mut self, at: Spot) {
226        self.player = Player::at(at.top_left(self.tuning.size), &self.tuning);
227        self.riding = None;
228        self.belt = 0.0;
229        self.events.push(Event::Respawned);
230    }
231
232    /// Run `hook` of every ability.
233    fn abilities(
234        &mut self,
235        terrain: &mut dyn Terrain,
236        intent: &Intent,
237        moves: &mut Moves,
238        others: &[Rect],
239        hook: fn(&mut dyn Ability, &mut Step<'_>),
240    ) {
241        let mut s = Step {
242            player: &mut self.player,
243            intent,
244            terrain,
245            tuning: &self.tuning,
246            moves,
247            events: &mut self.events,
248            others,
249            dt: DT,
250            scale: self.scale,
251        };
252        for a in &mut self.abilities {
253            hook(a.as_mut(), &mut s);
254        }
255    }
256}
257
258#[cfg(test)]
259mod tests;