1
0
mirror of https://github.com/osmarks/ewo3.git synced 2025-06-28 16:12:50 +00:00

World generation rework

This commit is contained in:
osmarks 2024-07-19 11:06:44 +01:00
parent fbf154fa7c
commit 561063f953
8 changed files with 745 additions and 112 deletions

3
.gitignore vendored
View File

@ -1,2 +1,3 @@
/target
/node_modules
/node_modules
out.png

127
Cargo.lock generated
View File

@ -56,6 +56,12 @@ dependencies = [
"rustc-demangle",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.5.0"
@ -71,6 +77,12 @@ dependencies = [
"generic-array",
]
[[package]]
name = "bytemuck"
version = "1.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e"
[[package]]
name = "byteorder"
version = "1.5.0"
@ -104,6 +116,40 @@ dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]]
name = "crypto-common"
version = "0.1.6"
@ -130,6 +176,12 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "either"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "equivalent"
version = "1.0.1"
@ -155,9 +207,11 @@ dependencies = [
"fastrand",
"futures-util",
"hecs",
"image",
"indexmap",
"lazy_static",
"noise-functions",
"rayon",
"seahash",
"serde",
"serde_json",
@ -173,6 +227,25 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
[[package]]
name = "fdeflate"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645"
dependencies = [
"simd-adler32",
]
[[package]]
name = "flate2"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -293,6 +366,18 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
[[package]]
name = "image"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11"
dependencies = [
"bytemuck",
"byteorder",
"num-traits",
"png",
]
[[package]]
name = "indexmap"
version = "2.2.6"
@ -350,6 +435,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae"
dependencies = [
"adler",
"simd-adler32",
]
[[package]]
@ -438,6 +524,19 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "png"
version = "0.17.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1"
dependencies = [
"bitflags 1.3.2",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
[[package]]
name = "ppv-lite86"
version = "0.2.17"
@ -492,13 +591,33 @@ dependencies = [
"getrandom",
]
[[package]]
name = "rayon"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "redox_syscall"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e"
dependencies = [
"bitflags",
"bitflags 2.5.0",
]
[[package]]
@ -576,6 +695,12 @@ dependencies = [
"libc",
]
[[package]]
name = "simd-adler32"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "slab"
version = "0.4.9"

View File

@ -3,8 +3,6 @@ name = "ewo3"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
hecs = { version = "0.10", features = ["column-serialize"] }
euclid = { version = "0.22", features = ["serde"] }
@ -20,4 +18,10 @@ slab = "0.4"
lazy_static = "1"
seahash = "4"
noise-functions = "0.2"
indexmap = "2"
indexmap = "2"
image = { version = "0.25", default-features = false, features = ["png"] }
rayon = "1"
[[bin]]
name = "worldgen"
path = "src/worldgen_test.rs"

29
src/coordtest.py Normal file
View File

@ -0,0 +1,29 @@
from collections import namedtuple
import math
Coord = namedtuple("Coord", ["q", "r"])
def naivec2i(c, rad):
q = c.q + rad
r = c.r + rad
coords_above = 0
for rprime in range(r):
coords_above += 2*rad+1 - abs(rad-rprime)
q_start = -c.r if r < rad else c.r
return coords_above + (q - q_start)
def fastc2i(c, rad):
q = c.q + rad
r = c.r + rad
fh = min(r, rad)
coords_above = fh*(rad+1) + fh*(fh-1)//2
if fh < r:
d = r - fh
coords_above += d*(2*rad+1) - d*(d-1)//2
q_start = r if r < rad else -rad
print(coords_above, q, q_start, q)
return coords_above + (c.q - q_start)
print(naivec2i(Coord(-512, 1), 512))
q, r = 0, -2
print(naivec2i(Coord(q, r), 2), fastc2i(Coord(q, r), 2))

View File

@ -1,8 +1,6 @@
use hecs::{CommandBuffer, Entity, World};
use euclid::{Point3D, Point2D, Vector2D};
use futures_util::{stream::TryStreamExt, SinkExt, StreamExt};
use indexmap::IndexMap;
use noise_functions::Sample3;
use tokio::net::{TcpListener, TcpStream};
use tokio_tungstenite::tungstenite::protocol::Message;
use tokio::sync::{mpsc, Mutex};
@ -11,29 +9,10 @@ use std::{collections::{hash_map::Entry, HashMap, HashSet, VecDeque}, convert::T
use slab::Slab;
use serde::{Serialize, Deserialize};
struct AxialWorldSpace;
struct CubicWorldSpace;
type Coord = Point2D<i64, AxialWorldSpace>;
type CubicCoord = Point3D<i64, CubicWorldSpace>;
type CoordVec = Vector2D<i64, AxialWorldSpace>;
pub mod worldgen;
pub mod map;
fn to_cubic(p0: Coord) -> CubicCoord {
CubicCoord::new(p0.x, p0.y, -p0.x - p0.y)
}
fn hex_distance(p0: Coord, p1: Coord) -> i64 {
let ax_dist = p0 - p1;
(ax_dist.x.abs() + ax_dist.y.abs() + (ax_dist.x + ax_dist.y).abs()) / 2
}
fn on_axis(p: CoordVec) -> bool {
let p = to_cubic(Coord::origin() + p);
let mut zero_ax = 0;
if p.x == 0 { zero_ax += 1 }
if p.y == 0 { zero_ax += 1 }
if p.z == 0 { zero_ax += 1 }
zero_ax >= 1
}
use map::*;
async fn handle_connection(raw_stream: TcpStream, addr: SocketAddr, mut frames_rx: mpsc::Receiver<Frame>, inputs_tx: mpsc::Sender<Input>) -> Result<()> {
let ws_stream = tokio_tungstenite::accept_async(raw_stream).await.context("websocket handshake failure")?;
@ -105,7 +84,8 @@ struct Client {
struct GameState {
world: World,
clients: Slab<Client>,
ticks: u64
ticks: u64,
map: worldgen::GeneratedWorld
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@ -255,57 +235,7 @@ impl Inventory {
}
const VIEW: i64 = 15;
const WALL: i64 = 128;
const RANDOM_DESPAWN_INV_RATE: u64 = 4000;
const DIRECTIONS: &[CoordVec] = &[CoordVec::new(0, -1), CoordVec::new(1, -1), CoordVec::new(-1, 0), CoordVec::new(1, 0), CoordVec::new(0, 1), CoordVec::new(-1, 1)];
#[derive(Debug, Clone, PartialEq, Eq)]
enum BaseTerrain {
Empty,
Occupied,
VeryOccupied
}
impl BaseTerrain {
fn can_enter(&self) -> bool {
*self == BaseTerrain::Empty
}
fn symbol(&self) -> Option<char> {
match *self {
Self::Empty => None,
Self::Occupied => Some('#'),
Self::VeryOccupied => Some('█')
}
}
}
const NOISE_SCALE: f32 = 0.05;
fn get_base_terrain(pos: Coord) -> BaseTerrain {
let distance = hex_distance(pos, Coord::origin());
if distance >= (WALL + 12) {
return BaseTerrain::VeryOccupied
}
if distance >= WALL {
return BaseTerrain::Occupied
}
let pos = to_cubic(pos);
let noise = noise_functions::CellDistance.ridged(2, 1.00, 0.20).seed(406).sample3([pos.x as f32 * NOISE_SCALE, pos.y as f32 * NOISE_SCALE, pos.z as f32 * NOISE_SCALE]);
if noise >= 0.3 {
return BaseTerrain::VeryOccupied
}
if noise >= 0.2 {
return BaseTerrain::Occupied
}
return BaseTerrain::Empty
}
fn sample_range(range: i64) -> CoordVec {
let q = fastrand::i64(-range..=range);
let r = fastrand::i64((-range).max(-q-range)..=range.min(-q+range));
CoordVec::new(q, r)
}
struct EnemySpec {
symbol: char,
@ -335,10 +265,6 @@ impl EnemySpec {
}
}
fn count_hexes(x: i64) -> i64 {
x*(x+1)*3+1
}
fn rng_from_hash<H: Hash>(x: H) -> fastrand::Rng {
let mut h = seahash::SeaHasher::new();
x.hash(&mut h);
@ -425,7 +351,7 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
}
newpos = pos + sample_range(*spawn_range.end());
}
if !occupied && get_base_terrain(newpos).can_enter() && hex_distance(newpos, pos) >= *spawn_range.start() {
if !occupied && state.map.get_terrain(newpos).entry_cost().is_some() && hex_distance(newpos, pos) >= *spawn_range.start() {
let mut spec = EnemySpec::random();
spec.drops.push((Item::Bone, StochasticNumber::Triangle { min: 0.7 * spec.initial_health / 40.0, max: 1.3 * spec.initial_health / 40.0, mode: spec.initial_health / 40.0 }));
if spec.ranged {
@ -558,7 +484,7 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
}
let target = position + next_movement;
if get_base_terrain(target).can_enter() && target != position {
if state.map.get_terrain(target).entry_cost().is_some() && target != position {
buffer.insert_one(client.entity, MovingInto(target));
}
}
@ -568,6 +494,7 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
let mut despawn_buffer = HashSet::new();
// This might lead to a duping glitch, which would at least be funny.
// TODO: Players should drop items on disconnect.
let kill = |buffer: &mut CommandBuffer, despawn_buffer: &mut HashSet<Entity>, state: &GameState, entity: Entity, killer: Option<Entity>, position: Option<Coord>| {
let position = position.unwrap_or_else(|| state.world.get::<&Position>(entity).unwrap().head());
despawn_buffer.insert(entity);
@ -618,7 +545,8 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
}
}
if get_base_terrain(*target_pos).can_enter() {
if let Some(entry_cost) = state.map.get_terrain(*target_pos).entry_cost() {
move_cost += entry_cost as f32;
let entry = match positions.entry(*target_pos) {
Entry::Occupied(o) => {
let target_entity = *o.get();
@ -696,28 +624,24 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
let mut nearby = vec![];
if let Ok(pos) = state.world.get::<&Position>(client.entity) {
let pos = pos.head();
for q in -VIEW..=VIEW {
for r in (-VIEW).max(-q - VIEW)..= VIEW.min(-q+VIEW) {
let offset = CoordVec::new(q, r);
let pos = pos + offset;
if let Some(symbol) = get_base_terrain(pos).symbol() {
nearby.push((q, r, symbol, 1.0));
} else {
if let Some(entity) = positions.get(&pos) {
let render = state.world.get::<&Render>(*entity)?;
let health = if let Ok(h) = state.world.get::<&Health>(*entity) {
h.pct()
} else { 1.0 };
nearby.push((q, r, render.0, health));
} else if let Some(entity) = terrain_positions.get(&pos) {
let render = state.world.get::<&Render>(*entity)?;
nearby.push((q, r, render.0, 1.0));
} else {
let mut rng = rng_from_hash(pos);
let bg = if rng.usize(0..10) == 0 { ',' } else { '.' };
nearby.push((q, r, bg, rng.f32() * 0.1 + 0.9))
}
}
for offset in hex_circle(VIEW) {
let pos = pos + offset;
let mut rng = rng_from_hash(pos);
if let Some(entity) = positions.get(&pos) {
let render = state.world.get::<&Render>(*entity)?;
let health = if let Ok(h) = state.world.get::<&Health>(*entity) {
h.pct()
} else { 1.0 };
nearby.push((offset.x, offset.y, render.0, health));
} else if let Some(entity) = terrain_positions.get(&pos) {
let render = state.world.get::<&Render>(*entity)?;
nearby.push((offset.x, offset.y, render.0, 1.0));
} else if let Some(terrain) = state.map.get_terrain(pos).symbol() {
nearby.push((offset.x, offset.y, terrain, rng.f32() * 0.1 + 0.9));
} else {
let bg = if rng.usize(0..10) == 0 { ',' } else { '.' };
nearby.push((offset.x, offset.y, bg, rng.f32() * 0.1 + 0.9))
}
}
let health = state.world.get::<&Health>(client.entity)?.0;
@ -756,8 +680,8 @@ fn random_identifier() -> char {
fn add_new_player(state: &mut GameState) -> Result<Entity> {
let pos = loop {
let pos = Coord::origin() + sample_range(WALL - 10);
if get_base_terrain(pos).can_enter() {
let pos = Coord::origin() + sample_range(state.map.radius() - 10);
if state.map.get_terrain(pos).entry_cost().is_some() {
break pos;
}
};
@ -786,7 +710,8 @@ async fn main() -> Result<()> {
let state = Arc::new(Mutex::new(GameState {
world: World::new(),
clients: Slab::new(),
ticks: 0
ticks: 0,
map: worldgen::generate_world()
}));
let try_socket = TcpListener::bind(&addr).await;

55
src/map.rs Normal file
View File

@ -0,0 +1,55 @@
use euclid::{Point3D, Point2D, Vector2D};
pub struct AxialWorldSpace;
pub struct CubicWorldSpace;
pub type Coord = Point2D<i64, AxialWorldSpace>;
pub type CubicCoord = Point3D<i64, CubicWorldSpace>;
pub type CoordVec = Vector2D<i64, AxialWorldSpace>;
pub fn to_cubic(p0: Coord) -> CubicCoord {
CubicCoord::new(p0.x, p0.y, -p0.x - p0.y)
}
pub fn hex_distance(p0: Coord, p1: Coord) -> i64 {
let ax_dist = p0 - p1;
(ax_dist.x.abs() + ax_dist.y.abs() + (ax_dist.x + ax_dist.y).abs()) / 2
}
pub fn on_axis(p: CoordVec) -> bool {
let p = to_cubic(Coord::origin() + p);
let mut zero_ax = 0;
if p.x == 0 { zero_ax += 1 }
if p.y == 0 { zero_ax += 1 }
if p.z == 0 { zero_ax += 1 }
zero_ax >= 1
}
pub fn rotate_60(p0: CoordVec) -> CoordVec {
let s = -p0.x - p0.y;
CoordVec::new(s, p0.x)
}
pub const DIRECTIONS: &[CoordVec] = &[CoordVec::new(0, -1), CoordVec::new(1, -1), CoordVec::new(-1, 0), CoordVec::new(1, 0), CoordVec::new(0, 1), CoordVec::new(-1, 1)];
pub fn sample_range(range: i64) -> CoordVec {
let q = fastrand::i64(-range..=range);
let r = fastrand::i64((-range).max(-q-range)..=range.min(-q+range));
CoordVec::new(q, r)
}
pub fn hex_circle(range: i64) -> impl Iterator<Item=CoordVec> {
(-range..=range).flat_map(move |q| {
((-range).max(-q - range)..= range.min(-q+range)).map(move |r| {
CoordVec::new(q, r)
})
})
}
pub fn hex_range(range: i64) -> impl Iterator<Item=(i64, CoordVec)> {
(0..=range).flat_map(|x| hex_circle(x).map(move |c| (x, c)))
}
pub fn count_hexes(x: i64) -> i64 {
x*(x+1)*3+1
}

446
src/worldgen.rs Normal file
View File

@ -0,0 +1,446 @@
use std::{cmp::Ordering, collections::{hash_map::Entry, BinaryHeap, HashMap, HashSet}, hash::{Hash, Hasher}, ops::{Index, IndexMut}};
use noise_functions::Sample3;
use serde::{Deserialize, Serialize};
use crate::map::*;
pub const WORLD_RADIUS: i64 = 1024;
const NOISE_SCALE: f32 = 0.001;
const HEIGHT_EXPONENT: f32 = 0.3;
const WATER_SOURCES: usize = 40;
pub fn height_baseline(pos: Coord) -> f32 {
let w_frac = (hex_distance(pos, Coord::origin()) as f32 / WORLD_RADIUS as f32).powf(3.0);
let pos = to_cubic(pos);
let noise =
noise_functions::OpenSimplex2s.ridged(6, 0.7, 1.55).seed(406).sample3([10.0 + pos.x as f32 * NOISE_SCALE, pos.y as f32 * NOISE_SCALE, pos.z as f32 * NOISE_SCALE]);
let range = 1.0 - 2.0 * (w_frac - 0.5).powf(2.0);
noise * range - w_frac
}
fn percentilize<F: Fn(f32) -> f32>(raw: &mut Map<f32>, postprocess: F) {
let mut xs: Vec<(usize, f32)> = raw.data.iter().copied().enumerate().collect();
xs.sort_unstable_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
for (j, (i, _x)) in xs.into_iter().enumerate() {
let percentile = j as f32 / raw.data.len() as f32;
raw.data[i] = postprocess(percentile);
}
}
pub fn generate_heights() -> Map<f32> {
let mut raw = Map::<f32>::from_fn(height_baseline, WORLD_RADIUS);
percentilize(&mut raw, |x| {
let s = 1.0 - (1.0 - x).powf(HEIGHT_EXPONENT);
s * 2.0 - 1.0
});
raw
}
struct CoordsIndexIterator {
radius: i64,
index: usize,
r: i64,
q: i64,
max: usize
}
impl Iterator for CoordsIndexIterator {
type Item = (Coord, usize);
fn next(&mut self) -> Option<Self::Item> {
if self.index == self.max {
return None;
}
let result = (Coord::new(self.q, self.r), self.index);
self.index += 1;
self.q += 1;
if self.r < 0 && self.q == self.radius + 1 {
self.r += 1;
self.q = -self.radius - self.r;
}
if self.r >= 0 && self.q + self.r == self.radius + 1 {
self.r += 1;
self.q = -self.radius;
}
Some(result)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Map<T> {
pub data: Vec<T>,
pub radius: i64
}
impl<T> Map<T> {
pub fn new(radius: i64, fill: T) -> Map<T> where T: Clone {
let size = count_hexes(radius) as usize;
Map {
data: vec![fill; size],
radius
}
}
pub fn from_fn<S, F: FnMut(Coord) -> S>(mut f: F, radius: i64) -> Map<S> {
let size = count_hexes(radius) as usize;
Map {
radius,
data: Vec::from_iter(CoordsIndexIterator {
radius,
index: 0,
max: size,
r: -radius,
q: 0
}.map(|(c, _i)| f(c)))
}
}
pub fn map<S, F: FnMut(&T) -> S>(mut f: F, other: &Self) -> Map<S> {
Map::<S>::from_fn(|c| f(&other[c]), other.radius)
}
fn coord_to_index(&self, c: Coord) -> usize {
let r = c.y + self.radius;
let fh = r.min(self.radius);
let mut coords_above = fh*(self.radius+1) + fh*(fh-1)/2;
if fh < r {
let d = r - fh;
coords_above += d*(2*self.radius+1) - d*(d-1)/2;
}
let q_start = if r < self.radius { -r } else { -self.radius };
(coords_above + (c.x - q_start)) as usize
}
fn in_range(&self, coord: Coord) -> bool {
hex_distance(coord, Coord::origin()) <= self.radius
}
fn iter_coords(&self) -> impl Iterator<Item=(Coord, usize)> {
CoordsIndexIterator {
radius: self.radius,
index: 0,
max: self.data.len(),
r: -self.radius,
q: 0
}
}
pub fn iter(&self) -> impl Iterator<Item=(Coord, &T)> {
self.iter_coords().map(|(c, i)| (c, &self.data[i]))
}
}
impl<T> Index<Coord> for Map<T> {
type Output = T;
fn index(&self, index: Coord) -> &Self::Output {
//println!("{:?}", index);
&self.data[self.coord_to_index(index)]
}
}
impl<T> IndexMut<Coord> for Map<T> {
fn index_mut(&mut self, index: Coord) -> &mut Self::Output {
let i = self.coord_to_index(index);
&mut self.data[i]
}
}
pub fn generate_contours(field: &Map<f32>, interval: f32) -> Vec<(Coord, f32, f32, CoordVec)> {
let mut v = vec![];
// Starting at the origin, we want to detect contour lines in any of the six directions.
// Go in one of the perpendicular directions to generate base directions then scan until the edge is reached starting from any of those points.
for scan_direction in DIRECTIONS {
// "perpendicular" doesn't really work in axial coordinates, but this apparently does so whatever
let slit_direction = rotate_60(*scan_direction);
for x in 0..field.radius {
let base = Coord::zero() + slit_direction * x;
let mut last: Option<f32> = None;
for y in 0..field.radius {
let point = base + *scan_direction * y;
if hex_distance(point, Coord::zero()) <= field.radius {
let sample = field[point] / interval;
if let Some(last) = last {
if last.trunc() != sample.trunc() {
v.push((point, last, sample, *scan_direction));
}
}
last = Some(sample);
}
}
}
}
v
}
#[derive(Clone, Copy, Debug)]
struct PointWrapper<T: Hash + Eq + PartialEq>(f32, T);
impl<T: Hash + Eq + PartialEq> PartialEq for PointWrapper<T> {
fn eq(&self, other: &Self) -> bool {
assert!(self.0.is_finite() && other.0.is_finite());
self.0 == other.0 && self.1 == other.1
}
}
impl<T: Hash + Eq + PartialEq> Eq for PointWrapper<T> {}
fn hash_thing<T: Hash>(thing: &T) -> u64 {
let mut hasher = seahash::SeaHasher::new();
thing.hash(&mut hasher);
hasher.finish()
}
// we only need a consistent ordering, even if it's not semantically meaningful
// reverse order so that we can pop the smallest element first
impl<T: Hash + Eq + PartialEq> Ord for PointWrapper<T> {
fn cmp(&self, other: &Self) -> Ordering {
other.0.partial_cmp(&self.0).unwrap().then_with(|| hash_thing(&self.1).cmp(&hash_thing(&other.1)))
}
}
impl<T: Hash + Eq + PartialEq> PartialOrd for PointWrapper<T> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
pub fn generate_separated_high_points(n: usize, sep: i64, map: &Map<f32>) -> Vec<Coord> {
let mut points = vec![];
let mut priority = BinaryHeap::with_capacity(map.data.len());
for (coord, height) in map.iter() {
priority.push(PointWrapper(-*height, coord));
}
while points.len() < n {
let next = priority.pop().unwrap();
if points.iter().all(|x| hex_distance(*x, next.1) > sep) {
points.push(next.1)
}
}
points
}
fn astar<C: PartialEq + Eq + Hash + Copy + std::fmt::Debug, F: FnMut(C) -> f32, G: FnMut(C) -> I, I: Iterator<Item=(C, f32)>, H: FnMut(C) -> bool>(start: C, mut is_end: H, mut heuristic: F, mut get_neighbors: G) -> Vec<C> {
let mut frontier = BinaryHeap::new();
frontier.push(PointWrapper(0.0, start));
let mut came_from: HashMap<C, PointWrapper<C>> = HashMap::new();
came_from.insert(start, PointWrapper(0.0, start));
let mut end = None;
while let Some(PointWrapper(_est_cost, current)) = frontier.pop() {
if is_end(current) {
end = Some(current);
break
}
let cost = came_from[&current].0;
for (neighbour, next_cost) in get_neighbors(current) {
let new_cost = cost + next_cost;
match came_from.entry(neighbour.clone()) {
Entry::Occupied(mut o) => {
let PointWrapper(old_cost, _old_parent) = o.get();
if new_cost < *old_cost {
o.insert(PointWrapper(new_cost, current.clone()));
frontier.push(PointWrapper(new_cost + heuristic(neighbour), neighbour));
}
},
Entry::Vacant(v) => {
v.insert(PointWrapper(new_cost, current.clone()));
frontier.push(PointWrapper(new_cost + heuristic(neighbour), neighbour));
}
}
}
}
let mut end = end.unwrap();
let mut path = vec![];
while let Some(next) = came_from.get(&end) {
path.push(next.1);
end = came_from.get(&end).unwrap().1;
if end == start {
break;
}
}
path.reverse();
path
}
pub fn distance_map<I: Iterator<Item=Coord>>(radius: i64, sources: I) -> Map<f32> {
let radius_f = radius as f32;
let mut distances = Map::<f32>::new(radius, radius_f);
let mut queue = BinaryHeap::new();
for source in sources {
queue.push(PointWrapper(0.0, source));
}
while let Some(PointWrapper(dist, coord)) = queue.pop() {
if distances[coord] < radius_f {
continue;
}
if dist < radius_f {
distances[coord] = dist;
}
for offset in DIRECTIONS {
let neighbor = coord + *offset;
let new_distance = dist + 1.0;
if distances.in_range(neighbor) && new_distance < distances[neighbor] {
queue.push(PointWrapper(new_distance, neighbor));
}
}
}
distances
}
pub fn compute_humidity(distances: Map<f32>, heightmap: &Map<f32>) -> Map<f32> {
let mut humidity = distances;
percentilize(&mut humidity, |x| (1.0 - x).powf(0.3));
for (coord, h) in heightmap.iter() {
humidity[coord] -= *h * 0.6;
}
percentilize(&mut humidity, |x| x.powf(1.0));
humidity
}
const SEA_LEVEL: f32 = -0.8;
const EROSION: f32 = 0.05;
pub fn simulate_water(heightmap: &mut Map<f32>) -> Map<f32> {
let mut watermap = Map::<f32>::new(heightmap.radius, 0.0);
let sources = generate_separated_high_points(WATER_SOURCES, WORLD_RADIUS / 10, &heightmap);
let sinks = heightmap.iter_coords().filter(|(c, _)| heightmap[*c] <= SEA_LEVEL).map(|(c, _)| c).collect::<HashSet<_>>();
for source in sources.iter() {
let heightmap_ = &*heightmap;
let watermap_ = &watermap;
let get_neighbours = |c: Coord| {
DIRECTIONS.iter().flat_map(move |offset| {
let neighbor = c + *offset;
if heightmap_.in_range(neighbor) {
let factor = if watermap_[neighbor] > 0.0 { 0.1 } else { 1.0 };
Some((neighbor, factor * (heightmap_[neighbor] - heightmap_[c] + 0.00).max(0.0)))
} else {
None
}
})
};
let heuristic = |c: Coord| {
(heightmap[c] * 0.00).max(0.0)
};
let path = astar(*source, |c| sinks.contains(&c), heuristic, get_neighbours);
for point in path {
let water_range_raw = watermap[point] * 1.0 - heightmap[point];
let water_range = water_range_raw.ceil() as i64;
for (_, nearby) in hex_range(water_range) {
if !watermap.in_range(point + nearby) {
continue;
}
watermap[point + nearby] += 0.5;
watermap[point + nearby] = watermap[point + nearby].min(3.0);
}
let erosion_range_raw = water_range_raw * 2.0 + 2.0;
let erosion_range = erosion_range_raw.ceil() as i64;
for (this_range, nearby) in hex_range(erosion_range) {
if !watermap.in_range(point + nearby) {
continue;
}
if this_range > 0 {
heightmap[point + nearby] -= EROSION * watermap[point] / (this_range as f32) / erosion_range_raw.max(1.0);
heightmap[point + nearby] = heightmap[point + nearby].max(SEA_LEVEL);
}
}
}
}
for sink in sinks {
watermap[sink] = 10.0;
}
watermap
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TerrainType {
Empty,
Contour,
ShallowWater,
DeepWater,
Wall
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeneratedWorld {
heightmap: Map<f32>,
terrain: Map<TerrainType>,
humidity: Map<f32>
}
pub fn generate_world() -> GeneratedWorld {
let mut heightmap = generate_heights();
let mut terrain = Map::<TerrainType>::new(heightmap.radius, TerrainType::Empty);
let water = simulate_water(&mut heightmap);
let contours = generate_contours(&heightmap, 0.15);
for (point, _, _, _) in contours {
terrain[point] = TerrainType::Contour;
}
for (point, water) in water.iter() {
if *water > 1.0 {
terrain[point] = TerrainType::DeepWater;
} else if *water > 0.0 {
terrain[point] = TerrainType::ShallowWater;
}
}
let distances = distance_map(
WORLD_RADIUS,
water.iter().filter_map(|(c, i)| if *i > 0.0 { Some(c) } else { None }));
let humidity = compute_humidity(distances, &heightmap);
GeneratedWorld {
heightmap,
terrain,
humidity
}
}
impl TerrainType {
pub fn entry_cost(&self) -> Option<i64> {
match *self {
Self::Empty => Some(0),
Self::Wall => None,
Self::ShallowWater => Some(10),
Self::DeepWater => None,
Self::Contour => Some(1)
}
}
pub fn symbol(&self) -> Option<char> {
match *self {
Self::Empty => None,
Self::Wall => Some('#'),
Self::ShallowWater => Some('~'),
Self::DeepWater => Some('≈'),
Self::Contour => Some('/')
}
}
}
impl GeneratedWorld {
pub fn get_terrain(&self, pos: Coord) -> TerrainType {
let distance = hex_distance(pos, Coord::origin());
if distance >= self.heightmap.radius {
return TerrainType::Wall
}
self.terrain[pos].clone()
}
pub fn radius(&self) -> i64 {
self.heightmap.radius
}
}

48
src/worldgen_test.rs Normal file
View File

@ -0,0 +1,48 @@
use anyhow::Result;
use image::{ImageBuffer, Rgb};
use std::collections::{HashMap, HashSet};
mod worldgen;
mod map;
use worldgen::*;
use map::*;
fn main() -> Result<()> {
let mut heightmap = generate_heights();
println!("hydro...");
let water = simulate_water(&mut heightmap);
println!("contours...");
let contours = generate_contours(&heightmap, 0.15);
let mut contour_points = HashMap::new();
for (point, x1, x2, _) in contours {
let steepness = x1 - x2;
let entry = contour_points.entry(point).or_default();
*entry = std::cmp::max(*entry, (steepness * 4000.0).abs() as u8);
}
println!("humidity...");
let water_distances = distance_map(
WORLD_RADIUS,
water.iter().filter_map(|(c, i)| if *i > 0.0 { Some(c) } else { None }));
let humidity = compute_humidity(water_distances, &heightmap);
println!("rendering...");
let mut image = ImageBuffer::from_pixel((WORLD_RADIUS * 2 + 1) as u32, (WORLD_RADIUS * 2 + 1) as u32, Rgb::from([0u8, 0, 0]));
for (position, value) in heightmap.iter() {
let col = position.x + (position.y - (position.y & 1)) / 2 + WORLD_RADIUS;
let row = position.y + WORLD_RADIUS;
//let contour = contour_points.get(&position).copied().unwrap_or_default();
let contour = (255.0 * humidity[position]) as u8;
let water = water[position];
image.put_pixel(col as u32, row as u32, Rgb::from([contour, 0, (water.min(1.0) * 255.0) as u8]));
}
image.save("./out.png")?;
Ok(())
}