breakhack/src/player.c

563 lines
13 KiB
C

/*
* BreakHack - A dungeone crawler RPG
* Copyright (C) 2018 Linus Probert <linus.probert@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "player.h"
#include "monster.h"
#include "util.h"
#include "gui.h"
#include "item.h"
#include "particle_engine.h"
#include "keyboard.h"
#include "mixer.h"
#include "random.h"
#include "projectile.h"
#include "texturecache.h"
#include "vector2d.h"
#define ENGINEER_STATS { 12, 12, 5, 7, 2, 2, 1 }
#define MAGE_STATS { 12, 12, 5, 7, 1, 2, 1 }
#define PALADIN_STATS { 12, 12, 8, 9, 3, 1, 1 }
#define ROGUE_STATS { 12, 12, 5, 7, 1, 2, 1 }
#define WARRIOR_STATS { 12, 12, 8, 9, 3, 1, 1 }
static void
player_levelup(Player *player)
{
mixer_play_effect(LEVEL_UP);
player->stats.lvl += 1;
player->stats.maxhp += 9;
player->stats.dmg += 5;
player->stats.atk += 1;
// Limit health to 3 rows of hearts
if (player->stats.maxhp > 72)
player->stats.maxhp = 72;
player->stats.hp = player->stats.maxhp;
}
static unsigned int
next_level_threshold(unsigned int current_level)
{
unsigned int last_level = 0;
unsigned int padding = 0;
if (current_level > 0) {
last_level = next_level_threshold(current_level - 1);
padding = (current_level - 1) * 150;
}
return last_level + (current_level * 50) + padding;
}
static void
player_gain_xp(Player *player, unsigned int xp_gain)
{
player->xp += xp_gain;
if (player->xp >= next_level_threshold(player->stats.lvl)) {
player_levelup(player);
gui_log("You have reached level %u", player->stats.lvl);
gui_event_message("You reached level %u", player->stats.lvl);
}
}
static void
action_spent(Player *p)
{
p->steps++;
p->total_steps++;
for (size_t i = 0; i < PLAYER_SKILL_COUNT; ++i) {
if (p->skills[i] != NULL && p->skills[i]->resetCountdown > 0)
p->skills[i]->resetCountdown--;
}
}
static void
player_step(Player *p)
{
action_spent(p);
p->missText->pos = p->sprite->pos;
p->hitText->pos = p->sprite->pos;
}
static bool
has_collided(Player *player, RoomMatrix *matrix)
{
bool collided = false;
Position roomCoord = position_to_room_coords(&player->sprite->pos);
if (roomCoord.x != matrix->roomPos.x
|| roomCoord.y != matrix->roomPos.y) {
return collided;
}
Position matrixPos = position_to_matrix_coords(&player->sprite->pos);
RoomSpace *space = &matrix->spaces[matrixPos.x][matrixPos.y];
collided = space->occupied;
if (space->monster != NULL) {
unsigned int hit = stats_fight(&player->stats,
&space->monster->stats);
mixer_play_effect(SWING0 + get_random(2));
monster_hit(space->monster, hit);
if (hit > 0) {
player->hits += 1;
mixer_play_effect(SWORD_HIT);
} else {
player->misses += 1;
}
if (hit > 0)
gui_log("You hit %s for %u damage",
space->monster->lclabel, hit);
else
gui_log("You missed %s", space->monster->lclabel);
player_monster_kill_check(player, space->monster);
action_spent(player);
} else if (collided) {
mixer_play_effect(BONK);
gui_log("Ouch! There is something in the way");
}
if (space->items != NULL && !collided) {
LinkedList *items = space->items;
while (items != NULL) {
Item *item = items->data;
items = items->next;
item_collected(item, player);
}
}
return collided;
}
static void
move_left(Player *player, RoomMatrix *matrix)
{
player->sprite->clip.y = 16;
player->sprite->pos.x -= TILE_DIMENSION;
if (has_collided(player, matrix))
player->sprite->pos.x += TILE_DIMENSION;
else
player_step(player);
}
static void
move_right(Player *player, RoomMatrix *matrix)
{
player->sprite->clip.y = 32;
player->sprite->pos.x += TILE_DIMENSION;
if (has_collided(player, matrix))
player->sprite->pos.x -= TILE_DIMENSION;
else
player_step(player);
}
static void
move_up(Player *player, RoomMatrix *matrix)
{
player->sprite->clip.y = 48;
player->sprite->pos.y -= TILE_DIMENSION;
if (has_collided(player, matrix))
player->sprite->pos.y += TILE_DIMENSION;
else
player_step(player);
}
static void
move_down(Player *player, RoomMatrix *matrix)
{
player->sprite->clip.y = 0;
player->sprite->pos.y += TILE_DIMENSION;
if (has_collided(player, matrix))
player->sprite->pos.y -= TILE_DIMENSION;
else
player_step(player);
}
void
player_sip_health(Player *player)
{
if (player->potion_sips > 0) {
--player->potion_sips;
++player->stats.hp;
mixer_play_effect(BUBBLE0 + get_random(2));
gui_log("You take a sip of health potion");
} else {
gui_log("You have nothing to sip");
}
}
static void
handle_movement_input(Player *player, RoomMatrix *matrix, SDL_Event *event)
{
static unsigned int step = 1;
bool moved = false;
if (keyboard_direction_press(LEFT, event)) {
move_left(player, matrix);
moved = true;
}
if (keyboard_direction_press(RIGHT, event)) {
move_right(player, matrix);
moved = true;
}
if (keyboard_direction_press(UP, event)) {
move_up(player, matrix);
moved = true;
}
if (keyboard_direction_press(DOWN, event)) {
move_down(player, matrix);
moved = true;
}
#ifdef DEBUG
if (keyboard_mod_press(SDLK_SPACE, KMOD_CTRL, event)) {
Position pos = player->sprite->pos;
pos.x += 8;
pos.y += 8;
particle_engine_bloodspray(pos, (Dimension) { 8, 8 }, 200);
player->stats.hp = 0;
}
#endif // DEBUG
if (moved) {
player->sprite->clip.x = 16*step;
++step;
step = step % 4;
}
}
static void
use_skill(Skill *skill, SkillData *skillData)
{
skill->active = false;
skill->use(skill, skillData);
if (skill->actionRequired)
player_step(skillData->player);
skill->resetCountdown = skill->resetTime;
}
static void
check_skill_activation(Player *player, RoomMatrix *matrix, SDL_Event *event)
{
// TODO(Linus): This could be "smarter"
unsigned int selected = 0;
if (keyboard_press(SDLK_1, event)) {
selected = 1;
} else if (keyboard_press(SDLK_2, event)) {
selected = 2;
} else if (keyboard_press(SDLK_3, event)) {
selected = 3;
} else if (keyboard_press(SDLK_4, event)) {
selected = 4;
} else if (keyboard_press(SDLK_5, event)) {
selected = 5;
}
if (selected == 0)
return;
for (size_t i = 0; i < PLAYER_SKILL_COUNT; ++i) {
if (!player->skills[i])
continue;
Skill *skill = player->skills[i];
if (skill->available && !skill->available(player))
continue;
skill->active = (selected - 1) == i && !skill->active && skill->resetCountdown == 0;
if (skill->active && skill->instantUse) {
SkillData skillData = { player, matrix, VECTOR2D_NODIR };
use_skill(skill, &skillData);
}
}
}
static bool
check_skill_trigger(Player *player, RoomMatrix *matrix, SDL_Event *event)
{
int activeSkill = -1;
for (int i = 0; i < PLAYER_SKILL_COUNT; ++i) {
if (player->skills[i] && player->skills[i]->active) {
activeSkill = i;
break;
}
}
if (activeSkill < 0)
return false;
Vector2d dir;
if (keyboard_direction_press(UP, event))
dir = VECTOR2D_UP;
else if (keyboard_direction_press(DOWN, event))
dir = VECTOR2D_DOWN;
else if (keyboard_direction_press(LEFT, event))
dir = VECTOR2D_LEFT;
else if (keyboard_direction_press(RIGHT, event))
dir = VECTOR2D_RIGHT;
else
return false;
SkillData skillData = { player, matrix, dir };
use_skill(player->skills[activeSkill], &skillData);
return true;
}
static void
handle_player_input(Player *player, RoomMatrix *matrix, SDL_Event *event)
{
if (event->type != SDL_KEYDOWN)
return;
if (player->projectiles)
return;
check_skill_activation(player, matrix, event);
if (!check_skill_trigger(player, matrix, event))
handle_movement_input(player, matrix, event);
}
static void
player_load_texts(Player *p, SDL_Renderer *renderer)
{
ActionText *t = actiontext_create();
actiontext_load_font(t, "GUI/SDS_6x6.ttf", 14);
t->color = (SDL_Color) { 255, 100, 0, 255 };
actiontext_set_text(t, "HIT", renderer);
t->pos = p->sprite->pos;
p->hitText = t;
t = actiontext_create();
actiontext_load_font(t, "GUI/SDS_6x6.ttf", 14);
t->color = (SDL_Color) { 255, 255, 0, 255 };
actiontext_set_text(t, "MISS", renderer);
t->pos = p->sprite->pos;
p->missText = t;
}
Player*
player_create(class_t class, SDL_Renderer *renderer)
{
Player *player = malloc(sizeof(Player));
player->sprite = sprite_create();
player->daggers = 10;
player->total_steps = 0;
player->steps = 0;
player->xp = 0;
player->hits = 0;
player->kills = 0;
player->misses = 0;
player->gold = 0;
player->potion_sips = 0;
player->class = class;
for (size_t i = 0; i < PLAYER_SKILL_COUNT; ++i) {
player->skills[i] = NULL;
}
char asset[100];
switch (class) {
case ENGINEER:
m_strcpy(asset, 100, "Commissions/Engineer.png");
player->stats = (Stats) ENGINEER_STATS;
break;
case MAGE:
m_strcpy(asset, 100, "Commissions/Mage.png");
player->stats = (Stats) MAGE_STATS;
break;
case PALADIN:
m_strcpy(asset, 100, "Commissions/Paladin.png");
player->stats = (Stats) PALADIN_STATS;
break;
case ROGUE:
m_strcpy(asset, 100, "Commissions/Rogue.png");
player->stats = (Stats) ROGUE_STATS;
break;
case WARRIOR:
m_strcpy(asset, 100, "Commissions/Warrior.png");
player->stats = (Stats) WARRIOR_STATS;
player->skills[0] = skill_create(FLURRY);
player->skills[1] = skill_create(CHARGE);
player->skills[2] = skill_create(DAGGER_THROW);
break;
}
player->skills[4] = skill_create(SIP_HEALTH);
sprite_load_texture(player->sprite, asset, 0, renderer);
player->sprite->pos = (Position) { TILE_DIMENSION, TILE_DIMENSION };
player->sprite->dim = GAME_DIMENSION;
player->sprite->clip = (SDL_Rect) { 0, 0, 16, 16 };
player->handle_event = &handle_player_input;
player->projectiles = linkedlist_create();
player_load_texts(player, renderer);
return player;
}
ExperienceData player_get_xp_data(Player *p)
{
ExperienceData data;
data.previousLevel = next_level_threshold(p->stats.lvl - 1);
data.current = p->xp;
data.nextLevel = next_level_threshold(p->stats.lvl);
data.level = p->stats.lvl;
return data;
}
void
player_monster_kill_check(Player *player, Monster *monster)
{
if (!monster)
return;
if (monster->stats.hp <= 0) {
unsigned int gained_xp = 5 * monster->stats.lvl;
player->kills += 1;
mixer_play_effect(DEATH);
gui_log("You killed %s and gained %d xp",
monster->lclabel, gained_xp);
player_gain_xp(player, gained_xp);
}
}
void
player_hit(Player *p, unsigned int dmg)
{
if (p->stats.hp <= 0) {
dmg = 200;
}
if (dmg > 0) {
p->hitText->active = true;
p->missText->active = false;
Position pos = p->sprite->pos;
pos.x += 8;
pos.y += 8;
particle_engine_bloodspray(pos, (Dimension) { 8, 8 }, dmg);
mixer_play_effect(PLAYER_HIT0 + get_random(2));
} else {
p->missText->active = true;
p->hitText->active = false;
}
}
void
player_render(Player *player, Camera *cam)
{
sprite_render(player->sprite, cam);
actiontext_render(player->hitText, cam);
actiontext_render(player->missText, cam);
LinkedList *projectile = player->projectiles;
while (projectile) {
projectile_render(projectile->data, cam);
projectile = projectile->next;
}
}
static void
player_print(Player *p)
{
Position roomPos = position_to_matrix_coords(&p->sprite->pos);
Position pos = p->sprite->pos;
debug("\n");
debug("--------=== <[ Player Stats ]> ===--------");
debug("Hits: %u\tMisses:\t%u", p->hits, p->misses);
debug("Kills: %u", p->kills);
debug("Steps: %u", p->total_steps);
debug("Pos: %dx%d\tRoomPos: %dx%d", pos.x, pos.y,
roomPos.x, roomPos.y);
debug("------------------------------------------");
}
void
player_reset_steps(Player *p)
{
p->steps = 0;
player_print(p);
}
void player_update(Player *player, RoomMatrix *rm, float deltatime)
{
if (!player->projectiles)
return;
LinkedList *last, *current, *next;
last = NULL;
current = player->projectiles;
next = NULL;
while (current) {
Projectile *p = current->data;
projectile_update(p, player, rm, deltatime);
if (!p->alive) {
if (last == NULL)
player->projectiles = current->next;
else
last->next = current->next;
projectile_destroy(p);
next = current->next;
current->data = NULL;
current->next = NULL;
linkedlist_destroy(&current);
current = next;
action_spent(player);
} else {
last = current;
current = current->next;
}
}
}
void
player_destroy(Player *player)
{
if (player->sprite)
sprite_destroy(player->sprite);
actiontext_destroy(player->hitText);
actiontext_destroy(player->missText);
for (size_t i = 0; i < PLAYER_SKILL_COUNT; ++i) {
if (player->skills[i])
skill_destroy(player->skills[i]);
player->skills[i] = NULL;
}
while (player->projectiles)
projectile_destroy(linkedlist_pop(&player->projectiles));
free(player);
}