Units face eight directions and animate their actions

- five sprite views per kind and gear tier (front, front three-quarter, profile, back three-quarter, back), mirrored for the other three directions; chosen from the unit's facing and the camera
- units face where they walk, dig, repair or fight
- animations: imps stomp while claiming (squash on landing), swing while digging, hammer while repairing; lunges in combat, punches in training, nodding while eating, swaying while reading or praying, breathing when idle, leaning back with loads, lying in bed, chained creatures pulling at their chains, flyers flapping

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-03 15:41:09 +01:00
1 parent 1016ee5d6c
commit d06ce2edb5
3 files changed
+234 -21

No files matched your search

+82 -4
View File
@@ -1085,6 +1085,9 @@ impl R3d {
0.5
}
};
// the camera's right and viewing direction on the ground (for the eight views)
let rg = vec2(r.x, r.z).normalize_or_zero();
let tg = vec2(toward.x, toward.z).normalize_or_zero();
let mut mb = MB::new();
let sw = SPRITE_TEX_W;
let sh = SPRITE_TEX_H;
@@ -1095,12 +1098,81 @@ impl R3d {
let w = sprite_size(e.kind);
let h = w;
let sleeping = matches!(e.job, Job::Sleep(_)) && !e.moving;
// which of the eight directions the unit shows (five views, three of them mirrored)
let d = vec2(e.dir.cos(), e.dir.sin());
let ang = d.dot(rg).atan2(d.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 }; // screen direction the unit faces
let ph = t * 10.0 + e.id as f32 * 1.7;
let (mut sx, mut sy, mut lunge) = (1.0f32, 1.0f32, 0.0f32);
let mut bob = if e.moving { (e.anim * 12.0).sin().abs() * 0.06 } else { 0.0 };
let mut rot = 0.0;
if e.moving {
// a little sway while walking; loads are carried leaning back
rot = (e.anim * 12.0).sin() * 0.05;
if e.carry > 0 || matches!(e.job, Job::Carry(_)) {
rot += 0.12 * fwd;
bob *= 0.6;
}
} else if e.working && !sleeping {
match e.job {
// claiming: stomping the ground, squashed on landing
Job::Claim(_) | Job::Help(_) => {
let k = (ph * 1.1).sin();
bob = k.max(0.0) * 0.13;
if k < 0.0 {
sy = 1.0 + k * 0.14;
sx = 1.0 - k * 0.1;
}
}
// digging: a swing at the wall
Job::Dig(_) | Job::TearBridge(_) => {
let k = (ph * 1.3).sin();
rot = -(k * 0.35).max(-0.12) * fwd;
lunge = k.max(0.0) * 0.07;
}
// hammering
Job::Reinforce(_) | Job::Repair(_) => {
let k = (ph * 2.2).sin();
rot = -k.max(0.0) * 0.25 * fwd;
bob = (ph * 2.2).cos().max(0.0) * 0.03;
}
// fighting: lunging at the enemy
Job::Attack(_) | Job::AttackTower(_) | Job::AttackCore | Job::Torment(_) => {
let k = (ph * 0.9).sin().max(0.0).powi(2);
lunge = k * 0.14;
rot = -k * 0.2 * fwd;
}
// training: quick punches
Job::Train(_) => {
let k = (ph * 1.6).sin();
lunge = k.max(0.0) * 0.08;
rot = -k * 0.12 * fwd;
bob = (ph * 3.2).sin().abs() * 0.03;
}
// eating: nodding over the food
Job::Eat(_) => {
sy = 1.0 - (ph * 0.8).sin().abs() * 0.1;
rot = -(ph * 0.8).sin().abs() * 0.12 * fwd;
}
// reading and praying: a slow sway
Job::Study(_) | Job::Pray(_) | Job::HealCore => rot = (t * 1.8 + e.id as f32).sin() * 0.07,
_ => rot = (ph * 1.4).sin() * 0.15,
}
} else if !sleeping {
// breathing
sy = 1.0 + (t * 2.4 + e.id as f32).sin() * 0.025;
}
// flying creatures hover and flap (on their perch while sleeping)
if flies(e.kind) && !sleeping {
bob = 0.45 + (t * 7.0 + e.anim * 3.0).sin() * 0.06;
sx *= 1.0 + (t * 14.0 + e.id as f32).sin() * 0.06;
} else if sleeping {
// asleep: lying in bed
rot = std::f32::consts::FRAC_PI_2 * 0.9 * fwd;
bob = 0.04;
}
let mut rot = if e.working && !sleeping { (t * 14.0 + e.anim).sin() * 0.15 } else { 0.0 };
// prisoners: lying unconscious, carried above an imp, or shaking in the torture chamber
match e.captive {
1 => rot = std::f32::consts::FRAC_PI_2,
@@ -1111,11 +1183,16 @@ impl R3d {
3 => rot = (t * 25.0 + e.anim).sin() * 0.08,
_ => {}
}
if e.chained {
// pulling at the chains
rot = (t * 3.0 + e.id as f32).sin() * 0.1;
lunge = 0.0;
}
let white = e.flash > 0.07;
let src = Gfx::sprite_cell(e.kind, white, e.level);
let src = Gfx::sprite_cell_view(e.kind, white, e.level, if e.captive != 0 { 0 } else { view });
let (mut u0, v0) = (src.x / sw, src.y / sh);
let (mut u1, v1) = ((src.x + src.w) / sw, (src.y + src.h) / sh);
if e.face < 0.0 {
if mirror {
std::mem::swap(&mut u0, &mut u1);
}
let a = amb(e.x, e.y) * if sleeping || e.captive == 1 { 0.6 } else { 1.0 };
@@ -1128,9 +1205,10 @@ impl R3d {
};
// pulled towards the camera: unchanged on screen (orthographic), but walls behind
// the unit no longer cut into the sprite
let base = vec3(e.x, bob, e.y) + toward * 0.45;
let base = vec3(e.x, bob, e.y) + toward * 0.45 + r * (lunge * fwd);
let (sr, cr) = rot.sin_cos();
let corner = |dx: f32, dy: f32| {
let (dx, dy) = (dx * sx, dy * sy);
let x = dx * cr - dy * sr;
let y = dx * sr + dy * cr;
base + r * x + Vec3::Y * y