forked from tas/major_tom
Implement four feature phases: Phase 1 - Pause menu: extract bitmap font into shared engine/font module, add MODE_PAUSED with Resume/Restart/Quit overlay. Phase 2 - Laser turret hazard: ENT_LASER_TURRET with charge/fire/ cooldown state machine, per-pixel beam raycast, two variants (fixed and tracking). Registered in entity registry with editor icons. Phase 3 - Charger and Spawner enemies: charger ground patrol with detect/telegraph/charge/stun cycle (2s charge timeout), spawner that periodically creates grunts up to a global cap of 3. Phase 4 - Mars campaign: two handcrafted levels (mars01 surface, mars02 base), mars_tileset.png, PARALLAX_STYLE_MARS with salmon sky and red mesas, THEME_MARS_SURFACE/THEME_MARS_BASE for the procedural generator with per-theme gravity/tileset/parallax. Moon campaign now chains moon03 -> mars01 -> mars02 -> victory. Also fix review findings: deterministic seed on generated level restart, NULL checks on calloc in spawn functions, charge timeout to prevent infinite charge on flat terrain, and stop suppressing stderr in Makefile web-serve target so real errors are visible.
1764 lines
69 KiB
C
1764 lines
69 KiB
C
#include "game/levelgen.h"
|
|
#include "engine/parallax.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
/* ═══════════════════════════════════════════════════
|
|
* 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;
|
|
}
|