Add in-game level editor with auto-discovered tile/entity palettes

Implements a full level editor that runs inside the game engine as an
alternative mode, accessible via --edit flag or E key during gameplay.
The editor auto-discovers available tiles from the tileset texture and
entities from a new central registry, so adding new game content
automatically appears in the editor without any editor-specific changes.

Editor features: tile painting (pencil/eraser/flood fill) across 3
layers, entity placement with drag-to-move, player spawn point tool,
camera pan/zoom, grid overlay, .lvl save/load, map resize, and test
play (P to play, ESC to return to editor).

Supporting changes:
- Entity registry centralizes spawn functions (replaces strcmp chain)
- Mouse input + raw keyboard access added to input system
- Camera zoom support for editor overview
- Zoom-aware rendering in tilemap, renderer, and sprite systems
- Powerup and drone sprites/animations wired up (were defined but unused)
- Bitmap font renderer for editor UI (4x6 pixel glyphs, no dependencies)
This commit is contained in:
Thomas
2026-02-28 20:24:43 +00:00
parent c66c12ae68
commit ea6e16358f
30 changed files with 4959 additions and 51 deletions

903
src/game/levelgen.c Normal file
View File

@@ -0,0 +1,903 @@
#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 */
#define FLOOR_ROWS 3 /* rows 20, 21, 22 */
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_TYPE_COUNT
} SegmentType;
/* ═══════════════════════════════════════════════════
* Tile placement helpers
* ═══════════════════════════════════════════════════ */
static void set_tile(uint16_t *layer, int map_w, int x, int y, uint16_t id) {
if (x >= 0 && x < map_w && y >= 0 && y < SEG_HEIGHT) {
layer[y * map_w + x] = id;
}
}
static void fill_rect(uint16_t *layer, int map_w,
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, x, y, id);
}
}
}
static void fill_ground(uint16_t *layer, int map_w, int x0, int x1) {
fill_rect(layer, map_w, x0, GROUND_ROW, x1, SEG_HEIGHT - 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, float difficulty, LevelTheme theme) {
(void)theme;
uint16_t *col = map->collision_layer;
int mw = map->width;
fill_ground(col, mw, x0, x0 + w - 1);
/* 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 = rng_range(13, 17);
int pw = rng_range(3, 5);
for (int j = 0; j < pw && px + j < x0 + w; j++) {
set_tile(col, mw, 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, float difficulty, LevelTheme theme) {
uint16_t *col = map->collision_layer;
int mw = map->width;
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, x0, x0 + pit_start - 1);
/* Ground after pit */
if (x0 + pit_end < x0 + w) {
fill_ground(col, mw, x0 + pit_end, x0 + w - 1);
}
/* 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) */
add_entity(map, "flame_vent", fx, GROUND_ROW);
fill_rect(col, mw, fx - 1, SEG_HEIGHT - 2, fx + 1, SEG_HEIGHT - 1, 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, sx, GROUND_ROW - 1, TILE_PLAT);
set_tile(col, mw, sx + 1, GROUND_ROW - 1, TILE_PLAT);
}
}
}
/* SEG_PLATFORMS: vertical platforming section */
static void gen_platforms(Tilemap *map, int x0, int w, float difficulty, LevelTheme theme) {
uint16_t *col = map->collision_layer;
int mw = map->width;
/* Ground at bottom */
fill_ground(col, mw, x0, x0 + w - 1);
/* 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;
for (int i = 0; i < num_plats; i++) {
int py = GROUND_ROW - 3 - i * 3;
if (py < 3) 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, 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 = rng_range(10, 15);
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) {
if (theme == THEME_PLANET_SURFACE) {
/* Surface: flyers are alien wildlife */
add_entity(map, "flyer", x0 + rng_range(2, w - 3), rng_range(5, 10));
} else if (theme == THEME_SPACE_STATION) {
/* Station: turret mounted high up or flyer drone */
if (rng_float() < 0.5f) {
add_entity(map, "turret", x0 + rng_range(2, w - 3), rng_range(4, 8));
} else {
add_entity(map, "flyer", x0 + rng_range(2, w - 3), rng_range(5, 10));
}
} else {
/* Base: mix */
add_entity(map, "flyer", x0 + rng_range(2, w - 3), rng_range(5, 10));
}
}
}
/* SEG_CORRIDOR: walled section with ceiling */
static void gen_corridor(Tilemap *map, int x0, int w, float difficulty, LevelTheme theme) {
uint16_t *col = map->collision_layer;
int mw = map->width;
int ceil_row = rng_range(12, 15);
/* Ground */
fill_ground(col, mw, x0, x0 + w - 1);
/* Ceiling */
fill_rect(col, mw, x0, ceil_row - 2, x0 + w - 1, ceil_row, TILE_SOLID_1);
/* Walls at edges (short, just to frame) */
fill_rect(col, mw, x0, ceil_row, x0, GROUND_ROW - 1, TILE_SOLID_1);
fill_rect(col, mw, 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, x0, GROUND_ROW - 1, TILE_EMPTY);
set_tile(col, mw, x0, GROUND_ROW - 2, TILE_EMPTY);
set_tile(col, mw, x0, GROUND_ROW - 3, TILE_EMPTY);
/* Opening in right wall */
set_tile(col, mw, x0 + w - 1, GROUND_ROW - 1, TILE_EMPTY);
set_tile(col, mw, x0 + w - 1, GROUND_ROW - 2, TILE_EMPTY);
set_tile(col, mw, 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);
}
}
/* SEG_ARENA: wide open area, multiple enemies */
static void gen_arena(Tilemap *map, int x0, int w, float difficulty, LevelTheme theme) {
uint16_t *col = map->collision_layer;
int mw = map->width;
fill_ground(col, mw, x0, x0 + w - 1);
/* Raised platforms on sides */
int plat_h = rng_range(16, 18);
fill_rect(col, mw, x0, plat_h, x0 + 2, GROUND_ROW - 1, TILE_SOLID_1);
fill_rect(col, mw, x0 + w - 3, plat_h, x0 + w - 1, GROUND_ROW - 1, TILE_SOLID_1);
/* Central platform */
int cp_y = rng_range(14, 16);
int cp_x = x0 + w / 2 - 2;
for (int j = 0; j < 4; j++) {
set_tile(col, mw, cp_x + j, cp_y, TILE_PLAT);
}
/* 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) {
/* Alien wildlife: more grunts, some flyers */
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(8, 14));
} else if (theme == THEME_SPACE_STATION) {
/* Station security: more flyers (drones), some grunts */
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(8, 14));
} else {
/* Base: balanced mix */
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(8, 14));
}
}
/* 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) {
/* Surface: flame vents on the arena floor */
add_entity(map, "flame_vent", x0 + w / 2, GROUND_ROW - 1);
} else {
/* Base/Station: turret on elevated ledge */
add_entity(map, "turret", tx, plat_h - 1);
}
}
}
/* SEG_SHAFT: vertical shaft with platforms to climb */
static void gen_shaft(Tilemap *map, int x0, int w, float difficulty, LevelTheme theme) {
uint16_t *col = map->collision_layer;
int mw = map->width;
/* Ground at bottom */
fill_ground(col, mw, x0, x0 + w - 1);
/* Walls on both sides forming a shaft */
int shaft_left = x0 + 1;
int shaft_right = x0 + w - 2;
fill_rect(col, mw, x0, 3, x0, GROUND_ROW - 1, TILE_SOLID_1);
fill_rect(col, mw, x0 + w - 1, 3, x0 + w - 1, GROUND_ROW - 1, TILE_SOLID_1);
/* Opening at top */
set_tile(col, mw, x0, 3, TILE_EMPTY);
set_tile(col, mw, x0 + w - 1, 3, TILE_EMPTY);
/* Openings at bottom to enter */
for (int r = GROUND_ROW - 3; r < GROUND_ROW; r++) {
set_tile(col, mw, x0, r, TILE_EMPTY);
set_tile(col, mw, 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 < 4) 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, 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) {
add_entity(map, "platform_v", x0 + w / 2, 10);
}
/* 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 {
/* Station: force field at bottom of shaft */
add_entity(map, "force_field", x0 + w / 2, GROUND_ROW - 2);
}
}
/* Aerial threat in shaft */
if (difficulty > 0.4f && rng_float() < difficulty) {
if (theme == THEME_SPACE_STATION && rng_float() < 0.4f) {
/* Station: turret on shaft wall */
int side = rng_range(0, 1);
int turret_x = side ? x0 + 1 : x0 + w - 2;
add_entity(map, "turret", turret_x, rng_range(8, 14));
} else {
add_entity(map, "flyer", x0 + w / 2, rng_range(6, 12));
}
}
}
/* ═══════════════════════════════════════════════════
* 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;
default:
return (SegmentType)rng_range(0, SEG_TYPE_COUNT - 1);
}
}
/* 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 */
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,
float difficulty, LevelTheme from, LevelTheme to) {
(void)difficulty;
uint16_t *col = map->collision_layer;
int mw = map->width;
/* ── Ground: solid floor across the whole transition ── */
fill_ground(col, mw, x0, x0 + w - 1);
/* ── 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)
*
* Rows 4-6: thick ceiling / airlock header
* Rows 7-8: door frame top (inner columns only)
* Rows 9-18: open passageway
* Row 19: floor level (ground starts at 20)
*/
int left = x0;
int right = x0 + w - 1;
/* Outer bulkhead walls — full height from top to ground */
fill_rect(col, mw, left, 0, left, GROUND_ROW - 1, TILE_SOLID_1);
fill_rect(col, mw, right, 0, right, GROUND_ROW - 1, TILE_SOLID_1);
/* Thick ceiling header (airlock hull) */
fill_rect(col, mw, left, 4, right, 7, TILE_SOLID_2);
/* Inner frame columns — pillars flanking the doorway */
fill_rect(col, mw, left + 1, 8, left + 1, GROUND_ROW - 1, TILE_SOLID_2);
fill_rect(col, mw, right - 1, 8, right - 1, GROUND_ROW - 1, TILE_SOLID_2);
/* Clear the inner chamber (passable area between pillars) */
for (int y = 8; y < GROUND_ROW; y++) {
for (int x = left + 2; x <= right - 2; x++) {
set_tile(col, mw, 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, left, y, TILE_EMPTY);
set_tile(col, mw, 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, x, GROUND_ROW - 1, TILE_PLAT);
}
/* ── Hazards inside the airlock ── */
/* 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, 14);
}
/* 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, 13);
}
}
/* ═══════════════════════════════════════════════════
* 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 config->themes[0];
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;
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};
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;
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";
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 < SEG_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 and widths ── */
/* We may insert transition segments between theme boundaries,
* so the final count can be larger than num_segs.
* Max: num_segs content + (num_segs-1) transitions = 2*num_segs-1 */
#define MAX_FINAL_SEGS 20
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: source theme */
int final_seg_count = 0;
int total_width = 0;
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; /* entering new theme */
seg_from[final_seg_count] = prev_t; /* leaving old theme */
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; /* not a transition */
total_width += seg_widths[final_seg_count];
final_seg_count++;
}
num_segs = final_seg_count;
/* 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: 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 */
/* Left border wall */
fill_rect(map->collision_layer, map->width, 0, 0, 1, SEG_HEIGHT - 1, TILE_SOLID_1);
for (int i = 0; i < num_segs; i++) {
int w = seg_widths[i];
LevelTheme theme = seg_themes[i];
switch (seg_types[i]) {
case SEG_FLAT: gen_flat(map, cursor, w, config->difficulty, theme); break;
case SEG_PIT: gen_pit(map, cursor, w, config->difficulty, theme); break;
case SEG_PLATFORMS: gen_platforms(map, cursor, w, config->difficulty, theme); break;
case SEG_CORRIDOR: gen_corridor(map, cursor, w, config->difficulty, theme); break;
case SEG_ARENA: gen_arena(map, cursor, w, config->difficulty, theme); break;
case SEG_SHAFT: gen_shaft(map, cursor, w, config->difficulty, theme); break;
case SEG_TRANSITION:
gen_transition(map, cursor, w, config->difficulty, seg_from[i], theme);
break;
default: gen_flat(map, cursor, w, config->difficulty, theme); break;
}
cursor += w;
}
/* Right border wall */
fill_rect(map->collision_layer, map->width,
map->width - 2, 0, map->width - 1, SEG_HEIGHT - 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 ── */
map->player_spawn = vec2(3.0f * TILE_SIZE, (GROUND_ROW - 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);
/* Music */
strncpy(map->music_path, "assets/sounds/algardalgar.ogg", ASSET_PATH_MAX - 1);
/* 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"
};
printf("levelgen: generated %dx%d level (%d segments, seed=%u)\n",
map->width, map->height, num_segs, s_rng_state);
printf(" segments:");
for (int i = 0; i < num_segs; i++) {
printf(" %s[%s]", seg_names[seg_types[i]], theme_label(seg_themes[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->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);
}
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");
/* 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;
}