225 lines
9.7 KiB
Rust
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");
|
|
}
|
|
}
|