1
0
Fork 0
forked from NotAShelf/rogged

Compare commits

...

10 commits

Author SHA1 Message Date
09b46e4a32
font: squash commit into main
Squashed commit of the following:

commit a53942249c
Author: A.M. Rowsell <amr@frzn.dev>
Date:   Thu Apr 9 23:59:13 2026 -0400

    font: extensive tweaks, looks much better

commit 64205e137c
Author: A.M. Rowsell <amr@frzn.dev>
Date:   Thu Apr 9 12:13:00 2026 -0400

    font: fully implemented font changes to UI, size/spacing need tweaking

commit 901f063696
Author: A.M. Rowsell <amr@frzn.dev>
Date:   Wed Apr 8 09:36:03 2026 -0400

    font: tweak sizes of stats

commit 20f8c71fdf
Author: A.M. Rowsell <amr@frzn.dev>
Date:   Wed Apr 8 09:28:22 2026 -0400

    render: implement experimental font change, needs work
2026-04-10 08:20:10 -04:00
raf
702b4258e0 Merge pull request 'map: implement seeded generation; allow passing custom seed' (#17) from notashelf/push-svxosluqnsnp into main
Reviewed-on: NotAShelf/rogged#17
2026-04-10 11:15:44 +00:00
4475e6c276
various: persist seed display; fix seed 0 handling
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I349ed28a792d4de685f8468eddd33a136a6a6964
2026-04-10 14:15:20 +03:00
8bbca55b78
rogged: re-seed on game-over; display seed in game-end screen
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I0178392036902a87b64fde63f5a5f56a6a6a6964
2026-04-10 13:29:50 +03:00
f51b754e76
map: implement seeded generation; allow passing custom seed
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I742b7e59c7ca872539d4ebfe3a03b44f6a6a6964
2026-04-10 11:09:51 +03:00
raf
09f7e659b5 Merge pull request 'various: implement fog of war; make enemy AI slightly more intelligent' (#14) from notashelf/push-wlxxmonulyzt into main
Reviewed-on: NotAShelf/rogged#14
Reviewed-by: A.M. Rowsell <amr@noreply.localhost>
2026-04-10 06:07:57 +00:00
71343311eb
enemy: add alert memory; vision variance based on type
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I2f5c7cac72c8772e5871b99026d106b46a6a6964
2026-04-09 21:28:18 +03:00
f85d28e932
various: implement fog of war; make enemy AI slightly more intelligent
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I3e22dbc5e10690871255980c52a24c226a6a6964
2026-04-09 21:28:17 +03: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
17 changed files with 610 additions and 220 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

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;
@ -30,6 +34,8 @@ typedef struct {
TileType tiles[MAP_HEIGHT][MAP_WIDTH];
Room rooms[MAX_ROOMS];
int room_count;
unsigned char visible[MAP_HEIGHT][MAP_WIDTH];
unsigned char remembered[MAP_HEIGHT][MAP_WIDTH];
} Map;
// Dungeon
@ -57,7 +63,7 @@ typedef struct {
// Player
typedef struct {
int x, y;
Vec2 position;
int hp, max_hp;
int attack;
int defense;
@ -83,7 +89,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;
@ -98,6 +104,11 @@ typedef struct {
int status_chance;
int crit_chance; // crit chance percentage (0-100)
int crit_mult; // crit damage multiplier percentage (e.g. 150 = 1.5x)
// vision
int vision_range;
int alert; // 1 = aware of player, searching
int last_known_x; // last position where enemy saw player
int last_known_y;
// status effects
StatusEffect effects[MAX_EFFECTS];
int effect_count;
@ -162,6 +173,8 @@ typedef struct {
int potions_used;
int floors_reached;
int final_score;
// Seed for this run
unsigned int run_seed;
} GameState;

View file

@ -2,7 +2,9 @@
#include "combat.h"
#include "common.h"
#include "map.h"
#include "movement.h"
#include "rng.h"
#include "settings.h"
#include <string.h>
// Forward declaration
@ -29,7 +31,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 +43,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;
@ -68,6 +70,7 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) {
e.resistance[DMG_PIERCE] = 0;
e.resistance[DMG_FIRE] = -25;
e.resistance[DMG_POISON] = 50;
e.vision_range = 7;
break;
case ENEMY_SKELETON:
e.max_hp = ENEMY_BASE_HP + floor + 2;
@ -85,6 +88,7 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) {
e.resistance[DMG_PIERCE] = 50;
e.resistance[DMG_FIRE] = 25;
e.resistance[DMG_POISON] = 75;
e.vision_range = 6;
break;
case ENEMY_ORC:
e.max_hp = ENEMY_BASE_HP + floor + 4;
@ -102,6 +106,7 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) {
e.resistance[DMG_PIERCE] = -25;
e.resistance[DMG_FIRE] = 0;
e.resistance[DMG_POISON] = 0;
e.vision_range = 5;
break;
default:
e.max_hp = ENEMY_BASE_HP;
@ -115,6 +120,7 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) {
e.crit_chance = ENEMY_CRIT_CHANCE;
e.crit_mult = ENEMY_CRIT_MULT;
memset(e.resistance, 0, sizeof(e.resistance));
e.vision_range = ENEMY_VIEW_RANGE;
break;
}
e.cooldown = e.speed;
@ -127,58 +133,129 @@ 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;
}
}
return 0;
}
// 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;
return (dx >= -1 && dx <= 1 && dy >= -1 && dy <= 1);
// Check if enemy can see player (within view range and line of sight)
static int can_see_player(Enemy *e, Player *p, Map *map) {
return can_see_entity(map, e->position.x, e->position.y, p->position.x, p->position.y, e->vision_range);
}
// 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;
dir.x = 0;
dir.y = dy;
if (dy != 0) {
try_move_entity(&e->position, dir, map, p, all_enemies, enemy_count, false);
}
}
// Move enemy in a random direction (patrol)
static void enemy_patrol(Enemy *e, Map *map, Enemy *all_enemies, int enemy_count) {
if (rng_int(0, 100) > ENEMY_PATROL_MOVE_CHANCE)
return;
int dx = rng_int(-1, 1);
int dy = rng_int(-1, 1);
if (dx == 0 && dy == 0)
return;
int new_x = e->position.x + dx;
int new_y = e->position.y + dy;
if (is_floor(map, new_x, new_y) && !is_enemy_at(all_enemies, enemy_count, new_x, new_y)) {
e->position.x = new_x;
e->position.y = new_y;
}
}
// Move enemy toward last known player position
static void enemy_move_to_last_known(Enemy *e, Map *map, Enemy *all_enemies, int enemy_count) {
int dx = 0, dy = 0;
if (e->last_known_x > e->position.x)
dx = 1;
else if (e->last_known_x < e->position.x)
dx = -1;
if (e->last_known_y > e->position.y)
dy = 1;
else if (e->last_known_y < e->position.y)
dy = -1;
int new_x = e->position.x + dx;
int new_y = e->position.y;
if (dx != 0 && is_floor(map, new_x, new_y) && !is_enemy_at(all_enemies, enemy_count, new_x, new_y)) {
e->position.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;
new_x = e->position.x;
new_y = e->position.y + dy;
if (is_floor(map, new_x, new_y) && !is_enemy_at(all_enemies, enemy_count, new_x, new_y)) {
e->position.x = new_x;
e->position.y = new_y;
}
}
if (e->position.x == e->last_known_x && e->position.y == e->last_known_y)
e->alert = 0;
}
// Check if position is within alert radius of another enemy
static int is_nearby_enemy(const Enemy *enemies, int count, int x, int y, int radius) {
for (int i = 0; i < count; i++) {
if (!enemies[i].alive)
continue;
int dx = enemies[i].position.x - x;
int dy = enemies[i].position.y - y;
if (dx * dx + dy * dy <= radius * radius)
return 1;
}
return 0;
}
// Propagate alert to nearby enemies
static void propagate_alert(Enemy *trigger_enemy, Enemy *all_enemies, int enemy_count) {
for (int i = 0; i < enemy_count; i++) {
Enemy *e = &all_enemies[i];
if (!e->alive || e == trigger_enemy)
continue;
if (e->alert)
continue;
if (is_nearby_enemy(all_enemies, enemy_count, e->position.x, e->position.y, 5)) {
e->alert = 1;
e->last_known_x = trigger_enemy->last_known_x;
e->last_known_y = trigger_enemy->last_known_y;
}
}
}
// Perform a single action for an enemy (attack if adjacent, otherwise move)
// Perform a single action for an enemy (attack if visible, otherwise patrol or search)
void enemy_act(Enemy *e, Player *p, Map *map, Enemy *all_enemies, int enemy_count) {
if (!e->alive)
return;
@ -187,14 +264,37 @@ void enemy_act(Enemy *e, Player *p, Map *map, Enemy *all_enemies, int enemy_coun
if (combat_has_effect(e->effects, e->effect_count, EFFECT_STUN))
return;
// Check if adjacent to player - attack
if (can_see_player(e, p)) {
int can_see = can_see_player(e, p, map);
// If we can see the player, update alert state and last known position
if (can_see) {
e->alert = 1;
e->last_known_x = p->position.x;
e->last_known_y = p->position.y;
}
// Attack if adjacent to player
if (can_see && can_see_entity(map, e->position.x, e->position.y, p->position.x, p->position.y, 1)) {
combat_enemy_attack(e, p);
propagate_alert(e, all_enemies, enemy_count);
return;
}
// Otherwise, move toward player
enemy_move_toward_player(e, p, map, all_enemies, enemy_count);
// Move toward player if visible
if (can_see) {
enemy_move_toward_player(e, p, map, all_enemies, enemy_count);
propagate_alert(e, all_enemies, enemy_count);
return;
}
// If alert but can't see player, move toward last known position
if (e->alert) {
enemy_move_to_last_known(e, map, all_enemies, enemy_count);
return;
}
// Not alert - patrol randomly
enemy_patrol(e, map, all_enemies, enemy_count);
}
void enemy_update_all(Enemy enemies[], int count, Player *p, Map *map) {

View file

@ -4,14 +4,18 @@
#include "enemy.h"
#include "items.h"
#include "map.h"
#include "movement.h"
#include "player.h"
#include "raylib.h"
#include "render.h"
#include "rng.h"
#include "settings.h"
#include <ctype.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
// Add message to action log
static void add_log(GameState *gs, const char *msg) {
@ -101,11 +105,14 @@ static void update_effects(GameState *gs) {
// Initialize a new floor
static void init_floor(GameState *gs, int floor_num) {
// Seed RNG with run seed combined with floor number for deterministic generation
rng_seed(gs->run_seed + floor_num * 54321);
// Generate dungeon
dungeon_generate(&gs->dungeon, &gs->map, floor_num);
// Seed rng for this floor's content
rng_seed(floor_num * 54321);
rng_seed(gs->run_seed + floor_num * 98765);
// Find spawn position
int start_x, start_y;
@ -117,11 +124,14 @@ 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;
// Calculate initial visibility
calculate_visibility(&gs->map, gs->player.position.x, gs->player.position.y);
// Spawn enemies
enemy_spawn(gs->enemies, &gs->enemy_count, &gs->map, &gs->player, floor_num);
@ -137,7 +147,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 +166,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 +184,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 +193,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);
@ -215,6 +226,9 @@ static void post_action(GameState *gs, Enemy *attacked_enemy) {
}
}
// Update visibility based on player's new position
calculate_visibility(&gs->map, gs->player.position.x, gs->player.position.y);
// Enemy turns - uses speed/cooldown system
enemy_update_all(gs->enemies, gs->enemy_count, &gs->player, &gs->map);
@ -224,8 +238,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
@ -246,6 +260,7 @@ static int handle_stun_turn(GameState *gs) {
if (gs->game_over)
return 1;
enemy_update_all(gs->enemies, gs->enemy_count, &gs->player, &gs->map);
calculate_visibility(&gs->map, gs->player.position.x, gs->player.position.y);
if (gs->player.hp <= 0)
gs->game_over = 1;
gs->last_message = "You are stunned!";
@ -388,7 +403,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 +432,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;
}
@ -462,7 +484,13 @@ static int handle_input(GameState *gs) {
// Check for restart (works during game over)
if (IsKeyPressed(KEY_R) && gs->game_over) {
memset(gs, 0, sizeof(GameState));
// Generate a new random seed for the new run
gs->run_seed = (unsigned int)time(NULL);
init_floor(gs, 1);
// Update window title with new seed
char title[128];
snprintf(title, sizeof(title), "Roguelike - Seed: %u", gs->run_seed);
SetWindowTitle(title);
return 0;
}
@ -492,12 +520,19 @@ void load_audio_assets(GameState *gs) {
}
// Main game loop
static void game_loop(void) {
static void game_loop(unsigned int run_seed) {
GameState gs;
memset(&gs, 0, sizeof(GameState));
<<<<<<< HEAD
gs.run_seed = run_seed;
=======
// load external assets
// sound
>>>>>>> acdbc9c (font: squash commit into main)
load_audio_assets(&gs);
// font
Font fontTTF = LoadFontEx("./assets/fonts/Tomorrow_Night.ttf", 24, NULL, 0);
// Initialize first floor
rng_seed(12345);
init_floor(&gs, 1);
// Disable esc to exit
@ -517,10 +552,16 @@ static void game_loop(void) {
break;
if (IsKeyPressed(KEY_R)) {
memset(&gs, 0, sizeof(GameState));
// Generate a new random seed for the new run
gs.run_seed = (unsigned int)time(NULL);
gs.game_over = 0;
gs.game_won = 0;
load_audio_assets(&gs);
init_floor(&gs, 1);
// Update window title with new seed
char title[128];
snprintf(title, sizeof(title), "Roguelike - Seed: %u", gs.run_seed);
SetWindowTitle(title);
}
}
@ -541,28 +582,31 @@ static void game_loop(void) {
cam.offset = (Vector2){(float)gs.shake_x, (float)gs.shake_y};
BeginMode2D(cam);
render_map(&gs.map);
render_items(gs.items, gs.item_count);
render_enemies(gs.enemies, gs.enemy_count);
render_items(gs.items, gs.item_count, gs.map.visible);
render_enemies(gs.enemies, gs.enemy_count, gs.map.visible);
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);
render_ui(&gs.player, &fontTTF);
// Draw action log
render_action_log(gs.action_log, gs.log_count, gs.log_head);
render_action_log(gs.action_log, gs.log_count, gs.log_head, &fontTTF);
// Draw inventory overlay if active
if (gs.show_inventory) {
render_inventory_overlay(&gs.player, gs.inv_selected);
render_inventory_overlay(&gs.player, gs.inv_selected, &fontTTF);
}
// Draw message if any
if (gs.last_message != NULL && gs.message_timer > 0) {
render_message(gs.last_message);
render_message(gs.last_message, &fontTTF);
}
// Draw persistent seed display in top right
render_seed_display(gs.run_seed);
// Draw game over screen
if (gs.game_over) {
// Compute final score
@ -573,7 +617,7 @@ static void game_loop(void) {
}
render_end_screen(gs.game_won, gs.total_kills, gs.items_collected, gs.damage_dealt, gs.damage_taken,
gs.crits_landed, gs.times_hit, gs.potions_used, gs.floors_reached, gs.turn_count,
gs.final_score);
gs.final_score, gs.run_seed, &fontTTF);
}
EndDrawing();
@ -583,17 +627,67 @@ static void game_loop(void) {
}
}
int main(void) {
// Check if a string is a valid unsigned integer
static int is_valid_uint(const char *str) {
if (str == NULL || *str == '\0')
return 0;
// Check for optional leading +
if (*str == '+')
str++;
// Must have at least one digit
if (*str == '\0')
return 0;
// All characters must be digits
for (const char *p = str; *p != '\0'; p++) {
if (!isdigit((unsigned char)*p))
return 0;
}
return 1;
}
int main(int argc, char **argv) {
// Parse command-line arguments
unsigned int run_seed = 0;
int seed_provided = 0;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--seed") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "Error: --seed requires a value\n");
fprintf(stderr, "Usage: %s [--seed <number>]\n", argv[0]);
return 1;
}
const char *seed_str = argv[i + 1];
if (!is_valid_uint(seed_str)) {
fprintf(stderr, "Error: Invalid seed value: %s\n", seed_str);
fprintf(stderr, "Seed must be a non-negative integer\n");
return 1;
}
run_seed = (unsigned int)strtoul(seed_str, NULL, 10);
seed_provided = 1;
i++; // Skip the value
}
}
// If no seed provided, generate random seed from time
if (!seed_provided) {
run_seed = (unsigned int)time(NULL);
}
printf("Starting game with seed: %u\n", run_seed);
// Initialize audio
audio_init();
// Initialize random number generator
SetRandomSeed(88435);
// Initialize window
InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT + 60, "Roguelike");
// Initialize window with seed in title
char title[128];
snprintf(title, sizeof(title), "Roguelike - Seed: %u", run_seed);
InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT + 60, title);
SetTargetFPS(60);
// Run game
game_loop();
game_loop(run_seed);
// Cleanup
CloseWindow();

View file

@ -1,6 +1,8 @@
#include "map.h"
#include "rng.h"
#include "settings.h"
#include "utils.h"
#include <stdlib.h>
#include <string.h>
void map_init(Map *map) {
@ -10,6 +12,8 @@ void map_init(Map *map) {
map->tiles[y][x] = TILE_WALL;
}
}
memset(map->visible, 0, sizeof(map->visible));
memset(map->remembered, 0, sizeof(map->remembered));
map->room_count = 0;
}
@ -166,9 +170,6 @@ void get_random_floor_tile(Map *map, int *x, int *y, int attempts) {
}
void dungeon_generate(Dungeon *d, Map *map, int floor_num) {
// Seed RNG with floor number for deterministic generation
rng_seed(floor_num * 12345);
// Initialize map to all walls
map_init(map);
@ -186,3 +187,67 @@ void dungeon_generate(Dungeon *d, Map *map, int floor_num) {
d->room_count = map->room_count;
memcpy(d->rooms, map->rooms, sizeof(Room) * map->room_count);
}
int is_in_view_range(int x, int y, int view_x, int view_y, int range) {
int dx = x - view_x;
int dy = y - view_y;
return (dx * dx + dy * dy) <= (range * range);
}
static int trace_line_of_sight(const Map *map, int x1, int y1, int x2, int y2) {
int dx = abs(x2 - x1);
int dy = abs(y2 - y1);
int sx = (x1 < x2) ? 1 : -1;
int sy = (y1 < y2) ? 1 : -1;
int err = dx - dy;
int x = x1;
int y = y1;
while (1) {
if (!in_bounds(x, y, MAP_WIDTH, MAP_HEIGHT))
return 0;
if (x == x2 && y == y2)
return 1;
if (map->tiles[y][x] == TILE_WALL && !(x == x1 && y == y1))
return 0;
int e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x += sx;
}
if (e2 < dx) {
err += dx;
y += sy;
}
}
}
int has_line_of_sight(const Map *map, int x1, int y1, int x2, int y2) {
if (!in_bounds(x1, y1, MAP_WIDTH, MAP_HEIGHT) || !in_bounds(x2, y2, MAP_WIDTH, MAP_HEIGHT))
return 0;
return trace_line_of_sight(map, x1, y1, x2, y2);
}
int can_see_entity(const Map *map, int from_x, int from_y, int to_x, int to_y, int range) {
if (!is_in_view_range(to_x, to_y, from_x, from_y, range))
return 0;
return has_line_of_sight(map, from_x, from_y, to_x, to_y);
}
void calculate_visibility(Map *map, int x, int y) {
memset(map->visible, 0, sizeof(map->visible));
for (int ty = 0; ty < MAP_HEIGHT; ty++) {
for (int tx = 0; tx < MAP_WIDTH; tx++) {
if (is_in_view_range(tx, ty, x, y, PLAYER_VIEW_RANGE)) {
if (has_line_of_sight(map, x, y, tx, ty)) {
map->visible[ty][tx] = 1;
map->remembered[ty][tx] = 1;
}
}
}
}
}

View file

@ -18,4 +18,10 @@ void map_init(Map *map);
// Get a random floor tile position
void get_random_floor_tile(Map *map, int *x, int *y, int attempts);
// Visibility / Fog of War
int is_in_view_range(int x, int y, int view_x, int view_y, int range);
int has_line_of_sight(const Map *map, int x1, int y1, int x2, int y2);
void calculate_visibility(Map *map, int x, int y);
int can_see_entity(const Map *map, int from_x, int from_y, int to_x, int to_y, int range);
#endif // MAP_H

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

@ -11,18 +11,31 @@ void render_map(const Map *map) {
for (int y = 0; y < MAP_HEIGHT; y++) {
for (int x = 0; x < MAP_WIDTH; x++) {
Rectangle rect = {(float)(x * TILE_SIZE), (float)(y * TILE_SIZE), (float)TILE_SIZE, (float)TILE_SIZE};
int visible = map->visible[y][x];
int remembered = map->remembered[y][x];
if (!visible && !remembered) {
DrawRectangleRec(rect, (Color){5, 5, 10, 255});
continue;
}
Color wall_color = visible ? DARKGRAY : (Color){25, 25, 30, 255};
Color floor_color = visible ? BLACK : (Color){15, 15, 20, 255};
Color stairs_color = visible ? (Color){100, 100, 100, 255} : (Color){40, 40, 45, 255};
switch (map->tiles[y][x]) {
case TILE_WALL:
DrawRectangleRec(rect, DARKGRAY);
DrawRectangleRec(rect, wall_color);
break;
case TILE_FLOOR:
DrawRectangleRec(rect, BLACK);
DrawRectangleRec(rect, floor_color);
break;
case TILE_STAIRS:
DrawRectangleRec(rect, (Color){100, 100, 100, 255});
// Draw stairs marker
DrawText(">", x * TILE_SIZE + 4, y * TILE_SIZE + 2, 12, WHITE);
DrawRectangleRec(rect, stairs_color);
if (visible)
DrawText(">", x * TILE_SIZE + 4, y * TILE_SIZE + 2, 12, WHITE);
else
DrawText(">", x * TILE_SIZE + 4, y * TILE_SIZE + 2, 12, (Color){60, 60, 65, 255});
break;
}
}
@ -30,17 +43,20 @@ 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};
Rectangle rect = {(float)(p->position.x * TILE_SIZE), (float)(p->position.y * TILE_SIZE), (float)TILE_SIZE,
(float)TILE_SIZE};
DrawRectangleRec(rect, BLUE);
}
void render_enemies(const Enemy *enemies, int count) {
void render_enemies(const Enemy *enemies, int count, const unsigned char visible[MAP_HEIGHT][MAP_WIDTH]) {
for (int i = 0; i < count; i++) {
if (!enemies[i].alive)
continue;
if (!visible[enemies[i].position.y][enemies[i].position.x])
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,17 +88,19 @@ 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);
}
}
}
void render_items(const Item *items, int count) {
void render_items(const Item *items, int count, const unsigned char visible[MAP_HEIGHT][MAP_WIDTH]) {
for (int i = 0; i < count; i++) {
if (items[i].picked_up)
continue;
if (!visible[items[i].y][items[i].x])
continue;
Rectangle rect = {(float)(items[i].x * TILE_SIZE), (float)(items[i].y * TILE_SIZE), (float)TILE_SIZE,
(float)TILE_SIZE};
@ -108,7 +126,7 @@ void render_items(const Item *items, int count) {
}
}
void render_ui(const Player *p) {
void render_ui(const Player *p, Font *font) {
// HUD Panel
const int hud_y = MAP_HEIGHT * TILE_SIZE;
const int hud_height = 60;
@ -152,7 +170,7 @@ void render_ui(const Player *p) {
int bar_height = 16;
// HP Label, above bar
DrawText("HP", bar_x, bar_y - 11, 9, text_dim);
DrawTextEx(*font, "HP", (Vector2){bar_x, bar_y - 17}, BIG_FONT, NAR_CHAR_SPACE, text_dim);
// HP Bar background
DrawRectangle(bar_x, bar_y, bar_width, bar_height, (Color){20, 15, 15, 255});
@ -177,8 +195,9 @@ void render_ui(const Player *p) {
// HP text, centered in bar
char hp_text[32];
snprintf(hp_text, sizeof(hp_text), "%d/%d", p->hp, p->max_hp);
int hp_text_w = MeasureText(hp_text, 10);
DrawText(hp_text, bar_x + (bar_width - hp_text_w) / 2, bar_y + 2, 10, WHITE);
int hp_text_w = MeasureText(hp_text, 12);
DrawTextEx(*font, hp_text, (Vector2){bar_x + (bar_width - hp_text_w) / 2.0f, bar_y + 2}, MEDIUM_FONT,
SMALL_CHAR_SPACE, WHITE);
// Status effects
int effect_x = bar_x;
@ -213,7 +232,7 @@ void render_ui(const Player *p) {
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, effect_y, 9, eff_color);
DrawTextEx(*font, eff_text, (Vector2){effect_x, effect_y}, SMALL_FONT, NAR_CHAR_SPACE, eff_color);
effect_x += 28;
}
}
@ -225,65 +244,68 @@ void render_ui(const Player *p) {
// Floor
char floor_text[16];
snprintf(floor_text, sizeof(floor_text), "F%d", p->floor);
DrawText(floor_text, stats_x, stats_y, 14, text_bright);
DrawText("Floor", stats_x, stats_y + 16, 9, text_dim);
DrawTextEx(*font, floor_text, (Vector2){stats_x, stats_y}, 16, NORM_CHAR_SPACE, text_bright);
DrawTextEx(*font, "Floor", (Vector2){stats_x, stats_y + 16}, 12, NAR_CHAR_SPACE, text_dim);
// ATK
char atk_text[16];
snprintf(atk_text, sizeof(atk_text), "%d", p->attack);
DrawText(atk_text, stats_x + stat_spacing, stats_y, 14, YELLOW);
DrawText("ATK", stats_x + stat_spacing, stats_y + 16, 9, text_dim);
DrawTextEx(*font, atk_text, (Vector2){stats_x + stat_spacing, stats_y}, 16, NORM_CHAR_SPACE, YELLOW);
DrawTextEx(*font, "ATK", (Vector2){stats_x + stat_spacing, stats_y + 16}, 12, NAR_CHAR_SPACE, text_dim);
// DEF
char def_text[16];
snprintf(def_text, sizeof(def_text), "%d", p->defense);
DrawText(def_text, stats_x + stat_spacing * 2, stats_y, 14, (Color){100, 150, 255, 255});
DrawText("DEF", stats_x + stat_spacing * 2, stats_y + 16, 9, text_dim);
DrawTextEx(*font, def_text, (Vector2){stats_x + stat_spacing * 2, stats_y}, 16, NORM_CHAR_SPACE,
(Color){100, 150, 255, 255});
DrawTextEx(*font, "DEF", (Vector2){stats_x + stat_spacing * 2, stats_y + 16}, 12, NAR_CHAR_SPACE, text_dim);
int equip_x = section2_end + 15;
int equip_y = hud_y + 8;
// Weapon slot
DrawText("WEAPON", equip_x, equip_y, 9, text_dim);
DrawTextEx(*font, "WEAPON", (Vector2){equip_x, equip_y}, MEDIUM_FONT, NAR_CHAR_SPACE, text_dim);
if (p->has_weapon) {
const char *weapon_name = item_get_name(&p->equipped_weapon);
if (weapon_name) {
char weapon_text[64];
snprintf(weapon_text, sizeof(weapon_text), "%s +%d [%s]", weapon_name, p->equipped_weapon.power,
dmg_class_get_short(p->equipped_weapon.dmg_class));
DrawText(weapon_text, equip_x, equip_y + 11, 10, (Color){255, 220, 100, 255});
DrawTextEx(*font, weapon_text, (Vector2){equip_x, equip_y + 11}, 10, NAR_CHAR_SPACE, (Color){255, 220, 100, 255});
}
} else {
DrawText("None [IMP]", equip_x, equip_y + 11, 10, (Color){80, 75, 70, 255});
DrawTextEx(*font, "None [IMP]", (Vector2){equip_x, equip_y + 11}, 10, NAR_CHAR_SPACE, (Color){80, 75, 70, 255});
}
// Armor slot
DrawText("ARMOR", equip_x, equip_y + 26, 9, text_dim);
DrawTextEx(*font, "ARMOR", (Vector2){equip_x, equip_y + 26}, MEDIUM_FONT, NAR_CHAR_SPACE, text_dim);
if (p->has_armor) {
const char *armor_name = item_get_name(&p->equipped_armor);
if (armor_name) {
char armor_text[48];
snprintf(armor_text, sizeof(armor_text), "%s +%d", armor_name, p->equipped_armor.power);
DrawText(armor_text, equip_x, equip_y + 37, 10, (Color){100, 150, 255, 255});
DrawTextEx(*font, armor_text, (Vector2){equip_x, equip_y + 37}, 10, NAR_CHAR_SPACE, (Color){100, 150, 255, 255});
}
} else {
DrawText("None", equip_x, equip_y + 37, 10, (Color){80, 75, 70, 255});
DrawTextEx(*font, "None", (Vector2){equip_x, equip_y + 37}, 10, NAR_CHAR_SPACE, (Color){80, 75, 70, 255});
}
int ctrl_x = section3_end + 20;
int ctrl_y = hud_y + 14;
DrawText("[WASD] Move [G] Pickup [I] Inventory [U] Use", ctrl_x, ctrl_y, 11, (Color){139, 119, 89, 255});
DrawText("[E] Equip [D] Drop [Q] Quit", ctrl_x, ctrl_y + 16, 11, (Color){139, 119, 89, 255});
DrawTextEx(*font, "[WASD] Move [G] Pickup [I] Inventory [U] Use", (Vector2){ctrl_x, ctrl_y}, MEDIUM_FONT,
MED_CHAR_SPACE, (Color){139, 119, 89, 255});
DrawTextEx(*font, "[E] Equip [D] Drop [Q] Quit", (Vector2){ctrl_x, ctrl_y + 16}, MEDIUM_FONT, MED_CHAR_SPACE,
(Color){139, 119, 89, 255});
// INV count in top-right corner of HUD
char inv_text[16];
snprintf(inv_text, sizeof(inv_text), "INV: %d/%d", p->inventory_count, MAX_INVENTORY);
int inv_width = MeasureText(inv_text, 10);
DrawText(inv_text, SCREEN_WIDTH - inv_width - 10, hud_y + 5, 10, GREEN);
DrawTextEx(*font, inv_text, (Vector2){SCREEN_WIDTH - inv_width - 10, hud_y + 5}, 10, NAR_CHAR_SPACE, GREEN);
}
void render_action_log(const char log[5][128], int count, int head) {
void render_action_log(const char log[5][128], int count, int head, Font *font) {
// Roguelike scroll/log panel styling
const int log_width = 250;
const int log_height = 90;
@ -305,7 +327,8 @@ void render_action_log(const char log[5][128], int count, int head) {
// Title bar
DrawRectangle(log_x + 4, log_y + 4, log_width - 8, 16, (Color){30, 25, 20, 255});
DrawText("MESSAGE LOG", log_x + 8, log_y + 6, 10, (Color){180, 160, 130, 255});
DrawTextEx(*font, "MESSAGE LOG", (Vector2){log_x + 8, log_y + 6}, MEDIUM_FONT, NAR_CHAR_SPACE,
(Color){180, 160, 130, 255});
// Separator line under title
DrawLine(log_x + 4, log_y + 22, log_x + log_width - 5, log_y + 22, log_border_dark);
@ -330,15 +353,15 @@ void render_action_log(const char log[5][128], int count, int head) {
} else {
text_color = (Color){120, 110, 100, 200}; // oldest: dim
}
DrawText(log[idx], text_x, text_start_y + i * line_height, 10, text_color);
DrawTextEx(*font, log[idx], (Vector2){text_x, text_start_y + i * line_height}, 10, NAR_CHAR_SPACE, text_color);
}
}
}
void render_inventory_overlay(const Player *p, int selected) {
void render_inventory_overlay(const Player *p, int selected, Font *font) {
// Overlay dimensions
int ov_width = 360;
int ov_height = 300;
int ov_height = 320;
Rectangle overlay = {(float)(SCREEN_WIDTH - ov_width) / 2, (float)(SCREEN_HEIGHT - ov_height) / 2 - 60,
(float)ov_width, (float)ov_height};
DrawRectangleRec(overlay, (Color){12, 12, 12, 252});
@ -347,12 +370,14 @@ void render_inventory_overlay(const Player *p, int selected) {
// Title
const char *title = "INVENTORY";
int title_w = MeasureText(title, 24);
DrawText(title, overlay.x + (overlay.width - title_w) / 2, overlay.y + 12, 24, WHITE);
Vector2 t_w = MeasureTextEx(*font, title, 30, NORM_CHAR_SPACE);
DrawTextEx(*font, title, (Vector2){overlay.x + (overlay.width - t_w.x) / 2, overlay.y + 10}, HUGE_FONT,
NORM_CHAR_SPACE, WHITE);
// Draw each inventory slot
char slot_text[64];
int row_height = 26;
int start_y = overlay.y + 50;
int start_y = overlay.y + 40;
for (int i = 0; i < MAX_INVENTORY; i++) {
int y_pos = start_y + (i * row_height);
@ -369,7 +394,8 @@ void render_inventory_overlay(const Player *p, int selected) {
// Slot number
snprintf(slot_text, sizeof(slot_text), "%d.", i + 1);
DrawText(slot_text, overlay.x + 16, y_pos + 4, 14, (Color){80, 80, 80, 255});
DrawTextEx(*font, slot_text, (Vector2){overlay.x + 16, y_pos + 4}, MEDIUM_FONT, SMALL_CHAR_SPACE,
(Color){80, 80, 80, 255});
// Item name
const char *name = item_get_name(item);
@ -377,31 +403,31 @@ void render_inventory_overlay(const Player *p, int selected) {
Color name_color = (item->type == ITEM_POTION) ? (Color){255, 140, 140, 255}
: (item->type == ITEM_WEAPON) ? (Color){255, 255, 140, 255}
: (Color){140, 140, 255, 255};
DrawText(name, overlay.x + 45, y_pos + 4, 14, name_color);
DrawTextEx(*font, name, (Vector2){overlay.x + 45, y_pos + 4}, 14, NORM_CHAR_SPACE, name_color);
}
// Power
snprintf(slot_text, sizeof(slot_text), "+%d", item->power);
DrawText(slot_text, overlay.x + 150, y_pos + 4, 14, YELLOW);
DrawTextEx(*font, slot_text, (Vector2){overlay.x + 150, y_pos + 4}, 14, NORM_CHAR_SPACE, YELLOW);
// Action
if (item->type == ITEM_POTION) {
DrawText("[U]se", overlay.x + 200, y_pos + 4, 14, GREEN);
DrawTextEx(*font, "[U]se", (Vector2){overlay.x + 200, y_pos + 4}, 14, NORM_CHAR_SPACE, GREEN);
} else {
DrawText("[E]quip [D]rop", overlay.x + 200, y_pos + 4, 14, GOLD);
DrawTextEx(*font, "[E]quip [D]rop", (Vector2){overlay.x + 200, y_pos + 4}, 14, NORM_CHAR_SPACE, GOLD);
}
} else {
// Empty slot
snprintf(slot_text, sizeof(slot_text), "%d.", i + 1);
DrawText(slot_text, overlay.x + 16, y_pos + 4, 14, (Color){40, 40, 40, 255});
DrawTextEx(*font, slot_text, (Vector2){overlay.x + 16, y_pos + 4}, MEDIUM_FONT, SMALL_CHAR_SPACE,
(Color){40, 40, 40, 255});
}
}
// Instructions at bottom
const char *hint = "[1-0] Select [E] Equip [U] Use [D] Drop [I/ESC] Close";
int hint_w = MeasureText(hint, 12);
DrawText(hint, overlay.x + (overlay.width - hint_w) / 2, overlay.y + overlay.height - 22, 12,
(Color){65, 65, 65, 255});
Vector2 hint_w = MeasureTextEx(*font, hint, NORM_FONT, NAR_CHAR_SPACE);
DrawTextEx(*font, hint, (Vector2){overlay.x + (overlay.width - hint_w.x) / 2, overlay.y + overlay.height - 22},
NORM_FONT, NAR_CHAR_SPACE, (Color){80, 80, 80, 255});
}
static Color label_color(FloatingText *ft, int alpha) {
@ -489,7 +515,7 @@ void render_floating_texts(FloatingText *texts, int count, int shake_x, int shak
}
void render_end_screen(int is_victory, int kills, int items, int damage_dealt, int damage_taken, int crits,
int times_hit, int potions, int floors, int turns, int score) {
int times_hit, int potions, int floors, int turns, int score, unsigned int seed, Font *font) {
// Semi-transparent overlay
Rectangle overlay = {0, 0, (float)SCREEN_WIDTH, (float)SCREEN_HEIGHT};
DrawRectangleRec(overlay, (Color){0, 0, 0, 210});
@ -499,13 +525,14 @@ void render_end_screen(int is_victory, int kills, int items, int damage_dealt, i
int title_font_size = 60;
Color title_color = is_victory ? GOLD : RED;
int title_width = MeasureText(title, title_font_size);
DrawText(title, (SCREEN_WIDTH - title_width) / 2, 30, title_font_size, title_color);
DrawTextEx(*font, title, (Vector2){(SCREEN_WIDTH - title_width) / 2.0f, 30}, title_font_size, NORM_CHAR_SPACE,
title_color);
// Stats box
int box_x = SCREEN_WIDTH / 2 - 200;
int box_y = 110;
int box_w = 400;
int box_h = 320;
int box_h = 350;
DrawRectangle(box_x, box_y, box_w, box_h, (Color){20, 20, 20, 240});
DrawRectangleLines(box_x, box_y, box_w, box_h, (Color){100, 100, 100, 255});
@ -519,71 +546,80 @@ void render_end_screen(int is_victory, int kills, int items, int damage_dealt, i
Color value_color = WHITE;
// Column 1
DrawText("Kills:", col1_x, row_y, 18, label_color);
DrawTextEx(*font, "Kills:", (Vector2){col1_x, row_y}, 18, NORM_CHAR_SPACE, label_color);
snprintf(line, sizeof(line), "%d", kills);
DrawText(line, col1_x + 80, row_y, 18, value_color);
DrawTextEx(*font, line, (Vector2){col1_x + 80, row_y}, 18, NORM_CHAR_SPACE, value_color);
row_y += line_height;
DrawText("Items:", col1_x, row_y, 18, label_color);
DrawTextEx(*font, "Items:", (Vector2){col1_x, row_y}, 18, NORM_CHAR_SPACE, label_color);
snprintf(line, sizeof(line), "%d", items);
DrawText(line, col1_x + 80, row_y, 18, value_color);
DrawTextEx(*font, line, (Vector2){col1_x + 80, row_y}, 18, NORM_CHAR_SPACE, value_color);
row_y += line_height;
DrawText("Damage Dealt:", col1_x, row_y, 18, label_color);
DrawTextEx(*font, "Damage Dealt:", (Vector2){col1_x, row_y}, 18, NORM_CHAR_SPACE, label_color);
snprintf(line, sizeof(line), "%d", damage_dealt);
DrawText(line, col1_x + 140, row_y, 18, value_color);
DrawTextEx(*font, line, (Vector2){col1_x + 140, row_y}, 18, NORM_CHAR_SPACE, value_color);
row_y += line_height;
DrawText("Damage Taken:", col1_x, row_y, 18, label_color);
DrawTextEx(*font, "Damage Taken:", (Vector2){col1_x, row_y}, 18, NORM_CHAR_SPACE, label_color);
snprintf(line, sizeof(line), "%d", damage_taken);
DrawText(line, col1_x + 140, row_y, 18, value_color);
DrawTextEx(*font, line, (Vector2){col1_x + 140, row_y}, 18, NORM_CHAR_SPACE, value_color);
row_y += line_height;
DrawText("Crits:", col1_x, row_y, 18, label_color);
DrawTextEx(*font, "Crits:", (Vector2){col1_x, row_y}, 18, NORM_CHAR_SPACE, label_color);
snprintf(line, sizeof(line), "%d", crits);
DrawText(line, col1_x + 80, row_y, 18, value_color);
DrawTextEx(*font, line, (Vector2){col1_x + 80, row_y}, 18, NORM_CHAR_SPACE, value_color);
row_y += line_height;
DrawText("Times Hit:", col1_x, row_y, 18, label_color);
DrawTextEx(*font, "Times Hit:", (Vector2){col1_x, row_y}, 18, NORM_CHAR_SPACE, label_color);
snprintf(line, sizeof(line), "%d", times_hit);
DrawText(line, col1_x + 80, row_y, 18, value_color);
DrawTextEx(*font, line, (Vector2){col1_x + 80, row_y}, 18, NORM_CHAR_SPACE, value_color);
row_y += line_height;
// Column 2
int col2_row_y = box_y + 20;
DrawText("Potions:", col2_x, col2_row_y, 18, label_color);
DrawTextEx(*font, "Potions:", (Vector2){col2_x, col2_row_y}, 18, NORM_CHAR_SPACE, label_color);
snprintf(line, sizeof(line), "%d", potions);
DrawText(line, col2_x + 80, col2_row_y, 18, value_color);
DrawTextEx(*font, line, (Vector2){col2_x + 80, col2_row_y}, 18, NORM_CHAR_SPACE, value_color);
col2_row_y += line_height;
DrawText("Floors:", col2_x, col2_row_y, 18, label_color);
DrawTextEx(*font, "Floors:", (Vector2){col2_x, col2_row_y}, 18, NORM_CHAR_SPACE, label_color);
snprintf(line, sizeof(line), "%d", floors);
DrawText(line, col2_x + 80, col2_row_y, 18, value_color);
DrawTextEx(*font, line, (Vector2){col2_x + 80, col2_row_y}, 18, NORM_CHAR_SPACE, value_color);
col2_row_y += line_height;
DrawText("Turns:", col2_x, col2_row_y, 18, label_color);
DrawTextEx(*font, "Turns:", (Vector2){col2_x, col2_row_y}, 18, NORM_CHAR_SPACE, label_color);
snprintf(line, sizeof(line), "%d", turns);
DrawText(line, col2_x + 80, col2_row_y, 18, value_color);
DrawTextEx(*font, line, (Vector2){col2_x + 80, col2_row_y}, 18, NORM_CHAR_SPACE, value_color);
col2_row_y += line_height;
// Score: placed below the last row of the longer column (6 items, row_y is already there)
row_y += 10;
DrawText("SCORE:", col1_x, row_y, 22, GOLD);
DrawTextEx(*font, "SCORE:", (Vector2){col1_x, row_y}, 22, NORM_CHAR_SPACE, GOLD);
snprintf(line, sizeof(line), "%d", score);
DrawText(line, col1_x + 90, row_y, 22, GOLD);
DrawTextEx(*font, line, (Vector2){col1_x + 90, col2_row_y}, 22, NORM_CHAR_SPACE, GOLD);
row_y += 35;
// Seed display
DrawTextEx(*font, "SEED:", (Vector2){col1_x, row_y}, 18, SMALL_CHAR_SPACE, label_color);
snprintf(line, sizeof(line), "%u", seed);
DrawTextEx(*font, line, (Vector2){col1_x + 60, row_y}, 18, SMALL_CHAR_SPACE, END_SEED);
// Instructions
if (is_victory) {
const char *subtitle = "Press R to play again or Q to quit";
int sub_width = MeasureText(subtitle, 20);
DrawText(subtitle, (SCREEN_WIDTH - sub_width) / 2, SCREEN_HEIGHT - 50, 20, LIGHTGRAY);
DrawTextEx(*font, subtitle, (Vector2){(SCREEN_WIDTH - sub_width) / 2.0f, SCREEN_HEIGHT - 50}, 20, NORM_CHAR_SPACE,
LIGHTGRAY);
} else {
const char *subtitle = "Press R to restart or Q to quit";
int sub_width = MeasureText(subtitle, 20);
DrawText(subtitle, (SCREEN_WIDTH - sub_width) / 2, SCREEN_HEIGHT - 50, 20, LIGHTGRAY);
DrawTextEx(*font, subtitle, (Vector2){(SCREEN_WIDTH - sub_width) / 2.0f, SCREEN_HEIGHT - 50}, 20, NORM_CHAR_SPACE,
LIGHTGRAY);
}
}
void render_message(const char *message) {
void render_message(const char *message, Font *font) {
if (message == NULL)
return;
@ -652,5 +688,20 @@ void render_message(const char *message) {
text_y = (int)box_y + padding_y;
}
DrawText(message, text_x, text_y, font_size, WHITE);
DrawTextEx(*font, message, (Vector2){text_x, text_y}, font_size, NORM_CHAR_SPACE, WHITE);
}
void render_seed_display(unsigned int seed) {
char seed_text[64];
snprintf(seed_text, sizeof(seed_text), "Seed: %u", seed);
const int font_size = 14;
int text_width = MeasureText(seed_text, font_size);
// Position at top right with padding
int x = SCREEN_WIDTH - text_width - 10;
int y = 5;
// Draw with non-obstructive dim text color
DrawText(seed_text, x, y, font_size, TEXT_DIM);
}

View file

@ -1,3 +1,4 @@
#ifndef RENDER_H
#define RENDER_H
@ -68,6 +69,7 @@
#define END_OVERLAY (Color){0, 0, 0, 210}
#define END_BOX_BG (Color){20, 20, 20, 240}
#define END_BOX_BORDER (Color){100, 100, 100, 255}
#define END_SEED (Color){150, 200, 255, 255}
// Portrait placeholder
// FIXME: remove when player sprites are available
@ -80,28 +82,31 @@ void render_map(const Map *map);
void render_player(const Player *p);
// Render all enemies
void render_enemies(const Enemy *enemies, int count);
void render_enemies(const Enemy *enemies, int count, const unsigned char visible[MAP_HEIGHT][MAP_WIDTH]);
// Render all items
void render_items(const Item *items, int count);
void render_items(const Item *items, int count, const unsigned char visible[MAP_HEIGHT][MAP_WIDTH]);
// Render UI overlay
void render_ui(const Player *p);
void render_ui(const Player *p, Font *font);
// Render action log (bottom left corner)
void render_action_log(const char log[5][128], int count, int head);
void render_action_log(const char log[5][128], int count, int head, Font *font);
// Render inventory selection overlay
void render_inventory_overlay(const Player *p, int selected);
void render_inventory_overlay(const Player *p, int selected, Font *font);
// Render floating damage text
void render_floating_texts(FloatingText *texts, int count, int shake_x, int shake_y);
// Render end screen (victory or death) with stats breakdown
void render_end_screen(int is_victory, int kills, int items, int damage_dealt, int damage_taken, int crits,
int times_hit, int potions, int floors, int turns, int score);
int times_hit, int potions, int floors, int turns, int score, unsigned int seed, Font *font);
// Render a message popup
void render_message(const char *message);
void render_message(const char *message, Font *font);
// Render seed display at top right of screen
void render_seed_display(unsigned int seed);
#endif // RENDER_H

View file

@ -8,6 +8,21 @@
#define SCREEN_WIDTH (MAP_WIDTH * TILE_SIZE)
#define SCREEN_HEIGHT (MAP_HEIGHT * TILE_SIZE)
// Font constants
#define NORM_CHAR_SPACE 4.0f
#define MED_CHAR_SPACE 2.5f
#define SMALL_CHAR_SPACE 1.6f
#define NAR_CHAR_SPACE 1.0f
#define CRAMPED_CHAR_SPACE 0.5f
#define TINY_FONT 8
#define SMALL_FONT 10
#define NORM_FONT 12
#define MEDIUM_FONT 14
#define LARGE_FONT 18
#define BIG_FONT 22
#define HUGE_FONT 30
// Game Limits
#define MAX_ENEMIES 64
#define MAX_ITEMS 128
@ -63,4 +78,9 @@
// Message timer
#define MESSAGE_TIMER_DURATION 60
// Visibility / Fog of War
#define PLAYER_VIEW_RANGE 8
#define ENEMY_VIEW_RANGE 6
#define ENEMY_PATROL_MOVE_CHANCE 30
#endif // SETTINGS_H