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
+2
-2
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
Reference in new issue
Block a user