From 4407932a2dfb042d9c9caada42232049959ecf59 Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 5 Mar 2026 19:21:41 +0000 Subject: [PATCH] Add Mars surface atmosphere particels --- TODO.md | 14 ++++++---- src/engine/particle.c | 62 +++++++++++++++++++++++++++++++++++++++++++ src/engine/particle.h | 5 ++++ src/game/level.c | 9 +++++++ 4 files changed, 85 insertions(+), 5 deletions(-) diff --git a/TODO.md b/TODO.md index d965c09..6cb7875 100644 --- a/TODO.md +++ b/TODO.md @@ -145,11 +145,15 @@ which defers a level load into MODE_PLAY on the next frame. Tears down the current mode (editor or play), loads the selected `.lvl` file, and seeds `s_edit_path` so pressing E opens the same level in the editor. -## Mars Surface atmosphere particles -Mars Surface levels should have ambient floating particles (dust motes, fine -sand) drifting across the screen to sell the atmosphere. Could tie into the -existing wind system — particles drift with the wind direction. Should be -subtle, low-alpha, and layered behind/in front of the player for depth. +## ~~Mars Surface atmosphere particles~~ ✓ +Implemented: `particle_emit_atmosphere_dust()` emits ambient dust motes each +frame on Mars Surface levels (keyed on `PARALLAX_STYLE_MARS`). Three +sub-layers for depth: large slow "far" motes, small quick "near" specks, +and occasional interior spawns to prevent edge seams. All particles drift +with the wind system via `gravity_scale` — wind pushes them across the +viewport. Reddish-tan color palette with per-particle variation. Low drag, +long lifetimes (3-7 s), subtle alpha fade. ~2-3 particles/frame at 60 Hz, +well within the 1024-particle pool budget. ## Skip spacecraft transition for non-surface levels The spacecraft fly-in animation should only play on surface levels (moon01, diff --git a/src/engine/particle.c b/src/engine/particle.c index 3ef1d0c..c7838ac 100644 --- a/src/engine/particle.c +++ b/src/engine/particle.c @@ -529,3 +529,65 @@ void particle_emit_wall_slide_dust(Vec2 pos, int wall_dir) { }; particle_emit(&dust); } + +/* Spawn a single dust mote with the given visual properties. */ +static void spawn_dust_mote(Vec2 pos, Vec2 vel, + float life_min, float life_max, + float size_min, float size_max, + float drag, float gscale, + uint8_t r, uint8_t g, uint8_t b, int vary) { + Particle *p = alloc_particle(); + p->pos = pos; + p->vel = vel; + p->life = randf_range(life_min, life_max); + p->max_life = p->life; + p->size = randf_range(size_min, size_max); + p->drag = drag; + p->gravity_scale = gscale; + p->active = true; + p->color.r = clamp_u8(r + (int)randf_range(-vary, vary)); + p->color.g = clamp_u8(g + (int)randf_range(-vary, vary)); + p->color.b = clamp_u8(b + (int)randf_range(-vary, vary)); + p->color.a = 255; /* alpha applied during render from life ratio */ +} + +void particle_emit_atmosphere_dust(Vec2 cam_pos, Vec2 vp) { + /* Ambient Mars dust — subtle motes drifting across the viewport. + * Two sub-layers for depth: large slow "far" motes and small quick + * "near" specks. Wind carries them; gravity_scale controls how much + * environmental forces (wind + gravity) affect each particle. + * Particles spawn along the upwind viewport edge and drift inward; + * occasional interior spawns prevent a visible edge seam. */ + float wind = physics_get_wind(); + float margin = 32.0f; + float dir = (wind >= 0.0f) ? 1.0f : -1.0f; /* velocity sign */ + + /* Upwind edge X for the two edge-spawned layers */ + float edge_far = (wind >= 0.0f) ? cam_pos.x - margin + : cam_pos.x + vp.x + margin; + float edge_near = (wind >= 0.0f) ? cam_pos.x - margin * 0.5f + : cam_pos.x + vp.x + margin * 0.5f; + + /* Far dust motes — large, slow, translucent (1/frame) */ + spawn_dust_mote( + vec2(edge_far, cam_pos.y + randf() * vp.y), + vec2(dir * randf_range(8.0f, 25.0f), randf_range(-6.0f, 6.0f)), + 4.0f, 7.0f, 1.5f, 3.0f, 0.3f, 0.08f, + 180, 140, 100, 25); + + /* Near dust specks — small, quicker, brighter (1/frame) */ + spawn_dust_mote( + vec2(edge_near, cam_pos.y + randf() * vp.y), + vec2(dir * randf_range(15.0f, 40.0f), randf_range(-10.0f, 10.0f)), + 2.5f, 5.0f, 0.8f, 1.5f, 0.2f, 0.12f, + 200, 160, 120, 20); + + /* Occasional interior spawn — prevents edge seam on calm wind */ + if (rand() % 3 == 0) { + spawn_dust_mote( + vec2(cam_pos.x + randf() * vp.x, cam_pos.y + randf() * vp.y), + vec2(randf_range(-5.0f, 5.0f), randf_range(-8.0f, 3.0f)), + 3.0f, 6.0f, 1.0f, 2.5f, 0.4f, 0.06f, + 160, 130, 95, 25); + } +} diff --git a/src/engine/particle.h b/src/engine/particle.h index f7a7ba2..b39d4d5 100644 --- a/src/engine/particle.h +++ b/src/engine/particle.h @@ -89,4 +89,9 @@ void particle_emit_muzzle_flash(Vec2 pos, Vec2 shoot_dir); /* Wall slide dust (small puffs while scraping against a wall) */ void particle_emit_wall_slide_dust(Vec2 pos, int wall_dir); +/* Ambient atmosphere dust (call each frame for Mars Surface levels). + * Spawns subtle dust motes around the camera viewport that drift with wind. + * cam_pos = camera top-left world position, vp = viewport size in pixels. */ +void particle_emit_atmosphere_dust(Vec2 cam_pos, Vec2 vp); + #endif /* JNR_PARTICLE_H */ diff --git a/src/game/level.c b/src/game/level.c index e996b97..2723fdb 100644 --- a/src/game/level.c +++ b/src/game/level.c @@ -578,6 +578,15 @@ void level_update(Level *level, float dt) { } } + /* Emit ambient atmosphere dust on Mars Surface levels before the + * particle update pass so new motes get their first physics step + * this frame — consistent with other per-frame emitters. */ + if (level->map.parallax_style == PARALLAX_STYLE_MARS) { + particle_emit_atmosphere_dust( + level->camera.pos, + level->camera.viewport); + } + /* Update particles */ particle_update(dt);