Brought over a bunch of code from the roguelike and now will use it to generate a random map.

This commit is contained in:
Zed A. Shaw 2025-01-30 11:38:57 -05:00
parent 8d3d3b4ec3
commit 2daa1c9bd5
59 changed files with 4303 additions and 411 deletions

View file

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

47
assets/devices.json Normal file
View file

@ -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"]
}
]
}
}

23
assets/enemies.json Normal file
View file

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

75
assets/items.json Normal file
View file

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

20
assets/tiles.json Normal file
View file

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

BIN
assets/wall_simple-256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

16
combat.cpp Normal file
View file

@ -0,0 +1,16 @@
#include "combat.hpp"
#include "rand.hpp"
namespace components {
int Combat::attack(Combat &target) {
int attack = Random::uniform<int>(0,1);
int my_dmg = 0;
if(attack) {
my_dmg = Random::uniform<int>(1, damage);
target.hp -= my_dmg;
}
return my_dmg;
}
}

13
combat.hpp Normal file
View file

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

37
components.cpp Normal file
View file

@ -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<Weapon>(entity, {config["damage"]});
} else if(comp_type == "LightSource") {
world.set<LightSource>(entity, {config["strength"], config["radius"]});
} else if(comp_type == "Loot") {
world.set<Loot>(entity, {config["amount"]});
} else if(comp_type == "Tile") {
world.set<Tile>(entity, {config["chr"]});
} else if(comp_type == "EnemyConfig") {
world.set<EnemyConfig>(entity, {config["hearing_distance"]});
} else if(comp_type == "Combat") {
world.set<Combat>(entity, {config["hp"], config["damage"]});
} else if(comp_type == "Curative") {
world.set<Curative>(entity, {config["hp"]});
} else if(comp_type == "Motion") {
world.set<Motion>(entity, {config["dx"], config["dy"], config["random"]});
} else if(comp_type == "Device") {
Device device{.config=config, .events={}};
device.configure_events(comp["events"]);
world.set<Device>(entity, device);
} else {
dbc::sentinel(fmt::format("ITEM COMPONENT TYPE MISSING: {}",
std::string(comp_type)));
}
// json config variable dies
}
}
}

64
components.hpp Normal file
View file

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

View file

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

24
devices.cpp Normal file
View file

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

15
devices.hpp Normal file
View file

@ -0,0 +1,15 @@
#pragma once
#include "dinkyecs.hpp"
#include <nlohmann/json.hpp>
#include <vector>
namespace components {
using namespace nlohmann;
struct Device {
json config;
std::vector<int> events;
void configure_events(json &event_names);
};
}

158
dinkyecs.hpp Normal file
View file

@ -0,0 +1,158 @@
#pragma once
#include <functional>
#include <typeindex>
#include <typeinfo>
#include <unordered_map>
#include <any>
#include <tuple>
#include <queue>
#include "tser.hpp"
#include "dbc.hpp"
namespace DinkyECS {
typedef unsigned long Entity;
using EntityMap = std::unordered_map<Entity, std::any>;
struct Event {
int event = 0;
Entity entity = 0;
std::any data;
};
typedef std::queue<Event> EventQueue;
struct World {
unsigned long entity_count = 0;
std::unordered_map<std::type_index, EntityMap> $components;
std::unordered_map<std::type_index, std::any> $facts;
std::unordered_map<std::type_index, EventQueue> $events;
std::vector<Entity> $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 <typename Comp>
EntityMap& entity_map_for() {
return $components[std::type_index(typeid(Comp))];
}
template <typename Comp>
EventQueue& queue_map_for() {
return $events[std::type_index(typeid(Comp))];
}
template <typename Comp>
void remove(Entity ent) {
EntityMap &map = entity_map_for<Comp>();
map.erase(ent);
}
template <typename Comp>
void set_the(Comp val) {
$facts.insert_or_assign(std::type_index(typeid(Comp)), val);
}
template <typename Comp>
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<Comp&>(res);
}
template <typename Comp>
bool has_the() {
auto comp_id = std::type_index(typeid(Comp));
return $facts.contains(comp_id);
}
template <typename Comp>
void set(Entity ent, Comp val) {
EntityMap &map = entity_map_for<Comp>();
map.insert_or_assign(ent, val);
}
template <typename Comp>
Comp &get(Entity ent) {
EntityMap &map = entity_map_for<Comp>();
// use .at for bounds checking
std::any &res = map.at(ent);
return std::any_cast<Comp&>(res);
}
template <typename Comp>
bool has(Entity ent) {
EntityMap &map = entity_map_for<Comp>();
return map.contains(ent);
}
template<typename Comp>
void query(std::function<void(const Entity&, Comp&)> cb) {
EntityMap &map = entity_map_for<Comp>();
for(auto& [entity, any_comp] : map) {
Comp &res = std::any_cast<Comp&>(any_comp);
cb(entity, res);
}
}
template<typename CompA, typename CompB>
void query(std::function<void(const Entity&, CompA&, CompB&)> cb) {
EntityMap &map_a = entity_map_for<CompA>();
EntityMap &map_b = entity_map_for<CompB>();
for(auto& [entity, any_a] : map_a) {
if(map_b.contains(entity)) {
CompA &res_a = std::any_cast<CompA&>(any_a);
CompB &res_b = get<CompB>(entity);
cb(entity, res_a, res_b);
}
}
}
template<typename Comp>
void send(Comp event, Entity entity, std::any data) {
EventQueue &queue = queue_map_for<Comp>();
queue.push({event, entity, data});
}
template<typename Comp>
Event recv() {
EventQueue &queue = queue_map_for<Comp>();
Event evt = queue.front();
queue.pop();
return evt;
}
template<typename Comp>
bool has_event() {
EventQueue &queue = queue_map_for<Comp>();
return !queue.empty();
}
};
}

