mirror of
https://github.com/osmarks/ewo3.git
synced 2025-06-29 08:32:52 +00:00
World generation rework
This commit is contained in:
parent
fbf154fa7c
commit
561063f953
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
/target
|
/target
|
||||||
/node_modules
|
/node_modules
|
||||||
|
out.png
|
127
Cargo.lock
generated
127
Cargo.lock
generated
@ -56,6 +56,12 @@ dependencies = [
|
|||||||
"rustc-demangle",
|
"rustc-demangle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "1.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@ -71,6 +77,12 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytemuck"
|
||||||
|
version = "1.16.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@ -104,6 +116,40 @@ dependencies = [
|
|||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
@ -130,6 +176,12 @@ dependencies = [
|
|||||||
"crypto-common",
|
"crypto-common",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@ -155,9 +207,11 @@ dependencies = [
|
|||||||
"fastrand",
|
"fastrand",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hecs",
|
"hecs",
|
||||||
|
"image",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"noise-functions",
|
"noise-functions",
|
||||||
|
"rayon",
|
||||||
"seahash",
|
"seahash",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -173,6 +227,25 @@ version = "2.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
|
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]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@ -293,6 +366,18 @@ version = "1.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
|
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]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.2.6"
|
version = "2.2.6"
|
||||||
@ -350,6 +435,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae"
|
checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"adler",
|
"adler",
|
||||||
|
"simd-adler32",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -438,6 +524,19 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
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]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@ -492,13 +591,33 @@ dependencies = [
|
|||||||
"getrandom",
|
"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]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e"
|
checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.5.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -576,6 +695,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simd-adler32"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.9"
|
version = "0.4.9"
|
||||||
|
@ -3,8 +3,6 @@ name = "ewo3"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hecs = { version = "0.10", features = ["column-serialize"] }
|
hecs = { version = "0.10", features = ["column-serialize"] }
|
||||||
euclid = { version = "0.22", features = ["serde"] }
|
euclid = { version = "0.22", features = ["serde"] }
|
||||||
@ -21,3 +19,9 @@ lazy_static = "1"
|
|||||||
seahash = "4"
|
seahash = "4"
|
||||||
noise-functions = "0.2"
|
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
29
src/coordtest.py
Normal 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))
|
119
src/main.rs
119
src/main.rs
@ -1,8 +1,6 @@
|
|||||||
use hecs::{CommandBuffer, Entity, World};
|
use hecs::{CommandBuffer, Entity, World};
|
||||||
use euclid::{Point3D, Point2D, Vector2D};
|
|
||||||
use futures_util::{stream::TryStreamExt, SinkExt, StreamExt};
|
use futures_util::{stream::TryStreamExt, SinkExt, StreamExt};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use noise_functions::Sample3;
|
|
||||||
use tokio::net::{TcpListener, TcpStream};
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
use tokio_tungstenite::tungstenite::protocol::Message;
|
use tokio_tungstenite::tungstenite::protocol::Message;
|
||||||
use tokio::sync::{mpsc, Mutex};
|
use tokio::sync::{mpsc, Mutex};
|
||||||
@ -11,29 +9,10 @@ use std::{collections::{hash_map::Entry, HashMap, HashSet, VecDeque}, convert::T
|
|||||||
use slab::Slab;
|
use slab::Slab;
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
struct AxialWorldSpace;
|
pub mod worldgen;
|
||||||
struct CubicWorldSpace;
|
pub mod map;
|
||||||
type Coord = Point2D<i64, AxialWorldSpace>;
|
|
||||||
type CubicCoord = Point3D<i64, CubicWorldSpace>;
|
|
||||||
type CoordVec = Vector2D<i64, AxialWorldSpace>;
|
|
||||||
|
|
||||||
fn to_cubic(p0: Coord) -> CubicCoord {
|
use map::*;
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_connection(raw_stream: TcpStream, addr: SocketAddr, mut frames_rx: mpsc::Receiver<Frame>, inputs_tx: mpsc::Sender<Input>) -> Result<()> {
|
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")?;
|
let ws_stream = tokio_tungstenite::accept_async(raw_stream).await.context("websocket handshake failure")?;
|
||||||
@ -105,7 +84,8 @@ struct Client {
|
|||||||
struct GameState {
|
struct GameState {
|
||||||
world: World,
|
world: World,
|
||||||
clients: Slab<Client>,
|
clients: Slab<Client>,
|
||||||
ticks: u64
|
ticks: u64,
|
||||||
|
map: worldgen::GeneratedWorld
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
@ -255,57 +235,7 @@ impl Inventory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const VIEW: i64 = 15;
|
const VIEW: i64 = 15;
|
||||||
const WALL: i64 = 128;
|
|
||||||
const RANDOM_DESPAWN_INV_RATE: u64 = 4000;
|
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 {
|
struct EnemySpec {
|
||||||
symbol: char,
|
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 {
|
fn rng_from_hash<H: Hash>(x: H) -> fastrand::Rng {
|
||||||
let mut h = seahash::SeaHasher::new();
|
let mut h = seahash::SeaHasher::new();
|
||||||
x.hash(&mut h);
|
x.hash(&mut h);
|
||||||
@ -425,7 +351,7 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
|
|||||||
}
|
}
|
||||||
newpos = pos + sample_range(*spawn_range.end());
|
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();
|
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 }));
|
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 {
|
if spec.ranged {
|
||||||
@ -558,7 +484,7 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let target = position + next_movement;
|
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));
|
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();
|
let mut despawn_buffer = HashSet::new();
|
||||||
|
|
||||||
// This might lead to a duping glitch, which would at least be funny.
|
// 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 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());
|
let position = position.unwrap_or_else(|| state.world.get::<&Position>(entity).unwrap().head());
|
||||||
despawn_buffer.insert(entity);
|
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) {
|
let entry = match positions.entry(*target_pos) {
|
||||||
Entry::Occupied(o) => {
|
Entry::Occupied(o) => {
|
||||||
let target_entity = *o.get();
|
let target_entity = *o.get();
|
||||||
@ -696,28 +624,24 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
|
|||||||
let mut nearby = vec![];
|
let mut nearby = vec![];
|
||||||
if let Ok(pos) = state.world.get::<&Position>(client.entity) {
|
if let Ok(pos) = state.world.get::<&Position>(client.entity) {
|
||||||
let pos = pos.head();
|
let pos = pos.head();
|
||||||
for q in -VIEW..=VIEW {
|
for offset in hex_circle(VIEW) {
|
||||||
for r in (-VIEW).max(-q - VIEW)..= VIEW.min(-q+VIEW) {
|
|
||||||
let offset = CoordVec::new(q, r);
|
|
||||||
let pos = pos + offset;
|
let pos = pos + offset;
|
||||||
if let Some(symbol) = get_base_terrain(pos).symbol() {
|
let mut rng = rng_from_hash(pos);
|
||||||
nearby.push((q, r, symbol, 1.0));
|
|
||||||
} else {
|
|
||||||
if let Some(entity) = positions.get(&pos) {
|
if let Some(entity) = positions.get(&pos) {
|
||||||
let render = state.world.get::<&Render>(*entity)?;
|
let render = state.world.get::<&Render>(*entity)?;
|
||||||
let health = if let Ok(h) = state.world.get::<&Health>(*entity) {
|
let health = if let Ok(h) = state.world.get::<&Health>(*entity) {
|
||||||
h.pct()
|
h.pct()
|
||||||
} else { 1.0 };
|
} else { 1.0 };
|
||||||
nearby.push((q, r, render.0, health));
|
nearby.push((offset.x, offset.y, render.0, health));
|
||||||
} else if let Some(entity) = terrain_positions.get(&pos) {
|
} else if let Some(entity) = terrain_positions.get(&pos) {
|
||||||
let render = state.world.get::<&Render>(*entity)?;
|
let render = state.world.get::<&Render>(*entity)?;
|
||||||
nearby.push((q, r, render.0, 1.0));
|
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 {
|
} else {
|
||||||
let mut rng = rng_from_hash(pos);
|
|
||||||
let bg = if rng.usize(0..10) == 0 { ',' } else { '.' };
|
let bg = if rng.usize(0..10) == 0 { ',' } else { '.' };
|
||||||
nearby.push((q, r, bg, rng.f32() * 0.1 + 0.9))
|
nearby.push((offset.x, offset.y, bg, rng.f32() * 0.1 + 0.9))
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let health = state.world.get::<&Health>(client.entity)?.0;
|
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> {
|
fn add_new_player(state: &mut GameState) -> Result<Entity> {
|
||||||
let pos = loop {
|
let pos = loop {
|
||||||
let pos = Coord::origin() + sample_range(WALL - 10);
|
let pos = Coord::origin() + sample_range(state.map.radius() - 10);
|
||||||
if get_base_terrain(pos).can_enter() {
|
if state.map.get_terrain(pos).entry_cost().is_some() {
|
||||||
break pos;
|
break pos;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -786,7 +710,8 @@ async fn main() -> Result<()> {
|
|||||||
let state = Arc::new(Mutex::new(GameState {
|
let state = Arc::new(Mutex::new(GameState {
|
||||||
world: World::new(),
|
world: World::new(),
|
||||||
clients: Slab::new(),
|
clients: Slab::new(),
|
||||||
ticks: 0
|
ticks: 0,
|
||||||
|
map: worldgen::generate_world()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let try_socket = TcpListener::bind(&addr).await;
|
let try_socket = TcpListener::bind(&addr).await;
|
||||||
|
55
src/map.rs
Normal file
55
src/map.rs
Normal 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
446
src/worldgen.rs
Normal 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[¤t].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
48
src/worldgen_test.rs
Normal 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(())
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user