166 lines
6.0 KiB
Rust
166 lines
6.0 KiB
Rust
// 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");
|
|
}
|
|
}
|