Skip to main content

game_toolkit_platformer/sim/
player.rs

1//! [`Player`]: the hero's body: position, speed, timers, and the movement rules.
2//!
3//! [`Player::step`] runs the abilities in a fixed order, each in its own module:
4//!
5//! 1. timers: jump and dash buffers, coyote time, refills on the ground, mode clocks
6//! 2. `dash`: start a buffered dash (8-way, from the stick)
7//! 3. `dive`: start a dive in the air; the dive's own motion
8//! 4. `climb`: grab, climb, hop over the top, run out of stamina
9//! 5. running (horizontal speed by mode)
10//! 6. `jump`: ground, coyote, roll, wall, climb and air jumps; drop-through
11//! 7. falling: gravity with hangtime, variable jump, jump cut, wall slide
12//! 8. `collide`: move unit by unit, corner correction
13//! 9. `land`: landings (plain, roll, hard, dive impact), leaving the ground
14//!
15//! Your own moves hook in before and after through [`super::Ability`].
16
17mod climb;
18mod collide;
19mod dash;
20mod dive;
21mod jump;
22mod land;
23mod super_dash;
24
25use super::{Contacts, Event, Intent, JumpKind, Moves, Rect, Terrain, Tuning};
26
27/// The default hitbox width in units ([`Tuning::size`] sets it per character).
28pub const W: i32 = 6;
29/// The default hitbox height in units ([`Tuning::size`] sets it per character).
30pub const H: i32 = 12;
31
32/// Seconds over which [`Player::lift`] follows the ridden ground's speed. Ground that moves
33/// in whole units moves 1, 0, 1, 0 units per step at half a unit per step; smoothed, that
34/// reads as the steady speed it is.
35pub const LIFT_SMOOTHING: f32 = 0.1;
36
37/// What the body is busy with, beyond plain running and jumping. New moves bring new
38/// modes, so matches need a `_` arm.
39#[derive(Clone, Copy, Debug, PartialEq)]
40#[non_exhaustive]
41pub enum Mode {
42    /// Free movement.
43    Normal,
44    /// Rolling out of a high landing.
45    Roll {
46        /// Seconds since the roll started.
47        t: f32,
48    },
49    /// Recovering from a hard landing.
50    HardLand {
51        /// Seconds since touching down.
52        t: f32,
53    },
54    /// Dashing in a fixed direction, gravity off.
55    Dash {
56        /// Seconds since the dash started.
57        t: f32,
58        /// Horizontal direction (normalized with `dy`).
59        dx: f32,
60        /// Vertical direction, positive down.
61        dy: f32,
62    },
63    /// Holding on to the wall on the facing side.
64    Climb,
65    /// Diving: a short hang, then straight down.
66    Dive {
67        /// Seconds since the dive started.
68        t: f32,
69    },
70    /// Charging a super dash, standing (`wall` 0) or holding on to the wall on side `wall`.
71    SuperCharge {
72        /// Seconds charged.
73        t: f32,
74        /// Side of the wall held, or 0 on the ground.
75        wall: i32,
76    },
77    /// Flying a super dash: straight and level until a wall or a cancel.
78    SuperDash {
79        /// Direction, -1 or 1.
80        dir: i32,
81    },
82    /// Recovering from a dive's impact.
83    DiveLand {
84        /// Seconds since the impact.
85        t: f32,
86    },
87    /// A move of your own ([`super::Ability`]) has the body: no built-in move starts or
88    /// runs, no gravity; it still moves by its speed, collides and lands.
89    Custom {
90        /// Which of your moves (see [`super::CustomId`]; the same kind of id as
91        /// [`Event::Custom`]).
92        id: super::CustomId,
93        /// Seconds since it started (counted by the movement).
94        t: f32,
95    },
96}
97
98/// The hero's body. What a game or an [`Ability`](super::Ability) may change is public:
99/// position (a nudge; [`Player::teleport`] for a jump elsewhere), speed, facing, mode and
100/// refills. The movement's own state (timers, latches, what it stands on) is read through
101/// methods: written by hand it could leave the body in a state the movement never
102/// produces.
103#[derive(Clone, Debug, PartialEq)]
104pub struct Player {
105    /// Left edge of the hitbox, units (the movement's space; see `docs/scale.md`).
106    pub x: i32,
107    /// Top edge of the hitbox, units.
108    pub y: i32,
109    /// Hitbox width in units, from [`Tuning::size`].
110    pub(crate) w: i32,
111    /// Hitbox height in units, from [`Tuning::size`].
112    pub(crate) h: i32,
113    /// Fraction of a unit moved sideways but not yet applied to [`Player::x`].
114    rem_x: f32,
115    /// Fraction of a unit moved vertically but not yet applied to [`Player::y`].
116    rem_y: f32,
117    /// Horizontal speed, units/s (pixels/s only at a scale of 1; see `docs/scale.md`).
118    pub vx: f32,
119    /// Vertical speed, units/s, positive down.
120    pub vy: f32,
121    /// 1 facing right, -1 facing left.
122    pub facing: i32,
123    /// Standing on something.
124    pub(crate) grounded: bool,
125    /// What the body is busy with.
126    pub mode: Mode,
127    /// Coyote time left.
128    pub(crate) coyote: f32,
129    /// Jump buffer time left: a press waiting to become a jump.
130    pub(crate) buffer: f32,
131    /// Dash buffer time left: a press waiting to become a dash.
132    pub(crate) dash_buffer: f32,
133    /// Variable-jump time left: holding jump keeps the upward speed.
134    pub(crate) var_jump: f32,
135    /// The upward speed the variable jump sustains.
136    var_jump_speed: f32,
137    /// The current jump can still be cut by releasing.
138    can_cut: bool,
139    /// Air jumps left.
140    pub air_jumps: u8,
141    /// Dashes left.
142    pub dashes: u8,
143    /// Seconds until the next dash may start.
144    pub(crate) dash_cooldown: f32,
145    /// Climbing stamina left.
146    pub stamina: f32,
147    /// Seconds the stick is overridden after a wall jump.
148    pub(crate) force_move: f32,
149    /// The stick's override direction during [`Player::force_move`].
150    force_dir: i32,
151    /// Seconds platforms still let the hero through after dropping.
152    pub(crate) drop_through: f32,
153    /// Side of the wall being slid down (-1, 1), or 0.
154    pub(crate) wall_slide: i32,
155    /// Wall-stick time left: pressing away from the wall slid down doesn't let go yet.
156    pub(crate) wall_stick: f32,
157    /// Holding on to a wall but sliding down it (a slipping or tired grip).
158    pub(crate) slipping: bool,
159    /// Seconds before a wall can be grabbed again after hopping over its top.
160    pub(crate) regrab: f32,
161    /// Looking up (-1) or down (1) while standing still, or 0.
162    pub(crate) look: i32,
163    /// Seconds the current look has been held.
164    pub(crate) look_time: f32,
165    /// A jump was pressed in the air and is still held: falling, it glides.
166    glide_armed: bool,
167    /// Gliding.
168    pub(crate) gliding: bool,
169    /// Down was held last step, to see a fresh tap.
170    was_down: bool,
171    /// Super dash was held last step, to see a fresh press.
172    was_super: bool,
173    /// Highest (smallest) `y` of this airtime (units), for the drop height on landing.
174    pub(crate) peak_y: i32,
175    /// The speed of the moving ground (or wall) ridden, units/s, smoothed over about
176    /// [`LIFT_SMOOTHING`] seconds: a jump keeps it.
177    pub(crate) lift: (f32, f32),
178    /// Seconds [`Player::lift`] still counts after leaving what it rode.
179    lift_time: f32,
180    /// What its movement ran into in the last step.
181    pub(crate) contacts: Contacts,
182}
183
184impl Player {
185    /// Standing with its hitbox's top-left at `x, y` (units), with everything refilled.
186    pub fn at((x, y): (i32, i32), t: &Tuning) -> Self {
187        Self {
188            x,
189            y,
190            w: t.size.0,
191            h: t.size.1,
192            rem_x: 0.0,
193            rem_y: 0.0,
194            vx: 0.0,
195            vy: 0.0,
196            facing: 1,
197            grounded: true,
198            mode: Mode::Normal,
199            coyote: 0.0,
200            buffer: 0.0,
201            dash_buffer: 0.0,
202            var_jump: 0.0,
203            var_jump_speed: 0.0,
204            can_cut: false,
205            air_jumps: t.air_jumps,
206            dashes: t.dashes,
207            dash_cooldown: 0.0,
208            stamina: t.stamina,
209            force_move: 0.0,
210            force_dir: 0,
211            drop_through: 0.0,
212            wall_slide: 0,
213            wall_stick: 0.0,
214            slipping: false,
215            regrab: 0.0,
216            look: 0,
217            look_time: 0.0,
218            glide_armed: false,
219            gliding: false,
220            was_down: false,
221            was_super: false,
222            peak_y: y,
223            lift: (0.0, 0.0),
224            lift_time: 0.0,
225            contacts: Contacts::default(),
226        }
227    }
228
229    /// Set the speed from outside: a spring, a bounce pad, knockback. Whatever the body was
230    /// busy with ends (a dash, a dive, a climb), a jump in progress can no longer be
231    /// sustained or cut, and going up leaves the ground for good (no coyote jump).
232    pub fn launch(&mut self, vx: f32, vy: f32) {
233        self.vx = vx;
234        self.vy = vy;
235        self.stop_jump();
236        self.mode = Mode::Normal;
237        self.force_move = 0.0;
238        self.wall_slide = 0;
239        self.wall_stick = 0.0;
240        self.slipping = false;
241        self.glide_armed = false;
242        self.lift = (0.0, 0.0);
243        if vy < 0.0 {
244            self.grounded = false;
245            self.coyote = 0.0;
246        }
247    }
248
249    /// Put the hitbox's top-left at `x, y` (units) without moving through what's between:
250    /// a door, a portal. The sub-pixel remainder is dropped and the airtime starts over;
251    /// speed and state stay (follow with [`Player::launch`] to stop it). Teleported into
252    /// the air while standing, it walks off as if from a ledge.
253    pub fn teleport(&mut self, x: i32, y: i32) {
254        self.x = x;
255        self.y = y;
256        self.rem_x = 0.0;
257        self.rem_y = 0.0;
258        self.peak_y = y;
259        self.wall_slide = 0;
260        self.wall_stick = 0.0;
261        self.lift = (0.0, 0.0);
262    }
263
264    /// Refill dashes, air jumps and climbing stamina, as touching the ground does.
265    pub fn refill(&mut self, t: &Tuning) {
266        self.dashes = t.dashes;
267        self.air_jumps = t.air_jumps;
268        self.stamina = t.stamina;
269    }
270
271    /// The hitbox.
272    pub fn rect(&self) -> Rect {
273        Rect {
274            x: self.x,
275            y: self.y,
276            w: self.w,
277            h: self.h,
278        }
279    }
280
281    /// Position with the sub-pixel remainder, for drawing between steps.
282    pub fn exact_pos(&self) -> (f32, f32) {
283        (self.x as f32 + self.rem_x, self.y as f32 + self.rem_y)
284    }
285
286    /// One step of `dt` seconds (see the module docs for the order), with the built-in
287    /// moves `moves` allows.
288    pub fn step(
289        &mut self,
290        level: &dyn Terrain,
291        i: &Intent,
292        moves: &Moves,
293        t: &Tuning,
294        dt: f32,
295        ev: &mut Vec<Event>,
296    ) {
297        let move_x = if self.force_move > 0.0 {
298            self.force_dir as f32
299        } else {
300            i.move_x.clamp(-1.0, 1.0)
301        };
302        self.tick_timers(i, t, dt, ev);
303        self.tick_mode(t, dt, ev);
304        if moves.dash {
305            self.try_dash(i, t, ev);
306        }
307        if moves.dive {
308            self.try_dive(i, t, ev);
309        }
310        self.dive_motion(t);
311        // What's ruled out acts as if its button weren't held: a climb lets go.
312        let held = Intent {
313            grab: i.grab && moves.climb,
314            super_dash: i.super_dash && moves.super_dash,
315            ..*i
316        };
317        self.climb(level, &held, t, dt, ev);
318        self.super_dash(level, &held, t, ev);
319        self.run(level, move_x, t.sprint && i.sprint, moves.max_run, t, dt);
320        let airborne = !self.grounded && self.coyote <= 0.0;
321        let before = ev.len();
322        let jumped = moves.jump && self.jump(level, i, move_x, t, ev);
323        let off_wall = ev[before..].iter().any(|e| {
324            matches!(
325                e,
326                Event::Jumped {
327                    kind: JumpKind::Wall | JumpKind::Climb,
328                    ..
329                }
330            )
331        });
332        if i.jump_pressed && airborne && !off_wall {
333            self.glide_armed = true;
334        }
335        if !i.jump_held || self.mode != Mode::Normal || !moves.glide {
336            self.glide_armed = false;
337        }
338        self.fall(level, i, move_x, t, dt, ev);
339
340        let landing_vy = self.vy;
341        self.contacts = Contacts::default();
342        let moved_at = ev.len();
343        let toward = if self.vx == 0.0 {
344            0
345        } else {
346            self.vx.signum() as i32
347        };
348        let blocked = self.move_x(level, self.vx * dt);
349        let flush = toward != 0 && self.wall(level, toward);
350        if blocked || flush {
351            self.contacts.wall = toward;
352        }
353        // Standing flush against the wall it runs toward, the sub-pixel remainder would let
354        // speed build for a few steps before the next whole unit is blocked: a running
355        // animation that starts and stops. On the ground the speed stays at zero instead; in
356        // the air it is kept, so a jump along a wall still carries over its top.
357        if flush && !blocked && self.grounded && self.mode == Mode::Normal {
358            self.vx = 0.0;
359        }
360        if blocked {
361            self.vx = 0.0;
362            if let Mode::SuperDash { dir } = self.mode {
363                self.super_bonk(dir, ev);
364            }
365            if matches!(self.mode, Mode::Roll { .. }) {
366                self.mode = Mode::Normal;
367                ev.push(Event::RollEnded);
368            }
369        }
370        self.move_y(level, self.vy * dt, t.corner_correction, ev);
371        self.land(level, move_x, landing_vy, jumped, t, ev);
372        for e in &ev[moved_at..] {
373            match e {
374                Event::Bonked => self.contacts.ceiling = true,
375                Event::Landed { .. } => self.contacts.floor = true,
376                _ => {}
377            }
378        }
379        self.update_look(i, move_x, dt);
380        self.run_roll(i, t, ev);
381    }
382
383    /// Open or close the glide, reporting the change.
384    fn set_gliding(&mut self, on: bool, ev: &mut Vec<Event>) {
385        if on != self.gliding {
386            self.gliding = on;
387            ev.push(if on {
388                Event::GlideStarted
389            } else {
390                Event::GlideEnded
391            });
392        }
393    }
394
395    /// A fresh tap of down while running at speed on the ground rolls.
396    fn run_roll(&mut self, i: &Intent, t: &Tuning, ev: &mut Vec<Event>) {
397        let down = i.down();
398        let tap = down && !self.was_down;
399        self.was_down = down;
400        if !t.run_roll
401            || !tap
402            || !self.grounded
403            || self.mode != Mode::Normal
404            || self.vx.abs() < t.run_roll_speed
405        {
406            return;
407        }
408        self.facing = self.vx.signum() as i32;
409        self.vx = self.facing as f32 * self.vx.abs().max(t.roll_speed);
410        self.mode = Mode::Roll { t: 0.0 };
411        ev.push(Event::RollStarted);
412    }
413
414    /// Holding up or down while standing still looks that way.
415    fn update_look(&mut self, i: &Intent, move_x: f32, dt: f32) {
416        let still =
417            self.grounded && self.mode == Mode::Normal && move_x == 0.0 && self.vx.abs() < 1.0;
418        let dir = match () {
419            _ if !still => 0,
420            _ if i.move_y < -0.5 => -1,
421            _ if i.move_y > 0.5 => 1,
422            _ => 0,
423        };
424        if dir != self.look {
425            self.look = dir;
426            self.look_time = 0.0;
427        } else if dir != 0 {
428            self.look_time += dt;
429        }
430    }
431
432    /// Buffers, coyote time, refills on the ground, cooldowns.
433    fn tick_timers(&mut self, i: &Intent, t: &Tuning, dt: f32, ev: &mut Vec<Event>) {
434        let tick = |v: f32| (v - dt).max(0.0);
435        self.buffer = if i.jump_pressed {
436            t.jump_buffer
437        } else {
438            tick(self.buffer)
439        };
440        self.dash_buffer = if i.dash_pressed {
441            t.dash_buffer
442        } else {
443            tick(self.dash_buffer)
444        };
445        self.dash_cooldown = tick(self.dash_cooldown);
446        self.force_move = tick(self.force_move);
447        self.drop_through = tick(self.drop_through);
448        self.regrab = tick(self.regrab);
449        if self.grounded {
450            self.coyote = t.coyote_time;
451            self.air_jumps = t.air_jumps;
452            self.stamina = t.stamina;
453            let dashing =
454                matches!(self.mode, Mode::Dash { t: dt_, .. } if dt_ < t.dash_recharge_after);
455            if !dashing && self.dashes < t.dashes {
456                self.dashes = t.dashes;
457                ev.push(Event::Recharged { crystal: false });
458            }
459        } else {
460            self.coyote = tick(self.coyote);
461            self.wall_refill(t, ev);
462        }
463    }
464
465    /// On a wall (sliding down it or holding on, as of the last step), refill air jumps
466    /// and, if the tuning says so, dashes (a design choice: walls as a place to recover).
467    fn wall_refill(&mut self, t: &Tuning, ev: &mut Vec<Event>) {
468        if self.wall_slide == 0 && self.mode != Mode::Climb {
469            return;
470        }
471        if t.wall_refills_air_jumps {
472            self.air_jumps = t.air_jumps;
473        }
474        if t.wall_refills_dashes && self.dashes < t.dashes {
475            self.dashes = t.dashes;
476            ev.push(Event::Recharged { crystal: false });
477        }
478    }
479
480    /// The clocks of timed modes: rolls, recoveries and dashes run out.
481    fn tick_mode(&mut self, t: &Tuning, dt: f32, ev: &mut Vec<Event>) {
482        if let Mode::Roll { t: x }
483        | Mode::HardLand { t: x }
484        | Mode::Dash { t: x, .. }
485        | Mode::Dive { t: x }
486        | Mode::DiveLand { t: x }
487        | Mode::SuperCharge { t: x, .. }
488        | Mode::Custom { t: x, .. } = &mut self.mode
489        {
490            let before = *x;
491            *x += dt;
492            if matches!(self.mode, Mode::SuperCharge { .. })
493                && before < t.super_charge
494                && self.mode_time() >= t.super_charge
495            {
496                ev.push(Event::SuperCharged);
497            }
498        }
499        let time = self.mode_time();
500        match self.mode {
501            Mode::Roll { .. } if time >= t.roll_time => {
502                self.mode = Mode::Normal;
503                ev.push(Event::RollEnded);
504            }
505            Mode::HardLand { .. } if time >= t.hard_land_time => {
506                self.mode = Mode::Normal;
507                ev.push(Event::Recovered);
508            }
509            Mode::DiveLand { .. } if time >= t.dive_land_time => {
510                self.mode = Mode::Normal;
511                ev.push(Event::Recovered);
512            }
513            Mode::Dash { .. } if time >= t.dash_time => {
514                self.end_dash(t);
515                ev.push(Event::DashEnded);
516            }
517            _ => {}
518        }
519    }
520
521    /// Seconds in the current timed mode (0 for untimed ones).
522    pub fn mode_time(&self) -> f32 {
523        match self.mode {
524            Mode::Normal | Mode::Climb | Mode::SuperDash { .. } => 0.0,
525            Mode::SuperCharge { t, .. }
526            | Mode::Roll { t }
527            | Mode::HardLand { t }
528            | Mode::Dash { t, .. }
529            | Mode::Dive { t }
530            | Mode::DiveLand { t }
531            | Mode::Custom { t, .. } => t,
532        }
533    }
534
535    /// Horizontal speed: rolls keep their speed, recoveries brake, dashes, dives and climbs
536    /// set their own, otherwise accelerate toward the stick, up to `max_run` at most.
537    fn run(
538        &mut self,
539        level: &dyn Terrain,
540        move_x: f32,
541        sprint: bool,
542        max_run: f32,
543        t: &Tuning,
544        dt: f32,
545    ) {
546        let side = self.wall_slide;
547        if self.mode == Mode::Normal
548            && side != 0
549            && move_x * (side as f32) < 0.0
550            && self.wall_stick > 0.0
551            && self.wall(level, side)
552        {
553            // Stuck: pressing away from the wall doesn't pull off it yet.
554            self.wall_stick = (self.wall_stick - dt).max(0.0);
555            self.vx = 0.0;
556            return;
557        }
558        match self.mode {
559            Mode::Roll { .. } => {
560                self.vx = self.facing as f32 * self.vx.abs().max(t.roll_speed);
561            }
562            Mode::HardLand { .. } | Mode::DiveLand { .. } => {
563                self.vx = approach(self.vx, 0.0, t.run_decel * dt);
564            }
565            Mode::Dash { .. }
566            | Mode::Dive { .. }
567            | Mode::Climb
568            | Mode::SuperCharge { .. }
569            | Mode::SuperDash { .. }
570            | Mode::Custom { .. } => {}
571            Mode::Normal => {
572                let top = if sprint { t.sprint_speed } else { t.run_speed }.min(max_run);
573                if move_x != 0.0 {
574                    self.facing = move_x.signum() as i32;
575                }
576                let target = move_x * top;
577                let accel = if move_x == 0.0 {
578                    t.run_decel
579                } else if self.vx * move_x < 0.0 {
580                    t.turn_accel
581                } else if self.vx.abs() > top {
582                    t.overspeed_decel
583                } else {
584                    t.run_accel
585                };
586                // On the ground, the surface scales the grip (ice, sticky ground).
587                let air = if self.grounded {
588                    level.surface(self.feet()).friction
589                } else {
590                    t.air_control
591                };
592                self.vx = approach(self.vx, target, accel * air * dt);
593            }
594        }
595    }
596
597    /// Gravity (lighter at the apex while jump is held, heavier falling), the variable
598    /// jump's sustain, the jump cut on release, and sliding down walls. Only free and
599    /// rolling bodies fall; the other modes set their own vertical speed.
600    fn fall(
601        &mut self,
602        level: &dyn Terrain,
603        i: &Intent,
604        move_x: f32,
605        t: &Tuning,
606        dt: f32,
607        ev: &mut Vec<Event>,
608    ) {
609        let stuck = self.wall_slide != 0 && self.vx == 0.0 && self.wall_stick > 0.0;
610        let stuck_side = self.wall_slide;
611        self.wall_slide = 0;
612        if self.grounded || !matches!(self.mode, Mode::Normal | Mode::Roll { .. }) {
613            self.wall_stick = 0.0;
614            self.set_gliding(false, ev);
615            return;
616        }
617        let before = self.vy;
618        let mult = if i.jump_held && self.vy.abs() < t.apex_threshold {
619            t.apex_gravity
620        } else if self.vy > 0.0 {
621            t.fall_gravity
622        } else {
623            1.0
624        };
625        let mut max = if i.down() { t.fast_fall } else { t.max_fall };
626        let side = move_x.signum() as i32;
627        if move_x != 0.0 && self.vy > 0.0 && self.wall(level, side) {
628            max = max.min(t.wall_slide_speed);
629            self.wall_slide = side;
630            self.wall_stick = t.wall_stick_time;
631        } else if stuck && self.wall(level, stuck_side) {
632            max = max.min(t.wall_slide_speed);
633            self.wall_slide = stuck_side;
634        } else {
635            self.wall_stick = 0.0;
636        }
637        let glide = t.glide
638            && self.glide_armed
639            && i.jump_held
640            && before > 0.0
641            && self.wall_slide == 0
642            && self.mode == Mode::Normal;
643        self.set_gliding(glide, ev);
644        if glide {
645            max = max.min(t.glide_fall_speed);
646        }
647        // Over the cap (a fast fall released, a wall caught), brake toward it; otherwise
648        // accelerate up to it.
649        self.vy = if before > max {
650            approach(before, max, t.gravity * dt)
651        } else {
652            (before + t.gravity * mult * dt).min(max)
653        };
654        if self.var_jump > 0.0 {
655            if i.jump_held {
656                self.vy = self.vy.min(-self.var_jump_speed);
657            } else {
658                self.var_jump = 0.0;
659            }
660            self.var_jump = (self.var_jump - dt).max(0.0);
661        }
662        if self.can_cut && !i.jump_held && self.vy < 0.0 {
663            self.vy *= t.jump_cut;
664            self.can_cut = false;
665            ev.push(Event::JumpCut);
666        }
667        if self.vy >= 0.0 {
668            self.can_cut = false;
669            if before < 0.0 {
670                ev.push(Event::Apex);
671            }
672        }
673    }
674
675    /// Whether a solid wall touches the hitbox on `side` (-1 left, 1 right).
676    pub fn wall(&self, level: &dyn Terrain, side: i32) -> bool {
677        side != 0 && level.solid(self.rect().shifted(side, 0))
678    }
679
680    /// Whether a wall the character may hold on to touches the hitbox on `side`: solid,
681    /// and its [`super::Surface`] climbable.
682    pub fn climbable(&self, level: &dyn Terrain, side: i32) -> bool {
683        self.wall(level, side) && level.surface(self.beside(side)).climbable
684    }
685
686    /// The row just under the hitbox.
687    pub fn feet(&self) -> Rect {
688        let r = self.rect();
689        Rect {
690            y: r.bottom(),
691            h: 1,
692            ..r
693        }
694    }
695
696    /// The column just beside the hitbox on `side` (-1 left, 1 right).
697    pub fn beside(&self, side: i32) -> Rect {
698        let r = self.rect();
699        let x = if side > 0 { r.right() } else { r.x - 1 };
700        Rect { x, w: 1, ..r }
701    }
702
703    /// What the body rides on: the row under its feet while standing, the column beside it
704    /// while holding on to a wall, or nothing.
705    pub fn riding(&self) -> Option<Rect> {
706        if self.grounded {
707            Some(self.feet())
708        } else if self.mode == Mode::Climb {
709            Some(self.beside(self.facing))
710        } else {
711            None
712        }
713    }
714
715    /// Carried `dx, dy` units in a step of `dt` by the moving ground (or moving wall) it
716    /// rides, or `None` when it rides nothing. Moved through the same unit-by-unit
717    /// collision, testing only the edge it moves toward, so the ground it rides (now
718    /// overlapping the feet when rising) doesn't hold it back. Its speed becomes
719    /// [`Player::lift`], which a jump keeps for [`Tuning::coyote_time`] after leaving.
720    pub fn ride(&mut self, level: &dyn Terrain, carry: Option<(f32, f32)>, t: &Tuning, dt: f32) {
721        let Some((dx, dy)) = carry else {
722            self.lift_time = (self.lift_time - dt).max(0.0);
723            if self.lift_time <= 0.0 {
724                self.lift = (0.0, 0.0);
725            }
726            return;
727        };
728        let a = 1.0 - (-dt / LIFT_SMOOTHING).exp();
729        self.lift.0 += (dx / dt - self.lift.0) * a;
730        self.lift.1 += (dy / dt - self.lift.1) * a;
731        self.lift_time = t.coyote_time;
732        self.carry(level, dx, dy);
733    }
734
735    /// Forget any jump in progress: no sustain, no cut.
736    fn stop_jump(&mut self) {
737        self.var_jump = 0.0;
738        self.can_cut = false;
739    }
740}
741
742/// Move `v` toward `target` by at most `step`.
743pub(crate) fn approach(v: f32, target: f32, step: f32) -> f32 {
744    if v < target {
745        (v + step).min(target)
746    } else {
747        (v - step).max(target)
748    }
749}
750
751/// The movement's own state, to read (drivers, debug overlays).
752impl Player {
753    /// Hitbox width in units, from [`Tuning::size`].
754    pub fn w(&self) -> i32 {
755        self.w
756    }
757
758    /// Hitbox height in units, from [`Tuning::size`].
759    pub fn h(&self) -> i32 {
760        self.h
761    }
762
763    /// Standing on something.
764    pub fn grounded(&self) -> bool {
765        self.grounded
766    }
767
768    /// Coyote time left.
769    pub fn coyote(&self) -> f32 {
770        self.coyote
771    }
772
773    /// Jump buffer time left: a press waiting to become a jump.
774    pub fn buffer(&self) -> f32 {
775        self.buffer
776    }
777
778    /// Dash buffer time left: a press waiting to become a dash.
779    pub fn dash_buffer(&self) -> f32 {
780        self.dash_buffer
781    }
782
783    /// Variable-jump time left: holding jump keeps the upward speed.
784    pub fn var_jump(&self) -> f32 {
785        self.var_jump
786    }
787
788    /// Seconds until the next dash may start.
789    pub fn dash_cooldown(&self) -> f32 {
790        self.dash_cooldown
791    }
792
793    /// Seconds the stick is overridden after a wall jump.
794    pub fn force_move(&self) -> f32 {
795        self.force_move
796    }
797
798    /// Seconds platforms still let the hero through after dropping.
799    pub fn drop_through(&self) -> f32 {
800        self.drop_through
801    }
802
803    /// Side of the wall being slid down (-1, 1), or 0.
804    pub fn wall_slide(&self) -> i32 {
805        self.wall_slide
806    }
807
808    /// Wall-stick time left: pressing away from the wall slid down doesn't let go yet.
809    pub fn wall_stick(&self) -> f32 {
810        self.wall_stick
811    }
812
813    /// Holding on to a wall but sliding down it (a slipping or tired grip).
814    pub fn slipping(&self) -> bool {
815        self.slipping
816    }
817
818    /// Seconds before a wall can be grabbed again after hopping over its top.
819    pub fn regrab(&self) -> f32 {
820        self.regrab
821    }
822
823    /// Looking up (-1) or down (1) while standing still, or 0.
824    pub fn look(&self) -> i32 {
825        self.look
826    }
827
828    /// Seconds the current look has been held.
829    pub fn look_time(&self) -> f32 {
830        self.look_time
831    }
832
833    /// Gliding.
834    pub fn gliding(&self) -> bool {
835        self.gliding
836    }
837
838    /// Highest (smallest) `y` of this airtime (units), for the drop height on landing.
839    pub fn peak_y(&self) -> i32 {
840        self.peak_y
841    }
842
843    /// The speed of the moving ground (or wall) ridden, units/s, smoothed over about
844    /// [`LIFT_SMOOTHING`] seconds: a jump keeps it.
845    pub fn lift(&self) -> (f32, f32) {
846        self.lift
847    }
848
849    /// What its movement ran into in the last step.
850    pub fn contacts(&self) -> Contacts {
851        self.contacts
852    }
853}