diff --git a/CHANGELOG.md b/CHANGELOG.md index f758bb5579..51b1bd830a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Seperated character randomization buttons into appearance and name. +- Reworked mindflayer to have unique attacks + ### Removed ### Fixed diff --git a/assets/common/abilities/unique/mindflayer/raiseundead.ron b/assets/common/abilities/unique/mindflayer/raiseundead.ron deleted file mode 100644 index ca0309ede7..0000000000 --- a/assets/common/abilities/unique/mindflayer/raiseundead.ron +++ /dev/null @@ -1 +0,0 @@ -BasicBlock \ No newline at end of file diff --git a/assets/common/abilities/unique/mindflayer/summonminions.ron b/assets/common/abilities/unique/mindflayer/summonminions.ron new file mode 100644 index 0000000000..73d9d78e42 --- /dev/null +++ b/assets/common/abilities/unique/mindflayer/summonminions.ron @@ -0,0 +1,16 @@ +BasicSummon( + buildup_duration: 0.5, + cast_duration: 1.0, + recover_duration: 0.5, + summon_amount: 4, + summon_info: ( + body: QuadrupedMedium(( + species: Darkhound, + body_type: Male, + )), + scale: None, + health_scaling: 0, + loadout_config: None, + skillset_config: None, + ), +) \ No newline at end of file diff --git a/assets/common/abilities/weapon_ability_manifest.ron b/assets/common/abilities/weapon_ability_manifest.ron index d34b8651e0..ec481c4b9c 100644 --- a/assets/common/abilities/weapon_ability_manifest.ron +++ b/assets/common/abilities/weapon_ability_manifest.ron @@ -199,7 +199,7 @@ secondary: "common.abilities.unique.mindflayer.necroticvortex", abilities: [ (None, "common.abilities.unique.mindflayer.dimensionaldoor"), - (None, "common.abilities.unique.mindflayer.raiseundead"), + (None, "common.abilities.unique.mindflayer.summonminions"), ], ), Debug: ( diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index c4808bb94b..6af12a1fdf 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -256,6 +256,13 @@ pub enum CharacterAbility { recover_duration: f32, max_range: f32, }, + BasicSummon { + buildup_duration: f32, + cast_duration: f32, + recover_duration: f32, + summon_amount: u32, + summon_info: basic_summon::SummonInfo, + }, } impl Default for CharacterAbility { @@ -544,6 +551,17 @@ impl CharacterAbility { *buildup_duration /= speed; *recover_duration /= speed; }, + BasicSummon { + 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 } @@ -570,7 +588,7 @@ impl CharacterAbility { 0 } }, - BasicBlock | Boost { .. } | ComboMelee { .. } | Blink { .. } => 0, + BasicBlock | Boost { .. } | ComboMelee { .. } | Blink { .. } | BasicSummon { .. } => 0, } } @@ -1586,6 +1604,25 @@ impl From<(&CharacterAbility, AbilityInfo)> for CharacterState { timer: Duration::default(), stage_section: StageSection::Buildup, }), + CharacterAbility::BasicSummon { + buildup_duration, + cast_duration, + recover_duration, + summon_amount, + summon_info, + } => 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_info: *summon_info, + ability_info, + }, + summon_count: 0, + timer: Duration::default(), + stage_section: StageSection::Buildup, + }), } } } diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs index 759a81ce6e..f0a9b9db51 100644 --- a/common/src/comp/character_state.rs +++ b/common/src/comp/character_state.rs @@ -95,6 +95,8 @@ pub enum CharacterState { HealingBeam(healing_beam::Data), /// A short teleport that targets either a position or entity Blink(blink::Data), + /// Summons creatures that fight for the caster + BasicSummon(basic_summon::Data), } impl CharacterState { diff --git a/common/src/comp/health.rs b/common/src/comp/health.rs index d13e612844..61234f26bf 100644 --- a/common/src/comp/health.rs +++ b/common/src/comp/health.rs @@ -34,7 +34,7 @@ pub enum HealthSource { Unknown, } -#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] pub struct Health { current: u32, base_max: u32, diff --git a/common/src/comp/inventory/loadout_builder.rs b/common/src/comp/inventory/loadout_builder.rs index 9b91368886..36108bfe53 100644 --- a/common/src/comp/inventory/loadout_builder.rs +++ b/common/src/comp/inventory/loadout_builder.rs @@ -12,6 +12,7 @@ use crate::{ trade::{Good, SiteInformation}, }; use rand::Rng; +use serde::{Deserialize, Serialize}; /// Builder for character Loadouts, containing weapon and armour items belonging /// to a character, along with some helper methods for loading Items and @@ -34,7 +35,7 @@ use rand::Rng; #[derive(Clone)] pub struct LoadoutBuilder(Loadout); -#[derive(Copy, Clone)] +#[derive(Copy, Clone, PartialEq, Serialize, Deserialize, Debug)] pub enum LoadoutConfig { Adlet, Gnarling, diff --git a/common/src/skillset_builder.rs b/common/src/skillset_builder.rs index 33c28ef77c..8f58ae0a88 100644 --- a/common/src/skillset_builder.rs +++ b/common/src/skillset_builder.rs @@ -4,9 +4,10 @@ use crate::comp::{ AxeSkill, BowSkill, HammerSkill, Skill, SkillGroupKind, SkillSet, StaffSkill, SwordSkill, }, }; +use serde::{Deserialize, Serialize}; use tracing::warn; -#[derive(Copy, Clone)] +#[derive(Copy, Clone, PartialEq, Serialize, Deserialize, Debug)] pub enum SkillSetConfig { Adlet, Gnarling, diff --git a/common/src/states/basic_summon.rs b/common/src/states/basic_summon.rs new file mode 100644 index 0000000000..b46555b6ab --- /dev/null +++ b/common/src/states/basic_summon.rs @@ -0,0 +1,176 @@ +use crate::{ + comp::{ + self, + inventory::loadout_builder::{LoadoutBuilder, LoadoutConfig}, + CharacterState, StateUpdate, + }, + event::ServerEvent, + skillset_builder::{SkillSetBuilder, SkillSetConfig}, + states::{ + behavior::{CharacterBehavior, JoinData}, + utils::*, + }, +}; +use serde::{Deserialize, Serialize}; +use std::time::Duration; + +/// 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, + /// How many creatures the state should summon + pub summon_amount: u32, + /// Information about the summoned creature + pub summon_info: SummonInfo, + /// 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, + /// How many creatures have been summoned + pub summon_count: u32, + /// 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: &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::BasicSummon(Data { + timer: self + .timer + .checked_add(Duration::from_secs_f32(data.dt.0)) + .unwrap_or_default(), + ..*self + }); + } else { + // Transitions to recover section of stage + update.character = CharacterState::BasicSummon(Data { + timer: Duration::default(), + stage_section: StageSection::Cast, + ..*self + }); + } + }, + StageSection::Cast => { + if self.timer < self.static_data.cast_duration + || self.summon_count < self.static_data.summon_amount + { + if self.timer + > self.static_data.cast_duration * self.summon_count + / self.static_data.summon_amount + { + let body = self.static_data.summon_info.body; + + let mut stats = comp::Stats::new("Summon".to_string()); + stats.skill_set = SkillSetBuilder::build_skillset( + &None, + self.static_data.summon_info.skillset_config, + ) + .build(); + let loadout = LoadoutBuilder::build_loadout( + body, + None, + self.static_data.summon_info.loadout_config, + None, + ) + .build(); + + update.server_events.push_front(ServerEvent::CreateNpc { + pos: *data.pos, + stats, + health: comp::Health::new( + body, + self.static_data.summon_info.health_scaling, + ), + poise: comp::Poise::new(body), + loadout, + body, + agent: Some(comp::Agent::new(None, false, None, &body, true)), + alignment: comp::Alignment::Owned(*data.uid), + scale: self + .static_data + .summon_info + .scale + .unwrap_or(comp::Scale(1.0)), + home_chunk: None, + drop_item: None, + rtsim_entity: None, + }); + + update.character = CharacterState::BasicSummon(Data { + timer: self + .timer + .checked_add(Duration::from_secs_f32(data.dt.0)) + .unwrap_or_default(), + summon_count: self.summon_count + 1, + ..*self + }); + } else { + // Cast + update.character = CharacterState::BasicSummon(Data { + timer: self + .timer + .checked_add(Duration::from_secs_f32(data.dt.0)) + .unwrap_or_default(), + ..*self + }); + } + } else { + // Transitions to recover section of stage + update.character = CharacterState::BasicSummon(Data { + timer: Duration::default(), + stage_section: StageSection::Recover, + ..*self + }); + } + }, + StageSection::Recover => { + if self.timer < self.static_data.recover_duration { + // Recovery + update.character = CharacterState::BasicSummon(Data { + timer: self + .timer + .checked_add(Duration::from_secs_f32(data.dt.0)) + .unwrap_or_default(), + ..*self + }); + } else { + // Done + update.character = CharacterState::Wielding; + } + }, + _ => { + // If it somehow ends up in an incorrect stage section + update.character = CharacterState::Wielding; + }, + } + + update + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct SummonInfo { + body: comp::Body, + scale: Option, + health_scaling: u16, + loadout_config: Option, + skillset_config: Option, +} diff --git a/common/src/states/blink.rs b/common/src/states/blink.rs index 1814eef99c..884b612555 100644 --- a/common/src/states/blink.rs +++ b/common/src/states/blink.rs @@ -12,9 +12,13 @@ use std::time::Duration; /// 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 recovers for pub recover_duration: Duration, + /// What the max range of the teleport is pub max_range: f32, + /// Miscellaneous information about the ability pub ability_info: AbilityInfo, } diff --git a/common/src/states/mod.rs b/common/src/states/mod.rs index 519a6f432a..15cb6c72d6 100644 --- a/common/src/states/mod.rs +++ b/common/src/states/mod.rs @@ -3,6 +3,7 @@ pub mod basic_beam; pub mod basic_block; pub mod basic_melee; pub mod basic_ranged; +pub mod basic_summon; pub mod behavior; pub mod blink; pub mod boost; diff --git a/common/src/states/spin_melee.rs b/common/src/states/spin_melee.rs index 134c2a45ea..947909feca 100644 --- a/common/src/states/spin_melee.rs +++ b/common/src/states/spin_melee.rs @@ -47,7 +47,7 @@ pub struct StaticData { pub num_spins: u32, /// What key is used to press ability pub ability_info: AbilityInfo, - /// Used to specify the beam to the frontend + /// Used to specify the melee attack to the frontend pub specifier: Option, } diff --git a/common/sys/src/character_behavior.rs b/common/sys/src/character_behavior.rs index 2a02313a64..acd301a83f 100644 --- a/common/sys/src/character_behavior.rs +++ b/common/sys/src/character_behavior.rs @@ -303,6 +303,7 @@ impl<'a> System<'a> for Sys { CharacterState::BasicAura(data) => data.handle_event(&j, action), CharacterState::HealingBeam(data) => data.handle_event(&j, action), CharacterState::Blink(data) => data.handle_event(&j, action), + CharacterState::BasicSummon(data) => data.handle_event(&j, action), }; local_emitter.append(&mut state_update.local_events); server_emitter.append(&mut state_update.server_events); @@ -356,6 +357,7 @@ impl<'a> System<'a> for Sys { CharacterState::BasicAura(data) => data.behavior(&j), CharacterState::HealingBeam(data) => data.behavior(&j), CharacterState::Blink(data) => data.behavior(&j), + CharacterState::BasicSummon(data) => data.behavior(&j), }; local_emitter.append(&mut state_update.local_events); diff --git a/common/sys/src/stats.rs b/common/sys/src/stats.rs index 6a92a08887..189e8f6cf4 100644 --- a/common/sys/src/stats.rs +++ b/common/sys/src/stats.rs @@ -247,7 +247,8 @@ impl<'a> System<'a> for Sys { | CharacterState::BasicBeam { .. } | CharacterState::BasicAura { .. } | CharacterState::HealingBeam { .. } - | CharacterState::Blink { .. } => { + | CharacterState::Blink { .. } + | CharacterState::BasicSummon { .. } => { if energy.get_unchecked().regen_rate != 0.0 { energy.get_mut_unchecked().regen_rate = 0.0 } diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index 7fc859e3ef..ed9c515d32 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -1,4 +1,4 @@ -use crate::{sys, Server, StateExt}; +use crate::{client::Client, sys, Server, StateExt}; use common::{ character::CharacterId, comp::{ @@ -6,15 +6,16 @@ use common::{ aura::{Aura, AuraKind, AuraTarget}, beam, buff::{BuffCategory, BuffData, BuffKind, BuffSource}, - group, inventory::loadout::Loadout, shockwave, Agent, Alignment, Body, Gravity, Health, HomeChunk, Inventory, Item, ItemDrop, LightEmitter, Object, Ori, Poise, Pos, Projectile, Scale, Stats, Vel, WaypointArea, }, outcome::Outcome, rtsim::RtSimEntity, + uid::Uid, util::Dir, }; +use common_net::{msg::ServerGeneral, sync::WorldSyncExt}; use specs::{Builder, Entity as EcsEntity, WorldExt}; use std::time::Duration; use vek::{Rgb, Vec3}; @@ -59,15 +60,6 @@ pub fn handle_create_npc( home_chunk: Option, rtsim_entity: Option, ) { - let group = match alignment { - Alignment::Wild => None, - Alignment::Passive => None, - Alignment::Enemy => Some(group::ENEMY), - Alignment::Npc | Alignment::Tame => Some(group::NPC), - // TODO: handle - Alignment::Owned(_) => None, - }; - let inventory = Inventory::new_with_loadout(loadout); let entity = server @@ -76,12 +68,6 @@ pub fn handle_create_npc( .with(scale) .with(alignment); - let entity = if let Some(group) = group { - entity.with(group) - } else { - entity - }; - let entity = if let Some(agent) = agent.into() { entity.with(agent) } else { @@ -106,7 +92,45 @@ pub fn handle_create_npc( entity }; - entity.build(); + let new_entity = entity.build(); + + // Add to group system if a pet + if let comp::Alignment::Owned(owner_uid) = alignment { + let state = server.state(); + let clients = state.ecs().read_storage::(); + let uids = state.ecs().read_storage::(); + let mut group_manager = state.ecs().write_resource::(); + if let Some(owner) = state.ecs().entity_from_uid(owner_uid.into()) { + group_manager.new_pet( + new_entity, + owner, + &mut state.ecs().write_storage(), + &state.ecs().entities(), + &state.ecs().read_storage(), + &uids, + &mut |entity, group_change| { + clients + .get(entity) + .and_then(|c| { + group_change + .try_map(|e| uids.get(e).copied()) + .map(|g| (g, c)) + }) + .map(|(g, c)| { + c.send_fallible(ServerGeneral::GroupUpdate(g)); + }); + }, + ); + } + } else if let Some(group) = match alignment { + comp::Alignment::Wild => None, + comp::Alignment::Passive => None, + comp::Alignment::Enemy => Some(comp::group::ENEMY), + comp::Alignment::Npc | comp::Alignment::Tame => Some(comp::group::NPC), + comp::Alignment::Owned(_) => unreachable!(), + } { + let _ = server.state.ecs().write_storage().insert(new_entity, group); + } } #[allow(clippy::too_many_arguments)]