/* * BreakHack - A dungeone crawler RPG * Copyright (C) 2018 Linus Probert * * 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 . */ #include #include #include #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" #include "actiontextbuilder.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) { static SDL_Color c_green = { 0, 255, 0, 255 }; char msg[10]; m_sprintf(msg, 5, "+%dxp", xp_gain); actiontextbuilder_create_text(msg, c_green, &player->sprite->pos); 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); actiontextbuilder_create_text("Level up", c_green, &player->sprite->pos); } } static void action_spent(Player *p) { p->stat_data.steps++; p->stat_data.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); } static bool has_collided(Player *player, RoomMatrix *matrix, Vector2d direction) { 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) { gui_log("You hit %s for %u damage", space->monster->lclabel, hit); player->stat_data.hits += 1; mixer_play_effect(SWORD_HIT); } else { gui_log("You missed %s", space->monster->lclabel); player->stat_data.misses += 1; } player_monster_kill_check(player, space->monster); action_spent(player); } else if (collided) { mixer_play_effect(BONK); camera_shake(direction, 100); 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); } } if (space->lethal && !collided) { mixer_play_effect(FALL); player->state = FALLING; } return collided; } static void set_clip_for_direction(Player *player, Vector2d *direction) { if (vector2d_equals(*direction, VECTOR2D_LEFT)) player->sprite->clip.y = 16; else if (vector2d_equals(*direction, VECTOR2D_RIGHT)) player->sprite->clip.y = 32; else if (vector2d_equals(*direction, VECTOR2D_UP)) player->sprite->clip.y = 48; else if (vector2d_equals(*direction, VECTOR2D_DOWN)) player->sprite->clip.y = 0; } static void move(Player *player, RoomMatrix *matrix, Vector2d direction) { set_clip_for_direction(player, &direction); player->sprite->pos.x += TILE_DIMENSION * (int) direction.x; player->sprite->pos.y += TILE_DIMENSION * (int) direction.y; if (has_collided(player, matrix, direction)) { player->sprite->pos.x -= TILE_DIMENSION * (int) direction.x; player->sprite->pos.y -= TILE_DIMENSION * (int) direction.y; } 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; Vector2d direction = VECTOR2D_NODIR; if (keyboard_direction_press(LEFT, event)) direction = VECTOR2D_LEFT; if (keyboard_direction_press(RIGHT, event)) direction = VECTOR2D_RIGHT; if (keyboard_direction_press(UP, event)) direction = VECTOR2D_UP; if (keyboard_direction_press(DOWN, event)) direction = VECTOR2D_DOWN; if (!vector2d_equals(direction, VECTOR2D_NODIR)) move(player, matrix, direction); map_room_modifier_player_effect(player, matrix, &direction, move); #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 (!vector2d_equals(VECTOR2D_NODIR, direction)) { 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->levelcap > player->stats.lvl) continue; 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 (player->state != ALIVE) return; 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); } Player* player_create(class_t class, SDL_Renderer *renderer) { Player *player = malloc(sizeof(Player)); player->sprite = sprite_create(); player->daggers = 0; player->stat_data.total_steps = 0; player->stat_data.steps = 0; player->stat_data.hits = 0; player->stat_data.kills = 0; player->stat_data.misses = 0; player->xp = 0; player->gold = 0; player->potion_sips = 0; player->class = class; player->state = ALIVE; player->projectiles = linkedlist_create(); player->animationTimer = timer_create(); 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; 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->stat_data.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) { static SDL_Color c_red = { 255, 0, 0, 255 }; static SDL_Color c_yellow = { 255, 255, 0, 255 }; if (p->stats.hp <= 0) { dmg = 200; } if (dmg > 0) { 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)); char msg[5]; m_sprintf(msg, 5, "-%d", dmg); actiontextbuilder_create_text(msg, c_red, &p->sprite->pos); } else { actiontextbuilder_create_text("Dodged", c_yellow, &p->sprite->pos); } } void player_render(Player *player, Camera *cam) { sprite_render(player->sprite, cam); LinkedList *projectile = player->projectiles; while (projectile) { projectile_render(projectile->data, cam); projectile = projectile->next; } } void player_reset_steps(Player *p) { p->stat_data.steps = 0; } void player_update(UpdateData *data) { Player *player = data->player; if (player->state == FALLING && player->stats.hp > 0) { if (!timer_started(player->animationTimer)) { timer_start(player->animationTimer); player->sprite->clip = CLIP16(0, 0); } else { if (timer_get_ticks(player->animationTimer) > 100) { timer_start(player->animationTimer); player->sprite->angle += 60; player->sprite->dim.width -= 4; player->sprite->dim.height -= 4; player->sprite->pos.x += 2; player->sprite->pos.y += 2; player->sprite->rotationPoint = (SDL_Point) { player->sprite->dim.width /2, player->sprite->dim.height /2 }; if (player->sprite->dim.width <= 4) player->stats.hp = 0; } } } LinkedList *remaining = linkedlist_create(); while (player->projectiles) { Projectile *p = linkedlist_pop(&player->projectiles); projectile_update(p, data); if (p->alive) { linkedlist_push(&remaining, p); } else { projectile_destroy(p); action_spent(player); } } linkedlist_destroy(&player->projectiles); player->projectiles = remaining; } void player_destroy(Player *player) { if (player->sprite) sprite_destroy(player->sprite); timer_destroy(player->animationTimer); 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); }