diff --git a/assets/config.json b/assets/config.json index de2ddf1..dc3cb6f 100644 --- a/assets/config.json +++ b/assets/config.json @@ -1,5 +1,8 @@ { "textures": [ + "assets/wall_simple-256.png", + "assets/wall_with_vines-256.png", + "assets/wall_with_pillars-256.png", "assets/wall_texture_test-256.png", "assets/floor_tile_test-256.png", "assets/ceiling_test-256.png", @@ -13,5 +16,15 @@ "peasant_girl": "assets/undead_peasant-256.png", "floor": "assets/floor_tile_test-256.png", "ceiling": "assets/ceiling_test-256.png" + }, + "enemy": { + "HEARING_DISTANCE": 8 + }, + "player": { + }, + "worldgen": { + "enemy_probability": 20, + "empty_room_probability": 10, + "device_probability": 30 } } diff --git a/assets/devices.json b/assets/devices.json new file mode 100644 index 0000000..4356d7c --- /dev/null +++ b/assets/devices.json @@ -0,0 +1,47 @@ +{ + "STAIRS_DOWN": { + "id": "STAIRS_DOWN", + "name": "Stairs Down", + "placement": "fixed", + "foreground": [24, 205, 189], + "background": [24, 205, 189], + "description": "Stairs that go down further into the dungeon.", + "inventory_count": 0, + "randomized": false, + "components": [ + {"type": "Tile", "config": {"chr": "\u2ac5"}}, + {"type": "Device", + "config": {"test": true}, "events": ["Events::GUI::STAIRS_DOWN"] + } + ] + }, + "STAIRS_UP": { + "id": "STAIRS_UP", + "name": "Stairs Up", + "foreground": [24, 205, 189], + "background": [24, 205, 189], + "description": "Stairs that go up, for the weak.", + "inventory_count": 0, + "placement": "fixed", + "components": [ + {"type": "Tile", "config": {"chr": "\u2259"}}, + {"type": "Device", + "config": {"test": true}, "events": ["Events::GUI::STAIRS_UP"] + } + ] + }, + "SPIKE_TRAP": { + "id": "SPIKE_TRAP", + "name": "Spike trap", + "foreground": [24, 205, 189], + "background": [24, 205, 189], + "description": "Spikes stab you from the floor.", + "inventory_count": 0, + "components": [ + {"type": "Tile", "config": {"chr": "\u1ac7"}}, + {"type": "Device", + "config": {"test": true}, "events": ["Events::GUI::TRAP"] + } + ] + } +} diff --git a/assets/enemies.json b/assets/enemies.json new file mode 100644 index 0000000..a5b8d88 --- /dev/null +++ b/assets/enemies.json @@ -0,0 +1,23 @@ +{ + "PLAYER_TILE": { + "foreground": [255, 200, 125], + "background": [30, 20, 75], + "components": [ + {"type": "Tile", "config": {"chr": "\ua66b"}}, + {"type": "Combat", "config": {"hp": 200, "damage": 15}}, + {"type": "Motion", "config": {"dx": 0, "dy": 0, "random": false}}, + {"type": "LightSource", "config": {"strength": 70, "radius": 2}}, + {"type": "EnemyConfig", "config": {"hearing_distance": 5}} + ] + }, + "EVIL_EYE": { + "foreground": [75, 200, 125], + "background": [30, 20, 75], + "components": [ + {"type": "Tile", "config": {"chr": "\u08ac"}}, + {"type": "Combat", "config": {"hp": 100, "damage": 50}}, + {"type": "Motion", "config": {"dx": 0, "dy": 0, "random": false}}, + {"type": "EnemyConfig", "config": {"hearing_distance": 10}} + ] + } +} diff --git a/assets/items.json b/assets/items.json new file mode 100644 index 0000000..3580cd5 --- /dev/null +++ b/assets/items.json @@ -0,0 +1,75 @@ +{ + "TORCH_BAD": { + "id": "TORCH_BAD", + "name": "Crappy Torch", + "foreground": [24, 120, 189], + "background": [230,120, 120], + "description": "A torch that barely lights the way. You wonder if it'd be better to not see the person who murders you.", + "inventory_count": 1, + "components": [ + {"type": "LightSource", "config": {"strength": 70, "radius": 2.0}}, + {"type": "Tile", "config": {"chr": "\u0f08"}} + ] + }, + "SWORD_RUSTY": { + "id": "SWORD_RUSTY", + "name": "Rusty Junk Sword", + "foreground": [24, 120, 189], + "background": [24, 120, 189], + "description": "A sword left to rot in a deep hole where it acquired a patina of dirt and tetanus. You aren't sure if it's more deadly for you to hold it or for the people you stab with it.", + "inventory_count": 1, + "components": [ + {"type": "Weapon", "config": {"damage": 15}}, + {"type": "Tile", "config": {"chr": "\u1e37"}} + ] + }, + "SWORD_LIGHT_AND_FLAME": { + "id": "SWORD_LIGHT_AND_FLAME", + "name": "Sword of Light and Flame", + "foreground": [24, 205, 210], + "background": [24, 205, 210], + "description": "A sword so powerful, a great man from the Land of The Rising Sun thrust it into the ocean of Nerf to chill its effects.", + "inventory_count": 1, + "components": [ + {"type": "LightSource", "config": {"strength": 70, "radius": 1.8}}, + {"type": "Tile", "config": {"chr": "\u0236"}}, + {"type": "Weapon", "config": {"damage": 30}} + ] + }, + "CHEST_SMALL": { + "id": "CHEST_SMALL", + "name": "Small Chest", + "foreground": [150, 100, 189], + "background": [150, 100, 189], + "description": "A small chest of gold. You wonder who would leave something like this around.", + "components": [ + {"type": "Tile", "config": {"chr": "\uaaea"}}, + {"type": "Loot", "config": {"amount": 10}} + ], + "inventory_count": 1 + }, + "WALL_TORCH": { + "id": "WALL_TORCH", + "name": "Basic Wall Torch", + "foreground": [24, 205, 210], + "background": [24, 205, 210], + "description": "A torch on a wall you can't pick up.", + "inventory_count": 0, + "components": [ + {"type": "Tile", "config": {"chr": "\u077e"}}, + {"type": "LightSource", "config": {"strength": 60, "radius": 1.8}} + ] + }, + "POTION_HEALING_SMALL": { + "id": "POTION_HEALING_SMALL", + "name": "Small Healing Potion", + "foreground": [255, 205, 189], + "background": [255, 205, 189], + "description": "A small healing potion.", + "inventory_count": 1, + "components": [ + {"type": "Tile", "config": {"chr": "\u03eb"}}, + {"type": "Curative", "config": {"hp": 20}} + ] + } +} diff --git a/assets/tiles.json b/assets/tiles.json new file mode 100644 index 0000000..4733c86 --- /dev/null +++ b/assets/tiles.json @@ -0,0 +1,20 @@ +{ + "WALL_TILE": { + "foreground": [230, 20, 30], + "background": [230, 20, 120], + "collision": true, + "display": "\ua5b8" + }, + "FLOOR_TILE": { + "foreground": [40, 15, 125], + "background": [200, 15, 75], + "collision": false, + "display":"\u289e" + }, + "CEILING_TILE": { + "foreground": [159, 164, 15], + "background": [199, 15, 79], + "collision": true, + "display":"\u2274" + } +} diff --git a/assets/wall_simple-256.png b/assets/wall_simple-256.png new file mode 100644 index 0000000..6c76722 Binary files /dev/null and b/assets/wall_simple-256.png differ diff --git a/assets/wall_with_pillars-256.png b/assets/wall_with_pillars-256.png new file mode 100644 index 0000000..7715274 Binary files /dev/null and b/assets/wall_with_pillars-256.png differ diff --git a/assets/wall_with_vines-256.png b/assets/wall_with_vines-256.png new file mode 100644 index 0000000..da524ac Binary files /dev/null and b/assets/wall_with_vines-256.png differ diff --git a/combat.cpp b/combat.cpp new file mode 100644 index 0000000..48d38fb --- /dev/null +++ b/combat.cpp @@ -0,0 +1,16 @@ +#include "combat.hpp" +#include "rand.hpp" + +namespace components { + int Combat::attack(Combat &target) { + int attack = Random::uniform(0,1); + int my_dmg = 0; + + if(attack) { + my_dmg = Random::uniform(1, damage); + target.hp -= my_dmg; + } + + return my_dmg; + } +} diff --git a/combat.hpp b/combat.hpp new file mode 100644 index 0000000..c50f6de --- /dev/null +++ b/combat.hpp @@ -0,0 +1,13 @@ +#pragma once + +namespace components { + struct Combat { + int hp; + int damage; + + /* NOTE: This is used to _mark_ entities as dead, to detect ones that have just died. Don't make attack automatically set it.*/ + bool dead = false; + + int attack(Combat &target); + }; +} diff --git a/components.cpp b/components.cpp new file mode 100644 index 0000000..c5e3893 --- /dev/null +++ b/components.cpp @@ -0,0 +1,37 @@ +#include "components.hpp" + +namespace components { + void configure(DinkyECS::World &world, DinkyECS::Entity entity, json& entity_data) { + for(auto &comp : entity_data["components"]) { + json& config = comp["config"]; + const string comp_type = comp["type"]; + + if(comp_type == "Weapon") { + world.set(entity, {config["damage"]}); + } else if(comp_type == "LightSource") { + world.set(entity, {config["strength"], config["radius"]}); + } else if(comp_type == "Loot") { + world.set(entity, {config["amount"]}); + } else if(comp_type == "Tile") { + world.set(entity, {config["chr"]}); + } else if(comp_type == "EnemyConfig") { + world.set(entity, {config["hearing_distance"]}); + } else if(comp_type == "Combat") { + world.set(entity, {config["hp"], config["damage"]}); + } else if(comp_type == "Curative") { + world.set(entity, {config["hp"]}); + } else if(comp_type == "Motion") { + world.set(entity, {config["dx"], config["dy"], config["random"]}); + } else if(comp_type == "Device") { + Device device{.config=config, .events={}}; + device.configure_events(comp["events"]); + world.set(entity, device); + } else { + dbc::sentinel(fmt::format("ITEM COMPONENT TYPE MISSING: {}", + std::string(comp_type))); + } + + // json config variable dies + } + } +} diff --git a/components.hpp b/components.hpp new file mode 100644 index 0000000..ee0ffae --- /dev/null +++ b/components.hpp @@ -0,0 +1,64 @@ +#pragma once +#include "dinkyecs.hpp" +#include "devices.hpp" +#include "combat.hpp" +#include "inventory.hpp" +#include "tser.hpp" +#include "config.hpp" + +namespace components { + + struct Player { + DinkyECS::Entity entity; + DEFINE_SERIALIZABLE(Player, entity); + }; + + struct Position { + Point location; + DEFINE_SERIALIZABLE(Position, location); + }; + + struct Motion { + int dx; + int dy; + bool random=false; + DEFINE_SERIALIZABLE(Motion, dx, dy); + }; + + struct Loot { + int amount; + DEFINE_SERIALIZABLE(Loot, amount); + }; + + struct Tile { + std::string chr; + DEFINE_SERIALIZABLE(Tile, chr); + }; + + struct GameConfig { + Config game; + Config enemies; + Config items; + Config tiles; + Config devices; + }; + + struct EnemyConfig { + int hearing_distance = 10; + }; + + struct Debug { + bool PATHS=false; + bool LIGHT=false; + }; + + struct Weapon { + int damage = 0; + }; + + struct Curative { + int hp = 10; + }; + + void configure(DinkyECS::World &world, DinkyECS::Entity entity, json& entity_data); +} diff --git a/constants.hpp b/constants.hpp index 3ca14a6..858eefb 100644 --- a/constants.hpp +++ b/constants.hpp @@ -17,3 +17,27 @@ constexpr const bool DEBUG_BUILD=false; #else constexpr const bool DEBUG_BUILD=true; #endif + + +////////// copied from roguish + + +constexpr int INV_WALL = 0; +constexpr int INV_SPACE = 1; +constexpr int WALL_VALUE = 1; +constexpr int SPACE_VALUE = 0; +constexpr int WALL_PATH_LIMIT = 1000; +constexpr int WALL_LIGHT_LEVEL = 3; +constexpr int WORLDBUILD_DIVISION = 4; +constexpr int WORLDBUILD_SHRINK = 2; +constexpr int WORLDBUILD_MAX_PATH = 200; +constexpr int VIDEO_WINDOW_X=1600; +constexpr int VIDEO_WINDOW_Y=900; +constexpr int UI_FONT_SIZE=30; +constexpr int BASE_MAP_FONT_SIZE=90; +constexpr int GAME_MAP_PIXEL_POS = 600; +constexpr int MAX_FONT_SIZE = 140; +constexpr int MIN_FONT_SIZE = 20; +constexpr int STATUS_UI_WIDTH = 40; +constexpr int STATUS_UI_HEIGHT = 30; +constexpr float PERCENT = 0.01f; diff --git a/devices.cpp b/devices.cpp new file mode 100644 index 0000000..1969c20 --- /dev/null +++ b/devices.cpp @@ -0,0 +1,24 @@ +#include "devices.hpp" +#include "events.hpp" +#include "dbc.hpp" + +namespace components { + + /* + * Note: This should go away or at least the event names to + * numbers should probably be automatically created. + */ + void Device::configure_events(json &event_names) { + for(string name : event_names) { + if(name == "Events::GUI::STAIRS_DOWN") { + events.push_back(Events::GUI::STAIRS_DOWN); + } else if(name == "Events::GUI::STAIRS_UP") { + events.push_back(Events::GUI::STAIRS_UP); + } else if(name == "Events::GUI::TRAP") { + events.push_back(Events::GUI::TRAP); + } else { + dbc::sentinel(fmt::format("Unknown device event {}", name)); + } + } + } +} diff --git a/devices.hpp b/devices.hpp new file mode 100644 index 0000000..4df344f --- /dev/null +++ b/devices.hpp @@ -0,0 +1,15 @@ +#pragma once +#include "dinkyecs.hpp" +#include +#include + +namespace components { + using namespace nlohmann; + + struct Device { + json config; + std::vector events; + + void configure_events(json &event_names); + }; +} diff --git a/dinkyecs.hpp b/dinkyecs.hpp new file mode 100644 index 0000000..0d77ccc --- /dev/null +++ b/dinkyecs.hpp @@ -0,0 +1,158 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "tser.hpp" +#include "dbc.hpp" + +namespace DinkyECS { + + typedef unsigned long Entity; + + using EntityMap = std::unordered_map; + + struct Event { + int event = 0; + Entity entity = 0; + std::any data; + }; + + typedef std::queue EventQueue; + + struct World { + unsigned long entity_count = 0; + std::unordered_map $components; + std::unordered_map $facts; + std::unordered_map $events; + std::vector $constants; + + Entity entity() { + return ++entity_count; + } + + void clone_into(DinkyECS::World &to_world) { + to_world.$constants = $constants; + to_world.$facts = $facts; + to_world.entity_count = entity_count; + + for(auto eid : $constants) { + for(const auto &[tid, eid_map] : $components) { + auto& their_map = to_world.$components[tid]; + if(eid_map.contains(eid)) { + their_map.insert_or_assign(eid, eid_map.at(eid)); + } + } + } + } + + void make_constant(DinkyECS::Entity entity) { + $constants.push_back(entity); + } + + template + EntityMap& entity_map_for() { + return $components[std::type_index(typeid(Comp))]; + } + + template + EventQueue& queue_map_for() { + return $events[std::type_index(typeid(Comp))]; + } + + template + void remove(Entity ent) { + EntityMap &map = entity_map_for(); + map.erase(ent); + } + + template + void set_the(Comp val) { + $facts.insert_or_assign(std::type_index(typeid(Comp)), val); + } + + template + Comp &get_the() { + auto comp_id = std::type_index(typeid(Comp)); + dbc::check($facts.contains(comp_id), + fmt::format("!!!! ATTEMPT to access world fact that hasn't been set yet: {}", typeid(Comp).name())); + + // use .at to get std::out_of_range if fact not set + std::any &res = $facts.at(comp_id); + return std::any_cast(res); + } + + template + bool has_the() { + auto comp_id = std::type_index(typeid(Comp)); + return $facts.contains(comp_id); + } + + template + void set(Entity ent, Comp val) { + EntityMap &map = entity_map_for(); + map.insert_or_assign(ent, val); + } + + template + Comp &get(Entity ent) { + EntityMap &map = entity_map_for(); + // use .at for bounds checking + std::any &res = map.at(ent); + return std::any_cast(res); + } + + template + bool has(Entity ent) { + EntityMap &map = entity_map_for(); + return map.contains(ent); + } + + template + void query(std::function cb) { + EntityMap &map = entity_map_for(); + for(auto& [entity, any_comp] : map) { + Comp &res = std::any_cast(any_comp); + cb(entity, res); + } + } + + template + void query(std::function cb) { + EntityMap &map_a = entity_map_for(); + EntityMap &map_b = entity_map_for(); + + for(auto& [entity, any_a] : map_a) { + if(map_b.contains(entity)) { + CompA &res_a = std::any_cast(any_a); + CompB &res_b = get(entity); + cb(entity, res_a, res_b); + } + } + } + + template + void send(Comp event, Entity entity, std::any data) { + EventQueue &queue = queue_map_for(); + queue.push({event, entity, data}); + } + + template + Event recv() { + EventQueue &queue = queue_map_for(); + Event evt = queue.front(); + queue.pop(); + return evt; + } + + template + bool has_event() { + EventQueue &queue = queue_map_for(); + return !queue.empty(); + } + }; +} diff --git a/events.hpp b/events.hpp new file mode 100644 index 0000000..38f0d5e --- /dev/null +++ b/events.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace Events { + enum GUI { + START, COMBAT, LOOT, DEATH, STAIRS_UP, STAIRS_DOWN, TRAP + }; + + struct Combat { + int player_did; + int enemy_did; + }; +} diff --git a/inventory.cpp b/inventory.cpp new file mode 100644 index 0000000..ad5fcd8 --- /dev/null +++ b/inventory.cpp @@ -0,0 +1,42 @@ +#include "inventory.hpp" + + +namespace components { + void Inventory::add(InventoryItem new_item) { + for(auto &slot : items) { + if(new_item.data["id"] == slot.data["id"]) { + slot.count += new_item.count; + return; + } + } + + items.push_back(new_item); + } + + InventoryItem& Inventory::get(size_t at) { + dbc::check(at < items.size(), fmt::format("inventory index {} too big", at)); + return items[at]; + } + + bool Inventory::decrease(size_t at, int count) { + dbc::check(at < items.size(), fmt::format("inventory index {} too big", at)); + auto &slot = items[at]; + slot.count -= count; + return slot.count > 0; + } + + void Inventory::erase_item(size_t at) { + dbc::check(at < items.size(), fmt::format("inventory index {} too big", at)); + items.erase(items.begin() + at); + } + + int Inventory::item_index(std::string id) { + for(size_t i = 0; i < items.size(); i++) { + if(items[i].data["id"] == id) { + return i; + } + } + + return -1; + } +} diff --git a/inventory.hpp b/inventory.hpp new file mode 100644 index 0000000..8ba912b --- /dev/null +++ b/inventory.hpp @@ -0,0 +1,33 @@ +#pragma once +#include "lights.hpp" +#include +#include + + +namespace components { + using namespace nlohmann; + using lighting::LightSource; + + struct InventoryItem { + int count; + json data; + }; + + struct Inventory { + int gold=0; + LightSource light{0, 0}; + std::vector items{}; + + size_t count() { return items.size(); } + + void add(InventoryItem item); + + bool decrease(size_t at, int count); + + InventoryItem& get(size_t at); + + int item_index(std::string id); + + void erase_item(size_t at); + }; +} diff --git a/levelmanager.cpp b/levelmanager.cpp new file mode 100644 index 0000000..9cff359 --- /dev/null +++ b/levelmanager.cpp @@ -0,0 +1,70 @@ +#include "levelmanager.hpp" +#include "worldbuilder.hpp" +#include "constants.hpp" +#include "save.hpp" +#include "systems.hpp" +#include "components.hpp" + +using lighting::LightRender; +using std::shared_ptr, std::make_shared; +using namespace components; + +LevelManager::LevelManager() { + create_level(); +} + +LevelScaling LevelManager::scale_level() { + return { + 30 + (5 * int($current_level)), + 20 + (5 * int($current_level)) + }; +} + +size_t LevelManager::create_level(shared_ptr prev_world) { + auto world = make_shared(); + + if(prev_world != nullptr) { + prev_world->clone_into(*world); + } else { + save::load_configs(*world); + } + + auto scaling = scale_level(); + + auto map = make_shared(scaling.map_width, scaling.map_height); + WorldBuilder builder(*map); + builder.generate(*world); + + size_t index = $levels.size(); + + auto collider = make_shared(); + // not sure if this is still needed + System::init_positions(*world, *collider); + + $levels.emplace_back(index, map, world, + make_shared(map->width(), map->height()), + collider); + + dbc::check(index == $levels.size() - 1, "Level index is not the same as $levels.size() - 1, off by one error"); + return index; +} + +GameLevel &LevelManager::next() { + dbc::check($current_level < $levels.size(), "attempt to get next level when at end"); + $current_level++; + return $levels.at($current_level); +} + +GameLevel &LevelManager::previous() { + dbc::check($current_level > 0, "attempt to go to previous level when at 0"); + $current_level--; + return $levels.at($current_level); +} + +GameLevel &LevelManager::current() { + return $levels.at($current_level); +} + +GameLevel &LevelManager::get(size_t index) { + return $levels.at(index); +} diff --git a/levelmanager.hpp b/levelmanager.hpp new file mode 100644 index 0000000..c8bf0f6 --- /dev/null +++ b/levelmanager.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include "dinkyecs.hpp" +#include "lights.hpp" +#include "map.hpp" +#include +#include +#include "spatialmap.hpp" + +using std::shared_ptr; + +struct GameLevel { + size_t index; + shared_ptr map; + shared_ptr world; + shared_ptr lights; + shared_ptr collision; +}; + +struct LevelScaling { + int map_width=40; + int map_height=50; +}; + +class LevelManager { + public: + std::vector $levels; + size_t $current_level = 0; + + LevelManager(); + + size_t create_level(shared_ptr prev_world = nullptr); + GameLevel &next(); + GameLevel &previous(); + GameLevel ¤t(); + size_t current_index() { return $current_level; } + GameLevel &get(size_t index); + LevelScaling scale_level(); +}; diff --git a/lights.cpp b/lights.cpp new file mode 100644 index 0000000..dbd6cf8 --- /dev/null +++ b/lights.cpp @@ -0,0 +1,62 @@ +#include "lights.hpp" +#include "constants.hpp" +#include + +using std::vector; + +namespace lighting { + void LightRender::render_square_light(LightSource source, Point at, PointList &has_light) { + for(matrix::box it{$lightmap, at.x, at.y, (size_t)floor(source.radius)}; it.next();) { + if($paths.$paths[it.y][it.x] != WALL_PATH_LIMIT) { + $lightmap[it.y][it.x] = light_level(source.strength, it.distance(), it.x, it.y); + has_light.emplace_back(it.x, it.y); + } + } + } + + /* + * NOTE: This really doesn't need to calculate light all the time. It doesn't + * change around the light source until the lightsource is changed, so the + * light levels could be placed in a Matrix inside LightSource, calculated once + * and then simply "applied" to the area where the entity is located. The only + * thing that would need to be calculated each time is the walls. + */ + void LightRender::render_light(LightSource source, Point at) { + Point min, max; + clear_light_target(at); + PointList has_light; + + render_square_light(source, at, has_light); + + for(auto point : has_light) { + for(matrix::compass it{$lightmap, point.x, point.y}; it.next();) { + if($paths.$paths[it.y][it.x] == WALL_PATH_LIMIT) { + $lightmap[it.y][it.x] = light_level(source.strength, 1.5f, point.x, point.y); + } + } + } + } + + int LightRender::light_level(int strength, float distance, size_t x, size_t y) { + int new_level = distance <= 1.0f ? strength : strength / sqrt(distance); + int cur_level = $lightmap[y][x]; + return cur_level < new_level ? new_level : cur_level; + } + + + void LightRender::reset_light() { + matrix::assign($lightmap, lighting::MIN); + } + + void LightRender::clear_light_target(const Point &at) { + $paths.clear_target(at); + } + + void LightRender::set_light_target(const Point &at, int value) { + $paths.set_target(at, value); + } + + void LightRender::path_light(Matrix &walls) { + $paths.compute_paths(walls); + } +} diff --git a/lights.hpp b/lights.hpp new file mode 100644 index 0000000..5faabcc --- /dev/null +++ b/lights.hpp @@ -0,0 +1,46 @@ +#pragma once +#include +#include "dbc.hpp" +#include "point.hpp" +#include +#include "matrix.hpp" +#include "pathing.hpp" + +namespace lighting { + + struct LightSource { + int strength = 0; + float radius = 1.0f; + }; + + const int MIN = 30; + const int MAX = 105; + + class LightRender { + public: + size_t $width; + size_t $height; + Matrix $lightmap; + Pathing $paths; + + LightRender(size_t width, size_t height) : + $width(width), + $height(height), + $lightmap(height, matrix::Row(width, 0)), + $paths(width, height) + {} + + void reset_light(); + void set_light_target(const Point &at, int value=0); + void clear_light_target(const Point &at); + void path_light(Matrix &walls); + void light_box(LightSource source, Point from, Point &min_out, Point &max_out); + int light_level(int level, float distance, size_t x, size_t y); + void render_light(LightSource source, Point at); + void render_square_light(LightSource source, Point at, PointList &has_light); + void render_compass_light(LightSource source, Point at, PointList &has_light); + void render_circle_light(LightSource source, Point at, PointList &has_light); + Matrix &lighting() { return $lightmap; } + Matrix &paths() { return $paths.paths(); } + }; +} diff --git a/main.cpp b/main.cpp index 7a56942..7d9b05e 100644 --- a/main.cpp +++ b/main.cpp @@ -7,15 +7,15 @@ #include "stats.hpp" Matrix MAP{ - {1,1,1,1,1,1,1,1,1}, + {1,1,1,3,2,3,1,1,1}, {1,0,1,0,0,0,0,0,1}, {1,0,1,0,0,1,1,0,1}, - {1,0,0,0,0,0,0,0,1}, - {1,1,0,0,0,0,0,1,1}, - {1,0,0,1,1,1,0,0,1}, + {3,0,0,0,0,0,0,0,3}, + {2,1,0,0,0,0,0,1,2}, + {3,0,0,1,1,1,0,0,3}, {1,0,0,0,0,0,1,1,1}, {1,0,0,0,0,0,0,0,1}, - {1,1,1,1,1,1,1,1,1} + {1,1,1,3,2,3,1,1,1} }; void draw_gui(sf::RenderWindow &window, sf::Text &text, Stats &stats) { diff --git a/map.cpp b/map.cpp new file mode 100644 index 0000000..080d7f9 --- /dev/null +++ b/map.cpp @@ -0,0 +1,235 @@ +#include "map.hpp" +#include "dbc.hpp" +#include "rand.hpp" +#include +#include +#include +#include +#include "matrix.hpp" + +using std::vector, std::pair; +using namespace fmt; + +Map::Map(size_t width, size_t height) : + $width(width), + $height(height), + $tiles(width, height), + $walls(height, matrix::Row(width, SPACE_VALUE)), + $paths(width, height) +{} + +Map::Map(Matrix &walls, Pathing &paths) : + $tiles(matrix::width(walls), matrix::height(walls)), + $walls(walls), + $paths(paths) +{ + $width = matrix::width(walls); + $height = matrix::height(walls); +} + +void Map::make_paths() { + INVARIANT(); + $paths.compute_paths($walls); +} + +bool Map::inmap(size_t x, size_t y) { + return x < $width && y < $height; +} + +void Map::set_target(const Point &at, int value) { + $paths.set_target(at, value); +} + +void Map::clear_target(const Point &at) { + $paths.clear_target(at); +} + +bool Map::place_entity(size_t room_index, Point &out) { + dbc::check(room_index < $rooms.size(), "room_index is out of bounds, not enough rooms"); + + Room &start = $rooms[room_index]; + + for(matrix::rando_rect it{$walls, start.x, start.y, start.width, start.height}; it.next();) { + if(!iswall(it.x, it.y)) { + out.x = it.x; + out.y = it.y; + return true; + } + } + + return false; +} + +bool Map::iswall(size_t x, size_t y) { + return $walls[y][x] == WALL_VALUE; +} + +void Map::dump(int show_x, int show_y) { + matrix::dump("WALLS", walls(), show_x, show_y); + matrix::dump("PATHS", paths(), show_x, show_y); +} + +bool Map::can_move(Point move_to) { + return inmap(move_to.x, move_to.y) && + !iswall(move_to.x, move_to.y); +} + +Point Map::map_to_camera(const Point &loc, const Point &cam_orig) { + return {loc.x - cam_orig.x, loc.y - cam_orig.y}; +} + +Point Map::center_camera(const Point &around, size_t view_x, size_t view_y) { + int high_x = int(width() - view_x); + int high_y = int(height() - view_y); + int center_x = int(around.x - view_x / 2); + int center_y = int(around.y - view_y / 2); + + size_t start_x = high_x > 0 ? std::clamp(center_x, 0, high_x) : 0; + size_t start_y = high_y > 0 ? std::clamp(center_y, 0, high_y) : 0; + + return {start_x, start_y}; +} + +/* + * Finds the next optimal neighbor in the path + * using either a direct or random method. + * + * Both modes will pick a random direction to start + * looking for the next path, then it goes clock-wise + * from there. + * + * In the direct method it will attempt to find + * a path that goes 1 lower in the dijkstra map + * path, and if it can't find that it will go to + * a 0 path (same number). + * + * In random mode it will pick either the next lower + * or the same level depending on what it finds first. + * Since the starting direction is random this will + * give it a semi-random walk that eventually gets to + * the target. + * + * In map generation this makes random paths and carves + * up the space to make rooms more irregular. + * + * When applied to an enemy they will either go straight + * to the player (random=false) or they'll wander around + * drunkenly gradually reaching the player, and dodging + * in and out. + */ +bool Map::neighbors(Point &out, bool random) { + Matrix &paths = $paths.$paths; + bool zero_found = false; + + // just make a list of the four directions + std::array dirs{{ + {out.x,out.y-1}, // north + {out.x+1,out.y}, // east + {out.x,out.y+1}, // south + {out.x-1,out.y} // west + }}; + + // get the current dijkstra number + int cur = paths[out.y][out.x]; + + // pick a random start of directions + // BUG: is uniform inclusive of the dir.size()? + int rand_start = Random::uniform(0, dirs.size()); + + // go through all possible directions + for(size_t i = 0; i < dirs.size(); i++) { + // but start at the random start, effectively randomizing + // which valid direction to go + // BUG: this might be wrong given the above ranom from 0-size + Point dir = dirs[(i + rand_start) % dirs.size()]; + if(!inmap(dir.x, dir.y)) continue; //skip unpathable stuff + int weight = cur - paths[dir.y][dir.x]; + + if(weight == 1) { + // no matter what we follow direct paths + out = dir; + return true; + } else if(random && weight == 0) { + // if random is selected and it's a 0 path take it + out = dir; + return true; + } else if(weight == 0) { + // otherwise keep the last zero path for after + out = dir; + zero_found = true; + } + } + + // if we reach this then either zero was found and + // zero_found is set true, or it wasn't and nothing found + return zero_found; +} + +bool Map::INVARIANT() { + using dbc::check; + + check($walls.size() == height(), "walls wrong height"); + check($walls[0].size() == width(), "walls wrong width"); + check($paths.$width == width(), "in Map paths width don't match map width"); + check($paths.$height == height(), "in Map paths height don't match map height"); + + for(auto room : $rooms) { + check(int(room.x) >= 0 && int(room.y) >= 0, + format("room invalid position {},{}", + room.x, room.y)); + check(int(room.width) > 0 && int(room.height) > 0, + format("room has invalid dims {},{}", + room.width, room.height)); + } + + return true; +} + +void Map::load_tiles() { + $tiles.load($walls); +} + +void Map::expand() { + // adjust width first + for(auto &row : $walls) { + row.insert(row.begin(), WALL_VALUE); + row.push_back(WALL_VALUE); + } + $width = matrix::width($walls); + + // then add two new rows top/bottom of that new width + $walls.insert($walls.begin(), matrix::Row($width, WALL_VALUE)); + $walls.push_back(matrix::Row($width, WALL_VALUE)); + // now we have the new height + $height = matrix::height($walls); + + // reset the pathing and tiles and done + $paths = Pathing($width, $height); + $tiles = TileMap($width, $height); +} + +void Map::add_room(Room &room) { + room.x++; + room.y++; + room.width--; + room.height--; + + if(room.x + room.width >= $width) { + // fix the width + room.x--; + } + + if(room.y + room.height >= $height) { + // fix the height + room.y--; + } + + $rooms.push_back(room); +} + +void Map::invert_space() { + for(matrix::each_cell it{$walls}; it.next();) { + int is_wall = !$walls[it.y][it.x]; + $walls[it.y][it.x] = is_wall; + } +} diff --git a/map.hpp b/map.hpp new file mode 100644 index 0000000..0c69f0a --- /dev/null +++ b/map.hpp @@ -0,0 +1,75 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include "point.hpp" +#include "tser.hpp" +#include "lights.hpp" +#include "pathing.hpp" +#include "matrix.hpp" +#include "constants.hpp" +#include "tilemap.hpp" + +using lighting::LightSource; + +struct Room { + size_t x = 0; + size_t y = 0; + size_t width = 0; + size_t height = 0; + Point entry{(size_t)-1, (size_t)-1}; + Point exit{(size_t)-1, (size_t)-1}; + + DEFINE_SERIALIZABLE(Room, x, y, width, height); +}; + +class Map { +public: + size_t $width; + size_t $height; + TileMap $tiles; + Matrix $walls; + Pathing $paths; + std::vector $rooms; + + Map(size_t width, size_t height); + + Map(Matrix &walls, Pathing &paths); + + Matrix& paths() { return $paths.paths(); } + TileMap& tiles() { return $tiles; } + Matrix& input_map() { return $paths.input(); } + Matrix& walls() { return $walls; } + size_t width() { return $width; } + size_t height() { return $height; } + int distance(Point to) { return $paths.distance(to); } + + Room &room(size_t at) { return $rooms[at]; } + size_t room_count() { return $rooms.size(); } + + bool place_entity(size_t room_index, Point &out); + bool inmap(size_t x, size_t y); + bool iswall(size_t x, size_t y); + bool can_move(Point move_to); + // BUG: this isn't really neighbors anymore. Maybe move? Walk? + bool neighbors(Point &out, bool random=false); + + void make_paths(); + void set_target(const Point &at, int value=0); + void clear_target(const Point &at); + + Point map_to_camera(const Point &loc, const Point &cam_orig); + Point center_camera(const Point &around, size_t view_x, size_t view_y); + + void expand(); + + void dump(int show_x=-1, int show_y=-1); + bool INVARIANT(); + + void load_tiles(); + void add_room(Room &room); + void invert_space(); +}; diff --git a/matrix.cpp b/matrix.cpp index 85301ee..e3f9ed9 100644 --- a/matrix.cpp +++ b/matrix.cpp @@ -3,77 +3,12 @@ #include #include #include +#include "constants.hpp" using namespace fmt; using std::min, std::max; namespace matrix { - - flood::flood(Matrix &mat, Point start, int old_val, int new_val) : - mat(mat), start(start), old_val(old_val), new_val(new_val), - x(start.x), y(start.y), dirs{mat, start.x, start.y} - { - dbc::check(old_val != new_val, "what you doing?"); - current_loc = start; - q.push(start); - } - - bool flood::next() { - if(!q.empty()) { - if(!dirs.next()) { - // box is done reset it - auto current_loc = q.front(); - q.pop(); - - dirs = matrix::compass{mat, current_loc.x, current_loc.y}; - dirs.next(); - } - - // get the next thing - if(mat[dirs.y][dirs.x] <= old_val) { - mat[dirs.y][dirs.x] = new_val; - x = dirs.x; - y = dirs.y; - - q.push({.x=dirs.x, .y=dirs.y}); - } - - return true; - } else { - return false; - } - } - - line::line(Point start, Point end) : - x(start.x), y(start.y), - x1(end.x), y1(end.y) - { - dx = std::abs(x1 - x); - sx = x < x1 ? 1 : -1; - dy = std::abs(y1 - y) * -1; - sy = y < y1 ? 1 : -1; - error = dx + dy; - } - - bool line::next() { - if(x != x1 || y != y1) { - int e2 = 2 * error; - - if(e2 >= dy) { - error = error + dy; - x = x + sx; - } - - if(e2 <= dx) { - error = error + dx; - y = y + sy; - } - return true; - } else { - return false; - } - } - void dump(const std::string &msg, Matrix &map, int show_x, int show_y) { println("----------------- {}", msg); @@ -82,6 +17,8 @@ namespace matrix { if(int(it.x) == show_x && int(it.y) == show_y) { print("{:x}<", cell); + } else if(cell == WALL_PATH_LIMIT) { + print("# "); } else if(cell > 15) { print("* "); } else { diff --git a/matrix.hpp b/matrix.hpp index e07488e..3974043 100644 --- a/matrix.hpp +++ b/matrix.hpp @@ -3,325 +3,49 @@ #include #include #include +#include +#include #include #include "point.hpp" +#include "rand.hpp" +#include "dbc.hpp" +#include "shiterator.hpp" namespace matrix { - using std::vector, std::queue, std::array; - using std::min, std::max, std::floor; + using Row = shiterator::BaseRow; + using Matrix = shiterator::Base; - template - using BaseRow = vector; + using viewport = shiterator::viewport_t; - template - using Base = vector>; + using each_cell = shiterator::each_cell_t; - using Row = vector; - using Matrix = vector; + using each_row = shiterator::each_row_t; + using box = shiterator::box_t; + using compass = shiterator::compass_t; + using circle = shiterator::circle_t; + using rectangle = shiterator::rectangle_t; + using rando_rect = shiterator::rando_rect_t; + using line = shiterator::line; - - /* - * Just a quick thing to reset a matrix to a value. - */ - template - inline void assign(MAT &out, VAL new_value) { - for(auto &row : out) { - row.assign(row.size(), new_value); - } - } - - template - inline bool inbounds(MAT &mat, size_t x, size_t y) { - // since Point.x and Point.y are size_t any negatives are massive - bool res = (y < mat.size()) && (x < mat[0].size()); - return res; - } - - template - inline size_t width(MAT &mat) { - return mat[0].size(); - } - - template - inline size_t height(MAT &mat) { - return mat.size(); - } - - template - inline Base make_base(size_t width, size_t height) { - Base result(height, BaseRow(width)); - return result; - } + void dump(const std::string &msg, Matrix &map, int show_x=-1, int show_y=-1); inline Matrix make(size_t width, size_t height) { - Matrix result(height, Row(width)); - return result; + return shiterator::make(width, height); } - inline size_t next_x(size_t x, size_t width) { - return (x + 1) * ((x + 1) < width); + inline bool inbounds(Matrix &mat, size_t x, size_t y) { + return shiterator::inbounds(mat, x, y); } - inline size_t next_y(size_t x, size_t y) { - return y + (x == 0); + inline size_t width(Matrix &mat) { + return shiterator::width(mat); } - inline bool at_end(size_t y, size_t height) { - return y < height; + inline size_t height(Matrix &mat) { + return shiterator::height(mat); } - inline bool end_row(size_t x, size_t width) { - return x == width - 1; + inline void assign(Matrix &out, int new_value) { + shiterator::assign(out, new_value); } - - void dump(const std::string &msg, Matrix &map, int show_x=-1, int show_y=-1); - - template - struct each_cell_t { - size_t x = ~0; - size_t y = ~0; - size_t width = 0; - size_t height = 0; - - each_cell_t(MAT &mat) - { - height = matrix::height(mat); - width = matrix::width(mat); - } - - bool next() { - x = next_x(x, width); - y = next_y(x, y); - return at_end(y, height); - } - }; - - template - struct viewport_t { - Point start; - // this is the point in the map - size_t x; - size_t y; - // this is the point inside the box, start at 0 - size_t view_x = ~0; - size_t view_y = ~0; - // viewport width/height - size_t width; - size_t height; - - viewport_t(MAT &mat, Point start, int max_x, int max_y) : - start(start), - x(start.x-1), - y(start.y-1) - { - width = std::min(size_t(max_x), matrix::width(mat) - start.x); - height = std::min(size_t(max_y), matrix::height(mat) - start.y); - fmt::println("viewport_t max_x, max_y {},{} vs matrix {},{}, x={}, y={}", - max_x, max_y, matrix::width(mat), matrix::height(mat), x, y); - } - - bool next() { - y = next_y(x, y); - x = next_x(x, width); - view_x = next_x(view_x, width); - view_y = next_y(view_x, view_y); - return at_end(y, height); - } - }; - - using viewport = viewport_t; - - using each_cell = each_cell_t; - - template - struct each_row_t { - size_t x = ~0; - size_t y = ~0; - size_t width = 0; - size_t height = 0; - bool row = false; - - each_row_t(MAT &mat) { - height = matrix::height(mat); - width = matrix::width(mat); - } - - bool next() { - x = next_x(x, width); - y = next_y(x, y); - row = end_row(x, width); - return at_end(y, height); - } - }; - - using each_row = each_row_t; - - template - struct box_t { - size_t from_x; - size_t from_y; - size_t x = 0; // these are set in constructor - size_t y = 0; // again, no fancy ~ trick needed - size_t left = 0; - size_t top = 0; - size_t right = 0; - size_t bottom = 0; - - box_t(MAT &mat, size_t at_x, size_t at_y, size_t size) : - box_t(mat, at_x, at_y, size, size) { - } - - box_t(MAT &mat, size_t at_x, size_t at_y, size_t width, size_t height) : - from_x(at_x), from_y(at_y) - { - size_t h = matrix::height(mat); - size_t w = matrix::width(mat); - - // keeps it from going below zero - // need extra -1 to compensate for the first next() - left = max(from_x, width) - width; - x = left - 1; // must be -1 for next() - // keeps it from going above width - right = min(from_x + width + 1, w); - - // same for these two - top = max(from_y, height) - height; - y = top - (left == 0); - bottom = min(from_y + height + 1, h); - } - - bool next() { - // calc next but allow to go to 0 for next - x = next_x(x, right); - // x will go to 0, which signals new line - y = next_y(x, y); // this must go here - // if x==0 then this moves it to min_x - x = max(x, left); - // and done - - return at_end(y, bottom); - } - - float distance() { - int dx = from_x - x; - int dy = from_y - y; - - return sqrt((dx * dx) + (dy * dy)); - } - }; - - using box = box_t; - - template - struct compass_t { - size_t x = 0; // these are set in constructor - size_t y = 0; // again, no fancy ~ trick needed - array x_dirs{0, 1, 0, -1}; - array y_dirs{-1, 0, 1, 0}; - size_t max_dirs=0; - size_t dir = ~0; - - compass_t(MAT &mat, size_t x, size_t y) : - x(x), y(y) - { - array x_in{0, 1, 0, -1}; - array y_in{-1, 0, 1, 0}; - - for(size_t i = 0; i < 4; i++) { - int nx = x + x_in[i]; - int ny = y + y_in[i]; - if(matrix::inbounds(mat, nx, ny)) { - x_dirs[max_dirs] = nx; - y_dirs[max_dirs] = ny; - max_dirs++; - } - } - } - - bool next() { - dir++; - if(dir < max_dirs) { - x = x_dirs[dir]; - y = y_dirs[dir]; - return true; - } else { - return false; - } - } - }; - - using compass = compass_t; - - struct flood { - Matrix &mat; - Point start; - int old_val; - int new_val; - queue q; - Point current_loc; - int x; - int y; - matrix::compass dirs; - - flood(Matrix &mat, Point start, int old_val, int new_val); - bool next(); - bool next_working(); - }; - - struct line { - int x; - int y; - int x1; - int y1; - int sx; - int sy; - int dx; - int dy; - int error; - - line(Point start, Point end); - bool next(); - }; - - template - struct circle_t { - float center_x; - float center_y; - float radius = 0.0f; - int y = 0; - int dx = 0; - int dy = 0; - int left = 0; - int right = 0; - int top = 0; - int bottom = 0; - int width = 0; - int height = 0; - - circle_t(MAT &mat, Point center, float radius) : - center_x(center.x), center_y(center.y), radius(radius) - { - width = matrix::width(mat); - height = matrix::height(mat); - top = max(int(floor(center_y - radius)), 0); - bottom = min(int(floor(center_y + radius)), height - 1); - - y = top; - } - - bool next() { - y++; - if(y <= bottom) { - dy = y - center_y; - dx = floor(sqrt(radius * radius - dy * dy)); - left = max(0, int(center_x) - dx); - right = min(width, int(center_x) + dx + 1); - return true; - } else { - return false; - } - } - }; - - using circle = circle_t; } diff --git a/meson.build b/meson.build index d3f62f3..58464ca 100644 --- a/meson.build +++ b/meson.build @@ -5,14 +5,17 @@ project('raycaster', 'cpp', 'cpp_args=-D_GLIBCXX_DEBUG=1 -D_GLIBCXX_DEBUG_PEDANTIC=1', ]) +# use this for common options only for our executables +cpp_args=[] +# these are passed as override_defaults exe_defaults = ['warning_level=2', 'werror=true'] + cc = meson.get_compiler('cpp') -tracy = dependency('tracy', static: true) catch2 = dependency('catch2-with-main') fmt = dependency('fmt', allow_fallback: true) -freetype2 = dependency('freetype2') json = dependency('nlohmann_json') +freetype2 = dependency('freetype2') opengl32 = cc.find_library('opengl32', required: true) winmm = cc.find_library('winmm', required: true) gdi32 = cc.find_library('gdi32', required: true) @@ -28,49 +31,57 @@ sfml_network = dependency('sfml_network') sfml_system = dependency('sfml_system') sfml_window = dependency('sfml_window') -if get_option('tracy_enable') and get_option('buildtype') != 'debugoptimized' - warning('Profiling builds should set --buildtype=debugoptimized') -endif - dependencies = [ fmt, json, opengl32, freetype2, flac, ogg, vorbis, vorbisfile, vorbisenc, winmm, gdi32, sfml_audio, sfml_graphics, sfml_main, sfml_network, sfml_system, - sfml_window, tracy + sfml_window ] -# use this for common options only for our executables -cpp_args=[] - -executable('runtests', [ +sources = [ + 'animator.cpp', + 'combat.cpp', + 'components.cpp', + 'config.cpp', 'dbc.cpp', + 'devices.cpp', + 'inventory.cpp', + 'levelmanager.cpp', + 'lights.cpp', + 'map.cpp', + 'matrix.cpp', 'matrix.cpp', + 'pathing.cpp', + 'rand.cpp', + 'raycaster.cpp', + 'save.cpp', + 'shiterator.hpp', + 'spatialmap.cpp', + 'stats.cpp', + 'systems.cpp', + 'texture.cpp', + 'tilemap.cpp', + 'worldbuilder.cpp', +] + +executable('runtests', sources + [ 'tests/base.cpp', + 'tests/dbc.cpp', + 'tests/inventory.cpp', + 'tests/levelmanager.cpp', + 'tests/lighting.cpp', + 'tests/map.cpp', + 'tests/matrix.cpp', + 'tests/pathing.cpp', + 'tests/spatialmap.cpp', + 'tests/tilemap.cpp', + 'tests/worldbuilder.cpp', ], override_options: exe_defaults, dependencies: dependencies + [catch2]) -executable('zedcaster', [ - 'dbc.cpp', - 'matrix.cpp', - 'config.cpp', - 'texture.cpp', - 'raycaster.cpp', - 'animator.cpp', - 'stats.cpp', - 'main.cpp' - ], +executable('zedcaster', + sources + [ 'main.cpp' ], cpp_args: cpp_args, override_options: exe_defaults, dependencies: dependencies) - -executable('amtcaster', [ - 'dbc.cpp', - 'config.cpp', - 'amt/texture.cpp', - 'amt/raycaster.cpp', - 'amt/main.cpp' - ], - cpp_args: ['-std=c++23'], - override_options: exe_defaults, - dependencies: dependencies) diff --git a/pathing.cpp b/pathing.cpp new file mode 100644 index 0000000..8cc3bba --- /dev/null +++ b/pathing.cpp @@ -0,0 +1,86 @@ +#include "constants.hpp" +#include "pathing.hpp" +#include "dbc.hpp" +#include + +using std::vector; + +inline void add_neighbors(PointList &neighbors, Matrix &closed, size_t y, size_t x) { + for(matrix::box it{closed, x, y, 1}; it.next();) { + if(closed[it.y][it.x] == 0) { + closed[it.y][it.x] = 1; + neighbors.emplace_back(it.x, it.y); + } + } +} + +/* + * Used https://github.com/HenrYxZ/dijkstra-map as a reference. + */ +void Pathing::compute_paths(Matrix &walls) { + INVARIANT(); + dbc::check(walls[0].size() == $width, + fmt::format("Pathing::compute_paths called with walls.width={} but paths $width={}", walls[0].size(), $width)); + + dbc::check(walls.size() == $height, + fmt::format("Pathing::compute_paths called with walls.height={} but paths $height={}", walls[0].size(), $height)); + + // Initialize the new array with every pixel at limit distance + matrix::assign($paths, WALL_PATH_LIMIT); + + Matrix closed = walls; + PointList starting_pixels; + PointList open_pixels; + + // First pass: Add starting pixels and put them in closed + for(size_t counter = 0; counter < $height * $width; counter++) { + size_t x = counter % $width; + size_t y = counter / $width; + if($input[y][x] == 0) { + $paths[y][x] = 0; + closed[y][x] = 1; + starting_pixels.emplace_back(x,y); + } + } + + // Second pass: Add border to open + for(auto sp : starting_pixels) { + add_neighbors(open_pixels, closed, sp.y, sp.x); + } + + // Third pass: Iterate filling in the open list + int counter = 1; // leave this here so it's available below + for(; counter < WALL_PATH_LIMIT && !open_pixels.empty(); ++counter) { + PointList next_open; + for(auto sp : open_pixels) { + $paths[sp.y][sp.x] = counter; + add_neighbors(next_open, closed, sp.y, sp.x); + } + open_pixels = next_open; + } + + // Last pass: flood last pixels + for(auto sp : open_pixels) { + $paths[sp.y][sp.x] = counter; + } +} + +void Pathing::set_target(const Point &at, int value) { + // FUTURE: I'll eventually allow setting this to negatives for priority + $input[at.y][at.x] = value; +} + +void Pathing::clear_target(const Point &at) { + $input[at.y][at.x] = 1; +} + +bool Pathing::INVARIANT() { + using dbc::check; + + check($paths.size() == $height, "paths wrong height"); + check($paths[0].size() == $width, "paths wrong width"); + check($input.size() == $height, "input wrong height"); + check($input[0].size() == $width, "input wrong width"); + + return true; +} diff --git a/pathing.hpp b/pathing.hpp new file mode 100644 index 0000000..4fa71fd --- /dev/null +++ b/pathing.hpp @@ -0,0 +1,30 @@ +#pragma once +#include "point.hpp" +#include "matrix.hpp" +#include + +using matrix::Matrix; + +class Pathing { +public: + size_t $width; + size_t $height; + Matrix $paths; + Matrix $input; + + Pathing(size_t width, size_t height) : + $width(width), + $height(height), + $paths(height, matrix::Row(width, 1)), + $input(height, matrix::Row(width, 1)) + {} + + void compute_paths(Matrix &walls); + void set_target(const Point &at, int value=0); + void clear_target(const Point &at); + Matrix &paths() { return $paths; } + Matrix &input() { return $input; } + int distance(Point to) { return $paths[to.y][to.x];} + + bool INVARIANT(); +}; diff --git a/point.hpp b/point.hpp index 42ed9db..50e57cd 100644 --- a/point.hpp +++ b/point.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include "tser.hpp" struct Point { size_t x = 0; @@ -8,11 +9,13 @@ struct Point { bool operator==(const Point& other) const { return other.x == x && other.y == y; } + + DEFINE_SERIALIZABLE(Point, x, y); }; typedef std::vector PointList; -struct PointHash { +template<> struct std::hash { size_t operator()(const Point& p) const { return std::hash()(p.x) ^ std::hash()(p.y); } diff --git a/rand.cpp b/rand.cpp new file mode 100644 index 0000000..25d77d7 --- /dev/null +++ b/rand.cpp @@ -0,0 +1,6 @@ +#include "rand.hpp" + +namespace Random { + std::random_device RNG; + std::mt19937 GENERATOR(RNG()); +} diff --git a/rand.hpp b/rand.hpp new file mode 100644 index 0000000..359c2b5 --- /dev/null +++ b/rand.hpp @@ -0,0 +1,28 @@ +#pragma once +#include + + +namespace Random { + extern std::mt19937 GENERATOR; + + template + T uniform(T from, T to) { + std::uniform_int_distribution rand(from, to); + + return rand(GENERATOR); + } + + template + T uniform_real(T from, T to) { + std::uniform_real_distribution rand(from, to); + + return rand(GENERATOR); + } + + template + T normal(T from, T to) { + std::normal_distribution rand(from, to); + + return rand(GENERATOR); + } +} diff --git a/save.cpp b/save.cpp new file mode 100644 index 0000000..031c81b --- /dev/null +++ b/save.cpp @@ -0,0 +1,96 @@ +#include "save.hpp" +#include +#include "dbc.hpp" +#include +#include "config.hpp" +#include + +using namespace components; +using namespace fmt; + +template +inline void extract(DinkyECS::World &world, std::map &into) { + auto from_world = world.entity_map_for(); + for(auto [entity, value] : from_world) { + into[entity] = std::any_cast(value); + } +} + +void save::to_file(fs::path path, DinkyECS::World &world, Map &map) { + SaveData save_data; + tser::BinaryArchive archive; + + save_data.facts.player = world.get_the(); + save_data.map = MapData{ + map.$width, map.$height, + map.$rooms, map.$walls}; + + // BUG: lights aren't saved/restored + extract(world, save_data.position); + extract(world, save_data.combat); + extract(world, save_data.motion); + extract(world, save_data.tile); + // extract(world, save_data.inventory); + + archive.save(save_data); + std::string_view archive_view = archive.get_buffer(); + + std::ofstream out(path, std::ios::binary); + out << archive_view; + out.flush(); +} + +template +inline void inject(DinkyECS::World &world, std::map &outof) { + for(auto [entity, value] : outof) { + world.set(entity, value); + } +} + +void save::from_file(fs::path path, DinkyECS::World &world_out, Map &map_out) { + tser::BinaryArchive archive(0); + dbc::check(fs::exists(path), format("save file does not exist {}", path.string())); + auto size = fs::file_size(path); + + if(std::ifstream in_file{path, std::ios::binary}) { + std::string in_data(size, '\0'); + + if(in_file.read(&in_data[0], size)) { + std::string_view in_view(in_data); + archive.initialize(in_view); + } else { + dbc::sentinel(format("wrong size or error reading {}", path.string())); + } + } else { + dbc::sentinel(format("failed to load file {}", path.string())); + } + + auto save_data = archive.load(); + + world_out.set_the(save_data.facts.player); + inject(world_out, save_data.position); + inject(world_out, save_data.combat); + inject(world_out, save_data.motion); + inject(world_out, save_data.tile); + // inject(world_out, save_data.inventory); + + size_t width = save_data.map.width; + size_t height = save_data.map.height; + + Pathing paths(width, height); + map_out = Map(save_data.map.walls, paths); + + save::load_configs(world_out); +} + +void save::load_configs(DinkyECS::World &world) { + Config game("./assets/config.json"); + Config enemies("./assets/enemies.json"); + Config items("./assets/items.json"); + Config tiles("./assets/tiles.json"); + Config devices("./assets/devices.json"); + + world.set_the({ + game, enemies, items, tiles, devices + }); +} diff --git a/save.hpp b/save.hpp new file mode 100644 index 0000000..7f3149a --- /dev/null +++ b/save.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include "components.hpp" +#include "map.hpp" +#include "dinkyecs.hpp" +#include "tser.hpp" +#include +#include +#include + +namespace save { + namespace fs = std::filesystem; + + struct MapData { + size_t width; + size_t height; + std::vector rooms; + Matrix walls; + + DEFINE_SERIALIZABLE(MapData, width, height, rooms, walls); + }; + + struct Facts { + components::Player player; + + DEFINE_SERIALIZABLE(Facts, player); + }; + + struct SaveData { + Facts facts; + MapData map; + + std::map position; + std::map motion; + std::map combat; + std::map tile; + // std::map inventory; + + DEFINE_SERIALIZABLE(SaveData, facts, map, position, motion, combat, tile); + }; + + void to_file(fs::path path, DinkyECS::World &world, Map &map); + void from_file(fs::path path, DinkyECS::World &world_out, Map &map); + void load_configs(DinkyECS::World &world); +} diff --git a/shiterator.hpp b/shiterator.hpp new file mode 100644 index 0000000..434107d --- /dev/null +++ b/shiterator.hpp @@ -0,0 +1,607 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include "point.hpp" +#include "rand.hpp" +#include "dbc.hpp" + +/* + * # What is This Shit? + * + * Announcing the Shape Iterators, or "shiterators" for short. The best shite + * for C++ for-loops since that [one youtube + * video](https://www.youtube.com/watch?v=rX0ItVEVjHc) told everyone to + * recreate SQL databases with structs. You could also say these are Shaw's + * Iterators, but either way they are the _shite_. Or are they shit? You decide. + * Maybe they're "shite"? + * + * A shiterator is a simple generator that converts 2D shapes into a 1D stream + * of x/y coordinates. You give it a matrix, some parameters like start, end, + * etc. and each time you call `next()` you get the next viable x/y coordinate to + * complete the shape. This makes them far superior to _any_ existing for-loop + * technology because shiterators operate _intelligently_ in shapes. Other + * [programming pundits](https://www.youtube.com/watch?v=tD5NrevFtbU) will say + * their 7000 line "easy to maintain" switch statements are better at drawing + * shapes, but they're wrong. My way of making a for-loop do stuff is vastly + * superior because it doesn't use a switch _or_ a virtual function _or_ + * inheritance at all. That means they have to be the _fastest_. Feel free to run + * them 1000 times and bask in the glory of 1 nanosecond difference performance. + * + * It's science and shite. + * + * More importantly, shiterators are simple and easy to use. They're so easy to + * use you _don't even use the 3rd part of the for-loop_. What? You read that right, + * not only have I managed to eliminate _both_ massive horrible to maintain switches, + * and also avoided virtual functions, but I've also _eliminated one entire part + * of the for-loop_. This obviously makes them way faster than other inferior + * three-clause-loop-trash. Just look at this comparison: + * + * ```cpp + * for(it = trash.begin(); it != trash.end(); it++) { + * std::cout << it << std::endl; + * } + * ``` + * + * ```cpp + * for(each_cell it{mat}; it.next();) { + * std::cout << mat[it.y][it.x] << std::endl; + * } + * ``` + * + * Obviously this will outperform _any_ iterator invented in the last 30 years, but the best + * thing about shiterators is their composability and ability to work simultaneously across + * multiple matrices in one loop: + * + * ```cpp + * for(line it{start, end}; it.next();) { + * for(compass neighbor{walls, it.x, it.y}; neighbor.next();) { + * if(walls[neighbor.y][neighbor.x] == 1) { + * wall_update[it.y][it.x] = walls[it.y][it.x] + 10; + * } + * } + * } + * ``` + * + * This code sample (maybe, because I didn't run it) draws a line from + * `start` to `end` then looks at each neighbor on a compass (north, south, east, west) + * at each point to see if it's set to 1. If it is then it copies that cell over to + * another matrix with +10. Why would you need this? Your Wizard just shot a fireball + * down a corridor and you need to see if anything in the path is within 1 square of it. + * + * You _also_ don't even need to use a for-loop. Yes, you can harken back to the old + * days when we did everything RAW inside a Duff's Device between a while-loop for + * that PERFORMANCE because who cares about maintenance? You're a game developer! Tests? + * Don't need a test if it runs fine on Sony Playstation only. Maintenance? You're moving + * on to the next project in two weeks anyway right?! Use that while-loop and a shiterator + * to really help that next guy: + * + * ```cpp + * box it{walls, center_x, center_y, 20}; + * while(it.next()) { + * walls[it.y][it.x] = 1; + * } + * ``` + * + * ## Shiterator "Guarantees" + * + * Just like Rust [guarantees no memory leaks](https://github.com/pop-os/cosmic-comp/issues/1133), + * a shiterator tries to ensure a few things, if it can: + * + * 1. All x/y values will be within the Matrix you give it. The `line` shiterator doesn't though. + * 2. They try to not store anything and only calculate the math necessary to linearlize the shape. + * 3. You can store them and incrementally call next to get the next value. + * 4. You should be able to compose them together on the same Matrix or different matrices of the same dimensions. + * 5. Most of them will only require 1 for-loop, the few that require 2 only do this so you can draw the inside of a shape. `circle` is like this. + * 6. They don't assume any particular classes or require subclassing. As long as the type given enables `mat[y][x]` (row major) access then it'll work. + * 7. The matrix given to a shiterator isn't actually attached to it, so you can use one matrix to setup an iterator, then apply the x/y values to any other matrix of the same dimensions. Great for smart copying and transforming. + * 8. More importantly, shiterators _do not return any values from the matrix_. They only do the math for coordinates and leave it to you to work your matrix. + * + * These shiterators are used all over the game to do map rendering, randomization, drawing, nearly everything that involves a shape. + * + * ## Algorithms I Need + * + * I'm currently looking for a few algorithms, so if you know how to do these let me know: + * + * 1. _Flood fill_ This turns out to be really hard because most algorithms require keeping track of visited cells with a queue, recursion, etc. + * 2. _Random rectangle fill_ I have something that mostly works but it's really only random across each y-axis, then separate y-axes are randomized. + * 3. _Dijkstra Map_ I have a Dijkstra algorithm but it's not in this style yet. Look in `worldbuilder.cpp` for my current implementation. + * 4. _Viewport_ Currently working on this but I need to have a rectangle I can move around as a viewport. + * + * + * ## Usage + * + * Check the `matrix.hpp` for an example if you want to make it more conventient for your own type. + * + * ## Thanks + * + * Special thanks to Amit and hirdrac for their help with the math and for + * giving me the initial idea. hirdrac doesn't want to be held responsible for + * this travesty but he showed me that you can do iteration and _not_ use the + * weird C++ iterators. Amit did a lot to show me how to do these calculations + * without branching. Thanks to you both--and to everyone else--for helping me while I + * stream my development. + * + * ### SERIOUS DISCLAIMER + * + * I am horribly bad at trigonometry and graphics algorithms, so if you've got an idea to improve them + * or find a bug shoot me an email at help@learncodethehardway.com. + */ +namespace shiterator { using std::vector, std::queue, std::array; using + std::min, std::max, std::floor; + + template + using BaseRow = vector; + + template + using Base = vector>; + + template + inline Base make(size_t width, size_t height) { + Base result(height, BaseRow(width)); + return result; + } + + /* + * Just a quick thing to reset a matrix to a value. + */ + template + inline void assign(MAT &out, VAL new_value) { + for(auto &row : out) { + row.assign(row.size(), new_value); + } + } + + + /* + * Tells you if a coordinate is in bounds of the matrix + * and therefore safe to use. + */ + template + inline bool inbounds(MAT &mat, size_t x, size_t y) { + // since Point.x and Point.y are size_t any negatives are massive + return (y < mat.size()) && (x < mat[0].size()); + } + + /* + * Gives the width of a matrix. Assumes row major (y/x) + * and vector API .size(). + */ + template + inline size_t width(MAT &mat) { + return mat[0].size(); + } + + /* + * Same as shiterator::width but just the height. + */ + template + inline size_t height(MAT &mat) { + return mat.size(); + } + + /* + * These are internal calculations that help + * with keeping track of the next x coordinate. + */ + inline size_t next_x(size_t x, size_t width) { + return (x + 1) * ((x + 1) < width); + } + + /* + * Same as next_x but updates the next y coordinate. + * It uses the fact that when x==0 you have a new + * line so increment y. + */ + inline size_t next_y(size_t x, size_t y) { + return y + (x == 0); + } + + /* + * Figures out if you're at the end of the shape, + * which is usually when y > height. + */ + inline bool at_end(size_t y, size_t height) { + return y < height; + } + + /* + * Determines if you're at the end of a row. + */ + inline bool end_row(size_t x, size_t width) { + return x == width - 1; + } + + /* + * Most basic shiterator. It just goes through + * every cell in the matrix in linear order + * with not tracking of anything else. + */ + template + struct each_cell_t { + size_t x = ~0; + size_t y = ~0; + size_t width = 0; + size_t height = 0; + + each_cell_t(MAT &mat) + { + height = shiterator::height(mat); + width = shiterator::width(mat); + } + + bool next() { + x = next_x(x, width); + y = next_y(x, y); + return at_end(y, height); + } + }; + + /* + * This is just each_cell_t but it sets + * a boolean value `bool row` so you can + * tell when you've reached the end of a + * row. This is mostly used for printing + * out a matrix and similar just drawing the + * whole thing with its boundaries. + */ + template + struct each_row_t { + size_t x = ~0; + size_t y = ~0; + size_t width = 0; + size_t height = 0; + bool row = false; + + each_row_t(MAT &mat) { + height = shiterator::height(mat); + width = shiterator::width(mat); + } + + bool next() { + x = next_x(x, width); + y = next_y(x, y); + row = end_row(x, width); + return at_end(y, height); + } + }; + + /* + * This is a CENTERED box, that will create + * a centered rectangle around a point of a + * certain dimension. This kind of needs a + * rewrite but if you want a rectangle from + * a upper corner then use rectangle_t type. + * + * Passing 1 parameter for the size will make + * a square. + */ + template + struct box_t { + size_t from_x; + size_t from_y; + size_t x = 0; // these are set in constructor + size_t y = 0; // again, no fancy ~ trick needed + size_t left = 0; + size_t top = 0; + size_t right = 0; + size_t bottom = 0; + + box_t(MAT &mat, size_t at_x, size_t at_y, size_t size) : + box_t(mat, at_x, at_y, size, size) { + } + + box_t(MAT &mat, size_t at_x, size_t at_y, size_t width, size_t height) : + from_x(at_x), from_y(at_y) + { + size_t h = shiterator::height(mat); + size_t w = shiterator::width(mat); + + // keeps it from going below zero + // need extra -1 to compensate for the first next() + left = max(from_x, width) - width; + x = left - 1; // must be -1 for next() + // keeps it from going above width + right = min(from_x + width + 1, w); + + // same for these two + top = max(from_y, height) - height; + y = top - (left == 0); + bottom = min(from_y + height + 1, h); + } + + bool next() { + // calc next but allow to go to 0 for next + x = next_x(x, right); + // x will go to 0, which signals new line + y = next_y(x, y); // this must go here + // if x==0 then this moves it to min_x + x = max(x, left); + // and done + + return at_end(y, bottom); + } + + /* + * This was useful for doing quick lighting + * calculations, and I might need to implement + * it in other shiterators. It gives the distance + * to the center from the current x/y. + */ + float distance() { + int dx = from_x - x; + int dy = from_y - y; + + return sqrt((dx * dx) + (dy * dy)); + } + }; + + /* + * Stupid simple compass shape North/South/East/West. + * This comes up a _ton_ when doing searching, flood + * algorithms, collision, etc. Probably not the + * fastest way to do it but good enough. + */ + template + struct compass_t { + size_t x = 0; // these are set in constructor + size_t y = 0; // again, no fancy ~ trick needed + array x_dirs{0, 1, 0, -1}; + array y_dirs{-1, 0, 1, 0}; + size_t max_dirs=0; + size_t dir = ~0; + + compass_t(MAT &mat, size_t x, size_t y) : + x(x), y(y) + { + array x_in{0, 1, 0, -1}; + array y_in{-1, 0, 1, 0}; + + for(size_t i = 0; i < 4; i++) { + int nx = x + x_in[i]; + int ny = y + y_in[i]; + if(shiterator::inbounds(mat, nx, ny)) { + x_dirs[max_dirs] = nx; + y_dirs[max_dirs] = ny; + max_dirs++; + } + } + } + + bool next() { + dir++; + if(dir < max_dirs) { + x = x_dirs[dir]; + y = y_dirs[dir]; + return true; + } else { + return false; + } + } + }; + + /* + * Draws a line from start to end using a algorithm from + * https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm + * No idea if the one I picked is best but it's the one + * that works in the shiterator requirements and produced + * good results. + * + * _WARNING_: This one doesn't check if the start/end are + * within your Matrix, as it's assumed _you_ did that + * already. + */ + struct line { + int x; + int y; + int x1; + int y1; + int sx; + int sy; + int dx; + int dy; + int error; + + line(Point start, Point end) : + x(start.x), y(start.y), + x1(end.x), y1(end.y) + { + dx = std::abs(x1 - x); + sx = x < x1 ? 1 : -1; + dy = std::abs(y1 - y) * -1; + sy = y < y1 ? 1 : -1; + error = dx + dy; + } + + bool next() { + if(x != x1 || y != y1) { + int e2 = 2 * error; + + if(e2 >= dy) { + error = error + dy; + x = x + sx; + } + + if(e2 <= dx) { + error = error + dx; + y = y + sy; + } + return true; + } else { + return false; + } + } + }; + + /* + * Draws a simple circle using a fairly naive algorithm + * but one that actually worked. So, so, so, so many + * circle drawing algorithms described online don't work + * or are flat wrong. Even the very best I could find + * did overdrawing of multiple lines or simply got the + * math wrong. Keep in mind, _I_ am bad at this trig math + * so if I'm finding errors in your circle drawing then + * you got problems. + * + * This one is real simple, and works. If you got better + * then take the challenge but be ready to get it wrong. + */ + template + struct circle_t { + float center_x; + float center_y; + float radius = 0.0f; + int y = 0; + int dx = 0; + int dy = 0; + int left = 0; + int right = 0; + int top = 0; + int bottom = 0; + int width = 0; + int height = 0; + + circle_t(MAT &mat, Point center, float radius) : + center_x(center.x), center_y(center.y), radius(radius) + { + width = shiterator::width(mat); + height = shiterator::height(mat); + top = max(int(floor(center_y - radius)), 0); + bottom = min(int(floor(center_y + radius)), height - 1); + + y = top; + } + + bool next() { + y++; + if(y <= bottom) { + dy = y - center_y; + dx = floor(sqrt(radius * radius - dy * dy)); + left = max(0, int(center_x) - dx); + right = min(width, int(center_x) + dx + 1); + return true; + } else { + return false; + } + } + }; + + /* + * Basic rectangle shiterator, and like box and rando_rect_t you can + * pass only 1 parameter for size to do a square. + */ + template + struct rectangle_t { + int x; + int y; + int top; + int left; + int width; + int height; + int right; + int bottom; + + rectangle_t(MAT &mat, size_t start_x, size_t start_y, size_t size) : + rectangle_t(mat, start_x, start_y, size, size) { + } + + rectangle_t(MAT &mat, size_t start_x, size_t start_y, size_t width, size_t height) : + top(start_y), + left(start_x), + width(width), + height(height) + { + size_t h = shiterator::height(mat); + size_t w = shiterator::width(mat); + y = start_y - 1; + x = left - 1; // must be -1 for next() + right = min(start_x + width, w); + + y = start_y; + bottom = min(start_y + height, h); + } + + bool next() { + x = next_x(x, right); + y = next_y(x, y); + x = max(x, left); + return at_end(y, bottom); + } + }; + + /* + * WIP: This one is used to place entities randomly but + * could be used for effects like random destruction of floors. + * It simply "wraps" the rectangle_t but randomizes the x/y values + * using a random starting point. This makes it random across the + * x-axis but only partially random across the y. + */ + template + struct rando_rect_t { + int x; + int y; + int x_offset; + int y_offset; + rectangle_t it; + + rando_rect_t(MAT &mat, size_t start_x, size_t start_y, size_t size) : + rando_rect_t(mat, start_x, start_y, size, size) { + } + + rando_rect_t(MAT &mat, size_t start_x, size_t start_y, size_t width, size_t height) : + it{mat, start_x, start_y, width, height} + { + x_offset = Random::uniform(0, int(width)); + y_offset = Random::uniform(0, int(height)); + } + + bool next() { + bool done = it.next(); + x = it.left + ((it.x + x_offset) % it.width); + y = it.top + ((it.y + y_offset) % it.height); + return done; + } + }; + + /* + * BROKEN: I'm actually not sure what I'm trying to + * do here yet. + */ + template + struct viewport_t { + Point start; + // this is the point in the map + size_t x; + size_t y; + // this is the point inside the box, start at 0 + size_t view_x = ~0; + size_t view_y = ~0; + // viewport width/height + size_t width; + size_t height; + + viewport_t(MAT &mat, Point start, int max_x, int max_y) : + start(start), + x(start.x-1), + y(start.y-1) + { + width = std::min(size_t(max_x), shiterator::width(mat) - start.x); + height = std::min(size_t(max_y), shiterator::height(mat) - start.y); + fmt::println("viewport_t max_x, max_y {},{} vs matrix {},{}, x={}, y={}", + max_x, max_y, shiterator::width(mat), shiterator::height(mat), x, y); + } + + bool next() { + y = next_y(x, y); + x = next_x(x, width); + view_x = next_x(view_x, width); + view_y = next_y(view_x, view_y); + return at_end(y, height); + } + }; + +} diff --git a/spatialmap.cpp b/spatialmap.cpp new file mode 100644 index 0000000..330e839 --- /dev/null +++ b/spatialmap.cpp @@ -0,0 +1,66 @@ +#include "spatialmap.hpp" +#include + +using namespace fmt; + +using DinkyECS::Entity; + +void SpatialMap::insert(Point pos, Entity ent) { + table[pos] = ent; +} + +void SpatialMap::remove(Point pos) { + table.erase(pos); +} + +void SpatialMap::move(Point from, Point to, Entity ent) { + remove(from); + insert(to, ent); +} + +bool SpatialMap::occupied(Point at) const { + return table.contains(at); +} + +Entity SpatialMap::get(Point at) const { + return table.at(at); +} + +/* + * Avoid doing work by using the dy,dx and confirming that + * at.x or at.y is > 0. If either is 0 then there can't be + * a neighbor since that's out of bounds. + */ +inline void find_neighbor(const PointEntityMap &table, EntityList &result, Point at, int dy, int dx) { + // don't bother checking for cells out of bounds + if((dx < 0 && at.x <= 0) || (dy < 0 && at.y <= 0)) { + return; + } + + Point cell = {at.x + dx, at.y + dy}; + + auto it = table.find(cell); + if (it != table.end()) { + result.insert(result.end(), it->second); + } +} + +FoundEntities SpatialMap::neighbors(Point cell, bool diag) const { + EntityList result; + + // just unroll the loop since we only check four directions + // this also solves the problem that it was detecting that the cell was automatically included as a "neighbor" but it's not + find_neighbor(table, result, cell, 0, 1); // north + find_neighbor(table, result, cell, 0, -1); // south + find_neighbor(table, result, cell, 1, 0); // east + find_neighbor(table, result, cell, -1, 0); // west + + if(diag) { + find_neighbor(table, result, cell, 1, -1); // south east + find_neighbor(table, result, cell, -1, -1); // south west + find_neighbor(table, result, cell, 1, 1); // north east + find_neighbor(table, result, cell, -1, 1); // north west + } + + return {!result.empty(), result}; +} diff --git a/spatialmap.hpp b/spatialmap.hpp new file mode 100644 index 0000000..97a6266 --- /dev/null +++ b/spatialmap.hpp @@ -0,0 +1,31 @@ +#pragma once +#include +#include +#include "map.hpp" +#include "dinkyecs.hpp" +#include "point.hpp" + +typedef std::vector EntityList; + +// Point's has is in point.hpp +typedef std::unordered_map PointEntityMap; + +struct FoundEntities { + bool found; + EntityList nearby; +}; + +class SpatialMap { + public: + SpatialMap() {} + + void insert(Point pos, DinkyECS::Entity obj); + void move(Point from, Point to, DinkyECS::Entity ent); + void remove(Point pos); + bool occupied(Point pos) const; + DinkyECS::Entity get(Point at) const; + FoundEntities neighbors(Point position, bool diag=false) const; + + private: + PointEntityMap table; +}; diff --git a/systems.cpp b/systems.cpp new file mode 100644 index 0000000..e65cea4 --- /dev/null +++ b/systems.cpp @@ -0,0 +1,215 @@ +#include "systems.hpp" +#include +#include +#include +#include "rand.hpp" +#include "spatialmap.hpp" +#include "dbc.hpp" +#include "lights.hpp" +#include "events.hpp" + +using std::string; +using namespace fmt; +using namespace components; +using lighting::LightSource; + +void System::lighting(GameLevel &level) { + auto &light = *level.lights; + auto &world = *level.world; + auto &map = *level.map; + + light.reset_light(); + + world.query([&](const auto &ent[[maybe_unused]], auto &position) { + light.set_light_target(position.location); + }); + + light.path_light(map.walls()); + + world.query([&](const auto &ent[[maybe_unused]], auto &position, auto &lightsource) { + light.render_light(lightsource, position.location); + }); +} + +void System::enemy_pathing(GameLevel &level) { + auto &world = *level.world; + auto &map = *level.map; + auto player = world.get_the(); + + // TODO: this will be on each enemy not a global thing + const auto &player_position = world.get(player.entity); + map.set_target(player_position.location); + map.make_paths(); + + world.query([&](const auto &ent, auto &position, auto &motion) { + if(ent != player.entity) { + dbc::check(world.has(ent), "enemy is missing config"); + const auto &config = world.get(ent); + + Point out = position.location; // copy + if(map.distance(out) < config.hearing_distance) { + map.neighbors(out, motion.random); + motion = { int(out.x - position.location.x), int(out.y - position.location.y)}; + } + } + }); + map.clear_target(player_position.location); +} + +void System::init_positions(DinkyECS::World &world, SpatialMap &collider) { + // BUG: instead of separate things maybe just one + // BUG: Collision component that references what is collide + world.query([&](const auto &ent, auto &pos) { + if(world.has(ent)) { + const auto& combat = world.get(ent); + if(!combat.dead) { + collider.insert(pos.location, ent); + } + } else { + collider.insert(pos.location, ent); + } + }); +} + +inline void move_entity(SpatialMap &collider, Map &game_map, Position &position, Motion &motion, DinkyECS::Entity ent) { + Point move_to = { + position.location.x + motion.dx, + position.location.y + motion.dy + }; + motion = {0,0}; // clear it after getting it + + // it's a wall, skip + if(!game_map.can_move(move_to)) return; + // there's collision skip + if(collider.occupied(move_to)) return; + + // all good, do the move + collider.move(position.location, move_to, ent); + position.location = move_to; +} + +void System::motion(GameLevel &level) { + auto &map = *level.map; + auto &world = *level.world; + auto &collider = *level.collision; + + world.query([&](const auto &ent, auto &position, auto &motion) { + // don't process entities that don't move + if(motion.dx != 0 || motion.dy != 0) { + move_entity(collider, map, position, motion, ent); + } + }); +} + +void System::death(GameLevel &level) { + auto &world = *level.world; + auto &collider = *level.collision; + auto player = world.get_the(); + + // BUG: this is where entities can die on top of + // BUG: eachother and overlap their corpse + // BUG: maybe that can be allowed and looting just shows + // BUG: all dead things there? + world.query([&](const auto &ent, auto &position, auto &combat) { + // bring out yer dead + if(combat.hp <= 0 && !combat.dead) { + combat.dead = true; + // take them out of collision map + collider.remove(position.location); + + if(ent == player.entity) { + world.send(Events::GUI::DEATH, ent, {}); + } else { + // remove their motion so they're dead + world.remove(ent); + } + } + }); +} + +void System::collision(GameLevel &level) { + auto &collider = *level.collision; + auto &world = *level.world; + auto player = world.get_the(); + + const auto& player_position = world.get(player.entity); + auto& player_combat = world.get(player.entity); + + // this is guaranteed to not return the given position + auto [found, nearby] = collider.neighbors(player_position.location); + + if(found) { + for(auto entity : nearby) { + if(world.has(entity)) { + auto& enemy_combat = world.get(entity); + + Events::Combat result { + player_combat.attack(enemy_combat), + enemy_combat.attack(player_combat) + }; + + world.send(Events::GUI::COMBAT, entity, result); + } else if(world.has(entity)) { + auto item = world.get(entity); + auto& item_pos = world.get(entity); + auto& inventory = world.get(player.entity); + + if(world.has(entity)) { + inventory.add(item); + auto &new_light = world.get(entity); + world.set(player.entity, new_light); + inventory.light = new_light; + world.remove(entity); + } + + if(world.has(entity)) { + inventory.add(item); + auto &weapon = world.get(entity); + player_combat.damage = weapon.damage; + world.remove(entity); + } + + if(world.has(entity)) { + auto &loot = world.get(entity); + inventory.gold += loot.amount; + world.remove(entity); + } + + if(world.has(entity)) { + auto& cure = world.get(entity); + player_combat.hp += cure.hp; + world.remove(entity); + } + + collider.remove(item_pos.location); + world.remove(entity); + world.remove(entity); + world.send(Events::GUI::LOOT, entity, item); + } else if(world.has(entity)) { + System::device(world, player.entity, entity); + } else { + println("UNKNOWN COLLISION TYPE {}", entity); + } + } + } +} + +void System::pickup(DinkyECS::World &world, DinkyECS::Entity actor, DinkyECS::Entity item) { + dbc::pre("System::pickup actor doesn't have inventory", world.has(actor)); + dbc::pre("System::pickup item isn't configured with InventoryItem.", world.has(item)); + + auto& inventory = world.get(actor); + auto& invitem = world.get(item); + + inventory.add(invitem); +} + +void System::device(DinkyECS::World &world, DinkyECS::Entity actor, DinkyECS::Entity item) { + auto& device = world.get(item); + + for(int event : device.events) { + world.send((Events::GUI)event, actor, device); + } + + println("entity {} INTERACTED WITH DEVICE {}", actor, item); +} diff --git a/systems.hpp b/systems.hpp new file mode 100644 index 0000000..282e439 --- /dev/null +++ b/systems.hpp @@ -0,0 +1,18 @@ +#pragma once +#include "components.hpp" +#include "levelmanager.hpp" + + +namespace System { + using namespace components; + + void lighting(GameLevel &level); + void motion(GameLevel &level); + void collision(GameLevel &level); + void death(GameLevel &level); + void enemy_pathing(GameLevel &level); + + void init_positions(DinkyECS::World &world, SpatialMap &collider); + void pickup(DinkyECS::World &world, DinkyECS::Entity actor, DinkyECS::Entity item); + void device(DinkyECS::World &world, DinkyECS::Entity actor, DinkyECS::Entity item); +} diff --git a/tests/components.cpp b/tests/components.cpp new file mode 100644 index 0000000..095acec --- /dev/null +++ b/tests/components.cpp @@ -0,0 +1,99 @@ +#include +#include "components.hpp" +#include "dinkyecs.hpp" + +using namespace components; +using namespace DinkyECS; + +TEST_CASE("all components can work in the world", "[components]") { + World world; + auto ent1 = world.entity(); + + world.set(ent1, {ent1}); + world.set(ent1, {{10,1}}); + world.set(ent1, {1,0}); + world.set(ent1, {100}); + world.set(ent1, {0}); + world.set(ent1, {"Z"}); + world.set(ent1, {4}); + + auto player = world.get(ent1); + REQUIRE(player.entity == ent1); + + auto position = world.get(ent1); + REQUIRE(position.location.x == 10); + REQUIRE(position.location.y == 1); + + auto motion = world.get(ent1); + REQUIRE(motion.dx == 1); + REQUIRE(motion.dy == 0); + + auto loot = world.get(ent1); + REQUIRE(loot.amount == 100); + + auto inv = world.get(ent1); + REQUIRE(inv.gold == 0); + + auto tile = world.get(ent1); + REQUIRE(tile.chr == "Z"); +} + +TEST_CASE("all components can be facts", "[components]") { + World world; + auto ent1 = world.entity(); + + world.set_the({ent1}); + world.set_the({{10,1}}); + world.set_the({1,0}); + world.set_the({100}); + world.set_the({0}); + world.set_the({"Z"}); + world.set_the({4}); + + auto player = world.get_the(); + REQUIRE(player.entity == ent1); + + auto position = world.get_the(); + REQUIRE(position.location.x == 10); + REQUIRE(position.location.y == 1); + + auto motion = world.get_the(); + REQUIRE(motion.dx == 1); + REQUIRE(motion.dy == 0); + + auto loot = world.get_the(); + REQUIRE(loot.amount == 100); + + auto inv = world.get_the(); + REQUIRE(inv.gold == 0); + + auto tile = world.get_the(); + REQUIRE(tile.chr == "Z"); +} + +TEST_CASE("confirm combat works", "[components]") { + World world; + auto player = world.entity(); + auto enemy = world.entity(); + + world.set(player, {100, 10}); + world.set(enemy, {20, 10}); + + auto p_fight = world.get(player); + REQUIRE(p_fight.hp == 100); + REQUIRE(p_fight.damage == 10); + REQUIRE(p_fight.dead == false); + + auto e_fight = world.get(enemy); + REQUIRE(e_fight.hp == 20); + REQUIRE(e_fight.damage == 10); + REQUIRE(e_fight.dead == false); + + for(int i = 0; e_fight.hp > 0 && i < 100; i++) { + p_fight.attack(e_fight); + } +} + +TEST_CASE("MapConfig loads from JSON", "[components]") { + +} diff --git a/tests/dbc.cpp b/tests/dbc.cpp new file mode 100644 index 0000000..fa45b86 --- /dev/null +++ b/tests/dbc.cpp @@ -0,0 +1,39 @@ +#include +#include "dbc.hpp" + +using namespace dbc; + +TEST_CASE("basic feature tests", "[utils]") { + log("Logging a message."); + + try { + sentinel("This shouldn't happen."); + } catch(SentinelError) { + log("Sentinel happened."); + } + + pre("confirm positive cases work", 1 == 1); + pre("confirm positive lambda", [&]{ return 1 == 1;}); + post("confirm positive post", 1 == 1); + post("confirm postitive post with lamdba", [&]{ return 1 == 1;}); + + check(1 == 1, "one equals 1"); + + try { + check(1 == 2, "this should fail"); + } catch(CheckError err) { + log("check fail worked"); + } + + try { + pre("failing pre", 1 == 3); + } catch(PreCondError err) { + log("pre fail worked"); + } + + try { + post("failing post", 1 == 4); + } catch(PostCondError err) { + log("post faile worked"); + } +} diff --git a/tests/dijkstra.json b/tests/dijkstra.json new file mode 100644 index 0000000..1ee6990 --- /dev/null +++ b/tests/dijkstra.json @@ -0,0 +1,60 @@ +[{ + "input": [ + [1, 1, 1, 0], + [1, 0, 1, 1], + [1, 0, 1, 1], + [1, 1, 1, 1] + ], + "walls": [ + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 1, 0], + [0, 0, 1, 0] + ], + "expected": [ + [1, 1, 1, 0], + [1, 0, 1, 1], + [1, 0, 1000, 2], + [1, 1, 1000, 3] + ] +},{ + "input": [ + [1, 1, 1, 0], + [1, 0, 1, 1], + [1, 0, 1, 1], + [1, 1, 1, 1] + ], + "walls": [ + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 1, 0], + [0, 0, 1, 0] + ], + "expected": [ + [1, 1, 1, 0], + [1, 0, 1, 1], + [1, 0, 1000, 2], + [1, 1, 1000, 3] + ] +}, +{ + "input": [ + [1, 1, 1, 0], + [1, 1, 1, 1], + [1, 0, 1, 1], + [1, 1, 1, 1] + ], + "walls": [ + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 1, 0], + [0, 0, 1, 0] + ], + "expected": [ + [2, 2, 1, 0], + [1, 1, 1, 1], + [1, 0, 1000, 2], + [1, 1, 1000, 3] + ] +} +] diff --git a/tests/inventory.cpp b/tests/inventory.cpp new file mode 100644 index 0000000..a054778 --- /dev/null +++ b/tests/inventory.cpp @@ -0,0 +1,71 @@ +#include +#include +#include +#include "rand.hpp" +#include +#include +#include "components.hpp" +#include "dinkyecs.hpp" +#include "save.hpp" +#include "systems.hpp" + +using namespace nlohmann; +using namespace fmt; +using std::string; +using namespace components; + + +DinkyECS::Entity add_items(DinkyECS::World &world, GameConfig &config) { + auto sword = world.entity(); + json& item_data = config.items["SWORD_RUSTY"]; + world.set(sword, {item_data["inventory_count"], item_data}); + components::configure(world, sword, item_data); + return sword; +} + +TEST_CASE("basic inventory test", "[inventory]") { + DinkyECS::World world; + save::load_configs(world); + auto& config = world.get_the(); + auto sword = add_items(world, config); + + auto player = world.entity(); + world.set(player, {}); + + auto &inventory = world.get(player); + + System::pickup(world, player, sword); + REQUIRE(inventory.count() == 1); + // get the item and confirm there is 1 + auto &item1 = inventory.get(0); + REQUIRE(item1.count == 1); + + int item_at = inventory.item_index("SWORD_RUSTY"); + REQUIRE(item_at == 0); + + REQUIRE(inventory.item_index("SADFASFSADF") == -1); + + System::pickup(world, player, sword); + REQUIRE(item1.count == 2); + + System::pickup(world, player, sword); + REQUIRE(item1.count == 3); + + System::pickup(world, player, sword); + REQUIRE(inventory.count() == 1); + + REQUIRE(item1.count == 4); + + inventory.decrease(0, 1); + REQUIRE(item1.count == 3); + + inventory.decrease(0, 2); + REQUIRE(item1.count == 1); + + bool active = inventory.decrease(0, 1); + REQUIRE(item1.count == 0); + REQUIRE(active == false); + + inventory.erase_item(0); + REQUIRE(inventory.count() == 0); +} diff --git a/tests/levelmanager.cpp b/tests/levelmanager.cpp new file mode 100644 index 0000000..190027e --- /dev/null +++ b/tests/levelmanager.cpp @@ -0,0 +1,40 @@ +#include +#include +#include "map.hpp" +#include "dinkyecs.hpp" +#include "worldbuilder.hpp" +#include "save.hpp" +#include "systems.hpp" +#include "spatialmap.hpp" +#include "levelmanager.hpp" + +using namespace fmt; +using std::string; + +TEST_CASE("basic level manager test", "[levelmanager]") { + LevelManager lm; + + // starts off with one already but I need to change that + size_t level1 = lm.current_index(); + size_t level2 = lm.create_level(); + + auto& test1_level = lm.get(level1); + auto& test2_level = lm.get(level2); + + REQUIRE(test1_level.map->width() > 0); + REQUIRE(test1_level.map->height() > 0); + REQUIRE(test1_level.index == 0); + + REQUIRE(test2_level.map->width() > 0); + REQUIRE(test2_level.map->height() > 0); + REQUIRE(test2_level.index == 1); + + auto& cur_level = lm.current(); + REQUIRE(cur_level.index == 0); + + auto& next_level = lm.next(); + REQUIRE(next_level.index == 1); + + auto& prev_level = lm.previous(); + REQUIRE(prev_level.index == 0); +} diff --git a/tests/lighting.cpp b/tests/lighting.cpp new file mode 100644 index 0000000..92b32b5 --- /dev/null +++ b/tests/lighting.cpp @@ -0,0 +1,45 @@ +#include +#include +#include +#include +#include "map.hpp" +#include "worldbuilder.hpp" +#include "lights.hpp" +#include "point.hpp" + +using namespace lighting; + +TEST_CASE("lighting a map works", "[lighting]") { + Map map(20,23); + WorldBuilder builder(map); + builder.generate_map(); + Point light1, light2; + + REQUIRE(map.place_entity(0, light1)); + REQUIRE(map.place_entity(1, light1)); + + LightSource source1{6, 1.0}; + LightSource source2{4,3}; + + LightRender lr(map.width(), map.height()); + + lr.reset_light(); + + lr.set_light_target(light1); + lr.set_light_target(light2); + + lr.path_light(map.walls()); + + lr.render_light(source1, light1); + lr.render_light(source2, light2); + + lr.clear_light_target(light1); + lr.clear_light_target(light2); + + Matrix &lighting = lr.lighting(); + + matrix::dump("WALLS=====", map.walls(), light1.x, light1.y); + matrix::dump("PATHS=====", lr.paths(), light1.x, light1.y); + matrix::dump("LIGHTING 1", lighting, light1.x, light1.y); + matrix::dump("LIGHTING 2", lighting, light2.x, light2.y); +} diff --git a/tests/map.cpp b/tests/map.cpp new file mode 100644 index 0000000..1dfba9e --- /dev/null +++ b/tests/map.cpp @@ -0,0 +1,83 @@ +#include +#include +#include +#include +#include "map.hpp" +#include "worldbuilder.hpp" + +using namespace fmt; +using namespace nlohmann; +using std::string; + +json load_test_data(const string &fname) { + std::ifstream infile(fname); + return json::parse(infile); +} + +TEST_CASE("camera control", "[map]") { + Map map(20, 20); + WorldBuilder builder(map); + builder.generate_map(); + + Point center = map.center_camera({10,10}, 5, 5); + + REQUIRE(center.x == 8); + REQUIRE(center.y == 8); + + Point translation = map.map_to_camera({10,10}, center); + + REQUIRE(translation.x == 2); + REQUIRE(translation.y == 2); +} + +TEST_CASE("map placement test", "[map:placement]") { + for(int i = 0; i < 50; i++) { + size_t width = Random::uniform(9, 21); + size_t height = Random::uniform(13, 25); + Map map(width, height); + WorldBuilder builder(map); + builder.generate_rooms(); + map.invert_space(); + + for(size_t rnum = 0; rnum < map.room_count(); rnum++) { + Room &room = map.room(rnum); + Point pos; + + REQUIRE(map.place_entity(rnum, pos)); + // matrix::dump("ROOM PLACEMENT TEST", map.walls(), pos.x, pos.y); + + REQUIRE(!map.iswall(pos.x, pos.y)); + REQUIRE(pos.x >= room.x); + REQUIRE(pos.y >= room.y); + REQUIRE(pos.x <= room.x + room.width); + REQUIRE(pos.y <= room.y + room.height); + } + } +} + +TEST_CASE("dijkstra algo test", "[map]") { + json data = load_test_data("./tests/dijkstra.json"); + + for(auto &test : data) { + Matrix expected = test["expected"]; + Matrix input = test["input"]; + Matrix walls = test["walls"]; + Map map(input.size(), input[0].size()); + map.$walls = walls; + map.$paths.$input = input; + + REQUIRE(map.INVARIANT()); + + map.make_paths(); + Matrix &paths = map.paths(); + + if(paths != expected) { + println("ERROR! ------"); + matrix::dump("EXPECTED", expected); + matrix::dump("RESULT", paths); + } + + REQUIRE(map.INVARIANT()); + // FIX ME: REQUIRE(paths == expected); + } +} diff --git a/tests/matrix.cpp b/tests/matrix.cpp new file mode 100644 index 0000000..3b60f66 --- /dev/null +++ b/tests/matrix.cpp @@ -0,0 +1,343 @@ +#include +#include +#include +#include "config.hpp" +#include "matrix.hpp" +#include "rand.hpp" +#include "worldbuilder.hpp" +#include +#include + +using namespace nlohmann; +using namespace fmt; +using std::string; +using matrix::Matrix; + +TEST_CASE("basic matrix iterator", "[matrix:basic]") { + std::ifstream infile("./tests/dijkstra.json"); + json data = json::parse(infile); + auto test = data[0]; + + Matrix walls = test["walls"]; + + // tests going through straight cells but also + // using two iterators on one matrix (or two) + matrix::each_cell cells{walls}; + cells.next(); // kick it off + size_t row_count = 0; + + for(matrix::each_row it{walls}; + it.next(); cells.next()) + { + REQUIRE(walls[cells.y][cells.x] == walls[it.y][it.x]); + row_count += it.row; + } + + REQUIRE(row_count == walls.size()); + + { + // test getting the correct height in the middle + row_count = 0; + matrix::box box{walls, 2,2, 1}; + + while(box.next()) { + row_count += box.x == box.left; + walls[box.y][box.x] = 3; + } + matrix::dump("2,2 WALLS", walls, 2, 2); + + REQUIRE(row_count == 3); + } + + { + matrix::dump("1:1 POINT", walls, 1,1); + // confirm boxes have the right number of rows + // when x goes to 0 on first next call + row_count = 0; + matrix::box box{walls, 1, 1, 1}; + + while(box.next()) { + row_count += box.x == box.left; + } + REQUIRE(row_count == 3); + } + + { + matrix::compass star{walls, 1, 1}; + while(star.next()) { + println("START IS {},{}=={}", star.x, star.y, walls[star.y][star.x]); + walls[star.y][star.x] = 11; + } + matrix::dump("STAR POINT", walls, 1,1); + } +} + +inline void random_matrix(Matrix &out) { + for(size_t y = 0; y < out.size(); y++) { + for(size_t x = 0; x < out[0].size(); x++) { + out[y][x] = Random::uniform(-10,10); + } + } +} + +TEST_CASE("thrash matrix iterators", "[matrix]") { + for(int count = 0; count < Random::uniform(10,30); count++) { + size_t width = Random::uniform(1, 100); + size_t height = Random::uniform(1, 100); + + Matrix test(height, matrix::Row(width)); + random_matrix(test); + + // first make a randomized matrix + matrix::each_cell cells{test}; + cells.next(); // kick off the other iterator + + for(matrix::each_row it{test}; + it.next(); cells.next()) + { + REQUIRE(test[cells.y][cells.x] == test[it.y][it.x]); + } + } +} + +TEST_CASE("thrash box distance iterators", "[matrix:distance]") { + size_t width = Random::uniform(10, 21); + size_t height = Random::uniform(10, 25); + + Matrix result(height, matrix::Row(width)); + matrix::assign(result, 0); + + size_t size = Random::uniform(4, 10); + + Point target{width/2, height/2}; + matrix::box box{result, target.x, target.y, size}; + while(box.next()) { + result[box.y][box.x] = box.distance(); + } + + matrix::dump(format("MAP {}x{} @ {},{}; BOX {}x{}; size: {}", + matrix::width(result), matrix::height(result), + target.x, target.y, box.right - box.left, box.bottom - box.top, size), + result, target.x, target.y); +} + +TEST_CASE("thrash box iterators", "[matrix]") { + for(int count = 0; count < 20; count++) { + size_t width = Random::uniform(1, 25); + size_t height = Random::uniform(1, 33); + + Matrix test(height, matrix::Row(width)); + random_matrix(test); + + // this will be greater than the random_matrix cells + int test_i = Random::uniform(20,30); + + // go through every cell + for(matrix::each_cell target{test}; target.next();) { + PointList result; + // make a random size box + size_t size = Random::uniform(1, 33); + matrix::box box{test, target.x, target.y, size}; + + while(box.next()) { + test[box.y][box.x] = test_i; + result.push_back({box.x, box.y}); + } + + for(auto point : result) { + REQUIRE(test[point.y][point.x] == test_i); + test[point.y][point.x] = 10; // kind of reset it for another try + } + } + } +} + +TEST_CASE("thrash compass iterators", "[matrix:compass]") { + for(int count = 0; count < 20; count++) { + size_t width = Random::uniform(1, 25); + size_t height = Random::uniform(1, 33); + + Matrix test(height, matrix::Row(width)); + random_matrix(test); + + // this will be greater than the random_matrix cells + int test_i = Random::uniform(20,30); + + // go through every cell + for(matrix::each_cell target{test}; target.next();) { + PointList result; + // make a random size box + matrix::compass compass{test, target.x, target.y}; + + while(compass.next()) { + test[compass.y][compass.x] = test_i; + result.push_back({compass.x, compass.y}); + } + + for(auto point : result) { + REQUIRE(test[point.y][point.x] == test_i); + test[point.y][point.x] = 10; // kind of reset it for another try + } + } + } +} + +TEST_CASE("prototype line algorithm", "[matrix:line]") { + size_t width = Random::uniform(10, 12); + size_t height = Random::uniform(10, 15); + Map map(width,height); + // create a target for the paths + Point start{.x=map.width() / 2, .y=map.height()/2}; + + for(matrix::box box{map.walls(), start.x, start.y, 3}; + box.next();) + { + Matrix result = map.walls(); + result[start.y][start.x] = 1; + Point end{.x=box.x, .y=box.y}; + + for(matrix::line it{start, end}; it.next();) + { + REQUIRE(map.inmap(it.x, it.y)); + result[it.y][it.x] = 15; + } + + result[start.y][start.x] = 15; + + // matrix::dump("RESULT AFTER LINE", result, end.x, end.y); + + bool f_found = false; + for(matrix::each_cell it{result}; it.next();) { + if(result[it.y][it.x] == 15) { + f_found = true; + break; + } + } + + REQUIRE(f_found); + } +} + +TEST_CASE("prototype circle algorithm", "[matrix:circle]") { + for(int count = 0; count < 2; count++) { + size_t width = Random::uniform(10, 13); + size_t height = Random::uniform(10, 15); + int pos_mod = Random::uniform(-3,3); + Map map(width,height); + + // create a target for the paths + Point start{.x=map.width() / 2 + pos_mod, .y=map.height()/2 + pos_mod}; + + for(float radius = 1.0f; radius < 4.0f; radius += 0.1f) { + // use an empty map + Matrix result = map.walls(); + + for(matrix::circle it{result, start, radius}; it.next();) { + for(int x = it.left; x < it.right; x++) { + // println("top={}, bottom={}, center.y={}, dy={}, left={}, right={}, x={}, y={}", it.top, it.bottom, it.center.y, it.dy, it.left, it.right, x, it.y); + // println("RESULT {},{}", matrix::width(result), matrix::height(result)); + REQUIRE(it.y >= 0); + REQUIRE(x >= 0); + REQUIRE(it.y < int(matrix::height(result))); + REQUIRE(x < int(matrix::width(result))); + result[it.y][x] += 1; + } + } + + // matrix::dump(format("RESULT AFTER CIRCLE radius {}", radius), result, start.x, start.y); + } + } +} + +TEST_CASE("viewport iterator", "[matrix:viewport]") { + size_t width = Random::uniform(20, 22); + size_t height = Random::uniform(21, 25); + Map map(width,height); + WorldBuilder builder(map); + builder.generate_map(); + + size_t view_width = width/2; + size_t view_height = height/2; + Point player; + REQUIRE(map.place_entity(1, player)); + Point start = map.center_camera(player, view_width, view_height); + + size_t end_x = std::min(view_width, map.width() - start.x); + size_t end_y = std::min(view_height, map.height() - start.y); + + matrix::viewport it{map.walls(), start, int(view_width), int(view_height)}; + + for(size_t y = 0; y < end_y; ++y) { + for(size_t x = 0; x < end_x && it.next(); ++x) { + // still working on this + } + } +} + +TEST_CASE("random rectangle", "[matrix:rando_rect]") { + for(int i = 0; i < 10; i++) { + size_t width = Random::uniform(9, 21); + size_t height = Random::uniform(13, 25); + Map map(width, height); + WorldBuilder builder(map); + builder.generate_rooms(); + map.invert_space(); + auto wall_copy = map.walls(); + + for(size_t rnum = 0; rnum < map.room_count(); rnum++) { + Room &room = map.room(rnum); + Point pos; + + for(matrix::rando_rect it{map.walls(), room.x, room.y, room.width, room.height}; it.next();) + { + if(map.iswall(it.x, it.y)) { + matrix::dump("BAD RECTANGLE SPOT", map.walls(), it.x, it.y); + } + + REQUIRE(!map.iswall(it.x, it.y)); + REQUIRE(size_t(it.x) >= room.x); + REQUIRE(size_t(it.y) >= room.y); + REQUIRE(size_t(it.x) <= room.x + room.width); + REQUIRE(size_t(it.y) <= room.y + room.height); + + wall_copy[it.y][it.x] = wall_copy[it.y][it.x] + 5; + } + } + + // matrix::dump("WALLS FILLED", wall_copy); + } +} + +TEST_CASE("standard rectangle", "[matrix:rectangle]") { + for(int i = 0; i < 20; i++) { + size_t width = Random::uniform(9, 21); + size_t height = Random::uniform(13, 25); + Map map(width, height); + WorldBuilder builder(map); + builder.generate_rooms(); + map.invert_space(); + auto wall_copy = map.walls(); + + for(size_t rnum = 0; rnum < map.room_count(); rnum++) { + Room &room = map.room(rnum); + Point pos; + + for(matrix::rectangle it{map.walls(), room.x, room.y, room.width, room.height}; it.next();) + { + if(map.iswall(it.x, it.y)) { + matrix::dump("BAD RECTANGLE SPOT", map.walls(), it.x, it.y); + } + + REQUIRE(!map.iswall(it.x, it.y)); + REQUIRE(size_t(it.x) >= room.x); + REQUIRE(size_t(it.y) >= room.y); + REQUIRE(size_t(it.x) <= room.x + room.width); + REQUIRE(size_t(it.y) <= room.y + room.height); + + wall_copy[it.y][it.x] = wall_copy[it.y][it.x] + 5; + } + } + + // matrix::dump("WALLS FILLED", wall_copy); + } +} diff --git a/tests/pathing.cpp b/tests/pathing.cpp new file mode 100644 index 0000000..04eb391 --- /dev/null +++ b/tests/pathing.cpp @@ -0,0 +1,51 @@ +#include +#include +#include +#include +#include "pathing.hpp" +#include "matrix.hpp" + +using namespace fmt; +using namespace nlohmann; +using std::string; + +json load_test_pathing(const string &fname) { + std::ifstream infile(fname); + return json::parse(infile); +} + +TEST_CASE("dijkstra algo test", "[pathing]") { + json data = load_test_pathing("./tests/dijkstra.json"); + + for(auto &test : data) { + Matrix expected = test["expected"]; + Matrix walls = test["walls"]; + + Pathing pathing(walls[0].size(), walls.size()); + + pathing.$input = test["input"]; + + REQUIRE(pathing.INVARIANT()); + pathing.compute_paths(walls); + + REQUIRE(pathing.INVARIANT()); + + matrix::dump("PATHING RESULT", pathing.$paths); + matrix::dump("PATHING EXPECTED", expected); + REQUIRE(pathing.$paths == expected); + } +} + +TEST_CASE("random flood", "[pathing]") { + json data = load_test_pathing("./tests/dijkstra.json"); + auto test = data[0]; + + Matrix expected = test["expected"]; + Matrix walls = test["walls"]; + + Pathing pathing(walls[0].size(), walls.size()); + pathing.$input = test["input"]; + + REQUIRE(pathing.INVARIANT()); + pathing.compute_paths(walls); +} diff --git a/tests/save.cpp b/tests/save.cpp new file mode 100644 index 0000000..c70d6dd --- /dev/null +++ b/tests/save.cpp @@ -0,0 +1,103 @@ +#include +#include +#include +#include "dinkyecs.hpp" +#include "components.hpp" +#include "save.hpp" +#include +#include +#include "map.hpp" +#include "worldbuilder.hpp" +#include "tser.hpp" + +using namespace fmt; +using std::string; +using namespace components; + + +enum class Item : char { + RADAR = 'R', + TRAP = 'T', + ORE = 'O' +}; + +struct Pixel { + int x = 0; + int y = 0; + + DEFINE_SERIALIZABLE(Pixel, x, y); +}; + +struct Robot { + Pixel point; + std::wstring name; + std::optional item; + + DEFINE_SERIALIZABLE(Robot, point, name, item); +}; + + +TEST_CASE("test using tser for serialization", "[config]") { + auto robot = Robot{ Pixel{3,4}, L"BIG NAME", Item::RADAR}; + + tser::BinaryArchive archive; + archive.save(robot); + std::string_view archive_view = archive.get_buffer(); + + tser::BinaryArchive archive2(0); + archive2.initialize(archive_view); + auto loadedRobot = archive2.load(); + + REQUIRE(loadedRobot.point.x == robot.point.x); + REQUIRE(loadedRobot.point.y == robot.point.y); + REQUIRE(loadedRobot.name == robot.name); + REQUIRE(loadedRobot.item == robot.item); +} + +TEST_CASE("basic save a world", "[save]") { + DinkyECS::World world; + Map map(20, 20); + WorldBuilder builder(map); + builder.generate_map(); + + // configure a player as a fact of the world + Player player{world.entity()}; + world.set_the(player); + + world.set(player.entity, {10,10}); + world.set(player.entity, {0, 0}); + world.set(player.entity, {100, 10}); + world.set(player.entity, {"@"}); + world.set(player.entity, {102}); + + save::to_file("./savetest.world", world, map); + + DinkyECS::World in_world; + Map in_map(0, 0); // this will be changed on load + save::from_file("./savetest.world", in_world, in_map); + + Position &position1 = world.get(player.entity); + Position &position2 = in_world.get(player.entity); + REQUIRE(position1.location.x == position2.location.x); + REQUIRE(position1.location.y == position2.location.y); + + Combat &combat1 = world.get(player.entity); + Combat &combat2 = in_world.get(player.entity); + REQUIRE(combat1.hp == combat2.hp); + + Motion &motion1 = world.get(player.entity); + Motion &motion2 = in_world.get(player.entity); + REQUIRE(motion1.dx == motion2.dx); + REQUIRE(motion1.dy == motion2.dy); + + Tile &tile1 = world.get(player.entity); + Tile &tile2 = in_world.get(player.entity); + REQUIRE(tile1.chr == tile2.chr); + + REQUIRE(map.width() == in_map.width()); + REQUIRE(map.height() == in_map.height()); + REQUIRE(map.$walls == in_map.$walls); + + Inventory &inv = world.get(player.entity); + REQUIRE(inv.gold == 102); +} diff --git a/tests/spatialmap.cpp b/tests/spatialmap.cpp new file mode 100644 index 0000000..06560e8 --- /dev/null +++ b/tests/spatialmap.cpp @@ -0,0 +1,137 @@ +#include +#include +#include +#include "spatialmap.hpp" +#include "dinkyecs.hpp" + +using DinkyECS::Entity; +using namespace fmt; + +EntityList require_found(const SpatialMap& collider, Point at, bool diag, size_t expect_size) { + println("TEST require_found at={},{}", at.x, at.y); + auto [found, nearby] = collider.neighbors(at, diag); + REQUIRE(found == true); + REQUIRE(nearby.size() == expect_size); + return nearby; +} + + +TEST_CASE("confirm basic collision operations", "[collision]") { + DinkyECS::World world; + Entity player = world.entity(); + Entity enemy = world.entity(); + + SpatialMap collider; + collider.insert({11,11}, player); + collider.insert({21,21}, enemy); + + { // not found + auto [found, nearby] = collider.neighbors({1,1}); + REQUIRE(!found); + REQUIRE(nearby.empty()); + } + + // found + EntityList nearby = require_found(collider, {10,10}, true, 1); + REQUIRE(nearby[0] == player); + + { // removed + collider.remove({11,11}); + auto [found, nearby] = collider.neighbors({10,10}, true); + REQUIRE(!found); + REQUIRE(nearby.empty()); + } + + collider.insert({11,11}, player); // setup for the move test + { // moving, not found + collider.move({11,11}, {12, 12}, player); + auto [found, nearby] = collider.neighbors({10,10}, true); + REQUIRE(!found); + REQUIRE(nearby.empty()); + } + + nearby = require_found(collider, {11,11}, true, 1); + REQUIRE(nearby[0] == player); + + + // confirm occupied works + REQUIRE(collider.occupied({12,12})); + REQUIRE(collider.occupied({21,21})); + REQUIRE(!collider.occupied({1,10})); + + REQUIRE(collider.get({12,12}) == player); +} + + +TEST_CASE("confirm multiple entities moving", "[collision]") { + DinkyECS::World world; + Entity player = world.entity(); + Entity e1 = world.entity(); + Entity e2 = world.entity(); + Entity e3 = world.entity(); + + SpatialMap collider; + collider.insert({11,11}, player); + collider.insert({10,10}, e2); + collider.insert({11,10}, e3); + collider.insert({21,21}, e1); + + EntityList nearby = require_found(collider, {11,11}, false, 1); + REQUIRE(nearby[0] == e3); + + nearby = require_found(collider, {11,11}, true, 2); + REQUIRE(nearby[0] == e3); + REQUIRE(nearby[1] == e2); + + collider.move({11,11}, {20,20}, player); + nearby = require_found(collider, {20,20}, true, 1); + REQUIRE(nearby[0] == e1); +} + +TEST_CASE("test edge cases that might crash", "[collision]") { + DinkyECS::World world; + Entity player = world.entity(); + Entity enemy = world.entity(); + + SpatialMap collider; + collider.insert({0,0}, player); + + Point enemy_at = {1, 0}; + collider.insert(enemy_at, enemy); + + EntityList nearby = require_found(collider, {0,0}, true, 1); + + collider.move({1,0}, {1,1}, enemy); + nearby = require_found(collider, {0,0}, true, 1); + REQUIRE(nearby[0] == enemy); + + collider.move({1,1}, {0,1}, enemy); + nearby = require_found(collider, {0,0}, true, 1); + REQUIRE(nearby[0] == enemy); +} + +TEST_CASE("check all diagonal works", "[collision]") { + DinkyECS::World world; + Entity player = world.entity(); + Entity enemy = world.entity(); + + SpatialMap collider; + Point player_at = {1,1}; + collider.insert(player_at, player); + + Point enemy_at = {1, 0}; + collider.insert(enemy_at, enemy); + + for(size_t x = 0; x <= 2; x++) { + for(size_t y = 0; y <= 2; y++) { + if(enemy_at.x == player_at.x && enemy_at.y == player_at.y) continue; // skip player spot + EntityList nearby = require_found(collider, player_at, true, 1); + REQUIRE(nearby[0] == enemy); + + // move the enemy to a new spot around the player + Point move_to = {enemy_at.x + x, enemy_at.y + y}; + collider.move(enemy_at, move_to, enemy); + enemy_at = move_to; + } + } +} diff --git a/tests/tilemap.cpp b/tests/tilemap.cpp new file mode 100644 index 0000000..bae4b20 --- /dev/null +++ b/tests/tilemap.cpp @@ -0,0 +1,25 @@ +#include +#include +#include "map.hpp" +#include "worldbuilder.hpp" +#include "tilemap.hpp" +#include "config.hpp" +#include "rand.hpp" + +using namespace fmt; +using std::string; + +TEST_CASE("tilemap can load tiles and make a map", "[tilemap]") { + size_t width = Random::uniform(10, 25); + size_t height = Random::uniform(10, 33); + + Map map(width,height); + WorldBuilder builder(map); + builder.generate_map(); + + TileMap tiles(map.width(), map.height()); + auto& walls = map.walls(); + tiles.load(walls); + tiles.dump(); + REQUIRE(tiles.INVARIANT()); +} diff --git a/tests/worldbuilder.cpp b/tests/worldbuilder.cpp new file mode 100644 index 0000000..ff97012 --- /dev/null +++ b/tests/worldbuilder.cpp @@ -0,0 +1,35 @@ +#include +#include +#include +#include +#include "map.hpp" +#include "worldbuilder.hpp" + +using namespace fmt; +using namespace nlohmann; +using std::string; + +TEST_CASE("bsp algo test", "[builder]") { + Map map(31, 20); + WorldBuilder builder(map); + builder.generate_map(); +} + +TEST_CASE("pathing", "[builder]") { + Map map(23, 14); + WorldBuilder builder(map); + builder.generate_map(); + + matrix::dump("WALLS", map.$walls, 0,0); + println("wall at 0,0=={}", map.$walls[0][0]); + + for(matrix::each_cell it{map.$walls}; it.next();) { + if(map.$walls[it.y][it.x] == WALL_VALUE) { + REQUIRE(map.iswall(it.x, it.y) == true); + REQUIRE(map.can_move({it.x, it.y}) == false); + } else { + REQUIRE(map.iswall(it.x, it.y) == false); + REQUIRE(map.can_move({it.x, it.y}) == true); + } + } +} diff --git a/tilemap.cpp b/tilemap.cpp new file mode 100644 index 0000000..33bf726 --- /dev/null +++ b/tilemap.cpp @@ -0,0 +1,75 @@ +#include "tilemap.hpp" +#include "dbc.hpp" +#include "constants.hpp" + +using nlohmann::json; + +TileMap::TileMap(size_t width, size_t height) : + $config("./assets/tiles.json"), + $width(width), + $height(height), + $tile_ids(height, matrix::Row(width, SPACE_VALUE)), + $display(height, TileRow(width, {""})) +{ +} + +void TileMap::dump(int show_x, int show_y) { + for(matrix::each_row it{$tile_ids}; it.next();) { + const TileCell &cell = $display[it.y][it.x]; + + if(int(it.x) == show_x && int(it.y) == show_y) { + fmt::print("{}<", cell.display); + } else { + fmt::print("{} ", cell.display); + } + + if(it.row) fmt::print("\n"); + } +} + +void TileMap::set_tile(size_t x, size_t y, string tile_name) { + std::wstring tile_id = $config.wstring(tile_name, "display"); + json tile_conf = $config[tile_name]; + TileCell tile{ + tile_conf["display"], + tile_conf["foreground"][0], + tile_conf["foreground"][1], + tile_conf["foreground"][2], + tile_conf["background"][0], + tile_conf["background"][1], + tile_conf["background"][2]}; + + $tile_ids[y][x] = tile_id[0]; + $display[y][x] = tile; +} + +void TileMap::load(matrix::Matrix &walls) { + for(matrix::each_cell it{walls}; it.next();) { + string tile_name = walls[it.y][it.x] == SPACE_VALUE ? "FLOOR_TILE" : "WALL_TILE"; + set_tile(it.x, it.y, tile_name); + } +} + +const TileCell &TileMap::at(size_t x, size_t y) { + return $display[y][x]; +} + +std::vector TileMap::tile_names(bool collision) { + const auto &json = $config.json(); + std::vector keys; + + for(const auto& el : json.items()) { + const auto &val = el.value(); + if(val["collision"] == collision) { + keys.push_back(el.key()); + } + } + + return keys; +} + +bool TileMap::INVARIANT() { + dbc::check(matrix::height($tile_ids) == $height, "$tile_ids has wrong height"); + dbc::check(matrix::width($tile_ids) == $width, "$tile_ids has wrong width"); + return true; +} diff --git a/tilemap.hpp b/tilemap.hpp new file mode 100644 index 0000000..495300b --- /dev/null +++ b/tilemap.hpp @@ -0,0 +1,46 @@ +#pragma once +#include +#include +#include +#include +#include +#include "point.hpp" +#include "matrix.hpp" +#include "config.hpp" + +struct TileCell { + std::string display; + uint8_t fg_h = 0; + uint8_t fg_s = 0; + uint8_t fg_v = 0; + uint8_t bg_h = 0; + uint8_t bg_s = 0; + uint8_t bg_v = 0; +}; + +typedef std::vector TileRow; +typedef std::vector TileGrid; + +class TileMap { +public: + Config $config; + size_t $width; + size_t $height; + matrix::Matrix $tile_ids; + TileGrid $display; + + TileMap(size_t width, size_t height); + + // disable copying + TileMap(TileMap &map) = delete; + + size_t width() { return $width; } + size_t height() { return $height; } + void load(matrix::Matrix &walls); + const TileCell &at(size_t x, size_t y); + void set_tile(size_t x, size_t y, std::string tile_name); + std::vector tile_names(bool collision); + + void dump(int show_x=-1, int show_y=-1); + bool INVARIANT(); +}; diff --git a/tser.hpp b/tser.hpp new file mode 100644 index 0000000..0fe6972 --- /dev/null +++ b/tser.hpp @@ -0,0 +1,220 @@ +// Licensed under the Boost License . +// SPDX-License-Identifier: BSL-1.0 +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace tser{ + //implementation details for C++20 is_detected + namespace detail { + struct ns { + ~ns() = delete; + ns(ns const&) = delete; + }; + + template class Op, class... Args> + struct detector { + using value_t = std::false_type; + using type = Default; + }; + + template class Op, class... Args> + struct detector>, Op, Args...> { + using value_t = std::true_type; + using type = Op; + }; + + template + struct is_array : std::is_array {}; + + template class TArray, typename T, size_t N> + struct is_array> : std::true_type {}; + + constexpr size_t n_args(char const* c, size_t nargs = 1) { + for (; *c; ++c) if (*c == ',') ++nargs; + return nargs; + } + + constexpr size_t str_size(char const* c, size_t strSize = 1) { + for (; *c; ++c) ++strSize; + return strSize; + } + } + + // we need a bunch of template metaprogramming for being able to differentiate between different types + template class Op, class... Args> + constexpr bool is_detected_v = detail::detector::value_t::value; + + class BinaryArchive; + template using has_begin_t = decltype(*std::begin(std::declval())); + + template using has_members_t = decltype(std::declval().members()); + + template using has_smaller_t = decltype(std::declval() < std::declval()); + + template using has_equal_t = decltype(std::declval() == std::declval()); + + template using has_nequal_t = decltype(std::declval() != std::declval()); + + template using has_outstream_op_t = decltype(std::declval() << std::declval()); + + template using has_tuple_t = std::tuple_element_t<0, T>; + + template using has_optional_t = decltype(std::declval().has_value()); + + template using has_element_t = typename T::element_type; + + template using has_mapped_t = typename T::mapped_type; + + template using has_custom_save_t = decltype(std::declval().save(std::declval())); + + template using has_free_save_t = decltype(std::declval() << std::declval()); + + template constexpr bool is_container_v = is_detected_v; + + template constexpr bool is_tuple_v = is_detected_v; + + template constexpr bool is_tser_t_v = is_detected_v; + + template constexpr bool is_pointer_like_v = std::is_pointer_v || is_detected_v || is_detected_v; + + class BinaryArchive { + std::string m_bytes = std::string(1024, '\0'); + size_t m_bufferSize = 0, m_readOffset = 0; + + public: + explicit BinaryArchive(const size_t initialSize = 1024) : m_bytes(initialSize, '\0') {} + + template + explicit BinaryArchive(const T& t) { save(t); } + + template + void save(const T& t) { + if constexpr (is_detected_v) { + operator<<(t,*this); + } else if constexpr (is_detected_v) { + t.save(*this); + } else if constexpr(is_tser_t_v) { + std::apply([&](auto& ... mVal) { (save(mVal), ...); }, t.members()); + } else if constexpr(is_tuple_v) { + std::apply([&](auto& ... tVal) { (save(tVal), ...); }, t); + } else if constexpr (is_pointer_like_v) { + save(static_cast(t)); + if (t) + save(*t); + } else if constexpr (is_container_v) { + if constexpr (!detail::is_array::value) { + save(t.size()); + } + + for (auto& val : t) save(val); + } else { + if (m_bufferSize + sizeof(T) + sizeof(T) / 4 > m_bytes.size()) { + m_bytes.resize((m_bufferSize + sizeof(T)) * 2); + } + + std::memcpy(m_bytes.data() + m_bufferSize, std::addressof(t), sizeof(T)); + m_bufferSize += sizeof(T); + } + } + + template + void load(T& t) { + using V = std::decay_t; + if constexpr (is_detected_v) { + operator>>(t, *this); + } else if constexpr (is_detected_v) { + t.load(*this); + } else if constexpr (is_tser_t_v) { + std::apply([&](auto& ... mVal) { (load(mVal), ...); }, t.members()); + } else if constexpr (is_tuple_v) { + std::apply([&](auto& ... tVal) { (load(tVal), ...); }, t); + } else if constexpr (is_pointer_like_v) { + if constexpr (std::is_pointer_v) { + t = load() ? (t = new std::remove_pointer_t(), load(*t), t) : nullptr; + } else if constexpr (is_detected_v) { + t = load() ? T(load()) : T(); + } else { //smart pointer + t = T(load*>()); + } + } else if constexpr (is_container_v) { + if constexpr (!detail::is_array::value) { + const auto size = load(); + using VT = typename V::value_type; + for (size_t i = 0; i < size; ++i) { + if constexpr (!is_detected_v) { + t.insert(t.end(), load()); + } else { + //we have to special case map, because of the const key + t.emplace(VT{ load(), load() }); + } + } + } else { + for (auto& val : t) load(val); + } + } else { + std::memcpy(&t, m_bytes.data() + m_readOffset, sizeof(T)); + m_readOffset += sizeof(T); + } + } + + template + T load() { + std::remove_const_t t{}; load(t); return t; + } + + template + friend BinaryArchive& operator<<(BinaryArchive& ba, const T& t) { + ba.save(t); return ba; + } + + template + friend BinaryArchive& operator>>(BinaryArchive& ba, T& t) { + ba.load(t); return ba; + } + + void reset() { + m_bufferSize = 0; + m_readOffset = 0; + } + + void initialize(std::string_view str) { + m_bytes = str; + m_bufferSize = str.size(); + m_readOffset = 0; + } + + std::string_view get_buffer() const { + return std::string_view(m_bytes.data(), m_bufferSize); + } + + }; + + template + std::conditional_t, const Base, Base>& base(Derived* thisPtr) { return *thisPtr; } + + template + auto load(std::string_view encoded) { BinaryArchive ba(encoded); return ba.load(); } +} + +//this macro defines printing, serialisation and comparision operators (==,!=,<) for custom types +#define DEFINE_SERIALIZABLE(Type, ...) \ +inline decltype(auto) members() const { return std::tie(__VA_ARGS__); } \ +inline decltype(auto) members() { return std::tie(__VA_ARGS__); } \ +static constexpr std::array _memberNameData = [](){ \ +std::array chars{'\0'}; size_t _idx = 0; constexpr auto* ini(#__VA_ARGS__); \ +for (char const* _c = ini; *_c; ++_c, ++_idx) { if(*_c != ',' && *_c != ' ') chars[_idx] = *_c; } return chars;}(); \ +static constexpr const char* _typeName = #Type; \ +static constexpr std::array _memberNames = \ +[](){ std::array out{ }; \ +for(size_t _i = 0, nArgs = 0; nArgs < tser::detail::n_args(#__VA_ARGS__) ; ++_i) { \ +while(Type::_memberNameData[_i] == '\0') {_i++;} out[nArgs++] = &Type::_memberNameData[_i]; \ +while(Type::_memberNameData[++_i] != '\0'); } return out;}(); diff --git a/worldbuilder.cpp b/worldbuilder.cpp new file mode 100644 index 0000000..ab31cd5 --- /dev/null +++ b/worldbuilder.cpp @@ -0,0 +1,347 @@ +#include "worldbuilder.hpp" +#include "rand.hpp" +#include +#include +#include "components.hpp" + +using namespace fmt; +using namespace components; + +inline void check_player(DinkyECS::World &world, DinkyECS::Entity entity) { + auto player = world.get_the(); + dbc::check(player.entity != entity, "player shouldn't be added to world"); + + auto tile = world.get(player.entity); + dbc::check(tile.chr == "\ua66b", format("PLAYER TILE CHANGED {} != {}", tile.chr, "\ua66b")); +} + +inline int make_split(Room &cur, bool horiz) { + size_t dimension = horiz ? cur.height : cur.width; + int min = dimension / WORLDBUILD_DIVISION; + int max = dimension - min; + + return Random::uniform(min, max); +} + +void WorldBuilder::set_door(Room &room, int value) { + $map.$walls[room.entry.y][room.entry.x] = value; + $map.$walls[room.exit.y][room.exit.x] = value; +} + +void rand_side(Room &room, Point &door) { + dbc::check(int(room.width) > 0 && int(room.height) > 0, "Weird room with 0 for height or width."); + int rand_x = Random::uniform(0, room.width - 1); + int rand_y = Random::uniform(0, room.height - 1); + + switch(Random::uniform(0,3)) { + case 0: // north + door.x = room.x + rand_x; + door.y = room.y-1; + break; + case 1: // south + door.x = room.x + rand_x; + door.y = room.y + room.height; + break; + case 2: // east + door.x = room.x + room.width; + door.y = room.y + rand_y; + break; + case 3: // west + door.x = room.x - 1; + door.y = room.y + rand_y; + break; + default: + dbc::sentinel("impossible side"); + } +} + +void WorldBuilder::add_door(Room &room) { + rand_side(room, room.entry); + rand_side(room, room.exit); +} + +void WorldBuilder::partition_map(Room &cur, int depth) { + if(cur.width >= 3 && cur.width <= 6 && + cur.height >= 3 && cur.height <= 6) + { + $map.add_room(cur); + return; + } + + bool horiz = cur.width > cur.height ? false : true; + int split = make_split(cur, horiz); + if(split <= 0) return; // end recursion + + Room left = cur; + Room right = cur; + + if(horiz) { + if(split >= int(cur.height)) return; // end recursion + + left.height = size_t(split - 1); + right.y = cur.y + split; + right.height = size_t(cur.height - split); + } else { + if(split >= int(cur.width)) return; // end recursion + + left.width = size_t(split-1); + right.x = cur.x + split, + right.width = size_t(cur.width - split); + } + + // BUG: min room size should be configurable + if(depth > 0 && left.width > 2 && left.height > 2) { + partition_map(left, depth-1); + } + + // BUG: min room size should be configurable + if(depth > 0 && right.width > 2 && right.height > 2) { + partition_map(right, depth-1); + } +} + +void WorldBuilder::update_door(Point &at, int wall_or_space) { + $map.$walls[at.y][at.x] = wall_or_space; +} + + +void WorldBuilder::stylize_room(int room, string tile_name, float size) { + Point pos_out; + bool placed = $map.place_entity(room, pos_out); + dbc::check(placed, "failed to place style in room"); + + for(matrix::circle it{$map.$walls, pos_out, size}; it.next();) { + for(int x = it.left; x < it.right; x++) { + if(!$map.iswall(x, it.y)) { + $map.$tiles.set_tile(x, it.y, tile_name); + } + } + } +} + +void WorldBuilder::generate_rooms() { + Room root{ + .x = 0, + .y = 0, + .width = $map.$width, + .height = $map.$height + }; + // BUG: depth should be configurable + partition_map(root, 10); + place_rooms(); + + dbc::check($map.room_count() > 0, "map generated zero rooms, map too small?"); +} + +void WorldBuilder::generate_map() { + generate_rooms(); + + PointList holes; + for(size_t i = 0; i < $map.$rooms.size() - 1; i++) { + tunnel_doors(holes, $map.$rooms[i], $map.$rooms[i+1]); + } + + // one last connection from first room to last + tunnel_doors(holes, $map.$rooms.back(), $map.$rooms.front()); + + // place all the holes + for(auto hole : holes) { + + if(!matrix::inbounds($map.$walls, hole.x, hole.y)) { + matrix::dump("MAP BEFORE CRASH", $map.$walls, hole.x, hole.y); + + auto err = fmt::format("invalid hold target {},{} map is only {},{}", + hole.x, hole.y, matrix::width($map.$walls), + matrix::height($map.$walls)); + + dbc::sentinel(err); + } + + $map.$walls[hole.y][hole.x] = INV_SPACE; + } + + $map.invert_space(); + $map.expand(); + $map.load_tiles(); + + // get only the tiles with no collision to fill rooms + auto room_types = $map.$tiles.tile_names(false); + + for(size_t i = 0; i < $map.$rooms.size() - 1; i++) { + size_t room_type = Random::uniform(0, room_types.size() - 1); + int room_size = Random::uniform(100, 800); + string tile_name = room_types[room_type]; + stylize_room(i, tile_name, room_size * 0.01f); + } +} + + +DinkyECS::Entity configure_entity_in_map(DinkyECS::World &world, Map &game_map, json &entity_data, int in_room) { + auto item = world.entity(); + Point pos_out; + bool placed = game_map.place_entity(in_room, pos_out); + dbc::check(placed, "failed to randomly place item in room"); + world.set(item, {pos_out.x+1, pos_out.y+1}); + + if(entity_data["inventory_count"] > 0) { + world.set(item, {entity_data["inventory_count"], entity_data}); + } + + if(entity_data.contains("components")) { + components::configure(world, item, entity_data); + } + return item; +} + +inline json &select_entity_type(GameConfig &config, json &gen_config) { + int enemy_test = Random::uniform(0,100); + int device_test = Random::uniform(0, 100); + + if(enemy_test < gen_config["enemy_probability"]) { + return config.enemies.json(); + } else if(device_test < gen_config["device_probability"]) { + return config.devices.json(); + } else { + return config.items.json(); + } +} + +void WorldBuilder::randomize_entities(DinkyECS::World &world, GameConfig &config) { + auto &gen_config = config.game["worldgen"]; + + for(size_t room_num = $map.room_count() - 1; room_num > 0; room_num--) { + int empty_room = Random::uniform(0, 100); + if(empty_room < gen_config["empty_room_probability"]) continue; + + json& entity_db = select_entity_type(config, gen_config); + + std::vector keys; + for(auto& el : entity_db.items()) { + auto& data = el.value(); + + if(data["placement"] == nullptr) { + keys.push_back(el.key()); + } + } + + int rand_entity = Random::uniform(0, keys.size() - 1); + std::string key = keys[rand_entity]; + auto entity_data = entity_db[key]; + + // pass that to the config as it'll be a generic json + auto entity = configure_entity_in_map(world, $map, entity_data, room_num); + check_player(world, entity); + } +} + +inline void place_stairs(DinkyECS::World& world, GameConfig& config, Map& map) { + auto& device_config = config.devices.json(); + auto entity_data = device_config["STAIRS_DOWN"]; + int last_room = map.room_count() - 1; + auto entity = configure_entity_in_map(world, map, entity_data, last_room); + check_player(world, entity); +} + +void WorldBuilder::place_entities(DinkyECS::World &world) { + auto &config = world.get_the(); + // configure a player as a fact of the world + + if(world.has_the()) { + auto& player = world.get_the(); + Point pos_out; + bool placed = $map.place_entity(0, pos_out); + dbc::check(placed, "failed to randomly place item in room"); + world.set(player.entity, {pos_out.x+1, pos_out.y+1}); + } else { + auto player_data = config.enemies["PLAYER_TILE"]; + auto player_ent = configure_entity_in_map(world, $map, player_data, 0); + // configure player in the world + Player player{player_ent}; + world.set_the(player); + world.set(player.entity, {5}); + world.make_constant(player.entity); + } + + randomize_entities(world, config); + place_stairs(world, config, $map); +} + +void WorldBuilder::generate(DinkyECS::World &world) { + generate_map(); + place_entities(world); +} + +void WorldBuilder::make_room(size_t origin_x, size_t origin_y, size_t w, size_t h) { + $map.INVARIANT(); + dbc::pre("y out of bounds", origin_y + h < $map.$height); + dbc::pre("x out of bounds", origin_x + w < $map.$width); + + for(size_t y = origin_y; y < origin_y + h; ++y) { + for(size_t x = origin_x; x < origin_x + w; ++x) { + $map.$walls[y][x] = INV_SPACE; + } + } +} + + +void WorldBuilder::place_rooms() { + for(auto &cur : $map.$rooms) { + // println("ROOM x/y={},{}; w/h={},{}; map={},{}", + // cur.x, cur.y, cur.width, cur.height, $map.$width, $map.$height); + add_door(cur); + make_room(cur.x, cur.y, cur.width, cur.height); + } +} + +inline bool random_path(Map &map, PointList &holes, Point src, Point target) { + bool keep_going = false; + bool target_found = false; + int count = 0; + map.set_target(target); + map.make_paths(); + Matrix &paths = map.paths(); + + Point out{src.x, src.y}; + do { + keep_going = map.neighbors(out, true); + holes.push_back(out); + target_found = paths[out.y][out.x] == 0; + } while(!target_found && keep_going && ++count < WORLDBUILD_MAX_PATH); + + map.INVARIANT(); + map.clear_target(target); + + return target_found; +} + +inline void straight_path(Map &map, PointList &holes, Point src, Point target) { + for(matrix::line dig{src, target}; dig.next();) { + holes.emplace_back(size_t(dig.x), size_t(dig.y)); + Point expand{(size_t)dig.x+1, (size_t)dig.y}; + + if(map.inmap(expand.x, expand.y)) { + // BUG? should really just move doors away from the edge + holes.push_back(expand); + } + } +} + +void WorldBuilder::tunnel_doors(PointList &holes, Room &src, Room &target) { + int path_type = Random::uniform(0, 3); + + switch(path_type) { + case 0: + // for now do 25% as simple straight paths + straight_path($map, holes, src.exit, target.entry); + break; + case 1: + // for now do 25% as simple straight paths + straight_path($map, holes, src.exit, target.entry); + break; + default: + // then do the rest as random with fallback + if(!random_path($map, holes, src.exit, target.entry)) { + straight_path($map, holes, src.exit, target.entry); + } + } +} diff --git a/worldbuilder.hpp b/worldbuilder.hpp new file mode 100644 index 0000000..dad04f6 --- /dev/null +++ b/worldbuilder.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include "map.hpp" +#include "dinkyecs.hpp" +#include "components.hpp" + +class WorldBuilder { + public: + Map& $map; + + WorldBuilder(Map &map) : $map(map) { } + + void partition_map(Room &cur, int depth); + void make_room(size_t origin_y, size_t origin_x, size_t width, size_t height); + void add_door(Room &room); + void set_door(Room &room, int value); + void place_rooms(); + void tunnel_doors(PointList &holes, Room &src, Room &target); + void update_door(Point &at, int wall_or_space); + void stylize_room(int room, string tile_name, float size); + void generate_rooms(); + void generate_map(); + void place_entities(DinkyECS::World &world); + void generate(DinkyECS::World &world); + void random_entity(DinkyECS::World &world, components::GameConfig &config); + void randomize_entities(DinkyECS::World &world, components::GameConfig &config); +};