Files
dungeoncraft/src/rogues.rs
T

368 lines
13 KiB
Rust

// 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);
self.sfx(Sfx::Steal, e.x, e.y);
}
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.bane = crate::names::BANE_SNARE;
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::Snare, 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::Sermon, 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));
}
}