Explorar el Código

Merge pull request #14 from jblanked/dev_0.4

FlipWorld - v0.4
JBlanked hace 11 meses
padre
commit
e47b5eb4d3

+ 9 - 3
README.md

@@ -15,7 +15,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 +23,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**: Attack/Teleport (set to attack until all enemies are defeated).
+- **HOLD OK**: In-Game Menu.
+- **Press BACK**: Leave the menu.
+- **HOLD BACK**: Exit the game.
 
 **Player Attributes**
 
@@ -67,9 +70,12 @@ An enemy attack registers if the enemy is facing you and collides with you. Howe
 
 **v0.4**
 - New game features
+- World expansion
+- Stability patch
 
 **v0.5**
-- ???
+- New game features
+- Custom Controller Support
 
 **v0.6**
 - ???

+ 4 - 4
alloc/alloc.c

@@ -24,13 +24,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;
@@ -78,7 +78,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);
     }
 

+ 1 - 1
application.fam

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

BIN
assets/01-home.png


BIN
assets/02-town.png


BIN
assets/03-town.png


BIN
assets/06-tree.png


+ 11 - 3
assets/CHANGELOG.md

@@ -1,10 +1,18 @@
-**0.3 (2025-01-14)**
+## 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 +24,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.

+ 10 - 4
assets/README.md

@@ -1,4 +1,4 @@
-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
 
@@ -13,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**
 
@@ -21,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**: Attack/Teleport (set to attack until all enemies are defeated).
+- **HOLD OK**: In-Game Menu.
+- **Press BACK**: Leave the menu.
+- **HOLD BACK**: Exit the game.
 
 **Player Attributes**
 
@@ -64,9 +67,12 @@ 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**
-- ???
+- New game features
+- Custom Controller Support
 
 **v0.6**
 - ???

BIN
assets/icon_title_screen_128x64px.png


+ 287 - 118
callback/callback.c

@@ -37,9 +37,9 @@ static int32_t game_app(void *p)
 
     // Setup game engine settings...
     GameEngineSettings settings = game_engine_settings_init();
-    settings.target_fps = game_fps_choices_2[game_fps_index];
+    settings.target_fps = atof_(fps_choices_str[fps_index]);
     settings.show_fps = game.show_fps;
-    settings.always_backlight = strstr(yes_or_no_choices[game_screen_always_on_index], "Yes") != NULL;
+    settings.always_backlight = strstr(yes_or_no_choices[screen_always_on_index], "Yes") != NULL;
     settings.frame_callback = frame_cb;
     settings.context = game_manager;
     GameEngine *engine = game_engine_alloc(settings);
@@ -91,11 +91,11 @@ static int32_t game_app(void *p)
     return 0;
 }
 
-static void flip_world_request_error_draw(Canvas *canvas, DataLoaderModel *model)
+static void error_draw(Canvas *canvas, DataLoaderModel *model)
 {
     if (canvas == NULL)
     {
-        FURI_LOG_E(TAG, "flip_world_request_error_draw - canvas is NULL");
+        FURI_LOG_E(TAG, "error_draw - canvas is NULL");
         DEV_CRASH();
         return;
     }
@@ -148,18 +148,21 @@ static bool alloc_about_view(void *context);
 static bool alloc_text_input_view(void *context, char *title);
 static bool alloc_variable_item_list(void *context, uint32_t view_id);
 //
-static void wifi_settings_item_selected(void *context, uint32_t index);
-static void text_updated_wifi_ssid(void *context);
-static void text_updated_wifi_pass(void *context);
-static void text_updated_username(void *context);
-static void text_updated_password(void *context);
+static void wifi_settings_select(void *context, uint32_t index);
+static void updated_wifi_ssid(void *context);
+static void updated_wifi_pass(void *context);
+static void updated_username(void *context);
+static void updated_password(void *context);
 //
-static void flip_world_game_fps_change(VariableItem *item);
-static void game_settings_item_selected(void *context, uint32_t index);
-static void user_settings_item_selected(void *context, uint32_t index);
-static void flip_world_game_screen_always_on_change(VariableItem *item);
-static void flip_world_game_sound_on_change(VariableItem *item);
-static void flip_world_game_vibration_on_change(VariableItem *item);
+static void fps_change(VariableItem *item);
+static void game_settings_select(void *context, uint32_t index);
+static void user_settings_select(void *context, uint32_t index);
+static void screen_on_change(VariableItem *item);
+static void sound_on_change(VariableItem *item);
+static void vibration_on_change(VariableItem *item);
+static void player_on_change(VariableItem *item);
+static void vgm_x_change(VariableItem *item);
+static void vgm_y_change(VariableItem *item);
 
 uint32_t callback_to_submenu(void *context)
 {
@@ -177,7 +180,7 @@ static uint32_t callback_to_settings(void *context)
     return FlipWorldViewSettings;
 }
 
-static void flip_world_view_about_draw_callback(Canvas *canvas, void *model)
+static void about_draw_callback(Canvas *canvas, void *model)
 {
     UNUSED(model);
     canvas_clear(canvas);
@@ -203,7 +206,7 @@ static bool alloc_about_view(void *context)
     }
     if (!app->view_about)
     {
-        if (!easy_flipper_set_view(&app->view_about, FlipWorldViewAbout, flip_world_view_about_draw_callback, NULL, callback_to_submenu, &app->view_dispatcher, app))
+        if (!easy_flipper_set_view(&app->view_about, FlipWorldViewAbout, about_draw_callback, NULL, callback_to_submenu, &app->view_dispatcher, app))
         {
             return false;
         }
@@ -251,9 +254,9 @@ static bool alloc_text_input_view(void *context, char *title)
                 title,
                 app->text_input_temp_buffer,
                 app->text_input_buffer_size,
-                strcmp(title, "SSID") == 0 ? text_updated_wifi_ssid : strcmp(title, "Password") == 0     ? text_updated_wifi_pass
-                                                                  : strcmp(title, "Username-Login") == 0 ? text_updated_username
-                                                                                                         : text_updated_password,
+                is_str(title, "SSID") ? updated_wifi_ssid : is_str(title, "Password")     ? updated_wifi_pass
+                                                        : is_str(title, "Username-Login") ? updated_username
+                                                                                          : updated_password,
                 callback_to_wifi_settings,
                 &app->view_dispatcher,
                 app))
