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

185
libs/combat/attack.zig Normal file
View file

@ -0,0 +1,185 @@
const c = @import("c.zig");
const event = @import("event.zig");
const effects = @import("effects.zig");
fn rng(min: c_int, max: c_int) c_int {
return c.rng_int(min, max);
}
fn applyResistance(damage: c_int, resistance: c_int) c_int {
if (resistance >= 100) return 0;
const factor = 100 - resistance;
var result = @divTrunc(damage * factor, 100);
if (result < 1) result = 1;
return result;
}
fn rollProc(
target_effects: [*c]c.StatusEffect,
target_count: [*c]c_int,
dmg_class: c.DamageClass,
status_chance: c_int,
) c.StatusEffectType {
if (status_chance <= 0 or rng(0, 99) >= status_chance)
return c.EFFECT_NONE;
const eff_type = effects.procForClass(dmg_class);
if (eff_type == c.EFFECT_NONE)
return c.EFFECT_NONE;
const params = effects.paramsFor(eff_type);
effects.apply(target_effects, target_count, eff_type, params.duration, params.intensity);
return eff_type;
}
pub fn playerAttack(p: [*c]c.Player, e: [*c]c.Enemy) void {
if (p == null or e == null) return;
if (e[0].alive == 0) return;
event.reset();
if (e[0].dodge > 0 and rng(0, 99) < e[0].dodge) {
event.last.is_player_damage = 0;
event.last.was_dodged = 1;
event.last.message = "Enemy dodged!";
return;
}
// Read weapon stats or unarmed defaults
var dmg_class: c.DamageClass = c.DMG_IMPACT;
var crit_chance: c_int = c.UNARMED_CRIT_CHANCE;
var crit_mult: c_int = c.UNARMED_CRIT_MULT;
var status_chance: c_int = c.UNARMED_STATUS_CHANCE;
if (p[0].has_weapon != 0) {
dmg_class = p[0].equipped_weapon.dmg_class;
crit_chance = p[0].equipped_weapon.crit_chance;
crit_mult = p[0].equipped_weapon.crit_multiplier;
status_chance = p[0].equipped_weapon.status_chance;
}
var base_attack = p[0].attack;
if (effects.has(&p[0].effects, p[0].effect_count, c.EFFECT_WEAKEN))
base_attack -= c.WEAKEN_ATTACK_REDUCTION;
if (base_attack < 1) base_attack = 1;
const variance = rng(80, 120);
var damage = @divTrunc(base_attack * variance, 100);
if (damage < 1) damage = 1;
if (rng(0, 99) < crit_chance) {
damage = @divTrunc(damage * crit_mult, 100);
event.last.is_critical = 1;
}
const res_index: usize = @intCast(dmg_class);
if (res_index < c.NUM_DMG_CLASSES) {
damage = applyResistance(damage, e[0].resistance[res_index]);
}
if (damage == 0) {
event.last.damage = 0;
event.last.is_player_damage = 0;
event.last.message = "No effect!";
return;
}
if (e[0].block > 0 and rng(0, 99) < 30) {
var blocked = e[0].block;
if (blocked > damage) blocked = damage;
damage -= blocked;
if (damage < 1) damage = 1;
event.last.was_blocked = 1;
event.last.block_amount = blocked;
}
e[0].hp -= damage;
event.last.damage = damage;
event.last.is_player_damage = 0;
const applied = rollProc(&e[0].effects, &e[0].effect_count, dmg_class, status_chance);
event.last.applied_effect = applied;
if (e[0].hp <= 0) {
e[0].hp = 0;
e[0].alive = 0;
event.last.message = "Enemy killed!";
} else if (applied != c.EFFECT_NONE) {
event.last.message = switch (applied) {
c.EFFECT_BLEED => "Hit! Bleeding!",
c.EFFECT_STUN => "Hit! Stunned!",
c.EFFECT_WEAKEN => "Hit! Weakened!",
c.EFFECT_BURN => "Hit! Burning!",
c.EFFECT_POISON => "Hit! Poisoned!",
else => "You hit",
};
} else if (event.last.is_critical != 0) {
event.last.message = "Critical hit!";
} else {
event.last.message = "You hit";
}
}
pub fn enemyAttack(e: [*c]c.Enemy, p: [*c]c.Player) void {
if (e == null or p == null) return;
if (e[0].alive == 0) return;
event.reset();
event.last.is_player_damage = 1;
if (p[0].dodge > 0 and rng(0, 99) < p[0].dodge) {
event.last.was_dodged = 1;
event.last.message = "You dodged!";
return;
}
var base_damage = e[0].attack;
if (effects.has(&e[0].effects, e[0].effect_count, c.EFFECT_WEAKEN))
base_damage -= c.WEAKEN_ATTACK_REDUCTION;
base_damage -= p[0].defense;
if (base_damage < 1) base_damage = 1;
const variance = rng(80, 120);
var damage = @divTrunc(base_damage * variance, 100);
if (damage < 1) damage = 1;
if (rng(0, 99) < c.ENEMY_CRIT_CHANCE) {
damage = @divTrunc(damage * c.ENEMY_CRIT_MULT, 100);
event.last.is_critical = 1;
}
if (p[0].block > 0 and rng(0, 99) < 30) {
var blocked = p[0].block;
if (blocked > damage) blocked = damage;
damage -= blocked;
if (damage < 1) damage = 1;
event.last.was_blocked = 1;
event.last.block_amount = blocked;
}
p[0].hp -= damage;
event.last.damage = damage;
const applied = rollProc(&p[0].effects, &p[0].effect_count, e[0].dmg_class, e[0].status_chance);
event.last.applied_effect = applied;
if (p[0].hp <= 0) {
p[0].hp = 0;
event.last.message = "You died!";
} else if (applied != c.EFFECT_NONE) {
event.last.message = switch (applied) {
c.EFFECT_POISON => "Hit! Poisoned!",
c.EFFECT_BLEED => "Hit! Bleeding!",
c.EFFECT_STUN => "Hit! Stunned!",
c.EFFECT_BURN => "Hit! Burning!",
c.EFFECT_WEAKEN => "Hit! Weakened!",
else => "Hit",
};
} else if (event.last.was_blocked != 0) {
event.last.message = "Blocked some damage";
} else if (event.last.is_critical != 0) {
event.last.message = "Critical!";
} else {
event.last.message = "Hit";
}
}