diff --git a/assets/common/abilities/custom/birdlargebasic/summontornadoes.ron b/assets/common/abilities/custom/birdlargebasic/summontornadoes.ron index 9ad07751b4..21c90ae04a 100644 --- a/assets/common/abilities/custom/birdlargebasic/summontornadoes.ron +++ b/assets/common/abilities/custom/birdlargebasic/summontornadoes.ron @@ -3,11 +3,16 @@ BasicSummon( cast_duration: 1.0, recover_duration: 0.5, summon_amount: 6, + summon_distance: (1, 5), summon_info: ( body: Object(Tornado), scale: None, - health_scaling: 80, + health_scaling: None, loadout_config: None, skillset_config: None, ), + duration: Some(( + secs: 5, + nanos: 0, + )), ) \ No newline at end of file diff --git a/assets/common/abilities/custom/mindflayer/summonminions.ron b/assets/common/abilities/custom/mindflayer/summonminions.ron index 99117ce2c0..7fdfb01ec1 100644 --- a/assets/common/abilities/custom/mindflayer/summonminions.ron +++ b/assets/common/abilities/custom/mindflayer/summonminions.ron @@ -3,14 +3,16 @@ BasicSummon( cast_duration: 1.0, recover_duration: 0.5, summon_amount: 6, + summon_distance: (3, 3), summon_info: ( body: BipedSmall(( species: Husk, body_type: Male, )), scale: None, - health_scaling: 80, + health_scaling: Some(80), loadout_config: Some(HuskSummon), skillset_config: None, ), + duration: None, ) diff --git a/assets/common/abilities/custom/tidalwarrior/totem.ron b/assets/common/abilities/custom/tidalwarrior/totem.ron index 31df64a7cd..c997b8954c 100644 --- a/assets/common/abilities/custom/tidalwarrior/totem.ron +++ b/assets/common/abilities/custom/tidalwarrior/totem.ron @@ -3,11 +3,13 @@ BasicSummon( cast_duration: 1.0, recover_duration: 0.5, summon_amount: 1, + summon_distance: (1, 1), summon_info: ( body: Object(SeaLantern), scale: None, - health_scaling: 0, + health_scaling: Some(0), loadout_config: None, skillset_config: None, ), + duration: None, ) diff --git a/assets/common/abilities/custom/tornado/spin.ron b/assets/common/abilities/custom/tornado/spin.ron index df0320d9c2..de072c981e 100644 --- a/assets/common/abilities/custom/tornado/spin.ron +++ b/assets/common/abilities/custom/tornado/spin.ron @@ -1,13 +1,13 @@ SpinMelee( - buildup_duration: 0.2, - swing_duration: 0.6, - recover_duration: 0.2, - base_damage: 70, - base_poise_damage: 25, - knockback: ( strength: 0.0, direction: Away), + buildup_duration: 0.0, + swing_duration: 0.5, + recover_duration: 0.0, + base_damage: 15000, + base_poise_damage: 200, + knockback: ( strength: 200.0, direction: Away), range: 3.5, damage_effect: None, - energy_cost: 100, + energy_cost: 0, is_infinite: true, movement_behavior: AxeHover, is_interruptible: false, diff --git a/assets/common/items/npc_weapons/unique/tornado.ron b/assets/common/items/npc_weapons/unique/tornado.ron new file mode 100644 index 0000000000..14c1054395 --- /dev/null +++ b/assets/common/items/npc_weapons/unique/tornado.ron @@ -0,0 +1,19 @@ +ItemDef( + name: "Tornado", + description: "Tornado weapon", + kind: Tool(( + kind: Natural, + hands: Two, + stats: Direct(( + equip_time_secs: 0.01, + power: 1.0, + poise_strength: 1.0, + speed: 1.0, + crit_chance: 0.0, + crit_mult: 0.0, + )), + )), + quality: Low, + tags: [], + ability_spec: Some(Custom("Tornado")), +) \ No newline at end of file diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index d224061a8e..f755fe58d5 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -280,7 +280,9 @@ pub enum CharacterAbility { cast_duration: f32, recover_duration: f32, summon_amount: u32, + summon_distance: (f32, f32), summon_info: basic_summon::SummonInfo, + duration: Option<Duration>, }, SelfBuff { buildup_duration: f32, @@ -1737,15 +1739,19 @@ impl From<(&CharacterAbility, AbilityInfo)> for CharacterState { cast_duration, recover_duration, summon_amount, + summon_distance, summon_info, + duration, } => CharacterState::BasicSummon(basic_summon::Data { static_data: basic_summon::StaticData { buildup_duration: Duration::from_secs_f32(*buildup_duration), cast_duration: Duration::from_secs_f32(*cast_duration), recover_duration: Duration::from_secs_f32(*recover_duration), summon_amount: *summon_amount, + summon_distance: *summon_distance, summon_info: *summon_info, ability_info, + duration: *duration, }, summon_count: 0, timer: Duration::default(), diff --git a/common/src/comp/inventory/loadout_builder.rs b/common/src/comp/inventory/loadout_builder.rs index 6f316f1e62..1978cf3fd9 100644 --- a/common/src/comp/inventory/loadout_builder.rs +++ b/common/src/comp/inventory/loadout_builder.rs @@ -306,6 +306,9 @@ fn default_main_tool(body: &Body) -> Item { object::Body::SeaLantern => Some(Item::new_from_asset_expect( "common.items.npc_weapons.unique.tidal_totem", )), + object::Body::Tornado => Some(Item::new_from_asset_expect( + "common.items.npc_weapons.unique.tornado", + )), _ => None, }, Body::BipedSmall(biped_small) => match (biped_small.species, biped_small.body_type) { diff --git a/common/src/event.rs b/common/src/event.rs index d21eee05c5..ffa178fdd4 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -117,7 +117,7 @@ pub enum ServerEvent { pos: comp::Pos, stats: comp::Stats, skill_set: comp::SkillSet, - health: comp::Health, + health: Option<comp::Health>, poise: comp::Poise, loadout: comp::inventory::loadout::Loadout, body: comp::Body, @@ -127,6 +127,7 @@ pub enum ServerEvent { home_chunk: Option<comp::HomeChunk>, drop_item: Option<Item>, rtsim_entity: Option<RtSimEntity>, + projectile: Option<comp::Projectile>, }, CreateShip { pos: comp::Pos, diff --git a/common/src/states/basic_summon.rs b/common/src/states/basic_summon.rs index 6a49e33c60..44c71b9f47 100644 --- a/common/src/states/basic_summon.rs +++ b/common/src/states/basic_summon.rs @@ -2,7 +2,7 @@ use crate::{ comp::{ self, inventory::loadout_builder::{self, LoadoutBuilder}, - Behavior, BehaviorCapability, CharacterState, StateUpdate, + Behavior, BehaviorCapability, CharacterState, Projectile, StateUpdate, }, event::{LocalEvent, ServerEvent}, outcome::Outcome, @@ -11,9 +11,13 @@ use crate::{ behavior::{CharacterBehavior, JoinData}, utils::*, }, + terrain::Block, + vol::ReadVol, }; +use rand::Rng; use serde::{Deserialize, Serialize}; -use std::time::Duration; +use std::{f32::consts::PI, time::Duration}; +use vek::*; /// Separated out to condense update portions of character state #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -26,10 +30,14 @@ pub struct StaticData { pub recover_duration: Duration, /// How many creatures the state should summon pub summon_amount: u32, + /// Range of the summons relative to the summonner + pub summon_distance: (f32, f32), /// Information about the summoned creature pub summon_info: SummonInfo, /// Miscellaneous information about the ability pub ability_info: AbilityInfo, + /// Duration of the summoned entity + pub duration: Option<Duration>, } #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -102,15 +110,67 @@ impl CharacterBehavior for Data { }; let stats = comp::Stats::new("Summon".to_string()); + + let health_scaling = self + .static_data + .summon_info + .health_scaling + .map(|health_scaling| comp::Health::new(body, health_scaling)); + + // Ray cast to check where summon should happen + let summon_frac = + self.summon_count as f32 / self.static_data.summon_amount as f32; + + let length = rand::thread_rng().gen_range( + self.static_data.summon_distance.0..self.static_data.summon_distance.1, + ); + + // Summon in a clockwise fashion + let ray_vector = Vec3::new( + (summon_frac * 2.0 * PI).sin() * length, + (summon_frac * 2.0 * PI).cos() * length, + data.body.eye_height(), + ); + + // Check for collision on the xy plane + let obstacle_xy = data + .terrain + .ray(data.pos.0, data.pos.0 + length * ray_vector) + .until(Block::is_solid) + .cast() + .0; + + let collision_vector = Vec3::new( + data.pos.0.x + (summon_frac * 2.0 * PI).sin() * obstacle_xy, + data.pos.0.y + (summon_frac * 2.0 * PI).cos() * obstacle_xy, + data.pos.0.z, + ); + + // Check for collision in z up to 50 blocks + let obstacle_z = data + .terrain + .ray(collision_vector, collision_vector - Vec3::unit_z() * 50.0) + .until(Block::is_solid) + .cast() + .0; + + // If a duration is specified, create a projectile componenent for the npc + let projectile = self.static_data.duration.map(|duration| Projectile { + hit_solid: Vec::new(), + hit_entity: Vec::new(), + time_left: duration, + owner: Some(*data.uid), + ignore_group: true, + is_sticky: false, + is_point: false, + }); + // Send server event to create npc update.server_events.push_front(ServerEvent::CreateNpc { - pos: *data.pos, + pos: comp::Pos(collision_vector - Vec3::unit_z() * obstacle_z), stats, skill_set, - health: comp::Health::new( - body, - self.static_data.summon_info.health_scaling, - ), + health: health_scaling, poise: comp::Poise::new(body), loadout, body, @@ -129,6 +189,7 @@ impl CharacterBehavior for Data { home_chunk: None, drop_item: None, rtsim_entity: None, + projectile, }); // Send local event used for frontend shenanigans @@ -186,7 +247,7 @@ impl CharacterBehavior for Data { pub struct SummonInfo { body: comp::Body, scale: Option<comp::Scale>, - health_scaling: u16, + health_scaling: Option<u16>, // TODO: use assets for specifying skills and loadout? loadout_config: Option<loadout_builder::Preset>, skillset_config: Option<skillset_builder::Preset>, diff --git a/common/src/states/behavior.rs b/common/src/states/behavior.rs index a1fea34724..d66c827b9a 100644 --- a/common/src/states/behavior.rs +++ b/common/src/states/behavior.rs @@ -5,6 +5,7 @@ use crate::{ InventoryAction, Mass, Melee, Ori, PhysicsState, Pos, SkillSet, StateUpdate, Stats, Vel, }, resources::DeltaTime, + terrain::TerrainGrid, uid::Uid, }; use specs::{ @@ -96,6 +97,7 @@ pub struct JoinData<'a> { pub msm: &'a MaterialStatManifest, pub combo: &'a Combo, pub alignment: Option<&'a comp::Alignment>, + pub terrain: &'a TerrainGrid, } type RestrictedMut<'a, C> = PairedStorage< @@ -128,6 +130,7 @@ pub struct JoinStruct<'a> { pub skill_set: &'a SkillSet, pub combo: &'a Combo, pub alignment: Option<&'a comp::Alignment>, + pub terrain: &'a TerrainGrid, } impl<'a> JoinData<'a> { @@ -161,6 +164,7 @@ impl<'a> JoinData<'a> { msm, combo: j.combo, alignment: j.alignment, + terrain: j.terrain, } } } diff --git a/common/systems/src/character_behavior.rs b/common/systems/src/character_behavior.rs index e7db1e0b36..382067f7d7 100644 --- a/common/systems/src/character_behavior.rs +++ b/common/systems/src/character_behavior.rs @@ -1,6 +1,6 @@ use specs::{ - shred::ResourceId, Entities, Join, LazyUpdate, Read, ReadStorage, SystemData, World, Write, - WriteStorage, + shred::ResourceId, Entities, Join, LazyUpdate, Read, ReadExpect, ReadStorage, SystemData, + World, Write, WriteStorage, }; use common::{ @@ -16,6 +16,7 @@ use common::{ self, behavior::{CharacterBehavior, JoinData, JoinStruct}, }, + terrain::TerrainGrid, uid::Uid, }; use common_ecs::{Job, Origin, Phase, System}; @@ -67,6 +68,7 @@ pub struct ReadData<'a> { msm: Read<'a, MaterialStatManifest>, combos: ReadStorage<'a, Combo>, alignments: ReadStorage<'a, comp::Alignment>, + terrain: ReadExpect<'a, TerrainGrid>, } /// ## Character Behavior System @@ -280,6 +282,7 @@ impl<'a> System<'a> for Sys { skill_set: &skill_set, combo: &combo, alignment: read_data.alignments.get(entity), + terrain: &read_data.terrain, }; for action in actions { diff --git a/server/src/cmd.rs b/server/src/cmd.rs index b8d56c4fbb..fcdd197e1a 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1014,7 +1014,7 @@ fn handle_spawn( pos, comp::Stats::new(get_npc_name(id, npc::BodyType::from_body(body))), comp::SkillSet::default(), - comp::Health::new(body, 1), + Some(comp::Health::new(body, 1)), comp::Poise::new(body), inventory, body, @@ -1116,7 +1116,7 @@ fn handle_spawn_training_dummy( pos, stats, skill_set, - health, + Some(health), poise, Inventory::new_empty(), body, diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index c66317cf0e..014ed849a7 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -53,7 +53,7 @@ pub fn handle_create_npc( pos: Pos, stats: Stats, skill_set: SkillSet, - health: Health, + health: Option<Health>, poise: Poise, loadout: Loadout, body: Body, @@ -63,6 +63,7 @@ pub fn handle_create_npc( drop_item: Option<Item>, home_chunk: Option<HomeChunk>, rtsim_entity: Option<RtSimEntity>, + projectile: Option<Projectile>, ) { let inventory = Inventory::new_with_loadout(loadout); @@ -96,6 +97,12 @@ pub fn handle_create_npc( entity }; + let entity = if let Some(projectile) = projectile { + entity.with(projectile) + } else { + entity + }; + let new_entity = entity.build(); // Add to group system if a pet diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index 7079b3bda3..7ab52e6b60 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -147,6 +147,7 @@ impl Server { home_chunk, drop_item, rtsim_entity, + projectile, } => handle_create_npc( self, pos, @@ -162,6 +163,7 @@ impl Server { drop_item, home_chunk, rtsim_entity, + projectile, ), ServerEvent::CreateShip { pos, diff --git a/server/src/rtsim/tick.rs b/server/src/rtsim/tick.rs index c729445fa9..70f0608f61 100644 --- a/server/src/rtsim/tick.rs +++ b/server/src/rtsim/tick.rs @@ -127,7 +127,7 @@ impl<'a> System<'a> for Sys { pos: comp::Pos(spawn_pos), stats: comp::Stats::new(entity.get_name()), skill_set: comp::SkillSet::default(), - health: comp::Health::new(body, 10), + health: Some(comp::Health::new(body, 10)), loadout: match body { comp::Body::Humanoid(_) => entity.get_loadout(), _ => LoadoutBuilder::new().build(), @@ -146,6 +146,7 @@ impl<'a> System<'a> for Sys { drop_item: None, home_chunk: None, rtsim_entity, + projectile: None, }, }; server_emitter.emit(event); diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index a929d8224d..339ec7b921 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -39,7 +39,7 @@ pub trait StateExt { pos: comp::Pos, stats: comp::Stats, skill_set: comp::SkillSet, - health: comp::Health, + health: Option<comp::Health>, poise: comp::Poise, inventory: comp::Inventory, body: comp::Body, @@ -175,7 +175,7 @@ impl StateExt for State { pos: comp::Pos, stats: comp::Stats, skill_set: comp::SkillSet, - health: comp::Health, + health: Option<comp::Health>, poise: comp::Poise, inventory: comp::Inventory, body: comp::Body, @@ -215,7 +215,7 @@ impl StateExt for State { )) .with(stats) .with(skill_set) - .with(health) + .maybe_with(health) .with(poise) .with(comp::Alignment::Npc) .with(comp::CharacterState::default()) diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index 458c710b28..eb668c7690 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -239,7 +239,7 @@ impl<'a> System<'a> for Sys { loadout_builder.build() }; - let health = comp::Health::new(body, entity.level.unwrap_or(0)); + let health = Some(comp::Health::new(body, entity.level.unwrap_or(0))); let poise = comp::Poise::new(body); let can_speak = match body { @@ -293,6 +293,7 @@ impl<'a> System<'a> for Sys { home_chunk: Some(comp::HomeChunk(key)), drop_item: entity.loot_drop, rtsim_entity: None, + projectile: None, }) } diff --git a/voxygen/anim/src/bird_large/mod.rs b/voxygen/anim/src/bird_large/mod.rs index 1c4c83663e..67e7da5166 100644 --- a/voxygen/anim/src/bird_large/mod.rs +++ b/voxygen/anim/src/bird_large/mod.rs @@ -209,7 +209,7 @@ impl<'a> From<&'a Body> for SkeletonAttr { feed: match (body.species, body.body_type) { (Phoenix, _) => (-0.65), (Cockatrice, _) => (-0.5), - (Roc, _) => (-0.65), + (Roc, _) => (-0.4), }, } }