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

+38 -10
View File
@@ -379,6 +379,7 @@ pub struct Fx {
pub h: f32, // height of the start point (3D)
pub h2: f32, // height of the end point (projectiles)
pub size: f32, // 1 = normal; attacks of higher levels look bigger
pub style: u8, // projectiles: arrow, cannonball, fireball, ... (see particles.rs)
}
#[derive(Serialize, Deserialize)]
@@ -446,6 +447,12 @@ pub struct Game {
#[serde(skip)]
pub springs: usize, // claimed mana springs (recounted)
#[serde(skip)]
pub parts: Vec<crate::particles::Part>, // visual particles (never saved)
#[serde(skip)]
pub stuck: Vec<crate::particles::Part>, // arrows stuck in walls and floors, scorch marks
#[serde(skip, default = "crate::particles::vrng")]
pub vrng: Rng, // random numbers for the particles only
#[serde(skip)]
pub demo: Option<crate::demo::Demo>, // the endless bridge battle behind the title menu
#[serde(default)]
pub dwarf_caves: Vec<crate::dwarves::DwarfCave>,
@@ -606,6 +613,9 @@ impl Game {
springs: 0,
freed: 0,
demo: None,
parts: Vec::new(),
stuck: Vec::new(),
vrng: crate::particles::vrng(),
dwarf_caves: Vec::new(),
dwarf_t: 30.0,
rune_free: 0,
@@ -922,8 +932,9 @@ impl Game {
pub fn fx(&mut self, kind: u8, x: f32, y: f32, x2: f32, y2: f32, max: f32, col: u32) {
let h = if kind == 0 { 0.45 } else { 0.4 };
self.fx_particles(kind, x, y, h, col);
if self.fx.len() < 400 {
self.fx.push(Fx { kind, x, y, x2, y2, t: 0.0, max, col, h, h2: 0.45, size: 1.0 });
self.fx.push(Fx { kind, x, y, x2, y2, t: 0.0, max, col, h, h2: 0.45, size: 1.0, style: 0 });
}
}
@@ -931,7 +942,7 @@ impl Game {
#[allow(clippy::too_many_arguments)]
pub fn shot(&mut self, x: f32, y: f32, h: f32, x2: f32, y2: f32, h2: f32, max: f32, col: u32) {
if self.fx.len() < 400 {
self.fx.push(Fx { kind: 0, x, y, x2, y2, t: 0.0, max, col, h, h2, size: 1.0 });
self.fx.push(Fx { kind: 0, x, y, x2, y2, t: 0.0, max, col, h, h2, size: 1.0, style: 0 });
}
}
@@ -944,8 +955,9 @@ impl Game {
// effect at a given height
pub fn fx_at(&mut self, kind: u8, x: f32, y: f32, h: f32, max: f32, col: u32) {
self.fx_particles(kind, x, y, h, col);
if self.fx.len() < 400 {
self.fx.push(Fx { kind, x, y, x2: 0.0, y2: 0.0, t: 0.0, max, col, h, h2: h, size: 1.0 });
self.fx.push(Fx { kind, x, y, x2: 0.0, y2: 0.0, t: 0.0, max, col, h, h2: h, size: 1.0, style: 0 });
}
}
@@ -961,6 +973,7 @@ impl Game {
self.step(dt);
}
self.sync_treasury();
self.update_particles(dt);
for f in self.fx.iter_mut() {
f.t += dt;
}
@@ -1875,6 +1888,15 @@ impl Game {
let h = if flies(e.kind) { 0.8 } else { 0.45 };
self.shot(e.x, e.y, h, tx, ty, 0.45, 0.25, col);
self.grow_last_fx(lv);
use crate::particles::*;
self.style_last_fx(match e.kind {
EK::Archer => ST_ARROW,
EK::Mage => ST_SHADOW,
EK::Priest => ST_HOLY,
EK::Priestess | EK::Wyrm => ST_FIRE,
EK::Owl => ST_ARCANE,
_ => ST_PLAIN,
});
self.sfx(Sfx::Shoot, e.x, e.y);
} else {
self.fx(2, tx, ty, 0.0, 0.0, 0.25, if e.level >= 9 { 0xff60e0ff } else { 0xffffffff });
@@ -1904,8 +1926,6 @@ impl Game {
}
if e.kind == EK::Priestess || e.kind == EK::Wyrm {
// fireball / fire breath: half damage to enemies around the target
self.fx(3, tx, ty, 0.0, 0.0, 0.5, 0xff3080ff);
self.grow_last_fx(lv);
let me = e.side();
let splash: Vec<usize> = self
.ents
@@ -4050,7 +4070,12 @@ impl Game {
TowerKind::Rune => 0xff40c0ff,
};
self.shot(mx, my, mh, ex, ey, eh, 0.3, col);
self.fx_at(2, ex, ey, eh, 0.3, 0xffffffff);
self.style_last_fx(match t.kind {
TowerKind::Arrow => crate::particles::ST_ARROW,
TowerKind::Cannon => crate::particles::ST_CANNON,
TowerKind::Magic => crate::particles::ST_MAGIC,
TowerKind::Rune => crate::particles::ST_RUNE,
});
self.sfx(match t.kind {
TowerKind::Arrow => Sfx::Shoot,
TowerKind::Cannon => Sfx::Cannon,
@@ -4123,8 +4148,9 @@ impl Game {
TowerKind::Arrow => {
// the arrow pierces up to two more heroes behind the target
let dir = norm(ex - px, ey - py);
let end = (px + dir.0 * t.range(), py + dir.1 * t.range());
self.shot(mx, my, mh, end.0, end.1, 0.45, 0.3, 0xffd0e0f0);
let (endx, endy, wall) = self.arrow_end(px, py, dir.0, dir.1, t.range());
self.shot(mx, my, mh, endx, endy, 0.45, 0.3, 0xffd0e0f0);
self.style_last_fx(if wall { crate::particles::ST_ARROW_WALL } else { crate::particles::ST_ARROW });
self.sfx(Sfx::Shoot, px, py);
let mut hits = vec![(j, dmg)];
for (o, e) in self.ents.iter().enumerate() {
@@ -4147,8 +4173,7 @@ impl Game {
TowerKind::Cannon => {
// splash damage and knockback around the impact
self.shot(mx, my, mh, ex, ey, 0.4, 0.35, 0xff303030);
self.fx(3, ex, ey, 0.0, 0.0, 0.6, 0xff4070a0);
self.fx(2, ex, ey, 0.0, 0.0, 0.3, 0xff60c0ff);
self.style_last_fx(crate::particles::ST_CANNON);
self.sfx(Sfx::Cannon, px, py);
let mut hits = Vec::new();
for (o, e) in self.ents.iter().enumerate() {
@@ -4176,6 +4201,7 @@ impl Game {
TowerKind::Magic => {
// sure hit, slows, jumps to a second hero
self.shot(mx, my, mh, ex, ey, 0.45, 0.2, 0xffff60d0);
self.style_last_fx(crate::particles::ST_MAGIC);
self.sfx(Sfx::Magic, px, py);
self.ents[j].slow = 2.0;
let chain = self
@@ -4202,6 +4228,7 @@ impl Game {
TowerKind::Rune => {
// a rune bolt that jumps on to up to three more enemies nearby
self.shot(mx, my, mh, ex, ey, 0.45, 0.25, 0xff40c0ff);
self.style_last_fx(crate::particles::ST_RUNE);
self.fx(2, ex, ey, 0.0, 0.0, 0.35, 0xffffc040);
self.sfx(Sfx::Magic, px, py);
let mut hit = vec![j];
@@ -4255,6 +4282,7 @@ impl Game {
let dmg = e.atk() * (0.85 + self.rng.f() * 0.3);
if s.range > 1.5 {
self.shot(e.x, e.y, 0.45, tx, ty, hit_h, 0.25, 0xff70b0d0);
self.style_last_fx(if e.kind == EK::Archer { crate::particles::ST_ARROW } else { crate::particles::ST_PLAIN });
} else {
self.fx_at(2, tx, ty, hit_h, 0.25, 0xffffffff);
}