Compare commits

..

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

22 changed files with 183 additions and 357 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -4,55 +4,8 @@ pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const c_flags = [_][]const u8{
"-std=c99",
"-Wall",
"-Wextra",
"-O2",
};
// RNG library
const rng_lib = b.addLibrary(.{
.name = "rng",
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
.link_libc = true,
}),
});
rng_lib.addCSourceFiles(.{
.files = &[_][]const u8{"libs/rng/rng.c"},
.flags = &c_flags,
});
rng_lib.addIncludePath(b.path("libs/rng"));
// Map library
const map_lib = b.addLibrary(.{
.name = "map",
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
.link_libc = true,
}),
});
map_lib.addCSourceFiles(.{
.files = &[_][]const u8{
"libs/map/map.c",
"libs/map/utils.c",
},
.flags = &c_flags,
});
// map.h includes common.h and settings.h which live in src/
map_lib.addIncludePath(b.path("src"));
// map.c includes rng/rng.h via libs/ root
map_lib.addIncludePath(b.path("libs"));
// utils.h is co-located with map.c
map_lib.addIncludePath(b.path("libs/map"));
// Zig combat library. This must be compiled as an object and linked
// directly to bypassing the archive step, or it yields a corrupt
// archive that forces the user to clear the cache each time.
const combat_obj = b.addObject(.{
// Zig combat library
const combat_lib = b.addLibrary(.{
.name = "combat",
.root_module = b.createModule(.{
.root_source_file = b.path("libs/combat/combat.zig"),
@ -61,20 +14,29 @@ pub fn build(b: *std.Build) void {
.link_libc = true,
}),
});
// common.h and settings.h live in src/; rng.h exposed bare from libs/rng
combat_obj.addIncludePath(b.path("src"));
combat_obj.addIncludePath(b.path("libs/rng"));
combat_lib.addIncludePath(b.path("src"));
combat_lib.linkSystemLibrary("raylib");
// C sources remaining in src/
// C sources (everything except combat, which is now Zig)
const c_sources = [_][]const u8{
"src/audio.c",
"src/enemy.c",
"src/items.c",
"src/main.c",
"src/movement.c",
"src/map.c",
"src/player.c",
"src/movement.c",
"src/render.c",
"src/rng.c",
"src/settings.c",
"src/utils.c",
};
const c_flags = [_][]const u8{
"-std=c99",
"-Wall",
"-Wextra",
"-O2",
};
// Main executable
@ -92,13 +54,8 @@ pub fn build(b: *std.Build) void {
.flags = &c_flags,
});
// src/ for own headers; libs/ so "rng/rng.h" and "map/map.h" resolve
exe.addIncludePath(b.path("src"));
exe.addIncludePath(b.path("libs"));
exe.linkLibrary(rng_lib);
exe.linkLibrary(map_lib);
exe.addObject(combat_obj);
exe.linkLibrary(combat_lib);
exe.linkSystemLibrary("raylib");
exe.linkSystemLibrary("m");
exe.linkSystemLibrary("pthread");

View file

@ -1,4 +1,6 @@
#include "audio.h"
#include "raylib.h"
#include "common.h"
#include <math.h>
#include <stddef.h>

View file

@ -1,6 +1,6 @@
#ifndef AUDIO_H
#define AUDIO_H
#include "game_state.h"
#include "common.h"
// Initialize audio system
void audio_init(void);

View file

@ -2,7 +2,7 @@
#define COMMON_H
#include "settings.h"
#include <stdbool.h>
#include <raylib.h>
typedef struct {
int x, y;
@ -114,5 +114,66 @@ typedef struct {
int effect_count;
} Enemy;
// Floating damage text
typedef enum { LABEL_NONE = 0, LABEL_DODGE, LABEL_BLOCK, LABEL_CRIT, LABEL_SLAIN, LABEL_PROC } FloatingLabel;
typedef struct {
int x, y;
int value;
int lifetime; // frames remaining
int is_critical;
FloatingLabel label; // label type instead of string
StatusEffectType effect_type; // used to pick color for proc labels
} FloatingText;
// AudioAssets
typedef struct {
Sound attack1, attack2, attack3;
Sound pickup;
Sound staircase;
Sound dodge1, dodge2, dodge3;
Sound crit;
} AudioAssets;
// GameState - encapsulates all game state for testability and save/load
typedef struct {
Player player;
Map map;
Dungeon dungeon;
Enemy enemies[MAX_ENEMIES];
int enemy_count;
Item items[MAX_ITEMS];
int item_count;
int game_over;
int game_won;
const char *last_message;
int message_timer;
int turn_count;
int awaiting_descend; // 0 = normal, 1 = waiting for Y/N
int show_inventory; // 0 = hidden, 1 = show overlay
int inv_selected; // currently selected inventory index
// action log
char action_log[5][128];
int log_count;
int log_head;
// visual effects
FloatingText floating_texts[8];
int floating_count;
int screen_shake; // frames of screen shake remaining
int shake_x;
int shake_y;
AudioAssets sounds;
// Statistics
int total_kills;
int items_collected;
int damage_dealt;
int damage_taken;
int crits_landed;
int times_hit;
int potions_used;
int floors_reached;
int final_score;
} GameState;
#endif // COMMON_H

View file

@ -1,9 +1,9 @@
#include "enemy.h"
#include "combat.h"
#include "common.h"
#include "map/map.h"
#include "map.h"
#include "movement.h"
#include "rng/rng.h"
#include "rng.h"
#include "settings.h"
#include <string.h>

View file

@ -1,70 +0,0 @@
#ifndef GAME_STATE_H
#define GAME_STATE_H
#include "common.h"
#include <raylib.h>
// Floating damage text
typedef enum { LABEL_NONE = 0, LABEL_DODGE, LABEL_BLOCK, LABEL_CRIT, LABEL_SLAIN, LABEL_PROC } FloatingLabel;
typedef struct {
int x, y;
int value;
int lifetime; // frames remaining
int is_critical;
FloatingLabel label; // label type instead of string
StatusEffectType effect_type; // used to pick color for proc labels
} FloatingText;
// AudioAssets
typedef struct {
Sound attack1, attack2, attack3;
Sound pickup;
Sound staircase;
Sound dodge1, dodge2, dodge3;
Sound crit;
} AudioAssets;
// GameState - encapsulates all game state for testability and save/load
typedef struct {
Player player;
Map map;
Dungeon dungeon;
Enemy enemies[MAX_ENEMIES];
int enemy_count;
Item items[MAX_ITEMS];
int item_count;
int game_over;
int game_won;
const char *last_message;
int message_timer;
int turn_count;
int awaiting_descend; // 0 = normal, 1 = waiting for Y/N
int show_inventory; // 0 = hidden, 1 = show overlay
int inv_selected; // currently selected inventory index
// action log
char action_log[5][128];
int log_count;
int log_head;
// visual effects
FloatingText floating_texts[8];
int floating_count;
int screen_shake; // frames of screen shake remaining
int shake_x;
int shake_y;
AudioAssets sounds;
// Statistics
int total_kills;
int items_collected;
int damage_dealt;
int damage_taken;
int crits_landed;
int times_hit;
int potions_used;
int floors_reached;
int final_score;
// Seed for this run
unsigned int run_seed;
} GameState;
#endif // GAME_STATE_H

View file

@ -1,6 +1,6 @@
#include "common.h"
#include "map/map.h"
#include "rng/rng.h"
#include "map.h"
#include "rng.h"
#include "settings.h"
#include <stddef.h>

View file

@ -1,20 +1,18 @@
#include "audio.h"
#include "combat.h"
#include "game_state.h"
#include "common.h"
#include "enemy.h"
#include "items.h"
#include "map/map.h"
#include "map.h"
#include "movement.h"
#include "player.h"
#include "raylib.h"
#include "render.h"
#include "rng/rng.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) {
@ -104,14 +102,11 @@ 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(gs->run_seed + floor_num * 98765);
rng_seed(floor_num * 54321);
// Find spawn position
int start_x, start_y;
@ -483,13 +478,7 @@ 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;
}
@ -519,16 +508,12 @@ void load_audio_assets(GameState *gs) {
}
// Main game loop
static void game_loop(unsigned int run_seed) {
static void game_loop(void) {
GameState gs;
memset(&gs, 0, sizeof(GameState));
gs.run_seed = run_seed;
// load external assets
// sound
load_audio_assets(&gs);
// font
Font fontTTF = LoadFontEx("./assets/fonts/spartan_500.ttf", 36, NULL, 0);
// Initialize first floor
rng_seed(12345);
init_floor(&gs, 1);
// Disable esc to exit
@ -548,16 +533,10 @@ static void game_loop(unsigned int run_seed) {
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);
}
}
@ -585,24 +564,21 @@ static void game_loop(unsigned int run_seed) {
// Floating texts follow world shake
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
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
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
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
if (gs.game_over) {
// Compute final score
@ -613,7 +589,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,
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();
@ -623,67 +599,17 @@ static void game_loop(unsigned int run_seed) {
}
}
// 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);
int main(void) {
// Initialize audio
audio_init();
// Initialize random number generator
SetRandomSeed(88435);
// 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);
// Initialize window
InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT + 60, "Roguelike");
SetTargetFPS(60);
// Run game
game_loop(run_seed);
game_loop();
// Cleanup
CloseWindow();

View file

@ -1,5 +1,5 @@
#include "map.h"
#include "rng/rng.h"
#include "rng.h"
#include "settings.h"
#include "utils.h"
#include <stdlib.h>
@ -170,6 +170,9 @@ 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);

View file

@ -1,6 +1,6 @@
#include "movement.h"
#include "enemy.h"
#include "map/map.h"
#include "map.h"
#include <stdbool.h>
// Check if position is occupied by player

View file

@ -1,5 +1,7 @@
#include "render.h"
#include "common.h"
#include "items.h"
#include "raylib.h"
#include "settings.h"
#include <stddef.h>
#include <stdio.h>
@ -124,7 +126,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
const int hud_y = MAP_HEIGHT * TILE_SIZE;
const int hud_height = 60;
@ -168,8 +170,7 @@ void render_ui(const Player *p, Font *font) {
int bar_height = 16;
// HP Label, above bar
// Vector2 hp_width = MeasureTextEx(*font, "HP", BIG_FONT, NAR_CHAR_SPACE);
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
DrawRectangle(bar_x, bar_y, bar_width, bar_height, (Color){20, 15, 15, 255});
@ -194,9 +195,8 @@ void render_ui(const Player *p, Font *font) {
// 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, 12);
DrawTextEx(*font, hp_text, (Vector2){bar_x + (bar_width - hp_text_w) / 2.0f, bar_y + 2}, MEDIUM_FONT,
SMALL_CHAR_SPACE, WHITE);
int hp_text_w = MeasureText(hp_text, 10);
DrawText(hp_text, bar_x + (bar_width - hp_text_w) / 2, bar_y + 2, 10, WHITE);
// Status effects
int effect_x = bar_x;
@ -231,7 +231,7 @@ void render_ui(const Player *p, Font *font) {
if (p->effects[i].duration > 0) {
char eff_text[16];
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;
}
}
@ -243,68 +243,65 @@ void render_ui(const Player *p, Font *font) {
// Floor
char floor_text[16];
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);
DrawTextEx(*font, "Floor", (Vector2){stats_x, stats_y + 16}, 12, NAR_CHAR_SPACE, text_dim);
DrawText(floor_text, stats_x, stats_y, 14, text_bright);
DrawText("Floor", stats_x, stats_y + 16, 9, text_dim);
// ATK
char atk_text[16];
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);
DrawTextEx(*font, "ATK", (Vector2){stats_x + stat_spacing, stats_y + 16}, 12, NAR_CHAR_SPACE, text_dim);
DrawText(atk_text, stats_x + stat_spacing, stats_y, 14, YELLOW);
DrawText("ATK", stats_x + stat_spacing, stats_y + 16, 9, text_dim);
// DEF
char def_text[16];
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,
(Color){100, 150, 255, 255});
DrawTextEx(*font, "DEF", (Vector2){stats_x + stat_spacing * 2, stats_y + 16}, 12, NAR_CHAR_SPACE, text_dim);
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);
int equip_x = section2_end + 15;
int equip_y = hud_y + 8;
// 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) {
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));
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 {
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
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) {
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);
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 {
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_y = hud_y + 14;
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});
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});
// 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);
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
const int log_width = 250;
const int log_height = 90;
@ -326,8 +323,7 @@ void render_action_log(const char log[5][128], int count, int head, Font *font)
// Title bar
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,
(Color){180, 160, 130, 255});
DrawText("MESSAGE LOG", log_x + 8, log_y + 6, 10, (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);
@ -352,16 +348,15 @@ void render_action_log(const char log[5][128], int count, int head, Font *font)
} else {
text_color = (Color){120, 110, 100, 200}; // oldest: dim
}
DrawTextEx(*font, log[idx], (Vector2){text_x, text_start_y + i * line_height}, NORM_FONT, SMALL_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
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,
(float)ov_width, (float)ov_height};
DrawRectangleRec(overlay, (Color){12, 12, 12, 252});
@ -369,15 +364,13 @@ void render_inventory_overlay(const Player *p, int selected, Font *font) {
// Title
const char *title = "INVENTORY";
// int title_w = MeasureText(title, 24);
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);
int title_w = MeasureText(title, 24);
DrawText(title, overlay.x + (overlay.width - title_w) / 2, overlay.y + 12, 24, WHITE);
// Draw each inventory slot
char slot_text[64];
int row_height = 26;
int start_y = overlay.y + 40;
int start_y = overlay.y + 50;
for (int i = 0; i < MAX_INVENTORY; i++) {
int y_pos = start_y + (i * row_height);
@ -394,8 +387,7 @@ void render_inventory_overlay(const Player *p, int selected, Font *font) {
// Slot number
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,
(Color){80, 80, 80, 255});
DrawText(slot_text, overlay.x + 16, y_pos + 4, 14, (Color){80, 80, 80, 255});
// Item name
const char *name = item_get_name(item);
@ -403,31 +395,31 @@ void render_inventory_overlay(const Player *p, int selected, Font *font) {
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};
DrawTextEx(*font, name, (Vector2){overlay.x + 45, y_pos + 4}, NORM_FONT, SMALL_CHAR_SPACE, name_color);
DrawText(name, overlay.x + 45, y_pos + 4, 14, name_color);
}
// Power
snprintf(slot_text, sizeof(slot_text), "+%d", item->power);
DrawTextEx(*font, slot_text, (Vector2){overlay.x + 150, y_pos + 4}, NORM_FONT, SMALL_CHAR_SPACE, YELLOW);
DrawText(slot_text, overlay.x + 150, y_pos + 4, 14, YELLOW);
// Action
if (item->type == ITEM_POTION) {
DrawTextEx(*font, "[U]se", (Vector2){overlay.x + 200, y_pos + 4}, NORM_FONT, SMALL_CHAR_SPACE, GREEN);
DrawText("[U]se", overlay.x + 200, y_pos + 4, 14, GREEN);
} else {
DrawTextEx(*font, "[E]quip [D]rop", (Vector2){overlay.x + 200, y_pos + 4}, NORM_FONT, SMALL_CHAR_SPACE, GOLD);
DrawText("[E]quip [D]rop", overlay.x + 200, y_pos + 4, 14, GOLD);
}
} else {
// Empty slot
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,
(Color){40, 40, 40, 255});
DrawText(slot_text, overlay.x + 16, y_pos + 4, 14, (Color){40, 40, 40, 255});
}
}
// Instructions at bottom
const char *hint = "[1-0] Select [E] Equip [U] Use [D] Drop [I/ESC] Close";
Vector2 hint_w = MeasureTextEx(*font, hint, SMALL_FONT, NAR_CHAR_SPACE);
DrawTextEx(*font, hint, (Vector2){overlay.x + (overlay.width - hint_w.x) / 2.0f, overlay.y + overlay.height - 22},
SMALL_FONT, NAR_CHAR_SPACE, (Color){80, 80, 80, 255});
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});
}
static Color label_color(FloatingText *ft, int alpha) {
@ -515,7 +507,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, unsigned int seed, Font *font) {
int times_hit, int potions, int floors, int turns, int score) {
// Semi-transparent overlay
Rectangle overlay = {0, 0, (float)SCREEN_WIDTH, (float)SCREEN_HEIGHT};
DrawRectangleRec(overlay, (Color){0, 0, 0, 210});
@ -525,14 +517,13 @@ 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);
DrawTextEx(*font, title, (Vector2){(SCREEN_WIDTH - title_width) / 2.0f, 30}, title_font_size, NORM_CHAR_SPACE,
title_color);
DrawText(title, (SCREEN_WIDTH - title_width) / 2, 30, title_font_size, title_color);
// Stats box
int box_x = SCREEN_WIDTH / 2 - 200;
int box_y = 110;
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});
DrawRectangleLines(box_x, box_y, box_w, box_h, (Color){100, 100, 100, 255});
@ -546,80 +537,71 @@ void render_end_screen(int is_victory, int kills, int items, int damage_dealt, i
Color value_color = WHITE;
// 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);
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;
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);
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;
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);
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;
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);
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;
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);
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;
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);
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;
// Column 2
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);
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;
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);
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;
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);
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;
// Score: placed below the last row of the longer column (6 items, row_y is already there)
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);
DrawTextEx(*font, line, (Vector2){col1_x + 90, col2_row_y}, 22, NORM_CHAR_SPACE, GOLD);
row_y += 35;
DrawText(line, col1_x + 90, row_y, 22, GOLD);
// 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);
DrawTextEx(*font, subtitle, (Vector2){(SCREEN_WIDTH - sub_width) / 2.0f, SCREEN_HEIGHT - 50}, 20, NORM_CHAR_SPACE,
LIGHTGRAY);
DrawText(subtitle, (SCREEN_WIDTH - sub_width) / 2, SCREEN_HEIGHT - 50, 20, LIGHTGRAY);
} else {
const char *subtitle = "Press R to restart or Q to quit";
int sub_width = MeasureText(subtitle, 20);
DrawTextEx(*font, subtitle, (Vector2){(SCREEN_WIDTH - sub_width) / 2.0f, SCREEN_HEIGHT - 50}, 20, NORM_CHAR_SPACE,
LIGHTGRAY);
DrawText(subtitle, (SCREEN_WIDTH - sub_width) / 2, SCREEN_HEIGHT - 50, 20, LIGHTGRAY);
}
}
void render_message(const char *message, Font *font) {
void render_message(const char *message) {
if (message == NULL)
return;
@ -654,8 +636,8 @@ void render_message(const char *message, Font *font) {
longest_line_width = current_line_width;
// Measure full message
Vector2 total_msg_width = MeasureTextEx(*font, message, font_size, NORM_CHAR_SPACE);
int box_width = total_msg_width.x + (padding_x * 2);
int total_msg_width = MeasureText(message, font_size);
int box_width = total_msg_width + (padding_x * 2);
// If message is too long, use wrapped width
if (box_width > max_box_width) {
@ -679,7 +661,7 @@ void render_message(const char *message, Font *font) {
DrawRectangleLines((int)msg_bg.x, (int)msg_bg.y, (int)msg_bg.width, (int)msg_bg.height, (Color){180, 180, 180, 255});
// Draw text centered
int text_x = (SCREEN_WIDTH - total_msg_width.x) / 2;
int text_x = (SCREEN_WIDTH - total_msg_width) / 2;
int text_y = (SCREEN_HEIGHT - font_size) / 2;
// For wrapped text, draw at box center with padding
@ -688,20 +670,5 @@ void render_message(const char *message, Font *font) {
text_y = (int)box_y + padding_y;
}
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);
DrawText(message, text_x, text_y, font_size, WHITE);
}

View file

@ -1,8 +1,7 @@
#ifndef RENDER_H
#define RENDER_H
#include "game_state.h"
#include "common.h"
// HUD colors
#define HUD_BG (Color){25, 20, 15, 255}
@ -69,7 +68,6 @@
#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
@ -88,25 +86,22 @@ void render_enemies(const Enemy *enemies, int count, const unsigned char visible
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, Font *font);
void render_ui(const Player *p);
// 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
void render_inventory_overlay(const Player *p, int selected, Font *font);
void render_inventory_overlay(const Player *p, int selected);
// 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, unsigned int seed, Font *font);
int times_hit, int potions, int floors, int turns, int score);
// Render a message popup
void render_message(const char *message, Font *font);
// Render seed display at top right of screen
void render_seed_display(unsigned int seed);
void render_message(const char *message);
#endif // RENDER_H

View file

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