feat: world generation and initial work for enemy spawning

This commit is contained in:
Sara 2025-06-05 22:44:46 +02:00
parent 1f6be6b92b
commit 9b332a09a9
13 changed files with 211 additions and 73 deletions

BIN
resources/critte.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 B

BIN
resources/player.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 386 B

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 380 B

View file

@ -26,17 +26,19 @@ struct CharacterData {
}; };
struct Character { struct Character {
Character() = default;
Character(Tile location, CharacterLogic *logic, CharacterData stats); Character(Tile location, CharacterLogic *logic, CharacterData stats);
void act(); void act();
void draw(); void draw();
bool deal_damage(int damage); bool deal_damage(int damage);
CharacterData const data; CharacterData data{};
std::shared_ptr<CharacterLogic> logic{nullptr};
int health{1}; int health{1};
Tile location{0, 0}; Tile location{0, 0};
World *world{nullptr}; World *world{nullptr};
private:
std::shared_ptr<CharacterLogic> logic{nullptr};
}; };
struct NullCharacterLogic : public CharacterLogic { struct NullCharacterLogic : public CharacterLogic {

View file

@ -77,14 +77,15 @@ void Render::provide_render_data(RenderData &data) {
Render::data = &data; Render::data = &data;
} }
void Render::clear(Tile camera_center) { void Render::clear(Tile camera_center, unsigned fov) {
assert_render_initialized(); assert_render_initialized();
Render::camera_width = fov;
SDL_SetRenderDrawColor(Render::data->renderer, 0u, 0u, 0u, 255u); SDL_SetRenderDrawColor(Render::data->renderer, 0u, 0u, 0u, 255u);
SDL_RenderClear(Render::data->renderer); SDL_RenderClear(Render::data->renderer);
int width, height; int width, height;
SDL_GetWindowSize(Render::data->window, &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::half_tile_screen_width = Render::tile_screen_width >> 1;
Render::camera_center = camera_center; Render::camera_center = camera_center;
@ -94,6 +95,10 @@ void Render::clear(Tile camera_center) {
void Render::draw(Tile world_space_tile, Sprite sprite) { void Render::draw(Tile world_space_tile, Sprite sprite) {
assert_render_initialized(); 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{ SDL_Rect const rect{
.x = (world_space_tile.x * Render::tile_screen_width) - Render::half_tile_screen_width + Render::camera_offset.x, .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, .y = (world_space_tile.y * Render::tile_screen_width) - Render::half_tile_screen_width + Render::camera_offset.y,

View file

@ -29,7 +29,7 @@ private:
public: public:
static void provide_render_data(RenderData &data); 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 draw(Tile world_space_tile, Sprite sprite);
static void present(); static void present();
private: private:
@ -51,6 +51,7 @@ public:
RenderDataSetup &with_sprite(std::filesystem::path sprite_path); RenderDataSetup &with_sprite(std::filesystem::path sprite_path);
RenderData build(); RenderData build();
private: private:
unsigned fov{10};
std::optional<std::filesystem::path> resource_base_path; std::optional<std::filesystem::path> resource_base_path;
SDL_Window *window{nullptr}; SDL_Window *window{nullptr};
SDL_Renderer *renderer{nullptr}; SDL_Renderer *renderer{nullptr};

View file

@ -3,12 +3,14 @@
#include "SDL2/SDL_rect.h" #include "SDL2/SDL_rect.h"
enum Directions : unsigned short {
NORTH = 0x1, typedef unsigned short Directions;
EAST = 0x2, #define NORTH 0x1u
SOUTH = 0x4, #define EAST (0x1u << 1)
WEST = 0x8, #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)\ #define casel(M_switch, M_case)\
case M_case:\ case M_case:\

View file

@ -3,6 +3,7 @@
#include "core/renderer.h" #include "core/renderer.h"
#include "core/roguedefs.h" #include "core/roguedefs.h"
#include <SDL2/SDL_log.h> #include <SDL2/SDL_log.h>
#include <algorithm>
#include <cassert> #include <cassert>
#include <cstdlib> #include <cstdlib>
#include <memory> #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}; || local_tile.y >= this->rect.y + this->rect.h-1};
int chunk_size2 = chunk_size/2; int chunk_size2 = chunk_size/2;
bool const is_outside_hallways{ 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 & 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 & 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 & 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 & EAST) == 0 || local_tile.y != chunk_size / 2 || local_tile.x < chunk_size2)
}; };
return is_outside_room && is_outside_hallways; return is_outside_room && is_outside_hallways;
} }
@ -47,13 +48,14 @@ World::World()
} {} } {}
void World::act() { void World::act() {
player.act();
for(Character &character : this->characters) { for(Character &character : this->characters) {
character.act(); character.act();
} }
} }
void World::render() { void World::render() {
Render::clear(this->player->location); Render::clear(this->player.location, 15);
for(size_t i{0}; i < this->rooms.size(); ++i) { for(size_t i{0}; i < this->rooms.size(); ++i) {
this->rooms[i].draw({ this->rooms[i].draw({
.x = int(i % this->shear * this->chunk_size), .x = int(i % this->shear * this->chunk_size),
@ -63,20 +65,33 @@ void World::render() {
for(Character &character : this->characters) { for(Character &character : this->characters) {
character.draw(); character.draw();
} }
player.draw();
} }
Room &World::get_room(Chunk chunk) { Room &World::get_room(Chunk chunk) {
return rooms[chunk.x + chunk.y * this->shear]; 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) { TileData World::query_tile(Tile tile) {
Room &room{this->get_room({tile.x / this->chunk_size, tile.y / this->chunk_size})}; Room &room{this->get_room({tile.x / this->chunk_size, tile.y / this->chunk_size})};
TileData out{ TileData out{
.character = nullptr, .character = nullptr,
.is_wall = room.tile_is_wall({tile.x % this->chunk_size, tile.y % this->chunk_size}) .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) { for(Character &character : this->characters) {
if(character.location == tile) { if(character.location == tile && character.health > 0) {
out.character = &character; out.character = &character;
return out; return out;
} }
@ -118,6 +133,16 @@ WorldGenerator &WorldGenerator::with_player(Character character) {
return *this; 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() { std::unique_ptr<World> WorldGenerator::generate() {
assert(this->chunk_side_length > 0 && "Chunk size is required to generate world"); 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"); 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"); assert(this->player.has_value() && "World cannot be generated without a player");
std::unique_ptr<World> world{new World()}; 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->setup_player(world, start_room);
this->generate_world(world, start_room);
return world; 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) { void WorldGenerator::generate_world(std::unique_ptr<World> &world, Chunk start) {
world->chunk_size = this->chunk_side_length; world->chunk_size = this->chunk_side_length;
world->shear = this->world_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), .hallway_paths = Directions(0x0),
.chunk_size = this->chunk_side_length, .chunk_size = this->chunk_side_length,
.rect = { .rect = {
(this->chunk_side_length - this->max_room_size) / 2, 0,
(this->chunk_side_length - this->max_room_size) / 2, 0,
this->max_room_size, this->chunk_side_length,
this->max_room_size 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; world->get_room(start).initialized = true;
this->generate_room(world, start); this->generate_room(world, start);
} }
void WorldGenerator::generate_room(std::unique_ptr<World> &world, Chunk last) { void WorldGenerator::generate_room(std::unique_ptr<World> &world, Chunk const &last) {
static const Chunk options[]{ 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 - 1, last.y}, {last.x - 1, last.y},
{last.x, last.y + 1}, {last.x, last.y + 1},
{last.x, last.y - 1} {last.x, last.y - 1}
}; };
static const Directions directions[]{ this->add_enemies(world, last, last_room);
EAST, WEST, SOUTH, NORTH 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) { for(size_t i{0}; i < 4; ++i) {
Chunk next{options[i]}; size_t const true_offset{(i + rand_offset) % 4};
Room &option{world->get_room(next)}; Chunk const &next{options[true_offset]};
if(options[i].x < 0 || options[i].x >= this->world_side_length if(next.x < 0 || next.x >= this->world_side_length
|| options[i].y < 0 || options[i].y >= this->world_side_length) { || next.y < 0 || next.y >= this->world_side_length) {
continue;
} else if(option.initialized) {
continue; continue;
} }
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 // register hallway with the last chunk
option.hallway_paths = Directions(option.hallway_paths | directions[i]); last_room.hallway_paths |= directions[true_offset];
// generate random rect that encloses center of chunk // add returning hallway from the new chunk
// add hallway to 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) { void WorldGenerator::setup_player(std::unique_ptr<World> &world, Chunk spawn_chunk) {
world->characters.push_back(player.value()); world->player = this->player.value();
world->player = &world->characters[0]; world->player.world = world.get();
world->player->world = world.get(); world->player.location = {
world->player->location = {
(spawn_chunk.x * this->chunk_side_length) + (this->chunk_side_length / 2), (spawn_chunk.x * this->chunk_side_length) + (this->chunk_side_length / 2),
(spawn_chunk.y * this->chunk_side_length) + (this->chunk_side_length / 2), (spawn_chunk.y * this->chunk_side_length) + (this->chunk_side_length / 2),
}; };

View file

@ -2,6 +2,7 @@
#define ROGUE_WORLD_H #define ROGUE_WORLD_H
#include <optional> #include <optional>
#include <utility>
#include <vector> #include <vector>
#include <SDL2/SDL_rect.h> #include <SDL2/SDL_rect.h>
#include "character.h" #include "character.h"
@ -34,16 +35,17 @@ struct World {
void act(); void act();
void render(); void render();
Room &get_room(Chunk chunk); Room &get_room(Chunk chunk);
Character *get_player();
Chunk get_chunk(Tile tile);
TileData query_tile(Tile tile); TileData query_tile(Tile tile);
void on_input(bool); void on_input(bool);
private: private:
int chunk_size{1}; int chunk_size{1};
unsigned shear{0}; unsigned shear{0};
Character *player{nullptr}; Character player{};
std::vector<Room> rooms{}; std::vector<Room> rooms{};
std::vector<Character> characters{}; std::vector<Character> characters{};
KeyEventHandler direction_pressed; KeyEventHandler direction_pressed;
private:
}; };
class WorldGenerator { class WorldGenerator {
@ -53,16 +55,20 @@ public:
WorldGenerator &with_world_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_room_size(unsigned min_side_length, unsigned max_side_length);
WorldGenerator &with_player(Character character); 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(); std::unique_ptr<World> generate();
private: private:
Chunk select_start() const;
void generate_world(std::unique_ptr<World> &world, Chunk start); 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); void setup_player(std::unique_ptr<World> &world, Chunk spawn_chunk);
private: private:
std::optional<Character> player; 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 chunk_side_length{0}, world_side_length{0};
int max_room_size{0}, min_room_size{0}; int max_room_size{0}, min_room_size{0};
unsigned connecting_chance{1};
}; };
} }

48
src/enemies.cpp Normal file
View file

@ -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
};
}
}

13
src/enemies.h Normal file
View file

@ -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

View file

@ -4,23 +4,36 @@
#include "core/input.h" #include "core/input.h"
#include "core/renderer.h" #include "core/renderer.h"
#include "core/world.h" #include "core/world.h"
#include "enemies.h"
#include "player_logic.h" #include "player_logic.h"
using namespace rogue; 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[]) { int main(int argc, char *argv[]) {
std::unique_ptr<World> world{WorldGenerator() std::unique_ptr<World> world{WorldGenerator()
.with_world_size(5) .with_world_size(20)
.with_chunk_size(9) .with_chunk_size(10)
.with_room_size(2, 5) .with_room_size(6, 10)
.with_player(Character({9/2, 9/2}, .with_connecting_chance(5)
new PlayerCharacterLogic(), .with_player(Character({0,0}, new PlayerCharacterLogic(), player_data))
CharacterData { .with_enemy(std::bind(&create_character<CritteLogic>, critte_data, std::placeholders::_1), 2)
.health = 1,
.damage = 1,
.sprite = 0
}
))
.generate() .generate()
}; };
RenderData data{RenderDataSetup() RenderData data{RenderDataSetup()
@ -29,6 +42,8 @@ int main(int argc, char *argv[]) {
.with_resource_path("resources/") .with_resource_path("resources/")
.with_sprite("wizard.png") .with_sprite("wizard.png")
.with_sprite("wall.png") .with_sprite("wall.png")
.with_sprite("critte.png")
.with_sprite("player.png")
.build() .build()
}; };
Render::provide_render_data(data); Render::provide_render_data(data);