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 = .{};
|
||||||
|
}
|
||||||
123
src/combat.c
123
src/combat.c
|
|
@ -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;
|
|
||||||
}
|
|
||||||
26
src/combat.h
26
src/combat.h
|
|
@ -15,6 +15,18 @@ int combat_was_player_damage(void);
|
||||||
// Was it a critical hit?
|
// Was it a critical hit?
|
||||||
int combat_was_critical(void);
|
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
|
// Reset combat event
|
||||||
void combat_reset_event(void);
|
void combat_reset_event(void);
|
||||||
|
|
||||||
|
|
@ -24,4 +36,18 @@ void combat_player_attack(Player *p, Enemy *e);
|
||||||
// Enemy attacks player
|
// Enemy attacks player
|
||||||
void combat_enemy_attack(Enemy *e, Player *p);
|
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
|
#endif // COMBAT_H
|
||||||
|
|
|
||||||
33
src/common.h
33
src/common.h
|
|
@ -6,6 +6,19 @@
|
||||||
// Tile types
|
// Tile types
|
||||||
typedef enum { TILE_WALL, TILE_FLOOR, TILE_STAIRS } TileType;
|
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
|
// Room
|
||||||
typedef struct {
|
typedef struct {
|
||||||
int x, y, w, h;
|
int x, y, w, h;
|
||||||
|
|
@ -35,6 +48,10 @@ typedef struct {
|
||||||
int power;
|
int power;
|
||||||
int floor;
|
int floor;
|
||||||
int picked_up;
|
int picked_up;
|
||||||
|
DamageClass dmg_class;
|
||||||
|
int crit_chance;
|
||||||
|
int crit_multiplier;
|
||||||
|
int status_chance;
|
||||||
} Item;
|
} Item;
|
||||||
|
|
||||||
// Player
|
// Player
|
||||||
|
|
@ -47,15 +64,17 @@ typedef struct {
|
||||||
int step_count;
|
int step_count;
|
||||||
int speed; // actions per 100 ticks (100 = 1 action per turn)
|
int speed; // actions per 100 ticks (100 = 1 action per turn)
|
||||||
int cooldown; // countdown to next action (0 = can act)
|
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;
|
Item equipped_weapon;
|
||||||
int has_weapon;
|
int has_weapon;
|
||||||
Item equipped_armor;
|
Item equipped_armor;
|
||||||
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)
|
// status effects
|
||||||
int dmg_variance_min; // minimum damage multiplier (80 = 0.8x)
|
StatusEffect effects[MAX_EFFECTS];
|
||||||
int dmg_variance_max; // maximum damage multiplier (120 = 1.2x)
|
int effect_count;
|
||||||
} Player;
|
} Player;
|
||||||
|
|
||||||
// Enemy types
|
// Enemy types
|
||||||
|
|
@ -71,6 +90,14 @@ typedef struct {
|
||||||
EnemyType type;
|
EnemyType type;
|
||||||
int speed; // actions per 100 ticks
|
int speed; // actions per 100 ticks
|
||||||
int cooldown; // countdown to next action
|
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;
|
} Enemy;
|
||||||
|
|
||||||
// Floating damage text
|
// Floating damage text
|
||||||
|
|
|
||||||
39
src/enemy.c
39
src/enemy.c
|
|
@ -3,6 +3,7 @@
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include "map.h"
|
#include "map.h"
|
||||||
#include "rng.h"
|
#include "rng.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
// Forward declaration
|
// Forward declaration
|
||||||
int is_enemy_at(const Enemy *enemies, int count, int x, int y);
|
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
|
// Create enemy
|
||||||
Enemy e;
|
Enemy e;
|
||||||
|
memset(&e, 0, sizeof(Enemy));
|
||||||
e.x = ex;
|
e.x = ex;
|
||||||
e.y = ey;
|
e.y = ey;
|
||||||
e.alive = 1;
|
e.alive = 1;
|
||||||
e.type = rng_int(ENEMY_GOBLIN, max_type);
|
e.type = rng_int(ENEMY_GOBLIN, max_type);
|
||||||
|
e.effect_count = 0;
|
||||||
|
|
||||||
// Stats based on type and floor
|
// Stats based on type and floor
|
||||||
switch (e.type) {
|
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.hp = e.max_hp;
|
||||||
e.attack = ENEMY_BASE_ATTACK;
|
e.attack = ENEMY_BASE_ATTACK;
|
||||||
e.speed = 55 + rng_int(0, 10);
|
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;
|
break;
|
||||||
case ENEMY_SKELETON:
|
case ENEMY_SKELETON:
|
||||||
e.max_hp = ENEMY_BASE_HP + floor + 2;
|
e.max_hp = ENEMY_BASE_HP + floor + 2;
|
||||||
e.hp = e.max_hp;
|
e.hp = e.max_hp;
|
||||||
e.attack = ENEMY_BASE_ATTACK + 1;
|
e.attack = ENEMY_BASE_ATTACK + 1;
|
||||||
e.speed = 70 + rng_int(0, 10);
|
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;
|
break;
|
||||||
case ENEMY_ORC:
|
case ENEMY_ORC:
|
||||||
e.max_hp = ENEMY_BASE_HP + floor + 4;
|
e.max_hp = ENEMY_BASE_HP + floor + 4;
|
||||||
e.hp = e.max_hp;
|
e.hp = e.max_hp;
|
||||||
e.attack = ENEMY_BASE_ATTACK + 2;
|
e.attack = ENEMY_BASE_ATTACK + 2;
|
||||||
e.speed = 85 + rng_int(0, 10);
|
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;
|
break;
|
||||||
default:
|
default:
|
||||||
e.max_hp = ENEMY_BASE_HP;
|
e.max_hp = ENEMY_BASE_HP;
|
||||||
e.hp = e.max_hp;
|
e.hp = e.max_hp;
|
||||||
e.attack = ENEMY_BASE_ATTACK;
|
e.attack = ENEMY_BASE_ATTACK;
|
||||||
e.speed = 60;
|
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;
|
break;
|
||||||
}
|
}
|
||||||
e.cooldown = e.speed;
|
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)
|
if (!e->alive)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Stunned enemies skip their action
|
||||||
|
if (combat_has_effect(e->effects, e->effect_count, EFFECT_STUN))
|
||||||
|
return;
|
||||||
|
|
||||||
// Check if adjacent to player - attack
|
// Check if adjacent to player - attack
|
||||||
if (can_see_player(e, p)) {
|
if (can_see_player(e, p)) {
|
||||||
combat_enemy_attack(e, p);
|
combat_enemy_attack(e, p);
|
||||||
|
|
|
||||||
68
src/items.c
68
src/items.c
|
|
@ -1,8 +1,24 @@
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include "map.h"
|
#include "map.h"
|
||||||
#include "rng.h"
|
#include "rng.h"
|
||||||
|
#include "settings.h"
|
||||||
#include <stddef.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) {
|
void item_spawn(Item items[], int *count, Map *map, int floor) {
|
||||||
*count = 0;
|
*count = 0;
|
||||||
|
|
||||||
|
|
@ -33,6 +49,10 @@ void item_spawn(Item items[], int *count, Map *map, int floor) {
|
||||||
item.y = iy;
|
item.y = iy;
|
||||||
item.floor = floor;
|
item.floor = floor;
|
||||||
item.picked_up = 0;
|
item.picked_up = 0;
|
||||||
|
item.dmg_class = DMG_SLASH;
|
||||||
|
item.crit_chance = 0;
|
||||||
|
item.crit_multiplier = 100;
|
||||||
|
item.status_chance = 0;
|
||||||
|
|
||||||
// Item type distribution
|
// Item type distribution
|
||||||
int type_roll = rng_int(0, 99);
|
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) {
|
if (type_roll < 50) {
|
||||||
// 50% chance for potion
|
// 50% chance for potion
|
||||||
item.type = ITEM_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) {
|
} else if (type_roll < 80) {
|
||||||
// 30% chance for weapon
|
// 30% chance for weapon, pick a random template
|
||||||
item.type = ITEM_WEAPON;
|
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 {
|
} else {
|
||||||
// 20% chance for armor
|
// 20% chance for armor
|
||||||
item.type = ITEM_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)++;
|
(*count)++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -65,7 +91,20 @@ const char *item_get_name(const Item *i) {
|
||||||
case ITEM_POTION:
|
case ITEM_POTION:
|
||||||
return "Potion";
|
return "Potion";
|
||||||
case ITEM_WEAPON:
|
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:
|
case ITEM_ARMOR:
|
||||||
return "Armor";
|
return "Armor";
|
||||||
default:
|
default:
|
||||||
|
|
@ -124,3 +163,20 @@ void item_use(Player *p, Item *i) {
|
||||||
break;
|
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 "???";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,4 +20,7 @@ const char *item_get_description(const Item *i);
|
||||||
// Get item power value
|
// Get item power value
|
||||||
int item_get_power(const Item *i);
|
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
|
#endif // ITEMS_H
|
||||||
|
|
|
||||||
79
src/main.c
79
src/main.c
|
|
@ -25,14 +25,26 @@ static void add_log(GameState *gs, const char *msg) {
|
||||||
|
|
||||||
// spawn floating damage text
|
// spawn floating damage text
|
||||||
static void spawn_floating_text(GameState *gs, int x, int y, int value, int is_critical) {
|
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) {
|
if (gs->floating_count < 8) {
|
||||||
gs->floating_texts[gs->floating_count].x = x;
|
slot = gs->floating_count;
|
||||||
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;
|
|
||||||
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
|
// update floating texts and screen shake
|
||||||
|
|
@ -87,6 +99,37 @@ static void init_floor(GameState *gs, int floor_num) {
|
||||||
gs->turn_count = 0;
|
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
|
// Handle player input - returns: 0=continue, 1=acted, -1=quit
|
||||||
static int handle_input(GameState *gs) {
|
static int handle_input(GameState *gs) {
|
||||||
int dx = 0, dy = 0;
|
int dx = 0, dy = 0;
|
||||||
|
|
@ -103,6 +146,25 @@ static int handle_input(GameState *gs) {
|
||||||
return 0;
|
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 (gs->show_inventory) {
|
||||||
if (IsKeyPressed(KEY_I) || IsKeyPressed(KEY_ESCAPE)) {
|
if (IsKeyPressed(KEY_I) || IsKeyPressed(KEY_ESCAPE)) {
|
||||||
gs->show_inventory = 0;
|
gs->show_inventory = 0;
|
||||||
|
|
@ -298,6 +360,11 @@ static int handle_input(GameState *gs) {
|
||||||
// Increment turn counter
|
// Increment turn counter
|
||||||
gs->turn_count++;
|
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
|
// Check if stepped on stairs
|
||||||
if (gs->map.tiles[gs->player.y][gs->player.x] == TILE_STAIRS) {
|
if (gs->map.tiles[gs->player.y][gs->player.x] == TILE_STAIRS) {
|
||||||
gs->awaiting_descend = 1;
|
gs->awaiting_descend = 1;
|
||||||
|
|
@ -358,6 +425,8 @@ static void game_loop(void) {
|
||||||
while (!WindowShouldClose()) {
|
while (!WindowShouldClose()) {
|
||||||
// Handle input
|
// Handle input
|
||||||
if (!gs.game_over) {
|
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);
|
int quit = handle_input(&gs);
|
||||||
if (quit == -1)
|
if (quit == -1)
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
27
src/player.c
27
src/player.c
|
|
@ -3,6 +3,7 @@
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include "items.h"
|
#include "items.h"
|
||||||
#include "map.h"
|
#include "map.h"
|
||||||
|
#include "settings.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
@ -18,6 +19,8 @@ void player_init(Player *p, int x, int y) {
|
||||||
p->step_count = 0;
|
p->step_count = 0;
|
||||||
p->speed = 100;
|
p->speed = 100;
|
||||||
p->cooldown = 0;
|
p->cooldown = 0;
|
||||||
|
p->dodge = PLAYER_BASE_DODGE;
|
||||||
|
p->block = PLAYER_BASE_BLOCK;
|
||||||
p->has_weapon = 0;
|
p->has_weapon = 0;
|
||||||
p->has_armor = 0;
|
p->has_armor = 0;
|
||||||
memset(&p->equipped_weapon, 0, sizeof(Item));
|
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_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->effect_count = 0;
|
||||||
p->dmg_variance_max = 120;
|
memset(p->effects, 0, sizeof(p->effects));
|
||||||
|
|
||||||
// Initialize inventory to empty
|
// Initialize inventory to empty
|
||||||
for (int i = 0; i < MAX_INVENTORY; i++) {
|
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->x = new_x;
|
||||||
p->y = new_y;
|
p->y = new_y;
|
||||||
p->step_count += 1;
|
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;
|
p->hp += 1;
|
||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
|
|
@ -188,16 +195,6 @@ 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;
|
||||||
|
|
@ -207,11 +204,15 @@ int player_equip_item(Player *p, int inv_index) {
|
||||||
// Unequip current armor first
|
// Unequip current armor first
|
||||||
if (p->has_armor) {
|
if (p->has_armor) {
|
||||||
p->defense -= p->equipped_armor.power;
|
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
|
// Equip new armor
|
||||||
p->equipped_armor = *item;
|
p->equipped_armor = *item;
|
||||||
p->has_armor = 1;
|
p->has_armor = 1;
|
||||||
p->defense += item->power;
|
p->defense += item->power;
|
||||||
|
p->block += item->power / 2; // armor grants block bonus
|
||||||
// Remove from inventory
|
// Remove from inventory
|
||||||
player_remove_inventory_item(p, inv_index);
|
player_remove_inventory_item(p, inv_index);
|
||||||
return 1;
|
return 1;
|
||||||
|
|
|
||||||
54
src/render.c
54
src/render.c
|
|
@ -138,24 +138,62 @@ void render_ui(const Player *p) {
|
||||||
int hp_text_w = MeasureText(hp_text, 14);
|
int hp_text_w = MeasureText(hp_text, 14);
|
||||||
DrawText(hp_text, bar_x + (bar_width - hp_text_w) / 2, bar_y + 2, 14, WHITE);
|
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
|
// 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;
|
int stats_y = bar_y;
|
||||||
DrawText("F1", bar_x + bar_width + 15, stats_y, 14, WHITE);
|
DrawText("F1", stats_x_start, stats_y, 14, WHITE);
|
||||||
DrawText("ATK", bar_x + bar_width + 50, stats_y, 14, YELLOW);
|
DrawText("ATK", stats_x_start + 35, stats_y, 14, YELLOW);
|
||||||
DrawText("DEF", bar_x + bar_width + 100, stats_y, 14, BLUE);
|
DrawText("DEF", stats_x_start + 85, stats_y, 14, BLUE);
|
||||||
DrawText("INV", bar_x + bar_width + 145, stats_y, 14, GREEN);
|
DrawText("INV", stats_x_start + 130, stats_y, 14, GREEN);
|
||||||
|
|
||||||
// Row 2: equipment slots and controls
|
// Row 2: equipment slots and controls
|
||||||
int row2_y = stats_y + 24;
|
int row2_y = stats_y + 24;
|
||||||
|
|
||||||
// Equipment (left side of row 2)
|
// Equipment (left side of row 2)
|
||||||
if (p->has_weapon) {
|
if (p->has_weapon) {
|
||||||
char weapon_text[48];
|
char weapon_text[64];
|
||||||
snprintf(weapon_text, sizeof(weapon_text), "Wpn:%s +%d", item_get_name(&p->equipped_weapon),
|
snprintf(weapon_text, sizeof(weapon_text), "Wpn:%s +%d [%s]", item_get_name(&p->equipped_weapon),
|
||||||
p->equipped_weapon.power);
|
p->equipped_weapon.power, dmg_class_get_short(p->equipped_weapon.dmg_class));
|
||||||
DrawText(weapon_text, 10, row2_y, 12, YELLOW);
|
DrawText(weapon_text, 10, row2_y, 12, YELLOW);
|
||||||
} else {
|
} 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) {
|
if (p->has_armor) {
|
||||||
|
|
|
||||||
|
|
@ -25,4 +25,31 @@
|
||||||
#define NUM_FLOORS 5
|
#define NUM_FLOORS 5
|
||||||
#define MAX_INVENTORY 10
|
#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
|
#endif // SETTINGS_H
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue