First cut of pulling out the relevant parts of my original game to make a little framework.

This commit is contained in:
Zed A. Shaw 2026-03-22 10:37:45 -04:00
commit 6a0c9e8d46
177 changed files with 18197 additions and 0 deletions

420
src/game/autowalker.cpp Normal file
View file

@ -0,0 +1,420 @@
#include "game/autowalker.hpp"
#include "ai/ai_debug.hpp"
#include "game/level.hpp"
#include "game/systems.hpp"
struct InventoryStats {
int healing = 0;
int other = 0;
};
template<typename Comp>
int number_left() {
int count = 0;
auto world = GameDB::current_world();
auto player = GameDB::the_player();
world->query<components::Position, Comp>(
[&](const auto ent, auto&, auto&) {
if(ent != player) {
count++;
}
});
return count;
}
template<typename Comp>
Pathing compute_paths() {
auto& level = GameDB::current_level();
auto walls_copy = level.map->$walls;
Pathing paths{matrix::width(walls_copy), matrix::height(walls_copy)};
System::multi_path<Comp>(level, paths, walls_copy);
return paths;
}
DinkyECS::Entity Autowalker::camera_aim() {
auto& level = GameDB::current_level();
auto player_pos = GameDB::player_position();
// what happens if there's two things at that spot
if(level.collision->something_there(player_pos.aiming_at)) {
return level.collision->get(player_pos.aiming_at);
} else {
return DinkyECS::NONE;
}
}
void Autowalker::log(std::wstring msg) {
fsm.$map_ui.log(msg);
}
void Autowalker::status(std::wstring msg) {
fsm.$main_ui.$overlay_ui.show_text("bottom", msg);
}
void Autowalker::close_status() {
fsm.$main_ui.$overlay_ui.close_text("bottom");
}
Pathing Autowalker::path_to_enemies() {
return compute_paths<components::Combat>();
}
Pathing Autowalker::path_to_items() {
return compute_paths<components::Curative>();
}
void Autowalker::handle_window_events() {
fsm.$window.handleEvents(
[&](const sf::Event::KeyPressed &) {
fsm.autowalking = false;
close_status();
log(L"Aborting autowalk.");
},
[&](const sf::Event::MouseButtonPressed &) {
fsm.autowalking = false;
close_status();
log(L"Aborting autowalk.");
}
);
}
void Autowalker::process_combat() {
while(fsm.in_state(gui::State::IN_COMBAT)
|| fsm.in_state(gui::State::ATTACKING))
{
if(fsm.in_state(gui::State::ATTACKING)) {
send_event(game::Event::TICK);
} else {
send_event(game::Event::ATTACK);
}
}
}
void Autowalker::path_fail(const std::string& msg, Matrix& bad_paths, Point pos) {
dbc::log(msg);
status(L"PATH FAIL");
matrix::dump("MOVE FAIL PATHS", bad_paths, pos.x, pos.y);
log(L"Autowalk failed to find a path.");
send_event(game::Event::BOSS_START);
}
bool Autowalker::path_player(Pathing& paths, Point& target_out) {
auto& level = GameDB::current_level();
auto found = paths.find_path(target_out, PATHING_TOWARD, false);
if(found == PathingResult::FAIL) {
// failed to find a linear path, try diagonal
if(paths.find_path(target_out, PATHING_TOWARD, true) == PathingResult::FAIL) {
path_fail("random_walk", paths.$paths, target_out);
return false;
}
}
if(!level.map->can_move(target_out)) {
path_fail("level_map->can_move", paths.$paths, target_out);
return false;
}
return true;
}
void Autowalker::rotate_player(Point target) {
auto &player = GameDB::player_position();
if(target == player.location) {
dbc::log("player stuck at a locatoin");
fsm.autowalking = false;
return;
}
auto dir = System::shortest_rotate(player.location, player.aiming_at, target);
for(int i = 0; player.aiming_at != target; i++) {
if(i > 10) {
dbc::log("HIT OVER ROTATE BUG!");
break;
}
send_event(dir);
while(fsm.in_state(gui::State::ROTATING) ||
fsm.in_state(gui::State::COMBAT_ROTATE))
{
send_event(game::Event::TICK);
}
}
fsm.autowalking = player.aiming_at == target;
}
void Autowalker::update_state(ai::EntityAI& player_ai) {
int enemy_count = number_left<components::Combat>();
int item_count = number_left<components::InventoryItem>();
player_ai.set_state("no_more_enemies", enemy_count == 0);
player_ai.set_state("no_more_items", item_count == 0);
player_ai.set_state("enemy_found", found_enemy());
player_ai.set_state("health_good", player_health_good());
player_ai.set_state("in_combat",
fsm.in_state(gui::State::IN_COMBAT) ||
fsm.in_state(gui::State::ATTACKING));
auto inv = player_item_count();
player_ai.set_state("have_item", inv.other > 0 || inv.healing > 0);
player_ai.set_state("have_healing", inv.healing > 0);
player_ai.update();
}
void Autowalker::handle_player_walk(ai::State& start, ai::State& goal) {
ai::EntityAI player_ai("Host::actions", start, goal);
update_state(player_ai);
auto level = GameDB::current_level();
if(player_ai.wants_to("find_enemy")) {
status(L"FINDING ENEMY");
auto paths = path_to_enemies();
process_move(paths, [&](auto target) -> bool {
return level.collision->occupied(target);
});
face_enemy();
} else if(player_ai.wants_to("kill_enemy")) {
status(L"KILLING ENEMY");
if(fsm.in_state(gui::State::IN_COMBAT)) {
if(face_enemy()) {
process_combat();
}
}
} else if(player_ai.wants_to("use_healing")) {
status(L"USING HEALING");
player_use_healing();
} else if(player_ai.wants_to("collect_items") || player_ai.wants_to("find_healing")) {
fmt::println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
status(player_ai.wants_to("collect_items") ? L"COLLECTING ITEMS" : L"FIND HEALING");
player_ai.dump();
auto paths = path_to_items();
bool found_it = process_move(paths, [&](auto target) -> bool {
if(!level.collision->something_there(target)) return false;
auto entity = level.collision->get(target);
return level.world->has<components::Curative>(entity);
});
if(found_it) pickup_item();
fmt::println("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<");
} else if(!player_ai.active()) {
close_status();
log(L"FINAL ACTION! Autowalk done.");
fsm.autowalking = false;
} else {
close_status();
dbc::log($F("Unknown action: {}", player_ai.to_string()));
}
}
void Autowalker::open_map() {
if(!map_opened_once) {
if(!fsm.$map_open) {
send_event(game::Event::MAP_OPEN);
map_opened_once = true;
}
}
}
void Autowalker::close_map() {
if(fsm.$map_open) {
send_event(game::Event::MAP_OPEN);
}
}
void Autowalker::autowalk() {
handle_window_events();
if(!fsm.autowalking) {
close_status();
return;
}
open_map();
face_enemy();
int move_attempts = 0;
auto start = ai::load_state("Host::initial_state");
auto goal = ai::load_state("Host::final_state");
do {
handle_window_events();
handle_player_walk(start, goal);
close_map();
move_attempts++;
} while(move_attempts < 100 && fsm.autowalking);
}
bool Autowalker::process_move(Pathing& paths, std::function<bool(Point)> is_that_it) {
// target has to start at the player location then...
auto target_out = GameDB::player_position().location;
// ... target gets modified as an out parameter to find the path
if(!path_player(paths, target_out)) {
close_status();
log(L"No paths found, aborting autowalk.");
return false;
}
if(rayview->aiming_at != target_out) rotate_player(target_out);
bool found_it = is_that_it(target_out);
if(!found_it) {
send_event(game::Event::MOVE_FORWARD);
while(fsm.in_state(gui::State::MOVING)) send_event(game::Event::TICK);
}
return found_it;
}
bool Autowalker::found_enemy() {
auto& level = GameDB::current_level();
auto player = GameDB::player_position();
for(matrix::compass it{level.map->$walls, player.location.x, player.location.y}; it.next();) {
Point aim{it.x, it.y};
auto aimed_ent = level.collision->occupied_by(player.aiming_at);
if(aim != player.aiming_at || aimed_ent == DinkyECS::NONE) continue;
if(level.world->has<components::Combat>(aimed_ent)) return true;
}
return false;
}
bool Autowalker::found_item() {
auto world = GameDB::current_world();
auto aimed_at = camera_aim();
return aimed_at != DinkyECS::NONE && world->has<components::InventoryItem>(aimed_at);
}
void Autowalker::send_event(game::Event ev, std::any data) {
fsm.event(ev, data);
fsm.render();
fsm.handle_world_events();
}
bool Autowalker::player_health_good() {
auto world = GameDB::current_world();
auto player = GameDB::the_player();
auto combat = world->get<components::Combat>(player);
float health = float(combat.hp) / float(combat.max_hp);
return health > 0.5f;
}
InventoryStats Autowalker::player_item_count() {
InventoryStats stats;
auto& level = GameDB::current_level();
auto& inventory = level.world->get<inventory::Model>(level.player);
if(inventory.has("pocket_r")) {
stats.other += 1;
stats.healing += 1;
}
if(inventory.has("pocket_l")) {
stats.other += 1;
stats.healing += 1;
}
return stats;
}
void Autowalker::player_use_healing() {
auto& level = GameDB::current_level();
auto& inventory = level.world->get<inventory::Model>(level.player);
if(inventory.has("pocket_r")) {
auto gui_id = fsm.$status_ui.$gui.entity("pocket_r");
send_event(game::Event::USE_ITEM, gui_id);
}
if(inventory.has("pocket_l")) {
auto gui_id = fsm.$status_ui.$gui.entity("pocket_l");
send_event(game::Event::USE_ITEM, gui_id);
}
}
void Autowalker::start_autowalk() {
fsm.autowalking = true;
}
void Autowalker::face_target(Point target) {
if(rayview->aiming_at != target) rotate_player(target);
}
bool Autowalker::face_enemy() {
auto& level = GameDB::current_level();
auto player_at = GameDB::player_position();
auto [found, neighbors] = level.collision->neighbors(player_at.location, true);
if(found) {
auto enemy_pos = level.world->get<components::Position>(neighbors[0]);
face_target(enemy_pos.location);
} else {
dbc::log("No enemies nearby, moving on.");
}
return found;
}
void Autowalker::click_inventory(const std::string& name, guecs::Modifiers mods) {
auto& cell = fsm.$status_ui.$gui.cell_for(name);
fsm.$status_ui.mouse(cell.mid_x, cell.mid_y, mods);
fsm.handle_world_events();
}
void Autowalker::pocket_potion(GameDB::Level &level) {
auto& inventory = level.world->get<inventory::Model>(level.player);
if(inventory.has("pocket_r") && inventory.has("pocket_l")) {
player_use_healing();
}
send_event(game::Event::AIM_CLICK);
if(inventory.has("pocket_r")) {
click_inventory("pocket_l", {1 << guecs::ModBit::left});
} else {
click_inventory("pocket_r", {1 << guecs::ModBit::left});
}
}
void Autowalker::pickup_item() {
auto& level = GameDB::current_level();
auto& player_pos = GameDB::player_position();
auto collision = level.collision;
if(collision->something_there(player_pos.aiming_at)) {
auto entity = collision->get(player_pos.aiming_at);
fmt::println("AIMING AT entity {} @ {},{}",
entity, player_pos.aiming_at.x, player_pos.aiming_at.y);
if(level.world->has<components::Curative>(entity)) {
pocket_potion(level);
status(L"A POTION");
} else {
send_event(game::Event::AIM_CLICK);
status(L"I DON'T KNOW");
}
}
}

51
src/game/autowalker.hpp Normal file
View file

@ -0,0 +1,51 @@
#pragma once
#include "ai/ai.hpp"
#include "gui/fsm.hpp"
#include <guecs/ui.hpp>
struct InventoryStats;
struct Autowalker {
int enemy_count = 0;
int item_count = 0;
int device_count = 0;
bool map_opened_once = false;
gui::FSM& fsm;
std::shared_ptr<Raycaster> rayview;
Autowalker(gui::FSM& fsm)
: fsm(fsm), rayview(fsm.$main_ui.$rayview) {}
void autowalk();
void start_autowalk();
void open_map();
void close_map();
bool found_enemy();
bool found_item();
void handle_window_events();
void handle_player_walk(ai::State& start, ai::State& goal);
void send_event(game::Event ev, std::any data={});
void process_combat();
bool process_move(Pathing& paths, std::function<bool(Point)> cb);
bool path_player(Pathing& paths, Point &target_out);
void path_fail(const std::string& msg, Matrix& bad_paths, Point pos);
void rotate_player(Point target);
void log(std::wstring msg);
void status(std::wstring msg);
void close_status();
bool player_health_good();
void player_use_healing();
InventoryStats player_item_count();
void update_state(ai::EntityAI& player_ai);
DinkyECS::Entity camera_aim();
Pathing path_to_enemies();
Pathing path_to_items();
void face_target(Point target);
bool face_enemy();
void pickup_item();
void pocket_potion(GameDB::Level &level);
void click_inventory(const std::string& name, guecs::Modifiers mods);
};

36
src/game/components.cpp Normal file
View file

@ -0,0 +1,36 @@
#include "game/components.hpp"
#include "algos/point.hpp"
namespace components {
static ComponentMap MAP;
static bool MAP_configured = false;
void configure_entity(DinkyECS::World& world, DinkyECS::Entity ent, json& data) {
for (auto &i : data) {
dbc::check(i.contains("_type") && i["_type"].is_string(), $F("component has no _type: {}", data.dump()));
dbc::check(MAP.contains(i["_type"]), $F("MAP doesn't have type {}", std::string(i["_type"])));
MAP.at(i["_type"])(world, ent, i);
}
}
void init() {
if(!MAP_configured) {
components::enroll<AnimatedScene>(MAP);
components::enroll<Storyboard>(MAP);
components::enroll<Combat>(MAP);
components::enroll<Position>(MAP);
components::enroll<Curative>(MAP);
components::enroll<EnemyConfig>(MAP);
components::enroll<Personality>(MAP);
components::enroll<Tile>(MAP);
components::enroll<Motion>(MAP);
components::enroll<LightSource>(MAP);
components::enroll<Device>(MAP);
components::enroll<Sprite>(MAP);
components::enroll<Sound>(MAP);
components::enroll<Collision>(MAP);
MAP_configured = true;
}
}
}

193
src/game/components.hpp Normal file
View file

@ -0,0 +1,193 @@
#pragma once
#include "game/config.hpp"
#include "constants.hpp"
#include "algos/dinkyecs.hpp"
#include "algos/point.hpp"
#include <SFML/Graphics/Rect.hpp>
#include <SFML/Graphics/Shader.hpp>
#include <SFML/Graphics/Sprite.hpp>
#include <SFML/Graphics/View.hpp>
#include <SFML/System/Vector2.hpp>
#include <functional>
#include <optional>
#include "game/json_mods.hpp"
#include "ai/goap.hpp"
#include <array>
namespace combat {
enum class BattleHostState;
}
namespace components {
using std::string;
using namespace nlohmann;
struct CombatResult {
DinkyECS::Entity attacker;
combat::BattleHostState host_state;
int player_did = 0;
int enemy_did = 0;
};
struct InventoryItem {
int count;
json data;
};
struct SpriteEffect {
int frames;
std::shared_ptr<sf::Shader> effect;
};
struct Temporary {
bool is = true;
};
struct Collision {
bool has = true;
};
struct Position {
Point location{0,0};
Point aiming_at{0,0};
};
struct Motion {
int dx;
int dy;
bool random=false;
};
struct Tile {
wchar_t display;
std::string foreground;
std::string background;
};
struct GameConfig {
settings::Config game;
settings::Config enemies;
settings::Config items;
settings::Config tiles;
settings::Config devices;
};
struct Personality {
int hearing_distance = 10;
bool tough = true;
};
struct EnemyConfig {
std::string ai_script;
std::string ai_start_name;
std::string ai_goal_name;
};
struct Curative {
int hp = 10;
};
struct Sprite {
string name;
float scale;
};
struct AnimatedScene {
std::string background;
std::vector<std::string> layout;
json actors;
json fixtures;
};
struct Storyboard {
std::string image;
std::string audio;
std::vector<std::string> layout;
std::vector<std::array<std::string, 4>> beats;
};
struct Combat {
int hp;
int max_hp;
int ap_delta;
int max_ap;
int damage;
// everyone starts at 0 but ap_delta is added each round
int ap = 0;
/* 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);
};
struct LightSource {
int strength = 0;
float radius = 1.0f;
};
struct Device {
json config;
std::vector<std::string> events;
};
struct Sound {
std::string attack;
std::string death;
};
struct Player {
DinkyECS::Entity entity;
};
template <typename T> struct NameOf;
using ReflFuncSignature = std::function<void(DinkyECS::World& world, DinkyECS::Entity ent, nlohmann::json &j)>;
using ComponentMap = std::unordered_map<std::string, ReflFuncSignature>;
ENROLL_COMPONENT(Tile, display, foreground, background);
ENROLL_COMPONENT(AnimatedScene, background, layout, actors, fixtures);
ENROLL_COMPONENT(Sprite, name, scale);
ENROLL_COMPONENT(Curative, hp);
ENROLL_COMPONENT(LightSource, strength, radius);
ENROLL_COMPONENT(Position, location.x, location.y);
ENROLL_COMPONENT(EnemyConfig, ai_script, ai_start_name, ai_goal_name);
ENROLL_COMPONENT(Personality, hearing_distance, tough);
ENROLL_COMPONENT(Motion, dx, dy, random);
ENROLL_COMPONENT(Combat, hp, max_hp, ap_delta, max_ap, damage, dead);
ENROLL_COMPONENT(Device, config, events);
ENROLL_COMPONENT(Storyboard, image, audio, layout, beats);
ENROLL_COMPONENT(Sound, attack, death);
ENROLL_COMPONENT(Collision, has);
template<typename COMPONENT> COMPONENT convert(nlohmann::json &data) {
COMPONENT result;
from_json(data, result);
return result;
}
template<typename COMPONENT> COMPONENT get(nlohmann::json &data) {
for (auto &i : data["components"]) {
if(i["_type"] == NameOf<COMPONENT>::name) {
return convert<COMPONENT>(i);
}
}
return {};
}
template <typename COMPONENT> void enroll(ComponentMap &m) {
m[NameOf<COMPONENT>::name] = [](DinkyECS::World& world, DinkyECS::Entity ent, nlohmann::json &j) {
COMPONENT c;
from_json(j, c);
world.set<COMPONENT>(ent, c);
};
}
void init();
void configure_entity(DinkyECS::World& world, DinkyECS::Entity ent, json& data);
}

68
src/game/config.cpp Normal file
View file

@ -0,0 +1,68 @@
#include "game/config.hpp"
#include "dbc.hpp"
#include <fmt/core.h>
namespace settings {
using nlohmann::json;
std::filesystem::path Config::BASE_DIR{"."};
Config::Config(const std::string src_path) : $src_path(src_path) {
auto path_to = Config::path_to($src_path);
dbc::check(std::filesystem::exists(path_to),
$F("requested config file {} doesn't exist", path_to.string()));
std::ifstream infile(path_to);
$config = json::parse(infile);
}
nlohmann::json &Config::operator[](size_t key) {
return $config[key];
}
json &Config::operator[](const std::string &key) {
dbc::check($config.contains(key), $F("ERROR in config, key {} doesn't exist.", key));
return $config[key];
}
std::wstring Config::wstring(const std::string main_key, const std::string sub_key) {
dbc::check($config.contains(main_key),
$F("ERROR wstring main/key in config, main_key {} doesn't exist.", main_key));
dbc::check($config[main_key].contains(sub_key),
$F("ERROR wstring in config, main_key/key {}/{} doesn't exist.", main_key, sub_key));
const std::string& str_val = $config[main_key][sub_key];
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> $converter;
return $converter.from_bytes(str_val);
}
std::vector<std::string> Config::keys() {
std::vector<std::string> the_fucking_keys;
for(auto& [key, value] : $config.items()) {
the_fucking_keys.push_back(key);
}
return the_fucking_keys;
}
void Config::set_base_dir(const char *optarg) {
Config::BASE_DIR.assign(optarg);
}
std::filesystem::path Config::path_to(const std::string& path) {
return Config::BASE_DIR / path;
}
Config get(const std::string& name) {
if(name.ends_with(".json")) {
return {name};
} else {
auto path = Config::BASE_DIR / fmt::format("assets/{}.json", name);
dbc::check(std::filesystem::exists(path),
$F("config file {} does not exist", path.string()));
return {path.string()};
}
}
}

29
src/game/config.hpp Normal file
View file

@ -0,0 +1,29 @@
#pragma once
#include <nlohmann/json.hpp>
#include <fstream>
#include <codecvt>
#include <filesystem>
namespace settings {
struct Config {
static std::filesystem::path BASE_DIR;
nlohmann::json $config;
std::string $src_path;
Config(const std::string src_path);
Config(nlohmann::json config, std::string src_path)
: $config(config), $src_path(src_path) {}
nlohmann::json &operator[](size_t);
nlohmann::json &operator[](const std::string &key);
nlohmann::json &json() { return $config; };
std::wstring wstring(const std::string main_key, const std::string sub_key);
std::vector<std::string> keys();
static void set_base_dir(const char *optarg);
static std::filesystem::path path_to(const std::string& path);
};
Config get(const std::string &name);
}

99
src/game/inventory.cpp Normal file
View file

@ -0,0 +1,99 @@
#include "game/inventory.hpp"
namespace inventory {
bool Model::add(const std::string in_slot, DinkyECS::Entity ent) {
// NOTE: for the C++ die hards, copy the in_slot on purpose to avoid dangling reference
if(by_slot.contains(in_slot) || by_entity.contains(ent)) return false;
by_entity.insert_or_assign(ent, in_slot);
by_slot.insert_or_assign(in_slot, ent);
invariant();
return true;
}
const std::string& Model::get(DinkyECS::Entity ent) {
return by_entity.at(ent);
}
DinkyECS::Entity Model::get(const std::string& slot) {
return by_slot.at(slot);
}
bool Model::has(DinkyECS::Entity ent) {
return by_entity.contains(ent);
}
bool Model::has(const std::string& slot) {
return by_slot.contains(slot);
}
void Model::remove(DinkyECS::Entity ent) {
dbc::check(by_entity.contains(ent), "attempt to remove entity that isn't in by_entity");
// NOTE: this was a reference but that caused corruption, just copy
auto slot = by_entity.at(ent);
dbc::log($F("removing entity {} and slot {}", ent, slot));
dbc::check(by_slot.contains(slot), "entity is in by_entity but the slot is not in by_slot");
// NOTE: you have to erase the entity after the slot or else you get corruption
by_slot.erase(slot);
by_entity.erase(ent);
invariant();
}
void Model::invariant() {
for(auto& [slot, ent] : by_slot) {
dbc::check(by_entity.contains(ent),
$F("entity {} in by_slot isn't in by_entity?", ent));
dbc::check(by_entity.at(ent) == slot,
$F("mismatched slot {} in by_slot doesn't match entity {}", slot, ent));
}
for(auto& [ent, slot] : by_entity) {
dbc::check(by_slot.contains(slot),
$F("slot {} in by_entity isn't in by_slot?", ent));
dbc::check(by_slot.at(slot) == ent,
$F("mismatched entity {} in by_entity doesn't match entity {}", ent, slot));
}
dbc::check(by_slot.size() == by_entity.size(), "by_slot and by_entity have differing sizes");
}
void Model::dump() {
invariant();
fmt::println("INVENTORY has {} slots, sizes equal? {}, contents:",
by_entity.size(), by_entity.size() == by_slot.size());
for(auto [slot, ent] : by_slot) {
fmt::println("slot={}, ent={}, both={}, equal={}",
slot, ent, by_entity.contains(ent), by_entity.at(ent) == slot);
}
}
void Model::swap(DinkyECS::Entity a_ent, DinkyECS::Entity b_ent) {
dbc::check(by_entity.contains(a_ent), "a_entity not in inventory");
dbc::check(by_entity.contains(b_ent), "b_entity not in inventory");
if(a_ent == b_ent) return;
auto a_slot = get(a_ent);
auto b_slot = get(b_ent);
dbc::check(a_slot != b_slot, "somehow I got two different entities but they gave the same slot?");
by_slot.insert_or_assign(a_slot, b_ent);
by_entity.insert_or_assign(b_ent, a_slot);
by_slot.insert_or_assign(b_slot, a_ent);
by_entity.insert_or_assign(a_ent, b_slot);
}
size_t Model::count() {
dbc::check(by_slot.size() == by_entity.size(), "entity and slot maps have different sizes");
return by_entity.size();
}
}

25
src/game/inventory.hpp Normal file
View file

@ -0,0 +1,25 @@
#pragma once
#include "algos/dinkyecs.hpp"
#include <unordered_map>
// BUG: this should have a bool for "permanent" or "constant" so that
// everything working with it knows to do the make_constant/not_constant
// dance when using it. Idea is the System:: ops for this would get it
// and then look at the bool and add the constant ops as needed.
namespace inventory {
struct Model {
std::unordered_map<std::string, DinkyECS::Entity> by_slot;
std::unordered_map<DinkyECS::Entity, std::string> by_entity;
bool add(const std::string in_slot, DinkyECS::Entity ent);
const std::string& get(DinkyECS::Entity ent);
DinkyECS::Entity get(const std::string& slot);
bool has(DinkyECS::Entity ent);
bool has(const std::string& slot);
void remove(DinkyECS::Entity ent);
void invariant();
void dump();
void swap(DinkyECS::Entity a_ent, DinkyECS::Entity b_ent);
size_t count();
};
}

45
src/game/json_mods.hpp Normal file
View file

@ -0,0 +1,45 @@
#pragma once
#include <nlohmann/json.hpp>
#include <nlohmann/json_fwd.hpp>
#include <optional>
#define ENROLL_COMPONENT(COMPONENT, ...) \
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(COMPONENT, __VA_ARGS__); \
template <> struct NameOf<COMPONENT> { \
static constexpr const char *name = #COMPONENT; \
};
// partial specialization (full specialization works too)
namespace nlohmann {
template <typename T>
struct adl_serializer<std::optional<T>> {
static void to_json(json& j, const std::optional<T>& opt) {
if (opt == std::nullopt) {
j = nullptr;
} else {
j = *opt; // this will call adl_serializer<T>::to_json which will
// find the free function to_json in T's namespace!
}
}
static void from_json(const json& j, std::optional<T>& opt) {
if (j.is_null() || j == false) {
opt = std::nullopt;
} else {
opt = std::make_optional<T>(j.template get<T>());
// same as above, but with adl_serializer<T>::from_json
}
}
};
template<>
struct adl_serializer<std::chrono::milliseconds> {
static void to_json(json& j, const std::chrono::milliseconds& opt) {
j = opt.count();
}
static void from_json(const json& j, std::chrono::milliseconds& opt) {
opt = std::chrono::milliseconds{int(j)};
}
};
}

134
src/game/level.cpp Normal file
View file

@ -0,0 +1,134 @@
#include "game/level.hpp"
#include "game/components.hpp"
#include "game/worldbuilder.hpp"
#include "constants.hpp"
#include "game/systems.hpp"
#include "graphics/textures.hpp"
#include <list>
using lighting::LightRender;
using std::shared_ptr, std::make_shared;
using namespace components;
struct LevelScaling {
int map_width=INITIAL_MAP_W;
int map_height=INITIAL_MAP_H;
};
namespace GameDB {
using std::shared_ptr, std::string, std::make_shared;
struct LevelDB {
std::list<GameDB::Level> levels;
Level* current_level = nullptr;
int level_count = 0;
};
shared_ptr<LevelDB> LDB = nullptr;
bool initialized = false;
LevelScaling scale_level() {
return {
INITIAL_MAP_W + int(LDB->level_count * 2),
INITIAL_MAP_H + int(LDB->level_count * 2)
};
}
shared_ptr<DinkyECS::World> clone_load_world(shared_ptr<DinkyECS::World> prev_world) {
auto world = make_shared<DinkyECS::World>();
if(prev_world == nullptr) {
GameDB::load_configs(*world);
} else {
prev_world->clone_into(*world);
}
return world;
}
void register_level(Level level) {
// size BEFORE push to get the correct index
level.index = LDB->levels.size();
LDB->levels.push_back(level);
dbc::check(level.index == LDB->levels.size() - 1, "Level index is not the same as LDB->levels.size() - 1, off by one error");
LDB->level_count = level.index;
LDB->current_level = &LDB->levels.back();
}
void new_level(std::shared_ptr<DinkyECS::World> prev_world) {
dbc::check(initialized, "Forgot to call GameDB::init()");
auto world = clone_load_world(prev_world);
auto scaling = scale_level();
auto map = make_shared<Map>(scaling.map_width, scaling.map_height);
auto collision = std::make_shared<SpatialMap>();
WorldBuilder builder(*map, *collision);
builder.generate(*world);
auto lights = make_shared<LightRender>(map->tiles());
auto player = world->get_the<Player>();
register_level({
.player=player.entity,
.map=map,
.world=world,
.lights=lights,
.collision=collision});
}
void init() {
components::init();
textures::init();
if(!initialized) {
LDB = make_shared<LevelDB>();
initialized = true;
new_level(NULL);
}
}
shared_ptr<DinkyECS::World> current_world() {
dbc::check(initialized, "Forgot to call GameDB::init()");
return current_level().world;
}
Level& create_level() {
dbc::check(initialized, "Forgot to call GameDB::init()");
new_level(current_world());
return LDB->levels.back();
}
Level &current_level() {
dbc::check(initialized, "Forgot to call GameDB::init()");
return *LDB->current_level;
}
components::Position& player_position() {
dbc::check(initialized, "Forgot to call GameDB::init()");
auto& level = current_level();
return level.world->get<components::Position>(level.player);
}
DinkyECS::Entity the_player() {
dbc::check(initialized, "Forgot to call GameDB::init()");
return current_level().player;
}
void load_configs(DinkyECS::World &world) {
world.set_the<GameConfig>({
settings::get("config"),
settings::get("enemies"),
settings::get("items"),
settings::get("tiles"),
settings::get("devices"),
});
}
}

34
src/game/level.hpp Normal file
View file

@ -0,0 +1,34 @@
#pragma once
#include "algos/dinkyecs.hpp"
#include "graphics/lights.hpp"
#include "game/map.hpp"
#include <memory>
#include "algos/spatialmap.hpp"
namespace components {
struct Position;
}
namespace GameDB {
struct Level {
size_t index = 0;
DinkyECS::Entity player = DinkyECS::NONE;
std::shared_ptr<Map> map = nullptr;
std::shared_ptr<DinkyECS::World> world = nullptr;
std::shared_ptr<lighting::LightRender> lights = nullptr;
std::shared_ptr<SpatialMap> collision = nullptr;
};
Level& create_level();
void init();
Level &current_level();
std::shared_ptr<DinkyECS::World> current_world();
components::Position& player_position();
DinkyECS::Entity the_player();
std::shared_ptr<DinkyECS::World> clone_load_world(std::shared_ptr<DinkyECS::World> prev_world);
void load_configs(DinkyECS::World &world);
void register_level(Level level);
}

132
src/game/map.cpp Normal file
View file

@ -0,0 +1,132 @@
#include "game/map.hpp"
#include "dbc.hpp"
#include "algos/rand.hpp"
#include <vector>
#include <array>
#include <fmt/core.h>
#include <utility>
#include "algos/matrix.hpp"
using std::vector, std::pair;
using namespace fmt;
Map::Map(size_t width, size_t height) :
$width(width),
$height(height),
$walls(height, matrix::Row(width, SPACE_VALUE)),
$paths(width, height)
{}
Map::Map(Matrix &walls, Pathing &paths) :
$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($dead_ends.size() != 0, "no dead ends?!");
if(room_index < $rooms.size()) {
Room &start = $rooms.at(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;
}
}
}
out = $dead_ends.at(room_index % $dead_ends.size());
return true;
}
bool Map::iswall(size_t x, size_t y) {
return !$doors.contains({x, y}) && $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};
}
bool Map::random_walk(Point &out, bool random, int direction) {
int choice = Random::uniform(0,4);
return $paths.find_path(out, direction, random && choice == 0) != PathingResult::FAIL;
}
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,
$F("room invalid position {},{}",
room.x, room.y));
check(int(room.width) > 0 && int(room.height) > 0,
$F("room has invalid dims {},{}",
room.width, room.height));
}
return true;
}
void Map::init_tiles() {
$tiles = $walls;
}
void Map::add_room(Room &room) {
$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;
}
}

96
src/game/map.hpp Normal file
View file

@ -0,0 +1,96 @@
#pragma once
#include <vector>
#include <utility>
#include <string>
#include <random>
#include <algorithm>
#include <fmt/core.h>
#include "algos/point.hpp"
#include "graphics/lights.hpp"
#include "algos/pathing.hpp"
#include "algos/matrix.hpp"
#include "constants.hpp"
using lighting::LightSource;
struct Room {
size_t x = 0;
size_t y = 0;
size_t width = 0;
size_t height = 0;
bool contains(Point at) {
return at.x >= x
&& at.x <= x + width -1
&& at.y >= y
&& at.y <= y + height - 1;
}
bool overlaps(Room other) {
return
// other left > this right == other too far right
!( other.x > x + width
// other right < this left == other too far left
|| other.x + other.width < x
// other top > this bottom == too far below
|| other.y > y + height
// other bottom < this top == too far above
|| other.y + other.height < y);
}
bool operator==(const Room&) const = default;
};
using EntityGrid = std::unordered_map<Point, wchar_t>;
class Map {
public:
size_t $width;
size_t $height;
Matrix $walls;
Matrix $tiles;
Pathing $paths;
std::vector<Room> $rooms;
std::vector<Point> $dead_ends;
std::unordered_map<Point, bool> $doors;
Map(size_t width, size_t height);
Map(Matrix &walls, Pathing &paths);
Matrix& paths() { return $paths.paths(); }
Matrix& input_map() { return $paths.input(); }
Matrix& walls() { return $walls; }
Matrix& tiles() { return $tiles; }
std::vector<Room>& rooms() { return $rooms; }
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);
bool random_walk(Point &out, bool random=false, int direction=PATHING_TOWARD);
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 dump(int show_x=-1, int show_y=-1);
bool INVARIANT();
void init_tiles();
void add_room(Room &room);
void invert_space();
};

81
src/game/sound.cpp Normal file
View file

@ -0,0 +1,81 @@
#include "game/sound.hpp"
#include "dbc.hpp"
#include <fmt/core.h>
#include "game/config.hpp"
namespace sound {
static SoundManager SMGR;
static bool initialized = false;
static bool muted = false;
using namespace fmt;
using std::make_shared;
namespace fs = std::filesystem;
SoundPair& get_sound_pair(const std::string& name) {
dbc::check(initialized, "You need to call sound::init() first");
if(SMGR.sounds.contains(name)) {
// get the sound from the sound map
return SMGR.sounds.at(name);
} else {
dbc::log($F("Attempted to stop {} sound but not available.", name));
return SMGR.sounds.at("blank");
}
}
void init() {
if(!initialized) {
auto assets = settings::get("config");
for(auto& el : assets["sounds"].items()) {
load(el.key(), el.value());
}
initialized = true;
}
}
void load(const std::string& name, const std::string& sound_path) {
dbc::check(fs::exists(sound_path), $F("sound file {} does not exist", sound_path));
// create the buffer and keep in the buffer map
auto buffer = make_shared<sf::SoundBuffer>(sound_path);
// set it on the sound and keep in the sound map
auto sound = make_shared<sf::Sound>(*buffer);
sound->setRelativeToListener(false);
sound->setPosition({0.0f, 0.0f, 1.0f});
SMGR.sounds.try_emplace(name, buffer, sound);
}
void play(const std::string& name, bool loop) {
if(muted) return;
auto& pair = get_sound_pair(name);
pair.sound->setLooping(loop);
// play it
pair.sound->play();
}
void stop(const std::string& name) {
auto& pair = get_sound_pair(name);
pair.sound->stop();
}
bool playing(const std::string& name) {
auto& pair = get_sound_pair(name);
auto status = pair.sound->getStatus();
return status == sf::SoundSource::Status::Playing;
}
void play_at(const std::string& name, float x, float y, float z) {
auto& pair = get_sound_pair(name);
pair.sound->setPosition({x, y, z});
pair.sound->play();
}
void mute(bool setting) {
muted = setting;
}
}

26
src/game/sound.hpp Normal file
View file

@ -0,0 +1,26 @@
#pragma once
#include <string>
#include <filesystem>
#include <memory>
#include <unordered_map>
#include <SFML/Audio.hpp>
namespace sound {
struct SoundPair {
std::shared_ptr<sf::SoundBuffer> buffer;
std::shared_ptr<sf::Sound> sound;
};
struct SoundManager {
std::unordered_map<std::string, SoundPair> sounds;
};
void init();
void load(const std::string& name, const std::string& path);
void play(const std::string& name, bool loop=false);
void play_at(const std::string& name, float x, float y, float z);
void stop(const std::string& name);
void mute(bool setting);
bool playing(const std::string& name);
SoundPair& get_sound_pair(const std::string& name);
}

656
src/game/systems.cpp Normal file
View file

@ -0,0 +1,656 @@
#include "game/systems.hpp"
#include <fmt/core.h>
#include <string>
#include <cmath>
#include "algos/rand.hpp"
#include "algos/spatialmap.hpp"
#include "dbc.hpp"
#include "graphics/lights.hpp"
#include "events.hpp"
#include "game/sound.hpp"
#include "ai/ai.hpp"
#include "ai/ai_debug.hpp"
#include "algos/shiterator.hpp"
#include "combat/battle.hpp"
#include <iostream>
#include "graphics/shaders.hpp"
#include "graphics/textures.hpp"
#include "game/inventory.hpp"
#include "game/level.hpp"
#include "events.hpp"
#include "graphics/animation.hpp"
using std::string;
using namespace fmt;
using namespace combat;
using namespace components;
using namespace DinkyECS;
using lighting::LightSource;
void System::set_position(World& world, SpatialMap& collision, Entity entity, Position pos) {
world.set<Position>(entity, pos);
bool has_collision = world.has<Collision>(entity);
collision.insert(pos.location, entity, has_collision);
}
void System::lighting() {
auto& level = GameDB::current_level();
auto& light = *level.lights;
auto& world = *level.world;
auto& map = *level.map;
light.reset_light();
world.query<Position>([&](auto, auto &position) {
light.set_light_target(position.location);
});
light.path_light(map.walls());
world.query<Position, LightSource>([&](auto ent, auto &position, auto &lightsource) {
light.render_light(lightsource, position.location);
if(ent == level.player) {
light.update_fow(position.location, lightsource);
}
});
}
void System::generate_paths() {
auto& level = GameDB::current_level();
const auto &player_pos = GameDB::player_position();
level.map->set_target(player_pos.location);
level.map->make_paths();
}
void System::enemy_ai_initialize() {
auto& level = GameDB::current_level();
auto& world = *level.world;
auto& map = *level.map;
world.query<Position, EnemyConfig>([&](const auto ent, auto& pos, auto& config) {
if(world.has<ai::EntityAI>(ent)) {
auto& enemy = world.get<ai::EntityAI>(ent);
auto& personality = world.get<Personality>(ent);
enemy.set_state("detect_enemy", map.distance(pos.location) < personality.hearing_distance);
enemy.update();
} else {
auto ai_start = ai::load_state(config.ai_start_name);
auto ai_goal = ai::load_state(config.ai_goal_name);
ai::EntityAI enemy(config.ai_script, ai_start, ai_goal);
auto& personality = world.get<Personality>(ent);
enemy.set_state("tough_personality", personality.tough);
enemy.set_state("detect_enemy", map.distance(pos.location) < personality.hearing_distance);
enemy.update();
world.set<ai::EntityAI>(ent, enemy);
}
});
}
void System::enemy_pathing() {
auto& level = GameDB::current_level();
auto& world = *level.world;
auto& map = *level.map;
const auto &player_pos = GameDB::player_position();
world.query<Position, Motion>([&](auto ent, auto &position, auto &motion) {
if(ent != level.player) {
auto& enemy_ai = world.get<ai::EntityAI>(ent);
Point out = position.location; // copy
bool found_path = false;
if(enemy_ai.wants_to("find_enemy")) {
found_path = map.random_walk(out, motion.random, PATHING_TOWARD);
} else if(enemy_ai.wants_to("run_away")) {
found_path = map.random_walk(out, motion.random, PATHING_AWAY);
} else {
motion = {0,0};
return; // enemy doesn't want to move
}
enemy_ai.set_state("cant_move", !found_path);
enemy_ai.update();
motion = { int(out.x - position.location.x), int(out.y - position.location.y)};
}
});
map.clear_target(player_pos.location);
}
void System::motion() {
auto& level = GameDB::current_level();
auto world = level.world;
auto map = level.map;
auto collider = level.collision;
world->query<Position, Motion>(
[&](auto ent, auto &position, auto &motion) {
// skip enemies that aren't moving
if(motion.dx == 0 && motion.dy == 0) return;
Point move_to = {
position.location.x + motion.dx,
position.location.y + motion.dy
};
motion = {0,0}; // clear it after getting it
dbc::check(map->can_move(move_to), "Enemy pathing failed, move_to is wall.");
bool cant_move = collider->occupied(move_to);
if(auto enemy_ai = world->get_if<ai::EntityAI>(ent)) {
enemy_ai->set_state("cant_move", cant_move);
}
// it's a wall, skip
if(cant_move) return;
// all good, do the move
collider->move(position.location, move_to, ent);
position.location = move_to;
});
}
void System::distribute_loot(Position target_pos) {
auto& level = GameDB::current_level();
auto& world = *level.world;
auto& config = world.get_the<GameConfig>();
int inventory_count = Random::uniform(0, 3);
auto loot_entity = world.entity();
if(inventory_count > 0) {
auto& entity_data = config.devices["DEAD_BODY_LOOTABLE"];
components::configure_entity(world, loot_entity, entity_data["components"]);
// BUG: inventory_count here isn't really used to remove it
world.set<InventoryItem>(loot_entity, {inventory_count, entity_data});
} else {
// this creates a dead body on the ground
auto& entity_data = config.devices["DEAD_BODY"];
components::configure_entity(world, loot_entity, entity_data["components"]);
}
set_position(world, *level.collision, loot_entity, target_pos);
level.world->send<game::Event>(game::Event::ENTITY_SPAWN, loot_entity, {});
}
void System::death() {
auto& level = GameDB::current_level();
auto& world = *level.world;
auto player = world.get_the<Player>();
std::vector<Entity> dead_things;
world.query<Combat>([&](auto ent, auto &combat) {
// bring out yer dead
if(combat.hp <= 0 && !combat.dead) {
combat.dead = true;
if(ent != player.entity) {
// we won't change out the player's components later
dead_things.push_back(ent);
}
// we need to send this event for everything that dies
world.send<game::Event>(game::Event::DEATH, ent, {});
} else if(float(combat.hp) / float(combat.max_hp) < 0.5f) {
// if enemies are below 50% health they are marked with bad health
if(world.has<ai::EntityAI>(ent)) {
auto& enemy_ai = world.get<ai::EntityAI>(ent);
enemy_ai.set_state("health_good", false);
enemy_ai.update();
}
}
});
// this goes through everything that died and changes them to a gravestone
for(auto ent : dead_things) {
if(auto snd = world.get_if<Sound>(ent)) {
sound::stop(snd->attack);
sound::play(snd->death);
}
auto pos = world.get<Position>(ent);
// need to remove _after_ getting the position
level.collision->remove(pos.location, ent);
// distribute_loot is then responsible for putting something there
System::distribute_loot(pos);
world.destroy(ent);
}
}
void System::combat(int attack_id) {
auto& level = GameDB::current_level();
auto& collider = *level.collision;
auto& world = *level.world;
const auto& player_pos = GameDB::player_position();
auto& player_combat = world.get<Combat>(level.player);
auto& player_ai = world.get<ai::EntityAI>(level.player);
// this is guaranteed to not return the given position
auto [found, nearby] = collider.neighbors(player_pos.location);
combat::BattleEngine battle;
if(found) {
for(auto entity : nearby) {
if(world.has<ai::EntityAI>(entity)) {
auto& enemy_ai = world.get<ai::EntityAI>(entity);
auto& enemy_combat = world.get<Combat>(entity);
battle.add_enemy({entity, &enemy_ai, &enemy_combat});
}
}
battle.add_enemy({level.player, &player_ai, &player_combat, true});
battle.set_all("enemy_found", true);
battle.set_all("in_combat", true);
battle.player_request("kill_enemy");
battle.ap_refresh();
battle.plan();
}
// battle.dump();
while(auto act = battle.next()) {
auto [enemy, enemy_action, cost, host_state] = *act;
// player shouldn't hit theirself
if(host_state != BattleHostState::not_host) continue;
components::CombatResult result {
.attacker=enemy.entity,
.host_state=host_state,
.player_did=player_combat.attack(*enemy.combat),
.enemy_did=0
};
if(result.player_did > 0) {
spawn_attack(world, attack_id, enemy.entity);
}
if(enemy_action == "kill_enemy") {
result.enemy_did = enemy.combat->attack(player_combat);
animation::animate_entity(world, enemy.entity);
}
world.send<game::Event>(game::Event::COMBAT, enemy.entity, result);
}
}
void System::collision() {
auto& level = GameDB::current_level();
auto& collider = *level.collision;
auto& world = *level.world;
const auto& player_pos = GameDB::player_position();
// this is guaranteed to not return the given position
auto [found, nearby] = collider.neighbors(player_pos.location);
int combat_count = 0;
// AI: I think also this would a possible place to run AI decisions
for(auto entity : nearby) {
if(world.has<Combat>(entity)) {
auto combat = world.get<Combat>(entity);
if(!combat.dead) {
combat_count++;
world.send<game::Event>(game::Event::COMBAT_START, entity, entity);
}
} else {
dbc::log($F("UNKNOWN COLLISION TYPE {}", entity));
}
}
if(combat_count == 0) {
// BUG: this is probably how we get stuck in combat
world.send<game::Event>(game::Event::NO_NEIGHBORS, level.player, level.player);
}
}
/*
* This isn't for destroying something, but just removing it
* from the world for say, putting into a container or inventory.
*/
void System::remove_from_world(Entity entity) {
auto& level = GameDB::current_level();
auto& item_pos = level.world->get<Position>(entity);
level.collision->remove(item_pos.location, entity);
// if you don't do this you get the bug that you can pickup
// an item and it'll also be in your inventory
level.world->remove<Position>(entity);
}
void System::pickup() {
auto& level = GameDB::current_level();
auto& world = *level.world;
auto& collision = *level.collision;
auto pos = GameDB::player_position();
if(!collision.something_there(pos.aiming_at)) return;
auto entity = level.collision->find(pos.aiming_at, [&](auto data) -> bool {
return (world.has<InventoryItem>(data.entity) ||
world.has<Device>(data.entity));
});
if(entity == DinkyECS::NONE) {
dbc::log("no inventory or devices there");
return;
}
// use spatial find to find an item with inventory...
if(world.has<InventoryItem>(entity)) {
// NOTE: this might need to be a separate system so that people can leave stuff alone
remove_from_world(entity);
// NOTE: chests are different from say a torch, maybe 2 events or the
// GUI figures out which it is, then when you click either pick it up
// and move it or show the loot container UI
world.send<game::Event>(game::Event::LOOT_ITEM, entity, entity);
} else if(world.has<Device>(entity)) {
System::device(world, level.player, entity);
} else {
dbc::log("BUG: is this a bug in pickup?!");
}
}
void System::device(World &world, Entity actor, Entity item) {
auto& device = world.get<Device>(item);
dbc::log($F("entity {} INTERACTED WITH DEVICE {}", actor, item));
for(auto event : device.events) {
if(event == "STAIRS_DOWN") {
world.send<game::Event>(game::Event::STAIRS_DOWN, actor, device);
} else if(event == "STAIRS_UP") {
world.send<game::Event>(game::Event::STAIRS_UP, actor, device);
} else if(event == "TRAP") {
world.send<game::Event>(game::Event::TRAP, actor, device);
} else if(event == "LOOT_CONTAINER") {
world.send<game::Event>(game::Event::LOOT_CONTAINER, actor, device);
} else {
dbc::log($F(
"INVALID EVENT {} for device {}",
event, (std::string)device.config["name"]));
}
}
}
void System::move_player(Position move_to) {
auto& level = GameDB::current_level();
auto old_pos = level.world->get<Position>(level.player);
level.world->set<Position>(level.player, move_to);
level.collision->move(old_pos.location, move_to.location, level.player);
}
void System::player_status() {
auto& level = GameDB::current_level();
auto& combat = level.world->get<Combat>(level.player);
float percent = float(combat.hp) / float(combat.max_hp);
if(percent > 0.8) {
sound::play("hp_status_80");
} else if(percent > 0.6) {
sound::play("hp_status_60");
} else if(percent > 0.3) {
sound::play("hp_status_30");
} else if(percent > 0.1) {
sound::play("hp_status_10");
} else {
sound::play("hp_status_00");
}
}
std::shared_ptr<sf::Shader> System::sprite_effect(Entity entity) {
auto world = GameDB::current_world();
if(auto se = world->get_if<SpriteEffect>(entity)) {
if(se->frames > 0) {
se->frames--;
return se->effect;
} else {
world->remove<SpriteEffect>(entity);
return nullptr;
}
} else {
return nullptr;
}
}
Entity System::spawn_item(World& world, const std::string& name) {
auto& config = world.get_the<GameConfig>().items;
auto& item_config = config[name];
auto item_id = world.entity();
world.set<InventoryItem>(item_id, {1, item_config});
components::configure_entity(world, item_id, item_config["components"]);
return item_id;
}
void System::drop_item(Entity item) {
auto& level = GameDB::current_level();
auto& world = *level.world;
auto& map = *level.map;
auto player_pos = GameDB::player_position();
dbc::check(map.can_move(player_pos.location), "impossible, the player can't be in a wall");
Position drop_spot = {player_pos.aiming_at.x, player_pos.aiming_at.y};
// if they're aiming at a wall then drop at their feet
if(!map.can_move(drop_spot.location)) drop_spot = player_pos;
set_position(world, *level.collision, item, drop_spot);
level.world->not_constant(item);
level.world->send<game::Event>(game::Event::ENTITY_SPAWN, item, {});
}
// NOTE: I think pickup and this need to be different
bool System::place_in_container(Entity cont_id, const std::string& name, Entity world_entity) {
auto world = GameDB::current_world();
auto& container = world->get<inventory::Model>(cont_id);
if(container.has(world_entity)) {
fmt::println("container {} already has entity {}, skip", cont_id, world_entity);
// NOTE: I think this would be a move?!
return false;
} else if(container.has(name)) {
// this is an already occupied slot
fmt::println("container {} already has SLOT {}, skip", cont_id, name);
return false;
} else {
// this should only apply to the player's inventory
fmt::println("adding {} entity to loot with name {}", world_entity, name);
container.add(name, world_entity);
return true;
}
}
void System::remove_from_container(Entity cont_id, const std::string& slot_id) {
auto world = GameDB::current_world();
auto& container = world->get<inventory::Model>(cont_id);
auto entity = container.get(slot_id);
container.remove(entity);
}
void System::inventory_swap(Entity container_id, const std::string& a_name, const std::string &b_name) {
auto& level = GameDB::current_level();
dbc::check(a_name != b_name, "Attempt to inventory swap the same slot, you should check this and avoid calling me.");
auto& inventory = level.world->get<inventory::Model>(container_id);
auto a_ent = inventory.get(a_name);
auto b_ent = inventory.get(b_name);
inventory.swap(a_ent, b_ent);
}
bool System::inventory_occupied(Entity container_id, const std::string& name) {
auto world = GameDB::current_world();
auto& inventory = world->get<inventory::Model>(container_id);
return inventory.has(name);
}
void System::draw_map(Matrix& grid, EntityGrid& entity_map) {
auto& level = GameDB::current_level();
auto& world = *level.world;
Map &map = *level.map;
Matrix &fow = level.lights->$fow;
size_t view_x = matrix::width(grid) - 1;
size_t view_y = matrix::height(grid) - 1;
entity_map.clear();
auto player_pos = world.get<Position>(level.player).location;
Point cam_orig = map.center_camera(player_pos, view_x, view_y);
auto &tiles = map.tiles();
auto &tile_set = textures::get_map_tile_set();
/* I'm doing double tid->wchar_t conversion here, maybe just
* render the tids into the grid then let someone else do this. */
// first fill it with the map cells
for(shiterator::each_cell_t it{grid}; it.next();) {
size_t tile_y = size_t(it.y) + cam_orig.y;
size_t tile_x = size_t(it.x) + cam_orig.x;
if(matrix::inbounds(tiles, tile_x, tile_y) && fow[tile_y][tile_x]) {
size_t tid = tiles[tile_y][tile_x];
grid[it.y][it.x] = tile_set[tid];
} else {
grid[it.y][it.x] = L' ';
}
}
// then get the enemy/item/device tiles and fill those in
world.query<Position, Tile>([&](auto, auto &pos, auto &entity_glyph) {
// BUG: don't I have a within bounds macro somewhere?
if(pos.location.x >= cam_orig.x
&& pos.location.x <= cam_orig.x + view_x
&& pos.location.y >= cam_orig.y
&& pos.location.y <= cam_orig.y + view_y)
{
if(fow[pos.location.y][pos.location.x]) {
Point view_pos = map.map_to_camera(pos.location, cam_orig);
entity_map.insert_or_assign(view_pos, entity_glyph.display);
}
}
});
}
void System::render_map(Matrix& tiles, EntityGrid& entity_map, sf::RenderTexture& render, int compass_dir, wchar_t player_display) {
sf::Vector2i tile_sprite_dim{MAP_TILE_DIM,MAP_TILE_DIM};
unsigned int width = matrix::width(tiles);
unsigned int height = matrix::height(tiles);
sf::Vector2u dim{width * tile_sprite_dim.x, height * tile_sprite_dim.y};
auto render_size = render.getSize();
if(render_size.x != width || render_size.y != height) {
bool worked = render.resize(dim);
dbc::check(worked, "Failed to resize map render target.");
}
render.clear({0,0,0,255});
for(matrix::each_row it{tiles}; it.next();) {
wchar_t display = tiles[it.y][it.x];
if(display == L' ') continue; // skip for now
auto& sprite = textures::get_map_sprite(display);
sprite.setPosition({float(it.x * tile_sprite_dim.x), float(it.y * tile_sprite_dim.y)});
render.draw(sprite);
}
for(auto [point, display] : entity_map) {
auto& sprite = textures::get_map_sprite(display);
if(display == player_display) {
sf::Vector2f center{float(tile_sprite_dim.x / 2), float(tile_sprite_dim.y / 2)};
float degrees = (((compass_dir * 45) + PLAYER_SPRITE_DIR_CORRECTION) % 360);
sprite.setOrigin(center);
sprite.setRotation(sf::degrees(degrees));
sprite.setPosition({float(point.x * tile_sprite_dim.x) + center.x, float(point.y * tile_sprite_dim.y) + center.y});
} else {
sprite.setPosition({float(point.x * tile_sprite_dim.x), float(point.y * tile_sprite_dim.y)});
}
render.draw(sprite);
}
render.display();
}
bool System::use_item(const string& slot_name) {
auto& level = GameDB::current_level();
auto& world = *level.world;
auto& inventory = world.get<inventory::Model>(level.player);
auto& player_combat = world.get<Combat>(level.player);
if(player_combat.hp >= player_combat.max_hp) return false;
if(!inventory.has(slot_name)) return false;
auto what = inventory.get(slot_name);
if(auto curative = world.get_if<Curative>(what)) {
inventory.remove(what);
player_combat.hp += curative->hp;
if(player_combat.hp > player_combat.max_hp) {
player_combat.hp = player_combat.max_hp;
}
dbc::log($F("player health now {}",
player_combat.hp));
world.remove<Curative>(what);
return true;
} else {
dbc::log($F("no usable item at {}", what));
return false;
}
}
game::Event System::shortest_rotate(Point player_at, Point aiming_at, Point target) {
dbc::check(aiming_at != target, "you're already pointing there.");
dbc::check(player_at != target, "you can't turn on yourself");
float target_dx = float(player_at.x) - float(target.x);
float target_dy = float(player_at.y) - float(target.y);
float aiming_dx = float(player_at.x) - float(aiming_at.x);
float aiming_dy = float(player_at.y) - float(aiming_at.y);
float target_angle = atan2(-target_dy, target_dx) * (180.0 / std::numbers::pi);
float aiming_angle = atan2(-aiming_dy, aiming_dx) * (180.0 / std::numbers::pi);
float diff = target_angle - aiming_angle;
double normalized = fmod(diff + 360.0, 360.0);
return normalized < 180.0 ? game::Event::ROTATE_LEFT : game::Event::ROTATE_RIGHT;
}
void System::clear_attack() {
auto world = GameDB::current_world();
std::vector<Entity> dead_anim;
world->query<animation::Animation, Temporary>([&](auto ent, auto& anim, auto&) {
if(!anim.playing) dead_anim.push_back(ent);
});
for(auto ent : dead_anim) {
world->remove<Sprite>(ent);
world->remove<animation::Animation>(ent);
world->remove<SpriteEffect>(ent);
remove_from_world(ent);
}
}
void System::spawn_attack(World& world, int attack_id, DinkyECS::Entity enemy) {
}

74
src/game/systems.hpp Normal file
View file

@ -0,0 +1,74 @@
#pragma once
#include "game/components.hpp"
#include <SFML/Graphics/RenderTexture.hpp>
#include "game/map.hpp"
#include "algos/spatialmap.hpp"
#include "game/level.hpp"
#include "events.hpp"
namespace System {
using namespace components;
using namespace DinkyECS;
using std::string, matrix::Matrix;
void lighting();
void motion();
void collision();
void death();
void generate_paths();
void enemy_pathing();
void enemy_ai_initialize();
void device(World &world, Entity actor, Entity item);
void move_player(Position move_to);
Entity spawn_item(World& world, const string& name);
void drop_item(Entity item);
void enemy_ai();
void combat(int attack_id);
std::shared_ptr<sf::Shader> sprite_effect(Entity entity);
void player_status();
void distribute_loot(Position target_pos);
void pickup();
bool place_in_container(Entity cont_id, const string& name, Entity world_entity);
void remove_from_container(Entity cont_id, const std::string& name);
void remove_from_world(Entity entity);
void inventory_swap(Entity container_id, const std::string& a_name, const std::string &b_name);
bool inventory_occupied(Entity container_id, const std::string& name);
void draw_map(Matrix& grid, EntityGrid& entity_map);
void render_map(Matrix& tiles, EntityGrid& entity_map, sf::RenderTexture& render, int compass_dir, wchar_t player_display);
void set_position(DinkyECS::World& world, SpatialMap& collision, Entity entity, Position pos);
bool use_item(const std::string& slot_name);
game::Event shortest_rotate(Point player_at, Point aiming_at, Point turning_to);
template <typename T>
void multi_path(GameDB::Level& level, Pathing& paths, Matrix& walls) {
// first, put everything of this type as a target
level.world->query<Position, T>(
[&](const auto ent, auto& position, auto&) {
if(ent != level.player) {
paths.set_target(position.location);
}
});
level.world->query<Collision>(
[&](const auto ent, auto& collision) {
if(collision.has && ent != level.player) {
auto& pos = level.world->get<Position>(ent);
walls[pos.location.y][pos.location.x] = WALL_VALUE;
}
});
paths.compute_paths(walls);
}
void clear_attack();
void spawn_attack(World& world, int attack_id, DinkyECS::Entity enemy);
}

256
src/game/worldbuilder.cpp Normal file
View file

@ -0,0 +1,256 @@
#include "game/worldbuilder.hpp"
#include "algos/rand.hpp"
#include <fmt/core.h>
#include <iostream>
#include "game/components.hpp"
#include "algos/maze.hpp"
#include "graphics/textures.hpp"
#include "game/inventory.hpp"
#include "game/systems.hpp"
#include "graphics/animation.hpp"
using namespace fmt;
using namespace components;
void WorldBuilder::stylize_rooms() {
auto& tiles = $map.tiles();
auto style_config = settings::get("room_themes");
json& styles = style_config.json();
for(auto& room : $map.rooms()) {
auto& style = styles[Random::uniform(size_t(0), styles.size() - 1)];
dbc::check(style.contains("floor"),
$F("no floor spec in style {}", (std::string)style["name"]));
dbc::check(style.contains("walls"),
$F("no walls spec in style {}", (std::string)style["name"]));
auto& floor_name = style["floor"];
auto& wall_name = style["walls"];
size_t floor_id = textures::get_id(floor_name);
size_t wall_id = textures::get_id(wall_name);
for(matrix::box it{tiles, room.x, room.y, room.width+1, room.height+1}; it.next();) {
if(tiles[it.y][it.x] == 1) {
tiles[it.y][it.x] = wall_id;
} else if(tiles[it.y][it.x] == 0) {
tiles[it.y][it.x] = floor_id;
}
}
}
}
void WorldBuilder::generate_map() {
auto script = R"(
[
{"action": "hunt_and_kill"},
{"action": "clear"},
{"action": "randomize_rooms", "data": [3]},
{"action": "hunt_and_kill"},
{"action": "place_doors"}
]
)"_json;
int i = 0;
for(; i < 10; i++) {
auto [maze, valid] = maze::script($map, script);
if(valid) {
break;
} else {
maze.dump(fmt::format("FAILED width={}", $map.width()), true);
}
}
dbc::check(i < 10, "failed to find a valid map after 10 attempts");
$map.init_tiles();
stylize_rooms();
}
bool WorldBuilder::find_open_spot(Point& pos_out) {
size_t i = 0;
// horribly bad but I need to place things _somewhere_ so just fan out
for(i = 2; i < $map.width(); i++) {
// rando_rect starts at the top/left corner not center
for(matrix::rando_box it{$map.walls(), pos_out.x, pos_out.y, i}; it.next();) {
Point test{size_t(it.x), size_t(it.y)};
if($map.can_move(test) && !$collision.something_there(test)) {
pos_out = test;
return true;
}
}
}
matrix::dump("FAIL PLACE!", $map.walls(), pos_out.x, pos_out.y);
dbc::sentinel($F("failed to place entity in the entire map?: i={}; width={};", i, $map.width()));
return false;
}
DinkyECS::Entity WorldBuilder::configure_entity_in_map(DinkyECS::World &world, json &entity_data, Point pos) {
bool found = find_open_spot(pos);
dbc::check(found, "Failed to find a place for this thing.");
auto item = world.entity();
int inv_count = entity_data.contains("inventory_count") ? (int)entity_data["inventory_count"] : 0;
if(inv_count > 0) {
world.set<InventoryItem>(item, {entity_data["inventory_count"], entity_data});
}
if(entity_data.contains("components")) {
components::configure_entity(world, item, entity_data["components"]);
}
System::set_position(world, $collision, item, {pos.x, pos.y});
animation::configure(world, item);
return item;
}
DinkyECS::Entity WorldBuilder::configure_entity_in_room(DinkyECS::World &world, json &entity_data, int in_room) {
Point pos_out;
bool placed = $map.place_entity(in_room, pos_out);
dbc::check(placed, "failed to randomly place item in room");
auto entity = configure_entity_in_map(world, entity_data, pos_out);
return entity;
}
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();
}
}
inline json& random_entity_data(GameConfig& config, json& gen_config) {
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];
return entity_db[key];
}
void WorldBuilder::randomize_entities(DinkyECS::World &world, GameConfig &config) {
auto& gen_config = config.game["worldgen"];
for(int room_num = $map.room_count() - 1; room_num > 0; room_num--) {
// pass that to the config as it'll be a generic json
auto& entity_data = random_entity_data(config, gen_config);
configure_entity_in_room(world, entity_data, room_num);
}
for(auto& at : $map.$dead_ends) {
if($map.$doors.contains(at)) continue;
auto& entity_data = random_entity_data(config, gen_config);
configure_entity_in_map(world, entity_data, at);
}
}
void WorldBuilder::place_doors(DinkyECS::World& world, GameConfig& config) {
auto& device_config = config.devices.json();
auto entity_data = device_config["DOOR_PLAIN"];
auto& tiles = $map.tiles();
auto& walls = $map.walls();
for(auto [door_at, _] : $map.$doors) {
// note, we set this to WALL_VALUE so it renders as a wall but map.iswall will check if its a door for collision
walls[door_at.y][door_at.x] = WALL_VALUE;
for(matrix::compass it{tiles, door_at.x, door_at.y}; it.next();) {
if(walls[it.y][it.x] == WALL_VALUE) {
// found a wall near the door, and since doors always have n/s/e/w walls it should be the one to use
size_t wall_id = tiles[it.y][it.x]; // this is wall to use
tiles[door_at.y][door_at.x] = textures::door_for_wall(wall_id);
break;
}
}
}
}
void WorldBuilder::place_stairs(DinkyECS::World& world, GameConfig& config) {
auto& device_config = config.devices.json();
auto entity_data = device_config["STAIRS_DOWN"];
auto at_end = $map.$dead_ends.back();
configure_entity_in_map(world, entity_data, at_end);
}
void WorldBuilder::configure_starting_items(DinkyECS::World &world) {
auto& player = world.get_the<Player>();
auto torch_id = System::spawn_item(world, "TORCH_BAD");
auto &inventory = world.get<inventory::Model>(player.entity);
inventory.add("hand_r", torch_id);
world.make_constant(torch_id);
auto healing = System::spawn_item(world, "POTION_HEALING_SMALL");
inventory.add("pocket_l", healing);
world.make_constant(healing);
}
void WorldBuilder::place_entities(DinkyECS::World &world) {
auto &config = world.get_the<GameConfig>();
// configure a player as a fact of the world
Position player_pos{0,0};
if(world.has_the<Player>()) {
auto& player = world.get_the<Player>();
// first get a guess from the map
bool placed = $map.place_entity(0, player_pos.location);
dbc::check(placed, "map.place_entity failed to position player");
// then use the collision map to place the player safely
placed = find_open_spot(player_pos.location);
dbc::check(placed, "WorldBuild.find_open_spot also failed to position player");
System::set_position(world, $collision, player.entity, player_pos);
} else {
auto player_data = config.enemies["PLAYER_TILE"];
auto player_ent = configure_entity_in_room(world, player_data, 0);
player_pos = world.get<Position>(player_ent);
// configure player in the world
Player player{player_ent};
world.set_the<Player>(player);
world.set<inventory::Model>(player_ent, {});
configure_starting_items(world);
world.make_constant(player.entity);
}
dbc::check(player_pos.location.x != 0 && player_pos.location.y != 0,
"failed to place the player correctly");
place_doors(world, config);
randomize_entities(world, config);
place_stairs(world, config);
}
void WorldBuilder::generate(DinkyECS::World &world) {
generate_map();
place_entities(world);
}

32
src/game/worldbuilder.hpp Normal file
View file

@ -0,0 +1,32 @@
#pragma once
#include "game/map.hpp"
#include "algos/dinkyecs.hpp"
#include "game/components.hpp"
#include "algos/spatialmap.hpp"
class WorldBuilder {
public:
Map& $map;
SpatialMap& $collision;
WorldBuilder(Map &map, SpatialMap& collision) :
$map(map),
$collision(collision)
{ }
void generate_map();
DinkyECS::Entity configure_entity_in_map(DinkyECS::World &world, nlohmann::json &entity_data, Point pos);
DinkyECS::Entity configure_entity_in_room(DinkyECS::World &world, nlohmann::json &entity_data, int in_room);
bool find_open_spot(Point& pos_out);
void place_entities(DinkyECS::World &world);
void generate(DinkyECS::World &world);
void randomize_entities(DinkyECS::World &world, components::GameConfig &config);
void place_stairs(DinkyECS::World& world, components::GameConfig& config);
void place_doors(DinkyECS::World& world, components::GameConfig& config);
void configure_starting_items(DinkyECS::World &world);
void stylize_rooms();
};