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:
Richard ZinkandClaude Opus 5.5 committed 2026-11-05 22:46:06 +01:00
1 parent 5a8ebb984e
commit 1ae41228ab
28 files changed
+530 -15

No files matched your search

+164
View File
@@ -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");
}
}