diff --git a/Makefile b/Makefile index 562ac6d..a7354d5 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ tracy_build: meson compile -j 10 -C builddir test: build - ./builddir/runtests + ./builddir/runtests "[animation]" run: build test powershell "cp ./builddir/zedcaster.exe ." diff --git a/animation.cpp b/animation.cpp new file mode 100644 index 0000000..2e5856d --- /dev/null +++ b/animation.cpp @@ -0,0 +1,103 @@ +#include "animation.hpp" + +namespace components { + void Animation::play() { + if(!playing) { + current = 0; + subframe = 0.0f; + playing = true; + } + } + + float Animation::twitching() { + switch(easing) { + case ease::NONE: + return 0.0; + case ease::SINE: + return ease::sine(float(frames) / subframe * ease_rate); + case ease::OUT_CIRC: + return ease::out_circ(ease::sine(float(frames) / subframe * ease_rate)); + case ease::OUT_BOUNCE: + return ease::out_bounce(ease::sine(float(frames) / subframe * ease_rate)); + case ease::IN_OUT_BACK: + return ease::in_out_back(ease::sine(float(frames) / subframe * ease_rate)); + default: + dbc::sentinel( + fmt::format("Invalid easing {} given to animation", + int(easing))); + } + } + + void Animation::step(sf::Vector2f& scale_out, sf::Vector2f& pos_out, sf::IntRect& rect_out) { + if(playing && current < frames) { + float tick = twitching(); + scale_out.x = std::lerp(scale_out.x, scale_out.x + scale, tick); + scale_out.y = std::lerp(scale_out.y, scale_out.y + scale, tick); + + if(stationary) { + pos_out.y = pos_out.y - (pos_out.y * scale_out.y - pos_out.y); + } + + if(!simple) { + rect_out.position.x += current * frame_width; + } + + subframe += speed; + current = int(subframe); + } else if(!looped) { + playing = false; + current = frames - 1; + subframe = float(frames - 1); + + if(!simple) { + rect_out.position.x += current * frame_width; + } + } else { + playing = false; + current = 0; + subframe = 0.0f; + } + } +} + + +namespace animation { + using namespace components; + using namespace textures; + + static AnimationManager MGR; + static bool initialized = false; + + bool apply(Animation& anim, SpriteTexture& target) { + auto size = target.texture->getSize(); + anim.frame_width = int(size.x) / (unsigned int)anim.frames; + sf::IntRect rect{{0,0}, {anim.frame_width, int(size.y)}}; + sf::Vector2f scale{1.0, 1.0}; + sf::Vector2f pos{0, 0}; + + anim.step(scale, pos, rect); + + target.sprite->setTextureRect(rect); + target.sprite->setPosition(pos); + target.sprite->setScale(scale); + + return anim.playing; + } + + void init() { + if(!initialized) { + Config config("assets/animations.json"); + + for(auto& [name, data] : config.json().items()) { + auto anim = components::convert(data); + MGR.animations.insert_or_assign(name, anim); + } + + initialized = true; + } + } + + Animation load(std::string name) { + return MGR.animations.at(name); + } +} diff --git a/animation.hpp b/animation.hpp new file mode 100644 index 0000000..44bac0e --- /dev/null +++ b/animation.hpp @@ -0,0 +1,14 @@ +#pragma once +#include "components.hpp" +#include "textures.hpp" +#include "easings.hpp" + +namespace animation { + struct AnimationManager { + std::unordered_map animations; + }; + + bool apply(components::Animation& anim, textures::SpriteTexture& target); + void init(); + components::Animation load(std::string name); +} diff --git a/assets/animations.json b/assets/animations.json new file mode 100644 index 0000000..bfcb8b8 --- /dev/null +++ b/assets/animations.json @@ -0,0 +1,12 @@ +{ + "ritual_blanket": { + "_type": "Animation", + "easing": 0, + "ease_rate": 0.5, + "scale": 1.0, + "simple": false, + "frames": 3, + "speed": 0.2, + "stationary": true + } +} diff --git a/assets/config.json b/assets/config.json index 8ae40f8..573cc22 100644 --- a/assets/config.json +++ b/assets/config.json @@ -52,7 +52,8 @@ "devils_fingers_stage": "assets/devils_fingers_stage.png", "tunnel_with_rocks": "assets/tunnel_with_rocks.png", "tunnel_with_rocks_stage": "assets/tunnel_with_rocks_stage.png", - "ritual_crafting_area": "assets/ritual_crafting_area.png" + "ritual_crafting_area": "assets/ritual_crafting_area.png", + "the_ritual_circle": "assets/the_ritual_circle.png" }, "worldgen": { "enemy_probability": 50, diff --git a/assets/the_ritual_circle.png b/assets/the_ritual_circle.png new file mode 100644 index 0000000..8f6d447 Binary files /dev/null and b/assets/the_ritual_circle.png differ diff --git a/boss_fight_ui.cpp b/boss_fight_ui.cpp index e2b0f2e..23d89f6 100644 --- a/boss_fight_ui.cpp +++ b/boss_fight_ui.cpp @@ -32,7 +32,7 @@ namespace gui { void BossFightUI::configure_sprite() { $sprite_config = $world->get($boss_id); $animation = $world->get($boss_id); - $animation.texture_width = $sprite_config.width; + $animation.frame_width = $sprite_config.width; $boss_image = textures::get($sprite_config.name); sf::IntRect frame_rect{{0,0},{$sprite_config.width,$sprite_config.height}}; diff --git a/components.cpp b/components.cpp index 8898df0..65c84fe 100644 --- a/components.cpp +++ b/components.cpp @@ -28,62 +28,4 @@ namespace components { components::enroll(component_map); components::enroll(component_map); } - - void Animation::play() { - if(!playing) { - current = 0; - subframe = 0.0f; - playing = true; - } - } - - float Animation::twitching() { - switch(easing) { - case ease::NONE: - return 0.0; - case ease::SINE: - return ease::sine(float(frames) / subframe * ease_rate); - case ease::OUT_CIRC: - return ease::out_circ(ease::sine(float(frames) / subframe * ease_rate)); - case ease::OUT_BOUNCE: - return ease::out_bounce(ease::sine(float(frames) / subframe * ease_rate)); - case ease::IN_OUT_BACK: - return ease::in_out_back(ease::sine(float(frames) / subframe * ease_rate)); - default: - dbc::sentinel( - fmt::format("Invalid easing {} given to animation", - int(easing))); - } - } - - void Animation::step(sf::Vector2f& scale_out, sf::Vector2f& pos_out, sf::IntRect& rect_out) { - if(playing && current < frames) { - float tick = twitching(); - scale_out.x = std::lerp(scale_out.x, scale_out.x + scale, tick); - scale_out.y = std::lerp(scale_out.y, scale_out.y + scale, tick); - - if(stationary) { - pos_out.y = pos_out.y - (pos_out.y * scale_out.y - pos_out.y); - } - - if(!simple) { - rect_out.position.x += current * texture_width; - } - - subframe += speed; - current = int(subframe); - } else if(!looped) { - playing = false; - current = frames - 1; - subframe = float(frames - 1); - - if(!simple) { - rect_out.position.x += current * texture_width; - } - } else { - playing = false; - current = 0; - subframe = 0.0f; - } - } } diff --git a/components.hpp b/components.hpp index e68b3d5..23341c0 100644 --- a/components.hpp +++ b/components.hpp @@ -123,7 +123,7 @@ namespace components { float subframe = 0.0f; bool looped = false; // BUG: this is weirdly not used in most animations but also named wrong should be frame_width - int texture_width = TEXTURE_WIDTH; + int frame_width = TEXTURE_WIDTH; void play(); float twitching(); diff --git a/main.cpp b/main.cpp index bbb2867..f1f434f 100644 --- a/main.cpp +++ b/main.cpp @@ -3,6 +3,7 @@ #include "sound.hpp" #include "autowalker.hpp" #include "ai.hpp" +#include "animation.hpp" #include int main(int argc, char* argv[]) { @@ -10,6 +11,9 @@ int main(int argc, char* argv[]) { textures::init(); sound::init(); ai::init("assets/ai.json"); + animation::init(); + + sound::mute(true); gui::FSM main; main.event(gui::Event::STARTED); diff --git a/meson.build b/meson.build index 54c00da..86af5aa 100644 --- a/meson.build +++ b/meson.build @@ -79,6 +79,8 @@ dependencies += [ sources = [ 'ai.cpp', 'ai_debug.cpp', + 'animation.cpp', + 'animation.cpp', 'autowalker.cpp', 'boss_fight_ui.cpp', 'camera.cpp', @@ -138,6 +140,7 @@ executable('runtests', sources + [ 'tests/rituals.cpp', 'tests/sound.cpp', 'tests/spatialmap.cpp', + 'tests/animation.cpp', 'tests/stats.cpp', 'tests/textures.cpp', 'tests/tilemap.cpp', diff --git a/ritual_ui.cpp b/ritual_ui.cpp index 565ad33..e8d0e20 100644 --- a/ritual_ui.cpp +++ b/ritual_ui.cpp @@ -2,6 +2,7 @@ #include "components.hpp" #include "guecs.hpp" #include "rand.hpp" +#include "animation.hpp" namespace gui { using namespace guecs; @@ -13,12 +14,12 @@ namespace gui { $gui.position(STATUS_UI_X, STATUS_UI_Y, STATUS_UI_WIDTH, STATUS_UI_HEIGHT); $gui.layout( "[_]" - "[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| inv_slot20]" "[inv_slot21 | inv_slot22 | inv_slot23| inv_slot24]" - "[*%(100,500)circle_area]" + "[*%(100,600)circle_area]" + "[_]" "[_]" "[_]" "[_]" @@ -27,17 +28,17 @@ namespace gui { } void RitualUI::init() { - // $gui.world().set_the({$gui.$parser}); - for(auto& [name, cell] : $gui.cells()) { auto button = $gui.entity(name); if(name == "circle_area") { // $gui.set(button, {}); + $gui.set(button, {"the_ritual_circle"}); $gui.set(button, { [this](auto, auto){ dbc::log("circle clicked"); } }); } else if(name.starts_with("inv_slot")) { + $gui.set(button, {"the_ritual_circle"}); // $gui.set(button, {}); $gui.set(button, { [this, name](auto, auto){ dbc::log(fmt::format("inv_slot {}", name)); } @@ -53,13 +54,7 @@ namespace gui { $ritual_ui.sprite->setPosition({0,0}); $ritual_ui.sprite->setTextureRect($ritual_closed_rect); $ritual_state = RitualUIState::CLOSED; - $ritual_anim.simple = false; - $ritual_anim.looped = false; - $ritual_anim.easing = ease::NONE; - $ritual_anim.stationary = true; - $ritual_anim.texture_width = 380; - $ritual_anim.frames = 3; - $ritual_anim.speed = 0.2f; + $ritual_anim = animation::load("ritual_blanket"); $gui.init(); } @@ -75,20 +70,11 @@ namespace gui { void RitualUI::toggle() { using enum RitualUIState; - switch($ritual_state) { - case OPEN: - $ritual_state = CLOSING; - break; - case CLOSED: - $ritual_state = OPENING; - $ritual_anim.play(); - break; - case OPENING: // ignored - break; - case CLOSING: // ignored - break; - default: - dbc::sentinel("INVALID RitualUIState"); + if($ritual_state == OPEN) { + $ritual_state = CLOSING; + } else if($ritual_state == CLOSED) { + $ritual_state = OPENING; + $ritual_anim.play(); } } @@ -98,40 +84,16 @@ namespace gui { } void RitualUI::render(sf::RenderWindow &window) { - sf::IntRect rect; - sf::Vector2f scale{1.0, 1.0}; - sf::Vector2f pos{0, 0}; - using enum RitualUIState; - switch($ritual_state) { - case OPEN: { - rect = $ritual_open_rect; - } break; - case CLOSED: { - rect = $ritual_closed_rect; - } - break; - case OPENING: { - if($ritual_anim.playing) { - rect = $ritual_closed_rect; - $ritual_anim.step(scale, pos, rect); - } else { - $ritual_state = OPEN; - rect = $ritual_open_rect; - } - } - break; - case CLOSING: { - rect = $ritual_closed_rect; - $ritual_state = CLOSED; - } break; - default: - dbc::sentinel("INVALID RitualUIState"); - } - $ritual_ui.sprite->setTextureRect(rect); - $ritual_ui.sprite->setPosition(pos); - $ritual_ui.sprite->setScale(scale); + if($ritual_state == OPENING) { + if(!animation::apply($ritual_anim, $ritual_ui)) { + $ritual_state = OPEN; + } + } else if($ritual_state == CLOSING) { + $ritual_ui.sprite->setTextureRect($ritual_closed_rect); + $ritual_state = CLOSED; + } window.draw(*$ritual_ui.sprite); if($ritual_state == OPEN) $gui.render(window); diff --git a/tests/animation.cpp b/tests/animation.cpp new file mode 100644 index 0000000..d3fc991 --- /dev/null +++ b/tests/animation.cpp @@ -0,0 +1,53 @@ +#include +#include "animation.hpp" +#include "dinkyecs.hpp" +#include "config.hpp" +#include + +using namespace components; +using namespace textures; + +TEST_CASE("animation easing tests", "[animation]") { + Animation anim; + + anim.easing = ease::NONE; + float res = anim.twitching(); + REQUIRE(res == 0.0); + + anim.easing = ease::SINE; + anim.subframe = 1.0f; + res = anim.twitching(); + REQUIRE(!std::isnan(res)); + + anim.easing = ease::OUT_CIRC; + res = anim.twitching(); + REQUIRE(!std::isnan(res)); + + anim.easing = ease::OUT_BOUNCE; + res = anim.twitching(); + REQUIRE(!std::isnan(res)); + + anim.easing = ease::IN_OUT_BACK; + res = anim.twitching(); + REQUIRE(!std::isnan(res)); + + anim.easing = ease::FUCKFACE; + bool throws = false; + try { anim.twitching(); } catch(...) { throws = true; } + REQUIRE(throws); +} + + +TEST_CASE("animation utility API", "[animation]") { + textures::init(); + animation::init(); + + auto blanket = textures::get("ritual_crafting_area"); + auto anim = animation::load("ritual_blanket"); + + anim.play(); + + while(animation::apply(anim, blanket)) { + fmt::println("animation: {}", anim.subframe); + } +} diff --git a/tests/components.cpp b/tests/components.cpp index 5f95c23..bdca7e2 100644 --- a/tests/components.cpp +++ b/tests/components.cpp @@ -60,34 +60,3 @@ TEST_CASE("make sure json_mods works", "[components]") { auto boss2 = world.get(devils_fingers); REQUIRE(boss2.stage != std::nullopt); } - - -TEST_CASE("animation component special cases", "[components]") { - Animation anim; - - anim.easing = ease::NONE; - float res = anim.twitching(); - REQUIRE(res == 0.0); - - anim.easing = ease::SINE; - anim.subframe = 1.0f; - res = anim.twitching(); - REQUIRE(!std::isnan(res)); - - anim.easing = ease::OUT_CIRC; - res = anim.twitching(); - REQUIRE(!std::isnan(res)); - - anim.easing = ease::OUT_BOUNCE; - res = anim.twitching(); - REQUIRE(!std::isnan(res)); - - anim.easing = ease::IN_OUT_BACK; - res = anim.twitching(); - REQUIRE(!std::isnan(res)); - - anim.easing = ease::FUCKFACE; - bool throws = false; - try { anim.twitching(); } catch(...) { throws = true; } - REQUIRE(throws); -}