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:
parent
fbf154fa7c
commit
561063f953
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
/target
|
||||
/node_modules
|
||||
/node_modules
|
||||
out.png
|
127
Cargo.lock
generated
127
Cargo.lock
generated
@ -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"
|
||||
|
10
Cargo.toml
10
Cargo.toml
@ -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
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))
|
139
src/main.rs
139
src/main.rs
@ -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
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