diff --git a/.editorconfig b/.editorconfig index 5b9877a..368d066 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,6 +5,8 @@ charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true +indent_size = 2 +tab_width = 2 [*.c] ident_style = space diff --git a/.gitignore b/.gitignore index a5869bb..a53104b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,12 @@ -# ignore build artifacts -result +# Nix +/.direnv/ +result* + +# Build artifacts build obj roguelike + +# Zig .zig-cache +.zig-out diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..a425cb3 --- /dev/null +++ b/Justfile @@ -0,0 +1,20 @@ +# Build the project +build: + zig build + +# Build and run +dev: + zig build run + +# Clean build artifacts +clean: + rm -rf zig-out .zig-cache + +# Format all C source files +fmt: + clang-format -i src/*.c src/*.h + zig fmt libs/combat/*.zig + +# Check formatting +fmt-check: + clang-format --dry-run --Werror src/*.c src/*.h diff --git a/Makefile b/Makefile deleted file mode 100644 index 607167e..0000000 --- a/Makefile +++ /dev/null @@ -1,45 +0,0 @@ -# Makefile for Roguelike Game -# Requires raylib, pkg-config - -CC := cc -CFLAGS := -Wall -Wextra -O2 -std=c99 -Isrc -LDFLAGS := -lraylib -lm -lpthread -ldl -lrt - -TARGET := roguelike -SRCDIR := src -OBJDIR := obj - -SOURCES := $(wildcard $(SRCDIR)/*.c) -OBJECTS := $(patsubst $(SRCDIR)/%.c,$(OBJDIR)/%.o,$(SOURCES)) - -.PHONY: all clean format format-check - -all: $(TARGET) - -$(TARGET): $(OBJECTS) - $(CC) $^ -o $@ $(LDFLAGS) - -$(OBJDIR)/%.o: $(SRCDIR)/%.c - @mkdir -p $(dir $@) - $(CC) $(CFLAGS) -c $< -o $@ - -clean: - rm -rf $(OBJDIR) $(TARGET) - -# Alias for development -dev: all - ./$(TARGET) - -# Format all source files with clang-format -fmt: - @command -v clang-format >/dev/null 2>&1 || { echo "Error: clang-format is missing"; exit 1; } - @echo "Formatting source files..." - @clang-format -i $(SOURCES) $(wildcard $(SRCDIR)/*.h) - @echo "Done formatting." - -# Check formatting without modifying files -fmt-check: - @command -v clang-format >/dev/null 2>&1 || { echo "Error: clang-format is missing"; exit 1; } - @echo "Checking formatting..." - @clang-format --dry-run --Werror \ - $(SOURCES) $(wildcard $(SRCDIR)/*.h) && echo "All files properly formatted." || { echo "Formatting issues found. Run 'make fmt' to fix."; exit 1; } diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..eb3ff5d --- /dev/null +++ b/build.zig @@ -0,0 +1,73 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + // Zig combat library + const combat_lib = b.addLibrary(.{ + .name = "combat", + .root_module = b.createModule(.{ + .root_source_file = b.path("libs/combat/combat.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }), + }); + combat_lib.addIncludePath(b.path("src")); + + // C sources (everything except combat, which is now Zig) + const c_sources = [_][]const u8{ + "src/audio.c", + "src/enemy.c", + "src/items.c", + "src/main.c", + "src/map.c", + "src/player.c", + "src/render.c", + "src/rng.c", + "src/settings.c", + "src/utils.c", + }; + + const c_flags = [_][]const u8{ + "-std=c99", + "-Wall", + "-Wextra", + "-O2", + }; + + // Main executable + const exe = b.addExecutable(.{ + .name = "roguelike", + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, + }), + }); + + exe.addCSourceFiles(.{ + .files = &c_sources, + .flags = &c_flags, + }); + + exe.addIncludePath(b.path("src")); + exe.linkLibrary(combat_lib); + exe.linkSystemLibrary("raylib"); + exe.linkSystemLibrary("m"); + exe.linkSystemLibrary("pthread"); + exe.linkSystemLibrary("dl"); + exe.linkSystemLibrary("rt"); + + b.installArtifact(exe); + + // Run step + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| { + run_cmd.addArgs(args); + } + const run_step = b.step("run", "Build and run the roguelike"); + run_step.dependOn(&run_cmd.step); +} diff --git a/libs/combat/attack.zig b/libs/combat/attack.zig new file mode 100644 index 0000000..38109c2 --- /dev/null +++ b/libs/combat/attack.zig @@ -0,0 +1,192 @@ +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 { + var r = resistance; + if (r > 75) r = 75; + const factor = 100 - r; + 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; + + var damage = base_attack; + + if (rng(0, 99) < crit_chance) { + damage = @divTrunc(damage * crit_mult, 100); + event.last.is_critical = 1; + } + + const variance = rng(80, 120); + damage = @divTrunc(damage * variance, 100); + if (damage < 1) damage = 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; + + var damage = base_damage; + + const e_crit_chance = if (e[0].crit_chance > 0) e[0].crit_chance else c.ENEMY_CRIT_CHANCE; + const e_crit_mult = if (e[0].crit_mult > 0) e[0].crit_mult else c.ENEMY_CRIT_MULT; + if (rng(0, 99) < e_crit_chance) { + damage = @divTrunc(damage * e_crit_mult, 100); + event.last.is_critical = 1; + } + + const variance = rng(80, 120); + damage = @divTrunc(damage * variance, 100); + if (damage < 1) damage = 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"; + } +} diff --git a/libs/combat/c.zig b/libs/combat/c.zig new file mode 100644 index 0000000..2890d81 --- /dev/null +++ b/libs/combat/c.zig @@ -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; diff --git a/libs/combat/combat.zig b/libs/combat/combat.zig new file mode 100644 index 0000000..870a277 --- /dev/null +++ b/libs/combat/combat.zig @@ -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); +} diff --git a/libs/combat/effects.zig b/libs/combat/effects.zig new file mode 100644 index 0000000..a665422 --- /dev/null +++ b/libs/combat/effects.zig @@ -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 = 2, .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; +} diff --git a/libs/combat/event.zig b/libs/combat/event.zig new file mode 100644 index 0000000..1084e25 --- /dev/null +++ b/libs/combat/event.zig @@ -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 = .{}; +} diff --git a/nix/package.nix b/nix/package.nix index a097561..8fb5c18 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -1,6 +1,12 @@ -{stdenv}: +{ + lib, + stdenv, + zig, + raylib, + pkg-config, +}: stdenv.mkDerivation (finalAttrs: { - pname = "sample-c-cpp"; + pname = "rogged"; version = "0.0.1"; src = builtins.path { @@ -8,5 +14,32 @@ stdenv.mkDerivation (finalAttrs: { name = finalAttrs.pname; }; - makeFlags = ["PREFIX=$(out)"]; + nativeBuildInputs = [ + zig + pkg-config + ]; + + buildInputs = [raylib]; + + dontConfigure = true; + + buildPhase = '' + runHook preBuild + export ZIG_GLOBAL_CACHE_DIR="$TMPDIR/zig-cache" + zig build --release=fast + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + mkdir -p $out/bin + cp zig-out/bin/roguelike $out/bin/ + runHook postInstall + ''; + + meta = { + description = "A turn-based roguelike game"; + license = lib.licenses.mit; + mainProgram = "roguelike"; + }; }) diff --git a/nix/shell.nix b/nix/shell.nix index 23ce860..663f55b 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -2,9 +2,10 @@ mkShell, clang-tools, raylib, - gnumake, pkg-config, + just, zig, + zig-zlint, }: mkShell { strictDeps = true; @@ -14,7 +15,9 @@ mkShell { nativeBuildInputs = [ pkg-config clang-tools - gnumake + just + zig + zig-zlint ]; } diff --git a/src/audio.c b/src/audio.c index c6bcefd..b273369 100644 --- a/src/audio.c +++ b/src/audio.c @@ -98,3 +98,26 @@ void audio_play_stairs(void) { play_tone(600.0f, 0.1f, 0.3f); play_tone(800.0f, 0.15f, 0.3f); } + +void audio_play_dodge(void) { + // High-pitched whoosh + play_tone(900.0f, 0.08f, 0.3f); +} + +void audio_play_block(void) { + // Low-then-mid metallic clang + play_tone(250.0f, 0.06f, 0.5f); + play_tone(350.0f, 0.04f, 0.3f); +} + +void audio_play_crit(void) { + // Sharp crack with high-pitched follow + play_tone(600.0f, 0.05f, 0.7f); + play_tone(900.0f, 0.1f, 0.5f); +} + +void audio_play_proc(void) { + // Ascending two-tone proc chime + play_tone(500.0f, 0.08f, 0.4f); + play_tone(700.0f, 0.1f, 0.35f); +} diff --git a/src/audio.h b/src/audio.h index a574a9a..3c2cb36 100644 --- a/src/audio.h +++ b/src/audio.h @@ -25,4 +25,16 @@ void audio_play_player_damage(void); // Play stairs/level change sound void audio_play_stairs(void); +// Play dodge sound +void audio_play_dodge(void); + +// Play block sound +void audio_play_block(void); + +// Play critical hit sound +void audio_play_crit(void); + +// Play status effect proc sound +void audio_play_proc(void); + #endif // AUDIO_H diff --git a/src/combat.c b/src/combat.c deleted file mode 100644 index fbb66cc..0000000 --- a/src/combat.c +++ /dev/null @@ -1,123 +0,0 @@ -#include "combat.h" -#include "common.h" -#include "rng.h" -#include - -// 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; -} diff --git a/src/combat.h b/src/combat.h index c53f6a7..62295d3 100644 --- a/src/combat.h +++ b/src/combat.h @@ -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 diff --git a/src/common.h b/src/common.h index b963c3b..9af8771 100644 --- a/src/common.h +++ b/src/common.h @@ -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,16 @@ 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; + int crit_chance; // crit chance percentage (0-100) + int crit_mult; // crit damage multiplier percentage (e.g. 150 = 1.5x) + // status effects + StatusEffect effects[MAX_EFFECTS]; + int effect_count; } Enemy; // Floating damage text @@ -79,6 +108,8 @@ typedef struct { int value; int lifetime; // frames remaining int is_critical; + char label[8]; // non-empty -> show label instead of numeric value + StatusEffectType effect_type; // used to pick color for proc labels } FloatingText; // GameState - encapsulates all game state for testability and save/load diff --git a/src/enemy.c b/src/enemy.c index f6ca10f..9f72670 100644 --- a/src/enemy.c +++ b/src/enemy.c @@ -3,6 +3,7 @@ #include "common.h" #include "map.h" #include "rng.h" +#include // Forward declaration int is_enemy_at(const Enemy *enemies, int count, int x, int y); @@ -39,36 +40,81 @@ 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 + // Attack scales with floor: +1 per 2 floors so deeper enemies hit harder + int floor_atk = floor / 2; + switch (e.type) { case ENEMY_GOBLIN: e.max_hp = ENEMY_BASE_HP + floor; e.hp = e.max_hp; - e.attack = ENEMY_BASE_ATTACK; + e.attack = ENEMY_BASE_ATTACK + floor_atk; e.speed = 55 + rng_int(0, 10); + e.dodge = 15; + e.block = 0; + e.dmg_class = DMG_POISON; + e.status_chance = 15; + e.crit_chance = 8; + e.crit_mult = 150; + 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.attack = ENEMY_BASE_ATTACK + 1 + floor_atk; e.speed = 70 + rng_int(0, 10); + e.dodge = 5; + e.block = 0; + e.dmg_class = DMG_SLASH; + e.status_chance = 10; + e.crit_chance = 6; + e.crit_mult = 150; + e.resistance[DMG_SLASH] = -25; + e.resistance[DMG_IMPACT] = -50; + e.resistance[DMG_PIERCE] = 50; + e.resistance[DMG_FIRE] = 25; + e.resistance[DMG_POISON] = 75; break; case ENEMY_ORC: e.max_hp = ENEMY_BASE_HP + floor + 4; e.hp = e.max_hp; - e.attack = ENEMY_BASE_ATTACK + 2; + e.attack = ENEMY_BASE_ATTACK + 2 + floor_atk; e.speed = 85 + rng_int(0, 10); + e.dodge = 0; + e.block = 3; + e.dmg_class = DMG_IMPACT; + e.status_chance = 20; + e.crit_chance = 5; + e.crit_mult = 175; + 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; + e.crit_chance = ENEMY_CRIT_CHANCE; + e.crit_mult = ENEMY_CRIT_MULT; + memset(e.resistance, 0, sizeof(e.resistance)); break; } e.cooldown = e.speed; @@ -137,6 +183,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); diff --git a/src/items.c b/src/items.c index c758955..e5bf212 100644 --- a/src/items.c +++ b/src/items.c @@ -1,8 +1,24 @@ #include "common.h" #include "map.h" #include "rng.h" +#include "settings.h" #include +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 "???"; + } +} diff --git a/src/items.h b/src/items.h index fa913f2..86e40e2 100644 --- a/src/items.h +++ b/src/items.h @@ -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 diff --git a/src/main.c b/src/main.c index cf0e9d1..8313f81 100644 --- a/src/main.c +++ b/src/main.c @@ -23,15 +23,54 @@ static void add_log(GameState *gs, const char *msg) { } } +// Reuse an expired float slot, or claim the next free one +static int float_slot(GameState *gs) { + if (gs->floating_count < 8) + return gs->floating_count++; + for (int i = 0; i < 8; i++) { + if (gs->floating_texts[i].lifetime <= 0) + return i; + } + return -1; +} + // spawn floating damage text static void spawn_floating_text(GameState *gs, int x, int y, int value, int is_critical) { - 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; - gs->floating_count++; + int slot = float_slot(gs); + 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; + gs->floating_texts[slot].label[0] = '\0'; // numeric, no label + gs->floating_texts[slot].effect_type = EFFECT_NONE; +} + +// spawn floating label text (DODGE, BLOCK, CRIT!, proc names, SLAIN) +static void spawn_floating_label(GameState *gs, int x, int y, const char *label, StatusEffectType effect_type) { + int slot = float_slot(gs); + if (slot < 0) + return; + gs->floating_texts[slot].x = x; + gs->floating_texts[slot].y = y; + gs->floating_texts[slot].value = 0; + gs->floating_texts[slot].lifetime = 60; + gs->floating_texts[slot].is_critical = 0; + gs->floating_texts[slot].effect_type = effect_type; + strncpy(gs->floating_texts[slot].label, label, 7); + gs->floating_texts[slot].label[7] = '\0'; +} + +static const char *proc_label_for(StatusEffectType effect) { + switch (effect) { + case EFFECT_POISON: return "POISON!"; + case EFFECT_BLEED: return "BLEED!"; + case EFFECT_BURN: return "BURN!"; + case EFFECT_STUN: return "STUN!"; + case EFFECT_WEAKEN: return "WEAKEN!"; + default: return ""; } } @@ -87,165 +126,253 @@ static void init_floor(GameState *gs, int floor_num) { gs->turn_count = 0; } -// Handle player input - returns: 0=continue, 1=acted, -1=quit -static int handle_input(GameState *gs) { - int dx = 0, dy = 0; - - // Check for quit first (always works) - if (IsKeyPressed(KEY_Q)) { - return -1; +// 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 for restart (works during game over) - if (IsKeyPressed(KEY_R) && gs->game_over) { - memset(gs, 0, sizeof(GameState)); - init_floor(gs, 1); + // 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!"); + } + } +} + +// attacked_enemy: the enemy the player attacked this turn, or NULL if player only moved +static void post_action(GameState *gs, Enemy *attacked_enemy) { + gs->turn_count++; + + // Tick status effects at the start of this turn + tick_all_effects(gs); + if (gs->game_over) + return; + + // Check if stepped on stairs + if (gs->map.tiles[gs->player.y][gs->player.x] == TILE_STAIRS) { + gs->awaiting_descend = 1; + gs->last_message = "Descend to next floor? (Y/N)"; + gs->message_timer = 120; + return; + } + + // combat feedback - player attacked an enemy this turn + if (attacked_enemy != NULL) { + int ex = attacked_enemy->x * TILE_SIZE + 8; + int ey = attacked_enemy->y * TILE_SIZE; + + if (combat_was_dodged()) { + spawn_floating_label(gs, ex, ey, "DODGE", EFFECT_NONE); + audio_play_dodge(); + } else { + if (combat_get_last_damage() > 0) + spawn_floating_text(gs, ex, ey, combat_get_last_damage(), combat_was_critical()); + if (combat_was_blocked()) { + spawn_floating_label(gs, ex, ey - 10, "BLOCK", EFFECT_NONE); + audio_play_block(); + } + if (combat_was_critical()) { + spawn_floating_label(gs, ex + 8, ey - 10, "CRIT!", EFFECT_NONE); + audio_play_crit(); + } + StatusEffectType applied = combat_get_applied_effect(); + if (applied != EFFECT_NONE) { + spawn_floating_label(gs, ex, ey - 20, proc_label_for(applied), applied); + audio_play_proc(); + } + if (!attacked_enemy->alive) { + spawn_floating_label(gs, ex, ey - 20, "SLAIN", EFFECT_NONE); + audio_play_enemy_death(); + } + } + } + + // Enemy turns - uses speed/cooldown system + enemy_update_all(gs->enemies, gs->enemy_count, &gs->player, &gs->map); + + // Check if player took damage + if (combat_was_player_damage() && combat_get_last_damage() > 0) { + audio_play_player_damage(); + gs->screen_shake = 8; + spawn_floating_text(gs, gs->player.x * TILE_SIZE + 8, gs->player.y * TILE_SIZE, combat_get_last_damage(), + combat_was_critical()); + } + + // Set message and check game over + gs->last_message = combat_get_last_message(); + gs->message_timer = 60; + + if (gs->player.hp <= 0) + gs->game_over = 1; +} + +// If player is stunned, wait for any key then consume the turn +static int handle_stun_turn(GameState *gs) { + 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; +} + +static int handle_inventory_input(GameState *gs) { + if (IsKeyPressed(KEY_I) || IsKeyPressed(KEY_ESCAPE)) { + gs->show_inventory = 0; return 0; } - if (gs->show_inventory) { - if (IsKeyPressed(KEY_I) || IsKeyPressed(KEY_ESCAPE)) { - gs->show_inventory = 0; - return 0; - } - - if (IsKeyPressed(KEY_DOWN) || IsKeyPressed(KEY_S)) { - gs->inv_selected++; - if (gs->inv_selected >= gs->player.inventory_count) { - gs->inv_selected = 0; - } - return 0; - } - if (IsKeyPressed(KEY_UP) || IsKeyPressed(KEY_W)) { - if (gs->inv_selected == 0) { - gs->inv_selected = (gs->player.inventory_count > 0) ? gs->player.inventory_count - 1 : 0; - } else { - gs->inv_selected--; - } - return 0; - } - - if (IsKeyPressed(KEY_ONE)) + if (IsKeyPressed(KEY_DOWN) || IsKeyPressed(KEY_S)) { + gs->inv_selected++; + if (gs->inv_selected >= gs->player.inventory_count) gs->inv_selected = 0; - if (IsKeyPressed(KEY_TWO)) - gs->inv_selected = 1; - if (IsKeyPressed(KEY_THREE)) - gs->inv_selected = 2; - if (IsKeyPressed(KEY_FOUR)) - gs->inv_selected = 3; - if (IsKeyPressed(KEY_FIVE)) - gs->inv_selected = 4; - if (IsKeyPressed(KEY_SIX)) - gs->inv_selected = 5; - if (IsKeyPressed(KEY_SEVEN)) - gs->inv_selected = 6; - if (IsKeyPressed(KEY_EIGHT)) - gs->inv_selected = 7; - if (IsKeyPressed(KEY_NINE)) - gs->inv_selected = 8; - if (IsKeyPressed(KEY_ZERO)) - gs->inv_selected = 9; - - // E to equip selected item - if (IsKeyPressed(KEY_E)) { - if (player_equip_item(&gs->player, gs->inv_selected)) { - gs->last_message = "Item equipped!"; - gs->message_timer = 60; - add_log(gs, "Equipped item"); - if (gs->inv_selected >= gs->player.inventory_count && gs->inv_selected > 0) { - gs->inv_selected--; - } - return 1; - } else { - gs->last_message = "Cannot equip that!"; - gs->message_timer = 60; - } - } - - // U or Enter to use selected item - if (IsKeyPressed(KEY_U) || IsKeyPressed(KEY_ENTER)) { - if (gs->player.inventory_count > 0) { - Item *item = player_get_inventory_item(&gs->player, gs->inv_selected); - if (item != NULL) { - if (item->type == ITEM_POTION) { - player_use_item(&gs->player, item); - player_remove_inventory_item(&gs->player, gs->inv_selected); - gs->last_message = "Used potion!"; - gs->message_timer = 60; - add_log(gs, "Used potion"); - gs->show_inventory = 0; - return 1; - } else { - gs->last_message = "Equip weapons/armor with E!"; - gs->message_timer = 60; - } - } - } - } - - // D to drop selected item - if (IsKeyPressed(KEY_D)) { - if (gs->player.inventory_count > 0) { - Item *item = player_get_inventory_item(&gs->player, gs->inv_selected); - if (item != NULL) { - char drop_msg[64]; - snprintf(drop_msg, sizeof(drop_msg), "Dropped %s", item_get_name(item)); - if (player_drop_item(&gs->player, gs->inv_selected, gs->items, gs->item_count)) { - add_log(gs, drop_msg); - gs->last_message = "Item dropped!"; - gs->message_timer = 60; - if (gs->inv_selected >= gs->player.inventory_count && gs->inv_selected > 0) { - gs->inv_selected--; - } - return 1; - } else { - gs->last_message = "Cannot drop!"; - gs->message_timer = 60; - } - } - } - } - + return 0; + } + if (IsKeyPressed(KEY_UP) || IsKeyPressed(KEY_W)) { + gs->inv_selected = (gs->inv_selected == 0) ? (gs->player.inventory_count > 0 ? gs->player.inventory_count - 1 : 0) + : gs->inv_selected - 1; return 0; } - // Handle descend confirmation - if (gs->awaiting_descend) { - if (IsKeyPressed(KEY_Y)) { - // Descend - if (gs->player.floor < NUM_FLOORS) { - audio_play_stairs(); - init_floor(gs, gs->player.floor + 1); - gs->last_message = "Descended to next floor!"; - gs->message_timer = 60; - add_log(gs, "Descended stairs"); - gs->awaiting_descend = 0; - return 1; - } else { - gs->game_won = 1; - gs->game_over = 1; - gs->awaiting_descend = 0; - return 1; - } - } - if (IsKeyPressed(KEY_N)) { - gs->awaiting_descend = 0; - gs->last_message = "Stayed on floor."; + if (IsKeyPressed(KEY_ONE)) + gs->inv_selected = 0; + if (IsKeyPressed(KEY_TWO)) + gs->inv_selected = 1; + if (IsKeyPressed(KEY_THREE)) + gs->inv_selected = 2; + if (IsKeyPressed(KEY_FOUR)) + gs->inv_selected = 3; + if (IsKeyPressed(KEY_FIVE)) + gs->inv_selected = 4; + if (IsKeyPressed(KEY_SIX)) + gs->inv_selected = 5; + if (IsKeyPressed(KEY_SEVEN)) + gs->inv_selected = 6; + if (IsKeyPressed(KEY_EIGHT)) + gs->inv_selected = 7; + if (IsKeyPressed(KEY_NINE)) + gs->inv_selected = 8; + if (IsKeyPressed(KEY_ZERO)) + gs->inv_selected = 9; + + // E to equip selected item + if (IsKeyPressed(KEY_E)) { + if (player_equip_item(&gs->player, gs->inv_selected)) { + gs->last_message = "Item equipped!"; gs->message_timer = 60; + add_log(gs, "Equipped item"); + if (gs->inv_selected >= gs->player.inventory_count && gs->inv_selected > 0) + gs->inv_selected--; return 1; } - return 0; + gs->last_message = "Cannot equip that!"; + gs->message_timer = 60; } + // U or Enter to use selected item + if (IsKeyPressed(KEY_U) || IsKeyPressed(KEY_ENTER)) { + if (gs->player.inventory_count > 0) { + Item *item = player_get_inventory_item(&gs->player, gs->inv_selected); + if (item != NULL) { + if (item->type == ITEM_POTION) { + player_use_item(&gs->player, item); + player_remove_inventory_item(&gs->player, gs->inv_selected); + gs->last_message = "Used potion!"; + gs->message_timer = 60; + add_log(gs, "Used potion"); + gs->show_inventory = 0; + return 1; + } + gs->last_message = "Equip weapons/armor with E!"; + gs->message_timer = 60; + } + } + } + + // D to drop selected item + if (IsKeyPressed(KEY_D)) { + if (gs->player.inventory_count > 0) { + Item *item = player_get_inventory_item(&gs->player, gs->inv_selected); + if (item != NULL) { + char drop_msg[64]; + snprintf(drop_msg, sizeof(drop_msg), "Dropped %s", item_get_name(item)); + if (player_drop_item(&gs->player, gs->inv_selected, gs->items, gs->item_count)) { + add_log(gs, drop_msg); + gs->last_message = "Item dropped!"; + gs->message_timer = 60; + if (gs->inv_selected >= gs->player.inventory_count && gs->inv_selected > 0) + gs->inv_selected--; + return 1; + } + gs->last_message = "Cannot drop!"; + gs->message_timer = 60; + } + } + } + + return 0; +} + +static int handle_descend_input(GameState *gs) { + if (IsKeyPressed(KEY_Y)) { + if (gs->player.floor < NUM_FLOORS) { + audio_play_stairs(); + init_floor(gs, gs->player.floor + 1); + gs->last_message = "Descended to next floor!"; + gs->message_timer = 60; + add_log(gs, "Descended stairs"); + } else { + gs->game_won = 1; + gs->game_over = 1; + } + gs->awaiting_descend = 0; + return 1; + } + if (IsKeyPressed(KEY_N)) { + gs->awaiting_descend = 0; + gs->last_message = "Stayed on floor."; + gs->message_timer = 60; + return 1; + } + return 0; +} + +static int handle_movement_input(GameState *gs) { // Check for inventory toggle (I key) - if (IsKeyPressed(KEY_I) && !gs->game_over) { + if (IsKeyPressed(KEY_I)) { gs->show_inventory = 1; gs->inv_selected = 0; - return 0; // don't consume turn + return 0; } // Check for manual item pickup (G key) - if (IsKeyPressed(KEY_G) && !gs->game_over) { + if (IsKeyPressed(KEY_G)) { Item *item = get_item_at_floor(gs->items, gs->item_count, gs->player.x, gs->player.y); if (item != NULL) { if (player_pickup(&gs->player, item)) { @@ -255,92 +382,83 @@ static int handle_input(GameState *gs) { gs->last_message = "Picked up item!"; gs->message_timer = 60; audio_play_item_pickup(); - return 1; } else { gs->last_message = "Inventory full!"; gs->message_timer = 60; - return 1; } + return 1; } } // Check for item usage (U key - use first potion) - if (IsKeyPressed(KEY_U) && !gs->game_over) { - if (gs->player.inventory_count > 0) { - if (player_use_first_item(&gs->player)) { - gs->last_message = "Used potion!"; - gs->message_timer = 60; - audio_play_item_pickup(); - return 1; // consume a turn - } - } - } - - // Movement, use iskeydown for held key repeat, with delay - if (IsKeyDown(KEY_W) || IsKeyDown(KEY_UP)) { - dy = -1; - } else if (IsKeyDown(KEY_S) || IsKeyDown(KEY_DOWN)) { - dy = 1; - } else if (IsKeyDown(KEY_A) || IsKeyDown(KEY_LEFT)) { - dx = -1; - } else if (IsKeyDown(KEY_D) || IsKeyDown(KEY_RIGHT)) { - dx = 1; - } - - if (dx != 0 || dy != 0) { - // Reset combat message - combat_reset_event(); - - // Player action - int action = player_move(&gs->player, dx, dy, &gs->map, gs->enemies, gs->enemy_count); - - if (action) { - // Increment turn counter - gs->turn_count++; - - // Check if stepped on stairs - if (gs->map.tiles[gs->player.y][gs->player.x] == TILE_STAIRS) { - gs->awaiting_descend = 1; - gs->last_message = "Descend to next floor? (Y/N)"; - gs->message_timer = 120; - return 1; - } - - // combat feedback - player attacked enemy - if (combat_get_last_damage() > 0 && !combat_was_player_damage()) { - // find the enemy we attacked - for (int i = 0; i < gs->enemy_count; i++) { - if (!gs->enemies[i].alive && combat_get_last_damage() > 0) { - spawn_floating_text(gs, gs->enemies[i].x * TILE_SIZE + 8, gs->enemies[i].y * TILE_SIZE, - combat_get_last_damage(), combat_was_critical()); - break; - } - } - } - - // Enemy turns - now uses speed/cooldown system (no more % 2 hack) - enemy_update_all(gs->enemies, gs->enemy_count, &gs->player, &gs->map); - - // Check if player took damage - if (combat_was_player_damage() && combat_get_last_damage() > 0) { - audio_play_player_damage(); - gs->screen_shake = 8; - spawn_floating_text(gs, gs->player.x * TILE_SIZE + 8, gs->player.y * TILE_SIZE, combat_get_last_damage(), - combat_was_critical()); - } - - // Set message - gs->last_message = combat_get_last_message(); + if (IsKeyPressed(KEY_U)) { + if (gs->player.inventory_count > 0 && player_use_first_item(&gs->player)) { + gs->last_message = "Used potion!"; gs->message_timer = 60; - - // Check game over - if (gs->player.hp <= 0) { - gs->game_over = 1; - } + audio_play_item_pickup(); + return 1; } } - return 0; + // Movement: use IsKeyDown for held-key repeat + int dx = 0, dy = 0; + if (IsKeyDown(KEY_W) || IsKeyDown(KEY_UP)) + dy = -1; + else if (IsKeyDown(KEY_S) || IsKeyDown(KEY_DOWN)) + dy = 1; + else if (IsKeyDown(KEY_A) || IsKeyDown(KEY_LEFT)) + dx = -1; + else if (IsKeyDown(KEY_D) || IsKeyDown(KEY_RIGHT)) + dx = 1; + + if (dx == 0 && dy == 0) + return 0; + + // Reset combat event before player acts + combat_reset_event(); + + int new_x = gs->player.x + dx; + int new_y = gs->player.y + dy; + int action = 0; + + // Attack enemy at target tile, or move into it + Enemy *target = player_find_enemy_at(gs->enemies, gs->enemy_count, new_x, new_y); + if (target != NULL) { + player_attack(&gs->player, target); + action = 1; + } else { + action = player_move(&gs->player, dx, dy, &gs->map); + } + + if (action) + post_action(gs, target); // target is NULL on move, enemy ptr on attack + + return action; +} + +// Handle player input - returns: 0=continue, 1=acted, -1=quit +static int handle_input(GameState *gs) { + // Check for quit first (always works) + if (IsKeyPressed(KEY_Q)) + return -1; + + // Check for restart (works during game over) + if (IsKeyPressed(KEY_R) && gs->game_over) { + memset(gs, 0, sizeof(GameState)); + init_floor(gs, 1); + return 0; + } + + if (combat_has_effect(gs->player.effects, gs->player.effect_count, EFFECT_STUN)) + return handle_stun_turn(gs); + + if (gs->show_inventory) + return handle_inventory_input(gs); + + if (gs->awaiting_descend) + return handle_descend_input(gs); + + return handle_movement_input(gs); } // Main game loop @@ -358,6 +476,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; @@ -384,15 +504,18 @@ static void game_loop(void) { BeginDrawing(); ClearBackground(BLACK); - // Draw game elements (with screen shake offset) - if (gs.screen_shake > 0) { - // Apply shake offset to drawing - } - + // Draw game world with screen shake applied via camera offset + Camera2D cam = {0}; + cam.zoom = 1.0f; + cam.offset = (Vector2){(float)gs.shake_x, (float)gs.shake_y}; + BeginMode2D(cam); render_map(&gs.map); render_items(gs.items, gs.item_count); render_enemies(gs.enemies, gs.enemy_count); render_player(&gs.player); + EndMode2D(); + + // Floating texts follow world shake render_floating_texts(gs.floating_texts, gs.floating_count, gs.shake_x, gs.shake_y); render_ui(&gs.player); diff --git a/src/player.c b/src/player.c index 218d855..3bb4c4d 100644 --- a/src/player.c +++ b/src/player.c @@ -3,6 +3,7 @@ #include "common.h" #include "items.h" #include "map.h" +#include "settings.h" #include "utils.h" #include #include @@ -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++) { @@ -34,43 +37,39 @@ void player_init(Player *p, int x, int y) { } } -// Check if position has an enemy -static Enemy *get_enemy_at(Enemy *enemies, int count, int x, int y) { +Enemy *player_find_enemy_at(Enemy *enemies, int count, int x, int y) { + if (enemies == NULL || count <= 0) + return NULL; + if (count > MAX_ENEMIES) + count = MAX_ENEMIES; for (int i = 0; i < count; i++) { - if (enemies[i].alive && enemies[i].x == x && enemies[i].y == y) { + if (enemies[i].alive && enemies[i].x == x && enemies[i].y == y) return &enemies[i]; - } } return NULL; } -int player_move(Player *p, int dx, int dy, Map *map, Enemy *enemies, int enemy_count) { +int player_move(Player *p, int dx, int dy, Map *map) { int new_x = p->x + dx; int new_y = p->y + dy; // Check bounds - if (!in_bounds(new_x, new_y, MAP_WIDTH, MAP_HEIGHT)) { + if (!in_bounds(new_x, new_y, MAP_WIDTH, MAP_HEIGHT)) return 0; - } // Check if walkable - if (!is_floor(map, new_x, new_y)) { + if (!is_floor(map, new_x, new_y)) return 0; - } - - // Check for enemy at target position - Enemy *enemy = get_enemy_at(enemies, enemy_count, new_x, new_y); - if (enemy != NULL) { - // Attack the enemy - player_attack(p, enemy); - return 1; - } // Move player 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 +187,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 +196,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; diff --git a/src/player.h b/src/player.h index 7d6c151..ca91abf 100644 --- a/src/player.h +++ b/src/player.h @@ -6,8 +6,11 @@ // Initialize player at position void player_init(Player *p, int x, int y); -// Move player, return 1 if moved/attacked, 0 if blocked -int player_move(Player *p, int dx, int dy, Map *map, Enemy *enemies, int enemy_count); +// Move player to (x+dx, y+dy); returns 1 if moved, 0 if blocked +int player_move(Player *p, int dx, int dy, Map *map); + +// Find a living enemy at tile (x, y); returns NULL if none +Enemy *player_find_enemy_at(Enemy *enemies, int count, int x, int y); // Player attacks enemy (deal damage) void player_attack(Player *p, Enemy *e); diff --git a/src/render.c b/src/render.c index de0c254..1993447 100644 --- a/src/render.c +++ b/src/render.c @@ -61,12 +61,20 @@ void render_enemies(const Enemy *enemies, int count) { DrawRectangleRec(rect, enemy_color); - // Draw hp bar above enemy - int hp_percent = (enemies[i].hp * TILE_SIZE) / enemies[i].max_hp; - if (hp_percent > 0) { - Rectangle hp_bar = {(float)(enemies[i].x * TILE_SIZE), (float)(enemies[i].y * TILE_SIZE - 4), (float)hp_percent, + // Draw hp bar above enemy, color-coded by health remaining + int hp_pixels = (enemies[i].hp * TILE_SIZE) / enemies[i].max_hp; + if (hp_pixels > 0) { + float hp_ratio = (float)enemies[i].hp / (float)enemies[i].max_hp; + Color bar_color; + if (hp_ratio > 0.5f) + bar_color = (Color){60, 180, 60, 255}; // green + else if (hp_ratio > 0.25f) + bar_color = (Color){200, 180, 40, 255}; // yellow + else + bar_color = (Color){200, 60, 60, 255}; // red + Rectangle hp_bar = {(float)(enemies[i].x * TILE_SIZE), (float)(enemies[i].y * TILE_SIZE - 4), (float)hp_pixels, 3}; - DrawRectangleRec(hp_bar, GREEN); + DrawRectangleRec(hp_bar, bar_color); } } } @@ -138,24 +146,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) { @@ -249,6 +295,35 @@ void render_inventory_overlay(const Player *p, int selected) { (Color){65, 65, 65, 255}); } +static Color label_color(FloatingText *ft, int alpha) { + if (ft->label[0] == '\0') + return (Color){255, 100, 100, alpha}; // numeric damage default + if (strcmp(ft->label, "DODGE") == 0) + return (Color){160, 160, 160, alpha}; + if (strcmp(ft->label, "BLOCK") == 0) + return (Color){80, 130, 220, alpha}; + if (strcmp(ft->label, "CRIT!") == 0) + return (Color){255, 200, 50, alpha}; + if (strcmp(ft->label, "SLAIN") == 0) + return (Color){220, 50, 50, alpha}; + + // Proc label, color driven by effect_type stored in the struct + switch (ft->effect_type) { + case EFFECT_POISON: + return (Color){50, 200, 50, alpha}; + case EFFECT_BLEED: + return (Color){200, 50, 50, alpha}; + case EFFECT_BURN: + return (Color){230, 130, 30, alpha}; + case EFFECT_STUN: + return (Color){200, 200, 50, alpha}; + case EFFECT_WEAKEN: + return (Color){120, 120, 120, alpha}; + default: + return (Color){200, 200, 200, alpha}; + } +} + void render_floating_texts(FloatingText *texts, int count, int shake_x, int shake_y) { for (int i = 0; i < count; i++) { if (texts[i].lifetime <= 0) @@ -256,15 +331,23 @@ void render_floating_texts(FloatingText *texts, int count, int shake_x, int shak int x = texts[i].x + shake_x; int y = texts[i].y + shake_y - (60 - texts[i].lifetime); // rise over time - float alpha = (float)texts[i].lifetime / 60.0f; - Color color = - texts[i].is_critical ? (Color){255, 200, 50, (int)(255 * alpha)} : (Color){255, 100, 100, (int)(255 * alpha)}; + int a = (int)(255 * alpha); - char text[16]; - snprintf(text, sizeof(text), "%d", texts[i].value); - int text_w = MeasureText(text, 18); - DrawText(text, x - text_w / 2, y, 18, color); + if (texts[i].label[0] != '\0') { + // Label text (DODGE, BLOCK, CRIT!, proc name, SLAIN) + int font_size = (texts[i].label[0] == 'C') ? 16 : 14; // CRIT! slightly larger + Color color = label_color(&texts[i], a); + int text_w = MeasureText(texts[i].label, font_size); + DrawText(texts[i].label, x - text_w / 2, y, font_size, color); + } else { + // Numeric damage + Color color = texts[i].is_critical ? (Color){255, 200, 50, a} : (Color){255, 100, 100, a}; + char text[16]; + snprintf(text, sizeof(text), "%d", texts[i].value); + int text_w = MeasureText(text, 18); + DrawText(text, x - text_w / 2, y, 18, color); + } } } @@ -289,8 +372,8 @@ void render_message(const char *message) { int msg_len = strlen(message); float msg_ratio = 13.5; - // Draw message box + // Draw message box // TODO: Separate out the calculation of the x/y and width/height so that if a message takes up more than, say, // 75% of the screen width, we add a line break and increase the height. That would then require calculating the // width based on the longest line. diff --git a/src/settings.h b/src/settings.h index 47f3c12..e87c082 100644 --- a/src/settings.h +++ b/src/settings.h @@ -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 3 +#define BLEED_STACK_DAMAGE 3 +#define BURN_BASE_DAMAGE 7 +#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