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

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

78
src/gui/backend.cpp Normal file
View file

@ -0,0 +1,78 @@
#include "backend.hpp"
#include "graphics/shaders.hpp"
#include "game/sound.hpp"
#include "graphics/textures.hpp"
#include "game/config.hpp"
#include "graphics/palette.hpp"
namespace gui {
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/gui/backend.hpp Normal file
View file

@ -0,0 +1,20 @@
#include "guecs/ui.hpp"
namespace gui {
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();
};
}

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

@ -0,0 +1,102 @@
#include "gui/debug_ui.hpp"
#include "constants.hpp"
#include "events.hpp"
#include <optional>
#include <fmt/core.h>
#include <fmt/xchar.h>
#include "game/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("RAT_GIANT", "rat_with_sword", "spawn4");
$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 = $F(L"STATS\n"
L"HP: {}\n"
L"mean:{:>8.5}\n"
L"sdev: {:>8.5}\n"
L"min: {:>8.5}\n"
L"max: {:>8.5}\n"
L"count:{:<10}\n"
L"level: {} size: {}x{}\n\n"
L"VSync? {}\n"
L"FR Limit: {}\n"
L"Debug? {}\n\n",
player_combat.hp, $stats.mean(), $stats.stddev(), $stats.min,
$stats.max, $stats.n, level.index, map->width(), map->height(),
VSYNC, FRAME_LIMIT, DEBUG_BUILD);
$gui.show_text("debug_text", stats);
$gui.render(window);
// $gui.debug_layout(window);
}
}
void DebugUI::debug() {
active = !active;
if(active) {
auto& level = GameDB::current_level();
// it's on now, enable things
auto player = level.world->get_the<components::Player>();
auto& player_combat = level.world->get<components::Combat>(player.entity);
player_combat.hp = player_combat.max_hp;
$gui.show_text("debug_text", L"STATS");
} else {
// it's off now, close it
$gui.close<Text>("debug_text");
}
}
bool DebugUI::mouse(float x, float y, guecs::Modifiers mods) {
return $gui.mouse(x, y, mods);
}
Stats::TimeBullshit DebugUI::time_start() {
return $stats.time_start();
}
void DebugUI::sample_time(Stats::TimeBullshit start) {
$stats.sample_time(start);
}
void DebugUI::reset_stats() {
$stats.reset();
}
}

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

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

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

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

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

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

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

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

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

@ -0,0 +1,49 @@
#pragma once
#include "events.hpp"
#include "events.hpp"
#include "algos/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;
}
};
}
}

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

@ -0,0 +1,515 @@
#include "gui/fsm.hpp"
#include <iostream>
#include <chrono>
#include <numeric>
#include <functional>
#include "game/components.hpp"
#include <numbers>
#include "game/systems.hpp"
#include "events.hpp"
#include "game/sound.hpp"
#include "graphics/shaders.hpp"
#include <fmt/xchar.h>
#include "gui/guecstra.hpp"
#include "game/level.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),
$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, 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);
$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($F("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, DEFAULT_ROTATE);
state(State::ROTATING);
break;
case ROTATE_RIGHT:
$main_ui.plan_rotate(1, DEFAULT_ROTATE);
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 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::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($F("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);
$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::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);
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::update() {
}
void FSM::render() {
$window.clear();
// this clears any attack animations, like fire
System::clear_attack();
// BUG: this is the render for this class, and where I add an update
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_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($F(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($F(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::ATTACK:
event(Event::ATTACK, data);
break;
case eGUI::STAIRS_DOWN:
dbc::sentinel("make me!");
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($F(L"NOOP EVENT! {},{}", evt, entity));
}
} break;
default:
dbc::log($F("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() {
GameDB::create_level();
$status_ui.update_level();
$main_ui.update_level();
$loot_ui.update_level();
run_systems();
}
}

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

@ -0,0 +1,73 @@
#pragma once
#include "constants.hpp"
#include "algos/simplefsm.hpp"
#include "gui/debug_ui.hpp"
#include "gui/main_ui.hpp"
#include "gui/status_ui.hpp"
#include "gui/loot_ui.hpp"
#include "gui/map_view.hpp"
#include "events.hpp"
#include "gui/event_router.hpp"
#include "gui/dnd_loot.hpp"
#include "events.hpp"
namespace gui {
enum class State {
START=__LINE__,
MOVING=__LINE__,
IN_COMBAT=__LINE__,
COMBAT_ROTATE=__LINE__,
ATTACKING=__LINE__,
ROTATING=__LINE__,
LOOTING=__LINE__,
IDLE=__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;
StatusUI $status_ui{STATUS_UI_X, STATUS_UI_Y, STATUS_UI_WIDTH, STATUS_UI_HEIGHT};
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 LOOTING(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 update();
void render();
bool active();
void run_systems();
void handle_world_events();
void next_level();
void debug_render();
void take_screenshot();
};
}

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

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

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

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

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

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

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

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

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

@ -0,0 +1,155 @@
#include "gui/main_ui.hpp"
#include "game/components.hpp"
#include <fmt/xchar.h>
#include "graphics/animation.hpp"
#include "constants.hpp"
#include "game/level.hpp"
#include "ai/ai.hpp"
namespace gui {
using namespace components;
MainUI::MainUI(sf::RenderWindow& window) :
$window(window),
$rayview(std::make_shared<Raycaster>(RAY_VIEW_X, RAY_VIEW_Y, 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->position_camera(player.x + 0.5, player.y + 0.5);
$overlay_ui.init();
}
void MainUI::render() {
// BUG: bring back the $needs_render optimization
$rayview->update();
$rayview->render($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);
}
}
}

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

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

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

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

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

@ -0,0 +1,27 @@
#pragma once
#include "graphics/textures.hpp"
#include "algos/matrix.hpp"
#include <guecs/ui.hpp>
#include <string>
#include "algos/dinkyecs.hpp"
#include "game/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();
};
}

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

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

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

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

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

@ -0,0 +1,132 @@
#include "gui/status_ui.hpp"
#include "game/components.hpp"
#include <guecs/ui.hpp>
#include "algos/rand.hpp"
#include <fmt/xchar.h>
#include "gui/guecstra.hpp"
#include "game/systems.hpp"
#include "game/inventory.hpp"
#include "game/level.hpp"
namespace gui {
using namespace guecs;
using std::any, std::any_cast, std::string, std::make_any;
StatusUI::StatusUI(size_t x, size_t y, size_t width, size_t height)
{
$gui.position(x, y, width, height);
$gui.layout(
"[=slot1]"
"[=slot2]"
"[=hand_r]"
"[=pocket_l]");
}
void StatusUI::init() {
$gui.set<Background>($gui.MAIN, {$gui.$parser, });
for(auto& [name, cell] : $gui.cells()) {
auto gui_id = $gui.entity(name);
$gui.set<Rectangle>(gui_id, {});
$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);
}
});
}
$gui.init();
update();
}
bool StatusUI::mouse(float x, float y, guecs::Modifiers mods) {
return $gui.mouse(x, y, mods);
}
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);
}
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));
}
}

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

@ -0,0 +1,32 @@
#pragma once
#include "constants.hpp"
#include <deque>
#include "graphics/textures.hpp"
#include <guecs/ui.hpp>
#include "gui/guecstra.hpp"
namespace gui {
class StatusUI {
public:
guecs::UI $gui;
explicit StatusUI(size_t x, size_t y, size_t width, size_t height);
StatusUI(const StatusUI& other) = delete;
StatusUI(StatusUI&& other) = delete;
~StatusUI() = default;
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);
};
}