Browse Source

Merge pull request #10 from jblanked/dev_0.2

FlipWorld - v0.2
JBlanked 1 year ago
parent
commit
597a892409
73 changed files with 3591 additions and 1725 deletions
  1. 56 7
      README.md
  2. 3 1
      alloc/alloc.c
  3. 2 3
      alloc/alloc.h
  4. 142 17
      app.c
  5. 5 2
      application.fam
  6. BIN
      assets/01-home.png
  7. 18 2
      assets/CHANGELOG.md
  8. 55 8
      assets/README.md
  9. BIN
      assets/icon_alien_gun_10x10px.png
  10. BIN
      assets/icon_axe_10x10px.png
  11. BIN
      assets/icon_axe_16x16px.png
  12. BIN
      assets/icon_axe_alt_10x10px.png
  13. BIN
      assets/icon_axe_alt_16x16px.png
  14. BIN
      assets/icon_bow_10x10px.png
  15. BIN
      assets/icon_sword_10x10px.png
  16. 291 329
      callback/callback.c
  17. 1 14
      callback/callback.h
  18. 52 50
      engine/level.h
  19. BIN
      file_assets/sprites/enemy_left_cyclops_10x11px.fxbm
  20. BIN
      file_assets/sprites/enemy_left_ghost_15x15px.fxbm
  21. BIN
      file_assets/sprites/enemy_left_ogre_10x13px.fxbm
  22. BIN
      file_assets/sprites/enemy_right_cyclops_10x11px.fxbm
  23. BIN
      file_assets/sprites/enemy_right_ghost_15x15px.fxbm
  24. BIN
      file_assets/sprites/enemy_right_ogre_10x13px.fxbm
  25. BIN
      file_assets/sprites/player_left_axe_15x11px.fxbm
  26. BIN
      file_assets/sprites/player_left_bow_13x11px.fxbm
  27. 0 0
      file_assets/sprites/player_left_naked_10x10px.fxbm
  28. BIN
      file_assets/sprites/player_left_sword_15x11px.fxbm
  29. BIN
      file_assets/sprites/player_right_axe_15x11px.fxbm
  30. BIN
      file_assets/sprites/player_right_bow_13x11.fxbm
  31. 0 0
      file_assets/sprites/player_right_naked_10x10px.fxbm
  32. BIN
      file_assets/sprites/player_right_sword_15x11px.fxbm
  33. 4 130
      flip_storage/storage.c
  34. 0 11
      flip_storage/storage.h
  35. 13 3
      flip_world.c
  36. 8 12
      flip_world.h
  37. 325 148
      flipper_http/flipper_http.c
  38. 52 31
      flipper_http/flipper_http.h
  39. 45 25
      game/draw.c
  40. 6 4
      game/draw.h
  41. 699 0
      game/enemy.c
  42. 58 0
      game/enemy.h
  43. 43 174
      game/game.c
  44. 3 23
      game/game.h
  45. 273 387
      game/icon.c
  46. 3 4
      game/icon.h
  47. 119 31
      game/level.c
  48. 1 0
      game/level.h
  49. 395 0
      game/player.c
  50. 76 0
      game/player.h
  51. 748 0
      game/storage.c
  52. 11 0
      game/storage.h
  53. 66 290
      game/world.c
  54. 2 4
      game/world.h
  55. 1 2
      jsmn/jsmn.c
  56. 1 7
      jsmn/jsmn.h
  57. 1 5
      jsmn/jsmn_furi.c
  58. 1 1
      jsmn/jsmn_furi.h
  59. 12 0
      jsmn/jsmn_h.h
  60. BIN
      sprites/enemy_left_cyclops_10x11px.png
  61. BIN
      sprites/enemy_left_ghost_15x15px.png
  62. BIN
      sprites/enemy_left_ogre_10x13px.png
  63. BIN
      sprites/enemy_right_cyclops_10x11px.png
  64. BIN
      sprites/enemy_right_ghost_15x15px.png
  65. BIN
      sprites/enemy_right_ogre_10x13px.png
  66. BIN
      sprites/player_left_axe_15x11px.png
  67. BIN
      sprites/player_left_bow_13x11px.png
  68. 0 0
      sprites/player_left_naked_10x10px.png
  69. BIN
      sprites/player_left_sword_15x11px.png
  70. BIN
      sprites/player_right_axe_15x11px.png
  71. BIN
      sprites/player_right_bow_13x11.png
  72. 0 0
      sprites/player_right_naked_10x10px.png
  73. BIN
      sprites/player_right_sword_15x11px.png

+ 56 - 7
README.md

@@ -3,38 +3,87 @@
 The first open-world multiplayer game for the Flipper Zero, best played with the VGM.
 
 ## Requirements
-- WiFi Developer Board, Raspberry Pi, or ESP32 device with the FlipperHTTP flash: https://github.com/jblanked/FlipperHTTP
+
+- WiFi Developer Board, Raspberry Pi, or ESP32 device with the FlipperHTTP flash: [FlipperHTTP GitHub](https://github.com/jblanked/FlipperHTTP)
 - 2.4 GHz WiFi access point
 
 ## How It Works
-FlipWorld and FlipSocial are connected. Your login information is the same in both apps; if you register an account in either app, you can log in to both using that information. This also means that your friends, messages, and achievements are synced between apps. You only need a username and password to start, which is set in the User Settings. Keep in mind your username will be displayed to others, so choose wisely.
 
-We've simplified the settings for easy use of the app. In the Game Settings, you can adjust the FPS (30, 60, 120, or 240), download World Packs, and select whether you want the screen backlight to always be on.
+FlipWorld and FlipSocial are connected. Your login information is the same in both apps; if you register an account in either app, you can log in to both using that information. This also means that your friends, messages, and achievements are synced between apps. You only need a username and password to start, which are set in the User Settings. Keep in mind your username will be displayed to others, so choose wisely.
+
+**Settings**
+
+- **WiFi**: Enter your SSID and password to connect to your 2.4 GHz network.
+- **User**: Add or update your username and password (this is the same login information as your FlipSocial account).
+- **Game**: Install the Official World Pack, set your FPS (30, 60, 120, or 240), and select whether you want the screen backlight to always be on, the sound to be on, and the vibration to be on.
+
+**Controls**
+
+- **Press/Hold LEFT**: Turn left if not already facing left, then walk left if the button is still pressed.
+- **Press/Hold RIGHT**: Turn right if not already facing right, then walk right if the button is still pressed.
+- **Press/Hold UP**: Walk up.
+- **Press/Hold DOWN**: Walk down.
+- **Press/Hold UP**: Attack/Teleport (set to attack until all enemies are defeated).
+
+**Player Attributes**
+
+- **Health**: The amount of life points the player has.
+- **XP**: The amount of experience points the player has.
+- **Level**: The rank/level of the player.
+- **Strength**: The attack power of the player's attacks.
+- **Health Regeneration**: The amount of health a player gains per second.
+- **Attack Timer**: The duration the player must wait between attacks.
+
+As a new player, you have 100 health, 0 XP, 10 strength, 1 health regeneration, an attack timer of 1, and are level 1. Each level, the player gains an extra 1 strength and 10 health. Additionally, the amount of XP needed to level up increases exponentially by 1.5. For example, to reach level 2, you need 100 XP; for level 3, 150 XP; for level 4, 225 XP; and so on.
+
+**Enemies**
+
+Enemies have similar attributes to players but do not have XP or health regeneration. For example, level 1 enemies have 100 health and 10 strength, just like a level 1 player.
+
+**Attacks**
+
+If an enemy attacks you, your health decreases by the enemy's strength (attack power). Additionally, if an enemy defeats you, your XP decreases by the amount of the enemy's strength. Conversely, when you successfully attack an enemy, you gain 10% of the enemy's strength as health and increase your XP by the enemy's full strength.
+
+## Short Tutorial
 
-The controls in the game are straightforward. Press and hold left to go left, press and hold right to go right, etc. If you press the OK button, it will teleport you to the next available world. **Do NOT spam the OK button** as it could freeze your Flipper Zero. Press it once and wait for it to load.
+1. Ensure your WiFi Developer Board and Video Game Module are flashed with FlipperHTTP.
+2. Install the app.
+3. Restart your Flipper Zero, then open FlipWorld.
+4. Click `Settings -> WiFi`, then input your WiFi SSID and password.
+5. Hit the `BACK` button, click `User`. If your username is not present, click `Username` and add one. Do the same for the password field.
+6. Go back to the main menu and hit `Play`. It will register an account if necessary and fetch data from our API that's used to render our graphics.
 
 ## Roadmap
+
 **v0.2**
-- Stability patch
+
+- Game Mechanics
 - Video Game Module support
 
 **v0.3**
-- ???
+
+- Stability patch
 
 **v0.4**
+
 - ???
 
 **v0.5**
+
 - ???
 
 **v0.6**
+
 - ???
 
 **v0.7**
+
 - ???
 
 **v0.8**
+
 - Multiplayer support
 
 **v1.0**
-- Official release
+
+- Official release

+ 3 - 1
alloc/alloc.c

@@ -1,4 +1,5 @@
 #include <alloc/alloc.h>
+#include <callback/callback.h>
 
 /**
  * @brief Navigation callback for exiting the application
@@ -90,5 +91,6 @@ void flip_world_app_free(FlipWorldApp *app)
     furi_record_close(RECORD_GUI);
 
     // free the app
-    free(app);
+    if (app)
+        free(app);
 }

+ 2 - 3
alloc/alloc.h

@@ -1,6 +1,5 @@
 #pragma once
 #include <flip_world.h>
-#include <callback/callback.h>
 
-extern FlipWorldApp *flip_world_app_alloc();
-extern void flip_world_app_free(FlipWorldApp *app);
+FlipWorldApp *flip_world_app_alloc();
+void flip_world_app_free(FlipWorldApp *app);

+ 142 - 17
app.c

@@ -1,4 +1,5 @@
 #include <alloc/alloc.h>
+#include <flip_storage/storage.h>
 
 // Entry point for the FlipWorld application
 int32_t flip_world_main(void *p)
@@ -14,34 +15,158 @@ int32_t flip_world_main(void *p)
         return -1;
     }
 
+    // initialize the VGM
+    furi_hal_gpio_init_simple(&gpio_ext_pc1, GpioModeOutputPushPull);
+    furi_hal_gpio_write(&gpio_ext_pc1, false); // pull pin 15 low
+
     // check if board is connected (Derek Jamison)
-    // initialize the http
-    if (flipper_http_init(flipper_http_rx_callback, app))
+    FlipperHTTP *fhttp = flipper_http_alloc();
+    if (!fhttp)
     {
-        if (!flipper_http_ping())
-        {
-            FURI_LOG_E(TAG, "Failed to ping the device");
-            return -1;
-        }
+        easy_flipper_dialog("FlipperHTTP Error", "The UART is likely busy.\nEnsure you have the correct\nflash for your board then\nrestart your Flipper Zero.");
+        return -1;
+    }
+
+    if (!flipper_http_ping(fhttp))
+    {
+        FURI_LOG_E(TAG, "Failed to ping the device");
+        flipper_http_free(fhttp);
+        return -1;
+    }
 
-        // Try to wait for pong response.
-        uint8_t counter = 10;
-        while (fhttp.state == INACTIVE && --counter > 0)
+    // Try to wait for pong response.
+    uint32_t counter = 10;
+    while (fhttp->state == INACTIVE && --counter > 0)
+    {
+        FURI_LOG_D(TAG, "Waiting for PONG");
+        furi_delay_ms(100); // this causes a BusFault
+    }
+
+    flipper_http_free(fhttp);
+    if (counter == 0)
+    {
+        easy_flipper_dialog("FlipperHTTP Error", "Ensure your WiFi Developer\nBoard or Pico W is connected\nand the latest FlipperHTTP\nfirmware is installed.");
+    }
+
+    // this will be removed in version 0.3. we'll keep all our data in the data folder from now on
+    // load app version
+    char saved_app_version[16];
+    if (load_char("app_version", saved_app_version, sizeof(saved_app_version)))
+    {
+        float saved_version = strtod(saved_app_version, NULL);
+        if (saved_version == 0.1)
         {
-            FURI_LOG_D(TAG, "Waiting for PONG");
-            furi_delay_ms(100);
-        }
+            // transfer files over into the data folder (to bs used to load the player context)
+            char directory_path[256];
+            snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data");
+
+            // Create the directory
+            Storage *storage = furi_record_open(RECORD_STORAGE);
+            storage_common_mkdir(storage, directory_path);
 
-        if (counter == 0)
-            easy_flipper_dialog("FlipperHTTP Error", "Ensure your WiFi Developer\nBoard or Pico W is connected\nand the latest FlipperHTTP\nfirmware is installed.");
+            // copy the whole folder
+            char source_path[128];
+            snprintf(source_path, sizeof(source_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world");
 
-        flipper_http_deinit();
+            if (storage_common_migrate(storage, source_path, directory_path) != FSE_OK)
+            {
+                FURI_LOG_E(TAG, "Failed to migrate files");
+            }
+            else
+            {
+
+                void clean_up(char *file_path)
+                {
+                    char updated_file_path[128];
+                    snprintf(updated_file_path, sizeof(updated_file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/%s", file_path);
+
+                    // check if the file exists
+                    if (storage_file_exists(storage, updated_file_path) &&
+                        storage_common_remove(storage, updated_file_path) != FSE_OK)
+                    {
+                        FURI_LOG_E(TAG, "Failed to delete %s", updated_file_path);
+                    }
+
+                    // check if the directory exists
+                    if (storage_dir_exists(storage, updated_file_path) &&
+                        storage_common_remove(storage, updated_file_path) != FSE_OK)
+                    {
+                        FURI_LOG_E(TAG, "Failed to delete %s", updated_file_path);
+                    }
+                }
+
+                // clean up
+                clean_up("WiFi-SSID.txt");
+                clean_up("WiFi-Password.txt");
+                clean_up("Flip-Social-Username.txt");
+                clean_up("Flip-Social-Password.txt");
+                clean_up("Game-FPS.txt");
+                clean_up("Game-Screen-Always-On.txt");
+                clean_up("is_logged_in.txt");
+                clean_up("data/worlds");
+                clean_up("data/settings.bin");
+            }
+        }
     }
     else
     {
-        easy_flipper_dialog("FlipperHTTP Error", "The UART is likely busy.\nEnsure you have the correct\nflash for your board then\nrestart your Flipper Zero.");
+        // transfer files over into the data folder (to bs used to load the player context)
+        char directory_path[128];
+        snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data");
+
+        // Create the directory
+        Storage *storage = furi_record_open(RECORD_STORAGE);
+        storage_common_mkdir(storage, directory_path);
+
+        // copy the whole folder
+        char source_path[128];
+        snprintf(source_path, sizeof(source_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world");
+
+        if (storage_common_migrate(storage, source_path, directory_path) != FSE_OK)
+        {
+            FURI_LOG_E(TAG, "Failed to migrate files");
+        }
+        else
+        {
+
+            void clean_up(char *file_path)
+            {
+                char updated_file_path[128];
+                snprintf(updated_file_path, sizeof(updated_file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/%s", file_path);
+
+                // check if the file exists
+                if (storage_file_exists(storage, updated_file_path) &&
+                    storage_common_remove(storage, updated_file_path) != FSE_OK)
+                {
+                    FURI_LOG_E(TAG, "Failed to delete %s", updated_file_path);
+                }
+
+                // check if the directory exists
+                if (storage_dir_exists(storage, updated_file_path) &&
+                    storage_common_remove(storage, updated_file_path) != FSE_OK)
+                {
+                    FURI_LOG_E(TAG, "Failed to delete %s", updated_file_path);
+                }
+            }
+
+            // clean up
+            clean_up("WiFi-SSID.txt");
+            clean_up("WiFi-Password.txt");
+            clean_up("Flip-Social-Username.txt");
+            clean_up("Flip-Social-Password.txt");
+            clean_up("Game-FPS.txt");
+            clean_up("Game-Screen-Always-On.txt");
+            clean_up("is_logged_in.txt");
+            clean_up("data/worlds");
+            clean_up("data/settings.bin");
+        }
     }
 
+    // svae app version
+    char app_version[16];
+    snprintf(app_version, sizeof(app_version), "%f", (double)VERSION);
+    save_char("app_version", app_version);
+
     // Run the view dispatcher
     view_dispatcher_run(app->view_dispatcher);
 

+ 5 - 2
application.fam

@@ -8,11 +8,14 @@ App(
     fap_category="GPIO",
     fap_description="The first open-world multiplayer game, best played with the VGM.", 
     fap_icon_assets="assets",
-    fap_file_assets="assets", 
+    fap_file_assets="file_assets", 
     fap_extbuild=(
         ExtFile(
-            path="${FAP_SRC_DIR}/assets",
+            path="${FAP_SRC_DIR}/file_assets",
             command="${PYTHON3} ${FAP_SRC_DIR}/engine/scripts/sprite_builder.py ${FAP_SRC_DIR.abspath}/sprites ${TARGET.abspath}/sprites",
         ),
     ),
+    fap_author="JBlanked",
+    fap_weburl="https://github.com/jblanked/FlipWorld",
+    fap_version="0.2",
 )

BIN
assets/01-home.png


+ 18 - 2
assets/CHANGELOG.md

@@ -1,2 +1,18 @@
-## 0.1
-- Initial Release
+## 0.2 (2025-01-02)
+Added
+- **Video Game Module Support:** Added support for the Video Game Module (requires FlipperHTTP flash).
+- **Enemies:** Introduced various enemy types to enhance gameplay.
+- **Player Attributes:** Added player health, XP, level, health regeneration, attack, and strength.
+- **Notifications:** Implemented vibration, sound, and LED notifications when a player is attacking or being attacked.
+- **User Interface Enhancements:**: Displayed the player's username above their character and showed the player's health, XP, and level in the bottom left corner of the screen, visible at all times.
+
+Changed
+- **Icons:** Updated all game icons for better visual appeal.
+- **Library Update:** Upgraded to the latest version of the FlipperHTTP library.
+- **Game Settings:** Revised toggles in the Game Settings to ensure they work as intended.
+- **Collisions:** Improved collision mechanics for more accurate interactions.
+- **Default Character Icon:** Updated the default icon representing the player's character.
+
+## 0.1 (2024-12-21)
+Added
+- **Initial Release:** Launched the first version of the game with basic features.

+ 55 - 8
assets/README.md

@@ -1,40 +1,87 @@
-# FlipWorld
-
 The first open-world multiplayer game for the Flipper Zero, best played with the VGM.
 
 ## Requirements
+
 - WiFi Developer Board, Raspberry Pi, or ESP32 device with the FlipperHTTP flash: https://github.com/jblanked/FlipperHTTP
 - 2.4 GHz WiFi access point
 
 ## How It Works
-FlipWorld and FlipSocial are connected. Your login information is the same in both apps; if you register an account in either app, you can log in to both using that information. This also means that your friends, messages, and achievements are synced between apps. You only need a username and password to start, which is set in the User Settings. Keep in mind your username will be displayed to others, so choose wisely.
 
-We've simplified the settings for easy use of the app. In the Game Settings, you can adjust the FPS (30, 60, 120, or 240), download World Packs, and select whether you want the screen backlight to always be on.
+FlipWorld and FlipSocial are connected. Your login information is the same in both apps; if you register an account in either app, you can log in to both using that information. This also means that your friends, messages, and achievements are synced between apps. You only need a username and password to start, which are set in the User Settings. Keep in mind your username will be displayed to others, so choose wisely.
+
+**Settings**
+
+- **WiFi**: Enter your SSID and password to connect to your 2.4 GHz network.
+- **User**: Add or update your username and password (this is the same login information as your FlipSocial account).
+- **Game**: Install the Official World Pack, set your FPS (30, 60, 120, or 240), and select whether you want the screen backlight to always be on, the sound to be on, and the vibration to be on.
+
+**Controls**
+
+- **Press/Hold LEFT**: Turn left if not already facing left, then walk left if the button is still pressed.
+- **Press/Hold RIGHT**: Turn right if not already facing right, then walk right if the button is still pressed.
+- **Press/Hold UP**: Walk up.
+- **Press/Hold DOWN**: Walk down.
+- **Press/Hold UP**: Attack/Teleport (set to attack until all enemies are defeated).
+
+**Player Attributes**
+
+- **Health**: The amount of life points the player has.
+- **XP**: The amount of experience points the player has.
+- **Level**: The rank/level of the player.
+- **Strength**: The attack power of the player's attacks.
+- **Health Regeneration**: The amount of health a player gains per second.
+- **Attack Timer**: The duration the player must wait between attacks.
+
+As a new player, you have 100 health, 0 XP, 10 strength, 1 health regeneration, an attack timer of 1, and are level 1. Each level, the player gains an extra 1 strength and 10 health. Additionally, the amount of XP needed to level up increases exponentially by 1.5. For example, to reach level 2, you need 100 XP; for level 3, 150 XP; for level 4, 225 XP; and so on.
+
+**Enemies**
+
+Enemies have similar attributes to players but do not have XP or health regeneration. For example, level 1 enemies have 100 health and 10 strength, just like a level 1 player.
+
+**Attacks**
+
+If an enemy attacks you, your health decreases by the enemy's strength (attack power). Additionally, if an enemy defeats you, your XP decreases by the amount of the enemy's strength. Conversely, when you successfully attack an enemy, you gain 10% of the enemy's strength as health and increase your XP by the enemy's full strength.
 
-The controls in the game are straightforward. Press and hold left to go left, press and hold right to go right, etc. If you press the OK button, it will teleport you to the next available world. **Do NOT spam the OK button** as it could freeze your Flipper Zero. Press it once and wait for it to load.
+## Short Tutorial
+
+1. Ensure your WiFi Developer Board and Video Game Module are flashed with FlipperHTTP.
+2. Install the app.
+3. Restart your Flipper Zero, then open FlipWorld.
+4. Click "Settings -> WiFi", then input your WiFi SSID and password.
+5. Hit the "BACK" button, click "User". If your username is not present, click "Username" and add one. Do the same for the password field.
+6. Go back to the main menu and hit "Play". It will register an account if necessary and fetch data from our API that's used to render our graphics.
 
 ## Roadmap
+
 **v0.2**
-- Stability patch
+
+- Game Mechanics
 - Video Game Module support
 
 **v0.3**
-- ???
+
+- Stability patch
 
 **v0.4**
+
 - ???
 
 **v0.5**
+
 - ???
 
 **v0.6**
+
 - ???
 
 **v0.7**
+
 - ???
 
 **v0.8**
+
 - Multiplayer support
 
 **v1.0**
-- Official release
+
+- Official release

BIN
assets/icon_alien_gun_10x10px.png


BIN
assets/icon_axe_10x10px.png


BIN
assets/icon_axe_16x16px.png


BIN
assets/icon_axe_alt_10x10px.png


BIN
assets/icon_axe_alt_16x16px.png


BIN
assets/icon_bow_10x10px.png


BIN
assets/icon_sword_10x10px.png


File diff suppressed because it is too large
+ 291 - 329
callback/callback.c


+ 1 - 14
callback/callback.h

@@ -2,14 +2,6 @@
 #include <flip_world.h>
 #include <flip_storage/storage.h>
 
-//
-#include <furi.h>
-#include "engine/engine.h"
-#include "engine/game_engine.h"
-#include "engine/game_manager_i.h"
-#include "engine/level_i.h"
-#include "engine/entity_i.h"
-
 void free_all_views(void *context, bool should_free_variable_item_list, bool should_free_submenu_settings);
 void callback_submenu_choices(void *context, uint32_t index);
 uint32_t callback_to_submenu(void *context);
@@ -26,12 +18,6 @@ enum DataState
     DataStateError,
 };
 
-// typedef enum FlipWorldCustomEvent FlipWorldCustomEvent;
-// enum FlipWorldCustomEvent
-// {
-//     FlipWorldCustomEventProcess,
-// };
-
 typedef struct DataLoaderModel DataLoaderModel;
 typedef bool (*DataLoaderFetch)(DataLoaderModel *model);
 typedef char *(*DataLoaderParser)(DataLoaderModel *model);
@@ -47,6 +33,7 @@ struct DataLoaderModel
     size_t request_count;
     ViewNavigationCallback back_callback;
     FuriTimer *timer;
+    FlipperHTTP *fhttp;
 };
 void flip_world_generic_switch_to_view(FlipWorldApp *app, char *title, DataLoaderFetch fetcher, DataLoaderParser parser, size_t request_count, ViewNavigationCallback back, uint32_t view_id);
 

+ 52 - 50
engine/level.h

@@ -3,58 +3,60 @@
 #include "entity.h"
 
 #ifdef __cplusplus
-extern "C" {
+extern "C"
+{
 #endif
 
-typedef struct GameManager GameManager;
-
-typedef struct {
-    void (*alloc)(Level* level, GameManager* manager, void* context);
-    void (*free)(Level* level, GameManager* manager, void* context);
-    void (*start)(Level* level, GameManager* manager, void* context);
-    void (*stop)(Level* level, GameManager* manager, void* context);
-    size_t context_size;
-} LevelBehaviour;
-
-/**
- * @brief Remove all entities from the level
- * 
- * @param level level instance
- */
-void level_clear(Level* level);
-
-/**
- * @brief Add an entity to the level
- * 
- * @param level level instance
- * @param behaviour entity behaviour
- * @return Entity* 
- */
-Entity* level_add_entity(Level* level, const EntityDescription* behaviour);
-
-/**
- * @brief Remove an entity from the level
- * 
- * @param level level instance
- * @param entity entity to remove
- */
-void level_remove_entity(Level* level, Entity* entity);
-
-/**
- * @brief Send an event to all entities of a certain type in the level
- * 
- * @param level level instance
- * @param sender entity that sends the event
- * @param receiver_desc entity description that will receive the event, NULL for all entities
- * @param type event type
- * @param value event value
- */
-void level_send_event(
-    Level* level,
-    Entity* sender,
-    const EntityDescription* receiver_desc,
-    uint32_t type,
-    EntityEventValue value);
+    typedef struct GameManager GameManager;
+
+    typedef struct
+    {
+        void (*alloc)(Level *level, GameManager *manager, void *context);
+        void (*free)(Level *level, GameManager *manager, void *context);
+        void (*start)(Level *level, GameManager *manager, void *context);
+        void (*stop)(Level *level, GameManager *manager, void *context);
+        size_t context_size;
+    } LevelBehaviour;
+
+    /**
+     * @brief Remove all entities from the level
+     *
+     * @param level level instance
+     */
+    void level_clear(Level *level);
+
+    /**
+     * @brief Add an entity to the level
+     *
+     * @param level level instance
+     * @param behaviour entity behaviour
+     * @return Entity*
+     */
+    Entity *level_add_entity(Level *level, const EntityDescription *behaviour);
+
+    /**
+     * @brief Remove an entity from the level
+     *
+     * @param level level instance
+     * @param entity entity to remove
+     */
+    void level_remove_entity(Level *level, Entity *entity);
+
+    /**
+     * @brief Send an event to all entities of a certain type in the level
+     *
+     * @param level level instance
+     * @param sender entity that sends the event
+     * @param receiver_desc entity description that will receive the event, NULL for all entities
+     * @param type event type
+     * @param value event value
+     */
+    void level_send_event(
+        Level *level,
+        Entity *sender,
+        const EntityDescription *receiver_desc,
+        uint32_t type,
+        EntityEventValue value);
 
 #ifdef __cplusplus
 }

