Просмотр исходного кода

Merge pull request #22 from jblanked/dev_0.8.1

FlipWorld - v0.8.1
- Improved memory allocation, saving 26 KB.
- Added auto-updating (currently uses the catalog API).
- Major code refactoring.
JBlanked 9 месяцев назад
Родитель
Сommit
df3f01ba99
27 измененных файлов с 1134 добавлено и 722 удалено
  1. 13 5
      app.c
  2. 1 1
      application.fam
  3. BIN
      assets/01-home.png
  4. 20 15
      assets/CHANGELOG.md
  5. 17 21
      callback/alloc.c
  6. 125 55
      callback/callback.c
  7. 17 16
      callback/callback.h
  8. 54 218
      callback/game.c
  9. 9 1
      callback/game.h
  10. 425 1
      flip_world.c
  11. 6 1
      flip_world.h
  12. 43 14
      flipper_http/flipper_http.c
  13. 5 43
      game/draw.c
  14. 3 8
      game/draw.h
  15. 21 21
      game/enemy.c
  16. 1 1
      game/enemy.h
  17. 3 3
      game/game.c
  18. 46 127
      game/icon.c
  19. 33 25
      game/icon.h
  20. 9 9
      game/level.c
  21. 1 1
      game/level.h
  22. 12 11
      game/npc.c
  23. 1 1
      game/npc.h
  24. 80 43
      game/player.c
  25. 17 5
      game/player.h
  26. 166 71
      game/world.c
  27. 6 5
      game/world.h

+ 13 - 5
app.c

@@ -43,17 +43,25 @@ int32_t flip_world_main(void *p)
     while (fhttp->state == INACTIVE && --counter > 0)
     while (fhttp->state == INACTIVE && --counter > 0)
     {
     {
         FURI_LOG_D(TAG, "Waiting for PONG");
         FURI_LOG_D(TAG, "Waiting for PONG");
-        furi_delay_ms(100); // this causes a BusFault
+        furi_delay_ms(100);
     }
     }
-    flipper_http_free(fhttp);
 
 
     if (counter == 0)
     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.");
         easy_flipper_dialog("FlipperHTTP Error", "Ensure your WiFi Developer\nBoard or Pico W is connected\nand the latest FlipperHTTP\nfirmware is installed.");
 
 
     // save app version
     // save app version
-    char app_version[16];
-    snprintf(app_version, sizeof(app_version), "%f", (double)VERSION);
-    save_char("app_version", app_version);
+    // char app_version[16];
+    // snprintf(app_version, sizeof(app_version), "%f", (double)VERSION);
+    save_char("app_version", VERSION);
+
+    // for now use the catalog API until I implement caching on the server
+
+    if (flip_world_handle_app_update(fhttp, true))
+    {
+        easy_flipper_dialog("Update Status", "Complete.\nRestart your Flipper Zero.");
+    }
+
+    flipper_http_free(fhttp);
 
 
     // Run the view dispatcher
     // Run the view dispatcher
     view_dispatcher_run(app->view_dispatcher);
     view_dispatcher_run(app->view_dispatcher);

+ 1 - 1
application.fam

@@ -17,5 +17,5 @@ App(
     ),
     ),
     fap_author="JBlanked",
     fap_author="JBlanked",
     fap_weburl="https://github.com/jblanked/FlipWorld",
     fap_weburl="https://github.com/jblanked/FlipWorld",
-    fap_version="0.8",
+    fap_version="0.8.1",
 )
 )

BIN
assets/01-home.png


+ 20 - 15
assets/CHANGELOG.md

@@ -1,50 +1,55 @@
+## 0.8.1 (2025-04-09)
+- Improved memory allocation, saving 26 KB.
+- Added auto-updating (currently uses the catalog API).
+- Major code refactoring.
+
 ## 0.8 (2025-04-05)
 ## 0.8 (2025-04-05)
 - Improved memory allocation.
 - Improved memory allocation.
 - Added multiplayer support.
 - Added multiplayer support.
-- Updated the default vibration/sound settings.
+- Updated the default settings for vibration and sound.
 
 
 ## 0.7 (2025-03-21)
 ## 0.7 (2025-03-21)
 - Sped up player movement.
 - Sped up player movement.
-- Added a Tutorial mode.
-- Fixed transition of worlds.
+- Added tutorial mode.
+- Fixed issues with transitioning between worlds.
 
 
 ## 0.6.1 (2025-03-15)
 ## 0.6.1 (2025-03-15)
-- Switched the server backend to prepare for multiplayer in version 0.8.
+- Switched the server backend in preparation for multiplayer features introduced in version 0.8.
 
 
 ## 0.6 (2025-03-10)
 ## 0.6 (2025-03-10)
-- Fixed saving of player attributes so that it works as intended.
-- Updated the player's level and strength as XP increases.
+- Fixed saving of player attributes to ensure they are stored as intended.
+- Updated the player's level and strength progression with XP increases.
 - Started implementing multiplayer (requires FlipperHTTP v1.7).
 - Started implementing multiplayer (requires FlipperHTTP v1.7).
 - Fixed the display of user stats when switching worlds.
 - Fixed the display of user stats when switching worlds.
 
 
 ## 0.5 (2025-01-31)
 ## 0.5 (2025-01-31)
-- Fixed saving errors.
+- Fixed errors with saving.
 - Improved memory allocation.
 - Improved memory allocation.
 - Added NPCs.
 - Added NPCs.
 
 
 ## 0.4 (2025-01-23)
 ## 0.4 (2025-01-23)
-- Added an In-Game menu.
-- Added new controls (HOLD OK to access the In-Game menu, PRESS BACK to exit the menu, and HOLD BACK to leave the game).
-- Added option to choose player's weapon in the Game Settings.
-- Added transition icon for switching worlds.
+- Added an in-game menu.
+- Added new controls: hold OK to access the in-game menu, press BACK to exit the menu, and hold BACK to exit the game.
+- Added an option to choose the player's weapon in the game settings.
+- Added a transition icon for switching worlds.
 - Doubled the size of each world (from 384x192 to 768x384).
 - Doubled the size of each world (from 384x192 to 768x384).
 - Improved memory allocation.
 - Improved memory allocation.
 
 
 ## 0.3 (2025-01-14)
 ## 0.3 (2025-01-14)
 - Added new worlds.
 - Added new worlds.
 - Improved memory allocation.
 - Improved memory allocation.
-- Updated API integration to load and save player attributes.
+- Updated API integration for loading and saving player attributes.
 - Upgraded FlipperHTTP to the latest version.
 - Upgraded FlipperHTTP to the latest version.
 
 
 ## 0.2 (2025-01-02)
 ## 0.2 (2025-01-02)
-- Added support for the Video Game Module (requires a FlipperHTTP flash).
+- Added support for the Video Game Module (requires FlipperHTTP flash).
 - Introduced various enemy types to enhance gameplay.
 - Introduced various enemy types to enhance gameplay.
 - Added features for player health, XP, level, health regeneration, attack, and strength.
 - Added features for player health, XP, level, health regeneration, attack, and strength.
 - Implemented vibration, sound, and LED notifications when a player is attacking or being attacked.
 - Implemented vibration, sound, and LED notifications when a player is attacking or being attacked.
-- 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 at all times.
+- Displayed the player's username above the character and continuously showed health, XP, and level in the bottom-left corner of the screen.
 - Updated all game icons for improved visual appeal.
 - Updated all game icons for improved visual appeal.
 - Upgraded to the latest version of the FlipperHTTP library.
 - Upgraded to the latest version of the FlipperHTTP library.
-- Revised toggles in the Game Settings to ensure they work as intended.
+- Revised toggles in the game settings to ensure they work as intended.
 - Improved collision mechanics for more accurate interactions.
 - Improved collision mechanics for more accurate interactions.
 - Updated the default icon representing the player's character.
 - Updated the default icon representing the player's character.
 
 

+ 17 - 21
callback/alloc.c

@@ -15,13 +15,13 @@ bool alloc_message_view(void *context, MessageState state)
     switch (state)
     switch (state)
     {
     {
     case MessageStateAbout:
     case MessageStateAbout:
-        easy_flipper_set_view(&app->view_message, FlipWorldViewMessage, message_draw_callback, NULL, callback_to_submenu, &app->view_dispatcher, app);
+        easy_flipper_set_view(&app->view_message, FlipWorldViewMessage, callback_message_draw, NULL, callback_to_submenu, &app->view_dispatcher, app);
         break;
         break;
     case MessageStateLoading:
     case MessageStateLoading:
-        easy_flipper_set_view(&app->view_message, FlipWorldViewMessage, message_draw_callback, NULL, NULL, &app->view_dispatcher, app);
+        easy_flipper_set_view(&app->view_message, FlipWorldViewMessage, callback_message_draw, NULL, NULL, &app->view_dispatcher, app);
         break;
         break;
     case MessageStateWaitingLobby:
     case MessageStateWaitingLobby:
-        easy_flipper_set_view(&app->view_message, FlipWorldViewMessage, message_draw_callback, message_input_callback, NULL, &app->view_dispatcher, app);
+        easy_flipper_set_view(&app->view_message, FlipWorldViewMessage, callback_message_draw, callback_message_input, NULL, &app->view_dispatcher, app);
         break;
         break;
     }
     }
     if (!app->view_message)
     if (!app->view_message)
@@ -67,9 +67,9 @@ bool alloc_text_input_view(void *context, char *title)
                 title,
                 title,
                 app->text_input_temp_buffer,
                 app->text_input_temp_buffer,
                 app->text_input_buffer_size,
                 app->text_input_buffer_size,
-                is_str(title, "SSID") ? updated_wifi_ssid : is_str(title, "Password")     ? updated_wifi_pass
-                                                        : is_str(title, "Username-Login") ? updated_username
-                                                                                          : updated_password,
+                is_str(title, "SSID") ? callback_updated_wifi_ssid : is_str(title, "Password")     ? callback_updated_wifi_pass
+                                                                 : is_str(title, "Username-Login") ? callback_updated_username
+                                                                                                   : callback_updated_password,
                 callback_to_wifi_settings,
                 callback_to_wifi_settings,
                 &app->view_dispatcher,
                 &app->view_dispatcher,
                 app))
                 app))
@@ -109,11 +109,7 @@ bool alloc_text_input_view(void *context, char *title)
 bool alloc_variable_item_list(void *context, uint32_t view_id)
 bool alloc_variable_item_list(void *context, uint32_t view_id)
 {
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
     FlipWorldApp *app = (FlipWorldApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWorldApp is NULL");
-        return false;
-    }
+    furi_check(app, "FlipWorldApp is NULL");
     char ssid[64];
     char ssid[64];
     char pass[64];
     char pass[64];
     char username[64];
     char username[64];
@@ -123,7 +119,7 @@ bool alloc_variable_item_list(void *context, uint32_t view_id)
         switch (view_id)
         switch (view_id)
         {
         {
         case FlipWorldSubmenuIndexWiFiSettings:
         case FlipWorldSubmenuIndexWiFiSettings:
-            if (!easy_flipper_set_variable_item_list(&app->variable_item_list, FlipWorldViewVariableItemList, wifi_settings_select, callback_to_settings, &app->view_dispatcher, app))
+            if (!easy_flipper_set_variable_item_list(&app->variable_item_list, FlipWorldViewVariableItemList, callback_wifi_settings_select, callback_to_settings, &app->view_dispatcher, app))
             {
             {
                 FURI_LOG_E(TAG, "Failed to allocate variable item list");
                 FURI_LOG_E(TAG, "Failed to allocate variable item list");
                 return false;
                 return false;
@@ -156,7 +152,7 @@ bool alloc_variable_item_list(void *context, uint32_t view_id)
             }
             }
             break;
             break;
         case FlipWorldSubmenuIndexGameSettings:
         case FlipWorldSubmenuIndexGameSettings:
-            if (!easy_flipper_set_variable_item_list(&app->variable_item_list, FlipWorldViewVariableItemList, game_settings_select, callback_to_settings, &app->view_dispatcher, app))
+            if (!easy_flipper_set_variable_item_list(&app->variable_item_list, FlipWorldViewVariableItemList, callback_game_settings_select, callback_to_settings, &app->view_dispatcher, app))
             {
             {
                 FURI_LOG_E(TAG, "Failed to allocate variable item list");
                 FURI_LOG_E(TAG, "Failed to allocate variable item list");
                 return false;
                 return false;
@@ -175,43 +171,43 @@ bool alloc_variable_item_list(void *context, uint32_t view_id)
             }
             }
             if (!app->variable_item_game_player_sprite)
             if (!app->variable_item_game_player_sprite)
             {
             {
-                app->variable_item_game_player_sprite = variable_item_list_add(app->variable_item_list, "Weapon", 4, player_on_change, NULL);
+                app->variable_item_game_player_sprite = variable_item_list_add(app->variable_item_list, "Weapon", 4, callback_player_on_change, NULL);
                 variable_item_set_current_value_index(app->variable_item_game_player_sprite, 1);
                 variable_item_set_current_value_index(app->variable_item_game_player_sprite, 1);
                 variable_item_set_current_value_text(app->variable_item_game_player_sprite, player_sprite_choices[1]);
                 variable_item_set_current_value_text(app->variable_item_game_player_sprite, player_sprite_choices[1]);
             }
             }
             if (!app->variable_item_game_fps)
             if (!app->variable_item_game_fps)
             {
             {
-                app->variable_item_game_fps = variable_item_list_add(app->variable_item_list, "FPS", 4, fps_change, NULL);
+                app->variable_item_game_fps = variable_item_list_add(app->variable_item_list, "FPS", 4, callback_fps_change, NULL);
                 variable_item_set_current_value_index(app->variable_item_game_fps, 0);
                 variable_item_set_current_value_index(app->variable_item_game_fps, 0);
                 variable_item_set_current_value_text(app->variable_item_game_fps, fps_choices_str[0]);
                 variable_item_set_current_value_text(app->variable_item_game_fps, fps_choices_str[0]);
             }
             }
             if (!app->variable_item_game_vgm_x)
             if (!app->variable_item_game_vgm_x)
             {
             {
-                app->variable_item_game_vgm_x = variable_item_list_add(app->variable_item_list, "VGM Horizontal", 12, vgm_x_change, NULL);
+                app->variable_item_game_vgm_x = variable_item_list_add(app->variable_item_list, "VGM Horizontal", 12, callback_vgm_x_change, NULL);
                 variable_item_set_current_value_index(app->variable_item_game_vgm_x, 2);
                 variable_item_set_current_value_index(app->variable_item_game_vgm_x, 2);
                 variable_item_set_current_value_text(app->variable_item_game_vgm_x, vgm_levels[2]);
                 variable_item_set_current_value_text(app->variable_item_game_vgm_x, vgm_levels[2]);
             }
             }
             if (!app->variable_item_game_vgm_y)
             if (!app->variable_item_game_vgm_y)
             {
             {
-                app->variable_item_game_vgm_y = variable_item_list_add(app->variable_item_list, "VGM Vertical", 12, vgm_y_change, NULL);
+                app->variable_item_game_vgm_y = variable_item_list_add(app->variable_item_list, "VGM Vertical", 12, callback_vgm_y_change, NULL);
                 variable_item_set_current_value_index(app->variable_item_game_vgm_y, 2);
                 variable_item_set_current_value_index(app->variable_item_game_vgm_y, 2);
                 variable_item_set_current_value_text(app->variable_item_game_vgm_y, vgm_levels[2]);
                 variable_item_set_current_value_text(app->variable_item_game_vgm_y, vgm_levels[2]);
             }
             }
             if (!app->variable_item_game_screen_always_on)
             if (!app->variable_item_game_screen_always_on)
             {
             {
-                app->variable_item_game_screen_always_on = variable_item_list_add(app->variable_item_list, "Keep Screen On?", 2, screen_on_change, NULL);
+                app->variable_item_game_screen_always_on = variable_item_list_add(app->variable_item_list, "Keep Screen On?", 2, callback_screen_on_change, NULL);
                 variable_item_set_current_value_index(app->variable_item_game_screen_always_on, 1);
                 variable_item_set_current_value_index(app->variable_item_game_screen_always_on, 1);
                 variable_item_set_current_value_text(app->variable_item_game_screen_always_on, yes_or_no_choices[1]);
                 variable_item_set_current_value_text(app->variable_item_game_screen_always_on, yes_or_no_choices[1]);
             }
             }
             if (!app->variable_item_game_sound_on)
             if (!app->variable_item_game_sound_on)
             {
             {
-                app->variable_item_game_sound_on = variable_item_list_add(app->variable_item_list, "Sound On?", 2, sound_on_change, NULL);
+                app->variable_item_game_sound_on = variable_item_list_add(app->variable_item_list, "Sound On?", 2, callback_sound_on_change, NULL);
                 variable_item_set_current_value_index(app->variable_item_game_sound_on, 0);
                 variable_item_set_current_value_index(app->variable_item_game_sound_on, 0);
                 variable_item_set_current_value_text(app->variable_item_game_sound_on, yes_or_no_choices[0]);
                 variable_item_set_current_value_text(app->variable_item_game_sound_on, yes_or_no_choices[0]);
             }
             }
             if (!app->variable_item_game_vibration_on)
             if (!app->variable_item_game_vibration_on)
             {
             {
-                app->variable_item_game_vibration_on = variable_item_list_add(app->variable_item_list, "Vibration On?", 2, vibration_on_change, NULL);
+                app->variable_item_game_vibration_on = variable_item_list_add(app->variable_item_list, "Vibration On?", 2, callback_vibration_on_change, NULL);
                 variable_item_set_current_value_index(app->variable_item_game_vibration_on, 0);
                 variable_item_set_current_value_index(app->variable_item_game_vibration_on, 0);
                 variable_item_set_current_value_text(app->variable_item_game_vibration_on, yes_or_no_choices[0]);
                 variable_item_set_current_value_text(app->variable_item_game_vibration_on, yes_or_no_choices[0]);
             }
             }
@@ -303,7 +299,7 @@ bool alloc_variable_item_list(void *context, uint32_t view_id)
             }
             }
             break;
             break;
         case FlipWorldSubmenuIndexUserSettings:
         case FlipWorldSubmenuIndexUserSettings:
-            if (!easy_flipper_set_variable_item_list(&app->variable_item_list, FlipWorldViewVariableItemList, user_settings_select, callback_to_settings, &app->view_dispatcher, app))
+            if (!easy_flipper_set_variable_item_list(&app->variable_item_list, FlipWorldViewVariableItemList, callback_user_settings_select, callback_to_settings, &app->view_dispatcher, app))
             {
             {
                 FURI_LOG_E(TAG, "Failed to allocate variable item list");
                 FURI_LOG_E(TAG, "Failed to allocate variable item list");
                 return false;
                 return false;

+ 125 - 55
callback/callback.c

@@ -6,7 +6,7 @@
 #include "alloc/alloc.h"
 #include "alloc/alloc.h"
 #include <flip_storage/storage.h>
 #include <flip_storage/storage.h>
 
 
-bool message_input_callback(InputEvent *event, void *context)
+bool callback_message_input(InputEvent *event, void *context)
 {
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
     FlipWorldApp *app = (FlipWorldApp *)context;
     furi_check(app);
     furi_check(app);
@@ -18,7 +18,7 @@ bool message_input_callback(InputEvent *event, void *context)
     return true;
     return true;
 }
 }
 
 
-void message_draw_callback(Canvas *canvas, void *model)
+void callback_message_draw(Canvas *canvas, void *model)
 {
 {
     MessageModel *message_model = model;
     MessageModel *message_model = model;
     canvas_clear(canvas);
     canvas_clear(canvas);
@@ -83,15 +83,15 @@ void callback_submenu_choices(void *context, uint32_t index)
         break;
         break;
     case FlipWorldSubmenuIndexStory:
     case FlipWorldSubmenuIndexStory:
         game_mode_index = 2; // GAME_MODE_STORY
         game_mode_index = 2; // GAME_MODE_STORY
-        run(app);
+        game_run(app);
         break;
         break;
     case FlipWorldSubmenuIndexPvP:
     case FlipWorldSubmenuIndexPvP:
         game_mode_index = 1; // GAME_MODE_PVP
         game_mode_index = 1; // GAME_MODE_PVP
-        run(app);
+        game_run(app);
         break;
         break;
     case FlipWorldSubmenuIndexPvE:
     case FlipWorldSubmenuIndexPvE:
         game_mode_index = 0; // GAME_MODE_PVE
         game_mode_index = 0; // GAME_MODE_PVE
-        run(app);
+        game_run(app);
         break;
         break;
     case FlipWorldSubmenuIndexMessage:
     case FlipWorldSubmenuIndexMessage:
         // About menu.
         // About menu.
@@ -145,14 +145,10 @@ void callback_submenu_choices(void *context, uint32_t index)
     }
     }
 }
 }
 
 
