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;