12
events.hpp Normal file
View file

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

42
inventory.cpp Normal file
View file

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

33
inventory.hpp Normal file
View file

@ -0,0 +1,33 @@
#pragma once
#include "lights.hpp"
#include <nlohmann/json.hpp>
#include <fmt/core.h>
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<InventoryItem> 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);
};
}

70
levelmanager.cpp Normal file
View file

@ -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<DinkyECS::World> prev_world) {
auto world = make_shared<DinkyECS::World>();
if(prev_world != nullptr) {
prev_world->clone_into(*world);
} else {
save::load_configs(*world);
}
auto scaling = scale_level();
auto map = make_shared<Map>(scaling.map_width, scaling.map_height);
WorldBuilder builder(*map);
builder.generate(*world);
size_t index = $levels.size();
auto collider = make_shared<SpatialMap>();
// not sure if this is still needed
System::init_positions(*world, *collider);
$levels.emplace_back(index, map, world,
make_shared<LightRender>(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);
}

39
levelmanager.hpp Normal file
View file

@ -0,0 +1,39 @@
#pragma once
#include "dinkyecs.hpp"
#include "lights.hpp"
#include "map.hpp"
#include <vector>
#include <memory>
#include "spatialmap.hpp"
using std::shared_ptr;
struct GameLevel {
size_t index;
shared_ptr<Map> map;
shared_ptr<DinkyECS::World> world;
shared_ptr<lighting::LightRender> lights;
shared_ptr<SpatialMap> collision;
};
struct LevelScaling {
int map_width=40;
int map_height=50;
};
class LevelManager {
public:
std::vector<GameLevel> $levels;
size_t $current_level = 0;
LevelManager();
size_t create_level(shared_ptr<DinkyECS::World> prev_world = nullptr);
GameLevel &next();
GameLevel &previous();
GameLevel &current();
size_t current_index() { return $current_level; }
GameLevel &get(size_t index);
LevelScaling scale_level();
};

62
lights.cpp Normal file
View file

@ -0,0 +1,62 @@
#include "lights.hpp"
#include "constants.hpp"
#include <vector>
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);
}
}

46
lights.hpp Normal file
View file

@ -0,0 +1,46 @@
#pragma once
#include <array>
#include "dbc.hpp"
#include "point.hpp"
#include <algorithm>
#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(); }
};
}

View file

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

235
map.cpp Normal file
View file

@ -0,0 +1,235 @@
#include "map.hpp"
#include "dbc.hpp"
#include "rand.hpp"
#include <vector>
#include <array>
#include <fmt/core.h>
#include <utility>
#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<Point, 4> 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<int>(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;
}
}

75
map.hpp Normal file
View file

@ -0,0 +1,75 @@
#pragma once
#include <vector>
#include <utility>
#include <string>
#include <random>
#include <algorithm>
#include <fmt/core.h>
#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<Room> $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();
};

View file

@ -3,77 +3,12 @@
#include <fmt/core.h>
#include <cmath>
#include <cstdlib>
#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 {

View file

@ -3,325 +3,49 @@
#include <queue>
#include <string>
#include <array>
#include <numeric>
#include <algorithm>
#include <fmt/core.h>
#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<int>;
using Matrix = shiterator::Base<int>;
template<typename T>
using BaseRow = vector<T>;
using viewport = shiterator::viewport_t<Matrix>;
template<typename T>
using Base = vector<BaseRow<T>>;
using each_cell = shiterator::each_cell_t<Matrix>;
using Row = vector<int>;
using Matrix = vector<Row>;
/*
* Just a quick thing to reset a matrix to a value.
*/
template<typename MAT, typename VAL>
inline void assign(MAT &out, VAL new_value) {
for(auto &row : out) {
row.assign(row.size(), new_value);
}
}
template<typename MAT>
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<typename MAT>
inline size_t width(MAT &mat) {
return mat[0].size();
}
template<typename MAT>
inline size_t height(MAT &mat) {
return mat.size();
}
template<typename T>
inline Base<T> make_base(size_t width, size_t height) {
Base<T> result(height, BaseRow<T>(width));
return result;
}
inline Matrix make(size_t width, size_t height) {
Matrix result(height, Row(width));
return result;
}
inline size_t next_x(size_t x, size_t width) {
return (x + 1) * ((x + 1) < width);
}
inline size_t next_y(size_t x, size_t y) {
return y + (x == 0);
}
inline bool at_end(size_t y, size_t height) {
return y < height;
}
inline bool end_row(size_t x, size_t width) {
return x == width - 1;
}
using each_row = shiterator::each_row_t<Matrix>;
using box = shiterator::box_t<Matrix>;
using compass = shiterator::compass_t<Matrix>;
using circle = shiterator::circle_t<Matrix>;
using rectangle = shiterator::rectangle_t<Matrix>;
using rando_rect = shiterator::rando_rect_t<Matrix>;
using line = shiterator::line;
void dump(const std::string &msg, Matrix &map, int show_x=-1, int show_y=-1);
template<typename MAT>
struct each_cell_t {
size_t x = ~0;
size_t y = ~0;
size_t width = 0;
size_t height = 0;
inline Matrix make(size_t width, size_t height) {
return shiterator::make<int>(width, height);
}
each_cell_t(MAT &mat)
{
height = matrix::height(mat);
width = matrix::width(mat);
}
inline bool inbounds(Matrix &mat, size_t x, size_t y) {
return shiterator::inbounds(mat, x, y);
}
bool next() {
x = next_x(x, width);
y = next_y(x, y);
return at_end(y, height);
}
};
inline size_t width(Matrix &mat) {
return shiterator::width(mat);
}
template<typename MAT>
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;
inline size_t height(Matrix &mat) {
return shiterator::height(mat);
}
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<Matrix>;
using each_cell = each_cell_t<Matrix>;
template<typename MAT>
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<Matrix>;
template<typename MAT>
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<Matrix>;
template<typename MAT>
struct compass_t {
size_t x = 0; // these are set in constructor
size_t y = 0; // again, no fancy ~ trick needed
array<int, 4> x_dirs{0, 1, 0, -1};
array<int, 4> 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<int, 4> x_in{0, 1, 0, -1};
array<int, 4> 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<Matrix>;
struct flood {
Matrix &mat;
Point start;
int old_val;
int new_val;
queue<Point> 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<typename MAT>
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<Matrix>;
inline void assign(Matrix &out, int new_value) {
shiterator::assign(out, new_value);
}
}

View file

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

86
pathing.cpp Normal file
View file

@ -0,0 +1,86 @@
#include "constants.hpp"
#include "pathing.hpp"
#include "dbc.hpp"
#include <vector>
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;
}

30
pathing.hpp Normal file
View file

@ -0,0 +1,30 @@
#pragma once
#include "point.hpp"
#include "matrix.hpp"
#include <functional>
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();
};

View file

