master
parent
8d3d3b4ec3
commit
2daa1c9bd5
@ -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"] |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
} |
@ -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}} |
||||||
|
] |
||||||
|
} |
||||||
|
} |
@ -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}} |
||||||
|
] |
||||||
|
} |
||||||
|
} |
@ -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" |
||||||
|
} |
||||||
|
} |
After Width: | Height: | Size: 81 KiB |
After Width: | Height: | Size: 88 KiB |
After Width: | Height: | Size: 99 KiB |
@ -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; |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
}; |
||||||
|
} |
@ -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
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
@ -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)); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
}; |
||||||
|
} |
@ -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(); |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
@ -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; |
||||||
|
}; |
||||||
|
} |
@ -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; |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
}; |
||||||
|
} |
@ -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); |
||||||
|
} |
@ -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 ¤t(); |
||||||
|
size_t current_index() { return $current_level; } |
||||||
|
GameLevel &get(size_t index); |
||||||
|
LevelScaling scale_level(); |
||||||
|
}; |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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(); } |
||||||
|
}; |
||||||
|
} |
@ -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; |
||||||
|
} |
||||||
|
} |
@ -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(); |
||||||
|
}; |
@ -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; |
||||||
|
} |
@ -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(); |
||||||
|
}; |
@ -0,0 +1,6 @@ |
|||||||
|
#include "rand.hpp" |
||||||
|
|
||||||
|
namespace Random { |
||||||
|
std::random_device RNG; |
||||||
|
std::mt19937 GENERATOR(RNG()); |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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 |
||||||
|
}); |
||||||
|
} |
@ -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); |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
} |
@ -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}; |
||||||
|
} |
@ -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; |
||||||
|
}; |
@ -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); |
||||||
|
} |
@ -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); |
||||||
|
} |
@ -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]") { |
||||||
|
|
||||||
|
} |
@ -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"); |
||||||
|
} |
||||||
|
} |
@ -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] |
||||||
|
] |
||||||
|
} |
||||||
|
] |
@ -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); |
||||||
|
} |
@ -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); |
||||||
|
} |
@ -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); |
||||||
|
} |
@ -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);
|
||||||
|
} |
||||||
|
} |
@ -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);
|
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
@ -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); |
||||||
|
} |
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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()); |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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; |
||||||
|
} |
@ -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(); |
||||||
|
}; |
@ -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;}(); |
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
}; |
Loading…
Reference in new issue