Surface world: grass in daylight, forests, cliffs with cave mouths; free-game and editor option
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ef10b5c432
commit
8a35a903de
32 files changed
+343
-24
No files matched your search
+142
@@ -0,0 +1,142 @@
|
||||
// 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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user