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:
Richard ZinkandClaude Opus 5.5 committed 2026-11-05 22:35:46 +01:00
1 parent ef10b5c432
commit 8a35a903de
32 files changed
+343 -24

No files matched your search

+142
View File
@@ -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());
}
}