-void updated_wifi_ssid(void *context)
+void callback_updated_wifi_ssid(void *context)
 {
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
     FlipWorldApp *app = (FlipWorldApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWorldApp is NULL");
-        return;
-    }
+    furi_check(app, "FlipWorldApp is NULL");
 
 
     // store the entered text
     // store the entered text
     strncpy(app->text_input_buffer, app->text_input_temp_buffer, app->text_input_buffer_size);
     strncpy(app->text_input_buffer, app->text_input_temp_buffer, app->text_input_buffer_size);
@@ -205,14 +201,10 @@ void updated_wifi_ssid(void *context)
     // switch to the settings view
     // switch to the settings view
     view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewVariableItemList);
     view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewVariableItemList);
 }
 }
-void updated_wifi_pass(void *context)
+void callback_updated_wifi_pass(void *context)
 {
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
     FlipWorldApp *app = (FlipWorldApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWorldApp is NULL");
-        return;
-    }
+    furi_check(app, "FlipWorldApp is NULL");
 
 
     // store the entered text
     // store the entered text
     strncpy(app->text_input_buffer, app->text_input_temp_buffer, app->text_input_buffer_size);
     strncpy(app->text_input_buffer, app->text_input_temp_buffer, app->text_input_buffer_size);
@@ -265,14 +257,10 @@ void updated_wifi_pass(void *context)
     // switch to the settings view
     // switch to the settings view
     view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewVariableItemList);
     view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewVariableItemList);
 }
 }
-void updated_username(void *context)
+void callback_updated_username(void *context)
 {
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
     FlipWorldApp *app = (FlipWorldApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWorldApp is NULL");
-        return;
-    }
+    furi_check(app, "FlipWorldApp is NULL");
 
 
     // store the entered text
     // store the entered text
     strncpy(app->text_input_buffer, app->text_input_temp_buffer, app->text_input_buffer_size);
     strncpy(app->text_input_buffer, app->text_input_temp_buffer, app->text_input_buffer_size);
@@ -290,14 +278,10 @@ void updated_username(void *context)
     }
     }
     view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewVariableItemList); // back to user settings
     view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewVariableItemList); // back to user settings
 }
 }
-void updated_password(void *context)
+void callback_updated_password(void *context)
 {
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
     FlipWorldApp *app = (FlipWorldApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWorldApp is NULL");
-        return;
-    }
+    furi_check(app, "FlipWorldApp is NULL");
 
 
     // store the entered text
     // store the entered text
     strncpy(app->text_input_buffer, app->text_input_temp_buffer, app->text_input_buffer_size);
     strncpy(app->text_input_buffer, app->text_input_temp_buffer, app->text_input_buffer_size);
@@ -331,14 +315,10 @@ void updated_password(void *context)
     view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewVariableItemList); // back to user settings
     view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewVariableItemList); // back to user settings
 }
 }
 
 
-void wifi_settings_select(void *context, uint32_t index)
+void callback_wifi_settings_select(void *context, uint32_t index)
 {
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
     FlipWorldApp *app = (FlipWorldApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWorldApp is NULL");
-        return;
-    }
+    furi_check(app, "FlipWorldApp is NULL");
     char ssid[64];
     char ssid[64];
     char pass[64];
     char pass[64];
     char username[64];
     char username[64];
@@ -380,7 +360,7 @@ void wifi_settings_select(void *context, uint32_t index)
         break;
         break;
     }
     }
 }
 }
-void fps_change(VariableItem *item)
+void callback_fps_change(VariableItem *item)
 {
 {
     uint8_t index = variable_item_get_current_value_index(item);
     uint8_t index = variable_item_get_current_value_index(item);
     fps_index = index;
     fps_index = index;
@@ -388,7 +368,7 @@ void fps_change(VariableItem *item)
     variable_item_set_current_value_index(item, index);
     variable_item_set_current_value_index(item, index);
     save_char("Game-FPS", fps_choices_str[index]);
     save_char("Game-FPS", fps_choices_str[index]);
 }
 }
-void screen_on_change(VariableItem *item)
+void callback_screen_on_change(VariableItem *item)
 {
 {
     uint8_t index = variable_item_get_current_value_index(item);
     uint8_t index = variable_item_get_current_value_index(item);
     screen_always_on_index = index;
     screen_always_on_index = index;
@@ -396,7 +376,7 @@ void screen_on_change(VariableItem *item)
     variable_item_set_current_value_index(item, index);
     variable_item_set_current_value_index(item, index);
     save_char("Game-Screen-Always-On", yes_or_no_choices[index]);
     save_char("Game-Screen-Always-On", yes_or_no_choices[index]);
 }
 }
-void sound_on_change(VariableItem *item)
+void callback_sound_on_change(VariableItem *item)
 {
 {
     uint8_t index = variable_item_get_current_value_index(item);
     uint8_t index = variable_item_get_current_value_index(item);
     sound_on_index = index;
     sound_on_index = index;
@@ -404,7 +384,7 @@ void sound_on_change(VariableItem *item)
     variable_item_set_current_value_index(item, index);
     variable_item_set_current_value_index(item, index);
     save_char("Game-Sound-On", yes_or_no_choices[index]);
     save_char("Game-Sound-On", yes_or_no_choices[index]);
 }
 }
-void vibration_on_change(VariableItem *item)
+void callback_vibration_on_change(VariableItem *item)
 {
 {
     uint8_t index = variable_item_get_current_value_index(item);
     uint8_t index = variable_item_get_current_value_index(item);
     vibration_on_index = index;
     vibration_on_index = index;
@@ -412,7 +392,7 @@ void vibration_on_change(VariableItem *item)
     variable_item_set_current_value_index(item, index);
     variable_item_set_current_value_index(item, index);
     save_char("Game-Vibration-On", yes_or_no_choices[index]);
     save_char("Game-Vibration-On", yes_or_no_choices[index]);
 }
 }
-void player_on_change(VariableItem *item)
+void callback_player_on_change(VariableItem *item)
 {
 {
     uint8_t index = variable_item_get_current_value_index(item);
     uint8_t index = variable_item_get_current_value_index(item);
     player_sprite_index = index;
     player_sprite_index = index;
@@ -420,7 +400,7 @@ void player_on_change(VariableItem *item)
     variable_item_set_current_value_index(item, index);
     variable_item_set_current_value_index(item, index);
     save_char("Game-Player-Sprite", player_sprite_choices[index]);
     save_char("Game-Player-Sprite", player_sprite_choices[index]);
 }
 }
-void vgm_x_change(VariableItem *item)
+void callback_vgm_x_change(VariableItem *item)
 {
 {
     uint8_t index = variable_item_get_current_value_index(item);
     uint8_t index = variable_item_get_current_value_index(item);
     vgm_x_index = index;
     vgm_x_index = index;
@@ -428,7 +408,7 @@ void vgm_x_change(VariableItem *item)
     variable_item_set_current_value_index(item, index);
     variable_item_set_current_value_index(item, index);
     save_char("Game-VGM-X", vgm_levels[index]);
     save_char("Game-VGM-X", vgm_levels[index]);
 }
 }
-void vgm_y_change(VariableItem *item)
+void callback_vgm_y_change(VariableItem *item)
 {
 {
     uint8_t index = variable_item_get_current_value_index(item);
     uint8_t index = variable_item_get_current_value_index(item);
     vgm_y_index = index;
     vgm_y_index = index;
@@ -460,7 +440,7 @@ static char *_parse_worlds(DataLoaderModel *model)
     UNUSED(model);
     UNUSED(model);
     return "World Pack Installed";
     return "World Pack Installed";
 }
 }
-void switch_to_view_get_worlds(FlipWorldApp *app)
+static void switch_to_view_get_worlds(FlipWorldApp *app)
 {
 {
     if (!loader_view_alloc(app))
     if (!loader_view_alloc(app))
     {
     {
@@ -469,14 +449,10 @@ void switch_to_view_get_worlds(FlipWorldApp *app)
     }
     }
     loader_switch_to_view(app, "Fetching World Pack..", _fetch_worlds, _parse_worlds, 1, callback_to_submenu, FlipWorldViewLoader);
     loader_switch_to_view(app, "Fetching World Pack..", _fetch_worlds, _parse_worlds, 1, callback_to_submenu, FlipWorldViewLoader);
 }
 }
-void game_settings_select(void *context, uint32_t index)
+void callback_game_settings_select(void *context, uint32_t index)
 {
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
     FlipWorldApp *app = (FlipWorldApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWorldApp is NULL");
-        return;
-    }
+    furi_check(app, "FlipWorldApp is NULL");
     switch (index)
     switch (index)
     {
     {
     case 0: // Download all world data as one huge json
     case 0: // Download all world data as one huge json
@@ -497,14 +473,10 @@ void game_settings_select(void *context, uint32_t index)
         break;
         break;
     }
     }
 }
 }
-void user_settings_select(void *context, uint32_t index)
+void callback_user_settings_select(void *context, uint32_t index)
 {
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
     FlipWorldApp *app = (FlipWorldApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWorldApp is NULL");
-        return;
-    }
+    furi_check(app, "FlipWorldApp is NULL");
     switch (index)
     switch (index)
     {
     {
     case 0: // Username
     case 0: // Username
@@ -526,4 +498,102 @@ void user_settings_select(void *context, uint32_t index)
         view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewTextInput);
         view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewTextInput);
         break;
         break;
     }
     }
+}
+
+void callback_submenu_lobby_choices(void *context, uint32_t index)
+{
+    /* Handle other game lobbies
+             1. when clicked on, send request to fetch the selected game lobby details
+             2. start the websocket session
+             3. start the game thread (the rest will be handled by game_start and player_update)
+             */
+    FlipWorldApp *app = (FlipWorldApp *)context;
+    furi_check(app, "FlipWorldApp is NULL");
+    if (index >= FlipWorldSubmenuIndexLobby && index < FlipWorldSubmenuIndexLobby + 10)
+    {
+        lobby_index = index - FlipWorldSubmenuIndexLobby;
+
+        FlipperHTTP *fhttp = flipper_http_alloc();
+        if (!fhttp)
+        {
+            FURI_LOG_E(TAG, "Failed to allocate FlipperHTTP");
+            easy_flipper_dialog("Error", "Failed to allocate FlipperHTTP. Press BACK to return.");
+            return;
+        }
+
+        // fetch the lobby details
+        if (!game_fetch_lobby(fhttp, lobby_list[lobby_index]))
+        {
+            FURI_LOG_E(TAG, "Failed to fetch lobby details");
+            easy_flipper_dialog("Error", "Failed to fetch lobby details. Press BACK to return.");
+            flipper_http_free(fhttp);
+            return;
+        }
+
+        // load the lobby details
+        FuriString *lobby = flipper_http_load_from_file(fhttp->file_path);
+        if (!lobby)
+        {
+            FURI_LOG_E(TAG, "Failed to load lobby details");
+            flipper_http_free(fhttp);
+            return;
+        }
+
+        // if there are no players, add the user to the lobby and make the user wait until another player joins
+        // if there is one player and it's the user, make the user wait until another player joins
+        // if there is one player and it's not the user, parse_lobby and start websocket
+        // if there are 2 players (which there shouldn't be at this point), show an error message saying the lobby is full
+        switch (game_lobby_count(fhttp, lobby))
+        {
+        case -1:
+            FURI_LOG_E(TAG, "Failed to get player count");
+            easy_flipper_dialog("Error", "Failed to get player count. Press BACK to return.");
+            flipper_http_free(fhttp);
+            furi_string_free(lobby);
+            return;
+        case 0:
+            // add the user to the lobby
+            if (!game_join_lobby(fhttp, lobby_list[lobby_index]))
+            {
+                FURI_LOG_E(TAG, "Failed to join lobby");
+                easy_flipper_dialog("Error", "Failed to join lobby. Press BACK to return.");
+                flipper_http_free(fhttp);
+                furi_string_free(lobby);
+                return;
+            }
+            // send the user to the waiting screen
+            game_waiting_lobby(app);
+            return;
+        case 1:
+            // check if the user is in the lobby
+            if (game_in_lobby(fhttp, lobby))
+            {
+                // send the user to the waiting screen
+                FURI_LOG_I(TAG, "User is in the lobby");
+                flipper_http_free(fhttp);
+                furi_string_free(lobby);
+                game_waiting_lobby(app);
+                return;
+            }
+            // add the user to the lobby
+            if (!game_join_lobby(fhttp, lobby_list[lobby_index]))
+            {
+                FURI_LOG_E(TAG, "Failed to join lobby");
+                easy_flipper_dialog("Error", "Failed to join lobby. Press BACK to return.");
+                flipper_http_free(fhttp);
+                furi_string_free(lobby);
+                return;
+            }
+            break;
+        case 2:
+            // show an error message saying the lobby is full
+            FURI_LOG_E(TAG, "Lobby is full");
+            easy_flipper_dialog("Error", "Lobby is full. Press BACK to return.");
+            flipper_http_free(fhttp);
+            furi_string_free(lobby);
+            return;
+        };
+
+        game_start_pvp(fhttp, lobby, app); // this will free both the fhttp and lobby, and start the game
+    }
 }
 }

+ 17 - 16
callback/callback.h

@@ -2,19 +2,20 @@
 #include <flip_world.h>
 #include <flip_world.h>
 
 
 void callback_submenu_choices(void *context, uint32_t index);
 void callback_submenu_choices(void *context, uint32_t index);
-bool message_input_callback(InputEvent *event, void *context);
-void message_draw_callback(Canvas *canvas, void *model);
-void wifi_settings_select(void *context, uint32_t index);
-void updated_wifi_ssid(void *context);
-void updated_wifi_pass(void *context);
-void updated_username(void *context);
-void updated_password(void *context);
-void fps_change(VariableItem *item);
-void game_settings_select(void *context, uint32_t index);
-void user_settings_select(void *context, uint32_t index);
-void screen_on_change(VariableItem *item);
-void sound_on_change(VariableItem *item);
-void vibration_on_change(VariableItem *item);
-void player_on_change(VariableItem *item);
-void vgm_x_change(VariableItem *item);
-void vgm_y_change(VariableItem *item);
+bool callback_message_input(InputEvent *event, void *context);
+void callback_message_draw(Canvas *canvas, void *model);
+void callback_wifi_settings_select(void *context, uint32_t index);
+void callback_updated_wifi_ssid(void *context);
+void callback_updated_wifi_pass(void *context);
+void callback_updated_username(void *context);
+void callback_updated_password(void *context);
+void callback_fps_change(VariableItem *item);
+void callback_game_settings_select(void *context, uint32_t index);
+void callback_user_settings_select(void *context, uint32_t index);
+void callback_screen_on_change(VariableItem *item);
+void callback_sound_on_change(VariableItem *item);
+void callback_vibration_on_change(VariableItem *item);
+void callback_player_on_change(VariableItem *item);
+void callback_vgm_x_change(VariableItem *item);
+void callback_vgm_y_change(VariableItem *item);
+void callback_submenu_lobby_choices(void *context, uint32_t index);

+ 54 - 218
callback/game.c

@@ -1,13 +1,17 @@
 #include <callback/game.h>
 #include <callback/game.h>
+//
 #include "engine/engine.h"
 #include "engine/engine.h"
 #include "engine/game_engine.h"
 #include "engine/game_engine.h"
 #include "engine/game_manager_i.h"
 #include "engine/game_manager_i.h"
 #include "engine/level_i.h"
 #include "engine/level_i.h"
 #include "engine/entity_i.h"
 #include "engine/entity_i.h"
+//
 #include "game/storage.h"
 #include "game/storage.h"
+//
 #include <callback/loader.h>
 #include <callback/loader.h>
 #include <callback/free.h>
 #include <callback/free.h>
 #include <callback/alloc.h>
 #include <callback/alloc.h>
+#include <callback/callback.h>
 #include "alloc/alloc.h"
 #include "alloc/alloc.h"
 #include <flip_storage/storage.h>
 #include <flip_storage/storage.h>
 
 
@@ -17,19 +21,13 @@ char *lobby_list[10];
 
 
 static uint8_t timer_iteration = 0; // timer iteration for the loading screen
 static uint8_t timer_iteration = 0; // timer iteration for the loading screen
 static uint8_t timer_refresh = 5;   // duration for timer to refresh
 static uint8_t timer_refresh = 5;   // duration for timer to refresh
-//
-static void waiting_loader_process_callback(FlipperHTTP *fhttp, void *context);
-static void waiting_lobby(void *context);
-static bool fetch_lobby(FlipperHTTP *fhttp, char *lobby_name);
-//
+
 FuriThread *game_thread = NULL;
 FuriThread *game_thread = NULL;
 FuriThread *waiting_thread = NULL;
 FuriThread *waiting_thread = NULL;
 bool game_thread_running = false;
 bool game_thread_running = false;
 bool waiting_thread_running = false;
 bool waiting_thread_running = false;
-//
-static void callback_submenu_lobby_choices(void *context, uint32_t index);
 
 
-static void frame_cb(GameEngine *engine, Canvas *canvas, InputState input, void *context)
+static void game_frame_cb(GameEngine *engine, Canvas *canvas, InputState input, void *context)
 {
 {
     UNUSED(engine);
     UNUSED(engine);
     GameManager *game_manager = context;
     GameManager *game_manager = context;
@@ -53,7 +51,7 @@ static int32_t game_app(void *p)
     settings.target_fps = atof_(fps_choices_str[fps_index]);
     settings.target_fps = atof_(fps_choices_str[fps_index]);
     settings.show_fps = game.show_fps;
     settings.show_fps = game.show_fps;
     settings.always_backlight = strstr(yes_or_no_choices[screen_always_on_index], "Yes") != NULL;
     settings.always_backlight = strstr(yes_or_no_choices[screen_always_on_index], "Yes") != NULL;
-    settings.frame_callback = frame_cb;
+    settings.frame_callback = game_frame_cb;
     settings.context = game_manager;
     settings.context = game_manager;
     GameEngine *engine = game_engine_alloc(settings);
     GameEngine *engine = game_engine_alloc(settings);
     if (!engine)
     if (!engine)
@@ -104,7 +102,7 @@ static int32_t game_app(void *p)
     return 0;
     return 0;
 }
 }
 
 
-static int32_t waiting_app_callback(void *p)
+static int32_t game_waiting_app_callback(void *p)
 {
 {
     FlipWorldApp *app = (FlipWorldApp *)p;
     FlipWorldApp *app = (FlipWorldApp *)p;
     furi_check(app);
     furi_check(app);
@@ -120,7 +118,7 @@ static int32_t waiting_app_callback(void *p)
     while (timer_iteration < 60 && !user_hit_back)
     while (timer_iteration < 60 && !user_hit_back)
     {
     {
         FURI_LOG_I(TAG, "Waiting for more players...");
         FURI_LOG_I(TAG, "Waiting for more players...");
-        waiting_loader_process_callback(fhttp, app);
+        game_waiting_process(fhttp, app);
         FURI_LOG_I(TAG, "Waiting for more players... %d", timer_iteration);
         FURI_LOG_I(TAG, "Waiting for more players... %d", timer_iteration);
         timer_iteration++;
         timer_iteration++;
         furi_delay_ms(1000 * timer_refresh);
         furi_delay_ms(1000 * timer_refresh);
@@ -133,7 +131,7 @@ static int32_t waiting_app_callback(void *p)
     return 0;
     return 0;
 }
 }
 
 
-static bool start_waiting_thread(void *context)
+static bool game_start_waiting_thread(void *context)
 {
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
     FlipWorldApp *app = (FlipWorldApp *)context;
     furi_check(app);
     furi_check(app);
@@ -149,7 +147,7 @@ static bool start_waiting_thread(void *context)
         }
         }
     }
     }
     // start waiting thread
     // start waiting thread
-    FuriThread *thread = furi_thread_alloc_ex("waiting_thread", 2048, waiting_app_callback, app);
+    FuriThread *thread = furi_thread_alloc_ex("waiting_thread", 2048, game_waiting_app_callback, app);
     if (!thread)
     if (!thread)
     {
     {
         FURI_LOG_E(TAG, "Failed to allocate waiting thread");
         FURI_LOG_E(TAG, "Failed to allocate waiting thread");
@@ -162,7 +160,7 @@ static bool start_waiting_thread(void *context)
     return true;
     return true;
 }
 }
 
 
-static bool fetch_world_list(FlipperHTTP *fhttp)
+static bool game_fetch_world_list(FlipperHTTP *fhttp)
 {
 {
     if (!fhttp)
     if (!fhttp)
     {
     {
@@ -187,7 +185,7 @@ static bool fetch_world_list(FlipperHTTP *fhttp)
 }
 }
 // we will load the palyer stats from the API and save them
 // we will load the palyer stats from the API and save them
 // in player_spawn game method, it will load the player stats that we saved
 // in player_spawn game method, it will load the player stats that we saved
-static bool fetch_player_stats(FlipperHTTP *fhttp)
+static bool game_fetch_player_stats(FlipperHTTP *fhttp)
 {
 {
     if (!fhttp)
     if (!fhttp)
     {
     {
@@ -221,62 +219,7 @@ static bool fetch_player_stats(FlipperHTTP *fhttp)
     return flipper_http_request(fhttp, GET, url, "{\"Content-Type\":\"application/json\"}", NULL);
     return flipper_http_request(fhttp, GET, url, "{\"Content-Type\":\"application/json\"}", NULL);
 }
 }
 
 
-// static bool fetch_app_update(FlipperHTTP *fhttp)
-// {
-//     if (!fhttp)
-//     {
-//         FURI_LOG_E(TAG, "fhttp is NULL");
-//         easy_flipper_dialog("Error", "fhttp is NULL. Press BACK to return.");
-//         return false;
-//     }
-
-//     return flipper_http_get_request_with_headers(fhttp, "https://www.jblanked.com/flipper/api/app/last-updated/flip_world/", "{\"Content-Type\":\"application/json\"}");
-// }
-
-// static bool parse_app_update(FlipperHTTP *fhttp)
-// {
-//     if (!fhttp)
-//     {
-//         FURI_LOG_E(TAG, "fhttp is NULL");
-//         easy_flipper_dialog("Error", "fhttp is NULL. Press BACK to return.");
-//         return false;
-//     }
-//     if (fhttp->last_response == NULL || strlen(fhttp->last_response) == 0)
-//     {
-//         FURI_LOG_E(TAG, "fhttp->last_response is NULL or empty");
-//         easy_flipper_dialog("Error", "fhttp->last_response is NULL or empty. Press BACK to return.");
-//         return false;
-//     }
-//     bool last_update_available = false;
-//     char last_updated_old[32];
-//     // load the previous last_updated
-//     if (!load_char("last_updated", last_updated_old, sizeof(last_updated_old)))
-//     {
-//         FURI_LOG_E(TAG, "Failed to load last_updated");
-//         // it's okay, we'll just update it
-//     }
-//     // save the new last_updated
-//     save_char("last_updated", fhttp->last_response);
-
-//     // compare the two
-//     if (strlen(last_updated_old) == 0 || !is_str(last_updated_old, fhttp->last_response))
-//     {
-//         last_update_available = true;
-//     }
-
-//     if (last_update_available)
-//     {
-//         easy_flipper_dialog("Update Available", "An update is available. Press OK to update.");
-//         return true;
-//     }
-//     else
-//     {
-//         easy_flipper_dialog("No Update Available", "No update is available. Press OK to continue.");
-//         return false;
-//     }
-// }
-
-static bool start_game_thread(void *context)
+static bool game_thread_start(void *context)
 {
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
     FlipWorldApp *app = (FlipWorldApp *)context;
     if (!app)
     if (!app)
@@ -318,7 +261,7 @@ static bool start_game_thread(void *context)
     return true;
     return true;
 }
 }
 // combine register, login, and world list fetch into one function to switch to the loader view
 // combine register, login, and world list fetch into one function to switch to the loader view
-static bool _fetch_game(DataLoaderModel *model)
+static bool game_fetch(DataLoaderModel *model)
 {
 {
     FlipWorldApp *app = (FlipWorldApp *)model->parser_context;
     FlipWorldApp *app = (FlipWorldApp *)model->parser_context;
     if (!app)
     if (!app)
@@ -393,13 +336,13 @@ static bool _fetch_game(DataLoaderModel *model)
         else
         else
         {
         {
             model->title = "Fetching World List..";
             model->title = "Fetching World List..";
-            return fetch_world_list(model->fhttp);
+            return game_fetch_world_list(model->fhttp);
         }
         }
     }
     }
     else if (model->request_index == 2)
     else if (model->request_index == 2)
     {
     {
         model->title = "Fetching World List..";
         model->title = "Fetching World List..";
-        return fetch_world_list(model->fhttp);
+        return game_fetch_world_list(model->fhttp);
     }
     }
     else if (model->request_index == 3)
     else if (model->request_index == 3)
     {
     {
@@ -429,7 +372,7 @@ static bool _fetch_game(DataLoaderModel *model)
             furi_string_free(world_list);
             furi_string_free(world_list);
             furi_string_free(first_world);
             furi_string_free(first_world);
 
 
-            if (!start_game_thread(app))
+            if (!game_thread_start(app))
             {
             {
                 FURI_LOG_E(TAG, "Failed to start game thread");
                 FURI_LOG_E(TAG, "Failed to start game thread");
                 easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return.");
                 easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return.");
@@ -451,7 +394,7 @@ static bool _fetch_game(DataLoaderModel *model)
     FURI_LOG_E(TAG, "Unknown request index");
     FURI_LOG_E(TAG, "Unknown request index");
     return false;
     return false;
 }
 }
-static char *_parse_game(DataLoaderModel *model)
+static char *game_parse(DataLoaderModel *model)
 {
 {
     FlipWorldApp *app = (FlipWorldApp *)model->parser_context;
     FlipWorldApp *app = (FlipWorldApp *)model->parser_context;
 
 
@@ -572,7 +515,7 @@ static char *_parse_game(DataLoaderModel *model)
         }
         }
         else
         else
         {
         {
-            if (!start_game_thread(app))
+            if (!game_thread_start(app))
             {
             {
                 FURI_LOG_E(TAG, "Failed to start game thread");
                 FURI_LOG_E(TAG, "Failed to start game thread");
                 easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return.");
                 easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return.");
@@ -589,7 +532,7 @@ static char *_parse_game(DataLoaderModel *model)
     }
     }
     else if (model->request_index == 3)
     else if (model->request_index == 3)
     {
     {
-        if (!start_game_thread(app))
+        if (!game_thread_start(app))
         {
         {
             FURI_LOG_E(TAG, "Failed to start game thread");
             FURI_LOG_E(TAG, "Failed to start game thread");
             easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return.");
             easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return.");
@@ -603,25 +546,21 @@ static char *_parse_game(DataLoaderModel *model)
     view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now
     view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now
     return "Unknown error";
     return "Unknown error";
 }
 }
-static void switch_to_view_get_game(FlipWorldApp *app)
+static void game_switch_to_view(FlipWorldApp *app)
 {
 {
     if (!loader_view_alloc(app))
     if (!loader_view_alloc(app))
     {
     {
         FURI_LOG_E(TAG, "Failed to allocate view loader");
         FURI_LOG_E(TAG, "Failed to allocate view loader");
         return;
         return;
     }
     }
-    loader_switch_to_view(app, "Starting Game..", _fetch_game, _parse_game, 5, callback_to_submenu, FlipWorldViewLoader);
+    loader_switch_to_view(app, "Starting Game..", game_fetch, game_parse, 5, callback_to_submenu, FlipWorldViewLoader);
 }
 }
-void run(FlipWorldApp *app)
+void game_run(FlipWorldApp *app)
 {
 {
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWorldApp is NULL");
-        return;
-    }
+    furi_check(app, "FlipWorldApp is NULL");
     free_all_views(app, true, true, false);
     free_all_views(app, true, true, false);
-    // only need to check if they have 50k free (game needs about 38k currently)
-    if (!is_enough_heap(50000, false))
+    // only need to check if they have 30k free (game needs about 12k currently)
+    if (!is_enough_heap(30000, false))
     {
     {
         const size_t min_free = memmgr_get_free_heap();
         const size_t min_free = memmgr_get_free_heap();
         char message[64];
         char message[64];
@@ -639,18 +578,18 @@ void run(FlipWorldApp *app)
             easy_flipper_dialog("Error", "Failed to allocate FlipperHTTP. Press BACK to return.");
             easy_flipper_dialog("Error", "Failed to allocate FlipperHTTP. Press BACK to return.");
             return;
             return;
         }
         }
-        bool fetch_world_list_i()
+        bool game_fetch_world_list_i()
         {
         {
-            return fetch_world_list(fhttp);
+            return game_fetch_world_list(fhttp);
         }
         }
         bool parse_world_list_i()
         bool parse_world_list_i()
         {
         {
             return fhttp->state != ISSUE;
             return fhttp->state != ISSUE;
         }
         }
 
 
-        bool fetch_player_stats_i()
+        bool game_fetch_player_stats_i()
         {
         {
-            return fetch_player_stats(fhttp);
+            return game_fetch_player_stats(fhttp);
         }
         }
 
 
         if (!alloc_message_view(app, MessageStateLoading))
         if (!alloc_message_view(app, MessageStateLoading))
@@ -663,7 +602,7 @@ void run(FlipWorldApp *app)
         // Make the request
         // Make the request
         if (game_mode_index != 1) // not GAME_MODE_PVP
         if (game_mode_index != 1) // not GAME_MODE_PVP
         {
         {
-            if (!flipper_http_process_response_async(fhttp, fetch_world_list_i, parse_world_list_i) || !flipper_http_process_response_async(fhttp, fetch_player_stats_i, set_player_context))
+            if (!flipper_http_process_response_async(fhttp, game_fetch_world_list_i, parse_world_list_i) || !flipper_http_process_response_async(fhttp, game_fetch_player_stats_i, set_player_context))
             {
             {
                 FURI_LOG_E(HTTP_TAG, "Failed to make request");
                 FURI_LOG_E(HTTP_TAG, "Failed to make request");
                 flipper_http_free(fhttp);
                 flipper_http_free(fhttp);
@@ -673,7 +612,7 @@ void run(FlipWorldApp *app)
                 flipper_http_free(fhttp);
                 flipper_http_free(fhttp);
             }
             }
 
 
-            if (!start_game_thread(app))
+            if (!game_thread_start(app))
             {
             {
                 FURI_LOG_E(TAG, "Failed to start game thread");
                 FURI_LOG_E(TAG, "Failed to start game thread");
                 easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return.");
                 easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return.");
@@ -694,9 +633,9 @@ void run(FlipWorldApp *app)
                 storage_common_mkdir(storage, directory_path);
                 storage_common_mkdir(storage, directory_path);
                 snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/pvp/lobbies");
                 snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/pvp/lobbies");
                 storage_common_mkdir(storage, directory_path);
                 storage_common_mkdir(storage, directory_path);
-                furi_record_close(RECORD_STORAGE);
                 snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/pvp/pvp_lobbies.json");
                 snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/pvp/pvp_lobbies.json");
                 storage_simply_remove_recursive(storage, fhttp->file_path); // ensure the file is empty
                 storage_simply_remove_recursive(storage, fhttp->file_path); // ensure the file is empty
+                furi_record_close(RECORD_STORAGE);
                 fhttp->save_received_data = true;
                 fhttp->save_received_data = true;
                 // 2 players max, 10 lobbies
                 // 2 players max, 10 lobbies
                 return flipper_http_request(fhttp, GET, "https://www.jblanked.com/flipper/api/world/pvp/lobbies/2/10/", "{\"Content-Type\":\"application/json\"}", NULL);
                 return flipper_http_request(fhttp, GET, "https://www.jblanked.com/flipper/api/world/pvp/lobbies/2/10/", "{\"Content-Type\":\"application/json\"}", NULL);
@@ -755,7 +694,7 @@ void run(FlipWorldApp *app)
             }
             }
 
 
             // load pvp lobbies and player stats
             // load pvp lobbies and player stats
-            if (!flipper_http_process_response_async(fhttp, fetch_pvp_lobbies, parse_pvp_lobbies) || !flipper_http_process_response_async(fhttp, fetch_player_stats_i, set_player_context))
+            if (!flipper_http_process_response_async(fhttp, fetch_pvp_lobbies, parse_pvp_lobbies) || !flipper_http_process_response_async(fhttp, game_fetch_player_stats_i, set_player_context))
             {
             {
                 // unlike the pve/story, receiving data is necessary
                 // unlike the pve/story, receiving data is necessary
                 // so send the user back to the main menu if it fails
                 // so send the user back to the main menu if it fails
@@ -776,11 +715,11 @@ void run(FlipWorldApp *app)
     }
     }
     else
     else
     {
     {
-        switch_to_view_get_game(app);
+        game_switch_to_view(app);
     }
     }
 }
 }
 
 
-static bool fetch_lobby(FlipperHTTP *fhttp, char *lobby_name)
+bool game_fetch_lobby(FlipperHTTP *fhttp, char *lobby_name)
 {
 {
     if (!fhttp)
     if (!fhttp)
     {
     {
@@ -815,7 +754,7 @@ static bool fetch_lobby(FlipperHTTP *fhttp, char *lobby_name)
     }
     }
     return true;
     return true;
 }
 }
-static bool join_lobby(FlipperHTTP *fhttp, char *lobby_name)
+bool game_join_lobby(FlipperHTTP *fhttp, char *lobby_name)
 {
 {
     if (!fhttp)
     if (!fhttp)
     {
     {
@@ -850,7 +789,7 @@ static bool join_lobby(FlipperHTTP *fhttp, char *lobby_name)
     }
     }
     return true;
     return true;
 }
 }
-static bool create_pvp_enemy(FuriString *lobby_details)
+static bool game_create_pvp_enemy(FuriString *lobby_details)
 {
 {
     if (!lobby_details)
     if (!lobby_details)
     {
     {
@@ -978,9 +917,8 @@ static bool create_pvp_enemy(FuriString *lobby_details)
 
 
     return true;
     return true;
 }
 }
-// since we aren't using FURI_LOG, we will use easy_flipper_dialog and the last_error_message
-// char last_error_message[64];
-static size_t lobby_count(FlipperHTTP *fhttp, FuriString *lobby)
+
+size_t game_lobby_count(FlipperHTTP *fhttp, FuriString *lobby)
 {
 {
     if (!fhttp)
     if (!fhttp)
     {
     {
@@ -1003,7 +941,7 @@ static size_t lobby_count(FlipperHTTP *fhttp, FuriString *lobby)
     furi_string_free(player_count);
     furi_string_free(player_count);
     return count;
     return count;
 }
 }
-static bool in_lobby(FlipperHTTP *fhttp, FuriString *lobby)
+bool game_in_lobby(FlipperHTTP *fhttp, FuriString *lobby)
 {
 {
     if (!fhttp)
     if (!fhttp)
     {
     {
@@ -1028,7 +966,7 @@ static bool in_lobby(FlipperHTTP *fhttp, FuriString *lobby)
     return in_game;
     return in_game;
 }
 }
 
 
-static bool start_ws(FlipperHTTP *fhttp, char *lobby_name)
+static bool game_start_ws(FlipperHTTP *fhttp, char *lobby_name)
 {
 {
     if (!fhttp)
     if (!fhttp)
     {
     {
@@ -1056,12 +994,12 @@ static bool start_ws(FlipperHTTP *fhttp, char *lobby_name)
     return true;
     return true;
 }
 }
 // this will free both the fhttp and lobby
 // this will free both the fhttp and lobby
-static void start_pvp(FlipperHTTP *fhttp, FuriString *lobby, void *context)
+void game_start_pvp(FlipperHTTP *fhttp, FuriString *lobby, void *context)
 {
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
     FlipWorldApp *app = (FlipWorldApp *)context;
     furi_check(app, "FlipWorldApp is NULL");
     furi_check(app, "FlipWorldApp is NULL");
     // only thing left to do is create the enemy data and start the websocket session
     // only thing left to do is create the enemy data and start the websocket session
-    if (!create_pvp_enemy(lobby))
+    if (!game_create_pvp_enemy(lobby))
     {
     {
         FURI_LOG_E(TAG, "Failed to create pvp enemy context.");
         FURI_LOG_E(TAG, "Failed to create pvp enemy context.");
         easy_flipper_dialog("Error", "Failed to create pvp enemy context. Press BACK to return.");
         easy_flipper_dialog("Error", "Failed to create pvp enemy context. Press BACK to return.");
@@ -1073,7 +1011,7 @@ static void start_pvp(FlipperHTTP *fhttp, FuriString *lobby, void *context)
     furi_string_free(lobby);
     furi_string_free(lobby);
 
 
     // start the websocket session
     // start the websocket session
-    if (!start_ws(fhttp, lobby_list[lobby_index]))
+    if (!game_start_ws(fhttp, lobby_list[lobby_index]))
     {
     {
         FURI_LOG_E(TAG, "Failed to start websocket session");
         FURI_LOG_E(TAG, "Failed to start websocket session");
         easy_flipper_dialog("Error", "Failed to start websocket session. Press BACK to return.");
         easy_flipper_dialog("Error", "Failed to start websocket session. Press BACK to return.");
@@ -1084,21 +1022,17 @@ static void start_pvp(FlipperHTTP *fhttp, FuriString *lobby, void *context)
     flipper_http_free(fhttp);
     flipper_http_free(fhttp);
 
 
     // start the game thread
     // start the game thread
-    if (!start_game_thread(app))
+    if (!game_thread_start(app))
     {
     {
         FURI_LOG_E(TAG, "Failed to start game thread");
         FURI_LOG_E(TAG, "Failed to start game thread");
         easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return.");
         easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return.");
         return;
         return;
     }
     }
 };
 };
-static void waiting_loader_process_callback(FlipperHTTP *fhttp, void *context)
+void game_waiting_process(FlipperHTTP *fhttp, void *context)
 {
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
     FlipWorldApp *app = (FlipWorldApp *)context;
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWorldApp is NULL");
-        return;
-    }
+    furi_check(app, "FlipWorldApp is NULL");
     if (!fhttp)
     if (!fhttp)
     {
     {
         FURI_LOG_E(TAG, "Failed to allocate FlipperHTTP");
         FURI_LOG_E(TAG, "Failed to allocate FlipperHTTP");
@@ -1107,7 +1041,7 @@ static void waiting_loader_process_callback(FlipperHTTP *fhttp, void *context)
         return;
         return;
     }
     }
     // fetch the lobby details
     // fetch the lobby details
-    if (!fetch_lobby(fhttp, lobby_list[lobby_index]))
+    if (!game_fetch_lobby(fhttp, lobby_list[lobby_index]))
     {
     {
         FURI_LOG_E(TAG, "Failed to fetch lobby details");
         FURI_LOG_E(TAG, "Failed to fetch lobby details");
         flipper_http_free(fhttp);
         flipper_http_free(fhttp);
@@ -1126,21 +1060,21 @@ static void waiting_loader_process_callback(FlipperHTTP *fhttp, void *context)
         return;
         return;
     }
     }
     // get the player count
     // get the player count
-    const size_t count = lobby_count(fhttp, lobby);
+    const size_t count = game_lobby_count(fhttp, lobby);
     if (count == 2)
     if (count == 2)
     {
     {
         // break out of this and start the game
         // break out of this and start the game
-        start_pvp(fhttp, lobby, app); // this will free both the fhttp and lobby
+        game_start_pvp(fhttp, lobby, app); // this will free both the fhttp and lobby
         return;
         return;
     }
     }
     furi_string_free(lobby);
     furi_string_free(lobby);
 }
 }
 
 
-static void waiting_lobby(void *context)
+void game_waiting_lobby(void *context)
 {
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
     FlipWorldApp *app = (FlipWorldApp *)context;
     furi_check(app, "waiting_lobby: FlipWorldApp is NULL");
     furi_check(app, "waiting_lobby: FlipWorldApp is NULL");
-    if (!start_waiting_thread(app))
+    if (!game_start_waiting_thread(app))
     {
     {
         FURI_LOG_E(TAG, "Failed to start waiting thread");
         FURI_LOG_E(TAG, "Failed to start waiting thread");
         easy_flipper_dialog("Error", "Failed to start waiting thread. Press BACK to return.");
         easy_flipper_dialog("Error", "Failed to start waiting thread. Press BACK to return.");
@@ -1155,101 +1089,3 @@ static void waiting_lobby(void *context)
     // finally, switch to the waiting lobby view
     // finally, switch to the waiting lobby view
     view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewMessage);
     view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewMessage);
 };
 };
-
-static void callback_submenu_lobby_choices(void *context, uint32_t index)
-{
-    /* Handle other game lobbies
-             1. when clicked on, send request to fetch the selected game lobby details
-             2. start the websocket session
-             3. start the game thread (the rest will be handled by game_start and player_update)
-             */
-    FlipWorldApp *app = (FlipWorldApp *)context;
-    furi_check(app, "FlipWorldApp is NULL");
-    if (index >= FlipWorldSubmenuIndexLobby && index < FlipWorldSubmenuIndexLobby + 10)
-    {
-        lobby_index = index - FlipWorldSubmenuIndexLobby;
-
-        FlipperHTTP *fhttp = flipper_http_alloc();
-        if (!fhttp)
-        {
-            FURI_LOG_E(TAG, "Failed to allocate FlipperHTTP");
-            easy_flipper_dialog("Error", "Failed to allocate FlipperHTTP. Press BACK to return.");
-            return;
-        }
-
-        // fetch the lobby details
-        if (!fetch_lobby(fhttp, lobby_list[lobby_index]))
-        {
-            FURI_LOG_E(TAG, "Failed to fetch lobby details");
-            easy_flipper_dialog("Error", "Failed to fetch lobby details. Press BACK to return.");
-            flipper_http_free(fhttp);
-            return;
-        }
-
-        // load the lobby details
-        FuriString *lobby = flipper_http_load_from_file(fhttp->file_path);
-        if (!lobby)
-        {
-            FURI_LOG_E(TAG, "Failed to load lobby details");
-            flipper_http_free(fhttp);
-            return;
-        }
-
-        // if there are no players, add the user to the lobby and make the user wait until another player joins
-        // if there is one player and it's the user, make the user wait until another player joins
-        // if there is one player and it's not the user, parse_lobby and start websocket
-        // if there are 2 players (which there shouldn't be at this point), show an error message saying the lobby is full
-        switch (lobby_count(fhttp, lobby))
-        {
-        case -1:
-            FURI_LOG_E(TAG, "Failed to get player count");
-            easy_flipper_dialog("Error", "Failed to get player count. Press BACK to return.");
-            flipper_http_free(fhttp);
-            furi_string_free(lobby);
-            return;
-        case 0:
-            // add the user to the lobby
-            if (!join_lobby(fhttp, lobby_list[lobby_index]))
-            {
-                FURI_LOG_E(TAG, "Failed to join lobby");
-                easy_flipper_dialog("Error", "Failed to join lobby. Press BACK to return.");
-                flipper_http_free(fhttp);
-                furi_string_free(lobby);
-                return;
-            }
-            // send the user to the waiting screen
-            waiting_lobby(app);
-            return;
-        case 1:
-            // check if the user is in the lobby
-            if (in_lobby(fhttp, lobby))
-            {
-                // send the user to the waiting screen
-                FURI_LOG_I(TAG, "User is in the lobby");
-                flipper_http_free(fhttp);
-                furi_string_free(lobby);
-                waiting_lobby(app);
-                return;
-            }
-            // add the user to the lobby
-            if (!join_lobby(fhttp, lobby_list[lobby_index]))
-            {
-                FURI_LOG_E(TAG, "Failed to join lobby");
-                easy_flipper_dialog("Error", "Failed to join lobby. Press BACK to return.");
-                flipper_http_free(fhttp);
-                furi_string_free(lobby);
-                return;
-            }
-            break;
-        case 2:
-            // show an error message saying the lobby is full
-            FURI_LOG_E(TAG, "Lobby is full");
-            easy_flipper_dialog("Error", "Lobby is full. Press BACK to return.");
-            flipper_http_free(fhttp);
-            furi_string_free(lobby);
-            return;
-        };
-
-        start_pvp(fhttp, lobby, app); // this will free both the fhttp and lobby, and start the game
-    }
-}

+ 9 - 1
callback/game.h

@@ -7,4 +7,12 @@ extern FuriThread *game_thread;
 extern FuriThread *waiting_thread;
 extern FuriThread *waiting_thread;
 extern bool game_thread_running;
 extern bool game_thread_running;
 extern bool waiting_thread_running;
 extern bool waiting_thread_running;
-void run(FlipWorldApp *app);
+//
+void game_run(FlipWorldApp *app);
+bool game_fetch_lobby(FlipperHTTP *fhttp, char *lobby_name);
+bool game_join_lobby(FlipperHTTP *fhttp, char *lobby_name);
+size_t game_lobby_count(FlipperHTTP *fhttp, FuriString *lobby);
+bool game_in_lobby(FlipperHTTP *fhttp, FuriString *lobby);
+void game_start_pvp(FlipperHTTP *fhttp, FuriString *lobby, void *context);
+void game_waiting_lobby(void *context);
+void game_waiting_process(FlipperHTTP *fhttp, void *context);

+ 425 - 1
flip_world.c

@@ -1,4 +1,5 @@
 #include <flip_world.h>
 #include <flip_world.h>
+#include <flip_storage/storage.h>
 char *fps_choices_str[] = {"30", "60", "120", "240"};
 char *fps_choices_str[] = {"30", "60", "120", "240"};
 uint8_t fps_index = 0;
 uint8_t fps_index = 0;
 char *yes_or_no_choices[] = {"No", "Yes"};
 char *yes_or_no_choices[] = {"No", "Yes"};
@@ -33,4 +34,427 @@ bool is_enough_heap(size_t heap_size, bool check_blocks)
         }
         }
     }
     }
     return true;
     return true;
-}
+}
+
+static bool flip_world_json_to_datetime(DateTime *rtc_time, FuriString *str)
+{
+    if (!rtc_time || !str)
+    {
+        FURI_LOG_E(TAG, "rtc_time or str is NULL");
+        return false;
+    }
+    FuriString *hour = get_json_value_furi("hour", str);
+    if (hour)
+    {
+        rtc_time->hour = atoi(furi_string_get_cstr(hour));
+        furi_string_free(hour);
+    }
+    FuriString *minute = get_json_value_furi("minute", str);
+    if (minute)
+    {
+        rtc_time->minute = atoi(furi_string_get_cstr(minute));
+        furi_string_free(minute);
+    }
+    FuriString *second = get_json_value_furi("second", str);
+    if (second)
+    {
+        rtc_time->second = atoi(furi_string_get_cstr(second));
+        furi_string_free(second);
+    }
+    FuriString *day = get_json_value_furi("day", str);
+    if (day)
+    {
+        rtc_time->day = atoi(furi_string_get_cstr(day));
+        furi_string_free(day);
+    }
+    FuriString *month = get_json_value_furi("month", str);
+    if (month)
+    {
+        rtc_time->month = atoi(furi_string_get_cstr(month));
+        furi_string_free(month);
+    }
+    FuriString *year = get_json_value_furi("year", str);
+    if (year)
+    {
+        rtc_time->year = atoi(furi_string_get_cstr(year));
+        furi_string_free(year);
+    }
+    FuriString *weekday = get_json_value_furi("weekday", str);
+    if (weekday)
+    {
+        rtc_time->weekday = atoi(furi_string_get_cstr(weekday));
+        furi_string_free(weekday);
+    }
+    return datetime_validate_datetime(rtc_time);
+}
+
+static FuriString *flip_world_datetime_to_json(DateTime *rtc_time)
+{
+    if (!rtc_time)
+    {
+        FURI_LOG_E(TAG, "rtc_time is NULL");
+        return NULL;
+    }
+    char json[256];
+    snprintf(
+        json,
+        sizeof(json),
+        "{\"hour\":%d,\"minute\":%d,\"second\":%d,\"day\":%d,\"month\":%d,\"year\":%d,\"weekday\":%d}",
+        rtc_time->hour,
+        rtc_time->minute,
+        rtc_time->second,
+        rtc_time->day,
+        rtc_time->month,
+        rtc_time->year,
+        rtc_time->weekday);
+    return furi_string_alloc_set_str(json);
+}
+
+static bool flip_world_save_rtc_time(DateTime *rtc_time)
+{
+    if (!rtc_time)
+    {
+        FURI_LOG_E(TAG, "rtc_time is NULL");
+        return false;
+    }
+    FuriString *json = flip_world_datetime_to_json(rtc_time);
+    if (!json)
+    {
+        FURI_LOG_E(TAG, "Failed to convert DateTime to JSON");
+        return false;
+    }
+    save_char("last_checked", furi_string_get_cstr(json));
+    furi_string_free(json);
+    return true;
+}
+
+//
+// Returns true if time_current is one hour (or more) later than the stored last_updated time
+//
+static bool flip_world_is_update_time(DateTime *time_current)
+{
+    if (!time_current)
+    {
+        FURI_LOG_E(TAG, "time_current is NULL");
+        return false;
+    }
+    char last_updated_old[128];
+    if (!load_char("last_updated", last_updated_old, sizeof(last_updated_old)))
+    {
+        FURI_LOG_E(TAG, "Failed to load last_updated");
+        FuriString *json = flip_world_datetime_to_json(time_current);
+        if (json)
+        {
+            save_char("last_updated", furi_string_get_cstr(json));
+            furi_string_free(json);
+        }
+        return false;
+    }
+
+    DateTime last_updated_time;
+
+    FuriString *last_updated_furi = char_to_furi_string(last_updated_old);
+    if (!last_updated_furi)
+    {
+        FURI_LOG_E(TAG, "Failed to convert char to FuriString");
+        return false;
+    }
+    if (!flip_world_json_to_datetime(&last_updated_time, last_updated_furi))
+    {
+        FURI_LOG_E(TAG, "Failed to convert JSON to DateTime");
+        furi_string_free(last_updated_furi);
+        return false;
+    }
+    furi_string_free(last_updated_furi); // Free after usage.
+
+    bool time_diff = false;
+    // If the date is different assume more than one hour has passed.
+    if (time_current->year != last_updated_time.year ||
+        time_current->month != last_updated_time.month ||
+        time_current->day != last_updated_time.day)
+    {
+        time_diff = true;
+    }
+    else
+    {
+        // For the same day, compute seconds from midnight.
+        int seconds_current = time_current->hour * 3600 + time_current->minute * 60 + time_current->second;
+        int seconds_last = last_updated_time.hour * 3600 + last_updated_time.minute * 60 + last_updated_time.second;
+        if ((seconds_current - seconds_last) >= 3600)
+        {
+            time_diff = true;
+        }
+    }
+
+    return time_diff;
+}
+
+// Sends a request to fetch the last updated date of the app.
+static bool flip_world_last_app_update(FlipperHTTP *fhttp, bool flipper_server)
+{
+    if (!fhttp)
+    {
+        FURI_LOG_E(TAG, "fhttp is NULL");
+        return false;
+    }
+    char url[256];
+    if (flipper_server)
+    {
+        // make sure folder is created
+        char directory_path[256];
+        snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world");
+
+        // Create the directory
+        Storage *storage = furi_record_open(RECORD_STORAGE);
+        storage_common_mkdir(storage, directory_path);
+        snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data");
+        storage_common_mkdir(storage, directory_path);
+        snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data/last_update_request.txt");
+        storage_simply_remove_recursive(storage, directory_path); // ensure the file is empty
+        furi_record_close(RECORD_STORAGE);
+
+        fhttp->save_received_data = true;
+        fhttp->is_bytes_request = false;
+
+        snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data/last_update_request.txt");
+        snprintf(url, sizeof(url), "https://catalog.flipperzero.one/api/v0/0/application/%s?is_latest_release_version=true", BUILD_ID);
+        return flipper_http_request(fhttp, GET, url, "{\"Content-Type\":\"application/json\"}", NULL);
+    }
+    else
+    {
+        snprintf(url, sizeof(url), "https://www.jblanked.com/flipper/api/app/last-updated/flip_world/");
+        return flipper_http_request(fhttp, GET, url, "{\"Content-Type\":\"application/json\"}", NULL);
+    }
+}
+
+// Parses the server response and returns true if an update is available.
+static bool flip_world_parse_last_app_update(FlipperHTTP *fhttp, DateTime *time_current, bool flipper_server)
+{
+    if (!fhttp)
+    {
+        FURI_LOG_E(TAG, "fhttp is NULL");
+        return false;
+    }
+    if (fhttp->state == ISSUE)
+    {
+        FURI_LOG_E(TAG, "Failed to fetch last app update");
+        return false;
+    }
+    char version_str[32];
+    if (!flipper_server)
+    {
+        if (fhttp->last_response == NULL || strlen(fhttp->last_response) == 0)
+        {
+            FURI_LOG_E(TAG, "fhttp->last_response is NULL or empty");
+            return false;
+        }
+
+        char *app_version = get_json_value("version", fhttp->last_response);
+        if (app_version)
+        {
+            // Save the server app version: it should save something like: 0.8
+            save_char("server_app_version", app_version);
+            snprintf(version_str, sizeof(version_str), "%s", app_version);
+            free(app_version);
+        }
+        else
+        {
+            FURI_LOG_E(TAG, "Failed to get app version");
+            return false;
+        }
+    }
+    else
+    {
+        FuriString *app_data = flipper_http_load_from_file_with_limit(fhttp->file_path, memmgr_heap_get_max_free_block());
+        if (!app_data)
+        {
+            FURI_LOG_E(TAG, "Failed to load app data");
+            return false;
+        }
+        FuriString *current_version = get_json_value_furi("current_version", app_data);
+        if (!current_version)
+        {
+            FURI_LOG_E(TAG, "Failed to get current version");
+            furi_string_free(app_data);
+            return false;
+        }
+        furi_string_free(app_data);
+        FuriString *version = get_json_value_furi("version", current_version);
+        if (!version)
+        {
+            FURI_LOG_E(TAG, "Failed to get version");
+            furi_string_free(current_version);
+            furi_string_free(app_data);
+            return false;
+        }
+        // Save the server app version: it should save something like: 0.8
+        save_char("server_app_version", furi_string_get_cstr(version));
+        snprintf(version_str, sizeof(version_str), "%s", furi_string_get_cstr(version));
+        furi_string_free(current_version);
+        furi_string_free(version);
+        // furi_string_free(app_data);
+    }
+    // Only check for an update if an hour or more has passed.
+    if (flip_world_is_update_time(time_current))
+    {
+        char app_version[32];
+        if (!load_char("app_version", app_version, sizeof(app_version)))
+        {
+            FURI_LOG_E(TAG, "Failed to load app version");
+            return false;
+        }
+        FURI_LOG_I(TAG, "App version: %s", app_version);
+        FURI_LOG_I(TAG, "Server version: %s", version_str);
+        // Check if the app version is different from the server version.
+        if (!is_str(app_version, version_str))
+        {
+            easy_flipper_dialog("Update available", "New update available!\nPress BACK to download.");
+            return true; // Update available.
+        }
+        FURI_LOG_I(TAG, "No update available");
+        return false; // No update available.
+    }
+    FURI_LOG_I(TAG, "Not enough time has passed since the last update check");
+    return false; // Not yet time to update.
+}
+
+static bool flip_world_get_fap_file(FlipperHTTP *fhttp, bool flipper_server)
+{
+    if (!fhttp)
+    {
+        FURI_LOG_E(TAG, "FlipperHTTP is NULL.");
+        return false;
+    }
+    char url[256];
+    fhttp->save_received_data = false;
+    fhttp->is_bytes_request = true;
+#ifndef FW_ORIGIN_Momentum
+    snprintf(
+        fhttp->file_path,
+        sizeof(fhttp->file_path),
+        STORAGE_EXT_PATH_PREFIX "/apps/GPIO/flip_world.fap");
+#else
+    snprintf(
+        fhttp->file_path,
+        sizeof(fhttp->file_path),
+        STORAGE_EXT_PATH_PREFIX "/apps/GPIO/FlipperHTTP/flip_world.fap");
+#endif
+    if (flipper_server)
+    {
+        char build_id[32];
+        snprintf(build_id, sizeof(build_id), "%s", BUILD_ID);
+        uint8_t target;
+        target = furi_hal_version_get_hw_target();
+        uint16_t api_major, api_minor;
+        furi_hal_info_get_api_version(&api_major, &api_minor);
+        snprintf(
+            url,
+            sizeof(url),
+            "https://catalog.flipperzero.one/api/v0/application/version/%s/build/compatible?target=f%d&api=%d.%d",
+            build_id,
+            target,
+            api_major,
+            api_minor);
+    }
+    else
+    {
+        snprintf(url, sizeof(url), "https://www.jblanked.com/flipper/api/app/download/flip_world/");
+    }
+    return flipper_http_request(fhttp, BYTES, url, "{\"Content-Type\": \"application/octet-stream\"}", NULL);
+}
+
+// Updates the app. Uses the supplied current time for validating if update check should proceed.
+static bool flip_world_update_app(FlipperHTTP *fhttp, DateTime *time_current, bool use_flipper_api)
+{
+    if (!fhttp)
+    {
+        FURI_LOG_E(TAG, "fhttp is NULL");
+        return false;
+    }
+    if (!flip_world_last_app_update(fhttp, use_flipper_api))
+    {
+        FURI_LOG_E(TAG, "Failed to fetch last app update");
+        return false;
+    }
+    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)
+    {
+        furi_delay_ms(100);
+    }
+    furi_timer_stop(fhttp->get_timeout_timer);
+    if (flip_world_parse_last_app_update(fhttp, time_current, use_flipper_api))
+    {
+        if (!flip_world_get_fap_file(fhttp, false))
+        {
+            FURI_LOG_E(TAG, "Failed to fetch fap file 1");
+            return false;
+        }
+        fhttp->state = RECEIVING;
+
+        while (fhttp->state == RECEIVING)
+        {
+            furi_delay_ms(100);
+        }
+
+        if (fhttp->state == ISSUE)
+        {
+            FURI_LOG_E(TAG, "Failed to fetch fap file 2");
+            return false;
+        }
+        return true;
+    }
+
+    FURI_LOG_I(TAG, "No update available");
+    return false; // No update available.
+}
+
+// Handles the app update routine. This function obtains the current RTC time,
+// checks the "last_checked" value, and if it is more than one hour old, calls for an update.
+bool flip_world_handle_app_update(FlipperHTTP *fhttp, bool use_flipper_api)
+{
+    if (!fhttp)
+    {
+        FURI_LOG_E(TAG, "fhttp is NULL");
+        return false;
+    }
+    DateTime rtc_time;
+    furi_hal_rtc_get_datetime(&rtc_time);
+    char last_checked[32];
+    if (!load_char("last_checked", last_checked, sizeof(last_checked)))
+    {
+        // First time – save the current time and check for an update.
+        if (!flip_world_save_rtc_time(&rtc_time))
+        {
+            FURI_LOG_E(TAG, "Failed to save RTC time");
+            return false;
+        }
+        return flip_world_update_app(fhttp, &rtc_time, use_flipper_api);
+    }
+    else
+    {
+        // Check if the current RTC time is at least one hour past the stored time.
+        if (flip_world_is_update_time(&rtc_time))
+        {
+            if (!flip_world_update_app(fhttp, &rtc_time, use_flipper_api))
+            {
+                FURI_LOG_E(TAG, "Failed to update app");
+                // save the current time for the next check.
+                if (!flip_world_save_rtc_time(&rtc_time))
+                {
+                    FURI_LOG_E(TAG, "Failed to save RTC time");
+                    return false;
+                }
+                return false;
+            }
+            // Save the current time for the next check.
+            if (!flip_world_save_rtc_time(&rtc_time))
+            {
+                FURI_LOG_E(TAG, "Failed to save RTC time");
+                return false;
+            }
+            return true;
+        }
+        return false; // No update necessary.
+    }
+}

+ 6 - 1
flip_world.h

@@ -15,8 +15,12 @@
 //
 //
 
 
 #define TAG "FlipWorld"
 #define TAG "FlipWorld"
-#define VERSION 0.8
+#define VERSION "0.8.1"
 #define VERSION_TAG TAG " " FAP_VERSION
 #define VERSION_TAG TAG " " FAP_VERSION
+//
+
+#define APP_ID "67f22e9a25a4a6f1fb4a2c4a"
+#define BUILD_ID "676900d983aa88302bc114c6"
 
 
 // Define the submenu items for our FlipWorld application
 // Define the submenu items for our FlipWorld application
 typedef enum
 typedef enum
@@ -107,3 +111,4 @@ float atof_(const char *nptr);
 float atof_furi(const FuriString *nptr);
 float atof_furi(const FuriString *nptr);
 bool is_str(const char *src, const char *dst);
 bool is_str(const char *src, const char *dst);
 bool is_enough_heap(size_t heap_size, bool check_blocks);
 bool is_enough_heap(size_t heap_size, bool check_blocks);
+bool flip_world_handle_app_update(FlipperHTTP *fhttp, bool use_flipper_api);

+ 43 - 14
flipper_http/flipper_http.c

@@ -415,32 +415,43 @@ FuriString *flipper_http_load_from_file(char *file_path)
         return NULL;
         return NULL;
     }
     }
 
 
-    // Allocate a FuriString to hold the received data
-    FuriString *str_result = furi_string_alloc();
-    if (!str_result)
+    size_t file_size = storage_file_size(file);
+
+    // final memory check
+    if (memmgr_heap_get_max_free_block() < file_size)
     {
     {
-        FURI_LOG_E(HTTP_TAG, "Failed to allocate FuriString");
+        FURI_LOG_E(HTTP_TAG, "Not enough heap to read file.");
         storage_file_close(file);
         storage_file_close(file);
         storage_file_free(file);
         storage_file_free(file);
         furi_record_close(RECORD_STORAGE);
         furi_record_close(RECORD_STORAGE);
         return NULL;
         return NULL;
     }
     }
 
 
-    // Reset the FuriString to ensure it's empty before reading
-    furi_string_reset(str_result);
-
-    // Define a buffer to hold the read data
-    uint8_t *buffer = (uint8_t *)malloc(MAX_FILE_SHOW);
+    // Allocate a buffer to hold the read data
+    uint8_t *buffer = (uint8_t *)malloc(file_size);
     if (!buffer)
     if (!buffer)
     {
     {
         FURI_LOG_E(HTTP_TAG, "Failed to allocate buffer");
         FURI_LOG_E(HTTP_TAG, "Failed to allocate buffer");
-        furi_string_free(str_result);
         storage_file_close(file);
         storage_file_close(file);
         storage_file_free(file);
         storage_file_free(file);
         furi_record_close(RECORD_STORAGE);
         furi_record_close(RECORD_STORAGE);
         return NULL;
         return NULL;
     }
     }
 
 
+    // Allocate a FuriString to hold the received data
+    FuriString *str_result = furi_string_alloc();
+    if (!str_result)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to allocate FuriString");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return NULL;
+    }
+
+    // Reset the FuriString to ensure it's empty before reading
+    furi_string_reset(str_result);
+
     // Read data into the buffer
     // Read data into the buffer
     size_t read_count = storage_file_read(file, buffer, MAX_FILE_SHOW);
     size_t read_count = storage_file_read(file, buffer, MAX_FILE_SHOW);
     if (storage_file_get_error(file) != FSE_OK)
     if (storage_file_get_error(file) != FSE_OK)
@@ -475,6 +486,12 @@ FuriString *flipper_http_load_from_file(char *file_path)
  */
  */
 FuriString *flipper_http_load_from_file_with_limit(char *file_path, size_t limit)
 FuriString *flipper_http_load_from_file_with_limit(char *file_path, size_t limit)
 {
 {
+    if (memmgr_heap_get_max_free_block() < limit)
+    {
+        FURI_LOG_E(HTTP_TAG, "Not enough heap to read file.");
+        return NULL;
+    }
+
     // Open the storage record
     // Open the storage record
     Storage *storage = furi_record_open(RECORD_STORAGE);
     Storage *storage = furi_record_open(RECORD_STORAGE);
     if (!storage)
     if (!storage)
@@ -501,7 +518,19 @@ FuriString *flipper_http_load_from_file_with_limit(char *file_path, size_t limit
         return NULL;
         return NULL;
     }
     }
 
 
-    if (memmgr_get_free_heap() < limit)
+    size_t file_size = storage_file_size(file);
+
+    if (file_size > limit)
+    {
+        FURI_LOG_E(HTTP_TAG, "File size exceeds limit: %d > %d", file_size, limit);
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return NULL;
+    }
+
+    // final memory check
+    if (memmgr_heap_get_max_free_block() < file_size)
     {
     {
         FURI_LOG_E(HTTP_TAG, "Not enough heap to read file.");
         FURI_LOG_E(HTTP_TAG, "Not enough heap to read file.");
         storage_file_close(file);
         storage_file_close(file);
@@ -511,7 +540,7 @@ FuriString *flipper_http_load_from_file_with_limit(char *file_path, size_t limit
     }
     }
 
 
     // Allocate a buffer to hold the read data
     // Allocate a buffer to hold the read data
-    uint8_t *buffer = (uint8_t *)malloc(limit);
+    uint8_t *buffer = (uint8_t *)malloc(file_size);
     if (!buffer)
     if (!buffer)
     {
     {
         FURI_LOG_E(HTTP_TAG, "Failed to allocate buffer");
         FURI_LOG_E(HTTP_TAG, "Failed to allocate buffer");
@@ -532,10 +561,10 @@ FuriString *flipper_http_load_from_file_with_limit(char *file_path, size_t limit
         furi_record_close(RECORD_STORAGE);
         furi_record_close(RECORD_STORAGE);
         return NULL;
         return NULL;
     }
     }
-    furi_string_reserve(str_result, limit);
+    furi_string_reserve(str_result, file_size);
 
 
     // Read data into the buffer
     // Read data into the buffer
-    size_t read_count = storage_file_read(file, buffer, limit);
+    size_t read_count = storage_file_read(file, buffer, file_size);
     if (storage_file_get_error(file) != FSE_OK)
     if (storage_file_get_error(file) != FSE_OK)
     {
     {
         FURI_LOG_E(HTTP_TAG, "Error reading from file.");
         FURI_LOG_E(HTTP_TAG, "Error reading from file.");

+ 5 - 43
game/draw.c

@@ -1,8 +1,8 @@
 #include <game/draw.h>
 #include <game/draw.h>
 
 
 // Global variables to store camera position
 // Global variables to store camera position
-int camera_x = 0;
-int camera_y = 0;
+int draw_camera_x = 0;
+int draw_camera_y = 0;
 
 
 // Draw the user stats (health, xp, and level)
 // Draw the user stats (health, xp, and level)
 void draw_user_stats(Canvas *canvas, Vector pos, GameManager *manager)
 void draw_user_stats(Canvas *canvas, Vector pos, GameManager *manager)
@@ -38,50 +38,12 @@ void draw_username(Canvas *canvas, Vector pos, char *username)
     // first draw a black rectangle to make the text more readable
     // first draw a black rectangle to make the text more readable
     // draw box around the username
     // draw box around the username
     canvas_invert_color(canvas);
     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_draw_box(canvas, pos.x - draw_camera_x - (strlen(username) * 2) - 1, pos.y - draw_camera_y - 14, strlen(username) * 4 + 1, 8);
     canvas_invert_color(canvas);
     canvas_invert_color(canvas);
 
 
     // draw username over player's head
     // draw username over player's head
     canvas_set_font_custom(canvas, FONT_SIZE_SMALL);
     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);
-}
-
-char g_name[32];
-// Draw an icon at a specific position (with collision detection)
-void spawn_icon(GameManager *manager, Level *level, const char *icon_id, float x, float y)
-{
-    snprintf(g_name, sizeof(g_name), "%s", icon_id);
-    Entity *e = level_add_entity(level, &icon_desc);
-    entity_pos_set(e, (Vector){x, y});
-    GameContext *game_context = game_manager_game_context_get(manager);
-    game_context->icon_count++;
-}
-// Draw a line of icons at a specific position (with collision detection)
-void spawn_icon_line(GameManager *manager, Level *level, const char *icon_id, float x, float y, uint8_t amount, bool horizontal, uint8_t spacing)
-{
-    for (int i = 0; i < amount; i++)
-    {
-        if (horizontal)
-        {
-            // check if element is outside the world
-            if (x + (i * spacing) > WORLD_WIDTH)
-            {
-                break;
-            }
-
-            spawn_icon(manager, level, icon_id, x + (i * spacing), y);
-        }
-        else
-        {
-            // check if element is outside the world
-            if (y + (i * spacing) > WORLD_HEIGHT)
-            {
-                break;
-            }
-
-            spawn_icon(manager, level, icon_id, x, y + (i * spacing));
-        }
-    }
+    canvas_draw_str(canvas, pos.x - draw_camera_x - (strlen(username) * 2), pos.y - draw_camera_y - 7, username);
 }
 }
 
 
 static void draw_menu(GameManager *manager, Canvas *canvas)
 static void draw_menu(GameManager *manager, Canvas *canvas)
@@ -172,7 +134,7 @@ static void draw_menu(GameManager *manager, Canvas *canvas)
     }
     }
 }
 }
 
 
-void background_render(Canvas *canvas, GameManager *manager)
+void draw_background_render(Canvas *canvas, GameManager *manager)
 {
 {
     if (!canvas || !manager)
     if (!canvas || !manager)
         return;
         return;

+ 3 - 8
game/draw.h

@@ -2,13 +2,8 @@
 #include "game.h"
 #include "game.h"
 #include "game/icon.h"
 #include "game/icon.h"
 #include <game/player.h>
 #include <game/player.h>
-
-// Global variables to store camera position
-extern int camera_x;
-extern int camera_y;
+extern int draw_camera_x;
+extern int draw_camera_y;
 void draw_user_stats(Canvas *canvas, Vector pos, GameManager *manager);
 void draw_user_stats(Canvas *canvas, Vector pos, GameManager *manager);
 void draw_username(Canvas *canvas, Vector pos, char *username);
 void draw_username(Canvas *canvas, Vector pos, char *username);
-void spawn_icon(GameManager *manager, Level *level, const char *icon_id, float x, float y);
-void spawn_icon_line(GameManager *manager, Level *level, const char *icon_id, float x, float y, uint8_t amount, bool horizontal, uint8_t spacing);
-extern char g_name[32];
-void background_render(Canvas *canvas, GameManager *manager);
+void draw_background_render(Canvas *canvas, GameManager *manager);

+ 21 - 21
game/enemy.c

@@ -7,7 +7,7 @@
 static EntityContext *enemy_context_generic;
 static EntityContext *enemy_context_generic;
 // Allocation function
 // Allocation function
 static EntityContext *enemy_generic_alloc(
 static EntityContext *enemy_generic_alloc(
-    const char *id,
+    SpriteID id,
     int index,
     int index,
     Vector size,
     Vector size,
     Vector start_position,
     Vector start_position,
@@ -29,7 +29,7 @@ static EntityContext *enemy_generic_alloc(
         FURI_LOG_E("Game", "Failed to allocate EntityContext");
         FURI_LOG_E("Game", "Failed to allocate EntityContext");
         return NULL;
         return NULL;
     }
     }
-    snprintf(enemy_context_generic->id, sizeof(enemy_context_generic->id), "%s", id);
+    enemy_context_generic->id = id;
     enemy_context_generic->index = index;
     enemy_context_generic->index = index;
     enemy_context_generic->size = size;
     enemy_context_generic->size = size;
     enemy_context_generic->start_position = start_position;
     enemy_context_generic->start_position = start_position;
@@ -78,7 +78,7 @@ static void enemy_start(Entity *self, GameManager *manager, void *context)
 
 
     EntityContext *enemy_context = (EntityContext *)context;
     EntityContext *enemy_context = (EntityContext *)context;
     // Copy fields from generic context
     // Copy fields from generic context
-    snprintf(enemy_context->id, sizeof(enemy_context->id), "%s", enemy_context_generic->id);
+    enemy_context->id = enemy_context_generic->id;
     enemy_context->index = enemy_context_generic->index;
     enemy_context->index = enemy_context_generic->index;
     enemy_context->size = enemy_context_generic->size;
     enemy_context->size = enemy_context_generic->size;
     enemy_context->start_position = enemy_context_generic->start_position;
     enemy_context->start_position = enemy_context_generic->start_position;
@@ -117,8 +117,8 @@ static void enemy_render(Entity *self, GameManager *manager, Canvas *canvas, voi
     Vector pos = entity_pos_get(self);
     Vector pos = entity_pos_get(self);
 
 
     // Get the camera position
     // Get the camera position
-    int x_pos = pos.x - camera_x - enemy_context->size.x / 2;
-    int y_pos = pos.y - camera_y - enemy_context->size.y / 2;
+    int x_pos = pos.x - draw_camera_x - enemy_context->size.x / 2;
+    int y_pos = pos.y - draw_camera_y - enemy_context->size.y / 2;
 
 
     // check if position is within the screen
     // check if position is within the screen
     if (x_pos + enemy_context->size.x < 0 || x_pos > SCREEN_WIDTH || y_pos + enemy_context->size.y < 0 || y_pos > SCREEN_HEIGHT)
     if (x_pos + enemy_context->size.x < 0 || x_pos > SCREEN_WIDTH || y_pos + enemy_context->size.y < 0 || y_pos > SCREEN_HEIGHT)
@@ -143,8 +143,8 @@ static void enemy_render(Entity *self, GameManager *manager, Canvas *canvas, voi
         canvas_draw_sprite(
         canvas_draw_sprite(
             canvas,
             canvas,
             current_sprite,
             current_sprite,
-            pos.x - camera_x - (enemy_context->size.x / 2),
-            pos.y - camera_y - (enemy_context->size.y / 2));
+            pos.x - draw_camera_x - (enemy_context->size.x / 2),
+            pos.y - draw_camera_y - (enemy_context->size.y / 2));
 
 
         // draw health of enemy
         // draw health of enemy
         char health_str[32];
         char health_str[32];
@@ -153,7 +153,7 @@ static void enemy_render(Entity *self, GameManager *manager, Canvas *canvas, voi
     }
     }
 }
 }
 
 
-static void atk_notify(GameContext *game_context, EntityContext *enemy_context, bool player_attacked)
+static void enemy_atk_notify(GameContext *game_context, EntityContext *enemy_context, bool player_attacked)
 {
 {
     if (!game_context || !enemy_context)
     if (!game_context || !enemy_context)
     {
     {
@@ -185,7 +185,7 @@ static void atk_notify(GameContext *game_context, EntityContext *enemy_context,
         {
         {
             notification_message(notifications, &sequence_blink_blue_100);
             notification_message(notifications, &sequence_blink_blue_100);
         }
         }
-        FURI_LOG_I("Game", "Player attacked enemy '%s'!", enemy_context->id);
+        FURI_LOG_I("Game", "Player attacked enemy '%d'!", enemy_context->id);
     }
     }
     else
     else
     {
     {
@@ -207,7 +207,7 @@ static void atk_notify(GameContext *game_context, EntityContext *enemy_context,
             notification_message(notifications, &sequence_blink_red_100);
             notification_message(notifications, &sequence_blink_red_100);
         }
         }
 
 
-        FURI_LOG_I("Game", "Enemy '%s' attacked the player!", enemy_context->id);
+        FURI_LOG_I("Game", "Enemy '%d' attacked the player!", enemy_context->id);
     }
     }
 
 
     // close the notifications
     // close the notifications
@@ -267,7 +267,7 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
         {
         {
             if (game_context->game_mode == GAME_MODE_STORY && game_context->tutorial_step == 4)
             if (game_context->game_mode == GAME_MODE_STORY && game_context->tutorial_step == 4)
             {
             {
-                // FURI_LOG_I("Game", "Player attacked enemy '%s'!", enemy_context->id);
+                // FURI_LOG_I("Game", "Player attacked enemy '%d'!", enemy_context->id);
                 game_context->tutorial_step++;
                 game_context->tutorial_step++;
             }
             }
             // Reset last button
             // Reset last button
@@ -275,7 +275,7 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
 
 
             if (player_context->elapsed_attack_timer >= player_context->attack_timer)
             if (player_context->elapsed_attack_timer >= player_context->attack_timer)
             {
             {
-                atk_notify(game_context, enemy_context, true);
+                enemy_atk_notify(game_context, enemy_context, true);
 
 
                 // Reset player's elapsed attack timer
                 // Reset player's elapsed attack timer
                 player_context->elapsed_attack_timer = 0.0f;
                 player_context->elapsed_attack_timer = 0.0f;
@@ -334,7 +334,7 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
             }
             }
             else
             else
             {
             {
-                FURI_LOG_I("Game", "Player attack on enemy '%s' is on cooldown: %f seconds remaining", enemy_context->id, (double)(player_context->attack_timer - player_context->elapsed_attack_timer));
+                FURI_LOG_I("Game", "Player attack on enemy '%d' is on cooldown: %f seconds remaining", enemy_context->id, (double)(player_context->attack_timer - player_context->elapsed_attack_timer));
             }
             }
         }
         }
         // Handle Enemy Attacking Player (enemy facing player)
         // Handle Enemy Attacking Player (enemy facing player)
@@ -342,7 +342,7 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
         {
         {
             if (enemy_context->elapsed_attack_timer >= enemy_context->attack_timer)
             if (enemy_context->elapsed_attack_timer >= enemy_context->attack_timer)
             {
             {
-                atk_notify(game_context, enemy_context, false);
+                enemy_atk_notify(game_context, enemy_context, false);
 
 
                 // Reset enemy's elapsed attack timer
                 // Reset enemy's elapsed attack timer
                 enemy_context->elapsed_attack_timer = 0.0f;
                 enemy_context->elapsed_attack_timer = 0.0f;
@@ -377,7 +377,7 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
                 }
                 }
                 else
                 else
                 {
                 {
-                    FURI_LOG_I("Game", "Player took %f damage from enemy '%s'", (double)enemy_context->strength, enemy_context->id);
+                    FURI_LOG_I("Game", "Player took %f damage from enemy '%d'", (double)enemy_context->strength, enemy_context->id);
                     player_context->state = ENTITY_ATTACKED;
                     player_context->state = ENTITY_ATTACKED;
 
 
                     // Bounce the player back by X units opposite their last movement direction
                     // Bounce the player back by X units opposite their last movement direction
@@ -433,7 +433,7 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
     }
     }
 }
 }
 
 
-static void pvp_position(GameContext *game_context, EntityContext *enemy, Entity *self)
+static void enemy_pvp_position(GameContext *game_context, EntityContext *enemy, Entity *self)
 {
 {
     if (!game_context || !enemy || !self)
     if (!game_context || !enemy || !self)
     {
     {
@@ -564,7 +564,7 @@ static void enemy_update(Entity *self, GameManager *manager, void *context)
     if (game_context->game_mode == GAME_MODE_PVP)
     if (game_context->game_mode == GAME_MODE_PVP)
     {
     {
         // update enemy position
         // update enemy position
-        pvp_position(game_context, enemy_context, self);
+        enemy_pvp_position(game_context, enemy_context, self);
     }
     }
     else
     else
     {
     {
@@ -708,7 +708,7 @@ static const EntityDescription _generic_enemy = {
 };
 };
 
 
 // Enemy function to return the entity description
 // Enemy function to return the entity description
-const EntityDescription *enemy(
+static const EntityDescription *enemy(
     GameManager *manager,
     GameManager *manager,
     const char *id,
     const char *id,
     int index,
     int index,
@@ -723,7 +723,7 @@ const EntityDescription *enemy(
     FuriString *username)
     FuriString *username)
 
 
 {
 {
-    SpriteContext *sprite_context = get_sprite_context(id);
+    SpriteContext *sprite_context = sprite_context_get(id);
     if (!sprite_context)
     if (!sprite_context)
     {
     {
         FURI_LOG_E("Game", "Failed to get SpriteContext");
         FURI_LOG_E("Game", "Failed to get SpriteContext");
@@ -732,7 +732,7 @@ const EntityDescription *enemy(
 
 
     // Allocate a new EntityContext with provided parameters
     // Allocate a new EntityContext with provided parameters
     enemy_context_generic = enemy_generic_alloc(
     enemy_context_generic = enemy_generic_alloc(
-        id,
+        sprite_context->id,
         index,
         index,
         (Vector){sprite_context->width, sprite_context->height},
         (Vector){sprite_context->width, sprite_context->height},
         start_position,
         start_position,
@@ -776,7 +776,7 @@ const EntityDescription *enemy(
     return &_generic_enemy;
     return &_generic_enemy;
 }
 }
 
 
-void spawn_enemy(Level *level, GameManager *manager, FuriString *json)
+void enemy_spawn(Level *level, GameManager *manager, FuriString *json)
 {
 {
     if (!level || !manager || !json)
     if (!level || !manager || !json)
     {
     {

+ 1 - 1
game/enemy.h

@@ -2,4 +2,4 @@
 #include <game/game.h>
 #include <game/game.h>
 #include "flip_world.h"
 #include "flip_world.h"
 
 
-void spawn_enemy(Level *level, GameManager *manager, FuriString *json);
+void enemy_spawn(Level *level, GameManager *manager, FuriString *json);

+ 3 - 3
game/game.c

@@ -53,7 +53,7 @@ static void game_start(GameManager *game_manager, void *ctx)
             {
             {
                 if (i == 0)
                 if (i == 0)
                 {
                 {
-                    game_context->levels[0] = game_manager_add_level(game_manager, training_world());
+                    game_context->levels[0] = game_manager_add_level(game_manager, world_training());
                     game_context->level_count = 1;
                     game_context->level_count = 1;
                 }
                 }
                 break;
                 break;
@@ -65,13 +65,13 @@ static void game_start(GameManager *game_manager, void *ctx)
     else if (game_context->game_mode == GAME_MODE_STORY)
     else if (game_context->game_mode == GAME_MODE_STORY)
     {
     {
         // show tutorial only for now
         // show tutorial only for now
-        game_context->levels[0] = game_manager_add_level(game_manager, training_world());
+        game_context->levels[0] = game_manager_add_level(game_manager, world_training());
         game_context->level_count = 1;
         game_context->level_count = 1;
     }
     }
     else if (game_context->game_mode == GAME_MODE_PVP)
     else if (game_context->game_mode == GAME_MODE_PVP)
     {
     {
         // show pvp
         // show pvp
-        game_context->levels[0] = game_manager_add_level(game_manager, pvp_world());
+        game_context->levels[0] = game_manager_add_level(game_manager, world_pvp());
         game_context->level_count = 1;
         game_context->level_count = 1;
     }
     }
 
 

+ 46 - 127
game/icon.c

@@ -1,151 +1,70 @@
 #include "game/icon.h"
 #include "game/icon.h"
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
 
 
-static void icon_collision(Entity *self, Entity *other, GameManager *manager, void *context)
+IconGroupContext *g_current_icon_group = NULL;
+
+// ---------------------------------------------------------------------
+// Icon Group Entity Implementation
+// ---------------------------------------------------------------------
+
+// Render callback: iterate over the group and draw each icon.
+static void icon_group_render(Entity *self, GameManager *manager, Canvas *canvas, void *context)
 {
 {
-    UNUSED(manager);
     UNUSED(self);
     UNUSED(self);
-    IconContext *ictx = (IconContext *)context;
-    if (ictx && entity_description_get(other) == &player_desc)
+    UNUSED(manager);
+    IconGroupContext *igctx = (IconGroupContext *)context;
+    if (!igctx)
     {
     {
-        PlayerContext *player = (PlayerContext *)entity_context_get(other);
-        if (player)
+        FURI_LOG_E("Game", "Icon group context is NULL in render");
+        return;
+    }
+    for (int i = 0; i < igctx->count; i++)
+    {
+        IconSpec *spec = &igctx->icons[i];
+        int x_pos = spec->pos.x - draw_camera_x - (spec->size.x / 2);
+        int y_pos = spec->pos.y - draw_camera_y - (spec->size.y / 2);
+        if (x_pos + spec->size.x < 0 || x_pos > SCREEN_WIDTH ||
+            y_pos + spec->size.y < 0 || y_pos > SCREEN_HEIGHT)
         {
         {
-            // Set the player's old position to prevent collision
-            entity_pos_set(other, player->old_position);
-
-            // Reset movement to prevent re-collision
-            player->dx = 0;
-            player->dy = 0;
+            continue;
         }
         }
+        canvas_draw_icon(canvas, x_pos, y_pos, spec->icon);
     }
     }
 }
 }
 
 
-static void icon_render(Entity *self, GameManager *manager, Canvas *canvas, void *context)
+// Start callback: nothing special is needed here.
+static void icon_group_start(Entity *self, GameManager *manager, void *context)
 {
 {
+    UNUSED(self);
     UNUSED(manager);
     UNUSED(manager);
-    IconContext *ictx = (IconContext *)context;
-    furi_check(ictx, "Icon context is NULL");
-    Vector pos = entity_pos_get(self);
-    int x_pos = pos.x - camera_x - ictx->size.x / 2;
-    int y_pos = pos.y - camera_y - ictx->size.y / 2;
-    // check if position is within the screen
-    if (x_pos + ictx->size.x < 0 || x_pos > SCREEN_WIDTH || y_pos + ictx->size.y < 0 || y_pos > SCREEN_HEIGHT)
-        return;
-    canvas_draw_icon(canvas, x_pos, y_pos, ictx->icon);
+    UNUSED(context);
+    // The context is assumed to be pre-filled by the world code.
 }
 }
 
 
-static void icon_start(Entity *self, GameManager *manager, void *context)
+// Free callback: free the icon specs array.
+static void icon_group_free(Entity *self, GameManager *manager, void *context)
 {
 {
+    UNUSED(self);
     UNUSED(manager);
     UNUSED(manager);
-
-    IconContext *ictx_self = (IconContext *)context;
-    if (!ictx_self)
-    {
-        FURI_LOG_E("Game", "Icon context self is NULL");
-        return;
-    }
-    IconContext *ictx = entity_context_get(self);
-    if (!ictx)
-    {
-        FURI_LOG_E("Game", "Icon context is NULL");
-        return;
-    }
-
-    IconContext *loaded_data = get_icon_context(g_name);
-    if (!loaded_data)
+    IconGroupContext *igctx = (IconGroupContext *)context;
+    if (igctx && igctx->icons)
     {
     {
-        FURI_LOG_E("Game", "Failed to find icon data for %s", g_name);
-        return;
+        free(igctx->icons);
+        igctx->icons = NULL;
     }
     }
-
-    ictx_self->icon = loaded_data->icon;
-    ictx_self->size = (Vector){loaded_data->size.x, loaded_data->size.y};
-    ictx->icon = loaded_data->icon;
-    ictx->size = (Vector){loaded_data->size.x, loaded_data->size.y};
-
-    Vector pos = entity_pos_get(self);
-    pos.x += ictx_self->size.x / 2;
-    pos.y += ictx_self->size.y / 2;
-    entity_pos_set(self, pos);
-
-    entity_collider_add_circle(
-        self,
-        (ictx_self->size.x + ictx_self->size.y) / 4);
-
-    free(loaded_data);
+    g_current_icon_group = NULL;
 }
 }
 
 
-// -------------- Entity description --------------
+// The entity description for the icon group.
+// Note: we set context_size to sizeof(IconGroupContext).
 const EntityDescription icon_desc = {
 const EntityDescription icon_desc = {
-    .start = icon_start,
-    .stop = NULL,
+    .start = icon_group_start,
+    .stop = icon_group_free,
     .update = NULL,
     .update = NULL,
-    .render = icon_render,
-    .collision = icon_collision,
+    .render = icon_group_render,
+    .collision = NULL,
     .event = NULL,
     .event = NULL,
-    .context_size = sizeof(IconContext),
+    .context_size = sizeof(IconGroupContext),
 };
 };
-
-static IconContext *icon_generic_alloc(IconID id, const Icon *icon, uint8_t width, uint8_t height)
-{
-    IconContext *ctx = malloc(sizeof(IconContext));
-    if (!ctx)
-    {
-        FURI_LOG_E("Game", "Failed to allocate IconContext");
-        return NULL;
-    }
-    ctx->id = id;
-    ctx->icon = icon;
-    ctx->size = (Vector){width, height};
-    return ctx;
-}
-
-IconContext *get_icon_context(const char *name)
-{
-    if (is_str(name, "house"))
-        return icon_generic_alloc(ICON_ID_HOUSE, &I_icon_house_48x32px, 48, 32);
-    else if (is_str(name, "man"))
-        return icon_generic_alloc(ICON_ID_MAN, &I_icon_man_7x16, 7, 16);
-    else if (is_str(name, "plant"))
-        return icon_generic_alloc(ICON_ID_PLANT, &I_icon_plant_16x16, 16, 16);
-    else if (is_str(name, "tree"))
-        return icon_generic_alloc(ICON_ID_TREE, &I_icon_tree_16x16, 16, 16);
-    else if (is_str(name, "woman"))
-        return icon_generic_alloc(ICON_ID_WOMAN, &I_icon_woman_9x16, 9, 16);
-    else if (is_str(name, "fence"))
-        return icon_generic_alloc(ICON_ID_FENCE, &I_icon_fence_16x8px, 16, 8);
-    else if (is_str(name, "fence_end"))
-        return icon_generic_alloc(ICON_ID_FENCE_END, &I_icon_fence_end_16x8px, 16, 8);
-    else if (is_str(name, "fence_vertical_end"))
-        return icon_generic_alloc(ICON_ID_FENCE_VERTICAL_END, &I_icon_fence_vertical_end_6x8px, 6, 8);
-    else if (is_str(name, "fence_vertical_start"))
-        return icon_generic_alloc(ICON_ID_FENCE_VERTICAL_START, &I_icon_fence_vertical_start_6x15px, 6, 15);
-    else if (is_str(name, "flower"))
-        return icon_generic_alloc(ICON_ID_FLOWER, &I_icon_flower_16x16, 16, 16);
-    else if (is_str(name, "lake_bottom"))
-        return icon_generic_alloc(ICON_ID_LAKE_BOTTOM, &I_icon_lake_bottom_31x12px, 31, 12);
-    else if (is_str(name, "lake_bottom_left"))
-        return icon_generic_alloc(ICON_ID_LAKE_BOTTOM_LEFT, &I_icon_lake_bottom_left_24x22px, 24, 22);
-    else if (is_str(name, "lake_bottom_right"))
-        return icon_generic_alloc(ICON_ID_LAKE_BOTTOM_RIGHT, &I_icon_lake_bottom_right_24x22px, 24, 22);
-    else if (is_str(name, "lake_left"))
-        return icon_generic_alloc(ICON_ID_LAKE_LEFT, &I_icon_lake_left_11x31px, 11, 31);
-    else if (is_str(name, "lake_right"))
-        return icon_generic_alloc(ICON_ID_LAKE_RIGHT, &I_icon_lake_right_11x31, 11, 31);
-    else if (is_str(name, "lake_top"))
-        return icon_generic_alloc(ICON_ID_LAKE_TOP, &I_icon_lake_top_31x12px, 31, 12);
-    else if (is_str(name, "lake_top_left"))
-        return icon_generic_alloc(ICON_ID_LAKE_TOP_LEFT, &I_icon_lake_top_left_24x22px, 24, 22);
-    else if (is_str(name, "lake_top_right"))
-        return icon_generic_alloc(ICON_ID_LAKE_TOP_RIGHT, &I_icon_lake_top_right_24x22px, 24, 22);
-    else if (is_str(name, "rock_large"))
-        return icon_generic_alloc(ICON_ID_ROCK_LARGE, &I_icon_rock_large_18x19px, 18, 19);
-    else if (is_str(name, "rock_medium"))
-        return icon_generic_alloc(ICON_ID_ROCK_MEDIUM, &I_icon_rock_medium_16x14px, 16, 14);
-    else if (is_str(name, "rock_small"))
-        return icon_generic_alloc(ICON_ID_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;
-}

+ 33 - 25
game/icon.h

@@ -4,35 +4,43 @@
 
 
 typedef enum
 typedef enum
 {
 {
-    ICON_ID_HOUSE,                // House
-    ICON_ID_MAN,                  // Man
-    ICON_ID_PLANT,                // Plant
-    ICON_ID_TREE,                 // Tree
-    ICON_ID_WOMAN,                // Woman
-    ICON_ID_FENCE,                // Fence
-    ICON_ID_FENCE_END,            // Fence end
-    ICON_ID_FENCE_VERTICAL_END,   // Vertical fence end
-    ICON_ID_FENCE_VERTICAL_START, // Vertical fence start
-    ICON_ID_FLOWER,               // Flower
-    ICON_ID_LAKE_BOTTOM,          // Lake bottom
-    ICON_ID_LAKE_BOTTOM_LEFT,     // Lake bottom left
-    ICON_ID_LAKE_BOTTOM_RIGHT,    // Lake bottom right
-    ICON_ID_LAKE_LEFT,            // Lake left
-    ICON_ID_LAKE_RIGHT,           // Lake right
-    ICON_ID_LAKE_TOP,             // Lake top
-    ICON_ID_LAKE_TOP_LEFT,        // Lake top left
-    ICON_ID_LAKE_TOP_RIGHT,       // Lake top right
-    ICON_ID_ROCK_LARGE,           // Large rock
-    ICON_ID_ROCK_MEDIUM,          // Medium rock
-    ICON_ID_ROCK_SMALL,           // Small rock
+    ICON_ID_HOUSE,
+    ICON_ID_MAN,
+    ICON_ID_PLANT,
+    ICON_ID_TREE,
+    ICON_ID_WOMAN,
+    ICON_ID_FENCE,
+    ICON_ID_FENCE_END,
+    ICON_ID_FENCE_VERTICAL_END,
+    ICON_ID_FENCE_VERTICAL_START,
+    ICON_ID_FLOWER,
+    ICON_ID_LAKE_BOTTOM,
+    ICON_ID_LAKE_BOTTOM_LEFT,
+    ICON_ID_LAKE_BOTTOM_RIGHT,
+    ICON_ID_LAKE_LEFT,
+    ICON_ID_LAKE_RIGHT,
+    ICON_ID_LAKE_TOP,
+    ICON_ID_LAKE_TOP_LEFT,
+    ICON_ID_LAKE_TOP_RIGHT,
+    ICON_ID_ROCK_LARGE,
+    ICON_ID_ROCK_MEDIUM,
+    ICON_ID_ROCK_SMALL,
 } IconID;
 } IconID;
 
 
 typedef struct
 typedef struct
 {
 {
     IconID id;
     IconID id;
     const Icon *icon;
     const Icon *icon;
-    Vector size;
-} IconContext;
+    Vector pos;  // position at which to draw the icon
+    Vector size; // dimensions for centering
+} IconSpec;
 
 
-extern const EntityDescription icon_desc;
-IconContext *get_icon_context(const char *name);
+typedef struct
+{
+    int count;       // number of icons in this group
+    IconSpec *icons; // pointer to an array of icon specs
+} IconGroupContext;
+
+extern IconGroupContext *g_current_icon_group;
+
+extern const EntityDescription icon_desc;

+ 9 - 9
game/level.c

@@ -12,7 +12,7 @@ bool allocate_level(GameManager *manager, int index)
     if (!world_list)
     if (!world_list)
     {
     {
         FURI_LOG_E("Game", "Failed to load world list");
         FURI_LOG_E("Game", "Failed to load world list");
-        game_context->levels[0] = game_manager_add_level(manager, training_world());
+        game_context->levels[0] = game_manager_add_level(manager, world_training());
         game_context->level_count = 1;
         game_context->level_count = 1;
         return false;
         return false;
     }
     }
@@ -29,7 +29,7 @@ bool allocate_level(GameManager *manager, int index)
     furi_string_free(world_list);
     furi_string_free(world_list);
     return true;
     return true;
 }
 }
-void set_world(Level *level, GameManager *manager, char *id)
+void level_set_world(Level *level, GameManager *manager, char *id)
 {
 {
     char file_path[256];
     char file_path[256];
     snprintf(file_path, sizeof(file_path),
     snprintf(file_path, sizeof(file_path),
@@ -45,7 +45,7 @@ void set_world(Level *level, GameManager *manager, char *id)
         return;
         return;
     }
     }
 
 
-    if (!is_enough_heap(28400, true))
+    if (!is_enough_heap(20000, true))
     {
     {
         FURI_LOG_E("Game", "Not enough heap memory.. ending game early.");
         FURI_LOG_E("Game", "Not enough heap memory.. ending game early.");
         GameContext *game_context = game_manager_game_context_get(manager);
         GameContext *game_context = game_manager_game_context_get(manager);
@@ -56,7 +56,7 @@ void set_world(Level *level, GameManager *manager, char *id)
     }
     }
 
 
     FURI_LOG_I("Game", "Drawing world");
     FURI_LOG_I("Game", "Drawing world");
-    if (!draw_json_world_furi(manager, level, json_data_str))
+    if (!world_json_draw(manager, level, json_data_str))
     {
     {
         FURI_LOG_E("Game", "Failed to draw world");
         FURI_LOG_E("Game", "Failed to draw world");
         furi_string_free(json_data_str);
         furi_string_free(json_data_str);
@@ -90,7 +90,7 @@ void set_world(Level *level, GameManager *manager, char *id)
                 break;
                 break;
             }
             }
 
 
-            spawn_enemy(level, manager, single_enemy_data);
+            enemy_spawn(level, manager, single_enemy_data);
             furi_string_free(single_enemy_data);
             furi_string_free(single_enemy_data);
         }
         }
         furi_string_free(enemy_data_str);
         furi_string_free(enemy_data_str);
@@ -122,7 +122,7 @@ void set_world(Level *level, GameManager *manager, char *id)
                 break;
                 break;
             }
             }
 
 
-            spawn_npc(level, manager, single_npc_data);
+            npc_spawn(level, manager, single_npc_data);
             furi_string_free(single_npc_data);
             furi_string_free(single_npc_data);
         }
         }
         furi_string_free(npc_data_str);
         furi_string_free(npc_data_str);
@@ -159,7 +159,7 @@ static void level_start(Level *level, GameManager *manager, void *context)
     if (!world_exists(level_context->id))
     if (!world_exists(level_context->id))
     {
     {
         FURI_LOG_E("Game", "World does not exist.. downloading now");
         FURI_LOG_E("Game", "World does not exist.. downloading now");
-        FuriString *world_data = fetch_world(level_context->id);
+        FuriString *world_data = world_fetch(level_context->id);
         if (!world_data)
         if (!world_data)
         {
         {
             FURI_LOG_E("Game", "Failed to fetch world data");
             FURI_LOG_E("Game", "Failed to fetch world data");
@@ -171,7 +171,7 @@ static void level_start(Level *level, GameManager *manager, void *context)
         }
         }
         furi_string_free(world_data);
         furi_string_free(world_data);
 
 
-        set_world(level, manager, level_context->id);
+        level_set_world(level, manager, level_context->id);
         FURI_LOG_I("Game", "World set.");
         FURI_LOG_I("Game", "World set.");
         // furi_delay_ms(1000);
         // furi_delay_ms(1000);
         game_context->is_switching_level = false;
         game_context->is_switching_level = false;
@@ -179,7 +179,7 @@ static void level_start(Level *level, GameManager *manager, void *context)
     else
     else
     {
     {
         FURI_LOG_I("Game", "World exists.. loading now");
         FURI_LOG_I("Game", "World exists.. loading now");
-        set_world(level, manager, level_context->id);
+        level_set_world(level, manager, level_context->id);
         FURI_LOG_I("Game", "World set.");
         FURI_LOG_I("Game", "World set.");
         // furi_delay_ms(1000);
         // furi_delay_ms(1000);
         game_context->is_switching_level = false;
         game_context->is_switching_level = false;

+ 1 - 1
game/level.h

@@ -9,4 +9,4 @@ typedef struct
 
 
 const LevelBehaviour *generic_level(const char *id, int index);
 const LevelBehaviour *generic_level(const char *id, int index);
 bool allocate_level(GameManager *manager, int index);
 bool allocate_level(GameManager *manager, int index);
-void set_world(Level *level, GameManager *manager, char *id);
+void level_set_world(Level *level, GameManager *manager, char *id);

+ 12 - 11
game/npc.c

@@ -3,7 +3,7 @@ static EntityContext *npc_context_generic;
 
 
 // Allocation function
 // Allocation function
 static EntityContext *npc_generic_alloc(
 static EntityContext *npc_generic_alloc(
-    const char *id,
+    SpriteID id,
     int index,
     int index,
     Vector size,
     Vector size,
     Vector start_position,
     Vector start_position,
@@ -21,7 +21,7 @@ static EntityContext *npc_generic_alloc(
         FURI_LOG_E("Game", "Failed to allocate EntityContext");
         FURI_LOG_E("Game", "Failed to allocate EntityContext");
         return NULL;
         return NULL;
     }
     }
-    snprintf(npc_context_generic->id, sizeof(npc_context_generic->id), "%s", id);
+    npc_context_generic->id = id;
     npc_context_generic->index = index;
     npc_context_generic->index = index;
     npc_context_generic->size = size;
     npc_context_generic->size = size;
     npc_context_generic->start_position = start_position;
     npc_context_generic->start_position = start_position;
@@ -57,7 +57,7 @@ static void npc_start(Entity *self, GameManager *manager, void *context)
 
 
     EntityContext *npc_context = (EntityContext *)context;
     EntityContext *npc_context = (EntityContext *)context;
     // Copy fields from generic context
     // Copy fields from generic context
-    snprintf(npc_context->id, sizeof(npc_context->id), "%s", npc_context_generic->id);
+    npc_context->id = npc_context_generic->id;
     snprintf(npc_context->message, sizeof(npc_context->message), "%s", npc_context_generic->message);
     snprintf(npc_context->message, sizeof(npc_context->message), "%s", npc_context_generic->message);
     npc_context->index = npc_context_generic->index;
     npc_context->index = npc_context_generic->index;
     npc_context->size = npc_context_generic->size;
     npc_context->size = npc_context_generic->size;
@@ -91,8 +91,8 @@ static void npc_render(Entity *self, GameManager *manager, Canvas *canvas, void
     // Get the position of the NPC
     // Get the position of the NPC
     Vector pos = entity_pos_get(self);
     Vector pos = entity_pos_get(self);
 
 
-    int x_pos = pos.x - camera_x - npc_context->size.x / 2;
-    int y_pos = pos.y - camera_y - npc_context->size.y / 2;
+    int x_pos = pos.x - draw_camera_x - npc_context->size.x / 2;
+    int y_pos = pos.y - draw_camera_y - npc_context->size.y / 2;
 
 
     // check if position is within the screen
     // check if position is within the screen
     if (x_pos + npc_context->size.x < 0 || x_pos > SCREEN_WIDTH || y_pos + npc_context->size.y < 0 || y_pos > SCREEN_HEIGHT)
     if (x_pos + npc_context->size.x < 0 || x_pos > SCREEN_WIDTH || y_pos + npc_context->size.y < 0 || y_pos > SCREEN_HEIGHT)
@@ -115,8 +115,8 @@ static void npc_render(Entity *self, GameManager *manager, Canvas *canvas, void
         canvas_draw_sprite(
         canvas_draw_sprite(
             canvas,
             canvas,
             current_sprite,
             current_sprite,
-            pos.x - camera_x - (npc_context->size.x / 2),
-            pos.y - camera_y - (npc_context->size.y / 2));
+            pos.x - draw_camera_x - (npc_context->size.x / 2),
+            pos.y - draw_camera_y - (npc_context->size.y / 2));
     }
     }
 }
 }
 
 
@@ -324,7 +324,7 @@ static const EntityDescription _generic_npc = {
 };
 };
 
 
 // Spawn function to return the entity description
 // Spawn function to return the entity description
-const EntityDescription *npc(
+static const EntityDescription *npc(
     GameManager *manager,
     GameManager *manager,
     const char *id,
     const char *id,
     int index,
     int index,
@@ -334,7 +334,7 @@ const EntityDescription *npc(
     float speed,
     float speed,
     const char *message)
     const char *message)
 {
 {
-    SpriteContext *sprite_context = get_sprite_context(id);
+    SpriteContext *sprite_context = sprite_context_get(id);
     if (!sprite_context)
     if (!sprite_context)
     {
     {
         FURI_LOG_E("Game", "Failed to get SpriteContext");
         FURI_LOG_E("Game", "Failed to get SpriteContext");
@@ -343,7 +343,7 @@ const EntityDescription *npc(
 
 
     // Allocate a new EntityContext with provided parameters
     // Allocate a new EntityContext with provided parameters
     npc_context_generic = npc_generic_alloc(
     npc_context_generic = npc_generic_alloc(
-        id,
+        sprite_context->id,
         index,
         index,
         (Vector){sprite_context->width, sprite_context->height},
         (Vector){sprite_context->width, sprite_context->height},
         start_position,
         start_position,
@@ -384,7 +384,7 @@ const EntityDescription *npc(
     return &_generic_npc;
     return &_generic_npc;
 }
 }
 
 
-void spawn_npc(Level *level, GameManager *manager, FuriString *json)
+void npc_spawn(Level *level, GameManager *manager, FuriString *json)
 {
 {
     if (!level || !manager || !json)
     if (!level || !manager || !json)
     {
     {
@@ -439,4 +439,5 @@ void spawn_npc(Level *level, GameManager *manager, FuriString *json)
     furi_string_free(end_position_y);
     furi_string_free(end_position_y);
     furi_string_free(move_timer);
     furi_string_free(move_timer);
     furi_string_free(speed);
     furi_string_free(speed);
+    furi_string_free(message);
 }
 }

+ 1 - 1
game/npc.h

@@ -2,4 +2,4 @@
 #include <game/game.h>
 #include <game/game.h>
 #include "flip_world.h"
 #include "flip_world.h"
 
 
-void spawn_npc(Level *level, GameManager *manager, FuriString *json);
+void npc_spawn(Level *level, GameManager *manager, FuriString *json);

+ 80 - 43
game/player.c

@@ -1,11 +1,12 @@
 #include <game/player.h>
 #include <game/player.h>
+#include <game/icon.h>
 #include <game/storage.h>
 #include <game/storage.h>
 #include <stdio.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <stdlib.h>
 #include <math.h>
 #include <math.h>
 #include <engine/entity_i.h>
 #include <engine/entity_i.h>
 /****** Entities: Player ******/
 /****** Entities: Player ******/
-static Level *next_level(GameManager *manager)
+static Level *player_next_level(GameManager *manager)
 {
 {
     GameContext *game_context = game_manager_game_context_get(manager);
     GameContext *game_context = game_manager_game_context_get(manager);
     if (!game_context)
     if (!game_context)
@@ -55,7 +56,7 @@ static Level *next_level(GameManager *manager)
 }
 }
 
 
 // Update player stats based on XP using iterative method
 // Update player stats based on XP using iterative method
-static int get_player_level_iterative(uint32_t xp)
+static int player_level_iterative_get(uint32_t xp)
 {
 {
     int level = 1;
     int level = 1;
     uint32_t xp_required = 100; // Base XP for level 2
     uint32_t xp_required = 100; // Base XP for level 2
@@ -102,7 +103,7 @@ void player_spawn(Level *level, GameManager *manager)
         return;
         return;
     }
     }
 
 
-    SpriteContext *sprite_context = get_sprite_context(player_sprite_choices[player_sprite_index]);
+    SpriteContext *sprite_context = sprite_context_get(player_sprite_choices[player_sprite_index]);
     if (!sprite_context)
     if (!sprite_context)
     {
     {
         FURI_LOG_E(TAG, "Failed to get sprite context");
         FURI_LOG_E(TAG, "Failed to get sprite context");
@@ -159,7 +160,7 @@ void player_spawn(Level *level, GameManager *manager)
     pctx->start_position = entity_pos_get(game_context->player);
     pctx->start_position = entity_pos_get(game_context->player);
 
 
     // Determine the player's level based on XP
     // Determine the player's level based on XP
-    pctx->level = get_player_level_iterative(pctx->xp);
+    pctx->level = player_level_iterative_get(pctx->xp);
 
 
     // Update strength and max health based on the new level
     // Update strength and max health based on the new level
     pctx->strength = 10 + (pctx->level * 1);           // 1 strength per level
     pctx->strength = 10 + (pctx->level * 1);           // 1 strength per level
@@ -171,13 +172,13 @@ void player_spawn(Level *level, GameManager *manager)
     free(sprite_context);
     free(sprite_context);
 }
 }
 
 
-static int vgm_increase(float value, float increase)
+static int player_vgm_increase(float value, float increase)
 {
 {
     const int val = abs((int)(round(value + increase) / 2));
     const int val = abs((int)(round(value + increase) / 2));
     return val < 1 ? 1 : val;
     return val < 1 ? 1 : val;
 }
 }
 
 
-static void vgm_direction(Imu *imu, PlayerContext *player, Vector *pos)
+static void player_vgm_direction(Imu *imu, PlayerContext *player, Vector *pos)
 {
 {
     const float pitch = -imu_pitch_get(imu);
     const float pitch = -imu_pitch_get(imu);
     const float roll = -imu_roll_get(imu);
     const float roll = -imu_roll_get(imu);
@@ -185,29 +186,62 @@ static void vgm_direction(Imu *imu, PlayerContext *player, Vector *pos)
     const float min_y = atof_(vgm_levels[vgm_y_index]) + 5.0; // minimum of 3
     const float min_y = atof_(vgm_levels[vgm_y_index]) + 5.0; // minimum of 3
     if (pitch > min_x)
     if (pitch > min_x)
     {
     {
-        pos->x += vgm_increase(pitch, min_x);
+        pos->x += player_vgm_increase(pitch, min_x);
         player->dx = 1;
         player->dx = 1;
         player->direction = ENTITY_RIGHT;
         player->direction = ENTITY_RIGHT;
     }
     }
     else if (pitch < -min_x)
     else if (pitch < -min_x)
     {
     {
-        pos->x += -vgm_increase(pitch, min_x);
+        pos->x += -player_vgm_increase(pitch, min_x);
         player->dx = -1;
         player->dx = -1;
         player->direction = ENTITY_LEFT;
         player->direction = ENTITY_LEFT;
     }
     }
     if (roll > min_y)
     if (roll > min_y)
     {
     {
-        pos->y += vgm_increase(roll, min_y);
+        pos->y += player_vgm_increase(roll, min_y);
         player->dy = 1;
         player->dy = 1;
         player->direction = ENTITY_DOWN;
         player->direction = ENTITY_DOWN;
     }
     }
     else if (roll < -min_y)
     else if (roll < -min_y)
     {
     {
-        pos->y += -vgm_increase(roll, min_y);
+        pos->y += -player_vgm_increase(roll, min_y);
         player->dy = -1;
         player->dy = -1;
         player->direction = ENTITY_UP;
         player->direction = ENTITY_UP;
     }
     }
 }
 }
+
+// This static function handles collisions with icons.
+// It receives the player entity pointer, the player's current position, and a pointer to PlayerContext.
+static void player_handle_collision(Entity *playerEntity, Vector playerPos, PlayerContext *player)
+{
+    // If there is no active icon group, do nothing.
+    if (!g_current_icon_group)
+        return;
+
+    // Loop over all icon specifications in the current icon group.
+    for (int i = 0; i < g_current_icon_group->count; i++)
+    {
+        IconSpec *spec = &g_current_icon_group->icons[i];
+
+        // Calculate the difference between player's position and the icon's center.
+        float dx = playerPos.x - spec->pos.x;
+        float dy = playerPos.y - spec->pos.y;
+
+        // Use an approximate collision radius:
+        float radius = (spec->size.x + spec->size.y) / 4.0f;
+
+        // Collision: if player's distance to the icon center is less than the collision radius.
+        if ((dx * dx + dy * dy) < (radius * radius))
+        {
+            // Revert the player's position and reset movement.
+            entity_pos_set(playerEntity, player->old_position);
+            player->dx = 0;
+            player->dy = 0;
+            break;
+        }
+    }
+}
+
 uint16_t elapsed_ws_timer = 0;
 uint16_t elapsed_ws_timer = 0;
 static void player_update(Entity *self, GameManager *manager, void *context)
 static void player_update(Entity *self, GameManager *manager, void *context)
 {
 {
@@ -251,7 +285,7 @@ static void player_update(Entity *self, GameManager *manager, void *context)
     player->old_position = pos;
     player->old_position = pos;
 
 
     // Determine the player's level based on XP
     // Determine the player's level based on XP
-    player->level = get_player_level_iterative(player->xp);
+    player->level = player_level_iterative_get(player->xp);
     player->strength = 10 + (player->level * 1);           // 1 strength per level
     player->strength = 10 + (player->level * 1);           // 1 strength per level
     player->max_health = 100 + ((player->level - 1) * 10); // 10 health per level
     player->max_health = 100 + ((player->level - 1) * 10); // 10 health per level
 
 
@@ -266,7 +300,7 @@ static void player_update(Entity *self, GameManager *manager, void *context)
     if (game_context->imu_present)
     if (game_context->imu_present)
     {
     {
         // update position using the IMU
         // update position using the IMU
-        vgm_direction(game_context->imu, player, &pos);
+        player_vgm_direction(game_context->imu, player, &pos);
     }
     }
 
 
     // Apply health regeneration
     // Apply health regeneration
@@ -387,7 +421,7 @@ static void player_update(Entity *self, GameManager *manager, void *context)
             game_context->is_switching_level = true;
             game_context->is_switching_level = true;
             save_player_context(player);
             save_player_context(player);
             furi_delay_ms(100);
             furi_delay_ms(100);
-            game_manager_next_level_set(manager, next_level(manager));
+            game_manager_next_level_set(manager, player_next_level(manager));
             return;
             return;
         }
         }
 
 
@@ -479,9 +513,12 @@ static void player_update(Entity *self, GameManager *manager, void *context)
     }
     }
     else
     else
         player->state = ENTITY_MOVING;
         player->state = ENTITY_MOVING;
+
+    // handle icon collision
+    player_handle_collision(self, pos, player);
 }
 }
 
 
-static void draw_tutorial(Canvas *canvas, GameManager *manager)
+static void player_draw_tutorial(Canvas *canvas, GameManager *manager)
 {
 {
     GameContext *game_context = game_manager_game_context_get(manager);
     GameContext *game_context = game_manager_game_context_get(manager);
     canvas_set_font(canvas, FontPrimary);
     canvas_set_font(canvas, FontPrimary);
@@ -540,12 +577,12 @@ static void player_render(Entity *self, GameManager *manager, Canvas *canvas, vo
     Vector pos = entity_pos_get(self);
     Vector pos = entity_pos_get(self);
 
 
     // Calculate camera offset to center the player
     // Calculate camera offset to center the player
-    camera_x = pos.x - (SCREEN_WIDTH / 2);
-    camera_y = pos.y - (SCREEN_HEIGHT / 2);
+    draw_camera_x = pos.x - (SCREEN_WIDTH / 2);
+    draw_camera_y = pos.y - (SCREEN_HEIGHT / 2);
 
 
     // Clamp camera position to prevent showing areas outside the world
     // Clamp camera position to prevent showing areas outside the world
-    camera_x = CLAMP(camera_x, WORLD_WIDTH - SCREEN_WIDTH, 0);
-    camera_y = CLAMP(camera_y, WORLD_HEIGHT - SCREEN_HEIGHT, 0);
+    draw_camera_x = CLAMP(draw_camera_x, WORLD_WIDTH - SCREEN_WIDTH, 0);
+    draw_camera_y = CLAMP(draw_camera_y, WORLD_HEIGHT - SCREEN_HEIGHT, 0);
 
 
     // if player is moving right or left, draw the corresponding sprite
     // if player is moving right or left, draw the corresponding sprite
     if (player->direction == ENTITY_RIGHT || player->direction == ENTITY_LEFT)
     if (player->direction == ENTITY_RIGHT || player->direction == ENTITY_LEFT)
@@ -553,8 +590,8 @@ static void player_render(Entity *self, GameManager *manager, Canvas *canvas, vo
         canvas_draw_sprite(
         canvas_draw_sprite(
             canvas,
             canvas,
             player->direction == ENTITY_RIGHT ? player->sprite_right : player->sprite_left,
             player->direction == ENTITY_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
+            pos.x - draw_camera_x - 5, // Center the sprite horizontally
+            pos.y - draw_camera_y - 5  // Center the sprite vertically
         );
         );
         player->left = false;
         player->left = false;
     }
     }
@@ -564,28 +601,28 @@ static void player_render(Entity *self, GameManager *manager, Canvas *canvas, vo
         canvas_draw_sprite(
         canvas_draw_sprite(
             canvas,
             canvas,
             player->left ? player->sprite_left : player->sprite_right,
             player->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
+            pos.x - draw_camera_x - 5, // Center the sprite horizontally
+            pos.y - draw_camera_y - 5  // Center the sprite vertically
         );
         );
     }
     }
 
 
     // Draw the outer bounds adjusted by camera offset
     // Draw the outer bounds adjusted by camera offset
-    canvas_draw_frame(canvas, -camera_x, -camera_y, WORLD_WIDTH, WORLD_HEIGHT);
+    canvas_draw_frame(canvas, -draw_camera_x, -draw_camera_y, WORLD_WIDTH, WORLD_HEIGHT);
 
 
     // render tutorial
     // render tutorial
     if (game_context->game_mode == GAME_MODE_STORY)
     if (game_context->game_mode == GAME_MODE_STORY)
     {
     {
-        draw_tutorial(canvas, manager);
+        player_draw_tutorial(canvas, manager);
 
 
         if (game_context->is_menu_open)
         if (game_context->is_menu_open)
         {
         {
-            background_render(canvas, manager);
+            draw_background_render(canvas, manager);
         }
         }
     }
     }
     else
     else
     {
     {
         // render background
         // render background
-        background_render(canvas, manager);
+        draw_background_render(canvas, manager);
     }
     }
 }
 }
 
 
@@ -599,7 +636,7 @@ const EntityDescription player_desc = {
     .context_size = sizeof(PlayerContext), // size of entity context, will be automatically allocated and freed
     .context_size = sizeof(PlayerContext), // size of entity context, will be automatically allocated and freed
 };
 };
 
 
-static SpriteContext *sprite_generic_alloc(const char *id, const char *type, uint8_t width, uint8_t height)
+static SpriteContext *sprite_generic_alloc(SpriteID id, const char *char_id, const char *type, uint8_t width, uint8_t height)
 {
 {
     SpriteContext *ctx = malloc(sizeof(SpriteContext));
     SpriteContext *ctx = malloc(sizeof(SpriteContext));
     if (!ctx)
     if (!ctx)
@@ -607,47 +644,47 @@ static SpriteContext *sprite_generic_alloc(const char *id, const char *type, uin
         FURI_LOG_E("Game", "Failed to allocate SpriteContext");
         FURI_LOG_E("Game", "Failed to allocate SpriteContext");
         return NULL;
         return NULL;
     }
     }
-    snprintf(ctx->id, sizeof(ctx->id), "%s", id);
+    ctx->id = id;
     ctx->width = width;
     ctx->width = width;
     ctx->height = height;
     ctx->height = height;
     if (is_str(type, "player"))
     if (is_str(type, "player"))
     {
     {
-        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);
+        snprintf(ctx->right_file_name, sizeof(ctx->right_file_name), "player_right_%s_%dx%dpx.fxbm", char_id, width, height);
+        snprintf(ctx->left_file_name, sizeof(ctx->left_file_name), "player_left_%s_%dx%dpx.fxbm", char_id, width, height);
     }
     }
     else if (is_str(type, "enemy"))
     else if (is_str(type, "enemy"))
     {
     {
-        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);
+        snprintf(ctx->right_file_name, sizeof(ctx->right_file_name), "enemy_right_%s_%dx%dpx.fxbm", char_id, width, height);
+        snprintf(ctx->left_file_name, sizeof(ctx->left_file_name), "enemy_left_%s_%dx%dpx.fxbm", char_id, width, height);
     }
     }
     else if (is_str(type, "npc"))
     else if (is_str(type, "npc"))
     {
     {
-        snprintf(ctx->right_file_name, sizeof(ctx->right_file_name), "npc_right_%s_%dx%dpx.fxbm", id, width, height);
-        snprintf(ctx->left_file_name, sizeof(ctx->left_file_name), "npc_left_%s_%dx%dpx.fxbm", id, width, height);
+        snprintf(ctx->right_file_name, sizeof(ctx->right_file_name), "npc_right_%s_%dx%dpx.fxbm", char_id, width, height);
+        snprintf(ctx->left_file_name, sizeof(ctx->left_file_name), "npc_left_%s_%dx%dpx.fxbm", char_id, width, height);
     }
     }
     return ctx;
     return ctx;
 }
 }
 
 
-SpriteContext *get_sprite_context(const char *name)
+SpriteContext *sprite_context_get(const char *name)
 {
 {
     if (is_str(name, "axe"))
     if (is_str(name, "axe"))
-        return sprite_generic_alloc("axe", "player", 15, 11);
+        return sprite_generic_alloc(SPRITE_ID_AXE, "axe", "player", 15, 11);
     else if (is_str(name, "bow"))
     else if (is_str(name, "bow"))
-        return sprite_generic_alloc("bow", "player", 13, 11);
+        return sprite_generic_alloc(SPRITE_ID_BOW, "bow", "player", 13, 11);
     else if (is_str(name, "naked"))
     else if (is_str(name, "naked"))
-        return sprite_generic_alloc("naked", "player", 10, 10);
+        return sprite_generic_alloc(SPRITE_ID_NAKED, "naked", "player", 10, 10);
     else if (is_str(name, "sword"))
     else if (is_str(name, "sword"))
-        return sprite_generic_alloc("sword", "player", 15, 11);
+        return sprite_generic_alloc(SPRITE_ID_SWORD, "sword", "player", 15, 11);
     //
     //
     else if (is_str(name, "cyclops"))
     else if (is_str(name, "cyclops"))
-        return sprite_generic_alloc("cyclops", "enemy", 10, 11);
+        return sprite_generic_alloc(SPRITE_ID_CYCLOPS, "cyclops", "enemy", 10, 11);
     else if (is_str(name, "ghost"))
     else if (is_str(name, "ghost"))
-        return sprite_generic_alloc("ghost", "enemy", 15, 15);
+        return sprite_generic_alloc(SPRITE_ID_GHOST, "ghost", "enemy", 15, 15);
     else if (is_str(name, "ogre"))
     else if (is_str(name, "ogre"))
-        return sprite_generic_alloc("ogre", "enemy", 10, 13);
+        return sprite_generic_alloc(SPRITE_ID_OGRE, "ogre", "enemy", 10, 13);
     //
     //
     else if (is_str(name, "funny"))
     else if (is_str(name, "funny"))
-        return sprite_generic_alloc("funny", "npc", 15, 21);
+        return sprite_generic_alloc(SPRITE_ID_FUNNY, "funny", "npc", 15, 21);
 
 
     // If no match is found
     // If no match is found
     FURI_LOG_E("Game", "Sprite not found: %s", name);
     FURI_LOG_E("Game", "Sprite not found: %s", name);

+ 17 - 5
game/player.h

@@ -8,10 +8,22 @@
 #define MAX_LEVELS 5
 #define MAX_LEVELS 5
 #define MAX_NPCS 1
 #define MAX_NPCS 1
 
 
+typedef enum
+{
+    SPRITE_ID_AXE,
+    SPRITE_ID_BOW,
+    SPRITE_ID_NAKED,
+    SPRITE_ID_SWORD,
+    SPRITE_ID_CYCLOPS,
+    SPRITE_ID_GHOST,
+    SPRITE_ID_OGRE,
+    SPRITE_ID_FUNNY
+} SpriteID;
+
 // EntityContext definition
 // EntityContext definition
 typedef struct
 typedef struct
 {
 {
-    char id[32];                // Unique ID for the entity type
+    SpriteID id;                // Unique ID for the entity type
     uint8_t index;              // Index for the specific entity instance
     uint8_t index;              // Index for the specific entity instance
     Vector size;                // Size of the entity
     Vector size;                // Size of the entity
     Sprite *sprite_right;       // Entity sprite when looking right
     Sprite *sprite_right;       // Entity sprite when looking right
@@ -111,13 +123,13 @@ typedef struct
 
 
 typedef struct
 typedef struct
 {
 {
-    char id[16];
-    char left_file_name[64];
-    char right_file_name[64];
+    SpriteID id;
+    char left_file_name[33];
+    char right_file_name[33];
     uint8_t width;
     uint8_t width;
     uint8_t height;
     uint8_t height;
 } SpriteContext;
 } SpriteContext;
 
 
 extern const EntityDescription player_desc;
 extern const EntityDescription player_desc;
 void player_spawn(Level *level, GameManager *manager);
 void player_spawn(Level *level, GameManager *manager);
-SpriteContext *get_sprite_context(const char *name);
+SpriteContext *sprite_context_get(const char *name);

+ 166 - 71
game/world.c

@@ -1,91 +1,186 @@
 #include <game/world.h>
 #include <game/world.h>
 #include <game/storage.h>
 #include <game/storage.h>
 #include <flip_storage/storage.h>
 #include <flip_storage/storage.h>
+#include "game/icon.h"
 
 
-bool draw_json_world_furi(GameManager *manager, Level *level, const FuriString *json_data)
+static IconSpec world_get_icon_spec(const char *name)
+{
+    if (is_str(name, "house"))
+        return (IconSpec){.id = ICON_ID_HOUSE, .icon = &I_icon_house_48x32px, .size = (Vector){48, 32}};
+    else if (is_str(name, "man"))
+        return (IconSpec){.id = ICON_ID_MAN, .icon = &I_icon_man_7x16, .size = (Vector){7, 16}};
+    else if (is_str(name, "plant"))
+        return (IconSpec){.id = ICON_ID_PLANT, .icon = &I_icon_plant_16x16, .size = (Vector){16, 16}};
+    else if (is_str(name, "tree"))
+        return (IconSpec){.id = ICON_ID_TREE, .icon = &I_icon_tree_16x16, .size = (Vector){16, 16}};
+    else if (is_str(name, "woman"))
+        return (IconSpec){.id = ICON_ID_WOMAN, .icon = &I_icon_woman_9x16, .size = (Vector){9, 16}};
+    else if (is_str(name, "fence"))
+        return (IconSpec){.id = ICON_ID_FENCE, .icon = &I_icon_fence_16x8px, .size = (Vector){16, 8}};
+    else if (is_str(name, "fence_end"))
+        return (IconSpec){.id = ICON_ID_FENCE_END, .icon = &I_icon_fence_end_16x8px, .size = (Vector){16, 8}};
+    else if (is_str(name, "fence_vertical_end"))
+        return (IconSpec){.id = ICON_ID_FENCE_VERTICAL_END, .icon = &I_icon_fence_vertical_end_6x8px, .size = (Vector){6, 8}};
+    else if (is_str(name, "fence_vertical_start"))
+        return (IconSpec){.id = ICON_ID_FENCE_VERTICAL_START, .icon = &I_icon_fence_vertical_start_6x15px, .size = (Vector){6, 15}};
+    else if (is_str(name, "flower"))
+        return (IconSpec){.id = ICON_ID_FLOWER, .icon = &I_icon_flower_16x16, .size = (Vector){16, 16}};
+    else if (is_str(name, "lake_bottom"))
+        return (IconSpec){.id = ICON_ID_LAKE_BOTTOM, .icon = &I_icon_lake_bottom_31x12px, .size = (Vector){31, 12}};
+    else if (is_str(name, "lake_bottom_left"))
+        return (IconSpec){.id = ICON_ID_LAKE_BOTTOM_LEFT, .icon = &I_icon_lake_bottom_left_24x22px, .size = (Vector){24, 22}};
+    else if (is_str(name, "lake_bottom_right"))
+        return (IconSpec){.id = ICON_ID_LAKE_BOTTOM_RIGHT, .icon = &I_icon_lake_bottom_right_24x22px, .size = (Vector){24, 22}};
+    else if (is_str(name, "lake_left"))
+        return (IconSpec){.id = ICON_ID_LAKE_LEFT, .icon = &I_icon_lake_left_11x31px, .size = (Vector){11, 31}};
+    else if (is_str(name, "lake_right"))
+        return (IconSpec){.id = ICON_ID_LAKE_RIGHT, .icon = &I_icon_lake_right_11x31, .size = (Vector){11, 31}};
+    else if (is_str(name, "lake_top"))
+        return (IconSpec){.id = ICON_ID_LAKE_TOP, .icon = &I_icon_lake_top_31x12px, .size = (Vector){31, 12}};
+    else if (is_str(name, "lake_top_left"))
+        return (IconSpec){.id = ICON_ID_LAKE_TOP_LEFT, .icon = &I_icon_lake_top_left_24x22px, .size = (Vector){24, 22}};
+    else if (is_str(name, "lake_top_right"))
+        return (IconSpec){.id = ICON_ID_LAKE_TOP_RIGHT, .icon = &I_icon_lake_top_right_24x22px, .size = (Vector){24, 22}};
+    else if (is_str(name, "rock_large"))
+        return (IconSpec){.id = ICON_ID_ROCK_LARGE, .icon = &I_icon_rock_large_18x19px, .size = (Vector){18, 19}};
+    else if (is_str(name, "rock_medium"))
+        return (IconSpec){.id = ICON_ID_ROCK_MEDIUM, .icon = &I_icon_rock_medium_16x14px, .size = (Vector){16, 14}};
+    else if (is_str(name, "rock_small"))
+        return (IconSpec){.id = ICON_ID_ROCK_SMALL, .icon = &I_icon_rock_small_10x8px, .size = (Vector){10, 8}};
+
+    return (IconSpec){.id = -1, .icon = NULL, .size = (Vector){0, 0}};
+}
+
+bool world_json_draw(GameManager *manager, Level *level, const FuriString *json_data)
 {
 {
     if (!json_data)
     if (!json_data)
     {
     {
         FURI_LOG_E("Game", "JSON data is NULL");
         FURI_LOG_E("Game", "JSON data is NULL");
         return false;
         return false;
     }
     }
-    int levels_added = 0;
-    FURI_LOG_I("Game", "Looping through world data");
+
+    // Pass 1: Count the total number of icons.
+    int total_icons = 0;
     for (int i = 0; i < MAX_WORLD_OBJECTS; i++)
     for (int i = 0; i < MAX_WORLD_OBJECTS; i++)
     {
     {
-        FURI_LOG_I("Game", "Looping through world data: %d", i);
         FuriString *data = get_json_array_value_furi("json_data", i, json_data);
         FuriString *data = get_json_array_value_furi("json_data", i, json_data);
         if (!data)
         if (!data)
-        {
             break;
             break;
-        }
-
-        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 *amount = get_json_value_furi("amount", data);
-        FuriString *horizontal = get_json_value_furi("horizontal", data);
-
-        if (!icon || !x || !y || !amount || !horizontal)
+        if (amount)
         {
         {
-            FURI_LOG_E("Game", "Failed Data: %s", furi_string_get_cstr(data));
-
-            if (data)
-                furi_string_free(data);
-            if (icon)
-                furi_string_free(icon);
-            if (x)
-                furi_string_free(x);
-            if (y)
-                furi_string_free(y);
-            if (amount)
-                furi_string_free(amount);
-            if (horizontal)
-                furi_string_free(horizontal);
-
-            level_clear(level);
-            return false;
+            int count = atoi(furi_string_get_cstr(amount));
+            if (count < 1)
+                count = 1;
+            total_icons += count;
+            furi_string_free(amount);
         }
         }
+        furi_string_free(data);
+    }
+    FURI_LOG_I("Game", "Total icons to spawn: %d", total_icons);
+
+    // Allocate the icon group context (local instance)
+    IconGroupContext igctx;
+    igctx.count = total_icons;
+    igctx.icons = malloc(total_icons * sizeof(IconSpec));
+    if (!igctx.icons)
+    {
+        FURI_LOG_E("Game", "Failed to allocate icon group array for %d icons", total_icons);
+        return false;
+    }
+    GameContext *game_context = game_manager_game_context_get(manager);
+    game_context->icon_count = total_icons;
 
 
-        int count = atoi(furi_string_get_cstr(amount));
-        if (count < 2)
+    // Pass 2: Parse the JSON to fill the icon specs.
+    int spec_index = 0;
+    for (int i = 0; i < MAX_WORLD_OBJECTS; i++)
+    {
+        FuriString *data = get_json_array_value_furi("json_data", i, json_data);
+        if (!data)
+            break;
+
+        FuriString *icon_str = get_json_value_furi("icon", data);
+        FuriString *x_str = get_json_value_furi("x", data);
+        FuriString *y_str = get_json_value_furi("y", data);
+        FuriString *amount_str = get_json_value_furi("amount", data);
+        FuriString *horizontal_str = get_json_value_furi("horizontal", data);
+
+        if (!icon_str || !x_str || !y_str || !amount_str || !horizontal_str)
         {
         {
-            // Just one icon
-            spawn_icon(
-                manager,
-                level,
-                furi_string_get_cstr(icon),
-                atoi(furi_string_get_cstr(x)),
-                atoi(furi_string_get_cstr(y)));
+            FURI_LOG_E("Game", "Incomplete icon data: %s", furi_string_get_cstr(data));
+            if (icon_str)
+                furi_string_free(icon_str);
+            if (x_str)
+                furi_string_free(x_str);
+            if (y_str)
+                furi_string_free(y_str);
+            if (amount_str)
+                furi_string_free(amount_str);
+            if (horizontal_str)
+                furi_string_free(horizontal_str);
+            furi_string_free(data);
+            continue;
         }
         }
-        else
+
+        int count = atoi(furi_string_get_cstr(amount_str));
+        if (count < 1)
+            count = 1;
+        float base_x = atof_furi(x_str);
+        float base_y = atof_furi(y_str);
+        bool is_horizontal = (furi_string_cmp(horizontal_str, "true") == 0);
+        int spacing = 17;
+
+        for (int j = 0; j < count; j++)
         {
         {
-            bool is_horizontal = (furi_string_cmp(horizontal, "true") == 0);
-            spawn_icon_line(
-                manager,
-                level,
-                furi_string_get_cstr(icon),
-                atoi(furi_string_get_cstr(x)),
-                atoi(furi_string_get_cstr(y)),
-                count,
-                is_horizontal,
-                17 // set as 17 for now
-            );
+            IconSpec spec = world_get_icon_spec(furi_string_get_cstr(icon_str));
+            if (!spec.icon)
+            {
+                FURI_LOG_E("Game", "Icon name not recognized: %s", furi_string_get_cstr(icon_str));
+                continue;
+            }
+            if (is_horizontal)
+            {
+                spec.pos.x = base_x + (j * spacing);
+                spec.pos.y = base_y;
+            }
+            else
+            {
+                spec.pos.x = base_x;
+                spec.pos.y = base_y + (j * spacing);
+            }
+            igctx.icons[spec_index++] = spec;
         }
         }
 
 
+        furi_string_free(icon_str);
+        furi_string_free(x_str);
+        furi_string_free(y_str);
+        furi_string_free(amount_str);
+        furi_string_free(horizontal_str);
         furi_string_free(data);
         furi_string_free(data);
-        furi_string_free(icon);
-        furi_string_free(x);
-        furi_string_free(y);
-        furi_string_free(amount);
-        furi_string_free(horizontal);
-        levels_added++;
     }
     }
+
+    // Spawn one icon group entity.
+    Entity *groupEntity = level_add_entity(level, &icon_desc);
+    IconGroupContext *entityContext = (IconGroupContext *)entity_context_get(groupEntity);
+    if (entityContext)
+    {
+        memcpy(entityContext, &igctx, sizeof(IconGroupContext));
+    }
+    else
+    {
+        FURI_LOG_E("Game", "Failed to get entity context for icon group");
+        free(igctx.icons);
+        return false;
+    }
+
+    // Set the global pointer so that player collision logic can use it.
+    g_current_icon_group = entityContext;
+
     FURI_LOG_I("Game", "Finished loading world data");
     FURI_LOG_I("Game", "Finished loading world data");
-    return levels_added > 0;
+    return true;
 }
 }
 
 
-static void draw_town_world(Level *level, GameManager *manager, void *context)
+static void world_draw_town(Level *level, GameManager *manager, void *context)
 {
 {
     UNUSED(context);
     UNUSED(context);
     if (!manager || !level)
     if (!manager || !level)
@@ -102,7 +197,7 @@ static void draw_town_world(Level *level, GameManager *manager, void *context)
         FURI_LOG_E("Game", "Failed to separate world data");
         FURI_LOG_E("Game", "Failed to separate world data");
     }
     }
     furi_string_free(json_data_str);
     furi_string_free(json_data_str);
-    set_world(level, manager, "shadow_woods_v5");
+    level_set_world(level, manager, "shadow_woods_v5");
     game_context->icon_offset = 0;
     game_context->icon_offset = 0;
     if (!game_context->imu_present)
     if (!game_context->imu_present)
     {
     {
@@ -111,20 +206,20 @@ static void draw_town_world(Level *level, GameManager *manager, void *context)
     player_spawn(level, manager);
     player_spawn(level, manager);
 }
 }
 
 
-static const LevelBehaviour _training_world = {
+static const LevelBehaviour _world_training = {
     .alloc = NULL,
     .alloc = NULL,
     .free = NULL,
     .free = NULL,
-    .start = draw_town_world,
+    .start = world_draw_town,
     .stop = NULL,
     .stop = NULL,
     .context_size = 0,
     .context_size = 0,
 };
 };
 
 
-const LevelBehaviour *training_world()
+const LevelBehaviour *world_training()
 {
 {
-    return &_training_world;
+    return &_world_training;
 }
 }
 
 
-static void draw_pvp_world(Level *level, GameManager *manager, void *context)
+static void world_draw_pvp(Level *level, GameManager *manager, void *context)
 {
 {
     UNUSED(context);
     UNUSED(context);
     if (!manager || !level)
     if (!manager || !level)
@@ -141,7 +236,7 @@ static void draw_pvp_world(Level *level, GameManager *manager, void *context)
         FURI_LOG_E("Game", "Failed to separate world data");
         FURI_LOG_E("Game", "Failed to separate world data");
     }
     }
     furi_string_free(json_data_str);
     furi_string_free(json_data_str);
-    set_world(level, manager, "pvp_world");
+    level_set_world(level, manager, "pvp_world");
     game_context->is_switching_level = false;
     game_context->is_switching_level = false;
     game_context->icon_offset = 0;
     game_context->icon_offset = 0;
     if (!game_context->imu_present)
     if (!game_context->imu_present)
@@ -151,20 +246,20 @@ static void draw_pvp_world(Level *level, GameManager *manager, void *context)
     player_spawn(level, manager);
     player_spawn(level, manager);
 }
 }
 
 
-static const LevelBehaviour _pvp_world = {
+static const LevelBehaviour _world_pvp = {
     .alloc = NULL,
     .alloc = NULL,
     .free = NULL,
     .free = NULL,
-    .start = draw_pvp_world,
+    .start = world_draw_pvp,
     .stop = NULL,
     .stop = NULL,
     .context_size = 0,
     .context_size = 0,
 };
 };
 
 
-const LevelBehaviour *pvp_world()
+const LevelBehaviour *world_pvp()
 {
 {
-    return &_pvp_world;
+    return &_world_pvp;
 }
 }
 
 
-FuriString *fetch_world(const char *name)
+FuriString *world_fetch(const char *name)
 {
 {
     if (!name)
     if (!name)
     {
     {

+ 6 - 5
game/world.h

@@ -9,8 +9,9 @@
 #define WORLD_HEIGHT 384
 #define WORLD_HEIGHT 384
 
 
 // Maximum number of world objects
 // Maximum number of world objects
-#define MAX_WORLD_OBJECTS 25 // any more than that and we may run out of heap when switching worlds
-const LevelBehaviour *training_world();
-const LevelBehaviour *pvp_world();
-bool draw_json_world_furi(GameManager *manager, Level *level, const FuriString *json_data);
-FuriString *fetch_world(const char *name);
+#define MAX_WORLD_OBJECTS 25
+
+const LevelBehaviour *world_training();
+const LevelBehaviour *world_pvp();
+bool world_json_draw(GameManager *manager, Level *level, const FuriString *json_data);
+FuriString *world_fetch(const char *name);