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

@@ -538,6 +538,24 @@ static SegmentType pick_segment_type(LevelTheme theme, int index, int total) {
if (r < 0.90f) return SEG_FLAT;
return SEG_PIT;
case THEME_MARS_SURFACE:
/* Red dusty exterior: spacey, wide-open, few obstacles.
Charger enemies, occasional pits, wind gusts. */
if (r < 0.35f) return SEG_FLAT;
if (r < 0.55f) return SEG_PIT;
if (r < 0.75f) return SEG_ARENA;
if (r < 0.90f) return SEG_PLATFORMS;
return SEG_SHAFT;
case THEME_MARS_BASE:
/* Indoor Mars facility: very vertical, narrow corridors,
90-degree turns, heavy turret/spawner presence. */
if (r < 0.30f) return SEG_CORRIDOR;
if (r < 0.55f) return SEG_SHAFT;
if (r < 0.70f) return SEG_ARENA;
if (r < 0.85f) return SEG_PLATFORMS;
return SEG_FLAT;
default:
/* Only pick content types (exclude TRANSITION and CLIMB connectors) */
return (SegmentType)rng_range(0, SEG_SHAFT);
@@ -758,6 +776,8 @@ static float gravity_for_theme(LevelTheme theme) {
case THEME_PLANET_SURFACE: return 400.0f;
case THEME_PLANET_BASE: return 600.0f;
case THEME_SPACE_STATION: return 750.0f;
case THEME_MARS_SURFACE: return 370.0f; /* Mars: ~0.38g */
case THEME_MARS_BASE: return 700.0f; /* artificial gravity */
default: return 600.0f;
}
}
@@ -768,6 +788,8 @@ static SDL_Color bg_color_for_theme(LevelTheme theme) {
case THEME_PLANET_SURFACE: return (SDL_Color){12, 8, 20, 255};
case THEME_PLANET_BASE: return (SDL_Color){10, 14, 22, 255};
case THEME_SPACE_STATION: return (SDL_Color){5, 5, 18, 255};
case THEME_MARS_SURFACE: return (SDL_Color){30, 12, 8, 255};
case THEME_MARS_BASE: return (SDL_Color){18, 10, 8, 255};
default: return (SDL_Color){15, 15, 30, 255};
}
}
@@ -778,6 +800,8 @@ static ParallaxStyle parallax_style_for_theme(LevelTheme theme) {
case THEME_PLANET_SURFACE: return PARALLAX_STYLE_ALIEN_SKY;
case THEME_PLANET_BASE: return PARALLAX_STYLE_INTERIOR;
case THEME_SPACE_STATION: return PARALLAX_STYLE_DEEP_SPACE;
case THEME_MARS_SURFACE: return PARALLAX_STYLE_MARS;
case THEME_MARS_BASE: return PARALLAX_STYLE_INTERIOR;
default: return PARALLAX_STYLE_DEFAULT;
}
}
@@ -787,6 +811,8 @@ static const char *theme_label(LevelTheme t) {
case THEME_PLANET_SURFACE: return "surface";
case THEME_PLANET_BASE: return "base";
case THEME_SPACE_STATION: return "station";
case THEME_MARS_SURFACE: return "mars_surf";
case THEME_MARS_BASE: return "mars_base";
default: return "?";
}
}
@@ -891,14 +917,18 @@ bool levelgen_generate(Tilemap *map, const LevelGenConfig *config) {
* to give each theme its own vertical zone. Single-theme levels
* stay at the standard 23-tile height. */
bool has_surface = false, has_base = false, has_station = false;
bool has_mars_surf = false, has_mars_base = false;
for (int i = 0; i < num_segs; i++) {
if (seg_themes[i] == THEME_PLANET_SURFACE) has_surface = true;
if (seg_themes[i] == THEME_PLANET_BASE) has_base = true;
if (seg_themes[i] == THEME_SPACE_STATION) has_station = true;
if (seg_themes[i] == THEME_MARS_SURFACE) has_mars_surf = true;
if (seg_themes[i] == THEME_MARS_BASE) has_mars_base = true;
}
/* Go tall if we have theme variety that benefits from verticality */
uses_tall = (has_surface && (has_base || has_station)) ||
(has_mars_surf && has_mars_base) ||
(has_station && num_segs >= 5);
if (uses_tall) {
@@ -914,9 +944,11 @@ bool levelgen_generate(Tilemap *map, const LevelGenConfig *config) {
}
switch (seg_themes[i]) {
case THEME_PLANET_SURFACE:
case THEME_MARS_SURFACE:
seg_ground[i] = ZONE_LOW_GROUND;
break;
case THEME_PLANET_BASE:
case THEME_MARS_BASE:
seg_ground[i] = ZONE_HIGH_GROUND;
break;
case THEME_SPACE_STATION:
@@ -1092,6 +1124,13 @@ bool levelgen_generate(Tilemap *map, const LevelGenConfig *config) {
map->has_bg_color = true;
map->parallax_style = (int)parallax_style_for_theme(primary_theme);
/* Select tileset based on theme. Mars themes use a dedicated tileset;
* all others use the default. level_load_generated() loads the texture. */
if (primary_theme == THEME_MARS_SURFACE || primary_theme == THEME_MARS_BASE) {
snprintf(map->tileset_path, sizeof(map->tileset_path),
"%s", "assets/tiles/mars_tileset.png");
}
/* 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