1
0
mirror of https://github.com/osmarks/ewo3.git synced 2026-04-20 22:11:24 +00:00

current prototype

This commit is contained in:
osmarks
2026-02-24 11:44:34 +00:00
parent 6ae79eb758
commit df71026ef1
8 changed files with 433 additions and 220 deletions

1
Cargo.lock generated
View File

@@ -236,6 +236,7 @@ dependencies = [
"serde",
"serde_json",
"slab",
"smallvec",
"tokio",
"tokio-macros 0.2.6",
"tokio-tungstenite",

View File

@@ -22,10 +22,11 @@ indexmap = "2"
image = { version = "0.25", default-features = false, features = ["png"] }
rayon = "1"
bincode = { version = "2.0.0-rc.3", features = ["serde"] }
smallvec = "1"
[[bin]]
name = "worldgen"
path = "src/worldgen_test.rs"
[target.x86_64-unknown-linux-gnu]
rustflags = ["-Ctarget-cpu=native"]
[profile.release]
debug = true

View File

@@ -4,7 +4,7 @@
\:global(html)
scrollbar-color: black lightgray
\:global(body)
font-family: "Fira Sans", "Noto Sans", "Segoe UI", Verdana, sans-serif
font-weight: 300
@@ -98,7 +98,7 @@
let ws
const connect = () => {
ws = new WebSocket(window.location.protocol === "https:" ? "wss://ewo.osmarks.net/" : "ws://localhost:8080/")
ws = new WebSocket(window.location.protocol === "https:" ? "wss://ewo.osmarks.net/" : "ws://localhost:8011/")
ws.addEventListener("message", ev => {
const data = JSON.parse(ev.data)

View File

@@ -1,16 +1,18 @@
use hecs::{CommandBuffer, Entity, World};
use hecs::{CommandBuffer, Entity, With, World};
use futures_util::{stream::TryStreamExt, SinkExt, StreamExt};
use indexmap::IndexMap;
use smallvec::{smallvec, SmallVec};
use tokio::net::{TcpListener, TcpStream};
use tokio_tungstenite::tungstenite::protocol::Message;
use tokio::sync::{mpsc, Mutex};
use anyhow::{Result, Context, anyhow};
use std::{collections::{hash_map::Entry, HashMap, HashSet, VecDeque}, convert::TryFrom, hash::{Hash, Hasher}, net::SocketAddr, sync::Arc, time::Duration};
use std::{convert::TryFrom, hash::{Hash, Hasher}, net::SocketAddr, ops::DerefMut, sync::Arc, time::Duration};
use slab::Slab;
use serde::{Serialize, Deserialize};
pub mod worldgen;
pub mod map;
pub mod plant;
use map::*;
@@ -85,7 +87,8 @@ struct GameState {
world: World,
clients: Slab<Client>,
ticks: u64,
map: worldgen::GeneratedWorld
map: worldgen::GeneratedWorld,
positions: PositionIndex
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -112,19 +115,79 @@ impl Item {
}
}
struct PositionIndex {
particles: Map<Option<Entity>>,
entities: Map<Option<Entity>>,
terrain: Map<Option<Entity>>
}
impl PositionIndex {
fn new(radius: i32) -> Self {
Self {
particles: Map::new(radius, None),
entities: Map::new(radius, None),
terrain: Map::new(radius, None)
}
}
}
#[derive(Debug, Clone)]
struct PlayerCharacter;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
enum MapLayer {
Particles,
Entities,
Terrain
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Position(VecDeque<Coord>);
struct Position {
layer: MapLayer,
coords: SmallVec<[Coord; 2]>
}
impl Position {
fn head(&self) -> Coord {
*self.0.front().unwrap()
self.coords[0]
}
fn single_tile(c: Coord) -> Self {
Self(VecDeque::from([c]))
fn single_tile(c: Coord, layer: MapLayer) -> Self {
Self {
layer,
coords: smallvec![c]
}
}
fn iter_coords(&self) -> impl Iterator<Item=Coord> + '_ {
self.coords.iter().copied()
}
fn record_for(&mut self, index: &mut PositionIndex, entity: Option<Entity>) {
let target_layer = match self.layer {
MapLayer::Particles => &mut index.particles,
MapLayer::Entities => &mut index.entities,
MapLayer::Terrain => &mut index.terrain,
};
for coord in self.coords.iter() {
target_layer[*coord] = entity;
}
}
// return value is whether it is now dead/positionless
fn remove_coord(&mut self, coord: Coord, index: &mut PositionIndex, entity: Entity) -> bool {
self.record_for(index, None);
self.coords.retain(|x| *x != coord);
self.record_for(index, Some(entity));
self.coords.len() > 0
}
fn move_into(&mut self, coord: Coord, index: &mut PositionIndex, entity: Entity) -> Coord {
self.record_for(index, None);
let fst = self.coords.remove(0);
self.coords.push(coord);
self.record_for(index, Some(entity));
fst
}
}
@@ -141,6 +204,9 @@ impl Health {
}
}
#[derive(Debug, Clone)]
struct ShrinkOnDeath;
#[derive(Debug, Clone)]
struct Render(char);
@@ -165,15 +231,9 @@ struct Enemy;
#[derive(Debug, Clone)]
struct MoveCost(StochasticNumber);
#[derive(Debug, Clone)]
struct Collidable;
#[derive(Debug, Clone)]
struct Velocity(CoordVec);
#[derive(Debug, Clone)]
struct Terrain;
#[derive(Debug, Clone)]
struct Obstruction { entry_multiplier: f32, exit_multiplier: f32 }
@@ -203,6 +263,15 @@ struct DespawnOnImpact;
#[derive(Debug, Clone)]
struct Inventory(indexmap::IndexMap<Item, u64>);
#[derive(Debug, Clone)]
struct Plant(plant::Genome);
#[derive(Debug, Clone)]
struct NewlyAdded; // ugly hack to work around ECS deficiencies
#[derive(Debug, Clone)]
struct BlocksEnemySpawn;
impl Inventory {
fn add(&mut self, item: Item, qty: u64) {
*self.0.entry(item).or_default() += qty;
@@ -253,7 +322,7 @@ struct EnemySpec {
}
impl EnemySpec {
// Numbers ported from original EWO. Fudge constants added elsewhere.
// 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![], movement: 1 }, // IBIS
@@ -275,21 +344,10 @@ fn rng_from_hash<H: Hash>(x: H) -> fastrand::Rng {
fastrand::Rng::with_seed(h.finish())
}
fn consume_energy_if_available(e: &mut Option<&mut Energy>, cost: f32) -> bool {
fn consume_energy_if_available<R: DerefMut<Target=Energy>>(e: &mut Option<R>, cost: f32) -> bool {
e.is_none() || e.as_mut().unwrap().try_consume(cost)
}
// Box-Muller transform
fn normal() -> f32 {
let u = fastrand::f32();
let v = fastrand::f32();
(v * std::f32::consts::TAU).cos() * (-2.0 * u.ln()).sqrt()
}
fn normal_scaled(mu: f32, sigma: f32) -> f32 {
normal() * sigma + mu
}
fn triangle_distribution(min: f32, max: f32, mode: f32) -> f32 {
let sample = fastrand::f32();
let threshold = (mode - min) / (max - min);
@@ -324,23 +382,15 @@ impl StochasticNumber {
}
async fn game_tick(state: &mut GameState) -> Result<()> {
let mut terrain_positions = HashMap::new();
let mut positions = HashMap::new();
for (entity, pos) in state.world.query_mut::<hecs::With<&Position, &Collidable>>() {
for subpos in pos.0.iter() {
positions.insert(*subpos, entity);
}
}
for (entity, pos) in state.world.query_mut::<hecs::With<&Position, &Terrain>>() {
for subpos in pos.0.iter() {
terrain_positions.insert(*subpos, entity);
}
}
let mut buffer = hecs::CommandBuffer::new();
for (entity, position) in state.world.query_mut::<With<&mut Position, &NewlyAdded>>() {
position.record_for(&mut state.positions, Some(entity));
buffer.remove_one::<NewlyAdded>(entity);
}
buffer.run_on(&mut state.world);
// Spawn enemies
for (_entity, (pos, EnemyTarget { spawn_range, spawn_density, spawn_rate_inv, .. })) in state.world.query::<(&Position, &EnemyTarget)>().iter() {
let pos = pos.head();
@@ -349,9 +399,11 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
let mut newpos = pos + sample_range(*spawn_range.end());
let mut occupied = false;
for _ in 0..(c as f32 / spawn_density * 0.005).ceil() as usize {
if positions.contains_key(&newpos) {
occupied = true;
break;
if let Some(entity) = state.positions.entities[newpos] {
if state.world.get::<&BlocksEnemySpawn>(entity).is_ok() {
occupied = true;
break;
}
}
newpos = pos + sample_range(*spawn_range.end());
}
@@ -364,29 +416,31 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
Health(spec.initial_health, spec.initial_health),
Enemy,
RangedAttack { damage: StochasticNumber::triangle_from_min_range(spec.min_damage, spec.damage_range), energy: spec.attack_cooldown as f32, range: 4 },
Position::single_tile(newpos),
Position::single_tile(newpos, MapLayer::Entities),
MoveCost(StochasticNumber::Triangle { min: 0.0, max: 2.0 * spec.move_delay as f32 / 3.0, mode: spec.move_delay as f32 / 3.0 }),
Collidable,
DespawnRandomly(RANDOM_DESPAWN_INV_RATE),
Energy { regeneration_rate: 1.0, current: 0.0, burst: 0.0 },
Drops(spec.drops),
Jump(spec.movement)
));
Jump(spec.movement),
NewlyAdded,
BlocksEnemySpawn
))
} else {
buffer.spawn((
Render(spec.symbol),
Health(spec.initial_health, spec.initial_health),
Enemy,
Attack { damage: StochasticNumber::triangle_from_min_range(spec.min_damage, spec.damage_range), energy: spec.attack_cooldown as f32 },
Position::single_tile(newpos),
Position::single_tile(newpos, MapLayer::Entities),
MoveCost(StochasticNumber::Triangle { min: 0.0, max: 2.0 * spec.move_delay as f32 / 3.0, mode: spec.move_delay as f32 / 3.0 }),
Collidable,
DespawnRandomly(RANDOM_DESPAWN_INV_RATE),
Energy { regeneration_rate: 1.0, current: 0.0, burst: 0.0 },
Drops(spec.drops),
Jump(spec.movement)
));
}
Jump(spec.movement),
NewlyAdded,
BlocksEnemySpawn
))
};
}
}
}
@@ -396,7 +450,7 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
let pos = pos.head();
for direction in DIRECTIONS.iter() {
if let Some(target) = positions.get(&(pos + *direction)) {
if let Some(target) = &state.positions.entities[pos + *direction] {
if let Ok(_) = state.world.get::<&EnemyTarget>(*target) {
buffer.insert_one(entity, MovingInto(pos + *direction));
continue;
@@ -434,8 +488,10 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
Enemy,
Attack { damage: ranged_attack.damage, energy: 0.0 },
Velocity(atk_dir),
Position::single_tile(pos),
DespawnOnTick(state.ticks.wrapping_add(ranged_attack.range))
Position::single_tile(pos, MapLayer::Particles),
DespawnOnTick(state.ticks.wrapping_add(ranged_attack.range)),
DespawnOnImpact,
NewlyAdded
));
}
} else {
@@ -483,13 +539,14 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
Input::DownRight => next_movement = CoordVec::new(0, 1),
Input::DownLeft => next_movement = CoordVec::new(-1, 1),
Input::Dig => {
if terrain_positions.get(&position).is_none() && energy.try_consume(5.0) {
// Dig a hole
if state.positions.terrain[position].is_none() && energy.try_consume(5.0) {
buffer.spawn((
Terrain,
Render('_'),
Obstruction { entry_multiplier: 5.0, exit_multiplier: 5.0 },
DespawnOnTick(state.ticks.wrapping_add(StochasticNumber::triangle_from_min_range(5000.0, 5000.0).sample().round() as u64)),
Position::single_tile(position)
Position::single_tile(position, MapLayer::Terrain),
NewlyAdded
));
inventory.add(Item::Dirt, StochasticNumber::triangle_from_min_range(1.0, 3.0).sample_rounded());
}
@@ -498,7 +555,7 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
Err(e) => return Err(e.into())
}
}
let target = position + next_movement;
if state.map.get_terrain(target).entry_cost().is_some() && target != position {
buffer.insert_one(client.entity, MovingInto(target));
@@ -507,13 +564,15 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
buffer.run_on(&mut state.world);
let mut despawn_buffer = HashSet::new();
let mut despawn_buffer = Vec::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);
// The final position argument is in some sense redundant but exists to satisfy dynamic borrow checking.
let kill = |buffer: &mut CommandBuffer, despawn_buffer: &mut Vec<(Entity, Position)>, state: &GameState, entity: Entity, killer: Option<Entity>| {
let position = (*state.world.get::<&Position>(entity).unwrap()).clone();
let position_head = position.head();
despawn_buffer.push((entity, position));
buffer.despawn(entity);
let mut materialized_drops = Inventory::empty();
if let Ok(drops) = state.world.get::<&Drops>(entity) {
@@ -534,22 +593,25 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
} else { false };
if !killer_consumed_items && !materialized_drops.is_empty() {
buffer.spawn((
Position::single_tile(position),
Position::single_tile(position_head, MapLayer::Entities),
Render('☒'),
materialized_drops
materialized_drops,
NewlyAdded,
Health(10.0, 10.0)
));
}
};
let mut about_to_move = Vec::new();
// 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() {
for (entity, (current_pos, MovingInto(target_pos), damage, mut energy, move_cost, despawn_on_impact)) in state.world.query::<(&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() {
for tile in current_pos.iter_coords() {
// TODO: perhaps large enemies should not be exponentially more vulnerable to environmental hazards
if let Some(current_terrain) = terrain_positions.get(tile) {
if let Some(current_terrain) = &state.positions.terrain[tile] {
if let Ok(obstruction) = state.world.get::<&Obstruction>(*current_terrain) {
move_cost *= obstruction.exit_multiplier;
}
@@ -557,7 +619,7 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
}
// TODO: attacks into obstructions are still cheap; is this desirable?
if let Some(target_terrain) = terrain_positions.get(target_pos) {
if let Some(target_terrain) = &state.positions.terrain[*target_pos] {
if let Ok(obstruction) = state.world.get::<&Obstruction>(*target_terrain) {
move_cost *= obstruction.entry_multiplier;
}
@@ -565,9 +627,9 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
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();
let can_move = match &state.positions.entities[*target_pos] {
Some(target_entity) => {
let target_entity = target_entity.clone();
if let Ok(mut x) = state.world.get::<&mut Health>(target_entity) {
match damage {
Some(Attack { damage, energy: energy_cost }) => {
@@ -578,32 +640,50 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
_ => ()
}
if despawn_on_impact.is_some() {
kill(&mut buffer, &mut despawn_buffer, &state, entity, Some(target_entity), Some(*target_pos));
kill(&mut buffer, &mut despawn_buffer, &state, entity, Some(target_entity));
}
if x.0 < 0.0 {
kill(&mut buffer, &mut despawn_buffer, &state, target_entity, Some(entity), Some(*target_pos));
Some(Entry::Occupied(o))
// TODO: this may be totally broken
if state.world.get::<&ShrinkOnDeath>(target_entity).is_ok() {
let mut positions = state.world.get::<&mut Position>(target_entity).unwrap();
if positions.remove_coord(*target_pos, &mut state.positions, target_entity) {
std::mem::drop(positions);
kill(&mut buffer, &mut despawn_buffer, &state, target_entity, Some(entity));
} else {
x.0 = x.1; // reset health
}
} else {
kill(&mut buffer, &mut despawn_buffer, &state, target_entity, Some(entity));
}
true // murdered to death; space is now open
} else {
None
false // still alive; cannot move there
}
} else {
None // no "on pickup" exists; emulated with health 0
false // if no health, cannot be destroyed
}
},
Entry::Vacant(v) => Some(Entry::Vacant(v))
None => true // empty, can move
};
if let Some(entry) = entry {
// TODO: perhaps this should be applied to attacks too?
if consume_energy_if_available(&mut energy, move_cost) {
*entry.or_insert(entity) = entity;
positions.remove(&current_pos.0.pop_back().unwrap());
current_pos.0.push_front(*target_pos);
}
if can_move {
about_to_move.push((entity, *target_pos, move_cost));
}
}
buffer.remove_one::<MovingInto>(entity);
}
for (entity, target_pos, move_cost) in about_to_move.drain(..) {
// TODO: perhaps this should be applied to attacks too?
let mut energy = state.world.get::<&mut Energy>(entity).ok();
let mut current_pos = state.world.get::<&mut Position>(entity).unwrap() ;
if consume_energy_if_available(&mut energy, move_cost) {
state.positions.entities[target_pos] = Some(entity.clone());
let tail_pos = current_pos.move_into(target_pos, &mut state.positions, entity);
state.positions.entities[tail_pos] = None;
}
}
buffer.run_on(&mut state.world);
for (_entity, energy) in state.world.query_mut::<&mut Energy>() {
@@ -613,27 +693,32 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
// Process transient entities
for (entity, tick) in state.world.query::<&DespawnOnTick>().iter() {
if state.ticks == tick.0 {
kill(&mut buffer, &mut despawn_buffer, &state, entity, None, None);
kill(&mut buffer, &mut despawn_buffer, &state, entity, None);
}
}
for (entity, DespawnRandomly(inv_rate)) in state.world.query::<&DespawnRandomly>().iter() {
if fastrand::u64(0..*inv_rate) == 0 {
kill(&mut buffer, &mut despawn_buffer, &state, entity, None, None);
kill(&mut buffer, &mut despawn_buffer, &state, entity, None);
}
}
buffer.run_on(&mut state.world);
let mut delete = vec![];
for (position, entity) in positions.iter() {
if despawn_buffer.contains(entity) {
delete.push(*position);
for (entity, position) in despawn_buffer.drain(..) {
for coord in position.iter_coords() {
// TODO: fix
if state.positions.particles[coord] == Some(entity) {
state.positions.particles[coord] = None;
}
if state.positions.entities[coord] == Some(entity) {
state.positions.entities[coord] = None;
}
if state.positions.terrain[coord] == Some(entity) {
state.positions.terrain[coord] = None;
}
}
}
for position in delete {
positions.remove(&position);
}
// Send views to clients
// TODO: terrain layer below others
@@ -646,13 +731,13 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
let pos = pos + offset;
let mut rng = rng_from_hash(pos);
if let Some(entity) = positions.get(&pos) {
if let Some(entity) = &state.positions.particles[pos].or(state.positions.entities[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) {
} else if let Some(entity) = &state.positions.terrain[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() {
@@ -704,10 +789,9 @@ fn add_new_player(state: &mut GameState) -> Result<Entity> {
}
};
Ok(state.world.spawn((
Position::single_tile(pos),
Position::single_tile(pos, MapLayer::Entities),
PlayerCharacter,
Render(random_identifier()),
Collidable,
Attack { damage: StochasticNumber::Triangle { min: 20.0, max: 60.0, mode: 20.0 }, energy: 5.0 },
Health(128.0, 128.0),
EnemyTarget {
@@ -717,7 +801,9 @@ fn add_new_player(state: &mut GameState) -> Result<Entity> {
aggression_range: 5
},
Energy { current: 0.0, regeneration_rate: 1.0, burst: 5.0 },
Inventory::empty()
Inventory::empty(),
NewlyAdded,
BlocksEnemySpawn
)))
}
@@ -728,7 +814,7 @@ async fn load_world() -> Result<worldgen::GeneratedWorld> {
#[tokio::main]
async fn main() -> Result<()> {
let addr = std::env::args().nth(1).unwrap_or_else(|| "0.0.0.0:8080".to_string());
let addr = std::env::args().nth(1).unwrap_or_else(|| "0.0.0.0:8011".to_string());
let world = match load_world().await {
Ok(world) => world,
@@ -744,9 +830,27 @@ async fn main() -> Result<()> {
world: World::new(),
clients: Slab::new(),
ticks: 0,
positions: PositionIndex::new(world.radius),
map: world
}));
{
let mut state = state.lock().await;
let count = count_hexes(state.map.radius() / 5);
let mut batch = Vec::with_capacity(count as usize);
for (distance, offset) in hex_range(state.map.radius() / 5) {
batch.push((
Position::single_tile(Coord::origin() + offset * 5, MapLayer::Entities),
Render('+'),
Health(100.0, 100.0),
//ShrinkOnDeath,
Plant(plant::Genome::random()),
NewlyAdded
));
}
state.world.spawn_batch(batch);
}
let try_socket = TcpListener::bind(&addr).await;
let listener = try_socket.expect("Failed to bind");
println!("Listening on: {}", addr);
@@ -786,9 +890,16 @@ async fn main() -> Result<()> {
println!("conn result {:?}", handle_connection(stream, addr, frames_rx, inputs_tx).await);
let mut state = state_.lock().await;
state.clients.remove(id);
let mut pos = match state.world.get::<&Position>(entity) {
Err(_) => return,
Ok(p) => {
(*p).clone()
}
};
pos.record_for(&mut state.positions, None);
let _ = state.world.despawn(entity);
});
}
Ok(())
}
}

View File

@@ -1,4 +1,6 @@
use euclid::{Point3D, Point2D, Vector2D};
use serde::{Deserialize, Serialize};
use std::ops::{Index, IndexMut};
pub struct AxialWorldSpace;
pub struct CubicWorldSpace;
@@ -56,4 +58,112 @@ pub fn hex_range(range: i32) -> impl Iterator<Item=(i32, CoordVec)> {
pub fn count_hexes(x: i32) -> i32 {
x*(x+1)*3+1
}
struct CoordsIndexIterator {
radius: i32,
index: usize,
r: i32,
q: i32,
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: i32
}
impl<T> Map<T> {
pub fn new(radius: i32, 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: i32) -> 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)
}
pub 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
}
pub fn in_range(&self, coord: Coord) -> bool {
hex_distance(coord, Coord::origin()) <= self.radius
}
pub 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]
}
}

96
src/plant.rs Normal file
View File

@@ -0,0 +1,96 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CropType {
Grass,
EucalyptusTree,
BushTomato,
GoldenWattleTree
}
#[derive(Debug, Clone, Copy)]
pub struct Genome {
crop_type: CropType,
// polygenic traits; parameterized as N(0,1)
growth_rate: f32,
nitrogen_fixation_rate: f32,
optimal_water_level: f32,
optimal_temperature: f32,
reproduction_rate: f32,
temperature_tolerance: f32,
water_tolerance: f32,
max_size: f32
// TODO color trait
}
fn sigmoid(x: f32) -> f32 {
1.0 / (1.0 + (-x).exp())
}
// Box-Muller transform
fn normal() -> f32 {
let u = fastrand::f32();
let v = fastrand::f32();
(v * std::f32::consts::TAU).cos() * (-2.0 * u.ln()).sqrt()
}
fn normal_scaled(mu: f32, sigma: f32) -> f32 {
normal() * sigma + mu
}
impl Genome {
pub fn effective_growth_rate(&self, water: f32, temperature: f32) -> f32 {
let water_diff = (water - self.optimal_water_level).abs();
let temperature_diff = (temperature - self.optimal_temperature).abs();
1.5f32.powf(self.growth_rate)
- self.reproduction_rate * 0.1 // faster reproduction trades off slightly against growth
- self.nitrogen_fixation_rate.max(0.0) * 0.16 // same for nitrogen fixation
- (water_diff - sigmoid(self.water_tolerance)).max(0.0) // penalize plants when far from optimal environmental range
- (temperature_diff - sigmoid(self.temperature_tolerance)).max(0.0) // same for temperature
- self.water_tolerance * 0.2
- self.temperature_tolerance * 0.2
}
pub fn random() -> Genome {
let crop_type = match fastrand::usize(0..4) {
0 => CropType::Grass,
1 => CropType::EucalyptusTree,
2 => CropType::BushTomato,
3 => CropType::GoldenWattleTree,
_ => unreachable!()
};
let (nitrogen_fixation_rate, optimal_water_level, optimal_temperature, max_size) = match crop_type {
CropType::Grass => (-10.0, 0.0, 0.0, 0.0),
CropType::EucalyptusTree => (-10.0, 2.0, 1.0, 5.0),
CropType::BushTomato => (-10.0, -1.0, 1.5, 1.0),
CropType::GoldenWattleTree => (2.0, 1.5, 1.0, 3.0),
};
Genome {
crop_type: crop_type,
growth_rate: normal(),
nitrogen_fixation_rate,
optimal_water_level,
optimal_temperature,
reproduction_rate: normal(),
temperature_tolerance: normal(),
water_tolerance: normal(),
max_size
}
}
pub fn hybridize(&self, other: &Genome) -> Option<Genome> {
if self.crop_type != other.crop_type { return None }
Some(Genome {
crop_type: self.crop_type,
growth_rate: (self.growth_rate + other.growth_rate) / 2.0 + normal_scaled(0.0, 0.1),
nitrogen_fixation_rate: (self.nitrogen_fixation_rate + other.nitrogen_fixation_rate) / 2.0 + normal_scaled(0.0, 0.03),
optimal_water_level: (self.optimal_water_level + other.optimal_water_level) / 2.0 + normal_scaled(0.0, 0.03),
optimal_temperature: (self.optimal_temperature + other.optimal_temperature) / 2.0 + normal_scaled(0.0, 0.03),
reproduction_rate: (self.reproduction_rate + other.reproduction_rate) / 2.0 + normal_scaled(0.0, 0.5),
temperature_tolerance: (self.temperature_tolerance + other.temperature_tolerance) / 2.0 + normal_scaled(0.0, 0.2),
water_tolerance: (self.water_tolerance + other.water_tolerance) / 2.0 + normal_scaled(0.0, 0.2),
max_size: (self.max_size + other.max_size) / 2.0 + normal_scaled(0.0, 0.3)
})
}
}

View File

@@ -1,4 +1,4 @@
use std::{cmp::Ordering, collections::{hash_map::Entry, BinaryHeap, HashMap, HashSet, VecDeque}, hash::{Hash, Hasher}, ops::{Index, IndexMut}};
use std::{cmp::Ordering, collections::{hash_map::Entry, BinaryHeap, HashMap, HashSet, VecDeque}, hash::{Hash, Hasher}};
use noise_functions::Sample3;
use serde::{Deserialize, Serialize};
@@ -48,114 +48,6 @@ pub fn generate_heights() -> Map<f32> {
raw
}
struct CoordsIndexIterator {
radius: i32,
index: usize,
r: i32,
q: i32,
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: i32
}
impl<T> Map<T> {
pub fn new(radius: i32, 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: i32) -> 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.
@@ -482,8 +374,8 @@ fn smooth(map: &Map<f32>, radius: i32) -> Map<f32> {
const BASE_TEMPERATURE: f32 = 30.0; // degrees
const HEIGHT_SCALE: f32 = 1e3; // unrealistic but makes world more interesting; m
const SEA_LEVEL_AIR_PRESSURE: f32 = 1013.0; // hPa
const PRESSURE_DROP_PER_METER: f32 = 0.001; // hPa m^-1
//const SEA_LEVEL_AIR_PRESSURE: f32 = 1013.0; // hPa
//const PRESSURE_DROP_PER_METER: f32 = 0.001; // hPa m^-1
const AIR_SPECIFIC_HEAT_CAPACITY: f32 = 1012.0; // J kg^-1 K^-1
const EARTH_GRAVITY: f32 = 9.81; // m s^-2
@@ -577,7 +469,8 @@ pub struct GeneratedWorld {
salt: Map<f32>,
atmo_humidity: Map<f32>,
temperature: Map<f32>,
soil_nutrients: Map<f32>
soil_nutrients: Map<f32>,
pub radius: i32
}
pub fn generate_world() -> GeneratedWorld {
@@ -609,6 +502,7 @@ pub fn generate_world() -> GeneratedWorld {
let soil_nutrients = soil_nutrients(&groundwater);
GeneratedWorld {
radius: heightmap.radius,
heightmap,
terrain,
groundwater,

View File

@@ -804,7 +804,7 @@
let inventory = [];
let ws;
const connect = () => {
ws = new WebSocket(window.location.protocol === "https:" ? "wss://ewo.osmarks.net/" : "ws://localhost:8080/");
ws = new WebSocket(window.location.protocol === "https:" ? "wss://ewo.osmarks.net/" : "ws://localhost:8011/");
ws.addEventListener("message", (ev) => {
const data = JSON.parse(ev.data);
if (data.Display) {