Particle effects: falling trails behind projectiles, exploding fireballs and cannonballs, arrows stuck in walls and floors

- projectiles get styles: arrows (shaft and fletching), cannonballs and fireballs fly in an arc, shadow, holy, arcane, rune and magic bolts with their own glow
- trails of embers, sparks, motes or smoke that drift down to the floor
- fireballs explode on the ground (flash, shock wave, embers, smoke, scorch mark), cannonballs burst into stones, dirt, sparks and smoke
- arrows stay stuck in walls (or glance off into the floor) for five minutes, at most 400; they drop out when the wall is dug away
- spells: lightning sprays sparks and scorches the floor, healing lifts green motes, level-ups and summoning rain sparkles; digging throws earth, gold pops coins
- particles are never saved and use their own random numbers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-03 20:29:32 +01:00
1 parent 33b9879b4c
commit 80982b71f7
4 files changed
+487 -10

No files matched your search

+95
View File
@@ -1026,6 +1026,7 @@ impl R3d {
draw_landmarks_solid(g, t);
draw_towers_solid(g);
crate::props::draw_props_solid(g, cam, t);
draw_parts_solid(g);
// ---- unlit glow / overlays
gl_use_material(&self.glow);
@@ -1062,6 +1063,7 @@ impl R3d {
for f in &g.fx {
draw_fx(f, t);
}
draw_parts_glow(g);
gl_use_default_material();
crate::ui::begin_ui();
if let (true, Some((rt, tw, th))) = (div > 1, self.target.as_ref()) {
@@ -2070,12 +2072,105 @@ fn draw_flag(f: usize, t: f32) {
draw_mesh(&Mesh { vertices: v, indices: vec![0, 1, 2], texture: None });
}
// projectiles with a look of their own (styles in particles.rs)
fn draw_projectile(f: &Fx, p: f32, t: f32) {
use crate::particles::*;
let at = |q: f32| {
let (x, y, h) = fx_pos(f, q);
vec3(x, h, y)
};
let head = at(p);
let c = rgba(f.col);
let sz = f.size;
match f.style {
ST_ARROW | ST_ARROW_WALL => {
let back = at((p - 0.08).max(0.0));
let dir = (head - back).normalize_or_zero();
line3(head - dir * 0.3, head, 0.03, Color::from_rgba(120, 84, 48, 255));
draw_cube(head, Vec3::splat(0.035), None, Color::from_rgba(200, 200, 210, 255));
draw_cube(head - dir * 0.28, Vec3::splat(0.05), None, Color::from_rgba(230, 230, 230, 255));
}
ST_CANNON => {
draw_sphere(head, 0.1 * sz, None, Color::from_rgba(24, 24, 28, 255));
draw_sphere(head, 0.14 * sz, None, Color::new(1.0, 0.6, 0.2, 0.15));
}
ST_FIRE => {
let flick = 1.0 + (t * 30.0 + f.x).sin() * 0.12;
draw_sphere(head, 0.08 * sz * flick, None, Color::new(1.0, 0.95, 0.6, 1.0));
draw_sphere(head, 0.14 * sz * flick, None, Color::new(1.0, 0.55, 0.12, 0.8));
draw_sphere(head, 0.24 * sz * flick, None, Color::new(1.0, 0.3, 0.05, 0.25));
}
_ => {
let tail = at((p - 0.3).max(0.0));
line3(tail, head, 0.06 * sz, Color::new(c.r, c.g, c.b, 0.7));
draw_sphere(head, 0.08 * sz, None, c);
draw_sphere(head, 0.17 * sz, None, Color::new(c.r, c.g, c.b, 0.25));
}
}
}
// particles that glow (embers, sparks, smoke, flashes, shock waves)
fn draw_parts_glow(g: &Game) {
use crate::particles::*;
for p in &g.parts {
let c = rgba(p.col);
let life = (p.t / p.max).clamp(0.0, 1.0);
let at = vec3(p.x, p.h, p.y);
match p.kind {
EMBER | SPARK | MOTE => {
let s = p.size * (1.0 - life * 0.6);
draw_cube(at, Vec3::splat(s), None, Color::new(c.r, c.g, c.b, 1.0 - life * life));
}
SMOKE => draw_sphere(at, p.size, None, Color::new(c.r, c.g, c.b, 0.35 * (1.0 - life))),
FLASH => {
let r = p.size * (0.3 + life * 0.9);
draw_sphere(at, r * 0.6, None, Color::new(1.0, 0.95, 0.75, 1.0 - life));
draw_sphere(at, r, None, Color::new(c.r, c.g, c.b, 0.6 * (1.0 - life)));
}
RING => ring(at, p.size * (0.2 + life), Color::new(c.r, c.g, c.b, 0.8 * (1.0 - life))),
_ => {}
}
}
// scorch marks on the floor
for p in g.stuck.iter().filter(|p| p.kind == SCORCH) {
let fade = 1.0 - (p.t / p.max).clamp(0.0, 1.0);
disc(vec3(p.x, 0.015, p.y), p.size, Color::new(0.03, 0.02, 0.02, 0.55 * fade), 14);
}
}
// solid particles: debris, coins and the arrows stuck in walls and floors
fn draw_parts_solid(g: &Game) {
use crate::particles::*;
for p in &g.parts {
let c = rgba(p.col);
let at = vec3(p.x, p.h, p.y);
match p.kind {
DEBRIS | COIN => {
let life = (p.t / p.max).clamp(0.0, 1.0);
let s = p.size * if life > 0.75 { (1.0 - life) * 4.0 } else { 1.0 };
draw_cube(at, vec3(s, if p.kind == COIN { s * 0.4 } else { s }, s), None, c);
}
_ => {}
}
}
for p in g.stuck.iter().filter(|p| p.kind == ARROW) {
let left = p.max - p.t;
let k = if left < 10.0 { left / 10.0 } else { 1.0 };
let tip = vec3(p.x, p.h, p.y);
let dir = vec3(p.vx, p.vh, p.vy).normalize_or_zero();
let tail = tip - dir * (0.32 * k);
line3(tail, tip + dir * 0.03, 0.03, Color::from_rgba(116, 80, 46, 255));
draw_cube(tail, vec3(0.05, 0.05, 0.05) * k, None, Color::from_rgba(226, 226, 226, 255));
}
}
fn draw_fx(f: &Fx, t: f32) {
let p = (f.t / f.max).clamp(0.0, 1.0);
let c = rgba(f.col);
let a = |alpha: f32| Color::new(c.r, c.g, c.b, alpha);
let at = vec3(f.x, f.h, f.y);
match f.kind {
0 if f.style != crate::particles::ST_PLAIN => draw_projectile(f, p, t),
0 => {
let from = vec3(f.x, f.h, f.y);
let to = vec3(f.x2, f.h2, f.y2);