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:
1 parent
7972e8ad1b
commit
8c5afe872f
10 files changed
+218
-51
No files matched your search
+47
-51
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user