veloren_common/comp/body/
ship.rs

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