Ver código fonte

Merge pull request #17 from jblanked/dev_0.5

FlipWorld - v0.5
JBlanked 11 meses atrás
pai
commit
49a7cd2220

+ 10 - 8
README.md

@@ -1,10 +1,8 @@
 # FlipWorld
-
 The first open-world multiplayer game for the Flipper Zero, best played with the VGM. Here's a video tutorial: https://www.youtube.com/watch?v=Qp7qmYMfdUA
 
 ## Requirements
-
-- WiFi Developer Board, Raspberry Pi, or ESP32 device with the FlipperHTTP flash: [FlipperHTTP GitHub](https://github.com/jblanked/FlipperHTTP)
+- WiFi Developer Board, Raspberry Pi, or ESP32 device with the [FlipperHTTP flash](https://github.com/jblanked/FlipperHTTP).
 - 2.4 GHz WiFi access point
 
 ## How It Works
@@ -23,7 +21,7 @@ FlipWorld and FlipSocial are connected. Your login information is the same in bo
 - **Press/Hold RIGHT**: Turn right if not already facing right, then walk right if the button is still pressed.
 - **Press/Hold UP**: Walk up.
 - **Press/Hold DOWN**: Walk down.
-- **Press OK**: Attack/Teleport (set to attack until all enemies are defeated).
+- **Press OK**: Interact/Attack/Teleport (set to attack until all enemies are defeated and interact when colliding with NPCs)
 - **HOLD OK**: In-Game Menu.
 - **Press BACK**: Leave the menu.
 - **HOLD BACK**: Exit the game.
@@ -49,6 +47,9 @@ If an enemy attacks you, your health decreases by the enemy's strength (attack p
 
 An enemy attack registers if the enemy is facing you and collides with you. However, to attack an enemy successfully, the enemy must be facing away from you, and you must collide with them while pressing `OK`.
 
+**NPCs**
+
+NPCs are friendly characters that players can interact with. Currently, you can interact with them by clicking `OK` while colliding with them.
 
 ## Short Tutorial
 
@@ -70,15 +71,16 @@ An enemy attack registers if the enemy is facing you and collides with you. Howe
 
 **v0.4**
 - New game features
-- World expansion
 - Stability patch
+- World expansion
 
 **v0.5**
-- New game features
-- Custom Controller Support
+- Stability patch
+- NPCs
 
 **v0.6**
-- ???
+- New game features
+- Custom Controller Support
 
 **v0.7**
 - ???

+ 12 - 3
alloc/alloc.c

@@ -12,10 +12,20 @@ static uint32_t callback_exit_app(void *context)
     return VIEW_NONE; // Return VIEW_NONE to exit the app
 }
 
+void *global_app;
+void flip_world_show_submenu()
+{
+    FlipWorldApp *app = (FlipWorldApp *)global_app;
+    if (app->submenu) {
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu);
+    }
+}
+
 // Function to allocate resources for the FlipWorldApp
 FlipWorldApp *flip_world_app_alloc()
 {
     FlipWorldApp *app = (FlipWorldApp *)malloc(sizeof(FlipWorldApp));
+    global_app = app;
 
     Gui *gui = furi_record_open(RECORD_GUI);
 
@@ -42,7 +52,7 @@ FlipWorldApp *flip_world_app_alloc()
         return NULL;
     }
     submenu_add_item(app->submenu, "Play", FlipWorldSubmenuIndexRun, callback_submenu_choices, app);
-    submenu_add_item(app->submenu, "About", FlipWorldSubmenuIndexAbout, callback_submenu_choices, app);
+    submenu_add_item(app->submenu, "About", FlipWorldSubmenuIndexMessage, callback_submenu_choices, app);
     submenu_add_item(app->submenu, "Settings", FlipWorldSubmenuIndexSettings, callback_submenu_choices, app);
     //
 
@@ -91,6 +101,5 @@ void flip_world_app_free(FlipWorldApp *app)
     furi_record_close(RECORD_GUI);
 
     // free the app
-    if (app)
-        free(app);
+    if (app) free(app);
 }

+ 2 - 1
alloc/alloc.h

@@ -2,4 +2,5 @@
 #include <flip_world.h>
 
 FlipWorldApp *flip_world_app_alloc();
-void flip_world_app_free(FlipWorldApp *app);
+void flip_world_app_free(FlipWorldApp *app);
+void flip_world_show_submenu();

+ 1 - 6
app.c

@@ -10,10 +10,7 @@ int32_t flip_world_main(void *p)
     // Initialize the FlipWorld application
     FlipWorldApp *app = flip_world_app_alloc();
     if (!app)
-    {
-        FURI_LOG_E(TAG, "Failed to allocate FlipWorldApp");
         return -1;
-    }
 
     // initialize the VGM
     furi_hal_gpio_init_simple(&gpio_ext_pc1, GpioModeOutputPushPull);
@@ -41,12 +38,10 @@ int32_t flip_world_main(void *p)
         FURI_LOG_D(TAG, "Waiting for PONG");
         furi_delay_ms(100); // this causes a BusFault
     }
-
     flipper_http_free(fhttp);
+
     if (counter == 0)
-    {
         easy_flipper_dialog("FlipperHTTP Error", "Ensure your WiFi Developer\nBoard or Pico W is connected\nand the latest FlipperHTTP\nfirmware is installed.");
-    }
 
     // save app version
     char app_version[16];

+ 1 - 1
application.fam

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

BIN
assets/01-home.png


+ 5 - 0
assets/CHANGELOG.md

@@ -1,3 +1,8 @@
+## 0.5 (2025-01-31)
+- Fixed saving errors.
+- Improved memory allocation.
+- Added NPCs.
+
 ## 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).

+ 9 - 5
assets/README.md

@@ -1,7 +1,6 @@
 The first open-world multiplayer game for the Flipper Zero, best played with the VGM. Here's a video tutorial: https://www.youtube.com/watch?v=Qp7qmYMfdUA
 
 ## Requirements
-
 - WiFi Developer Board, Raspberry Pi, or ESP32 device with the FlipperHTTP flash: https://github.com/jblanked/FlipperHTTP
 - 2.4 GHz WiFi access point
 
@@ -21,7 +20,7 @@ FlipWorld and FlipSocial are connected. Your login information is the same in bo
 - **Press/Hold RIGHT**: Turn right if not already facing right, then walk right if the button is still pressed.
 - **Press/Hold UP**: Walk up.
 - **Press/Hold DOWN**: Walk down.
-- **Press OK**: Attack/Teleport (set to attack until all enemies are defeated).
+- **Press OK**: Interact/Attack/Teleport (set to attack until all enemies are defeated and interact when colliding with NPCs)
 - **HOLD OK**: In-Game Menu.
 - **Press BACK**: Leave the menu.
 - **HOLD BACK**: Exit the game.
@@ -47,6 +46,10 @@ If an enemy attacks you, your health decreases by the enemy's strength (attack p
 
 An enemy attack registers if the enemy is facing you and collides with you. However, to attack an enemy successfully, the enemy must be facing away from you, and you must collide with them while pressing "OK".
 
+**NPCs**
+
+NPCs are friendly characters that players can interact with. Currently, you can interact with them by clicking "OK" while colliding with them.
+
 ## Short Tutorial
 
 1. Ensure your WiFi Developer Board and Video Game Module are flashed with FlipperHTTP.
@@ -71,11 +74,12 @@ An enemy attack registers if the enemy is facing you and collides with you. Howe
 - World expansion
 
 **v0.5**
-- New game features
-- Custom Controller Support
+- Stability patch
+- NPCs
 
 **v0.6**
-- ???
+- New game features
+- Custom Controller Support
 
 **v0.7**
 - ???

BIN
assets/icon_chest_closed_16x13px.png


BIN
assets/icon_chest_open_16x16px.png


BIN
assets/icon_earth_15x16.png


BIN
assets/icon_home_15x16.png


BIN
assets/icon_house_3d_34x45px.png


BIN
assets/icon_info_15x16.png


BIN
assets/icon_plant_fern_18x16px.png


BIN
assets/icon_plant_pointy_13x16px.png


BIN
assets/icon_title_screen_128x64px.png


BIN
assets/icon_tree_29x30px.png


BIN
assets/icon_tree_48x48px.png


+ 165 - 140
callback/callback.c

@@ -5,12 +5,13 @@
 #include "engine/level_i.h"
 #include "engine/entity_i.h"
 #include "game/storage.h"
+#include "alloc/alloc.h"
 
 // Below added by Derek Jamison
 // FURI_LOG_DEV will log only during app development. Be sure that Settings/System/Log Device is "LPUART"; so we dont use serial port.
 #ifdef DEVELOPMENT
 #define FURI_LOG_DEV(tag, format, ...) furi_log_print_format(FuriLogLevelInfo, tag, format, ##__VA_ARGS__)
-#define DEV_CRASH() furi_crash()
+#define DEV_CRASH()                    furi_crash()
 #else
 #define FURI_LOG_DEV(tag, format, ...)
 #define DEV_CRASH()
@@ -144,7 +145,7 @@ static void error_draw(Canvas *canvas, DataLoaderModel *model)
     }
 }
 
-static bool alloc_about_view(void *context);
+static bool alloc_message_view(void *context, MessageState state);
 static bool alloc_text_input_view(void *context, char *title);
 static bool alloc_variable_item_list(void *context, uint32_t view_id);
 //
@@ -180,23 +181,32 @@ static uint32_t callback_to_settings(void *context)
     return FlipWorldViewSettings;
 }
 
-static void about_draw_callback(Canvas *canvas, void *model)
+static void message_draw_callback(Canvas *canvas, void *model)
 {
-    UNUSED(model);
+    MessageModel *message_model = model;
     canvas_clear(canvas);
-    // canvas_set_font_custom(canvas, FONT_SIZE_XLARGE);
-    canvas_draw_str(canvas, 0, 10, VERSION_TAG);
-    // canvas_set_font_custom(canvas, FONT_SIZE_MEDIUM);
-    canvas_set_font_custom(canvas, FONT_SIZE_SMALL);
-    canvas_draw_str(canvas, 0, 20, "Dev: JBlanked, codeallnight");
-    canvas_draw_str(canvas, 0, 30, "GFX: the1anonlypr3");
-    canvas_draw_str(canvas, 0, 40, "github.com/jblanked/FlipWorld");
-
-    canvas_draw_str_multi(canvas, 0, 55, "The first open world multiplayer\ngame on the Flipper Zero.");
+    if (message_model->message_state == MessageStateAbout)
+    {
+        canvas_draw_str(canvas, 0, 10, VERSION_TAG);
+        canvas_set_font_custom(canvas, FONT_SIZE_SMALL);
+        canvas_draw_str(canvas, 0, 20, "Dev: JBlanked, codeallnight");
+        canvas_draw_str(canvas, 0, 30, "GFX: the1anonlypr3");
+        canvas_draw_str(canvas, 0, 40, "github.com/jblanked/FlipWorld");
+
+        canvas_draw_str_multi(canvas, 0, 55, "The first open world multiplayer\ngame on the Flipper Zero.");
+    }
+    else if (message_model->message_state == MessageStateLoading)
+    {
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str_aligned(canvas, 64, 0, AlignCenter, AlignTop, "Starting FlipWorld");
+        canvas_set_font(canvas, FontSecondary);
+        canvas_draw_str(canvas, 0, 50, "Please wait while your");
+        canvas_draw_str(canvas, 0, 60, "game is started.");
+    }
 }
 
 // alloc
-static bool alloc_about_view(void *context)
+static bool alloc_message_view(void *context, MessageState state)
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
     if (!app)
@@ -204,16 +214,19 @@ static bool alloc_about_view(void *context)
         FURI_LOG_E(TAG, "FlipWorldApp is NULL");
         return false;
     }
-    if (!app->view_about)
+    if (!app->view_message)
     {
-        if (!easy_flipper_set_view(&app->view_about, FlipWorldViewAbout, about_draw_callback, NULL, callback_to_submenu, &app->view_dispatcher, app))
+        if (!easy_flipper_set_view(&app->view_message, FlipWorldViewMessage, message_draw_callback, NULL, (state == MessageStateLoading) ? NULL : callback_to_submenu, &app->view_dispatcher, app))
         {
             return false;
         }
-        if (!app->view_about)
+        if (!app->view_message)
         {
             return false;
         }
+        view_allocate_model(app->view_message, ViewModelTypeLockFree, sizeof(MessageModel));
+        MessageModel *model = view_get_model(app->view_message);
+        model->message_state = state;
     }
     return true;
 }
@@ -417,7 +430,7 @@ static bool alloc_variable_item_list(void *context, uint32_t view_id)
             char _game_fps[8];
             if (load_char("Game-FPS", _game_fps, sizeof(_game_fps)))
             {
-                int index = is_str(_game_fps, "30") ? 0 : is_str(_game_fps, "60") ? 1
+                                int index = is_str(_game_fps, "30") ? 0 : is_str(_game_fps, "60") ? 1
                                                       : is_str(_game_fps, "120")  ? 2
                                                       : is_str(_game_fps, "240")  ? 3
                                                                                   : 0;
@@ -428,19 +441,20 @@ static bool alloc_variable_item_list(void *context, uint32_t view_id)
             if (load_char("Game-VGM-X", _game_vgm_x, sizeof(_game_vgm_x)))
             {
                 int vgm_x = atoi(_game_vgm_x);
-                int index = vgm_x == -2 ? 0 : vgm_x == -1 ? 1
-                                          : vgm_x == 0    ? 2
-                                          : vgm_x == 1    ? 3
-                                          : vgm_x == 2    ? 4
-                                          : vgm_x == 3    ? 5
-                                          : vgm_x == 4    ? 6
-                                          : vgm_x == 5    ? 7
-                                          : vgm_x == 6    ? 8
-                                          : vgm_x == 7    ? 9
-                                          : vgm_x == 8    ? 10
-                                          : vgm_x == 9    ? 11
-                                          : vgm_x == 10   ? 12
-                                                          : 2;
+                int index = vgm_x == -2 ? 0 :
+                            vgm_x == -1 ? 1 :
+                            vgm_x == 0  ? 2 :
+                            vgm_x == 1  ? 3 :
+                            vgm_x == 2  ? 4 :
+                            vgm_x == 3  ? 5 :
+                            vgm_x == 4  ? 6 :
+                            vgm_x == 5  ? 7 :
+                            vgm_x == 6  ? 8 :
+                            vgm_x == 7  ? 9 :
+                            vgm_x == 8  ? 10 :
+                            vgm_x == 9  ? 11 :
+                            vgm_x == 10 ? 12 :
+                                          2;
                 variable_item_set_current_value_index(app->variable_item_game_vgm_x, index);
                 variable_item_set_current_value_text(app->variable_item_game_vgm_x, vgm_levels[index]);
             }
@@ -448,43 +462,41 @@ static bool alloc_variable_item_list(void *context, uint32_t view_id)
             if (load_char("Game-VGM-Y", _game_vgm_y, sizeof(_game_vgm_y)))
             {
                 int vgm_y = atoi(_game_vgm_y);
-                int index = vgm_y == -2 ? 0 : vgm_y == -1 ? 1
-                                          : vgm_y == 0    ? 2
-                                          : vgm_y == 1    ? 3
-                                          : vgm_y == 2    ? 4
-                                          : vgm_y == 3    ? 5
-                                          : vgm_y == 4    ? 6
-                                          : vgm_y == 5    ? 7
-                                          : vgm_y == 6    ? 8
-                                          : vgm_y == 7    ? 9
-                                          : vgm_y == 8    ? 10
-                                          : vgm_y == 9    ? 11
-                                          : vgm_y == 10   ? 12
-                                                          : 2;
+                int index = vgm_y == -2 ? 0 :
+                            vgm_y == -1 ? 1 :
+                            vgm_y == 0  ? 2 :
+                            vgm_y == 1  ? 3 :
+                            vgm_y == 2  ? 4 :
+                            vgm_y == 3  ? 5 :
+                            vgm_y == 4  ? 6 :
+                            vgm_y == 5  ? 7 :
+                            vgm_y == 6  ? 8 :
+                            vgm_y == 7  ? 9 :
+                            vgm_y == 8  ? 10 :
+                            vgm_y == 9  ? 11 :
+                            vgm_y == 10 ? 12 :
+                                          2;
                 variable_item_set_current_value_index(app->variable_item_game_vgm_y, index);
                 variable_item_set_current_value_text(app->variable_item_game_vgm_y, vgm_levels[index]);
             }
             char _game_screen_always_on[8];
             if (load_char("Game-Screen-Always-On", _game_screen_always_on, sizeof(_game_screen_always_on)))
             {
-                int index = is_str(_game_screen_always_on, "No") ? 0 : is_str(_game_screen_always_on, "Yes") ? 1
-                                                                                                             : 0;
+                int index = is_str(_game_screen_always_on, "No") ? 0 : is_str(_game_screen_always_on, "Yes") ? 1 : 0;
                 variable_item_set_current_value_text(app->variable_item_game_screen_always_on, yes_or_no_choices[index]);
                 variable_item_set_current_value_index(app->variable_item_game_screen_always_on, index);
             }
             char _game_sound_on[8];
             if (load_char("Game-Sound-On", _game_sound_on, sizeof(_game_sound_on)))
             {
-                int index = is_str(_game_sound_on, "No") ? 0 : is_str(_game_sound_on, "Yes") ? 1
-                                                                                             : 0;
+                int index = is_str(_game_sound_on, "No") ? 0 : is_str(_game_sound_on, "Yes") ? 1 : 0;
                 variable_item_set_current_value_text(app->variable_item_game_sound_on, yes_or_no_choices[index]);
                 variable_item_set_current_value_index(app->variable_item_game_sound_on, index);
             }
             char _game_vibration_on[8];
             if (load_char("Game-Vibration-On", _game_vibration_on, sizeof(_game_vibration_on)))
             {
-                int index = is_str(_game_vibration_on, "No") ? 0 : is_str(_game_vibration_on, "Yes") ? 1
-                                                                                                     : 0;
+                int index = is_str(_game_vibration_on, "No") ? 0 : is_str(_game_vibration_on, "Yes") ? 1 : 0;
                 variable_item_set_current_value_text(app->variable_item_game_vibration_on, yes_or_no_choices[index]);
                 variable_item_set_current_value_index(app->variable_item_game_vibration_on, index);
             }
@@ -564,7 +576,7 @@ static bool alloc_submenu_settings(void *context)
     return true;
 }
 // free
-static void free_about_view(void *context)
+static void free_message_view(void *context)
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
     if (!app)
@@ -572,11 +584,11 @@ static void free_about_view(void *context)
         FURI_LOG_E(TAG, "FlipWorldApp is NULL");
         return;
     }
-    if (app->view_about)
+    if (app->view_message)
     {
-        view_dispatcher_remove_view(app->view_dispatcher, FlipWorldViewAbout);
-        view_free(app->view_about);
-        app->view_about = NULL;
+        view_dispatcher_remove_view(app->view_dispatcher, FlipWorldViewMessage);
+        view_free(app->view_message);
+        app->view_message = NULL;
     }
 }
 
@@ -695,7 +707,7 @@ static void free_submenu_settings(void *context)
         app->submenu_settings = NULL;
     }
 }
-static FuriThreadId thread_id;
+static FuriThread *game_thread;
 static bool game_thread_running = false;
 void free_all_views(void *context, bool should_free_variable_item_list, bool should_free_submenu_settings)
 {
@@ -709,19 +721,23 @@ void free_all_views(void *context, bool should_free_variable_item_list, bool sho
     {
         free_variable_item_list(app);
     }
-    free_about_view(app);
+    free_message_view(app);
     free_text_input_view(app);
 
     // free game thread
     if (game_thread_running)
     {
         game_thread_running = false;
-        furi_thread_flags_set(thread_id, WorkerEvtStop);
-        furi_thread_free(thread_id);
+        if (game_thread)
+        {
+            furi_thread_flags_set(furi_thread_get_id(game_thread), WorkerEvtStop);
+            furi_thread_join(game_thread);
+            furi_thread_free(game_thread);
+            game_thread = NULL;
+        }
     }
 
-    if (should_free_submenu_settings)
-        free_submenu_settings(app);
+    if (should_free_submenu_settings) free_submenu_settings(app);
 }
 static bool fetch_world_list(FlipperHTTP *fhttp)
 {
@@ -731,24 +747,20 @@ static bool fetch_world_list(FlipperHTTP *fhttp)
         easy_flipper_dialog("Error", "fhttp is NULL. Press BACK to return.");
         return false;
     }
-    // Create the directory for saving worlds
-    char directory_path[128];
-    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds");
 
-    // Create the directory
+    // ensure flip_world directory exists
+    char directory_path[128];
+    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world");
     Storage *storage = furi_record_open(RECORD_STORAGE);
     storage_common_mkdir(storage, directory_path);
-
-    // free storage
+    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds");
+    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/worlds/world_list.json");
+    snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/world_list.json");
 
     fhttp->save_received_data = true;
-    return flipper_http_get_request_with_headers(fhttp, "https://www.flipsocial.net/api/world/v4/list/10/", "{\"Content-Type\":\"application/json\"}");
+    return flipper_http_get_request_with_headers(fhttp, "https://www.flipsocial.net/api/world/v5/list/10/", "{\"Content-Type\":\"application/json\"}");
 }
 // 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
@@ -769,11 +781,19 @@ static bool fetch_player_stats(FlipperHTTP *fhttp)
     }
     char url[128];
     snprintf(url, sizeof(url), "https://www.flipsocial.net/api/user/game-stats/%s/", username);
-    snprintf(
-        fhttp->file_path,
-        sizeof(fhttp->file_path),
-        STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data/player/player_stats.json");
 
+    // ensure the folders exist
+    char directory_path[128];
+    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world");
+    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/player");
+    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/data/player/player_stats.json");
     fhttp->save_received_data = true;
     return flipper_http_get_request_with_headers(fhttp, url, "{\"Content-Type\":\"application/json\"}");
 }
@@ -846,8 +866,12 @@ static bool start_game_thread(void *context)
     if (game_thread_running)
     {
         game_thread_running = false;
-        furi_thread_flags_set(thread_id, WorkerEvtStop);
-        furi_thread_free(thread_id);
+        if (game_thread)
+        {
+            furi_thread_flags_set(furi_thread_get_id(game_thread), WorkerEvtStop);
+            furi_thread_join(game_thread);
+            furi_thread_free(game_thread);
+        }
     }
     // start game thread
     FuriThread *thread = furi_thread_alloc_ex("game", 2048, game_app, app);
@@ -858,7 +882,7 @@ static bool start_game_thread(void *context)
         return false;
     }
     furi_thread_start(thread);
-    thread_id = furi_thread_get_id(thread);
+    game_thread = thread;
     game_thread_running = true;
     return true;
 }
@@ -880,14 +904,16 @@ static bool _fetch_game(DataLoaderModel *model)
         if (!load_char("Flip-Social-Username", username, sizeof(username)))
         {
             FURI_LOG_E(TAG, "Failed to load Flip-Social-Username");
-            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
             easy_flipper_dialog("Error", "Failed to load saved username\nGo to user settings to update.");
             return false;
         }
         if (!load_char("Flip-Social-Password", password, sizeof(password)))
         {
             FURI_LOG_E(TAG, "Failed to load Flip-Social-Password");
-            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
             easy_flipper_dialog("Error", "Failed to load saved password\nGo to settings to update.");
             return false;
         }
@@ -903,7 +929,8 @@ static bool _fetch_game(DataLoaderModel *model)
         {
             FURI_LOG_E(TAG, "Failed to load is_logged_in");
             easy_flipper_dialog("Error", "Failed to load is_logged_in\nGo to user settings to update.");
-            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 false;
         }
         if (is_str(is_logged_in, "false") && is_str(model->title, "Registering..."))
@@ -915,14 +942,16 @@ static bool _fetch_game(DataLoaderModel *model)
             {
                 FURI_LOG_E(TAG, "Failed to load Flip-Social-Username");
                 easy_flipper_dialog("Error", "Failed to load saved username. Go to settings to update.");
-                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 false;
             }
             if (!load_char("Flip-Social-Password", password, sizeof(password)))
             {
                 FURI_LOG_E(TAG, "Failed to load Flip-Social-Password");
                 easy_flipper_dialog("Error", "Failed to load saved password. Go to settings to update.");
-                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 false;
             }
             char payload[172];
@@ -943,15 +972,13 @@ static bool _fetch_game(DataLoaderModel *model)
     }
     else if (model->request_index == 3)
     {
-        snprintf(
-            model->fhttp->file_path,
-            sizeof(model->fhttp->file_path),
-            STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/world_list.json");
+        snprintf(model->fhttp->file_path, sizeof(model->fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/world_list.json");
 
         FuriString *world_list = flipper_http_load_from_file(model->fhttp->file_path);
         if (!world_list)
         {
-            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
             FURI_LOG_E(TAG, "Failed to load world list");
             easy_flipper_dialog("Error", "Failed to load world list. Go to game settings to download packs.");
             return false;
@@ -959,7 +986,8 @@ static bool _fetch_game(DataLoaderModel *model)
         FuriString *first_world = get_json_array_value_furi("worlds", 0, world_list);
         if (!first_world)
         {
-            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
             FURI_LOG_E(TAG, "Failed to get first world");
             easy_flipper_dialog("Error", "Failed to get first world. Go to game settings to download packs.");
             furi_string_free(world_list);
@@ -974,19 +1002,17 @@ static bool _fetch_game(DataLoaderModel *model)
             {
                 FURI_LOG_E(TAG, "Failed to start game thread");
                 easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return.");
-                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 "Failed to start game thread";
             }
             return true;
         }
-        snprintf(
-            model->fhttp->file_path,
-            sizeof(model->fhttp->file_path),
-            STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s.json", furi_string_get_cstr(first_world));
+        snprintf(model->fhttp->file_path, sizeof(model->fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s.json", furi_string_get_cstr(first_world));
 
         model->fhttp->save_received_data = true;
         char url[128];
-        snprintf(url, sizeof(url), "https://www.flipsocial.net/api/world/v4/get/world/%s/", furi_string_get_cstr(first_world));
+        snprintf(url, sizeof(url), "https://www.flipsocial.net/api/world/v5/get/world/%s/", furi_string_get_cstr(first_world));
         furi_string_free(world_list);
         furi_string_free(first_world);
         return flipper_http_get_request_with_headers(model->fhttp, url, "{\"Content-Type\":\"application/json\"}");
@@ -1094,19 +1120,22 @@ static char *_parse_game(DataLoaderModel *model)
             else if (strstr(model->fhttp->last_response, "Username or password not provided") != NULL)
             {
                 easy_flipper_dialog("Error", "Please enter your credentials.\nPress BACK to return.");
-                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 "Please enter your credentials.";
             }
             else if (strstr(model->fhttp->last_response, "User already exists") != NULL || strstr(model->fhttp->last_response, "Multiple users found") != NULL)
             {
                 easy_flipper_dialog("Error", "Registration failed...\nUsername already exists.\nPress BACK to return.");
-                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 "Username already exists.";
             }
             else
             {
                 easy_flipper_dialog("Error", "Registration failed...\nUpdate your credentials.\nPress BACK to return.");
-                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 "Registration failed...";
             }
         }
@@ -1116,7 +1145,8 @@ static char *_parse_game(DataLoaderModel *model)
             {
                 FURI_LOG_E(TAG, "Failed to start game thread");
                 easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return.");
-                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 "Failed to start game thread";
             }
             return "Thanks for playing FlipWorld!\n\n\n\nPress BACK to return if this\ndoesn't automatically close.";
@@ -1132,7 +1162,8 @@ static char *_parse_game(DataLoaderModel *model)
         {
             FURI_LOG_E(TAG, "Failed to start game thread");
             easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return.");
-            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 "Failed to start game thread";
         }
         return "Thanks for playing FlipWorld!\n\n\n\nPress BACK to return if this\ndoesn't automatically close.";
@@ -1158,7 +1189,7 @@ void callback_submenu_choices(void *context, uint32_t index)
     {
     case FlipWorldSubmenuIndexRun:
         free_all_views(app, true, true);
-        if (!is_enough_heap(45000)) // lowered from 60k to 45k since we saved 15k bytes
+        if (!is_enough_heap(60000))
         {
             easy_flipper_dialog("Error", "Not enough heap memory.\nPlease restart your Flipper.");
             return;
@@ -1187,62 +1218,53 @@ void callback_submenu_choices(void *context, uint32_t index)
                 return fetch_player_stats(fhttp);
             }
 
-            Loading *loading;
-            int32_t loading_view_id = 987654321; // Random ID
-
-            loading = loading_alloc();
-            if (!loading)
+            if (!alloc_message_view(app, MessageStateLoading))
             {
-                FURI_LOG_E(HTTP_TAG, "Failed to allocate loading");
-                view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu);
-                flipper_http_free(fhttp);
+                FURI_LOG_E(TAG, "Failed to allocate message view");
                 return;
             }
-
-            view_dispatcher_add_view(app->view_dispatcher, loading_view_id, loading_get_view(loading));
-
-            // Switch to the loading view
-            view_dispatcher_switch_to_view(app->view_dispatcher, loading_view_id);
+            view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewMessage);
 
             // Make the request
-            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, fetch_world_list_i, parse_world_list_i) || !flipper_http_process_response_async(fhttp, fetch_player_stats_i, set_player_context))
             {
                 FURI_LOG_E(HTTP_TAG, "Failed to make request");
-                view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu);
-                view_dispatcher_remove_view(app->view_dispatcher, loading_view_id);
-                loading_free(loading);
                 flipper_http_free(fhttp);
             }
             else
             {
-                view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu);
-                view_dispatcher_remove_view(app->view_dispatcher, loading_view_id);
-                loading_free(loading);
                 flipper_http_free(fhttp);
             }
+
+            if (!alloc_submenu_settings(app))
+            {
+                FURI_LOG_E(TAG, "Failed to allocate settings view");
+                return;
+            }
+
             if (!start_game_thread(app))
             {
                 FURI_LOG_E(TAG, "Failed to start game thread");
                 easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return.");
                 return;
             }
-
-            easy_flipper_dialog("Starting Game", "Please wait...");
+            
         }
         else
         {
             switch_to_view_get_game(app);
         }
         break;
-    case FlipWorldSubmenuIndexAbout:
+    case FlipWorldSubmenuIndexMessage:
+        // About menu.
         free_all_views(app, true, true);
-        if (!alloc_about_view(app))
+        if (!alloc_message_view(app, MessageStateAbout))
         {
-            FURI_LOG_E(TAG, "Failed to allocate about view");
+            FURI_LOG_E(TAG, "Failed to allocate message view");
             return;
         }
-        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewAbout);
+
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewMessage);
         break;
     case FlipWorldSubmenuIndexSettings:
         free_all_views(app, true, true);
@@ -1584,20 +1606,16 @@ static bool _fetch_worlds(DataLoaderModel *model)
         FURI_LOG_E(TAG, "model or fhttp is NULL");
         return false;
     }
-    // Create the directory for saving settings
-    char directory_path[256];
-    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds");
-
-    // Create the directory
+    char directory_path[128];
+    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world");
     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/worlds");
+    storage_common_mkdir(storage, directory_path);
     furi_record_close(RECORD_STORAGE);
-    snprintf(
-        model->fhttp->file_path,
-        sizeof(model->fhttp->file_path),
-        STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/world_list_full.json");
+    snprintf(model->fhttp->file_path, sizeof(model->fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/world_list_full.json");
     model->fhttp->save_received_data = true;
-    return flipper_http_get_request_with_headers(model->fhttp, "https://www.flipsocial.net/api/world/v4/get/10/", "{\"Content-Type\":\"application/json\"}");
+    return flipper_http_get_request_with_headers(model->fhttp, "https://www.flipsocial.net/api/world/v5/get/10/", "{\"Content-Type\":\"application/json\"}");
 }
 static char *_parse_worlds(DataLoaderModel *model)
 {
@@ -1849,7 +1867,14 @@ static void loader_process_callback(void *context)
 
     DataState current_data_state;
     DataLoaderModel *loader_model = NULL;
-    with_view_model(view, DataLoaderModel * model, { current_data_state = model->data_state; loader_model = model; }, false);
+    with_view_model(
+        view,
+        DataLoaderModel * model,
+        {
+            current_data_state = model->data_state;
+            loader_model = model;
+        },
+        false);
     if (!loader_model || !loader_model->fhttp)
     {
         FURI_LOG_E(TAG, "Model or fhttp is NULL");

+ 13 - 1
callback/callback.h

@@ -18,6 +18,18 @@ enum DataState
     DataStateError,
 };
 
+typedef enum MessageState MessageState;
+enum MessageState
+{
+    MessageStateAbout,
+    MessageStateLoading,
+};
+typedef struct MessageModel MessageModel;
+struct MessageModel
+{
+    MessageState message_state;
+};
+
 typedef struct DataLoaderModel DataLoaderModel;
 typedef bool (*DataLoaderFetch)(DataLoaderModel *model);
 typedef char *(*DataLoaderParser)(DataLoaderModel *model);
@@ -42,4 +54,4 @@ void loader_draw_callback(Canvas *canvas, void *model);
 void loader_init(View *view);
 
 void loader_free_model(View *view);
-bool custom_event_callback(void *context, uint32_t index);
+bool custom_event_callback(void *context, uint32_t index);

BIN
file_assets/sprites/npc_left_funny_15x21px.fxbm


BIN
file_assets/sprites/npc_right_funny_15x21px.fxbm


+ 1 - 1
flip_storage/storage.c

@@ -230,7 +230,7 @@ bool load_char(
     {
         storage_file_free(file);
         furi_record_close(RECORD_STORAGE);
-        return NULL; // Return false if the file does not exist
+        return false; // Return false if the file does not exist
     }
 
     // Read data into the buffer

+ 10 - 7
flip_world.h

@@ -1,7 +1,7 @@
 #pragma once
-#include <font/font.h>
-#include <flipper_http/flipper_http.h>
 #include <easy_flipper/easy_flipper.h>
+#include <flipper_http/flipper_http.h>
+#include <font/font.h>
 
 // added by Derek Jamison to lower memory usage
 #undef FURI_LOG_E
@@ -9,17 +9,20 @@
 
 #undef FURI_LOG_I
 #define FURI_LOG_I(tag, msg, ...)
+
+#undef FURI_LOG_D
+#define FURI_LOG_D(tag, msg, ...)
 //
 
 #define TAG "FlipWorld"
-#define VERSION 0.4
+#define VERSION 0.5
 #define VERSION_TAG TAG " " FAP_VERSION
 
 // Define the submenu items for our FlipWorld application
 typedef enum
 {
     FlipWorldSubmenuIndexRun, // Click to run the FlipWorld application
-    FlipWorldSubmenuIndexAbout,
+    FlipWorldSubmenuIndexMessage,
     FlipWorldSubmenuIndexSettings,
     FlipWorldSubmenuIndexWiFiSettings,
     FlipWorldSubmenuIndexGameSettings,
@@ -30,7 +33,7 @@ typedef enum
 typedef enum
 {
     FlipWorldViewSubmenu,          // The submenu
-    FlipWorldViewAbout,            // The about screen
+    FlipWorldViewMessage,          // The about, loading screen
     FlipWorldViewSettings,         // The settings screen
     FlipWorldViewVariableItemList, // The variable item list screen
     FlipWorldViewTextInput,        // The text input screen
@@ -53,7 +56,7 @@ typedef struct
     Widget *widget_result;
     //
     ViewDispatcher *view_dispatcher;       // Switches between our views
-    View *view_about;                      // The about screen
+    View *view_message;                    // The about, loading screen
     Submenu *submenu;                      // The submenu
     Submenu *submenu_settings;             // The settings submenu
     VariableItemList *variable_item_list;  // The variable item list (settngs)
@@ -92,4 +95,4 @@ extern int vgm_y_index;
 float atof_(const char *nptr);
 float atof_furi(const FuriString *nptr);
 bool is_str(const char *src, const char *dst);
-bool is_enough_heap(size_t heap_size);
+bool is_enough_heap(size_t heap_size);

+ 30 - 47
game/draw.c

@@ -46,33 +46,6 @@ void draw_username(Canvas *canvas, Vector pos, char *username)
     canvas_draw_str(canvas, pos.x - camera_x - (strlen(username) * 2), pos.y - camera_y - 7, username);
 }
 
-// Draw a line of icons (16 width)
-void draw_icon_line(Canvas *canvas, Vector pos, int amount, bool horizontal, const Icon *icon)
-{
-    for (int i = 0; i < amount; i++)
-    {
-        if (horizontal)
-        {
-            // check if element is outside the world
-            if (pos.x + (i * 17) > WORLD_WIDTH)
-            {
-                break;
-            }
-
-            canvas_draw_icon(canvas, pos.x + (i * 17) - camera_x, pos.y - camera_y, icon);
-        }
-        else
-        {
-            // check if element is outside the world
-            if (pos.y + (i * 17) > WORLD_HEIGHT)
-            {
-                break;
-            }
-
-            canvas_draw_icon(canvas, pos.x - camera_x, pos.y + (i * 17) - camera_y, icon);
-        }
-    }
-}
 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)
@@ -84,29 +57,29 @@ void spawn_icon(GameManager *manager, Level *level, const char *icon_id, float x
     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)
+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 * 17) > WORLD_WIDTH)
+            if (x + (i * spacing) > WORLD_WIDTH)
             {
                 break;
             }
 
-            spawn_icon(manager, level, icon_id, x + (i * 17), y);
+            spawn_icon(manager, level, icon_id, x + (i * spacing), y);
         }
         else
         {
             // check if element is outside the world
-            if (y + (i * 17) > WORLD_HEIGHT)
+            if (y + (i * spacing) > WORLD_HEIGHT)
             {
                 break;
             }
 
-            spawn_icon(manager, level, icon_id, x, y + (i * 17));
+            spawn_icon(manager, level, icon_id, x, y + (i * spacing));
         }
     }
 }
@@ -178,6 +151,11 @@ static void draw_menu(GameManager *manager, Canvas *canvas)
         canvas_set_font(canvas, FontPrimary);
         canvas_draw_str(canvas, 86, 42, "More");
         break;
+    case GAME_MENU_NPC:
+        // draw NPC dialog
+        canvas_set_font_custom(canvas, FONT_SIZE_SMALL);
+        canvas_draw_str(canvas, 7, 16, game_context->message);
+        break;
     default:
         break;
     }
@@ -189,26 +167,31 @@ void background_render(Canvas *canvas, GameManager *manager)
         return;
 
     GameContext *game_context = game_manager_game_context_get(manager);
+    if (!game_context->is_menu_open)
+    {
 
-    // get player position
-    Vector posi = entity_pos_get(game_context->player);
+        // get player position
+        Vector posi = entity_pos_get(game_context->player);
 
-    // draw username over player's head
-    draw_username(canvas, posi, game_context->player_context->username);
+        // draw username over player's head
+        draw_username(canvas, posi, game_context->player_context->username);
 
-    // draw switch world icon
-    if (game_context->is_switching_level)
-    {
-        canvas_draw_icon(
-            canvas,
-            0,
-            0,
-            &I_icon_world_change_128x64px);
-    }
+        // draw switch world icon
+        if (game_context->is_switching_level)
+        {
+            canvas_draw_icon(
+                canvas,
+                0,
+                0,
+                &I_icon_world_change_128x64px);
+        }
 
-    // draw menu
-    if (game_context->is_menu_open)
+        // Draw user stats
+        draw_user_stats(canvas, (Vector){0, 50}, manager);
+    }
+    else
     {
+        // draw menu
         draw_menu(manager, canvas);
     }
 };

+ 1 - 2
game/draw.h

@@ -8,8 +8,7 @@ extern int camera_x;
 extern int camera_y;
 void draw_user_stats(Canvas *canvas, Vector pos, GameManager *manager);
 void draw_username(Canvas *canvas, Vector pos, char *username);
-void draw_icon_line(Canvas *canvas, Vector pos, int amount, bool horizontal, const Icon *icon);
 void 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);
+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);

+ 93 - 85
game/enemy.c

@@ -2,10 +2,10 @@
 #include <game/enemy.h>
 #include <notification/notification_messages.h>
 
-static EnemyContext *enemy_context_generic;
+static EntityContext *enemy_context_generic;
 
 // Allocation function
-static EnemyContext *enemy_generic_alloc(
+static EntityContext *enemy_generic_alloc(
     const char *id,
     int index,
     Vector size,
@@ -19,11 +19,11 @@ static EnemyContext *enemy_generic_alloc(
 {
     if (!enemy_context_generic)
     {
-        enemy_context_generic = malloc(sizeof(EnemyContext));
+        enemy_context_generic = malloc(sizeof(EntityContext));
     }
     if (!enemy_context_generic)
     {
-        FURI_LOG_E("Game", "Failed to allocate EnemyContext");
+        FURI_LOG_E("Game", "Failed to allocate EntityContext");
         return NULL;
     }
     snprintf(enemy_context_generic->id, sizeof(enemy_context_generic->id), "%s", id);
@@ -38,30 +38,15 @@ static EnemyContext *enemy_generic_alloc(
     enemy_context_generic->strength = strength;
     enemy_context_generic->health = health;
     // Initialize other fields as needed
-    enemy_context_generic->sprite_right = NULL;         // Assign appropriate sprite
-    enemy_context_generic->sprite_left = NULL;          // Assign appropriate sprite
-    enemy_context_generic->direction = ENEMY_RIGHT;     // Default direction
-    enemy_context_generic->state = ENEMY_MOVING_TO_END; // Start in IDLE state
+    enemy_context_generic->sprite_right = NULL;          // sprite is assigned later
+    enemy_context_generic->sprite_left = NULL;           // sprite is assigned later
+    enemy_context_generic->direction = ENTITY_RIGHT;     // Default direction
+    enemy_context_generic->state = ENTITY_MOVING_TO_END; // Start in IDLE state
     // Set radius based on size, for example, average of size.x and size.y divided by 2
     enemy_context_generic->radius = (size.x + size.y) / 4.0f;
     return enemy_context_generic;
 }
 
-// Free function
-static void enemy_generic_free(void *context)
-{
-    if (context)
-    {
-        free(context);
-        context = NULL;
-    }
-    if (enemy_context_generic)
-    {
-        free(enemy_context_generic);
-        enemy_context_generic = NULL;
-    }
-}
-
 // Enemy start function
 static void enemy_start(Entity *self, GameManager *manager, void *context)
 {
@@ -77,7 +62,7 @@ static void enemy_start(Entity *self, GameManager *manager, void *context)
         return;
     }
 
-    EnemyContext *enemy_context = (EnemyContext *)context;
+    EntityContext *enemy_context = (EntityContext *)context;
     // Copy fields from generic context
     snprintf(enemy_context->id, sizeof(enemy_context->id), "%s", enemy_context_generic->id);
     enemy_context->index = enemy_context_generic->index;
@@ -109,14 +94,22 @@ static void enemy_render(Entity *self, GameManager *manager, Canvas *canvas, voi
     if (!self || !context || !canvas || !manager)
         return;
 
-    EnemyContext *enemy_context = (EnemyContext *)context;
+    EntityContext *enemy_context = (EntityContext *)context;
 
     // Get the position of the enemy
     Vector pos = entity_pos_get(self);
 
+    // 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;
+
+    // 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)
+        return;
+
     // Choose sprite based on direction
     Sprite *current_sprite = NULL;
-    if (enemy_context->direction == ENEMY_LEFT)
+    if (enemy_context->direction == ENTITY_LEFT)
     {
         current_sprite = enemy_context->sprite_left;
     }
@@ -136,12 +129,9 @@ static void enemy_render(Entity *self, GameManager *manager, Canvas *canvas, voi
     char health_str[32];
     snprintf(health_str, sizeof(health_str), "%.0f", (double)enemy_context->health);
     draw_username(canvas, pos, health_str);
-
-    // Draw user stats (this has to be done for all enemies)
-    draw_user_stats(canvas, (Vector){0, 50}, manager);
 }
 
-static void atk_notify(GameContext *game_context, EnemyContext *enemy_context, bool player_attacked)
+static void atk_notify(GameContext *game_context, EntityContext *enemy_context, bool player_attacked)
 {
     if (!game_context || !enemy_context)
     {
@@ -210,24 +200,13 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
         FURI_LOG_E("Game", "Enemy collision: Invalid parameters");
         return;
     }
-
+    EntityContext *enemy_context = (EntityContext *)context;
+    furi_check(enemy_context, "Enemy collision: EntityContext is NULL");
+    GameContext *game_context = game_manager_game_context_get(manager);
+    furi_check(game_context, "Enemy collision: GameContext is NULL");
     // Check if the enemy collided with the player
     if (entity_description_get(other) == &player_desc)
     {
-        // Retrieve enemy context
-        EnemyContext *enemy_context = (EnemyContext *)context;
-        GameContext *game_context = game_manager_game_context_get(manager);
-        // InputState input = game_manager_input_get(manager);
-        if (!enemy_context)
-        {
-            FURI_LOG_E("Game", "Enemy collision: EnemyContext is NULL");
-            return;
-        }
-        if (!game_context)
-        {
-            FURI_LOG_E("Game", "Enemy collision: GameContext is NULL");
-            return;
-        }
 
         // Get positions of the enemy and the player
         Vector enemy_pos = entity_pos_get(self);
@@ -238,19 +217,19 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
         bool player_is_facing_enemy = false;
 
         // Determine if the enemy is facing the player
-        if ((enemy_context->direction == ENEMY_LEFT && player_pos.x < enemy_pos.x) ||
-            (enemy_context->direction == ENEMY_RIGHT && player_pos.x > enemy_pos.x) ||
-            (enemy_context->direction == ENEMY_UP && player_pos.y < enemy_pos.y) ||
-            (enemy_context->direction == ENEMY_DOWN && player_pos.y > enemy_pos.y))
+        if ((enemy_context->direction == ENTITY_LEFT && player_pos.x < enemy_pos.x) ||
+            (enemy_context->direction == ENTITY_RIGHT && player_pos.x > enemy_pos.x) ||
+            (enemy_context->direction == ENTITY_UP && player_pos.y < enemy_pos.y) ||
+            (enemy_context->direction == ENTITY_DOWN && player_pos.y > enemy_pos.y))
         {
             enemy_is_facing_player = true;
         }
 
         // Determine if the player is facing the enemy
-        if ((game_context->player_context->direction == PLAYER_LEFT && enemy_pos.x < player_pos.x) ||
-            (game_context->player_context->direction == PLAYER_RIGHT && enemy_pos.x > player_pos.x) ||
-            (game_context->player_context->direction == PLAYER_UP && enemy_pos.y < player_pos.y) ||
-            (game_context->player_context->direction == PLAYER_DOWN && enemy_pos.y > player_pos.y))
+        if ((game_context->player_context->direction == ENTITY_LEFT && enemy_pos.x < player_pos.x) ||
+            (game_context->player_context->direction == ENTITY_RIGHT && enemy_pos.x > player_pos.x) ||
+            (game_context->player_context->direction == ENTITY_UP && enemy_pos.y < player_pos.y) ||
+            (game_context->player_context->direction == ENTITY_DOWN && enemy_pos.y > player_pos.y))
         {
             player_is_facing_enemy = true;
         }
@@ -285,7 +264,7 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
                 if (enemy_context->health <= 0)
                 {
                     FURI_LOG_I("Game", "Enemy '%s' is dead.. resetting enemy position and health", enemy_context->id);
-                    enemy_context->state = ENEMY_DEAD;
+                    enemy_context->state = ENTITY_DEAD;
 
                     // Reset enemy position and health
                     enemy_context->health = 100; // this needs to be set to the enemy's max health
@@ -300,9 +279,9 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
                 else
                 {
                     FURI_LOG_I("Game", "Enemy '%s' took %f damage from player", enemy_context->id, (double)game_context->player_context->strength);
-                    enemy_context->state = ENEMY_ATTACKED;
-
-                    // Bounce the enemy back by X units opposite their last movement direction
+                    enemy_context->state = ENTITY_ATTACKED;
+                    // Vector old_pos = entity_pos_get(self);
+                    //  Bounce the enemy back by X units opposite their last movement direction
                     enemy_pos.x -= game_context->player_context->dx * enemy_context->radius + game_context->icon_offset;
                     enemy_pos.y -= game_context->player_context->dy * enemy_context->radius + game_context->icon_offset;
                     entity_pos_set(self, enemy_pos);
@@ -333,7 +312,7 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
                 if (game_context->player_context->health <= 0)
                 {
                     FURI_LOG_I("Game", "Player is dead.. resetting player position and health");
-                    game_context->player_context->state = PLAYER_DEAD;
+                    game_context->player_context->state = ENTITY_DEAD;
 
                     // Reset player position and health
                     entity_pos_set(other, game_context->player_context->start_position);
@@ -349,7 +328,7 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
                 else
                 {
                     FURI_LOG_I("Game", "Player took %f damage from enemy '%s'", (double)enemy_context->strength, enemy_context->id);
-                    game_context->player_context->state = PLAYER_ATTACKED;
+                    game_context->player_context->state = ENTITY_ATTACKED;
 
                     // Bounce the player back by X units opposite their last movement direction
                     player_pos.x -= game_context->player_context->dx * enemy_context->radius + game_context->icon_offset;
@@ -372,16 +351,40 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
         }
 
         // Reset enemy's state
-        enemy_context->state = ENEMY_IDLE;
+        enemy_context->state = ENTITY_IDLE;
         enemy_context->elapsed_move_timer = 0.0f;
 
-        if (game_context->player_context->state == PLAYER_DEAD)
+        if (game_context->player_context->state == ENTITY_DEAD)
         {
             // Reset player's position and health
             entity_pos_set(other, game_context->player_context->start_position);
             game_context->player_context->health = game_context->player_context->max_health;
         }
     }
+    // if not player than must be an icon or npc; so push back
+    else
+    {
+        // push enemy back
+        Vector enemy_pos = entity_pos_get(self);
+        switch (enemy_context->direction)
+        {
+        case ENTITY_LEFT:
+            enemy_pos.x += (enemy_context->size.x + game_context->icon_offset);
+            break;
+        case ENTITY_RIGHT:
+            enemy_pos.x -= (enemy_context->size.x + game_context->icon_offset);
+            break;
+        case ENTITY_UP:
+            enemy_pos.y += (enemy_context->size.y + game_context->icon_offset);
+            break;
+        case ENTITY_DOWN:
+            enemy_pos.y -= (enemy_context->size.y + game_context->icon_offset);
+            break;
+        default:
+            break;
+        }
+        entity_pos_set(self, enemy_pos);
+    }
 }
 
 // Enemy update function
@@ -390,8 +393,8 @@ static void enemy_update(Entity *self, GameManager *manager, void *context)
     if (!self || !context || !manager)
         return;
 
-    EnemyContext *enemy_context = (EnemyContext *)context;
-    if (!enemy_context || enemy_context->state == ENEMY_DEAD)
+    EntityContext *enemy_context = (EntityContext *)context;
+    if (!enemy_context || enemy_context->state == ENTITY_DEAD)
     {
         return;
     }
@@ -410,7 +413,7 @@ static void enemy_update(Entity *self, GameManager *manager, void *context)
 
     switch (enemy_context->state)
     {
-    case ENEMY_IDLE:
+    case ENTITY_IDLE:
         // Increment the elapsed_move_timer
         enemy_context->elapsed_move_timer += delta_time;
 
@@ -422,21 +425,21 @@ static void enemy_update(Entity *self, GameManager *manager, void *context)
             if (fabs(current_pos.x - enemy_context->start_position.x) < (double)1.0 &&
                 fabs(current_pos.y - enemy_context->start_position.y) < (double)1.0)
             {
-                enemy_context->state = ENEMY_MOVING_TO_END;
+                enemy_context->state = ENTITY_MOVING_TO_END;
             }
             else
             {
-                enemy_context->state = ENEMY_MOVING_TO_START;
+                enemy_context->state = ENTITY_MOVING_TO_START;
             }
             enemy_context->elapsed_move_timer = 0.0f;
         }
         break;
 
-    case ENEMY_MOVING_TO_END:
-    case ENEMY_MOVING_TO_START:
+    case ENTITY_MOVING_TO_END:
+    case ENTITY_MOVING_TO_START:
     {
         // Determine the target position based on the current state
-        Vector target_position = (enemy_context->state == ENEMY_MOVING_TO_END) ? enemy_context->end_position : enemy_context->start_position;
+        Vector target_position = (enemy_context->state == ENTITY_MOVING_TO_END) ? enemy_context->end_position : enemy_context->start_position;
 
         // Get current position
         Vector current_pos = entity_pos_get(self);
@@ -446,23 +449,23 @@ static void enemy_update(Entity *self, GameManager *manager, void *context)
         if (current_pos.x < target_position.x)
         {
             direction_vector.x = 1.0f;
-            enemy_context->direction = ENEMY_RIGHT;
+            enemy_context->direction = ENTITY_RIGHT;
         }
         else if (current_pos.x > target_position.x)
         {
             direction_vector.x = -1.0f;
-            enemy_context->direction = ENEMY_LEFT;
+            enemy_context->direction = ENTITY_LEFT;
         }
 
         if (current_pos.y < target_position.y)
         {
             direction_vector.y = 1.0f;
-            enemy_context->direction = ENEMY_DOWN;
+            enemy_context->direction = ENTITY_DOWN;
         }
         else if (current_pos.y > target_position.y)
         {
             direction_vector.y = -1.0f;
-            enemy_context->direction = ENEMY_UP;
+            enemy_context->direction = ENTITY_UP;
         }
 
         // Normalize direction vector
@@ -500,7 +503,7 @@ static void enemy_update(Entity *self, GameManager *manager, void *context)
         // If reached the target position on both axes, transition to IDLE
         if (reached_x && reached_y)
         {
-            enemy_context->state = ENEMY_IDLE;
+            enemy_context->state = ENTITY_IDLE;
             enemy_context->elapsed_move_timer = 0.0f;
         }
     }
@@ -516,8 +519,12 @@ static void enemy_free(Entity *self, GameManager *manager, void *context)
 {
     UNUSED(self);
     UNUSED(manager);
-    if (context)
-        enemy_generic_free(context);
+    UNUSED(context);
+    if (enemy_context_generic)
+    {
+        free(enemy_context_generic);
+        enemy_context_generic = NULL;
+    }
 }
 
 // Enemy behavior structure
@@ -528,7 +535,7 @@ static const EntityDescription _generic_enemy = {
     .render = enemy_render,
     .collision = enemy_collision,
     .event = NULL,
-    .context_size = sizeof(EnemyContext),
+    .context_size = sizeof(EntityContext),
 };
 
 // Enemy function to return the entity description
@@ -551,7 +558,7 @@ const EntityDescription *enemy(
         return NULL;
     }
 
-    // Allocate a new EnemyContext with provided parameters
+    // Allocate a new EntityContext with provided parameters
     enemy_context_generic = enemy_generic_alloc(
         id,
         index,
@@ -565,37 +572,38 @@ const EntityDescription *enemy(
         health);
     if (!enemy_context_generic)
     {
-        FURI_LOG_E("Game", "Failed to allocate EnemyContext");
+        FURI_LOG_E("Game", "Failed to allocate EntityContext");
         return NULL;
     }
 
+    // assign sprites to the context
     enemy_context_generic->sprite_right = game_manager_sprite_load(manager, sprite_context->right_file_name);
     enemy_context_generic->sprite_left = game_manager_sprite_load(manager, sprite_context->left_file_name);
 
     // Set initial direction based on start and end positions
     if (start_position.x < end_position.x)
     {
-        enemy_context_generic->direction = ENEMY_RIGHT;
+        enemy_context_generic->direction = ENTITY_RIGHT;
     }
     else
     {
-        enemy_context_generic->direction = ENEMY_LEFT;
+        enemy_context_generic->direction = ENTITY_LEFT;
     }
 
     // Set initial state based on movement
     if (start_position.x != end_position.x || start_position.y != end_position.y)
     {
-        enemy_context_generic->state = ENEMY_MOVING_TO_END;
+        enemy_context_generic->state = ENTITY_MOVING_TO_END;
     }
     else
     {
-        enemy_context_generic->state = ENEMY_IDLE;
+        enemy_context_generic->state = ENTITY_IDLE;
     }
-
+    free(sprite_context);
     return &_generic_enemy;
 }
 
-void spawn_enemy_json_furi(Level *level, GameManager *manager, FuriString *json)
+void spawn_enemy(Level *level, GameManager *manager, FuriString *json)
 {
     if (!level || !manager || !json)
     {

+ 1 - 42
game/enemy.h

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

+ 54 - 26
game/game.c

@@ -1,5 +1,7 @@
+#include <gui/view_holder.h>
 #include <game/game.h>
 #include <game/storage.h>
+#include <alloc/alloc.h>
 
 /****** Game ******/
 /*
@@ -17,12 +19,19 @@ static void game_start(GameManager *game_manager, void *ctx)
     game_context->current_level = 0;
     game_context->level_count = 0;
     game_context->enemy_count = 0;
+    game_context->npc_count = 0;
 
     // set all levels to NULL
     for (int i = 0; i < MAX_LEVELS; i++)
-    {
         game_context->levels[i] = NULL;
-    }
+
+    // set all enemies to NULL
+    for (int i = 0; i < MAX_ENEMIES; i++)
+        game_context->enemies[i] = NULL;
+
+    // set all npcs to NULL
+    for (int i = 0; i < MAX_NPCS; i++)
+        game_context->npcs[i] = NULL;
 
     // attempt to allocate all levels
     for (int i = 0; i < MAX_LEVELS; i++)
@@ -31,16 +40,13 @@ static void game_start(GameManager *game_manager, void *ctx)
         {
             if (i == 0)
             {
-                game_context->levels[0] = game_manager_add_level(game_manager, generic_level("town_world_v2", 0));
+                game_context->levels[0] = game_manager_add_level(game_manager, training_world());
                 game_context->level_count = 1;
-                break;
             }
             break;
         }
         else
-        {
             game_context->level_count++;
-        }
     }
 
     // imu
@@ -48,6 +54,16 @@ static void game_start(GameManager *game_manager, void *ctx)
     game_context->imu_present = imu_present(game_context->imu);
 }
 
+static void thanks(Canvas *canvas, void *context)
+{
+    UNUSED(context);
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str(canvas, 35, 8, "Saving game");
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str(canvas, 0, 50, "Please wait while your");
+    canvas_draw_str(canvas, 0, 60, "game is saved.");
+}
+
 /*
     Write here the stop code for your game, for example, freeing memory, if it was allocated.
     You don't need to free level, sprites or entities, it will be done automatically.
@@ -55,37 +71,49 @@ static void game_start(GameManager *game_manager, void *ctx)
 */
 static void game_stop(void *ctx)
 {
-    if (!ctx)
-    {
-        FURI_LOG_E("Game", "Invalid game context");
-        return;
-    }
-
+    furi_check(ctx);
     GameContext *game_context = ctx;
-    if (!game_context)
-    {
-        FURI_LOG_E("Game", "Game context is NULL");
-        return;
-    }
-
     imu_free(game_context->imu);
     game_context->imu = NULL;
 
+    // clear current level early
+    if (game_context->levels[game_context->current_level])
+    {
+        level_clear(game_context->levels[game_context->current_level]);
+    }
+
     if (game_context->player_context)
     {
-        FURI_LOG_I("Game", "Game ending");
         if (!game_context->ended_early)
-        {
-            easy_flipper_dialog("Game Over", "Thanks for playing FlipWorld!\nHit BACK then wait for\nthe game to save.");
-        }
+            easy_flipper_dialog(
+                "Game Over",
+                "Thanks for playing FlipWorld!\nHit BACK then wait for\nthe game to save.");
         else
+            easy_flipper_dialog(
+                "Game Over", "Ran out of memory so the\ngame ended early.\nHit BACK to exit.");
+
+        ViewPort *view_port = view_port_alloc();
+        view_port_draw_callback_set(view_port, thanks, NULL);
+        Gui *gui = furi_record_open(RECORD_GUI);
+        gui_add_view_port(gui, view_port, GuiLayerFullscreen);
+        uint32_t tick_count = furi_get_tick();
+        furi_delay_ms(800);
+
+        save_player_context_api(game_context->player_context);
+
+        const uint32_t delay = 2500;
+        tick_count = (tick_count + delay) - furi_get_tick();
+        if (tick_count <= delay)
         {
-            easy_flipper_dialog("Game Over", "Ran out of memory so the\ngame ended early.\nHit BACK to exit.");
+            furi_delay_ms(tick_count);
         }
-        FURI_LOG_I("Game", "Saving player context");
-        save_player_context_api(game_context->player_context);
-        FURI_LOG_I("Game", "Player context saved");
+
         easy_flipper_dialog("Game Saved", "Hit BACK to exit.");
+
+        flip_world_show_submenu();
+
+        gui_remove_view_port(gui, view_port);
+        furi_record_close(RECORD_GUI);
     }
 }
 

+ 21 - 1
game/game.h

@@ -1,8 +1,28 @@
 #pragma once
+typedef enum
+{
+    ENTITY_IDLE,
+    ENTITY_MOVING,
+    ENTITY_MOVING_TO_START,
+    ENTITY_MOVING_TO_END,
+    ENTITY_ATTACKING,
+    ENTITY_ATTACKED,
+    ENTITY_DEAD
+} EntityState;
+
+typedef enum
+{
+    ENTITY_UP,
+    ENTITY_DOWN,
+    ENTITY_LEFT,
+    ENTITY_RIGHT
+} EntityDirection;
+
 #include "engine/engine.h"
 #include <engine/level_i.h>
+#include "flip_world.h"
 #include <game/world.h>
 #include <game/level.h>
 #include <game/enemy.h>
-#include "flip_world.h"
 #include <game/player.h>
+#include <game/npc.h>

+ 9 - 179
game/icon.c

@@ -24,17 +24,14 @@ static void icon_render(Entity *self, GameManager *manager, Canvas *canvas, void
 {
     UNUSED(manager);
     IconContext *ictx = (IconContext *)context;
-    if (ictx)
-    {
-        Vector pos = entity_pos_get(self);
-
-        // Draw the icon, centered
-        canvas_draw_icon(
-            canvas,
-            pos.x - camera_x - ictx->size.x / 2,
-            pos.y - camera_y - ictx->size.y / 2,
-            ictx->icon);
-    }
+    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);
 }
 
 static void icon_start(Entity *self, GameManager *manager, void *context)
@@ -83,10 +80,7 @@ static void icon_free(Entity *self, GameManager *manager, void *context)
 {
     UNUSED(self);
     UNUSED(manager);
-    if (context)
-    {
-        free(context);
-    }
+    UNUSED(context);
 }
 
 // -------------- Entity description --------------
@@ -116,130 +110,48 @@ static IconContext *icon_generic_alloc(const char *id, const Icon *icon, uint8_t
 
 IconContext *get_icon_context(const char *name)
 {
-    // if (is_str(name, "earth") )
-    // {
-    //     return icon_generic_alloc("earth", &I_icon_earth_15x16, 15, 16);
-    // }
-    // else if (is_str(name, "home") )
-    // {
-    //     return icon_generic_alloc("home", &I_icon_home_15x16, 15, 16);
-    // }
     if (is_str(name, "house"))
-    {
         return icon_generic_alloc("house", &I_icon_house_48x32px, 48, 32);
-    }
-    // else if (is_str(name, "house_3d") )
-    // {
-    //     return icon_generic_alloc("house_3d", &I_icon_house_3d_34x45px, 34, 45);
-    // }
-    // else if (is_str(name, "info") )
-    // {
-    //     return icon_generic_alloc("info", &I_icon_info_15x16, 15, 16);
-    // }
     else if (is_str(name, "man"))
-    {
         return icon_generic_alloc("man", &I_icon_man_7x16, 7, 16);
-    }
     else if (is_str(name, "plant"))
-    {
         return icon_generic_alloc("plant", &I_icon_plant_16x16, 16, 16);
-    }
-    // else if (is_str(name, "plant_fern") )
-    // {
-    //     return icon_generic_alloc("plant_fern", &I_icon_plant_fern_18x16px, 18, 16);
-    // }
-    // else if (is_str(name, "plant_pointy") )
-    // {
-    //     return icon_generic_alloc("plant_pointy", &I_icon_plant_pointy_13x16px, 13, 16);
-    // }
     else if (is_str(name, "tree"))
-    {
         return icon_generic_alloc("tree", &I_icon_tree_16x16, 16, 16);
-    }
-    // else if (is_str(name, "tree_29x30") )
-    // {
-    //     return icon_generic_alloc("tree_29x30", &I_icon_tree_29x30px, 29, 30);
-    // }
-    // else if (is_str(name, "tree_48x48") )
-    // {
-    //     return icon_generic_alloc("tree_48x48", &I_icon_tree_48x48px, 48, 48);
-    // }
     else if (is_str(name, "woman"))
-    {
         return icon_generic_alloc("woman", &I_icon_woman_9x16, 9, 16);
-    }
-    // else if (is_str(name, "chest_closed"))
-    // {
-    //     return icon_generic_alloc("chest_closed", &I_icon_chest_closed_16x13px, 16, 13);
-    // }
-    // else if (is_str(name, "chest_open") )
-    // {
-    //     return icon_generic_alloc("chest_open", &I_icon_chest_open_16x16px, 16, 16);
-    // }
     else if (is_str(name, "fence"))
-    {
         return icon_generic_alloc("fence", &I_icon_fence_16x8px, 16, 8);
-    }
     else if (is_str(name, "fence_end"))
-    {
         return icon_generic_alloc("fence_end", &I_icon_fence_end_16x8px, 16, 8);
-    }
     // else if (is_str(name, "fence_vertical_end") )
-    // {
     //     return icon_generic_alloc("fence_vertical_end", &I_icon_fence_vertical_end_6x8px, 6, 8);
-    // }
     // else if (is_str(name, "fence_vertical_start") )
-    // {
     //     return icon_generic_alloc("fence_vertical_start", &I_icon_fence_vertical_start_6x15px, 6, 15);
-    // }
     else if (is_str(name, "flower"))
-    {
         return icon_generic_alloc("flower", &I_icon_flower_16x16, 16, 16);
-    }
     else if (is_str(name, "lake_bottom"))
-    {
         return icon_generic_alloc("lake_bottom", &I_icon_lake_bottom_31x12px, 31, 12);
-    }
     else if (is_str(name, "lake_bottom_left"))
-    {
         return icon_generic_alloc("lake_bottom_left", &I_icon_lake_bottom_left_24x22px, 24, 22);
-    }
     else if (is_str(name, "lake_bottom_right"))
-    {
         return icon_generic_alloc("lake_bottom_right", &I_icon_lake_bottom_right_24x22px, 24, 22);
-    }
     else if (is_str(name, "lake_left"))
-    {
         return icon_generic_alloc("lake_left", &I_icon_lake_left_11x31px, 11, 31);
-    }
     else if (is_str(name, "lake_right"))
-    {
         return icon_generic_alloc("lake_right", &I_icon_lake_right_11x31, 11, 31);
-    }
     else if (is_str(name, "lake_top"))
-    {
         return icon_generic_alloc("lake_top", &I_icon_lake_top_31x12px, 31, 12);
-    }
     else if (is_str(name, "lake_top_left"))
-    {
         return icon_generic_alloc("lake_top_left", &I_icon_lake_top_left_24x22px, 24, 22);
-    }
     else if (is_str(name, "lake_top_right"))
-    {
         return icon_generic_alloc("lake_top_right", &I_icon_lake_top_right_24x22px, 24, 22);
-    }
     else if (is_str(name, "rock_large"))
-    {
         return icon_generic_alloc("rock_large", &I_icon_rock_large_18x19px, 18, 19);
-    }
     else if (is_str(name, "rock_medium"))
-    {
         return icon_generic_alloc("rock_medium", &I_icon_rock_medium_16x14px, 16, 14);
-    }
     else if (is_str(name, "rock_small"))
-    {
         return icon_generic_alloc("rock_small", &I_icon_rock_small_10x8px, 10, 8);
-    }
 
     // If no match is found
     FURI_LOG_E("Game", "Icon not found: %s", name);
@@ -248,130 +160,48 @@ IconContext *get_icon_context(const char *name)
 
 const char *icon_get_id(const Icon *icon)
 {
-    // if (icon == &I_icon_earth_15x16)
-    // {
-    //     return "earth";
-    // }
-    // else if (icon == &I_icon_home_15x16)
-    // {
-    //     return "home";
-    // }
     if (icon == &I_icon_house_48x32px)
-    {
         return "house";
-    }
-    // else if (icon == &I_icon_house_3d_34x45px)
-    // {
-    //     return "house_3d";
-    // }
-    // else if (icon == &I_icon_info_15x16)
-    // {
-    //     return "info";
-    // }
     else if (icon == &I_icon_man_7x16)
-    {
         return "man";
-    }
     else if (icon == &I_icon_plant_16x16)
-    {
         return "plant";
-    }
-    // else if (icon == &I_icon_plant_fern_18x16px)
-    // {
-    //     return "plant_fern";
-    // }
-    // else if (icon == &I_icon_plant_pointy_13x16px)
-    // {
-    //     return "plant_pointy";
-    // }
     else if (icon == &I_icon_tree_16x16)
-    {
         return "tree";
-    }
-    // else if (icon == &I_icon_tree_29x30px)
-    // {
-    //     return "tree_29x30";
-    // }
-    // else if (icon == &I_icon_tree_48x48px)
-    // {
-    //     return "tree_48x48";
-    // }
     else if (icon == &I_icon_woman_9x16)
-    {
         return "woman";
-    }
-    // else if (icon == &I_icon_chest_closed_16x13px)
-    // {
-    //     return "chest_closed";
-    // }
-    // else if (icon == &I_icon_chest_open_16x16px)
-    // {
-    //     return "chest_open";
-    // }
     else if (icon == &I_icon_fence_16x8px)
-    {
         return "fence";
-    }
     else if (icon == &I_icon_fence_end_16x8px)
-    {
         return "fence_end";
-    }
     // else if (icon == &I_icon_fence_vertical_end_6x8px)
-    // {
     //     return "fence_vertical_end";
-    // }
     // else if (icon == &I_icon_fence_vertical_start_6x15px)
-    // {
     //     return "fence_vertical_start";
-    // }
     else if (icon == &I_icon_flower_16x16)
-    {
         return "flower";
-    }
     else if (icon == &I_icon_lake_bottom_31x12px)
-    {
         return "lake_bottom";
-    }
     else if (icon == &I_icon_lake_bottom_left_24x22px)
-    {
         return "lake_bottom_left";
-    }
     else if (icon == &I_icon_lake_bottom_right_24x22px)
-    {
         return "lake_bottom_right";
-    }
     else if (icon == &I_icon_lake_left_11x31px)
-    {
         return "lake_left";
-    }
     else if (icon == &I_icon_lake_right_11x31)
-    {
         return "lake_right";
-    }
     else if (icon == &I_icon_lake_top_31x12px)
-    {
         return "lake_top";
-    }
     else if (icon == &I_icon_lake_top_left_24x22px)
-    {
         return "lake_top_left";
-    }
     else if (icon == &I_icon_lake_top_right_24x22px)
-    {
         return "lake_top_right";
-    }
     else if (icon == &I_icon_rock_large_18x19px)
-    {
         return "rock_large";
-    }
     else if (icon == &I_icon_rock_medium_16x14px)
-    {
         return "rock_medium";
-    }
     else if (icon == &I_icon_rock_small_10x8px)
-    {
         return "rock_small";
-    }
 
     // If no match is found
     FURI_LOG_E("Game", "Icon ID not found for given icon pointer.");

+ 40 - 8
game/level.c

@@ -12,7 +12,7 @@ bool allocate_level(GameManager *manager, int index)
     if (!world_list)
     {
         FURI_LOG_E("Game", "Failed to load world list");
-        game_context->levels[0] = game_manager_add_level(manager, generic_level("town_world_v2", 0));
+        game_context->levels[0] = game_manager_add_level(manager, training_world());
         game_context->level_count = 1;
         return false;
     }
@@ -29,7 +29,7 @@ bool allocate_level(GameManager *manager, int index)
     furi_string_free(world_list);
     return true;
 }
-static void set_world(Level *level, GameManager *manager, char *id)
+void set_world(Level *level, GameManager *manager, char *id)
 {
     char file_path[256];
     snprintf(file_path, sizeof(file_path),
@@ -40,7 +40,7 @@ static void set_world(Level *level, GameManager *manager, char *id)
     if (!json_data_str || furi_string_empty(json_data_str))
     {
         FURI_LOG_E("Game", "Failed to load json data from file");
-        draw_town_world(manager, level);
+        // draw_town_world(manager, level);
         return;
     }
 
@@ -58,7 +58,7 @@ static void set_world(Level *level, GameManager *manager, char *id)
     if (!draw_json_world_furi(manager, level, json_data_str))
     {
         FURI_LOG_E("Game", "Failed to draw world");
-        draw_town_world(manager, level);
+        // draw_town_world(manager, level);
         furi_string_free(json_data_str);
     }
     else
@@ -73,7 +73,7 @@ static void set_world(Level *level, GameManager *manager, char *id)
         if (!enemy_data_str || furi_string_empty(enemy_data_str))
         {
             FURI_LOG_E("Game", "Failed to get enemy data");
-            draw_town_world(manager, level);
+            // draw_town_world(manager, level);
             return;
         }
 
@@ -89,11 +89,43 @@ static void set_world(Level *level, GameManager *manager, char *id)
                 break;
             }
 
-            spawn_enemy_json_furi(level, manager, single_enemy_data);
+            spawn_enemy(level, manager, single_enemy_data);
             furi_string_free(single_enemy_data);
         }
         furi_string_free(enemy_data_str);
-        FURI_LOG_I("Game", "Finished loading world data");
+
+        // Draw NPCs
+        FURI_LOG_I("Game", "Drawing NPCs");
+        snprintf(file_path, sizeof(file_path),
+                 STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s/%s_npc_data.json",
+                 id, id);
+
+        FuriString *npc_data_str = flipper_http_load_from_file(file_path);
+        if (!npc_data_str || furi_string_empty(npc_data_str))
+        {
+            FURI_LOG_E("Game", "Failed to get npc data");
+            // draw_town_world(manager, level);
+            return;
+        }
+
+        // Loop through the array
+        for (int i = 0; i < MAX_NPCS; i++)
+        {
+            FuriString *single_npc_data = get_json_array_value_furi("npc_data", i, npc_data_str);
+            if (!single_npc_data || furi_string_empty(single_npc_data))
+            {
+                // No more npc elements found
+                if (single_npc_data)
+                    furi_string_free(single_npc_data);
+                break;
+            }
+
+            spawn_npc(level, manager, single_npc_data);
+            furi_string_free(single_npc_data);
+        }
+        furi_string_free(npc_data_str);
+
+        FURI_LOG_I("Game", "World drawn");
     }
 }
 static void level_start(Level *level, GameManager *manager, void *context)
@@ -129,7 +161,7 @@ static void level_start(Level *level, GameManager *manager, void *context)
         if (!world_data)
         {
             FURI_LOG_E("Game", "Failed to fetch world data");
-            draw_town_world(manager, level);
+            // draw_town_world(manager, level);
             game_context->is_switching_level = false;
             // furi_delay_ms(1000);
             player_spawn(level, manager);

+ 2 - 1
game/level.h

@@ -8,4 +8,5 @@ typedef struct
 } LevelContext;
 
 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);

+ 436 - 0
game/npc.c

@@ -0,0 +1,436 @@
+#include <game/npc.h>
+static EntityContext *npc_context_generic;
+
+// Allocation function
+static EntityContext *npc_generic_alloc(
+    const char *id,
+    int index,
+    Vector size,
+    Vector start_position,
+    Vector end_position,
+    float move_timer, // Wait duration before moving again
+    float speed,
+    const char *message)
+{
+    if (!npc_context_generic)
+    {
+        npc_context_generic = malloc(sizeof(EntityContext));
+    }
+    if (!npc_context_generic)
+    {
+        FURI_LOG_E("Game", "Failed to allocate EntityContext");
+        return NULL;
+    }
+    snprintf(npc_context_generic->id, sizeof(npc_context_generic->id), "%s", id);
+    npc_context_generic->index = index;
+    npc_context_generic->size = size;
+    npc_context_generic->start_position = start_position;
+    npc_context_generic->end_position = end_position;
+    npc_context_generic->move_timer = move_timer;   // Set wait duration
+    npc_context_generic->elapsed_move_timer = 0.0f; // Initialize elapsed timer
+    npc_context_generic->speed = speed;
+    snprintf(npc_context_generic->message, sizeof(npc_context_generic->message), "%s", message);
+    // Initialize other fields as needed
+    npc_context_generic->sprite_right = NULL;          // sprite is assigned later
+    npc_context_generic->sprite_left = NULL;           // sprite is assigned later
+    npc_context_generic->direction = ENTITY_RIGHT;     // Default direction
+    npc_context_generic->state = ENTITY_MOVING_TO_END; // Start in IDLE state
+    // Set radius based on size, for example, average of size.x and size.y divided by 2
+    npc_context_generic->radius = (size.x + size.y) / 4.0f;
+    return npc_context_generic;
+}
+
+// NPC start function
+static void npc_start(Entity *self, GameManager *manager, void *context)
+{
+    UNUSED(manager);
+    if (!self || !context)
+    {
+        FURI_LOG_E("Game", "Enemy start: Invalid parameters");
+        return;
+    }
+    if (!npc_context_generic)
+    {
+        FURI_LOG_E("Game", "NPC start: NPC context not set");
+        return;
+    }
+
+    EntityContext *npc_context = (EntityContext *)context;
+    // Copy fields from generic context
+    snprintf(npc_context->id, sizeof(npc_context->id), "%s", npc_context_generic->id);
+    snprintf(npc_context->message, sizeof(npc_context->message), "%s", npc_context_generic->message);
+    npc_context->index = npc_context_generic->index;
+    npc_context->size = npc_context_generic->size;
+    npc_context->start_position = npc_context_generic->start_position;
+    npc_context->end_position = npc_context_generic->end_position;
+    npc_context->move_timer = npc_context_generic->move_timer;
+    npc_context->elapsed_move_timer = npc_context_generic->elapsed_move_timer;
+    npc_context->speed = npc_context_generic->speed;
+    npc_context->sprite_right = npc_context_generic->sprite_right;
+    npc_context->sprite_left = npc_context_generic->sprite_left;
+    npc_context->direction = npc_context_generic->direction;
+    npc_context->state = npc_context_generic->state;
+    npc_context->radius = npc_context_generic->radius;
+
+    // Set NPC's initial position based on start_position
+    entity_pos_set(self, npc_context->start_position);
+
+    // Add collision circle based on the NPC's radius
+    entity_collider_add_circle(self, npc_context->radius);
+}
+
+// NPC render function
+static void npc_render(Entity *self, GameManager *manager, Canvas *canvas, void *context)
+{
+    if (!self || !context || !canvas || !manager)
+        return;
+
+    EntityContext *npc_context = (EntityContext *)context;
+
+    // Get the position of the NPC
+    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;
+
+    // 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)
+        return;
+
+    // Choose sprite based on direction
+    Sprite *current_sprite = NULL;
+    if (npc_context->direction == ENTITY_LEFT)
+    {
+        current_sprite = npc_context->sprite_left;
+    }
+    else
+    {
+        current_sprite = npc_context->sprite_right;
+    }
+
+    // Draw NPC sprite relative to camera, centered on the NPC's position
+    canvas_draw_sprite(
+        canvas,
+        current_sprite,
+        pos.x - camera_x - (npc_context->size.x / 2),
+        pos.y - camera_y - (npc_context->size.y / 2));
+}
+
+// NPC collision function
+static void npc_collision(Entity *self, Entity *other, GameManager *manager, void *context)
+{
+    if (!self || !other || !context || !manager)
+    {
+        FURI_LOG_E("Game", "NPC collision: Invalid parameters");
+        return;
+    }
+
+    // Check if the NPC collided with the player
+    if (entity_description_get(other) == &player_desc)
+    {
+        // Retrieve NPC context
+        EntityContext *npc_context = (EntityContext *)context;
+        GameContext *game_context = game_manager_game_context_get(manager);
+        furi_check(npc_context);
+        furi_check(game_context);
+
+        // Get positions of the NPC and the player
+        Vector npc_pos = entity_pos_get(self);
+        Vector player_pos = entity_pos_get(other);
+
+        // Determine if the NPC is facing the player or player is facing the NPC
+        bool player_is_facing_npc = false;
+
+        // Determine if the player is facing the NPC
+        if ((game_context->player_context->direction == ENTITY_LEFT && npc_pos.x < player_pos.x) ||
+            (game_context->player_context->direction == ENTITY_RIGHT && npc_pos.x > player_pos.x) ||
+            (game_context->player_context->direction == ENTITY_UP && npc_pos.y < player_pos.y) ||
+            (game_context->player_context->direction == ENTITY_DOWN && npc_pos.y > player_pos.y))
+        {
+            player_is_facing_npc = true;
+        }
+
+        // bounce the player back to where it came from
+        // Set the player's old position to prevent collision
+        entity_pos_set(other, game_context->player_context->old_position);
+        // Reset player's movement direction to prevent immediate re-collision
+        game_context->player_context->dx = 0;
+        game_context->player_context->dy = 0;
+
+        // Press OK and facing NPC
+        if (player_is_facing_npc && game_context->last_button == GameKeyOk)
+        {
+            // show the NPC dialog on the game menu
+            game_context->menu_screen = GAME_MENU_NPC;
+            game_context->menu_selection = 0;
+            snprintf(game_context->message, sizeof(game_context->message), "%s", npc_context->message);
+            game_context->is_menu_open = true;
+        }
+    }
+}
+
+// NPC update function
+static void npc_update(Entity *self, GameManager *manager, void *context)
+{
+    if (!self || !context || !manager)
+        return;
+
+    EntityContext *npc_context = (EntityContext *)context;
+    if (!npc_context || npc_context->state == ENTITY_DEAD)
+    {
+        return;
+    }
+
+    GameContext *game_context = game_manager_game_context_get(manager);
+    if (!game_context)
+    {
+        FURI_LOG_E("Game", "NPC update: Failed to get GameContext");
+        return;
+    }
+
+    float delta_time = 1.0f / game_context->fps;
+
+    switch (npc_context->state)
+    {
+    case ENTITY_IDLE:
+        // Increment the elapsed_move_timer
+        npc_context->elapsed_move_timer += delta_time;
+
+        // Check if it's time to move again
+        if (npc_context->elapsed_move_timer >= npc_context->move_timer)
+        {
+            // Determine the next state based on the current position
+            Vector current_pos = entity_pos_get(self);
+            if (fabs(current_pos.x - npc_context->start_position.x) < (double)1.0 &&
+                fabs(current_pos.y - npc_context->start_position.y) < (double)1.0)
+            {
+                npc_context->state = ENTITY_MOVING_TO_END;
+            }
+            else
+            {
+                npc_context->state = ENTITY_MOVING_TO_START;
+            }
+            npc_context->elapsed_move_timer = 0.0f;
+        }
+        break;
+
+    case ENTITY_MOVING_TO_END:
+    case ENTITY_MOVING_TO_START:
+    {
+        // Determine the target position based on the current state
+        Vector target_position = (npc_context->state == ENTITY_MOVING_TO_END) ? npc_context->end_position : npc_context->start_position;
+
+        // Get current position
+        Vector current_pos = entity_pos_get(self);
+        Vector direction_vector = {0, 0};
+
+        // Calculate direction towards the target
+        if (current_pos.x < target_position.x)
+        {
+            direction_vector.x = 1.0f;
+            npc_context->direction = ENTITY_RIGHT;
+        }
+        else if (current_pos.x > target_position.x)
+        {
+            direction_vector.x = -1.0f;
+            npc_context->direction = ENTITY_LEFT;
+        }
+
+        if (current_pos.y < target_position.y)
+        {
+            direction_vector.y = 1.0f;
+            npc_context->direction = ENTITY_DOWN;
+        }
+        else if (current_pos.y > target_position.y)
+        {
+            direction_vector.y = -1.0f;
+            npc_context->direction = ENTITY_UP;
+        }
+
+        // Normalize direction vector
+        float length = sqrt(direction_vector.x * direction_vector.x + direction_vector.y * direction_vector.y);
+        if (length != 0)
+        {
+            direction_vector.x /= length;
+            direction_vector.y /= length;
+        }
+
+        // Update position based on direction and speed
+        Vector new_pos = current_pos;
+        new_pos.x += direction_vector.x * npc_context->speed * delta_time;
+        new_pos.y += direction_vector.y * npc_context->speed * delta_time;
+
+        // Clamp the position to the target to prevent overshooting
+        if ((direction_vector.x > 0.0f && new_pos.x > target_position.x) ||
+            (direction_vector.x < 0.0f && new_pos.x < target_position.x))
+        {
+            new_pos.x = target_position.x;
+        }
+
+        if ((direction_vector.y > 0.0f && new_pos.y > target_position.y) ||
+            (direction_vector.y < 0.0f && new_pos.y < target_position.y))
+        {
+            new_pos.y = target_position.y;
+        }
+
+        entity_pos_set(self, new_pos);
+
+        // Check if the nPC has reached or surpassed the target_position
+        bool reached_x = fabs(new_pos.x - target_position.x) < (double)1.0;
+        bool reached_y = fabs(new_pos.y - target_position.y) < (double)1.0;
+
+        // If reached the target position on both axes, transition to IDLE
+        if (reached_x && reached_y)
+        {
+            npc_context->state = ENTITY_IDLE;
+            npc_context->elapsed_move_timer = 0.0f;
+        }
+    }
+    break;
+
+    default:
+        break;
+    }
+}
+
+// Free function for the entity
+static void npc_free(Entity *self, GameManager *manager, void *context)
+{
+    UNUSED(self);
+    UNUSED(manager);
+    UNUSED(context);
+    if (npc_context_generic)
+    {
+        free(npc_context_generic);
+        npc_context_generic = NULL;
+    }
+}
+
+// NPC Behavior structure
+static const EntityDescription _generic_npc = {
+    .start = npc_start,
+    .stop = npc_free,
+    .update = npc_update,
+    .render = npc_render,
+    .collision = npc_collision,
+    .event = NULL,
+    .context_size = sizeof(EntityContext),
+};
+
+// Spawn function to return the entity description
+const EntityDescription *npc(
+    GameManager *manager,
+    const char *id,
+    int index,
+    Vector start_position,
+    Vector end_position,
+    float move_timer, // Wait duration before moving again
+    float speed,
+    const char *message)
+{
+    SpriteContext *sprite_context = get_sprite_context(id);
+    if (!sprite_context)
+    {
+        FURI_LOG_E("Game", "Failed to get SpriteContext");
+        return NULL;
+    }
+
+    // Allocate a new EntityContext with provided parameters
+    npc_context_generic = npc_generic_alloc(
+        id,
+        index,
+        (Vector){sprite_context->width, sprite_context->height},
+        start_position,
+        end_position,
+        move_timer,
+        speed,
+        message);
+    if (!npc_context_generic)
+    {
+        FURI_LOG_E("Game", "Failed to allocate EntityContext");
+        return NULL;
+    }
+
+    // assign sprites to the context
+    npc_context_generic->sprite_right = game_manager_sprite_load(manager, sprite_context->right_file_name);
+    npc_context_generic->sprite_left = game_manager_sprite_load(manager, sprite_context->left_file_name);
+
+    // Set initial direction based on start and end positions
+    if (start_position.x < end_position.x)
+    {
+        npc_context_generic->direction = ENTITY_RIGHT;
+    }
+    else
+    {
+        npc_context_generic->direction = ENTITY_LEFT;
+    }
+
+    // Set initial state based on movement
+    if (start_position.x != end_position.x || start_position.y != end_position.y)
+    {
+        npc_context_generic->state = ENTITY_MOVING_TO_END;
+    }
+    else
+    {
+        npc_context_generic->state = ENTITY_IDLE;
+    }
+    free(sprite_context);
+    return &_generic_npc;
+}
+
+void spawn_npc(Level *level, GameManager *manager, FuriString *json)
+{
+    if (!level || !manager || !json)
+    {
+        FURI_LOG_E("Game", "Level, GameManager, or JSON is NULL");
+        return;
+    }
+
+    FuriString *id = get_json_value_furi("id", json);
+    FuriString *_index = get_json_value_furi("index", json);
+    //
+    FuriString *start_position = get_json_value_furi("start_position", json);
+    FuriString *start_position_x = get_json_value_furi("x", start_position);
+    FuriString *start_position_y = get_json_value_furi("y", start_position);
+    //
+    FuriString *end_position = get_json_value_furi("end_position", json);
+    FuriString *end_position_x = get_json_value_furi("x", end_position);
+    FuriString *end_position_y = get_json_value_furi("y", end_position);
+    //
+    FuriString *move_timer = get_json_value_furi("move_timer", json);
+    FuriString *speed = get_json_value_furi("speed", json);
+    //
+    FuriString *message = get_json_value_furi("message", json);
+
+    if (!id || !_index || !start_position || !start_position_x || !start_position_y || !end_position || !end_position_x || !end_position_y || !move_timer || !speed || !message)
+    {
+        FURI_LOG_E("Game", "Failed to get JSON values");
+        return;
+    }
+
+    GameContext *game_context = game_manager_game_context_get(manager);
+    if (game_context && game_context->npc_count < MAX_NPCS && !game_context->npcs[game_context->npc_count])
+    {
+        game_context->npcs[game_context->npc_count] = level_add_entity(level, npc(
+                                                                                  manager,
+                                                                                  furi_string_get_cstr(id),
+                                                                                  atoi(furi_string_get_cstr(_index)),
+                                                                                  (Vector){atof_furi(start_position_x), atof_furi(start_position_y)},
+                                                                                  (Vector){atof_furi(end_position_x), atof_furi(end_position_y)},
+                                                                                  atof_furi(move_timer),
+                                                                                  atof_furi(speed),
+                                                                                  furi_string_get_cstr(message)));
+        game_context->npc_count++;
+    }
+
+    furi_string_free(id);
+    furi_string_free(_index);
+    furi_string_free(start_position);
+    furi_string_free(start_position_x);
+    furi_string_free(start_position_y);
+    furi_string_free(end_position);
+    furi_string_free(end_position_x);
+    furi_string_free(end_position_y);
+    furi_string_free(move_timer);
+    furi_string_free(speed);
+}

+ 5 - 0
game/npc.h

@@ -0,0 +1,5 @@
+#pragma once
+#include <game/game.h>
+#include "flip_world.h"
+
+void spawn_npc(Level *level, GameManager *manager, FuriString *json);

+ 62 - 48
game/player.c

@@ -68,9 +68,6 @@ void player_spawn(Level *level, GameManager *manager)
     // Set player position.
     entity_pos_set(game_context->player, (Vector){WORLD_WIDTH / 2, WORLD_HEIGHT / 2});
 
-    // Box is centered in player x and y, and its size
-    entity_collider_add_rect(game_context->player, 13, 11);
-
     // Get player context
     PlayerContext *pctx = entity_context_get(game_context->player);
     if (!pctx)
@@ -86,6 +83,9 @@ void player_spawn(Level *level, GameManager *manager)
         return;
     }
 
+    // add a collider to the player entity
+    entity_collider_add_rect(game_context->player, sprite_context->width, sprite_context->height);
+
     // player context must be set each level or NULL pointer will be dereferenced
     if (!load_player_context(pctx))
     {
@@ -94,7 +94,8 @@ void player_spawn(Level *level, GameManager *manager)
         // Initialize default player context
         pctx->sprite_right = game_manager_sprite_load(manager, sprite_context->right_file_name);
         pctx->sprite_left = game_manager_sprite_load(manager, sprite_context->left_file_name);
-        pctx->direction = PLAYER_RIGHT; // default direction
+        pctx->direction = ENTITY_RIGHT; // default direction
+        pctx->left = false;             // default sprite direction
         pctx->health = 100;
         pctx->strength = 10;
         pctx->level = 1;
@@ -124,11 +125,11 @@ void player_spawn(Level *level, GameManager *manager)
         {
             FURI_LOG_E(TAG, "Failed to save player context after initialization");
         }
-
+        free(sprite_context);
         return;
     }
 
-    // Load player sprite (we'll add this to the JSON later when players can choose their sprite)
+    // Load player sprite
     pctx->sprite_right = game_manager_sprite_load(manager, sprite_context->right_file_name);
     pctx->sprite_left = game_manager_sprite_load(manager, sprite_context->left_file_name);
 
@@ -156,8 +157,12 @@ void player_spawn(Level *level, GameManager *manager)
     pctx->strength = 10 + (pctx->level * 1);           // 1 strength per level
     pctx->max_health = 100 + ((pctx->level - 1) * 10); // 10 health per level
 
+    // set the player's left sprite direction
+    pctx->left = pctx->direction == ENTITY_LEFT ? true : false;
+
     // Assign loaded player context to game context
     game_context->player_context = pctx;
+    free(sprite_context);
 }
 
 static int vgm_increase(float value, float increase)
@@ -176,25 +181,25 @@ static void vgm_direction(Imu *imu, PlayerContext *player, Vector *pos)
     {
         pos->x += vgm_increase(pitch, min_x);
         player->dx = 1;
-        player->direction = PLAYER_RIGHT;
+        player->direction = ENTITY_RIGHT;
     }
     else if (pitch < -min_x)
     {
         pos->x += -vgm_increase(pitch, min_x);
         player->dx = -1;
-        player->direction = PLAYER_LEFT;
+        player->direction = ENTITY_LEFT;
     }
     if (roll > min_y)
     {
         pos->y += vgm_increase(roll, min_y);
         player->dy = 1;
-        player->direction = PLAYER_DOWN;
+        player->direction = ENTITY_DOWN;
     }
     else if (roll < -min_y)
     {
         pos->y += -vgm_increase(roll, min_y);
         player->dy = -1;
-        player->direction = PLAYER_UP;
+        player->direction = ENTITY_UP;
     }
 }
 
@@ -248,7 +253,7 @@ static void player_update(Entity *self, GameManager *manager, void *context)
         {
             pos.y -= (2 + game_context->icon_offset);
             player->dy = -1;
-            player->direction = PLAYER_UP;
+            player->direction = ENTITY_UP;
         }
         else
         {
@@ -269,7 +274,7 @@ static void player_update(Entity *self, GameManager *manager, void *context)
         {
             pos.y += (2 + game_context->icon_offset);
             player->dy = 1;
-            player->direction = PLAYER_DOWN;
+            player->direction = ENTITY_DOWN;
         }
         else
         {
@@ -290,7 +295,7 @@ static void player_update(Entity *self, GameManager *manager, void *context)
         {
             pos.x -= (2 + game_context->icon_offset);
             player->dx = -1;
-            player->direction = PLAYER_LEFT;
+            player->direction = ENTITY_LEFT;
         }
         else
         {
@@ -313,7 +318,7 @@ static void player_update(Entity *self, GameManager *manager, void *context)
         {
             pos.x += (2 + game_context->icon_offset);
             player->dx = 1;
-            player->direction = PLAYER_RIGHT;
+            player->direction = ENTITY_RIGHT;
         }
         else
         {
@@ -390,10 +395,10 @@ static void player_update(Entity *self, GameManager *manager, void *context)
     {
         player->dx = prev_dx;
         player->dy = prev_dy;
-        player->state = PLAYER_IDLE;
+        player->state = ENTITY_IDLE;
     }
     else
-        player->state = PLAYER_MOVING;
+        player->state = ENTITY_MOVING;
 }
 
 static void player_render(Entity *self, GameManager *manager, Canvas *canvas, void *context)
@@ -415,18 +420,32 @@ static void player_render(Entity *self, GameManager *manager, Canvas *canvas, vo
     camera_x = CLAMP(camera_x, WORLD_WIDTH - SCREEN_WIDTH, 0);
     camera_y = CLAMP(camera_y, WORLD_HEIGHT - SCREEN_HEIGHT, 0);
 
-    // Draw player sprite relative to camera, centered on the player's position
-    canvas_draw_sprite(
-        canvas,
-        player->direction == PLAYER_RIGHT ? player->sprite_right : player->sprite_left,
-        pos.x - camera_x - 5, // Center the sprite horizontally
-        pos.y - camera_y - 5  // Center the sprite vertically
-    );
+    // if player is moving right or left, draw the corresponding sprite
+    if (player->direction == ENTITY_RIGHT || player->direction == ENTITY_LEFT)
+    {
+        canvas_draw_sprite(
+            canvas,
+            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
+        );
+        player->left = false;
+    }
+    else // otherwise
+    {
+        // Default to last sprite direction
+        canvas_draw_sprite(
+            canvas,
+            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
+        );
+    }
 
     // Draw the outer bounds adjusted by camera offset
     canvas_draw_frame(canvas, -camera_x, -camera_y, WORLD_WIDTH, WORLD_HEIGHT);
 
-    // Draw the user stats (health, xp, and level)
+    // render background
     background_render(canvas, manager);
 }
 
@@ -440,7 +459,7 @@ const EntityDescription player_desc = {
     .context_size = sizeof(PlayerContext), // size of entity context, will be automatically allocated and freed
 };
 
-static SpriteContext *sprite_generic_alloc(const char *id, bool is_enemy, uint8_t width, uint8_t height)
+static SpriteContext *sprite_generic_alloc(const char *id, const char *type, uint8_t width, uint8_t height)
 {
     SpriteContext *ctx = malloc(sizeof(SpriteContext));
     if (!ctx)
@@ -451,49 +470,44 @@ static SpriteContext *sprite_generic_alloc(const char *id, bool is_enemy, uint8_
     snprintf(ctx->id, sizeof(ctx->id), "%s", id);
     ctx->width = width;
     ctx->height = height;
-    if (!is_enemy)
+    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);
     }
-    else
+    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);
     }
+    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);
+    }
     return ctx;
 }
 
 SpriteContext *get_sprite_context(const char *name)
 {
     if (is_str(name, "axe"))
-    {
-        return sprite_generic_alloc("axe", false, 15, 11);
-    }
+        return sprite_generic_alloc("axe", "player", 15, 11);
     else if (is_str(name, "bow"))
-    {
-        return sprite_generic_alloc("bow", false, 13, 11);
-    }
+        return sprite_generic_alloc("bow", "player", 13, 11);
     else if (is_str(name, "naked"))
-    {
-        return sprite_generic_alloc("naked", false, 10, 10);
-    }
+        return sprite_generic_alloc("naked", "player", 10, 10);
     else if (is_str(name, "sword"))
-    {
-        return sprite_generic_alloc("sword", false, 15, 11);
-    }
+        return sprite_generic_alloc("sword", "player", 15, 11);
+    //
     else if (is_str(name, "cyclops"))
-    {
-        return sprite_generic_alloc("cyclops", true, 10, 11);
-    }
+        return sprite_generic_alloc("cyclops", "enemy", 10, 11);
     else if (is_str(name, "ghost"))
-    {
-        return sprite_generic_alloc("ghost", true, 15, 15);
-    }
+        return sprite_generic_alloc("ghost", "enemy", 15, 15);
     else if (is_str(name, "ogre"))
-    {
-        return sprite_generic_alloc("ogre", true, 10, 13);
-    }
+        return sprite_generic_alloc("ogre", "enemy", 10, 13);
+    //
+    else if (is_str(name, "funny"))
+        return sprite_generic_alloc("funny", "npc", 15, 21);
 
     // If no match is found
     FURI_LOG_E("Game", "Sprite not found: %s", name);

+ 30 - 18
game/player.h

@@ -4,32 +4,38 @@
 #include <game/game.h>
 #include "engine/sensors/imu.h"
 
-// Maximum enemies
 #define MAX_ENEMIES 10
 #define MAX_LEVELS 10
+#define MAX_NPCS 10
 
-typedef enum
-{
-    PLAYER_IDLE,
-    PLAYER_MOVING,
-    PLAYER_ATTACKING,
-    PLAYER_ATTACKED,
-    PLAYER_DEAD,
-} PlayerState;
-
-typedef enum
+// EntityContext definition
+typedef struct
 {
-    PLAYER_UP,
-    PLAYER_DOWN,
-    PLAYER_LEFT,
-    PLAYER_RIGHT
-} PlayerDirection;
+    char id[64];                // Unique ID for the entity type
+    int index;                  // Index for the specific entity instance
+    Vector size;                // Size of the entity
+    Sprite *sprite_right;       // Entity sprite when looking right
+    Sprite *sprite_left;        // Entity sprite when looking left
+    EntityDirection direction;  // Direction the entity is facing
+    EntityState state;          // Current state of the entity
+    Vector start_position;      // Start position of the entity
+    Vector end_position;        // End position of the entity
+    float move_timer;           // Timer for the entity movement
+    float elapsed_move_timer;   // Elapsed time for the entity movement
+    float radius;               // Collision radius for the entity
+    float speed;                // Speed of the entity
+    float attack_timer;         // Cooldown duration between attacks
+    float elapsed_attack_timer; // Time elapsed since the last attack
+    float strength;             // Damage the entity deals
+    float health;               // Health of the entity
+    char message[64];           // Message to display when interacting with the entity
+} EntityContext;
 
 typedef struct
 {
     Vector old_position;        // previous position of the player
-    PlayerDirection direction;  // direction the player is facing
-    PlayerState state;          // current state of the player
+    EntityDirection direction;  // direction the player is facing
+    EntityState state;          // current state of the player
     Vector start_position;      // starting position of the player
     Sprite *sprite_right;       // player sprite looking right
     Sprite *sprite_left;        // player sprite looking left
@@ -45,6 +51,7 @@ typedef struct
     float attack_timer;         // Cooldown duration between attacks
     float elapsed_attack_timer; // Time elapsed since the last attack
     char username[32];          // player username
+    bool left;                  // track player sprite direction
 } PlayerContext;
 
 // two screens for the game menu
@@ -52,6 +59,7 @@ typedef enum
 {
     GAME_MENU_INFO, // level, health, xp, etc.
     GAME_MENU_MORE, // more settings
+    GAME_MENU_NPC,  // NPC dialog
 } GameMenuScreen;
 
 typedef struct
@@ -59,10 +67,12 @@ typedef struct
     PlayerContext *player_context;
     Level *levels[MAX_LEVELS];
     Entity *enemies[MAX_ENEMIES];
+    Entity *npcs[MAX_NPCS];
     Entity *player;
     float fps;
     int level_count;
     int enemy_count;
+    int npc_count;
     int current_level;
     bool ended_early;
     Imu *imu;
@@ -79,6 +89,8 @@ typedef struct
     //
     int icon_count;
     int icon_offset;
+    //
+    char message[64];
 } GameContext;
 
 typedef struct

+ 88 - 100
game/storage.c

@@ -30,13 +30,15 @@ bool save_player_context(PlayerContext *player_context)
         return false;
     }
 
-    // Create the directory for saving settings
-    char directory_path[256];
-    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data/player");
-
-    // Create the directory
+    // ensure the folders exist
+    char directory_path[128];
+    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world");
     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/player");
+    storage_common_mkdir(storage, directory_path);
     furi_record_close(RECORD_STORAGE);
 
     // 1. Username (String)
@@ -114,16 +116,16 @@ bool save_player_context(PlayerContext *player_context)
         char direction_str[2];
         switch (player_context->direction)
         {
-        case PLAYER_UP:
+        case ENTITY_UP:
             strncpy(direction_str, "0", sizeof(direction_str));
             break;
-        case PLAYER_DOWN:
+        case ENTITY_DOWN:
             strncpy(direction_str, "1", sizeof(direction_str));
             break;
-        case PLAYER_LEFT:
+        case ENTITY_LEFT:
             strncpy(direction_str, "2", sizeof(direction_str));
             break;
-        case PLAYER_RIGHT:
+        case ENTITY_RIGHT:
         default:
             strncpy(direction_str, "3", sizeof(direction_str));
             break;
@@ -142,19 +144,19 @@ bool save_player_context(PlayerContext *player_context)
         char state_str[2];
         switch (player_context->state)
         {
-        case PLAYER_IDLE:
+        case ENTITY_IDLE:
             strncpy(state_str, "0", sizeof(state_str));
             break;
-        case PLAYER_MOVING:
+        case ENTITY_MOVING:
             strncpy(state_str, "1", sizeof(state_str));
             break;
-        case PLAYER_ATTACKING:
+        case ENTITY_ATTACKING:
             strncpy(state_str, "2", sizeof(state_str));
             break;
-        case PLAYER_ATTACKED:
+        case ENTITY_ATTACKED:
             strncpy(state_str, "3", sizeof(state_str));
             break;
-        case PLAYER_DEAD:
+        case ENTITY_DEAD:
             strncpy(state_str, "4", sizeof(state_str));
             break;
         default:
@@ -291,16 +293,16 @@ bool save_player_context_api(PlayerContext *player_context)
     furi_string_cat_str(json, "\"direction\":");
     switch (player_context->direction)
     {
-    case PLAYER_UP:
+    case ENTITY_UP:
         furi_string_cat_str(json, "\"up\",");
         break;
-    case PLAYER_DOWN:
+    case ENTITY_DOWN:
         furi_string_cat_str(json, "\"down\",");
         break;
-    case PLAYER_LEFT:
+    case ENTITY_LEFT:
         furi_string_cat_str(json, "\"left\",");
         break;
-    case PLAYER_RIGHT:
+    case ENTITY_RIGHT:
     default:
         furi_string_cat_str(json, "\"right\",");
         break;
@@ -310,19 +312,19 @@ bool save_player_context_api(PlayerContext *player_context)
     furi_string_cat_str(json, "\"state\":");
     switch (player_context->state)
     {
-    case PLAYER_IDLE:
+    case ENTITY_IDLE:
         furi_string_cat_str(json, "\"idle\",");
         break;
-    case PLAYER_MOVING:
+    case ENTITY_MOVING:
         furi_string_cat_str(json, "\"moving\",");
         break;
-    case PLAYER_ATTACKING:
+    case ENTITY_ATTACKING:
         furi_string_cat_str(json, "\"attacking\",");
         break;
-    case PLAYER_ATTACKED:
+    case ENTITY_ATTACKED:
         furi_string_cat_str(json, "\"attacked\",");
         break;
-    case PLAYER_DEAD:
+    case ENTITY_DEAD:
         furi_string_cat_str(json, "\"dead\",");
         break;
     default:
@@ -633,63 +635,63 @@ bool load_player_context(PlayerContext *player_context)
 
     // 11. Direction (enum PlayerDirection)
     {
-        int direction_int = 3; // Default to PLAYER_RIGHT
+        int direction_int = 3; // Default to ENTITY_RIGHT
         if (!load_number("player/direction", &direction_int))
         {
-            FURI_LOG_E(TAG, "No data or parse error for direction. Defaulting to PLAYER_RIGHT");
+            FURI_LOG_E(TAG, "No data or parse error for direction. Defaulting to ENTITY_RIGHT");
             direction_int = 3;
         }
 
         switch (direction_int)
         {
         case 0:
-            player_context->direction = PLAYER_UP;
+            player_context->direction = ENTITY_UP;
             break;
         case 1:
-            player_context->direction = PLAYER_DOWN;
+            player_context->direction = ENTITY_DOWN;
             break;
         case 2:
-            player_context->direction = PLAYER_LEFT;
+            player_context->direction = ENTITY_LEFT;
             break;
         case 3:
-            player_context->direction = PLAYER_RIGHT;
+            player_context->direction = ENTITY_RIGHT;
             break;
         default:
-            FURI_LOG_E(TAG, "Invalid direction value: %d. Defaulting to PLAYER_RIGHT", direction_int);
-            player_context->direction = PLAYER_RIGHT;
+            FURI_LOG_E(TAG, "Invalid direction value: %d. Defaulting to ENTITY_RIGHT", direction_int);
+            player_context->direction = ENTITY_RIGHT;
             break;
         }
     }
 
     // 12. State (enum PlayerState)
     {
-        int state_int = 0; // Default to PLAYER_IDLE
+        int state_int = 0; // Default to ENTITY_IDLE
         if (!load_number("player/state", &state_int))
         {
-            FURI_LOG_E(TAG, "No data or parse error for state. Defaulting to PLAYER_IDLE");
+            FURI_LOG_E(TAG, "No data or parse error for state. Defaulting to ENTITY_IDLE");
             state_int = 0;
         }
 
         switch (state_int)
         {
         case 0:
-            player_context->state = PLAYER_IDLE;
+            player_context->state = ENTITY_IDLE;
             break;
         case 1:
-            player_context->state = PLAYER_MOVING;
+            player_context->state = ENTITY_MOVING;
             break;
         case 2:
-            player_context->state = PLAYER_ATTACKING;
+            player_context->state = ENTITY_ATTACKING;
             break;
         case 3:
-            player_context->state = PLAYER_ATTACKED;
+            player_context->state = ENTITY_ATTACKED;
             break;
         case 4:
-            player_context->state = PLAYER_DEAD;
+            player_context->state = ENTITY_DEAD;
             break;
         default:
-            FURI_LOG_E(TAG, "Invalid state value: %d. Defaulting to PLAYER_IDLE", state_int);
-            player_context->state = PLAYER_IDLE;
+            FURI_LOG_E(TAG, "Invalid state value: %d. Defaulting to ENTITY_IDLE", state_int);
+            player_context->state = ENTITY_IDLE;
             break;
         }
     }
@@ -895,62 +897,9 @@ static inline void furi_string_remove_str(FuriString *string, const char *needle
     furi_string_replace_str(string, needle, "", 0);
 }
 
-static FuriString *enemy_data(const FuriString *world_data)
-{
-    size_t enemy_data_pos = furi_string_search_str(world_data, "enemy_data", 0);
-    if (enemy_data_pos == FURI_STRING_FAILURE)
-    {
-        FURI_LOG_E("Game", "Failed to find enemy_data in world data");
-
-        return NULL;
-    }
-
-    size_t bracket_start = furi_string_search_char(world_data, '[', enemy_data_pos);
-    if (bracket_start == FURI_STRING_FAILURE)
-    {
-        FURI_LOG_E("Game", "Failed to find start of enemy_data array");
-
-        return NULL;
-    }
-
-    size_t bracket_end = furi_string_search_char(world_data, ']', bracket_start);
-    if (bracket_end == FURI_STRING_FAILURE)
-    {
-        FURI_LOG_E("Game", "Failed to find end of enemy_data array");
-
-        return NULL;
-    }
-
-    FuriString *enemy_data_str = furi_string_alloc();
-    if (!enemy_data_str)
-    {
-        FURI_LOG_E("Game", "Failed to allocate enemy_data string");
-
-        return NULL;
-    }
-
-    furi_string_cat_str(enemy_data_str, "{\"enemy_data\":");
-
-    {
-        FuriString *temp_sub = furi_string_alloc();
-
-        furi_string_set_strn(
-            temp_sub,
-            furi_string_get_cstr(world_data) + bracket_start,
-            (bracket_end + 1) - bracket_start);
-
-        furi_string_cat(enemy_data_str, temp_sub);
-        furi_string_free(temp_sub);
-    }
-
-    furi_string_cat_str(enemy_data_str, "}");
-
-    return enemy_data_str;
-}
-
-static FuriString *json_data(const FuriString *world_data)
+static FuriString *json_data(const FuriString *world_data, const char *key)
 {
-    size_t json_data_pos = furi_string_search_str(world_data, "json_data", 0);
+    size_t json_data_pos = furi_string_search_str(world_data, key, 0);
     if (json_data_pos == FURI_STRING_FAILURE)
     {
         FURI_LOG_E("Game", "Failed to find json_data in world data");
@@ -982,7 +931,9 @@ static FuriString *json_data(const FuriString *world_data)
         return NULL;
     }
 
-    furi_string_cat_str(json_data_str, "{\"json_data\":");
+    furi_string_cat_str(json_data_str, "{\"");
+    furi_string_cat_str(json_data_str, key);
+    furi_string_cat_str(json_data_str, "\":");
 
     {
         FuriString *temp_sub = furi_string_alloc();
@@ -1008,7 +959,7 @@ bool separate_world_data(char *id, FuriString *world_data)
         FURI_LOG_E("Game", "Invalid parameters");
         return false;
     }
-    FuriString *file_json_data = json_data(world_data);
+    FuriString *file_json_data = json_data(world_data, "json_data");
     if (!file_json_data || furi_string_size(file_json_data) == 0)
     {
         FURI_LOG_E("Game", "Failed to get json data in separate_world_data");
@@ -1049,10 +1000,48 @@ bool separate_world_data(char *id, FuriString *world_data)
     furi_string_replace_at(file_json_data, furi_string_size(file_json_data) - 1, 1, "");
     // include the comma at the end of the json_data array
     furi_string_cat_str(file_json_data, ",");
+
     furi_string_remove_str(world_data, furi_string_get_cstr(file_json_data));
     furi_string_free(file_json_data);
 
-    FuriString *file_enemy_data = enemy_data(world_data);
+    // save npc_data to disk
+    FuriString *file_npc_data = json_data(world_data, "npc_data");
+    if (!file_npc_data)
+    {
+        FURI_LOG_E("Game", "Failed to get npc data");
+        return false;
+    }
+
+    snprintf(file_path, sizeof(file_path),
+             STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s/%s_npc_data.json",
+             id, id);
+
+    if (!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS))
+    {
+        FURI_LOG_E("Game", "Failed to open file for writing: %s", file_path);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        furi_string_free(file_npc_data);
+        return false;
+    }
+
+    data_size = furi_string_size(file_npc_data);
+    if (storage_file_write(file, furi_string_get_cstr(file_npc_data), data_size) != data_size)
+    {
+        FURI_LOG_E("Game", "Failed to write npc_data");
+    }
+    storage_file_close(file);
+
+    furi_string_replace_at(file_npc_data, 0, 1, "");
+    furi_string_replace_at(file_npc_data, furi_string_size(file_npc_data) - 1, 1, "");
+    // include the comma at the end of the npc_data array
+    furi_string_cat_str(file_npc_data, ",");
+
+    furi_string_remove_str(world_data, furi_string_get_cstr(file_npc_data));
+    furi_string_free(file_npc_data);
+
+    // Save enemy_data to disk
+    FuriString *file_enemy_data = json_data(world_data, "enemy_data");
     if (!file_enemy_data)
     {
         FURI_LOG_E("Game", "Failed to get enemy data");
@@ -1077,12 +1066,11 @@ bool separate_world_data(char *id, FuriString *world_data)
     {
         FURI_LOG_E("Game", "Failed to write enemy_data");
     }
+    furi_string_free(file_enemy_data);
 
     // Clean up
-    furi_string_free(file_enemy_data);
     storage_file_close(file);
     storage_file_free(file);
     furi_record_close(RECORD_STORAGE);
-
     return true;
 }

Diferenças do arquivo suprimidas por serem muito extensas
+ 11 - 56
game/world.c


+ 1 - 2
game/world.h

@@ -10,7 +10,6 @@
 
 // Maximum number of world objects
 #define MAX_WORLD_OBJECTS 25 // any more than that and we may run out of heap when switching worlds
-
-void draw_town_world(GameManager *manager, Level *level);
+const LevelBehaviour *training_world();
 bool draw_json_world_furi(GameManager *manager, Level *level, const FuriString *json_data);
 FuriString *fetch_world(const char *name);

BIN
sprites/npc_left_funny_15x21px.png


BIN
sprites/npc_right_funny_15x21px.png


Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff