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:
@@ -26,6 +26,7 @@ typedef enum EntityType {
|
||||
ENT_FORCE_FIELD,
|
||||
ENT_POWERUP,
|
||||
ENT_DRONE,
|
||||
ENT_ASTEROID,
|
||||
ENT_TYPE_COUNT
|
||||
} EntityType;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user