veloren_common/comp/body/
ship.rs

1use crate::{
2    comp::{Collider, Density, Mass},
3    consts::{AIR_DENSITY, WATER_DENSITY},
4    terrain::{Block, BlockKind, SpriteKind},
5};
6use rand::prelude::SliceRandom;
7use serde::{Deserialize, Serialize};
8use std::sync::Arc;
9use strum::EnumIter;
10use vek::*;
11
12// Doesn't include a lot of bodies...
13// Intentionally?
14pub const ALL_BODIES: [Body; 6] = [
15    Body::DefaultAirship,
16    Body::AirBalloon,
17    Body::SailBoat,
18    Body::Galleon,
19    Body::Skiff,
20    Body::Submarine,
21];
22
23pub const ALL_AIRSHIPS: [Body; 2] = [Body::DefaultAirship, Body::AirBalloon];
24pub const ALL_SHIPS: [Body; 6] = [
25    Body::SailBoat,
26    Body::Galleon,
27    Body::Skiff,
28    Body::Submarine,
29    Body::Carriage,
30    Body::Cart,
31];
32
33#[derive(
34    Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, EnumIter,
35)]
36#[repr(u32)]
37pub enum Body {
38    DefaultAirship = 0,
39    AirBalloon = 1,
40    SailBoat = 2,
41    Galleon = 3,
42    Volume = 4,
43    Skiff = 5,
44    Submarine = 6,
45    Carriage = 7,
46    Cart = 8,
47}
48
49impl From<Body> for super::Body {
50    fn from(body: Body) -> Self { super::Body::Ship(body) }
51}
52
53impl Body {
54    pub fn random() -> Self {
55        let mut rng = rand::thread_rng();
56        Self::random_with(&mut rng)
57    }
58
59    pub fn random_with(rng: &mut impl rand::Rng) -> Self { *ALL_BODIES.choose(rng).unwrap() }
60
61    pub fn random_airship_with(rng: &mut impl rand::Rng) -> Self {
62        *ALL_AIRSHIPS.choose(rng).unwrap()
63    }
64
65    pub fn random_ship_with(rng: &mut impl rand::Rng) -> Self { *ALL_SHIPS.choose(rng).unwrap() }
66
67    /// Return the structure manifest that this ship uses. `None` means that it
68    /// should be derived from the collider.
69    pub fn manifest_entry(&self) -> Option<&'static str> {
70        match self {
71            Body::DefaultAirship => Some("airship_human.structure"),
72            Body::AirBalloon => Some("air_balloon.structure"),
73            Body::SailBoat => Some("sail_boat.structure"),
74            Body::Galleon => Some("galleon.structure"),
75            Body::Skiff => Some("skiff.structure"),
76            Body::Submarine => Some("submarine.structure"),
77            Body::Carriage => Some("carriage.structure"),
78            Body::Cart => Some("cart.structure"),
79            Body::Volume => None,
80        }
81    }
82
83    pub fn dimensions(&self) -> Vec3<f32> {
84        match self {
85            Body::DefaultAirship | Body::Volume => Vec3::new(25.0, 50.0, 40.0),
86            Body::AirBalloon => Vec3::new(25.0, 50.0, 40.0),
87            Body::SailBoat => Vec3::new(12.0, 32.0, 6.0),
88            Body::Galleon => Vec3::new(14.0, 48.0, 10.0),
89            Body::Skiff => Vec3::new(7.0, 15.0, 10.0),
90            Body::Submarine => Vec3::new(2.0, 15.0, 8.0),
91            Body::Carriage => Vec3::new(5.0, 12.0, 2.0),
92            Body::Cart => Vec3::new(3.0, 6.0, 1.0),
93        }
94    }
95
96    fn balloon_vol(&self) -> f32 {
97        match self {
98            Body::DefaultAirship | Body::AirBalloon | Body::Volume => {
99                let spheroid_vol = |equat_d: f32, polar_d: f32| -> f32 {
100                    (std::f32::consts::PI / 6.0) * equat_d.powi(2) * polar_d
101                };
102                let dim = self.dimensions();
103                spheroid_vol(dim.z, dim.y)
104            },
105            _ => 0.0,
106        }
107    }
108
109    fn hull_vol(&self) -> f32 {
110        // height from bottom of keel to deck
111        let deck_height = 10_f32;
112        let dim = self.dimensions();
113        (std::f32::consts::PI / 6.0) * (deck_height * 1.5).powi(2) * dim.y
114    }
115
116    pub fn hull_density(&self) -> Density {
117        let oak_density = 600_f32;
118        let ratio = 0.1;
119        Density(ratio * oak_density + (1.0 - ratio) * AIR_DENSITY)
120    }
121
122    pub fn density(&self) -> Density {
123        match self {
124            Body::DefaultAirship | Body::AirBalloon | Body::Volume => Density(AIR_DENSITY),
125            Body::Submarine => Density(WATER_DENSITY), // Neutrally buoyant
126            Body::Carriage => Density(WATER_DENSITY * 0.5),
127            Body::Cart => Density(500.0 / self.dimensions().product()), /* Carts get a constant */
128            // mass
129            _ => Density(AIR_DENSITY * 0.95 + WATER_DENSITY * 0.05), /* Most boats should be very
130                                                                      * buoyant */
131        }
132    }
133
134    pub fn mass(&self) -> Mass {
135        if self.can_fly() {
136            Mass((self.hull_vol() + self.balloon_vol()) * self.density().0)
137        } else {
138            Mass(self.density().0 * self.dimensions().product())
139        }
140    }
141
142    pub fn can_fly(&self) -> bool {
143        matches!(self, Body::DefaultAirship | Body::AirBalloon | Body::Volume)
144    }
145
146    pub fn vectored_propulsion(&self) -> bool { matches!(self, Body::DefaultAirship) }
147
148    pub fn flying_height(&self) -> f32 {
149        if self.can_fly() {
150            match self {
151                Body::DefaultAirship => 300.0,
152                Body::AirBalloon => 200.0,
153                _ => 0.0,
154            }
155        } else {
156            0.0
157        }
158    }
159
160    pub fn has_water_thrust(&self) -> bool {
161        matches!(self, Body::SailBoat | Body::Galleon | Body::Skiff)
162    }
163
164    pub fn has_wheels(&self) -> bool { matches!(self, Body::Carriage | Body::Cart) }
165
166    pub fn make_collider(&self) -> Collider {
167        match self.manifest_entry() {
168            Some(manifest_entry) => Collider::Voxel {
169                id: manifest_entry.to_string(),
170            },
171            None => {
172                use rand::prelude::*;
173                let sz = Vec3::broadcast(11);
174                Collider::Volume(Arc::new(figuredata::VoxelCollider::from_fn(sz, |_pos| {
175                    if thread_rng().gen_bool(0.25) {
176                        Block::new(BlockKind::Rock, Rgb::new(255, 0, 0))
177                    } else {
178                        Block::air(SpriteKind::Empty)
179                    }
180                })))
181            },
182        }
183    }
184
185    /// Max speed in block/s
186    pub fn get_speed(&self) -> f32 {
187        match self {
188            Body::DefaultAirship => 35.0,
189            Body::AirBalloon => 8.0,
190            Body::SailBoat => 5.0,
191            Body::Galleon => 6.0,
192            Body::Skiff => 6.0,
193            Body::Submarine => 4.0,
194            _ => 10.0,
195        }
196    }
197}
198
199/// Terrain is 11.0 scale relative to small-scale voxels,
200/// airship scale is multiplied by 11 to reach terrain scale.
201pub const AIRSHIP_SCALE: f32 = 11.0;
202
203/// Duplicate of some of the things defined in `voxygen::scene::figure::load` to
204/// avoid having to refactor all of that to `common` for using voxels as
205/// collider geometry
206pub mod figuredata {
207    use crate::{
208        assets::{self, AssetExt, AssetHandle, DotVoxAsset, Ron},
209        figure::TerrainSegment,
210        terrain::{
211            StructureSprite,
212            block::{Block, BlockKind},
213            structure::load_base_structure,
214        },
215    };
216    use hashbrown::HashMap;
217    use lazy_static::lazy_static;
218    use serde::{Deserialize, Serialize};
219    use vek::{Rgb, Vec3};
220
221    #[derive(Deserialize)]
222    pub struct VoxSimple(pub String);
223
224    #[derive(Deserialize)]
225    pub struct ShipCentralSpec(pub HashMap<super::Body, SidedShipCentralVoxSpec>);
226
227    #[derive(Deserialize)]
228    pub enum DeBlock {
229        Block(BlockKind),
230        Air(StructureSprite),
231        Water(StructureSprite),
232    }
233
234    impl DeBlock {
235        fn to_block(&self, color: Rgb<u8>) -> Block {
236            match *self {
237                DeBlock::Block(block) => Block::new(block, color),
238                DeBlock::Air(sprite) => sprite.get_block(Block::air),
239                DeBlock::Water(sprite) => sprite.get_block(Block::water),
240            }
241        }
242    }
243
244    #[derive(Deserialize)]
245    pub struct SidedShipCentralVoxSpec {
246        pub bone0: ShipCentralSubSpec,
247        pub bone1: ShipCentralSubSpec,
248        pub bone2: ShipCentralSubSpec,
249        pub bone3: ShipCentralSubSpec,
250
251        // TODO: Use StructureBlock here instead. Which would require passing `IndexRef` and
252        // `Calendar` when loading the voxel colliders, which wouldn't work while it's stored in a
253        // static.
254        #[serde(default)]
255        pub custom_indices: HashMap<u8, DeBlock>,
256    }
257
258    #[derive(Deserialize)]
259    pub struct ShipCentralSubSpec {
260        pub offset: [f32; 3],
261        pub central: VoxSimple,
262        #[serde(default)]
263        pub model_index: u32,
264    }
265
266    /// manual instead of through `make_vox_spec!` so that it can be in `common`
267    #[derive(Clone)]
268    pub struct ShipSpec {
269        pub central: AssetHandle<Ron<ShipCentralSpec>>,
270        pub colliders: HashMap<String, VoxelCollider>,
271    }
272
273    #[derive(Clone, Debug, Serialize, Deserialize)]
274    pub struct VoxelCollider {
275        pub(super) dyna: TerrainSegment,
276        pub translation: Vec3<f32>,
277        /// This value should be incremented every time the volume is mutated
278        /// and can be used to keep track of volume changes.
279        pub mut_count: usize,
280    }
281
282    impl VoxelCollider {
283        pub fn from_fn<F: FnMut(Vec3<i32>) -> Block>(sz: Vec3<u32>, f: F) -> Self {
284            Self {
285                dyna: TerrainSegment::from_fn(sz, (), f),
286                translation: -sz.map(|e| e as f32) / 2.0,
287                mut_count: 0,
288            }
289        }
290
291        pub fn volume(&self) -> &TerrainSegment { &self.dyna }
292    }
293
294    impl assets::Compound for ShipSpec {
295        fn load(
296            cache: assets::AnyCache,
297            _: &assets::SharedString,
298        ) -> Result<Self, assets::BoxedError> {
299            let manifest: AssetHandle<Ron<ShipCentralSpec>> =
300                AssetExt::load("common.manifests.ship_manifest")?;
301            let mut colliders = HashMap::new();
302            for (_, spec) in (manifest.read().0).0.iter() {
303                for (index, bone) in [&spec.bone0, &spec.bone1, &spec.bone2, &spec.bone3]
304                    .iter()
305                    .enumerate()
306                {
307                    // TODO: Currently both client and server load models and manifests from
308                    // "common.voxel.". In order to support CSG procedural airships, we probably
309                    // need to load them in the server and sync them as an ECS resource.
310                    let vox =
311                        cache.load::<DotVoxAsset>(&["common.voxel.", &bone.central.0].concat())?;
312
313                    let base_structure = load_base_structure(&vox.read().0, |col| col);
314                    let dyna = base_structure.vol.map_into(|cell| {
315                        if let Some(i) = cell {
316                            let color = base_structure.palette[u8::from(i) as usize];
317                            if let Some(block) = spec.custom_indices.get(&i.get())
318                                && index == 0
319                            {
320                                block.to_block(color)
321                            } else {
322                                Block::new(BlockKind::Misc, color)
323                            }
324                        } else {
325                            Block::empty()
326                        }
327                    });
328                    let collider = VoxelCollider {
329                        dyna,
330                        translation: Vec3::from(bone.offset),
331                        mut_count: 0,
332                    };
333                    colliders.insert(bone.central.0.clone(), collider);
334                }
335            }
336            Ok(ShipSpec {
337                central: manifest,
338                colliders,
339            })
340        }
341    }
342
343    lazy_static! {
344        // TODO: Load this from the ECS as a resource, and maybe make it more general than ships
345        // (although figuring out how to keep the figure bones in sync with the terrain offsets seems
346        // like a hard problem if they're not the same manifest)
347        pub static ref VOXEL_COLLIDER_MANIFEST: AssetHandle<ShipSpec> = AssetExt::load_expect("common.manifests.ship_manifest");
348    }
349
350    #[test]
351    fn test_ship_manifest_entries() {
352        for body in super::ALL_BODIES {
353            if let Some(entry) = body.manifest_entry() {
354                assert!(
355                    VOXEL_COLLIDER_MANIFEST
356                        .read()
357                        .colliders
358                        .get(entry)
359                        .is_some()
360                );
361            }
362        }
363    }
364}