diff --git a/assets/common/abilities/custom/harvester/ensnaringvines.ron b/assets/common/abilities/custom/harvester/ensnaringvines.ron index 88b2b2d48a..ede5f831dc 100644 --- a/assets/common/abilities/custom/harvester/ensnaringvines.ron +++ b/assets/common/abilities/custom/harvester/ensnaringvines.ron @@ -1,15 +1,7 @@ -BasicRanged( - energy_cost: 0, - buildup_duration: 0.5, - recover_duration: 0.8, - projectile: ExplodingPumpkin( - damage: 500.0, - knockback: 25.0, - radius: 10.0, - ), - projectile_body: Object(Pumpkin), - projectile_light: None, - projectile_speed: 30.0, - num_projectiles: 1, - projectile_spread: 0.0, -) +SpriteSummon( + buildup_duration: 0.3, + cast_duration: 1.0, + recover_duration: 0.2, + sprite: EnsnaringVines, + summon_distance: (0, 25), +) \ No newline at end of file diff --git a/assets/voxygen/voxel/sprite/misc/ensnaring_vines.vox b/assets/voxygen/voxel/sprite/misc/ensnaring_vines.vox new file mode 100644 index 0000000000..2bf31ebd2d --- /dev/null +++ b/assets/voxygen/voxel/sprite/misc/ensnaring_vines.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35a24b6eefb92f621b565fc14a7d502e22cecc416ff45b9856c621c39abf17ec +size 1480 diff --git a/assets/voxygen/voxel/sprite_manifest.ron b/assets/voxygen/voxel/sprite_manifest.ron index 8ca8d3bbb2..36e7be1102 100644 --- a/assets/voxygen/voxel/sprite_manifest.ron +++ b/assets/voxygen/voxel/sprite_manifest.ron @@ -3247,4 +3247,14 @@ CookingPot: Some(( ], wind_sway: 0.0, )), +EnsnaringVines: Some(( + variations: [ + ( + model: "voxygen.voxel.sprite.misc.ensnaring_vines", + offset: (-5.0, -6.5, 0.0), + lod_axes: (0.0, 0.0, 0.0), + ), + ], + wind_sway: 0.0, +)), ) diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index 4d5c4aaf50..9931ac54c6 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -10,6 +10,7 @@ use crate::{ utils::{AbilityInfo, StageSection}, *, }, + terrain::SpriteKind, }; use serde::{Deserialize, Serialize}; use std::time::Duration; @@ -294,6 +295,13 @@ pub enum CharacterAbility { buff_duration: Option, energy_cost: f32, }, + SpriteSummon { + buildup_duration: f32, + cast_duration: f32, + recover_duration: f32, + sprite: SpriteKind, + summon_distance: (f32, f32), + }, } impl Default for CharacterAbility { @@ -365,7 +373,8 @@ impl CharacterAbility { | CharacterAbility::Boost { .. } | CharacterAbility::BasicBeam { .. } | CharacterAbility::Blink { .. } - | CharacterAbility::BasicSummon { .. } => true, + | CharacterAbility::BasicSummon { .. } + | CharacterAbility::SpriteSummon { .. } => true, } } @@ -627,6 +636,17 @@ impl CharacterAbility { *cast_duration /= speed; *recover_duration /= speed; }, + SpriteSummon { + ref mut buildup_duration, + ref mut cast_duration, + ref mut recover_duration, + .. + } => { + // TODO: Figure out how/if power should affect this + *buildup_duration /= speed; + *cast_duration /= speed; + *recover_duration /= speed; + }, } self } @@ -655,7 +675,11 @@ impl CharacterAbility { 0 } }, - Boost { .. } | ComboMelee { .. } | Blink { .. } | BasicSummon { .. } => 0, + Boost { .. } + | ComboMelee { .. } + | Blink { .. } + | BasicSummon { .. } + | SpriteSummon { .. } => 0, } } @@ -1781,6 +1805,25 @@ impl From<(&CharacterAbility, AbilityInfo)> for CharacterState { timer: Duration::default(), stage_section: StageSection::Buildup, }), + CharacterAbility::SpriteSummon { + buildup_duration, + cast_duration, + recover_duration, + sprite, + summon_distance, + } => CharacterState::SpriteSummon(sprite_summon::Data { + static_data: sprite_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), + sprite: *sprite, + summon_distance: *summon_distance, + ability_info, + }, + timer: Duration::default(), + stage_section: StageSection::Buildup, + achieved_radius: 0, + }), } } } diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs index 86db71191b..87102015bb 100644 --- a/common/src/comp/character_state.rs +++ b/common/src/comp/character_state.rs @@ -103,6 +103,8 @@ pub enum CharacterState { BasicSummon(basic_summon::Data), /// Inserts a buff on the caster SelfBuff(self_buff::Data), + /// Creates sprites around the caster + SpriteSummon(sprite_summon::Data), } impl CharacterState { @@ -125,6 +127,9 @@ impl CharacterState { | CharacterState::BasicAura(_) | CharacterState::HealingBeam(_) | CharacterState::SelfBuff(_) + | CharacterState::Blink(_) + | CharacterState::BasicSummon(_) + | CharacterState::SpriteSummon(_) ) } @@ -149,6 +154,9 @@ impl CharacterState { | CharacterState::BasicAura(_) | CharacterState::HealingBeam(_) | CharacterState::SelfBuff(_) + | CharacterState::Blink(_) + | CharacterState::BasicSummon(_) + | CharacterState::SpriteSummon(_) ) } diff --git a/common/src/event.rs b/common/src/event.rs index 1056da4716..617726765c 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -9,6 +9,7 @@ use crate::{ }, outcome::Outcome, rtsim::RtSimEntity, + terrain::SpriteKind, trade::{TradeAction, TradeId}, uid::Uid, util::Dir, @@ -181,6 +182,10 @@ pub enum ServerEvent { Sound { sound: Sound, }, + CreateSprite { + pos: Vec3, + sprite: SpriteKind, + }, } pub struct EventBus { diff --git a/common/src/spiral.rs b/common/src/spiral.rs index 7b0cb2da51..fc763a8278 100644 --- a/common/src/spiral.rs +++ b/common/src/spiral.rs @@ -10,12 +10,22 @@ pub struct Spiral2d { impl Spiral2d { #[allow(clippy::new_without_default)] // TODO: Pending review in #587 + /// Creates a new spiral starting at the origin pub fn new() -> Self { Self { layer: 0, i: 0 } } + /// Creates an iterator over points in a spiral starting at the origin and + /// going out to some radius pub fn radius(self, radius: i32) -> impl Iterator> { self.take((radius * 2 + 1).pow(2) as usize) .filter(move |pos| pos.magnitude_squared() < (radius + 1).pow(2)) } + + /// Creates an iterator over points in the edge of a circle of some radius + pub fn edge_radius(self, radius: i32) -> impl Iterator> { + self.take((radius * 2 + 1).pow(2) as usize) + .filter(move |pos| pos.magnitude_squared() < (radius + 1).pow(2)) + .filter(move |pos| pos.magnitude_squared() >= radius.pow(2)) + } } impl Iterator for Spiral2d { diff --git a/common/src/states/mod.rs b/common/src/states/mod.rs index 30b86e3766..0e63dd48d4 100644 --- a/common/src/states/mod.rs +++ b/common/src/states/mod.rs @@ -26,6 +26,7 @@ pub mod shockwave; pub mod sit; pub mod sneak; pub mod spin_melee; +pub mod sprite_summon; pub mod stunned; pub mod talk; pub mod utils; diff --git a/common/src/states/sprite_summon.rs b/common/src/states/sprite_summon.rs new file mode 100644 index 0000000000..76ec4f0062 --- /dev/null +++ b/common/src/states/sprite_summon.rs @@ -0,0 +1,156 @@ +use crate::{ + comp::{CharacterState, StateUpdate}, + event::ServerEvent, + spiral::Spiral2d, + states::{ + behavior::{CharacterBehavior, JoinData}, + utils::*, + }, + terrain::{Block, SpriteKind}, + vol::ReadVol, +}; +use serde::{Deserialize, Serialize}; +use std::time::Duration; +use vek::*; + +/// Separated out to condense update portions of character state +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct StaticData { + /// How long the state builds up for + pub buildup_duration: Duration, + /// How long the state is casting for + pub cast_duration: Duration, + /// How long the state recovers for + pub recover_duration: Duration, + /// What kind of sprite is created by this state + pub sprite: SpriteKind, + /// Range that sprites are created relative to the summonner + pub summon_distance: (f32, f32), + /// Miscellaneous information about the ability + pub ability_info: AbilityInfo, +} + +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Data { + /// Struct containing data that does not change over the course of the + /// character state + pub static_data: StaticData, + /// Timer for each stage + pub timer: Duration, + /// What section the character stage is in + pub stage_section: StageSection, + /// What radius of sprites have already been summoned + pub achieved_radius: i32, +} + +impl CharacterBehavior for Data { + fn behavior(&self, data: &JoinData) -> StateUpdate { + let mut update = StateUpdate::from(data); + + match self.stage_section { + StageSection::Buildup => { + if self.timer < self.static_data.buildup_duration { + // Build up + update.character = CharacterState::SpriteSummon(Data { + timer: tick_attack_or_default(data, self.timer, None), + ..*self + }); + } else { + // Transitions to recover section of stage + update.character = CharacterState::SpriteSummon(Data { + timer: Duration::default(), + stage_section: StageSection::Cast, + ..*self + }); + } + }, + StageSection::Cast => { + if self.timer < self.static_data.cast_duration { + let timer_frac = + self.timer.as_secs_f32() / self.static_data.cast_duration.as_secs_f32(); + + // Determines distance from summoner sprites should be created. Goes outward + // with time. + let summon_distance = timer_frac + * (self.static_data.summon_distance.1 - self.static_data.summon_distance.0) + + self.static_data.summon_distance.0; + let summon_distance = summon_distance.round() as i32; + + // Only summons sprites if summon distance is greater than achieved radius + if summon_distance > self.achieved_radius { + // Creates a spiral iterator for the newly achieved radius + let spiral = Spiral2d::new().edge_radius(summon_distance); + for point in spiral { + // The coordinates of where the sprite is created + let sprite_pos = Vec3::new( + data.pos.0.x.floor() as i32 + point.x, + data.pos.0.y.floor() as i32 + point.y, + data.pos.0.z.floor() as i32, + ); + + // Check for collision in z up to 25 blocks up or down + let obstacle_z = data + .terrain + .ray( + sprite_pos.map(|x| x as f32 + 0.5) + Vec3::unit_z() * 25.0, + sprite_pos.map(|x| x as f32 + 0.5) - Vec3::unit_z() * 25.0, + ) + .until(|b| { + Block::is_solid(b) + && !matches!( + b.get_sprite(), + Some(SpriteKind::EnsnaringVines) + ) + }) + .cast() + .0; + + // z height relative to caster + let z = sprite_pos.z + (25.5 - obstacle_z).ceil() as i32; + + // Location sprite will be created + let sprite_pos = Vec3::new(sprite_pos.x as i32, sprite_pos.y as i32, z); + + // Send server event to create sprite + update.server_events.push_front(ServerEvent::CreateSprite { + pos: sprite_pos, + sprite: self.static_data.sprite, + }); + } + } + + update.character = CharacterState::SpriteSummon(Data { + timer: tick_attack_or_default(data, self.timer, None), + achieved_radius: summon_distance, + ..*self + }); + } else { + // Transitions to recover section of stage + update.character = CharacterState::SpriteSummon(Data { + timer: Duration::default(), + stage_section: StageSection::Recover, + ..*self + }); + } + }, + StageSection::Recover => { + if self.timer < self.static_data.recover_duration { + // Recovery + update.character = CharacterState::SpriteSummon(Data { + timer: tick_attack_or_default(data, self.timer, None), + ..*self + }); + } else { + // Done + update.character = CharacterState::Wielding; + } + }, + _ => { + // If it somehow ends up in an incorrect stage section + update.character = CharacterState::Wielding; + }, + } + + update + } +} diff --git a/common/src/terrain/sprite.rs b/common/src/terrain/sprite.rs index e35a99035f..021efcce82 100644 --- a/common/src/terrain/sprite.rs +++ b/common/src/terrain/sprite.rs @@ -173,6 +173,7 @@ make_case_elim!( CrystalLow = 0x92, CeilingMushroom = 0x93, Orb = 0x94, + EnsnaringVines = 0x95, } ); @@ -256,6 +257,7 @@ impl SpriteKind { | SpriteKind::Tin | SpriteKind::Silver | SpriteKind::Gold => 0.6, + SpriteKind::EnsnaringVines => 0.1, _ => return None, }) } diff --git a/common/systems/src/character_behavior.rs b/common/systems/src/character_behavior.rs index 382067f7d7..1782ef693f 100644 --- a/common/systems/src/character_behavior.rs +++ b/common/systems/src/character_behavior.rs @@ -331,6 +331,7 @@ impl<'a> System<'a> for Sys { CharacterState::Blink(data) => data.handle_event(&j, action), CharacterState::BasicSummon(data) => data.handle_event(&j, action), CharacterState::SelfBuff(data) => data.handle_event(&j, action), + CharacterState::SpriteSummon(data) => data.handle_event(&j, action), }; local_emitter.append(&mut state_update.local_events); server_emitter.append(&mut state_update.server_events); @@ -386,6 +387,7 @@ impl<'a> System<'a> for Sys { CharacterState::Blink(data) => data.behavior(&j), CharacterState::BasicSummon(data) => data.behavior(&j), CharacterState::SelfBuff(data) => data.behavior(&j), + CharacterState::SpriteSummon(data) => data.behavior(&j), }; local_emitter.append(&mut state_update.local_events); diff --git a/common/systems/src/stats.rs b/common/systems/src/stats.rs index ed6c9b3a9f..50a7578e8c 100644 --- a/common/systems/src/stats.rs +++ b/common/systems/src/stats.rs @@ -279,7 +279,8 @@ impl<'a> System<'a> for Sys { | CharacterState::HealingBeam { .. } | CharacterState::Blink { .. } | CharacterState::BasicSummon { .. } - | CharacterState::SelfBuff { .. } => { + | CharacterState::SelfBuff { .. } + | CharacterState::SpriteSummon { .. } => { if energy.get_unchecked().regen_rate != 0.0 { energy.get_mut_unchecked().regen_rate = 0.0 } diff --git a/server/src/events/interaction.rs b/server/src/events/interaction.rs index acd935f9a6..bf5047154b 100644 --- a/server/src/events/interaction.rs +++ b/server/src/events/interaction.rs @@ -16,6 +16,7 @@ use common::{ }, consts::{MAX_MOUNT_RANGE, SOUND_TRAVEL_DIST_PER_VOLUME}, outcome::Outcome, + terrain::{Block, SpriteKind}, uid::Uid, vol::ReadVol, }; @@ -418,3 +419,17 @@ pub fn handle_sound(server: &mut Server, sound: &Sound) { ecs.write_resource::>().push(outcome); } } + +pub fn handle_create_sprite(server: &mut Server, pos: Vec3, sprite: SpriteKind) { + let state = server.state_mut(); + if state.can_set_block(pos) { + let block = state.terrain().get(pos).ok().copied(); + if block.map_or(false, |b| (*b).is_air()) { + let new_block = state + .get_block(pos) + .unwrap_or_else(|| Block::air(SpriteKind::Empty)) + .with_sprite(sprite); + server.state.set_block(pos, new_block); + } + } +} diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index 38c73e6c52..43093f391f 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -13,8 +13,8 @@ use entity_manipulation::{ use group_manip::handle_group; use information::handle_site_info; use interaction::{ - handle_lantern, handle_mine_block, handle_mount, handle_npc_interaction, handle_possess, - handle_sound, handle_unmount, + handle_create_sprite, handle_lantern, handle_mine_block, handle_mount, handle_npc_interaction, + handle_possess, handle_sound, handle_unmount, }; use inventory_manip::handle_inventory; use invite::{handle_invite, handle_invite_response}; @@ -221,6 +221,9 @@ impl Server { self.state.create_safezone(range, pos).build(); }, ServerEvent::Sound { sound } => handle_sound(self, &sound), + ServerEvent::CreateSprite { pos, sprite } => { + handle_create_sprite(self, pos, sprite) + }, } }