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