1
0
Fork 0
forked from NotAShelf/rogged

combat: rewrite in Zig; add basic damage types and weapon archetypes

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ic8055a1cf6bdad1aca13673ea171b4b46a6a6964
This commit is contained in:
raf 2026-04-05 20:11:06 +03:00
commit 22ab6fc6eb
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
15 changed files with 802 additions and 158 deletions

View file

@ -1,123 +0,0 @@
#include "combat.h"
#include "common.h"
#include "rng.h"
#include <stddef.h>
// Track combat events for feedback
typedef struct {
const char *message;
int damage;
int is_player_damage;
int is_critical;
} CombatEvent;
static CombatEvent last_event = {NULL, 0, 0, 0};
const char *combat_get_last_message(void) {
return last_event.message;
}
int combat_get_last_damage(void) {
return last_event.damage;
}
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;
last_event.is_critical = 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;
// 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.damage = 0;
last_event.is_player_damage = 0;
last_event.message = "You missed";
}
}
void combat_enemy_attack(Enemy *e, Player *p) {
if (e == NULL || !e->alive)
return;
if (p == NULL)
return;
last_event.is_critical = 0;
// 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)
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.damage = 0;
last_event.is_player_damage = 1;
last_event.message = "Missed";
}
}
void combat_reset_event(void) {
last_event.message = NULL;
last_event.damage = 0;
last_event.is_player_damage = 0;
last_event.is_critical = 0;
}

View file

@ -15,6 +15,18 @@ int combat_was_player_damage(void);
// Was it a critical hit?
int combat_was_critical(void);
// Was the attack dodged?
int combat_was_dodged(void);
// Was the attack blocked?
int combat_was_blocked(void);
// Get block amount from last event
int combat_get_block_amount(void);
// Get the status effect applied in last event
StatusEffectType combat_get_applied_effect(void);
// Reset combat event
void combat_reset_event(void);
@ -24,4 +36,18 @@ void combat_player_attack(Player *p, Enemy *e);
// Enemy attacks player
void combat_enemy_attack(Enemy *e, Player *p);
// Tick status effects on the player (call at start of turn)
// Returns total damage dealt by effects this tick
int combat_tick_effects(Player *p);
// Tick status effects on an enemy (call at start of turn)
// Returns total damage dealt by effects this tick
int combat_tick_enemy_effects(Enemy *e);
// Apply a status effect to an effect array, stacking/refreshing if already present
void combat_apply_effect(StatusEffect effects[], int *count, StatusEffectType type, int duration, int intensity);
// Check if an entity has a specific effect active
int combat_has_effect(const StatusEffect effects[], int count, StatusEffectType type);
#endif // COMBAT_H

View file

@ -6,6 +6,19 @@
// Tile types
typedef enum { TILE_WALL, TILE_FLOOR, TILE_STAIRS } TileType;
// Status effect types
typedef enum { EFFECT_NONE, EFFECT_POISON, EFFECT_STUN, EFFECT_BLEED, EFFECT_WEAKEN, EFFECT_BURN } StatusEffectType;
// Damage classes
typedef enum { DMG_SLASH, DMG_IMPACT, DMG_PIERCE, DMG_FIRE, DMG_POISON } DamageClass;
// Status effect instance
typedef struct {
StatusEffectType type;
int duration; // turns remaining
int intensity; // damage per tick or stat reduction amount
} StatusEffect;
// Room
typedef struct {
int x, y, w, h;
@ -35,6 +48,10 @@ typedef struct {
int power;
int floor;
int picked_up;
DamageClass dmg_class;
int crit_chance;
int crit_multiplier;
int status_chance;
} Item;
// Player
@ -47,15 +64,17 @@ typedef struct {
int step_count;
int speed; // actions per 100 ticks (100 = 1 action per turn)
int cooldown; // countdown to next action (0 = can act)
int dodge; // dodge chance percentage
int block; // flat damage reduction on successful block roll
Item equipped_weapon;
int has_weapon;
Item equipped_armor;
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)
// status effects
StatusEffect effects[MAX_EFFECTS];
int effect_count;
} Player;
// Enemy types
@ -71,6 +90,14 @@ typedef struct {
EnemyType type;
int speed; // actions per 100 ticks
int cooldown; // countdown to next action
int dodge; // dodge chance percentage
int block; // flat damage reduction
int resistance[NUM_DMG_CLASSES];
DamageClass dmg_class;
int status_chance;
// status effects
StatusEffect effects[MAX_EFFECTS];
int effect_count;
} Enemy;
// Floating damage text