@@ -270,19 +273,19 @@ static bool alloc_text_input_view(void *context, char *title)
         char password[64];
         if (load_settings(ssid, sizeof(ssid), pass, sizeof(pass), username, sizeof(username), password, sizeof(password)))
         {
-            if (strcmp(title, "SSID") == 0)
+            if (is_str(title, "SSID"))
             {
                 strncpy(app->text_input_temp_buffer, ssid, app->text_input_buffer_size);
             }
-            else if (strcmp(title, "Password") == 0)
+            else if (is_str(title, "Password"))
             {
                 strncpy(app->text_input_temp_buffer, pass, app->text_input_buffer_size);
             }
-            else if (strcmp(title, "Username-Login") == 0)
+            else if (is_str(title, "Username-Login"))
             {
                 strncpy(app->text_input_temp_buffer, username, app->text_input_buffer_size);
             }
-            else if (strcmp(title, "Password-Login") == 0)
+            else if (is_str(title, "Password-Login"))
             {
                 strncpy(app->text_input_temp_buffer, password, app->text_input_buffer_size);
             }
@@ -307,7 +310,7 @@ static bool alloc_variable_item_list(void *context, uint32_t view_id)
         switch (view_id)
         {
         case FlipWorldSubmenuIndexWiFiSettings:
-            if (!easy_flipper_set_variable_item_list(&app->variable_item_list, FlipWorldViewVariableItemList, wifi_settings_item_selected, callback_to_settings, &app->view_dispatcher, app))
+            if (!easy_flipper_set_variable_item_list(&app->variable_item_list, FlipWorldViewVariableItemList, wifi_settings_select, callback_to_settings, &app->view_dispatcher, app))
             {
                 FURI_LOG_E(TAG, "Failed to allocate variable item list");
                 return false;
@@ -340,7 +343,7 @@ static bool alloc_variable_item_list(void *context, uint32_t view_id)
             }
             break;
         case FlipWorldSubmenuIndexGameSettings:
-            if (!easy_flipper_set_variable_item_list(&app->variable_item_list, FlipWorldViewVariableItemList, game_settings_item_selected, callback_to_settings, &app->view_dispatcher, app))
+            if (!easy_flipper_set_variable_item_list(&app->variable_item_list, FlipWorldViewVariableItemList, game_settings_select, callback_to_settings, &app->view_dispatcher, app))
             {
                 FURI_LOG_E(TAG, "Failed to allocate variable item list");
                 return false;
@@ -357,67 +360,137 @@ static bool alloc_variable_item_list(void *context, uint32_t view_id)
                 app->variable_item_game_download_world = variable_item_list_add(app->variable_item_list, "Install Official World Pack", 0, NULL, NULL);
                 variable_item_set_current_value_text(app->variable_item_game_download_world, "");
             }
+            if (!app->variable_item_game_player_sprite)
+            {
+                app->variable_item_game_player_sprite = variable_item_list_add(app->variable_item_list, "Weapon", 4, player_on_change, NULL);
+                variable_item_set_current_value_index(app->variable_item_game_player_sprite, 1);
+                variable_item_set_current_value_text(app->variable_item_game_player_sprite, player_sprite_choices[1]);
+            }
             if (!app->variable_item_game_fps)
             {
-                app->variable_item_game_fps = variable_item_list_add(app->variable_item_list, "FPS", 4, flip_world_game_fps_change, NULL);
+                app->variable_item_game_fps = variable_item_list_add(app->variable_item_list, "FPS", 4, fps_change, NULL);
                 variable_item_set_current_value_index(app->variable_item_game_fps, 0);
-                variable_item_set_current_value_text(app->variable_item_game_fps, game_fps_choices[0]);
+                variable_item_set_current_value_text(app->variable_item_game_fps, fps_choices_str[0]);
+            }
+            if (!app->variable_item_game_vgm_x)
+            {
+                app->variable_item_game_vgm_x = variable_item_list_add(app->variable_item_list, "VGM Horizontal", 12, vgm_x_change, NULL);
+                variable_item_set_current_value_index(app->variable_item_game_vgm_x, 2);
+                variable_item_set_current_value_text(app->variable_item_game_vgm_x, vgm_levels[2]);
+            }
+            if (!app->variable_item_game_vgm_y)
+            {
+                app->variable_item_game_vgm_y = variable_item_list_add(app->variable_item_list, "VGM Vertical", 12, vgm_y_change, NULL);
+                variable_item_set_current_value_index(app->variable_item_game_vgm_y, 2);
+                variable_item_set_current_value_text(app->variable_item_game_vgm_y, vgm_levels[2]);
             }
             if (!app->variable_item_game_screen_always_on)
             {
-                app->variable_item_game_screen_always_on = variable_item_list_add(app->variable_item_list, "Keep Screen On?", 2, flip_world_game_screen_always_on_change, NULL);
+                app->variable_item_game_screen_always_on = variable_item_list_add(app->variable_item_list, "Keep Screen On?", 2, screen_on_change, NULL);
                 variable_item_set_current_value_index(app->variable_item_game_screen_always_on, 1);
                 variable_item_set_current_value_text(app->variable_item_game_screen_always_on, yes_or_no_choices[1]);
             }
             if (!app->variable_item_game_sound_on)
             {
-                app->variable_item_game_sound_on = variable_item_list_add(app->variable_item_list, "Sound On?", 2, flip_world_game_sound_on_change, NULL);
+                app->variable_item_game_sound_on = variable_item_list_add(app->variable_item_list, "Sound On?", 2, sound_on_change, NULL);
                 variable_item_set_current_value_index(app->variable_item_game_sound_on, 0);
                 variable_item_set_current_value_text(app->variable_item_game_sound_on, yes_or_no_choices[0]);
             }
             if (!app->variable_item_game_vibration_on)
             {
-                app->variable_item_game_vibration_on = variable_item_list_add(app->variable_item_list, "Vibration On?", 2, flip_world_game_vibration_on_change, NULL);
+                app->variable_item_game_vibration_on = variable_item_list_add(app->variable_item_list, "Vibration On?", 2, vibration_on_change, NULL);
                 variable_item_set_current_value_index(app->variable_item_game_vibration_on, 0);
                 variable_item_set_current_value_text(app->variable_item_game_vibration_on, yes_or_no_choices[0]);
             }
+            char _game_player_sprite[8];
+            if (load_char("Game-Player-Sprite", _game_player_sprite, sizeof(_game_player_sprite)))
+            {
+                int index = is_str(_game_player_sprite, "naked") ? 0 : is_str(_game_player_sprite, "sword") ? 1
+                                                                   : is_str(_game_player_sprite, "axe")     ? 2
+                                                                   : is_str(_game_player_sprite, "bow")     ? 3
+                                                                                                            : 0;
+                variable_item_set_current_value_index(app->variable_item_game_player_sprite, index);
+                variable_item_set_current_value_text(
+                    app->variable_item_game_player_sprite,
+                    is_str(player_sprite_choices[index], "naked") ? "None" : player_sprite_choices[index]);
+            }
             char _game_fps[8];
             if (load_char("Game-FPS", _game_fps, sizeof(_game_fps)))
             {
-                int index = strcmp(_game_fps, "30") == 0 ? 0 : strcmp(_game_fps, "60") == 0 ? 1
-                                                           : strcmp(_game_fps, "120") == 0  ? 2
-                                                           : strcmp(_game_fps, "240") == 0  ? 3
-                                                                                            : 0;
-                variable_item_set_current_value_text(app->variable_item_game_fps, game_fps_choices[index]);
+                int index = is_str(_game_fps, "30") ? 0 : is_str(_game_fps, "60") ? 1
+                                                      : is_str(_game_fps, "120")  ? 2
+                                                      : is_str(_game_fps, "240")  ? 3
+                                                                                  : 0;
+                variable_item_set_current_value_text(app->variable_item_game_fps, fps_choices_str[index]);
                 variable_item_set_current_value_index(app->variable_item_game_fps, index);
             }
+            char _game_vgm_x[8];
+            if (load_char("Game-VGM-X", _game_vgm_x, sizeof(_game_vgm_x)))
+            {
+                int vgm_x = atoi(_game_vgm_x);
+                int index = vgm_x == -2 ? 0 : vgm_x == -1 ? 1
+                                          : vgm_x == 0    ? 2
+                                          : vgm_x == 1    ? 3
+                                          : vgm_x == 2    ? 4
+                                          : vgm_x == 3    ? 5
+                                          : vgm_x == 4    ? 6
+                                          : vgm_x == 5    ? 7
+                                          : vgm_x == 6    ? 8
+                                          : vgm_x == 7    ? 9
+                                          : vgm_x == 8    ? 10
+                                          : vgm_x == 9    ? 11
+                                          : vgm_x == 10   ? 12
+                                                          : 2;
+                variable_item_set_current_value_index(app->variable_item_game_vgm_x, index);
+                variable_item_set_current_value_text(app->variable_item_game_vgm_x, vgm_levels[index]);
+            }
+            char _game_vgm_y[8];
+            if (load_char("Game-VGM-Y", _game_vgm_y, sizeof(_game_vgm_y)))
+            {
+                int vgm_y = atoi(_game_vgm_y);
+                int index = vgm_y == -2 ? 0 : vgm_y == -1 ? 1
+                                          : vgm_y == 0    ? 2
+                                          : vgm_y == 1    ? 3
+                                          : vgm_y == 2    ? 4
+                                          : vgm_y == 3    ? 5
+                                          : vgm_y == 4    ? 6
+                                          : vgm_y == 5    ? 7
+                                          : vgm_y == 6    ? 8
+                                          : vgm_y == 7    ? 9
+                                          : vgm_y == 8    ? 10
+                                          : vgm_y == 9    ? 11
+                                          : vgm_y == 10   ? 12
+                                                          : 2;
+                variable_item_set_current_value_index(app->variable_item_game_vgm_y, index);
+                variable_item_set_current_value_text(app->variable_item_game_vgm_y, vgm_levels[index]);
+            }
             char _game_screen_always_on[8];
             if (load_char("Game-Screen-Always-On", _game_screen_always_on, sizeof(_game_screen_always_on)))
             {
-                int index = strcmp(_game_screen_always_on, "No") == 0 ? 0 : strcmp(_game_screen_always_on, "Yes") == 0 ? 1
-                                                                                                                       : 0;
+                int index = is_str(_game_screen_always_on, "No") ? 0 : is_str(_game_screen_always_on, "Yes") ? 1
+                                                                                                             : 0;
                 variable_item_set_current_value_text(app->variable_item_game_screen_always_on, yes_or_no_choices[index]);
                 variable_item_set_current_value_index(app->variable_item_game_screen_always_on, index);
             }
             char _game_sound_on[8];
             if (load_char("Game-Sound-On", _game_sound_on, sizeof(_game_sound_on)))
             {
-                int index = strcmp(_game_sound_on, "No") == 0 ? 0 : strcmp(_game_sound_on, "Yes") == 0 ? 1
-                                                                                                       : 0;
+                int index = is_str(_game_sound_on, "No") ? 0 : is_str(_game_sound_on, "Yes") ? 1
+                                                                                             : 0;
                 variable_item_set_current_value_text(app->variable_item_game_sound_on, yes_or_no_choices[index]);
                 variable_item_set_current_value_index(app->variable_item_game_sound_on, index);
             }
             char _game_vibration_on[8];
             if (load_char("Game-Vibration-On", _game_vibration_on, sizeof(_game_vibration_on)))
             {
-                int index = strcmp(_game_vibration_on, "No") == 0 ? 0 : strcmp(_game_vibration_on, "Yes") == 0 ? 1
-                                                                                                               : 0;
+                int index = is_str(_game_vibration_on, "No") ? 0 : is_str(_game_vibration_on, "Yes") ? 1
+                                                                                                     : 0;
                 variable_item_set_current_value_text(app->variable_item_game_vibration_on, yes_or_no_choices[index]);
                 variable_item_set_current_value_index(app->variable_item_game_vibration_on, index);
             }
             break;
         case FlipWorldSubmenuIndexUserSettings:
-            if (!easy_flipper_set_variable_item_list(&app->variable_item_list, FlipWorldViewVariableItemList, user_settings_item_selected, callback_to_settings, &app->view_dispatcher, app))
+            if (!easy_flipper_set_variable_item_list(&app->variable_item_list, FlipWorldViewVariableItemList, user_settings_select, callback_to_settings, &app->view_dispatcher, app))
             {
                 FURI_LOG_E(TAG, "Failed to allocate variable item list");
                 return false;
@@ -581,6 +654,21 @@ static void free_variable_item_list(void *context)
         free(app->variable_item_game_vibration_on);
         app->variable_item_game_vibration_on = NULL;
     }
+    if (app->variable_item_game_player_sprite)
+    {
+        free(app->variable_item_game_player_sprite);
+        app->variable_item_game_player_sprite = NULL;
+    }
+    if (app->variable_item_game_vgm_x)
+    {
+        free(app->variable_item_game_vgm_x);
+        app->variable_item_game_vgm_x = NULL;
+    }
+    if (app->variable_item_game_vgm_y)
+    {
+        free(app->variable_item_game_vgm_y);
+        app->variable_item_game_vgm_y = NULL;
+    }
     if (app->variable_item_user_username)
     {
         free(app->variable_item_user_username);
@@ -660,7 +748,7 @@ static bool fetch_world_list(FlipperHTTP *fhttp)
         STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/world_list.json");
 
     fhttp->save_received_data = true;
-    return flipper_http_get_request_with_headers(fhttp, "https://www.flipsocial.net/api/world/v3/list/10/", "{\"Content-Type\":\"application/json\"}");
+    return flipper_http_get_request_with_headers(fhttp, "https://www.flipsocial.net/api/world/v4/list/10/", "{\"Content-Type\":\"application/json\"}");
 }
 // we will load the palyer stats from the API and save them
 // in player_spawn game method, it will load the player stats that we saved
@@ -690,6 +778,61 @@ static bool fetch_player_stats(FlipperHTTP *fhttp)
     return flipper_http_get_request_with_headers(fhttp, url, "{\"Content-Type\":\"application/json\"}");
 }
 
+// static bool fetch_app_update(FlipperHTTP *fhttp)
+// {
+//     if (!fhttp)
+//     {
+//         FURI_LOG_E(TAG, "fhttp is NULL");
+//         easy_flipper_dialog("Error", "fhttp is NULL. Press BACK to return.");
+//         return false;
+//     }
+
+//     return flipper_http_get_request_with_headers(fhttp, "https://www.flipsocial.net/api/app/last-updated/flip_world/", "{\"Content-Type\":\"application/json\"}");
+// }
+
+// static bool parse_app_update(FlipperHTTP *fhttp)
+// {
+//     if (!fhttp)
+//     {
+//         FURI_LOG_E(TAG, "fhttp is NULL");
+//         easy_flipper_dialog("Error", "fhttp is NULL. Press BACK to return.");
+//         return false;
+//     }
+//     if (fhttp->last_response == NULL || strlen(fhttp->last_response) == 0)
+//     {
+//         FURI_LOG_E(TAG, "fhttp->last_response is NULL or empty");
+//         easy_flipper_dialog("Error", "fhttp->last_response is NULL or empty. Press BACK to return.");
+//         return false;
+//     }
+//     bool last_update_available = false;
+//     char last_updated_old[32];
+//     // load the previous last_updated
+//     if (!load_char("last_updated", last_updated_old, sizeof(last_updated_old)))
+//     {
+//         FURI_LOG_E(TAG, "Failed to load last_updated");
+//         // it's okay, we'll just update it
+//     }
+//     // save the new last_updated
+//     save_char("last_updated", fhttp->last_response);
+
+//     // compare the two
+//     if (strlen(last_updated_old) == 0 || !is_str(last_updated_old, fhttp->last_response))
+//     {
+//         last_update_available = true;
+//     }
+
+//     if (last_update_available)
+//     {
+//         easy_flipper_dialog("Update Available", "An update is available. Press OK to update.");
+//         return true;
+//     }
+//     else
+//     {
+//         easy_flipper_dialog("No Update Available", "No update is available. Press OK to continue.");
+//         return false;
+//     }
+// }
+
 static bool start_game_thread(void *context)
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
@@ -720,7 +863,7 @@ static bool start_game_thread(void *context)
     return true;
 }
 // combine register, login, and world list fetch into one function to switch to the loader view
-static bool flip_world_fetch_game(DataLoaderModel *model)
+static bool _fetch_game(DataLoaderModel *model)
 {
     FlipWorldApp *app = (FlipWorldApp *)model->parser_context;
     if (!app)
@@ -763,7 +906,7 @@ static bool flip_world_fetch_game(DataLoaderModel *model)
             view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now
             return false;
         }
-        if (strcmp(is_logged_in, "false") == 0 && strcmp(model->title, "Registering...") == 0)
+        if (is_str(is_logged_in, "false") && is_str(model->title, "Registering..."))
         {
             // register
             char username[64];
@@ -843,7 +986,7 @@ static bool flip_world_fetch_game(DataLoaderModel *model)
 
         model->fhttp->save_received_data = true;
         char url[128];
-        snprintf(url, sizeof(url), "https://www.flipsocial.net/api/world/v3/get/world/%s/", furi_string_get_cstr(first_world));
+        snprintf(url, sizeof(url), "https://www.flipsocial.net/api/world/v4/get/world/%s/", furi_string_get_cstr(first_world));
         furi_string_free(world_list);
         furi_string_free(first_world);
         return flipper_http_get_request_with_headers(model->fhttp, url, "{\"Content-Type\":\"application/json\"}");
@@ -851,7 +994,7 @@ static bool flip_world_fetch_game(DataLoaderModel *model)
     FURI_LOG_E(TAG, "Unknown request index");
     return false;
 }
-static char *flip_world_parse_game(DataLoaderModel *model)
+static char *_parse_game(DataLoaderModel *model)
 {
     FlipWorldApp *app = (FlipWorldApp *)model->parser_context;
 
@@ -904,7 +1047,7 @@ static char *flip_world_parse_game(DataLoaderModel *model)
     }
     else if (model->request_index == 1)
     {
-        if (strcmp(model->title, "Registering...") == 0)
+        if (is_str(model->title, "Registering..."))
         {
             // check registration response
             if (model->fhttp->last_response != NULL && (strstr(model->fhttp->last_response, "[SUCCESS]") != NULL || strstr(model->fhttp->last_response, "User created") != NULL))
@@ -998,9 +1141,9 @@ static char *flip_world_parse_game(DataLoaderModel *model)
     view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now
     return "Unknown error";
 }
-void flip_world_switch_to_view_get_game(FlipWorldApp *app)
+static void switch_to_view_get_game(FlipWorldApp *app)
 {
-    flip_world_generic_switch_to_view(app, "Starting Game..", flip_world_fetch_game, flip_world_parse_game, 5, callback_to_submenu, FlipWorldViewLoader);
+    generic_switch_to_view(app, "Starting Game..", _fetch_game, _parse_game, 5, callback_to_submenu, FlipWorldViewLoader);
 }
 
 void callback_submenu_choices(void *context, uint32_t index)
@@ -1089,7 +1232,7 @@ void callback_submenu_choices(void *context, uint32_t index)
         }
         else
         {
-            flip_world_switch_to_view_get_game(app);
+            switch_to_view_get_game(app);
         }
         break;
     case FlipWorldSubmenuIndexAbout:
@@ -1142,7 +1285,7 @@ void callback_submenu_choices(void *context, uint32_t index)
     }
 }
 
-static void text_updated_wifi_ssid(void *context)
+static void updated_wifi_ssid(void *context)
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
     if (!app)
@@ -1202,7 +1345,7 @@ static void text_updated_wifi_ssid(void *context)
     // switch to the settings view
     view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewVariableItemList);
 }
-static void text_updated_wifi_pass(void *context)
+static void updated_wifi_pass(void *context)
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
     if (!app)
@@ -1262,7 +1405,7 @@ static void text_updated_wifi_pass(void *context)
     // switch to the settings view
     view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewVariableItemList);
 }
-static void text_updated_username(void *context)
+static void updated_username(void *context)
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
     if (!app)
@@ -1287,7 +1430,7 @@ static void text_updated_username(void *context)
     }
     view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewVariableItemList); // back to user settings
 }
-static void text_updated_password(void *context)
+static void updated_password(void *context)
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
     if (!app)
@@ -1328,7 +1471,7 @@ static void text_updated_password(void *context)
     view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewVariableItemList); // back to user settings
 }
 
-static void wifi_settings_item_selected(void *context, uint32_t index)
+static void wifi_settings_select(void *context, uint32_t index)
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
     if (!app)
@@ -1377,48 +1520,64 @@ static void wifi_settings_item_selected(void *context, uint32_t index)
         break;
     }
 }
-static void flip_world_game_fps_change(VariableItem *item)
+static void fps_change(VariableItem *item)
 {
     uint8_t index = variable_item_get_current_value_index(item);
-    game_fps_index = index;
-    variable_item_set_current_value_text(item, game_fps_choices[index]);
+    fps_index = index;
+    variable_item_set_current_value_text(item, fps_choices_str[index]);
     variable_item_set_current_value_index(item, index);
-
-    // save the fps
-    save_char("Game-FPS", game_fps_choices[index]);
+    save_char("Game-FPS", fps_choices_str[index]);
 }
-static void flip_world_game_screen_always_on_change(VariableItem *item)
+static void screen_on_change(VariableItem *item)
 {
     uint8_t index = variable_item_get_current_value_index(item);
-    game_screen_always_on_index = index;
+    screen_always_on_index = index;
     variable_item_set_current_value_text(item, yes_or_no_choices[index]);
     variable_item_set_current_value_index(item, index);
-
-    // save the screen always on
     save_char("Game-Screen-Always-On", yes_or_no_choices[index]);
 }
-static void flip_world_game_sound_on_change(VariableItem *item)
+static void sound_on_change(VariableItem *item)
 {
     uint8_t index = variable_item_get_current_value_index(item);
-    game_sound_on_index = index;
+    sound_on_index = index;
     variable_item_set_current_value_text(item, yes_or_no_choices[index]);
     variable_item_set_current_value_index(item, index);
-
-    // save the screen always on
     save_char("Game-Sound-On", yes_or_no_choices[index]);
 }
-static void flip_world_game_vibration_on_change(VariableItem *item)
+static void vibration_on_change(VariableItem *item)
 {
     uint8_t index = variable_item_get_current_value_index(item);
-    game_vibration_on_index = index;
+    vibration_on_index = index;
     variable_item_set_current_value_text(item, yes_or_no_choices[index]);
     variable_item_set_current_value_index(item, index);
-
-    // save the screen always on
     save_char("Game-Vibration-On", yes_or_no_choices[index]);
 }
+static void player_on_change(VariableItem *item)
+{
+    uint8_t index = variable_item_get_current_value_index(item);
+    player_sprite_index = index;
+    variable_item_set_current_value_text(item, is_str(player_sprite_choices[index], "naked") ? "None" : player_sprite_choices[index]);
+    variable_item_set_current_value_index(item, index);
+    save_char("Game-Player-Sprite", player_sprite_choices[index]);
+}
+static void vgm_x_change(VariableItem *item)
+{
+    uint8_t index = variable_item_get_current_value_index(item);
+    vgm_x_index = index;
+    variable_item_set_current_value_text(item, vgm_levels[index]);
+    variable_item_set_current_value_index(item, index);
+    save_char("Game-VGM-X", vgm_levels[index]);
+}
+static void vgm_y_change(VariableItem *item)
+{
+    uint8_t index = variable_item_get_current_value_index(item);
+    vgm_y_index = index;
+    variable_item_set_current_value_text(item, vgm_levels[index]);
+    variable_item_set_current_value_index(item, index);
+    save_char("Game-VGM-Y", vgm_levels[index]);
+}
 
