Skip to main content

game_toolkit_platformer/
platformer.rs

1//! [`Platformer`]: one playable character, plug and play: movement, its animation, and (with
2//! the `toolkit` feature) the controls.
3//!
4//! ```
5//! use game_toolkit_platformer::prelude::*;
6//!
7//! let mut level = Level::demo(); // or your own `Terrain`
8//! let mut hero = Platformer::builder()
9//!     .preset(Preset::Snappy)
10//!     .tune(|t| t.jump_speed = 115.0)
11//!     .spawn(level.start())
12//!     .build()
13//!     .unwrap();
14//!
15//! // Each frame: an intent (or `hero.update(&ctx.input, &mut level, dt)` with
16//! // game-toolkit), then whatever happened, and what to draw.
17//! hero.advance(Intent { move_x: 1.0, ..Intent::default() }, &mut level, 1.0 / 60.0);
18//! for event in hero.drain_events() {
19//!     let _ = event; // Jumped, Landed, Dashed, ...: sounds, effects
20//! }
21//! let (_x, _y) = hero.position();
22//! let _frame = hero.sprite();
23//! ```
24
25mod animate;
26mod scaled;
27
28use std::sync::Arc;
29
30#[cfg(test)]
31use crate::anim::Params;
32use crate::anim::{AnimEvent, Animator, Driver, GraphSpec, SpecError};
33#[cfg(feature = "toolkit")]
34use crate::controls::{Controls, Scheme};
35use crate::hero::{HeroDriver, spec};
36use crate::sim::{
37    Ability, Contacts, DT, Event, Intent, Level, Mode, Preset, Rect, Spot, Terrain, Tuning, World,
38};
39
40use animate::AnimHook;
41pub use animate::Animate;
42pub(crate) use scaled::Scaled;
43
44/// At most this many fixed steps per frame, so a hitch doesn't spiral.
45const MAX_STEPS: u32 = 8;
46
47/// Add `dt` seconds of frame time to the clock `acc` and take the fixed steps due: how
48/// many (at most [`MAX_STEPS`]).
49pub(crate) fn steps(acc: &mut f32, dt: f32) -> u32 {
50    *acc += dt;
51    let n = ((*acc / DT) as u32).min(MAX_STEPS);
52    *acc = (*acc - n as f32 * DT).clamp(0.0, DT);
53    n
54}
55
56/// One playable character. The world around it is yours: pass your [`Terrain`] (or a
57/// [`Level`]) to each update. For spikes, springs, checkpoints and pickups, test
58/// [`Platformer::hitboxes`] after the update and answer with [`Platformer::respawn`],
59/// [`Platformer::launch`], [`Platformer::set_spawn`] or [`Platformer::refill`]
60/// (`docs/hazards.md` has the recipe).
61///
62/// The methods keep the character consistent: [`Platformer::tune`] or
63/// [`Platformer::set_preset`] for the tuning, [`Platformer::add_ability`] and
64/// [`Platformer::remove_ability`] for abilities, [`Platformer::set_graph`] for the graph,
65/// [`Platformer::set_spawn`] for the spawn. For what they don't cover,
66/// [`Platformer::world_mut`] and [`Platformer::animator_mut`] open the simulation and the
67/// animation; see there for what is safe to change.
68pub struct Platformer {
69    /// The simulation: body, tuning, abilities, events.
70    pub(crate) world: World,
71    /// The animation graph's runtime.
72    pub(crate) animator: Animator,
73    /// Movement facts and events to graph parameters (unless turned off).
74    driver: Option<HeroDriver>,
75    /// Whether the built-in driver runs (kept for [`Platformer::set_graph`]).
76    hero_driver: bool,
77    /// Abilities that drive their own animation parameters.
78    animated: Vec<AnimHook>,
79    /// Your own drivers, run after the built-in one each step.
80    drivers: Vec<Box<dyn Driver<World>>>,
81    /// The preset the tuning came from, if any.
82    preset: Option<Preset>,
83    /// Pixels of your terrain per unit of the movement (see [`Builder::scale`]).
84    scale: i32,
85    /// Frame time not yet stepped.
86    acc: f32,
87    /// Presses seen since the last step.
88    latch: Intent,
89    /// The body's position before the last step, for drawing between steps.
90    prev: (f32, f32),
91    /// The hitbox after each step of the last frame.
92    hitboxes: Vec<Rect>,
93    /// Movement events since the last drain.
94    events: Vec<Event>,
95    /// Animation events since the last drain.
96    anim_events: Vec<AnimEvent>,
97    /// The controls, read from game-toolkit's input in [`Platformer::update`].
98    #[cfg(feature = "toolkit")]
99    pub controls: Controls,
100}
101
102/// Sets up a [`Platformer`]: preset, tuning, controls, animation graph, spawn point.
103pub struct Builder {
104    /// The movement feel.
105    tuning: Tuning,
106    /// The preset it came from.
107    preset: Option<Preset>,
108    /// A custom animation graph, or the built-in one.
109    graph: Option<GraphSpec>,
110    /// The graph may leave out the built-in driver's parameters.
111    partial: bool,
112    /// Run the built-in driver.
113    hero_driver: bool,
114    /// Your own drivers, run after the built-in one.
115    drivers: Vec<Box<dyn Driver<World>>>,
116    /// Moves of your own.
117    abilities: Vec<Box<dyn Ability>>,
118    /// Those of them that drive their own animation.
119    animated: Vec<AnimHook>,
120    /// Where to start, in the terrain's space.
121    spawn: Spot,
122    /// Put it back at the start after falling out.
123    respawn: bool,
124    /// Pixels of the terrain per unit.
125    scale: i32,
126    /// The control scheme, or the preset's.
127    #[cfg(feature = "toolkit")]
128    scheme: Option<Scheme>,
129}
130
131/// The animator for `graph` (or the built-in one) and the built-in driver if it runs:
132/// strict about its parameters unless `partial`.
133fn animation(
134    graph: Option<GraphSpec>,
135    hero_driver: bool,
136    partial: bool,
137) -> Result<(Animator, Option<HeroDriver>), SpecError> {
138    let graph = graph.unwrap_or_else(spec).compile()?;
139    let mut animator = Animator::new(Arc::new(graph));
140    let driver = match (hero_driver, partial) {
141        (false, _) => None,
142        (true, false) => Some(HeroDriver::try_new(&animator.params)?),
143        (true, true) => Some(HeroDriver::declaring(&mut animator.params)),
144    };
145    Ok((animator, driver))
146}
147
148impl Builder {
149    /// Feel (and, with game-toolkit, controls) of `preset`.
150    pub fn preset(mut self, preset: Preset) -> Self {
151        self.preset = Some(preset);
152        self.tuning = preset.tuning();
153        self
154    }
155
156    /// A tuning of your own (replaces the preset's numbers).
157    pub fn tuning(mut self, tuning: Tuning) -> Self {
158        self.tuning = tuning;
159        self
160    }
161
162    /// Adjust the tuning so far, e.g. `.tune(|t| t.air_jumps = 2)`.
163    pub fn tune(mut self, f: impl FnOnce(&mut Tuning)) -> Self {
164        f(&mut self.tuning);
165        self
166    }
167
168    /// An animation graph of your own instead of the built-in one. It has to declare the
169    /// parameters the built-in driver writes (see `hero::spec`), though it may ignore any
170    /// of them; [`Builder::build`] says which one is missing (a typo, say). For a graph
171    /// that declares only what it uses, see [`Builder::partial_graph`].
172    pub fn graph(mut self, graph: GraphSpec) -> Self {
173        self.graph = Some(graph);
174        self.partial = false;
175        self
176    }
177
178    /// An animation graph of your own that declares only the built-in driver's
179    /// parameters it uses: the rest are written but never read. A misspelled one goes
180    /// unnoticed, which [`Builder::graph`] would catch.
181    pub fn partial_graph(mut self, graph: GraphSpec) -> Self {
182        self.graph = Some(graph);
183        self.partial = true;
184        self
185    }
186
187    /// A driver of your own, run after the built-in one every step (several run in the
188    /// order added): for parameters of your game (`hurt`, `attacking`, `carrying`), or to
189    /// override what the built-in one wrote. A closure `|world: &World, params: &mut
190    /// Params| ...` is a driver. Parameters only your game sets (not the built-in driver)
191    /// can also be set between updates through [`Platformer::animator`].
192    pub fn driver(mut self, driver: impl Driver<World> + 'static) -> Self {
193        self.drivers.push(Box::new(driver));
194        self
195    }
196
197    /// A move of your own (an attack lunge, a ledge grab), run in every step; several run
198    /// in the order added. See [`Ability`]; for one with animation of its own, see
199    /// [`Builder::animated`].
200    pub fn ability(mut self, ability: impl Ability + Clone + 'static) -> Self {
201        self.abilities.push(Box::new(ability));
202        self
203    }
204
205    /// A move of your own that also drives its animation parameters ([`Animate`]): one
206    /// registration for both halves. Runs as an [`Builder::ability`]; its
207    /// [`Animate::drive`] runs after each step, after the built-in driver and before
208    /// [`Builder::driver`]s. [`Builder::build`] fails if the graph lacks one of its
209    /// [`Animate::PARAMS`].
210    pub fn animated<T: Ability + Animate + Clone + 'static>(mut self, ability: T) -> Self {
211        AnimHook::add(&mut self.animated, AnimHook::of::<T>());
212        self.abilities.push(Box::new(ability));
213        self
214    }
215
216    /// Run without the built-in driver: your drivers ([`Builder::driver`]) set every
217    /// parameter, and the graph needn't declare the built-in ones.
218    pub fn without_hero_driver(mut self) -> Self {
219        self.hero_driver = false;
220        self
221    }
222
223    /// Your terrain speaks pixels, `k` of them per unit of the movement (default 1): with
224    /// 16-pixel tiles, say, `scale(2)`, so the feel made for 8-unit tiles fits. The
225    /// character then converts at its edges: the terrain is asked in your pixels, and
226    /// everything the facade hands in or out is in them too: positions
227    /// ([`Platformer::position`], [`Platformer::hitbox`], every [`Spot`]), speeds
228    /// ([`Platformer::velocity`], [`Platformer::launch`]) and the events of
229    /// [`Platformer::drain_events`]. What runs inside the movement stays in units:
230    /// [`Platformer::world`], the [`Tuning`], drivers, abilities (their `Step`) and the
231    /// data of [`Event::Custom`]. `docs/scale.md` has the table.
232    ///
233    /// A [`Level`] already speaks units: keep the scale at 1 with one and draw it scaled.
234    pub fn scale(mut self, k: i32) -> Self {
235        assert!(k >= 1, "a scale of {k} pixels per unit");
236        self.scale = k;
237        self
238    }
239
240    /// Start at `spot` (units, or pixels with a [`Builder::scale`]): `Spot::Feet(x,
241    /// floor_y)` to stand on ground of your own, `level.start()` on a [`Level`]'s spawn
242    /// tile, `Spot::TopLeft(x, y)` for the hitbox's corner.
243    pub fn spawn(mut self, spot: Spot) -> Self {
244        self.spawn = spot;
245        self
246    }
247
248    /// Whether to put the character back at its start when it falls out of the world
249    /// (default: yes). [`Event::FellOut`] is reported either way, so you can do your own.
250    pub fn respawn(mut self, on: bool) -> Self {
251        self.respawn = on;
252        self
253    }
254
255    /// A control scheme of your own instead of the preset's.
256    #[cfg(feature = "toolkit")]
257    pub fn controls(mut self, scheme: Scheme) -> Self {
258        self.scheme = Some(scheme);
259        self
260    }
261
262    /// The character, or why the animation graph doesn't fit.
263    pub fn build(self) -> Result<Platformer, SpecError> {
264        let (animator, driver) = animation(self.graph, self.hero_driver, self.partial)?;
265        let mut animated = self.animated;
266        for h in &mut animated {
267            h.resolve(&animator.params)?;
268        }
269        let k = self.scale;
270        let mut world = World::new(self.tuning, self.spawn.in_units(k));
271        world.respawn = self.respawn;
272        world.abilities = self.abilities;
273        Ok(Platformer {
274            prev: world.player.exact_pos(),
275            world,
276            animator,
277            driver,
278            hero_driver: self.hero_driver,
279            animated,
280            drivers: self.drivers,
281            preset: self.preset,
282            scale: k,
283            acc: 0.0,
284            latch: Intent::default(),
285            hitboxes: Vec::new(),
286            events: Vec::new(),
287            anim_events: Vec::new(),
288            #[cfg(feature = "toolkit")]
289            controls: Controls::new(
290                self.scheme
291                    .unwrap_or_else(|| crate::controls::Scheme::of(self.preset)),
292            ),
293        })
294    }
295}
296
297impl Platformer {
298    /// A character on `level`'s spawn tile with `preset`'s feel and controls and the
299    /// built-in animation.
300    pub fn on(level: &Level, preset: Preset) -> Self {
301        Self::at(level.start(), preset)
302    }
303
304    /// A character at `spot` (units), otherwise like [`Platformer::on`]: the one-liner for
305    /// a terrain of your own, with `Spot::Feet(x, floor_y)` to stand on its ground.
306    pub fn at(spot: Spot, preset: Preset) -> Self {
307        Self::builder()
308            .preset(preset)
309            .spawn(spot)
310            .build()
311            .expect("the built-in animation graph fits the built-in driver")
312    }
313
314    /// Set one up step by step; starts from the hybrid preset with the hitbox at `0, 0`.
315    pub fn builder() -> Builder {
316        Builder {
317            tuning: Preset::Hybrid.tuning(),
318            preset: Some(Preset::Hybrid),
319            graph: None,
320            partial: false,
321            hero_driver: true,
322            drivers: Vec::new(),
323            abilities: Vec::new(),
324            animated: Vec::new(),
325            spawn: Spot::TopLeft(0, 0),
326            respawn: true,
327            scale: 1,
328            #[cfg(feature = "toolkit")]
329            scheme: None,
330        }
331    }
332
333    /// Read game-toolkit's input with [`Platformer::controls`] and advance by `dt` seconds
334    /// of frame time in `terrain`. Returns the fixed steps taken.
335    #[cfg(feature = "toolkit")]
336    pub fn update(
337        &mut self,
338        input: &game_toolkit::prelude::Input,
339        terrain: &mut dyn Terrain,
340        dt: f32,
341    ) -> u32 {
342        let intent = self.controls.read(input);
343        self.advance(intent, terrain, dt)
344    }
345
346    /// Advance by `dt` seconds of frame time with `intent` in `terrain`, in fixed steps of
347    /// [`DT`].
348    /// Presses are kept until the next step, so a frame without one doesn't lose them.
349    /// Returns the steps taken; [`Platformer::hitboxes`] then has where each one left the
350    /// character.
351    pub fn advance(&mut self, intent: Intent, terrain: &mut dyn Terrain, dt: f32) -> u32 {
352        self.begin_frame();
353        self.hold_presses(&intent);
354        let n = steps(&mut self.acc, dt);
355        for _ in 0..n {
356            let i = self.with_presses(intent);
357            self.step_once(&i, terrain);
358        }
359        n
360    }
361
362    /// Keep `intent`'s presses until the next step.
363    pub(crate) fn hold_presses(&mut self, intent: &Intent) {
364        self.latch.jump_pressed |= intent.jump_pressed;
365        self.latch.dash_pressed |= intent.dash_pressed;
366        self.latch.dive_pressed |= intent.dive_pressed;
367        self.latch.custom_pressed |= intent.custom_pressed;
368    }
369
370    /// `intent` with the presses kept since the last step, which are then used up.
371    pub(crate) fn with_presses(&mut self, intent: Intent) -> Intent {
372        let i = Intent {
373            jump_pressed: self.latch.jump_pressed,
374            dash_pressed: self.latch.dash_pressed,
375            dive_pressed: self.latch.dive_pressed,
376            custom_pressed: self.latch.custom_pressed,
377            ..intent
378        };
379        self.latch = Intent::default();
380        i
381    }
382
383    /// Exactly one fixed step of [`DT`] with `intent` in `terrain` (for your own clock,
384    /// replays, tests). It counts as a frame of one step for [`Platformer::hitboxes`].
385    pub fn step(&mut self, intent: &Intent, terrain: &mut dyn Terrain) {
386        self.begin_frame();
387        self.step_once(intent, terrain);
388    }
389
390    /// One fixed step, within a frame.
391    fn step_once(&mut self, intent: &Intent, terrain: &mut dyn Terrain) {
392        self.begin_step();
393        if self.scale == 1 {
394            self.world.step(terrain, intent);
395        } else {
396            let mut scaled = Scaled {
397                inner: terrain,
398                k: self.scale,
399            };
400            self.world.step(&mut scaled, intent);
401        }
402        self.end_step();
403    }
404
405    /// Pixels of the terrain per unit of the movement (see [`Builder::scale`]).
406    pub fn scale(&self) -> i32 {
407        self.scale
408    }
409
410    /// Set the clock's leftover time (a [`crate::Party`] keeps one clock for everyone), so
411    /// [`Platformer::position`] draws between steps.
412    pub(crate) fn set_clock(&mut self, acc: f32) {
413        self.acc = acc;
414    }
415
416    /// Before a frame's steps: forget the last frame's hitboxes.
417    pub(crate) fn begin_frame(&mut self) {
418        self.hitboxes.clear();
419    }
420
421    /// Before a step: remember where the body was, to draw between steps.
422    pub(crate) fn begin_step(&mut self) {
423        self.prev = self.world.player.exact_pos();
424        // Every step, so a world swapped in through `world_mut` still tells abilities.
425        self.world.set_scale(self.scale);
426    }
427
428    /// After a step: collect its events and hitbox, drive the animation. A respawn in the
429    /// step (the character fell out) starts the animation over and snaps the drawing, as
430    /// [`Platformer::respawn`] does.
431    pub(crate) fn end_step(&mut self) {
432        let k = self.scale;
433        self.events
434            .extend(self.world.events.iter().map(|e| e.in_pixels(k)));
435        self.hitboxes.push(self.hitbox());
436        if self.world.events.contains(&Event::Respawned) {
437            self.prev = self.world.player.exact_pos();
438            self.animator.reset();
439            return;
440        }
441        if let Some(d) = &mut self.driver {
442            d.drive(&self.world, &mut self.animator.params);
443        }
444        for h in &self.animated {
445            h.run(&self.world, &mut self.animator.params);
446        }
447        for d in &mut self.drivers {
448            d.drive(&self.world, &mut self.animator.params);
449        }
450        self.animator.update(DT);
451        self.anim_events.extend(self.animator.drain_events());
452    }
453
454    /// The movement events since the last call: jumps, landings, dashes, rolls, ... In
455    /// your terrain's space: with a [`Builder::scale`], a landing's `drop` and `impact`
456    /// are in pixels (see [`Event::in_pixels`]). The buffer is kept, so draining every frame doesn't allocate (`.collect()` for a `Vec`).
457    pub fn drain_events(&mut self) -> std::vec::Drain<'_, Event> {
458        self.events.drain(..)
459    }
460
461    /// The animation events since the last call: states entered, markers (`"step"`),
462    /// clips finished. Like [`Platformer::drain_events`], without allocating.
463    pub fn drain_anim_events(&mut self) -> std::vec::Drain<'_, AnimEvent> {
464        self.anim_events.drain(..)
465    }
466
467    /// The sprite number to draw (see `hero::sprite`).
468    pub fn sprite(&self) -> u16 {
469        self.animator.sprite()
470    }
471
472    /// The animation state's name.
473    pub fn state(&self) -> &str {
474        self.animator.state_name()
475    }
476
477    /// 1 facing right, -1 facing left.
478    pub fn facing(&self) -> i32 {
479        self.world.player.facing
480    }
481
482    /// Standing on something (the ground, a platform, moving ground, another character).
483    pub fn grounded(&self) -> bool {
484        self.world.player.grounded
485    }
486
487    /// The speed in units/s (pixels/s with a [`Builder::scale`]), y down.
488    pub fn velocity(&self) -> (f32, f32) {
489        let k = self.scale as f32;
490        (self.world.player.vx * k, self.world.player.vy * k)
491    }
492
493    /// What the body is busy with: running and jumping freely, dashing, climbing, a move
494    /// of your own, ...
495    pub fn mode(&self) -> Mode {
496        self.world.player.mode
497    }
498
499    /// Dashes left before touching the ground (or a refill) again.
500    pub fn dashes(&self) -> u8 {
501        self.world.player.dashes
502    }
503
504    /// Air jumps left.
505    pub fn air_jumps(&self) -> u8 {
506        self.world.player.air_jumps
507    }
508
509    /// Climbing stamina left (out of [`Tuning::stamina`]).
510    pub fn stamina(&self) -> f32 {
511        self.world.player.stamina
512    }
513
514    /// The side of the wall it holds on to or slides down (-1 left, 1 right), if any.
515    pub fn on_wall(&self) -> Option<i32> {
516        let p = &self.world.player;
517        match (p.mode, p.wall_slide) {
518            (Mode::Climb, _) => Some(p.facing),
519            (_, 0) => None,
520            (_, side) => Some(side),
521        }
522    }
523
524    /// The hitbox's top-left in units (pixels with a [`Builder::scale`]), smoothly between
525    /// fixed steps: where to draw.
526    pub fn position(&self) -> (f32, f32) {
527        self.position_at(self.acc)
528    }
529
530    /// [`Platformer::position`] with `acc` seconds into the next step.
531    pub(crate) fn position_at(&self, acc: f32) -> (f32, f32) {
532        let (x, y) = self.world.player.exact_pos();
533        let (px, py) = self.prev;
534        let k = self.scale as f32;
535        if (x - px).abs() > 16.0 || (y - py).abs() > 16.0 {
536            return (x * k, y * k);
537        }
538        let a = (acc / DT).clamp(0.0, 1.0);
539        ((px + (x - px) * a) * k, (py + (y - py) * a) * k)
540    }
541
542    /// The hitbox in units (pixels with a [`Builder::scale`]), as of the last step.
543    pub fn hitbox(&self) -> Rect {
544        self.world.player.rect().scaled(self.scale)
545    }
546
547    /// The hitbox after each fixed step of the last frame ([`Platformer::advance`],
548    /// [`Platformer::update`], [`Platformer::step`] or a [`crate::Party`]'s), oldest
549    /// first; empty when the frame took no step. Test hazards and triggers against every
550    /// one: at a low frame rate a frame takes several steps, and a spike touched in one
551    /// of them may be left again by the last ([`Platformer::hitbox`]).
552    pub fn hitboxes(&self) -> &[Rect] {
553        &self.hitboxes
554    }
555
556    /// The first of [`Platformer::hitboxes`] that overlaps `thing` (a spike, an enemy, a
557    /// pickup, in your terrain's space), if any. A copy, so you can act on the character
558    /// right away: `if hero.touched(spikes).is_some() { hero.respawn(); }`. On a
559    /// [`Level`]'s tiles, see [`Level::touches`].
560    pub fn touched(&self, thing: Rect) -> Option<Rect> {
561        self.hitboxes.iter().copied().find(|r| r.intersects(thing))
562    }
563
564    /// Set the speed from outside: a spring, a bounce pad, knockback (units/s, y down). It
565    /// ends a dash or a climb, can't be cut by releasing jump, and going up leaves the
566    /// ground. See [`Player::launch`](crate::sim::Player::launch).
567    pub fn launch(&mut self, vx: f32, vy: f32) {
568        let k = self.scale as f32;
569        self.world.player.launch(vx / k, vy / k);
570    }
571
572    /// Set the speed (units/s, pixels/s with a [`Builder::scale`], y down) without
573    /// anything else [`Platformer::launch`] does: a dash or climb goes on, a jump can still
574    /// be cut. A conveyor of your own, a slowdown, a stop.
575    pub fn set_velocity(&mut self, vx: f32, vy: f32) {
576        let k = self.scale as f32;
577        (self.world.player.vx, self.world.player.vy) = (vx / k, vy / k);
578    }
579
580    /// Face right (`1`) or left (`-1`; any other sign counts as its side, 0 keeps it): a
581    /// cutscene, a respawn facing into the level.
582    pub fn set_facing(&mut self, dir: i32) {
583        if dir != 0 {
584            self.world.player.facing = dir.signum();
585        }
586    }
587
588    /// The hitbox's top-left in units for a body at `spot` in the terrain's space.
589    fn top_left(&self, spot: Spot) -> (i32, i32) {
590        spot.in_units(self.scale).top_left(self.world.tuning.size)
591    }
592
593    /// Put the character at `spot` at once, without drawing it sliding there: a door, a
594    /// portal. Speed and state stay; see [`Player::teleport`](crate::sim::Player::teleport).
595    pub fn teleport(&mut self, spot: Spot) {
596        let (x, y) = self.top_left(spot);
597        self.world.player.teleport(x, y);
598        self.prev = self.world.player.exact_pos();
599    }
600
601    /// Start over at the spawn point: a fresh body, the animation from its entry state, and
602    /// an [`Event::Respawned`]. For hazards; see [`Platformer::set_spawn`] for checkpoints.
603    pub fn respawn(&mut self) {
604        self.respawn_in_units(self.world.spawn);
605    }
606
607    /// Start over at `spot`, like [`Platformer::respawn`], without moving the spawn point.
608    pub fn respawn_at(&mut self, spot: Spot) {
609        self.respawn_in_units(spot.in_units(self.scale));
610    }
611
612    /// [`Platformer::respawn_at`] with `at` in units.
613    fn respawn_in_units(&mut self, at: Spot) {
614        self.world.respawn_at(at);
615        self.events.push(Event::Respawned);
616        self.hitboxes.push(self.hitbox());
617        self.prev = self.world.player.exact_pos();
618        self.animator.reset();
619    }
620
621    /// Where [`Platformer::respawn`] (and falling out) puts the character from now on: a
622    /// checkpoint.
623    pub fn set_spawn(&mut self, spot: Spot) {
624        self.world.spawn = spot.in_units(self.scale);
625    }
626
627    /// The spawn point (units, or pixels with a [`Builder::scale`]).
628    pub fn spawn(&self) -> Spot {
629        self.world.spawn.in_pixels(self.scale)
630    }
631
632    /// Refill dashes, air jumps and climbing stamina: a pickup, a checkpoint.
633    pub fn refill(&mut self) {
634        self.world.player.refill(&self.world.tuning);
635    }
636
637    /// The preset the tuning came from, if any.
638    pub fn preset(&self) -> Option<Preset> {
639        self.preset
640    }
641
642    /// Switch to `preset`'s feel (and, with game-toolkit, its controls, keeping your custom
643    /// buttons), keeping the character where it is and its size.
644    pub fn set_preset(&mut self, preset: Preset) {
645        self.preset = Some(preset);
646        self.world.tuning = Tuning {
647            size: self.world.tuning.size,
648            ..preset.tuning()
649        };
650        #[cfg(feature = "toolkit")]
651        {
652            // The game's own buttons stay bound.
653            let custom = std::mem::take(&mut self.controls.scheme.custom);
654            self.controls = Controls::for_preset(preset);
655            self.controls.scheme.custom = custom;
656        }
657    }
658
659    /// Change the tuning at runtime, e.g. `hero.tune(|t| t.air_jumps = 2)` for a power-up.
660    /// The feel is no longer a preset's: [`Platformer::preset`] says `None` from now on
661    /// (the controls stay). A new [`Tuning::size`] takes effect at the next respawn.
662    pub fn tune(&mut self, f: impl FnOnce(&mut Tuning)) {
663        f(&mut self.world.tuning);
664        self.preset = None;
665    }
666
667    /// Add a move of your own at runtime (a power-up), run after the ones already there.
668    /// See [`Builder::ability`].
669    pub fn add_ability(&mut self, ability: impl Ability + Clone + 'static) {
670        self.world.abilities.push(Box::new(ability));
671    }
672
673    /// Add a move of your own with animation of its own at runtime. See
674    /// [`Builder::animated`]. If the graph lacks one of its [`Animate::PARAMS`], nothing
675    /// is added and the error says which.
676    pub fn add_animated<T: Ability + Animate + Clone + 'static>(
677        &mut self,
678        ability: T,
679    ) -> Result<(), SpecError> {
680        let mut hook = AnimHook::of::<T>();
681        hook.resolve(&self.animator.params)?;
682        AnimHook::add(&mut self.animated, hook);
683        self.world.abilities.push(Box::new(ability));
684        Ok(())
685    }
686
687    /// Take away the first move of type `T` (a power-up that runs out) and return it. Its
688    /// [`Animate`] half stops with it; the other abilities keep driving their own
689    /// animation.
690    pub fn remove_ability<T: Ability + Clone + 'static>(&mut self) -> Option<T> {
691        let i = self
692            .world
693            .abilities
694            .iter()
695            .position(|a| a.as_any().is::<T>())?;
696        let removed = self.world.abilities.remove(i);
697        if self.world.ability::<T>().is_none() {
698            let ty = std::any::TypeId::of::<T>();
699            self.animated.retain(|h| h.ty != ty);
700        }
701        removed.as_any().downcast_ref::<T>().cloned()
702    }
703
704    /// The first move of type `T`, to read its state (a charge level, a phase).
705    pub fn ability<T: Ability + 'static>(&self) -> Option<&T> {
706        self.world.ability()
707    }
708
709    /// [`Platformer::ability`], to change (top up a charge, reset a cooldown).
710    pub fn ability_mut<T: Ability + 'static>(&mut self) -> Option<&mut T> {
711        self.world.ability_mut()
712    }
713
714    /// What the body's own movement ran into in the last step: a wall, a ceiling, the
715    /// floor (see [`Contacts`]).
716    pub fn contacts(&self) -> Contacts {
717        self.world.player.contacts
718    }
719
720    /// The movement feel in effect (change it with [`Platformer::tune`]).
721    pub fn tuning(&self) -> &Tuning {
722        &self.world.tuning
723    }
724
725    /// The simulation, to read anything the methods don't cover: the body
726    /// (`world().player`), the step count, the last step's events.
727    pub fn world(&self) -> &World {
728        &self.world
729    }
730
731    /// The simulation, to change what the methods don't cover: the escape hatch. Games
732    /// shouldn't need it: [`Platformer::launch`], [`Platformer::set_velocity`],
733    /// [`Platformer::set_facing`], [`Platformer::refill`], [`Platformer::teleport`],
734    /// [`Platformer::respawn`], [`Platformer::tune`], the ability and spawn methods cover
735    /// what games change. What's writable on the body ([`crate::sim::Player`]) is safe;
736    /// the rest of the world you keep consistent yourself. Everything here is in units,
737    /// not in a [`Builder::scale`]'s pixels.
738    pub fn world_mut(&mut self) -> &mut World {
739        &mut self.world
740    }
741
742    /// The animation graph's runtime: its state, parameters, clip times.
743    pub fn animator(&self) -> &Animator {
744        &self.animator
745    }
746
747    /// The animation graph's runtime, to change. Safe: parameters only your game sets
748    /// (`params`); the drivers overwrite theirs every step. Swap the graph with
749    /// [`Platformer::set_graph`].
750    pub fn animator_mut(&mut self) -> &mut Animator {
751        &mut self.animator
752    }
753
754    /// Swap the animation graph at runtime, checked like [`Builder::graph`]: it must
755    /// declare the built-in driver's parameters (unless that is off) and those of every
756    /// [`Animate`] ability ([`Animate::PARAMS`]). The animation starts
757    /// over from the new graph's entry state; on an error nothing changes.
758    pub fn set_graph(&mut self, graph: GraphSpec) -> Result<(), SpecError> {
759        self.swap_graph(graph, false)
760    }
761
762    /// [`Platformer::set_graph`] for a graph that declares only what it uses, like
763    /// [`Builder::partial_graph`].
764    pub fn set_partial_graph(&mut self, graph: GraphSpec) -> Result<(), SpecError> {
765        self.swap_graph(graph, true)
766    }
767
768    /// [`Platformer::set_graph`], strict unless `partial`.
769    fn swap_graph(&mut self, graph: GraphSpec, partial: bool) -> Result<(), SpecError> {
770        let (animator, driver) = animation(Some(graph), self.hero_driver, partial)?;
771        let ids = self
772            .animated
773            .iter()
774            .map(|h| h.lookup(&animator.params))
775            .collect::<Result<Vec<_>, _>>()?;
776        for (h, ids) in self.animated.iter_mut().zip(ids) {
777            h.set_ids(ids);
778        }
779        self.animator = animator;
780        self.driver = driver;
781        Ok(())
782    }
783}
784
785#[cfg(test)]
786mod tests {
787    use super::*;
788    use crate::anim::{Clip, ParamId, ParamKind, Playback, StateSpec};
789    use crate::sim::Step;
790
791    #[test]
792    fn a_frame_without_a_step_keeps_the_press() {
793        // A fresh clock: no leftover time, so a zero-length frame takes no step.
794        let mut level = Level::demo();
795        let mut hero = Platformer::on(&level, Preset::Precise);
796        let press = Intent {
797            jump_pressed: true,
798            jump_held: true,
799            ..Intent::default()
800        };
801        assert_eq!(
802            hero.advance(press, &mut level, 0.0),
803            0,
804            "a frame without a step"
805        );
806        let held = Intent {
807            jump_held: true,
808            ..Intent::default()
809        };
810        hero.advance(held, &mut level, 1.0 / 60.0);
811        assert!(
812            hero.drain_events()
813                .any(|e| matches!(e, Event::Jumped { .. }))
814        );
815        assert_eq!(hero.state(), "takeoff");
816    }
817
818    #[test]
819    fn custom_presses_are_latched_like_the_built_in_ones() {
820        /// The game's attack button.
821        const ATTACK: u8 = 3;
822        let mut level = Level::demo();
823        let mut hero = Platformer::on(&level, Preset::Precise);
824        let press = Intent {
825            custom: 1 << ATTACK,
826            custom_pressed: 1 << ATTACK,
827            ..Intent::default()
828        };
829        assert_eq!(hero.advance(press, &mut level, 0.0), 0);
830        assert!(hero.latch.pressed(ATTACK), "kept for the next step");
831        hero.advance(Intent::default(), &mut level, 1.0 / 60.0);
832        assert!(!hero.latch.pressed(ATTACK), "used up by the step");
833        assert!(press.held(ATTACK) && !press.held(ATTACK + 1));
834    }
835
836    #[cfg(feature = "toolkit")]
837    #[test]
838    fn switching_presets_keeps_the_custom_buttons() {
839        use crate::controls::Custom;
840        use game_toolkit::prelude::Key;
841        let attack: std::borrow::Cow<'static, [Custom]> =
842            vec![Custom::new(0, &[Key::KeyX], &[])].into();
843        let mut hero = Platformer::builder()
844            .controls(Scheme {
845                custom: attack,
846                ..Scheme::SNAPPY
847            })
848            .build()
849            .unwrap();
850        hero.set_preset(Preset::Precise);
851        assert_eq!(hero.controls.scheme.jump, Scheme::PRECISE.jump);
852        assert_eq!(hero.controls.scheme.custom.len(), 1);
853    }
854
855    #[test]
856    fn tune_and_presets_set_the_feel() {
857        let hero = Platformer::builder()
858            .preset(Preset::Momentum)
859            .tune(|t| t.air_jumps = 2)
860            .build()
861            .unwrap();
862        assert_eq!(hero.world.tuning.air_jumps, 2);
863        assert_eq!(
864            hero.world.tuning.sprint_speed,
865            Preset::Momentum.tuning().sprint_speed
866        );
867        assert_eq!(hero.preset(), Some(Preset::Momentum));
868    }
869
870    #[test]
871    fn a_custom_graph_missing_a_driven_parameter_is_an_error() {
872        let tiny = GraphSpec::new().state(StateSpec::new(
873            "idle",
874            Clip::uniform(Playback::Loop, &[0], 0.1),
875        ));
876        let err = Platformer::builder().graph(tiny).build().err();
877        assert!(matches!(err, Some(SpecError::UnknownParam(_))), "{err:?}");
878    }
879
880    /// Idle while grounded, air otherwise, by the `grounded` bool alone.
881    fn two_state_graph() -> GraphSpec {
882        use crate::anim::{Cond, TransitionSpec};
883        let c = |s: u16| Clip::uniform(Playback::Loop, &[s], 0.1);
884        GraphSpec::new()
885            .bool("grounded")
886            .state(StateSpec::new("idle", c(0)))
887            .state(StateSpec::new("air", c(1)))
888            .transition(TransitionSpec::new(
889                ["idle"],
890                "air",
891                Cond::not(Cond::Is("grounded".into())),
892            ))
893            .transition(TransitionSpec::new(
894                ["air"],
895                "idle",
896                Cond::Is("grounded".into()),
897            ))
898    }
899
900    #[test]
901    fn a_partial_graph_gets_only_what_it_declares() {
902        assert!(
903            Platformer::builder()
904                .graph(two_state_graph())
905                .build()
906                .is_err(),
907            "strict by default"
908        );
909        let mut hero = Platformer::builder()
910            .partial_graph(two_state_graph())
911            .spawn(Spot::TopLeft(20, 40))
912            .build()
913            .unwrap();
914        idle(&mut hero);
915        assert_eq!(hero.state(), "air");
916        for _ in 0..60 {
917            idle(&mut hero);
918        }
919        assert_eq!(hero.state(), "idle");
920        // Starting over clears the parameters the driver added too.
921        hero.respawn();
922        idle(&mut hero);
923    }
924
925    #[test]
926    fn own_drivers_run_after_the_built_in_one() {
927        let hero = |b: Builder| {
928            let mut h = b
929                .graph(spec().bool("fast"))
930                .driver(|w: &World, p: &mut Params| {
931                    let id = p.id("fast").unwrap();
932                    p.set_bool(id, w.player.vx.abs() > 50.0);
933                })
934                .spawn(Spot::TopLeft(20, 88))
935                .build()
936                .unwrap();
937            for _ in 0..30 {
938                h.advance(
939                    Intent {
940                        move_x: 1.0,
941                        ..Intent::default()
942                    },
943                    &mut floor,
944                    1.0 / 60.0,
945                );
946            }
947            h
948        };
949        let h = hero(Platformer::builder());
950        let fast = h.animator.params.id("fast").unwrap();
951        assert!(h.animator.params.get_bool(fast), "set by the game's driver");
952
953        // Without the built-in driver, nothing else is written.
954        let h = hero(Platformer::builder().without_hero_driver());
955        let grounded = h.animator.params.id("grounded").unwrap();
956        assert!(!h.animator.params.get_bool(grounded));
957        assert!(h.animator.params.get_bool(fast));
958
959        // Later drivers win.
960        let mut h = Platformer::builder()
961            .driver(|_: &World, p: &mut Params| {
962                let id = p.id("grounded").unwrap();
963                p.set_bool(id, false);
964            })
965            .spawn(Spot::TopLeft(20, 88))
966            .build()
967            .unwrap();
968        idle(&mut h);
969        assert!(h.world.player.grounded);
970        assert!(!h.animator.params.get_bool(grounded));
971    }
972
973    /// A flat floor at y = 100, wide open above.
974    fn floor(r: Rect) -> bool {
975        r.bottom() > 100
976    }
977
978    /// One frame of 1/60 s without input on the flat floor.
979    fn idle(hero: &mut Platformer) -> Vec<Event> {
980        hero.advance(Intent::default(), &mut floor, 1.0 / 60.0);
981        hero.drain_events().collect()
982    }
983
984    #[test]
985    fn a_launch_is_not_cut_and_leaves_the_ground() {
986        let mut hero = Platformer::at(Spot::TopLeft(20, 88), Preset::Precise);
987        idle(&mut hero);
988        assert!(hero.world.player.grounded);
989        hero.launch(0.0, -300.0);
990        // Jump isn't held: a jump would be cut now, a spring isn't.
991        let events = idle(&mut hero);
992        assert!(!events.contains(&Event::JumpCut), "{events:?}");
993        assert!(!hero.world.player.grounded);
994        assert!(hero.world.player.vy < -250.0, "{}", hero.world.player.vy);
995        assert!(hero.hitbox().y < 88);
996    }
997
998    #[test]
999    fn a_launch_ends_a_dash() {
1000        let mut hero = Platformer::at(Spot::TopLeft(20, 88), Preset::Precise);
1001        idle(&mut hero);
1002        let dash = Intent {
1003            move_x: 1.0,
1004            dash_pressed: true,
1005            ..Intent::default()
1006        };
1007        hero.advance(dash, &mut floor, 1.0 / 60.0);
1008        assert!(matches!(
1009            hero.world.player.mode,
1010            crate::sim::Mode::Dash { .. }
1011        ));
1012        hero.launch(-100.0, -200.0);
1013        assert_eq!(hero.world.player.mode, crate::sim::Mode::Normal);
1014        idle(&mut hero);
1015        assert!(hero.world.player.vx < 0.0, "knocked back");
1016    }
1017
1018    #[test]
1019    fn a_teleport_snaps_without_smearing() {
1020        let mut hero = Platformer::at(Spot::TopLeft(20, 88), Preset::Precise);
1021        // Half a step of leftover time, so drawing would blend between two positions.
1022        hero.advance(Intent::default(), &mut floor, 1.5 * DT);
1023        hero.teleport(Spot::TopLeft(150, 40));
1024        assert_eq!(hero.position(), (150.0, 40.0));
1025        assert_eq!(hero.hitbox().x, 150);
1026        // It falls from there to the floor and lands.
1027        let mut events = Vec::new();
1028        for _ in 0..60 {
1029            events.extend(idle(&mut hero));
1030        }
1031        assert!(hero.world.player.grounded);
1032        assert_eq!(hero.hitbox().bottom(), 100);
1033        assert!(
1034            events
1035                .iter()
1036                .any(|e| matches!(e, Event::Landed { drop, .. } if *drop >= 40)),
1037            "{events:?}"
1038        );
1039    }
1040
1041    #[test]
1042    fn respawning_resets_body_and_animation_and_says_so() {
1043        let mut hero = Platformer::at(Spot::TopLeft(20, 88), Preset::Precise);
1044        let run = Intent {
1045            move_x: 1.0,
1046            ..Intent::default()
1047        };
1048        for _ in 0..20 {
1049            hero.advance(run, &mut floor, 1.0 / 60.0);
1050        }
1051        hero.world.player.dashes = 0;
1052        hero.drain_events();
1053        assert_ne!(hero.state(), "idle");
1054
1055        hero.set_spawn(Spot::TopLeft(60, 88));
1056        hero.respawn();
1057        assert_eq!(hero.drain_events().collect::<Vec<_>>(), [Event::Respawned]);
1058        assert_eq!(hero.hitbox().x, 60);
1059        assert_eq!(hero.position(), (60.0, 88.0));
1060        assert_eq!(hero.world.player.vx, 0.0);
1061        assert_eq!(hero.world.player.dashes, hero.world.tuning.dashes);
1062        assert_eq!(hero.state(), "idle");
1063
1064        hero.respawn_at(Spot::TopLeft(100, 88));
1065        assert_eq!(hero.hitbox().x, 100);
1066        assert_eq!(
1067            hero.spawn(),
1068            Spot::TopLeft(60, 88),
1069            "respawn_at doesn't move the spawn"
1070        );
1071    }
1072
1073    #[test]
1074    fn refill_restores_dashes_air_jumps_and_stamina() {
1075        let mut hero = Platformer::at(Spot::TopLeft(20, 88), Preset::Snappy);
1076        let p = &mut hero.world.player;
1077        (p.dashes, p.air_jumps, p.stamina) = (0, 0, 0.0);
1078        hero.refill();
1079        let (p, t) = (&hero.world.player, &hero.world.tuning);
1080        assert_eq!(
1081            (p.dashes, p.air_jumps, p.stamina),
1082            (t.dashes, t.air_jumps, t.stamina)
1083        );
1084    }
1085
1086    #[test]
1087    fn spawning_standing_on_a_custom_floor_or_a_level_tile() {
1088        let mut hero = Platformer::builder()
1089            .spawn(Spot::Feet(50, 100))
1090            .build()
1091            .unwrap();
1092        let r = hero.hitbox();
1093        assert_eq!((r.bottom(), r.x + r.w / 2), (100, 50));
1094        let events = idle(&mut hero);
1095        assert!(hero.world.player.grounded, "stands at once");
1096        assert!(events.is_empty(), "no fall, no landing: {events:?}");
1097        assert_eq!(Spot::Feet(50, 100).top_left((r.w, r.h)), (r.x, r.y));
1098
1099        // On a level, standing on the bottom of the spawn tile, centered on it.
1100        let level = Level::demo();
1101        let hero = Platformer::on(&level, Preset::Hybrid);
1102        let (sx, sy) = level.spawn_tile();
1103        let r = hero.hitbox();
1104        assert_eq!((r.x + r.w / 2, r.bottom()), (sx * 8 + 4, (sy + 1) * 8));
1105        assert_eq!(level.start(), Spot::Feet(sx * 8 + 4, (sy + 1) * 8));
1106    }
1107
1108    #[test]
1109    fn the_hitbox_size_comes_from_the_tuning() {
1110        let mut level = Level::demo();
1111        let mut hero = Platformer::builder()
1112            .tune(|t| t.size = (10, 20))
1113            .spawn(level.start())
1114            .build()
1115            .unwrap();
1116        let r = hero.hitbox();
1117        assert_eq!((r.w, r.h), (10, 20));
1118        let (sx, sy) = level.spawn_tile();
1119        let t = level.tile_size();
1120        assert_eq!(r.bottom(), (sy + 1) * t, "stands on the spawn tile");
1121        assert_eq!(r.x + r.w / 2, sx * t + t / 2, "centered on it");
1122        for _ in 0..30 {
1123            hero.advance(Intent::default(), &mut level, 1.0 / 60.0);
1124        }
1125        assert!(hero.world.player.grounded);
1126        assert_eq!(hero.hitbox().bottom(), (sy + 1) * t);
1127
1128        hero.set_preset(Preset::Momentum);
1129        hero.respawn();
1130        assert_eq!(hero.hitbox().h, 20, "a preset is a feel, not a body");
1131    }
1132
1133    #[test]
1134    fn abilities_from_the_builder_run_with_custom_buttons() {
1135        use crate::sim::{CustomId, Mode, Step};
1136        /// The brake's mode and event.
1137        const BRAKE: CustomId = CustomId::new("tests::brake");
1138        /// Custom button 0 stops the character dead, in a mode of its own for 0.1 s.
1139        #[derive(Clone, Debug)]
1140        struct Brake;
1141        impl Ability for Brake {
1142            fn before(&mut self, s: &mut Step<'_>) {
1143                match s.player.mode {
1144                    Mode::Normal if s.intent.pressed(0) => {
1145                        s.player.mode = Mode::Custom { id: BRAKE, t: 0.0 };
1146                        s.player.vx = 0.0;
1147                        s.events.push(Event::custom(BRAKE));
1148                    }
1149                    Mode::Custom { id: BRAKE, t } if t >= 0.1 => s.player.mode = Mode::Normal,
1150                    _ => {}
1151                }
1152            }
1153        }
1154        let mut hero = Platformer::builder()
1155            .ability(Brake)
1156            .spawn(Spot::TopLeft(20, 88))
1157            .build()
1158            .unwrap();
1159        let run = Intent {
1160            move_x: 1.0,
1161            ..Intent::default()
1162        };
1163        for _ in 0..20 {
1164            hero.advance(run, &mut floor, 1.0 / 60.0);
1165        }
1166        hero.drain_events();
1167        // The press lands in a frame without a step and is still seen.
1168        let brake = Intent {
1169            custom_pressed: 1,
1170            ..run
1171        };
1172        hero.advance(brake, &mut floor, 0.0);
1173        hero.advance(run, &mut floor, DT);
1174        assert!(hero.drain_events().any(|e| e == Event::custom(BRAKE)));
1175        assert_eq!(hero.world.player.vx, 0.0, "the run doesn't accelerate it");
1176        for _ in 0..10 {
1177            hero.advance(run, &mut floor, 1.0 / 60.0);
1178        }
1179        assert_eq!(hero.world.player.mode, Mode::Normal, "handed back");
1180        assert!(hero.world.player.vx > 0.0, "running again");
1181    }
1182
1183    #[test]
1184    fn a_closure_is_enough_terrain_and_falling_out_respawns_or_not() {
1185        // A floor at y = 100 from x = 0 to 200; beyond it, a drop into nothing.
1186        let mut floor = |r: Rect| r.bottom() > 100 && r.x < 200;
1187        let mut hero = Platformer::at(Spot::TopLeft(20, 88), Preset::Hybrid);
1188        for _ in 0..30 {
1189            hero.advance(Intent::default(), &mut floor, 1.0 / 60.0);
1190        }
1191        assert!(hero.world.player.grounded, "stands on the closure's floor");
1192        assert_eq!(hero.state(), "idle");
1193
1194        // A terrain that also says where the world ends.
1195        struct Pit;
1196        impl Terrain for Pit {
1197            fn solid(&self, r: Rect) -> bool {
1198                r.bottom() > 100 && r.x < 200
1199            }
1200            fn fell_out(&self, r: Rect) -> bool {
1201                r.y > 300
1202            }
1203            crate::terrain_layer!();
1204        }
1205        let mut hero = Platformer::builder()
1206            .spawn(Spot::TopLeft(220, 50))
1207            .respawn(false)
1208            .build()
1209            .unwrap();
1210        let mut events = Vec::new();
1211        for _ in 0..180 {
1212            hero.advance(Intent::default(), &mut Pit, 1.0 / 60.0);
1213            events.extend(hero.drain_events());
1214        }
1215        assert!(events.contains(&Event::FellOut));
1216        assert!(!events.contains(&Event::Respawned), "respawning was off");
1217    }
1218    #[test]
1219    fn standing_spawns_like_the_builder_route() {
1220        let mut hero = Platformer::at(Spot::Feet(50, 100), Preset::Snappy);
1221        let built = Platformer::builder()
1222            .preset(Preset::Snappy)
1223            .spawn(Spot::Feet(50, 100))
1224            .build()
1225            .unwrap();
1226        assert_eq!(hero.hitbox(), built.hitbox());
1227        assert_eq!(hero.preset(), Some(Preset::Snappy));
1228        let events = idle(&mut hero);
1229        assert!(hero.grounded(), "stands on the closure's floor at once");
1230        assert!(events.is_empty(), "{events:?}");
1231    }
1232
1233    #[test]
1234    fn accessors_match_the_body() {
1235        let mut hero = Platformer::at(Spot::TopLeft(20, 88), Preset::Precise);
1236        let run = Intent {
1237            move_x: 1.0,
1238            ..Intent::default()
1239        };
1240        for _ in 0..10 {
1241            hero.advance(run, &mut floor, 1.0 / 60.0);
1242        }
1243        let p = &hero.world.player;
1244        assert_eq!(hero.grounded(), p.grounded);
1245        assert_eq!(hero.velocity(), (p.vx, p.vy));
1246        assert!(hero.velocity().0 > 0.0);
1247        assert_eq!(hero.mode(), p.mode);
1248        assert_eq!(hero.dashes(), p.dashes);
1249        assert_eq!(hero.air_jumps(), p.air_jumps);
1250        assert_eq!(hero.stamina(), p.stamina);
1251        assert_eq!(hero.on_wall(), None);
1252
1253        // Against a wall at x = 60, grabbing it.
1254        let mut wall = |r: Rect| r.bottom() > 100 || r.right() > 60;
1255        let grab = Intent {
1256            move_x: 1.0,
1257            grab: true,
1258            ..Intent::default()
1259        };
1260        for _ in 0..30 {
1261            hero.advance(grab, &mut wall, 1.0 / 60.0);
1262        }
1263        assert_eq!(hero.mode(), Mode::Climb, "{:?}", hero.hitbox());
1264        assert_eq!(hero.on_wall(), Some(1));
1265        assert!(hero.stamina() < hero.world.tuning.stamina);
1266    }
1267
1268    #[test]
1269    fn falling_out_starts_the_animation_over() {
1270        // A floor up to x = 200, the character running off its end; the spawn on it.
1271        struct Pit;
1272        impl Terrain for Pit {
1273            fn solid(&self, r: Rect) -> bool {
1274                r.bottom() > 100 && r.x < 200
1275            }
1276            fn fell_out(&self, r: Rect) -> bool {
1277                r.y > 300
1278            }
1279            crate::terrain_layer!();
1280        }
1281        let mut hero = Platformer::at(Spot::TopLeft(180, 88), Preset::Hybrid);
1282        let run = Intent {
1283            move_x: 1.0,
1284            ..Intent::default()
1285        };
1286        let entry = {
1287            let mut h = Platformer::at(Spot::TopLeft(20, 88), Preset::Hybrid);
1288            h.respawn();
1289            h.state().to_owned()
1290        };
1291        let mut respawned = false;
1292        for _ in 0..600 {
1293            hero.step(&run, &mut Pit);
1294            if hero.drain_events().any(|e| e == Event::Respawned) {
1295                respawned = true;
1296                break;
1297            }
1298            assert!(hero.world.tick < 590, "never fell out");
1299        }
1300        assert!(respawned);
1301        assert_eq!(
1302            hero.state(),
1303            entry,
1304            "from the entry state, as after respawn()"
1305        );
1306        assert_eq!(
1307            hero.position(),
1308            (180.0, 88.0),
1309            "drawn at the spawn, not sliding there"
1310        );
1311    }
1312    #[test]
1313    fn a_scaled_terrain_plays_out_like_one_in_units() {
1314        // The same room in units and in pixels at 2 per unit: a floor, a wall, a ledge.
1315        let units = |r: Rect| r.bottom() > 100 || r.right() > 200 || (r.x < 60 && r.bottom() > 76);
1316        let pixels =
1317            |r: Rect| r.bottom() > 200 || r.right() > 400 || (r.x < 120 && r.bottom() > 152);
1318        let mut a = Platformer::at(Spot::Feet(100, 100), Preset::Snappy);
1319        let mut b = Platformer::builder()
1320            .preset(Preset::Snappy)
1321            .scale(2)
1322            .spawn(Spot::Feet(200, 200))
1323            .build()
1324            .unwrap();
1325        assert_eq!(b.hitbox(), a.hitbox().scaled(2));
1326        let script = |k: u32| Intent {
1327            move_x: if k % 120 < 70 { 1.0 } else { -1.0 },
1328            jump_pressed: k.is_multiple_of(50),
1329            jump_held: k % 50 < 25,
1330            dash_pressed: k % 80 == 30,
1331            grab: k % 200 > 150,
1332            ..Intent::default()
1333        };
1334        for k in 0..600 {
1335            a.advance(script(k), &mut units.clone(), 1.0 / 60.0);
1336            b.advance(script(k), &mut pixels.clone(), 1.0 / 60.0);
1337            assert_eq!(b.hitbox(), a.hitbox().scaled(2), "frame {k}");
1338            assert!(
1339                a.drain_events()
1340                    .map(|e| e.in_pixels(2))
1341                    .eq(b.drain_events()),
1342                "frame {k}"
1343            );
1344            let ((ax, ay), (bx, by)) = (a.position(), b.position());
1345            assert!((bx - 2.0 * ax).abs() < 1e-3 && (by - 2.0 * ay).abs() < 1e-3);
1346            let ((avx, avy), (bvx, bvy)) = (a.velocity(), b.velocity());
1347            assert!((bvx - 2.0 * avx).abs() < 1e-3 && (bvy - 2.0 * avy).abs() < 1e-3);
1348        }
1349
1350        // In and out of the facade: pixels.
1351        b.teleport(Spot::TopLeft(240, 100));
1352        assert_eq!((b.hitbox().x, b.hitbox().y), (240, 100));
1353        assert_eq!((b.world.player.x, b.world.player.y), (120, 50));
1354        b.launch(-200.0, 0.0);
1355        assert_eq!(b.velocity(), (-200.0, 0.0));
1356        assert_eq!(b.world.player.vx, -100.0);
1357        b.set_spawn(Spot::Feet(200, 200));
1358        assert_eq!(b.spawn(), Spot::Feet(200, 200));
1359        b.respawn();
1360        a.respawn_at(Spot::Feet(100, 100));
1361        assert_eq!(b.hitbox(), a.hitbox().scaled(2));
1362        assert_eq!(b.hitbox().bottom(), 200);
1363    }
1364
1365    #[test]
1366    fn an_ability_asks_a_found_terrain_in_its_pixels() {
1367        /// A floor and a marked column, in pixels at 2 per unit.
1368        struct Map {
1369            /// The marked column's pixels, left to right.
1370            marked: std::ops::Range<i32>,
1371        }
1372        impl Terrain for Map {
1373            fn solid(&self, r: Rect) -> bool {
1374                r.bottom() > 200
1375            }
1376            crate::terrain_layer!();
1377        }
1378        /// Counts the steps the body overlaps the marked column.
1379        #[derive(Clone, Debug, Default)]
1380        struct Probe {
1381            /// Steps on the mark, asked with the scaled rect.
1382            hits: u32,
1383            /// Steps on the mark, asked with the rect in units (the mistake).
1384            unscaled: u32,
1385        }
1386        impl Ability for Probe {
1387            fn before(&mut self, s: &mut Step<'_>) {
1388                let body = s.player.rect();
1389                let k = s.scale;
1390                let map = s.terrain.find_mut::<Map>().expect("the game's map");
1391                let on = |r: Rect| map.marked.start < r.right() && r.x < map.marked.end;
1392                self.hits += u32::from(on(body.scaled(k)));
1393                self.unscaled += u32::from(on(body));
1394            }
1395        }
1396        let mut map = Map { marked: 200..216 };
1397        let mut hero = Platformer::builder()
1398            .scale(2)
1399            .spawn(Spot::Feet(206, 200))
1400            .ability(Probe::default())
1401            .build()
1402            .unwrap();
1403        hero.advance(Intent::default(), &mut map, 1.0 / 60.0);
1404        assert_eq!(hero.world().scale(), 2);
1405        let probe = hero.ability::<Probe>().unwrap();
1406        assert_eq!(probe.hits, 2, "both steps on the mark, in pixels");
1407        assert_eq!(probe.unscaled, 0, "units against pixels miss it");
1408    }
1409
1410    #[test]
1411    fn a_scaled_landing_reports_pixels() {
1412        let drop_of = |k: i32| {
1413            let mut floor = move |r: Rect| r.bottom() > 100 * k;
1414            let mut hero = Platformer::builder()
1415                .scale(k)
1416                .spawn(Spot::Feet(20 * k, 50 * k))
1417                .build()
1418                .unwrap();
1419            (0..120)
1420                .flat_map(|_| {
1421                    hero.advance(Intent::default(), &mut floor, 1.0 / 60.0);
1422                    hero.drain_events().collect::<Vec<_>>()
1423                })
1424                .find_map(|e| match e {
1425                    Event::Landed { drop, impact } => Some((drop, impact)),
1426                    _ => None,
1427                })
1428                .expect("landed")
1429        };
1430        let ((d1, i1), (d2, i2)) = (drop_of(1), drop_of(2));
1431        assert!(d1 >= 40, "{d1}");
1432        assert_eq!(d2, 2 * d1, "the drop in the terrain's pixels");
1433        assert!((i2 - 2.0 * i1).abs() < 1e-3, "{i1} {i2}");
1434    }
1435
1436    #[test]
1437    fn set_velocity_and_facing_change_only_that() {
1438        let mut hero = Platformer::builder()
1439            .scale(2)
1440            .spawn(Spot::Feet(40, 200))
1441            .build()
1442            .unwrap();
1443        let mut floor = |r: Rect| r.bottom() > 200;
1444        hero.advance(Intent::default(), &mut floor, 1.0 / 60.0);
1445        hero.set_velocity(-60.0, 0.0);
1446        assert_eq!(hero.velocity(), (-60.0, 0.0), "in pixels/s");
1447        assert_eq!(hero.world.player.vx, -30.0, "units inside");
1448        assert!(hero.grounded(), "still standing: not a launch");
1449        hero.set_facing(-5);
1450        assert_eq!(hero.facing(), -1);
1451        hero.set_facing(0);
1452        assert_eq!(hero.facing(), -1, "0 keeps it");
1453        hero.set_facing(1);
1454        assert_eq!(hero.facing(), 1);
1455    }
1456
1457    #[test]
1458    fn hitboxes_hold_every_step_of_the_frame() {
1459        let mut hero = Platformer::at(Spot::Feet(20, 100), Preset::Hybrid);
1460        let run = Intent {
1461            move_x: 1.0,
1462            ..Intent::default()
1463        };
1464        assert_eq!(hero.advance(run, &mut floor, 1.0 / 15.0), 8);
1465        let boxes = hero.hitboxes().to_vec();
1466        assert_eq!(boxes.len(), 8);
1467        assert_eq!(*boxes.last().unwrap(), hero.hitbox());
1468        assert!(boxes.windows(2).all(|p| p[0].x <= p[1].x), "{boxes:?}");
1469        assert!(boxes[0].x < boxes[7].x, "moved along");
1470        // A frame without a step has none; a single step has one.
1471        hero.advance(run, &mut floor, 0.0);
1472        assert!(hero.hitboxes().is_empty());
1473        hero.step(&run, &mut floor);
1474        assert_eq!(hero.hitboxes(), [hero.hitbox()]);
1475    }
1476
1477    #[test]
1478    fn touched_and_touches_find_a_steps_hitbox_by_value() {
1479        let mut level = Level::parse_with(&["......", ".P.^..", "######"], |c| {
1480            (c == '^').then_some(crate::sim::Tile::Custom(0))
1481        });
1482        let mut hero = Platformer::on(&level, Preset::Hybrid);
1483        let run = Intent {
1484            move_x: 1.0,
1485            ..Intent::default()
1486        };
1487        let spikes = crate::sim::Tile::Custom(0);
1488        let mut hit = None;
1489        for _ in 0..60 {
1490            hero.advance(run, &mut level, 1.0 / 15.0);
1491            if let Some(r) = level.touches(hero.hitboxes(), spikes) {
1492                hit = Some(r);
1493                hero.respawn(); // the hitbox is a copy: no borrow left
1494                break;
1495            }
1496        }
1497        let r = hit.expect("ran into the spikes");
1498        assert!(r.right() > 24 && r.x < 32, "{r:?}");
1499        assert!(
1500            hero.touched(r).is_some(),
1501            "r is one of the frame's hitboxes"
1502        );
1503        assert_eq!(level.touches(&[], spikes), None);
1504        assert_eq!(
1505            hero.touched(Rect {
1506                x: 500,
1507                y: 0,
1508                w: 1,
1509                h: 1
1510            }),
1511            None
1512        );
1513    }
1514
1515    #[test]
1516    fn runtime_changes_keep_the_facade_consistent() {
1517        let mut hero = Platformer::at(Spot::Feet(20, 100), Preset::Snappy);
1518        hero.tune(|t| t.air_jumps = 3);
1519        assert_eq!(hero.world.tuning.air_jumps, 3);
1520        assert_eq!(hero.preset(), None, "no longer the preset's feel");
1521        hero.set_preset(Preset::Precise);
1522        assert_eq!(hero.preset(), Some(Preset::Precise));
1523
1524        // A graph without the built-in driver's parameters is refused, and nothing changes.
1525        let tiny = GraphSpec::new().state(StateSpec::new(
1526            "idle",
1527            Clip::uniform(Playback::Loop, &[0], 0.1),
1528        ));
1529        let before = hero.state().to_owned();
1530        assert!(matches!(
1531            hero.set_graph(tiny.clone()),
1532            Err(SpecError::UnknownParam(_))
1533        ));
1534        assert_eq!(hero.state(), before);
1535        hero.set_partial_graph(two_state_graph()).unwrap();
1536        idle(&mut hero);
1537        assert_eq!(hero.state(), "idle", "the new graph runs, driven");
1538        hero.set_graph(spec()).unwrap();
1539        idle(&mut hero);
1540        assert_eq!(hero.state(), "idle");
1541    }
1542
1543    /// Holding custom button 0 powers up; the level drives `power` and `powering`.
1544    #[derive(Clone, Debug, Default)]
1545    struct Charge {
1546        /// Seconds charged.
1547        level: f32,
1548    }
1549
1550    impl Ability for Charge {
1551        fn before(&mut self, s: &mut crate::sim::Step<'_>) {
1552            if s.intent.held(0) {
1553                self.level += s.dt;
1554                s.moves.max_run = 0.0;
1555            } else {
1556                self.level = 0.0;
1557            }
1558        }
1559    }
1560
1561    impl Animate for Charge {
1562        const PARAMS: &'static [(&'static str, ParamKind)] =
1563            &[("power", ParamKind::Float), ("powering", ParamKind::Bool)];
1564
1565        fn drive(&self, _world: &World, p: &mut Params, ids: &[ParamId]) {
1566            p.set_float(ids[0], self.level);
1567            p.set_bool(ids[1], self.level > 0.0);
1568        }
1569    }
1570
1571    #[test]
1572    fn an_animated_ability_drives_its_own_parameters() {
1573        use crate::anim::{Cond, TransitionSpec};
1574        let graph = spec()
1575            .float("power")
1576            .bool("powering")
1577            .state(StateSpec::new(
1578                "power",
1579                Clip::uniform(Playback::Loop, &[0], 0.1),
1580            ))
1581            .transition(TransitionSpec::new(
1582                ["idle"],
1583                "power",
1584                Cond::Is("powering".into()),
1585            ));
1586        let mut hero = Platformer::builder()
1587            .graph(graph.clone())
1588            .animated(Charge::default())
1589            .spawn(Spot::Feet(20, 100))
1590            .build()
1591            .unwrap();
1592        idle(&mut hero);
1593        let hold = Intent {
1594            custom: 1,
1595            ..Intent::default()
1596        };
1597        for _ in 0..30 {
1598            hero.advance(hold, &mut floor, 1.0 / 60.0);
1599        }
1600        let charge = hero.animator.params.id("power").unwrap();
1601        assert!((hero.animator.params.get_float(charge) - 0.5).abs() < 0.02);
1602        assert_eq!(hero.state(), "power");
1603        assert_eq!(
1604            hero.world.ability::<Charge>().map(|c| c.level > 0.4),
1605            Some(true)
1606        );
1607
1608        // Added at runtime, the same.
1609        let mut hero = Platformer::builder()
1610            .graph(graph)
1611            .spawn(Spot::Feet(20, 100))
1612            .build()
1613            .unwrap();
1614        hero.add_ability(Brakes);
1615        hero.add_animated(Charge::default()).unwrap();
1616        for _ in 0..10 {
1617            hero.advance(hold, &mut floor, 1.0 / 60.0);
1618        }
1619        assert_eq!(hero.state(), "power");
1620        assert_eq!(hero.world.abilities.len(), 2);
1621    }
1622
1623    #[test]
1624    fn removing_or_reordering_abilities_keeps_each_driving_its_own_animation() {
1625        let graph = spec().float("power").bool("powering");
1626        let mut hero = Platformer::builder()
1627            .partial_graph(graph)
1628            .ability(Brakes)
1629            .animated(Charge::default())
1630            .spawn(Spot::Feet(20, 100))
1631            .build()
1632            .unwrap();
1633        idle(&mut hero);
1634        let hold = Intent {
1635            custom: 1,
1636            ..Intent::default()
1637        };
1638        let power = hero.animator().params.id("power").unwrap();
1639        let charged = |hero: &Platformer| hero.animator().params.get_float(power);
1640
1641        // The ability before it goes: the charge still drives `power`.
1642        assert!(hero.remove_ability::<Brakes>().is_some());
1643        assert!(
1644            hero.remove_ability::<Brakes>().is_none(),
1645            "only one was there"
1646        );
1647        for _ in 0..15 {
1648            hero.advance(hold, &mut floor, 1.0 / 60.0);
1649        }
1650        assert!(charged(&hero) > 0.2, "{}", charged(&hero));
1651
1652        // Reordered through the world: the same.
1653        hero.add_ability(Brakes);
1654        hero.world_mut().abilities.reverse();
1655        for _ in 0..15 {
1656            hero.advance(hold, &mut floor, 1.0 / 60.0);
1657        }
1658        assert!(charged(&hero) > 0.45, "{}", charged(&hero));
1659        assert_eq!(
1660            hero.ability::<Charge>().map(|c| c.level),
1661            Some(charged(&hero))
1662        );
1663        hero.ability_mut::<Charge>().unwrap().level = 2.0;
1664        assert_eq!(hero.ability::<Charge>().unwrap().level, 2.0);
1665
1666        // Removed itself: its hook goes with it, the parameter stays where it was.
1667        let removed = hero.remove_ability::<Charge>().unwrap();
1668        assert_eq!(removed.level, 2.0);
1669        let before = charged(&hero);
1670        for _ in 0..15 {
1671            hero.advance(hold, &mut floor, 1.0 / 60.0);
1672        }
1673        assert_eq!(charged(&hero), before);
1674        assert!(hero.animated.is_empty());
1675
1676        // Adding the same kind twice drives it once.
1677        hero.add_animated(Charge::default()).unwrap();
1678        hero.add_animated(Charge::default()).unwrap();
1679        assert_eq!(hero.animated.len(), 1);
1680    }
1681
1682    #[test]
1683    fn an_animated_abilitys_parameters_are_checked_when_the_graph_is_built() {
1684        let only = |power: ParamKind| {
1685            let g = spec().bool("powering");
1686            match power {
1687                ParamKind::Float => g.float("power"),
1688                _ => g.bool("power"),
1689            }
1690        };
1691        let build = |g: GraphSpec| {
1692            Platformer::builder()
1693                .partial_graph(g)
1694                .animated(Charge::default())
1695                .build()
1696        };
1697        assert!(matches!(
1698            build(spec().bool("powering")).err(),
1699            Some(SpecError::UnknownParam(n)) if n == "power"
1700        ));
1701        assert!(matches!(
1702            build(only(ParamKind::Bool)).err(),
1703            Some(SpecError::WrongKind { param, declared: ParamKind::Bool }) if param == "power"
1704        ));
1705        let mut hero = build(only(ParamKind::Float)).unwrap();
1706
1707        // Swapping in a graph without them: refused, nothing changes.
1708        let before = hero.animator().graph().find("idle");
1709        assert!(hero.set_partial_graph(spec()).is_err());
1710        assert_eq!(hero.animator().graph().find("idle"), before);
1711        assert!(hero.animator().params.id("power").is_some());
1712
1713        // At runtime, the same check; nothing is added on an error.
1714        let mut plain = Platformer::builder().build().unwrap();
1715        assert!(plain.add_animated(Charge::default()).is_err());
1716        assert!(plain.ability::<Charge>().is_none());
1717    }
1718
1719    #[test]
1720    fn accessors_read_what_games_need_without_the_world() {
1721        let mut hero = Platformer::builder()
1722            .tune(|t| t.air_jumps = 2)
1723            .spawn(Spot::Feet(20, 100))
1724            .build()
1725            .unwrap();
1726        assert_eq!(hero.tuning().air_jumps, 2);
1727        // Run into a wall at x = 60.
1728        let mut walled = |r: Rect| r.bottom() > 100 || r.right() > 60;
1729        let run = Intent {
1730            move_x: 1.0,
1731            ..Intent::default()
1732        };
1733        let mut hit = false;
1734        for _ in 0..30 {
1735            hero.advance(run, &mut walled, 1.0 / 60.0);
1736            hit |= hero.contacts().wall == 1;
1737        }
1738        assert!(hit, "ran into the wall");
1739        assert_eq!(hero.contacts(), hero.world().player.contacts);
1740        assert_eq!(hero.animator().state_name(), hero.state());
1741    }
1742
1743    /// An ability that does nothing, to sit before another.
1744    #[derive(Clone, Debug)]
1745    struct Brakes;
1746
1747    impl Ability for Brakes {}
1748
1749    #[test]
1750    fn draining_keeps_the_buffers() {
1751        let mut hero = Platformer::at(Spot::Feet(20, 100), Preset::Hybrid);
1752        let jump = Intent {
1753            jump_pressed: true,
1754            jump_held: true,
1755            ..Intent::default()
1756        };
1757        hero.advance(jump, &mut floor, 1.0 / 60.0);
1758        let cap = hero.events.capacity();
1759        assert!(hero.drain_events().count() > 0);
1760        assert_eq!(hero.events.capacity(), cap, "no new allocation next frame");
1761        assert!(hero.drain_events().next().is_none());
1762        hero.drain_anim_events().for_each(drop);
1763        assert!(hero.anim_events.capacity() > 0);
1764    }
1765}