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

@@ -26,6 +26,7 @@ typedef enum EntityType {
ENT_FORCE_FIELD,
ENT_POWERUP,
ENT_DRONE,
ENT_ASTEROID,
ENT_TYPE_COUNT
} EntityType;

View File

@@ -692,6 +692,198 @@ static void generate_deep_space_near(Parallax *p, SDL_Renderer *renderer) {
p->near_layer.owns_texture = true;
}
/* ── Themed: Moon surface ───────────────────────────── */
static void generate_moon_far(Parallax *p, SDL_Renderer *renderer) {
int w = SCREEN_WIDTH;
int h = SCREEN_HEIGHT;
SDL_Texture *tex = SDL_CreateTexture(renderer,
SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, w, h);
if (!tex) return;
SDL_SetRenderTarget(renderer, tex);
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
SDL_RenderClear(renderer);
unsigned int saved_seed = (unsigned int)rand();
srand(77);
/* Dim, cold stars — no atmosphere, but sparse */
for (int i = 0; i < 100; i++) {
int x = (int)(randf() * w);
int y = (int)(randf() * h * 0.55f);
uint8_t brightness = (uint8_t)(80 + (int)(randf() * 120));
/* Cool-white tint for airless sky */
uint8_t r = clamp_u8(brightness - 5);
uint8_t g = clamp_u8(brightness);
uint8_t b = clamp_u8(brightness + 10);
uint8_t a = (uint8_t)(120 + (int)(randf() * 100));
SDL_SetRenderDrawColor(renderer, r, g, b, a);
SDL_Rect dot = {x, y, 1, 1};
SDL_RenderFillRect(renderer, &dot);
}
/* A few bright feature stars */
for (int i = 0; i < 12; i++) {
int x = (int)(randf() * w);
int y = (int)(randf() * h * 0.45f);
SDL_SetRenderDrawColor(renderer, 240, 240, 255, 220);
SDL_Rect dot = {x, y, 1, 1};
SDL_RenderFillRect(renderer, &dot);
/* Cross glow on some */
if (i < 4) {
SDL_SetRenderDrawColor(renderer, 200, 200, 230, 80);
SDL_Rect halo[] = {
{x - 1, y, 3, 1},
{x, y - 1, 1, 3},
};
SDL_RenderFillRect(renderer, &halo[0]);
SDL_RenderFillRect(renderer, &halo[1]);
}
}
/* Distant crater-pocked terrain silhouette */
int terrain_base = (int)(h * 0.65f);
for (int x = 0; x < w; x++) {
float t = (float)x / (float)w;
/* Gentle rolling hills with crater dips */
float h1 = sinf(t * 6.28f * 1.5f) * 12.0f;
float h2 = sinf(t * 6.28f * 3.7f + 0.8f) * 8.0f;
float h3 = sinf(t * 6.28f * 8.2f + 2.5f) * 4.0f;
/* Crater depressions — sharp V-shaped dips */
float crater = 0.0f;
float c_positions[] = {0.15f, 0.35f, 0.55f, 0.78f, 0.92f};
float c_widths[] = {0.06f, 0.08f, 0.05f, 0.10f, 0.04f};
float c_depths[] = {18.0f, 25.0f, 15.0f, 30.0f, 12.0f};
for (int c = 0; c < 5; c++) {
float dist = (t - c_positions[c]) / c_widths[c];
if (dist > -1.0f && dist < 1.0f) {
float d = 1.0f - dist * dist; /* parabolic bowl */
crater += c_depths[c] * d;
}
}
int peak = terrain_base - (int)(h1 + h2 + h3) + (int)(crater);
if (peak < terrain_base - 35) peak = terrain_base - 35;
if (peak > h - 5) peak = h - 5;
for (int y = peak; y < h; y++) {
int depth = y - peak;
/* Grey regolith tones — lighter at ridgeline, darker below */
uint8_t base = clamp_u8(30 - depth / 4);
uint8_t r = clamp_u8(base + 5);
uint8_t g = clamp_u8(base + 3);
uint8_t b = clamp_u8(base);
uint8_t a = (uint8_t)(depth < 3 ? 100 : 160);
SDL_SetRenderDrawColor(renderer, r, g, b, a);
SDL_Rect px = {x, y, 1, 1};
SDL_RenderFillRect(renderer, &px);
}
}
srand(saved_seed);
SDL_SetRenderTarget(renderer, NULL);
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
p->far_layer.texture = tex;
p->far_layer.tex_w = w;
p->far_layer.tex_h = h;
p->far_layer.scroll_x = 0.03f;
p->far_layer.scroll_y = 0.03f;
p->far_layer.active = true;
p->far_layer.owns_texture = true;
}
static void generate_moon_near(Parallax *p, SDL_Renderer *renderer) {
int w = SCREEN_WIDTH;
int h = SCREEN_HEIGHT;
SDL_Texture *tex = SDL_CreateTexture(renderer,
SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, w, h);
if (!tex) return;
SDL_SetRenderTarget(renderer, tex);
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
SDL_RenderClear(renderer);
unsigned int saved_seed = (unsigned int)rand();
srand(166);
/* Closer crater-rim terrain — taller, more detail */
int terrain_base = (int)(h * 0.72f);
for (int x = 0; x < w; x++) {
float t = (float)x / (float)w;
float h1 = sinf(t * 6.28f * 2.3f + 1.0f) * 18.0f;
float h2 = sinf(t * 6.28f * 5.1f + 3.5f) * 10.0f;
float h3 = sinf(t * 6.28f * 13.0f + 0.7f) * 3.0f;
/* Near craters — fewer but bolder */
float crater = 0.0f;
float c_positions[] = {0.25f, 0.60f, 0.85f};
float c_widths[] = {0.10f, 0.12f, 0.07f};
float c_depths[] = {22.0f, 35.0f, 18.0f};
for (int c = 0; c < 3; c++) {
float dist = (t - c_positions[c]) / c_widths[c];
if (dist > -1.0f && dist < 1.0f) {
float d = 1.0f - dist * dist;
crater += c_depths[c] * d;
}
}
int peak = terrain_base - (int)(h1 + h2 + h3) + (int)(crater);
if (peak < terrain_base - 45) peak = terrain_base - 45;
if (peak > h - 5) peak = h - 5;
for (int y = peak; y < h; y++) {
int depth = y - peak;
/* Slightly brighter grey than far layer */
uint8_t base = clamp_u8(40 - depth / 3);
uint8_t r = clamp_u8(base + 8);
uint8_t g = clamp_u8(base + 5);
uint8_t b = clamp_u8(base + 2);
uint8_t a = (uint8_t)(depth < 2 ? 80 : 140);
SDL_SetRenderDrawColor(renderer, r, g, b, a);
SDL_Rect px = {x, y, 1, 1};
SDL_RenderFillRect(renderer, &px);
}
/* Rim highlight — bright edge at the very top of terrain */
if (peak < h - 5) {
uint8_t rim_a = (uint8_t)(40 + (int)(randf() * 30));
SDL_SetRenderDrawColor(renderer, 80, 75, 65, rim_a);
SDL_Rect px = {x, peak, 1, 1};
SDL_RenderFillRect(renderer, &px);
}
}
/* Scattered regolith dust particles */
for (int i = 0; i < 30; i++) {
int x = (int)(randf() * w);
int y = (int)(h * 0.5f + randf() * h * 0.4f);
uint8_t grey = (uint8_t)(30 + (int)(randf() * 30));
SDL_SetRenderDrawColor(renderer, grey + 5, grey + 3, grey,
(uint8_t)(20 + (int)(randf() * 25)));
SDL_Rect dot = {x, y, 1, 1};
SDL_RenderFillRect(renderer, &dot);
}
srand(saved_seed);
SDL_SetRenderTarget(renderer, NULL);
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
p->near_layer.texture = tex;
p->near_layer.tex_w = w;
p->near_layer.tex_h = h;
p->near_layer.scroll_x = 0.10f;
p->near_layer.scroll_y = 0.06f;
p->near_layer.active = true;
p->near_layer.owns_texture = true;
}
/* ── Themed parallax dispatcher ─────────────────────── */
void parallax_generate_themed(Parallax *p, SDL_Renderer *renderer, ParallaxStyle style) {
@@ -708,6 +900,10 @@ void parallax_generate_themed(Parallax *p, SDL_Renderer *renderer, ParallaxStyle
generate_deep_space_far(p, renderer);
generate_deep_space_near(p, renderer);
break;
case PARALLAX_STYLE_MOON:
generate_moon_far(p, renderer);
generate_moon_near(p, renderer);
break;
case PARALLAX_STYLE_DEFAULT:
default:
parallax_generate_stars(p, renderer);

View File

@@ -43,6 +43,7 @@ typedef enum ParallaxStyle {
PARALLAX_STYLE_ALIEN_SKY, /* alien planet surface: dusty, hazy */
PARALLAX_STYLE_INTERIOR, /* indoor base: panels, pipes, structural */
PARALLAX_STYLE_DEEP_SPACE, /* space station windows: vivid stars */
PARALLAX_STYLE_MOON, /* moon surface: craters, grey terrain */
} ParallaxStyle;
/* Generate both layers with a unified style */

View File

@@ -71,6 +71,34 @@ bool tilemap_load(Tilemap *map, const char *path, SDL_Renderer *renderer) {
sscanf(line + 14, "%255s", map->parallax_near_path);
} else if (strncmp(line, "MUSIC ", 6) == 0) {
sscanf(line + 6, "%255s", map->music_path);
} else if (strncmp(line, "PARALLAX_STYLE ", 15) == 0) {
int style = 0;
if (sscanf(line + 15, "%d", &style) == 1) {
map->parallax_style = style;
}
} else if (strncmp(line, "PLAYER_UNARMED", 14) == 0) {
map->player_unarmed = true;
} else if (strncmp(line, "EXIT ", 5) == 0) {
if (map->exit_zone_count < MAX_EXIT_ZONES) {
ExitZone *ez = &map->exit_zones[map->exit_zone_count];
float tx, ty, tw, th;
char target[ASSET_PATH_MAX] = {0};
/* EXIT <tile_x> <tile_y> <tile_w> <tile_h> [target_path] */
int n = sscanf(line + 5, "%f %f %f %f %255s",
&tx, &ty, &tw, &th, target);
if (n >= 4) {
ez->x = tx * TILE_SIZE;
ez->y = ty * TILE_SIZE;
ez->w = tw * TILE_SIZE;
ez->h = th * TILE_SIZE;
if (n == 5) {
snprintf(ez->target, sizeof(ez->target), "%s", target);
} else {
ez->target[0] = '\0'; /* no target = victory */
}
map->exit_zone_count++;
}
}
} else if (strncmp(line, "ENTITY ", 7) == 0) {
if (map->entity_spawn_count < MAX_ENTITY_SPAWNS) {
EntitySpawn *es = &map->entity_spawns[map->entity_spawn_count];

View File

@@ -30,6 +30,14 @@ typedef struct EntitySpawn {
float x, y; /* world position (pixels) */
} EntitySpawn;
/* Exit zone — triggers level transition on player overlap */
typedef struct ExitZone {
float x, y, w, h; /* world-space bounding box */
char target[ASSET_PATH_MAX]; /* path to next level, or
* "generate" for procgen,
* or empty for "you win" */
} ExitZone;
typedef struct Tilemap {
int width, height; /* map size in tiles */
uint16_t *bg_layer; /* background tile IDs */
@@ -47,8 +55,11 @@ typedef struct Tilemap {
char parallax_far_path[ASSET_PATH_MAX]; /* far bg image path */
char parallax_near_path[ASSET_PATH_MAX]; /* near bg image path */
int parallax_style; /* procedural bg style (0=default) */
bool player_unarmed; /* if true, player starts without gun */
EntitySpawn entity_spawns[MAX_ENTITY_SPAWNS];
int entity_spawn_count;
ExitZone exit_zones[MAX_EXIT_ZONES];
int exit_zone_count;
} Tilemap;
bool tilemap_load(Tilemap *map, const char *path, SDL_Renderer *renderer);