High-res effects: explosions, smoke, bursts, impact stars, healing and glowing projectiles as flipbooks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-08 01:00:20 +01:00
1 parent 7972e8ad1b
commit 8c5afe872f
10 files changed
+218 -51

No files matched your search

+68
View File
@@ -1602,6 +1602,9 @@ impl R3d {
let m = cam.matrix();
self.update_chunks(g, gfx, &m);
EYE.with(|e| e.set(cam.eye_dir()));
let (_, cr, cu) = cam.basis();
let hd_fx = self.pixel_rows >= crate::settings::HIGH_RES_ROWS as u32;
FX.with(|f| *f.borrow_mut() = hd_fx.then(|| (gfx.fx.clone(), cr, cu)));
// pixel look: the world is rendered at a fixed number of rows and scaled up to the screen
// on the virtual screen (ui.rs) the world takes exactly k x k interface pixels per pixel
let vrows = crate::ui::virtual_rows();
@@ -2712,6 +2715,44 @@ fn draw_towers_glow(g: &Game, t: f32) {
// Camera direction for camera-facing line quads (set once per frame)
thread_local! {
static EYE: std::cell::Cell<Vec3> = std::cell::Cell::new(Vec3::Y);
// the high-res style's effect flipbooks and the camera's right and up (set once per frame;
// None in the pixel-art style, whose effects stay simple shapes)
static FX: std::cell::RefCell<Option<(Texture2D, Vec3, Vec3)>> = const { std::cell::RefCell::new(None) };
}
// the rows of the effect atlas (8 frames of 64x64 each)
const FX_EXPLOSION: u32 = 0;
const FX_SMOKE: u32 = 1;
const FX_BURST: u32 = 2; // white, tinted
const FX_IMPACT: u32 = 3; // white, tinted
const FX_HEAL: u32 = 4;
const FX_ORBS: u32 = 5; // projectiles: 0-1 fireball, 2-3 white orb (tinted), 4 holy, 5 shadow, 6 rune, 7 arcane
// draws frame `frame` of an effect row as a camera-facing picture (`turn`: radians in the
// screen plane); false in the pixel-art style (the caller draws its simple shapes)
fn fx_sprite(row: u32, frame: u32, at: Vec3, size: f32, turn: f32, c: Color) -> bool {
FX.with(|f| {
let f = f.borrow();
let Some((tex, r, u)) = f.as_ref() else { return false };
let (s, co) = turn.sin_cos();
let (rr, uu) = ((*r * co + *u * s) * size * 0.5, (*u * co - *r * s) * size * 0.5);
let (u0, v0) = ((frame.min(7) as f32) / 8.0, row as f32 / 6.0);
let (u1, v1) = (u0 + 1.0 / 8.0, v0 + 1.0 / 6.0);
let col = [(c.r * 255.0) as u8, (c.g * 255.0) as u8, (c.b * 255.0) as u8, (c.a.clamp(0.0, 1.0) * 255.0) as u8];
let v = |p: Vec3, uv: Vec2| Vertex { position: p, uv, color: col, normal: Vec4::ZERO };
let mesh = Mesh {
vertices: vec![v(at - rr + uu, vec2(u0, v0)), v(at + rr + uu, vec2(u1, v0)), v(at + rr - uu, vec2(u1, v1)), v(at - rr - uu, vec2(u0, v1))],
indices: vec![0, 1, 2, 0, 2, 3],
texture: Some(tex.clone()),
};
draw_mesh(&mesh);
true
})
}
// the frame of an 8-frame effect at life p (0..1)
fn fx_frame(p: f32) -> u32 {
((p * 8.0) as u32).min(7)
}
// Thick 3D line as a camera-facing quad (GL lines don't work with custom materials)
@@ -3509,6 +3550,23 @@ fn draw_projectile(f: &Fx, p: f32, t: f32) {
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 | ST_HOLY | ST_SHADOW | ST_RUNE | ST_ARCANE | ST_MAGIC if {
// the high-res style: a picture, a fireball's tail pointing back along its way
let back = at((p - 0.05).max(0.0));
let (_, r, u) = FX.with(|x| x.borrow().as_ref().map(|x| (0, x.1, x.2))).unwrap_or((0, Vec3::X, Vec3::Y));
let d = head - back;
let turn = d.dot(u).atan2(d.dot(r));
let flick = ((t * 12.0 + f.x) as u32) % 2;
let (frame, tint) = match f.style {
ST_FIRE => (flick, WHITE),
ST_HOLY => (4, WHITE),
ST_SHADOW => (5, WHITE),
ST_RUNE => (6, WHITE),
ST_ARCANE => (7, WHITE),
_ => (2 + flick, Color::new(c.r, c.g, c.b, 1.0)),
};
fx_sprite(FX_ORBS, frame, head, 0.42 * sz, if f.style == ST_FIRE { turn } else { 0.0 }, tint)
} => {}
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));
@@ -3801,7 +3859,14 @@ fn draw_parts_glow(g: &Game) {
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 if fx_sprite(FX_SMOKE, fx_frame(life), at, p.size * 2.6, p.size * 3.0, Color::new(c.r.max(0.5), c.g.max(0.5), c.b.max(0.5), 0.55 * (1.0 - life))) => {}
SMOKE => draw_sphere(at, p.size, None, Color::new(c.r, c.g, c.b, 0.14 * (1.0 - life))),
FLASH if {
// fire explodes, anything else bursts in its colour
let fiery = c.r > 0.8 && c.g < 0.8 && c.b < 0.45;
let (row, tint) = if fiery { (FX_EXPLOSION, WHITE) } else { (FX_BURST, Color::new(c.r, c.g, c.b, 1.0)) };
fx_sprite(row, fx_frame(life), at, p.size * 2.6, 0.0, tint)
} => {}
FLASH => {
let r = p.size * (0.3 + life * 0.9);
draw_sphere(at, r * 0.6, None, Color::new(1.0, 0.95, 0.8, 0.95 * (1.0 - life)));
@@ -3886,6 +3951,7 @@ fn draw_fx(f: &Fx, t: f32) {
}
disc(vec3(f.x, 0.05, f.y), 1.6 * (0.5 + p), Color::new(1.0, 1.0, 0.7, 0.35 * (1.0 - p)), 24);
}
2 if fx_sprite(FX_IMPACT, fx_frame(p), at, 0.7 * f.size, 0.0, Color::new(c.r.max(0.4), c.g.max(0.4), c.b.max(0.4), 1.0)) => {}
2 => {
let r = (0.1 + 0.3 * p) * f.size;
let n = if f.size > 1.35 { 10 } else { 6 };
@@ -3897,7 +3963,9 @@ fn draw_fx(f: &Fx, t: f32) {
disc(vec3(f.x, 0.03, f.y), r * 1.3, a(0.3 * (1.0 - p)), 12);
}
}
3 if fx_sprite(FX_BURST, fx_frame(p), vec3(f.x, 0.4, f.y), 1.4 * f.size, 0.0, Color::new(c.r, c.g, c.b, 1.0)) => {}
3 => draw_sphere(vec3(f.x, 0.3, f.y), (0.2 + 0.5 * p) * f.size, None, a(0.4 * (1.0 - p))),
4 if fx_sprite(FX_HEAL, fx_frame(p), at + vec3(0.0, 0.4, 0.0), 1.0, 0.0, WHITE) => {}
4 => {
for k in 0..3 {
let o = vec3((k as f32 - 1.0) * 0.2, 0.3 + p * 0.9 + k as f32 * 0.1, 0.0);