Compare commits
No commits in common. "main" and "msgfix" have entirely different histories.
40 changed files with 458 additions and 1505 deletions
|
|
@ -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
|
||||
|
|
|
|||
11
.gitignore
vendored
11
.gitignore
vendored
|
|
@ -1,12 +1,5 @@
|
|||
# Nix
|
||||
/.direnv/
|
||||
result*
|
||||
|
||||
# Build artifacts
|
||||
# ignore build artifacts
|
||||
result
|
||||
build
|
||||
obj
|
||||
roguelike
|
||||
|
||||
# Zig
|
||||
.zig-cache
|
||||
.zig-out
|
||||
|
|
|
|||
20
Justfile
20
Justfile
|
|
@ -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
45
Makefile
Normal 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
122
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 :/
|
||||
|
|
|
|||
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.
73
build.zig
73
build.zig
|
|
@ -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
6
flake.lock
generated
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 = .{};
|
||||
}
|
||||
|
|
@ -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)"];
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
];
|
||||
}
|
||||
|
|
|
|||
66
src/audio.c
66
src/audio.c
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
12
src/audio.h
12
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
|
||||
|
|
|
|||
123
src/combat.c
Normal file
123
src/combat.c
Normal 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;
|
||||
}
|
||||
26
src/combat.h
26
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
|
||||
|
|
|
|||
37
src/common.h
37
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
|
||||
|
|
|
|||
56
src/enemy.c
56
src/enemy.c
|
|
@ -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);
|
||||
|
|
|
|||
68
src/items.c
68
src/items.c
|
|
@ -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 "???";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
569
src/main.c
569
src/main.c
|
|
@ -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);
|
||||
|
|
|
|||
53
src/player.c
53
src/player.c
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
127
src/render.c
127
src/render.c
|
|
@ -61,20 +61,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 +138,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 +249,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 +256,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -372,8 +289,8 @@ void render_message(const char *message) {
|
|||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue