// Traps and doors: the player marks a spot in the build menu, creatures in the forge // manufacture the piece (work, no gold), the finished crate waits in the forge, an imp carries // it to the spot and installs it. Traps fire at every non-player unit; doors let the dungeon's // creatures through, enemies have to smash them (locked doors stop everybody). use crate::game::*; use crate::map::*; use serde::{Deserialize, Serialize}; #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] pub enum TrapKind { Spikes, Poison, Fire, Roller, Alarm, Mine, Frost, WoodDoor, IronDoor, MagicDoor, } pub const TRAP_KINDS: [TrapKind; 10] = [ TrapKind::Spikes, TrapKind::Poison, TrapKind::Fire, TrapKind::Roller, TrapKind::Alarm, TrapKind::Mine, TrapKind::Frost, TrapKind::WoodDoor, TrapKind::IronDoor, TrapKind::MagicDoor, ]; // stages of a trap pub const ORDERED: u8 = 0; // marked, waiting for the forge pub const CRATED: u8 = 1; // finished, a crate in the forge pub const CARRIED: u8 = 2; // an imp carries it pub const ARMED: u8 = 3; // installed impl TrapKind { pub fn door(self) -> bool { matches!(self, TrapKind::WoodDoor | TrapKind::IronDoor | TrapKind::MagicDoor) } // work the forge needs (worker rate ~1 per second) pub fn work(self) -> f32 { match self { TrapKind::Spikes => 40.0, TrapKind::Poison => 60.0, TrapKind::Fire => 75.0, TrapKind::Roller => 80.0, TrapKind::Alarm => 25.0, TrapKind::Mine => 70.0, TrapKind::Frost => 60.0, TrapKind::WoodDoor => 35.0, TrapKind::IronDoor => 70.0, TrapKind::MagicDoor => 110.0, } } // health: doors are smashed, traps can be wrecked by giants pub fn hp(self) -> f32 { match self { TrapKind::WoodDoor => 400.0, TrapKind::IronDoor => 1100.0, TrapKind::MagicDoor => 1600.0, _ => 150.0, } } pub fn id(self) -> &'static str { match self { TrapKind::Spikes => "spikes", TrapKind::Poison => "poison", TrapKind::Fire => "fire", TrapKind::Roller => "roller", TrapKind::Alarm => "alarm", TrapKind::Mine => "mine", TrapKind::Frost => "frost", TrapKind::WoodDoor => "door_wood", TrapKind::IronDoor => "door_iron", TrapKind::MagicDoor => "door_magic", } } } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Trap { pub id: u32, pub kind: TrapKind, pub tile: usize, pub stage: u8, pub crate_tile: usize, pub progress: f32, pub cd: f32, pub hp: f32, pub locked: bool, pub carrier: u32, // imp carrying or installing it (entity id + 1) pub fired: f32, // time since it last went off (for drawing) pub aim: f32, // direction of the last shot (fire jet, roller) #[serde(default)] pub revealed: bool, // a fairy has found it: heroes wreck it #[serde(default)] pub sprung: bool, // has gone off at least once (its light shows to the other masters from then on) } // a boulder rolling down a corridor #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Roller { pub x: f32, pub y: f32, pub dx: f32, pub dy: f32, pub t: f32, pub hit: Vec, } // a poison cloud #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Cloud { pub x: f32, pub y: f32, pub t: f32, } const CLOUD_TIME: f32 = 6.0; // how hard a unit hits doors pub fn door_breaker(k: EK) -> f32 { match k { EK::Giant => 3.0, EK::Ram => 6.0, _ => 1.0, } } fn enemy(e: &Ent) -> bool { e.alive && !e.held && e.captive == 0 && !e.chained && e.side() != 0 } impl Game { pub fn trap_at(&self, i: usize) -> Option<&Trap> { self.traps.iter().find(|t| t.tile == i) } pub fn forge_tiles(&self) -> usize { self.counts[crate::game::ROOM_FORGE] } // the build menu: mark a spot for a trap or door (a door that stands there is locked/unlocked) pub fn place_trap(&mut self, kind: TrapKind, x: i32, y: i32) { if !inb(x, y) { return; } let i = idx(x, y); if let Some(k) = self.traps.iter().position(|t| t.tile == i) { let t = &mut self.traps[k]; if t.kind.door() && t.stage == ARMED { t.locked = !t.locked; let locked = t.locked; self.map.t[i].res = locked as u8; self.msg(if locked { "msg.door_locked" } else { "msg.door_unlocked" }, vec![]); self.sfx_ui(Sfx::Build); } else { self.hint(25, "hint.trap_where"); self.sfx_ui(Sfx::Error); } return; } let t = self.map.t[i]; if t.kind != Kind::Floor || t.owner != self.me { self.hint(25, "hint.trap_where"); self.sfx_ui(Sfx::Error); return; } if self.forge_tiles() == 0 { self.hint(26, "hint.need_forge"); self.sfx_ui(Sfx::Error); return; } // a wooden door costs wood if kind == TrapKind::WoodDoor && !self.spend_wood(crate::wood::DOOR_WOOD) { self.msg("msg.no_wood", vec![]); self.sfx_ui(Sfx::Error); return; } let id = self.next_id; self.next_id += 1; self.traps.push(Trap { id, kind, tile: i, stage: ORDERED, crate_tile: 0, progress: 0.0, cd: 0.0, hp: kind.hp(), locked: false, carrier: 0, fired: 99.0, aim: 0.0, revealed: false, sprung: false }); self.fx(8, x as f32 + 0.5, y as f32 + 0.5, 0.0, 0.0, 0.4, 0xff40d0ff); self.sfx_ui(Sfx::FlagSet); } // sell: traps and doors are taken out again (nothing comes back) pub(crate) fn remove_trap_at(&mut self, i: usize) -> bool { let Some(k) = self.traps.iter().position(|t| t.tile == i) else { return false }; let t = self.traps.remove(k); if t.kind.door() && t.stage == ARMED { self.map.t[i].kind = Kind::Floor; self.map.t[i].res = 0; } true } // the forge: the work of the creatures there goes into the oldest order pub(crate) fn update_forge(&mut self, dt: f32) { let work = std::mem::take(&mut self.forge_work); if let Some(k) = self.traps.iter().position(|t| t.stage == ORDERED) { self.traps[k].progress += work; if self.traps[k].progress >= self.traps[k].kind.work() { // finished: a crate on a forge tile let spot = self.map.t.iter().enumerate().filter(|(_, t)| t.kind == Kind::Forge && t.owner == self.me).map(|(i, _)| i).nth(self.traps[k].id as usize % self.forge_tiles().max(1)); match spot { Some(s) => { let t = &mut self.traps[k]; t.stage = CRATED; t.crate_tile = s; let (x, y) = xy(s); let key = t.kind.id(); self.fx(7, x as f32 + 0.5, y as f32 + 0.5, 0.0, 0.0, 0.8, 0xff40c0ff); self.sfx(Sfx::Build, x as f32 + 0.5, y as f32 + 0.5); self.msg("msg.trap_made", vec![format!("@trap.{}", key)]); } None => self.traps[k].progress = self.traps[k].kind.work(), } } } // orders on lost ground are dropped let map = &self.map; self.traps.retain(|t| t.stage == ARMED || map.t[t.tile].owner == self.me); for t in self.traps.iter_mut() { t.fired += dt; t.cd -= dt; } self.update_traps(dt); } // may the local master see this trap's light and glow? His own always; another master's // only once it has gone off (in a match the light must not betray a trap) pub fn trap_glows(&self, t: &Trap) -> bool { t.kind.door() || self.map.t[t.tile].owner == self.me || t.sprung } // is there forge work to do (an order waiting)? pub fn forge_orders(&self) -> bool { self.traps.iter().any(|t| t.stage == ORDERED) } // an imp looking for work: a finished crate to carry; returns the path to the crate pub(crate) fn crate_to_carry(&self, cur: usize) -> Option<(u32, Vec)> { let mut best: Option<(u32, Vec)> = None; for t in self.traps.iter().filter(|t| t.stage == CRATED && t.carrier == 0) { if let Some(p) = self.map.path_to(cur, t.crate_tile) { if best.as_ref().map_or(true, |b| p.len() < b.1.len()) { best = Some((t.id, p)); } } } best } // the imp's job: fetch the crate, carry it to the spot, install it pub(crate) fn install_job(&mut self, e: &mut Ent, id: u32, dt: f32) { let me = e.id + 1; let Some(k) = self.traps.iter().position(|t| t.id == id) else { e.job = Job::None; return; }; let cur = e.tile(); let (stage, crate_tile, tile) = (self.traps[k].stage, self.traps[k].crate_tile, self.traps[k].tile); if self.traps[k].carrier != me && self.traps[k].carrier != 0 { e.job = Job::None; return; } self.traps[k].carrier = me; match stage { CRATED => { if cur != crate_tile { if e.path.is_empty() { match self.map.creature_path(flies(e.kind), cur, crate_tile) { Some(p) => e.path = p, None => { self.traps[k].carrier = 0; e.job = Job::None; } } } return; } self.traps[k].stage = CARRIED; e.path.clear(); } CARRIED => { if cur != tile { if e.path.is_empty() { match self.map.creature_path(flies(e.kind), cur, tile) { Some(p) => e.path = p, None => { // no way there: the crate goes back into the forge self.traps[k].stage = CRATED; self.traps[k].crate_tile = cur; self.traps[k].carrier = 0; e.job = Job::None; } } } return; } e.working = true; e.timer += dt; if e.timer >= 2.0 { e.timer = 0.0; let t = &mut self.traps[k]; t.stage = ARMED; t.carrier = 0; let door = t.kind.door(); let (x, y) = xy(tile); if door { self.map.t[tile].kind = Kind::Door; self.map.t[tile].res = 0; } self.fx(8, x as f32 + 0.5, y as f32 + 0.5, 0.0, 0.0, 0.6, 0xff40d0ff); self.sfx(Sfx::Build, x as f32 + 0.5, y as f32 + 0.5); e.job = Job::None; } } _ => { self.traps[k].carrier = 0; e.job = Job::None; } } } // enemies on their way: doors are smashed (the magic door strikes back); true = busy pub(crate) fn bash_door(&mut self, e: &mut Ent, tile: usize, dt: f32) -> bool { let Some(k) = self.traps.iter().position(|t| t.tile == tile && t.stage == ARMED && t.kind.door()) else { return false }; e.working = true; let (tx, ty) = xy(tile); e.face = if tx as f32 + 0.5 > e.x { 1.0 } else { -1.0 }; e.timer += dt; let interval = st(e.kind).interval; if e.timer < interval { return true; } e.timer = 0.0; let mult = crate::traps::door_breaker(e.kind); let dmg = e.atk() * mult; let (cx, cy) = (tx as f32 + 0.5, ty as f32 + 0.5); self.traps[k].hp -= dmg; self.traps[k].fired = 0.0; self.traps[k].sprung = true; self.fx(2, cx, cy, 0.0, 0.0, 0.25, 0xff4080c0); self.sfx(Sfx::Dig, cx, cy); if self.traps[k].kind == TrapKind::MagicDoor && self.traps[k].cd <= 0.0 { // the magic door hurls the attacker back self.traps[k].cd = 1.5; e.hp -= 12.0; e.flash = 0.15; let (dx, dy) = (e.x - cx, e.y - cy); let l = (dx * dx + dy * dy).sqrt().max(0.01); let (nx, ny) = (e.x + dx / l * 0.8, e.y + dy / l * 0.8); if self.map.walk(nx as i32, ny as i32) { e.x = nx; e.y = ny; e.path.clear(); } self.fx(0, cx, cy, e.x, e.y, 0.2, 0xffff60d0); } if self.traps[k].hp <= 0.0 { self.traps.remove(k); self.map.t[tile].kind = Kind::Floor; self.map.t[tile].res = 0; self.fx(3, cx, cy, 0.0, 0.0, 0.8, 0xff505070); self.fx(6, cx, cy, 0.0, 0.0, 0.5, 0xff3a5a7a); self.msg("msg.door_broken", vec![]); } true } fn damage_unit(&mut self, j: usize, dmg: f32) { let o = &mut self.ents[j]; o.hp -= dmg; o.flash = 0.15; if o.hp <= 0.0 { self.kill(j); } } // armed traps go off when enemies come close // the title demo: its armed traps count down and go off (there is no forge economy there) pub(crate) fn demo_traps(&mut self, dt: f32) { for t in self.traps.iter_mut() { t.fired += dt; t.cd -= dt; } self.update_traps(dt); } fn update_traps(&mut self, dt: f32) { // poison clouds and rolling boulders for c in self.clouds.iter_mut() { c.t += dt; } self.clouds.retain(|c| c.t < CLOUD_TIME); let clouds: Vec<(f32, f32)> = self.clouds.iter().map(|c| (c.x, c.y)).collect(); for (cx, cy) in clouds { for j in 0..self.ents.len() { if enemy(&self.ents[j]) && (self.ents[j].x - cx).powi(2) + (self.ents[j].y - cy).powi(2) < 1.8 * 1.8 { self.damage_unit(j, 9.0 * dt); } } if self.vrng.f() < 6.0 * dt { let (ox, oy) = (self.vrng.f() * 2.4 - 1.2, self.vrng.f() * 2.4 - 1.2); self.parts.push(crate::particles::Part { kind: crate::particles::SMOKE, x: cx + ox, y: cy + oy, h: 0.2, vx: 0.0, vy: 0.0, vh: 0.15, t: 0.0, max: 1.6, col: crate::particles::abgr(120, 220, 80), size: 0.3 }); } } let mut rollers = std::mem::take(&mut self.rollers); for r in rollers.iter_mut() { r.t += dt; let (nx, ny) = (r.x + r.dx * 5.0 * dt, r.y + r.dy * 5.0 * dt); if !self.map.walk(nx as i32, ny as i32) || r.t > 6.0 { r.t = 99.0; self.fx(3, r.x, r.y, 0.0, 0.0, 0.6, 0xff505070); continue; } r.x = nx; r.y = ny; for j in 0..self.ents.len() { let o = &self.ents[j]; if enemy(o) && !r.hit.contains(&o.id) && (o.x - r.x).powi(2) + (o.y - r.y).powi(2) < 0.55 * 0.55 { r.hit.push(o.id); self.knock(j, r.dx * 0.9, r.dy * 0.9); if self.ents[j].alive { self.damage_unit(j, 70.0); } } } } rollers.retain(|r| r.t < 50.0); self.rollers = rollers; // fairies find traps, giants and heroes wreck the traps they know about let mut wrecked = Vec::new(); for k in 0..self.traps.len() { let t = &self.traps[k]; if t.stage != ARMED || t.kind.door() { continue; } let (tx, ty) = xy(t.tile); let (cx, cy) = (tx as f32 + 0.5, ty as f32 + 0.5); let mut damage = 0.0; let mut found = t.revealed; for e in self.ents.iter().filter(|e| e.alive && !e.held && e.active_hero()) { let d2 = (e.x - cx).powi(2) + (e.y - cy).powi(2); if e.kind == EK::Fairy && d2 < 9.0 { found = true; } if d2 < 1.6 && (found || e.kind == EK::Giant) { damage += if e.kind == EK::Giant { 40.0 } else { 15.0 }; } } let t = &mut self.traps[k]; if found && !t.revealed { t.revealed = true; } if damage > 0.0 { t.hp -= damage * dt; t.fired = 0.0; t.sprung = true; if t.hp <= 0.0 { wrecked.push(k); } } } for &k in wrecked.iter().rev() { let t = self.traps.remove(k); let (tx, ty) = xy(t.tile); self.fx(3, tx as f32 + 0.5, ty as f32 + 0.5, 0.0, 0.0, 0.8, 0xff505070); self.fx(6, tx as f32 + 0.5, ty as f32 + 0.5, 0.0, 0.0, 0.5, 0xff3a5a7a); self.msg("msg.trap_wrecked", vec![format!("@trap.{}", t.kind.id())]); } for k in 0..self.traps.len() { if k >= self.traps.len() { break; } let t = self.traps[k].clone(); if t.stage != ARMED || t.kind.door() { continue; } let (tx, ty) = xy(t.tile); let (cx, cy) = (tx as f32 + 0.5, ty as f32 + 0.5); let near = |r: f32, ents: &Vec| -> Vec { (0..ents.len()).filter(|&j| enemy(&ents[j]) && (ents[j].x - cx).powi(2) + (ents[j].y - cy).powi(2) < r * r).collect() }; match t.kind { TrapKind::Frost => { // always on: cold around it slows and bites, and keeps lava next to it cooled self.cool_lava_around(cx, cy, 2.5, 3.0); for j in near(2.5, &self.ents) { self.ents[j].slow = self.ents[j].slow.max(0.5); self.damage_unit(j, 2.0 * dt); } if self.vrng.f() < 4.0 * dt { let a = self.vrng.f() * 6.28; self.parts.push(crate::particles::Part { kind: crate::particles::MOTE, x: cx + a.cos() * 1.5, y: cy + a.sin() * 1.5, h: 0.1, vx: 0.0, vy: 0.0, vh: 0.3, t: 0.0, max: 1.2, col: crate::particles::abgr(170, 230, 255), size: 0.04 }); } } _ if t.cd > 0.0 => {} TrapKind::Spikes => { let hit = near(0.7, &self.ents); if !hit.is_empty() { self.traps[k].cd = 2.5; self.traps[k].fired = 0.0; self.traps[k].sprung = true; for j in hit { self.damage_unit(j, 45.0); } self.fx(2, cx, cy, 0.0, 0.0, 0.3, 0xffd0d0d0); self.sfx(Sfx::MeleeHit, cx, cy); } } TrapKind::Poison => { if !near(0.9, &self.ents).is_empty() { self.traps[k].cd = 12.0; self.traps[k].fired = 0.0; self.traps[k].sprung = true; self.clouds.push(Cloud { x: cx, y: cy, t: 0.0 }); self.sfx(Sfx::Magic, cx, cy); } } TrapKind::Fire => { // a jet of fire towards the nearest enemy in reach let targets = near(3.2, &self.ents); if let Some(&j) = targets.first() { let (ex, ey) = (self.ents[j].x, self.ents[j].y); let a = (ey - cy).atan2(ex - cx); self.traps[k].cd = 4.0; self.traps[k].fired = 0.0; self.traps[k].sprung = true; self.traps[k].aim = a; let (dx, dy) = (a.cos(), a.sin()); self.ignite_line(cx, cy, dx, dy, 3.2); for o in near(3.4, &self.ents) { let (rx, ry) = (self.ents[o].x - cx, self.ents[o].y - cy); let along = rx * dx + ry * dy; let side = (rx * dy - ry * dx).abs(); if along > 0.0 && side < 0.6 { self.damage_unit(o, 55.0); } } for q in 0..14 { let f = q as f32 / 14.0 * 3.2; let v = (dx * 3.0 + self.vrng.f() - 0.5, dy * 3.0 + self.vrng.f() - 0.5, 0.8); self.parts.push(crate::particles::Part { kind: crate::particles::EMBER, x: cx + dx * f * 0.3, y: cy + dy * f * 0.3, h: 0.35, vx: v.0, vy: v.1, vh: v.2, t: 0.0, max: 0.8, col: crate::particles::abgr(255, 140, 40), size: 0.07 }); } self.sfx(Sfx::Cannon, cx, cy); } } TrapKind::Roller => { let targets = near(4.0, &self.ents); if let Some(&j) = targets.first() { let (ex, ey) = (self.ents[j].x - cx, self.ents[j].y - cy); // along the main axis of the corridor towards the enemy let (dx, dy) = if ex.abs() > ey.abs() { (ex.signum(), 0.0) } else { (0.0, ey.signum()) }; self.traps[k].cd = 18.0; self.traps[k].fired = 0.0; self.traps[k].sprung = true; self.traps[k].aim = dy.atan2(dx); self.rollers.push(Roller { x: cx, y: cy, dx, dy, t: 0.0, hit: Vec::new() }); self.sfx(Sfx::DigDone, cx, cy); } } TrapKind::Alarm => { let seen = near(3.0, &self.ents); if let Some(&j) = seen.first() { self.traps[k].cd = 20.0; self.traps[k].fired = 0.0; self.traps[k].sprung = true; let target = self.ents[j].id; for o in self.ents.iter_mut() { if o.alive && o.creature() && !matches!(o.job, Job::Attack(_)) && (o.x - cx).powi(2) + (o.y - cy).powi(2) < 18.0 * 18.0 { o.job = Job::Attack(target); o.path.clear(); o.repath = 0.0; } } self.fx(7, cx, cy, 0.0, 0.0, 0.8, 0xff30a0ff); self.sfx(Sfx::FlagSet, cx, cy); self.msg("msg.alarm", vec![]); } } TrapKind::Mine => { if !near(0.8, &self.ents).is_empty() { for j in near(2.2, &self.ents) { if self.ents[j].alive { self.damage_unit(j, 160.0); } } self.shot(cx, cy, 0.6, cx, cy, 0.1, 0.05, 0xff40c0ff); self.style_last_fx(crate::particles::ST_CANNON); self.sfx(Sfx::Cannon, cx, cy); self.traps.remove(k); } } _ => {} } } } } #[cfg(test)] mod tests { use super::*; fn forge_game() -> (Game, i32, i32) { let mut g = Game::new(4, 64, 64); let (cx, cy) = g.map.core; for y in cy - 8..=cy + 8 { for x in cx + 4..=cx + 14 { let t = &mut g.map.t[idx(x, y)]; t.kind = Kind::Floor; t.owner = Owner::Player; } } for y in cy - 8..=cy - 6 { for x in cx + 4..=cx + 7 { g.map.t[idx(x, y)].kind = Kind::Forge; } } g.recount(); (g, cx, cy) } #[test] fn forge_makes_traps_and_imps_install_them() { let (mut g, cx, cy) = forge_game(); let (tx, ty) = (cx + 12, cy + 4); g.place_trap(TrapKind::Spikes, tx, ty); g.place_trap(TrapKind::WoodDoor, cx + 12, cy + 6); assert_eq!(g.traps.len(), 2); for k in 0..3 { g.spawn(EK::Cogling, false, cx as f32 + 5.5 + k as f32, cy as f32 - 2.5, 3); } let mut armed = false; for _ in 0..(20 * 200) { g.update(0.05); if g.traps.iter().filter(|t| t.stage == ARMED).count() == 2 { armed = true; break; } } assert!(armed, "stages: {:?}", g.traps.iter().map(|t| t.stage).collect::>()); assert_eq!(g.map.t[idx(cx + 12, cy + 6)].kind, Kind::Door); // a knight steps on the spikes let id = g.spawn(EK::Knight, true, tx as f32 + 0.5, ty as f32 + 0.5, 1); let hp0 = g.ents[g.find(id).unwrap()].max_hp; g.update(0.05); let hp = g.find(id).map_or(0.0, |j| g.ents[j].hp); assert!(hp < hp0, "spikes hurt"); } #[test] fn doors_stop_enemies_until_smashed_and_lock() { let (mut g, cx, cy) = forge_game(); let i = idx(cx + 10, cy); g.traps.push(Trap { id: 999, kind: TrapKind::WoodDoor, tile: i, stage: ARMED, crate_tile: 0, progress: 0.0, cd: 0.0, hp: 400.0, locked: false, carrier: 0, fired: 99.0, aim: 0.0, revealed: false, sprung: false }); g.map.t[i].kind = Kind::Door; assert!(g.map.walk_cost()(i).is_some()); g.place_trap(TrapKind::WoodDoor, cx + 10, cy); assert!(g.traps.iter().find(|t| t.id == 999).unwrap().locked); assert!(g.map.walk_cost()(i).is_none(), "locked doors stop our own creatures too"); // an ogre hero... a knight bashes it down let id = g.spawn(EK::Knight, true, cx as f32 + 10.5, cy as f32 - 0.5, 8); let j = g.find(id).unwrap(); let mut e = g.ents[j].clone(); let mut steps = 0; while g.trap_at(i).is_some() && steps < 2000 { g.bash_door(&mut e, i, 0.05); steps += 1; } assert!(g.trap_at(i).is_none(), "door smashed"); assert_eq!(g.map.t[i].kind, Kind::Floor); } #[test] fn fairies_find_traps_and_giants_wreck_them() { let (mut g, cx, cy) = forge_game(); let a = idx(cx + 10, cy); let b = idx(cx + 10, cy + 4); for (id, i, kind) in [(1001, a, TrapKind::Frost), (1002, b, TrapKind::Alarm)] { g.traps.push(Trap { id, kind, tile: i, stage: ARMED, crate_tile: 0, progress: 0.0, cd: 99.0, hp: kind.hp(), locked: false, carrier: 0, fired: 99.0, aim: 0.0, revealed: false, sprung: false }); } g.spawn(EK::Fairy, true, cx as f32 + 11.5, cy as f32 + 0.5, 1); g.spawn(EK::Giant, true, cx as f32 + 10.5, cy as f32 + 5.5, 1); for j in 0..g.ents.len() { g.ents[j].stun = 999.0; // stand still } for _ in 0..(20 * 15) { g.update(0.05); for j in 0..g.ents.len() { g.ents[j].stun = 999.0; } } assert!(g.traps.iter().all(|t| t.id != 1002), "the giant wrecked the alarm"); assert!(g.traps.iter().find(|t| t.id == 1001).map_or(true, |t| t.revealed), "the fairy found the frost glow"); } #[test] fn foreign_traps_glow_only_after_going_off() { let mut g = Game::new(3, 64, 64); let (cx, cy) = g.map.core; let mine = idx(cx + 2, cy - 2); let theirs = idx(cx + 3, cy - 2); g.map.t[mine].owner = g.me; g.map.t[theirs].owner = Owner::Rival1; let trap = |tile| Trap { id: 1, kind: TrapKind::Fire, tile, stage: ARMED, crate_tile: 0, progress: 0.0, cd: 0.0, hp: 10.0, locked: false, carrier: 0, fired: 99.0, aim: 0.0, revealed: false, sprung: false }; assert!(g.trap_glows(&trap(mine)), "my own trap"); let mut t = trap(theirs); assert!(!g.trap_glows(&t), "a rival's trap stays dark"); t.sprung = true; assert!(g.trap_glows(&t), "until it has gone off"); } }