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.
This commit is contained in:
Thomas
2026-03-01 14:57:53 +00:00
parent 9d828c47b1
commit ad2d68a8b4
8 changed files with 813 additions and 192 deletions

View File

@@ -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

44
TODO.md
View File

@@ -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.

View File

@@ -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

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

File diff suppressed because it is too large Load Diff

View File

@@ -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;