197 lines
8.5 KiB
Rust
197 lines
8.5 KiB
Rust
// The wild underground: rivers of water and lava across free maps (with neutral bridges) and
|
|
// natural caverns until about a quarter of the map is open - some with a pool, veins along the
|
|
// walls, now and then a small treasure or a creature in chains waiting to be freed.
|
|
// Everything the map or the mission placed on purpose (core, portals, gate, fortresses,
|
|
// islands, sealed caves, dwarf caves, rival dungeons) is left alone.
|
|
|
|
use crate::game::*;
|
|
use crate::map::*;
|
|
|
|
const OPEN_SHARE: f32 = 0.25;
|
|
|
|
impl Game {
|
|
pub fn add_wilds(&mut self, seed: u32, rivers: bool) {
|
|
let mut rng = Rng(seed.wrapping_mul(0x2c1b_3c6d) ^ 0x297a_2d39 | 1);
|
|
for _ in 0..6 {
|
|
rng.next();
|
|
}
|
|
let (w, h) = (mw() as i32, mh() as i32);
|
|
let area = (w * h) as f32;
|
|
if rivers {
|
|
// one river per ~100x100 tiles, at least one; some of lava
|
|
let n = ((area / 10000.0).round() as i32).clamp(1, 4);
|
|
for k in 0..n {
|
|
// away from the start: vertical rivers in the right part, horizontal ones above or below the core
|
|
let vertical = rng.f() < 0.5;
|
|
let pos = if vertical { 0.42 + rng.f() * 0.4 } else if rng.f() < 0.5 { 0.12 + rng.f() * 0.18 } else { 0.7 + rng.f() * 0.18 };
|
|
let r = River { lava: rng.f() < 0.4, vertical, pos, width: 3 + (rng.f() < 0.5) as i32, bridges: 2 + (area > 15000.0) as u32 };
|
|
let bridges = self.map.carve_river(&r, seed.wrapping_add(k as u32 * 7919));
|
|
// free maps: the bridges belong to nobody (anybody may claim them)
|
|
for b in bridges {
|
|
self.map.t[b].owner = Owner::None;
|
|
}
|
|
}
|
|
}
|
|
// what must not be touched: owned ground, buildings, mission pieces and their surroundings
|
|
let n = self.map.t.len();
|
|
let mut keep = vec![false; n];
|
|
let mark = |keep: &mut Vec<bool>, x: i32, y: i32, r: i32| {
|
|
for yy in y - r..=y + r {
|
|
for xx in x - r..=x + r {
|
|
if inb(xx, yy) {
|
|
keep[idx(xx, yy)] = true;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
for i in 0..n {
|
|
let t = &self.map.t[i];
|
|
let special = t.owner != Owner::None
|
|
|| matches!(t.kind, Kind::Core | Kind::Portal | Kind::Gate | Kind::Tower | Kind::Shard | Kind::Treasury | Kind::ManaSpring | Kind::RuneChest | Kind::Bridge | Kind::LavaBridge);
|
|
if special {
|
|
let (x, y) = xy(i);
|
|
mark(&mut keep, x, y, 3);
|
|
}
|
|
}
|
|
for c in self.dwarf_caves.clone() {
|
|
mark(&mut keep, c.at.0, c.at.1, 7);
|
|
}
|
|
for e in self.ents.iter().filter(|e| e.alive && (e.chained || e.hero || e.dwarf() || e.rival())) {
|
|
mark(&mut keep, e.x as i32, e.y as i32, 4);
|
|
}
|
|
let (cx, cy) = (self.map.core.0 + 1, self.map.core.1 + 1);
|
|
mark(&mut keep, cx, cy, 9);
|
|
let open = |m: &Map| m.t.iter().filter(|t| !t.solid() && t.kind != Kind::Rock).count() as f32;
|
|
let mut tries = 0;
|
|
while open(&self.map) < area * OPEN_SHARE && tries < 400 {
|
|
tries += 1;
|
|
let (x, y) = (rng.range(4, w - 5), rng.range(4, h - 5));
|
|
if keep[idx(x, y)] {
|
|
continue;
|
|
}
|
|
self.carve_cavern(x, y, &keep, &mut rng);
|
|
}
|
|
// giant mushrooms in the open: the underground's little wood
|
|
self.grow_mushrooms(seed);
|
|
// and now and then a den of wild beasts
|
|
self.place_beasts(seed);
|
|
}
|
|
|
|
// a cavern: a few overlapping blobs, ragged edges
|
|
fn carve_cavern(&mut self, x: i32, y: i32, keep: &[bool], rng: &mut Rng) {
|
|
let (w, h) = (mw() as i32, mh() as i32);
|
|
let blobs = 2 + rng.range(0, 3);
|
|
let mut tiles: Vec<usize> = Vec::new();
|
|
let (mut bx, mut by) = (x, y);
|
|
for _ in 0..blobs {
|
|
let r = rng.range(2, 5);
|
|
for yy in by - r..=by + r {
|
|
for xx in bx - r..=bx + r {
|
|
if xx < 3 || yy < 3 || xx >= w - 3 || yy >= h - 3 {
|
|
continue;
|
|
}
|
|
let d = (((xx - bx).pow(2) + (yy - by).pow(2)) as f32).sqrt();
|
|
if d > r as f32 + 0.3 - rng.f() * 0.9 {
|
|
continue;
|
|
}
|
|
let i = idx(xx, yy);
|
|
let t = &self.map.t[i];
|
|
if keep[i] || !t.solid() || t.kind == Kind::Rock || t.kind == Kind::Steel {
|
|
continue;
|
|
}
|
|
tiles.push(i);
|
|
}
|
|
}
|
|
bx += rng.range(-r, r);
|
|
by += rng.range(-r, r);
|
|
}
|
|
if tiles.len() < 6 {
|
|
return;
|
|
}
|
|
for &i in &tiles {
|
|
let t = &mut self.map.t[i];
|
|
t.kind = Kind::Floor;
|
|
t.owner = Owner::None;
|
|
t.gold = 0;
|
|
t.marked = false;
|
|
t.reinforced = false;
|
|
}
|
|
// veins in the walls around
|
|
for &i in &tiles {
|
|
let (tx, ty) = xy(i);
|
|
for &(dx, dy) in &DIR4 {
|
|
let (nx, ny) = (tx + dx, ty + dy);
|
|
if !inb(nx, ny) || keep[idx(nx, ny)] {
|
|
continue;
|
|
}
|
|
let t = &mut self.map.t[idx(nx, ny)];
|
|
if t.kind == Kind::Earth && rng.f() < 0.07 {
|
|
let r = rng.f();
|
|
let (k, g) = if r < 0.06 { (Kind::Ruby, rng.range(500, 900)) } else if r < 0.3 { (Kind::Silver, rng.range(300, 600)) } else { (Kind::Gold, rng.range(400, 900)) };
|
|
t.kind = k;
|
|
t.gold = g;
|
|
t.hp = 6.0;
|
|
}
|
|
}
|
|
}
|
|
// the middle: sometimes a pool, rarely a treasure or a creature in chains
|
|
let (sx, sy) = tiles.iter().fold((0i64, 0i64), |a, &i| (a.0 + xy(i).0 as i64, a.1 + xy(i).1 as i64));
|
|
let (mx, my) = ((sx / tiles.len() as i64) as i32, (sy / tiles.len() as i64) as i32);
|
|
let inside = |m: &Map, x: i32, y: i32| inb(x, y) && m.t[idx(x, y)].kind == Kind::Floor && m.t[idx(x, y)].owner == Owner::None;
|
|
let roll = rng.f();
|
|
if tiles.len() > 30 && roll < 0.35 {
|
|
let kind = if rng.f() < 0.4 { Kind::Lava } else { Kind::Water };
|
|
for yy in my - 1..=my + 1 {
|
|
for xx in mx - 1..=mx + 1 {
|
|
// only where floor surrounds it, so the cavern stays passable around the pool
|
|
if inside(&self.map, xx, yy) && DIR8.iter().all(|&(dx, dy)| !self.map.get(xx + dx, yy + dy).map_or(true, |t| t.solid())) {
|
|
self.map.t[idx(xx, yy)].kind = kind;
|
|
}
|
|
}
|
|
}
|
|
} else if roll < 0.47 && inside(&self.map, mx, my) {
|
|
for &(dx, dy) in &[(0, 0), (1, 0)] {
|
|
if inside(&self.map, mx + dx, my + dy) {
|
|
let t = &mut self.map.t[idx(mx + dx, my + dy)];
|
|
t.kind = Kind::Treasury;
|
|
t.gold = rng.range(200, 450);
|
|
t.res = (rng.f() < 0.3) as u8 * 2;
|
|
}
|
|
}
|
|
} else if roll < 0.53 && inside(&self.map, mx, my) {
|
|
let kinds = [EK::Spider, EK::Kobold, EK::Ogre, EK::Bat, EK::Mage];
|
|
let kind = kinds[rng.range(0, kinds.len() as i32 - 1) as usize];
|
|
let level = rng.range(2, 4) as u32;
|
|
let id = self.spawn(kind, false, mx as f32 + 0.5, my as f32 + 0.5, level);
|
|
if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) {
|
|
e.chained = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn open_share(g: &Game) -> f32 {
|
|
g.map.t.iter().filter(|t| !t.solid() && t.kind != Kind::Rock).count() as f32 / g.map.t.len() as f32
|
|
}
|
|
|
|
#[test]
|
|
fn free_maps_have_rivers_and_caverns_and_the_heroes_find_a_way() {
|
|
for seed in 1..=8u32 {
|
|
for &(w, h) in &[(64usize, 64usize), (128, 128)] {
|
|
let g = Game::new(seed, w, h);
|
|
let share = open_share(&g);
|
|
assert!(share > 0.2, "seed {} {}x{}: only {:.0}% open", seed, w, h, share * 100.0);
|
|
assert!(g.map.t.iter().any(|t| t.liquid()), "rivers or pools");
|
|
let (gx, gy) = g.map.gate;
|
|
assert!(g.map.hero_path_to_core(idx(gx + 1, gy + 1)).is_some(), "seed {}: heroes reach the core", seed);
|
|
let (cx, cy) = g.map.core;
|
|
assert_eq!(g.map.t[idx(cx + 1, cy + 1)].kind, Kind::Core);
|
|
}
|
|
}
|
|
}
|
|
}
|