#define FSM_DEBUG 1 #include "sound.hpp" #include "ai.hpp" #include #include "shaders.hpp" #include "backend.hpp" #include "constants.hpp" #include "animation.hpp" #include "tools/animator.hpp" #include #include #include using namespace std::chrono_literals; bool YES_SYNC=true; namespace animator { void FSM::init(const std::string &sprite_name, const std::string &anim_name, const std::string &background) { $timer.start(); $sprite_name = sprite_name; $anim_name = anim_name; $background = background; // this loads the animation reload(); sf::Vector2u new_size{(unsigned int)$anim.sheet.frame_width, (unsigned int)$anim.sheet.frame_height}; $window = sf::RenderWindow(sf::VideoMode(new_size), "Animation Crafting Tool"); $window.setPosition({0,0}); $ui.init($sprite_name, $background, new_size.x, new_size.y); // need to keep these around $pos = $ui.sprite->getPosition(); $scale = $ui.sprite->getScale(); if(YES_SYNC) { $window.setVerticalSyncEnabled(VSYNC); if(FRAME_LIMIT) $window.setFramerateLimit(FRAME_LIMIT); } $ui.update_status($anim); } void FSM::event(Event ev, std::any data) { switch($state) { FSM_STATE(State, START, ev); FSM_STATE(State, ANIMATE, ev); FSM_STATE(State, END, ev); } } void FSM::START(Event ev) { state(State::ANIMATE); } void FSM::ANIMATE(Event ev) { switch(ev) { case Event::PLAY_STOP: if($anim.playing) { $anim.stop(); } else { $anim.play(); } break; case Event::PREV_FORM: change_form(-1); $ui.update_status($anim); break; case Event::NEXT_FORM: change_form(1); $ui.update_status($anim); break; case Event::RELOAD: reload(); $ui.update_status($anim); state(State::START); break; case Event::TEST_SHADER: $ui.effect = $ui.effect == nullptr ? shaders::get("flame") : nullptr; break; default: state(State::START); } } void FSM::END(Event ev) { } void FSM::change_form(int direction) { if($anim.forms.size() == 0) { $ui.show_error("NO FORMS!"); return; } $cur_form_i = std::clamp($cur_form_i + direction, 0, int($anim.forms.size()) - 1); dbc::check($cur_form_i >= 0, "CATASTROPHE! cur_form_i went below 0. How?"); // this is the dumbest shit ever auto key_view = std::views::keys($anim.forms); std::vector keys(key_view.begin(), key_view.end()); dbc::check(size_t($cur_form_i) < keys.size(), "form index outside of form keys vector"); $cur_form = keys[$cur_form_i]; fmt::println("cur_form_i {}; cur_form: {}", $cur_form_i, $cur_form); // set_form will stop the animation $anim.set_form($cur_form); $anim.play(); } void FSM::update() { if($anim.playing) { $anim.update(); $anim.apply(*$ui.sprite); if($ui.effect != nullptr) $anim.apply_effect($ui.effect); $anim.motion(*$ui.sprite, $pos, $scale); } } void FSM::check_changed() { if($timer.getElapsedTime().toDuration() > 500ms) { try { auto mod_time = std::filesystem::last_write_time("assets/animation.json"); if($last_mod_time < mod_time) { event(Event::RELOAD); } $timer.restart(); } catch(const std::filesystem::filesystem_error& err) { fmt::println("failed to open {}: {}", err.path1().string(), err.what()); $timer.restart(); } } } void FSM::reload() { animation::Animation new_anim; try { new_anim = animation::load("assets/animation.json", $anim_name); } catch(...) { $ui.show_error("Failed to load JSON"); return; } if(!new_anim.has_form($cur_form)) { $ui.show_error(fmt::format("No form {}", $cur_form)); $cur_form = "idle"; $cur_form_i = 0; } new_anim.set_form($cur_form); try { $last_mod_time = std::filesystem::last_write_time("assets/animation.json"); } catch(...) { $ui.show_error("Filesystem error"); } if($anim.form_name == new_anim.form_name) { $ui.clear_error(); } $anim = new_anim; $anim.play(); } void FSM::handle_keyboard_mouse() { while(const auto ev = $window.pollEvent()) { using enum game::Event; using KEY = sf::Keyboard::Scan; auto gui_ev = $router.process_event(ev); auto mouse_pos = $window.mapPixelToCoords($router.position); switch(gui_ev) { case KEY_PRESS: if($router.scancode == KEY::Space) { event(Event::PLAY_STOP); } else if($router.scancode == KEY::Up) { event(Event::PREV_FORM); } else if($router.scancode == KEY::Down) { event(Event::NEXT_FORM); } else if($router.scancode == KEY::M) { $mute = !$mute; sound::mute($mute); } else if($router.scancode == KEY::S) { event(Event::TEST_SHADER); } break; 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 QUIT: state(State::END); default: break; // ignored } } } void FSM::render() { $window.clear(); $ui.render($window, false); $window.display(); } bool FSM::active() { return !in_state(State::END); } void UI::init(const std::string& sprite_name, const std::string& background, int width, int height) { $ui.position(0,0, width, height); $ui.layout("[=viewer]"); if(background != "") { $ui.set($ui.MAIN, {$ui.$parser, guecs::THEME.TRANSPARENT}); auto& bg = $ui.get($ui.MAIN); bg.set_sprite(background, true); } auto viewer = $ui.entity("viewer"); // BUG: this is some jank bullshit but it works $ui.set(viewer, {sprite_name, 0, false}); $ui.init(); sprite = $ui.get(viewer).sprite; dbc::check(sprite != nullptr, "failed to initialize $ui.sprite"); $ui.remove(viewer); $overlay.position(0, 0, width/4, height/4); $overlay.layout( "[form]" "[sequence]" "[transform]" "[error]"); $overlay.init(); $initialized_this_sucks_ass = true; } void UI::render(sf::RenderWindow& window, bool debug) { $ui.render(window); window.draw(*sprite, effect.get()); $overlay.render(window); if(debug) { $ui.debug_layout(window); $overlay.debug_layout(window); } } bool UI::mouse(float x, float y, guecs::Modifiers mods) { return $ui.mouse(x, y, mods); } void UI::update_status(animation::Animation& anim) { $overlay.show_text("form", guecs::to_wstring(anim.form_name)); $overlay.show_text("sequence", guecs::to_wstring(anim.sequence_name)); $overlay.show_text("transform", guecs::to_wstring(anim.transform_name)); } void UI::show_error(const std::string& message) { if($initialized_this_sucks_ass) { $overlay.show_text("error", guecs::to_wstring(message)); } else { dbc::log(message); } } void UI::clear_error() { if($initialized_this_sucks_ass) { $overlay.show_text("error", L""); } } std::shared_ptr UI::get_sprite() { auto viewer = $ui.entity("viewer"); return $ui.get(viewer).sprite; } } int error_usage() { fmt::println("USAGE: animator -h -b -s -a "); return 1; } int main(int argc, char* argv[]) { ai::init("ai"); components::init(); sfml::Backend backend; guecs::init(&backend); shaders::init(); std::string sprite_name; std::string background; std::string anim_name; int opt = 0; while((opt = getopt(argc, argv, "hb:s:a:")) != -1) { switch(opt) { case 'b': background = optarg; break; case 's': sprite_name = optarg; break; case 'a': anim_name = optarg; break; case 'h': // fallthrough error_usage(); break; default: return error_usage(); break; } } if(sprite_name == "") { return error_usage(); } else if(anim_name == "") { anim_name = sprite_name; // default to the same } sound::mute(true); animator::FSM main; main.init(sprite_name, anim_name, background); while(main.active()) { main.update(); main.render(); main.check_changed(); main.handle_keyboard_mouse(); } return 0; }