/* 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 #include 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); }