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
+1
-1
@@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
|
||||
// From which mission (0-based) on each tool is available (same order as ui::TOOLS):
|
||||
// dig, hand, treasury, lair, farm, training, library, sell, flag, lightning, heal, imp,
|
||||
// torture, temple, arrow tower, cannon tower, magic tower, bridge, aerie, steel
|
||||
pub const TOOL_UNLOCK: [usize; 21] = [0, 0, 0, 0, 0, 2, 5, 0, 0, 3, 5, 0, 8, 12, 3, 7, 12, 1, 8, 17, 0];
|
||||
pub const TOOL_UNLOCK: [usize; 32] = [0, 0, 0, 0, 0, 2, 5, 0, 0, 3, 5, 0, 8, 12, 3, 7, 12, 1, 8, 17, 0, 7, 7, 10, 12, 12, 7, 16, 16, 7, 10, 16];
|
||||
|
||||
pub struct FortDef {
|
||||
pub at: (f32, f32), // centre as a fraction of the map size
|
||||
|
||||
+89
-7
@@ -107,6 +107,7 @@ pub enum EK {
|
||||
Gargoyle, // aerie + torture chamber: stone, tough
|
||||
Owl, // aerie + library: arcane bolts from afar
|
||||
Wyrm, // big aerie + treasury: rare, very strong, fire breath
|
||||
Cogling, // forge: a sooty little tinkerer, builds traps and doors fastest
|
||||
}
|
||||
|
||||
pub struct St {
|
||||
@@ -143,6 +144,7 @@ pub fn st(k: EK) -> St {
|
||||
EK::Gargoyle => s("unit.gargoyle", 260.0, 15.0, 1.0, 2.2, 1.2, 120, 0),
|
||||
EK::Owl => s("unit.owl", 70.0, 12.0, 4.5, 2.8, 1.3, 100, 0),
|
||||
EK::Wyrm => s("unit.wyrm", 650.0, 30.0, 3.0, 2.5, 1.4, 380, 0),
|
||||
EK::Cogling => s("unit.cogling", 90.0, 9.0, 1.0, 2.4, 0.9, 70, 0),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +181,8 @@ pub enum Job {
|
||||
MakeBed(usize), // new creature: sets up its bed
|
||||
FetchWage(usize), // payday: walks to the treasury (or the core) for the wage
|
||||
Help(usize), // imp with nothing else to do helps another imp at this tile
|
||||
Forge(usize), // creature works in the forge (traps and doors)
|
||||
Install(u32), // imp carries a finished trap or door (id) to its spot and installs it
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
@@ -405,7 +409,7 @@ pub struct Game {
|
||||
pub paused: bool,
|
||||
pub hand: Vec<u32>,
|
||||
#[serde(skip)]
|
||||
pub counts: [usize; 8], // treasury, lair, farm, training hall, library, torture chamber, temple, aerie (recounted)
|
||||
pub counts: [usize; N_ROOMS], // room tiles per room kind (ROOM_KINDS order, recounted)
|
||||
pub gate_hp: f32,
|
||||
pub gate_hit_t: f32,
|
||||
pub gate_stage: u32, // defender thresholds triggered (0..3)
|
||||
@@ -452,6 +456,14 @@ pub struct Game {
|
||||
pub stuck: Vec<crate::particles::Part>, // arrows stuck in walls and floors, scorch marks
|
||||
#[serde(skip, default = "crate::particles::vrng")]
|
||||
pub vrng: Rng, // random numbers for the particles only
|
||||
#[serde(default)]
|
||||
pub traps: Vec<crate::traps::Trap>, // traps and doors (ordered, in the forge, carried, armed)
|
||||
#[serde(default)]
|
||||
pub rollers: Vec<crate::traps::Roller>,
|
||||
#[serde(default)]
|
||||
pub clouds: Vec<crate::traps::Cloud>,
|
||||
#[serde(skip)]
|
||||
pub forge_work: f32, // work done in the forge this frame
|
||||
#[serde(skip)]
|
||||
pub demo: Option<crate::demo::Demo>, // the endless bridge battle behind the title menu
|
||||
#[serde(default)]
|
||||
@@ -492,9 +504,11 @@ impl Torch {
|
||||
}
|
||||
}
|
||||
|
||||
pub const ROOM_KINDS: [Kind; 8] = [Kind::Treasury, Kind::Lair, Kind::Farm, Kind::Training, Kind::Library, Kind::Torture, Kind::Temple, Kind::Aerie];
|
||||
pub const ROOM_COST: [i32; 8] = [60, 80, 100, 140, 160, 200, 180, 120];
|
||||
pub const ROOM_NAMES: [&str; 8] = ["room.treasury", "room.lair", "room.farm", "room.training", "room.library", "room.torture", "room.temple", "room.aerie"];
|
||||
pub const N_ROOMS: usize = 9;
|
||||
pub const ROOM_FORGE: usize = 8;
|
||||
pub const ROOM_KINDS: [Kind; N_ROOMS] = [Kind::Treasury, Kind::Lair, Kind::Farm, Kind::Training, Kind::Library, Kind::Torture, Kind::Temple, Kind::Aerie, Kind::Forge];
|
||||
pub const ROOM_COST: [i32; N_ROOMS] = [60, 80, 100, 140, 160, 200, 180, 120, 150];
|
||||
pub const ROOM_NAMES: [&str; N_ROOMS] = ["room.treasury", "room.lair", "room.farm", "room.training", "room.library", "room.torture", "room.temple", "room.aerie", "room.forge"];
|
||||
|
||||
// bridge cost per tile: over water, over lava
|
||||
pub const BRIDGE_COST: [i32; 2] = [30, 75];
|
||||
@@ -585,7 +599,7 @@ impl Game {
|
||||
state: 0,
|
||||
paused: false,
|
||||
hand: Vec::new(),
|
||||
counts: [0; 8],
|
||||
counts: [0; N_ROOMS],
|
||||
gate_hp: GATE_HP,
|
||||
gate_hit_t: -100.0,
|
||||
gate_stage: 0,
|
||||
@@ -613,6 +627,10 @@ impl Game {
|
||||
springs: 0,
|
||||
freed: 0,
|
||||
demo: None,
|
||||
traps: Vec::new(),
|
||||
rollers: Vec::new(),
|
||||
clouds: Vec::new(),
|
||||
forge_work: 0.0,
|
||||
parts: Vec::new(),
|
||||
stuck: Vec::new(),
|
||||
vrng: crate::particles::vrng(),
|
||||
@@ -754,7 +772,7 @@ impl Game {
|
||||
}
|
||||
|
||||
pub fn recount(&mut self) {
|
||||
let mut c = [0usize; 8];
|
||||
let mut c = [0usize; N_ROOMS];
|
||||
self.springs = self.map.t.iter().filter(|t| t.kind == Kind::ManaSpring && t.owner == Owner::Player).count();
|
||||
for t in &self.map.t {
|
||||
if t.owner == Owner::Player {
|
||||
@@ -1050,6 +1068,7 @@ impl Game {
|
||||
self.update_contested();
|
||||
self.update_units(dt);
|
||||
self.update_towers(dt);
|
||||
self.update_forge(dt);
|
||||
if !self.rivals.is_empty() {
|
||||
self.update_rivals(dt);
|
||||
}
|
||||
@@ -1260,6 +1279,9 @@ impl Game {
|
||||
if self.counts[6] >= 6 {
|
||||
opts.push((EK::Priestess, 2));
|
||||
}
|
||||
if self.counts[ROOM_FORGE] >= 6 {
|
||||
opts.push((EK::Cogling, 3));
|
||||
}
|
||||
// the Chainbreaker only visits big dungeons with experienced creatures and a rich treasury
|
||||
let room_tiles: usize = self.counts.iter().sum();
|
||||
let veteran = self.ents.iter().any(|e| e.alive && e.creature() && e.level >= 5);
|
||||
@@ -1687,6 +1709,18 @@ impl Game {
|
||||
}
|
||||
let next = e.path[0];
|
||||
let tile = self.map.t[next];
|
||||
if tile.kind == Kind::Door {
|
||||
if e.side() != 0 {
|
||||
// enemies smash doors on their way
|
||||
if self.bash_door(e, next, dt) {
|
||||
return true;
|
||||
}
|
||||
} else if tile.res & 1 == 1 && e.tile() != next {
|
||||
// locked: our own creatures have to find another way
|
||||
e.path.clear();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if !tile.walkable() && !(flies(e.kind) && tile.liquid()) {
|
||||
if (e.hero || e.rival()) && tile.diggable() {
|
||||
// heroes dig their way through
|
||||
@@ -2563,6 +2597,9 @@ impl Game {
|
||||
Job::Carry(id) => {
|
||||
self.carry_job(e, id, dt);
|
||||
}
|
||||
Job::Install(id) => {
|
||||
self.install_job(e, id, dt);
|
||||
}
|
||||
Job::Repair(id) => {
|
||||
let Some(k) = self.towers.iter().position(|t| t.id == id) else {
|
||||
e.job = Job::None;
|
||||
@@ -2731,6 +2768,16 @@ impl Game {
|
||||
if e.carry > 0 && self.start_deliver(e) {
|
||||
return;
|
||||
}
|
||||
// finished traps and doors are carried to their spots
|
||||
if let Some((id, p)) = self.crate_to_carry(cur) {
|
||||
if let Some(k) = self.traps.iter().position(|t| t.id == id) {
|
||||
self.traps[k].carrier = me;
|
||||
}
|
||||
e.path = p;
|
||||
e.job = Job::Install(id);
|
||||
e.timer = 0.0;
|
||||
return;
|
||||
}
|
||||
// damaged towers are repaired (free of charge) before more ground is claimed
|
||||
if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.damaged_tower_near(i).is_some(), true) {
|
||||
let end = p.last().copied().unwrap_or(cur);
|
||||
@@ -2992,6 +3039,22 @@ impl Game {
|
||||
e.job = Job::None;
|
||||
}
|
||||
}
|
||||
Job::Forge(t) => {
|
||||
if cur != t || self.map.t[t].kind != Kind::Forge || !self.forge_orders() {
|
||||
self.unreserve(me);
|
||||
e.job = Job::None;
|
||||
return;
|
||||
}
|
||||
e.working = true;
|
||||
let rate = if e.kind == EK::Cogling { 2.5 } else { 1.0 } * (1.0 + 0.1 * (e.level - 1) as f32);
|
||||
self.forge_work += rate * dt;
|
||||
e.xp += 2.5 * dt;
|
||||
e.timer -= dt;
|
||||
if e.timer <= 0.0 {
|
||||
self.unreserve(me);
|
||||
e.job = Job::None;
|
||||
}
|
||||
}
|
||||
Job::Study(t) => {
|
||||
if cur != t || self.map.t[t].kind != Kind::Library {
|
||||
self.unreserve(me);
|
||||
@@ -3281,6 +3344,20 @@ impl Game {
|
||||
}
|
||||
}
|
||||
}
|
||||
// the forge: orders waiting (coglings love it, others lend a hand now and then)
|
||||
if self.forge_orders() && self.counts[ROOM_FORGE] > 0 {
|
||||
let r = self.rng.f();
|
||||
if (e.kind == EK::Cogling && r < 0.9) || r < 0.25 {
|
||||
if let Some(p) = self.find_room_tile(cur, Kind::Forge, me, false) {
|
||||
let t = p.last().copied().unwrap_or(cur);
|
||||
self.map.t[t].reserved = me;
|
||||
e.path = p;
|
||||
e.job = Job::Forge(t);
|
||||
e.timer = 10.0 + self.rng.f() * 6.0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
let r = self.rng.f();
|
||||
if (e.kind == EK::Mage && r < 0.8) || r < 0.2 {
|
||||
if let Some(p) = self.find_room_tile(cur, Kind::Library, me, false) {
|
||||
@@ -3788,6 +3865,9 @@ impl Game {
|
||||
continue;
|
||||
}
|
||||
let i = idx(x, y);
|
||||
if self.remove_trap_at(i) {
|
||||
continue;
|
||||
}
|
||||
if let Some(k) = self.towers.iter().position(|t| t.tile == i && !t.hero) {
|
||||
refund += self.towers[k].invested() as f32 * 0.5;
|
||||
if self.towers[k].kind == TowerKind::Rune && !self.towers[k].rune_final {
|
||||
@@ -4620,6 +4700,8 @@ pub fn job_name(e: &Ent) -> &'static str {
|
||||
}
|
||||
Job::Eat(_) => "job.eat",
|
||||
Job::Train(_) => "job.train",
|
||||
Job::Forge(_) => "job.forge",
|
||||
Job::Install(_) => "job.install",
|
||||
Job::Study(_) => "job.study",
|
||||
Job::Attack(_) => "job.fight",
|
||||
Job::TearBridge(_) => "job.tear_bridge",
|
||||
@@ -5191,7 +5273,7 @@ mod tests {
|
||||
#[test]
|
||||
fn aerie_attracts_flyers() {
|
||||
let mut g = Game::new(3, 64, 64);
|
||||
g.counts = [16, 0, 9, 0, 6, 4, 0, 16];
|
||||
g.counts = [16, 0, 9, 0, 6, 4, 0, 16, 0];
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for _ in 0..200 {
|
||||
g.try_portal_for_test();
|
||||
|
||||
+139
-3
@@ -6,7 +6,7 @@ use macroquad::prelude::*;
|
||||
|
||||
pub const CELL: f32 = 16.0;
|
||||
const ATLAS_COLS: u32 = 16;
|
||||
const ATLAS_ROWS: u32 = 9;
|
||||
pub const ATLAS_ROWS: u32 = 14;
|
||||
|
||||
// atlas indices (4 variants each)
|
||||
pub const T_ROCK: u32 = 0;
|
||||
@@ -70,6 +70,8 @@ pub const I_STEEL: u32 = 134;
|
||||
pub const T_RIVAL1: u32 = 135; // 4 variants: floor of the green master
|
||||
pub const T_RIVAL2: u32 = 139; // 4 variants: floor of the blue master
|
||||
pub const I_RUNE: u32 = 143; // rune forge tower
|
||||
pub const T_FORGE: u32 = 144; // 4 variants: sooty flagstones with glowing cracks
|
||||
pub const I_TRAPS: u32 = 148; // 10 icons in TRAP_KINDS order
|
||||
|
||||
pub struct Gfx {
|
||||
pub atlas: Texture2D,
|
||||
@@ -749,6 +751,118 @@ fn gen_atlas() -> Image {
|
||||
let h = hash(cu, cv, seed) % 3;
|
||||
Some(if e { lo } else if h == 0 || (u % 4 == 1 && v % 4 == 1) { hi } else if d > 0.65 { lo } else { mid })
|
||||
};
|
||||
// forge: sooty dark flagstones, iron filings and glowing cracks
|
||||
for v in 0..4u32 {
|
||||
cv.cell(T_FORGE + v, |x, y| {
|
||||
let base = stones(x, y, v + 300, (52, 46, 46), (22, 18, 18), 8);
|
||||
if (x * 7 + y * 3 + v as i32 * 11) % 23 == 0 || ((x + y * 2 + v as i32) % 19 == 0 && y % 8 != 0) {
|
||||
Some(c(255, 120, 40))
|
||||
} else if hash(x, y, v + 301) % 13 == 0 {
|
||||
Some(c(120, 120, 130))
|
||||
} else {
|
||||
Some(base)
|
||||
}
|
||||
});
|
||||
}
|
||||
// trap and door icons
|
||||
let icons: [fn(i32, i32) -> Option<Color>; 10] = [
|
||||
// spikes
|
||||
|x, y| {
|
||||
if y >= 12 {
|
||||
Some(c(90, 90, 100))
|
||||
} else if y >= 3 && (x % 4 - 2).abs() <= (y - 3) / 4 && (2..14).contains(&x) {
|
||||
Some(if x % 4 == 2 { c(230, 230, 240) } else { c(160, 160, 175) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
// poison cloud
|
||||
|x, y| {
|
||||
let d = |cx: i32, cy: i32, r: i32| (x - cx).pow(2) + (y - cy).pow(2) <= r * r;
|
||||
if d(5, 9, 4) || d(10, 8, 4) || d(8, 5, 3) {
|
||||
Some(if (x + y) % 5 == 0 { c(200, 255, 140) } else { c(110, 200, 70) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
// fire jet
|
||||
|x, y| {
|
||||
let w = (y - 1).max(0) / 3;
|
||||
if (x - 8).abs() <= w && y <= 14 && y >= 1 {
|
||||
Some(if (x - 8).abs() <= w / 2 && y > 6 { c(255, 230, 120) } else { c(255, 120, 30) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
// stone roller
|
||||
|x, y| {
|
||||
let d = (x - 8).pow(2) + (y - 8).pow(2);
|
||||
if d <= 36 {
|
||||
Some(if (x + y) % 6 == 0 || d > 28 { c(80, 76, 72) } else { c(140, 132, 124) })
|
||||
} else if y == 14 && x < 15 {
|
||||
Some(c(90, 70, 50))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
// alarm bell
|
||||
|x, y| {
|
||||
let w = 2 + y / 3;
|
||||
if (2..=11).contains(&y) && (x - 8).abs() <= w.min(6) {
|
||||
Some(if x < 8 { c(255, 220, 90) } else { c(200, 160, 50) })
|
||||
} else if (12..=13).contains(&y) && (x - 8).abs() <= 1 {
|
||||
Some(c(140, 100, 40))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
// rune mine
|
||||
|x, y| {
|
||||
let d = (((x - 8).pow(2) + (y - 8).pow(2)) as f32).sqrt();
|
||||
if (5.0..6.5).contains(&d) || ((x - 8).abs() <= 1 && (3..=12).contains(&y)) || (y == 8 && (5..=11).contains(&x)) {
|
||||
Some(c(90, 200, 255))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
// frost glow
|
||||
|x, y| {
|
||||
let (dx, dy) = (x - 8, y - 8);
|
||||
if (dx == 0 || dy == 0 || dx == dy || dx == -dy) && dx.abs() <= 6 && dy.abs() <= 6 {
|
||||
Some(if dx.abs() + dy.abs() <= 2 { c(255, 255, 255) } else { c(150, 220, 255) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
// wooden door
|
||||
|x, y| {
|
||||
if (3..=12).contains(&x) && y >= 2 {
|
||||
Some(if x % 3 == 0 { c(80, 50, 28) } else if y == 5 || y == 11 { c(70, 70, 76) } else { c(130, 86, 48) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
// iron door
|
||||
|x, y| {
|
||||
if (3..=12).contains(&x) && y >= 2 {
|
||||
Some(if (x == 4 || x == 11) && y % 3 == 0 { c(200, 200, 210) } else if y == 7 { c(60, 60, 66) } else { c(110, 112, 122) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
// magic door
|
||||
|x, y| {
|
||||
if (3..=12).contains(&x) && y >= 2 {
|
||||
let rune = (x == 8 && (5..=11).contains(&y)) || (y == 8 && (6..=10).contains(&x));
|
||||
Some(if rune { c(240, 160, 255) } else if x == 3 || x == 12 || y == 2 { c(70, 40, 100) } else { c(110, 60, 160) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
];
|
||||
for (k, f) in icons.iter().enumerate() {
|
||||
cv.cell(I_TRAPS + k as u32, |x, y| f(x, y));
|
||||
}
|
||||
// aerie: straw and twigs on stone, a few feathers
|
||||
for v in 0..4u32 {
|
||||
cv.cell(T_AERIE + v, |x, y| {
|
||||
@@ -1005,6 +1119,19 @@ fn sprite_rows(k: EK) -> [&'static str; 10] {
|
||||
".kubbbbuk.",
|
||||
"..ky..yk..",
|
||||
],
|
||||
// cogling: a sooty little tinkerer with brass goggles and a hammer
|
||||
EK::Cogling => [
|
||||
"........SS",
|
||||
"...kkkk.SS",
|
||||
"..kOOOOk.b",
|
||||
".kyekkeykb",
|
||||
"..kOooOk.b",
|
||||
"...kCCk..b",
|
||||
"..kOCCOkbb",
|
||||
"..kOCCOk..",
|
||||
"...kO.Ok..",
|
||||
"...kk.kk..",
|
||||
],
|
||||
EK::Wyrm => [
|
||||
"R.......kw",
|
||||
"RR....krrk",
|
||||
@@ -1074,7 +1201,7 @@ fn pal(ch: char) -> Option<Color> {
|
||||
})
|
||||
}
|
||||
|
||||
pub const SPRITE_KINDS: [EK; 17] = [
|
||||
pub const SPRITE_KINDS: [EK; 18] = [
|
||||
EK::Worker,
|
||||
EK::Kobold,
|
||||
EK::Spider,
|
||||
@@ -1092,9 +1219,10 @@ pub const SPRITE_KINDS: [EK; 17] = [
|
||||
EK::Gargoyle,
|
||||
EK::Owl,
|
||||
EK::Wyrm,
|
||||
EK::Cogling,
|
||||
];
|
||||
// width of the sprite texture in pixels
|
||||
pub const SPRITE_TEX_W: f32 = (17 * 12) as f32;
|
||||
pub const SPRITE_TEX_W: f32 = (SPRITE_KINDS.len() * 12) as f32;
|
||||
pub const SPRITE_TEX_H: f32 = (2 * VIEWS * GEAR_TIERS * SPR_CELL) as f32;
|
||||
|
||||
pub fn sprite_index(k: EK) -> u32 {
|
||||
@@ -1504,6 +1632,14 @@ impl Gfx {
|
||||
Kind::Steel => T_STEEL + v,
|
||||
Kind::ManaSpring => T_PORTALFLOOR + v,
|
||||
Kind::RuneChest => T_FLOOR + v,
|
||||
Kind::Forge => T_FORGE + v,
|
||||
Kind::Door => {
|
||||
if claimed {
|
||||
T_CLAIM + v
|
||||
} else {
|
||||
T_FLOOR + v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -20,6 +20,7 @@ mod save;
|
||||
mod settings;
|
||||
mod sfx;
|
||||
mod tracks;
|
||||
mod traps;
|
||||
mod sound;
|
||||
mod touch;
|
||||
mod ui;
|
||||
@@ -115,6 +116,7 @@ fn world_action(app: &mut App, ui: &mut Ui, r3d: &R3d, inp: &mut Input, mx: f32,
|
||||
(Tool::Heal, _) => app.game.heal(wx, wy),
|
||||
(Tool::Summon, _) => app.game.summon(wx, wy),
|
||||
(Tool::Tower(k), Some((x, z))) => app.game.build_tower(k, x, z),
|
||||
(Tool::Trap(k), Some((x, z))) => app.game.place_trap(k, x, z),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -564,7 +566,8 @@ async fn main() {
|
||||
// ---------------- keyboard (only while playing)
|
||||
if playing {
|
||||
for i in 0..TOOLS.len() {
|
||||
if is_key_pressed(key(Action::Tool(i))) && app.game.tool_allowed(i) {
|
||||
let k = key(Action::Tool(i));
|
||||
if k != KeyCode::Unknown && is_key_pressed(k) && app.game.tool_allowed(i) && app.game.tool_visible(i) {
|
||||
ui.tool = TOOLS[i];
|
||||
inp.ldown = None;
|
||||
}
|
||||
@@ -1043,6 +1046,7 @@ fn draw_frame(app: &mut App, gfx: &Gfx, r3d: &mut R3d, ui: &mut Ui, menu: &mut M
|
||||
Tool::Bridge => (Color::new(0.9, 0.7, 0.4, 1.0), None),
|
||||
Tool::Steel => (Color::new(0.75, 0.8, 0.9, 1.0), None),
|
||||
Tool::Tower(_) => (Color::new(1.0, 0.6, 0.25, 1.0), None),
|
||||
Tool::Trap(_) => (Color::new(1.0, 0.4, 0.3, 1.0), None),
|
||||
};
|
||||
let ov = Overlay {
|
||||
hover: if over_ui { None } else { hover_tile.filter(|&(x, z)| inb(x, z)) },
|
||||
|
||||
+17
-2
@@ -61,6 +61,8 @@ pub enum Kind {
|
||||
Steel, // steel wall: hardly anyone gets through
|
||||
ManaSpring, // crystal spring: once claimed, more mana and a higher mana maximum
|
||||
RuneChest, // dwarven chest with the plans of a rune forge tower (claim it: one free build)
|
||||
Forge, // room: creatures manufacture traps and doors
|
||||
Door, // floor with an installed door (res bit 0: locked)
|
||||
}
|
||||
|
||||
// options for the map generator (campaign missions)
|
||||
@@ -89,7 +91,7 @@ pub struct River {
|
||||
}
|
||||
|
||||
// order matters for the packed savegame codec: only append new kinds
|
||||
const KINDS: [Kind; 27] = [
|
||||
const KINDS: [Kind; 29] = [
|
||||
Kind::Rock,
|
||||
Kind::Earth,
|
||||
Kind::Gold,
|
||||
@@ -117,6 +119,8 @@ const KINDS: [Kind; 27] = [
|
||||
Kind::Steel,
|
||||
Kind::ManaSpring,
|
||||
Kind::RuneChest,
|
||||
Kind::Forge,
|
||||
Kind::Door,
|
||||
];
|
||||
|
||||
// treasures (veins, treasury content, imp loads): 0 gold, 1 silver, 2 ruby, 3 diamond
|
||||
@@ -213,6 +217,8 @@ impl Tile {
|
||||
| Kind::Aerie
|
||||
| Kind::ManaSpring
|
||||
| Kind::RuneChest
|
||||
| Kind::Forge
|
||||
| Kind::Door
|
||||
)
|
||||
}
|
||||
// river tiles (open, but nobody can walk there)
|
||||
@@ -642,7 +648,15 @@ impl Map {
|
||||
}
|
||||
|
||||
pub fn walk_cost(&self) -> impl Fn(usize) -> Option<u32> + '_ {
|
||||
move |i| if self.t[i].walkable() { Some(1) } else { None }
|
||||
move |i| {
|
||||
let t = &self.t[i];
|
||||
// a locked door stops the dungeon's own creatures too
|
||||
if t.walkable() && !(t.kind == Kind::Door && t.res & 1 == 1) {
|
||||
Some(1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// flying creatures also cross water and lava
|
||||
@@ -663,6 +677,7 @@ impl Map {
|
||||
Kind::Earth => Some(7),
|
||||
Kind::Gold | Kind::Silver | Kind::Ruby | Kind::Diamond => Some(12),
|
||||
Kind::Rock | Kind::Core | Kind::Tower | Kind::Water | Kind::Lava => None,
|
||||
Kind::Door => Some(10),
|
||||
_ => Some(1),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ 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; 12] = [EK::Worker, EK::Kobold, EK::Spider, EK::Mage, EK::Ogre, EK::Jailer, EK::Priestess, EK::Chainbreaker, EK::Bat, EK::Gargoyle, EK::Owl, EK::Wyrm];
|
||||
const CREATURES: [EK; 13] = [EK::Worker, EK::Kobold, EK::Spider, EK::Mage, EK::Ogre, EK::Jailer, EK::Priestess, EK::Chainbreaker, EK::Cogling, EK::Bat, EK::Gargoyle, EK::Owl, EK::Wyrm];
|
||||
const HUMANS: [EK; 5] = [EK::Knight, EK::Archer, EK::Priest, EK::Dwarf, EK::Paladin];
|
||||
// buildings: indices into ui::TOOLS
|
||||
const BUILDINGS: [usize; 14] = [2, 3, 4, 5, 6, 12, 13, 18, 17, 19, 14, 15, 16, 20];
|
||||
const BUILDINGS: [usize; 25] = [2, 3, 4, 5, 6, 12, 13, 18, 21, 17, 19, 14, 15, 16, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31];
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Entry {
|
||||
@@ -243,6 +243,7 @@ impl Menu {
|
||||
crate::ui::Tool::Room(r) => crate::game::ROOM_NAMES[r].trim_start_matches("room.").to_string(),
|
||||
crate::ui::Tool::Tower(k) => crate::game::tower_name(k).trim_start_matches("tower.").to_string(),
|
||||
crate::ui::Tool::Bridge => "bridge".into(),
|
||||
crate::ui::Tool::Trap(k) => k.id().into(),
|
||||
_ => "steel".into(),
|
||||
};
|
||||
text_key = format!("best.b.{}.text", id);
|
||||
|
||||
+34
-1
@@ -72,7 +72,7 @@ fn usage(g: &Game) -> Use {
|
||||
continue;
|
||||
}
|
||||
match e.job {
|
||||
Job::Train(t) | Job::Study(t) | Job::Pray(t) if e.working || e.path.is_empty() => {
|
||||
Job::Train(t) | Job::Study(t) | Job::Pray(t) | Job::Forge(t) if e.working || e.path.is_empty() => {
|
||||
u.busy.insert(t);
|
||||
}
|
||||
_ => {}
|
||||
@@ -139,6 +139,8 @@ pub fn draw_props_solid(g: &Game, cam: &Cam, t: f32) {
|
||||
Kind::Library if h % 3 == 0 => lectern(c, l),
|
||||
Kind::ManaSpring => spring_basin(c, l),
|
||||
Kind::RuneChest => rune_chest(c, l),
|
||||
Kind::Forge if (x + z) % 2 == 0 => anvil(c, h, l),
|
||||
Kind::Forge if h % 4 == 1 => furnace(c, l),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -169,6 +171,22 @@ fn rune_chest(c: Vec3, l: f32) {
|
||||
cube(c + vec3(0.0, 0.22, 0.18), vec3(0.08, 0.1, 0.03), rgb(200, 170, 80, l));
|
||||
}
|
||||
|
||||
// forge: an anvil on a block, a hammer leaning against it
|
||||
fn anvil(c: Vec3, h: u32, l: f32) {
|
||||
let o = vec3(((h >> 3) % 3) as f32 * 0.08 - 0.08, 0.0, ((h >> 5) % 3) as f32 * 0.08 - 0.08);
|
||||
cube(c + o + vec3(0.0, 0.12, 0.0), vec3(0.22, 0.24, 0.22), rgb(70, 50, 36, l));
|
||||
cube(c + o + vec3(0.0, 0.28, 0.0), vec3(0.16, 0.08, 0.16), rgb(56, 56, 62, l));
|
||||
cube(c + o + vec3(0.0, 0.35, 0.0), vec3(0.38, 0.06, 0.16), rgb(76, 76, 84, l));
|
||||
cube(c + o + vec3(0.22, 0.35, 0.0), vec3(0.1, 0.04, 0.08), rgb(76, 76, 84, l));
|
||||
}
|
||||
|
||||
// forge: a squat stone furnace (its fire glows in the glow pass)
|
||||
fn furnace(c: Vec3, l: f32) {
|
||||
cube(c + vec3(0.0, 0.25, 0.0), vec3(0.6, 0.5, 0.6), rgb(64, 52, 50, l));
|
||||
cube(c + vec3(0.0, 0.55, 0.0), vec3(0.4, 0.1, 0.4), rgb(50, 42, 40, l));
|
||||
cube(c + vec3(0.0, 0.75, 0.0), vec3(0.16, 0.32, 0.16), rgb(44, 38, 38, l));
|
||||
}
|
||||
|
||||
// a stone basin around a mana spring
|
||||
fn spring_basin(c: Vec3, l: f32) {
|
||||
for (dx, dz, sx, sz) in [(0.0, -0.38, 0.8, 0.1), (0.0, 0.38, 0.8, 0.1), (-0.38, 0.0, 0.1, 0.8), (0.38, 0.0, 0.1, 0.8)] {
|
||||
@@ -234,6 +252,21 @@ pub fn draw_props_glow(g: &Game, cam: &Cam, t: f32) {
|
||||
}
|
||||
}
|
||||
}
|
||||
Kind::Forge if h % 4 == 1 && (x + z) % 2 != 0 => {
|
||||
// furnace fire
|
||||
let f = 0.7 + 0.3 * (t * 9.0 + k).sin();
|
||||
draw_cube(c + vec3(0.0, 0.2, 0.31), vec3(0.28, 0.2 * f, 0.02), None, Color::new(1.0, 0.5, 0.1, 0.95));
|
||||
draw_sphere(c + vec3(0.0, 0.25, 0.35), 0.22 * f, None, Color::new(1.0, 0.35, 0.05, 0.25));
|
||||
}
|
||||
Kind::Forge if (x + z) % 2 == 0 && (u.busy.contains(&i) || u.busy.iter().any(|&b| adj8(b, i))) => {
|
||||
// sparks fly from the anvil while somebody hammers
|
||||
for n in 0..5 {
|
||||
let ph = (t * 2.2 + n as f32 * 0.21 + k * 0.1).fract();
|
||||
let a = n as f32 * 1.3 + k;
|
||||
let p = c + vec3(a.cos() * ph * 0.5, 0.4 + ph * 0.4 - ph * ph * 0.9, a.sin() * ph * 0.5);
|
||||
draw_cube(p, Vec3::splat(0.03), None, Color::new(1.0, 0.75, 0.3, 1.0 - ph));
|
||||
}
|
||||
}
|
||||
Kind::RuneChest => {
|
||||
let f = 0.6 + 0.4 * (t * 2.5 + k).sin();
|
||||
draw_cube(c + vec3(0.0, 0.39, 0.0), vec3(0.14, 0.02, 0.14), None, Color::new(0.35, 0.8, 1.0, f));
|
||||
|
||||
+172
-1
@@ -14,7 +14,7 @@ pub const WALL_H: f32 = 0.95;
|
||||
const CHUNK: i32 = 16;
|
||||
pub const MAX_LIGHTS: usize = 24;
|
||||
const ATLAS_W: f32 = 256.0;
|
||||
const ATLAS_H: f32 = 144.0;
|
||||
const ATLAS_H: f32 = (crate::gfx::ATLAS_ROWS * 16) as f32;
|
||||
const DISP_AMP: f32 = 0.11; // wall surface irregularity
|
||||
|
||||
// ------------------------------------------------------------------ camera
|
||||
@@ -1027,6 +1027,7 @@ impl R3d {
|
||||
draw_towers_solid(g);
|
||||
crate::props::draw_props_solid(g, cam, t);
|
||||
draw_parts_solid(g);
|
||||
draw_traps_solid(g, t);
|
||||
|
||||
// ---- unlit glow / overlays
|
||||
gl_use_material(&self.glow);
|
||||
@@ -1063,6 +1064,7 @@ impl R3d {
|
||||
for f in &g.fx {
|
||||
draw_fx(f, t);
|
||||
}
|
||||
draw_traps_glow(g, t);
|
||||
draw_parts_glow(g);
|
||||
gl_use_default_material();
|
||||
crate::ui::begin_ui();
|
||||
@@ -2119,6 +2121,173 @@ fn draw_projectile(f: &Fx, p: f32, t: f32) {
|
||||
}
|
||||
}
|
||||
|
||||
// a crate with a finished trap or door (in the forge or carried by an imp)
|
||||
fn trap_crate(at: Vec3) {
|
||||
draw_cube(at + vec3(0.0, 0.14, 0.0), vec3(0.34, 0.28, 0.34), None, Color::from_rgba(120, 82, 46, 255));
|
||||
for dy in [0.04f32, 0.24] {
|
||||
draw_cube(at + vec3(0.0, dy, 0.0), vec3(0.36, 0.035, 0.36), None, Color::from_rgba(70, 70, 76, 255));
|
||||
}
|
||||
draw_cube(at + vec3(0.0, 0.29, 0.0), vec3(0.1, 0.02, 0.1), None, Color::from_rgba(200, 60, 40, 255));
|
||||
}
|
||||
|
||||
// traps and doors: the installed pieces, crates in the forge and on the way
|
||||
fn draw_traps_solid(g: &Game, t: f32) {
|
||||
use crate::traps::*;
|
||||
for tr in &g.traps {
|
||||
let (tx, tz) = xy(tr.tile);
|
||||
let c = vec3(tx as f32 + 0.5, 0.0, tz as f32 + 0.5);
|
||||
match tr.stage {
|
||||
CRATED => {
|
||||
let (x, z) = xy(tr.crate_tile);
|
||||
let k = (tr.id % 3) as f32 * 0.2 - 0.2;
|
||||
trap_crate(vec3(x as f32 + 0.5 + k, 0.0, z as f32 + 0.5 - k));
|
||||
}
|
||||
CARRIED => {
|
||||
if let Some(e) = g.ents.iter().find(|e| e.id + 1 == tr.carrier) {
|
||||
trap_crate(vec3(e.x, 0.42, e.y));
|
||||
}
|
||||
}
|
||||
ARMED => {
|
||||
let iron = Color::from_rgba(70, 70, 78, 255);
|
||||
let fresh = (1.0 - tr.fired / 0.6).clamp(0.0, 1.0);
|
||||
match tr.kind {
|
||||
TrapKind::Spikes => {
|
||||
draw_cube(c + vec3(0.0, 0.02, 0.0), vec3(0.8, 0.04, 0.8), None, iron);
|
||||
let up = 0.06 + 0.3 * fresh;
|
||||
for q in 0..9 {
|
||||
let (dx, dz) = ((q % 3) as f32 * 0.24 - 0.24, (q / 3) as f32 * 0.24 - 0.24);
|
||||
line3(c + vec3(dx, 0.03, dz), c + vec3(dx, 0.03 + up, dz), 0.05, Color::from_rgba(200, 200, 212, 255));
|
||||
}
|
||||
}
|
||||
TrapKind::Poison => {
|
||||
draw_cube(c + vec3(0.0, 0.03, 0.0), vec3(0.6, 0.06, 0.6), None, iron);
|
||||
for q in 0..3 {
|
||||
draw_cube(c + vec3(q as f32 * 0.16 - 0.16, 0.065, 0.0), vec3(0.06, 0.02, 0.5), None, Color::from_rgba(30, 40, 26, 255));
|
||||
}
|
||||
}
|
||||
TrapKind::Fire => {
|
||||
// a demon head on a pedestal, its mouth towards the last target
|
||||
draw_cube(c + vec3(0.0, 0.15, 0.0), vec3(0.36, 0.3, 0.36), None, Color::from_rgba(50, 40, 44, 255));
|
||||
let (ax, az) = (tr.aim.cos(), tr.aim.sin());
|
||||
draw_cube(c + vec3(0.0, 0.4, 0.0), vec3(0.3, 0.22, 0.3), None, Color::from_rgba(70, 56, 52, 255));
|
||||
draw_cube(c + vec3(ax * 0.16, 0.38, az * 0.16), vec3(0.14, 0.1, 0.14), None, Color::from_rgba(30, 20, 20, 255));
|
||||
for sgn in [-1.0f32, 1.0] {
|
||||
line3(c + vec3(-az * 0.12 * sgn, 0.5, ax * 0.12 * sgn), c + vec3(-az * 0.2 * sgn - ax * 0.08, 0.68, ax * 0.2 * sgn - az * 0.08), 0.04, Color::from_rgba(214, 204, 178, 255));
|
||||
}
|
||||
}
|
||||
TrapKind::Roller => {
|
||||
// the boulder waits in its niche (until it rolls)
|
||||
if tr.fired > 18.0 || tr.cd <= 0.0 {
|
||||
draw_sphere(c + vec3(0.0, 0.3, 0.0), 0.3, None, Color::from_rgba(120, 112, 104, 255));
|
||||
}
|
||||
draw_cube(c + vec3(0.0, 0.03, 0.0), vec3(0.8, 0.06, 0.8), None, Color::from_rgba(60, 56, 52, 255));
|
||||
}
|
||||
TrapKind::Alarm => {
|
||||
draw_cube(c + vec3(0.0, 0.35, 0.0), vec3(0.06, 0.7, 0.06), None, Color::from_rgba(90, 64, 40, 255));
|
||||
draw_cube(c + vec3(0.0, 0.7, 0.0), vec3(0.34, 0.05, 0.06), None, Color::from_rgba(90, 64, 40, 255));
|
||||
let swing = (t * 18.0).sin() * 0.06 * fresh.max(((tr.fired < 3.0) as u8) as f32);
|
||||
draw_cube(c + vec3(swing, 0.58, 0.0), vec3(0.18, 0.18, 0.18), None, Color::from_rgba(220, 180, 70, 255));
|
||||
}
|
||||
TrapKind::Mine => {
|
||||
draw_cube(c + vec3(0.0, 0.015, 0.0), vec3(0.4, 0.03, 0.4), None, Color::from_rgba(50, 54, 66, 255));
|
||||
}
|
||||
TrapKind::Frost => {
|
||||
draw_cube(c + vec3(0.0, 0.06, 0.0), vec3(0.4, 0.12, 0.4), None, Color::from_rgba(80, 90, 110, 255));
|
||||
}
|
||||
_ => {
|
||||
// doors: posts, lintel and the leaf across the corridor
|
||||
let across_x = g.map.get(tx - 1, tz).map_or(true, |t| t.solid()) || g.map.get(tx + 1, tz).map_or(true, |t| t.solid());
|
||||
let (len, thin) = if across_x { (vec3(1.0, 0.0, 0.0), vec3(0.0, 0.0, 1.0)) } else { (vec3(0.0, 0.0, 1.0), vec3(1.0, 0.0, 0.0)) };
|
||||
let size = |l: f32, h: f32, w: f32| len * l + Vec3::Y * h + thin * w;
|
||||
let (leaf, trim) = match tr.kind {
|
||||
TrapKind::WoodDoor => (Color::from_rgba(120, 80, 44, 255), Color::from_rgba(70, 68, 74, 255)),
|
||||
TrapKind::IronDoor => (Color::from_rgba(96, 98, 108, 255), Color::from_rgba(150, 150, 160, 255)),
|
||||
_ => (Color::from_rgba(70, 36, 100, 255), Color::from_rgba(30, 20, 40, 255)),
|
||||
};
|
||||
for sgn in [-1.0f32, 1.0] {
|
||||
draw_cube(c + len * (0.44 * sgn) + Vec3::Y * 0.6, size(0.12, 1.2, 0.3), None, Color::from_rgba(40, 32, 36, 255));
|
||||
}
|
||||
draw_cube(c + Vec3::Y * 1.22, size(1.0, 0.14, 0.32), None, Color::from_rgba(40, 32, 36, 255));
|
||||
let shake = if tr.fired < 0.2 { (t * 60.0).sin() * 0.02 } else { 0.0 };
|
||||
draw_cube(c + Vec3::Y * 0.56 + thin * shake, size(0.76, 1.1, 0.1), None, leaf);
|
||||
for y in [0.25f32, 0.85] {
|
||||
draw_cube(c + Vec3::Y * y, size(0.78, 0.06, 0.12), None, trim);
|
||||
}
|
||||
if tr.locked {
|
||||
draw_cube(c + Vec3::Y * 0.6, size(0.86, 0.08, 0.16), None, Color::from_rgba(60, 60, 66, 255));
|
||||
draw_cube(c + Vec3::Y * 0.6 + thin * 0.09, vec3(0.1, 0.12, 0.1), None, Color::from_rgba(230, 190, 70, 255));
|
||||
}
|
||||
// damage: darker the more it is beaten
|
||||
let _ = tr.hp / tr.kind.hp();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
for r in &g.rollers {
|
||||
let spin = r.t * 9.0;
|
||||
let at = vec3(r.x, 0.3, r.y);
|
||||
draw_sphere(at, 0.3, None, Color::from_rgba(120, 112, 104, 255));
|
||||
draw_cube(at + vec3(spin.cos() * 0.22 * r.dy.abs(), spin.sin() * 0.22, spin.cos() * 0.22 * r.dx.abs()), Vec3::splat(0.1), None, Color::from_rgba(80, 74, 70, 255));
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_traps_glow(g: &Game, t: f32) {
|
||||
use crate::traps::*;
|
||||
for tr in &g.traps {
|
||||
let (tx, tz) = xy(tr.tile);
|
||||
let c = vec3(tx as f32 + 0.5, 0.0, tz as f32 + 0.5);
|
||||
if tr.stage == ORDERED || tr.stage == CRATED || tr.stage == CARRIED {
|
||||
// the planned spot: a pulsing outline with a small progress bar
|
||||
let a = 0.4 + 0.3 * (t * 3.0 + tr.id as f32).sin();
|
||||
let col = Color::new(0.4, 0.85, 1.0, a);
|
||||
let y = 0.05;
|
||||
for (p0, p1) in [((-0.42f32, -0.42f32), (0.42f32, -0.42f32)), ((0.42, -0.42), (0.42, 0.42)), ((0.42, 0.42), (-0.42, 0.42)), ((-0.42, 0.42), (-0.42, -0.42))] {
|
||||
line3(c + vec3(p0.0, y, p0.1), c + vec3(p1.0, y, p1.1), 0.04, col);
|
||||
}
|
||||
let f = if tr.stage == ORDERED { (tr.progress / tr.kind.work()).clamp(0.0, 1.0) } else { 1.0 };
|
||||
line3(c + vec3(-0.35, y, 0.0), c + vec3(-0.35 + 0.7 * f, y, 0.0), 0.08, Color::new(1.0, 0.7, 0.3, 0.8));
|
||||
continue;
|
||||
}
|
||||
if tr.stage != ARMED {
|
||||
continue;
|
||||
}
|
||||
match tr.kind {
|
||||
TrapKind::Poison => disc(c + vec3(0.0, 0.07, 0.0), 0.22, Color::new(0.4, 1.0, 0.3, 0.3 + 0.15 * (t * 2.0).sin()), 12),
|
||||
TrapKind::Fire => {
|
||||
let (ax, az) = (tr.aim.cos(), tr.aim.sin());
|
||||
let f = 0.5 + 0.5 * (t * 6.0).sin();
|
||||
draw_sphere(c + vec3(ax * 0.24, 0.38, az * 0.24), 0.05 + 0.03 * f, None, Color::new(1.0, 0.5, 0.1, 0.9));
|
||||
}
|
||||
TrapKind::Mine => {
|
||||
let glow = 0.4 + 0.4 * (t * 2.5 + tr.id as f32).sin();
|
||||
ring(c + vec3(0.0, 0.04, 0.0), 0.16, Color::new(0.35, 0.8, 1.0, glow));
|
||||
line3(c + vec3(-0.1, 0.04, 0.0), c + vec3(0.1, 0.04, 0.0), 0.03, Color::new(0.35, 0.8, 1.0, glow));
|
||||
line3(c + vec3(0.0, 0.04, -0.1), c + vec3(0.0, 0.04, 0.1), 0.03, Color::new(0.35, 0.8, 1.0, glow));
|
||||
}
|
||||
TrapKind::Frost => {
|
||||
let p = 1.0 + 0.1 * (t * 2.0).sin();
|
||||
draw_cube(c + vec3(0.0, 0.3, 0.0), vec3(0.12, 0.36 * p, 0.12), None, Color::new(0.75, 0.92, 1.0, 0.9));
|
||||
draw_cube(c + vec3(0.1, 0.22, 0.05), vec3(0.07, 0.2, 0.07), None, Color::new(0.7, 0.9, 1.0, 0.85));
|
||||
draw_cube(c + vec3(-0.08, 0.2, -0.06), vec3(0.07, 0.16, 0.07), None, Color::new(0.7, 0.9, 1.0, 0.85));
|
||||
disc(c + vec3(0.0, 0.02, 0.0), 2.5, Color::new(0.6, 0.85, 1.0, 0.06), 24);
|
||||
}
|
||||
TrapKind::MagicDoor => {
|
||||
let across_x = g.map.get(tx - 1, tz).map_or(true, |t| t.solid()) || g.map.get(tx + 1, tz).map_or(true, |t| t.solid());
|
||||
let thin = if across_x { vec3(0.0, 0.0, 1.0) } else { vec3(1.0, 0.0, 0.0) };
|
||||
let glow = 0.5 + 0.5 * (t * 2.0).sin();
|
||||
for side in [-1.0f32, 1.0] {
|
||||
let p = c + Vec3::Y * 0.6 + thin * (0.06 * side);
|
||||
draw_cube(p, vec3(0.06, 0.4, 0.06), None, Color::new(0.9, 0.5, 1.0, 0.5 + 0.4 * glow));
|
||||
draw_cube(p + Vec3::Y * 0.06, vec3(0.22, 0.06, 0.06), None, Color::new(0.9, 0.5, 1.0, 0.5 + 0.4 * glow));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// particles that glow (embers, sparks, smoke, flashes, shock waves)
|
||||
fn draw_parts_glow(g: &Game) {
|
||||
use crate::particles::*;
|
||||
@@ -2361,6 +2530,8 @@ pub fn minimap_color(tile: &Tile) -> [u8; 4] {
|
||||
Kind::Steel => [150, 160, 180, 255],
|
||||
Kind::ManaSpring => [90, 230, 255, 255],
|
||||
Kind::RuneChest => [80, 200, 255, 255],
|
||||
Kind::Forge => [150, 70, 40, 255],
|
||||
Kind::Door => [140, 90, 50, 255],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -25,7 +25,7 @@ pub enum Action {
|
||||
}
|
||||
|
||||
pub fn all_actions() -> Vec<Action> {
|
||||
let mut v: Vec<Action> = (0..21).map(Action::Tool).collect();
|
||||
let mut v: Vec<Action> = (0..crate::ui::TOOLS.len()).map(Action::Tool).collect();
|
||||
v.extend_from_slice(&[
|
||||
Action::CamForward,
|
||||
Action::CamBack,
|
||||
@@ -70,7 +70,8 @@ impl Action {
|
||||
fn default_key(self) -> KeyCode {
|
||||
use KeyCode::*;
|
||||
match self {
|
||||
Action::Tool(i) => [Key1, Key2, Key3, Key4, Key5, Key6, Key7, Key8, Key9, Key0, X, V, K, J, B, C, T, U, Y, O, N][i.min(20)],
|
||||
// later tools have no key of their own (they can be bound in the settings)
|
||||
Action::Tool(i) => [Key1, Key2, Key3, Key4, Key5, Key6, Key7, Key8, Key9, Key0, X, V, K, J, B, C, T, U, Y, O, N].get(i).copied().unwrap_or(Unknown),
|
||||
Action::CamForward => W,
|
||||
Action::CamBack => S,
|
||||
Action::CamLeft => A,
|
||||
@@ -112,6 +113,9 @@ const KEY_NAMES: &[(KeyCode, &str)] = &[
|
||||
];
|
||||
|
||||
pub fn key_name(k: KeyCode) -> String {
|
||||
if k == KeyCode::Unknown {
|
||||
return "-".into();
|
||||
}
|
||||
KEY_NAMES.iter().find(|(c, _)| *c == k).map(|(_, n)| n.to_string()).unwrap_or_else(|| "?".into())
|
||||
}
|
||||
|
||||
|
||||
+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);
|
||||
}
|
||||
}
|
||||
@@ -157,10 +157,11 @@ pub enum Tool {
|
||||
Tower(TowerKind),
|
||||
Bridge,
|
||||
Steel,
|
||||
Trap(crate::traps::TrapKind),
|
||||
}
|
||||
|
||||
// New tools are appended so that existing key bindings ("tool<i>") keep their meaning
|
||||
pub const TOOLS: [Tool; 21] = [
|
||||
pub const TOOLS: [Tool; 32] = [
|
||||
Tool::Dig,
|
||||
Tool::Hand,
|
||||
Tool::Room(0),
|
||||
@@ -182,6 +183,17 @@ pub const TOOLS: [Tool; 21] = [
|
||||
Tool::Room(7),
|
||||
Tool::Steel,
|
||||
Tool::Tower(TowerKind::Rune),
|
||||
Tool::Room(8),
|
||||
Tool::Trap(crate::traps::TrapKind::Spikes),
|
||||
Tool::Trap(crate::traps::TrapKind::Poison),
|
||||
Tool::Trap(crate::traps::TrapKind::Fire),
|
||||
Tool::Trap(crate::traps::TrapKind::Roller),
|
||||
Tool::Trap(crate::traps::TrapKind::Alarm),
|
||||
Tool::Trap(crate::traps::TrapKind::Mine),
|
||||
Tool::Trap(crate::traps::TrapKind::Frost),
|
||||
Tool::Trap(crate::traps::TrapKind::WoodDoor),
|
||||
Tool::Trap(crate::traps::TrapKind::IronDoor),
|
||||
Tool::Trap(crate::traps::TrapKind::MagicDoor),
|
||||
];
|
||||
|
||||
|
||||
@@ -203,7 +215,9 @@ pub fn tool_info(t: Tool) -> ToolInfo {
|
||||
Tool::Room(4) => ("library", T_LIBRARY, Some(4)),
|
||||
Tool::Room(5) => ("torture", T_TORTURE, Some(5)),
|
||||
Tool::Room(6) => ("temple", T_TEMPLE, Some(6)),
|
||||
Tool::Room(_) => ("aerie", T_AERIE, Some(7)),
|
||||
Tool::Room(7) => ("aerie", T_AERIE, Some(7)),
|
||||
Tool::Room(_) => ("forge", T_FORGE, Some(8)),
|
||||
Tool::Trap(k) => (k.id(), I_TRAPS + crate::traps::TRAP_KINDS.iter().position(|&t| t == k).unwrap_or(0) as u32, None),
|
||||
Tool::Tower(TowerKind::Arrow) => ("arrow", I_ARROW, None),
|
||||
Tool::Tower(TowerKind::Cannon) => ("cannon", I_CANNON, None),
|
||||
Tool::Tower(TowerKind::Magic) => ("magic", I_MAGIC, None),
|
||||
@@ -226,6 +240,7 @@ pub fn tool_info(t: Tool) -> ToolInfo {
|
||||
// costs that do not depend on the game (bestiary)
|
||||
pub fn tool_cost_static(t: Tool) -> String {
|
||||
match t {
|
||||
Tool::Trap(k) => trf("cost.work", &[(k.work() as i32).to_string()]),
|
||||
Tool::Bridge => trf("cost.bridge", &[BRIDGE_COST[0].to_string(), BRIDGE_COST[1].to_string()]),
|
||||
Tool::Steel => trf("cost.steel", &[STEEL_COST[0].to_string(), STEEL_COST[1].to_string()]),
|
||||
Tool::Tower(TowerKind::Rune) => {
|
||||
@@ -246,6 +261,7 @@ pub fn tool_cost_static(t: Tool) -> String {
|
||||
|
||||
pub fn tool_cost(t: Tool, g: &Game) -> String {
|
||||
match t {
|
||||
Tool::Trap(_) => tool_cost_static(t),
|
||||
Tool::Room(r) => trf("cost.gold_tile", &[ROOM_COST[r].to_string()]),
|
||||
Tool::Lightning => trf("cost.mana", &["50".into()]),
|
||||
Tool::Heal => trf("cost.mana", &["60".into()]),
|
||||
@@ -269,11 +285,12 @@ pub fn tool_cost(t: Tool, g: &Game) -> String {
|
||||
}
|
||||
|
||||
// Touch toolbar: tool categories (indices into TOOLS); categories with several tools open a row
|
||||
pub const CATS: [(&str, &[usize]); 6] = [
|
||||
pub const CATS: [(&str, &[usize]); 7] = [
|
||||
("cat.dig", &[0]),
|
||||
("cat.hand", &[1]),
|
||||
("cat.rooms", &[2, 3, 4, 5, 6, 12, 13, 18, 17]),
|
||||
("cat.rooms", &[2, 3, 4, 5, 6, 12, 13, 18, 21, 17]),
|
||||
("cat.towers", &[14, 15, 16, 20]),
|
||||
("cat.traps", &[22, 23, 24, 25, 26, 27, 28, 29, 30, 31]),
|
||||
("cat.spells", &[9, 10, 11]),
|
||||
("cat.orders", &[8, 7, 19]),
|
||||
];
|
||||
@@ -1107,6 +1124,22 @@ pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec
|
||||
return out;
|
||||
}
|
||||
let Some((x, y)) = tile else { return out };
|
||||
if let Some(tr) = g.trap_at(idx(x, y)) {
|
||||
use crate::traps::*;
|
||||
out.push((tr_name(tr.kind), FRAME_HI));
|
||||
let state = match tr.stage {
|
||||
ORDERED => trf("trap.ordered", &[((tr.progress / tr.kind.work() * 100.0) as i32).to_string()]),
|
||||
CRATED => tr_key("trap.crated"),
|
||||
CARRIED => tr_key("trap.carried"),
|
||||
_ if tr.kind.door() => trf("info.hp", &[(tr.hp.max(0.0) as i32).to_string(), (tr.kind.hp() as i32).to_string()]),
|
||||
_ => tr_key("trap.armed"),
|
||||
};
|
||||
out.push((state, if tr.stage == ARMED { GREEN } else { GOLD }));
|
||||
if tr.kind.door() && tr.stage == ARMED {
|
||||
out.push((tr_key(if tr.locked { "trap.locked" } else { "trap.unlocked" }), DIM));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
let Some(t) = g.map.get(x, y) else { return out };
|
||||
let owner = match t.owner {
|
||||
Owner::Player => "owner.player",
|
||||
@@ -1249,3 +1282,11 @@ mod clip_tests {
|
||||
assert!(clip_segment(vec2(-5.0, -1.0), vec2(-1.0, 20.0), 10.0, 10.0).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
fn tr_name(k: crate::traps::TrapKind) -> String {
|
||||
tr(&format!("trap.{}", k.id()))
|
||||
}
|
||||
|
||||
fn tr_key(key: &str) -> String {
|
||||
tr(key)
|
||||
}
|
||||
Reference in new issue
Block a user