diff --git a/assets/common/abilities/sword/cleaving_dive.ron b/assets/common/abilities/sword/cleaving_dive.ron index 5659fb767c..6239263c9d 100644 --- a/assets/common/abilities/sword/cleaving_dive.ron +++ b/assets/common/abilities/sword/cleaving_dive.ron @@ -6,11 +6,17 @@ DiveMelee( recover_duration: 0.3, melee_constructor: ( kind: Slash( - damage: 30, + damage: 20, poise: 0, knockback: 0, energy_regen: 10, ), + scaled: Some(Slash( + damage: 10, + poise: 0, + knockback: 0, + energy_regen: 10, + )), range: 6.0, angle: 15.0, multi_target: true, diff --git a/assets/common/abilities/sword/parrying_counter.ron b/assets/common/abilities/sword/parrying_counter.ron index 507cb8e4b1..17f8660403 100644 --- a/assets/common/abilities/sword/parrying_counter.ron +++ b/assets/common/abilities/sword/parrying_counter.ron @@ -10,6 +10,7 @@ ComboMelee2( ), range: 6.0, angle: 5.0, + damage_effect: Some(BuildupsVulnerable), ), buildup_duration: 0.05, swing_duration: 0.1, diff --git a/assets/voxygen/i18n/en/buff.ftl b/assets/voxygen/i18n/en/buff.ftl index c4d1d9e1eb..16d54192ff 100644 --- a/assets/voxygen/i18n/en/buff.ftl +++ b/assets/voxygen/i18n/en/buff.ftl @@ -64,6 +64,9 @@ buff-desc-ensnared = Vines grasp at your legs, impeding your movement. ## Fortitude buff-title-fortitude = Fortitude buff-desc-fortitude = You can withstand staggers. +## Parried +buff-title-parried = Parried +buff-desc-parried = You were parried and now are slow to recover. ## Util buff-text-over_seconds = over { $dur_secs } seconds buff-text-for_seconds = for { $dur_secs } seconds diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 9af913f81f..5d5db82595 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -154,6 +154,7 @@ lazy_static! { BuffKind::Poisoned => "poisoned", BuffKind::Hastened => "hastened", BuffKind::Fortitude => "fortitude", + BuffKind::Parried => "parried", }; let mut buff_parser = HashMap::new(); for kind in BuffKind::iter() { diff --git a/common/src/combat.rs b/common/src/combat.rs index f15c8ee1d7..53e5c20e0d 100644 --- a/common/src/combat.rs +++ b/common/src/combat.rs @@ -12,6 +12,7 @@ use crate::{ }, event::ServerEvent, outcome::Outcome, + states::utils::StageSection, uid::{Uid, UidAllocator}, util::Dir, }; @@ -441,6 +442,16 @@ impl Attack { } } }, + CombatEffect::BuildupsVulnerable => { + if target.char_state.map_or(false, |cs| { + matches!(cs.stage_section(), Some(StageSection::Buildup)) + }) { + emit(ServerEvent::HealthChange { + entity: target.entity, + change, + }); + } + }, } } } @@ -605,6 +616,8 @@ impl Attack { } } }, + // Only has an effect when attached to a damage + CombatEffect::BuildupsVulnerable => {}, } } } @@ -738,6 +751,11 @@ pub enum CombatEffect { Combo(i32), // If the attack kills the target, reset the melee attack ResetMelee, + // If the attack hits the target while they are in the buildup portion of a character state, + // deal double damage Only has an effect when attached to a damage, otherwise does nothing + // if only attached to the attack TODO: Maybe try to make it do something if tied to + // attack, not sure if it should double count in that instance? + BuildupsVulnerable, } #[cfg(not(target_arch = "wasm32"))] diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index aacca400af..bcab5e6652 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -2507,19 +2507,21 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState { recover_duration, melee_constructor, energy_cost: _, - vertical_speed: _, + vertical_speed, meta: _, } => CharacterState::DiveMelee(dive_melee::Data { static_data: dive_melee::StaticData { movement_duration: Duration::from_secs_f32(*movement_duration), swing_duration: Duration::from_secs_f32(*swing_duration), recover_duration: Duration::from_secs_f32(*recover_duration), + vertical_speed: *vertical_speed, melee_constructor: *melee_constructor, ability_info, }, timer: Duration::default(), stage_section: StageSection::Movement, exhausted: false, + max_vertical_speed: 0.0, }), CharacterAbility::RiposteMelee { energy_cost: _, @@ -2598,9 +2600,13 @@ pub enum SwordStance { bitflags::bitflags! { #[derive(Default, Serialize, Deserialize)] pub struct Capability: u8 { + // Allows rolls to interrupt the ability at any point, not just during buildup const ROLL_INTERRUPT = 0b00000001; + // Allows blocking to interrupt the ability at any point const BLOCK_INTERRUPT = 0b00000010; + // When the ability is in the buyildup section, it counts as a parry const BUILDUP_PARRIES = 0b00000100; + // When in the ability, an entity only receives half as much poise damage const POISE_RESISTANT = 0b00001000; } } diff --git a/common/src/comp/buff.rs b/common/src/comp/buff.rs index 26088496df..2672cb65d4 100644 --- a/common/src/comp/buff.rs +++ b/common/src/comp/buff.rs @@ -87,6 +87,10 @@ pub enum BuffKind { /// Drain stamina to a creature over time /// Strength should be the energy per second of the debuff Poisoned, + /// Results from having an attack parried. + /// Causes your attack speed to be slower to emulate the recover duration of + /// an ability being lengthened. + Parried, } #[cfg(not(target_arch = "wasm32"))] @@ -113,7 +117,8 @@ impl BuffKind { | BuffKind::Frozen | BuffKind::Wet | BuffKind::Ensnared - | BuffKind::Poisoned => false, + | BuffKind::Poisoned + | BuffKind::Parried => false, } } @@ -390,6 +395,7 @@ impl Buff { vec![BuffEffect::PoiseReduction(data.strength)], data.duration, ), + BuffKind::Parried => (vec![BuffEffect::AttackSpeed(0.5)], data.duration), }; Buff { kind, diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs index ddbf779352..0c3cf4178e 100644 --- a/common/src/comp/character_state.rs +++ b/common/src/comp/character_state.rs @@ -13,7 +13,7 @@ use crate::{ }; use serde::{Deserialize, Serialize}; use specs::{Component, DerefFlaggedStorage}; -use std::collections::BTreeMap; +use std::{collections::BTreeMap, time::Duration}; use strum::Display; /// Data returned from character behavior fn's to Character Behavior System. @@ -571,6 +571,199 @@ impl CharacterState { CharacterState::RapidMelee(data) => Some(data.stage_section), } } + + pub fn durations(&self) -> Option { + match &self { + CharacterState::Idle(_) => None, + CharacterState::Talk => None, + CharacterState::Climb(_) => None, + CharacterState::Wallrun(_) => None, + CharacterState::Skate(_) => None, + CharacterState::Glide(_) => None, + CharacterState::GlideWield(_) => None, + CharacterState::Stunned(data) => Some(DurationsInfo { + buildup: Some(data.static_data.buildup_duration), + recover: Some(data.static_data.recover_duration), + ..Default::default() + }), + CharacterState::Sit => None, + CharacterState::Dance => None, + CharacterState::BasicBlock(data) => Some(DurationsInfo { + buildup: Some(data.static_data.buildup_duration), + recover: Some(data.static_data.recover_duration), + ..Default::default() + }), + CharacterState::Roll(data) => Some(DurationsInfo { + buildup: Some(data.static_data.buildup_duration), + recover: Some(data.static_data.recover_duration), + movement: Some(data.static_data.movement_duration), + ..Default::default() + }), + CharacterState::Wielding(_) => None, + CharacterState::Equipping(_) => None, + CharacterState::ComboMelee(data) => { + let stage_index = data.stage_index(); + let stage = data.static_data.stage_data[stage_index]; + Some(DurationsInfo { + buildup: Some(stage.base_buildup_duration), + action: Some(stage.base_swing_duration), + recover: Some(stage.base_recover_duration), + ..Default::default() + }) + }, + CharacterState::ComboMelee2(data) => { + let strike = data.strike_data(); + Some(DurationsInfo { + buildup: Some(strike.buildup_duration), + action: Some(strike.swing_duration), + recover: Some(strike.recover_duration), + ..Default::default() + }) + }, + CharacterState::BasicMelee(data) => Some(DurationsInfo { + buildup: Some(data.static_data.buildup_duration), + action: Some(data.static_data.swing_duration), + recover: Some(data.static_data.recover_duration), + ..Default::default() + }), + CharacterState::BasicRanged(data) => Some(DurationsInfo { + buildup: Some(data.static_data.buildup_duration), + recover: Some(data.static_data.recover_duration), + ..Default::default() + }), + CharacterState::Boost(data) => Some(DurationsInfo { + movement: Some(data.static_data.movement_duration), + ..Default::default() + }), + CharacterState::DashMelee(data) => Some(DurationsInfo { + buildup: Some(data.static_data.buildup_duration), + action: Some(data.static_data.swing_duration), + recover: Some(data.static_data.recover_duration), + charge: Some(data.static_data.charge_duration), + ..Default::default() + }), + CharacterState::LeapMelee(data) => Some(DurationsInfo { + buildup: Some(data.static_data.buildup_duration), + action: Some(data.static_data.swing_duration), + recover: Some(data.static_data.recover_duration), + movement: Some(data.static_data.movement_duration), + ..Default::default() + }), + CharacterState::SpinMelee(data) => Some(DurationsInfo { + buildup: Some(data.static_data.buildup_duration), + action: Some(data.static_data.swing_duration), + recover: Some(data.static_data.recover_duration), + ..Default::default() + }), + CharacterState::ChargedMelee(data) => Some(DurationsInfo { + action: Some(data.static_data.swing_duration), + recover: Some(data.static_data.recover_duration), + charge: Some(data.static_data.charge_duration), + ..Default::default() + }), + CharacterState::ChargedRanged(data) => Some(DurationsInfo { + buildup: Some(data.static_data.buildup_duration), + recover: Some(data.static_data.recover_duration), + charge: Some(data.static_data.charge_duration), + ..Default::default() + }), + CharacterState::RepeaterRanged(data) => Some(DurationsInfo { + buildup: Some(data.static_data.buildup_duration), + action: Some(data.static_data.shoot_duration), + recover: Some(data.static_data.recover_duration), + ..Default::default() + }), + CharacterState::Shockwave(data) => Some(DurationsInfo { + buildup: Some(data.static_data.buildup_duration), + action: Some(data.static_data.swing_duration), + recover: Some(data.static_data.recover_duration), + ..Default::default() + }), + CharacterState::BasicBeam(data) => Some(DurationsInfo { + buildup: Some(data.static_data.buildup_duration), + recover: Some(data.static_data.recover_duration), + ..Default::default() + }), + CharacterState::BasicAura(data) => Some(DurationsInfo { + buildup: Some(data.static_data.buildup_duration), + action: Some(data.static_data.cast_duration), + recover: Some(data.static_data.recover_duration), + ..Default::default() + }), + CharacterState::Blink(data) => Some(DurationsInfo { + buildup: Some(data.static_data.buildup_duration), + recover: Some(data.static_data.recover_duration), + ..Default::default() + }), + CharacterState::BasicSummon(data) => Some(DurationsInfo { + buildup: Some(data.static_data.buildup_duration), + action: Some(data.static_data.cast_duration), + recover: Some(data.static_data.recover_duration), + ..Default::default() + }), + CharacterState::SelfBuff(data) => Some(DurationsInfo { + buildup: Some(data.static_data.buildup_duration), + action: Some(data.static_data.cast_duration), + recover: Some(data.static_data.recover_duration), + ..Default::default() + }), + CharacterState::SpriteSummon(data) => Some(DurationsInfo { + buildup: Some(data.static_data.buildup_duration), + action: Some(data.static_data.cast_duration), + recover: Some(data.static_data.recover_duration), + ..Default::default() + }), + CharacterState::UseItem(data) => Some(DurationsInfo { + buildup: Some(data.static_data.buildup_duration), + action: Some(data.static_data.use_duration), + recover: Some(data.static_data.recover_duration), + ..Default::default() + }), + CharacterState::SpriteInteract(data) => Some(DurationsInfo { + buildup: Some(data.static_data.buildup_duration), + action: Some(data.static_data.use_duration), + recover: Some(data.static_data.recover_duration), + ..Default::default() + }), + CharacterState::FinisherMelee(data) => Some(DurationsInfo { + buildup: Some(data.static_data.buildup_duration), + action: Some(data.static_data.swing_duration), + recover: Some(data.static_data.recover_duration), + ..Default::default() + }), + CharacterState::Music(data) => Some(DurationsInfo { + action: Some(data.static_data.play_duration), + ..Default::default() + }), + CharacterState::DiveMelee(data) => Some(DurationsInfo { + action: Some(data.static_data.swing_duration), + recover: Some(data.static_data.recover_duration), + movement: Some(data.static_data.movement_duration), + ..Default::default() + }), + CharacterState::RiposteMelee(data) => Some(DurationsInfo { + buildup: Some(data.static_data.buildup_duration), + action: Some(data.static_data.swing_duration), + recover: Some(data.static_data.recover_duration), + ..Default::default() + }), + CharacterState::RapidMelee(data) => Some(DurationsInfo { + buildup: Some(data.static_data.buildup_duration), + action: Some(data.static_data.swing_duration), + recover: Some(data.static_data.recover_duration), + ..Default::default() + }), + } + } +} + +#[derive(Default, Copy, Clone)] +pub struct DurationsInfo { + pub buildup: Option, + pub action: Option, + pub recover: Option, + pub movement: Option, + pub charge: Option, } impl Default for CharacterState { diff --git a/common/src/states/combo_melee.rs b/common/src/states/combo_melee.rs index 9ecce143a0..d588c1854f 100644 --- a/common/src/states/combo_melee.rs +++ b/common/src/states/combo_melee.rs @@ -162,14 +162,7 @@ impl CharacterBehavior for Data { handle_move(data, &mut update, 0.4); - // Index should be `self.stage - 1`, however in cases of client-server desync - // this can cause panics. This ensures that `self.stage - 1` is valid, and if it - // isn't, index of 0 is used, which is always safe. - let stage_index = self - .static_data - .stage_data - .get(self.stage as usize - 1) - .map_or(0, |_| self.stage as usize - 1); + let stage_index = self.stage_index(); let speed_modifier = 1.0 + self.static_data.max_speed_increase @@ -372,6 +365,19 @@ impl CharacterBehavior for Data { } } +impl Data { + /// Index should be `self.stage - 1`, however in cases of client-server desync + /// this can cause panics. This ensures that `self.stage - 1` is valid, and if it + /// isn't, index of 0 is used, which is always safe. + pub fn stage_index(&self) -> usize { + self + .static_data + .stage_data + .get(self.stage as usize - 1) + .map_or(0, |_| self.stage as usize - 1) + } +} + fn reset_state( data: &Data, join: &JoinData, diff --git a/common/src/states/combo_melee2.rs b/common/src/states/combo_melee2.rs index 1e83b477bd..bea8d28146 100644 --- a/common/src/states/combo_melee2.rs +++ b/common/src/states/combo_melee2.rs @@ -122,8 +122,7 @@ impl CharacterBehavior for Data { handle_move(data, &mut update, 0.7); handle_interrupts(data, &mut update, Some(ability_input)); - let strike_data = - self.static_data.strikes[self.completed_strikes % self.static_data.strikes.len()]; + let strike_data = self.strike_data(); match self.stage_section { Some(StageSection::Buildup) => { @@ -149,10 +148,8 @@ impl CharacterBehavior for Data { } if input_is_pressed(data, ability_input) { if let CharacterState::ComboMelee2(c) = &mut update.character { - // Only allow next strike if attack is a stance or has multiple strikes that - // have not finished yet - c.start_next_strike = c.static_data.is_stance - || (c.completed_strikes + 1) < c.static_data.strikes.len(); + // Only have the next strike skip the recover period of this strike if not every strike in the combo is complete yet + c.start_next_strike = (c.completed_strikes + 1) < c.static_data.strikes.len(); } } if self.timer.as_secs_f32() @@ -251,6 +248,12 @@ impl CharacterBehavior for Data { } } +impl Data { + pub fn strike_data(&self) -> &Strike { + &self.static_data.strikes[self.completed_strikes % self.static_data.strikes.len()] + } +} + fn next_strike(update: &mut StateUpdate) { if let CharacterState::ComboMelee2(c) = &mut update.character { if update diff --git a/common/src/states/dive_melee.rs b/common/src/states/dive_melee.rs index 1e37f5dae1..dab7621f14 100644 --- a/common/src/states/dive_melee.rs +++ b/common/src/states/dive_melee.rs @@ -18,6 +18,8 @@ pub struct StaticData { pub swing_duration: Duration, /// How long the state has until exiting pub recover_duration: Duration, + /// The minimum vertical speed the state needed + pub vertical_speed: f32, /// Used to construct the Melee attack pub melee_constructor: MeleeConstructor, /// What key is used to press ability @@ -35,12 +37,18 @@ pub struct Data { pub stage_section: StageSection, /// Whether the attack can deal more damage pub exhausted: bool, + /// The maximum negative vertical velocity achieved during the state + pub max_vertical_speed: f32, } impl CharacterBehavior for Data { fn behavior(&self, data: &JoinData, _output_events: &mut OutputEvents) -> StateUpdate { let mut update = StateUpdate::from(data); + if let CharacterState::DiveMelee(c) = &mut update.character { + c.max_vertical_speed = c.max_vertical_speed.max(-data.vel.0.z); + } + match self.stage_section { StageSection::Movement => { if data.physics.on_ground.is_some() { @@ -67,11 +75,13 @@ impl CharacterBehavior for Data { // Attack let crit_data = get_crit_data(data, self.static_data.ability_info); let buff_strength = get_buff_strength(data, self.static_data.ability_info); + let scaling = self.max_vertical_speed / self.static_data.vertical_speed; data.updater.insert( data.entity, self.static_data .melee_constructor + .handle_scaling(scaling) .create_melee(crit_data, buff_strength), ); diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index a3740ac163..09ae83dc5d 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -42,7 +42,7 @@ use rand_distr::Distribution; use specs::{ join::Join, saveload::MarkerAllocator, Builder, Entity as EcsEntity, Entity, WorldExt, }; -use std::{collections::HashMap, iter, time::Duration}; +use std::{collections::HashMap, iter, ops::Mul, time::Duration}; use tracing::{debug, error}; use vek::{Vec2, Vec3}; @@ -1224,8 +1224,9 @@ pub fn handle_combo_change(server: &Server, entity: EcsEntity, change: i32) { } } -pub fn handle_parry_hook(server: &Server, defender: EcsEntity, _attacker: Option) { +pub fn handle_parry_hook(server: &Server, defender: EcsEntity, attacker: Option) { let ecs = &server.state.ecs(); + let server_eventbus = ecs.read_resource::>(); // Reset character state of defender if let Some(mut char_state) = ecs .write_storage::() @@ -1247,7 +1248,36 @@ pub fn handle_parry_hook(server: &Server, defender: EcsEntity, _attacker: Option const PARRY_REWARD: f32 = 5.0; energy.change_by(PARRY_REWARD); } - // TODO: Some penalties for attacker + + if let Some(attacker) = attacker { + if let Some(char_state) = ecs.read_storage::().get(attacker) { + // By having a duration twice as long as either the recovery duration or 0.5 s, + // causes recover duration to effectively be either doubled or increased by 0.5 + // s when a buff is applied that halves their attack speed. + let duration = char_state + .durations() + .and_then(|durs| durs.recover) + .map_or(0.5, |dur| dur.as_secs_f32()) + .max(0.5) + .mul(2.0); + let data = buff::BuffData::new(1.0, Some(Duration::from_secs_f32(duration))); + let source = if let Some(uid) = ecs.read_storage::().get(defender) { + BuffSource::Character { by: *uid } + } else { + BuffSource::World + }; + let buff = buff::Buff::new( + BuffKind::Parried, + data, + vec![buff::BuffCategory::Physical], + source, + ); + server_eventbus.emit_now(ServerEvent::Buff { + entity: attacker, + buff_change: buff::BuffChange::Add(buff), + }); + } + } } pub fn handle_teleport_to(server: &Server, entity: EcsEntity, target: Uid, max_range: Option) { diff --git a/voxygen/i18n-helpers/src/lib.rs b/voxygen/i18n-helpers/src/lib.rs index 66717159b3..d2a6087064 100644 --- a/voxygen/i18n-helpers/src/lib.rs +++ b/voxygen/i18n-helpers/src/lib.rs @@ -106,7 +106,7 @@ pub fn localize_chat_message( tracing::error!("Player was killed by a positive buff!"); "hud-outcome-mysterious" }, - BuffKind::Wet | BuffKind::Ensnared | BuffKind::Poisoned => { + BuffKind::Wet | BuffKind::Ensnared | BuffKind::Poisoned | BuffKind::Parried => { tracing::error!("Player was killed by a debuff that doesn't do damage!"); "hud-outcome-mysterious" }, diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index eb2092717c..dba6a36ed7 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -4621,6 +4621,8 @@ pub fn get_buff_image(buff: BuffKind, imgs: &Imgs) -> conrod_core::image::Id { BuffKind::Wet => imgs.debuff_wet_0, BuffKind::Ensnared => imgs.debuff_ensnared_0, BuffKind::Poisoned => imgs.debuff_poisoned_0, + // TODO: Get unique icon + BuffKind::Parried => imgs.debuff_crippled_0, } } @@ -4652,6 +4654,7 @@ pub fn get_buff_title(buff: BuffKind, localized_strings: &Localization) -> Cow localized_strings.get_msg("buff-title-wet"), BuffKind::Ensnared { .. } => localized_strings.get_msg("buff-title-ensnared"), BuffKind::Poisoned { .. } => localized_strings.get_msg("buff-title-poisoned"), + BuffKind::Parried { .. } => localized_strings.get_msg("buff-title-parried"), } } @@ -4687,6 +4690,7 @@ pub fn get_buff_desc(buff: BuffKind, data: BuffData, localized_strings: &Localiz BuffKind::Wet { .. } => localized_strings.get_msg("buff-desc-wet"), BuffKind::Ensnared { .. } => localized_strings.get_msg("buff-desc-ensnared"), BuffKind::Poisoned { .. } => localized_strings.get_msg("buff-desc-poisoned"), + BuffKind::Parried { .. } => localized_strings.get_msg("buff-desc-parried"), } } diff --git a/voxygen/src/hud/util.rs b/voxygen/src/hud/util.rs index 5f77fc81ef..8d32dbc9e0 100644 --- a/voxygen/src/hud/util.rs +++ b/voxygen/src/hud/util.rs @@ -184,7 +184,8 @@ pub fn consumable_desc(effects: &[Effect], i18n: &Localization) -> Vec { | BuffKind::Ensnared | BuffKind::Poisoned | BuffKind::Hastened - | BuffKind::Fortitude => Cow::Borrowed(""), + | BuffKind::Fortitude + | BuffKind::Parried => Cow::Borrowed(""), }; write!(&mut description, "{}", buff_desc).unwrap(); @@ -215,7 +216,8 @@ pub fn consumable_desc(effects: &[Effect], i18n: &Localization) -> Vec { | BuffKind::Ensnared | BuffKind::Poisoned | BuffKind::Hastened - | BuffKind::Fortitude => Cow::Borrowed(""), + | BuffKind::Fortitude + | BuffKind::Parried => Cow::Borrowed(""), } } else if let BuffKind::Saturation | BuffKind::Regeneration | BuffKind::EnergyRegen = buff.kind