Kaynağa Gözat

Merge flip_world from https://github.com/jblanked/FlipWorld

# Conflicts:
#	flip_world/text_input/uart_text_input.c
Willy-JL 11 ay önce
ebeveyn
işleme
525ae217fb
51 değiştirilmiş dosya ile 1844 ekleme ve 1137 silme
  1. 15 7
      flip_world/README.md
  2. 16 7
      flip_world/alloc/alloc.c
  3. 2 1
      flip_world/alloc/alloc.h
  4. 1 6
      flip_world/app.c
  5. 1 1
      flip_world/application.fam
  6. BIN
      flip_world/assets/01-home.png
  7. BIN
      flip_world/assets/02-town.png
  8. BIN
      flip_world/assets/03-town.png
  9. BIN
      flip_world/assets/06-tree.png
  10. 16 3
      flip_world/assets/CHANGELOG.md
  11. 17 7
      flip_world/assets/README.md
  12. BIN
      flip_world/assets/icon_chest_closed_16x13px.png
  13. BIN
      flip_world/assets/icon_chest_open_16x16px.png
  14. BIN
      flip_world/assets/icon_earth_15x16.png
  15. BIN
      flip_world/assets/icon_home_15x16.png
  16. BIN
      flip_world/assets/icon_house_3d_34x45px.png
  17. BIN
      flip_world/assets/icon_info_15x16.png
  18. BIN
      flip_world/assets/icon_plant_fern_18x16px.png
  19. BIN
      flip_world/assets/icon_plant_pointy_13x16px.png
  20. BIN
      flip_world/assets/icon_title_screen_128x64px.png
  21. BIN
      flip_world/assets/icon_tree_29x30px.png
  22. BIN
      flip_world/assets/icon_tree_48x48px.png
  23. 359 182
      flip_world/callback/callback.c
  24. 17 5
      flip_world/callback/callback.h
  25. 8 0
      flip_world/easy_flipper/easy_flipper.h
  26. BIN
      flip_world/file_assets/sprites/npc_left_funny_15x21px.fxbm
  27. BIN
      flip_world/file_assets/sprites/npc_right_funny_15x21px.fxbm
  28. 1 1
      flip_world/flip_storage/storage.c
  29. 13 6
      flip_world/flip_world.c
  30. 29 13
      flip_world/flip_world.h
  31. 8 0
      flip_world/flipper_http/flipper_http.h
  32. 125 52
      flip_world/game/draw.c
  33. 5 6
      flip_world/game/draw.h
  34. 119 150
      flip_world/game/enemy.c
  35. 1 54
      flip_world/game/enemy.h
  36. 59 29
      flip_world/game/game.c
  37. 21 1
      flip_world/game/game.h
  38. 47 233
      flip_world/game/icon.c
  39. 1 2
      flip_world/game/icon.h
  40. 69 12
      flip_world/game/level.c
  41. 2 1
      flip_world/game/level.h
  42. 436 0
      flip_world/game/npc.c
  43. 5 0
      flip_world/game/npc.h
  44. 285 168
      flip_world/game/player.c
  45. 51 20
      flip_world/game/player.h
  46. 88 100
      flip_world/game/storage.c
  47. 14 63
      flip_world/game/world.c
  48. 5 7
      flip_world/game/world.h
  49. 8 0
      flip_world/jsmn/jsmn_h.h
  50. BIN
      flip_world/sprites/npc_left_funny_15x21px.png
  51. BIN
      flip_world/sprites/npc_right_funny_15x21px.png

+ 15 - 7
flip_world/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
@@ -15,7 +13,7 @@ FlipWorld and FlipSocial are connected. Your login information is the same in bo
 
 - **WiFi**: Enter your SSID and password to connect to your 2.4 GHz network.
 - **User**: Add or update your username and password (this is the same login information as your FlipSocial account).
-- **Game**: Install the Official World Pack, set your FPS (30, 60, 120, or 240), and select whether you want the screen backlight to always be on, the sound to be on, and the vibration to be on.
+- **Game**: Install the Official World Pack, choose your weapon, set your FPS (30, 60, 120, or 240), and select whether you want the screen backlight to always be on, the sound to be on, and the vibration to be on.
 
 **Controls**
 
@@ -23,7 +21,10 @@ 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/Hold 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.
 
 **Player Attributes**
 
@@ -46,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
 
@@ -67,12 +71,16 @@ An enemy attack registers if the enemy is facing you and collides with you. Howe
 
 **v0.4**
 - New game features
+- Stability patch
+- World expansion
 
 **v0.5**
-- ???
+- Stability patch
+- NPCs
 
 **v0.6**
-- ???
+- New game features
+- Custom Controller Support
 
 **v0.7**
 - ???

+ 16 - 7
flip_world/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);
 
@@ -24,13 +34,13 @@ FlipWorldApp *flip_world_app_alloc()
     {
         return NULL;
     }
-    view_dispatcher_set_custom_event_callback(app->view_dispatcher, flip_world_custom_event_callback);
+    view_dispatcher_set_custom_event_callback(app->view_dispatcher, custom_event_callback);
     // Main view
-    if (!easy_flipper_set_view(&app->view_loader, FlipWorldViewLoader, flip_world_loader_draw_callback, NULL, callback_to_submenu, &app->view_dispatcher, app))
+    if (!easy_flipper_set_view(&app->view_loader, FlipWorldViewLoader, loader_draw_callback, NULL, callback_to_submenu, &app->view_dispatcher, app))
     {
         return NULL;
     }
-    flip_world_loader_init(app->view_loader);
+    loader_init(app->view_loader);
     if (!easy_flipper_set_widget(&app->widget_result, FlipWorldViewWidgetResult, "", callback_to_submenu, &app->view_dispatcher))
     {
         return NULL;
@@ -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);
     //
 
@@ -78,7 +88,7 @@ void flip_world_app_free(FlipWorldApp *app)
     if (app->view_loader)
     {
         view_dispatcher_remove_view(app->view_dispatcher, FlipWorldViewLoader);
-        flip_world_loader_free_model(app->view_loader);
+        loader_free_model(app->view_loader);
         view_free(app->view_loader);
     }
 
@@ -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
flip_world/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
flip_world/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
flip_world/application.fam

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

BIN
flip_world/assets/01-home.png


BIN
flip_world/assets/02-town.png


BIN
flip_world/assets/03-town.png


BIN
flip_world/assets/06-tree.png


+ 16 - 3
flip_world/assets/CHANGELOG.md

@@ -1,10 +1,23 @@
-**0.3 (2025-01-14)**
+## 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).
+- Added option to choose player weapon in the Game Settings.
+- Added transition icon for switching worlds.
+- Doubled the size of each world (from 384x192 to 768x384).
+- Improved memory allocation.
+
+## 0.3 (2025-01-14)
 - Added new worlds.
 - Improved memory allocation.
 - Updated API integration to load and save player attributes.
 - Upgraded FlipperHTTP to the latest version.
 
-**0.2 (2025-01-02)**
+## 0.2 (2025-01-02)
 - Added support for the Video Game Module (requires a FlipperHTTP flash).
 - Introduced various enemy types to enhance gameplay.
 - Added features for player health, XP, level, health regeneration, attack, and strength.
@@ -16,5 +29,5 @@
 - Improved collision mechanics for more accurate interactions.
 - Updated the default icon representing the player's character.
 
-**0.1 (2024-12-21)**
+## 0.1 (2024-12-21)
 - Initial release.

+ 17 - 7
flip_world/assets/README.md

@@ -1,7 +1,6 @@
-The first open-world multiplayer game for the Flipper Zero, best played with the VGM.
+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
 
@@ -13,7 +12,7 @@ FlipWorld and FlipSocial are connected. Your login information is the same in bo
 
 - **WiFi**: Enter your SSID and password to connect to your 2.4 GHz network.
 - **User**: Add or update your username and password (this is the same login information as your FlipSocial account).
-- **Game**: Install the Official World Pack, set your FPS (30, 60, 120, or 240), and select whether you want the screen backlight to always be on, the sound to be on, and the vibration to be on.
+- **Game**: Install the Official World Pack, choose your weapon, set your FPS (30, 60, 120, or 240), and select whether you want the screen backlight to always be on, the sound to be on, and the vibration to be on.
 
 **Controls**
 
@@ -21,7 +20,10 @@ 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/Hold 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.
 
 **Player Attributes**
 
@@ -42,7 +44,11 @@ Enemies have similar attributes to players but do not have XP or health regenera
 
 If an enemy attacks you, your health decreases by the enemy's strength (attack power). Additionally, if an enemy defeats you, your XP decreases by the enemy's strength. Conversely, when you successfully attack an enemy, you gain health equal to 10% of the enemy's strength and increase your XP by the enemy's full strength.
 
-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`.
+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
 
@@ -64,12 +70,16 @@ An enemy attack registers if the enemy is facing you and collides with you. Howe
 
 **v0.4**
 - New game features
+- Stability patch
+- World expansion
 
 **v0.5**
-- ???
+- Stability patch
+- NPCs
 
 **v0.6**
-- ???
+- New game features
+- Custom Controller Support
 
 **v0.7**
 - ???

BIN
flip_world/assets/icon_chest_closed_16x13px.png


BIN
flip_world/assets/icon_chest_open_16x16px.png


BIN
flip_world/assets/icon_earth_15x16.png


BIN
flip_world/assets/icon_home_15x16.png


BIN
flip_world/assets/icon_house_3d_34x45px.png


BIN
flip_world/assets/icon_info_15x16.png


BIN
flip_world/assets/icon_plant_fern_18x16px.png


BIN
flip_world/assets/icon_plant_pointy_13x16px.png


BIN
flip_world/assets/icon_title_screen_128x64px.png


BIN
flip_world/assets/icon_tree_29x30px.png


BIN
flip_world/assets/icon_tree_48x48px.png


Dosya farkı çok büyük olduğundan ihmal edildi
+ 359 - 182
flip_world/callback/callback.c


+ 17 - 5
flip_world/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);
@@ -35,11 +47,11 @@ struct DataLoaderModel
     FuriTimer *timer;
     FlipperHTTP *fhttp;
 };
-void flip_world_generic_switch_to_view(FlipWorldApp *app, char *title, DataLoaderFetch fetcher, DataLoaderParser parser, size_t request_count, ViewNavigationCallback back, uint32_t view_id);
+void generic_switch_to_view(FlipWorldApp *app, char *title, DataLoaderFetch fetcher, DataLoaderParser parser, size_t request_count, ViewNavigationCallback back, uint32_t view_id);
 
-void flip_world_loader_draw_callback(Canvas *canvas, void *model);
+void loader_draw_callback(Canvas *canvas, void *model);
 
-void flip_world_loader_init(View *view);
+void loader_init(View *view);
 
-void flip_world_loader_free_model(View *view);
-bool flip_world_custom_event_callback(void *context, uint32_t index);
+void loader_free_model(View *view);
+bool custom_event_callback(void *context, uint32_t index);

+ 8 - 0
flip_world/easy_flipper/easy_flipper.h

@@ -25,6 +25,14 @@
 #include <jsmn/jsmn_furi.h>
 #include <jsmn/jsmn.h>
 
+// added by Derek Jamison to lower memory usage
+#undef FURI_LOG_E
+#define FURI_LOG_E(tag, msg, ...)
+
+#undef FURI_LOG_I
+#define FURI_LOG_I(tag, msg, ...)
+//
+
 #define EASY_TAG "EasyFlipper"
 
 void easy_flipper_dialog(

BIN
flip_world/file_assets/sprites/npc_left_funny_15x21px.fxbm


BIN
flip_world/file_assets/sprites/npc_right_funny_15x21px.fxbm


+ 1 - 1
flip_world/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

+ 13 - 6
flip_world/flip_world.c

@@ -1,9 +1,16 @@
 #include <flip_world.h>
-char *game_fps_choices[] = {"30", "60", "120", "240"};
-const float game_fps_choices_2[] = {30.0, 60.0, 120.0, 240.0};
-int game_fps_index = 0;
+char *fps_choices_str[] = {"30", "60", "120", "240"};
+int fps_index = 0;
 char *yes_or_no_choices[] = {"No", "Yes"};
-int game_screen_always_on_index = 1;
-int game_sound_on_index = 0;
-int game_vibration_on_index = 0;
+int screen_always_on_index = 1;
+int sound_on_index = 0;
+int vibration_on_index = 0;
+char *player_sprite_choices[] = {"naked", "sword", "axe", "bow"};
+int player_sprite_index = 1;
+char *vgm_levels[] = {"-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"};
+int vgm_x_index = 2;
+int vgm_y_index = 2;
+float atof_(const char *nptr) { return (float)strtod(nptr, NULL); }
+float atof_furi(const FuriString *nptr) { return atof_(furi_string_get_cstr(nptr)); }
+bool is_str(const char *src, const char *dst) { return strcmp(src, dst) == 0; }
 bool is_enough_heap(size_t heap_size) { return memmgr_get_free_heap() > (heap_size + 1024); } // 1KB buffer

+ 29 - 13
flip_world/flip_world.h

@@ -1,22 +1,28 @@
 #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
 #define FURI_LOG_E(tag, msg, ...)
+
+#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.3
-#define VERSION_TAG "FlipWorld v0.3"
+#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,
@@ -27,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
@@ -50,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)
@@ -62,6 +68,9 @@ typedef struct
     VariableItem *variable_item_game_download_world;   // The variable item for Download world
     VariableItem *variable_item_game_sound_on;         // The variable item for Sound on
     VariableItem *variable_item_game_vibration_on;     // The variable item for Vibration on
+    VariableItem *variable_item_game_player_sprite;    // The variable item for Player sprite
+    VariableItem *variable_item_game_vgm_x;            // The variable item for VGM X
+    VariableItem *variable_item_game_vgm_y;            // The variable item for VGM Y
     //
     VariableItem *variable_item_user_username; // The variable item for the User username
     VariableItem *variable_item_user_password; // The variable item for the User password
@@ -72,11 +81,18 @@ typedef struct
     uint32_t text_input_buffer_size; // Size of the text input buffer
 } FlipWorldApp;
 
-extern char *game_fps_choices[];
-extern const float game_fps_choices_2[];
-extern int game_fps_index;
+extern char *fps_choices_str[];
+extern int fps_index;
 extern char *yes_or_no_choices[];
-extern int game_screen_always_on_index;
-extern int game_sound_on_index;
-extern int game_vibration_on_index;
+extern int screen_always_on_index;
+extern int sound_on_index;
+extern int vibration_on_index;
+extern char *player_sprite_choices[];
+extern int player_sprite_index;
+extern char *vgm_levels[];
+extern int vgm_x_index;
+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);

+ 8 - 0
flip_world/flipper_http/flipper_http.h

@@ -15,6 +15,14 @@
 #include <storage/storage.h>
 #include <momentum/settings.h>
 
+// added by Derek Jamison to lower memory usage
+#undef FURI_LOG_E
+#define FURI_LOG_E(tag, msg, ...)
+
+#undef FURI_LOG_I
+#define FURI_LOG_I(tag, msg, ...)
+//
+
 // STORAGE_EXT_PATH_PREFIX is defined in the Furi SDK as /ext
 
 #define HTTP_TAG "FlipWorld"              // change this to your app name

+ 125 - 52
flip_world/game/draw.c

@@ -4,24 +4,6 @@
 int camera_x = 0;
 int camera_y = 0;
 
-// Background rendering function (no collision detection)
-void draw_background(Canvas *canvas, Vector pos)
-{
-    // Clear the canvas
-    canvas_clear(canvas);
-
-    // Calculate camera offset to center the player
-    camera_x = pos.x - (SCREEN_WIDTH / 2);
-    camera_y = pos.y - (SCREEN_HEIGHT / 2);
-
-    // Clamp camera position to prevent showing areas outside the world
-    camera_x = CLAMP(camera_x, WORLD_WIDTH - SCREEN_WIDTH, 0);
-    camera_y = CLAMP(camera_y, WORLD_HEIGHT - SCREEN_HEIGHT, 0);
-
-    // Draw the outer bounds adjusted by camera offset
-    draw_bounds(canvas);
-}
-
 // Draw the user stats (health, xp, and level)
 void draw_user_stats(Canvas *canvas, Vector pos, GameManager *manager)
 {
@@ -38,9 +20,13 @@ void draw_user_stats(Canvas *canvas, Vector pos, GameManager *manager)
     char level[32];
 
     snprintf(health, sizeof(health), "HP : %ld", player->health);
-    snprintf(xp, sizeof(xp), "XP : %ld", player->xp);
     snprintf(level, sizeof(level), "LVL: %ld", player->level);
 
+    if (player->xp < 10000)
+        snprintf(xp, sizeof(xp), "XP : %ld", player->xp);
+    else
+        snprintf(xp, sizeof(xp), "XP : %ldK", player->xp / 1000);
+
     canvas_set_font_custom(canvas, FONT_SIZE_SMALL);
     canvas_draw_str(canvas, pos.x, pos.y, health);
     canvas_draw_str(canvas, pos.x, pos.y + 7, xp);
@@ -60,65 +46,152 @@ 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)
+char g_name[32];
+// Draw an icon at a specific position (with collision detection)
+void spawn_icon(GameManager *manager, Level *level, const char *icon_id, float x, float y)
+{
+    snprintf(g_name, sizeof(g_name), "%s", icon_id);
+    Entity *e = level_add_entity(level, &icon_desc);
+    entity_pos_set(e, (Vector){x, y});
+    GameContext *game_context = game_manager_game_context_get(manager);
+    game_context->icon_count++;
+}
+// Draw a line of icons at a specific position (with collision detection)
+void spawn_icon_line(GameManager *manager, Level *level, const char *icon_id, float x, float y, uint8_t amount, bool horizontal, uint8_t spacing)
 {
     for (int i = 0; i < amount; i++)
     {
         if (horizontal)
         {
             // check if element is outside the world
-            if (pos.x + (i * 17) > WORLD_WIDTH)
+            if (x + (i * spacing) > WORLD_WIDTH)
             {
                 break;
             }
 
-            canvas_draw_icon(canvas, pos.x + (i * 17) - camera_x, pos.y - camera_y, icon);
+            spawn_icon(manager, level, icon_id, x + (i * spacing), y);
         }
         else
         {
             // check if element is outside the world
-            if (pos.y + (i * 17) > WORLD_HEIGHT)
+            if (y + (i * spacing) > WORLD_HEIGHT)
             {
                 break;
             }
 
-            canvas_draw_icon(canvas, pos.x - camera_x, pos.y + (i * 17) - camera_y, icon);
+            spawn_icon(manager, level, icon_id, x, y + (i * spacing));
         }
     }
 }
-char g_temp_spawn_name[32];
-// Draw an icon at a specific position (with collision detection)
-void spawn_icon(Level *level, const char *icon_id, float x, float y)
+
+static void draw_menu(GameManager *manager, Canvas *canvas)
 {
-    snprintf(g_temp_spawn_name, sizeof(g_temp_spawn_name), "%s", icon_id);
-    Entity *e = level_add_entity(level, &icon_desc);
-    entity_pos_set(e, (Vector){x, y});
+    GameContext *game_context = game_manager_game_context_get(manager);
+
+    // draw background rectangle
+    canvas_draw_icon(
+        canvas,
+        0,
+        0,
+        &I_icon_menu_128x64px);
+
+    // draw menu options
+    switch (game_context->menu_screen)
+    {
+    case GAME_MENU_INFO:
+        // draw info
+        // first option is highlighted
+        char health[32];
+        char xp[32];
+        char level[32];
+        char strength[32];
+
+        snprintf(level, sizeof(level), "Level   : %ld", game_context->player_context->level);
+        snprintf(health, sizeof(health), "Health  : %ld", game_context->player_context->health);
+        snprintf(xp, sizeof(xp), "XP      : %ld", game_context->player_context->xp);
+        snprintf(strength, sizeof(strength), "Strength: %ld", game_context->player_context->strength);
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str(canvas, 7, 16, game_context->player_context->username);
+        canvas_set_font_custom(canvas, FONT_SIZE_SMALL);
+        canvas_draw_str(canvas, 7, 30, level);
+        canvas_draw_str(canvas, 7, 37, health);
+        canvas_draw_str(canvas, 7, 44, xp);
+        canvas_draw_str(canvas, 7, 51, strength);
+
+        // draw a box around the selected option
+        canvas_draw_frame(canvas, 80, 18, 36, 30);
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str(canvas, 86, 30, "Info");
+        canvas_set_font(canvas, FontSecondary);
+        canvas_draw_str(canvas, 86, 42, "More");
+        break;
+    case GAME_MENU_MORE:
+        // draw settings
+        switch (game_context->menu_selection)
+        {
+        case 0:
+            // first option is highlighted
+            break;
+        case 1:
+            // second option is highlighted
+            break;
+        default:
+            break;
+        }
+
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str(canvas, 7, 16, VERSION_TAG);
+        canvas_set_font_custom(canvas, FONT_SIZE_SMALL);
+        canvas_draw_str_multi(canvas, 7, 25, "Developed by\nJBlanked and Derek \nJamison. Graphics\nfrom Pr3!\n\nwww.github.com/jblanked");
+
+        // draw a box around the selected option
+        canvas_draw_frame(canvas, 80, 18, 36, 30);
+        canvas_set_font(canvas, FontSecondary);
+        canvas_draw_str(canvas, 86, 30, "Info");
+        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;
+    }
 }
-// Draw a line of icons at a specific position (with collision detection)
-void spawn_icon_line(Level *level, const char *icon_id, float x, float y, uint8_t amount, bool horizontal)
+
+void background_render(Canvas *canvas, GameManager *manager)
 {
-    for (int i = 0; i < amount; i++)
+    if (!canvas || !manager)
+        return;
+
+    GameContext *game_context = game_manager_game_context_get(manager);
+    if (!game_context->is_menu_open)
     {
-        if (horizontal)
-        {
-            // check if element is outside the world
-            if (x + (i * 17) > WORLD_WIDTH)
-            {
-                break;
-            }
 
-            spawn_icon(level, icon_id, x + (i * 17), y);
-        }
-        else
-        {
-            // check if element is outside the world
-            if (y + (i * 17) > WORLD_HEIGHT)
-            {
-                break;
-            }
+        // 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);
 
-            spawn_icon(level, icon_id, x, y + (i * 17));
+        // draw switch world icon
+        if (game_context->is_switching_level)
+        {
+            canvas_draw_icon(
+                canvas,
+                0,
+                0,
+                &I_icon_world_change_128x64px);
         }
+
+        // Draw user stats
+        draw_user_stats(canvas, (Vector){0, 50}, manager);
+    }
+    else
+    {
+        // draw menu
+        draw_menu(manager, canvas);
     }
-}
+};

+ 5 - 6
flip_world/game/draw.h

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

+ 119 - 150
flip_world/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,32 +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)
-    {
-        FURI_LOG_E("Game", "Enemy generic free: Invalid context");
-        return;
-    }
-    free(context);
-    context = NULL;
-    if (enemy_context_generic)
-    {
-        free(enemy_context_generic);
-        enemy_context_generic = NULL;
-    }
-}
-
 // Enemy start function
 static void enemy_start(Entity *self, GameManager *manager, void *context)
 {
@@ -79,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;
@@ -111,15 +94,22 @@ static void enemy_render(Entity *self, GameManager *manager, Canvas *canvas, voi
     if (!self || !context || !canvas || !manager)
         return;
 
-    EnemyContext *enemy_context = (EnemyContext *)context;
-    GameContext *game_context = game_manager_game_context_get(manager);
+    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;
     }
@@ -135,20 +125,13 @@ static void enemy_render(Entity *self, GameManager *manager, Canvas *canvas, voi
         pos.x - camera_x - (enemy_context->size.x / 2),
         pos.y - camera_y - (enemy_context->size.y / 2));
 
-    // instead of username, draw health
+    // draw health of enemy
     char health_str[32];
     snprintf(health_str, sizeof(health_str), "%.0f", (double)enemy_context->health);
     draw_username(canvas, pos, health_str);
-
-    // Draw user stats (this has to be done for all enemies)
-    draw_user_stats(canvas, (Vector){0, 50}, manager);
-
-    // draw player username from GameContext
-    Vector posi = entity_pos_get(game_context->player);
-    draw_username(canvas, posi, game_context->player_context->username);
 }
 
-static void send_attack_notification(GameContext *game_context, EnemyContext *enemy_context, bool player_attacked)
+static void atk_notify(GameContext *game_context, EntityContext *enemy_context, bool player_attacked)
 {
     if (!game_context || !enemy_context)
     {
@@ -158,8 +141,8 @@ static void send_attack_notification(GameContext *game_context, EnemyContext *en
 
     NotificationApp *notifications = furi_record_open(RECORD_NOTIFICATION);
 
-    const bool vibration_allowed = strstr(yes_or_no_choices[game_vibration_on_index], "Yes") != NULL;
-    const bool sound_allowed = strstr(yes_or_no_choices[game_sound_on_index], "Yes") != NULL;
+    const bool vibration_allowed = strstr(yes_or_no_choices[vibration_on_index], "Yes") != NULL;
+    const bool sound_allowed = strstr(yes_or_no_choices[sound_on_index], "Yes") != NULL;
 
     if (player_attacked)
     {
@@ -217,23 +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);
-        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);
@@ -244,29 +217,32 @@ 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;
         }
 
         // Handle Player Attacking Enemy (Press OK, facing enemy, and enemy not facing player)
-        if (player_is_facing_enemy && game_context->user_input == GameKeyOk && !enemy_is_facing_player)
+        if (player_is_facing_enemy && game_context->last_button == GameKeyOk && !enemy_is_facing_player)
         {
+            // Reset last button
+            game_context->last_button = -1;
+
             if (game_context->player_context->elapsed_attack_timer >= game_context->player_context->attack_timer)
             {
-                send_attack_notification(game_context, enemy_context, true);
+                atk_notify(game_context, enemy_context, true);
 
                 // Reset player's elapsed attack timer
                 game_context->player_context->elapsed_attack_timer = 0.0f;
@@ -288,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
@@ -303,11 +279,11 @@ 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_pos.x -= game_context->player_context->dx * enemy_context->radius;
-                    enemy_pos.y -= game_context->player_context->dy * enemy_context->radius;
+                    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);
 
                     // Reset enemy's movement direction to prevent immediate re-collision
@@ -325,7 +301,7 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
         {
             if (enemy_context->elapsed_attack_timer >= enemy_context->attack_timer)
             {
-                send_attack_notification(game_context, enemy_context, false);
+                atk_notify(game_context, enemy_context, false);
 
                 // Reset enemy's elapsed attack timer
                 enemy_context->elapsed_attack_timer = 0.0f;
@@ -336,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);
@@ -352,11 +328,11 @@ 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;
-                    player_pos.y -= game_context->player_context->dy * enemy_context->radius;
+                    player_pos.x -= game_context->player_context->dx * enemy_context->radius + game_context->icon_offset;
+                    player_pos.y -= game_context->player_context->dy * enemy_context->radius + game_context->icon_offset;
                     entity_pos_set(other, player_pos);
 
                     // Reset player's movement direction to prevent immediate re-collision
@@ -364,52 +340,50 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
                     game_context->player_context->dy = 0;
                 }
             }
-            else
-            {
-                FURI_LOG_I("Game", "Enemy '%s' attack on player is on cooldown: %f seconds remaining", enemy_context->id, (double)(enemy_context->attack_timer - enemy_context->elapsed_attack_timer));
-            }
         }
         else // handle other collisions
         {
-            // bounce player and enemy away from each other
-            Vector player_pos = entity_pos_get(other);
-            Vector enemy_pos = entity_pos_get(self);
-
-            // Calculate the direction vector from player to enemy
-            Vector direction_vector = {
-                enemy_pos.x - player_pos.x,
-                enemy_pos.y - player_pos.y};
-
-            // Normalize the direction vector
-            float length = sqrt(direction_vector.x * direction_vector.x + direction_vector.y * direction_vector.y);
-            if (length != 0)
-            {
-                direction_vector.x /= length;
-                direction_vector.y /= length;
-            }
-
-            // Move the player and enemy away from each other
-            player_pos.y -= direction_vector.y * 3;
-            entity_pos_set(other, player_pos);
-
-            enemy_pos.x += direction_vector.x * 3;
-            entity_pos_set(self, enemy_pos);
-
+            // 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;
         }
 
         // 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 = 100;
+            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);
     }
 }
 
@@ -419,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;
     }
@@ -439,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;
 
@@ -451,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);
@@ -475,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
@@ -529,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;
         }
     }
@@ -545,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
@@ -557,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
@@ -580,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,
@@ -594,54 +572,45 @@ 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)
+    if (!level || !manager || !json)
     {
-        FURI_LOG_E("Game", "Level is NULL");
+        FURI_LOG_E("Game", "Level, GameManager, or JSON is NULL");
         return;
     }
-    if (!json)
-    {
-        FURI_LOG_E("Game", "JSON is NULL");
-        return;
-    }
-    if (!manager)
-    {
-        FURI_LOG_E("Game", "GameManager is NULL");
-        return;
-    }
-    // parameters: id, index, size.x, size.y, start_position.x, start_position.y, end_position.x, end_position.y, move_timer, speed, attack_timer, strength, health
+
     FuriString *id = get_json_value_furi("id", json);
     FuriString *_index = get_json_value_furi("index", json);
     //
@@ -673,13 +642,13 @@ void spawn_enemy_json_furi(Level *level, GameManager *manager, FuriString *json)
                                                                                        manager,
                                                                                        furi_string_get_cstr(id),
                                                                                        atoi(furi_string_get_cstr(_index)),
-                                                                                       (Vector){strtod(furi_string_get_cstr(start_position_x), NULL), strtod(furi_string_get_cstr(start_position_y), NULL)},
-                                                                                       (Vector){strtod(furi_string_get_cstr(end_position_x), NULL), strtod(furi_string_get_cstr(end_position_y), NULL)},
-                                                                                       strtod(furi_string_get_cstr(move_timer), NULL),
-                                                                                       strtod(furi_string_get_cstr(speed), NULL),
-                                                                                       strtod(furi_string_get_cstr(attack_timer), NULL),
-                                                                                       strtod(furi_string_get_cstr(strength), NULL),
-                                                                                       strtod(furi_string_get_cstr(health), NULL)));
+                                                                                       (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),
+                                                                                       atof_furi(attack_timer),
+                                                                                       atof_furi(strength),
+                                                                                       atof_furi(health)));
         game_context->enemy_count++;
     }
 

+ 1 - 54
flip_world/game/enemy.h

@@ -2,57 +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;
-
-const EntityDescription *enemy(
-    GameManager *manager,
-    const char *id,
-    int index,
-    Vector start_position,
-    Vector end_position,
-    float move_timer, // Wait duration before moving again
-    float speed,
-    float attack_timer,
-    float strength,
-    float health);
-
-void spawn_enemy_json_furi(Level *level, GameManager *manager, FuriString *json);
+void spawn_enemy(Level *level, GameManager *manager, FuriString *json);

+ 59 - 29
flip_world/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 ******/
 /*
@@ -11,17 +13,25 @@ static void game_start(GameManager *game_manager, void *ctx)
     // Do some initialization here, for example you can load score from storage.
     // For simplicity, we will just set it to 0.
     GameContext *game_context = ctx;
-    game_context->fps = game_fps_choices_2[game_fps_index];
+    game_context->fps = atof_(fps_choices_str[fps_index]);
     game_context->player_context = NULL;
-    game_context->current_level = 0;
     game_context->ended_early = false;
+    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++)
@@ -30,15 +40,13 @@ static void game_start(GameManager *game_manager, void *ctx)
         {
             if (i == 0)
             {
-                FURI_LOG_E("Game", "Failed to allocate level %d, loading default level", i);
-                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;
             }
-            FURI_LOG_E("Game", "No more levels to load");
             break;
         }
-        game_context->level_count++;
+        else
+            game_context->level_count++;
     }
 
     // imu
@@ -46,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.
@@ -53,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 Flip World!\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
flip_world/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>

+ 47 - 233
flip_world/game/icon.c

@@ -4,23 +4,14 @@ static void icon_collision(Entity *self, Entity *other, GameManager *manager, vo
 {
     UNUSED(manager);
     UNUSED(self);
-    IconContext *icon_ctx = (IconContext *)context;
-    if (!icon_ctx)
-    {
-        FURI_LOG_E("Game", "Icon context is NULL");
-        return;
-    }
-
-    if (entity_description_get(other) == &player_desc)
+    IconContext *ictx = (IconContext *)context;
+    if (ictx && entity_description_get(other) == &player_desc)
     {
         PlayerContext *player = (PlayerContext *)entity_context_get(other);
         if (player)
         {
-            Vector pos = entity_pos_get(other);
-            // Bounce back by 2
-            pos.x -= player->dx * 2;
-            pos.y -= player->dy * 2;
-            entity_pos_set(other, pos);
+            // Set the player's old position to prevent collision
+            entity_pos_set(other, player->old_position);
 
             // Reset movement to prevent re-collision
             player->dx = 0;
@@ -32,61 +23,54 @@ static void icon_collision(Entity *self, Entity *other, GameManager *manager, vo
 static void icon_render(Entity *self, GameManager *manager, Canvas *canvas, void *context)
 {
     UNUSED(manager);
-    IconContext *icon_ctx = (IconContext *)context;
-    if (!icon_ctx)
-    {
-        FURI_LOG_E("Game", "Icon context is NULL");
-        return;
-    }
+    IconContext *ictx = (IconContext *)context;
+    furi_check(ictx, "Icon context is NULL");
     Vector pos = entity_pos_get(self);
-
-    // Draw the icon, centered
-    canvas_draw_icon(
-        canvas,
-        pos.x - camera_x - icon_ctx->width / 2,
-        pos.y - camera_y - icon_ctx->height / 2,
-        icon_ctx->icon);
+    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)
 {
     UNUSED(manager);
 
-    IconContext *icon_ctx_self = (IconContext *)context;
-    if (!icon_ctx_self)
+    IconContext *ictx_self = (IconContext *)context;
+    if (!ictx_self)
     {
         FURI_LOG_E("Game", "Icon context self is NULL");
         return;
     }
-    IconContext *icon_ctx = entity_context_get(self);
-    if (!icon_ctx)
+    IconContext *ictx = entity_context_get(self);
+    if (!ictx)
     {
         FURI_LOG_E("Game", "Icon context is NULL");
         return;
     }
 
-    IconContext *loaded_data = get_icon_context(g_temp_spawn_name);
+    IconContext *loaded_data = get_icon_context(g_name);
     if (!loaded_data)
     {
-        FURI_LOG_E("Game", "Failed to find icon data for %s", g_temp_spawn_name);
+        FURI_LOG_E("Game", "Failed to find icon data for %s", g_name);
         return;
     }
 
-    icon_ctx_self->icon = loaded_data->icon;
-    icon_ctx_self->width = loaded_data->width;
-    icon_ctx_self->height = loaded_data->height;
-    icon_ctx->icon = loaded_data->icon;
-    icon_ctx->width = loaded_data->width;
-    icon_ctx->height = loaded_data->height;
+    ictx_self->icon = loaded_data->icon;
+    ictx_self->size = (Vector){loaded_data->size.x, loaded_data->size.y};
+    ictx->icon = loaded_data->icon;
+    ictx->size = (Vector){loaded_data->size.x, loaded_data->size.y};
 
     Vector pos = entity_pos_get(self);
-    pos.x += icon_ctx_self->width / 2;
-    pos.y += icon_ctx_self->height / 2;
+    pos.x += ictx_self->size.x / 2;
+    pos.y += ictx_self->size.y / 2;
     entity_pos_set(self, pos);
 
     entity_collider_add_circle(
         self,
-        (icon_ctx_self->width + icon_ctx_self->height) / 4);
+        (ictx_self->size.x + ictx_self->size.y) / 4);
 
     free(loaded_data);
 }
@@ -97,10 +81,6 @@ static void icon_free(Entity *self, GameManager *manager, void *context)
     UNUSED(self);
     UNUSED(manager);
     UNUSED(context);
-    if (context)
-    {
-        free(context);
-    }
 }
 
 // -------------- Entity description --------------
@@ -124,138 +104,54 @@ static IconContext *icon_generic_alloc(const char *id, const Icon *icon, uint8_t
     }
     snprintf(ctx->id, sizeof(ctx->id), "%s", id);
     ctx->icon = icon;
-    ctx->width = width;
-    ctx->height = height;
+    ctx->size = (Vector){width, height};
     return ctx;
 }
 
 IconContext *get_icon_context(const char *name)
 {
-    // if (strcmp(name, "earth") == 0)
-    // {
-    //     return icon_generic_alloc("earth", &I_icon_earth_15x16, 15, 16);
-    // }
-    // else if (strcmp(name, "home") == 0)
-    // {
-    //     return icon_generic_alloc("home", &I_icon_home_15x16, 15, 16);
-    // }
-    if (strcmp(name, "house") == 0)
-    {
+    if (is_str(name, "house"))
         return icon_generic_alloc("house", &I_icon_house_48x32px, 48, 32);
-    }
-    // else if (strcmp(name, "house_3d") == 0)
-    // {
-    //     return icon_generic_alloc("house_3d", &I_icon_house_3d_34x45px, 34, 45);
-    // }
-    // else if (strcmp(name, "info") == 0)
-    // {
-    //     return icon_generic_alloc("info", &I_icon_info_15x16, 15, 16);
-    // }
-    else if (strcmp(name, "man") == 0)
-    {
+    else if (is_str(name, "man"))
         return icon_generic_alloc("man", &I_icon_man_7x16, 7, 16);
-    }
-    else if (strcmp(name, "plant") == 0)
-    {
+    else if (is_str(name, "plant"))
         return icon_generic_alloc("plant", &I_icon_plant_16x16, 16, 16);
-    }
-    // else if (strcmp(name, "plant_fern") == 0)
-    // {
-    //     return icon_generic_alloc("plant_fern", &I_icon_plant_fern_18x16px, 18, 16);
-    // }
-    // else if (strcmp(name, "plant_pointy") == 0)
-    // {
-    //     return icon_generic_alloc("plant_pointy", &I_icon_plant_pointy_13x16px, 13, 16);
-    // }
-    else if (strcmp(name, "tree") == 0)
-    {
+    else if (is_str(name, "tree"))
         return icon_generic_alloc("tree", &I_icon_tree_16x16, 16, 16);
-    }
-    // else if (strcmp(name, "tree_29x30") == 0)
-    // {
-    //     return icon_generic_alloc("tree_29x30", &I_icon_tree_29x30px, 29, 30);
-    // }
-    // else if (strcmp(name, "tree_48x48") == 0)
-    // {
-    //     return icon_generic_alloc("tree_48x48", &I_icon_tree_48x48px, 48, 48);
-    // }
-    else if (strcmp(name, "woman") == 0)
-    {
+    else if (is_str(name, "woman"))
         return icon_generic_alloc("woman", &I_icon_woman_9x16, 9, 16);
-    }
-    // else if (strcmp(name, "chest_closed") == 0)
-    // {
-    //     return icon_generic_alloc("chest_closed", &I_icon_chest_closed_16x13px, 16, 13);
-    // }
-    // else if (strcmp(name, "chest_open") == 0)
-    // {
-    //     return icon_generic_alloc("chest_open", &I_icon_chest_open_16x16px, 16, 16);
-    // }
-    else if (strcmp(name, "fence") == 0)
-    {
+    else if (is_str(name, "fence"))
         return icon_generic_alloc("fence", &I_icon_fence_16x8px, 16, 8);
-    }
-    else if (strcmp(name, "fence_end") == 0)
-    {
+    else if (is_str(name, "fence_end"))
         return icon_generic_alloc("fence_end", &I_icon_fence_end_16x8px, 16, 8);
-    }
-    // else if (strcmp(name, "fence_vertical_end") == 0)
-    // {
+    // 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 (strcmp(name, "fence_vertical_start") == 0)
-    // {
+    // 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 (strcmp(name, "flower") == 0)
-    {
+    else if (is_str(name, "flower"))
         return icon_generic_alloc("flower", &I_icon_flower_16x16, 16, 16);
-    }
-    else if (strcmp(name, "lake_bottom") == 0)
-    {
+    else if (is_str(name, "lake_bottom"))
         return icon_generic_alloc("lake_bottom", &I_icon_lake_bottom_31x12px, 31, 12);
-    }
-    else if (strcmp(name, "lake_bottom_left") == 0)
-    {
+    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 (strcmp(name, "lake_bottom_right") == 0)
-    {
+    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 (strcmp(name, "lake_left") == 0)
-    {
+    else if (is_str(name, "lake_left"))
         return icon_generic_alloc("lake_left", &I_icon_lake_left_11x31px, 11, 31);
-    }
-    else if (strcmp(name, "lake_right") == 0)
-    {
-        // Assuming 11x31
+    else if (is_str(name, "lake_right"))
         return icon_generic_alloc("lake_right", &I_icon_lake_right_11x31, 11, 31);
-    }
-    else if (strcmp(name, "lake_top") == 0)
-    {
+    else if (is_str(name, "lake_top"))
         return icon_generic_alloc("lake_top", &I_icon_lake_top_31x12px, 31, 12);
-    }
-    else if (strcmp(name, "lake_top_left") == 0)
-    {
+    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 (strcmp(name, "lake_top_right") == 0)
-    {
+    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 (strcmp(name, "rock_large") == 0)
-    {
+    else if (is_str(name, "rock_large"))
         return icon_generic_alloc("rock_large", &I_icon_rock_large_18x19px, 18, 19);
-    }
-    else if (strcmp(name, "rock_medium") == 0)
-    {
+    else if (is_str(name, "rock_medium"))
         return icon_generic_alloc("rock_medium", &I_icon_rock_medium_16x14px, 16, 14);
-    }
-    else if (strcmp(name, "rock_small") == 0)
-    {
+    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);
@@ -264,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.");

+ 1 - 2
flip_world/game/icon.h

@@ -6,8 +6,7 @@ typedef struct
 {
     char id[32];
     const Icon *icon;
-    uint8_t width;
-    uint8_t height;
+    Vector size;
 } IconContext;
 
 extern const EntityDescription icon_desc;

+ 69 - 12
flip_world/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(level);
+        // draw_town_world(manager, level);
         return;
     }
 
@@ -55,10 +55,10 @@ static void set_world(Level *level, GameManager *manager, char *id)
     }
 
     FURI_LOG_I("Game", "Drawing world");
-    if (!draw_json_world_furi(level, json_data_str))
+    if (!draw_json_world_furi(manager, level, json_data_str))
     {
         FURI_LOG_E("Game", "Failed to draw world");
-        draw_town_world(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(level);
+            // draw_town_world(manager, level);
             return;
         }
 
@@ -89,28 +89,67 @@ 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)
 {
-    if (!level || !context || !manager)
+    if (!manager || !level || !context)
+    {
+        FURI_LOG_E("Game", "Manager, level, or context is NULL");
+        return;
+    }
+    GameContext *game_context = game_manager_game_context_get(manager);
+    if (!level || !context)
     {
         FURI_LOG_E("Game", "Level, context, or manager is NULL");
+        game_context->is_switching_level = false;
         return;
     }
 
     level_clear(level);
-    player_spawn(level, manager);
 
     LevelContext *level_context = context;
     if (!level_context)
     {
         FURI_LOG_E("Game", "Level context is NULL");
+        game_context->is_switching_level = false;
         return;
     }
 
@@ -122,21 +161,39 @@ 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(level);
+            // draw_town_world(manager, level);
+            game_context->is_switching_level = false;
+            // furi_delay_ms(1000);
+            player_spawn(level, manager);
             return;
         }
         furi_string_free(world_data);
 
         set_world(level, manager, level_context->id);
-
         FURI_LOG_I("Game", "World set.");
+        // furi_delay_ms(1000);
+        game_context->is_switching_level = false;
     }
     else
     {
         FURI_LOG_I("Game", "World exists.. loading now");
         set_world(level, manager, level_context->id);
         FURI_LOG_I("Game", "World set.");
+        // furi_delay_ms(1000);
+        game_context->is_switching_level = false;
     }
+    /*
+       adjust the player's position n such based on icon count
+       the more icons to draw, the slower the player moves
+       so we'll increase the player's speed as the icon count increases
+       by 0.1 for every 8 icons
+   */
+    game_context->icon_offset = 0;
+    if (!game_context->imu_present)
+    {
+        game_context->icon_offset += ((game_context->icon_count / 8) / 10);
+    }
+    player_spawn(level, manager);
 }
 
 static LevelContext *level_context_generic;

+ 2 - 1
flip_world/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
flip_world/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
flip_world/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);

+ 285 - 168
flip_world/game/player.c

@@ -1,7 +1,11 @@
 #include <game/player.h>
 #include <game/storage.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <math.h>
+#include <engine/entity_i.h>
 /****** Entities: Player ******/
-static Level *get_next_level(GameManager *manager)
+static Level *next_level(GameManager *manager)
 {
     GameContext *game_context = game_manager_game_context_get(manager);
     if (!game_context)
@@ -9,6 +13,20 @@ static Level *get_next_level(GameManager *manager)
         FURI_LOG_E(TAG, "Failed to get game context");
         return NULL;
     }
+    // check if there are more levels to load
+    if (game_context->current_level + 1 >= game_context->level_count)
+    {
+        game_context->current_level = 0;
+        if (!game_context->levels[game_context->current_level])
+        {
+            if (!allocate_level(manager, game_context->current_level))
+            {
+                FURI_LOG_E(TAG, "Failed to allocate level %d", game_context->current_level);
+                return NULL;
+            }
+        }
+        return game_context->levels[game_context->current_level];
+    }
     for (int i = game_context->current_level + 1; i < game_context->level_count; i++)
     {
         if (!game_context->levels[i])
@@ -22,7 +40,6 @@ static Level *get_next_level(GameManager *manager)
         game_context->current_level = i;
         return game_context->levels[i];
     }
-    FURI_LOG_I(TAG, "No more levels to load");
     return NULL;
 }
 
@@ -51,63 +68,74 @@ 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 *player_context = entity_context_get(game_context->player);
-    if (!player_context)
+    PlayerContext *pctx = entity_context_get(game_context->player);
+    if (!pctx)
     {
         FURI_LOG_E(TAG, "Failed to get player context");
         return;
     }
 
+    SpriteContext *sprite_context = get_sprite_context(player_sprite_choices[player_sprite_index]);
+    if (!sprite_context)
+    {
+        FURI_LOG_E(TAG, "Failed to get sprite context");
+        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(player_context))
+    if (!load_player_context(pctx))
     {
         FURI_LOG_E(TAG, "Loading player context failed. Initializing default values.");
 
         // Initialize default player context
-        player_context->sprite_right = game_manager_sprite_load(manager, "player_right_sword_15x11px.fxbm");
-        player_context->sprite_left = game_manager_sprite_load(manager, "player_left_sword_15x11px.fxbm");
-        player_context->direction = PLAYER_RIGHT; // default direction
-        player_context->health = 100;
-        player_context->strength = 10;
-        player_context->level = 1;
-        player_context->xp = 0;
-        player_context->start_position = entity_pos_get(game_context->player);
-        player_context->attack_timer = 0.1f;
-        player_context->elapsed_attack_timer = player_context->attack_timer;
-        player_context->health_regen = 1; // 1 health per second
-        player_context->elapsed_health_regen = 0;
-        player_context->max_health = 100 + ((player_context->level - 1) * 10); // 10 health per level
+        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 = ENTITY_RIGHT; // default direction
+        pctx->left = false;             // default sprite direction
+        pctx->health = 100;
+        pctx->strength = 10;
+        pctx->level = 1;
+        pctx->xp = 0;
+        pctx->start_position = entity_pos_get(game_context->player);
+        pctx->attack_timer = 0.1f;
+        pctx->elapsed_attack_timer = pctx->attack_timer;
+        pctx->health_regen = 1; // 1 health per second
+        pctx->elapsed_health_regen = 0;
+        pctx->max_health = 100 + ((pctx->level - 1) * 10); // 10 health per level
 
         // Set player username
-        if (!load_char("Flip-Social-Username", player_context->username, sizeof(player_context->username)))
+        if (!load_char("Flip-Social-Username", pctx->username, sizeof(pctx->username)))
         {
-            // If loading username fails, default to "Player"
-            snprintf(player_context->username, sizeof(player_context->username), "Player");
+            // check if data/player/username
+            if (!load_char("player/username", pctx->username, sizeof(pctx->username)))
+            {
+                // If loading username fails, default to "Player"
+                snprintf(pctx->username, sizeof(pctx->username), "Player");
+            }
         }
 
-        game_context->player_context = player_context;
+        game_context->player_context = pctx;
 
         // Save the initialized context
-        if (!save_player_context(player_context))
+        if (!save_player_context(pctx))
         {
             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)
-    player_context->sprite_right = game_manager_sprite_load(manager, "player_right_sword_15x11px.fxbm");
-    player_context->sprite_left = game_manager_sprite_load(manager, "player_left_sword_15x11px.fxbm");
+    // 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);
 
-    player_context->start_position = entity_pos_get(game_context->player);
+    pctx->start_position = entity_pos_get(game_context->player);
 
     // Update player stats based on XP using iterative method
-    // Function to get the current level based on XP iteratively
     int get_player_level_iterative(uint32_t xp)
     {
         int level = 1;
@@ -123,43 +151,56 @@ void player_spawn(Level *level, GameManager *manager)
     }
 
     // Determine the player's level based on XP
-    player_context->level = get_player_level_iterative(player_context->xp);
+    pctx->level = get_player_level_iterative(pctx->xp);
 
     // Update strength and max health based on the new level
-    player_context->strength = 10 + (player_context->level * 1);           // 1 strength per level
-    player_context->max_health = 100 + ((player_context->level - 1) * 10); // 10 health per level
+    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 = player_context;
+    game_context->player_context = pctx;
+    free(sprite_context);
+}
+
+static int vgm_increase(float value, float increase)
+{
+    const int val = abs((int)(round(value + increase) / 2));
+    return val < 1 ? 1 : val;
 }
 
-// code from Derek Jamison
-// eventually we'll add dynamic positioning based on how much pitch/roll is detected
-// instead of assigning a fixed value
-static int player_x_from_pitch(float pitch)
+static void vgm_direction(Imu *imu, PlayerContext *player, Vector *pos)
 {
-    if (pitch > 6.0)
+    const float pitch = -imu_pitch_get(imu);
+    const float roll = -imu_roll_get(imu);
+    const float min_x = atof_(vgm_levels[vgm_x_index]) + 5.0; // minimum of 3
+    const float min_y = atof_(vgm_levels[vgm_y_index]) + 5.0; // minimum of 3
+    if (pitch > min_x)
     {
-        return 1;
+        pos->x += vgm_increase(pitch, min_x);
+        player->dx = 1;
+        player->direction = ENTITY_RIGHT;
     }
-    else if (pitch < -8.0)
+    else if (pitch < -min_x)
     {
-        return -1;
+        pos->x += -vgm_increase(pitch, min_x);
+        player->dx = -1;
+        player->direction = ENTITY_LEFT;
     }
-    return 0;
-}
-
-static int player_y_from_roll(float roll)
-{
-    if (roll > 9.0)
+    if (roll > min_y)
     {
-        return 1;
+        pos->y += vgm_increase(roll, min_y);
+        player->dy = 1;
+        player->direction = ENTITY_DOWN;
     }
-    else if (roll < -20.0)
+    else if (roll < -min_y)
     {
-        return -1;
+        pos->y += -vgm_increase(roll, min_y);
+        player->dy = -1;
+        player->direction = ENTITY_UP;
     }
-    return 0;
 }
 
 static void player_update(Entity *self, GameManager *manager, void *context)
@@ -170,6 +211,7 @@ static void player_update(Entity *self, GameManager *manager, void *context)
     PlayerContext *player = (PlayerContext *)context;
     InputState input = game_manager_input_get(manager);
     Vector pos = entity_pos_get(self);
+    player->old_position = pos;
     GameContext *game_context = game_manager_game_context_get(manager);
 
     // Store previous direction
@@ -182,36 +224,8 @@ static void player_update(Entity *self, GameManager *manager, void *context)
 
     if (game_context->imu_present)
     {
-        player->dx = player_x_from_pitch(-imu_pitch_get(game_context->imu));
-        player->dy = player_y_from_roll(-imu_roll_get(game_context->imu));
-
-        switch (player->dx)
-        {
-        case -1:
-            player->direction = PLAYER_LEFT;
-            pos.x -= 1;
-            break;
-        case 1:
-            player->direction = PLAYER_RIGHT;
-            pos.x += 1;
-            break;
-        default:
-            break;
-        }
-
-        switch (player->dy)
-        {
-        case -1:
-            player->direction = PLAYER_UP;
-            pos.y -= 1;
-            break;
-        case 1:
-            player->direction = PLAYER_DOWN;
-            pos.y += 1;
-            break;
-        default:
-            break;
-        }
+        // update position using the IMU
+        vgm_direction(game_context->imu, player, &pos);
     }
 
     // Apply health regeneration
@@ -230,101 +244,209 @@ static void player_update(Entity *self, GameManager *manager, void *context)
     // Handle movement input
     if (input.held & GameKeyUp)
     {
-        pos.y -= 2;
-        player->dy = -1;
-        player->direction = PLAYER_UP;
-        game_context->user_input = GameKeyUp;
+        if (game_context->last_button == GameKeyUp)
+            game_context->elapsed_button_timer += 1;
+        else
+            game_context->elapsed_button_timer = 0;
+
+        if (!game_context->is_menu_open)
+        {
+            pos.y -= (2 + game_context->icon_offset);
+            player->dy = -1;
+            player->direction = ENTITY_UP;
+        }
+        else
+        {
+            // next menu view
+            // we can only go up to info from settings
+            game_context->menu_screen = GAME_MENU_INFO;
+        }
+        game_context->last_button = GameKeyUp;
     }
     if (input.held & GameKeyDown)
     {
-        pos.y += 2;
-        player->dy = 1;
-        player->direction = PLAYER_DOWN;
-        game_context->user_input = GameKeyDown;
+        if (game_context->last_button == GameKeyDown)
+            game_context->elapsed_button_timer += 1;
+        else
+            game_context->elapsed_button_timer = 0;
+
+        if (!game_context->is_menu_open)
+        {
+            pos.y += (2 + game_context->icon_offset);
+            player->dy = 1;
+            player->direction = ENTITY_DOWN;
+        }
+        else
+        {
+            // next menu view
+            // we can only go down to more from info
+            game_context->menu_screen = GAME_MENU_MORE;
+        }
+        game_context->last_button = GameKeyDown;
     }
     if (input.held & GameKeyLeft)
     {
-        pos.x -= 2;
-        player->dx = -1;
-        player->direction = PLAYER_LEFT;
-        game_context->user_input = GameKeyLeft;
+        if (game_context->last_button == GameKeyLeft)
+            game_context->elapsed_button_timer += 1;
+        else
+            game_context->elapsed_button_timer = 0;
+
+        if (!game_context->is_menu_open)
+        {
+            pos.x -= (2 + game_context->icon_offset);
+            player->dx = -1;
+            player->direction = ENTITY_LEFT;
+        }
+        else
+        {
+            // if the menu is open, move the selection left
+            if (game_context->menu_selection < 1)
+            {
+                game_context->menu_selection += 1;
+            }
+        }
+        game_context->last_button = GameKeyLeft;
     }
     if (input.held & GameKeyRight)
     {
-        pos.x += 2;
-        player->dx = 1;
-        player->direction = PLAYER_RIGHT;
-        game_context->user_input = GameKeyRight;
-    }
+        if (game_context->last_button == GameKeyRight)
+            game_context->elapsed_button_timer += 1;
+        else
+            game_context->elapsed_button_timer = 0;
 
-    // Clamp the player's position to stay within world bounds
-    pos.x = CLAMP(pos.x, WORLD_WIDTH - 5, 5);
-    pos.y = CLAMP(pos.y, WORLD_HEIGHT - 5, 5);
+        if (!game_context->is_menu_open)
+        {
+            pos.x += (2 + game_context->icon_offset);
+            player->dx = 1;
+            player->direction = ENTITY_RIGHT;
+        }
+        else
+        {
+            // if the menu is open, move the selection right
+            if (game_context->menu_selection < 1)
+            {
+                game_context->menu_selection += 1;
+            }
+        }
+        game_context->last_button = GameKeyRight;
+    }
+    if (input.held & GameKeyOk)
+    {
+        if (game_context->last_button == GameKeyOk)
+            game_context->elapsed_button_timer += 1;
+        else
+            game_context->elapsed_button_timer = 0;
 
-    // Update player position
-    entity_pos_set(self, pos);
+        game_context->last_button = GameKeyOk;
 
-    // switch levels if holding OK
-    if (input.pressed & GameKeyOk)
-    {
         // if all enemies are dead, allow the "OK" button to switch levels
         // otherwise the "OK" button will be used to attack
-        if (game_context->enemy_count == 0)
+        if (game_context->enemy_count == 0 && !game_context->is_switching_level)
         {
-            FURI_LOG_I(TAG, "Switching levels");
+            game_context->is_switching_level = true;
             save_player_context(player);
-            game_manager_next_level_set(manager, get_next_level(manager));
-            furi_delay_ms(500);
+            game_manager_next_level_set(manager, next_level(manager));
             return;
         }
+
+        // if the OK button is held for 1 seconds,show the menu
+        if (game_context->elapsed_button_timer > (1 * game_context->fps))
+        {
+            // open up menu on the INFO screen
+            game_context->menu_screen = GAME_MENU_INFO;
+            game_context->menu_selection = 0;
+            game_context->is_menu_open = true;
+        }
+    }
+    if (input.held & GameKeyBack)
+    {
+        if (game_context->last_button == GameKeyBack)
+            game_context->elapsed_button_timer += 1;
         else
+            game_context->elapsed_button_timer = 0;
+
+        game_context->last_button = GameKeyBack;
+
+        if (game_context->is_menu_open)
+        {
+            game_context->is_menu_open = false;
+        }
+
+        // if the back button is held for 1 seconds, stop the game
+        if (game_context->elapsed_button_timer > (1 * game_context->fps))
         {
-            game_context->user_input = GameKeyOk;
-            // furi_delay_ms(100);
+            if (!game_context->is_menu_open)
+            {
+                game_manager_game_stop(manager);
+                return;
+            }
         }
     }
 
+    // Clamp the player's position to stay within world bounds
+    pos.x = CLAMP(pos.x, WORLD_WIDTH - 5, 5);
+    pos.y = CLAMP(pos.y, WORLD_HEIGHT - 5, 5);
+
+    // Update player position
+    entity_pos_set(self, pos);
+
     // If the player is not moving, retain the last movement direction
     if (player->dx == 0 && player->dy == 0)
     {
         player->dx = prev_dx;
         player->dy = prev_dy;
-        player->state = PLAYER_IDLE;
-        game_context->user_input = -1; // reset user input
+        player->state = ENTITY_IDLE;
     }
     else
-    {
-        player->state = PLAYER_MOVING;
-    }
-
-    // Handle back button to stop the game
-    if (input.pressed & GameKeyBack)
-    {
-        game_manager_game_stop(manager);
-    }
+        player->state = ENTITY_MOVING;
 }
 
 static void player_render(Entity *self, GameManager *manager, Canvas *canvas, void *context)
 {
-    UNUSED(manager);
-    if (!self || !context || !canvas)
+    if (!self || !context || !canvas || !manager)
         return;
+
     // Get player context
     PlayerContext *player = context;
 
     // Get player position
     Vector pos = entity_pos_get(self);
 
-    // Draw background (updates camera_x and camera_y)
-    draw_background(canvas, pos);
+    // Calculate camera offset to center the player
+    camera_x = pos.x - (SCREEN_WIDTH / 2);
+    camera_y = pos.y - (SCREEN_HEIGHT / 2);
+
+    // Clamp camera position to prevent showing areas outside the world
+    camera_x = CLAMP(camera_x, WORLD_WIDTH - SCREEN_WIDTH, 0);
+    camera_y = CLAMP(camera_y, WORLD_HEIGHT - SCREEN_HEIGHT, 0);
 
-    // Draw 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);
+
+    // render background
+    background_render(canvas, manager);
 }
 
 const EntityDescription player_desc = {
@@ -337,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)
@@ -348,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 (strcmp(name, "axe") == 0)
-    {
-        return sprite_generic_alloc("axe", false, 15, 11);
-    }
-    else if (strcmp(name, "bow") == 0)
-    {
-        return sprite_generic_alloc("bow", false, 13, 11);
-    }
-    else if (strcmp(name, "naked") == 0)
-    {
-        return sprite_generic_alloc("naked", false, 10, 10);
-    }
-    else if (strcmp(name, "sword") == 0)
-    {
-        return sprite_generic_alloc("sword", false, 15, 11);
-    }
-    else if (strcmp(name, "cyclops") == 0)
-    {
-        return sprite_generic_alloc("cyclops", true, 10, 11);
-    }
-    else if (strcmp(name, "ghost") == 0)
-    {
-        return sprite_generic_alloc("ghost", true, 15, 15);
-    }
-    else if (strcmp(name, "ogre") == 0)
-    {
-        return sprite_generic_alloc("ogre", true, 10, 13);
-    }
+    if (is_str(name, "axe"))
+        return sprite_generic_alloc("axe", "player", 15, 11);
+    else if (is_str(name, "bow"))
+        return sprite_generic_alloc("bow", "player", 13, 11);
+    else if (is_str(name, "naked"))
+        return sprite_generic_alloc("naked", "player", 10, 10);
+    else if (is_str(name, "sword"))
+        return sprite_generic_alloc("sword", "player", 15, 11);
+    //
+    else if (is_str(name, "cyclops"))
+        return sprite_generic_alloc("cyclops", "enemy", 10, 11);
+    else if (is_str(name, "ghost"))
+        return sprite_generic_alloc("ghost", "enemy", 15, 15);
+    else if (is_str(name, "ogre"))
+        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);

+ 51 - 20
flip_world/game/player.h

@@ -4,31 +4,38 @@
 #include <game/game.h>
 #include "engine/sensors/imu.h"
 
-// Maximum enemies
-#define MAX_ENEMIES 2
+#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
 {
-    PlayerDirection direction;  // direction the player is facing
-    PlayerState state;          // current state of the player
+    Vector old_position;        // previous position 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
@@ -44,22 +51,46 @@ 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
+typedef enum
+{
+    GAME_MENU_INFO, // level, health, xp, etc.
+    GAME_MENU_MORE, // more settings
+    GAME_MENU_NPC,  // NPC dialog
+} GameMenuScreen;
+
 typedef struct
 {
     PlayerContext *player_context;
     Level *levels[MAX_LEVELS];
     Entity *enemies[MAX_ENEMIES];
+    Entity *npcs[MAX_NPCS];
     Entity *player;
-    GameKey user_input;
     float fps;
     int level_count;
     int enemy_count;
+    int npc_count;
     int current_level;
     bool ended_early;
     Imu *imu;
     bool imu_present;
+    //
+    bool is_switching_level;
+    bool is_menu_open;
+    //
+    uint32_t elapsed_button_timer;
+    uint32_t last_button;
+    //
+    GameMenuScreen menu_screen;
+    uint8_t menu_selection;
+    //
+    int icon_count;
+    int icon_offset;
+    //
+    char message[64];
 } GameContext;
 
 typedef struct

+ 88 - 100
flip_world/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;
 }

Dosya farkı çok büyük olduğundan ihmal edildi
+ 14 - 63
flip_world/game/world.c


+ 5 - 7
flip_world/game/world.h

@@ -4,14 +4,12 @@
 #define SCREEN_WIDTH 128
 #define SCREEN_HEIGHT 64
 
-// World size (3x3)
-#define WORLD_WIDTH 384
-#define WORLD_HEIGHT 192
+// World size (6x6)
+#define WORLD_WIDTH 768
+#define WORLD_HEIGHT 384
 
 // 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_bounds(Canvas *canvas);
-void draw_town_world(Level *level);
-bool draw_json_world_furi(Level *level, const FuriString *json_data);
+const LevelBehaviour *training_world();
+bool draw_json_world_furi(GameManager *manager, Level *level, const FuriString *json_data);
 FuriString *fetch_world(const char *name);

+ 8 - 0
flip_world/jsmn/jsmn_h.h

@@ -4,6 +4,14 @@
 #include <string.h>
 #include <stdlib.h>
 #include <stdio.h>
+// added by Derek Jamison to lower memory usage
+#undef FURI_LOG_E
+#define FURI_LOG_E(tag, msg, ...)
+
+#undef FURI_LOG_I
+#define FURI_LOG_I(tag, msg, ...)
+//
+
 typedef enum
 {
     JSMN_UNDEFINED = 0,

BIN
flip_world/sprites/npc_left_funny_15x21px.png


BIN
flip_world/sprites/npc_right_funny_15x21px.png


Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor