Explorar el Código

Merge pull request #19 from jblanked/dev_0.7

FlipWorld v0.7
JBlanked hace 10 meses
padre
commit
e15a222c68
Se han modificado 15 ficheros con 330 adiciones y 172 borrados
  1. 1 1
      alloc/alloc.c
  2. 3 3
      app.c
  3. 1 1
      application.fam
  4. BIN
      assets/01-home.png
  5. 5 0
      assets/CHANGELOG.md
  6. 76 66
      callback/callback.c
  7. 1 1
      flip_world.h
  8. 63 53
      game/draw.c
  9. 27 11
      game/enemy.c
  10. 23 9
      game/game.c
  11. 2 7
      game/level.c
  12. 11 7
      game/npc.c
  13. 114 6
      game/player.c
  14. 2 0
      game/player.h
  15. 1 7
      game/world.c

+ 1 - 1
alloc/alloc.c

@@ -60,7 +60,7 @@ FlipWorldApp *flip_world_app_alloc()
     submenu_add_item(app->submenu, "About", FlipWorldSubmenuIndexMessage, callback_submenu_choices, app);
     submenu_add_item(app->submenu, "Settings", FlipWorldSubmenuIndexSettings, callback_submenu_choices, app);
     //
-    submenu_add_item(app->submenu_game, "Story", FlipWorldSubmenuIndexStory, callback_submenu_choices, app);
+    submenu_add_item(app->submenu_game, "Tutorial", FlipWorldSubmenuIndexStory, callback_submenu_choices, app);
     submenu_add_item(app->submenu_game, "PvP", FlipWorldSubmenuIndexPvP, callback_submenu_choices, app);
     submenu_add_item(app->submenu_game, "PvE", FlipWorldSubmenuIndexPvE, callback_submenu_choices, app);
 

+ 3 - 3
app.c

@@ -44,9 +44,9 @@ int32_t flip_world_main(void *p)
         easy_flipper_dialog("FlipperHTTP Error", "Ensure your WiFi Developer\nBoard or Pico W is connected\nand the latest FlipperHTTP\nfirmware is installed.");
 
     // save app version
-    // char app_version[16];
-    // snprintf(app_version, sizeof(app_version), "%f", (double)VERSION);
-    save_char("app_version", VERSION);
+    char app_version[16];
+    snprintf(app_version, sizeof(app_version), "%f", (double)VERSION);
+    save_char("app_version", app_version);
 
     // Run the view dispatcher
     view_dispatcher_run(app->view_dispatcher);

+ 1 - 1
application.fam

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

BIN
assets/01-home.png


+ 5 - 0
assets/CHANGELOG.md

@@ -1,3 +1,8 @@
+## 0.7 (2025-03-21)
+- Sped up player movement.
+- Added a Tutorial mode.
+- Fixed transition of worlds.
+
 ## 0.6.1 (2025-03-15)
 - Switched the server backend to prepare for multiplayer in version 0.8.
 

+ 76 - 66
callback/callback.c

@@ -1179,6 +1179,80 @@ static void switch_to_view_get_game(FlipWorldApp *app)
     generic_switch_to_view(app, "Starting Game..", _fetch_game, _parse_game, 5, callback_to_submenu, FlipWorldViewLoader);
 }
 
