diff --git a/CHANGELOG.md b/CHANGELOG.md index c387001e17..35ec7fd29b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Stealth stat values on (some) armors - All new dismantling interface found at your nearest dismantling staion - Wearable headgear, including hood, crown, bandanas +- Bomb sprites (can be exploded with arrows or other explosions) +- Campfire waypoints in towns +- Arbitrary volume entities ### Changed diff --git a/assets/common/abilities/ability_set_manifest.ron b/assets/common/abilities/ability_set_manifest.ron index bb94cd51db..62cca4443f 100644 --- a/assets/common/abilities/ability_set_manifest.ron +++ b/assets/common/abilities/ability_set_manifest.ron @@ -294,6 +294,11 @@ secondary: "common.abilities.empty.basic", abilities: [], ), + Custom("Golf Club"): ( + primary: "common.abilities.hammer.singlestrike", + secondary: "common.abilities.tool.golf_club.charged", + abilities: [], + ), Tool(Debug): ( primary: "common.abilities.debug.forwardboost", secondary: "common.abilities.debug.upboost", diff --git a/assets/common/abilities/tool/golf_club/charged.ron b/assets/common/abilities/tool/golf_club/charged.ron new file mode 100644 index 0000000000..7b0ca2e58a --- /dev/null +++ b/assets/common/abilities/tool/golf_club/charged.ron @@ -0,0 +1,17 @@ +ChargedMelee( + energy_cost: 1, + energy_drain: 1.0, + initial_damage: 1.0, + scaled_damage: 0.1, + initial_poise_damage: 1, + scaled_poise_damage: 1, + initial_knockback: 30.0, + scaled_knockback: 60.0, + range: 5.0, + max_angle: 30.0, + charge_duration: 2.0, + swing_duration: 0.1, + hit_timing: 0.2, + recover_duration: 1.5, + damage_kind: Crushing, +) diff --git a/assets/common/items/weapons/tool/golf_club.ron b/assets/common/items/weapons/tool/golf_club.ron new file mode 100644 index 0000000000..80a4d20624 --- /dev/null +++ b/assets/common/items/weapons/tool/golf_club.ron @@ -0,0 +1,21 @@ +ItemDef( + name: "Golf Club", + description: "Peasant swatter. Fiercely anti-urbanist. Climate crisis? What climate crisis?", + kind: Tool(( + kind: Hammer, + hands: Two, + stats: Direct(( + equip_time_secs: 0.5, + power: 0.6, + effect_power: 1.0, + speed: 1.0, + crit_chance: 0.083333336, + range: 1.0, + energy_efficiency: 1.0, + buff_strength: 1.0, + )), + )), + quality: Low, + tags: [], + ability_spec: Some(Custom("Golf Club")), +) diff --git a/assets/voxygen/item_image_manifest.ron b/assets/voxygen/item_image_manifest.ron index e4d3c04279..e353e31449 100644 --- a/assets/voxygen/item_image_manifest.ron +++ b/assets/voxygen/item_image_manifest.ron @@ -2866,6 +2866,11 @@ "voxel.weapon.tool.broom_belzeshrub_purple", (0.0, 0.0, 0.0), (-135.0, 90.0, 0.0), 1.1, ), + // Misc + Tool("common.items.weapons.tool.golf_club"): VoxTrans( + "voxel.weapon.tool.golf_club", + (2.0, -1.0, 0.0), (-135.0, 25.0, 0.0), 1.1, + ), // Gems Ingredient("Amethyst"): VoxTrans( "voxel.sprite.mineral.gem.amethystgem", diff --git a/assets/voxygen/voxel/biped_weapon_manifest.ron b/assets/voxygen/voxel/biped_weapon_manifest.ron index a87845437a..9bf91969d1 100644 --- a/assets/voxygen/voxel/biped_weapon_manifest.ron +++ b/assets/voxygen/voxel/biped_weapon_manifest.ron @@ -1000,6 +1000,10 @@ vox_spec: ("weapon.sword.frost-1", (-2.0, -4.5, -7.5)), color: None ), + "common.items.weapons.tool.golf_club": ( + vox_spec: ("weapon.tool.golf_club", (-5.5, -4.0, -4.0)), + color: None + ), // Misc "common.items.weapons.empty.empty": ( vox_spec: ("armor.empty", (-3.0, -3.5, 1.0)), diff --git a/assets/voxygen/voxel/sprite_manifest.ron b/assets/voxygen/voxel/sprite_manifest.ron index e2831a62f4..f674af54c6 100644 --- a/assets/voxygen/voxel/sprite_manifest.ron +++ b/assets/voxygen/voxel/sprite_manifest.ron @@ -3411,7 +3411,7 @@ WitchWindow: Some(( ( model: "voxygen.voxel.sprite.window.witch_purple", offset: (-5.5, -5.5, 0.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.5, 0.5, 0.5), ), ], wind_sway: 0.0, @@ -3453,27 +3453,27 @@ CavernGrassBlueShort: Some(( ( model: "voxygen.voxel.sprite.cavern.grass_short-0", offset: (-5.5, -5.5, 0.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.0, 0.0, 1.0), ), ( model: "voxygen.voxel.sprite.cavern.grass_short-1", offset: (-5.5, -5.5, 0.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.0, 0.0, 1.0), ), ( model: "voxygen.voxel.sprite.cavern.grass_short-2", offset: (-5.5, -5.5, 0.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.0, 0.0, 1.0), ), ( model: "voxygen.voxel.sprite.cavern.grass_short-3", offset: (-5.5, -5.5, 0.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.0, 0.0, 1.0), ), ( model: "voxygen.voxel.sprite.cavern.grass_short-4", offset: (-5.5, -5.5, 0.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.0, 0.0, 1.0), ), ], wind_sway: 0.0, @@ -3484,22 +3484,22 @@ CavernGrassBlueMedium: Some(( ( model: "voxygen.voxel.sprite.cavern.grass_med-0", offset: (-5.5, -5.5, 0.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.0, 0.0, 1.0), ), ( model: "voxygen.voxel.sprite.cavern.grass_med-1", offset: (-5.5, -5.5, 0.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.0, 0.0, 1.0), ), ( model: "voxygen.voxel.sprite.cavern.grass_med-2", offset: (-5.5, -5.5, 0.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.0, 0.0, 1.0), ), ( model: "voxygen.voxel.sprite.cavern.grass_med-3", offset: (-5.5, -5.5, 0.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.0, 0.0, 1.0), ), ], wind_sway: 0.0, @@ -3510,42 +3510,42 @@ CavernGrassBlueLong: Some(( ( model: "voxygen.voxel.sprite.cavern.grass_long-0", offset: (-5.5, -5.5, 0.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.0, 0.0, 1.0), ), ( model: "voxygen.voxel.sprite.cavern.grass_long-1", offset: (-5.5, -5.5, 0.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.0, 0.0, 1.0), ), ( model: "voxygen.voxel.sprite.cavern.grass_long-2", offset: (-5.5, -5.5, 0.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.0, 0.0, 1.0), ), ( model: "voxygen.voxel.sprite.cavern.grass_long-3", offset: (-5.5, -5.5, 0.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.0, 0.0, 1.0), ), ( model: "voxygen.voxel.sprite.cavern.grass_long-4", offset: (-5.5, -5.5, 0.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.0, 0.0, 1.0), ), ( model: "voxygen.voxel.sprite.cavern.grass_long-5", offset: (-5.5, -5.5, 0.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.0, 0.0, 1.0), ), ( model: "voxygen.voxel.sprite.cavern.grass_long-6", offset: (-5.5, -5.5, 0.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.0, 0.0, 1.0), ), ( model: "voxygen.voxel.sprite.cavern.grass_long-7", offset: (-5.5, -5.5, 0.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.0, 0.0, 1.0), ), ], wind_sway: 0.0, @@ -3556,27 +3556,27 @@ CavernLillypadBlue: Some(( ( model: "voxygen.voxel.sprite.cavern.lillypad-0", offset: (-5.5, -5.5, -1.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.5, 0.5, 0.0), ), ( model: "voxygen.voxel.sprite.cavern.lillypad-1", offset: (-5.5, -5.5, -1.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.5, 0.5, 0.0), ), ( model: "voxygen.voxel.sprite.cavern.lillypad-2", offset: (-5.5, -5.5, -1.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.5, 0.5, 0.0), ), ( model: "voxygen.voxel.sprite.cavern.lillypad-3", offset: (-5.5, -5.5, -1.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.5, 0.5, 0.0), ), ( model: "voxygen.voxel.sprite.cavern.lillypad-4", offset: (-5.5, -5.5, -1.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.5, 0.5, 0.0), ), ], wind_sway: 0.0, @@ -3587,22 +3587,22 @@ CavernMycelBlue: Some(( ( model: "voxygen.voxel.sprite.cavern.mycel-0", offset: (-0.5, -0.5, -21.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.0, 0.0, 1.0), ), ( model: "voxygen.voxel.sprite.cavern.mycel-1", offset: (-0.5, -0.5, -31.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.0, 0.0, 1.0), ), ( model: "voxygen.voxel.sprite.cavern.mycel-2", offset: (-0.5, -0.5, -14.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.0, 0.0, 1.0), ), ( model: "voxygen.voxel.sprite.cavern.mycel-3", offset: (-0.5, -0.5, -40.0), - lod_axes: (0.0, 0.0, 0.0), + lod_axes: (0.0, 0.0, 1.0), ), ], wind_sway: 0.1, @@ -3658,4 +3658,14 @@ LillyPads: Some(( ], wind_sway: 0.6, )), +Bomb: Some(( + variations: [ + ( + model: "voxygen.voxel.object.bomb", + offset: (-5.5, -5.5, 0.0), + lod_axes: (0.5, 0.5, 0.5), + ), + ], + wind_sway: 0.0, +)), ) diff --git a/assets/voxygen/voxel/weapon/tool/golf_club.vox b/assets/voxygen/voxel/weapon/tool/golf_club.vox new file mode 100644 index 0000000000..ff1b82d5d0 --- /dev/null +++ b/assets/voxygen/voxel/weapon/tool/golf_club.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:376af4f3d3c46d12d65d68dd25b2eb9fa584d8c9162441efc69ae2178196c33d +size 1464 diff --git a/common/Cargo.toml b/common/Cargo.toml index 126081e07d..0e197365de 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -64,7 +64,7 @@ csv = { version = "1.1.3", optional = true } structopt = { version = "0.3.13", optional = true } # graphviz exporters petgraph = { version = "0.5.1", optional = true } -# K-d trees used for RRT pathfinding +# K-d trees used for RRT pathfinding kiddo = { version = "0.1", optional = true } # Data structures diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 69398bf91f..02c78b683c 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -107,6 +107,7 @@ pub enum ChatCommand { Whitelist, Wiring, World, + MakeVolume, } #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] @@ -638,6 +639,7 @@ impl ChatCommand { "Send messages to everyone on the server", None, ), + ChatCommand::MakeVolume => cmd(vec![], "Create a volume (experimental)", Some(Admin)), } } @@ -709,6 +711,7 @@ impl ChatCommand { ChatCommand::Wiring => "wiring", ChatCommand::Whitelist => "whitelist", ChatCommand::World => "world", + ChatCommand::MakeVolume => "make_volume", } } diff --git a/common/src/comp/body.rs b/common/src/comp/body.rs index 2d2db26548..d624c1d75c 100644 --- a/common/src/comp/body.rs +++ b/common/src/comp/body.rs @@ -859,6 +859,7 @@ impl Body { ship::Body::AirBalloon => [0.0, 0.0, 5.0], ship::Body::SailBoat => [-2.0, -5.0, 4.0], ship::Body::Galleon => [-2.0, -5.0, 4.0], + ship::Body::Volume => [0.0, 0.0, 0.0], }, _ => [0.0, 0.0, 0.0], } diff --git a/common/src/comp/body/ship.rs b/common/src/comp/body/ship.rs index 79121e4aff..ca94fb2c89 100644 --- a/common/src/comp/body/ship.rs +++ b/common/src/comp/body/ship.rs @@ -1,11 +1,13 @@ use crate::{ - comp::{Density, Mass}, + comp::{Collider, Density, Mass}, consts::{AIR_DENSITY, WATER_DENSITY}, make_case_elim, + terrain::{Block, BlockKind, SpriteKind}, }; use rand::prelude::SliceRandom; use serde::{Deserialize, Serialize}; -use vek::Vec3; +use std::sync::Arc; +use vek::*; pub const ALL_BODIES: [Body; 4] = [ Body::DefaultAirship, @@ -23,6 +25,7 @@ make_case_elim!( AirBalloon = 1, SailBoat = 2, Galleon = 3, + Volume = 4, } ); @@ -38,18 +41,21 @@ impl Body { pub fn random_with(rng: &mut impl rand::Rng) -> Self { *(&ALL_BODIES).choose(rng).unwrap() } - pub fn manifest_entry(&self) -> &'static str { + /// 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 { - Body::DefaultAirship => "airship_human.structure", - Body::AirBalloon => "air_balloon.structure", - Body::SailBoat => "sail_boat.structure", - Body::Galleon => "galleon.structure", + 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 { match self { - Body::DefaultAirship => Vec3::new(25.0, 50.0, 40.0), + Body::DefaultAirship | Body::Volume => 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), Body::Galleon => Vec3::new(13.0, 32.0, 3.0), @@ -58,7 +64,7 @@ impl Body { fn balloon_vol(&self) -> f32 { match self { - Body::DefaultAirship | Body::AirBalloon => { + Body::DefaultAirship | Body::AirBalloon | Body::Volume => { let spheroid_vol = |equat_d: f32, polar_d: f32| -> f32 { (std::f32::consts::PI / 6.0) * equat_d.powi(2) * polar_d }; @@ -84,18 +90,39 @@ impl Body { pub fn density(&self) -> Density { match self { - Body::DefaultAirship | Body::AirBalloon => Density(AIR_DENSITY), + Body::DefaultAirship | Body::AirBalloon | Body::Volume => 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 can_fly(&self) -> bool { + matches!(self, Body::DefaultAirship | Body::AirBalloon | Body::Volume) + } pub fn has_water_thrust(&self) -> bool { !self.can_fly() // TODO: Differentiate this more carefully } + + 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, @@ -117,7 +144,7 @@ pub mod figuredata { }; use hashbrown::HashMap; use lazy_static::lazy_static; - use serde::Deserialize; + use serde::{Deserialize, Serialize}; use vek::Vec3; #[derive(Deserialize)] @@ -148,10 +175,25 @@ pub mod figuredata { pub colliders: HashMap, } - #[derive(Clone)] + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct VoxelCollider { - pub dyna: Dyna, + pub(super) dyna: Dyna, pub translation: Vec3, + /// 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) -> Block>(sz: Vec3, 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 { &self.dyna } } impl assets::Compound for ShipSpec { @@ -180,6 +222,7 @@ pub mod figuredata { let collider = VoxelCollider { dyna, translation: Vec3::from(bone.offset) + Vec3::from(bone.phys_offset), + mut_count: 0, }; colliders.insert(bone.central.0.clone(), collider); } @@ -201,13 +244,15 @@ pub mod figuredata { #[test] fn test_ship_manifest_entries() { for body in super::ALL_BODIES { - assert!( - VOXEL_COLLIDER_MANIFEST - .read() - .colliders - .get(body.manifest_entry()) - .is_some() - ); + if let Some(entry) = body.manifest_entry() { + assert!( + VOXEL_COLLIDER_MANIFEST + .read() + .colliders + .get(entry) + .is_some() + ); + } } } } diff --git a/common/src/comp/phys.rs b/common/src/comp/phys.rs index c52dc49364..efa306fb28 100644 --- a/common/src/comp/phys.rs +++ b/common/src/comp/phys.rs @@ -1,9 +1,12 @@ use super::{Fluid, Ori}; -use crate::{consts::WATER_DENSITY, terrain::Block, uid::Uid}; +use crate::{ + comp::body::ship::figuredata::VoxelCollider, consts::WATER_DENSITY, terrain::Block, uid::Uid, +}; use hashbrown::HashSet; use serde::{Deserialize, Serialize}; use specs::{Component, DerefFlaggedStorage, NullStorage}; use specs_idvs::IdvStorage; +use std::sync::Arc; use vek::*; /// Position @@ -103,13 +106,16 @@ impl Component for Density { } // Collider -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub enum Collider { + /// A volume based on an existing voxel asset. // TODO: pass the map from ids -> voxel data to get_radius // and get_z_limits to compute a bounding cylinder. Voxel { id: String, }, + /// A mutable volume. + Volume(Arc), /// Capsule prism with line segment from p0 to p1 CapsulePrism { p0: Vec2, @@ -122,9 +128,11 @@ pub enum Collider { } impl Collider { + pub fn is_voxel(&self) -> bool { matches!(self, Collider::Voxel { .. } | Collider::Volume(_)) } + pub fn bounding_radius(&self) -> f32 { match self { - Collider::Voxel { .. } => 1.0, + Collider::Voxel { .. } | Collider::Volume(_) => 1.0, Collider::CapsulePrism { radius, p0, p1, .. } => { let a = p0.distance(*p1); a / 2.0 + *radius @@ -140,7 +148,7 @@ impl Collider { pub fn get_z_limits(&self, modifier: f32) -> (f32, f32) { match self { - Collider::Voxel { .. } => (0.0, 1.0), + Collider::Voxel { .. } | Collider::Volume(_) => (0.0, 1.0), Collider::CapsulePrism { z_min, z_max, .. } => (*z_min * modifier, *z_max * modifier), Collider::Point => (0.0, 0.0), } diff --git a/common/src/states/sprite_interact.rs b/common/src/states/sprite_interact.rs index 82f48c98f0..20d0336c9b 100644 --- a/common/src/states/sprite_interact.rs +++ b/common/src/states/sprite_interact.rs @@ -156,7 +156,8 @@ impl From for Option { | SpriteKind::VialEmpty | SpriteKind::Bowl | SpriteKind::PotionMinor - | SpriteKind::Seashells => Some(SpriteInteractKind::Collectible), + | SpriteKind::Seashells + | SpriteKind::Bomb => Some(SpriteInteractKind::Collectible), // Collectible checked in addition to container for case that sprite requires a tool to // collect and cannot be collected by hand, yet still meets the container check _ if sprite_kind.is_container() && sprite_kind.is_collectible() => { diff --git a/common/src/terrain/block.rs b/common/src/terrain/block.rs index 86bab3ec99..d7fdbd4c01 100644 --- a/common/src/terrain/block.rs +++ b/common/src/terrain/block.rs @@ -279,7 +279,9 @@ impl Block { #[inline] pub fn is_bonkable(&self) -> bool { match self.get_sprite() { - Some(SpriteKind::Apple | SpriteKind::Beehive | SpriteKind::Coconut) => self.is_solid(), + Some( + SpriteKind::Apple | SpriteKind::Beehive | SpriteKind::Coconut | SpriteKind::Bomb, + ) => self.is_solid(), _ => false, } } diff --git a/common/src/terrain/sprite.rs b/common/src/terrain/sprite.rs index 55f36b0a5e..b62e9c92e6 100644 --- a/common/src/terrain/sprite.rs +++ b/common/src/terrain/sprite.rs @@ -187,6 +187,7 @@ make_case_elim!( LillyPads = 0xA0, JungleLeafyPlant = 0xA1, JungleRedGrass = 0xA2, + Bomb = 0xA3, } ); @@ -261,7 +262,8 @@ impl SpriteKind { | SpriteKind::Window3 | SpriteKind::Window4 | SpriteKind::DropGate - | SpriteKind::WitchWindow => 1.0, + | SpriteKind::WitchWindow + | SpriteKind::Bomb => 1.0, // TODO: Figure out if this should be solid or not. SpriteKind::Shelf => 1.0, SpriteKind::Lantern => 0.9, @@ -335,6 +337,7 @@ impl SpriteKind { SpriteKind::RoundCactus => item("common.items.crafting_ing.cactus"), SpriteKind::ShortFlatCactus => item("common.items.crafting_ing.cactus"), SpriteKind::MedFlatCactus => item("common.items.crafting_ing.cactus"), + SpriteKind::Bomb => item("common.items.utility.bomb"), SpriteKind::DungeonChest0 => table("common.loot_tables.dungeon.tier-0.chest"), SpriteKind::DungeonChest1 => table("common.loot_tables.dungeon.tier-1.chest"), SpriteKind::DungeonChest2 => table("common.loot_tables.dungeon.tier-2.chest"), diff --git a/common/src/volumes/dyna.rs b/common/src/volumes/dyna.rs index 2be6b25cbe..c51666a35c 100644 --- a/common/src/volumes/dyna.rs +++ b/common/src/volumes/dyna.rs @@ -129,6 +129,19 @@ impl Dyna { } } + /// Same as [`Dyna::filled`], but with the voxel determined by the function + /// `f`. + pub fn from_fn) -> V>(sz: Vec3, meta: M, mut f: F) -> Self { + Self { + vox: (0..sz.product() as usize) + .map(|idx| f(A::pos(idx, sz))) + .collect(), + meta, + sz, + _phantom: std::marker::PhantomData, + } + } + /// Get a reference to the internal metadata. pub fn metadata(&self) -> &M { &self.meta } @@ -138,6 +151,8 @@ impl Dyna { pub trait Access { fn idx(pos: Vec3, sz: Vec3) -> usize; + /// `idx` must be in range, permitted to panic otherwise. + fn pos(idx: usize, sz: Vec3) -> Vec3; } #[derive(Copy, Clone, Debug)] @@ -147,4 +162,11 @@ impl Access for ColumnAccess { fn idx(pos: Vec3, sz: Vec3) -> usize { (pos.x * sz.y as i32 * sz.z as i32 + pos.y * sz.z as i32 + pos.z) as usize } + + fn pos(idx: usize, sz: Vec3) -> Vec3 { + let z = idx as u32 % sz.z; + let y = (idx as u32 / sz.z) % sz.y; + let x = idx as u32 / (sz.y * sz.z); + Vec3::new(x, y, z).map(|e| e as i32) + } } diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index d4a625870e..39480e8177 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -225,7 +225,7 @@ impl<'a> PhysicsData<'a> { let neighborhood_radius = match collider { Collider::CapsulePrism { radius, .. } => radius * scale, - Collider::Voxel { .. } | Collider::Point => flat_radius, + Collider::Voxel { .. } | Collider::Volume(_) | Collider::Point => flat_radius, }; phys_cache.neighborhood_radius = neighborhood_radius; @@ -265,7 +265,7 @@ impl<'a> PhysicsData<'a> { Some((p0, p1)) } }, - Collider::Voxel { .. } | Collider::Point => None, + Collider::Voxel { .. } | Collider::Volume(_) | Collider::Point => None, }; phys_cache.origins = origins; phys_cache.ori = ori; @@ -508,6 +508,9 @@ impl<'a> PhysicsData<'a> { ref read, ref write, } = self; + + let voxel_colliders_manifest = VOXEL_COLLIDER_MANIFEST.read(); + // NOTE: i32 places certain constraints on how far out collision works // NOTE: uses the radius of the entity and their current position rather than // the radius of their bounding sphere for the current frame of movement @@ -527,13 +530,14 @@ impl<'a> PhysicsData<'a> { ) .join() { - let voxel_id = match collider { - Collider::Voxel { id } => id, - _ => continue, + let vol = match collider { + Collider::Voxel { id } => voxel_colliders_manifest.colliders.get(&*id), + Collider::Volume(vol) => Some(&**vol), + _ => None, }; - if let Some(voxel_collider) = VOXEL_COLLIDER_MANIFEST.read().colliders.get(&*voxel_id) { - let sphere = voxel_collider_bounding_sphere(voxel_collider, pos, ori); + if let Some(vol) = vol { + let sphere = voxel_collider_bounding_sphere(vol, pos, ori); let radius = sphere.radius.ceil() as u32; let pos_2d = sphere.center.xy().map(|e| e as i32); const POS_TRUNCATION_ERROR: u32 = 1; @@ -729,7 +733,7 @@ impl<'a> PhysicsData<'a> { !&read.mountings, ) .par_join() - .filter(|tuple| matches!(tuple.3, Collider::Voxel { .. }) == terrain_like_entities) + .filter(|tuple| tuple.3.is_voxel() == terrain_like_entities) .map_init( || { prof_span!(guard, "physics e<>t rayon job"); @@ -760,7 +764,7 @@ impl<'a> PhysicsData<'a> { let old_ori = *ori; let mut ori = *ori; - let scale = if let Collider::Voxel { .. } = collider { + let scale = if collider.is_voxel() { scale.map(|s| s.0).unwrap_or(1.0) } else { // TODO: Use scale & actual proportions when pathfinding is good @@ -806,7 +810,7 @@ impl<'a> PhysicsData<'a> { character_state.map_or(false, |cs| matches!(cs, CharacterState::Climb(_))); match &collider { - Collider::Voxel { .. } => { + Collider::Voxel { .. } | Collider::Volume(_) => { // For now, treat entities with voxel colliders // as their bounding cylinders for the purposes of // colliding them with terrain. @@ -1003,6 +1007,8 @@ impl<'a> PhysicsData<'a> { let query_center = path_sphere.center.xy(); let query_radius = path_sphere.radius; + let voxel_colliders_manifest = VOXEL_COLLIDER_MANIFEST.read(); + voxel_collider_spatial_grid .in_circle_aabr(query_center, query_radius) .filter_map(|entity| { @@ -1029,10 +1035,12 @@ impl<'a> PhysicsData<'a> { return; } - let voxel_id = if let Collider::Voxel { id } = collider_other { - id - } else { - return; + let voxel_collider = match collider_other { + Collider::Voxel { id } => { + voxel_colliders_manifest.colliders.get(id) + }, + Collider::Volume(vol) => Some(&**vol), + _ => None, }; // use bounding cylinder regardless of our collider @@ -1045,9 +1053,7 @@ impl<'a> PhysicsData<'a> { let z_min = 0.0; let z_max = z_max.clamped(1.2, 1.95) * scale; - if let Some(voxel_collider) = - VOXEL_COLLIDER_MANIFEST.read().colliders.get(voxel_id) - { + if let Some(voxel_collider) = voxel_collider { // TODO: cache/precompute sphere? let voxel_sphere = voxel_collider_bounding_sphere( voxel_collider, @@ -1112,7 +1118,7 @@ impl<'a> PhysicsData<'a> { let cylinder = (radius, z_min, z_max); box_voxel_collision( cylinder, - &voxel_collider.dyna, + &voxel_collider.volume(), entity, &mut cpos, transform_to.mul_point(tgt_pos - wpos), @@ -1226,7 +1232,7 @@ impl<'a> PhysicsData<'a> { &read.colliders, ) .join() - .filter(|tuple| matches!(tuple.5, Collider::Voxel { .. }) == terrain_like_entities) + .filter(|tuple| tuple.5.is_voxel() == terrain_like_entities) { if let Some(new_pos) = pos_vel_ori_defer.pos.take() { *pos = new_pos; @@ -1725,8 +1731,8 @@ fn voxel_collider_bounding_sphere( ) -> Sphere { let origin_offset = voxel_collider.translation; use common::vol::SizedVol; - let lower_bound = voxel_collider.dyna.lower_bound().map(|e| e as f32); - let upper_bound = voxel_collider.dyna.upper_bound().map(|e| e as f32); + let lower_bound = voxel_collider.volume().lower_bound().map(|e| e as f32); + let upper_bound = voxel_collider.volume().upper_bound().map(|e| e as f32); let center = (lower_bound + upper_bound) / 2.0; // Compute vector from the origin (where pos value corresponds to) and the model // center @@ -1845,8 +1851,8 @@ fn resolve_e2e_collision( && (!is_sticky || is_mid_air) && diff.magnitude_squared() > 0.0 && !is_projectile - && !matches!(collider_other, Collider::Voxel { .. }) - && !matches!(collider, Collider::Voxel { .. }) + && !collider_other.is_voxel() + && !collider.is_voxel() { const ELASTIC_FORCE_COEFFICIENT: f32 = 400.0; let mass_coefficient = mass_other.0 / (mass.0 + mass_other.0); diff --git a/server/src/cmd.rs b/server/src/cmd.rs index ba1d9c3c37..7963dd57a5 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -37,7 +37,7 @@ use common::{ resources::{BattleMode, PlayerPhysicsSettings, Time, TimeOfDay}, terrain::{Block, BlockKind, SpriteKind, TerrainChunkSize}, uid::Uid, - vol::RectVolSize, + vol::{ReadVol, RectVolSize}, Damage, DamageKind, DamageSource, Explosion, LoadoutBuilder, RadiusEffect, }; use common_net::{ @@ -50,7 +50,7 @@ use hashbrown::{HashMap, HashSet}; use humantime::Duration as HumanDuration; use rand::Rng; use specs::{storage::StorageEntry, Builder, Entity as EcsEntity, Join, WorldExt}; -use std::str::FromStr; +use std::{str::FromStr, sync::Arc}; use vek::*; use wiring::{Circuit, Wire, WiringAction, WiringActionEffect, WiringElement}; use world::util::Sampler; @@ -176,6 +176,7 @@ fn do_command( ChatCommand::Wiring => handle_spawn_wiring, ChatCommand::Whitelist => handle_whitelist, ChatCommand::World => handle_world, + ChatCommand::MakeVolume => handle_make_volume, }; handler(server, client, target, args, cmd) @@ -1269,7 +1270,7 @@ fn handle_spawn_airship( let ship = comp::ship::Body::random(); let mut builder = server .state - .create_ship(pos, ship, true) + .create_ship(pos, ship, |ship| ship.make_collider(), true) .with(LightEmitter { col: Rgb::new(1.0, 0.65, 0.2), strength: 2.0, @@ -1293,6 +1294,46 @@ fn handle_spawn_airship( Ok(()) } +fn handle_make_volume( + server: &mut Server, + client: EcsEntity, + target: EcsEntity, + _args: Vec, + _action: &ChatCommand, +) -> CmdResult<()> { + use comp::body::ship::figuredata::VoxelCollider; + + //let () = parse_args!(args); + let pos = position(server, target, "target")?; + let ship = comp::ship::Body::Volume; + let sz = Vec3::new(15, 15, 15); + let collider = { + let terrain = server.state().terrain(); + comp::Collider::Volume(Arc::new(VoxelCollider::from_fn(sz, |rpos| { + terrain + .get(pos.0.map(|e| e.floor() as i32) + rpos - sz.map(|e| e as i32) / 2) + .ok() + .copied() + .unwrap_or_else(Block::empty) + }))) + }; + server + .state + .create_ship( + comp::Pos(pos.0 + Vec3::unit_z() * 50.0), + ship, + move |_| collider, + true, + ) + .build(); + + server.notify_client( + client, + ServerGeneral::server_msg(ChatType::CommandInfo, "Created a volume"), + ); + Ok(()) +} + fn handle_spawn_campfire( server: &mut Server, client: EcsEntity, diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index 4d6cbc4cd2..06d4e4fc4d 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -156,7 +156,9 @@ pub fn handle_create_ship( agent: Option, rtsim_entity: Option, ) { - let mut entity = server.state.create_ship(pos, ship, mountable); + let mut entity = server + .state + .create_ship(pos, ship, |ship| ship.make_collider(), mountable); if let Some(mut agent) = agent { let (kp, ki, kd) = pid_coefficients(&Body::Ship(ship)); fn pure_z(sp: Vec3, pv: Vec3) -> f32 { (sp - pv).z } diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index fbea2b88c8..46c6ef7894 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -526,7 +526,8 @@ pub fn handle_explosion(server: &Server, pos: Vec3, explosion: Explosion, o }); let explosion_volume = 2.5 * explosion.radius; - server_eventbus.emit_now(ServerEvent::Sound { + let mut emitter = server_eventbus.emitter(); + emitter.emit(ServerEvent::Sound { sound: Sound::new(SoundKind::Explosion, pos, explosion_volume, time.0), }); @@ -659,6 +660,14 @@ pub fn handle_explosion(server: &Server, pos: Vec3, explosion: Explosion, o block_change.set(block_pos, Block::new(block.kind(), color)); } } + + if block.is_bonkable() { + emitter.emit(ServerEvent::Bonk { + pos: block_pos.map(|e| e as f32 + 0.5), + owner, + target: None, + }); + } } } @@ -804,7 +813,7 @@ pub fn handle_explosion(server: &Server, pos: Vec3, explosion: Explosion, o attack_options, strength, combat::AttackSource::Explosion, - |e| server_eventbus.emit_now(e), + |e| emitter.emit(e), |o| outcomes.push(o), ); } @@ -866,7 +875,7 @@ pub fn handle_explosion(server: &Server, pos: Vec3, explosion: Explosion, o } } -pub fn handle_bonk(server: &mut Server, pos: Vec3, _owner: Option, target: Option) { +pub fn handle_bonk(server: &mut Server, pos: Vec3, owner: Option, target: Option) { let ecs = &server.state.ecs(); let terrain = ecs.read_resource::(); let mut block_change = ecs.write_resource::(); @@ -891,10 +900,15 @@ pub fn handle_bonk(server: &mut Server, pos: Vec3, _owner: Option, tar Some(SpriteKind::Apple) => comp::object::Body::Apple, Some(SpriteKind::Beehive) => comp::object::Body::Hive, Some(SpriteKind::Coconut) => comp::object::Body::Coconut, + Some(SpriteKind::Bomb) => comp::object::Body::Bomb, _ => comp::object::Body::Pouch, }) .with(comp::Pos(pos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0))) .with(item) + .maybe_with(match block.get_sprite() { + Some(SpriteKind::Bomb) => Some(comp::Object::Bomb { owner }), + _ => None, + }) .build(); } }; diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 1b4dc8dd5f..9ae8599926 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -51,10 +51,11 @@ pub trait StateExt { ) -> EcsEntityBuilder; /// Build a static object entity fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder; - fn create_ship( + fn create_ship comp::Collider>( &mut self, pos: comp::Pos, ship: comp::ship::Body, + make_collider: F, mountable: bool, ) -> EcsEntityBuilder; /// Build a projectile @@ -200,9 +201,7 @@ impl StateExt for State { .with(body.mass()) .with(body.density()) .with(match body { - comp::Body::Ship(ship) => comp::Collider::Voxel { - id: ship.manifest_entry().to_string(), - }, + comp::Body::Ship(ship) => ship.make_collider(), _ => capsule(&body), }) .with(comp::Controller::default()) @@ -243,10 +242,11 @@ impl StateExt for State { .with(body) } - fn create_ship( + fn create_ship comp::Collider>( &mut self, pos: comp::Pos, ship: comp::ship::Body, + make_collider: F, mountable: bool, ) -> EcsEntityBuilder { let body = comp::Body::Ship(ship); @@ -258,9 +258,7 @@ impl StateExt for State { .with(comp::Ori::default()) .with(body.mass()) .with(body.density()) - .with(comp::Collider::Voxel { - id: ship.manifest_entry().to_string(), - }) + .with(make_collider(ship)) .with(body) .with(comp::Scale(comp::ship::AIRSHIP_SCALE)) .with(comp::Controller::default()) diff --git a/voxygen/anim/src/lib.rs b/voxygen/anim/src/lib.rs index c894de6835..edc81e626e 100644 --- a/voxygen/anim/src/lib.rs +++ b/voxygen/anim/src/lib.rs @@ -81,7 +81,7 @@ pub struct FigureBoneData(pub MatRaw, pub MatRaw); pub const MAX_BONE_COUNT: usize = 16; -fn make_bone(mat: Mat4) -> FigureBoneData { +pub fn make_bone(mat: Mat4) -> FigureBoneData { let normal = mat.map_cols(Vec4::normalized); FigureBoneData(mat.into_col_arrays(), normal.into_col_arrays()) } diff --git a/voxygen/anim/src/ship/mod.rs b/voxygen/anim/src/ship/mod.rs index 5a32b68326..81d09e9a90 100644 --- a/voxygen/anim/src/ship/mod.rs +++ b/voxygen/anim/src/ship/mod.rs @@ -95,24 +95,28 @@ impl<'a> From<&'a Body> for SkeletonAttr { AirBalloon => (0.0, 0.0, 0.0), SailBoat => (0.0, 0.0, 0.0), Galleon => (0.0, 0.0, 0.0), + Volume => (0.0, 0.0, 0.0), }, bone1: match body { DefaultAirship => (-13.0, -25.0, 10.0), AirBalloon => (0.0, 0.0, 0.0), SailBoat => (0.0, 0.0, 0.0), Galleon => (0.0, 0.0, 0.0), + Volume => (0.0, 0.0, 0.0), }, bone2: match body { DefaultAirship => (13.0, -25.0, 10.0), AirBalloon => (0.0, 0.0, 0.0), SailBoat => (0.0, 0.0, 0.0), Galleon => (0.0, 0.0, 0.0), + Volume => (0.0, 0.0, 0.0), }, bone3: match body { DefaultAirship => (0.0, -27.5, 8.5), AirBalloon => (0.0, -9.0, 8.0), SailBoat => (0.0, 0.0, 0.0), Galleon => (0.0, 0.0, 0.0), + Volume => (0.0, 0.0, 0.0), }, } } diff --git a/voxygen/src/scene/figure/cache.rs b/voxygen/src/scene/figure/cache.rs index 7f25fd606c..ca35f63874 100644 --- a/voxygen/src/scene/figure/cache.rs +++ b/voxygen/src/scene/figure/cache.rs @@ -6,7 +6,6 @@ use crate::{ }; use anim::Skeleton; use common::{ - assets::AssetHandle, comp::{ inventory::{ slot::{ArmorSlot, EquipSlot}, @@ -287,7 +286,7 @@ where Skel::Body: BodySpec, { models: HashMap, ((FigureModelEntryFuture, Skel::Attr), u64)>, - manifests: AssetHandle<::Spec>, + manifests: ::Manifests, } impl FigureModelCache @@ -345,6 +344,7 @@ where col_lights: &mut super::FigureColLights, body: Skel::Body, inventory: Option<&Inventory>, + extra: ::Extra, tick: u64, camera_mode: CameraMode, character_state: Option<&CharacterState>, @@ -407,13 +407,12 @@ where Entry::Vacant(v) => { let key = v.key().clone(); let slot = Arc::new(atomic::AtomicCell::new(None)); - let manifests = self.manifests; + let manifests = self.manifests.clone(); let slot_ = Arc::clone(&slot); slow_jobs.spawn("FIGURE_MESHING", move || { // First, load all the base vertex data. - let manifests = &*manifests.read(); - let meshes = ::bone_meshes(&key, manifests); + let meshes = ::bone_meshes(&key, &manifests, extra); // Then, set up meshing context. let mut greedy = FigureModel::make_greedy(); @@ -562,7 +561,7 @@ where { // Check for reloaded manifests // TODO: maybe do this in a different function, maintain? - if self.manifests.reloaded() { + if ::is_reloaded(&mut self.manifests) { col_lights.atlas.clear(); self.models.clear(); } diff --git a/voxygen/src/scene/figure/load.rs b/voxygen/src/scene/figure/load.rs index a249148052..632044255b 100644 --- a/voxygen/src/scene/figure/load.rs +++ b/voxygen/src/scene/figure/load.rs @@ -87,9 +87,16 @@ fn recolor_grey(rgb: Rgb, color: Rgb) -> Rgb { /// A set of reloadable specifications for a Body. pub trait BodySpec: Sized { type Spec; + /// Cloned on each cache invalidation. If this type is expensive to clone, + /// place it behind an [`Arc`]. + type Manifests: Send + Sync + Clone; + type Extra: Send + Sync; /// Initialize all the specifications for this Body. - fn load_spec() -> Result, assets::Error>; + fn load_spec() -> Result; + + /// Determine whether the cache's manifest was reloaded + fn is_reloaded(manifests: &mut Self::Manifests) -> bool; /// Mesh bones using the given spec, character state, and mesh generation /// function. @@ -100,7 +107,8 @@ pub trait BodySpec: Sized { /// in which case this strategy might change. fn bone_meshes( key: &FigureKey, - spec: &Self::Spec, + manifests: &Self::Manifests, + extra: Self::Extra, ) -> [Option; anim::MAX_BONE_COUNT]; } @@ -125,16 +133,22 @@ macro_rules! make_vox_spec { impl BodySpec for $body { type Spec = $Spec; + type Manifests = AssetHandle; + type Extra = (); #[allow(unused_variables)] - fn load_spec() -> Result, assets::Error> { + fn load_spec() -> Result { Self::Spec::load("") } + fn is_reloaded(manifests: &mut Self::Manifests) -> bool { manifests.reloaded() } + fn bone_meshes( $self_pat: &FigureKey, - $spec_pat: &Self::Spec, + manifests: &Self::Manifests, + _: Self::Extra, ) -> [Option; anim::MAX_BONE_COUNT] { + let $spec_pat = &*manifests.read(); $bone_meshes } } @@ -4528,15 +4542,21 @@ fn mesh_ship_bone &ShipCentralSubSpec } impl BodySpec for ship::Body { + type Extra = (); + type Manifests = AssetHandle; type Spec = ShipSpec; #[allow(unused_variables)] - fn load_spec() -> Result, assets::Error> { Self::Spec::load("") } + fn load_spec() -> Result { Self::Spec::load("") } + + fn is_reloaded(manifests: &mut Self::Manifests) -> bool { manifests.reloaded() } fn bone_meshes( FigureKey { body, .. }: &FigureKey, - spec: &Self::Spec, + manifests: &Self::Manifests, + _: Self::Extra, ) -> [Option; anim::MAX_BONE_COUNT] { + let spec = &*manifests.read(); let map = &(spec.central.read().0).0; [ Some(mesh_ship_bone(map, body, |spec| &spec.bone0)), diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index b50098b474..cb27054041 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -1,8 +1,10 @@ mod cache; pub mod load; +mod volume; pub use cache::FigureModelCache; pub use load::load_mesh; // TODO: Don't make this public. +pub use volume::VolumeKey; use crate::{ ecs::comp::Interpolated, @@ -30,7 +32,7 @@ use common::{ comp::{ inventory::slot::EquipSlot, item::{Hands, ItemKind, ToolKind}, - Body, CharacterState, Controller, Health, Inventory, Item, Last, LightAnimation, + Body, CharacterState, Collider, Controller, Health, Inventory, Item, Last, LightAnimation, LightEmitter, Mounting, Ori, PhysicsState, PoiseState, Pos, Scale, Vel, }, resources::DeltaTime, @@ -50,6 +52,7 @@ use core::{ use guillotiere::AtlasAllocator; use hashbrown::HashMap; use specs::{saveload::MarkerAllocator, Entity as EcsEntity, Join, LazyUpdate, WorldExt}; +use std::sync::Arc; use treeculler::{BVol, BoundingSphere}; use vek::*; @@ -113,6 +116,7 @@ struct FigureMgrStates { golem_states: HashMap>, object_states: HashMap>, ship_states: HashMap>, + volume_states: HashMap>, } impl FigureMgrStates { @@ -133,6 +137,7 @@ impl FigureMgrStates { golem_states: HashMap::new(), object_states: HashMap::new(), ship_states: HashMap::new(), + volume_states: HashMap::new(), } } @@ -193,7 +198,13 @@ impl FigureMgrStates { .map(DerefMut::deref_mut), Body::Golem(_) => self.golem_states.get_mut(entity).map(DerefMut::deref_mut), Body::Object(_) => self.object_states.get_mut(entity).map(DerefMut::deref_mut), - Body::Ship(_) => self.ship_states.get_mut(entity).map(DerefMut::deref_mut), + Body::Ship(ship) => { + if ship.manifest_entry().is_some() { + self.ship_states.get_mut(entity).map(DerefMut::deref_mut) + } else { + self.volume_states.get_mut(entity).map(DerefMut::deref_mut) + } + }, } } @@ -217,7 +228,13 @@ impl FigureMgrStates { Body::BipedSmall(_) => self.biped_small_states.remove(entity).map(|e| e.meta), Body::Golem(_) => self.golem_states.remove(entity).map(|e| e.meta), Body::Object(_) => self.object_states.remove(entity).map(|e| e.meta), - Body::Ship(_) => self.ship_states.remove(entity).map(|e| e.meta), + Body::Ship(ship) => { + if ship.manifest_entry().is_some() { + self.ship_states.remove(entity).map(|e| e.meta) + } else { + self.volume_states.remove(entity).map(|e| e.meta) + } + }, } } @@ -238,6 +255,7 @@ impl FigureMgrStates { self.golem_states.retain(|k, v| f(k, &mut *v)); self.object_states.retain(|k, v| f(k, &mut *v)); self.ship_states.retain(|k, v| f(k, &mut *v)); + self.volume_states.retain(|k, v| f(k, &mut *v)); } fn count(&self) -> usize { @@ -257,6 +275,7 @@ impl FigureMgrStates { + self.golem_states.len() + self.object_states.len() + self.ship_states.len() + + self.volume_states.len() } fn count_visible(&self) -> usize { @@ -330,6 +349,11 @@ impl FigureMgrStates { .filter(|(_, c)| c.visible()) .count() + self.ship_states.iter().filter(|(_, c)| c.visible()).count() + + self + .volume_states + .iter() + .filter(|(_, c)| c.visible()) + .count() } } @@ -350,6 +374,7 @@ pub struct FigureMgr { object_model_cache: FigureModelCache, ship_model_cache: FigureModelCache, golem_model_cache: FigureModelCache, + volume_model_cache: FigureModelCache, states: FigureMgrStates, } @@ -372,6 +397,7 @@ impl FigureMgr { object_model_cache: FigureModelCache::new(), ship_model_cache: FigureModelCache::new(), golem_model_cache: FigureModelCache::new(), + volume_model_cache: FigureModelCache::new(), states: FigureMgrStates::default(), } } @@ -404,6 +430,7 @@ impl FigureMgr { self.object_model_cache.clean(&mut self.col_lights, tick); self.ship_model_cache.clean(&mut self.col_lights, tick); self.golem_model_cache.clean(&mut self.col_lights, tick); + self.volume_model_cache.clean(&mut self.col_lights, tick); } pub fn update_lighting(&mut self, scene_data: &SceneData) { @@ -600,6 +627,7 @@ impl FigureMgr { item, light_emitter, mountings, + collider, ), ) in ( &ecs.entities(), @@ -617,6 +645,7 @@ impl FigureMgr { ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), + ecs.read_storage::().maybe(), ) .join() .enumerate() @@ -794,6 +823,7 @@ impl FigureMgr { &mut self.col_lights, body, inventory, + (), tick, player_camera_mode, player_character_state, @@ -1670,6 +1700,7 @@ impl FigureMgr { &mut self.col_lights, body, inventory, + (), tick, player_camera_mode, player_character_state, @@ -1859,6 +1890,7 @@ impl FigureMgr { &mut self.col_lights, body, inventory, + (), tick, player_camera_mode, player_character_state, @@ -2173,6 +2205,7 @@ impl FigureMgr { &mut self.col_lights, body, inventory, + (), tick, player_camera_mode, player_character_state, @@ -2519,6 +2552,7 @@ impl FigureMgr { &mut self.col_lights, body, inventory, + (), tick, player_camera_mode, player_character_state, @@ -2620,6 +2654,7 @@ impl FigureMgr { &mut self.col_lights, body, inventory, + (), tick, player_camera_mode, player_character_state, @@ -2700,6 +2735,7 @@ impl FigureMgr { &mut self.col_lights, body, inventory, + (), tick, player_camera_mode, player_character_state, @@ -3096,6 +3132,7 @@ impl FigureMgr { &mut self.col_lights, body, inventory, + (), tick, player_camera_mode, player_character_state, @@ -3180,6 +3217,7 @@ impl FigureMgr { &mut self.col_lights, body, inventory, + (), tick, player_camera_mode, player_character_state, @@ -3356,6 +3394,7 @@ impl FigureMgr { &mut self.col_lights, body, inventory, + (), tick, player_camera_mode, player_character_state, @@ -3676,6 +3715,7 @@ impl FigureMgr { &mut self.col_lights, body, inventory, + (), tick, player_camera_mode, player_character_state, @@ -3756,6 +3796,7 @@ impl FigureMgr { &mut self.col_lights, body, inventory, + (), tick, player_camera_mode, player_character_state, @@ -4375,6 +4416,7 @@ impl FigureMgr { &mut self.col_lights, body, inventory, + (), tick, player_camera_mode, player_character_state, @@ -4613,6 +4655,7 @@ impl FigureMgr { &mut self.col_lights, body, inventory, + (), tick, player_camera_mode, player_character_state, @@ -4733,16 +4776,57 @@ impl FigureMgr { ); }, Body::Ship(body) => { - let (model, skeleton_attr) = self.ship_model_cache.get_or_create_model( - renderer, - &mut self.col_lights, - body, - inventory, - tick, - player_camera_mode, - player_character_state, - &slow_jobs, - ); + let (model, skeleton_attr) = if let Some(Collider::Volume(vol)) = collider { + let vk = VolumeKey { + entity, + mut_count: vol.mut_count, + }; + let (model, _skeleton_attr) = self.volume_model_cache.get_or_create_model( + renderer, + &mut self.col_lights, + vk, + inventory, + Arc::clone(vol), + tick, + player_camera_mode, + player_character_state, + &slow_jobs, + ); + + let state = self + .states + .volume_states + .entry(entity) + .or_insert_with(|| FigureState::new(renderer, vk, vk)); + + state.update( + renderer, + &mut update_buf, + &common_params, + state_animation_rate, + model, + vk, + ); + + break; + } else if body.manifest_entry().is_some() { + self.ship_model_cache.get_or_create_model( + renderer, + &mut self.col_lights, + body, + inventory, + (), + tick, + player_camera_mode, + player_character_state, + &slow_jobs, + ) + } else { + // No way to determine model (this is okay, we might just not have received + // the `Collider` for the entity yet. Wait until the + // next tick. + break; + }; let state = self.states.ship_states.entry(entity).or_insert_with(|| { FigureState::new(renderer, ShipSkeleton::default(), body) @@ -4847,11 +4931,12 @@ impl FigureMgr { ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), + ecs.read_storage::().maybe(), ) .join() // Don't render dead entities - .filter(|(_, _, _, _, health, _, _)| health.map_or(true, |h| !h.is_dead)) - .for_each(|(entity, pos, _, body, _, inventory, scale)| { + .filter(|(_, _, _, _, health, _, _, _)| health.map_or(true, |h| !h.is_dead)) + .for_each(|(entity, pos, _, body, _, inventory, scale, collider)| { if let Some((bound, model, _)) = self.get_model_for_render( tick, camera, @@ -4862,6 +4947,10 @@ impl FigureMgr { false, pos.0, figure_lod_render_distance * scale.map_or(1.0, |s| s.0), + match collider { + Some(Collider::Volume(vol)) => vol.mut_count, + _ => 0, + }, |state| state.can_shadow_sun(), ) { drawer.draw(model, bound); @@ -4884,19 +4973,20 @@ impl FigureMgr { let character_state_storage = state.read_storage::(); let character_state = character_state_storage.get(player_entity); - for (entity, pos, body, _, inventory, scale) in ( + for (entity, pos, body, _, inventory, scale, collider) in ( &ecs.entities(), &ecs.read_storage::(), &ecs.read_storage::(), ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), - ecs.read_storage::().maybe() + ecs.read_storage::().maybe(), + ecs.read_storage::().maybe(), ) .join() // Don't render dead entities - .filter(|(_, _, _, health, _, _)| health.map_or(true, |h| !h.is_dead)) + .filter(|(_, _, _, health, _, _, _)| health.map_or(true, |h| !h.is_dead)) // Don't render player - .filter(|(entity, _, _, _, _, _)| *entity != player_entity) + .filter(|(entity, _, _, _, _, _, _)| *entity != player_entity) { if let Some((bound, model, col_lights)) = self.get_model_for_render( tick, @@ -4908,6 +4998,10 @@ impl FigureMgr { false, pos.0, figure_lod_render_distance * scale.map_or(1.0, |s| s.0), + match collider { + Some(Collider::Volume(vol)) => vol.mut_count, + _ => 0, + }, |state| state.visible(), ) { drawer.draw(model, bound, col_lights); @@ -4953,6 +5047,7 @@ impl FigureMgr { true, pos.0, figure_lod_render_distance, + 0, |state| state.visible(), ) { drawer.draw(model, bound, col_lights); @@ -4980,6 +5075,7 @@ impl FigureMgr { is_player: bool, pos: vek::Vec3, figure_lod_render_distance: f32, + mut_count: usize, filter_state: impl Fn(&FigureStateMeta) -> bool, ) -> Option { let body = *body; @@ -5010,6 +5106,7 @@ impl FigureMgr { object_model_cache, ship_model_cache, golem_model_cache, + volume_model_cache, states: FigureMgrStates { character_states, @@ -5027,6 +5124,7 @@ impl FigureMgr { golem_states, object_states, ship_states, + volume_states, }, } = self; let col_lights = &*col_lights_; @@ -5255,22 +5353,43 @@ impl FigureMgr { ), ) }), - Body::Ship(body) => ship_states - .get(&entity) - .filter(|state| filter_state(*state)) - .map(move |state| { - ( - state.bound(), - ship_model_cache.get_model( - col_lights, - body, - inventory, - tick, - player_camera_mode, - character_state, - ), - ) - }), + Body::Ship(body) => { + if body.manifest_entry().is_some() { + ship_states + .get(&entity) + .filter(|state| filter_state(*state)) + .map(move |state| { + ( + state.bound(), + ship_model_cache.get_model( + col_lights, + body, + inventory, + tick, + player_camera_mode, + character_state, + ), + ) + }) + } else { + volume_states + .get(&entity) + .filter(|state| filter_state(*state)) + .map(move |state| { + ( + state.bound(), + volume_model_cache.get_model( + col_lights, + VolumeKey { entity, mut_count }, + inventory, + tick, + player_camera_mode, + character_state, + ), + ) + }) + } + }, } { let model_entry = model_entry?; diff --git a/voxygen/src/scene/figure/volume.rs b/voxygen/src/scene/figure/volume.rs new file mode 100644 index 0000000000..9f4664c80c --- /dev/null +++ b/voxygen/src/scene/figure/volume.rs @@ -0,0 +1,99 @@ +use super::{ + cache::FigureKey, + load::{BodySpec, BoneMeshes}, + EcsEntity, +}; +use common::{ + assets, + comp::ship::figuredata::VoxelCollider, + figure::{Cell, Segment}, + vol::ReadVol, +}; +use std::{convert::TryFrom, sync::Arc}; + +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +pub struct VolumeKey { + pub entity: EcsEntity, + pub mut_count: usize, +} + +impl<'a> From<&'a Self> for VolumeKey { + fn from(this: &Self) -> Self { *this } +} + +impl anim::Skeleton for VolumeKey { + type Attr = Self; + type Body = Self; + + const BONE_COUNT: usize = 4; + + //#[cfg(feature = "use-dyn-lib")] + // TODO + + fn compute_matrices_inner( + &self, + base_mat: anim::vek::Mat4, + buf: &mut [anim::FigureBoneData; anim::MAX_BONE_COUNT], + _: Self::Body, + ) -> anim::Offsets { + let scale_mat = anim::vek::Mat4::scaling_3d(1.0 / 11.0); + + let bone = base_mat * scale_mat; + + *(<&mut [_; Self::BONE_COUNT]>::try_from(&mut buf[0..Self::BONE_COUNT]).unwrap()) = [ + anim::make_bone(bone), + anim::make_bone(bone), + anim::make_bone(bone), + anim::make_bone(bone), + ]; + + anim::Offsets { + lantern: None, + mount_bone: anim::vek::Transform::default(), + } + } +} + +impl BodySpec for VolumeKey { + type Extra = Arc; + type Manifests = (); + type Spec = (); + + fn load_spec() -> Result { Ok(()) } + + fn is_reloaded(_: &mut Self::Manifests) -> bool { false } + + fn bone_meshes( + _: &FigureKey, + _: &Self::Manifests, + collider: Self::Extra, + ) -> [Option; anim::MAX_BONE_COUNT] { + println!("Generating segment..."); + [ + Some(( + Segment::from_fn(collider.volume().sz, (), |pos| { + match collider.volume().get(pos).unwrap().get_color() { + Some(col) => Cell::new(col, false, false, false), + None => Cell::Empty, + } + }), + -collider.volume().sz.map(|e| e as f32) / 2.0, + )), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + ] + } +} diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index 3f69f0dfe6..336356d9d5 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -1198,7 +1198,9 @@ impl Scene { let hb_ori = [ori.x, ori.y, ori.z, ori.w]; self.debug.set_context(*shape_id, hb_pos, color, hb_ori); }, - comp::Collider::Voxel { .. } | comp::Collider::Point => { + comp::Collider::Voxel { .. } + | comp::Collider::Volume(_) + | comp::Collider::Point => { // ignore terrain-like or point-hitboxes }, } diff --git a/voxygen/src/scene/simple.rs b/voxygen/src/scene/simple.rs index 7381a920ac..e41c919fe1 100644 --- a/voxygen/src/scene/simple.rs +++ b/voxygen/src/scene/simple.rs @@ -320,6 +320,7 @@ impl Scene { &mut self.col_lights, body, inventory, + (), scene_data.tick, CameraMode::default(), None, diff --git a/world/src/civ/mod.rs b/world/src/civ/mod.rs index 0a86227eca..76450f204f 100644 --- a/world/src/civ/mod.rs +++ b/world/src/civ/mod.rs @@ -137,10 +137,10 @@ impl Civs { SiteKind::Tree => (12i32, 8.0), }; - let (raise, raise_dist): (f32, i32) = match &site.kind { - SiteKind::Settlement => (10.0, 6), - SiteKind::Castle => (0.0, 6), - _ => (0.0, 0), + let (raise, raise_dist, make_waypoint): (f32, i32, bool) = match &site.kind { + SiteKind::Settlement => (10.0, 6, true), + SiteKind::Castle => (0.0, 6, true), + _ => (0.0, 0, false), }; // Flatten ground @@ -173,6 +173,10 @@ impl Civs { chunk.basement += diff; chunk.rockiness = 0.0; chunk.surface_veg *= 1.0 - factor * rng.gen_range(0.25..0.9); + + if make_waypoint && offs == Vec2::zero() { + chunk.contains_waypoint = true; + } }); } }