Aerie and flying creatures: bats, chain gargoyles, arcane owls, ember wyrms

- New room: aerie (key Y, rooms category, campaign from mission 7); flyers sleep there
- Arrivals: aerie 4+ bats; + torture chamber gargoyles; + library owls; big aerie and treasury
  (16+ each) a rare ember wyrm; portal checks free places in lairs and aerie separately
- Flyers cross water and lava (path finding, flag, attacks), are not burnt by lava, patrol far
  when idle, attack single enemies, flee to the aerie below 25% health and rest there up to 75%
- Owls shoot arcane bolts, the wyrm breathes fire with splash damage; shots from the air
- Hovering sprites with flapping, aerie floor, perches, roosts, plinths, twig and ember nests
- Room counts are recounted on load (no longer stored); tests for flying and arrivals

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-03 13:01:26 +01:00
1 parent 729dd4fe4c
commit b7d7fc2b52
10 files changed
+454 -56

No files matched your search

+15 -7
View File
@@ -1088,11 +1088,14 @@ impl R3d {
if !e.alive || e.held {
continue;
}
let big = matches!(e.kind, EK::Ogre | EK::Paladin | EK::Jailer | EK::Chainbreaker);
let w = if e.kind == EK::Chainbreaker { 1.0 } else if big { 0.8 } else { 0.62 };
let w = sprite_size(e.kind);
let h = w;
let sleeping = matches!(e.job, Job::Sleep(_)) && !e.moving;
let mut bob = if e.moving { (e.anim * 12.0).sin().abs() * 0.06 } else { 0.0 };
// 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;
}
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 {
@@ -1157,8 +1160,9 @@ impl R3d {
if !e.alive || e.held {
continue;
}
let big = matches!(e.kind, EK::Ogre | EK::Paladin | EK::Jailer | EK::Chainbreaker);
let head = proj(vec3(e.x, if big { 0.9 } else { 0.72 }, e.y));
let big = matches!(e.kind, EK::Ogre | EK::Paladin | EK::Jailer | EK::Chainbreaker | EK::Wyrm | EK::Gargoyle);
let lift = if flies(e.kind) && !(matches!(e.job, Job::Sleep(_)) && !e.moving) { 0.45 } else { 0.0 };
let head = proj(vec3(e.x, lift + if big { 0.9 } else { 0.72 }, e.y));
// bar width in block characters, depending on the zoom
let cols = ((px_per_tile * 0.6 / cw) as usize).clamp(3, 8);
if (e.hp < e.max_hp || e.hero) && e.captive == 0 {
@@ -1222,9 +1226,9 @@ fn shade_c(r: u8, g: u8, b: u8, f: f32) -> Color {
// size of a unit's sprite (same as in the billboards)
fn sprite_size(k: EK) -> f32 {
if k == EK::Chainbreaker {
if matches!(k, EK::Chainbreaker | EK::Wyrm) {
1.0
} else if matches!(k, EK::Ogre | EK::Paladin | EK::Jailer) {
} else if matches!(k, EK::Ogre | EK::Paladin | EK::Jailer | EK::Gargoyle) {
0.8
} else {
0.62
@@ -1242,7 +1246,10 @@ fn draw_gear(g: &Game, cam: &Cam) {
let toward = cam.eye_dir();
for e in g.ents.iter().filter(|e| gear_shown(e)) {
let w = sprite_size(e.kind);
let bob = if e.moving { (e.anim * 12.0).sin().abs() * 0.06 } else { 0.0 };
let mut bob = if e.moving { (e.anim * 12.0).sin().abs() * 0.06 } else { 0.0 };
if flies(e.kind) {
bob = 0.45;
}
let base = vec3(e.x, bob, e.y) + toward * 0.5;
let at = |dx: f32, dy: f32| base + r * (dx * w) + Vec3::Y * (dy * w);
let metal = if e.level >= 8 {
@@ -1709,6 +1716,7 @@ pub fn minimap_color(tile: &Tile) -> [u8; 4] {
Kind::Bridge => [150, 110, 60, 255],
Kind::LavaBridge => [130, 120, 115, 255],
Kind::Shard => [255, 60, 120, 255],
Kind::Aerie => [170, 140, 90, 255],
}
}