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:
1 parent
33b9879b4c
commit
80982b71f7
4 files changed
+487
-10
No files matched your search
+38
-10
@@ -379,6 +379,7 @@ pub struct Fx {
|
|||||||
pub h: f32, // height of the start point (3D)
|
pub h: f32, // height of the start point (3D)
|
||||||
pub h2: f32, // height of the end point (projectiles)
|
pub h2: f32, // height of the end point (projectiles)
|
||||||
pub size: f32, // 1 = normal; attacks of higher levels look bigger
|
pub size: f32, // 1 = normal; attacks of higher levels look bigger
|
||||||
|
pub style: u8, // projectiles: arrow, cannonball, fireball, ... (see particles.rs)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
@@ -446,6 +447,12 @@ pub struct Game {
|
|||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub springs: usize, // claimed mana springs (recounted)
|
pub springs: usize, // claimed mana springs (recounted)
|
||||||
#[serde(skip)]
|
#[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
|
pub demo: Option<crate::demo::Demo>, // the endless bridge battle behind the title menu
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub dwarf_caves: Vec<crate::dwarves::DwarfCave>,
|
pub dwarf_caves: Vec<crate::dwarves::DwarfCave>,
|
||||||
@@ -606,6 +613,9 @@ impl Game {
|
|||||||
springs: 0,
|
springs: 0,
|
||||||
freed: 0,
|
freed: 0,
|
||||||
demo: None,
|
demo: None,
|
||||||
|
parts: Vec::new(),
|
||||||
|
stuck: Vec::new(),
|
||||||
|
vrng: crate::particles::vrng(),
|
||||||
dwarf_caves: Vec::new(),
|
dwarf_caves: Vec::new(),
|
||||||
dwarf_t: 30.0,
|
dwarf_t: 30.0,
|
||||||
rune_free: 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) {
|
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 };
|
let h = if kind == 0 { 0.45 } else { 0.4 };
|
||||||
|
self.fx_particles(kind, x, y, h, col);
|
||||||
if self.fx.len() < 400 {
|
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)]
|
#[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) {
|
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 {
|
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
|
// effect at a given height
|
||||||
pub fn fx_at(&mut self, kind: u8, x: f32, y: f32, h: f32, max: f32, col: u32) {
|
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 {
|
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.step(dt);
|
||||||
}
|
}
|
||||||
self.sync_treasury();
|
self.sync_treasury();
|
||||||
|
self.update_particles(dt);
|
||||||
for f in self.fx.iter_mut() {
|
for f in self.fx.iter_mut() {
|
||||||
f.t += dt;
|
f.t += dt;
|
||||||
}
|
}
|
||||||
@@ -1875,6 +1888,15 @@ impl Game {
|
|||||||
let h = if flies(e.kind) { 0.8 } else { 0.45 };
|
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.shot(e.x, e.y, h, tx, ty, 0.45, 0.25, col);
|
||||||
self.grow_last_fx(lv);
|
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);
|
self.sfx(Sfx::Shoot, e.x, e.y);
|
||||||
} else {
|
} else {
|
||||||
self.fx(2, tx, ty, 0.0, 0.0, 0.25, if e.level >= 9 { 0xff60e0ff } else { 0xffffffff });
|
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 {
|
if e.kind == EK::Priestess || e.kind == EK::Wyrm {
|
||||||
// fireball / fire breath: half damage to enemies around the target
|
// 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 me = e.side();
|
||||||
let splash: Vec<usize> = self
|
let splash: Vec<usize> = self
|
||||||
.ents
|
.ents
|
||||||
@@ -4050,7 +4070,12 @@ impl Game {
|
|||||||
TowerKind::Rune => 0xff40c0ff,
|
TowerKind::Rune => 0xff40c0ff,
|
||||||
};
|
};
|
||||||
self.shot(mx, my, mh, ex, ey, eh, 0.3, col);
|
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 {
|
self.sfx(match t.kind {
|
||||||
TowerKind::Arrow => Sfx::Shoot,
|
TowerKind::Arrow => Sfx::Shoot,
|
||||||
TowerKind::Cannon => Sfx::Cannon,
|
TowerKind::Cannon => Sfx::Cannon,
|
||||||
@@ -4123,8 +4148,9 @@ impl Game {
|
|||||||
TowerKind::Arrow => {
|
TowerKind::Arrow => {
|
||||||
// the arrow pierces up to two more heroes behind the target
|
// the arrow pierces up to two more heroes behind the target
|
||||||
let dir = norm(ex - px, ey - py);
|
let dir = norm(ex - px, ey - py);
|
||||||
let end = (px + dir.0 * t.range(), py + dir.1 * t.range());
|
let (endx, endy, wall) = self.arrow_end(px, py, dir.0, dir.1, t.range());
|
||||||
self.shot(mx, my, mh, end.0, end.1, 0.45, 0.3, 0xffd0e0f0);
|
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);
|
self.sfx(Sfx::Shoot, px, py);
|
||||||
let mut hits = vec![(j, dmg)];
|
let mut hits = vec![(j, dmg)];
|
||||||
for (o, e) in self.ents.iter().enumerate() {
|
for (o, e) in self.ents.iter().enumerate() {
|
||||||
@@ -4147,8 +4173,7 @@ impl Game {
|
|||||||
TowerKind::Cannon => {
|
TowerKind::Cannon => {
|
||||||
// splash damage and knockback around the impact
|
// splash damage and knockback around the impact
|
||||||
self.shot(mx, my, mh, ex, ey, 0.4, 0.35, 0xff303030);
|
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.style_last_fx(crate::particles::ST_CANNON);
|
||||||
self.fx(2, ex, ey, 0.0, 0.0, 0.3, 0xff60c0ff);
|
|
||||||
self.sfx(Sfx::Cannon, px, py);
|
self.sfx(Sfx::Cannon, px, py);
|
||||||
let mut hits = Vec::new();
|
let mut hits = Vec::new();
|
||||||
for (o, e) in self.ents.iter().enumerate() {
|
for (o, e) in self.ents.iter().enumerate() {
|
||||||
@@ -4176,6 +4201,7 @@ impl Game {
|
|||||||
TowerKind::Magic => {
|
TowerKind::Magic => {
|
||||||
// sure hit, slows, jumps to a second hero
|
// sure hit, slows, jumps to a second hero
|
||||||
self.shot(mx, my, mh, ex, ey, 0.45, 0.2, 0xffff60d0);
|
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.sfx(Sfx::Magic, px, py);
|
||||||
self.ents[j].slow = 2.0;
|
self.ents[j].slow = 2.0;
|
||||||
let chain = self
|
let chain = self
|
||||||
@@ -4202,6 +4228,7 @@ impl Game {
|
|||||||
TowerKind::Rune => {
|
TowerKind::Rune => {
|
||||||
// a rune bolt that jumps on to up to three more enemies nearby
|
// 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.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.fx(2, ex, ey, 0.0, 0.0, 0.35, 0xffffc040);
|
||||||
self.sfx(Sfx::Magic, px, py);
|
self.sfx(Sfx::Magic, px, py);
|
||||||
let mut hit = vec![j];
|
let mut hit = vec![j];
|
||||||
@@ -4255,6 +4282,7 @@ impl Game {
|
|||||||
let dmg = e.atk() * (0.85 + self.rng.f() * 0.3);
|
let dmg = e.atk() * (0.85 + self.rng.f() * 0.3);
|
||||||
if s.range > 1.5 {
|
if s.range > 1.5 {
|
||||||
self.shot(e.x, e.y, 0.45, tx, ty, hit_h, 0.25, 0xff70b0d0);
|
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 {
|
} else {
|
||||||
self.fx_at(2, tx, ty, hit_h, 0.25, 0xffffffff);
|
self.fx_at(2, tx, ty, hit_h, 0.25, 0xffffffff);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ mod i18n;
|
|||||||
mod map;
|
mod map;
|
||||||
mod menu;
|
mod menu;
|
||||||
mod platform;
|
mod platform;
|
||||||
|
mod particles;
|
||||||
mod props;
|
mod props;
|
||||||
mod rival;
|
mod rival;
|
||||||
mod render3d;
|
mod render3d;
|
||||||
|
|||||||
@@ -0,0 +1,353 @@
|
|||||||
|
// Particles: trails that fall to the ground behind projectiles, explosions of fireballs and
|
||||||
|
// cannonballs, bursts of the spells, debris and coins, scorch marks, and arrows that stay stuck
|
||||||
|
// in walls and the ground for five minutes. Purely visual: never saved, with their own random
|
||||||
|
// numbers (the game's random sequence stays untouched).
|
||||||
|
|
||||||
|
use crate::game::*;
|
||||||
|
use crate::map::*;
|
||||||
|
|
||||||
|
pub const STUCK_MAX: usize = 400; // arrows and scorch marks
|
||||||
|
pub const STUCK_LIFE: f32 = 300.0; // seconds an arrow stays
|
||||||
|
const PARTS_MAX: usize = 900;
|
||||||
|
const GRAVITY: f32 = 6.0;
|
||||||
|
|
||||||
|
// particle kinds
|
||||||
|
pub const EMBER: u8 = 0; // glowing, falls
|
||||||
|
pub const SMOKE: u8 = 1; // rises and grows
|
||||||
|
pub const SPARK: u8 = 2; // glowing, falls fast, bounces once
|
||||||
|
pub const DEBRIS: u8 = 3; // solid chunk, falls and rests a moment
|
||||||
|
pub const MOTE: u8 = 4; // glowing, floats up slowly
|
||||||
|
pub const COIN: u8 = 5; // solid gold, bounces
|
||||||
|
pub const ARROW: u8 = 6; // stuck arrow (vx, vy, vh = its direction)
|
||||||
|
pub const SCORCH: u8 = 7; // dark mark on the floor
|
||||||
|
pub const FLASH: u8 = 8; // expanding fireball
|
||||||
|
pub const RING: u8 = 9; // shock wave
|
||||||
|
|
||||||
|
// projectile styles (Fx::style)
|
||||||
|
pub const ST_PLAIN: u8 = 0;
|
||||||
|
pub const ST_ARROW: u8 = 1;
|
||||||
|
pub const ST_CANNON: u8 = 2;
|
||||||
|
pub const ST_FIRE: u8 = 3;
|
||||||
|
pub const ST_SHADOW: u8 = 4;
|
||||||
|
pub const ST_HOLY: u8 = 5;
|
||||||
|
pub const ST_ARCANE: u8 = 6;
|
||||||
|
pub const ST_RUNE: u8 = 7;
|
||||||
|
pub const ST_MAGIC: u8 = 8;
|
||||||
|
pub const ST_ARROW_WALL: u8 = 9; // an arrow that ends in a wall
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct Part {
|
||||||
|
pub kind: u8,
|
||||||
|
pub x: f32,
|
||||||
|
pub y: f32,
|
||||||
|
pub h: f32,
|
||||||
|
pub vx: f32,
|
||||||
|
pub vy: f32,
|
||||||
|
pub vh: f32,
|
||||||
|
pub t: f32,
|
||||||
|
pub max: f32,
|
||||||
|
pub col: u32,
|
||||||
|
pub size: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn vrng() -> Rng {
|
||||||
|
Rng(0x2545_f491)
|
||||||
|
}
|
||||||
|
|
||||||
|
// colour as 0xAABBGGRR (like Fx::col)
|
||||||
|
pub const fn abgr(r: u8, g: u8, b: u8) -> u32 {
|
||||||
|
0xff00_0000 | (b as u32) << 16 | (g as u32) << 8 | r as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
// position of a projectile at progress p (cannonballs and fireballs fly in an arc)
|
||||||
|
pub fn fx_pos(f: &Fx, p: f32) -> (f32, f32, f32) {
|
||||||
|
let p = p.clamp(0.0, 1.0);
|
||||||
|
let x = f.x + (f.x2 - f.x) * p;
|
||||||
|
let y = f.y + (f.y2 - f.y) * p;
|
||||||
|
let dist = ((f.x2 - f.x).powi(2) + (f.y2 - f.y).powi(2)).sqrt();
|
||||||
|
let arc = match f.style {
|
||||||
|
ST_CANNON => 0.22,
|
||||||
|
ST_FIRE => 0.12,
|
||||||
|
ST_ARROW | ST_ARROW_WALL => 0.04,
|
||||||
|
_ => 0.0,
|
||||||
|
} * dist;
|
||||||
|
let h = f.h + (f.h2 - f.h) * p + arc * (p * std::f32::consts::PI).sin();
|
||||||
|
(x, y, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Game {
|
||||||
|
fn part(&mut self, kind: u8, (x, y, h): (f32, f32, f32), (vx, vy, vh): (f32, f32, f32), max: f32, col: u32, size: f32) {
|
||||||
|
if self.parts.len() < PARTS_MAX {
|
||||||
|
self.parts.push(Part { kind, x, y, h, vx, vy, vh, t: 0.0, max, col, size });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stick(&mut self, p: Part) {
|
||||||
|
if self.stuck.len() >= STUCK_MAX {
|
||||||
|
self.stuck.remove(0);
|
||||||
|
}
|
||||||
|
self.stuck.push(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
// a random direction with the given speed (upwards bias `up`)
|
||||||
|
fn burst_vel(&mut self, speed: f32, up: f32) -> (f32, f32, f32) {
|
||||||
|
let a = self.vrng.f() * std::f32::consts::TAU;
|
||||||
|
let s = speed * (0.4 + 0.6 * self.vrng.f());
|
||||||
|
(a.cos() * s, a.sin() * s, up * (0.5 + self.vrng.f()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn burst(&mut self, kind: u8, at: (f32, f32, f32), n: usize, speed: f32, up: f32, max: f32, col: u32, size: f32) {
|
||||||
|
for _ in 0..n {
|
||||||
|
let v = self.burst_vel(speed, up);
|
||||||
|
let m = max * (0.6 + 0.6 * self.vrng.f());
|
||||||
|
let sz = size * (0.7 + 0.6 * self.vrng.f());
|
||||||
|
self.part(kind, at, v, m, col, sz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// particles that come with a new effect (spells, hits, digging, gold, ...)
|
||||||
|
pub(crate) fn fx_particles(&mut self, kind: u8, x: f32, y: f32, h: f32, col: u32) {
|
||||||
|
match kind {
|
||||||
|
// lightning: a shower of sparks, smoke and a burnt spot
|
||||||
|
1 => {
|
||||||
|
self.burst(SPARK, (x, y, 0.1), 40, 3.5, 3.5, 1.0, abgr(255, 250, 200), 0.05);
|
||||||
|
self.burst(SMOKE, (x, y, 0.2), 6, 0.4, 0.6, 2.0, abgr(90, 90, 100), 0.25);
|
||||||
|
self.part(FLASH, (x, y, 0.3), (0.0, 0.0, 0.0), 0.35, abgr(255, 255, 220), 1.0);
|
||||||
|
self.stick(Part { kind: SCORCH, x, y, h: 0.0, vx: 0.0, vy: 0.0, vh: 0.0, t: 0.0, max: 20.0, col: 0, size: 0.8 });
|
||||||
|
}
|
||||||
|
// hit: a few sparks of the effect's colour
|
||||||
|
2 => self.burst(SPARK, (x, y, h.max(0.3)), 5, 1.5, 1.5, 0.5, col, 0.035),
|
||||||
|
3 => self.burst(SMOKE, (x, y, 0.3), 4, 0.3, 0.5, 1.6, abgr(80, 80, 90), 0.2),
|
||||||
|
// healing: green motes rising in a spiral, sparkles falling
|
||||||
|
4 => {
|
||||||
|
for k in 0..12 {
|
||||||
|
let a = k as f32 * 0.52;
|
||||||
|
self.part(MOTE, (x + a.cos() * 0.3, y + a.sin() * 0.3, 0.1 + k as f32 * 0.03), (-a.sin() * 0.3, a.cos() * 0.3, 0.9), 1.3, abgr(120, 255, 140), 0.05);
|
||||||
|
}
|
||||||
|
self.burst(EMBER, (x, y, 0.9), 6, 0.6, 0.5, 1.0, abgr(200, 255, 200), 0.03);
|
||||||
|
}
|
||||||
|
// gold: coins pop up and fall
|
||||||
|
5 => self.burst(COIN, (x, y, 0.4), 5, 1.2, 2.5, 1.6, abgr(255, 210, 60), 0.06),
|
||||||
|
// digging: earth chunks
|
||||||
|
6 => self.burst(DEBRIS, (x, y, 0.5), 4, 1.2, 1.5, 1.4, abgr(110, 80, 56), 0.06),
|
||||||
|
// level up, spells, specials: sparkles in the effect's colour shoot up and rain down
|
||||||
|
7 => self.burst(EMBER, (x, y, 0.5), 18, 1.6, 3.0, 1.4, col, 0.04),
|
||||||
|
// claiming: a puff of dust
|
||||||
|
8 => self.burst(SMOKE, (x, y, 0.1), 2, 0.3, 0.2, 0.8, abgr(120, 60, 60), 0.12),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// advances projectiles (trails, impacts) and particles
|
||||||
|
pub(crate) fn update_particles(&mut self, dt: f32) {
|
||||||
|
// trails and impacts of the projectiles
|
||||||
|
let mut trails: Vec<(u8, (f32, f32, f32), f32)> = Vec::new();
|
||||||
|
let mut impacts: Vec<Fx> = Vec::new();
|
||||||
|
for f in self.fx.iter() {
|
||||||
|
if f.kind != 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let p0 = f.t / f.max;
|
||||||
|
let p1 = (f.t + dt) / f.max;
|
||||||
|
if p0 < 1.0 {
|
||||||
|
trails.push((f.style, fx_pos(f, p1.min(1.0)), f.size));
|
||||||
|
}
|
||||||
|
if p0 < 1.0 && p1 >= 1.0 {
|
||||||
|
impacts.push(*f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (style, at, size) in trails {
|
||||||
|
let (rate, kind, col, psize, life) = match style {
|
||||||
|
ST_FIRE => (70.0, EMBER, abgr(255, 150, 40), 0.045, 0.9),
|
||||||
|
ST_SHADOW => (45.0, EMBER, abgr(200, 70, 255), 0.035, 1.0),
|
||||||
|
ST_HOLY => (35.0, MOTE, abgr(255, 240, 150), 0.03, 0.8),
|
||||||
|
ST_ARCANE => (40.0, EMBER, abgr(255, 170, 90), 0.03, 0.9),
|
||||||
|
ST_RUNE => (45.0, SPARK, abgr(90, 200, 255), 0.03, 0.7),
|
||||||
|
ST_MAGIC => (45.0, EMBER, abgr(255, 90, 220), 0.035, 0.9),
|
||||||
|
ST_CANNON => (30.0, SMOKE, abgr(70, 70, 76), 0.1, 0.9),
|
||||||
|
ST_ARROW | ST_ARROW_WALL => (0.0, EMBER, 0, 0.0, 0.0),
|
||||||
|
_ => (12.0, EMBER, abgr(180, 210, 230), 0.03, 0.6),
|
||||||
|
};
|
||||||
|
let n = rate * dt * size;
|
||||||
|
let mut count = n.floor() as usize;
|
||||||
|
if self.vrng.f() < n.fract() {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
for _ in 0..count {
|
||||||
|
let v = (self.vrng.f() * 0.6 - 0.3, self.vrng.f() * 0.6 - 0.3, if kind == SMOKE { 0.3 } else { 0.2 });
|
||||||
|
let j = (self.vrng.f() * 0.06 - 0.03, self.vrng.f() * 0.06 - 0.03);
|
||||||
|
let m = life * (0.7 + 0.6 * self.vrng.f());
|
||||||
|
self.part(kind, (at.0 + j.0, at.1 + j.1, at.2), v, m, col, psize * size.sqrt());
|
||||||
|
}
|
||||||
|
if style == ST_FIRE && self.vrng.f() < 12.0 * dt {
|
||||||
|
self.part(SMOKE, at, (0.0, 0.0, 0.4), 1.2, abgr(60, 50, 50), 0.12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for f in impacts {
|
||||||
|
self.impact(&f);
|
||||||
|
}
|
||||||
|
// particles
|
||||||
|
for p in self.parts.iter_mut() {
|
||||||
|
p.t += dt;
|
||||||
|
match p.kind {
|
||||||
|
SMOKE => {
|
||||||
|
p.x += p.vx * dt;
|
||||||
|
p.y += p.vy * dt;
|
||||||
|
p.h += p.vh * dt;
|
||||||
|
p.vx *= 1.0 - dt;
|
||||||
|
p.vy *= 1.0 - dt;
|
||||||
|
p.size += dt * 0.35;
|
||||||
|
}
|
||||||
|
MOTE => {
|
||||||
|
p.x += p.vx * dt;
|
||||||
|
p.y += p.vy * dt;
|
||||||
|
p.h += p.vh * dt;
|
||||||
|
}
|
||||||
|
FLASH | RING => {}
|
||||||
|
_ => {
|
||||||
|
// falling: embers drift down slower than sparks and debris
|
||||||
|
let g = if p.kind == EMBER { GRAVITY * 0.35 } else { GRAVITY };
|
||||||
|
p.vh -= g * dt;
|
||||||
|
p.x += p.vx * dt;
|
||||||
|
p.y += p.vy * dt;
|
||||||
|
p.h += p.vh * dt;
|
||||||
|
if p.h <= 0.02 {
|
||||||
|
p.h = 0.02;
|
||||||
|
if matches!(p.kind, SPARK | COIN) && p.vh < -1.0 {
|
||||||
|
p.vh = -p.vh * 0.35;
|
||||||
|
p.vx *= 0.5;
|
||||||
|
p.vy *= 0.5;
|
||||||
|
} else {
|
||||||
|
p.vh = 0.0;
|
||||||
|
p.vx *= 0.8;
|
||||||
|
p.vy *= 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.parts.retain(|p| p.t < p.max);
|
||||||
|
// stuck arrows fall out when their wall is dug away; everything fades in the end
|
||||||
|
let map = &self.map;
|
||||||
|
for p in self.stuck.iter_mut() {
|
||||||
|
p.t += dt;
|
||||||
|
if p.kind == ARROW && p.h > 0.1 {
|
||||||
|
let (tx, ty) = ((p.x + p.vx * 0.08) as i32, (p.y + p.vy * 0.08) as i32);
|
||||||
|
if !map.get(tx, ty).map_or(false, |t| t.solid()) {
|
||||||
|
p.t = p.max;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.stuck.retain(|p| p.t < p.max);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn impact(&mut self, f: &Fx) {
|
||||||
|
let size = f.size;
|
||||||
|
let (x, y) = (f.x2, f.y2);
|
||||||
|
match f.style {
|
||||||
|
ST_FIRE => {
|
||||||
|
// the fireball explodes on the ground
|
||||||
|
let at = (x, y, 0.15);
|
||||||
|
self.part(FLASH, at, (0.0, 0.0, 0.0), 0.45, abgr(255, 170, 60), 0.9 * size);
|
||||||
|
self.part(RING, (x, y, 0.05), (0.0, 0.0, 0.0), 0.5, abgr(255, 120, 40), 1.4 * size);
|
||||||
|
self.burst(EMBER, at, (24.0 * size) as usize, 2.8 * size, 3.0, 1.2, abgr(255, 140, 40), 0.05);
|
||||||
|
self.burst(SPARK, at, 10, 3.5, 2.5, 0.7, abgr(255, 230, 120), 0.035);
|
||||||
|
self.burst(SMOKE, (x, y, 0.3), 8, 0.6, 0.7, 2.2, abgr(70, 60, 60), 0.22);
|
||||||
|
self.stick(Part { kind: SCORCH, x, y, h: 0.0, vx: 0.0, vy: 0.0, vh: 0.0, t: 0.0, max: 25.0, col: 0, size: 0.7 * size });
|
||||||
|
}
|
||||||
|
ST_CANNON => {
|
||||||
|
// the ball explodes: flash, stones and dirt flying, smoke
|
||||||
|
let at = (x, y, 0.2);
|
||||||
|
self.part(FLASH, at, (0.0, 0.0, 0.0), 0.3, abgr(255, 200, 120), 0.7 * size);
|
||||||
|
self.part(RING, (x, y, 0.05), (0.0, 0.0, 0.0), 0.4, abgr(220, 200, 170), 1.6);
|
||||||
|
self.burst(DEBRIS, at, 16, 3.0, 3.5, 1.6, abgr(84, 70, 64), 0.07);
|
||||||
|
self.burst(DEBRIS, at, 6, 2.5, 3.0, 1.4, abgr(120, 92, 70), 0.05);
|
||||||
|
self.burst(SPARK, at, 12, 4.0, 2.5, 0.6, abgr(255, 200, 100), 0.03);
|
||||||
|
self.burst(SMOKE, (x, y, 0.3), 10, 0.8, 0.6, 2.4, abgr(80, 80, 86), 0.25);
|
||||||
|
self.stick(Part { kind: SCORCH, x, y, h: 0.0, vx: 0.0, vy: 0.0, vh: 0.0, t: 0.0, max: 20.0, col: 0, size: 0.6 });
|
||||||
|
}
|
||||||
|
ST_ARROW | ST_ARROW_WALL => {
|
||||||
|
let (dx, dy) = (f.x2 - f.x, f.y2 - f.y);
|
||||||
|
let l = (dx * dx + dy * dy).sqrt().max(0.01);
|
||||||
|
let (dx, dy) = (dx / l, dy / l);
|
||||||
|
if f.style == ST_ARROW_WALL {
|
||||||
|
self.burst(DEBRIS, (x, y, f.h2), 3, 0.8, 1.0, 0.8, abgr(110, 90, 80), 0.04);
|
||||||
|
let h = f.h2 + self.vrng.f() * 0.2;
|
||||||
|
self.stick(Part { kind: ARROW, x, y, h, vx: dx, vy: dy, vh: -0.08, t: 0.0, max: STUCK_LIFE, col: 0, size: 1.0 });
|
||||||
|
} else if self.vrng.f() < 0.35 {
|
||||||
|
// glanced off: stuck in the floor next to the target
|
||||||
|
let (ox, oy) = (self.vrng.f() * 0.7 - 0.35, self.vrng.f() * 0.7 - 0.35);
|
||||||
|
let tilt = -0.6 - self.vrng.f() * 0.6;
|
||||||
|
let n = (1.0 + tilt * tilt).sqrt();
|
||||||
|
self.stick(Part { kind: ARROW, x: x + ox, y: y + oy, h: 0.04, vx: dx / n, vy: dy / n, vh: tilt / n, t: 0.0, max: STUCK_LIFE, col: 0, size: 1.0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ST_SHADOW => self.burst(EMBER, (x, y, f.h2), 14, 1.6, 1.5, 0.9, abgr(190, 70, 255), 0.04),
|
||||||
|
ST_HOLY => self.burst(MOTE, (x, y, f.h2), 12, 0.8, 0.8, 0.8, abgr(255, 240, 160), 0.035),
|
||||||
|
ST_ARCANE => self.burst(EMBER, (x, y, f.h2), 14, 1.8, 1.5, 0.8, abgr(255, 180, 100), 0.035),
|
||||||
|
ST_RUNE => {
|
||||||
|
self.burst(SPARK, (x, y, f.h2), 16, 2.5, 2.0, 0.7, abgr(110, 210, 255), 0.035);
|
||||||
|
self.part(FLASH, (x, y, f.h2), (0.0, 0.0, 0.0), 0.2, abgr(150, 220, 255), 0.4);
|
||||||
|
}
|
||||||
|
ST_MAGIC => self.burst(EMBER, (x, y, f.h2), 14, 1.8, 1.5, 0.9, abgr(255, 100, 220), 0.04),
|
||||||
|
_ => self.burst(SPARK, (x, y, f.h2), 6, 1.5, 1.2, 0.5, f.col, 0.03),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the last projectile gets a style
|
||||||
|
pub(crate) fn style_last_fx(&mut self, style: u8) {
|
||||||
|
if let Some(f) = self.fx.last_mut() {
|
||||||
|
f.style = style;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// where an arrow flying from (x, y) along (dx, dy) ends: the first wall within `max`
|
||||||
|
pub(crate) fn arrow_end(&self, x: f32, y: f32, dx: f32, dy: f32, max: f32) -> (f32, f32, bool) {
|
||||||
|
let mut d = 0.0;
|
||||||
|
while d < max {
|
||||||
|
d += 0.1;
|
||||||
|
let (px, py) = (x + dx * d, y + dy * d);
|
||||||
|
if self.map.get(px as i32, py as i32).map_or(true, |t| t.solid()) {
|
||||||
|
let back = (d - 0.12).max(0.0);
|
||||||
|
return (x + dx * back, y + dy * back, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(x + dx * max, y + dy * max, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn projectiles_trail_and_explode_and_arrows_stick() {
|
||||||
|
let mut g = Game::new(3, 64, 64);
|
||||||
|
let (cx, cy) = g.map.core;
|
||||||
|
let (x, y) = (cx as f32 + 1.5, cy as f32 - 1.5);
|
||||||
|
g.shot(x, y, 0.5, x + 3.0, y, 0.45, 0.3, 0xffffffff);
|
||||||
|
g.style_last_fx(ST_FIRE);
|
||||||
|
let (ex, ey, wall) = g.arrow_end(x, y, -1.0, 0.0, 20.0);
|
||||||
|
assert!(wall);
|
||||||
|
g.shot(x, y, 0.5, ex, ey, 0.45, 0.3, 0xffffffff);
|
||||||
|
g.style_last_fx(ST_ARROW_WALL);
|
||||||
|
let mut trail = 0;
|
||||||
|
for _ in 0..4 {
|
||||||
|
g.update(0.05);
|
||||||
|
trail = trail.max(g.parts.len());
|
||||||
|
}
|
||||||
|
assert!(trail > 3, "fire trail: {}", trail);
|
||||||
|
for _ in 0..6 {
|
||||||
|
g.update(0.05);
|
||||||
|
}
|
||||||
|
assert!(g.parts.iter().any(|p| p.kind == FLASH) || g.stuck.iter().any(|p| p.kind == SCORCH), "explosion");
|
||||||
|
assert!(g.stuck.iter().any(|p| p.kind == ARROW), "arrow stuck");
|
||||||
|
// particles fall to the ground and vanish
|
||||||
|
for _ in 0..200 {
|
||||||
|
g.update(0.05);
|
||||||
|
}
|
||||||
|
assert!(g.parts.iter().all(|p| p.h >= 0.0));
|
||||||
|
assert!(g.parts.len() < 5);
|
||||||
|
assert!(g.stuck.iter().any(|p| p.kind == ARROW), "arrows stay for minutes");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1026,6 +1026,7 @@ impl R3d {
|
|||||||
draw_landmarks_solid(g, t);
|
draw_landmarks_solid(g, t);
|
||||||
draw_towers_solid(g);
|
draw_towers_solid(g);
|
||||||
crate::props::draw_props_solid(g, cam, t);
|
crate::props::draw_props_solid(g, cam, t);
|
||||||
|
draw_parts_solid(g);
|
||||||
|
|
||||||
// ---- unlit glow / overlays
|
// ---- unlit glow / overlays
|
||||||
gl_use_material(&self.glow);
|
gl_use_material(&self.glow);
|
||||||
@@ -1062,6 +1063,7 @@ impl R3d {
|
|||||||
for f in &g.fx {
|
for f in &g.fx {
|
||||||
draw_fx(f, t);
|
draw_fx(f, t);
|
||||||
}
|
}
|
||||||
|
draw_parts_glow(g);
|
||||||
gl_use_default_material();
|
gl_use_default_material();
|
||||||
crate::ui::begin_ui();
|
crate::ui::begin_ui();
|
||||||
if let (true, Some((rt, tw, th))) = (div > 1, self.target.as_ref()) {
|
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 });
|
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) {
|
fn draw_fx(f: &Fx, t: f32) {
|
||||||
let p = (f.t / f.max).clamp(0.0, 1.0);
|
let p = (f.t / f.max).clamp(0.0, 1.0);
|
||||||
let c = rgba(f.col);
|
let c = rgba(f.col);
|
||||||
let a = |alpha: f32| Color::new(c.r, c.g, c.b, alpha);
|
let a = |alpha: f32| Color::new(c.r, c.g, c.b, alpha);
|
||||||
let at = vec3(f.x, f.h, f.y);
|
let at = vec3(f.x, f.h, f.y);
|
||||||
match f.kind {
|
match f.kind {
|
||||||
|
0 if f.style != crate::particles::ST_PLAIN => draw_projectile(f, p, t),
|
||||||
0 => {
|
0 => {
|
||||||
let from = vec3(f.x, f.h, f.y);
|
let from = vec3(f.x, f.h, f.y);
|
||||||
let to = vec3(f.x2, f.h2, f.y2);
|
let to = vec3(f.x2, f.h2, f.y2);
|
||||||
|
|||||||
Reference in new issue
Block a user