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

+6
View File
@@ -48,6 +48,7 @@ pub struct MissionDef {
pub rivals: Vec<(f32, f32, u8, u8)>, // rival masters: position (fraction of the map), difficulty, side (2 Mossgrim, 3 Lady Nightwell)
pub islands: Vec<IslandDef>,
pub caves: Vec<CaveDef>,
pub surface: f32, // share of the width from where the map lies above ground (0: none)
}
// an island in a lake of water or lava, with treasure and/or a mana spring
@@ -166,6 +167,7 @@ fn base(key: &str, size: (usize, usize), seed: u32, goals: Vec<Goal>) -> Mission
rivals: Vec::new(),
islands: Vec::new(),
caves: Vec::new(),
surface: 0.0,
}
}
@@ -181,6 +183,7 @@ pub fn mission_def_from_doc(d: &crate::campaign_doc::MissionDoc) -> MissionDef {
out.gate = d.waves;
out.gate_defenders = d.waves;
out.time_limit = d.time_limit;
out.surface = if d.gen.surface { 0.5 } else { 0.0 };
out
}
@@ -537,6 +540,9 @@ fn start_mission_def(d: &MissionDef, i: usize, carry: Option<&Carry>, coop: bool
}
g.place_dwarf_caves(d.seed);
g.add_wilds(d.seed, false);
if d.surface > 0.0 {
g.add_surface(d.seed, d.surface, 2 + (h / 64) as u32);
}
g.init_research(Some(i));
g.init_fog();
let heart = d.gate || d.forts.iter().any(|f| f.heart);
+3 -1
View File
@@ -145,11 +145,13 @@ pub struct GenDoc {
pub rivals: u8, // 0..2 rival masters
#[serde(default = "default_difficulty")]
pub difficulty: u8, // 0 easy, 1 medium, 2 hard
#[serde(default)]
pub surface: bool, // the eastern half lies above ground (forest)
}
impl Default for GenDoc {
fn default() -> Self {
GenDoc { rivers: 1, lava: 0, forts: 1, rivals: 0, difficulty: 1 }
GenDoc { rivers: 1, lava: 0, forts: 1, rivals: 0, difficulty: 1, surface: false }
}
}
+1 -1
View File
@@ -91,7 +91,7 @@ pub fn coop_campaign() -> CampaignDoc {
waves: sp.waves.0,
wave_limit: sp.waves.1,
first_wave: if sp.waves.0 { sp.waves.2 } else { 300.0 },
gen: GenDoc { rivers: sp.gen.0, lava: sp.gen.1, forts: sp.gen.2, rivals: 0, difficulty: sp.gen.3 },
gen: GenDoc { rivers: sp.gen.0, lava: sp.gen.1, forts: sp.gen.2, rivals: 0, difficulty: sp.gen.3, surface: false },
next: (i + 1 < specs.len()).then(|| format!("c{:02}", i + 2)),
pos: (40.0 + (i % 5) as f32 * 220.0, 40.0 + (i / 5) as f32 * 140.0),
..MissionDoc::default()
+8 -3
View File
@@ -35,7 +35,7 @@ enum Cat {
const CATS: [Cat; 9] = [Cat::Terrain, Cat::Rooms, Cat::Places, Cat::Units, Cat::Towers, Cat::Traps, Cat::Forts, Cat::Zones, Cat::Erase];
const TERRAIN: [&str; 14] = ["floor", "earth", "reinforced", "rock", "water", "lava", "bridge", "lavabridge", "gold", "silver", "ruby", "diamond", "steel", "mushroom"];
const TERRAIN: [&str; 16] = ["floor", "earth", "reinforced", "rock", "water", "lava", "bridge", "lavabridge", "gold", "silver", "ruby", "diamond", "steel", "mushroom", "grass", "tree"];
const PLACES: [&str; 7] = ["core", "portal", "gate", "spring", "shard", "chest", "chained"];
const OWNERS: [Owner; 6] = [Owner::Player, Owner::Hero, Owner::Rival1, Owner::Rival2, Owner::Rival3, Owner::None];
const ZONE_SHAPES: [&str; 4] = ["rect", "circle", "tiles", "line"];
@@ -76,6 +76,8 @@ fn kind_of_terrain(t: &str) -> (Kind, bool) {
"bridge" => (Kind::Bridge, false),
"lavabridge" => (Kind::LavaBridge, false),
"mushroom" => (Kind::Mushroom, false),
"grass" => (Kind::Floor, false),
"tree" => (Kind::Tree, false),
"gold" => (Kind::Gold, false),
"silver" => (Kind::Silver, false),
"ruby" => (Kind::Ruby, false),
@@ -209,7 +211,10 @@ impl MapEdit {
}
match self.cat {
Cat::Terrain => {
let (k, reinforced) = kind_of_terrain(TERRAIN[self.item.min(TERRAIN.len() - 1)]);
let name = TERRAIN[self.item.min(TERRAIN.len() - 1)];
let (k, reinforced) = kind_of_terrain(name);
// grass and trees lie above ground; anything else is painted underground
self.game.map.set_sky(i, matches!(name, "grass" | "tree"));
self.game.torches.retain(|tr| tr.wall != i);
let owner = if k == Kind::Floor || reinforced || matches!(k, Kind::Bridge | Kind::LavaBridge) { OWNERS[self.owner] } else { Owner::None };
let tl = &mut self.game.map.t[i];
@@ -218,7 +223,7 @@ impl MapEdit {
tl.owner = if reinforced && owner == Owner::None { Owner::Player } else { owner };
tl.marked = false;
if tl.plant() {
tl.hp = crate::wood::MUSHROOM_HP;
tl.hp = if k == Kind::Tree { crate::wood::TREE_HP } else { crate::wood::MUSHROOM_HP };
}
tl.gold = match k {
Kind::Gold => 1200,
+2
View File
@@ -777,6 +777,7 @@ impl Editor {
(tr("ed.rivals"), m.gen.rivals.to_string(), 6),
(tr("ed.difficulty"), tr(["diff.0", "diff.1", "diff.2"][m.gen.difficulty.min(2) as usize]), 7),
(tr("ed.portals"), m.portals.to_string(), 8),
(tr("ed.surface"), tr(if m.gen.surface { "val.on" } else { "val.off" }), 9),
];
if fixed {
rows.truncate(0);
@@ -794,6 +795,7 @@ impl Editor {
5 => m.gen.forts = (m.gen.forts as i32 + d.signum()).clamp(0, 3) as u8,
6 => m.gen.rivals = (m.gen.rivals as i32 + d.signum()).clamp(0, 2) as u8,
7 => m.gen.difficulty = (m.gen.difficulty as i32 + d.signum()).clamp(0, 2) as u8,
9 => m.gen.surface = !m.gen.surface,
_ => m.portals = (m.portals as i32 + d.signum()).clamp(0, 6) as usize,
}
}
+2 -2
View File
@@ -93,7 +93,7 @@ pub fn demo_campaign() -> CampaignDoc {
seed: 4242,
gold: 4000.0,
goals: vec!["Towers".into()],
gen: GenDoc { rivers: 1, lava: 0, forts: 1, rivals: 0, difficulty: 0 },
gen: GenDoc { rivers: 1, lava: 0, forts: 1, rivals: 0, difficulty: 0, surface: false },
next: Some("demo2".into()),
branches: vec![BranchDoc { id: "rich".into(), goal: "Loot(2000)".into(), title: tr("demo.branch"), next: "demo2".into(), priority: Some(1) }],
pos: (40.0, 60.0),
@@ -226,7 +226,7 @@ pub fn demo_campaign() -> CampaignDoc {
seed: 77,
gold: 3000.0,
goals: vec!["KillAll".into()],
gen: GenDoc { rivers: 1, lava: 1, forts: 1, rivals: 0, difficulty: 0 },
gen: GenDoc { rivers: 1, lava: 1, forts: 1, rivals: 0, difficulty: 0, surface: false },
pos: (300.0, 60.0),
..MissionDoc::default()
};
+1 -1
View File
@@ -788,7 +788,7 @@ mod tests {
use crate::blocks::{Block, Val};
use crate::campaign_doc::*;
let mut doc = CampaignDoc { name: "Coop test".into(), ..CampaignDoc::default() };
let mut m = MissionDoc { key: "c1".into(), title: "Random".into(), size: (80, 80), seed: 5, goals: vec!["KillAll".into(), "Rivals".into()], waves: true, first_wave: 3.0, gen: GenDoc { rivers: 1, lava: 1, forts: 1, rivals: 1, difficulty: 1 }, ..MissionDoc::default() };
let mut m = MissionDoc { key: "c1".into(), title: "Random".into(), size: (80, 80), seed: 5, goals: vec!["KillAll".into(), "Rivals".into()], waves: true, first_wave: 3.0, gen: GenDoc { rivers: 1, lava: 1, forts: 1, rivals: 1, difficulty: 1, surface: false }, ..MissionDoc::default() };
m.scripts.start = vec![Block::Gold { amount: Val::num(250.0) }];
m.scripts.triggers = vec![TriggerDoc { name: "wave".into(), kind: TriggerKind::Timer { secs: 2.0, every: true }, once: false, enabled: true, script: vec![Block::Wave { count: Val::num(1.0) }] }];
doc.missions.push(m.clone());
+7 -1
View File
@@ -37,6 +37,7 @@ mod sfx;
mod tracks;
mod traps;
mod sound;
mod surface;
mod spells;
mod touch;
mod ui;
@@ -173,7 +174,12 @@ fn finish_rect(app: &mut App, tool: Tool, start: (i32, i32), mx: f32, my: f32) {
}
fn new_game(seed: u32, s: &Settings) -> (Game, Cam) {
let g = Game::new_free(seed, s.map_w, s.map_h, s.difficulty);
let mut g = Game::new_free(seed, s.map_w, s.map_h, s.difficulty);
if s.surface {
// the eastern half above ground: forest, paths, cave mouths
g.add_surface(seed, 0.5, 2 + (s.map_h / 64) as u32);
g.init_fog();
}
let (cx, cy) = g.map.core;
let cam = Cam::new(cx as f32 + 1.5, cy as f32 + 1.5);
(g, cam)
+18 -1
View File
@@ -350,6 +350,8 @@ pub struct Map {
pub explored: Vec<bool>, // fog of war: tiles the dungeon knows (empty: no fog)
#[serde(skip)]
pub visible: Vec<bool>, // fog of war: tiles in sight right now
#[serde(default, with = "crate::fog::bits")]
pub sky: Vec<bool>, // tiles above ground (daylight, grass); empty: all underground
}
fn default_size() -> usize {
@@ -367,6 +369,21 @@ impl Map {
}
}
// tile i lies above ground
pub fn sky(&self, i: usize) -> bool {
self.sky.get(i).copied().unwrap_or(false)
}
pub fn set_sky(&mut self, i: usize, on: bool) {
if self.sky.len() != self.t.len() {
if !on {
return;
}
self.sky = vec![false; self.t.len()];
}
self.sky[i] = on;
}
pub fn get(&self, x: i32, y: i32) -> Option<&Tile> {
if inb(x, y) {
Some(&self.t[idx(x, y)])
@@ -587,7 +604,7 @@ impl Map {
tl.hp = 6.0;
}
}
Map { w, h, t, core, portals, legacy_portal: None, gate, treasures, explored: Vec::new(), visible: Vec::new() }
Map { w, h, t, core, portals, legacy_portal: None, gate, treasures, explored: Vec::new(), visible: Vec::new(), sky: Vec::new() }
}
// Carves a meandering river; the player's area, the core and portals stay dry.
+15 -7
View File
@@ -230,6 +230,7 @@ enum Btn {
Tab(usize),
Change(usize, i32),
MapW(i32),
Land,
MapH(i32),
Difficulty(i32),
Start,
@@ -675,6 +676,10 @@ impl Menu {
settings.difficulty = (settings.difficulty as i32 + d).rem_euclid(3) as u8;
MenuAction::SettingsChanged
}
Some(Btn::Land) => {
settings.surface = !settings.surface;
MenuAction::SettingsChanged
}
Some(Btn::MapH(d)) => {
settings.map_h = (settings.map_h as i32 + d * 32).clamp(MIN_SIZE as i32, MAX_SIZE as i32) as usize;
MenuAction::SettingsChanged
@@ -1153,27 +1158,30 @@ impl Menu {
let sh = sh();
let (mx, my) = mouse();
let cols = 48.min((sw / CW()) as i32);
let rows = 22;
let rows = 25;
let x = grid_center(sw, cols as f32 * CW()).max(0.0);
let y = (grid_center_y(sh, rows as f32 * CH())).max(0.0);
gfx.window_titled(x, y, cols, rows, S(), true, FRAME_HI, Color::new(0.05, 0.03, 0.04, 0.97), &tr("menu.new"), FRAME_HI);
let diff = tr(&format!("diff.{}", s.difficulty.min(2)));
let lines = [("newgame.width", s.map_w.to_string(), 0), ("newgame.height", s.map_h.to_string(), 1), ("newgame.difficulty", diff, 2)];
let land = tr(if s.surface { "newgame.land1" } else { "newgame.land0" });
let lines = [("newgame.width", s.map_w.to_string(), 0), ("newgame.height", s.map_h.to_string(), 1), ("newgame.difficulty", diff, 2), ("newgame.land", land, 3)];
for (k, (key, val, which)) in lines.iter().enumerate() {
let (key, which) = (*key, *which);
let by = y + CH() * (2.0 + k as f32 * 2.5);
gfx.text(&tr(key), x + CW() * 3.0, by + CH() * 0.5, S(), TXT);
let vw = 14.0 * CW();
let vw = 18.0 * CW();
let vx = x + cols as f32 * CW() - vw - CW() * 3.0;
let left = Rect::new(vx, by, CW() * 2.5, CH() * 2.0);
let right = Rect::new(vx + vw - CW() * 2.5, by, CW() * 2.5, CH() * 2.0);
button(gfx, left, "<", left.contains(vec2(mx, my)), FRAME);
button(gfx, right, ">", right.contains(vec2(mx, my)), FRAME);
let v = val.clone();
gfx.text(&v, vx + (vw - gfx.measure(&v, S())) * 0.5, by + CH() * 0.5, S(), GOLD);
let vs = if gfx.measure(&v, S()) > vw - CW() * 5.5 { SMALL() } else { S() };
gfx.text(&v, vx + (vw - gfx.measure(&v, vs)) * 0.5, by + CH() * 0.5, vs, GOLD);
let (l, r) = match which {
0 => (Btn::MapW(-1), Btn::MapW(1)),
1 => (Btn::MapH(-1), Btn::MapH(1)),
3 => (Btn::Land, Btn::Land),
_ => (Btn::Difficulty(-1), Btn::Difficulty(1)),
};
self.buttons.push((left, l));
@@ -1183,14 +1191,14 @@ impl Menu {
let area = (s.map_w * s.map_h) as f32 / 4096.0;
let portals = (area.round() as usize).max(1);
let info = trf("newgame.info", &[portals.to_string(), (portals - 1).to_string()]);
gfx.text(&info, x + CW() * 3.0, y + CH() * 10.0, S(), DIM);
gfx.text(&info, x + CW() * 3.0, y + CH() * 12.5, S(), DIM);
let opp = tr(if s.map_w * s.map_h > 128 * 128 { "newgame.opponents2" } else { "newgame.opponents1" });
let maxc = (cols - 6) as usize;
for (k, l) in wrap(&opp, maxc).iter().take(2).enumerate() {
gfx.text(l, x + CW() * 3.0, y + CH() * (11.0 + k as f32), S(), DIM);
gfx.text(l, x + CW() * 3.0, y + CH() * (13.5 + k as f32), S(), DIM);
}
let w = (cols - 6) as f32 * CW();
self.column(gfx, x + CW() * 3.0, y + CH() * 14.0, w, &[(Btn::Start, tr("newgame.start")), (Btn::Back, tr("menu.back"))]);
self.column(gfx, x + CW() * 3.0, y + CH() * 16.5, w, &[(Btn::Start, tr("newgame.start")), (Btn::Back, tr("menu.back"))]);
}
fn draw_pause(&mut self, gfx: &Gfx) {
+19 -7
View File
@@ -449,6 +449,10 @@ fn ambient_field(map: &Map) -> Vec<f32> {
(false, Owner::Hero) => 0.6,
_ => 0.0,
};
// daylight above ground (cliffs and trees too)
if map.sky(i) {
f[i] = 1.6;
}
if t.river() == Some(Kind::Lava) {
f[i] = f[i].max(0.8); // lava lights up its surroundings
}
@@ -672,7 +676,11 @@ fn build_chunk(map: &Map, amb: &[f32], glow: &[f32], cx: i32, cz: i32, atlas: &T
for z in cz * CHUNK..((cz + 1) * CHUNK).min(mh() as i32) {
for x in cx * CHUNK..((cx + 1) * CHUNK).min(mw() as i32) {
let t = map.t[idx(x, z)];
let tex = Gfx::tile_tex(t.kind, t.owner, t.var, t.reinforced);
let mut tex = Gfx::tile_tex(t.kind, t.owner, t.var, t.reinforced);
// open land above ground is grass (until a master claims it)
if map.sky(idx(x, z)) && t.kind == Kind::Floor && t.owner == Owner::None {
tex = T_GRASS + (t.var as u32 & 3);
}
let (ua, ub) = cell_uv(tex);
let (xf, zf) = (x as f32, z as f32);
if !map.shown(idx(x, z)) {
@@ -836,11 +844,13 @@ fn build_chunk(map: &Map, amb: &[f32], glow: &[f32], cx: i32, cz: i32, atlas: &T
c,
[Vec3::Y; 4],
);
// outlines: north and west edge, only between two open tiles (each edge once)
if !solid_at(map, x, z - 1) {
// outlines: north and west edge, only between two open tiles (each edge once);
// open land above ground has none
let outdoors = map.sky(idx(x, z)) && t.owner == Owner::None;
if !solid_at(map, x, z - 1) && !outdoors {
ln.strip(vec3(xf, 0.0, zf), vec3(xf + 1.0, 0.0, zf), Vec3::Y, OUTLINE_W, OUTLINE_C);
}
if !solid_at(map, x - 1, z) {
if !solid_at(map, x - 1, z) && !outdoors {
ln.strip(vec3(xf, 0.0, zf), vec3(xf, 0.0, zf + 1.0), Vec3::Y, OUTLINE_W, OUTLINE_C);
}
// room decor
@@ -871,7 +881,7 @@ fn build_chunk(map: &Map, amb: &[f32], glow: &[f32], cx: i32, cz: i32, atlas: &T
}
}
// wear and tear: grit and cracks on plain floors
if matches!(t.kind, Kind::Floor) {
if matches!(t.kind, Kind::Floor) && !outdoors {
let hsh = (x as u32).wrapping_mul(2654435761) ^ (z as u32).wrapping_mul(2246822519);
let hsh = (hsh ^ (hsh >> 15)).wrapping_mul(2246822519);
let decal = match hsh % 11 {
@@ -1083,7 +1093,7 @@ fn chunk_sig(map: &Map, cx: i32, cz: i32) -> u64 {
Kind::Drawbridge => t.res as u64,
_ => 0,
};
let shown = map.shown(idx(x, z)) as u64;
let shown = map.shown(idx(x, z)) as u64 | (map.sky(idx(x, z)) as u64) << 1;
(t.kind as u64) | (t.owner as u64) << 5 | (t.marked as u64) << 8 | (t.reinforced as u64) << 9 | shown << 10 | extra << 11
}
None => 0xff,
@@ -1462,7 +1472,9 @@ impl R3d {
let amb = |x: f32, z: f32| {
let (ix, iz) = (x as i32, z as i32);
let own = g.map.get(ix, iz).map_or(false, |t| t.owner == Owner::Player);
if own {
if inb(ix, iz) && g.map.sky(idx(ix, iz)) {
0.95
} else if own {
0.75
} else {
0.5
+9
View File
@@ -200,4 +200,13 @@ fn export_scenes() {
g.plan_walls(cx + 5, cy - 1, cx + 5, cy + 2);
run(&mut g, 2.0);
save("wood", &mut g, mx as f32 + 0.5, my as f32 + 0.5, 14.0, 0.0, false);
// above ground: the cliff with a cave mouth, grass, a forest
let mut g = Game::new(23, 128, 96);
g.add_surface(23, 0.45, 3);
g.init_fog();
let mid = (mh() / 2) as i32;
let ex = (0..mw() as i32).find(|&x| g.map.sky(idx(x, mid))).unwrap_or(60);
run(&mut g, 1.0);
save("surface", &mut g, ex as f32 + 6.0, mid as f32, 24.0, 0.0, false);
}
+2
View File
@@ -162,6 +162,7 @@ pub struct Settings {
pub difficulty: u8, // rival masters in free games: 0 easy, 1 medium, 2 hard
pub map_w: usize, // size of new maps (64..256)
pub map_h: usize,
pub surface: bool, // new free games: the eastern half lies above ground (forest)
// key bindings: action id -> key name
pub keys: BTreeMap<String, String>,
}
@@ -196,6 +197,7 @@ impl Default for Settings {
difficulty: 1,
map_w: 64,
map_h: 64,
surface: false,
keys: BTreeMap::new(),
}
}
+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());
}
}