From 48cef82988d6209987ae27fe29b72d7d5e402b3c Mon Sep 17 00:00:00 2001
From: 3gg <3gg@shellblade.net>
Date: Wed, 19 Jul 2023 08:35:00 -0700
Subject: Add sprites.

---
 gfx-iso/src/isogfx.c | 362 +++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 321 insertions(+), 41 deletions(-)

(limited to 'gfx-iso/src')

diff --git a/gfx-iso/src/isogfx.c b/gfx-iso/src/isogfx.c
index 3ed0fde..9ba1bec 100644
--- a/gfx-iso/src/isogfx.c
+++ b/gfx-iso/src/isogfx.c
@@ -13,9 +13,29 @@
 #include <stdlib.h>
 #include <string.h>
 
-/// Maximum number of tiles unless the user chooses a non-zero value.
+/// Maximum number of tiles unless the user specifies a value.
 #define DEFAULT_MAX_NUM_TILES 1024
 
+/// Maximum number of sprites unless the user specifies a value.
+#define DEFAULT_MAX_NUM_SPRITES 128
+
+/// Size of sprite sheet pool in bytes unless the user specifies a value.
+#define DEFAULT_SPRITE_SHEET_POOL_SIZE_BYTES (8 * 1024 * 1024)
+
+/// Default animation speed.
+#define ANIMATION_FPS 10
+
+/// Time between animation updates.
+#define ANIMATION_UPDATE_DELTA (1.0 / ANIMATION_FPS)
+
+typedef struct ivec2 {
+  int x, y;
+} ivec2;
+
+typedef struct vec2 {
+  double x, y;
+} vec2;
+
 // -----------------------------------------------------------------------------
 // Tile set (TS) and tile map (TM) file formats.
 // -----------------------------------------------------------------------------
@@ -69,6 +89,39 @@ static inline const Ts_Tile* ts_tileset_get_next_tile(
                           ((tile->width * tile->height - 1) * sizeof(Pixel)));
 }
 
+// -----------------------------------------------------------------------------
+// Sprite sheet file format.
+// -----------------------------------------------------------------------------
+
+/// A row of sprites in a sprite sheet.
+///
+/// Each row in a sprite sheet can have a different number of columns.
+///
+/// The pixels of the row follow a "sprite-major" order. It contains the
+/// 'sprite_width * sprite_height' pixels for the first column/sprite, then the
+/// second column/sprite, etc.
+typedef struct Ss_Row {
+  uint16_t num_cols;  /// Number of columns in this row.
+  Pixel    pixels[1]; /// Count: num_cols * sprite_width * sprite_height.
+} Ss_Row;
+
+/// Sprite sheet top-level data definition.
+///
+/// Sprite width and height are assumed constant throughout the sprite sheet.
+typedef struct Ss_SpriteSheet {
+  uint16_t sprite_width;  /// Sprite width in pixels.
+  uint16_t sprite_height; /// Sprite height in pixels.
+  uint16_t num_rows;
+  Ss_Row   rows[1]; /// Count: num_rows.
+} Ss_SpriteSheet;
+
+const Ss_Row* get_sprite_sheet_row(const Ss_SpriteSheet* sheet, int row) {
+  assert(sheet);
+  assert(row >= 0);
+  assert(row < sheet->num_rows);
+  return &sheet->rows[row];
+}
+
 // -----------------------------------------------------------------------------
 // Renderer state.
 // -----------------------------------------------------------------------------
@@ -79,34 +132,45 @@ typedef struct TileData {
   uint16_t pixels_handle; // Handle to the tile's pixels in the pixel pool.
 } TileData;
 
+// File format is already convenient for working in memory.
+typedef Ss_Row         SpriteSheetRow;
+typedef Ss_SpriteSheet SpriteSheetData;
+
+typedef struct SpriteData {
+  SpriteSheet sheet; // Handle to the sprite's sheet.
+  ivec2       position;
+  int         animation; // Current animation.
+  int         frame;     // Current frame of animation.
+} SpriteData;
+
 DEF_MEMPOOL_DYN(TilePool, TileData)
 DEF_MEM_DYN(PixelPool, Pixel)
 
+DEF_MEMPOOL_DYN(SpritePool, SpriteData)
+DEF_MEM_DYN(SpriteSheetPool, SpriteSheetData)
+
 typedef struct IsoGfx {
-  int       screen_width;
-  int       screen_height;
-  int       tile_width;
-  int       tile_height;
-  int       world_width;
-  int       world_height;
-  Tile*     world;
-  Pixel*    screen;
-  TilePool  tiles;
-  PixelPool pixels;
+  int             screen_width;
+  int             screen_height;
+  int             tile_width;
+  int             tile_height;
+  int             world_width;
+  int             world_height;
+  int             max_num_sprites;
+  int             sprite_sheet_pool_size_bytes;
+  double          last_animation_time;
+  Tile*           world;
+  Pixel*          screen;
+  TilePool        tiles;
+  PixelPool       pixels;
+  SpritePool      sprites;
+  SpriteSheetPool sheets;
 } IsoGfx;
 
 // -----------------------------------------------------------------------------
 // Math and world / tile / screen access.
 // -----------------------------------------------------------------------------
 
-typedef struct ivec2 {
-  int x, y;
-} ivec2;
-
-typedef struct vec2 {
-  double x, y;
-} vec2;
-
 static inline ivec2 ivec2_add(ivec2 a, ivec2 b) {
   return (ivec2){.x = a.x + b.x, .y = a.y + b.y};
 }
@@ -220,8 +284,15 @@ IsoGfx* isogfx_new(const IsoGfxDesc* desc) {
   iso->screen_width  = desc->screen_width;
   iso->screen_height = desc->screen_height;
 
-  const int screen_size = desc->screen_width * desc->screen_height;
+  iso->last_animation_time = 0.0;
+
+  iso->max_num_sprites = desc->max_num_sprites == 0 ? DEFAULT_MAX_NUM_SPRITES
+                                                    : desc->max_num_sprites;
+  iso->sprite_sheet_pool_size_bytes = desc->sprite_sheet_pool_size_bytes == 0
+                                          ? DEFAULT_SPRITE_SHEET_POOL_SIZE_BYTES
+                                          : desc->sprite_sheet_pool_size_bytes;
 
+  const int screen_size = desc->screen_width * desc->screen_height;
   if (!(iso->screen = calloc(screen_size, sizeof(Pixel)))) {
     goto cleanup;
   }
@@ -233,7 +304,7 @@ cleanup:
   return 0;
 }
 
