Compare commits

..

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

24 changed files with 325 additions and 3327 deletions

3
.gitignore vendored
View file

@ -9,5 +9,4 @@ roguelike
# Zig # Zig
.zig-cache .zig-cache
zig-out .zig-out
*.o

View file

@ -4,7 +4,7 @@ build:
# Build and run # Build and run
dev: dev:
zig build -Dadmin-controls=true run zig build run
# Clean build artifacts # Clean build artifacts
clean: clean:

View file

@ -3,22 +3,13 @@ const std = @import("std");
pub fn build(b: *std.Build) void { pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{}); const optimize = b.standardOptimizeOption(.{});
const admin_controls = b.option(bool, "admin-controls", "Enable in-game development admin controls") orelse false;
const base_c_flags = [_][]const u8{ const c_flags = [_][]const u8{
"-std=c99", "-std=c99",
"-Wall", "-Wall",
"-Wextra", "-Wextra",
"-O2", "-O2",
}; };
const admin_c_flags = [_][]const u8{
"-std=c99",
"-Wall",
"-Wextra",
"-O2",
"-DROGGED_ADMIN_CONTROLS=1",
};
const c_flags = if (admin_controls) &admin_c_flags else &base_c_flags;
// RNG library // RNG library
const rng_lib = b.addLibrary(.{ const rng_lib = b.addLibrary(.{
@ -29,11 +20,11 @@ pub fn build(b: *std.Build) void {
.link_libc = true, .link_libc = true,
}), }),
}); });
rng_lib.root_module.addCSourceFiles(.{ rng_lib.addCSourceFiles(.{
.files = &[_][]const u8{"libs/rng/rng.c"}, .files = &[_][]const u8{"libs/rng/rng.c"},
.flags = c_flags, .flags = &c_flags,
}); });
rng_lib.root_module.addIncludePath(b.path("libs/rng")); rng_lib.addIncludePath(b.path("libs/rng"));
// Map library // Map library
const map_lib = b.addLibrary(.{ const map_lib = b.addLibrary(.{
@ -44,41 +35,19 @@ pub fn build(b: *std.Build) void {
.link_libc = true, .link_libc = true,
}), }),
}); });
map_lib.root_module.addCSourceFiles(.{ map_lib.addCSourceFiles(.{
.files = &[_][]const u8{ .files = &[_][]const u8{
"libs/map/map.c", "libs/map/map.c",
"libs/map/utils.c", "libs/map/utils.c",
}, },
.flags = c_flags, .flags = &c_flags,
}); });
// map.h includes common.h and settings.h which live in src/ // map.h includes common.h and settings.h which live in src/
map_lib.root_module.addIncludePath(b.path("src")); map_lib.addIncludePath(b.path("src"));
// map.c includes rng/rng.h via libs/ root // map.c includes rng/rng.h via libs/ root
map_lib.root_module.addIncludePath(b.path("libs")); map_lib.addIncludePath(b.path("libs"));
// utils.h is co-located with map.c // utils.h is co-located with map.c
map_lib.root_module.addIncludePath(b.path("libs/map")); map_lib.addIncludePath(b.path("libs/map"));
// Tileset library
const tileset_obj = b.addObject(.{
.name = "tileset",
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
.link_libc = true,
}),
});
tileset_obj.root_module.addCSourceFiles(.{
.files = &[_][]const u8{
"libs/tileset/tileset.c",
"libs/tileset/tileset_paint.c",
},
.flags = c_flags,
});
// tileset.h includes settings.h which lives in src/
tileset_obj.root_module.addIncludePath(b.path("src"));
// tileset.c includes tileset.h which is co-located
tileset_obj.root_module.addIncludePath(b.path("libs/tileset"));
tileset_obj.root_module.linkSystemLibrary("raylib", .{});
// Zig combat library. This must be compiled as an object and linked // Zig combat library. This must be compiled as an object and linked
// directly to bypassing the archive step, or it yields a corrupt // directly to bypassing the archive step, or it yields a corrupt
@ -93,8 +62,8 @@ pub fn build(b: *std.Build) void {
}), }),
}); });
// common.h and settings.h live in src/; rng.h exposed bare from libs/rng // common.h and settings.h live in src/; rng.h exposed bare from libs/rng
combat_obj.root_module.addIncludePath(b.path("src")); combat_obj.addIncludePath(b.path("src"));
combat_obj.root_module.addIncludePath(b.path("libs/rng")); combat_obj.addIncludePath(b.path("libs/rng"));
// C sources remaining in src/ // C sources remaining in src/
const c_sources = [_][]const u8{ const c_sources = [_][]const u8{
@ -118,24 +87,23 @@ pub fn build(b: *std.Build) void {
}), }),
}); });
exe.root_module.addCSourceFiles(.{ exe.addCSourceFiles(.{
.files = &c_sources, .files = &c_sources,
.flags = c_flags, .flags = &c_flags,
}); });
// src/ for own headers; libs/ so "rng/rng.h" and "map/map.h" resolve // src/ for own headers; libs/ so "rng/rng.h" and "map/map.h" resolve
exe.root_module.addIncludePath(b.path("src")); exe.addIncludePath(b.path("src"));
exe.root_module.addIncludePath(b.path("libs")); exe.addIncludePath(b.path("libs"));
exe.root_module.linkLibrary(rng_lib); exe.linkLibrary(rng_lib);
exe.root_module.linkLibrary(map_lib); exe.linkLibrary(map_lib);
exe.root_module.addObject(tileset_obj); exe.addObject(combat_obj);
exe.root_module.addObject(combat_obj); exe.linkSystemLibrary("raylib");
exe.root_module.linkSystemLibrary("raylib", .{}); exe.linkSystemLibrary("m");
exe.root_module.linkSystemLibrary("m", .{}); exe.linkSystemLibrary("pthread");
exe.root_module.linkSystemLibrary("pthread", .{}); exe.linkSystemLibrary("dl");
exe.root_module.linkSystemLibrary("dl", .{}); exe.linkSystemLibrary("rt");
exe.root_module.linkSystemLibrary("rt", .{});
b.installArtifact(exe); b.installArtifact(exe);

View file

@ -28,7 +28,7 @@
// Tracks the earliest Zig version that the package considers to be a // Tracks the earliest Zig version that the package considers to be a
// supported use case. // supported use case.
.minimum_zig_version = "0.16.0", .minimum_zig_version = "0.15.2",
// This field is optional. // This field is optional.
// Each dependency must either provide a `url` and `hash`, or a `path`. // Each dependency must either provide a `url` and `hash`, or a `path`.

View file

@ -28,7 +28,7 @@ to be viable. For a semi-complete list of things that need to be done, see the
Rogged is built on a relatively simple stack. It uses C99 for the main game 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 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.16.0, but 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 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 `just` for common development tasks in the case you plan to contribute. For
building, Zig is enough. building, Zig is enough.
@ -53,7 +53,7 @@ $ just dev
### Manual Build ### Manual Build
If you are allergic to good tooling and would rather use your system Zig, you If you are allergic to good tooling and would rather use your system Zig, you
may simply invoke `zig build` after acquiring Zig 0.16.0. may simply invoke `zig build` after acquiring Zig 0.15.2.
```sh ```sh
# Full build # Full build

6
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1780749050, "lastModified": 1775036866,
"narHash": "sha256-3av0pIjlOWQ6rDbNOmpUSvbNnJkGORQKKjb4LtCZsIY=", "narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "a799d3e3886da994fa307f817a6bc705ae538eeb", "rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -101,18 +101,18 @@ fn compact(effects: [*c]c.StatusEffect, count: [*c]c_int) void {
count[0] = @intCast(write); count[0] = @intCast(write);
} }
fn tickOne(eff: [*c]c.StatusEffect, hp: [*c]c_int) c_int { fn tickOne(eff: *c.StatusEffect, hp: *c_int) c_int {
if (eff[0].duration <= 0) return 0; if (eff.duration <= 0) return 0;
var dmg: c_int = 0; var dmg: c_int = 0;
switch (eff[0].type) { switch (eff.type) {
c.EFFECT_POISON, c.EFFECT_BLEED, c.EFFECT_BURN => { c.EFFECT_POISON, c.EFFECT_BLEED, c.EFFECT_BURN => {
dmg = eff[0].intensity; dmg = eff.intensity;
hp[0] -= dmg; hp.* -= dmg;
}, },
else => {}, else => {},
} }
eff[0].duration -= 1; eff.duration -= 1;
return dmg; return dmg;
} }

View file

@ -2,31 +2,25 @@
#include "rng/rng.h" #include "rng/rng.h"
#include "settings.h" #include "settings.h"
#include "utils.h" #include "utils.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
void map_init(Map *map) { void map_init(Map *map) {
// Fill entire map with walls
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++) {
map->tiles[y][x] = TILE_WALL; map->tiles[y][x] = TILE_WALL;
} }
} }
memset(map->light_map, 0, sizeof(map->light_map)); memset(map->visible, 0, sizeof(map->visible));
memset(map->remembered, 0, sizeof(map->remembered)); memset(map->remembered, 0, sizeof(map->remembered));
memset(map->door_open_from, 255, sizeof(map->door_open_from));
memset(map->door_anim_timer, 0, sizeof(map->door_anim_timer));
memset(map->door_anim_target, 0, sizeof(map->door_anim_target));
map->room_count = 0; map->room_count = 0;
} }
int is_floor(const Map *map, int x, int y) { int is_floor(const Map *map, int x, int y) {
if (!in_bounds(x, y, MAP_WIDTH, MAP_HEIGHT)) if (!in_bounds(x, y, MAP_WIDTH, MAP_HEIGHT))
return 0; return 0;
return map->tiles[y][x] == TILE_FLOOR || map->tiles[y][x] == TILE_STAIRS || map->tiles[y][x] == TILE_DOOR_OPEN || return map->tiles[y][x] == TILE_FLOOR || map->tiles[y][x] == TILE_STAIRS;
map->tiles[y][x] == TILE_DOOR_RUINED || map->tiles[y][x] == TILE_DOOR_CLOSED ||
map->tiles[y][x] == TILE_RUBBLE || map->tiles[y][x] == TILE_SHALLOW_WATER;
} }
void get_room_center(Room *room, int *cx, int *cy) { void get_room_center(Room *room, int *cx, int *cy) {
@ -38,25 +32,9 @@ void get_room_center(Room *room, int *cx, int *cy) {
static void carve_room(Map *map, Room *room) { static void carve_room(Map *map, Room *room) {
for (int y = room->y; y < room->y + room->h; y++) { for (int y = room->y; y < room->y + room->h; y++) {
for (int x = room->x; x < room->x + room->w; x++) { for (int x = room->x; x < room->x + room->w; x++) {
if (!in_bounds(x, y, MAP_WIDTH, MAP_HEIGHT)) if (in_bounds(x, y, MAP_WIDTH, MAP_HEIGHT)) {
continue;
int local_x = x - room->x;
int local_y = y - room->y;
int carved = 1;
if ((room->type == ROOM_SHRINE || room->type == ROOM_VAULT) &&
((local_x == 0 && local_y == 0) || (local_x == room->w - 1 && local_y == 0) ||
(local_x == 0 && local_y == room->h - 1) || (local_x == room->w - 1 && local_y == room->h - 1))) {
carved = 0;
}
if (room->type == ROOM_CRYPT &&
((local_x == 0 || local_x == room->w - 1) && (local_y == 0 || local_y == room->h - 1)))
carved = 0;
if (carved)
map->tiles[y][x] = TILE_FLOOR; map->tiles[y][x] = TILE_FLOOR;
}
} }
} }
} }
@ -88,125 +66,39 @@ static int room_overlaps(Room *rooms, int count, Room *new_room) {
// Add padding to prevent rooms from touching // Add padding to prevent rooms from touching
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
Room *r = &rooms[i]; Room *r = &rooms[i];
if (!(new_room->x > r->x + r->w + 2 || new_room->x + new_room->w + 2 < r->x || new_room->y > r->y + r->h + 2 || if (!(new_room->x > r->x + r->w || new_room->x + new_room->w < r->x || new_room->y > r->y + r->h ||
new_room->y + new_room->h + 2 < r->y)) { new_room->y + new_room->h < r->y)) {
return 1; return 1;
} }
} }
return 0; return 0;
} }
static RoomType pick_room_type(int floor, int room_index) {
if (room_index == 0)
return ROOM_START;
int roll = rng_int(0, 99);
if (floor <= 1) {
if (roll < 35)
return ROOM_GUARD;
if (roll < 55)
return ROOM_ARMORY;
if (roll < 75)
return ROOM_SHRINE;
return ROOM_CRYPT;
}
if (floor == 2) {
if (roll < 40)
return ROOM_CISTERN;
if (roll < 60)
return ROOM_GUARD;
if (roll < 80)
return ROOM_SHRINE;
return ROOM_CRYPT;
}
if (floor == 3) {
if (roll < 35)
return ROOM_CRYPT;
if (roll < 60)
return ROOM_LIBRARY;
if (roll < 80)
return ROOM_CISTERN;
return ROOM_VAULT;
}
if (floor == 4) {
if (roll < 35)
return ROOM_FORGE;
if (roll < 55)
return ROOM_ARMORY;
if (roll < 75)
return ROOM_CRYPT;
return ROOM_VAULT;
}
if (roll < 30)
return ROOM_VAULT;
if (roll < 55)
return ROOM_CRYPT;
if (roll < 75)
return ROOM_FORGE;
return ROOM_SHRINE;
}
static void room_size_for_type(RoomType type, int *w, int *h) {
switch (type) {
case ROOM_START:
*w = rng_int(7, 10);
*h = rng_int(6, 8);
break;
case ROOM_GUARD:
case ROOM_ARMORY:
*w = rng_int(6, 11);
*h = rng_int(5, 8);
break;
case ROOM_SHRINE:
case ROOM_VAULT:
*w = rng_int(7, 11);
*h = rng_int(7, 11);
break;
case ROOM_CISTERN:
*w = rng_int(8, 14);
*h = rng_int(6, 10);
break;
case ROOM_CRYPT:
case ROOM_LIBRARY:
*w = rng_int(8, 13);
*h = rng_int(5, 9);
break;
case ROOM_FORGE:
*w = rng_int(7, 12);
*h = rng_int(6, 9);
break;
}
}
// Generate rooms for this floor // Generate rooms for this floor
static int generate_rooms(Map *map, Room *rooms, int floor) { static int generate_rooms(Map *map, Room *rooms, int floor) {
int room_count = 0; int room_count = 0;
int attempts = 0; int attempts = 0;
int max_attempts = 250; int max_attempts = 100;
// Room count varies by floor, but capped at max_rooms // Room count varies by floor, but capped at max_rooms
int target_rooms = 8 + (floor % 3) + rng_int(0, 3); int target_rooms = 5 + (floor % 3) + rng_int(0, 3);
if (target_rooms > MAX_ROOMS) if (target_rooms > MAX_ROOMS)
target_rooms = MAX_ROOMS; target_rooms = MAX_ROOMS;
while (room_count < target_rooms && attempts < max_attempts) { while (room_count < target_rooms && attempts < max_attempts) {
attempts++; attempts++;
RoomType type = pick_room_type(floor, room_count); // Random room dimensions
int w, h; int w = rng_int(5, 12);
room_size_for_type(type, &w, &h); int h = rng_int(5, 10);
int x, y; // Random position (within map bounds with 1-tile border)
if (room_count == 0) { int x = rng_int(2, MAP_WIDTH - w - 2);
x = rng_int(3, 8); int y = rng_int(2, MAP_HEIGHT - h - 2);
y = rng_int(3, MAP_HEIGHT - h - 4);
} else {
x = rng_int(2, MAP_WIDTH - w - 3);
y = rng_int(2, MAP_HEIGHT - h - 3);
}
Room new_room = {x, y, w, h, type}; Room new_room = {x, y, w, h};
// Check for overlap
if (!room_overlaps(rooms, room_count, &new_room)) { if (!room_overlaps(rooms, room_count, &new_room)) {
rooms[room_count] = new_room; rooms[room_count] = new_room;
carve_room(map, &new_room); carve_room(map, &new_room);
@ -217,227 +109,22 @@ static int generate_rooms(Map *map, Room *rooms, int floor) {
return room_count; return room_count;
} }
// Check if a tile is at a room boundary (adjacent to wall but inside room)
static int is_room_boundary(Map *map, int x, int y) {
// Must be floor
if (map->tiles[y][x] != TILE_FLOOR)
return 0;
// Must have at least one adjacent wall
if (in_bounds(x - 1, y, MAP_WIDTH, MAP_HEIGHT) && map->tiles[y][x - 1] == TILE_WALL)
return 1;
if (in_bounds(x + 1, y, MAP_WIDTH, MAP_HEIGHT) && map->tiles[y][x + 1] == TILE_WALL)
return 1;
if (in_bounds(x, y - 1, MAP_WIDTH, MAP_HEIGHT) && map->tiles[y - 1][x] == TILE_WALL)
return 1;
if (in_bounds(x, y + 1, MAP_WIDTH, MAP_HEIGHT) && map->tiles[y + 1][x] == TILE_WALL)
return 1;
return 0;
}
// A corridor tile is a narrow floor passage with walls on at least 2 sides.
static int is_corridor_tile(const Map *map, int x, int y) {
if (map->tiles[y][x] != TILE_FLOOR)
return 0;
int walls = 0;
if (y > 0 && map->tiles[y - 1][x] == TILE_WALL)
walls++;
if (y < MAP_HEIGHT - 1 && map->tiles[y + 1][x] == TILE_WALL)
walls++;
if (x > 0 && map->tiles[y][x - 1] == TILE_WALL)
walls++;
if (x < MAP_WIDTH - 1 && map->tiles[y][x + 1] == TILE_WALL)
walls++;
return walls >= 2;
}
static int tile_in_any_room(int x, int y, Room *rooms, int room_count) {
for (int i = 0; i < room_count; i++) {
if (x >= rooms[i].x && x < rooms[i].x + rooms[i].w && y >= rooms[i].y && y < rooms[i].y + rooms[i].h)
return 1;
}
return 0;
}
static int tile_in_room(const Room *room, int x, int y) {
return x >= room->x && x < room->x + room->w && y >= room->y && y < room->y + room->h;
}
Room *map_room_at(Map *map, int x, int y) {
for (int i = 0; i < map->room_count; i++) {
if (tile_in_room(&map->rooms[i], x, y))
return &map->rooms[i];
}
return NULL;
}
static int protected_room_tile(const Room *room, int x, int y) {
int cx, cy;
get_room_center((Room *)room, &cx, &cy);
return (abs(x - cx) <= 1 && abs(y - cy) <= 1) || x == room->x || y == room->y || x == room->x + room->w - 1 ||
y == room->y + room->h - 1;
}
static void set_room_tile(Map *map, const Room *room, int x, int y, TileType tile) {
if (!in_bounds(x, y, MAP_WIDTH, MAP_HEIGHT) || !tile_in_room(room, x, y) || protected_room_tile(room, x, y))
return;
if (map->tiles[y][x] == TILE_FLOOR)
map->tiles[y][x] = tile;
}
static void decorate_room(Map *map, const Room *room, int floor) {
int cx, cy;
get_room_center((Room *)room, &cx, &cy);
switch (room->type) {
case ROOM_START:
if (room->w >= 8 && room->h >= 6) {
set_room_tile(map, room, room->x + 2, room->y + 2, TILE_RUBBLE);
set_room_tile(map, room, room->x + room->w - 3, room->y + room->h - 3, TILE_RUBBLE);
}
break;
case ROOM_GUARD:
for (int y = room->y + 2; y < room->y + room->h - 2; y += 3) {
set_room_tile(map, room, room->x + 2, y, TILE_STATUE);
set_room_tile(map, room, room->x + room->w - 3, y, TILE_STATUE);
}
break;
case ROOM_SHRINE:
set_room_tile(map, room, cx - 2, cy, TILE_STATUE);
set_room_tile(map, room, cx + 2, cy, TILE_STATUE);
set_room_tile(map, room, cx, cy - 2, TILE_RUBBLE);
set_room_tile(map, room, cx, cy + 2, TILE_RUBBLE);
break;
case ROOM_CISTERN:
for (int y = cy - 1; y <= cy + 1; y++) {
for (int x = cx - 2; x <= cx + 2; x++) {
if ((x + y + floor) % 5 != 0)
set_room_tile(map, room, x, y, TILE_SHALLOW_WATER);
}
}
break;
case ROOM_ARMORY:
for (int x = room->x + 2; x < room->x + room->w - 2; x += 3) {
set_room_tile(map, room, x, room->y + 2, TILE_RUBBLE);
set_room_tile(map, room, x, room->y + room->h - 3, TILE_RUBBLE);
}
break;
case ROOM_CRYPT:
for (int x = room->x + 2; x < room->x + room->w - 2; x += 3) {
set_room_tile(map, room, x, cy - 1, TILE_STATUE);
set_room_tile(map, room, x, cy + 1, TILE_RUBBLE);
}
break;
case ROOM_LIBRARY:
for (int y = room->y + 2; y < room->y + room->h - 2; y += 2) {
set_room_tile(map, room, room->x + 2, y, TILE_STATUE);
set_room_tile(map, room, room->x + room->w - 3, y, TILE_RUBBLE);
}
break;
case ROOM_FORGE:
for (int y = cy - 1; y <= cy + 1; y++) {
set_room_tile(map, room, cx - 2, y, TILE_RUBBLE);
set_room_tile(map, room, cx + 2, y, TILE_RUBBLE);
}
set_room_tile(map, room, cx, cy - 2, TILE_STATUE);
break;
case ROOM_VAULT:
set_room_tile(map, room, cx - 2, cy - 2, TILE_STATUE);
set_room_tile(map, room, cx + 2, cy - 2, TILE_STATUE);
set_room_tile(map, room, cx - 2, cy + 2, TILE_STATUE);
set_room_tile(map, room, cx + 2, cy + 2, TILE_STATUE);
break;
}
}
static void decorate_rooms(Map *map, Room *rooms, int room_count, int floor) {
for (int i = 0; i < room_count; i++) {
decorate_room(map, &rooms[i], floor);
}
}
// Place doors at corridor-room junctions.
// Doors sit on corridor tiles, not room tiles, so they occupy the actual doorway.
static void place_doors(Map *map, Room *rooms, int room_count) {
for (int y = 0; y < MAP_HEIGHT; y++) {
for (int x = 0; x < MAP_WIDTH; x++) {
if (map->tiles[y][x] != TILE_FLOOR)
continue;
if (!is_corridor_tile(map, x, y))
continue;
// Don't place doors inside rooms — corridors are between rooms
if (tile_in_any_room(x, y, rooms, room_count))
continue;
// Corridor must be adjacent to a room floor tile
const int dx[4] = {0, 0, 1, -1};
const int dy[4] = {1, -1, 0, 0};
for (int i = 0; i < 4; i++) {
int nx = x + dx[i];
int ny = y + dy[i];
if (!in_bounds(nx, ny, MAP_WIDTH, MAP_HEIGHT))
continue;
if (map->tiles[ny][nx] != TILE_FLOOR)
continue;
if (tile_in_any_room(nx, ny, rooms, room_count)) {
map->tiles[y][x] = TILE_DOOR_CLOSED;
break;
}
}
}
}
}
// Connect all rooms with corridors // Connect all rooms with corridors
static int room_distance2(Room *a, Room *b) {
int ax, ay, bx, by;
get_room_center(a, &ax, &ay);
get_room_center(b, &bx, &by);
int dx = ax - bx;
int dy = ay - by;
return dx * dx + dy * dy;
}
static void carve_connection(Map *map, Room *a, Room *b) {
int cx1, cy1, cx2, cy2;
get_room_center(a, &cx1, &cy1);
get_room_center(b, &cx2, &cy2);
if (rng_int(0, 2) == 0) {
int mid_x = (cx1 + cx2) / 2 + rng_int(-3, 3);
carve_h_corridor(map, cx1, mid_x, cy1);
carve_v_corridor(map, mid_x, cy1, cy2);
carve_h_corridor(map, mid_x, cx2, cy2);
} else if (rng_int(0, 1) == 0) {
carve_h_corridor(map, cx1, cx2, cy1);
carve_v_corridor(map, cx2, cy1, cy2);
} else {
carve_v_corridor(map, cx1, cy1, cy2);
carve_h_corridor(map, cx1, cx2, cy2);
}
}
static void connect_rooms(Map *map, Room *rooms, int room_count) { static void connect_rooms(Map *map, Room *rooms, int room_count) {
for (int i = 1; i < room_count; i++) { for (int i = 0; i < room_count - 1; i++) {
int nearest = 0; int cx1, cy1, cx2, cy2;
int best = room_distance2(&rooms[i], &rooms[0]); get_room_center(&rooms[i], &cx1, &cy1);
for (int j = 1; j < i; j++) { get_room_center(&rooms[i + 1], &cx2, &cy2);
int dist = room_distance2(&rooms[i], &rooms[j]);
if (dist < best) { // Carve L-shaped corridor between rooms
best = dist; if (rng_int(0, 1) == 0) {
nearest = j; carve_h_corridor(map, cx1, cx2, cy1);
} carve_v_corridor(map, cx2, cy1, cy2);
} else {
carve_v_corridor(map, cx1, cy1, cy2);
carve_h_corridor(map, cx1, cx2, cy2);
} }
carve_connection(map, &rooms[i], &rooms[nearest]);
} }
for (int i = 0; i < room_count; i++) {
if (rng_int(0, 99) >= 28)
continue;
int j = rng_int(0, room_count - 1);
if (i != j)
carve_connection(map, &rooms[i], &rooms[j]);
}
place_doors(map, rooms, room_count);
} }
// Place stairs in the last room (furthest from start) // Place stairs in the last room (furthest from start)
@ -447,43 +134,8 @@ static void place_stairs(Map *map, Room *rooms, int room_count) {
int cx, cy; int cx, cy;
get_room_center(last_room, &cx, &cy); get_room_center(last_room, &cx, &cy);
// Ensure stairs are placed on a floor tile, not a wall // Place stairs at center of last room
if (in_bounds(cx, cy, MAP_WIDTH, MAP_HEIGHT) && map->tiles[cy][cx] == TILE_FLOOR) {
map->tiles[cy][cx] = TILE_STAIRS;
return;
}
// 3x3 fallback
for (int dy = -1; dy <= 1; dy++) {
for (int dx = -1; dx <= 1; dx++) {
int nx = cx + dx;
int ny = cy + dy;
if (in_bounds(nx, ny, MAP_WIDTH, MAP_HEIGHT) && map->tiles[ny][nx] == TILE_FLOOR) {
map->tiles[ny][nx] = TILE_STAIRS;
return;
}
}
}
// Expanded fallback: scan the room for any floor tile
for (int dy = 0; dy < last_room->h; dy++) {
for (int dx = 0; dx < last_room->w; dx++) {
int nx = last_room->x + dx;
int ny = last_room->y + dy;
if (in_bounds(nx, ny, MAP_WIDTH, MAP_HEIGHT) && map->tiles[ny][nx] == TILE_FLOOR) {
map->tiles[ny][nx] = TILE_STAIRS;
return;
}
}
}
// Final fallback: force the center tile to stairs regardless of type
fprintf(stderr, "Warning: No floor tile found for stairs at room center (%d, %d). Forcing stairs placement.\n", cx,
cy);
if (in_bounds(cx, cy, MAP_WIDTH, MAP_HEIGHT)) { if (in_bounds(cx, cy, MAP_WIDTH, MAP_HEIGHT)) {
if (map->tiles[cy][cx] == TILE_WALL) {
map->tiles[cy][cx] = TILE_FLOOR;
}
map->tiles[cy][cx] = TILE_STAIRS; map->tiles[cy][cx] = TILE_STAIRS;
} }
} }
@ -517,121 +169,18 @@ void get_random_floor_tile(Map *map, int *x, int *y, int attempts) {
} }
} }
int get_room_floor_tile(const Map *map, const Room *room, int *x, int *y) {
*x = -1;
*y = -1;
int cx = room->x + room->w / 2;
int cy = room->y + room->h / 2;
if (in_bounds(cx, cy, MAP_WIDTH, MAP_HEIGHT) && map->tiles[cy][cx] == TILE_FLOOR) {
*x = cx;
*y = cy;
return 1;
}
for (int radius = 1; radius < room->w + room->h; radius++) {
for (int yy = cy - radius; yy <= cy + radius; yy++) {
for (int xx = cx - radius; xx <= cx + radius; xx++) {
if (!tile_in_room(room, xx, yy) || !in_bounds(xx, yy, MAP_WIDTH, MAP_HEIGHT))
continue;
if (map->tiles[yy][xx] == TILE_FLOOR) {
*x = xx;
*y = yy;
return 1;
}
}
}
}
return 0;
}
int get_random_floor_tile_excluding_room(Map *map, const Room *excluded, int *x, int *y, int attempts) {
*x = -1;
*y = -1;
for (int i = 0; i < attempts; i++) {
int tx = rng_int(1, MAP_WIDTH - 2);
int ty = rng_int(1, MAP_HEIGHT - 2);
if (map->tiles[ty][tx] == TILE_FLOOR && (excluded == NULL || !tile_in_room(excluded, tx, ty))) {
*x = tx;
*y = ty;
return 1;
}
}
for (int ty = 1; ty < MAP_HEIGHT - 1; ty++) {
for (int tx = 1; tx < MAP_WIDTH - 1; tx++) {
if (map->tiles[ty][tx] == TILE_FLOOR && (excluded == NULL || !tile_in_room(excluded, tx, ty))) {
*x = tx;
*y = ty;
return 1;
}
}
}
return 0;
}
int map_validate_layout(const Map *map) {
if (map->room_count <= 0)
return 0;
int sx, sy;
if (!get_room_floor_tile(map, &map->rooms[0], &sx, &sy))
return 0;
unsigned char visited[MAP_HEIGHT][MAP_WIDTH];
Vec2 queue[MAP_HEIGHT * MAP_WIDTH];
memset(visited, 0, sizeof(visited));
int head = 0;
int tail = 0;
queue[tail++] = (Vec2){sx, sy};
visited[sy][sx] = 1;
while (head < tail) {
Vec2 p = queue[head++];
const int dx[4] = {0, 0, 1, -1};
const int dy[4] = {1, -1, 0, 0};
for (int i = 0; i < 4; i++) {
int nx = p.x + dx[i];
int ny = p.y + dy[i];
if (!in_bounds(nx, ny, MAP_WIDTH, MAP_HEIGHT) || visited[ny][nx] || !is_floor(map, nx, ny))
continue;
visited[ny][nx] = 1;
queue[tail++] = (Vec2){nx, ny};
}
}
int stairs = 0;
for (int y = 0; y < MAP_HEIGHT; y++) {
for (int x = 0; x < MAP_WIDTH; x++) {
if (is_floor(map, x, y) && !visited[y][x])
return 0;
if (map->tiles[y][x] == TILE_STAIRS) {
if (!visited[y][x])
return 0;
stairs++;
}
}
}
return stairs == 1;
}
void dungeon_generate(Dungeon *d, Map *map, int floor_num) { void dungeon_generate(Dungeon *d, Map *map, int floor_num) {
for (int attempt = 0; attempt < 4; attempt++) { // Initialize map to all walls
map_init(map); map_init(map);
map->room_count = generate_rooms(map, map->rooms, floor_num);
connect_rooms(map, map->rooms, map->room_count);
decorate_rooms(map, map->rooms, map->room_count, floor_num);
place_stairs(map, map->rooms, map->room_count);
if (map_validate_layout(map)) // Generate rooms
break; map->room_count = generate_rooms(map, map->rooms, floor_num);
}
// Connect rooms with corridors
connect_rooms(map, map->rooms, map->room_count);
// Place stairs in last room
place_stairs(map, map->rooms, map->room_count);
// Store dungeon state // Store dungeon state
d->current_floor = floor_num; d->current_floor = floor_num;
@ -661,8 +210,7 @@ static int trace_line_of_sight(const Map *map, int x1, int y1, int x2, int y2) {
if (x == x2 && y == y2) if (x == x2 && y == y2)
return 1; return 1;
TileType t = map->tiles[y][x]; if (map->tiles[y][x] == TILE_WALL && !(x == x1 && y == y1))
if ((t == TILE_WALL || t == TILE_DOOR_CLOSED || t == TILE_STATUE) && !(x == x1 && y == y1))
return 0; return 0;
int e2 = 2 * err; int e2 = 2 * err;
@ -686,155 +234,20 @@ int has_line_of_sight(const Map *map, int x1, int y1, int x2, int y2) {
int can_see_entity(const Map *map, int from_x, int from_y, int to_x, int to_y, int range) { 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)) if (!is_in_view_range(to_x, to_y, from_x, from_y, range))
return 0; return 0;
if (!has_line_of_sight(map, from_x, from_y, to_x, to_y)) return has_line_of_sight(map, from_x, from_y, to_x, to_y);
return 0;
return tile_brightness(map, to_x, to_y) > LIGHT_SIGHT_THRESHOLD;
} }
static int is_solid(const Map *map, int sub_x, int sub_y) { void calculate_visibility(Map *map, int x, int y) {
int map_w = MAP_WIDTH; memset(map->visible, 0, sizeof(map->visible));
int map_h = MAP_HEIGHT;
int tx = sub_x / SUB_TILE_RES;
int ty = sub_y / SUB_TILE_RES;
if (tx < 0 || tx >= map_w || ty < 0 || ty >= map_h)
return 1;
TileType t = map->tiles[ty][tx];
return t == TILE_WALL || t == TILE_DOOR_CLOSED || t == TILE_STATUE;
}
static float smoothstep_light(float edge0, float edge1, float x) {
float t = (x - edge0) / (edge1 - edge0);
if (t < 0.0f)
return 0.0f;
if (t > 1.0f)
return 1.0f;
return t * t * (3.0f - 2.0f * t);
}
static int trace_sub_los(const Map *map, int sx, int sy, int tx, int ty) {
int dx = abs(tx - sx);
int dy = abs(ty - sy);
int step_x = (sx < tx) ? 1 : -1;
int step_y = (sy < ty) ? 1 : -1;
int err = dx - dy;
int x = sx, y = sy;
while (1) {
if (x == tx && y == ty)
return 1;
if (is_solid(map, x, y) && !(x == sx && y == sy))
return 0;
int e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x += step_x;
}
if (e2 < dx) {
err += dx;
y += step_y;
}
}
}
void compute_lighting(Map *map, const LightSource *sources, int num_sources) {
memset(map->light_map, 0, sizeof(map->light_map));
int map_sub_w = MAP_WIDTH * SUB_TILE_RES;
int map_sub_h = MAP_HEIGHT * SUB_TILE_RES;
for (int si = 0; si < num_sources; si++) {
int cx = sources[si].x * SUB_TILE_RES + SUB_TILE_RES / 2;
int cy = sources[si].y * SUB_TILE_RES + SUB_TILE_RES / 2;
int range_sub = sources[si].range * SUB_TILE_RES;
int intensity = sources[si].intensity;
for (int dy = -range_sub; dy <= range_sub; dy++) {
for (int dx = -range_sub; dx <= range_sub; dx++) {
int sub_x = cx + dx;
int sub_y = cy + dy;
if (sub_x < 0 || sub_x >= map_sub_w || sub_y < 0 || sub_y >= map_sub_h)
continue;
int dist_sq = dx * dx + dy * dy;
if (dist_sq > range_sub * range_sub)
continue;
if (!trace_sub_los(map, cx, cy, sub_x, sub_y))
continue;
float dist = sqrtf((float)dist_sq);
float t = dist / (float)range_sub;
float brightness = 1.0f - smoothstep_light(0.35f, 1.0f, t);
int val = (int)(brightness * (float)intensity);
if (val > map->light_map[sub_y][sub_x])
map->light_map[sub_y][sub_x] = (unsigned char)val;
}
}
int src_x = sources[si].x;
int src_y = sources[si].y;
int src_range = sources[si].range;
int src_intensity = sources[si].intensity;
for (int ty = 0; ty < MAP_HEIGHT; ty++) {
for (int tx = 0; tx < MAP_WIDTH; tx++) {
if (map->tiles[ty][tx] != TILE_WALL && map->tiles[ty][tx] != TILE_DOOR_CLOSED)
continue;
if (!is_in_view_range(tx, ty, src_x, src_y, src_range))
continue;
if (!has_line_of_sight(map, src_x, src_y, tx, ty))
continue;
int dx = tx - src_x;
int dy = ty - src_y;
float t = sqrtf((float)(dx * dx + dy * dy)) / (float)src_range;
float fb = 1.0f - smoothstep_light(0.35f, 1.0f, t);
int val = (int)(fb * (float)src_intensity);
int base_x = tx * SUB_TILE_RES;
int base_y = ty * SUB_TILE_RES;
for (int cy2 = 0; cy2 < SUB_TILE_RES; cy2++) {
for (int cx2 = 0; cx2 < SUB_TILE_RES; cx2++) {
if (val > map->light_map[base_y + cy2][base_x + cx2])
map->light_map[base_y + cy2][base_x + cx2] = (unsigned char)val;
}
}
}
}
}
for (int ty = 0; ty < MAP_HEIGHT; ty++) { for (int ty = 0; ty < MAP_HEIGHT; ty++) {
for (int tx = 0; tx < MAP_WIDTH; tx++) { for (int tx = 0; tx < MAP_WIDTH; tx++) {
int max_bright = 0; if (is_in_view_range(tx, ty, x, y, PLAYER_VIEW_RANGE)) {
int bx = tx * SUB_TILE_RES; if (has_line_of_sight(map, x, y, tx, ty)) {
int by = ty * SUB_TILE_RES; map->visible[ty][tx] = 1;
for (int dy = 0; dy < SUB_TILE_RES; dy++) { map->remembered[ty][tx] = 1;
for (int dx = 0; dx < SUB_TILE_RES; dx++) {
int v = map->light_map[by + dy][bx + dx];
if (v > max_bright)
max_bright = v;
} }
} }
if (max_bright > LIGHT_SIGHT_THRESHOLD)
map->remembered[ty][tx] = 1;
} }
} }
} }
int tile_brightness(const Map *map, int tx, int ty) {
int sum = 0;
int base_x = tx * SUB_TILE_RES;
int base_y = ty * SUB_TILE_RES;
for (int dy = 0; dy < SUB_TILE_RES; dy++) {
for (int dx = 0; dx < SUB_TILE_RES; dx++) {
sum += map->light_map[base_y + dy][base_x + dx];
}
}
return sum / (SUB_TILE_RES * SUB_TILE_RES);
}
int is_tile_revealed(const Map *map, int tx, int ty) {
return tile_brightness(map, tx, ty) > LIGHT_SIGHT_THRESHOLD;
}

View file

@ -17,17 +17,11 @@ 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);
int get_room_floor_tile(const Map *map, const Room *room, int *x, int *y);
int get_random_floor_tile_excluding_room(Map *map, const Room *excluded, int *x, int *y, int attempts);
Room *map_room_at(Map *map, int x, int y);
int map_validate_layout(const Map *map);
// Visibility / Fog of War // Visibility / Fog of War
int is_in_view_range(int x, int y, int view_x, int view_y, int range); 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); int has_line_of_sight(const Map *map, int x1, int y1, int x2, int y2);
void compute_lighting(Map *map, const LightSource *sources, int num_sources); void calculate_visibility(Map *map, int x, int y);
int tile_brightness(const Map *map, int tx, int ty);
int is_tile_revealed(const Map *map, int tx, int ty);
int can_see_entity(const Map *map, int from_x, int from_y, int to_x, int to_y, int range); 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,118 +0,0 @@
#include "tileset.h"
#include <stddef.h>
#include <stdio.h>
#include <string.h>
int tileset_init(Tileset *ts, int tile_w, int tile_h) {
if (ts == NULL)
return 0;
if (tile_w <= 0 || tile_h <= 0)
return 0;
memset(ts, 0, sizeof(Tileset));
ts->tile_w = tile_w;
ts->tile_h = tile_h;
// Compute grid dimensions to fit MAX_TILE_ID tiles
ts->atlas_cols = 4; // 4 columns
ts->atlas_rows = (MAX_TILE_ID + ts->atlas_cols - 1) / ts->atlas_cols; // round up
int atlas_w = ts->atlas_cols * tile_w;
int atlas_h = ts->atlas_rows * tile_h;
// Validate atlas dimensions are reasonable
if (atlas_w <= 0 || atlas_h <= 0 || atlas_w > 4096 || atlas_h > 4096)
return 0;
ts->render_target = LoadRenderTexture(atlas_w, atlas_h);
if (!IsRenderTextureValid(ts->render_target))
return 0;
// Clear to transparent so unpainted regions don't show artifacts
BeginTextureMode(ts->render_target);
ClearBackground(BLANK);
EndTextureMode();
ts->finalized = 0;
ts->tile_count = 0;
return 1;
}
int tileset_register(Tileset *ts, int id) {
if (ts == NULL)
return 0;
if (id < 0 || id >= MAX_TILE_ID)
return 0;
if (ts->render_target.id == 0)
return 0;
if (ts->finalized)
return 0;
if (ts->regions[id].width != 0)
return 0; // already registered
int col = id % ts->atlas_cols;
int row = id / ts->atlas_cols;
ts->regions[id] =
(Rectangle){(float)(col * ts->tile_w), (float)(row * ts->tile_h), (float)ts->tile_w, (float)ts->tile_h};
ts->tile_count++;
return 1;
}
int tileset_finalize(Tileset *ts) {
if (ts == NULL)
return 0;
if (ts->render_target.id == 0)
return 0;
if (ts->finalized)
return 1; // already finalized
// Convert RenderTexture to regular Texture2D
// RenderTexture textures are flipped vertically in raylib, so we need to handle that
Texture2D old_texture = ts->render_target.texture;
// Create a new texture from the render texture data
Image img = LoadImageFromTexture(old_texture);
if (img.data == NULL) {
return 0;
}
// Flip image vertically because RenderTexture is upside-down
ImageFlipVertical(&img);
Texture2D new_tex = LoadTextureFromImage(img);
UnloadImage(img);
if (new_tex.id == 0) {
return 0;
}
// Unload the old render texture and replace with the new regular texture
UnloadRenderTexture(ts->render_target);
ts->render_target.id = 0;
ts->atlas = new_tex;
ts->finalized = 1;
return 1;
}
Rectangle tileset_get_region(const Tileset *ts, int id) {
if (ts == NULL || id < 0 || id >= MAX_TILE_ID)
return (Rectangle){0, 0, 0, 0};
if (!ts->finalized)
return (Rectangle){0, 0, 0, 0};
return ts->regions[id];
}
void tileset_destroy(Tileset *ts) {
if (ts == NULL)
return;
if (ts->finalized) {
if (ts->atlas.id != 0)
UnloadTexture(ts->atlas);
} else {
if (ts->render_target.id != 0)
UnloadRenderTexture(ts->render_target);
}
memset(ts, 0, sizeof(Tileset));
}

