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

+2 -2
View File
@@ -10,9 +10,9 @@ use serde::{Deserialize, Serialize};
pub const SIDES: [i32; 6] = [-1, 0, 1, 2, 3, 4];
// creature and hero kinds a block may name ("" = any)
pub const KINDS: [&str; 22] = [
pub const KINDS: [&str; 24] = [
"Worker", "Kobold", "Spider", "Mage", "Ogre", "Jailer", "Priestess", "Chainbreaker", "Bat", "Gargoyle", "Owl", "Wyrm", "Cogling", "Revenant", "Knight", "Archer", "Priest", "Dwarf", "Paladin", "Giant", "Fairy",
"BattleMage",
"BattleMage", "Ram", "Catapult",
];
// ground a Terrain block can make
+7 -1
View File
@@ -34,6 +34,11 @@ pub enum Act {
Sell(Rect),
Bridge(Rect),
Steel(Rect),
Siege {
which: u8, // 0 ram, 1 catapult
x: i32,
y: i32,
},
Wall(Rect), // plan walls on own floor (or cancel planned ones)
Drawbridge(Rect), // build drawbridges, or (one tile on an own drawbridge) switch it: down, up, automatic
Tower {
@@ -145,7 +150,7 @@ impl Game {
Act::Sell(r) => (rect_center(r), MARK_SELL, 4.0),
Act::Bridge(r) | Act::Drawbridge(r) | Act::Wall(r) => (rect_center(r), MARK_BRIDGE, 4.0),
Act::Steel(r) => (rect_center(r), MARK_STEEL, 4.0),
Act::Tower { x, y, .. } => (point(x as f32, y as f32), MARK_TOWER, 6.0),
Act::Tower { x, y, .. } | Act::Siege { x, y, .. } => (point(x as f32, y as f32), MARK_TOWER, 6.0),
Act::Trap { x, y, .. } => (point(x as f32, y as f32), MARK_TRAP, 6.0),
Act::Cast { x, y, .. } => (point(x, y), MARK_SPELL, 5.0),
Act::Lightning { x, y } => (point(x, y), MARK_LIGHTNING, 5.0),
@@ -189,6 +194,7 @@ impl Game {
Act::Bridge(r) => self.build_bridges(r.x0, r.y0, r.x1, r.y1),
Act::Drawbridge(r) => self.drawbridge_order(r.x0, r.y0, r.x1, r.y1),
Act::Wall(r) => self.plan_walls(r.x0, r.y0, r.x1, r.y1),
Act::Siege { which, x, y } => self.build_siege(which, x, y),
Act::Steel(r) => self.build_steel(r.x0, r.y0, r.x1, r.y1),
Act::Tower { kind, x, y } => self.build_tower(kind, x, y),
Act::Trap { kind, x, y } => self.place_trap(kind, x, y),
+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 });
+61 -1
View File
@@ -89,6 +89,8 @@ pub const T_BARK: u32 = 201; // tree trunk
pub const T_LEAVES: u32 = 202; // 2 variants: tree crown
pub const T_GRASS: u32 = 204; // 4 variants: grass above ground
pub const I_WALL: u32 = 208; // wall tool: bricks rising from the floor
pub const I_RAM: u32 = 209; // battering ram tool
pub const I_CATAPULT: u32 = 210; // catapult tool
pub struct Gfx {
pub atlas: Texture2D,
@@ -842,6 +844,36 @@ fn gen_atlas() -> Image {
None
}
});
// siege icons: a ram (a trunk with an iron cap under a roof), a catapult (arm and stone)
cv.cell(I_RAM, |x, y| {
if (3..=12).contains(&x) && y == 4 + (x - 3).min(12 - x).min(3) * 0 && false {
None
} else if y >= 3 && y <= 5 && (x - 7).abs() <= 6 - (y - 3) * 0 && (x + y) % 2 == 0 {
Some(c(92, 58, 32))
} else if (7..=9).contains(&y) && (1..=13).contains(&x) {
Some(if x <= 3 { c(170, 175, 190) } else { c(140, 92, 52) })
} else if y >= 11 && y <= 13 && ((x - 4).abs() <= 1 || (x - 11).abs() <= 1) {
Some(c(30, 24, 26))
} else if y == 10 && (3..=12).contains(&x) {
Some(c(92, 58, 32))
} else {
None
}
});
cv.cell(I_CATAPULT, |x, y| {
let arm = x + y == 14 && (3..=11).contains(&x);
if (x - 12).pow(2) + (y - 3).pow(2) <= 3 {
Some(c(150, 152, 140))
} else if arm {
Some(c(140, 92, 52))
} else if y == 11 && (2..=13).contains(&x) {
Some(c(92, 58, 32))
} else if y >= 12 && ((x - 4).abs() <= 1 || (x - 11).abs() <= 1) {
Some(c(30, 24, 26))
} else {
None
}
});
// frost spell icon: a snowflake
cv.cell(I_FREEZE, |x, y| {
let (dx, dy) = (x - 8, y - 8);
@@ -1519,6 +1551,32 @@ fn sprite_rows(k: EK) -> [&'static str; 10] {
"...y..y...",
"..........",
],
// battering ram: a roofed frame on wheels, an iron-capped trunk
EK::Ram => [
"..........",
"...BBBB...",
"..BbbbbB..",
".BbbbbbbB.",
"SsbbbbbbB.",
"SsBBBBBBB.",
"..b.b.b.b.",
".kBk..kBk.",
".kck..kck.",
"..k....k..",
],
// catapult: a throwing arm with a stone in its cup
EK::Catapult => [
"......cc..",
".....kCCk.",
".....kbk..",
"....kb....",
"...kb.....",
"..kb......",
".BBBBBBBB.",
".bkbbbbkb.",
".kck..kck.",
"..k....k..",
],
// battle mage: robes of the order, a burning staff
EK::BattleMage => [
"....kk...o",
@@ -1601,7 +1659,7 @@ fn pal(ch: char) -> Option<Color> {
})
}
pub const SPRITE_KINDS: [EK; 22] = [
pub const SPRITE_KINDS: [EK; 24] = [
EK::Worker,
EK::Kobold,
EK::Spider,
@@ -1624,6 +1682,8 @@ pub const SPRITE_KINDS: [EK; 22] = [
EK::Giant,
EK::Fairy,
EK::BattleMage,
EK::Ram,
EK::Catapult,
];
// width of the sprite texture in pixels
pub const SPRITE_TEX_W: f32 = (SPRITE_KINDS.len() * 12) as f32;
+3
View File
@@ -33,6 +33,7 @@ mod blocks;
mod bridges;
mod mscript;
mod settings;
mod siege;
mod sfx;
mod tracks;
mod traps;
@@ -146,6 +147,7 @@ fn world_action(app: &mut App, ui: &mut Ui, r3d: &R3d, inp: &mut Input, mx: f32,
(Tool::Tower(k), Some((x, z))) => app.game.queue(Act::Tower { kind: k, x, y: z }),
(Tool::Trap(k), Some((x, z))) => app.game.queue(Act::Trap { kind: k, x, y: z }),
(Tool::Spell(sp), _) => app.game.queue(Act::Cast { spell: sp, x: wx, y: wy }),
(Tool::Siege(which), Some((tx, ty))) => app.game.queue(Act::Siege { which, x: tx, y: ty }),
(Tool::Order(0), _) => {
let j = r3d.pick_ent(&app.game, &app.cam, mx, my, false);
let ent = j.filter(|&j| j < app.game.ents.len()).map(|j| app.game.ents[j].id);
@@ -1958,6 +1960,7 @@ fn draw_frame(app: &mut App, gfx: &Gfx, r3d: &mut R3d, ui: &mut Ui, menu: &mut M
Tool::Summon => (Color::new(0.8, 0.5, 1.0, 1.0), None),
Tool::Bridge | Tool::Drawbridge => (Color::new(0.9, 0.7, 0.4, 1.0), None),
Tool::Wall => (Color::new(0.8, 0.4, 1.0, 1.0), None),
Tool::Siege(_) => (Color::new(0.9, 0.7, 0.4, 1.0), None),
Tool::Steel => (Color::new(0.75, 0.8, 0.9, 1.0), None),
Tool::Tower(_) => (Color::new(1.0, 0.6, 0.25, 1.0), None),
Tool::Trap(_) => (Color::new(1.0, 0.4, 0.3, 1.0), None),
+1 -1
View File
@@ -8,7 +8,7 @@ use crate::ui::{tool_cost_static, BLUE as BLUE_C, GREEN as GREEN_C, RED as RED_C
pub const TABS: [&str; 4] = ["best.tab.creatures", "best.tab.humans", "best.tab.dungeon", "best.tab.ranks"];
const CREATURES: [EK; 14] = [EK::Worker, EK::Kobold, EK::Spider, EK::Mage, EK::Ogre, EK::Jailer, EK::Priestess, EK::Chainbreaker, EK::Cogling, EK::Revenant, EK::Bat, EK::Gargoyle, EK::Owl, EK::Wyrm];
const HUMANS: [EK; 8] = [EK::Knight, EK::Archer, EK::Priest, EK::Dwarf, EK::Fairy, EK::BattleMage, EK::Giant, EK::Paladin];
const HUMANS: [EK; 10] = [EK::Knight, EK::Archer, EK::Priest, EK::Dwarf, EK::Fairy, EK::BattleMage, EK::Giant, EK::Paladin, EK::Ram, EK::Catapult];
// buildings: indices into ui::TOOLS
const BUILDINGS: [usize; 32] = [2, 3, 4, 5, 6, 12, 40, 13, 18, 21, 41, 42, 43, 44, 17, 47, 49, 19, 14, 15, 16, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31];
+2
View File
@@ -42,6 +42,8 @@ pub const RESEARCH: &[(usize, f32)] = &[
(39, 950.0), // quake
(44, 900.0), // sacrifice altar
(31, 1100.0), // magic door
(50, 900.0), // battering ram
(51, 1000.0), // catapult
];
// known from the start in free play
+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");
}
}
+4 -4
View File
@@ -127,10 +127,10 @@ const CLOUD_TIME: f32 = 6.0;
// how hard a unit hits doors
pub fn door_breaker(k: EK) -> f32 {
if k == EK::Giant {
3.0
} else {
1.0
match k {
EK::Giant => 3.0,
EK::Ram => 6.0,
_ => 1.0,
}
}
+15 -2
View File
@@ -176,13 +176,14 @@ pub enum Tool {
Steel,
Drawbridge,
Wall,
Siege(u8), // 0 battering ram, 1 catapult
Trap(crate::traps::TrapKind),
Spell(crate::spells::Spell),
Order(u8), // 0 attack, 1 guard
}
// New tools are appended so that existing key bindings ("tool<i>") keep their meaning
pub const TOOLS: [Tool; 50] = [
pub const TOOLS: [Tool; 52] = [
Tool::Dig,
Tool::Hand,
Tool::Room(0),
@@ -233,6 +234,8 @@ pub const TOOLS: [Tool; 50] = [
Tool::Drawbridge,
Tool::Spell(crate::spells::Spell::Freeze),
Tool::Wall,
Tool::Siege(0),
Tool::Siege(1),
];
@@ -277,6 +280,8 @@ pub fn tool_info(t: Tool) -> ToolInfo {
Tool::Bridge => ("bridge", I_BRIDGE, None),
Tool::Drawbridge => ("drawbridge", I_DRAWBRIDGE, None),
Tool::Wall => ("wall", I_WALL, None),
Tool::Siege(0) => ("ram", I_RAM, None),
Tool::Siege(_) => ("catapult", I_CATAPULT, None),
Tool::Steel => ("steel", I_STEEL, None),
};
let name = match room {
@@ -293,6 +298,10 @@ pub fn tool_cost_static(t: Tool) -> String {
Tool::Trap(k) => trf("cost.work", &[(k.work() as i32).to_string()]),
Tool::Bridge => trf("cost.bridge", &[BRIDGE_COST[0].to_string(), (crate::wood::BRIDGE_WOOD as i32).to_string(), BRIDGE_COST[1].to_string()]),
Tool::Wall => trf("cost.gold_tile", &[(crate::walls::WALL_COST as i32).to_string()]),
Tool::Siege(w) => {
let (g, wd) = if w == 0 { crate::siege::RAM_COST } else { crate::siege::CATAPULT_COST };
trf("cost.gold_wood", &[(g as i32).to_string(), (wd as i32).to_string()])
}
Tool::Drawbridge => trf("cost.drawbridge", &[DRAWBRIDGE_COST[0].to_string(), (crate::wood::DRAWBRIDGE_WOOD as i32).to_string(), DRAWBRIDGE_COST[1].to_string()]),
Tool::Steel => trf("cost.steel", &[STEEL_COST[0].to_string(), STEEL_COST[1].to_string()]),
Tool::Tower(TowerKind::Rune) => {
@@ -320,6 +329,10 @@ pub fn tool_cost(t: Tool, g: &Game) -> String {
Tool::Summon => trf("cost.mana", &[(g.summon_cost() as i32).to_string()]),
Tool::Bridge => trf("cost.bridge", &[BRIDGE_COST[0].to_string(), (crate::wood::BRIDGE_WOOD as i32).to_string(), BRIDGE_COST[1].to_string()]),
Tool::Wall => trf("cost.gold_tile", &[(crate::walls::WALL_COST as i32).to_string()]),
Tool::Siege(w) => {
let (g, wd) = if w == 0 { crate::siege::RAM_COST } else { crate::siege::CATAPULT_COST };
trf("cost.gold_wood", &[(g as i32).to_string(), (wd as i32).to_string()])
}
Tool::Drawbridge => trf("cost.drawbridge", &[DRAWBRIDGE_COST[0].to_string(), (crate::wood::DRAWBRIDGE_WOOD as i32).to_string(), DRAWBRIDGE_COST[1].to_string()]),
Tool::Steel => trf("cost.steel", &[STEEL_COST[0].to_string(), STEEL_COST[1].to_string()]),
Tool::Tower(TowerKind::Rune) => {
@@ -346,7 +359,7 @@ pub const CATS: [(&str, &[usize]); 7] = [
("cat.towers", &[14, 15, 16, 20]),
("cat.traps", &[22, 23, 24, 25, 26, 27, 28, 29, 30, 31]),
("cat.spells", &[9, 10, 11, 32, 33, 34, 35, 36, 37, 38, 39, 48]),
("cat.orders", &[8, 45, 46, 7, 19]),
("cat.orders", &[8, 45, 46, 7, 19, 50, 51]),
];
#[derive(Clone, Copy, PartialEq, Debug)]