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

Re-organize and update settings

- add settings screen that has WiFi, Game, and User
- started the process for selecting the FPS
jblanked 1 год назад
Родитель
Сommit
d389528cc0
14 измененных файлов с 893 добавлено и 83 удалено
  1. 2 1
      alloc/alloc.c
  2. 197 45
      callback/callback.c
  3. 3 2
      callback/callback.h
  4. 12 10
      engine/engine.h
  5. 2 0
      flip_world.c
  6. 25 25
      flip_world.h
  7. 104 0
      game/draw.c
  8. 13 0
      game/draw.h
  9. 180 0
      game/game.c
  10. 20 0
      game/game.h
  11. 80 0
      game/icon.c
  12. 13 0
      game/icon.h
  13. 223 0
      game/world.c
  14. 19 0
      game/world.h

+ 2 - 1
alloc/alloc.c

@@ -33,6 +33,7 @@ FlipWorldApp *flip_world_app_alloc()
     submenu_add_item(app->submenu, "Play", FlipWorldSubmenuIndexRun, callback_submenu_choices, app);
     submenu_add_item(app->submenu, "About", FlipWorldSubmenuIndexAbout, callback_submenu_choices, app);
     submenu_add_item(app->submenu, "Settings", FlipWorldSubmenuIndexSettings, callback_submenu_choices, app);
+    //
 
     // Switch to the main view
     view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu);
@@ -56,7 +57,7 @@ void flip_world_app_free(FlipWorldApp *app)
         submenu_free(app->submenu);
     }
 
-    free_all_views(app, true);
+    free_all_views(app, true, true);
 
     // free the view dispatcher
     view_dispatcher_free(app->view_dispatcher);

+ 197 - 45
callback/callback.c

@@ -9,7 +9,7 @@ static void frame_cb(GameEngine *engine, Canvas *canvas, InputState input, void
     game_manager_render(game_manager, canvas);
 }
 
-int32_t game_app(void *p)
+static int32_t game_app(void *p)
 {
     UNUSED(p);
     GameManager *game_manager = game_manager_alloc();
@@ -65,18 +65,26 @@ int32_t game_app(void *p)
 
 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);
+static bool alloc_variable_item_list(void *context, uint32_t view_id);
 //
-static void settings_item_selected(void *context, uint32_t index);
+static void wifi_settings_item_selected(void *context, uint32_t index);
 static void text_updated_ssid(void *context);
 static void text_updated_pass(void *context);
+//
+static void flip_world_game_fps_change(VariableItem *item);
+static void game_settings_item_selected(void *context, uint32_t index);
 
-static uint32_t callback_to_submenu(void *context)
+uint32_t callback_to_submenu(void *context)
 {
     UNUSED(context);
     return FlipWorldViewSubmenu;
 }
 static uint32_t callback_to_wifi_settings(void *context)
+{
+    UNUSED(context);
+    return FlipWorldViewVariableItemList;
+}
+static uint32_t callback_to_settings(void *context)
 {
     UNUSED(context);
     return FlipWorldViewSettings;
@@ -182,7 +190,7 @@ static bool alloc_text_input_view(void *context, char *title)
     }
     return true;
 }
-static bool alloc_variable_item_list(void *context)
+static bool alloc_variable_item_list(void *context, uint32_t view_id)
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
     if (!app)
@@ -192,31 +200,97 @@ static bool alloc_variable_item_list(void *context)
     }
     if (!app->variable_item_list)
     {
-        if (!easy_flipper_set_variable_item_list(&app->variable_item_list, FlipWorldViewSettings, settings_item_selected, callback_to_submenu, &app->view_dispatcher, app))
-            return false;
+        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))
+            {
+                FURI_LOG_E(TAG, "Failed to allocate variable item list");
+                return false;
+            }
 
-        if (!app->variable_item_list)
-            return false;
+            if (!app->variable_item_list)
+            {
+                FURI_LOG_E(TAG, "Variable item list is NULL");
+                return false;
+            }
 
-        if (!app->variable_item_ssid)
-        {
-            app->variable_item_ssid = variable_item_list_add(app->variable_item_list, "SSID", 0, NULL, NULL);
-            variable_item_set_current_value_text(app->variable_item_ssid, "");
+            if (!app->variable_item_wifi_ssid)
+            {
+                app->variable_item_wifi_ssid = variable_item_list_add(app->variable_item_list, "SSID", 0, NULL, NULL);
+                variable_item_set_current_value_text(app->variable_item_wifi_ssid, "");
+            }
+            if (!app->variable_item_wifi_pass)
+            {
+                app->variable_item_wifi_pass = variable_item_list_add(app->variable_item_list, "Password", 0, NULL, NULL);
+                variable_item_set_current_value_text(app->variable_item_wifi_pass, "");
+            }
+            char ssid[64];
+            char pass[64];
+            if (load_settings(ssid, sizeof(ssid), pass, sizeof(pass)))
+            {
+                variable_item_set_current_value_text(app->variable_item_wifi_ssid, ssid);
+                // variable_item_set_current_value_text(app->variable_item_wifi_pass, pass);
+                save_char("WiFi-SSID", ssid);
+                save_char("WiFi-Password", pass);
+            }
+            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))
+            {
+                FURI_LOG_E(TAG, "Failed to allocate variable item list");
+                return false;
+            }
+
+            if (!app->variable_item_list)
+            {
+                FURI_LOG_E(TAG, "Variable item list is NULL");
+                return false;
+            }
+
+            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);
+                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]);
+            }
+            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]);
+                variable_item_set_current_value_index(app->variable_item_game_fps, index);
+                snprintf(game_fps, 8, "%s", _game_fps);
+            }
+            break;
         }