View file

@ -1,91 +0,0 @@
#ifndef TILESET_H
#define TILESET_H
#include "settings.h"
#include <raylib.h>
// Maximum number of tiles that can be registered in a single atlas
#define MAX_TILE_ID 32
// Tile IDs for map tiles (variants for visual variety)
#define TILE_WALL_0 0
#define TILE_WALL_1 1
#define TILE_FLOOR_0 2
#define TILE_FLOOR_1 3
#define TILE_FLOOR_2 4
#define TILE_FLOOR_3 5
#define TILE_STAIRS_SPRITE 6
// Sprite IDs for entities
#define SPRITE_PLAYER 7
#define SPRITE_PLAYER_WALK_0 8
#define SPRITE_PLAYER_WALK_1 9
#define SPRITE_PLAYER_ATTACK 10
#define SPRITE_ENEMY_GOBLIN 11
#define SPRITE_ENEMY_GOBLIN_WALK_0 12
#define SPRITE_ENEMY_GOBLIN_WALK_1 13
#define SPRITE_ENEMY_GOBLIN_ATTACK 14
#define SPRITE_ENEMY_SKELETON 15
#define SPRITE_ENEMY_SKELETON_WALK_0 16
#define SPRITE_ENEMY_SKELETON_WALK_1 17
#define SPRITE_ENEMY_SKELETON_ATTACK 18
#define SPRITE_ENEMY_ORC 19
#define SPRITE_ENEMY_ORC_WALK_0 20
#define SPRITE_ENEMY_ORC_WALK_1 21
#define SPRITE_ENEMY_ORC_ATTACK 22
#define SPRITE_ITEM_POTION 23
#define SPRITE_ITEM_WEAPON 24
#define SPRITE_ITEM_ARMOR 25
// Door tiles
#define TILE_DOOR_CLOSED_SPRITE 26
#define TILE_DOOR_OPEN_SPRITE 27
// Effect/status sprites
#define SPRITE_EFFECT_BURN 28
#define SPRITE_EFFECT_POISON 29
#define SPRITE_EFFECT_BLOCK 30
#define SPRITE_SLASH_EFFECT 31
// Total count of defined tiles
#define NUM_TILE_IDS 32
// Tileset encapsulates a GPU texture atlas with sub-rectangle regions per tile ID.
// The atlas is built at startup by painting into a RenderTexture, then finalized
// into a regular Texture2D for efficient drawing via DrawTexturePro.
typedef struct {
RenderTexture2D render_target; // RenderTexture for painting (valid before finalize)
Texture2D atlas; // GPU texture (valid after finalize)
int tile_w; // width of each tile in pixels
int tile_h; // height of each tile in pixels
Rectangle regions[MAX_TILE_ID]; // sub-rectangles within atlas for each tile ID
int tile_count; // number of registered tiles
int atlas_cols; // number of columns in the atlas grid
int atlas_rows; // number of rows in the atlas grid
int finalized; // 1 after tileset_finalize called, 0 otherwise
} Tileset;
// Initialize a tileset with the given tile dimensions.
// Computes atlas grid size based on MAX_TILE_ID and allocates a RenderTexture.
// Returns 0 on failure (e.g., RenderTexture allocation failed), non-zero on success.
int tileset_init(Tileset *ts, int tile_w, int tile_h);
// Register a tile ID with its atlas region.
// The region is computed automatically based on tile_w/tile_h and the ID index.
// Returns 0 if the ID is out of bounds or already registered, non-zero on success.
int tileset_register(Tileset *ts, int id);
// Finalize the tileset: converts the internal RenderTexture into a regular Texture2D
// suitable for DrawTexturePro. After this call, painting functions must not be used.
// Returns 0 on failure, non-zero on success.
int tileset_finalize(Tileset *ts);
// Get the atlas sub-rectangle for a given tile ID.
// Returns a zeroed Rectangle if the ID is invalid or not registered.
Rectangle tileset_get_region(const Tileset *ts, int id);
// Destroy a tileset, unloading the atlas texture and zeroing the struct.
// Safe to call on a zero-initialized or already-destroyed tileset.
void tileset_destroy(Tileset *ts);
#endif // TILESET_H

View file

