1
0
Fork 0
forked from NotAShelf/rogged

Compare commits

..

No commits in common. "09b46e4a32012a9621e42034d4da73693a8ab98f" and "eed5c3aff30232c786a5cff24fe81c59a4e7ae53" have entirely different histories.

17 changed files with 220 additions and 610 deletions

View file

@ -1,13 +1,11 @@
# Rogged # Rogged
Turn-based, infinite roguelike dungeon crawler built with C99 and with a dash of Turn-based roguelike dungeon crawler, built in C99 and with a dash of Zig to
Zig. Meant to serve primarily, but not exclusively, as a learning opportunity. serve as a learning opportunity. Rogged is basically a classic roguelike where
Rogged is basically a classic roguelike where you descend through floors of a you descend through floors of a procedurally generated dungeon, fighting
procedurally generated dungeon, fighting enemies, managing inventory, and trying enemies, managing inventory, and trying to reach the bottom alive.
to reach the bottom alive or die chasing a highscore!
The game itself, be it the code or mechanics, is _heavily_ in development. For A non-exhaustive list of its (current) features:
now, a non-exhaustive list of its (current) features are as follows:
- Turn-based combat with damage variance, critical hits, dodge, and block - Turn-based combat with damage variance, critical hits, dodge, and block
mechanics mechanics
@ -20,27 +18,33 @@ now, a non-exhaustive list of its (current) features are as follows:
- Procedural audio via raylib - Procedural audio via raylib
- ASCII-inspired tile rendering, with HP bars and floating damage text - ASCII-inspired tile rendering, with HP bars and floating damage text
There are still some features lacking polish, or lacking _any_ kind of attention **Controls:**
to be viable. For a semi-complete list of things that need to be done, see the
[future plans section](#future-plans). | 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 ## Build Instructions
Rogged is built on a relatively simple stack. It uses C99 for the main game Rogged is built with C99 and Zig. Besides `raylib` and `pkg-config` you will
logic, and Zig for the combat library. Besides `raylib` and `pkg-config`, you need a C compiler and basic Zig tooling. For now, we use C99 and Zig 0.15.2.
only need the core Zig tooling. For now the required Zig version is 0.15.2, but Those might change in the future.
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 Additionally you will need `clang-format` and `just` for the developer workflow
building, Zig is enough. if you plan to contribute.
### Using Nix (Recommended) ### Using Nix (Recommended)
The _recommended_ way of developing this project is using The recommended developer tooling is [Nix](https://nixos.org). This provides a
[Nix](https://nixos.org) and relying on devshells for pure, reproducible pure, reproducible devshell across all machines.
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 ```sh
# Enter the development shell # Enter the development shell
@ -52,9 +56,6 @@ $ just dev
### Manual Build ### 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 ```sh
# Full build # Full build
$ zig build $ zig build
@ -66,8 +67,6 @@ $ zig build run
$ just dev $ just dev
``` ```
The Justfile provides commands that work across both methods.
### Task Runner Commands ### Task Runner Commands
There's a `Justfile` designed to make common tasks somewhat easier. For now, There's a `Justfile` designed to make common tasks somewhat easier. For now,
@ -85,14 +84,10 @@ If the project gets more complicated, new tasks might be added.
## Future Plans ## Future Plans
The game is currently **playable end-to-end**, but it lacks a fair bit of polish The game is currently **playable end-to-end** but it lacks _serious_ polish to
to claim its place as a fun, engaging roguelike you can just boot up and play. claim its place as a fun roguelike. Some of the features I'd like to introduce,
Some of the features that are planned for the future, in no particular order, in no particular order, are as follows:
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 - [ ] **Save / Load system** - Persist and restore game state between sessions
- [ ] **More enemy variety** - Additional enemy types with unique abilities - [ ] **More enemy variety** - Additional enemy types with unique abilities
- [ ] **More item variety** - Rings, wands, scrolls, and cursed items - [ ] **More item variety** - Rings, wands, scrolls, and cursed items
@ -105,9 +100,8 @@ are as follows:
- [ ] **UI polish** - Better message log history, item descriptions, death - [ ] **UI polish** - Better message log history, item descriptions, death
screen screen
Later down the line it might be an interesting choice to provide a scripting In addition, it might be interesting to allow customizing the "world state" by
API, likely with Lua, to allow customizing the game state and events. Though, as scripting API. Though, that is for much later.
that is for much later.
## Attributions ## Attributions
@ -124,6 +118,4 @@ 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 This was perhaps my best experience in developing a graphical application, and
CERTAINLY the most ergonomic when it comes to writing a game. 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.

View file

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

View file

@ -4,10 +4,6 @@
#include "settings.h" #include "settings.h"
#include <raylib.h> #include <raylib.h>
typedef struct {
int x, y;
} Vec2;
// Tile types // Tile types
typedef enum { TILE_WALL, TILE_FLOOR, TILE_STAIRS } TileType; typedef enum { TILE_WALL, TILE_FLOOR, TILE_STAIRS } TileType;
@ -34,8 +30,6 @@ typedef struct {
TileType tiles[MAP_HEIGHT][MAP_WIDTH]; TileType tiles[MAP_HEIGHT][MAP_WIDTH];
Room rooms[MAX_ROOMS]; Room rooms[MAX_ROOMS];
int room_count; int room_count;
unsigned char visible[MAP_HEIGHT][MAP_WIDTH];
unsigned char remembered[MAP_HEIGHT][MAP_WIDTH];
} Map; } Map;
// Dungeon // Dungeon
@ -63,7 +57,7 @@ typedef struct {
// Player // Player
typedef struct { typedef struct {
Vec2 position; int x, y;
int hp, max_hp; int hp, max_hp;
int attack; int attack;
int defense; int defense;
@ -89,7 +83,7 @@ typedef enum { ENEMY_GOBLIN, ENEMY_SKELETON, ENEMY_ORC } EnemyType;
// Enemy // Enemy
typedef struct { typedef struct {
Vec2 position; int x, y;
int hp; int hp;
int max_hp; int max_hp;
int attack; int attack;
@ -104,11 +98,6 @@ typedef struct {
int status_chance; int status_chance;
int crit_chance; // crit chance percentage (0-100) int crit_chance; // crit chance percentage (0-100)
int crit_mult; // crit damage multiplier percentage (e.g. 150 = 1.5x) 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 // status effects
StatusEffect effects[MAX_EFFECTS]; StatusEffect effects[MAX_EFFECTS];
int effect_count; int effect_count;
@ -173,8 +162,6 @@ typedef struct {
int potions_used; int potions_used;
int floors_reached; int floors_reached;
int final_score; int final_score;
// Seed for this run
unsigned int run_seed;
} GameState; } GameState;

View file

@ -2,9 +2,7 @@
#include "combat.h" #include "combat.h"
#include "common.h" #include "common.h"
#include "map.h" #include "map.h"
#include "movement.h"
#include "rng.h" #include "rng.h"
#include "settings.h"
#include <string.h> #include <string.h>
// Forward declaration // Forward declaration
@ -31,7 +29,7 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) {
get_random_floor_tile(map, &ex, &ey, 50); get_random_floor_tile(map, &ex, &ey, 50);
// Don't spawn on player position // Don't spawn on player position
if (ex == p->position.x && ey == p->position.y) { if (ex == p->x && ey == p->y) {
continue; continue;
} }
@ -43,8 +41,8 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) {
// Create enemy // Create enemy
Enemy e; Enemy e;
memset(&e, 0, sizeof(Enemy)); memset(&e, 0, sizeof(Enemy));
e.position.x = ex; e.x = ex;
e.position.y = ey; e.y = ey;
e.alive = 1; e.alive = 1;
e.type = rng_int(ENEMY_GOBLIN, max_type); e.type = rng_int(ENEMY_GOBLIN, max_type);
e.effect_count = 0; e.effect_count = 0;
@ -70,7 +68,6 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) {
e.resistance[DMG_PIERCE] = 0; e.resistance[DMG_PIERCE] = 0;
e.resistance[DMG_FIRE] = -25; e.resistance[DMG_FIRE] = -25;
e.resistance[DMG_POISON] = 50; e.resistance[DMG_POISON] = 50;
e.vision_range = 7;
break; break;
case ENEMY_SKELETON: case ENEMY_SKELETON:
e.max_hp = ENEMY_BASE_HP + floor + 2; e.max_hp = ENEMY_BASE_HP + floor + 2;
@ -88,7 +85,6 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) {
e.resistance[DMG_PIERCE] = 50; e.resistance[DMG_PIERCE] = 50;
e.resistance[DMG_FIRE] = 25; e.resistance[DMG_FIRE] = 25;
e.resistance[DMG_POISON] = 75; e.resistance[DMG_POISON] = 75;
e.vision_range = 6;
break; break;
case ENEMY_ORC: case ENEMY_ORC:
e.max_hp = ENEMY_BASE_HP + floor + 4; e.max_hp = ENEMY_BASE_HP + floor + 4;
@ -106,7 +102,6 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) {
e.resistance[DMG_PIERCE] = -25; e.resistance[DMG_PIERCE] = -25;
e.resistance[DMG_FIRE] = 0; e.resistance[DMG_FIRE] = 0;
e.resistance[DMG_POISON] = 0; e.resistance[DMG_POISON] = 0;
e.vision_range = 5;
break; break;
default: default:
e.max_hp = ENEMY_BASE_HP; e.max_hp = ENEMY_BASE_HP;
@ -120,7 +115,6 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) {
e.crit_chance = ENEMY_CRIT_CHANCE; e.crit_chance = ENEMY_CRIT_CHANCE;
e.crit_mult = ENEMY_CRIT_MULT; e.crit_mult = ENEMY_CRIT_MULT;
memset(e.resistance, 0, sizeof(e.resistance)); memset(e.resistance, 0, sizeof(e.resistance));
e.vision_range = ENEMY_VIEW_RANGE;
break; break;
} }
e.cooldown = e.speed; e.cooldown = e.speed;
@ -133,129 +127,58 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) {
// Check if position has an enemy // Check if position has an enemy
int is_enemy_at(const Enemy *enemies, int count, int x, int y) { int is_enemy_at(const Enemy *enemies, int count, int x, int y) {
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
if (enemies[i].alive && enemies[i].position.x == x && enemies[i].position.y == y) { if (enemies[i].alive && enemies[i].x == x && enemies[i].y == y) {
return 1; return 1;
} }
} }
return 0; return 0;
} }
// Check if enemy can see player (within view range and line of sight) // Check if enemy can see player (adjacent)
static int can_see_player(Enemy *e, Player *p, Map *map) { static int can_see_player(Enemy *e, Player *p) {
return can_see_entity(map, e->position.x, e->position.y, p->position.x, p->position.y, e->vision_range); int dx = p->x - e->x;
int dy = p->y - e->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 // Move enemy toward player
static void enemy_move_toward_player(Enemy *e, Player *p, Map *map, Enemy *all_enemies, int enemy_count) { static void enemy_move_toward_player(Enemy *e, Player *p, Map *map, Enemy *all_enemies, int enemy_count) {
int dx = 0, dy = 0; int dx = 0, dy = 0;
if (p->position.x > e->position.x) if (p->x > e->x)
dx = 1; dx = 1;
else if (p->position.x < e->position.x) else if (p->x < e->x)
dx = -1; dx = -1;
if (p->position.y > e->position.y) if (p->y > e->y)
dy = 1; dy = 1;
else if (p->position.y < e->position.y) else if (p->y < e->y)
dy = -1; dy = -1;
Vec2 dir = {dx, 0}; // Try horizontal first, then vertical
if (dx != 0) { int new_x = e->x + dx;
MoveResult r = try_move_entity(&e->position, dir, map, p, all_enemies, enemy_count, false); int new_y = e->y;
if (r == MOVE_RESULT_MOVED)
return;
}
dir.x = 0; if (dx != 0 && is_floor(map, new_x, new_y) && !is_enemy_at(all_enemies, enemy_count, new_x, new_y) &&
dir.y = dy; !is_player_at(p, new_x, new_y)) {
if (dy != 0) { e->x = new_x;
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) { } else if (dy != 0) {
new_x = e->position.x; new_x = e->x;
new_y = e->position.y + dy; new_y = e->y + dy;
if (is_floor(map, new_x, new_y) && !is_enemy_at(all_enemies, enemy_count, new_x, new_y)) { if (is_floor(map, new_x, new_y) && !is_enemy_at(all_enemies, enemy_count, new_x, new_y) &&
e->position.x = new_x; !is_player_at(p, new_x, new_y)) {
e->position.y = new_y; e->x = new_x;
} e->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 visible, otherwise patrol or search) // Perform a single action for an enemy (attack if adjacent, otherwise move)
void enemy_act(Enemy *e, Player *p, Map *map, Enemy *all_enemies, int enemy_count) { void enemy_act(Enemy *e, Player *p, Map *map, Enemy *all_enemies, int enemy_count) {
if (!e->alive) if (!e->alive)
return; return;
@ -264,37 +187,14 @@ 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)) if (combat_has_effect(e->effects, e->effect_count, EFFECT_STUN))
return; return;
int can_see = can_see_player(e, p, map); // Check if adjacent to player - attack
if (can_see_player(e, p)) {
// 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); combat_enemy_attack(e, p);
propagate_alert(e, all_enemies, enemy_count);
return; return;
} }
// Move toward player if visible // Otherwise, move toward player
if (can_see) { enemy_move_toward_player(e, p, map, all_enemies, enemy_count);
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) { void enemy_update_all(Enemy enemies[], int count, Player *p, Map *map) {

View file

@ -4,18 +4,14 @@
#include "enemy.h" #include "enemy.h"
#include "items.h" #include "items.h"
#include "map.h" #include "map.h"
#include "movement.h"
#include "player.h" #include "player.h"
#include "raylib.h" #include "raylib.h"
#include "render.h" #include "render.h"
#include "rng.h" #include "rng.h"
#include "settings.h" #include "settings.h"
#include <ctype.h>
#include <stddef.h> #include <stddef.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include <time.h>
// Add message to action log // Add message to action log
static void add_log(GameState *gs, const char *msg) { static void add_log(GameState *gs, const char *msg) {
@ -105,14 +101,11 @@ static void update_effects(GameState *gs) {
// Initialize a new floor // Initialize a new floor
static void init_floor(GameState *gs, int floor_num) { 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 // Generate dungeon
dungeon_generate(&gs->dungeon, &gs->map, floor_num); dungeon_generate(&gs->dungeon, &gs->map, floor_num);
// Seed rng for this floor's content // Seed rng for this floor's content
rng_seed(gs->run_seed + floor_num * 98765); rng_seed(floor_num * 54321);
// Find spawn position // Find spawn position
int start_x, start_y; int start_x, start_y;
@ -124,14 +117,11 @@ static void init_floor(GameState *gs, int floor_num) {
gs->floors_reached = 1; gs->floors_reached = 1;
} else { } else {
// Move player to new floor position // Move player to new floor position
gs->player.position.x = start_x; gs->player.x = start_x;
gs->player.position.y = start_y; gs->player.y = start_y;
} }
gs->player.floor = floor_num; gs->player.floor = floor_num;
// Calculate initial visibility
calculate_visibility(&gs->map, gs->player.position.x, gs->player.position.y);
// Spawn enemies // Spawn enemies
enemy_spawn(gs->enemies, &gs->enemy_count, &gs->map, &gs->player, floor_num); enemy_spawn(gs->enemies, &gs->enemy_count, &gs->map, &gs->player, floor_num);
@ -147,8 +137,7 @@ static void tick_all_effects(GameState *gs) {
// Player effects // Player effects
int player_effect_dmg = combat_tick_effects(&gs->player); int player_effect_dmg = combat_tick_effects(&gs->player);
if (player_effect_dmg > 0) { if (player_effect_dmg > 0) {
spawn_floating_text(gs, gs->player.position.x * TILE_SIZE + 8, gs->player.position.y * TILE_SIZE, player_effect_dmg, spawn_floating_text(gs, gs->player.x * TILE_SIZE + 8, gs->player.y * TILE_SIZE, player_effect_dmg, 0);
0);
gs->screen_shake = SHAKE_EFFECT_DURATION; gs->screen_shake = SHAKE_EFFECT_DURATION;
} }
@ -166,7 +155,7 @@ static void tick_all_effects(GameState *gs) {
continue; continue;
int enemy_effect_dmg = combat_tick_enemy_effects(e); int enemy_effect_dmg = combat_tick_enemy_effects(e);
if (enemy_effect_dmg > 0) { if (enemy_effect_dmg > 0) {
spawn_floating_text(gs, e->position.x * TILE_SIZE + 8, e->position.y * TILE_SIZE, enemy_effect_dmg, 0); spawn_floating_text(gs, e->x * TILE_SIZE + 8, e->y * TILE_SIZE, enemy_effect_dmg, 0);
} }
if (!e->alive) { if (!e->alive) {
add_log(gs, "Enemy died from effects!"); add_log(gs, "Enemy died from effects!");
@ -184,7 +173,7 @@ static void post_action(GameState *gs, Enemy *attacked_enemy) {
return; return;
// Check if stepped on stairs // Check if stepped on stairs
if (gs->map.tiles[gs->player.position.y][gs->player.position.x] == TILE_STAIRS) { if (gs->map.tiles[gs->player.y][gs->player.x] == TILE_STAIRS) {
gs->awaiting_descend = 1; gs->awaiting_descend = 1;
gs->last_message = "Descend to next floor? (Y/N)"; gs->last_message = "Descend to next floor? (Y/N)";
gs->message_timer = 120; gs->message_timer = 120;
@ -193,8 +182,8 @@ static void post_action(GameState *gs, Enemy *attacked_enemy) {
// combat feedback - player attacked an enemy this turn // combat feedback - player attacked an enemy this turn
if (attacked_enemy != NULL) { if (attacked_enemy != NULL) {
int ex = attacked_enemy->position.x * TILE_SIZE + 8; int ex = attacked_enemy->x * TILE_SIZE + 8;
int ey = attacked_enemy->position.y * TILE_SIZE; int ey = attacked_enemy->y * TILE_SIZE;
if (combat_was_dodged()) { if (combat_was_dodged()) {
spawn_floating_label(gs, ex, ey, LABEL_DODGE, EFFECT_NONE); spawn_floating_label(gs, ex, ey, LABEL_DODGE, EFFECT_NONE);
@ -226,9 +215,6 @@ 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 turns - uses speed/cooldown system
enemy_update_all(gs->enemies, gs->enemy_count, &gs->player, &gs->map); enemy_update_all(gs->enemies, gs->enemy_count, &gs->player, &gs->map);
@ -238,8 +224,8 @@ static void post_action(GameState *gs, Enemy *attacked_enemy) {
gs->screen_shake = SHAKE_PLAYER_DAMAGE_DURATION; gs->screen_shake = SHAKE_PLAYER_DAMAGE_DURATION;
gs->damage_taken += combat_get_last_damage(); gs->damage_taken += combat_get_last_damage();
gs->times_hit++; gs->times_hit++;
spawn_floating_text(gs, gs->player.position.x * TILE_SIZE + 8, gs->player.position.y * TILE_SIZE, spawn_floating_text(gs, gs->player.x * TILE_SIZE + 8, gs->player.y * TILE_SIZE, combat_get_last_damage(),
combat_get_last_damage(), combat_was_critical()); combat_was_critical());
} }
// Set message and check game over // Set message and check game over
@ -260,7 +246,6 @@ static int handle_stun_turn(GameState *gs) {
if (gs->game_over) if (gs->game_over)
return 1; return 1;
enemy_update_all(gs->enemies, gs->enemy_count, &gs->player, &gs->map); 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) if (gs->player.hp <= 0)
gs->game_over = 1; gs->game_over = 1;
gs->last_message = "You are stunned!"; gs->last_message = "You are stunned!";
@ -403,7 +388,7 @@ static int handle_movement_input(GameState *gs) {
// Check for manual item pickup (G key) // Check for manual item pickup (G key)
if (IsKeyPressed(KEY_G)) { if (IsKeyPressed(KEY_G)) {
Item *item = get_item_at_floor(gs->items, gs->item_count, gs->player.position.x, gs->player.position.y); Item *item = get_item_at_floor(gs->items, gs->item_count, gs->player.x, gs->player.y);
if (item != NULL) { if (item != NULL) {
if (player_pickup(&gs->player, item)) { if (player_pickup(&gs->player, item)) {
gs->items_collected++; gs->items_collected++;
@ -432,45 +417,38 @@ static int handle_movement_input(GameState *gs) {
} }
} }
// Movement: use IsKeyDown for held-key repeat
Vec2 direction = {0, 0}; int dx = 0, dy = 0;
if (IsKeyDown(KEY_W) || IsKeyDown(KEY_UP)) if (IsKeyDown(KEY_W) || IsKeyDown(KEY_UP))
direction.y = -1; dy = -1;
else if (IsKeyDown(KEY_S) || IsKeyDown(KEY_DOWN)) else if (IsKeyDown(KEY_S) || IsKeyDown(KEY_DOWN))
direction.y = 1; dy = 1;
else if (IsKeyDown(KEY_A) || IsKeyDown(KEY_LEFT)) else if (IsKeyDown(KEY_A) || IsKeyDown(KEY_LEFT))
direction.x = -1; dx = -1;
else if (IsKeyDown(KEY_D) || IsKeyDown(KEY_RIGHT)) else if (IsKeyDown(KEY_D) || IsKeyDown(KEY_RIGHT))
direction.x = 1; dx = 1;
if (direction.x == 0 && direction.y == 0) if (dx == 0 && dy == 0)
return 0; return 0;
// Reset combat event before player acts // Reset combat event before player acts
combat_reset_event(); combat_reset_event();
int new_x = gs->player.x + dx;
int new_x = gs->player.position.x + direction.x; int new_y = gs->player.y + dy;
int new_y = gs->player.position.y + direction.y;
Enemy *target = NULL;
int action = 0; int action = 0;
MoveResult result = // Attack enemy at target tile, or move into it
try_move_entity(&gs->player.position, direction, &gs->map, &gs->player, gs->enemies, gs->enemy_count, true); Enemy *target = player_find_enemy_at(gs->enemies, gs->enemy_count, new_x, new_y);
if (result == MOVE_RESULT_MOVED) { if (target != NULL) {
player_on_move(&gs->player); player_attack(&gs->player, target);
action = 1; action = 1;
} else if (result == MOVE_RESULT_BLOCKED_ENEMY) { } else {
target = player_find_enemy_at(gs->enemies, gs->enemy_count, new_x, new_y); action = player_move(&gs->player, dx, dy, &gs->map);
if (target != NULL) {
player_attack(&gs->player, target);
action = 1;
}
} }
if (action) if (action)
post_action(gs, target); post_action(gs, target); // target is NULL on move, enemy ptr on attack
return action; return action;
} }
@ -484,13 +462,7 @@ static int handle_input(GameState *gs) {
// Check for restart (works during game over) // Check for restart (works during game over)
if (IsKeyPressed(KEY_R) && gs->game_over) { if (IsKeyPressed(KEY_R) && gs->game_over) {
memset(gs, 0, sizeof(GameState)); 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); 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; return 0;
} }
@ -520,19 +492,12 @@ void load_audio_assets(GameState *gs) {
} }
// Main game loop // Main game loop
static void game_loop(unsigned int run_seed) { static void game_loop(void) {
GameState gs; GameState gs;
memset(&gs, 0, sizeof(GameState)); 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); load_audio_assets(&gs);
// font
Font fontTTF = LoadFontEx("./assets/fonts/Tomorrow_Night.ttf", 24, NULL, 0);
// Initialize first floor // Initialize first floor
rng_seed(12345);
init_floor(&gs, 1); init_floor(&gs, 1);
// Disable esc to exit // Disable esc to exit
@ -552,16 +517,10 @@ static void game_loop(unsigned int run_seed) {
break; break;
if (IsKeyPressed(KEY_R)) { if (IsKeyPressed(KEY_R)) {
memset(&gs, 0, sizeof(GameState)); 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_over = 0;
gs.game_won = 0; gs.game_won = 0;
load_audio_assets(&gs); load_audio_assets(&gs);
init_floor(&gs, 1); 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);
} }
} }
@ -582,31 +541,28 @@ static void game_loop(unsigned int run_seed) {
cam.offset = (Vector2){(float)gs.shake_x, (float)gs.shake_y}; cam.offset = (Vector2){(float)gs.shake_x, (float)gs.shake_y};
BeginMode2D(cam); BeginMode2D(cam);
render_map(&gs.map); render_map(&gs.map);
render_items(gs.items, gs.item_count, gs.map.visible); render_items(gs.items, gs.item_count);
render_enemies(gs.enemies, gs.enemy_count, gs.map.visible); render_enemies(gs.enemies, gs.enemy_count);
render_player(&gs.player); render_player(&gs.player);
EndMode2D(); EndMode2D();
// Floating texts follow world shake // Floating texts follow world shake
render_floating_texts(gs.floating_texts, gs.floating_count, gs.shake_x, gs.shake_y); render_floating_texts(gs.floating_texts, gs.floating_count, gs.shake_x, gs.shake_y);
render_ui(&gs.player, &fontTTF); render_ui(&gs.player);
// Draw action log // Draw action log
render_action_log(gs.action_log, gs.log_count, gs.log_head, &fontTTF); render_action_log(gs.action_log, gs.log_count, gs.log_head);
// Draw inventory overlay if active // Draw inventory overlay if active
if (gs.show_inventory) { if (gs.show_inventory) {
render_inventory_overlay(&gs.player, gs.inv_selected, &fontTTF); render_inventory_overlay(&gs.player, gs.inv_selected);
} }
// Draw message if any // Draw message if any
if (gs.last_message != NULL && gs.message_timer > 0) { if (gs.last_message != NULL && gs.message_timer > 0) {
render_message(gs.last_message, &fontTTF); render_message(gs.last_message);
} }
// Draw persistent seed display in top right
render_seed_display(gs.run_seed);
// Draw game over screen // Draw game over screen
if (gs.game_over) { if (gs.game_over) {
// Compute final score // Compute final score
@ -617,7 +573,7 @@ static void game_loop(unsigned int run_seed) {
} }
render_end_screen(gs.game_won, gs.total_kills, gs.items_collected, gs.damage_dealt, gs.damage_taken, 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.crits_landed, gs.times_hit, gs.potions_used, gs.floors_reached, gs.turn_count,
gs.final_score, gs.run_seed, &fontTTF); gs.final_score);
} }
EndDrawing(); EndDrawing();
@ -627,67 +583,17 @@ static void game_loop(unsigned int run_seed) {
} }
} }
// Check if a string is a valid unsigned integer int main(void) {
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 // Initialize audio
audio_init(); audio_init();
// Initialize random number generator // Initialize random number generator
SetRandomSeed(88435); SetRandomSeed(88435);
// Initialize window with seed in title // Initialize window
char title[128]; InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT + 60, "Roguelike");
snprintf(title, sizeof(title), "Roguelike - Seed: %u", run_seed);
InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT + 60, title);
SetTargetFPS(60); SetTargetFPS(60);
// Run game // Run game
game_loop(run_seed); game_loop();
// Cleanup // Cleanup
CloseWindow(); CloseWindow();

View file

@ -1,8 +1,6 @@
#include "map.h" #include "map.h"
#include "rng.h" #include "rng.h"
#include "settings.h"
#include "utils.h" #include "utils.h"
#include <stdlib.h>
#include <string.h> #include <string.h>
void map_init(Map *map) { void map_init(Map *map) {
@ -12,8 +10,6 @@ void map_init(Map *map) {
map->tiles[y][x] = TILE_WALL; map->tiles[y][x] = TILE_WALL;
} }
} }
memset(map->visible, 0, sizeof(map->visible));
memset(map->remembered, 0, sizeof(map->remembered));
map->room_count = 0; map->room_count = 0;
} }
@ -170,6 +166,9 @@ void get_random_floor_tile(Map *map, int *x, int *y, int attempts) {
} }
void dungeon_generate(Dungeon *d, Map *map, int floor_num) { 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 // Initialize map to all walls
map_init(map); map_init(map);
@ -187,67 +186,3 @@ void dungeon_generate(Dungeon *d, Map *map, int floor_num) {
d->room_count = map->room_count; d->room_count = map->room_count;
memcpy(d->rooms, map->rooms, sizeof(Room) * 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,10 +18,4 @@ void map_init(Map *map);
// Get a random floor tile position // Get a random floor tile position
void get_random_floor_tile(Map *map, int *x, int *y, int attempts); 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 #endif // MAP_H

View file

@ -1,29 +0,0 @@
#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;
}

View file

@ -1,17 +0,0 @@
#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,12 +2,15 @@
#include "combat.h" #include "combat.h"
#include "common.h" #include "common.h"
#include "items.h" #include "items.h"
#include "map.h"
#include "settings.h" #include "settings.h"
#include "utils.h"
#include <stdlib.h>
#include <string.h> #include <string.h>
void player_init(Player *p, int x, int y) { void player_init(Player *p, int x, int y) {
p->position.x = x; p->x = x;
p->position.y = y; p->y = y;
p->hp = PLAYER_BASE_HP; p->hp = PLAYER_BASE_HP;
p->max_hp = PLAYER_BASE_HP; p->max_hp = PLAYER_BASE_HP;
p->attack = PLAYER_BASE_ATTACK; p->attack = PLAYER_BASE_ATTACK;
@ -40,20 +43,36 @@ Enemy *player_find_enemy_at(Enemy *enemies, int count, int x, int y) {
if (count > MAX_ENEMIES) if (count > MAX_ENEMIES)
count = MAX_ENEMIES; count = MAX_ENEMIES;
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
if (enemies[i].alive && enemies[i].position.x == x && enemies[i].position.y == y) if (enemies[i].alive && enemies[i].x == x && enemies[i].y == y)
return &enemies[i]; return &enemies[i];
} }
return NULL; return NULL;
} }
void player_on_move(Player *p) { 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;
p->step_count += 1; p->step_count += 1;
// Regen suppressed while poisoned, bleeding, or burning
if (p->step_count % REGEN_STEP_INTERVAL == 0 && p->hp < p->max_hp && 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_POISON) &&
!combat_has_effect(p->effects, p->effect_count, EFFECT_BLEED) && !combat_has_effect(p->effects, p->effect_count, EFFECT_BLEED) &&
!combat_has_effect(p->effects, p->effect_count, EFFECT_BURN)) { !combat_has_effect(p->effects, p->effect_count, EFFECT_BURN)) {
p->hp += 1; p->hp += 1;
} }
return 1;
} }
void player_attack(Player *p, Enemy *e) { void player_attack(Player *p, Enemy *e) {
@ -209,8 +228,8 @@ int player_drop_item(Player *p, int inv_index, Item *items, int item_count) {
if (items[i].picked_up) { if (items[i].picked_up) {
// Place dropped item at this position // Place dropped item at this position
items[i] = *item; items[i] = *item;
items[i].x = p->position.x; items[i].x = p->x;
items[i].y = p->position.y; items[i].y = p->y;
items[i].picked_up = 0; items[i].picked_up = 0;
// Remove from inventory // Remove from inventory
player_remove_inventory_item(p, inv_index); player_remove_inventory_item(p, inv_index);

View file

@ -6,8 +6,8 @@
// Initialize player at position // Initialize player at position
void player_init(Player *p, int x, int y); void player_init(Player *p, int x, int y);
// Apply status effects, healing, etc // Move player to (x+dx, y+dy); returns 1 if moved, 0 if blocked
void player_on_move(Player *p); int player_move(Player *p, int dx, int dy, Map *map);
// Find a living enemy at tile (x, y); returns NULL if none // 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); Enemy *player_find_enemy_at(Enemy *enemies, int count, int x, int y);

View file

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

View file

@ -8,21 +8,6 @@
#define SCREEN_WIDTH (MAP_WIDTH * TILE_SIZE) #define SCREEN_WIDTH (MAP_WIDTH * TILE_SIZE)
#define SCREEN_HEIGHT (MAP_HEIGHT * 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 // Game Limits
#define MAX_ENEMIES 64 #define MAX_ENEMIES 64
#define MAX_ITEMS 128 #define MAX_ITEMS 128
@ -78,9 +63,4 @@
// Message timer // Message timer
#define MESSAGE_TIMER_DURATION 60 #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 #endif // SETTINGS_H