Traps and doors made in a new forge room, installed by imps; new creature: the cogling
- forge room: creatures work there (coglings fastest), the work goes into the oldest planned trap or door; finished pieces wait as crates, imps carry and install them - traps: spike pit, poison vent (cloud), fire jet, stone roller, alarm bell (calls creatures), rune mine (one big blast), frost glow (slows around it) - doors: wood, iron, magic (hurls attackers back); own creatures pass, enemies smash them on their way; clicking a door with a door tool locks it for everybody - new 'Traps' category in the toolbar, forge in the rooms; texts EN/DE, bestiary entries; atlas grown for new textures and icons - tests: forge to installed trap, spikes hurt, doors lock and get smashed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1696297ac3
commit
4f20223c93
13 files changed
+1267
-24
No files matched your search
+604
@@ -0,0 +1,604 @@
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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 != Owner::Player {
|
||||
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;
|
||||
}
|
||||
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 });
|
||||
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 == Owner::Player).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 == Owner::Player);
|
||||
for t in self.traps.iter_mut() {
|
||||
t.fired += dt;
|
||||
t.cd -= dt;
|
||||
}
|
||||
self.update_traps(dt);
|
||||
}
|
||||
|
||||
// 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.path_to(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.path_to(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.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
|
||||
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;
|
||||
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
|
||||
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;
|
||||
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.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].aim = a;
|
||||
let (dx, dy) = (a.cos(), a.sin());
|
||||
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].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;
|
||||
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 });
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user