veloren/common/src/comp/body/ship.rs

268 lines
9.0 KiB
Rust
Raw Normal View History

use crate::{
2021-11-04 12:45:08 +00:00
comp::{Collider, Density, Mass},
2021-09-01 22:48:50 +00:00
consts::{AIR_DENSITY, WATER_DENSITY},
make_case_elim,
2021-11-04 12:45:08 +00:00
terrain::{Block, BlockKind, SpriteKind},
};
use rand::prelude::SliceRandom;
use serde::{Deserialize, Serialize};
2021-11-04 12:45:08 +00:00
use std::sync::Arc;
use vek::*;
pub const ALL_BODIES: [Body; 4] = [
Body::DefaultAirship,
Body::AirBalloon,
Body::SailBoat,
Body::Galleon,
];
2022-02-04 05:00:21 +00:00
pub const ALL_AIRSHIPS: [Body; 2] = [Body::DefaultAirship, Body::AirBalloon];
pub const ALL_SHIPS: [Body; 2] = [Body::SailBoat, Body::Galleon];
2022-02-04 05:00:21 +00:00
make_case_elim!(
body,
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u32)]
pub enum Body {
DefaultAirship = 0,
AirBalloon = 1,
2021-09-01 22:48:50 +00:00
SailBoat = 2,
Galleon = 3,
2021-11-04 12:45:08 +00:00
Volume = 4,
}
);
impl From<Body> 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)
}
2022-09-08 19:51:02 +00:00
pub fn random_with(rng: &mut impl rand::Rng) -> Self { *ALL_BODIES.choose(rng).unwrap() }
2022-02-04 05:00:21 +00:00
pub fn random_airship_with(rng: &mut impl rand::Rng) -> Self {
2022-09-08 19:51:02 +00:00
*ALL_AIRSHIPS.choose(rng).unwrap()
2022-02-04 05:00:21 +00:00
}
2022-09-08 19:51:02 +00:00
pub fn random_ship_with(rng: &mut impl rand::Rng) -> Self { *ALL_SHIPS.choose(rng).unwrap() }
2021-11-04 12:45:08 +00:00
/// Return the structure manifest that this ship uses. `None` means that it
/// should be derived from the collider.
pub fn manifest_entry(&self) -> Option<&'static str> {
match self {
2021-11-04 12:45:08 +00:00
Body::DefaultAirship => Some("airship_human.structure"),
Body::AirBalloon => Some("air_balloon.structure"),
Body::SailBoat => Some("sail_boat.structure"),
Body::Galleon => Some("galleon.structure"),
Body::Volume => None,
}
}
pub fn dimensions(&self) -> Vec3<f32> {
match self {
2021-11-04 12:45:08 +00:00
Body::DefaultAirship | Body::Volume => Vec3::new(25.0, 50.0, 40.0),
Body::AirBalloon => Vec3::new(25.0, 50.0, 40.0),
2021-09-17 14:40:54 +00:00
Body::SailBoat => Vec3::new(13.0, 31.0, 3.0),
2021-10-26 00:20:59 +00:00
Body::Galleon => Vec3::new(13.0, 32.0, 3.0),
}
}
fn balloon_vol(&self) -> f32 {
2021-09-01 22:48:50 +00:00
match self {
2021-11-04 12:45:08 +00:00
Body::DefaultAirship | Body::AirBalloon | Body::Volume => {
2021-09-01 22:48:50 +00:00
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)
}
2021-09-01 22:48:50 +00:00
pub fn density(&self) -> Density {
match self {
2021-11-04 12:45:08 +00:00
Body::DefaultAirship | Body::AirBalloon | Body::Volume => Density(AIR_DENSITY),
2021-09-17 14:40:54 +00:00
_ => Density(AIR_DENSITY * 0.8 + WATER_DENSITY * 0.2), // Most boats should be buoyant
2021-09-01 22:48:50 +00:00
}
}
pub fn mass(&self) -> Mass { Mass((self.hull_vol() + self.balloon_vol()) * self.density().0) }
2021-11-04 12:45:08 +00:00
pub fn can_fly(&self) -> bool {
matches!(self, Body::DefaultAirship | Body::AirBalloon | Body::Volume)
}
2021-09-01 22:48:50 +00:00
pub fn has_water_thrust(&self) -> bool {
!self.can_fly() // TODO: Differentiate this more carefully
}
2021-11-04 12:45:08 +00:00
pub fn make_collider(&self) -> Collider {
match self.manifest_entry() {
Some(manifest_entry) => Collider::Voxel {
id: manifest_entry.to_string(),
},
None => {
use rand::prelude::*;
let sz = Vec3::broadcast(11);
Collider::Volume(Arc::new(figuredata::VoxelCollider::from_fn(sz, |_pos| {
if thread_rng().gen_bool(0.25) {
Block::new(BlockKind::Rock, Rgb::new(255, 0, 0))
} else {
Block::air(SpriteKind::Empty)
}
})))
},
}
}
}
/// 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;
2021-11-04 12:45:08 +00:00
use serde::{Deserialize, Serialize};
use vek::Vec3;
#[derive(Deserialize)]
pub struct VoxSimple(pub String);
#[derive(Deserialize)]
pub struct ShipCentralSpec(pub HashMap<super::Body, SidedShipCentralVoxSpec>);
#[derive(Deserialize)]
pub struct SidedShipCentralVoxSpec {
pub bone0: ShipCentralSubSpec,
pub bone1: ShipCentralSubSpec,
pub bone2: ShipCentralSubSpec,
2021-03-22 01:43:49 +00:00
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<Ron<ShipCentralSpec>>,
pub colliders: HashMap<String, VoxelCollider>,
}
2021-11-04 12:45:08 +00:00
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct VoxelCollider {
2021-11-04 12:45:08 +00:00
pub(super) dyna: Dyna<Block, (), ColumnAccess>,
pub translation: Vec3<f32>,
2021-11-04 12:45:08 +00:00
/// This value should be incremented every time the volume is mutated
/// and can be used to keep track of volume changes.
pub mut_count: usize,
}
impl VoxelCollider {
pub fn from_fn<F: FnMut(Vec3<i32>) -> Block>(sz: Vec3<u32>, f: F) -> Self {
Self {
dyna: Dyna::from_fn(sz, (), f),
translation: -sz.map(|e| e as f32) / 2.0,
mut_count: 0,
}
}
pub fn volume(&self) -> &Dyna<Block, (), ColumnAccess> { &self.dyna }
}
impl assets::Compound for ShipSpec {
2022-12-04 22:00:10 +00:00
fn load(
cache: assets::AnyCache,
_: &assets::SharedString,
) -> Result<Self, assets::BoxedError> {
let manifest: AssetHandle<Ron<ShipCentralSpec>> =
AssetExt::load("common.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
// "common.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::<DotVoxAsset>(&["common.voxel.", &bone.central.0].concat())?;
let dyna = Dyna::<Cell, (), ColumnAccess>::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),
2021-11-04 12:45:08 +00:00
mut_count: 0,
};
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<ShipSpec> = AssetExt::load_expect("common.manifests.ship_manifest");
}
#[test]
fn test_ship_manifest_entries() {
for body in super::ALL_BODIES {
2021-11-07 17:13:20 +00:00
if let Some(entry) = body.manifest_entry() {
assert!(
VOXEL_COLLIDER_MANIFEST
.read()
.colliders
.get(entry)
.is_some()
);
}
}
}
}