diff --git a/DESIGN.md b/DESIGN.md index 4882b1f..c621b19 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -78,10 +78,12 @@ Already implemented: `GRAVITY`, `WIND`, `BG_COLOR`, `MUSIC`, `PARALLAX_FAR`, `PA - **Flyer** — Purple bat-like. Bobs in air, chases player when close, shoots fireballs. 1 HP. - **Turret** — Stationary, rotates to aim at player, fires periodically +- **Charger** — Ground patrol, detects player in 200 px horizontal LOS, ALERT → CHARGE (150 px/s) → STUNNED on wall hit. 2 HP. +- **Spawner** — Stationary, spawns grunts every 4.5 s (max 3 alive). 3 HP, destructible. +- **Laser Turret** — State machine (IDLE → CHARGING → FIRING → COOLDOWN). Per-pixel beam raycast. Fixed variant aims left; tracking variant rotates toward player at 1.5 rad/s. + ### Planned -- **Charger** — Detects player at range, charges in a straight line at high speed - **Shielder** — Has a directional shield, must be hit from behind or above -- **Spawner** — Stationary, periodically spawns smaller enemies - **Boss** — Large, multi-phase encounters. One per world area. --- @@ -175,7 +177,7 @@ Current directives: `TILESET`, `SIZE`, `SPAWN`, `GRAVITY`, `WIND`, `BG_COLOR`, ` - [ ] Better tileset art (space-themed) - [ ] Player sprite polish (more animation frames) - [x] Death / respawn system -- [ ] Pause menu +- [x] Pause menu ### Low Priority (Future) - [ ] World map mode @@ -209,7 +211,7 @@ Current directives: `TILESET`, `SIZE`, `SPAWN`, `GRAVITY`, `WIND`, `BG_COLOR`, ` | Aim diag | UP+LEFT/RIGHT (+ shoot) | Implemented | | Dash | C | Implemented | | Look up | UP (stand still) | Implemented | -| Pause | Escape | Quits game | +| Pause | Escape | Implemented | --- diff --git a/TODO.md b/TODO.md index 3f6472a..dbdf2ab 100644 --- a/TODO.md +++ b/TODO.md @@ -94,14 +94,47 @@ Implemented in `enemy.h`/`enemy.c`: color scheme. Both registered in entity registry with editor icons. ## ~~Mars campaign~~ ✓ -Implemented: two handcrafted levels plus procedural generator support. +Implemented: three handcrafted levels, a dedicated Mars Base generator, and +Mars themes in all generic segment generators. + +### Handcrafted levels - **mars01.lvl** (250×23, Mars Surface): low gravity (370), wind, wide-open red terrain, charger + grunt enemies, spacecraft intro. New `mars_tileset.png` and `PARALLAX_STYLE_MARS` (salmon sky, red mesas, dust). - **mars02.lvl** (40×46, Mars Base): normal gravity (700), tall vertical corridors with narrow passages, turrets, laser turrets (fixed + tracking), - spawners, chargers, grunts. Victory exit at bottom. -- Generator: `THEME_MARS_SURFACE` / `THEME_MARS_BASE` with per-theme gravity, - bg color, parallax style, tileset path, segment probabilities, and height - zone assignment. Mars themes added to procedural progression (6 options). -- Moon campaign now chains to Mars: moon03 → mars01 → mars02 → victory. + spawners, chargers, grunts. Exits to `generate:mars_base`. +- **mars03.lvl** (30×23, Boss Arena): heavy enemies (spawners, chargers, + laser turrets), exits to `generate:station`. + +### Mars Base dedicated generator +- `levelgen_generate_mars_base()` with 6 custom segment types: Entry (safe + start), Shaft (vertical with alternating platforms + laser turrets), + Corridor (narrow horizontal passage), Turret Hall (three-level laser + gauntlet), Hive (spawner room), Arena (tall multi-level combat). +- 46-tile tall levels, Mars tileset, interior parallax, 700 gravity. +- Depth scaling: 6→7→8 segments, difficulty 0.5→0.7→0.9. +- After 2 generated levels, exit points to mars03.lvl (boss arena). +- `generate:mars_base` exit target handling in main.c with `s_mars_depth`. + +### Mars themes in generic generators +- All 7 generic segment generators (`gen_flat`, `gen_pit`, `gen_platforms`, + `gen_corridor`, `gen_arena`, `gen_shaft`, `gen_transition`) place + Mars-specific enemies (chargers, laser turrets, spawners) when the active + theme is `THEME_MARS_SURFACE` or `THEME_MARS_BASE`. + +### Music +- `kaffe_og_kage.ogg` used for all Mars levels (handcrafted + both generators). + +### Level chain +- moon03 → mars01 → mars02 → generate:mars_base (×2) → mars03 → generate:station. + +### Bug fixes (this batch) +- Deterministic seed on generated level restart (`s_gen_seed` snapshot). +- NULL checks on calloc in `charger_spawn`, `spawner_spawn`, `laser_spawn_internal`. +- Charger charge timeout (2 s max) prevents infinite charge on flat terrain. +- Removed no-op `flags |= 0` in spawner_spawn. +- Jetpack fuel pickup preserves recharge progress instead of resetting timer. +- Makefile web-serve: removed `2>/dev/null` so real errors are visible. +- `s_mars_depth` and `s_station_depth` reset when game loops back to beginning. +- Added `gen_bg_decoration()` call to Mars Base generator. diff --git a/assets/levels/mars01.lvl b/assets/levels/mars01.lvl index fe266df..53e5567 100644 --- a/assets/levels/mars01.lvl +++ b/assets/levels/mars01.lvl @@ -11,7 +11,7 @@ GRAVITY 370 WIND 25 BG_COLOR 30 12 8 PARALLAX_STYLE 5 -MUSIC assets/sounds/algardalgar.ogg +MUSIC assets/sounds/kaffe_og_kage.ogg ENTITY spacecraft 1 14 diff --git a/assets/levels/mars02.lvl b/assets/levels/mars02.lvl index 6b3fbde..38e3b08 100644 --- a/assets/levels/mars02.lvl +++ b/assets/levels/mars02.lvl @@ -11,7 +11,7 @@ SPAWN 3 7 GRAVITY 700 BG_COLOR 20 10 6 PARALLAX_STYLE 5 -MUSIC assets/sounds/algardalgar.ogg +MUSIC assets/sounds/kaffe_og_kage.ogg ENTITY spacecraft 1 3 @@ -42,7 +42,7 @@ ENTITY powerup_hp 24 34 ENTITY powerup_gun 12 42 # Exit at bottom right — victory (end of Mars campaign) -EXIT 36 40 2 3 generate:station +EXIT 36 40 2 3 generate:mars_base # Tile definitions (Mars tileset) TILEDEF 1 0 0 1 diff --git a/assets/levels/mars03.lvl b/assets/levels/mars03.lvl new file mode 100644 index 0000000..e4aef84 --- /dev/null +++ b/assets/levels/mars03.lvl @@ -0,0 +1,79 @@ +# Mars Base - Core Chamber (Boss Arena) +# ====================================== +# Final Mars level: small arena room deep underground. +# Heavy enemy presence — spawners, laser turrets, chargers. +# Survive the onslaught to reach the exit. Gun pickup at start. +# Exits to procedural station levels. + +TILESET assets/tiles/mars_tileset.png +SIZE 30 23 +SPAWN 3 18 +GRAVITY 700 +BG_COLOR 15 8 5 +PARALLAX_STYLE 3 +MUSIC assets/sounds/kaffe_og_kage.ogg + +# Gun pickup right at spawn — the player needs it +ENTITY powerup_gun 5 18 + +# Spawners on upper ledges (both sides) +ENTITY spawner 5 6 +ENTITY spawner 24 6 + +# Laser turrets guarding the arena +ENTITY laser_turret 1 10 +ENTITY laser_turret_track 28 10 + +# Turrets on the ceiling +ENTITY turret 10 2 +ENTITY turret 19 2 + +# Chargers on the ground floor +ENTITY charger 10 18 +ENTITY charger 20 18 + +# Grunts +ENTITY grunt 14 18 +ENTITY grunt 16 18 + +# Health pickups on side ledges — reward for platforming up +ENTITY powerup_hp 5 10 +ENTITY powerup_hp 24 10 + +# Jetpack fuel on central platform +ENTITY powerup_fuel 14 12 + +EXIT 26 17 2 3 generate:station + +# Tile definitions (Mars tileset) +TILEDEF 1 0 0 1 +TILEDEF 2 1 0 1 +TILEDEF 3 2 0 1 +TILEDEF 4 0 1 2 + +# Collision layer (30 wide x 23 tall) +# Enclosed arena: walls on all sides, upper ledges, central platform +LAYER collision +1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 +1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 +1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 +1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 +1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 +1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 +1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 +1 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 1 +1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 +1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 +1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 +1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 +1 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 1 +1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 +1 0 0 0 0 0 0 0 0 4 4 4 0 0 0 0 0 0 4 4 4 0 0 0 0 0 0 0 0 1 +1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 +1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 +1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 +1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 +1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 +1 3 1 1 3 1 1 3 1 3 1 1 1 3 1 1 3 1 3 1 1 3 1 1 3 1 1 3 1 1 +1 1 3 3 1 3 3 1 3 1 3 3 3 1 3 3 1 3 1 3 3 1 3 3 1 3 3 1 3 3 +3 1 3 3 1 3 3 1 3 1 3 3 3 1 3 3 1 3 1 3 3 1 3 3 1 3 3 1 3 3 diff --git a/assets/sounds/kaffe_og_kage.ogg b/assets/sounds/kaffe_og_kage.ogg new file mode 100644 index 0000000..57fe7c8 Binary files /dev/null and b/assets/sounds/kaffe_og_kage.ogg differ diff --git a/src/game/levelgen.c b/src/game/levelgen.c index 06b398b..b7974eb 100644 --- a/src/game/levelgen.c +++ b/src/game/levelgen.c @@ -140,7 +140,6 @@ static void add_entity(Tilemap *map, const char *type, int tile_x, int tile_y) { /* SEG_FLAT: solid ground, optionally with platforms and enemies */ static void gen_flat(Tilemap *map, int x0, int w, int ground_row, float difficulty, LevelTheme theme) { - (void)theme; uint16_t *col = map->collision_layer; int mw = map->width; int mh = map->height; @@ -163,9 +162,18 @@ static void gen_flat(Tilemap *map, int x0, int w, int ground_row, } } - /* Ground enemies */ + /* Ground enemies — Mars themes get chargers */ if (rng_float() < 0.4f + difficulty * 0.4f) { - add_entity(map, "grunt", x0 + rng_range(2, w - 3), ground_row - 1); + if (theme == THEME_MARS_BASE || theme == THEME_MARS_SURFACE) { + add_entity(map, "charger", x0 + rng_range(2, w - 3), ground_row - 1); + } else { + add_entity(map, "grunt", x0 + rng_range(2, w - 3), ground_row - 1); + } + } + + /* Mars Base: laser turret on a platform */ + if (theme == THEME_MARS_BASE && difficulty > 0.3f && rng_float() < 0.5f) { + add_entity(map, "laser_turret", x0 + rng_range(2, w - 3), ground_row - 5); } } @@ -191,13 +199,20 @@ static void gen_pit(Tilemap *map, int x0, int w, int ground_row, /* Hazard at pit bottom — theme dependent */ if (difficulty > 0.2f && rng_float() < 0.5f) { int fx = x0 + pit_start + pit_width / 2; - if (theme == THEME_PLANET_SURFACE || theme == THEME_PLANET_BASE) { + if (theme == THEME_PLANET_SURFACE || theme == THEME_PLANET_BASE || + theme == THEME_MARS_SURFACE) { /* Flame vents: natural hazard (surface) or industrial (base). * Pedestal at the bottom of the ground layer, not map bottom. */ add_entity(map, "flame_vent", fx, ground_row); int ped_top = ground_row + FLOOR_ROWS - 2; int ped_bot = ground_row + FLOOR_ROWS - 1; fill_rect(col, mw, mh, fx - 1, ped_top, fx + 1, ped_bot, TILE_SOLID_1); + } else if (theme == THEME_MARS_BASE) { + /* Mars Base: force field + laser turret overlooking pit */ + add_entity(map, "force_field", fx, ground_row - 2); + if (difficulty > 0.4f) { + add_entity(map, "laser_turret", fx - 3, ground_row - 4); + } } else { /* Space station: force field across the pit */ add_entity(map, "force_field", fx, ground_row - 2); @@ -260,7 +275,7 @@ static void gen_platforms(Tilemap *map, int x0, int w, int ground_row, int fly_lo = ground_row - 15; int fly_hi = ground_row - 10; if (fly_lo < ceil_row) fly_lo = ceil_row; - if (theme == THEME_PLANET_SURFACE) { + if (theme == THEME_PLANET_SURFACE || theme == THEME_MARS_SURFACE) { add_entity(map, "flyer", x0 + rng_range(2, w - 3), rng_range(fly_lo, fly_hi)); } else if (theme == THEME_SPACE_STATION) { if (rng_float() < 0.5f) { @@ -268,6 +283,13 @@ static void gen_platforms(Tilemap *map, int x0, int w, int ground_row, } else { add_entity(map, "flyer", x0 + rng_range(2, w - 3), rng_range(fly_lo, fly_hi)); } + } else if (theme == THEME_MARS_BASE) { + /* Mars Base: turrets and laser turrets dominate */ + if (rng_float() < 0.6f) { + add_entity(map, "laser_turret", x0 + rng_range(2, w - 3), rng_range(fly_lo, fly_lo + 4)); + } else { + add_entity(map, "turret", x0 + rng_range(2, w - 3), rng_range(fly_lo, fly_lo + 4)); + } } else { add_entity(map, "flyer", x0 + rng_range(2, w - 3), rng_range(fly_lo, fly_hi)); } @@ -312,7 +334,18 @@ static void gen_corridor(Tilemap *map, int x0, int w, int ground_row, set_tile(col, mw, mh, x0 + w - 1, ground_row - 3, TILE_EMPTY); /* Theme-dependent corridor hazards */ - if (theme == THEME_PLANET_BASE || theme == THEME_SPACE_STATION) { + if (theme == THEME_MARS_BASE) { + /* Mars Base: laser turrets, turrets, and spawners */ + if (difficulty > 0.2f && rng_float() < 0.7f) { + add_entity(map, "laser_turret", x0 + w / 2, ceil_row + 1); + } + if (difficulty > 0.4f && rng_float() < 0.5f) { + add_entity(map, "turret", x0 + rng_range(2, w - 3), ceil_row + 1); + } + if (difficulty > 0.5f && rng_float() < 0.4f) { + add_entity(map, "spawner", x0 + w / 2, ceil_row + 2); + } + } else if (theme == THEME_PLANET_BASE || theme == THEME_SPACE_STATION) { /* Tech environments: turrets and force fields */ if (difficulty > 0.2f && rng_float() < 0.7f) { add_entity(map, "turret", x0 + w / 2, ceil_row + 1); @@ -321,7 +354,7 @@ static void gen_corridor(Tilemap *map, int x0, int w, int ground_row, add_entity(map, "force_field", x0 + w / 2, ground_row - 1); } } else { - /* Planet surface: flame vents leak through the floor */ + /* Planet/Mars surface: flame vents leak through the floor */ if (difficulty > 0.3f && rng_float() < 0.5f) { add_entity(map, "flame_vent", x0 + rng_range(2, w - 3), ground_row - 1); } @@ -331,9 +364,13 @@ static void gen_corridor(Tilemap *map, int x0, int w, int ground_row, } } - /* Grunt patrol inside (all themes) */ + /* Patrol inside — Mars themes prefer chargers */ if (rng_float() < 0.5f + difficulty * 0.3f) { - add_entity(map, "grunt", x0 + rng_range(2, w - 3), ground_row - 1); + if (theme == THEME_MARS_BASE || theme == THEME_MARS_SURFACE) { + add_entity(map, "charger", x0 + rng_range(2, w - 3), ground_row - 1); + } else { + add_entity(map, "grunt", x0 + rng_range(2, w - 3), ground_row - 1); + } } /* Health pickup near the exit — reward for surviving the corridor */ @@ -377,8 +414,19 @@ static void gen_arena(Tilemap *map, int x0, int w, int ground_row, int num_enemies = 1 + (int)(difficulty * 3); for (int i = 0; i < num_enemies; i++) { float r = rng_float(); - if (theme == THEME_PLANET_SURFACE) { - if (r < 0.65f) + if (theme == THEME_PLANET_SURFACE || theme == THEME_MARS_SURFACE) { + /* Surface: mostly ground enemies, some chargers on Mars */ + if (r < 0.50f) + add_entity(map, "grunt", x0 + rng_range(3, w - 4), ground_row - 1); + else if (r < 0.75f && theme == THEME_MARS_SURFACE) + add_entity(map, "charger", x0 + rng_range(3, w - 4), ground_row - 1); + else + add_entity(map, "flyer", x0 + rng_range(3, w - 4), rng_range(fly_lo, fly_hi)); + } else if (theme == THEME_MARS_BASE) { + /* Mars Base: chargers, grunts, and flyers */ + if (r < 0.35f) + add_entity(map, "charger", x0 + rng_range(3, w - 4), ground_row - 1); + else if (r < 0.60f) add_entity(map, "grunt", x0 + rng_range(3, w - 4), ground_row - 1); else add_entity(map, "flyer", x0 + rng_range(3, w - 4), rng_range(fly_lo, fly_hi)); @@ -399,8 +447,14 @@ static void gen_arena(Tilemap *map, int x0, int w, int ground_row, if (difficulty > 0.5f && rng_float() < 0.7f) { int side = rng_range(0, 1); int tx = side ? x0 + w - 2 : x0 + 1; - if (theme == THEME_PLANET_SURFACE) { + if (theme == THEME_PLANET_SURFACE || theme == THEME_MARS_SURFACE) { add_entity(map, "flame_vent", x0 + w / 2, ground_row - 1); + } else if (theme == THEME_MARS_BASE) { + /* Mars Base: laser turret + spawner in arenas */ + add_entity(map, "laser_turret_track", tx, plat_h - 1); + if (difficulty > 0.6f) { + add_entity(map, "spawner", x0 + w / 2, plat_h - 1); + } } else { add_entity(map, "turret", tx, plat_h - 1); } @@ -470,8 +524,17 @@ static void gen_shaft(Tilemap *map, int x0, int w, int ground_row, /* Bottom hazard — theme dependent */ if (difficulty > 0.3f && rng_float() < 0.5f) { - if (theme == THEME_PLANET_SURFACE || theme == THEME_PLANET_BASE) { + if (theme == THEME_PLANET_SURFACE || theme == THEME_PLANET_BASE || + theme == THEME_MARS_SURFACE) { add_entity(map, "flame_vent", x0 + w / 2, ground_row - 1); + } else if (theme == THEME_MARS_BASE) { + /* Mars Base: force field + optional laser turret */ + add_entity(map, "force_field", x0 + w / 2, ground_row - 2); + if (difficulty > 0.5f) { + int side = rng_range(0, 1); + add_entity(map, "laser_turret", side ? x0 + 1 : x0 + w - 2, + ground_row - 5); + } } else { add_entity(map, "force_field", x0 + w / 2, ground_row - 2); } @@ -480,7 +543,16 @@ static void gen_shaft(Tilemap *map, int x0, int w, int ground_row, /* Aerial threat in shaft */ if (difficulty > 0.4f && rng_float() < difficulty) { int mid_y = (ceil_row + ground_row) / 2; - if (theme == THEME_SPACE_STATION && rng_float() < 0.4f) { + if (theme == THEME_MARS_BASE) { + /* Mars Base: laser turrets on walls dominate shafts */ + int side = rng_range(0, 1); + int turret_x = side ? x0 + 1 : x0 + w - 2; + if (rng_float() < 0.6f) { + add_entity(map, "laser_turret_track", turret_x, rng_range(ceil_row + 2, mid_y)); + } else { + add_entity(map, "turret", turret_x, rng_range(ceil_row + 2, mid_y)); + } + } else if (theme == THEME_SPACE_STATION && rng_float() < 0.4f) { int side = rng_range(0, 1); int turret_x = side ? x0 + 1 : x0 + w - 2; add_entity(map, "turret", turret_x, rng_range(ceil_row + 2, mid_y)); @@ -659,6 +731,11 @@ static void gen_transition(Tilemap *map, int x0, int w, int ground_row, (from == THEME_SPACE_STATION && to == THEME_PLANET_BASE)) { add_entity(map, "force_field", x0 + w / 2, hazard_y - 1); } + + /* Mars transitions: force field entering the base from surface */ + if (to == THEME_MARS_BASE && from == THEME_MARS_SURFACE) { + add_entity(map, "force_field", x0 + w / 2, hazard_y); + } } /* ═══════════════════════════════════════════════════ @@ -1154,8 +1231,12 @@ bool levelgen_generate(Tilemap *map, const LevelGenConfig *config) { } } - /* Music */ - snprintf(map->music_path, sizeof(map->music_path), "assets/sounds/algardalgar.ogg"); + /* Music — Mars themes get their own track */ + if (primary_theme == THEME_MARS_SURFACE || primary_theme == THEME_MARS_BASE) { + snprintf(map->music_path, sizeof(map->music_path), "assets/sounds/kaffe_og_kage.ogg"); + } else { + snprintf(map->music_path, sizeof(map->music_path), "assets/sounds/algardalgar.ogg"); + } /* Tileset */ /* NOTE: tileset texture will be loaded by level_load_generated */ @@ -1649,6 +1730,632 @@ bool levelgen_generate_station(Tilemap *map, const LevelGenConfig *config) { return true; } +/* ═══════════════════════════════════════════════════ + * Mars Base Generator + * + * A dedicated generator for vertical, claustrophobic + * Mars underground base levels. Uses the tall 46-tile + * height to create multi-level structures connected by + * shafts. Heavy laser turret, spawner, and charger + * presence. The base digs underground because the + * surface is hazardous. + * + * Layout: narrow vertical shafts connect wider rooms. + * Ceiling and floor are solid rock. Playable area is + * roughly 38 tiles tall (rows 3-41) with internal + * floors dividing it into 3 levels. + * ═══════════════════════════════════════════════════ */ + +/* Mars Base layout constants */ +#define MB_HEIGHT 46 /* full level height (tall) */ +#define MB_CEIL_ROW 2 /* top rock ceiling bottom */ +#define MB_FLOOR_ROW 43 /* bottom rock floor top */ +#define MB_MID_UPPER 15 /* upper internal floor */ +#define MB_MID_LOWER 29 /* lower internal floor */ + +static void mb_fill_shell(uint16_t *col, int mw, int mh, int x0, int x1) { + /* Top rock: rows 0 through MB_CEIL_ROW */ + fill_rect(col, mw, mh, x0, 0, x1, MB_CEIL_ROW, TILE_SOLID_1); + /* Bottom rock: rows MB_FLOOR_ROW through bottom */ + fill_rect(col, mw, mh, x0, MB_FLOOR_ROW, x1, mh - 1, TILE_SOLID_1); +} + +/* Mars Base segment types */ +typedef enum MarsBaseSegType { + MBSEG_ENTRY, /* safe entry room at the top */ + MBSEG_SHAFT, /* vertical shaft spanning multiple levels */ + MBSEG_CORRIDOR, /* narrow horizontal corridor on one level */ + MBSEG_TURRET_HALL, /* long hall with laser turrets on walls */ + MBSEG_HIVE, /* spawner room with grunt waves */ + MBSEG_ARENA, /* tall multi-level combat arena */ + MBSEG_TYPE_COUNT +} MarsBaseSegType; + +/* ── Mars Base segment: entry room (top level) ── */ +static void gen_mb_entry(Tilemap *map, int x0, int w, float difficulty) { + uint16_t *col = map->collision_layer; + int mw = map->width; + int mh = map->height; + + mb_fill_shell(col, mw, mh, x0, x0 + w - 1); + + /* Internal floors at upper and lower mid-levels */ + fill_rect(col, mw, mh, x0, MB_MID_UPPER, x0 + w - 1, MB_MID_UPPER + 1, TILE_SOLID_1); + fill_rect(col, mw, mh, x0, MB_MID_LOWER, x0 + w - 1, MB_MID_LOWER + 1, TILE_SOLID_1); + + /* Open space on the top level only (rows CEIL+1 to MID_UPPER-1) */ + /* Side walls */ + fill_rect(col, mw, mh, x0, MB_CEIL_ROW + 1, x0, MB_MID_UPPER - 1, TILE_SOLID_1); + fill_rect(col, mw, mh, x0 + w - 1, MB_CEIL_ROW + 1, x0 + w - 1, MB_MID_UPPER - 1, TILE_SOLID_1); + + /* Opening on the right wall to exit to the next segment */ + for (int y = MB_MID_UPPER - 4; y < MB_MID_UPPER; y++) { + set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); + } + + /* Health pickup */ + add_entity(map, "powerup_hp", x0 + w / 2, MB_MID_UPPER - 1); + + /* Charger at higher difficulty */ + if (difficulty > 0.5f) { + add_entity(map, "charger", x0 + rng_range(3, w - 4), MB_MID_UPPER - 1); + } +} + +/* ── Mars Base segment: vertical shaft ── */ +static void gen_mb_shaft(Tilemap *map, int x0, int w, float difficulty) { + uint16_t *col = map->collision_layer; + int mw = map->width; + int mh = map->height; + + mb_fill_shell(col, mw, mh, x0, x0 + w - 1); + + /* Walls on both sides spanning the full playable height */ + fill_rect(col, mw, mh, x0, MB_CEIL_ROW + 1, x0, MB_FLOOR_ROW - 1, TILE_SOLID_1); + fill_rect(col, mw, mh, x0 + w - 1, MB_CEIL_ROW + 1, x0 + w - 1, MB_FLOOR_ROW - 1, TILE_SOLID_1); + + /* Openings at top and bottom of walls for connectivity */ + for (int y = MB_CEIL_ROW + 1; y < MB_CEIL_ROW + 5; y++) { + set_tile(col, mw, mh, x0, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); + } + for (int y = MB_FLOOR_ROW - 4; y < MB_FLOOR_ROW; y++) { + set_tile(col, mw, mh, x0, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); + } + + /* Alternating platforms climbing up the shaft */ + int shaft_depth = MB_FLOOR_ROW - MB_CEIL_ROW - 2; + int num_plats = shaft_depth / 4; + if (num_plats > 9) num_plats = 9; + int inner_w = w - 2; + + for (int i = 0; i < num_plats; i++) { + int py = MB_FLOOR_ROW - 3 - i * 4; + if (py < MB_CEIL_ROW + 2) break; + bool left_side = (i % 2 == 0); + int px = left_side ? x0 + 1 : x0 + w - 1 - (inner_w > 4 ? 3 : 2); + int pw = (inner_w > 4) ? 3 : 2; + for (int j = 0; j < pw; j++) { + set_tile(col, mw, mh, px + j, py, TILE_PLAT); + } + } + + /* Vertical moving platform */ + if (rng_float() < 0.5f) { + int mid_y = (MB_CEIL_ROW + MB_FLOOR_ROW) / 2; + add_entity(map, "platform_v", x0 + w / 2, mid_y); + } + + /* Laser turret on one wall — fires across the shaft */ + if (difficulty > 0.2f) { + int side = rng_range(0, 1); + int turret_x = side ? x0 + 1 : x0 + w - 2; + int turret_y = rng_range(MB_CEIL_ROW + 6, MB_MID_LOWER); + add_entity(map, "laser_turret", turret_x, turret_y); + } + + /* Second laser turret (tracking) at higher difficulty */ + if (difficulty > 0.6f && rng_float() < 0.6f) { + int side = rng_range(0, 1); + int turret_x = side ? x0 + 1 : x0 + w - 2; + int turret_y = rng_range(MB_MID_UPPER, MB_MID_LOWER - 4); + add_entity(map, "laser_turret_track", turret_x, turret_y); + } + + /* Flyer in the shaft */ + if (difficulty > 0.3f && rng_float() < difficulty) { + int mid_y = (MB_CEIL_ROW + MB_FLOOR_ROW) / 2; + add_entity(map, "flyer", x0 + w / 2, rng_range(mid_y - 6, mid_y + 6)); + } + + /* Fuel pickup near the top */ + if (rng_float() < 0.5f) { + add_entity(map, "powerup_fuel", x0 + w / 2, MB_CEIL_ROW + 4); + } +} + +/* ── Mars Base segment: narrow corridor (one level) ── */ +static void gen_mb_corridor(Tilemap *map, int x0, int w, float difficulty) { + uint16_t *col = map->collision_layer; + int mw = map->width; + int mh = map->height; + + mb_fill_shell(col, mw, mh, x0, x0 + w - 1); + + /* Pick which level this corridor is on (upper, mid, or lower) */ + int level = rng_range(0, 2); + int floor_row, ceil_row; + switch (level) { + case 0: /* upper */ + ceil_row = MB_CEIL_ROW; + floor_row = MB_MID_UPPER; + break; + case 1: /* middle */ + ceil_row = MB_MID_UPPER + 2; + floor_row = MB_MID_LOWER; + break; + default: /* lower */ + ceil_row = MB_MID_LOWER + 2; + floor_row = MB_FLOOR_ROW; + break; + } + + /* Floor and ceiling for this level */ + fill_rect(col, mw, mh, x0, ceil_row, x0 + w - 1, ceil_row + 1, TILE_SOLID_1); + fill_rect(col, mw, mh, x0, floor_row, x0 + w - 1, floor_row + 1, TILE_SOLID_1); + + /* Fill rest with rock (above and below the corridor) */ + if (ceil_row > MB_CEIL_ROW + 1) { + fill_rect(col, mw, mh, x0, MB_CEIL_ROW + 1, x0 + w - 1, ceil_row - 1, TILE_SOLID_1); + } + if (floor_row + 2 < MB_FLOOR_ROW) { + fill_rect(col, mw, mh, x0, floor_row + 2, x0 + w - 1, MB_FLOOR_ROW - 1, TILE_SOLID_1); + } + + /* Side walls with openings */ + fill_rect(col, mw, mh, x0, ceil_row + 2, x0, floor_row - 1, TILE_SOLID_1); + fill_rect(col, mw, mh, x0 + w - 1, ceil_row + 2, x0 + w - 1, floor_row - 1, TILE_SOLID_1); + /* Door openings */ + for (int y = floor_row - 4; y < floor_row; y++) { + set_tile(col, mw, mh, x0, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); + } + + /* Charger patrol */ + if (rng_float() < 0.5f + difficulty * 0.3f) { + add_entity(map, "charger", x0 + rng_range(2, w - 3), floor_row - 1); + } + + /* Grunt */ + if (rng_float() < 0.4f + difficulty * 0.3f) { + add_entity(map, "grunt", x0 + rng_range(2, w - 3), floor_row - 1); + } + + /* Turret on ceiling */ + if (difficulty > 0.3f && rng_float() < 0.6f) { + add_entity(map, "turret", x0 + w / 2, ceil_row + 2); + } + + /* Health pickup */ + if (rng_float() < 0.3f) { + add_entity(map, "powerup_hp", x0 + rng_range(2, w - 3), floor_row - 1); + } +} + +/* ── Mars Base segment: turret hall (laser gauntlet) ── */ +static void gen_mb_turret_hall(Tilemap *map, int x0, int w, float difficulty) { + uint16_t *col = map->collision_layer; + int mw = map->width; + int mh = map->height; + + mb_fill_shell(col, mw, mh, x0, x0 + w - 1); + + /* Walls spanning full height */ + fill_rect(col, mw, mh, x0, MB_CEIL_ROW + 1, x0, MB_FLOOR_ROW - 1, TILE_SOLID_1); + fill_rect(col, mw, mh, x0 + w - 1, MB_CEIL_ROW + 1, x0 + w - 1, MB_FLOOR_ROW - 1, TILE_SOLID_1); + + /* Internal floors dividing into 3 levels */ + fill_rect(col, mw, mh, x0 + 1, MB_MID_UPPER, x0 + w - 2, MB_MID_UPPER + 1, TILE_SOLID_1); + fill_rect(col, mw, mh, x0 + 1, MB_MID_LOWER, x0 + w - 2, MB_MID_LOWER + 1, TILE_SOLID_1); + + /* Openings between levels: holes in the floors to drop through */ + int hole1_x = x0 + rng_range(2, w / 2 - 1); + int hole2_x = x0 + rng_range(w / 2 + 1, w - 3); + for (int j = 0; j < 3 && hole1_x + j < x0 + w - 1; j++) { + set_tile(col, mw, mh, hole1_x + j, MB_MID_UPPER, TILE_EMPTY); + set_tile(col, mw, mh, hole1_x + j, MB_MID_UPPER + 1, TILE_EMPTY); + } + for (int j = 0; j < 3 && hole2_x + j < x0 + w - 1; j++) { + set_tile(col, mw, mh, hole2_x + j, MB_MID_LOWER, TILE_EMPTY); + set_tile(col, mw, mh, hole2_x + j, MB_MID_LOWER + 1, TILE_EMPTY); + } + + /* Door openings on sides at each level */ + for (int y = MB_MID_UPPER - 4; y < MB_MID_UPPER; y++) { + set_tile(col, mw, mh, x0, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); + } + for (int y = MB_MID_LOWER - 4; y < MB_MID_LOWER; y++) { + set_tile(col, mw, mh, x0, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); + } + for (int y = MB_FLOOR_ROW - 4; y < MB_FLOOR_ROW; y++) { + set_tile(col, mw, mh, x0, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); + } + + /* Laser turrets on walls at each level — the gauntlet */ + /* Upper level */ + add_entity(map, "laser_turret", x0 + 1, MB_CEIL_ROW + 5); + + /* Middle level */ + add_entity(map, "laser_turret", x0 + w - 2, MB_MID_UPPER + 5); + + /* Lower level — tracking at higher difficulty */ + if (difficulty > 0.4f) { + add_entity(map, "laser_turret_track", x0 + 1, MB_MID_LOWER + 5); + } else { + add_entity(map, "laser_turret", x0 + 1, MB_MID_LOWER + 5); + } + + /* Extra turret at high difficulty */ + if (difficulty > 0.7f) { + add_entity(map, "turret", x0 + w / 2, MB_CEIL_ROW + 2); + } + + /* One-way platforms for navigation between holes */ + int plat_y1 = MB_MID_UPPER - 4; + int plat_y2 = MB_MID_LOWER - 4; + for (int j = 0; j < 3; j++) { + set_tile(col, mw, mh, x0 + w / 2 - 1 + j, plat_y1, TILE_PLAT); + set_tile(col, mw, mh, x0 + w / 2 - 1 + j, plat_y2, TILE_PLAT); + } + + /* Fuel pickup as reward for surviving the gauntlet */ + if (rng_float() < 0.5f) { + add_entity(map, "powerup_fuel", x0 + w / 2, MB_FLOOR_ROW - 1); + } +} + +/* ── Mars Base segment: hive / spawner room ── */ +static void gen_mb_hive(Tilemap *map, int x0, int w, float difficulty) { + uint16_t *col = map->collision_layer; + int mw = map->width; + int mh = map->height; + + mb_fill_shell(col, mw, mh, x0, x0 + w - 1); + + /* Walls */ + fill_rect(col, mw, mh, x0, MB_CEIL_ROW + 1, x0, MB_FLOOR_ROW - 1, TILE_SOLID_1); + fill_rect(col, mw, mh, x0 + w - 1, MB_CEIL_ROW + 1, x0 + w - 1, MB_FLOOR_ROW - 1, TILE_SOLID_1); + + /* Internal floor at lower mid-level only — open upper area */ + fill_rect(col, mw, mh, x0 + 1, MB_MID_LOWER, x0 + w - 2, MB_MID_LOWER + 1, TILE_SOLID_1); + + /* Hole in lower floor for vertical connectivity */ + int hole_x = x0 + rng_range(2, w - 4); + for (int j = 0; j < 3 && hole_x + j < x0 + w - 1; j++) { + set_tile(col, mw, mh, hole_x + j, MB_MID_LOWER, TILE_EMPTY); + set_tile(col, mw, mh, hole_x + j, MB_MID_LOWER + 1, TILE_EMPTY); + } + + /* Door openings on sides */ + for (int y = MB_MID_LOWER - 4; y < MB_MID_LOWER; y++) { + set_tile(col, mw, mh, x0, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); + } + for (int y = MB_FLOOR_ROW - 4; y < MB_FLOOR_ROW; y++) { + set_tile(col, mw, mh, x0, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); + } + + /* Spawner in the upper area — the hive */ + add_entity(map, "spawner", x0 + w / 2, MB_CEIL_ROW + 4); + + /* Second spawner at high difficulty */ + if (difficulty > 0.6f) { + add_entity(map, "spawner", x0 + w / 2, MB_MID_LOWER + 5); + } + + /* Platforms for vertical navigation in the upper area */ + for (int i = 0; i < 3; i++) { + int py = MB_MID_LOWER - 3 - i * 5; + if (py < MB_CEIL_ROW + 3) break; + int px = (i % 2 == 0) ? x0 + 2 : x0 + w - 5; + for (int j = 0; j < 3; j++) { + set_tile(col, mw, mh, px + j, py, TILE_PLAT); + } + } + + /* Turret guarding the spawner */ + if (difficulty > 0.3f) { + add_entity(map, "turret", x0 + rng_range(2, w - 3), MB_CEIL_ROW + 2); + } + + /* Health and fuel pickups — earned by destroying the hive */ + add_entity(map, "powerup_hp", x0 + w / 2 - 2, MB_FLOOR_ROW - 1); + if (rng_float() < 0.4f) { + add_entity(map, "powerup_fuel", x0 + w / 2 + 2, MB_MID_LOWER - 1); + } +} + +/* ── Mars Base segment: tall multi-level arena ── */ +static void gen_mb_arena(Tilemap *map, int x0, int w, float difficulty) { + uint16_t *col = map->collision_layer; + int mw = map->width; + int mh = map->height; + + mb_fill_shell(col, mw, mh, x0, x0 + w - 1); + + /* Walls */ + fill_rect(col, mw, mh, x0, MB_CEIL_ROW + 1, x0, MB_FLOOR_ROW - 1, TILE_SOLID_1); + fill_rect(col, mw, mh, x0 + w - 1, MB_CEIL_ROW + 1, x0 + w - 1, MB_FLOOR_ROW - 1, TILE_SOLID_1); + + /* Side ledges at three heights — creates a vertical arena */ + int ledge_w = 3; + /* Upper ledges */ + fill_rect(col, mw, mh, x0 + 1, MB_CEIL_ROW + 8, x0 + ledge_w, MB_CEIL_ROW + 8, TILE_SOLID_1); + fill_rect(col, mw, mh, x0 + w - 1 - ledge_w, MB_CEIL_ROW + 8, x0 + w - 2, MB_CEIL_ROW + 8, TILE_SOLID_1); + /* Mid ledges */ + fill_rect(col, mw, mh, x0 + 1, MB_MID_UPPER + 6, x0 + ledge_w, MB_MID_UPPER + 6, TILE_SOLID_1); + fill_rect(col, mw, mh, x0 + w - 1 - ledge_w, MB_MID_UPPER + 6, x0 + w - 2, MB_MID_UPPER + 6, TILE_SOLID_1); + /* Lower ledges */ + fill_rect(col, mw, mh, x0 + 1, MB_MID_LOWER + 6, x0 + ledge_w, MB_MID_LOWER + 6, TILE_SOLID_1); + fill_rect(col, mw, mh, x0 + w - 1 - ledge_w, MB_MID_LOWER + 6, x0 + w - 2, MB_MID_LOWER + 6, TILE_SOLID_1); + + /* Central floating platforms at different heights */ + int cp_x = x0 + w / 2 - 2; + for (int j = 0; j < 4; j++) { + set_tile(col, mw, mh, cp_x + j, MB_CEIL_ROW + 12, TILE_PLAT); + set_tile(col, mw, mh, cp_x + j, MB_MID_LOWER - 2, TILE_PLAT); + } + + /* Ground floor for walking */ + fill_rect(col, mw, mh, x0 + 1, MB_FLOOR_ROW - 1, x0 + w - 2, MB_FLOOR_ROW - 1, TILE_SOLID_1); + + /* Door openings on sides */ + for (int y = MB_FLOOR_ROW - 5; y < MB_FLOOR_ROW - 1; y++) { + set_tile(col, mw, mh, x0, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); + } + for (int y = MB_CEIL_ROW + 1; y < MB_CEIL_ROW + 5; y++) { + set_tile(col, mw, mh, x0, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); + } + + /* Enemies — mixed chargers, grunts, flyers across levels */ + int num_enemies = 2 + (int)(difficulty * 3); + for (int i = 0; i < num_enemies; i++) { + float r = rng_float(); + if (r < 0.30f) { + add_entity(map, "charger", x0 + rng_range(3, w - 4), MB_FLOOR_ROW - 2); + } else if (r < 0.55f) { + add_entity(map, "grunt", x0 + rng_range(3, w - 4), MB_FLOOR_ROW - 2); + } else { + int fly_y = rng_range(MB_CEIL_ROW + 5, MB_FLOOR_ROW - 6); + add_entity(map, "flyer", x0 + rng_range(3, w - 4), fly_y); + } + } + + /* Turrets on ledges */ + if (difficulty > 0.3f) { + add_entity(map, "turret", x0 + 2, MB_CEIL_ROW + 7); + } + if (difficulty > 0.6f) { + add_entity(map, "turret", x0 + w - 3, MB_MID_UPPER + 5); + } + + /* Laser turret for extra threat */ + if (difficulty > 0.5f && rng_float() < 0.6f) { + add_entity(map, "laser_turret_track", x0 + 1, MB_MID_LOWER + 3); + } + + /* Powerup on central platform */ + if (rng_float() < 0.5f) { + if (difficulty > 0.5f && rng_float() < 0.3f) { + add_entity(map, "powerup_drone", cp_x + 2, MB_CEIL_ROW + 11); + } else { + add_entity(map, "powerup_hp", cp_x + 2, MB_MID_LOWER - 3); + } + } +} + +/* ── Mars Base segment selection ── */ + +static MarsBaseSegType pick_mb_segment(int index, int total) { + if (index == 0) return MBSEG_ENTRY; + if (index == total - 1 && rng_float() < 0.6f) return MBSEG_ARENA; + + float r = rng_float(); + /* Heavy shaft + turret hall bias for that vertical feel */ + if (r < 0.25f) return MBSEG_SHAFT; + if (r < 0.45f) return MBSEG_TURRET_HALL; + if (r < 0.60f) return MBSEG_CORRIDOR; + if (r < 0.75f) return MBSEG_HIVE; + if (r < 0.90f) return MBSEG_ARENA; + return MBSEG_CORRIDOR; +} + +static int mb_segment_width(MarsBaseSegType type) { + switch (type) { + case MBSEG_ENTRY: return rng_range(10, 14); + case MBSEG_SHAFT: return rng_range(8, 12); + case MBSEG_CORRIDOR: return rng_range(12, 18); + case MBSEG_TURRET_HALL:return rng_range(12, 16); + case MBSEG_HIVE: return rng_range(14, 18); + case MBSEG_ARENA: return rng_range(16, 22); + default: return 12; + } +} + +/* ── Config preset for Mars Base ── */ + +LevelGenConfig levelgen_mars_base_config(uint32_t seed, int depth) { + if (depth < 0) depth = 0; + + /* Segments grow with depth: 6 -> 7 -> 8 (capped) */ + int segments = 6 + depth; + if (segments > 8) segments = 8; + + /* Difficulty ramps: 0.5 -> 0.7 -> 0.9 (capped) */ + float diff = 0.5f + depth * 0.2f; + if (diff > 1.0f) diff = 1.0f; + + LevelGenConfig config = { + .seed = seed, + .num_segments = segments, + .difficulty = diff, + .gravity = 700.0f, /* artificial gravity in the base */ + .theme_count = 1, + }; + config.themes[0] = THEME_MARS_BASE; + return config; +} + +/* ── Generate a Mars Base level ── */ + +bool levelgen_generate_mars_base(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 > 10) num_segs = 10; + + /* ── Phase 1: decide segment types and widths ── */ + MarsBaseSegType seg_types[12]; + int seg_widths[12]; + int total_width = 0; + + for (int i = 0; i < num_segs && i < 12; i++) { + seg_types[i] = pick_mb_segment(i, num_segs); + seg_widths[i] = mb_segment_width(seg_types[i]); + total_width += seg_widths[i]; + } + + /* 2-tile buffer on each side */ + total_width += 4; + + /* ── Phase 2: allocate tilemap ── */ + memset(map, 0, sizeof(Tilemap)); + map->width = total_width; + map->height = MB_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_mars_base: 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, map->height, + 0, 0, 1, map->height - 1, TILE_SOLID_1); + + static const char *mb_seg_names[] = { + "entry", "shaft", "corr", "turret", "hive", "arena" + }; + + for (int i = 0; i < num_segs; i++) { + int w = seg_widths[i]; + float diff = config->difficulty; + + switch (seg_types[i]) { + case MBSEG_ENTRY: gen_mb_entry(map, cursor, w, diff); break; + case MBSEG_SHAFT: gen_mb_shaft(map, cursor, w, diff); break; + case MBSEG_CORRIDOR: gen_mb_corridor(map, cursor, w, diff); break; + case MBSEG_TURRET_HALL: gen_mb_turret_hall(map, cursor, w, diff); break; + case MBSEG_HIVE: gen_mb_hive(map, cursor, w, diff); break; + case MBSEG_ARENA: gen_mb_arena(map, cursor, w, diff); break; + default: gen_mb_corridor(map, cursor, w, diff); break; + } + + cursor += w; + } + + /* Right border wall */ + fill_rect(map->collision_layer, map->width, map->height, + map->width - 2, 0, map->width - 1, map->height - 1, TILE_SOLID_1); + + /* ── Phase 5: visual variety (random solid variants for interior tiles) ── */ + 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 5b: background decoration ── */ + gen_bg_decoration(map); + + /* ── Phase 6: metadata ── */ + map->player_spawn = vec2(4.0f * TILE_SIZE, + (MB_MID_UPPER - 2) * TILE_SIZE); + + map->gravity = config->gravity > 0 ? config->gravity : 700.0f; + map->bg_color = (SDL_Color){18, 10, 8, 255}; + map->has_bg_color = true; + map->parallax_style = (int)PARALLAX_STYLE_INTERIOR; + + /* Mars tileset */ + snprintf(map->tileset_path, sizeof(map->tileset_path), + "%s", "assets/tiles/mars_tileset.png"); + + /* Exit zone at the bottom-right of the level */ + 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 = MB_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:mars_base"); + + /* 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/kaffe_og_kage.ogg"); + + printf("levelgen_mars_base: generated %dx%d level (%d segments, seed=%u)\n", + map->width, map->height, num_segs, s_rng_state); + printf(" segments:"); + for (int i = 0; i < num_segs; i++) { + printf(" %s", mb_seg_names[seg_types[i]]); + } + printf("\n"); + + return true; +} + /* ═══════════════════════════════════════════════════ * Dump to .lvl file format * ═══════════════════════════════════════════════════ */ diff --git a/src/game/levelgen.h b/src/game/levelgen.h index bbe6da3..fb62f78 100644 --- a/src/game/levelgen.h +++ b/src/game/levelgen.h @@ -70,6 +70,17 @@ bool levelgen_generate(Tilemap *map, const LevelGenConfig *config); * Returns true on success. */ bool levelgen_generate_station(Tilemap *map, const LevelGenConfig *config); +/* Config preset for a Mars Base level: + * tall (46 tiles), vertical, claustrophobic. + * Heavy laser turret/spawner/charger presence. + * depth (0-based) escalates difficulty and length. */ +LevelGenConfig levelgen_mars_base_config(uint32_t seed, int depth); + +/* Generate a Mars Base level: tall multi-level underground + * facility with shafts, turret halls, spawner hives. + * Returns true on success. */ +bool levelgen_generate_mars_base(Tilemap *map, const LevelGenConfig *config); + /* Dump a generated (or any) Tilemap to a .lvl file. * Useful for inspecting/editing procedural output. * Returns true on success. */ diff --git a/src/main.c b/src/main.c index ab05fa8..0181eb6 100644 --- a/src/main.c +++ b/src/main.c @@ -41,6 +41,11 @@ static bool s_testing_from_editor = false; * Drives escalating difficulty and length. */ static int s_station_depth = 0; +/* Mars Base depth: increments each generated mars_base level. + * After 2 levels, transitions to the boss arena (mars03). */ +static int s_mars_depth = 0; +#define MARS_BASE_GEN_COUNT 2 + /* ── Pause menu state ── */ #define PAUSE_ITEM_COUNT 3 static int s_pause_selection = 0; /* 0=Resume, 1=Restart, 2=Quit */ @@ -185,6 +190,37 @@ static void load_station_level(void) { s_level_path[0] = '\0'; /* generated levels have no file path */ } +static void load_mars_base_level(void) { + LevelGenConfig config = levelgen_mars_base_config(s_gen_seed, s_mars_depth); + s_mars_depth++; + + printf("Generating Mars Base level (depth=%d, gravity=%.0f, segments=%d, difficulty=%.2f)\n", + s_mars_depth, config.gravity, config.num_segments, config.difficulty); + + Tilemap gen_map; + if (!levelgen_generate_mars_base(&gen_map, &config)) { + fprintf(stderr, "Failed to generate Mars Base level!\n"); + g_engine.running = false; + return; + } + + /* After MARS_BASE_GEN_COUNT generated levels, point exit to boss arena */ + if (s_mars_depth >= MARS_BASE_GEN_COUNT && gen_map.exit_zone_count > 0) { + ExitZone *ez = &gen_map.exit_zones[gen_map.exit_zone_count - 1]; + snprintf(ez->target, sizeof(ez->target), "assets/levels/mars03.lvl"); + } + + if (s_dump_lvl) { + levelgen_dump_lvl(&gen_map, "assets/levels/generated_mars_base.lvl"); + } + + if (!level_load_generated(&s_level, &gen_map)) { + fprintf(stderr, "Failed to load Mars Base level!\n"); + g_engine.running = false; + } + s_level_path[0] = '\0'; +} + /* ── Switch to editor mode ── */ static void enter_editor(void) { if (s_mode == MODE_PLAY) { @@ -367,8 +403,10 @@ static void game_update(float dt) { if (target[0] == '\0') { /* Empty target = victory / end of game */ printf("Level complete! (no next level)\n"); - /* Loop back to the beginning */ + /* Loop back to the beginning, reset progression state */ level_free(&s_level); + s_station_depth = 0; + s_mars_depth = 0; if (!load_level_file("assets/levels/moon01.lvl")) { g_engine.running = false; } @@ -384,6 +422,12 @@ static void game_update(float dt) { level_free(&s_level); s_gen_seed = (uint32_t)time(NULL); load_station_level(); + } else if (strcmp(target, "generate:mars_base") == 0) { + /* Procedurally generated Mars Base level */ + printf("Transitioning to Mars Base level\n"); + level_free(&s_level); + s_gen_seed = (uint32_t)time(NULL); + load_mars_base_level(); } else { /* Load a specific level file */ printf("Transitioning to: %s\n", target);