Files
dungeoncraft/src/surface.rs
T

225 lines
9.7 KiB
Rust

// The surface world: part of a map lies above ground - grass in daylight, forests whose trees
// imps fell for wood, winding paths, and a cliff with a few cave mouths between the dungeon in
// the rock and the open land. (The knights' fortress up there: see fortress.rs.)
use crate::game::*;
use crate::map::*;
// value noise in 0..1, smooth over `cell` tiles
fn noise(x: i32, y: i32, cell: i32, seed: u32) -> f32 {
let h = |x: i32, y: i32| {
let mut v = (x as u32).wrapping_mul(374761393) ^ (y as u32).wrapping_mul(668265263) ^ seed.wrapping_mul(2246822519);
v = (v ^ (v >> 13)).wrapping_mul(1274126177);
((v ^ (v >> 16)) & 0xffff) as f32 / 65535.0
};
let (cx, cy) = (x.div_euclid(cell), y.div_euclid(cell));
let (fx, fy) = ((x.rem_euclid(cell)) as f32 / cell as f32, (y.rem_euclid(cell)) as f32 / cell as f32);
let s = |t: f32| t * t * (3.0 - 2.0 * t);
let (sx, sy) = (s(fx), s(fy));
let a = h(cx, cy) * (1.0 - sx) + h(cx + 1, cy) * sx;
let b = h(cx, cy + 1) * (1.0 - sx) + h(cx + 1, cy + 1) * sx;
a * (1.0 - sy) + b * sy
}
impl Game {
// Everything east of `from` (share of the width, the edge a little ragged) becomes surface:
// open grass with forests and paths. A cliff along the edge keeps the dungeon apart, broken
// by `mouths` cave entrances. Mission pieces (core, portals, gate, towers ...) stay.
pub fn add_surface(&mut self, seed: u32, from: f32, mouths: u32) {
let (w, h) = (mw() as i32, mh() as i32);
let edge = |y: i32| (from * w as f32) as i32 + ((noise(0, y, 6, seed ^ 0x51) - 0.5) * 6.0) as i32;
let mut mouth_rows: Vec<i32> = (0..mouths).map(|k| ((k as f32 + 0.5) / mouths as f32 * (h - 16) as f32) as i32 + 8 + ((noise(k as i32, 3, 2, seed) - 0.5) * 8.0) as i32).collect();
mouth_rows.sort_unstable();
// winding paths across the land (stay open, no trees)
let paths: Vec<f32> = (0..2).map(|k| 0.25 + 0.5 * k as f32 + (noise(k, 9, 2, seed) - 0.5) * 0.2).collect();
for y in 0..h {
let ex = edge(y);
for x in 0..w {
if x < ex - 1 {
continue;
}
let i = idx(x, y);
let t = self.map.t[i];
let keep = matches!(t.kind, Kind::Core | Kind::Portal | Kind::Gate | Kind::Tower | Kind::Shard | Kind::ManaSpring | Kind::RuneChest | Kind::Water | Kind::Lava | Kind::Bridge | Kind::LavaBridge | Kind::Door)
|| t.owner != Owner::None
|| x < 2
|| y < 2
|| x >= w - 2
|| y >= h - 2;
// the cliff: two tiles of rock along the edge, open where a cave mouth is
if x <= ex {
let mouth = mouth_rows.iter().any(|&m| (y - m).abs() <= 1);
if !keep {
let tl = &mut self.map.t[i];
if mouth {
tl.kind = Kind::Floor;
} else {
tl.kind = Kind::Rock;
}
tl.owner = Owner::None;
tl.gold = 0;
tl.marked = false;
tl.reinforced = false;
}
continue;
}
self.map.set_sky(i, true);
// no lava under the open sky: rivers up here carry water
let tl = &mut self.map.t[i];
match tl.kind {
Kind::Lava | Kind::Obsidian => tl.kind = Kind::Water,
Kind::LavaBridge => tl.kind = Kind::Bridge,
Kind::Drawbridge => tl.res &= !DB_LAVA,
_ => {}
}
if keep {
continue;
}
let on_path = paths.iter().any(|&p| {
let py = p * h as f32 + (noise(x, 0, 9, seed ^ 0x77) - 0.5) * 14.0;
(y as f32 - py).abs() < 1.2
});
let wood = noise(x, y, 5, seed ^ 0xf0);
let rocks = noise(x, y, 4, seed ^ 0x0b);
let tl = &mut self.map.t[i];
tl.owner = Owner::None;
tl.gold = 0;
tl.marked = false;
tl.reinforced = false;
tl.hp = 0.0;
if !on_path && rocks > 0.86 {
tl.kind = Kind::Rock;
} else if !on_path && wood > 0.52 && (x * 7 + y * 13) % 3 != 0 {
tl.kind = Kind::Tree;
tl.hp = crate::wood::TREE_HP;
} else {
tl.kind = Kind::Floor;
}
}
}
// a clearing around the knights' fortresses (and anything else they hold) above ground
let held: Vec<usize> = (0..self.map.t.len()).filter(|&i| self.map.sky(i) && self.map.t[i].owner == Owner::Hero).collect();
let mut cleared = vec![false; self.map.t.len()];
for &i in &held {
let (x, y) = xy(i);
for yy in y - 3..=y + 3 {
for xx in x - 3..=x + 3 {
if !inb(xx, yy) || xx < 2 || yy < 2 || xx >= w - 2 || yy >= h - 2 {
continue;
}
let j = idx(xx, yy);
if cleared[j] || !self.map.sky(j) {
continue;
}
cleared[j] = true;
let t = &mut self.map.t[j];
if t.owner == Owner::None && matches!(t.kind, Kind::Tree | Kind::Rock) {
t.kind = Kind::Floor;
t.hp = 0.0;
}
}
}
}
// the cave mouths run through into the dungeon: dig until open ground is reached
for &m in &mouth_rows {
let ex = edge(m);
let mut x = ex - 2;
while x > 2 {
let i = idx(x, m);
if self.map.t[i].walkable() {
break;
}
if self.map.t[i].solid() && !matches!(self.map.t[i].kind, Kind::Steel) {
let tl = &mut self.map.t[i];
tl.kind = Kind::Floor;
tl.owner = Owner::None;
tl.gold = 0;
}
x -= 1;
}
}
self.recount();
// torches on stands light the clearing at night (and look fine by day)
self.place_all_torches();
}
// the share of the map above ground (0: a dungeon map)
#[allow(dead_code)] // tests and debugging
pub fn surface_share(&self) -> f32 {
if self.map.sky.is_empty() {
return 0.0;
}
self.map.sky.iter().filter(|&&s| s).count() as f32 / self.map.sky.len() as f32
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn half_the_map_becomes_a_forest_above_ground() {
let mut g = Game::new(21, 128, 96);
let core = g.map.core;
g.add_surface(21, 0.5, 3);
let share = g.surface_share();
assert!(share > 0.35 && share < 0.6, "share {}", share);
let trees = g.map.t.iter().filter(|t| t.kind == Kind::Tree).count();
assert!(trees > 200, "{} trees", trees);
assert_eq!(g.map.core, core);
assert_eq!(g.map.t[idx(core.0 + 1, core.1 + 1)].kind, Kind::Core);
// the surface can be reached from the core (cave mouths, trees can be felled)
let goal = |i: usize| g.map.sky(i) && g.map.t[i].walkable();
assert!(g.map.search(idx(core.0 + 1, core.1 + 3), g.map.hero_cost_as(false), goal, true).is_some());
}
#[test]
fn fortresses_above_ground_stand_in_a_clearing() {
let mut g = Game::new(22, 128, 96);
let spec = crate::fortress::FortSpec { x: 90, y: 40, w: 15, h: 13, doors: vec![3], towers: vec![TowerKind::Arrow], tower_level: 1, guards: vec![(EK::Knight, 1)], patrols: Vec::new(), vaults: 1, vault_value: 500, shard: false, heart: false };
g.build_fortress(&spec);
g.add_surface(22, 0.5, 3);
let wall = g.map.t.iter().enumerate().filter(|(i, t)| g.map.sky(*i) && t.owner == Owner::Hero && t.kind == Kind::Earth).count();
assert!(wall > 20, "the fortress walls stand");
// no tree right next to the walls
for y in 37..56 {
for x in 87..108 {
let near = (x >= 89 && x <= 105 && y >= 39 && y <= 53) as bool;
if near && g.map.t[idx(x, y)].owner == Owner::None {
assert_ne!(g.map.t[idx(x, y)].kind, Kind::Tree, "tree at {},{}", x, y);
}
}
}
}
#[test]
fn the_finale_is_fought_above_ground() {
let g = crate::campaign::start_mission(29, None);
assert!(g.surface_share() > 0.3, "share {}", g.surface_share());
assert_eq!(g.rivals.len(), 2);
assert!(g.gate_hp > 0.0 && g.has_gate());
let (gx, gy) = g.map.gate;
assert!(g.map.sky(idx(gx + 1, gy + 1)), "the gate of the order stands in the forest");
// the heroes find their way from the gate to the core
assert!(g.map.hero_path_to_core(idx(gx + 1, gy + 1)).is_some());
}
#[test]
fn no_lava_under_the_open_sky() {
let mut g = Game::new(31, 128, 96);
// a lava river across the whole map
for x in 2..126 {
for y in 40..44 {
g.map.t[idx(x, y)].kind = Kind::Lava;
}
}
g.add_surface(31, 0.5, 3);
for i in 0..g.map.t.len() {
if g.map.sky(i) {
assert!(!matches!(g.map.t[i].kind, Kind::Lava | Kind::LavaBridge), "lava above ground");
}
}
assert!(g.map.t.iter().any(|t| t.kind == Kind::Lava), "the lava below stays");
}
}