BIN
file_assets/sprites/enemy_left_cyclops_10x11px.fxbm


BIN
file_assets/sprites/enemy_left_ghost_15x15px.fxbm


BIN
file_assets/sprites/enemy_left_ogre_10x13px.fxbm


BIN
file_assets/sprites/enemy_right_cyclops_10x11px.fxbm


BIN
file_assets/sprites/enemy_right_ghost_15x15px.fxbm


BIN
file_assets/sprites/enemy_right_ogre_10x13px.fxbm


BIN
file_assets/sprites/player_left_axe_15x11px.fxbm


BIN
file_assets/sprites/player_left_bow_13x11px.fxbm


+ 0 - 0
assets/sprites/player_left.fxbm → file_assets/sprites/player_left_naked_10x10px.fxbm


BIN
file_assets/sprites/player_left_sword_15x11px.fxbm


BIN
file_assets/sprites/player_right_axe_15x11px.fxbm


BIN
file_assets/sprites/player_right_bow_13x11.fxbm


+ 0 - 0
assets/sprites/player_right.fxbm → file_assets/sprites/player_right_naked_10x10px.fxbm


BIN
file_assets/sprites/player_right_sword_15x11px.fxbm


+ 4 - 130
flip_storage/storage.c

@@ -170,7 +170,7 @@ bool save_char(
     }
     // Create the directory for saving settings
     char directory_path[256];
-    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world");
+    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data");
 
     // Create the directory
     Storage *storage = furi_record_open(RECORD_STORAGE);
@@ -179,7 +179,7 @@ bool save_char(
     // Open the settings file
     File *file = storage_file_alloc(storage);
     char file_path[256];
-    snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/%s.txt", path_name);
+    snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data/%s.txt", path_name);
 
     // Open the file in write mode
     if (!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS))
@@ -221,7 +221,7 @@ bool load_char(
     File *file = storage_file_alloc(storage);
 
     char file_path[256];
-    snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/%s.txt", path_name);
+    snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data/%s.txt", path_name);
 
     // Open the file for reading
     if (!storage_file_open(file, file_path, FSAM_READ, FSOM_OPEN_EXISTING))
@@ -249,133 +249,7 @@ bool load_char(
     storage_file_free(file);
     furi_record_close(RECORD_STORAGE);
 
-    return true;
-}
-
-bool save_world(
-    const char *name,
-    const char *world_data)
-{
-    // Create the directory for saving settings
-    char directory_path[256];
-    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds");
-
-    // Create the directory
-    Storage *storage = furi_record_open(RECORD_STORAGE);
-    storage_common_mkdir(storage, directory_path);
-
-    // Open the settings file
-    File *file = storage_file_alloc(storage);
-    char file_path[256];
-    snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s.json", name);
-
-    // Open the file in write mode
-    if (!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS))
-    {
-        FURI_LOG_E(HTTP_TAG, "Failed to open file for writing: %s", file_path);
-        storage_file_free(file);
-        furi_record_close(RECORD_STORAGE);
-        return false;
-    }
-
-    // Write the data to the file
-    size_t data_size = strlen(world_data) + 1; // Include null terminator
-    if (storage_file_write(file, world_data, data_size) != data_size)
-    {
-        FURI_LOG_E(HTTP_TAG, "Failed to append data to file");
-        storage_file_close(file);
-        storage_file_free(file);
-        furi_record_close(RECORD_STORAGE);
-        return false;
-    }
-
-    storage_file_close(file);
-    storage_file_free(file);
-    furi_record_close(RECORD_STORAGE);
-
-    return true;
-}
-
-bool save_world_furi(FuriString *name, FuriString *world_data)
-{
-    // Create the directory for saving settings
-    char directory_path[256];
-    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds");
-
-    // Create the directory
-    Storage *storage = furi_record_open(RECORD_STORAGE);
-    storage_common_mkdir(storage, directory_path);
-
-    // Open the settings file
-    File *file = storage_file_alloc(storage);
-    char file_path[256];
-    snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s.json", furi_string_get_cstr(name));
-
-    // Open the file in write mode
-    if (!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS))
-    {
-        FURI_LOG_E(HTTP_TAG, "Failed to open file for writing: %s", file_path);
-        storage_file_free(file);
-        furi_record_close(RECORD_STORAGE);
-        return false;
-    }
-
-    // Write the data to the file
-    size_t data_size = furi_string_size(world_data) + 1; // Include null terminator
-    if (storage_file_write(file, furi_string_get_cstr(world_data), data_size) != data_size)
-    {
-        FURI_LOG_E(HTTP_TAG, "Failed to append data to file");
-        storage_file_close(file);
-        storage_file_free(file);
-        furi_record_close(RECORD_STORAGE);
-        return false;
-    }
-
-    storage_file_close(file);
-    storage_file_free(file);
-    furi_record_close(RECORD_STORAGE);
-
-    return true;
-}
-
-bool load_world(
-    const char *name,
-    char *json_data,
-    size_t json_data_size)
-{
-    Storage *storage = furi_record_open(RECORD_STORAGE);
-    File *file = storage_file_alloc(storage);
-
-    char file_path[256];
-    snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s.json", name);
-
-    // Open the file for reading
-    if (!storage_file_open(file, file_path, FSAM_READ, FSOM_OPEN_EXISTING))
-    {
-        storage_file_free(file);
-        furi_record_close(RECORD_STORAGE);
-        return NULL; // Return false if the file does not exist
-    }
-
-    // Read data into the buffer
-    size_t read_count = storage_file_read(file, json_data, json_data_size);
-    if (storage_file_get_error(file) != FSE_OK)
-    {
-        FURI_LOG_E(HTTP_TAG, "Error reading from file.");
-        storage_file_close(file);
-        storage_file_free(file);
-        furi_record_close(RECORD_STORAGE);
-        return false;
-    }
-
-    // Ensure null-termination
-    json_data[read_count - 1] = '\0';
-
-    storage_file_close(file);
-    storage_file_free(file);
-    furi_record_close(RECORD_STORAGE);
-
-    return true;
+    return strlen(value) > 0;
 }
 
 FuriString *load_furi_world(

+ 0 - 11
flip_storage/storage.h

@@ -30,17 +30,6 @@ bool load_char(
     char *value,
     size_t value_size);
 
-bool save_world(
-    const char *name,
-    const char *world_data);
-
-bool save_world_furi(FuriString *name, FuriString *world_data);
-
-bool load_world(
-    const char *name,
-    char *json_data,
-    size_t json_data_size);
-
 FuriString *load_furi_world(
     const char *name);
 

+ 13 - 3
flip_world.c

@@ -1,7 +1,17 @@
 #include <flip_world.h>
 char *game_fps_choices[] = {"30", "60", "120", "240"};
 const float game_fps_choices_2[] = {30.0, 60.0, 120.0, 240.0};
-int game_fps_index = 1;
-char *game_screen_always_on_choices[] = {"No", "Yes"};
+int game_fps_index = 0;
+char *yes_or_no_choices[] = {"No", "Yes"};
 int game_screen_always_on_index = 1;
-FlipWorldApp *app_instance = NULL;
+int game_sound_on_index = 0;
+int game_vibration_on_index = 0;
+bool is_enough_heap(size_t heap_size)
+{
+    size_t free_heap = memmgr_get_free_heap();
+
+    FURI_LOG_I(TAG, "Free heap: %d", free_heap);
+    FURI_LOG_I(TAG, "Total heap: %d", memmgr_get_total_heap());
+
+    return free_heap > (heap_size + 1024); // 1KB buffer
+}

+ 8 - 12
flip_world.h

@@ -2,17 +2,10 @@
 #include <font/font.h>
 #include <flipper_http/flipper_http.h>
 #include <easy_flipper/easy_flipper.h>
-#include <furi.h>
-#include <furi_hal.h>
-#include <gui/gui.h>
-#include <gui/view.h>
-#include <gui/modules/submenu.h>
-#include <gui/view_dispatcher.h>
-#include <notification/notification.h>
-#include <dialogs/dialogs.h>
 
 #define TAG "FlipWorld"
-#define VERSION_TAG "FlipWorld v0.1"
+#define VERSION 0.2
+#define VERSION_TAG "FlipWorld v0.2"
 
 // Define the submenu items for our FlipWorld application
 typedef enum
@@ -62,6 +55,8 @@ typedef struct
     VariableItem *variable_item_game_fps;              // The variable item for Game FPS
     VariableItem *variable_item_game_screen_always_on; // The variable item for Screen always on
     VariableItem *variable_item_game_download_world;   // The variable item for Download world
+    VariableItem *variable_item_game_sound_on;         // The variable item for Sound on
+    VariableItem *variable_item_game_vibration_on;     // The variable item for Vibration on
     //
     VariableItem *variable_item_user_username; // The variable item for the User username
     VariableItem *variable_item_user_password; // The variable item for the User password
@@ -70,12 +65,13 @@ typedef struct
     char *text_input_buffer;         // Buffer for the text input
     char *text_input_temp_buffer;    // Temporary buffer for the text input
     uint32_t text_input_buffer_size; // Size of the text input buffer
-
 } FlipWorldApp;
 
 extern char *game_fps_choices[];
 extern const float game_fps_choices_2[];
 extern int game_fps_index;
-extern char *game_screen_always_on_choices[];
+extern char *yes_or_no_choices[];
 extern int game_screen_always_on_index;
-extern FlipWorldApp *app_instance;
+extern int game_sound_on_index;
+extern int game_vibration_on_index;
+bool is_enough_heap(size_t heap_size);

File diff suppressed because it is too large
+ 325 - 148
flipper_http/flipper_http.c


+ 52 - 31
flipper_http/flipper_http.h

@@ -2,8 +2,7 @@
 // License: MIT
 // Author: JBlanked
 // File: flipper_http.h
-#ifndef FLIPPER_HTTP_H
-#define FLIPPER_HTTP_H
+#pragma once
 
 #include <gui/gui.h>
 #include <gui/view.h>
@@ -23,7 +22,7 @@
 #define TIMEOUT_DURATION_TICKS (5 * 1000) // 5 seconds
 #define BAUDRATE (115200)                 // UART baudrate
 #define RX_BUF_SIZE 2048                  // UART RX buffer size
-#define RX_LINE_BUFFER_SIZE 4096          // UART RX line buffer size (increase for large responses)
+#define RX_LINE_BUFFER_SIZE 3000          // UART RX line buffer size (increase for large responses)
 #define MAX_FILE_SHOW 3000                // Maximum data from file to show
 #define FILE_BUFFER_SIZE 512              // File buffer size
 
@@ -91,8 +90,6 @@ typedef struct
     size_t file_buffer_len;
 } FlipperHTTP;
 
-extern FlipperHTTP fhttp;
-
 // fhttp.last_response holds the last received data from the UART
 
 // Function to append received data to file
