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:
1 parent
a1a8f9d71d
commit
ff48a69571
29 files changed
+724
-12
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; 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
@@ -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
@@ -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
|
||||
|
||||
@@ -29,6 +29,7 @@ mod particles;
|
||||
mod props;
|
||||
mod research;
|
||||
mod rival;
|
||||
mod rogues;
|
||||
mod render3d;
|
||||
mod rooms;
|
||||
mod save;
|
||||
|
||||
@@ -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
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user