-        if (!app->variable_item_pass)
+    }
+    return true;
+}
+static bool alloc_submenu_settings(void *context)
+{
+    FlipWorldApp *app = (FlipWorldApp *)context;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "FlipWorldApp is NULL");
+        return false;
+    }
+    if (!app->submenu_settings)
+    {
+        if (!easy_flipper_set_submenu(&app->submenu_settings, FlipWorldViewSettings, "Settings", callback_to_submenu, &app->view_dispatcher))
         {
-            app->variable_item_pass = variable_item_list_add(app->variable_item_list, "Password", 0, NULL, NULL);
-            variable_item_set_current_value_text(app->variable_item_pass, "");
+            return NULL;
         }
-        char ssid[64];
-        char pass[64];
-        if (load_settings(ssid, sizeof(ssid), pass, sizeof(pass)))
+        if (!app->submenu_settings)
         {
-            variable_item_set_current_value_text(app->variable_item_ssid, ssid);
-            // variable_item_set_current_value_text(app->variable_item_pass, pass);
-            save_char("WiFi-SSID", ssid);
-            save_char("WiFi-Password", pass);
+            return false;
         }
+        submenu_add_item(app->submenu_settings, "WiFi", FlipWorldSubmenuIndexWiFiSettings, callback_submenu_choices, app);
+        submenu_add_item(app->submenu_settings, "Game", FlipWorldSubmenuIndexGameSettings, callback_submenu_choices, app);
+        submenu_add_item(app->submenu_settings, "User", FlipWorldSubmenuIndexUserSettings, callback_submenu_choices, app);
     }
     return true;
 }
@@ -287,24 +361,54 @@ static void free_variable_item_list(void *context)
     }
     if (app->variable_item_list)
     {
-        view_dispatcher_remove_view(app->view_dispatcher, FlipWorldViewSettings);
+        view_dispatcher_remove_view(app->view_dispatcher, FlipWorldViewVariableItemList);
         variable_item_list_free(app->variable_item_list);
         app->variable_item_list = NULL;
     }
-    if (app->variable_item_ssid)
+    if (app->variable_item_wifi_ssid)
     {
-        free(app->variable_item_ssid);
-        app->variable_item_ssid = NULL;
+        free(app->variable_item_wifi_ssid);
+        app->variable_item_wifi_ssid = NULL;
     }
-    if (app->variable_item_pass)
+    if (app->variable_item_wifi_pass)
     {
-        free(app->variable_item_pass);
-        app->variable_item_pass = NULL;
+        free(app->variable_item_wifi_pass);
+        app->variable_item_wifi_pass = NULL;
+    }
+    if (app->variable_item_game_fps)
+    {
+        free(app->variable_item_game_fps);
+        app->variable_item_game_fps = NULL;
+    }
+    if (app->variable_item_user_username)
+    {
+        free(app->variable_item_user_username);
+        app->variable_item_user_username = NULL;
+    }
+    if (app->variable_item_user_password)
+    {
+        free(app->variable_item_user_password);
+        app->variable_item_user_password = NULL;
+    }
+}
+static void free_submenu_settings(void *context)
+{
+    FlipWorldApp *app = (FlipWorldApp *)context;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "FlipWorldApp is NULL");
+        return;
+    }
+    if (app->submenu_settings)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipWorldViewSettings);
+        submenu_free(app->submenu_settings);
+        app->submenu_settings = NULL;
     }
 }
 static FuriThreadId thread_id;
 static bool game_thread_running = false;
-void free_all_views(void *context, bool should_free_variable_item_list)
+void free_all_views(void *context, bool should_free_variable_item_list, bool should_free_submenu_settings)
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
     if (!app)
@@ -332,6 +436,9 @@ void free_all_views(void *context, bool should_free_variable_item_list)
         furi_thread_flags_set(thread_id, WorkerEvtStop);
         furi_thread_free(thread_id);
     }
+
+    if (should_free_submenu_settings)
+        free_submenu_settings(app);
 }
 
 void callback_submenu_choices(void *context, uint32_t index)
@@ -352,7 +459,7 @@ void callback_submenu_choices(void *context, uint32_t index)
             furi_thread_flags_set(thread_id, WorkerEvtStop);
             furi_thread_free(thread_id);
         }
-        free_all_views(app, true);
+        free_all_views(app, true, true);
         if (!app->view_main)
         {
             if (!easy_flipper_set_view(&app->view_main, FlipWorldViewMain, NULL, NULL, callback_to_submenu, &app->view_dispatcher, app))
@@ -376,7 +483,7 @@ void callback_submenu_choices(void *context, uint32_t index)
         game_thread_running = true;
         break;
     case FlipWorldSubmenuIndexAbout:
-        free_all_views(app, true);
+        free_all_views(app, true, true);
         if (!alloc_about_view(app))
         {
             FURI_LOG_E(TAG, "Failed to allocate about view");
@@ -385,14 +492,35 @@ void callback_submenu_choices(void *context, uint32_t index)
         view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewAbout);
         break;
     case FlipWorldSubmenuIndexSettings:
-        free_all_views(app, true);
-        if (!alloc_variable_item_list(app))
+        free_all_views(app, true, true);
+        if (!alloc_submenu_settings(app))
         {
-            FURI_LOG_E(TAG, "Failed to allocate variable item list");
+            FURI_LOG_E(TAG, "Failed to allocate settings view");
             return;
         }
         view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSettings);
         break;
+    case FlipWorldSubmenuIndexWiFiSettings:
+        free_all_views(app, true, false);
+        if (!alloc_variable_item_list(app, FlipWorldSubmenuIndexWiFiSettings))
+        {
+            FURI_LOG_E(TAG, "Failed to allocate variable item list");
+            return;
+        }
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewVariableItemList);
+        break;
+    case FlipWorldSubmenuIndexGameSettings:
+        free_all_views(app, true, false);
+        if (!alloc_variable_item_list(app, FlipWorldSubmenuIndexGameSettings))
+        {
+            FURI_LOG_E(TAG, "Failed to allocate variable item list");
+            return;
+        }
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewVariableItemList);
+        break;
+    case FlipWorldSubmenuIndexUserSettings:
+        easy_flipper_dialog("User Settings", "Coming soon...");
+        break;
     default:
         break;
     }
@@ -417,9 +545,9 @@ static void text_updated_ssid(void *context)
     save_char("WiFi-SSID", app->text_input_buffer);
 
     // update the variable item text
-    if (app->variable_item_ssid)
+    if (app->variable_item_wifi_ssid)
     {
-        variable_item_set_current_value_text(app->variable_item_ssid, app->text_input_buffer);
+        variable_item_set_current_value_text(app->variable_item_wifi_ssid, app->text_input_buffer);
 
         // get value of password
         char pass[64];
@@ -451,7 +579,7 @@ static void text_updated_ssid(void *context)
     }
 
     // switch to the settings view
-    view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSettings);
+    view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewVariableItemList);
 }
 static void text_updated_pass(void *context)
 {
@@ -472,9 +600,9 @@ static void text_updated_pass(void *context)
     save_char("WiFi-Password", app->text_input_buffer);
 
     // update the variable item text
-    if (app->variable_item_pass)
+    if (app->variable_item_wifi_pass)
     {
-        // variable_item_set_current_value_text(app->variable_item_pass, app->text_input_buffer);
+        // variable_item_set_current_value_text(app->variable_item_wifi_pass, app->text_input_buffer);
     }
 
     // get value of ssid
@@ -506,10 +634,10 @@ static void text_updated_pass(void *context)
     }
 
     // switch to the settings view
-    view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSettings);
+    view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewVariableItemList);
 }
 
-static void settings_item_selected(void *context, uint32_t index)
+static void wifi_settings_item_selected(void *context, uint32_t index)
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
     if (!app)
@@ -522,7 +650,7 @@ static void settings_item_selected(void *context, uint32_t index)
     switch (index)
     {
     case 0: // Input SSID
-        free_all_views(app, false);
+        free_all_views(app, false, false);
         if (!alloc_text_input_view(app, "SSID"))
         {
             FURI_LOG_E(TAG, "Failed to allocate text input view");
@@ -537,7 +665,7 @@ static void settings_item_selected(void *context, uint32_t index)
         view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewTextInput);
         break;
     case 1: // Input Password
-        free_all_views(app, false);
+        free_all_views(app, false, false);
         if (!alloc_text_input_view(app, "Password"))
         {
             FURI_LOG_E(TAG, "Failed to allocate text input view");
@@ -555,4 +683,28 @@ static void settings_item_selected(void *context, uint32_t index)
         FURI_LOG_E(TAG, "Unknown configuration item index");
         break;
     }
+}
+static void flip_world_game_fps_change(VariableItem *item)
+{
+    uint8_t index = variable_item_get_current_value_index(item);
+    variable_item_set_current_value_text(item, game_fps_choices[index]);
+
+    // save the fps
+    snprintf(game_fps, 8, "%s", game_fps_choices[index]);
+    save_char("Game-FPS", game_fps);
+}
+
+static void game_settings_item_selected(void *context, uint32_t index)
+{
+    FlipWorldApp *app = (FlipWorldApp *)context;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "FlipWorldApp is NULL");
+        return;
+    }
+    switch (index)
+    {
+    case 0:    // Game FPS
+        break; // handled by flip_world_game_fps_change
+    }
 }

+ 3 - 2
callback/callback.h

@@ -10,5 +10,6 @@
 #include "engine/level_i.h"
 #include "engine/entity_i.h"
 
-void free_all_views(void *context, bool free_variable_item_list);
-void callback_submenu_choices(void *context, uint32_t index);
+void free_all_views(void *context, bool should_free_variable_item_list, bool should_free_submenu_settings);
+void callback_submenu_choices(void *context, uint32_t index);
+uint32_t callback_to_submenu(void *context);

+ 12 - 10
engine/engine.h

@@ -6,19 +6,21 @@
 #include "game_manager.h"
 
 #ifdef __cplusplus
-extern "C" {
+extern "C"
+{
 #endif
 
-typedef struct {
-    float target_fps;
-    bool show_fps;
-    bool always_backlight;
-    void (*start)(GameManager* game_manager, void* context);
-    void (*stop)(void* context);
-    size_t context_size;
-} Game;
+    typedef struct
+    {
+        float target_fps;
+        bool show_fps;
+        bool always_backlight;
+        void (*start)(GameManager *game_manager, void *context);
+        void (*stop)(void *context);
+        size_t context_size;
+    } Game;
 
-extern const Game game;
+    extern const Game game;
 
 #ifdef __cplusplus
 }

+ 2 - 0
flip_world.c

@@ -1 +1,3 @@
 #include <flip_world.h>
+char *game_fps_choices[] = {"30", "60", "120", "240"};
+char *game_fps = "60";

+ 25 - 25
flip_world.h

@@ -14,35 +14,26 @@
 #define TAG "FlipWorld"
 #define VERSION_TAG "FlipWorld v0.1"
 
-// Screen size
-#define SCREEN_WIDTH 128
-#define SCREEN_HEIGHT 64
-
-// World size (3x3)
-#define WORLD_WIDTH 384
-#define WORLD_HEIGHT 192
-
-// Maximum number of world objects
-#define MAX_WORLD_OBJECTS 100
-// Maximum number of world tokens
-#define MAX_WORLD_TOKENS 512
-
 // Define the submenu items for our FlipWorld application
 typedef enum
 {
     FlipWorldSubmenuIndexRun, // Click to run the FlipWorld application
     FlipWorldSubmenuIndexAbout,
     FlipWorldSubmenuIndexSettings,
+    FlipWorldSubmenuIndexWiFiSettings,
+    FlipWorldSubmenuIndexGameSettings,
+    FlipWorldSubmenuIndexUserSettings,
 } FlipWorldSubmenuIndex;
 
 // Define a single view for our FlipWorld application
 typedef enum
 {
-    FlipWorldViewMain,      // The main screen
-    FlipWorldViewSubmenu,   // The submenu
-    FlipWorldViewAbout,     // The about screen
-    FlipWorldViewSettings,  // The settings screen
-    FlipWorldViewTextInput, // The text input screen
+    FlipWorldViewMain,             // The main screen
+    FlipWorldViewSubmenu,          // The submenu
+    FlipWorldViewAbout,            // The about screen
+    FlipWorldViewSettings,         // The settings screen
+    FlipWorldViewVariableItemList, // The variable item list screen
+    FlipWorldViewTextInput,        // The text input screen
 } FlipWorldView;
 
 // Define a custom event for our FlipWorld application
@@ -55,18 +46,27 @@ typedef enum
 typedef struct
 {
     // necessary
-    ViewDispatcher *view_dispatcher;      // Switches between our views
-    View *view_main;                      // The game screen
-    View *view_about;                     // The about screen
-    Submenu *submenu;                     // The submenu
-    VariableItemList *variable_item_list; // The variable item list (settngs)
-    VariableItem *variable_item_ssid;     // The variable item
-    VariableItem *variable_item_pass;     // The variable item
+    ViewDispatcher *view_dispatcher;       // Switches between our views
+    View *view_main;                       // The game screen
+    View *view_about;                      // The about screen
+    Submenu *submenu;                      // The submenu
+    Submenu *submenu_settings;             // The settings submenu
+    VariableItemList *variable_item_list;  // The variable item list (settngs)
+    VariableItem *variable_item_wifi_ssid; // The variable item for WiFi SSID
+    VariableItem *variable_item_wifi_pass; // The variable item for WiFi password
+    //
+    VariableItem *variable_item_game_fps; // The variable item for Game FPS
+    //
+    VariableItem *variable_item_user_username; // The variable item for the User username
+    VariableItem *variable_item_user_password; // The variable item for the User password
 
     UART_TextInput *text_input;      // The text input
     char *text_input_buffer;         // Buffer for the text input
     char *text_input_temp_buffer;    // Temporary buffer for the text input
     uint32_t text_input_buffer_size; // Size of the text input buffer
+
 } FlipWorldApp;
 
+extern char *game_fps_choices[];
+extern char *game_fps; // The game FPS
 // TODO - Add Download world function and download world pack button

+ 104 - 0
game/draw.c

@@ -0,0 +1,104 @@
+#include <game/draw.h>
+
+// Global variables to store camera position
+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 a line of icons (16 width)
+void draw_icon_line(Canvas *canvas, Vector pos, int amount, bool horizontal, const Icon *icon)
+{
+    for (int i = 0; i < amount; i++)
+    {
+        if (horizontal)
+        {
+            // check if element is outside the world
+            if (pos.x + (i * 17) > WORLD_WIDTH)
+            {
+                break;
+            }
+
+            canvas_draw_icon(canvas, pos.x + (i * 17) - camera_x, pos.y - camera_y, icon);
+        }
+        else
+        {
+            // check if element is outside the world
+            if (pos.y + (i * 17) > WORLD_HEIGHT)
+            {
+                break;
+            }
+
+            canvas_draw_icon(canvas, pos.x - camera_x, pos.y + (i * 17) - camera_y, icon);
+        }
+    }
+}
+// Draw a half section of icons (16 width)
+void draw_icon_half_world(Canvas *canvas, bool right, const Icon *icon)
+{
+    for (int i = 0; i < 10; i++)
+    {
+        if (right)
+        {
+            draw_icon_line(canvas, (Vector){WORLD_WIDTH / 2 + 6, i * 19 + 2}, 11, true, icon);
+        }
+        else
+        {
+            draw_icon_line(canvas, (Vector){0, i * 19 + 2}, 11, true, icon);
+        }
+    }
+}
+// Draw an icon at a specific position (with collision detection)
+void spawn_icon(Level *level, const Icon *icon, float x, float y, uint8_t width, uint8_t height)
+{
+    Entity *e = level_add_entity(level, &icon_desc);
+    IconContext *icon_ctx = entity_context_get(e);
+    icon_ctx->icon = icon;
+    icon_ctx->width = width;
+    icon_ctx->height = height;
+    // Set the entity position to the center of the icon
+    entity_pos_set(e, (Vector){x + 8, y + 8});
+}
+// Draw a line of icons at a specific position (with collision detection)
+void spawn_icon_line(Level *level, const Icon *icon, float x, float y, uint8_t width, uint8_t height, uint8_t amount, bool horizontal)
+{
+    for (int i = 0; i < amount; i++)
+    {
+        if (horizontal)
+        {
+            // check if element is outside the world
+            if (x + (i * 17) > WORLD_WIDTH)
+            {
+                break;
+            }
+
+            spawn_icon(level, icon, x + (i * 17), y, width, height);
+        }
+        else
+        {
+            // check if element is outside the world
+            if (y + (i * 17) > WORLD_HEIGHT)
+            {
+                break;
+            }
+
+            spawn_icon(level, icon, x, y + (i * 17), width, height);
+        }
+    }
+}

+ 13 - 0
game/draw.h

@@ -0,0 +1,13 @@
+#pragma once
+#include "game/icon.h"
+
+// Global variables to store camera position
+extern int camera_x;
+extern int camera_y;
+void draw_background(Canvas *canvas, Vector pos);
+void draw_icon_line(Canvas *canvas, Vector pos, int amount, bool horizontal, const Icon *icon);
+void draw_icon_half_world(Canvas *canvas, bool right, const Icon *icon);
+void spawn_icon(Level *level, const Icon *icon, float x, float y, uint8_t width, uint8_t height);
+void spawn_icon_line(Level *level, const Icon *icon, float x, float y, uint8_t width, uint8_t height, uint8_t amount, bool horizontal);
+
+// create custom icons at https://lopaka.app/sandbox

+ 180 - 0
game/game.c

@@ -0,0 +1,180 @@
+#include "game.h"
+
+/****** Entities: Player ******/
+
+// Forward declaration of player_desc, because it's used in player_spawn function.
+
+static void player_spawn(Level *level, GameManager *manager)
+{
+    Entity *player = level_add_entity(level, &player_desc);
+
+    // Set player position.
+    // Depends on your game logic, it can be done in start entity function, but also can be done here.
+    entity_pos_set(player, (Vector){WORLD_WIDTH / 2, WORLD_HEIGHT / 2});
+
+    // Add collision box to player entity
+    // Box is centered in player x and y, and it's size is 10x10
+    entity_collider_add_rect(player, 10, 10);
+
+    // Get player context
+    PlayerContext *player_context = entity_context_get(player);
+
+    // Load player sprite
+    player_context->sprite = game_manager_sprite_load(manager, "player.fxbm");
+}
+
+// Modify player_update to track direction
+static void player_update(Entity *self, GameManager *manager, void *context)
+{
+    PlayerContext *player = (PlayerContext *)context;
+    InputState input = game_manager_input_get(manager);
+    Vector pos = entity_pos_get(self);
+
+    // Reset direction each frame
+    player->dx = 0;
+    player->dy = 0;
+
+    if (input.held & GameKeyUp)
+    {
+        pos.y -= 2;
+        player->dy = -1;
+    }
+    if (input.held & GameKeyDown)
+    {
+        pos.y += 2;
+        player->dy = 1;
+    }
+    if (input.held & GameKeyLeft)
+    {
+        pos.x -= 2;
+        player->dx = -1;
+    }
+    if (input.held & GameKeyRight)
+    {
+        pos.x += 2;
+        player->dx = 1;
+    }
+
+    pos.x = CLAMP(pos.x, WORLD_WIDTH - 5, 5);
+    pos.y = CLAMP(pos.y, WORLD_HEIGHT - 5, 5);
+
+    entity_pos_set(self, pos);
+
+    if (input.pressed & GameKeyBack)
+    {
+        game_manager_game_stop(manager);
+    }
+}
+
+static void player_render(Entity *self, GameManager *manager, Canvas *canvas, void *context)
+{
+    // Get player context
+    UNUSED(manager);
+    PlayerContext *player = context;
+
+    // Get player position
+    Vector pos = entity_pos_get(self);
+
+    // Draw background (updates camera_x and camera_y)
+    draw_background(canvas, pos);
+
+    // Draw player sprite relative to camera
+    canvas_draw_sprite(canvas, player->sprite, pos.x - camera_x - 5, pos.y - camera_y - 5);
+}
+
+const EntityDescription player_desc = {
+    .start = NULL,                         // called when entity is added to the level
+    .stop = NULL,                          // called when entity is removed from the level
+    .update = player_update,               // called every frame
+    .render = player_render,               // called every frame, after update
+    .collision = NULL,                     // called when entity collides with another entity
+    .event = NULL,                         // called when entity receives an event
+    .context_size = sizeof(PlayerContext), // size of entity context, will be automatically allocated and freed
+};
+
+/****** Level ******/
+
+static void level_alloc(Level *level, GameManager *manager, void *context)
+{
+    UNUSED(manager);
+    UNUSED(context);
+
+    // Add player entity to the level
+    player_spawn(level, manager);
+
+    draw_tree_world(level);
+    // draw_example_world(level);
+}
+
+static const LevelBehaviour level = {
+    .alloc = level_alloc, // called once, when level allocated
+    .free = NULL,         // called once, when level freed
+    .start = NULL,        // called when level is changed to this level
+    .stop = NULL,         // called when level is changed from this level
+    .context_size = 0,    // size of level context, will be automatically allocated and freed
+};
+
+/****** Game ******/
+
+/*
+    Write here the start code for your game, for example: creating a level and so on.
+    Game context is allocated (game.context_size) and passed to this function, you can use it to store your game data.
+*/
+static void game_start(GameManager *game_manager, void *ctx)
+{
+    // Do some initialization here, for example you can load score from storage.
+    // For simplicity, we will just set it to 0.
+    GameContext *game_context = ctx;
+    game_context->score = 0;
+
+    // Add level to the game
+    game_manager_add_level(game_manager, &level);
+}
+
+/*
+    Write here the stop code for your game, for example, freeing memory, if it was allocated.
+    You don't need to free level, sprites or entities, it will be done automatically.
+    Also, you don't need to free game_context, it will be done automatically, after this function.
+*/
+static void game_stop(void *ctx)
+{
+    UNUSED(ctx);
+    // GameContext *game_context = ctx;
+    //  Do some deinitialization here, for example you can save score to storage.
+    //  For simplicity, we will just print it.
+    // FURI_LOG_I("Game", "Your score: %lu", game_context->score);
+}
+float game_fps_int()
+{
+    if (strcmp(game_fps, "30") == 0)
+    {
+        return 30.0;
+    }
+    else if (strcmp(game_fps, "60") == 0)
+    {
+        return 60.0;
+    }
+    else if (strcmp(game_fps, "120") == 0)
+    {
+        return 120.0;
+    }
+    else if (strcmp(game_fps, "240") == 0)
+    {
+        return 240.0;
+    }
+    else
+    {
+        return 60.0;
+    }
+}
+/*
+    Your game configuration, do not rename this variable, but you can change its content here.
+*/
+const Game game = {
+    .target_fps = 240,                   // target fps, game will try to keep this value
+    .show_fps = false,                   // show fps counter on the screen
+    .always_backlight = true,            // keep display backlight always on
+    .start = game_start,                 // will be called once, when game starts
+    .stop = game_stop,                   // will be called once, when game stops
+    .context_size = sizeof(GameContext), // size of game context
+};

+ 20 - 0
game/game.h

@@ -0,0 +1,20 @@
+#pragma once
+#include "engine/engine.h"
+#include <game/world.h>
+#include "flip_world.h"
+
+typedef struct
+{
+    uint32_t score;
+} GameContext;
+
+typedef struct
+{
+    Vector trajectory; // Direction player would like to move.
+    float radius;      // collision radius
+    int8_t dx;         // x direction
+    int8_t dy;         // y direction
+    Sprite *sprite;    // player sprite
+} PlayerContext;
+
+extern const EntityDescription player_desc;

+ 80 - 0
game/icon.c

@@ -0,0 +1,80 @@
+#include "game/icon.h"
+const Icon *get_icon(char *name)
+{
+    if (strcmp(name, "earth") == 0)
+    {
+        return &I_icon_earth;
+    }
+    if (strcmp(name, "home") == 0)
+    {
+        return &I_icon_home;
+    }
+    if (strcmp(name, "info") == 0)
+    {
+        return &I_icon_info;
+    }
+    if (strcmp(name, "man") == 0)
+    {
+        return &I_icon_man;
+    }
+    if (strcmp(name, "plant") == 0)
+    {
+        return &I_icon_plant;
+    }
+    if (strcmp(name, "tree") == 0)
+    {
+        return &I_icon_tree;
+    }
+    if (strcmp(name, "woman") == 0)
+    {
+        return &I_icon_woman;
+    }
+    return NULL;
+}
+// Icon entity description
+
+static void icon_collision(Entity *self, Entity *other, GameManager *manager, void *context)
+{
+    UNUSED(manager);
+    UNUSED(self);
+    IconContext *icon = (IconContext *)context;
+    UNUSED(icon);
+    if (entity_description_get(other) == &player_desc)
+    {
+        PlayerContext *player = (PlayerContext *)entity_context_get(other);
+        if (player)
+        {
+            Vector pos = entity_pos_get(other);
+            // Bounce the player back by 3 units opposite their last movement direction
+            pos.x -= player->dx * 3;
+            pos.y -= player->dy * 3;
+            entity_pos_set(other, pos);
+        }
+    }
+}
+
+static void icon_render(Entity *self, GameManager *manager, Canvas *canvas, void *context)
+{
+    UNUSED(manager);
+    IconContext *icon_ctx = (IconContext *)context;
+    Vector pos = entity_pos_get(self);
+    canvas_draw_icon(canvas, pos.x - camera_x - 8, pos.y - camera_y - 8, icon_ctx->icon);
+}
+
+static void icon_start(Entity *self, GameManager *manager, void *context)
+{
+    UNUSED(manager);
+    UNUSED(context);
+    // Just add the collision rectangle for 16x16 icon
+    entity_collider_add_rect(self, 16, 16);
+}
+
+const EntityDescription icon_desc = {
+    .start = icon_start,
+    .stop = NULL,
+    .update = NULL,
+    .render = icon_render,
+    .collision = icon_collision,
+    .event = NULL,
+    .context_size = sizeof(IconContext),
+};

+ 13 - 0
game/icon.h

@@ -0,0 +1,13 @@
+#pragma once
+#include "flip_world_icons.h"
+#include "game.h"
+
+typedef struct
+{
+    const Icon *icon;
+    uint8_t width;
+    uint8_t height;
+} IconContext;
+
+extern const EntityDescription icon_desc;
+const Icon *get_icon(char *name);

+ 223 - 0
game/world.c

@@ -0,0 +1,223 @@
+#include <game/world.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(Level *level, FuriString *json_data)
+{
+    for (int i = 0; i < MAX_WORLD_OBJECTS; i++)
+    {
+        char *data = get_json_array_value("json_data", i, (char *)furi_string_get_cstr(json_data), MAX_WORLD_TOKENS);
+        if (data == NULL)
+        {
+            break;
+        }
+        char *icon = get_json_value("icon", data, 64);
+        char *x = get_json_value("x", data, 64);
+        char *y = get_json_value("y", data, 64);
+        char *width = get_json_value("width", data, 64);
+        char *height = get_json_value("height", data, 64);
+        char *amount = get_json_value("amount", data, 64);
+        char *horizontal = get_json_value("horizontal", data, 64);
+        if (icon == NULL || x == NULL || y == NULL || width == NULL || height == NULL || amount == NULL || horizontal == NULL)
+        {
+            return false;
+        }
+        // if amount is less than 2, we spawn a single icon
+        if (atoi(amount) < 2)
+        {
+            spawn_icon(level, get_icon(icon), atoi(x), atoi(y), atoi(width), atoi(height));
+            free(data);
+            free(icon);
+            free(x);
+            free(y);
+            free(width);
+            free(height);
+            free(amount);
+            free(horizontal);
+            continue;
+        }
+        spawn_icon_line(level, get_icon(icon), atoi(x), atoi(y), atoi(width), atoi(height), atoi(amount), strcmp(horizontal, "true") == 0);
+        free(data);
+        free(icon);
+        free(x);
+        free(y);
+        free(width);
+        free(height);
+        free(amount);
+        free(horizontal);
+    }
+    return true;
+}
+
+void draw_example_world(Level *level)
+{
+    spawn_icon(level, &I_icon_earth, 112, 56, 15, 16);
+    spawn_icon(level, &I_icon_home, 128, 24, 15, 16);
+    spawn_icon(level, &I_icon_info, 144, 24, 15, 16);
+    spawn_icon(level, &I_icon_man, 160, 56, 7, 16);
+    spawn_icon(level, &I_icon_woman, 168, 56, 9, 16);
+    spawn_icon(level, &I_icon_plant, 168, 32, 16, 16);
+}
+
+/* JSON of the draw_example_world with fields icon, x, y, width, height
+{
+    "name": "Example World",
+    "author": "JBlanked",
+    "json_data": [
+        {
+            "icon": "earth",
+            "x": 112,
+            "y": 56,
+            "width": 15,
+            "height": 16,
+            "amount": 1,
+            "horizontal": true
+        },
+        {
+            "icon": "home",
+            "x": 128,
+            "y": 24,
+            "width": 15,
+            "height": 16,
+            "amount": 1,
+            "horizontal": true
+        },
+        {
+            "icon": "info",
+            "x": 144,
+            "y": 24,
+            "width": 15,
+            "height": 16,
+            "amount": 1,
+            "horizontal": true
+        },
+        {
+            "icon": "man",
+            "x": 160,
+            "y": 56,
+            "width": 7,
+            "height": 16,
+            "amount": 1,
+            "horizontal": true
+        },
+        {
+            "icon": "woman",
+            "x": 168,
+            "y": 56,
+            "width": 9,
+            "height": 16,
+            "amount": 1,
+            "horizontal": true
+        },
+        {
+            "icon": "plant",
+            "x": 168,
+            "y": 32,
+            "width": 16,
+            "height": 16,
+            "amount": 1,
+            "horizontal": true
+        }
+    ]
+}
+
+*/
+
+void draw_tree_world(Level *level)
+{
+    // Spawn two full left/up tree lines
+    for (int i = 0; i < 2; i++)
+    {
+        // Horizontal line of 22 icons
+        spawn_icon_line(level, &I_icon_tree, 5, 2 + i * 17, 16, 16, 22, true);
+        // Vertical line of 11 icons
+        spawn_icon_line(level, &I_icon_tree, 5 + i * 17, 2, 16, 16, 11, false);
+    }
+
+    // Spawn two full down tree lines
+    for (int i = 9; i < 11; i++)
+    {
+        // Horizontal line of 22 icons
+        spawn_icon_line(level, &I_icon_tree, 5, 2 + i * 17, 16, 16, 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, &I_icon_tree, 5 + i * 17, 50, 16, 16, 8, false);
+    }
+
+    // Labyrinth lines
+    // Third line (14 left, then a gap, then 3 middle)
+    spawn_icon_line(level, &I_icon_tree, 5, 2 + 2 * 17, 16, 16, 14, true);
+    spawn_icon_line(level, &I_icon_tree, 5 + 16 * 17, 2 + 2 * 17, 16, 16, 3, true);
+
+    // Fourth line (3 left, 6 middle, 4 right)
+    spawn_icon_line(level, &I_icon_tree, 5, 2 + 3 * 17, 16, 16, 3, true);           // 3 left
+    spawn_icon_line(level, &I_icon_tree, 5 + 7 * 17, 2 + 3 * 17, 16, 16, 6, true);  // 6 middle
+    spawn_icon_line(level, &I_icon_tree, 5 + 15 * 17, 2 + 3 * 17, 16, 16, 4, true); // 4 right
+
+    // Fifth line (6 left, 7 middle)
+    spawn_icon_line(level, &I_icon_tree, 5, 2 + 4 * 17, 16, 16, 6, true);
+    spawn_icon_line(level, &I_icon_tree, 5 + 7 * 17, 2 + 4 * 17, 16, 16, 7, true);
+
+    // Sixth line (5 left, 3 middle, 7 right)
+    spawn_icon_line(level, &I_icon_tree, 5, 2 + 5 * 17, 16, 16, 5, true);           // 5 left
+    spawn_icon_line(level, &I_icon_tree, 5 + 7 * 17, 2 + 5 * 17, 16, 16, 3, true);  // 3 middle
+    spawn_icon_line(level, &I_icon_tree, 5 + 15 * 17, 2 + 5 * 17, 16, 16, 7, true); // 7 right
+
+    // Seventh line (0 left, 7 middle, 4 right)
+    spawn_icon_line(level, &I_icon_tree, 5 + 6 * 17, 2 + 6 * 17, 16, 16, 7, true);  // 7 middle
+    spawn_icon_line(level, &I_icon_tree, 5 + 14 * 17, 2 + 6 * 17, 16, 16, 4, true); // 4 right
+
+    // Eighth line (4 left, 3 middle, 4 right)
+    spawn_icon_line(level, &I_icon_tree, 5, 2 + 7 * 17, 16, 16, 4, true);           // 4 left
+    spawn_icon_line(level, &I_icon_tree, 5 + 7 * 17, 2 + 7 * 17, 16, 16, 3, true);  // 3 middle
+    spawn_icon_line(level, &I_icon_tree, 5 + 15 * 17, 2 + 7 * 17, 16, 16, 4, true); // 4 right
+
+    // Ninth line (3 left, 1 middle, 3 right)
+    spawn_icon_line(level, &I_icon_tree, 5, 2 + 8 * 17, 16, 16, 3, true);           // 3 left
+    spawn_icon_line(level, &I_icon_tree, 5 + 5 * 17, 2 + 8 * 17, 16, 16, 1, true);  // 1 middle
+    spawn_icon_line(level, &I_icon_tree, 5 + 11 * 17, 2 + 8 * 17, 16, 16, 3, true); // 3 right
+}
+
+/* JSON of the draw_tree_world
+{
+    "name" : "tree_world",
+    "author" : "JBlanked",
+    "json_data" : [
+        {"icon" : "tree", "x" : 5, "y" : 2, "width" : 16, "height" : 16, "amount" : 22, "horizontal" : true},
+        {"icon" : "tree", "x" : 5, "y" : 2, "width" : 16, "height" : 16, "amount" : 11, "horizontal" : false},
+        {"icon" : "tree", "x" : 22, "y" : 2, "width" : 16, "height" : 16, "amount" : 22, "horizontal" : true},
+        {"icon" : "tree", "x" : 22, "y" : 2, "width" : 16, "height" : 16, "amount" : 11, "horizontal" : false},
+        {"icon" : "tree", "x" : 5, "y" : 155, "width" : 16, "height" : 16, "amount" : 22, "horizontal" : true},
+        {"icon" : "tree", "x" : 5, "y" : 172, "width" : 16, "height" : 16, "amount" : 22, "horizontal" : true},
+        {"icon" : "tree", "x" : 345, "y" : 50, "width" : 16, "height" : 16, "amount" : 8, "horizontal" : false},
+        {"icon" : "tree", "x" : 362, "y" : 50, "width" : 16, "height" : 16, "amount" : 8, "horizontal" : false},
+        {"icon" : "tree", "x" : 5, "y" : 36, "width" : 16, "height" : 16, "amount" : 14, "horizontal" : true},
+        {"icon" : "tree", "x" : 277, "y" : 36, "width" : 16, "height" : 16, "amount" : 3, "horizontal" : true},
+        {"icon" : "tree", "x" : 5, "y" : 53, "width" : 16, "height" : 16, "amount" : 3, "horizontal" : true},
+        {"icon" : "tree", "x" : 124, "y" : 53, "width" : 16, "height" : 16, "amount" : 6, "horizontal" : true},
+        {"icon" : "tree", "x" : 260, "y" : 53, "width" : 16, "height" : 16, "amount" : 4, "horizontal" : true},
+        {"icon" : "tree", "x" : 5, "y" : 70, "width" : 16, "height" : 16, "amount" : 6, "horizontal" : true},
+        {"icon" : "tree", "x" : 124, "y" : 70, "width" : 16, "height" : 16, "amount" : 7, "horizontal" : true},
+        {"icon" : "tree", "x" : 5, "y" : 87, "width" : 16, "height" : 16, "amount" : 5, "horizontal" : true},
+        {"icon" : "tree", "x" : 124, "y" : 87, "width" : 16, "height" : 16, "amount" : 3, "horizontal" : true},
+        {"icon" : "tree", "x" : 260, "y" : 87, "width" : 16, "height" : 16, "amount" : 7, "horizontal" : true},
+        {"icon" : "tree", "x" : 107, "y" : 104, "width" : 16, "height" : 16, "amount" : 7, "horizontal" : true},
+        {"icon" : "tree", "x" : 243, "y" : 104, "width" : 16, "height" : 16, "amount" : 4, "horizontal" : true},
+        {"icon" : "tree", "x" : 5, "y" : 121, "width" : 16, "height" : 16, "amount" : 4, "horizontal" : true},
+        {"icon" : "tree", "x" : 124, "y" : 121, "width" : 16, "height" : 16, "amount" : 3, "horizontal" : true},
+        {"icon" : "tree", "x" : 260, "y" : 121, "width" : 16, "height" : 16, "amount" : 4, "horizontal" : true},
+        {"icon" : "tree", "x" : 5, "y" : 138, "width" : 16, "height" : 16, "amount" : 3, "horizontal" : true},
+        {"icon" : "tree", "x" : 90, "y" : 138, "width" : 16, "height" : 16, "amount" : 1, "horizontal" : true},
+        {"icon" : "tree", "x" : 192, "y" : 138, "width" : 16, "height" : 16, "amount" : 3, "horizontal" : true}
+    ]
+}
+*/

+ 19 - 0
game/world.h

@@ -0,0 +1,19 @@
+#pragma once
+#include <game/draw.h>
+// Screen size
+#define SCREEN_WIDTH 128
+#define SCREEN_HEIGHT 64
+
+// World size (3x3)
+#define WORLD_WIDTH 384
+#define WORLD_HEIGHT 192
+
+// Maximum number of world objects
+#define MAX_WORLD_OBJECTS 100
+
+// Maximum number of world tokens
+#define MAX_WORLD_TOKENS 512
+void draw_bounds(Canvas *canvas);
+void draw_example_world(Level *level);
+void draw_tree_world(Level *level);
+bool draw_json_world(Level *level, FuriString *json_data);