Add Mars Base generator, mars03 boss arena, and Mars campaign polish
Dedicated Mars Base generator with 6 segment types (entry, shaft, corridor, turret hall, hive, arena), depth-scaled difficulty, and exit to boss arena after 2 generated levels. Mars themes integrated into all 7 generic segment generators. Full level chain: moon03 → mars01 → mars02 → mars_base (×2) → mars03 → station. Also: jetpack fuel pickup preserves recharge progress, depth counters reset on game loop, NULL checks on entity spawns, charger charge timeout, bg decoration in Mars Base generator.
This commit is contained in:
10
DESIGN.md
10
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 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
45
TODO.md
45
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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
79
assets/levels/mars03.lvl
Normal file
79
assets/levels/mars03.lvl
Normal file
@@ -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
|
||||
BIN
assets/sounds/kaffe_og_kage.ogg
Normal file
BIN
assets/sounds/kaffe_og_kage.ogg
Normal file
Binary file not shown.
@@ -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
|
||||
* ═══════════════════════════════════════════════════ */
|
||||
|
||||
@@ -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. */
|
||||
|
||||
46
src/main.c
46
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);
|
||||
|
||||
Reference in New Issue
Block a user