From 6269d10807ac7c06f2be7d15c6e5ced1c123abcc Mon Sep 17 00:00:00 2001
From: "Zed A. Shaw" <zed.shaw@gmail.com>
Date: Thu, 1 May 2025 14:40:24 -0400
Subject: [PATCH] The ritual UI is now a lot better using a FSM to control
 everything. Probably one more session to work out the remaining
 functionality.

---
 constants.hpp |   1 +
 guecs.cpp     |   9 +++
 guecs.hpp     |   1 +
 ritual_ui.cpp | 161 ++++++++++++++++++++++++++++++++++++++++++++------
 ritual_ui.hpp |  32 +++++++---
 rituals.cpp   |   4 ++
 rituals.hpp   |   1 +
 7 files changed, 184 insertions(+), 25 deletions(-)

diff --git a/constants.hpp b/constants.hpp
index 30d62a6..a5db392 100644
--- a/constants.hpp
+++ b/constants.hpp
@@ -4,6 +4,7 @@
 #include "color.hpp"
 #include <array>
 
+constexpr const int INV_SLOTS=24;
 constexpr const int TEXTURE_WIDTH=256;
 constexpr const int TEXTURE_HEIGHT=256;
 constexpr const int RAY_VIEW_WIDTH=900;
diff --git a/guecs.cpp b/guecs.cpp
index 47b8af8..57113df 100644
--- a/guecs.cpp
+++ b/guecs.cpp
@@ -27,6 +27,15 @@ namespace guecs {
     text->setString(content);
   }
 
