forked from tas/major_tom
Add moon surface intro level with asteroid hazards and unarmed mechanics
Introduce moon01.lvl as the starting level — a pure jump-and-run intro with no gun and no enemies, just platforming over gaps and dodging falling asteroids. The player picks up their gun upon transitioning to level01. New features: - Moon tileset and PARALLAX_STYLE_MOON with crater terrain backgrounds - Asteroid entity (ENT_ASTEROID): falls from sky, damages on contact, explodes on ground with particles, respawns after delay - PLAYER_UNARMED directive disables gun for the level - Pit rescue mechanic: falling costs 1 HP and auto-dashes upward - Gun powerup entity type for future armed-pickup levels - Segment-based procedural level generator with themed rooms - Extended editor with entity palette and improved tile cycling - Web shell improvements for Emscripten builds
This commit is contained in:
@@ -251,6 +251,14 @@ static void gen_platforms(Tilemap *map, int x0, int w, float difficulty, LevelTh
|
||||
add_entity(map, "flyer", x0 + rng_range(2, w - 3), rng_range(5, 10));
|
||||
}
|
||||
}
|
||||
|
||||
/* Jetpack refill on a high platform — reward for climbing */
|
||||
if (rng_float() < 0.3f) {
|
||||
int top_py = GROUND_ROW - 3 - (num_plats - 1) * 3;
|
||||
if (top_py >= 3) {
|
||||
add_entity(map, "powerup_jet", x0 + rng_range(2, w - 3), top_py - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* SEG_CORRIDOR: walled section with ceiling */
|
||||
@@ -304,6 +312,11 @@ static void gen_corridor(Tilemap *map, int x0, int w, float difficulty, LevelThe
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/* SEG_ARENA: wide open area, multiple enemies */
|
||||
@@ -362,6 +375,15 @@ static void gen_arena(Tilemap *map, int x0, int w, float difficulty, LevelTheme
|
||||
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 {
|
||||
add_entity(map, "powerup_hp", cp_x + 2, cp_y - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* SEG_SHAFT: vertical shaft with platforms to climb */
|
||||
@@ -428,6 +450,11 @@ static void gen_shaft(Tilemap *map, int x0, int w, float difficulty, LevelTheme
|
||||
add_entity(map, "flyer", x0 + w / 2, rng_range(6, 12));
|
||||
}
|
||||
}
|
||||
|
||||
/* Jetpack refill near the top — reward for climbing */
|
||||
if (rng_float() < 0.4f) {
|
||||
add_entity(map, "powerup_jet", x0 + w / 2, 6);
|
||||
}
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════
|
||||
@@ -791,8 +818,30 @@ bool levelgen_generate(Tilemap *map, const LevelGenConfig *config) {
|
||||
map->has_bg_color = true;
|
||||
map->parallax_style = (int)parallax_style_for_theme(primary_theme);
|
||||
|
||||
/* 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 exit_x = map->width - 5; /* a few tiles from the right wall */
|
||||
int exit_y = GROUND_ROW - 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 */
|
||||
strncpy(map->music_path, "assets/sounds/algardalgar.ogg", ASSET_PATH_MAX - 1);
|
||||
snprintf(map->music_path, sizeof(map->music_path), "assets/sounds/algardalgar.ogg");
|
||||
|
||||
/* Tileset */
|
||||
/* NOTE: tileset texture will be loaded by level_load_generated */
|
||||
@@ -812,6 +861,470 @@ bool levelgen_generate(Tilemap *map, const LevelGenConfig *config) {
|
||||
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 x0, int x1) {
|
||||
/* Ceiling: rows 0 through STATION_CEIL_ROW */
|
||||
fill_rect(col, mw, x0, 0, x1, STATION_CEIL_ROW, TILE_SOLID_1);
|
||||
/* Floor: rows STATION_FLOOR_ROW through bottom */
|
||||
fill_rect(col, mw, 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;
|
||||
|
||||
station_fill_envelope(col, mw, 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, 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;
|
||||
|
||||
station_fill_envelope(col, mw, x0, x0 + w - 1);
|
||||
|
||||
/* Central bulkhead wall with a doorway */
|
||||
int wall_x = x0 + w / 2;
|
||||
fill_rect(col, mw, 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, 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;
|
||||
|
||||
station_fill_envelope(col, mw, 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, 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, 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;
|
||||
|
||||
station_fill_envelope(col, mw, 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, x, STATION_CEIL_ROW, TILE_EMPTY);
|
||||
set_tile(col, mw, 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, cp_x + j, cp_y, TILE_PLAT);
|
||||
}
|
||||
|
||||
/* Ledges on the sides */
|
||||
fill_rect(col, mw, x0, STATION_FLOOR_ROW - 3, x0 + 1, STATION_FLOOR_ROW - 1, TILE_SOLID_1);
|
||||
fill_rect(col, mw, 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;
|
||||
|
||||
station_fill_envelope(col, mw, x0, x0 + w - 1);
|
||||
|
||||
/* Lower the ceiling even more for a tight crawlspace */
|
||||
int vent_ceil = STATION_CEIL_ROW + 3;
|
||||
fill_rect(col, mw, 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, 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, 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);
|
||||
}
|
||||
|
||||
/* Jetpack refill reward (useful in low gravity) */
|
||||
if (rng_float() < 0.35f) {
|
||||
add_entity(map, "powerup_jet", 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;
|
||||
|
||||
station_fill_envelope(col, mw, 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;
|
||||
|
||||
/* Left border wall */
|
||||
fill_rect(map->collision_layer, map->width, 0, 0, 1, SEG_HEIGHT - 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,
|
||||
map->width - 2, 0, map->width - 1, SEG_HEIGHT - 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
|
||||
* ═══════════════════════════════════════════════════ */
|
||||
@@ -849,6 +1362,10 @@ bool levelgen_dump_lvl(const Tilemap *map, const char *path) {
|
||||
fprintf(f, "MUSIC %s\n", map->music_path);
|
||||
}
|
||||
|
||||
if (map->player_unarmed) {
|
||||
fprintf(f, "PLAYER_UNARMED\n");
|
||||
}
|
||||
|
||||
fprintf(f, "\n");
|
||||
|
||||
/* Entity spawns */
|
||||
@@ -861,6 +1378,21 @@ bool levelgen_dump_lvl(const Tilemap *map, const char *path) {
|
||||
|
||||
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];
|
||||
|
||||
Reference in New Issue
Block a user