Add pause menu, laser turret, charger/spawner enemies, and Mars campaign

Implement four feature phases:

Phase 1 - Pause menu: extract bitmap font into shared engine/font
module, add MODE_PAUSED with Resume/Restart/Quit overlay.

Phase 2 - Laser turret hazard: ENT_LASER_TURRET with charge/fire/
cooldown state machine, per-pixel beam raycast, two variants (fixed
and tracking). Registered in entity registry with editor icons.

Phase 3 - Charger and Spawner enemies: charger ground patrol with
detect/telegraph/charge/stun cycle (2s charge timeout), spawner that
periodically creates grunts up to a global cap of 3.

Phase 4 - Mars campaign: two handcrafted levels (mars01 surface,
mars02 base), mars_tileset.png, PARALLAX_STYLE_MARS with salmon sky
and red mesas, THEME_MARS_SURFACE/THEME_MARS_BASE for the procedural
generator with per-theme gravity/tileset/parallax. Moon campaign now
chains moon03 -> mars01 -> mars02 -> victory.

Also fix review findings: deterministic seed on generated level
restart, NULL checks on calloc in spawn functions, charge timeout
to prevent infinite charge on flat terrain, and stop suppressing
stderr in Makefile web-serve target so real errors are visible.
This commit is contained in:
Thomas
2026-03-02 19:34:12 +00:00
parent e5e91247fe
commit d0853fb38d
22 changed files with 1519 additions and 147 deletions

View File

@@ -900,6 +900,156 @@ static void generate_moon_near(Parallax *p, SDL_Renderer *renderer) {
p->near_layer.owns_texture = true;
}
/* ── Mars: salmon sky, red mesas, dust ──────────────── */
static void generate_mars_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(91);
/* Salmon/butterscotch sky gradient (upper half) */
for (int y = 0; y < h / 2; y++) {
float t = (float)y / (float)(h / 2);
uint8_t r = clamp_u8((int)(60 + 40 * t));
uint8_t g = clamp_u8((int)(25 + 20 * t));
uint8_t b = clamp_u8((int)(15 + 10 * t));
SDL_SetRenderDrawColor(renderer, r, g, b, (uint8_t)(20 + (int)(30 * t)));
SDL_Rect row = {0, y, w, 1};
SDL_RenderFillRect(renderer, &row);
}
/* Dim stars visible through thin atmosphere */
for (int i = 0; i < 30; i++) {
int x = (int)(randf() * w);
int y = (int)(randf() * h * 0.35f);
uint8_t bright = (uint8_t)(50 + (int)(randf() * 60));
SDL_SetRenderDrawColor(renderer, bright, clamp_u8(bright - 15),
clamp_u8(bright - 30), (uint8_t)(60 + (int)(randf() * 40)));
SDL_Rect dot = {x, y, 1, 1};
SDL_RenderFillRect(renderer, &dot);
}
/* Distant mesa/mountain silhouette — flat-topped with vertical cliffs. */
int base_y = (int)(h * 0.65f);
for (int x = 0; x < w; x++) {
float t = (float)x / (float)w;
/* Mesa profile: flat plateaus interrupted by steep drops. */
float mesa = sinf(t * 6.28f * 1.5f + 0.8f) * 12.0f;
float ridge = sinf(t * 6.28f * 4.1f + 2.0f) * 6.0f;
float detail = sinf(t * 6.28f * 9.7f) * 3.0f;
/* Flatten tops: clamp positive values to create plateaus */
float profile = mesa + ridge + detail;
if (profile > 8.0f) profile = 8.0f + (profile - 8.0f) * 0.2f;
int peak = base_y - (int)profile;
if (peak > base_y + 5) peak = base_y + 5;
for (int y = peak; y < h; y++) {
int depth = y - peak;
/* Dark reddish-brown silhouette */
uint8_t r = clamp_u8(25 + depth / 4);
uint8_t g = clamp_u8(10 + depth / 8);
uint8_t b = clamp_u8(8 + depth / 10);
uint8_t a = (uint8_t)(depth < 2 ? 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_mars_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(203);
/* Dust haze bands in warm reds and oranges */
typedef struct { uint8_t r, g, b; } DustColor;
DustColor dust_palette[] = {
{100, 40, 20}, /* rust */
{ 80, 30, 15}, /* dark rust */
{ 90, 50, 25}, /* sandy rust */
{110, 45, 30}, /* bright rust */
{ 70, 35, 35}, /* muted red */
};
int dust_count = sizeof(dust_palette) / sizeof(dust_palette[0]);
for (int band = 0; band < 5; band++) {
float cy = randf() * h * 0.6f + h * 0.2f;
DustColor col = dust_palette[band % dust_count];
int blobs = 15 + (int)(randf() * 20);
for (int b = 0; b < blobs; b++) {
int bx = (int)(randf() * w);
int by = (int)(cy + (randf() - 0.5f) * 60.0f);
int bw = 20 + (int)(randf() * 40);
int bh = 4 + (int)(randf() * 8);
uint8_t br = clamp_u8(col.r + (int)(randf() * 20 - 10));
uint8_t bg = clamp_u8(col.g + (int)(randf() * 15 - 7));
uint8_t bb = clamp_u8(col.b + (int)(randf() * 15 - 7));
SDL_SetRenderDrawColor(renderer, br, bg, bb,
(uint8_t)(5 + (int)(randf() * 10)));
SDL_Rect rect = {bx - bw / 2, by - bh / 2, bw, bh};
SDL_RenderFillRect(renderer, &rect);
}
}
/* Scattered dust particles */
for (int i = 0; i < 50; i++) {
int x = (int)(randf() * w);
int y = (int)(randf() * h);
DustColor col = dust_palette[(int)(randf() * dust_count)];
SDL_SetRenderDrawColor(renderer, col.r, col.g, col.b,
(uint8_t)(20 + (int)(randf() * 30)));
SDL_Rect dot = {x, y, 1 + (int)(randf() * 2), 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) {
@@ -920,6 +1070,10 @@ void parallax_generate_themed(Parallax *p, SDL_Renderer *renderer, ParallaxStyle
generate_moon_far(p, renderer);
generate_moon_near(p, renderer);
break;
case PARALLAX_STYLE_MARS:
generate_mars_far(p, renderer);
generate_mars_near(p, renderer);
break;
case PARALLAX_STYLE_DEFAULT:
default:
parallax_generate_stars(p, renderer);