// Dwarf caves: a neutral people that guards a treasure in a cave surrounded by rich veins. // The dwarves defend themselves but never stray far from their cave, and they never count // as opponents for victory. Some caves hold a chest with the plans of a rune forge tower: // the player may build it once for free. use crate::game::*; use crate::map::*; use serde::{Deserialize, Serialize}; pub const DWARF_SIDE: u8 = 4; const LEASH: f32 = 7.0; // dwarves do not chase enemies further from the cave centre const CHEST_CHANCE: f32 = 0.3; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct DwarfCave { pub at: (i32, i32), // centre pub base: u32, // base level of its dwarves (grows with the distance to the player's core) } impl Game { // 1 to 4 cave slots depending on the map size, each filled with a 50% chance (at least one) pub fn place_dwarf_caves(&mut self, seed: u32) { let mut rng = Rng(seed.wrapping_mul(0x9e37_79b9) ^ 0x5bd1_e995 | 1); for _ in 0..8 { rng.next(); } let area = (mw() * mh()) as f32 / 4096.0; let slots = (area.sqrt().ceil() as usize).clamp(1, 4); let mut n = (0..slots).filter(|_| rng.f() < 0.5).count(); if n == 0 { n = 1; } for _ in 0..n { if let Some(at) = self.dwarf_spot(&mut rng) { self.carve_dwarf_cave(at, &mut rng); } } } // a spot far from everything else whose surroundings are untouched neutral rock and earth fn dwarf_spot(&self, rng: &mut Rng) -> Option<(i32, i32)> { let (w, h) = (mw() as i32, mh() as i32); let (cx, cy) = (self.map.core.0 + 1, self.map.core.1 + 1); let mut others: Vec<(i32, i32)> = vec![(cx, cy), (self.map.gate.0 + 1, self.map.gate.1 + 1)]; others.extend(self.map.portals.iter().map(|&(x, y)| (x + 1, y + 1))); others.extend(self.map.treasures.iter().copied()); others.extend(self.rivals.iter().map(|r| (r.core.0 + 1, r.core.1 + 1))); others.extend(self.dwarf_caves.iter().map(|c| c.at)); let mut best = None; let mut bd = -1; for _ in 0..200 { let p = (rng.range(9, w - 10), rng.range(9, h - 10)); if (p.0 - cx).pow(2) + (p.1 - cy).pow(2) < 20 * 20 { continue; } let clear = (p.1 - 6..=p.1 + 6).all(|y| { (p.0 - 6..=p.0 + 6).all(|x| { let t = &self.map.t[idx(x, y)]; t.owner == Owner::None && !t.liquid() && !matches!(t.kind, Kind::Core | Kind::Portal | Kind::Gate | Kind::Tower | Kind::Shard | Kind::Bridge | Kind::LavaBridge | Kind::Treasury | Kind::ManaSpring | Kind::RuneChest) }) }); if !clear { continue; } let d = others.iter().map(|q| (p.0 - q.0).pow(2) + (p.1 - q.1).pow(2)).min().unwrap_or(i32::MAX); if d < 14 * 14 { continue; } // not hidden among the knights: keep off fortress ground and units if self.ents.iter().any(|e| e.alive && (e.x - p.0 as f32).abs() < 8.0 && (e.y - p.1 as f32).abs() < 8.0) { continue; } if d > bd { bd = d; best = Some(p); } } best } fn carve_dwarf_cave(&mut self, (cx, cy): (i32, i32), rng: &mut Rng) { let (px, py) = (self.map.core.0 + 1, self.map.core.1 + 1); let dist = (((cx - px).pow(2) + (cy - py).pow(2)) as f32).sqrt(); // rich veins around the cave, with gems for y in cy - 5..=cy + 5 { for x in cx - 5..=cx + 5 { let d = (((x - cx).pow(2) + (y - cy).pow(2)) as f32).sqrt(); let t = &mut self.map.t[idx(x, y)]; t.marked = false; t.reinforced = false; t.hp = 6.0; if (x - cx).abs() <= 2 && (y - cy).abs() <= 2 { t.kind = Kind::Floor; t.gold = 0; } else if d <= 5.2 && rng.f() < 0.7 { let r = rng.f(); let (k, g) = if r < 0.08 { (Kind::Diamond, rng.range(1200, 1800)) } else if r < 0.28 { (Kind::Ruby, rng.range(800, 1200)) } else if r < 0.45 { (Kind::Silver, rng.range(500, 800)) } else { (Kind::Gold, rng.range(800, 1400)) }; t.kind = k; t.gold = g; } else { t.kind = Kind::Earth; t.gold = 0; } } } // the hoard: treasury piles in the corners and along the walls, more the further away let hoard = (1500.0 + dist * 20.0).min(4000.0) as i32; let spots = [(-2, -2), (2, -2), (-2, 2), (2, 2), (0, -2), (0, 2), (-2, 0), (2, 0)]; let piles = ((hoard + TREASURY_TILE as i32 - 1) / TREASURY_TILE as i32).clamp(1, spots.len() as i32) as usize; for (k, &(dx, dy)) in spots.iter().take(piles).enumerate() { let t = &mut self.map.t[idx(cx + dx, cy + dy)]; t.kind = Kind::Treasury; t.gold = (hoard - k as i32 * TREASURY_TILE as i32).clamp(0, TREASURY_TILE as i32); let r = rng.f(); t.res = if r < 0.12 { 3 } else if r < 0.35 { 2 } else if r < 0.55 { 1 } else { 0 }; } if rng.f() < CHEST_CHANCE { self.map.t[idx(cx, cy)].kind = Kind::RuneChest; } let base = (2 + (dist / 30.0) as u32).min(7); let count = 4 + (dist > 60.0) as usize + (dist > 100.0) as usize; for k in 0..count { let a = k as f32 / count as f32 * std::f32::consts::TAU; let id = self.spawn(EK::Dwarf, false, cx as f32 + 0.5 + a.cos() * 1.2, cy as f32 + 0.5 + a.sin() * 1.2, base); if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) { e.side = DWARF_SIDE; e.home = Some((cx as f32 + 0.5, cy as f32 + 0.5)); e.happy = 1.0; } } self.dwarf_caves.push(DwarfCave { at: (cx, cy), base }); } // Every 30 s the dwarves of a cave grow stronger until they match about half the player's // army (or their base level): one of the weakest idle dwarves gains a level. pub fn update_dwarves(&mut self, dt: f32) { self.dwarf_t -= dt; if self.dwarf_t > 0.0 || self.dwarf_caves.is_empty() { return; } self.dwarf_t = 30.0; let army = self.army_value() * 0.5; for c in 0..self.dwarf_caves.len() { let home = (self.dwarf_caves[c].at.0 as f32 + 0.5, self.dwarf_caves[c].at.1 as f32 + 0.5); let mine: Vec = (0..self.ents.len()).filter(|&j| { let e = &self.ents[j]; e.alive && e.dwarf() && e.home == Some(home) }).collect(); if mine.is_empty() { continue; } let list: Vec<(EK, u32)> = mine.iter().map(|&j| (self.ents[j].kind, self.ents[j].level)).collect(); if Game::squad_value(&list) >= army { continue; } if let Some(&j) = mine.iter().filter(|&&j| self.ents[j].level < 10 && !matches!(self.ents[j].job, Job::Attack(_))).min_by_key(|&&j| self.ents[j].level) { let e = &mut self.ents[j]; e.level += 1; let old = e.max_hp; e.max_hp = st(e.kind).hp * (1.0 + 0.15 * (e.level - 1) as f32); e.hp += e.max_hp - old; } } } // a dwarf: fights what comes close to the cave, otherwise potters about at home and heals pub fn dwarf_ai(&mut self, e: &mut Ent, dt: f32) { let (hx, hy) = e.home.unwrap_or((e.x, e.y)); let from_home = |x: f32, y: f32| ((x - hx).powi(2) + (y - hy).powi(2)).sqrt(); if e.think <= 0.0 { e.think = 0.4; if !matches!(e.job, Job::Attack(_)) { if let Some(id) = self.nearest_enemy(e, 6.5) { if let Some(j) = self.find(id) { if from_home(self.ents[j].x, self.ents[j].y) < LEASH { e.job = Job::Attack(id); e.path.clear(); e.repath = 0.0; } } } } } if let Job::Attack(id) = e.job { let near = self.find(id).map_or(false, |j| from_home(self.ents[j].x, self.ents[j].y) < LEASH + 1.0); if near && from_home(e.x, e.y) < LEASH + 1.0 && self.attack_job(e, id, dt) { return; } e.job = Job::None; e.path.clear(); e.repath = 0.0; } e.hp = (e.hp + e.max_hp * 0.01 * dt).min(e.max_hp); if e.path.is_empty() && e.repath <= 0.0 { e.repath = 3.0 + self.rng.f() * 4.0; let (tx, ty) = if from_home(e.x, e.y) > 3.0 { (hx, hy) } else { (hx + self.rng.f() * 4.0 - 2.0, hy + self.rng.f() * 4.0 - 2.0) }; if self.map.walk(tx as i32, ty as i32) { if let Some(p) = self.map.creature_path(flies(e.kind), e.tile(), idx(tx as i32, ty as i32)) { e.path = p; } } } self.step_path(e, dt); } // rune forge towers that can be built now pub fn rune_available(&self) -> u32 { self.rune_free + self.rune_final } } #[cfg(test)] mod tests { use super::*; fn dwarves(g: &Game) -> usize { g.ents.iter().filter(|e| e.alive && e.dwarf()).count() } #[test] fn caves_by_map_size() { for seed in 1..=10u32 { for &(w, h, max) in &[(64usize, 64usize, 1usize), (128, 128, 2), (256, 256, 4)] { let g = Game::new_free(seed, w, h, 1); let n = g.dwarf_caves.len(); assert!(n >= 1 && n <= max, "seed {} {}x{}: {} caves", seed, w, h, n); assert!(g.map.treasures.is_empty(), "knight caves replaced"); assert!(dwarves(&g) >= 4 * n); for c in &g.dwarf_caves { let t = &g.map.t[idx(c.at.0, c.at.1)]; assert!(matches!(t.kind, Kind::Floor | Kind::RuneChest), "cave centre {:?}", t.kind); } } } } #[test] fn dwarves_are_no_opponents_and_stay_home() { let mut g = (1..100u32).map(|s| Game::new_free(s, 64, 64, 0)).find(|g| g.rivals.len() == 1 && !g.gate_on).unwrap(); g.eliminate_rival(0); for _ in 0..40 { g.update(0.05); } assert_eq!(g.state, 1, "dwarves alive, but the game is won"); assert!(dwarves(&g) > 0); } #[test] fn dwarves_fight_intruders_but_do_not_chase() { let mut g = Game::new_free(3, 64, 64, 0); let c = g.dwarf_caves[0].clone(); // an ogre of the player walks into the cave let id = g.spawn(EK::Ogre, false, c.at.0 as f32 + 0.5, c.at.1 as f32 + 0.5, 3); let mut hit = false; for _ in 0..(20 * 20) { g.update(0.05); if let Some(j) = g.find(id) { hit |= g.ents[j].hp < g.ents[j].max_hp; } } assert!(hit, "the dwarves attack"); let home = (c.at.0 as f32 + 0.5, c.at.1 as f32 + 0.5); for e in g.ents.iter().filter(|e| e.alive && e.dwarf()) { let d = ((e.x - home.0).powi(2) + (e.y - home.1).powi(2)).sqrt(); assert!(d < LEASH + 2.5, "dwarf strayed {:.1} tiles", d); } } #[test] fn dwarves_grow_with_the_army() { let mut g = Game::new_free(5, 64, 64, 0); let lvl = |g: &Game| g.ents.iter().filter(|e| e.alive && e.dwarf()).map(|e| e.level).sum::(); let before = lvl(&g); let (cx, cy) = g.map.core; for k in 0..8 { g.spawn(EK::Ogre, false, cx as f32 + 1.0 + k as f32 * 0.2, cy as f32 - 1.0, 8); } g.dwarf_t = 0.0; g.update_dwarves(0.1); assert!(lvl(&g) > before); } #[test] fn rune_chest_gives_a_free_tower_once() { let mut g = Game::new_free(5, 64, 64, 0); let (cx, cy) = g.map.core; let (tx, ty) = (cx + 4, cy + 1); g.map.t[idx(tx, ty)].kind = Kind::RuneChest; g.map.t[idx(tx, ty)].owner = Owner::None; g.open_rune_chest(idx(tx, ty)); assert_eq!(g.rune_available(), 1); g.map.t[idx(tx, ty)].owner = Owner::Player; let gold = g.gold; g.build_tower(TowerKind::Rune, tx, ty); assert_eq!(g.towers.len(), 1); assert_eq!(g.gold, gold, "free"); assert_eq!(g.rune_available(), 0); g.build_tower(TowerKind::Rune, tx + 1, ty); assert_eq!(g.towers.len(), 1, "only one per chest"); // free play: selling gives the base tower back at once - but only once g.sell_rect(tx, ty, tx, ty); assert_eq!(g.rune_available(), 1); g.build_tower(TowerKind::Rune, tx, ty); assert!(g.towers[0].rune_final); g.sell_rect(tx, ty, tx, ty); assert_eq!(g.rune_available(), 0, "no second refund"); // in a mission the tower comes back in the next one let mut m = crate::campaign::start_mission(0, None); let (cx, cy) = m.map.core; m.rune_free = 1; m.build_tower(TowerKind::Rune, cx + 4, cy + 1); assert_eq!(m.towers.len(), 1, "built in the mission"); m.sell_rect(cx + 4, cy + 1, cx + 4, cy + 1); assert_eq!((m.rune_available(), m.rune_next), (0, 1)); let c = crate::campaign::carry_from(&m); let next = crate::campaign::start_mission(1, Some(&c)); assert_eq!(next.rune_final, 1); } #[test] fn imps_repair_towers_before_claiming() { let mut g = Game::new(4, 64, 64); let (cx, cy) = g.map.core; // plenty of unclaimed floor next to the dungeon for y in cy - 3..=cy + 5 { for x in cx + 6..=cx + 12 { g.map.t[idx(x, y)].kind = Kind::Floor; g.map.t[idx(x, y)].owner = Owner::None; } } let (tx, ty) = (cx + 4, cy - 2); g.map.t[idx(tx, ty)].kind = Kind::Floor; g.map.t[idx(tx, ty)].owner = Owner::Player; g.build_tower(TowerKind::Arrow, tx, ty); assert_eq!(g.towers.len(), 1); g.towers[0].hp = 50.0; let gold = g.gold; for _ in 0..(20 * 20) { g.update(0.05); } assert!(g.towers[0].hp >= g.towers[0].max_hp() - 1.0, "repaired: {}", g.towers[0].hp); assert!(g.gold >= gold - 1.0, "repairs are free"); } #[test] fn missions_have_dwarf_caves() { let mut total = 0; for i in 0..crate::campaign::missions().len() { let g = crate::campaign::start_mission(i, None); total += g.dwarf_caves.len(); } println!("{} dwarf caves in the campaign", total); assert!(total >= 20); } }