// Siege engines: battering rams smash doors, walls and towers; catapults hurl stones at towers // from afar. The knights bring them along in later waves; a master with a forge builds his own // (gold and wood) to break a fortress: they follow the attack flag and go for the gate and the // knights' towers. use crate::game::*; use crate::map::*; // cost of a ram and a catapult: gold, wood pub const RAM_COST: (f32, f32) = (250.0, 15.0); pub const CATAPULT_COST: (f32, f32) = (350.0, 25.0); pub fn siege(k: EK) -> bool { matches!(k, EK::Ram | EK::Catapult) } // how much harder a unit hits towers pub fn tower_breaker(k: EK) -> f32 { match k { EK::Catapult => 2.5, EK::Ram => 2.0, _ => 1.0, } } impl Game { // the siege tool: a ram (0) or a catapult (1) rolls out on own ground (a forge is needed) pub fn build_siege(&mut self, which: u8, x: i32, y: i32) { if !inb(x, y) || !self.map.walk(x, y) || self.map.t[idx(x, y)].owner != self.me { self.hint(34, "hint.siege_where"); self.sfx_ui(Sfx::Error); return; } if self.forge_tiles() == 0 { self.hint(26, "hint.need_forge"); self.sfx_ui(Sfx::Error); return; } let (kind, (gold, wood)) = if which == 0 { (EK::Ram, RAM_COST) } else { (EK::Catapult, CATAPULT_COST) }; if self.gold < gold { self.msg("msg.no_gold", vec![]); self.sfx_ui(Sfx::Error); return; } if !self.spend_wood(wood) { self.msg("msg.no_wood", vec![]); self.sfx_ui(Sfx::Error); return; } self.gold -= gold; self.count(crate::stats::SIEGE, 1.0); let id = self.spawn(kind, false, x as f32 + 0.5, y as f32 + 0.5, 1); if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) { e.side = self.me_side; e.home = Some((x as f32 + 0.5, y as f32 + 0.5)); e.settled = true; e.happy = 1.0; } self.fx(8, x as f32 + 0.5, y as f32 + 0.5, 0.0, 0.0, 0.5, 0xff40d0ff); self.sfx(Sfx::Build, x as f32 + 0.5, y as f32 + 0.5); self.msg("msg.siege_built", vec![format!("@{}", st(kind).name)]); } // a master's own siege engine: no needs; it goes where the attack flag is and breaks the // gate and the knights' towers there (a catapult shoots any knight tower in reach) pub(crate) fn siege_ai(&mut self, e: &mut Ent, dt: f32) { let s = st(e.kind); // a knight tower in reach (the catapult from afar, the ram next to it) if e.think <= 0.0 { e.think = 0.5; let reach = if e.kind == EK::Catapult { s.range + 0.5 } else { 2.5 }; let near = self .towers .iter() .filter(|t| t.hero) .map(|t| (t.id, (t.pos().0 - e.x).powi(2) + (t.pos().1 - e.y).powi(2))) .filter(|&(_, d2)| d2 < reach * reach) .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)) .map(|t| t.0); if let Some(id) = near { e.job = Job::AttackTower(id); e.path.clear(); } } if let Job::AttackTower(id) = e.job { if self.attack_tower(e, id, dt) { return; } e.job = Job::None; } let Some(f) = self.flag else { // no flag: wait at home self.step_path(e, dt); return; }; let (fx, fy) = xy(f); let (fx, fy) = (fx as f32 + 0.5, fy as f32 + 0.5); // the gate of the order near the flag let (gx, gy) = self.gate_center(); if self.gate_hp > 0.0 && self.has_gate() && (fx - gx).powi(2) + (fy - gy).powi(2) <= 64.0 { let dg = ((e.x - gx).powi(2) + (e.y - gy).powi(2)).sqrt(); if dg <= s.range + 1.6 && (s.range < 1.5 || self.los(e.x, e.y, gx, gy)) { e.path.clear(); e.face = if gx > e.x { 1.0 } else { -1.0 }; e.working = true; if e.cd <= 0.0 { self.hit_gate(e); e.cd = s.interval; } return; } } let d2 = (e.x - fx).powi(2) + (e.y - fy).powi(2); if d2 > 2.0 * 2.0 && (e.path.is_empty() || e.repath <= 0.0) { e.repath = 1.5; if let Some(p) = self.map.path_to(e.tile(), f) { e.path = p; } } self.step_path(e, dt); } } #[cfg(test)] mod tests { use super::*; #[test] fn a_master_builds_a_catapult_that_shoots_knight_towers() { let mut g = Game::new(6, 80, 64); let (cx, cy) = g.map.core; g.gold = 5000.0; g.wood = 100.0; // own ground with a forge for y in cy - 6..=cy + 8 { for x in cx - 2..=cx + 14 { let t = &mut g.map.t[idx(x, y)]; if !matches!(t.kind, Kind::Core) { t.kind = Kind::Floor; t.owner = Owner::Player; } } } g.build_rect(8, cx + 1, cy + 5, cx + 4, cy + 7); g.build_siege(1, cx + 6, cy + 2); let cat = g.ents.iter().position(|e| e.kind == EK::Catapult).expect("a catapult"); assert_eq!(g.ents[cat].side, 0); assert!(g.gold < 5000.0 && g.wood < 100.0); // a knight tower in reach let i = idx(cx + 12, cy + 2); g.map.t[i].kind = Kind::Tower; g.map.t[i].owner = Owner::Hero; let id = g.next_id; g.next_id += 1; let mut tw = Tower { id, kind: TowerKind::Arrow, tile: i, level: 1, hp: 1.0, cd: 9.0, aim: 0.0, flash: 0.0, hero: true, rune_final: false }; tw.hp = tw.max_hp(); let hp0 = tw.hp; g.towers.push(tw); for _ in 0..(15 * 20) { g.update(0.05); } let hp = g.towers.iter().find(|t| t.id == id).map_or(0.0, |t| t.hp); assert!(hp < hp0, "the catapult hit the tower"); } }