use crate::{ comp::{Density, Mass}, consts::{AIR_DENSITY, WATER_DENSITY}, make_case_elim, }; use rand::prelude::SliceRandom; use serde::{Deserialize, Serialize}; use vek::Vec3; pub const ALL_BODIES: [Body; 3] = [Body::DefaultAirship, Body::AirBalloon, Body::SailBoat]; make_case_elim!( body, #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[repr(u32)] pub enum Body { DefaultAirship = 0, AirBalloon = 1, SailBoat = 2, } ); impl From for super::Body { fn from(body: Body) -> Self { super::Body::Ship(body) } } impl Body { pub fn random() -> Self { let mut rng = rand::thread_rng(); Self::random_with(&mut rng) } pub fn random_with(rng: &mut impl rand::Rng) -> Self { *(&ALL_BODIES).choose(rng).unwrap() } pub fn manifest_entry(&self) -> &'static str { match self { Body::DefaultAirship => "airship_human.structure", Body::AirBalloon => "air_balloon.structure", Body::SailBoat => "sail_boat.structure", } } pub fn dimensions(&self) -> Vec3 { match self { Body::DefaultAirship => Vec3::new(25.0, 50.0, 40.0), Body::AirBalloon => Vec3::new(25.0, 50.0, 40.0), Body::SailBoat => Vec3::new(13.0, 31.0, 3.0), } } fn balloon_vol(&self) -> f32 { match self { Body::DefaultAirship | Body::AirBalloon => { let spheroid_vol = |equat_d: f32, polar_d: f32| -> f32 { (std::f32::consts::PI / 6.0) * equat_d.powi(2) * polar_d }; let dim = self.dimensions(); spheroid_vol(dim.z, dim.y) }, _ => 0.0, } } fn hull_vol(&self) -> f32 { // height from bottom of keel to deck let deck_height = 10_f32; let dim = self.dimensions(); (std::f32::consts::PI / 6.0) * (deck_height * 1.5).powi(2) * dim.y } pub fn hull_density(&self) -> Density { let oak_density = 600_f32; let ratio = 0.1; Density(ratio * oak_density + (1.0 - ratio) * AIR_DENSITY) } pub fn density(&self) -> Density { match self { Body::DefaultAirship | Body::AirBalloon => Density(AIR_DENSITY), _ => Density(AIR_DENSITY * 0.8 + WATER_DENSITY * 0.2), // Most boats should be buoyant } } pub fn mass(&self) -> Mass { Mass((self.hull_vol() + self.balloon_vol()) * self.density().0) } pub fn can_fly(&self) -> bool { matches!(self, Body::DefaultAirship | Body::AirBalloon) } pub fn has_water_thrust(&self) -> bool { !self.can_fly() // TODO: Differentiate this more carefully } } /// Terrain is 11.0 scale relative to small-scale voxels, /// airship scale is multiplied by 11 to reach terrain scale. pub const AIRSHIP_SCALE: f32 = 11.0; /// Duplicate of some of the things defined in `voxygen::scene::figure::load` to /// avoid having to refactor all of that to `common` for using voxels as /// collider geometry pub mod figuredata { use crate::{ assets::{self, AssetExt, AssetHandle, DotVoxAsset, Ron}, figure::cell::Cell, terrain::{ block::{Block, BlockKind}, sprite::SpriteKind, }, volumes::dyna::{ColumnAccess, Dyna}, }; use hashbrown::HashMap; use lazy_static::lazy_static; use serde::Deserialize; use vek::Vec3; #[derive(Deserialize)] pub struct VoxSimple(pub String); #[derive(Deserialize)] pub struct ShipCentralSpec(pub HashMap); #[derive(Deserialize)] pub struct SidedShipCentralVoxSpec { pub bone0: ShipCentralSubSpec, pub bone1: ShipCentralSubSpec, pub bone2: ShipCentralSubSpec, pub bone3: ShipCentralSubSpec, } #[derive(Deserialize)] pub struct ShipCentralSubSpec { pub offset: [f32; 3], pub phys_offset: [f32; 3], pub central: VoxSimple, } /// manual instead of through `make_vox_spec!` so that it can be in `common` #[derive(Clone)] pub struct ShipSpec { pub central: AssetHandle>, pub colliders: HashMap, } #[derive(Clone)] pub struct VoxelCollider { pub dyna: Dyna, pub translation: Vec3, } impl assets::Compound for ShipSpec { fn load( cache: &assets::AssetCache, _: &str, ) -> Result { let manifest: AssetHandle> = AssetExt::load("server.manifests.ship_manifest")?; let mut colliders = HashMap::new(); for (_, spec) in (manifest.read().0).0.iter() { for bone in [&spec.bone0, &spec.bone1, &spec.bone2].iter() { // TODO: Currently both client and server load models and manifests from // "server.voxel.". In order to support CSG procedural airships, we probably // need to load them in the server and sync them as an ECS resource. let vox = cache.load::(&["server.voxel.", &bone.central.0].concat())?; let dyna = Dyna::::from_vox(&vox.read().0, false); let dyna = dyna.map_into(|cell| { if let Some(rgb) = cell.get_color() { Block::new(BlockKind::Misc, rgb) } else { Block::air(SpriteKind::Empty) } }); let collider = VoxelCollider { dyna, translation: Vec3::from(bone.offset) + Vec3::from(bone.phys_offset), }; colliders.insert(bone.central.0.clone(), collider); } } Ok(ShipSpec { central: manifest, colliders, }) } } lazy_static! { // TODO: Load this from the ECS as a resource, and maybe make it more general than ships // (although figuring out how to keep the figure bones in sync with the terrain offsets seems // like a hard problem if they're not the same manifest) pub static ref VOXEL_COLLIDER_MANIFEST: AssetHandle = AssetExt::load_expect("server.manifests.ship_manifest"); } #[test] fn test_ship_manifest_entries() { for body in super::ALL_BODIES { assert!( VOXEL_COLLIDER_MANIFEST .read() .colliders .get(body.manifest_entry()) .is_some() ); } } }