diff --git a/resources/critte.png b/resources/critte.png new file mode 100644 index 0000000..ab1a661 Binary files /dev/null and b/resources/critte.png differ diff --git a/resources/player.png b/resources/player.png new file mode 100644 index 0000000..408275f Binary files /dev/null and b/resources/player.png differ diff --git a/resources/wall.png b/resources/wall.png index 2187d61..b1d0ddd 100644 Binary files a/resources/wall.png and b/resources/wall.png differ diff --git a/resources/wizard.png b/resources/wizard.png index 4083189..75b923a 100644 Binary files a/resources/wizard.png and b/resources/wizard.png differ diff --git a/src/core/character.h b/src/core/character.h index ef3b955..e60c7db 100644 --- a/src/core/character.h +++ b/src/core/character.h @@ -26,17 +26,19 @@ struct CharacterData { }; struct Character { + Character() = default; Character(Tile location, CharacterLogic *logic, CharacterData stats); void act(); void draw(); bool deal_damage(int damage); - CharacterData const data; - std::shared_ptr<CharacterLogic> logic{nullptr}; + CharacterData data{}; int health{1}; Tile location{0, 0}; World *world{nullptr}; +private: + std::shared_ptr<CharacterLogic> logic{nullptr}; }; struct NullCharacterLogic : public CharacterLogic { diff --git a/src/core/renderer.cpp b/src/core/renderer.cpp index d8c800d..e8a92f8 100644 --- a/src/core/renderer.cpp +++ b/src/core/renderer.cpp @@ -77,14 +77,15 @@ void Render::provide_render_data(RenderData &data) { Render::data = &data; } -void Render::clear(Tile camera_center) { +void Render::clear(Tile camera_center, unsigned fov) { assert_render_initialized(); + Render::camera_width = 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 / camera_width); + Render::tile_screen_width = int(width / Render::camera_width); Render::half_tile_screen_width = Render::tile_screen_width >> 1; Render::camera_center = camera_center; @@ -94,6 +95,10 @@ void Render::clear(Tile camera_center) { void Render::draw(Tile world_space_tile, Sprite sprite) { assert_render_initialized(); + if(abs(world_space_tile.x - Render::camera_center.x) > Render::camera_width / 2 + || abs(world_space_tile.y - Render::camera_center.y) > Render::camera_width / 2) { + return; + } SDL_Rect const rect{ .x = (world_space_tile.x * Render::tile_screen_width) - Render::half_tile_screen_width + Render::camera_offset.x, .y = (world_space_tile.y * Render::tile_screen_width) - Render::half_tile_screen_width + Render::camera_offset.y, diff --git a/src/core/renderer.h b/src/core/renderer.h index e2e287b..0c7a1e8 100644 --- a/src/core/renderer.h +++ b/src/core/renderer.h @@ -29,7 +29,7 @@ private: public: static void provide_render_data(RenderData &data); - static void clear(Tile camera_center); + static void clear(Tile camera_center, unsigned fov = 10); static void draw(Tile world_space_tile, Sprite sprite); static void present(); private: @@ -51,6 +51,7 @@ public: RenderDataSetup &with_sprite(std::filesystem::path sprite_path); RenderData build(); private: + unsigned fov{10}; std::optional<std::filesystem::path> resource_base_path; SDL_Window *window{nullptr}; SDL_Renderer *renderer{nullptr}; diff --git a/src/core/roguedefs.h b/src/core/roguedefs.h index 257798a..336df9e 100644 --- a/src/core/roguedefs.h +++ b/src/core/roguedefs.h @@ -3,12 +3,14 @@ #include "SDL2/SDL_rect.h" -enum Directions : unsigned short { - NORTH = 0x1, - EAST = 0x2, - SOUTH = 0x4, - WEST = 0x8, -}; + +typedef unsigned short Directions; +#define NORTH 0x1u +#define EAST (0x1u << 1) +#define SOUTH (0x1u << 2) +#define WEST (0x1u << 3) + +#define WRAP(M_x, M_min, M_max) (((M_x - M_min) % (M_max - M_min)) + M_min) #define casel(M_switch, M_case)\ case M_case:\ diff --git a/src/core/world.cpp b/src/core/world.cpp index c9e598f..0d26d46 100644 --- a/src/core/world.cpp +++ b/src/core/world.cpp @@ -3,6 +3,7 @@ #include "core/renderer.h" #include "core/roguedefs.h" #include <SDL2/SDL_log.h> +#include <algorithm> #include <cassert> #include <cstdlib> #include <memory> @@ -30,10 +31,10 @@ bool Room::tile_is_wall(Tile local_tile) const { || local_tile.y >= this->rect.y + this->rect.h-1}; int chunk_size2 = chunk_size/2; bool const is_outside_hallways{ - ((this->hallway_paths & Directions::NORTH) == 0 || local_tile.x != chunk_size / 2 || local_tile.y > chunk_size2) - && ((this->hallway_paths & Directions::SOUTH) == 0 || local_tile.x != chunk_size / 2 || local_tile.y < chunk_size2) - && ((this->hallway_paths & Directions::WEST) == 0 || local_tile.y != chunk_size / 2 || local_tile.x < chunk_size2) - && ((this->hallway_paths & Directions::EAST) == 0 || local_tile.y != chunk_size / 2 || local_tile.x > chunk_size2) + ((this->hallway_paths & NORTH) == 0 || local_tile.x != chunk_size / 2 || local_tile.y > chunk_size2) + && ((this->hallway_paths & SOUTH) == 0 || local_tile.x != chunk_size / 2 || local_tile.y < chunk_size2) + && ((this->hallway_paths & WEST) == 0 || local_tile.y != chunk_size / 2 || local_tile.x > chunk_size2) + && ((this->hallway_paths & EAST) == 0 || local_tile.y != chunk_size / 2 || local_tile.x < chunk_size2) }; return is_outside_room && is_outside_hallways; } @@ -47,13 +48,14 @@ World::World() } {} void World::act() { + player.act(); for(Character &character : this->characters) { character.act(); } } void World::render() { - Render::clear(this->player->location); + Render::clear(this->player.location, 15); for(size_t i{0}; i < this->rooms.size(); ++i) { this->rooms[i].draw({ .x = int(i % this->shear * this->chunk_size), @@ -63,20 +65,33 @@ void World::render() { for(Character &character : this->characters) { character.draw(); } + player.draw(); } Room &World::get_room(Chunk chunk) { return rooms[chunk.x + chunk.y * this->shear]; } +Character *World::get_player() { + return &this->player; +} + +Chunk World::get_chunk(Tile tile) { + return {tile.x / this->chunk_size, tile.y / this->chunk_size}; +} + TileData World::query_tile(Tile tile) { Room &room{this->get_room({tile.x / this->chunk_size, tile.y / this->chunk_size})}; TileData out{ .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; + return out; + } for(Character &character : this->characters) { - if(character.location == tile) { + if(character.location == tile && character.health > 0) { out.character = &character; return out; } @@ -118,6 +133,16 @@ WorldGenerator &WorldGenerator::with_player(Character character) { return *this; } +WorldGenerator &WorldGenerator::with_enemy(std::function<Character(Tile)> spawner, int inv_frequency) { + this->enemy_spawners.push_back({spawner, inv_frequency}); + return *this; +} + +WorldGenerator &WorldGenerator::with_connecting_chance(unsigned num) { + this->connecting_chance = num; + return *this; +} + std::unique_ptr<World> WorldGenerator::generate() { assert(this->chunk_side_length > 0 && "Chunk size is required to generate world"); assert(this->min_room_size > 0 && "Room size is required to generate world"); @@ -126,28 +151,12 @@ std::unique_ptr<World> WorldGenerator::generate() { assert(this->player.has_value() && "World cannot be generated without a player"); std::unique_ptr<World> world{new World()}; - Chunk start_room{this->select_start()}; + Chunk start_room{this->world_side_length / 2, this->world_side_length / 2}; this->setup_player(world, start_room); + this->generate_world(world, start_room); return world; } -Chunk WorldGenerator::select_start() const { - Chunk start{0, 0}; - switch(std::rand() % 4) { - case 0: - start.y = this->world_side_length - 1; - case 1: - start.x = std::rand() % this->world_side_length; - break; - case 2: - start.x = this->world_side_length - 1; - case 3: - start.y = std::rand() % this->world_side_length; - break; - } - return start; -} - void WorldGenerator::generate_world(std::unique_ptr<World> &world, Chunk start) { world->chunk_size = this->chunk_side_length; world->shear = this->world_side_length; @@ -157,48 +166,85 @@ void WorldGenerator::generate_world(std::unique_ptr<World> &world, Chunk start) .hallway_paths = Directions(0x0), .chunk_size = this->chunk_side_length, .rect = { - (this->chunk_side_length - this->max_room_size) / 2, - (this->chunk_side_length - this->max_room_size) / 2, - this->max_room_size, - this->max_room_size + 0, + 0, + this->chunk_side_length, + this->chunk_side_length } }; } + world->get_room(start).rect = SDL_Rect { + .x = this->chunk_side_length/2-2, + .y = this->chunk_side_length/2-2, + .w = 5, + .h = 5 + }; world->get_room(start).initialized = true; this->generate_room(world, start); } -void WorldGenerator::generate_room(std::unique_ptr<World> &world, Chunk last) { - static const Chunk options[]{ +void WorldGenerator::generate_room(std::unique_ptr<World> &world, Chunk const &last) { + Room &last_room{world->get_room(last)}; + last_room.initialized = true; + // generate a room that encloses center tile of this chunk + last_room.rect.w = WRAP(std::rand(), this->min_room_size, this->max_room_size); + last_room.rect.x = WRAP(std::rand(), + std::max(this->chunk_side_length/2 - last_room.rect.w + 2, 0), + 1 + std::min(this->chunk_side_length - last_room.rect.w - 1, this->chunk_side_length/2)); + last_room.rect.h = WRAP(std::rand(), this->min_room_size, this->max_room_size); + last_room.rect.y = WRAP(std::rand(), + std::max(this->chunk_side_length/2 - last_room.rect.h + 2, 0), + 1 + std::min(this->chunk_side_length - last_room.rect.h - 1, this->chunk_side_length/2)); + const Chunk options[]{ {last.x + 1, last.y}, {last.x - 1, last.y}, {last.x, last.y + 1}, {last.x, last.y - 1} }; - static const Directions directions[]{ - EAST, WEST, SOUTH, NORTH - }; + 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}; for(size_t i{0}; i < 4; ++i) { - Chunk next{options[i]}; - Room &option{world->get_room(next)}; - if(options[i].x < 0 || options[i].x >= this->world_side_length - || options[i].y < 0 || options[i].y >= this->world_side_length) { - continue; - } else if(option.initialized) { + size_t const true_offset{(i + rand_offset) % 4}; + Chunk const &next{options[true_offset]}; + if(next.x < 0 || next.x >= this->world_side_length + || next.y < 0 || next.y >= this->world_side_length) { continue; } - // register hallway with the last chunk - option.hallway_paths = Directions(option.hallway_paths | directions[i]); - // generate random rect that encloses center of chunk - // add hallway to the new chunk + Room &option{world->get_room(next)}; + 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 + option.hallway_paths |= inv_directions[true_offset]; + } + if(option.initialized) { + continue; + } + // generate surrounding rooms + this->generate_room(world, next); + } +} + +void WorldGenerator::add_enemies(std::unique_ptr<World> &world, Chunk const &chunk, Room &room) { + for(std::pair<std::function<Character(Tile)>, 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) + }; + Character character{pair.first(offset)}; + character.world = world.get(); + world->characters.push_back(character); + } } } void WorldGenerator::setup_player(std::unique_ptr<World> &world, Chunk spawn_chunk) { - world->characters.push_back(player.value()); - world->player = &world->characters[0]; - world->player->world = world.get(); - world->player->location = { + world->player = this->player.value(); + world->player.world = world.get(); + world->player.location = { (spawn_chunk.x * this->chunk_side_length) + (this->chunk_side_length / 2), (spawn_chunk.y * this->chunk_side_length) + (this->chunk_side_length / 2), }; diff --git a/src/core/world.h b/src/core/world.h index 1245131..a213722 100644 --- a/src/core/world.h +++ b/src/core/world.h @@ -2,6 +2,7 @@ #define ROGUE_WORLD_H #include <optional> +#include <utility> #include <vector> #include <SDL2/SDL_rect.h> #include "character.h" @@ -34,16 +35,17 @@ struct World { void act(); void render(); Room &get_room(Chunk chunk); + Character *get_player(); + Chunk get_chunk(Tile tile); TileData query_tile(Tile tile); void on_input(bool); private: int chunk_size{1}; unsigned shear{0}; - Character *player{nullptr}; + Character player{}; std::vector<Room> rooms{}; std::vector<Character> characters{}; KeyEventHandler direction_pressed; -private: }; class WorldGenerator { @@ -53,16 +55,20 @@ public: 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_enemy(std::function<Character(Tile)> spawner, int inv_frequency); + WorldGenerator &with_connecting_chance(unsigned num); std::unique_ptr<World> generate(); private: - Chunk select_start() const; void generate_world(std::unique_ptr<World> &world, Chunk start); - void generate_room(std::unique_ptr<World> &world, Chunk last); + void generate_room(std::unique_ptr<World> &world, Chunk const &last); + void add_enemies(std::unique_ptr<World> &world, Chunk const &chunk, Room &room); void setup_player(std::unique_ptr<World> &world, Chunk spawn_chunk); private: std::optional<Character> player; + std::vector<std::pair<std::function<Character(Tile)>, 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}; }; } diff --git a/src/enemies.cpp b/src/enemies.cpp new file mode 100644 index 0000000..1339e4e --- /dev/null +++ b/src/enemies.cpp @@ -0,0 +1,48 @@ +#include "enemies.h" +#include "core/roguedefs.h" +#include "core/world.h" +#include <functional> + +namespace rogue { +Tile CritteLogic::move() { + Tile location{this->character->location}; + Character *player{this->character->world->get_player()}; + if(this->is_aware_of_player) { + // cache query_tile call for repeated use + std::function<TileData(Tile)> query_tile{std::bind(&World::query_tile, this->character->world, std::placeholders::_1)}; + Tile difference{ + player->location.x - location.x, + player->location.y - location.y + }; + Tile abs{std::abs(difference.x), std::abs(difference.y)}; + Tile direction{ + difference.x == 0 ? 0 : difference.x / std::abs(difference.x), + difference.y == 0 ? 0 : difference.y / std::abs(difference.y) + }; + Tile x_motion{location.x + direction.x, location.y}; + Tile y_motion{location.x, location.y + direction.y}; + if(abs.x > abs.y) { + if(!query_tile(x_motion).is_wall) + return x_motion; + else if(!query_tile(y_motion).is_wall) { + return y_motion; + } + } else { + if(!query_tile(y_motion).is_wall) { + return y_motion; + } else if(!query_tile(x_motion).is_wall) { + return x_motion; + } + } + } else { + // cache get_chunk call for easier reading + std::function<Chunk(Tile)> 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); + } + // fallback return value, for if there is no desirable tile to move to + return { + .x = location.x, + .y = location.y + }; +} +} diff --git a/src/enemies.h b/src/enemies.h new file mode 100644 index 0000000..7e97d96 --- /dev/null +++ b/src/enemies.h @@ -0,0 +1,13 @@ +#ifndef ENEMIES_H +#define ENEMIES_H + +#include "core/character.h" + +namespace rogue { +class CritteLogic : public CharacterLogic { + virtual Tile move() override; + bool is_aware_of_player{false}; +}; +} + +#endif // !ENEMIES_H diff --git a/src/main.cpp b/src/main.cpp index dd5a620..c6137aa 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,23 +4,36 @@ #include "core/input.h" #include "core/renderer.h" #include "core/world.h" +#include "enemies.h" #include "player_logic.h" using namespace rogue; +CharacterData const player_data{ + .health = 20, + .damage = 2, + .sprite = 3 +}; + +CharacterData const critte_data{ + .health = 5, + .damage = 1, + .sprite = 2 +}; + +template <typename TLogicType> +Character create_character(CharacterData const &stats, Tile tile) { + return Character(tile, new TLogicType, stats); +} + int main(int argc, char *argv[]) { std::unique_ptr<World> world{WorldGenerator() - .with_world_size(5) - .with_chunk_size(9) - .with_room_size(2, 5) - .with_player(Character({9/2, 9/2}, - new PlayerCharacterLogic(), - CharacterData { - .health = 1, - .damage = 1, - .sprite = 0 - } - )) + .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)) + .with_enemy(std::bind(&create_character<CritteLogic>, critte_data, std::placeholders::_1), 2) .generate() }; RenderData data{RenderDataSetup() @@ -29,6 +42,8 @@ int main(int argc, char *argv[]) { .with_resource_path("resources/") .with_sprite("wizard.png") .with_sprite("wall.png") + .with_sprite("critte.png") + .with_sprite("player.png") .build() }; Render::provide_render_data(data);