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

+47 -51
View File
@@ -104,6 +104,7 @@ pub struct Gfx {
pub hd_sprites: LazyTex, // detailed sprites, 64x64 level (see HD_SPRITES, hd_picture)
pub hd_sprites32: LazyTex, // the same, 32x32 level
pub gear: LazyTex, // their gear parts (see GEAR_HELMET etc.)
pub fx: LazyTex, // effect flipbooks of the high-res style (see render3d FX_*)
pub atlas_hd: LazyTex, // the tile atlas of the high-res style, 64x64 tiles (see HD_TILES)
pub atlas_hd2: LazyTex, // the same with 128x128 tiles (close up)
pub materials: [Texture2D; 9], // furniture materials, high-res (see crate::props::Mat)
@@ -1815,65 +1816,55 @@ struct HdSprite {
idle_32: &'static [u8],
anims: &'static [(HdAnim, &'static [u8], &'static [u8])], // 64x64 and 32x32 sheets
}
macro_rules! hd_sprite {
($kind:expr, $name:literal) => {
HdSprite {
kind: $kind,
idle: include_bytes!(concat!("../assets/sprites/", $name, "-64.png")),
idle_32: include_bytes!(concat!("../assets/sprites/", $name, "-32.png")),
anims: &[],
}
// a detailed sprite: the idle sheets assets/sprites/<name>-64.png / -32.png, and for every
// animation named (walk, attack, die, hack) the sheets <name>-<animation>-64.png / -32.png
macro_rules! hd_anim {
($name:literal, $anim:ident, $file:literal) => {
(HdAnim::$anim, include_bytes!(concat!("../assets/sprites/", $name, "-", $file, "-64.png")) as &[u8], include_bytes!(concat!("../assets/sprites/", $name, "-", $file, "-32.png")) as &[u8])
};
// with walking and attacking (assets/sprites/<name>-walk-64.png etc.)
($kind:expr, $name:literal, animated) => {
}
macro_rules! hd_sprite {
($kind:expr, $name:literal $(, $anim:ident $file:literal)*) => {
HdSprite {
kind: $kind,
idle: include_bytes!(concat!("../assets/sprites/", $name, "-64.png")),
idle_32: include_bytes!(concat!("../assets/sprites/", $name, "-32.png")),
anims: &[
(HdAnim::Walk, include_bytes!(concat!("../assets/sprites/", $name, "-walk-64.png")), include_bytes!(concat!("../assets/sprites/", $name, "-walk-32.png"))),
(HdAnim::Attack, include_bytes!(concat!("../assets/sprites/", $name, "-attack-64.png")), include_bytes!(concat!("../assets/sprites/", $name, "-attack-32.png"))),
],
anims: &[$(hd_anim!($name, $anim, $file)),*],
}
};
}
const HD_SPRITES: [HdSprite; 31] = [
HdSprite {
kind: EK::Worker,
idle: include_bytes!("../assets/sprites/imp-64.png"),
idle_32: include_bytes!("../assets/sprites/imp-32.png"),
anims: &[(HdAnim::Hack, include_bytes!("../assets/sprites/imp-hack-64.png"), include_bytes!("../assets/sprites/imp-hack-32.png"))],
},
hd_sprite!(EK::Kobold, "kobold", animated),
hd_sprite!(EK::Spider, "spider"),
hd_sprite!(EK::Mage, "mage"),
hd_sprite!(EK::Ogre, "ogre"),
hd_sprite!(EK::Knight, "knight", animated),
hd_sprite!(EK::Archer, "archer"),
hd_sprite!(EK::Priest, "priest"),
hd_sprite!(EK::Dwarf, "dwarf"),
hd_sprite!(EK::Paladin, "paladin"),
hd_sprite!(EK::Jailer, "jailer"),
hd_sprite!(EK::Priestess, "priestess"),
hd_sprite!(EK::Chainbreaker, "chainbreaker"),
hd_sprite!(EK::Bat, "bat"),
hd_sprite!(EK::Gargoyle, "gargoyle"),
hd_sprite!(EK::Owl, "owl"),
hd_sprite!(EK::Wyrm, "wyrm"),
hd_sprite!(EK::Cogling, "cogling"),
hd_sprite!(EK::Revenant, "revenant"),
hd_sprite!(EK::Giant, "giant"),
hd_sprite!(EK::Fairy, "fairy"),
hd_sprite!(EK::BattleMage, "battlemage"),
hd_sprite!(EK::Ram, "ram"),
hd_sprite!(EK::Catapult, "catapult"),
hd_sprite!(EK::CaveBear, "cavebear"),
hd_sprite!(EK::Beetle, "beetle"),
hd_sprite!(EK::Trader, "trader"),
hd_sprite!(EK::Thief, "thief"),
hd_sprite!(EK::Monk, "monk"),
hd_sprite!(EK::Ranger, "ranger"),
hd_sprite!(EK::Inquisitor, "inquisitor"),
hd_sprite!(EK::Worker, "imp", Hack "hack", Walk "walk", Die "die"),
hd_sprite!(EK::Kobold, "kobold", Walk "walk", Die "die", Attack "attack"),
hd_sprite!(EK::Spider, "spider", Walk "walk", Die "die", Attack "attack"),
hd_sprite!(EK::Mage, "mage", Walk "walk", Die "die", Attack "attack"),
hd_sprite!(EK::Ogre, "ogre", Walk "walk", Die "die", Attack "attack"),
hd_sprite!(EK::Knight, "knight", Walk "walk", Die "die", Attack "attack"),
hd_sprite!(EK::Archer, "archer", Walk "walk", Die "die", Attack "attack"),
hd_sprite!(EK::Priest, "priest", Walk "walk", Die "die"),
hd_sprite!(EK::Dwarf, "dwarf", Walk "walk", Die "die", Attack "attack"),
hd_sprite!(EK::Paladin, "paladin", Walk "walk", Die "die", Attack "attack"),
hd_sprite!(EK::Jailer, "jailer", Walk "walk", Die "die"),
hd_sprite!(EK::Priestess, "priestess", Walk "walk", Die "die"),
hd_sprite!(EK::Chainbreaker, "chainbreaker", Walk "walk", Die "die"),
hd_sprite!(EK::Bat, "bat", Walk "walk", Die "die"),
hd_sprite!(EK::Gargoyle, "gargoyle", Walk "walk", Die "die"),
hd_sprite!(EK::Owl, "owl", Walk "walk", Die "die"),
hd_sprite!(EK::Wyrm, "wyrm", Walk "walk", Die "die"),
hd_sprite!(EK::Cogling, "cogling", Walk "walk", Die "die"),
hd_sprite!(EK::Revenant, "revenant", Walk "walk", Die "die"),
hd_sprite!(EK::Giant, "giant", Walk "walk", Die "die"),
hd_sprite!(EK::Fairy, "fairy", Walk "walk", Die "die"),
hd_sprite!(EK::BattleMage, "battlemage", Walk "walk", Die "die"),
hd_sprite!(EK::Ram, "ram", Walk "walk", Die "die"),
hd_sprite!(EK::Catapult, "catapult", Walk "walk", Die "die"),
hd_sprite!(EK::CaveBear, "cavebear", Walk "walk", Die "die"),
hd_sprite!(EK::Beetle, "beetle", Walk "walk", Die "die"),
hd_sprite!(EK::Trader, "trader", Walk "walk", Die "die"),
hd_sprite!(EK::Thief, "thief", Walk "walk", Die "die"),
hd_sprite!(EK::Monk, "monk", Walk "walk", Die "die"),
hd_sprite!(EK::Ranger, "ranger", Walk "walk", Die "die"),
hd_sprite!(EK::Inquisitor, "inquisitor", Walk "walk", Die "die"),
];
// a detailed sprite's cell: the same shape as a 10x10 sprite's 12x12 cell (6 texels per pixel),
// so the billboard is the same; the 64x64 picture sits where the 10x10 sprite would; the 32x32
@@ -3087,6 +3078,11 @@ impl Gfx {
.clone()
});
let gear = LazyTex::new(|| tex(&gen_gear()));
let fx = LazyTex::new(|| {
let t = Texture2D::from_file_with_format(include_bytes!("../assets/textures/fx/fx.png"), Some(ImageFormat::Png));
t.set_filter(FilterMode::Nearest);
t
});
let mat = |b: &[u8]| {
let t = Texture2D::from_file_with_format(b, Some(ImageFormat::Png));
t.set_filter(FilterMode::Nearest);
@@ -3094,7 +3090,7 @@ impl Gfx {
};
let materials = MATERIALS.map(|(hd, _)| mat(hd));
let materials_px = MATERIALS.map(|(_, px)| mat(px));
Gfx { atlas, sprites, hd_sprites, hd_sprites32, gear, atlas_hd, atlas_hd2, materials, materials_px, cursors, badges, font, font_map }
Gfx { atlas, sprites, hd_sprites, hd_sprites32, gear, fx, atlas_hd, atlas_hd2, materials, materials_px, cursors, badges, font, font_map }
}
pub fn cell(id: u32) -> Rect {
+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);