@@ -141,172 +138,189 @@ void _flipper_http_rx_callback(
 // UART initialization function
 /**
  * @brief      Initialize UART.
- * @return     true if the UART was initialized successfully, false otherwise.
- * @param      callback  The callback function to handle received data (ex. flipper_http_rx_callback).
- * @param      context   The context to pass to the callback.
+ * @return     FlipperHTTP context if the UART was initialized successfully, NULL otherwise.
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_init(FlipperHTTP_Callback callback, void *context);
+FlipperHTTP *flipper_http_alloc();
 
 // Deinitialize UART
 /**
  * @brief      Deinitialize UART.
  * @return     void
+ * @param fhttp The FlipperHTTP context
  * @note       This function will stop the asynchronous RX, release the serial handle, and free the resources.
  */
-void flipper_http_deinit();
+void flipper_http_free(FlipperHTTP *fhttp);
 
 // Function to send data over UART with newline termination
 /**
  * @brief      Send data over UART with newline termination.
  * @return     true if the data was sent successfully, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @param      data  The data to send over UART.
  * @note       The data will be sent over UART with a newline character appended.
  */
-bool flipper_http_send_data(const char *data);
+bool flipper_http_send_data(FlipperHTTP *fhttp, const char *data);
 
 // Function to send a PING request
 /**
  * @brief      Send a PING request to check if the Wifi Dev Board is connected.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @note       The received data will be handled asynchronously via the callback.
  * @note       This is best used to check if the Wifi Dev Board is connected.
  * @note       The state will remain INACTIVE until a PONG is received.
  */
-bool flipper_http_ping();
+bool flipper_http_ping(FlipperHTTP *fhttp);
 
 // Function to list available commands
 /**
  * @brief      Send a command to list available commands.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_list_commands();
+bool flipper_http_list_commands(FlipperHTTP *fhttp);
 
 // Function to turn on the LED
 /**
  * @brief      Allow the LED to display while processing.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_led_on();
+bool flipper_http_led_on(FlipperHTTP *fhttp);
 
 // Function to turn off the LED
 /**
  * @brief      Disable the LED from displaying while processing.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_led_off();
+bool flipper_http_led_off(FlipperHTTP *fhttp);
 
 // Function to parse JSON data
 /**
  * @brief      Parse JSON data.
  * @return     true if the JSON data was parsed successfully, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @param      key       The key to parse from the JSON data.
  * @param      json_data The JSON data to parse.
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_parse_json(const char *key, const char *json_data);
+bool flipper_http_parse_json(FlipperHTTP *fhttp, const char *key, const char *json_data);
 
 // Function to parse JSON array data
 /**
  * @brief      Parse JSON array data.
  * @return     true if the JSON array data was parsed successfully, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @param      key       The key to parse from the JSON array data.
  * @param      index     The index to parse from the JSON array data.
  * @param      json_data The JSON array data to parse.
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_parse_json_array(const char *key, int index, const char *json_data);
+bool flipper_http_parse_json_array(FlipperHTTP *fhttp, const char *key, int index, const char *json_data);
 
 // Function to scan for WiFi networks
 /**
  * @brief      Send a command to scan for WiFi networks.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_scan_wifi();
+bool flipper_http_scan_wifi(FlipperHTTP *fhttp);
 
 // Function to save WiFi settings (returns true if successful)
 /**
  * @brief      Send a command to save WiFi settings.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_save_wifi(const char *ssid, const char *password);
+bool flipper_http_save_wifi(FlipperHTTP *fhttp, const char *ssid, const char *password);
 
 // Function to get IP address of WiFi Devboard
 /**
  * @brief      Send a command to get the IP address of the WiFi Devboard
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_ip_address();
+bool flipper_http_ip_address(FlipperHTTP *fhttp);
 
 // Function to get IP address of the connected WiFi network
 /**
  * @brief      Send a command to get the IP address of the connected WiFi network.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_ip_wifi();
+bool flipper_http_ip_wifi(FlipperHTTP *fhttp);
 
 // Function to disconnect from WiFi (returns true if successful)
 /**
  * @brief      Send a command to disconnect from WiFi.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_disconnect_wifi();
+bool flipper_http_disconnect_wifi(FlipperHTTP *fhttp);
 
 // Function to connect to WiFi (returns true if successful)
 /**
  * @brief      Send a command to connect to WiFi.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_connect_wifi();
+bool flipper_http_connect_wifi(FlipperHTTP *fhttp);
 
 // Function to send a GET request
 /**
  * @brief      Send a GET request to the specified URL.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @param      url  The URL to send the GET request to.
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_get_request(const char *url);
+bool flipper_http_get_request(FlipperHTTP *fhttp, const char *url);
 
 // Function to send a GET request with headers
 /**
  * @brief      Send a GET request to the specified URL.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @param      url  The URL to send the GET request to.
  * @param      headers  The headers to send with the GET request.
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_get_request_with_headers(const char *url, const char *headers);
+bool flipper_http_get_request_with_headers(FlipperHTTP *fhttp, const char *url, const char *headers);
 
 // Function to send a GET request with headers and return bytes
 /**
  * @brief      Send a GET request to the specified URL.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @param      url  The URL to send the GET request to.
  * @param      headers  The headers to send with the GET request.
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_get_request_bytes(const char *url, const char *headers);
+bool flipper_http_get_request_bytes(FlipperHTTP *fhttp, const char *url, const char *headers);
 
 // Function to send a POST request with headers
 /**
  * @brief      Send a POST request to the specified URL.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @param      url  The URL to send the POST request to.
  * @param      headers  The headers to send with the POST request.
  * @param      data  The data to send with the POST request.
  * @note       The received data will be handled asynchronously via the callback.
  */
 bool flipper_http_post_request_with_headers(
+    FlipperHTTP *fhttp,
     const char *url,
     const char *headers,
     const char *payload);
@@ -315,23 +329,26 @@ bool flipper_http_post_request_with_headers(
 /**
  * @brief      Send a POST request to the specified URL.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @param      url  The URL to send the POST request to.
  * @param      headers  The headers to send with the POST request.
  * @param      payload  The data to send with the POST request.
  * @note       The received data will be handled asynchronously via the callback.
  */
-bool flipper_http_post_request_bytes(const char *url, const char *headers, const char *payload);
+bool flipper_http_post_request_bytes(FlipperHTTP *fhttp, const char *url, const char *headers, const char *payload);
 
 // Function to send a PUT request with headers
 /**
  * @brief      Send a PUT request to the specified URL.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @param      url  The URL to send the PUT request to.
  * @param      headers  The headers to send with the PUT request.
  * @param      data  The data to send with the PUT request.
  * @note       The received data will be handled asynchronously via the callback.
  */
 bool flipper_http_put_request_with_headers(
+    FlipperHTTP *fhttp,
     const char *url,
     const char *headers,
     const char *payload);
@@ -340,12 +357,14 @@ bool flipper_http_put_request_with_headers(
 /**
  * @brief      Send a DELETE request to the specified URL.
  * @return     true if the request was successful, false otherwise.
+ * @param fhttp The FlipperHTTP context
  * @param      url  The URL to send the DELETE request to.
  * @param      headers  The headers to send with the DELETE request.
  * @param      data  The data to send with the DELETE request.
  * @note       The received data will be handled asynchronously via the callback.
  */
 bool flipper_http_delete_request_with_headers(
+    FlipperHTTP *fhttp,
     const char *url,
     const char *headers,
     const char *payload);
@@ -355,21 +374,23 @@ bool flipper_http_delete_request_with_headers(
  * @brief      Callback function to handle received data asynchronously.
  * @return     void
  * @param      line     The received line.
- * @param      context  The context passed to the callback.
+ * @param      context  The FlipperHTTP context.
  * @note       The received data will be handled asynchronously via the callback and handles the state of the UART.
  */
 void flipper_http_rx_callback(const char *line, void *context);
 
 /**
  * @brief Process requests and parse JSON data asynchronously
+ * @param fhttp The FlipperHTTP context
  * @param http_request The function to send the request
  * @param parse_json The function to parse the JSON
  * @return true if successful, false otherwise
  */
-bool flipper_http_process_response_async(bool (*http_request)(void), bool (*parse_json)(void));
+bool flipper_http_process_response_async(FlipperHTTP *fhttp, bool (*http_request)(void), bool (*parse_json)(void));
 
 /**
  * @brief Perform a task while displaying a loading screen
+ * @param fhttp The FlipperHTTP context
  * @param http_request The function to send the request
  * @param parse_response The function to parse the response
  * @param success_view_id The view ID to switch to on success
@@ -377,9 +398,9 @@ bool flipper_http_process_response_async(bool (*http_request)(void), bool (*pars
  * @param view_dispatcher The view dispatcher to use
  * @return
  */
-void flipper_http_loading_task(bool (*http_request)(void),
+void flipper_http_loading_task(FlipperHTTP *fhttp,
+                               bool (*http_request)(void),
                                bool (*parse_response)(void),
                                uint32_t success_view_id,
                                uint32_t failure_view_id,
                                ViewDispatcher **view_dispatcher);
-#endif // FLIPPER_HTTP_H

+ 45 - 25
game/draw.c

@@ -22,6 +22,44 @@ void draw_background(Canvas *canvas, Vector pos)
     draw_bounds(canvas);
 }
 
+// Draw the user stats (health, xp, and level)
+void draw_user_stats(Canvas *canvas, Vector pos, GameManager *manager)
+{
+    GameContext *game_context = game_manager_game_context_get(manager);
+    PlayerContext *player = game_context->player_context;
+
+    // first draw a black rectangle to make the text more readable
+    canvas_invert_color(canvas);
+    canvas_draw_box(canvas, pos.x - 1, pos.y - 7, 34, 21);
+    canvas_invert_color(canvas);
+
+    char health[32];
+    char xp[32];
+    char level[32];
+
+    snprintf(health, sizeof(health), "HP : %ld", player->health);
+    snprintf(xp, sizeof(xp), "XP : %ld", player->xp);
+    snprintf(level, sizeof(level), "LVL: %ld", player->level);
+
+    canvas_set_font_custom(canvas, FONT_SIZE_SMALL);
+    canvas_draw_str(canvas, pos.x, pos.y, health);
+    canvas_draw_str(canvas, pos.x, pos.y + 7, xp);
+    canvas_draw_str(canvas, pos.x, pos.y + 14, level);
+}
+
+void draw_username(Canvas *canvas, Vector pos, char *username)
+{
+    // first draw a black rectangle to make the text more readable
+    // draw box around the username
+    canvas_invert_color(canvas);
+    canvas_draw_box(canvas, pos.x - camera_x - (strlen(username) * 2) - 1, pos.y - camera_y - 14, strlen(username) * 4 + 1, 8);
+    canvas_invert_color(canvas);
+
+    // draw username over player's head
+    canvas_set_font_custom(canvas, FONT_SIZE_SMALL);
+    canvas_draw_str(canvas, pos.x - camera_x - (strlen(username) * 2), pos.y - camera_y - 7, username);
+}
+
 // Draw a line of icons (16 width)
 void draw_icon_line(Canvas *canvas, Vector pos, int amount, bool horizontal, const Icon *icon)
 {
@@ -49,34 +87,16 @@ void draw_icon_line(Canvas *canvas, Vector pos, int amount, bool horizontal, con
         }
     }
 }
-// Draw a half section of icons (16 width)
-void draw_icon_half_world(Canvas *canvas, bool right, const Icon *icon)
-{
-    for (int i = 0; i < 10; i++)
-    {
-        if (right)
-        {
-            draw_icon_line(canvas, (Vector){WORLD_WIDTH / 2 + 6, i * 19 + 2}, 11, true, icon);
-        }
-        else
-        {
-            draw_icon_line(canvas, (Vector){0, i * 19 + 2}, 11, true, icon);
-        }
-    }
-}
+char g_temp_spawn_name[32];
 // Draw an icon at a specific position (with collision detection)
-void spawn_icon(Level *level, const Icon *icon, float x, float y, uint8_t width, uint8_t height)
+void spawn_icon(Level *level, const char *icon_id, float x, float y)
 {
+    snprintf(g_temp_spawn_name, sizeof(g_temp_spawn_name), "%s", icon_id);
     Entity *e = level_add_entity(level, &icon_desc);
-    IconContext *icon_ctx = entity_context_get(e);
-    icon_ctx->icon = icon;
-    icon_ctx->width = width;
-    icon_ctx->height = height;
-    // Set the entity position to the center of the icon
-    entity_pos_set(e, (Vector){x + (width / 2), y + (height / 2)});
+    entity_pos_set(e, (Vector){x, y});
 }
 // Draw a line of icons at a specific position (with collision detection)
-void spawn_icon_line(Level *level, const Icon *icon, float x, float y, uint8_t width, uint8_t height, uint8_t amount, bool horizontal)
+void spawn_icon_line(Level *level, const char *icon_id, float x, float y, uint8_t amount, bool horizontal)
 {
     for (int i = 0; i < amount; i++)
     {
@@ -88,7 +108,7 @@ void spawn_icon_line(Level *level, const Icon *icon, float x, float y, uint8_t w
                 break;
             }
 
-            spawn_icon(level, icon, x + (i * 17), y, width, height);
+            spawn_icon(level, icon_id, x + (i * 17), y);
         }
         else
         {
@@ -98,7 +118,7 @@ void spawn_icon_line(Level *level, const Icon *icon, float x, float y, uint8_t w
                 break;
             }
 
-            spawn_icon(level, icon, x, y + (i * 17), width, height);
+            spawn_icon(level, icon_id, x, y + (i * 17));
         }
     }
 }

+ 6 - 4
game/draw.h

@@ -1,13 +1,15 @@
 #pragma once
 #include "game/icon.h"
+#include <game/player.h>
 
 // Global variables to store camera position
 extern int camera_x;
 extern int camera_y;
 void draw_background(Canvas *canvas, Vector pos);
+void draw_user_stats(Canvas *canvas, Vector pos, GameManager *manager);
+void draw_username(Canvas *canvas, Vector pos, char *username);
 void draw_icon_line(Canvas *canvas, Vector pos, int amount, bool horizontal, const Icon *icon);
-void draw_icon_half_world(Canvas *canvas, bool right, const Icon *icon);
-void spawn_icon(Level *level, const Icon *icon, float x, float y, uint8_t width, uint8_t height);
-void spawn_icon_line(Level *level, const Icon *icon, float x, float y, uint8_t width, uint8_t height, uint8_t amount, bool horizontal);
-
+void spawn_icon(Level *level, const char *icon_id, float x, float y);
+void spawn_icon_line(Level *level, const char *icon_id, float x, float y, uint8_t amount, bool horizontal);
+extern char g_temp_spawn_name[32];
 // create custom icons at https://lopaka.app/sandbox

+ 699 - 0
game/enemy.c

@@ -0,0 +1,699 @@
+// enemy.c
+#include <game/enemy.h>
+#include <notification/notification_messages.h>
+
+static EnemyContext *enemy_context_generic;
+
+// Allocation function
+static EnemyContext *enemy_generic_alloc(
+    const char *id,
+    int index,
+    Vector size,
+    Vector start_position,
+    Vector end_position,
+    float move_timer, // Wait duration before moving again
+    float speed,
+    float attack_timer,
+    float strength,
+    float health)
+{
+    if (!enemy_context_generic)
+    {
+        enemy_context_generic = malloc(sizeof(EnemyContext));
+    }
+    if (!enemy_context_generic)
+    {
+        FURI_LOG_E("Game", "Failed to allocate EnemyContext");
+        return NULL;
+    }
+    snprintf(enemy_context_generic->id, sizeof(enemy_context_generic->id), "%s", id);
+    enemy_context_generic->index = index;
+    enemy_context_generic->size = size;
+    enemy_context_generic->start_position = start_position;
+    enemy_context_generic->end_position = end_position;
+    enemy_context_generic->move_timer = move_timer;   // Set wait duration
+    enemy_context_generic->elapsed_move_timer = 0.0f; // Initialize elapsed timer
+    enemy_context_generic->speed = speed;
+    enemy_context_generic->attack_timer = attack_timer;
+    enemy_context_generic->strength = strength;
+    enemy_context_generic->health = health;
+    // Initialize other fields as needed
+    enemy_context_generic->sprite_right = NULL;         // Assign appropriate sprite
+    enemy_context_generic->sprite_left = NULL;          // Assign appropriate sprite
+    enemy_context_generic->direction = ENEMY_RIGHT;     // Default direction
+    enemy_context_generic->state = ENEMY_MOVING_TO_END; // Start in IDLE state
+    // Set radius based on size, for example, average of size.x and size.y divided by 2
+    enemy_context_generic->radius = (size.x + size.y) / 4.0f;
+    return enemy_context_generic;
+}
+
+// Free function
+static void enemy_generic_free(void *context)
+{
+    if (!context)
+    {
+        FURI_LOG_E("Game", "Enemy generic free: Invalid context");
+        return;
+    }
+    free(context);
+    context = NULL;
+    if (enemy_context_generic)
+    {
+        free(enemy_context_generic);
+        enemy_context_generic = NULL;
+    }
+}
+
+// Enemy start function
+static void enemy_start(Entity *self, GameManager *manager, void *context)
+{
+    UNUSED(manager);
+    if (!self || !context)
+    {
+        FURI_LOG_E("Game", "Enemy start: Invalid parameters");
+        return;
+    }
+    if (!enemy_context_generic)
+    {
+        FURI_LOG_E("Game", "Enemy start: Enemy context not set");
+        return;
+    }
+
+    EnemyContext *enemy_context = (EnemyContext *)context;
+    // Copy fields from generic context
+    snprintf(enemy_context->id, sizeof(enemy_context->id), "%s", enemy_context_generic->id);
+    enemy_context->index = enemy_context_generic->index;
+    enemy_context->size = enemy_context_generic->size;
+    enemy_context->start_position = enemy_context_generic->start_position;
+    enemy_context->end_position = enemy_context_generic->end_position;
+    enemy_context->move_timer = enemy_context_generic->move_timer;
+    enemy_context->elapsed_move_timer = enemy_context_generic->elapsed_move_timer;
+    enemy_context->speed = enemy_context_generic->speed;
+    enemy_context->attack_timer = enemy_context_generic->attack_timer;
+    enemy_context->strength = enemy_context_generic->strength;
+    enemy_context->health = enemy_context_generic->health;
+    enemy_context->sprite_right = enemy_context_generic->sprite_right;
+    enemy_context->sprite_left = enemy_context_generic->sprite_left;
+    enemy_context->direction = enemy_context_generic->direction;
+    enemy_context->state = enemy_context_generic->state;
+    enemy_context->radius = enemy_context_generic->radius;
+
+    // Set enemy's initial position based on start_position
+    entity_pos_set(self, enemy_context->start_position);
+
+    // Add collision circle based on the enemy's radius
+    entity_collider_add_circle(self, enemy_context->radius);
+}
+
+// Enemy render function
+static void enemy_render(Entity *self, GameManager *manager, Canvas *canvas, void *context)
+{
+    if (!self || !context || !canvas || !manager)
+        return;
+
+    EnemyContext *enemy_context = (EnemyContext *)context;
+    GameContext *game_context = game_manager_game_context_get(manager);
+
+    // Get the position of the enemy
+    Vector pos = entity_pos_get(self);
+
+    // Choose sprite based on direction
+    Sprite *current_sprite = NULL;
+    if (enemy_context->direction == ENEMY_LEFT)
+    {
+        current_sprite = enemy_context->sprite_left;
+    }
+    else
+    {
+        current_sprite = enemy_context->sprite_right;
+    }
+
+    // Draw enemy sprite relative to camera, centered on the enemy's position
+    canvas_draw_sprite(
+        canvas,
+        current_sprite,
+        pos.x - camera_x - (enemy_context->size.x / 2),
+        pos.y - camera_y - (enemy_context->size.y / 2));
+
+    // instead of username, draw health
+    char health_str[32];
+    snprintf(health_str, sizeof(health_str), "%.0f", (double)enemy_context->health);
+    draw_username(canvas, pos, health_str);
+
+    // Draw user stats (this has to be done for all enemies)
+    draw_user_stats(canvas, (Vector){0, 50}, manager);
+
+    // draw player username from GameContext
+    Vector posi = entity_pos_get(game_context->player);
+    draw_username(canvas, posi, game_context->player_context->username);
+}
+
+static void send_attack_notification(GameContext *game_context, EnemyContext *enemy_context, bool player_attacked)
+{
+    if (!game_context || !enemy_context)
+    {
+        FURI_LOG_E("Game", "Send attack notification: Invalid parameters");
+        return;
+    }
+
+    NotificationApp *notifications = furi_record_open(RECORD_NOTIFICATION);
+
+    const bool vibration_allowed = strstr(yes_or_no_choices[game_vibration_on_index], "Yes") != NULL;
+    const bool sound_allowed = strstr(yes_or_no_choices[game_sound_on_index], "Yes") != NULL;
+
+    if (player_attacked)
+    {
+        if (vibration_allowed && sound_allowed)
+        {
+            notification_message(notifications, &sequence_success);
+        }
+        else if (vibration_allowed && !sound_allowed)
+        {
+            notification_message(notifications, &sequence_single_vibro);
+        }
+        else if (!vibration_allowed && sound_allowed)
+        {
+            // change this to sound later
+            notification_message(notifications, &sequence_blink_blue_100);
+        }
+        else
+        {
+            notification_message(notifications, &sequence_blink_blue_100);
+        }
+        FURI_LOG_I("Game", "Player attacked enemy '%s'!", enemy_context->id);
+    }
+    else
+    {
+        if (vibration_allowed && sound_allowed)
+        {
+            notification_message(notifications, &sequence_error);
+        }
+        else if (vibration_allowed && !sound_allowed)
+        {
+            notification_message(notifications, &sequence_single_vibro);
+        }
+        else if (!vibration_allowed && sound_allowed)
+        {
+            // change this to sound later
+            notification_message(notifications, &sequence_blink_red_100);
+        }
+        else
+        {
+            notification_message(notifications, &sequence_blink_red_100);
+        }
+
+        FURI_LOG_I("Game", "Enemy '%s' attacked the player!", enemy_context->id);
+    }
+
+    // close the notifications
+    furi_record_close(RECORD_NOTIFICATION);
+}
+
+// Enemy collision function
+static void enemy_collision(Entity *self, Entity *other, GameManager *manager, void *context)
+{
+    if (!self || !other || !context || !manager)
+    {
+        FURI_LOG_E("Game", "Enemy collision: Invalid parameters");
+        return;
+    }
+
+    // Check if the enemy collided with the player
+    if (entity_description_get(other) == &player_desc)
+    {
+        // Retrieve enemy context
+        EnemyContext *enemy_context = (EnemyContext *)context;
+        GameContext *game_context = game_manager_game_context_get(manager);
+        if (!enemy_context)
+        {
+            FURI_LOG_E("Game", "Enemy collision: EnemyContext is NULL");
+            return;
+        }
+        if (!game_context)
+        {
+            FURI_LOG_E("Game", "Enemy collision: GameContext is NULL");
+            return;
+        }
+
+        // Get positions of the enemy and the player
+        Vector enemy_pos = entity_pos_get(self);
+        Vector player_pos = entity_pos_get(other);
+
+        // Determine if the enemy is facing the player or player is facing the enemy
+        bool enemy_is_facing_player = false;
+        bool player_is_facing_enemy = false;
+
+        // Determine if the enemy is facing the player
+        if ((enemy_context->direction == ENEMY_LEFT && player_pos.x < enemy_pos.x) ||
+            (enemy_context->direction == ENEMY_RIGHT && player_pos.x > enemy_pos.x) ||
+            (enemy_context->direction == ENEMY_UP && player_pos.y < enemy_pos.y) ||
+            (enemy_context->direction == ENEMY_DOWN && player_pos.y > enemy_pos.y))
+        {
+            enemy_is_facing_player = true;
+        }
+
+        // Determine if the player is facing the enemy
+        if ((game_context->player_context->direction == PLAYER_LEFT && enemy_pos.x < player_pos.x) ||
+            (game_context->player_context->direction == PLAYER_RIGHT && enemy_pos.x > player_pos.x) ||
+            (game_context->player_context->direction == PLAYER_UP && enemy_pos.y < player_pos.y) ||
+            (game_context->player_context->direction == PLAYER_DOWN && enemy_pos.y > player_pos.y))
+        {
+            player_is_facing_enemy = true;
+        }
+
+        // Handle Player Attacking Enemy (Press OK, facing enemy, and enemy not facing player)
+        if (player_is_facing_enemy && game_context->user_input == GameKeyOk && !enemy_is_facing_player)
+        {
+            if (game_context->player_context->elapsed_attack_timer >= game_context->player_context->attack_timer)
+            {
+                send_attack_notification(game_context, enemy_context, true);
+
+                // Reset player's elapsed attack timer
+                game_context->player_context->elapsed_attack_timer = 0.0f;
+                enemy_context->elapsed_attack_timer = 0.0f; // Reset enemy's attack timer to block enemy attack
+
+                // Increase XP by the enemy's strength
+                game_context->player_context->xp += enemy_context->strength;
+
+                // Increase healthy by 10% of the enemy's strength
+                game_context->player_context->health += enemy_context->strength * 0.1f;
+                if (game_context->player_context->health > game_context->player_context->max_health)
+                {
+                    game_context->player_context->health = game_context->player_context->max_health;
+                }
+
+                // Decrease enemy health by player strength
+                enemy_context->health -= game_context->player_context->strength;
+
+                if (enemy_context->health <= 0)
+                {
+                    FURI_LOG_I("Game", "Enemy '%s' is dead.. resetting enemy position and health", enemy_context->id);
+                    enemy_context->state = ENEMY_DEAD;
+
+                    // Reset enemy position and health
+                    enemy_context->health = 100; // this needs to be set to the enemy's max health
+
+                    // remove from game context and set in safe zone
+                    game_context->enemies[enemy_context->index] = NULL;
+                    game_context->enemy_count--;
+                    entity_collider_remove(self);
+                    entity_pos_set(self, (Vector){-100, -100});
+                    return;
+                }
+                else
+                {
+                    FURI_LOG_I("Game", "Enemy '%s' took %f damage from player", enemy_context->id, (double)game_context->player_context->strength);
+                    enemy_context->state = ENEMY_ATTACKED;
+
+                    // Bounce the enemy back by X units opposite their last movement direction
+                    enemy_pos.x -= game_context->player_context->dx * enemy_context->radius;
+                    enemy_pos.y -= game_context->player_context->dy * enemy_context->radius;
+                    entity_pos_set(self, enemy_pos);
+
+                    // Reset enemy's movement direction to prevent immediate re-collision
+                    game_context->player_context->dx = 0;
+                    game_context->player_context->dy = 0;
+                }
+            }
+            else
+            {
+                FURI_LOG_I("Game", "Player attack on enemy '%s' is on cooldown: %f seconds remaining", enemy_context->id, (double)(game_context->player_context->attack_timer - game_context->player_context->elapsed_attack_timer));
+            }
+        }
+        // Handle Enemy Attacking Player (enemy facing player)
+        else if (enemy_is_facing_player)
+        {
+            if (enemy_context->elapsed_attack_timer >= enemy_context->attack_timer)
+            {
+                send_attack_notification(game_context, enemy_context, false);
+
+                // Reset enemy's elapsed attack timer
+                enemy_context->elapsed_attack_timer = 0.0f;
+
+                // Decrease player health by enemy strength
+                game_context->player_context->health -= enemy_context->strength;
+
+                if (game_context->player_context->health <= 0)
+                {
+                    FURI_LOG_I("Game", "Player is dead.. resetting player position and health");
+                    game_context->player_context->state = PLAYER_DEAD;
+
+                    // Reset player position and health
+                    entity_pos_set(other, game_context->player_context->start_position);
+                    game_context->player_context->health = game_context->player_context->max_health;
+
+                    // subtract player's XP by the enemy's strength
+                    game_context->player_context->xp -= enemy_context->strength;
+                    if ((int)game_context->player_context->xp < 0)
+                    {
+                        game_context->player_context->xp = 0;
+                    }
+                }
+                else
+                {
+                    FURI_LOG_I("Game", "Player took %f damage from enemy '%s'", (double)enemy_context->strength, enemy_context->id);
+                    game_context->player_context->state = PLAYER_ATTACKED;
+
+                    // Bounce the player back by X units opposite their last movement direction
+                    player_pos.x -= game_context->player_context->dx * enemy_context->radius;
+                    player_pos.y -= game_context->player_context->dy * enemy_context->radius;
+                    entity_pos_set(other, player_pos);
+
+                    // Reset player's movement direction to prevent immediate re-collision
+                    game_context->player_context->dx = 0;
+                    game_context->player_context->dy = 0;
+                }
+            }
+            else
+            {
+                FURI_LOG_I("Game", "Enemy '%s' attack on player is on cooldown: %f seconds remaining", enemy_context->id, (double)(enemy_context->attack_timer - enemy_context->elapsed_attack_timer));
+            }
+        }
+        else // handle other collisions
+        {
+            // bounce player and enemy away from each other
+            Vector player_pos = entity_pos_get(other);
+            Vector enemy_pos = entity_pos_get(self);
+
+            // Calculate the direction vector from player to enemy
+            Vector direction_vector = {
+                enemy_pos.x - player_pos.x,
+                enemy_pos.y - player_pos.y};
+
+            // Normalize the direction vector
+            float length = sqrt(direction_vector.x * direction_vector.x + direction_vector.y * direction_vector.y);
+            if (length != 0)
+            {
+                direction_vector.x /= length;
+                direction_vector.y /= length;
+            }
+
+            // Move the player and enemy away from each other
+            player_pos.y -= direction_vector.y * 3;
+            entity_pos_set(other, player_pos);
+
+            enemy_pos.x += direction_vector.x * 3;
+            entity_pos_set(self, enemy_pos);
+
+            // Reset player's movement direction to prevent immediate re-collision
+            game_context->player_context->dx = 0;
+            game_context->player_context->dy = 0;
+        }
+
+        // Reset enemy's state
+        enemy_context->state = ENEMY_IDLE;
+        enemy_context->elapsed_move_timer = 0.0f;
+
+        if (game_context->player_context->state == PLAYER_DEAD)
+        {
+            // Reset player's position and health
+            entity_pos_set(other, game_context->player_context->start_position);
+            game_context->player_context->health = 100;
+        }
+    }
+}
+
+// Enemy update function
+static void enemy_update(Entity *self, GameManager *manager, void *context)
+{
+    if (!self || !context || !manager)
+        return;
+
+    EnemyContext *enemy_context = (EnemyContext *)context;
+    if (!enemy_context || enemy_context->state == ENEMY_DEAD)
+    {
+        return;
+    }
+
+    GameContext *game_context = game_manager_game_context_get(manager);
+    if (!game_context)
+    {
+        FURI_LOG_E("Game", "Enemy update: Failed to get GameContext");
+        return;
+    }
+
+    float delta_time = 1.0f / game_context->fps;
+
+    // Increment the elapsed_attack_timer for the enemy
+    enemy_context->elapsed_attack_timer += delta_time;
+
+    switch (enemy_context->state)
+    {
+    case ENEMY_IDLE:
+        // Increment the elapsed_move_timer
+        enemy_context->elapsed_move_timer += delta_time;
+
+        // Check if it's time to move again
+        if (enemy_context->elapsed_move_timer >= enemy_context->move_timer)
+        {
+            // Determine the next state based on the current position
+            Vector current_pos = entity_pos_get(self);
+            if (fabs(current_pos.x - enemy_context->start_position.x) < (double)1.0 &&
+                fabs(current_pos.y - enemy_context->start_position.y) < (double)1.0)
+            {
+                enemy_context->state = ENEMY_MOVING_TO_END;
+            }
+            else
+            {
+                enemy_context->state = ENEMY_MOVING_TO_START;
+            }
+            enemy_context->elapsed_move_timer = 0.0f;
+        }
+        break;
+
+    case ENEMY_MOVING_TO_END:
+    case ENEMY_MOVING_TO_START:
+    {
+        // Determine the target position based on the current state
+        Vector target_position = (enemy_context->state == ENEMY_MOVING_TO_END) ? enemy_context->end_position : enemy_context->start_position;
+
+        // Get current position
+        Vector current_pos = entity_pos_get(self);
+        Vector direction_vector = {0, 0};
+
+        // Calculate direction towards the target
+        if (current_pos.x < target_position.x)
+        {
+            direction_vector.x = 1.0f;
+            enemy_context->direction = ENEMY_RIGHT;
+        }
+        else if (current_pos.x > target_position.x)
+        {
+            direction_vector.x = -1.0f;
+            enemy_context->direction = ENEMY_LEFT;
+        }
+
+        if (current_pos.y < target_position.y)
+        {
+            direction_vector.y = 1.0f;
+            enemy_context->direction = ENEMY_DOWN;
+        }
+        else if (current_pos.y > target_position.y)
+        {
+            direction_vector.y = -1.0f;
+            enemy_context->direction = ENEMY_UP;
+        }
+
+        // Normalize direction vector
+        float length = sqrt(direction_vector.x * direction_vector.x + direction_vector.y * direction_vector.y);
+        if (length != 0)
+        {
+            direction_vector.x /= length;
+            direction_vector.y /= length;
+        }
+
+        // Update position based on direction and speed
+        Vector new_pos = current_pos;
+        new_pos.x += direction_vector.x * enemy_context->speed * delta_time;
+        new_pos.y += direction_vector.y * enemy_context->speed * delta_time;
+
+        // Clamp the position to the target to prevent overshooting
+        if ((direction_vector.x > 0.0f && new_pos.x > target_position.x) ||
+            (direction_vector.x < 0.0f && new_pos.x < target_position.x))
+        {
+            new_pos.x = target_position.x;
+        }
+
+        if ((direction_vector.y > 0.0f && new_pos.y > target_position.y) ||
+            (direction_vector.y < 0.0f && new_pos.y < target_position.y))
+        {
+            new_pos.y = target_position.y;
+        }
+
+        entity_pos_set(self, new_pos);
+
+        // Check if the enemy has reached or surpassed the target_position
+        bool reached_x = fabs(new_pos.x - target_position.x) < (double)1.0;
+        bool reached_y = fabs(new_pos.y - target_position.y) < (double)1.0;
+
+        // If reached the target position on both axes, transition to IDLE
+        if (reached_x && reached_y)
+        {
+            enemy_context->state = ENEMY_IDLE;
+            enemy_context->elapsed_move_timer = 0.0f;
+        }
+    }
+    break;
+
+    default:
+        break;
+    }
+}
+
+// Free function for the entity
+static void enemy_free(Entity *self, GameManager *manager, void *context)
+{
+    UNUSED(self);
+    UNUSED(manager);
+    if (context)
+        enemy_generic_free(context);
+}
+
+// Enemy behavior structure
+static const EntityDescription _generic_enemy = {
+    .start = enemy_start,
+    .stop = enemy_free,
+    .update = enemy_update,
+    .render = enemy_render,
+    .collision = enemy_collision,
+    .event = NULL,
+    .context_size = sizeof(EnemyContext),
+};
+
+// Enemy function to return the entity description
+const EntityDescription *enemy(
+    GameManager *manager,
+    const char *id,
+    int index,
+    Vector start_position,
+    Vector end_position,
+    float move_timer, // Wait duration before moving again
+    float speed,
+    float attack_timer,
+    float strength,
+    float health)
+{
+    SpriteContext *sprite_context = get_sprite_context(id);
+    if (!sprite_context)
+    {
+        FURI_LOG_E("Game", "Failed to get SpriteContext");
+        return NULL;
+    }
+
+    // Allocate a new EnemyContext with provided parameters
+    enemy_context_generic = enemy_generic_alloc(
+        id,
+        index,
+        (Vector){sprite_context->width, sprite_context->height},
+        start_position,
+        end_position,
+        move_timer, // Set wait duration
+        speed,
+        attack_timer,
+        strength,
+        health);
+    if (!enemy_context_generic)
+    {
+        FURI_LOG_E("Game", "Failed to allocate EnemyContext");
+        return NULL;
+    }
+
+    enemy_context_generic->sprite_right = game_manager_sprite_load(manager, sprite_context->right_file_name);
+    enemy_context_generic->sprite_left = game_manager_sprite_load(manager, sprite_context->left_file_name);
+
+    // Set initial direction based on start and end positions
+    if (start_position.x < end_position.x)
+    {
+        enemy_context_generic->direction = ENEMY_RIGHT;
+    }
+    else
+    {
+        enemy_context_generic->direction = ENEMY_LEFT;
+    }
+
+    // Set initial state based on movement
+    if (start_position.x != end_position.x || start_position.y != end_position.y)
+    {
+        enemy_context_generic->state = ENEMY_MOVING_TO_END;
+    }
+    else
+    {
+        enemy_context_generic->state = ENEMY_IDLE;
+    }
+
+    return &_generic_enemy;
+}
+
+void spawn_enemy_json_furi(Level *level, GameManager *manager, FuriString *json)
+{
+    if (!level)
+    {
+        FURI_LOG_E("Game", "Level is NULL");
+        return;
+    }
+    if (!json)
+    {
+        FURI_LOG_E("Game", "JSON is NULL");
+        return;
+    }
+    if (!manager)
+    {
+        FURI_LOG_E("Game", "GameManager is NULL");
+        return;
+    }
+    // parameters: id, index, size.x, size.y, start_position.x, start_position.y, end_position.x, end_position.y, move_timer, speed, attack_timer, strength, health
+    FuriString *id = get_json_value_furi("id", json);
+    FuriString *_index = get_json_value_furi("index", json);
+    //
+    FuriString *start_position = get_json_value_furi("start_position", json);
+    FuriString *start_position_x = get_json_value_furi("x", start_position);
+    FuriString *start_position_y = get_json_value_furi("y", start_position);
+    //
+    FuriString *end_position = get_json_value_furi("end_position", json);
+    FuriString *end_position_x = get_json_value_furi("x", end_position);
+    FuriString *end_position_y = get_json_value_furi("y", end_position);
+    //
+    FuriString *move_timer = get_json_value_furi("move_timer", json);
+    FuriString *speed = get_json_value_furi("speed", json);
+    FuriString *attack_timer = get_json_value_furi("attack_timer", json);
+    FuriString *strength = get_json_value_furi("strength", json);
+    FuriString *health = get_json_value_furi("health", json);
+    //
+
+    if (!id || !_index || !start_position || !start_position_x || !start_position_y || !end_position || !end_position_x || !end_position_y || !move_timer || !speed || !attack_timer || !strength || !health)
+    {
+        FURI_LOG_E("Game", "Failed to parse JSON values");
+        return;
+    }
+
+    GameContext *game_context = game_manager_game_context_get(manager);
+    if (game_context && game_context->enemy_count < MAX_ENEMIES && !game_context->enemies[game_context->enemy_count])
+    {
+        game_context->enemies[game_context->enemy_count] = level_add_entity(level, enemy(
+                                                                                       manager,
+                                                                                       furi_string_get_cstr(id),
+                                                                                       atoi(furi_string_get_cstr(_index)),
+                                                                                       (Vector){strtod(furi_string_get_cstr(start_position_x), NULL), strtod(furi_string_get_cstr(start_position_y), NULL)},
+                                                                                       (Vector){strtod(furi_string_get_cstr(end_position_x), NULL), strtod(furi_string_get_cstr(end_position_y), NULL)},
+                                                                                       strtod(furi_string_get_cstr(move_timer), NULL),
+                                                                                       strtod(furi_string_get_cstr(speed), NULL),
+                                                                                       strtod(furi_string_get_cstr(attack_timer), NULL),
+                                                                                       strtod(furi_string_get_cstr(strength), NULL),
+                                                                                       strtod(furi_string_get_cstr(health), NULL)));
+        game_context->enemy_count++;
+    }
+
+    furi_string_free(id);
+    furi_string_free(_index);
+    furi_string_free(start_position);
+    furi_string_free(start_position_x);
+    furi_string_free(start_position_y);
+    furi_string_free(end_position);
+    furi_string_free(end_position_x);
+    furi_string_free(end_position_y);
+    furi_string_free(move_timer);
+    furi_string_free(speed);
+    furi_string_free(attack_timer);
+    furi_string_free(strength);
+    furi_string_free(health);
+}

+ 58 - 0
game/enemy.h

@@ -0,0 +1,58 @@
+#pragma once
+#include <game/game.h>
+#include "flip_world.h"
+
+typedef enum
+{
+    ENEMY_IDLE,
+    ENEMY_MOVING,
+    ENEMY_MOVING_TO_START,
+    ENEMY_MOVING_TO_END,
+    ENEMY_ATTACKING,
+    ENEMY_ATTACKED,
+    ENEMY_DEAD
+} EnemyState;
+
+typedef enum
+{
+    ENEMY_UP,
+    ENEMY_DOWN,
+    ENEMY_LEFT,
+    ENEMY_RIGHT
+} EnemyDirection;
+
+// EnemyContext definition
+typedef struct
+{
+    char id[64];                // Unique ID for the enemy type
+    int index;                  // Index for the specific enemy instance
+    Vector size;                // Size of the enemy
+    Sprite *sprite_right;       // Enemy sprite when looking right
+    Sprite *sprite_left;        // Enemy sprite when looking left
+    EnemyDirection direction;   // Direction the enemy is facing
+    EnemyState state;           // Current state of the enemy
+    Vector start_position;      // Start position of the enemy
+    Vector end_position;        // End position of the enemy
+    float move_timer;           // Timer for the enemy movement
+    float elapsed_move_timer;   // Elapsed time for the enemy movement
+    float radius;               // Collision radius for the enemy
+    float speed;                // Speed of the enemy
+    float attack_timer;         // Cooldown duration between attacks
+    float elapsed_attack_timer; // Time elapsed since the last attack
+    float strength;             // Damage the enemy deals
+    float health;               // Health of the enemy
+} EnemyContext;
+
+const EntityDescription *enemy(
+    GameManager *manager,
+    const char *id,
+    int index,
+    Vector start_position,
+    Vector end_position,
+    float move_timer, // Wait duration before moving again
+    float speed,
+    float attack_timer,
+    float strength,
+    float health);
+
+void spawn_enemy_json_furi(Level *level, GameManager *manager, FuriString *json);

+ 43 - 174
game/game.c

@@ -1,148 +1,5 @@
-#include "game.h"
-
-/****** Entities: Player ******/
-static Level *levels[10];
-static int level_count = 0;
-static bool has_pressed_ok = false;
-
-static Level *get_next_level(GameManager *manager)
-{
-    has_pressed_ok = false;
-    Level *current_level = game_manager_current_level_get(manager);
-    for (int i = 0; i < level_count; i++)
-    {
-        if (levels[i] == current_level)
-        {
-            // check if i+1 is out of bounds, if so, return the first level
-            return levels[(i + 1) % level_count] ? levels[(i + 1) % level_count] : levels[0];
-        }
-    }
-    return levels[0] ? levels[0] : game_manager_add_level(manager, generic_level("town_world", 0));
-}
-
-void player_spawn(Level *level, GameManager *manager)
-{
-    Entity *player = level_add_entity(level, &player_desc);
-
-    // Set player position.
-    // Depends on your game logic, it can be done in start entity function, but also can be done here.
-    entity_pos_set(player, (Vector){WORLD_WIDTH / 2, WORLD_HEIGHT / 2});
-
-    // Add collision box to player entity
-    // Box is centered in player x and y, and it's size is 10x10
-    entity_collider_add_rect(player, 10 + PLAYER_COLLISION_HORIZONTAL, 10 + PLAYER_COLLISION_VERTICAL);
-
-    // Get player context
-    PlayerContext *player_context = entity_context_get(player);
-
-    // Load player sprite
-    player_context->sprite_right = game_manager_sprite_load(manager, "player_right.fxbm");
-    player_context->sprite_left = game_manager_sprite_load(manager, "player_left.fxbm");
-    player_context->is_looking_left = false; // player starts looking right
-}
-
-// Modify player_update to track direction
-static void player_update(Entity *self, GameManager *manager, void *context)
-{
-    PlayerContext *player = (PlayerContext *)context;
-    InputState input = game_manager_input_get(manager);
-    Vector pos = entity_pos_get(self);
-
-    // Store previous direction
-    int prev_dx = player->dx;
-    int prev_dy = player->dy;
-
-    // Reset movement deltas each frame
-    player->dx = 0;
-    player->dy = 0;
-
-    // Handle movement input
-    if (input.held & GameKeyUp)
-    {
-        pos.y -= 2;
-        player->dy = -1;
-    }
-    if (input.held & GameKeyDown)
-    {
-        pos.y += 2;
-        player->dy = 1;
-    }
-    if (input.held & GameKeyLeft)
-    {
-        pos.x -= 2;
-        player->dx = -1;
-        player->is_looking_left = true;
-    }
-    if (input.held & GameKeyRight)
-    {
-        pos.x += 2;
-        player->dx = 1;
-        player->is_looking_left = false;
-    }
-
-    // switch levels if holding OK
-    if (input.held & GameKeyOk)
-    {
-        if (!has_pressed_ok)
-        {
-            has_pressed_ok = true;
-            game_manager_next_level_set(manager, get_next_level(manager));
-            furi_delay_ms(500);
-        }
-        return;
-    }
-
-    // Clamp the player's position to stay within world bounds
-    pos.x = CLAMP(pos.x, WORLD_WIDTH - 5, 5);
-    pos.y = CLAMP(pos.y, WORLD_HEIGHT - 5, 5);
-
-    // Update player position
-    entity_pos_set(self, pos);
-
-    // If the player is not moving, retain the last movement direction
-    if (player->dx == 0 && player->dy == 0)
-    {
-        player->dx = prev_dx;
-        player->dy = prev_dy;
-    }
-
-    // Handle back button to stop the game
-    if (input.pressed & GameKeyBack)
-    {
-        game_manager_game_stop(manager);
-    }
-}
-
-static void player_render(Entity *self, GameManager *manager, Canvas *canvas, void *context)
-{
-    // Get player context
-    UNUSED(manager);
-    PlayerContext *player = context;
-
-    // Get player position
-    Vector pos = entity_pos_get(self);
-
-    // Draw background (updates camera_x and camera_y)
-    draw_background(canvas, pos);
-
-    // Draw player sprite relative to camera, centered on the player's position
-    canvas_draw_sprite(
-        canvas,
-        player->is_looking_left ? player->sprite_left : player->sprite_right,
-        pos.x - camera_x - 5, // Center the sprite horizontally
-        pos.y - camera_y - 5  // Center the sprite vertically
-    );
-}
-
-const EntityDescription player_desc = {
-    .start = NULL,                         // called when entity is added to the level
-    .stop = NULL,                          // called when entity is removed from the level
-    .update = player_update,               // called every frame
-    .render = player_render,               // called every frame, after update
-    .collision = NULL,                     // called when entity collides with another entity
-    .event = NULL,                         // called when entity receives an event
-    .context_size = sizeof(PlayerContext), // size of entity context, will be automatically allocated and freed
-};
+#include <game/game.h>
+#include <game/storage.h>
 
 /****** Game ******/
 /*
@@ -154,31 +11,21 @@ static void game_start(GameManager *game_manager, void *ctx)
     // Do some initialization here, for example you can load score from storage.
     // For simplicity, we will just set it to 0.
     GameContext *game_context = ctx;
-    game_context->score = 0;
-
-    // open the world list from storage, then create a level for each world
-    char file_path[128];
-    snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/world_list.json");
-    FuriString *world_list = flipper_http_load_from_file(file_path);
-    if (!world_list)
+    game_context->fps = game_fps_choices_2[game_fps_index];
+    game_context->player_context = NULL;
+    game_context->current_level = 0;
+    game_context->ended_early = false;
+    if (!allocate_level(game_manager, 0))
     {
-        FURI_LOG_E("Game", "Failed to load world list");
-        levels[0] = game_manager_add_level(game_manager, generic_level("town_world", 0));
-        level_count = 1;
+        FURI_LOG_E("Game", "Failed to allocate level 0");
         return;
     }
-    for (int i = 0; i < 10; i++)
-    {
-        FuriString *world_name = get_json_array_value_furi("worlds", i, world_list);
-        if (!world_name)
-        {
-            break;
-        }
-        levels[i] = game_manager_add_level(game_manager, generic_level(furi_string_get_cstr(world_name), i));
-        furi_string_free(world_name);
-        level_count++;
-    }
-    furi_string_free(world_list);
+    game_context->level_count = 1;
+    game_context->levels[1] = NULL;
+
+    // imu
+    game_context->imu = imu_alloc();
+    game_context->imu_present = imu_present(game_context->imu);
 }
 
 /*
@@ -188,16 +35,38 @@ static void game_start(GameManager *game_manager, void *ctx)
 */
 static void game_stop(void *ctx)
 {
-    UNUSED(ctx);
-    // If you want to do other final logic (like saving scores), do it here.
-    // But do NOT free levels[] if the engine manages them.
+    if (!ctx)
+    {
+        FURI_LOG_E("Game", "Invalid game context");
+        return;
+    }
 
-    // Just clear out your pointer array if you like (not strictly necessary)
-    for (int i = 0; i < level_count; i++)
+    GameContext *game_context = ctx;
+    if (!game_context)
     {
-        levels[i] = NULL;
+        FURI_LOG_E("Game", "Game context is NULL");
+        return;
+    }
+
+    imu_free(game_context->imu);
+    game_context->imu = NULL;
+
+    if (game_context->player_context)
+    {
+        FURI_LOG_I("Game", "Game ending");
+        if (!game_context->ended_early)
+        {
+            easy_flipper_dialog("Game Over", "Thanks for playing Flip World!\nHit BACK then wait for\nthe game to save.");
+        }
+        else
+        {
+            easy_flipper_dialog("Game Over", "Ran out of memory so the\ngame ended early.\nHit BACK to exit.");
+        }
+        FURI_LOG_I("Game", "Saving player context");
+        save_player_context(game_context->player_context);
+        FURI_LOG_I("Game", "Player context saved");
+        easy_flipper_dialog("Game Saved", "Hit BACK to exit.");
     }
-    level_count = 0;
 }
 
 /*

+ 3 - 23
game/game.h

@@ -1,28 +1,8 @@
 #pragma once
 #include "engine/engine.h"
+#include <engine/level_i.h>
 #include <game/world.h>
 #include <game/level.h>
+#include <game/enemy.h>
 #include "flip_world.h"
-#include "flip_storage/storage.h"
-
-#define PLAYER_COLLISION_VERTICAL 5
-#define PLAYER_COLLISION_HORIZONTAL 5
-
-typedef struct
-{
-    uint32_t score;
-} GameContext;
-
-typedef struct
-{
-    Vector trajectory;    // Direction player would like to move.
-    float radius;         // collision radius
-    int8_t dx;            // x direction
-    int8_t dy;            // y direction
-    Sprite *sprite_right; // player sprite
-    Sprite *sprite_left;  // player sprite looking left
-    bool is_looking_left; // player is looking left
-} PlayerContext;
-
-extern const EntityDescription player_desc;
-void player_spawn(Level *level, GameManager *manager);
+#include <game/player.h>

+ 273 - 387
game/icon.c

@@ -1,13 +1,15 @@
 #include "game/icon.h"
 
-// Icon entity description
-
 static void icon_collision(Entity *self, Entity *other, GameManager *manager, void *context)
 {
     UNUSED(manager);
     UNUSED(self);
-    IconContext *icon = (IconContext *)context;
-    UNUSED(icon);
+    IconContext *icon_ctx = (IconContext *)context;
+    if (!icon_ctx)
+    {
+        FURI_LOG_E("Game", "Icon context is NULL");
+        return;
+    }
 
     if (entity_description_get(other) == &player_desc)
     {
@@ -15,13 +17,12 @@ static void icon_collision(Entity *self, Entity *other, GameManager *manager, vo
         if (player)
         {
             Vector pos = entity_pos_get(other);
-
-            // Bounce the player back by 2 units opposite their last movement direction
+            // Bounce back by 2
             pos.x -= player->dx * 2;
             pos.y -= player->dy * 2;
             entity_pos_set(other, pos);
 
-            // Reset player's movement direction to prevent immediate re-collision
+            // Reset movement to prevent re-collision
             player->dx = 0;
             player->dy = 0;
         }
@@ -32,21 +33,80 @@ static void icon_render(Entity *self, GameManager *manager, Canvas *canvas, void
 {
     UNUSED(manager);
     IconContext *icon_ctx = (IconContext *)context;
+    if (!icon_ctx)
+    {
+        FURI_LOG_E("Game", "Icon context is NULL");
+        return;
+    }
     Vector pos = entity_pos_get(self);
-    canvas_draw_icon(canvas, pos.x - camera_x - icon_ctx->width / 2, pos.y - camera_y - icon_ctx->height / 2, icon_ctx->icon);
+
+    // Draw the icon, centered
+    canvas_draw_icon(
+        canvas,
+        pos.x - camera_x - icon_ctx->width / 2,
+        pos.y - camera_y - icon_ctx->height / 2,
+        icon_ctx->icon);
 }
 
 static void icon_start(Entity *self, GameManager *manager, void *context)
 {
     UNUSED(manager);
-    IconContext *icon_ctx = (IconContext *)context;
-    // Just add the collision rectangle for 16x16 icon
-    entity_collider_add_rect(self, icon_ctx->width + COLLISION_BOX_PADDING_HORIZONTAL, icon_ctx->height + COLLISION_BOX_PADDING_VERTICAL);
+
+    IconContext *icon_ctx_self = (IconContext *)context;
+    if (!icon_ctx_self)
+    {
+        FURI_LOG_E("Game", "Icon context self is NULL");
+        return;
+    }
+    IconContext *icon_ctx = entity_context_get(self);
+    if (!icon_ctx)
+    {
+        FURI_LOG_E("Game", "Icon context is NULL");
+        return;
+    }
+
+    IconContext *loaded_data = get_icon_context(g_temp_spawn_name);
+    if (!loaded_data)
+    {
+        FURI_LOG_E("Game", "Failed to find icon data for %s", g_temp_spawn_name);
+        return;
+    }
+
+    icon_ctx_self->icon = loaded_data->icon;
+    icon_ctx_self->width = loaded_data->width;
+    icon_ctx_self->height = loaded_data->height;
+    icon_ctx->icon = loaded_data->icon;
+    icon_ctx->width = loaded_data->width;
+    icon_ctx->height = loaded_data->height;
+
+    Vector pos = entity_pos_get(self);
+    pos.x += icon_ctx_self->width / 2;
+    pos.y += icon_ctx_self->height / 2;
+    entity_pos_set(self, pos);
+
+    entity_collider_add_circle(
+        self,
+        (icon_ctx_self->width + icon_ctx_self->height) / 4);
+
+    free(loaded_data);
+}
+
+// -------------- Stop callback --------------
+static void icon_free(Entity *self, GameManager *manager, void *context)
+{
+    UNUSED(self);
+    UNUSED(manager);
+    UNUSED(context);
+    if (context)
+    {
+        free(context);
+    }
 }
 
+// -------------- Entity description --------------
 const EntityDescription icon_desc = {
     .start = icon_start,
-    .stop = NULL,
+    .stop = icon_free,
     .update = NULL,
     .render = icon_render,
     .collision = icon_collision,
@@ -54,456 +114,282 @@ const EntityDescription icon_desc = {
     .context_size = sizeof(IconContext),
 };
 
-static IconContext _generic_icon = {
-    .icon = &I_icon_earth_15x16,
-    .width = 15,
-    .height = 16,
-};
-
-IconContext *get_icon_context(char *name)
+static IconContext *icon_generic_alloc(const char *id, const Icon *icon, uint8_t width, uint8_t height)
 {
-    if (strcmp(name, "earth") == 0)
-    {
-        _generic_icon.icon = &I_icon_earth_15x16;
-        _generic_icon.width = 15;
-        _generic_icon.height = 16;
-        return &_generic_icon;
-    }
-    if (strcmp(name, "home") == 0)
+    IconContext *ctx = malloc(sizeof(IconContext));
+    if (!ctx)
     {
-        _generic_icon.icon = &I_icon_home_15x16;
-        _generic_icon.width = 15;
-        _generic_icon.height = 16;
-        return &_generic_icon;
+        FURI_LOG_E("Game", "Failed to allocate IconContext");
+        return NULL;
     }
+    snprintf(ctx->id, sizeof(ctx->id), "%s", id);
+    ctx->icon = icon;
+    ctx->width = width;
+    ctx->height = height;
+    return ctx;
+}
+
+IconContext *get_icon_context(const char *name)
+{
+    // if (strcmp(name, "earth") == 0)
+    // {
+    //     return icon_generic_alloc("earth", &I_icon_earth_15x16, 15, 16);
+    // }
+    // else if (strcmp(name, "home") == 0)
+    // {
+    //     return icon_generic_alloc("home", &I_icon_home_15x16, 15, 16);
+    // }
     if (strcmp(name, "house") == 0)
     {
-        _generic_icon.icon = &I_icon_house_48x32px;
-        _generic_icon.width = 48;
-        _generic_icon.height = 32;
-        return &_generic_icon;
-    }
-    if (strcmp(name, "house_3d") == 0)
-    {
-        _generic_icon.icon = &I_icon_house_3d_34x45px;
-        _generic_icon.width = 34;
-        _generic_icon.height = 45;
-        return &_generic_icon;
-    }
-    if (strcmp(name, "info") == 0)
-    {
-        _generic_icon.icon = &I_icon_info_15x16;
-        _generic_icon.width = 15;
-        _generic_icon.height = 16;
-        return &_generic_icon;
-    }
-    if (strcmp(name, "man") == 0)
-    {
-        _generic_icon.icon = &I_icon_man_7x16;
-        _generic_icon.width = 7;
-        _generic_icon.height = 16;
-        return &_generic_icon;
-    }
-    if (strcmp(name, "plant") == 0)
-    {
-        _generic_icon.icon = &I_icon_plant_16x16;
-        _generic_icon.width = 16;
-        _generic_icon.height = 16;
-        return &_generic_icon;
-    }
-    if (strcmp(name, "plant_fern") == 0)
-    {
-        _generic_icon.icon = &I_icon_plant_fern_18x16px;
-        _generic_icon.width = 18;
-        _generic_icon.height = 16;
-        return &_generic_icon;
+        return icon_generic_alloc("house", &I_icon_house_48x32px, 48, 32);
     }
-    if (strcmp(name, "plant_pointy") == 0)
+    // else if (strcmp(name, "house_3d") == 0)
+    // {
+    //     return icon_generic_alloc("house_3d", &I_icon_house_3d_34x45px, 34, 45);
+    // }
+    // else if (strcmp(name, "info") == 0)
+    // {
+    //     return icon_generic_alloc("info", &I_icon_info_15x16, 15, 16);
+    // }
+    else if (strcmp(name, "man") == 0)
     {
-        _generic_icon.icon = &I_icon_plant_pointy_13x16px;
-        _generic_icon.width = 13;
-        _generic_icon.height = 16;
-        return &_generic_icon;
+        return icon_generic_alloc("man", &I_icon_man_7x16, 7, 16);
     }
-    if (strcmp(name, "tree") == 0)
+    else if (strcmp(name, "plant") == 0)
     {
-        _generic_icon.icon = &I_icon_tree_16x16;
-        _generic_icon.width = 16;
-        _generic_icon.height = 16;
-        return &_generic_icon;
+        return icon_generic_alloc("plant", &I_icon_plant_16x16, 16, 16);
     }
-    if (strcmp(name, "tree_29x30") == 0)
+    // else if (strcmp(name, "plant_fern") == 0)
+    // {
+    //     return icon_generic_alloc("plant_fern", &I_icon_plant_fern_18x16px, 18, 16);
+    // }
+    // else if (strcmp(name, "plant_pointy") == 0)
+    // {
+    //     return icon_generic_alloc("plant_pointy", &I_icon_plant_pointy_13x16px, 13, 16);
+    // }
+    else if (strcmp(name, "tree") == 0)
     {
-        _generic_icon.icon = &I_icon_tree_29x30px;
-        _generic_icon.width = 29;
-        _generic_icon.height = 30;
-        return &_generic_icon;
+        return icon_generic_alloc("tree", &I_icon_tree_16x16, 16, 16);
     }
-    if (strcmp(name, "tree_48x48") == 0)
+    // else if (strcmp(name, "tree_29x30") == 0)
+    // {
+    //     return icon_generic_alloc("tree_29x30", &I_icon_tree_29x30px, 29, 30);
+    // }
+    // else if (strcmp(name, "tree_48x48") == 0)
+    // {
+    //     return icon_generic_alloc("tree_48x48", &I_icon_tree_48x48px, 48, 48);
+    // }
+    else if (strcmp(name, "woman") == 0)
     {
-        _generic_icon.icon = &I_icon_tree_48x48px;
-        _generic_icon.width = 48;
-        _generic_icon.height = 48;
-        return &_generic_icon;
+        return icon_generic_alloc("woman", &I_icon_woman_9x16, 9, 16);
     }
-    if (strcmp(name, "woman") == 0)
+    // else if (strcmp(name, "chest_closed") == 0)
+    // {
+    //     return icon_generic_alloc("chest_closed", &I_icon_chest_closed_16x13px, 16, 13);
+    // }
+    // else if (strcmp(name, "chest_open") == 0)
+    // {
+    //     return icon_generic_alloc("chest_open", &I_icon_chest_open_16x16px, 16, 16);
+    // }
+    else if (strcmp(name, "fence") == 0)
     {
-        _generic_icon.icon = &I_icon_woman_9x16;
-        _generic_icon.width = 9;
-        _generic_icon.height = 16;
-        return &_generic_icon;
+        return icon_generic_alloc("fence", &I_icon_fence_16x8px, 16, 8);
     }
-    if (strcmp(name, "chest_closed") == 0)
+    else if (strcmp(name, "fence_end") == 0)
     {
-        _generic_icon.icon = &I_icon_chest_closed_16x13px;
-        _generic_icon.width = 16;
-        _generic_icon.height = 13;
-        return &_generic_icon;
+        return icon_generic_alloc("fence_end", &I_icon_fence_end_16x8px, 16, 8);
     }
-    if (strcmp(name, "chest_open") == 0)
+    // else if (strcmp(name, "fence_vertical_end") == 0)
+    // {
+    //     return icon_generic_alloc("fence_vertical_end", &I_icon_fence_vertical_end_6x8px, 6, 8);
+    // }
+    // else if (strcmp(name, "fence_vertical_start") == 0)
+    // {
+    //     return icon_generic_alloc("fence_vertical_start", &I_icon_fence_vertical_start_6x15px, 6, 15);
+    // }
+    else if (strcmp(name, "flower") == 0)
     {
-        _generic_icon.icon = &I_icon_chest_open_16x16px;
-        _generic_icon.width = 16;
-        _generic_icon.height = 16;
-        return &_generic_icon;
+        return icon_generic_alloc("flower", &I_icon_flower_16x16, 16, 16);
     }
-    if (strcmp(name, "fence") == 0)
+    else if (strcmp(name, "lake_bottom") == 0)
     {
-        _generic_icon.icon = &I_icon_fence_16x8px;
-        _generic_icon.width = 16;
-        _generic_icon.height = 8;
-        return &_generic_icon;
+        return icon_generic_alloc("lake_bottom", &I_icon_lake_bottom_31x12px, 31, 12);
     }
-    if (strcmp(name, "fence_end") == 0)
+    else if (strcmp(name, "lake_bottom_left") == 0)
     {
-        _generic_icon.icon = &I_icon_fence_end_16x8px;
-        _generic_icon.width = 16;
-        _generic_icon.height = 8;
-        return &_generic_icon;
+        return icon_generic_alloc("lake_bottom_left", &I_icon_lake_bottom_left_24x22px, 24, 22);
     }
-    if (strcmp(name, "fence_vertical_end") == 0)
+    else if (strcmp(name, "lake_bottom_right") == 0)
     {
-        _generic_icon.icon = &I_icon_fence_vertical_end_6x8px;
-        _generic_icon.width = 6;
-        _generic_icon.height = 8;
-        return &_generic_icon;
+        return icon_generic_alloc("lake_bottom_right", &I_icon_lake_bottom_right_24x22px, 24, 22);
     }
-    if (strcmp(name, "fence_vertical_start") == 0)
+    else if (strcmp(name, "lake_left") == 0)
     {
-        _generic_icon.icon = &I_icon_fence_vertical_start_6x15px;
-        _generic_icon.width = 6;
-        _generic_icon.height = 15;
-        return &_generic_icon;
+        return icon_generic_alloc("lake_left", &I_icon_lake_left_11x31px, 11, 31);
     }
-    if (strcmp(name, "flower") == 0)
+    else if (strcmp(name, "lake_right") == 0)
     {
-        _generic_icon.icon = &I_icon_flower_16x16;
-        _generic_icon.width = 16;
-        _generic_icon.height = 16;
-        return &_generic_icon;
+        // Assuming 11x31
+        return icon_generic_alloc("lake_right", &I_icon_lake_right_11x31, 11, 31);
     }
-    if (strcmp(name, "lake_bottom") == 0)
+    else if (strcmp(name, "lake_top") == 0)
     {
-        _generic_icon.icon = &I_icon_lake_bottom_31x12px;
-        _generic_icon.width = 31;
-        _generic_icon.height = 12;
-        return &_generic_icon;
+        return icon_generic_alloc("lake_top", &I_icon_lake_top_31x12px, 31, 12);
     }
-    if (strcmp(name, "lake_bottom_left") == 0)
+    else if (strcmp(name, "lake_top_left") == 0)
     {
-        _generic_icon.icon = &I_icon_lake_bottom_left_24x22px;
-        _generic_icon.width = 24;
-        _generic_icon.height = 22;
-        return &_generic_icon;
+        return icon_generic_alloc("lake_top_left", &I_icon_lake_top_left_24x22px, 24, 22);
     }
-    if (strcmp(name, "lake_bottom_right") == 0)
+    else if (strcmp(name, "lake_top_right") == 0)
     {
-        _generic_icon.icon = &I_icon_lake_bottom_right_24x22px;
-        _generic_icon.width = 24;
-        _generic_icon.height = 22;
-        return &_generic_icon;
-    }
-    if (strcmp(name, "lake_left") == 0)
-    {
-        _generic_icon.icon = &I_icon_lake_left_11x31px;
-        _generic_icon.width = 11;
-        _generic_icon.height = 31;
-        return &_generic_icon;
-    }
-    if (strcmp(name, "lake_right") == 0)
-    {
-        _generic_icon.icon = &I_icon_lake_right_11x31; // Assuming it's 11x31
-        _generic_icon.width = 11;
-        _generic_icon.height = 31;
-        return &_generic_icon;
-    }
-    if (strcmp(name, "lake_top") == 0)
-    {
-        _generic_icon.icon = &I_icon_lake_top_31x12px;
-        _generic_icon.width = 31;
-        _generic_icon.height = 12;
-        return &_generic_icon;
-    }
-    if (strcmp(name, "lake_top_left") == 0)
-    {
-        _generic_icon.icon = &I_icon_lake_top_left_24x22px;
-        _generic_icon.width = 24;
-        _generic_icon.height = 22;
-        return &_generic_icon;
-    }
-    if (strcmp(name, "lake_top_right") == 0)
-    {
-        _generic_icon.icon = &I_icon_lake_top_right_24x22px;
-        _generic_icon.width = 24;
-        _generic_icon.height = 22;
-        return &_generic_icon;
-    }
-    if (strcmp(name, "rock_large") == 0)
-    {
-        _generic_icon.icon = &I_icon_rock_large_18x19px;
-        _generic_icon.width = 18;
-        _generic_icon.height = 19;
-        return &_generic_icon;
-    }
-    if (strcmp(name, "rock_medium") == 0)
-    {
-        _generic_icon.icon = &I_icon_rock_medium_16x14px;
-        _generic_icon.width = 16;
-        _generic_icon.height = 14;
-        return &_generic_icon;
-    }
-    if (strcmp(name, "rock_small") == 0)
-    {
-        _generic_icon.icon = &I_icon_rock_small_10x8px;
-        _generic_icon.width = 10;
-        _generic_icon.height = 8;
-        return &_generic_icon;
+        return icon_generic_alloc("lake_top_right", &I_icon_lake_top_right_24x22px, 24, 22);
     }
+    // else if (strcmp(name, "rock_large") == 0)
+    // {
+    //     return icon_generic_alloc("rock_large", &I_icon_rock_large_18x19px, 18, 19);
+    // }
+    // else if (strcmp(name, "rock_medium") == 0)
+    // {
+    //     return icon_generic_alloc("rock_medium", &I_icon_rock_medium_16x14px, 16, 14);
+    // }
+    // else if (strcmp(name, "rock_small") == 0)
+    // {
+    //     return icon_generic_alloc("rock_small", &I_icon_rock_small_10x8px, 10, 8);
+    // }
 
     // If no match is found
+    FURI_LOG_E("Game", "Icon not found: %s", name);
     return NULL;
 }
 
-IconContext *get_icon_context_furi(FuriString *name)
+const char *icon_get_id(const Icon *icon)
 {
-    if (furi_string_cmp(name, "earth") == 0)
-    {
-        _generic_icon.icon = &I_icon_earth_15x16;
-        _generic_icon.width = 15;
-        _generic_icon.height = 16;
-        return &_generic_icon;
-    }
-    if (furi_string_cmp(name, "home") == 0)
-    {
-        _generic_icon.icon = &I_icon_home_15x16;
-        _generic_icon.width = 15;
-        _generic_icon.height = 16;
-        return &_generic_icon;
-    }
-    if (furi_string_cmp(name, "house") == 0)
-    {
-        _generic_icon.icon = &I_icon_house_48x32px;
-        _generic_icon.width = 48;
-        _generic_icon.height = 32;
-        return &_generic_icon;
-    }
-    if (furi_string_cmp(name, "house_3d") == 0)
-    {
-        _generic_icon.icon = &I_icon_house_3d_34x45px;
-        _generic_icon.width = 34;
-        _generic_icon.height = 45;
-        return &_generic_icon;
-    }
-    if (furi_string_cmp(name, "info") == 0)
-    {
-        _generic_icon.icon = &I_icon_info_15x16;
-        _generic_icon.width = 15;
-        _generic_icon.height = 16;
-        return &_generic_icon;
-    }
-    if (furi_string_cmp(name, "man") == 0)
-    {
-        _generic_icon.icon = &I_icon_man_7x16;
-        _generic_icon.width = 7;
-        _generic_icon.height = 16;
-        return &_generic_icon;
-    }
-    if (furi_string_cmp(name, "plant") == 0)
-    {
-        _generic_icon.icon = &I_icon_plant_16x16;
-        _generic_icon.width = 16;
-        _generic_icon.height = 16;
-        return &_generic_icon;
-    }
-    if (furi_string_cmp(name, "plant_fern") == 0)
-    {
-        _generic_icon.icon = &I_icon_plant_fern_18x16px;
-        _generic_icon.width = 18;
-        _generic_icon.height = 16;
-        return &_generic_icon;
-    }
-    if (furi_string_cmp(name, "plant_pointy") == 0)
-    {
-        _generic_icon.icon = &I_icon_plant_pointy_13x16px;
-        _generic_icon.width = 13;
-        _generic_icon.height = 16;
-        return &_generic_icon;
-    }
-    if (furi_string_cmp(name, "tree") == 0)
-    {
-        _generic_icon.icon = &I_icon_tree_16x16;
-        _generic_icon.width = 16;
-        _generic_icon.height = 16;
-        return &_generic_icon;
-    }
-    if (furi_string_cmp(name, "tree_29x30") == 0)
-    {
-        _generic_icon.icon = &I_icon_tree_29x30px;
-        _generic_icon.width = 29;
-        _generic_icon.height = 30;
-        return &_generic_icon;
-    }
-    if (furi_string_cmp(name, "tree_48x48") == 0)
-    {
-        _generic_icon.icon = &I_icon_tree_48x48px;
-        _generic_icon.width = 48;
-        _generic_icon.height = 48;
-        return &_generic_icon;
-    }
-    if (furi_string_cmp(name, "woman") == 0)
-    {
-        _generic_icon.icon = &I_icon_woman_9x16;
-        _generic_icon.width = 9;
-        _generic_icon.height = 16;
-        return &_generic_icon;
-    }
-    if (furi_string_cmp(name, "chest_closed") == 0)
-    {
-        _generic_icon.icon = &I_icon_chest_closed_16x13px;
-        _generic_icon.width = 16;
-        _generic_icon.height = 13;
-        return &_generic_icon;
-    }
-    if (furi_string_cmp(name, "chest_open") == 0)
-    {
-        _generic_icon.icon = &I_icon_chest_open_16x16px;
-        _generic_icon.width = 16;
-        _generic_icon.height = 16;
-        return &_generic_icon;
-    }
-    if (furi_string_cmp(name, "fence") == 0)
+    // if (icon == &I_icon_earth_15x16)
+    // {
+    //     return "earth";
+    // }
+    // else if (icon == &I_icon_home_15x16)
+    // {
+    //     return "home";
+    // }
+    if (icon == &I_icon_house_48x32px)
     {
-        _generic_icon.icon = &I_icon_fence_16x8px;
-        _generic_icon.width = 16;
-        _generic_icon.height = 8;
-        return &_generic_icon;
+        return "house";
     }
-    if (furi_string_cmp(name, "fence_end") == 0)
+    // else if (icon == &I_icon_house_3d_34x45px)
+    // {
+    //     return "house_3d";
+    // }
+    // else if (icon == &I_icon_info_15x16)
+    // {
+    //     return "info";
+    // }
+    else if (icon == &I_icon_man_7x16)
     {
-        _generic_icon.icon = &I_icon_fence_end_16x8px;
-        _generic_icon.width = 16;
-        _generic_icon.height = 8;
-        return &_generic_icon;
+        return "man";
     }
-    if (furi_string_cmp(name, "fence_vertical_end") == 0)
+    else if (icon == &I_icon_plant_16x16)
     {
-        _generic_icon.icon = &I_icon_fence_vertical_end_6x8px;
-        _generic_icon.width = 6;
-        _generic_icon.height = 8;
-        return &_generic_icon;
+        return "plant";
     }
-    if (furi_string_cmp(name, "fence_vertical_start") == 0)
+    // else if (icon == &I_icon_plant_fern_18x16px)
+    // {
+    //     return "plant_fern";
+    // }
+    // else if (icon == &I_icon_plant_pointy_13x16px)
+    // {
+    //     return "plant_pointy";
+    // }
+    else if (icon == &I_icon_tree_16x16)
     {
-        _generic_icon.icon = &I_icon_fence_vertical_start_6x15px;
-        _generic_icon.width = 6;
-        _generic_icon.height = 15;
-        return &_generic_icon;
+        return "tree";
     }
-    if (furi_string_cmp(name, "flower") == 0)
+    // else if (icon == &I_icon_tree_29x30px)
+    // {
+    //     return "tree_29x30";
+    // }
+    // else if (icon == &I_icon_tree_48x48px)
+    // {
+    //     return "tree_48x48";
+    // }
+    else if (icon == &I_icon_woman_9x16)
     {
-        _generic_icon.icon = &I_icon_flower_16x16;
-        _generic_icon.width = 16;
-        _generic_icon.height = 16;
-        return &_generic_icon;
+        return "woman";
     }
-    if (furi_string_cmp(name, "lake_bottom") == 0)
+    // else if (icon == &I_icon_chest_closed_16x13px)
+    // {
+    //     return "chest_closed";
+    // }
+    // else if (icon == &I_icon_chest_open_16x16px)
+    // {
+    //     return "chest_open";
+    // }
+    else if (icon == &I_icon_fence_16x8px)
     {
-        _generic_icon.icon = &I_icon_lake_bottom_31x12px;
-        _generic_icon.width = 31;
-        _generic_icon.height = 12;
-        return &_generic_icon;
+        return "fence";
     }
-    if (furi_string_cmp(name, "lake_bottom_left") == 0)
+    else if (icon == &I_icon_fence_end_16x8px)
     {
-        _generic_icon.icon = &I_icon_lake_bottom_left_24x22px;
-        _generic_icon.width = 24;
-        _generic_icon.height = 22;
-        return &_generic_icon;
+        return "fence_end";
     }
-    if (furi_string_cmp(name, "lake_bottom_right") == 0)
+    // else if (icon == &I_icon_fence_vertical_end_6x8px)
+    // {
+    //     return "fence_vertical_end";
+    // }
+    // else if (icon == &I_icon_fence_vertical_start_6x15px)
+    // {
+    //     return "fence_vertical_start";
+    // }
+    else if (icon == &I_icon_flower_16x16)
     {
-        _generic_icon.icon = &I_icon_lake_bottom_right_24x22px;
-        _generic_icon.width = 24;
-        _generic_icon.height = 22;
-        return &_generic_icon;
+        return "flower";
     }
-    if (furi_string_cmp(name, "lake_left") == 0)
+    else if (icon == &I_icon_lake_bottom_31x12px)
     {
-        _generic_icon.icon = &I_icon_lake_left_11x31px;
-        _generic_icon.width = 11;
-        _generic_icon.height = 31;
-        return &_generic_icon;
+        return "lake_bottom";
     }
-    if (furi_string_cmp(name, "lake_right") == 0)
+    else if (icon == &I_icon_lake_bottom_left_24x22px)
     {
-        _generic_icon.icon = &I_icon_lake_right_11x31; // Assuming dimensions
-        _generic_icon.width = 11;
-        _generic_icon.height = 31;
-        return &_generic_icon;
+        return "lake_bottom_left";
     }
-    if (furi_string_cmp(name, "lake_top") == 0)
+    else if (icon == &I_icon_lake_bottom_right_24x22px)
     {
-        _generic_icon.icon = &I_icon_lake_top_31x12px;
-        _generic_icon.width = 31;
-        _generic_icon.height = 12;
-        return &_generic_icon;
+        return "lake_bottom_right";
     }
-    if (furi_string_cmp(name, "lake_top_left") == 0)
+    else if (icon == &I_icon_lake_left_11x31px)
     {
-        _generic_icon.icon = &I_icon_lake_top_left_24x22px;
-        _generic_icon.width = 24;
-        _generic_icon.height = 22;
-        return &_generic_icon;
+        return "lake_left";
     }
-    if (furi_string_cmp(name, "lake_top_right") == 0)
+    else if (icon == &I_icon_lake_right_11x31)
     {
-        _generic_icon.icon = &I_icon_lake_top_right_24x22px;
-        _generic_icon.width = 24;
-        _generic_icon.height = 22;
-        return &_generic_icon;
+        return "lake_right";
     }
-    if (furi_string_cmp(name, "rock_large") == 0)
+    else if (icon == &I_icon_lake_top_31x12px)
     {
-        _generic_icon.icon = &I_icon_rock_large_18x19px;
-        _generic_icon.width = 18;
-        _generic_icon.height = 19;
-        return &_generic_icon;
+        return "lake_top";
     }
-    if (furi_string_cmp(name, "rock_medium") == 0)
+    else if (icon == &I_icon_lake_top_left_24x22px)
     {
-        _generic_icon.icon = &I_icon_rock_medium_16x14px;
-        _generic_icon.width = 16;
-        _generic_icon.height = 14;
-        return &_generic_icon;
+        return "lake_top_left";
     }
-    if (furi_string_cmp(name, "rock_small") == 0)
+    else if (icon == &I_icon_lake_top_right_24x22px)
     {
-        _generic_icon.icon = &I_icon_rock_small_10x8px;
-        _generic_icon.width = 10;
-        _generic_icon.height = 8;
-        return &_generic_icon;
+        return "lake_top_right";
     }
+    // else if (icon == &I_icon_rock_large_18x19px)
+    // {
+    //     return "rock_large";
+    // }
+    // else if (icon == &I_icon_rock_medium_16x14px)
+    // {
+    //     return "rock_medium";
+    // }
+    // else if (icon == &I_icon_rock_small_10x8px)
+    // {
+    //     return "rock_small";
+    // }
 
     // If no match is found
+    FURI_LOG_E("Game", "Icon ID not found for given icon pointer.");
     return NULL;
 }

+ 3 - 4
game/icon.h

@@ -1,16 +1,15 @@
 #pragma once
 #include "flip_world_icons.h"
 #include "game.h"
-#define COLLISION_BOX_PADDING_HORIZONTAL 10
-#define COLLISION_BOX_PADDING_VERTICAL 12
 
 typedef struct
 {
+    char id[32];
     const Icon *icon;
     uint8_t width;
     uint8_t height;
 } IconContext;
 
 extern const EntityDescription icon_desc;
-IconContext *get_icon_context(char *name);
-IconContext *get_icon_context_furi(FuriString *name);
+IconContext *get_icon_context(const char *name);
+const char *icon_get_id(const Icon *icon);

+ 119 - 31
game/level.c

@@ -1,17 +1,117 @@
 #include <game/level.h>
+#include <flip_storage/storage.h>
+#include <game/storage.h>
+bool allocate_level(GameManager *manager, int index)
+{
+    GameContext *game_context = game_manager_game_context_get(manager);
+
+    // open the world list from storage, then create a level for each world
+    char file_path[128];
+    snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/world_list.json");
+    FuriString *world_list = flipper_http_load_from_file(file_path);
+    if (!world_list)
+    {
+        FURI_LOG_E("Game", "Failed to load world list");
+        game_context->levels[0] = game_manager_add_level(manager, generic_level("town_world_v2", 0));
+        game_context->level_count = 1;
+        return false;
+    }
+    FuriString *world_name = get_json_array_value_furi("worlds", index, world_list);
+    if (!world_name)
+    {
+        FURI_LOG_E("Game", "Failed to get world name");
+        furi_string_free(world_list);
+        return false;
+    }
+    game_context->levels[game_context->current_level] = game_manager_add_level(manager, generic_level(furi_string_get_cstr(world_name), index));
+    furi_string_free(world_name);
+    furi_string_free(world_list);
+    return true;
+}
+static void set_world(Level *level, GameManager *manager, char *id)
+{
+    char file_path[256];
+    snprintf(file_path, sizeof(file_path),
+             STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s/%s_json_data.json",
+             id, id);
+
+    FuriString *json_data_str = flipper_http_load_from_file(file_path);
+    if (!json_data_str || furi_string_empty(json_data_str))
+    {
+        FURI_LOG_E("Game", "Failed to load json data from file");
+        draw_town_world(level);
+        return;
+    }
+
+    if (!is_enough_heap(28400))
+    {
+        FURI_LOG_E("Game", "Not enough heap memory.. ending game early.");
+        GameContext *game_context = game_manager_game_context_get(manager);
+        game_context->ended_early = true;
+        game_manager_game_stop(manager); // end game early
+        furi_string_free(json_data_str);
+        return;
+    }
+
+    FURI_LOG_I("Game", "Drawing world");
+    if (!draw_json_world_furi(level, json_data_str))
+    {
+        FURI_LOG_E("Game", "Failed to draw world");
+        draw_town_world(level);
+        furi_string_free(json_data_str);
+    }
+    else
+    {
+        FURI_LOG_I("Game", "Drawing enemies");
+        furi_string_free(json_data_str);
+        snprintf(file_path, sizeof(file_path),
+                 STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s/%s_enemy_data.json",
+                 id, id);
+
+        FuriString *enemy_data_str = flipper_http_load_from_file(file_path);
+        if (!enemy_data_str || furi_string_empty(enemy_data_str))
+        {
+            FURI_LOG_E("Game", "Failed to get enemy data");
+            draw_town_world(level);
+            return;
+        }
 
+        // Loop through the array
+        for (int i = 0; i < MAX_ENEMIES; i++)
+        {
+            FuriString *single_enemy_data = get_json_array_value_furi("enemy_data", i, enemy_data_str);
+            if (!single_enemy_data || furi_string_empty(single_enemy_data))
+            {
+                // No more enemy elements found
+                if (single_enemy_data)
+                    furi_string_free(single_enemy_data);
+                break;
+            }
+
+            spawn_enemy_json_furi(level, manager, single_enemy_data);
+            furi_string_free(single_enemy_data);
+        }
+        furi_string_free(enemy_data_str);
+        FURI_LOG_I("Game", "Finished loading world data");
+    }
+}
 static void level_start(Level *level, GameManager *manager, void *context)
 {
-    UNUSED(manager);
-    if (!level || !context)
+    if (!level || !context || !manager)
     {
-        FURI_LOG_E("Game", "Level or context is NULL");
+        FURI_LOG_E("Game", "Level, context, or manager is NULL");
         return;
     }
 
     level_clear(level);
     player_spawn(level, manager);
+
     LevelContext *level_context = context;
+    if (!level_context)
+    {
+        FURI_LOG_E("Game", "Level context is NULL");
+        return;
+    }
 
     // check if the world exists
     if (!world_exists(level_context->id))
@@ -21,39 +121,21 @@ static void level_start(Level *level, GameManager *manager, void *context)
         if (!world_data)
         {
             FURI_LOG_E("Game", "Failed to fetch world data");
-            draw_tree_world(level);
+            draw_town_world(level);
             return;
         }
-
-        if (!draw_json_world(level, furi_string_get_cstr(world_data)))
-        {
-            FURI_LOG_E("Game", "Failed to draw world");
-            draw_tree_world(level);
-        }
-
-        // world_data is guaranteed non-NULL here
         furi_string_free(world_data);
-        return;
-    }
 
-    // get the world data
-    FuriString *world_data = load_furi_world(level_context->id);
-    if (!world_data)
-    {
-        FURI_LOG_E("Game", "Failed to load world data");
-        draw_tree_world(level);
-        return;
-    }
+        set_world(level, manager, level_context->id);
 
-    // draw the world
-    if (!draw_json_world(level, furi_string_get_cstr(world_data)))
+        FURI_LOG_I("Game", "World set.");
+    }
+    else
     {
-        FURI_LOG_E("Game", "World exists but failed to draw.");
-        draw_tree_world(level);
+        FURI_LOG_I("Game", "World exists.. loading now");
+        set_world(level, manager, level_context->id);
+        FURI_LOG_I("Game", "World set.");
     }
-
-    // world_data is guaranteed non-NULL here
-    furi_string_free(world_data);
 }
 
 static LevelContext *level_context_generic;
@@ -62,6 +144,12 @@ static LevelContext *level_generic_alloc(const char *id, int index)
 {
     if (level_context_generic == NULL)
     {
+        size_t heap_size = memmgr_get_free_heap();
+        if (heap_size < sizeof(LevelContext))
+        {
+            FURI_LOG_E("Game", "Not enough heap to allocate level context");
+            return NULL;
+        }
         level_context_generic = malloc(sizeof(LevelContext));
     }
     snprintf(level_context_generic->id, sizeof(level_context_generic->id), "%s", id);
@@ -78,7 +166,7 @@ static void level_generic_free()
     }
 }
 
-static void level_free(Level *level, GameManager *manager, void *context)
+static void free_level(Level *level, GameManager *manager, void *context)
 {
     UNUSED(level);
     UNUSED(manager);
@@ -107,7 +195,7 @@ static void level_alloc_generic_world(Level *level, GameManager *manager, void *
 
 const LevelBehaviour _generic_level = {
     .alloc = level_alloc_generic_world,
-    .free = level_free,
+    .free = free_level,
     .start = level_start,
     .stop = NULL,
     .context_size = sizeof(LevelContext),

+ 1 - 0
game/level.h

@@ -8,3 +8,4 @@ typedef struct
 } LevelContext;
 
 const LevelBehaviour *generic_level(const char *id, int index);
+bool allocate_level(GameManager *manager, int index);

+ 395 - 0
game/player.c

@@ -0,0 +1,395 @@
+#include <game/player.h>
+#include <game/storage.h>
+/****** Entities: Player ******/
+static Level *get_next_level(GameManager *manager)
+{
+    GameContext *game_context = game_manager_game_context_get(manager);
+    if (!game_context)
+    {
+        FURI_LOG_E(TAG, "Failed to get game context");
+        return NULL;
+    }
+    game_context->current_level = game_context->current_level == 0 ? 1 : 0;
+    if (!game_context->levels[game_context->current_level])
+    {
+        if (!allocate_level(manager, game_context->current_level))
+        {
+            FURI_LOG_E(TAG, "Failed to allocate level %d", game_context->current_level);
+            return NULL;
+        }
+    }
+    return game_context->levels[game_context->current_level];
+}
+
+void player_spawn(Level *level, GameManager *manager)
+{
+    if (!level || !manager)
+    {
+        FURI_LOG_E(TAG, "Invalid arguments to player_spawn");
+        return;
+    }
+
+    GameContext *game_context = game_manager_game_context_get(manager);
+    if (!game_context)
+    {
+        FURI_LOG_E(TAG, "Failed to get game context");
+        return;
+    }
+
+    game_context->player = level_add_entity(level, &player_desc);
+    if (!game_context->player)
+    {
+        FURI_LOG_E(TAG, "Failed to add player entity to level");
+        return;
+    }
+
+    // Set player position.
+    entity_pos_set(game_context->player, (Vector){WORLD_WIDTH / 2, WORLD_HEIGHT / 2});
+
+    // Box is centered in player x and y, and its size
+    entity_collider_add_rect(game_context->player, 13, 11);
+
+    // Get player context
+    PlayerContext *player_context = entity_context_get(game_context->player);
+    if (!player_context)
+    {
+        FURI_LOG_E(TAG, "Failed to get player context");
+        return;
+    }
+
+    // player context must be set each level or NULL pointer will be dereferenced
+    if (!load_player_context(player_context))
+    {
+        FURI_LOG_E(TAG, "Loading player context failed. Initializing default values.");
+
+        // Initialize default player context
+        player_context->sprite_right = game_manager_sprite_load(manager, "player_right_sword_15x11px.fxbm");
+        player_context->sprite_left = game_manager_sprite_load(manager, "player_left_sword_15x11px.fxbm");
+        player_context->direction = PLAYER_RIGHT; // default direction
+        player_context->health = 100;
+        player_context->strength = 10;
+        player_context->level = 1;
+        player_context->xp = 0;
+        player_context->start_position = entity_pos_get(game_context->player);
+        player_context->attack_timer = 0.1f;
+        player_context->elapsed_attack_timer = player_context->attack_timer;
+        player_context->health_regen = 1; // 1 health per second
+        player_context->elapsed_health_regen = 0;
+        player_context->max_health = 100 + ((player_context->level - 1) * 10); // 10 health per level
+
+        // Set player username
+        if (!load_char("Flip-Social-Username", player_context->username, sizeof(player_context->username)))
+        {
+            // If loading username fails, default to "Player"
+            snprintf(player_context->username, sizeof(player_context->username), "Player");
+        }
+
+        game_context->player_context = player_context;
+
+        // Save the initialized context
+        if (!save_player_context(player_context))
+        {
+            FURI_LOG_E(TAG, "Failed to save player context after initialization");
+        }
+
+        return;
+    }
+
+    // Load player sprite (we'll add this to the JSON later when players can choose their sprite)
+    player_context->sprite_right = game_manager_sprite_load(manager, "player_right_sword_15x11px.fxbm");
+    player_context->sprite_left = game_manager_sprite_load(manager, "player_left_sword_15x11px.fxbm");
+
+    player_context->start_position = entity_pos_get(game_context->player);
+
+    // Update player stats based on XP using iterative method
+    // Function to get the current level based on XP iteratively
+    int get_player_level_iterative(uint32_t xp)
+    {
+        int level = 1;
+        uint32_t xp_required = 100; // Base XP for level 2
+
+        while (level < 100 && xp >= xp_required) // Maximum level supported
+        {
+            level++;
+            xp_required = (uint32_t)(xp_required * 1.5); // 1.5 growth factor per level
+        }
+
+        return level;
+    }
+
+    // Determine the player's level based on XP
+    player_context->level = get_player_level_iterative(player_context->xp);
+
+    // Update strength and max health based on the new level
+    player_context->strength = 10 + (player_context->level * 1);           // 1 strength per level
+    player_context->max_health = 100 + ((player_context->level - 1) * 10); // 10 health per level
+
+    // Assign loaded player context to game context
+    game_context->player_context = player_context;
+}
+
+// code from Derek Jamison
+// eventually we'll add dynamic positioning based on how much pitch/roll is detected
+// instead of assigning a fixed value
+static int player_x_from_pitch(float pitch)
+{
+    if (pitch > 6.0)
+    {
+        return 1;
+    }
+    else if (pitch < -8.0)
+    {
+        return -1;
+    }
+    return 0;
+}
+
+static int player_y_from_roll(float roll)
+{
+    if (roll > 9.0)
+    {
+        return 1;
+    }
+    else if (roll < -20.0)
+    {
+        return -1;
+    }
+    return 0;
+}
+
+static void player_update(Entity *self, GameManager *manager, void *context)
+{
+    if (!self || !manager || !context)
+        return;
+
+    PlayerContext *player = (PlayerContext *)context;
+    InputState input = game_manager_input_get(manager);
+    Vector pos = entity_pos_get(self);
+    GameContext *game_context = game_manager_game_context_get(manager);
+
+    // Store previous direction
+    int prev_dx = player->dx;
+    int prev_dy = player->dy;
+
+    // Reset movement deltas each frame
+    player->dx = 0;
+    player->dy = 0;
+
+    if (game_context->imu_present)
+    {
+        player->dx = player_x_from_pitch(-imu_pitch_get(game_context->imu));
+        player->dy = player_y_from_roll(-imu_roll_get(game_context->imu));
+
+        switch (player->dx)
+        {
+        case -1:
+            player->direction = PLAYER_LEFT;
+            pos.x -= 1;
+            break;
+        case 1:
+            player->direction = PLAYER_RIGHT;
+            pos.x += 1;
+            break;
+        default:
+            break;
+        }
+
+        switch (player->dy)
+        {
+        case -1:
+            player->direction = PLAYER_UP;
+            pos.y -= 1;
+            break;
+        case 1:
+            player->direction = PLAYER_DOWN;
+            pos.y += 1;
+            break;
+        default:
+            break;
+        }
+    }
+
+    // Apply health regeneration
+    player->elapsed_health_regen += 1.0f / game_context->fps;
+    if (player->elapsed_health_regen >= 1.0f && player->health < player->max_health)
+    {
+        player->health += (player->health_regen + player->health > player->max_health)
+                              ? (player->max_health - player->health)
+                              : player->health_regen;
+        player->elapsed_health_regen = 0;
+    }
+
+    // Increment the elapsed_attack_timer for the player
+    player->elapsed_attack_timer += 1.0f / game_context->fps;
+
+    // Handle movement input
+    if (input.held & GameKeyUp)
+    {
+        pos.y -= 2;
+        player->dy = -1;
+        player->direction = PLAYER_UP;
+        game_context->user_input = GameKeyUp;
+    }
+    if (input.held & GameKeyDown)
+    {
+        pos.y += 2;
+        player->dy = 1;
+        player->direction = PLAYER_DOWN;
+        game_context->user_input = GameKeyDown;
+    }
+    if (input.held & GameKeyLeft)
+    {
+        pos.x -= 2;
+        player->dx = -1;
+        player->direction = PLAYER_LEFT;
+        game_context->user_input = GameKeyLeft;
+    }
+    if (input.held & GameKeyRight)
+    {
+        pos.x += 2;
+        player->dx = 1;
+        player->direction = PLAYER_RIGHT;
+        game_context->user_input = GameKeyRight;
+    }
+
+    // Clamp the player's position to stay within world bounds
+    pos.x = CLAMP(pos.x, WORLD_WIDTH - 5, 5);
+    pos.y = CLAMP(pos.y, WORLD_HEIGHT - 5, 5);
+
+    // Update player position
+    entity_pos_set(self, pos);
+
+    // switch levels if holding OK
+    if (input.pressed & GameKeyOk)
+    {
+        FURI_LOG_I(TAG, "Player is pressing OK");
+        // if all enemies are dead, allow the "OK" button to switch levels
+        // otherwise the "OK" button will be used to attack
+        if (game_context->enemy_count == 0)
+        {
+            FURI_LOG_I(TAG, "Switching levels");
+            save_player_context(player);
+            game_manager_next_level_set(manager, get_next_level(manager));
+            furi_delay_ms(500);
+        }
+        else
+        {
+            FURI_LOG_I(TAG, "Player is attacking");
+            game_context->user_input = GameKeyOk;
+            // furi_delay_ms(100);
+        }
+        FURI_LOG_I(TAG, "Player is done pressing OK");
+    }
+
+    // If the player is not moving, retain the last movement direction
+    if (player->dx == 0 && player->dy == 0)
+    {
+        player->dx = prev_dx;
+        player->dy = prev_dy;
+        player->state = PLAYER_IDLE;
+        game_context->user_input = -1; // reset user input
+    }
+    else
+    {
+        player->state = PLAYER_MOVING;
+    }
+
+    // Handle back button to stop the game
+    if (input.pressed & GameKeyBack)
+    {
+        game_manager_game_stop(manager);
+    }
+}
+
+static void player_render(Entity *self, GameManager *manager, Canvas *canvas, void *context)
+{
+    UNUSED(manager);
+    if (!self || !context || !canvas)
+        return;
+    // Get player context
+    PlayerContext *player = context;
+
+    // Get player position
+    Vector pos = entity_pos_get(self);
+
+    // Draw background (updates camera_x and camera_y)
+    draw_background(canvas, pos);
+
+    // Draw player sprite relative to camera, centered on the player's position
+    canvas_draw_sprite(
+        canvas,
+        player->direction == PLAYER_RIGHT ? player->sprite_right : player->sprite_left,
+        pos.x - camera_x - 5, // Center the sprite horizontally
+        pos.y - camera_y - 5  // Center the sprite vertically
+    );
+}
+
+const EntityDescription player_desc = {
+    .start = NULL,                         // called when entity is added to the level
+    .stop = NULL,                          // called when entity is removed from the level
+    .update = player_update,               // called every frame
+    .render = player_render,               // called every frame, after update
+    .collision = NULL,                     // called when entity collides with another entity
+    .event = NULL,                         // called when entity receives an event
+    .context_size = sizeof(PlayerContext), // size of entity context, will be automatically allocated and freed
+};
+
+static SpriteContext *sprite_generic_alloc(const char *id, bool is_enemy, uint8_t width, uint8_t height)
+{
+    SpriteContext *ctx = malloc(sizeof(SpriteContext));
+    if (!ctx)
+    {
+        FURI_LOG_E("Game", "Failed to allocate SpriteContext");
+        return NULL;
+    }
+    snprintf(ctx->id, sizeof(ctx->id), "%s", id);
+    ctx->width = width;
+    ctx->height = height;
+    if (!is_enemy)
+    {
+        snprintf(ctx->right_file_name, sizeof(ctx->right_file_name), "player_right_%s_%dx%dpx.fxbm", id, width, height);
+        snprintf(ctx->left_file_name, sizeof(ctx->left_file_name), "player_left_%s_%dx%dpx.fxbm", id, width, height);
+    }
+    else
+    {
+        snprintf(ctx->right_file_name, sizeof(ctx->right_file_name), "enemy_right_%s_%dx%dpx.fxbm", id, width, height);
+        snprintf(ctx->left_file_name, sizeof(ctx->left_file_name), "enemy_left_%s_%dx%dpx.fxbm", id, width, height);
+    }
+    return ctx;
+}
+
+SpriteContext *get_sprite_context(const char *name)
+{
+    if (strcmp(name, "axe") == 0)
+    {
+        return sprite_generic_alloc("axe", false, 15, 11);
+    }
+    else if (strcmp(name, "bow") == 0)
+    {
+        return sprite_generic_alloc("bow", false, 13, 11);
+    }
+    else if (strcmp(name, "naked") == 0)
+    {
+        return sprite_generic_alloc("naked", false, 10, 10);
+    }
+    else if (strcmp(name, "sword") == 0)
+    {
+        return sprite_generic_alloc("sword", false, 15, 11);
+    }
+    else if (strcmp(name, "cyclops") == 0)
+    {
+        return sprite_generic_alloc("cyclops", true, 10, 11);
+    }
+    else if (strcmp(name, "ghost") == 0)
+    {
+        return sprite_generic_alloc("ghost", true, 15, 15);
+    }
+    else if (strcmp(name, "ogre") == 0)
+    {
+        return sprite_generic_alloc("ogre", true, 10, 13);
+    }
+
+    // If no match is found
+    FURI_LOG_E("Game", "Sprite not found: %s", name);
+    return NULL;
+}

+ 76 - 0
game/player.h

@@ -0,0 +1,76 @@
+#pragma once
+#include "engine/engine.h"
+#include <flip_world.h>
+#include <game/game.h>
+#include "engine/sensors/imu.h"
+
+// Maximum enemies
+#define MAX_ENEMIES 2
+#define MAX_LEVELS 10
+
+typedef enum
+{
+    PLAYER_IDLE,
+    PLAYER_MOVING,
+    PLAYER_ATTACKING,
+    PLAYER_ATTACKED,
+    PLAYER_DEAD,
+} PlayerState;
+
+typedef enum
+{
+    PLAYER_UP,
+    PLAYER_DOWN,
+    PLAYER_LEFT,
+    PLAYER_RIGHT
+} PlayerDirection;
+
+typedef struct
+{
+    PlayerDirection direction;  // direction the player is facing
+    PlayerState state;          // current state of the player
+    Vector start_position;      // starting position of the player
+    Sprite *sprite_right;       // player sprite looking right
+    Sprite *sprite_left;        // player sprite looking left
+    int8_t dx;                  // x direction
+    int8_t dy;                  // y direction
+    uint32_t xp;                // experience points
+    uint32_t level;             // player level
+    uint32_t strength;          // player strength
+    uint32_t health;            // player health
+    uint32_t max_health;        // player maximum health
+    uint32_t health_regen;      // player health regeneration rate per second/frame
+    float elapsed_health_regen; // time elapsed since last health regeneration
+    float attack_timer;         // Cooldown duration between attacks
+    float elapsed_attack_timer; // Time elapsed since the last attack
+    char username[32];          // player username
+} PlayerContext;
+
+typedef struct
+{
+    PlayerContext *player_context;
+    Level *levels[MAX_LEVELS];
+    Entity *enemies[MAX_ENEMIES];
+    Entity *player;
+    GameKey user_input;
+    float fps;
+    int level_count;
+    int enemy_count;
+    int current_level;
+    bool ended_early;
+    Imu *imu;
+    bool imu_present;
+} GameContext;
+
+typedef struct
+{
+    char id[16];
+    char left_file_name[64];
+    char right_file_name[64];
+    uint8_t width;
+    uint8_t height;
+} SpriteContext;
+
+extern const EntityDescription player_desc;
+void player_spawn(Level *level, GameManager *manager);
+SpriteContext *get_sprite_context(const char *name);

+ 748 - 0
game/storage.c

@@ -0,0 +1,748 @@
+#include <game/storage.h>
+
+static bool save_uint32(const char *path_name, uint32_t value)
+{
+    char buffer[32];
+    snprintf(buffer, sizeof(buffer), "%lu", value);
+    return save_char(path_name, buffer);
+}
+
+// Helper function to save an int8_t
+static bool save_int8(const char *path_name, int8_t value)
+{
+    char buffer[32];
+    snprintf(buffer, sizeof(buffer), "%d", value);
+    return save_char(path_name, buffer);
+}
+
+// Helper function to save a float
+static bool save_float(const char *path_name, float value)
+{
+    char buffer[32];
+    snprintf(buffer, sizeof(buffer), "%.6f", (double)value); // Limit to 6 decimal places
+    return save_char(path_name, buffer);
+}
+bool save_player_context(PlayerContext *player_context)
+{
+    if (!player_context)
+    {
+        FURI_LOG_E(TAG, "Invalid player context");
+        return false;
+    }
+
+    // Create the directory for saving settings
+    char directory_path[256];
+    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data/player");
+
+    // Create the directory
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    storage_common_mkdir(storage, directory_path);
+    furi_record_close(RECORD_STORAGE);
+
+    // 1. Username (String)
+    if (!save_char("player/username", player_context->username))
+    {
+        FURI_LOG_E(TAG, "Failed to save player username");
+        return false;
+    }
+
+    // 2. Level (uint32_t)
+    if (!save_uint32("player/level", player_context->level))
+    {
+        FURI_LOG_E(TAG, "Failed to save player level");
+        return false;
+    }
+
+    // 3. XP (uint32_t)
+    if (!save_uint32("player/xp", player_context->xp))
+    {
+        FURI_LOG_E(TAG, "Failed to save player xp");
+        return false;
+    }
+
+    // 4. Health (uint32_t)
+    if (!save_uint32("player/health", player_context->health))
+    {
+        FURI_LOG_E(TAG, "Failed to save player health");
+        return false;
+    }
+
+    // 5. Strength (uint32_t)
+    if (!save_uint32("player/strength", player_context->strength))
+    {
+        FURI_LOG_E(TAG, "Failed to save player strength");
+        return false;
+    }
+
+    // 6. Max Health (uint32_t)
+    if (!save_uint32("player/max_health", player_context->max_health))
+    {
+        FURI_LOG_E(TAG, "Failed to save player max health");
+        return false;
+    }
+
+    // 7. Health Regen (uint32_t)
+    if (!save_uint32("player/health_regen", player_context->health_regen))
+    {
+        FURI_LOG_E(TAG, "Failed to save player health regen");
+        return false;
+    }
+
+    // 8. Elapsed Health Regen (float)
+    if (!save_float("player/elapsed_health_regen", player_context->elapsed_health_regen))
+    {
+        FURI_LOG_E(TAG, "Failed to save player elapsed health regen");
+        return false;
+    }
+
+    // 9. Attack Timer (float)
+    if (!save_float("player/attack_timer", player_context->attack_timer))
+    {
+        FURI_LOG_E(TAG, "Failed to save player attack timer");
+        return false;
+    }
+
+    // 10. Elapsed Attack Timer (float)
+    if (!save_float("player/elapsed_attack_timer", player_context->elapsed_attack_timer))
+    {
+        FURI_LOG_E(TAG, "Failed to save player elapsed attack timer");
+        return false;
+    }
+
+    // 11. Direction (enum PlayerDirection)
+    {
+        char direction_str[2];
+        switch (player_context->direction)
+        {
+        case PLAYER_UP:
+            strncpy(direction_str, "0", sizeof(direction_str));
+            break;
+        case PLAYER_DOWN:
+            strncpy(direction_str, "1", sizeof(direction_str));
+            break;
+        case PLAYER_LEFT:
+            strncpy(direction_str, "2", sizeof(direction_str));
+            break;
+        case PLAYER_RIGHT:
+        default:
+            strncpy(direction_str, "3", sizeof(direction_str));
+            break;
+        }
+        direction_str[1] = '\0'; // Ensure null termination
+
+        if (!save_char("player/direction", direction_str))
+        {
+            FURI_LOG_E(TAG, "Failed to save player direction");
+            return false;
+        }
+    }
+
+    // 12. State (enum PlayerState)
+    {
+        char state_str[2];
+        switch (player_context->state)
+        {
+        case PLAYER_IDLE:
+            strncpy(state_str, "0", sizeof(state_str));
+            break;
+        case PLAYER_MOVING:
+            strncpy(state_str, "1", sizeof(state_str));
+            break;
+        case PLAYER_ATTACKING:
+            strncpy(state_str, "2", sizeof(state_str));
+            break;
+        case PLAYER_ATTACKED:
+            strncpy(state_str, "3", sizeof(state_str));
+            break;
+        case PLAYER_DEAD:
+            strncpy(state_str, "4", sizeof(state_str));
+            break;
+        default:
+            strncpy(state_str, "5", sizeof(state_str)); // Assuming '5' for unknown states
+            break;
+        }
+        state_str[1] = '\0'; // Ensure null termination
+
+        if (!save_char("player/state", state_str))
+        {
+            FURI_LOG_E(TAG, "Failed to save player state");
+            return false;
+        }
+    }
+
+    // 13. Start Position X (float)
+    if (!save_float("player/start_position_x", player_context->start_position.x))
+    {
+        FURI_LOG_E(TAG, "Failed to save player start position x");
+        return false;
+    }
+
+    // 14. Start Position Y (float)
+    if (!save_float("player/start_position_y", player_context->start_position.y))
+    {
+        FURI_LOG_E(TAG, "Failed to save player start position y");
+        return false;
+    }
+
+    // 15. dx (int8_t)
+    if (!save_int8("player/dx", player_context->dx))
+    {
+        FURI_LOG_E(TAG, "Failed to save player dx");
+        return false;
+    }
+
+    // 16. dy (int8_t)
+    if (!save_int8("player/dy", player_context->dy))
+    {
+        FURI_LOG_E(TAG, "Failed to save player dy");
+        return false;
+    }
+
+    return true;
+}
+
+// Helper function to load an integer
+static bool load_number(const char *path_name, int *value)
+{
+    if (!path_name || !value)
+    {
+        FURI_LOG_E(TAG, "Invalid arguments to load_number");
+        return false;
+    }
+
+    char buffer[64];
+
+    if (!load_char(path_name, buffer, sizeof(buffer)))
+    {
+        FURI_LOG_E(TAG, "Failed to load number from path: %s", path_name);
+        return false;
+    }
+
+    *value = atoi(buffer);
+    return true;
+}
+
+// Helper function to load a float
+static bool load_float(const char *path_name, float *value)
+{
+    if (!path_name || !value)
+    {
+        FURI_LOG_E(TAG, "Invalid arguments to load_float");
+        return false;
+    }
+
+    char buffer[64];
+    if (!load_char(path_name, buffer, sizeof(buffer)))
+    {
+        FURI_LOG_E(TAG, "Failed to load float from path: %s", path_name);
+        return false;
+    }
+
+    // check if the string is a valid float
+    char *endptr;
+    *value = strtof(buffer, &endptr);
+    if (endptr == buffer)
+    {
+        FURI_LOG_E(TAG, "Failed to parse float from path: %s", path_name);
+        return false;
+    }
+
+    return true;
+}
+
+// Helper function to load an int8_t
+static bool load_int8(const char *path_name, int8_t *value)
+{
+    if (!path_name || !value)
+    {
+        FURI_LOG_E(TAG, "Invalid arguments to load_int8");
+        return false;
+    }
+
+    char buffer[64];
+
+    if (!load_char(path_name, buffer, sizeof(buffer)))
+    {
+        FURI_LOG_E(TAG, "Failed to load int8 from path: %s", path_name);
+        return false;
+    }
+
+    long temp = strtol(buffer, NULL, 10);
+    if (temp < INT8_MIN || temp > INT8_MAX)
+    {
+        FURI_LOG_E(TAG, "Value out of range for int8: %ld", temp);
+        return false;
+    }
+
+    // check if the string is a valid int8
+    char *endptr;
+    *value = (int8_t)strtol(buffer, &endptr, 10);
+    if (endptr == buffer)
+    {
+        FURI_LOG_E(TAG, "Failed to parse int8 from path: %s", path_name);
+        return false;
+    }
+    return true;
+}
+
+// Helper function to load a uint32_t
+static bool load_uint32(const char *path_name, uint32_t *value)
+{
+    if (!path_name || !value)
+    {
+        FURI_LOG_E(TAG, "Invalid arguments to load_uint32");
+        return false;
+    }
+
+    char buffer[64];
+    if (!load_char(path_name, buffer, sizeof(buffer)))
+    {
+        FURI_LOG_E(TAG, "Failed to load uint32 from path: %s", path_name);
+        return false;
+    }
+
+    // check if the string is a valid uint32
+    char *endptr;
+    *value = strtoul(buffer, &endptr, 10);
+    if (endptr == buffer)
+    {
+        FURI_LOG_E(TAG, "Failed to parse uint32 from path: %s", path_name);
+        return false;
+    }
+
+    return true;
+}
+
+bool load_player_context(PlayerContext *player_context)
+{
+    if (!player_context)
+    {
+        FURI_LOG_E(TAG, "Invalid player context");
+        return false;
+    }
+
+    // 1. Username (String)
+    if (!load_char("player/username", player_context->username, sizeof(player_context->username)))
+    {
+        FURI_LOG_E(TAG, "No data or parse error for username. Using default: 'Unknown'");
+        memset(player_context->username, 0, sizeof(player_context->username));
+        strncpy(player_context->username, "Unknown", sizeof(player_context->username) - 1);
+    }
+
+    // 2. Level (uint32_t)
+    {
+        uint32_t temp = 1; // Default
+        if (!load_char("player/level", (char *)&temp, sizeof(temp)))
+        {
+            FURI_LOG_E(TAG, "No data or parse error for level. Using default: 1");
+        }
+        else
+        {
+            // char buffer[64];
+            if (load_uint32("player/level", &temp))
+            {
+                player_context->level = temp;
+            }
+            else
+            {
+                FURI_LOG_E(TAG, "Failed to parse level. Using default: 1");
+                player_context->level = 1;
+            }
+        }
+        player_context->level = temp;
+    }
+
+    // 3. XP (uint32_t)
+    {
+        uint32_t temp = 0; // Default
+        if (!load_uint32("player/xp", &temp))
+        {
+            FURI_LOG_E(TAG, "No data or parse error for xp. Using default: 0");
+            temp = 0;
+        }
+        player_context->xp = temp;
+    }
+
+    // 4. Health (uint32_t)
+    {
+        uint32_t temp = 100; // Default
+        if (!load_uint32("player/health", &temp))
+        {
+            FURI_LOG_E(TAG, "No data or parse error for health. Using default: 100");
+            temp = 100;
+        }
+        player_context->health = temp;
+    }
+
+    // 5. Strength (uint32_t)
+    {
+        uint32_t temp = 10; // Default
+        if (!load_uint32("player/strength", &temp))
+        {
+            FURI_LOG_E(TAG, "No data or parse error for strength. Using default: 10");
+            temp = 10;
+        }
+        player_context->strength = temp;
+    }
+
+    // 6. Max Health (uint32_t)
+    {
+        uint32_t temp = 100; // Default
+        if (!load_uint32("player/max_health", &temp))
+        {
+            FURI_LOG_E(TAG, "No data or parse error for max_health. Using default: 100");
+            temp = 100;
+        }
+        player_context->max_health = temp;
+    }
+
+    // 7. Health Regen (uint32_t)
+    {
+        uint32_t temp = 1; // Default
+        if (!load_uint32("player/health_regen", &temp))
+        {
+            FURI_LOG_E(TAG, "No data or parse error for health_regen. Using default: 1");
+            temp = 1;
+        }
+        player_context->health_regen = temp;
+    }
+
+    // 8. Elapsed Health Regen (float)
+    {
+        float temp = 0.0f; // Default
+        if (!load_float("player/elapsed_health_regen", &temp))
+        {
+            FURI_LOG_E(TAG, "No data or parse error for elapsed_health_regen. Using default: 0.0f");
+            temp = 0.0f;
+        }
+        player_context->elapsed_health_regen = temp;
+    }
+
+    // 9. Attack Timer (float)
+    {
+        float temp = 0.1f; // Default
+        if (!load_float("player/attack_timer", &temp))
+        {
+            FURI_LOG_E(TAG, "No data or parse error for attack_timer. Using default: 0.1f");
+            temp = 0.1f;
+        }
+        player_context->attack_timer = temp;
+    }
+
+    // 10. Elapsed Attack Timer (float)
+    {
+        float temp = 0.0f; // Default
+        if (!load_float("player/elapsed_attack_timer", &temp))
+        {
+            FURI_LOG_E(TAG, "No data or parse error for elapsed_attack_timer. Using default: 0.0f");
+            temp = 0.0f;
+        }
+        player_context->elapsed_attack_timer = temp;
+    }
+
+    // 11. Direction (enum PlayerDirection)
+    {
+        int direction_int = 3; // Default to PLAYER_RIGHT
+        if (!load_number("player/direction", &direction_int))
+        {
+            FURI_LOG_E(TAG, "No data or parse error for direction. Defaulting to PLAYER_RIGHT");
+            direction_int = 3;
+        }
+
+        switch (direction_int)
+        {
+        case 0:
+            player_context->direction = PLAYER_UP;
+            break;
+        case 1:
+            player_context->direction = PLAYER_DOWN;
+            break;
+        case 2:
+            player_context->direction = PLAYER_LEFT;
+            break;
+        case 3:
+            player_context->direction = PLAYER_RIGHT;
+            break;
+        default:
+            FURI_LOG_E(TAG, "Invalid direction value: %d. Defaulting to PLAYER_RIGHT", direction_int);
+            player_context->direction = PLAYER_RIGHT;
+            break;
+        }
+    }
+
+    // 12. State (enum PlayerState)
+    {
+        int state_int = 0; // Default to PLAYER_IDLE
+        if (!load_number("player/state", &state_int))
+        {
+            FURI_LOG_E(TAG, "No data or parse error for state. Defaulting to PLAYER_IDLE");
+            state_int = 0;
+        }
+
+        switch (state_int)
+        {
+        case 0:
+            player_context->state = PLAYER_IDLE;
+            break;
+        case 1:
+            player_context->state = PLAYER_MOVING;
+            break;
+        case 2:
+            player_context->state = PLAYER_ATTACKING;
+            break;
+        case 3:
+            player_context->state = PLAYER_ATTACKED;
+            break;
+        case 4:
+            player_context->state = PLAYER_DEAD;
+            break;
+        default:
+            FURI_LOG_E(TAG, "Invalid state value: %d. Defaulting to PLAYER_IDLE", state_int);
+            player_context->state = PLAYER_IDLE;
+            break;
+        }
+    }
+
+    // 13. Start Position X (float)
+    {
+        float temp = 192.0f; // Default
+        if (!load_float("player/start_position_x", &temp))
+        {
+            FURI_LOG_E(TAG, "No data or parse error for start_position_x. Using default: 192.0f");
+            temp = 192.0f;
+        }
+        player_context->start_position.x = temp;
+    }
+
+    // 14. Start Position Y (float)
+    {
+        float temp = 96.0f; // Default
+        if (!load_float("player/start_position_y", &temp))
+        {
+            FURI_LOG_E(TAG, "No data or parse error for start_position_y. Using default: 96.0f");
+            temp = 96.0f;
+        }
+        player_context->start_position.y = temp;
+    }
+
+    // 15. dx (int8_t)
+    {
+        int8_t temp = 1; // Default
+        if (!load_int8("player/dx", &temp))
+        {
+            FURI_LOG_E(TAG, "No data or parse error for dx. Using default: 1");
+            temp = 1;
+        }
+        player_context->dx = temp;
+    }
+
+    // 16. dy (int8_t)
+    {
+        int8_t temp = 0; // Default
+        if (!load_int8("player/dy", &temp))
+        {
+            FURI_LOG_E(TAG, "No data or parse error for dy. Using default: 0");
+            temp = 0;
+        }
+        player_context->dy = temp;
+    }
+
+    return true;
+}
+
+static inline void furi_string_remove_str(FuriString *string, const char *needle)
+{
+    furi_string_replace_str(string, needle, "", 0);
+}
+
+static FuriString *enemy_data(const FuriString *world_data)
+{
+    size_t enemy_data_pos = furi_string_search_str(world_data, "enemy_data", 0);
+    if (enemy_data_pos == FURI_STRING_FAILURE)
+    {
+        FURI_LOG_E("Game", "Failed to find enemy_data in world data");
+
+        return NULL;
+    }
+
+    size_t bracket_start = furi_string_search_char(world_data, '[', enemy_data_pos);
+    if (bracket_start == FURI_STRING_FAILURE)
+    {
+        FURI_LOG_E("Game", "Failed to find start of enemy_data array");
+
+        return NULL;
+    }
+
+    size_t bracket_end = furi_string_search_char(world_data, ']', bracket_start);
+    if (bracket_end == FURI_STRING_FAILURE)
+    {
+        FURI_LOG_E("Game", "Failed to find end of enemy_data array");
+
+        return NULL;
+    }
+
+    FuriString *enemy_data_str = furi_string_alloc();
+    if (!enemy_data_str)
+    {
+        FURI_LOG_E("Game", "Failed to allocate enemy_data string");
+
+        return NULL;
+    }
+
+    furi_string_cat_str(enemy_data_str, "{\"enemy_data\":");
+
+    {
+        FuriString *temp_sub = furi_string_alloc();
+
+        furi_string_set_strn(
+            temp_sub,
+            furi_string_get_cstr(world_data) + bracket_start,
+            (bracket_end + 1) - bracket_start);
+
+        furi_string_cat(enemy_data_str, temp_sub);
+        furi_string_free(temp_sub);
+    }
+
+    furi_string_cat_str(enemy_data_str, "}");
+
+    return enemy_data_str;
+}
+
+static FuriString *json_data(const FuriString *world_data)
+{
+    size_t json_data_pos = furi_string_search_str(world_data, "json_data", 0);
+    if (json_data_pos == FURI_STRING_FAILURE)
+    {
+        FURI_LOG_E("Game", "Failed to find json_data in world data");
+
+        return NULL;
+    }
+
+    size_t bracket_start = furi_string_search_char(world_data, '[', json_data_pos);
+    if (bracket_start == FURI_STRING_FAILURE)
+    {
+        FURI_LOG_E("Game", "Failed to find start of json_data array");
+
+        return NULL;
+    }
+
+    size_t bracket_end = furi_string_search_char(world_data, ']', bracket_start);
+    if (bracket_end == FURI_STRING_FAILURE)
+    {
+        FURI_LOG_E("Game", "Failed to find end of json_data array");
+
+        return NULL;
+    }
+
+    FuriString *json_data_str = furi_string_alloc();
+    if (!json_data_str)
+    {
+        FURI_LOG_E("Game", "Failed to allocate json_data string");
+
+        return NULL;
+    }
+
+    furi_string_cat_str(json_data_str, "{\"json_data\":");
+
+    {
+        FuriString *temp_sub = furi_string_alloc();
+
+        furi_string_set_strn(
+            temp_sub,
+            furi_string_get_cstr(world_data) + bracket_start,
+            (bracket_end + 1) - bracket_start);
+
+        furi_string_cat(json_data_str, temp_sub);
+        furi_string_free(temp_sub);
+    }
+
+    furi_string_cat_str(json_data_str, "}");
+
+    return json_data_str;
+}
+
+bool separate_world_data(char *id, FuriString *world_data)
+{
+    if (!id || !world_data)
+    {
+        FURI_LOG_E("Game", "Invalid parameters");
+        return false;
+    }
+    FuriString *file_json_data = json_data(world_data);
+    if (!file_json_data || furi_string_size(file_json_data) == 0)
+    {
+        FURI_LOG_E("Game", "Failed to get json data in separate_world_data");
+        return false;
+    }
+
+    // Save file_json_data to disk
+    char directory_path[256];
+    snprintf(directory_path, sizeof(directory_path),
+             STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s", id);
+
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    storage_common_mkdir(storage, directory_path);
+
+    File *file = storage_file_alloc(storage);
+    char file_path[256];
+    snprintf(file_path, sizeof(file_path),
+             STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s/%s_json_data.json",
+             id, id);
+
+    if (!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS))
+    {
+        FURI_LOG_E("Game", "Failed to open file for writing: %s", file_path);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        furi_string_free(file_json_data);
+        return false;
+    }
+
+    size_t data_size = furi_string_size(file_json_data);
+    if (storage_file_write(file, furi_string_get_cstr(file_json_data), data_size) != data_size)
+    {
+        FURI_LOG_E("Game", "Failed to write json_data");
+    }
+    storage_file_close(file);
+
+    furi_string_replace_at(file_json_data, 0, 1, "");
+    furi_string_replace_at(file_json_data, furi_string_size(file_json_data) - 1, 1, "");
+    // include the comma at the end of the json_data array
+    furi_string_cat_str(file_json_data, ",");
+    furi_string_remove_str(world_data, furi_string_get_cstr(file_json_data));
+    furi_string_free(file_json_data);
+
+    FuriString *file_enemy_data = enemy_data(world_data);
+    if (!file_enemy_data)
+    {
+        FURI_LOG_E("Game", "Failed to get enemy data");
+        return false;
+    }
+
+    snprintf(file_path, sizeof(file_path),
+             STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s/%s_enemy_data.json",
+             id, id);
+
+    if (!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS))
+    {
+        FURI_LOG_E("Game", "Failed to open file for writing: %s", file_path);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        furi_string_free(file_enemy_data);
+        return false;
+    }
+
+    data_size = furi_string_size(file_enemy_data);
+    if (storage_file_write(file, furi_string_get_cstr(file_enemy_data), data_size) != data_size)
+    {
+        FURI_LOG_E("Game", "Failed to write enemy_data");
+    }
+
+    // Clean up
+    furi_string_free(file_enemy_data);
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_record_close(RECORD_STORAGE);
+
+    return true;
+}

+ 11 - 0
game/storage.h

@@ -0,0 +1,11 @@
+#pragma once
+#include <game/player.h>
+#include <game/game.h>
+#include <flip_world.h>
+#include <flip_storage/storage.h>
+
+bool save_player_context(PlayerContext *player_context);
+bool load_player_context(PlayerContext *player_context);
+
+// save the json_data and enemy_data to separate files
+bool separate_world_data(char *id, FuriString *world_data);

+ 66 - 290
game/world.c

@@ -1,5 +1,6 @@
 #include <game/world.h>
-
+#include <game/storage.h>
+#include <flip_storage/storage.h>
 void draw_bounds(Canvas *canvas)
 {
     // Draw the outer bounds adjusted by camera offset
@@ -7,125 +8,30 @@ void draw_bounds(Canvas *canvas)
     canvas_draw_frame(canvas, -camera_x, -camera_y, WORLD_WIDTH, WORLD_HEIGHT);
 }
 
-bool draw_json_world(Level *level, const char *json_data)
+bool draw_json_world_furi(Level *level, const FuriString *json_data)
 {
-    for (int i = 0; i < MAX_WORLD_OBJECTS; i++)
+    if (!json_data)
     {
-        // 1) Get data array item
-        char *data = get_json_array_value("json_data", i, json_data);
-        if (!data)
-        {
-            // Means we've reached the end of the array
-            break;
-        }
-
-        // 2) Extract all required fields
-        char *icon = get_json_value("icon", data);
-        char *x = get_json_value("x", data);
-        char *y = get_json_value("y", data);
-        char *amount = get_json_value("amount", data);
-        char *horizontal = get_json_value("horizontal", data);
-
-        // 3) Check for any NULL pointers
-        if (!icon || !x || !y || !amount || !horizontal)
-        {
-            FURI_LOG_E("Game", "Failed Data: %s", data);
-
-            // Free everything carefully
-            if (data)
-                free(data);
-            if (icon)
-                free(icon);
-            if (x)
-                free(x);
-            if (y)
-                free(y);
-            if (amount)
-                free(amount);
-            if (horizontal)
-                free(horizontal);
-
-            level_clear(level);
-            return false;
-        }
-
-        // 4) Get the IconContext
-        IconContext *icon_context = get_icon_context(icon);
-        if (!icon_context)
-        {
-            FURI_LOG_E("Game", "Failed Icon: %s", icon);
-
-            free(data);
-            free(icon);
-            free(x);
-            free(y);
-            free(amount);
-            free(horizontal);
-
-            level_clear(level);
-            return false;
-        }
-
-        // 5) Decide how many icons to spawn
-        int count = atoi(amount);
-        if (count < 2)
-        {
-            // Just one icon
-            spawn_icon(
-                level,
-                icon_context->icon,
-                atoi(x),
-                atoi(y),
-                icon_context->width,
-                icon_context->height);
-        }
-        else
-        {
-            // Spawn multiple in a line
-            bool is_horizontal = (strcmp(horizontal, "true") == 0);
-            spawn_icon_line(
-                level,
-                icon_context->icon,
-                atoi(x),
-                atoi(y),
-                icon_context->width,
-                icon_context->height,
-                count,
-                is_horizontal);
-        }
-
-        // 6) Cleanup
-        free(data);
-        free(icon);
-        free(x);
-        free(y);
-        free(amount);
-        free(horizontal);
-        free(icon_context);
+        FURI_LOG_E("Game", "JSON data is NULL");
+        return false;
     }
-    return true;
-}
-
-bool draw_json_world_furi(Level *level, FuriString *json_data)
-{
+    int levels_added = 0;
+    FURI_LOG_I("Game", "Looping through world data");
     for (int i = 0; i < MAX_WORLD_OBJECTS; i++)
     {
-        // 1) Get data array item as FuriString
+        FURI_LOG_I("Game", "Looping through world data: %d", i);
         FuriString *data = get_json_array_value_furi("json_data", i, json_data);
         if (!data)
         {
-            // Means we've reached the end of the array
             break;
         }
 
-        // 2) Extract all required fields
         FuriString *icon = get_json_value_furi("icon", data);
         FuriString *x = get_json_value_furi("x", data);
         FuriString *y = get_json_value_furi("y", data);
         FuriString *amount = get_json_value_furi("amount", data);
         FuriString *horizontal = get_json_value_furi("horizontal", data);
 
-        // 3) Check for any NULL pointers
         if (!icon || !x || !y || !amount || !horizontal)
         {
             FURI_LOG_E("Game", "Failed Data: %s", furi_string_get_cstr(data));
@@ -147,241 +53,109 @@ bool draw_json_world_furi(Level *level, FuriString *json_data)
             return false;
         }
 
-        // 4) Get the IconContext from a FuriString
-        IconContext *icon_context = get_icon_context_furi(icon);
-        if (!icon_context)
-        {
-            FURI_LOG_E("Game", "Failed Icon: %s", furi_string_get_cstr(icon));
-
-            furi_string_free(data);
-            furi_string_free(icon);
-            furi_string_free(x);
-            furi_string_free(y);
-            furi_string_free(amount);
-            furi_string_free(horizontal);
-
-            level_clear(level);
-            return false;
-        }
-
-        // 5) Decide how many icons to spawn
         int count = atoi(furi_string_get_cstr(amount));
         if (count < 2)
         {
             // Just one icon
             spawn_icon(
                 level,
-                icon_context->icon,
+                furi_string_get_cstr(icon),
                 atoi(furi_string_get_cstr(x)),
-                atoi(furi_string_get_cstr(y)),
-                icon_context->width,
-                icon_context->height);
+                atoi(furi_string_get_cstr(y)));
         }
         else
         {
-            // Spawn multiple in a line
             bool is_horizontal = (furi_string_cmp(horizontal, "true") == 0);
             spawn_icon_line(
                 level,
-                icon_context->icon,
+                furi_string_get_cstr(icon),
                 atoi(furi_string_get_cstr(x)),
                 atoi(furi_string_get_cstr(y)),
-                icon_context->width,
-                icon_context->height,
                 count,
                 is_horizontal);
         }
 
-        // 6) Clean up after every iteration
         furi_string_free(data);
         furi_string_free(icon);
         furi_string_free(x);
         furi_string_free(y);
         furi_string_free(amount);
         furi_string_free(horizontal);
-        free(icon_context);
+        levels_added++;
     }
-    return true;
-}
-
-void draw_tree_world(Level *level)
-{
-    IconContext *tree_icon = get_icon_context("tree");
-    if (!tree_icon)
-    {
-        FURI_LOG_E("Game", "Failed to get tree icon context");
-        return;
-    }
-    // Spawn two full left/up tree lines
-    for (int i = 0; i < 2; i++)
-    {
-        // Horizontal line of 22 icons
-        spawn_icon_line(level, tree_icon->icon, 5, 2 + i * 17, tree_icon->width, tree_icon->height, 22, true);
-        // Vertical line of 11 icons
-        spawn_icon_line(level, tree_icon->icon, 5 + i * 17, 2, tree_icon->width, tree_icon->height, 11, false);
-    }
-
-    // Spawn two full down tree lines
-    for (int i = 9; i < 11; i++)
-    {
-        // Horizontal line of 22 icons
-        spawn_icon_line(level, tree_icon->icon, 5, 2 + i * 17, tree_icon->width, tree_icon->height, 22, true);
-    }
-
-    // Spawn two full right tree lines
-    for (int i = 20; i < 22; i++)
-    {
-        // Vertical line of 8 icons starting further down (y=50)
-        spawn_icon_line(level, tree_icon->icon, 5 + i * 17, 50, tree_icon->width, tree_icon->height, 8, false);
-    }
-
-    // Labyrinth lines
-    // Third line (14 left, then a gap, then 3 middle)
-    spawn_icon_line(level, tree_icon->icon, 5, 2 + 2 * 17, tree_icon->width, tree_icon->height, 14, true);
-    spawn_icon_line(level, tree_icon->icon, 5 + 16 * 17, 2 + 2 * 17, tree_icon->width, tree_icon->height, 3, true);
-
-    // Fourth line (3 left, 6 middle, 4 right)
-    spawn_icon_line(level, tree_icon->icon, 5, 2 + 3 * 17, tree_icon->width, tree_icon->height, 3, true);           // 3 left
-    spawn_icon_line(level, tree_icon->icon, 5 + 7 * 17, 2 + 3 * 17, tree_icon->width, tree_icon->height, 6, true);  // 6 middle
-    spawn_icon_line(level, tree_icon->icon, 5 + 15 * 17, 2 + 3 * 17, tree_icon->width, tree_icon->height, 4, true); // 4 right
-
-    // Fifth line (6 left, 7 middle)
-    spawn_icon_line(level, tree_icon->icon, 5, 2 + 4 * 17, tree_icon->width, tree_icon->height, 6, true);
-    spawn_icon_line(level, tree_icon->icon, 5 + 7 * 17, 2 + 4 * 17, tree_icon->width, tree_icon->height, 7, true);
-
-    // Sixth line (5 left, 3 middle, 7 right)
-    spawn_icon_line(level, tree_icon->icon, 5, 2 + 5 * 17, tree_icon->width, tree_icon->height, 5, true);           // 5 left
-    spawn_icon_line(level, tree_icon->icon, 5 + 7 * 17, 2 + 5 * 17, tree_icon->width, tree_icon->height, 3, true);  // 3 middle
-    spawn_icon_line(level, tree_icon->icon, 5 + 15 * 17, 2 + 5 * 17, tree_icon->width, tree_icon->height, 7, true); // 7 right
-
-    // Seventh line (0 left, 7 middle, 4 right)
-    spawn_icon_line(level, tree_icon->icon, 5 + 6 * 17, 2 + 6 * 17, tree_icon->width, tree_icon->height, 7, true);  // 7 middle
-    spawn_icon_line(level, tree_icon->icon, 5 + 14 * 17, 2 + 6 * 17, tree_icon->width, tree_icon->height, 4, true); // 4 right
-
-    // Eighth line (4 left, 3 middle, 4 right)
-    spawn_icon_line(level, tree_icon->icon, 5, 2 + 7 * 17, tree_icon->width, tree_icon->height, 4, true);           // 4 left
-    spawn_icon_line(level, tree_icon->icon, 5 + 7 * 17, 2 + 7 * 17, tree_icon->width, tree_icon->height, 3, true);  // 3 middle
-    spawn_icon_line(level, tree_icon->icon, 5 + 15 * 17, 2 + 7 * 17, tree_icon->width, tree_icon->height, 4, true); // 4 right
-
-    // Ninth line (3 left, 1 middle, 3 right)
-    spawn_icon_line(level, tree_icon->icon, 5, 2 + 8 * 17, tree_icon->width, tree_icon->height, 3, true);           // 3 left
-    spawn_icon_line(level, tree_icon->icon, 5 + 5 * 17, 2 + 8 * 17, tree_icon->width, tree_icon->height, 1, true);  // 1 middle
-    spawn_icon_line(level, tree_icon->icon, 5 + 11 * 17, 2 + 8 * 17, tree_icon->width, tree_icon->height, 3, true); // 3 right
-
-    free(tree_icon);
+    FURI_LOG_I("Game", "Finished loading world data");
+    return levels_added > 0;
 }
 
 void draw_town_world(Level *level)
 {
-    // define all the icons
-    IconContext *house_icon = get_icon_context("house");
-    IconContext *fence_icon = get_icon_context("fence");
-    IconContext *fence_end_icon = get_icon_context("fence_end");
-    IconContext *plant_icon = get_icon_context("plant");
-    IconContext *flower_icon = get_icon_context("flower");
-    IconContext *man_icon = get_icon_context("man");
-    IconContext *woman_icon = get_icon_context("woman");
-    IconContext *lake_top_left_icon = get_icon_context("lake_top_left");
-    IconContext *lake_top_icon = get_icon_context("lake_top");
-    IconContext *lake_top_right_icon = get_icon_context("lake_top_right");
-    IconContext *lake_left_icon = get_icon_context("lake_left");
-    IconContext *lake_right_icon = get_icon_context("lake_right");
-    IconContext *lake_bottom_left_icon = get_icon_context("lake_bottom_left");
-    IconContext *lake_bottom_icon = get_icon_context("lake_bottom");
-    IconContext *lake_bottom_right_icon = get_icon_context("lake_bottom_right");
-    IconContext *tree_icon = get_icon_context("tree");
-
-    // check if any of the icons are NULL
-    if (!house_icon || !fence_icon || !fence_end_icon || !plant_icon || !flower_icon ||
-        !man_icon || !woman_icon || !lake_top_left_icon || !lake_top_icon || !lake_top_right_icon ||
-        !lake_left_icon || !lake_right_icon || !lake_bottom_left_icon || !lake_bottom_icon || !lake_bottom_right_icon || !tree_icon)
-    {
-        FURI_LOG_E("Game", "Failed to get icon context");
-        return;
-    }
 
     // house-fence group 1
-    spawn_icon(level, house_icon->icon, 148, 36, house_icon->width, house_icon->height);
-    spawn_icon(level, fence_icon->icon, 148, 72, fence_icon->width, fence_icon->height);
-    spawn_icon(level, fence_icon->icon, 164, 72, fence_icon->width, fence_icon->height);
-    spawn_icon(level, fence_end_icon->icon, 180, 72, fence_end_icon->width, fence_end_icon->height);
+    spawn_icon(level, "house", 164, 40);
+    spawn_icon(level, "fence", 148, 64);
+    spawn_icon(level, "fence", 164, 64);
+    spawn_icon(level, "fence_end", 180, 64);
 
     // house-fence group 4 (the left of group 1)
-    spawn_icon(level, house_icon->icon, 96, 36, house_icon->width, house_icon->height);
-    spawn_icon(level, fence_icon->icon, 96, 72, fence_icon->width, fence_icon->height);
-    spawn_icon(level, fence_icon->icon, 110, 72, fence_icon->width, fence_icon->height);
-    spawn_icon(level, fence_end_icon->icon, 126, 72, fence_end_icon->width, fence_end_icon->height);
+    spawn_icon(level, "house", 110, 40);
+    spawn_icon(level, "fence", 96, 64);
+    spawn_icon(level, "fence", 110, 64);
+    spawn_icon(level, "fence_end", 126, 64);
 
     // house-fence group 5 (the left of group 4)
-    spawn_icon(level, house_icon->icon, 40, 36, house_icon->width, house_icon->height);
-    spawn_icon(level, fence_icon->icon, 40, 72, fence_icon->width, fence_icon->height);
-    spawn_icon(level, fence_icon->icon, 56, 72, fence_icon->width, fence_icon->height);
-    spawn_icon(level, fence_end_icon->icon, 72, 72, fence_end_icon->width, fence_end_icon->height);
+    spawn_icon(level, "house", 56, 40);
+    spawn_icon(level, "fence", 40, 64);
+    spawn_icon(level, "fence", 56, 64);
+    spawn_icon(level, "fence_end", 72, 64);
 
     // line of fences on the 8th row (using spawn_icon_line)
-    spawn_icon_line(level, fence_icon->icon, 8, 100, fence_icon->width, fence_icon->height, 10, true);
+    spawn_icon_line(level, "fence", 8, 96, 10, true);
 
     // plants spaced out underneath the fences
-    spawn_icon_line(level, plant_icon->icon, 40, 110, plant_icon->width, plant_icon->height, 6, true);
-    spawn_icon_line(level, flower_icon->icon, 40, 140, flower_icon->width, flower_icon->height, 6, true);
+    spawn_icon_line(level, "plant", 40, 110, 6, true);
+    spawn_icon_line(level, "flower", 40, 140, 6, true);
 
     // man and woman
-    spawn_icon(level, man_icon->icon, 156, 110, man_icon->width, man_icon->height);
-    spawn_icon(level, woman_icon->icon, 164, 110, woman_icon->width, woman_icon->height);
+    spawn_icon(level, "man", 156, 110);
+    spawn_icon(level, "woman", 164, 110);
 
     // lake
     // Top row
-    spawn_icon(level, lake_top_left_icon->icon, 240, 52, lake_top_left_icon->width, lake_top_left_icon->height);
-    spawn_icon(level, lake_top_icon->icon, 264, 52, lake_top_icon->width, lake_top_icon->height);
-    spawn_icon(level, lake_top_right_icon->icon, 295, 52, lake_top_right_icon->width, lake_top_right_icon->height);
+    spawn_icon(level, "lake_top_left", 240, 62);
+    spawn_icon(level, "lake_top", 264, 57);
+    spawn_icon(level, "lake_top_right", 295, 62);
 
     // Middle row
-    spawn_icon(level, lake_left_icon->icon, 231, 74, lake_left_icon->width, lake_left_icon->height);
-    spawn_icon(level, lake_right_icon->icon, 317, 74, lake_right_icon->width, lake_right_icon->height);
+    spawn_icon(level, "lake_left", 231, 84);
+    spawn_icon(level, "lake_right", 304, 84);
 
     // Bottom row
-    spawn_icon(level, lake_bottom_left_icon->icon, 240, 105, lake_bottom_left_icon->width, lake_bottom_left_icon->height);
-    spawn_icon(level, lake_bottom_icon->icon, 264, 124, lake_bottom_icon->width, lake_bottom_icon->height);
-    spawn_icon(level, lake_bottom_right_icon->icon, 295, 105, lake_bottom_right_icon->width, lake_bottom_right_icon->height);
+    spawn_icon(level, "lake_bottom_left", 240, 115);
+    spawn_icon(level, "lake_bottom", 264, 120);
+    spawn_icon(level, "lake_bottom_right", 295, 115);
 
     // Spawn two full left/up tree lines
     for (int i = 0; i < 2; i++)
     {
         // Horizontal line of 22 icons
-        spawn_icon_line(level, tree_icon->icon, 5, 2 + i * 17, tree_icon->width, tree_icon->height, 22, true);
+        spawn_icon_line(level, "tree", 5, 2 + i * 17, 22, true);
         // Vertical line of 11 icons
-        spawn_icon_line(level, tree_icon->icon, 5 + i * 17, 2, tree_icon->width, tree_icon->height, 11, false);
+        spawn_icon_line(level, "tree", 5 + i * 17, 2, 11, false);
     }
 
     // Spawn two full down tree lines
     for (int i = 9; i < 11; i++)
     {
         // Horizontal line of 22 icons
-        spawn_icon_line(level, tree_icon->icon, 5, 2 + i * 17, tree_icon->width, tree_icon->height, 22, true);
+        spawn_icon_line(level, "tree", 5, 2 + i * 17, 22, true);
     }
 
     // Spawn two full right tree lines
     for (int i = 20; i < 22; i++)
     {
         // Vertical line of 8 icons starting further down (y=50)
-        spawn_icon_line(level, tree_icon->icon, 5 + i * 17, 50, tree_icon->width, tree_icon->height, 8, false);
+        spawn_icon_line(level, "tree", 5 + i * 17, 50, 8, false);
     }
-    free(house_icon);
-    free(fence_icon);
-    free(fence_end_icon);
-    free(plant_icon);
-    free(flower_icon);
-    free(man_icon);
-    free(woman_icon);
-    free(lake_top_left_icon);
-    free(lake_top_icon);
-    free(lake_top_right_icon);
-    free(lake_left_icon);
-    free(lake_right_icon);
-    free(lake_bottom_left_icon);
-    free(lake_bottom_icon);
-    free(lake_bottom_right_icon);
-    free(tree_icon);
 }
 
 FuriString *fetch_world(const char *name)
@@ -391,48 +165,50 @@ FuriString *fetch_world(const char *name)
         FURI_LOG_E("Game", "World name is NULL");
         return NULL;
     }
-    if (!app_instance)
-    {
-        // as long as the game is running, app_instance should be non-NULL
-        FURI_LOG_E("Game", "App instance is NULL");
-        return NULL;
-    }
 
-    if (!flipper_http_init(flipper_http_rx_callback, app_instance))
+    FlipperHTTP *fhttp = flipper_http_alloc();
+    if (!fhttp)
     {
-        FURI_LOG_E("Game", "Failed to initialize HTTP");
+        FURI_LOG_E("Game", "Failed to allocate HTTP");
         return NULL;
     }
+
     char url[256];
-    snprintf(url, sizeof(url), "https://www.flipsocial.net/api/world/get/world/%s/", name);
-    snprintf(fhttp.file_path, sizeof(fhttp.file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s.json", name);
-    fhttp.save_received_data = true;
-    if (!flipper_http_get_request_with_headers(url, "{\"Content-Type\": \"application/json\"}"))
+    snprintf(url, sizeof(url), "https://www.flipsocial.net/api/world/v2/get/world/%s/", name);
+    snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s.json", name);
+    fhttp->save_received_data = true;
+    if (!flipper_http_get_request_with_headers(fhttp, url, "{\"Content-Type\": \"application/json\"}"))
     {
         FURI_LOG_E("Game", "Failed to send HTTP request");
-        flipper_http_deinit();
+        flipper_http_free(fhttp);
         return NULL;
     }
-    fhttp.state = RECEIVING;
-    furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
-    while (fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0)
+    fhttp->state = RECEIVING;
+    furi_timer_start(fhttp->get_timeout_timer, TIMEOUT_DURATION_TICKS);
+    while (fhttp->state == RECEIVING && furi_timer_is_running(fhttp->get_timeout_timer) > 0)
     {
         // Wait for the request to be received
         furi_delay_ms(100);
     }
-    furi_timer_stop(fhttp.get_timeout_timer);
-    if (fhttp.state != IDLE)
+    furi_timer_stop(fhttp->get_timeout_timer);
+    if (fhttp->state != IDLE)
     {
         FURI_LOG_E("Game", "Failed to receive world data");
-        flipper_http_deinit();
+        flipper_http_free(fhttp);
         return NULL;
     }
-    flipper_http_deinit();
+    flipper_http_free(fhttp);
     FuriString *returned_data = load_furi_world(name);
     if (!returned_data)
     {
         FURI_LOG_E("Game", "Failed to load world data from file");
         return NULL;
     }
+    if (!separate_world_data((char *)name, returned_data))
+    {
+        FURI_LOG_E("Game", "Failed to separate world data");
+        furi_string_free(returned_data);
+        return NULL;
+    }
     return returned_data;
 }

+ 2 - 4
game/world.h

@@ -9,11 +9,9 @@
 #define WORLD_HEIGHT 192
 
 // Maximum number of world objects
-#define MAX_WORLD_OBJECTS 100
+#define MAX_WORLD_OBJECTS 25 // any more than that and we may run out of heap when switching worlds
 
 void draw_bounds(Canvas *canvas);
-void draw_tree_world(Level *level);
 void draw_town_world(Level *level);
-bool draw_json_world(Level *level, const char *json_data);
-bool draw_json_world_furi(Level *level, FuriString *json_data);
+bool draw_json_world_furi(Level *level, const FuriString *json_data);
 FuriString *fetch_world(const char *name);

+ 1 - 2
jsmn/jsmn.c

@@ -422,7 +422,7 @@ int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len,
 }
 
 // Helper function to create a JSON object
