Improve levlegen

This commit is contained in:
Thomas
2026-03-05 17:22:21 +00:00
parent 6b32199f25
commit 27691a28dd

View File

@@ -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++) {