under_the_ashland_dome/src/gui/fsm.cpp

555 lines
14 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() :
$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, 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, 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::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();
state(State::IDLE);
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::IDLE);
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::IDLE);
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:
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);
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(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($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::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::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();
}
if($cur_scene != nullptr) {
$cur_scene->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();
}
}
void FSM::update() {
if($cur_scene != nullptr) {
$cur_scene->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);
}
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:
show_scene("NEXT_LEVEL");
state(State::NEXT_LEVEL_SCENE);
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 {
show_scene("DEATH");
state(State::DEATH_SCENE);
}
} 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;
}
}