// Bosses: every fifth wave is led by one of the Order's champions - a paladin with a name, more // than twice the health, an ability of his own and loot for whoever brings him down (gold and a // free rune forge tower). His health bar stands at the top of the screen while he lives. use crate::game::*; use crate::particles::*; // the champions (Ent::boss = index + 1): ability cooldowns in seconds pub const BOSSES: [(&str, f32); 4] = [("aldric", 12.0), ("seraphine", 10.0), ("halvard", 18.0), ("rothgar", 9.0)]; pub const BOSS_HP: f32 = 2.5; // health factor pub const BOSS_GOLD: f32 = 600.0; pub fn boss_name(b: u8) -> String { format!("@boss.{}", BOSSES[(b.max(1) as usize - 1) % BOSSES.len()].0) } impl Game { // the unit at index j leads the wave as champion number `b` (1..=4) pub(crate) fn make_boss(&mut self, j: usize, b: u8) { let e = &mut self.ents[j]; e.boss = b; e.max_hp *= BOSS_HP; e.hp = e.max_hp; e.ab_cd = 4.0; let name = boss_name(b); self.msg("msg.boss_comes", vec![name]); } // the champion's ability, used by itself whenever it is ready pub(crate) fn boss_ability(&mut self, e: &mut Ent, dt: f32) { e.ab_cd -= dt; if e.ab_cd > 0.0 || e.captive != 0 { return; } let (bx, by) = (e.x, e.y); let idx_b = (e.boss.max(1) as usize - 1) % BOSSES.len(); let foe = |o: &Ent| o.alive && !o.held && !o.hero && o.captive == 0 && !o.chained; let near = |o: &Ent, r: f32| (o.x - bx).powi(2) + (o.y - by).powi(2) < r * r; let used = match idx_b { // Sir Aldric: shield wall - heroes around are healed and shielded 0 => { for o in self.ents.iter_mut().filter(|o| o.alive && o.hero && (o.x - bx).powi(2) + (o.y - by).powi(2) < 16.0) { o.hp = (o.hp + 60.0).min(o.max_hp); o.shield = o.shield.max(6.0); } self.part(RING, (bx, by, 0.1), (0.0, 0.0, 0.0), 0.7, abgr(255, 230, 140), 4.0); true } // Dame Seraphine: radiant burst - the dungeon's units around are burnt by light 1 => { let hit: Vec = (0..self.ents.len()).filter(|&k| foe(&self.ents[k]) && near(&self.ents[k], 3.0)).collect(); if hit.is_empty() { false } else { for k in hit { self.damage_unit(k, 45.0); } self.part(FLASH, (bx, by, 0.5), (0.0, 0.0, 0.0), 0.5, abgr(255, 250, 200), 3.0); true } } // Brother Halvard: call to arms - two knights join him 2 => { let level = e.level; for k in 0..2 { let a = k as f32 * 3.1; let id = self.spawn(EK::Knight, true, bx + a.cos(), by + a.sin(), level.max(2) - 1); if let Some(o) = self.ents.iter_mut().find(|o| o.id == id) { o.target = e.target; } } self.fx(4, bx, by, 0.0, 0.0, 0.8, 0xffffe0a0); true } // Captain Rothgar: charge - he rushes at an enemy, hits hard and stuns _ => { let target = (0..self.ents.len()).filter(|&k| foe(&self.ents[k]) && near(&self.ents[k], 6.0)).min_by(|&a, &b| { let da = (self.ents[a].x - bx).powi(2) + (self.ents[a].y - by).powi(2); let db = (self.ents[b].x - bx).powi(2) + (self.ents[b].y - by).powi(2); da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal) }); match target { Some(k) => { let (tx, ty) = (self.ents[k].x, self.ents[k].y); let d = ((tx - bx).powi(2) + (ty - by).powi(2)).sqrt().max(0.01); if self.map.walk((tx - (tx - bx) / d * 0.8) as i32, (ty - (ty - by) / d * 0.8) as i32) { e.x = tx - (tx - bx) / d * 0.8; e.y = ty - (ty - by) / d * 0.8; e.path.clear(); } self.ents[k].stun = self.ents[k].stun.max(1.5); self.damage_unit(k, 50.0); for _ in 0..10 { let v = (self.vrng.f() * 2.0 - 1.0, self.vrng.f() * 2.0 - 1.0, 0.5); self.parts.push(Part { kind: DEBRIS, x: bx, y: by, h: 0.1, vx: v.0, vy: v.1, vh: v.2, t: 0.0, max: 0.8, col: abgr(150, 130, 110), size: 0.05 }); } true } None => false, } } }; e.ab_cd = if used { BOSSES[idx_b].1 } else { 1.0 }; } // a champion fell (or was captured): his loot pub(crate) fn boss_loot(&mut self, j: usize) { let b = self.ents[j].boss; if b == 0 { return; } self.ents[j].boss = 0; self.count_local(crate::stats::BOSSES); let got = self.add_gold(BOSS_GOLD); self.rune_free += 1; let (x, y) = (self.ents[j].x, self.ents[j].y); self.fx(7, x, y, 0.0, 0.0, 1.2, 0xff40e0ff); self.sfx_ui(Sfx::Victory); self.msg("msg.boss_slain", vec![boss_name(b), (got as i32).to_string()]); } // the living champion with the most health (for the bar at the top) pub fn boss_alive(&self) -> Option<&Ent> { self.ents.iter().filter(|e| e.alive && e.boss > 0 && e.captive == 0).max_by(|a, b| a.hp.partial_cmp(&b.hp).unwrap_or(std::cmp::Ordering::Equal)) } } #[cfg(test)] mod tests { use super::*; #[test] fn every_fifth_wave_brings_a_champion_with_loot() { let mut g = Game::new(8, 80, 64); g.wave = 4; // the next wave is the fifth let before = g.ents.len(); g.spawn_wave(); let boss = g.ents[before..].iter().position(|e| e.boss > 0).map(|k| k + before).expect("a champion"); assert!(g.ents[boss].max_hp > st(g.ents[boss].kind).hp * 2.0); assert!(g.boss_alive().is_some()); let (gold, runes) = (g.gold, g.rune_free); g.boss_loot(boss); assert!(g.gold > gold && g.rune_free == runes + 1); assert!(g.boss_alive().is_none()); } #[test] fn champions_use_their_abilities() { for b in 1..=4u8 { let mut g = Game::new(8, 80, 64); let (cx, cy) = g.map.core; let (x, y) = (cx as f32 + 1.5, cy as f32 + 4.5); let id = g.spawn(EK::Paladin, true, x, y, 5); let j = g.ents.iter().position(|e| e.id == id).unwrap(); g.make_boss(j, b); // an ogre of the dungeon close by g.spawn(EK::Ogre, false, x + 1.5, y, 3); let n0 = g.ents.len(); let mut e = g.ents[j].clone(); e.ab_cd = 0.0; e.hp = e.max_hp * 0.5; g.boss_ability(&mut e, 0.1); assert!(e.ab_cd > 1.5, "champion {} used his ability", b); if b == 3 { assert_eq!(g.ents.len(), n0 + 2, "two knights joined"); } } } }