-/// Destroy the world and its tile set.
+/// Destroy the world, its tile set, and the underlying pools.
 static void destroy_world(IsoGfx* iso) {
   assert(iso);
   if (iso->world) {
@@ -244,11 +315,19 @@ static void destroy_world(IsoGfx* iso) {
   mem_del(&iso->pixels);
 }
 
+/// Destroy all loaded sprites and the underlying pools.
+static void destroy_sprites(IsoGfx* iso) {
+  assert(iso);
+  mempool_del(&iso->sprites);
+  mem_del(&iso->sheets);
+}
+
 void isogfx_del(IsoGfx** pIso) {
   assert(pIso);
   IsoGfx* iso = *pIso;
   if (iso) {
     destroy_world(iso);
+    destroy_sprites(iso);
     if (iso->screen) {
       free(iso->screen);
       iso->screen = 0;
@@ -341,7 +420,7 @@ bool isogfx_load_world(IsoGfx* iso, const char* filepath) {
     // Tile set path is relative to the tile map file. Make it relative to the
     // current working directory before loading.
     char ts_path_cwd[PATH_MAX] = {0};
-    if (!make_relative_path(MAX_PATH_LENGTH, filepath, ts_path, ts_path_cwd)) {
+    if (!make_relative_path(filepath, ts_path, ts_path_cwd, PATH_MAX)) {
       goto cleanup;
     }
 
@@ -498,36 +577,199 @@ void isogfx_set_tiles(IsoGfx* iso, int x0, int y0, int x1, int y1, Tile tile) {
   }
 }
 
+bool isogfx_load_sprite_sheet(
+    IsoGfx* iso, const char* filepath, SpriteSheet* p_sheet) {
+  assert(iso);
+  assert(filepath);
+  assert(p_sheet);
+
+  bool success = false;
+
+  // Lazy initialization of sprite pools.
+  if (mempool_capacity(&iso->sprites) == 0) {
+    if (!mempool_make_dyn(
+            &iso->sprites, iso->max_num_sprites, sizeof(SpriteData))) {
+      return false;
+    }
+  }
+  if (mem_capacity(&iso->sheets) == 0) {
+    // Using a block size of 1 byte for sprite sheet data.
+    if (!mem_make_dyn(&iso->sheets, iso->sprite_sheet_pool_size_bytes, 1)) {
+      return false;
+    }
+  }
+
+  // Load sprite sheet file.
+  printf("Load sprite sheet: %s\n", filepath);
+  FILE* file = fopen(filepath, "rb");
+  if (file == NULL) {
+    goto cleanup;
+  }
+  const size_t     sheet_size = get_file_size(file);
+  SpriteSheetData* ss_sheet   = mem_alloc(&iso->sheets, sheet_size);
+  if (!ss_sheet) {
+    goto cleanup;
+  }
+  if (fread(ss_sheet, sheet_size, 1, file) != 1) {
+    goto cleanup;
+  }
+
+  *p_sheet = mem_get_chunk_handle(&iso->sheets, ss_sheet);
+  success  = true;
+
+cleanup:
+  // Pools remain initialized since client may attempt to load other sprites.
+  if (file != NULL) {
+    fclose(file);
+  }
+  if (!success) {
+    if (ss_sheet) {
+      mem_free(&iso->sheets, &ss_sheet);
+    }
+  }
+  return success;
+}
+
+Sprite isogfx_make_sprite(IsoGfx* iso, SpriteSheet sheet) {
+  assert(iso);
+
+  SpriteData* sprite = mempool_alloc(&iso->sprites);
+  assert(sprite);
+
+  sprite->sheet = sheet;
+
+  return mempool_get_block_index(&iso->sprites, sprite);
+}
+
+#define with_sprite(SPRITE, BODY)                                \
+  {                                                              \
+    SpriteData* data = mempool_get_block(&iso->sprites, sprite); \
+    assert(data);                                                \
+    BODY;                                                        \
+  }
+
+void isogfx_set_sprite_position(IsoGfx* iso, Sprite sprite, int x, int y) {
+  assert(iso);
+  with_sprite(sprite, {
+    data->position.x = x;
+    data->position.y = y;
+  });
+}
+
+void isogfx_set_sprite_animation(IsoGfx* iso, Sprite sprite, int animation) {
+  assert(iso);
+  with_sprite(sprite, { data->animation = animation; });
+}
+
+void isogfx_update(IsoGfx* iso, double t) {
+  assert(iso);
+
+  // If this is the first time update() is called after initialization, just
+  // record the starting animation time.
+  if (iso->last_animation_time == 0.0) {
+    iso->last_animation_time = t;
+    return;
+  }
+
+  if ((t - iso->last_animation_time) >= ANIMATION_UPDATE_DELTA) {
+    // TODO: Consider linking animated sprites in a list so that we only walk
+    // over those here and not also the static sprites.
+    mempool_foreach(&iso->sprites, sprite, {
+      const SpriteSheetData* sheet = mem_get_chunk(&iso->sheets, sprite->sheet);
+      assert(sheet); // TODO: Make this a hard assert inside the mem/pool.
+      const SpriteSheetRow* row =
+          get_sprite_sheet_row(sheet, sprite->animation);
+      sprite->frame = (sprite->frame + 1) % row->num_cols;
+    });
+
+    iso->last_animation_time = t;
+  }
+}
+
 // -----------------------------------------------------------------------------
 // Rendering and picking.
 // -----------------------------------------------------------------------------
 
-static void draw_tile(IsoGfx* iso, ivec2 origin, Tile tile) {
+typedef struct CoordSystem {
+  ivec2 o; /// Origin.
+  ivec2 x;
+  ivec2 y;
+} CoordSystem;
+
+/// Create the basis for the isometric coordinate system with origin and vectors
+/// expressed in the Cartesian system.
+static CoordSystem make_iso_coord_system(const IsoGfx* iso) {
   assert(iso);
+  // const ivec2 o = {(iso->screen_width / 2) - (iso->tile_width / 2), 0};
+  const ivec2 o = {
+      (iso->screen_width / 2) - (iso->tile_width / 2), iso->tile_height};
+  const ivec2 x = {.x = iso->tile_width / 2, .y = iso->tile_height / 2};
+  const ivec2 y = {.x = -iso->tile_width / 2, .y = iso->tile_height / 2};
+  return (CoordSystem){o, x, y};
+}
 
-  const TileData* tile_data = mempool_get_block(&iso->tiles, tile);
-  assert(tile_data);
+static Pixel alpha_blend(Pixel src, Pixel dst) {
+  if ((src.a == 255) || (dst.a == 0)) {
+    return src;
+  }
+  const uint16_t one_minus_alpha = 255 - src.a;
+#define blend(s, d)                             \
+  (Channel)(                                    \
+      (double)((uint16_t)s * (uint16_t)src.a +  \
+               (uint16_t)d * one_minus_alpha) / \
+      255.0)
+  return (Pixel){
+      .r = blend(src.r, dst.r),
+      .g = blend(src.g, dst.g),
+      .b = blend(src.b, dst.b),
+      .a = src.a};
+}
+
+/// Draw a rectangle (tile or sprite).
+///
+/// The rectangle's bottom-left corner is mapped to the given origin. The
+/// rectangle then extends to the right and top of the origin.
+///
+/// The rectangle's pixels are assumed to be arranged in a linear, row-major
+/// fashion.
+static void draw_rect(
+    IsoGfx* iso, ivec2 origin, int rect_width, int rect_height,
+    const Pixel* pixels) {
+  assert(iso);
 
-  // Tile can exceed screen bounds, so we must clip it.
+  // Rect can exceed screen bounds, so we must clip it.
 #define max(a, b) (a > b ? a : b)
-  const int py_offset = max(0, (int)tile_data->height - origin.y);
-  origin.y            = max(0, origin.y - (int)tile_data->height);
+  const int py_offset = max(0, rect_height - origin.y);
+  origin.y            = max(0, origin.y - rect_height);
 
   // Clip along Y and X as we draw.
   for (int py = py_offset;
-       (py < tile_data->height) && (origin.y + py < iso->screen_height); ++py) {
+       (py < rect_height) && (origin.y + py < iso->screen_height); ++py) {
     const int sy = origin.y + py - py_offset;
-    for (int px = 0;
-         (px < tile_data->width) && (origin.x + px < iso->screen_width); ++px) {
-      const Pixel colour = tile_xy(iso, tile_data, px, py);
+    for (int px = 0; (px < rect_width) && (origin.x + px < iso->screen_width);
+         ++px) {
+      const Pixel colour = pixels[py * rect_width + px];
       if (colour.a > 0) {
-        const int sx                = origin.x + px;
-        *screen_xy_mut(iso, sx, sy) = colour;
+        const int   sx              = origin.x + px;
+        const Pixel dst             = screen_xy(iso, sx, sy);
+        const Pixel final           = alpha_blend(colour, dst);
+        *screen_xy_mut(iso, sx, sy) = final;
       }
     }
   }
 }
 
+static void draw_tile(IsoGfx* iso, ivec2 origin, Tile tile) {
+  assert(iso);
+
+  const TileData* tile_data = mempool_get_block(&iso->tiles, tile);
+  assert(tile_data);
+
+  const Pixel* pixels = tile_xy_const_ref(iso, tile_data, 0, 0);
+
+  draw_rect(iso, origin, tile_data->width, tile_data->height, pixels);
+}
+
 static void draw(IsoGfx* iso) {
   assert(iso);
 
@@ -536,11 +778,7 @@ static void draw(IsoGfx* iso) {
 
   memset(iso->screen, 0, W * H * sizeof(Pixel));
 
-  // const ivec2 o = {(iso->screen_width / 2) - (iso->tile_width / 2), 0};
-  const ivec2 o = {
-      (iso->screen_width / 2) - (iso->tile_width / 2), iso->tile_height};
-  const ivec2 x = {.x = iso->tile_width / 2, .y = iso->tile_height / 2};
-  const ivec2 y = {.x = -iso->tile_width / 2, .y = iso->tile_height / 2};
+  const CoordSystem iso_space = make_iso_coord_system(iso);
 
   // TODO: Culling.
   // Ex: map the screen corners to tile space to cull.
@@ -550,16 +788,58 @@ static void draw(IsoGfx* iso) {
   for (int ty = 0; ty < iso->world_height; ++ty) {
     for (int tx = 0; tx < iso->world_width; ++tx) {
       const Tile  tile = world_xy(iso, tx, ty);
-      const ivec2 so =
-          ivec2_add(o, ivec2_add(ivec2_scale(x, tx), ivec2_scale(y, ty)));
+      const ivec2 so   = ivec2_add(
+          iso_space.o,
+          ivec2_add(
+              ivec2_scale(iso_space.x, tx), ivec2_scale(iso_space.y, ty)));
       draw_tile(iso, so, tile);
     }
   }
 }
 
+static void draw_sprite(
+    IsoGfx* iso, ivec2 origin, const SpriteData* sprite,
+    const SpriteSheetData* sheet) {
+  assert(iso);
+  assert(sprite);
+  assert(sheet);
+  assert(sprite->animation >= 0);
+  assert(sprite->animation < sheet->num_rows);
+  assert(sprite->frame >= 0);
+
+  const SpriteSheetRow* ss_row = &sheet->rows[sprite->animation];
+  assert(sprite->frame < ss_row->num_cols);
+
+  const int sprite_offset =
+      sprite->frame * sheet->sprite_width * sheet->sprite_height;
+
+  const Pixel* frame = &ss_row->pixels[sprite_offset];
+
+  draw_rect(iso, origin, sheet->sprite_width, sheet->sprite_height, frame);
+}
+
+static void draw_sprites(IsoGfx* iso) {
+  assert(iso);
+
+  const CoordSystem iso_space = make_iso_coord_system(iso);
+
+  mempool_foreach(&iso->sprites, sprite, {
+    const SpriteSheetData* sheet = mem_get_chunk(&iso->sheets, sprite->sheet);
+    assert(sheet);
+
+    const ivec2 so = ivec2_add(
+        iso_space.o, ivec2_add(
+                         ivec2_scale(iso_space.x, sprite->position.x),
+                         ivec2_scale(iso_space.y, sprite->position.y)));
+
+    draw_sprite(iso, so, sprite, sheet);
+  });
+}
+
 void isogfx_render(IsoGfx* iso) {
   assert(iso);
   draw(iso);
+  draw_sprites(iso);
 }
 
 void isogfx_draw_tile(IsoGfx* iso, int x, int y, Tile tile) {
-- 
cgit v1.2.3