mirror of
https://github.com/osmarks/ewo3.git
synced 2025-01-02 21:40:36 +00:00
Fix rivers and lakes
This commit is contained in:
parent
561063f953
commit
7977ed4d10
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
/target
|
||||
/node_modules
|
||||
out.png
|
||||
out.png
|
||||
world.bin
|
26
Cargo.lock
generated
26
Cargo.lock
generated
@ -56,6 +56,25 @@ dependencies = [
|
||||
"rustc-demangle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "2.0.0-rc.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f11ea1a0346b94ef188834a65c068a03aec181c94896d481d7a0a40d85b0ce95"
|
||||
dependencies = [
|
||||
"bincode_derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bincode_derive"
|
||||
version = "2.0.0-rc.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e30759b3b99a1b802a7a3aa21c85c3ded5c28e1c83170d82d70f08bbf7f3e4c"
|
||||
dependencies = [
|
||||
"virtue",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
@ -203,6 +222,7 @@ name = "ewo3"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
"euclid",
|
||||
"fastrand",
|
||||
"futures-util",
|
||||
@ -869,6 +889,12 @@ version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "virtue"
|
||||
version = "0.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9dcc60c0624df774c82a0ef104151231d37da4962957d691c011c852b2473314"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
|
@ -21,6 +21,7 @@ noise-functions = "0.2"
|
||||
indexmap = "2"
|
||||
image = { version = "0.25", default-features = false, features = ["png"] }
|
||||
rayon = "1"
|
||||
bincode = { version = "2.0.0-rc.3", features = ["serde"] }
|
||||
|
||||
[[bin]]
|
||||
name = "worldgen"
|
||||
|
61
src/main.rs
61
src/main.rs
@ -183,6 +183,9 @@ struct Energy { current: f32, regeneration_rate: f32, burst: f32 }
|
||||
#[derive(Debug, Clone)]
|
||||
struct Drops(Vec<(Item, StochasticNumber)>);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Jump(i64);
|
||||
|
||||
impl Energy {
|
||||
fn try_consume(&mut self, cost: f32) -> bool {
|
||||
if self.current >= -1e-12 { // numerics
|
||||
@ -245,6 +248,7 @@ struct EnemySpec {
|
||||
move_delay: usize,
|
||||
attack_cooldown: u64,
|
||||
ranged: bool,
|
||||
movement: i64,
|
||||
drops: Vec<(Item, StochasticNumber)>
|
||||
}
|
||||
|
||||
@ -252,14 +256,14 @@ impl EnemySpec {
|
||||
// Numbers ported from original EWO. Fudge constants added elsewhere.
|
||||
fn random() -> EnemySpec {
|
||||
match fastrand::usize(0..650) {
|
||||
0..=99 => EnemySpec { symbol: 'I', min_damage: 10.0, damage_range: 5.0, initial_health: 50.0, move_delay: 70, attack_cooldown: 10, ranged: false, drops: vec![] }, // IBIS
|
||||
100..=199 => EnemySpec { symbol: 'K', min_damage: 5.0, damage_range: 15.0, initial_health: 30.0, move_delay: 40, attack_cooldown: 10, ranged: false, drops: vec![] }, // KESTREL
|
||||
200..=299 => EnemySpec { symbol: 'S', min_damage: 5.0, damage_range: 5.0, initial_health: 20.0, move_delay: 50, attack_cooldown: 10, ranged: false, drops: vec![] }, // SNAKE
|
||||
300..=399 => EnemySpec { symbol: 'E', min_damage: 10.0, damage_range: 20.0, initial_health: 80.0, move_delay: 80, attack_cooldown: 10, ranged: false, drops: vec![] }, // EMU
|
||||
400..=499 => EnemySpec { symbol: 'O', min_damage: 8.0, damage_range: 17.0, initial_health: 150.0, move_delay: 100, attack_cooldown: 10, ranged: false, drops: vec![] }, // OGRE
|
||||
500..=599 => EnemySpec { symbol: 'R', min_damage: 5.0, damage_range: 5.0, initial_health: 15.0, move_delay: 40, attack_cooldown: 10, ranged: false, drops: vec![] }, // RAT
|
||||
600..=609 => EnemySpec { symbol: 'M' , min_damage: 20.0, damage_range: 10.0, initial_health: 150.0, move_delay: 70, attack_cooldown: 10, ranged: false, drops: vec![] }, // MOA
|
||||
610..=649 => EnemySpec { symbol: 'P', min_damage: 10.0, damage_range: 5.0, initial_health: 15.0, move_delay: 20, attack_cooldown: 10, ranged: true, drops: vec![] }, // PLATYPUS
|
||||
0..=99 => EnemySpec { symbol: 'I', min_damage: 10.0, damage_range: 5.0, initial_health: 50.0, move_delay: 70, attack_cooldown: 10, ranged: false, drops: vec![], movement: 1 }, // IBIS
|
||||
100..=199 => EnemySpec { symbol: 'K', min_damage: 5.0, damage_range: 25.0, initial_health: 60.0, move_delay: 30, attack_cooldown: 12, ranged: false, drops: vec![], movement: 2 }, // KANGAROO
|
||||
200..=299 => EnemySpec { symbol: 'S', min_damage: 5.0, damage_range: 5.0, initial_health: 20.0, move_delay: 50, attack_cooldown: 10, ranged: false, drops: vec![], movement: 1 }, // SNAKE
|
||||
300..=399 => EnemySpec { symbol: 'E', min_damage: 10.0, damage_range: 20.0, initial_health: 80.0, move_delay: 80, attack_cooldown: 10, ranged: false, drops: vec![], movement: 1 }, // EMU
|
||||
400..=499 => EnemySpec { symbol: 'O', min_damage: 8.0, damage_range: 17.0, initial_health: 150.0, move_delay: 100, attack_cooldown: 10, ranged: false, drops: vec![], movement: 1 }, // OGRE
|
||||
500..=599 => EnemySpec { symbol: 'R', min_damage: 5.0, damage_range: 5.0, initial_health: 15.0, move_delay: 40, attack_cooldown: 10, ranged: false, drops: vec![], movement: 1 }, // RAT
|
||||
600..=609 => EnemySpec { symbol: 'M' , min_damage: 20.0, damage_range: 10.0, initial_health: 150.0, move_delay: 70, attack_cooldown: 10, ranged: false, drops: vec![], movement: 1 }, // MOA
|
||||
610..=649 => EnemySpec { symbol: 'P', min_damage: 10.0, damage_range: 5.0, initial_health: 15.0, move_delay: 20, attack_cooldown: 10, ranged: true, drops: vec![], movement: 1 }, // PLATYPUS
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
@ -365,7 +369,8 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
|
||||
Collidable,
|
||||
DespawnRandomly(RANDOM_DESPAWN_INV_RATE),
|
||||
Energy { regeneration_rate: 1.0, current: 0.0, burst: 0.0 },
|
||||
Drops(spec.drops)
|
||||
Drops(spec.drops),
|
||||
Jump(spec.movement)
|
||||
));
|
||||
} else {
|
||||
buffer.spawn((
|
||||
@ -378,7 +383,8 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
|
||||
Collidable,
|
||||
DespawnRandomly(RANDOM_DESPAWN_INV_RATE),
|
||||
Energy { regeneration_rate: 1.0, current: 0.0, burst: 0.0 },
|
||||
Drops(spec.drops)
|
||||
Drops(spec.drops),
|
||||
Jump(spec.movement)
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -386,7 +392,7 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
|
||||
}
|
||||
|
||||
// Process enemy motion and ranged attacks
|
||||
for (entity, (pos, ranged, energy)) in state.world.query::<hecs::With<(&Position, Option<&mut RangedAttack>, Option<&mut Energy>), &Enemy>>().iter() {
|
||||
for (entity, (pos, ranged, energy, jump)) in state.world.query::<hecs::With<(&Position, Option<&mut RangedAttack>, Option<&mut Energy>, Option<&Jump>), &Enemy>>().iter() {
|
||||
let pos = pos.head();
|
||||
|
||||
for direction in DIRECTIONS.iter() {
|
||||
@ -433,8 +439,18 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
|
||||
));
|
||||
}
|
||||
} else {
|
||||
let direction = DIRECTIONS.iter().min_by_key(|dir| hex_distance(pos + **dir, target_pos)).unwrap();
|
||||
buffer.insert_one(entity, MovingInto(pos + *direction));
|
||||
let direction = *DIRECTIONS.iter().min_by_key(|dir| hex_distance(pos + **dir, target_pos)).unwrap();
|
||||
let max_movement_distance = jump.map(|j| j.0).unwrap_or(1);
|
||||
let mut best_scale = 1;
|
||||
let mut best_distance = hex_distance(pos + direction, target_pos);
|
||||
for i in 1..=max_movement_distance {
|
||||
let new_distance = hex_distance(pos + direction * i, target_pos);
|
||||
if new_distance < best_distance {
|
||||
best_distance = new_distance;
|
||||
best_scale = i;
|
||||
}
|
||||
}
|
||||
buffer.insert_one(entity, MovingInto(pos + direction * best_scale));
|
||||
}
|
||||
} else {
|
||||
// wander randomly (ethical)
|
||||
@ -528,6 +544,8 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
|
||||
// Process motion and attacks
|
||||
for (entity, (current_pos, MovingInto(target_pos), damage, mut energy, move_cost, despawn_on_impact)) in state.world.query::<(&mut Position, &MovingInto, Option<&mut Attack>, Option<&mut Energy>, Option<&MoveCost>, Option<&DespawnOnImpact>)>().iter() {
|
||||
let mut move_cost = move_cost.map(|x| x.0.sample()).unwrap_or(0.0);
|
||||
|
||||
move_cost *= (hex_distance(*target_pos, current_pos.head()) as f32).powf(0.5);
|
||||
|
||||
for tile in current_pos.0.iter() {
|
||||
// TODO: perhaps large enemies should not be exponentially more vulnerable to environmental hazards
|
||||
@ -703,15 +721,30 @@ fn add_new_player(state: &mut GameState) -> Result<Entity> {
|
||||
)))
|
||||
}
|
||||
|
||||
async fn load_world() -> Result<worldgen::GeneratedWorld> {
|
||||
let data = tokio::fs::read("world.bin").await?;
|
||||
Ok(bincode::serde::decode_from_slice(&data, bincode::config::standard())?.0)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let addr = std::env::args().nth(1).unwrap_or_else(|| "0.0.0.0:8080".to_string());
|
||||
|
||||
let world = match load_world().await {
|
||||
Ok(world) => world,
|
||||
Err(e) => {
|
||||
println!("Failed to load world, generating new one: {:?}", e);
|
||||
let world = worldgen::generate_world();
|
||||
tokio::fs::write("world.bin", bincode::serde::encode_to_vec(&world, bincode::config::standard())?).await?;
|
||||
world
|
||||
}
|
||||
};
|
||||
|
||||
let state = Arc::new(Mutex::new(GameState {
|
||||
world: World::new(),
|
||||
clients: Slab::new(),
|
||||
ticks: 0,
|
||||
map: worldgen::generate_world()
|
||||
map: world
|
||||
}));
|
||||
|
||||
let try_socket = TcpListener::bind(&addr).await;
|
||||
|
@ -10,9 +10,13 @@ pub fn to_cubic(p0: Coord) -> CubicCoord {
|
||||
CubicCoord::new(p0.x, p0.y, -p0.x - p0.y)
|
||||
}
|
||||
|
||||
pub fn vec_length(ax_dist: CoordVec) -> i64 {
|
||||
(ax_dist.x.abs() + ax_dist.y.abs() + (ax_dist.x + ax_dist.y).abs()) / 2
|
||||
}
|
||||
|
||||
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
|
||||
vec_length(ax_dist)
|
||||
}
|
||||
|
||||
pub fn on_axis(p: CoordVec) -> bool {
|
||||
|
@ -1,8 +1,7 @@
|
||||
use std::{cmp::Ordering, collections::{hash_map::Entry, BinaryHeap, HashMap, HashSet}, hash::{Hash, Hasher}, ops::{Index, IndexMut}};
|
||||
use std::{cmp::Ordering, collections::{hash_map::Entry, BinaryHeap, HashMap, HashSet, VecDeque}, hash::{Hash, Hasher}, ops::{Index, IndexMut}};
|
||||
|
||||
use noise_functions::Sample3;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::map::*;
|
||||
|
||||
pub const WORLD_RADIUS: i64 = 1024;
|
||||
@ -300,13 +299,56 @@ pub fn compute_humidity(distances: Map<f32>, heightmap: &Map<f32>) -> Map<f32> {
|
||||
}
|
||||
|
||||
const SEA_LEVEL: f32 = -0.8;
|
||||
const EROSION: f32 = 0.05;
|
||||
const EROSION: f32 = 0.09;
|
||||
const EROSION_EXPONENT: f32 = 1.5;
|
||||
|
||||
fn floodfill(src: Coord, all: &HashSet<Coord>) -> HashSet<Coord> {
|
||||
let mut out = HashSet::new();
|
||||
let mut queue = VecDeque::new();
|
||||
queue.push_back(src);
|
||||
out.insert(src);
|
||||
while let Some(coord) = queue.pop_front() {
|
||||
for offset in DIRECTIONS {
|
||||
let neighbor = coord + *offset;
|
||||
if all.contains(&neighbor) && !out.contains(&neighbor) {
|
||||
queue.push_back(neighbor);
|
||||
out.insert(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
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<_>>();
|
||||
let mut remainder = sinks.clone();
|
||||
let sea = floodfill(Coord::new(0, WORLD_RADIUS), &sinks);
|
||||
|
||||
for s in sea.iter() {
|
||||
remainder.remove(&s);
|
||||
}
|
||||
|
||||
let mut lakes = vec![];
|
||||
loop {
|
||||
let next = remainder.iter().next();
|
||||
match next {
|
||||
Some(s) => {
|
||||
let lake = floodfill(*s, &remainder);
|
||||
for l in lake.iter() {
|
||||
remainder.remove(l);
|
||||
}
|
||||
lakes.push(lake);
|
||||
},
|
||||
None => break
|
||||
}
|
||||
}
|
||||
|
||||
for sink in sinks.iter() {
|
||||
watermap[*sink] = 10.0;
|
||||
}
|
||||
|
||||
for source in sources.iter() {
|
||||
let heightmap_ = &*heightmap;
|
||||
@ -316,7 +358,7 @@ pub fn simulate_water(heightmap: &mut Map<f32>) -> Map<f32> {
|
||||
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)))
|
||||
Some((neighbor, factor * (heightmap_[neighbor] - heightmap_[c] + 0.0001).max(0.0)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -325,7 +367,13 @@ pub fn simulate_water(heightmap: &mut Map<f32>) -> Map<f32> {
|
||||
let heuristic = |c: Coord| {
|
||||
(heightmap[c] * 0.00).max(0.0)
|
||||
};
|
||||
let path = astar(*source, |c| sinks.contains(&c), heuristic, get_neighbours);
|
||||
let mut path = astar(*source, |c| sinks.contains(&c), heuristic, get_neighbours);
|
||||
|
||||
let end = path.last().unwrap();
|
||||
if !sea.contains(end) {
|
||||
// route lake to sea
|
||||
path.extend(astar(*end, |c| sea.contains(&c), heuristic, get_neighbours));
|
||||
}
|
||||
|
||||
for point in path {
|
||||
let water_range_raw = watermap[point] * 1.0 - heightmap[point];
|
||||
@ -338,24 +386,20 @@ pub fn simulate_water(heightmap: &mut Map<f32>) -> Map<f32> {
|
||||
watermap[point + nearby] = watermap[point + nearby].min(3.0);
|
||||
}
|
||||
|
||||
let erosion_range_raw = water_range_raw * 2.0 + 2.0;
|
||||
let erosion_range_raw = (water_range_raw * 2.0 + 2.0).powf(EROSION_EXPONENT);
|
||||
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] -= EROSION * watermap[point] / (this_range as f32) / erosion_range_raw.max(1.0).powf(EROSION_EXPONENT);
|
||||
heightmap[point + nearby] = heightmap[point + nearby].max(SEA_LEVEL);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for sink in sinks {
|
||||
watermap[sink] = 10.0;
|
||||
}
|
||||
|
||||
watermap
|
||||
}
|
||||
|
||||
|
@ -36,10 +36,11 @@ fn main() -> Result<()> {
|
||||
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 height = ((*value + 1.0) * 127.5) as u8;
|
||||
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.put_pixel(col as u32, row as u32, Rgb::from([contour, height, (water.min(1.0) * 255.0) as u8]));
|
||||
}
|
||||
|
||||
image.save("./out.png")?;
|
||||
|
Loading…
Reference in New Issue
Block a user