@ -1,5 +1,6 @@
#pragma once
#include <vector>
#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<Point> PointList;
struct PointHash {
template<> struct std::hash<Point> {
size_t operator()(const Point& p) const {
return std::hash<int>()(p.x) ^ std::hash<int>()(p.y);
}

6
rand.cpp Normal file
View file

@ -0,0 +1,6 @@
#include "rand.hpp"
namespace Random {
std::random_device RNG;
std::mt19937 GENERATOR(RNG());
}

28
rand.hpp Normal file
View file

@ -0,0 +1,28 @@
#pragma once
#include <random>
namespace Random {
extern std::mt19937 GENERATOR;
template<typename T>
T uniform(T from, T to) {
std::uniform_int_distribution<T> rand(from, to);
return rand(GENERATOR);
}
template<typename T>
T uniform_real(T from, T to) {
std::uniform_real_distribution<T> rand(from, to);
return rand(GENERATOR);
}
template<typename T>
T normal(T from, T to) {
std::normal_distribution<T> rand(from, to);
return rand(GENERATOR);
}
}

96
save.cpp Normal file
View file

@ -0,0 +1,96 @@
#include "save.hpp"
#include <fstream>
#include "dbc.hpp"
#include <fmt/core.h>
#include "config.hpp"
#include <filesystem>
using namespace components;
using namespace fmt;
template<typename CompT>
inline void extract(DinkyECS::World &world, std::map<DinkyECS::Entity, CompT> &into) {
auto from_world = world.entity_map_for<CompT>();
for(auto [entity, value] : from_world) {
into[entity] = std::any_cast<CompT>(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<Player>();
save_data.map = MapData{
map.$width, map.$height,
map.$rooms, map.$walls};
// BUG: lights aren't saved/restored
extract<Position>(world, save_data.position);
extract<Combat>(world, save_data.combat);
extract<Motion>(world, save_data.motion);
extract<Tile>(world, save_data.tile);
// extract<Inventory>(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<typename CompT>
inline void inject(DinkyECS::World &world, std::map<DinkyECS::Entity, CompT> &outof) {
for(auto [entity, value] : outof) {
world.set<CompT>(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<SaveData>();
world_out.set_the<Player>(save_data.facts.player);
inject<Position>(world_out, save_data.position);
inject<Combat>(world_out, save_data.combat);
inject<Motion>(world_out, save_data.motion);
inject<Tile>(world_out, save_data.tile);
// inject<Inventory>(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<GameConfig>({
game, enemies, items, tiles, devices
});
}

45
save.hpp Normal file
View file

@ -0,0 +1,45 @@
#pragma once
#include "components.hpp"
#include "map.hpp"
#include "dinkyecs.hpp"
#include "tser.hpp"
#include <filesystem>
#include <string>
#include <map>
namespace save {
namespace fs = std::filesystem;
struct MapData {
size_t width;
size_t height;
std::vector<Room> 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<DinkyECS::Entity, components::Position> position;
std::map<DinkyECS::Entity, components::Motion> motion;
std::map<DinkyECS::Entity, components::Combat> combat;
std::map<DinkyECS::Entity, components::Tile> tile;
// std::map<DinkyECS::Entity, components::Inventory> 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);
}

607
shiterator.hpp Normal file
View file

@ -0,0 +1,607 @@
#pragma once
#include <vector>
#include <queue>
#include <string>
#include <array>
#include <numeric>
#include <algorithm>
#include <fmt/core.h>
#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<typename T>
using BaseRow = vector<T>;
template<typename T>
using Base = vector<BaseRow<T>>;
template<typename T>
inline Base<T> make(size_t width, size_t height) {
Base<T> result(height, BaseRow<T>(width));
return result;
}
/*
* Just a quick thing to reset a matrix to a value.
*/
template<typename MAT, typename VAL>
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<typename MAT>
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<typename MAT>
inline size_t width(MAT &mat) {
return mat[0].size();
}
/*
* Same as shiterator::width but just the height.
*/
template<typename MAT>
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<typename MAT>
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<typename MAT>
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<typename MAT>
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<typename MAT>
struct compass_t {
size_t x = 0; // these are set in constructor
size_t y = 0; // again, no fancy ~ trick needed
array<int, 4> x_dirs{0, 1, 0, -1};
array<int, 4> 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<int, 4> x_in{0, 1, 0, -1};
array<int, 4> 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<typename MAT>
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<typename MAT>
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<typename MAT>
struct rando_rect_t {
int x;
int y;
int x_offset;
int y_offset;
rectangle_t<MAT> 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<typename MAT>
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);
}
};
}

66
spatialmap.cpp Normal file
View file

@ -0,0 +1,66 @@
#include "spatialmap.hpp"
#include <fmt/core.h>
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};
}

31
spatialmap.hpp Normal file
View file

@ -0,0 +1,31 @@
#pragma once
#include <vector>
#include <unordered_map>
#include "map.hpp"
#include "dinkyecs.hpp"
#include "point.hpp"
typedef std::vector<DinkyECS::Entity> EntityList;
// Point's has is in point.hpp
typedef std::unordered_map<Point, DinkyECS::Entity> 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;
};

215
systems.cpp Normal file
View file

@ -0,0 +1,215 @@
#include "systems.hpp"
#include <fmt/core.h>
#include <string>
#include <cmath>
#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<Position>([&](const auto &ent[[maybe_unused]], auto &position) {
light.set_light_target(position.location);
});
light.path_light(map.walls());
world.query<Position, LightSource>([&](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<Player>();
// TODO: this will be on each enemy not a global thing
const auto &player_position = world.get<Position>(player.entity);
map.set_target(player_position.location);
map.make_paths();
world.query<Position, Motion>([&](const auto &ent, auto &position, auto &motion) {
if(ent != player.entity) {
dbc::check(world.has<EnemyConfig>(ent), "enemy is missing config");
const auto &config = world.get<EnemyConfig>(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<Position>([&](const auto &ent, auto &pos) {
if(world.has<Combat>(ent)) {
const auto& combat = world.get<Combat>(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<Position, Motion>([&](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<Player>();
// 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<Position, Combat>([&](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>(Events::GUI::DEATH, ent, {});
} else {
// remove their motion so they're dead
world.remove<Motion>(ent);
}
}
});
}
void System::collision(GameLevel &level) {
auto &collider = *level.collision;
auto &world = *level.world;
auto player = world.get_the<Player>();
const auto& player_position = world.get<Position>(player.entity);
auto& player_combat = world.get<Combat>(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<Combat>(entity)) {
auto& enemy_combat = world.get<Combat>(entity);
Events::Combat result {
player_combat.attack(enemy_combat),
enemy_combat.attack(player_combat)
};
world.send<Events::GUI>(Events::GUI::COMBAT, entity, result);
} else if(world.has<InventoryItem>(entity)) {
auto item = world.get<InventoryItem>(entity);
auto& item_pos = world.get<Position>(entity);
auto& inventory = world.get<Inventory>(player.entity);
if(world.has<LightSource>(entity)) {
inventory.add(item);
auto &new_light = world.get<LightSource>(entity);
world.set<LightSource>(player.entity, new_light);
inventory.light = new_light;
world.remove<LightSource>(entity);
}
if(world.has<Weapon>(entity)) {
inventory.add(item);
auto &weapon = world.get<Weapon>(entity);
player_combat.damage = weapon.damage;
world.remove<Weapon>(entity);
}
if(world.has<Loot>(entity)) {
auto &loot = world.get<Loot>(entity);
inventory.gold += loot.amount;
world.remove<Loot>(entity);
}
if(world.has<Curative>(entity)) {
auto& cure = world.get<Curative>(entity);
player_combat.hp += cure.hp;
world.remove<Curative>(entity);
}
collider.remove(item_pos.location);
world.remove<Tile>(entity);
world.remove<InventoryItem>(entity);
world.send<Events::GUI>(Events::GUI::LOOT, entity, item);
} else if(world.has<Device>(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<Inventory>(actor));
dbc::pre("System::pickup item isn't configured with InventoryItem.", world.has<InventoryItem>(item));
auto& inventory = world.get<Inventory>(actor);
auto& invitem = world.get<InventoryItem>(item);
inventory.add(invitem);
}
void System::device(DinkyECS::World &world, DinkyECS::Entity actor, DinkyECS::Entity item) {
auto& device = world.get<Device>(item);
for(int event : device.events) {
world.send<Events::GUI>((Events::GUI)event, actor, device);
}
println("entity {} INTERACTED WITH DEVICE {}", actor, item);
}

18
systems.hpp Normal file
View file

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

99
tests/components.cpp Normal file
View file

@ -0,0 +1,99 @@
#include <catch2/catch_test_macros.hpp>
#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<Player>(ent1, {ent1});
world.set<Position>(ent1, {{10,1}});
world.set<Motion>(ent1, {1,0});
world.set<Loot>(ent1, {100});
world.set<Inventory>(ent1, {0});
world.set<Tile>(ent1, {"Z"});
world.set<EnemyConfig>(ent1, {4});
auto player = world.get<Player>(ent1);
REQUIRE(player.entity == ent1);
auto position = world.get<Position>(ent1);
REQUIRE(position.location.x == 10);
REQUIRE(position.location.y == 1);
auto motion = world.get<Motion>(ent1);
REQUIRE(motion.dx == 1);
REQUIRE(motion.dy == 0);
auto loot = world.get<Loot>(ent1);
REQUIRE(loot.amount == 100);
auto inv = world.get<Inventory>(ent1);
REQUIRE(inv.gold == 0);
auto tile = world.get<Tile>(ent1);
REQUIRE(tile.chr == "Z");
}
TEST_CASE("all components can be facts", "[components]") {
World world;
auto ent1 = world.entity();
world.set_the<Player>({ent1});
world.set_the<Position>({{10,1}});
world.set_the<Motion>({1,0});
world.set_the<Loot>({100});
world.set_the<Inventory>({0});
world.set_the<Tile>({"Z"});
world.set_the<EnemyConfig>({4});
auto player = world.get_the<Player>();
REQUIRE(player.entity == ent1);
auto position = world.get_the<Position>();
REQUIRE(position.location.x == 10);
REQUIRE(position.location.y == 1);
auto motion = world.get_the<Motion>();
REQUIRE(motion.dx == 1);
REQUIRE(motion.dy == 0);
auto loot = world.get_the<Loot>();
REQUIRE(loot.amount == 100);
auto inv = world.get_the<Inventory>();
REQUIRE(inv.gold == 0);
auto tile = world.get_the<Tile>();
REQUIRE(tile.chr == "Z");
}
TEST_CASE("confirm combat works", "[components]") {
World world;
auto player = world.entity();
auto enemy = world.entity();
world.set<Combat>(player, {100, 10});
world.set<Combat>(enemy, {20, 10});
auto p_fight = world.get<Combat>(player);
REQUIRE(p_fight.hp == 100);
REQUIRE(p_fight.damage == 10);
REQUIRE(p_fight.dead == false);
auto e_fight = world.get<Combat>(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]") {
}

39
tests/dbc.cpp Normal file
View file

@ -0,0 +1,39 @@
#include <catch2/catch_test_macros.hpp>
#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");
}
}

60
tests/dijkstra.json Normal file
View file

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

71
tests/inventory.cpp Normal file
View file

@ -0,0 +1,71 @@
#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h>
#include <string>
#include "rand.hpp"
#include <nlohmann/json.hpp>
#include <fstream>
#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<InventoryItem>(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<GameConfig>();
auto sword = add_items(world, config);
auto player = world.entity();
world.set<Inventory>(player, {});
auto &inventory = world.get<Inventory>(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);
}

40
tests/levelmanager.cpp Normal file
View file

@ -0,0 +1,40 @@
#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h>
#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);
}

45
tests/lighting.cpp Normal file
View file

@ -0,0 +1,45 @@
#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h>
#include <nlohmann/json.hpp>
#include <fstream>
#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);
}

83
tests/map.cpp Normal file
View file

@ -0,0 +1,83 @@
#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h>
#include <nlohmann/json.hpp>
#include <fstream>
#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<size_t>(9, 21);
size_t height = Random::uniform<size_t>(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);
}
}

