Walk and attack animations for knight and kobold; the dead topple, sink and fade (death animations where drawn)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-07 23:51:36 +01:00
1 parent 16e299d1f1
commit fb71009110
29 files changed
+697 -3

No files matched your search

+15 -2
View File
@@ -1780,6 +1780,7 @@ pub enum HdAnim {
Hack,
Walk,
Attack,
Die,
}
pub const ANIM_FRAMES: u32 = 4;
struct HdSprite {
@@ -1797,6 +1798,18 @@ macro_rules! hd_sprite {
anims: &[],
}
};
// with walking and attacking (assets/sprites/<name>-walk-64.png etc.)
($kind:expr, $name:literal, animated) => {
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"))),
],
}
};
}
const HD_SPRITES: [HdSprite; 31] = [
HdSprite {
@@ -1805,11 +1818,11 @@ const HD_SPRITES: [HdSprite; 31] = [
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"),
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"),
hd_sprite!(EK::Knight, "knight", animated),
hd_sprite!(EK::Archer, "archer"),
hd_sprite!(EK::Priest, "priest"),
hd_sprite!(EK::Dwarf, "dwarf"),
+88 -1
View File
@@ -1264,8 +1264,25 @@ pub struct R3d {
lava: Vec<f32>, // lava light per tile (see lava_field)
chunks_hd: bool, // the chunks were built with the high-res atlas
chunks_holiday: u8, // ... and with this holiday's decorations
dying: Vec<Dying>, // units shown dying (see update_dying)
seen: Vec<Dying>, // the units of the last frame (their last looks)
seen_time: f32, // the game time of the last frame
}
// a unit that has just died, shown falling for DEATH_T seconds of game time (only the renderer
// knows: the game removes the dead at once)
#[derive(Clone, Copy)]
struct Dying {
id: u32,
kind: EK,
x: f32,
z: f32,
dir: f32,
level: u32,
t0: f32,
}
const DEATH_T: f32 = 1.4;
// The 3D resolution setting refers to a window 1080 device pixels high: smaller windows use a
// proportionally smaller divisor, so the 3D pixels keep their size relative to the window.
// 1 (full resolution) stays 1.
@@ -1314,7 +1331,7 @@ fn chunk_sig(map: &Map, cx: i32, cz: i32) -> u64 {
impl R3d {
pub fn new() -> R3d {
R3d { lit: lit_material(), glow: glow_material(), opaque: opaque_material(), chunks: Vec::new(), ncx: 0, ncz: 0, outlines: true, wall_sub: 3, max_lights: MAX_LIGHTS, pixel_rows: crate::settings::PIXEL_ART_ROWS as u32, target: None, lava: Vec::new(), chunks_hd: false, chunks_holiday: 0 }
R3d { lit: lit_material(), glow: glow_material(), opaque: opaque_material(), chunks: Vec::new(), ncx: 0, ncz: 0, outlines: true, wall_sub: 3, max_lights: MAX_LIGHTS, pixel_rows: crate::settings::PIXEL_ART_ROWS as u32, target: None, lava: Vec::new(), chunks_hd: false, chunks_holiday: 0, dying: Vec::new(), seen: Vec::new(), seen_time: 0.0 }
}
// Changing the wall detail rebuilds all chunks
@@ -1662,6 +1679,7 @@ impl R3d {
// sprites as billboards
// the world's pixel rows (detailed sprites only where they get enough pixels)
let world_rows = pix.map_or(screen_height() * screen_dpi_scale(), |(_, th, _)| th as f32);
self.update_dying(g);
for mesh in self.billboards(g, gfx, cam, t, world_rows).iter() {
draw_mesh(mesh);
}
@@ -1739,6 +1757,29 @@ impl R3d {
self.draw_2d(g, gfx, cam, &m, t);
}
// units that vanished since the last frame died (or left): they are shown falling for a
// moment; a new game, a loaded one or a jump in time starts afresh
fn update_dying(&mut self, g: &Game) {
let now: Vec<Dying> = g
.ents
.iter()
.filter(|e| e.alive)
.map(|e| Dying { id: e.id, kind: e.kind, x: e.disp_x, z: e.disp_y, dir: e.dir, level: e.level, t0: g.time })
.collect();
if g.time < self.seen_time || g.time > self.seen_time + 2.0 {
self.dying.clear();
} else {
let gone: Vec<Dying> = self.seen.iter().filter(|d| !now.iter().any(|e| e.id == d.id)).copied().collect();
// many at once: not deaths but a change of the map
if gone.len() <= 24 {
self.dying.extend(gone.into_iter().map(|d| Dying { t0: g.time, ..d }));
}
}
self.dying.retain(|d| g.time - d.t0 < DEATH_T);
self.seen = now;
self.seen_time = g.time;
}
// the units as billboards: the 10x10 sprites, the detailed ones (two levels) and their gear
fn billboards(&self, g: &Game, gfx: &Gfx, cam: &Cam, t: f32, world_rows: f32) -> Vec<Mesh> {
let (_, r, _) = cam.basis();
@@ -2014,6 +2055,52 @@ impl R3d {
[toward; 4],
);
}
// the dying: their death animation (detailed sprites that have one), else they topple
// over; then they sink into the floor and fade
for d in &self.dying {
let el = g.time - d.t0;
let w = sprite_size(d.kind);
let dv = vec2(d.dir.cos(), d.dir.sin());
let ang = dv.dot(rg).atan2(dv.dot(tg));
let view = ((ang / std::f32::consts::FRAC_PI_4).round().abs() as u32).min(4);
let mirror = ang < 0.0;
let fwd = if mirror { -1.0 } else { 1.0 };
let texel = w * 1.2 * px_per_tile / crate::gfx::HD_CELL as f32;
let fine = texel >= 0.85;
let die = Gfx::hd_has_anim(d.kind, crate::gfx::HdAnim::Die);
let frame = ((el / 0.22) as u32).min(crate::gfx::ANIM_FRAMES - 1);
let pic = if hd && texel * 2.0 >= 0.85 { Gfx::hd_picture(d.kind, view, die.then_some((crate::gfx::HdAnim::Die, frame)), false) } else { None };
let rot = if die && pic.is_some() { 0.0 } else { (el / 0.35).min(1.0) * std::f32::consts::FRAC_PI_2 * 0.95 * fwd };
let sink = ((el - DEATH_T * 0.55) / (DEATH_T * 0.45)).clamp(0.0, 1.0);
let src = match pic {
Some(p) => Gfx::hd_uv(p, fine),
None => {
let r = Gfx::sprite_cell_view(d.kind, false, d.level, view);
Rect::new(r.x / sw, r.y / sh, r.w / sw, r.h / sh)
}
};
let (mut u0, v0, mut u1, v1) = (src.x, src.y, src.x + src.w, src.y + src.h);
if mirror {
std::mem::swap(&mut u0, &mut u1);
}
// fading: the see-through look of invisible units (every other pixel) at the end
let a = amb(d.x, d.z) * (1.0 - 0.4 * sink);
let c = col(Vec3::splat(a), if sink > 0.5 { 0.35 } else { 1.0 });
let base = vec3(d.x, -sink * 0.25 * w, d.z) + toward * 0.45;
let (sr, cr) = rot.sin_cos();
let corner = |dx: f32, dy: f32| base + r * (dx * cr - dy * sr) + Vec3::Y * (dx * sr + dy * cr);
let mb = match pic {
Some(_) if fine => &mut mb_hd,
Some(_) => &mut mb_hd32,
None => &mut mb,
};
mb.quad(
[corner(-w * 0.6, w * 1.1), corner(w * 0.6, w * 1.1), corner(w * 0.6, -w * 0.1), corner(-w * 0.6, -w * 0.1)],
[vec2(u0, v0), vec2(u1, v0), vec2(u1, v1), vec2(u0, v1)],
[c; 4],
[toward; 4],
);
}
let mesh = |mb: MB, tex: &Texture2D| if mb.v.is_empty() { None } else { Some(Mesh { vertices: mb.v, indices: mb.i, texture: Some(tex.clone()) }) };
[mesh(mb, &gfx.sprites), mesh(mb_hd, &gfx.hd_sprites), mesh(mb_hd32, &gfx.hd_sprites32), mesh(mb_gear, &gfx.gear)].into_iter().flatten().collect()
}