Skip to main content

game_toolkit_platformer/sim/
ability.rs

1//! [`Ability`]: a move of your game's own (an attack lunge, a ledge grab, a grapple, a
2//! pogo), hooked into [`World::step`](super::World::step) without forking the movement.
3//!
4//! An ability runs twice per step: [`Ability::before`] ahead of the built-in moves, and
5//! [`Ability::after`] once they moved the body. It may change anything public on the
6//! [`Player`] (speed, facing, refills, [`Player::launch`] and [`Player::teleport`]), read
7//! the [`Intent`] (your own buttons arrive in [`Intent::custom`]), read and change the
8//! terrain (your own type through [`find_mut`](trait.Terrain.html#method.find_mut): break a
9//! block, flip a switch, use up a pickup; the movement sees the change at once; it answers
10//! in its own pixels under a `Builder::scale`, so scale the body's rects by
11//! [`Step::scale`] first), and report
12//! [`Event::Custom`]s (with up to four numbers of data: a knockback, a hit target).
13//!
14//! Name your modes and events with a [`CustomId`](super::CustomId) after your crate
15//! (`CustomId::new("my_crate::lunge")`): abilities from different crates then never have
16//! to agree on numbers. Custom buttons ([`Intent::custom`]) belong to the game, which binds
17//! them to keys: an ability meant for others takes its button's bit as a parameter.
18//!
19//! To replace a built-in move, rule it out and do your own in its place. To react to
20//! collisions, read [`Player::contacts`] in [`Ability::after`]. In a [`crate::Party`],
21//! [`Step::others`] holds the other characters' bodies.
22//!
23//! To rule out single built-in moves for a step (no dash while attacking, no wall grab
24//! while carrying, slow walking while pushing), clear them in [`Step::moves`] in
25//! [`Ability::before`].
26//!
27//! To take the body over for a while, put it in [`Mode::Custom`](super::Mode::Custom):
28//! no built-in move starts or runs then (no running, no gravity, no jumps or dashes), but
29//! the body still moves by its speed through the collision, lands and is carried by moving
30//! ground. The mode's clock counts up on its own; set [`Mode::Normal`](super::Mode::Normal)
31//! to hand the body back.
32//!
33//! Abilities are part of the world: a cloned world clones them, and it must play out
34//! identically, so keep their state in the ability (no clocks or randomness from outside)
35//! and derive `Clone`.
36//!
37//! ```
38//! use game_toolkit_platformer::sim::{Ability, CustomId, Event, Intent, Mode, Preset, Spot, Step, World};
39//!
40//! /// Custom button 0 lunges forward on the ground.
41//! #[derive(Clone, Debug)]
42//! struct Lunge;
43//!
44//! /// The lunge's mode and event id, named after the crate so no other ability's collides.
45//! const LUNGE: CustomId = CustomId::new("my_game::lunge");
46//!
47//! impl Ability for Lunge {
48//!     fn before(&mut self, s: &mut Step<'_>) {
49//!         let p = &mut *s.player;
50//!         match p.mode {
51//!             Mode::Normal if s.intent.pressed(0) && p.grounded() => {
52//!                 p.mode = Mode::Custom { id: LUNGE, t: 0.0 };
53//!                 p.vx = 240.0 * p.facing as f32;
54//!                 p.vy = 0.0;
55//!                 s.events.push(Event::custom(LUNGE).with([p.vx, 0.0, 0.0, 0.0]));
56//!             }
57//!             Mode::Custom { id: LUNGE, t } if t >= 0.15 => p.mode = Mode::Normal,
58//!             _ => {}
59//!         }
60//!         // No dash out of the lunge's recovery, while custom button 0 is held.
61//!         s.moves.dash = !s.intent.held(0);
62//!     }
63//! }
64//!
65//! let mut world = World::new(Preset::Hybrid.tuning(), Spot::TopLeft(20, 88));
66//! world.abilities.push(Box::new(Lunge));
67//! let mut floor = |r: game_toolkit_platformer::sim::Rect| r.bottom() > 100;
68//! world.step(&mut floor, &Intent { custom_pressed: 1, ..Intent::default() });
69//! assert!(world.events.iter().any(|e| e.custom_data(LUNGE).is_some()));
70//! ```
71
72use std::any::Any;
73use std::fmt::Debug;
74
75use super::{Event, Intent, Moves, Player, Rect, Terrain, Tuning};
76
77/// What an [`Ability`] gets to work with in one step.
78pub struct Step<'a> {
79    /// The body.
80    pub player: &'a mut Player,
81    /// This step's input.
82    pub intent: &'a Intent,
83    /// The world around it; changes count from here on, in this step too.
84    pub terrain: &'a mut dyn Terrain,
85    /// The movement feel.
86    pub tuning: &'a Tuning,
87    /// The built-in moves allowed this step: clear some in [`Ability::before`]. Changes in
88    /// [`Ability::after`] come too late for this step (and the next starts from
89    /// [`Moves::ALL`] again).
90    pub moves: &'a mut Moves,
91    /// This step's events so far; push yours ([`Event::Custom`]).
92    pub events: &'a mut Vec<Event>,
93    /// The other characters' bodies on the same terrain (a [`crate::Party`]), as the step
94    /// started for those yet to move and as they ended up for those already moved; empty
95    /// for a character on its own.
96    pub others: &'a [Rect],
97    /// The step's length in seconds ([`super::DT`]).
98    pub dt: f32,
99    /// Pixels of the game's terrain per unit ([`super::World::scale`]; 1 without a
100    /// `Builder::scale`). Everything in the step is in units, but a terrain of the game's
101    /// own reached through [`find_mut`](trait.Terrain.html#method.find_mut) answers in its
102    /// pixels: ask it with `rect.scaled(s.scale)`.
103    pub scale: i32,
104}
105
106/// A move of your own, run in every [`World::step`](super::World::step). Derive `Clone`
107/// and `Debug`; both hooks default to nothing.
108pub trait Ability: AbilityClone + Debug {
109    /// Ahead of the built-in moves: start your move, steer it, end it; rule out built-in
110    /// moves ([`Step::moves`]).
111    fn before(&mut self, _s: &mut Step<'_>) {}
112
113    /// After the built-in moves, collision and landing: react to where the body ended up
114    /// and what it ran into ([`Player::contacts`]: a wall, a ceiling, the floor).
115    fn after(&mut self, _s: &mut Step<'_>) {}
116}
117
118/// Cloning a boxed [`Ability`], so a world with abilities stays `Clone`, and reaching its
119/// concrete type ([`super::World::ability`]). Implemented for every `Ability + Clone`; you
120/// don't implement it yourself.
121pub trait AbilityClone {
122    /// A boxed copy.
123    fn clone_box(&self) -> Box<dyn Ability>;
124    /// Itself, to downcast.
125    fn as_any(&self) -> &dyn Any;
126    /// Itself, to downcast and change.
127    fn as_any_mut(&mut self) -> &mut dyn Any;
128}
129
130impl<T: Ability + Clone + 'static> AbilityClone for T {
131    fn clone_box(&self) -> Box<dyn Ability> {
132        Box::new(self.clone())
133    }
134
135    fn as_any(&self) -> &dyn Any {
136        self
137    }
138
139    fn as_any_mut(&mut self) -> &mut dyn Any {
140        self
141    }
142}
143
144impl Clone for Box<dyn Ability> {
145    fn clone(&self) -> Self {
146        self.clone_box()
147    }
148}