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:
parent
7af642612b
commit
22ab6fc6eb
15 changed files with 802 additions and 158 deletions
185
libs/combat/attack.zig
Normal file
185
libs/combat/attack.zig
Normal 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";
|
||||
}
|
||||
}
|
||||
43
libs/combat/c.zig
Normal file
43
libs/combat/c.zig
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
pub const raw = @cImport({
|
||||
@cInclude("common.h");
|
||||
@cInclude("rng.h");
|
||||
});
|
||||
|
||||
pub const StatusEffectType = raw.StatusEffectType;
|
||||
pub const StatusEffect = raw.StatusEffect;
|
||||
pub const Player = raw.Player;
|
||||
pub const Enemy = raw.Enemy;
|
||||
pub const DamageClass = raw.DamageClass;
|
||||
|
||||
pub const EFFECT_NONE = raw.EFFECT_NONE;
|
||||
pub const EFFECT_POISON = raw.EFFECT_POISON;
|
||||
pub const EFFECT_STUN = raw.EFFECT_STUN;
|
||||
pub const EFFECT_BLEED = raw.EFFECT_BLEED;
|
||||
pub const EFFECT_WEAKEN = raw.EFFECT_WEAKEN;
|
||||
pub const EFFECT_BURN = raw.EFFECT_BURN;
|
||||
|
||||
pub const DMG_SLASH = raw.DMG_SLASH;
|
||||
pub const DMG_IMPACT = raw.DMG_IMPACT;
|
||||
pub const DMG_PIERCE = raw.DMG_PIERCE;
|
||||
pub const DMG_FIRE = raw.DMG_FIRE;
|
||||
pub const DMG_POISON = raw.DMG_POISON;
|
||||
|
||||
pub const ENEMY_GOBLIN = raw.ENEMY_GOBLIN;
|
||||
pub const ENEMY_SKELETON = raw.ENEMY_SKELETON;
|
||||
pub const ENEMY_ORC = raw.ENEMY_ORC;
|
||||
|
||||
pub const MAX_EFFECTS = raw.MAX_EFFECTS;
|
||||
pub const NUM_DMG_CLASSES = raw.NUM_DMG_CLASSES;
|
||||
pub const WEAKEN_ATTACK_REDUCTION = raw.WEAKEN_ATTACK_REDUCTION;
|
||||
pub const POISON_BASE_DAMAGE = raw.POISON_BASE_DAMAGE;
|
||||
pub const BLEED_STACK_DAMAGE = raw.BLEED_STACK_DAMAGE;
|
||||
pub const BURN_BASE_DAMAGE = raw.BURN_BASE_DAMAGE;
|
||||
|
||||
pub const UNARMED_CRIT_CHANCE = raw.UNARMED_CRIT_CHANCE;
|
||||
pub const UNARMED_CRIT_MULT = raw.UNARMED_CRIT_MULT;
|
||||
pub const UNARMED_STATUS_CHANCE = raw.UNARMED_STATUS_CHANCE;
|
||||
|
||||
pub const ENEMY_CRIT_CHANCE = raw.ENEMY_CRIT_CHANCE;
|
||||
pub const ENEMY_CRIT_MULT = raw.ENEMY_CRIT_MULT;
|
||||
|
||||
pub const rng_int = raw.rng_int;
|
||||
84
libs/combat/combat.zig
Normal file
84
libs/combat/combat.zig
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
const c = @import("c.zig");
|
||||
const event = @import("event.zig");
|
||||
const fx = @import("effects.zig");
|
||||
const atk = @import("attack.zig");
|
||||
|
||||
comptime {
|
||||
_ = @import("c.zig");
|
||||
_ = @import("event.zig");
|
||||
_ = @import("effects.zig");
|
||||
_ = @import("attack.zig");
|
||||
}
|
||||
|
||||
export fn combat_get_last_message() [*c]const u8 {
|
||||
return event.last.message;
|
||||
}
|
||||
|
||||
export fn combat_get_last_damage() c_int {
|
||||
return event.last.damage;
|
||||
}
|
||||
|
||||
export fn combat_was_player_damage() c_int {
|
||||
return event.last.is_player_damage;
|
||||
}
|
||||
|
||||
export fn combat_was_critical() c_int {
|
||||
return event.last.is_critical;
|
||||
}
|
||||
|
||||
export fn combat_was_dodged() c_int {
|
||||
return event.last.was_dodged;
|
||||
}
|
||||
|
||||
export fn combat_was_blocked() c_int {
|
||||
return event.last.was_blocked;
|
||||
}
|
||||
|
||||
export fn combat_get_block_amount() c_int {
|
||||
return event.last.block_amount;
|
||||
}
|
||||
|
||||
export fn combat_get_applied_effect() c.StatusEffectType {
|
||||
return event.last.applied_effect;
|
||||
}
|
||||
|
||||
export fn combat_reset_event() void {
|
||||
event.reset();
|
||||
}
|
||||
|
||||
export fn combat_has_effect(
|
||||
effects: [*c]const c.StatusEffect,
|
||||
count: c_int,
|
||||
effect_type: c.StatusEffectType,
|
||||
) c_int {
|
||||
if (effects == null) return 0;
|
||||
return if (fx.has(effects, count, effect_type)) 1 else 0;
|
||||
}
|
||||
|
||||
export fn combat_apply_effect(
|
||||
effects: [*c]c.StatusEffect,
|
||||
count: [*c]c_int,
|
||||
effect_type: c.StatusEffectType,
|
||||
duration: c_int,
|
||||
intensity: c_int,
|
||||
) void {
|
||||
if (effects == null or count == null) return;
|
||||
if (effect_type == c.EFFECT_NONE) return;
|
||||
fx.apply(effects, count, effect_type, duration, intensity);
|
||||
}
|
||||
|
||||
export fn combat_tick_effects(p: [*c]c.Player) c_int {
|
||||
return fx.tickPlayer(p);
|
||||
}
|
||||
|
||||
export fn combat_tick_enemy_effects(e: [*c]c.Enemy) c_int {
|
||||
return fx.tickEnemy(e);
|
||||
}
|
||||
|
||||
export fn combat_player_attack(p: [*c]c.Player, e: [*c]c.Enemy) void {
|
||||
atk.playerAttack(p, e);
|
||||
}
|
||||
|
||||
export fn combat_enemy_attack(e: [*c]c.Enemy, p: [*c]c.Player) void {
|
||||
atk.enemyAttack(e, p);
|
||||
}
|
||||
151
libs/combat/effects.zig
Normal file
151
libs/combat/effects.zig
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
const c = @import("c.zig");
|
||||
|
||||
pub const EffectParams = struct {
|
||||
duration: c_int,
|
||||
intensity: c_int,
|
||||
};
|
||||
|
||||
pub fn procForClass(dmg_class: c.DamageClass) c.StatusEffectType {
|
||||
return switch (dmg_class) {
|
||||
c.DMG_SLASH => c.EFFECT_BLEED,
|
||||
c.DMG_IMPACT => c.EFFECT_STUN,
|
||||
c.DMG_PIERCE => c.EFFECT_WEAKEN,
|
||||
c.DMG_FIRE => c.EFFECT_BURN,
|
||||
c.DMG_POISON => c.EFFECT_POISON,
|
||||
else => c.EFFECT_NONE,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn name(effect: c.StatusEffectType) ?[*:0]const u8 {
|
||||
return switch (effect) {
|
||||
c.EFFECT_POISON => "Poisoned",
|
||||
c.EFFECT_BLEED => "Bleeding",
|
||||
c.EFFECT_STUN => "Stunned",
|
||||
c.EFFECT_BURN => "Burning",
|
||||
c.EFFECT_WEAKEN => "Weakened",
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn paramsFor(effect: c.StatusEffectType) EffectParams {
|
||||
return switch (effect) {
|
||||
c.EFFECT_BLEED => .{ .duration = 4, .intensity = c.BLEED_STACK_DAMAGE },
|
||||
c.EFFECT_STUN => .{ .duration = 1, .intensity = 0 },
|
||||
c.EFFECT_WEAKEN => .{ .duration = 3, .intensity = c.WEAKEN_ATTACK_REDUCTION },
|
||||
c.EFFECT_BURN => .{ .duration = 2, .intensity = c.BURN_BASE_DAMAGE },
|
||||
c.EFFECT_POISON => .{ .duration = 5, .intensity = c.POISON_BASE_DAMAGE },
|
||||
else => .{ .duration = 0, .intensity = 0 },
|
||||
};
|
||||
}
|
||||
|
||||
pub fn clampCount(count: c_int) usize {
|
||||
if (count < 0) return 0;
|
||||
if (count > c.MAX_EFFECTS) return @intCast(c.MAX_EFFECTS);
|
||||
return @intCast(count);
|
||||
}
|
||||
|
||||
pub fn has(effects: [*c]const c.StatusEffect, count: c_int, effect_type: c.StatusEffectType) bool {
|
||||
const safe_count = clampCount(count);
|
||||
for (0..safe_count) |i| {
|
||||
if (effects[i].type == effect_type and effects[i].duration > 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn apply(
|
||||
effects: [*c]c.StatusEffect,
|
||||
count: [*c]c_int,
|
||||
effect_type: c.StatusEffectType,
|
||||
duration: c_int,
|
||||
intensity: c_int,
|
||||
) void {
|
||||
const safe_count = clampCount(count[0]);
|
||||
|
||||
for (0..safe_count) |i| {
|
||||
if (effects[i].type == effect_type and effects[i].duration > 0) {
|
||||
if (effect_type == c.EFFECT_BLEED) {
|
||||
effects[i].intensity += intensity;
|
||||
if (effects[i].duration < duration)
|
||||
effects[i].duration = duration;
|
||||
} else {
|
||||
effects[i].duration = duration;
|
||||
if (intensity > effects[i].intensity)
|
||||
effects[i].intensity = intensity;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (safe_count < @as(usize, @intCast(c.MAX_EFFECTS))) {
|
||||
effects[safe_count] = .{
|
||||
.type = effect_type,
|
||||
.duration = duration,
|
||||
.intensity = intensity,
|
||||
};
|
||||
count[0] = @intCast(safe_count + 1);
|
||||
}
|
||||
}
|
||||
|
||||
fn compact(effects: [*c]c.StatusEffect, count: [*c]c_int) void {
|
||||
var write: usize = 0;
|
||||
const safe_count = clampCount(count[0]);
|
||||
|
||||
for (0..safe_count) |read| {
|
||||
if (effects[read].duration > 0) {
|
||||
if (write != read)
|
||||
effects[write] = effects[read];
|
||||
write += 1;
|
||||
}
|
||||
}
|
||||
count[0] = @intCast(write);
|
||||
}
|
||||
|
||||
fn tickOne(eff: *c.StatusEffect, hp: *c_int) c_int {
|
||||
if (eff.duration <= 0) return 0;
|
||||
|
||||
var dmg: c_int = 0;
|
||||
switch (eff.type) {
|
||||
c.EFFECT_POISON, c.EFFECT_BLEED, c.EFFECT_BURN => {
|
||||
dmg = eff.intensity;
|
||||
hp.* -= dmg;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
eff.duration -= 1;
|
||||
return dmg;
|
||||
}
|
||||
|
||||
pub fn tickPlayer(p: [*c]c.Player) c_int {
|
||||
if (p == null) return 0;
|
||||
|
||||
var total: c_int = 0;
|
||||
const safe_count = clampCount(p[0].effect_count);
|
||||
|
||||
for (0..safe_count) |i| {
|
||||
total += tickOne(&p[0].effects[i], &p[0].hp);
|
||||
}
|
||||
|
||||
compact(&p[0].effects, &p[0].effect_count);
|
||||
return total;
|
||||
}
|
||||
|
||||
pub fn tickEnemy(e: [*c]c.Enemy) c_int {
|
||||
if (e == null) return 0;
|
||||
if (e[0].alive == 0) return 0;
|
||||
|
||||
var total: c_int = 0;
|
||||
const safe_count = clampCount(e[0].effect_count);
|
||||
|
||||
for (0..safe_count) |i| {
|
||||
total += tickOne(&e[0].effects[i], &e[0].hp);
|
||||
}
|
||||
|
||||
if (e[0].hp <= 0) {
|
||||
e[0].hp = 0;
|
||||
e[0].alive = 0;
|
||||
}
|
||||
|
||||
compact(&e[0].effects, &e[0].effect_count);
|
||||
return total;
|
||||
}
|
||||
18
libs/combat/event.zig
Normal file
18
libs/combat/event.zig
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
const c = @import("c.zig");
|
||||
|
||||
pub const CombatEvent = struct {
|
||||
message: [*c]const u8 = null,
|
||||
damage: c_int = 0,
|
||||
is_player_damage: c_int = 0,
|
||||
is_critical: c_int = 0,
|
||||
was_dodged: c_int = 0,
|
||||
was_blocked: c_int = 0,
|
||||
block_amount: c_int = 0,
|
||||
applied_effect: c.StatusEffectType = c.EFFECT_NONE,
|
||||
};
|
||||
|
||||
pub var last: CombatEvent = .{};
|
||||
|
||||
pub fn reset() void {
|
||||
last = .{};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue