Files are now in a src directory and I'm using a src/meson.build and tests/meson.build to specify what to build.

This commit is contained in:
Zed A. Shaw 2026-02-27 10:49:19 -05:00
parent 4778677647
commit 1d4ae911b9
108 changed files with 94 additions and 83 deletions

212
src/ai.cpp Normal file
View file

@ -0,0 +1,212 @@
#include "dbc.hpp"
#include "ai.hpp"
namespace ai {
using namespace nlohmann;
using namespace dbc;
static AIManager AIMGR;
static bool initialized = false;
inline void validate_profile(nlohmann::json& profile) {
for(auto& [name_key, value] : profile.items()) {
check(value < STATE_MAX,
fmt::format("profile field {} has value {} greater than STATE_MAX {}", (std::string)name_key, (int)value, STATE_MAX));
}
}
Action config_action(AIProfile& profile, nlohmann::json& config) {
check(config.contains("name"), "config_action: action config missing name");
check(config.contains("cost"), "config_action: action config missing cost");
Action result(config["name"], config["cost"]);
check(config.contains("needs"),
fmt::format("config_action: no 'needs' field", result.name));
check(config.contains("effects"),
fmt::format("config_action: no 'effects' field", result.name));
for(auto& [name_key, value] : config["needs"].items()) {
check(profile.contains(name_key), fmt::format("config_action({}): profile does not have need named {}", result.name, name_key));
result.needs(profile.at(name_key), bool(value));
}
for(auto& [name_key, value] : config["effects"].items()) {
check(profile.contains(name_key), fmt::format("config_action({}): profile does not have effect named {}", result.name, name_key));
result.effect(profile.at(name_key), bool(value));
}
return result;
}
State config_state(AIProfile& profile, nlohmann::json& config) {
State result;
for(auto& [name_key, value] : config.items()) {
check(profile.contains(name_key), fmt::format("config_state: profile does not have name {}", name_key));
int name_id = profile.at(name_key);
result[name_id] = bool(value);
}
return result;
}
/*
* This is only used in tests so I can load different fixtures.
*/
void reset() {
initialized = false;
AIMGR.actions.clear();
AIMGR.states.clear();
AIMGR.scripts.clear();
AIMGR.profile = json({});
}
void init(std::string config_path) {
if(!initialized) {
auto config = settings::get(config_path);
// profile specifies what keys (bitset indexes) are allowed
// and how they map to the bitset of State
validate_profile(config["profile"]);
// relies on json conversion?
AIMGR.profile = config["profile"];
// load all actions
auto& actions = config["actions"];
for(auto& action_vars : actions) {
auto the_action = config_action(AIMGR.profile, action_vars);
AIMGR.actions.insert_or_assign(the_action.name, the_action);
}
// load all states
auto& states = config["states"];
for(auto& [name, state_vars] : states.items()) {
auto the_state = config_state(AIMGR.profile, state_vars);
AIMGR.states.insert_or_assign(name, the_state);
}
auto& scripts = config["scripts"];
for(auto& [script_name, action_names] : scripts.items()) {
std::vector<Action> the_script;
for(auto name : action_names) {
check(AIMGR.actions.contains(name),
fmt::format("ai::init(): script {} uses action {} that doesn't exist",
(std::string)script_name, (std::string)name));
the_script.push_back(AIMGR.actions.at(name));
}
AIMGR.scripts.insert_or_assign(script_name, the_script);
}
initialized = true;
} else {
dbc::sentinel("DOUBLE INIT: AI manager should only be intialized once if not in tests.");
}
}
void check_valid_action(std::string name, std::string msg) {
dbc::check(AIMGR.actions.contains(name),
fmt::format("{} tried to access action that doesn't exist {}",
msg, name));
}
State load_state(std::string state_name) {
check(initialized, "you forgot to initialize the AI first.");
check(AIMGR.states.contains(state_name), fmt::format(
"ai::load_state({}): state does not exist in config",
state_name));
return AIMGR.states.at(state_name);
}
Action load_action(std::string action_name) {
check(initialized, "you forgot to initialize the AI first.");
check(AIMGR.actions.contains(action_name), fmt::format(
"ai::load_action({}): action does not exist in config",
action_name));
return AIMGR.actions.at(action_name);
}
std::vector<Action> load_script(std::string script_name) {
check(AIMGR.scripts.contains(script_name), fmt::format(
"ai::load_script(): no script named {} configured", script_name));
return AIMGR.scripts.at(script_name);
}
ActionPlan plan(std::string script_name, State start, State goal) {
// BUG: could probably memoize here, since:
// same script+same start+same goal will/should produce the same results
check(initialized, "you forgot to initialize the AI first.");
auto script = load_script(script_name);
return plan_actions(script, start, goal);
}
int state_id(std::string name) {
check(AIMGR.profile.contains(name), fmt::format(
"ai::state_id({}): id is not configured in profile",
name));
return AIMGR.profile.at(name);
}
void set(State& state, std::string name, bool value) {
// resort by best fit
state.set(state_id(name), value);
}
bool test(State state, std::string name) {
return state.test(state_id(name));
}
void EntityAI::fit_sort() {
if(active()) {
std::sort(plan.script.begin(), plan.script.end(),
[&](auto& l, auto& r) {
int l_cost = l.cost + ai::distance_to_goal(start, goal);
int r_cost = r.cost + ai::distance_to_goal(start, goal);
return l_cost < r_cost;
});
}
}
std::string& EntityAI::wants_to() {
return plan.script[0].name;
}
bool EntityAI::wants_to(std::string name) {
ai::check_valid_action(name, "EntityAI::wants_to");
return plan.script.size() > 0 && plan.script[0].name == name;
}
bool EntityAI::active() {
if(plan.script.size() == 1) {
return plan.script[0] != FINAL_ACTION;
} else {
return plan.script.size() != 0;
}
}
void EntityAI::set_state(std::string name, bool setting) {
fit_sort();
ai::set(start, name, setting);
}
bool EntityAI::get_state(std::string name) {
return ai::test(start, name);
}
void EntityAI::update() {
plan = ai::plan(script, start, goal);
fit_sort();
}
AIProfile* profile() {
return &AIMGR.profile;
}
}

65
src/ai.hpp Normal file
View file

@ -0,0 +1,65 @@
#pragma once
#include <vector>
#include "matrix.hpp"
#include <bitset>
#include <limits>
#include <optional>
#include <nlohmann/json.hpp>
#include "config.hpp"
#include "goap.hpp"
namespace ai {
struct EntityAI {
std::string script;
ai::State start;
ai::State goal;
ai::ActionPlan plan;
EntityAI(std::string script, ai::State start, ai::State goal) :
script(script), start(start), goal(goal)
{
}
EntityAI() {};
bool wants_to(std::string name);
std::string& wants_to();
void fit_sort();
bool active();
void set_state(std::string name, bool setting);
bool get_state(std::string name);
void update();
void dump();
std::string to_string();
};
struct AIManager {
AIProfile profile;
std::unordered_map<std::string, Action> actions;
std::unordered_map<std::string, State> states;
std::unordered_map<std::string, std::vector<Action>> scripts;
};
/* This is really only used in test to load different fixtures. */
void reset();
void init(std::string config_path);
Action config_action(AIProfile& profile, nlohmann::json& config);
State config_state(AIProfile& profile, nlohmann::json& config);
int state_id(std::string name);
State load_state(std::string state_name);
Action load_action(std::string action_name);
std::vector<Action> load_script(std::string script_name);
void set(State& state, std::string name, bool value=true);
bool test(State state, std::string name);
ActionPlan plan(std::string script_name, State start, State goal);
/* Mostly used for debugging and validation. */
void check_valid_action(std::string name, std::string msg);
}

74
src/ai_debug.cpp Normal file
View file

@ -0,0 +1,74 @@
#include "ai.hpp"
#include "ai_debug.hpp"
namespace ai {
/*
* Yeah this is weird but it's only to debug things like
* the preconditions which are weirdly done.
*/
void dump_only(State state, bool matching, bool show_as) {
AIProfile* profile = ai::profile();
for(auto& [name, name_id] : *profile) {
if(state.test(name_id) == matching) {
fmt::println("\t{}={}", name, show_as);
}
}
}
void dump_state(State state) {
AIProfile* profile = ai::profile();
for(auto& [name, name_id] : *profile) {
fmt::println("\t{}={}", name,
state.test(name_id));
}
}
void dump_action(Action& action) {
fmt::println(" --ACTION: {}, cost={}", action.name, action.cost);
fmt::println(" PRECONDS:");
dump_only(action.$positive_preconds, true, true);
dump_only(action.$negative_preconds, true, false);
fmt::println(" EFFECTS:");
dump_only(action.$positive_effects, true, true);
dump_only(action.$negative_effects, true, false);
}
State dump_script(std::string msg, State start, Script& script) {
fmt::println("--SCRIPT DUMP: {}", msg);
fmt::println("# STATE BEFORE:");
dump_state(start);
fmt::print("% ACTIONS PLANNED:");
for(auto& action : script) {
fmt::print("{} ", action.name);
}
fmt::print("\n");
for(auto& action : script) {
dump_action(action);
start = action.apply_effect(start);
fmt::println(" ## STATE AFTER:");
dump_state(start);
}
return start;
}
void EntityAI::dump() {
dump_script(script, start, plan.script);
}
std::string EntityAI::to_string() {
AIProfile* profile = ai::profile();
std::string result = wants_to();
for(auto& [name, name_id] : *profile) {
result += fmt::format("\n{}={}", name, start.test(name_id));
}
return result;
}
}

10
src/ai_debug.hpp Normal file
View file

@ -0,0 +1,10 @@
#pragma once
#include "goap.hpp"
namespace ai {
AIProfile* profile();
void dump_only(State state, bool matching, bool show_as);
void dump_state(State state);
void dump_action(Action& action);
State dump_script(std::string msg, State start, Script& script);
}

314
src/animation.cpp Normal file
View file

@ -0,0 +1,314 @@
#include "animation.hpp"
#include <memory>
#include <chrono>
#include "dbc.hpp"
#include "rand.hpp"
#include <iostream>
#include <fstream>
#include "sound.hpp"
#include "components.hpp"
constexpr float SUB_FRAME_SENSITIVITY = 0.999f;
namespace animation {
using namespace std::chrono_literals;
std::vector<sf::IntRect> Animation::calc_frames() {
dbc::check(sequence.frames.size() == sequence.durations.size(), "sequence.frames.size() != sequence.durations.size()");
std::vector<sf::IntRect> frames;
for(int frame_i : sequence.frames) {
dbc::check(frame_i < sheet.frames, "frame index greater than sheet frames");
frames.emplace_back(
sf::Vector2i{sheet.frame_width * frame_i, 0}, // NOTE: one row only for now
sf::Vector2i{sheet.frame_width,
sheet.frame_height});
}
return frames;
}
void Animation::play() {
dbc::check(!playing, "can't call play while playing?");
sequence.current = 0;
sequence.subframe = 0.0f;
sequence.loop_count = 0;
playing = true;
sequence.timer.start();
sequence.INVARIANT();
}
void Animation::stop() {
playing = false;
sequence.timer.reset();
}
// need one for each kind of thing to animate
// NOTE: possibly find a way to only run apply on frame change?
void Animation::apply(sf::Sprite& sprite) {
dbc::check(sequence.current < $frame_rects.size(), "current frame past $frame_rects");
// NOTE: pos is not updated yet
auto& rect = $frame_rects.at(sequence.current);
sprite.setTextureRect(rect);
}
/*
* Alternative mostly used in raycaster.cpp that
* DOES NOT setTextureRect() but just points
* the rect_io at the correct frame, but leaves
* it's size and base position alone.
*/
void Animation::apply(sf::Sprite& sprite, sf::IntRect& rect_io) {
dbc::check(sequence.current < $frame_rects.size(), "current frame past $frame_rects");
auto& rect = $frame_rects.at(sequence.current);
rect_io.position.x += rect.position.x;
rect_io.position.y += rect.position.y;
}
void Animation::motion(sf::Transformable& sprite, sf::Vector2f pos, sf::Vector2f scale) {
sequence.INVARIANT();
transform.apply(sequence, pos, scale);
if(transform.flipped) {
scale.x *= -1;
}
sprite.setPosition(pos);
if(transform.scaled) {
sprite.setScale(scale);
}
}
void Animation::motion(sf::View& view_out, sf::Vector2f pos, sf::Vector2f size) {
dbc::check(size.x > 1.0f && size.y > 1.0f, "motion size must be above 1.0 since it's not a ratio");
dbc::check(transform.flipped == false, "transform must be false, has no effect on View");
sf::Vector2f scale{transform.min_x, transform.min_y};
transform.apply(sequence, pos, scale);
view_out.setCenter(pos);
if(transform.scaled) {
view_out.setSize({size.x * scale.x, size.y * scale.y});
} else {
view_out.setSize(size);
}
}
void Animation::apply_effect(std::shared_ptr<sf::Shader> effect) {
dbc::check(effect != nullptr, "can't apply null effect");
effect->setUniform("u_time", sequence.timer.getElapsedTime().asSeconds());
sf::Vector2f u_resolution{float(sheet.frame_width), float(sheet.frame_height)};
effect->setUniform("u_resolution", u_resolution);
}
void Animation::play_sound() {
// BUG: this can be optimized way better
if(sounds.contains(form_name)) {
for(auto& [at_frame, sound_name] : sounds.at(form_name)) {
if(sequence.current == at_frame) {
sound::play(sound_name);
}
}
} else {
fmt::println("Animation has not sound {}", form_name);
}
}
/* REFACTOR: I believe this is wrong still. If ::commit() determines number of ticks+alpha since last
* render then update needs to be called 1/tick. The Timer will keep track of alpha as the error
* between commit calls, so this function only really needs to care about ticks. But, I'm still
* calling getElapsedTime() when I already did that in commit(), so should I just ignore that and assume
* elapsed is DELTA, or use elapsed here?
*/
void Animation::update() {
dbc::check(playing, "attempt to update animation that's not playing");
sequence.INVARIANT();
auto [ticks, alpha] = sequence.timer.commit();
int duration = sequence.durations.at(sequence.current);
sequence.subframe += ticks;
sequence.easing_position += ticks;
bool frame_change = false;
if(sequence.subframe >= duration) {
sequence.timer.restart();
sequence.current++;
sequence.subframe = 0;
frame_change = true;
}
if(sequence.current >= sequence.frame_count) {
sequence.loop_count++;
sequence.easing_position = 0;
playing = onLoop(sequence, transform);
sequence.INVARIANT();
}
if(frame_change) play_sound();
if(frame_change && onFrame != nullptr) onFrame();
}
void Timer::start() {
clock.start();
prev_time = clock.getElapsedTime().asSeconds();
}
void Timer::reset() {
elapsed_ticks = 0;
clock.reset();
}
void Timer::restart() {
elapsed_ticks = 0;
clock.restart();
prev_time = clock.getElapsedTime().asSeconds();
}
sf::Time Timer::getElapsedTime() {
return clock.getElapsedTime();
}
std::pair<int, double> Timer::commit() {
// determine frame duration based on previous time
current_time = clock.getElapsedTime().asSeconds();
frame_duration = current_time - prev_time;
// update prev_time for the next call
prev_time = current_time;
// update accumulator, retaining previous errors
accumulator += frame_duration;
// find the tick count based on DELTA
double tick_count = floor(accumulator / DELTA);
// reduce accumulator by the number of DELTAS
accumulator -= tick_count * DELTA;
// that leaves the remaining errors for next loop
elapsed_ticks += tick_count;
// alpha is then what we lerp...but WHY?!
alpha = accumulator / DELTA;
// return the number of even DELTA ticks and the alpha
return {int(tick_count), alpha};
}
void Transform::apply(Sequence& seq, sf::Vector2f& pos_out, sf::Vector2f& scale_out) {
float tick = easing_func(seq.easing_position / seq.easing_duration);
motion_func(*this, pos_out, scale_out, tick, relative);
}
bool Animation::has_form(const std::string& as_form) {
return forms.contains(as_form);
}
void Animation::set_form(const std::string& as_form) {
dbc::check(forms.contains(as_form),
fmt::format("form {} does not exist in animation", as_form));
stop();
const auto& [seq_name, tr_name] = forms.at(as_form);
dbc::check(sequences.contains(seq_name),
fmt::format("sequences do NOT have \"{}\" name", seq_name));
dbc::check(transforms.contains(tr_name),
fmt::format("transforms do NOT have \"{}\" name", tr_name));
// everything good, do the update
form_name = as_form;
sequence_name = seq_name;
transform_name = tr_name;
sequence = sequences.at(seq_name);
transform = transforms.at(tr_name);
sequence.frame_count = sequence.frames.size();
// BUG: should this be configurable instead?
for(auto duration : sequence.durations) {
sequence.easing_duration += float(duration);
}
dbc::check(sequence.easing_duration > 0.0, "bad easing duration");
$frame_rects = calc_frames();
transform.easing_func = ease2::get_easing(transform.easing);
transform.motion_func = ease2::get_motion(transform.motion);
sequence.INVARIANT();
}
Animation load(const std::string &file, const std::string &anim_name) {
using nlohmann::json;
std::ifstream infile(file);
auto data = json::parse(infile);
dbc::check(data.contains(anim_name),
fmt::format("{} animation config does not have animation {}", file, anim_name));
Animation anim;
animation::from_json(data[anim_name], anim);
anim.name = anim_name;
dbc::check(anim.forms.contains("idle"),
fmt::format("animation {} must have 'idle' form", anim_name));
anim.set_form("idle");
return anim;
}
void Sequence::INVARIANT(const std::source_location location) {
dbc::check(frames.size() == durations.size(),
fmt::format("frames.size={} doesn't match durations.size={}",
frames.size(), durations.size()), location);
dbc::check(easing_duration > 0.0,
fmt::format("bad easing duration: {}", easing_duration), location);
dbc::check(frame_count == frames.size(),
fmt::format("frame_count={} doesn't match frames.size={}", frame_count, frames.size()), location);
dbc::check(frame_count == durations.size(),
fmt::format("frame_count={} doesn't match durations.size={}", frame_count, durations.size()), location);
dbc::check(current < durations.size(),
fmt::format("current={} went past end of fame durations.size={}",
current, durations.size()), location);
}
// BUG: BAAADD REMOVE
bool has(const std::string& name) {
using nlohmann::json;
std::ifstream infile("assets/animation.json");
auto data = json::parse(infile);
return data.contains(name);
}
void configure(DinkyECS::World& world, DinkyECS::Entity entity) {
auto sprite = world.get_if<components::Sprite>(entity);
if(sprite != nullptr && has(sprite->name)) {
world.set<Animation>(entity, animation::load("assets/animation.json", sprite->name));
}
}
void animate_entity(DinkyECS::World &world, DinkyECS::Entity entity) {
auto anim = world.get_if<Animation>(entity);
if(anim != nullptr && !anim->playing) {
anim->play();
}
}
}

150
src/animation.hpp Normal file
View file

@ -0,0 +1,150 @@
#pragma once
#include <memory>
#include <chrono>
#include <SFML/Graphics/Rect.hpp>
#include <SFML/Graphics/Sprite.hpp>
#include <SFML/Graphics/Shader.hpp>
#include <SFML/Graphics/View.hpp>
#include <SFML/System/Clock.hpp>
#include <SFML/System/Time.hpp>
#include <functional>
#include "easing.hpp"
#include <fmt/core.h>
#include "json_mods.hpp"
#include <source_location>
#include "dinkyecs.hpp"
namespace animation {
template <typename T> struct NameOf;
struct Sheet {
int frames{0};
int frame_width{0};
int frame_height{0};
};
struct Timer {
double DELTA = 1.0/60.0;
double accumulator = 0.0;
double prev_time = 0.0;
double current_time = 0.0;
double frame_duration = 0.0;
double alpha = 0.0;
int elapsed_ticks = 0;
sf::Clock clock{};
std::pair<int, double> commit();
void start();
void reset();
void restart();
sf::Time getElapsedTime();
};
struct Sequence {
std::vector<int> frames{};
std::vector<int> durations{}; // in ticks
size_t current{0};
int loop_count{0};
size_t frame_count{frames.size()};
Timer timer{};
int subframe{0};
float easing_duration{0.0f};
float easing_position{0.0f};
void INVARIANT(const std::source_location location = std::source_location::current());
};
struct Transform {
// how to know when a transform ends?
float min_x{1.0f};
float min_y{1.0f};
float max_x{1.0f};
float max_y{1.0f};
bool flipped{false};
bool scaled{false};
bool relative{false};
// handled by onLoop
bool toggled{false};
bool looped{false};
std::string easing{"in_out_back"};
std::string motion{"move_rush"};
// change to using a callback function for these
ease2::EaseFunc easing_func{ease2::get_easing(easing)};
ease2::MotionFunc motion_func{ease2::get_motion(motion)};
std::shared_ptr<sf::Shader> shader{nullptr};
void apply(Sequence& seq, sf::Vector2f& pos_out, sf::Vector2f& scale_out);
};
/* Gets the number of times it looped, and returns if it should stop. */
using OnLoopHandler = std::function<bool(Sequence& seq, Transform& tr)>;
using OnFrameHandler = std::function<void()>;
inline bool DefaultOnLoop(Sequence& seq, Transform& tr) {
if(tr.toggled) {
seq.current = seq.frame_count - 1;
} else {
seq.current = 0;
}
return tr.looped;
}
using Form = std::pair<std::string, std::string>;
using Sound = std::pair<size_t, std::string>;
class Animation {
public:
Sheet sheet;
std::unordered_map<std::string, Sequence> sequences;
std::unordered_map<std::string, Transform> transforms;
std::unordered_map<std::string, Form> forms;
std::unordered_map<std::string, std::vector<Sound>> sounds;
OnFrameHandler onFrame = nullptr;
Sequence sequence{};
Transform transform{};
std::vector<sf::IntRect> $frame_rects{calc_frames()};
OnLoopHandler onLoop = DefaultOnLoop;
bool playing = false;
// mostly for debugging purposes
std::string form_name="idle";
std::string sequence_name="";
std::string transform_name="";
std::string name="";
std::vector<sf::IntRect> calc_frames();
void play();
void play_sound();
void stop();
bool has_form(const std::string& as_form);
void set_form(const std::string& form);
void apply(sf::Sprite& sprite);
void apply(sf::Sprite& sprite, sf::IntRect& rect_io);
void apply_effect(std::shared_ptr<sf::Shader> effect);
void update();
void motion(sf::Transformable& sprite, sf::Vector2f pos, sf::Vector2f scale);
void motion(sf::View& view_out, sf::Vector2f pos, sf::Vector2f scale);
};
Animation load(const std::string &file, const std::string &anim_name);
// BUG: brought over from animation to finish the refactor, but these may not be needed or maybe they go in system.cpp?
bool has(const std::string& name);
void configure(DinkyECS::World& world, DinkyECS::Entity entity);
void animate_entity(DinkyECS::World &world, DinkyECS::Entity entity);
ENROLL_COMPONENT(Sheet, frames, frame_width, frame_height);
ENROLL_COMPONENT(Sequence, frames, durations);
ENROLL_COMPONENT(Transform, min_x, min_y, max_x, max_y,
flipped, scaled, relative, toggled, looped, easing, motion);
ENROLL_COMPONENT(Animation, sheet, sequences, transforms, forms, sounds);
}

461
src/autowalker.cpp Normal file
View file

@ -0,0 +1,461 @@
#include "autowalker.hpp"
#include "ai_debug.hpp"
#include "gui/ritual_ui.hpp"
#include "game_level.hpp"
#include "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_boss_fight() {
// skip the boss fight for now
if(fsm.in_state(gui::State::BOSS_FIGHT)) {
// eventually we'll have AI handle this too
send_event(game::Event::BOSS_END);
face_enemy();
}
}
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) ||
level.world->has<ritual::JunkPile>(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(fmt::format("Unknown action: {}", player_ai.to_string()));
}
}
void Autowalker::craft_weapon() {
if(!weapon_crafted) {
auto& ritual_ui = fsm.$status_ui.$ritual_ui;
fsm.$status_ui.$gui.click_on("ritual_ui");
while(!ritual_ui.in_state(gui::ritual::State::OPENED)) {
send_event(game::Event::TICK);
}
ritual_ui.$gui.click_on("inv_slot0");
send_event(game::Event::TICK);
ritual_ui.$gui.click_on("inv_slot1");
send_event(game::Event::TICK);
while(!ritual_ui.in_state(gui::ritual::State::CRAFTING)) {
send_event(game::Event::TICK);
}
ritual_ui.$gui.click_on("result_image", true);
send_event(game::Event::TICK);
ritual_ui.$gui.click_on("ritual_ui");
send_event(game::Event::TICK);
weapon_crafted = true;
}
}
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;
}
craft_weapon();
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_boss_fight();
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");
}
}
}

54
src/autowalker.hpp Normal file
View file

@ -0,0 +1,54 @@
#pragma once
#include "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;
bool weapon_crafted = 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 craft_weapon();
void open_map();
void close_map();
bool found_enemy();
bool found_item();
void handle_window_events();
void handle_boss_fight();
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);
};

78
src/backend.cpp Normal file
View file

@ -0,0 +1,78 @@
#include "backend.hpp"
#include "shaders.hpp"
#include "sound.hpp"
#include "textures.hpp"
#include "config.hpp"
#include "palette.hpp"
namespace sfml {
using namespace nlohmann;
guecs::SpriteTexture Backend::get_sprite(const string& name) {
auto sp = textures::get_sprite(name);
return {sp.sprite, sp.texture, sp.frame_size};
}
guecs::SpriteTexture Backend::get_icon(const string& name) {
auto sp = textures::get_icon(name);
return {sp.sprite, sp.texture, sp.frame_size};
}
Backend::Backend() {
sound::init();
shaders::init();
textures::init();
}
void Backend::sound_play(const string& name) {
sound::play(name);
}
void Backend::sound_stop(const string& name) {
sound::stop(name);
}
std::shared_ptr<sf::Shader> Backend::get_shader(const std::string& name) {
return shaders::get(name);
}
bool Backend::shader_updated() {
if(shaders::updated($shaders_version)) {
$shaders_version = shaders::version();
return true;
} else {
return false;
}
}
guecs::Theme Backend::theme() {
palette::init();
auto config = settings::Config("assets/config.json")["theme"];
guecs::Theme theme {
.BLACK=palette::get("gui/theme:black"),
.DARK_DARK=palette::get("gui/theme:dark_dark"),
.DARK_MID=palette::get("gui/theme:dark_mid"),
.DARK_LIGHT=palette::get("gui/theme:dark_light"),
.MID=palette::get("gui/theme:mid"),
.LIGHT_DARK=palette::get("gui/theme:light_dark"),
.LIGHT_MID=palette::get("gui/theme:light_mid"),
.LIGHT_LIGHT=palette::get("gui/theme:light_light"),
.WHITE=palette::get("gui/theme:white"),
.TRANSPARENT = palette::get("color:transparent")
};
theme.PADDING = config["padding"];
theme.BORDER_PX = config["border_px"];
theme.TEXT_SIZE = config["text_size"];
theme.LABEL_SIZE = config["label_size"];
theme.FILL_COLOR = palette::get("gui/theme:fill_color");
theme.TEXT_COLOR = palette::get("gui/theme:text_color");
theme.BG_COLOR = palette::get("gui/theme:bg_color");
theme.BORDER_COLOR = palette::get("gui/theme:border_color");
theme.BG_COLOR_DARK = palette::get("gui/theme:bg_color_dark");
theme.FONT_FILE_NAME = settings::Config::path_to(config["font_file_name"]).string();
return theme;
}
}

20
src/backend.hpp Normal file
View file

@ -0,0 +1,20 @@
#include "guecs/ui.hpp"
namespace sfml {
using std::string;
class Backend : public guecs::Backend {
int $shaders_version = 0;
public:
Backend();
guecs::SpriteTexture get_sprite(const string& name);
guecs::SpriteTexture get_icon(const string& name);
void sound_play(const string& name);
void sound_stop(const string& name);
std::shared_ptr<sf::Shader> get_shader(const std::string& name);
bool shader_updated();
guecs::Theme theme();
};
}

133
src/battle.cpp Normal file
View file

@ -0,0 +1,133 @@
#include "rituals.hpp"
#include "battle.hpp"
namespace combat {
void BattleEngine::add_enemy(Combatant enemy) {
$combatants.try_emplace(enemy.entity, enemy);
if(enemy.is_host) {
dbc::check($host_combat == nullptr, "added the host twice!");
$host_combat = enemy.combat;
}
}
bool BattleEngine::player_request(const std::string& request) {
auto action = ai::load_action(request);
bool can_go = player_pending_ap() >= action.cost;
if(can_go) {
$player_requests.try_emplace(request, action);
}
return can_go;
}
void BattleEngine::clear_requests() {
$player_requests.clear();
}
void BattleEngine::ap_refresh() {
for(auto& [entity, enemy] : $combatants) {
if(enemy.combat->ap < enemy.combat->max_ap) {
int new_ap = std::min(enemy.combat->max_ap, enemy.combat->ap_delta + enemy.combat->ap);
// only add up to the max
fmt::println("enemy {} get more ap {}->{}",
entity, enemy.combat->ap, new_ap);
enemy.combat->ap = new_ap;
}
}
}
int BattleEngine::player_pending_ap() {
dbc::check($host_combat != nullptr, "didn't set host before checking AP");
int pending_ap = $host_combat->ap;
for(auto& [name, action] : $player_requests) {
pending_ap -= action.cost;
}
return pending_ap;
}
bool BattleEngine::plan() {
using enum BattleHostState;
int active = 0;
bool had_host = false;
for(auto& [entity, enemy] : $combatants) {
//NOTE: this is just for asserting I'm using things right
if(enemy.is_host) had_host = true;
enemy.ai->update();
active += enemy.ai->active();
if(enemy.ai->active()) {
for(auto& action : enemy.ai->plan.script) {
BattleHostState host_state = not_host;
if(action.cost > enemy.combat->ap) {
host_state = out_of_ap;
} else if(enemy.is_host) {
host_state = $player_requests.contains(action.name) ? agree : disagree;
}
if(host_state != out_of_ap) {
enemy.combat->ap -= action.cost;
}
$pending_actions.emplace_back(enemy, action.name, action.cost, host_state);
}
dbc::check(enemy.combat->ap >= 0, "enemy's AP went below 0");
dbc::check(enemy.combat->ap <= enemy.combat->max_ap, "enemy's AP went above max");
}
}
dbc::check(had_host, "FAIL, you forgot to set enemy.is_host=true for one entity");
if($pending_actions.size() > 0) {
std::sort($pending_actions.begin(), $pending_actions.end(),
[](const auto& a, const auto& b) -> bool
{
return a.cost > b.cost;
});
}
return active > 0;
}
std::optional<BattleResult> BattleEngine::next() {
if($pending_actions.size() == 0) return std::nullopt;
auto ba = $pending_actions.back();
$pending_actions.pop_back();
return std::make_optional(ba);
}
void BattleEngine::dump() {
for(auto& [entity, enemy] : $combatants) {
fmt::println("\n\n###### ENTITY #{}", entity);
enemy.ai->dump();
}
}
void BattleEngine::set(DinkyECS::Entity entity, const std::string& state, bool setting) {
dbc::check($combatants.contains(entity), "invalid combatant given to BattleEngine");
auto& action = $combatants.at(entity);
action.ai->set_state(state, setting);
}
void BattleEngine::set_all(const std::string& state, bool setting) {
for(auto& [ent, action] : $combatants) {
action.ai->set_state(state, setting);
}
}
Combatant& BattleEngine::get_enemy(DinkyECS::Entity entity) {
dbc::check($combatants.contains(entity), "invalid combatant given to BattleEngine");
return $combatants.at(entity);
}
}

50
src/battle.hpp Normal file
View file

@ -0,0 +1,50 @@
#pragma once
#include "rituals.hpp"
#include "config.hpp"
#include "dinkyecs.hpp"
#include <optional>
#include "components.hpp"
#include <unordered_map>
namespace combat {
enum class BattleHostState {
not_host = 0,
agree = 1,
disagree = 2,
out_of_ap = 3
};
struct Combatant {
DinkyECS::Entity entity = DinkyECS::NONE;
ai::EntityAI* ai = nullptr;
components::Combat* combat = nullptr;
bool is_host=false;
};
struct BattleResult {
Combatant enemy;
std::string wants_to;
int cost;
BattleHostState host_state;
};
struct BattleEngine {
std::unordered_map<DinkyECS::Entity, Combatant> $combatants;
std::vector<BattleResult> $pending_actions;
std::unordered_map<std::string, ai::Action> $player_requests;
components::Combat* $host_combat = nullptr;
void add_enemy(Combatant ba);
Combatant& get_enemy(DinkyECS::Entity entity);
bool plan();
std::optional<BattleResult> next();
void dump();
void set(DinkyECS::Entity entity, const std::string& state, bool setting);
void set_all(const std::string& state, bool setting);
bool player_request(const std::string& request);
int player_pending_ap();
void clear_requests();
void ap_refresh();
};
}

273
src/boss/fight.cpp Normal file
View file

@ -0,0 +1,273 @@
#define FSM_DEBUG 1
#include "boss/fight.hpp"
#include "boss/system.hpp"
#include "game_level.hpp"
#include <iostream>
#include "rand.hpp"
#include "events.hpp"
namespace boss {
using namespace DinkyECS;
Fight::Fight(shared_ptr<World> world, Entity boss_id, Entity player_id) :
$world(world),
$boss_id(boss_id),
$battle(System::create_battle($world, $boss_id)),
$ui(world, boss_id, player_id),
$host(player_id)
{
$host_combat = $world->get_if<components::Combat>(player_id);
dbc::check($host_combat,
fmt::format("No combat for host with player_id={}", player_id));
$ui.init();
}
void Fight::event(game::Event ev, std::any data) {
// if the mouse event is handled the done, this may be wrong later
if(handle_mouse(ev)) return;
switch($state) {
FSM_STATE(State, START, ev, data);
FSM_STATE(State, PLAYER_REQUESTS, ev, data);
FSM_STATE(State, EXEC_PLAN, ev, data);
FSM_STATE(State, ANIMATE, ev, data);
FSM_STATE(State, END, ev, data);
}
}
void Fight::START(game::Event ev, std::any) {
using enum game::Event;
switch(ev) {
case COMBAT_START:
$ui.status(L"PLAYER REQUESTS", L"COMMIT");
$battle.ap_refresh();
state(State::PLAYER_REQUESTS);
break;
default:
break;
}
}
void Fight::PLAYER_REQUESTS(game::Event ev, std::any data) {
using enum game::Event;
switch(ev) {
// this is only if using the debug X key to skip it
case BOSS_END:
state(State::END);
break;
case COMBAT_START:
System::plan_battle($battle, $world, $boss_id);
$ui.status(L"EXEC PLAN", L"START");
state(State::EXEC_PLAN);
break;
case ATTACK:
if($battle.player_request("kill_enemy")) {
fmt::println("player requests kill_enemy {} vs. {}",
$host_combat->ap, $battle.player_pending_ap());
} else {
fmt::println("NO MORE ACTION!");
}
break;
default:
break;
}
}
void Fight::EXEC_PLAN(game::Event ev, std::any data) {
using enum game::Event;
switch(ev) {
// this is only if using the debug X key to skip it
case BOSS_END:
state(State::END);
break;
case COMBAT_START:
$ui.status(L"EXEC PLAN", L"ANIMATE");
next_combat();
break;
case COMBAT:
do_combat(data);
state(State::ANIMATE);
break;
default:
break;
}
}
void Fight::ANIMATE(game::Event ev, std::any data) {
using enum game::Event;
switch(ev) {
case ANIMATION_END:
$ui.status(L"ANIMATE END", L"NEXT");
next_combat();
break;
default:
// ignored
break;
}
}
void Fight::END(game::Event ev, std::any) {
fmt::println("BOSS_FIGHT:END event {}", (int)ev);
}
void Fight::next_combat() {
if(auto action = $battle.next()) {
fmt::println("next combat has action");
System::combat(*action, $world, $boss_id, 0);
state(State::EXEC_PLAN);
} else if(player_dead()) {
fmt::println("player died");
$ui.status(L"YOU DIED", L"DEAD");
state(State::END);
} else {
fmt::println("end combat next turn");
$ui.status(L"PLAYER REQUESTS", L"COMMIT");
$ui.reset_camera();
$battle.ap_refresh();
$battle.clear_requests();
state(State::PLAYER_REQUESTS);
}
}
void Fight::do_combat(std::any data) {
using combat::BattleHostState;
dbc::check(data.type() == typeid(components::CombatResult),
fmt::format("Boss Fight wrong any data={}", data.type().name()));
auto result = std::any_cast<components::CombatResult>(data);
switch(result.host_state) {
case BattleHostState::agree:
case BattleHostState::disagree: {
std::string player_move = Random::uniform(0, 1) == 0 ? "player1" : "player3";
$ui.move_actor("player", player_move);
if(result.player_did > 0) {
$ui.animate_actor("player", "attack");
} else {
// NO need a no animation event
$ui.animate_actor("player", "idle");
}
$ui.damage("player", "boss", result.player_did);
} break;
case BattleHostState::not_host: {
std::string boss_move = Random::uniform(0, 1) == 0 ? "boss5" : "boss6";
$ui.move_actor("boss", boss_move);
if(result.enemy_did > 0) {
$ui.animate_actor("boss", "attack");
} else {
// NO need a no animation event
$ui.animate_actor("boss", "idle");
}
$ui.damage("boss", "player", result.enemy_did);
} break;
case BattleHostState::out_of_ap:
// NO need an out of AP animation
$ui.animate_actor("player", "idle");
break;
}
}
void Fight::run_systems() {
$ui.update_stats();
$battle.set($host, "tough_personality", false);
$battle.set($host, "have_healing", false);
$battle.set($host, "health_good", $host_combat->hp > 100);
}
void Fight::render(sf::RenderWindow& window) {
$ui.render();
}
void Fight::update() {
$ui.update();
}
bool Fight::handle_mouse(game::Event ev) {
using enum game::Event;
$mouse_pos = $window->mapPixelToCoords($router.position);
switch(ev) {
case MOUSE_CLICK: {
$ui.mouse($mouse_pos.x, $mouse_pos.y, guecs::NO_MODS);
} break;
case MOUSE_MOVE: {
$ui.mouse($mouse_pos.x, $mouse_pos.y, {1 << guecs::ModBit::hover});
} break;
case MOUSE_DRAG:
dbc::log("mouse drag");
break;
case MOUSE_DRAG_START:
dbc::log("mouse START drag");
break;
case MOUSE_DROP:
dbc::log("mouse DROP");
break;
default:
return false;
}
// switch/default didn't happen so handled it.
return true;
}
bool Fight::player_dead() {
return $host_combat->hp <= 0;
}
void Fight::init_fight() {
System::initialize_actor_ai(*$world, $boss_id);
run_systems();
}
void Fight::set_window(sf::RenderWindow* window) {
$window = window;
$ui.set_window(window);
}
bool Fight::handle_keyboard_mouse() {
dbc::check($window != nullptr, "you didn't set_window");
while(const auto ev = $window->pollEvent()) {
auto gui_ev = $router.process_event(ev);
if(gui_ev == game::Event::KEY_PRESS) {
using KEY = sf::Keyboard::Scan;
switch($router.scancode) {
// REALLY? just go to state end or use another event
case KEY::X:
event(game::Event::BOSS_END, {});
break;
default:
fmt::println("key press!");
}
} else if(gui_ev == game::Event::QUIT) {
return true;
} else {
event(gui_ev, {});
}
}
return in_state(State::END);
}
bool Fight::handle_world_events() {
if($world->has_event<game::Event>()) {
while($world->has_event<game::Event>()) {
auto [evt, entity, data] = $world->recv<game::Event>();
event(game::Event(evt), data);
run_systems();
}
}
return in_state(State::END);
}
}

61
src/boss/fight.hpp Normal file
View file

@ -0,0 +1,61 @@
#pragma once
#include "simplefsm.hpp"
#include "dinkyecs.hpp"
#include "boss/ui.hpp"
#include "events.hpp"
#include "battle.hpp"
#include "gui/event_router.hpp"
#include <memory>
#include <any>
namespace boss {
using std::shared_ptr;
enum class State {
START=__LINE__,
PLAYER_REQUESTS=__LINE__,
EXEC_PLAN=__LINE__,
ANIMATE=__LINE__,
END=__LINE__
};
class Fight : public DeadSimpleFSM<State, game::Event> {
public:
sf::RenderWindow* $window = nullptr;
shared_ptr<DinkyECS::World> $world = nullptr;
DinkyECS::Entity $boss_id = DinkyECS::NONE;
combat::BattleEngine $battle;
boss::UI $ui;
DinkyECS::Entity $host = DinkyECS::NONE;
components::Combat* $host_combat = nullptr;
gui::routing::Router $router{};
sf::Vector2f $mouse_pos{};
Fight(shared_ptr<DinkyECS::World> world,
DinkyECS::Entity boss_id,
DinkyECS::Entity player_id);
void set_window(sf::RenderWindow* window);
bool handle_mouse(game::Event ev);
void event(game::Event ev, std::any data);
void START(game::Event ev, std::any data);
void PLAYER_REQUESTS(game::Event ev, std::any data);
void EXEC_PLAN(game::Event ev, std::any data);
void ANIMATE(game::Event ev, std::any data);
void END(game::Event ev, std::any data);
void render(sf::RenderWindow& window);
void update();
bool handle_world_events();
void run_systems();
bool player_dead();
void init_fight();
void do_combat(std::any data);
void next_combat();
sf::Vector2f mouse_position();
bool handle_keyboard_mouse();
};
}

140
src/boss/system.cpp Normal file
View file

@ -0,0 +1,140 @@
#include "boss/system.hpp"
#include <fmt/core.h>
#include "components.hpp"
#include "game_level.hpp"
#include "ai.hpp"
#include "battle.hpp"
namespace boss {
using namespace components;
using namespace combat;
void System::load_config() {
fmt::println("load it");
}
void System::initialize_actor_ai(DinkyECS::World& world, DinkyECS::Entity entity_id) {
dbc::check(world.has<EnemyConfig>(entity_id), "boss doesn't have an AI EnemyConfig");
auto& config = world.get<EnemyConfig>(entity_id);
auto ai_start = ai::load_state(config.ai_start_name);
auto ai_goal = ai::load_state(config.ai_goal_name);
ai::EntityAI boss_ai(config.ai_script, ai_start, ai_goal);
boss_ai.set_state("enemy_found", true);
boss_ai.set_state("in_combat", true);
boss_ai.set_state("tough_personality", true);
boss_ai.set_state("health_good", true);
world.set<ai::EntityAI>(entity_id, boss_ai);
}
shared_ptr<boss::Fight> System::create_bossfight() {
// need to copy so we can clone it and keep things working, even
// if we don't use things like map and lighting
auto level = GameDB::current_level();
dbc::check(level.world != nullptr, "Starter world for boss fights can't be null.");
level.world = GameDB::clone_load_world(level.world);
auto& config = level.world->get_the<GameConfig>();
auto boss_names = config.bosses.keys();
auto& level_name = boss_names[level.index % boss_names.size()];
auto& boss_data = config.bosses[level_name];
auto boss_id = level.world->entity();
components::configure_entity(*level.world, boss_id, boss_data["components"]);
initialize_actor_ai(*level.world, boss_id);
dbc::check(level.world->has<ai::EntityAI>(boss_id), "boss doesn't have an AI");
initialize_actor_ai(*level.world, level.player);
dbc::check(level.world->has<ai::EntityAI>(level.player), "player/host doesn't have an AI");
GameDB::register_level(level);
return make_shared<boss::Fight>(level.world, boss_id, level.player);
}
BattleEngine System::create_battle(std::shared_ptr<DinkyECS::World> world, DinkyECS::Entity boss_id) {
auto& level = GameDB::current_level();
auto player_combat = world->get_if<Combat>(level.player);
dbc::check(player_combat != nullptr, "No Combat for player.");
auto boss_combat = world->get_if<Combat>(boss_id);
dbc::check(boss_combat != nullptr, "No Combat for Boss.");
// BUG: should I reset AP here?
player_combat->ap = player_combat->max_ap;
boss_combat->ap = boss_combat->max_ap;
auto boss_ai = world->get_if<ai::EntityAI>(boss_id);
dbc::check(boss_ai != nullptr, "boss doesn't have an AI");
auto host_ai = world->get_if<ai::EntityAI>(boss_id);
dbc::check(host_ai != nullptr, "host doesn't have an AI");
BattleEngine battle;
battle.add_enemy({boss_id, boss_ai, boss_combat, false});
battle.add_enemy({level.player, host_ai, player_combat, true});
battle.set_all("enemy_found", true);
battle.set_all("in_combat", true);
battle.set(boss_id, "tough_personality", true);
return battle;
}
void System::plan_battle(BattleEngine& battle, std::shared_ptr<DinkyECS::World> world, DinkyECS::Entity boss_id) {
// BUG: make this loop the list of entities in the battle then
// use their world state to configure the plan
battle.plan();
}
void System::combat(BattleResult& action, std::shared_ptr<DinkyECS::World> world, DinkyECS::Entity boss_id, int attack_id) {
fmt::println("COMBAT >>> enemy={}, wants_to={}, cost={}, host_state={}",
int(action.enemy.entity), action.wants_to, action.cost, int(action.host_state));
auto& level = GameDB::current_level();
auto& player_combat = world->get<Combat>(level.player);
auto& boss_combat = world->get<Combat>(boss_id);
auto& [enemy, wants_to, cost, host_state] = action;
components::CombatResult result {
.attacker=enemy.entity,
.host_state=host_state,
.player_did=0,
.enemy_did=0
};
switch(host_state) {
case BattleHostState::agree:
// BUG: this is hard coding only one boss, how to select targets?
if(wants_to == "kill_enemy") {
result.player_did = player_combat.attack(boss_combat);
}
break;
case BattleHostState::disagree:
fmt::println("HOST DISAGREES! {}", wants_to);
if(wants_to == "kill_enemy") {
result.player_did = player_combat.attack(boss_combat);
}
break;
case BattleHostState::not_host:
dbc::log("kill_enemy");
if(wants_to == "kill_enemy") {
result.enemy_did = enemy.combat->attack(player_combat);
}
break;
case BattleHostState::out_of_ap:
fmt::println("OUT OF AP {}", wants_to);
break;
}
world->send<game::Event>(game::Event::COMBAT, enemy.entity, result);
}
}

20
src/boss/system.hpp Normal file
View file

@ -0,0 +1,20 @@
#pragma once
#include "dinkyecs.hpp"
#include <memory>
#include "boss/fight.hpp"
namespace boss {
namespace System {
void load_config();
std::shared_ptr<boss::Fight> create_bossfight();
void initialize_actor_ai(DinkyECS::World& world, DinkyECS::Entity boss_id);
combat::BattleEngine create_battle(std::shared_ptr<DinkyECS::World> world, DinkyECS::Entity boss_id);
void plan_battle(combat::BattleEngine& battle, std::shared_ptr<DinkyECS::World> world, DinkyECS::Entity boss_id);
void combat(combat::BattleResult& action, std::shared_ptr<DinkyECS::World> world, DinkyECS::Entity boss_id, int attack_id);
}
}

130
src/boss/ui.cpp Normal file
View file

@ -0,0 +1,130 @@
#include "boss/ui.hpp"
#include "scene.hpp"
#include "constants.hpp"
#include "components.hpp"
#include <fmt/xchar.h>
#include "game_level.hpp"
#include "gui/guecstra.hpp"
#include "events.hpp"
namespace boss {
using namespace guecs;
using namespace DinkyECS;
UI::UI(shared_ptr<World> world, Entity boss_id, Entity player_id) :
$world(world),
$boss_id(boss_id),
$player_id(player_id),
$combat_ui(true),
$arena(world->get<components::AnimatedScene>($boss_id)),
$view_texture({BOSS_VIEW_WIDTH, BOSS_VIEW_HEIGHT}),
$view_sprite($view_texture.getTexture())
{
$arena.set_end_cb([&]() {
$world->send<game::Event>(game::Event::ANIMATION_END, DinkyECS::NONE, {});
});
$view_sprite.setPosition({BOSS_VIEW_X, BOSS_VIEW_Y});
}
void UI::init() {
$arena.init();
$actions.position(0,0, SCREEN_WIDTH-BOSS_VIEW_WIDTH, SCREEN_HEIGHT);
$actions.layout(
"[*%(100,400)combat]"
"[_]"
"[_]"
"[_]"
"[commit]"
"[*%(100,300)stats]"
"[_]"
"[_]");
auto commit = $actions.entity("commit");
$actions.set<Rectangle>(commit, {});
$actions.set<Text>(commit, {L"COMMIT"});
$actions.set<Effect>(commit, {});
$actions.set<Clickable>(commit,
guecs::make_action(commit, game::Event::COMBAT_START, {}));
auto stats = $actions.entity("stats");
$actions.set<Rectangle>(stats, {});
update_stats();
$actions.init();
auto& cell = $actions.cell_for("combat");
$combat_ui.init(cell.x, cell.y, cell.w, cell.h);
}
void UI::update_stats() {
auto& player_combat = $world->get<components::Combat>($player_id);
auto& boss_combat = $world->get<components::Combat>($boss_id);
std::wstring status = fmt::format(
L"--PLAYER--\nHP:{}/{}\nAP:{}/{}\n\n--BOSS--\nHP:{}/{}\nAP:{}/{}\n----\n",
player_combat.hp, player_combat.max_hp,
player_combat.ap, player_combat.max_ap,
boss_combat.hp, boss_combat.max_hp,
boss_combat.ap, boss_combat.max_ap);
$actions.show_text("stats", status);
}
void UI::set_window(sf::RenderWindow* window) {
$window = window;
}
void UI::render() {
$actions.render(*$window);
$combat_ui.render(*$window);
$arena.render($view_texture);
$view_texture.display();
$window->draw($view_sprite);
}
void UI::update() {
$arena.update();
}
bool UI::mouse(float x, float y, Modifiers mods) {
// BUG: arena is getting the _window_ coordinates, not the rendertexture
return $combat_ui.mouse(x, y, mods)
|| $actions.mouse(x, y, mods) || $arena.mouse(x, y, mods);
}
void UI::status(const std::wstring& msg, const std::wstring &button_msg) {
$arena.$ui.show_text("status", msg);
$actions.show_text("commit", button_msg);
}
void UI::move_actor(const std::string& actor, const std::string& cell_name) {
$arena.move_actor(actor, cell_name);
}
void UI::animate_actor(const std::string& actor, const std::string& form) {
$arena.animate_actor(actor, form);
}
void UI::damage(const string& actor, const std::string& target, int amount) {
if(amount > 0) {
$arena.attach_text(target, fmt::format("{}", amount));
$arena.apply_effect(actor, "lightning");
// USING SCALE
float scale = 0.8f;
std::string style = "pan";
$arena.zoom(target, style, scale);
} else {
$arena.attach_text(actor, "MISSED");
}
}
void UI::reset_camera() {
$arena.reset($view_texture);
}
}

44
src/boss/ui.hpp Normal file
View file

@ -0,0 +1,44 @@
#pragma once
#include <memory>
#include <guecs/ui.hpp>
#include "gui/combat_ui.hpp"
#include "scene.hpp"
namespace components {
struct Animation;
}
// needed to break an include cycle
namespace GameDB {
struct Level;
}
namespace boss {
using std::shared_ptr;
struct UI {
sf::RenderWindow* $window = nullptr;
shared_ptr<DinkyECS::World> $world = nullptr;
DinkyECS::Entity $boss_id = DinkyECS::NONE;
DinkyECS::Entity $player_id = DinkyECS::NONE;
gui::CombatUI $combat_ui;
scene::Engine $arena;
guecs::UI $actions;
sf::RenderTexture $view_texture;
sf::Sprite $view_sprite;
UI(shared_ptr<DinkyECS::World> world, DinkyECS::Entity boss_id, DinkyECS::Entity player_id);
void init();
void set_window(sf::RenderWindow* window);
void render();
void update();
bool mouse(float x, float y, guecs::Modifiers mods);
void status(const std::wstring& msg, const std::wstring &button_msg);
void move_actor(const std::string& actor, const std::string& cell_name);
void animate_actor(const std::string& actor, const std::string& form);
void update_stats();
void damage(const std::string& actor, const std::string& target, int amount);
void reset_camera();
};
}

132
src/camera.cpp Normal file
View file

@ -0,0 +1,132 @@
#include "camera.hpp"
#include <unordered_map>
#include "components.hpp"
#include "config.hpp"
#include <algorithm>
#include <iostream>
#include <cstdlib>
namespace cinematic {
using animation::Animation, std::string, std::min, std::clamp;
struct CameraManager {
std::unordered_map<string, Animation> animations;
};
static CameraManager MGR;
static bool initialized = false;
void init() {
if(!initialized) {
// BUG: it should be that you give a camera to load by name, not just one for all cameras
auto data = settings::get("cameras");
for(auto [key, value] : data.json().items()) {
auto anim = components::convert<Animation>(value);
MGR.animations.try_emplace(key, anim);
}
initialized = true;
}
}
Camera::Camera(sf::Vector2f size, const std::string &name) :
anim(MGR.animations.at(name)),
size(size),
base_size(size),
aimed_at{size.x/2, size.y/2},
going_to{size.x/2, size.y/2},
camera_bounds{{0,0}, size},
view{aimed_at, size}
{
anim.sheet.frame_width = base_size.x;
anim.sheet.frame_height = base_size.y;
}
void Camera::update_camera_bounds(sf::Vector2f size) {
// camera bounds now constrains the x/y so that the mid-point
// of the size won't go too far outside of the frame
camera_bounds = {
{size.x / 2.0f, size.y / 2.0f},
{base_size.x - size.x / 2.0f, base_size.y - size.y / 2.0f}
};
}
void Camera::scale(float ratio) {
size.x = base_size.x * ratio;
size.y = base_size.y * ratio;
update_camera_bounds(size);
}
void Camera::resize(float width) {
dbc::check(width <= base_size.x, "invalid width for camera");
size.x = width;
size.y = base_size.y * (width / base_size.x);
update_camera_bounds(size);
}
void Camera::style(const std::string &name) {
anim.set_form(name);
}
void Camera::position(float x, float y) {
aimed_at.x = clamp(x, camera_bounds.position.x, camera_bounds.size.x);
aimed_at.y = clamp(y, camera_bounds.position.y, camera_bounds.size.y);
}
void Camera::move(float x, float y) {
going_to.x = clamp(x, camera_bounds.position.x, camera_bounds.size.x);
going_to.y = clamp(y, camera_bounds.position.y, camera_bounds.size.y);
if(!anim.transform.relative) {
anim.transform.min_x = aimed_at.x;
anim.transform.min_y = aimed_at.y;
anim.transform.max_x = going_to.x;
anim.transform.max_y = going_to.y;
}
}
void Camera::reset(sf::RenderTexture& target) {
size = {base_size.x, base_size.y};
aimed_at = {base_size.x/2, base_size.y/2};
going_to = {base_size.x/2, base_size.y/2};
view = {aimed_at, size};
camera_bounds = {{0,0}, base_size};
// BUG: is getDefaultView different from view?
target.setView(target.getDefaultView());
}
void Camera::render(sf::RenderTexture& target) {
if(anim.playing) {
anim.motion(view, going_to, size);
target.setView(view);
}
}
void Camera::update() {
if(anim.playing) anim.update();
}
bool Camera::playing() {
return anim.playing;
}
void Camera::play() {
anim.play();
}
void Camera::from_story(components::Storyboard& story) {
anim.sequences.clear();
anim.forms.clear();
for(auto& [timecode, cell, transform, duration] : story.beats) {
animation::Sequence seq{.frames={0}, .durations={std::stoi(duration)}};
anim.sequences.try_emplace(timecode, seq);
animation::Form form{timecode, transform};
anim.forms.try_emplace(timecode, form);
}
}
}

37
src/camera.hpp Normal file
View file

@ -0,0 +1,37 @@
#pragma once
#include "animation.hpp"
#include "constants.hpp"
#include <SFML/Graphics/RenderTexture.hpp>
namespace components {
struct Storyboard;
}
namespace cinematic {
struct Camera {
animation::Animation anim;
sf::Vector2f size{SCREEN_WIDTH, SCREEN_HEIGHT};
sf::Vector2f base_size{SCREEN_WIDTH, SCREEN_HEIGHT};
sf::Vector2f aimed_at{0,0};
sf::Vector2f going_to{0,0};
sf::FloatRect camera_bounds{{0,0},{SCREEN_WIDTH, SCREEN_HEIGHT}};
sf::View view;
Camera(sf::Vector2f size, const std::string &name);
void resize(float width);
void scale(float ratio);
void position(float x, float y);
void move(float x, float y);
bool playing();
void update();
void render(sf::RenderTexture& target);
void play();
void style(const std::string &name);
void reset(sf::RenderTexture& target);
void update_camera_bounds(sf::Vector2f size);
void from_story(components::Storyboard& story);
};
void init();
}

16
src/combat.cpp Normal file
View file

@ -0,0 +1,16 @@
#include "components.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;
}
}

36
src/components.cpp Normal file
View file

@ -0,0 +1,36 @@
#include "components.hpp"
#include "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(), fmt::format("component has no _type: {}", data.dump()));
dbc::check(MAP.contains(i["_type"]), fmt::format("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;
}
}
}

196
src/components.hpp Normal file
View file

@ -0,0 +1,196 @@
#pragma once
#include "config.hpp"
#include "constants.hpp"
#include "dinkyecs.hpp"
#include "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 "json_mods.hpp"
#include "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;
settings::Config bosses;
settings::Config rituals;
settings::Config stories;
};
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);
}

67
src/config.cpp Normal file
View file

@ -0,0 +1,67 @@
#include "config.hpp"
#include "dbc.hpp"
#include <fmt/core.h>
namespace settings {
using nlohmann::json;
using fmt::format;
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),
fmt::format("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), fmt::format("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), fmt::format("ERROR wstring main/key in config, main_key {} doesn't exist.", main_key));
dbc::check($config[main_key].contains(sub_key), fmt::format("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), fmt::format(
"config file {} does not exist", path.string()));
return {path.string()};
}
}
}

29
src/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);
}

83
src/constants.hpp Normal file
View file

