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:
94
AGENTS.md
94
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
|
||||
|
||||
44
TODO.md
44
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.
|
||||
|
||||
87
assets/levels/tall_test.lvl
Normal file
87
assets/levels/tall_test.lvl
Normal 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
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user