Dwarf caves in all modes: neutral dwarves guard a hoard and sometimes a rune chest; the rune forge tower; imps repair towers before claiming
- 1-4 cave slots by map size, 50% each (at least one); knight treasure caves only where a mission places them - dwarves fight intruders near the cave, never chase far, heal at home, grow with distance and the player's army, never count for victory - rune chest: one free rune forge tower (chain bolt to three more enemies), upgradeable for gold, sold once it comes back (next mission / free play at once), unbuilt ones carry over - new tool (key N) shown in the tower menu while a rune tower can be built or upgraded Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d92b45a0b8
commit
9a9f04ae1f
17 files changed
+658
-32
No files matched your search
+355
@@ -0,0 +1,355 @@
|
||||
// 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<usize> = (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.path_to(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::<u32>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user