render: use tileset atlas for all entity and tile rendering; anims

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Idb42cff72368e26d8d44db79ba9c413a6a6a6964
This commit is contained in:
raf 2026-04-28 15:32:56 +03:00
commit 5b640dcefd
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
7 changed files with 556 additions and 90 deletions

View file

@ -1,6 +1,8 @@
#include "render.h"
#include "items.h"
#include "settings.h"
#include "map/utils.h"
#include <math.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
@ -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);
}
}
}