Skip to main content

game_toolkit_platformer/sim/
tuning.rs

1//! [`Tuning`]: every number that shapes how the hero moves. Units and seconds
2//! (the movement's space: pixels at a scale of 1; see `docs/scale.md`).
3//!
4//! The defaults ([`Preset::Hybrid`]) sit near Celeste's (same 8-unit tiles: run, jump,
5//! dash, climb), with Hollow Knight's heavier fall, jump cut, double jump, dive and landing
6//! roll mixed in. The other [`Preset`]s lean all the way toward one game's feel; abilities
7//! a preset doesn't have are switched off (zero counts, false flags, unreachable drops).
8
9/// A named starting point for [`Tuning`].
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum Preset {
12    /// Everything on: the default.
13    Hybrid,
14    /// Precise and floaty: 8-way dash, climbing with stamina, no air jump, no jump cut
15    /// (tuned toward the feel of Celeste).
16    Precise,
17    /// Snappy: instant run, hard jump cut, double jump, horizontal dash, wall cling that
18    /// slips, dive, super dash, hard landings (tuned toward the feel of Hollow Knight).
19    Snappy,
20    /// Fast and slippery: walk or hold to run, momentum, strong wall jumps, wall stick,
21    /// nothing else (tuned toward the feel of Super Meat Boy).
22    Momentum,
23    /// Snappy with a double jump that leads into a glide while jump is held; sideways dash,
24    /// wall jumps, no dive (tuned toward the feel of Hollow Knight: Silksong).
25    Drifter,
26}
27
28impl Preset {
29    /// Every preset, in cycling order.
30    pub const ALL: [Preset; 5] = [
31        Preset::Hybrid,
32        Preset::Precise,
33        Preset::Snappy,
34        Preset::Drifter,
35        Preset::Momentum,
36    ];
37
38    /// Display name.
39    pub fn name(self) -> &'static str {
40        match self {
41            Preset::Hybrid => "hybrid",
42            Preset::Precise => "precise",
43            Preset::Snappy => "snappy",
44            Preset::Momentum => "momentum",
45            Preset::Drifter => "drifter",
46        }
47    }
48
49    /// The next preset, wrapping.
50    pub fn next(self) -> Preset {
51        let i = Self::ALL.iter().position(|&p| p == self).unwrap_or(0);
52        Self::ALL[(i + 1) % Self::ALL.len()]
53    }
54
55    /// The tuning.
56    pub fn tuning(self) -> Tuning {
57        let base = Tuning::default();
58        let never = i32::MAX;
59        match self {
60            Preset::Hybrid => base,
61            Preset::Precise => Tuning {
62                fall_gravity: 1.0,
63                jump_cut: 1.0,
64                air_jumps: 0,
65                dive: false,
66                roll_drop: never,
67                hard_land_drop: never,
68                wall_stick_time: 0.0,
69                // Only the ground (and crystals) give the dash back.
70                wall_refills_dashes: false,
71                glide: false,
72                run_roll: false,
73                sprint: false,
74                super_dash: false,
75                ..base
76            },
77            Preset::Snappy => Tuning {
78                run_speed: 83.0,
79                run_accel: 4000.0,
80                turn_accel: 4000.0,
81                run_decel: 4000.0,
82                air_control: 1.0,
83                jump_cut: 0.15,
84                apex_gravity: 0.75,
85                dash_eight_way: false,
86                dash_end_speed: 40.0,
87                grab: false,
88                wall_slide_speed: 60.0,
89                wall_jump_lock: 0.2,
90                wall_climb_speed: 80.0,
91                wall_climb_lock: 0.08,
92                roll_drop: never,
93                wings: true,
94                glide: false,
95                run_roll: false,
96                sprint: false,
97                ..base
98            },
99            Preset::Drifter => Tuning {
100                dive: false,
101                super_dash: false,
102                glide: true,
103                run_roll: false,
104                wings: false,
105                ..Preset::Snappy.tuning()
106            },
107            Preset::Momentum => Tuning {
108                // Walk, and hold the run button to run, as in Super Meat Boy.
109                run_speed: 90.0,
110                sprint_speed: 160.0,
111                run_accel: 600.0,
112                turn_accel: 1400.0,
113                run_decel: 900.0,
114                overspeed_decel: 200.0,
115                air_control: 0.9,
116                max_fall: 220.0,
117                fast_fall: 300.0,
118                jump_speed: 120.0,
119                var_jump_time: 0.18,
120                jump_cut: 0.5,
121                coyote_time: 0.06,
122                air_jumps: 0,
123                dashes: 0,
124                grab: false,
125                dive: false,
126                wall_slide_speed: 50.0,
127                wall_jump_speed: 170.0,
128                wall_jump_lock: 0.05,
129                wall_stick_time: 0.12,
130                wall_climb_speed: 90.0,
131                wall_climb_lock: 0.05,
132                roll_drop: never,
133                hard_land_drop: never,
134                glide: false,
135                run_roll: false,
136                super_dash: false,
137                ..base
138            },
139        }
140    }
141}
142
143/// The movement feel. All lengths in units, speeds units/s, accelerations units/s², times
144/// seconds.
145#[derive(Clone, Copy, Debug, PartialEq)]
146pub struct Tuning {
147    /// The hitbox's width and height in units (default 6x12, [`super::player::W`] x
148    /// [`super::player::H`]). The other numbers stay in units and don't scale with it:
149    /// tuned for a 6x12 body on 8-unit tiles, a much bigger body may want a higher jump.
150    pub size: (i32, i32),
151
152    /// Top running speed.
153    pub run_speed: f32,
154    /// A sprint button exists.
155    pub sprint: bool,
156    /// Top speed while sprinting.
157    pub sprint_speed: f32,
158    /// Acceleration toward the stick's speed on the ground.
159    pub run_accel: f32,
160    /// Acceleration when the stick points against the motion (turning around).
161    pub turn_accel: f32,
162    /// Deceleration with the stick released.
163    pub run_decel: f32,
164    /// Deceleration toward top speed when moving faster than it (after a roll jump or a
165    /// dash), so momentum carries.
166    pub overspeed_decel: f32,
167    /// Multiplier on all horizontal acceleration in the air.
168    pub air_control: f32,
169
170    /// Gravity.
171    pub gravity: f32,
172    /// Gravity multiplier while falling, for a heavier way down.
173    pub fall_gravity: f32,
174    /// Below this vertical speed (either way) with jump held, the hero is "at the apex".
175    pub apex_threshold: f32,
176    /// Gravity multiplier at the apex with jump held: the hangtime.
177    pub apex_gravity: f32,
178    /// Terminal falling speed.
179    pub max_fall: f32,
180    /// Terminal falling speed with down held.
181    pub fast_fall: f32,
182
183    /// Upward speed of a ground jump.
184    pub jump_speed: f32,
185    /// Horizontal speed added by a ground jump in the stick's direction.
186    pub jump_boost: f32,
187    /// Seconds after a jump during which holding jump keeps the full upward speed.
188    pub var_jump_time: f32,
189    /// Releasing jump while rising multiplies the upward speed by this (1: no cut).
190    pub jump_cut: f32,
191    /// Seconds after leaving a ledge during which a ground jump still works.
192    pub coyote_time: f32,
193    /// Seconds a jump press waits for the jump to become possible.
194    pub jump_buffer: f32,
195    /// Seconds after dropping through a platform during which platforms don't hold.
196    pub drop_through_time: f32,
197
198    /// Jumps available in the air before landing again.
199    pub air_jumps: u8,
200    /// Upward speed of an air jump.
201    pub double_jump_speed: f32,
202    /// Hold window of an air jump, like [`Tuning::var_jump_time`].
203    pub double_var_jump_time: f32,
204    /// Falling with ground this many units below, a jump press waits for the landing
205    /// (a queued ground jump) instead of spending the air jump.
206    pub land_jump_lookahead: i32,
207
208    /// Dashes available before touching ground or a crystal again (0: no dash).
209    pub dashes: u8,
210    /// Dashes go eight ways; otherwise only sideways.
211    pub dash_eight_way: bool,
212    /// Speed during a dash.
213    pub dash_speed: f32,
214    /// Length of a dash.
215    pub dash_time: f32,
216    /// Speed handed over when a dash ends.
217    pub dash_end_speed: f32,
218    /// Multiplier on the handed-over speed of upward dashes.
219    pub dash_end_up: f32,
220    /// Seconds after a dash starts before another one can.
221    pub dash_cooldown: f32,
222    /// Seconds a dash press waits for a dash to become possible.
223    pub dash_buffer: f32,
224    /// Seconds into a dash before the ground refills dashes (so dashing along the floor
225    /// isn't free).
226    pub dash_recharge_after: f32,
227    /// Seconds before a used crystal comes back.
228    pub crystal_respawn: f32,
229
230    /// Grabbing walls and climbing is possible at all.
231    pub grab: bool,
232    /// Holding still on a wall slides down at this speed: 0 holds firm, more is a grip
233    /// that slips.
234    pub slip_speed: f32,
235    /// Below this stamina even a firm grip slips, at [`Tuning::tired_slip_speed`].
236    pub tired_stamina: f32,
237    /// How fast a tired grip slips.
238    pub tired_slip_speed: f32,
239    /// Stamina when full; climbing spends it, the ground refills it.
240    pub stamina: f32,
241    /// Climbing speed up a wall.
242    pub climb_up_speed: f32,
243    /// Climbing speed down a wall.
244    pub climb_down_speed: f32,
245    /// Stamina per second climbing up.
246    pub climb_up_cost: f32,
247    /// Stamina per second holding still on a wall.
248    pub climb_still_cost: f32,
249    /// Stamina for a jump straight up a wall.
250    pub climb_jump_cost: f32,
251    /// Upward speed of the hop over the top of a wall.
252    pub climb_hop_speed: f32,
253    /// Terminal falling speed pressing into a wall without grabbing.
254    pub wall_slide_speed: f32,
255    /// Horizontal speed of a wall jump.
256    pub wall_jump_speed: f32,
257    /// Seconds after a wall jump during which the stick can't pull back to the wall.
258    pub wall_jump_lock: f32,
259    /// A wall this many units away still counts for a wall jump.
260    pub wall_jump_reach: i32,
261    /// Horizontal speed of a wall jump made holding toward the wall: small, so the hero
262    /// arcs back onto the same wall (Hollow Knight, Super Meat Boy). Equal to
263    /// [`Tuning::wall_jump_speed`] to turn it off.
264    pub wall_climb_speed: f32,
265    /// Stick lock of such a wall jump. Between toward and away, speed and lock blend.
266    pub wall_climb_lock: f32,
267    /// How much stick up or down tilts a wall jump's upward speed (0.15: +-15 %).
268    pub wall_jump_aim: f32,
269    /// Touching a wall (sliding or holding on) gives the air jumps back.
270    pub wall_refills_air_jumps: bool,
271    /// Touching a wall gives the dashes back too.
272    pub wall_refills_dashes: bool,
273    /// Sliding down a wall, pressing away keeps the hero stuck to it this long, so
274    /// "away + jump" becomes a wall jump instead of a fall.
275    pub wall_stick_time: f32,
276
277    /// Diving is possible at all.
278    pub dive: bool,
279
280    /// Gliding: a jump pressed in the air and held (after the air jump, if one is left)
281    /// opens a cloak once falling.
282    pub glide: bool,
283    /// Terminal falling speed gliding.
284    pub glide_fall_speed: f32,
285
286    /// Super dash (Hollow Knight's Crystal Heart): hold to charge on the ground or on a
287    /// wall, release when charged to fly straight and level until a wall or a cancel.
288    pub super_dash: bool,
289    /// Seconds to charge it.
290    pub super_charge: f32,
291    /// Flying speed.
292    pub super_speed: f32,
293    /// Horizontal speed kept when the flight is cancelled (jump or the button again).
294    pub super_cancel_speed: f32,
295
296    /// Tapping down while running at speed rolls.
297    pub run_roll: bool,
298    /// Horizontal speed needed for a roll from a run.
299    pub run_roll_speed: f32,
300
301    /// Presentation: air jumps flap wings (Hollow Knight's Monarch Wings) instead of a
302    /// flip. Read by the hero's animation driver, not by the movement.
303    pub wings: bool,
304
305    /// Seconds hanging in the air before a dive plunges.
306    pub dive_windup: f32,
307    /// Speed of a dive.
308    pub dive_speed: f32,
309    /// How long a dive's impact takes away control.
310    pub dive_land_time: f32,
311
312    /// Fall height in units from which a landing while moving becomes a roll.
313    pub roll_drop: i32,
314    /// Roll speed (the roll keeps any faster speed it lands with).
315    pub roll_speed: f32,
316    /// Length of a roll.
317    pub roll_time: f32,
318    /// Seconds into a roll before it can be cancelled into a jump.
319    pub roll_jump_after: f32,
320    /// Fall height in units from which a landing without moving is a hard landing.
321    pub hard_land_drop: i32,
322    /// How long a hard landing takes away control.
323    pub hard_land_time: f32,
324
325    /// Units the head may be nudged sideways around a ceiling corner.
326    pub corner_correction: i32,
327
328    /// How much of the speed of moving ground (or a moving wall held) a jump off it keeps:
329    /// 1 all of it, 0 none. Kept for [`Tuning::coyote_time`] after leaving it.
330    pub lift_boost: f32,
331}
332
333impl Default for Tuning {
334    fn default() -> Self {
335        Self {
336            size: (super::player::W, super::player::H),
337
338            run_speed: 90.0,
339            sprint: true,
340            sprint_speed: 140.0,
341            run_accel: 1000.0,
342            turn_accel: 1600.0,
343            run_decel: 1200.0,
344            overspeed_decel: 400.0,
345            air_control: 0.65,
346
347            gravity: 900.0,
348            fall_gravity: 1.15,
349            apex_threshold: 40.0,
350            apex_gravity: 0.5,
351            max_fall: 160.0,
352            fast_fall: 240.0,
353
354            jump_speed: 105.0,
355            jump_boost: 40.0,
356            var_jump_time: 0.2,
357            jump_cut: 0.6,
358            coyote_time: 0.1,
359            jump_buffer: 0.1,
360            drop_through_time: 0.15,
361
362            air_jumps: 1,
363            double_jump_speed: 100.0,
364            double_var_jump_time: 0.12,
365            land_jump_lookahead: 6,
366
367            dashes: 1,
368            dash_eight_way: true,
369            dash_speed: 240.0,
370            dash_time: 0.15,
371            dash_end_speed: 160.0,
372            dash_end_up: 0.75,
373            dash_cooldown: 0.2,
374            dash_buffer: 0.08,
375            dash_recharge_after: 0.1,
376            crystal_respawn: 2.5,
377
378            grab: true,
379            slip_speed: 0.0,
380            tired_stamina: 20.0,
381            tired_slip_speed: 20.0,
382            stamina: 110.0,
383            climb_up_speed: 45.0,
384            climb_down_speed: 80.0,
385            climb_up_cost: 45.0,
386            climb_still_cost: 10.0,
387            climb_jump_cost: 27.5,
388            climb_hop_speed: 120.0,
389            wall_slide_speed: 40.0,
390            wall_jump_speed: 130.0,
391            wall_jump_lock: 0.16,
392            wall_jump_reach: 2,
393            wall_climb_speed: 70.0,
394            wall_climb_lock: 0.07,
395            wall_jump_aim: 0.15,
396            wall_refills_air_jumps: true,
397            wall_refills_dashes: true,
398            wall_stick_time: 0.1,
399
400            dive: true,
401
402            glide: true,
403            glide_fall_speed: 30.0,
404
405            super_dash: true,
406            super_charge: 0.8,
407            super_speed: 280.0,
408            super_cancel_speed: 90.0,
409
410            run_roll: true,
411            run_roll_speed: 100.0,
412
413            wings: false,
414
415            dive_windup: 0.08,
416            dive_speed: 320.0,
417            dive_land_time: 0.2,
418
419            roll_drop: 48,
420            roll_speed: 120.0,
421            roll_time: 0.32,
422            roll_jump_after: 0.05,
423            hard_land_drop: 96,
424            hard_land_time: 0.3,
425
426            corner_correction: 3,
427            lift_boost: 1.0,
428        }
429    }
430}