-static bool flip_world_fetch_worlds(DataLoaderModel *model)
+static bool _fetch_worlds(DataLoaderModel *model)
 {
     if (!model || !model->fhttp)
     {
@@ -1438,18 +1597,18 @@ static bool flip_world_fetch_worlds(DataLoaderModel *model)
         sizeof(model->fhttp->file_path),
         STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/world_list_full.json");
     model->fhttp->save_received_data = true;
-    return flipper_http_get_request_with_headers(model->fhttp, "https://www.flipsocial.net/api/world/v3/get/10/", "{\"Content-Type\":\"application/json\"}");
+    return flipper_http_get_request_with_headers(model->fhttp, "https://www.flipsocial.net/api/world/v4/get/10/", "{\"Content-Type\":\"application/json\"}");
 }
-static char *flip_world_parse_worlds(DataLoaderModel *model)
+static char *_parse_worlds(DataLoaderModel *model)
 {
     UNUSED(model);
     return "World Pack Installed";
 }
-static void flip_world_switch_to_view_get_worlds(FlipWorldApp *app)
+static void switch_to_view_get_worlds(FlipWorldApp *app)
 {
-    flip_world_generic_switch_to_view(app, "Fetching World Pack..", flip_world_fetch_worlds, flip_world_parse_worlds, 1, callback_to_submenu, FlipWorldViewLoader);
+    generic_switch_to_view(app, "Fetching World Pack..", _fetch_worlds, _parse_worlds, 1, callback_to_submenu, FlipWorldViewLoader);
 }
-static void game_settings_item_selected(void *context, uint32_t index)
+static void game_settings_select(void *context, uint32_t index)
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
     if (!app)
@@ -1459,15 +1618,25 @@ static void game_settings_item_selected(void *context, uint32_t index)
     }
     switch (index)
     {
-    case 0: // Download all world data s one huge json
-        flip_world_switch_to_view_get_worlds(app);
-    case 1: // Change FPS
+    case 0: // Download all world data as one huge json
+        switch_to_view_get_worlds(app);
+    case 1: // Player Sprite
+        break;
+    case 2: // Change FPS
+        break;
+    case 3: // VGM X
+        break;
+    case 4: // VGM Y
+        break;
+    case 5: // Screen Always On
+        break;
+    case 6: // Sound On
         break;
-    case 2: // Screen Always On
+    case 7: // Vibration On
         break;
     }
 }
-static void user_settings_item_selected(void *context, uint32_t index)
+static void user_settings_select(void *context, uint32_t index)
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
     if (!app)
@@ -1498,17 +1667,17 @@ static void user_settings_item_selected(void *context, uint32_t index)
     }
 }
 
-static void flip_world_widget_set_text(char *message, Widget **widget)
+static void widget_set_text(char *message, Widget **widget)
 {
     if (widget == NULL)
     {
-        FURI_LOG_E(TAG, "flip_world_set_widget_text - widget is NULL");
+        FURI_LOG_E(TAG, "set_widget_text - widget is NULL");
         DEV_CRASH();
         return;
     }
     if (message == NULL)
     {
-        FURI_LOG_E(TAG, "flip_world_set_widget_text - message is NULL");
+        FURI_LOG_E(TAG, "set_widget_text - message is NULL");
         DEV_CRASH();
         return;
     }
@@ -1601,11 +1770,11 @@ static void flip_world_widget_set_text(char *message, Widget **widget)
     widget_add_text_scroll_element(*widget, 0, 0, 128, 64, formatted_message);
 }
 
-void flip_world_loader_draw_callback(Canvas *canvas, void *model)
+void loader_draw_callback(Canvas *canvas, void *model)
 {
     if (!canvas || !model)
     {
-        FURI_LOG_E(TAG, "flip_world_loader_draw_callback - canvas or model is NULL");
+        FURI_LOG_E(TAG, "loader_draw_callback - canvas or model is NULL");
         return;
     }
 
@@ -1629,7 +1798,7 @@ void flip_world_loader_draw_callback(Canvas *canvas, void *model)
 
     if (data_state == DataStateError || data_state == DataStateParseError)
     {
-        flip_world_request_error_draw(canvas, data_loader_model);
+        error_draw(canvas, data_loader_model);
         return;
     }
 
@@ -1666,11 +1835,11 @@ void flip_world_loader_draw_callback(Canvas *canvas, void *model)
     }
 }
 
-static void flip_world_loader_process_callback(void *context)
+static void loader_process_callback(void *context)
 {
     if (context == NULL)
     {
-        FURI_LOG_E(TAG, "flip_world_loader_process_callback - context is NULL");
+        FURI_LOG_E(TAG, "loader_process_callback - context is NULL");
         DEV_CRASH();
         return;
     }
@@ -1721,15 +1890,15 @@ static void flip_world_loader_process_callback(void *context)
             {
                 FURI_LOG_DEV(TAG, "PONG received.");
             }
-            else if (strncmp(loader_model->fhttp->last_response, "[SUCCESS]", 9) == 0)
+            else if (strncmp(loader_model->fhttp->last_response, "[SUCCESS]", 9))
             {
                 FURI_LOG_DEV(TAG, "SUCCESS received. %s", loader_model->fhttp->last_response ? loader_model->fhttp->last_response : "NULL");
             }
-            else if (strncmp(loader_model->fhttp->last_response, "[ERROR]", 9) == 0)
+            else if (strncmp(loader_model->fhttp->last_response, "[ERROR]", 9))
             {
                 FURI_LOG_DEV(TAG, "ERROR received. %s", loader_model->fhttp->last_response ? loader_model->fhttp->last_response : "NULL");
             }
-            else if (strlen(loader_model->fhttp->last_response) == 0)
+            else if (strlen(loader_model->fhttp->last_response))
             {
                 // Still waiting on response
             }
@@ -1798,7 +1967,7 @@ static void flip_world_loader_process_callback(void *context)
                 }
                 else
                 {
-                    flip_world_widget_set_text(model->data_text != NULL ? model->data_text : "", &app->widget_result);
+                    widget_set_text(model->data_text != NULL ? model->data_text : "", &app->widget_result);
                     if (model->data_text != NULL)
                     {
                         free(model->data_text);
@@ -1812,11 +1981,11 @@ static void flip_world_loader_process_callback(void *context)
     }
 }
 
-static void flip_world_loader_timer_callback(void *context)
+static void loader_timer_callback(void *context)
 {
     if (context == NULL)
     {
-        FURI_LOG_E(TAG, "flip_world_loader_timer_callback - context is NULL");
+        FURI_LOG_E(TAG, "loader_timer_callback - context is NULL");
         DEV_CRASH();
         return;
     }
@@ -1824,11 +1993,11 @@ static void flip_world_loader_timer_callback(void *context)
     view_dispatcher_send_custom_event(app->view_dispatcher, FlipWorldCustomEventProcess);
 }
 
-static void flip_world_loader_on_enter(void *context)
+static void loader_on_enter(void *context)
 {
     if (context == NULL)
     {
-        FURI_LOG_E(TAG, "flip_world_loader_on_enter - context is NULL");
+        FURI_LOG_E(TAG, "loader_on_enter - context is NULL");
         DEV_CRASH();
         return;
     }
@@ -1841,18 +2010,18 @@ static void flip_world_loader_on_enter(void *context)
             view_set_previous_callback(view, model->back_callback);
             if (model->timer == NULL)
             {
-                model->timer = furi_timer_alloc(flip_world_loader_timer_callback, FuriTimerTypePeriodic, app);
+                model->timer = furi_timer_alloc(loader_timer_callback, FuriTimerTypePeriodic, app);
             }
             furi_timer_start(model->timer, 250);
         },
         true);
 }
 
-static void flip_world_loader_on_exit(void *context)
+static void loader_on_exit(void *context)
 {
     if (context == NULL)
     {
-        FURI_LOG_E(TAG, "flip_world_loader_on_exit - context is NULL");
+        FURI_LOG_E(TAG, "loader_on_exit - context is NULL");
         DEV_CRASH();
         return;
     }
@@ -1870,24 +2039,24 @@ static void flip_world_loader_on_exit(void *context)
         false);
 }
 
-void flip_world_loader_init(View *view)
+void loader_init(View *view)
 {
     if (view == NULL)
     {
-        FURI_LOG_E(TAG, "flip_world_loader_init - view is NULL");
+        FURI_LOG_E(TAG, "loader_init - view is NULL");
         DEV_CRASH();
         return;
     }
     view_allocate_model(view, ViewModelTypeLocking, sizeof(DataLoaderModel));
-    view_set_enter_callback(view, flip_world_loader_on_enter);
-    view_set_exit_callback(view, flip_world_loader_on_exit);
+    view_set_enter_callback(view, loader_on_enter);
+    view_set_exit_callback(view, loader_on_exit);
 }
 
-void flip_world_loader_free_model(View *view)
+void loader_free_model(View *view)
 {
     if (view == NULL)
     {
-        FURI_LOG_E(TAG, "flip_world_loader_free_model - view is NULL");
+        FURI_LOG_E(TAG, "loader_free_model - view is NULL");
         DEV_CRASH();
         return;
     }
@@ -1915,11 +2084,11 @@ void flip_world_loader_free_model(View *view)
         false);
 }
 
-bool flip_world_custom_event_callback(void *context, uint32_t index)
+bool custom_event_callback(void *context, uint32_t index)
 {
     if (context == NULL)
     {
-        FURI_LOG_E(TAG, "flip_world_custom_event_callback - context is NULL");
+        FURI_LOG_E(TAG, "custom_event_callback - context is NULL");
         DEV_CRASH();
         return false;
     }
@@ -1927,19 +2096,19 @@ bool flip_world_custom_event_callback(void *context, uint32_t index)
     switch (index)
     {
     case FlipWorldCustomEventProcess:
-        flip_world_loader_process_callback(context);
+        loader_process_callback(context);
         return true;
     default:
-        FURI_LOG_DEV(TAG, "flip_world_custom_event_callback. Unknown index: %ld", index);
+        FURI_LOG_DEV(TAG, "custom_event_callback. Unknown index: %ld", index);
         return false;
     }
 }
 
-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)
 {
     if (app == NULL)
     {
-        FURI_LOG_E(TAG, "flip_world_generic_switch_to_view - app is NULL");
+        FURI_LOG_E(TAG, "generic_switch_to_view - app is NULL");
         DEV_CRASH();
         return;
     }
@@ -1947,7 +2116,7 @@ void flip_world_generic_switch_to_view(FlipWorldApp *app, char *title, DataLoade
     View *view = app->view_loader;
     if (view == NULL)
     {
-        FURI_LOG_E(TAG, "flip_world_generic_switch_to_view - view is NULL");
+        FURI_LOG_E(TAG, "generic_switch_to_view - view is NULL");
         DEV_CRASH();
         return;
     }

+ 5 - 5
callback/callback.h

@@ -35,11 +35,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
easy_flipper/easy_flipper.h

@@ -26,6 +26,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(

+ 13 - 6
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

+ 22 - 9
flip_world.h

@@ -6,11 +6,14 @@
 // 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 TAG "FlipWorld"
-#define VERSION 0.3
-#define VERSION_TAG "FlipWorld v0.3"
+#define VERSION 0.4
+#define VERSION_TAG TAG " " FAP_VERSION
 
 // Define the submenu items for our FlipWorld application
 typedef enum
@@ -62,6 +65,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 +78,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;
-bool is_enough_heap(size_t heap_size);
+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
flipper_http/flipper_http.h

@@ -14,6 +14,14 @@
 #include <furi_hal_serial.h>
 #include <storage/storage.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

+ 116 - 26
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);
@@ -87,16 +73,18 @@ void draw_icon_line(Canvas *canvas, Vector pos, int amount, bool horizontal, con
         }
     }
 }
-char g_temp_spawn_name[32];
+char g_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)
+void spawn_icon(GameManager *manager, Level *level, const char *icon_id, float x, float y)
 {
-    snprintf(g_temp_spawn_name, sizeof(g_temp_spawn_name), "%s", icon_id);
+    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(Level *level, const char *icon_id, float x, float y, uint8_t amount, bool horizontal)
+void spawn_icon_line(GameManager *manager, Level *level, const char *icon_id, float x, float y, uint8_t amount, bool horizontal)
 {
     for (int i = 0; i < amount; i++)
     {
@@ -108,7 +96,7 @@ void spawn_icon_line(Level *level, const char *icon_id, float x, float y, uint8_
                 break;
             }
 
-            spawn_icon(level, icon_id, x + (i * 17), y);
+            spawn_icon(manager, level, icon_id, x + (i * 17), y);
         }
         else
         {
@@ -118,7 +106,109 @@ void spawn_icon_line(Level *level, const char *icon_id, float x, float y, uint8_
                 break;
             }
 
-            spawn_icon(level, icon_id, x, y + (i * 17));
+            spawn_icon(manager, level, icon_id, x, y + (i * 17));
         }
     }
-}
+}
+
+static void draw_menu(GameManager *manager, Canvas *canvas)
+{
+    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;
+    default:
+        break;
+    }
+}
+
+void background_render(Canvas *canvas, GameManager *manager)
+{
+    if (!canvas || !manager)
+        return;
+
+    GameContext *game_context = game_manager_game_context_get(manager);
+
+    // get player position
+    Vector posi = entity_pos_get(game_context->player);
+
+    // draw username over player's head
+    draw_username(canvas, posi, game_context->player_context->username);
+
+    // draw switch world icon
+    if (game_context->is_switching_level)
+    {
+        canvas_draw_icon(
+            canvas,
+            0,
+            0,
+            &I_icon_world_change_128x64px);
+    }
+
+    // draw menu
+    if (game_context->is_menu_open)
+    {
+        draw_menu(manager, canvas);
+    }
+};

+ 5 - 5
game/draw.h

@@ -1,15 +1,15 @@
 #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);
+extern char g_name[32];
+void background_render(Canvas *canvas, GameManager *manager);

+ 31 - 70
game/enemy.c

@@ -50,13 +50,11 @@ static EnemyContext *enemy_generic_alloc(
 // Free function
 static void enemy_generic_free(void *context)
 {
-    if (!context)
+    if (context)
     {
-        FURI_LOG_E("Game", "Enemy generic free: Invalid context");
-        return;
+        free(context);
+        context = NULL;
     }
-    free(context);
-    context = NULL;
     if (enemy_context_generic)
     {
         free(enemy_context_generic);
@@ -112,7 +110,6 @@ static void enemy_render(Entity *self, GameManager *manager, Canvas *canvas, voi
         return;
 
     EnemyContext *enemy_context = (EnemyContext *)context;
-    GameContext *game_context = game_manager_game_context_get(manager);
 
     // Get the position of the enemy
     Vector pos = entity_pos_get(self);
@@ -135,20 +132,16 @@ 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, EnemyContext *enemy_context, bool player_attacked)
 {
     if (!game_context || !enemy_context)
     {
@@ -158,8 +151,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)
     {
@@ -224,6 +217,7 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
         // Retrieve enemy context
         EnemyContext *enemy_context = (EnemyContext *)context;
         GameContext *game_context = game_manager_game_context_get(manager);
+        // InputState input = game_manager_input_get(manager);
         if (!enemy_context)
         {
             FURI_LOG_E("Game", "Enemy collision: EnemyContext is NULL");
@@ -262,11 +256,14 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
         }
 
         // 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;
@@ -306,8 +303,8 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
                     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_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 +322,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;
@@ -355,8 +352,8 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
                     game_context->player_context->state = PLAYER_ATTACKED;
 
                     // Bounce the player back by X units opposite their last movement direction
-                    player_pos.x -= game_context->player_context->dx * enemy_context->radius;
-                    player_pos.y -= game_context->player_context->dy * enemy_context->radius;
+                    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,37 +361,11 @@ 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;
@@ -408,7 +379,7 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
         {
             // 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;
         }
     }
 }
@@ -626,22 +597,12 @@ const EntityDescription *enemy(
 
 void spawn_enemy_json_furi(Level *level, GameManager *manager, FuriString *json)
 {
-    if (!level)
-    {
-        FURI_LOG_E("Game", "Level is NULL");
-        return;
-    }
-    if (!json)
+    if (!level || !manager || !json)
     {
-        FURI_LOG_E("Game", "JSON is NULL");
+        FURI_LOG_E("Game", "Level, GameManager, or 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 +634,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++;
     }
 

+ 0 - 12
game/enemy.h

@@ -43,16 +43,4 @@ typedef struct
     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);

+ 8 - 6
game/game.c

@@ -11,11 +11,12 @@ 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;
 
     // set all levels to NULL
     for (int i = 0; i < MAX_LEVELS; i++)
@@ -30,15 +31,16 @@ 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->level_count = 1;
                 break;
             }
-            FURI_LOG_E("Game", "No more levels to load");
             break;
         }
-        game_context->level_count++;
+        else
+        {
+            game_context->level_count++;
+        }
     }
 
     // imu
@@ -74,7 +76,7 @@ static void game_stop(void *ctx)
         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
         {

+ 59 - 75
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,57 @@ 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)
+    IconContext *ictx = (IconContext *)context;
+    if (ictx)
     {
-        FURI_LOG_E("Game", "Icon context is NULL");
-        return;
-    }
-    Vector pos = entity_pos_get(self);
+        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);
+        // Draw the icon, centered
+        canvas_draw_icon(
+            canvas,
+            pos.x - camera_x - ictx->size.x / 2,
+            pos.y - camera_y - ictx->size.y / 2,
+            ictx->icon);
+    }
 }
 
 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);
 }