@ -1,892 +0,0 @@
#include "tileset_paint.h"
#include <stddef.h>
#include <stdlib.h>
// Simple LCG for deterministic noise (seeded by position)
static unsigned int lcg_seed = 0;
static void lcg_srand(unsigned int seed) {
lcg_seed = seed;
}
static unsigned int lcg_rand(void) {
lcg_seed = lcg_seed * 1103515245 + 12345;
return lcg_seed;
}
static int lcg_rand_range(int min, int max) {
if (max <= min)
return min;
return min + (int)(lcg_rand() % (unsigned int)(max - min + 1));
}
// Get the RenderTexture target for painting a specific tile ID
static RenderTexture2D get_target(Tileset *ts) {
return ts->render_target;
}
// Compute screen offset for a tile ID within the atlas
static Vector2 get_paint_offset(Tileset *ts, int id) {
int col = id % ts->atlas_cols;
int row = id / ts->atlas_cols;
return (Vector2){(float)(col * ts->tile_w), (float)(row * ts->tile_h)};
}
void paint_wall_tile(Tileset *ts, int id, int variant) {
if (ts == NULL || id < 0 || id >= MAX_TILE_ID)
return;
if (ts->render_target.id == 0 || ts->finalized)
return;
Vector2 off = get_paint_offset(ts, id);
int w = ts->tile_w;
int h = ts->tile_h;
BeginTextureMode(ts->render_target);
int ox = (int)off.x;
int oy = (int)off.y;
Color base = variant == 0 ? (Color){38, 37, 39, 255} : (Color){33, 32, 35, 255};
Color edge = variant == 0 ? (Color){48, 46, 46, 255} : (Color){42, 40, 41, 255};
Color shadow = variant == 0 ? (Color){24, 23, 25, 255} : (Color){20, 19, 21, 255};
Color highlight = variant == 0 ? (Color){56, 54, 52, 255} : (Color){49, 47, 46, 255};
Color moss = variant == 0 ? (Color){38, 58, 42, 255} : (Color){28, 45, 34, 255};
DrawRectangle(ox, oy, w, h, base);
lcg_srand((unsigned int)(variant * 4099 + id * 9176));
int row_y = 0;
for (int row = 0; row < 3; row++) {
int block_h = row == 2 ? h - row_y : lcg_rand_range(5, 6);
int x = 0;
int stagger = (row % 2) ? lcg_rand_range(-4, 0) : lcg_rand_range(-1, 2);
while (x < w) {
int block_w = lcg_rand_range(7, 12);
int bx = ox + x + stagger;
int by = oy + row_y;
int bw = block_w;
if (bx < ox) {
bw -= ox - bx;
bx = ox;
}
if (bx + bw > ox + w)
bw = ox + w - bx;
if (bw > 0) {
Color face = ((x + row + variant) % 3 == 0) ? edge : base;
DrawRectangle(bx, by, bw, block_h, face);
if (row == 0)
DrawLine(bx, by, bx + bw - 1, by, highlight);
DrawLine(bx, by + block_h - 1, bx + bw - 1, by + block_h - 1, shadow);
if (bw > 5 && block_h > 4 && lcg_rand_range(0, 99) < 18)
DrawPixel(bx + lcg_rand_range(1, bw - 2), by + lcg_rand_range(1, block_h - 2), shadow);
}
x += block_w;
}
row_y += block_h;
}
for (int i = 0; i < 3 + variant; i++) {
int px = ox + lcg_rand_range(1, w - 2);
int py = oy + lcg_rand_range(1, h - 2);
DrawPixel(px, py, lcg_rand_range(0, 1) ? highlight : shadow);
}
if (variant == 1) {
DrawLine(ox + 3, oy + 1, ox + 5, oy + 5, shadow);
DrawLine(ox + 5, oy + 5, ox + 4, oy + 8, shadow);
DrawLine(ox + 11, oy + 4, ox + 8, oy + 7, shadow);
} else {
DrawLine(ox + 9, oy + 2, ox + 12, oy + 5, shadow);
DrawLine(ox + 12, oy + 5, ox + 10, oy + 9, shadow);
DrawPixel(ox + 3, oy + 13, moss);
DrawPixel(ox + 4, oy + 13, moss);
DrawPixel(ox + 4, oy + 14, moss);
}
DrawRectangle(ox, oy + h - 2, w, 2, (Color){16, 15, 17, 255});
EndTextureMode();
}
void paint_floor_tile(Tileset *ts, int id, int variant) {
if (ts == NULL || id < 0 || id >= MAX_TILE_ID)
return;
if (ts->render_target.id == 0 || ts->finalized)
return;
Vector2 off = get_paint_offset(ts, id);
int w = ts->tile_w;
int h = ts->tile_h;
BeginTextureMode(ts->render_target);
int ox = (int)off.x;
int oy = (int)off.y;
Color base = variant == 0 ? (Color){61, 58, 55, 255}
: variant == 1 ? (Color){67, 63, 58, 255}
: variant == 2 ? (Color){56, 55, 58, 255}
: (Color){64, 59, 53, 255};
Color bevel = (Color){86, 82, 75, 255};
Color seam = (Color){34, 32, 31, 255};
Color chip = (Color){43, 41, 40, 255};
DrawRectangle(ox, oy, w, h, base);
lcg_srand((unsigned int)(variant * 7919 + id * 104729));
int split_x = lcg_rand_range(6, 10);
int split_y = lcg_rand_range(6, 10);
DrawLine(ox + split_x, oy + 1, ox + split_x, oy + h - 2, seam);
DrawLine(ox + 1, oy + split_y, ox + w - 2, oy + split_y, seam);
DrawLine(ox + 1, oy + 1, ox + w - 2, oy + 1, bevel);
DrawLine(ox + 1, oy + 1, ox + 1, oy + h - 2, bevel);
DrawLine(ox + 1, oy + h - 2, ox + w - 2, oy + h - 2, seam);
DrawLine(ox + w - 2, oy + 1, ox + w - 2, oy + h - 2, seam);
int num_dots = 14 + variant * 5;
for (int i = 0; i < num_dots; i++) {
int px = ox + lcg_rand_range(1, w - 2);
int py = oy + lcg_rand_range(1, h - 2);
Color c = lcg_rand_range(0, 2) == 0 ? bevel : chip;
DrawPixel(px, py, c);
}
if (variant >= 1) {
int crack_x = ox + lcg_rand_range(3, w - 4);
int crack_y = oy + lcg_rand_range(3, h - 4);
DrawPixel(crack_x, crack_y, seam);
DrawPixel(crack_x + 1, crack_y, seam);
DrawPixel(crack_x + 1, crack_y + 1, seam);
if (variant >= 2)
DrawPixel(crack_x + 2, crack_y + 2, seam);
}
EndTextureMode();
}
void paint_stairs_tile(Tileset *ts, int id) {
if (ts == NULL || id < 0 || id >= MAX_TILE_ID)
return;
if (ts->render_target.id == 0 || ts->finalized)
return;
Vector2 off = get_paint_offset(ts, id);
int w = ts->tile_w;
int h = ts->tile_h;
BeginTextureMode(ts->render_target);
int ox = (int)off.x;
int oy = (int)off.y;
DrawRectangle(ox, oy, w, h, (Color){31, 29, 27, 255});
Color face = (Color){82, 76, 68, 255};
Color lip = (Color){118, 106, 84, 255};
Color drop = (Color){28, 25, 23, 255};
for (int i = 0; i < 4; i++) {
int y = oy + 3 + i * 3;
int inset = i + 1;
DrawRectangle(ox + inset, y, w - inset * 2, 2, face);
DrawLine(ox + inset, y, ox + w - inset - 1, y, lip);
DrawLine(ox + inset, y + 2, ox + w - inset - 1, y + 2, drop);
}
DrawLine(ox + 4, oy + 2, ox + 11, oy + 2, (Color){120, 111, 93, 255});
DrawPixel(ox + 7, oy + 1, (Color){142, 134, 114, 255});
DrawPixel(ox + 8, oy + 1, (Color){142, 134, 114, 255});
DrawRectangle(ox + 6, oy + 13, 4, 1, (Color){12, 10, 9, 255});
EndTextureMode();
}
static void draw_player_base(Tileset *ts, int id, int leg_offset_left, int leg_offset_right, int arm_offset_left,
int arm_offset_right) {
if (ts == NULL || id < 0 || id >= MAX_TILE_ID)
return;
if (ts->render_target.id == 0 || ts->finalized)
return;
Vector2 off = get_paint_offset(ts, id);
int w = ts->tile_w;
int h = ts->tile_h;
int ox = (int)off.x;
int oy = (int)off.y;
BeginTextureMode(ts->render_target);
// Clear to transparent
DrawRectangle(ox, oy, w, h, BLANK);
// Simple adventurer silhouette (16x16)
Color skin = (Color){230, 200, 160, 255};
Color tunic = (Color){60, 100, 180, 255};
Color pants = (Color){80, 60, 40, 255};
Color boots = (Color){50, 40, 30, 255};
Color hair = (Color){120, 80, 40, 255};
// Head (3x3)
DrawRectangle(ox + 6, oy + 2, 4, 3, skin);
// Hair
DrawRectangle(ox + 6, oy + 1, 4, 1, hair);
DrawPixel(ox + 5, oy + 2, hair);
DrawPixel(ox + 10, oy + 2, hair);
// Body/tunic (4x5)
DrawRectangle(ox + 5, oy + 5, 6, 5, tunic);
// Belt
DrawRectangle(ox + 5, oy + 9, 6, 1, (Color){120, 80, 30, 255});
// Legs with offset for walking animation
DrawRectangle(ox + 6 + leg_offset_left, oy + 10, 2, 4, pants);
DrawRectangle(ox + 8 + leg_offset_right, oy + 10, 2, 4, pants);
// Boots with offset
DrawRectangle(ox + 6 + leg_offset_left, oy + 14, 2, 2, boots);
DrawRectangle(ox + 8 + leg_offset_right, oy + 14, 2, 2, boots);
// Arms with offset
DrawRectangle(ox + 3 + arm_offset_left, oy + 6, 2, 3, skin);
DrawRectangle(ox + 11 + arm_offset_right, oy + 6, 2, 3, skin);
EndTextureMode();
}
void paint_player_tile(Tileset *ts, int id) {
// Idle pose - no offsets
draw_player_base(ts, id, 0, 0, 0, 0);
}
void paint_player_walk_tile(Tileset *ts, int id, int frame) {
// Frame 0: left leg forward, right leg back
// Frame 1: right leg forward, left leg back
int leg_left = (frame == 0) ? -1 : 1;
int leg_right = (frame == 0) ? 1 : -1;
int arm_left = (frame == 0) ? 1 : -1;
int arm_right = (frame == 0) ? -1 : 1;
draw_player_base(ts, id, leg_left, leg_right, arm_left, arm_right);
}
void paint_player_attack_tile(Tileset *ts, int id) {
if (ts == NULL || id < 0 || id >= MAX_TILE_ID)
return;
if (ts->render_target.id == 0 || ts->finalized)
return;
Vector2 off = get_paint_offset(ts, id);
int w = ts->tile_w;
int h = ts->tile_h;
int ox = (int)off.x;
int oy = (int)off.y;
BeginTextureMode(ts->render_target);
// Clear to transparent
DrawRectangle(ox, oy, w, h, BLANK);
// Attack pose - lunging forward with sword arm extended
Color skin = (Color){230, 200, 160, 255};
Color tunic = (Color){60, 100, 180, 255};
Color pants = (Color){80, 60, 40, 255};
Color boots = (Color){50, 40, 30, 255};
Color hair = (Color){120, 80, 40, 255};
Color steel = (Color){180, 180, 190, 255};
// Head (3x3)
DrawRectangle(ox + 7, oy + 2, 4, 3, skin);
// Hair
DrawRectangle(ox + 7, oy + 1, 4, 1, hair);
DrawPixel(ox + 6, oy + 2, hair);
DrawPixel(ox + 11, oy + 2, hair);
// Body/tunic (4x5) - shifted right for lunge
DrawRectangle(ox + 6, oy + 5, 6, 5, tunic);
// Belt
DrawRectangle(ox + 6, oy + 9, 6, 1, (Color){120, 80, 30, 255});
// Legs - left forward, right back
DrawRectangle(ox + 5, oy + 10, 2, 4, pants);
DrawRectangle(ox + 9, oy + 10, 2, 4, pants);
// Boots
DrawRectangle(ox + 5, oy + 14, 2, 2, boots);
DrawRectangle(ox + 9, oy + 14, 2, 2, boots);
// Left arm (back)
DrawRectangle(ox + 4, oy + 6, 2, 3, skin);
// Right arm extended forward with sword
DrawRectangle(ox + 12, oy + 6, 3, 2, skin);
// Sword blade
DrawRectangle(ox + 14, oy + 4, 1, 6, steel);
// Sword hilt
DrawRectangle(ox + 13, oy + 7, 3, 1, (Color){100, 80, 40, 255});
EndTextureMode();
}
// Draw goblin base with configurable leg/arm offsets
static void draw_goblin_base(Tileset *ts, int id, int leg_left, int leg_right, int arm_left, int arm_right) {
if (ts == NULL || id < 0 || id >= MAX_TILE_ID)
return;
if (ts->render_target.id == 0 || ts->finalized)
return;
Vector2 off = get_paint_offset(ts, id);
int ox = (int)off.x;
int oy = (int)off.y;
Color skin = (Color){80, 140, 60, 255};
Color dark = (Color){50, 90, 35, 255};
BeginTextureMode(ts->render_target);
DrawRectangle(ox, oy, ts->tile_w, ts->tile_h, BLANK);
// Head (large, hunched forward)
DrawRectangle(ox + 4, oy + 3, 8, 6, skin);
// Eyes (angry)
DrawPixel(ox + 5, oy + 5, (Color){200, 50, 50, 255});
DrawPixel(ox + 10, oy + 5, (Color){200, 50, 50, 255});
// Ears (pointy)
DrawPixel(ox + 3, oy + 4, skin);
DrawPixel(ox + 12, oy + 4, skin);
// Body (small, hunched)
DrawRectangle(ox + 5, oy + 9, 6, 4, dark);
// Legs with offsets
DrawRectangle(ox + 5 + leg_left, oy + 13, 2, 3, skin);
DrawRectangle(ox + 9 + leg_right, oy + 13, 2, 3, skin);
// Arms with offsets
DrawRectangle(ox + 3 + arm_left, oy + 10, 2, 2, skin);
DrawRectangle(ox + 11 + arm_right, oy + 10, 2, 2, skin);
EndTextureMode();
}
// Draw skeleton base with configurable leg/arm offsets
static void draw_skeleton_base(Tileset *ts, int id, int leg_left, int leg_right, int arm_left, int arm_right) {
if (ts == NULL || id < 0 || id >= MAX_TILE_ID)
return;
if (ts->render_target.id == 0 || ts->finalized)
return;
Vector2 off = get_paint_offset(ts, id);
int ox = (int)off.x;
int oy = (int)off.y;
Color bone = (Color){220, 220, 210, 255};
Color dark_bone = (Color){180, 180, 170, 255};
BeginTextureMode(ts->render_target);
DrawRectangle(ox, oy, ts->tile_w, ts->tile_h, BLANK);
// Skull
DrawRectangle(ox + 5, oy + 2, 6, 5, bone);
// Eye sockets
DrawPixel(ox + 6, oy + 4, (Color){20, 20, 20, 255});
DrawPixel(ox + 9, oy + 4, (Color){20, 20, 20, 255});
// Ribs
for (int i = 0; i < 3; i++) {
DrawRectangle(ox + 4, oy + 8 + i * 2, 8, 1, bone);
}
// Spine
DrawRectangle(ox + 7, oy + 7, 2, 6, dark_bone);
// Legs with offsets
DrawRectangle(ox + 5 + leg_left, oy + 13, 2, 3, bone);
DrawRectangle(ox + 9 + leg_right, oy + 13, 2, 3, bone);
// Arms with offsets
DrawRectangle(ox + 3 + arm_left, oy + 8, 2, 3, bone);
DrawRectangle(ox + 11 + arm_right, oy + 8, 2, 3, bone);
EndTextureMode();
}
// Draw orc base with configurable leg/arm offsets
static void draw_orc_base(Tileset *ts, int id, int leg_left, int leg_right, int arm_left, int arm_right) {
if (ts == NULL || id < 0 || id >= MAX_TILE_ID)
return;
if (ts->render_target.id == 0 || ts->finalized)
return;
Vector2 off = get_paint_offset(ts, id);
int ox = (int)off.x;
int oy = (int)off.y;
Color skin = (Color){60, 100, 45, 255};
Color dark = (Color){40, 70, 30, 255};
BeginTextureMode(ts->render_target);
DrawRectangle(ox, oy, ts->tile_w, ts->tile_h, BLANK);
// Large head
DrawRectangle(ox + 3, oy + 2, 10, 7, skin);
// Small angry eyes
DrawPixel(ox + 5, oy + 5, (Color){250, 250, 50, 255});
DrawPixel(ox + 10, oy + 5, (Color){250, 250, 50, 255});
// Tusks
DrawPixel(ox + 6, oy + 7, (Color){240, 240, 220, 255});
DrawPixel(ox + 9, oy + 7, (Color){240, 240, 220, 255});
// Broad body
DrawRectangle(ox + 3, oy + 9, 10, 5, dark);
// Thick legs with offsets
DrawRectangle(ox + 4 + leg_left, oy + 14, 3, 2, skin);
DrawRectangle(ox + 9 + leg_right, oy + 14, 3, 2, skin);
// Thick arms with offsets
DrawRectangle(ox + 1 + arm_left, oy + 10, 3, 3, skin);
DrawRectangle(ox + 12 + arm_right, oy + 10, 3, 3, skin);
EndTextureMode();
}
void paint_enemy_tile(Tileset *ts, int id, int enemy_type) {
// Idle pose - no offsets
switch (enemy_type) {
case 0:
draw_goblin_base(ts, id, 0, 0, 0, 0);
break;
case 1:
draw_skeleton_base(ts, id, 0, 0, 0, 0);
break;
case 2:
draw_orc_base(ts, id, 0, 0, 0, 0);
break;
default:
break;
}
}
void paint_enemy_walk_tile(Tileset *ts, int id, int enemy_type, int frame) {
// Frame 0: left leg forward, right leg back
// Frame 1: right leg forward, left leg back
int leg_left = (frame == 0) ? -1 : 1;
int leg_right = (frame == 0) ? 1 : -1;
int arm_left = (frame == 0) ? 1 : -1;
int arm_right = (frame == 0) ? -1 : 1;
switch (enemy_type) {
case 0:
draw_goblin_base(ts, id, leg_left, leg_right, arm_left, arm_right);
break;
case 1:
draw_skeleton_base(ts, id, leg_left, leg_right, arm_left, arm_right);
break;
case 2:
draw_orc_base(ts, id, leg_left, leg_right, arm_left, arm_right);
break;
default:
break;
}
}
void paint_enemy_attack_tile(Tileset *ts, int id, int enemy_type) {
if (ts == NULL || id < 0 || id >= MAX_TILE_ID)
return;
if (ts->render_target.id == 0 || ts->finalized)
return;
Vector2 off = get_paint_offset(ts, id);
int ox = (int)off.x;
int oy = (int)off.y;
BeginTextureMode(ts->render_target);
DrawRectangle(ox, oy, ts->tile_w, ts->tile_h, BLANK);
switch (enemy_type) {
case 0: {
// Goblin attack - lunging with dagger
Color skin = (Color){80, 140, 60, 255};
Color dark = (Color){50, 90, 35, 255};
Color steel = (Color){180, 180, 190, 255};
// Head (leaning forward)
DrawRectangle(ox + 6, oy + 3, 8, 6, skin);
DrawPixel(ox + 7, oy + 5, (Color){200, 50, 50, 255});
DrawPixel(ox + 12, oy + 5, (Color){200, 50, 50, 255});
// Body
DrawRectangle(ox + 7, oy + 9, 6, 4, dark);
// Left leg back
DrawRectangle(ox + 5, oy + 13, 2, 3, skin);
// Right leg forward
DrawRectangle(ox + 11, oy + 13, 2, 3, skin);
// Left arm back
DrawRectangle(ox + 4, oy + 10, 2, 2, skin);
// Right arm extended with dagger
DrawRectangle(ox + 14, oy + 9, 2, 2, skin);
DrawRectangle(ox + 15, oy + 7, 1, 4, steel);
break;
}
case 1: {
// Skeleton attack - swinging sword
Color bone = (Color){220, 220, 210, 255};
Color dark_bone = (Color){180, 180, 170, 255};
Color steel = (Color){180, 180, 190, 255};
// Skull
DrawRectangle(ox + 6, oy + 2, 6, 5, bone);
DrawPixel(ox + 7, oy + 4, (Color){20, 20, 20, 255});
DrawPixel(ox + 10, oy + 4, (Color){20, 20, 20, 255});
// Ribs
for (int i = 0; i < 3; i++) {
DrawRectangle(ox + 5, oy + 8 + i * 2, 8, 1, bone);
}
// Spine
DrawRectangle(ox + 8, oy + 7, 2, 6, dark_bone);
// Legs
DrawRectangle(ox + 6, oy + 13, 2, 3, bone);
DrawRectangle(ox + 10, oy + 13, 2, 3, bone);
// Left arm back
DrawRectangle(ox + 4, oy + 8, 2, 3, bone);
// Right arm extended with sword
DrawRectangle(ox + 13, oy + 7, 3, 2, bone);
DrawRectangle(ox + 15, oy + 5, 1, 6, steel);
break;
}
case 2: {
// Orc attack - overhead smash
Color skin = (Color){60, 100, 45, 255};
Color dark = (Color){40, 70, 30, 255};
Color steel = (Color){180, 180, 190, 255};
// Head
DrawRectangle(ox + 4, oy + 2, 10, 7, skin);
DrawPixel(ox + 6, oy + 5, (Color){250, 250, 50, 255});
DrawPixel(ox + 11, oy + 5, (Color){250, 250, 50, 255});
// Tusks
DrawPixel(ox + 7, oy + 7, (Color){240, 240, 220, 255});
DrawPixel(ox + 10, oy + 7, (Color){240, 240, 220, 255});
// Body
DrawRectangle(ox + 4, oy + 9, 10, 5, dark);
// Legs
DrawRectangle(ox + 5, oy + 14, 3, 2, skin);
DrawRectangle(ox + 10, oy + 14, 3, 2, skin);
// Left arm back
DrawRectangle(ox + 2, oy + 10, 3, 3, skin);
// Right arm raised with club
DrawRectangle(ox + 13, oy + 4, 3, 3, skin);
DrawRectangle(ox + 14, oy + 1, 2, 5, steel);
break;
}
default:
break;
}
EndTextureMode();
}
void paint_item_tile(Tileset *ts, int id, int item_type) {
if (ts == NULL || id < 0 || id >= MAX_TILE_ID)
return;
if (ts->render_target.id == 0 || ts->finalized)
return;
Vector2 off = get_paint_offset(ts, id);
int w = ts->tile_w;
int h = ts->tile_h;
int ox = (int)off.x;
int oy = (int)off.y;
BeginTextureMode(ts->render_target);
DrawRectangle(ox, oy, w, h, BLANK);
switch (item_type) {
case 0: {
// Flask shape
Color glass = (Color){200, 60, 60, 255};
Color liquid = (Color){255, 80, 80, 255};
Color highlight = (Color){255, 150, 150, 255};
// Neck
DrawRectangle(ox + 6, oy + 2, 4, 3, glass);
// Body
DrawRectangle(ox + 4, oy + 5, 8, 8, liquid);
// Cork
DrawRectangle(ox + 6, oy + 1, 4, 1, (Color){160, 120, 60, 255});
// Highlight
DrawPixel(ox + 5, oy + 6, highlight);
DrawPixel(ox + 5, oy + 7, highlight);
break;
}
case 1: {
// Sword
Color blade = (Color){220, 220, 230, 255};
Color hilt = (Color){160, 120, 40, 255};
Color guard = (Color){140, 140, 150, 255};
// Blade
DrawRectangle(ox + 7, oy + 2, 2, 9, blade);
// Tip
DrawPixel(ox + 7, oy + 1, blade);
DrawPixel(ox + 8, oy + 1, blade);
// Guard
DrawRectangle(ox + 5, oy + 11, 6, 1, guard);
// Hilt
DrawRectangle(ox + 7, oy + 12, 2, 3, hilt);
// Pommel
DrawPixel(ox + 7, oy + 15, guard);
DrawPixel(ox + 8, oy + 15, guard);
break;
}
case 2: {
// Chestplate
Color metal = (Color){100, 120, 160, 255};
Color dark_metal = (Color){70, 85, 115, 255};
Color highlight = (Color){140, 160, 200, 255};
// Main plate
DrawRectangle(ox + 4, oy + 3, 8, 9, metal);
// Collar
DrawRectangle(ox + 5, oy + 2, 6, 1, dark_metal);
// Vertical ridge
DrawRectangle(ox + 7, oy + 3, 2, 9, highlight);
// Bottom trim
DrawRectangle(ox + 4, oy + 11, 8, 1, dark_metal);
break;
}
default:
break;
}
EndTextureMode();
}
void paint_door_closed_tile(Tileset *ts, int id) {
if (ts == NULL || id < 0 || id >= MAX_TILE_ID)
return;
if (ts->render_target.id == 0 || ts->finalized)
return;
Vector2 off = get_paint_offset(ts, id);
int ox = (int)off.x;
int oy = (int)off.y;
int w = ts->tile_w;
int h = ts->tile_h;
BeginTextureMode(ts->render_target);
Color iron = (Color){35, 32, 30, 255};
Color wood_dark = (Color){72, 47, 30, 255};
Color wood_mid = (Color){111, 73, 42, 255};
Color wood_light = (Color){151, 103, 58, 255};
DrawRectangle(ox, oy, w, h, iron);
DrawRectangle(ox + 2, oy + 1, w - 4, h - 2, wood_dark);
DrawRectangle(ox + 3, oy + 2, 3, h - 4, wood_mid);
DrawRectangle(ox + 7, oy + 2, 3, h - 4, wood_light);
DrawRectangle(ox + 11, oy + 2, 2, h - 4, wood_mid);
DrawRectangle(ox + 2, oy + 5, w - 4, 2, iron);
DrawRectangle(ox + 2, oy + 11, w - 4, 2, iron);
DrawPixel(ox + 4, oy + 3, (Color){180, 129, 72, 255});
DrawPixel(ox + 8, oy + 9, (Color){84, 53, 32, 255});
DrawPixel(ox + 12, oy + 8, (Color){208, 169, 69, 255});
DrawPixel(ox + 12, oy + 9, (Color){166, 124, 51, 255});
EndTextureMode();
}
void paint_door_open_tile(Tileset *ts, int id) {
if (ts == NULL || id < 0 || id >= MAX_TILE_ID)
return;
if (ts->render_target.id == 0 || ts->finalized)
return;
Vector2 off = get_paint_offset(ts, id);
int ox = (int)off.x;
int oy = (int)off.y;
int w = ts->tile_w;
int h = ts->tile_h;
BeginTextureMode(ts->render_target);
Color floor = (Color){58, 55, 52, 255};
Color seam = (Color){32, 30, 29, 255};
Color wood_dark = (Color){72, 47, 30, 255};
Color wood_light = (Color){131, 88, 49, 255};
DrawRectangle(ox, oy, w, h, floor);
DrawLine(ox + 7, oy + 1, ox + 7, oy + h - 2, seam);
DrawLine(ox + 1, oy + 8, ox + w - 2, oy + 8, seam);
DrawRectangle(ox, oy, 3, h, wood_dark);
DrawRectangle(ox + 2, oy + 2, 2, h - 4, wood_light);
DrawPixel(ox + 1, oy + 4, (Color){72, 72, 72, 255});
DrawPixel(ox + 1, oy + 11, (Color){72, 72, 72, 255});
DrawRectangle(ox + 4, oy + 13, 6, 1, (Color){25, 20, 17, 255});
EndTextureMode();
}
void paint_effect_burn_tile(Tileset *ts, int id) {
if (ts == NULL || id < 0 || id >= MAX_TILE_ID)
return;
if (ts->render_target.id == 0 || ts->finalized)
return;
Vector2 off = get_paint_offset(ts, id);
int ox = (int)off.x;
int oy = (int)off.y;
BeginTextureMode(ts->render_target);
DrawRectangle(ox, oy, ts->tile_w, ts->tile_h, BLANK);
// Fire effect - orange/red flames
Color flame1 = (Color){255, 100, 20, 255};
Color flame2 = (Color){255, 180, 40, 255};
Color flame3 = (Color){255, 60, 10, 255};
// Flame shapes
DrawRectangle(ox + 4, oy + 8, 2, 6, flame1);
DrawRectangle(ox + 7, oy + 6, 2, 8, flame2);
DrawRectangle(ox + 10, oy + 9, 2, 5, flame3);
DrawPixel(ox + 5, oy + 5, flame2);
DrawPixel(ox + 8, oy + 4, flame1);
DrawPixel(ox + 11, oy + 7, flame2);
EndTextureMode();
}
void paint_effect_poison_tile(Tileset *ts, int id) {
if (ts == NULL || id < 0 || id >= MAX_TILE_ID)
return;
if (ts->render_target.id == 0 || ts->finalized)
return;
Vector2 off = get_paint_offset(ts, id);
int ox = (int)off.x;
int oy = (int)off.y;
BeginTextureMode(ts->render_target);
DrawRectangle(ox, oy, ts->tile_w, ts->tile_h, BLANK);
// Poison effect - green bubbles/drops
Color poison1 = (Color){50, 200, 50, 255};
Color poison2 = (Color){30, 150, 30, 255};
Color poison3 = (Color){80, 255, 80, 255};
// Bubbles
DrawRectangle(ox + 5, oy + 4, 3, 3, poison1);
DrawRectangle(ox + 9, oy + 7, 2, 2, poison2);
DrawRectangle(ox + 4, oy + 10, 2, 2, poison3);
DrawRectangle(ox + 8, oy + 11, 3, 2, poison1);
DrawPixel(ox + 11, oy + 5, poison3);
DrawPixel(ox + 6, oy + 13, poison2);
EndTextureMode();
}
void paint_effect_block_tile(Tileset *ts, int id) {
if (ts == NULL || id < 0 || id >= MAX_TILE_ID)
return;
if (ts->render_target.id == 0 || ts->finalized)
return;
Vector2 off = get_paint_offset(ts, id);
int ox = (int)off.x;
int oy = (int)off.y;
BeginTextureMode(ts->render_target);
DrawRectangle(ox, oy, ts->tile_w, ts->tile_h, BLANK);
// Block/shield effect - blue shield shape
Color shield = (Color){80, 130, 220, 255};
Color shield_light = (Color){120, 170, 255, 255};
Color shield_dark = (Color){50, 90, 180, 255};
// Shield outline
DrawRectangle(ox + 4, oy + 2, 8, 12, shield);
// Inner highlight
DrawRectangle(ox + 6, oy + 4, 4, 8, shield_light);
// Border
DrawRectangle(ox + 4, oy + 2, 8, 1, shield_dark);
DrawRectangle(ox + 4, oy + 13, 8, 1, shield_dark);
DrawRectangle(ox + 4, oy + 2, 1, 12, shield_dark);
DrawRectangle(ox + 11, oy + 2, 1, 12, shield_dark);
// Cross in center
DrawRectangle(ox + 7, oy + 6, 2, 4, shield_dark);
DrawRectangle(ox + 6, oy + 7, 4, 2, shield_dark);
EndTextureMode();
}
void paint_slash_effect_tile(Tileset *ts, int id) {
if (ts == NULL || id < 0 || id >= MAX_TILE_ID)
return;
if (ts->render_target.id == 0 || ts->finalized)
return;
Vector2 off = get_paint_offset(ts, id);
int ox = (int)off.x;
int oy = (int)off.y;
BeginTextureMode(ts->render_target);
DrawRectangle(ox, oy, ts->tile_w, ts->tile_h, BLANK);
// Slash effect - white/gray diagonal slash
Color slash = (Color){255, 255, 255, 255};
Color slash_trail = (Color){200, 200, 220, 255};
// Main slash line (diagonal)
DrawRectangle(ox + 2, oy + 12, 3, 2, slash);
DrawRectangle(ox + 4, oy + 10, 3, 2, slash);
DrawRectangle(ox + 6, oy + 8, 3, 2, slash);
DrawRectangle(ox + 8, oy + 6, 3, 2, slash);
DrawRectangle(ox + 10, oy + 4, 3, 2, slash);
DrawRectangle(ox + 12, oy + 2, 2, 2, slash);
// Trail
DrawPixel(ox + 3, oy + 11, slash_trail);
DrawPixel(ox + 5, oy + 9, slash_trail);
DrawPixel(ox + 7, oy + 7, slash_trail);
DrawPixel(ox + 9, oy + 5, slash_trail);
DrawPixel(ox + 11, oy + 3, slash_trail);
EndTextureMode();
}
int tileset_paint_all(Tileset *ts) {
if (ts == NULL)
return 0;
if (ts->render_target.id == 0 || ts->finalized)
return 0;
// Register all tile IDs first
for (int id = 0; id < NUM_TILE_IDS; id++) {
if (!tileset_register(ts, id))
return 0;
}
// Paint map tiles
paint_wall_tile(ts, TILE_WALL_0, 0);
paint_wall_tile(ts, TILE_WALL_1, 1);
paint_floor_tile(ts, TILE_FLOOR_0, 0);
paint_floor_tile(ts, TILE_FLOOR_1, 1);
paint_floor_tile(ts, TILE_FLOOR_2, 2);
paint_floor_tile(ts, TILE_FLOOR_3, 3);
paint_stairs_tile(ts, TILE_STAIRS_SPRITE);
// Paint entity sprites
paint_player_tile(ts, SPRITE_PLAYER);
paint_player_walk_tile(ts, SPRITE_PLAYER_WALK_0, 0);
paint_player_walk_tile(ts, SPRITE_PLAYER_WALK_1, 1);
paint_player_attack_tile(ts, SPRITE_PLAYER_ATTACK);
// Enemy goblin sprites
paint_enemy_tile(ts, SPRITE_ENEMY_GOBLIN, 0);
paint_enemy_walk_tile(ts, SPRITE_ENEMY_GOBLIN_WALK_0, 0, 0);
paint_enemy_walk_tile(ts, SPRITE_ENEMY_GOBLIN_WALK_1, 0, 1);
paint_enemy_attack_tile(ts, SPRITE_ENEMY_GOBLIN_ATTACK, 0);
// Enemy skeleton sprites
paint_enemy_tile(ts, SPRITE_ENEMY_SKELETON, 1);
paint_enemy_walk_tile(ts, SPRITE_ENEMY_SKELETON_WALK_0, 1, 0);
paint_enemy_walk_tile(ts, SPRITE_ENEMY_SKELETON_WALK_1, 1, 1);
paint_enemy_attack_tile(ts, SPRITE_ENEMY_SKELETON_ATTACK, 1);
// Enemy orc sprites
paint_enemy_tile(ts, SPRITE_ENEMY_ORC, 2);
paint_enemy_walk_tile(ts, SPRITE_ENEMY_ORC_WALK_0, 2, 0);
paint_enemy_walk_tile(ts, SPRITE_ENEMY_ORC_WALK_1, 2, 1);
paint_enemy_attack_tile(ts, SPRITE_ENEMY_ORC_ATTACK, 2);
paint_item_tile(ts, SPRITE_ITEM_POTION, 0);
paint_item_tile(ts, SPRITE_ITEM_WEAPON, 1);
paint_item_tile(ts, SPRITE_ITEM_ARMOR, 2);
// Door tiles
paint_door_closed_tile(ts, TILE_DOOR_CLOSED_SPRITE);
paint_door_open_tile(ts, TILE_DOOR_OPEN_SPRITE);
// Effect sprites
paint_effect_burn_tile(ts, SPRITE_EFFECT_BURN);
paint_effect_poison_tile(ts, SPRITE_EFFECT_POISON);
paint_effect_block_tile(ts, SPRITE_EFFECT_BLOCK);
paint_slash_effect_tile(ts, SPRITE_SLASH_EFFECT);
return 1;
}

