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:
parent
4778677647
commit
1d4ae911b9
108 changed files with 94 additions and 83 deletions
212
src/ai.cpp
Normal file
212
src/ai.cpp
Normal 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
65
src/ai.hpp
Normal 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
74
src/ai_debug.cpp
Normal 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
10
src/ai_debug.hpp
Normal 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
314
src/animation.cpp
Normal 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
150
src/animation.hpp
Normal 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
461
src/autowalker.cpp
Normal 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
54
src/autowalker.hpp
Normal 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
78
src/backend.cpp
Normal 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
20
src/backend.hpp
Normal 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
133
src/battle.cpp
Normal 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
50
src/battle.hpp
Normal 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
273
src/boss/fight.cpp
Normal 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
61
src/boss/fight.hpp
Normal 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
140
src/boss/system.cpp
Normal 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
20
src/boss/system.hpp
Normal 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
130
src/boss/ui.cpp
Normal 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
44
src/boss/ui.hpp
Normal 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
132
src/camera.cpp
Normal 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
37
src/camera.hpp
Normal 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
16
src/combat.cpp
Normal 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
36
src/components.cpp
Normal 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
196
src/components.hpp
Normal 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
67
src/config.cpp
Normal 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
29
src/config.hpp
Normal 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
83
src/constants.hpp
Normal 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
47
src/dbc.cpp
Normal 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
50
src/dbc.hpp
Normal 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
248
src/dinkyecs.hpp
Normal 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
141
src/easing.cpp
Normal 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
32
src/easing.hpp
Normal 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
49
src/events.hpp
Normal 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
139
src/game_level.cpp
Normal 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 ¤t_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
35
src/game_level.hpp
Normal 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 ¤t_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
188
src/goap.cpp
Normal 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
90
src/goap.hpp
Normal 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
102
src/gui/combat_ui.cpp
Normal 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
23
src/gui/combat_ui.hpp
Normal 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
106
src/gui/debug_ui.cpp
Normal 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
26
src/gui/debug_ui.hpp
Normal 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
328
src/gui/dnd_loot.cpp
Normal 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
66
src/gui/dnd_loot.hpp
Normal 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
143
src/gui/event_router.cpp
Normal 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
49
src/gui/event_router.hpp
Normal 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
577
src/gui/fsm.cpp
Normal 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
83
src/gui/fsm.hpp
Normal 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
40
src/gui/guecstra.cpp
Normal 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
24
src/gui/guecstra.hpp
Normal 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
154
src/gui/loot_ui.cpp
Normal 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
37
src/gui/loot_ui.hpp
Normal 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
155
src/gui/main_ui.cpp
Normal 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
52
src/gui/main_ui.hpp
Normal 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
75
src/gui/map_view.cpp
Normal 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
27
src/gui/map_view.hpp
Normal 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
33
src/gui/mini_map.cpp
Normal 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
17
src/gui/mini_map.hpp
Normal 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
63
src/gui/overlay_ui.cpp
Normal 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
25
src/gui/overlay_ui.hpp
Normal 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
264
src/gui/ritual_ui.cpp
Normal 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
69
src/gui/ritual_ui.hpp
Normal 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
156
src/gui/status_ui.cpp
Normal 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
35
src/gui/status_ui.hpp
Normal 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
99
src/inventory.cpp
Normal 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
25
src/inventory.hpp
Normal 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
45
src/json_mods.hpp
Normal 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
87
src/lights.cpp
Normal 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
40
src/lights.hpp
Normal 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
67
src/main.cpp
Normal 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
150
src/map.cpp
Normal 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
72
src/map.hpp
Normal 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
36
src/matrix.cpp
Normal 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
43
src/matrix.hpp
Normal 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
221
src/maze.cpp
Normal 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
29
src/maze.hpp
Normal 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
51
src/meson.build
Normal 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
72
src/palette.cpp
Normal 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
13
src/palette.hpp
Normal 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
134
src/pathing.cpp
Normal 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
40
src/pathing.hpp
Normal 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
20
src/point.hpp
Normal 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
13
src/rand.cpp
Normal 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
31
src/rand.hpp
Normal 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
526
src/raycaster.cpp
Normal 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
92
src/raycaster.hpp
Normal 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
220
src/rituals.cpp
Normal 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
102
src/rituals.hpp
Normal 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
189
src/scene.cpp
Normal 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
60
src/scene.hpp
Normal 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
78
src/shaders.cpp
Normal 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
28
src/shaders.hpp
Normal 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
634
src/shiterator.hpp
Normal 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
32
src/simplefsm.hpp
Normal 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
82
src/sound.cpp
Normal 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
26
src/sound.hpp
Normal 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
138
src/spatialmap.cpp
Normal 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
49
src/spatialmap.hpp
Normal 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
11
src/stats.cpp
Normal 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
59
src/stats.hpp
Normal 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
109
src/storyboard/ui.cpp
Normal 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
35
src/storyboard/ui.hpp
Normal 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
Loading…
Add table
Add a link
Reference in a new issue