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