@@ -96,7 +83,6 @@ static void icon_free(Entity *self, GameManager *manager, void *context)
 {
     UNUSED(self);
     UNUSED(manager);
-    UNUSED(context);
     if (context)
     {
         free(context);
@@ -124,135 +110,133 @@ 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)
+    // if (is_str(name, "earth") )
     // {
     //     return icon_generic_alloc("earth", &I_icon_earth_15x16, 15, 16);
     // }
-    // else if (strcmp(name, "home") == 0)
+    // else if (is_str(name, "home") )
     // {
     //     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)
+    // else if (is_str(name, "house_3d") )
     // {
     //     return icon_generic_alloc("house_3d", &I_icon_house_3d_34x45px, 34, 45);
     // }
-    // else if (strcmp(name, "info") == 0)
+    // else if (is_str(name, "info") )
     // {
     //     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)
+    // else if (is_str(name, "plant_fern") )
     // {
     //     return icon_generic_alloc("plant_fern", &I_icon_plant_fern_18x16px, 18, 16);
     // }
-    // else if (strcmp(name, "plant_pointy") == 0)
+    // else if (is_str(name, "plant_pointy") )
     // {
     //     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)
+    // else if (is_str(name, "tree_29x30") )
     // {
     //     return icon_generic_alloc("tree_29x30", &I_icon_tree_29x30px, 29, 30);
     // }
-    // else if (strcmp(name, "tree_48x48") == 0)
+    // else if (is_str(name, "tree_48x48") )
     // {
     //     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)
+    // else if (is_str(name, "chest_closed"))
     // {
     //     return icon_generic_alloc("chest_closed", &I_icon_chest_closed_16x13px, 16, 13);
     // }
-    // else if (strcmp(name, "chest_open") == 0)
+    // else if (is_str(name, "chest_open") )
     // {
     //     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)
+    else if (is_str(name, "lake_right"))
     {
-        // Assuming 11x31
         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);
     }

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

+ 33 - 8
game/level.c

@@ -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;
         }
 
@@ -98,19 +98,26 @@ static void set_world(Level *level, GameManager *manager, char *id)
 }
 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 +129,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;

+ 233 - 130
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;
 }
 
@@ -55,44 +72,55 @@ void player_spawn(Level *level, GameManager *manager)
     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;
+    }
+
     // 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 = PLAYER_RIGHT; // default 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");
         }
@@ -101,13 +129,12 @@ void player_spawn(Level *level, GameManager *manager)
     }
 
     // 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");
+    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 +150,52 @@ 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
 
     // Assign loaded player context to game context
-    game_context->player_context = player_context;
+    game_context->player_context = pctx;
+}
+
+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 = PLAYER_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 = PLAYER_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 = PLAYER_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 = PLAYER_UP;
     }
-    return 0;
 }
 
 static void player_update(Entity *self, GameManager *manager, void *context)
@@ -170,6 +206,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 +219,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,93 +239,181 @@ 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 = PLAYER_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 = PLAYER_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 = PLAYER_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 = PLAYER_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->user_input = GameKeyOk;
-            // furi_delay_ms(100);
+            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))
+        {
+            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
     }
     else
-    {
         player->state = PLAYER_MOVING;
-    }
-
-    // Handle back button to stop the game
-    if (input.pressed & GameKeyBack)
-    {
-        game_manager_game_stop(manager);
-    }
 }
 
 static void player_render(Entity *self, GameManager *manager, Canvas *canvas, void *context)
 {
-    UNUSED(manager);
-    if (!self || !context || !canvas)
+    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(
@@ -325,6 +422,12 @@ static void player_render(Entity *self, GameManager *manager, Canvas *canvas, vo
         pos.x - camera_x - 5, // Center the sprite horizontally
         pos.y - camera_y - 5  // Center the sprite vertically
     );
+
+    // Draw the outer bounds adjusted by camera offset
+    canvas_draw_frame(canvas, -camera_x, -camera_y, WORLD_WIDTH, WORLD_HEIGHT);
+
+    // Draw the user stats (health, xp, and level)
+    background_render(canvas, manager);
 }
 
 const EntityDescription player_desc = {
@@ -363,31 +466,31 @@ static SpriteContext *sprite_generic_alloc(const char *id, bool is_enemy, uint8_
 
 SpriteContext *get_sprite_context(const char *name)
 {
-    if (strcmp(name, "axe") == 0)
+    if (is_str(name, "axe"))
     {
         return sprite_generic_alloc("axe", false, 15, 11);
     }
-    else if (strcmp(name, "bow") == 0)
+    else if (is_str(name, "bow"))
     {
         return sprite_generic_alloc("bow", false, 13, 11);
     }
-    else if (strcmp(name, "naked") == 0)
+    else if (is_str(name, "naked"))
     {
         return sprite_generic_alloc("naked", false, 10, 10);
     }
-    else if (strcmp(name, "sword") == 0)
+    else if (is_str(name, "sword"))
     {
         return sprite_generic_alloc("sword", false, 15, 11);
     }
-    else if (strcmp(name, "cyclops") == 0)
+    else if (is_str(name, "cyclops"))
     {
         return sprite_generic_alloc("cyclops", true, 10, 11);
     }
-    else if (strcmp(name, "ghost") == 0)
+    else if (is_str(name, "ghost"))
     {
         return sprite_generic_alloc("ghost", true, 15, 15);
     }
-    else if (strcmp(name, "ogre") == 0)
+    else if (is_str(name, "ogre"))
     {
         return sprite_generic_alloc("ogre", true, 10, 13);
     }

+ 21 - 2
game/player.h

@@ -5,7 +5,7 @@
 #include "engine/sensors/imu.h"
 
 // Maximum enemies
-#define MAX_ENEMIES 2
+#define MAX_ENEMIES 10
 #define MAX_LEVELS 10
 
 typedef enum
@@ -27,6 +27,7 @@ typedef enum
 
 typedef struct
 {
+    Vector old_position;        // previous position of the player
     PlayerDirection direction;  // direction the player is facing
     PlayerState state;          // current state of the player
     Vector start_position;      // starting position of the player
@@ -46,13 +47,19 @@ typedef struct
     char username[32];          // player username
 } PlayerContext;
 
+// two screens for the game menu
+typedef enum
+{
+    GAME_MENU_INFO, // level, health, xp, etc.
+    GAME_MENU_MORE, // more settings
+} GameMenuScreen;
+
 typedef struct
 {
     PlayerContext *player_context;
     Level *levels[MAX_LEVELS];
     Entity *enemies[MAX_ENEMIES];
     Entity *player;
-    GameKey user_input;
     float fps;
     int level_count;
     int enemy_count;
@@ -60,6 +67,18 @@ typedef struct
     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;
 } GameContext;
 
 typedef struct

+ 34 - 38
game/world.c

@@ -1,14 +1,8 @@
 #include <game/world.h>
 #include <game/storage.h>
 #include <flip_storage/storage.h>
-void draw_bounds(Canvas *canvas)
-{
-    // Draw the outer bounds adjusted by camera offset
-    // we draw this last to ensure users can see the bounds
-    canvas_draw_frame(canvas, -camera_x, -camera_y, WORLD_WIDTH, WORLD_HEIGHT);
-}
 
-bool draw_json_world_furi(Level *level, const FuriString *json_data)
+bool draw_json_world_furi(GameManager *manager, Level *level, const FuriString *json_data)
 {
     if (!json_data)
     {
@@ -58,6 +52,7 @@ bool draw_json_world_furi(Level *level, const FuriString *json_data)
         {
             // Just one icon
             spawn_icon(
+                manager,
                 level,
                 furi_string_get_cstr(icon),
                 atoi(furi_string_get_cstr(x)),
@@ -67,6 +62,7 @@ bool draw_json_world_furi(Level *level, const FuriString *json_data)
         {
             bool is_horizontal = (furi_string_cmp(horizontal, "true") == 0);
             spawn_icon_line(
+                manager,
                 level,
                 furi_string_get_cstr(icon),
                 atoi(furi_string_get_cstr(x)),
@@ -87,74 +83,74 @@ bool draw_json_world_furi(Level *level, const FuriString *json_data)
     return levels_added > 0;
 }
 
-void draw_town_world(Level *level)
+void draw_town_world(GameManager *manager, Level *level)
 {
 
     // house-fence group 1
-    spawn_icon(level, "house", 164, 40);
-    spawn_icon(level, "fence", 148, 64);
-    spawn_icon(level, "fence", 164, 64);
-    spawn_icon(level, "fence_end", 180, 64);
+    spawn_icon(manager, level, "house", 164, 40);
+    spawn_icon(manager, level, "fence", 148, 64);
+    spawn_icon(manager, level, "fence", 164, 64);
+    spawn_icon(manager, level, "fence_end", 180, 64);
 
     // house-fence group 4 (the left of group 1)
-    spawn_icon(level, "house", 110, 40);
-    spawn_icon(level, "fence", 96, 64);
-    spawn_icon(level, "fence", 110, 64);
-    spawn_icon(level, "fence_end", 126, 64);
+    spawn_icon(manager, level, "house", 110, 40);
+    spawn_icon(manager, level, "fence", 96, 64);
+    spawn_icon(manager, level, "fence", 110, 64);
+    spawn_icon(manager, level, "fence_end", 126, 64);
 
     // house-fence group 5 (the left of group 4)
-    spawn_icon(level, "house", 56, 40);
-    spawn_icon(level, "fence", 40, 64);
-    spawn_icon(level, "fence", 56, 64);
-    spawn_icon(level, "fence_end", 72, 64);
+    spawn_icon(manager, level, "house", 56, 40);
+    spawn_icon(manager, level, "fence", 40, 64);
+    spawn_icon(manager, level, "fence", 56, 64);
+    spawn_icon(manager, level, "fence_end", 72, 64);
 
     // line of fences on the 8th row (using spawn_icon_line)
-    spawn_icon_line(level, "fence", 8, 96, 10, true);
+    spawn_icon_line(manager, level, "fence", 8, 96, 10, true);
 
     // plants spaced out underneath the fences
-    spawn_icon_line(level, "plant", 40, 110, 6, true);
-    spawn_icon_line(level, "flower", 40, 140, 6, true);
+    spawn_icon_line(manager, level, "plant", 40, 110, 6, true);
+    spawn_icon_line(manager, level, "flower", 40, 140, 6, true);
 
     // man and woman
-    spawn_icon(level, "man", 156, 110);
-    spawn_icon(level, "woman", 164, 110);
+    spawn_icon(manager, level, "man", 156, 110);
+    spawn_icon(manager, level, "woman", 164, 110);
 
     // lake
     // Top row
-    spawn_icon(level, "lake_top_left", 240, 62);
-    spawn_icon(level, "lake_top", 264, 57);
-    spawn_icon(level, "lake_top_right", 295, 62);
+    spawn_icon(manager, level, "lake_top_left", 240, 62);
+    spawn_icon(manager, level, "lake_top", 264, 57);
+    spawn_icon(manager, level, "lake_top_right", 295, 62);
 
     // Middle row
-    spawn_icon(level, "lake_left", 231, 84);
-    spawn_icon(level, "lake_right", 304, 84);
+    spawn_icon(manager, level, "lake_left", 231, 84);
+    spawn_icon(manager, level, "lake_right", 304, 84);
 
     // Bottom row
-    spawn_icon(level, "lake_bottom_left", 240, 115);
-    spawn_icon(level, "lake_bottom", 264, 120);
-    spawn_icon(level, "lake_bottom_right", 295, 115);
+    spawn_icon(manager, level, "lake_bottom_left", 240, 115);
+    spawn_icon(manager, level, "lake_bottom", 264, 120);
+    spawn_icon(manager, level, "lake_bottom_right", 295, 115);
 
     // Spawn two full left/up tree lines
     for (int i = 0; i < 2; i++)
     {
         // Horizontal line of 22 icons
-        spawn_icon_line(level, "tree", 5, 2 + i * 17, 22, true);
+        spawn_icon_line(manager, level, "tree", 5, 2 + i * 17, 22, true);
         // Vertical line of 11 icons
-        spawn_icon_line(level, "tree", 5 + i * 17, 2, 11, false);
+        spawn_icon_line(manager, level, "tree", 5 + i * 17, 2, 11, false);
     }
 
     // Spawn two full down tree lines
     for (int i = 9; i < 11; i++)
     {
         // Horizontal line of 22 icons
-        spawn_icon_line(level, "tree", 5, 2 + i * 17, 22, true);
+        spawn_icon_line(manager, level, "tree", 5, 2 + i * 17, 22, true);
     }
 
     // Spawn two full right tree lines
     for (int i = 20; i < 22; i++)
     {
         // Vertical line of 8 icons starting further down (y=50)
-        spawn_icon_line(level, "tree", 5 + i * 17, 50, 8, false);
+        spawn_icon_line(manager, level, "tree", 5 + i * 17, 50, 8, false);
     }
 }
 
@@ -174,7 +170,7 @@ FuriString *fetch_world(const char *name)
     }
 
     char url[256];
-    snprintf(url, sizeof(url), "https://www.flipsocial.net/api/world/v3/get/world/%s/", name);
+    snprintf(url, sizeof(url), "https://www.flipsocial.net/api/world/v4/get/world/%s/", name);
     snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s.json", name);
     fhttp->save_received_data = true;
     if (!flipper_http_get_request_with_headers(fhttp, url, "{\"Content-Type\": \"application/json\"}"))

+ 5 - 6
game/world.h

@@ -4,14 +4,13 @@
 #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);
+void draw_town_world(GameManager *manager, Level *level);
+bool draw_json_world_furi(GameManager *manager, Level *level, const FuriString *json_data);
 FuriString *fetch_world(const char *name);

+ 8 - 0
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,

+ 7 - 0
text_input/uart_text_input.c

@@ -4,6 +4,13 @@
 #include <gui/elements.h>
 #include "flip_world_icons.h"
 #include <furi.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, ...)
+//
 
 struct UART_TextInput
 {