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:
Thomas
2026-03-01 09:20:49 +00:00
parent ea6e16358f
commit fac7085056
30 changed files with 2139 additions and 83 deletions

View File

@@ -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];