343
tests/matrix.cpp Normal file
View file

@ -0,0 +1,343 @@
#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h>
#include <string>
#include "config.hpp"
#include "matrix.hpp"
#include "rand.hpp"
#include "worldbuilder.hpp"
#include <nlohmann/json.hpp>
#include <fstream>
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<int>(-10,10);
}
}
}
TEST_CASE("thrash matrix iterators", "[matrix]") {
for(int count = 0; count < Random::uniform<int>(10,30); count++) {
size_t width = Random::uniform<size_t>(1, 100);
size_t height = Random::uniform<size_t>(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<size_t>(10, 21);
size_t height = Random::uniform<size_t>(10, 25);
Matrix result(height, matrix::Row(width));
matrix::assign(result, 0);
size_t size = Random::uniform<int>(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<size_t>(1, 25);
size_t height = Random::uniform<size_t>(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<size_t>(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<int>(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<size_t>(1, 25);
size_t height = Random::uniform<size_t>(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<size_t>(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<size_t>(10, 12);
size_t height = Random::uniform<size_t>(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<size_t>(10, 13);
size_t height = Random::uniform<size_t>(10, 15);
int pos_mod = Random::uniform<int>(-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<size_t>(20, 22);
size_t height = Random::uniform<size_t>(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<size_t>(9, 21);
size_t height = Random::uniform<size_t>(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<size_t>(9, 21);
size_t height = Random::uniform<size_t>(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);
}
}

51
tests/pathing.cpp Normal file
View file

@ -0,0 +1,51 @@
#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h>
#include <nlohmann/json.hpp>
#include <fstream>
#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);
}

103
tests/save.cpp Normal file
View file

@ -0,0 +1,103 @@
#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h>
#include <string>
#include "dinkyecs.hpp"
#include "components.hpp"
#include "save.hpp"
#include <optional>
#include <iostream>
#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> 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<Robot>();
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>(player);
world.set<Position>(player.entity, {10,10});
world.set<Motion>(player.entity, {0, 0});
world.set<Combat>(player.entity, {100, 10});
world.set<Tile>(player.entity, {"@"});
world.set<Inventory>(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<Position>(player.entity);
Position &position2 = in_world.get<Position>(player.entity);
REQUIRE(position1.location.x == position2.location.x);
REQUIRE(position1.location.y == position2.location.y);
Combat &combat1 = world.get<Combat>(player.entity);
Combat &combat2 = in_world.get<Combat>(player.entity);
REQUIRE(combat1.hp == combat2.hp);
Motion &motion1 = world.get<Motion>(player.entity);
Motion &motion2 = in_world.get<Motion>(player.entity);
REQUIRE(motion1.dx == motion2.dx);
REQUIRE(motion1.dy == motion2.dy);
Tile &tile1 = world.get<Tile>(player.entity);
Tile &tile2 = in_world.get<Tile>(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<Inventory>(player.entity);
REQUIRE(inv.gold == 102);
}

137
tests/spatialmap.cpp Normal file
View file

@ -0,0 +1,137 @@
#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h>
#include <string>
#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;
}
}
}

25
tests/tilemap.cpp Normal file
View file

@ -0,0 +1,25 @@
#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h>
#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<size_t>(10, 25);
size_t height = Random::uniform<size_t>(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());
}

35
tests/worldbuilder.cpp Normal file
View file

@ -0,0 +1,35 @@
#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h>
#include <nlohmann/json.hpp>
#include <fstream>
#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);
}
}
}

75
tilemap.cpp Normal file
View file

@ -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<std::string> TileMap::tile_names(bool collision) {
const auto &json = $config.json();
std::vector<std::string> 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;
}

46
tilemap.hpp Normal file
View file

@ -0,0 +1,46 @@
#pragma once
#include <vector>
#include <utility>
#include <string>
#include <algorithm>
#include <fmt/core.h>
#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<TileCell> TileRow;
typedef std::vector<TileRow> 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<std::string> tile_names(bool collision);
void dump(int show_x=-1, int show_y=-1);
bool INVARIANT();
};

220
tser.hpp Normal file
View file

@ -0,0 +1,220 @@
// Licensed under the Boost License <https://opensource.org/licenses/BSL-1.0>.
// SPDX-License-Identifier: BSL-1.0
#pragma once
#include <array>
#include <ostream>
#include <cstring>
#include <string>
#include <string_view>
#include <type_traits>
#include <tuple>
#include <locale>
#include <codecvt>
namespace tser{
//implementation details for C++20 is_detected
namespace detail {
struct ns {
~ns() = delete;
ns(ns const&) = delete;
};
template <class Default, class AlwaysVoid, template<class...> class Op, class... Args>
struct detector {
using value_t = std::false_type;
using type = Default;
};
template <class Default, template<class...> class Op, class... Args>
struct detector<Default, std::void_t<Op<Args...>>, Op, Args...> {
using value_t = std::true_type;
using type = Op<Args...>;
};
template<class T>
struct is_array : std::is_array<T> {};
template<template<typename, size_t> class TArray, typename T, size_t N>
struct is_array<TArray<T, N>> : 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 <template<class...> class Op, class... Args>
constexpr bool is_detected_v = detail::detector<detail::ns, void, Op, Args...>::value_t::value;
class BinaryArchive;
template<class T> using has_begin_t = decltype(*std::begin(std::declval<T>()));
template<class T> using has_members_t = decltype(std::declval<T>().members());
template<class T> using has_smaller_t = decltype(std::declval<T>() < std::declval<T>());
template<class T> using has_equal_t = decltype(std::declval<T>() == std::declval<T>());
template<class T> using has_nequal_t = decltype(std::declval<T>() != std::declval<T>());
template<class T> using has_outstream_op_t = decltype(std::declval<std::ostream>() << std::declval<T>());
template<class T> using has_tuple_t = std::tuple_element_t<0, T>;
template<class T> using has_optional_t = decltype(std::declval<T>().has_value());
template<class T> using has_element_t = typename T::element_type;
template<class T> using has_mapped_t = typename T::mapped_type;
template<class T> using has_custom_save_t = decltype(std::declval<T>().save(std::declval<BinaryArchive&>()));
template<class T> using has_free_save_t = decltype(std::declval<const T&>() << std::declval<BinaryArchive&>());
template<class T> constexpr bool is_container_v = is_detected_v<has_begin_t, T>;
template<class T> constexpr bool is_tuple_v = is_detected_v<has_tuple_t, T>;
template<class T> constexpr bool is_tser_t_v = is_detected_v<has_members_t, T>;
template<class T> constexpr bool is_pointer_like_v = std::is_pointer_v<T> || is_detected_v<has_element_t, T> || is_detected_v<has_optional_t, T>;
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<typename T>
explicit BinaryArchive(const T& t) { save(t); }
template<typename T>
void save(const T& t) {
if constexpr (is_detected_v<has_free_save_t, T>) {
operator<<(t,*this);
} else if constexpr (is_detected_v<has_custom_save_t, T>) {
t.save(*this);
} else if constexpr(is_tser_t_v<T>) {
std::apply([&](auto& ... mVal) { (save(mVal), ...); }, t.members());
} else if constexpr(is_tuple_v<T>) {
std::apply([&](auto& ... tVal) { (save(tVal), ...); }, t);
} else if constexpr (is_pointer_like_v<T>) {
save(static_cast<bool>(t));
if (t)
save(*t);
} else if constexpr (is_container_v<T>) {
if constexpr (!detail::is_array<T>::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<typename T>
void load(T& t) {
using V = std::decay_t<T>;
if constexpr (is_detected_v<has_free_save_t, V>) {
operator>>(t, *this);
} else if constexpr (is_detected_v<has_custom_save_t, T>) {
t.load(*this);
} else if constexpr (is_tser_t_v<T>) {
std::apply([&](auto& ... mVal) { (load(mVal), ...); }, t.members());
} else if constexpr (is_tuple_v<V>) {
std::apply([&](auto& ... tVal) { (load(tVal), ...); }, t);
} else if constexpr (is_pointer_like_v<T>) {
if constexpr (std::is_pointer_v<T>) {
t = load<bool>() ? (t = new std::remove_pointer_t<T>(), load(*t), t) : nullptr;
} else if constexpr (is_detected_v<has_optional_t, T>) {
t = load<bool>() ? T(load<typename V::value_type>()) : T();
} else { //smart pointer
t = T(load<has_element_t<V>*>());
}
} else if constexpr (is_container_v<T>) {
if constexpr (!detail::is_array<T>::value) {
const auto size = load<decltype(t.size())>();
using VT = typename V::value_type;
for (size_t i = 0; i < size; ++i) {
if constexpr (!is_detected_v<has_mapped_t, V>) {
t.insert(t.end(), load<VT>());
} else {
//we have to special case map, because of the const key
t.emplace(VT{ load<typename V::key_type>(), load<typename V::mapped_type>() });
}
}
} else {
for (auto& val : t) load(val);
}
} else {
std::memcpy(&t, m_bytes.data() + m_readOffset, sizeof(T));
m_readOffset += sizeof(T);
}
}
template<typename T>
T load() {
std::remove_const_t<T> t{}; load(t); return t;
}
template<typename T>
friend BinaryArchive& operator<<(BinaryArchive& ba, const T& t) {
ba.save(t); return ba;
}
template<typename T>
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<class Base, typename Derived>
std::conditional_t<std::is_const_v<Derived>, const Base, Base>& base(Derived* thisPtr) { return *thisPtr; }
template<typename T>
auto load(std::string_view encoded) { BinaryArchive ba(encoded); return ba.load<T>(); }
}
//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<char, tser::detail::str_size(#__VA_ARGS__)> _memberNameData = [](){ \
std::array<char, tser::detail::str_size(#__VA_ARGS__)> 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<const char*, tser::detail::n_args(#__VA_ARGS__)> _memberNames = \
[](){ std::array<const char*, tser::detail::n_args(#__VA_ARGS__)> 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;}();

347
worldbuilder.cpp Normal file
View file

@ -0,0 +1,347 @@
#include "worldbuilder.hpp"
#include "rand.hpp"
#include <fmt/core.h>
#include <iostream>
#include "components.hpp"
using namespace fmt;
using namespace components;
inline void check_player(DinkyECS::World &world, DinkyECS::Entity entity) {
auto player = world.get_the<Player>();
dbc::check(player.entity != entity, "player shouldn't be added to world");
auto tile = world.get<Tile>(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<int>(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<int>(0, room.width - 1);
int rand_y = Random::uniform<int>(0, room.height - 1);
switch(Random::uniform<int>(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<size_t>(0, room_types.size() - 1);
int room_size = Random::uniform<int>(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<Position>(item, {pos_out.x+1, pos_out.y+1});
if(entity_data["inventory_count"] > 0) {
world.set<InventoryItem>(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<int>(0,100);
int device_test = Random::uniform<int>(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<int>(0, 100);
if(empty_room < gen_config["empty_room_probability"]) continue;
json& entity_db = select_entity_type(config, gen_config);
std::vector<std::string> 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<int>(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<GameConfig>();
// configure a player as a fact of the world
if(world.has_the<Player>()) {
auto& player = world.get_the<Player>();
Point pos_out;
bool placed = $map.place_entity(0, pos_out);
dbc::check(placed, "failed to randomly place item in room");
world.set<Position>(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>(player);
world.set<Inventory>(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<int>(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);
}
}
}

27
worldbuilder.hpp Normal file
View file

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