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
@@ -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
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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