combat: nicer UI with floating labels, HP bar colors, world shake & audio

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I9e1720b112a0a5ceab64da56735f4fb36a6a6964
This commit is contained in:
raf 2026-04-05 22:35:07 +03:00
commit e14af1f9f0
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
2 changed files with 137 additions and 41 deletions

View file

@ -61,12 +61,20 @@ void render_enemies(const Enemy *enemies, int count) {
DrawRectangleRec(rect, enemy_color);
// Draw hp bar above enemy
int hp_percent = (enemies[i].hp * TILE_SIZE) / enemies[i].max_hp;
if (hp_percent > 0) {
Rectangle hp_bar = {(float)(enemies[i].x * TILE_SIZE), (float)(enemies[i].y * TILE_SIZE - 4), (float)hp_percent,
// Draw hp bar above enemy, color-coded by health remaining
int hp_pixels = (enemies[i].hp * TILE_SIZE) / enemies[i].max_hp;
if (hp_pixels > 0) {
float hp_ratio = (float)enemies[i].hp / (float)enemies[i].max_hp;
Color bar_color;
if (hp_ratio > 0.5f)
bar_color = (Color){60, 180, 60, 255}; // green
else if (hp_ratio > 0.25f)
bar_color = (Color){200, 180, 40, 255}; // yellow
else
bar_color = (Color){200, 60, 60, 255}; // red
Rectangle hp_bar = {(float)(enemies[i].x * TILE_SIZE), (float)(enemies[i].y * TILE_SIZE - 4), (float)hp_pixels,
3};
DrawRectangleRec(hp_bar, GREEN);
DrawRectangleRec(hp_bar, bar_color);
}
}
}
@ -287,6 +295,35 @@ void render_inventory_overlay(const Player *p, int selected) {
(Color){65, 65, 65, 255});
}
static Color label_color(FloatingText *ft, int alpha) {
if (ft->label[0] == '\0')
return (Color){255, 100, 100, alpha}; // numeric damage default
if (strcmp(ft->label, "DODGE") == 0)
return (Color){160, 160, 160, alpha};
if (strcmp(ft->label, "BLOCK") == 0)
return (Color){80, 130, 220, alpha};
if (strcmp(ft->label, "CRIT!") == 0)
return (Color){255, 200, 50, alpha};
if (strcmp(ft->label, "SLAIN") == 0)
return (Color){220, 50, 50, alpha};
// Proc label, color driven by effect_type stored in the struct
switch (ft->effect_type) {
case EFFECT_POISON:
return (Color){50, 200, 50, alpha};
case EFFECT_BLEED:
return (Color){200, 50, 50, alpha};
case EFFECT_BURN:
return (Color){230, 130, 30, alpha};
case EFFECT_STUN:
return (Color){200, 200, 50, alpha};
case EFFECT_WEAKEN:
return (Color){120, 120, 120, alpha};
default:
return (Color){200, 200, 200, alpha};
}
}
void render_floating_texts(FloatingText *texts, int count, int shake_x, int shake_y) {
for (int i = 0; i < count; i++) {
if (texts[i].lifetime <= 0)
@ -294,15 +331,23 @@ void render_floating_texts(FloatingText *texts, int count, int shake_x, int shak
int x = texts[i].x + shake_x;
int y = texts[i].y + shake_y - (60 - texts[i].lifetime); // rise over time
float alpha = (float)texts[i].lifetime / 60.0f;
Color color =
texts[i].is_critical ? (Color){255, 200, 50, (int)(255 * alpha)} : (Color){255, 100, 100, (int)(255 * alpha)};
int a = (int)(255 * alpha);
char text[16];
snprintf(text, sizeof(text), "%d", texts[i].value);
int text_w = MeasureText(text, 18);
DrawText(text, x - text_w / 2, y, 18, color);
if (texts[i].label[0] != '\0') {
// Label text (DODGE, BLOCK, CRIT!, proc name, SLAIN)
int font_size = (texts[i].label[0] == 'C') ? 16 : 14; // CRIT! slightly larger
Color color = label_color(&texts[i], a);
int text_w = MeasureText(texts[i].label, font_size);
DrawText(texts[i].label, x - text_w / 2, y, font_size, 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, 18);
DrawText(text, x - text_w / 2, y, 18, color);
}
}
}
@ -327,8 +372,8 @@ void render_message(const char *message) {
int msg_len = strlen(message);
float msg_ratio = 13.5;
// Draw message box
// Draw message box
// TODO: Separate out the calculation of the x/y and width/height so that if a message takes up more than, say,
// 75% of the screen width, we add a line break and increase the height. That would then require calculating the
// width based on the longest line.