painfull-success-cg/src/player.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);
}