368 lines
13 KiB
Rust
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));
|
|
}
|
|
}
|