615 lines
15 KiB
C++
615 lines
15 KiB
C++
#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(sf::RenderWindow& window) :
|
|
$window(window),
|
|
$main_ui($window),
|
|
$dnd_loot($status_ui, $loot_ui, $window, $router)
|
|
{
|
|
System::init($systems);
|
|
}
|
|
|
|
void FSM::event(Event ev, std::any data) {
|
|
switch($state) {
|
|
FSM_STATE(State, START, ev);
|
|
FSM_STATE(State, INTRO, ev);
|
|
FSM_STATE(State, START_SCENE, ev);
|
|
FSM_STATE(State, DEATH_SCENE, ev);
|
|
FSM_STATE(State, WIN_SCENE, ev);
|
|
FSM_STATE(State, NEXT_LEVEL_SCENE, 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, LOOTING, ev, data);
|
|
FSM_STATE(State, END_QUIT, ev);
|
|
FSM_STATE(State, END_PLAY_AGAIN, ev);
|
|
FSM_STATE(State, END_PLAYER_DIED, ev);
|
|
}
|
|
}
|
|
|
|
void FSM::START(Event ) {
|
|
$main_ui.update_level();
|
|
$main_ui.init();
|
|
$loot_ui.init();
|
|
|
|
for(auto& [name, scene] : $scenes) {
|
|
scene->init();
|
|
}
|
|
|
|
// BUG: maybe this is a function on main_ui?
|
|
auto cell = $main_ui.overlay_cell("left");
|
|
$debug_ui.init(cell);
|
|
$status_ui.init();
|
|
run_systems();
|
|
|
|
show_scene("STARTING");
|
|
state(State::START_SCENE);
|
|
}
|
|
|
|
void FSM::INTRO(Event ev) {
|
|
dbc::check($story != nullptr, "you forgot the stroy");
|
|
|
|
if(ev == game::Event::MOUSE_CLICK) {
|
|
$story->stop();
|
|
}
|
|
|
|
if(!$story->playing()) {
|
|
$story = nullptr;
|
|
state(State::IDLE);
|
|
}
|
|
}
|
|
|
|
void FSM::START_SCENE(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 START:
|
|
close_scene();
|
|
$story = std::make_shared<storyboard::UI>("intro_story");
|
|
$story->init();
|
|
state(State::INTRO);
|
|
break;
|
|
case QUIT:
|
|
FSM::IDLE(ev, {});
|
|
break;
|
|
case TICK:
|
|
break;
|
|
default:
|
|
dbc::log($F("unhandled event: {}", int(ev)));
|
|
break;
|
|
}
|
|
}
|
|
|
|
void FSM::DEATH_SCENE(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 START:
|
|
close_scene();
|
|
state(State::END_PLAYER_DIED);
|
|
break;
|
|
case QUIT:
|
|
FSM::IDLE(ev, {});
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void FSM::WIN_SCENE(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 START:
|
|
close_scene();
|
|
state(State::END_PLAY_AGAIN);
|
|
break;
|
|
case QUIT:
|
|
FSM::IDLE(ev, {});
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void FSM::NEXT_LEVEL_SCENE(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 START:
|
|
next_level();
|
|
close_scene();
|
|
state(State::IDLE);
|
|
break;
|
|
case QUIT:
|
|
FSM::IDLE(ev, {});
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void FSM::MOVING(Event ) {
|
|
// this should be an optional that returns a point
|
|
if(auto move_to = $main_ui.play_move()) {
|
|
$systems.runMoving(*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");
|
|
$systems.runCombat(0);
|
|
run_systems();
|
|
state(State::IDLE);
|
|
} break;
|
|
case ATTACK: {
|
|
int attack_id = std::any_cast<int>(data);
|
|
$systems.runCombat(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::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_QUIT);
|
|
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 ATTACK:
|
|
$main_ui.play_hands();
|
|
$main_ui.dirty();
|
|
sound::play("Sword_Hit_1");
|
|
$status_ui.update();
|
|
state(State::ATTACKING);
|
|
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);
|
|
$systems.runUseItem(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:
|
|
$systems.runPickup();
|
|
break;
|
|
case COMBAT_START:
|
|
fmt::println("!!!!!!!!!!!!!! COMBAT START in IDLE");
|
|
break;
|
|
case COMBAT_STOP:
|
|
fmt::println("!!!!!!!!!!!!!! COMBAT STOP in IDLE");
|
|
break;
|
|
default:
|
|
break; // ignore everything else
|
|
}
|
|
}
|
|
|
|
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_QUIT(Event ev) {
|
|
dbc::log($F("END_QUIT: received event after done: {}", int(ev)));
|
|
}
|
|
|
|
void FSM::END_PLAYER_DIED(Event ev) {
|
|
dbc::log($F("END_PLAYER_DIED: received event after done: {}", int(ev)));
|
|
}
|
|
|
|
void FSM::END_PLAY_AGAIN(Event ev) {
|
|
dbc::log($F("END_PLAY_AGAIN: 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($cur_scene != nullptr) {
|
|
$cur_scene->mouse(pos.x, pos.y, mods);
|
|
return;
|
|
}
|
|
|
|
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::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::Z:
|
|
$main_ui.toggle_mind_reading();
|
|
break;
|
|
case KEY::K:
|
|
player_died();
|
|
break;
|
|
case KEY::N:
|
|
check_player_wins();
|
|
break;
|
|
case KEY::H:
|
|
case KEY::F1:
|
|
toggle_help();
|
|
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($cur_scene != nullptr) {
|
|
$cur_scene->render($window);
|
|
} else if($story != nullptr) {
|
|
$story->render($window);
|
|
} 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($debug_ui.active) {
|
|
debug_render();
|
|
}
|
|
}
|
|
}
|
|
|
|
void FSM::update() {
|
|
if($cur_scene != nullptr) {
|
|
$cur_scene->update();
|
|
} else if($story != nullptr) {
|
|
$story->update();
|
|
}
|
|
}
|
|
|
|
void FSM::render() {
|
|
// this clears any attack animations, like fire
|
|
$systems.runRender();
|
|
// BUG: this is the render for this class, and where I add an update
|
|
draw_gui();
|
|
|
|
$window.display();
|
|
}
|
|
|
|
void FSM::run_systems() {
|
|
$systems.runUpdate();
|
|
}
|
|
|
|
bool FSM::active() {
|
|
return !(in_state(State::END_QUIT)
|
|
|| in_state(State::END_PLAY_AGAIN)
|
|
|| in_state(State::END_PLAYER_DIED));
|
|
}
|
|
|
|
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);
|
|
}
|
|
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::ATTACK:
|
|
event(Event::ATTACK, data);
|
|
break;
|
|
case eGUI::STAIRS_DOWN:
|
|
check_player_wins();
|
|
break;
|
|
case eGUI::START:
|
|
event(Event::START, data);
|
|
break;
|
|
case eGUI::DEATH: {
|
|
$status_ui.update();
|
|
|
|
if(entity != player.entity) {
|
|
$main_ui.dead_entity(entity);
|
|
} else {
|
|
player_died();
|
|
}
|
|
} break;
|
|
case eGUI::NOOP: {
|
|
if(data.type() == typeid(std::string)) {
|
|
auto name = std::any_cast<std::string>(data);
|
|
}
|
|
} 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();
|
|
}
|
|
|
|
void FSM::show_scene(const std::string& name) {
|
|
dbc::check($scenes.contains(name), $F("$scenes does not contain {}", name));
|
|
$cur_scene = $scenes[name];
|
|
}
|
|
|
|
void FSM::close_scene() {
|
|
$cur_scene = nullptr;
|
|
}
|
|
|
|
void FSM::player_died() {
|
|
show_scene("DEATH");
|
|
state(State::DEATH_SCENE);
|
|
}
|
|
|
|
void FSM::check_player_wins() {
|
|
auto& level = GameDB::current_level();
|
|
auto config = settings::get("config");
|
|
size_t levels_to_win = config["game_play"]["levels_to_win"];
|
|
|
|
if(level.index <= levels_to_win) {
|
|
show_scene("NEXT_LEVEL");
|
|
state(State::NEXT_LEVEL_SCENE);
|
|
} else {
|
|
show_scene("WIN");
|
|
state(State::WIN_SCENE);
|
|
}
|
|
}
|
|
|
|
void FSM::toggle_help() {
|
|
$display_help = !$display_help;
|
|
auto config = settings::get("config");
|
|
std::string help = config["game_play"]["help_text"];
|
|
|
|
if($display_help) {
|
|
$main_ui.$overlay_ui.show_text("middle", guecs::to_wstring(help));
|
|
} else{
|
|
$main_ui.$overlay_ui.close_text("middle");
|
|
}
|
|
}
|
|
}
|