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
|
- `snprintf` / `strncpy` with explicit size limits for strings
|
||||||
- `ASSET_PATH_MAX` (256) for path buffers
|
- `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
|
## Architecture Patterns
|
||||||
|
|
||||||
### Entity System
|
### 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.
|
intro, 5 asteroids, unarmed until gun powerup near exit. Exit to level01.
|
||||||
- **level01.lvl** — Space station with spacecraft landing intro (arriving from moon).
|
- **level01.lvl** — Space station with spacecraft landing intro (arriving from moon).
|
||||||
|
|
||||||
## Level generator: height zones (verticality)
|
## ~~Level generator: height zones (verticality)~~ ✓
|
||||||
Add vertical variety to generated levels using a "height zones" approach:
|
Implemented: tall level support (46 tiles, two screens) with height zones.
|
||||||
- Double tilemap height from 23 to ~46 tiles (two screens tall)
|
- Camera vertical look-ahead (30px lead when player moves vertically > 50 px/s)
|
||||||
- Define two elevation bands: HIGH (ground ~row 17) and LOW (ground ~row 40)
|
- All segment generators (`gen_flat`, `gen_pit`, `gen_platforms`, `gen_corridor`,
|
||||||
- Existing segment generators get a `y_offset` parameter so internal logic
|
`gen_arena`, `gen_shaft`, `gen_transition`) accept `ground_row` parameter —
|
||||||
barely changes — platforms, enemies, corridors all shift by offset
|
platforms, enemies, and hazards are placed relative to the zone's ground level
|
||||||
- New vertical connector segments (ramps/climbs) bridge between zones
|
- `SEG_CLIMB` connector segment type: vertical shaft with alternating platforms,
|
||||||
- Theme decides zone usage: Surface stays LOW, Station uses both, Base uses HIGH
|
wall openings, optional moving platform and enemies, bridges height zones
|
||||||
- Camera needs vertical look-ahead when player has vertical velocity
|
- `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
|
## ~~Jetpack boost blue flame effects~~ ✓
|
||||||
1. Add vertical camera look-ahead (small Y lead when player moves vertically)
|
Implemented: `particle_emit_jetpack_boost_burst()` (electric blue core +
|
||||||
2. Create a handcrafted tall test level (~40x46) to validate camera, physics,
|
blue-white flare, 18 particles mixed into regular burst) and
|
||||||
rendering, and entities all work with vertical scrolling
|
`particle_emit_jetpack_boost_trail()` (blue sparks + pale blue wisps,
|
||||||
3. Refactor `set_tile()` / `fill_ground()` to accept a height parameter instead
|
3 particles/frame). Both activate only when `jetpack_boost_timer > 0`.
|
||||||
of hardcoded `SEG_HEIGHT`
|
Burst fires on dash start, trail emits each frame during dash.
|
||||||
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
|
|
||||||
|
|||||||
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->bounds_max = vec2(vp_w, vp_h); /* default: one screen */
|
||||||
c->smoothing = 5.0f;
|
c->smoothing = 5.0f;
|
||||||
c->deadzone = vec2(30.0f, 20.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;
|
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;
|
if (velocity.x > 10.0f) desired.x += c->look_ahead.x;
|
||||||
else 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 */
|
/* Smooth follow using exponential decay */
|
||||||
float t = 1.0f - expf(-c->smoothing * dt);
|
float t = 1.0f - expf(-c->smoothing * dt);
|
||||||
c->pos = vec2_lerp(c->pos, desired, t);
|
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);
|
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) {
|
void particle_emit_muzzle_flash(Vec2 pos, Vec2 shoot_dir) {
|
||||||
float angle = atan2f(shoot_dir.y, shoot_dir.x);
|
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) */
|
/* Jetpack exhaust trail (call each frame while dashing) */
|
||||||
void particle_emit_jetpack_trail(Vec2 pos, Vec2 dash_dir);
|
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) */
|
/* Muzzle flash (short bright burst in shoot direction) */
|
||||||
void particle_emit_muzzle_flash(Vec2 pos, Vec2 shoot_dir);
|
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);
|
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 */
|
/* Skip normal movement during dash */
|
||||||
physics_update(body, dt, map);
|
physics_update(body, dt, map);
|
||||||
animation_update(&self->anim, dt);
|
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);
|
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);
|
audio_play_sound(s_sfx_dash, 96);
|
||||||
return;
|
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 ─────────────────── */
|
/* ── Horizontal movement ─────────────────── */
|
||||||
float target_vx = 0.0f;
|
float target_vx = 0.0f;
|
||||||
if (hold_left) target_vx -= PLAYER_SPEED;
|
if (hold_left) target_vx -= PLAYER_SPEED;
|
||||||
|
|||||||
Reference in New Issue
Block a user