Procházet zdrojové kódy

add npc context and simplify entity defs

jblanked před 11 měsíci
rodič
revize
46ac84dacf
8 změnil soubory, kde provedl 519 přidání a 143 odebrání
  1. 37 37
      game/enemy.c
  2. 2 21
      game/enemy.h
  3. 21 1
      game/game.h
  4. 381 0
      game/npc.c
  5. 23 0
      game/npc.h
  6. 15 29
      game/player.c
  7. 5 20
      game/player.h
  8. 35 35
      game/storage.c

+ 37 - 37
game/enemy.c

@@ -38,10 +38,10 @@ static EnemyContext *enemy_generic_alloc(
     enemy_context_generic->strength = strength;
     enemy_context_generic->health = health;
     // Initialize other fields as needed
-    enemy_context_generic->sprite_right = NULL;         // Assign appropriate sprite
-    enemy_context_generic->sprite_left = NULL;          // Assign appropriate sprite
-    enemy_context_generic->direction = ENEMY_RIGHT;     // Default direction
-    enemy_context_generic->state = ENEMY_MOVING_TO_END; // Start in IDLE state
+    enemy_context_generic->sprite_right = NULL;          // Assign appropriate sprite
+    enemy_context_generic->sprite_left = NULL;           // Assign appropriate sprite
+    enemy_context_generic->direction = ENTITY_RIGHT;     // Default direction
+    enemy_context_generic->state = ENTITY_MOVING_TO_END; // Start in IDLE state
     // Set radius based on size, for example, average of size.x and size.y divided by 2
     enemy_context_generic->radius = (size.x + size.y) / 4.0f;
     return enemy_context_generic;
@@ -116,7 +116,7 @@ static void enemy_render(Entity *self, GameManager *manager, Canvas *canvas, voi
 
     // Choose sprite based on direction
     Sprite *current_sprite = NULL;
-    if (enemy_context->direction == ENEMY_LEFT)
+    if (enemy_context->direction == ENTITY_LEFT)
     {
         current_sprite = enemy_context->sprite_left;
     }
@@ -238,19 +238,19 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
         bool player_is_facing_enemy = false;
 
         // Determine if the enemy is facing the player
-        if ((enemy_context->direction == ENEMY_LEFT && player_pos.x < enemy_pos.x) ||
-            (enemy_context->direction == ENEMY_RIGHT && player_pos.x > enemy_pos.x) ||
-            (enemy_context->direction == ENEMY_UP && player_pos.y < enemy_pos.y) ||
-            (enemy_context->direction == ENEMY_DOWN && player_pos.y > enemy_pos.y))
+        if ((enemy_context->direction == ENTITY_LEFT && player_pos.x < enemy_pos.x) ||
+            (enemy_context->direction == ENTITY_RIGHT && player_pos.x > enemy_pos.x) ||
+            (enemy_context->direction == ENTITY_UP && player_pos.y < enemy_pos.y) ||
+            (enemy_context->direction == ENTITY_DOWN && player_pos.y > enemy_pos.y))
         {
             enemy_is_facing_player = true;
         }
 
         // Determine if the player is facing the enemy
-        if ((game_context->player_context->direction == PLAYER_LEFT && enemy_pos.x < player_pos.x) ||
-            (game_context->player_context->direction == PLAYER_RIGHT && enemy_pos.x > player_pos.x) ||
-            (game_context->player_context->direction == PLAYER_UP && enemy_pos.y < player_pos.y) ||
-            (game_context->player_context->direction == PLAYER_DOWN && enemy_pos.y > player_pos.y))
+        if ((game_context->player_context->direction == ENTITY_LEFT && enemy_pos.x < player_pos.x) ||
+            (game_context->player_context->direction == ENTITY_RIGHT && enemy_pos.x > player_pos.x) ||
+            (game_context->player_context->direction == ENTITY_UP && enemy_pos.y < player_pos.y) ||
+            (game_context->player_context->direction == ENTITY_DOWN && enemy_pos.y > player_pos.y))
         {
             player_is_facing_enemy = true;
         }
@@ -285,7 +285,7 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
                 if (enemy_context->health <= 0)
                 {
                     FURI_LOG_I("Game", "Enemy '%s' is dead.. resetting enemy position and health", enemy_context->id);
-                    enemy_context->state = ENEMY_DEAD;
+                    enemy_context->state = ENTITY_DEAD;
 
                     // Reset enemy position and health
                     enemy_context->health = 100; // this needs to be set to the enemy's max health
@@ -300,7 +300,7 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
                 else
                 {
                     FURI_LOG_I("Game", "Enemy '%s' took %f damage from player", enemy_context->id, (double)game_context->player_context->strength);
-                    enemy_context->state = ENEMY_ATTACKED;
+                    enemy_context->state = ENTITY_ATTACKED;
                     // Vector old_pos = entity_pos_get(self);
                     //  Bounce the enemy back by X units opposite their last movement direction
                     enemy_pos.x -= game_context->player_context->dx * enemy_context->radius + game_context->icon_offset;
@@ -333,7 +333,7 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
                 if (game_context->player_context->health <= 0)
                 {
                     FURI_LOG_I("Game", "Player is dead.. resetting player position and health");
-                    game_context->player_context->state = PLAYER_DEAD;
+                    game_context->player_context->state = ENTITY_DEAD;
 
                     // Reset player position and health
                     entity_pos_set(other, game_context->player_context->start_position);
@@ -349,7 +349,7 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
                 else
                 {
                     FURI_LOG_I("Game", "Player took %f damage from enemy '%s'", (double)enemy_context->strength, enemy_context->id);
-                    game_context->player_context->state = PLAYER_ATTACKED;
+                    game_context->player_context->state = ENTITY_ATTACKED;
 
                     // Bounce the player back by X units opposite their last movement direction
                     player_pos.x -= game_context->player_context->dx * enemy_context->radius + game_context->icon_offset;
@@ -372,10 +372,10 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
         }
 
         // Reset enemy's state
-        enemy_context->state = ENEMY_IDLE;
+        enemy_context->state = ENTITY_IDLE;
         enemy_context->elapsed_move_timer = 0.0f;
 
-        if (game_context->player_context->state == PLAYER_DEAD)
+        if (game_context->player_context->state == ENTITY_DEAD)
         {
             // Reset player's position and health
             entity_pos_set(other, game_context->player_context->start_position);
@@ -385,13 +385,13 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
 }
 
 // Enemy update function
-static void enemy_update(Entity *self, GameManager *manager, void *context)
+static void entity_update(Entity *self, GameManager *manager, void *context)
 {
     if (!self || !context || !manager)
         return;
 
     EnemyContext *enemy_context = (EnemyContext *)context;
-    if (!enemy_context || enemy_context->state == ENEMY_DEAD)
+    if (!enemy_context || enemy_context->state == ENTITY_DEAD)
     {
         return;
     }
@@ -410,7 +410,7 @@ static void enemy_update(Entity *self, GameManager *manager, void *context)
 
     switch (enemy_context->state)
     {
-    case ENEMY_IDLE:
+    case ENTITY_IDLE:
         // Increment the elapsed_move_timer
         enemy_context->elapsed_move_timer += delta_time;
 
@@ -422,21 +422,21 @@ static void enemy_update(Entity *self, GameManager *manager, void *context)
             if (fabs(current_pos.x - enemy_context->start_position.x) < (double)1.0 &&
                 fabs(current_pos.y - enemy_context->start_position.y) < (double)1.0)
             {
-                enemy_context->state = ENEMY_MOVING_TO_END;
+                enemy_context->state = ENTITY_MOVING_TO_END;
             }
             else
             {
-                enemy_context->state = ENEMY_MOVING_TO_START;
+                enemy_context->state = ENTITY_MOVING_TO_START;
             }
             enemy_context->elapsed_move_timer = 0.0f;
         }
         break;
 
-    case ENEMY_MOVING_TO_END:
-    case ENEMY_MOVING_TO_START:
+    case ENTITY_MOVING_TO_END:
+    case ENTITY_MOVING_TO_START:
     {
         // Determine the target position based on the current state
-        Vector target_position = (enemy_context->state == ENEMY_MOVING_TO_END) ? enemy_context->end_position : enemy_context->start_position;
+        Vector target_position = (enemy_context->state == ENTITY_MOVING_TO_END) ? enemy_context->end_position : enemy_context->start_position;
 
         // Get current position
         Vector current_pos = entity_pos_get(self);
@@ -446,23 +446,23 @@ static void enemy_update(Entity *self, GameManager *manager, void *context)
         if (current_pos.x < target_position.x)
         {
             direction_vector.x = 1.0f;
-            enemy_context->direction = ENEMY_RIGHT;
+            enemy_context->direction = ENTITY_RIGHT;
         }
         else if (current_pos.x > target_position.x)
         {
             direction_vector.x = -1.0f;
-            enemy_context->direction = ENEMY_LEFT;
+            enemy_context->direction = ENTITY_LEFT;
         }
 
         if (current_pos.y < target_position.y)
         {
             direction_vector.y = 1.0f;
-            enemy_context->direction = ENEMY_DOWN;
+            enemy_context->direction = ENTITY_DOWN;
         }
         else if (current_pos.y > target_position.y)
         {
             direction_vector.y = -1.0f;
-            enemy_context->direction = ENEMY_UP;
+            enemy_context->direction = ENTITY_UP;
         }
 
         // Normalize direction vector
@@ -500,7 +500,7 @@ static void enemy_update(Entity *self, GameManager *manager, void *context)
         // If reached the target position on both axes, transition to IDLE
         if (reached_x && reached_y)
         {
-            enemy_context->state = ENEMY_IDLE;
+            enemy_context->state = ENTITY_IDLE;
             enemy_context->elapsed_move_timer = 0.0f;
         }
     }
@@ -524,7 +524,7 @@ static void enemy_free(Entity *self, GameManager *manager, void *context)
 static const EntityDescription _generic_enemy = {
     .start = enemy_start,
     .stop = enemy_free,
-    .update = enemy_update,
+    .update = entity_update,
     .render = enemy_render,
     .collision = enemy_collision,
     .event = NULL,
@@ -575,21 +575,21 @@ const EntityDescription *enemy(
     // Set initial direction based on start and end positions
     if (start_position.x < end_position.x)
     {
-        enemy_context_generic->direction = ENEMY_RIGHT;
+        enemy_context_generic->direction = ENTITY_RIGHT;
     }
     else
     {
-        enemy_context_generic->direction = ENEMY_LEFT;
+        enemy_context_generic->direction = ENTITY_LEFT;
     }
 
     // Set initial state based on movement
     if (start_position.x != end_position.x || start_position.y != end_position.y)
     {
-        enemy_context_generic->state = ENEMY_MOVING_TO_END;
+        enemy_context_generic->state = ENTITY_MOVING_TO_END;
     }
     else
     {
-        enemy_context_generic->state = ENEMY_IDLE;
+        enemy_context_generic->state = ENTITY_IDLE;
     }
 
     return &_generic_enemy;

+ 2 - 21
game/enemy.h

@@ -2,25 +2,6 @@
 #include <game/game.h>
 #include "flip_world.h"
 
-typedef enum
-{
-    ENEMY_IDLE,
-    ENEMY_MOVING,
-    ENEMY_MOVING_TO_START,
-    ENEMY_MOVING_TO_END,
-    ENEMY_ATTACKING,
-    ENEMY_ATTACKED,
-    ENEMY_DEAD
-} EnemyState;
-
-typedef enum
-{
-    ENEMY_UP,
-    ENEMY_DOWN,
-    ENEMY_LEFT,
-    ENEMY_RIGHT
-} EnemyDirection;
-
 // EnemyContext definition
 typedef struct
 {
@@ -29,8 +10,8 @@ typedef struct
     Vector size;                // Size of the enemy
     Sprite *sprite_right;       // Enemy sprite when looking right
     Sprite *sprite_left;        // Enemy sprite when looking left
-    EnemyDirection direction;   // Direction the enemy is facing
-    EnemyState state;           // Current state of the enemy
+    EntityDirection direction;  // Direction the enemy is facing
+    EntityState state;          // Current state of the enemy
     Vector start_position;      // Start position of the enemy
     Vector end_position;        // End position of the enemy
     float move_timer;           // Timer for the enemy movement

+ 21 - 1
game/game.h

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

+ 381 - 0
game/npc.c

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

+ 23 - 0
game/npc.h

@@ -0,0 +1,23 @@
+#pragma once
+#include <game/game.h>
+#include "flip_world.h"
+
+// NPCContext definition
+typedef struct
+{
+    char id[64];               // Unique ID for the NPC type
+    int index;                 // Index for the specific NPC instance
+    Vector size;               // Size of the NPC
+    Sprite *sprite_right;      // NPC sprite when looking right
+    Sprite *sprite_left;       // NPC sprite when looking left
+    EntityDirection direction; // Direction the NPC is facing
+    EntityState state;         // Current state of the NPC
+    Vector start_position;     // Start position of the NPC
+    Vector end_position;       // End position of the NPC
+    float move_timer;          // Timer for the NPC movement
+    float elapsed_move_timer;  // Elapsed time for the NPC movement
+    float radius;              // Collision radius for the NPC
+    float speed;               // Speed of the NPC
+} NPCContext;
+
+void spawn_npc(Level *level, GameManager *manager, FuriString *json);

+ 15 - 29
game/player.c

@@ -94,7 +94,7 @@ void player_spawn(Level *level, GameManager *manager)
         // Initialize default player context
         pctx->sprite_right = game_manager_sprite_load(manager, sprite_context->right_file_name);
         pctx->sprite_left = game_manager_sprite_load(manager, sprite_context->left_file_name);
-        pctx->direction = PLAYER_RIGHT; // default direction
+        pctx->direction = ENTITY_RIGHT; // default direction
         pctx->left = false;             // default sprite direction
         pctx->health = 100;
         pctx->strength = 10;
@@ -158,7 +158,7 @@ void player_spawn(Level *level, GameManager *manager)
     pctx->max_health = 100 + ((pctx->level - 1) * 10); // 10 health per level
 
     // set the player's left sprite direction
-    pctx->left = pctx->direction == PLAYER_LEFT ? true : false;
+    pctx->left = pctx->direction == ENTITY_LEFT ? true : false;
 
     // Assign loaded player context to game context
     game_context->player_context = pctx;
@@ -180,25 +180,25 @@ static void vgm_direction(Imu *imu, PlayerContext *player, Vector *pos)
     {
         pos->x += vgm_increase(pitch, min_x);
         player->dx = 1;
-        player->direction = PLAYER_RIGHT;
+        player->direction = ENTITY_RIGHT;
     }
     else if (pitch < -min_x)
     {
         pos->x += -vgm_increase(pitch, min_x);
         player->dx = -1;
-        player->direction = PLAYER_LEFT;
+        player->direction = ENTITY_LEFT;
     }
     if (roll > min_y)
     {
         pos->y += vgm_increase(roll, min_y);
         player->dy = 1;
-        player->direction = PLAYER_DOWN;
+        player->direction = ENTITY_DOWN;
     }
     else if (roll < -min_y)
     {
         pos->y += -vgm_increase(roll, min_y);
         player->dy = -1;
-        player->direction = PLAYER_UP;
+        player->direction = ENTITY_UP;
     }
 }
 
@@ -252,7 +252,7 @@ static void player_update(Entity *self, GameManager *manager, void *context)
         {
             pos.y -= (2 + game_context->icon_offset);
             player->dy = -1;
-            player->direction = PLAYER_UP;
+            player->direction = ENTITY_UP;
         }
         else
         {
@@ -273,7 +273,7 @@ static void player_update(Entity *self, GameManager *manager, void *context)
         {
             pos.y += (2 + game_context->icon_offset);
             player->dy = 1;
-            player->direction = PLAYER_DOWN;
+            player->direction = ENTITY_DOWN;
         }
         else
         {
@@ -294,7 +294,7 @@ static void player_update(Entity *self, GameManager *manager, void *context)
         {
             pos.x -= (2 + game_context->icon_offset);
             player->dx = -1;
-            player->direction = PLAYER_LEFT;
+            player->direction = ENTITY_LEFT;
         }
         else
         {
@@ -317,7 +317,7 @@ static void player_update(Entity *self, GameManager *manager, void *context)
         {
             pos.x += (2 + game_context->icon_offset);
             player->dx = 1;
-            player->direction = PLAYER_RIGHT;
+            player->direction = ENTITY_RIGHT;
         }
         else
         {
@@ -394,10 +394,10 @@ static void player_update(Entity *self, GameManager *manager, void *context)
     {
         player->dx = prev_dx;
         player->dy = prev_dy;
-        player->state = PLAYER_IDLE;
+        player->state = ENTITY_IDLE;
     }
     else
-        player->state = PLAYER_MOVING;
+        player->state = ENTITY_MOVING;
 }
 
 static void player_render(Entity *self, GameManager *manager, Canvas *canvas, void *context)
@@ -420,11 +420,11 @@ static void player_render(Entity *self, GameManager *manager, Canvas *canvas, vo
     camera_y = CLAMP(camera_y, WORLD_HEIGHT - SCREEN_HEIGHT, 0);
 
     // if player is moving right or left, draw the corresponding sprite
-    if (player->direction == PLAYER_RIGHT || player->direction == PLAYER_LEFT)
+    if (player->direction == ENTITY_RIGHT || player->direction == ENTITY_LEFT)
     {
         canvas_draw_sprite(
             canvas,
-            player->direction == PLAYER_RIGHT ? player->sprite_right : player->sprite_left,
+            player->direction == ENTITY_RIGHT ? player->sprite_right : player->sprite_left,
             pos.x - camera_x - 5, // Center the sprite horizontally
             pos.y - camera_y - 5  // Center the sprite vertically
         );
@@ -471,7 +471,7 @@ static SpriteContext *sprite_generic_alloc(const char *id, bool is_enemy, uint8_
     ctx->height = height;
     if (!is_enemy)
     {
-        snprintf(ctx->right_file_name, sizeof(ctx->right_file_name), "player_right_%s_%dx%dpx.fxbm", id, width, height);
+        snprintf(ctx->right_file_name, sizeof(ctx->right_file_name), "eNTITY_right_%s_%dx%dpx.fxbm", id, width, height);
         snprintf(ctx->left_file_name, sizeof(ctx->left_file_name), "player_left_%s_%dx%dpx.fxbm", id, width, height);
     }
     else
@@ -485,33 +485,19 @@ static SpriteContext *sprite_generic_alloc(const char *id, bool is_enemy, uint8_
 SpriteContext *get_sprite_context(const char *name)
 {
     if (is_str(name, "axe"))
-    {
         return sprite_generic_alloc("axe", false, 15, 11);
-    }
     else if (is_str(name, "bow"))
-    {
         return sprite_generic_alloc("bow", false, 13, 11);
-    }
     else if (is_str(name, "naked"))
-    {
         return sprite_generic_alloc("naked", false, 10, 10);
-    }
     else if (is_str(name, "sword"))
-    {
         return sprite_generic_alloc("sword", false, 15, 11);
-    }
     else if (is_str(name, "cyclops"))
-    {
         return sprite_generic_alloc("cyclops", true, 10, 11);
-    }
     else if (is_str(name, "ghost"))
-    {
         return sprite_generic_alloc("ghost", true, 15, 15);
-    }
     else if (is_str(name, "ogre"))
-    {
         return sprite_generic_alloc("ogre", true, 10, 13);
-    }
 
     // If no match is found
     FURI_LOG_E("Game", "Sprite not found: %s", name);

+ 5 - 20
game/player.h

@@ -4,32 +4,15 @@
 #include <game/game.h>
 #include "engine/sensors/imu.h"
 
-// Maximum enemies
 #define MAX_ENEMIES 10
 #define MAX_LEVELS 10
-
-typedef enum
-{
-    PLAYER_IDLE,
-    PLAYER_MOVING,
-    PLAYER_ATTACKING,
-    PLAYER_ATTACKED,
-    PLAYER_DEAD,
-} PlayerState;
-
-typedef enum
-{
-    PLAYER_UP,
-    PLAYER_DOWN,
-    PLAYER_LEFT,
-    PLAYER_RIGHT
-} PlayerDirection;
+#define MAX_NPCS 10
 
 typedef struct
 {
     Vector old_position;        // previous position of the player
-    PlayerDirection direction;  // direction the player is facing
-    PlayerState state;          // current state of the player
+    EntityDirection direction;  // direction the player is facing
+    EntityState state;          // current state of the player
     Vector start_position;      // starting position of the player
     Sprite *sprite_right;       // player sprite looking right
     Sprite *sprite_left;        // player sprite looking left
@@ -60,10 +43,12 @@ typedef struct
     PlayerContext *player_context;
     Level *levels[MAX_LEVELS];
     Entity *enemies[MAX_ENEMIES];
+    Entity *npcs[MAX_NPCS];
     Entity *player;
     float fps;
     int level_count;
     int enemy_count;
+    int npc_count;
     int current_level;
     bool ended_early;
     Imu *imu;

+ 35 - 35
game/storage.c

@@ -116,16 +116,16 @@ bool save_player_context(PlayerContext *player_context)
         char direction_str[2];
         switch (player_context->direction)
         {
-        case PLAYER_UP:
+        case ENTITY_UP:
             strncpy(direction_str, "0", sizeof(direction_str));
             break;
-        case PLAYER_DOWN:
+        case ENTITY_DOWN:
             strncpy(direction_str, "1", sizeof(direction_str));
             break;
-        case PLAYER_LEFT:
+        case ENTITY_LEFT:
             strncpy(direction_str, "2", sizeof(direction_str));
             break;
-        case PLAYER_RIGHT:
+        case ENTITY_RIGHT:
         default:
             strncpy(direction_str, "3", sizeof(direction_str));
             break;
@@ -144,19 +144,19 @@ bool save_player_context(PlayerContext *player_context)
         char state_str[2];
         switch (player_context->state)
         {
-        case PLAYER_IDLE:
+        case ENTITY_IDLE:
             strncpy(state_str, "0", sizeof(state_str));
             break;
-        case PLAYER_MOVING:
+        case ENTITY_MOVING:
             strncpy(state_str, "1", sizeof(state_str));
             break;
-        case PLAYER_ATTACKING:
+        case ENTITY_ATTACKING:
             strncpy(state_str, "2", sizeof(state_str));
             break;
-        case PLAYER_ATTACKED:
+        case ENTITY_ATTACKED:
             strncpy(state_str, "3", sizeof(state_str));
             break;
-        case PLAYER_DEAD:
+        case ENTITY_DEAD:
             strncpy(state_str, "4", sizeof(state_str));
             break;
         default:
@@ -293,16 +293,16 @@ bool save_player_context_api(PlayerContext *player_context)
     furi_string_cat_str(json, "\"direction\":");
     switch (player_context->direction)
     {
-    case PLAYER_UP:
+    case ENTITY_UP:
         furi_string_cat_str(json, "\"up\",");
         break;
-    case PLAYER_DOWN:
+    case ENTITY_DOWN:
         furi_string_cat_str(json, "\"down\",");
         break;
-    case PLAYER_LEFT:
+    case ENTITY_LEFT:
         furi_string_cat_str(json, "\"left\",");
         break;
-    case PLAYER_RIGHT:
+    case ENTITY_RIGHT:
     default:
         furi_string_cat_str(json, "\"right\",");
         break;
@@ -312,19 +312,19 @@ bool save_player_context_api(PlayerContext *player_context)
     furi_string_cat_str(json, "\"state\":");
     switch (player_context->state)
     {
-    case PLAYER_IDLE:
+    case ENTITY_IDLE:
         furi_string_cat_str(json, "\"idle\",");
         break;
-    case PLAYER_MOVING:
+    case ENTITY_MOVING:
         furi_string_cat_str(json, "\"moving\",");
         break;
-    case PLAYER_ATTACKING:
+    case ENTITY_ATTACKING:
         furi_string_cat_str(json, "\"attacking\",");
         break;
-    case PLAYER_ATTACKED:
+    case ENTITY_ATTACKED:
         furi_string_cat_str(json, "\"attacked\",");
         break;
-    case PLAYER_DEAD:
+    case ENTITY_DEAD:
         furi_string_cat_str(json, "\"dead\",");
         break;
     default:
@@ -635,63 +635,63 @@ bool load_player_context(PlayerContext *player_context)
 
     // 11. Direction (enum PlayerDirection)
     {
-        int direction_int = 3; // Default to PLAYER_RIGHT
+        int direction_int = 3; // Default to ENTITY_RIGHT
         if (!load_number("player/direction", &direction_int))
         {
-            FURI_LOG_E(TAG, "No data or parse error for direction. Defaulting to PLAYER_RIGHT");
+            FURI_LOG_E(TAG, "No data or parse error for direction. Defaulting to ENTITY_RIGHT");
             direction_int = 3;
         }
 
         switch (direction_int)
         {
         case 0:
-            player_context->direction = PLAYER_UP;
+            player_context->direction = ENTITY_UP;
             break;
         case 1:
-            player_context->direction = PLAYER_DOWN;
+            player_context->direction = ENTITY_DOWN;
             break;
         case 2:
-            player_context->direction = PLAYER_LEFT;
+            player_context->direction = ENTITY_LEFT;
             break;
         case 3:
-            player_context->direction = PLAYER_RIGHT;
+            player_context->direction = ENTITY_RIGHT;
             break;
         default:
-            FURI_LOG_E(TAG, "Invalid direction value: %d. Defaulting to PLAYER_RIGHT", direction_int);
-            player_context->direction = PLAYER_RIGHT;
+            FURI_LOG_E(TAG, "Invalid direction value: %d. Defaulting to ENTITY_RIGHT", direction_int);
+            player_context->direction = ENTITY_RIGHT;
             break;
         }
     }
 
     // 12. State (enum PlayerState)
     {
-        int state_int = 0; // Default to PLAYER_IDLE
+        int state_int = 0; // Default to ENTITY_IDLE
         if (!load_number("player/state", &state_int))
         {
-            FURI_LOG_E(TAG, "No data or parse error for state. Defaulting to PLAYER_IDLE");
+            FURI_LOG_E(TAG, "No data or parse error for state. Defaulting to ENTITY_IDLE");
             state_int = 0;
         }
 
         switch (state_int)
         {
         case 0:
-            player_context->state = PLAYER_IDLE;
+            player_context->state = ENTITY_IDLE;
             break;
         case 1:
-            player_context->state = PLAYER_MOVING;
+            player_context->state = ENTITY_MOVING;
             break;
         case 2:
-            player_context->state = PLAYER_ATTACKING;
+            player_context->state = ENTITY_ATTACKING;
             break;
         case 3:
-            player_context->state = PLAYER_ATTACKED;
+            player_context->state = ENTITY_ATTACKED;
             break;
         case 4:
-            player_context->state = PLAYER_DEAD;
+            player_context->state = ENTITY_DEAD;
             break;
         default:
-            FURI_LOG_E(TAG, "Invalid state value: %d. Defaulting to PLAYER_IDLE", state_int);
-            player_context->state = PLAYER_IDLE;
+            FURI_LOG_E(TAG, "Invalid state value: %d. Defaulting to ENTITY_IDLE", state_int);
+            player_context->state = ENTITY_IDLE;
             break;
         }
     }