diff --git a/.editorconfig b/.editorconfig index 368d066..f267605 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,12 +5,10 @@ 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 -ident_size = 2 +ident_size = 4 [Makefile*] ident_style = tab diff --git a/.gitignore b/.gitignore index a53104b..6746cd7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,5 @@ -# Nix -/.direnv/ -result* - -# Build artifacts +# ignore build artifacts +result build obj roguelike - -# Zig -.zig-cache -.zig-out diff --git a/Justfile b/Justfile deleted file mode 100644 index a425cb3..0000000 --- a/Justfile +++ /dev/null @@ -1,20 +0,0 @@ -# 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 new file mode 100644 index 0000000..607167e --- /dev/null +++ b/Makefile @@ -0,0 +1,45 @@ +# 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/README.md b/README.md index 9d63b10..843330f 100644 --- a/README.md +++ b/README.md @@ -1,121 +1,3 @@ -# Rogged +# rogged -Turn-based roguelike dungeon crawler, built in C99 and with a dash of Zig to -serve as a learning opportunity. Rogged is basically a classic roguelike where -you descend through floors of a procedurally generated dungeon, fighting -enemies, managing inventory, and trying to reach the bottom alive. - -A non-exhaustive list of its (current) features: - -- Turn-based combat with damage variance, critical hits, dodge, and block - mechanics -- Damage classes (Slash, Impact, Pierce, Fire, and Poison) -- Status effects (Poison, Bleed, Stun, Weaken, and Burn) -- Various enemy classes (Goblin, Skeleton, Orc) with distinct resistance - profiles -- Procedural dungeon generation with rooms and corridors, seeded per floor -- Inventory and equipment system (weapons, armor, potions) -- Procedural audio via raylib -- ASCII-inspired tile rendering, with HP bars and floating damage text - -**Controls:** - -| Key | Action | -| ------------- | ----------------------------------- | -| WASD / Arrows | Move or attack | -| G | Pick up item | -| I | Open inventory | -| U | Use a potion | -| E | Equip item from inventory | -| D | Drop item | -| Y / N | Confirm / decline descending stairs | -| R | Restart (on game over) | -| Q | Quit | - -## Build Instructions - -Rogged is built with C99 and Zig. Besides `raylib` and `pkg-config` you will -need a C compiler and basic Zig tooling. For now, we use C99 and Zig 0.15.2. -Those might change in the future. - -Additionally you will need `clang-format` and `just` for the developer workflow -if you plan to contribute. - -### Using Nix (Recommended) - -The recommended developer tooling is [Nix](https://nixos.org). This provides a -pure, reproducible devshell across all machines. - -```sh -# Enter the development shell -$ nix develop - -# Build and run -$ just dev -``` - -### Manual Build - -```sh -# Full build -$ zig build - -# Build and run -$ zig build run - -# or -$ just dev -``` - -### Task Runner Commands - -There's a `Justfile` designed to make common tasks somewhat easier. For now, -they are as follows: - -```sh -just build # zig build -just dev # zig build run -just clean # remove zig-out/ and .zig-cache/ -just fmt # format all C and Zig source files -just fmt-check # check formatting -``` - -If the project gets more complicated, new tasks might be added. - -## Future Plans - -The game is currently **playable end-to-end** but it lacks _serious_ polish to -claim its place as a fun roguelike. Some of the features I'd like to introduce, -in no particular order, are as follows: - -- [ ] **Save / Load system** - Persist and restore game state between sessions -- [ ] **More enemy variety** - Additional enemy types with unique abilities -- [ ] **More item variety** - Rings, wands, scrolls, and cursed items -- [ ] **Multiple floors beyond 5** - Endless or configurable depth -- [ ] **Test suite** - Unit tests for combat math, dungeon generation, and RNG -- [ ] **Field of view** - Fog of war and line-of-sight mechanics -- [ ] **Level transitions** - Upward stairs and floor teleportation -- [ ] **Achievements / death log** - Track runs and causes of death with a - leaderboard -- [ ] **UI polish** - Better message log history, item descriptions, death - screen - -In addition, it might be interesting to allow customizing the "world state" by -as scripting API. Though, that is for much later. - -## Attributions - -[Shattered Pixel Dungeon]: https://github.com/00-Evan/shattered-pixel-dungeon -[Raylib]: https://www.raylib.com - -This project draws a fair bit of inspiration from [Shattered Pixel Dungeon]. -While the mechanics are generally different, and commit to remain as such, I -cannot deny the amount of inspiration & ideas Shattered Pixel Dungeon has given -me. It's a GPL licensed project, and no code was borrowed. Still, some -resemblance may occur. - -Additionally, _huge_ thanks to [Raylib] for how easy it made graphics and audio. -This was perhaps my best experience in developing a graphical application, and -CERTAINLY the most ergonomic when it comes to writing a game. - -_I got rogged :/_ +I got rogged :/ diff --git a/build.zig b/build.zig deleted file mode 100644 index eb3ff5d..0000000 --- a/build.zig +++ /dev/null @@ -1,73 +0,0 @@ -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/flake.lock b/flake.lock index 4b75299..9bb7806 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1775036866, - "narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=", + "lastModified": 1773646010, + "narHash": "sha256-iYrs97hS7p5u4lQzuNWzuALGIOdkPXvjz7bviiBjUu8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "6201e203d09599479a3b3450ed24fa81537ebc4e", + "rev": "5b2c2d84341b2afb5647081c1386a80d7a8d8605", "type": "github" }, "original": { diff --git a/libs/combat/attack.zig b/libs/combat/attack.zig deleted file mode 100644 index 38109c2..0000000 --- a/libs/combat/attack.zig +++ /dev/null @@ -1,192 +0,0 @@ -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 deleted file mode 100644 index 2890d81..0000000 --- a/libs/combat/c.zig +++ /dev/null @@ -1,43 +0,0 @@ -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 deleted file mode 100644 index 870a277..0000000 --- a/libs/combat/combat.zig +++ /dev/null @@ -1,84 +0,0 @@ -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 deleted file mode 100644 index a665422..0000000 --- a/libs/combat/effects.zig +++ /dev/null @@ -1,151 +0,0 @@ -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 deleted file mode 100644 index 1084e25..0000000 --- a/libs/combat/event.zig +++ /dev/null @@ -1,18 +0,0 @@ -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 8fb5c18..a097561 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -1,12 +1,6 @@ -{ - lib, - stdenv, - zig, - raylib, - pkg-config, -}: +{stdenv}: stdenv.mkDerivation (finalAttrs: { - pname = "rogged"; + pname = "sample-c-cpp"; version = "0.0.1"; src = builtins.path { @@ -14,32 +8,5 @@ stdenv.mkDerivation (finalAttrs: { name = finalAttrs.pname; }; - 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"; - }; + makeFlags = ["PREFIX=$(out)"]; }) diff --git a/nix/shell.nix b/nix/shell.nix index 663f55b..e51be03 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -2,22 +2,21 @@ mkShell, clang-tools, raylib, + gnumake, pkg-config, - just, - zig, - zig-zlint, }: mkShell { strictDeps = true; + packages = [ + clang-tools + gnumake + ]; - buildInputs = [raylib]; + buildInputs = [ + raylib + ]; nativeBuildInputs = [ pkg-config - clang-tools - just - - zig - zig-zlint ]; } diff --git a/src/audio.c b/src/audio.c index b273369..c6bcefd 100644 --- a/src/audio.c +++ b/src/audio.c @@ -98,26 +98,3 @@ 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 3c2cb36..a574a9a 100644 --- a/src/audio.h +++ b/src/audio.h @@ -25,16 +25,4 @@ 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 new file mode 100644 index 0000000..fbb66cc --- /dev/null +++ b/src/combat.c @@ -0,0 +1,123 @@ +#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 62295d3..c53f6a7 100644 --- a/src/combat.h +++ b/src/combat.h @@ -15,18 +15,6 @@ 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); @@ -36,18 +24,4 @@ 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 9af8771..b963c3b 100644 --- a/src/common.h +++ b/src/common.h @@ -6,19 +6,6 @@ // 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; @@ -48,10 +35,6 @@ typedef struct { int power; int floor; int picked_up; - DamageClass dmg_class; - int crit_chance; - int crit_multiplier; - int status_chance; } Item; // Player @@ -64,17 +47,15 @@ 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; - // status effects - StatusEffect effects[MAX_EFFECTS]; - int effect_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) } Player; // Enemy types @@ -90,16 +71,6 @@ 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 @@ -108,8 +79,6 @@ 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 9f72670..f6ca10f 100644 --- a/src/enemy.c +++ b/src/enemy.c @@ -3,7 +3,6 @@ #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); @@ -40,81 +39,36 @@ 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 + floor_atk; + e.attack = ENEMY_BASE_ATTACK; e.speed = 55 + rng_int(0, 10); - e.dodge = 15; - e.block = 0; - e.dmg_class = DMG_POISON; - e.status_chance = 15; - e.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 + floor_atk; + e.attack = ENEMY_BASE_ATTACK + 1; e.speed = 70 + rng_int(0, 10); - e.dodge = 5; - e.block = 0; - e.dmg_class = DMG_SLASH; - e.status_chance = 10; - e.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 + floor_atk; + e.attack = ENEMY_BASE_ATTACK + 2; e.speed = 85 + rng_int(0, 10); - e.dodge = 0; - e.block = 3; - e.dmg_class = DMG_IMPACT; - e.status_chance = 20; - e.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; @@ -183,10 +137,6 @@ 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 e5bf212..c758955 100644 --- a/src/items.c +++ b/src/items.c @@ -1,24 +1,8 @@ #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; @@ -49,10 +33,6 @@ 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); @@ -60,24 +40,18 @@ 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); + item.power = 5 + rng_int(0, floor * 2); // healing: 5 + 0-2*floor } else if (type_roll < 80) { - // 30% chance for weapon, pick a random template + // 30% chance for weapon item.type = ITEM_WEAPON; - 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; + item.power = 1 + rng_int(0, floor); // attack bonus: 1 + 0-floor } else { // 20% chance for armor item.type = ITEM_ARMOR; - item.power = 1 + rng_int(0, floor / 2); + item.power = 1 + rng_int(0, floor / 2); // defense bonus } - items[*count] = item; + items[i] = item; (*count)++; } } @@ -91,20 +65,7 @@ const char *item_get_name(const Item *i) { case ITEM_POTION: return "Potion"; case ITEM_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"; - } + return "Weapon"; case ITEM_ARMOR: return "Armor"; default: @@ -163,20 +124,3 @@ 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 86e40e2..fa913f2 100644 --- a/src/items.h +++ b/src/items.h @@ -20,7 +20,4 @@ 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 8313f81..cf0e9d1 100644 --- a/src/main.c +++ b/src/main.c @@ -23,54 +23,15 @@ 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) { - 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 ""; + 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++; } } @@ -126,253 +87,165 @@ static void init_floor(GameState *gs, int floor_num) { gs->turn_count = 0; } -// Tick all status effects at the start of a turn -static void tick_all_effects(GameState *gs) { - // Player effects - int player_effect_dmg = combat_tick_effects(&gs->player); - if (player_effect_dmg > 0) { - spawn_floating_text(gs, gs->player.x * TILE_SIZE + 8, gs->player.y * TILE_SIZE, player_effect_dmg, 0); - gs->screen_shake = 4; +// 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; } - // Check if player died from effects - if (gs->player.hp <= 0) { - gs->player.hp = 0; - gs->game_over = 1; - return; + // 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; } - // 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 (gs->show_inventory) { + if (IsKeyPressed(KEY_I) || IsKeyPressed(KEY_ESCAPE)) { + gs->show_inventory = 0; + return 0; } - if (!e->alive) { - add_log(gs, "Enemy died from effects!"); + + if (IsKeyPressed(KEY_DOWN) || IsKeyPressed(KEY_S)) { + gs->inv_selected++; + if (gs->inv_selected >= gs->player.inventory_count) { + gs->inv_selected = 0; + } + return 0; } - } -} - -// 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 (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)) { - gs->inv_selected = (gs->inv_selected == 0) ? (gs->player.inventory_count > 0 ? gs->player.inventory_count - 1 : 0) - : gs->inv_selected - 1; - return 0; - } - - 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) + 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)) + 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; + } + + // 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."; + gs->message_timer = 60; return 1; } - gs->last_message = "Cannot equip that!"; - gs->message_timer = 60; + return 0; } - // 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)) { + if (IsKeyPressed(KEY_I) && !gs->game_over) { gs->show_inventory = 1; gs->inv_selected = 0; - return 0; + return 0; // don't consume turn } // Check for manual item pickup (G key) - if (IsKeyPressed(KEY_G)) { + if (IsKeyPressed(KEY_G) && !gs->game_over) { 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)) { @@ -382,83 +255,92 @@ static int handle_movement_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)) { - if (gs->player.inventory_count > 0 && player_use_first_item(&gs->player)) { - gs->last_message = "Used potion!"; - gs->message_timer = 60; - audio_play_item_pickup(); - return 1; + 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 - int dx = 0, dy = 0; - if (IsKeyDown(KEY_W) || IsKeyDown(KEY_UP)) + // 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)) + } else if (IsKeyDown(KEY_S) || IsKeyDown(KEY_DOWN)) { dy = 1; - else if (IsKeyDown(KEY_A) || IsKeyDown(KEY_LEFT)) + } else if (IsKeyDown(KEY_A) || IsKeyDown(KEY_LEFT)) { dx = -1; - else if (IsKeyDown(KEY_D) || IsKeyDown(KEY_RIGHT)) + } 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 + if (dx != 0 || dy != 0) { + // Reset combat message + combat_reset_event(); - return action; -} + // Player action + int action = player_move(&gs->player, dx, dy, &gs->map, gs->enemies, gs->enemy_count); -// 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; + if (action) { + // Increment turn counter + gs->turn_count++; - // 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; + // 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(); + gs->message_timer = 60; + + // Check game over + if (gs->player.hp <= 0) { + gs->game_over = 1; + } + } } - 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); + return 0; } // Main game loop @@ -476,8 +358,6 @@ 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; @@ -504,18 +384,15 @@ static void game_loop(void) { BeginDrawing(); ClearBackground(BLACK); - // 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); + // Draw game elements (with screen shake offset) + if (gs.screen_shake > 0) { + // Apply shake offset to drawing + } + 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 3bb4c4d..218d855 100644 --- a/src/player.c +++ b/src/player.c @@ -3,7 +3,6 @@ #include "common.h" #include "items.h" #include "map.h" -#include "settings.h" #include "utils.h" #include #include @@ -19,8 +18,6 @@ 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)); @@ -28,8 +25,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->effect_count = 0; - memset(p->effects, 0, sizeof(p->effects)); + p->dmg_variance_min = 80; + p->dmg_variance_max = 120; // Initialize inventory to empty for (int i = 0; i < MAX_INVENTORY; i++) { @@ -37,39 +34,43 @@ void player_init(Player *p, 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; +// Check if position has an enemy +static Enemy *get_enemy_at(Enemy *enemies, int count, int x, int y) { 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) { +int player_move(Player *p, int dx, int dy, Map *map, Enemy *enemies, int enemy_count) { 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; - // 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)) { + if (p->step_count % 15 == 0 && p->hp < p->max_hp) { p->hp += 1; } return 1; @@ -187,6 +188,16 @@ 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; @@ -196,15 +207,11 @@ 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 ca91abf..7d6c151 100644 --- a/src/player.h +++ b/src/player.h @@ -6,11 +6,8 @@ // Initialize player at position void player_init(Player *p, int x, int y); -// 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); +// 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); // Player attacks enemy (deal damage) void player_attack(Player *p, Enemy *e); diff --git a/src/render.c b/src/render.c index 1993447..4ecacdf 100644 --- a/src/render.c +++ b/src/render.c @@ -5,7 +5,6 @@ #include "settings.h" #include #include -#include void render_map(const Map *map) { for (int y = 0; y < MAP_HEIGHT; y++) { @@ -61,20 +60,12 @@ void render_enemies(const Enemy *enemies, int count) { DrawRectangleRec(rect, enemy_color); - // 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, + // 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, 3}; - DrawRectangleRec(hp_bar, bar_color); + DrawRectangleRec(hp_bar, GREEN); } } } @@ -146,62 +137,24 @@ 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", 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); + 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); // 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[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)); + char weapon_text[48]; + snprintf(weapon_text, sizeof(weapon_text), "Wpn:%s +%d", item_get_name(&p->equipped_weapon), + p->equipped_weapon.power); DrawText(weapon_text, 10, row2_y, 12, YELLOW); } else { - DrawText("Wpn:--- [IMP]", 10, row2_y, 12, (Color){60, 60, 60, 255}); + DrawText("Wpn:---", 10, row2_y, 12, (Color){60, 60, 60, 255}); } if (p->has_armor) { @@ -295,35 +248,6 @@ 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) @@ -331,23 +255,15 @@ 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; - int a = (int)(255 * alpha); - 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); - } + 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)}; + + 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); } } @@ -370,15 +286,8 @@ void render_message(const char *message) { if (message == NULL) return; - int msg_len = strlen(message); - float msg_ratio = 13.5; - // 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. - Rectangle msg_bg = {(float)(SCREEN_WIDTH / 2.0f - ((msg_ratio / 2.03f) * msg_len)), - (float)(SCREEN_HEIGHT / 2.0f - 30.0f), msg_ratio * msg_len, 60}; + Rectangle msg_bg = {(float)(SCREEN_WIDTH / 2 - 150), (float)(SCREEN_HEIGHT / 2 - 30), 300, 60}; DrawRectangleRec(msg_bg, (Color){45, 45, 45, 235}); DrawRectangleLines((int)msg_bg.x, (int)msg_bg.y, (int)msg_bg.width, (int)msg_bg.height, (Color){180, 180, 180, 255}); diff --git a/src/settings.h b/src/settings.h index e87c082..47f3c12 100644 --- a/src/settings.h +++ b/src/settings.h @@ -25,31 +25,4 @@ #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