1
0
Fork 0
forked from NotAShelf/rogged

Compare commits

...

3 commits

Author SHA1 Message Date
0b3608b18c
render: add player sprite. welcome to dude man 2026-04-09 17:03:59 -04:00
4dfe52ae72 movement: generalize; use vectors (#16)
Generalized movement, so that all entities move the same.

Reviewed-on: NotAShelf/rogged#16
Reviewed-by: raf <raf@notashelf.dev>
Co-authored-by: Squirrel Modeller <squirrelmodeller@protonmail.com>
Co-committed-by: Squirrel Modeller <squirrelmodeller@protonmail.com>
2026-04-09 14:11:46 +00:00
1d738c35d4
docs: move README to docs/; reword most sections
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ida6ac152f3fa71ceb1d980afba9a82fc6a6a6964
2026-04-09 09:36:18 +03:00
14 changed files with 171 additions and 124 deletions

BIN
assets/entities/player.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

View file

@ -25,6 +25,7 @@ pub fn build(b: *std.Build) void {
"src/main.c",
"src/map.c",
"src/player.c",
"src/movement.c",
"src/render.c",
"src/rng.c",
"src/settings.c",

View file

@ -1,11 +1,13 @@
# 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.
Turn-based, infinite roguelike dungeon crawler built with C99 and with a dash of
Zig. Meant to serve primarily, but not exclusively, 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 or die chasing a highscore!
A non-exhaustive list of its (current) features:
The game itself, be it the code or mechanics, is _heavily_ in development. For
now, a non-exhaustive list of its (current) features are as follows:
- Turn-based combat with damage variance, critical hits, dodge, and block
mechanics
@ -18,33 +20,27 @@ A non-exhaustive list of its (current) features:
- 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 |
There are still some features lacking polish, or lacking _any_ kind of attention
to be viable. For a semi-complete list of things that need to be done, see the
[future plans section](#future-plans).
## 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.
Rogged is built on a relatively simple stack. It uses C99 for the main game
logic, and Zig for the combat library. Besides `raylib` and `pkg-config`, you
only need the core Zig tooling. For now the required Zig version is 0.15.2, but
this might change in the future. Additionally, you will need `clang-format` and
`just` for common development tasks in the case you plan to contribute. For
building, Zig is enough.
### Using Nix (Recommended)
The recommended developer tooling is [Nix](https://nixos.org). This provides a
pure, reproducible devshell across all machines.
The _recommended_ way of developing this project is using
[Nix](https://nixos.org) and relying on devshells for pure, reproducible
developer environment across all machines.
If you are a [Direnv](https://direnv.net) user, you may simply run
`direnv allow` or you may use `nix develop` to enter the default shell.
```sh
# Enter the development shell
@ -56,6 +52,9 @@ $ just dev
### Manual Build
If you are allergic to good tooling and would rather use your system Zig, you
may simply invoke `zig build` after acquiring Zig 0.15.2.
```sh
# Full build
$ zig build
@ -67,6 +66,8 @@ $ zig build run
$ just dev
```
The Justfile provides commands that work across both methods.
### Task Runner Commands
There's a `Justfile` designed to make common tasks somewhat easier. For now,
@ -84,10 +85,14 @@ 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:
The game is currently **playable end-to-end**, but it lacks a fair bit of polish
to claim its place as a fun, engaging roguelike you can just boot up and play.
Some of the features that are planned for the future, in no particular order,
are as follows:
- [ ] **Better enemy AI** - The current AI is very simple.
- [ ] **Fog of War** - Instead of loading the entire map, let the player
discover the rooms
- [ ] **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
@ -100,8 +105,9 @@ in no particular order, are as follows:
- [ ] **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.
Later down the line it might be an interesting choice to provide a scripting
API, likely with Lua, to allow customizing the game state and events. Though,
that is for much later.
## Attributions
@ -118,4 +124,6 @@ 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 :/_

View file

@ -4,6 +4,10 @@
#include "settings.h"
#include <raylib.h>
typedef struct {
int x, y;
} Vec2;
// Tile types
typedef enum { TILE_WALL, TILE_FLOOR, TILE_STAIRS } TileType;
@ -57,7 +61,7 @@ typedef struct {
// Player
typedef struct {
int x, y;
Vec2 position;
int hp, max_hp;
int attack;
int defense;
@ -83,7 +87,7 @@ typedef enum { ENEMY_GOBLIN, ENEMY_SKELETON, ENEMY_ORC } EnemyType;
// Enemy
typedef struct {
int x, y;
Vec2 position;
int hp;
int max_hp;
int attack;

View file

@ -2,6 +2,7 @@
#include "combat.h"
#include "common.h"
#include "map.h"
#include "movement.h"
#include "rng.h"
#include <string.h>
@ -29,7 +30,7 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) {
get_random_floor_tile(map, &ex, &ey, 50);
// Don't spawn on player position
if (ex == p->x && ey == p->y) {
if (ex == p->position.x && ey == p->position.y) {
continue;
}
@ -41,8 +42,8 @@ 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.position.x = ex;
e.position.y = ey;
e.alive = 1;
e.type = rng_int(ENEMY_GOBLIN, max_type);
e.effect_count = 0;
@ -127,7 +128,7 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) {
// Check if position has an enemy
int is_enemy_at(const 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].position.x == x && enemies[i].position.y == y) {
return 1;
}
}
@ -136,45 +137,37 @@ int is_enemy_at(const Enemy *enemies, int count, int x, int y) {
// Check if enemy can see player (adjacent)
static int can_see_player(Enemy *e, Player *p) {
int dx = p->x - e->x;
int dy = p->y - e->y;
int dx = p->position.x - e->position.x;
int dy = p->position.y - e->position.y;
return (dx >= -1 && dx <= 1 && dy >= -1 && dy <= 1);
}
// Check if position is occupied by player
static int is_player_at(Player *p, int x, int y) {
return (p->x == x && p->y == y);
}
// Move enemy toward player
static void enemy_move_toward_player(Enemy *e, Player *p, Map *map, Enemy *all_enemies, int enemy_count) {
int dx = 0, dy = 0;
if (p->x > e->x)
if (p->position.x > e->position.x)
dx = 1;
else if (p->x < e->x)
else if (p->position.x < e->position.x)
dx = -1;
if (p->y > e->y)
if (p->position.y > e->position.y)
dy = 1;
else if (p->y < e->y)
else if (p->position.y < e->position.y)
dy = -1;
// Try horizontal first, then vertical
int new_x = e->x + dx;
int new_y = e->y;
Vec2 dir = {dx, 0};
if (dx != 0) {
MoveResult r = try_move_entity(&e->position, dir, map, p, all_enemies, enemy_count, false);
if (r == MOVE_RESULT_MOVED)
return;
}
if (dx != 0 && is_floor(map, new_x, new_y) && !is_enemy_at(all_enemies, enemy_count, new_x, new_y) &&
!is_player_at(p, new_x, new_y)) {
e->x = new_x;
} else if (dy != 0) {
new_x = e->x;
new_y = e->y + dy;
if (is_floor(map, new_x, new_y) && !is_enemy_at(all_enemies, enemy_count, new_x, new_y) &&
!is_player_at(p, new_x, new_y)) {
e->x = new_x;
e->y = new_y;
}
dir.x = 0;
dir.y = dy;
if (dy != 0) {
try_move_entity(&e->position, dir, map, p, all_enemies, enemy_count, false);
}
}

View file

@ -4,6 +4,7 @@
#include "enemy.h"
#include "items.h"
#include "map.h"
#include "movement.h"
#include "player.h"
#include "raylib.h"
#include "render.h"
@ -117,8 +118,8 @@ static void init_floor(GameState *gs, int floor_num) {
gs->floors_reached = 1;
} else {
// Move player to new floor position
gs->player.x = start_x;
gs->player.y = start_y;
gs->player.position.x = start_x;
gs->player.position.y = start_y;
}
gs->player.floor = floor_num;
@ -137,7 +138,8 @@ 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);
spawn_floating_text(gs, gs->player.position.x * TILE_SIZE + 8, gs->player.position.y * TILE_SIZE, player_effect_dmg,
0);
gs->screen_shake = SHAKE_EFFECT_DURATION;
}
@ -155,7 +157,7 @@ static void tick_all_effects(GameState *gs) {
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);
spawn_floating_text(gs, e->position.x * TILE_SIZE + 8, e->position.y * TILE_SIZE, enemy_effect_dmg, 0);
}
if (!e->alive) {
add_log(gs, "Enemy died from effects!");
@ -173,7 +175,7 @@ static void post_action(GameState *gs, Enemy *attacked_enemy) {
return;
// Check if stepped on stairs
if (gs->map.tiles[gs->player.y][gs->player.x] == TILE_STAIRS) {
if (gs->map.tiles[gs->player.position.y][gs->player.position.x] == TILE_STAIRS) {
gs->awaiting_descend = 1;
gs->last_message = "Descend to next floor? (Y/N)";
gs->message_timer = 120;
@ -182,8 +184,8 @@ static void post_action(GameState *gs, Enemy *attacked_enemy) {
// 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;
int ex = attacked_enemy->position.x * TILE_SIZE + 8;
int ey = attacked_enemy->position.y * TILE_SIZE;
if (combat_was_dodged()) {
spawn_floating_label(gs, ex, ey, LABEL_DODGE, EFFECT_NONE);
@ -224,8 +226,8 @@ static void post_action(GameState *gs, Enemy *attacked_enemy) {
gs->screen_shake = SHAKE_PLAYER_DAMAGE_DURATION;
gs->damage_taken += combat_get_last_damage();
gs->times_hit++;
spawn_floating_text(gs, gs->player.x * TILE_SIZE + 8, gs->player.y * TILE_SIZE, combat_get_last_damage(),
combat_was_critical());
spawn_floating_text(gs, gs->player.position.x * TILE_SIZE + 8, gs->player.position.y * TILE_SIZE,
combat_get_last_damage(), combat_was_critical());
}
// Set message and check game over
@ -388,7 +390,7 @@ static int handle_movement_input(GameState *gs) {
// Check for manual item pickup (G key)
if (IsKeyPressed(KEY_G)) {
Item *item = get_item_at_floor(gs->items, gs->item_count, gs->player.x, gs->player.y);
Item *item = get_item_at_floor(gs->items, gs->item_count, gs->player.position.x, gs->player.position.y);
if (item != NULL) {
if (player_pickup(&gs->player, item)) {
gs->items_collected++;
@ -417,38 +419,45 @@ static int handle_movement_input(GameState *gs) {
}
}
// Movement: use IsKeyDown for held-key repeat
int dx = 0, dy = 0;
if (IsKeyDown(KEY_W) || IsKeyDown(KEY_UP))
dy = -1;
else if (IsKeyDown(KEY_S) || IsKeyDown(KEY_DOWN))
dy = 1;
else if (IsKeyDown(KEY_A) || IsKeyDown(KEY_LEFT))
dx = -1;
else if (IsKeyDown(KEY_D) || IsKeyDown(KEY_RIGHT))
dx = 1;
if (dx == 0 && dy == 0)
Vec2 direction = {0, 0};
if (IsKeyDown(KEY_W) || IsKeyDown(KEY_UP))
direction.y = -1;
else if (IsKeyDown(KEY_S) || IsKeyDown(KEY_DOWN))
direction.y = 1;
else if (IsKeyDown(KEY_A) || IsKeyDown(KEY_LEFT))
direction.x = -1;
else if (IsKeyDown(KEY_D) || IsKeyDown(KEY_RIGHT))
direction.x = 1;
if (direction.x == 0 && direction.y == 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 new_x = gs->player.position.x + direction.x;
int new_y = gs->player.position.y + direction.y;
Enemy *target = NULL;
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);
MoveResult result =
try_move_entity(&gs->player.position, direction, &gs->map, &gs->player, gs->enemies, gs->enemy_count, true);
if (result == MOVE_RESULT_MOVED) {
player_on_move(&gs->player);
action = 1;
} else {
action = player_move(&gs->player, dx, dy, &gs->map);
} else if (result == MOVE_RESULT_BLOCKED_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;
}
}
if (action)
post_action(gs, target); // target is NULL on move, enemy ptr on attack
post_action(gs, target);
return action;
}
@ -496,6 +505,8 @@ static void game_loop(void) {
GameState gs;
memset(&gs, 0, sizeof(GameState));
load_audio_assets(&gs);
// entities
Texture2D player_tile = LoadTexture("./assets/entities/player_white.png");
// Initialize first floor
rng_seed(12345);
init_floor(&gs, 1);
@ -543,7 +554,7 @@ static void game_loop(void) {
render_map(&gs.map);
render_items(gs.items, gs.item_count);
render_enemies(gs.enemies, gs.enemy_count);
render_player(&gs.player);
render_player(&gs.player, &player_tile);
EndMode2D();
// Floating texts follow world shake

29
src/movement.c Normal file
View file

@ -0,0 +1,29 @@
#include "movement.h"
#include "enemy.h"
#include "map.h"
#include <stdbool.h>
// Check if position is occupied by player
static int is_player_at(Player *p, int x, int y) {
return (p->position.x == x && p->position.y == y);
}
MoveResult try_move_entity(Vec2 *p, Vec2 direction, Map *map, Player *player, Enemy *enemies, int enemy_count,
bool moving_is_player) {
int new_x = p->x + direction.x;
int new_y = p->y + direction.y;
if (!is_floor(map, new_x, new_y))
return MOVE_RESULT_BLOCKED_WALL;
if (is_enemy_at(enemies, enemy_count, new_x, new_y))
return MOVE_RESULT_BLOCKED_ENEMY;
if (!moving_is_player) {
if (is_player_at(player, new_x, new_y))
return MOVE_RESULT_BLOCKED_PLAYER;
}
p->x = new_x;
p->y = new_y;
return MOVE_RESULT_MOVED;
}

17
src/movement.h Normal file
View file

@ -0,0 +1,17 @@
#ifndef MOVEMENT_H
#define MOVEMENT_H
#include "common.h"
typedef enum {
MOVE_RESULT_MOVED,
MOVE_RESULT_BLOCKED_WALL,
MOVE_RESULT_BLOCKED_PLAYER,
MOVE_RESULT_BLOCKED_ENEMY
} MoveResult;
// Attempts to move entity in a given direction. Returns outcome of action.
MoveResult try_move_entity(Vec2 *p, Vec2 direction, Map *map, Player *player, Enemy *enemies, int enemy_count,
bool moving_is_player);
#endif // MOVEMENT_H

View file

@ -2,15 +2,12 @@
#include "combat.h"
#include "common.h"
#include "items.h"
#include "map.h"
#include "settings.h"
#include "utils.h"
#include <stdlib.h>
#include <string.h>
void player_init(Player *p, int x, int y) {
p->x = x;
p->y = y;
p->position.x = x;
p->position.y = y;
p->hp = PLAYER_BASE_HP;
p->max_hp = PLAYER_BASE_HP;
p->attack = PLAYER_BASE_ATTACK;
@ -43,36 +40,20 @@ Enemy *player_find_enemy_at(Enemy *enemies, int count, int x, int y) {
if (count > MAX_ENEMIES)
count = MAX_ENEMIES;
for (int i = 0; i < count; i++) {
if (enemies[i].alive && enemies[i].x == x && enemies[i].y == y)
if (enemies[i].alive && enemies[i].position.x == x && enemies[i].position.y == y)
return &enemies[i];
}
return NULL;
}
int player_move(Player *p, int dx, int dy, Map *map) {
int new_x = p->x + dx;
int new_y = p->y + dy;
// Check bounds
if (!in_bounds(new_x, new_y, MAP_WIDTH, MAP_HEIGHT))
return 0;
// Check if walkable
if (!is_floor(map, new_x, new_y))
return 0;
// Move player
p->x = new_x;
p->y = new_y;
void player_on_move(Player *p) {
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)) {
p->hp += 1;
}
return 1;
}
void player_attack(Player *p, Enemy *e) {
@ -228,8 +209,8 @@ int player_drop_item(Player *p, int inv_index, Item *items, int item_count) {
if (items[i].picked_up) {
// Place dropped item at this position
items[i] = *item;
items[i].x = p->x;
items[i].y = p->y;
items[i].x = p->position.x;
items[i].y = p->position.y;
items[i].picked_up = 0;
// Remove from inventory
player_remove_inventory_item(p, inv_index);

View file

@ -6,8 +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);
// Apply status effects, healing, etc
void player_on_move(Player *p);
// 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);

View file

@ -29,9 +29,12 @@ void render_map(const Map *map) {
}
}
void render_player(const Player *p) {
Rectangle rect = {(float)(p->x * TILE_SIZE), (float)(p->y * TILE_SIZE), (float)TILE_SIZE, (float)TILE_SIZE};
DrawRectangleRec(rect, BLUE);
void render_player(const Player *p, Texture2D *ptile) {
//Rectangle rect = {(float)(p->position.x * TILE_SIZE), (float)(p->position.y * TILE_SIZE), (float)TILE_SIZE,
// (float)TILE_SIZE};
//DrawRectangleRec(rect, BLUE);
DrawTexture(*ptile, (float)(p->x * TILE_SIZE), (float)(p->y * TILE_SIZE), (Color){255, 255, 255, 255});
}
void render_enemies(const Enemy *enemies, int count) {
@ -39,8 +42,8 @@ void render_enemies(const Enemy *enemies, int count) {
if (!enemies[i].alive)
continue;
Rectangle rect = {(float)(enemies[i].x * TILE_SIZE), (float)(enemies[i].y * TILE_SIZE), (float)TILE_SIZE,
(float)TILE_SIZE};
Rectangle rect = {(float)(enemies[i].position.x * TILE_SIZE), (float)(enemies[i].position.y * TILE_SIZE),
(float)TILE_SIZE, (float)TILE_SIZE};
// Different colors based on enemy type
Color enemy_color;
@ -72,8 +75,8 @@ void render_enemies(const Enemy *enemies, int count) {
bar_color = (Color){200, 180, 40, 255}; // yellow
else
bar_color = (Color){200, 60, 60, 255}; // red
Rectangle hp_bar = {(float)(enemies[i].x * TILE_SIZE), (float)(enemies[i].y * TILE_SIZE - 4), (float)hp_pixels,
3};
Rectangle hp_bar = {(float)(enemies[i].position.x * TILE_SIZE), (float)(enemies[i].position.y * TILE_SIZE - 4),
(float)hp_pixels, 3};
DrawRectangleRec(hp_bar, bar_color);
}
}

View file

@ -77,7 +77,7 @@
void render_map(const Map *map);
// Render the player
void render_player(const Player *p);
void render_player(const Player *p, Texture2D *ptile);
// Render all enemies
void render_enemies(const Enemy *enemies, int count);