forked from tas/major_tom
When wind is zero or near-zero, dust particles were always spawning at the left viewport edge (the "upwind" edge for wind >= 0). Without wind force to carry them across, they accumulated on the left side. Now particles spawn across the full viewport when wind is calm (< 5 px/s²), with random drift directions for even distribution. Closes #2 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
625 lines
20 KiB
C
625 lines
20 KiB
C
#include "engine/particle.h"
|
|
#include "engine/renderer.h"
|
|
#include "engine/physics.h"
|
|
#include "engine/camera.h"
|
|
#include <stdlib.h>
|
|
#include <math.h>
|
|
|
|
#ifndef M_PI
|
|
#define M_PI 3.14159265358979323846
|
|
#endif
|
|
|
|
static Particle s_particles[MAX_PARTICLES];
|
|
|
|
/* ── Helpers ─────────────────────────────────────────── */
|
|
|
|
static float randf(void) {
|
|
return (float)rand() / (float)RAND_MAX;
|
|
}
|
|
|
|
static float randf_range(float lo, float hi) {
|
|
return lo + randf() * (hi - lo);
|
|
}
|
|
|
|
static uint8_t clamp_u8(int v) {
|
|
if (v < 0) return 0;
|
|
if (v > 255) return 255;
|
|
return (uint8_t)v;
|
|
}
|
|
|
|
/* ── Core API ────────────────────────────────────────── */
|
|
|
|
void particle_init(void) {
|
|
for (int i = 0; i < MAX_PARTICLES; i++) {
|
|
s_particles[i].active = false;
|
|
}
|
|
}
|
|
|
|
static Particle *alloc_particle(void) {
|
|
/* Find a free slot */
|
|
for (int i = 0; i < MAX_PARTICLES; i++) {
|
|
if (!s_particles[i].active) return &s_particles[i];
|
|
}
|
|
/* Steal the oldest (lowest remaining life) */
|
|
float min_life = 1e9f;
|
|
int min_idx = 0;
|
|
for (int i = 0; i < MAX_PARTICLES; i++) {
|
|
if (s_particles[i].life < min_life) {
|
|
min_life = s_particles[i].life;
|
|
min_idx = i;
|
|
}
|
|
}
|
|
return &s_particles[min_idx];
|
|
}
|
|
|
|
void particle_emit(const ParticleBurst *b) {
|
|
for (int i = 0; i < b->count; i++) {
|
|
Particle *p = alloc_particle();
|
|
|
|
/* Random angle within spread cone */
|
|
float angle = b->direction + randf_range(-b->spread, b->spread);
|
|
float speed = randf_range(b->speed_min, b->speed_max);
|
|
|
|
p->pos = b->origin;
|
|
p->vel = vec2(cosf(angle) * speed, sinf(angle) * speed);
|
|
p->life = randf_range(b->life_min, b->life_max);
|
|
p->max_life = p->life;
|
|
p->size = randf_range(b->size_min, b->size_max);
|
|
p->drag = b->drag;
|
|
p->gravity_scale = b->gravity_scale;
|
|
p->active = true;
|
|
|
|
/* Color with optional variation */
|
|
p->color = b->color;
|
|
if (b->color_vary) {
|
|
int vary = 30;
|
|
p->color.r = clamp_u8(b->color.r + (int)randf_range(-vary, vary));
|
|
p->color.g = clamp_u8(b->color.g + (int)randf_range(-vary, vary));
|
|
p->color.b = clamp_u8(b->color.b + (int)randf_range(-vary, vary));
|
|
}
|
|
}
|
|
}
|
|
|
|
void particle_update(float dt) {
|
|
float gravity = physics_get_gravity();
|
|
float wind = physics_get_wind();
|
|
|
|
for (int i = 0; i < MAX_PARTICLES; i++) {
|
|
Particle *p = &s_particles[i];
|
|
if (!p->active) continue;
|
|
|
|
p->life -= dt;
|
|
if (p->life <= 0) {
|
|
p->active = false;
|
|
continue;
|
|
}
|
|
|
|
/* Apply gravity */
|
|
p->vel.y += gravity * p->gravity_scale * dt;
|
|
|
|
/* Apply wind (reuse gravity_scale as environmental-force scale) */
|
|
if (wind != 0.0f && p->gravity_scale > 0.0f) {
|
|
p->vel.x += wind * p->gravity_scale * dt;
|
|
}
|
|
|
|
/* Apply drag */
|
|
if (p->drag > 0) {
|
|
float factor = 1.0f - p->drag * dt;
|
|
if (factor < 0) factor = 0;
|
|
p->vel.x *= factor;
|
|
p->vel.y *= factor;
|
|
}
|
|
|
|
/* Move */
|
|
p->pos.x += p->vel.x * dt;
|
|
p->pos.y += p->vel.y * dt;
|
|
}
|
|
}
|
|
|
|
void particle_render(const Camera *cam) {
|
|
for (int i = 0; i < MAX_PARTICLES; i++) {
|
|
Particle *p = &s_particles[i];
|
|
if (!p->active) continue;
|
|
|
|
/* Alpha fade based on remaining life */
|
|
float t = p->life / p->max_life;
|
|
SDL_Color c = p->color;
|
|
c.a = (uint8_t)(t * 255.0f);
|
|
|
|
/* Shrink slightly as particle ages */
|
|
float sz = p->size * (0.3f + 0.7f * t);
|
|
if (sz < 0.5f) sz = 0.5f;
|
|
|
|
renderer_draw_rect(p->pos, vec2(sz, sz), c, LAYER_PARTICLES, cam);
|
|
}
|
|
}
|
|
|
|
void particle_clear(void) {
|
|
for (int i = 0; i < MAX_PARTICLES; i++) {
|
|
s_particles[i].active = false;
|
|
}
|
|
}
|
|
|
|
/* ── Presets ─────────────────────────────────────────── */
|
|
|
|
void particle_emit_death_puff(Vec2 pos, SDL_Color color) {
|
|
ParticleBurst b = {
|
|
.origin = pos,
|
|
.count = 12,
|
|
.speed_min = 30.0f,
|
|
.speed_max = 100.0f,
|
|
.life_min = 0.2f,
|
|
.life_max = 0.5f,
|
|
.size_min = 1.5f,
|
|
.size_max = 3.5f,
|
|
.spread = (float)M_PI, /* full circle */
|
|
.direction = 0,
|
|
.drag = 3.0f,
|
|
.gravity_scale = 0.3f,
|
|
.color = color,
|
|
.color_vary = true,
|
|
};
|
|
particle_emit(&b);
|
|
}
|
|
|
|
void particle_emit_landing_dust(Vec2 pos) {
|
|
ParticleBurst b = {
|
|
.origin = pos,
|
|
.count = 6,
|
|
.speed_min = 20.0f,
|
|
.speed_max = 60.0f,
|
|
.life_min = 0.15f,
|
|
.life_max = 0.35f,
|
|
.size_min = 1.0f,
|
|
.size_max = 2.5f,
|
|
.spread = 0.6f, /* ~35 degrees */
|
|
.direction = -(float)M_PI * 0.5f, /* upward */
|
|
.drag = 4.0f,
|
|
.gravity_scale = 0.0f,
|
|
.color = {180, 170, 150, 255}, /* dusty tan */
|
|
.color_vary = true,
|
|
};
|
|
particle_emit(&b);
|
|
}
|
|
|
|
void particle_emit_spark(Vec2 pos, SDL_Color color) {
|
|
ParticleBurst b = {
|
|
.origin = pos,
|
|
.count = 5,
|
|
.speed_min = 40.0f,
|
|
.speed_max = 120.0f,
|
|
.life_min = 0.1f,
|
|
.life_max = 0.25f,
|
|
.size_min = 1.0f,
|
|
.size_max = 2.0f,
|
|
.spread = (float)M_PI,
|
|
.direction = 0,
|
|
.drag = 2.0f,
|
|
.gravity_scale = 0.5f,
|
|
.color = color,
|
|
.color_vary = true,
|
|
};
|
|
particle_emit(&b);
|
|
}
|
|
|
|
void particle_emit_jetpack_burst(Vec2 pos, Vec2 dash_dir) {
|
|
/* Exhaust fires opposite to dash direction */
|
|
float exhaust_angle = atan2f(-dash_dir.y, -dash_dir.x);
|
|
|
|
/* Big initial burst — hot core (white/yellow) */
|
|
ParticleBurst core = {
|
|
.origin = pos,
|
|
.count = 18,
|
|
.speed_min = 80.0f,
|
|
.speed_max = 200.0f,
|
|
.life_min = 0.15f,
|
|
.life_max = 0.35f,
|
|
.size_min = 2.0f,
|
|
.size_max = 4.0f,
|
|
.spread = 0.5f, /* ~30 degree cone */
|
|
.direction = exhaust_angle,
|
|
.drag = 3.0f,
|
|
.gravity_scale = 0.1f,
|
|
.color = {255, 220, 120, 255}, /* bright yellow */
|
|
.color_vary = true,
|
|
};
|
|
particle_emit(&core);
|
|
|
|
/* Outer plume — orange/red, wider spread */
|
|
ParticleBurst plume = {
|
|
.origin = pos,
|
|
.count = 14,
|
|
.speed_min = 50.0f,
|
|
.speed_max = 150.0f,
|
|
.life_min = 0.2f,
|
|
.life_max = 0.45f,
|
|
.size_min = 1.5f,
|
|
.size_max = 3.5f,
|
|
.spread = 0.8f, /* wider cone */
|
|
.direction = exhaust_angle,
|
|
.drag = 2.5f,
|
|
.gravity_scale = 0.2f,
|
|
.color = {255, 120, 40, 255}, /* orange */
|
|
.color_vary = true,
|
|
};
|
|
particle_emit(&plume);
|
|
|
|
/* Smoke tail — grey, slow, lingers */
|
|
ParticleBurst smoke = {
|
|
.origin = pos,
|
|
.count = 8,
|
|
.speed_min = 20.0f,
|
|
.speed_max = 60.0f,
|
|
.life_min = 0.3f,
|
|
.life_max = 0.6f,
|
|
.size_min = 2.0f,
|
|
.size_max = 4.5f,
|
|
.spread = 1.0f,
|
|
.direction = exhaust_angle,
|
|
.drag = 4.0f,
|
|
.gravity_scale = -0.1f, /* slight float-up */
|
|
.color = {160, 150, 140, 200}, /* smoke grey */
|
|
.color_vary = true,
|
|
};
|
|
particle_emit(&smoke);
|
|
}
|
|
|
|
void particle_emit_jetpack_trail(Vec2 pos, Vec2 dash_dir) {
|
|
/* Continuous trail — fewer particles per frame, same exhaust direction */
|
|
float exhaust_angle = atan2f(-dash_dir.y, -dash_dir.x);
|
|
|
|
/* Hot sparks */
|
|
ParticleBurst sparks = {
|
|
.origin = pos,
|
|
.count = 3,
|
|
.speed_min = 60.0f,
|
|
.speed_max = 160.0f,
|
|
.life_min = 0.1f,
|
|
.life_max = 0.25f,
|
|
.size_min = 1.5f,
|
|
.size_max = 3.0f,
|
|
.spread = 0.4f,
|
|
.direction = exhaust_angle,
|
|
.drag = 3.0f,
|
|
.gravity_scale = 0.1f,
|
|
.color = {255, 200, 80, 255}, /* yellow-orange */
|
|
.color_vary = true,
|
|
};
|
|
particle_emit(&sparks);
|
|
|
|
/* Smoke wisps */
|
|
ParticleBurst smoke = {
|
|
.origin = pos,
|
|
.count = 2,
|
|
.speed_min = 15.0f,
|
|
.speed_max = 40.0f,
|
|
.life_min = 0.2f,
|
|
.life_max = 0.4f,
|
|
.size_min = 1.5f,
|
|
.size_max = 3.5f,
|
|
.spread = 0.7f,
|
|
.direction = exhaust_angle,
|
|
.drag = 3.5f,
|
|
.gravity_scale = -0.1f,
|
|
.color = {140, 130, 120, 180},
|
|
.color_vary = true,
|
|
};
|
|
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);
|
|
|
|
/* Perpendicular angle for the up/down flare lines */
|
|
float perp = angle + (float)M_PI * 0.5f;
|
|
|
|
/* Forward beam flash — bright cyan, fast, tight cone */
|
|
ParticleBurst beam = {
|
|
.origin = pos,
|
|
.count = 5,
|
|
.speed_min = 120.0f,
|
|
.speed_max = 250.0f,
|
|
.life_min = 0.03f,
|
|
.life_max = 0.07f,
|
|
.size_min = 1.5f,
|
|
.size_max = 2.5f,
|
|
.spread = 0.15f, /* very tight forward */
|
|
.direction = angle,
|
|
.drag = 6.0f,
|
|
.gravity_scale = 0.0f,
|
|
.color = {100, 220, 255, 255}, /* cyan */
|
|
.color_vary = true,
|
|
};
|
|
particle_emit(&beam);
|
|
|
|
/* Vertical flare — perpendicular lines (up) */
|
|
ParticleBurst flare_up = {
|
|
.origin = pos,
|
|
.count = 3,
|
|
.speed_min = 60.0f,
|
|
.speed_max = 140.0f,
|
|
.life_min = 0.02f,
|
|
.life_max = 0.06f,
|
|
.size_min = 1.0f,
|
|
.size_max = 2.0f,
|
|
.spread = 0.12f,
|
|
.direction = perp,
|
|
.drag = 8.0f,
|
|
.gravity_scale = 0.0f,
|
|
.color = {160, 240, 255, 255}, /* light cyan */
|
|
.color_vary = true,
|
|
};
|
|
particle_emit(&flare_up);
|
|
|
|
/* Vertical flare — perpendicular lines (down) */
|
|
ParticleBurst flare_down = flare_up;
|
|
flare_down.direction = perp + (float)M_PI; /* opposite perpendicular */
|
|
particle_emit(&flare_down);
|
|
|
|
/* Bright white core — instant pop at barrel */
|
|
ParticleBurst core = {
|
|
.origin = pos,
|
|
.count = 2,
|
|
.speed_min = 5.0f,
|
|
.speed_max = 20.0f,
|
|
.life_min = 0.03f,
|
|
.life_max = 0.06f,
|
|
.size_min = 2.5f,
|
|
.size_max = 4.0f,
|
|
.spread = (float)M_PI, /* all directions, stays near barrel */
|
|
.direction = 0,
|
|
.drag = 10.0f,
|
|
.gravity_scale = 0.0f,
|
|
.color = {200, 255, 255, 255}, /* white-cyan glow */
|
|
.color_vary = false,
|
|
};
|
|
particle_emit(&core);
|
|
}
|
|
|
|
void particle_emit_wall_slide_dust(Vec2 pos, int wall_dir) {
|
|
/* Small dust puffs pushed away from the wall */
|
|
float away_angle = (wall_dir < 0) ? 0.0f : (float)M_PI; /* away from wall */
|
|
|
|
ParticleBurst dust = {
|
|
.origin = pos,
|
|
.count = 2,
|
|
.speed_min = 15.0f,
|
|
.speed_max = 40.0f,
|
|
.life_min = 0.1f,
|
|
.life_max = 0.25f,
|
|
.size_min = 1.0f,
|
|
.size_max = 2.0f,
|
|
.spread = 0.8f,
|
|
.direction = away_angle,
|
|
.drag = 4.0f,
|
|
.gravity_scale = -0.1f, /* slight float-up */
|
|
.color = {180, 170, 150, 200}, /* dusty tan */
|
|
.color_vary = true,
|
|
};
|
|
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.
|
|
* When wind is strong, particles spawn along the upwind viewport edge
|
|
* and drift inward. When wind is calm, particles spawn across the
|
|
* full viewport to avoid clustering on one side. */
|
|
float wind = physics_get_wind();
|
|
float margin = 32.0f;
|
|
float abs_wind = (wind >= 0.0f) ? wind : -wind;
|
|
int has_wind = abs_wind > 5.0f; /* threshold for edge-spawning */
|
|
|
|
if (has_wind) {
|
|
float dir = (wind >= 0.0f) ? 1.0f : -1.0f;
|
|
|
|
/* 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 */
|
|
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);
|
|
}
|
|
} else {
|
|
/* Calm wind — spawn across the full viewport to distribute evenly */
|
|
|
|
/* Far dust motes — large, slow, translucent (1/frame) */
|
|
spawn_dust_mote(
|
|
vec2(cam_pos.x + randf() * vp.x, cam_pos.y + randf() * vp.y),
|
|
vec2(randf_range(-10.0f, 10.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(cam_pos.x + randf() * vp.x, cam_pos.y + randf() * vp.y),
|
|
vec2(randf_range(-15.0f, 15.0f), randf_range(-10.0f, 10.0f)),
|
|
2.5f, 5.0f, 0.8f, 1.5f, 0.2f, 0.12f,
|
|
200, 160, 120, 20);
|
|
|
|
/* Extra interior mote for density parity with windy path */
|
|
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);
|
|
}
|
|
}
|
|
}
|