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:
parent
fdc0657237
commit
9cbbb9636f
5 changed files with 140 additions and 28 deletions
72
src/combat.c
72
src/combat.c
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
21
src/common.h
21
src/common.h
|
|
@ -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
|
||||||
|
|
|
||||||
41
src/player.c
41
src/player.c
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue