243 lines
6.7 KiB
C
243 lines
6.7 KiB
C
/* SPDX-License-Identifier: MIT
|
|
* Copyright (c) 2021 KikooDX
|
|
* This file is part of
|
|
* [Painfull Success CG](https://git.sr.ht/~kikoodx/painfull-success-cg),
|
|
* which is MIT licensed. The MIT license requires this copyright notice to be
|
|
* included in all copies and substantial portions of the software. */
|
|
#include "player.h"
|
|
#include "conf.h"
|
|
#include "input.h"
|
|
#include "lazyint.h"
|
|
#include "level.h"
|
|
#include "tiles.h"
|
|
#include "vec2.h"
|
|
#include <gint/display.h>
|
|
#include <stdbool.h>
|
|
|
|
void
|
|
player_init(Player *player)
|
|
{
|
|
player->pos = (Vec2){0, 0};
|
|
player->spd_x = 0.0;
|
|
player->spd_y = 0.0;
|
|
player->facing = 1;
|
|
player->stun = false;
|
|
player->knocked = false;
|
|
player->jump_buffer = 0;
|
|
player->coyot = 0;
|
|
}
|
|
|
|
void
|
|
player_update(Player *player, Level *level, Input input, u8 *level_id)
|
|
{
|
|
const Vec2 one_px_down = (Vec2){player->pos.x, player->pos.y + 1};
|
|
const bool on_ground =
|
|
player_collide(*level, one_px_down, SOLID_TILE, 0) ||
|
|
player_collide(*level, one_px_down, SEMI_SOLID_TILE, 0);
|
|
if (!player->stun) {
|
|
/* Get input. */
|
|
const i8 move_x = sign(input_is_down(input, K_RIGHT) -
|
|
input_is_down(input, K_LEFT));
|
|
const bool fastfall = input_is_down(input, K_DOWN);
|
|
const bool jump_pressed = input_is_pressed(input, K_JUMP);
|
|
const bool jump_held = input_is_down(input, K_JUMP);
|
|
/* Facing direction. */
|
|
if (move_x != 0)
|
|
player->facing = move_x;
|
|
/* Friction. */
|
|
if (on_ground)
|
|
player->spd_x *= 1 - GROUND_FRICTION;
|
|
else
|
|
player->spd_x *= 1 - AIR_FRICTION;
|
|
/* Acceleration. */
|
|
if (on_ground)
|
|
player->spd_x += move_x * GROUND_ACCELERATION;
|
|
else
|
|
player->spd_x += move_x * AIR_ACCELERATION;
|
|
/* Gravity. */
|
|
player->spd_y += GRAVITY;
|
|
/* Vertical friction. */
|
|
if (fastfall && player->spd_y > 0)
|
|
player->spd_y *= 1 - FASTFALL_RESISTANCE;
|
|
else
|
|
player->spd_y *= 1 - FALL_RESISTANCE;
|
|
/* Jump logic. */
|
|
/** Coyot jump. */
|
|
if (on_ground)
|
|
player->coyot = COYOT;
|
|
else if (player->coyot > 0)
|
|
player->coyot -= 1;
|
|
/** Input buffer. */
|
|
if (jump_pressed)
|
|
player->jump_buffer = JUMP_BUFFER;
|
|
else if (player->jump_buffer > 0)
|
|
player->jump_buffer -= 1;
|
|
/** Jump. */
|
|
if (jump_held && player->coyot > 0 && player->jump_buffer > 0) {
|
|
player->spd_y = JUMP_SPD;
|
|
player->jump_buffer = 0;
|
|
player->coyot = 0;
|
|
}
|
|
} else {
|
|
/* Gravity. */
|
|
player->spd_y += GRAVITY;
|
|
/* Vertical friction. */
|
|
player->spd_y *= 1 - FALL_RESISTANCE;
|
|
}
|
|
/* Semi-solid platform stop fall. */
|
|
if (player->spd_y > 0 &&
|
|
player_collide(*level, player->pos, SEMI_SOLID_TILE, 0))
|
|
player->spd_y = 0;
|
|
/* Apply movement. */
|
|
const bool hit_wall_x =
|
|
player_move(player, *level, round(player->spd_x), 0);
|
|
player_move(player, *level, 0, round(player->spd_y));
|
|
/* If player hit a wall in stun mode, turn back. */
|
|
if (hit_wall_x && player->stun) {
|
|
player->facing *= -1;
|
|
player->spd_x = KNOCKBACK_X * -player->facing;
|
|
}
|
|
/* Damageboost a.k.a. stun mode. */
|
|
if (player_collide(*level, player->pos, PAIN_TILE, 5)) {
|
|
if (!player->knocked) {
|
|
player->spd_y = -signf(player->spd_y) * KNOCKBACK_Y;
|
|
player->spd_x = KNOCKBACK_X * -player->facing;
|
|
player->knocked = true;
|
|
}
|
|
player->stun = true;
|
|
} else {
|
|
player->knocked = false;
|
|
if (on_ground)
|
|
player->stun = false;
|
|
}
|
|
/* Return to last checkpoint if out of bounds. */
|
|
if ((player->pos.x <= -PLAYER_WIDTH) ||
|
|
(player->pos.y <= -PLAYER_HEIGHT) ||
|
|
(player->pos.x >= LEVEL_WIDTH_PX) ||
|
|
(player->pos.y >= LEVEL_HEIGHT_PX)) {
|
|
player_init(player);
|
|
player->pos = level->start_pos;
|
|
}
|
|
/* Get keys. */
|
|
player_collide_keys(player, level);
|
|
/* Exit, victory! */
|
|
if (player->keys_left == 0 &&
|
|
player_collide(*level, player->pos, EXIT_TILE, 0)) {
|
|
*level_id += 1;
|
|
level_load(level, player, *level_id);
|
|
} else if (input_is_pressed(input, K_RESTART)) /* Manual restart. */
|
|
level_load(level, player, *level_id);
|
|
}
|
|
|
|
void
|
|
player_draw(Player player, i16 y_offset)
|
|
{
|
|
/* Draw colored rectangle depending on player state. */
|
|
u32 color = 0;
|
|
if (player.stun)
|
|
color = C_RGB(230 / 8, 41 / 8, 55 / 8); /* Red. */
|
|
else
|
|
color = C_RGB(0 / 8, 121 / 8, 241 / 8); /* Blue. */
|
|
const vec2_int_t x = player.pos.x + DRAW_OFFSET_X;
|
|
const vec2_int_t y = player.pos.y + y_offset;
|
|
drect(x, y, x + PLAYER_WIDTH - 1, y + PLAYER_HEIGHT - 1, color);
|
|
}
|
|
|
|
/* Helper functions */
|
|
|
|
bool
|
|
player_collide(Level level, Vec2 pos, tile_t tile, u8 margin)
|
|
{
|
|
/* Note: these `* 2` are here to completely replicate the behavior of
|
|
* the original Painfull Success. If you want to reuse this in your
|
|
* game, remove them. They are a bug! */
|
|
const vec2_int_t xl = pos.x + margin;
|
|
const vec2_int_t xr = pos.x + PLAYER_WIDTH - 1 - margin * 2;
|
|
const vec2_int_t yt = pos.y + margin;
|
|
const vec2_int_t yb = pos.y + PLAYER_HEIGHT - 1 - margin * 2;
|
|
return ((tile == level_get_tile_at_px(level, (Vec2){xl, yt})) ||
|
|
(tile == level_get_tile_at_px(level, (Vec2){xr, yt})) ||
|
|
(tile == level_get_tile_at_px(level, (Vec2){xl, yb})) ||
|
|
(tile == level_get_tile_at_px(level, (Vec2){xr, yb})));
|
|
}
|
|
|
|
bool
|
|
player_move(Player *player, Level level, i8 spd_x, i8 spd_y)
|
|
{
|
|
player->pos.x += spd_x;
|
|
player->pos.y += spd_y;
|
|
/* If player is in solid, move back until it ain't. */
|
|
if (player_collide(level, player->pos, SOLID_TILE, 0)) {
|
|
const i8 sign_spd_x = sign(spd_x);
|
|
const i8 sign_spd_y = sign(spd_y);
|
|
if (sign_spd_x != 0)
|
|
player->spd_x = 0;
|
|
if (sign_spd_y != 0)
|
|
player->spd_y = 0;
|
|
while (player_collide(level, player->pos, SOLID_TILE, 0)) {
|
|
player->pos.x -= sign_spd_x;
|
|
player->pos.y -= sign_spd_y;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* Seek for a key tile at given position, if found destroy it and return 1.
|
|
* Otherwise, return 0. */
|
|
u8
|
|
player_collide_key(Level *level, vec2_int_t x, vec2_int_t y)
|
|
{
|
|
if (level_get_tile_at_px(*level, (Vec2){x, y}) == KEY_TILE) {
|
|
level_set_tile_at_px(level, (Vec2){x, y}, AIR_TILE);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Destroy keys the player is touching and deduce them from keys left. */
|
|
void
|
|
player_collide_keys(Player *player, Level *level)
|
|
{
|
|
const vec2_int_t xl = player->pos.x;
|
|
const vec2_int_t xr = xl + PLAYER_WIDTH - 1;
|
|
const vec2_int_t yt = player->pos.y;
|
|
const vec2_int_t yb = yt + PLAYER_HEIGHT - 1;
|
|
player->keys_left -= player_collide_key(level, xl, yt) +
|
|
player_collide_key(level, xr, yt) +
|
|
player_collide_key(level, xl, yb) +
|
|
player_collide_key(level, xr, yb);
|
|
}
|
|
|
|
/* Used by `collide_keys()`. Don't call this one directly. */
|
|
i8
|
|
sign(i8 value)
|
|
{
|
|
if (value > 0)
|
|
return 1;
|
|
else if (value < 0)
|
|
return -1;
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
f32
|
|
signf(f32 value)
|
|
{
|
|
if (value > 0.0)
|
|
return 1.0;
|
|
else if (value < 0.0)
|
|
return -1.0;
|
|
else
|
|
return 0.0;
|
|
}
|
|
|
|
i8
|
|
round(f32 value)
|
|
{
|
|
if (value > 0)
|
|
return (i8)(value + 0.5);
|
|
else
|
|
return (i8)(value - 0.5);
|
|
}
|