New heroes: thief steals gold, monk ignores spells, ranger lays snares, inquisitor turns weak creatures

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-06 11:02:50 +01:00
1 parent a1a8f9d71d
commit ff48a69571
29 files changed
+724 -12

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; 26] = [
pub const KINDS: [&str; 30] = [
"Worker", "Kobold", "Spider", "Mage", "Ogre", "Jailer", "Priestess", "Chainbreaker", "Bat", "Gargoyle", "Owl", "Wyrm", "Cogling", "Revenant", "Knight", "Archer", "Priest", "Dwarf", "Paladin", "Giant", "Fairy",
"BattleMage", "Ram", "Catapult", "CaveBear", "Beetle",
"BattleMage", "Ram", "Catapult", "CaveBear", "Beetle", "Thief", "Monk", "Ranger", "Inquisitor",
];
// ground a Terrain block can make
+28 -3
View File
@@ -132,6 +132,11 @@ pub enum EK {
CaveBear, // strong and shaggy, likes the mushroom garden
Beetle, // a giant armoured beetle, likes the lair
Trader, // a merchant visiting through the portal (traders.rs)
// heroes of the later waves (rogues.rs)
Thief, // fast, sneaks to a treasury, steals gold and runs off with it
Monk, // fists and faith: spells do not touch him
Ranger, // a bow from afar, lays snares in the dungeon's corridors
Inquisitor, // fragile; now and then turns one of the weakest creatures to the heroes' side
}
pub struct St {
@@ -179,6 +184,10 @@ pub fn st(k: EK) -> St {
EK::CaveBear => s("unit.cavebear", 420.0, 24.0, 1.0, 1.9, 1.4, 110, 0),
EK::Beetle => s("unit.beetle", 300.0, 13.0, 1.0, 1.7, 1.1, 70, 0),
EK::Trader => s("unit.trader", 200.0, 1.0, 1.0, 1.5, 2.0, 0, 0),
EK::Thief => s("unit.thief", 60.0, 6.0, 1.0, 3.1, 0.9, 50, 120),
EK::Monk => s("unit.monk", 150.0, 12.0, 1.0, 2.3, 0.7, 80, 180),
EK::Ranger => s("unit.ranger", 90.0, 11.0, 5.0, 2.2, 1.4, 80, 160),
EK::Inquisitor => s("unit.inquisitor", 70.0, 7.0, 3.5, 1.8, 1.6, 90, 250),
}
}
@@ -629,6 +638,8 @@ pub struct Game {
#[serde(default)]
pub happenings: Option<crate::events::Events>, // random events (free games and versus), see events.rs
#[serde(default)]
pub snares: Vec<usize>, // tiles with a ranger's snare (rogues.rs)
#[serde(default)]
pub guard_marks: Vec<usize>, // guard markers (shields) of the master being processed (orders.rs)
#[serde(default)]
pub attack_marks: Vec<usize>, // attack markers (swords)
@@ -978,6 +989,7 @@ impl Game {
bridgework: Default::default(),
trading: Default::default(),
happenings: None,
snares: Vec::new(),
guard_marks: Vec::new(),
attack_marks: Vec::new(),
marks_t: 0.0,
@@ -1778,6 +1790,7 @@ impl Game {
self.update_contested();
self.update_units(dt);
self.switch_to(first); // update_units worked master by master
self.update_snares();
self.update_towers(dt);
self.update_reveals(dt);
self.update_graves(dt);
@@ -2263,6 +2276,12 @@ impl Game {
if w >= 6 {
pool.push((Giant, 1));
}
// the rogues of the later waves (in the campaign only from the later missions on)
for (k, from_wave, from_mission) in crate::rogues::ROGUES {
if w >= from_wave && self.mission.as_ref().map_or(true, |m| m.index >= from_mission) {
pool.push((k, 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));
@@ -2754,6 +2773,7 @@ impl Game {
EK::Priestess => 0xff3080ff,
EK::Owl => 0xffffa060,
EK::Wyrm => 0xff2080ff,
EK::Inquisitor => 0xff40d0ff,
_ => 0xff70b0d0,
};
// flyers shoot from the air
@@ -2762,7 +2782,8 @@ impl Game {
self.grow_last_fx(lv);
use crate::particles::*;
self.style_last_fx(match e.kind {
EK::Archer => ST_ARROW,
EK::Archer | EK::Ranger => ST_ARROW,
EK::Inquisitor => ST_HOLY,
EK::Mage => ST_SHADOW,
EK::Priest => ST_HOLY,
EK::Priestess | EK::Wyrm | EK::BattleMage => ST_FIRE,
@@ -2924,7 +2945,7 @@ impl Game {
self.switch_to(me);
}
if hero {
let b = s.bounty as f32;
let b = s.bounty as f32 + if kind == EK::Thief { self.thief_loot(id) } else { 0.0 };
let got = self.add_gold(b);
self.fx(5, x, y, 0.0, 0.0, 1.0, 0xff30d0ff);
self.sfx(Sfx::HeroDeath, x, y);
@@ -4631,6 +4652,10 @@ impl Game {
if e.boss > 0 {
self.boss_ability(e, dt);
}
// the rogues' tricks: a thief who is busy stealing or running does nothing else
if self.rogue_ai(e, dt) {
return;
}
if e.kind == EK::Fairy {
// fairy dust: the heroes around are healed a little all the time
for o in self.ents.iter_mut() {
@@ -5681,7 +5706,7 @@ impl Game {
self.ignite_at(wx, wy);
let mut kills = Vec::new();
for (j, e) in self.ents.iter_mut().enumerate() {
if e.alive && !e.held && e.active_hero() && (e.x - wx).powi(2) + (e.y - wy).powi(2) < 2.6 {
if e.alive && !e.held && e.active_hero() && !crate::rogues::spell_proof(e) && (e.x - wx).powi(2) + (e.y - wy).powi(2) < 2.6 {
e.hp -= 55.0;
e.flash = 0.2;
if e.hp <= 0.0 {
+58 -2
View File
@@ -1455,6 +1455,58 @@ fn sprite_rows(k: EK) -> [&'static str; 10] {
"..kBkkBk..",
"..kk..kk..",
],
// thief: a dark hood and mask, a dagger, a sack of gold on the back
EK::Thief => [
"...kkkk...",
"..kddddk..",
"..kdfkfk.Y",
"..kddddkYy",
".kdddddkYy",
"skdddddk..",
".kdkdddk..",
"..kddddk..",
"..kdkkdk..",
"..kk..kk..",
],
// monk: shaven head, an orange robe tied with a rope, bare fists
EK::Monk => [
"...kkkk...",
"..kffffk..",
"..kfkfkk..",
"..kffffk..",
".kooooook.",
"kfoooooofk",
".kooyyook.",
".kOooooOk.",
".kOooooOk.",
"..kkkkkk..",
],
// ranger: a green hood and cloak, a long bow, leather boots
EK::Ranger => [
"...kkkk.b.",
"..kgGGgkkb",
"..kGfkfk.b",
"..kGfffk.b",
".kGgggggkb",
".kGgbbggkb",
".kGggggk.b",
"..kBBBBk.b",
"..kBkkBkb.",
"..kk..kk..",
],
// inquisitor: a tall red hat, white and red robes, a glowing holy book
EK::Inquisitor => [
"...kRRk...",
"..kRrrRk..",
"..kRrrRk..",
"..kffffk..",
"..kfkfkk..",
".kwwrrwwk.",
"kwkwrrwkek",
".kwwrrwwkk",
".kRwwwwRk.",
"..kkkkkk..",
],
EK::Priest => [
"....yy....",
"...kkkk...",
@@ -1739,7 +1791,7 @@ fn pal(ch: char) -> Option<Color> {
})
}
pub const SPRITE_KINDS: [EK; 27] = [
pub const SPRITE_KINDS: [EK; 31] = [
EK::Worker,
EK::Kobold,
EK::Spider,
@@ -1767,6 +1819,10 @@ pub const SPRITE_KINDS: [EK; 27] = [
EK::CaveBear,
EK::Beetle,
EK::Trader,
EK::Thief,
EK::Monk,
EK::Ranger,
EK::Inquisitor,
];
// width of the sprite texture in pixels
pub const SPRITE_TEX_W: f32 = (SPRITE_KINDS.len() * 12) as f32;
@@ -1784,7 +1840,7 @@ pub fn gear_tier(level: u32) -> u32 {
}
fn is_hero_kind(k: EK) -> bool {
matches!(k, EK::Knight | EK::Archer | EK::Priest | EK::Dwarf | EK::Paladin | EK::Giant | EK::Fairy | EK::BattleMage)
matches!(k, EK::Knight | EK::Archer | EK::Priest | EK::Dwarf | EK::Paladin | EK::Giant | EK::Fairy | EK::BattleMage | EK::Thief | EK::Monk | EK::Ranger | EK::Inquisitor)
}
// Views of a sprite for eight directions (the others are mirrored): 0 front, 1 front
+1
View File
@@ -29,6 +29,7 @@ mod particles;
mod props;
mod research;
mod rival;
mod rogues;
mod render3d;
mod rooms;
mod save;
+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; 16] = [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, EK::CaveBear, EK::Beetle];
const HUMANS: [EK; 10] = [EK::Knight, EK::Archer, EK::Priest, EK::Dwarf, EK::Fairy, EK::BattleMage, EK::Giant, EK::Paladin, EK::Ram, EK::Catapult];
const HUMANS: [EK; 14] = [EK::Knight, EK::Archer, EK::Priest, EK::Dwarf, EK::Fairy, EK::Thief, EK::BattleMage, EK::Monk, EK::Giant, EK::Ranger, EK::Inquisitor, 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];
+9 -1
View File
@@ -142,7 +142,15 @@ pub fn kind_by_name(name: &str) -> Option<EK> {
18 => EK::Paladin,
19 => EK::Giant,
20 => EK::Fairy,
_ => EK::BattleMage,
21 => EK::BattleMage,
22 => EK::Ram,
23 => EK::Catapult,
24 => EK::CaveBear,
25 => EK::Beetle,
26 => EK::Thief,
27 => EK::Monk,
28 => EK::Ranger,
_ => EK::Inquisitor,
})
}
+18
View File
@@ -2372,6 +2372,24 @@ fn draw_landmarks_solid(g: &Game, t: f32) {
// soul core: an obsidian throne whose four curved spikes hold the crystal heart in chains
let (cx, cy) = g.map.core;
draw_throne(vec3(cx as f32 + 1.5, 0.0, cy as f32 + 1.5), t);
// a ranger's snares: iron jaws on the floor, open and waiting
for &i in &g.snares {
if !g.map.shown(i) {
continue;
}
let (x, y) = xy(i);
let c = vec3(x as f32 + 0.5, 0.0, y as f32 + 0.5);
let iron = Color::from_rgba(96, 92, 100, 255);
let dark = Color::from_rgba(40, 36, 42, 255);
draw_cube(c + vec3(0.0, 0.02, 0.0), vec3(0.34, 0.03, 0.34), None, dark);
for side in [-1.0f32, 1.0] {
draw_cube(c + vec3(side * 0.16, 0.06, 0.0), vec3(0.04, 0.08, 0.36), None, iron);
for k in 0..3 {
let z = -0.12 + k as f32 * 0.12;
draw_cube(c + vec3(side * 0.12, 0.11, z), vec3(0.03, 0.05, 0.03), None, Color::from_rgba(200, 200, 210, 255));
}
}
}
// the other masters' cores: the same throne (their hearts glow in their colours)
for rv in g.rivals.iter().filter(|r| r.alive && g.map.shown(idx(r.core.0 + 1, r.core.1 + 1))) {
let (rx, rz) = rv.centre();
+365
View File
@@ -0,0 +1,365 @@
// The rogues of the later waves: the thief sneaks into a treasury and runs off with gold, the
// monk shrugs off spells, the ranger lays snares in the dungeon's corridors and the inquisitor
// now and then turns one of the weakest creatures (kobolds, spiders) to the heroes' side.
use crate::game::*;
use crate::map::*;
// (kind, first wave, first campaign mission (index)) - the wave pool takes them from then on
pub const ROGUES: [(EK, u32, usize); 4] = [(EK::Thief, 3, 4), (EK::Monk, 5, 8), (EK::Ranger, 7, 11), (EK::Inquisitor, 9, 15)];
// gold a thief can carry, and how fast he scoops it
pub const THIEF_BAG: i32 = 250;
const THIEF_GRAB: i32 = 25;
// a ranger's snares: one every SNARE_EVERY seconds, at most MAX_SNARES on the map
const SNARE_EVERY: f32 = 18.0;
const MAX_SNARES: usize = 30;
pub const SNARE_DAMAGE: f32 = 25.0;
pub const SNARE_STUN: f32 = 3.0;
// the inquisitor turns a weak creature every CONVERT_EVERY seconds, within CONVERT_RANGE tiles
pub const CONVERT_EVERY: f32 = 20.0;
const CONVERT_RANGE: f32 = 5.0;
// spells do not touch a monk of the order
pub fn spell_proof(e: &Ent) -> bool {
e.hero && e.kind == EK::Monk
}
// the creatures an inquisitor can turn: only the weakest
pub fn convertible(k: EK) -> bool {
matches!(k, EK::Kobold | EK::Spider)
}
fn master_ground(t: &Tile) -> bool {
Owner::MASTER_SIDES.contains(&t.owner.side())
}
impl Game {
// true: the rogue did all he wanted this step (the usual hero AI is skipped)
pub(crate) fn rogue_ai(&mut self, e: &mut Ent, dt: f32) -> bool {
if !e.active_hero() {
return false;
}
match e.kind {
EK::Thief => self.thief_ai(e, dt),
EK::Ranger => {
self.lay_snare(e);
false
}
EK::Inquisitor => {
self.convert_creature(e);
false
}
_ => false,
}
}
// ------------------------------------------------------------ thief
fn thief_ai(&mut self, e: &mut Ent, dt: f32) -> bool {
let cur = e.tile();
let full = e.carry >= THIEF_BAG;
// a full bag (or nothing left to steal): back to the gate and away
if full || (e.carry > 0 && !self.gold_to_steal(cur)) {
if !self.has_gate() {
return false;
}
let (gx, gy) = self.gate_center();
if (e.x - gx).powi(2) + (e.y - gy).powi(2) < 2.0 * 2.0 {
e.alive = false;
self.fx(3, e.x, e.y, 0.0, 0.0, 0.7, 0xff505070);
self.tell_local("msg.thief_escaped", vec![e.carry.to_string()], Some(Sfx::Error));
return true;
}
if e.path.is_empty() && e.repath <= 0.0 {
e.repath = 1.5;
if let Some(p) = self.map.path_to(cur, idx(gx as i32, gy as i32)) {
e.path = p;
}
}
self.step_path(e, dt);
return true;
}
// in a treasury with gold: scoop it up
let t = self.map.t[cur];
if t.kind == Kind::Treasury && t.gold > 0 && master_ground(&t) {
e.path.clear();
e.working = true;
if e.cd <= 0.0 {
e.cd = 0.5;
let want = THIEF_GRAB.min(THIEF_BAG - e.carry).min(t.gold);
let got = self.steal_gold(t.owner.side(), cur, want);
if got > 0 && e.carry == 0 && t.owner.side() == self.local_side {
self.tell_local("msg.thief_in_treasury", vec![], Some(Sfx::WaveAlarm));
}
e.carry += got;
self.fx(5, e.x, e.y, 0.0, 0.0, 0.5, 0xff30d0ff);
}
return true;
}
// on the way to the nearest treasury with gold
if e.path.is_empty() && e.repath <= 0.0 {
e.repath = 2.0;
let cost = self.map.hero_cost();
let found = self.map.search(cur, cost, |i| {
let t = &self.map.t[i];
t.kind == Kind::Treasury && t.gold > 0 && master_ground(t)
}, true);
match found {
Some(p) => e.path = p,
None if e.carry > 0 => {}
None => return false, // nothing to steal: he fights like the others
}
}
if e.path.is_empty() {
return e.carry > 0;
}
self.step_path(e, dt);
true
}
// is there a treasury with gold anywhere a thief could reach from tile i?
fn gold_to_steal(&self, i: usize) -> bool {
let cost = self.map.hero_cost();
self.map.search(i, cost, |j| {
let t = &self.map.t[j];
t.kind == Kind::Treasury && t.gold > 0 && master_ground(t)
}, true).is_some()
}
// takes gold from the treasury tile i of the master on `side`; returns what was taken
fn steal_gold(&mut self, side: u8, i: usize, want: i32) -> i32 {
let me = self.me_side;
if side != me {
if !self.is_master(side) {
return 0;
}
self.switch_to(side);
}
let take = (want as f32).min(self.gold).floor().max(0.0) as i32;
self.gold -= take as f32;
let t = &mut self.map.t[i];
t.gold = (t.gold - take).max(0);
if side != me {
self.switch_to(me);
}
take
}
// a slain thief drops his loot: it goes to whoever slew him (with the bounty)
pub(crate) fn thief_loot(&self, id: u32) -> f32 {
self.ents.iter().find(|e| e.id == id).map_or(0.0, |e| e.carry.max(0) as f32)
}
// ------------------------------------------------------------ ranger
// a snare on the dungeon's ground under the ranger's feet, now and then
fn lay_snare(&mut self, e: &mut Ent) {
if e.skill_cd > 0.0 || self.snares.len() >= MAX_SNARES {
return;
}
let i = e.tile();
let t = &self.map.t[i];
if !t.walkable() || !master_ground(t) || t.bridge() || self.snares.contains(&i) || self.traps.iter().any(|tr| tr.tile == i) {
e.skill_cd = 2.0;
return;
}
self.snares.push(i);
e.skill_cd = SNARE_EVERY;
self.fx(6, e.x, e.y, 0.0, 0.0, 0.4, 0xff3a5a7a);
}
// the shared step: a creature that steps into a snare is caught (hurt and stuck for a while)
pub(crate) fn update_snares(&mut self) {
if self.snares.is_empty() {
return;
}
let mut k = 0;
while k < self.snares.len() {
let i = self.snares[k];
if !self.map.t[i].walkable() {
self.snares.swap_remove(k);
continue;
}
let caught = self.ents.iter().position(|e| {
e.alive && !e.held && !e.hero && !e.chained && e.captive == 0 && !flies(e.kind) && Owner::MASTER_SIDES.contains(&e.side) && e.tile() == i
});
let Some(j) = caught else {
k += 1;
continue;
};
self.snares.swap_remove(k);
let e = &mut self.ents[j];
e.stun = e.stun.max(SNARE_STUN);
e.hp -= SNARE_DAMAGE;
e.flash = 0.2;
let (x, y, side, kind, dead) = (e.x, e.y, e.side, e.kind, e.hp <= 0.0);
self.fx(2, x, y, 0.0, 0.0, 0.4, 0xff4040ff);
self.sfx(Sfx::MeleeHit, x, y);
if side == self.local_side {
self.tell_local("msg.snare_caught", vec![format!("@{}", st(kind).name)], None);
}
if dead {
self.kill(j);
}
}
}
// ------------------------------------------------------------ inquisitor
// turns the nearest weak creature in sight to the heroes' side
fn convert_creature(&mut self, e: &mut Ent) {
if e.skill_cd > 0.0 {
return;
}
e.skill_cd = 1.0;
let target = self
.ents
.iter()
.enumerate()
.filter(|(_, o)| o.alive && !o.held && !o.hero && !o.chained && o.captive == 0 && convertible(o.kind) && Owner::MASTER_SIDES.contains(&o.side))
.map(|(j, o)| (j, (o.x - e.x).powi(2) + (o.y - e.y).powi(2)))
.filter(|&(j, d2)| d2 < CONVERT_RANGE * CONVERT_RANGE && self.los(e.x, e.y, self.ents[j].x, self.ents[j].y))
.min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(j, _)| j);
let Some(j) = target else { return };
e.skill_cd = CONVERT_EVERY;
let goal = e.target;
let (side, kind, id, x, y) = {
let o = &self.ents[j];
(o.side, o.kind, o.id, o.x, o.y)
};
self.release_all(id);
let o = &mut self.ents[j];
o.hero = true;
o.side = 1;
o.job = Job::None;
o.path.clear();
o.home = None;
o.bed = None;
o.guard = false;
o.target = goal;
o.waypoint = None;
o.flash = 0.4;
self.shot(e.x, e.y, 0.6, x, y, 0.45, 0.35, 0xff40d0ff);
self.style_last_fx(crate::particles::ST_HOLY);
self.fx(4, x, y, 0.0, 0.0, 0.9, 0xffffeecc);
self.sfx(Sfx::Capture, x, y);
if side == self.local_side {
self.tell_local("msg.converted", vec![format!("@{}", st(kind).name)], Some(Sfx::WaveAlarm));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
// a free game with an open dungeon around the core and no waves
fn open_game() -> Game {
let mut g = Game::new(5, 96, 96);
g.gate_on = false;
let (cx, cy) = g.map.core;
for y in cy - 8..cy + 10 {
for x in cx - 8..cx + 10 {
let t = &mut g.map.t[idx(x, y)];
if t.kind != Kind::Core {
t.kind = Kind::Floor;
t.owner = Owner::Player;
}
}
}
g.ents.clear();
g.recount();
g
}
fn hero(g: &mut Game, k: EK, x: f32, y: f32) -> u32 {
let id = g.spawn(k, true, x, y, 1);
if let Some(e) = g.ents.iter_mut().find(|e| e.id == id) {
e.job = Job::None;
}
id
}
#[test]
fn a_thief_steals_from_the_treasury() {
let mut g = open_game();
let (cx, cy) = g.map.core;
g.build_rect(0, cx + 4, cy + 4, cx + 6, cy + 6);
g.gold = 3000.0;
g.sync_treasury();
let gold0 = g.gold;
let id = hero(&mut g, EK::Thief, cx as f32 - 5.5, cy as f32 + 5.5);
let mut carried = 0;
for _ in 0..(60 * 20) {
g.state = 0; // no creatures in the test: it would count as over
g.update(0.05);
carried = g.ents.iter().find(|e| e.id == id).map_or(carried, |e| e.carry);
if carried >= THIEF_BAG {
break;
}
}
assert!(carried > 0, "the thief took gold");
assert!(g.gold < gold0, "the master lost it: {} -> {}", gold0, g.gold);
}
#[test]
fn monks_shrug_off_spells() {
let mut g = open_game();
let (cx, cy) = g.map.core;
let monk = hero(&mut g, EK::Monk, cx as f32 + 6.5, cy as f32 + 6.5);
let knight = hero(&mut g, EK::Knight, cx as f32 + 6.5, cy as f32 + 7.0);
g.mana = 400.0;
g.lightning(cx as f32 + 6.5, cy as f32 + 6.7);
let hp = |g: &Game, id: u32| g.ents.iter().find(|e| e.id == id).map_or(0.0, |e| e.hp);
assert_eq!(hp(&g, monk), st(EK::Monk).hp, "the monk is untouched");
assert!(hp(&g, knight) < st(EK::Knight).hp, "the knight is not");
}
#[test]
fn a_rangers_snare_catches_a_creature() {
let mut g = open_game();
let (cx, cy) = g.map.core;
let r = hero(&mut g, EK::Ranger, cx as f32 + 6.5, cy as f32 + 6.5);
let mut e = g.ents.iter().find(|e| e.id == r).unwrap().clone();
e.skill_cd = 0.0;
g.lay_snare(&mut e);
let i = idx(cx + 6, cy + 6);
assert!(g.snares.contains(&i));
g.ents.retain(|e| e.id != r);
let k = g.spawn(EK::Ogre, false, cx as f32 + 6.5, cy as f32 + 6.5, 1);
g.update_snares();
let o = g.ents.iter().find(|e| e.id == k).unwrap();
assert!(o.stun > 0.0 && o.hp < st(EK::Ogre).hp, "caught");
assert!(g.snares.is_empty(), "a snare works once");
}
#[test]
fn the_inquisitor_turns_only_weak_creatures() {
let mut g = open_game();
let (cx, cy) = g.map.core;
let ogre = g.spawn(EK::Ogre, false, cx as f32 + 5.5, cy as f32 + 6.0, 1);
let kobold = g.spawn(EK::Kobold, false, cx as f32 + 6.5, cy as f32 + 7.5, 1);
let q = hero(&mut g, EK::Inquisitor, cx as f32 + 6.5, cy as f32 + 5.5);
let mut e = g.ents.iter().find(|e| e.id == q).unwrap().clone();
e.skill_cd = 0.0;
g.convert_creature(&mut e);
let get = |g: &Game, id: u32| g.ents.iter().find(|e| e.id == id).unwrap().clone();
assert!(get(&g, kobold).hero, "the kobold now fights for the heroes");
assert!(!get(&g, ogre).hero, "an ogre is too strong to be turned");
assert_eq!(e.skill_cd, CONVERT_EVERY);
// nothing else to turn: the ogre stays
e.skill_cd = 0.0;
g.convert_creature(&mut e);
assert!(!get(&g, ogre).hero);
}
#[test]
fn rogues_join_the_later_waves() {
for (k, wave, mission) in ROGUES {
assert!(wave >= 3 && mission >= 4, "{:?} comes late", k);
}
assert!(ROGUES.iter().any(|r| r.0 == EK::Inquisitor && r.1 == 9));
}
}
+21
View File
@@ -243,3 +243,24 @@ fn export_grove_scene() {
run(&mut g, 1.0);
save("mushroom-grove", &mut g, x as f32 + 0.5, y as f32 + 0.5, 12.0, 0.0, false);
}
// the rogues of the later waves in a dungeon: thief, monk, ranger, inquisitor and a snare
#[test]
#[ignore]
fn export_rogues_scene() {
std::fs::create_dir_all("/tmp/scenes").unwrap();
let mut g = Game::new(11, 80, 64);
let (cx, cy) = g.map.core;
open(&mut g, cx - 9, cy - 8, cx + 11, cy + 9);
g.place_all_torches();
for (k, kind) in [EK::Thief, EK::Monk, EK::Ranger, EK::Inquisitor].into_iter().enumerate() {
let id = g.spawn(kind, true, cx as f32 + 4.5 + k as f32 * 1.2, cy as f32 + 5.5, 3);
if let Some(e) = g.ents.iter_mut().find(|e| e.id == id) {
e.guard = true;
e.home = Some((e.x, e.y));
}
}
g.snares.push(idx(cx + 5, cy + 3));
g.snares.push(idx(cx + 7, cy + 3));
save("rogues", &mut g, cx as f32 + 6.5, cy as f32 + 4.5, 8.0, 0.0, false);
}
+1 -1
View File
@@ -74,7 +74,7 @@ impl Spell {
}
fn foe(e: &Ent) -> bool {
e.alive && !e.held && e.captive == 0 && !e.chained && e.side() != 0
e.alive && !e.held && e.captive == 0 && !e.chained && e.side() != 0 && !crate::rogues::spell_proof(e)
}
fn own(e: &Ent) -> bool {