+  void Sprite::update(const std::string& new_name) {
+    if(new_name != name) {
+      name = new_name;
+      auto sprite_texture = textures::get(name);
+      sprite->setTexture(*sprite_texture.texture);
+      sprite->setTextureRect(sprite_texture.sprite->getTextureRect());
+    }
+  }
+
   void Sprite::init(lel::Cell &cell) {
     auto sprite_texture = textures::get(name);
 
diff --git a/guecs.hpp b/guecs.hpp
index 679f127..ee5f568 100644
--- a/guecs.hpp
+++ b/guecs.hpp
@@ -55,6 +55,7 @@ namespace guecs {
     std::shared_ptr<sf::Texture> texture = nullptr;
 
     void init(lel::Cell &cell);
+    void update(const std::string& new_name);
   };
 
   struct Rectangle {
diff --git a/ritual_ui.cpp b/ritual_ui.cpp
index 1df238a..b0d635a 100644
--- a/ritual_ui.cpp
+++ b/ritual_ui.cpp
@@ -11,10 +11,11 @@ namespace gui {
     using namespace guecs;
     using std::any, std::any_cast, std::string, std::make_any;
 
-    void UI::event(Event ev) {
+    void UI::event(Event ev, std::any data) {
       switch($state) {
         FSM_STATE(State, START, ev);
-        FSM_STATE(State, OPENED, ev);
+        FSM_STATE(State, OPENED, ev, data);
+        FSM_STATE(State, CRAFTING, ev, data);
         FSM_STATE(State, CLOSED, ev);
         FSM_STATE(State, OPENING, ev);
         FSM_STATE(State, CLOSING, ev);
@@ -28,17 +29,16 @@ namespace gui {
       state(State::CLOSED);
       $ritual_anim = animation::load("ritual_blanket");
 
-      for(auto& [name, cell] : $gui.cells()) {
-        auto button = $gui.entity(name);
-        $gui.set<Rectangle>(button, {GUECS_PADDING, {50, 50, 50, 150}});
-        $gui.set<Clickable>(button, {
-            [](auto ent, auto) { fmt::println("clicked {}", ent); }
-            });
-      }
-
       auto open_close_toggle = $gui.entity("ritual_ui");
       $gui.set<Clickable>(open_close_toggle, {
-          [&](auto, auto){ event(Event::TOGGLE); }
+        [&](auto, auto){ event(Event::TOGGLE); }
+      });
+
+      auto combine = $gui.entity("combine");
+      $gui.set<Rectangle>(combine, {});
+      $gui.set<Label>(combine, {L"combine"});
+      $gui.set<Clickable>(combine, {
+        [&](auto, auto){ event(Event::COMBINE); }
       });
 
       $gui.init();
@@ -46,15 +46,99 @@ namespace gui {
       state(State::CLOSED);
     }
 
-    void UI::OPENED(Event ev) {
+    void UI::OPENED(Event ev, std::any data) {
+      if(ev == Event::TOGGLE) {
+        clear_blanket();
+        state(State::CLOSING);
+      } else if(ev == Event::SELECT) {
+        // do this before transitioning
+        $craft_state = $ritual_engine.start();
+        state(State::CRAFTING);
+        UI::CRAFTING(ev, data);
+      }
+    }
+
+    void UI::CRAFTING(Event ev, std::any data) {
       if(ev == Event::TOGGLE) {
+        clear_blanket();
         state(State::CLOSING);
+      } else if(ev == Event::COMBINE) {
+        if($craft_state.is_combined()) {
+          auto ritual = $ritual_engine.finalize($craft_state);
+          auto& belt = $level.world->get_the<::ritual::Belt>();
+          belt.equip(belt.next(), ritual);
+          $level.world->send<Events::GUI>(Events::GUI::NEW_RITUAL, $level.player, {});
+          clear_craft_result();
+          $blanket.reset();
+          // BUG: need a way to clear selections
+          load_blanket();
+          $craft_state.reset();
+          state(State::OPENED);
+        }
+      } else if(ev == Event::SELECT) {
+        dbc::check(data.has_value(), "OPENED state given SELECT with no data");
+        auto pair = std::any_cast<SelectedItem>(data);
+        select_item(pair);
+
+        if($blanket.no_selections()) {
+          $craft_state.reset();
+          clear_craft_result();
+          state(State::OPENED);
+        } else {
+          run_crafting_engine();
+
+          if(!$craft_state.is_combined()) {
+            show_craft_failure();
+          } else {
+            show_craft_result();
+          }
+        }
+      }
+    }
+
+    void UI::run_crafting_engine() {
+      $craft_state.reset();
+
+      for(auto [item_id, setting] : $blanket.selected) {
+        auto& item = $blanket.get(item_id);
+        $ritual_engine.load_junk($craft_state, item);
       }
+
+      $ritual_engine.plan($craft_state);
+    }
+
+    void UI::show_craft_result() {
+      using enum ::ritual::Element;
+      auto ritual = $ritual_engine.finalize($craft_state);
+
+      switch(ritual.element) {
+        case FIRE:
+          $gui.show_sprite("result_image", "broken_yoyo-64");
+          break;
+        case LIGHTNING:
+          $gui.show_sprite("result_image", "pocket_watch-64");
+          break;
+        default:
+          $gui.show_sprite("result_image", "severed_finger-64");
+      }
+
+      $gui.show_label("result_text", L"CRAFTING");
+    }
+
+    void UI::clear_craft_result() {
+      $gui.close<Label>("result_text");
+      $gui.close<Sprite>("result_image");
+    }
+
+    void UI::show_craft_failure() {
+      $gui.close<Sprite>("result_image");
+      $gui.show_label("result_text", L"FAILED!");
     }
 
     void UI::CLOSED(Event ev) {
       if(ev == Event::TOGGLE) {
         $ritual_anim.play();
+        load_blanket();
         state(State::OPENING);
       }
     }
@@ -75,7 +159,8 @@ namespace gui {
     }
 
     UI::UI(GameLevel level) :
-      $level(level)
+      $level(level),
+      $blanket($level.world->get_the<::ritual::Blanket>())
     {
       $gui.position(STATUS_UI_X, STATUS_UI_Y, STATUS_UI_WIDTH, STATUS_UI_HEIGHT);
       $gui.layout(
@@ -84,12 +169,12 @@ namespace gui {
           "[inv_slot4 | 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]"
           "[reset                      |*%(200,400)result_text|_]"
           "[*%(100,200)result_image|_                     |_]"
           "[_|_|_]"
           "[combine|_|_]"
-          "[_|craft0|craft1|craft2|craft3|_]"
-          "[_|craft4|craft5|craft6|craft7|_]"
           "[ ritual_ui ]");
     }
 
@@ -106,9 +191,51 @@ namespace gui {
 
       window.draw(*$ritual_ui.sprite);
 
-      if(in_state(State::OPENED)) {
+      if(in_state(State::OPENED) || in_state(State::CRAFTING)) {
         $gui.render(window);
-        $gui.debug_layout(window);
+        // $gui.debug_layout(window);
+      }
+    }
+
+    void UI::clear_blanket() {
+      for(int i = 0; i < INV_SLOTS; i++) {
+        auto slot_id = $gui.entity("inv_slot", i);
+
+        if($gui.has<Sprite>(slot_id)) {
+          $gui.remove<Sprite>(slot_id);
+          $gui.remove<Clickable>(slot_id);
+        }
+      }
+
+      $blanket.reset();
+    }
+
+    void UI::select_item(SelectedItem pair) {
+      auto& sprite = $gui.get<Sprite>(pair.slot_id);
+
+      if($blanket.is_selected(pair.item_id)) {
+        $blanket.deselect(pair.item_id);
+        sprite.sprite->setColor({255, 255, 255, 255});
+      } else {
+        $blanket.select(pair.item_id);
+        sprite.sprite->setColor({255, 200, 200, 200});
+      }
+    }
+
+    void UI::load_blanket() {
+      // update the list of available items
+      int i = 0;
+      for(auto& [item_id, item] : $blanket.contents) {
+        auto slot_id = $gui.entity("inv_slot", i++);
+        auto icon_name = fmt::format("{}-64", item);
+
+        $gui.set_init<Sprite>(slot_id, {icon_name});
+        $gui.set<Clickable>(slot_id, {
+          [&, slot_id, item_id](auto, auto) {
+            auto data = std::make_any<SelectedItem>(slot_id, item_id);
+            event(Event::SELECT, data);
+          }
+        });
       }
     }
   }
diff --git a/ritual_ui.hpp b/ritual_ui.hpp
index 30cc3ae..bd0ef0d 100644
--- a/ritual_ui.hpp
+++ b/ritual_ui.hpp
@@ -8,24 +8,30 @@
 #include "fsm.hpp"
 
 namespace gui {
-
   namespace ritual {
     enum class State {
       START=0,
       OPENED=1,
       CLOSED=2,
       OPENING=3,
-      CLOSING=4
+      CLOSING=4,
+      CRAFTING=5
     };
 
-
     enum class Event {
       STARTED=0,
       TOGGLE=1,
-      TICK=2
+      TICK=2,
+      SELECT=3,
+      COMBINE=4
     };
 
-    class UI : public DeadSimpleFSM<State, Event>{
+    struct SelectedItem {
+      DinkyECS::Entity slot_id;
+      DinkyECS::Entity item_id;
+    };
+
+    class UI : public DeadSimpleFSM<State, Event> {
       public:
         sf::IntRect $ritual_closed_rect{{0,0},{380,720}};
         sf::IntRect $ritual_open_rect{{380 * 2,0},{380,720}};
@@ -33,12 +39,16 @@ namespace gui {
         guecs::UI $gui;
         GameLevel $level;
         textures::SpriteTexture $ritual_ui;
+        ::ritual::Blanket& $blanket;
+        ::ritual::Engine $ritual_engine;
+        ::ritual::CraftingState $craft_state;
 
         UI(GameLevel level);
 
-        void event(Event ev);
+        void event(Event ev, std::any data={});
         void START(Event);
-        void OPENED(Event);
+        void OPENED(Event, std::any data={});
+        void CRAFTING(Event, std::any data={});
         void CLOSED(Event);
         void OPENING(Event);
         void CLOSING(Event);
@@ -46,7 +56,13 @@ namespace gui {
         bool mouse(float x, float y, bool hover);
         void render(sf::RenderWindow &window);
         bool is_open();
+        void load_blanket();
+        void clear_blanket();
+        void select_item(SelectedItem pair);
+        void show_craft_result();
+        void clear_craft_result();
+        void show_craft_failure();
+        void run_crafting_engine();
     };
-
   }
 }
diff --git a/rituals.cpp b/rituals.cpp
index 1f9f4b1..0555e95 100644
--- a/rituals.cpp
+++ b/rituals.cpp
@@ -188,4 +188,8 @@ namespace ritual {
   void Blanket::reset() {
     selected.clear();
   }
+
+  bool Blanket::no_selections() {
+    return selected.size() == 0;
+  }
 }
diff --git a/rituals.hpp b/rituals.hpp
index d242aea..22b92f2 100644
--- a/rituals.hpp
+++ b/rituals.hpp
@@ -93,5 +93,6 @@ namespace ritual {
     void deselect(DinkyECS::Entity ent);
     void reset();
     bool is_selected(DinkyECS::Entity ent);
+    bool no_selections();
   };
 }