@ -0,0 +1,83 @@
#pragma once
#include <string>
#include <array>
constexpr const int INV_SLOTS=16;
constexpr const int TEXTURE_WIDTH=256;
constexpr const int TEXTURE_HEIGHT=256;
constexpr const int RAY_VIEW_WIDTH=900;
constexpr const int RAY_VIEW_HEIGHT=600;
constexpr const int SCREEN_WIDTH=1280;
constexpr const int SCREEN_HEIGHT=720;
constexpr const int RAY_VIEW_X=(SCREEN_WIDTH - RAY_VIEW_WIDTH);
constexpr const int RAY_VIEW_Y=0;
constexpr const int GLOW_LIMIT=220;
constexpr const int LIGHT_MULTIPLIER=2.5;
constexpr const float AIMED_AT_BRIGHTNESS=0.2f;
constexpr const int MAP_TILE_DIM=64;
constexpr const int ICONGEN_MAP_TILE_DIM=64;
constexpr const int PLAYER_SPRITE_DIR_CORRECTION=270;
constexpr const int RENDER_DISTANCE=500;
constexpr const int BOSS_VIEW_WIDTH=1080;
constexpr const int BOSS_VIEW_HEIGHT=SCREEN_HEIGHT;
constexpr const int BOSS_VIEW_X=SCREEN_WIDTH - BOSS_VIEW_WIDTH;
constexpr const int BOSS_VIEW_Y=0;
constexpr const bool VSYNC=false;
constexpr const int FRAME_LIMIT=60;
constexpr const int NUM_SPRITES=1;
constexpr const int MAX_LOG_MESSAGES=17;
#ifdef NDEBUG
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 UI_FONT_SIZE=20;
constexpr int BASE_MAP_FONT_SIZE=80;
constexpr int GAME_MAP_PIXEL_POS = 600;
constexpr int MAX_FONT_SIZE = 140;
constexpr int MIN_FONT_SIZE = 20;
constexpr float PERCENT = 0.01f;
constexpr int STATUS_UI_X = 0;
constexpr int STATUS_UI_Y = 0;
constexpr int STATUS_UI_WIDTH = SCREEN_WIDTH - RAY_VIEW_WIDTH;
constexpr int STATUS_UI_HEIGHT = SCREEN_HEIGHT;
constexpr int COMBAT_UI_X = RAY_VIEW_X;
constexpr int COMBAT_UI_Y = RAY_VIEW_HEIGHT;
constexpr int COMBAT_UI_WIDTH = RAY_VIEW_WIDTH ;
constexpr int COMBAT_UI_HEIGHT = SCREEN_HEIGHT - RAY_VIEW_HEIGHT;
constexpr int INITIAL_MAP_W = 17;
constexpr int INITIAL_MAP_H = 15;
constexpr float DEFAULT_ROTATE=0.25f;
// for the panels/renderer
constexpr wchar_t BG_TILE = L'';
constexpr wchar_t UI_BASE_CHAR = L'';
constexpr int BG_BOX_OFFSET=5;
constexpr const char *FONT_FILE_NAME="assets/text.otf";
constexpr std::array<std::wstring, 8> COMPASS{
// L"E", L"SE", L"S", L"SW", L"W", L"NW", L"N", L"NE"
L"\u2192", L"\u2198", L"\uffec", L"\u21d9", L"\u2190", L"\u2196", L"\uffea", L"\u21d7" };

47
src/dbc.cpp Normal file
View file

@ -0,0 +1,47 @@
#include "dbc.hpp"
#include <iostream>
void dbc::log(const string &message, const std::source_location location) {
std::cout << '[' << location.file_name() << ':'
<< location.line() << "|"
<< location.function_name() << "] "
<< message << std::endl;
}
void dbc::sentinel(const string &message, const std::source_location location) {
string err = fmt::format("[SENTINEL!] {}", message);
dbc::log(err, location);
throw dbc::SentinelError{err};
}
void dbc::pre(const string &message, bool test, const std::source_location location) {
if(!test) {
string err = fmt::format("[PRE!] {}", message);
dbc::log(err, location);
throw dbc::PreCondError{err};
}
}
void dbc::pre(const string &message, std::function<bool()> tester, const std::source_location location) {
dbc::pre(message, tester(), location);
}
void dbc::post(const string &message, bool test, const std::source_location location) {
if(!test) {
string err = fmt::format("[POST!] {}", message);
dbc::log(err, location);
throw dbc::PostCondError{err};
}
}
void dbc::post(const string &message, std::function<bool()> tester, const std::source_location location) {
dbc::post(message, tester(), location);
}
void dbc::check(bool test, const string &message, const std::source_location location) {
if(!test) {
string err = fmt::format("[CHECK!] {}\n", message);
dbc::log(err, location);
throw dbc::CheckError{err};
}
}

50
src/dbc.hpp Normal file
View file

@ -0,0 +1,50 @@
#pragma once
#include <string>
#include <fmt/core.h>
#include <functional>
#include <source_location>
namespace dbc {
using std::string;
class Error {
public:
const string message;
Error(string m) : message{m} {}
Error(const char *m) : message{m} {}
};
class CheckError : public Error {};
class SentinelError : public Error {};
class PreCondError : public Error {};
class PostCondError : public Error {};
void log(const string &message,
const std::source_location location =
std::source_location::current());
[[noreturn]] void sentinel(const string &message,
const std::source_location location =
std::source_location::current());
void pre(const string &message, bool test,
const std::source_location location =
std::source_location::current());
void pre(const string &message, std::function<bool()> tester,
const std::source_location location =
std::source_location::current());
void post(const string &message, bool test,
const std::source_location location =
std::source_location::current());
void post(const string &message, std::function<bool()> tester,
const std::source_location location =
std::source_location::current());
void check(bool test, const string &message,
const std::source_location location =
std::source_location::current());
}

248
src/dinkyecs.hpp Normal file
View file

@ -0,0 +1,248 @@
#pragma once
#include "dbc.hpp"
#include <any>
#include <functional>
#include <queue>
#include <tuple>
#include <typeindex>
#include <typeinfo>
#include <unordered_map>
#include <optional>
#include <memory>
namespace DinkyECS
{
using Entity = unsigned long;
const Entity NONE = 0;
template <typename T>
struct ComponentStorage {
std::vector<T> data;
};
struct Event {
int event = 0;
Entity entity = 0;
std::any data;
};
using EntityMap = std::unordered_map<Entity, size_t>;
using EventQueue = std::queue<Event>;
using TypeMap = std::unordered_map<std::type_index, std::any>;
struct World {
unsigned long entity_count = NONE+1;
std::unordered_map<std::type_index, EntityMap> $components;
std::shared_ptr<TypeMap> $facts = nullptr;
std::unordered_map<std::type_index, EventQueue> $events;
std::unordered_map<std::type_index, std::any> $component_storages;
std::unordered_map<std::type_index, std::queue<size_t>> $free_indices;
std::unordered_map<Entity, bool> $constants;
World() : $facts(std::make_shared<TypeMap>())
{}
Entity entity() { return ++entity_count; }
void destroy(DinkyECS::Entity entity) {
dbc::check(!$constants.contains(entity), "trying to destroy an entity in constants");
for(auto& [tid, map] : $components) {
if(map.contains(entity)) {
size_t index = map.at(entity);
auto& free_queue = $free_indices.at(tid);
free_queue.push(index);
map.erase(entity);
}
}
}
void clone_into(DinkyECS::World &to_world) {
to_world.$constants = $constants;
to_world.$facts = $facts;
// BUG*10: entity IDs should be a global counter, not per world
to_world.entity_count = entity_count;
to_world.$component_storages = $component_storages;
for(auto [eid, is_set] : $constants) {
dbc::check(is_set == true, "is_set was not true? WHAT?!");
dbc::check(eid <= entity_count, fmt::format(
"eid {} is not less than entity_count {}", eid, entity_count));
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.try_emplace(entity, true);
}
void not_constant(DinkyECS::Entity entity) {
$constants.erase(entity);
}
template <typename Comp>
size_t make_component() {
auto &storage = component_storage_for<Comp>();
auto &free_queue = $free_indices.at(std::type_index(typeid(Comp)));
size_t index;
if(!free_queue.empty()) {
index = free_queue.front();
free_queue.pop();
} else {
storage.data.emplace_back();
index = storage.data.size() - 1;
}
return index;
}
template <typename Comp>
ComponentStorage<Comp> &component_storage_for() {
auto type_index = std::type_index(typeid(Comp));
$component_storages.try_emplace(type_index, ComponentStorage<Comp>{});
$free_indices.try_emplace(type_index, std::queue<size_t>{});
return std::any_cast<ComponentStorage<Comp> &>(
$component_storages.at(type_index));
}
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>();
if(map.contains(ent)) {
size_t index = map.at(ent);
auto& free_queue = $free_indices.at(std::type_index(typeid(Comp)));
free_queue.push(index);
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>();
if(has<Comp>(ent)) {
get<Comp>(ent) = val;
return;
}
map.insert_or_assign(ent, make_component<Comp>());
get<Comp>(ent) = val;
}
template <typename Comp>
Comp &get(Entity ent) {
EntityMap &map = entity_map_for<Comp>();
auto &storage = component_storage_for<Comp>();
auto index = map.at(ent);
return storage.data[index];
}
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(Entity, Comp &)> cb) {
EntityMap &map = entity_map_for<Comp>();
for(auto &[entity, index] : map) {
cb(entity, get<Comp>(entity));
}
}
template <typename CompA, typename CompB>
void query(std::function<void(Entity, CompA &, CompB &)> cb) {
EntityMap &map_a = entity_map_for<CompA>();
EntityMap &map_b = entity_map_for<CompB>();
for(auto &[entity, index_a] : map_a) {
if(map_b.contains(entity)) {
cb(entity, get<CompA>(entity), get<CompB>(entity));
}
}
}
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();
}
/* std::optional can't do references. Don't try it!
* Actually, this sucks, either delete it or have it
* return pointers (assuming optional can handle pointers)
*/
template <typename Comp>
Comp* get_if(DinkyECS::Entity entity) {
EntityMap &map = entity_map_for<Comp>();
auto &storage = component_storage_for<Comp>();
if(map.contains(entity)) {
auto index = map.at(entity);
return &storage.data[index];
} else {
return nullptr;
}
}
};
} // namespace DinkyECS

141
src/easing.cpp Normal file
View file

@ -0,0 +1,141 @@
#include "rand.hpp"
#include "animation.hpp"
#include <fmt/core.h>
#include <unordered_map>
#include "dbc.hpp"
namespace ease2 {
using namespace animation;
double none(float tick) {
return 0.0;
}
double linear(float tick) {
return tick;
}
double sine(double x) {
// old one? return std::abs(std::sin(seq.subframe * ease_rate));
return (std::sin(x) + 1.0) / 2.0;
}
double out_circle(double x) {
return std::sqrt(1.0f - ((x - 1.0f) * (x - 1.0f)));
}
double out_bounce(double x) {
constexpr const double n1 = 7.5625;
constexpr const double d1 = 2.75;
if (x < 1 / d1) {
return n1 * x * x;
} else if (x < 2 / d1) {
x -= 1.5;
return n1 * (x / d1) * x + 0.75;
} else if (x < 2.5 / d1) {
x -= 2.25;
return n1 * (x / d1) * x + 0.9375;
} else {
x -= 2.625;
return n1 * (x / d1) * x + 0.984375;
}
}
double in_out_back(double x) {
constexpr const double c1 = 1.70158;
constexpr const double c2 = c1 * 1.525;
return x < 0.5
? (std::pow(2.0 * x, 2.0) * ((c2 + 1.0) * 2.0 * x - c2)) / 2.0
: (std::pow(2.0 * x - 2.0, 2.0) * ((c2 + 1.0) * (x * 2.0 - 2.0) + c2) + 2.0) / 2.0;
}
double random(double tick) {
return Random::uniform_real(0.0001f, 1.0f);
}
double normal_dist(double tick) {
return Random::normal(0.5f, 0.1f);
}
void move_shake(Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative) {
pos_out.x = std::lerp(tr.min_x, tr.max_x, tick) + (pos_out.x * relative);
}
void move_bounce(Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative) {
pos_out.y = std::lerp(tr.min_y, tr.max_y, tick) + (pos_out.y * relative);
}
void move_rush(Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative) {
scale_out.x = std::lerp(tr.min_x, tr.max_x, tick) + (scale_out.x * relative);
scale_out.y = std::lerp(tr.min_y, tr.max_y, tick) + (scale_out.y * relative);
pos_out.y = pos_out.y - (pos_out.y * scale_out.y - pos_out.y) + (pos_out.y * relative);
}
void scale_squeeze(Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative) {
scale_out.x = std::lerp(tr.min_x, tr.max_x, tick) + (scale_out.x * relative);
}
void scale_squash(Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative) {
scale_out.y = std::lerp(tr.min_y, tr.max_y, tick) + (scale_out.y * relative);
}
void scale_stretch(Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative) {
scale_out.x = std::lerp(tr.min_x, tr.max_x, tick) + (scale_out.x * relative);
}
void scale_grow(Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative) {
scale_out.y = std::lerp(tr.min_y, tr.max_y, tick) + (scale_out.y * relative);
}
void move_slide(Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative) {
pos_out.x = std::lerp(tr.min_x, tr.max_x, tick) + (pos_out.x * relative);
pos_out.y = std::lerp(tr.min_y, tr.max_y, tick) + (pos_out.y * relative);
}
void move_none(Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative) {
}
void scale_both(Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative) {
scale_out.x = std::lerp(scale_out.x * tr.min_x, scale_out.x * tr.max_x, tick) + (scale_out.x * relative);
scale_out.y = std::lerp(scale_out.y * tr.min_y, scale_out.y * tr.max_y, tick) + (scale_out.y * relative);
}
std::unordered_map<std::string, EaseFunc> map_of_easings{
{"sine", sine},
{"out_circle", out_circle},
{"out_bounce", out_bounce},
{"in_out_back", in_out_back},
{"random", random},
{"normal_dist", normal_dist},
{"none", none},
{"linear", linear},
};
std::unordered_map<std::string, MotionFunc> map_of_motions{
{"move_bounce", move_bounce},
{"move_rush", move_rush},
{"scale_squeeze", scale_squeeze},
{"scale_squash", scale_squash},
{"scale_stretch", scale_stretch},
{"scale_grow", scale_grow},
{"move_slide", move_slide},
{"move_none", move_none},
{"scale_both", scale_both},
{"move_shake", move_shake},
};
EaseFunc get_easing(const std::string& name) {
dbc::check(map_of_easings.contains(name),
fmt::format("easing name {} does not exist", name));
return map_of_easings.at(name);
}
MotionFunc get_motion(const std::string& name) {
dbc::check(map_of_motions.contains(name),
fmt::format("motion name {} does not exist", name));
return map_of_motions.at(name);
}
}

32
src/easing.hpp Normal file
View file

@ -0,0 +1,32 @@
#include <functional>
#include "animation.hpp"
namespace animation {
struct Transform;
}
namespace ease2 {
using EaseFunc = std::function<double(double)>;
using MotionFunc = std::function<void(animation::Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative)>;
EaseFunc get_easing(const std::string& name);
MotionFunc get_motion(const std::string& name);
double sine(double x);
double out_circle(double x);
double out_bounce(double x);
double in_out_back(double x);
double random(double tick);
double normal_dist(double tick);
void move_bounce(animation::Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative);
void move_rush(animation::Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative);
void scale_squeeze(animation::Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative);
void scale_squash(animation::Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative);
void scale_stretch(animation::Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative);
void scale_grow(animation::Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative);
void move_slide(animation::Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative);
void move_none(animation::Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative);
void scale_both(animation::Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative);
void move_shake(animation::Transform &tr, sf::Vector2f& pos_out, sf::Vector2f& scale_out, float tick, bool relative);
}

49
src/events.hpp Normal file
View file

@ -0,0 +1,49 @@
#pragma once
namespace game {
enum Event {
AIM_CLICK=__LINE__,
ANIMATION_END=__LINE__,
ANIMATION_START=__LINE__,
ATTACK=__LINE__,
BOSS_END=__LINE__,
BOSS_START=__LINE__,
CLOSE=__LINE__,
COMBAT=__LINE__,
COMBAT_START=__LINE__,
COMBAT_STOP=__LINE__,
DEATH=__LINE__,
ENTITY_SPAWN=__LINE__,
HP_STATUS=__LINE__,
INV_SELECT=__LINE__,
KEY_PRESS=__LINE__,
LOOT_CLOSE=__LINE__,
LOOT_CONTAINER=__LINE__,
LOOT_ITEM=__LINE__,
LOOT_OPEN=__LINE__,
LOOT_SELECT=__LINE__,
MAP_OPEN=__LINE__,
MOUSE_CLICK=__LINE__,
MOUSE_DRAG=__LINE__,
MOUSE_DRAG_START=__LINE__,
MOUSE_DROP=__LINE__,
MOUSE_MOVE=__LINE__,
MOVE_BACK=__LINE__,
MOVE_FORWARD=__LINE__,
MOVE_LEFT=__LINE__,
MOVE_RIGHT=__LINE__,
NEW_RITUAL=__LINE__,
NOOP=__LINE__,
NO_NEIGHBORS=__LINE__,
QUIT=__LINE__,
ROTATE_LEFT=__LINE__,
ROTATE_RIGHT=__LINE__,
STAIRS_DOWN=__LINE__,
STAIRS_UP=__LINE__,
START=__LINE__,
TICK=__LINE__,
TRAP=__LINE__,
UPDATE_SPRITE=__LINE__,
USE_ITEM=__LINE__,
};
}

139
src/game_level.cpp Normal file
View file

@ -0,0 +1,139 @@
#include "game_level.hpp"
#include "components.hpp"
#include "worldbuilder.hpp"
#include "constants.hpp"
#include "systems.hpp"
#include "components.hpp"
#include "rituals.hpp"
#include "textures.hpp"
#include <list>
using lighting::LightRender;
using std::shared_ptr, std::make_shared;
using namespace components;
struct LevelScaling {
int map_width=20;
int map_height=20;
};
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"),
settings::get("bosses"),
settings::get("rituals"),
settings::get("stories")
});
}
}

35
src/game_level.hpp Normal file
View file

@ -0,0 +1,35 @@
#pragma once
#include "dinkyecs.hpp"
#include "boss/ui.hpp"
#include "lights.hpp"
#include "map.hpp"
#include <memory>
#include "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);
}

188
src/goap.cpp Normal file
View file

@ -0,0 +1,188 @@
#include "dbc.hpp"
#include "goap.hpp"
#include "ai_debug.hpp"
#include "stats.hpp"
#include <queue>
// #define DEBUG_CYCLES 1
namespace ai {
using namespace nlohmann;
using namespace dbc;
bool is_subset(State& source, State& target) {
State result = source & target;
return result == target;
}
void Action::needs(int name, bool val) {
if(val) {
$positive_preconds[name] = true;
$negative_preconds[name] = false;
} else {
$negative_preconds[name] = true;
$positive_preconds[name] = false;
}
}
void Action::effect(int name, bool val) {
if(val) {
$positive_effects[name] = true;
$negative_effects[name] = false;
} else {
$negative_effects[name] = true;
$positive_effects[name] = false;
}
}
void Action::ignore(int name) {
$positive_preconds[name] = false;
$negative_preconds[name] = false;
}
bool Action::can_effect(State& state) {
bool posbit_match = (state & $positive_preconds) == $positive_preconds;
bool negbit_match = (state & $negative_preconds) == ALL_ZERO;
return posbit_match && negbit_match;
}
State Action::apply_effect(State& state) {
return (state | $positive_effects) & ~$negative_effects;
}
int distance_to_goal(State from, State to) {
auto result = from ^ to;
int count = result.count();
return count;
}
inline void dump_came_from(std::string msg, std::unordered_map<Action, Action>& came_from, Action& current) {
fmt::println("{}: {}", msg, current.name);
for(auto& [from, to] : came_from) {
fmt::println("from={}; to={}", from.name, to.name);
}
}
inline void path_invariant(std::unordered_map<Action, Action>& came_from, Action current) {
#if defined(DEBUG_CYCLES)
bool final_found = current == FINAL_ACTION;
for(size_t i = 0; i <= came_from.size() && came_from.contains(current); i++) {
current = came_from.at(current);
final_found = current == FINAL_ACTION;
}
if(!final_found) {
dump_came_from("CYCLE DETECTED!", came_from, current);
dbc::sentinel("AI CYCLE FOUND!");
}
#else
(void)came_from; // disable errors about unused
(void)current;
#endif
}
Script reconstruct_path(std::unordered_map<Action, Action>& came_from, Action& current) {
Script total_path{current};
path_invariant(came_from, current);
for(size_t i = 0; i <= came_from.size() && came_from.contains(current); i++) {
auto next = came_from.at(current);
if(next != FINAL_ACTION) {
// remove the previous node to avoid cycles and repeated actions
total_path.push_front(next);
came_from.erase(current);
current = next;
} else {
// found the terminator, done
break;
}
}
return total_path;
}
inline int h(State start, State goal) {
return distance_to_goal(start, goal);
}
inline int d(State start, State goal) {
return distance_to_goal(start, goal);
}
ActionState find_lowest(std::unordered_map<ActionState, int>& open_set) {
check(!open_set.empty(), "open set can't be empty in find_lowest");
int found_score = std::numeric_limits<int>::max();
ActionState found_as;
for(auto& kv : open_set) {
if(kv.second <= found_score) {
found_score = kv.second;
found_as = kv.first;
}
}
return found_as;
}
ActionPlan plan_actions(std::vector<Action>& actions, State start, State goal) {
std::unordered_map<ActionState, int> open_set;
std::unordered_map<Action, Action> came_from;
std::unordered_map<State, int> g_score;
std::unordered_map<State, bool> closed_set;
ActionState current{FINAL_ACTION, start};
g_score.insert_or_assign(start, 0);
open_set.insert_or_assign(current, h(start, goal));
while(!open_set.empty()) {
// current := the node in openSet having the lowest fScore[] value
current = find_lowest(open_set);
if(is_subset(current.state, goal)) {
return {true,
reconstruct_path(came_from, current.action)};
}
open_set.erase(current);
closed_set.insert_or_assign(current.state, true);
for(auto& neighbor_action : actions) {
// calculate the State being current/neighbor
if(!neighbor_action.can_effect(current.state)) continue;
auto neighbor = neighbor_action.apply_effect(current.state);
if(closed_set.contains(neighbor)) continue;
// BUG: no matter what I do cost really doesn't impact the graph
// Additionally, every other GOAP implementation has the same problem, and
// it's probably because the selection of actions is based more on sets matching
// than actual weights of paths. This reduces the probability that an action will
// be chosen over another due to only cost.
int d_score = d(current.state, neighbor) + neighbor_action.cost;
int tentative_g_score = g_score[current.state] + d_score;
int neighbor_g_score = g_score.contains(neighbor) ? g_score[neighbor] : SCORE_MAX;
if(tentative_g_score + neighbor_action.cost < neighbor_g_score) {
came_from.insert_or_assign(neighbor_action, current.action);
g_score.insert_or_assign(neighbor, tentative_g_score);
ActionState neighbor_as{neighbor_action, neighbor};
int score = tentative_g_score + h(neighbor, goal);
// this maybe doesn't need score
open_set.insert_or_assign(neighbor_as, score);
}
}
}
return {is_subset(current.state, goal), reconstruct_path(came_from, current.action)};
}
}

90
src/goap.hpp Normal file
View file

@ -0,0 +1,90 @@
#pragma once
#include <vector>
#include "matrix.hpp"
#include <bitset>
#include <limits>
#include <optional>
#include <nlohmann/json.hpp>
#include "config.hpp"
namespace ai {
// ZED: I don't know if this is the best place for this
using AIProfile = std::unordered_map<std::string, int>;
constexpr const int SCORE_MAX = std::numeric_limits<int>::max() / 2;
constexpr const size_t STATE_MAX = 32;
using State = std::bitset<STATE_MAX>;
const State ALL_ZERO;
const State ALL_ONES = ~ALL_ZERO;
struct Action {
std::string name;
int cost = 0;
State $positive_preconds;
State $negative_preconds;
State $positive_effects;
State $negative_effects;
Action() {}
Action(std::string name, int cost) :
name(name), cost(cost) { }
void needs(int name, bool val);
void effect(int name, bool val);
void ignore(int name);
bool can_effect(State& state);
State apply_effect(State& state);
bool operator==(const Action& other) const {
return other.name == name;
}
};
using Script = std::deque<Action>;
const Action FINAL_ACTION("END", SCORE_MAX);
struct ActionState {
Action action;
State state;
ActionState(Action action, State state) :
action(action), state(state) {}
ActionState() : action(FINAL_ACTION), state(0) {}
bool operator==(const ActionState& other) const {
return other.action == action && other.state == state;
}
};
struct ActionPlan {
bool complete = false;
Script script;
};
bool is_subset(State& source, State& target);
int distance_to_goal(State from, State to);
ActionPlan plan_actions(std::vector<Action>& actions, State start, State goal);
}
template<> struct std::hash<ai::Action> {
size_t operator()(const ai::Action& p) const {
return std::hash<std::string>{}(p.name);
}
};
template<> struct std::hash<ai::ActionState> {
size_t operator()(const ai::ActionState& p) const {
return std::hash<ai::Action>{}(p.action) ^ std::hash<ai::State>{}(p.state);
}
};

102
src/gui/combat_ui.cpp Normal file
View file

@ -0,0 +1,102 @@
#include "constants.hpp"
#include "rituals.hpp"
#include <fmt/xchar.h>
#include "gui/guecstra.hpp"
#include "game_level.hpp"
#include "gui/combat_ui.hpp"
namespace gui {
using guecs::THEME;
using namespace guecs;
CombatUI::CombatUI(bool boss_style) :
$boss_style(boss_style)
{
}
guecs::Entity CombatUI::make_button(
guecs::Entity button,
game::Event event,
int action,
const std::string &icon_name,
const std::string &sound,
const std::string &effect_name)
{
$gui.set<Sprite>(button, {icon_name});
$gui.set<Sound>(button, {sound});
$gui.set<Effect>(button, {.duration=0.5f, .name=effect_name});
$gui.set<Clickable>(button,
guecs::make_action(button, event, {action}));
return button;
}
void CombatUI::init(int x, int y, int w, int h) {
$gui.position(x, y, w, h);
if($boss_style) {
$gui.layout(
"[action0|action1]"
"[action2|action3]"
"[action4|action5]"
"[action6|action7]");
} else {
$gui.layout(
"[action0 | action1 | action2 | action3"
"|action4 | action5 | action6 | action7 | =hp_gauge ]");
}
auto world = GameDB::current_world();
$gui.set<Background>($gui.MAIN, {$gui.$parser, THEME.DARK_MID});
auto& the_belt = world->get_the<ritual::Belt>();
for(int slot = 0; slot < the_belt.max_slots; slot++) {
fmt::println("combat ui belt slot {}", slot);
if(the_belt.has(slot)) {
auto button = $gui.entity("action", slot);
fmt::println("combat ui has belt slot {} with id {}", slot, button);
auto& ritual = the_belt.get(slot);
using enum ritual::Element;
switch(ritual.element) {
case FIRE:
make_button(button, game::Event::ATTACK,
slot, "broken_yoyo", "fireball_01", "flame");
break;
case LIGHTNING:
make_button(button, game::Event::ATTACK,
slot, "pocket_watch", "electric_shock_01", "lightning");
break;
default:
make_button(button, game::Event::ATTACK,
slot, "severed_finger", "punch_cartoony", "ui_shader");
}
}
}
if(!$boss_style) {
auto hp_gauge = $gui.entity("hp_gauge");
$gui.set<Sprite>(hp_gauge, {"stone_doll_cursed"});
$gui.set<Clickable>(hp_gauge,
guecs::make_action(hp_gauge, game::Event::HP_STATUS, {}));
}
$gui.init();
}
void CombatUI::render(sf::RenderWindow& window) {
$gui.render(window);
// $gui.debug_layout(window);
}
void CombatUI::update_level() {
init(COMBAT_UI_X, COMBAT_UI_Y, COMBAT_UI_WIDTH, COMBAT_UI_HEIGHT);
}
bool CombatUI::mouse(float x, float y, guecs::Modifiers mods) {
return $gui.mouse(x, y, mods);
}
}

23
src/gui/combat_ui.hpp Normal file
View file

@ -0,0 +1,23 @@
#pragma once
#include <SFML/Graphics/RenderWindow.hpp>
#include <SFML/Graphics/Font.hpp>
#include <guecs/ui.hpp>
#include "events.hpp"
namespace gui {
class CombatUI {
public:
bool $boss_style;
guecs::UI $gui;
CombatUI(bool boss_style);
void init(int x, int y, int w, int h);
void render(sf::RenderWindow& window);
void update_level();
bool mouse(float x, float y, guecs::Modifiers mods);
guecs::Entity make_button(guecs::Entity button, game::Event event,
int action, const std::string &icon_name,
const std::string &sound, const std::string &effect_name);
};
}

106
src/gui/debug_ui.cpp Normal file
View file

@ -0,0 +1,106 @@
#include "gui/debug_ui.hpp"
#include "constants.hpp"
#include "events.hpp"
#include <optional>
#include <fmt/core.h>
#include <fmt/xchar.h>
#include "components.hpp"
namespace gui {
using namespace guecs;
void DebugUI::init(lel::Cell cell) {
$gui.position(cell.x, cell.y, cell.w, cell.h);
$gui.layout(
"[*%(100,400)debug_text]"
"[_]"
"[_]"
"[_]"
"[spawn1|spawn2|spawn3]"
"[spawn4|spawn5|spawn6]");
add_spawn_button("AXE_RANGER", "axe_ranger", "spawn1");
add_spawn_button("KNIGHT","armored_knight", "spawn2");
add_spawn_button("SPIDER_GIANT_HAIRY", "hairy_spider", "spawn3");
add_spawn_button("RAT_GIANT", "rat_with_sword", "spawn4");
add_spawn_button("GOLD_SAVIOR", "gold_savior", "spawn5");
$gui.init();
}
void DebugUI::add_spawn_button(std::string enemy_key, std::string sprite_name, std::string region) {
auto button = $gui.entity(region);
$gui.set<guecs::Clickable>(button, {
[this, enemy_key](auto){ spawn(enemy_key); }
});
$gui.set<guecs::Sprite>(button, { sprite_name});
}
void DebugUI::spawn(const std::string& enemy_key) {
(void)enemy_key;
dbc::log("THIS FUNCTION NEEDS A REWRITE");
// auto ent = $level_mgr.spawn_enemy(enemy_key);
// auto& level = $level_mgr.current();
// level.world->send<game::Event>(game::Event::ENTITY_SPAWN, ent, {});
}
void DebugUI::render(sf::RenderWindow& window) {
if(active) {
auto& level = GameDB::current_level();
auto player = level.world->get_the<components::Player>();
auto player_combat = level.world->get<components::Combat>(player.entity);
auto map = level.map;
std::wstring stats = fmt::format(L"STATS\n"
L"HP: {}\n"
L"mean:{:>8.5}\n"
L"sdev: {:>8.5}\n"
L"min: {:>8.5}\n"
L"max: {:>8.5}\n"
L"count:{:<10}\n"
L"level: {} size: {}x{}\n\n"
L"VSync? {}\n"
L"FR Limit: {}\n"
L"Debug? {}\n\n",
player_combat.hp, $stats.mean(), $stats.stddev(), $stats.min,
$stats.max, $stats.n, level.index, map->width(), map->height(),
VSYNC, FRAME_LIMIT, DEBUG_BUILD);
$gui.show_text("debug_text", stats);
$gui.render(window);
// $gui.debug_layout(window);
}
}
void DebugUI::debug() {
active = !active;
if(active) {
auto& level = GameDB::current_level();
// it's on now, enable things
auto player = level.world->get_the<components::Player>();
auto& player_combat = level.world->get<components::Combat>(player.entity);
player_combat.hp = player_combat.max_hp;
$gui.show_text("debug_text", L"STATS");
} else {
// it's off now, close it
$gui.close<Text>("debug_text");
}
}
bool DebugUI::mouse(float x, float y, guecs::Modifiers mods) {
return $gui.mouse(x, y, mods);
}
Stats::TimeBullshit DebugUI::time_start() {
return $stats.time_start();
}
void DebugUI::sample_time(Stats::TimeBullshit start) {
$stats.sample_time(start);
}
void DebugUI::reset_stats() {
$stats.reset();
}
}

26
src/gui/debug_ui.hpp Normal file
View file

@ -0,0 +1,26 @@
#pragma once
#include "game_level.hpp"
#include <SFML/Graphics/RenderWindow.hpp>
#include <SFML/Graphics/Font.hpp>
#include <guecs/ui.hpp>
#include "stats.hpp"
namespace gui {
class DebugUI {
public:
Stats $stats;
guecs::UI $gui;
bool active = false;
void init(lel::Cell cell);
void render(sf::RenderWindow& window);
bool mouse(float x, float y, guecs::Modifiers mods);
void debug();
void spawn(const std::string& enemy_key);
void add_spawn_button(std::string enemy_key, std::string sprite_name, std::string region);
Stats::TimeBullshit time_start();
void sample_time(Stats::TimeBullshit start);
void reset_stats();
};
}

328
src/gui/dnd_loot.cpp Normal file
View file

@ -0,0 +1,328 @@
#include "gui/guecstra.hpp"
#include "gui/dnd_loot.hpp"
namespace gui {
using Event = game::Event;
DNDLoot::DNDLoot(StatusUI& status_ui, LootUI& loot_ui, sf::RenderWindow &window, routing::Router& router) :
$status_ui(status_ui),
$loot_ui(loot_ui),
$window(window),
$router(router)
{
event(Event::START);
}
bool DNDLoot::event(Event ev, std::any data) {
switch($state) {
FSM_STATE(DNDState, START, ev);
FSM_STATE(DNDState, LOOTING, ev, data);
FSM_STATE(DNDState, LOOT_GRAB, ev, data);
FSM_STATE(DNDState, INV_GRAB, ev, data);
FSM_STATE(DNDState, ITEM_PICKUP, ev, data);
FSM_STATE(DNDState, INV_PICKUP, ev, data);
FSM_STATE(DNDState, END, ev, data);
default:
dbc::log(fmt::format("event received with data but state={} is not handled", int($state)));
}
return !in_state(DNDState::END);
}
void DNDLoot::START(Event ev) {
using enum Event;
dbc::check(ev == START, "START not given a STARTED event.");
END(CLOSE);
}
void DNDLoot::LOOTING(Event ev, std::any data) {
using enum Event;
switch(ev) {
case LOOT_OPEN:
END(CLOSE);
break;
case LOOT_SELECT:
$grab_source = start_grab($loot_ui.$gui, data);
if($grab_source) state(DNDState::LOOT_GRAB);
break;
case INV_SELECT:
$grab_source = start_grab($status_ui.$gui, data);
if($grab_source) state(DNDState::INV_GRAB);
break;
default:
break; // ignore
}
}
void DNDLoot::LOOT_GRAB(Event ev, std::any data) {
using enum Event;
switch(ev) {
case LOOT_OPEN:
END(CLOSE);
break;
case LOOT_SELECT: {
auto drop_id = std::any_cast<guecs::Entity>(data);
if(move_or_swap($loot_ui, drop_id)) {
state(DNDState::LOOTING);
}
} break;
case INV_SELECT:
if(commit_drop($loot_ui.$gui,
$status_ui.$gui, $grab_source, data))
{
state(DNDState::LOOTING);
}
break;
default:
handle_mouse(ev, $loot_ui.$gui);
}
}
void DNDLoot::INV_GRAB(Event ev, std::any data) {
using enum Event;
switch(ev) {
case LOOT_OPEN:
END(CLOSE);
break;
case LOOT_SELECT:
if(commit_drop($status_ui.$gui,
$loot_ui.$gui, $grab_source, data))
{
state(DNDState::LOOTING);
}
break;
case INV_SELECT: {
auto drop_id = std::any_cast<guecs::Entity>(data);
if(move_or_swap($status_ui, drop_id)) {
state(DNDState::LOOTING);
}
} break;
default:
handle_mouse(ev, $status_ui.$gui);
}
}
void DNDLoot::INV_PICKUP(Event ev, std::any data) {
using enum Event;
switch(ev) {
case AIM_CLICK: {
// take from inventory, drop on floor
throw_on_floor($status_ui.$gui, true);
END(CLOSE);
} break;
case INV_SELECT: {
auto drop_id = std::any_cast<guecs::Entity>(data);
if(move_or_swap($status_ui, drop_id)) {
END(CLOSE);
}
} break;
default:
handle_mouse(ev, $status_ui.$gui);
}
}
void DNDLoot::ITEM_PICKUP(Event ev, std::any data) {
using enum Event;
switch(ev) {
case INV_SELECT:
if(commit_drop($loot_ui.$gui, $status_ui.$gui, $grab_source, data))
{
END(CLOSE);
}
break;
case AIM_CLICK: {
// THIS IS PUT IT BACK ON THE FLOOR
throw_on_floor($loot_ui.$gui, false);
END(CLOSE);
} break;
default:
handle_mouse(ev, $loot_ui.$gui);
}
}
void DNDLoot::END(Event ev, std::any data) {
using enum Event;
switch(ev) {
case LOOT_ITEM: {
auto gui_id = $loot_ui.$gui.entity("item_0");
if(hold_item($loot_ui.$gui, gui_id)) {
state(DNDState::ITEM_PICKUP);
}
} break;
case INV_SELECT: {
auto gui_id = std::any_cast<guecs::Entity>(data);
if(hold_item($status_ui.$gui, gui_id)) {
state(DNDState::INV_PICKUP);
}
} break;
case LOOT_OPEN:
open();
state(DNDState::LOOTING);
break;
case CLOSE:
// called the first time transitioning to END
close();
state(DNDState::END);
break;
case TICK: // ignored
break;
default:
dbc::log(fmt::format("invalid event: {}", int(ev)));
}
}
void DNDLoot::handle_mouse(Event ev, guecs::UI& gui) {
using enum Event;
switch(ev) {
case MOUSE_DRAG:
case MOUSE_MOVE: {
if($grab_source) {
auto& source = gui.get<guecs::GrabSource>(*$grab_source);
source.move($window.mapPixelToCoords($router.position));
}
} break;
default:
break; // ignored
}
}
void DNDLoot::clear_grab() {
$grab_source = std::nullopt;
$grab_sprite = nullptr;
}
void DNDLoot::open() {
$loot_ui.active = true;
}
void DNDLoot::close() {
$loot_ui.active = false;
}
void DNDLoot::render() {
if($grab_source && $grab_sprite) {
$window.draw(*$grab_sprite);
}
}
std::optional<guecs::Entity> DNDLoot::start_grab(guecs::UI& gui, std::any data) {
auto gui_id = std::any_cast<guecs::Entity>(data);
if(auto source = gui.get_if<guecs::GrabSource>(gui_id)) {
$grab_sprite = source->sprite;
source->grab();
return gui_id;
} else {
return std::nullopt;
}
}
bool DNDLoot::commit_drop(guecs::UI& source, guecs::UI& target,
std::optional<guecs::Entity> source_id, std::any data)
{
if(!source_id) return false;
auto target_id = std::any_cast<guecs::Entity>(data);
dbc::check(target.has<guecs::DropTarget>(target_id),
"gui does not have a DropTarget at that slot");
dbc::check(source.has<guecs::GrabSource>(*source_id),
"gui does not have a GrabSource at that slot");
auto& grab = source.get<guecs::GrabSource>(*source_id);
auto& drop = target.get<guecs::DropTarget>(target_id);
if(drop.commit(grab.world_entity)) {
grab.commit();
clear_grab();
return true;
} else {
return false;
}
}
bool DNDLoot::commit_move(guecs::UI& gui, std::optional<guecs::Entity> source_id, guecs::Entity drop_id) {
dbc::check(source_id != std::nullopt, "source_id must exist");
auto& grab = gui.get<guecs::GrabSource>(*source_id);
grab.commit();
auto& drop = gui.get<guecs::DropTarget>(drop_id);
if(drop.commit(grab.world_entity)) {
clear_grab();
return true;
} else {
// swap with the target instead
return false;
}
}
bool DNDLoot::hold_item(guecs::UI &gui, guecs::Entity gui_id) {
// NOTE: if > 1 items, go to LOOT_OPEN instead
$grab_source = start_grab(gui, gui_id);
if($grab_source) {
auto& source = gui.get<guecs::GrabSource>(*$grab_source);
$grab_sprite = source.sprite;
// call this once to properly position the sprite
handle_mouse(Event::MOUSE_MOVE, gui);
}
return $grab_source != std::nullopt;
}
/*
* Dropping on the ground is only possible from the
* status_ui for now.
*/
void DNDLoot::throw_on_floor(guecs::UI& gui, bool from_status) {
dbc::check($grab_source != std::nullopt, "attempt to commit_drop but no grab_source set");
dbc::check(gui.has<guecs::GrabSource>(*$grab_source),
"StatusUI doesn't actually have that GrabSource in the gui.");
auto& grab = gui.get<guecs::GrabSource>(*$grab_source);
if(from_status) {
$status_ui.drop_item(grab.world_entity);
} else {
$loot_ui.drop_item(grab.world_entity);
}
grab.commit();
clear_grab();
}
/*
* If I refactored everything to use a levelmanager module then
* this and many other things could go away. Access to $level is
* making this too complicated. Do this for now, but fix bug #59.
*/
bool DNDLoot::move_or_swap(StatusUI& ui, guecs::Entity drop_id) {
if(ui.occupied(drop_id)) {
ui.swap(*$grab_source, drop_id);
clear_grab();
return true;
} else {
return commit_move(ui.$gui, $grab_source, drop_id);
}
}
bool DNDLoot::move_or_swap(LootUI& ui, guecs::Entity drop_id) {
if(ui.occupied(drop_id)) {
ui.swap(*$grab_source, drop_id);
clear_grab();
return true;
} else {
return commit_move(ui.$gui, $grab_source, drop_id);
}
}
}

66
src/gui/dnd_loot.hpp Normal file
View file

@ -0,0 +1,66 @@
#pragma once
#include "simplefsm.hpp"
#include <guecs/ui.hpp>
#include "gui/status_ui.hpp"
#include "gui/loot_ui.hpp"
#include "gui/event_router.hpp"
#include "events.hpp"
namespace gui {
enum class DNDState {
START=100,
LOOTING=101,
LOOT_GRAB=102,
INV_GRAB=103,
ITEM_PICKUP=104,
INV_PICKUP=105,
END=106
};
class DNDLoot : public DeadSimpleFSM<DNDState, game::Event> {
public:
std::optional<guecs::Entity> $grab_source = std::nullopt;
std::shared_ptr<sf::Sprite> $grab_sprite = nullptr;
StatusUI& $status_ui;
LootUI& $loot_ui;
sf::RenderWindow& $window;
routing::Router& $router;
DNDLoot(StatusUI& status_ui,
LootUI& loot_ui, sf::RenderWindow& window,
routing::Router& router);
bool event(game::Event ev, std::any data={});
void START(game::Event ev);
void LOOTING(game::Event ev, std::any data);
void LOOT_GRAB(game::Event ev, std::any data);
void INV_GRAB(game::Event ev, std::any data);
void END(game::Event ev, std::any data={});
void ITEM_PICKUP(game::Event ev, std::any data);
void INV_PICKUP(game::Event ev, std::any data);
void handle_mouse(game::Event ev, guecs::UI& gui);
void render();
void open();
void close();
std::optional<guecs::Entity> start_grab(guecs::UI& gui, std::any data);
bool commit_drop(guecs::UI& source, guecs::UI& target,
std::optional<guecs::Entity> source_id, std::any data);
bool commit_move(guecs::UI& gui,
std::optional<guecs::Entity> source_id, guecs::Entity drop_id);
bool hold_item(guecs::UI& gui, guecs::Entity gui_id);
void throw_on_floor(guecs::UI& gui, bool from_status);
void clear_grab();
bool move_or_swap(StatusUI& status_ui, guecs::Entity drop_id);
bool move_or_swap(LootUI& ui, guecs::Entity drop_id);
sf::Vector2f mouse_position();
};
}

143
src/gui/event_router.cpp Normal file
View file

@ -0,0 +1,143 @@
#include "event_router.hpp"
#include "dbc.hpp"
#include "events.hpp"
namespace gui {
namespace routing {
using enum Event;
using enum State;
game::Event Router::process_event(std::optional<sf::Event> ev) {
$next_event = game::Event::TICK;
if(ev->is<sf::Event::Closed>()) {
return game::Event::QUIT;
}
if(const auto* mouse = ev->getIf<sf::Event::MouseButtonPressed>()) {
if(mouse->button == sf::Mouse::Button::Left || mouse->button == sf::Mouse::Button::Right) {
left_button = mouse->button == sf::Mouse::Button::Left;
position = mouse->position;
event(MOUSE_DOWN);
}
} else if(const auto* mouse = ev->getIf<sf::Event::MouseButtonReleased>()) {
// need to sort this out but if you don't do this it thinks you're always pressing it
if(mouse->button == sf::Mouse::Button::Left || mouse->button == sf::Mouse::Button::Right) {
left_button = mouse->button == sf::Mouse::Button::Left;
position = mouse->position;
event(MOUSE_UP);
}
} else if(const auto* mouse = ev->getIf<sf::Event::MouseMoved>()) {
position = mouse->position;
event(MOUSE_MOVE);
}
if(const auto* key = ev->getIf<sf::Event::KeyPressed>()) {
scancode = key->scancode;
event(KEY_PRESS);
}
return $next_event;
}
void Router::event(Event ev) {
switch($state) {
FSM_STATE(State, START, ev);
FSM_STATE(State, IDLE, ev);
FSM_STATE(State, MOUSE_ACTIVE, ev);
FSM_STATE(State, MOUSE_MOVING, ev);
FSM_STATE(State, MOUSE_DRAGGING, ev);
}
}
void Router::START(Event ) {
state(State::IDLE);
}
void Router::IDLE(Event ev) {
switch(ev) {
case MOUSE_DOWN:
move_count=0;
set_event(game::Event::TICK);
state(State::MOUSE_ACTIVE);
break;
case MOUSE_UP:
set_event(game::Event::MOUSE_CLICK);
state(State::IDLE);
break;
case MOUSE_MOVE:
set_event(game::Event::MOUSE_MOVE);
break;
case KEY_PRESS:
set_event(game::Event::KEY_PRESS);
break;
default:
dbc::sentinel(fmt::format("invalid event: {}", int(ev)));
}
}
void Router::MOUSE_ACTIVE(Event ev) {
switch(ev) {
case MOUSE_UP:
set_event(game::Event::MOUSE_CLICK);
state(State::IDLE);
break;
case MOUSE_MOVE:
move_count++;
set_event(game::Event::MOUSE_DRAG);
state(State::MOUSE_MOVING);
break;
case KEY_PRESS:
set_event(game::Event::KEY_PRESS);
state(State::IDLE);
break;
default:
dbc::sentinel("invalid event");
}
}
void Router::MOUSE_MOVING(Event ev) {
switch(ev) {
case MOUSE_UP: {
dbc::check(move_count < $drag_tolerance, "mouse up but not in dragging state");
set_event(game::Event::MOUSE_CLICK);
state(State::IDLE);
} break;
case MOUSE_MOVE:
move_count++;
if(move_count < $drag_tolerance) {
set_event(game::Event::MOUSE_DRAG);
} else {
set_event(game::Event::MOUSE_DRAG_START);
state(State::MOUSE_DRAGGING);
}
break;
case KEY_PRESS:
set_event(game::Event::KEY_PRESS);
break;
default:
dbc::sentinel("invalid event");
}
}
void Router::MOUSE_DRAGGING(Event ev) {
switch(ev) {
case MOUSE_UP:
set_event(game::Event::MOUSE_DROP);
state(State::IDLE);
break;
case MOUSE_MOVE:
move_count++;
set_event(game::Event::MOUSE_DRAG);
break;
case KEY_PRESS:
set_event(game::Event::KEY_PRESS);
break;
default:
// invalid events: 1
dbc::sentinel(fmt::format("invalid events: {}", int(ev)));
}
}
}
}

49
src/gui/event_router.hpp Normal file
View file

@ -0,0 +1,49 @@
#pragma once
#include "events.hpp"
#include "events.hpp"
#include "simplefsm.hpp"
#include <SFML/Graphics.hpp>
namespace gui {
namespace routing {
enum class State {
START,
IDLE,
MOUSE_ACTIVE,
MOUSE_MOVING,
MOUSE_DRAGGING
};
enum class Event {
STARTED=0,
MOUSE_DOWN=1,
MOUSE_UP=2,
MOUSE_MOVE=3,
KEY_PRESS=4
};
class Router : public DeadSimpleFSM<State, Event> {
public:
sf::Vector2i position;
sf::Keyboard::Scancode scancode;
game::Event $next_event = game::Event::TICK;
int move_count = 0;
bool left_button = true;
int $drag_tolerance = 4;
void event(Event ev);
void START(Event ev);
void IDLE(Event ev);
void MOUSE_ACTIVE(Event ev);
void MOUSE_MOVING(Event ev);
void MOUSE_DRAGGING(Event ev);
game::Event process_event(std::optional<sf::Event> ev);
void set_event(game::Event ev) {
$next_event = ev;
}
};
}
}

577
src/gui/fsm.cpp Normal file
View file

@ -0,0 +1,577 @@
#include "gui/fsm.hpp"
#include <iostream>
#include <chrono>
#include <numeric>
#include <functional>
#include "components.hpp"
#include <numbers>
#include "systems.hpp"
#include "events.hpp"
#include "sound.hpp"
#include "shaders.hpp"
#include <fmt/xchar.h>
#include "gui/guecstra.hpp"
#include "game_level.hpp"
#include "boss/system.hpp"
namespace gui {
using namespace components;
using game::Event;
FSM::FSM() :
$window(sf::VideoMode({SCREEN_WIDTH, SCREEN_HEIGHT}), "Zed's Raycaster Thing"),
$main_ui($window),
$combat_ui(false),
$dnd_loot($status_ui, $loot_ui, $window, $router)
{
$window.setVerticalSyncEnabled(VSYNC);
if(FRAME_LIMIT) $window.setFramerateLimit(FRAME_LIMIT);
$window.setPosition({0,0});
}
void FSM::event(Event ev, std::any data) {
switch($state) {
FSM_STATE(State, START, ev);
FSM_STATE(State, MOVING, ev);
FSM_STATE(State, ATTACKING, ev, data);
FSM_STATE(State, ROTATING, ev);
FSM_STATE(State, IDLE, ev, data);
FSM_STATE(State, IN_COMBAT, ev);
FSM_STATE(State, COMBAT_ROTATE, ev);
FSM_STATE(State, CUT_SCENE_PLAYING, ev, data);
FSM_STATE(State, BOSS_FIGHT, ev, data);
FSM_STATE(State, LOOTING, ev, data);
FSM_STATE(State, END, ev);
}
}
void FSM::START(Event ) {
$main_ui.update_level();
$main_ui.init();
$loot_ui.init();
// BUG: maybe this is a function on main_ui?
auto cell = $main_ui.overlay_cell("left");
$debug_ui.init(cell);
$combat_ui.init(COMBAT_UI_X, COMBAT_UI_Y, COMBAT_UI_WIDTH, COMBAT_UI_HEIGHT);
$status_ui.init();
$map_ui.init();
$map_ui.log(L"Welcome to the game!");
run_systems();
state(State::IDLE);
}
void FSM::MOVING(Event ) {
// this should be an optional that returns a point
if(auto move_to = $main_ui.play_move()) {
System::move_player(*move_to);
run_systems();
$main_ui.dirty();
state(State::IDLE);
}
}
void FSM::ATTACKING(Event ev, std::any data) {
using enum Event;
switch(ev) {
case TICK: {
dbc::log("!!!!!! FIX System::combat(0) doesn't use any weapons, only first");
System::combat(0);
run_systems();
state(State::IN_COMBAT);
} break;
case COMBAT_STOP:
state(State::IDLE);
break;
case ATTACK: {
int attack_id = std::any_cast<int>(data);
System::combat(attack_id);
run_systems();
} break;
default:
dbc::log(fmt::format("In ATTACKING state, unhandled event {}", (int)ev));
state(State::IDLE);
}
}
void FSM::ROTATING(Event) {
if(auto aim = $main_ui.play_rotate()) {
auto& player_pos = GameDB::player_position();
player_pos.aiming_at = *aim;
state(State::IDLE);
}
}
void FSM::COMBAT_ROTATE(Event) {
if(auto aim = $main_ui.play_rotate()) {
auto& player_pos = GameDB::player_position();
player_pos.aiming_at = *aim;
state(State::IN_COMBAT);
}
}
void FSM::LOOTING(Event ev, std::any data) {
using enum Event;
switch(ev) {
case MOUSE_DRAG_START:
case MOUSE_CLICK:
case MOUSE_DROP:
mouse_action(guecs::NO_MODS);
break;
default:
if(!$dnd_loot.event(ev, data)) {
state(State::IDLE);
}
}
}
void FSM::IDLE(Event ev, std::any data) {
using enum Event;
sound::stop("walk");
switch(ev) {
case QUIT:
$window.close();
state(State::END);
return; // done
case MOVE_FORWARD:
try_move(1, false);
break;
case MOVE_BACK:
try_move(-1, false);
break;
case MOVE_LEFT:
try_move(-1, true);
break;
case MOVE_RIGHT:
try_move(1, true);
break;
case ROTATE_LEFT:
$main_ui.plan_rotate(-1, 0.25f);
state(State::ROTATING);
break;
case ROTATE_RIGHT:
$main_ui.plan_rotate(1, 0.25f);
state(State::ROTATING);
break;
case MAP_OPEN:
$map_open = !$map_open;
break;
case ATTACK:
state(State::ATTACKING);
break;
case COMBAT_START:
$map_open = false;
state(State::IN_COMBAT);
break;
case CLOSE:
dbc::log("Nothing to close.");
break;
case BOSS_START:
sound::stop("ambient");
next_level(true);
state(State::CUT_SCENE_PLAYING);
break;
case LOOT_ITEM:
$dnd_loot.event(Event::LOOT_ITEM);
state(State::LOOTING);
break;
case LOOT_OPEN:
$dnd_loot.event(Event::LOOT_OPEN);
state(State::LOOTING);
break;
case INV_SELECT:
$dnd_loot.event(Event::INV_SELECT, data);
state(State::LOOTING);
break;
case USE_ITEM: {
auto gui_id = std::any_cast<guecs::Entity>(data);
auto& slot_name = $status_ui.$gui.name_for(gui_id);
if(System::use_item(slot_name)) {
$status_ui.update();
}
} break;
case MOUSE_CLICK:
mouse_action(guecs::NO_MODS);
break;
case MOUSE_MOVE: {
mouse_action({1 << guecs::ModBit::hover});
} break;
case AIM_CLICK:
System::pickup();
break;
default:
break; // ignore everything else
}
}
void FSM::CUT_SCENE_PLAYING(Event ev, std::any data) {
if($boss_scene->playing()) {
if(ev == game::Event::MOUSE_CLICK) {
dbc::log("exiting cut scene");
$boss_scene->mouse(0,0, 0);
state(State::BOSS_FIGHT);
}
} else {
state(State::BOSS_FIGHT);
}
}
void FSM::BOSS_FIGHT(Event ev, std::any data) {
dbc::log("this should not run, it's handled in handle_boss_fight_events");
}
void FSM::IN_COMBAT(Event ev) {
using enum Event;
switch(ev) {
case MOUSE_CLICK:
mouse_action(guecs::NO_MODS);
break;
case MOUSE_MOVE: {
mouse_action({1 << guecs::ModBit::hover});
} break;
case TICK:
run_systems();
break;
case ATTACK:
$main_ui.play_hands();
$main_ui.dirty();
sound::play("Sword_Hit_1");
state(State::ATTACKING);
break;
case ROTATE_LEFT:
$main_ui.plan_rotate(-1, DEFAULT_ROTATE);
state(State::COMBAT_ROTATE);
break;
case ROTATE_RIGHT:
$main_ui.plan_rotate(1, DEFAULT_ROTATE);
state(State::COMBAT_ROTATE);
break;
case COMBAT_STOP:
$main_ui.$overlay_ui.close_sprite("top_right");
state(State::IDLE);
break;
case QUIT:
$window.close();
state(State::END);
return;
default:
break;
}
}
void FSM::try_move(int dir, bool strafe) {
auto& level = GameDB::current_level();
using enum State;
// prevent moving into occupied space
Point move_to = $main_ui.plan_move(dir, strafe);
if(level.map->can_move(move_to) && !level.collision->occupied(move_to)) {
sound::play("walk");
state(MOVING);
} else {
state(IDLE);
$main_ui.abort_plan();
}
}
void FSM::END(Event ev) {
dbc::log(fmt::format("END: received event after done: {}", int(ev)));
}
sf::Vector2f FSM::mouse_position() {
return $window.mapPixelToCoords($router.position);
}
void FSM::mouse_action(guecs::Modifiers mods) {
sf::Vector2f pos = mouse_position();
if($debug_ui.active) $debug_ui.mouse(pos.x, pos.y, mods);
$combat_ui.mouse(pos.x, pos.y, mods);
$status_ui.mouse(pos.x, pos.y, mods);
if($loot_ui.active) {
$loot_ui.mouse(pos.x, pos.y, mods);
} else {
$main_ui.mouse(pos.x, pos.y, mods);
}
}
void FSM::handle_keyboard_mouse() {
while(const auto ev = $window.pollEvent()) {
auto gui_ev = $router.process_event(ev);
if(gui_ev == Event::KEY_PRESS) {
using KEY = sf::Keyboard::Scan;
switch($router.scancode) {
case KEY::W:
event(Event::MOVE_FORWARD);
break;
case KEY::S:
event(Event::MOVE_BACK);
break;
case KEY::Q:
event(Event::ROTATE_LEFT);
break;
case KEY::E:
event(Event::ROTATE_RIGHT);
break;
case KEY::D:
event(Event::MOVE_RIGHT);
break;
case KEY::A:
event(Event::MOVE_LEFT);
break;
case KEY::R:
dbc::log("HEY! DIPSHIT! You need to move debug ui so you can rest stats.");
break;
case KEY::M:
event(Event::MAP_OPEN);
break;
case KEY::Escape:
event(Event::CLOSE);
break;
case KEY::Space:
event(Event::ATTACK);
break;
case KEY::P:
sound::mute(false);
if(!sound::playing("ambient_1")) sound::play("ambient_1", true);
$debug_ui.debug();
shaders::reload();
break;
case KEY::O:
autowalking = true;
break;
case KEY::L:
// This will go away as soon as containers work
$loot_ui.set_target($loot_ui.$temp_loot);
$loot_ui.update();
event(Event::LOOT_OPEN);
break;
case KEY::Z:
$main_ui.toggle_mind_reading();
break;
case KEY::X:
event(Event::BOSS_START);
break;
case KEY::F5:
take_screenshot();
break;
default:
break; // ignored
}
} else {
event(gui_ev);
}
}
}
void FSM::debug_render() {
auto start = $debug_ui.time_start();
$main_ui.render();
$debug_ui.sample_time(start);
$debug_ui.render($window);
}
void FSM::draw_gui() {
if($debug_ui.active) {
debug_render();
} else {
$main_ui.render();
}
$status_ui.render($window);
$combat_ui.render($window);
if($loot_ui.active) $loot_ui.render($window);
if(in_state(State::LOOTING)) $dnd_loot.render();
if($map_open) {
$map_ui.render($window, $main_ui.$compass_dir);
}
}
void FSM::render() {
$window.clear();
if(in_state(State::BOSS_FIGHT)) {
$boss_fight->render($window);
// this clears any attack animations, like fire
System::clear_attack();
} else if(in_state(State::CUT_SCENE_PLAYING)) {
$boss_scene->render($window);
} else {
// this clears any attack animations, like fire
System::clear_attack();
draw_gui();
}
$window.display();
}
void FSM::run_systems() {
System::generate_paths();
System::enemy_ai_initialize();
System::enemy_pathing();
System::motion();
System::collision();
System::lighting();
System::death();
}
bool FSM::active() {
return !in_state(State::END);
}
void FSM::handle_boss_fight_events() {
dbc::check($boss_fight != nullptr, "$boss_fight not initialized");
// true means State::END in $boss_fight
if($boss_fight->handle_world_events() || $boss_fight->handle_keyboard_mouse()) {
dbc::log("boss fight ended, transition to IDLE");
// fight is over, go back to regular game
sound::play("ambient");
next_level(false);
state(State::IDLE);
}
}
void FSM::handle_world_events() {
using eGUI = game::Event;
auto world = GameDB::current_world();
while(world->has_event<eGUI>()) {
auto [evt, entity, data] = world->recv<eGUI>();
auto player = world->get_the<Player>();
// HERE: this has to go, unify these events and just use them in the state machine directly
switch(evt) {
case eGUI::COMBAT: {
auto &damage = std::any_cast<components::CombatResult&>(data);
if(damage.enemy_did > 0) {
$map_ui.log(fmt::format(L"Enemy HIT YOU for {} damage!", damage.enemy_did));
} else {
$map_ui.log(L"Enemy MISSED YOU.");
}
if(damage.player_did > 0) {
$map_ui.log(fmt::format(L"You HIT enemy for {} damage!", damage.player_did));
} else {
$map_ui.log(L"You MISSED the enemy.");
}
}
break;
case eGUI::COMBAT_START:
event(Event::COMBAT_START);
break;
case eGUI::ENTITY_SPAWN: {
auto& sprite = world->get<components::Sprite>(entity);
$main_ui.$rayview->update_sprite(entity, sprite);
$main_ui.dirty();
run_systems();
} break;
case eGUI::NO_NEIGHBORS:
event(Event::COMBAT_STOP);
break;
case eGUI::LOOT_CLOSE:
// BUG: need to resolve GUI events vs. FSM events better
event(Event::LOOT_OPEN);
break;
case eGUI::LOOT_SELECT:
event(Event::LOOT_SELECT, data);
break;
case eGUI::INV_SELECT: {
if($router.left_button) {
event(Event::INV_SELECT, data);
} else {
event(Event::USE_ITEM, data);
}
} break;
case eGUI::AIM_CLICK:
event(Event::AIM_CLICK);
break;
case eGUI::LOOT_ITEM: {
dbc::check(world->has<components::InventoryItem>(entity),
"INVALID LOOT_ITEM, that entity has no InventoryItem");
$loot_ui.add_loose_item(entity);
event(Event::LOOT_ITEM);
} break;
case eGUI::LOOT_CONTAINER: {
$loot_ui.set_target($loot_ui.$temp_loot);
$loot_ui.update();
event(Event::LOOT_OPEN);
} break;
case eGUI::HP_STATUS:
System::player_status();
break;
case eGUI::NEW_RITUAL:
$combat_ui.init(COMBAT_UI_X, COMBAT_UI_Y, COMBAT_UI_WIDTH, COMBAT_UI_HEIGHT);
break;
case eGUI::ATTACK:
event(Event::ATTACK, data);
break;
case eGUI::STAIRS_DOWN:
event(Event::BOSS_START);
break;
case eGUI::DEATH: {
$status_ui.update();
if(entity != player.entity) {
$main_ui.dead_entity(entity);
} else {
dbc::log("NEED TO HANDLE PLAYER DYING.");
}
} break;
case eGUI::NOOP: {
if(data.type() == typeid(std::string)) {
auto name = std::any_cast<std::string>(data);
$map_ui.log(fmt::format(L"NOOP EVENT! {},{}", evt, entity));
}
} break;
default:
dbc::log(fmt::format("Unhandled event: evt={}; enemy={}; data={}",
evt, entity, data.type().name()));
event(game::Event(evt), data);
}
}
}
void FSM::take_screenshot() {
auto size = $window.getSize();
sf::Texture shot{size};
shot.update($window);
sf::Image out_img = shot.copyToImage();
bool worked = out_img.saveToFile("./screenshot.png");
dbc::check(worked, "Failed to write screenshot.png");
}
void FSM::next_level(bool bossfight) {
if(bossfight) {
$boss_scene = std::make_shared<storyboard::UI>("rat_king");
$boss_scene->init();
$boss_fight = boss::System::create_bossfight();
$boss_fight->set_window(&$window);
dbc::check($boss_scene->playing(), "boss scene doesn't play");
} else {
GameDB::create_level();
$status_ui.update_level();
$combat_ui.update_level();
$main_ui.update_level();
$loot_ui.update_level();
}
run_systems();
}
}

