Siege engines: knights bring rams and catapults, masters build their own in the forge
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5a8ebb984e
commit
1ae41228ab
28 files changed
+530
-15
No files matched your search
+164
@@ -0,0 +1,164 @@
|
||||
// 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;
|
||||
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");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user