Compare commits

..

No commits in common. "main" and "editconfig" have entirely different histories.

40 changed files with 457 additions and 1512 deletions

View file

@ -5,8 +5,6 @@ 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

11
.gitignore vendored
View file

@ -1,12 +1,5 @@
# Nix
/.direnv/
result*
# Build artifacts
# ignore build artifacts
result
build
obj
roguelike
# Zig
.zig-cache
.zig-out

View file

@ -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

45
Makefile Normal file
View file

@ -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; }

122
README.md
View file

@ -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 :/

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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);
}

6
flake.lock generated
View file

@ -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": {

View file

@ -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";
}
}

View file

@ -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;

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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 = .{};
}

View file

@ -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)"];
})

View file

@ -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
];
}

View file

@ -72,31 +72,12 @@ void audio_play_move(void) {
void audio_play_attack(void) {
// Mid-range hit sound
// play_tone(400.0f, 0.1f, 0.5f);
int choice = GetRandomValue(1, 3);
Sound attack;
switch (choice) {
case 1:
attack = LoadSound("./assets/sounds/sword1.wav");
break;
case 2:
attack = LoadSound("./assets/sounds/sword2.wav");
break;
case 3:
attack = LoadSound("./assets/sounds/sword3.wav");
break;
default:
attack = LoadSound("./assets/sounds/sword1.wav");
break;
}
PlaySound(attack);
play_tone(400.0f, 0.1f, 0.5f);
}
void audio_play_item_pickup(void) {
// High-pitched pickup sound
play_tone(800.0f, 0.15f, 0.4f);
Sound pickup = LoadSound("./assets/sounds/itempickup.wav");
PlaySound(pickup);
}
void audio_play_enemy_death(void) {
@ -113,46 +94,7 @@ void audio_play_player_damage(void) {
void audio_play_stairs(void) {
// Ascending stairs sound
Sound staircase = LoadSound("./assets/sounds/levelcomplete.wav");
PlaySound(staircase);
}
void audio_play_dodge(void) {
// High-pitched whoosh
// play_tone(900.0f, 0.08f, 0.3f);
int choice = GetRandomValue(1, 3);
Sound dodge;
switch (choice) {
case 1:
dodge = LoadSound("./assets/sounds/dodge1.wav");
break;
case 2:
dodge = LoadSound("./assets/sounds/dodge2.wav");
break;
case 3:
dodge = LoadSound("./assets/sounds/dodge3.wav");
break;
default:
dodge = LoadSound("./assets/sounds/dodge1.wav");
break;
}
PlaySound(dodge);
}
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);
play_tone(400.0f, 0.1f, 0.3f);
play_tone(600.0f, 0.1f, 0.3f);
play_tone(800.0f, 0.15f, 0.3f);
}

View file

@ -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

123
src/combat.c Normal file
View file

@ -0,0 +1,123 @@
#include "combat.h"
#include "common.h"
#include "rng.h"
#include <stddef.h>
// Track combat events for feedback
typedef struct {
const char *message;
int damage;
int is_player_damage;
int is_critical;
} CombatEvent;
static CombatEvent last_event = {NULL, 0, 0, 0};
const char *combat_get_last_message(void) {
return last_event.message;
}
int combat_get_last_damage(void) {
return last_event.damage;
}
int combat_was_player_damage(void) {
return last_event.is_player_damage;
}
int combat_was_critical(void) {
return last_event.is_critical;
}
void combat_player_attack(Player *p, Enemy *e) {
if (e == NULL || !e->alive)
return;
last_event.is_critical = 0;
// 90% hit chance
if (rng_int(0, 99) < 90) {
// calculate damage with variance from player stats
int base_damage = p->attack;
int variance = rng_int(p->dmg_variance_min, p->dmg_variance_max);
int damage = (base_damage * variance) / 100;
if (damage < 1)
damage = 1;
// 10% critical hit chance for 1.5x
if (rng_int(0, 9) == 0) {
damage = (damage * 3) / 2;
last_event.is_critical = 1;
}
e->hp -= damage;
last_event.damage = damage;
last_event.is_player_damage = 0;
if (e->hp <= 0) {
e->hp = 0;
e->alive = 0;
last_event.message = "Enemy killed!";
} else if (last_event.is_critical) {
last_event.message = "Critical hit!";
} else {
last_event.message = "You hit";
}
} else {
last_event.damage = 0;
last_event.is_player_damage = 0;
last_event.message = "You missed";
}
}
void combat_enemy_attack(Enemy *e, Player *p) {
if (e == NULL || !e->alive)
return;
if (p == NULL)
return;
last_event.is_critical = 0;
// 85% hit chance for enemies
if (rng_int(0, 99) < 85) {
// calculate damage with variance
int base_damage = e->attack - p->defense;
if (base_damage < 1)
base_damage = 1;
int variance = rng_int(80, 120);
int damage = (base_damage * variance) / 100;
if (damage < 1)
damage = 1;
// 5% critical hit chance for enemies
if (rng_int(0, 19) == 0) {
damage = (damage * 3) / 2;
last_event.is_critical = 1;
}
p->hp -= damage;
last_event.damage = damage;
last_event.is_player_damage = 1;
if (p->hp <= 0) {
p->hp = 0;
last_event.message = "You died!";
} else if (last_event.is_critical) {
last_event.message = "Critical!";
} else {
last_event.message = "Hit";
}
} else {
last_event.damage = 0;
last_event.is_player_damage = 1;
last_event.message = "Missed";
}
}
void combat_reset_event(void) {
last_event.message = NULL;
last_event.damage = 0;
last_event.is_player_damage = 0;
last_event.is_critical = 0;
}

View file

@ -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

View file

@ -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

View file

@ -3,7 +3,6 @@
#include "common.h"
#include "map.h"
#include "rng.h"
#include <string.h>
// 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);

View file

@ -1,24 +1,8 @@
#include "common.h"
#include "map.h"
#include "rng.h"
#include "settings.h"
#include <stddef.h>
typedef struct {
const char *name;
DamageClass dmg_class;
int base_power;
int crit_chance;
int crit_multiplier;
int status_chance;
} WeaponTemplate;
static const WeaponTemplate weapon_templates[NUM_WEAPON_TEMPLATES] = {
{"Dagger", DMG_SLASH, 1, 25, 200, 20}, {"Mace", DMG_IMPACT, 2, 10, 150, 30},
{"Spear", DMG_PIERCE, 2, 15, 175, 25}, {"Torch", DMG_FIRE, 1, 5, 150, 40},
{"Venom Blade", DMG_POISON, 1, 15, 175, 35},
};
void item_spawn(Item items[], int *count, Map *map, int floor) {
*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 "???";
}
}

View file

@ -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

View file

@ -23,60 +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++;
}
}
@ -132,254 +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());
audio_play_attack();
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)) {
@ -389,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
@ -483,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;
@ -511,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);
@ -560,8 +430,7 @@ static void game_loop(void) {
int main(void) {
// Initialize audio
audio_init();
// Initialize random number generator
SetRandomSeed(88435);
// Initialize window
InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT + 60, "Roguelike");
SetTargetFPS(60);

View file

@ -3,7 +3,6 @@
#include "common.h"
#include "items.h"
#include "map.h"
#include "settings.h"
#include "utils.h"
#include <stdlib.h>
#include <string.h>
@ -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;

View file

@ -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);

View file

@ -5,7 +5,6 @@
#include "settings.h"
#include <stddef.h>
#include <stdio.h>
#include <string.h>
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});

View file

@ -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