83
src/gui/fsm.hpp Normal file
View file

@ -0,0 +1,83 @@
#pragma once
#include "constants.hpp"
#include "simplefsm.hpp"
#include "gui/debug_ui.hpp"
#include "gui/main_ui.hpp"
#include "gui/combat_ui.hpp"
#include "gui/status_ui.hpp"
#include "gui/loot_ui.hpp"
#include "boss/fight.hpp"
#include "gui/map_view.hpp"
#include "events.hpp"
#include "gui/event_router.hpp"
#include "gui/dnd_loot.hpp"
#include "storyboard/ui.hpp"
#include "events.hpp"
namespace gui {
enum class State {
START=__LINE__,
MOVING=__LINE__,
IN_COMBAT=__LINE__,
COMBAT_ROTATE=__LINE__,
ATTACKING=__LINE__,
ROTATING=__LINE__,
BOSS_FIGHT=__LINE__,
LOOTING=__LINE__,
IDLE=__LINE__,
CUT_SCENE_PLAYING=__LINE__,
END=__LINE__,
};
class FSM : public DeadSimpleFSM<State, game::Event> {
public:
sf::RenderWindow $window;
bool $draw_stats = false;
bool autowalking = false;
bool $map_open = false;
DebugUI $debug_ui;
MainUI $main_ui;
std::shared_ptr<boss::Fight> $boss_fight = nullptr;
std::shared_ptr<storyboard::UI> $boss_scene = nullptr;
CombatUI $combat_ui;
StatusUI $status_ui;
MapViewUI $map_ui;
LootUI $loot_ui;
gui::routing::Router $router;
DNDLoot $dnd_loot;
FSM();
void event(game::Event ev, std::any data={});
void autowalk();
void start_autowalk(double rot_speed);
void START(game::Event ev);
void MOVING(game::Event ev);
void ATTACKING(game::Event ev, std::any data);
void MAPPING(game::Event ev);
void ROTATING(game::Event ev);
void IDLE(game::Event ev, std::any data);
void IN_COMBAT(game::Event ev);
void COMBAT_ROTATE(game::Event ev);
void BOSS_FIGHT(game::Event ev, std::any data);
void LOOTING(game::Event ev, std::any data);
void CUT_SCENE_PLAYING(game::Event ev, std::any data);
void END(game::Event ev);
void try_move(int dir, bool strafe);
sf::Vector2f mouse_position();
void mouse_action(guecs::Modifiers mods);
void handle_keyboard_mouse();
void draw_gui();
void render();
bool active();
void run_systems();
void handle_world_events();
void handle_boss_fight_events();
void next_level(bool bossfight);
void debug_render();
void take_screenshot();
};
}

40
src/gui/guecstra.cpp Normal file
View file

@ -0,0 +1,40 @@
#include "gui/guecstra.hpp"
#include "game_level.hpp"
namespace guecs {
Clickable make_action(guecs::Entity gui_id, game::Event event) {
return {[&, gui_id, event](auto){
auto world = GameDB::current_world();
world->send<game::Event>(event, gui_id, {});
}};
}
Clickable make_action(guecs::Entity gui_id, game::Event event, std::any data) {
return {[&, event, data](auto){
auto world = GameDB::current_world();
world->send<game::Event>(event, gui_id, data);
}};
}
DinkyECS::Entity GrabSource::grab() {
fmt::println("> Grab entity {}", world_entity);
return world_entity;
}
void GrabSource::setSprite(guecs::UI& gui, guecs::Entity gui_id) {
if(auto sp = gui.get_if<guecs::Icon>(gui_id)) {
sprite = sp->sprite;
} else if(auto sp = gui.get_if<guecs::Sprite>(gui_id)) {
sprite = sp->sprite;
} else {
dbc::sentinel("GrabSource given sprite gui_id that doesn't exist");
}
}
void GrabSource::move(sf::Vector2f pos) {
if(sprite) {
sprite->setPosition(pos);
}
}
}

24
src/gui/guecstra.hpp Normal file
View file

@ -0,0 +1,24 @@
#pragma once
#include "components.hpp"
#include "events.hpp"
#include <guecs/ui.hpp>
#include "textures.hpp"
namespace guecs {
Clickable make_action(guecs::Entity gui_id, game::Event event);
Clickable make_action(guecs::Entity gui_id, game::Event event, std::any data);
struct GrabSource {
DinkyECS::Entity world_entity;
std::function<void()> commit;
std::shared_ptr<sf::Sprite> sprite = nullptr;
DinkyECS::Entity grab();
void setSprite(guecs::UI& gui, guecs::Entity gui_id);
void move(sf::Vector2f pos);
};
struct DropTarget {
std::function<bool(DinkyECS::Entity world_entity)> commit;
};
}

154
src/gui/loot_ui.cpp Normal file
View file

@ -0,0 +1,154 @@
#include "gui/loot_ui.hpp"
#include "constants.hpp"
#include <fmt/xchar.h>
#include "systems.hpp"
#include "game_level.hpp"
namespace gui {
using namespace guecs;
LootUI::LootUI() :
$temp_loot(GameDB::current_world()->entity()),
$target($temp_loot)
{
$gui.position(RAY_VIEW_X+RAY_VIEW_WIDTH/2-200,
RAY_VIEW_Y+RAY_VIEW_HEIGHT/2-200, 400, 400);
$gui.layout(
"[=item_0 | =item_1 |=item_2 |=item_3 ]"
"[=item_4 | =item_5 |=item_6 |=item_7 ]"
"[=item_8 | =item_9 |=item_10|=item_11]"
"[=item_12| =item_13|=item_14|=item_15 ]"
"[ =take_all | =close| =destroy]");
auto world = GameDB::current_world();
world->set<inventory::Model>($temp_loot, {});
world->make_constant($temp_loot);
}
void LootUI::make_button(const std::string &name, const std::wstring& label, game::Event event) {
auto button = $gui.entity(name);
$gui.set<guecs::Rectangle>(button, {});
$gui.set<guecs::Text>(button, {label});
$gui.set<guecs::Clickable>(button,
guecs::make_action(button, event));
}
void LootUI::init() {
using guecs::THEME;
auto bg_color = THEME.DARK_LIGHT;
bg_color.a = 140;
$gui.set<Background>($gui.MAIN, {$gui.$parser, bg_color});
make_button("close", L"CLOSE", game::Event::LOOT_CLOSE);
make_button("take_all", L"TAKE ALL", game::Event::LOOT_CLOSE);
make_button("destroy", L"DESTROY", game::Event::LOOT_CLOSE);
for(int i = 0; i < INV_SLOTS; i++) {
auto name = fmt::format("item_{}", i);
auto id = $gui.entity(name);
$gui.set<guecs::Rectangle>(id, {THEME.PADDING,
THEME.TRANSPARENT, THEME.LIGHT_MID });
$gui.set<guecs::Effect>(id, {0.4f, "ui_shader"});
$gui.set<guecs::Clickable>(id, {
guecs::make_action(id, game::Event::LOOT_SELECT, {id})
});
}
$gui.init();
update();
}
void LootUI::update() {
auto world = GameDB::current_world();
dbc::check(world->has<inventory::Model>($target),
"update called but $target isn't in world");
auto& contents = world->get<inventory::Model>($target);
for(size_t i = 0; i < INV_SLOTS; i++) {
auto id = $gui.entity("item_", int(i));
auto& slot_name = $gui.name_for(id);
if(contents.has(slot_name)) {
auto item = contents.get(slot_name);
dbc::check(world->has<components::Sprite>(item),
"item in inventory UI doesn't exist in world. New level?");
auto& sprite = world->get<components::Sprite>(item);
$gui.set_init<guecs::Icon>(id, {sprite.name});
guecs::GrabSource grabber{
item, [&, id]() { return remove_slot(id); }};
grabber.setSprite($gui, id);
$gui.set<guecs::GrabSource>(id, grabber);
} else {
// BUG: fix remove so it's safe to call on empty
if($gui.has<guecs::GrabSource>(id)) {
$gui.remove<guecs::Icon>(id);
$gui.remove<guecs::GrabSource>(id);
}
$gui.set<guecs::DropTarget>(id, {
[&, id](DinkyECS::Entity world_entity) -> bool { return place_slot(id, world_entity); }
});
}
}
}
void LootUI::remove_slot(guecs::Entity slot_id) {
auto& name = $gui.name_for(slot_id);
fmt::println("LootUI remove slot inv::Model id={} slot={}", $target, name);
System::remove_from_container($target, name);
update();
}
bool LootUI::place_slot(guecs::Entity id, DinkyECS::Entity world_entity) {
fmt::println("LootUI target={} placing world entity {} in slot id {}",
$target, id, world_entity);
auto& name = $gui.name_for(id);
bool worked = System::place_in_container($target, name, world_entity);
if(worked) update();
return worked;
}
void LootUI::render(sf::RenderWindow& window) {
$gui.render(window);
}
void LootUI::update_level() {
init();
}
void LootUI::add_loose_item(DinkyECS::Entity entity) {
System::place_in_container($temp_loot, "item_0", entity);
set_target($temp_loot);
update();
}
void LootUI::drop_item(DinkyECS::Entity item_id) {
System::drop_item(item_id);
update();
}
bool LootUI::mouse(float x, float y, guecs::Modifiers mods) {
return $gui.mouse(x, y, mods);
}
bool LootUI::occupied(guecs::Entity slot) {
return System::inventory_occupied($target, $gui.name_for(slot));
}
void LootUI::swap(guecs::Entity gui_a, guecs::Entity gui_b) {
if(gui_a != gui_b) {
auto& a_name = $gui.name_for(gui_a);
auto& b_name = $gui.name_for(gui_b);
System::inventory_swap($target, a_name, b_name);
}
update();
}
}

37
src/gui/loot_ui.hpp Normal file
View file

@ -0,0 +1,37 @@
#pragma once
#include "gui/guecstra.hpp"
#include <SFML/Graphics/RenderWindow.hpp>
#include <SFML/Graphics/Font.hpp>
#include <guecs/ui.hpp>
#include "events.hpp"
#include "inventory.hpp"
namespace gui {
class LootUI {
public:
bool active = false;
guecs::UI $gui;
DinkyECS::Entity $temp_loot = DinkyECS::NONE;
DinkyECS::Entity $target = DinkyECS::NONE;
LootUI();
void set_target(DinkyECS::Entity entity) {
$target = entity;
}
void init();
void update();
void render(sf::RenderWindow& window);
void update_level();
bool mouse(float x, float y, guecs::Modifiers mods);
void make_button(const std::string &name, const std::wstring& label, game::Event event);
void remove_slot(guecs::Entity slot_id);
bool place_slot(guecs::Entity gui_id, DinkyECS::Entity world_entity);
void add_loose_item(DinkyECS::Entity entity);
void drop_item(DinkyECS::Entity item_id);
bool occupied(guecs::Entity gui_id);
void swap(guecs::Entity gui_a, guecs::Entity gui_b);
};
}

155
src/gui/main_ui.cpp Normal file
View file

@ -0,0 +1,155 @@
#include "gui/main_ui.hpp"
#include "components.hpp"
#include <fmt/xchar.h>
#include "animation.hpp"
#include "constants.hpp"
#include "game_level.hpp"
#include "ai.hpp"
namespace gui {
using namespace components;
MainUI::MainUI(sf::RenderWindow& window) :
$window(window),
$rayview(std::make_shared<Raycaster>(RAY_VIEW_WIDTH, RAY_VIEW_HEIGHT))
{
$window.setVerticalSyncEnabled(VSYNC);
$window.setFramerateLimit(FRAME_LIMIT);
auto config = settings::get("config");
$hand = textures::get_sprite(config["player"]["hands"]);
$hand_anim = animation::load("assets/animation.json", config["player"]["hands"]);
}
void MainUI::dirty() {
$needs_render = true;
}
void MainUI::init() {
auto& player_position = GameDB::player_position();
auto player = player_position.location;
$rayview->init_shaders();
$rayview->set_position(RAY_VIEW_X, RAY_VIEW_Y);
$rayview->position_camera(player.x + 0.5, player.y + 0.5);
$overlay_ui.init();
}
void MainUI::render() {
if($needs_render) $rayview->render();
$rayview->draw($window);
if($mind_reading) render_mind_reading();
$overlay_ui.render($window);
if($hand_anim.playing) render_hands();
}
lel::Cell MainUI::overlay_cell(const std::string& name) {
return $overlay_ui.$gui.cell_for(name);
}
std::optional<Point> MainUI::play_rotate() {
if($rayview->play_rotate()) {
$needs_render = false;
return std::make_optional<Point>($rayview->aiming_at);
} else {
$needs_render = true;
return std::nullopt;
}
}
std::optional<components::Position> MainUI::play_move() {
if($rayview->play_move()) {
$needs_render = false;
return std::make_optional<Position>(
$rayview->camera_at,
$rayview->aiming_at);
} else {
$needs_render = true;
return std::nullopt;
}
}
void MainUI::plan_rotate(int dir, float amount) {
// -1 is left, 1 is right
int extra = (amount == 0.5) * dir;
$compass_dir = ($compass_dir + dir + extra) % COMPASS.size();
$rayview->plan_rotate(dir, amount);
}
Point MainUI::plan_move(int dir, bool strafe) {
return $rayview->plan_move(dir, strafe);
}
void MainUI::abort_plan() {
$rayview->abort_plan();
}
void MainUI::dead_entity(DinkyECS::Entity entity) {
auto world = GameDB::current_world();
if(world->has<components::Sprite>(entity)) {
auto &sprite = world->get<components::Sprite>(entity);
$rayview->update_sprite(entity, sprite);
}
}
void MainUI::toggle_mind_reading() {
$mind_reading = !$mind_reading;
if($mind_reading) {
render_mind_reading();
} else {
$overlay_ui.close_text("left");
}
}
void MainUI::render_mind_reading() {
auto level = GameDB::current_level();
if(auto entity = level.collision->occupied_by($rayview->aiming_at)) {
if(auto enemy_ai = level.world->get_if<ai::EntityAI>(entity)) {
$overlay_ui.show_text("left", fmt::format(L"AI: {}",
guecs::to_wstring(enemy_ai->to_string())));
} else {
$overlay_ui.show_text("left", L"no mind to read");
}
} else {
$overlay_ui.show_text("left", L"nothing there");
}
}
void MainUI::update_level() {
auto& level = GameDB::current_level();
auto& player_position = GameDB::player_position();
auto player = player_position.location;
$rayview->update_level(level);
$rayview->position_camera(player.x + 0.5, player.y + 0.5);
player_position.aiming_at = $rayview->aiming_at;
$compass_dir = 0;
$overlay_ui.update_level();
dirty();
}
void MainUI::mouse(int x, int y, guecs::Modifiers mods) {
$overlay_ui.$gui.mouse(x, y, mods);
}
void MainUI::play_hands() {
if(!$hand_anim.playing) $hand_anim.play();
}
void MainUI::render_hands() {
if($hand_anim.playing) {
$hand_anim.update();
$hand_anim.apply(*$hand.sprite);
$hand.sprite->setPosition({RAY_VIEW_X, RAY_VIEW_Y});
$window.draw(*$hand.sprite);
}
}
}

52
src/gui/main_ui.hpp Normal file
View file

@ -0,0 +1,52 @@
#pragma once
#include <SFML/Graphics/RenderWindow.hpp>
#include <SFML/System/Clock.hpp>
#include "stats.hpp"
#include <guecs/ui.hpp>
#include "gui/overlay_ui.hpp"
#include "gui/debug_ui.hpp"
#include "raycaster.hpp"
#include <optional>
namespace animation {
class Animation;
}
namespace gui {
class MainUI {
public:
int $compass_dir = 0;
bool $needs_render = true;
bool $mind_reading = false;
sf::Clock $clock;
sf::RenderWindow& $window;
OverlayUI $overlay_ui;
std::shared_ptr<Raycaster> $rayview;
textures::SpriteTexture $hand;
animation::Animation $hand_anim;
MainUI(sf::RenderWindow& window);
void mouse(int x, int y, guecs::Modifiers mods);
void debug();
void render_debug();
void plan_rotate(int dir, float amount);
std::optional<Point> play_rotate();
std::optional<components::Position> play_move();
Point plan_move(int dir, bool strafe);
void abort_plan();
void update_level();
void init();
void render();
void dirty();
lel::Cell overlay_cell(const std::string& name);
void dead_entity(DinkyECS::Entity entity);
void toggle_mind_reading();
void render_mind_reading();
void play_hands();
void render_hands();
};
}

75
src/gui/map_view.cpp Normal file
View file

@ -0,0 +1,75 @@
#include "map_view.hpp"
#include <functional>
#include <string>
#include "dbc.hpp"
#include "components.hpp"
#include "rand.hpp"
#include "systems.hpp"
#include "rand.hpp"
#include <codecvt>
#include <iostream>
#include <fmt/xchar.h>
#include <fstream>
#include "palette.hpp"
#include "game_level.hpp"
constexpr const int MAP_WIDTH=13;
constexpr const int MAP_HEIGHT=13;
namespace gui {
using namespace components;
using namespace guecs;
MapViewUI::MapViewUI() :
$map_render(std::make_shared<sf::RenderTexture>()),
$map_sprite($map_render->getTexture()),
$map_tiles(matrix::make(MAP_WIDTH, MAP_HEIGHT))
{
auto world = GameDB::current_world();
auto player = GameDB::the_player();
$player_display = world->get<Tile>(player).display;
}
void MapViewUI::init() {
$gui.position(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
$gui.layout("[log_view| *%(200)map_grid | _ ]");
$gui.set<Background>($gui.MAIN, {$gui.$parser, palette::get("tiles/fg:wall_plain")});
$log_to = $gui.entity("log_view");
$gui.set<Rectangle>($log_to, {10, THEME.DARK_MID, THEME.BORDER_COLOR, 10});
$gui.set<Text>($log_to, {L"Welcome to the Game!", 25, THEME.TEXT_COLOR, 10});
auto map_cell = lel::center(MAP_TILE_DIM * MAP_WIDTH, MAP_TILE_DIM * MAP_HEIGHT, $gui.cell_for("map_grid"));
$map_sprite.setPosition({(float)map_cell.x, (float)map_cell.y + 30});
$gui.init();
}
void MapViewUI::render(sf::RenderWindow &window, int compass_dir) {
$gui.render(window);
System::draw_map($map_tiles, $entity_map);
System::render_map($map_tiles, $entity_map, *$map_render, compass_dir, $player_display);
$map_sprite.setTexture($map_render->getTexture(), true);
window.draw($map_sprite);
// $gui.debug_layout(window);
}
void MapViewUI::update() {
if(auto text = $gui.get_if<Text>($log_to)) {
//BUG: I'm calling this what it is, fix it
wstring log_garbage;
for(auto msg : $messages) {
log_garbage += msg + L"\n";
}
text->update(log_garbage);
}
}
void MapViewUI::log(wstring msg) {
$messages.push_front(msg);
if($messages.size() > MAX_LOG_MESSAGES) {
$messages.pop_back();
}
update();
}
}

27
src/gui/map_view.hpp Normal file
View file

@ -0,0 +1,27 @@
#pragma once
#include "textures.hpp"
#include "matrix.hpp"
#include <guecs/ui.hpp>
#include <string>
#include "dinkyecs.hpp"
#include "map.hpp"
namespace gui {
class MapViewUI {
public:
guecs::UI $gui;
wchar_t $player_display = L'@';
DinkyECS::Entity $log_to;
EntityGrid $entity_map;
std::deque<std::wstring> $messages;
std::shared_ptr<sf::RenderTexture> $map_render;
sf::Sprite $map_sprite;
matrix::Matrix $map_tiles;
MapViewUI();
void init();
void render(sf::RenderWindow &window, int compass_dir);
void log(std::wstring msg);
void update();
};
}

33
src/gui/mini_map.cpp Normal file
View file

@ -0,0 +1,33 @@
#include "mini_map.hpp"
#include <functional>
#include <string>
#include "dbc.hpp"
#include "components.hpp"
#include "rand.hpp"
#include "systems.hpp"
#include "rand.hpp"
#include <codecvt>
#include <iostream>
#include <memory>
namespace gui {
using namespace components;
MiniMapUI::MiniMapUI() :
$map_grid{L"...", 45, {200, 200, 200, 100}, 10}
{
$font = std::make_shared<sf::Font>(FONT_FILE_NAME);
}
void MiniMapUI::init(guecs::UI& overlay) {
auto top_right = overlay.entity("top_right");
auto cell = overlay.cell_for(top_right);
$map_grid.init(cell, $font);
}
void MiniMapUI::render(sf::RenderWindow &window, int compass_dir) {
(void)compass_dir;
$map_grid.update(L"I'M BROKEN");
window.draw(*$map_grid.text);
}
}

17
src/gui/mini_map.hpp Normal file
View file

@ -0,0 +1,17 @@
#pragma once
#include "textures.hpp"
#include <guecs/ui.hpp>
#include <memory>
namespace gui {
class MiniMapUI {
public:
guecs::Text $map_grid;
guecs::UI $gui;
std::shared_ptr<sf::Font> $font = nullptr;
MiniMapUI();
void init(guecs::UI& overlay);
void render(sf::RenderWindow &window, int compass_dir);
};
}

63
src/gui/overlay_ui.cpp Normal file
View file

@ -0,0 +1,63 @@
#include "gui/overlay_ui.hpp"
#include "gui/guecstra.hpp"
#include "constants.hpp"
#include "events.hpp"
#include <optional>
#include "game_level.hpp"
namespace gui {
using namespace guecs;
OverlayUI::OverlayUI() {
$gui.position(RAY_VIEW_X, RAY_VIEW_Y, RAY_VIEW_WIDTH, RAY_VIEW_HEIGHT);
$gui.layout(
"[*%(100,300)left|=top|>(170,170)top_right]"
"[_|=middle|=middle_right]"
"[_|=bottom|=bottom_right]"
);
$gui.init();
}
inline void make_clickable_area(guecs::UI &gui, const std::string &name) {
auto area = gui.entity(name);
gui.set<Clickable>(area, {
[&](auto) {
auto world = GameDB::current_world();
world->send<game::Event>(game::Event::AIM_CLICK, area, {});
}
});
}
void OverlayUI::init() {
// gui.init is in the constructor
make_clickable_area($gui, "top");
make_clickable_area($gui, "middle");
make_clickable_area($gui, "bottom");
}
void OverlayUI::render(sf::RenderWindow& window) {
$gui.render(window);
// $gui.debug_layout(window);
}
void OverlayUI::show_sprite(string region, string sprite_name) {
$gui.show_sprite(region, sprite_name);
}
void OverlayUI::close_sprite(string region) {
$gui.close<Sprite>(region);
}
void OverlayUI::show_text(string region, wstring content) {
$gui.show_text(region, content);
}
void OverlayUI::close_text(string region) {
$gui.close<Text>(region);
}
void OverlayUI::update_level() {
init();
}
}

25
src/gui/overlay_ui.hpp Normal file
View file

@ -0,0 +1,25 @@
#pragma once
#include <SFML/Graphics/RenderWindow.hpp>
#include <SFML/Graphics/Font.hpp>
#include <guecs/ui.hpp>
namespace gui {
using std::string;
class OverlayUI {
public:
guecs::UI $gui;
OverlayUI();
void init();
void update_level();
void render(sf::RenderWindow& window);
void show_sprite(string region, string sprite_name);
void close_sprite(string region);
void show_text(std::string region, std::wstring content);
void update_text(std::string region, std::wstring content);
void close_text(std::string region);
};
}

264
src/gui/ritual_ui.cpp Normal file
View file

@ -0,0 +1,264 @@
#include "gui/ritual_ui.hpp"
#include <guecs/ui.hpp>
#include "rand.hpp"
#include "sound.hpp"
#include "events.hpp"
#include "game_level.hpp"
namespace gui {
namespace ritual {
using namespace guecs;
using std::any, std::any_cast, std::string, std::make_any;
UI::UI() {
$gui.position(STATUS_UI_X, STATUS_UI_Y, STATUS_UI_WIDTH, STATUS_UI_HEIGHT);
$gui.layout(
"[_]"
"[inv_slot0 | inv_slot1 | inv_slot2| inv_slot3]"
"[inv_slot4 | inv_slot5 | inv_slot6| inv_slot7]"
"[inv_slot8 | inv_slot9 | inv_slot10| inv_slot11]"
"[inv_slot12 | inv_slot13 | inv_slot14| inv_slot15]"
"[inv_slot16 | inv_slot17 | inv_slot18| inv_slot19]"
"[_ |=*%(200,400)result_text|_]"
"[*%(100,200)result_image|_ |_]"
"[_|_|_]"
"[_|_|_]"
"[_]"
"[ ritual_ui ]");
}
void UI::event(Event ev, std::any data) {
switch($state) {
FSM_STATE(State, START, ev);
FSM_STATE(State, OPENED, ev, data);
FSM_STATE(State, CRAFTING, ev, data);
FSM_STATE(State, CLOSED, ev);
FSM_STATE(State, OPENING, ev);
FSM_STATE(State, CLOSING, ev);
}
}
void UI::START(Event) {
$ritual_ui = textures::get_sprite("ritual_crafting_area");
$ritual_ui.sprite->setPosition($gui.get_position());
$ritual_ui.sprite->setTextureRect($ritual_closed_rect);
$ritual_anim = animation::load("assets/animation.json", "ritual_crafting_area");
auto open_close_toggle = $gui.entity("ritual_ui");
$gui.set<Clickable>(open_close_toggle, {
[&](auto){ event(Event::TOGGLE); }
});
$craft_state = $ritual_engine.start();
$gui.init();
play_blanket("idle");
state(State::CLOSED);
}
void UI::OPENED(Event ev, std::any data) {
if(ev == Event::TOGGLE) {
clear_blanket();
play_blanket("close");
state(State::CLOSING);
} else if(ev == Event::SELECT) {
// do this before transitioning
state(State::CRAFTING);
UI::CRAFTING(ev, data);
}
}
void UI::CRAFTING(Event ev, std::any data) {
if(ev == Event::TOGGLE) {
clear_blanket();
play_blanket("close");
state(State::CLOSING);
} else if(ev == Event::COMBINE) {
complete_combine();
} else if(ev == Event::SELECT) {
dbc::check(data.has_value(), "OPENED state given SELECT with no data");
auto pair = std::any_cast<SelectedItem>(data);
select_item(pair);
update_selection_state();
}
}
void UI::CLOSED(Event ev) {
if(ev == Event::TOGGLE) {
load_blanket();
play_blanket("open");
state(State::OPENING);
}
}
void UI::OPENING(Event ev) {
if(ev == Event::TICK) {
if($ritual_anim.playing) {
$ritual_anim.update();
$ritual_anim.apply(*$ritual_ui.sprite);
} else {
state(State::OPENED);
}
}
}
void UI::CLOSING(Event ev) {
if($ritual_anim.playing) {
$ritual_anim.update();
$ritual_anim.apply(*$ritual_ui.sprite);
} else {
state(State::CLOSED);
}
}
bool UI::mouse(float x, float y, guecs::Modifiers mods) {
return $gui.mouse(x, y, mods);
}
bool UI::is_open() {
return !in_state(State::CLOSED);
}
void UI::render(sf::RenderWindow &window) {
event(Event::TICK);
window.draw(*$ritual_ui.sprite);
if(in_state(State::OPENED) || in_state(State::CRAFTING)) {
$gui.render(window);
// $gui.debug_layout(window);
}
}
void UI::clear_blanket() {
for(int i = 0; i < INV_SLOTS; i++) {
auto slot_id = $gui.entity("inv_slot", i);
if($gui.has<Sprite>(slot_id)) {
$gui.remove<Sprite>(slot_id);
$gui.remove<Clickable>(slot_id);
}
}
blanket().reset();
}
void UI::select_item(SelectedItem pair) {
auto& sprite = $gui.get<Sprite>(pair.slot_id);
if(blanket().is_selected(pair.item_id)) {
blanket().deselect(pair.item_id);
sprite.sprite->setColor({255, 255, 255, 255});
} else {
blanket().select(pair.item_id);
sprite.sprite->setColor({255, 200, 200, 200});
}
}
void UI::update_selection_state() {
if(blanket().no_selections()) {
clear_craft_result();
state(State::OPENED);
} else {
run_crafting_engine();
show_craft_result();
}
}
void UI::load_blanket() {
// update the list of available items
int i = 0;
for(auto& [item_id, item] : blanket().contents) {
auto slot_id = $gui.entity("inv_slot", i++);
$gui.set_init<Sprite>(slot_id, {item});
$gui.set<Clickable>(slot_id, {
[&, slot_id, item_id](auto) {
auto data = std::make_any<SelectedItem>(slot_id, item_id);
event(Event::SELECT, data);
}
});
}
for(; i < INV_SLOTS; i++) {
auto slot_id = $gui.entity("inv_slot", i);
$gui.remove<Sprite>(slot_id);
$gui.remove<Clickable>(slot_id);
}
}
void UI::complete_combine() {
auto world = GameDB::current_world();
auto player = GameDB::the_player();
if($craft_state.is_combined()) {
auto ritual = $ritual_engine.finalize($craft_state);
auto& belt = world->get_the<::ritual::Belt>();
belt.equip(belt.next(), ritual);
world->send<game::Event>(game::Event::NEW_RITUAL, player, {});
blanket().consume_crafting();
clear_craft_result();
load_blanket();
state(State::OPENED);
}
}
void UI::run_crafting_engine() {
$craft_state.reset();
for(auto [item_id, setting] : blanket().selected) {
auto& item = blanket().get(item_id);
$ritual_engine.load_junk($craft_state, item);
}
$ritual_engine.plan($craft_state);
}
void UI::show_craft_result() {
using enum ::ritual::Element;
auto ritual = $ritual_engine.finalize($craft_state);
auto combine = $gui.entity("result_image");
if($craft_state.is_combined()) {
$gui.show_text("result_text", L"This might work...");
switch(ritual.element) {
case FIRE:
$gui.show_sprite("result_image", "broken_yoyo");
break;
case LIGHTNING:
$gui.show_sprite("result_image", "pocket_watch");
break;
default:
$gui.show_sprite("result_image", "severed_finger");
}
$gui.set<Clickable>(combine, {
[&](auto){ event(Event::COMBINE); }
});
} else {
$gui.show_text("result_text", L"That won't work.");
$gui.show_sprite("result_image", "dubious_combination");
$gui.remove<Clickable>(combine);
return;
}
}
void UI::clear_craft_result() {
blanket().reset();
$gui.close<Text>("result_text");
$gui.close<Sprite>("result_image");
}
void UI::play_blanket(const std::string& form) {
$ritual_anim.set_form(form);
$ritual_anim.play();
}
::ritual::Blanket& UI::blanket() {
auto world = GameDB::current_world();
return world->get_the<::ritual::Blanket>();
}
}
}

69
src/gui/ritual_ui.hpp Normal file
View file

@ -0,0 +1,69 @@
#pragma once
#include "constants.hpp"
#include <deque>
#include "textures.hpp"
#include <guecs/ui.hpp>
#include "rituals.hpp"
#include "simplefsm.hpp"
#include "animation.hpp"
namespace gui {
namespace ritual {
enum class State {
START=0,
OPENED=1,
CLOSED=2,
OPENING=3,
CLOSING=4,
CRAFTING=5
};
enum class Event {
STARTED=0,
TOGGLE=1,
TICK=2,
SELECT=3,
COMBINE=4
};
struct SelectedItem {
guecs::Entity slot_id;
::ritual::Entity item_id;
};
class UI : public DeadSimpleFSM<State, Event> {
public:
sf::IntRect $ritual_closed_rect{{0,0},{380,720}};
sf::IntRect $ritual_open_rect{{380 * 2,0},{380,720}};
animation::Animation $ritual_anim;
guecs::UI $gui;
textures::SpriteTexture $ritual_ui;
::ritual::Engine $ritual_engine;
::ritual::CraftingState $craft_state;
UI();
void event(Event ev, std::any data={});
void START(Event);
void OPENED(Event, std::any data={});
void CRAFTING(Event, std::any data={});
void CLOSED(Event);
void OPENING(Event);
void CLOSING(Event);
bool mouse(float x, float y, guecs::Modifiers mods);
void render(sf::RenderWindow &window);
bool is_open();
void load_blanket();
void clear_blanket();
void select_item(SelectedItem pair);
void show_craft_result();
void clear_craft_result();
void run_crafting_engine();
void complete_combine();
void update_selection_state();
void play_blanket(const std::string& form);
::ritual::Blanket& blanket();
};
}
}

156
src/gui/status_ui.cpp Normal file
View file

@ -0,0 +1,156 @@
#include "gui/status_ui.hpp"
#include "components.hpp"
#include <guecs/ui.hpp>
#include "rand.hpp"
#include <fmt/xchar.h>
#include "gui/guecstra.hpp"
#include "systems.hpp"
#include "inventory.hpp"
#include "game_level.hpp"
namespace gui {
using namespace guecs;
using std::any, std::any_cast, std::string, std::make_any;
StatusUI::StatusUI()
{
$gui.position(STATUS_UI_X, STATUS_UI_Y, STATUS_UI_WIDTH, STATUS_UI_HEIGHT);
$gui.layout(
"[ritual_ui]"
"[=earring|=armor_head|=amulet]"
"[=back|=*%(200,300)character_view|_|=armor_bdy]"
"[=hand_r|_|_ |=hand_l]"
"[=ring_r|_|_ |=ring_l]"
"[=pocket_r|=armor_leg|=pocket_l]");
}
void StatusUI::init() {
$gui.set<Background>($gui.MAIN, {$gui.$parser, });
for(auto& [name, cell] : $gui.cells()) {
auto gui_id = $gui.entity(name);
if(name == "character_view") {
$gui.set<Rectangle>(gui_id, {});
$gui.set<Sprite>(gui_id, {"peasant_girl"});
} else {
$gui.set<Rectangle>(gui_id, {});
if(name == "ritual_ui") {
$gui.set<Clickable>(gui_id, {
[this](auto){ select_ritual(); }
});
$gui.set<Sound>(gui_id, {"pickup"});
} else {
$gui.set<Text>(gui_id, {guecs::to_wstring(name)});
$gui.set<Clickable>(gui_id, {
guecs::make_action(gui_id, game::Event::INV_SELECT, {gui_id})
});
$gui.set<DropTarget>(gui_id, {
.commit=[&, gui_id](DinkyECS::Entity world_target) -> bool {
return place_slot(gui_id, world_target);
}
});
}
}
}
$ritual_ui.event(ritual::Event::STARTED);
$gui.init();
update();
}
bool StatusUI::mouse(float x, float y, guecs::Modifiers mods) {
if($ritual_ui.is_open()) {
return $ritual_ui.mouse(x, y, mods);
} else {
return $gui.mouse(x, y, mods);
}
}
void StatusUI::select_ritual() {
$ritual_ui.event(ritual::Event::TOGGLE);
}
void StatusUI::update() {
auto world = GameDB::current_world();
auto player = world->get_the<components::Player>();
auto& inventory = world->get<inventory::Model>(player.entity);
for(const auto& [slot, cell] : $gui.cells()) {
if(inventory.has(slot)) {
auto gui_id = $gui.entity(slot);
auto world_entity = inventory.get(slot);
auto& sprite = world->get<components::Sprite>(world_entity);
$gui.set_init<guecs::Icon>(gui_id, {sprite.name});
guecs::GrabSource grabber{ world_entity,
[&, gui_id]() { return remove_slot(gui_id); }};
grabber.setSprite($gui, gui_id);
$gui.set<guecs::GrabSource>(gui_id, grabber);
} else {
auto gui_id = $gui.entity(slot);
if($gui.has<guecs::GrabSource>(gui_id)) {
$gui.remove<guecs::GrabSource>(gui_id);
$gui.remove<guecs::Icon>(gui_id);
}
}
}
}
void StatusUI::render(sf::RenderWindow &window) {
$gui.render(window);
// $gui.debug_layout(window);
$ritual_ui.render(window);
}
void StatusUI::update_level() {
init();
}
bool StatusUI::place_slot(guecs::Entity gui_id, DinkyECS::Entity world_entity) {
auto& level = GameDB::current_level();
auto& slot_name = $gui.name_for(gui_id);
auto& inventory = level.world->get<inventory::Model>(level.player);
if(inventory.add(slot_name, world_entity)) {
level.world->make_constant(world_entity);
update();
return true;
} else {
dbc::log("there's something there already");
return false;
}
}
void StatusUI::drop_item(DinkyECS::Entity item_id) {
System::drop_item(item_id);
update();
}
// NOTE: do I need this or how does it relate to drop_item?
void StatusUI::remove_slot(guecs::Entity slot_id) {
auto player = GameDB::the_player();
auto& slot_name = $gui.name_for(slot_id);
System::remove_from_container(player, slot_name);
update();
}
void StatusUI::swap(guecs::Entity gui_a, guecs::Entity gui_b) {
if(gui_a != gui_b) {
auto player = GameDB::the_player();
auto& a_name = $gui.name_for(gui_a);
auto& b_name = $gui.name_for(gui_b);
System::inventory_swap(player, a_name, b_name);
}
update();
}
bool StatusUI::occupied(guecs::Entity slot) {
auto player = GameDB::the_player();
return System::inventory_occupied(player, $gui.name_for(slot));
}
}

35
src/gui/status_ui.hpp Normal file
View file

@ -0,0 +1,35 @@
#pragma once
#include "constants.hpp"
#include <deque>
#include "textures.hpp"
#include <guecs/ui.hpp>
#include "gui/ritual_ui.hpp"
#include "gui/guecstra.hpp"
namespace gui {
class StatusUI {
public:
guecs::UI $gui;
ritual::UI $ritual_ui;
explicit StatusUI();
StatusUI(const StatusUI& other) = delete;
StatusUI(StatusUI&& other) = delete;
~StatusUI() = default;
void select_ritual();
void update_level();
void init();
void render(sf::RenderWindow &window);
void update();
bool mouse(float x, float y, guecs::Modifiers mods);
void remove_slot(guecs::Entity slot_id);
bool place_slot(guecs::Entity gui_id, DinkyECS::Entity world_entity);
void drop_item(DinkyECS::Entity item_id);
void swap(guecs::Entity gui_a, guecs::Entity gui_b);
bool occupied(guecs::Entity slot);
};
}

99
src/inventory.cpp Normal file
View file

@ -0,0 +1,99 @@
#include "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(fmt::format("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),
fmt::format("entity {} in by_slot isn't in by_entity?", ent));
dbc::check(by_entity.at(ent) == slot,
fmt::format("mismatched slot {} in by_slot doesn't match entity {}", slot, ent));
}
for(auto& [ent, slot] : by_entity) {
dbc::check(by_slot.contains(slot),
fmt::format("slot {} in by_entity isn't in by_slot?", ent));
dbc::check(by_slot.at(slot) == ent,
fmt::format("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/inventory.hpp Normal file
View file

@ -0,0 +1,25 @@
#pragma once
#include "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/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)};
}
};
}

87
src/lights.cpp Normal file
View file

@ -0,0 +1,87 @@
#include "lights.hpp"
#include "constants.hpp"
#include "textures.hpp"
#include <vector>
using std::vector;
namespace lighting {
LightRender::LightRender(Matrix& tiles) :
$width(matrix::width(tiles)),
$height(matrix::height(tiles)),
$lightmap(matrix::make($width, $height)),
$ambient(matrix::make($width, $height)),
$paths($width, $height),
$fow(matrix::make($width, $height))
{
auto& tile_ambient = textures::get_ambient_light();
for(matrix::each_cell it{tiles}; it.next();) {
size_t tile_id = tiles[it.y][it.x];
$ambient[it.y][it.x] = MIN + tile_ambient[tile_id];
}
}
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) {
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 boosted = strength + BOOST;
int new_level = distance <= 1.0f ? boosted : boosted / sqrt(distance);
int cur_level = $lightmap[y][x];
return cur_level < new_level ? new_level : cur_level;
}
void LightRender::reset_light() {
$lightmap = $ambient;
}
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::update_fow(Point at, LightSource source) {
for(matrix::circle it{$lightmap, at, source.radius}; it.next();) {
for(auto x = it.left; x < it.right; x++) {
$fow[it.y][x] = $lightmap[it.y][x];
}
}
}
void LightRender::path_light(Matrix &walls) {
$paths.compute_paths(walls);
}
}

40
src/lights.hpp Normal file
View file

@ -0,0 +1,40 @@
#pragma once
#include <array>
#include "dbc.hpp"
#include "point.hpp"
#include <algorithm>
#include "matrix.hpp"
#include "pathing.hpp"
#include "components.hpp"
namespace lighting {
using components::LightSource;
// THESE ARE PERCENTAGES!
const int MIN = 20;
const int BOOST = 10;
class LightRender {
public:
size_t $width;
size_t $height;
Matrix $lightmap;
Matrix $ambient;
Pathing $paths;
matrix::Matrix $fow;
LightRender(Matrix& walls);
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 update_fow(Point player_pos, LightSource source);
Matrix &lighting() { return $lightmap; }
Matrix &paths() { return $paths.paths(); }
};
}

67
src/main.cpp Normal file
View file

@ -0,0 +1,67 @@
#include "gui/fsm.hpp"
#include "textures.hpp"
#include "sound.hpp"
#include "autowalker.hpp"
#include "ai.hpp"
#include <iostream>
#include "shaders.hpp"
#include "backend.hpp"
#include "game_level.hpp"
#include "camera.hpp"
int main(int argc, char* argv[]) {
try {
sfml::Backend backend;
shaders::init();
components::init();
guecs::init(&backend);
ai::init("ai");
GameDB::init();
cinematic::init();
sound::mute(true);
gui::FSM main;
main.event(game::Event::START);
Autowalker walker(main);
sound::play("ambient_1", true);
if(argc > 1 && argv[1][0] == 't') {
walker.start_autowalk();
}
while(main.active()) {
main.render();
if(main.in_state(gui::State::BOSS_FIGHT)) {
main.handle_boss_fight_events();
} else {
// BUG: need to sort out how to deal with this in the FSM
if(main.in_state(gui::State::IDLE)
|| main.in_state(gui::State::LOOTING)
|| main.in_state(gui::State::CUT_SCENE_PLAYING)
|| main.in_state(gui::State::IN_COMBAT))
{
if(main.autowalking) {
walker.autowalk();
} else {
main.handle_keyboard_mouse();
}
} else{
main.event(game::Event::TICK);
}
main.handle_world_events();
}
}
return 0;
} catch(const std::system_error& e) {
std::cout << "WARNING: On OSX you'll get this error on shutdown.\n";
std::cout << "Caught system_error with code "
"[" << e.code() << "] meaning "
"[" << e.what() << "]\n";
}
}

150
src/map.cpp Normal file
View file

@ -0,0 +1,150 @@
#include "map.hpp"
#include "dbc.hpp"
#include "rand.hpp"
#include <vector>
#include <array>
#include <fmt/core.h>
#include <utility>
#include "matrix.hpp"
#include "rand.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 $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,
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::init_tiles() {
$tiles = $walls;
}
void Map::enclose() {
// wraps the outside edge with solid walls
std::array<Point, 4> starts{{
{0,0}, {$width-1, 0}, {$width-1, $height-1}, {0, $height-1}
}};
std::array<Point, 4> ends{{
{$width-1, 0}, {$width-1, $height-1}, {0, $height-1}, {0,0},
}};
for(size_t i = 0; i < starts.size(); i++) {
for(matrix::line it{starts[i], ends[i]}; it.next();) {
$walls[it.y][it.x] = 1;
}
}
}
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;
}
}

72
src/map.hpp Normal file
View file

@ -0,0 +1,72 @@
#pragma once
#include <vector>
#include <utility>
#include <string>
#include <random>
#include <algorithm>
#include <fmt/core.h>
#include "point.hpp"
#include "lights.hpp"
#include "pathing.hpp"
#include "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;
};
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;
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 enclose();
void dump(int show_x=-1, int show_y=-1);
bool INVARIANT();
void init_tiles();
void add_room(Room &room);
void invert_space();
};

36
src/matrix.cpp Normal file
View file

@ -0,0 +1,36 @@
#include "matrix.hpp"
#include <fmt/core.h>
#include "constants.hpp"
using namespace fmt;
using std::min, std::max;
namespace matrix {
void dump(const std::string &msg, Matrix &map, int show_x, int show_y) {
println("----------------- {}", msg);
for(each_row it{map}; it.next();) {
int cell = map[it.y][it.x];
if(int(it.x) == show_x && int(it.y) == show_y) {
if(cell == WALL_PATH_LIMIT) {
print("!<", cell);
} else {
print("{:x}<", cell);
}
} else if(cell == WALL_PATH_LIMIT) {
print("# ");
} else if(cell == 0) {
print(". ");
} else if(cell > 15 && cell < 32) {
print("{:x}+", cell - 16);
} else if(cell > 31) {
print("* ");
} else {
print("{:x} ", cell);
}
if(it.row) print("\n");
}
}
}

43
src/matrix.hpp Normal file
View file

@ -0,0 +1,43 @@
#pragma once
#include "shiterator.hpp"
namespace matrix {
using Row = shiterator::BaseRow<int>;
using Matrix = shiterator::Base<int>;
using viewport = shiterator::viewport_t<Matrix>;
using each_cell = shiterator::each_cell_t<Matrix>;
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 rando_rect = shiterator::rando_rect_t<Matrix>;
using rando_box = shiterator::rando_box_t<Matrix>;
using line = shiterator::line;
void dump(const std::string &msg, Matrix &map, int show_x=-1, int show_y=-1);
inline Matrix make(size_t width, size_t height) {
return shiterator::make<int>(width, height);
}
inline bool inbounds(Matrix &mat, size_t x, size_t y) {
return shiterator::inbounds(mat, x, y);
}
inline size_t width(Matrix &mat) {
return shiterator::width(mat);
}
inline size_t height(Matrix &mat) {
return shiterator::height(mat);
}
inline void assign(Matrix &out, int new_value) {
shiterator::assign(out, new_value);
}
}

221
src/maze.cpp Normal file
View file

@ -0,0 +1,221 @@
#include <fmt/core.h>
#include <string>
#include "rand.hpp"
#include "constants.hpp"
#include "maze.hpp"
using std::string;
using matrix::Matrix;
namespace maze {
inline size_t rand(size_t i, size_t j) {
if(i < j) {
return Random::uniform(i, j);
} else if(j < i) {
return Random::uniform(j, i);
} else {
return i;
}
}
inline bool complete(Matrix& maze) {
size_t width = matrix::width(maze);
size_t height = matrix::height(maze);
for(size_t row = 1; row < height; row += 2) {
for(size_t col = 1; col < width; col += 2) {
if(maze[row][col] != 0) return false;
}
}
return true;
}
std::vector<Point> neighborsAB(Matrix& maze, Point on) {
std::vector<Point> result;
std::array<Point, 4> points{{
{on.x, on.y - 2},
{on.x, on.y + 2},
{on.x - 2, on.y},
{on.x + 2, on.y}
}};
for(auto point : points) {
if(matrix::inbounds(maze, point.x, point.y)) {
result.push_back(point);
}
}
return result;
}
std::vector<Point> neighbors(Matrix& maze, Point on) {
std::vector<Point> result;
std::array<Point, 4> points{{
{on.x, on.y - 2},
{on.x, on.y + 2},
{on.x - 2, on.y},
{on.x + 2, on.y}
}};
for(auto point : points) {
if(matrix::inbounds(maze, point.x, point.y)) {
if(maze[point.y][point.x] == WALL_VALUE) {
result.push_back(point);
}
}
}
return result;
}
inline std::pair<Point, Point> find_coord(Matrix& maze) {
size_t width = matrix::width(maze);
size_t height = matrix::height(maze);
for(size_t y = 1; y < height; y += 2) {
for(size_t x = 1; x < width; x += 2) {
if(maze[y][x] == WALL_VALUE) {
auto found = neighborsAB(maze, {x, y});
for(auto point : found) {
if(maze[point.y][point.x] == 0) {
return {{x, y}, point};
}
}
}
}
}
matrix::dump("BAD MAZE", maze);
dbc::sentinel("failed to find coord?");
}
void Builder::randomize_rooms() {
// use those dead ends to randomly place rooms
for(auto at : $dead_ends) {
if(Random::uniform(0,1)) {
size_t offset = Random::uniform(0,1);
Room cur{at.x+offset, at.y+offset, 1, 1};
$rooms.push_back(cur);
}
}
}
void Builder::init() {
matrix::assign($walls, WALL_VALUE);
}
void Builder::divide(Point start, Point end) {
for(matrix::line it{start, end}; it.next();) {
$walls[it.y][it.x] = 0;
$walls[it.y+1][it.x] = 0;
}
}
void Builder::hunt_and_kill(Point on) {
for(auto& room : $rooms) {
for(matrix::box it{$walls, room.x, room.y, room.width}; it.next();) {
$walls[it.y][it.x] = 0;
}
}
while(!complete($walls)) {
auto n = neighbors($walls, on);
if(n.size() == 0) {
$dead_ends.push_back(on);
auto t = find_coord($walls);
on = t.first;
$walls[on.y][on.x] = 0;
size_t row = (on.y + t.second.y) / 2;
size_t col = (on.x + t.second.x) / 2;
$walls[row][col] = 0;
} else {
auto nb = n[rand(size_t(0), n.size() - 1)];
$walls[nb.y][nb.x] = 0;
size_t row = (nb.y + on.y) / 2;
size_t col = (nb.x + on.x) / 2;
$walls[row][col] = 0;
on = nb;
}
}
for(auto at : $dead_ends) {
for(auto& room : $rooms) {
Point room_ul{room.x - room.width - 1, room.y - room.height - 1};
Point room_lr{room.x + room.width - 1, room.y + room.height - 1};
if(at.x >= room_ul.x && at.y >= room_ul.y &&
at.x <= room_lr.x && at.y <= room_lr.y)
{
for(matrix::compass it{$walls, at.x, at.y}; it.next();) {
if($walls[it.y][it.x] == 1) {
$walls[it.y][it.x] = 0;
break;
}
}
}
}
}
}
void Builder::inner_donut(float outer_rad, float inner_rad) {
size_t x = matrix::width($walls) / 2;
size_t y = matrix::height($walls) / 2;
for(matrix::circle it{$walls, {x, y}, outer_rad};
it.next();)
{
for(int x = it.left; x < it.right; x++) {
$walls[it.y][x] = 0;
}
}
for(matrix::circle it{$walls, {x, y}, inner_rad};
it.next();)
{
for(int x = it.left; x < it.right; x++) {
$walls[it.y][x] = 1;
}
}
}
void Builder::inner_box(size_t outer_size, size_t inner_size) {
size_t x = matrix::width($walls) / 2;
size_t y = matrix::height($walls) / 2;
for(matrix::box it{$walls, x, y, outer_size};
it.next();)
{
$walls[it.y][it.x] = 0;
}
for(matrix::box it{$walls, x, y, inner_size};
it.next();)
{
$walls[it.y][it.x] = 1;
}
}
void Builder::remove_dead_ends() {
dbc::check($dead_ends.size() > 0, "you have to run an algo first, no dead_ends to remove");
for(auto at : $dead_ends) {
for(matrix::compass it{$walls, at.x, at.y}; it.next();) {
if($walls[it.y][it.x] == 0) {
int diff_x = at.x - it.x;
int diff_y = at.y - it.y;
$walls[at.y + diff_y][at.x + diff_x] = 0;
}
}
}
}
void Builder::dump(const std::string& msg) {
matrix::dump(msg, $walls);
}
}

29
src/maze.hpp Normal file
View file

@ -0,0 +1,29 @@
#pragma once
#include "matrix.hpp"
#include "map.hpp"
namespace maze {
struct Builder {
Matrix& $walls;
std::vector<Room>& $rooms;
std::vector<Point>& $dead_ends;
Builder(Map& map) :
$walls(map.$walls), $rooms(map.$rooms), $dead_ends(map.$dead_ends)
{
init();
}
void hunt_and_kill(Point on={1,1});
void init();
void randomize_rooms();
void inner_donut(float outer_rad, float inner_rad);
void inner_box(size_t outer_size, size_t inner_size);
void divide(Point start, Point end);
void remove_dead_ends();
void dump(const std::string& msg);
};
}

51
src/meson.build Normal file
View file

@ -0,0 +1,51 @@
sources = files(
'ai.cpp',
'ai_debug.cpp',
'animation.cpp',
'autowalker.cpp',
'backend.cpp',
'battle.cpp',
'boss/fight.cpp',
'boss/system.cpp',
'boss/ui.cpp',
'camera.cpp',
'combat.cpp',
'components.cpp',
'config.cpp',
'dbc.cpp',
'game_level.cpp',
'goap.cpp',
'gui/combat_ui.cpp',
'gui/debug_ui.cpp',
'gui/dnd_loot.cpp',
'gui/event_router.cpp',
'gui/fsm.cpp',
'gui/guecstra.cpp',
'gui/loot_ui.cpp',
'gui/main_ui.cpp',
'gui/map_view.cpp',
'gui/mini_map.cpp',
'gui/overlay_ui.cpp',
'gui/ritual_ui.cpp',
'gui/status_ui.cpp',
'inventory.cpp',
'lights.cpp',
'map.cpp',
'matrix.cpp',
'maze.cpp',
'palette.cpp',
'pathing.cpp',
'rand.cpp',
'raycaster.cpp',
'rituals.cpp',
'scene.cpp',
'shaders.cpp',
'sound.cpp',
'spatialmap.cpp',
'stats.cpp',
'storyboard/ui.cpp',
'systems.cpp',
'textures.cpp',
'worldbuilder.cpp',
'easing.cpp',
)

72
src/palette.cpp Normal file
View file

@ -0,0 +1,72 @@
#include <fmt/core.h>
#include "palette.hpp"
#include "config.hpp"
#include "dbc.hpp"
namespace palette {
using std::string;
using nlohmann::json;
struct PaletteMgr {
std::unordered_map<string, sf::Color> palettes;
std::string config;
std::unordered_map<string, string> pending_refs;
bool initialized = false;
};
static PaletteMgr COLOR;
bool initialized() {
return COLOR.initialized;
}
void init(const string &json_file) {
if(!COLOR.initialized) {
COLOR.initialized = true;
COLOR.config = json_file;
auto config = settings::get(json_file);
json& colors = config.json();
for(auto [key, value_specs] : colors.items()) {
const string& base_key = key;
for(auto [value, rgba] : value_specs.items()) {
auto color_path = base_key + ":" + value;
dbc::check(!COLOR.palettes.contains(color_path),
fmt::format("PALLETES config {} already has a color path {}", COLOR.config, color_path));
if(rgba.type() == json::value_t::string) {
COLOR.pending_refs.try_emplace(color_path, rgba);
} else {
uint8_t alpha = rgba.size() == 3 ? 255 : (uint8_t)rgba[3];
sf::Color color{rgba[0], rgba[1], rgba[2], alpha};
COLOR.palettes.try_emplace(color_path, color);
}
}
}
for(auto [color_path, ref] : COLOR.pending_refs) {
dbc::check(COLOR.palettes.contains(ref),
fmt::format("In {} you have {} referring to {} but {} doesn't exist.",
COLOR.config, color_path, ref, ref));
dbc::check(!COLOR.palettes.contains(color_path),
fmt::format("Color {} with ref {} is duplicated.", color_path, ref));
auto color = COLOR.palettes.at(ref);
COLOR.palettes.try_emplace(color_path, color);
}
}
}
sf::Color get(const string& key) {
dbc::check(COLOR.palettes.contains(key),
fmt::format("COLOR {} is missing from {}", key, COLOR.config));
return COLOR.palettes.at(key);
}
sf::Color get(const string& key, const string& value) {
return get(key + ":" + value);
}
}

13
src/palette.hpp Normal file
View file

@ -0,0 +1,13 @@
#include <string>
#include <SFML/Graphics/Color.hpp>
namespace palette {
using std::string;
bool initialized();
void init(const std::string &config="palette");
sf::Color get(const string &key);
sf::Color get(const string &key, const string &value);
}

134
src/pathing.cpp Normal file
View file

@ -0,0 +1,134 @@
#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);
}
}
}
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) {
$input[at.y][at.x] = value;
}
void Pathing::clear_target(const Point &at) {
$input[at.y][at.x] = 1;
}
PathingResult Pathing::find_path(Point &out, int direction, bool diag)
{
// get the current dijkstra number
int cur = $paths[out.y][out.x];
int target = cur;
bool found = false;
// a lambda makes it easy to capture what we have to change
auto next_step = [&](size_t x, size_t y) -> bool {
target = $paths[y][x];
// don't go through walls
if(target == WALL_PATH_LIMIT) return false;
int weight = cur - target;
if(weight == direction) {
out = {x, y};
found = true;
// only break if this is a lower path
return true;
} else if(weight == 0) {
out = {x, y};
found = true;
// only found an equal path, keep checking
}
// this says keep going
return false;
};
if(diag) {
for(matrix::box it{$paths, out.x, out.y, 1}; it.next();) {
bool should_stop = next_step(it.x, it.y);
if(should_stop) break;
}
} else {
for(matrix::compass it{$paths, out.x, out.y}; it.next();) {
bool should_stop = next_step(it.x, it.y);
if(should_stop) break;
}
}
if(target == 0) {
return PathingResult::FOUND;
} else if(!found) {
return PathingResult::FAIL;
} else {
return PathingResult::CONTINUE;
}
}
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;
}

40
src/pathing.hpp Normal file
View file

@ -0,0 +1,40 @@
#pragma once
#include "point.hpp"
#include "matrix.hpp"
#include <functional>
using matrix::Matrix;
constexpr const int PATHING_TOWARD=1;
constexpr const int PATHING_AWAY=-1;
enum class PathingResult {
FAIL=0,
FOUND=1,
CONTINUE=2
};
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];}
PathingResult find_path(Point &out, int direction, bool diag);
bool INVARIANT();
};

20
src/point.hpp Normal file
View file

@ -0,0 +1,20 @@
#pragma once
#include <vector>
struct Point {
size_t x = 0;
size_t y = 0;
bool operator==(const Point& other) const {
return other.x == x && other.y == y;
}
};
typedef std::vector<Point> PointList;
template<> struct std::hash<Point> {
size_t operator()(const Point& p) const {
auto hasher = std::hash<int>();
return hasher(p.x) ^ hasher(p.y);
}
};

13
src/rand.cpp Normal file
View file

@ -0,0 +1,13 @@
#include "rand.hpp"
namespace Random {
std::random_device RNG;
std::mt19937 GENERATOR(RNG());
std::chrono::milliseconds milliseconds(int from, int to) {
int tick = Random::uniform_real(float(from), float(to));
return std::chrono::milliseconds{tick};
}
}

31
src/rand.hpp Normal file
View file

@ -0,0 +1,31 @@
#pragma once
#include <random>
#include <chrono>
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 mean, T stddev) {
std::normal_distribution<T> rand(mean, stddev);
return rand(GENERATOR);
}
std::chrono::milliseconds milliseconds(int from, int to);
}

526
src/raycaster.cpp Normal file
View file

@ -0,0 +1,526 @@
#include "raycaster.hpp"
#include "dbc.hpp"
#include "matrix.hpp"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <fmt/core.h>
#include <memory>
#include <numbers>
#include "components.hpp"
#include "textures.hpp"
#include "systems.hpp"
#include "shaders.hpp"
#include "animation.hpp"
using namespace fmt;
using std::make_unique, std::shared_ptr;
union ColorConv {
struct {
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t a;
} as_color;
RGBA as_int;
};
// from: https://permadi.com/1996/05/ray-casting-tutorial-19/
// Intensity = (kI/(d+do))*(N*L)
// rcr says: kI = intensity coefficient, d = distance, d0 = fudge term to prevent division by zero, N is surface, L is direction to light from surface
//
// That formula is just "Inverse-square law" (except they don't square, which is physically dubious), and "Lambertian reflectance" ("Diffuse reflection") which sounds fancy but is super standard. All the quoted terms have wikipedia articles
//
// Distance means distance to surface from light.
//
// Intensity = Object Intensity/Distance * Multiplier
//
/* It's hard to believe, but this is faster than any bitfiddling
* I could devise. Just use a union with a struct, do the math
* and I guess the compiler can handle it better than shifting
* bits around.
*/
inline RGBA lighting_calc(RGBA pixel, float dist, int level) {
ColorConv conv{.as_int=pixel};
if(conv.as_color.b < GLOW_LIMIT
&& conv.as_color.r < GLOW_LIMIT
&& conv.as_color.g < GLOW_LIMIT)
{
float intensity = (float(level) * PERCENT) / (dist + 1) * LIGHT_MULTIPLIER;
conv.as_color.r *= intensity;
conv.as_color.g *= intensity;
conv.as_color.b *= intensity;
}
return conv.as_int;
}
Raycaster::Raycaster(int width, int height) :
$view_texture(sf::Vector2u{(unsigned int)width, (unsigned int)height}),
$view_sprite($view_texture),
$width(width), $height(height),
$zbuffer(width)
{
$view_sprite.setPosition({0, 0});
$pixels = make_unique<RGBA[]>($width * $height);
$view_texture.setSmooth(false);
$camera.target_x = $pos_x;
$camera.target_y = $pos_y;
update_camera_aiming();
}
void Raycaster::set_position(int x, int y) {
$screen_pos_x = x;
$screen_pos_y = y;
$view_sprite.setPosition({(float)x, (float)y});
}
void Raycaster::position_camera(float player_x, float player_y) {
// x and y start position
$pos_x = player_x;
$pos_y = player_y;
$dir_x = 1;
$dir_y = 0;
$plane_x = 0;
$plane_y = 0.66;
update_camera_aiming();
}
void Raycaster::draw_pixel_buffer() {
$view_texture.update((uint8_t *)$pixels.get(), {(unsigned int)$width, (unsigned int)$height}, {0, 0});
}
void Raycaster::apply_sprite_effect(shared_ptr<sf::Shader> effect, float width, float height) {
effect->setUniform("u_time", $clock.getElapsedTime().asSeconds());
sf::Vector2f u_resolution{width, height};
effect->setUniform("u_resolution", u_resolution);
}
inline void step_animation(animation::Animation& anim, sf::Sprite& sprite, sf::Vector2f& position, sf::Vector2f& scale, sf::IntRect& in_texture, sf::Vector2f& origin) {
anim.update();
anim.apply(sprite, in_texture);
anim.motion(sprite, position, scale);
sprite.setOrigin(origin);
sprite.setTextureRect(in_texture);
}
inline void set_scale_position(sf::Sprite& sprite, sf::Vector2f& position, sf::Vector2f& scale, sf::IntRect& in_texture, sf::Vector2f& origin) {
sprite.setScale(scale);
sprite.setPosition(position);
sprite.setOrigin(origin);
sprite.setTextureRect(in_texture);
}
void Raycaster::sprite_casting(sf::RenderTarget &target) {
auto& lights = $level.lights->lighting();
auto world = $level.world;
$level.collision->distance_sorted($sprite_order, {(size_t)$pos_x, (size_t)$pos_y}, RENDER_DISTANCE);
// after sorting the sprites, do the projection
for(auto& rec : $sprite_order) {
if(!$sprites.contains(rec.entity)) continue;
auto& sprite_texture = $sprites.at(rec.entity);
int texture_width = (float)sprite_texture.frame_size.x;
int texture_height =(float)sprite_texture.frame_size.y;
int half_height = texture_height / 2;
auto& sf_sprite = sprite_texture.sprite;
auto sprite_pos = world->get<components::Position>(rec.entity);
double sprite_x = double(sprite_pos.location.x) - rec.wiggle - $pos_x + 0.5;
double sprite_y = double(sprite_pos.location.y) - rec.wiggle - $pos_y + 0.5;
double inv_det = 1.0 / ($plane_x * $dir_y - $dir_x * $plane_y); // required for correct matrix multiplication
double transform_x = inv_det * ($dir_y * sprite_x - $dir_x * sprite_y);
//this is actually the depth inside the screen, that what Z is in 3D, the distance of sprite to player, matching sqrt(spriteDistance[i])
double transform_y = inv_det * (-$plane_y * sprite_x + $plane_x * sprite_y);
int sprite_screen_x = int(($width / 2) * (1 + transform_x / transform_y));
// calculate the height of the sprite on screen
//using "transform_y" instead of the real distance prevents fisheye
int sprite_height = abs(int($height / transform_y));
if(sprite_height == 0) continue;
// calculate width the the sprite
// same as height of sprite, given that it's square
int sprite_width = abs(int($height / transform_y));
if(sprite_width == 0) continue;
int draw_start_x = -sprite_width / 2 + sprite_screen_x;
if(draw_start_x < 0) draw_start_x = 0;
int draw_end_x = sprite_width / 2 + sprite_screen_x;
if(draw_end_x > $width) draw_end_x = $width;
int stripe = draw_start_x;
for(; stripe < draw_end_x; stripe++) {
//the conditions in the if are:
//1) it's in front of camera plane so you don't see things behind you
//2) $zbuffer, with perpendicular distance
if(!(transform_y > 0 && transform_y < $zbuffer[stripe])) break;
}
int tex_x_end = int(texture_width * (stripe - (-sprite_width / 2 + sprite_screen_x)) * texture_width / sprite_width) / texture_width;
if(draw_start_x < draw_end_x && transform_y > 0 && transform_y < $zbuffer[draw_start_x]) {
//calculate lowest and highest pixel to fill in current stripe
int draw_start_y = -sprite_height / 2 + $height / 2;
if(draw_start_y < 0) draw_start_y = 0;
int tex_x = int(texture_width * (draw_start_x - (-sprite_width / 2 + sprite_screen_x)) * texture_width / sprite_width) / texture_width;
int tex_render_width = tex_x_end - tex_x;
// avoid drawing sprites that are not visible (width < 0)
if(tex_render_width <= 0) continue;
float x = float(draw_start_x + $screen_pos_x);
float y = float(draw_start_y + $screen_pos_y);
if(x < $screen_pos_x) dbc::log("X < rayview left bounds");
if(y < $screen_pos_y) dbc::log("Y < rayview top bounds");
if(x >= SCREEN_WIDTH) dbc::log("OUT OF BOUNDS X");
if(y >= $height) dbc::log("OUT OF BOUNDS Y");
float sprite_scale_w = float(sprite_width) / float(texture_width);
float sprite_scale_h = float(sprite_height) / float(texture_height);
int d = y * texture_height - $height * half_height + sprite_height * half_height;
int tex_y = ((d * texture_height) / sprite_height) / texture_height;
// BUG: this data could be put into the world
// as frame data, then just have a system that
// constantly applies this to any sprite that
// has an animation and is visible
sf::Vector2f origin{texture_width / 2.0f, texture_height / 2.0f};
sf::Vector2f scale{sprite_scale_w, sprite_scale_h};
sf::Vector2f position{x + origin.x * scale.x, y + origin.y * scale.y};
sf::IntRect in_texture{ {tex_x, tex_y}, {tex_render_width, texture_height}};
shared_ptr<sf::Shader> effect = System::sprite_effect(rec.entity);
float level = lights[sprite_pos.location.y][sprite_pos.location.x] * PERCENT;
if(effect) {
apply_sprite_effect(effect, sprite_width, sprite_height);
} else {
effect = $brightness;
level += (aiming_at == sprite_pos.location) * AIMED_AT_BRIGHTNESS;
effect->setUniform("darkness", level);
}
auto anim = world->get_if<animation::Animation>(rec.entity);
if(anim != nullptr && anim->playing) {
step_animation(*anim, *sf_sprite, position, scale, in_texture, origin);
} else {
set_scale_position(*sf_sprite, position, scale, in_texture, origin);
}
target.draw(*sf_sprite, effect.get());
}
}
}
void Raycaster::cast_rays() {
constexpr static const int texture_width = TEXTURE_WIDTH;
constexpr static const int texture_height = TEXTURE_HEIGHT;
double perp_wall_dist;
auto& lights = $level.lights->lighting();
// WALL CASTING
for(int x = 0; x < $width; x++) {
// calculate ray position and direction
double cameraX = 2 * x / double($width) - 1; // x-coord in camera space
double ray_dir_x = $dir_x + $plane_x * cameraX;
double ray_dir_y = $dir_y + $plane_y * cameraX;
// which box of the map we're in
int map_x = int($pos_x);
int map_y = int($pos_y);
// length of ray from one x or y-side to next x or y-side
double delta_dist_x = std::abs(1.0 / ray_dir_x);
double delta_dist_y = std::abs(1.0 / ray_dir_y);
int step_x = 0;
int step_y = 0;
int hit = 0;
int side = 0;
// length of ray from current pos to next x or y-side
double side_dist_x;
double side_dist_y;
if(ray_dir_x < 0) {
step_x = -1;
side_dist_x = ($pos_x - map_x) * delta_dist_x;
} else {
step_x = 1;
side_dist_x = (map_x + 1.0 - $pos_x) * delta_dist_x;
}
if(ray_dir_y < 0) {
step_y = -1;
side_dist_y = ($pos_y - map_y) * delta_dist_y;
} else {
step_y = 1;
side_dist_y = (map_y + 1.0 - $pos_y) * delta_dist_y;
}
// perform DDA
while(hit == 0) {
if(side_dist_x < side_dist_y) {
side_dist_x += delta_dist_x;
map_x += step_x;
side = 0;
} else {
side_dist_y += delta_dist_y;
map_y += step_y;
side = 1;
}
if($walls[map_y][map_x] == 1) hit = 1;
}
if(side == 0) {
perp_wall_dist = (side_dist_x - delta_dist_x);
} else {
perp_wall_dist = (side_dist_y - delta_dist_y);
}
int line_height = int($height / perp_wall_dist);
int draw_start = -line_height / 2 + $height / 2 + $pitch;
if(draw_start < 0) draw_start = 0;
int draw_end = line_height / 2 + $height / 2 + $pitch;
if(draw_end >= $height) draw_end = $height - 1;
auto texture = textures::get_surface($tiles[map_y][map_x]);
// calculate value of wall_x
double wall_x; // where exactly the wall was hit
if(side == 0) {
wall_x = $pos_y + perp_wall_dist * ray_dir_y;
} else {
wall_x = $pos_x + perp_wall_dist * ray_dir_x;
}
wall_x -= floor(wall_x);
// x coorindate on the texture
int tex_x = int(wall_x * double(texture_width));
if(side == 0 && ray_dir_x > 0) tex_x = texture_width - tex_x - 1;
if(side == 1 && ray_dir_y < 0) tex_x = texture_width - tex_x - 1;
// LODE: an integer-only bresenham or DDA like algorithm could make the texture coordinate stepping faster
// How much to increase the texture coordinate per screen pixel
double step = 1.0 * texture_height / line_height;
// Starting texture coordinate
double tex_pos = (draw_start - $pitch - $height / 2 + line_height / 2) * step;
for(int y = draw_start; y < draw_end; y++) {
int tex_y = (int)tex_pos & (texture_height - 1);
tex_pos += step;
RGBA pixel = texture[texture_height * tex_y + tex_x];
int light_level = lights[map_y][map_x];
$pixels[pixcoord(x, y)] = lighting_calc(pixel, perp_wall_dist, light_level);
}
// SET THE ZBUFFER FOR THE SPRITE CASTING
$zbuffer[x] = perp_wall_dist;
}
}
void Raycaster::draw_ceiling_floor() {
constexpr const int texture_width = TEXTURE_WIDTH;
constexpr const int texture_height = TEXTURE_HEIGHT;
auto &lights = $level.lights->lighting();
size_t surface_i = 0;
const RGBA *floor_texture = textures::get_surface(surface_i);
const RGBA *ceiling_texture = textures::get_ceiling(surface_i);
for(int y = $height / 2 + 1; y < $height; ++y) {
// rayDir for leftmost ray (x=0) and rightmost (x = w)
float ray_dir_x0 = $dir_x - $plane_x;
float ray_dir_y0 = $dir_y - $plane_y;
float ray_dir_x1 = $dir_x + $plane_x;
float ray_dir_y1 = $dir_y + $plane_y;
// current y position compared to the horizon
int p = y - $height / 2;
// vertical position of the camera
// 0.5 will the camera at the center horizon. For a
// different value you need a separate loop for ceiling
// and floor since they're no longer symmetrical.
float pos_z = 0.5 * $height;
// horizontal distance from the camera to the floor for the current row
// 0.5 is the z position exactly in the middle between floor and ceiling
// See NOTE in Lode's code for more.
float row_distance = pos_z / p;
// calculate the real world step vector we have to add for each x (parallel to camera plane)
// adding step by step avoids multiplications with a wight in the inner loop
float floor_step_x = row_distance * (ray_dir_x1 - ray_dir_x0) / $width;
float floor_step_y = row_distance * (ray_dir_y1 - ray_dir_y0) / $width;
// real world coordinates of the leftmost column.
// This will be updated as we step to the right
float floor_x = $pos_x + row_distance * ray_dir_x0;
float floor_y = $pos_y + row_distance * ray_dir_y0;
for(int x = 0; x < $width; ++x) {
// the cell coord is simply taken from the int parts of
// floor_x and floor_y.
int cell_x = int(floor_x);
int cell_y = int(floor_y);
// get the texture coordinate from the fractional part
int tx = int(texture_width * (floor_x - cell_x)) & (texture_width - 1);
int ty = int(texture_width * (floor_y - cell_y)) & (texture_height - 1);
floor_x += floor_step_x;
floor_y += floor_step_y;
// now get the pixel from the texture
RGBA color;
// this uses the previous ty/tx fractional parts of
// floor_x cell_x to find the texture x/y. How?
int map_x = int(floor_x);
int map_y = int(floor_y);
if(!matrix::inbounds(lights, map_x, map_y)) continue;
int light_level = lights[map_y][map_x];
size_t new_surface_i = $tiles[map_y][map_x];
if(new_surface_i != surface_i) {
surface_i = new_surface_i;
floor_texture = textures::get_surface(surface_i);
ceiling_texture = textures::get_ceiling(surface_i);
}
// NOTE: use map_x/y to get the floor, ceiling texture.
// FLOOR
color = floor_texture[texture_width * ty + tx];
$pixels[pixcoord(x, y)] = lighting_calc(color, row_distance, light_level);
// CEILING
color = ceiling_texture[texture_width * ty + tx];
$pixels[pixcoord(x, $height - y - 1)] = lighting_calc(color, row_distance, light_level);
}
}
}
void Raycaster::render() {
draw_ceiling_floor();
cast_rays();
draw_pixel_buffer();
}
void Raycaster::draw(sf::RenderTarget& target) {
target.draw($view_sprite);
sprite_casting(target);
}
void Raycaster::update_sprite(DinkyECS::Entity ent, components::Sprite& sprite) {
auto sprite_txt = textures::get_sprite(sprite.name);
$sprites.insert_or_assign(ent, sprite_txt);
}
void Raycaster::update_level(GameDB::Level& level) {
$sprites.clear();
$sprite_order.clear();
$level = level;
$tiles = $level.map->tiles();
$walls = $level.map->walls();
$level.world->query<components::Sprite>([&](const auto ent, auto& sprite) {
// player doesn't need a sprite
if($level.player != ent) {
update_sprite(ent, sprite);
}
});
}
void Raycaster::init_shaders() {
$brightness = shaders::get("rayview_sprites");
}
Point Raycaster::plan_move(int dir, bool strafe) {
$camera.t = 0.0;
if(strafe) {
$camera.target_x = $pos_x + int(-$dir_y * 1.5 * dir);
$camera.target_y = $pos_y + int($dir_x * 1.5 * dir);
} else {
$camera.target_x = $pos_x + int($dir_x * 1.5 * dir);
$camera.target_y = $pos_y + int($dir_y * 1.5 * dir);
}
return {size_t($camera.target_x), size_t($camera.target_y)};
}
void Raycaster::plan_rotate(int dir, float amount) {
$camera.t = 0.0;
double angle_dir = std::numbers::pi * amount * float(dir);
$camera.target_dir_x = $dir_x * cos(angle_dir) - $dir_y * sin(angle_dir);
$camera.target_dir_y = $dir_x * sin(angle_dir) + $dir_y * cos(angle_dir);
$camera.target_plane_x = $plane_x * cos(angle_dir) - $plane_y * sin(angle_dir);
$camera.target_plane_y = $plane_x * sin(angle_dir) + $plane_y * cos(angle_dir);
}
bool Raycaster::play_rotate() {
$camera.t += $camera.rot_speed;
$dir_x = std::lerp($dir_x, $camera.target_dir_x, $camera.t);
$dir_y = std::lerp($dir_y, $camera.target_dir_y, $camera.t);
$plane_x = std::lerp($plane_x, $camera.target_plane_x, $camera.t);
$plane_y = std::lerp($plane_y, $camera.target_plane_y, $camera.t);
update_camera_aiming();
return $camera.t >= 1.0;
}
bool Raycaster::play_move() {
$camera.t += $camera.move_speed;
$pos_x = std::lerp($pos_x, $camera.target_x, $camera.t);
$pos_y = std::lerp($pos_y, $camera.target_y, $camera.t);
update_camera_aiming();
return $camera.t >= 1.0;
}
void Raycaster::abort_plan() {
$camera.target_x = $pos_x;
$camera.target_y = $pos_y;
update_camera_aiming();
}
bool Raycaster::is_target(DinkyECS::Entity entity) {
(void)entity;
return false;
}
void Raycaster::update_camera_aiming() {
aiming_at = { size_t($pos_x + $dir_x), size_t($pos_y + $dir_y) };
camera_at = { size_t($camera.target_x), size_t($camera.target_y) };
}

92
src/raycaster.hpp Normal file
View file

@ -0,0 +1,92 @@
#pragma once
#include <SFML/Graphics.hpp>
#include <SFML/System/Clock.hpp>
#include "spatialmap.hpp"
#include "game_level.hpp"
#include "textures.hpp"
using matrix::Matrix;
using RGBA = uint32_t;
struct CameraLOL {
double t = 0.0;
double move_speed = 0.1;
double rot_speed = 0.06;
double target_x = 0.0;
double target_y = 0.0;
double target_dir_x = 0.0;
double target_dir_y = 0.0;
double target_plane_x = 0.0;
double target_plane_y = 0.0;
};
struct Raycaster {
int $pitch=0;
sf::Clock $clock;
std::shared_ptr<sf::Shader> $brightness = nullptr;
double $pos_x = 0;
double $pos_y = 0;
// initial direction vector
double $dir_x = 1;
double $dir_y = 0;
// the 2d raycaster version of camera plane
double $plane_x = 0.0;
double $plane_y = 0.66;
sf::Texture $view_texture;
sf::Sprite $view_sprite;
Point aiming_at{0,0};
Point camera_at{0,0};
CameraLOL $camera;
std::unique_ptr<RGBA[]> $pixels = nullptr;
int $width;
int $height;
int $screen_pos_x = RAY_VIEW_X;
int $screen_pos_y = RAY_VIEW_Y;
std::unordered_map<DinkyECS::Entity, textures::SpriteTexture> $sprites;
SortedEntities $sprite_order;
GameDB::Level $level;
Matrix $tiles;
Matrix $walls;
std::vector<double> $zbuffer; // width
Raycaster(int width, int height);
void cast_rays();
void draw_ceiling_floor();
void draw_pixel_buffer();
void sprite_casting(sf::RenderTarget& target);
void render();
void draw(sf::RenderTarget& target);
void sort_sprites(std::vector<int>& order, std::vector<double>& dist, int amount);
void set_position(int x, int y);
inline size_t pixcoord(int x, int y) {
return ((y) * $width) + (x);
}
void update_level(GameDB::Level& level);
void update_sprite(DinkyECS::Entity ent, components::Sprite& sprite);
void init_shaders();
// camera things?
void position_camera(float player_x, float player_y);
Point plan_move(int dir, bool strafe);
void plan_rotate(int dir, float amount);
bool play_rotate();
bool play_move();
void abort_plan();
bool is_target(DinkyECS::Entity entity);
void update_camera_aiming();
// BUG: these should go away when Bug #42 is solved
void apply_sprite_effect(std::shared_ptr<sf::Shader> effect, float width, float height);
};

220
src/rituals.cpp Normal file
View file

@ -0,0 +1,220 @@
#include "rituals.hpp"
#include "ai_debug.hpp"
#include "ai.hpp"
namespace ritual {
Engine::Engine(std::string config_path) :
$config(config_path)
{
$profile = $config["profile"];
auto& actions = $config["actions"];
for(auto& ac : actions) {
auto action = ai::config_action($profile, ac);
$actions.insert_or_assign(action.name, action);
}
for(auto& [name, sc] : $config["states"].items()) {
auto state = ai::config_state($profile, sc);
$states.insert_or_assign(name, state);
}
auto& scripts = $config["scripts"];
for(auto& [script_name, action_names] : scripts.items()) {
std::vector<ai::Action> the_script;
for(auto name : action_names) {
the_script.push_back($actions.at(name));
}
$scripts.insert_or_assign(script_name, the_script);
}
}
ai::State Engine::load_state(std::string name) {
return $states.at(name);
}
void Engine::load_junk(CraftingState& ritual, const JunkItem& item) {
auto& junk = $config["junk"];
auto& item_desc = junk[item];
fmt::print("Item {} provides: ", item);
for(auto& effect : item_desc["provides"]) {
fmt::print("{} ", (std::string)effect);
set_state(ritual, effect, true);
}
fmt::print("\n");
}
ai::Action Engine::load_action(std::string name) {
return $actions.at(name);
}
CraftingState Engine::start() {
auto start = load_state("initial");
auto goal = load_state("final");
return {"actions", start, goal};
}
void Engine::set_state(CraftingState& ritual, std::string name, bool setting) {
dbc::check($profile.contains(name),
fmt::format("ritual action named {} is not in profile, look in {} config",
name, $config.$src_path));
ritual.start.set($profile.at(name), setting);
}
void CraftingState::reset() {
start = original;
plan.complete = false;
plan.script.clear();
}
void Engine::plan(CraftingState& ritual) {
ritual.plan = ai::plan_actions($scripts.at(ritual.script), ritual.start, ritual.goal);
}
bool CraftingState::will_do(std::string name) {
if(plan.script.size() == 0) return false;
return plan.script[0].name == name;
}
ai::Action CraftingState::pop() {
auto result = plan.script.front();
plan.script.pop_front();
return result;
}
void CraftingState::dump() {
ai::dump_script(script, start, plan.script);
}
bool CraftingState::is_combined() {
// it's only combined if it has > 1 and last is combined
if(plan.script.size() <= 1) return false;
auto& last = plan.script.back();
return last.name == "combined";
}
Action Engine::finalize(CraftingState& ritual) {
(void)ritual;
Action result;
auto effects = $config["effects"];
for(auto action : ritual.plan.script) {
if(effects.contains(action.name)) {
auto& effect = effects[action.name];
result.damage += int(effect["damage"]);
result.probability *= float(effect["probability"]);
if(effect.contains("kind")) result.kind = Kind(int(effect["kind"]));
if(effect.contains("element")) result.element = Element(int(effect["element"]));
}
}
return result;
}
void Action::dump() {
fmt::print("ritual has damage {}, prob: {}, kind: {}, element: {}; named: ",
damage, probability, int(kind), int(element));
for(auto& name : names) {
fmt::print("{} ", name);
}
fmt::println("\n");
}
Action& Belt::get(int index) {
return equipped.at(index);
}
void Belt::equip(int index, Action& action) {
equipped.insert_or_assign(index, action);
}
bool Belt::has(int index) {
return equipped.contains(index);
}
void Belt::unequip(int index) {
equipped.erase(index);
}
int Belt::next() {
int slot = next_slot % max_slots;
next_slot++;
return slot;
}
Entity Blanket::add(JunkItem name) {
Entity id = ++entity_counter;
contents.insert_or_assign(id, name);
return id;
}
std::string& Blanket::get(Entity ent) {
return contents.at(ent);
}
bool Blanket::has(Entity ent) {
return contents.contains(ent);
}
void Blanket::remove(Entity ent) {
contents.erase(ent);
}
void Blanket::select(Entity ent) {
selected.insert_or_assign(ent, true);
}
void Blanket::deselect(Entity ent) {
selected.erase(ent);
}
bool Blanket::is_selected(Entity ent) {
return selected.contains(ent) && selected.at(ent);
}
void Blanket::reset() {
selected.clear();
}
bool Blanket::no_selections() {
return selected.size() == 0;
}
void Blanket::consume_crafting() {
for(auto [item_id, setting] : selected) {
contents.erase(item_id);
}
}
JunkPile random_junk(components::GameConfig &config, int count) {
dbc::check(count > 0, "cant' call random_junk with count < 0");
// this means the entity dropped loot, so make a lootable tombstone
ritual::JunkPile pile;
auto& junk = config.rituals["junk"];
ritual::JunkPile select_from;
for(auto& el : junk.items()) {
select_from.contents.push_back(el.key());
}
for(int i = 0; i < count; i++) {
size_t max_junk = select_from.contents.size();
auto& item = select_from.contents.at(Random::uniform(size_t(0), max_junk-1));
pile.contents.push_back(item);
}
dbc::check(pile.contents.size() > 0, "ritual::random_junk returned junk size == 0");
return pile;
}
}

102
src/rituals.hpp Normal file
View file

@ -0,0 +1,102 @@
#pragma once
#include "goap.hpp"
#include "ai.hpp"
#include "config.hpp"
#include "components.hpp"
namespace ritual {
using JunkItem = std::string;
using Entity = unsigned long;
struct JunkPile {
std::vector<JunkItem> contents;
};
enum class Element {
NONE=0, FIRE=1, LIGHTNING=2
};
enum class Kind {
NONE=0, PHYSICAL=1, MAGICK=2
};
struct CraftingState {
std::string script;
ai::State start;
ai::State original;
ai::State goal;
ai::ActionPlan plan;
CraftingState(std::string script, ai::State start, ai::State goal) :
script(script), start(start), original(start), goal(goal)
{
}
CraftingState() {};
bool will_do(std::string name);
void dump();
ai::Action pop();
bool is_combined();
void reset();
};
struct Action {
float probability = 1.0f;
int damage = 0;
Kind kind{Kind::NONE};
Element element{Element::NONE};
std::vector<std::string> names;
void dump();
};
struct Engine {
settings::Config $config;
ai::AIProfile $profile;
std::unordered_map<std::string, ai::Action> $actions;
std::unordered_map<std::string, ai::State> $states;
std::unordered_map<std::string, std::vector<ai::Action>> $scripts;
Engine(std::string config_path="assets/rituals.json");
ai::State load_state(std::string name);
ai::Action load_action(std::string name);
CraftingState start();
void set_state(CraftingState& ritual, std::string name, bool setting);
void plan(CraftingState& ritual);
Action finalize(CraftingState& ritual);
void load_junk(CraftingState& ritual, const JunkItem& item);
};
struct Belt {
int next_slot = 0;
int max_slots = 8;
std::unordered_map<int, Action> equipped;
Action& get(int index);
void equip(int index, Action& action);
bool has(int index);
void unequip(int index);
int next();
};
struct Blanket {
size_t entity_counter = 0;
std::unordered_map<Entity, JunkItem> contents;
std::unordered_map<Entity, bool> selected;
Entity add(JunkItem name);
JunkItem& get(Entity ent);
bool has(Entity ent);
void remove(Entity ent);
void select(Entity ent);
void deselect(Entity ent);
void reset();
bool is_selected(Entity ent);
bool no_selections();
void consume_crafting();
};
JunkPile random_junk(components::GameConfig& config, int count);
}

189
src/scene.cpp Normal file
View file

@ -0,0 +1,189 @@
#include "scene.hpp"
#include "animation.hpp"
#include "shaders.hpp"
#include <fmt/core.h>
#include "dbc.hpp"
const bool DEBUG=false;
namespace scene {
Element Engine::config_scene_element(nlohmann::json& config, bool duped) {
std::string sprite_name = config["sprite"];
auto st = textures::get_sprite(sprite_name, duped);
float scale_x = config["scale_x"];
float scale_y = config["scale_y"];
float x = config["x"];
float y = config["y"];
bool flipped = config["flipped"];
// BUG: put the .json file to load as a default/optional arg
auto anim = animation::load("./assets/animation.json", sprite_name);
anim.play();
anim.transform.flipped = flipped;
std::string cell = config["cell"];
std::string name = config["name"];
bool at_mid = config["at_mid"];
sf::Text text(*$ui.$font, "", 60);
return {name, st, anim, cell, {scale_x, scale_y}, {x, y}, at_mid, flipped, nullptr, text};
}
Engine::Engine(components::AnimatedScene& scene) :
$scene(scene)
{
for(auto& config : $scene.actors) {
auto element = config_scene_element(config, false);
dbc::check(!$actor_name_ids.contains(element.name),
fmt::format("actors key {} already exists", element.name));
$actors.push_back(element);
$actor_name_ids.try_emplace(element.name, $actors.size() - 1);
}
for(auto& fixture : $scene.fixtures) {
auto element = config_scene_element(fixture, true);
$fixtures.push_back(element);
}
for(auto& line : $scene.layout) {
$layout.append(line);
}
}
void Engine::init() {
$ui.position(0,0, BOSS_VIEW_WIDTH, BOSS_VIEW_HEIGHT);
$ui.set<guecs::Background>($ui.MAIN, {$ui.$parser, guecs::THEME.TRANSPARENT});
auto& background = $ui.get<guecs::Background>($ui.MAIN);
background.set_sprite($scene.background, true);
$ui.layout($layout);
for(auto& actor : $actors) {
actor.pos = position_sprite(actor.st, actor.cell,
actor.scale, actor.at_mid, actor.pos.x, actor.pos.y);
}
for(auto& fixture : $fixtures) {
fixture.pos = position_sprite(fixture.st, fixture.cell,
fixture.scale, fixture.at_mid, fixture.pos.x, fixture.pos.y);
}
}
void Engine::apply_effect(const std::string& actor, const std::string& shader) {
auto& element = actor_config(actor);
element.effect = shaders::get(shader);
}
void Engine::attach_text(const std::string& actor, const std::string& text) {
auto& element = actor_config(actor);
element.text.setPosition(element.pos);
element.text.setScale(element.scale);
element.text.setFillColor(sf::Color::Red);
element.text.setOutlineThickness(2.0f);
element.text.setOutlineColor(sf::Color::Black);
element.text.setString(text);
}
bool Engine::mouse(float x, float y, guecs::Modifiers mods) {
return $ui.mouse(x, y, mods);
}
void Engine::render(sf::RenderTexture& view) {
$ui.render(view);
for(auto& fixture : $fixtures) {
view.draw(*fixture.st.sprite, fixture.effect.get());
}
for(auto& actor : $actors) {
view.draw(*actor.st.sprite, actor.effect.get());
if(actor.anim.playing) view.draw(actor.text);
}
$camera.render(view);
if(DEBUG) $ui.debug_layout(view);
}
Element& Engine::actor_config(const std::string& actor) {
dbc::check($actor_name_ids.contains(actor), fmt::format("scene does not contain actor {}", actor));
return $actors.at($actor_name_ids.at(actor));
}
void Engine::move_actor(const std::string& actor, const std::string& cell_name) {
auto& config = actor_config(actor);
config.cell = cell_name;
config.pos = position_sprite(config.st, config.cell, config.scale, config.at_mid);
}
void Engine::animate_actor(const std::string& actor, const std::string& form) {
auto& config = actor_config(actor);
config.anim.set_form(form);
if(!config.anim.playing) {
config.anim.play();
}
}
inline void this_is_stupid_refactor(std::vector<Element>& elements) {
for(auto& element : elements) {
if(element.anim.playing) {
element.anim.update();
element.anim.motion(*element.st.sprite, element.pos, element.scale);
element.anim.apply(*element.st.sprite);
if(element.effect != nullptr) element.anim.apply_effect(element.effect);
}
}
}
void Engine::update() {
this_is_stupid_refactor($fixtures);
this_is_stupid_refactor($actors);
}
sf::Vector2f Engine::position_sprite(textures::SpriteTexture& st, const std::string& cell_name, sf::Vector2f scale, bool at_mid, float x_diff, float y_diff) {
auto& cell = $ui.cell_for(cell_name);
float x = float(at_mid ? cell.mid_x : cell.x);
float y = float(at_mid ? cell.mid_y : cell.y);
sf::Vector2f pos{x + x_diff, y + y_diff};
st.sprite->setPosition(pos);
st.sprite->setScale(scale);
return pos;
}
void Engine::zoom(float mid_x, float mid_y, const std::string& style, float scale) {
$camera.style(style);
$camera.scale(scale);
$camera.move(mid_x, mid_y);
$camera.play();
}
void Engine::zoom(const std::string &actor, const std::string& style, float scale) {
auto& config = actor_config(actor);
auto bounds = config.st.sprite->getGlobalBounds();
float mid_x = config.pos.x + bounds.size.x / 2.0f;
float mid_y = config.pos.y + bounds.size.y / 2.0f;
zoom(mid_x, mid_y, style, scale);
}
void Engine::set_end_cb(std::function<void()> cb) {
for(auto& actor : $actors) {
actor.anim.onLoop = [&,cb](auto& seq, auto& tr) -> bool {
seq.current = tr.toggled ? seq.frame_count - 1 : 0;
cb();
actor.effect = nullptr;
return tr.looped;
};
}
}
void Engine::reset(sf::RenderTexture& view) {
$camera.reset(view);
}
}

60
src/scene.hpp Normal file
View file

@ -0,0 +1,60 @@
#pragma once
#include <memory>
#include <unordered_map>
#include "textures.hpp"
#include <SFML/Graphics/RenderWindow.hpp>
#include <guecs/ui.hpp>
#include "camera.hpp"
#include <functional>
#include "animation.hpp"
#include "components.hpp"
namespace scene {
using std::shared_ptr;
using namespace textures;
struct Element {
std::string name;
textures::SpriteTexture st;
animation::Animation anim;
std::string cell;
sf::Vector2f scale{1.0f, 1.0f};
sf::Vector2f pos{0.0f, 0.0f};
bool at_mid=false;
bool flipped=false;
std::shared_ptr<sf::Shader> effect = nullptr;
sf::Text text;
};
struct Engine {
sf::Clock $clock;
guecs::UI $ui;
components::AnimatedScene& $scene;
std::string $layout;
std::unordered_map<std::string, int> $actor_name_ids;
std::vector<Element> $fixtures;
std::vector<Element> $actors;
cinematic::Camera $camera{{BOSS_VIEW_WIDTH, BOSS_VIEW_HEIGHT}, "scene"};
Engine(components::AnimatedScene& scene);
void init();
void render(sf::RenderTexture& view);
void update();
bool mouse(float x, float y, guecs::Modifiers mods);
void attach_text(const std::string& actor, const std::string& text);
Element config_scene_element(nlohmann::json& config, bool duped);
sf::Vector2f position_sprite(textures::SpriteTexture& st, const std::string& cell_name, sf::Vector2f scale, bool at_mid, float x_diff=0.0f, float y_diff=0.0f);
void move_actor(const std::string& actor, const std::string& cell_name);
void animate_actor(const std::string& actor, const std::string& form);
void apply_effect(const std::string& actor, const std::string& shader);
Element& actor_config(const std::string& actor);
void zoom(const std::string& actor, const std::string& style, float scale=0.9f);
void zoom(float mid_x, float mid_y, const std::string& style, float scale);
void reset(sf::RenderTexture& view);
void set_end_cb(std::function<void()> cb);
};
}

78
src/shaders.cpp Normal file
View file

@ -0,0 +1,78 @@
#include "shaders.hpp"
#include <SFML/Graphics/Image.hpp>
#include "dbc.hpp"
#include <fmt/core.h>
#include "config.hpp"
#include "constants.hpp"
#include <memory>
namespace shaders {
using std::shared_ptr, std::make_shared;
static ShaderManager SMGR;
static bool INITIALIZED = false;
static int VERSION = 0;
inline void configure_shader_defaults(std::shared_ptr<sf::Shader> ptr) {
ptr->setUniform("source", sf::Shader::CurrentTexture);
}
bool load_shader(std::string name, nlohmann::json& settings) {
std::string file_name = settings["file_name"];
auto ptr = std::make_shared<sf::Shader>();
bool good = ptr->loadFromFile(file_name, sf::Shader::Type::Fragment);
if(good) {
configure_shader_defaults(ptr);
SMGR.shaders.try_emplace(name, name, file_name, ptr);
}
return good;
}
void init() {
if(!INITIALIZED) {
dbc::check(sf::Shader::isAvailable(), "no shaders?!");
INITIALIZED = true;
auto config = settings::get("shaders");
bool good = load_shader("ERROR", config["ERROR"]);
dbc::check(good, "Failed to load ERROR shader. Look in assets/shaders.json");
for(auto& [name, settings] : config.json().items()) {
if(name == "ERROR") continue;
dbc::check(!SMGR.shaders.contains(name),
fmt::format("shader name '{}' duplicated in assets/shaders.json", name));
good = load_shader(name, settings);
if(!good) {
dbc::log(fmt::format("failed to load shader {}", name));
SMGR.shaders.insert_or_assign(name, SMGR.shaders.at("ERROR"));
}
}
}
}
std::shared_ptr<sf::Shader> get(const std::string& name) {
dbc::check(INITIALIZED, "you forgot to shaders::init()");
dbc::check(SMGR.shaders.contains(name),
fmt::format("shader name '{}' not in assets/shaders.json", name));
auto& rec = SMGR.shaders.at(name);
return rec.ptr;
}
int reload() {
VERSION++;
INITIALIZED = false;
SMGR.shaders.clear();
init();
return VERSION;
}
bool updated(int my_version) {
return my_version != VERSION;
}
int version() {
return VERSION;
}
};

28
src/shaders.hpp Normal file
View file

@ -0,0 +1,28 @@
#pragma once
#include <cstdint>
#include <vector>
#include <string>
#include <SFML/Graphics.hpp>
#include <unordered_map>
#include <memory>
#include "matrix.hpp"
#include <nlohmann/json.hpp>
namespace shaders {
struct Record {
std::string name;
std::string file_name;
std::shared_ptr<sf::Shader> ptr = nullptr;
};
struct ShaderManager {
std::unordered_map<std::string, Record> shaders;
};
std::shared_ptr<sf::Shader> get(const std::string& name);
void init();
bool load_shader(std::string& name, nlohmann::json& settings);
bool updated(int my_version);
int reload();
int version();
}

634
src/shiterator.hpp Normal file
View file

@ -0,0 +1,634 @@
#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);
}
};
/*
* Same as rando_rect_t but it uses a centered box.
*/
template<typename MAT>
struct rando_box_t {
size_t x;
size_t y;
size_t x_offset;
size_t y_offset;
box_t<MAT> it;
rando_box_t(MAT &mat, size_t start_x, size_t start_y, size_t size) :
it{mat, start_x, start_y, size}
{
x_offset = Random::uniform(size_t(0), it.right);
y_offset = Random::uniform(size_t(0), it.bottom);
}
bool next() {
bool done = it.next();
x = it.left + ((it.x + x_offset) % it.right);
y = it.top + ((it.y + y_offset) % it.bottom);
return done;
}
};
/*
* 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);
}
};
}

32
src/simplefsm.hpp Normal file
View file

@ -0,0 +1,32 @@
#pragma once
#include <fmt/core.h>
#ifndef FSM_DEBUG
#define FSM_STATE(C, S, E, ...) case C::S: S(E, ##__VA_ARGS__); break
#else
static int last_event=-1;
#define FSM_STATE(C, S, E, ...) case C::S: if(last_event != int(E)) { last_event = int(E); fmt::println(">> " #C " " #S " event={}, state={}", int(E), int($state));}; S(E, ##__VA_ARGS__); break
#endif
template<typename S, typename E>
class DeadSimpleFSM {
protected:
// BUG: don't put this in your class because state() won't work
S $state = S::START;
public:
template<typename... Types>
void event(E event, Types... args);
void state(S next_state) {
#ifdef FSM_DEBUG
fmt::println("<< STATE: {} -> {}", int($state), int(next_state));
#endif
$state = next_state;
}
bool in_state(S state) {
return $state == state;
}
};

82
src/sound.cpp Normal file
View file

@ -0,0 +1,82 @@
#include "sound.hpp"
#include "dbc.hpp"
#include <fmt/core.h>
#include "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(fmt::format("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), fmt::format("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/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);
}

138
src/spatialmap.cpp Normal file
View file

@ -0,0 +1,138 @@
#include "spatialmap.hpp"
#include <fmt/core.h>
using namespace fmt;
using DinkyECS::Entity;
void SpatialMap::insert(Point pos, Entity ent, bool has_collision) {
if(has_collision) {
dbc::check(!occupied(pos), "attempt to insert an entity with collision in space with collision");
}
$collision.emplace(pos, CollisionData{ent, has_collision});
}
CollisionData SpatialMap::remove(Point pos, Entity ent) {
auto [begin, end] = $collision.equal_range(pos);
for(auto it = begin; it != end; ++it) {
if(it->second.entity == ent) {
// does the it->second go invalid after erase?
auto copy = it->second;
$collision.erase(it);
return copy;
}
}
dbc::sentinel("failed to find entity to remove");
}
void SpatialMap::move(Point from, Point to, Entity ent) {
auto data = remove(from, ent);
insert(to, ent, data.collision);
}
Entity SpatialMap::occupied_by(Point at) const {
auto [begin, end] = $collision.equal_range(at);
for(auto it = begin; it != end; ++it) {
if(it->second.collision) {
return it->second.entity;
}
}
return DinkyECS::NONE;
}
bool SpatialMap::occupied(Point at) const {
return occupied_by(at) != DinkyECS::NONE;
}
bool SpatialMap::something_there(Point at) const {
return $collision.count(at) > 0;
}
Entity SpatialMap::get(Point at) const {
dbc::check($collision.contains(at), "attempt to get entity when none there");
auto [begin, end] = $collision.equal_range(at);
return begin->second.entity;
}
void SpatialMap::find_neighbor(EntityList &result, Point at, int dy, int dx) const {
// 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 entity = find(cell, [&](auto data) {
return data.collision;
});
if(entity != DinkyECS::NONE) result.push_back(entity);
}
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(result, cell, 0, 1); // north
find_neighbor(result, cell, 0, -1); // south
find_neighbor(result, cell, 1, 0); // east
find_neighbor(result, cell, -1, 0); // west
if(diag) {
find_neighbor(result, cell, 1, -1); // south east
find_neighbor(result, cell, -1, -1); // south west
find_neighbor(result, cell, 1, 1); // north east
find_neighbor(result, cell, -1, 1); // north west
}
return {!result.empty(), result};
}
inline void update_sorted(SortedEntities& sprite_distance, PointEntityMap& table, Point from, int max_dist) {
Point seen{0,0};
float wiggle = 0.0f;
for(const auto &rec : table) {
Point sprite = rec.first;
int inside = (from.x - sprite.x) * (from.x - sprite.x) +
(from.y - sprite.y) * (from.y - sprite.y);
if(from == sprite || rec.second.collision) {
wiggle = 0.0f;
} else if(sprite == seen) {
wiggle += 0.02f;
} else {
wiggle = 0.0f;
seen = sprite;
}
if(inside < max_dist) {
sprite_distance.push_back({inside, rec.second.entity, wiggle});
}
}
}
Entity SpatialMap::find(Point at, std::function<bool(CollisionData)> cb) const {
auto [begin, end] = $collision.equal_range(at);
for(auto it = begin; it != end; ++it) {
if(cb(it->second)) return it->second.entity;
}
return DinkyECS::NONE;
}
void SpatialMap::distance_sorted(SortedEntities& sprite_distance, Point from, int max_dist) {
sprite_distance.clear();
update_sorted(sprite_distance, $collision, from, max_dist);
std::sort(sprite_distance.begin(), sprite_distance.end(), [](auto &a, auto &b) {
return a.dist_square > b.dist_square;
});
}

49
src/spatialmap.hpp Normal file
View file

@ -0,0 +1,49 @@
#pragma once
#include <vector>
#include <unordered_map>
#include "map.hpp"
#include "dinkyecs.hpp"
#include "point.hpp"
struct CollisionData {
DinkyECS::Entity entity = DinkyECS::NONE;
bool collision = false;
};
struct EntityDistance {
int dist_square=0;
DinkyECS::Entity entity=DinkyECS::NONE;
float wiggle=0.0f;
};
// Point's has is in point.hpp
using EntityList = std::vector<DinkyECS::Entity>;
using PointEntityMap = std::unordered_multimap<Point, CollisionData>;
using SortedEntities = std::vector<EntityDistance>;
struct FoundEntities {
bool found;
EntityList nearby;
};
class SpatialMap {
public:
SpatialMap() {}
PointEntityMap $collision;
void insert(Point pos, DinkyECS::Entity obj, bool has_collision);
void move(Point from, Point to, DinkyECS::Entity ent);
// return value is whether the removed thing has collision
CollisionData remove(Point pos, DinkyECS::Entity entity);
DinkyECS::Entity occupied_by(Point pos) const;
bool occupied(Point pos) const;
bool something_there(Point at) const;
DinkyECS::Entity get(Point at) const;
DinkyECS::Entity find(Point at, std::function<bool(CollisionData)> cb) const;
void find_neighbor(EntityList &result, Point at, int dy, int dx) const;
FoundEntities neighbors(Point position, bool diag=false) const;
void distance_sorted(SortedEntities& sorted_sprites, Point from, int max_distance);
size_t size() { return $collision.size(); }
};

11
src/stats.cpp Normal file
View file

@ -0,0 +1,11 @@
#include "stats.hpp"
#include <fmt/core.h>
#include "dbc.hpp"
void Stats::dump(std::string msg)
{
dbc::log(fmt::format("{}: sum: {}, sumsq: {}, n: {}, "
"min: {}, max: {}, mean: {}, stddev: {}",
msg, sum, sumsq, n, min, max, mean(),
stddev()));
}

59
src/stats.hpp Normal file
View file

@ -0,0 +1,59 @@
#pragma once
#include <cmath>
#include <chrono>
struct Stats {
using TimeBullshit = std::chrono::time_point<std::chrono::high_resolution_clock>;
double sum = 0.0;
double sumsq = 0.0;
double n = 0.0;
double min = 0.0;
double max = 0.0;
inline void reset() {
sum = 0.0;
sumsq = 0.0;
n = 0.0;
min = 0.0;
max = 0.0;
}
inline double mean() {
return sum / n;
}
inline double stddev() {
return std::sqrt((sumsq - (sum * sum / n)) / (n - 1));
}
inline void sample(double s) {
sum += s;
sumsq += s * s;
if (n == 0) {
min = s;
max = s;
} else {
if (min > s) min = s;
if (max < s) max = s;
}
n += 1;
}
inline TimeBullshit time_start() {
return std::chrono::high_resolution_clock::now();
}
inline void sample_time(TimeBullshit start) {
auto end = std::chrono::high_resolution_clock::now();
auto elapsed = std::chrono::duration<double>(end - start);
if(elapsed.count() > 0.0) {
sample(1.0/elapsed.count());
}
}
void dump(std::string msg="");
};

109
src/storyboard/ui.cpp Normal file
View file

@ -0,0 +1,109 @@
#include "storyboard/ui.hpp"
#include "components.hpp"
#include "sound.hpp"
#include "config.hpp"
#include <chrono>
#include <iostream>
#include <locale>
#include <sstream>
namespace storyboard {
UI::UI(const std::string& story_name) :
$view_texture({SCREEN_WIDTH, SCREEN_HEIGHT}),
$view_sprite($view_texture.getTexture())
{
$view_sprite.setPosition({0, 0});
auto config = settings::get("stories");
$story = components::convert<components::Storyboard>(config[story_name]);
$audio = sound::get_sound_pair($story.audio).sound;
$camera.from_story($story);
}
void UI::init() {
$ui.position(0,0, SCREEN_WIDTH, SCREEN_HEIGHT);
$ui.set<guecs::Background>($ui.MAIN, {$ui.$parser, guecs::THEME.TRANSPARENT});
auto& background = $ui.get<guecs::Background>($ui.MAIN);
background.set_sprite($story.image, true);
for(auto& line : $story.layout) {
$layout.append(line);
}
$ui.layout($layout);
$audio->play();
}
void UI::render(sf::RenderWindow &window) {
track_audio();
$view_texture.clear();
$camera.render($view_texture);
$ui.render($view_texture);
// $ui.debug_layout($view_texture);
$view_texture.display();
window.draw($view_sprite);
}
bool UI::playing() {
return $audio->getStatus() == sf::SoundSource::Status::Playing;
}
sf::Time parse_time_code(const std::string& time) {
std::chrono::seconds out{};
std::istringstream is{time};
is >> std::chrono::parse("%M:%S", out);
dbc::check(!is.fail(), fmt::format("Time parse failed: {}", time));
return sf::Time(out);
}
void UI::update() {
$camera.update();
}
void UI::track_audio() {
auto& [timecode, cell_name, form, _] = $story.beats[cur_beat % $story.beats.size()];
auto track_head = $audio->getPlayingOffset();
auto next_beat = parse_time_code(timecode);
if(track_head >= next_beat) {
if($moving) return;
$moving = true; // prevent motion until next tick
// get the original zoom target as from
auto& from_cell = $ui.cell_for($zoom_target);
$camera.position(from_cell.mid_x, from_cell.mid_y);
$zoom_target = cell_name;
$camera.style(timecode);
// get the new target from the cell names
zoom($zoom_target);
$camera.play();
cur_beat++;
} else {
$moving = false;
}
}
bool UI::mouse(float, float, guecs::Modifiers) {
$audio->stop();
return true;
}
void UI::zoom(const std::string &cell_name) {
auto& cell = $ui.cell_for(cell_name);
$camera.resize(float(cell.w));
$camera.move(float(cell.mid_x), float(cell.mid_y));
}
void UI::reset() {
$camera.reset($view_texture);
}
}

35
src/storyboard/ui.hpp Normal file
View file

@ -0,0 +1,35 @@
#pragma once
#include "constants.hpp"
#include <guecs/ui.hpp>
#include "camera.hpp"
#include <SFML/Audio/Sound.hpp>
#include "components.hpp"
namespace storyboard {
struct UI {
guecs::UI $ui;
sf::RenderTexture $view_texture;
sf::Sprite $view_sprite;
cinematic::Camera $camera{{SCREEN_WIDTH, SCREEN_HEIGHT}, "story"};
std::shared_ptr<sf::Sound> $audio;
std::string $zoom_target = "a";
bool $moving = false;
int cur_beat = 0;
components::Storyboard $story;
std::string $layout;
UI(const std::string& story_name);
void init();
void update();
void render(sf::RenderWindow &window);
bool mouse(float x, float y, guecs::Modifiers mods);
void zoom(const std::string &cell_name);
void reset();
void track_audio();
bool playing();
void config_camera(cinematic::Camera &camera);
};
}

Some files were not shown because too many files have changed in this diff Show more