From 5b640dcefd2e714392584b0e806dd20e489ecb54 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 28 Apr 2026 15:32:56 +0300 Subject: [PATCH] render: use tileset atlas for all entity and tile rendering; anims Signed-off-by: NotAShelf Change-Id: Idb42cff72368e26d8d44db79ba9c413a6a6a6964 --- src/enemy.c | 66 ++++++++ src/game_state.h | 7 + src/items.c | 17 ++ src/main.c | 107 ++++++++++-- src/player.c | 8 + src/render.c | 412 +++++++++++++++++++++++++++++++++++++++-------- src/render.h | 27 ++-- 7 files changed, 555 insertions(+), 89 deletions(-) diff --git a/src/enemy.c b/src/enemy.c index 6832386..084a954 100644 --- a/src/enemy.c +++ b/src/enemy.c @@ -5,6 +5,7 @@ #include "movement.h" #include "rng/rng.h" #include "settings.h" +#include "tileset/tileset.h" #include // Forward declaration @@ -25,6 +26,12 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) { if (floor >= 4) max_type = 3; + // Get the player's starting room (first room) to exclude from enemy spawn + Room *start_room = NULL; + if (map->room_count > 0) { + start_room = &map->rooms[0]; + } + for (int i = 0; i < num_enemies; i++) { // Find random floor position int ex, ey; @@ -35,6 +42,14 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) { continue; } + // Don't spawn in the starting room + if (start_room != NULL) { + if (ex >= start_room->x && ex < start_room->x + start_room->w && ey >= start_room->y && + ey < start_room->y + start_room->h) { + continue; + } + } + // Don't spawn on other enemies if (is_enemy_at(enemies, *count, ex, ey)) { continue; @@ -125,6 +140,27 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) { } e.cooldown = e.speed; + // Initialize animation state + e.anim_state = ENEMY_ANIM_IDLE; + e.anim_frame = 0; + e.anim_timer = 0; + e.facing_right = (e.position.x < p->position.x) ? 1 : 0; + // Set sprite tile ID based on enemy type + switch (e.type) { + case ENEMY_GOBLIN: + e.sprite_tile_id = SPRITE_ENEMY_GOBLIN; + break; + case ENEMY_SKELETON: + e.sprite_tile_id = SPRITE_ENEMY_SKELETON; + break; + case ENEMY_ORC: + e.sprite_tile_id = SPRITE_ENEMY_ORC; + break; + default: + e.sprite_tile_id = SPRITE_ENEMY_GOBLIN; + break; + } + enemies[i] = e; (*count)++; } @@ -275,6 +311,9 @@ void enemy_act(Enemy *e, Player *p, Map *map, Enemy *all_enemies, int enemy_coun // Attack if adjacent to player if (can_see && can_see_entity(map, e->position.x, e->position.y, p->position.x, p->position.y, 1)) { + e->anim_state = ENEMY_ANIM_ATTACK; + e->anim_timer = 12; + e->facing_right = (e->position.x < p->position.x) ? 1 : 0; combat_enemy_attack(e, p); propagate_alert(e, all_enemies, enemy_count); return; @@ -282,14 +321,30 @@ void enemy_act(Enemy *e, Player *p, Map *map, Enemy *all_enemies, int enemy_coun // Move toward player if visible if (can_see) { + int old_x = e->position.x; + int old_y = e->position.y; enemy_move_toward_player(e, p, map, all_enemies, enemy_count); + if (e->position.x != old_x || e->position.y != old_y) { + e->anim_state = ENEMY_ANIM_WALK; + e->anim_timer = 8; + e->facing_right = (e->position.x < p->position.x) ? 1 : 0; + } propagate_alert(e, all_enemies, enemy_count); return; } // If alert but can't see player, move toward last known position if (e->alert) { + int old_x = e->position.x; + int old_y = e->position.y; enemy_move_to_last_known(e, map, all_enemies, enemy_count); + if (e->position.x != old_x || e->position.y != old_y) { + e->anim_state = ENEMY_ANIM_WALK; + e->anim_timer = 8; + if (e->position.x != old_x) { + e->facing_right = (e->position.x < old_x) ? 0 : 1; + } + } return; } @@ -304,6 +359,17 @@ void enemy_update_all(Enemy enemies[], int count, Player *p, Map *map) { if (!e->alive) continue; + // Update animation timer + if (e->anim_timer > 0) { + e->anim_timer--; + if (e->anim_timer <= 0) { + e->anim_state = ENEMY_ANIM_IDLE; + e->anim_frame = 0; + } else if (e->anim_state == ENEMY_ANIM_WALK) { + e->anim_frame = (e->anim_timer / 4) % 2; + } + } + e->cooldown -= e->speed; if (e->cooldown <= 0) { enemy_act(e, p, map, enemies, count); diff --git a/src/game_state.h b/src/game_state.h index e0d7711..73cdac2 100644 --- a/src/game_state.h +++ b/src/game_state.h @@ -2,6 +2,7 @@ #define GAME_STATE_H #include "common.h" +#include "tileset/tileset.h" #include // Floating damage text @@ -65,6 +66,12 @@ typedef struct { int final_score; // Seed for this run unsigned int run_seed; + // Tileset atlas for rendering + Tileset tileset; + // Slash effect timer for attack animations + int slash_timer; // frames remaining for slash effect + int slash_x, slash_y; // position of slash effect + DamageClass slash_dmg_class; // damage type for slash visual } GameState; #endif // GAME_STATE_H diff --git a/src/items.c b/src/items.c index 27f343d..367a328 100644 --- a/src/items.c +++ b/src/items.c @@ -2,6 +2,7 @@ #include "map/map.h" #include "rng/rng.h" #include "settings.h" +#include "tileset/tileset.h" #include typedef struct { @@ -77,6 +78,22 @@ void item_spawn(Item items[], int *count, Map *map, int floor) { item.power = 1 + rng_int(0, floor / 2); } + // Set sprite tile ID based on item type + switch (item.type) { + case ITEM_POTION: + item.sprite_tile_id = SPRITE_ITEM_POTION; + break; + case ITEM_WEAPON: + item.sprite_tile_id = SPRITE_ITEM_WEAPON; + break; + case ITEM_ARMOR: + item.sprite_tile_id = SPRITE_ITEM_ARMOR; + break; + default: + item.sprite_tile_id = SPRITE_ITEM_POTION; + break; + } + items[*count] = item; (*count)++; } diff --git a/src/main.c b/src/main.c index e9089a5..e31dd99 100644 --- a/src/main.c +++ b/src/main.c @@ -4,11 +4,14 @@ #include "enemy.h" #include "items.h" #include "map/map.h" +#include "map/utils.h" #include "movement.h" #include "player.h" #include "render.h" #include "rng/rng.h" #include "settings.h" +#include "tileset/tileset.h" +#include "tileset/tileset_paint.h" #include #include #include @@ -182,19 +185,18 @@ static void post_action(GameState *gs, Enemy *attacked_enemy) { if (gs->game_over) return; - // Check if stepped on stairs - if (gs->map.tiles[gs->player.position.y][gs->player.position.x] == TILE_STAIRS) { - gs->awaiting_descend = 1; - gs->last_message = "Descend to next floor? (Y/N)"; - gs->message_timer = 120; - return; - } - // combat feedback - player attacked an enemy this turn if (attacked_enemy != NULL) { int ex = attacked_enemy->position.x * TILE_SIZE + 8; int ey = attacked_enemy->position.y * TILE_SIZE; + // Trigger slash effect + gs->slash_timer = 8; + gs->slash_x = attacked_enemy->position.x; + gs->slash_y = attacked_enemy->position.y; + // Use player's equipped weapon damage class, or default to slash + gs->slash_dmg_class = gs->player.has_weapon ? gs->player.equipped_weapon.dmg_class : DMG_SLASH; + if (combat_was_dodged()) { spawn_floating_label(gs, ex, ey, LABEL_DODGE, EFFECT_NONE); audio_play_dodge(gs); @@ -237,6 +239,7 @@ static void post_action(GameState *gs, Enemy *attacked_enemy) { gs->screen_shake = SHAKE_PLAYER_DAMAGE_DURATION; gs->damage_taken += combat_get_last_damage(); gs->times_hit++; + gs->player.flash_timer = 4; // Trigger damage flash spawn_floating_text(gs, gs->player.position.x * TILE_SIZE + 8, gs->player.position.y * TILE_SIZE, combat_get_last_damage(), combat_was_critical()); } @@ -247,6 +250,13 @@ static void post_action(GameState *gs, Enemy *attacked_enemy) { if (gs->player.hp <= 0) gs->game_over = 1; + + // Check if stepped on stairs AFTER enemy turns + if (gs->map.tiles[gs->player.position.y][gs->player.position.x] == TILE_STAIRS) { + gs->awaiting_descend = 1; + gs->last_message = "Descend to next floor? (Y/N)"; + gs->message_timer = 120; + } } // If player is stunned, wait for any key then consume the turn @@ -457,11 +467,27 @@ static int handle_movement_input(GameState *gs) { try_move_entity(&gs->player.position, direction, &gs->map, &gs->player, gs->enemies, gs->enemy_count, true); if (result == MOVE_RESULT_MOVED) { player_on_move(&gs->player); + // Set walk animation + gs->player.anim_state = PLAYER_ANIM_WALK; + gs->player.anim_frame = 0; + gs->player.anim_timer = 8; // frames to show each walk frame + // Update facing direction + if (direction.x != 0) + gs->player.facing_right = (direction.x > 0); action = 1; } else if (result == MOVE_RESULT_BLOCKED_ENEMY) { target = player_find_enemy_at(gs->enemies, gs->enemy_count, new_x, new_y); if (target != NULL) { player_attack(&gs->player, target); + // Set attack animation + gs->player.anim_state = PLAYER_ANIM_ATTACK; + gs->player.anim_frame = 0; + gs->player.anim_timer = 12; // frames to show attack + // Face the enemy + if (target->position.x > gs->player.position.x) + gs->player.facing_right = 1; + else if (target->position.x < gs->player.position.x) + gs->player.facing_right = 0; action = 1; } } @@ -526,13 +552,35 @@ static void game_loop(unsigned int run_seed, FontManager *fm) { load_audio_assets(&gs); // font init_fonts(fm); + // Initialize tileset atlas + if (!tileset_init(&gs.tileset, TILE_SIZE, TILE_SIZE)) { + fprintf(stderr, "Failed to initialize tileset\n"); + destroy_fonts(fm); + return; + } + if (!tileset_paint_all(&gs.tileset)) { + fprintf(stderr, "Failed to paint tiles\n"); + tileset_destroy(&gs.tileset); + destroy_fonts(fm); + return; + } + if (!tileset_finalize(&gs.tileset)) { + fprintf(stderr, "Failed to finalize tileset\n"); + tileset_destroy(&gs.tileset); + destroy_fonts(fm); + return; + } + // Initialize first floor init_floor(&gs, 1); // Disable esc to exit SetExitKey(0); + int frame_counter = 0; while (!WindowShouldClose()) { + frame_counter++; + // Handle input if (!gs.game_over) { // Tick status effects at the start of each frame where input is checked @@ -552,6 +600,12 @@ static void game_loop(unsigned int run_seed, FontManager *fm) { gs.game_won = 0; load_audio_assets(&gs); init_fonts(fm); + // Re-initialize tileset for new run + if (!tileset_init(&gs.tileset, TILE_SIZE, TILE_SIZE) || !tileset_paint_all(&gs.tileset) || + !tileset_finalize(&gs.tileset)) { + fprintf(stderr, "Failed to re-initialize tileset\n"); + break; + } init_floor(&gs, 1); // Update window title with new seed char title[128]; @@ -567,6 +621,27 @@ static void game_loop(unsigned int run_seed, FontManager *fm) { // Update effects update_effects(&gs); + // Update slash effect timer + if (gs.slash_timer > 0) + gs.slash_timer--; + + // Update player animation + if (gs.player.anim_timer > 0) { + gs.player.anim_timer--; + if (gs.player.anim_timer <= 0) { + // Animation finished, return to idle + gs.player.anim_state = PLAYER_ANIM_IDLE; + gs.player.anim_frame = 0; + } else if (gs.player.anim_state == PLAYER_ANIM_WALK) { + // Toggle walk frame every 4 frames + gs.player.anim_frame = (gs.player.anim_timer / 4) % 2; + } + } + + // Update player damage flash + if (gs.player.flash_timer > 0) + gs.player.flash_timer--; + // Render BeginDrawing(); ClearBackground(BLACK); @@ -576,15 +651,19 @@ static void game_loop(unsigned int run_seed, FontManager *fm) { cam.zoom = 1.0f; cam.offset = (Vector2){(float)gs.shake_x, (float)gs.shake_y}; BeginMode2D(cam); - render_map(&gs.map); - render_items(gs.items, gs.item_count, gs.map.visible); - render_enemies(gs.enemies, gs.enemy_count, gs.map.visible); - render_player(&gs.player); + render_map(&gs.map, &gs.tileset); + render_items(gs.items, gs.item_count, gs.map.visible, &gs.tileset); + render_enemies(gs.enemies, gs.enemy_count, gs.map.visible, &gs.tileset, frame_counter); + render_player(&gs.player, &gs.tileset, frame_counter); + // Draw slash effect on top of entities + if (gs.slash_timer > 0) { + render_slash_effect(gs.slash_x, gs.slash_y, gs.slash_dmg_class, gs.slash_timer); + } EndMode2D(); // Floating texts follow world shake - render_floating_texts(gs.floating_texts, gs.floating_count, gs.shake_x, gs.shake_y); - render_ui(&gs.player, fm); + render_floating_texts(gs.floating_texts, gs.floating_count, gs.shake_x, gs.shake_y, fm); + render_ui(&gs.player, &gs.tileset, fm); // Draw action log render_action_log(gs.action_log, gs.log_count, gs.log_head, fm); diff --git a/src/player.c b/src/player.c index ecc9967..f47becb 100644 --- a/src/player.c +++ b/src/player.c @@ -3,6 +3,7 @@ #include "common.h" #include "items.h" #include "settings.h" +#include "tileset/tileset.h" #include void player_init(Player *p, int x, int y) { @@ -28,6 +29,13 @@ void player_init(Player *p, int x, int y) { p->effect_count = 0; memset(p->effects, 0, sizeof(p->effects)); + // Initialize animation state + p->anim_state = PLAYER_ANIM_IDLE; + p->anim_frame = 0; + p->anim_timer = 0; + p->facing_right = 1; + p->sprite_tile_id = SPRITE_PLAYER; + // Initialize inventory to empty for (int i = 0; i < MAX_INVENTORY; i++) { p->inventory[i].picked_up = 1; // mark as invalid diff --git a/src/render.c b/src/render.c index 4dfb602..e7b4b27 100644 --- a/src/render.c +++ b/src/render.c @@ -1,6 +1,8 @@ #include "render.h" #include "items.h" #include "settings.h" +#include "map/utils.h" +#include #include #include #include @@ -63,78 +65,292 @@ static void draw_text_body(Font f, const char *text, float x, float y, int size, DrawTextEx(f, text, (Vector2){x, y}, (float)size, spacing, c); } -void render_map(const Map *map) { +void render_map(const Map *map, const Tileset *tileset) { for (int y = 0; y < MAP_HEIGHT; y++) { for (int x = 0; x < MAP_WIDTH; x++) { - Rectangle rect = {(float)(x * TILE_SIZE), (float)(y * TILE_SIZE), (float)TILE_SIZE, (float)TILE_SIZE}; + Rectangle dst = {(float)(x * TILE_SIZE), (float)(y * TILE_SIZE), (float)TILE_SIZE, (float)TILE_SIZE}; int visible = map->visible[y][x]; int remembered = map->remembered[y][x]; if (!visible && !remembered) { - DrawRectangleRec(rect, (Color){5, 5, 10, 255}); + DrawRectangleRec(dst, (Color){5, 5, 10, 255}); continue; } + int tile_id = -1; + switch (map->tiles[y][x]) { + case TILE_WALL: + tile_id = TILE_WALL_0 + ((x * 7 + y * 13) % 2); + break; + case TILE_FLOOR: + tile_id = TILE_FLOOR_0 + ((x * 7 + y * 13) % 4); + break; + case TILE_STAIRS: + tile_id = TILE_STAIRS_SPRITE; + break; + case TILE_DOOR_CLOSED: + tile_id = TILE_DOOR_CLOSED_SPRITE; + break; + case TILE_DOOR_OPEN: + tile_id = TILE_DOOR_OPEN_SPRITE; + break; + case TILE_DOOR_RUINED: + tile_id = TILE_DOOR_OPEN_SPRITE; + break; + } + + if (tile_id >= 0 && tileset != NULL && tileset->finalized) { + Rectangle src = tileset_get_region(tileset, tile_id); + if (src.width > 0) { + Color tint = WHITE; + if (!visible) { + // Dim remembered tiles + tint = (Color){128, 128, 128, 255}; + } + DrawTexturePro(tileset->atlas, src, dst, (Vector2){0, 0}, 0.0f, tint); + continue; + } + } + + // Fallback to solid colors if tileset not available Color wall_color = visible ? DARKGRAY : (Color){25, 25, 30, 255}; Color floor_color = visible ? BLACK : (Color){15, 15, 20, 255}; - Color stairs_color = visible ? (Color){100, 100, 100, 255} : (Color){40, 40, 45, 255}; + Color stairs_color = visible ? (Color){180, 160, 100, 255} : (Color){60, 55, 50, 255}; + Color door_color = visible ? (Color){139, 119, 89, 255} : (Color){60, 55, 50, 255}; switch (map->tiles[y][x]) { case TILE_WALL: - DrawRectangleRec(rect, wall_color); + DrawRectangleRec(dst, wall_color); break; case TILE_FLOOR: - DrawRectangleRec(rect, floor_color); + DrawRectangleRec(dst, floor_color); + // Torch flicker: warm tint on floor tiles adjacent to stairs + { + int is_adjacent_to_stairs = 0; + for (int dy = -1; dy <= 1 && !is_adjacent_to_stairs; dy++) { + for (int dx = -1; dx <= 1 && !is_adjacent_to_stairs; dx++) { + int nx = x + dx; + int ny = y + dy; + if (in_bounds(nx, ny, MAP_WIDTH, MAP_HEIGHT) && map->tiles[ny][nx] == TILE_STAIRS) { + is_adjacent_to_stairs = 1; + } + } + } + if (is_adjacent_to_stairs && visible) { + int flicker = (int)(sinf(GetTime() * 5.0f) * 15.0f); + DrawRectangleRec(dst, (Color){40 + flicker, 25, 10, 60}); + } + } + // Grid lines + if (DRAW_GRID_LINES && visible) { + DrawRectangleLines((int)dst.x, (int)dst.y, (int)dst.width, (int)dst.height, (Color){20, 20, 20, 80}); + } break; case TILE_STAIRS: - DrawRectangleRec(rect, stairs_color); - if (visible) - DrawText(">", x * TILE_SIZE + 4, y * TILE_SIZE + 2, NORM_FONT, WHITE); - else - DrawText(">", x * TILE_SIZE + 4, y * TILE_SIZE + 2, NORM_FONT, (Color){60, 60, 65, 255}); + DrawRectangleRec(dst, stairs_color); + // Make stairs very visible with bright symbol and bounce + { + int bounce = (int)(sinf(GetTime() * 3.0f) * 1.5f); + if (visible) + DrawText(">", x * TILE_SIZE + 4, y * TILE_SIZE + 1 + bounce, NORM_FONT + 2, (Color){255, 255, 200, 255}); + else + DrawText(">", x * TILE_SIZE + 4, y * TILE_SIZE + 1 + bounce, NORM_FONT + 2, (Color){100, 90, 70, 255}); + } + break; + case TILE_DOOR_CLOSED: + DrawRectangleRec(dst, door_color); + if (visible) { + DrawRectangle(x * TILE_SIZE + 2, y * TILE_SIZE + 2, TILE_SIZE - 4, TILE_SIZE - 4, (Color){100, 80, 60, 255}); + DrawRectangleLines(x * TILE_SIZE + 2, y * TILE_SIZE + 2, TILE_SIZE - 4, TILE_SIZE - 4, + (Color){60, 50, 40, 255}); + DrawText("+", x * TILE_SIZE + 5, y * TILE_SIZE + 1, NORM_FONT, WHITE); + } + break; + case TILE_DOOR_OPEN: + DrawRectangleRec(dst, floor_color); + if (visible) { + DrawRectangle(x * TILE_SIZE + 2, y * TILE_SIZE + 2, TILE_SIZE - 4, TILE_SIZE - 4, (Color){80, 70, 50, 180}); + DrawRectangleLines(x * TILE_SIZE + 2, y * TILE_SIZE + 2, TILE_SIZE - 4, TILE_SIZE - 4, + (Color){60, 50, 40, 200}); + DrawText("'", x * TILE_SIZE + 6, y * TILE_SIZE + 2, NORM_FONT, (Color){150, 140, 120, 255}); + } + break; + case TILE_DOOR_RUINED: + DrawRectangleRec(dst, (Color){60, 45, 30, 255}); + if (visible) { + DrawRectangle(x * TILE_SIZE + 1, y * TILE_SIZE + 1, TILE_SIZE - 2, TILE_SIZE - 2, (Color){80, 60, 40, 200}); + DrawLine(x * TILE_SIZE + 2, y * TILE_SIZE + 2, x * TILE_SIZE + TILE_SIZE - 2, y * TILE_SIZE + TILE_SIZE - 2, + (Color){120, 90, 60, 255}); + DrawLine(x * TILE_SIZE + TILE_SIZE - 2, y * TILE_SIZE + 2, x * TILE_SIZE + 2, y * TILE_SIZE + TILE_SIZE - 2, + (Color){120, 90, 60, 255}); + } break; } } } } -void render_player(const Player *p) { - Rectangle rect = {(float)(p->position.x * TILE_SIZE), (float)(p->position.y * TILE_SIZE), (float)TILE_SIZE, - (float)TILE_SIZE}; - DrawRectangleRec(rect, BLUE); +void render_player(const Player *p, const Tileset *tileset, int frame_counter) { + Rectangle dst = {(float)(p->position.x * TILE_SIZE), (float)(p->position.y * TILE_SIZE), (float)TILE_SIZE, + (float)TILE_SIZE}; + + if (tileset != NULL && tileset->finalized) { + int tile_id = p->sprite_tile_id; + switch (p->anim_state) { + case PLAYER_ANIM_WALK: + tile_id = (p->anim_frame == 0) ? SPRITE_PLAYER_WALK_0 : SPRITE_PLAYER_WALK_1; + break; + case PLAYER_ANIM_ATTACK: + tile_id = SPRITE_PLAYER_ATTACK; + break; + default: + // Idle breathing: subtle bob every 60 frames + if ((frame_counter / 30) % 2 == 0) { + dst.y -= 1; + } + tile_id = p->sprite_tile_id; + break; + } + + Rectangle src = tileset_get_region(tileset, tile_id); + if (src.width > 0) { + // Flip horizontally if facing left + if (!p->facing_right) { + src.width = -src.width; + } + DrawTexturePro(tileset->atlas, src, dst, (Vector2){0, 0}, 0.0f, WHITE); + + // Draw status effect overlays + for (int e = 0; e < p->effect_count && e < MAX_EFFECTS; e++) { + if (p->effects[e].duration > 0) { + int effect_tile = -1; + switch (p->effects[e].type) { + case EFFECT_BURN: + effect_tile = SPRITE_EFFECT_BURN; + break; + case EFFECT_POISON: + effect_tile = SPRITE_EFFECT_POISON; + break; + default: + break; + } + if (effect_tile >= 0) { + Rectangle eff_src = tileset_get_region(tileset, effect_tile); + if (eff_src.width > 0) { + Rectangle eff_dst = {dst.x + 8, dst.y + 8, 8, 8}; + DrawTexturePro(tileset->atlas, eff_src, eff_dst, (Vector2){0, 0}, 0.0f, (Color){255, 255, 255, 180}); + } + } + } + } + + // Damage flash overlay + if (p->flash_timer > 0) { + DrawRectangleRec(dst, (Color){255, 0, 0, 128}); + } + + return; + } + } + + // Fallback to solid color + DrawRectangleRec(dst, BLUE); + if (p->flash_timer > 0) { + DrawRectangleRec(dst, (Color){255, 0, 0, 128}); + } } -void render_enemies(const Enemy *enemies, int count, const unsigned char visible[MAP_HEIGHT][MAP_WIDTH]) { +void render_enemies(const Enemy *enemies, int count, const unsigned char visible[MAP_HEIGHT][MAP_WIDTH], + const Tileset *tileset, int frame_counter) { for (int i = 0; i < count; i++) { if (!enemies[i].alive) continue; if (!visible[enemies[i].position.y][enemies[i].position.x]) continue; - Rectangle rect = {(float)(enemies[i].position.x * TILE_SIZE), (float)(enemies[i].position.y * TILE_SIZE), - (float)TILE_SIZE, (float)TILE_SIZE}; + Rectangle dst = {(float)(enemies[i].position.x * TILE_SIZE), (float)(enemies[i].position.y * TILE_SIZE), + (float)TILE_SIZE, (float)TILE_SIZE}; - // Different colors based on enemy type - Color enemy_color; - switch (enemies[i].type) { - case ENEMY_GOBLIN: - enemy_color = COLOR_ENEMY_GOBLIN; // dark red - break; - case ENEMY_SKELETON: - enemy_color = COLOR_ENEMY_SKELETON; // light gray - break; - case ENEMY_ORC: - enemy_color = COLOR_ENEMY_ORC; // dark green - break; - default: - enemy_color = RED; - break; + // Select animation frame based on sprite_tile_id base + int base_tile = enemies[i].sprite_tile_id; + int tile_id; + if (enemies[i].anim_state == ENEMY_ANIM_WALK) { + tile_id = (enemies[i].anim_frame == 0) ? base_tile + 1 : base_tile + 2; + } else if (enemies[i].anim_state == ENEMY_ANIM_ATTACK) { + tile_id = base_tile + 3; + } else if (enemies[i].anim_state == ENEMY_ANIM_IDLE) { + // Idle breathing: subtle bob every 60 frames + if ((frame_counter / 30) % 2 == 0) { + dst.y -= 1; + } + tile_id = base_tile; + } else { + tile_id = base_tile; } - DrawRectangleRec(rect, enemy_color); + if (tile_id >= 0 && tileset != NULL && tileset->finalized) { + Rectangle src = tileset_get_region(tileset, tile_id); + if (src.width > 0) { + // Flip horizontally if facing left + if (!enemies[i].facing_right) { + src.width = -src.width; + } + DrawTexturePro(tileset->atlas, src, dst, (Vector2){0, 0}, 0.0f, WHITE); + + // Draw status effect overlays + for (int e = 0; e < enemies[i].effect_count && e < MAX_EFFECTS; e++) { + if (enemies[i].effects[e].duration > 0) { + int effect_tile = -1; + switch (enemies[i].effects[e].type) { + case EFFECT_BURN: + effect_tile = SPRITE_EFFECT_BURN; + break; + case EFFECT_POISON: + effect_tile = SPRITE_EFFECT_POISON; + break; + default: + break; + } + if (effect_tile >= 0) { + Rectangle eff_src = tileset_get_region(tileset, effect_tile); + if (eff_src.width > 0) { + Rectangle eff_dst = {dst.x + 8, dst.y + 8, 8, 8}; + DrawTexturePro(tileset->atlas, eff_src, eff_dst, (Vector2){0, 0}, 0.0f, (Color){255, 255, 255, 180}); + } + } + } + } + + // Enemy alert overlay (yellow tint when alert) + if (enemies[i].alert) { + DrawRectangleRec(dst, (Color){255, 255, 0, 30}); + } + } + } else { + // Fallback to solid colors + Color enemy_color; + switch (enemies[i].type) { + case ENEMY_GOBLIN: + enemy_color = COLOR_ENEMY_GOBLIN; + break; + case ENEMY_SKELETON: + enemy_color = COLOR_ENEMY_SKELETON; + break; + case ENEMY_ORC: + enemy_color = COLOR_ENEMY_ORC; + break; + default: + enemy_color = RED; + break; + } + DrawRectangleRec(dst, enemy_color); + if (enemies[i].alert) { + DrawRectangleRec(dst, (Color){255, 255, 0, 30}); + } + } // Draw hp bar above enemy, color-coded by health remaining - int hp_pixels = (enemies[i].hp * TILE_SIZE) / enemies[i].max_hp; + int hp_pixels = (enemies[i].max_hp > 0) ? (enemies[i].hp * TILE_SIZE) / enemies[i].max_hp : 0; if (hp_pixels > 0) { float hp_ratio = (float)enemies[i].hp / (float)enemies[i].max_hp; Color bar_color; @@ -151,38 +367,47 @@ void render_enemies(const Enemy *enemies, int count, const unsigned char visible } } -void render_items(const Item *items, int count, const unsigned char visible[MAP_HEIGHT][MAP_WIDTH]) { +void render_items(const Item *items, int count, const unsigned char visible[MAP_HEIGHT][MAP_WIDTH], + const Tileset *tileset) { for (int i = 0; i < count; i++) { if (items[i].picked_up) continue; if (!visible[items[i].y][items[i].x]) continue; - Rectangle rect = {(float)(items[i].x * TILE_SIZE), (float)(items[i].y * TILE_SIZE), (float)TILE_SIZE, - (float)TILE_SIZE}; + Rectangle dst = {(float)(items[i].x * TILE_SIZE), (float)(items[i].y * TILE_SIZE), (float)TILE_SIZE, + (float)TILE_SIZE}; - // Different colors based on item type - Color item_color; - switch (items[i].type) { - case ITEM_POTION: - item_color = COLOR_ITEM_POTION; // red/pink - break; - case ITEM_WEAPON: - item_color = COLOR_ITEM_WEAPON; // yellow - break; - case ITEM_ARMOR: - item_color = COLOR_ITEM_ARMOR; // blue - break; - default: - item_color = GREEN; - break; + int tile_id = items[i].sprite_tile_id; + + if (tile_id >= 0 && tileset != NULL && tileset->finalized) { + Rectangle src = tileset_get_region(tileset, tile_id); + if (src.width > 0) { + DrawTexturePro(tileset->atlas, src, dst, (Vector2){0, 0}, 0.0f, WHITE); + } + } else { + // Fallback to solid colors + Color item_color; + switch (items[i].type) { + case ITEM_POTION: + item_color = COLOR_ITEM_POTION; + break; + case ITEM_WEAPON: + item_color = COLOR_ITEM_WEAPON; + break; + case ITEM_ARMOR: + item_color = COLOR_ITEM_ARMOR; + break; + default: + item_color = GREEN; + break; + } + DrawRectangleRec(dst, item_color); } - - DrawRectangleRec(rect, item_color); } } -void render_ui(const Player *p, const FontManager *fm) { +void render_ui(const Player *p, const Tileset *tileset, const FontManager *fm) { // HUD Panel const int hud_y = MAP_HEIGHT * TILE_SIZE; const int hud_height = 60; @@ -213,10 +438,18 @@ void render_ui(const Player *p, const FontManager *fm) { int portrait_y = hud_y + 8; int portrait_size = 44; - // FIXME: for now this is just a blue square indicating the player. Once we - // model the player, add classes, sprites, etc. this will need to be revisited. - DrawRectangle(portrait_x, portrait_y, portrait_size, portrait_size, (Color){30, 30, 45, 255}); - DrawRectangle(portrait_x + 2, portrait_y + 2, portrait_size - 4, portrait_size - 4, BLUE); + // Draw player sprite in portrait + if (tileset != NULL && tileset->finalized) { + Rectangle src = tileset_get_region(tileset, SPRITE_PLAYER); + if (src.width > 0) { + Rectangle dst = {(float)portrait_x, (float)portrait_y, (float)portrait_size, (float)portrait_size}; + DrawTexturePro(tileset->atlas, src, dst, (Vector2){0, 0}, 0.0f, WHITE); + } else { + DrawRectangle(portrait_x, portrait_y, portrait_size, portrait_size, BLUE); + } + } else { + DrawRectangle(portrait_x, portrait_y, portrait_size, portrait_size, BLUE); + } DrawRectangleLines(portrait_x, portrait_y, portrait_size, portrait_size, (Color){139, 119, 89, 255}); // HP Bar, to the right of portrait @@ -552,7 +785,54 @@ static int label_font_size(FloatingLabel label) { return (label == LABEL_CRIT) ? FONT_SIZE_FLOAT_CRIT : FONT_SIZE_FLOAT_LABEL; } -void render_floating_texts(FloatingText *texts, int count, int shake_x, int shake_y) { +void render_slash_effect(int x, int y, DamageClass dmg_class, int timer) { + if (timer <= 0) + return; + + float alpha = (float)timer / 8.0f; + if (alpha > 1.0f) + alpha = 1.0f; + int a = (int)(255 * alpha); + int px = x * TILE_SIZE; + int py = y * TILE_SIZE; + + switch (dmg_class) { + case DMG_SLASH: + // Red diagonal slash + DrawLine(px + 2, py + 2, px + TILE_SIZE - 2, py + TILE_SIZE - 2, (Color){255, 80, 80, a}); + DrawLine(px + 4, py + 2, px + TILE_SIZE - 2, py + TILE_SIZE - 4, (Color){255, 120, 120, a}); + break; + case DMG_IMPACT: + // Orange burst (star pattern) + DrawLine(px + TILE_SIZE / 2, py + 2, px + TILE_SIZE / 2, py + TILE_SIZE - 2, (Color){255, 180, 60, a}); + DrawLine(px + 2, py + TILE_SIZE / 2, px + TILE_SIZE - 2, py + TILE_SIZE / 2, (Color){255, 180, 60, a}); + DrawLine(px + 4, py + 4, px + TILE_SIZE - 4, py + TILE_SIZE - 4, (Color){255, 200, 100, a}); + DrawLine(px + 4, py + TILE_SIZE - 4, px + TILE_SIZE - 4, py + 4, (Color){255, 200, 100, a}); + break; + case DMG_PIERCE: + // Yellow horizontal streak + DrawLine(px + 2, py + TILE_SIZE / 2, px + TILE_SIZE - 2, py + TILE_SIZE / 2, (Color){255, 255, 100, a}); + DrawLine(px + 2, py + TILE_SIZE / 2 - 2, px + TILE_SIZE - 2, py + TILE_SIZE / 2 - 2, (Color){255, 255, 150, a}); + DrawLine(px + 2, py + TILE_SIZE / 2 + 2, px + TILE_SIZE - 2, py + TILE_SIZE / 2 + 2, (Color){255, 255, 150, a}); + break; + case DMG_FIRE: + // Red-orange flame burst + DrawLine(px + TILE_SIZE / 2, py + TILE_SIZE - 2, px + TILE_SIZE / 2, py + 4, (Color){255, 100, 30, a}); + DrawLine(px + TILE_SIZE / 2 - 3, py + TILE_SIZE - 4, px + TILE_SIZE / 2 - 1, py + 6, (Color){255, 150, 50, a}); + DrawLine(px + TILE_SIZE / 2 + 3, py + TILE_SIZE - 4, px + TILE_SIZE / 2 + 1, py + 6, (Color){255, 150, 50, a}); + break; + case DMG_POISON: + // Green splash + DrawLine(px + 4, py + 4, px + TILE_SIZE - 4, py + TILE_SIZE - 4, (Color){50, 255, 100, a}); + DrawLine(px + TILE_SIZE - 4, py + 4, px + 4, py + TILE_SIZE - 4, (Color){80, 255, 120, a}); + DrawCircle(px + TILE_SIZE / 2, py + TILE_SIZE / 2, 3.0f, (Color){100, 255, 150, a / 2}); + break; + default: + break; + } +} + +void render_floating_texts(FloatingText *texts, int count, int shake_x, int shake_y, const FontManager *fm) { for (int i = 0; i < count; i++) { if (texts[i].lifetime <= 0) continue; @@ -568,15 +848,17 @@ void render_floating_texts(FloatingText *texts, int count, int shake_x, int shak int font_size = label_font_size(texts[i].label); Color color = label_color(&texts[i], a); const char *text = label_text(texts[i].label); - int text_w = MeasureText(text, font_size); - DrawText(text, x - text_w / 2, y, font_size, color); + Vector2 text_size = MeasureTextEx(fm->body_font, text, (float)font_size, NORM_CHAR_SPACE); + draw_text_body(fm->body_font, text, (float)(x - (int)text_size.x / 2), (float)y, font_size, NORM_CHAR_SPACE, + color); } else { // Numeric damage Color color = texts[i].is_critical ? (Color){255, 200, 50, a} : (Color){255, 100, 100, a}; char text[16]; snprintf(text, sizeof(text), "%d", texts[i].value); - int text_w = MeasureText(text, FONT_SIZE_FLOAT_DMG); - DrawText(text, x - text_w / 2, y, FONT_SIZE_FLOAT_DMG, color); + Vector2 text_size = MeasureTextEx(fm->body_font, text, (float)FONT_SIZE_FLOAT_DMG, NORM_CHAR_SPACE); + draw_text_body(fm->body_font, text, (float)(x - (int)text_size.x / 2), (float)y, FONT_SIZE_FLOAT_DMG, + NORM_CHAR_SPACE, color); } } } diff --git a/src/render.h b/src/render.h index d29b273..e36bcdb 100644 --- a/src/render.h +++ b/src/render.h @@ -99,20 +99,24 @@ int init_fonts(FontManager *fm); // Unload all fonts held by a FontManager void destroy_fonts(FontManager *fm); -// Render the map tiles -void render_map(const Map *map); +// Render the map tiles using tileset atlas +void render_map(const Map *map, const Tileset *tileset); -// Render the player -void render_player(const Player *p); +// Render the player using tileset atlas +// frame_counter is used for idle breathing animation +void render_player(const Player *p, const Tileset *tileset, int frame_counter); -// Render all enemies -void render_enemies(const Enemy *enemies, int count, const unsigned char visible[MAP_HEIGHT][MAP_WIDTH]); +// Render all enemies using tileset atlas +// frame_counter is used for idle breathing animation +void render_enemies(const Enemy *enemies, int count, const unsigned char visible[MAP_HEIGHT][MAP_WIDTH], + const Tileset *tileset, int frame_counter); -// Render all items -void render_items(const Item *items, int count, const unsigned char visible[MAP_HEIGHT][MAP_WIDTH]); +// Render all items using tileset atlas +void render_items(const Item *items, int count, const unsigned char visible[MAP_HEIGHT][MAP_WIDTH], + const Tileset *tileset); // Render UI overlay -void render_ui(const Player *p, const FontManager *fm); +void render_ui(const Player *p, const Tileset *tileset, const FontManager *fm); // Render action log (bottom left corner) void render_action_log(const char log[5][128], int count, int head, const FontManager *fm); @@ -121,7 +125,10 @@ void render_action_log(const char log[5][128], int count, int head, const FontMa void render_inventory_overlay(const Player *p, int selected, const FontManager *fm); // Render floating damage text -void render_floating_texts(FloatingText *texts, int count, int shake_x, int shake_y); +void render_floating_texts(FloatingText *texts, int count, int shake_x, int shake_y, const FontManager *fm); + +// Render slash effect during attacks +void render_slash_effect(int x, int y, DamageClass dmg_class, int timer); // Render end screen (victory or death) with stats breakdown void render_end_screen(int is_victory, int kills, int items, int damage_dealt, int damage_taken, int crits,