-char *jsmn(const char *key, const char *value)
+char *get_json(const char *key, const char *value)
 {
     int length = strlen(key) + strlen(value) + 8;         // Calculate required length
     char *result = (char *)malloc(length * sizeof(char)); // Allocate memory
@@ -602,7 +602,6 @@ char *get_json_array_value(char *key, uint32_t index, const char *json_data)
 
     if (index >= (uint32_t)tokens[0].size)
     {
-        FURI_LOG_E("JSMM.H", "Index %lu out of bounds for array with size %u.", index, tokens[0].size);
         free(tokens);
         free(array_str);
         return NULL;

+ 1 - 7
jsmn/jsmn.h

@@ -56,14 +56,8 @@ extern "C"
 #define JB_JSMN_EDIT
 /* Added in by JBlanked on 2024-10-16 for use in Flipper Zero SDK*/
 
-#include <string.h>
-#include <stdint.h>
-#include <stdlib.h>
-#include <stdio.h>
-#include <furi.h>
-
 // Helper function to create a JSON object
-char *jsmn(const char *key, const char *value);
+char *get_json(const char *key, const char *value);
 // Helper function to compare JSON keys
 int jsoneq(const char *json, jsmntok_t *tok, const char *s);
 

+ 1 - 5
jsmn/jsmn_furi.c

@@ -400,11 +400,8 @@ int jsmn_parse_furi(jsmn_parser *parser, const FuriString *js,
     return count;
 }
 
-// The rest of your code (e.g., get_json_value_furi, get_json_array_value_furi, etc.)
-// remains unchanged and can still rely on these updated parsing functions.
-
 // Helper function to create a JSON object: {"key":"value"}
-FuriString *jsmn_create_object(const FuriString *key, const FuriString *value)
+FuriString *get_json_furi(const FuriString *key, const FuriString *value)
 {
     FuriString *result = furi_string_alloc();
     furi_string_printf(result, "{\"%s\":\"%s\"}",
@@ -579,7 +576,6 @@ FuriString *get_json_array_value_furi(const char *key, uint32_t index, const Fur
 
     if (index >= (uint32_t)tokens[0].size)
     {
-        FURI_LOG_E("JSMM.H", "Index %lu out of bounds for array with size %u.", index, tokens[0].size);
         free(tokens);
         furi_string_free(array_str);
         return NULL;

+ 1 - 1
jsmn/jsmn_furi.h

@@ -47,7 +47,7 @@ extern "C"
 #define JB_JSMN_FURI_EDIT
 
 // Helper function to create a JSON object
-FuriString *jsmn_create_object(const FuriString *key, const FuriString *value);
+FuriString *get_json_furi(const FuriString *key, const FuriString *value);
 
 // Updated signatures to accept const char* key
 FuriString *get_json_value_furi(const char *key, const FuriString *json_data);

+ 12 - 0
jsmn/jsmn_h.h

@@ -38,4 +38,16 @@ typedef struct
     int toksuper;         /* superior token node, e.g. parent object or array */
 } jsmn_parser;
 
+typedef struct
+{
+    char *key;
+    char *value;
+} JSON;
+
+typedef struct
+{
+    FuriString *key;
+    FuriString *value;
+} FuriJSON;
+
 FuriString *char_to_furi_string(const char *str);

BIN
sprites/enemy_left_cyclops_10x11px.png


BIN
sprites/enemy_left_ghost_15x15px.png


BIN
sprites/enemy_left_ogre_10x13px.png


BIN
sprites/enemy_right_cyclops_10x11px.png


BIN
sprites/enemy_right_ghost_15x15px.png


BIN
sprites/enemy_right_ogre_10x13px.png


BIN
sprites/player_left_axe_15x11px.png


BIN
sprites/player_left_bow_13x11px.png


+ 0 - 0
sprites/player_left.png → sprites/player_left_naked_10x10px.png


BIN
sprites/player_left_sword_15x11px.png


BIN
sprites/player_right_axe_15x11px.png


BIN
sprites/player_right_bow_13x11.png


+ 0 - 0
sprites/player_right.png → sprites/player_right_naked_10x10px.png


BIN
sprites/player_right_sword_15x11px.png


Some files were not shown because too many files changed in this diff