From ad2d68a8b4839f578bd05608d56e3440bb7a0faa Mon Sep 17 00:00:00 2001 From: Thomas Date: Sun, 1 Mar 2026 14:57:53 +0000 Subject: [PATCH] Add height zones, jetpack boost particles, and TigerStyle guidelines Level generator: add vertical variety with tall levels (46 tiles). Segment generators accept ground_row parameter, SEG_CLIMB connects height zones, transitions inherit predecessor ground row to prevent walkability gaps. Climb segments respect traversal direction. Jetpack boost: add blue flame particles during dash (burst + trail) and continuous idle glow from player back while boost timer is active. Camera: add 30px vertical look-ahead when player velocity exceeds 50 px/s. Fix flame vent pedestal in gen_pit to use ground-relative position instead of map bottom (broken in tall HIGH-zone levels). Add TigerStyle coding guidelines to AGENTS.md adapted for C11. Add tall_test.lvl (40x46) for height zone validation. --- AGENTS.md | 94 ++++++ TODO.md | 44 ++- assets/levels/tall_test.lvl | 87 +++++ src/engine/camera.c | 6 +- src/engine/particle.c | 131 ++++++++ src/engine/particle.h | 10 + src/game/levelgen.c | 610 ++++++++++++++++++++++++++---------- src/game/player.c | 23 ++ 8 files changed, 813 insertions(+), 192 deletions(-) create mode 100644 assets/levels/tall_test.lvl diff --git a/AGENTS.md b/AGENTS.md index eeefa1d..1a8ac78 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -129,6 +129,100 @@ Paths are forward-slash, relative to `src/` or `include/`: `"engine/core.h"`, `" - `snprintf` / `strncpy` with explicit size limits for strings - `ASSET_PATH_MAX` (256) for path buffers +## TigerStyle Guidelines + +Follow the principles from [TigerStyle](https://github.com/tigerbeetle/tigerbeetle/blob/main/docs/TIGER_STYLE.md), +adapted for this C11 codebase. The design goals are **safety, performance, and developer +experience**, in that order. + +### Safety + +- **Simple, explicit control flow.** No recursion. Minimal abstractions — only where they + genuinely model the domain. Every abstraction has a cost; prefer straightforward code. + +- **Put a limit on everything.** All loops and queues must have a fixed upper bound. Use + `MAX_ENTITIES`, `MAX_ENTITY_SPAWNS`, `MAX_EXIT_ZONES`, etc. Where a loop cannot terminate + (e.g. the game loop), document why. + +- **Assert preconditions, postconditions, and invariants.** Validate function arguments and + return values. A function must not operate blindly on unchecked data. In C, use `assert()` + or early-return checks with `fprintf(stderr, ...)` for runtime-recoverable cases. + Split compound conditions: prefer `assert(a); assert(b);` over `assert(a && b);`. + +- **Assert the positive and negative space.** Check what you expect AND what you do not expect. + Bugs live at the boundary between valid and invalid data. + +- **Static allocation after initialization.** Fixed-size arrays for collections (`MAX_ENTITIES` + pool, tile defs). Dynamic allocation (`calloc`) only during level loading for tile layers. + No allocation or reallocation during gameplay. + +- **Smallest possible scope for variables.** Declare variables close to where they are used. + Minimize the number of live variables at any point. + +- **Hard limit of ~70 lines per function.** When splitting, centralize control flow (switch/if) + in the parent function and push pure computation into helpers. Keep leaf functions pure. + "Push `if`s up and `for`s down." + +- **Zero compiler warnings.** All builds must pass `-Wall -Wextra` with zero warnings. + +- **Handle all errors.** Every `fopen`, `calloc`, `snprintf` return must be checked. Early + return on failure. + +### Performance + +- **Think about performance in the design phase.** The biggest wins (1000x) come from + structural decisions, not micro-optimization after the fact. + +- **Back-of-the-envelope sketches** for the four resources: network, disk, memory, CPU. + For a game: frame budget is ~16ms at 60 Hz. Know where time goes. + +- **Optimize for the slowest resource first** (disk > memory > CPU), weighted by frequency. + A cache miss that happens every frame matters more than a disk read that happens once at + level load. + +- **Batch and amortize.** Sprite batching via `renderer_submit()`. Particle pools. Tile + culling to the viewport. Don't iterate what you don't need to. + +- **Be explicit with the compiler.** Don't rely on the optimizer to fix structural problems. + Extract hot inner loops into standalone helpers with primitive arguments when it helps + clarity and performance. + +### Developer Experience + +- **Get the nouns and verbs right.** Names should capture what a thing is or does. Take time + to find the right name. Module-prefixed functions (`player_update`, `tilemap_load`) already + enforce this. + +- **Add units or qualifiers last, sorted by descending significance.** For example, + `latency_ms_max` not `max_latency_ms`. Related variables then align: `latency_ms_min`, + `latency_ms_avg`. + +- **Always say why.** Comments explain rationale, not just what the code does. If a design + decision isn't obvious, explain the tradeoff. Code alone is not documentation. + +- **Comments are sentences.** Capital letter, full stop, space after `/*`. End-of-line + comments can be phrases without punctuation. + +- **Order matters for readability.** Put important things near the top of a file. Public API + first, then helpers. In structs, fields before methods. + +- **Don't duplicate variables or alias them.** One source of truth per value. Reduces the + chance of state getting out of sync. + +- **Calculate or check variables close to where they are used.** Don't introduce variables + before they are needed. Minimize the gap between place-of-check and place-of-use. + +- **Descriptive commit messages.** Imperative mood. Inform the reader about the why, not + just the what. The commit message is the permanent record — PR descriptions are not + stored in git. + +### Zero Technical Debt + +Do it right the first time. Don't allow known issues to slip through — exponential-complexity +algorithms, unbounded loops, potential buffer overflows. What we ship is solid. We may lack +features, but what we have meets our design goals. This is the only way to make steady +incremental progress. + ## Architecture Patterns ### Entity System diff --git a/TODO.md b/TODO.md index 7342ed0..833f0bd 100644 --- a/TODO.md +++ b/TODO.md @@ -41,29 +41,23 @@ Implemented: 3 moon levels connected by spacecraft takeoff/landing sequences. intro, 5 asteroids, unarmed until gun powerup near exit. Exit to level01. - **level01.lvl** — Space station with spacecraft landing intro (arriving from moon). -## Level generator: height zones (verticality) -Add vertical variety to generated levels using a "height zones" approach: -- Double tilemap height from 23 to ~46 tiles (two screens tall) -- Define two elevation bands: HIGH (ground ~row 17) and LOW (ground ~row 40) -- Existing segment generators get a `y_offset` parameter so internal logic - barely changes — platforms, enemies, corridors all shift by offset -- New vertical connector segments (ramps/climbs) bridge between zones -- Theme decides zone usage: Surface stays LOW, Station uses both, Base uses HIGH -- Camera needs vertical look-ahead when player has vertical velocity +## ~~Level generator: height zones (verticality)~~ ✓ +Implemented: tall level support (46 tiles, two screens) with height zones. +- Camera vertical look-ahead (30px lead when player moves vertically > 50 px/s) +- All segment generators (`gen_flat`, `gen_pit`, `gen_platforms`, `gen_corridor`, + `gen_arena`, `gen_shaft`, `gen_transition`) accept `ground_row` parameter — + platforms, enemies, and hazards are placed relative to the zone's ground level +- `SEG_CLIMB` connector segment type: vertical shaft with alternating platforms, + wall openings, optional moving platform and enemies, bridges height zones +- `levelgen_generate()` assigns zones per theme: Surface → LOW (row 43), + Base → HIGH (row 17), Station → alternates. `SEG_CLIMB` auto-inserted at + zone boundaries. Single-theme levels stay at standard 23-tile height. +- Station generator unchanged (23 tiles, corridor envelope constrains) +- Tall test level: `assets/levels/tall_test.lvl` (40x46) -### Implementation steps -1. Add vertical camera look-ahead (small Y lead when player moves vertically) -2. Create a handcrafted tall test level (~40x46) to validate camera, physics, - rendering, and entities all work with vertical scrolling -3. Refactor `set_tile()` / `fill_ground()` to accept a height parameter instead - of hardcoded `SEG_HEIGHT` -4. Add `y_offset` parameter to all segment generators -5. Add `SEG_CLIMB` connector segment type (shaft/platforms between zones) -6. Update `levelgen_generate()` to assign zones per segment and insert climbs -7. The station generator stays at 23 tiles (corridor envelope already constrains) - -## Jetpack boost blue flame effects -- Add continuous blue flame particles trailing from the player during jetpack - boost (while boost is active, not just on burst) -- Add blue accents to the jetpack burst effect itself — mix blue flame particles - into the existing yellow/orange exhaust plume +## ~~Jetpack boost blue flame effects~~ ✓ +Implemented: `particle_emit_jetpack_boost_burst()` (electric blue core + +blue-white flare, 18 particles mixed into regular burst) and +`particle_emit_jetpack_boost_trail()` (blue sparks + pale blue wisps, +3 particles/frame). Both activate only when `jetpack_boost_timer > 0`. +Burst fires on dash start, trail emits each frame during dash. diff --git a/assets/levels/tall_test.lvl b/assets/levels/tall_test.lvl new file mode 100644 index 0000000..d8d1565 --- /dev/null +++ b/assets/levels/tall_test.lvl @@ -0,0 +1,87 @@ +# Tall Test Level - Height Zones Validation +# ========================================== +# 46 tiles tall (two screens). Tests vertical scrolling, +# camera look-ahead, and climbing between height zones. + +TILESET assets/tiles/tileset.png +SIZE 40 46 +SPAWN 4 15 +GRAVITY 600 +BG_COLOR 10 14 22 +MUSIC assets/sounds/algardalgar.ogg + +# Enemies - high zone +ENTITY grunt 10 16 +ENTITY flyer 8 8 + +# Enemies - low zone +ENTITY grunt 28 42 +ENTITY grunt 34 42 +ENTITY flyer 30 34 +ENTITY turret 25 35 + +# Enemies - shaft +ENTITY flyer 18 28 + +# Powerups +ENTITY powerup_hp 13 12 +ENTITY powerup_jet 18 20 +ENTITY powerup_hp 32 38 + +# Exit zone at far right bottom +EXIT 36 40 2 3 assets/levels/level01.lvl + +# Tile definitions +TILEDEF 1 0 0 1 +TILEDEF 2 1 0 1 +TILEDEF 3 2 0 1 +TILEDEF 4 0 1 2 + +# Collision layer (40 wide x 46 tall) +LAYER collision +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 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 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 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 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 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 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 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 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 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 0 0 0 0 0 0 0 0 0 0 +1 1 0 0 0 4 4 4 4 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 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 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 0 0 0 0 0 0 0 0 0 0 +1 1 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 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 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 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 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 0 0 0 0 0 1 1 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 0 0 0 0 0 1 1 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 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 0 0 0 1 1 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 0 0 0 1 1 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 0 0 0 1 1 4 4 4 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 0 0 0 1 1 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 0 0 0 1 1 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 0 0 0 1 1 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 0 0 0 1 1 0 0 4 4 4 1 1 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 1 1 0 0 0 0 0 1 1 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 1 1 0 0 0 0 0 1 1 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 1 1 0 0 0 0 0 1 1 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 1 1 4 4 4 0 0 1 1 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 1 1 0 0 0 0 0 1 1 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 1 1 0 0 0 0 0 1 1 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 1 1 0 0 0 0 0 1 1 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 1 1 0 0 4 4 4 1 1 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 1 1 0 0 0 0 0 1 1 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 1 1 0 0 0 0 0 1 1 0 0 4 4 4 4 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 1 1 0 0 0 0 0 1 1 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 1 1 4 4 4 0 0 1 1 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 1 1 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 4 4 4 4 0 0 0 1 1 +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 1 1 +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 1 1 +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 1 1 +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 +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 +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 diff --git a/src/engine/camera.c b/src/engine/camera.c index b2d3db8..a245034 100644 --- a/src/engine/camera.c +++ b/src/engine/camera.c @@ -9,7 +9,7 @@ void camera_init(Camera *c, float vp_w, float vp_h) { c->bounds_max = vec2(vp_w, vp_h); /* default: one screen */ c->smoothing = 5.0f; c->deadzone = vec2(30.0f, 20.0f); - c->look_ahead = vec2(40.0f, 0.0f); + c->look_ahead = vec2(40.0f, 30.0f); c->zoom = 1.0f; } @@ -28,6 +28,10 @@ void camera_follow(Camera *c, Vec2 target, Vec2 velocity, float dt) { if (velocity.x > 10.0f) desired.x += c->look_ahead.x; else if (velocity.x < -10.0f) desired.x -= c->look_ahead.x; + /* Vertical look-ahead: lead camera when falling or rising */ + if (velocity.y > 50.0f) desired.y += c->look_ahead.y; + else if (velocity.y < -50.0f) desired.y -= c->look_ahead.y; + /* Smooth follow using exponential decay */ float t = 1.0f - expf(-c->smoothing * dt); c->pos = vec2_lerp(c->pos, desired, t); diff --git a/src/engine/particle.c b/src/engine/particle.c index 063e2eb..b14c696 100644 --- a/src/engine/particle.c +++ b/src/engine/particle.c @@ -301,6 +301,137 @@ void particle_emit_jetpack_trail(Vec2 pos, Vec2 dash_dir) { particle_emit(&smoke); } +void particle_emit_jetpack_boost_burst(Vec2 pos, Vec2 dash_dir) { + /* Blue flame accents mixed into the regular jetpack burst */ + float exhaust_angle = atan2f(-dash_dir.y, -dash_dir.x); + + /* Hot blue core — bright electric blue */ + ParticleBurst core = { + .origin = pos, + .count = 10, + .speed_min = 90.0f, + .speed_max = 220.0f, + .life_min = 0.15f, + .life_max = 0.35f, + .size_min = 2.0f, + .size_max = 4.5f, + .spread = 0.4f, + .direction = exhaust_angle, + .drag = 2.5f, + .gravity_scale = 0.05f, + .color = {80, 160, 255, 255}, /* electric blue */ + .color_vary = true, + }; + particle_emit(&core); + + /* Outer blue-white flare */ + ParticleBurst flare = { + .origin = pos, + .count = 8, + .speed_min = 40.0f, + .speed_max = 130.0f, + .life_min = 0.2f, + .life_max = 0.4f, + .size_min = 1.5f, + .size_max = 3.0f, + .spread = 0.7f, + .direction = exhaust_angle, + .drag = 3.0f, + .gravity_scale = 0.1f, + .color = {140, 200, 255, 255}, /* light blue */ + .color_vary = true, + }; + particle_emit(&flare); +} + +void particle_emit_jetpack_boost_trail(Vec2 pos, Vec2 dash_dir) { + /* Continuous blue flame trail while dashing with boost active */ + float exhaust_angle = atan2f(-dash_dir.y, -dash_dir.x); + + /* Blue flame sparks */ + ParticleBurst sparks = { + .origin = pos, + .count = 2, + .speed_min = 50.0f, + .speed_max = 140.0f, + .life_min = 0.1f, + .life_max = 0.25f, + .size_min = 1.5f, + .size_max = 3.0f, + .spread = 0.35f, + .direction = exhaust_angle, + .drag = 3.0f, + .gravity_scale = 0.05f, + .color = {60, 140, 255, 255}, /* bright blue */ + .color_vary = true, + }; + particle_emit(&sparks); + + /* Blue-white wisps */ + ParticleBurst wisps = { + .origin = pos, + .count = 1, + .speed_min = 20.0f, + .speed_max = 50.0f, + .life_min = 0.15f, + .life_max = 0.35f, + .size_min = 2.0f, + .size_max = 3.5f, + .spread = 0.5f, + .direction = exhaust_angle, + .drag = 3.5f, + .gravity_scale = -0.05f, + .color = {160, 210, 255, 200}, /* pale blue */ + .color_vary = true, + }; + particle_emit(&wisps); +} + +void particle_emit_jetpack_boost_idle(Vec2 pos, bool facing_left) { + /* Ambient blue flame simmering from the player's back while boost is + * active but the player isn't dashing. Exhaust drifts backward and + * slightly downward — a subtle idle glow effect. */ + float exhaust_angle = facing_left ? 0.0f : (float)M_PI; /* away from facing */ + + /* Small blue sparks drifting backward */ + ParticleBurst sparks = { + .origin = pos, + .count = 1, + .speed_min = 15.0f, + .speed_max = 45.0f, + .life_min = 0.12f, + .life_max = 0.3f, + .size_min = 1.0f, + .size_max = 2.5f, + .spread = 0.8f, + .direction = exhaust_angle, + .drag = 4.0f, + .gravity_scale = 0.15f, + .color = {50, 130, 255, 220}, /* medium blue */ + .color_vary = true, + }; + particle_emit(&sparks); + + /* Faint blue-white wisps floating up */ + ParticleBurst wisps = { + .origin = pos, + .count = 1, + .speed_min = 8.0f, + .speed_max = 25.0f, + .life_min = 0.15f, + .life_max = 0.35f, + .size_min = 1.5f, + .size_max = 2.5f, + .spread = 1.0f, + .direction = exhaust_angle - 0.3f, /* slightly upward */ + .drag = 4.5f, + .gravity_scale = -0.1f, + .color = {140, 200, 255, 160}, /* pale blue, translucent */ + .color_vary = true, + }; + particle_emit(&wisps); +} + void particle_emit_muzzle_flash(Vec2 pos, Vec2 shoot_dir) { float angle = atan2f(shoot_dir.y, shoot_dir.x); diff --git a/src/engine/particle.h b/src/engine/particle.h index b309c48..f7a7ba2 100644 --- a/src/engine/particle.h +++ b/src/engine/particle.h @@ -73,6 +73,16 @@ void particle_emit_jetpack_burst(Vec2 pos, Vec2 dash_dir); /* Jetpack exhaust trail (call each frame while dashing) */ void particle_emit_jetpack_trail(Vec2 pos, Vec2 dash_dir); +/* Blue flame burst (mixed into jetpack burst when boost is active) */ +void particle_emit_jetpack_boost_burst(Vec2 pos, Vec2 dash_dir); + +/* Continuous blue flame trail during jetpack boost (call each frame while dashing + boosted) */ +void particle_emit_jetpack_boost_trail(Vec2 pos, Vec2 dash_dir); + +/* Ambient blue glow from jetpack while boost is active but not dashing + * (call each frame; facing_left determines exhaust side) */ +void particle_emit_jetpack_boost_idle(Vec2 pos, bool facing_left); + /* Muzzle flash (short bright burst in shoot direction) */ void particle_emit_muzzle_flash(Vec2 pos, Vec2 shoot_dir); diff --git a/src/game/levelgen.c b/src/game/levelgen.c index 511c3ca..d9fc5d7 100644 --- a/src/game/levelgen.c +++ b/src/game/levelgen.c @@ -60,9 +60,14 @@ static float rng_float(void) { * ═══════════════════════════════════════════════════ */ #define SEG_HEIGHT 23 -#define GROUND_ROW 20 /* first ground row */ +#define GROUND_ROW 20 /* first ground row (single-screen levels) */ #define FLOOR_ROWS 3 /* rows 20, 21, 22 */ +/* ── Height zones for tall levels ─────────────────── */ +#define TALL_HEIGHT 46 /* two screens tall */ +#define ZONE_HIGH_GROUND 17 /* ground row for HIGH zone (upper area) */ +#define ZONE_LOW_GROUND 43 /* ground row for LOW zone (lower area) */ + typedef enum SegmentType { SEG_FLAT, /* flat ground, maybe some platforms above */ SEG_PIT, /* gap in the ground — must jump across */ @@ -71,6 +76,7 @@ typedef enum SegmentType { SEG_ARENA, /* open area, wider, good for combat */ SEG_SHAFT, /* vertical shaft with platforms inside */ SEG_TRANSITION, /* doorway/airlock between themes */ + SEG_CLIMB, /* vertical connector between height zones */ SEG_TYPE_COUNT } SegmentType; @@ -78,23 +84,25 @@ typedef enum SegmentType { * Tile placement helpers * ═══════════════════════════════════════════════════ */ -static void set_tile(uint16_t *layer, int map_w, int x, int y, uint16_t id) { - if (x >= 0 && x < map_w && y >= 0 && y < SEG_HEIGHT) { +static void set_tile(uint16_t *layer, int map_w, int map_h, + int x, int y, uint16_t id) { + if (x >= 0 && x < map_w && y >= 0 && y < map_h) { layer[y * map_w + x] = id; } } -static void fill_rect(uint16_t *layer, int map_w, +static void fill_rect(uint16_t *layer, int map_w, int map_h, int x0, int y0, int x1, int y1, uint16_t id) { for (int y = y0; y <= y1; y++) { for (int x = x0; x <= x1; x++) { - set_tile(layer, map_w, x, y, id); + set_tile(layer, map_w, map_h, x, y, id); } } } -static void fill_ground(uint16_t *layer, int map_w, int x0, int x1) { - fill_rect(layer, map_w, x0, GROUND_ROW, x1, SEG_HEIGHT - 1, TILE_SOLID_1); +static void fill_ground(uint16_t *layer, int map_w, int map_h, + int x0, int x1, int ground_row) { + fill_rect(layer, map_w, map_h, x0, ground_row, x1, map_h - 1, TILE_SOLID_1); } /* Add a random solid variant tile to break up visual monotony */ @@ -130,21 +138,23 @@ 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, float difficulty, LevelTheme theme) { +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; - fill_ground(col, mw, x0, x0 + w - 1); + fill_ground(col, mw, mh, x0, x0 + w - 1, ground_row); /* Random platforms above */ int num_plats = rng_range(0, 2); for (int i = 0; i < num_plats; i++) { int px = x0 + rng_range(1, w - 5); - int py = rng_range(13, 17); + int py = ground_row - rng_range(3, 7); int pw = rng_range(3, 5); for (int j = 0; j < pw && px + j < x0 + w; j++) { - set_tile(col, mw, px + j, py, TILE_PLAT); + set_tile(col, mw, mh, px + j, py, TILE_PLAT); } /* Maybe a grunt on the platform */ @@ -155,14 +165,16 @@ static void gen_flat(Tilemap *map, int x0, int w, float difficulty, LevelTheme t /* Ground enemies */ if (rng_float() < 0.4f + difficulty * 0.4f) { - add_entity(map, "grunt", x0 + rng_range(2, w - 3), GROUND_ROW - 1); + add_entity(map, "grunt", x0 + rng_range(2, w - 3), ground_row - 1); } } /* SEG_PIT: gap in the ground the player must jump */ -static void gen_pit(Tilemap *map, int x0, int w, float difficulty, LevelTheme theme) { +static void gen_pit(Tilemap *map, int x0, int w, int ground_row, + float difficulty, LevelTheme theme) { uint16_t *col = map->collision_layer; int mw = map->width; + int mh = map->height; int pit_start = rng_range(3, 5); int pit_width = rng_range(3, 5 + (int)(difficulty * 3)); @@ -170,22 +182,25 @@ static void gen_pit(Tilemap *map, int x0, int w, float difficulty, LevelTheme th int pit_end = pit_start + pit_width; /* Ground before pit */ - fill_ground(col, mw, x0, x0 + pit_start - 1); + fill_ground(col, mw, mh, x0, x0 + pit_start - 1, ground_row); /* Ground after pit */ if (x0 + pit_end < x0 + w) { - fill_ground(col, mw, x0 + pit_end, x0 + w - 1); + fill_ground(col, mw, mh, x0 + pit_end, x0 + w - 1, 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) { - /* Flame vents: natural hazard (surface) or industrial (base) */ - add_entity(map, "flame_vent", fx, GROUND_ROW); - fill_rect(col, mw, fx - 1, SEG_HEIGHT - 2, fx + 1, SEG_HEIGHT - 1, TILE_SOLID_1); + /* 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 { /* Space station: force field across the pit */ - add_entity(map, "force_field", fx, GROUND_ROW - 2); + add_entity(map, "force_field", fx, ground_row - 2); } } @@ -194,34 +209,40 @@ static void gen_pit(Tilemap *map, int x0, int w, float difficulty, LevelTheme th int sx = x0 + pit_start + pit_width / 2; if (theme == THEME_SPACE_STATION && rng_float() < 0.6f) { /* Moving platform instead of static stepping stones */ - add_entity(map, "platform", sx, GROUND_ROW - 2); + add_entity(map, "platform", sx, ground_row - 2); } else { - set_tile(col, mw, sx, GROUND_ROW - 1, TILE_PLAT); - set_tile(col, mw, sx + 1, GROUND_ROW - 1, TILE_PLAT); + set_tile(col, mw, mh, sx, ground_row - 1, TILE_PLAT); + set_tile(col, mw, mh, sx + 1, ground_row - 1, TILE_PLAT); } } } /* SEG_PLATFORMS: vertical platforming section */ -static void gen_platforms(Tilemap *map, int x0, int w, float difficulty, LevelTheme theme) { +static void gen_platforms(Tilemap *map, int x0, int w, int ground_row, + float difficulty, LevelTheme theme) { uint16_t *col = map->collision_layer; int mw = map->width; + int mh = map->height; /* Ground at bottom */ - fill_ground(col, mw, x0, x0 + w - 1); + fill_ground(col, mw, mh, x0, x0 + w - 1, ground_row); /* Stack of platforms ascending */ int num_plats = rng_range(3, 5); /* Space station: more platforms, low gravity makes climbing easier */ if (theme == THEME_SPACE_STATION) num_plats += 1; + /* Ceiling row: platforms should not go above this */ + int ceil_row = ground_row - 17; + if (ceil_row < 3) ceil_row = 3; + for (int i = 0; i < num_plats; i++) { - int py = GROUND_ROW - 3 - i * 3; - if (py < 3) break; + int py = ground_row - 3 - i * 3; + if (py < ceil_row) break; int px = x0 + rng_range(1, w - 4); int pw = rng_range(2, 4); for (int j = 0; j < pw && px + j < x0 + w; j++) { - set_tile(col, mw, px + j, py, TILE_PLAT); + set_tile(col, mw, mh, px + j, py, TILE_PLAT); } } @@ -229,64 +250,66 @@ static void gen_platforms(Tilemap *map, int x0, int w, float difficulty, LevelTh float plat_chance = (theme == THEME_SPACE_STATION) ? 0.8f : (theme == THEME_PLANET_SURFACE) ? 0.3f : 0.5f; if (rng_float() < plat_chance) { - int py = rng_range(10, 15); + int py = ground_row - rng_range(5, 10); bool vertical = (theme == THEME_SPACE_STATION && rng_float() < 0.5f); add_entity(map, vertical ? "platform_v" : "platform", x0 + w / 2, py); } /* Aerial threat */ if (difficulty > 0.3f && rng_float() < difficulty) { + 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) { - /* Surface: flyers are alien wildlife */ - add_entity(map, "flyer", x0 + rng_range(2, w - 3), rng_range(5, 10)); + add_entity(map, "flyer", x0 + rng_range(2, w - 3), rng_range(fly_lo, fly_hi)); } else if (theme == THEME_SPACE_STATION) { - /* Station: turret mounted high up or flyer drone */ if (rng_float() < 0.5f) { - add_entity(map, "turret", x0 + rng_range(2, w - 3), rng_range(4, 8)); + 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(5, 10)); + add_entity(map, "flyer", x0 + rng_range(2, w - 3), rng_range(fly_lo, fly_hi)); } } else { - /* Base: mix */ - add_entity(map, "flyer", x0 + rng_range(2, w - 3), rng_range(5, 10)); + add_entity(map, "flyer", x0 + rng_range(2, w - 3), rng_range(fly_lo, fly_hi)); } } /* Jetpack refill on a high platform — reward for climbing */ if (rng_float() < 0.3f) { - int top_py = GROUND_ROW - 3 - (num_plats - 1) * 3; - if (top_py >= 3) { + int top_py = ground_row - 3 - (num_plats - 1) * 3; + if (top_py >= ceil_row) { add_entity(map, "powerup_jet", x0 + rng_range(2, w - 3), top_py - 1); } } } /* SEG_CORRIDOR: walled section with ceiling */ -static void gen_corridor(Tilemap *map, int x0, int w, float difficulty, LevelTheme theme) { +static void gen_corridor(Tilemap *map, int x0, int w, int ground_row, + float difficulty, LevelTheme theme) { uint16_t *col = map->collision_layer; int mw = map->width; + int mh = map->height; - int ceil_row = rng_range(12, 15); + int ceil_row = ground_row - rng_range(5, 8); /* Ground */ - fill_ground(col, mw, x0, x0 + w - 1); + fill_ground(col, mw, mh, x0, x0 + w - 1, ground_row); /* Ceiling */ - fill_rect(col, mw, x0, ceil_row - 2, x0 + w - 1, ceil_row, TILE_SOLID_1); + fill_rect(col, mw, mh, x0, ceil_row - 2, x0 + w - 1, ceil_row, TILE_SOLID_1); /* Walls at edges (short, just to frame) */ - fill_rect(col, mw, x0, ceil_row, x0, GROUND_ROW - 1, TILE_SOLID_1); - fill_rect(col, mw, x0 + w - 1, ceil_row, x0 + w - 1, GROUND_ROW - 1, TILE_SOLID_1); + fill_rect(col, mw, mh, x0, ceil_row, x0, ground_row - 1, TILE_SOLID_1); + fill_rect(col, mw, mh, x0 + w - 1, ceil_row, x0 + w - 1, ground_row - 1, TILE_SOLID_1); /* Opening in left wall (1 tile above ground to enter) */ - set_tile(col, mw, x0, GROUND_ROW - 1, TILE_EMPTY); - set_tile(col, mw, x0, GROUND_ROW - 2, TILE_EMPTY); - set_tile(col, mw, x0, GROUND_ROW - 3, TILE_EMPTY); + set_tile(col, mw, mh, x0, ground_row - 1, TILE_EMPTY); + set_tile(col, mw, mh, x0, ground_row - 2, TILE_EMPTY); + set_tile(col, mw, mh, x0, ground_row - 3, TILE_EMPTY); /* Opening in right wall */ - set_tile(col, mw, x0 + w - 1, GROUND_ROW - 1, TILE_EMPTY); - set_tile(col, mw, x0 + w - 1, GROUND_ROW - 2, TILE_EMPTY); - set_tile(col, mw, x0 + w - 1, GROUND_ROW - 3, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 1, ground_row - 1, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 1, ground_row - 2, TILE_EMPTY); + 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) { @@ -295,12 +318,12 @@ static void gen_corridor(Tilemap *map, int x0, int w, float difficulty, LevelThe add_entity(map, "turret", x0 + w / 2, ceil_row + 1); } if (difficulty > 0.4f && rng_float() < 0.5f) { - add_entity(map, "force_field", x0 + w / 2, GROUND_ROW - 1); + add_entity(map, "force_field", x0 + w / 2, ground_row - 1); } } else { /* Planet 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); + add_entity(map, "flame_vent", x0 + rng_range(2, w - 3), ground_row - 1); } /* Rare turret even on surface (crashed tech, scavenged) */ if (difficulty > 0.5f && rng_float() < 0.3f) { @@ -310,56 +333,60 @@ static void gen_corridor(Tilemap *map, int x0, int w, float difficulty, LevelThe /* Grunt patrol inside (all themes) */ if (rng_float() < 0.5f + difficulty * 0.3f) { - add_entity(map, "grunt", x0 + rng_range(2, w - 3), GROUND_ROW - 1); + add_entity(map, "grunt", x0 + rng_range(2, w - 3), ground_row - 1); } /* Health pickup near the exit — reward for surviving the corridor */ if (difficulty > 0.3f && rng_float() < 0.35f) { - add_entity(map, "powerup_hp", x0 + w - 3, GROUND_ROW - 1); + add_entity(map, "powerup_hp", x0 + w - 3, ground_row - 1); } } /* SEG_ARENA: wide open area, multiple enemies */ -static void gen_arena(Tilemap *map, int x0, int w, float difficulty, LevelTheme theme) { +static void gen_arena(Tilemap *map, int x0, int w, int ground_row, + float difficulty, LevelTheme theme) { uint16_t *col = map->collision_layer; int mw = map->width; + int mh = map->height; - fill_ground(col, mw, x0, x0 + w - 1); + fill_ground(col, mw, mh, x0, x0 + w - 1, ground_row); /* Raised platforms on sides */ - int plat_h = rng_range(16, 18); - fill_rect(col, mw, x0, plat_h, x0 + 2, GROUND_ROW - 1, TILE_SOLID_1); - fill_rect(col, mw, x0 + w - 3, plat_h, x0 + w - 1, GROUND_ROW - 1, TILE_SOLID_1); + int plat_h = ground_row - rng_range(2, 4); + fill_rect(col, mw, mh, x0, plat_h, x0 + 2, ground_row - 1, TILE_SOLID_1); + fill_rect(col, mw, mh, x0 + w - 3, plat_h, x0 + w - 1, ground_row - 1, TILE_SOLID_1); /* Central platform */ - int cp_y = rng_range(14, 16); + int cp_y = ground_row - rng_range(4, 6); int cp_x = x0 + w / 2 - 2; for (int j = 0; j < 4; j++) { - set_tile(col, mw, cp_x + j, cp_y, TILE_PLAT); + set_tile(col, mw, mh, cp_x + j, cp_y, TILE_PLAT); } + /* Fly zone for aerial enemies */ + int fly_lo = ground_row - 12; + int fly_hi = ground_row - 6; + if (fly_lo < 3) fly_lo = 3; + /* Multiple enemies — composition depends on theme */ int num_enemies = 1 + (int)(difficulty * 3); for (int i = 0; i < num_enemies; i++) { float r = rng_float(); if (theme == THEME_PLANET_SURFACE) { - /* Alien wildlife: more grunts, some flyers */ if (r < 0.65f) - add_entity(map, "grunt", x0 + rng_range(3, w - 4), GROUND_ROW - 1); + 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(8, 14)); + add_entity(map, "flyer", x0 + rng_range(3, w - 4), rng_range(fly_lo, fly_hi)); } else if (theme == THEME_SPACE_STATION) { - /* Station security: more flyers (drones), some grunts */ if (r < 0.35f) - add_entity(map, "grunt", x0 + rng_range(3, w - 4), GROUND_ROW - 1); + 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(8, 14)); + add_entity(map, "flyer", x0 + rng_range(3, w - 4), rng_range(fly_lo, fly_hi)); } else { - /* Base: balanced mix */ if (r < 0.5f) - add_entity(map, "grunt", x0 + rng_range(3, w - 4), GROUND_ROW - 1); + 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(8, 14)); + add_entity(map, "flyer", x0 + rng_range(3, w - 4), rng_range(fly_lo, fly_hi)); } } @@ -368,10 +395,8 @@ static void gen_arena(Tilemap *map, int x0, int w, float difficulty, LevelTheme int side = rng_range(0, 1); int tx = side ? x0 + w - 2 : x0 + 1; if (theme == THEME_PLANET_SURFACE) { - /* Surface: flame vents on the arena floor */ - add_entity(map, "flame_vent", x0 + w / 2, GROUND_ROW - 1); + add_entity(map, "flame_vent", x0 + w / 2, ground_row - 1); } else { - /* Base/Station: turret on elevated ledge */ add_entity(map, "turret", tx, plat_h - 1); } } @@ -387,73 +412,79 @@ static void gen_arena(Tilemap *map, int x0, int w, float difficulty, LevelTheme } /* SEG_SHAFT: vertical shaft with platforms to climb */ -static void gen_shaft(Tilemap *map, int x0, int w, float difficulty, LevelTheme theme) { +static void gen_shaft(Tilemap *map, int x0, int w, int ground_row, + float difficulty, LevelTheme theme) { uint16_t *col = map->collision_layer; int mw = map->width; + int mh = map->height; /* Ground at bottom */ - fill_ground(col, mw, x0, x0 + w - 1); + fill_ground(col, mw, mh, x0, x0 + w - 1, ground_row); + + /* Ceiling row for the shaft */ + int ceil_row = ground_row - 17; + if (ceil_row < 3) ceil_row = 3; /* Walls on both sides forming a shaft */ int shaft_left = x0 + 1; int shaft_right = x0 + w - 2; - fill_rect(col, mw, x0, 3, x0, GROUND_ROW - 1, TILE_SOLID_1); - fill_rect(col, mw, x0 + w - 1, 3, x0 + w - 1, GROUND_ROW - 1, TILE_SOLID_1); + fill_rect(col, mw, mh, x0, ceil_row, x0, ground_row - 1, TILE_SOLID_1); + fill_rect(col, mw, mh, x0 + w - 1, ceil_row, x0 + w - 1, ground_row - 1, TILE_SOLID_1); /* Opening at top */ - set_tile(col, mw, x0, 3, TILE_EMPTY); - set_tile(col, mw, x0 + w - 1, 3, TILE_EMPTY); + set_tile(col, mw, mh, x0, ceil_row, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 1, ceil_row, TILE_EMPTY); /* Openings at bottom to enter */ - for (int r = GROUND_ROW - 3; r < GROUND_ROW; r++) { - set_tile(col, mw, x0, r, TILE_EMPTY); - set_tile(col, mw, x0 + w - 1, r, TILE_EMPTY); + for (int r = ground_row - 3; r < ground_row; r++) { + set_tile(col, mw, mh, x0, r, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 1, r, TILE_EMPTY); } /* Alternating platforms up the shaft */ int inner_w = shaft_right - shaft_left + 1; for (int i = 0; i < 5; i++) { - int py = GROUND_ROW - 3 - i * 3; - if (py < 4) break; + int py = ground_row - 3 - i * 3; + if (py < ceil_row + 1) break; bool left_side = (i % 2 == 0); int px = left_side ? shaft_left : shaft_right - 2; int pw = (inner_w > 4) ? 3 : 2; for (int j = 0; j < pw; j++) { - set_tile(col, mw, px + j, py, TILE_PLAT); + set_tile(col, mw, mh, px + j, py, TILE_PLAT); } } /* Vertical moving platform — very common in stations */ float vplat_chance = (theme == THEME_SPACE_STATION) ? 0.8f : 0.4f; if (rng_float() < vplat_chance) { - add_entity(map, "platform_v", x0 + w / 2, 10); + int mid_y = (ceil_row + ground_row) / 2; + add_entity(map, "platform_v", x0 + w / 2, mid_y); } /* Bottom hazard — theme dependent */ if (difficulty > 0.3f && rng_float() < 0.5f) { if (theme == THEME_PLANET_SURFACE || theme == THEME_PLANET_BASE) { - add_entity(map, "flame_vent", x0 + w / 2, GROUND_ROW - 1); + add_entity(map, "flame_vent", x0 + w / 2, ground_row - 1); } else { - /* Station: force field at bottom of shaft */ - add_entity(map, "force_field", x0 + w / 2, GROUND_ROW - 2); + add_entity(map, "force_field", x0 + w / 2, ground_row - 2); } } /* 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) { - /* Station: turret on shaft wall */ int side = rng_range(0, 1); int turret_x = side ? x0 + 1 : x0 + w - 2; - add_entity(map, "turret", turret_x, rng_range(8, 14)); + add_entity(map, "turret", turret_x, rng_range(ceil_row + 2, mid_y)); } else { - add_entity(map, "flyer", x0 + w / 2, rng_range(6, 12)); + add_entity(map, "flyer", x0 + w / 2, rng_range(ceil_row + 3, mid_y)); } } /* Jetpack refill near the top — reward for climbing */ if (rng_float() < 0.4f) { - add_entity(map, "powerup_jet", x0 + w / 2, 6); + add_entity(map, "powerup_jet", x0 + w / 2, ceil_row + 3); } } @@ -501,7 +532,8 @@ static SegmentType pick_segment_type(LevelTheme theme, int index, int total) { return SEG_PIT; default: - return (SegmentType)rng_range(0, SEG_TYPE_COUNT - 1); + /* Only pick content types (exclude TRANSITION and CLIMB connectors) */ + return (SegmentType)rng_range(0, SEG_SHAFT); } } @@ -515,6 +547,7 @@ static int segment_width(SegmentType type) { case SEG_ARENA: return rng_range(14, 20); case SEG_SHAFT: return rng_range(6, 10); case SEG_TRANSITION: return 6; /* fixed: doorway/airlock */ + case SEG_CLIMB: return 10; /* fixed: vertical connector */ default: return 10; } } @@ -526,14 +559,15 @@ static int segment_width(SegmentType type) { * Visually signals the environment change. * ═══════════════════════════════════════════════════ */ -static void gen_transition(Tilemap *map, int x0, int w, +static void gen_transition(Tilemap *map, int x0, int w, int ground_row, float difficulty, LevelTheme from, LevelTheme to) { (void)difficulty; uint16_t *col = map->collision_layer; int mw = map->width; + int mh = map->height; /* ── Ground: solid floor across the whole transition ── */ - fill_ground(col, mw, x0, x0 + w - 1); + fill_ground(col, mw, mh, x0, x0 + w - 1, ground_row); /* ── Airlock structure ── * Layout (6 tiles wide): @@ -543,56 +577,157 @@ static void gen_transition(Tilemap *map, int x0, int w, * col 4: inner right frame * col 5: outer wall (right bulkhead) * - * Rows 4-6: thick ceiling / airlock header - * Rows 7-8: door frame top (inner columns only) - * Rows 9-18: open passageway - * Row 19: floor level (ground starts at 20) + * Vertical layout is relative to ground_row: + * header: ground_row-16 to ground_row-13 + * inner frame: ground_row-12 to ground_row-1 + * open passageway in between */ int left = x0; int right = x0 + w - 1; - /* Outer bulkhead walls — full height from top to ground */ - fill_rect(col, mw, left, 0, left, GROUND_ROW - 1, TILE_SOLID_1); - fill_rect(col, mw, right, 0, right, GROUND_ROW - 1, TILE_SOLID_1); + int header_top = ground_row - 16; + if (header_top < 0) header_top = 0; + int header_bot = ground_row - 13; + int frame_top = ground_row - 12; + + /* Outer bulkhead walls — full height from header to ground */ + fill_rect(col, mw, mh, left, header_top, left, ground_row - 1, TILE_SOLID_1); + fill_rect(col, mw, mh, right, header_top, right, ground_row - 1, TILE_SOLID_1); /* Thick ceiling header (airlock hull) */ - fill_rect(col, mw, left, 4, right, 7, TILE_SOLID_2); + fill_rect(col, mw, mh, left, header_top, right, header_bot, TILE_SOLID_2); /* Inner frame columns — pillars flanking the doorway */ - fill_rect(col, mw, left + 1, 8, left + 1, GROUND_ROW - 1, TILE_SOLID_2); - fill_rect(col, mw, right - 1, 8, right - 1, GROUND_ROW - 1, TILE_SOLID_2); + fill_rect(col, mw, mh, left + 1, frame_top, left + 1, ground_row - 1, TILE_SOLID_2); + fill_rect(col, mw, mh, right - 1, frame_top, right - 1, ground_row - 1, TILE_SOLID_2); /* Clear the inner chamber (passable area between pillars) */ - for (int y = 8; y < GROUND_ROW; y++) { + for (int y = frame_top; y < ground_row; y++) { for (int x = left + 2; x <= right - 2; x++) { - set_tile(col, mw, x, y, TILE_EMPTY); + set_tile(col, mw, mh, x, y, TILE_EMPTY); } } /* Door openings in the outer walls (player entry/exit) */ - for (int y = GROUND_ROW - 4; y < GROUND_ROW; y++) { - set_tile(col, mw, left, y, TILE_EMPTY); - set_tile(col, mw, right, y, TILE_EMPTY); + for (int y = ground_row - 4; y < ground_row; y++) { + set_tile(col, mw, mh, left, y, TILE_EMPTY); + set_tile(col, mw, mh, right, y, TILE_EMPTY); } /* Floor plating inside the airlock — one-way platform */ for (int x = left + 2; x <= right - 2; x++) { - set_tile(col, mw, x, GROUND_ROW - 1, TILE_PLAT); + set_tile(col, mw, mh, x, ground_row - 1, TILE_PLAT); } /* ── Hazards inside the airlock ── */ + int hazard_y = (frame_top + ground_row) / 2; /* Force field barrier in the doorway when entering a tech zone */ if ((to == THEME_PLANET_BASE || to == THEME_SPACE_STATION) && from == THEME_PLANET_SURFACE) { - add_entity(map, "force_field", x0 + w / 2, 14); + add_entity(map, "force_field", x0 + w / 2, hazard_y); } /* Force field when transitioning between base and station too */ if ((from == THEME_PLANET_BASE && to == THEME_SPACE_STATION) || (from == THEME_SPACE_STATION && to == THEME_PLANET_BASE)) { - add_entity(map, "force_field", x0 + w / 2, 13); + add_entity(map, "force_field", x0 + w / 2, hazard_y - 1); + } +} + +/* ═══════════════════════════════════════════════════ + * SEG_CLIMB — vertical connector between height zones + * + * Bridges two different ground levels using a shaft + * with alternating platforms. When descending (high→low) + * the shaft goes from ground_from down to ground_to. + * The segment width is fixed at 10 tiles. + * ═══════════════════════════════════════════════════ */ + +static void gen_climb(Tilemap *map, int x0, int w, + int ground_from, int ground_to, + float difficulty, LevelTheme theme) { + uint16_t *col = map->collision_layer; + int mw = map->width; + int mh = map->height; + + /* Determine the top and bottom of the shaft */ + int top_ground = (ground_from < ground_to) ? ground_from : ground_to; + int bot_ground = (ground_from < ground_to) ? ground_to : ground_from; + + /* Respect traversal direction: left side is ground_from, right is ground_to. + * Ascending (from > to): left is LOW (bottom), right is HIGH (top) + * Descending (from < to): left is HIGH (top), right is LOW (bottom) */ + int left_ground = ground_from; + int right_ground = ground_to; + + /* Fill ground on both levels at the edges of the segment */ + fill_rect(col, mw, mh, x0, left_ground, x0 + 2, mh - 1, TILE_SOLID_1); + fill_rect(col, mw, mh, x0 + w - 3, right_ground, x0 + w - 1, mh - 1, TILE_SOLID_1); + + /* Shaft walls span the full height range between the two zones */ + int shaft_left = x0 + 2; + int shaft_right = x0 + w - 3; + fill_rect(col, mw, mh, shaft_left, top_ground, shaft_left, bot_ground - 1, TILE_SOLID_1); + fill_rect(col, mw, mh, shaft_right, top_ground, shaft_right, bot_ground - 1, TILE_SOLID_1); + + /* Connect left ground to shaft entrance */ + fill_rect(col, mw, mh, x0, left_ground, shaft_left, left_ground + FLOOR_ROWS - 1, TILE_SOLID_1); + + /* Connect right ground to shaft exit */ + fill_rect(col, mw, mh, shaft_right, right_ground, x0 + w - 1, right_ground + FLOOR_ROWS - 1, TILE_SOLID_1); + + /* Opening at left side of shaft — player enters from ground_from level */ + for (int r = left_ground - 3; r < left_ground; r++) { + set_tile(col, mw, mh, shaft_left, r, TILE_EMPTY); + } + + /* Opening at right side of shaft — player exits to ground_to level */ + for (int r = right_ground - 3; r < right_ground; r++) { + set_tile(col, mw, mh, shaft_right, r, TILE_EMPTY); + } + + /* Alternating platforms inside the shaft */ + int shaft_inner_w = shaft_right - shaft_left - 1; + int shaft_depth = bot_ground - top_ground; + int num_plats = shaft_depth / 4; + if (num_plats < 3) num_plats = 3; + if (num_plats > 8) num_plats = 8; + + for (int i = 0; i < num_plats; i++) { + int py = top_ground + 2 + i * (shaft_depth - 4) / num_plats; + if (py >= bot_ground - 1) break; + bool left_side = (i % 2 == 0); + int px = left_side ? shaft_left + 1 : shaft_right - 3; + int pw = (shaft_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 in the shaft center */ + if (rng_float() < 0.5f) { + int mid_x = (shaft_left + shaft_right) / 2; + int mid_y = (top_ground + bot_ground) / 2; + add_entity(map, "platform_v", mid_x, mid_y); + } + + /* Aerial threat inside the shaft */ + if (difficulty > 0.3f && rng_float() < difficulty) { + int mid_y = (top_ground + bot_ground) / 2; + if (theme == THEME_SPACE_STATION && rng_float() < 0.4f) { + int turret_x = (rng_range(0, 1)) ? shaft_left + 1 : shaft_right - 1; + add_entity(map, "turret", turret_x, mid_y); + } else { + add_entity(map, "flyer", (shaft_left + shaft_right) / 2, mid_y); + } + } + + /* Powerup midway through the climb */ + if (rng_float() < 0.4f) { + int mid_y = (top_ground + bot_ground) / 2; + add_entity(map, "powerup_jet", (shaft_left + shaft_right) / 2, mid_y - 2); } } @@ -658,7 +793,7 @@ static void gen_bg_decoration(Tilemap *map) { uint16_t *bg = map->bg_layer; int mw = map->width; - for (int y = 0; y < SEG_HEIGHT - FLOOR_ROWS; y++) { + for (int y = 0; y < map->height - FLOOR_ROWS; y++) { for (int x = 0; x < mw; x++) { /* Only place bg tiles where collision is empty */ if (map->collision_layer[y * mw + x] != TILE_EMPTY) continue; @@ -696,18 +831,24 @@ bool levelgen_generate(Tilemap *map, const LevelGenConfig *config) { if (num_segs < 2) num_segs = 2; if (num_segs > 10) num_segs = 10; - /* ── Phase 1: decide segment types and widths ── */ - /* We may insert transition segments between theme boundaries, + /* ── Phase 1: decide segment types, widths, and height zones ── */ + /* We may insert transition/climb segments between boundaries, * so the final count can be larger than num_segs. - * Max: num_segs content + (num_segs-1) transitions = 2*num_segs-1 */ - #define MAX_FINAL_SEGS 20 + * Max: num_segs content + (num_segs-1) connectors*2 = ~3*num_segs */ + #define MAX_FINAL_SEGS 32 SegmentType seg_types[MAX_FINAL_SEGS]; int seg_widths[MAX_FINAL_SEGS]; - LevelTheme seg_themes[MAX_FINAL_SEGS]; /* per-segment theme */ - LevelTheme seg_from[MAX_FINAL_SEGS]; /* for transitions: source theme */ + LevelTheme seg_themes[MAX_FINAL_SEGS]; /* per-segment theme */ + LevelTheme seg_from[MAX_FINAL_SEGS]; /* for transitions */ + int seg_ground[MAX_FINAL_SEGS]; /* ground row per segment */ int final_seg_count = 0; int total_width = 0; + bool uses_tall = false; /* track if any zone change occurs */ + /* Determine zone preference per theme: + * Surface: stays LOW (bottom of tall map, or normal if single-screen) + * Base: uses HIGH (upper area of tall map) + * Station: alternates between HIGH and LOW */ for (int i = 0; i < num_segs; i++) { LevelTheme t = theme_for_segment(config, i); @@ -717,8 +858,9 @@ bool levelgen_generate(Tilemap *map, const LevelGenConfig *config) { if (t != prev_t && final_seg_count < MAX_FINAL_SEGS) { seg_types[final_seg_count] = SEG_TRANSITION; seg_widths[final_seg_count] = segment_width(SEG_TRANSITION); - seg_themes[final_seg_count] = t; /* entering new theme */ - seg_from[final_seg_count] = prev_t; /* leaving old theme */ + seg_themes[final_seg_count] = t; + seg_from[final_seg_count] = prev_t; + seg_ground[final_seg_count] = 0; /* filled in later */ total_width += seg_widths[final_seg_count]; final_seg_count++; } @@ -729,19 +871,135 @@ bool levelgen_generate(Tilemap *map, const LevelGenConfig *config) { seg_types[final_seg_count] = pick_segment_type(t, i, num_segs); seg_widths[final_seg_count] = segment_width(seg_types[final_seg_count]); seg_themes[final_seg_count] = t; - seg_from[final_seg_count] = t; /* not a transition */ + seg_from[final_seg_count] = t; + seg_ground[final_seg_count] = 0; /* filled in later */ total_width += seg_widths[final_seg_count]; final_seg_count++; } num_segs = final_seg_count; + /* ── Phase 1b: assign height zones per segment ── */ + /* Decide if this level should be tall based on theme variety. + * A level with both Surface and Base/Station themes gets tall + * to give each theme its own vertical zone. Single-theme levels + * stay at the standard 23-tile height. */ + bool has_surface = false, has_base = false, has_station = false; + for (int i = 0; i < num_segs; i++) { + if (seg_themes[i] == THEME_PLANET_SURFACE) has_surface = true; + if (seg_themes[i] == THEME_PLANET_BASE) has_base = true; + if (seg_themes[i] == THEME_SPACE_STATION) has_station = true; + } + + /* Go tall if we have theme variety that benefits from verticality */ + uses_tall = (has_surface && (has_base || has_station)) || + (has_station && num_segs >= 5); + + if (uses_tall) { + /* Assign ground rows based on theme: + * Surface → LOW zone (ground at row 43, near bottom) + * Base → HIGH zone (ground at row 17, near top) + * Station → alternates, preferring HIGH */ + int station_zone_counter = 0; + for (int i = 0; i < num_segs; i++) { + if (seg_types[i] == SEG_TRANSITION) { + /* Skip: ground row filled in from predecessor below */ + continue; + } + switch (seg_themes[i]) { + case THEME_PLANET_SURFACE: + seg_ground[i] = ZONE_LOW_GROUND; + break; + case THEME_PLANET_BASE: + seg_ground[i] = ZONE_HIGH_GROUND; + break; + case THEME_SPACE_STATION: + /* Alternate every 2 segments */ + seg_ground[i] = (station_zone_counter / 2 % 2 == 0) + ? ZONE_HIGH_GROUND : ZONE_LOW_GROUND; + station_zone_counter++; + break; + default: + seg_ground[i] = ZONE_LOW_GROUND; + break; + } + } + + /* Fill in transition segment ground rows from their predecessor. + * Transitions must be walkable from the previous segment; if the + * zone also changes, a SEG_CLIMB is inserted *after* the transition + * to bridge to the new elevation. */ + for (int i = 0; i < num_segs; i++) { + if (seg_types[i] == SEG_TRANSITION) { + /* Find predecessor ground row */ + int prev_gr = ZONE_LOW_GROUND; + for (int j = i - 1; j >= 0; j--) { + if (seg_ground[j] != 0) { prev_gr = seg_ground[j]; break; } + } + seg_ground[i] = prev_gr; + } + } + + /* Insert SEG_CLIMB segments where the ground level changes. + * Climbs are inserted between any two adjacent segments whose + * ground rows differ, including after transitions that stayed + * at the old elevation while the next segment needs a new one. */ + SegmentType tmp_types[MAX_FINAL_SEGS]; + int tmp_widths[MAX_FINAL_SEGS]; + LevelTheme tmp_themes[MAX_FINAL_SEGS]; + LevelTheme tmp_from[MAX_FINAL_SEGS]; + int tmp_ground[MAX_FINAL_SEGS]; + int new_count = 0; + int climb_width_total = 0; + + for (int i = 0; i < num_segs; i++) { + /* Check if we need a climb before this segment */ + if (i > 0 && new_count > 0 && seg_ground[i] != 0 && + tmp_ground[new_count - 1] != seg_ground[i] && + new_count < MAX_FINAL_SEGS) { + /* Insert climb connector */ + tmp_types[new_count] = SEG_CLIMB; + tmp_widths[new_count] = segment_width(SEG_CLIMB); + tmp_themes[new_count] = seg_themes[i]; + tmp_from[new_count] = seg_themes[i > 0 ? i - 1 : i]; + tmp_ground[new_count] = seg_ground[i]; /* target zone */ + climb_width_total += tmp_widths[new_count]; + new_count++; + } + if (new_count < MAX_FINAL_SEGS) { + tmp_types[new_count] = seg_types[i]; + tmp_widths[new_count] = seg_widths[i]; + tmp_themes[new_count] = seg_themes[i]; + tmp_from[new_count] = seg_from[i]; + tmp_ground[new_count] = seg_ground[i]; + new_count++; + } + } + + /* Copy back */ + num_segs = new_count; + total_width += climb_width_total; + for (int i = 0; i < num_segs; i++) { + seg_types[i] = tmp_types[i]; + seg_widths[i] = tmp_widths[i]; + seg_themes[i] = tmp_themes[i]; + seg_from[i] = tmp_from[i]; + seg_ground[i] = tmp_ground[i]; + } + } else { + /* Standard single-screen height: all segments use GROUND_ROW */ + for (int i = 0; i < num_segs; i++) { + seg_ground[i] = GROUND_ROW; + } + } + /* Add 2-tile buffer on each side */ total_width += 4; /* ── Phase 2: allocate tilemap ── */ + int level_height = uses_tall ? TALL_HEIGHT : SEG_HEIGHT; memset(map, 0, sizeof(Tilemap)); map->width = total_width; - map->height = SEG_HEIGHT; + map->height = level_height; int total_tiles = map->width * map->height; map->collision_layer = calloc(total_tiles, sizeof(uint16_t)); @@ -762,33 +1020,41 @@ bool levelgen_generate(Tilemap *map, const LevelGenConfig *config) { /* ── Phase 4: generate segments ── */ int cursor = 2; /* start after left buffer */ + int mh = map->height; /* Left border wall */ - fill_rect(map->collision_layer, map->width, 0, 0, 1, SEG_HEIGHT - 1, TILE_SOLID_1); + fill_rect(map->collision_layer, map->width, mh, 0, 0, 1, mh - 1, TILE_SOLID_1); for (int i = 0; i < num_segs; i++) { int w = seg_widths[i]; LevelTheme theme = seg_themes[i]; + int gr = seg_ground[i]; switch (seg_types[i]) { - case SEG_FLAT: gen_flat(map, cursor, w, config->difficulty, theme); break; - case SEG_PIT: gen_pit(map, cursor, w, config->difficulty, theme); break; - case SEG_PLATFORMS: gen_platforms(map, cursor, w, config->difficulty, theme); break; - case SEG_CORRIDOR: gen_corridor(map, cursor, w, config->difficulty, theme); break; - case SEG_ARENA: gen_arena(map, cursor, w, config->difficulty, theme); break; - case SEG_SHAFT: gen_shaft(map, cursor, w, config->difficulty, theme); break; + case SEG_FLAT: gen_flat(map, cursor, w, gr, config->difficulty, theme); break; + case SEG_PIT: gen_pit(map, cursor, w, gr, config->difficulty, theme); break; + case SEG_PLATFORMS: gen_platforms(map, cursor, w, gr, config->difficulty, theme); break; + case SEG_CORRIDOR: gen_corridor(map, cursor, w, gr, config->difficulty, theme); break; + case SEG_ARENA: gen_arena(map, cursor, w, gr, config->difficulty, theme); break; + case SEG_SHAFT: gen_shaft(map, cursor, w, gr, config->difficulty, theme); break; case SEG_TRANSITION: - gen_transition(map, cursor, w, config->difficulty, seg_from[i], theme); + gen_transition(map, cursor, w, gr, config->difficulty, seg_from[i], theme); break; - default: gen_flat(map, cursor, w, config->difficulty, theme); break; + case SEG_CLIMB: { + /* Find the previous segment's ground row */ + int prev_gr = (i > 0) ? seg_ground[i - 1] : gr; + gen_climb(map, cursor, w, prev_gr, gr, config->difficulty, theme); + break; + } + default: gen_flat(map, cursor, w, gr, config->difficulty, theme); break; } cursor += w; } /* Right border wall */ - fill_rect(map->collision_layer, map->width, - map->width - 2, 0, map->width - 1, SEG_HEIGHT - 1, TILE_SOLID_1); + fill_rect(map->collision_layer, map->width, mh, + map->width - 2, 0, map->width - 1, mh - 1, TILE_SOLID_1); /* ── Phase 5: add visual variety to solid tiles ── */ for (int y = 0; y < map->height; y++) { @@ -809,7 +1075,8 @@ bool levelgen_generate(Tilemap *map, const LevelGenConfig *config) { gen_bg_decoration(map); /* ── Phase 7: metadata ── */ - map->player_spawn = vec2(3.0f * TILE_SIZE, (GROUND_ROW - 2) * TILE_SIZE); + int first_ground = seg_ground[0]; + map->player_spawn = vec2(3.0f * TILE_SIZE, (first_ground - 2) * TILE_SIZE); /* Theme-based gravity and atmosphere (uses first theme in sequence) */ LevelTheme primary_theme = config->themes[0]; @@ -824,8 +1091,9 @@ bool levelgen_generate(Tilemap *map, const LevelGenConfig *config) { * procedurally generated level. */ if (map->exit_zone_count < MAX_EXIT_ZONES) { ExitZone *ez = &map->exit_zones[map->exit_zone_count++]; + int last_ground = seg_ground[num_segs - 1]; int exit_x = map->width - 5; /* a few tiles from the right wall */ - int exit_y = GROUND_ROW - 3; /* 3 tiles above ground */ + int exit_y = last_ground - 3; /* 3 tiles above ground */ ez->x = (float)(exit_x * TILE_SIZE); ez->y = (float)(exit_y * TILE_SIZE); ez->w = 2.0f * TILE_SIZE; @@ -848,13 +1116,15 @@ bool levelgen_generate(Tilemap *map, const LevelGenConfig *config) { /* Segment type names for debug output */ static const char *seg_names[] = { - "flat", "pit", "plat", "corr", "arena", "shaft", "trans" + "flat", "pit", "plat", "corr", "arena", "shaft", "trans", "climb" }; - printf("levelgen: generated %dx%d level (%d segments, seed=%u)\n", - map->width, map->height, num_segs, s_rng_state); + printf("levelgen: generated %dx%d level (%d segments, seed=%u%s)\n", + map->width, map->height, num_segs, s_rng_state, + uses_tall ? ", tall" : ""); printf(" segments:"); for (int i = 0; i < num_segs; i++) { - printf(" %s[%s]", seg_names[seg_types[i]], theme_label(seg_themes[i])); + printf(" %s[%s@%d]", seg_names[seg_types[i]], + theme_label(seg_themes[i]), seg_ground[i]); } printf("\n"); @@ -879,19 +1149,20 @@ bool levelgen_generate(Tilemap *map, const LevelGenConfig *config) { #define STATION_FLOOR_ROW 17 /* floor top edge (rows 17-22 solid) */ #define STATION_PLAY_H 12 /* playable rows: 5-16 inclusive */ -static void station_fill_envelope(uint16_t *col, int mw, int x0, int x1) { +static void station_fill_envelope(uint16_t *col, int mw, int mh, int x0, int x1) { /* Ceiling: rows 0 through STATION_CEIL_ROW */ - fill_rect(col, mw, x0, 0, x1, STATION_CEIL_ROW, TILE_SOLID_1); + fill_rect(col, mw, mh, x0, 0, x1, STATION_CEIL_ROW, TILE_SOLID_1); /* Floor: rows STATION_FLOOR_ROW through bottom */ - fill_rect(col, mw, x0, STATION_FLOOR_ROW, x1, SEG_HEIGHT - 1, TILE_SOLID_1); + fill_rect(col, mw, mh, x0, STATION_FLOOR_ROW, x1, SEG_HEIGHT - 1, TILE_SOLID_1); } /* ── Station segment: long flat corridor ── */ static void gen_station_corridor(Tilemap *map, int x0, int w, float difficulty) { uint16_t *col = map->collision_layer; int mw = map->width; + int mh = map->height; - station_fill_envelope(col, mw, x0, x0 + w - 1); + station_fill_envelope(col, mw, mh, x0, x0 + w - 1); /* Random platforms floating in the corridor */ int num_plats = rng_range(1, 3); @@ -900,7 +1171,7 @@ static void gen_station_corridor(Tilemap *map, int x0, int w, float difficulty) int py = rng_range(STATION_CEIL_ROW + 3, STATION_FLOOR_ROW - 3); int pw = rng_range(2, 4); for (int j = 0; j < pw && px + j < x0 + w; j++) { - set_tile(col, mw, px + j, py, TILE_PLAT); + set_tile(col, mw, mh, px + j, py, TILE_PLAT); } } @@ -927,17 +1198,18 @@ static void gen_station_corridor(Tilemap *map, int x0, int w, float difficulty) static void gen_station_bulkhead(Tilemap *map, int x0, int w, float difficulty) { uint16_t *col = map->collision_layer; int mw = map->width; + int mh = map->height; - station_fill_envelope(col, mw, x0, x0 + w - 1); + station_fill_envelope(col, mw, mh, x0, x0 + w - 1); /* Central bulkhead wall with a doorway */ int wall_x = x0 + w / 2; - fill_rect(col, mw, wall_x, STATION_CEIL_ROW + 1, wall_x, STATION_FLOOR_ROW - 1, TILE_SOLID_2); + fill_rect(col, mw, mh, wall_x, STATION_CEIL_ROW + 1, wall_x, STATION_FLOOR_ROW - 1, TILE_SOLID_2); /* Doorway opening (3 tiles tall) */ int door_y = rng_range(STATION_CEIL_ROW + 3, STATION_FLOOR_ROW - 4); for (int y = door_y; y < door_y + 3; y++) { - set_tile(col, mw, wall_x, y, TILE_EMPTY); + set_tile(col, mw, mh, wall_x, y, TILE_EMPTY); } /* Turret guarding the doorway — always present */ @@ -970,15 +1242,16 @@ static void gen_station_bulkhead(Tilemap *map, int x0, int w, float difficulty) static void gen_station_platforms(Tilemap *map, int x0, int w, float difficulty) { uint16_t *col = map->collision_layer; int mw = map->width; + int mh = map->height; - station_fill_envelope(col, mw, x0, x0 + w - 1); + station_fill_envelope(col, mw, mh, x0, x0 + w - 1); /* Remove some floor sections to create pits (lethal in a station) */ int pit_start = x0 + rng_range(3, 6); int pit_end = x0 + w - rng_range(3, 6); for (int x = pit_start; x < pit_end && x < x0 + w; x++) { for (int y = STATION_FLOOR_ROW; y < STATION_FLOOR_ROW + 2; y++) { - set_tile(col, mw, x, y, TILE_EMPTY); + set_tile(col, mw, mh, x, y, TILE_EMPTY); } } @@ -991,7 +1264,7 @@ static void gen_station_platforms(Tilemap *map, int x0, int w, float difficulty) int py = rng_range(STATION_CEIL_ROW + 4, STATION_FLOOR_ROW - 2); int pw = rng_range(2, 3); for (int j = 0; j < pw && px + j < x0 + w; j++) { - set_tile(col, mw, px + j, py, TILE_PLAT); + set_tile(col, mw, mh, px + j, py, TILE_PLAT); } } @@ -1020,27 +1293,28 @@ static void gen_station_platforms(Tilemap *map, int x0, int w, float difficulty) static void gen_station_bay(Tilemap *map, int x0, int w, float difficulty) { uint16_t *col = map->collision_layer; int mw = map->width; + int mh = map->height; - station_fill_envelope(col, mw, x0, x0 + w - 1); + station_fill_envelope(col, mw, mh, x0, x0 + w - 1); /* Open up a taller space by raising the ceiling locally */ int bay_x0 = x0 + 2; int bay_x1 = x0 + w - 3; for (int x = bay_x0; x <= bay_x1; x++) { - set_tile(col, mw, x, STATION_CEIL_ROW, TILE_EMPTY); - set_tile(col, mw, x, STATION_CEIL_ROW - 1, TILE_EMPTY); + set_tile(col, mw, mh, x, STATION_CEIL_ROW, TILE_EMPTY); + set_tile(col, mw, mh, x, STATION_CEIL_ROW - 1, TILE_EMPTY); } /* Central floating platform */ int cp_x = x0 + w / 2 - 2; int cp_y = rng_range(STATION_CEIL_ROW + 2, STATION_FLOOR_ROW - 5); for (int j = 0; j < 4; j++) { - set_tile(col, mw, cp_x + j, cp_y, TILE_PLAT); + set_tile(col, mw, mh, cp_x + j, cp_y, TILE_PLAT); } /* Ledges on the sides */ - fill_rect(col, mw, x0, STATION_FLOOR_ROW - 3, x0 + 1, STATION_FLOOR_ROW - 1, TILE_SOLID_1); - fill_rect(col, mw, x0 + w - 2, STATION_FLOOR_ROW - 3, x0 + w - 1, STATION_FLOOR_ROW - 1, TILE_SOLID_1); + fill_rect(col, mw, mh, x0, STATION_FLOOR_ROW - 3, x0 + 1, STATION_FLOOR_ROW - 1, TILE_SOLID_1); + fill_rect(col, mw, mh, x0 + w - 2, STATION_FLOOR_ROW - 3, x0 + w - 1, STATION_FLOOR_ROW - 1, TILE_SOLID_1); /* Swarm of enemies — bays are the big combat encounters */ int num_enemies = 3 + (int)(difficulty * 4); @@ -1074,20 +1348,21 @@ static void gen_station_bay(Tilemap *map, int x0, int w, float difficulty) { static void gen_station_vent(Tilemap *map, int x0, int w, float difficulty) { uint16_t *col = map->collision_layer; int mw = map->width; + int mh = map->height; - station_fill_envelope(col, mw, x0, x0 + w - 1); + station_fill_envelope(col, mw, mh, x0, x0 + w - 1); /* Lower the ceiling even more for a tight crawlspace */ int vent_ceil = STATION_CEIL_ROW + 3; - fill_rect(col, mw, x0, STATION_CEIL_ROW + 1, x0 + w - 1, vent_ceil, TILE_SOLID_2); + fill_rect(col, mw, mh, x0, STATION_CEIL_ROW + 1, x0 + w - 1, vent_ceil, TILE_SOLID_2); /* Opening at left */ for (int y = vent_ceil - 1; y <= vent_ceil + 2 && y < STATION_FLOOR_ROW; y++) { - set_tile(col, mw, x0, y, TILE_EMPTY); + set_tile(col, mw, mh, x0, y, TILE_EMPTY); } /* Opening at right */ for (int y = vent_ceil - 1; y <= vent_ceil + 2 && y < STATION_FLOOR_ROW; y++) { - set_tile(col, mw, x0 + w - 1, y, TILE_EMPTY); + set_tile(col, mw, mh, x0 + w - 1, y, TILE_EMPTY); } /* Flame vents along the floor — always present, more at higher difficulty */ @@ -1116,8 +1391,10 @@ static void gen_station_vent(Tilemap *map, int x0, int w, float difficulty) { static void gen_station_entry(Tilemap *map, int x0, int w, float difficulty) { uint16_t *col = map->collision_layer; int mw = map->width; + int mh = map->height; + (void)col; (void)mw; (void)mh; /* used by station_fill_envelope */ - station_fill_envelope(col, mw, x0, x0 + w - 1); + station_fill_envelope(col, mw, mh, x0, x0 + w - 1); /* Health pickup to start — always present */ add_entity(map, "powerup_hp", x0 + w / 2, STATION_FLOOR_ROW - 1); @@ -1236,9 +1513,10 @@ bool levelgen_generate_station(Tilemap *map, const LevelGenConfig *config) { /* ── Phase 4: generate segments ── */ int cursor = 2; + int smh = map->height; /* Left border wall */ - fill_rect(map->collision_layer, map->width, 0, 0, 1, SEG_HEIGHT - 1, TILE_SOLID_1); + fill_rect(map->collision_layer, map->width, smh, 0, 0, 1, smh - 1, TILE_SOLID_1); static const char *sseg_names[] = { "entry", "corr", "bulk", "plat", "bay", "vent" @@ -1262,8 +1540,8 @@ bool levelgen_generate_station(Tilemap *map, const LevelGenConfig *config) { } /* Right border wall */ - fill_rect(map->collision_layer, map->width, - map->width - 2, 0, map->width - 1, SEG_HEIGHT - 1, TILE_SOLID_1); + fill_rect(map->collision_layer, map->width, smh, + map->width - 2, 0, map->width - 1, smh - 1, TILE_SOLID_1); /* ── Phase 5: visual variety ── */ for (int y = 0; y < map->height; y++) { diff --git a/src/game/player.c b/src/game/player.c index 355d424..7b42500 100644 --- a/src/game/player.c +++ b/src/game/player.c @@ -288,6 +288,11 @@ void player_update(Entity *self, float dt, const Tilemap *map) { ); particle_emit_jetpack_trail(exhaust_pos, pd->dash_dir); + /* Blue flame trail when boost is active */ + if (pd->jetpack_boost_timer > 0) { + particle_emit_jetpack_boost_trail(exhaust_pos, pd->dash_dir); + } + /* Skip normal movement during dash */ physics_update(body, dt, map); animation_update(&self->anim, dt); @@ -330,10 +335,28 @@ void player_update(Entity *self, float dt, const Tilemap *map) { ); particle_emit_jetpack_burst(exhaust_pos, pd->dash_dir); + /* Blue flame accents when boost powerup is active */ + if (pd->jetpack_boost_timer > 0) { + particle_emit_jetpack_boost_burst(exhaust_pos, pd->dash_dir); + } + audio_play_sound(s_sfx_dash, 96); return; } + /* ── Jetpack boost idle glow ─────────────── */ + /* Ambient blue flame from the player's back while boost is active + * and not dashing. Emits from the rear center of the sprite. */ + if (pd->jetpack_boost_timer > 0) { + bool facing_left = (self->flags & ENTITY_FACING_LEFT) != 0; + Vec2 back_pos = vec2( + facing_left ? body->pos.x + body->size.x - 1.0f + : body->pos.x + 1.0f, + body->pos.y + body->size.y * 0.45f + ); + particle_emit_jetpack_boost_idle(back_pos, facing_left); + } + /* ── Horizontal movement ─────────────────── */ float target_vx = 0.0f; if (hold_left) target_vx -= PLAYER_SPEED;