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