View file

@ -1,66 +0,0 @@
#ifndef TILESET_PAINT_H
#define TILESET_PAINT_H
#include "tileset.h"
// Forward declarations for types used in painting
typedef enum { ENEMY_GOBLIN_FWD, ENEMY_SKELETON_FWD, ENEMY_ORC_FWD } EnemyType_Paint;
typedef enum { ITEM_POTION_FWD, ITEM_WEAPON_FWD, ITEM_ARMOR_FWD } ItemType_Paint;
// Paint a wall tile with brick-like pattern.
// variant: 0 or 1 for shade variation.
void paint_wall_tile(Tileset *ts, int id, int variant);
// Paint a floor tile with stone/dithered pattern.
// variant: 0-3 for different noise patterns.
void paint_floor_tile(Tileset *ts, int id, int variant);
// Paint a stairs tile with depth illusion.
void paint_stairs_tile(Tileset *ts, int id);
// Paint the player sprite (adventurer silhouette).
void paint_player_tile(Tileset *ts, int id);
// Paint a player walking animation frame.
// frame: 0 or 1 for the two walk frames.
void paint_player_walk_tile(Tileset *ts, int id, int frame);
// Paint a player attacking animation frame.
void paint_player_attack_tile(Tileset *ts, int id);
// Paint an enemy sprite based on type.
void paint_enemy_tile(Tileset *ts, int id, int enemy_type);
// Paint an enemy walking animation frame.
// frame: 0 or 1 for the two walk frames.
void paint_enemy_walk_tile(Tileset *ts, int id, int enemy_type, int frame);
// Paint an enemy attacking animation frame.
void paint_enemy_attack_tile(Tileset *ts, int id, int enemy_type);
// Paint an item sprite based on type.
void paint_item_tile(Tileset *ts, int id, int item_type);
// Paint a closed door tile.
void paint_door_closed_tile(Tileset *ts, int id);
// Paint an open door tile.
void paint_door_open_tile(Tileset *ts, int id);
// Paint a burning/fire effect sprite.
void paint_effect_burn_tile(Tileset *ts, int id);
// Paint a poison effect sprite.
void paint_effect_poison_tile(Tileset *ts, int id);
// Paint a block/shield effect sprite.
void paint_effect_block_tile(Tileset *ts, int id);
// Paint a slash attack effect sprite.
void paint_slash_effect_tile(Tileset *ts, int id);
// Convenience: paint and register all tiles in one call.
// Returns 0 on failure, non-zero on success.
int tileset_paint_all(Tileset *ts);
#endif // TILESET_PAINT_H

