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

+37 -4
View File
@@ -124,6 +124,9 @@ pub enum EK {
Giant, // slow, huge, smashes doors and traps
Fairy, // flies, finds the dungeon's traps and heals heroes a little
BattleMage, // blasts reinforced walls from afar, throws fireballs
// siege engines (heroes bring them; a master can build his own, see siege.rs)
Ram, // smashes doors, walls and towers, ignores creatures
Catapult, // hurls stones at towers from afar
}
pub struct St {
@@ -166,6 +169,8 @@ pub fn st(k: EK) -> St {
EK::Giant => s("unit.giant", 900.0, 34.0, 1.1, 1.3, 1.6, 200, 600),
EK::Fairy => s("unit.fairy", 70.0, 5.0, 3.5, 3.2, 1.2, 60, 150),
EK::BattleMage => s("unit.battlemage", 120.0, 18.0, 5.0, 1.8, 1.8, 120, 250),
EK::Ram => s("unit.ram", 900.0, 45.0, 1.1, 1.0, 1.8, 0, 200),
EK::Catapult => s("unit.catapult", 320.0, 40.0, 7.5, 0.9, 3.5, 0, 200),
}
}
@@ -2134,6 +2139,13 @@ impl Game {
if w >= 6 {
pool.push((Giant, 1));
}
// siege engines for the dungeon's doors, walls and towers
if w >= 4 && self.traps.iter().any(|t| t.kind.door()) {
pool.push((Ram, 1));
}
if w >= 6 && self.towers.iter().any(|t| !t.hero) {
pool.push((Catapult, 1));
}
let boss = if w % 5 == 0 { Some(Paladin) } else { None };
let base_level = 1 + (w - 1) / 3;
let list = self.build_squad(target, base_level, &pool, boss, 2);
@@ -2255,7 +2267,7 @@ impl Game {
}
}
fn hit_gate(&mut self, e: &mut Ent) {
pub(crate) fn hit_gate(&mut self, e: &mut Ent) {
let s = st(e.kind);
e.cd = s.interval;
let dmg = e.atk() * (0.85 + self.rng.f() * 0.3);
@@ -2404,6 +2416,7 @@ impl Game {
let pow = match e.kind {
EK::Dwarf => 3.0,
EK::Paladin | EK::Giant => 2.0,
EK::Ram => 4.0,
_ => 1.0,
} * if e.kind == EK::BattleMage && (tile.reinforced || tile.kind == Kind::Steel) {
// the battle mage blasts reinforced walls (steel resists better)
@@ -3615,6 +3628,14 @@ impl Game {
}
fn creature_ai(&mut self, e: &mut Ent, dt: f32) {
if crate::siege::siege(e.kind) {
// a siege engine has no needs: it only follows the attack flag
e.hunger = 0.0;
e.energy = 1.0;
e.happy = 1.0;
self.siege_ai(e, dt);
return;
}
let me = e.id + 1;
if e.kind == EK::Revenant {
// the undead neither eat nor sleep
@@ -4469,9 +4490,21 @@ impl Game {
}
}
let fort = e.guard || !e.patrol.is_empty();
if !matches!(e.job, Job::Attack(_)) {
// a catapult aims at towers first
if e.kind == EK::Catapult && !matches!(e.job, Job::AttackTower(_)) {
let reach = st(e.kind).range + 1.0;
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();
e.repath = 0.0;
}
}
if !matches!(e.job, Job::Attack(_)) && !matches!((e.kind, e.job), (EK::Catapult, Job::AttackTower(_))) {
let radius = if fort { 8.0 } else { 6.5 };
if let Some(id) = self.nearest_enemy(e, radius) {
// a ram does not fight creatures
let foe = if e.kind == EK::Ram { None } else { self.nearest_enemy(e, radius) };
if let Some(id) = foe {
e.job = Job::Attack(id);
e.path.clear();
e.repath = 0.0;
@@ -5255,7 +5288,7 @@ impl Game {
e.working = true;
if e.cd <= 0.0 {
e.cd = s.interval;
let dmg = e.atk() * (0.85 + self.rng.f() * 0.3);
let dmg = e.atk() * (0.85 + self.rng.f() * 0.3) * crate::siege::tower_breaker(e.kind);
if s.range > 1.5 {
self.shot(e.x, e.y, 0.45, tx, ty, hit_h, 0.25, 0xff70b0d0);
self.style_last_fx(if e.kind == EK::Archer { crate::particles::ST_ARROW } else { crate::particles::ST_PLAIN });