Compare commits

..

3 commits

12 changed files with 181 additions and 334 deletions

View file

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

View file

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

View file

@ -4,10 +4,6 @@
#include "settings.h"
#include <raylib.h>
typedef struct {
int x, y;
} Vec2;
// Tile types
typedef enum { TILE_WALL, TILE_FLOOR, TILE_STAIRS } TileType;
@ -61,7 +57,7 @@ typedef struct {
// Player
typedef struct {
Vec2 position;
int x, y;
int hp, max_hp;
int attack;
int defense;
@ -87,7 +83,7 @@ typedef enum { ENEMY_GOBLIN, ENEMY_SKELETON, ENEMY_ORC } EnemyType;
// Enemy
typedef struct {
Vec2 position;
int x, y;
int hp;
int max_hp;
int attack;
@ -108,14 +104,12 @@ typedef struct {
} 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
char label[8]; // non-empty -> show label instead of numeric value
StatusEffectType effect_type; // used to pick color for proc labels
} FloatingText;

View file

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

View file

@ -4,7 +4,6 @@
#include "enemy.h"
#include "items.h"
#include "map.h"
#include "movement.h"
#include "player.h"
#include "raylib.h"
#include "render.h"
@ -43,14 +42,14 @@ static void spawn_floating_text(GameState *gs, int x, int y, int value, int is_c
gs->floating_texts[slot].x = x;
gs->floating_texts[slot].y = y;
gs->floating_texts[slot].value = value;
gs->floating_texts[slot].lifetime = FLOATING_TEXT_LIFETIME;
gs->floating_texts[slot].lifetime = 60;
gs->floating_texts[slot].is_critical = is_critical;
gs->floating_texts[slot].label = LABEL_NONE;
gs->floating_texts[slot].label[0] = '\0'; // numeric, no label
gs->floating_texts[slot].effect_type = EFFECT_NONE;
}
// spawn floating label text (DODGE, BLOCK, CRIT!, proc names, SLAIN)
static void spawn_floating_label(GameState *gs, int x, int y, FloatingLabel label, StatusEffectType effect_type) {
static void spawn_floating_label(GameState *gs, int x, int y, const char *label, StatusEffectType effect_type) {
int slot = float_slot(gs);
if (slot < 0)
return;
@ -59,24 +58,25 @@ static void spawn_floating_label(GameState *gs, int x, int y, FloatingLabel labe
gs->floating_texts[slot].value = 0;
gs->floating_texts[slot].lifetime = 60;
gs->floating_texts[slot].is_critical = 0;
gs->floating_texts[slot].label = label;
gs->floating_texts[slot].effect_type = effect_type;
strncpy(gs->floating_texts[slot].label, label, 7);
gs->floating_texts[slot].label[7] = '\0';
}
static FloatingLabel proc_label_for(StatusEffectType effect) {
static const char *proc_label_for(StatusEffectType effect) {
switch (effect) {
case EFFECT_POISON:
return LABEL_PROC;
return "POISON!";
case EFFECT_BLEED:
return LABEL_PROC;
return "BLEED!";
case EFFECT_BURN:
return LABEL_PROC;
return "BURN!";
case EFFECT_STUN:
return LABEL_PROC;
return "STUN!";
case EFFECT_WEAKEN:
return LABEL_PROC;
return "WEAKEN!";
default:
return LABEL_NONE;
return "";
}
}
@ -92,8 +92,8 @@ static void update_effects(GameState *gs) {
// update screen shake
if (gs->screen_shake > 0) {
gs->screen_shake--;
gs->shake_x = rng_int(-SHAKE_MAX_OFFSET, SHAKE_MAX_OFFSET);
gs->shake_y = rng_int(-SHAKE_MAX_OFFSET, SHAKE_MAX_OFFSET);
gs->shake_x = rng_int(-4, 4);
gs->shake_y = rng_int(-4, 4);
} else {
gs->shake_x = 0;
gs->shake_y = 0;
@ -118,8 +118,8 @@ static void init_floor(GameState *gs, int floor_num) {
gs->floors_reached = 1;
} else {
// Move player to new floor position
gs->player.position.x = start_x;
gs->player.position.y = start_y;
gs->player.x = start_x;
gs->player.y = start_y;
}
gs->player.floor = floor_num;
@ -138,9 +138,8 @@ static void tick_all_effects(GameState *gs) {
// Player effects
int player_effect_dmg = combat_tick_effects(&gs->player);
if (player_effect_dmg > 0) {
spawn_floating_text(gs, gs->player.position.x * TILE_SIZE + 8, gs->player.position.y * TILE_SIZE, player_effect_dmg,
0);
gs->screen_shake = SHAKE_EFFECT_DURATION;
spawn_floating_text(gs, gs->player.x * TILE_SIZE + 8, gs->player.y * TILE_SIZE, player_effect_dmg, 0);
gs->screen_shake = 4;
}
// Check if player died from effects
@ -157,7 +156,7 @@ static void tick_all_effects(GameState *gs) {
continue;
int enemy_effect_dmg = combat_tick_enemy_effects(e);
if (enemy_effect_dmg > 0) {
spawn_floating_text(gs, e->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) {
add_log(gs, "Enemy died from effects!");
@ -175,7 +174,7 @@ static void post_action(GameState *gs, Enemy *attacked_enemy) {
return;
// 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->last_message = "Descend to next floor? (Y/N)";
gs->message_timer = 120;
@ -184,11 +183,11 @@ static void post_action(GameState *gs, Enemy *attacked_enemy) {
// combat feedback - player attacked an enemy this turn
if (attacked_enemy != NULL) {
int ex = attacked_enemy->position.x * TILE_SIZE + 8;
int ey = attacked_enemy->position.y * TILE_SIZE;
int ex = attacked_enemy->x * TILE_SIZE + 8;
int ey = attacked_enemy->y * TILE_SIZE;
if (combat_was_dodged()) {
spawn_floating_label(gs, ex, ey, LABEL_DODGE, EFFECT_NONE);
spawn_floating_label(gs, ex, ey, "DODGE", EFFECT_NONE);
audio_play_dodge(gs);
} else {
if (combat_get_last_damage() > 0)
@ -196,21 +195,21 @@ static void post_action(GameState *gs, Enemy *attacked_enemy) {
spawn_floating_text(gs, ex, ey, combat_get_last_damage(), combat_was_critical());
audio_play_attack(gs);
if (combat_was_blocked()) {
spawn_floating_label(gs, ex, ey - 10, LABEL_BLOCK, EFFECT_NONE);
spawn_floating_label(gs, ex, ey - 10, "BLOCK", EFFECT_NONE);
audio_play_block(gs);
}
if (combat_was_critical()) {
spawn_floating_label(gs, ex + 8, ey - 10, LABEL_CRIT, EFFECT_NONE);
spawn_floating_label(gs, ex + 8, ey - 10, "CRIT!", EFFECT_NONE);
audio_play_crit(gs);
gs->crits_landed++;
}
StatusEffectType applied = combat_get_applied_effect();
if (applied != EFFECT_NONE) {
spawn_floating_label(gs, ex, ey - 20, LABEL_PROC, applied);
spawn_floating_label(gs, ex, ey - 20, proc_label_for(applied), applied);
audio_play_proc();
}
if (!attacked_enemy->alive) {
spawn_floating_label(gs, ex, ey - 20, LABEL_SLAIN, EFFECT_NONE);
spawn_floating_label(gs, ex, ey - 20, "SLAIN", EFFECT_NONE);
audio_play_enemy_death(gs);
gs->total_kills++;
}
@ -223,11 +222,11 @@ static void post_action(GameState *gs, Enemy *attacked_enemy) {
// Check if player took damage
if (combat_was_player_damage() && combat_get_last_damage() > 0) {
audio_play_player_damage(gs);
gs->screen_shake = SHAKE_PLAYER_DAMAGE_DURATION;
gs->screen_shake = 8;
gs->damage_taken += combat_get_last_damage();
gs->times_hit++;
spawn_floating_text(gs, gs->player.position.x * TILE_SIZE + 8, gs->player.position.y * TILE_SIZE,
combat_get_last_damage(), combat_was_critical());
spawn_floating_text(gs, gs->player.x * TILE_SIZE + 8, gs->player.y * TILE_SIZE, combat_get_last_damage(),
combat_was_critical());
}
// Set message and check game over
@ -390,7 +389,7 @@ static int handle_movement_input(GameState *gs) {
// Check for manual item pickup (G key)
if (IsKeyPressed(KEY_G)) {
Item *item = get_item_at_floor(gs->items, gs->item_count, gs->player.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 (player_pickup(&gs->player, item)) {
gs->items_collected++;
@ -419,45 +418,38 @@ static int handle_movement_input(GameState *gs) {
}
}
Vec2 direction = {0, 0};
// Movement: use IsKeyDown for held-key repeat
int dx = 0, dy = 0;
if (IsKeyDown(KEY_W) || IsKeyDown(KEY_UP))
direction.y = -1;
dy = -1;
else if (IsKeyDown(KEY_S) || IsKeyDown(KEY_DOWN))
direction.y = 1;
dy = 1;
else if (IsKeyDown(KEY_A) || IsKeyDown(KEY_LEFT))
direction.x = -1;
dx = -1;
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;
// Reset combat event before player acts
combat_reset_event();
int new_x = gs->player.position.x + direction.x;
int new_y = gs->player.position.y + direction.y;
Enemy *target = NULL;
int new_x = gs->player.x + dx;
int new_y = gs->player.y + dy;
int action = 0;
MoveResult result =
try_move_entity(&gs->player.position, direction, &gs->map, &gs->player, gs->enemies, gs->enemy_count, true);
if (result == MOVE_RESULT_MOVED) {
player_on_move(&gs->player);
// Attack enemy at target tile, or move into it
Enemy *target = player_find_enemy_at(gs->enemies, gs->enemy_count, new_x, new_y);
if (target != NULL) {
player_attack(&gs->player, target);
action = 1;
} else if (result == MOVE_RESULT_BLOCKED_ENEMY) {
target = player_find_enemy_at(gs->enemies, gs->enemy_count, new_x, new_y);
if (target != NULL) {
player_attack(&gs->player, target);
action = 1;
}
} else {
action = player_move(&gs->player, dx, dy, &gs->map);
}
if (action)
post_action(gs, target);
post_action(gs, target); // target is NULL on move, enemy ptr on attack
return action;
}

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

View file

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

View file

@ -30,8 +30,7 @@ void render_map(const Map *map) {
}
void render_player(const Player *p) {
Rectangle rect = {(float)(p->position.x * TILE_SIZE), (float)(p->position.y * TILE_SIZE), (float)TILE_SIZE,
(float)TILE_SIZE};
Rectangle rect = {(float)(p->x * TILE_SIZE), (float)(p->y * TILE_SIZE), (float)TILE_SIZE, (float)TILE_SIZE};
DrawRectangleRec(rect, BLUE);
}
@ -40,20 +39,20 @@ void render_enemies(const Enemy *enemies, int count) {
if (!enemies[i].alive)
continue;
Rectangle rect = {(float)(enemies[i].position.x * TILE_SIZE), (float)(enemies[i].position.y * TILE_SIZE),
(float)TILE_SIZE, (float)TILE_SIZE};
Rectangle rect = {(float)(enemies[i].x * TILE_SIZE), (float)(enemies[i].y * TILE_SIZE), (float)TILE_SIZE,
(float)TILE_SIZE};
// Different colors based on enemy type
Color enemy_color;
switch (enemies[i].type) {
case ENEMY_GOBLIN:
enemy_color = COLOR_ENEMY_GOBLIN; // dark red
enemy_color = (Color){150, 50, 50, 255}; // dark red
break;
case ENEMY_SKELETON:
enemy_color = COLOR_ENEMY_SKELETON; // light gray
enemy_color = (Color){200, 200, 200, 255}; // light gray
break;
case ENEMY_ORC:
enemy_color = COLOR_ENEMY_ORC; // dark green
enemy_color = (Color){50, 150, 50, 255}; // dark green
break;
default:
enemy_color = RED;
@ -73,8 +72,8 @@ void render_enemies(const Enemy *enemies, int count) {
bar_color = (Color){200, 180, 40, 255}; // yellow
else
bar_color = (Color){200, 60, 60, 255}; // red
Rectangle hp_bar = {(float)(enemies[i].position.x * TILE_SIZE), (float)(enemies[i].position.y * TILE_SIZE - 4),
(float)hp_pixels, 3};
Rectangle hp_bar = {(float)(enemies[i].x * TILE_SIZE), (float)(enemies[i].y * TILE_SIZE - 4), (float)hp_pixels,
3};
DrawRectangleRec(hp_bar, bar_color);
}
}
@ -92,13 +91,13 @@ void render_items(const Item *items, int count) {
Color item_color;
switch (items[i].type) {
case ITEM_POTION:
item_color = COLOR_ITEM_POTION; // red/pink
item_color = (Color){255, 100, 100, 255}; // red/pink
break;
case ITEM_WEAPON:
item_color = COLOR_ITEM_WEAPON; // yellow
item_color = (Color){255, 255, 100, 255}; // yellow
break;
case ITEM_ARMOR:
item_color = COLOR_ITEM_ARMOR; // blue
item_color = (Color){100, 100, 255, 255}; // blue
break;
default:
item_color = GREEN;
@ -378,12 +377,11 @@ void render_inventory_overlay(const Player *p, int selected, Font *font) {
// Item name
const char *name = item_get_name(item);
if (name) {
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}
: (Color){140, 140, 255, 255};
DrawTextEx(*font, name, (Vector2){overlay.x + 45, y_pos + 4}, 14, NORM_CHAR_SPACE, name_color);
}
DrawTextEx(*font, name, (Vector2){overlay.x + 45, y_pos + 4}, 14, NORM_CHAR_SPACE, name_color);
// Power
snprintf(slot_text, sizeof(slot_text), "+%d", item->power);
DrawTextEx(*font, slot_text, (Vector2){overlay.x + 150, y_pos + 4}, 14, NORM_CHAR_SPACE, YELLOW);
@ -409,60 +407,34 @@ void render_inventory_overlay(const Player *p, int selected, Font *font) {
}
static Color label_color(FloatingText *ft, int alpha) {
if (ft->label == LABEL_NONE)
return FLOAT_DAMAGE; // numeric damage default
if (ft->label[0] == '\0')
return (Color){255, 100, 100, alpha}; // numeric damage default
if (strcmp(ft->label, "DODGE") == 0)
return (Color){160, 160, 160, alpha};
if (strcmp(ft->label, "BLOCK") == 0)
return (Color){80, 130, 220, alpha};
if (strcmp(ft->label, "CRIT!") == 0)
return (Color){255, 200, 50, alpha};
if (strcmp(ft->label, "SLAIN") == 0)
return (Color){220, 50, 50, alpha};
switch (ft->label) {
case LABEL_DODGE:
return FLOAT_DODGE;
case LABEL_BLOCK:
return FLOAT_BLOCK;
case LABEL_CRIT:
return FLOAT_CRIT;
case LABEL_SLAIN:
return FLOAT_SLAIN;
case LABEL_PROC:
// Proc label, color driven by effect_type stored in the struct
switch (ft->effect_type) {
case EFFECT_POISON:
return (Color){50, 200, 50, alpha};
case EFFECT_BLEED:
return (Color){200, 50, 50, alpha};
case EFFECT_BURN:
return (Color){230, 130, 30, alpha};
case EFFECT_STUN:
return (Color){200, 200, 50, alpha};
case EFFECT_WEAKEN:
return (Color){120, 120, 120, alpha};
default:
return FLOAT_DEFAULT;
}
// Proc label, color driven by effect_type stored in the struct
switch (ft->effect_type) {
case EFFECT_POISON:
return (Color){50, 200, 50, alpha};
case EFFECT_BLEED:
return (Color){200, 50, 50, alpha};
case EFFECT_BURN:
return (Color){230, 130, 30, alpha};
case EFFECT_STUN:
return (Color){200, 200, 50, alpha};
case EFFECT_WEAKEN:
return (Color){120, 120, 120, alpha};
default:
return FLOAT_DAMAGE;
return (Color){200, 200, 200, alpha};
}
}
static const char *label_text(FloatingLabel label) {
switch (label) {
case LABEL_DODGE:
return "DODGE";
case LABEL_BLOCK:
return "BLOCK";
case LABEL_CRIT:
return "CRIT!";
case LABEL_SLAIN:
return "SLAIN";
case LABEL_PROC:
return "PROC";
default:
return "";
}
}
static int label_font_size(FloatingLabel label) {
return (label == LABEL_CRIT) ? FONT_SIZE_FLOAT_CRIT : FONT_SIZE_FLOAT_LABEL;
}
void render_floating_texts(FloatingText *texts, int count, int shake_x, int shake_y) {
for (int i = 0; i < count; i++) {
if (texts[i].lifetime <= 0)
@ -473,14 +445,13 @@ void render_floating_texts(FloatingText *texts, int count, int shake_x, int shak
float alpha = (float)texts[i].lifetime / 60.0f;
int a = (int)(255 * alpha);
if (texts[i].label != LABEL_NONE) {
// Label text (DODGE, BLOCK, CRIT!, SLAIN, or PROC)
// CRIT! gets larger font size
int font_size = label_font_size(texts[i].label);
if (texts[i].label[0] != '\0') {
// Label text (DODGE, BLOCK, CRIT!, proc name, SLAIN)
// Check for "CRIT!" specifically rather than just 'C' prefix
int font_size = (strcmp(texts[i].label, "CRIT!") == 0) ? 16 : 14;
Color color = label_color(&texts[i], a);
const char *text = label_text(texts[i].label);
int text_w = MeasureText(text, font_size);
DrawText(text, x - text_w / 2, y, font_size, color);
int text_w = MeasureText(texts[i].label, font_size);
DrawText(texts[i].label, x - text_w / 2, y, font_size, color);
} else {
// Numeric damage
Color color = texts[i].is_critical ? (Color){255, 200, 50, a} : (Color){255, 100, 100, a};

View file

@ -3,76 +3,6 @@
#include "common.h"
// HUD colors
#define HUD_BG (Color){25, 20, 15, 255}
#define HUD_BORDER (Color){139, 119, 89, 255}
#define HUD_LIGHT_LINE (Color){60, 55, 50, 255}
#define HUD_DARK_LINE (Color){15, 12, 10, 255}
#define TEXT_DIM (Color){160, 150, 140, 255}
#define TEXT_BRIGHT (Color){240, 230, 220, 255}
// HP bar colors
#define HP_HIGH (Color){60, 180, 60, 255}
#define HP_MED (Color){200, 180, 40, 255}
#define HP_LOW (Color){200, 60, 60, 255}
#define HP_BAR_BG (Color){20, 15, 15, 255}
#define HP_BAR_BORDER (Color){80, 70, 60, 255}
// Enemy type colors
#define COLOR_ENEMY_GOBLIN (Color){150, 50, 50, 255}
#define COLOR_ENEMY_SKELETON (Color){200, 200, 200, 255}
#define COLOR_ENEMY_ORC (Color){50, 150, 50, 255}
// Item type colors
#define COLOR_ITEM_POTION (Color){255, 100, 100, 255}
#define COLOR_ITEM_WEAPON (Color){255, 255, 100, 255}
#define COLOR_ITEM_ARMOR (Color){100, 100, 255, 255}
// Action log colors
#define LOG_BG (Color){15, 12, 10, 230}
#define LOG_BORDER (Color){100, 85, 65, 255}
#define LOG_DARK (Color){60, 50, 40, 255}
#define LOG_TITLE_BG (Color){30, 25, 20, 255}
#define LOG_TEXT (Color){180, 160, 130, 255}
#define LOG_NEWEST (Color){220, 210, 200, 255}
#define LOG_RECENT (Color){180, 170, 160, 255}
#define LOG_OLDER (Color){150, 140, 130, 230}
#define LOG_OLDEST (Color){120, 110, 100, 200}
// Inventory overlay colors
#define INV_OVERLAY_BG (Color){12, 12, 12, 252}
#define INV_BORDER (Color){70, 70, 70, 255}
#define INV_SLOT_BG (Color){45, 45, 45, 255}
#define INV_SELECTED (Color){180, 160, 80, 255}
#define INV_EMPTY (Color){40, 40, 40, 255}
#define INV_HINT (Color){65, 65, 65, 255}
// Floating text colors
#define FLOAT_DAMAGE (Color){255, 100, 100, 255}
#define FLOAT_CRIT (Color){255, 200, 50, 255}
#define FLOAT_DODGE (Color){160, 160, 160, 255}
#define FLOAT_BLOCK (Color){80, 130, 220, 255}
#define FLOAT_SLAIN (Color){220, 50, 50, 255}
#define FLOAT_DEFAULT (Color){200, 200, 200, 255}
// Floating label font sizes
#define FONT_SIZE_FLOAT_LABEL 14
#define FONT_SIZE_FLOAT_CRIT 16
#define FONT_SIZE_FLOAT_DMG 18
// Message box colors
#define MSG_BG (Color){45, 45, 45, 235}
#define MSG_BORDER (Color){180, 180, 180, 255}
// End screen colors
#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}
// Portrait placeholder
// FIXME: remove when player sprites are available
#define PORTRAIT_BG (Color){30, 30, 45, 255}
// Render the map tiles
void render_map(const Map *map);

View file

@ -56,15 +56,4 @@
#define PLAYER_BASE_DODGE 5
#define PLAYER_BASE_BLOCK 0
// Screen shake
#define SHAKE_EFFECT_DURATION 4
#define SHAKE_PLAYER_DAMAGE_DURATION 8
#define SHAKE_MAX_OFFSET 4
// Floating text
#define FLOATING_TEXT_LIFETIME 60
// Message timer
#define MESSAGE_TIMER_DURATION 60
#endif // SETTINGS_H