+static void run(FlipWorldApp *app)
+{
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "FlipWorldApp is NULL");
+        return;
+    }
+    free_all_views(app, true, true);
+    if (!is_enough_heap(60000))
+    {
+        easy_flipper_dialog("Error", "Not enough heap memory.\nPlease restart your Flipper.");
+        return;
+    }
+    // check if logged in
+    if (is_logged_in() || is_logged_in_to_flip_social())
+    {
+        FlipperHTTP *fhttp = flipper_http_alloc();
+        if (!fhttp)
+        {
+            FURI_LOG_E(TAG, "Failed to allocate FlipperHTTP");
+            easy_flipper_dialog("Error", "Failed to allocate FlipperHTTP. Press BACK to return.");
+            return;
+        }
+        bool fetch_world_list_i()
+        {
+            return fetch_world_list(fhttp);
+        }
+        bool parse_world_list_i()
+        {
+            return fhttp->state != ISSUE;
+        }
+
+        bool fetch_player_stats_i()
+        {
+            return fetch_player_stats(fhttp);
+        }
+
+        if (!alloc_message_view(app, MessageStateLoading))
+        {
+            FURI_LOG_E(TAG, "Failed to allocate message view");
+            return;
+        }
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewMessage);
+
+        // Make the request
+        if (!flipper_http_process_response_async(fhttp, fetch_world_list_i, parse_world_list_i) || !flipper_http_process_response_async(fhttp, fetch_player_stats_i, set_player_context))
+        {
+            FURI_LOG_E(HTTP_TAG, "Failed to make request");
+            flipper_http_free(fhttp);
+        }
+        else
+        {
+            flipper_http_free(fhttp);
+        }
+
+        if (!alloc_submenu_settings(app))
+        {
+            FURI_LOG_E(TAG, "Failed to allocate settings view");
+            return;
+        }
+
+        if (!start_game_thread(app))
+        {
+            FURI_LOG_E(TAG, "Failed to start game thread");
+            easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return.");
+            return;
+        }
+    }
+    else
+    {
+        switch_to_view_get_game(app);
+    }
+}
+
 void callback_submenu_choices(void *context, uint32_t index)
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
@@ -1194,7 +1268,7 @@ void callback_submenu_choices(void *context, uint32_t index)
         break;
     case FlipWorldSubmenuIndexStory:
         game_mode_index = 2; // GAME_MODE_STORY
-        easy_flipper_dialog("Unavailable", "\nStory mode is not ready yet.\nPress BACK to return.");
+        run(app);
         break;
     case FlipWorldSubmenuIndexPvP:
         game_mode_index = 1; // GAME_MODE_PVP
@@ -1202,71 +1276,7 @@ void callback_submenu_choices(void *context, uint32_t index)
         break;
     case FlipWorldSubmenuIndexPvE:
         game_mode_index = 0; // GAME_MODE_PVE
-        free_all_views(app, true, true);
-        if (!is_enough_heap(60000))
-        {
-            easy_flipper_dialog("Error", "Not enough heap memory.\nPlease restart your Flipper.");
-            return;
-        }
-        // check if logged in
-        if (is_logged_in() || is_logged_in_to_flip_social())
-        {
-            FlipperHTTP *fhttp = flipper_http_alloc();
-            if (!fhttp)
-            {
-                FURI_LOG_E(TAG, "Failed to allocate FlipperHTTP");
-                easy_flipper_dialog("Error", "Failed to allocate FlipperHTTP. Press BACK to return.");
-                return;
-            }
-            bool fetch_world_list_i()
-            {
-                return fetch_world_list(fhttp);
-            }
-            bool parse_world_list_i()
-            {
-                return fhttp->state != ISSUE;
-            }
-
-            bool fetch_player_stats_i()
-            {
-                return fetch_player_stats(fhttp);
-            }
-
-            if (!alloc_message_view(app, MessageStateLoading))
-            {
-                FURI_LOG_E(TAG, "Failed to allocate message view");
-                return;
-            }
-            view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewMessage);
-
-            // Make the request
-            if (!flipper_http_process_response_async(fhttp, fetch_world_list_i, parse_world_list_i) || !flipper_http_process_response_async(fhttp, fetch_player_stats_i, set_player_context))
-            {
-                FURI_LOG_E(HTTP_TAG, "Failed to make request");
-                flipper_http_free(fhttp);
-            }
-            else
-            {
-                flipper_http_free(fhttp);
-            }
-
-            if (!alloc_submenu_settings(app))
-            {
-                FURI_LOG_E(TAG, "Failed to allocate settings view");
-                return;
-            }
-
-            if (!start_game_thread(app))
-            {
-                FURI_LOG_E(TAG, "Failed to start game thread");
-                easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return.");
-                return;
-            }
-        }
-        else
-        {
-            switch_to_view_get_game(app);
-        }
+        run(app);
         break;
     case FlipWorldSubmenuIndexMessage:
         // About menu.

+ 1 - 1
flip_world.h

@@ -15,7 +15,7 @@
 //
 
 #define TAG "FlipWorld"
-#define VERSION "0.6.1"
+#define VERSION 0.7
 #define VERSION_TAG TAG " " FAP_VERSION
 
 // Define the submenu items for our FlipWorld application

+ 63 - 53
game/draw.c

@@ -95,69 +95,79 @@ static void draw_menu(GameManager *manager, Canvas *canvas)
         0,
         &I_icon_menu_128x64px);
 
