Detailed sprites: frames for walk and attack animations, gear laid over them, hit flash in the shader, 32x32 level in its own texture

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:32:29 +01:00
1 parent 01e5326721
commit ffad92b641
2 files changed
+366 -130

No files matched your search

+109 -17
View File
@@ -16,6 +16,8 @@ pub const MAX_LIGHTS: usize = 24;
const ATLAS_W: f32 = 256.0;
const ATLAS_H: f32 = (crate::gfx::ATLAS_ROWS * 16) as f32;
const DISP_AMP: f32 = 0.11; // wall surface irregularity
// vertex alpha of a detailed sprite flashing white after a hit (see the lit shader)
const FLASH_A: u8 = 204;
// ------------------------------------------------------------------ camera
@@ -294,6 +296,12 @@ void main() {
wave = dot(slope, vec2(0.45, 0.35)) * 0.6 + ripple * 0.4;
}
vec4 tex = texture2D(Texture, tuv);
// the hit flash of the detailed sprites (vertex alpha FLASH_A): the picture in white
if (color.a > 0.77 && color.a < 0.83) {
if (tex.a < 0.5) discard;
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
return;
}
if (liquid && HdLiquid > 0.5) {
// a second, larger layer drifting slower and sideways: the surface churns instead of
// only sliding along
@@ -1654,8 +1662,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);
let (small, detailed) = self.billboards(g, gfx, cam, t, world_rows);
for mesh in [small, detailed].iter().flatten() {
for mesh in self.billboards(g, gfx, cam, t, world_rows).iter() {
draw_mesh(mesh);
}
// solid unlit objects (default material, depth tested)
@@ -1732,8 +1739,8 @@ impl R3d {
self.draw_2d(g, gfx, cam, &m, t);
}
// the units as billboards: the 10x10 sprites and the detailed ones (two textures)
fn billboards(&self, g: &Game, gfx: &Gfx, cam: &Cam, t: f32, world_rows: f32) -> (Option<Mesh>, Option<Mesh>) {
// 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();
let toward = cam.eye_dir();
let amb = |x: f32, z: f32| {
@@ -1752,6 +1759,8 @@ impl R3d {
let tg = vec2(toward.x, toward.z).normalize_or_zero();
let mut mb = MB::new();
let mut mb_hd = MB::new();
let mut mb_hd32 = MB::new();
let mut mb_gear = MB::new();
let sw = SPRITE_TEX_W;
let sh = SPRITE_TEX_H;
// detailed sprites only in the high-res style, and only where a sprite's cell (1.2 sizes
@@ -1865,18 +1874,40 @@ impl R3d {
// treasure in a sack on the back; digging as a drawn animation (detailed sprites),
// frame by frame with the same rhythm as the swing below
let sack = e.carry > 0 && Gfx::has_sack(e.kind);
// drawn animations (detailed sprites): digging, walking, attacking, frame by frame in
// the rhythm of the swings below (two steps per walk cycle, as the bobbing)
let cycle = |p: f32| ((p / std::f32::consts::TAU).rem_euclid(1.0) * crate::gfx::ANIM_FRAMES as f32) as u32;
let hacking = e.working && !e.moving && !sleeping && matches!(e.job, Job::Dig(_) | Job::TearBridge(_));
let frame = ((ph * 1.3 / std::f32::consts::TAU).rem_euclid(1.0) * crate::gfx::HACK_FRAMES as f32) as u32;
let hd = if detailed { Gfx::hd_cell_view(e.kind, white, v, hacking.then_some(frame), sack, fine) } else { None };
if hd.is_some() && hacking {
// the frames swing the pickaxe: no tilting of the whole figure
rot = 0.0;
lunge = 0.0;
let attacking = e.working && !e.moving && !sleeping && matches!(e.job, Job::Attack(_) | Job::AttackTower(_) | Job::AttackCore | Job::Torment(_));
let anim = if e.captive != 0 || sleeping {
None
} else if hacking {
Some((crate::gfx::HdAnim::Hack, cycle(ph * 1.3)))
} else if e.moving {
Some((crate::gfx::HdAnim::Walk, cycle(e.anim * 12.0)))
} else if attacking {
Some((crate::gfx::HdAnim::Attack, cycle(ph * 0.9)))
} else {
None
};
let pic = if detailed { Gfx::hd_picture(e.kind, v, anim, sack) } else { None };
if let (Some(_), Some((a, _))) = (pic, anim) {
if Gfx::hd_has_anim(e.kind, a) {
// the frames move the figure: no tilting, lunging or bobbing of the whole
rot = 0.0;
lunge = 0.0;
bob = bob.min(0.02);
}
}
let src = hd.or_else(|| sack.then(|| Gfx::sprite_cell_sack(e.kind, white, e.level, v)).flatten()).unwrap_or_else(|| Gfx::sprite_cell_view(e.kind, white, e.level, v));
let (sw, sh) = if hd.is_some() { (crate::gfx::HD_TEX_W, crate::gfx::HD_TEX_H) } else { (sw, sh) };
let (mut u0, v0) = (src.x / sw, src.y / sh);
let (mut u1, v1) = ((src.x + src.w) / sw, (src.y + src.h) / sh);
let src = match pic {
Some(p) => Gfx::hd_uv(p, fine),
None => {
let r = sack.then(|| Gfx::sprite_cell_sack(e.kind, white, e.level, v)).flatten().unwrap_or_else(|| Gfx::sprite_cell_view(e.kind, white, e.level, v));
Rect::new(r.x / sw, r.y / sh, r.w / sw, r.h / sh)
}
};
let (mut u0, v0) = (src.x, src.y);
let (mut u1, v1) = (src.x + src.w, src.y + src.h);
if mirror {
std::mem::swap(&mut u0, &mut u1);
}
@@ -1884,7 +1915,9 @@ impl R3d {
// spell looks: stone grey, plague green, see-through when invisible
let alpha = if e.invis > 0.0 { 0.35 } else { 1.0 };
let c = if white {
[255, 255, 255, 255]
// the detailed sprites flash white in the shader (FLASH_A), the 10x10 ones have
// white silhouettes
[255, 255, 255, if pic.is_some() { FLASH_A } else { 255 }]
} else if e.captive == 3 {
col(vec3(a * 1.2, a * 0.55, a * 0.55), 1.0)
} else if e.shield > 0.0 {
@@ -1914,7 +1947,66 @@ impl R3d {
};
// the 12x12 cell (sprite with a one pixel border for the gear): the sprite's feet
// (bottom of its 11th pixel row) stay on the ground
let mb = if hd.is_some() { &mut mb_hd } else { &mut mb };
// the gear of the detailed sprites: parts laid over the picture where its anchors say
// (shoulder pieces from level 3, a helmet from 5, a cape from 7, gold and a crown
// from 9, like the 10x10 sprites' gear)
if let Some(p) = pic {
let tier = crate::gfx::gear_tier(e.level);
if tier > 0 && !white && e.captive == 0 && !sleeping {
let an = Gfx::hd_anchor(p);
let shade = |rgb: (f32, f32, f32)| {
let k = a * 1.25;
[(rgb.0 * k).min(255.0) as u8, (rgb.1 * k).min(255.0) as u8, (rgb.2 * k).min(255.0) as u8, 255u8]
};
let metal = shade(match tier {
1 => (196.0, 126.0, 60.0),
4 => (250.0, 206.0, 72.0),
_ => (208.0, 214.0, 228.0),
});
// a point of the picture's cell (shares of it) in the world
let at = |fx: f32, fy: f32, depth: f32| {
let fx = if mirror { 1.0 - fx } else { fx };
corner((fx - 0.5) * 1.2 * w, (1.0 - fy) * 1.2 * h - 0.1 * h) + toward * depth
};
let gt = crate::gfx::GEAR_TEX as f32;
let mut part = |x0: f32, y0: f32, x1: f32, y1: f32, uv: (u32, u32, u32, u32), c: [u8; 4], depth: f32, flip: bool| {
let (mut a0, b0) = (uv.0 as f32 / gt, uv.1 as f32 / gt);
let (mut a1, b1) = ((uv.0 + uv.2) as f32 / gt, (uv.1 + uv.3) as f32 / gt);
if mirror != flip {
std::mem::swap(&mut a0, &mut a1);
}
mb_gear.quad([at(x0, y0, depth), at(x1, y0, depth), at(x1, y1, depth), at(x0, y1, depth)], [vec2(a0, b0), vec2(a1, b0), vec2(a1, b1), vec2(a0, b1)], [c; 4], [toward; 4]);
};
let mid = (an.head.0 + an.head.1) * 0.5;
let neck = an.top + (an.shoulder - an.top) * 0.45;
if tier >= 3 {
// the cape hangs behind the figure (seen from behind: over its back)
let cape = shade(if e.hero { (44.0, 74.0, 156.0) } else { (128.0, 22.0, 34.0) });
let depth = if v >= 3 { 0.004 } else { -0.004 };
part(an.sh.0 - 0.03, an.shoulder - 0.02, an.sh.1 + 0.03, an.bottom - 0.1, crate::gfx::GEAR_CAPE, cape, depth, false);
}
if tier >= 2 {
part(an.head.0 - 0.02, an.top - 0.03, an.head.1 + 0.02, neck, crate::gfx::GEAR_HELMET, metal, 0.006, false);
if tier >= 4 {
part(mid - 0.1, an.top - 0.11, mid + 0.1, an.top + 0.01, crate::gfx::GEAR_CROWN, shade((255.0, 220.0, 90.0)), 0.008, false);
} else if e.hero {
part(mid - 0.035, an.top - 0.13, mid + 0.035, an.top + 0.01, crate::gfx::GEAR_PLUME, shade((210.0, 40.0, 40.0)), 0.008, false);
} else {
let bone = shade((232.0, 222.0, 200.0));
part(an.head.0 - 0.06, an.top - 0.11, an.head.0 + 0.02, an.top + 0.02, crate::gfx::GEAR_HORN, bone, 0.008, false);
part(an.head.1 - 0.02, an.top - 0.11, an.head.1 + 0.06, an.top + 0.02, crate::gfx::GEAR_HORN, bone, 0.008, true);
}
}
for (x, flip) in [(an.sh.0 + 0.03, false), (an.sh.1 - 0.03, true)] {
part(x - 0.075, an.shoulder - 0.05, x + 0.075, an.shoulder + 0.06, crate::gfx::GEAR_PAULDRON, metal, 0.007, flip);
}
}
}
let mb = match pic {
Some(_) if fine => &mut mb_hd,
Some(_) => &mut mb_hd32,
None => &mut mb,
};
mb.quad(
[corner(-w * 0.6, h * 1.1), corner(w * 0.6, h * 1.1), corner(w * 0.6, -h * 0.1), corner(-w * 0.6, -h * 0.1)],
[vec2(u0, v0), vec2(u1, v0), vec2(u1, v1), vec2(u0, v1)],
@@ -1923,7 +2015,7 @@ impl R3d {
);
}
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, &gfx.sprites), mesh(mb_hd, &gfx.hd_sprites), mesh(mb_hd32, &gfx.hd_sprites32), mesh(mb_gear, &gfx.gear)].into_iter().flatten().collect()
}
fn draw_2d(&self, g: &Game, gfx: &Gfx, cam: &Cam, m: &Mat4, t: f32) {