From 9cbbb9636f998e9e1209358753ff5a417e22dbb0 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 3 Apr 2026 15:44:57 +0300 Subject: [PATCH] combat: add hit chance & damage variance to make combat more engaging Signed-off-by: NotAShelf Change-Id: Ia1e0a503dba03e5df7b863b97db962e36a6a6964 --- src/combat.c | 98 ++++++++++++++++++++++++++++++++++++++-------------- src/combat.h | 5 ++- src/common.h | 21 +++++++++++ src/player.c | 41 +++++++++++++++++++++- src/player.h | 3 ++ 5 files changed, 140 insertions(+), 28 deletions(-) diff --git a/src/combat.c b/src/combat.c index 064e4c8..fbb66cc 100644 --- a/src/combat.c +++ b/src/combat.c @@ -1,5 +1,6 @@ #include "combat.h" #include "common.h" +#include "rng.h" #include // Track combat events for feedback @@ -7,9 +8,10 @@ typedef struct { const char *message; int damage; int is_player_damage; + int is_critical; } CombatEvent; -static CombatEvent last_event = {NULL, 0, 0}; +static CombatEvent last_event = {NULL, 0, 0, 0}; const char *combat_get_last_message(void) { return last_event.message; @@ -23,25 +25,48 @@ int combat_was_player_damage(void) { return last_event.is_player_damage; } +int combat_was_critical(void) { + return last_event.is_critical; +} + void combat_player_attack(Player *p, Enemy *e) { if (e == NULL || !e->alive) return; - // Deal damage - int damage = p->attack; - e->hp -= damage; + last_event.is_critical = 0; - // Set combat event - last_event.damage = damage; - last_event.is_player_damage = 0; + // 90% hit chance + if (rng_int(0, 99) < 90) { + // calculate damage with variance from player stats + int base_damage = p->attack; + int variance = rng_int(p->dmg_variance_min, p->dmg_variance_max); + int damage = (base_damage * variance) / 100; + if (damage < 1) + damage = 1; - // Check if enemy died - if (e->hp <= 0) { - e->hp = 0; - e->alive = 0; - last_event.message = "Enemy killed!"; + // 10% critical hit chance for 1.5x + if (rng_int(0, 9) == 0) { + damage = (damage * 3) / 2; + last_event.is_critical = 1; + } + + e->hp -= damage; + last_event.damage = damage; + last_event.is_player_damage = 0; + + if (e->hp <= 0) { + e->hp = 0; + e->alive = 0; + last_event.message = "Enemy killed!"; + } else if (last_event.is_critical) { + last_event.message = "Critical hit!"; + } else { + last_event.message = "You hit"; + } } else { - last_event.message = "You hit the enemy"; + last_event.damage = 0; + last_event.is_player_damage = 0; + last_event.message = "You missed"; } } @@ -51,22 +76,42 @@ void combat_enemy_attack(Enemy *e, Player *p) { if (p == NULL) return; - // Deal damage reduced by defense (minimum 1 damage) - int damage = e->attack - p->defense; - if (damage < 1) - damage = 1; - p->hp -= damage; + last_event.is_critical = 0; - // Set combat event - last_event.damage = damage; - last_event.is_player_damage = 1; + // 85% hit chance for enemies + if (rng_int(0, 99) < 85) { + // calculate damage with variance + int base_damage = e->attack - p->defense; + if (base_damage < 1) + base_damage = 1; - // Check if player died - if (p->hp <= 0) { - p->hp = 0; - last_event.message = "You died!"; + int variance = rng_int(80, 120); + int damage = (base_damage * variance) / 100; + if (damage < 1) + damage = 1; + + // 5% critical hit chance for enemies + if (rng_int(0, 19) == 0) { + damage = (damage * 3) / 2; + last_event.is_critical = 1; + } + + p->hp -= damage; + last_event.damage = damage; + last_event.is_player_damage = 1; + + if (p->hp <= 0) { + p->hp = 0; + last_event.message = "You died!"; + } else if (last_event.is_critical) { + last_event.message = "Critical!"; + } else { + last_event.message = "Hit"; + } } else { - last_event.message = "Enemy attacks!"; + last_event.damage = 0; + last_event.is_player_damage = 1; + last_event.message = "Missed"; } } @@ -74,4 +119,5 @@ void combat_reset_event(void) { last_event.message = NULL; last_event.damage = 0; last_event.is_player_damage = 0; + last_event.is_critical = 0; } diff --git a/src/combat.h b/src/combat.h index e969754..c53f6a7 100644 --- a/src/combat.h +++ b/src/combat.h @@ -12,10 +12,13 @@ int combat_get_last_damage(void); // Was last damage to player? int combat_was_player_damage(void); +// Was it a critical hit? +int combat_was_critical(void); + // Reset combat event void combat_reset_event(void); -// Player attacks enemy +// Player attacks enemy (pass player for damage variance) void combat_player_attack(Player *p, Enemy *e); // Enemy attacks player diff --git a/src/common.h b/src/common.h index 30b1c3c..b963c3b 100644 --- a/src/common.h +++ b/src/common.h @@ -53,6 +53,9 @@ typedef struct { int has_armor; Item inventory[MAX_INVENTORY]; int inventory_count; + // damage variance range (0.8 to 1.2 = 80 to 120) + int dmg_variance_min; // minimum damage multiplier (80 = 0.8x) + int dmg_variance_max; // maximum damage multiplier (120 = 1.2x) } Player; // Enemy types @@ -70,6 +73,14 @@ typedef struct { int cooldown; // countdown to next action } Enemy; +// Floating damage text +typedef struct { + int x, y; + int value; + int lifetime; // frames remaining + int is_critical; +} FloatingText; + // GameState - encapsulates all game state for testability and save/load typedef struct { Player player; @@ -87,6 +98,16 @@ typedef struct { int awaiting_descend; // 0 = normal, 1 = waiting for Y/N int show_inventory; // 0 = hidden, 1 = show overlay int inv_selected; // currently selected inventory index + // action log + char action_log[5][128]; + int log_count; + int log_head; + // visual effects + FloatingText floating_texts[8]; + int floating_count; + int screen_shake; // frames of screen shake remaining + int shake_x; + int shake_y; } GameState; #endif // COMMON_H diff --git a/src/player.c b/src/player.c index f6a0e30..218d855 100644 --- a/src/player.c +++ b/src/player.c @@ -25,6 +25,8 @@ void player_init(Player *p, int x, int y) { p->equipped_weapon.picked_up = 1; p->equipped_armor.picked_up = 1; // mark as invalid p->inventory_count = 0; + p->dmg_variance_min = 80; + p->dmg_variance_max = 120; // Initialize inventory to empty for (int i = 0; i < MAX_INVENTORY; i++) { @@ -186,6 +188,16 @@ int player_equip_item(Player *p, int inv_index) { p->equipped_weapon = *item; p->has_weapon = 1; p->attack += item->power; + // Adjust damage variance based on weapon power + // Higher power = wider range (more swingy but higher potential) + int min_var = 100 - (item->power * 3); + int max_var = 100 + (item->power * 5); + if (min_var < 60) + min_var = 60; + if (max_var > 150) + max_var = 150; + p->dmg_variance_min = min_var; + p->dmg_variance_max = max_var; // Remove from inventory player_remove_inventory_item(p, inv_index); return 1; @@ -205,5 +217,32 @@ int player_equip_item(Player *p, int inv_index) { return 1; } - return 0; // Not equippable (potion) + return 0; // not equippable (potion) +} + +int player_drop_item(Player *p, int inv_index, Item *items, int item_count) { + if (p == NULL) + return 0; + if (inv_index < 0 || inv_index >= MAX_INVENTORY) + return 0; + + Item *item = player_get_inventory_item(p, inv_index); + if (item == NULL) + return 0; + + // Find an empty slot in items array to place the dropped item + for (int i = 0; i < item_count; i++) { + if (items[i].picked_up) { + // Place dropped item at this position + items[i] = *item; + items[i].x = p->x; + items[i].y = p->y; + items[i].picked_up = 0; + // Remove from inventory + player_remove_inventory_item(p, inv_index); + return 1; + } + } + + return 0; // no room to drop } diff --git a/src/player.h b/src/player.h index a0460bd..7d6c151 100644 --- a/src/player.h +++ b/src/player.h @@ -33,4 +33,7 @@ void player_remove_inventory_item(Player *p, int index); // Equip weapon/armor from inventory, return 1 if successful int player_equip_item(Player *p, int inv_index); +// Drop item from inventory at index (returns it to floor), return 1 if successful +int player_drop_item(Player *p, int inv_index, Item *items, int item_count); + #endif // PLAYER_H