From 27691a28dd28b377e571fdce6ca0e38e1665829b Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 5 Mar 2026 17:22:21 +0000 Subject: [PATCH] Improve levlegen --- src/game/levelgen.c | 254 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 197 insertions(+), 57 deletions(-) diff --git a/src/game/levelgen.c b/src/game/levelgen.c index b7974eb..1a657f6 100644 --- a/src/game/levelgen.c +++ b/src/game/levelgen.c @@ -127,6 +127,72 @@ static void add_entity(Tilemap *map, const char *type, int tile_x, int tile_y) { map->entity_spawn_count++; } +/* ═══════════════════════════════════════════════════ + * Segment boundary connectivity helpers + * + * After all segments are generated, ensure_passage() + * scans each column boundary and carves a passable + * opening if none exists. This prevents rooms from + * being sealed off by adjacent segment tile writes. + * ═══════════════════════════════════════════════════ */ + +/* Check if a column has a vertical run of at least `need` empty rows + * within [row_lo, row_hi]. Returns true if passable. */ +static bool column_has_gap(const uint16_t *layer, int map_w, + int col, int row_lo, int row_hi, int need) { + int run = 0; + for (int y = row_lo; y <= row_hi; y++) { + if (layer[y * map_w + col] == TILE_EMPTY || + layer[y * map_w + col] == TILE_PLAT) { + run++; + if (run >= need) return true; + } else { + run = 0; + } + } + return false; +} + +/* Carve a 2-column-wide, 4-row-tall opening centered on ground_row. + * The opening spans columns [col, col+1] and rows [ground_row-3, ground_row]. + * This guarantees the player (12x16 px, ~1x2 tiles) can walk through. */ +static void carve_passage(uint16_t *layer, int map_w, int map_h, + int col, int ground_row) { + int top = ground_row - 3; + if (top < 1) top = 1; + int bot = ground_row; + if (bot >= map_h) bot = map_h - 1; + for (int y = top; y <= bot; y++) { + set_tile(layer, map_w, map_h, col, y, TILE_EMPTY); + set_tile(layer, map_w, map_h, col + 1, y, TILE_EMPTY); + } +} + +/* Scan all segment boundaries and ensure connectivity. + * seg_x[] holds the starting x of each segment, seg_count entries. + * seg_ground[] holds the ground row for each segment. */ +static void ensure_segment_connectivity(uint16_t *layer, int map_w, int map_h, + const int *seg_x, const int *seg_gr, + int seg_count) { + for (int i = 0; i < seg_count - 1; i++) { + int boundary_col = seg_x[i + 1]; /* first column of next segment */ + int left_col = boundary_col - 1; /* last column of this segment */ + int right_col = boundary_col; + + /* Use the lower (larger row number) ground of the two segments + * so the opening is at floor level for both sides. */ + int gr = (seg_gr[i] > seg_gr[i + 1]) ? seg_gr[i] : seg_gr[i + 1]; + + /* Check both columns at the boundary */ + bool left_ok = column_has_gap(layer, map_w, left_col, 1, gr, 3); + bool right_ok = column_has_gap(layer, map_w, right_col, 1, gr, 3); + + if (!left_ok || !right_ok) { + carve_passage(layer, map_w, map_h, left_col, gr - 1); + } + } +} + /* ═══════════════════════════════════════════════════ * Segment generators * @@ -323,15 +389,17 @@ static void gen_corridor(Tilemap *map, int x0, int w, int ground_row, fill_rect(col, mw, mh, x0, ceil_row, x0, ground_row - 1, TILE_SOLID_1); fill_rect(col, mw, mh, x0 + w - 1, ceil_row, x0 + w - 1, ground_row - 1, TILE_SOLID_1); - /* Opening in left wall (1 tile above ground to enter) */ - set_tile(col, mw, mh, x0, ground_row - 1, TILE_EMPTY); - set_tile(col, mw, mh, x0, ground_row - 2, TILE_EMPTY); - set_tile(col, mw, mh, x0, ground_row - 3, TILE_EMPTY); + /* Opening in left wall — 2 tiles wide so adjacent segment can't seal it */ + for (int r = ground_row - 3; r < ground_row; r++) { + set_tile(col, mw, mh, x0, r, TILE_EMPTY); + set_tile(col, mw, mh, x0 + 1, r, TILE_EMPTY); + } - /* Opening in right wall */ - set_tile(col, mw, mh, x0 + w - 1, ground_row - 1, TILE_EMPTY); - set_tile(col, mw, mh, x0 + w - 1, ground_row - 2, TILE_EMPTY); - set_tile(col, mw, mh, x0 + w - 1, ground_row - 3, TILE_EMPTY); + /* Opening in right wall — 2 tiles wide */ + for (int r = ground_row - 3; r < ground_row; r++) { + set_tile(col, mw, mh, x0 + w - 1, r, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 2, r, TILE_EMPTY); + } /* Theme-dependent corridor hazards */ if (theme == THEME_MARS_BASE) { @@ -492,14 +560,19 @@ static void gen_shaft(Tilemap *map, int x0, int w, int ground_row, fill_rect(col, mw, mh, x0, ceil_row, x0, ground_row - 1, TILE_SOLID_1); fill_rect(col, mw, mh, x0 + w - 1, ceil_row, x0 + w - 1, ground_row - 1, TILE_SOLID_1); - /* Opening at top */ + /* Opening at top — 2 tiles wide on each side */ set_tile(col, mw, mh, x0, ceil_row, TILE_EMPTY); + set_tile(col, mw, mh, x0 + 1, ceil_row, TILE_EMPTY); set_tile(col, mw, mh, x0 + w - 1, ceil_row, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 2, ceil_row, TILE_EMPTY); - /* Openings at bottom to enter */ + /* Openings at bottom to enter — 2 tiles wide so adjacent segments + * cannot seal them by filling their edge column solid. */ for (int r = ground_row - 3; r < ground_row; r++) { set_tile(col, mw, mh, x0, r, TILE_EMPTY); + set_tile(col, mw, mh, x0 + 1, r, TILE_EMPTY); set_tile(col, mw, mh, x0 + w - 1, r, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 2, r, TILE_EMPTY); } /* Alternating platforms up the shaft */ @@ -1137,11 +1210,13 @@ bool levelgen_generate(Tilemap *map, const LevelGenConfig *config) { /* ── Phase 4: generate segments ── */ int cursor = 2; /* start after left buffer */ int mh = map->height; + int seg_start_x[MAX_FINAL_SEGS]; /* track segment x-offsets for connectivity pass */ /* Left border wall */ fill_rect(map->collision_layer, map->width, mh, 0, 0, 1, mh - 1, TILE_SOLID_1); for (int i = 0; i < num_segs; i++) { + seg_start_x[i] = cursor; int w = seg_widths[i]; LevelTheme theme = seg_themes[i]; int gr = seg_ground[i]; @@ -1172,6 +1247,10 @@ bool levelgen_generate(Tilemap *map, const LevelGenConfig *config) { fill_rect(map->collision_layer, map->width, mh, map->width - 2, 0, map->width - 1, mh - 1, TILE_SOLID_1); + /* ── Phase 4b: ensure no segment boundary is sealed ── */ + ensure_segment_connectivity(map->collision_layer, map->width, mh, + seg_start_x, seg_ground, num_segs); + /* ── Phase 5: add visual variety to solid tiles ── */ for (int y = 0; y < map->height; y++) { for (int x = 0; x < map->width; x++) { @@ -1333,11 +1412,14 @@ static void gen_station_bulkhead(Tilemap *map, int x0, int w, float difficulty) int wall_x = x0 + w / 2; fill_rect(col, mw, mh, wall_x, STATION_CEIL_ROW + 1, wall_x, STATION_FLOOR_ROW - 1, TILE_SOLID_2); - /* Doorway opening (3 tiles tall) */ - int door_y = rng_range(STATION_CEIL_ROW + 3, STATION_FLOOR_ROW - 4); - for (int y = door_y; y < door_y + 3; y++) { + /* Doorway opening (4 tiles tall, always reachable from floor). + * Constrain the door bottom to touch the floor so the player + * can walk through without needing to jump. */ + int door_top = STATION_FLOOR_ROW - 4; + for (int y = door_top; y < STATION_FLOOR_ROW; y++) { set_tile(col, mw, mh, wall_x, y, TILE_EMPTY); } + int door_y = door_top; /* Turret guarding the doorway — always present */ add_entity(map, "turret", wall_x - 2, door_y - 1); @@ -1382,13 +1464,21 @@ static void gen_station_platforms(Tilemap *map, int x0, int w, float difficulty) } } - /* Floating platforms across the gap */ + /* Floating platforms across the gap. + * First and last platforms are pinned near floor level so the + * player can step onto/off the pit section from solid ground. */ int num_plats = rng_range(3, 5); int spacing = (pit_end - pit_start) / (num_plats + 1); if (spacing < 2) spacing = 2; for (int i = 0; i < num_plats; i++) { int px = pit_start + (i + 1) * spacing - 1; - int py = rng_range(STATION_CEIL_ROW + 4, STATION_FLOOR_ROW - 2); + int py; + if (i == 0 || i == num_plats - 1) { + /* Entry/exit platforms at floor level for walkability */ + py = STATION_FLOOR_ROW - 1; + } else { + py = rng_range(STATION_CEIL_ROW + 4, STATION_FLOOR_ROW - 2); + } int pw = rng_range(2, 3); for (int j = 0; j < pw && px + j < x0 + w; j++) { set_tile(col, mw, mh, px + j, py, TILE_PLAT); @@ -1483,14 +1573,23 @@ static void gen_station_vent(Tilemap *map, int x0, int w, float difficulty) { int vent_ceil = STATION_CEIL_ROW + 3; fill_rect(col, mw, mh, x0, STATION_CEIL_ROW + 1, x0 + w - 1, vent_ceil, TILE_SOLID_2); - /* Opening at left */ + /* Opening at left — both at vent level and at floor level so the + * player can enter from the standard corridor floor. */ for (int y = vent_ceil - 1; y <= vent_ceil + 2 && y < STATION_FLOOR_ROW; y++) { set_tile(col, mw, mh, x0, y, TILE_EMPTY); } - /* Opening at right */ + for (int y = STATION_FLOOR_ROW - 3; y < STATION_FLOOR_ROW; y++) { + set_tile(col, mw, mh, x0, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + 1, y, TILE_EMPTY); + } + /* Opening at right — same treatment */ for (int y = vent_ceil - 1; y <= vent_ceil + 2 && y < STATION_FLOOR_ROW; y++) { set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); } + for (int y = STATION_FLOOR_ROW - 3; y < STATION_FLOOR_ROW; y++) { + set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 2, y, TILE_EMPTY); + } /* Flame vents along the floor — always present, more at higher difficulty */ int num_vents = 1 + (int)(difficulty * 2); @@ -1641,6 +1740,8 @@ bool levelgen_generate_station(Tilemap *map, const LevelGenConfig *config) { /* ── Phase 4: generate segments ── */ int cursor = 2; int smh = map->height; + int sseg_start_x[20]; + int sseg_ground[20]; /* Left border wall */ fill_rect(map->collision_layer, map->width, smh, 0, 0, 1, smh - 1, TILE_SOLID_1); @@ -1650,6 +1751,8 @@ bool levelgen_generate_station(Tilemap *map, const LevelGenConfig *config) { }; for (int i = 0; i < num_segs; i++) { + sseg_start_x[i] = cursor; + sseg_ground[i] = STATION_FLOOR_ROW; int w = seg_widths[i]; float diff = config->difficulty; @@ -1670,6 +1773,10 @@ bool levelgen_generate_station(Tilemap *map, const LevelGenConfig *config) { fill_rect(map->collision_layer, map->width, smh, map->width - 2, 0, map->width - 1, smh - 1, TILE_SOLID_1); + /* ── Phase 4b: ensure no segment boundary is sealed ── */ + ensure_segment_connectivity(map->collision_layer, map->width, smh, + sseg_start_x, sseg_ground, num_segs); + /* ── Phase 5: visual variety ── */ for (int y = 0; y < map->height; y++) { for (int x = 0; x < map->width; x++) { @@ -1788,9 +1895,15 @@ static void gen_mb_entry(Tilemap *map, int x0, int w, float difficulty) { fill_rect(col, mw, mh, x0, MB_CEIL_ROW + 1, x0, MB_MID_UPPER - 1, TILE_SOLID_1); fill_rect(col, mw, mh, x0 + w - 1, MB_CEIL_ROW + 1, x0 + w - 1, MB_MID_UPPER - 1, TILE_SOLID_1); - /* Opening on the right wall to exit to the next segment */ - for (int y = MB_MID_UPPER - 4; y < MB_MID_UPPER; y++) { - set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); + /* Openings on the right wall at all standard heights so the + * next segment is reachable regardless of its layout. */ + int entry_open_rows[] = { MB_MID_UPPER, MB_MID_LOWER, MB_FLOOR_ROW }; + for (int h = 0; h < 3; h++) { + int base = entry_open_rows[h]; + for (int y = base - 4; y < base; y++) { + set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 2, y, TILE_EMPTY); + } } /* Health pickup */ @@ -1814,14 +1927,16 @@ static void gen_mb_shaft(Tilemap *map, int x0, int w, float difficulty) { fill_rect(col, mw, mh, x0, MB_CEIL_ROW + 1, x0, MB_FLOOR_ROW - 1, TILE_SOLID_1); fill_rect(col, mw, mh, x0 + w - 1, MB_CEIL_ROW + 1, x0 + w - 1, MB_FLOOR_ROW - 1, TILE_SOLID_1); - /* Openings at top and bottom of walls for connectivity */ - for (int y = MB_CEIL_ROW + 1; y < MB_CEIL_ROW + 5; y++) { - set_tile(col, mw, mh, x0, y, TILE_EMPTY); - set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); - } - for (int y = MB_FLOOR_ROW - 4; y < MB_FLOOR_ROW; y++) { - set_tile(col, mw, mh, x0, y, TILE_EMPTY); - set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); + /* Openings at all standard heights — 2 tiles wide */ + int shaft_open_rows[] = { MB_CEIL_ROW + 4, MB_MID_UPPER, MB_MID_LOWER, MB_FLOOR_ROW }; + for (int h = 0; h < 4; h++) { + int base = shaft_open_rows[h]; + for (int y = base - 4; y < base; y++) { + set_tile(col, mw, mh, x0, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + 1, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 2, y, TILE_EMPTY); + } } /* Alternating platforms climbing up the shaft */ @@ -1916,10 +2031,25 @@ static void gen_mb_corridor(Tilemap *map, int x0, int w, float difficulty) { /* Side walls with openings */ fill_rect(col, mw, mh, x0, ceil_row + 2, x0, floor_row - 1, TILE_SOLID_1); fill_rect(col, mw, mh, x0 + w - 1, ceil_row + 2, x0 + w - 1, floor_row - 1, TILE_SOLID_1); - /* Door openings */ + /* Door openings at this corridor's level — 2 tiles wide */ for (int y = floor_row - 4; y < floor_row; y++) { set_tile(col, mw, mh, x0, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + 1, y, TILE_EMPTY); set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 2, y, TILE_EMPTY); + } + /* Also open at standard heights so adjacent segments at different + * levels can still be reached. Vertical shafts or platforms inside + * the adjacent segment handle the height transition. */ + int mb_open_rows[] = { MB_MID_UPPER, MB_MID_LOWER, MB_FLOOR_ROW }; + for (int h = 0; h < 3; h++) { + int base = mb_open_rows[h]; + for (int y = base - 4; y < base; y++) { + set_tile(col, mw, mh, x0, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + 1, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 2, y, TILE_EMPTY); + } } /* Charger patrol */ @@ -1971,18 +2101,16 @@ static void gen_mb_turret_hall(Tilemap *map, int x0, int w, float difficulty) { set_tile(col, mw, mh, hole2_x + j, MB_MID_LOWER + 1, TILE_EMPTY); } - /* Door openings on sides at each level */ - for (int y = MB_MID_UPPER - 4; y < MB_MID_UPPER; y++) { - set_tile(col, mw, mh, x0, y, TILE_EMPTY); - set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); - } - for (int y = MB_MID_LOWER - 4; y < MB_MID_LOWER; y++) { - set_tile(col, mw, mh, x0, y, TILE_EMPTY); - set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); - } - for (int y = MB_FLOOR_ROW - 4; y < MB_FLOOR_ROW; y++) { - set_tile(col, mw, mh, x0, y, TILE_EMPTY); - set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); + /* Door openings on sides at all standard heights — 2 tiles wide */ + int th_open_rows[] = { MB_MID_UPPER, MB_MID_LOWER, MB_FLOOR_ROW }; + for (int h = 0; h < 3; h++) { + int base = th_open_rows[h]; + for (int y = base - 4; y < base; y++) { + set_tile(col, mw, mh, x0, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + 1, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 2, y, TILE_EMPTY); + } } /* Laser turrets on walls at each level — the gauntlet */ @@ -2040,14 +2168,16 @@ static void gen_mb_hive(Tilemap *map, int x0, int w, float difficulty) { set_tile(col, mw, mh, hole_x + j, MB_MID_LOWER + 1, TILE_EMPTY); } - /* Door openings on sides */ - for (int y = MB_MID_LOWER - 4; y < MB_MID_LOWER; y++) { - set_tile(col, mw, mh, x0, y, TILE_EMPTY); - set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); - } - for (int y = MB_FLOOR_ROW - 4; y < MB_FLOOR_ROW; y++) { - set_tile(col, mw, mh, x0, y, TILE_EMPTY); - set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); + /* Door openings at all standard heights — 2 tiles wide */ + int hive_open_rows[] = { MB_MID_UPPER, MB_MID_LOWER, MB_FLOOR_ROW }; + for (int h = 0; h < 3; h++) { + int base = hive_open_rows[h]; + for (int y = base - 4; y < base; y++) { + set_tile(col, mw, mh, x0, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + 1, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 2, y, TILE_EMPTY); + } } /* Spawner in the upper area — the hive */ @@ -2114,14 +2244,16 @@ static void gen_mb_arena(Tilemap *map, int x0, int w, float difficulty) { /* Ground floor for walking */ fill_rect(col, mw, mh, x0 + 1, MB_FLOOR_ROW - 1, x0 + w - 2, MB_FLOOR_ROW - 1, TILE_SOLID_1); - /* Door openings on sides */ - for (int y = MB_FLOOR_ROW - 5; y < MB_FLOOR_ROW - 1; y++) { - set_tile(col, mw, mh, x0, y, TILE_EMPTY); - set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); - } - for (int y = MB_CEIL_ROW + 1; y < MB_CEIL_ROW + 5; y++) { - set_tile(col, mw, mh, x0, y, TILE_EMPTY); - set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); + /* Door openings at all standard heights — 2 tiles wide */ + int arena_open_rows[] = { MB_CEIL_ROW + 4, MB_MID_UPPER, MB_MID_LOWER, MB_FLOOR_ROW }; + for (int h = 0; h < 4; h++) { + int base = arena_open_rows[h]; + for (int y = base - 4; y < base; y++) { + set_tile(col, mw, mh, x0, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + 1, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 2, y, TILE_EMPTY); + } } /* Enemies — mixed chargers, grunts, flyers across levels */ @@ -2262,6 +2394,8 @@ bool levelgen_generate_mars_base(Tilemap *map, const LevelGenConfig *config) { /* ── Phase 4: generate segments ── */ int cursor = 2; + int mb_seg_start_x[12]; + int mb_seg_ground[12]; /* Left border wall */ fill_rect(map->collision_layer, map->width, map->height, @@ -2272,6 +2406,8 @@ bool levelgen_generate_mars_base(Tilemap *map, const LevelGenConfig *config) { }; for (int i = 0; i < num_segs; i++) { + mb_seg_start_x[i] = cursor; + mb_seg_ground[i] = MB_FLOOR_ROW; int w = seg_widths[i]; float diff = config->difficulty; @@ -2292,6 +2428,10 @@ bool levelgen_generate_mars_base(Tilemap *map, const LevelGenConfig *config) { fill_rect(map->collision_layer, map->width, map->height, map->width - 2, 0, map->width - 1, map->height - 1, TILE_SOLID_1); + /* ── Phase 4b: ensure no segment boundary is sealed ── */ + ensure_segment_connectivity(map->collision_layer, map->width, map->height, + mb_seg_start_x, mb_seg_ground, num_segs); + /* ── Phase 5: visual variety (random solid variants for interior tiles) ── */ for (int y = 0; y < map->height; y++) { for (int x = 0; x < map->width; x++) {