diff --git a/assets/common/abilities/ability_set_manifest.ron b/assets/common/abilities/ability_set_manifest.ron index a142621547..ec7fd52fd4 100644 --- a/assets/common/abilities/ability_set_manifest.ron +++ b/assets/common/abilities/ability_set_manifest.ron @@ -930,6 +930,7 @@ secondary: Simple(None, "common.abilities.debug.upboost"), abilities: [ Simple(None, "common.abilities.debug.possess"), + Simple(None, "common.abilities.debug.evolve"), ], ), Tool(Farming): ( diff --git a/assets/common/abilities/debug/evolve.ron b/assets/common/abilities/debug/evolve.ron new file mode 100644 index 0000000000..128f53cf4c --- /dev/null +++ b/assets/common/abilities/debug/evolve.ron @@ -0,0 +1,6 @@ +Transform( + buildup_duration: 2.0, + recover_duration: 0.5, + target: "common.entity.wild.peaceful.crab", + specifier: Some(Evolve), +) diff --git a/assets/voxygen/i18n/en/hud/ability.ftl b/assets/voxygen/i18n/en/hud/ability.ftl index b46856e1d9..d2d23a745f 100644 --- a/assets/voxygen/i18n/en/hud/ability.ftl +++ b/assets/voxygen/i18n/en/hud/ability.ftl @@ -1,5 +1,7 @@ common-abilities-debug-possess = Possessing Arrow .desc = Shoots a poisonous arrow. Lets you control your target. +common-abilities-debug-evolve = Evolve + .desc = You become your better self. common-abilities-hammer-leap = Smash of Doom .desc = An AOE attack with knockback. Leaps to position of cursor. common-abilities-bow-shotgun = Burst diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index 46167a7d5c..f1ae20c54b 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -646,6 +646,7 @@ impl From<&CharacterState> for CharacterAbilityType { | CharacterState::UseItem(_) | CharacterState::SpriteInteract(_) | CharacterState::Skate(_) + | CharacterState::Transform(_) | CharacterState::Wallrun(_) => Self::Other, } } @@ -997,6 +998,15 @@ pub enum CharacterAbility { #[serde(default)] meta: AbilityMeta, }, + Transform { + buildup_duration: f32, + recover_duration: f32, + target: String, + #[serde(default)] + specifier: Option, + #[serde(default)] + meta: AbilityMeta, + }, } impl Default for CharacterAbility { @@ -1115,7 +1125,8 @@ impl CharacterAbility { | CharacterAbility::Blink { .. } | CharacterAbility::Music { .. } | CharacterAbility::BasicSummon { .. } - | CharacterAbility::SpriteSummon { .. } => true, + | CharacterAbility::SpriteSummon { .. } + | CharacterAbility::Transform { .. } => true, } } @@ -1662,6 +1673,16 @@ impl CharacterAbility { *energy_cost /= stats.energy_efficiency; *melee_constructor = melee_constructor.adjusted_by_stats(stats); }, + Transform { + ref mut buildup_duration, + ref mut recover_duration, + target: _, + specifier: _, + meta: _, + } => { + *buildup_duration /= stats.speed; + *recover_duration /= stats.speed; + }, } self } @@ -1702,7 +1723,8 @@ impl CharacterAbility { | Blink { .. } | Music { .. } | BasicSummon { .. } - | SpriteSummon { .. } => 0.0, + | SpriteSummon { .. } + | Transform { .. } => 0.0, } } @@ -1750,7 +1772,8 @@ impl CharacterAbility { | Blink { .. } | Music { .. } | BasicSummon { .. } - | SpriteSummon { .. } => 0, + | SpriteSummon { .. } + | Transform { .. } => 0, } } @@ -1782,7 +1805,8 @@ impl CharacterAbility { | Music { meta, .. } | DiveMelee { meta, .. } | RiposteMelee { meta, .. } - | RapidMelee { meta, .. } => *meta, + | RapidMelee { meta, .. } + | Transform { meta, .. } => *meta, } } @@ -2935,6 +2959,23 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState { stage_section: StageSection::Buildup, exhausted: false, }), + CharacterAbility::Transform { + buildup_duration, + recover_duration, + target, + specifier, + meta: _, + } => CharacterState::Transform(transform::Data { + static_data: transform::StaticData { + buildup_duration: Duration::from_secs_f32(*buildup_duration), + recover_duration: Duration::from_secs_f32(*recover_duration), + specifier: *specifier, + target: target.to_owned(), + ability_info, + }, + timer: Duration::default(), + stage_section: StageSection::Buildup, + }), } } } diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs index 9aa88a8251..a60bb68f71 100644 --- a/common/src/comp/character_state.rs +++ b/common/src/comp/character_state.rs @@ -51,6 +51,7 @@ event_emitters! { energy_change: event::EnergyChangeEvent, knockback: event::KnockbackEvent, sprite_light: event::ToggleSpriteLightEvent, + transform: event::TransformEvent, } } @@ -172,6 +173,8 @@ pub enum CharacterState { /// A series of consecutive, identical attacks that only go through buildup /// and recover once for the entire state RapidMelee(rapid_melee::Data), + /// Transforms an entity into another + Transform(transform::Data), } impl CharacterState { @@ -518,6 +521,7 @@ impl CharacterState { CharacterState::DiveMelee(data) => data.behavior(j, output_events), CharacterState::RiposteMelee(data) => data.behavior(j, output_events), CharacterState::RapidMelee(data) => data.behavior(j, output_events), + CharacterState::Transform(data) => data.behavior(j, output_events), } } @@ -573,6 +577,7 @@ impl CharacterState { CharacterState::DiveMelee(data) => data.handle_event(j, output_events, action), CharacterState::RiposteMelee(data) => data.handle_event(j, output_events, action), CharacterState::RapidMelee(data) => data.handle_event(j, output_events, action), + CharacterState::Transform(data) => data.handle_event(j, output_events, action), } } @@ -625,6 +630,7 @@ impl CharacterState { CharacterState::DiveMelee(data) => Some(data.static_data.ability_info), CharacterState::RiposteMelee(data) => Some(data.static_data.ability_info), CharacterState::RapidMelee(data) => Some(data.static_data.ability_info), + CharacterState::Transform(data) => Some(data.static_data.ability_info), } } @@ -669,6 +675,7 @@ impl CharacterState { CharacterState::DiveMelee(data) => Some(data.stage_section), CharacterState::RiposteMelee(data) => Some(data.stage_section), CharacterState::RapidMelee(data) => Some(data.stage_section), + CharacterState::Transform(data) => Some(data.stage_section), } } @@ -857,6 +864,11 @@ impl CharacterState { recover: Some(data.static_data.recover_duration), ..Default::default() }), + CharacterState::Transform(data) => Some(DurationsInfo { + buildup: Some(data.static_data.buildup_duration), + recover: Some(data.static_data.recover_duration), + ..Default::default() + }), } } @@ -901,6 +913,7 @@ impl CharacterState { CharacterState::DiveMelee(data) => Some(data.timer), CharacterState::RiposteMelee(data) => Some(data.timer), CharacterState::RapidMelee(data) => Some(data.timer), + CharacterState::Transform(data) => Some(data.timer), } } @@ -960,6 +973,7 @@ impl CharacterState { CharacterState::DiveMelee(_) => Some(AttackSource::Melee), CharacterState::RiposteMelee(_) => Some(AttackSource::Melee), CharacterState::RapidMelee(_) => Some(AttackSource::Melee), + CharacterState::Transform(_) => None, } } } diff --git a/common/src/states/mod.rs b/common/src/states/mod.rs index 31d96c0625..5ad0cdd153 100644 --- a/common/src/states/mod.rs +++ b/common/src/states/mod.rs @@ -35,6 +35,7 @@ pub mod sprite_interact; pub mod sprite_summon; pub mod stunned; pub mod talk; +pub mod transform; pub mod use_item; pub mod utils; pub mod wallrun; diff --git a/common/src/states/transform.rs b/common/src/states/transform.rs new file mode 100644 index 0000000000..b8a98cbfc3 --- /dev/null +++ b/common/src/states/transform.rs @@ -0,0 +1,125 @@ +use std::time::Duration; + +use common_assets::AssetExt; +use rand::thread_rng; +use serde::{Deserialize, Serialize}; +use vek::Vec3; + +use crate::{ + comp::{item::Reagent, CharacterState, StateUpdate}, + event::TransformEvent, + generation::{EntityConfig, EntityInfo}, + states::utils::{end_ability, tick_attack_or_default}, +}; + +use super::{ + behavior::CharacterBehavior, + utils::{AbilityInfo, StageSection}, +}; + +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +pub enum FrontendSpecifier { + Evolve, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct StaticData { + /// How long until state has until transformation + pub buildup_duration: Duration, + /// How long the state has until exiting + pub recover_duration: Duration, + /// The entity configuration you will be transformed into + pub target: String, + pub ability_info: AbilityInfo, + /// Used to specify the transformation to the frontend + pub specifier: Option, +} + +#[derive(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, +} + +impl CharacterBehavior for Data { + fn behavior( + &self, + data: &super::behavior::JoinData, + output_events: &mut crate::comp::character_state::OutputEvents, + ) -> crate::comp::StateUpdate { + let mut update = StateUpdate::from(data); + match self.stage_section { + StageSection::Buildup => { + // Tick the timer as long as buildup hasn't finihsed + if self.timer < self.static_data.buildup_duration { + update.character = CharacterState::Transform(Data { + static_data: self.static_data.clone(), + timer: tick_attack_or_default(data, self.timer, None), + ..*self + }); + // Buildup finished, start transformation + } else { + let Ok(entity_config) = EntityConfig::load(&self.static_data.target) else { + end_ability(data, &mut update); + return update; + }; + + let entity_info = EntityInfo::at(Vec3::zero()).with_entity_config( + entity_config.read().clone(), + Some(&self.static_data.target), + &mut thread_rng(), + None, + ); + + // Handle frontend events + if let Some(specifier) = self.static_data.specifier { + match specifier { + FrontendSpecifier::Evolve => { + output_events.emit_local(crate::event::LocalEvent::CreateOutcome( + crate::outcome::Outcome::Explosion { + pos: data.pos.0, + power: 5.0, + radius: 2.0, + is_attack: false, + reagent: Some(Reagent::White), + }, + )) + }, + } + } + + output_events.emit_server(TransformEvent(*data.uid, entity_info)); + update.character = CharacterState::Transform(Data { + static_data: self.static_data.clone(), + timer: Duration::default(), + stage_section: StageSection::Recover, + }); + } + }, + StageSection::Recover => { + // Wait for recovery period to finish + if self.timer < self.static_data.recover_duration { + update.character = CharacterState::Transform(Data { + static_data: self.static_data.clone(), + timer: tick_attack_or_default(data, self.timer, None), + ..*self + }); + } else { + // End the ability after recovery is done + end_ability(data, &mut update); + } + }, + _ => { + // If we somehow ended up in an incorrect character state, end the ability + end_ability(data, &mut update); + }, + } + + update + } +} diff --git a/common/systems/src/stats.rs b/common/systems/src/stats.rs index 2758e38f8e..830e234d55 100644 --- a/common/systems/src/stats.rs +++ b/common/systems/src/stats.rs @@ -209,6 +209,7 @@ impl<'a> System<'a> for Sys { | CharacterState::Stunned(_) | CharacterState::BasicBlock(_) | CharacterState::UseItem(_) + | CharacterState::Transform(_) | CharacterState::SpriteInteract(_) => {}, } }); diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index f24f7b76f9..333d22b923 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -1384,6 +1384,38 @@ impl ParticleMgr { }); } }, + CharacterState::Transform(data) => { + if matches!(data.stage_section, StageSection::Buildup) + && let Some(specifier) = data.static_data.specifier + { + match specifier { + states::transform::FrontendSpecifier::Evolve => { + self.particles.resize_with( + self.particles.len() + + usize::from( + self.scheduler.heartbeats(Duration::from_millis(10)), + ), + || { + let start_pos = interpolated.pos + + (Vec2::unit_y() + * rng.gen::() + * body.max_radius()) + .rotated_z(rng.gen_range(0.0..(PI * 2.0))) + .with_z(body.height() * rng.gen::()); + + Particle::new_directed( + Duration::from_millis(100), + time, + ParticleMode::BarrelOrgan, + start_pos, + start_pos + Vec3::unit_z() * 2.0, + ) + }, + ) + }, + } + } + }, _ => {}, } }