#include "game/levelgen.h" #include "engine/parallax.h" #include #include #include #include /* ═══════════════════════════════════════════════════ * RNG — simple xorshift32 for deterministic generation * ═══════════════════════════════════════════════════ */ static uint32_t s_rng_state; static void rng_seed(uint32_t seed) { s_rng_state = seed ? seed : (uint32_t)time(NULL); if (s_rng_state == 0) s_rng_state = 1; } static uint32_t rng_next(void) { uint32_t x = s_rng_state; x ^= x << 13; x ^= x >> 17; x ^= x << 5; s_rng_state = x; return x; } /* Random int in [min, max] inclusive */ static int rng_range(int min, int max) { if (min >= max) return min; return min + (int)(rng_next() % (uint32_t)(max - min + 1)); } /* Random float in [0, 1) */ static float rng_float(void) { return (float)(rng_next() & 0xFFFF) / 65536.0f; } /* ═══════════════════════════════════════════════════ * Tile IDs used in generation * (must match TILEDEF entries we set up) * ═══════════════════════════════════════════════════ */ #define TILE_EMPTY 0 #define TILE_SOLID_1 1 /* main ground/wall tile */ #define TILE_SOLID_2 2 /* variant solid tile */ #define TILE_SOLID_3 3 /* variant solid tile */ #define TILE_PLAT 4 /* one-way platform */ /* ═══════════════════════════════════════════════════ * Segment types and templates * * Each segment is a rectangular section of tiles * with a fixed height (the full level height) and * a variable width. Segments are stitched left-to-right. * * The level has a standard height of 23 tiles * (matching level01 — fits ~1.0 screen vertically). * Ground is at row 20-22 (3 tiles thick). * ═══════════════════════════════════════════════════ */ #define SEG_HEIGHT 23 #define GROUND_ROW 20 /* first ground row (single-screen levels) */ #define FLOOR_ROWS 3 /* rows 20, 21, 22 */ /* ── Height zones for tall levels ─────────────────── */ #define TALL_HEIGHT 46 /* two screens tall */ #define ZONE_HIGH_GROUND 17 /* ground row for HIGH zone (upper area) */ #define ZONE_LOW_GROUND 43 /* ground row for LOW zone (lower area) */ typedef enum SegmentType { SEG_FLAT, /* flat ground, maybe some platforms above */ SEG_PIT, /* gap in the ground — must jump across */ SEG_PLATFORMS, /* vertical platforming section */ SEG_CORRIDOR, /* walled corridor with ceiling */ SEG_ARENA, /* open area, wider, good for combat */ SEG_SHAFT, /* vertical shaft with platforms inside */ SEG_TRANSITION, /* doorway/airlock between themes */ SEG_CLIMB, /* vertical connector between height zones */ SEG_TYPE_COUNT } SegmentType; /* ═══════════════════════════════════════════════════ * Tile placement helpers * ═══════════════════════════════════════════════════ */ static void set_tile(uint16_t *layer, int map_w, int map_h, int x, int y, uint16_t id) { if (x >= 0 && x < map_w && y >= 0 && y < map_h) { layer[y * map_w + x] = id; } } static void fill_rect(uint16_t *layer, int map_w, int map_h, int x0, int y0, int x1, int y1, uint16_t id) { for (int y = y0; y <= y1; y++) { for (int x = x0; x <= x1; x++) { set_tile(layer, map_w, map_h, x, y, id); } } } static void fill_ground(uint16_t *layer, int map_w, int map_h, int x0, int x1, int ground_row) { fill_rect(layer, map_w, map_h, x0, ground_row, x1, map_h - 1, TILE_SOLID_1); } /* Add a random solid variant tile to break up visual monotony */ static uint16_t random_solid(void) { int r = rng_range(0, 5); if (r == 0) return TILE_SOLID_2; if (r == 1) return TILE_SOLID_3; return TILE_SOLID_1; } /* ═══════════════════════════════════════════════════ * Entity spawn helpers * ═══════════════════════════════════════════════════ */ static void add_entity(Tilemap *map, const char *type, int tile_x, int tile_y) { if (map->entity_spawn_count >= MAX_ENTITY_SPAWNS) return; EntitySpawn *es = &map->entity_spawns[map->entity_spawn_count]; strncpy(es->type_name, type, 31); es->type_name[31] = '\0'; es->x = (float)(tile_x * TILE_SIZE); es->y = (float)(tile_y * TILE_SIZE); map->entity_spawn_count++; } /* ═══════════════════════════════════════════════════ * Segment generators * * Each writes tiles into the collision layer starting * at column offset `x0` for `width` columns. * Returns the effective ground connectivity at the * right edge (row of the topmost ground tile, or -1 * for no ground at the edge). * ═══════════════════════════════════════════════════ */ /* SEG_FLAT: solid ground, optionally with platforms and enemies */ static void gen_flat(Tilemap *map, int x0, int w, int ground_row, float difficulty, LevelTheme theme) { (void)theme; uint16_t *col = map->collision_layer; int mw = map->width; int mh = map->height; fill_ground(col, mw, mh, x0, x0 + w - 1, ground_row); /* Random platforms above */ int num_plats = rng_range(0, 2); for (int i = 0; i < num_plats; i++) { int px = x0 + rng_range(1, w - 5); int py = ground_row - rng_range(3, 7); int pw = rng_range(3, 5); for (int j = 0; j < pw && px + j < x0 + w; j++) { set_tile(col, mw, mh, px + j, py, TILE_PLAT); } /* Maybe a grunt on the platform */ if (difficulty > 0.3f && rng_float() < difficulty * 0.5f) { add_entity(map, "grunt", px + pw / 2, py - 1); } } /* Ground enemies */ if (rng_float() < 0.4f + difficulty * 0.4f) { add_entity(map, "grunt", x0 + rng_range(2, w - 3), ground_row - 1); } } /* SEG_PIT: gap in the ground the player must jump */ static void gen_pit(Tilemap *map, int x0, int w, int ground_row, float difficulty, LevelTheme theme) { uint16_t *col = map->collision_layer; int mw = map->width; int mh = map->height; int pit_start = rng_range(3, 5); int pit_width = rng_range(3, 5 + (int)(difficulty * 3)); if (pit_width > w - 4) pit_width = w - 4; int pit_end = pit_start + pit_width; /* Ground before pit */ fill_ground(col, mw, mh, x0, x0 + pit_start - 1, ground_row); /* Ground after pit */ if (x0 + pit_end < x0 + w) { fill_ground(col, mw, mh, x0 + pit_end, x0 + w - 1, ground_row); } /* Hazard at pit bottom — theme dependent */ if (difficulty > 0.2f && rng_float() < 0.5f) { int fx = x0 + pit_start + pit_width / 2; if (theme == THEME_PLANET_SURFACE || theme == THEME_PLANET_BASE) { /* Flame vents: natural hazard (surface) or industrial (base). * Pedestal at the bottom of the ground layer, not map bottom. */ add_entity(map, "flame_vent", fx, ground_row); int ped_top = ground_row + FLOOR_ROWS - 2; int ped_bot = ground_row + FLOOR_ROWS - 1; fill_rect(col, mw, mh, fx - 1, ped_top, fx + 1, ped_bot, TILE_SOLID_1); } else { /* Space station: force field across the pit */ add_entity(map, "force_field", fx, ground_row - 2); } } /* Stepping stones in wider pits */ if (pit_width >= 5) { int sx = x0 + pit_start + pit_width / 2; if (theme == THEME_SPACE_STATION && rng_float() < 0.6f) { /* Moving platform instead of static stepping stones */ add_entity(map, "platform", sx, ground_row - 2); } else { set_tile(col, mw, mh, sx, ground_row - 1, TILE_PLAT); set_tile(col, mw, mh, sx + 1, ground_row - 1, TILE_PLAT); } } } /* SEG_PLATFORMS: vertical platforming section */ static void gen_platforms(Tilemap *map, int x0, int w, int ground_row, float difficulty, LevelTheme theme) { uint16_t *col = map->collision_layer; int mw = map->width; int mh = map->height; /* Ground at bottom */ fill_ground(col, mw, mh, x0, x0 + w - 1, ground_row); /* Stack of platforms ascending */ int num_plats = rng_range(3, 5); /* Space station: more platforms, low gravity makes climbing easier */ if (theme == THEME_SPACE_STATION) num_plats += 1; /* Ceiling row: platforms should not go above this */ int ceil_row = ground_row - 17; if (ceil_row < 3) ceil_row = 3; for (int i = 0; i < num_plats; i++) { int py = ground_row - 3 - i * 3; if (py < ceil_row) break; int px = x0 + rng_range(1, w - 4); int pw = rng_range(2, 4); for (int j = 0; j < pw && px + j < x0 + w; j++) { set_tile(col, mw, mh, px + j, py, TILE_PLAT); } } /* Moving platform — more common on stations, less on surface */ float plat_chance = (theme == THEME_SPACE_STATION) ? 0.8f : (theme == THEME_PLANET_SURFACE) ? 0.3f : 0.5f; if (rng_float() < plat_chance) { int py = ground_row - rng_range(5, 10); bool vertical = (theme == THEME_SPACE_STATION && rng_float() < 0.5f); add_entity(map, vertical ? "platform_v" : "platform", x0 + w / 2, py); } /* Aerial threat */ if (difficulty > 0.3f && rng_float() < difficulty) { int fly_lo = ground_row - 15; int fly_hi = ground_row - 10; if (fly_lo < ceil_row) fly_lo = ceil_row; if (theme == THEME_PLANET_SURFACE) { add_entity(map, "flyer", x0 + rng_range(2, w - 3), rng_range(fly_lo, fly_hi)); } else if (theme == THEME_SPACE_STATION) { if (rng_float() < 0.5f) { add_entity(map, "turret", x0 + rng_range(2, w - 3), rng_range(fly_lo, fly_lo + 4)); } else { add_entity(map, "flyer", x0 + rng_range(2, w - 3), rng_range(fly_lo, fly_hi)); } } else { add_entity(map, "flyer", x0 + rng_range(2, w - 3), rng_range(fly_lo, fly_hi)); } } /* Fuel pickup on a high platform — reward for climbing */ if (rng_float() < 0.45f) { int top_py = ground_row - 3 - (num_plats - 1) * 3; if (top_py >= ceil_row) { add_entity(map, "powerup_fuel", x0 + rng_range(2, w - 3), top_py - 1); } } } /* SEG_CORRIDOR: walled section with ceiling */ static void gen_corridor(Tilemap *map, int x0, int w, int ground_row, float difficulty, LevelTheme theme) { uint16_t *col = map->collision_layer; int mw = map->width; int mh = map->height; int ceil_row = ground_row - rng_range(5, 8); /* Ground */ fill_ground(col, mw, mh, x0, x0 + w - 1, ground_row); /* Ceiling */ fill_rect(col, mw, mh, x0, ceil_row - 2, x0 + w - 1, ceil_row, TILE_SOLID_1); /* Walls at edges (short, just to frame) */ 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 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); /* Theme-dependent corridor hazards */ if (theme == THEME_PLANET_BASE || theme == THEME_SPACE_STATION) { /* Tech environments: turrets and force fields */ if (difficulty > 0.2f && rng_float() < 0.7f) { add_entity(map, "turret", x0 + w / 2, ceil_row + 1); } if (difficulty > 0.4f && rng_float() < 0.5f) { add_entity(map, "force_field", x0 + w / 2, ground_row - 1); } } else { /* Planet surface: flame vents leak through the floor */ if (difficulty > 0.3f && rng_float() < 0.5f) { add_entity(map, "flame_vent", x0 + rng_range(2, w - 3), ground_row - 1); } /* Rare turret even on surface (crashed tech, scavenged) */ if (difficulty > 0.5f && rng_float() < 0.3f) { add_entity(map, "turret", x0 + w / 2, ceil_row + 1); } } /* Grunt patrol inside (all themes) */ if (rng_float() < 0.5f + difficulty * 0.3f) { add_entity(map, "grunt", x0 + rng_range(2, w - 3), ground_row - 1); } /* Health pickup near the exit — reward for surviving the corridor */ if (difficulty > 0.3f && rng_float() < 0.35f) { add_entity(map, "powerup_hp", x0 + w - 3, ground_row - 1); } /* Fuel pickup hidden in the corridor */ if (rng_float() < 0.3f) { add_entity(map, "powerup_fuel", x0 + rng_range(2, w - 3), ceil_row + 2); } } /* SEG_ARENA: wide open area, multiple enemies */ static void gen_arena(Tilemap *map, int x0, int w, int ground_row, float difficulty, LevelTheme theme) { uint16_t *col = map->collision_layer; int mw = map->width; int mh = map->height; fill_ground(col, mw, mh, x0, x0 + w - 1, ground_row); /* Raised platforms on sides */ int plat_h = ground_row - rng_range(2, 4); fill_rect(col, mw, mh, x0, plat_h, x0 + 2, ground_row - 1, TILE_SOLID_1); fill_rect(col, mw, mh, x0 + w - 3, plat_h, x0 + w - 1, ground_row - 1, TILE_SOLID_1); /* Central platform */ int cp_y = ground_row - rng_range(4, 6); int cp_x = x0 + w / 2 - 2; for (int j = 0; j < 4; j++) { set_tile(col, mw, mh, cp_x + j, cp_y, TILE_PLAT); } /* Fly zone for aerial enemies */ int fly_lo = ground_row - 12; int fly_hi = ground_row - 6; if (fly_lo < 3) fly_lo = 3; /* Multiple enemies — composition depends on theme */ int num_enemies = 1 + (int)(difficulty * 3); for (int i = 0; i < num_enemies; i++) { float r = rng_float(); if (theme == THEME_PLANET_SURFACE) { if (r < 0.65f) add_entity(map, "grunt", x0 + rng_range(3, w - 4), ground_row - 1); else add_entity(map, "flyer", x0 + rng_range(3, w - 4), rng_range(fly_lo, fly_hi)); } else if (theme == THEME_SPACE_STATION) { if (r < 0.35f) add_entity(map, "grunt", x0 + rng_range(3, w - 4), ground_row - 1); else add_entity(map, "flyer", x0 + rng_range(3, w - 4), rng_range(fly_lo, fly_hi)); } else { if (r < 0.5f) add_entity(map, "grunt", x0 + rng_range(3, w - 4), ground_row - 1); else add_entity(map, "flyer", x0 + rng_range(3, w - 4), rng_range(fly_lo, fly_hi)); } } /* Theme-dependent arena hazard */ if (difficulty > 0.5f && rng_float() < 0.7f) { int side = rng_range(0, 1); int tx = side ? x0 + w - 2 : x0 + 1; if (theme == THEME_PLANET_SURFACE) { add_entity(map, "flame_vent", x0 + w / 2, ground_row - 1); } else { add_entity(map, "turret", tx, plat_h - 1); } } /* Powerup reward on the central platform (earned by clearing the arena) */ if (rng_float() < 0.5f) { if (difficulty > 0.6f && rng_float() < 0.25f) { add_entity(map, "powerup_drone", cp_x + 2, cp_y - 1); } else if (rng_float() < 0.35f) { add_entity(map, "powerup_fuel", cp_x + 2, cp_y - 1); } else { add_entity(map, "powerup_hp", cp_x + 2, cp_y - 1); } } } /* SEG_SHAFT: vertical shaft with platforms to climb */ static void gen_shaft(Tilemap *map, int x0, int w, int ground_row, float difficulty, LevelTheme theme) { uint16_t *col = map->collision_layer; int mw = map->width; int mh = map->height; /* Ground at bottom */ fill_ground(col, mw, mh, x0, x0 + w - 1, ground_row); /* Ceiling row for the shaft */ int ceil_row = ground_row - 17; if (ceil_row < 3) ceil_row = 3; /* Walls on both sides forming a shaft */ int shaft_left = x0 + 1; int shaft_right = x0 + w - 2; 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 */ set_tile(col, mw, mh, x0, ceil_row, TILE_EMPTY); set_tile(col, mw, mh, x0 + w - 1, ceil_row, TILE_EMPTY); /* Openings at bottom to enter */ 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 + w - 1, r, TILE_EMPTY); } /* Alternating platforms up the shaft */ int inner_w = shaft_right - shaft_left + 1; for (int i = 0; i < 5; i++) { int py = ground_row - 3 - i * 3; if (py < ceil_row + 1) break; bool left_side = (i % 2 == 0); int px = left_side ? shaft_left : shaft_right - 2; int pw = (inner_w > 4) ? 3 : 2; for (int j = 0; j < pw; j++) { set_tile(col, mw, mh, px + j, py, TILE_PLAT); } } /* Vertical moving platform — very common in stations */ float vplat_chance = (theme == THEME_SPACE_STATION) ? 0.8f : 0.4f; if (rng_float() < vplat_chance) { int mid_y = (ceil_row + ground_row) / 2; add_entity(map, "platform_v", x0 + w / 2, mid_y); } /* Bottom hazard — theme dependent */ if (difficulty > 0.3f && rng_float() < 0.5f) { if (theme == THEME_PLANET_SURFACE || theme == THEME_PLANET_BASE) { add_entity(map, "flame_vent", x0 + w / 2, ground_row - 1); } else { add_entity(map, "force_field", x0 + w / 2, ground_row - 2); } } /* Aerial threat in shaft */ if (difficulty > 0.4f && rng_float() < difficulty) { int mid_y = (ceil_row + ground_row) / 2; if (theme == THEME_SPACE_STATION && rng_float() < 0.4f) { int side = rng_range(0, 1); int turret_x = side ? x0 + 1 : x0 + w - 2; add_entity(map, "turret", turret_x, rng_range(ceil_row + 2, mid_y)); } else { add_entity(map, "flyer", x0 + w / 2, rng_range(ceil_row + 3, mid_y)); } } /* Fuel pickup near the top — reward for climbing */ if (rng_float() < 0.5f) { add_entity(map, "powerup_fuel", x0 + w / 2, ceil_row + 3); } } /* ═══════════════════════════════════════════════════ * Segment type selection based on theme * ═══════════════════════════════════════════════════ */ static SegmentType pick_segment_type(LevelTheme theme, int index, int total) { /* First segment is always flat (safe start) */ if (index == 0) return SEG_FLAT; /* Last segment tends to be an arena (climactic) */ if (index == total - 1 && rng_float() < 0.6f) return SEG_ARENA; /* Theme biases */ float r = rng_float(); switch (theme) { case THEME_PLANET_SURFACE: /* Open terrain: lots of flat ground, pits, arenas. Harsh alien landscape — wide and horizontal. */ if (r < 0.25f) return SEG_FLAT; if (r < 0.50f) return SEG_PIT; if (r < 0.70f) return SEG_ARENA; if (r < 0.85f) return SEG_PLATFORMS; return SEG_SHAFT; case THEME_PLANET_BASE: /* Research outpost: corridors, shafts, structured. Indoor military feel — walled and enclosed. */ if (r < 0.30f) return SEG_CORRIDOR; if (r < 0.50f) return SEG_SHAFT; if (r < 0.65f) return SEG_ARENA; if (r < 0.80f) return SEG_FLAT; if (r < 0.90f) return SEG_PLATFORMS; return SEG_PIT; case THEME_SPACE_STATION: /* Orbital station: shafts, platforms, corridors. Vertical design, low gravity — lots of verticality. */ if (r < 0.25f) return SEG_SHAFT; if (r < 0.45f) return SEG_PLATFORMS; if (r < 0.60f) return SEG_CORRIDOR; if (r < 0.75f) return SEG_ARENA; if (r < 0.90f) return SEG_FLAT; return SEG_PIT; case THEME_MARS_SURFACE: /* Red dusty exterior: spacey, wide-open, few obstacles. Charger enemies, occasional pits, wind gusts. */ if (r < 0.35f) return SEG_FLAT; if (r < 0.55f) return SEG_PIT; if (r < 0.75f) return SEG_ARENA; if (r < 0.90f) return SEG_PLATFORMS; return SEG_SHAFT; case THEME_MARS_BASE: /* Indoor Mars facility: very vertical, narrow corridors, 90-degree turns, heavy turret/spawner presence. */ if (r < 0.30f) return SEG_CORRIDOR; if (r < 0.55f) return SEG_SHAFT; if (r < 0.70f) return SEG_ARENA; if (r < 0.85f) return SEG_PLATFORMS; return SEG_FLAT; default: /* Only pick content types (exclude TRANSITION and CLIMB connectors) */ return (SegmentType)rng_range(0, SEG_SHAFT); } } /* Width for each segment type */ static int segment_width(SegmentType type) { switch (type) { case SEG_FLAT: return rng_range(8, 14); case SEG_PIT: return rng_range(10, 14); case SEG_PLATFORMS: return rng_range(10, 14); case SEG_CORRIDOR: return rng_range(10, 16); case SEG_ARENA: return rng_range(14, 20); case SEG_SHAFT: return rng_range(6, 10); case SEG_TRANSITION: return 6; /* fixed: doorway/airlock */ case SEG_CLIMB: return 10; /* fixed: vertical connector */ default: return 10; } } /* ═══════════════════════════════════════════════════ * SEG_TRANSITION — doorway/airlock between themes * * A narrow walled passage with a doorway frame. * Visually signals the environment change. * ═══════════════════════════════════════════════════ */ static void gen_transition(Tilemap *map, int x0, int w, int ground_row, float difficulty, LevelTheme from, LevelTheme to) { (void)difficulty; uint16_t *col = map->collision_layer; int mw = map->width; int mh = map->height; /* ── Ground: solid floor across the whole transition ── */ fill_ground(col, mw, mh, x0, x0 + w - 1, ground_row); /* ── Airlock structure ── * Layout (6 tiles wide): * col 0: outer wall (left bulkhead) * col 1: inner left frame * col 2-3: airlock chamber (open space) * col 4: inner right frame * col 5: outer wall (right bulkhead) * * Vertical layout is relative to ground_row: * header: ground_row-16 to ground_row-13 * inner frame: ground_row-12 to ground_row-1 * open passageway in between */ int left = x0; int right = x0 + w - 1; int header_top = ground_row - 16; if (header_top < 0) header_top = 0; int header_bot = ground_row - 13; int frame_top = ground_row - 12; /* Outer bulkhead walls — full height from header to ground */ fill_rect(col, mw, mh, left, header_top, left, ground_row - 1, TILE_SOLID_1); fill_rect(col, mw, mh, right, header_top, right, ground_row - 1, TILE_SOLID_1); /* Thick ceiling header (airlock hull) */ fill_rect(col, mw, mh, left, header_top, right, header_bot, TILE_SOLID_2); /* Inner frame columns — pillars flanking the doorway */ fill_rect(col, mw, mh, left + 1, frame_top, left + 1, ground_row - 1, TILE_SOLID_2); fill_rect(col, mw, mh, right - 1, frame_top, right - 1, ground_row - 1, TILE_SOLID_2); /* Clear the inner chamber (passable area between pillars) */ for (int y = frame_top; y < ground_row; y++) { for (int x = left + 2; x <= right - 2; x++) { set_tile(col, mw, mh, x, y, TILE_EMPTY); } } /* Door openings in the outer walls (player entry/exit) */ for (int y = ground_row - 4; y < ground_row; y++) { set_tile(col, mw, mh, left, y, TILE_EMPTY); set_tile(col, mw, mh, right, y, TILE_EMPTY); } /* Floor plating inside the airlock — one-way platform */ for (int x = left + 2; x <= right - 2; x++) { set_tile(col, mw, mh, x, ground_row - 1, TILE_PLAT); } /* ── Hazards inside the airlock ── */ int hazard_y = (frame_top + ground_row) / 2; /* Force field barrier in the doorway when entering a tech zone */ if ((to == THEME_PLANET_BASE || to == THEME_SPACE_STATION) && from == THEME_PLANET_SURFACE) { add_entity(map, "force_field", x0 + w / 2, hazard_y); } /* Force field when transitioning between base and station too */ if ((from == THEME_PLANET_BASE && to == THEME_SPACE_STATION) || (from == THEME_SPACE_STATION && to == THEME_PLANET_BASE)) { add_entity(map, "force_field", x0 + w / 2, hazard_y - 1); } } /* ═══════════════════════════════════════════════════ * SEG_CLIMB — vertical connector between height zones * * Bridges two different ground levels using a shaft * with alternating platforms. When descending (high→low) * the shaft goes from ground_from down to ground_to. * The segment width is fixed at 10 tiles. * ═══════════════════════════════════════════════════ */ static void gen_climb(Tilemap *map, int x0, int w, int ground_from, int ground_to, float difficulty, LevelTheme theme) { uint16_t *col = map->collision_layer; int mw = map->width; int mh = map->height; /* Determine the top and bottom of the shaft */ int top_ground = (ground_from < ground_to) ? ground_from : ground_to; int bot_ground = (ground_from < ground_to) ? ground_to : ground_from; /* Respect traversal direction: left side is ground_from, right is ground_to. * Ascending (from > to): left is LOW (bottom), right is HIGH (top) * Descending (from < to): left is HIGH (top), right is LOW (bottom) */ int left_ground = ground_from; int right_ground = ground_to; /* Fill ground on both levels at the edges of the segment */ fill_rect(col, mw, mh, x0, left_ground, x0 + 2, mh - 1, TILE_SOLID_1); fill_rect(col, mw, mh, x0 + w - 3, right_ground, x0 + w - 1, mh - 1, TILE_SOLID_1); /* Shaft walls span the full height range between the two zones */ int shaft_left = x0 + 2; int shaft_right = x0 + w - 3; fill_rect(col, mw, mh, shaft_left, top_ground, shaft_left, bot_ground - 1, TILE_SOLID_1); fill_rect(col, mw, mh, shaft_right, top_ground, shaft_right, bot_ground - 1, TILE_SOLID_1); /* Connect left ground to shaft entrance */ fill_rect(col, mw, mh, x0, left_ground, shaft_left, left_ground + FLOOR_ROWS - 1, TILE_SOLID_1); /* Connect right ground to shaft exit */ fill_rect(col, mw, mh, shaft_right, right_ground, x0 + w - 1, right_ground + FLOOR_ROWS - 1, TILE_SOLID_1); /* Opening at left side of shaft — player enters from ground_from level */ for (int r = left_ground - 3; r < left_ground; r++) { set_tile(col, mw, mh, shaft_left, r, TILE_EMPTY); } /* Opening at right side of shaft — player exits to ground_to level */ for (int r = right_ground - 3; r < right_ground; r++) { set_tile(col, mw, mh, shaft_right, r, TILE_EMPTY); } /* Alternating platforms inside the shaft */ int shaft_inner_w = shaft_right - shaft_left - 1; int shaft_depth = bot_ground - top_ground; int num_plats = shaft_depth / 4; if (num_plats < 3) num_plats = 3; if (num_plats > 8) num_plats = 8; for (int i = 0; i < num_plats; i++) { int py = top_ground + 2 + i * (shaft_depth - 4) / num_plats; if (py >= bot_ground - 1) break; bool left_side = (i % 2 == 0); int px = left_side ? shaft_left + 1 : shaft_right - 3; int pw = (shaft_inner_w > 4) ? 3 : 2; for (int j = 0; j < pw; j++) { set_tile(col, mw, mh, px + j, py, TILE_PLAT); } } /* Vertical moving platform in the shaft center */ if (rng_float() < 0.5f) { int mid_x = (shaft_left + shaft_right) / 2; int mid_y = (top_ground + bot_ground) / 2; add_entity(map, "platform_v", mid_x, mid_y); } /* Aerial threat inside the shaft */ if (difficulty > 0.3f && rng_float() < difficulty) { int mid_y = (top_ground + bot_ground) / 2; if (theme == THEME_SPACE_STATION && rng_float() < 0.4f) { int turret_x = (rng_range(0, 1)) ? shaft_left + 1 : shaft_right - 1; add_entity(map, "turret", turret_x, mid_y); } else { add_entity(map, "flyer", (shaft_left + shaft_right) / 2, mid_y); } } /* Fuel pickup midway through the climb */ if (rng_float() < 0.5f) { int mid_y = (top_ground + bot_ground) / 2; add_entity(map, "powerup_fuel", (shaft_left + shaft_right) / 2, mid_y - 2); } } /* ═══════════════════════════════════════════════════ * Theme sequence helpers * ═══════════════════════════════════════════════════ */ /* Get the theme for a given segment index from the config */ static LevelTheme theme_for_segment(const LevelGenConfig *config, int seg_idx) { if (config->theme_count <= 0) return THEME_PLANET_SURFACE; if (seg_idx >= config->theme_count) return config->themes[config->theme_count - 1]; return config->themes[seg_idx]; } /* Get gravity for a theme. * Surface: low gravity alien world — floaty jumps. * Base: normal Earth-like artificial gravity. * Station: heavy centrifugal / mag-lock gravity. */ static float gravity_for_theme(LevelTheme theme) { switch (theme) { case THEME_PLANET_SURFACE: return 400.0f; case THEME_PLANET_BASE: return 600.0f; case THEME_SPACE_STATION: return 750.0f; case THEME_MARS_SURFACE: return 370.0f; /* Mars: ~0.38g */ case THEME_MARS_BASE: return 700.0f; /* artificial gravity */ default: return 600.0f; } } /* Get background color for a theme */ static SDL_Color bg_color_for_theme(LevelTheme theme) { switch (theme) { case THEME_PLANET_SURFACE: return (SDL_Color){12, 8, 20, 255}; case THEME_PLANET_BASE: return (SDL_Color){10, 14, 22, 255}; case THEME_SPACE_STATION: return (SDL_Color){5, 5, 18, 255}; case THEME_MARS_SURFACE: return (SDL_Color){30, 12, 8, 255}; case THEME_MARS_BASE: return (SDL_Color){18, 10, 8, 255}; default: return (SDL_Color){15, 15, 30, 255}; } } /* Map theme to parallax visual style */ static ParallaxStyle parallax_style_for_theme(LevelTheme theme) { switch (theme) { case THEME_PLANET_SURFACE: return PARALLAX_STYLE_ALIEN_SKY; case THEME_PLANET_BASE: return PARALLAX_STYLE_INTERIOR; case THEME_SPACE_STATION: return PARALLAX_STYLE_DEEP_SPACE; case THEME_MARS_SURFACE: return PARALLAX_STYLE_MARS; case THEME_MARS_BASE: return PARALLAX_STYLE_INTERIOR; default: return PARALLAX_STYLE_DEFAULT; } } static const char *theme_label(LevelTheme t) { switch (t) { case THEME_PLANET_SURFACE: return "surface"; case THEME_PLANET_BASE: return "base"; case THEME_SPACE_STATION: return "station"; case THEME_MARS_SURFACE: return "mars_surf"; case THEME_MARS_BASE: return "mars_base"; default: return "?"; } } /* ═══════════════════════════════════════════════════ * Background decoration (bg_layer) * ═══════════════════════════════════════════════════ */ static void gen_bg_decoration(Tilemap *map) { /* Scatter some background tiles for visual depth */ uint16_t *bg = map->bg_layer; int mw = map->width; for (int y = 0; y < map->height - FLOOR_ROWS; y++) { for (int x = 0; x < mw; x++) { /* Only place bg tiles where collision is empty */ if (map->collision_layer[y * mw + x] != TILE_EMPTY) continue; /* Sparse random decoration */ if (rng_float() < 0.02f) { bg[y * mw + x] = (uint16_t)rng_range(2, 3); } } } } /* ═══════════════════════════════════════════════════ * Main generator * ═══════════════════════════════════════════════════ */ LevelGenConfig levelgen_default_config(void) { LevelGenConfig config = { .seed = 0, .num_segments = 5, .difficulty = 0.5f, .gravity = 0, /* 0 = let theme decide */ .theme_count = 1, }; config.themes[0] = THEME_SPACE_STATION; return config; } bool levelgen_generate(Tilemap *map, const LevelGenConfig *config) { if (!map || !config) return false; rng_seed(config->seed); int num_segs = config->num_segments; if (num_segs < 2) num_segs = 2; if (num_segs > 10) num_segs = 10; /* ── Phase 1: decide segment types, widths, and height zones ── */ /* We may insert transition/climb segments between boundaries, * so the final count can be larger than num_segs. * Max: num_segs content + (num_segs-1) connectors*2 = ~3*num_segs */ #define MAX_FINAL_SEGS 32 SegmentType seg_types[MAX_FINAL_SEGS]; int seg_widths[MAX_FINAL_SEGS]; LevelTheme seg_themes[MAX_FINAL_SEGS]; /* per-segment theme */ LevelTheme seg_from[MAX_FINAL_SEGS]; /* for transitions */ int seg_ground[MAX_FINAL_SEGS]; /* ground row per segment */ int final_seg_count = 0; int total_width = 0; bool uses_tall = false; /* track if any zone change occurs */ /* Determine zone preference per theme: * Surface: stays LOW (bottom of tall map, or normal if single-screen) * Base: uses HIGH (upper area of tall map) * Station: alternates between HIGH and LOW */ for (int i = 0; i < num_segs; i++) { LevelTheme t = theme_for_segment(config, i); /* Insert a transition segment at theme boundaries */ if (i > 0) { LevelTheme prev_t = theme_for_segment(config, i - 1); if (t != prev_t && final_seg_count < MAX_FINAL_SEGS) { seg_types[final_seg_count] = SEG_TRANSITION; seg_widths[final_seg_count] = segment_width(SEG_TRANSITION); seg_themes[final_seg_count] = t; seg_from[final_seg_count] = prev_t; seg_ground[final_seg_count] = 0; /* filled in later */ total_width += seg_widths[final_seg_count]; final_seg_count++; } } if (final_seg_count >= MAX_FINAL_SEGS) break; seg_types[final_seg_count] = pick_segment_type(t, i, num_segs); seg_widths[final_seg_count] = segment_width(seg_types[final_seg_count]); seg_themes[final_seg_count] = t; seg_from[final_seg_count] = t; seg_ground[final_seg_count] = 0; /* filled in later */ total_width += seg_widths[final_seg_count]; final_seg_count++; } num_segs = final_seg_count; /* ── Phase 1b: assign height zones per segment ── */ /* Decide if this level should be tall based on theme variety. * A level with both Surface and Base/Station themes gets tall * to give each theme its own vertical zone. Single-theme levels * stay at the standard 23-tile height. */ bool has_surface = false, has_base = false, has_station = false; bool has_mars_surf = false, has_mars_base = false; for (int i = 0; i < num_segs; i++) { if (seg_themes[i] == THEME_PLANET_SURFACE) has_surface = true; if (seg_themes[i] == THEME_PLANET_BASE) has_base = true; if (seg_themes[i] == THEME_SPACE_STATION) has_station = true; if (seg_themes[i] == THEME_MARS_SURFACE) has_mars_surf = true; if (seg_themes[i] == THEME_MARS_BASE) has_mars_base = true; } /* Go tall if we have theme variety that benefits from verticality */ uses_tall = (has_surface && (has_base || has_station)) || (has_mars_surf && has_mars_base) || (has_station && num_segs >= 5); if (uses_tall) { /* Assign ground rows based on theme: * Surface → LOW zone (ground at row 43, near bottom) * Base → HIGH zone (ground at row 17, near top) * Station → alternates, preferring HIGH */ int station_zone_counter = 0; for (int i = 0; i < num_segs; i++) { if (seg_types[i] == SEG_TRANSITION) { /* Skip: ground row filled in from predecessor below */ continue; } switch (seg_themes[i]) { case THEME_PLANET_SURFACE: case THEME_MARS_SURFACE: seg_ground[i] = ZONE_LOW_GROUND; break; case THEME_PLANET_BASE: case THEME_MARS_BASE: seg_ground[i] = ZONE_HIGH_GROUND; break; case THEME_SPACE_STATION: /* Alternate every 2 segments */ seg_ground[i] = (station_zone_counter / 2 % 2 == 0) ? ZONE_HIGH_GROUND : ZONE_LOW_GROUND; station_zone_counter++; break; default: seg_ground[i] = ZONE_LOW_GROUND; break; } } /* Fill in transition segment ground rows from their predecessor. * Transitions must be walkable from the previous segment; if the * zone also changes, a SEG_CLIMB is inserted *after* the transition * to bridge to the new elevation. */ for (int i = 0; i < num_segs; i++) { if (seg_types[i] == SEG_TRANSITION) { /* Find predecessor ground row */ int prev_gr = ZONE_LOW_GROUND; for (int j = i - 1; j >= 0; j--) { if (seg_ground[j] != 0) { prev_gr = seg_ground[j]; break; } } seg_ground[i] = prev_gr; } } /* Insert SEG_CLIMB segments where the ground level changes. * Climbs are inserted between any two adjacent segments whose * ground rows differ, including after transitions that stayed * at the old elevation while the next segment needs a new one. */ SegmentType tmp_types[MAX_FINAL_SEGS]; int tmp_widths[MAX_FINAL_SEGS]; LevelTheme tmp_themes[MAX_FINAL_SEGS]; LevelTheme tmp_from[MAX_FINAL_SEGS]; int tmp_ground[MAX_FINAL_SEGS]; int new_count = 0; int climb_width_total = 0; for (int i = 0; i < num_segs; i++) { /* Check if we need a climb before this segment */ if (i > 0 && new_count > 0 && seg_ground[i] != 0 && tmp_ground[new_count - 1] != seg_ground[i] && new_count < MAX_FINAL_SEGS) { /* Insert climb connector */ tmp_types[new_count] = SEG_CLIMB; tmp_widths[new_count] = segment_width(SEG_CLIMB); tmp_themes[new_count] = seg_themes[i]; tmp_from[new_count] = seg_themes[i > 0 ? i - 1 : i]; tmp_ground[new_count] = seg_ground[i]; /* target zone */ climb_width_total += tmp_widths[new_count]; new_count++; } if (new_count < MAX_FINAL_SEGS) { tmp_types[new_count] = seg_types[i]; tmp_widths[new_count] = seg_widths[i]; tmp_themes[new_count] = seg_themes[i]; tmp_from[new_count] = seg_from[i]; tmp_ground[new_count] = seg_ground[i]; new_count++; } } /* Copy back */ num_segs = new_count; total_width += climb_width_total; for (int i = 0; i < num_segs; i++) { seg_types[i] = tmp_types[i]; seg_widths[i] = tmp_widths[i]; seg_themes[i] = tmp_themes[i]; seg_from[i] = tmp_from[i]; seg_ground[i] = tmp_ground[i]; } } else { /* Standard single-screen height: all segments use GROUND_ROW */ for (int i = 0; i < num_segs; i++) { seg_ground[i] = GROUND_ROW; } } /* Add 2-tile buffer on each side */ total_width += 4; /* ── Phase 2: allocate tilemap ── */ int level_height = uses_tall ? TALL_HEIGHT : SEG_HEIGHT; memset(map, 0, sizeof(Tilemap)); map->width = total_width; map->height = level_height; int total_tiles = map->width * map->height; map->collision_layer = calloc(total_tiles, sizeof(uint16_t)); map->bg_layer = calloc(total_tiles, sizeof(uint16_t)); map->fg_layer = calloc(total_tiles, sizeof(uint16_t)); if (!map->collision_layer || !map->bg_layer || !map->fg_layer) { fprintf(stderr, "levelgen: failed to allocate layers\n"); return false; } /* ── Phase 3: set tile definitions ── */ map->tile_defs[1] = (TileDef){0, 0, TILE_SOLID}; /* main solid */ map->tile_defs[2] = (TileDef){1, 0, TILE_SOLID}; /* solid variant */ map->tile_defs[3] = (TileDef){2, 0, TILE_SOLID}; /* solid variant */ map->tile_defs[4] = (TileDef){0, 1, TILE_PLATFORM}; /* one-way */ map->tile_def_count = 5; /* ── Phase 4: generate segments ── */ int cursor = 2; /* start after left buffer */ int mh = map->height; /* 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++) { int w = seg_widths[i]; LevelTheme theme = seg_themes[i]; int gr = seg_ground[i]; switch (seg_types[i]) { case SEG_FLAT: gen_flat(map, cursor, w, gr, config->difficulty, theme); break; case SEG_PIT: gen_pit(map, cursor, w, gr, config->difficulty, theme); break; case SEG_PLATFORMS: gen_platforms(map, cursor, w, gr, config->difficulty, theme); break; case SEG_CORRIDOR: gen_corridor(map, cursor, w, gr, config->difficulty, theme); break; case SEG_ARENA: gen_arena(map, cursor, w, gr, config->difficulty, theme); break; case SEG_SHAFT: gen_shaft(map, cursor, w, gr, config->difficulty, theme); break; case SEG_TRANSITION: gen_transition(map, cursor, w, gr, config->difficulty, seg_from[i], theme); break; case SEG_CLIMB: { /* Find the previous segment's ground row */ int prev_gr = (i > 0) ? seg_ground[i - 1] : gr; gen_climb(map, cursor, w, prev_gr, gr, config->difficulty, theme); break; } default: gen_flat(map, cursor, w, gr, config->difficulty, theme); break; } cursor += w; } /* Right border wall */ fill_rect(map->collision_layer, map->width, mh, map->width - 2, 0, map->width - 1, mh - 1, TILE_SOLID_1); /* ── Phase 5: add visual variety to solid tiles ── */ for (int y = 0; y < map->height; y++) { for (int x = 0; x < map->width; x++) { int idx = y * map->width + x; if (map->collision_layer[idx] == TILE_SOLID_1) { /* Only vary interior tiles (not edge tiles) */ bool has_air_neighbor = false; if (y > 0 && map->collision_layer[(y-1)*map->width+x] == 0) has_air_neighbor = true; if (!has_air_neighbor) { map->collision_layer[idx] = random_solid(); } } } } /* ── Phase 6: background decoration ── */ gen_bg_decoration(map); /* ── Phase 7: metadata ── */ int first_ground = seg_ground[0]; map->player_spawn = vec2(3.0f * TILE_SIZE, (first_ground - 2) * TILE_SIZE); /* Theme-based gravity and atmosphere (uses first theme in sequence) */ LevelTheme primary_theme = config->themes[0]; map->gravity = config->gravity > 0 ? config->gravity : gravity_for_theme(primary_theme); map->bg_color = bg_color_for_theme(primary_theme); map->has_bg_color = true; map->parallax_style = (int)parallax_style_for_theme(primary_theme); /* Select tileset based on theme. Mars themes use a dedicated tileset; * all others use the default. level_load_generated() loads the texture. */ if (primary_theme == THEME_MARS_SURFACE || primary_theme == THEME_MARS_BASE) { snprintf(map->tileset_path, sizeof(map->tileset_path), "%s", "assets/tiles/mars_tileset.png"); } /* Exit zone at the far-right end of the level. * Placed just inside the right border wall, 2 tiles wide, 3 tiles tall * sitting on the ground row. Target "generate" chains to another * procedurally generated level. */ if (map->exit_zone_count < MAX_EXIT_ZONES) { ExitZone *ez = &map->exit_zones[map->exit_zone_count++]; int last_ground = seg_ground[num_segs - 1]; int exit_x = map->width - 5; /* a few tiles from the right wall */ int exit_y = last_ground - 3; /* 3 tiles above ground */ ez->x = (float)(exit_x * TILE_SIZE); ez->y = (float)(exit_y * TILE_SIZE); ez->w = 2.0f * TILE_SIZE; ez->h = 3.0f * TILE_SIZE; snprintf(ez->target, sizeof(ez->target), "generate"); /* Clear any solid tiles in the exit zone area so the player can walk into it */ for (int y = exit_y; y < exit_y + 3 && y < map->height; y++) { for (int x = exit_x; x < exit_x + 2 && x < map->width; x++) { map->collision_layer[y * map->width + x] = 0; } } } /* Music */ snprintf(map->music_path, sizeof(map->music_path), "assets/sounds/algardalgar.ogg"); /* Tileset */ /* NOTE: tileset texture will be loaded by level_load_generated */ /* Segment type names for debug output */ static const char *seg_names[] = { "flat", "pit", "plat", "corr", "arena", "shaft", "trans", "climb" }; printf("levelgen: generated %dx%d level (%d segments, seed=%u%s)\n", map->width, map->height, num_segs, s_rng_state, uses_tall ? ", tall" : ""); printf(" segments:"); for (int i = 0; i < num_segs; i++) { printf(" %s[%s@%d]", seg_names[seg_types[i]], theme_label(seg_themes[i]), seg_ground[i]); } printf("\n"); return true; } /* ═══════════════════════════════════════════════════ * Space Station Generator * * A dedicated generator for long, narrow, low-gravity * space station levels. Uses the standard 23-tile height * but fills ceiling and floor to create a narrower * playable corridor (roughly 10 tiles of vertical space). * * Segment types are biased toward horizontal layouts: * corridors, flat sections, arenas. Shafts are replaced * by wider horizontal variants. * ═══════════════════════════════════════════════════ */ /* Station layout constants */ #define STATION_CEIL_ROW 4 /* ceiling bottom edge (rows 0-4 solid) */ #define STATION_FLOOR_ROW 17 /* floor top edge (rows 17-22 solid) */ #define STATION_PLAY_H 12 /* playable rows: 5-16 inclusive */ static void station_fill_envelope(uint16_t *col, int mw, int mh, int x0, int x1) { /* Ceiling: rows 0 through STATION_CEIL_ROW */ fill_rect(col, mw, mh, x0, 0, x1, STATION_CEIL_ROW, TILE_SOLID_1); /* Floor: rows STATION_FLOOR_ROW through bottom */ fill_rect(col, mw, mh, x0, STATION_FLOOR_ROW, x1, SEG_HEIGHT - 1, TILE_SOLID_1); } /* ── Station segment: long flat corridor ── */ static void gen_station_corridor(Tilemap *map, int x0, int w, float difficulty) { uint16_t *col = map->collision_layer; int mw = map->width; int mh = map->height; station_fill_envelope(col, mw, mh, x0, x0 + w - 1); /* Random platforms floating in the corridor */ int num_plats = rng_range(1, 3); for (int i = 0; i < num_plats; i++) { int px = x0 + rng_range(2, w - 5); int py = rng_range(STATION_CEIL_ROW + 3, STATION_FLOOR_ROW - 3); int pw = rng_range(2, 4); for (int j = 0; j < pw && px + j < x0 + w; j++) { set_tile(col, mw, mh, px + j, py, TILE_PLAT); } } /* Ground patrols — always at least 1, scales with difficulty */ int num_grunts = 1 + (int)(difficulty * 2); for (int i = 0; i < num_grunts; i++) { add_entity(map, "grunt", x0 + rng_range(3, w - 4), STATION_FLOOR_ROW - 1); } /* Flyers in the corridor */ int num_flyers = (int)(difficulty * 2); for (int i = 0; i < num_flyers; i++) { add_entity(map, "flyer", x0 + rng_range(3, w - 4), rng_range(STATION_CEIL_ROW + 2, STATION_FLOOR_ROW - 3)); } /* Turret at high difficulty */ if (difficulty > 0.6f && rng_float() < 0.5f) { add_entity(map, "turret", x0 + rng_range(3, w - 4), STATION_CEIL_ROW + 1); } } /* ── Station segment: bulkhead (walls with doorway) ── */ static void gen_station_bulkhead(Tilemap *map, int x0, int w, float difficulty) { uint16_t *col = map->collision_layer; int mw = map->width; int mh = map->height; station_fill_envelope(col, mw, mh, x0, x0 + w - 1); /* Central bulkhead wall with a doorway */ 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++) { set_tile(col, mw, mh, wall_x, y, TILE_EMPTY); } /* Turret guarding the doorway — always present */ add_entity(map, "turret", wall_x - 2, door_y - 1); /* Second turret on the other side at higher difficulty */ if (difficulty > 0.6f) { add_entity(map, "turret", wall_x + 2, door_y - 1); } /* Force field in the doorway */ if (difficulty > 0.4f && rng_float() < 0.6f) { add_entity(map, "force_field", wall_x, door_y + 1); } /* Grunts on both sides of the bulkhead */ add_entity(map, "grunt", x0 + rng_range(2, w / 2 - 2), STATION_FLOOR_ROW - 1); if (rng_float() < 0.5f + difficulty * 0.5f) { add_entity(map, "grunt", x0 + rng_range(w / 2 + 2, w - 3), STATION_FLOOR_ROW - 1); } /* Flyer patrol */ if (difficulty > 0.4f) { add_entity(map, "flyer", x0 + rng_range(3, w - 4), rng_range(STATION_CEIL_ROW + 2, STATION_FLOOR_ROW - 3)); } } /* ── Station segment: platform gauntlet ── */ static void gen_station_platforms(Tilemap *map, int x0, int w, float difficulty) { uint16_t *col = map->collision_layer; int mw = map->width; int mh = map->height; station_fill_envelope(col, mw, mh, x0, x0 + w - 1); /* Remove some floor sections to create pits (lethal in a station) */ int pit_start = x0 + rng_range(3, 6); int pit_end = x0 + w - rng_range(3, 6); for (int x = pit_start; x < pit_end && x < x0 + w; x++) { for (int y = STATION_FLOOR_ROW; y < STATION_FLOOR_ROW + 2; y++) { set_tile(col, mw, mh, x, y, TILE_EMPTY); } } /* Floating platforms across the gap */ 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 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); } } /* Moving platform */ if (rng_float() < 0.7f) { bool vertical = rng_float() < 0.4f; int mx = pit_start + (pit_end - pit_start) / 2; int my = rng_range(STATION_CEIL_ROW + 4, STATION_FLOOR_ROW - 3); add_entity(map, vertical ? "platform_v" : "platform", mx, my); } /* Flyers guarding the gap — always at least 1 */ int num_flyers = 1 + (int)(difficulty * 2); for (int i = 0; i < num_flyers; i++) { add_entity(map, "flyer", x0 + rng_range(3, w - 4), rng_range(STATION_CEIL_ROW + 2, STATION_FLOOR_ROW - 4)); } /* Turret overlooking the pit */ if (difficulty > 0.5f) { add_entity(map, "turret", x0 + rng_range(2, 4), STATION_CEIL_ROW + 1); } } /* ── Station segment: combat bay (wider arena) ── */ static void gen_station_bay(Tilemap *map, int x0, int w, float difficulty) { uint16_t *col = map->collision_layer; int mw = map->width; int mh = map->height; station_fill_envelope(col, mw, mh, x0, x0 + w - 1); /* Open up a taller space by raising the ceiling locally */ int bay_x0 = x0 + 2; int bay_x1 = x0 + w - 3; for (int x = bay_x0; x <= bay_x1; x++) { set_tile(col, mw, mh, x, STATION_CEIL_ROW, TILE_EMPTY); set_tile(col, mw, mh, x, STATION_CEIL_ROW - 1, TILE_EMPTY); } /* Central floating platform */ int cp_x = x0 + w / 2 - 2; int cp_y = rng_range(STATION_CEIL_ROW + 2, STATION_FLOOR_ROW - 5); for (int j = 0; j < 4; j++) { set_tile(col, mw, mh, cp_x + j, cp_y, TILE_PLAT); } /* Ledges on the sides */ fill_rect(col, mw, mh, x0, STATION_FLOOR_ROW - 3, x0 + 1, STATION_FLOOR_ROW - 1, TILE_SOLID_1); fill_rect(col, mw, mh, x0 + w - 2, STATION_FLOOR_ROW - 3, x0 + w - 1, STATION_FLOOR_ROW - 1, TILE_SOLID_1); /* Swarm of enemies — bays are the big combat encounters */ int num_enemies = 3 + (int)(difficulty * 4); for (int i = 0; i < num_enemies; i++) { float r = rng_float(); if (r < 0.30f) { add_entity(map, "grunt", x0 + rng_range(3, w - 4), STATION_FLOOR_ROW - 1); } else { add_entity(map, "flyer", x0 + rng_range(3, w - 4), rng_range(STATION_CEIL_ROW + 1, STATION_FLOOR_ROW - 4)); } } /* Turrets on both side ledges */ add_entity(map, "turret", x0 + 1, STATION_FLOOR_ROW - 4); if (difficulty > 0.5f) { add_entity(map, "turret", x0 + w - 2, STATION_FLOOR_ROW - 4); } /* Powerup on the central platform — reward for surviving */ if (rng_float() < 0.6f) { if (difficulty > 0.5f && rng_float() < 0.3f) { add_entity(map, "powerup_drone", cp_x + 2, cp_y - 1); } else { add_entity(map, "powerup_hp", cp_x + 2, cp_y - 1); } } } /* ── Station segment: vent / crawlspace ── */ static void gen_station_vent(Tilemap *map, int x0, int w, float difficulty) { uint16_t *col = map->collision_layer; int mw = map->width; int mh = map->height; station_fill_envelope(col, mw, mh, x0, x0 + w - 1); /* Lower the ceiling even more for a tight crawlspace */ 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 */ 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 = vent_ceil - 1; y <= vent_ceil + 2 && y < STATION_FLOOR_ROW; y++) { set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); } /* Flame vents along the floor — always present, more at higher difficulty */ int num_vents = 1 + (int)(difficulty * 2); for (int i = 0; i < num_vents; i++) { add_entity(map, "flame_vent", x0 + rng_range(2, w - 3), STATION_FLOOR_ROW - 1); } /* Force field obstacle */ if (difficulty > 0.3f && rng_float() < 0.6f) { add_entity(map, "force_field", x0 + w / 2, vent_ceil + 2); } /* Grunt lurking in the vent */ if (rng_float() < 0.4f + difficulty * 0.4f) { add_entity(map, "grunt", x0 + rng_range(2, w - 3), STATION_FLOOR_ROW - 1); } /* Fuel pickup reward (useful in low gravity) */ if (rng_float() < 0.4f) { add_entity(map, "powerup_fuel", x0 + rng_range(2, w - 3), vent_ceil + 1); } } /* ── Station segment: airlock entry (first segment) ── */ static void gen_station_entry(Tilemap *map, int x0, int w, float difficulty) { uint16_t *col = map->collision_layer; int mw = map->width; int mh = map->height; (void)col; (void)mw; (void)mh; /* used by station_fill_envelope */ station_fill_envelope(col, mw, mh, x0, x0 + w - 1); /* Health pickup to start — always present */ add_entity(map, "powerup_hp", x0 + w / 2, STATION_FLOOR_ROW - 1); /* At higher difficulty, even the entry isn't safe */ if (difficulty > 0.6f) { add_entity(map, "grunt", x0 + rng_range(w / 2 + 2, w - 3), STATION_FLOOR_ROW - 1); } if (difficulty > 0.8f) { add_entity(map, "flyer", x0 + rng_range(3, w - 4), rng_range(STATION_CEIL_ROW + 2, STATION_FLOOR_ROW - 3)); } } /* Segment type selection for station */ typedef enum StationSegType { SSEG_ENTRY, SSEG_CORRIDOR, SSEG_BULKHEAD, SSEG_PLATFORMS, SSEG_BAY, SSEG_VENT, SSEG_TYPE_COUNT } StationSegType; static StationSegType pick_station_segment(int index, int total) { if (index == 0) return SSEG_ENTRY; if (index == total - 1 && rng_float() < 0.6f) return SSEG_BAY; float r = rng_float(); if (r < 0.25f) return SSEG_CORRIDOR; if (r < 0.45f) return SSEG_BULKHEAD; if (r < 0.65f) return SSEG_PLATFORMS; if (r < 0.80f) return SSEG_BAY; return SSEG_VENT; } static int station_segment_width(StationSegType type) { switch (type) { case SSEG_ENTRY: return rng_range(12, 16); case SSEG_CORRIDOR: return rng_range(18, 28); case SSEG_BULKHEAD: return rng_range(16, 22); case SSEG_PLATFORMS: return rng_range(20, 30); case SSEG_BAY: return rng_range(24, 34); case SSEG_VENT: return rng_range(14, 20); default: return 18; } } LevelGenConfig levelgen_station_config(uint32_t seed, int depth) { if (depth < 0) depth = 0; /* Segments grow with depth: 10 -> 11 -> 12 -> 13 -> 14 (capped) */ int segments = 10 + depth; if (segments > 14) segments = 14; /* Difficulty ramps up: 0.5 -> 0.65 -> 0.8 -> 0.9 -> 1.0 (capped) */ float diff = 0.5f + depth * 0.15f; if (diff > 1.0f) diff = 1.0f; LevelGenConfig config = { .seed = seed, .num_segments = segments, .difficulty = diff, .gravity = 150.0f, /* near-zero: floaty space station */ .theme_count = 1, }; config.themes[0] = THEME_SPACE_STATION; return config; } bool levelgen_generate_station(Tilemap *map, const LevelGenConfig *config) { if (!map || !config) return false; rng_seed(config->seed); int num_segs = config->num_segments; if (num_segs < 3) num_segs = 3; if (num_segs > 14) num_segs = 14; /* ── Phase 1: decide segment types and widths ── */ StationSegType seg_types[20]; int seg_widths[20]; int total_width = 0; for (int i = 0; i < num_segs && i < 20; i++) { seg_types[i] = pick_station_segment(i, num_segs); seg_widths[i] = station_segment_width(seg_types[i]); total_width += seg_widths[i]; } /* Add 2-tile buffer on each side */ total_width += 4; /* ── Phase 2: allocate tilemap ── */ memset(map, 0, sizeof(Tilemap)); map->width = total_width; map->height = SEG_HEIGHT; int total_tiles = map->width * map->height; map->collision_layer = calloc(total_tiles, sizeof(uint16_t)); map->bg_layer = calloc(total_tiles, sizeof(uint16_t)); map->fg_layer = calloc(total_tiles, sizeof(uint16_t)); if (!map->collision_layer || !map->bg_layer || !map->fg_layer) { fprintf(stderr, "levelgen_station: failed to allocate layers\n"); return false; } /* ── Phase 3: tile definitions ── */ map->tile_defs[1] = (TileDef){0, 0, TILE_SOLID}; map->tile_defs[2] = (TileDef){1, 0, TILE_SOLID}; map->tile_defs[3] = (TileDef){2, 0, TILE_SOLID}; map->tile_defs[4] = (TileDef){0, 1, TILE_PLATFORM}; map->tile_def_count = 5; /* ── Phase 4: generate segments ── */ int cursor = 2; int smh = map->height; /* Left border wall */ fill_rect(map->collision_layer, map->width, smh, 0, 0, 1, smh - 1, TILE_SOLID_1); static const char *sseg_names[] = { "entry", "corr", "bulk", "plat", "bay", "vent" }; for (int i = 0; i < num_segs; i++) { int w = seg_widths[i]; float diff = config->difficulty; switch (seg_types[i]) { case SSEG_ENTRY: gen_station_entry(map, cursor, w, diff); break; case SSEG_CORRIDOR: gen_station_corridor(map, cursor, w, diff); break; case SSEG_BULKHEAD: gen_station_bulkhead(map, cursor, w, diff); break; case SSEG_PLATFORMS: gen_station_platforms(map, cursor, w, diff); break; case SSEG_BAY: gen_station_bay(map, cursor, w, diff); break; case SSEG_VENT: gen_station_vent(map, cursor, w, diff); break; default: gen_station_corridor(map, cursor, w, diff); break; } cursor += w; } /* Right border wall */ fill_rect(map->collision_layer, map->width, smh, map->width - 2, 0, map->width - 1, smh - 1, TILE_SOLID_1); /* ── Phase 5: visual variety ── */ for (int y = 0; y < map->height; y++) { for (int x = 0; x < map->width; x++) { int idx = y * map->width + x; if (map->collision_layer[idx] == TILE_SOLID_1) { bool has_air_neighbor = false; if (y > 0 && map->collision_layer[(y-1)*map->width+x] == 0) has_air_neighbor = true; if (!has_air_neighbor) { map->collision_layer[idx] = random_solid(); } } } } /* ── Phase 6: background decoration ── */ gen_bg_decoration(map); /* ── Phase 7: metadata ── */ map->player_spawn = vec2(4.0f * TILE_SIZE, (STATION_FLOOR_ROW - 2) * TILE_SIZE); map->gravity = config->gravity > 0 ? config->gravity : 150.0f; map->bg_color = (SDL_Color){3, 3, 14, 255}; /* very dark blue-black */ map->has_bg_color = true; map->parallax_style = (int)PARALLAX_STYLE_DEEP_SPACE; /* Exit zone at far right */ if (map->exit_zone_count < MAX_EXIT_ZONES) { ExitZone *ez = &map->exit_zones[map->exit_zone_count++]; int exit_x = map->width - 5; int exit_y = STATION_FLOOR_ROW - 3; ez->x = (float)(exit_x * TILE_SIZE); ez->y = (float)(exit_y * TILE_SIZE); ez->w = 2.0f * TILE_SIZE; ez->h = 3.0f * TILE_SIZE; snprintf(ez->target, sizeof(ez->target), "generate:station"); /* Clear exit zone area */ for (int y = exit_y; y < exit_y + 3 && y < map->height; y++) { for (int x = exit_x; x < exit_x + 2 && x < map->width; x++) { map->collision_layer[y * map->width + x] = 0; } } } /* Music */ snprintf(map->music_path, sizeof(map->music_path), "assets/sounds/algardalgar.ogg"); printf("levelgen_station: generated %dx%d level (%d segments, seed=%u, gravity=%.0f)\n", map->width, map->height, num_segs, s_rng_state, map->gravity); printf(" segments:"); for (int i = 0; i < num_segs; i++) { printf(" %s", sseg_names[seg_types[i]]); } printf("\n"); return true; } /* ═══════════════════════════════════════════════════ * Dump to .lvl file format * ═══════════════════════════════════════════════════ */ bool levelgen_dump_lvl(const Tilemap *map, const char *path) { if (!map || !path) return false; FILE *f = fopen(path, "w"); if (!f) { fprintf(stderr, "levelgen: failed to open %s for writing\n", path); return false; } fprintf(f, "# Procedurally generated level\n"); fprintf(f, "# Seed state: %u\n\n", s_rng_state); fprintf(f, "TILESET assets/tiles/tileset.png\n"); fprintf(f, "SIZE %d %d\n", map->width, map->height); /* Player spawn in tile coords */ int spawn_tx = (int)(map->player_spawn.x / TILE_SIZE); int spawn_ty = (int)(map->player_spawn.y / TILE_SIZE); fprintf(f, "SPAWN %d %d\n", spawn_tx, spawn_ty); if (map->gravity > 0) { fprintf(f, "GRAVITY %.0f\n", map->gravity); } if (map->wind != 0.0f) { fprintf(f, "WIND %.0f\n", map->wind); } if (map->has_bg_color) { fprintf(f, "BG_COLOR %d %d %d\n", map->bg_color.r, map->bg_color.g, map->bg_color.b); } if (map->music_path[0]) { fprintf(f, "MUSIC %s\n", map->music_path); } if (map->player_unarmed) { fprintf(f, "PLAYER_UNARMED\n"); } fprintf(f, "\n"); /* Entity spawns */ for (int i = 0; i < map->entity_spawn_count; i++) { const EntitySpawn *es = &map->entity_spawns[i]; int tx = (int)(es->x / TILE_SIZE); int ty = (int)(es->y / TILE_SIZE); fprintf(f, "ENTITY %s %d %d\n", es->type_name, tx, ty); } fprintf(f, "\n"); /* Exit zones */ for (int i = 0; i < map->exit_zone_count; i++) { const ExitZone *ez = &map->exit_zones[i]; int tx = (int)(ez->x / TILE_SIZE); int ty = (int)(ez->y / TILE_SIZE); int tw = (int)(ez->w / TILE_SIZE); int th = (int)(ez->h / TILE_SIZE); if (ez->target[0]) { fprintf(f, "EXIT %d %d %d %d %s\n", tx, ty, tw, th, ez->target); } else { fprintf(f, "EXIT %d %d %d %d\n", tx, ty, tw, th); } } if (map->exit_zone_count > 0) fprintf(f, "\n"); /* Tile definitions */ for (int id = 1; id < map->tile_def_count; id++) { const TileDef *td = &map->tile_defs[id]; if (td->flags != 0 || td->tex_x != 0 || td->tex_y != 0) { fprintf(f, "TILEDEF %d %d %d %u\n", id, td->tex_x, td->tex_y, td->flags); } } fprintf(f, "\n"); /* Collision layer */ fprintf(f, "LAYER collision\n"); for (int y = 0; y < map->height; y++) { for (int x = 0; x < map->width; x++) { if (x > 0) fprintf(f, " "); fprintf(f, "%d", map->collision_layer[y * map->width + x]); } fprintf(f, "\n"); } /* Background layer */ bool has_bg = false; for (int i = 0; i < map->width * map->height; i++) { if (map->bg_layer[i] != 0) { has_bg = true; break; } } if (has_bg) { fprintf(f, "\nLAYER bg\n"); for (int y = 0; y < map->height; y++) { for (int x = 0; x < map->width; x++) { if (x > 0) fprintf(f, " "); fprintf(f, "%d", map->bg_layer[y * map->width + x]); } fprintf(f, "\n"); } } fclose(f); printf("levelgen: dumped level to %s\n", path); return true; }