721 lines
28 KiB
Rust
721 lines
28 KiB
Rust
// 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<u32>,
|
|
}
|
|
|
|
// 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<usize>)> {
|
|
let mut best: Option<(u32, Vec<usize>)> = 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
|
|
}
|
|
|
|
pub(crate) 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<Ent>| -> Vec<usize> { (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::<Vec<_>>());
|
|
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");
|
|
}
|
|
}
|