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:
1 parent
729dd4fe4c
commit
b7d7fc2b52
10 files changed
+454
-56
No files matched your search
+256
-36
@@ -85,7 +85,7 @@ pub struct SfxEvent {
|
||||
pub const MANA_MAX: f32 = 400.0;
|
||||
pub const CARRY_MAX: i32 = 300;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
|
||||
pub enum EK {
|
||||
Worker,
|
||||
Kobold,
|
||||
@@ -100,6 +100,11 @@ pub enum EK {
|
||||
Jailer, // attracted by the torture chamber
|
||||
Priestess, // attracted by the temple
|
||||
Chainbreaker, // rare and very strong
|
||||
// flying creatures, attracted by the aerie
|
||||
Bat, // fast and weak
|
||||
Gargoyle, // aerie + torture chamber: stone, tough
|
||||
Owl, // aerie + library: arcane bolts from afar
|
||||
Wyrm, // big aerie + treasury: rare, very strong, fire breath
|
||||
}
|
||||
|
||||
pub struct St {
|
||||
@@ -130,9 +135,18 @@ pub fn st(k: EK) -> St {
|
||||
EK::Jailer => s("unit.jailer", 260.0, 16.0, 1.0, 1.6, 1.3, 140, 0),
|
||||
EK::Priestess => s("unit.priestess", 70.0, 14.0, 5.0, 2.0, 1.6, 120, 0),
|
||||
EK::Chainbreaker => s("unit.chainbreaker", 600.0, 36.0, 1.1, 2.3, 1.1, 400, 0),
|
||||
EK::Bat => s("unit.bat", 55.0, 6.0, 1.0, 3.6, 0.7, 30, 0),
|
||||
EK::Gargoyle => s("unit.gargoyle", 260.0, 15.0, 1.0, 2.2, 1.2, 120, 0),
|
||||
EK::Owl => s("unit.owl", 70.0, 12.0, 4.5, 2.8, 1.3, 100, 0),
|
||||
EK::Wyrm => s("unit.wyrm", 650.0, 30.0, 3.0, 2.5, 1.4, 380, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// flying creatures cross water and lava and sleep in the aerie
|
||||
pub fn flies(k: EK) -> bool {
|
||||
matches!(k, EK::Bat | EK::Gargoyle | EK::Owl | EK::Wyrm)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
pub enum Job {
|
||||
None,
|
||||
@@ -155,6 +169,7 @@ pub enum Job {
|
||||
Pray(usize), // temple tile
|
||||
HealCore, // flame priestess heals the soul core
|
||||
TearBridge(usize), // knight tears down a bridge of the player
|
||||
Rest(usize), // flying creature recovers in the aerie (flees when badly hurt)
|
||||
Help(usize), // imp with nothing else to do helps another imp at this tile
|
||||
}
|
||||
|
||||
@@ -334,7 +349,8 @@ pub struct Game {
|
||||
pub state: u8, // 0 running, 1 victory, 2 defeat
|
||||
pub paused: bool,
|
||||
pub hand: Vec<u32>,
|
||||
pub counts: [usize; 7], // treasury, lair, farm, training hall, library, torture chamber, temple
|
||||
#[serde(skip)]
|
||||
pub counts: [usize; 8], // treasury, lair, farm, training hall, library, torture chamber, temple, aerie (recounted)
|
||||
pub gate_hp: f32,
|
||||
pub gate_hit_t: f32,
|
||||
pub gate_stage: u32, // defender thresholds triggered (0..3)
|
||||
@@ -395,9 +411,9 @@ impl Torch {
|
||||
}
|
||||
}
|
||||
|
||||
pub const ROOM_KINDS: [Kind; 7] = [Kind::Treasury, Kind::Lair, Kind::Farm, Kind::Training, Kind::Library, Kind::Torture, Kind::Temple];
|
||||
pub const ROOM_COST: [i32; 7] = [60, 80, 100, 140, 160, 200, 180];
|
||||
pub const ROOM_NAMES: [&str; 7] = ["room.treasury", "room.lair", "room.farm", "room.training", "room.library", "room.torture", "room.temple"];
|
||||
pub const ROOM_KINDS: [Kind; 8] = [Kind::Treasury, Kind::Lair, Kind::Farm, Kind::Training, Kind::Library, Kind::Torture, Kind::Temple, Kind::Aerie];
|
||||
pub const ROOM_COST: [i32; 8] = [60, 80, 100, 140, 160, 200, 180, 120];
|
||||
pub const ROOM_NAMES: [&str; 8] = ["room.treasury", "room.lair", "room.farm", "room.training", "room.library", "room.torture", "room.temple", "room.aerie"];
|
||||
|
||||
// bridge cost per tile: over water, over lava
|
||||
pub const BRIDGE_COST: [i32; 2] = [30, 75];
|
||||
@@ -482,7 +498,7 @@ impl Game {
|
||||
state: 0,
|
||||
paused: false,
|
||||
hand: Vec::new(),
|
||||
counts: [0; 7],
|
||||
counts: [0; 8],
|
||||
gate_hp: GATE_HP,
|
||||
gate_hit_t: -100.0,
|
||||
gate_stage: 0,
|
||||
@@ -630,7 +646,7 @@ impl Game {
|
||||
}
|
||||
|
||||
pub fn recount(&mut self) {
|
||||
let mut c = [0usize; 7];
|
||||
let mut c = [0usize; 8];
|
||||
for t in &self.map.t {
|
||||
if t.owner == Owner::Player {
|
||||
if let Some(r) = room_index(t.kind) {
|
||||
@@ -1011,19 +1027,37 @@ impl Game {
|
||||
|
||||
// free beds in the lairs: much room lets creatures arrive faster, little room slower
|
||||
pub fn free_beds(&self) -> usize {
|
||||
self.counts[1].min(30).saturating_sub(self.creature_count())
|
||||
let (lair, aerie) = self.free_lair_aerie();
|
||||
lair + aerie
|
||||
}
|
||||
|
||||
// free places in the lairs (walking creatures) and in the aerie (flying creatures)
|
||||
pub fn free_lair_aerie(&self) -> (usize, usize) {
|
||||
let flyers = self.ents.iter().filter(|e| e.alive && e.creature() && flies(e.kind)).count();
|
||||
let walkers = self.creature_count() - flyers;
|
||||
(self.counts[1].min(30).saturating_sub(walkers), self.counts[7].min(20).saturating_sub(flyers))
|
||||
}
|
||||
|
||||
pub fn lair_factor(&self) -> f32 {
|
||||
(8.0 / (self.free_beds() as f32 + 2.0)).clamp(0.35, 3.0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn try_portal_for_test(&mut self) {
|
||||
let (px, py) = self.map.portals[0];
|
||||
for y in 0..3 {
|
||||
for x in 0..3 {
|
||||
self.map.t[idx(px + x, py + y)].owner = Owner::Player;
|
||||
}
|
||||
}
|
||||
self.try_portal();
|
||||
}
|
||||
|
||||
fn try_portal(&mut self) {
|
||||
let creatures = self.creature_count();
|
||||
let lair = self.counts[1];
|
||||
let aerie = self.counts[7];
|
||||
let farm = self.counts[2];
|
||||
let cap = lair.min(30);
|
||||
if lair == 0 {
|
||||
if lair == 0 && aerie == 0 {
|
||||
self.hint(0, "hint.portal_no_lair");
|
||||
return;
|
||||
}
|
||||
@@ -1031,11 +1065,46 @@ impl Game {
|
||||
self.hint(1, "hint.no_farm");
|
||||
return;
|
||||
}
|
||||
if creatures >= cap {
|
||||
let (free_lair, free_aerie) = self.free_lair_aerie();
|
||||
if free_lair == 0 && free_aerie == 0 {
|
||||
self.hint(2, "hint.lair_full");
|
||||
return;
|
||||
}
|
||||
let mut opts: Vec<(EK, u32)> = vec![(EK::Kobold, 4)];
|
||||
let mut opts: Vec<(EK, u32)> = Vec::new();
|
||||
// flying creatures need room in the aerie
|
||||
if free_aerie > 0 {
|
||||
if aerie >= 4 {
|
||||
opts.push((EK::Bat, 4));
|
||||
}
|
||||
if aerie >= 6 && self.counts[5] >= 4 {
|
||||
opts.push((EK::Gargoyle, 2));
|
||||
}
|
||||
if aerie >= 6 && self.counts[4] >= 6 {
|
||||
opts.push((EK::Owl, 2));
|
||||
}
|
||||
let has_wyrm = self.ents.iter().any(|e| e.alive && e.kind == EK::Wyrm && !e.hero);
|
||||
if aerie >= 16 && self.counts[0] >= 16 && !has_wyrm {
|
||||
opts.push((EK::Wyrm, 1));
|
||||
}
|
||||
}
|
||||
if free_lair == 0 {
|
||||
if opts.is_empty() {
|
||||
self.hint(2, "hint.lair_full");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
self.portal_walkers(&mut opts);
|
||||
}
|
||||
if opts.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.portal_arrival(opts);
|
||||
}
|
||||
|
||||
// walking creatures that the rooms attract
|
||||
fn portal_walkers(&mut self, opts: &mut Vec<(EK, u32)>) {
|
||||
let farm = self.counts[2];
|
||||
opts.push((EK::Kobold, 4));
|
||||
if farm >= 6 {
|
||||
opts.push((EK::Spider, 3));
|
||||
}
|
||||
@@ -1058,6 +1127,9 @@ impl Game {
|
||||
if room_tiles >= 40 && veteran && self.gold >= 4000.0 && !has_one {
|
||||
opts.push((EK::Chainbreaker, 1));
|
||||
}
|
||||
}
|
||||
|
||||
fn portal_arrival(&mut self, opts: Vec<(EK, u32)>) {
|
||||
let total: u32 = opts.iter().map(|o| o.1).sum();
|
||||
let mut r = self.rng.next() % total;
|
||||
let mut kind = EK::Kobold;
|
||||
@@ -1313,7 +1385,7 @@ impl Game {
|
||||
}
|
||||
let next = e.path[0];
|
||||
let tile = self.map.t[next];
|
||||
if !tile.walkable() {
|
||||
if !tile.walkable() && !(flies(e.kind) && tile.liquid()) {
|
||||
if e.hero && tile.diggable() {
|
||||
// heroes dig their way through
|
||||
e.working = true;
|
||||
@@ -1470,7 +1542,8 @@ impl Game {
|
||||
if e.path.is_empty() || e.repath <= 0.0 {
|
||||
e.repath = 0.6;
|
||||
let tt = idx(tx as i32, ty as i32);
|
||||
match self.map.path_to(e.tile(), tt) {
|
||||
let p = if flies(e.kind) { self.map.fly_path_to(e.tile(), tt) } else { self.map.path_to(e.tile(), tt) };
|
||||
match p {
|
||||
Some(p) => e.path = p,
|
||||
None => return false,
|
||||
}
|
||||
@@ -1496,9 +1569,13 @@ impl Game {
|
||||
EK::Mage => 0xffff44cc,
|
||||
EK::Priest => 0xff88ffff,
|
||||
EK::Priestess => 0xff3080ff,
|
||||
EK::Owl => 0xffffa060,
|
||||
EK::Wyrm => 0xff2080ff,
|
||||
_ => 0xff70b0d0,
|
||||
};
|
||||
self.fx(0, e.x, e.y, tx, ty, 0.25, col);
|
||||
// flyers shoot from the air
|
||||
let h = if flies(e.kind) { 0.8 } else { 0.45 };
|
||||
self.shot(e.x, e.y, h, tx, ty, 0.45, 0.25, col);
|
||||
self.sfx(Sfx::Shoot, e.x, e.y);
|
||||
} else {
|
||||
self.fx(2, tx, ty, 0.0, 0.0, 0.25, 0xffffffff);
|
||||
@@ -1524,8 +1601,8 @@ impl Game {
|
||||
}
|
||||
}
|
||||
}
|
||||
if e.kind == EK::Priestess {
|
||||
// fireball: half damage to enemies around the target
|
||||
if e.kind == EK::Priestess || e.kind == EK::Wyrm {
|
||||
// fireball / fire breath: half damage to enemies around the target
|
||||
self.fx(3, tx, ty, 0.0, 0.0, 0.5, 0xff3080ff);
|
||||
let side = !e.hero;
|
||||
let splash: Vec<usize> = self
|
||||
@@ -1651,6 +1728,15 @@ impl Game {
|
||||
let e = &self.ents[j];
|
||||
let (nx, ny) = (e.x + dx, e.y + dy);
|
||||
let Some(t) = self.map.get(nx as i32, ny as i32).copied() else { return };
|
||||
if flies(self.ents[j].kind) {
|
||||
if t.walkable() || t.liquid() {
|
||||
let e = &mut self.ents[j];
|
||||
e.x = nx;
|
||||
e.y = ny;
|
||||
e.path.clear();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if t.kind == Kind::Lava {
|
||||
let e = &mut self.ents[j];
|
||||
e.x = nx;
|
||||
@@ -1670,7 +1756,7 @@ impl Game {
|
||||
let (x, y) = xy(i);
|
||||
for j in 0..self.ents.len() {
|
||||
let e = &self.ents[j];
|
||||
if !e.alive || e.held || e.tile() != i {
|
||||
if !e.alive || e.held || e.tile() != i || flies(e.kind) {
|
||||
continue;
|
||||
}
|
||||
if lava {
|
||||
@@ -1973,6 +2059,38 @@ impl Game {
|
||||
false
|
||||
}
|
||||
|
||||
// flying creatures patrol far around (also over rivers and the land of the enemy)
|
||||
fn patrol_flight(&mut self, e: &mut Ent) {
|
||||
let (x, y) = xy(e.tile());
|
||||
for _ in 0..8 {
|
||||
let nx = x + self.rng.range(-14, 14);
|
||||
let ny = y + self.rng.range(-14, 14);
|
||||
let ok = self.map.get(nx, ny).map_or(false, |t| t.walkable() || t.liquid());
|
||||
if ok {
|
||||
if let Some(p) = self.map.fly_path_to(e.tile(), idx(nx, ny)) {
|
||||
if p.len() <= 40 {
|
||||
e.path = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
e.job = Job::Wander;
|
||||
e.timer = 2.0 + self.rng.f() * 3.0;
|
||||
}
|
||||
|
||||
// badly hurt flyers flee to the aerie and rest until they are healed to 75%
|
||||
fn start_rest(&mut self, e: &mut Ent) -> bool {
|
||||
let cur = e.tile();
|
||||
let target = e.bed.filter(|&b| self.map.t[b].kind == Kind::Aerie).or_else(|| {
|
||||
self.map.search(cur, self.map.fly_cost(), |i| self.map.t[i].kind == Kind::Aerie, true).map(|p| p.last().copied().unwrap_or(cur))
|
||||
});
|
||||
let Some(b) = target else { return false };
|
||||
e.path = self.map.fly_path_to(cur, b).unwrap_or_default();
|
||||
e.job = Job::Rest(b);
|
||||
true
|
||||
}
|
||||
|
||||
fn wander(&mut self, e: &mut Ent, radius: i32) {
|
||||
let (x, y) = xy(e.tile());
|
||||
for _ in 0..6 {
|
||||
@@ -2303,9 +2421,13 @@ impl Game {
|
||||
if e.think <= 0.0 {
|
||||
e.think = 0.4 + self.rng.f() * 0.3;
|
||||
// a new creature builds its own bed in the lair soon after its arrival
|
||||
if e.bed.is_none() && self.counts[1] > 0 && !matches!(e.job, Job::Attack(_) | Job::Leave | Job::Rally) && self.rng.f() < 0.15 {
|
||||
let bed_kind = if flies(e.kind) { Kind::Aerie } else { Kind::Lair };
|
||||
let beds = if flies(e.kind) { self.counts[7] } else { self.counts[1] };
|
||||
if e.bed.is_none() && beds > 0 && !matches!(e.job, Job::Attack(_) | Job::Leave | Job::Rally) && self.rng.f() < 0.15 {
|
||||
let cur = e.tile();
|
||||
if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.map.t[i].kind == Kind::Lair && self.map.t[i].bed == 0, true) {
|
||||
let free = |i: usize| self.map.t[i].kind == bed_kind && self.map.t[i].bed == 0;
|
||||
let found = if flies(e.kind) { self.map.search(cur, self.map.fly_cost(), free, true) } else { self.map.search(cur, self.map.walk_cost(), free, true) };
|
||||
if let Some(p) = found {
|
||||
let b = p.last().copied().unwrap_or(cur);
|
||||
self.map.t[b].bed = me;
|
||||
e.bed = Some(b);
|
||||
@@ -2313,8 +2435,15 @@ impl Game {
|
||||
self.fx(8, bx as f32 + 0.5, by as f32 + 0.5, 0.0, 0.0, 0.6, 0xff40d0ff);
|
||||
}
|
||||
}
|
||||
if !matches!(e.job, Job::Attack(_) | Job::Leave) {
|
||||
let radius = if e.job == Job::Rally { 8.5 } else { 7.0 };
|
||||
// flyers flee when badly hurt and come back at 75%
|
||||
if flies(e.kind) && e.hp < e.max_hp * 0.25 && !matches!(e.job, Job::Rest(_) | Job::Leave) {
|
||||
self.unreserve(me);
|
||||
if self.start_rest(e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if !matches!(e.job, Job::Attack(_) | Job::Leave | Job::Rest(_)) {
|
||||
let radius = if e.job == Job::Rally { 8.5 } else if flies(e.kind) { 9.0 } else { 7.0 };
|
||||
if let Some(id) = self.nearest_enemy(e, radius) {
|
||||
self.unreserve(me);
|
||||
e.job = Job::Attack(id);
|
||||
@@ -2338,6 +2467,28 @@ impl Game {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if let Job::Rest(b) = e.job {
|
||||
if self.step_path(e, dt) {
|
||||
return;
|
||||
}
|
||||
let (bx, by) = xy(b);
|
||||
let at_aerie = (e.x - bx as f32 - 0.5).abs() < 1.5 && (e.y - by as f32 - 0.5).abs() < 1.5;
|
||||
if !at_aerie {
|
||||
// pushed away on the way: fly on, or give up if there is no way
|
||||
match self.map.fly_path_to(e.tile(), b) {
|
||||
Some(p) if !p.is_empty() => e.path = p,
|
||||
_ => e.job = Job::None,
|
||||
}
|
||||
return;
|
||||
}
|
||||
// resting in the aerie heals quickly, back to work at 75%
|
||||
e.hp = (e.hp + e.max_hp * dt / 12.0).min(e.max_hp);
|
||||
e.energy = (e.energy + dt / 14.0).min(1.0);
|
||||
if e.hp >= e.max_hp * 0.75 {
|
||||
e.job = Job::None;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if e.kind == EK::Chainbreaker {
|
||||
// restless: without a fight for two minutes its mood sinks (skill_cd counts down as idle time)
|
||||
if matches!(e.job, Job::Attack(_)) {
|
||||
@@ -2529,7 +2680,8 @@ impl Game {
|
||||
}
|
||||
if e.path.is_empty() || e.repath <= 0.0 {
|
||||
e.repath = 1.5;
|
||||
match self.map.path_to(e.tile(), target) {
|
||||
let p = if flies(e.kind) { self.map.fly_path_to(e.tile(), target) } else { self.map.path_to(e.tile(), target) };
|
||||
match p {
|
||||
Some(p) => e.path = p,
|
||||
None => {
|
||||
self.hint(11, "hint.flag_unreachable");
|
||||
@@ -2592,22 +2744,20 @@ impl Game {
|
||||
return;
|
||||
}
|
||||
if e.energy < 0.25 {
|
||||
// flying creatures sleep in the aerie
|
||||
let bed_kind = if flies(e.kind) { Kind::Aerie } else { Kind::Lair };
|
||||
if let Some(b) = e.bed {
|
||||
let t = &self.map.t[b];
|
||||
if t.kind != Kind::Lair || t.bed != me {
|
||||
if t.kind != bed_kind || t.bed != me {
|
||||
e.bed = None;
|
||||
}
|
||||
}
|
||||
if e.bed.is_none() {
|
||||
let found = self.map.search(
|
||||
cur,
|
||||
self.map.walk_cost(),
|
||||
|i| {
|
||||
let t = &self.map.t[i];
|
||||
t.kind == Kind::Lair && t.bed == 0
|
||||
},
|
||||
true,
|
||||
);
|
||||
let free = |i: usize| {
|
||||
let t = &self.map.t[i];
|
||||
t.kind == bed_kind && t.bed == 0
|
||||
};
|
||||
let found = if flies(e.kind) { self.map.search(cur, self.map.fly_cost(), free, true) } else { self.map.search(cur, self.map.walk_cost(), free, true) };
|
||||
if let Some(p) = found {
|
||||
let b = p.last().copied().unwrap_or(cur);
|
||||
self.map.t[b].bed = me;
|
||||
@@ -2615,7 +2765,8 @@ impl Game {
|
||||
}
|
||||
}
|
||||
if let Some(b) = e.bed {
|
||||
if let Some(p) = self.map.path_to(cur, b) {
|
||||
let way = if flies(e.kind) { self.map.fly_path_to(cur, b) } else { self.map.path_to(cur, b) };
|
||||
if let Some(p) = way {
|
||||
e.path = p;
|
||||
e.job = Job::Sleep(b);
|
||||
return;
|
||||
@@ -2692,7 +2843,11 @@ impl Game {
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.wander(e, 4);
|
||||
if flies(e.kind) {
|
||||
self.patrol_flight(e);
|
||||
} else {
|
||||
self.wander(e, 4);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- prisoners
|
||||
@@ -3910,6 +4065,7 @@ pub fn job_name(e: &Ent) -> &'static str {
|
||||
Job::Study(_) => "job.study",
|
||||
Job::Attack(_) => "job.fight",
|
||||
Job::TearBridge(_) => "job.tear_bridge",
|
||||
Job::Rest(_) => "job.rest",
|
||||
Job::Help(_) => "job.help",
|
||||
Job::AttackCore => {
|
||||
if e.working {
|
||||
@@ -4382,6 +4538,70 @@ mod tests {
|
||||
assert!(best <= 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flyers_cross_rivers_flee_and_heal() {
|
||||
let (mut g, rx) = river_game(true);
|
||||
let (cx, cy) = g.map.core;
|
||||
// an aerie in the start area
|
||||
g.gold = 5000.0;
|
||||
g.build_rect(7, cx - 3, cy - 3, cx - 2, cy + 2);
|
||||
assert!(g.counts[7] >= 8);
|
||||
// a knight on the far side of the lava: an owl reaches it, a kobold does not
|
||||
let far = (rx + 4, cy);
|
||||
for x in rx + 3..rx + 7 {
|
||||
let t = &mut g.map.t[idx(x, cy)];
|
||||
t.kind = Kind::Floor;
|
||||
t.owner = Owner::None;
|
||||
}
|
||||
let k = g.spawn(EK::Knight, true, far.0 as f32 + 0.5, far.1 as f32 + 0.5, 1);
|
||||
let owl = g.spawn(EK::Owl, false, (cx + 4) as f32 + 0.5, cy as f32 + 0.5, 3);
|
||||
assert!(g.map.fly_path_to(idx(cx + 4, cy), idx(far.0, far.1)).is_some());
|
||||
assert!(g.map.path_to(idx(cx + 4, cy), idx(far.0, far.1)).is_none());
|
||||
let hp0 = g.ents.iter().find(|e| e.id == k).unwrap().hp;
|
||||
for _ in 0..300 {
|
||||
g.update(0.05);
|
||||
}
|
||||
let hit = g.ents.iter().find(|e| e.id == k).map_or(true, |e| e.hp < hp0);
|
||||
assert!(hit, "the owl attacks across the lava");
|
||||
// a badly hurt flyer flees to the aerie and heals up to 75%
|
||||
g.ents.retain(|e| !e.hero);
|
||||
let j = g.ents.iter().position(|e| e.id == owl).unwrap();
|
||||
g.ents[j].hp = g.ents[j].max_hp * 0.2;
|
||||
let mut rested = false;
|
||||
for _ in 0..800 {
|
||||
g.update(0.05);
|
||||
let e = g.ents.iter().find(|e| e.id == owl).unwrap();
|
||||
rested |= matches!(e.job, Job::Rest(_));
|
||||
if e.hp >= e.max_hp * 0.75 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let e = g.ents.iter().find(|e| e.id == owl).unwrap();
|
||||
assert!(rested && e.hp >= e.max_hp * 0.74, "fled and healed: {} / {}", e.hp, e.max_hp);
|
||||
// lava does not burn flyers
|
||||
let j = g.ents.iter().position(|e| e.id == owl).unwrap();
|
||||
g.ents[j].x = (rx - 1) as f32 + 0.5;
|
||||
g.ents[j].y = cy as f32 + 0.5;
|
||||
g.knock(j, 1.0, 0.0);
|
||||
assert!(g.ents[j].alive);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aerie_attracts_flyers() {
|
||||
let mut g = Game::new(3, 64, 64);
|
||||
g.counts = [16, 0, 9, 0, 6, 4, 0, 16];
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for _ in 0..200 {
|
||||
g.try_portal_for_test();
|
||||
for e in g.ents.iter().filter(|e| e.creature()) {
|
||||
seen.insert(e.kind);
|
||||
}
|
||||
g.ents.retain(|e| !e.creature());
|
||||
}
|
||||
assert!(seen.contains(&EK::Bat) && seen.contains(&EK::Gargoyle) && seen.contains(&EK::Owl), "{:?}", seen);
|
||||
assert!(seen.iter().all(|&k| flies(k)), "no lair: only flyers come ({:?})", seen);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imps_reinforce_walls() {
|
||||
let mut g = Game::new(3, 64, 64);
|
||||
|
||||
Reference in new issue
Block a user