mod climb;
mod collide;
mod dash;
mod dive;
mod jump;
mod land;
mod super_dash;
use super::{Contacts, Event, Intent, JumpKind, Moves, Rect, Terrain, Tuning};
pub const W: i32 = 6;
pub const H: i32 = 12;
pub const LIFT_SMOOTHING: f32 = 0.1;
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum Mode {
Normal,
Roll {
t: f32,
},
HardLand {
t: f32,
},
Dash {
t: f32,
dx: f32,
dy: f32,
},
Climb,
Dive {
t: f32,
},
SuperCharge {
t: f32,
wall: i32,
},
SuperDash {
dir: i32,
},
DiveLand {
t: f32,
},
Custom {
id: super::CustomId,
t: f32,
},
}
#[derive(Clone, Debug, PartialEq)]
pub struct Player {
pub x: i32,
pub y: i32,
pub(crate) w: i32,
pub(crate) h: i32,
rem_x: f32,
rem_y: f32,
pub vx: f32,
pub vy: f32,
pub facing: i32,
pub(crate) grounded: bool,
pub mode: Mode,
pub(crate) coyote: f32,
pub(crate) buffer: f32,
pub(crate) dash_buffer: f32,
pub(crate) var_jump: f32,
var_jump_speed: f32,
can_cut: bool,
pub air_jumps: u8,
pub dashes: u8,
pub(crate) dash_cooldown: f32,
pub stamina: f32,
pub(crate) force_move: f32,
force_dir: i32,
pub(crate) drop_through: f32,
pub(crate) wall_slide: i32,
pub(crate) wall_stick: f32,
pub(crate) slipping: bool,
pub(crate) regrab: f32,
pub(crate) look: i32,
pub(crate) look_time: f32,
glide_armed: bool,
pub(crate) gliding: bool,
was_down: bool,
was_super: bool,
pub(crate) peak_y: i32,
pub(crate) lift: (f32, f32),
lift_time: f32,
pub(crate) contacts: Contacts,
}
impl Player {
pub fn at((x, y): (i32, i32), t: &Tuning) -> Self {
Self {
x,
y,
w: t.size.0,
h: t.size.1,
rem_x: 0.0,
rem_y: 0.0,
vx: 0.0,
vy: 0.0,
facing: 1,
grounded: true,
mode: Mode::Normal,
coyote: 0.0,
buffer: 0.0,
dash_buffer: 0.0,
var_jump: 0.0,
var_jump_speed: 0.0,
can_cut: false,
air_jumps: t.air_jumps,
dashes: t.dashes,
dash_cooldown: 0.0,
stamina: t.stamina,
force_move: 0.0,
force_dir: 0,
drop_through: 0.0,
wall_slide: 0,
wall_stick: 0.0,
slipping: false,
regrab: 0.0,
look: 0,
look_time: 0.0,
glide_armed: false,
gliding: false,
was_down: false,
was_super: false,
peak_y: y,
lift: (0.0, 0.0),
lift_time: 0.0,
contacts: Contacts::default(),
}
}
pub fn launch(&mut self, vx: f32, vy: f32) {
self.vx = vx;
self.vy = vy;
self.stop_jump();
self.mode = Mode::Normal;
self.force_move = 0.0;
self.wall_slide = 0;
self.wall_stick = 0.0;
self.slipping = false;
self.glide_armed = false;
self.lift = (0.0, 0.0);
if vy < 0.0 {
self.grounded = false;
self.coyote = 0.0;
}
}
pub fn teleport(&mut self, x: i32, y: i32) {
self.x = x;
self.y = y;
self.rem_x = 0.0;
self.rem_y = 0.0;
self.peak_y = y;
self.wall_slide = 0;
self.wall_stick = 0.0;
self.lift = (0.0, 0.0);
}
pub fn refill(&mut self, t: &Tuning) {
self.dashes = t.dashes;
self.air_jumps = t.air_jumps;
self.stamina = t.stamina;
}
pub fn rect(&self) -> Rect {
Rect {
x: self.x,
y: self.y,
w: self.w,
h: self.h,
}
}
pub fn exact_pos(&self) -> (f32, f32) {
(self.x as f32 + self.rem_x, self.y as f32 + self.rem_y)
}
pub fn step(
&mut self,
level: &dyn Terrain,
i: &Intent,
moves: &Moves,
t: &Tuning,
dt: f32,
ev: &mut Vec<Event>,
) {
let move_x = if self.force_move > 0.0 {
self.force_dir as f32
} else {
i.move_x.clamp(-1.0, 1.0)
};
self.tick_timers(i, t, dt, ev);
self.tick_mode(t, dt, ev);
if moves.dash {
self.try_dash(i, t, ev);
}
if moves.dive {
self.try_dive(i, t, ev);
}
self.dive_motion(t);
let held = Intent {
grab: i.grab && moves.climb,
super_dash: i.super_dash && moves.super_dash,
..*i
};
self.climb(level, &held, t, dt, ev);
self.super_dash(level, &held, t, ev);
self.run(level, move_x, t.sprint && i.sprint, moves.max_run, t, dt);
let airborne = !self.grounded && self.coyote <= 0.0;
let before = ev.len();
let jumped = moves.jump && self.jump(level, i, move_x, t, ev);
let off_wall = ev[before..].iter().any(|e| {
matches!(
e,
Event::Jumped {
kind: JumpKind::Wall | JumpKind::Climb,
..
}
)
});
if i.jump_pressed && airborne && !off_wall {
self.glide_armed = true;
}
if !i.jump_held || self.mode != Mode::Normal || !moves.glide {
self.glide_armed = false;
}
self.fall(level, i, move_x, t, dt, ev);
let landing_vy = self.vy;
self.contacts = Contacts::default();
let moved_at = ev.len();
let toward = if self.vx == 0.0 {
0
} else {
self.vx.signum() as i32
};
let blocked = self.move_x(level, self.vx * dt);
let flush = toward != 0 && self.wall(level, toward);
if blocked || flush {
self.contacts.wall = toward;
}
if flush && !blocked && self.grounded && self.mode == Mode::Normal {
self.vx = 0.0;
}
if blocked {
self.vx = 0.0;
if let Mode::SuperDash { dir } = self.mode {
self.super_bonk(dir, ev);
}
if matches!(self.mode, Mode::Roll { .. }) {
self.mode = Mode::Normal;
ev.push(Event::RollEnded);
}
}
self.move_y(level, self.vy * dt, t.corner_correction, ev);
self.land(level, move_x, landing_vy, jumped, t, ev);
for e in &ev[moved_at..] {
match e {
Event::Bonked => self.contacts.ceiling = true,
Event::Landed { .. } => self.contacts.floor = true,
_ => {}
}
}
self.update_look(i, move_x, dt);
self.run_roll(i, t, ev);
}
fn set_gliding(&mut self, on: bool, ev: &mut Vec<Event>) {
if on != self.gliding {
self.gliding = on;
ev.push(if on {
Event::GlideStarted
} else {
Event::GlideEnded
});
}
}
fn run_roll(&mut self, i: &Intent, t: &Tuning, ev: &mut Vec<Event>) {
let down = i.down();
let tap = down && !self.was_down;
self.was_down = down;
if !t.run_roll
|| !tap
|| !self.grounded
|| self.mode != Mode::Normal
|| self.vx.abs() < t.run_roll_speed
{
return;
}
self.facing = self.vx.signum() as i32;
self.vx = self.facing as f32 * self.vx.abs().max(t.roll_speed);
self.mode = Mode::Roll { t: 0.0 };
ev.push(Event::RollStarted);
}
fn update_look(&mut self, i: &Intent, move_x: f32, dt: f32) {
let still =
self.grounded && self.mode == Mode::Normal && move_x == 0.0 && self.vx.abs() < 1.0;
let dir = match () {
_ if !still => 0,
_ if i.move_y < -0.5 => -1,
_ if i.move_y > 0.5 => 1,
_ => 0,
};
if dir != self.look {
self.look = dir;
self.look_time = 0.0;
} else if dir != 0 {
self.look_time += dt;
}
}
fn tick_timers(&mut self, i: &Intent, t: &Tuning, dt: f32, ev: &mut Vec<Event>) {
let tick = |v: f32| (v - dt).max(0.0);
self.buffer = if i.jump_pressed {
t.jump_buffer
} else {
tick(self.buffer)
};
self.dash_buffer = if i.dash_pressed {
t.dash_buffer
} else {
tick(self.dash_buffer)
};
self.dash_cooldown = tick(self.dash_cooldown);
self.force_move = tick(self.force_move);
self.drop_through = tick(self.drop_through);
self.regrab = tick(self.regrab);
if self.grounded {
self.coyote = t.coyote_time;
self.air_jumps = t.air_jumps;
self.stamina = t.stamina;
let dashing =
matches!(self.mode, Mode::Dash { t: dt_, .. } if dt_ < t.dash_recharge_after);
if !dashing && self.dashes < t.dashes {
self.dashes = t.dashes;
ev.push(Event::Recharged { crystal: false });
}
} else {
self.coyote = tick(self.coyote);
self.wall_refill(t, ev);
}
}
fn wall_refill(&mut self, t: &Tuning, ev: &mut Vec<Event>) {
if self.wall_slide == 0 && self.mode != Mode::Climb {
return;
}
if t.wall_refills_air_jumps {
self.air_jumps = t.air_jumps;
}
if t.wall_refills_dashes && self.dashes < t.dashes {
self.dashes = t.dashes;
ev.push(Event::Recharged { crystal: false });
}
}
fn tick_mode(&mut self, t: &Tuning, dt: f32, ev: &mut Vec<Event>) {
if let Mode::Roll { t: x }
| Mode::HardLand { t: x }
| Mode::Dash { t: x, .. }
| Mode::Dive { t: x }
| Mode::DiveLand { t: x }
| Mode::SuperCharge { t: x, .. }
| Mode::Custom { t: x, .. } = &mut self.mode
{
let before = *x;
*x += dt;
if matches!(self.mode, Mode::SuperCharge { .. })
&& before < t.super_charge
&& self.mode_time() >= t.super_charge
{
ev.push(Event::SuperCharged);
}
}
let time = self.mode_time();
match self.mode {
Mode::Roll { .. } if time >= t.roll_time => {
self.mode = Mode::Normal;
ev.push(Event::RollEnded);
}
Mode::HardLand { .. } if time >= t.hard_land_time => {
self.mode = Mode::Normal;
ev.push(Event::Recovered);
}
Mode::DiveLand { .. } if time >= t.dive_land_time => {
self.mode = Mode::Normal;
ev.push(Event::Recovered);
}
Mode::Dash { .. } if time >= t.dash_time => {
self.end_dash(t);
ev.push(Event::DashEnded);
}
_ => {}
}
}
pub fn mode_time(&self) -> f32 {
match self.mode {
Mode::Normal | Mode::Climb | Mode::SuperDash { .. } => 0.0,
Mode::SuperCharge { t, .. }
| Mode::Roll { t }
| Mode::HardLand { t }
| Mode::Dash { t, .. }
| Mode::Dive { t }
| Mode::DiveLand { t }
| Mode::Custom { t, .. } => t,
}
}
fn run(
&mut self,
level: &dyn Terrain,
move_x: f32,
sprint: bool,
max_run: f32,
t: &Tuning,
dt: f32,
) {
let side = self.wall_slide;
if self.mode == Mode::Normal
&& side != 0
&& move_x * (side as f32) < 0.0
&& self.wall_stick > 0.0
&& self.wall(level, side)
{
self.wall_stick = (self.wall_stick - dt).max(0.0);
self.vx = 0.0;
return;
}
match self.mode {
Mode::Roll { .. } => {
self.vx = self.facing as f32 * self.vx.abs().max(t.roll_speed);
}
Mode::HardLand { .. } | Mode::DiveLand { .. } => {
self.vx = approach(self.vx, 0.0, t.run_decel * dt);
}
Mode::Dash { .. }
| Mode::Dive { .. }
| Mode::Climb
| Mode::SuperCharge { .. }
| Mode::SuperDash { .. }
| Mode::Custom { .. } => {}
Mode::Normal => {
let top = if sprint { t.sprint_speed } else { t.run_speed }.min(max_run);
if move_x != 0.0 {
self.facing = move_x.signum() as i32;
}
let target = move_x * top;
let accel = if move_x == 0.0 {
t.run_decel
} else if self.vx * move_x < 0.0 {
t.turn_accel
} else if self.vx.abs() > top {
t.overspeed_decel
} else {
t.run_accel
};
let air = if self.grounded {
level.surface(self.feet()).friction
} else {
t.air_control
};
self.vx = approach(self.vx, target, accel * air * dt);
}
}
}
fn fall(
&mut self,
level: &dyn Terrain,
i: &Intent,
move_x: f32,
t: &Tuning,
dt: f32,
ev: &mut Vec<Event>,
) {
let stuck = self.wall_slide != 0 && self.vx == 0.0 && self.wall_stick > 0.0;
let stuck_side = self.wall_slide;
self.wall_slide = 0;
if self.grounded || !matches!(self.mode, Mode::Normal | Mode::Roll { .. }) {
self.wall_stick = 0.0;
self.set_gliding(false, ev);
return;
}
let before = self.vy;
let mult = if i.jump_held && self.vy.abs() < t.apex_threshold {
t.apex_gravity
} else if self.vy > 0.0 {
t.fall_gravity
} else {
1.0
};
let mut max = if i.down() { t.fast_fall } else { t.max_fall };
let side = move_x.signum() as i32;
if move_x != 0.0 && self.vy > 0.0 && self.wall(level, side) {
max = max.min(t.wall_slide_speed);
self.wall_slide = side;
self.wall_stick = t.wall_stick_time;
} else if stuck && self.wall(level, stuck_side) {
max = max.min(t.wall_slide_speed);
self.wall_slide = stuck_side;
} else {
self.wall_stick = 0.0;
}
let glide = t.glide
&& self.glide_armed
&& i.jump_held
&& before > 0.0
&& self.wall_slide == 0
&& self.mode == Mode::Normal;
self.set_gliding(glide, ev);
if glide {
max = max.min(t.glide_fall_speed);
}
self.vy = if before > max {
approach(before, max, t.gravity * dt)
} else {
(before + t.gravity * mult * dt).min(max)
};
if self.var_jump > 0.0 {
if i.jump_held {
self.vy = self.vy.min(-self.var_jump_speed);
} else {
self.var_jump = 0.0;
}
self.var_jump = (self.var_jump - dt).max(0.0);
}
if self.can_cut && !i.jump_held && self.vy < 0.0 {
self.vy *= t.jump_cut;
self.can_cut = false;
ev.push(Event::JumpCut);
}
if self.vy >= 0.0 {
self.can_cut = false;
if before < 0.0 {
ev.push(Event::Apex);
}
}
}
pub fn wall(&self, level: &dyn Terrain, side: i32) -> bool {
side != 0 && level.solid(self.rect().shifted(side, 0))
}
pub fn climbable(&self, level: &dyn Terrain, side: i32) -> bool {
self.wall(level, side) && level.surface(self.beside(side)).climbable
}
pub fn feet(&self) -> Rect {
let r = self.rect();
Rect {
y: r.bottom(),
h: 1,
..r
}
}
pub fn beside(&self, side: i32) -> Rect {
let r = self.rect();
let x = if side > 0 { r.right() } else { r.x - 1 };
Rect { x, w: 1, ..r }
}
pub fn riding(&self) -> Option<Rect> {
if self.grounded {
Some(self.feet())
} else if self.mode == Mode::Climb {
Some(self.beside(self.facing))
} else {
None
}
}
pub fn ride(&mut self, level: &dyn Terrain, carry: Option<(f32, f32)>, t: &Tuning, dt: f32) {
let Some((dx, dy)) = carry else {
self.lift_time = (self.lift_time - dt).max(0.0);
if self.lift_time <= 0.0 {
self.lift = (0.0, 0.0);
}
return;
};
let a = 1.0 - (-dt / LIFT_SMOOTHING).exp();
self.lift.0 += (dx / dt - self.lift.0) * a;
self.lift.1 += (dy / dt - self.lift.1) * a;
self.lift_time = t.coyote_time;
self.carry(level, dx, dy);
}
fn stop_jump(&mut self) {
self.var_jump = 0.0;
self.can_cut = false;
}
}
pub(crate) fn approach(v: f32, target: f32, step: f32) -> f32 {
if v < target {
(v + step).min(target)
} else {
(v - step).max(target)
}
}
impl Player {
pub fn w(&self) -> i32 {
self.w
}
pub fn h(&self) -> i32 {
self.h
}
pub fn grounded(&self) -> bool {
self.grounded
}
pub fn coyote(&self) -> f32 {
self.coyote
}
pub fn buffer(&self) -> f32 {
self.buffer
}
pub fn dash_buffer(&self) -> f32 {
self.dash_buffer
}
pub fn var_jump(&self) -> f32 {
self.var_jump
}
pub fn dash_cooldown(&self) -> f32 {
self.dash_cooldown
}
pub fn force_move(&self) -> f32 {
self.force_move
}
pub fn drop_through(&self) -> f32 {
self.drop_through
}
pub fn wall_slide(&self) -> i32 {
self.wall_slide
}
pub fn wall_stick(&self) -> f32 {
self.wall_stick
}
pub fn slipping(&self) -> bool {
self.slipping
}
pub fn regrab(&self) -> f32 {
self.regrab
}
pub fn look(&self) -> i32 {
self.look
}
pub fn look_time(&self) -> f32 {
self.look_time
}
pub fn gliding(&self) -> bool {
self.gliding
}
pub fn peak_y(&self) -> i32 {
self.peak_y
}
pub fn lift(&self) -> (f32, f32) {
self.lift
}
pub fn contacts(&self) -> Contacts {
self.contacts
}
}