-    // draw menu options
-    switch (game_context->menu_screen)
+    if (game_context->game_mode == GAME_MODE_STORY)
     {
-    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_draw_str(canvas, 45, 15, "Tutorial");
         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)
+        canvas_draw_str(canvas, 24, 35, "Press BACK to exit");
+    }
+    else
+    {
+        // draw menu options
+        switch (game_context->menu_screen)
         {
-        case 0:
+        case GAME_MENU_INFO:
+            // draw info
             // first option is highlighted
+            char health[32];
+            char xp[32];
+            char level[32];
+            char strength[32];
+
+            snprintf(level, sizeof(level), "Level   : %ld", game_context->player_context->level);
+            snprintf(health, sizeof(health), "Health  : %ld", game_context->player_context->health);
+            snprintf(xp, sizeof(xp), "XP      : %ld", game_context->player_context->xp);
+            snprintf(strength, sizeof(strength), "Strength: %ld", game_context->player_context->strength);
+            canvas_set_font(canvas, FontPrimary);
+            canvas_draw_str(canvas, 7, 16, game_context->player_context->username);
+            canvas_set_font_custom(canvas, FONT_SIZE_SMALL);
+            canvas_draw_str(canvas, 7, 30, level);
+            canvas_draw_str(canvas, 7, 37, health);
+            canvas_draw_str(canvas, 7, 44, xp);
+            canvas_draw_str(canvas, 7, 51, strength);
+
+            // draw a box around the selected option
+            canvas_draw_frame(canvas, 80, 18, 36, 30);
+            canvas_set_font(canvas, FontPrimary);
+            canvas_draw_str(canvas, 86, 30, "Info");
+            canvas_set_font(canvas, FontSecondary);
+            canvas_draw_str(canvas, 86, 42, "More");
+            break;
+        case GAME_MENU_MORE:
+            // draw settings
+            switch (game_context->menu_selection)
+            {
+            case 0:
+                // first option is highlighted
+                break;
+            case 1:
+                // second option is highlighted
+                break;
+            default:
+                break;
+            }
+
+            canvas_set_font(canvas, FontPrimary);
+            canvas_draw_str(canvas, 7, 16, VERSION_TAG);
+            canvas_set_font_custom(canvas, FONT_SIZE_SMALL);
+            canvas_draw_str_multi(canvas, 7, 25, "Developed by\nJBlanked and Derek \nJamison. Graphics\nfrom Pr3!\n\nwww.github.com/jblanked");
+
+            // draw a box around the selected option
+            canvas_draw_frame(canvas, 80, 18, 36, 30);
+            canvas_set_font(canvas, FontSecondary);
+            canvas_draw_str(canvas, 86, 30, "Info");
+            canvas_set_font(canvas, FontPrimary);
+            canvas_draw_str(canvas, 86, 42, "More");
             break;
-        case 1:
-            // second option is highlighted
+        case GAME_MENU_NPC:
+            // draw NPC dialog
+            canvas_set_font_custom(canvas, FONT_SIZE_SMALL);
+            canvas_draw_str(canvas, 7, 16, game_context->message);
             break;
         default:
             break;
         }
-
-        canvas_set_font(canvas, FontPrimary);
-        canvas_draw_str(canvas, 7, 16, VERSION_TAG);
-        canvas_set_font_custom(canvas, FONT_SIZE_SMALL);
-        canvas_draw_str_multi(canvas, 7, 25, "Developed by\nJBlanked and Derek \nJamison. Graphics\nfrom Pr3!\n\nwww.github.com/jblanked");
-
-        // draw a box around the selected option
-        canvas_draw_frame(canvas, 80, 18, 36, 30);
-        canvas_set_font(canvas, FontSecondary);
-        canvas_draw_str(canvas, 86, 30, "Info");
-        canvas_set_font(canvas, FontPrimary);
-        canvas_draw_str(canvas, 86, 42, "More");
-        break;
-    case GAME_MENU_NPC:
-        // draw NPC dialog
-        canvas_set_font_custom(canvas, FONT_SIZE_SMALL);
-        canvas_draw_str(canvas, 7, 16, game_context->message);
-        break;
-    default:
-        break;
     }
 }
 

+ 27 - 11
game/enemy.c

@@ -95,6 +95,7 @@ static void enemy_render(Entity *self, GameManager *manager, Canvas *canvas, voi
         return;
 
     EntityContext *enemy_context = (EntityContext *)context;
+    GameContext *game_context = game_manager_game_context_get(manager);
 
     // Get the position of the enemy
     Vector pos = entity_pos_get(self);
@@ -118,17 +119,22 @@ static void enemy_render(Entity *self, GameManager *manager, Canvas *canvas, voi
         current_sprite = enemy_context->sprite_right;
     }
 
-    // Draw enemy sprite relative to camera, centered on the enemy's position
-    canvas_draw_sprite(
-        canvas,
-        current_sprite,
-        pos.x - camera_x - (enemy_context->size.x / 2),
-        pos.y - camera_y - (enemy_context->size.y / 2));
-
-    // 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);
+    // no enemies in story mode for now
+    if (game_context->game_mode != GAME_MODE_STORY || (game_context->game_mode == GAME_MODE_STORY && game_context->tutorial_step == 4))
+    {
+
+        // Draw enemy sprite relative to camera, centered on the enemy's position
+        canvas_draw_sprite(
+            canvas,
+            current_sprite,
+            pos.x - camera_x - (enemy_context->size.x / 2),
+            pos.y - camera_y - (enemy_context->size.y / 2));
+
+        // 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);
+    }
 }
 
 static void atk_notify(GameContext *game_context, EntityContext *enemy_context, bool player_attacked)
@@ -204,6 +210,11 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
     furi_check(enemy_context, "Enemy collision: EntityContext is NULL");
     GameContext *game_context = game_manager_game_context_get(manager);
     furi_check(game_context, "Enemy collision: GameContext is NULL");
+    if (game_context->game_mode == GAME_MODE_STORY && game_context->tutorial_step != 4)
+    {
+        // FURI_LOG_I("Game", "Enemy collision: No enemies in story mode");
+        return;
+    }
     // Check if the enemy collided with the player
     if (entity_description_get(other) == &player_desc)
     {
@@ -237,6 +248,11 @@ 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->last_button == GameKeyOk && !enemy_is_facing_player)
         {
+            if (game_context->game_mode == GAME_MODE_STORY && game_context->tutorial_step == 4)
+            {
+                // FURI_LOG_I("Game", "Player attacked enemy '%s'!", enemy_context->id);
+                game_context->tutorial_step++;
+            }
             // Reset last button
             game_context->last_button = -1;
 

+ 23 - 9
game/game.c

@@ -35,20 +35,34 @@ static void game_start(GameManager *game_manager, void *ctx)
     for (int i = 0; i < MAX_NPCS; i++)
         game_context->npcs[i] = NULL;
 
-    // attempt to allocate all levels
-    for (int i = 0; i < MAX_LEVELS; i++)
+    if (game_context->game_mode == GAME_MODE_PVE)
     {
-        if (!allocate_level(game_manager, i))
+        // attempt to allocate all levels
+        for (int i = 0; i < MAX_LEVELS; i++)
         {
-            if (i == 0)
+            if (!allocate_level(game_manager, i))
             {
-                game_context->levels[0] = game_manager_add_level(game_manager, training_world());
-                game_context->level_count = 1;
+                if (i == 0)
+                {
+                    game_context->levels[0] = game_manager_add_level(game_manager, training_world());
+                    game_context->level_count = 1;
+                }
+                break;
             }
-            break;
+            else
+                game_context->level_count++;
         }
-        else
-            game_context->level_count++;
+    }
+    else if (game_context->game_mode == GAME_MODE_STORY)
+    {
+        // show tutorial only for now
+        game_context->levels[0] = game_manager_add_level(game_manager, training_world());
+        game_context->level_count = 1;
+    }
+    else if (game_context->game_mode == GAME_MODE_PVP)
+    {
+        // show pvp menu
+        easy_flipper_dialog("Unavailable", "\nPvP mode is not ready yet.\nPress BACK to return.");
     }
 
     // imu

+ 2 - 7
game/level.c

@@ -182,16 +182,11 @@ static void level_start(Level *level, GameManager *manager, void *context)
         // 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);
+        game_context->icon_offset += ((game_context->icon_count / 10) / 15);
     }
     player_spawn(level, manager);
 }

+ 11 - 7
game/npc.c

