big birds!

This commit is contained in:
Isse 2023-03-09 10:20:05 +01:00 committed by Joshua Barretto
parent 259bb6fce4
commit dda1be58d4
16 changed files with 370 additions and 189 deletions

View File

@ -446,6 +446,12 @@ impl EntityInfo {
self.no_flee = true; self.no_flee = true;
self self
} }
#[must_use]
pub fn with_loadout(mut self, loadout: LoadoutBuilder) -> Self {
self.loadout = loadout;
self
}
} }
#[derive(Default)] #[derive(Default)]

View File

@ -1,5 +1,5 @@
use common::{ use common::{
comp::{Body, Controller, InputKind, Ori, Pos, Scale, Vel, ControlAction}, comp::{Body, ControlAction, Controller, InputKind, Ori, Pos, Scale, Vel},
link::Is, link::Is,
mounting::Mount, mounting::Mount,
uid::UidAllocator, uid::UidAllocator,

View File

@ -5,7 +5,8 @@ use common::{
grid::Grid, grid::Grid,
rtsim::{FactionId, RtSimController, SiteId, VehicleId}, rtsim::{FactionId, RtSimController, SiteId, VehicleId},
store::Id, store::Id,
uid::Uid, vol::RectVolSize, uid::Uid,
vol::RectVolSize,
}; };
use hashbrown::HashMap; use hashbrown::HashMap;
use rand::prelude::*; use rand::prelude::*;
@ -21,7 +22,11 @@ use std::{
}, },
}; };
use vek::*; use vek::*;
use world::{civ::Track, site::Site as WorldSite, util::{RandomPerm, LOCALITY}}; use world::{
civ::Track,
site::Site as WorldSite,
util::{RandomPerm, LOCALITY},
};
use super::Actor; use super::Actor;
@ -78,6 +83,7 @@ pub struct Npc {
pub seed: u32, pub seed: u32,
pub wpos: Vec3<f32>, pub wpos: Vec3<f32>,
pub body: comp::Body,
pub profession: Option<Profession>, pub profession: Option<Profession>,
pub home: Option<SiteId>, pub home: Option<SiteId>,
pub faction: Option<FactionId>, pub faction: Option<FactionId>,
@ -113,6 +119,7 @@ impl Clone for Npc {
home: self.home, home: self.home,
faction: self.faction, faction: self.faction,
riding: self.riding.clone(), riding: self.riding.clone(),
body: self.body,
// Not persisted // Not persisted
chunk_pos: None, chunk_pos: None,
current_site: Default::default(), current_site: Default::default(),
@ -124,13 +131,11 @@ impl Clone for Npc {
} }
impl Npc { impl Npc {
const PERM_BODY: u32 = 1; pub fn new(seed: u32, wpos: Vec3<f32>, body: comp::Body) -> Self {
const PERM_SPECIES: u32 = 0;
pub fn new(seed: u32, wpos: Vec3<f32>) -> Self {
Self { Self {
seed, seed,
wpos, wpos,
body,
profession: None, profession: None,
home: None, home: None,
faction: None, faction: None,
@ -154,21 +159,17 @@ impl Npc {
} }
pub fn steering(mut self, vehicle: impl Into<Option<VehicleId>>) -> Self { pub fn steering(mut self, vehicle: impl Into<Option<VehicleId>>) -> Self {
self.riding = vehicle.into().map(|vehicle| { self.riding = vehicle.into().map(|vehicle| Riding {
Riding {
vehicle, vehicle,
steering: true, steering: true,
}
}); });
self self
} }
pub fn riding(mut self, vehicle: impl Into<Option<VehicleId>>) -> Self { pub fn riding(mut self, vehicle: impl Into<Option<VehicleId>>) -> Self {
self.riding = vehicle.into().map(|vehicle| { self.riding = vehicle.into().map(|vehicle| Riding {
Riding {
vehicle, vehicle,
steering: false, steering: false,
}
}); });
self self
} }
@ -179,13 +180,6 @@ impl Npc {
} }
pub fn rng(&self, perm: u32) -> impl Rng { RandomPerm::new(self.seed.wrapping_add(perm)) } pub fn rng(&self, perm: u32) -> impl Rng { RandomPerm::new(self.seed.wrapping_add(perm)) }
pub fn get_body(&self) -> comp::Body {
let species = *(&comp::humanoid::ALL_SPECIES)
.choose(&mut self.rng(Self::PERM_SPECIES))
.unwrap();
comp::humanoid::Body::random_with(&mut self.rng(Self::PERM_BODY), &species).into()
}
} }
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
@ -204,7 +198,7 @@ pub enum VehicleKind {
pub struct Vehicle { pub struct Vehicle {
pub wpos: Vec3<f32>, pub wpos: Vec3<f32>,
pub kind: VehicleKind, pub body: comp::ship::Body,
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub chunk_pos: Option<Vec2<i32>>, pub chunk_pos: Option<Vec2<i32>>,
@ -216,41 +210,36 @@ pub struct Vehicle {
// TODO: Find a way to detect riders when the vehicle is loaded // TODO: Find a way to detect riders when the vehicle is loaded
pub riders: Vec<Actor>, pub riders: Vec<Actor>,
/// Whether the Vehicle is in simulated or loaded mode (when rtsim is run on the /// Whether the Vehicle is in simulated or loaded mode (when rtsim is run on
/// server, loaded corresponds to being within a loaded chunk). When in /// the server, loaded corresponds to being within a loaded chunk). When
/// loaded mode, the interactions of the Vehicle should not be simulated but /// in loaded mode, the interactions of the Vehicle should not be
/// should instead be derived from the game. /// simulated but should instead be derived from the game.
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub mode: SimulationMode, pub mode: SimulationMode,
} }
impl Vehicle { impl Vehicle {
pub fn new(wpos: Vec3<f32>, kind: VehicleKind) -> Self { pub fn new(wpos: Vec3<f32>, body: comp::ship::Body) -> Self {
Self { Self {
wpos, wpos,
kind, body,
chunk_pos: None, chunk_pos: None,
driver: None, driver: None,
riders: Vec::new(), riders: Vec::new(),
mode: SimulationMode::Simulated, mode: SimulationMode::Simulated,
} }
} }
pub fn get_ship(&self) -> comp::ship::Body {
match self.kind {
VehicleKind::Airship => comp::ship::Body::DefaultAirship,
VehicleKind::Boat => comp::ship::Body::Galleon,
}
}
pub fn get_body(&self) -> comp::Body { pub fn get_body(&self) -> comp::Body { comp::Body::Ship(self.body) }
comp::Body::Ship(self.get_ship())
}
/// Max speed in block/s /// Max speed in block/s
pub fn get_speed(&self) -> f32 { pub fn get_speed(&self) -> f32 {
match self.kind { match self.body {
VehicleKind::Airship => 15.0, comp::ship::Body::DefaultAirship => 15.0,
VehicleKind::Boat => 13.0, comp::ship::Body::AirBalloon => 16.0,
comp::ship::Body::SailBoat => 12.0,
comp::ship::Body::Galleon => 13.0,
_ => 10.0,
} }
} }
} }
@ -274,23 +263,25 @@ fn construct_npc_grid() -> Grid<GridCell> { Grid::new(Vec2::zero(), Default::def
impl Npcs { impl Npcs {
pub fn create_npc(&mut self, npc: Npc) -> NpcId { self.npcs.insert(npc) } pub fn create_npc(&mut self, npc: Npc) -> NpcId { self.npcs.insert(npc) }
pub fn create_vehicle(&mut self, vehicle: Vehicle) -> VehicleId { self.vehicles.insert(vehicle) } pub fn create_vehicle(&mut self, vehicle: Vehicle) -> VehicleId {
self.vehicles.insert(vehicle)
}
/// Queries nearby npcs, not garantueed to work if radius > 32.0 /// Queries nearby npcs, not garantueed to work if radius > 32.0
pub fn nearby(&self, wpos: Vec2<f32>, radius: f32) -> impl Iterator<Item = NpcId> + '_ { pub fn nearby(&self, wpos: Vec2<f32>, radius: f32) -> impl Iterator<Item = NpcId> + '_ {
let chunk_pos = wpos.as_::<i32>() / common::terrain::TerrainChunkSize::RECT_SIZE.as_::<i32>(); let chunk_pos =
wpos.as_::<i32>() / common::terrain::TerrainChunkSize::RECT_SIZE.as_::<i32>();
let r_sqr = radius * radius; let r_sqr = radius * radius;
LOCALITY LOCALITY
.into_iter() .into_iter()
.filter_map(move |neighbor| { .filter_map(move |neighbor| {
self self.npc_grid.get(chunk_pos + neighbor).map(|cell| {
.npc_grid cell.npcs
.get(chunk_pos + neighbor) .iter()
.map(|cell| {
cell.npcs.iter()
.copied() .copied()
.filter(|npc| { .filter(|npc| {
self.npcs.get(*npc) self.npcs
.get(*npc)
.map_or(false, |npc| npc.wpos.xy().distance_squared(wpos) < r_sqr) .map_or(false, |npc| npc.wpos.xy().distance_squared(wpos) < r_sqr)
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()

View File

@ -8,7 +8,11 @@ use crate::data::{
Data, Nature, Data, Nature,
}; };
use common::{ use common::{
grid::Grid, resources::TimeOfDay, rtsim::WorldSettings, terrain::TerrainChunkSize, comp::{self, Body},
grid::Grid,
resources::TimeOfDay,
rtsim::WorldSettings,
terrain::TerrainChunkSize,
vol::RectVolSize, vol::RectVolSize,
}; };
use hashbrown::HashMap; use hashbrown::HashMap;
@ -72,12 +76,12 @@ impl Data {
"Registering {} rtsim sites from world sites.", "Registering {} rtsim sites from world sites.",
this.sites.len() this.sites.len()
); );
/*
// Spawn some test entities at the sites // Spawn some test entities at the sites
for (site_id, site) in this.sites.iter() for (site_id, site) in this.sites.iter()
// TODO: Stupid // TODO: Stupid
.filter(|(_, site)| site.world_site.map_or(false, |ws| .filter(|(_, site)| site.world_site.map_or(false, |ws|
matches!(&index.sites.get(ws).kind, SiteKind::Refactor(_)))) .skip(1) matches!(&index.sites.get(ws).kind, SiteKind::Refactor(_))))
{ {
let Some(good_or_evil) = site let Some(good_or_evil) = site
.faction .faction
@ -91,10 +95,17 @@ impl Data {
.map(|e| e as f32 + 0.5) .map(|e| e as f32 + 0.5)
.with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
}; };
let random_humanoid = |rng: &mut SmallRng| {
let species = comp::humanoid::ALL_SPECIES.choose(&mut *rng).unwrap();
Body::Humanoid(comp::humanoid::Body::random_with(
rng,
species,
))
};
if good_or_evil { if good_or_evil {
for _ in 0..32 { for _ in 0..32 {
this.npcs.create_npc( this.npcs.create_npc(
Npc::new(rng.gen(), rand_wpos(&mut rng)) Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng))
.with_faction(site.faction) .with_faction(site.faction)
.with_home(site_id) .with_home(site_id)
.with_profession(match rng.gen_range(0..20) { .with_profession(match rng.gen_range(0..20) {
@ -112,7 +123,7 @@ impl Data {
} else { } else {
for _ in 0..15 { for _ in 0..15 {
this.npcs.create_npc( this.npcs.create_npc(
Npc::new(rng.gen(), rand_wpos(&mut rng)) Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng))
.with_faction(site.faction) .with_faction(site.faction)
.with_home(site_id) .with_home(site_id)
.with_profession(match rng.gen_range(0..20) { .with_profession(match rng.gen_range(0..20) {
@ -122,17 +133,54 @@ impl Data {
} }
} }
this.npcs.create_npc( this.npcs.create_npc(
Npc::new(rng.gen(), rand_wpos(&mut rng)) Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng))
.with_home(site_id) .with_home(site_id)
.with_profession(Profession::Merchant), .with_profession(Profession::Merchant),
); );
if rng.gen_bool(0.4) {
let wpos = rand_wpos(&mut rng) + Vec3::unit_z() * 50.0; let wpos = rand_wpos(&mut rng) + Vec3::unit_z() * 50.0;
let vehicle_id = this.npcs.create_vehicle(Vehicle::new(wpos, VehicleKind::Airship)); let vehicle_id = this
.npcs
.create_vehicle(Vehicle::new(wpos, comp::body::ship::Body::DefaultAirship));
this.npcs.create_npc(Npc::new(rng.gen(), wpos).with_home(site_id).with_profession(Profession::Captain).steering(vehicle_id)); this.npcs.create_npc(
Npc::new(rng.gen(), wpos, random_humanoid(&mut rng))
.with_home(site_id)
.with_profession(Profession::Captain)
.steering(vehicle_id),
);
} }
}
*/
for (site_id, site) in this.sites.iter()
// TODO: Stupid
.filter(|(_, site)| site.world_site.map_or(false, |ws|
matches!(&index.sites.get(ws).kind, SiteKind::Dungeon(_))))
{
let rand_wpos = |rng: &mut SmallRng| {
let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10));
wpos2d
.map(|e| e as f32 + 0.5)
.with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
};
let species = [
comp::body::bird_large::Species::Phoenix,
comp::body::bird_large::Species::Cockatrice,
comp::body::bird_large::Species::Roc,
]
.choose(&mut rng)
.unwrap();
this.npcs.create_npc(
Npc::new(
rng.gen(),
rand_wpos(&mut rng),
Body::BirdLarge(comp::body::bird_large::Body::random_with(&mut rng, species)),
)
.with_home(site_id),
);
}
info!("Generated {} rtsim NPCs.", this.npcs.len()); info!("Generated {} rtsim NPCs.", this.npcs.len());
this this

View File

@ -14,7 +14,7 @@ use common::{
path::Path, path::Path,
rtsim::{Profession, SiteId}, rtsim::{Profession, SiteId},
store::Id, store::Id,
terrain::{TerrainChunkSize, SiteKindMeta}, terrain::{SiteKindMeta, TerrainChunkSize},
time::DayPeriod, time::DayPeriod,
vol::RectVolSize, vol::RectVolSize,
}; };
@ -32,7 +32,8 @@ use world::{
civ::{self, Track}, civ::{self, Track},
site::{Site as WorldSite, SiteKind}, site::{Site as WorldSite, SiteKind},
site2::{self, PlotKind, TileKind}, site2::{self, PlotKind, TileKind},
IndexRef, World, util::NEIGHBORS, util::NEIGHBORS,
IndexRef, World,
}; };
pub struct NpcAi; pub struct NpcAi;
@ -329,7 +330,11 @@ fn goto(wpos: Vec3<f32>, speed_factor: f32, goal_dist: f32) -> impl Action {
// Get the next waypoint on the route toward the goal // Get the next waypoint on the route toward the goal
let waypoint = let waypoint =
waypoint.get_or_insert_with(|| ctx.npc.wpos + (rpos / len) * len.min(STEP_DIST)); waypoint.get_or_insert_with(|| {
let wpos = ctx.npc.wpos + (rpos / len) * len.min(STEP_DIST);
wpos.with_z(ctx.world.sim().get_surface_alt_approx(wpos.xy().as_()).unwrap_or(wpos.z))
});
*ctx.controller = Controller::goto(*waypoint, speed_factor); *ctx.controller = Controller::goto(*waypoint, speed_factor);
}) })
@ -635,7 +640,11 @@ fn follow(npc: NpcId, distance: f32) -> impl Action {
.map(|_| {}) .map(|_| {})
} }
fn chunk_path(from: Vec2<i32>, to: Vec2<i32>, chunk_height: impl Fn(Vec2<i32>) -> Option<i32>) -> Box<dyn Action> { fn chunk_path(
from: Vec2<i32>,
to: Vec2<i32>,
chunk_height: impl Fn(Vec2<i32>) -> Option<i32>,
) -> Box<dyn Action> {
let heuristics = |(p, _): &(Vec2<i32>, i32)| p.distance_squared(to) as f32; let heuristics = |(p, _): &(Vec2<i32>, i32)| p.distance_squared(to) as f32;
let start = (from, chunk_height(from).unwrap()); let start = (from, chunk_height(from).unwrap());
let mut astar = Astar::new( let mut astar = Astar::new(
@ -648,35 +657,45 @@ fn chunk_path(from: Vec2<i32>, to: Vec2<i32>, chunk_height: impl Fn(Vec2<i32>) -
let path = astar.poll( let path = astar.poll(
1000, 1000,
heuristics, heuristics,
|&(p, _)| NEIGHBORS.into_iter().map(move |n| p + n).filter_map(|p| Some((p, chunk_height(p)?))), |&(p, _)| {
NEIGHBORS
.into_iter()
.map(move |n| p + n)
.filter_map(|p| Some((p, chunk_height(p)?)))
},
|(p0, h0), (p1, h1)| { |(p0, h0), (p1, h1)| {
let diff = ((p0 - p1).as_() * TerrainChunkSize::RECT_SIZE.as_()).with_z((h0 - h1) as f32); let diff =
((p0 - p1).as_() * TerrainChunkSize::RECT_SIZE.as_()).with_z((h0 - h1) as f32);
diff.magnitude_squared() diff.magnitude_squared()
}, },
|(e, _)| *e == to |(e, _)| *e == to,
); );
let path = match path { let path = match path {
PathResult::Exhausted(p) | PathResult::Path(p) => p, PathResult::Exhausted(p) | PathResult::Path(p) => p,
_ => return finish().boxed(), _ => return finish().boxed(),
}; };
let len = path.len(); let len = path.len();
seq( seq(path
path
.into_iter() .into_iter()
.enumerate() .enumerate()
.map(move |(i, (chunk_pos, height))| { .map(move |(i, (chunk_pos, height))| {
let wpos = TerrainChunkSize::center_wpos(chunk_pos).with_z(height).as_(); let wpos = TerrainChunkSize::center_wpos(chunk_pos)
goto(wpos, 1.0, 5.0).debug(move || format!("chunk path {i}/{len} chunk: {chunk_pos}, height: {height}")) .with_z(height)
}) .as_();
).boxed() goto(wpos, 1.0, 5.0)
.debug(move || format!("chunk path {i}/{len} chunk: {chunk_pos}, height: {height}"))
}))
.boxed()
} }
fn pilot() -> impl Action { fn pilot() -> impl Action {
// Travel between different towns in a straight line // Travel between different towns in a straight line
now(|ctx| { now(|ctx| {
let data = &*ctx.state.data(); let data = &*ctx.state.data();
let site = data.sites.iter() let site = data
.sites
.iter()
.filter(|(id, _)| Some(*id) != ctx.npc.current_site) .filter(|(id, _)| Some(*id) != ctx.npc.current_site)
.filter(|(_, site)| { .filter(|(_, site)| {
site.world_site site.world_site
@ -685,12 +704,14 @@ fn pilot() -> impl Action {
}) })
.choose(&mut thread_rng()); .choose(&mut thread_rng());
if let Some((_id, site)) = site { if let Some((_id, site)) = site {
let start_chunk = ctx.npc.wpos.xy().as_::<i32>() / TerrainChunkSize::RECT_SIZE.as_::<i32>(); let start_chunk =
ctx.npc.wpos.xy().as_::<i32>() / TerrainChunkSize::RECT_SIZE.as_::<i32>();
let end_chunk = site.wpos / TerrainChunkSize::RECT_SIZE.as_::<i32>(); let end_chunk = site.wpos / TerrainChunkSize::RECT_SIZE.as_::<i32>();
chunk_path(start_chunk, end_chunk, |chunk| { chunk_path(start_chunk, end_chunk, |chunk| {
ctx.world.sim().get_alt_approx(TerrainChunkSize::center_wpos(chunk)).map(|f| { ctx.world
(f + 150.0) as i32 .sim()
}) .get_alt_approx(TerrainChunkSize::center_wpos(chunk))
.map(|f| (f + 150.0) as i32)
}) })
} else { } else {
finish().boxed() finish().boxed()
@ -707,27 +728,42 @@ fn captain() -> impl Action {
if let Some(chunk) = NEIGHBORS if let Some(chunk) = NEIGHBORS
.into_iter() .into_iter()
.map(|neighbor| chunk + neighbor) .map(|neighbor| chunk + neighbor)
.filter(|neighbor| ctx.world.sim().get(*neighbor).map_or(false, |c| c.river.river_kind.is_some())) .filter(|neighbor| {
ctx.world
.sim()
.get(*neighbor)
.map_or(false, |c| c.river.river_kind.is_some())
})
.choose(&mut thread_rng()) .choose(&mut thread_rng())
{ {
let wpos = TerrainChunkSize::center_wpos(chunk); let wpos = TerrainChunkSize::center_wpos(chunk);
let wpos = wpos.as_().with_z(ctx.world.sim().get_interpolated(wpos, |chunk| chunk.water_alt).unwrap_or(0.0)); let wpos = wpos.as_().with_z(
ctx.world
.sim()
.get_interpolated(wpos, |chunk| chunk.water_alt)
.unwrap_or(0.0),
);
goto(wpos, 0.7, 5.0).boxed() goto(wpos, 0.7, 5.0).boxed()
} else { } else {
idle().boxed() idle().boxed()
} }
}) })
.repeat().map(|_| ()) .repeat()
.map(|_| ())
} }
fn think() -> impl Action { fn humanoid() -> impl Action {
choose(|ctx| { choose(|ctx| {
if let Some(riding) = &ctx.npc.riding { if let Some(riding) = &ctx.npc.riding {
if riding.steering { if riding.steering {
if let Some(vehicle) = ctx.state.data().npcs.vehicles.get(riding.vehicle) { if let Some(vehicle) = ctx.state.data().npcs.vehicles.get(riding.vehicle) {
match vehicle.kind { match vehicle.body {
VehicleKind::Airship => important(pilot()), common::comp::ship::Body::DefaultAirship
VehicleKind::Boat => important(captain()), | common::comp::ship::Body::AirBalloon => important(pilot()),
common::comp::ship::Body::SailBoat | common::comp::ship::Body::Galleon => {
important(captain())
},
_ => casual(idle()),
} }
} else { } else {
casual(finish()) casual(finish())
@ -748,6 +784,66 @@ fn think() -> impl Action {
}) })
} }
fn bird_large() -> impl Action {
choose(|ctx| {
let data = ctx.state.data();
if let Some(home) = ctx.npc.home {
let is_home = ctx.npc.current_site.map_or(false, |site| home == site);
if is_home {
if let Some((id, site)) = data
.sites
.iter()
.filter(|(id, site)| {
*id != home
&& site.world_site.map_or(false, |site| {
matches!(ctx.index.sites.get(site).kind, SiteKind::Dungeon(_))
})
})
.choose(&mut thread_rng())
{
casual(goto(
site.wpos.as_::<f32>().with_z(
ctx.world
.sim()
.get_surface_alt_approx(site.wpos)
.unwrap_or(0.0)
+ ctx.npc.body.flying_height(),
),
1.0,
20.0,
))
} else {
casual(idle())
}
} else if let Some(site) = data.sites.get(home) {
casual(goto(
site.wpos.as_::<f32>().with_z(
ctx.world
.sim()
.get_surface_alt_approx(site.wpos)
.unwrap_or(0.0)
+ ctx.npc.body.flying_height(),
),
1.0,
20.0,
))
} else {
casual(idle())
}
} else {
casual(idle())
}
})
}
fn think() -> impl Action {
choose(|ctx| match ctx.npc.body {
common::comp::Body::Humanoid(_) => casual(humanoid()),
common::comp::Body::BirdLarge(_) => casual(bird_large()),
_ => casual(idle()),
})
}
// if !matches!(stages.front(), Some(TravelStage::IntraSite { .. })) { // if !matches!(stages.front(), Some(TravelStage::IntraSite { .. })) {
// let data = ctx.state.data(); // let data = ctx.state.data();
// if let Some((site2, site)) = npc // if let Some((site2, site)) = npc

View File

@ -3,7 +3,7 @@ use crate::{
event::{OnSetup, OnTick}, event::{OnSetup, OnTick},
RtState, Rule, RuleError, RtState, Rule, RuleError,
}; };
use common::{terrain::TerrainChunkSize, vol::RectVolSize, grid::Grid}; use common::{grid::Grid, terrain::TerrainChunkSize, vol::RectVolSize};
use tracing::info; use tracing::info;
use vek::*; use vek::*;
@ -77,8 +77,6 @@ impl Rule for SimulateNpcs {
// Simulate the NPC's movement and interactions // Simulate the NPC's movement and interactions
if matches!(npc.mode, SimulationMode::Simulated) { if matches!(npc.mode, SimulationMode::Simulated) {
let body = npc.get_body();
if let Some(riding) = &npc.riding { if let Some(riding) = &npc.riding {
if let Some(vehicle) = data.npcs.vehicles.get_mut(riding.vehicle) { if let Some(vehicle) = data.npcs.vehicles.get_mut(riding.vehicle) {
if let Some(action) = npc.action && riding.steering { if let Some(action) = npc.action && riding.steering {
@ -94,28 +92,30 @@ impl Rule for SimulateNpcs {
.min(1.0)) .min(1.0))
.with_z(0.0); .with_z(0.0);
let is_valid = match vehicle.kind { let is_valid = match vehicle.body {
crate::data::npc::VehicleKind::Airship => true, common::comp::ship::Body::DefaultAirship | common::comp::ship::Body::AirBalloon => true,
crate::data::npc::VehicleKind::Boat => { common::comp::ship::Body::SailBoat | common::comp::ship::Body::Galleon => {
let chunk_pos = wpos.xy().as_::<i32>() / TerrainChunkSize::RECT_SIZE.as_::<i32>(); let chunk_pos = wpos.xy().as_::<i32>() / TerrainChunkSize::RECT_SIZE.as_::<i32>();
ctx.world.sim().get(chunk_pos).map_or(true, |f| f.river.river_kind.is_some()) ctx.world.sim().get(chunk_pos).map_or(true, |f| f.river.river_kind.is_some())
}, },
_ => false,
}; };
if is_valid { if is_valid {
match vehicle.kind { match vehicle.body {
crate::data::npc::VehicleKind::Airship => { common::comp::ship::Body::DefaultAirship | common::comp::ship::Body::AirBalloon => {
if let Some(alt) = ctx.world.sim().get_alt_approx(wpos.xy().as_()).filter(|alt| wpos.z < *alt) { if let Some(alt) = ctx.world.sim().get_alt_approx(wpos.xy().as_()).filter(|alt| wpos.z < *alt) {
wpos.z = alt; wpos.z = alt;
} }
}, },
crate::data::npc::VehicleKind::Boat => { common::comp::ship::Body::SailBoat | common::comp::ship::Body::Galleon => {
wpos.z = ctx wpos.z = ctx
.world .world
.sim() .sim()
.get_interpolated(wpos.xy().map(|e| e as i32), |chunk| chunk.water_alt) .get_interpolated(wpos.xy().map(|e| e as i32), |chunk| chunk.water_alt)
.unwrap_or(0.0); .unwrap_or(0.0);
}, },
_ => {},
} }
vehicle.wpos = wpos; vehicle.wpos = wpos;
} }
@ -138,7 +138,7 @@ impl Rule for SimulateNpcs {
if dist2 > 0.5f32.powi(2) { if dist2 > 0.5f32.powi(2) {
npc.wpos += (diff npc.wpos += (diff
* (body.max_speed_approx() * speed_factor * ctx.event.dt * (npc.body.max_speed_approx() * speed_factor * ctx.event.dt
/ dist2.sqrt()) / dist2.sqrt())
.min(1.0)) .min(1.0))
.with_z(0.0); .with_z(0.0);
@ -150,8 +150,8 @@ impl Rule for SimulateNpcs {
npc.wpos.z = ctx npc.wpos.z = ctx
.world .world
.sim() .sim()
.get_alt_approx(npc.wpos.xy().map(|e| e as i32)) .get_surface_alt_approx(npc.wpos.xy().map(|e| e as i32))
.unwrap_or(0.0); .unwrap_or(0.0) + npc.body.flying_height();
} }
} }

View File

@ -1486,7 +1486,8 @@ fn handle_spawn_airship(
animated: true, animated: true,
}); });
if let Some(pos) = destination { if let Some(pos) = destination {
let (kp, ki, kd) = comp::agent::pid_coefficients(&comp::Body::Ship(ship)).unwrap_or((1.0, 0.0, 0.0)); let (kp, ki, kd) =
comp::agent::pid_coefficients(&comp::Body::Ship(ship)).unwrap_or((1.0, 0.0, 0.0));
fn pure_z(sp: Vec3<f32>, pv: Vec3<f32>) -> f32 { (sp - pv).z } fn pure_z(sp: Vec3<f32>, pv: Vec3<f32>) -> f32 { (sp - pv).z }
let agent = comp::Agent::from_body(&comp::Body::Ship(ship)) let agent = comp::Agent::from_body(&comp::Body::Ship(ship))
.with_destination(pos) .with_destination(pos)
@ -1534,7 +1535,8 @@ fn handle_spawn_ship(
animated: true, animated: true,
}); });
if let Some(pos) = destination { if let Some(pos) = destination {
let (kp, ki, kd) = comp::agent::pid_coefficients(&comp::Body::Ship(ship)).unwrap_or((1.0, 0.0, 0.0)); let (kp, ki, kd) =
comp::agent::pid_coefficients(&comp::Body::Ship(ship)).unwrap_or((1.0, 0.0, 0.0));
fn pure_z(sp: Vec3<f32>, pv: Vec3<f32>) -> f32 { (sp - pv).z } fn pure_z(sp: Vec3<f32>, pv: Vec3<f32>) -> f32 { (sp - pv).z }
let agent = comp::Agent::from_body(&comp::Body::Ship(ship)) let agent = comp::Agent::from_body(&comp::Body::Ship(ship))
.with_destination(pos) .with_destination(pos)
@ -1575,11 +1577,9 @@ fn handle_make_volume(
}; };
server server
.state .state
.create_ship( .create_ship(comp::Pos(pos.0 + Vec3::unit_z() * 50.0), ship, move |_| {
comp::Pos(pos.0 + Vec3::unit_z() * 50.0), collider
ship, })
move |_| collider,
)
.build(); .build();
server.notify_client( server.notify_client(

View File

@ -14,14 +14,15 @@ use common::{
LightEmitter, Object, Ori, PidController, Poise, Pos, Projectile, Scale, SkillSet, Stats, LightEmitter, Object, Ori, PidController, Poise, Pos, Projectile, Scale, SkillSet, Stats,
TradingBehavior, Vel, WaypointArea, TradingBehavior, Vel, WaypointArea,
}, },
event::{EventBus, UpdateCharacterMetadata, NpcBuilder}, event::{EventBus, NpcBuilder, UpdateCharacterMetadata},
lottery::LootSpec, lottery::LootSpec,
mounting::Mounting,
outcome::Outcome, outcome::Outcome,
resources::{Secs, Time}, resources::{Secs, Time},
rtsim::{RtSimEntity, RtSimVehicle}, rtsim::{RtSimEntity, RtSimVehicle},
uid::Uid, uid::Uid,
util::Dir, util::Dir,
ViewDistances, mounting::Mounting, ViewDistances,
}; };
use common_net::{msg::ServerGeneral, sync::WorldSyncExt}; use common_net::{msg::ServerGeneral, sync::WorldSyncExt};
use specs::{Builder, Entity as EcsEntity, WorldExt}; use specs::{Builder, Entity as EcsEntity, WorldExt};
@ -91,14 +92,18 @@ pub fn handle_loaded_character_data(
server.notify_client(entity, ServerGeneral::CharacterDataLoadResult(Ok(metadata))); server.notify_client(entity, ServerGeneral::CharacterDataLoadResult(Ok(metadata)));
} }
pub fn handle_create_npc( pub fn handle_create_npc(server: &mut Server, pos: Pos, mut npc: NpcBuilder) -> EcsEntity {
server: &mut Server,
pos: Pos,
mut npc: NpcBuilder,
) -> EcsEntity {
let entity = server let entity = server
.state .state
.create_npc(pos, npc.stats, npc.skill_set, npc.health, npc.poise, npc.inventory, npc.body) .create_npc(
pos,
npc.stats,
npc.skill_set,
npc.health,
npc.poise,
npc.inventory,
npc.body,
)
.with(npc.scale); .with(npc.scale);
if let Some(agent) = &mut npc.agent { if let Some(agent) = &mut npc.agent {
@ -194,7 +199,6 @@ pub fn handle_create_ship(
rtsim_vehicle: Option<RtSimVehicle>, rtsim_vehicle: Option<RtSimVehicle>,
driver: Option<NpcBuilder>, driver: Option<NpcBuilder>,
passangers: Vec<NpcBuilder>, passangers: Vec<NpcBuilder>,
) { ) {
let mut entity = server let mut entity = server
.state .state
@ -213,7 +217,6 @@ pub fn handle_create_ship(
} }
let entity = entity.build(); let entity = entity.build();
if let Some(driver) = driver { if let Some(driver) = driver {
let npc_entity = handle_create_npc(server, pos, driver); let npc_entity = handle_create_npc(server, pos, driver);
@ -222,10 +225,13 @@ pub fn handle_create_ship(
(uids.get(npc_entity).copied(), uids.get(entity).copied()) (uids.get(npc_entity).copied(), uids.get(entity).copied())
{ {
drop(uids); drop(uids);
server.state.link(Mounting { server
.state
.link(Mounting {
mount: mount_uid, mount: mount_uid,
rider: rider_uid, rider: rider_uid,
}).expect("Failed to link driver to ship"); })
.expect("Failed to link driver to ship");
} else { } else {
panic!("Couldn't get Uid from newly created ship and npc"); panic!("Couldn't get Uid from newly created ship and npc");
} }

View File

@ -9,8 +9,9 @@ use common::{
dialogue::Subject, dialogue::Subject,
inventory::slot::EquipSlot, inventory::slot::EquipSlot,
loot_owner::LootOwnerKind, loot_owner::LootOwnerKind,
pet::is_mountable,
tool::ToolKind, tool::ToolKind,
Inventory, LootOwner, Pos, SkillGroupKind, pet::is_mountable, Inventory, LootOwner, Pos, SkillGroupKind,
}, },
consts::{MAX_MOUNT_RANGE, SOUND_TRAVEL_DIST_PER_VOLUME}, consts::{MAX_MOUNT_RANGE, SOUND_TRAVEL_DIST_PER_VOLUME},
event::EventBus, event::EventBus,
@ -119,10 +120,13 @@ pub fn handle_mount(server: &mut Server, rider: EcsEntity, mount: EcsEntity) {
Some(comp::Alignment::Owned(owner)) if *owner == rider_uid, Some(comp::Alignment::Owned(owner)) if *owner == rider_uid,
); );
let can_ride = state.ecs() let can_ride = state
.ecs()
.read_storage() .read_storage()
.get(mount) .get(mount)
.map_or(false, |mount_body| is_mountable(mount_body, state.ecs().read_storage().get(rider))); .map_or(false, |mount_body| {
is_mountable(mount_body, state.ecs().read_storage().get(rider))
});
if is_pet && can_ride { if is_pet && can_ride {
drop(uids); drop(uids);

View File

@ -188,15 +188,8 @@ impl Server {
ServerEvent::ExitIngame { entity } => { ServerEvent::ExitIngame { entity } => {
handle_exit_ingame(self, entity, false); handle_exit_ingame(self, entity, false);
}, },
ServerEvent::CreateNpc { ServerEvent::CreateNpc { pos, npc } => {
pos, handle_create_npc(self, pos, npc);
npc,
} => {
handle_create_npc(
self,
pos,
npc,
);
}, },
ServerEvent::CreateShip { ServerEvent::CreateShip {
pos, pos,

View File

@ -4,7 +4,7 @@ pub mod tick;
use common::{ use common::{
grid::Grid, grid::Grid,
rtsim::{ChunkResource, RtSimEntity, WorldSettings, RtSimVehicle}, rtsim::{ChunkResource, RtSimEntity, RtSimVehicle, WorldSettings},
slowjob::SlowJobPool, slowjob::SlowJobPool,
terrain::{Block, TerrainChunk}, terrain::{Block, TerrainChunk},
vol::RectRasterableVol, vol::RectRasterableVol,

View File

@ -1,5 +1,8 @@
use crate::rtsim2::{event::OnBlockChange, ChunkStates}; use crate::rtsim2::{event::OnBlockChange, ChunkStates};
use common::{terrain::{TerrainChunk, CoordinateConversions}, vol::RectRasterableVol}; use common::{
terrain::{CoordinateConversions, TerrainChunk},
vol::RectRasterableVol,
};
use rtsim2::{RtState, Rule, RuleError}; use rtsim2::{RtState, Rule, RuleError};
pub struct DepleteResources; pub struct DepleteResources;
@ -7,10 +10,7 @@ pub struct DepleteResources;
impl Rule for DepleteResources { impl Rule for DepleteResources {
fn start(rtstate: &mut RtState) -> Result<Self, RuleError> { fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
rtstate.bind::<Self, OnBlockChange>(|ctx| { rtstate.bind::<Self, OnBlockChange>(|ctx| {
let key = ctx let key = ctx.event.wpos.xy().wpos_to_cpos();
.event
.wpos
.xy().wpos_to_cpos();
if let Some(Some(chunk_state)) = ctx.state.resource_mut::<ChunkStates>().0.get(key) { if let Some(Some(chunk_state)) = ctx.state.resource_mut::<ChunkStates>().0.get(key) {
let mut chunk_res = ctx.state.data().nature.get_chunk_resources(key); let mut chunk_res = ctx.state.data().nature.get_chunk_resources(key);
// Remove resources // Remove resources

View File

@ -3,19 +3,20 @@
use super::*; use super::*;
use crate::sys::terrain::NpcData; use crate::sys::terrain::NpcData;
use common::{ use common::{
comp::{self, inventory::loadout::Loadout, skillset::skills, Body, Agent}, comp::{self, inventory::loadout::Loadout, skillset::skills, Agent, Body},
event::{EventBus, ServerEvent, NpcBuilder}, event::{EventBus, NpcBuilder, ServerEvent},
generation::{BodyBuilder, EntityConfig, EntityInfo}, generation::{BodyBuilder, EntityConfig, EntityInfo},
resources::{DeltaTime, Time, TimeOfDay}, resources::{DeltaTime, Time, TimeOfDay},
rtsim::{RtSimController, RtSimEntity, RtSimVehicle}, rtsim::{RtSimController, RtSimEntity, RtSimVehicle},
slowjob::SlowJobPool, slowjob::SlowJobPool,
terrain::CoordinateConversions,
trade::{Good, SiteInformation}, trade::{Good, SiteInformation},
LoadoutBuilder, SkillSetBuilder, terrain::CoordinateConversions, LoadoutBuilder, SkillSetBuilder, lottery::LootSpec,
}; };
use common_ecs::{Job, Origin, Phase, System}; use common_ecs::{Job, Origin, Phase, System};
use rtsim2::data::{ use rtsim2::data::{
npc::{SimulationMode, Profession}, npc::{Profession, SimulationMode},
Npc, Sites, Actor, Actor, Npc, Sites,
}; };
use specs::{Join, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage}; use specs::{Join, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage};
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
@ -126,9 +127,9 @@ fn profession_agent_mark(profession: Option<&Profession>) -> Option<comp::agent:
} }
fn get_npc_entity_info(npc: &Npc, sites: &Sites, index: IndexRef) -> EntityInfo { fn get_npc_entity_info(npc: &Npc, sites: &Sites, index: IndexRef) -> EntityInfo {
let body = npc.get_body();
let pos = comp::Pos(npc.wpos); let pos = comp::Pos(npc.wpos);
let mut rng = npc.rng(3);
if let Some(ref profession) = npc.profession { if let Some(ref profession) = npc.profession {
let economy = npc.home.and_then(|home| { let economy = npc.home.and_then(|home| {
let site = sites.get(home)?.world_site?; let site = sites.get(home)?.world_site?;
@ -137,9 +138,8 @@ fn get_npc_entity_info(npc: &Npc, sites: &Sites, index: IndexRef) -> EntityInfo
let config_asset = humanoid_config(profession); let config_asset = humanoid_config(profession);
let entity_config = let entity_config = EntityConfig::from_asset_expect_owned(config_asset)
EntityConfig::from_asset_expect_owned(config_asset).with_body(BodyBuilder::Exact(body)); .with_body(BodyBuilder::Exact(npc.body));
let mut rng = npc.rng(3);
EntityInfo::at(pos.0) EntityInfo::at(pos.0)
.with_entity_config(entity_config, Some(config_asset), &mut rng) .with_entity_config(entity_config, Some(config_asset), &mut rng)
.with_alignment(if matches!(profession, Profession::Cultist) { .with_alignment(if matches!(profession, Profession::Cultist) {
@ -151,10 +151,23 @@ fn get_npc_entity_info(npc: &Npc, sites: &Sites, index: IndexRef) -> EntityInfo
.with_lazy_loadout(profession_extra_loadout(npc.profession.as_ref())) .with_lazy_loadout(profession_extra_loadout(npc.profession.as_ref()))
.with_agent_mark(profession_agent_mark(npc.profession.as_ref())) .with_agent_mark(profession_agent_mark(npc.profession.as_ref()))
} else { } else {
let config_asset = match npc.body {
Body::BirdLarge(body) => match body.species {
comp::bird_large::Species::Phoenix => "common.entity.wild.peaceful.phoenix",
comp::bird_large::Species::Cockatrice => "common.entity.wild.aggressive.cockatrice",
comp::bird_large::Species::Roc => "common.entity.wild.aggressive.roc",
// Wildcard match used here as there is an array above
// which limits what species are used
_ => unimplemented!(),
},
_ => unimplemented!(),
};
let entity_config = EntityConfig::from_asset_expect_owned(config_asset)
.with_body(BodyBuilder::Exact(npc.body));
EntityInfo::at(pos.0) EntityInfo::at(pos.0)
.with_body(body) .with_entity_config(entity_config, Some(config_asset), &mut rng)
.with_alignment(comp::Alignment::Wild) .with_alignment(comp::Alignment::Wild)
.with_name("Rtsim NPC")
} }
} }
@ -228,7 +241,8 @@ impl<'a> System<'a> for Sys {
let npc = data.npcs.npcs.get_mut(npc_id)?; let npc = data.npcs.npcs.get_mut(npc_id)?;
if matches!(npc.mode, SimulationMode::Simulated) { if matches!(npc.mode, SimulationMode::Simulated) {
npc.mode = SimulationMode::Loaded; npc.mode = SimulationMode::Loaded;
let entity_info = get_npc_entity_info(npc, &data.sites, index.as_index_ref()); let entity_info =
get_npc_entity_info(npc, &data.sites, index.as_index_ref());
Some(match NpcData::from_entity_info(entity_info) { Some(match NpcData::from_entity_info(entity_info) {
NpcData::Data { NpcData::Data {
@ -265,11 +279,17 @@ impl<'a> System<'a> for Sys {
emitter.emit(ServerEvent::CreateShip { emitter.emit(ServerEvent::CreateShip {
pos: comp::Pos(vehicle.wpos), pos: comp::Pos(vehicle.wpos),
ship: vehicle.get_ship(), ship: vehicle.body,
// agent: None,//Some(Agent::from_body(&Body::Ship(ship))), // agent: None,//Some(Agent::from_body(&Body::Ship(ship))),
rtsim_entity: Some(RtSimVehicle(vehicle_id)), rtsim_entity: Some(RtSimVehicle(vehicle_id)),
driver: vehicle.driver.and_then(&mut actor_info), driver: vehicle.driver.and_then(&mut actor_info),
passangers: vehicle.riders.iter().copied().filter(|actor| vehicle.driver != Some(*actor)).filter_map(actor_info).collect(), passangers: vehicle
.riders
.iter()
.copied()
.filter(|actor| vehicle.driver != Some(*actor))
.filter_map(actor_info)
.collect(),
}); });
} }
} }
@ -319,10 +339,9 @@ impl<'a> System<'a> for Sys {
} }
// Synchronise rtsim NPC with entity data // Synchronise rtsim NPC with entity data
for (pos, rtsim_vehicle) in for (pos, rtsim_vehicle) in (&positions, &rtsim_vehicles).join() {
(&positions, &rtsim_vehicles).join() data.npcs
{ .vehicles
data.npcs.vehicles
.get_mut(rtsim_vehicle.0) .get_mut(rtsim_vehicle.0)
.filter(|npc| matches!(npc.mode, SimulationMode::Loaded)) .filter(|npc| matches!(npc.mode, SimulationMode::Loaded))
.map(|vehicle| { .map(|vehicle| {

View File

@ -19,7 +19,7 @@ use common_base::prof_span;
use common_ecs::{Job, Origin, ParMode, Phase, System}; use common_ecs::{Job, Origin, ParMode, Phase, System};
use rand::thread_rng; use rand::thread_rng;
use rayon::iter::ParallelIterator; use rayon::iter::ParallelIterator;
use specs::{Join, ParJoin, Read, WriteExpect, WriteStorage, saveload::MarkerAllocator}; use specs::{saveload::MarkerAllocator, Join, ParJoin, Read, WriteExpect, WriteStorage};
/// This system will allow NPCs to modify their controller /// This system will allow NPCs to modify their controller
#[derive(Default)] #[derive(Default)]
@ -101,7 +101,13 @@ impl<'a> System<'a> for Sys {
let mut rng = thread_rng(); let mut rng = thread_rng();
// The entity that is moving, if riding it's the mount, otherwise it's itself // The entity that is moving, if riding it's the mount, otherwise it's itself
let moving_entity = is_rider.and_then(|is_rider| read_data.uid_allocator.retrieve_entity_internal(is_rider.mount.into())).unwrap_or(entity); let moving_entity = is_rider
.and_then(|is_rider| {
read_data
.uid_allocator
.retrieve_entity_internal(is_rider.mount.into())
})
.unwrap_or(entity);
let moving_body = read_data.bodies.get(moving_entity); let moving_body = read_data.bodies.get(moving_entity);
@ -147,8 +153,13 @@ impl<'a> System<'a> for Sys {
Some(CharacterState::GlideWield(_) | CharacterState::Glide(_)) Some(CharacterState::GlideWield(_) | CharacterState::Glide(_))
) && physics_state.on_ground.is_none(); ) && physics_state.on_ground.is_none();
if let Some((kp, ki, kd)) = moving_body.and_then(comp::agent::pid_coefficients) { if let Some((kp, ki, kd)) = moving_body.and_then(comp::agent::pid_coefficients)
if agent.position_pid_controller.as_ref().map_or(false, |pid| (pid.kp, pid.ki, pid.kd) != (kp, ki, kd)) { {
if agent
.position_pid_controller
.as_ref()
.map_or(false, |pid| (pid.kp, pid.ki, pid.kd) != (kp, ki, kd))
{
agent.position_pid_controller = None; agent.position_pid_controller = None;
} }
let pid = agent.position_pid_controller.get_or_insert_with(|| { let pid = agent.position_pid_controller.get_or_insert_with(|| {

View File

@ -19,7 +19,7 @@ use common::{
comp::{ comp::{
self, agent, bird_medium, skillset::skills, BehaviorCapability, ForceUpdate, Pos, Waypoint, self, agent, bird_medium, skillset::skills, BehaviorCapability, ForceUpdate, Pos, Waypoint,
}, },
event::{EventBus, ServerEvent, NpcBuilder}, event::{EventBus, NpcBuilder, ServerEvent},
generation::EntityInfo, generation::EntityInfo,
lottery::LootSpec, lottery::LootSpec,
resources::{Time, TimeOfDay}, resources::{Time, TimeOfDay},
@ -225,7 +225,7 @@ impl<'a> System<'a> for Sys {
.with_agent(agent) .with_agent(agent)
.with_scale(scale) .with_scale(scale)
.with_anchor(comp::Anchor::Chunk(key)) .with_anchor(comp::Anchor::Chunk(key))
.with_loot(loot) .with_loot(loot),
}); });
}, },
} }

View File

@ -1914,6 +1914,13 @@ impl WorldSim {
} }
} }
/// Get the altitude of the surface, could be water or ground.
pub fn get_surface_alt_approx(&self, wpos: Vec2<i32>) -> Option<f32> {
self.get_interpolated(wpos, |chunk| chunk.alt)
.zip(self.get_interpolated(wpos, |chunk| chunk.water_alt))
.map(|(alt, water_alt)| alt.max(water_alt))
}
pub fn get_alt_approx(&self, wpos: Vec2<i32>) -> Option<f32> { pub fn get_alt_approx(&self, wpos: Vec2<i32>) -> Option<f32> {
self.get_interpolated(wpos, |chunk| chunk.alt) self.get_interpolated(wpos, |chunk| chunk.alt)
} }