diff --git a/resources/escape.png b/resources/escape.png new file mode 100644 index 0000000..d1060cd Binary files /dev/null and b/resources/escape.png differ diff --git a/resources/game_over.png b/resources/game_over.png new file mode 100644 index 0000000..f62a2ce Binary files /dev/null and b/resources/game_over.png differ diff --git a/resources/maze.png b/resources/maze.png new file mode 100644 index 0000000..890b9b5 Binary files /dev/null and b/resources/maze.png differ diff --git a/src/core/character.cpp b/src/core/character.cpp index e60c522..98fc361 100644 --- a/src/core/character.cpp +++ b/src/core/character.cpp @@ -13,16 +13,16 @@ void CharacterLogic::set_character(Character *character) { Character::Character(Tile location, CharacterLogic *logic, CharacterData stats) : data{stats} -, logic{logic} , health{stats.health} , location{location} -, world{nullptr} { +, world{nullptr} +, logic{logic} { this->logic->set_character(this); } void Character::act() { assert(this->world != nullptr && "World generation did not initialize character properly"); - if(this->health < 0) { + if(this->health <= 0) { return; } this->logic->set_character(this); diff --git a/src/core/input.cpp b/src/core/input.cpp index daa686d..90b67d5 100644 --- a/src/core/input.cpp +++ b/src/core/input.cpp @@ -12,12 +12,16 @@ InputHandler::~InputHandler() { Input::remove_handler(this); } -KeyEventHandler::KeyEventHandler(std::set code, Listener listener) +KeyEventHandler::KeyEventHandler(std::set code, Listener listener, bool allow_repeat) : scancode{code} -, listener{listener} {} +, listener{listener} +, allow_repeat{allow_repeat} +{} void KeyEventHandler::process_event(SDL_Event event) { - if((event.type == SDL_KEYDOWN || event.type == SDL_KEYUP) && this->scancode.contains(event.key.keysym.scancode)) { + if((event.type == SDL_KEYDOWN || event.type == SDL_KEYUP) + && (event.key.repeat == 0 || this->allow_repeat) + && this->scancode.contains(event.key.keysym.scancode)) { this->listener(event.type == SDL_KEYDOWN); } } diff --git a/src/core/input.h b/src/core/input.h index 0d1a7ea..e295d51 100644 --- a/src/core/input.h +++ b/src/core/input.h @@ -15,10 +15,11 @@ struct InputHandler { struct KeyEventHandler : public InputHandler { typedef std::function Listener; - KeyEventHandler(std::set code, Listener listener); + KeyEventHandler(std::set code, Listener listener, bool allow_repeat = true); virtual void process_event(SDL_Event event) override; std::set scancode{}; Listener listener{}; + bool allow_repeat{false}; }; class Input { diff --git a/src/core/renderer.cpp b/src/core/renderer.cpp index e8a92f8..cbd5df0 100644 --- a/src/core/renderer.cpp +++ b/src/core/renderer.cpp @@ -1,5 +1,6 @@ #include "renderer.h" #include +#include #include #include #include @@ -35,15 +36,15 @@ RenderDataSetup &RenderDataSetup::with_resource_path(std::filesystem::path path) } RenderDataSetup &RenderDataSetup::with_sprite(std::filesystem::path sprite_path) { - assert(this->renderer != nullptr && "Cannot create sprites without a renderer"); - assert(this->resource_base_path.has_value() && "Resource base path has to be set before loading sprites"); - std::filesystem::path complete_path{this->resource_base_path.value()/sprite_path}; - SDL_Log("Adding sprite: %s", complete_path.c_str()); - assert(std::filesystem::exists(complete_path) && "Sprite path has to exist"); - if(SDL_Texture *texture{IMG_LoadTexture(this->renderer, complete_path.native().c_str())}) { + if(SDL_Texture *texture{this->load_texture(sprite_path)}) { this->sprites.push_back(texture); - } else { - SDL_Log("Failed to load texture %s reason: %s", complete_path.c_str(), SDL_GetError()); + } + return *this; +} + +RenderDataSetup &RenderDataSetup::with_menu(std::filesystem::path file_path) { + if(SDL_Texture *texture{this->load_texture(file_path)}) { + this->menus.push_back(texture); } return *this; } @@ -56,13 +57,29 @@ RenderData RenderDataSetup::build() { data.window=this->window; data.renderer=this->renderer; data.sprites = this->sprites; + data.menus = this->menus; return data; } +SDL_Texture *RenderDataSetup::load_texture(std::filesystem::path file_path) { + assert(this->renderer != nullptr && "Cannot create sprites without a renderer"); + assert(this->resource_base_path.has_value() && "Resource base path has to be set before loading sprites"); + std::filesystem::path complete_path{this->resource_base_path.value() / file_path}; + SDL_Log("Adding sprite: %s", complete_path.string().c_str()); + assert(std::filesystem::exists(complete_path) && "Sprite path has to exist"); + if(SDL_Texture *texture{IMG_LoadTexture(this->renderer, complete_path.string().c_str())}) { + return texture; + } else { + SDL_Log("Failed to load texture %s reason: %s", complete_path.string().c_str(), SDL_GetError()); + return nullptr; + } +} + Tile Render::camera_center{0u, 0u}; RenderData* Render::data{nullptr}; unsigned Render::camera_width{10}; Tile Render::camera_offset{0, 0}; +SDL_Point Render::window_size{0, 0}; int Render::tile_screen_width{2}; int Render::half_tile_screen_width{1}; @@ -83,14 +100,13 @@ void Render::clear(Tile camera_center, unsigned fov) { SDL_SetRenderDrawColor(Render::data->renderer, 0u, 0u, 0u, 255u); SDL_RenderClear(Render::data->renderer); - int width, height; - SDL_GetWindowSize(Render::data->window, &width, &height); - Render::tile_screen_width = int(width / Render::camera_width); + SDL_GetWindowSize(Render::data->window, &window_size.x, &window_size.y); + Render::tile_screen_width = int(window_size.x / Render::camera_width); Render::half_tile_screen_width = Render::tile_screen_width >> 1; Render::camera_center = camera_center; - Render::camera_offset.x = -Render::camera_center.x * Render::tile_screen_width + (width >> 1); - Render::camera_offset.y = -Render::camera_center.y * Render::tile_screen_width + (height >> 1); + Render::camera_offset.x = -Render::camera_center.x * Render::tile_screen_width + (window_size.x >> 1); + Render::camera_offset.y = -Render::camera_center.y * Render::tile_screen_width + (window_size.y >> 1); } void Render::draw(Tile world_space_tile, Sprite sprite) { @@ -108,6 +124,30 @@ void Render::draw(Tile world_space_tile, Sprite sprite) { SDL_RenderCopy(Render::data->renderer, Render::data->sprites[sprite], 0 , &rect); } +void Render::draw_menu(Sprite menu) { + assert_render_initialized(); + Render::clear({0, 0}); + SDL_Rect const rect { + .x = window_size.x/2 - window_size.y/2, + .y = 0, + .w = window_size.y, + .h = window_size.y + }; + SDL_RenderCopy(Render::data->renderer, Render::data->menus[menu], 0, &rect); +} + +void Render::draw_healthbar(int current, int max, SDL_Rect area, int border) { + assert_render_initialized(); + SDL_SetRenderDrawColor(Render::data->renderer, 0,0, 0, 255); + SDL_RenderFillRect(Render::data->renderer, &area); + area.x += border; + area.y += border; + area.h -= border * 2; + area.w = ((area.w - border * 2) * current) / max; + SDL_SetRenderDrawColor(Render::data->renderer, 200,200, 200, 255); + SDL_RenderFillRect(Render::data->renderer, &area); +} + void Render::present() { assert_render_initialized(); SDL_RenderPresent(Render::data->renderer); diff --git a/src/core/renderer.h b/src/core/renderer.h index 0c7a1e8..10d153e 100644 --- a/src/core/renderer.h +++ b/src/core/renderer.h @@ -19,6 +19,7 @@ private: RenderData() = default; SDL_Window *window{nullptr}; SDL_Renderer *renderer{nullptr}; std::vector sprites{}; + std::vector menus{}; }; // Global interface for rendering @@ -31,11 +32,14 @@ public: static void clear(Tile camera_center, unsigned fov = 10); static void draw(Tile world_space_tile, Sprite sprite); + static void draw_menu(Sprite menu); + static void draw_healthbar(int current, int max, SDL_Rect area = {0, 0, 500, 100}, int border = 5); static void present(); private: static Tile camera_center; static unsigned camera_width; static Tile camera_offset; + static SDL_Point window_size; static int tile_screen_width; static int half_tile_screen_width; static RenderData *data; @@ -49,13 +53,17 @@ public: RenderDataSetup &with_renderer(Uint32 flags); RenderDataSetup &with_resource_path(std::filesystem::path base_path); RenderDataSetup &with_sprite(std::filesystem::path sprite_path); + RenderDataSetup &with_menu(std::filesystem::path sprite_path); RenderData build(); +private: + SDL_Texture *load_texture(std::filesystem::path file_path); private: unsigned fov{10}; std::optional resource_base_path; SDL_Window *window{nullptr}; SDL_Renderer *renderer{nullptr}; std::vector sprites{}; + std::vector menus{}; }; } diff --git a/src/core/world.cpp b/src/core/world.cpp index 0d26d46..3158ede 100644 --- a/src/core/world.cpp +++ b/src/core/world.cpp @@ -47,15 +47,23 @@ World::World() std::bind(&World::on_input, this, std::placeholders::_1) } {} +void World::act_if_requested() { + if(this->turn_requested) { + this->act(); + this->turn_requested = false; + } +} + void World::act() { - player.act(); + this->player.act(); + this->boss.act(); for(Character &character : this->characters) { character.act(); } } void World::render() { - Render::clear(this->player.location, 15); + Render::clear(this->player.location, 100); for(size_t i{0}; i < this->rooms.size(); ++i) { this->rooms[i].draw({ .x = int(i % this->shear * this->chunk_size), @@ -65,7 +73,9 @@ void World::render() { for(Character &character : this->characters) { character.draw(); } + boss.draw(); player.draw(); + Render::draw_healthbar(player.health, player.data.health); } Room &World::get_room(Chunk chunk) { @@ -76,6 +86,10 @@ Character *World::get_player() { return &this->player; } +Character *World::get_boss() { + return &this->boss; +} + Chunk World::get_chunk(Tile tile) { return {tile.x / this->chunk_size, tile.y / this->chunk_size}; } @@ -86,8 +100,11 @@ TileData World::query_tile(Tile tile) { .character = nullptr, .is_wall = room.tile_is_wall({tile.x % this->chunk_size, tile.y % this->chunk_size}) }; - if(player.location == tile) { - out.character = &player; + if(this->player.location == tile) { + out.character = &this->player; + return out; + } else if(this->boss.location == tile) { + out.character = &this->boss; return out; } for(Character &character : this->characters) { @@ -100,8 +117,8 @@ TileData World::query_tile(Tile tile) { } void World::on_input(bool pressed) { - if(!pressed) { - this->act(); + if(pressed) { + this->turn_requested = true; } } @@ -128,8 +145,9 @@ WorldGenerator &WorldGenerator::with_room_size(unsigned min_side_length, unsigne return *this; } -WorldGenerator &WorldGenerator::with_player(Character character) { - this->player.emplace(character); +WorldGenerator &WorldGenerator::with_player(std::function spawner) { + assert(this->player_spawner == nullptr && "Player spawner cannot be assigned twice, remove previous call"); + this->player_spawner = spawner; return *this; } @@ -138,6 +156,12 @@ WorldGenerator &WorldGenerator::with_enemy(std::function spawne return *this; } +WorldGenerator &WorldGenerator::with_boss(std::function spawner) { + assert(this->boss_spawner == nullptr && "Boss spawner cannot be assigned twice, remove previous call"); + this->boss_spawner = spawner; + return *this; +} + WorldGenerator &WorldGenerator::with_connecting_chance(unsigned num) { this->connecting_chance = num; return *this; @@ -148,7 +172,8 @@ std::unique_ptr WorldGenerator::generate() { assert(this->min_room_size > 0 && "Room size is required to generate world"); assert(this->min_room_size < this->max_room_size && "Room size is required to generate world"); assert(this->world_side_length > 0 && "World size is required to generate world"); - assert(this->player.has_value() && "World cannot be generated without a player"); + assert(this->player_spawner != nullptr && "World cannot be generated without a player"); + assert(this->boss_spawner != nullptr && "Boss spawner is required to generate world"); std::unique_ptr world{new World()}; Chunk start_room{this->world_side_length / 2, this->world_side_length / 2}; @@ -158,6 +183,7 @@ std::unique_ptr WorldGenerator::generate() { } void WorldGenerator::generate_world(std::unique_ptr &world, Chunk start) { + this->boss_generated = false; world->chunk_size = this->chunk_side_length; world->shear = this->world_side_length; world->rooms.resize(this->world_side_length * this->world_side_length); @@ -201,7 +227,6 @@ void WorldGenerator::generate_room(std::unique_ptr &world, Chunk const &l {last.x, last.y + 1}, {last.x, last.y - 1} }; - this->add_enemies(world, last, last_room); static const Directions directions[]{EAST, WEST, SOUTH, NORTH}; static const Directions inv_directions[] {WEST, EAST, NORTH, SOUTH}; int const rand_offset{std::rand() % 4}; @@ -213,7 +238,7 @@ void WorldGenerator::generate_room(std::unique_ptr &world, Chunk const &l continue; } Room &option{world->get_room(next)}; - if(this->connecting_chance > 0 && !option.initialized || WRAP(std::rand(), 0, this->connecting_chance) == 0) { + if((this->connecting_chance > 0 && !option.initialized) || WRAP(std::rand(), 0, this->connecting_chance) == 0) { // register hallway with the last chunk last_room.hallway_paths |= directions[true_offset]; // add returning hallway from the new chunk @@ -225,14 +250,25 @@ void WorldGenerator::generate_room(std::unique_ptr &world, Chunk const &l // generate surrounding rooms this->generate_room(world, next); } + this->add_enemies(world, last, last_room); } void WorldGenerator::add_enemies(std::unique_ptr &world, Chunk const &chunk, Room &room) { + // this will get executed only by the first room to run out of neighbours to fill + if(!this->boss_generated) { // spawn boss at the center of the room + Character boss{this->boss_spawner({ + chunk.x * this->chunk_side_length + room.rect.x + room.rect.w/2, + chunk.y * this->chunk_side_length + room.rect.y + room.rect.h/2 + })}; + boss.world = world.get(); + world->boss = boss; + this->boss_generated = true; + } for(std::pair, int> const &pair : this->enemy_spawners) { if(WRAP(std::rand(), 0, pair.second) == 0) { Tile offset{ - WRAP(std::rand(), 1, room.rect.w - 2) + (chunk.x * this->chunk_side_length), - WRAP(std::rand(), 1, room.rect.h - 2) + (chunk.y * this->chunk_side_length) + WRAP(std::rand(), 1, room.rect.w - 2) + (chunk.x * this->chunk_side_length) + room.rect.x, + WRAP(std::rand(), 1, room.rect.h - 2) + (chunk.y * this->chunk_side_length) + room.rect.y }; Character character{pair.first(offset)}; character.world = world.get(); @@ -242,11 +278,10 @@ void WorldGenerator::add_enemies(std::unique_ptr &world, Chunk const &chu } void WorldGenerator::setup_player(std::unique_ptr &world, Chunk spawn_chunk) { - world->player = this->player.value(); - world->player.world = world.get(); - world->player.location = { + world->player = this->player_spawner({ (spawn_chunk.x * this->chunk_side_length) + (this->chunk_side_length / 2), (spawn_chunk.y * this->chunk_side_length) + (this->chunk_side_length / 2), - }; + }); + world->player.world = world.get(); } } diff --git a/src/core/world.h b/src/core/world.h index a213722..37f7e5b 100644 --- a/src/core/world.h +++ b/src/core/world.h @@ -32,17 +32,21 @@ struct World { friend class WorldGenerator; World(); ~World() = default; + void act_if_requested(); void act(); void render(); Room &get_room(Chunk chunk); Character *get_player(); + Character *get_boss(); Chunk get_chunk(Tile tile); TileData query_tile(Tile tile); void on_input(bool); private: int chunk_size{1}; unsigned shear{0}; + bool turn_requested{false}; Character player{}; + Character boss{}; std::vector rooms{}; std::vector characters{}; KeyEventHandler direction_pressed; @@ -54,8 +58,9 @@ public: WorldGenerator &with_chunk_size(unsigned side_length); WorldGenerator &with_world_size(unsigned side_length); WorldGenerator &with_room_size(unsigned min_side_length, unsigned max_side_length); - WorldGenerator &with_player(Character character); + WorldGenerator &with_player(std::function spawner); WorldGenerator &with_enemy(std::function spawner, int inv_frequency); + WorldGenerator &with_boss(std::function spawner); WorldGenerator &with_connecting_chance(unsigned num); std::unique_ptr generate(); private: @@ -64,11 +69,13 @@ private: void add_enemies(std::unique_ptr &world, Chunk const &chunk, Room &room); void setup_player(std::unique_ptr &world, Chunk spawn_chunk); private: - std::optional player; + std::function player_spawner{}; + std::function boss_spawner{}; std::vector, int>> enemy_spawners{}; int chunk_side_length{0}, world_side_length{0}; int max_room_size{0}, min_room_size{0}; unsigned connecting_chance{1}; + bool boss_generated{false}; }; } diff --git a/src/enemies.cpp b/src/enemies.cpp index 1339e4e..46137af 100644 --- a/src/enemies.cpp +++ b/src/enemies.cpp @@ -37,7 +37,7 @@ Tile CritteLogic::move() { } else { // cache get_chunk call for easier reading std::function get_chunk{std::bind(&World::get_chunk, this->character->world, std::placeholders::_1)}; - this->is_aware_of_player |= get_chunk(player->location) != get_chunk(location); + this->is_aware_of_player |= get_chunk(player->location) == get_chunk(location); } // fallback return value, for if there is no desirable tile to move to return { diff --git a/src/main.cpp b/src/main.cpp index c6137aa..a191b24 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -9,55 +9,142 @@ using namespace rogue; +// the DOOM I/II solution :) +enum GameState { + MainMenu, + Game, + GameOver, + Escape +}; + CharacterData const player_data{ .health = 20, .damage = 2, .sprite = 3 }; +CharacterData const boss_data{ + .health = 10, + .damage = 3, + .sprite = 0 +}; + CharacterData const critte_data{ - .health = 5, + .health = 4, .damage = 1, .sprite = 2 }; template -Character create_character(CharacterData const &stats, Tile tile) { +static Character create_character(CharacterData const &stats, Tile tile) { return Character(tile, new TLogicType, stats); } int main(int argc, char *argv[]) { - std::unique_ptr world{WorldGenerator() - .with_world_size(20) - .with_chunk_size(10) - .with_room_size(6, 10) - .with_connecting_chance(5) - .with_player(Character({0,0}, new PlayerCharacterLogic(), player_data)) + GameState game_state{GameState::MainMenu}; + WorldGenerator generator{WorldGenerator() + .with_world_size(10) + .with_chunk_size(10) + .with_room_size(6, 10) + .with_connecting_chance(5) + .with_player(std::bind(&create_character, player_data, std::placeholders::_1)) .with_enemy(std::bind(&create_character, critte_data, std::placeholders::_1), 2) - .generate() - }; + .with_enemy(std::bind(&create_character, critte_data, std::placeholders::_1), 2) + .with_enemy(std::bind(&create_character, critte_data, std::placeholders::_1), 2) + .with_boss(std::bind(&create_character, boss_data, std::placeholders::_1))}; RenderData data{RenderDataSetup() + // create window and renderer, allowing for texture creation .with_window("roguelike", SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_FULLSCREEN) .with_renderer(SDL_RENDERER_ACCELERATED) - .with_resource_path("resources/") - .with_sprite("wizard.png") - .with_sprite("wall.png") - .with_sprite("critte.png") - .with_sprite("player.png") + // setup resources + .with_resource_path("resources/") + // sprites + .with_sprite("wizard.png") + .with_sprite("wall.png") + .with_sprite("critte.png") + .with_sprite("player.png") + // menus + .with_menu("maze.png") + .with_menu("game_over.png") + .with_menu("escape.png") .build() }; + // use features to capture game_state and listen for input within main() + KeyEventHandler return_to_main_input{{SDL_SCANCODE_RETURN}, + [&game_state](bool down) { + if(!down) return; + switch(game_state) { + case GameState::Game: // do nothing, let the input be handled by the world/characters + case GameState::MainMenu: // main menu starts with direction input not enter + break; + case GameState::GameOver: + game_state = GameState::MainMenu; + break; + case GameState::Escape: + game_state = GameState::MainMenu; + break; + } + }, + false + }; + KeyEventHandler start_game_input{{ + SDL_SCANCODE_H, SDL_SCANCODE_J, SDL_SCANCODE_K, SDL_SCANCODE_L, + SDL_SCANCODE_LEFT, SDL_SCANCODE_RIGHT, SDL_SCANCODE_UP, SDL_SCANCODE_DOWN + }, + [&game_state](bool down) { + if(!down) return; + switch(game_state) { + case GameState::Game: + case GameState::GameOver: + case GameState::Escape: + break; + case GameState::MainMenu: + game_state = GameState::Game; + break; + } + }, + false + }; Render::provide_render_data(data); + std::unique_ptr world{nullptr}; SDL_Event evt; for(;;) { - world->render(); + // DOOM-style menus + switch(game_state) { + case GameState::MainMenu: + Render::draw_menu(0); + break; + case GameState::Game: + // generate environment when needed + if(world == nullptr) { + world.reset(generator.generate().release()); + } + world->act_if_requested(); + world->render(); + // game end conditions (player or boss death) + if(world->get_player()->health <= 0) { + game_state = GameState::GameOver; + world.reset(); + } else if(world->get_boss()->health <= 0) { + game_state = GameState::Escape; + world.reset(); + } + break; + case GameState::GameOver: + Render::draw_menu(1); + break; + case GameState::Escape: + Render::draw_menu(2); + break; + } Render::present(); if(SDL_WaitEvent(&evt)) { if(evt.type == SDL_QUIT) { - goto main_loop; + goto break_main_loop; } else { Input::process_event(evt); } } - } main_loop: + } break_main_loop: return 0; } diff --git a/src/player_logic.h b/src/player_logic.h index 6ddd106..1f09a44 100644 --- a/src/player_logic.h +++ b/src/player_logic.h @@ -4,7 +4,6 @@ #include "core/character.h" #include "core/input.h" #include "core/roguedefs.h" -#include namespace rogue { class PlayerCharacterLogic : public CharacterLogic {