View file

@ -20,7 +20,6 @@ stdenv.mkDerivation {
(s + /libs) (s + /libs)
(s + /src) (s + /src)
(s + /build.zig) (s + /build.zig)
(s + /build.zig.zon)
]; ];
}; };

View file

@ -9,17 +9,7 @@ typedef struct {
} Vec2; } Vec2;
// Tile types // Tile types
typedef enum { typedef enum { TILE_WALL, TILE_FLOOR, TILE_STAIRS } TileType;
TILE_WALL,
TILE_FLOOR,
TILE_STAIRS,
TILE_DOOR_CLOSED,
TILE_DOOR_OPEN,
TILE_DOOR_RUINED,
TILE_RUBBLE,
TILE_SHALLOW_WATER,
TILE_STATUE,
} TileType;
// Status effect types // Status effect types
typedef enum { EFFECT_NONE, EFFECT_POISON, EFFECT_STUN, EFFECT_BLEED, EFFECT_WEAKEN, EFFECT_BURN } StatusEffectType; typedef enum { EFFECT_NONE, EFFECT_POISON, EFFECT_STUN, EFFECT_BLEED, EFFECT_WEAKEN, EFFECT_BURN } StatusEffectType;
@ -35,40 +25,17 @@ typedef struct {
} StatusEffect; } StatusEffect;
// Room // Room
typedef enum {
ROOM_START,
ROOM_GUARD,
ROOM_SHRINE,
ROOM_CISTERN,
ROOM_ARMORY,
ROOM_CRYPT,
ROOM_LIBRARY,
ROOM_FORGE,
ROOM_VAULT,
} RoomType;
typedef struct { typedef struct {
int x, y, w, h; int x, y, w, h;
RoomType type;
} Room; } Room;
// Light source for sub-tile lighting system
typedef struct {
int x, y;
int intensity;
int range;
} LightSource;
// Map // Map
typedef struct { 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 light_map[MAP_HEIGHT * SUB_TILE_RES][MAP_WIDTH * SUB_TILE_RES]; unsigned char visible[MAP_HEIGHT][MAP_WIDTH];
unsigned char remembered[MAP_HEIGHT][MAP_WIDTH]; unsigned char remembered[MAP_HEIGHT][MAP_WIDTH];
unsigned char door_open_from[MAP_HEIGHT][MAP_WIDTH]; // 0=N,1=S,2=E,3=W,255=closed
unsigned char door_anim_timer[MAP_HEIGHT][MAP_WIDTH];
unsigned char door_anim_target[MAP_HEIGHT][MAP_WIDTH]; // 0=closed, 1=open
} Map; } Map;
// Dungeon // Dungeon
@ -82,165 +49,69 @@ typedef struct {
typedef enum { ITEM_POTION, ITEM_WEAPON, ITEM_ARMOR } ItemType; typedef enum { ITEM_POTION, ITEM_WEAPON, ITEM_ARMOR } ItemType;
// Item // Item
typedef struct { typedef struct {
int x, y; int x, y;
ItemType type; ItemType type;
int power; int power;
int floor; int floor;
int picked_up; int picked_up;
DamageClass dmg_class; DamageClass dmg_class;
int crit_chance; int crit_chance;
int crit_multiplier; int crit_multiplier;
int status_chance; int status_chance;
// rendering
int sprite_tile_id; // tile ID for rendering
} Item; } Item;
// Player animation states
typedef enum { PLAYER_ANIM_IDLE, PLAYER_ANIM_WALK, PLAYER_ANIM_ATTACK } PlayerAnimState;
// Player // Player
typedef struct { typedef struct {
Vec2 position; Vec2 position;
int hp, max_hp; int hp, max_hp;
int attack; int attack;
int defense; int defense;
int floor; int floor;
int step_count; int step_count;
int speed; // actions per 100 ticks (100 = 1 action per turn)
int speed; // actions per 100 ticks (100 = 1 action per turn)
int cooldown; // countdown to next action (0 = can act) int cooldown; // countdown to next action (0 = can act)
int dodge; // dodge chance percentage
int dodge; // dodge chance percentage int block; // flat damage reduction on successful block roll
int block; // flat damage reduction on successful block roll
Item equipped_weapon; Item equipped_weapon;
int has_weapon; int has_weapon;
Item equipped_armor; Item equipped_armor;
int has_armor; int has_armor;
Item inventory[MAX_INVENTORY]; Item inventory[MAX_INVENTORY];
int inventory_count; int inventory_count;
// status effects // status effects
StatusEffect effects[MAX_EFFECTS]; StatusEffect effects[MAX_EFFECTS];
int effect_count; int effect_count;
// animation
PlayerAnimState anim_state;
int anim_frame; // current animation frame
int anim_timer; // frames until next frame
int facing_right; // 1 = facing right, 0 = facing left
// rendering
int sprite_tile_id; // tile ID for rendering
// visual effects
int flash_timer; // damage flash frames remaining
} Player; } Player;
// Enemy types // Enemy types
typedef enum { ENEMY_GOBLIN, ENEMY_SKELETON, ENEMY_ORC } EnemyType; typedef enum { ENEMY_GOBLIN, ENEMY_SKELETON, ENEMY_ORC } EnemyType;
// Enemy animation states
typedef enum { ENEMY_ANIM_IDLE, ENEMY_ANIM_WALK, ENEMY_ANIM_ATTACK } EnemyAnimState;
// Enemy // Enemy
typedef struct { typedef struct {
Vec2 position; Vec2 position;
int hp; int hp;
int max_hp; int max_hp;
int attack; int attack;
int alive; int alive;
EnemyType type; EnemyType type;
int speed; // actions per 100 ticks
int speed; // actions per 100 ticks
int cooldown; // countdown to next action int cooldown; // countdown to next action
int dodge; // dodge chance percentage
int dodge; // dodge chance percentage int block; // flat damage reduction
int block; // flat damage reduction
int resistance[NUM_DMG_CLASSES]; int resistance[NUM_DMG_CLASSES];
DamageClass dmg_class; DamageClass dmg_class;
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 // vision
int vision_range; int vision_range;
int alert; // 1 = aware of player, searching
int alert; // 1 = aware of player, searching
int last_known_x; // last position where enemy saw player int last_known_x; // last position where enemy saw player
int last_known_y; int last_known_y;
// status effects // status effects
StatusEffect effects[MAX_EFFECTS]; StatusEffect effects[MAX_EFFECTS];
int effect_count; int effect_count;
// animation
EnemyAnimState anim_state;
int anim_frame; // current animation frame
int anim_timer; // frames until next frame
int facing_right; // 1 = facing right, 0 = facing left
// rendering
int sprite_tile_id; // tile ID for rendering
} Enemy; } Enemy;

View file

@ -5,7 +5,6 @@
#include "movement.h" #include "movement.h"
#include "rng/rng.h" #include "rng/rng.h"
#include "settings.h" #include "settings.h"
#include "tileset/tileset.h"
#include <string.h> #include <string.h>
// Forward declaration // Forward declaration
@ -26,28 +25,16 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) {
if (floor >= 4) if (floor >= 4)
max_type = 3; max_type = 3;
Room *start_room = map_room_at(map, p->position.x, p->position.y); for (int i = 0; i < num_enemies; i++) {
// Find random floor position
int attempts = 0;
while (*count < num_enemies && attempts < num_enemies * 40) {
attempts++;
int ex, ey; int ex, ey;
if (!get_random_floor_tile_excluding_room(map, start_room, &ex, &ey, 80)) get_random_floor_tile(map, &ex, &ey, 50);
break;
// Don't spawn on player position // Don't spawn on player position
if (ex == p->position.x && ey == p->position.y) { if (ex == p->position.x && ey == p->position.y) {
continue; continue;
} }
// Don't spawn in the starting room
if (start_room != NULL) {
if (ex >= start_room->x && ex < start_room->x + start_room->w && ey >= start_room->y &&
ey < start_room->y + start_room->h) {
continue;
}
}
// Don't spawn on other enemies // Don't spawn on other enemies
if (is_enemy_at(enemies, *count, ex, ey)) { if (is_enemy_at(enemies, *count, ex, ey)) {
continue; continue;
@ -138,28 +125,7 @@ void enemy_spawn(Enemy enemies[], int *count, Map *map, Player *p, int floor) {
} }
e.cooldown = e.speed; e.cooldown = e.speed;
// Initialize animation state enemies[i] = e;
e.anim_state = ENEMY_ANIM_IDLE;
e.anim_frame = 0;
e.anim_timer = 0;
e.facing_right = (e.position.x < p->position.x) ? 1 : 0;
// Set sprite tile ID based on enemy type
switch (e.type) {
case ENEMY_GOBLIN:
e.sprite_tile_id = SPRITE_ENEMY_GOBLIN;
break;
case ENEMY_SKELETON:
e.sprite_tile_id = SPRITE_ENEMY_SKELETON;
break;
case ENEMY_ORC:
e.sprite_tile_id = SPRITE_ENEMY_ORC;
break;
default:
e.sprite_tile_id = SPRITE_ENEMY_GOBLIN;
break;
}
enemies[*count] = e;
(*count)++; (*count)++;
} }
} }
@ -309,9 +275,6 @@ void enemy_act(Enemy *e, Player *p, Map *map, Enemy *all_enemies, int enemy_coun
// Attack if adjacent to player // 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)) { if (can_see && can_see_entity(map, e->position.x, e->position.y, p->position.x, p->position.y, 1)) {
e->anim_state = ENEMY_ANIM_ATTACK;
e->anim_timer = 12;
e->facing_right = (e->position.x < p->position.x) ? 1 : 0;
combat_enemy_attack(e, p); combat_enemy_attack(e, p);
propagate_alert(e, all_enemies, enemy_count); propagate_alert(e, all_enemies, enemy_count);
return; return;
@ -319,30 +282,14 @@ void enemy_act(Enemy *e, Player *p, Map *map, Enemy *all_enemies, int enemy_coun
// Move toward player if visible // Move toward player if visible
if (can_see) { if (can_see) {
int old_x = e->position.x;
int old_y = e->position.y;
enemy_move_toward_player(e, p, map, all_enemies, enemy_count); enemy_move_toward_player(e, p, map, all_enemies, enemy_count);
if (e->position.x != old_x || e->position.y != old_y) {
e->anim_state = ENEMY_ANIM_WALK;
e->anim_timer = 8;
e->facing_right = (e->position.x < p->position.x) ? 1 : 0;
}
propagate_alert(e, all_enemies, enemy_count); propagate_alert(e, all_enemies, enemy_count);
return; return;
} }
// If alert but can't see player, move toward last known position // If alert but can't see player, move toward last known position
if (e->alert) { if (e->alert) {
int old_x = e->position.x;
int old_y = e->position.y;
enemy_move_to_last_known(e, map, all_enemies, enemy_count); enemy_move_to_last_known(e, map, all_enemies, enemy_count);
if (e->position.x != old_x || e->position.y != old_y) {
e->anim_state = ENEMY_ANIM_WALK;
e->anim_timer = 8;
if (e->position.x != old_x) {
e->facing_right = (e->position.x < old_x) ? 0 : 1;
}
}
return; return;
} }
@ -357,17 +304,6 @@ void enemy_update_all(Enemy enemies[], int count, Player *p, Map *map) {
if (!e->alive) if (!e->alive)
continue; continue;
// Update animation timer
if (e->anim_timer > 0) {
e->anim_timer--;
if (e->anim_timer <= 0) {
e->anim_state = ENEMY_ANIM_IDLE;
e->anim_frame = 0;
} else if (e->anim_state == ENEMY_ANIM_WALK) {
e->anim_frame = (e->anim_timer / 4) % 2;
}
}
e->cooldown -= e->speed; e->cooldown -= e->speed;
if (e->cooldown <= 0) { if (e->cooldown <= 0) {
enemy_act(e, p, map, enemies, count); enemy_act(e, p, map, enemies, count);

View file

@ -2,7 +2,6 @@
#define GAME_STATE_H #define GAME_STATE_H
#include "common.h" #include "common.h"
#include "tileset/tileset.h"
#include <raylib.h> #include <raylib.h>
// Floating damage text // Floating damage text
@ -66,15 +65,6 @@ typedef struct {
int final_score; int final_score;
// Seed for this run // Seed for this run
unsigned int run_seed; unsigned int run_seed;
// Tileset atlas for rendering
Tileset tileset;
// Sub-tile lighting
LightSource static_lights[32];
int static_light_count;
// Slash effect timer for attack animations
int slash_timer; // frames remaining for slash effect
int slash_x, slash_y; // position of slash effect
DamageClass slash_dmg_class; // damage type for slash visual
} GameState; } GameState;
#endif // GAME_STATE_H #endif // GAME_STATE_H

View file

@ -2,7 +2,6 @@
#include "map/map.h" #include "map/map.h"
#include "rng/rng.h" #include "rng/rng.h"
#include "settings.h" #include "settings.h"
#include "tileset/tileset.h"
#include <stddef.h> #include <stddef.h>
typedef struct { typedef struct {
@ -78,22 +77,6 @@ void item_spawn(Item items[], int *count, Map *map, int floor) {
item.power = 1 + rng_int(0, floor / 2); item.power = 1 + rng_int(0, floor / 2);
} }
// Set sprite tile ID based on item type
switch (item.type) {
case ITEM_POTION:
item.sprite_tile_id = SPRITE_ITEM_POTION;
break;
case ITEM_WEAPON:
item.sprite_tile_id = SPRITE_ITEM_WEAPON;
break;
case ITEM_ARMOR:
item.sprite_tile_id = SPRITE_ITEM_ARMOR;
break;
default:
item.sprite_tile_id = SPRITE_ITEM_POTION;
break;
}
items[*count] = item; items[*count] = item;
(*count)++; (*count)++;
} }

View file

@ -4,14 +4,11 @@
#include "enemy.h" #include "enemy.h"
#include "items.h" #include "items.h"
#include "map/map.h" #include "map/map.h"
#include "map/utils.h"
#include "movement.h" #include "movement.h"
#include "player.h" #include "player.h"
#include "render.h" #include "render.h"
#include "rng/rng.h" #include "rng/rng.h"
#include "settings.h" #include "settings.h"
#include "tileset/tileset.h"
#include "tileset/tileset_paint.h"
#include <ctype.h> #include <ctype.h>
#include <stddef.h> #include <stddef.h>
#include <stdio.h> #include <stdio.h>
@ -118,9 +115,7 @@ static void init_floor(GameState *gs, int floor_num) {
// Find spawn position // Find spawn position
int start_x, start_y; int start_x, start_y;
if (gs->map.room_count <= 0 || !get_room_floor_tile(&gs->map, &gs->map.rooms[0], &start_x, &start_y)) { get_random_floor_tile(&gs->map, &start_x, &start_y, 100);
get_random_floor_tile(&gs->map, &start_x, &start_y, 100);
}
// Initialize player position if first floor // Initialize player position if first floor
if (floor_num == 1) { if (floor_num == 1) {
@ -133,12 +128,8 @@ static void init_floor(GameState *gs, int floor_num) {
} }
gs->player.floor = floor_num; gs->player.floor = floor_num;
// Set initial player light and compute visibility // Calculate initial visibility
LightSource player_light = {gs->player.position.x, gs->player.position.y, PLAYER_LIGHT_INTENSITY, PLAYER_LIGHT_RANGE}; calculate_visibility(&gs->map, gs->player.position.x, gs->player.position.y);
LightSource sources[1 + 32];
sources[0] = player_light;
memcpy(sources + 1, gs->static_lights, gs->static_light_count * sizeof(LightSource));
compute_lighting(&gs->map, sources, 1 + gs->static_light_count);
// 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);
@ -150,151 +141,6 @@ static void init_floor(GameState *gs, int floor_num) {
gs->turn_count = 0; gs->turn_count = 0;
} }
#ifdef ROGGED_ADMIN_CONTROLS
static void admin_recompute_lighting(GameState *gs) {
LightSource player_light = {gs->player.position.x, gs->player.position.y, PLAYER_LIGHT_INTENSITY, PLAYER_LIGHT_RANGE};
LightSource sources[1 + 32];
sources[0] = player_light;
memcpy(sources + 1, gs->static_lights, gs->static_light_count * sizeof(LightSource));
compute_lighting(&gs->map, sources, 1 + gs->static_light_count);
}
static void admin_reveal_map(GameState *gs) {
for (int y = 0; y < MAP_HEIGHT; y++) {
for (int x = 0; x < MAP_WIDTH; x++) {
gs->map.remembered[y][x] = 1;
}
}
}
static void admin_apply_fullbright(GameState *gs) {
memset(gs->map.light_map, 255, sizeof(gs->map.light_map));
admin_reveal_map(gs);
}
static void admin_kill_enemies(GameState *gs) {
for (int i = 0; i < gs->enemy_count; i++) {
if (gs->enemies[i].alive) {
gs->enemies[i].alive = 0;
gs->enemies[i].hp = 0;
}
}
}
static void admin_teleport_to_stairs(GameState *gs) {
for (int y = 0; y < MAP_HEIGHT; y++) {
for (int x = 0; x < MAP_WIDTH; x++) {
if (gs->map.tiles[y][x] == TILE_STAIRS) {
gs->player.position.x = x;
gs->player.position.y = y;
gs->awaiting_descend = 1;
gs->last_message = "Admin: teleported to stairs";
gs->message_timer = 90;
admin_recompute_lighting(gs);
return;
}
}
}
}
static int admin_button(Rectangle rect, const char *label) {
Vector2 mouse = GetMousePosition();
int hovered = CheckCollisionPointRec(mouse, rect);
DrawRectangleRec(rect, hovered ? (Color){72, 62, 48, 235} : (Color){44, 42, 46, 235});
DrawRectangleLines((int)rect.x, (int)rect.y, (int)rect.width, (int)rect.height,
hovered ? (Color){210, 180, 110, 255} : (Color){112, 104, 92, 255});
DrawText(label, (int)rect.x + 8, (int)rect.y + 5, SMALL_FONT, (Color){235, 228, 210, 255});
return hovered && IsMouseButtonPressed(MOUSE_LEFT_BUTTON);
}
static void admin_controls(GameState *gs, int *visible, int *fullbright) {
if (IsKeyPressed(KEY_F1))
*visible = !*visible;
DrawRectangle(8, 8, 142, 22, (Color){10, 10, 12, 190});
DrawText("F1 Admin", 16, 13, SMALL_FONT, (Color){220, 190, 110, 255});
if (!*visible)
return;
const int x = 20;
const int start_y = 84;
const int w = 166;
const int h = 24;
const int gap = 8;
const int button_count = 10;
const int panel_padding_bottom = 16;
Rectangle panel = {8, 34, 190,
(float)(start_y - 34 + button_count * h + (button_count - 1) * gap + panel_padding_bottom)};
DrawRectangleRec(panel, (Color){12, 12, 15, 225});
DrawRectangleLines((int)panel.x, (int)panel.y, (int)panel.width, (int)panel.height, (Color){170, 145, 90, 255});
DrawText("Admin Controls", 20, 46, NORM_FONT, (Color){230, 205, 130, 255});
DrawText("development build only", 20, 62, TINY_FONT, (Color){170, 164, 150, 255});
int y = start_y;
if (admin_button((Rectangle){x, y, w, h}, "Heal full")) {
gs->player.hp = gs->player.max_hp;
gs->game_over = 0;
gs->last_message = "Admin: healed";
gs->message_timer = 60;
}
y += h + gap;
if (admin_button((Rectangle){x, y, w, h}, "+10 max HP")) {
gs->player.max_hp += 10;
gs->player.hp = gs->player.max_hp;
}
y += h + gap;
if (admin_button((Rectangle){x, y, w, h}, "+2 attack"))
gs->player.attack += 2;
y += h + gap;
if (admin_button((Rectangle){x, y, w, h}, "+2 defense"))
gs->player.defense += 2;
y += h + gap;
if (admin_button((Rectangle){x, y, w, h}, "Reveal map"))
admin_reveal_map(gs);
y += h + gap;
char fullbright_label[32];
snprintf(fullbright_label, sizeof(fullbright_label), "Fullbright: %s", *fullbright ? "on" : "off");
if (admin_button((Rectangle){x, y, w, h}, fullbright_label)) {
*fullbright = !*fullbright;
if (*fullbright) {
admin_apply_fullbright(gs);
} else {
admin_recompute_lighting(gs);
}
}
y += h + gap;
if (admin_button((Rectangle){x, y, w, h}, "Kill enemies"))
admin_kill_enemies(gs);
y += h + gap;
if (admin_button((Rectangle){x, y, w, h}, "Teleport stairs"))
admin_teleport_to_stairs(gs);
y += h + gap;
if (admin_button((Rectangle){x, y, w, h}, "Reroll floor")) {
init_floor(gs, gs->player.floor);
gs->last_message = "Admin: rerolled floor";
gs->message_timer = 60;
}
y += h + gap;
if (admin_button((Rectangle){x, y, w, h}, "Next floor")) {
int next_floor = gs->player.floor < NUM_FLOORS ? gs->player.floor + 1 : gs->player.floor;
init_floor(gs, next_floor);
gs->last_message = "Admin: advanced floor";
gs->message_timer = 60;
}
}
#endif
// Tick all status effects at the start of a turn // Tick all status effects at the start of a turn
static void tick_all_effects(GameState *gs) { static void tick_all_effects(GameState *gs) {
// Player effects // Player effects
@ -336,18 +182,19 @@ static void post_action(GameState *gs, Enemy *attacked_enemy) {
if (gs->game_over) if (gs->game_over)
return; return;
// Check if stepped on stairs
if (gs->map.tiles[gs->player.position.y][gs->player.position.x] == TILE_STAIRS) {
gs->awaiting_descend = 1;
gs->last_message = "Descend to next floor? (Y/N)";
gs->message_timer = 120;
return;
}
// 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->position.x * TILE_SIZE + 8;
int ey = attacked_enemy->position.y * TILE_SIZE; int ey = attacked_enemy->position.y * TILE_SIZE;
// Trigger slash effect
gs->slash_timer = 8;
gs->slash_x = attacked_enemy->position.x;
gs->slash_y = attacked_enemy->position.y;
// Use player's equipped weapon damage class, or default to slash
gs->slash_dmg_class = gs->player.has_weapon ? gs->player.equipped_weapon.dmg_class : DMG_SLASH;
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);
audio_play_dodge(gs); audio_play_dodge(gs);
@ -378,25 +225,8 @@ static void post_action(GameState *gs, Enemy *attacked_enemy) {
} }
} }
// Close doors that the player moved away from before recomputing lighting.
for (int dy = 0; dy < MAP_HEIGHT; dy++) {
for (int dx = 0; dx < MAP_WIDTH; dx++) {
if (gs->map.tiles[dy][dx] == TILE_DOOR_OPEN) {
if (gs->player.position.x != dx || gs->player.position.y != dy) {
gs->map.tiles[dy][dx] = TILE_DOOR_CLOSED;
gs->map.door_anim_target[dy][dx] = 0;
gs->map.door_anim_timer[dy][dx] = DOOR_ANIM_FRAMES;
}
}
}
}
// Update visibility based on player's new position // Update visibility based on player's new position
LightSource p_light = {gs->player.position.x, gs->player.position.y, PLAYER_LIGHT_INTENSITY, PLAYER_LIGHT_RANGE}; calculate_visibility(&gs->map, gs->player.position.x, gs->player.position.y);
LightSource srcs[1 + 32];
srcs[0] = p_light;
memcpy(srcs + 1, gs->static_lights, gs->static_light_count * sizeof(LightSource));
compute_lighting(&gs->map, srcs, 1 + gs->static_light_count);
// 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);
@ -407,7 +237,6 @@ 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++;
gs->player.flash_timer = 4; // Trigger damage flash
spawn_floating_text(gs, gs->player.position.x * TILE_SIZE + 8, gs->player.position.y * TILE_SIZE, spawn_floating_text(gs, gs->player.position.x * TILE_SIZE + 8, gs->player.position.y * TILE_SIZE,
combat_get_last_damage(), combat_was_critical()); combat_get_last_damage(), combat_was_critical());
} }
@ -418,13 +247,6 @@ static void post_action(GameState *gs, Enemy *attacked_enemy) {
if (gs->player.hp <= 0) if (gs->player.hp <= 0)
gs->game_over = 1; gs->game_over = 1;
// Check if stepped on stairs AFTER enemy turns
if (gs->map.tiles[gs->player.position.y][gs->player.position.x] == TILE_STAIRS) {
gs->awaiting_descend = 1;
gs->last_message = "Descend to next floor? (Y/N)";
gs->message_timer = 120;
}
} }
// If player is stunned, wait for any key then consume the turn // If player is stunned, wait for any key then consume the turn
@ -437,25 +259,7 @@ 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);
// Close doors that the player moved away from before recomputing lighting. calculate_visibility(&gs->map, gs->player.position.x, gs->player.position.y);
for (int dy = 0; dy < MAP_HEIGHT; dy++) {
for (int dx = 0; dx < MAP_WIDTH; dx++) {
if (gs->map.tiles[dy][dx] == TILE_DOOR_OPEN) {
if (gs->player.position.x != dx || gs->player.position.y != dy) {
gs->map.tiles[dy][dx] = TILE_DOOR_CLOSED;
gs->map.door_anim_target[dy][dx] = 0;
gs->map.door_anim_timer[dy][dx] = DOOR_ANIM_FRAMES;
}
}
}
}
{
LightSource l = {gs->player.position.x, gs->player.position.y, PLAYER_LIGHT_INTENSITY, PLAYER_LIGHT_RANGE};
LightSource s[1 + 32];
s[0] = l;
memcpy(s + 1, gs->static_lights, gs->static_light_count * sizeof(LightSource));
compute_lighting(&gs->map, s, 1 + gs->static_light_count);
}
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!";
@ -627,6 +431,7 @@ static int handle_movement_input(GameState *gs) {
} }
} }
Vec2 direction = {0, 0}; Vec2 direction = {0, 0};
if (IsKeyDown(KEY_W) || IsKeyDown(KEY_UP)) if (IsKeyDown(KEY_W) || IsKeyDown(KEY_UP))
direction.y = -1; direction.y = -1;
@ -643,6 +448,7 @@ static int handle_movement_input(GameState *gs) {
// Reset combat event before player acts // Reset combat event before player acts
combat_reset_event(); combat_reset_event();
int new_x = gs->player.position.x + direction.x; int new_x = gs->player.position.x + direction.x;
int new_y = gs->player.position.y + direction.y; int new_y = gs->player.position.y + direction.y;
@ -653,27 +459,11 @@ static int handle_movement_input(GameState *gs) {
try_move_entity(&gs->player.position, direction, &gs->map, &gs->player, gs->enemies, gs->enemy_count, true); try_move_entity(&gs->player.position, direction, &gs->map, &gs->player, gs->enemies, gs->enemy_count, true);
if (result == MOVE_RESULT_MOVED) { if (result == MOVE_RESULT_MOVED) {
player_on_move(&gs->player); player_on_move(&gs->player);
// Set walk animation
gs->player.anim_state = PLAYER_ANIM_WALK;
gs->player.anim_frame = 0;
gs->player.anim_timer = 8; // frames to show each walk frame
// Update facing direction
if (direction.x != 0)
gs->player.facing_right = (direction.x > 0);
action = 1; action = 1;
} else if (result == MOVE_RESULT_BLOCKED_ENEMY) { } else if (result == MOVE_RESULT_BLOCKED_ENEMY) {
target = player_find_enemy_at(gs->enemies, gs->enemy_count, new_x, new_y); target = player_find_enemy_at(gs->enemies, gs->enemy_count, new_x, new_y);
if (target != NULL) { if (target != NULL) {
player_attack(&gs->player, target); player_attack(&gs->player, target);
// Set attack animation
gs->player.anim_state = PLAYER_ANIM_ATTACK;
gs->player.anim_frame = 0;
gs->player.anim_timer = 12; // frames to show attack
// Face the enemy
if (target->position.x > gs->player.position.x)
gs->player.facing_right = 1;
else if (target->position.x < gs->player.position.x)
gs->player.facing_right = 0;
action = 1; action = 1;
} }
} }
@ -729,7 +519,7 @@ void load_audio_assets(GameState *gs) {
} }
// Main game loop // Main game loop
static void game_loop(unsigned int run_seed, FontManager *fm) { static void game_loop(unsigned int run_seed) {
GameState gs; GameState gs;
memset(&gs, 0, sizeof(GameState)); memset(&gs, 0, sizeof(GameState));
gs.run_seed = run_seed; gs.run_seed = run_seed;
@ -737,40 +527,14 @@ static void game_loop(unsigned int run_seed, FontManager *fm) {
// sound // sound
load_audio_assets(&gs); load_audio_assets(&gs);
// font // font
init_fonts(fm); Font fontTTF = LoadFontEx("./assets/fonts/spartan_500.ttf", 36, NULL, 0);
// Initialize tileset atlas
if (!tileset_init(&gs.tileset, TILE_SIZE, TILE_SIZE)) {
fprintf(stderr, "Failed to initialize tileset\n");
destroy_fonts(fm);
return;
}
if (!tileset_paint_all(&gs.tileset)) {
fprintf(stderr, "Failed to paint tiles\n");
tileset_destroy(&gs.tileset);
destroy_fonts(fm);
return;
}
if (!tileset_finalize(&gs.tileset)) {
fprintf(stderr, "Failed to finalize tileset\n");
tileset_destroy(&gs.tileset);
destroy_fonts(fm);
return;
}
// Initialize first floor // Initialize first floor
init_floor(&gs, 1); init_floor(&gs, 1);
// Disable esc to exit // Disable esc to exit
SetExitKey(0); SetExitKey(0);
int frame_counter = 0;
#ifdef ROGGED_ADMIN_CONTROLS
int admin_visible = 1;
int admin_fullbright = 0;
#endif
while (!WindowShouldClose()) { while (!WindowShouldClose()) {
frame_counter++;
// Handle input // Handle input
if (!gs.game_over) { if (!gs.game_over) {
// Tick status effects at the start of each frame where input is checked // Tick status effects at the start of each frame where input is checked
@ -789,13 +553,6 @@ static void game_loop(unsigned int run_seed, FontManager *fm) {
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_fonts(fm);
// Re-initialize tileset for new run
if (!tileset_init(&gs.tileset, TILE_SIZE, TILE_SIZE) || !tileset_paint_all(&gs.tileset) ||
!tileset_finalize(&gs.tileset)) {
fprintf(stderr, "Failed to re-initialize tileset\n");
break;
}
init_floor(&gs, 1); init_floor(&gs, 1);
// Update window title with new seed // Update window title with new seed
char title[128]; char title[128];
@ -811,43 +568,6 @@ static void game_loop(unsigned int run_seed, FontManager *fm) {
// Update effects // Update effects
update_effects(&gs); update_effects(&gs);
// Update slash effect timer
if (gs.slash_timer > 0)
gs.slash_timer--;
// Door animations are visual, so they tick every rendered frame.
for (int y = 0; y < MAP_HEIGHT; y++) {
for (int x = 0; x < MAP_WIDTH; x++) {
if (gs.map.door_anim_timer[y][x] > 0) {
gs.map.door_anim_timer[y][x]--;
if (gs.map.door_anim_timer[y][x] == 0 && gs.map.door_anim_target[y][x] == 0)
gs.map.door_open_from[y][x] = 255;
}
}
}
// Update player animation
if (gs.player.anim_timer > 0) {
gs.player.anim_timer--;
if (gs.player.anim_timer <= 0) {
// Animation finished, return to idle
gs.player.anim_state = PLAYER_ANIM_IDLE;
gs.player.anim_frame = 0;
} else if (gs.player.anim_state == PLAYER_ANIM_WALK) {
// Toggle walk frame every 4 frames
gs.player.anim_frame = (gs.player.anim_timer / 4) % 2;
}
}
// Update player damage flash
if (gs.player.flash_timer > 0)
gs.player.flash_timer--;
#ifdef ROGGED_ADMIN_CONTROLS
if (admin_fullbright)
admin_apply_fullbright(&gs);
#endif
// Render // Render
BeginDrawing(); BeginDrawing();
ClearBackground(BLACK); ClearBackground(BLACK);
@ -857,31 +577,27 @@ static void game_loop(unsigned int run_seed, FontManager *fm) {
cam.zoom = 1.0f; cam.zoom = 1.0f;
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, &gs.tileset); render_map(&gs.map);
render_items(gs.items, gs.item_count, &gs.map, &gs.tileset); render_items(gs.items, gs.item_count, gs.map.visible);
render_enemies(gs.enemies, gs.enemy_count, &gs.map, &gs.tileset, frame_counter); render_enemies(gs.enemies, gs.enemy_count, gs.map.visible);
render_player(&gs.player, &gs.tileset, frame_counter); render_player(&gs.player);
// Draw slash effect on top of entities
if (gs.slash_timer > 0) {
render_slash_effect(gs.slash_x, gs.slash_y, gs.slash_dmg_class, gs.slash_timer);
}
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, fm); render_floating_texts(gs.floating_texts, gs.floating_count, gs.shake_x, gs.shake_y);
render_ui(&gs.player, &gs.tileset, fm); render_ui(&gs.player, &fontTTF);
// Draw action log // Draw action log
render_action_log(gs.action_log, gs.log_count, gs.log_head, fm); render_action_log(gs.action_log, gs.log_count, gs.log_head, &fontTTF);
// 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, fm); render_inventory_overlay(&gs.player, gs.inv_selected, &fontTTF);
} }
// 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, fm); render_message(gs.last_message, &fontTTF);
} }
// Draw persistent seed display in top right // Draw persistent seed display in top right
@ -897,21 +613,14 @@ static void game_loop(unsigned int run_seed, FontManager *fm) {
} }
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, fm); gs.final_score, gs.run_seed, &fontTTF);
} }
#ifdef ROGGED_ADMIN_CONTROLS
admin_controls(&gs, &admin_visible, &admin_fullbright);
#endif
EndDrawing(); EndDrawing();
// small delay for key repeat control // small delay for key repeat control
WaitTime(0.08); WaitTime(0.08);
} }
// Cleanup
destroy_fonts(fm);
} }
// Check if a string is a valid unsigned integer // Check if a string is a valid unsigned integer
@ -974,9 +683,7 @@ int main(int argc, char **argv) {
SetTargetFPS(60); SetTargetFPS(60);
// Run game // Run game
FontManager fm; game_loop(run_seed);
init_fonts(&fm);
game_loop(run_seed, &fm);
// Cleanup // Cleanup
CloseWindow(); CloseWindow();

View file

@ -1,7 +1,6 @@
#include "movement.h" #include "movement.h"
#include "enemy.h" #include "enemy.h"
#include "map/map.h" #include "map/map.h"
#include "map/utils.h"
#include <stdbool.h> #include <stdbool.h>
// Check if position is occupied by player // Check if position is occupied by player
@ -9,33 +8,11 @@ static int is_player_at(Player *p, int x, int y) {
return (p->position.x == x && p->position.y == y); return (p->position.x == x && p->position.y == y);
} }
static int direction_to_door_open_from(Vec2 dir) {
if (dir.y == -1)
return 0; // N
if (dir.y == 1)
return 1; // S
if (dir.x == 1)
return 2; // E
if (dir.x == -1)
return 3; // W
return 255;
}
MoveResult try_move_entity(Vec2 *p, Vec2 direction, Map *map, Player *player, Enemy *enemies, int enemy_count, MoveResult try_move_entity(Vec2 *p, Vec2 direction, Map *map, Player *player, Enemy *enemies, int enemy_count,
bool moving_is_player) { bool moving_is_player) {
int new_x = p->x + direction.x; int new_x = p->x + direction.x;
int new_y = p->y + direction.y; int new_y = p->y + direction.y;
if (!in_bounds(new_x, new_y, MAP_WIDTH, MAP_HEIGHT))
return MOVE_RESULT_BLOCKED_WALL;
if (map->tiles[new_y][new_x] == TILE_DOOR_CLOSED) {
map->tiles[new_y][new_x] = TILE_DOOR_OPEN;
map->door_open_from[new_y][new_x] = direction_to_door_open_from(direction);
map->door_anim_target[new_y][new_x] = 1;
map->door_anim_timer[new_y][new_x] = DOOR_ANIM_FRAMES;
}
if (!is_floor(map, new_x, new_y)) if (!is_floor(map, new_x, new_y))
return MOVE_RESULT_BLOCKED_WALL; return MOVE_RESULT_BLOCKED_WALL;

View file

@ -3,7 +3,6 @@
#include "common.h" #include "common.h"
#include "items.h" #include "items.h"
#include "settings.h" #include "settings.h"
#include "tileset/tileset.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) {
@ -29,13 +28,6 @@ void player_init(Player *p, int x, int y) {
p->effect_count = 0; p->effect_count = 0;
memset(p->effects, 0, sizeof(p->effects)); memset(p->effects, 0, sizeof(p->effects));
// Initialize animation state
p->anim_state = PLAYER_ANIM_IDLE;
p->anim_frame = 0;
p->anim_timer = 0;
p->facing_right = 1;
p->sprite_tile_id = SPRITE_PLAYER;
// Initialize inventory to empty // Initialize inventory to empty
for (int i = 0; i < MAX_INVENTORY; i++) { for (int i = 0; i < MAX_INVENTORY; i++) {
p->inventory[i].picked_up = 1; // mark as invalid p->inventory[i].picked_up = 1; // mark as invalid

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,4 @@
#ifndef RENDER_H #ifndef RENDER_H
#define RENDER_H #define RENDER_H
@ -74,67 +75,36 @@
// FIXME: remove when player sprites are available // FIXME: remove when player sprites are available
#define PORTRAIT_BG (Color){30, 30, 45, 255} #define PORTRAIT_BG (Color){30, 30, 45, 255}
// Font manager encapsulates all loaded fonts with role-based mapping // Render the map tiles
typedef struct { void render_map(const Map *map);
Font title_font; // Royal_Decree_Bold.ttf -- end/title screens
Font hud_font; // Tomorrow_Night.ttf -- HUD and log panels
Font body_font; // spartan_500.ttf -- body text, floating labels
Font inv_font; // Royal_Decree.ttf -- inventory overlay
} FontManager;
// Font role constants for paint_tile functions (Phase 3) // Render the player
#define TILE_FONT_NONE 0 void render_player(const Player *p);
// Attempt to load a font from path; if the resulting texture is invalid (texture.id == 0), // Render all enemies
// fall back to fallback_path. If fallback also fails, the returned Font will have void render_enemies(const Enemy *enemies, int count, const unsigned char visible[MAP_HEIGHT][MAP_WIDTH]);
// texture.id == 0 and the caller must handle gracefully.
Font load_font_with_fallback(const char *path, int font_size, const char *fallback_path);
// Initialize a FontManager by loading all 4 available fonts with fallback chain. // Render all items
// Returns 0 on complete failure (all fonts failed to load), non-zero on success void render_items(const Item *items, int count, const unsigned char visible[MAP_HEIGHT][MAP_WIDTH]);
// (at least one font loaded). On partial failure, individual fields may be invalid
// (texture.id == 0); callers must check before using a given role font.
int init_fonts(FontManager *fm);
// Unload all fonts held by a FontManager
void destroy_fonts(FontManager *fm);
// Render the map tiles using tileset atlas
void render_map(const Map *map, const Tileset *tileset);
// Render the player using tileset atlas
// frame_counter is used for idle breathing animation
void render_player(const Player *p, const Tileset *tileset, int frame_counter);
// Render all enemies using tileset atlas
// frame_counter is used for idle breathing animation
void render_enemies(const Enemy *enemies, int count, const Map *map, const Tileset *tileset, int frame_counter);
// Render all items using tileset atlas
void render_items(const Item *items, int count, const Map *map, const Tileset *tileset);
// Render UI overlay // Render UI overlay
void render_ui(const Player *p, const Tileset *tileset, const FontManager *fm); void render_ui(const Player *p, Font *font);
// 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, const FontManager *fm); void render_action_log(const char log[5][128], int count, int head, Font *font);
// Render inventory selection overlay // Render inventory selection overlay
void render_inventory_overlay(const Player *p, int selected, const FontManager *fm); void render_inventory_overlay(const Player *p, int selected, Font *font);
// Render floating damage text // Render floating damage text
void render_floating_texts(FloatingText *texts, int count, int shake_x, int shake_y, const FontManager *fm); void render_floating_texts(FloatingText *texts, int count, int shake_x, int shake_y);
// Render slash effect during attacks
void render_slash_effect(int x, int y, DamageClass dmg_class, int timer);
// 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, int times_hit, int potions, int floors, int turns, int score, unsigned int seed, Font *font);
const FontManager *fm);
// Render a message popup // Render a message popup
void render_message(const char *message, const FontManager *fm); void render_message(const char *message, Font *font);
// Render seed display at top right of screen // Render seed display at top right of screen
void render_seed_display(unsigned int seed); void render_seed_display(unsigned int seed);

View file

@ -79,24 +79,8 @@
#define MESSAGE_TIMER_DURATION 60 #define MESSAGE_TIMER_DURATION 60
// Visibility / Fog of War // Visibility / Fog of War
#define PLAYER_VIEW_RANGE 8
#define ENEMY_VIEW_RANGE 6
#define ENEMY_PATROL_MOVE_CHANCE 30 #define ENEMY_PATROL_MOVE_CHANCE 30
// Sub-tile lighting
#define SUB_TILE_RES 8
#define LIGHT_SIGHT_THRESHOLD 40
#define AMBIENT_LIGHT_FACTOR 0.08f
#define REMEMBERED_LIGHT_FACTOR 0.18f
#define LIGHT_EXPONENT 1.7f
// Player light source parameters
#define PLAYER_LIGHT_RANGE 8
#define PLAYER_LIGHT_INTENSITY 255
// Enemy vision (default fallback for spawn)
#define ENEMY_VIEW_RANGE 6
// Visual polish
#define DRAW_GRID_LINES 1
#define DOOR_ANIM_FRAMES 8
#endif // SETTINGS_H #endif // SETTINGS_H