diff --git a/build.zig b/build.zig index 76a85b1..e63af2c 100644 --- a/build.zig +++ b/build.zig @@ -49,6 +49,28 @@ pub fn build(b: *std.Build) void { // utils.h is co-located with map.c map_lib.addIncludePath(b.path("libs/map")); + // Tileset library + const tileset_obj = b.addObject(.{ + .name = "tileset", + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, + }), + }); + tileset_obj.addCSourceFiles(.{ + .files = &[_][]const u8{ + "libs/tileset/tileset.c", + "libs/tileset/tileset_paint.c", + }, + .flags = &c_flags, + }); + // tileset.h includes settings.h which lives in src/ + tileset_obj.addIncludePath(b.path("src")); + // tileset.c includes tileset.h which is co-located + tileset_obj.addIncludePath(b.path("libs/tileset")); + tileset_obj.linkSystemLibrary("raylib"); + // Zig combat library. This must be compiled as an object and linked // directly to bypassing the archive step, or it yields a corrupt // archive that forces the user to clear the cache each time. @@ -98,6 +120,7 @@ pub fn build(b: *std.Build) void { exe.linkLibrary(rng_lib); exe.linkLibrary(map_lib); + exe.addObject(tileset_obj); exe.addObject(combat_obj); exe.linkSystemLibrary("raylib"); exe.linkSystemLibrary("m"); diff --git a/libs/tileset/tileset.c b/libs/tileset/tileset.c new file mode 100644 index 0000000..734899f --- /dev/null +++ b/libs/tileset/tileset.c @@ -0,0 +1,118 @@ +#include "tileset.h" +#include +#include +#include + +int tileset_init(Tileset *ts, int tile_w, int tile_h) { + if (ts == NULL) + return 0; + if (tile_w <= 0 || tile_h <= 0) + return 0; + + memset(ts, 0, sizeof(Tileset)); + + ts->tile_w = tile_w; + ts->tile_h = tile_h; + + // Compute grid dimensions to fit MAX_TILE_ID tiles + ts->atlas_cols = 4; // 4 columns + ts->atlas_rows = (MAX_TILE_ID + ts->atlas_cols - 1) / ts->atlas_cols; // round up + + int atlas_w = ts->atlas_cols * tile_w; + int atlas_h = ts->atlas_rows * tile_h; + + // Validate atlas dimensions are reasonable + if (atlas_w <= 0 || atlas_h <= 0 || atlas_w > 4096 || atlas_h > 4096) + return 0; + + ts->render_target = LoadRenderTexture(atlas_w, atlas_h); + if (!IsRenderTextureValid(ts->render_target)) + return 0; + + // Clear to transparent so unpainted regions don't show artifacts + BeginTextureMode(ts->render_target); + ClearBackground(BLANK); + EndTextureMode(); + + ts->finalized = 0; + ts->tile_count = 0; + return 1; +} + +int tileset_register(Tileset *ts, int id) { + if (ts == NULL) + return 0; + if (id < 0 || id >= MAX_TILE_ID) + return 0; + if (ts->render_target.id == 0) + return 0; + if (ts->finalized) + return 0; + if (ts->regions[id].width != 0) + return 0; // already registered + + int col = id % ts->atlas_cols; + int row = id / ts->atlas_cols; + + ts->regions[id] = + (Rectangle){(float)(col * ts->tile_w), (float)(row * ts->tile_h), (float)ts->tile_w, (float)ts->tile_h}; + ts->tile_count++; + return 1; +} + +int tileset_finalize(Tileset *ts) { + if (ts == NULL) + return 0; + if (ts->render_target.id == 0) + return 0; + if (ts->finalized) + return 1; // already finalized + + // Convert RenderTexture to regular Texture2D + // RenderTexture textures are flipped vertically in raylib, so we need to handle that + Texture2D old_texture = ts->render_target.texture; + + // Create a new texture from the render texture data + Image img = LoadImageFromTexture(old_texture); + if (img.data == NULL) { + return 0; + } + + // Flip image vertically because RenderTexture is upside-down + ImageFlipVertical(&img); + + Texture2D new_tex = LoadTextureFromImage(img); + UnloadImage(img); + + if (new_tex.id == 0) { + return 0; + } + + // Unload the old render texture and replace with the new regular texture + UnloadRenderTexture(ts->render_target); + ts->render_target.id = 0; + ts->atlas = new_tex; + ts->finalized = 1; + return 1; +} + +Rectangle tileset_get_region(const Tileset *ts, int id) { + if (ts == NULL || id < 0 || id >= MAX_TILE_ID) + return (Rectangle){0, 0, 0, 0}; + if (!ts->finalized) + return (Rectangle){0, 0, 0, 0}; + return ts->regions[id]; +} + +void tileset_destroy(Tileset *ts) { + if (ts == NULL) + return; + if (ts->finalized) { + if (ts->atlas.id != 0) + UnloadTexture(ts->atlas); + } else { + if (ts->render_target.id != 0) + UnloadRenderTexture(ts->render_target); + } + memset(ts, 0, sizeof(Tileset)); +} diff --git a/libs/tileset/tileset.h b/libs/tileset/tileset.h new file mode 100644 index 0000000..9cbe4cc --- /dev/null +++ b/libs/tileset/tileset.h @@ -0,0 +1,91 @@ +#ifndef TILESET_H +#define TILESET_H + +#include "settings.h" +#include + +// Maximum number of tiles that can be registered in a single atlas +#define MAX_TILE_ID 32 + +// Tile IDs for map tiles (variants for visual variety) +#define TILE_WALL_0 0 +#define TILE_WALL_1 1 +#define TILE_FLOOR_0 2 +#define TILE_FLOOR_1 3 +#define TILE_FLOOR_2 4 +#define TILE_FLOOR_3 5 +#define TILE_STAIRS_SPRITE 6 + +// Sprite IDs for entities +#define SPRITE_PLAYER 7 +#define SPRITE_PLAYER_WALK_0 8 +#define SPRITE_PLAYER_WALK_1 9 +#define SPRITE_PLAYER_ATTACK 10 +#define SPRITE_ENEMY_GOBLIN 11 +#define SPRITE_ENEMY_GOBLIN_WALK_0 12 +#define SPRITE_ENEMY_GOBLIN_WALK_1 13 +#define SPRITE_ENEMY_GOBLIN_ATTACK 14 +#define SPRITE_ENEMY_SKELETON 15 +#define SPRITE_ENEMY_SKELETON_WALK_0 16 +#define SPRITE_ENEMY_SKELETON_WALK_1 17 +#define SPRITE_ENEMY_SKELETON_ATTACK 18 +#define SPRITE_ENEMY_ORC 19 +#define SPRITE_ENEMY_ORC_WALK_0 20 +#define SPRITE_ENEMY_ORC_WALK_1 21 +#define SPRITE_ENEMY_ORC_ATTACK 22 +#define SPRITE_ITEM_POTION 23 +#define SPRITE_ITEM_WEAPON 24 +#define SPRITE_ITEM_ARMOR 25 + +// Door tiles +#define TILE_DOOR_CLOSED_SPRITE 26 +#define TILE_DOOR_OPEN_SPRITE 27 + +// Effect/status sprites +#define SPRITE_EFFECT_BURN 28 +#define SPRITE_EFFECT_POISON 29 +#define SPRITE_EFFECT_BLOCK 30 +#define SPRITE_SLASH_EFFECT 31 + +// Total count of defined tiles +#define NUM_TILE_IDS 32 + +// Tileset encapsulates a GPU texture atlas with sub-rectangle regions per tile ID. +// The atlas is built at startup by painting into a RenderTexture, then finalized +// into a regular Texture2D for efficient drawing via DrawTexturePro. +typedef struct { + RenderTexture2D render_target; // RenderTexture for painting (valid before finalize) + Texture2D atlas; // GPU texture (valid after finalize) + int tile_w; // width of each tile in pixels + int tile_h; // height of each tile in pixels + Rectangle regions[MAX_TILE_ID]; // sub-rectangles within atlas for each tile ID + int tile_count; // number of registered tiles + int atlas_cols; // number of columns in the atlas grid + int atlas_rows; // number of rows in the atlas grid + int finalized; // 1 after tileset_finalize called, 0 otherwise +} Tileset; + +// Initialize a tileset with the given tile dimensions. +// Computes atlas grid size based on MAX_TILE_ID and allocates a RenderTexture. +// Returns 0 on failure (e.g., RenderTexture allocation failed), non-zero on success. +int tileset_init(Tileset *ts, int tile_w, int tile_h); + +// Register a tile ID with its atlas region. +// The region is computed automatically based on tile_w/tile_h and the ID index. +// Returns 0 if the ID is out of bounds or already registered, non-zero on success. +int tileset_register(Tileset *ts, int id); + +// Finalize the tileset: converts the internal RenderTexture into a regular Texture2D +// suitable for DrawTexturePro. After this call, painting functions must not be used. +// Returns 0 on failure, non-zero on success. +int tileset_finalize(Tileset *ts); + +// Get the atlas sub-rectangle for a given tile ID. +// Returns a zeroed Rectangle if the ID is invalid or not registered. +Rectangle tileset_get_region(const Tileset *ts, int id); + +// Destroy a tileset, unloading the atlas texture and zeroing the struct. +// Safe to call on a zero-initialized or already-destroyed tileset. +void tileset_destroy(Tileset *ts); + +#endif // TILESET_H diff --git a/libs/tileset/tileset_paint.c b/libs/tileset/tileset_paint.c new file mode 100644 index 0000000..0925345 --- /dev/null +++ b/libs/tileset/tileset_paint.c @@ -0,0 +1,873 @@ +#include "tileset_paint.h" +#include +#include + +// Simple LCG for deterministic noise (seeded by position) +static unsigned int lcg_seed = 0; +static void lcg_srand(unsigned int seed) { + lcg_seed = seed; +} +static unsigned int lcg_rand(void) { + lcg_seed = lcg_seed * 1103515245 + 12345; + return lcg_seed; +} + +static int lcg_rand_range(int min, int max) { + if (max <= min) + return min; + return min + (int)(lcg_rand() % (unsigned int)(max - min + 1)); +} + +// Get the RenderTexture target for painting a specific tile ID +static RenderTexture2D get_target(Tileset *ts) { + return ts->render_target; +} + +// Compute screen offset for a tile ID within the atlas +static Vector2 get_paint_offset(Tileset *ts, int id) { + int col = id % ts->atlas_cols; + int row = id / ts->atlas_cols; + return (Vector2){(float)(col * ts->tile_w), (float)(row * ts->tile_h)}; +} + +void paint_wall_tile(Tileset *ts, int id, int variant) { + if (ts == NULL || id < 0 || id >= MAX_TILE_ID) + return; + if (ts->render_target.id == 0 || ts->finalized) + return; + + Vector2 off = get_paint_offset(ts, id); + int w = ts->tile_w; + int h = ts->tile_h; + + BeginTextureMode(ts->render_target); + + // Base color is much darker for better contrast against floor + Color base = variant == 0 ? (Color){45, 42, 38, 255} : (Color){35, 32, 28, 255}; + DrawRectangle((int)off.x, (int)off.y, w, h, base); + + // Brick pattern + int brick_h = h / 3; + int brick_w = w / 2; + Color mortar = (Color){25, 22, 18, 255}; + Color brick_light = variant == 0 ? (Color){70, 65, 60, 255} : (Color){50, 47, 43, 255}; + Color brick_dark = variant == 0 ? (Color){40, 37, 33, 255} : (Color){30, 27, 24, 255}; + + for (int row = 0; row < 3; row++) { + int y = (int)off.y + row * brick_h; + int offset_x = (row % 2 == 0) ? 0 : brick_w / 2; + for (int col = -1; col < 3; col++) { + int x = (int)off.x + offset_x + col * brick_w; + if (x >= (int)off.x + w) + break; + if (x + brick_w <= (int)off.x) + continue; + + // Clip to tile bounds + int draw_x = x < (int)off.x ? (int)off.x : x; + int draw_w = brick_w; + if (draw_x + draw_w > (int)off.x + w) + draw_w = (int)off.x + w - draw_x; + + Color c = ((col + row) % 2 == 0) ? brick_light : brick_dark; + DrawRectangle(draw_x, y, draw_w, brick_h - 1, c); + } + } + + // Mortar lines + for (int row = 1; row < 3; row++) { + int y = (int)off.y + row * brick_h; + DrawLine((int)off.x, y, (int)off.x + w - 1, y, mortar); + } + + EndTextureMode(); +} + +void paint_floor_tile(Tileset *ts, int id, int variant) { + if (ts == NULL || id < 0 || id >= MAX_TILE_ID) + return; + if (ts->render_target.id == 0 || ts->finalized) + return; + + Vector2 off = get_paint_offset(ts, id); + int w = ts->tile_w; + int h = ts->tile_h; + + BeginTextureMode(ts->render_target); + + // Base stone color - lighter than walls for contrast + Color base = (Color){75, 72, 68, 255}; + DrawRectangle((int)off.x, (int)off.y, w, h, base); + + // Seeded noise based on variant + lcg_srand((unsigned int)(variant * 7919 + id * 104729)); + + // Dithered noise dots - lighter shades + int num_dots = 8 + variant * 4; + for (int i = 0; i < num_dots; i++) { + int px = (int)off.x + lcg_rand_range(1, w - 2); + int py = (int)off.y + lcg_rand_range(1, h - 2); + int shade = lcg_rand_range(0, 3); + Color c; + if (shade == 0) + c = (Color){85, 82, 78, 255}; + else if (shade == 1) + c = (Color){65, 62, 58, 255}; + else + c = (Color){90, 87, 83, 255}; + DrawPixel(px, py, c); + } + + // Occasional crack line + if (variant >= 2) { + int crack_x = (int)off.x + lcg_rand_range(2, w - 3); + int crack_y = (int)off.y + lcg_rand_range(2, h - 3); + int crack_len = lcg_rand_range(2, 4); + int crack_dir = lcg_rand_range(0, 1); // 0 = horizontal, 1 = vertical + Color crack_color = (Color){55, 52, 48, 255}; + for (int i = 0; i < crack_len; i++) { + if (crack_dir == 0) + DrawPixel(crack_x + i, crack_y, crack_color); + else + DrawPixel(crack_x, crack_y + i, crack_color); + } + } + + EndTextureMode(); +} + +void paint_stairs_tile(Tileset *ts, int id) { + if (ts == NULL || id < 0 || id >= MAX_TILE_ID) + return; + if (ts->render_target.id == 0 || ts->finalized) + return; + + Vector2 off = get_paint_offset(ts, id); + int w = ts->tile_w; + int h = ts->tile_h; + + BeginTextureMode(ts->render_target); + + // Dark stone background + DrawRectangle((int)off.x, (int)off.y, w, h, (Color){40, 38, 35, 255}); + + // Stair steps (3 steps) + int step_h = h / 4; + Color step_light = (Color){100, 95, 90, 255}; + Color step_dark = (Color){60, 57, 53, 255}; + + for (int i = 0; i < 3; i++) { + int y = (int)off.y + h - (i + 1) * step_h; + int inset = i * 2; + int x = (int)off.x + inset; + DrawRectangle(x, y, w - inset * 2, step_h - 1, step_light); + DrawLine(x, y + step_h - 1, x + w - inset * 2 - 1, y + step_h - 1, step_dark); + } + + // ">" symbol on top step + int top_y = (int)off.y + 2; + int cx = (int)off.x + w / 2; + Color arrow = (Color){180, 175, 170, 255}; + DrawPixel(cx, top_y, arrow); + DrawPixel(cx - 1, top_y + 1, arrow); + DrawPixel(cx + 1, top_y + 1, arrow); + DrawPixel(cx - 2, top_y + 2, arrow); + DrawPixel(cx + 2, top_y + 2, arrow); + DrawPixel(cx, top_y + 3, arrow); + + EndTextureMode(); +} + +static void draw_player_base(Tileset *ts, int id, int leg_offset_left, int leg_offset_right, int arm_offset_left, + int arm_offset_right) { + if (ts == NULL || id < 0 || id >= MAX_TILE_ID) + return; + if (ts->render_target.id == 0 || ts->finalized) + return; + + Vector2 off = get_paint_offset(ts, id); + int w = ts->tile_w; + int h = ts->tile_h; + int ox = (int)off.x; + int oy = (int)off.y; + + BeginTextureMode(ts->render_target); + + // Clear to transparent + DrawRectangle(ox, oy, w, h, BLANK); + + // Simple adventurer silhouette (16x16) + Color skin = (Color){230, 200, 160, 255}; + Color tunic = (Color){60, 100, 180, 255}; + Color pants = (Color){80, 60, 40, 255}; + Color boots = (Color){50, 40, 30, 255}; + Color hair = (Color){120, 80, 40, 255}; + + // Head (3x3) + DrawRectangle(ox + 6, oy + 2, 4, 3, skin); + // Hair + DrawRectangle(ox + 6, oy + 1, 4, 1, hair); + DrawPixel(ox + 5, oy + 2, hair); + DrawPixel(ox + 10, oy + 2, hair); + + // Body/tunic (4x5) + DrawRectangle(ox + 5, oy + 5, 6, 5, tunic); + // Belt + DrawRectangle(ox + 5, oy + 9, 6, 1, (Color){120, 80, 30, 255}); + + // Legs with offset for walking animation + DrawRectangle(ox + 6 + leg_offset_left, oy + 10, 2, 4, pants); + DrawRectangle(ox + 8 + leg_offset_right, oy + 10, 2, 4, pants); + + // Boots with offset + DrawRectangle(ox + 6 + leg_offset_left, oy + 14, 2, 2, boots); + DrawRectangle(ox + 8 + leg_offset_right, oy + 14, 2, 2, boots); + + // Arms with offset + DrawRectangle(ox + 3 + arm_offset_left, oy + 6, 2, 3, skin); + DrawRectangle(ox + 11 + arm_offset_right, oy + 6, 2, 3, skin); + + EndTextureMode(); +} + +void paint_player_tile(Tileset *ts, int id) { + // Idle pose - no offsets + draw_player_base(ts, id, 0, 0, 0, 0); +} + +void paint_player_walk_tile(Tileset *ts, int id, int frame) { + // Frame 0: left leg forward, right leg back + // Frame 1: right leg forward, left leg back + int leg_left = (frame == 0) ? -1 : 1; + int leg_right = (frame == 0) ? 1 : -1; + int arm_left = (frame == 0) ? 1 : -1; + int arm_right = (frame == 0) ? -1 : 1; + draw_player_base(ts, id, leg_left, leg_right, arm_left, arm_right); +} + +void paint_player_attack_tile(Tileset *ts, int id) { + if (ts == NULL || id < 0 || id >= MAX_TILE_ID) + return; + if (ts->render_target.id == 0 || ts->finalized) + return; + + Vector2 off = get_paint_offset(ts, id); + int w = ts->tile_w; + int h = ts->tile_h; + int ox = (int)off.x; + int oy = (int)off.y; + + BeginTextureMode(ts->render_target); + + // Clear to transparent + DrawRectangle(ox, oy, w, h, BLANK); + + // Attack pose - lunging forward with sword arm extended + Color skin = (Color){230, 200, 160, 255}; + Color tunic = (Color){60, 100, 180, 255}; + Color pants = (Color){80, 60, 40, 255}; + Color boots = (Color){50, 40, 30, 255}; + Color hair = (Color){120, 80, 40, 255}; + Color steel = (Color){180, 180, 190, 255}; + + // Head (3x3) + DrawRectangle(ox + 7, oy + 2, 4, 3, skin); + // Hair + DrawRectangle(ox + 7, oy + 1, 4, 1, hair); + DrawPixel(ox + 6, oy + 2, hair); + DrawPixel(ox + 11, oy + 2, hair); + + // Body/tunic (4x5) - shifted right for lunge + DrawRectangle(ox + 6, oy + 5, 6, 5, tunic); + // Belt + DrawRectangle(ox + 6, oy + 9, 6, 1, (Color){120, 80, 30, 255}); + + // Legs - left forward, right back + DrawRectangle(ox + 5, oy + 10, 2, 4, pants); + DrawRectangle(ox + 9, oy + 10, 2, 4, pants); + + // Boots + DrawRectangle(ox + 5, oy + 14, 2, 2, boots); + DrawRectangle(ox + 9, oy + 14, 2, 2, boots); + + // Left arm (back) + DrawRectangle(ox + 4, oy + 6, 2, 3, skin); + + // Right arm extended forward with sword + DrawRectangle(ox + 12, oy + 6, 3, 2, skin); + // Sword blade + DrawRectangle(ox + 14, oy + 4, 1, 6, steel); + // Sword hilt + DrawRectangle(ox + 13, oy + 7, 3, 1, (Color){100, 80, 40, 255}); + + EndTextureMode(); +} + +// Draw goblin base with configurable leg/arm offsets +static void draw_goblin_base(Tileset *ts, int id, int leg_left, int leg_right, int arm_left, int arm_right) { + if (ts == NULL || id < 0 || id >= MAX_TILE_ID) + return; + if (ts->render_target.id == 0 || ts->finalized) + return; + + Vector2 off = get_paint_offset(ts, id); + int ox = (int)off.x; + int oy = (int)off.y; + + Color skin = (Color){80, 140, 60, 255}; + Color dark = (Color){50, 90, 35, 255}; + + BeginTextureMode(ts->render_target); + DrawRectangle(ox, oy, ts->tile_w, ts->tile_h, BLANK); + + // Head (large, hunched forward) + DrawRectangle(ox + 4, oy + 3, 8, 6, skin); + // Eyes (angry) + DrawPixel(ox + 5, oy + 5, (Color){200, 50, 50, 255}); + DrawPixel(ox + 10, oy + 5, (Color){200, 50, 50, 255}); + // Ears (pointy) + DrawPixel(ox + 3, oy + 4, skin); + DrawPixel(ox + 12, oy + 4, skin); + // Body (small, hunched) + DrawRectangle(ox + 5, oy + 9, 6, 4, dark); + // Legs with offsets + DrawRectangle(ox + 5 + leg_left, oy + 13, 2, 3, skin); + DrawRectangle(ox + 9 + leg_right, oy + 13, 2, 3, skin); + // Arms with offsets + DrawRectangle(ox + 3 + arm_left, oy + 10, 2, 2, skin); + DrawRectangle(ox + 11 + arm_right, oy + 10, 2, 2, skin); + + EndTextureMode(); +} + +// Draw skeleton base with configurable leg/arm offsets +static void draw_skeleton_base(Tileset *ts, int id, int leg_left, int leg_right, int arm_left, int arm_right) { + if (ts == NULL || id < 0 || id >= MAX_TILE_ID) + return; + if (ts->render_target.id == 0 || ts->finalized) + return; + + Vector2 off = get_paint_offset(ts, id); + int ox = (int)off.x; + int oy = (int)off.y; + + Color bone = (Color){220, 220, 210, 255}; + Color dark_bone = (Color){180, 180, 170, 255}; + + BeginTextureMode(ts->render_target); + DrawRectangle(ox, oy, ts->tile_w, ts->tile_h, BLANK); + + // Skull + DrawRectangle(ox + 5, oy + 2, 6, 5, bone); + // Eye sockets + DrawPixel(ox + 6, oy + 4, (Color){20, 20, 20, 255}); + DrawPixel(ox + 9, oy + 4, (Color){20, 20, 20, 255}); + // Ribs + for (int i = 0; i < 3; i++) { + DrawRectangle(ox + 4, oy + 8 + i * 2, 8, 1, bone); + } + // Spine + DrawRectangle(ox + 7, oy + 7, 2, 6, dark_bone); + // Legs with offsets + DrawRectangle(ox + 5 + leg_left, oy + 13, 2, 3, bone); + DrawRectangle(ox + 9 + leg_right, oy + 13, 2, 3, bone); + // Arms with offsets + DrawRectangle(ox + 3 + arm_left, oy + 8, 2, 3, bone); + DrawRectangle(ox + 11 + arm_right, oy + 8, 2, 3, bone); + + EndTextureMode(); +} + +// Draw orc base with configurable leg/arm offsets +static void draw_orc_base(Tileset *ts, int id, int leg_left, int leg_right, int arm_left, int arm_right) { + if (ts == NULL || id < 0 || id >= MAX_TILE_ID) + return; + if (ts->render_target.id == 0 || ts->finalized) + return; + + Vector2 off = get_paint_offset(ts, id); + int ox = (int)off.x; + int oy = (int)off.y; + + Color skin = (Color){60, 100, 45, 255}; + Color dark = (Color){40, 70, 30, 255}; + + BeginTextureMode(ts->render_target); + DrawRectangle(ox, oy, ts->tile_w, ts->tile_h, BLANK); + + // Large head + DrawRectangle(ox + 3, oy + 2, 10, 7, skin); + // Small angry eyes + DrawPixel(ox + 5, oy + 5, (Color){250, 250, 50, 255}); + DrawPixel(ox + 10, oy + 5, (Color){250, 250, 50, 255}); + // Tusks + DrawPixel(ox + 6, oy + 7, (Color){240, 240, 220, 255}); + DrawPixel(ox + 9, oy + 7, (Color){240, 240, 220, 255}); + // Broad body + DrawRectangle(ox + 3, oy + 9, 10, 5, dark); + // Thick legs with offsets + DrawRectangle(ox + 4 + leg_left, oy + 14, 3, 2, skin); + DrawRectangle(ox + 9 + leg_right, oy + 14, 3, 2, skin); + // Thick arms with offsets + DrawRectangle(ox + 1 + arm_left, oy + 10, 3, 3, skin); + DrawRectangle(ox + 12 + arm_right, oy + 10, 3, 3, skin); + + EndTextureMode(); +} + +void paint_enemy_tile(Tileset *ts, int id, int enemy_type) { + // Idle pose - no offsets + switch (enemy_type) { + case 0: + draw_goblin_base(ts, id, 0, 0, 0, 0); + break; + case 1: + draw_skeleton_base(ts, id, 0, 0, 0, 0); + break; + case 2: + draw_orc_base(ts, id, 0, 0, 0, 0); + break; + default: + break; + } +} + +void paint_enemy_walk_tile(Tileset *ts, int id, int enemy_type, int frame) { + // Frame 0: left leg forward, right leg back + // Frame 1: right leg forward, left leg back + int leg_left = (frame == 0) ? -1 : 1; + int leg_right = (frame == 0) ? 1 : -1; + int arm_left = (frame == 0) ? 1 : -1; + int arm_right = (frame == 0) ? -1 : 1; + + switch (enemy_type) { + case 0: + draw_goblin_base(ts, id, leg_left, leg_right, arm_left, arm_right); + break; + case 1: + draw_skeleton_base(ts, id, leg_left, leg_right, arm_left, arm_right); + break; + case 2: + draw_orc_base(ts, id, leg_left, leg_right, arm_left, arm_right); + break; + default: + break; + } +} + +void paint_enemy_attack_tile(Tileset *ts, int id, int enemy_type) { + if (ts == NULL || id < 0 || id >= MAX_TILE_ID) + return; + if (ts->render_target.id == 0 || ts->finalized) + return; + + Vector2 off = get_paint_offset(ts, id); + int ox = (int)off.x; + int oy = (int)off.y; + + BeginTextureMode(ts->render_target); + DrawRectangle(ox, oy, ts->tile_w, ts->tile_h, BLANK); + + switch (enemy_type) { + case 0: { + // Goblin attack - lunging with dagger + Color skin = (Color){80, 140, 60, 255}; + Color dark = (Color){50, 90, 35, 255}; + Color steel = (Color){180, 180, 190, 255}; + // Head (leaning forward) + DrawRectangle(ox + 6, oy + 3, 8, 6, skin); + DrawPixel(ox + 7, oy + 5, (Color){200, 50, 50, 255}); + DrawPixel(ox + 12, oy + 5, (Color){200, 50, 50, 255}); + // Body + DrawRectangle(ox + 7, oy + 9, 6, 4, dark); + // Left leg back + DrawRectangle(ox + 5, oy + 13, 2, 3, skin); + // Right leg forward + DrawRectangle(ox + 11, oy + 13, 2, 3, skin); + // Left arm back + DrawRectangle(ox + 4, oy + 10, 2, 2, skin); + // Right arm extended with dagger + DrawRectangle(ox + 14, oy + 9, 2, 2, skin); + DrawRectangle(ox + 15, oy + 7, 1, 4, steel); + break; + } + case 1: { + // Skeleton attack - swinging sword + Color bone = (Color){220, 220, 210, 255}; + Color dark_bone = (Color){180, 180, 170, 255}; + Color steel = (Color){180, 180, 190, 255}; + // Skull + DrawRectangle(ox + 6, oy + 2, 6, 5, bone); + DrawPixel(ox + 7, oy + 4, (Color){20, 20, 20, 255}); + DrawPixel(ox + 10, oy + 4, (Color){20, 20, 20, 255}); + // Ribs + for (int i = 0; i < 3; i++) { + DrawRectangle(ox + 5, oy + 8 + i * 2, 8, 1, bone); + } + // Spine + DrawRectangle(ox + 8, oy + 7, 2, 6, dark_bone); + // Legs + DrawRectangle(ox + 6, oy + 13, 2, 3, bone); + DrawRectangle(ox + 10, oy + 13, 2, 3, bone); + // Left arm back + DrawRectangle(ox + 4, oy + 8, 2, 3, bone); + // Right arm extended with sword + DrawRectangle(ox + 13, oy + 7, 3, 2, bone); + DrawRectangle(ox + 15, oy + 5, 1, 6, steel); + break; + } + case 2: { + // Orc attack - overhead smash + Color skin = (Color){60, 100, 45, 255}; + Color dark = (Color){40, 70, 30, 255}; + Color steel = (Color){180, 180, 190, 255}; + // Head + DrawRectangle(ox + 4, oy + 2, 10, 7, skin); + DrawPixel(ox + 6, oy + 5, (Color){250, 250, 50, 255}); + DrawPixel(ox + 11, oy + 5, (Color){250, 250, 50, 255}); + // Tusks + DrawPixel(ox + 7, oy + 7, (Color){240, 240, 220, 255}); + DrawPixel(ox + 10, oy + 7, (Color){240, 240, 220, 255}); + // Body + DrawRectangle(ox + 4, oy + 9, 10, 5, dark); + // Legs + DrawRectangle(ox + 5, oy + 14, 3, 2, skin); + DrawRectangle(ox + 10, oy + 14, 3, 2, skin); + // Left arm back + DrawRectangle(ox + 2, oy + 10, 3, 3, skin); + // Right arm raised with club + DrawRectangle(ox + 13, oy + 4, 3, 3, skin); + DrawRectangle(ox + 14, oy + 1, 2, 5, steel); + break; + } + default: + break; + } + + EndTextureMode(); +} + +void paint_item_tile(Tileset *ts, int id, int item_type) { + if (ts == NULL || id < 0 || id >= MAX_TILE_ID) + return; + if (ts->render_target.id == 0 || ts->finalized) + return; + + Vector2 off = get_paint_offset(ts, id); + int w = ts->tile_w; + int h = ts->tile_h; + int ox = (int)off.x; + int oy = (int)off.y; + + BeginTextureMode(ts->render_target); + DrawRectangle(ox, oy, w, h, BLANK); + + switch (item_type) { + case 0: { + // Flask shape + Color glass = (Color){200, 60, 60, 255}; + Color liquid = (Color){255, 80, 80, 255}; + Color highlight = (Color){255, 150, 150, 255}; + // Neck + DrawRectangle(ox + 6, oy + 2, 4, 3, glass); + // Body + DrawRectangle(ox + 4, oy + 5, 8, 8, liquid); + // Cork + DrawRectangle(ox + 6, oy + 1, 4, 1, (Color){160, 120, 60, 255}); + // Highlight + DrawPixel(ox + 5, oy + 6, highlight); + DrawPixel(ox + 5, oy + 7, highlight); + break; + } + case 1: { + // Sword + Color blade = (Color){220, 220, 230, 255}; + Color hilt = (Color){160, 120, 40, 255}; + Color guard = (Color){140, 140, 150, 255}; + // Blade + DrawRectangle(ox + 7, oy + 2, 2, 9, blade); + // Tip + DrawPixel(ox + 7, oy + 1, blade); + DrawPixel(ox + 8, oy + 1, blade); + // Guard + DrawRectangle(ox + 5, oy + 11, 6, 1, guard); + // Hilt + DrawRectangle(ox + 7, oy + 12, 2, 3, hilt); + // Pommel + DrawPixel(ox + 7, oy + 15, guard); + DrawPixel(ox + 8, oy + 15, guard); + break; + } + case 2: { + // Chestplate + Color metal = (Color){100, 120, 160, 255}; + Color dark_metal = (Color){70, 85, 115, 255}; + Color highlight = (Color){140, 160, 200, 255}; + // Main plate + DrawRectangle(ox + 4, oy + 3, 8, 9, metal); + // Collar + DrawRectangle(ox + 5, oy + 2, 6, 1, dark_metal); + // Vertical ridge + DrawRectangle(ox + 7, oy + 3, 2, 9, highlight); + // Bottom trim + DrawRectangle(ox + 4, oy + 11, 8, 1, dark_metal); + break; + } + default: + break; + } + + EndTextureMode(); +} + +void paint_door_closed_tile(Tileset *ts, int id) { + if (ts == NULL || id < 0 || id >= MAX_TILE_ID) + return; + if (ts->render_target.id == 0 || ts->finalized) + return; + + Vector2 off = get_paint_offset(ts, id); + int ox = (int)off.x; + int oy = (int)off.y; + int w = ts->tile_w; + int h = ts->tile_h; + + BeginTextureMode(ts->render_target); + + // Wooden door frame + Color wood_dark = (Color){100, 70, 40, 255}; + Color wood_light = (Color){140, 100, 60, 255}; + Color wood_mid = (Color){120, 85, 50, 255}; + + // Frame + DrawRectangle(ox, oy, w, h, wood_dark); + // Panels + DrawRectangle(ox + 2, oy + 2, w - 4, h - 4, wood_mid); + // Inner panel + DrawRectangle(ox + 4, oy + 4, w - 8, h - 8, wood_light); + // Cross pattern + DrawRectangle(ox + 7, oy + 2, 2, h - 4, wood_dark); + DrawRectangle(ox + 2, oy + 7, w - 4, 2, wood_dark); + // Handle + DrawPixel(ox + 12, oy + 8, (Color){200, 180, 50, 255}); + DrawPixel(ox + 12, oy + 9, (Color){200, 180, 50, 255}); + + EndTextureMode(); +} + +void paint_door_open_tile(Tileset *ts, int id) { + if (ts == NULL || id < 0 || id >= MAX_TILE_ID) + return; + if (ts->render_target.id == 0 || ts->finalized) + return; + + Vector2 off = get_paint_offset(ts, id); + int ox = (int)off.x; + int oy = (int)off.y; + int w = ts->tile_w; + int h = ts->tile_h; + + BeginTextureMode(ts->render_target); + + // Open door - shows floor with door frame on sides + Color floor = (Color){75, 72, 68, 255}; + Color wood_dark = (Color){100, 70, 40, 255}; + + // Floor + DrawRectangle(ox, oy, w, h, floor); + // Door frame on left side (open) + DrawRectangle(ox, oy, 3, h, wood_dark); + // Hinges + DrawPixel(ox + 1, oy + 4, (Color){80, 80, 80, 255}); + DrawPixel(ox + 1, oy + 12, (Color){80, 80, 80, 255}); + + EndTextureMode(); +} + +void paint_effect_burn_tile(Tileset *ts, int id) { + if (ts == NULL || id < 0 || id >= MAX_TILE_ID) + return; + if (ts->render_target.id == 0 || ts->finalized) + return; + + Vector2 off = get_paint_offset(ts, id); + int ox = (int)off.x; + int oy = (int)off.y; + + BeginTextureMode(ts->render_target); + DrawRectangle(ox, oy, ts->tile_w, ts->tile_h, BLANK); + + // Fire effect - orange/red flames + Color flame1 = (Color){255, 100, 20, 255}; + Color flame2 = (Color){255, 180, 40, 255}; + Color flame3 = (Color){255, 60, 10, 255}; + + // Flame shapes + DrawRectangle(ox + 4, oy + 8, 2, 6, flame1); + DrawRectangle(ox + 7, oy + 6, 2, 8, flame2); + DrawRectangle(ox + 10, oy + 9, 2, 5, flame3); + DrawPixel(ox + 5, oy + 5, flame2); + DrawPixel(ox + 8, oy + 4, flame1); + DrawPixel(ox + 11, oy + 7, flame2); + + EndTextureMode(); +} + +void paint_effect_poison_tile(Tileset *ts, int id) { + if (ts == NULL || id < 0 || id >= MAX_TILE_ID) + return; + if (ts->render_target.id == 0 || ts->finalized) + return; + + Vector2 off = get_paint_offset(ts, id); + int ox = (int)off.x; + int oy = (int)off.y; + + BeginTextureMode(ts->render_target); + DrawRectangle(ox, oy, ts->tile_w, ts->tile_h, BLANK); + + // Poison effect - green bubbles/drops + Color poison1 = (Color){50, 200, 50, 255}; + Color poison2 = (Color){30, 150, 30, 255}; + Color poison3 = (Color){80, 255, 80, 255}; + + // Bubbles + DrawRectangle(ox + 5, oy + 4, 3, 3, poison1); + DrawRectangle(ox + 9, oy + 7, 2, 2, poison2); + DrawRectangle(ox + 4, oy + 10, 2, 2, poison3); + DrawRectangle(ox + 8, oy + 11, 3, 2, poison1); + DrawPixel(ox + 11, oy + 5, poison3); + DrawPixel(ox + 6, oy + 13, poison2); + + EndTextureMode(); +} + +void paint_effect_block_tile(Tileset *ts, int id) { + if (ts == NULL || id < 0 || id >= MAX_TILE_ID) + return; + if (ts->render_target.id == 0 || ts->finalized) + return; + + Vector2 off = get_paint_offset(ts, id); + int ox = (int)off.x; + int oy = (int)off.y; + + BeginTextureMode(ts->render_target); + DrawRectangle(ox, oy, ts->tile_w, ts->tile_h, BLANK); + + // Block/shield effect - blue shield shape + Color shield = (Color){80, 130, 220, 255}; + Color shield_light = (Color){120, 170, 255, 255}; + Color shield_dark = (Color){50, 90, 180, 255}; + + // Shield outline + DrawRectangle(ox + 4, oy + 2, 8, 12, shield); + // Inner highlight + DrawRectangle(ox + 6, oy + 4, 4, 8, shield_light); + // Border + DrawRectangle(ox + 4, oy + 2, 8, 1, shield_dark); + DrawRectangle(ox + 4, oy + 13, 8, 1, shield_dark); + DrawRectangle(ox + 4, oy + 2, 1, 12, shield_dark); + DrawRectangle(ox + 11, oy + 2, 1, 12, shield_dark); + // Cross in center + DrawRectangle(ox + 7, oy + 6, 2, 4, shield_dark); + DrawRectangle(ox + 6, oy + 7, 4, 2, shield_dark); + + EndTextureMode(); +} + +void paint_slash_effect_tile(Tileset *ts, int id) { + if (ts == NULL || id < 0 || id >= MAX_TILE_ID) + return; + if (ts->render_target.id == 0 || ts->finalized) + return; + + Vector2 off = get_paint_offset(ts, id); + int ox = (int)off.x; + int oy = (int)off.y; + + BeginTextureMode(ts->render_target); + DrawRectangle(ox, oy, ts->tile_w, ts->tile_h, BLANK); + + // Slash effect - white/gray diagonal slash + Color slash = (Color){255, 255, 255, 255}; + Color slash_trail = (Color){200, 200, 220, 255}; + + // Main slash line (diagonal) + DrawRectangle(ox + 2, oy + 12, 3, 2, slash); + DrawRectangle(ox + 4, oy + 10, 3, 2, slash); + DrawRectangle(ox + 6, oy + 8, 3, 2, slash); + DrawRectangle(ox + 8, oy + 6, 3, 2, slash); + DrawRectangle(ox + 10, oy + 4, 3, 2, slash); + DrawRectangle(ox + 12, oy + 2, 2, 2, slash); + // Trail + DrawPixel(ox + 3, oy + 11, slash_trail); + DrawPixel(ox + 5, oy + 9, slash_trail); + DrawPixel(ox + 7, oy + 7, slash_trail); + DrawPixel(ox + 9, oy + 5, slash_trail); + DrawPixel(ox + 11, oy + 3, slash_trail); + + EndTextureMode(); +} + +int tileset_paint_all(Tileset *ts) { + if (ts == NULL) + return 0; + if (ts->render_target.id == 0 || ts->finalized) + return 0; + + // Register all tile IDs first + for (int id = 0; id < NUM_TILE_IDS; id++) { + if (!tileset_register(ts, id)) + return 0; + } + + // Paint map tiles + paint_wall_tile(ts, TILE_WALL_0, 0); + paint_wall_tile(ts, TILE_WALL_1, 1); + paint_floor_tile(ts, TILE_FLOOR_0, 0); + paint_floor_tile(ts, TILE_FLOOR_1, 1); + paint_floor_tile(ts, TILE_FLOOR_2, 2); + paint_floor_tile(ts, TILE_FLOOR_3, 3); + paint_stairs_tile(ts, TILE_STAIRS_SPRITE); + + // Paint entity sprites + paint_player_tile(ts, SPRITE_PLAYER); + paint_player_walk_tile(ts, SPRITE_PLAYER_WALK_0, 0); + paint_player_walk_tile(ts, SPRITE_PLAYER_WALK_1, 1); + paint_player_attack_tile(ts, SPRITE_PLAYER_ATTACK); + + // Enemy goblin sprites + paint_enemy_tile(ts, SPRITE_ENEMY_GOBLIN, 0); + paint_enemy_walk_tile(ts, SPRITE_ENEMY_GOBLIN_WALK_0, 0, 0); + paint_enemy_walk_tile(ts, SPRITE_ENEMY_GOBLIN_WALK_1, 0, 1); + paint_enemy_attack_tile(ts, SPRITE_ENEMY_GOBLIN_ATTACK, 0); + + // Enemy skeleton sprites + paint_enemy_tile(ts, SPRITE_ENEMY_SKELETON, 1); + paint_enemy_walk_tile(ts, SPRITE_ENEMY_SKELETON_WALK_0, 1, 0); + paint_enemy_walk_tile(ts, SPRITE_ENEMY_SKELETON_WALK_1, 1, 1); + paint_enemy_attack_tile(ts, SPRITE_ENEMY_SKELETON_ATTACK, 1); + + // Enemy orc sprites + paint_enemy_tile(ts, SPRITE_ENEMY_ORC, 2); + paint_enemy_walk_tile(ts, SPRITE_ENEMY_ORC_WALK_0, 2, 0); + paint_enemy_walk_tile(ts, SPRITE_ENEMY_ORC_WALK_1, 2, 1); + paint_enemy_attack_tile(ts, SPRITE_ENEMY_ORC_ATTACK, 2); + + paint_item_tile(ts, SPRITE_ITEM_POTION, 0); + paint_item_tile(ts, SPRITE_ITEM_WEAPON, 1); + paint_item_tile(ts, SPRITE_ITEM_ARMOR, 2); + + // Door tiles + paint_door_closed_tile(ts, TILE_DOOR_CLOSED_SPRITE); + paint_door_open_tile(ts, TILE_DOOR_OPEN_SPRITE); + + // Effect sprites + paint_effect_burn_tile(ts, SPRITE_EFFECT_BURN); + paint_effect_poison_tile(ts, SPRITE_EFFECT_POISON); + paint_effect_block_tile(ts, SPRITE_EFFECT_BLOCK); + paint_slash_effect_tile(ts, SPRITE_SLASH_EFFECT); + + return 1; +} diff --git a/libs/tileset/tileset_paint.h b/libs/tileset/tileset_paint.h new file mode 100644 index 0000000..032b224 --- /dev/null +++ b/libs/tileset/tileset_paint.h @@ -0,0 +1,66 @@ +#ifndef TILESET_PAINT_H +#define TILESET_PAINT_H + +#include "tileset.h" + +// Forward declarations for types used in painting +typedef enum { ENEMY_GOBLIN_FWD, ENEMY_SKELETON_FWD, ENEMY_ORC_FWD } EnemyType_Paint; +typedef enum { ITEM_POTION_FWD, ITEM_WEAPON_FWD, ITEM_ARMOR_FWD } ItemType_Paint; + +// Paint a wall tile with brick-like pattern. +// variant: 0 or 1 for shade variation. +void paint_wall_tile(Tileset *ts, int id, int variant); + +// Paint a floor tile with stone/dithered pattern. +// variant: 0-3 for different noise patterns. +void paint_floor_tile(Tileset *ts, int id, int variant); + +// Paint a stairs tile with depth illusion. +void paint_stairs_tile(Tileset *ts, int id); + +// Paint the player sprite (adventurer silhouette). +void paint_player_tile(Tileset *ts, int id); + +// Paint a player walking animation frame. +// frame: 0 or 1 for the two walk frames. +void paint_player_walk_tile(Tileset *ts, int id, int frame); + +// Paint a player attacking animation frame. +void paint_player_attack_tile(Tileset *ts, int id); + +// Paint an enemy sprite based on type. +void paint_enemy_tile(Tileset *ts, int id, int enemy_type); + +// Paint an enemy walking animation frame. +// frame: 0 or 1 for the two walk frames. +void paint_enemy_walk_tile(Tileset *ts, int id, int enemy_type, int frame); + +// Paint an enemy attacking animation frame. +void paint_enemy_attack_tile(Tileset *ts, int id, int enemy_type); + +// Paint an item sprite based on type. +void paint_item_tile(Tileset *ts, int id, int item_type); + +// Paint a closed door tile. +void paint_door_closed_tile(Tileset *ts, int id); + +// Paint an open door tile. +void paint_door_open_tile(Tileset *ts, int id); + +// Paint a burning/fire effect sprite. +void paint_effect_burn_tile(Tileset *ts, int id); + +// Paint a poison effect sprite. +void paint_effect_poison_tile(Tileset *ts, int id); + +// Paint a block/shield effect sprite. +void paint_effect_block_tile(Tileset *ts, int id); + +// Paint a slash attack effect sprite. +void paint_slash_effect_tile(Tileset *ts, int id); + +// Convenience: paint and register all tiles in one call. +// Returns 0 on failure, non-zero on success. +int tileset_paint_all(Tileset *ts); + +#endif // TILESET_PAINT_H