@@ -86,6 +86,7 @@ static void npc_render(Entity *self, GameManager *manager, Canvas *canvas, void
         return;
 
     EntityContext *npc_context = (EntityContext *)context;
+    GameContext *game_context = game_manager_game_context_get(manager);
 
     // Get the position of the NPC
     Vector pos = entity_pos_get(self);
@@ -107,13 +108,16 @@ static void npc_render(Entity *self, GameManager *manager, Canvas *canvas, void
     {
         current_sprite = npc_context->sprite_right;
     }
-
-    // Draw NPC sprite relative to camera, centered on the NPC's position
-    canvas_draw_sprite(
-        canvas,
-        current_sprite,
-        pos.x - camera_x - (npc_context->size.x / 2),
-        pos.y - camera_y - (npc_context->size.y / 2));
+    // no NPCs in story mode for now
+    if (game_context->game_mode != GAME_MODE_STORY)
+    {
+        // Draw NPC sprite relative to camera, centered on the NPC's position
+        canvas_draw_sprite(
+            canvas,
+            current_sprite,
+            pos.x - camera_x - (npc_context->size.x / 2),
+            pos.y - camera_y - (npc_context->size.y / 2));
+    }
 }
 
 // NPC collision function

+ 114 - 6
game/player.c

@@ -11,6 +11,7 @@ static Level *next_level(GameManager *manager)
     if (!game_context)
     {
         FURI_LOG_E(TAG, "Failed to get game context");
+        game_context->is_switching_level = false;
         return NULL;
     }
     // check if there are more levels to load
@@ -22,9 +23,13 @@ static Level *next_level(GameManager *manager)
             if (!allocate_level(manager, game_context->current_level))
             {
                 FURI_LOG_E(TAG, "Failed to allocate level %d", game_context->current_level);
+                game_context->is_switching_level = false;
+                furi_delay_ms(100);
                 return NULL;
             }
         }
+        game_context->is_switching_level = false;
+        furi_delay_ms(100);
         return game_context->levels[game_context->current_level];
     }
     for (int i = game_context->current_level + 1; i < game_context->level_count; i++)
@@ -34,12 +39,18 @@ static Level *next_level(GameManager *manager)
             if (!allocate_level(manager, i))
             {
                 FURI_LOG_E(TAG, "Failed to allocate level %d", i);
+                game_context->is_switching_level = false;
+                furi_delay_ms(100);
                 return NULL;
             }
         }
         game_context->current_level = i;
+        game_context->is_switching_level = false;
+        furi_delay_ms(100);
         return game_context->levels[i];
     }
+    game_context->is_switching_level = false;
+    furi_delay_ms(100);
     return NULL;
 }
 
@@ -256,7 +267,7 @@ static void player_update(Entity *self, GameManager *manager, void *context)
 
         if (!game_context->is_menu_open)
         {
-            pos.y -= (2 + game_context->icon_offset);
+            pos.y -= (1 + game_context->icon_offset);
             player->dy = -1;
             player->direction = ENTITY_UP;
         }
@@ -277,7 +288,7 @@ static void player_update(Entity *self, GameManager *manager, void *context)
 
         if (!game_context->is_menu_open)
         {
-            pos.y += (2 + game_context->icon_offset);
+            pos.y += (1 + game_context->icon_offset);
             player->dy = 1;
             player->direction = ENTITY_DOWN;
         }
@@ -298,7 +309,7 @@ static void player_update(Entity *self, GameManager *manager, void *context)
 
         if (!game_context->is_menu_open)
         {
-            pos.x -= (2 + game_context->icon_offset);
+            pos.x -= (1 + game_context->icon_offset);
             player->dx = -1;
             player->direction = ENTITY_LEFT;
         }
@@ -321,7 +332,7 @@ static void player_update(Entity *self, GameManager *manager, void *context)
 
         if (!game_context->is_menu_open)
         {
-            pos.x += (2 + game_context->icon_offset);
+            pos.x += (1 + game_context->icon_offset);
             player->dx = 1;
             player->direction = ENTITY_RIGHT;
         }
@@ -350,6 +361,7 @@ static void player_update(Entity *self, GameManager *manager, void *context)
         {
             game_context->is_switching_level = true;
             save_player_context(player);
+            furi_delay_ms(100);
             game_manager_next_level_set(manager, next_level(manager));
             return;
         }
@@ -390,6 +402,42 @@ static void player_update(Entity *self, GameManager *manager, void *context)
         }
     }
 
+    // adjust tutorial step
+    if (game_context->game_mode == GAME_MODE_STORY)
+    {
+        switch (game_context->tutorial_step)
+        {
+        case 0:
+            if (input.held & GameKeyLeft)
+                game_context->tutorial_step++;
+            break;
+        case 1:
+            if (input.held & GameKeyRight)
+                game_context->tutorial_step++;
+            break;
+        case 2:
+            if (input.held & GameKeyUp)
+                game_context->tutorial_step++;
+            break;
+        case 3:
+            if (input.held & GameKeyDown)
+                game_context->tutorial_step++;
+            break;
+        case 5:
+            if (input.held & GameKeyOk && game_context->is_menu_open)
+                game_context->tutorial_step++;
+            break;
+        case 6:
+            if (input.held & GameKeyBack)
+                game_context->tutorial_step++;
+            break;
+        case 7:
+            if (input.held & GameKeyBack)
+                game_context->tutorial_step++;
+            break;
+        }
+    }
+
     // 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);
@@ -408,11 +456,58 @@ static void player_update(Entity *self, GameManager *manager, void *context)
         player->state = ENTITY_MOVING;
 }
 
+static void draw_tutorial(Canvas *canvas, GameManager *manager)
+{
+    GameContext *game_context = game_manager_game_context_get(manager);
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str(canvas, 45, 12, "Tutorial");
+    canvas_set_font_custom(canvas, FONT_SIZE_SMALL);
+    switch (game_context->tutorial_step)
+    {
+    case 0:
+        canvas_draw_str(canvas, 15, 20, "Press LEFT to move left");
+        break;
+    case 1:
+        canvas_draw_str(canvas, 15, 20, "Press RIGHT to move right");
+        break;
+    case 2:
+        canvas_draw_str(canvas, 15, 20, "Press UP to move up");
+        break;
+    case 3:
+        canvas_draw_str(canvas, 15, 20, "Press DOWN to move down");
+        break;
+    case 4:
+        canvas_draw_str(canvas, 0, 20, "Press OK + collide with an enemy to attack");
+        break;
+    case 5:
+        canvas_draw_str(canvas, 15, 20, "Hold OK to open the menu");
+        break;
+    case 6:
+        canvas_draw_str(canvas, 15, 20, "Press BACK to escape the menu");
+        break;
+    case 7:
+        canvas_draw_str(canvas, 15, 20, "Hold BACK to save and exit");
+        break;
+    case 8:
+        // end of tutorial so quit
+        game_context->tutorial_step = 0;
+        game_context->is_menu_open = false;
+        game_context->is_switching_level = true;
+        game_manager_game_stop(manager);
+        return;
+    default:
+        break;
+    }
+}
+
 static void player_render(Entity *self, GameManager *manager, Canvas *canvas, void *context)
 {
     if (!self || !context || !canvas || !manager)
         return;
 
+    // Get game context
+    GameContext *game_context = game_manager_game_context_get(manager);
+
     // Get player context
     PlayerContext *player = context;
 
@@ -452,8 +547,21 @@ static void player_render(Entity *self, GameManager *manager, Canvas *canvas, vo
     // Draw the outer bounds adjusted by camera offset
     canvas_draw_frame(canvas, -camera_x, -camera_y, WORLD_WIDTH, WORLD_HEIGHT);
 
-    // render background
-    background_render(canvas, manager);
+    // render tutorial
+    if (game_context->game_mode == GAME_MODE_STORY)
+    {
+        draw_tutorial(canvas, manager);
+
+        if (game_context->is_menu_open)
+        {
+            background_render(canvas, manager);
+        }
+    }
+    else
+    {
+        // render background
+        background_render(canvas, manager);
+    }
 }
 
 const EntityDescription player_desc = {

+ 2 - 0
game/player.h

@@ -102,6 +102,8 @@ typedef struct
     int icon_offset;
     //
     char message[64];
+    //
+    uint8_t tutorial_step;
 } GameContext;
 
 typedef struct

+ 1 - 7
game/world.c

@@ -103,16 +103,10 @@ static void draw_town_world(Level *level, GameManager *manager, void *context)
     }
     furi_string_free(json_data_str);
     set_world(level, manager, "shadow_woods_v5");
-    /*
-      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);
+        game_context->icon_offset += ((game_context->icon_count / 10) / 15);
     }
     player_spawn(level, manager);
 }