View file

@ -3,6 +3,7 @@
#include "common.h"
#include "map.h"
#include "rng.h"
#include <string.h>
// Forward declaration
int is_enemy_at(const Enemy *enemies, int count, int x, int y);
@ -39,10 +40,12 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) {
// Create enemy
Enemy e;
memset(&e, 0, sizeof(Enemy));
e.x = ex;
e.y = ey;
e.alive = 1;
e.type = rng_int(ENEMY_GOBLIN, max_type);
e.effect_count = 0;
// Stats based on type and floor
switch (e.type) {
@ -51,24 +54,56 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) {
e.hp = e.max_hp;
e.attack = ENEMY_BASE_ATTACK;
e.speed = 55 + rng_int(0, 10);
e.dodge = 15;
e.block = 0;
e.dmg_class = DMG_POISON;
e.status_chance = 15;
e.resistance[DMG_SLASH] = 0;
e.resistance[DMG_IMPACT] = 0;
e.resistance[DMG_PIERCE] = 0;
e.resistance[DMG_FIRE] = -25;
e.resistance[DMG_POISON] = 50;
break;
case ENEMY_SKELETON:
e.max_hp = ENEMY_BASE_HP + floor + 2;
e.hp = e.max_hp;
e.attack = ENEMY_BASE_ATTACK + 1;
e.speed = 70 + rng_int(0, 10);
e.dodge = 5;
e.block = 0;
e.dmg_class = DMG_SLASH;
e.status_chance = 10;
e.resistance[DMG_SLASH] = -25;
e.resistance[DMG_IMPACT] = -50;
e.resistance[DMG_PIERCE] = 50;
e.resistance[DMG_FIRE] = 25;
e.resistance[DMG_POISON] = 100;
break;
case ENEMY_ORC:
e.max_hp = ENEMY_BASE_HP + floor + 4;
e.hp = e.max_hp;
e.attack = ENEMY_BASE_ATTACK + 2;
e.speed = 85 + rng_int(0, 10);
e.dodge = 0;
e.block = 3;
e.dmg_class = DMG_IMPACT;
e.status_chance = 20;
e.resistance[DMG_SLASH] = 0;
e.resistance[DMG_IMPACT] = 25;
e.resistance[DMG_PIERCE] = -25;
e.resistance[DMG_FIRE] = 0;
e.resistance[DMG_POISON] = 0;
break;
default:
e.max_hp = ENEMY_BASE_HP;
e.hp = e.max_hp;
e.attack = ENEMY_BASE_ATTACK;
e.speed = 60;
e.dodge = 0;
e.block = 0;
e.dmg_class = DMG_IMPACT;
e.status_chance = 0;
memset(e.resistance, 0, sizeof(e.resistance));
break;
}
e.cooldown = e.speed;
@ -137,6 +172,10 @@ void enemy_act(Enemy *e, Player *p, Map *map, Enemy *all_enemies, int enemy_coun
if (!e->alive)
return;
// Stunned enemies skip their action
if (combat_has_effect(e->effects, e->effect_count, EFFECT_STUN))
return;
// Check if adjacent to player - attack
if (can_see_player(e, p)) {
combat_enemy_attack(e, p);

View file

@ -1,8 +1,24 @@
#include "common.h"
#include "map.h"
#include "rng.h"
#include "settings.h"
#include <stddef.h>
typedef struct {
const char *name;
DamageClass dmg_class;
int base_power;
int crit_chance;
int crit_multiplier;
int status_chance;
} WeaponTemplate;
static const WeaponTemplate weapon_templates[NUM_WEAPON_TEMPLATES] = {
{"Dagger", DMG_SLASH, 1, 25, 200, 20}, {"Mace", DMG_IMPACT, 2, 10, 150, 30},
{"Spear", DMG_PIERCE, 2, 15, 175, 25}, {"Torch", DMG_FIRE, 1, 5, 150, 40},
{"Venom Blade", DMG_POISON, 1, 15, 175, 35},
};
void item_spawn(Item items[], int *count, Map *map, int floor) {
*count = 0;
@ -33,6 +49,10 @@ void item_spawn(Item items[], int *count, Map *map, int floor) {
item.y = iy;
item.floor = floor;
item.picked_up = 0;
item.dmg_class = DMG_SLASH;
item.crit_chance = 0;
item.crit_multiplier = 100;
item.status_chance = 0;
// Item type distribution
int type_roll = rng_int(0, 99);
@ -40,18 +60,24 @@ void item_spawn(Item items[], int *count, Map *map, int floor) {
if (type_roll < 50) {
// 50% chance for potion
item.type = ITEM_POTION;
item.power = 5 + rng_int(0, floor * 2); // healing: 5 + 0-2*floor
item.power = 5 + rng_int(0, floor * 2);
} else if (type_roll < 80) {
// 30% chance for weapon
// 30% chance for weapon, pick a random template
item.type = ITEM_WEAPON;
item.power = 1 + rng_int(0, floor); // attack bonus: 1 + 0-floor
int tmpl_idx = rng_int(0, NUM_WEAPON_TEMPLATES - 1);
const WeaponTemplate *tmpl = &weapon_templates[tmpl_idx];
item.power = tmpl->base_power + rng_int(0, floor);
item.dmg_class = tmpl->dmg_class;
item.crit_chance = tmpl->crit_chance;
item.crit_multiplier = tmpl->crit_multiplier;
item.status_chance = tmpl->status_chance;
} else {
// 20% chance for armor
item.type = ITEM_ARMOR;
item.power = 1 + rng_int(0, floor / 2); // defense bonus
item.power = 1 + rng_int(0, floor / 2);
}
items[i] = item;
items[*count] = item;
(*count)++;
}
}
@ -65,7 +91,20 @@ const char *item_get_name(const Item *i) {
case ITEM_POTION:
return "Potion";
case ITEM_WEAPON:
return "Weapon";
switch (i->dmg_class) {
case DMG_SLASH:
return "Dagger";
case DMG_IMPACT:
return "Mace";
case DMG_PIERCE:
return "Spear";
case DMG_FIRE:
return "Torch";
case DMG_POISON:
return "Venom Blade";
default:
return "Weapon";
}
case ITEM_ARMOR:
return "Armor";
default:
@ -124,3 +163,20 @@ void item_use(Player *p, Item *i) {
break;
}
}
const char *dmg_class_get_short(DamageClass dc) {
switch (dc) {
case DMG_SLASH:
return "SLA";
case DMG_IMPACT:
return "IMP";
case DMG_PIERCE:
return "PRC";
case DMG_FIRE:
return "FIR";
case DMG_POISON:
return "PSN";
default:
return "???";
}
}

View file

@ -20,4 +20,7 @@ const char *item_get_description(const Item *i);
// Get item power value
int item_get_power(const Item *i);
// Get short label for a damage class (SLA/IMP/PRC/FIR/PSN)
const char *dmg_class_get_short(DamageClass dc);
#endif // ITEMS_H

View file

@ -25,14 +25,26 @@ static void add_log(GameState *gs, const char *msg) {
// 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) {
gs->floating_texts[gs->floating_count].x = x;
gs->floating_texts[gs->floating_count].y = y;
gs->floating_texts[gs->floating_count].value = value;
gs->floating_texts[gs->floating_count].lifetime = 60;
gs->floating_texts[gs->floating_count].is_critical = is_critical;
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;
}
}
}
if (slot < 0)
return;
gs->floating_texts[slot].x = x;
gs->floating_texts[slot].y = y;
gs->floating_texts[slot].value = value;
gs->floating_texts[slot].lifetime = 60;
gs->floating_texts[slot].is_critical = is_critical;
}
// update floating texts and screen shake
@ -87,6 +99,37 @@ static void init_floor(GameState *gs, int floor_num) {
gs->turn_count = 0;
}
// Tick all status effects at the start of a turn
static void tick_all_effects(GameState *gs) {
// Player effects
int player_effect_dmg = combat_tick_effects(&gs->player);
if (player_effect_dmg > 0) {
spawn_floating_text(gs, gs->player.x * TILE_SIZE + 8, gs->player.y * TILE_SIZE, player_effect_dmg, 0);
gs->screen_shake = 4;
}
// Check if player died from effects
if (gs->player.hp <= 0) {
gs->player.hp = 0;
gs->game_over = 1;
return;
}
// Enemy effects
for (int i = 0; i < gs->enemy_count; i++) {
Enemy *e = &gs->enemies[i];
if (!e->alive)
continue;
int enemy_effect_dmg = combat_tick_enemy_effects(e);
if (enemy_effect_dmg > 0) {
spawn_floating_text(gs, e->x * TILE_SIZE + 8, e->y * TILE_SIZE, enemy_effect_dmg, 0);
}
if (!e->alive) {
add_log(gs, "Enemy died from effects!");
}
}
}
// Handle player input - returns: 0=continue, 1=acted, -1=quit
static int handle_input(GameState *gs) {
int dx = 0, dy = 0;
@ -103,6 +146,25 @@ static int handle_input(GameState *gs) {
return 0;
}
// If player is stunned, wait for any key then consume the turn
if (combat_has_effect(gs->player.effects, gs->player.effect_count, EFFECT_STUN)) {
if (!(IsKeyDown(KEY_W) || IsKeyDown(KEY_UP) || IsKeyDown(KEY_S) || IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_A) ||
IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_D) || IsKeyDown(KEY_RIGHT)))
return 0;
gs->turn_count++;
tick_all_effects(gs);
if (gs->game_over)
return 1;
enemy_update_all(gs->enemies, gs->enemy_count, &gs->player, &gs->map);
if (gs->player.hp <= 0) {
gs->game_over = 1;
}
gs->last_message = "You are stunned!";
gs->message_timer = 60;
add_log(gs, "Stunned! Lost a turn.");
return 1;
}
if (gs->show_inventory) {
if (IsKeyPressed(KEY_I) || IsKeyPressed(KEY_ESCAPE)) {
gs->show_inventory = 0;
@ -298,6 +360,11 @@ static int handle_input(GameState *gs) {
// Increment turn counter
gs->turn_count++;
// Tick status effects at the start of this turn
tick_all_effects(gs);
if (gs->game_over)
return 1;
// Check if stepped on stairs
if (gs->map.tiles[gs->player.y][gs->player.x] == TILE_STAIRS) {
gs->awaiting_descend = 1;
@ -358,6 +425,8 @@ static void game_loop(void) {
while (!WindowShouldClose()) {
// Handle input
if (!gs.game_over) {
// Tick status effects at the start of each frame where input is checked
// (effects tick once per player action via the acted flag below)
int quit = handle_input(&gs);
if (quit == -1)
break;

View file

@ -3,6 +3,7 @@
#include "common.h"
#include "items.h"
#include "map.h"
#include "settings.h"
#include "utils.h"
#include <stdlib.h>
#include <string.h>
@ -18,6 +19,8 @@ void player_init(Player *p, int x, int y) {
p->step_count = 0;
p->speed = 100;
p->cooldown = 0;
p->dodge = PLAYER_BASE_DODGE;
p->block = PLAYER_BASE_BLOCK;
p->has_weapon = 0;
p->has_armor = 0;
memset(&p->equipped_weapon, 0, sizeof(Item));
@ -25,8 +28,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;
p->effect_count = 0;
memset(p->effects, 0, sizeof(p->effects));
// Initialize inventory to empty
for (int i = 0; i < MAX_INVENTORY; i++) {
@ -70,7 +73,11 @@ int player_move(Player *p, int dx, int dy, Map *map, Enemy *enemies, int enemy_c
p->x = new_x;
p->y = new_y;
p->step_count += 1;
if (p->step_count % 15 == 0 && p->hp < p->max_hp) {
// Regen suppressed while poisoned, bleeding, or burning
if (p->step_count % REGEN_STEP_INTERVAL == 0 && p->hp < p->max_hp &&
!combat_has_effect(p->effects, p->effect_count, EFFECT_POISON) &&
!combat_has_effect(p->effects, p->effect_count, EFFECT_BLEED) &&
!combat_has_effect(p->effects, p->effect_count, EFFECT_BURN)) {
p->hp += 1;
}
return 1;
@ -188,16 +195,6 @@ 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;
@ -207,11 +204,15 @@ int player_equip_item(Player *p, int inv_index) {
// Unequip current armor first
if (p->has_armor) {
p->defense -= p->equipped_armor.power;
p->block -= p->equipped_armor.power / 2;
if (p->block < PLAYER_BASE_BLOCK)
p->block = PLAYER_BASE_BLOCK;
}
// Equip new armor
p->equipped_armor = *item;
p->has_armor = 1;
p->defense += item->power;
p->block += item->power / 2; // armor grants block bonus
// Remove from inventory
player_remove_inventory_item(p, inv_index);
return 1;

View file

@ -138,24 +138,62 @@ void render_ui(const Player *p) {
int hp_text_w = MeasureText(hp_text, 14);
DrawText(hp_text, bar_x + (bar_width - hp_text_w) / 2, bar_y + 2, 14, WHITE);
// Status effect indicators next to HP bar
int effect_x = bar_x + bar_width + 5;
for (int i = 0; i < p->effect_count && i < MAX_EFFECTS; i++) {
Color eff_color;
const char *eff_label = "";
switch (p->effects[i].type) {
case EFFECT_POISON:
eff_color = (Color){50, 200, 50, 255};
eff_label = "PSN";
break;
case EFFECT_BLEED:
eff_color = (Color){200, 50, 50, 255};
eff_label = "BLD";
break;
case EFFECT_STUN:
eff_color = (Color){200, 200, 50, 255};
eff_label = "STN";
break;
case EFFECT_WEAKEN:
eff_color = (Color){120, 120, 120, 255};
eff_label = "WKN";
break;
case EFFECT_BURN:
eff_color = (Color){230, 130, 30, 255};
eff_label = "BRN";
break;
default:
continue;
}
if (p->effects[i].duration > 0) {
char eff_text[16];
snprintf(eff_text, sizeof(eff_text), "%s%d", eff_label, p->effects[i].duration);
DrawText(eff_text, effect_x, bar_y, 12, eff_color);
effect_x += 40;
}
}
// Stats row 1: Floor, ATK, DEF, Inv
int stats_x_start = (effect_x > bar_x + bar_width + 15) ? effect_x + 10 : bar_x + bar_width + 15;
int stats_y = bar_y;
DrawText("F1", bar_x + bar_width + 15, stats_y, 14, WHITE);
DrawText("ATK", bar_x + bar_width + 50, stats_y, 14, YELLOW);
DrawText("DEF", bar_x + bar_width + 100, stats_y, 14, BLUE);
DrawText("INV", bar_x + bar_width + 145, stats_y, 14, GREEN);
DrawText("F1", stats_x_start, stats_y, 14, WHITE);
DrawText("ATK", stats_x_start + 35, stats_y, 14, YELLOW);
DrawText("DEF", stats_x_start + 85, stats_y, 14, BLUE);
DrawText("INV", stats_x_start + 130, stats_y, 14, GREEN);
// Row 2: equipment slots and controls
int row2_y = stats_y + 24;
// Equipment (left side of row 2)
if (p->has_weapon) {
char weapon_text[48];
snprintf(weapon_text, sizeof(weapon_text), "Wpn:%s +%d", item_get_name(&p->equipped_weapon),
p->equipped_weapon.power);
char weapon_text[64];
snprintf(weapon_text, sizeof(weapon_text), "Wpn:%s +%d [%s]", item_get_name(&p->equipped_weapon),
p->equipped_weapon.power, dmg_class_get_short(p->equipped_weapon.dmg_class));
DrawText(weapon_text, 10, row2_y, 12, YELLOW);
} else {
DrawText("Wpn:---", 10, row2_y, 12, (Color){60, 60, 60, 255});
DrawText("Wpn:--- [IMP]", 10, row2_y, 12, (Color){60, 60, 60, 255});
}
if (p->has_armor) {

View file

@ -25,4 +25,31 @@
#define NUM_FLOORS 5
#define MAX_INVENTORY 10
// Damage Classes
#define NUM_DMG_CLASSES 5
// Status Effects
#define MAX_EFFECTS 4
#define POISON_BASE_DAMAGE 1
#define BLEED_STACK_DAMAGE 1
#define BURN_BASE_DAMAGE 2
#define WEAKEN_ATTACK_REDUCTION 2
#define REGEN_STEP_INTERVAL 15
// Unarmed combat defaults
#define UNARMED_CRIT_CHANCE 5
#define UNARMED_CRIT_MULT 150
#define UNARMED_STATUS_CHANCE 0
// Weapon templates
#define NUM_WEAPON_TEMPLATES 5
// Enemy combat defaults
#define ENEMY_CRIT_CHANCE 5
#define ENEMY_CRIT_MULT 150
// Dodge/Block defaults
#define PLAYER_BASE_DODGE 5
#define PLAYER_BASE_BLOCK 0
#endif // SETTINGS_H