1
0
Fork 0
forked from NotAShelf/rogged

combat: add hit chance & damage variance to make combat more engaging

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ia1e0a503dba03e5df7b863b97db962e36a6a6964
This commit is contained in:
raf 2026-04-03 15:44:57 +03:00
commit 9cbbb9636f
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
5 changed files with 140 additions and 28 deletions

View file

@ -1,5 +1,6 @@
#include "combat.h" #include "combat.h"
#include "common.h" #include "common.h"
#include "rng.h"
#include <stddef.h> #include <stddef.h>
// Track combat events for feedback // Track combat events for feedback
@ -7,9 +8,10 @@ typedef struct {
const char *message; const char *message;
int damage; int damage;
int is_player_damage; int is_player_damage;
int is_critical;
} CombatEvent; } CombatEvent;
static CombatEvent last_event = {NULL, 0, 0}; static CombatEvent last_event = {NULL, 0, 0, 0};
const char *combat_get_last_message(void) { const char *combat_get_last_message(void) {
return last_event.message; return last_event.message;
@ -23,25 +25,48 @@ int combat_was_player_damage(void) {
return last_event.is_player_damage; return last_event.is_player_damage;
} }
int combat_was_critical(void) {
return last_event.is_critical;
}
void combat_player_attack(Player *p, Enemy *e) { void combat_player_attack(Player *p, Enemy *e) {
if (e == NULL || !e->alive) if (e == NULL || !e->alive)
return; return;
// Deal damage last_event.is_critical = 0;
int damage = p->attack;
e->hp -= damage;
// Set combat event // 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;
// 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.damage = damage;
last_event.is_player_damage = 0; last_event.is_player_damage = 0;
// Check if enemy died
if (e->hp <= 0) { if (e->hp <= 0) {
e->hp = 0; e->hp = 0;
e->alive = 0; e->alive = 0;
last_event.message = "Enemy killed!"; last_event.message = "Enemy killed!";
} else if (last_event.is_critical) {
last_event.message = "Critical hit!";
} else { } else {
last_event.message = "You hit the enemy"; last_event.message = "You hit";
}
} else {
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) if (p == NULL)
return; return;
// Deal damage reduced by defense (minimum 1 damage) last_event.is_critical = 0;
int damage = e->attack - p->defense;
// 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;
int variance = rng_int(80, 120);
int damage = (base_damage * variance) / 100;
if (damage < 1) if (damage < 1)
damage = 1; damage = 1;
p->hp -= damage;
// Set combat event // 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.damage = damage;
last_event.is_player_damage = 1; last_event.is_player_damage = 1;
// Check if player died
if (p->hp <= 0) { if (p->hp <= 0) {
p->hp = 0; p->hp = 0;
last_event.message = "You died!"; last_event.message = "You died!";
} else if (last_event.is_critical) {
last_event.message = "Critical!";
} else { } else {
last_event.message = "Enemy attacks!"; last_event.message = "Hit";
}
} else {
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.message = NULL;
last_event.damage = 0; last_event.damage = 0;
last_event.is_player_damage = 0; last_event.is_player_damage = 0;
last_event.is_critical = 0;
} }

View file

@ -12,10 +12,13 @@ int combat_get_last_damage(void);
// Was last damage to player? // Was last damage to player?
int combat_was_player_damage(void); int combat_was_player_damage(void);
// Was it a critical hit?
int combat_was_critical(void);
// Reset combat event // Reset combat event
void combat_reset_event(void); void combat_reset_event(void);
// Player attacks enemy // Player attacks enemy (pass player for damage variance)
void combat_player_attack(Player *p, Enemy *e); void combat_player_attack(Player *p, Enemy *e);
// Enemy attacks player // Enemy attacks player

View file

@ -53,6 +53,9 @@ typedef struct {
int has_armor; int has_armor;
Item inventory[MAX_INVENTORY]; Item inventory[MAX_INVENTORY];
int inventory_count; 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; } Player;
// Enemy types // Enemy types
@ -70,6 +73,14 @@ typedef struct {
int cooldown; // countdown to next action int cooldown; // countdown to next action
} Enemy; } 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 // GameState - encapsulates all game state for testability and save/load
typedef struct { typedef struct {
Player player; Player player;
@ -87,6 +98,16 @@ typedef struct {
int awaiting_descend; // 0 = normal, 1 = waiting for Y/N int awaiting_descend; // 0 = normal, 1 = waiting for Y/N
int show_inventory; // 0 = hidden, 1 = show overlay int show_inventory; // 0 = hidden, 1 = show overlay
int inv_selected; // currently selected inventory index 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; } GameState;
#endif // COMMON_H #endif // COMMON_H

View file

@ -25,6 +25,8 @@ void player_init(Player *p, int x, int y) {
p->equipped_weapon.picked_up = 1; p->equipped_weapon.picked_up = 1;
p->equipped_armor.picked_up = 1; // mark as invalid p->equipped_armor.picked_up = 1; // mark as invalid
p->inventory_count = 0; p->inventory_count = 0;
p->dmg_variance_min = 80;
p->dmg_variance_max = 120;
// Initialize inventory to empty // Initialize inventory to empty
for (int i = 0; i < MAX_INVENTORY; i++) { 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->equipped_weapon = *item;
p->has_weapon = 1; p->has_weapon = 1;
p->attack += item->power; 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 // Remove from inventory
player_remove_inventory_item(p, inv_index); player_remove_inventory_item(p, inv_index);
return 1; return 1;
@ -205,5 +217,32 @@ int player_equip_item(Player *p, int inv_index) {
return 1; 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
} }

View file

@ -33,4 +33,7 @@ void player_remove_inventory_item(Player *p, int index);
// Equip weapon/armor from inventory, return 1 if successful // Equip weapon/armor from inventory, return 1 if successful
int player_equip_item(Player *p, int inv_index); 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 #endif // PLAYER_H