package demonchime import "core:math" import "core:math/linalg" import "core:log" import "fw" import "phys" Player_State :: enum { Default, Dash, Wall_Slide, Wall_Jump, Dead, } // there will only ever be one player, so just make it a global :) player: struct { health: i16, max_health: i16, hit_timestamp: f32, jump_buffer: f32, coyote_time: f32, dash_cooldown: f32, dash_timer: f32, wall_jump_timer: f32, wall_slide_dir: f32, state: Player_State, outside_room: bool, has_double_jumped: bool, fall_height: f32, body: phys.Body_Handle, sprite: Sprite, scarf: Tail(7), gun: struct { sprite: Sprite, kickback: f32, }, owns_double_jump: bool, owns_dash: bool, } init_player :: proc() { body := phys.make_body( phys.Rect{{-4, -8}, {8, 16}}, layers = {.Player}, mask = {.Hard} ) player.body = body init_sprite(&player.sprite, .Player) set_sprite_offset_percentage(&player.sprite, {0.5, 0.5}) init_sprite(&player.gun.sprite, .Pistol) player.gun.sprite.offset = Vec2 { -PLAYER_GUN_DIST, math.round(f32(player.gun.sprite.height) * 0.5), } player.scarf = { point_max_dist = 2, point_rad = 2, color_start = Color {0.18, 0.13, 0.18, 1} * 0.5, color_end = {0.18, 0.13, 0.18, 1}, } player.max_health = 10 player.health = 10 } deinit_player :: proc() { phys.remove_body(player.body) } @(private = "file") _get_input_dir :: proc() -> f32 { if !point_inside_room(phys.get_position(player.body)) { return 0 } input: f32 if fw.is_keybind_down(actions.move_left) { input -= 1 } if fw.is_keybind_down(actions.move_right) { input += 1 } return input } @(private = "file") _can_wallslide :: proc(input: f32) -> bool { pos := phys.get_position(player.body) rect := phys.get_rect(player.body) rect.start += pos top_start := rect.start + {rect.size.x * 0.5, 0} top_end := top_start + {input * 8, 0} bot_start := rect.start + {rect.size.x * 0.5, rect.size.y} bot_end := bot_start + {input * 8, 0} top_rc := phys.make_raycast(top_start, top_end) bot_rc := phys.make_raycast(bot_start, bot_end) top_touching := phys.is_colliding(top_rc) bot_touching := phys.is_colliding(bot_rc) vel := phys.get_velocity(player.body) return vel.y > 0 && top_touching && bot_touching } @(private = "file") _default_state :: proc(dt: f32) { input := _get_input_dir() if input != 0 { set_sprite_active_tag(&player.sprite, "run") } else { set_sprite_active_tag(&player.sprite, "idle") } if fw.is_keybind_just_down(actions.jump) { player.jump_buffer = PLAYER_JUMP_BUFFERING } if player.owns_dash && fw.is_keybind_just_down(actions.dash) && player.dash_cooldown <= 0 { _enter_dash() return } // body := phys.get_body(player.body_handle) pos := phys.get_position(player.body) vel := phys.get_velocity(player.body) collisions := phys.get_collisions(player.body) if .Down in collisions { player.coyote_time = PLAYER_COYOTE_TIME player.has_double_jumped = false player.fall_height = pos.y } else { switch vel.y { case -math.INF_F32..<-50: set_sprite_active_tag(&player.sprite, "jump_up") case 50.. 0 { if player.coyote_time > 0 { vel.y = -PLAYER_JUMP_FORCE player.jump_buffer = 0 player.coyote_time = 0 } else if player.owns_double_jump && !player.has_double_jumped { vel.y = -PLAYER_DOUBLE_JUMP_FORCE player.has_double_jumped = true player.jump_buffer = 0 } } if .Down not_in collisions && !fw.is_keybind_down(actions.jump) && vel.y < PLAYER_JUMP_RELEASE_CUT { vel.y = PLAYER_JUMP_RELEASE_CUT } vel.x = dt_lerp( vel.x, input * PLAYER_SPEED, PLAYER_ACCEL, ) vel.y = math.min(vel.y + GRAVITY * dt, TERMINAL_VELOCITY) phys.set_velocity(player.body, vel) phys.update_body(player.body) new_collisions := phys.get_collisions(player.body) if .Down in new_collisions && .Down not_in collisions { // just landed height := phys.get_position(player.body).y distance_fallen := height - player.fall_height if distance_fallen > MAX_SAFE_FALL_HEIGHT { distance_fallen -= MAX_SAFE_FALL_HEIGHT damage := (distance_fallen / MAX_FALL_HEIGHT) * PLAYER_MAX_FALL_DAMAGE damage = math.clamp(damage, 0, PLAYER_MAX_FALL_DAMAGE) player_take_damage(damage, {0, 0}) } } } @(private = "file") _enter_dash :: proc() { input := _get_input_dir() if input == 0 { // the sprite x scale is the direction the player is facing :) input = math.sign(player.sprite.scale.x) } phys.set_velocity( player.body, {input * PLAYER_DASH_SPEED, 0}, ) player.dash_timer = PLAYER_DASH_TIME player.state = .Dash } @(private = "file") _dash_state :: proc(dt: f32) { phys.update_body(player.body) player.dash_timer -= dt if player.dash_timer <= 0 || phys.get_collisions(player.body) != nil { _exit_dash() } } @(private = "file") _exit_dash :: proc() { player.state = .Default player.dash_cooldown = PLAYER_DASH_COOLDOWN vel := phys.get_velocity(player.body) phys.set_velocity(player.body, vel / 2) pos := phys.get_position(player.body) player.fall_height = pos.y } @(private = "file") _enter_wall_slide :: proc() { player.has_double_jumped = false player.wall_slide_dir = _get_input_dir() player.state = .Wall_Slide } @(private = "file") _wall_slide_state :: proc(dt: f32) { set_sprite_active_tag(&player.sprite, "idle") input := _get_input_dir() collisions := phys.get_collisions(player.body) if !_can_wallslide(player.wall_slide_dir) { player.state = .Default } vel := phys.get_velocity(player.body) vel.x = dt_lerp( vel.x, input * PLAYER_SPEED, PLAYER_ACCEL, ) vel.y = PLAYER_WALL_SLIDE_SPEED phys.set_velocity(player.body, vel) phys.update_body(player.body) if fw.is_keybind_just_down(actions.jump) { _enter_wall_jump() } pos := phys.get_position(player.body) player.fall_height = pos.y } @(private = "file") _enter_wall_jump :: proc() { vel := phys.get_velocity(player.body) vel.x = -player.wall_slide_dir * PLAYER_WALL_JUMP_FORCE vel.y = -PLAYER_JUMP_FORCE phys.set_velocity(player.body, vel) player.state = .Wall_Jump player.wall_jump_timer = PLAYER_WALL_JUMP_TIME } @(private = "file") _wall_jump_state :: proc(dt: f32) { phys.update_body(player.body) player.wall_jump_timer -= dt if player.wall_jump_timer <= 0 { vel := phys.get_velocity(player.body) vel.y = PLAYER_JUMP_RELEASE_CUT * 2 phys.set_velocity(player.body, vel) player.state = .Default } } @(private = "file") _get_camera_target_pos :: proc() -> Vec2 { pos := phys.get_position(player.body) - SCREEN_SIZE * 0.5 room_size := Vec2{ f32(current_room.width), f32(current_room.height), } camera_bounds_min := Vec2{0, 0} camera_bounds_max := room_size - SCREEN_SIZE pos.x = math.clamp(pos.x, camera_bounds_min.x, camera_bounds_max.x) pos.y = math.clamp(pos.y, camera_bounds_min.y, camera_bounds_max.y) return pos } @(private = "file") _change_rooms :: proc() { pos := phys.get_position(player.body) if point_inside_room(pos) { return } prev_room_pos := Vec2{f32(current_room.x), f32(current_room.y)} changed := open_room_at({i32(pos.x), i32(pos.y)}) if changed { new_room_pos := Vec2{f32(current_room.x), f32(current_room.y)} diff := prev_room_pos - new_room_pos new_pos := pos + diff phys.set_position(player.body, new_pos) player.fall_height += diff.y if diff.y > 0 { vel := phys.get_velocity(player.body) vel.y = -PLAYER_JUMP_FORCE phys.set_velocity(player.body, vel.y) } else if diff.y > 0 { vel := phys.get_velocity(player.body) vel.y = 0 phys.set_velocity(player.body, vel.y) } new_cam_pos := _get_camera_target_pos() state.camera_pos += diff state.camera_target += diff } } update_player :: proc(dt: f32) { switch player.state { case .Default: _default_state(dt) case .Dash: _dash_state(dt) case .Wall_Slide: _wall_slide_state(dt) case .Wall_Jump: _wall_jump_state(dt) case .Dead: } pos := phys.get_position(player.body) player.sprite.pos = pos update_sprite(&player.sprite, dt) mouse_dir := linalg.normalize0(fw.get_mouse_pos() - player.gun.sprite.pos) player.gun.sprite.rotation = math.atan2(mouse_dir.y, mouse_dir.x) player.gun.sprite.pos = pos - Vec2{0, PLAYER_GUN_HEIGHT} player.gun.sprite.pos += -mouse_dir * player.gun.kickback player.gun.kickback = dt_lerp(player.gun.kickback, 0, 5) player.gun.sprite.scale.y = -1 if mouse_dir.x < 0 else 1 player.dash_cooldown -= dt player.jump_buffer -= dt player.coyote_time -= dt state.camera_target = _get_camera_target_pos() scarf_dist: f32 = PLAYER_SCARF_DIST if _get_input_dir() != 0 { scarf_dist -= 1 } scarf_pos := linalg.round( pos + Vec2{-scarf_dist * player.sprite.scale.x, 0}, ) set_tail_position(&player.scarf, scarf_pos, dt) _change_rooms() } draw_player :: proc() { draw_tail(player.scarf) draw_sprite(player.sprite) if player.state != .Dash { draw_sprite(player.gun.sprite) } health_bar_size := Vec2{16, 2} pos := player.sprite.pos pos.x -= health_bar_size.x / 2 pos.y -= 24 fw.draw_rect( pos - {1, 1}, health_bar_size + {2, 2}, color = fw.WHITE, ) fw.draw_progress_bar( pos, health_bar_size, fw.RED, fw.BLACK, f32(player.health) / f32(player.max_health), ) } player_take_damage :: proc(#any_int amt: i16, kb_dir: Vec2) { current_time := f32(fw.get_time()) if current_time - player.hit_timestamp < 0.2 { return } player.hit_timestamp = current_time player.health -= amt player.health = math.clamp(player.health, 0, player.max_health) if player.health == 0 { player.state = .Dead } vel := phys.get_velocity(player.body) + kb_dir phys.set_velocity(player.body, vel) } @(private = "file") already_spawned_player := false object_spawner_player_spawn :: proc(obj: Object_Resource) { if already_spawned_player { return } already_spawned_player = true phys.set_position(player.body, obj.pos) }