1mod 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
27pub const W: i32 = 6;
29pub const H: i32 = 12;
31
32pub const LIFT_SMOOTHING: f32 = 0.1;
36
37#[derive(Clone, Copy, Debug, PartialEq)]
40#[non_exhaustive]
41pub enum Mode {
42 Normal,
44 Roll {
46 t: f32,
48 },
49 HardLand {
51 t: f32,
53 },
54 Dash {
56 t: f32,
58 dx: f32,
60 dy: f32,
62 },
63 Climb,
65 Dive {
67 t: f32,
69 },
70 SuperCharge {
72 t: f32,
74 wall: i32,
76 },
77 SuperDash {
79 dir: i32,
81 },
82 DiveLand {
84 t: f32,
86 },
87 Custom {
90 id: super::CustomId,
93 t: f32,
95 },
96}
97
98#[derive(Clone, Debug, PartialEq)]
104pub struct Player {
105 pub x: i32,
107 pub y: i32,
109 pub(crate) w: i32,
111 pub(crate) h: i32,
113 rem_x: f32,
115 rem_y: f32,
117 pub vx: f32,
119 pub vy: f32,
121 pub facing: i32,
123 pub(crate) grounded: bool,
125 pub mode: Mode,
127 pub(crate) coyote: f32,
129 pub(crate) buffer: f32,
131 pub(crate) dash_buffer: f32,
133 pub(crate) var_jump: f32,
135 var_jump_speed: f32,
137 can_cut: bool,
139 pub air_jumps: u8,
141 pub dashes: u8,
143 pub(crate) dash_cooldown: f32,
145 pub stamina: f32,
147 pub(crate) force_move: f32,
149 force_dir: i32,
151 pub(crate) drop_through: f32,
153 pub(crate) wall_slide: i32,
155 pub(crate) wall_stick: f32,
157 pub(crate) slipping: bool,
159 pub(crate) regrab: f32,
161 pub(crate) look: i32,
163 pub(crate) look_time: f32,
165 glide_armed: bool,
167 pub(crate) gliding: bool,
169 was_down: bool,
171 was_super: bool,
173 pub(crate) peak_y: i32,
175 pub(crate) lift: (f32, f32),
178 lift_time: f32,
180 pub(crate) contacts: Contacts,
182}
183
184impl Player {
185 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn wall(&self, level: &dyn Terrain, side: i32) -> bool {
677 side != 0 && level.solid(self.rect().shifted(side, 0))
678 }
679
680 pub fn climbable(&self, level: &dyn Terrain, side: i32) -> bool {
683 self.wall(level, side) && level.surface(self.beside(side)).climbable
684 }
685
686 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 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 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 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 fn stop_jump(&mut self) {
737 self.var_jump = 0.0;
738 self.can_cut = false;
739 }
740}
741
742pub(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
751impl Player {
753 pub fn w(&self) -> i32 {
755 self.w
756 }
757
758 pub fn h(&self) -> i32 {
760 self.h
761 }
762
763 pub fn grounded(&self) -> bool {
765 self.grounded
766 }
767
768 pub fn coyote(&self) -> f32 {
770 self.coyote
771 }
772
773 pub fn buffer(&self) -> f32 {
775 self.buffer
776 }
777
778 pub fn dash_buffer(&self) -> f32 {
780 self.dash_buffer
781 }
782
783 pub fn var_jump(&self) -> f32 {
785 self.var_jump
786 }
787
788 pub fn dash_cooldown(&self) -> f32 {
790 self.dash_cooldown
791 }
792
793 pub fn force_move(&self) -> f32 {
795 self.force_move
796 }
797
798 pub fn drop_through(&self) -> f32 {
800 self.drop_through
801 }
802
803 pub fn wall_slide(&self) -> i32 {
805 self.wall_slide
806 }
807
808 pub fn wall_stick(&self) -> f32 {
810 self.wall_stick
811 }
812
813 pub fn slipping(&self) -> bool {
815 self.slipping
816 }
817
818 pub fn regrab(&self) -> f32 {
820 self.regrab
821 }
822
823 pub fn look(&self) -> i32 {
825 self.look
826 }
827
828 pub fn look_time(&self) -> f32 {
830 self.look_time
831 }
832
833 pub fn gliding(&self) -> bool {
835 self.gliding
836 }
837
838 pub fn peak_y(&self) -> i32 {
840 self.peak_y
841 }
842
843 pub fn lift(&self) -> (f32, f32) {
846 self.lift
847 }
848
849 pub fn contacts(&self) -> Contacts {
851 self.contacts
852 }
853}