From 944a37b848d17ebc8b526b34434a4875e4a37cac Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 6 Feb 2020 22:32:26 +0000 Subject: [PATCH] Initial settlement generation work --- common/src/astar.rs | 9 + common/src/lib.rs | 1 + common/src/path.rs | 2 + common/src/spiral.rs | 37 ++ voxygen/src/scene/terrain.rs | 35 +- world/examples/settlement_viewer.rs | 55 +++ world/src/generator/mod.rs | 1 + world/src/generator/settlement/mod.rs | 604 ++++++++++++++++++++++++++ world/src/sim/mod.rs | 4 +- world/src/util/structure.rs | 12 +- 10 files changed, 720 insertions(+), 40 deletions(-) create mode 100644 common/src/spiral.rs create mode 100644 world/examples/settlement_viewer.rs create mode 100644 world/src/generator/settlement/mod.rs diff --git a/common/src/astar.rs b/common/src/astar.rs index 4c153c9de9..8bbb5bc7b4 100644 --- a/common/src/astar.rs +++ b/common/src/astar.rs @@ -34,6 +34,15 @@ pub enum PathResult { Pending, } +impl PathResult { + pub fn into_path(self) -> Option> { + match self { + PathResult::Path(path) => Some(path), + _ => None, + } + } +} + #[derive(Clone, Debug)] pub struct Astar { iter: usize, diff --git a/common/src/lib.rs b/common/src/lib.rs index df969e5af4..f8da49935d 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -25,6 +25,7 @@ pub mod npc; pub mod path; pub mod ray; pub mod region; +pub mod spiral; pub mod state; pub mod states; pub mod sync; diff --git a/common/src/path.rs b/common/src/path.rs index 85e8f174af..fdbc9164f0 100644 --- a/common/src/path.rs +++ b/common/src/path.rs @@ -33,6 +33,8 @@ impl FromIterator for Path { impl Path { pub fn len(&self) -> usize { self.nodes.len() } + pub fn iter(&self) -> impl Iterator { self.nodes.iter() } + pub fn start(&self) -> Option<&T> { self.nodes.first() } pub fn end(&self) -> Option<&T> { self.nodes.last() } diff --git a/common/src/spiral.rs b/common/src/spiral.rs new file mode 100644 index 0000000000..f0044763ca --- /dev/null +++ b/common/src/spiral.rs @@ -0,0 +1,37 @@ +use vek::*; + +/// An iterator of coordinates that create a rectangular spiral out from the +/// origin +#[derive(Clone)] +pub struct Spiral2d { + layer: i32, + i: i32, +} + +impl Spiral2d { + pub fn new() -> Self { Self { layer: 0, i: 0 } } +} + +impl Iterator for Spiral2d { + type Item = Vec2; + + fn next(&mut self) -> Option { + let layer_size = (self.layer * 8 + 4 * self.layer.min(1) - 4).max(1); + if self.i >= layer_size { + self.layer += 1; + self.i = 0; + } + let layer_size = (self.layer * 8 + 4 * self.layer.min(1) - 4).max(1); + + let pos = Vec2::new( + -self.layer + (self.i - (layer_size / 4) * 0).max(0).min(self.layer * 2) + - (self.i - (layer_size / 4) * 2).max(0).min(self.layer * 2), + -self.layer + (self.i - (layer_size / 4) * 1).max(0).min(self.layer * 2) + - (self.i - (layer_size / 4) * 3).max(0).min(self.layer * 2), + ); + + self.i += 1; + + Some(pos) + } +} diff --git a/voxygen/src/scene/terrain.rs b/voxygen/src/scene/terrain.rs index be5f7c40c3..a4c4d25ce6 100644 --- a/voxygen/src/scene/terrain.rs +++ b/voxygen/src/scene/terrain.rs @@ -10,6 +10,7 @@ use super::SceneData; use common::{ assets, figure::Segment, + spiral::Spiral2D, terrain::{Block, BlockKind, TerrainChunk}, vol::{BaseVol, ReadVol, RectRasterableVol, SampleVol, Vox}, volumes::vol_grid_2d::{VolGrid2d, VolGrid2dError}, @@ -1487,37 +1488,3 @@ impl Terrain { }); } } - -#[derive(Clone)] -struct Spiral2d { - layer: i32, - i: i32, -} - -impl Spiral2d { - pub fn new() -> Self { Self { layer: 0, i: 0 } } -} - -impl Iterator for Spiral2d { - type Item = Vec2; - - fn next(&mut self) -> Option { - let layer_size = (self.layer * 8 + 4 * self.layer.min(1) - 4).max(1); - if self.i >= layer_size { - self.layer += 1; - self.i = 0; - } - let layer_size = (self.layer * 8 + 4 * self.layer.min(1) - 4).max(1); - - let pos = Vec2::new( - -self.layer + (self.i - (layer_size / 4) * 0).max(0).min(self.layer * 2) - - (self.i - (layer_size / 4) * 2).max(0).min(self.layer * 2), - -self.layer + (self.i - (layer_size / 4) * 1).max(0).min(self.layer * 2) - - (self.i - (layer_size / 4) * 3).max(0).min(self.layer * 2), - ); - - self.i += 1; - - Some(pos) - } -} diff --git a/world/examples/settlement_viewer.rs b/world/examples/settlement_viewer.rs new file mode 100644 index 0000000000..903dba861f --- /dev/null +++ b/world/examples/settlement_viewer.rs @@ -0,0 +1,55 @@ +use rand::thread_rng; +use vek::*; +use veloren_world::generator::settlement::Settlement; + +const W: usize = 640; +const H: usize = 480; + +fn main() { + let mut win = + minifb::Window::new("Settlement Viewer", W, H, minifb::WindowOptions::default()).unwrap(); + + let settlement = Settlement::generate(&mut thread_rng()); + + let mut focus = Vec2::::zero(); + let mut zoom = 1.0; + + while win.is_open() { + let mut buf = vec![0; W * H]; + + let win_to_pos = + |wp: Vec2| (wp.map(|e| e as f32) - Vec2::new(W as f32, H as f32) * 0.5) * zoom; + + for i in 0..W { + for j in 0..H { + let pos = focus + win_to_pos(Vec2::new(i, j)) * zoom; + + let color = settlement.get_color(pos); + + buf[j * W + i] = u32::from_le_bytes([color.b, color.g, color.r, 255]); + } + } + + let spd = 20.0; + if win.is_key_down(minifb::Key::W) { + focus.y -= spd * zoom; + } + if win.is_key_down(minifb::Key::A) { + focus.x -= spd * zoom; + } + if win.is_key_down(minifb::Key::S) { + focus.y += spd * zoom; + } + if win.is_key_down(minifb::Key::D) { + focus.x += spd * zoom; + } + if win.is_key_down(minifb::Key::Q) { + zoom *= 1.05; + } + if win.is_key_down(minifb::Key::E) { + zoom /= 1.05; + } + + win.update_with_buffer(&buf).unwrap(); + } +} diff --git a/world/src/generator/mod.rs b/world/src/generator/mod.rs index 46c9d41640..6ac7240b8d 100644 --- a/world/src/generator/mod.rs +++ b/world/src/generator/mod.rs @@ -1,3 +1,4 @@ +pub mod settlement; mod town; // Reexports diff --git a/world/src/generator/settlement/mod.rs b/world/src/generator/settlement/mod.rs new file mode 100644 index 0000000000..bbd5a0c272 --- /dev/null +++ b/world/src/generator/settlement/mod.rs @@ -0,0 +1,604 @@ +use crate::util::{Sampler, StructureGen2d}; +use common::{astar::Astar, path::Path, spiral::Spiral2d}; +use hashbrown::{HashMap, HashSet}; +use rand::prelude::*; +use std::{collections::VecDeque, f32, marker::PhantomData}; +use vek::*; + +pub fn gradient(line: [Vec2; 2]) -> f32 { + let r = (line[0].y - line[1].y) / (line[0].x - line[1].x); + if r.is_nan() { 100000.0 } else { r } +} + +pub fn intersect(a: [Vec2; 2], b: [Vec2; 2]) -> Option> { + let ma = gradient(a); + let mb = gradient(b); + + let ca = a[0].y - ma * a[0].x; + let cb = b[0].y - mb * b[0].x; + + if (ma - mb).abs() < 0.0001 || (ca - cb).abs() < 0.0001 { + None + } else { + let x = (cb - ca) / (ma - mb); + let y = ma * x + ca; + + Some(Vec2::new(x, y)) + } +} + +pub fn dist_to_line(line: [Vec2; 2], p: Vec2) -> f32 { + let lsq = line[0].distance_squared(line[1]); + + if lsq == 0.0 { + line[0].distance(p) + } else { + let t = ((p - line[0]).dot(line[1] - line[0]) / lsq) + .max(0.0) + .min(1.0); + p.distance(line[0] + (line[1] - line[0]) * t) + } +} + +pub fn center_of(p: [Vec2; 3]) -> Vec2 { + let ma = -1.0 / gradient([p[0], p[1]]); + let mb = -1.0 / gradient([p[1], p[2]]); + + let pa = (p[0] + p[1]) * 0.5; + let pb = (p[1] + p[2]) * 0.5; + + let ca = pa.y - ma * pa.x; + let cb = pb.y - mb * pb.x; + + let x = (cb - ca) / (ma - mb); + let y = ma * x + ca; + + Vec2::new(x, y) +} + +const AREA_SIZE: u32 = 32; + +fn to_tile(e: i32) -> i32 { ((e as f32).div_euclid(AREA_SIZE as f32)).floor() as i32 } + +pub enum StructureKind { + House, +} + +pub struct Structure { + kind: StructureKind, + bounds: Aabr, +} + +pub struct Settlement { + land: Land, + farms: Store, + structures: Vec, + town: Option, +} + +pub struct Town { + base_tile: Vec2, +} + +pub struct Farm { + base_tile: Vec2, +} + +impl Settlement { + pub fn generate(rng: &mut impl Rng) -> Self { + let mut this = Self { + land: Land::new(rng), + farms: Store::default(), + structures: Vec::new(), + town: None, + }; + + this.place_river(rng); + + this.place_farms(rng); + this.place_town(rng); + this.place_paths(rng); + + this + } + + pub fn place_river(&mut self, rng: &mut impl Rng) { + let river_dir = Vec2::new(rng.gen::() - 0.5, rng.gen::() - 0.5).normalized(); + let radius = 500.0 + rng.gen::().powf(2.0) * 1000.0; + let river = self.land.new_plot(Plot::Water); + + for theta in (0..500).map(|x| (x as f32) * f32::consts::PI / 250.0) { + let pos = river_dir * radius + Vec2::new(theta.sin(), theta.cos()) * radius; + + if pos.magnitude() > 300.0 { + continue; + } + + for dir in CARDINALS.iter() { + self.land.set( + pos.map2(*dir, |e, d| e.floor() as i32 + d * 12) + .map(to_tile), + river, + ); + } + } + } + + pub fn place_paths(&mut self, rng: &mut impl Rng) { + let mut dir = Vec2::zero(); + for _ in 0..6 { + dir = (Vec2::new(rng.gen::() - 0.5, rng.gen::() - 0.5) * 2.0 - dir) + .try_normalized() + .unwrap_or(Vec2::zero()); + let origin = dir.map(|e| (e * 20.0) as i32); + let origin = self + .land + .find_tile_near(origin, |plot| match plot { + Some(&Plot::Field { .. }) => true, + _ => false, + }) + .unwrap(); + + if let Some(path) = self.town.as_ref().and_then(|town| { + self.land + .find_path(origin, town.base_tile, |from, to| match (from, to) { + (_, Some(b)) if self.land.plot(b.plot) == &Plot::Dirt => 0.0, + (_, Some(b)) if self.land.plot(b.plot) == &Plot::Water => 20.0, + (Some(a), Some(b)) if a.contains(WayKind::Wall) => { + if b.contains(WayKind::Wall) { + 1000.0 + } else { + 10.0 + } + }, + (Some(_), Some(_)) => 1.0, + _ => 1000.0, + }) + }) { + let path = path.iter().copied().collect::>(); + self.land.write_path(&path, WayKind::Path, |_| true, true); + } + } + } + + pub fn place_town(&mut self, rng: &mut impl Rng) { + let mut origin = Vec2::new(rng.gen_range(-2, 3), rng.gen_range(-2, 3)); + + let town = self.land.new_plot(Plot::Town); + for i in 0..4 { + if let Some(base_tile) = self.land.find_tile_near(origin, |plot| match plot { + Some(Plot::Field { .. }) => true, + Some(Plot::Dirt) => true, + _ => false, + }) { + self.land.set(base_tile, town); + + if i == 0 { + for dir in CARDINALS.iter() { + self.land.set(base_tile + *dir, town); + } + + self.town = Some(Town { base_tile }); + origin = base_tile; + } + } + } + + // Border wall + let spokes = CARDINALS + .iter() + .filter_map(|dir| { + self.land.find_tile_dir(origin, *dir, |plot| match plot { + Some(Plot::Town) => false, + _ => true, + }) + }) + .collect::>(); + let mut wall_path = Vec::new(); + for i in 0..spokes.len() { + self.land + .find_path(spokes[i], spokes[(i + 1) % spokes.len()], |_, to| match to + .map(|to| self.land.plot(to.plot)) + { + Some(Plot::Town) => 1000.0, + _ => 1.0, + }) + .map(|path| wall_path.extend(path.iter().copied())); + } + let grass = self.land.new_plot(Plot::Grass); + for pos in wall_path.iter() { + if self.land.tile_at(*pos).is_none() { + self.land.set(*pos, grass); + } + } + wall_path.push(wall_path[0]); + self.land.write_path( + &wall_path, + WayKind::Wall, + |plot| match plot { + Plot::Water => false, + _ => true, + }, + true, + ); + } + + pub fn place_farms(&mut self, rng: &mut impl Rng) { + for _ in 0..6 { + if let Some(base_tile) = self + .land + .find_tile_near(Vec2::zero(), |plot| plot.is_none()) + { + // Farm + let farmhouse = self.land.new_plot(Plot::Dirt); + self.land.set(base_tile, farmhouse); + + // Farmhouses + for _ in 0..rng.gen_range(1, 4) { + let house_pos = base_tile.map(|e| e * AREA_SIZE as i32 + AREA_SIZE as i32 / 2) + + Vec2::new(rng.gen_range(-16, 16), rng.gen_range(-16, 16)); + + self.structures.push(Structure { + kind: StructureKind::House, + bounds: Aabr { + min: house_pos - Vec2::new(rng.gen_range(4, 6), rng.gen_range(4, 6)), + max: house_pos + Vec2::new(rng.gen_range(4, 6), rng.gen_range(4, 6)), + }, + }); + } + + // Fields + let farmland = self.farms.insert(Farm { base_tile }); + for _ in 0..5 { + self.place_field(farmland, base_tile, rng); + } + } + } + } + + pub fn place_field( + &mut self, + farm: Id, + origin: Vec2, + rng: &mut impl Rng, + ) -> Option> { + let max_size = 7; + + if let Some(center) = self.land.find_tile_near(origin, |plot| plot.is_none()) { + let field = self.land.new_plot(Plot::Field { + farm, + seed: rng.gen(), + }); + let tiles = self + .land + .grow_from(center, rng.gen_range(1, max_size), rng, |plot| { + plot.is_none() + }); + for pos in tiles.into_iter() { + self.land.set(pos, field); + } + Some(field) + } else { + None + } + } + + pub fn get_color(&self, pos: Vec2) -> Rgb { + let pos = pos.map(|e| e.floor() as i32); + + if let Some(structure) = self + .structures + .iter() + .find(|s| s.bounds.contains_point(pos)) + { + return match structure.kind { + StructureKind::House => Rgb::new(200, 80, 50), + }; + } + + match self.land.get_at_block(pos) { + Sample::Wilderness => Rgb::zero(), + Sample::Way(WayKind::Path) => Rgb::new(130, 100, 0), + Sample::Way(WayKind::Hedge) => Rgb::new(0, 150, 0), + Sample::Way(WayKind::Wall) => Rgb::new(60, 60, 60), + Sample::Plot(Plot::Dirt) => Rgb::new(130, 100, 0), + Sample::Plot(Plot::Grass) => Rgb::new(100, 200, 0), + Sample::Plot(Plot::Water) => Rgb::new(100, 150, 250), + Sample::Plot(Plot::Town) => { + if pos.map(|e| e.rem_euclid(4) < 2).reduce(|x, y| x ^ y) { + Rgb::new(200, 130, 120) + } else { + Rgb::new(160, 150, 120) + } + }, + Sample::Plot(Plot::Field { seed, .. }) => { + let furrow_dirs = [ + Vec2::new(1, 0), + Vec2::new(0, 1), + Vec2::new(1, 1), + Vec2::new(-1, 1), + ]; + let furrow_dir = furrow_dirs[*seed as usize % furrow_dirs.len()]; + let furrow = (pos * furrow_dir).sum().rem_euclid(4) < 2; + Rgb::new( + if furrow { + 150 + } else { + 48 + seed.to_le_bytes()[0] % 64 + }, + 128 + seed.to_le_bytes()[1] % 128, + 16 + seed.to_le_bytes()[2] % 32, + ) + }, + } + } +} + +#[derive(Copy, Clone, PartialEq)] +pub enum Plot { + Dirt, + Grass, + Water, + Town, + Field { farm: Id, seed: u32 }, +} + +const CARDINALS: [Vec2; 4] = [ + Vec2::new(0, 1), + Vec2::new(1, 0), + Vec2::new(0, -1), + Vec2::new(-1, 0), +]; + +#[derive(Copy, Clone, PartialEq)] +pub enum WayKind { + Path, + Hedge, + Wall, +} + +pub struct Tile { + plot: Id, + ways: [Option; 4], +} + +impl Tile { + pub fn contains(&self, kind: WayKind) -> bool { self.ways.iter().any(|way| way == &Some(kind)) } +} + +pub enum Sample<'a> { + Wilderness, + Plot(&'a Plot), + Way(&'a WayKind), +} + +pub struct Land { + tiles: HashMap, Tile>, + plots: Store, + sampler_warp: StructureGen2d, +} + +impl Land { + pub fn new(rng: &mut impl Rng) -> Self { + Self { + tiles: HashMap::new(), + plots: Store::default(), + sampler_warp: StructureGen2d::new(rng.gen(), AREA_SIZE, 12), + } + } + + pub fn get_at_block(&self, pos: Vec2) -> Sample { + let neighbors = self.sampler_warp.get(pos); + let closest = neighbors + .iter() + .min_by_key(|(center, _)| center.distance_squared(pos)) + .unwrap() + .0; + + let map = [1, 5, 7, 3]; + for (i, dir) in CARDINALS.iter().enumerate() { + let line = [ + neighbors[4].0.map(|e| e as f32), + neighbors[map[i]].0.map(|e| e as f32), + ]; + if dist_to_line(line, pos.map(|e| e as f32)) < 1.5 { + if let Some(way) = self + .tile_at(neighbors[4].0.map(to_tile)) + .and_then(|tile| tile.ways[i].as_ref()) + { + return Sample::Way(way); + } + } + } + + let plot = self.plot_at(closest.map(to_tile)); + + plot.map(|plot| Sample::Plot(plot)) + .unwrap_or(Sample::Wilderness) + } + + pub fn tile_at(&self, pos: Vec2) -> Option<&Tile> { self.tiles.get(&pos) } + + pub fn tile_at_mut(&mut self, pos: Vec2) -> Option<&mut Tile> { self.tiles.get_mut(&pos) } + + pub fn plot(&self, id: Id) -> &Plot { self.plots.get(id) } + + pub fn plot_at(&self, pos: Vec2) -> Option<&Plot> { + self.tiles.get(&pos).map(|tile| self.plots.get(tile.plot)) + } + + pub fn plot_at_mut(&mut self, pos: Vec2) -> Option<&mut Plot> { + self.tiles + .get(&pos) + .map(|tile| tile.plot) + .map(move |plot| self.plots.get_mut(plot)) + } + + pub fn set(&mut self, pos: Vec2, plot: Id) { + self.tiles.insert(pos, Tile { + plot, + ways: [None; 4], + }); + } + + fn find_tile_near( + &self, + origin: Vec2, + mut match_fn: impl FnMut(Option<&Plot>) -> bool, + ) -> Option> { + Spiral2d::new() + .map(|pos| origin + pos) + .find(|pos| match_fn(self.plot_at(*pos))) + } + + fn find_tile_dir( + &self, + origin: Vec2, + dir: Vec2, + mut match_fn: impl FnMut(Option<&Plot>) -> bool, + ) -> Option> { + (0..) + .map(|i| origin + dir * i) + .find(|pos| match_fn(self.plot_at(*pos))) + } + + fn find_path( + &self, + origin: Vec2, + dest: Vec2, + mut path_cost_fn: impl FnMut(Option<&Tile>, Option<&Tile>) -> f32, + ) -> Option>> { + let heuristic = |pos: &Vec2| pos.distance_squared(dest) as f32; + let neighbors = |pos: &Vec2| { + let pos = *pos; + CARDINALS.iter().map(move |dir| pos + *dir) + }; + let transition = + |from: &Vec2, to: &Vec2| path_cost_fn(self.tile_at(*from), self.tile_at(*to)); + let satisfied = |pos: &Vec2| *pos == dest; + + Astar::new(250, origin, heuristic) + .poll(250, heuristic, neighbors, transition, satisfied) + .into_path() + } + + fn grow_from( + &self, + start: Vec2, + max_size: usize, + rng: &mut impl Rng, + mut match_fn: impl FnMut(Option<&Plot>) -> bool, + ) -> HashSet> { + let mut open = VecDeque::new(); + open.push_back(start); + let mut closed = HashSet::new(); + + while open.len() + closed.len() < max_size { + let next_pos = if let Some(next_pos) = open.pop_front() { + closed.insert(next_pos); + next_pos + } else { + break; + }; + + let dirs = [ + Vec2::new(1, 0), + Vec2::new(-1, 0), + Vec2::new(0, 1), + Vec2::new(0, -1), + ]; + + for dir in dirs.iter() { + let neighbor = next_pos + dir; + if !closed.contains(&neighbor) && match_fn(self.plot_at(neighbor)) { + open.push_back(neighbor); + } + } + } + + closed.into_iter().chain(open.into_iter()).collect() + } + + fn write_path( + &mut self, + tiles: &[Vec2], + kind: WayKind, + mut permit_fn: impl FnMut(&Plot) -> bool, + overwrite: bool, + ) { + for tiles in tiles.windows(2) { + let dir = tiles[1] - tiles[0]; + let idx = if dir.y > 0 { + 1 + } else if dir.x > 0 { + 2 + } else if dir.y < 0 { + 3 + } else if dir.x < 0 { + 0 + } else { + continue; + }; + let mut plots = &self.plots; + self.tiles + .get_mut(&tiles[1]) + .filter(|tile| permit_fn(plots.get(tile.plot))) + .map(|tile| { + if overwrite || tile.ways[(idx + 2) % 4].is_none() { + tile.ways[(idx + 2) % 4] = Some(kind); + } + }); + self.tiles + .get_mut(&tiles[0]) + .filter(|tile| permit_fn(plots.get(tile.plot))) + .map(|tile| { + if overwrite || tile.ways[idx].is_none() { + tile.ways[idx] = Some(kind); + } + }); + } + } + + pub fn new_plot(&mut self, plot: Plot) -> Id { self.plots.insert(plot) } +} + +#[derive(Hash)] +pub struct Id(usize, PhantomData); + +impl Copy for Id {} +impl Clone for Id { + fn clone(&self) -> Self { Self(self.0, PhantomData) } +} +impl Eq for Id {} +impl PartialEq for Id { + fn eq(&self, other: &Self) -> bool { self.0 == other.0 } +} + +pub struct Store { + items: HashMap, + id_counter: usize, +} + +impl Default for Store { + fn default() -> Self { + Self { + items: HashMap::new(), + id_counter: 0, + } + } +} + +impl Store { + pub fn get(&self, id: Id) -> &T { self.items.get(&id.0).unwrap() } + + pub fn get_mut(&mut self, id: Id) -> &mut T { self.items.get_mut(&id.0).unwrap() } + + pub fn iter(&self) -> impl Iterator { self.items.values() } + + pub fn insert(&mut self, item: T) -> Id { + self.id_counter += 1; + let id = Id(self.id_counter, PhantomData); + self.items.insert(id.0, item); + id + } +} diff --git a/world/src/sim/mod.rs b/world/src/sim/mod.rs index feb3643eb5..9b75fd9bb4 100644 --- a/world/src/sim/mod.rs +++ b/world/src/sim/mod.rs @@ -65,8 +65,8 @@ use vek::*; // don't think we actually cast a chunk id to float, just coordinates... could // be wrong though! pub const WORLD_SIZE: Vec2 = Vec2 { - x: 1024 * 1, - y: 1024 * 1, + x: 256 * 1, + y: 256 * 1, }; /// A structure that holds cached noise values and cumulative distribution diff --git a/world/src/util/structure.rs b/world/src/util/structure.rs index 4868159d0b..48429f3e65 100644 --- a/world/src/util/structure.rs +++ b/world/src/util/structure.rs @@ -54,10 +54,14 @@ impl StructureGen2d { let pos = Vec3::from(center); ( center - + Vec2::new( - (x_field.get(pos) % spread_mul) as i32 - spread, - (y_field.get(pos) % spread_mul) as i32 - spread, - ), + + if spread_mul > 0 { + Vec2::new( + (x_field.get(pos) % spread_mul) as i32 - spread, + (y_field.get(pos) % spread_mul) as i32 - spread, + ) + } else { + Vec2::zero() + }, seed_field.get(pos), ) }