forked from NotAShelf/rogged
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:
parent
c495dc1d7e
commit
e14af1f9f0
2 changed files with 137 additions and 41 deletions
105
src/main.c
105
src/main.c
|
|
@ -23,21 +23,20 @@ static void add_log(GameState *gs, const char *msg) {
|
|||
}
|
||||
}
|
||||
|
||||
// Reuse an expired float slot, or claim the next free one
|
||||
static int float_slot(GameState *gs) {
|
||||
if (gs->floating_count < 8)
|
||||
return gs->floating_count++;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
if (gs->floating_texts[i].lifetime <= 0)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// spawn floating damage text
|
||||
static void spawn_floating_text(GameState *gs, int x, int y, int value, int is_critical) {
|
||||
// Reuse an expired slot if all slots are taken
|
||||
int slot = -1;
|
||||
if (gs->floating_count < 8) {
|
||||
slot = gs->floating_count;
|
||||
gs->floating_count++;
|
||||
} else {
|
||||
for (int i = 0; i < 8; i++) {
|
||||
if (gs->floating_texts[i].lifetime <= 0) {
|
||||
slot = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
int slot = float_slot(gs);
|
||||
if (slot < 0)
|
||||
return;
|
||||
gs->floating_texts[slot].x = x;
|
||||
|
|
@ -45,6 +44,34 @@ static void spawn_floating_text(GameState *gs, int x, int y, int value, int is_c
|
|||
gs->floating_texts[slot].value = value;
|
||||
gs->floating_texts[slot].lifetime = 60;
|
||||
gs->floating_texts[slot].is_critical = is_critical;
|
||||
gs->floating_texts[slot].label[0] = '\0'; // numeric, no label
|
||||
gs->floating_texts[slot].effect_type = EFFECT_NONE;
|
||||
}
|
||||
|
||||
// spawn floating label text (DODGE, BLOCK, CRIT!, proc names, SLAIN)
|
||||
static void spawn_floating_label(GameState *gs, int x, int y, const char *label, StatusEffectType effect_type) {
|
||||
int slot = float_slot(gs);
|
||||
if (slot < 0)
|
||||
return;
|
||||
gs->floating_texts[slot].x = x;
|
||||
gs->floating_texts[slot].y = y;
|
||||
gs->floating_texts[slot].value = 0;
|
||||
gs->floating_texts[slot].lifetime = 60;
|
||||
gs->floating_texts[slot].is_critical = 0;
|
||||
gs->floating_texts[slot].effect_type = effect_type;
|
||||
strncpy(gs->floating_texts[slot].label, label, 7);
|
||||
gs->floating_texts[slot].label[7] = '\0';
|
||||
}
|
||||
|
||||
static const char *proc_label_for(StatusEffectType effect) {
|
||||
switch (effect) {
|
||||
case EFFECT_POISON: return "POISON!";
|
||||
case EFFECT_BLEED: return "BLEED!";
|
||||
case EFFECT_BURN: return "BURN!";
|
||||
case EFFECT_STUN: return "STUN!";
|
||||
case EFFECT_WEAKEN: return "WEAKEN!";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
// update floating texts and screen shake
|
||||
|
|
@ -130,7 +157,8 @@ static void tick_all_effects(GameState *gs) {
|
|||
}
|
||||
}
|
||||
|
||||
static void post_action(GameState *gs) {
|
||||
// attacked_enemy: the enemy the player attacked this turn, or NULL if player only moved
|
||||
static void post_action(GameState *gs, Enemy *attacked_enemy) {
|
||||
gs->turn_count++;
|
||||
|
||||
// Tick status effects at the start of this turn
|
||||
|
|
@ -146,13 +174,33 @@ static void post_action(GameState *gs) {
|
|||
return;
|
||||
}
|
||||
|
||||
// combat feedback - player attacked enemy
|
||||
if (combat_get_last_damage() > 0 && !combat_was_player_damage()) {
|
||||
for (int i = 0; i < gs->enemy_count; i++) {
|
||||
if (!gs->enemies[i].alive) {
|
||||
spawn_floating_text(gs, gs->enemies[i].x * TILE_SIZE + 8, gs->enemies[i].y * TILE_SIZE,
|
||||
combat_get_last_damage(), combat_was_critical());
|
||||
break;
|
||||
// combat feedback - player attacked an enemy this turn
|
||||
if (attacked_enemy != NULL) {
|
||||
int ex = attacked_enemy->x * TILE_SIZE + 8;
|
||||
int ey = attacked_enemy->y * TILE_SIZE;
|
||||
|
||||
if (combat_was_dodged()) {
|
||||
spawn_floating_label(gs, ex, ey, "DODGE", EFFECT_NONE);
|
||||
audio_play_dodge();
|
||||
} else {
|
||||
if (combat_get_last_damage() > 0)
|
||||
spawn_floating_text(gs, ex, ey, combat_get_last_damage(), combat_was_critical());
|
||||
if (combat_was_blocked()) {
|
||||
spawn_floating_label(gs, ex, ey - 10, "BLOCK", EFFECT_NONE);
|
||||
audio_play_block();
|
||||
}
|
||||
if (combat_was_critical()) {
|
||||
spawn_floating_label(gs, ex + 8, ey - 10, "CRIT!", EFFECT_NONE);
|
||||
audio_play_crit();
|
||||
}
|
||||
StatusEffectType applied = combat_get_applied_effect();
|
||||
if (applied != EFFECT_NONE) {
|
||||
spawn_floating_label(gs, ex, ey - 20, proc_label_for(applied), applied);
|
||||
audio_play_proc();
|
||||
}
|
||||
if (!attacked_enemy->alive) {
|
||||
spawn_floating_label(gs, ex, ey - 20, "SLAIN", EFFECT_NONE);
|
||||
audio_play_enemy_death();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -383,7 +431,7 @@ static int handle_movement_input(GameState *gs) {
|
|||
}
|
||||
|
||||
if (action)
|
||||
post_action(gs);
|
||||
post_action(gs, target); // target is NULL on move, enemy ptr on attack
|
||||
|
||||
return action;
|
||||
}
|
||||
|
|
@ -456,15 +504,18 @@ static void game_loop(void) {
|
|||
BeginDrawing();
|
||||
ClearBackground(BLACK);
|
||||
|
||||
// Draw game elements (with screen shake offset)
|
||||
if (gs.screen_shake > 0) {
|
||||
// Apply shake offset to drawing
|
||||
}
|
||||
|
||||
// Draw game world with screen shake applied via camera offset
|
||||
Camera2D cam = {0};
|
||||
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);
|
||||
render_enemies(gs.enemies, gs.enemy_count);
|
||||
render_player(&gs.player);
|
||||
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);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue