From 00b3798ae07c016cb6796fed6eb5ec1a43805806 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 28 Apr 2026 15:57:17 +0300 Subject: [PATCH] various: sub-tile lighting; nicer visibility calculations Signed-off-by: NotAShelf Change-Id: I0f0a0c12db76cc8e0f4c8ccc72ca4b826a6a6964 --- libs/map/map.c | 147 +++++++++++++++++++++++++++++++++++++++++++---- libs/map/map.h | 4 +- src/common.h | 9 ++- src/game_state.h | 3 + src/main.c | 26 +++++++-- src/render.c | 45 ++++++++------- src/render.h | 6 +- src/settings.h | 13 ++++- 8 files changed, 206 insertions(+), 47 deletions(-) diff --git a/libs/map/map.c b/libs/map/map.c index 1e93d07..74f1c80 100644 --- a/libs/map/map.c +++ b/libs/map/map.c @@ -2,18 +2,18 @@ #include "rng/rng.h" #include "settings.h" #include "utils.h" +#include #include #include #include void map_init(Map *map) { - // Fill entire map with walls for (int y = 0; y < MAP_HEIGHT; y++) { for (int x = 0; x < MAP_WIDTH; x++) { map->tiles[y][x] = TILE_WALL; } } - memset(map->visible, 0, sizeof(map->visible)); + memset(map->light_map, 0, sizeof(map->light_map)); memset(map->remembered, 0, sizeof(map->remembered)); map->room_count = 0; } @@ -300,20 +300,145 @@ 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) { if (!is_in_view_range(to_x, to_y, from_x, from_y, range)) return 0; - return has_line_of_sight(map, from_x, from_y, to_x, to_y); + if (!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; } -void calculate_visibility(Map *map, int x, int y) { - memset(map->visible, 0, sizeof(map->visible)); +static int is_solid(const Map *map, int sub_x, int sub_y) { + int map_w = MAP_WIDTH; + 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; +} - for (int ty = 0; ty < MAP_HEIGHT; ty++) { - for (int tx = 0; tx < MAP_WIDTH; tx++) { - if (is_in_view_range(tx, ty, x, y, PLAYER_VIEW_RANGE)) { - if (has_line_of_sight(map, x, y, tx, ty)) { - map->visible[ty][tx] = 1; - map->remembered[ty][tx] = 1; +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 tx = 0; tx < MAP_WIDTH; tx++) { + if (tile_brightness(map, tx, ty) > 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; } diff --git a/libs/map/map.h b/libs/map/map.h index 84f1119..765a28d 100644 --- a/libs/map/map.h +++ b/libs/map/map.h @@ -21,7 +21,9 @@ void get_random_floor_tile(Map *map, int *x, int *y, int attempts); // Visibility / Fog of War int is_in_view_range(int x, int y, int view_x, int view_y, int range); int has_line_of_sight(const Map *map, int x1, int y1, int x2, int y2); -void calculate_visibility(Map *map, int x, int y); +void compute_lighting(Map *map, const LightSource *sources, int num_sources); +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); #endif // MAP_H diff --git a/src/common.h b/src/common.h index f474781..2db9859 100644 --- a/src/common.h +++ b/src/common.h @@ -29,12 +29,19 @@ typedef struct { int x, y, w, h; } Room; +// Light source for sub-tile lighting system +typedef struct { + int x, y; + int intensity; + int range; +} LightSource; + // Map typedef struct { TileType tiles[MAP_HEIGHT][MAP_WIDTH]; Room rooms[MAX_ROOMS]; int room_count; - unsigned char visible[MAP_HEIGHT][MAP_WIDTH]; + unsigned char light_map[MAP_HEIGHT * SUB_TILE_RES][MAP_WIDTH * SUB_TILE_RES]; unsigned char remembered[MAP_HEIGHT][MAP_WIDTH]; } Map; diff --git a/src/game_state.h b/src/game_state.h index 73cdac2..40cde3f 100644 --- a/src/game_state.h +++ b/src/game_state.h @@ -68,6 +68,9 @@ typedef struct { 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 diff --git a/src/main.c b/src/main.c index e31dd99..83007cd 100644 --- a/src/main.c +++ b/src/main.c @@ -131,8 +131,12 @@ static void init_floor(GameState *gs, int floor_num) { } gs->player.floor = floor_num; - // Calculate initial visibility - calculate_visibility(&gs->map, gs->player.position.x, gs->player.position.y); + // Set initial player light and compute visibility + 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); // Spawn enemies enemy_spawn(gs->enemies, &gs->enemy_count, &gs->map, &gs->player, floor_num); @@ -228,7 +232,11 @@ static void post_action(GameState *gs, Enemy *attacked_enemy) { } // Update visibility based on player's new position - calculate_visibility(&gs->map, gs->player.position.x, gs->player.position.y); + LightSource p_light = {gs->player.position.x, gs->player.position.y, PLAYER_LIGHT_INTENSITY, PLAYER_LIGHT_RANGE}; + 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_update_all(gs->enemies, gs->enemy_count, &gs->player, &gs->map); @@ -269,7 +277,13 @@ static int handle_stun_turn(GameState *gs) { if (gs->game_over) return 1; enemy_update_all(gs->enemies, gs->enemy_count, &gs->player, &gs->map); - calculate_visibility(&gs->map, gs->player.position.x, gs->player.position.y); + { + 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) gs->game_over = 1; gs->last_message = "You are stunned!"; @@ -652,8 +666,8 @@ static void game_loop(unsigned int run_seed, FontManager *fm) { cam.offset = (Vector2){(float)gs.shake_x, (float)gs.shake_y}; BeginMode2D(cam); render_map(&gs.map, &gs.tileset); - render_items(gs.items, gs.item_count, gs.map.visible, &gs.tileset); - render_enemies(gs.enemies, gs.enemy_count, gs.map.visible, &gs.tileset, frame_counter); + render_items(gs.items, gs.item_count, &gs.map, &gs.tileset); + render_enemies(gs.enemies, gs.enemy_count, &gs.map, &gs.tileset, frame_counter); render_player(&gs.player, &gs.tileset, frame_counter); // Draw slash effect on top of entities if (gs.slash_timer > 0) { diff --git a/src/render.c b/src/render.c index e7b4b27..f18254b 100644 --- a/src/render.c +++ b/src/render.c @@ -1,6 +1,7 @@ #include "render.h" #include "items.h" #include "settings.h" +#include "map/map.h" #include "map/utils.h" #include #include @@ -69,10 +70,10 @@ void render_map(const Map *map, const Tileset *tileset) { for (int y = 0; y < MAP_HEIGHT; y++) { for (int x = 0; x < MAP_WIDTH; x++) { Rectangle dst = {(float)(x * TILE_SIZE), (float)(y * TILE_SIZE), (float)TILE_SIZE, (float)TILE_SIZE}; - int visible = map->visible[y][x]; int remembered = map->remembered[y][x]; + int brightness = tile_brightness(map, x, y); - if (!visible && !remembered) { + if (!remembered) { DrawRectangleRec(dst, (Color){5, 5, 10, 255}); continue; } @@ -102,9 +103,12 @@ void render_map(const Map *map, const Tileset *tileset) { if (tile_id >= 0 && tileset != NULL && tileset->finalized) { Rectangle src = tileset_get_region(tileset, tile_id); if (src.width > 0) { - Color tint = WHITE; - if (!visible) { - // Dim remembered tiles + Color tint; + if (brightness > 0) { + float lit = (float)brightness / 255.0f; + tint = (Color){(unsigned char)(128 + (int)(127.0f * lit)), (unsigned char)(128 + (int)(127.0f * lit)), + (unsigned char)(128 + (int)(127.0f * lit)), 255}; + } else { tint = (Color){128, 128, 128, 255}; } DrawTexturePro(tileset->atlas, src, dst, (Vector2){0, 0}, 0.0f, tint); @@ -112,11 +116,10 @@ void render_map(const Map *map, const Tileset *tileset) { } } - // Fallback to solid colors if tileset not available - Color wall_color = visible ? DARKGRAY : (Color){25, 25, 30, 255}; - Color floor_color = visible ? BLACK : (Color){15, 15, 20, 255}; - Color stairs_color = visible ? (Color){180, 160, 100, 255} : (Color){60, 55, 50, 255}; - Color door_color = visible ? (Color){139, 119, 89, 255} : (Color){60, 55, 50, 255}; + Color wall_color = brightness > 0 ? DARKGRAY : (Color){25, 25, 30, 255}; + Color floor_color = brightness > 0 ? BLACK : (Color){15, 15, 20, 255}; + Color stairs_color = brightness > 0 ? (Color){180, 160, 100, 255} : (Color){60, 55, 50, 255}; + Color door_color = brightness > 0 ? (Color){139, 119, 89, 255} : (Color){60, 55, 50, 255}; switch (map->tiles[y][x]) { case TILE_WALL: @@ -136,13 +139,13 @@ void render_map(const Map *map, const Tileset *tileset) { } } } - if (is_adjacent_to_stairs && visible) { + if (is_adjacent_to_stairs && brightness > 0) { int flicker = (int)(sinf(GetTime() * 5.0f) * 15.0f); DrawRectangleRec(dst, (Color){40 + flicker, 25, 10, 60}); } } // Grid lines - if (DRAW_GRID_LINES && visible) { + if (DRAW_GRID_LINES && brightness > 0) { DrawRectangleLines((int)dst.x, (int)dst.y, (int)dst.width, (int)dst.height, (Color){20, 20, 20, 80}); } break; @@ -151,7 +154,7 @@ void render_map(const Map *map, const Tileset *tileset) { // Make stairs very visible with bright symbol and bounce { int bounce = (int)(sinf(GetTime() * 3.0f) * 1.5f); - if (visible) + if (brightness > 0) DrawText(">", x * TILE_SIZE + 4, y * TILE_SIZE + 1 + bounce, NORM_FONT + 2, (Color){255, 255, 200, 255}); else DrawText(">", x * TILE_SIZE + 4, y * TILE_SIZE + 1 + bounce, NORM_FONT + 2, (Color){100, 90, 70, 255}); @@ -159,7 +162,7 @@ void render_map(const Map *map, const Tileset *tileset) { break; case TILE_DOOR_CLOSED: DrawRectangleRec(dst, door_color); - if (visible) { + if (brightness > 0) { DrawRectangle(x * TILE_SIZE + 2, y * TILE_SIZE + 2, TILE_SIZE - 4, TILE_SIZE - 4, (Color){100, 80, 60, 255}); DrawRectangleLines(x * TILE_SIZE + 2, y * TILE_SIZE + 2, TILE_SIZE - 4, TILE_SIZE - 4, (Color){60, 50, 40, 255}); @@ -168,7 +171,7 @@ void render_map(const Map *map, const Tileset *tileset) { break; case TILE_DOOR_OPEN: DrawRectangleRec(dst, floor_color); - if (visible) { + if (brightness > 0) { DrawRectangle(x * TILE_SIZE + 2, y * TILE_SIZE + 2, TILE_SIZE - 4, TILE_SIZE - 4, (Color){80, 70, 50, 180}); DrawRectangleLines(x * TILE_SIZE + 2, y * TILE_SIZE + 2, TILE_SIZE - 4, TILE_SIZE - 4, (Color){60, 50, 40, 200}); @@ -177,7 +180,7 @@ void render_map(const Map *map, const Tileset *tileset) { break; case TILE_DOOR_RUINED: DrawRectangleRec(dst, (Color){60, 45, 30, 255}); - if (visible) { + if (brightness > 0) { DrawRectangle(x * TILE_SIZE + 1, y * TILE_SIZE + 1, TILE_SIZE - 2, TILE_SIZE - 2, (Color){80, 60, 40, 200}); DrawLine(x * TILE_SIZE + 2, y * TILE_SIZE + 2, x * TILE_SIZE + TILE_SIZE - 2, y * TILE_SIZE + TILE_SIZE - 2, (Color){120, 90, 60, 255}); @@ -260,12 +263,11 @@ void render_player(const Player *p, const Tileset *tileset, int frame_counter) { } } -void render_enemies(const Enemy *enemies, int count, const unsigned char visible[MAP_HEIGHT][MAP_WIDTH], - const Tileset *tileset, int frame_counter) { +void render_enemies(const Enemy *enemies, int count, const Map *map, const Tileset *tileset, int frame_counter) { for (int i = 0; i < count; i++) { if (!enemies[i].alive) continue; - if (!visible[enemies[i].position.y][enemies[i].position.x]) + if (!is_tile_revealed(map, enemies[i].position.x, enemies[i].position.y)) continue; Rectangle dst = {(float)(enemies[i].position.x * TILE_SIZE), (float)(enemies[i].position.y * TILE_SIZE), @@ -367,12 +369,11 @@ void render_enemies(const Enemy *enemies, int count, const unsigned char visible } } -void render_items(const Item *items, int count, const unsigned char visible[MAP_HEIGHT][MAP_WIDTH], - const Tileset *tileset) { +void render_items(const Item *items, int count, const Map *map, const Tileset *tileset) { for (int i = 0; i < count; i++) { if (items[i].picked_up) continue; - if (!visible[items[i].y][items[i].x]) + if (!is_tile_revealed(map, items[i].x, items[i].y)) continue; Rectangle dst = {(float)(items[i].x * TILE_SIZE), (float)(items[i].y * TILE_SIZE), (float)TILE_SIZE, diff --git a/src/render.h b/src/render.h index e36bcdb..6408f62 100644 --- a/src/render.h +++ b/src/render.h @@ -108,12 +108,10 @@ 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 unsigned char visible[MAP_HEIGHT][MAP_WIDTH], - const Tileset *tileset, int frame_counter); +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 unsigned char visible[MAP_HEIGHT][MAP_WIDTH], - const Tileset *tileset); +void render_items(const Item *items, int count, const Map *map, const Tileset *tileset); // Render UI overlay void render_ui(const Player *p, const Tileset *tileset, const FontManager *fm); diff --git a/src/settings.h b/src/settings.h index 2b700ac..19ecf14 100644 --- a/src/settings.h +++ b/src/settings.h @@ -79,10 +79,19 @@ #define MESSAGE_TIMER_DURATION 60 // Visibility / Fog of War -#define PLAYER_VIEW_RANGE 8 -#define ENEMY_VIEW_RANGE 6 #define ENEMY_PATROL_MOVE_CHANCE 30 +// Sub-tile lighting +#define SUB_TILE_RES 8 +#define LIGHT_SIGHT_THRESHOLD 40 + +// 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