diff --git a/assets/common/abilities/ability_set_manifest.ron b/assets/common/abilities/ability_set_manifest.ron index 6eb609f9e5..ac2a77a4ce 100644 --- a/assets/common/abilities/ability_set_manifest.ron +++ b/assets/common/abilities/ability_set_manifest.ron @@ -200,7 +200,7 @@ Simple(Hammer(Tremor), "common.abilities.hammer.tremor"), Simple(Hammer(VigorousBash), "common.abilities.hammer.vigorous_bash"), Simple(Hammer(Retaliate), "common.abilities.hammer.retaliate"), - // Simple(Hammer(SpineCracker), "common.abilities.hammer.spine_cracker"), + Simple(Hammer(SpineCracker), "common.abilities.hammer.spine_cracker"), // Simple(Hammer(Breach), "common.abilities.hammer.breach"), // Contextualized( // pseudo_id: "common.abilities.hammer.iron_tempest", diff --git a/assets/common/abilities/hammer/spine_cracker.ron b/assets/common/abilities/hammer/spine_cracker.ron new file mode 100644 index 0000000000..23ec67d73a --- /dev/null +++ b/assets/common/abilities/hammer/spine_cracker.ron @@ -0,0 +1,20 @@ +FinisherMelee( + energy_cost: 0, + buildup_duration: 0.1, + swing_duration: 0.1, + recover_duration: 0.3, + melee_constructor: ( + kind: Bash( + damage: 20, + poise: 0, + knockback: 0, + energy_regen: 0, + ), + range: 4.0, + angle: 15.0, + attack_effect: Some((Poise(50), BehindTarget)), + precision_flank_multiplier: 2.0, + ), + minimum_combo: 10, + combo_consumption: Cost, +) diff --git a/assets/voxygen/element/skills/hammer/spine_cracker.png b/assets/voxygen/element/skills/hammer/spine_cracker.png new file mode 100644 index 0000000000..62b32b0f06 --- /dev/null +++ b/assets/voxygen/element/skills/hammer/spine_cracker.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2cd660b77f09cc4ce6131ae67aa6fc9e571ad8ca2605b7de5ff660e9480c82b +size 1124 diff --git a/assets/voxygen/i18n/en/hud/ability.ftl b/assets/voxygen/i18n/en/hud/ability.ftl index 100005392c..ecf1ac2555 100644 --- a/assets/voxygen/i18n/en/hud/ability.ftl +++ b/assets/voxygen/i18n/en/hud/ability.ftl @@ -407,3 +407,7 @@ common-abilities-hammer-dual_intercept = Intercept common-abilities-hammer-retaliate = Retaliate .desc = After blocking an attack, retaliate with a heavy strike back. +common-abilities-hammer-spine_cracker = Spine Cracker + .desc = + When you foe turns their back to you, strike them hard in the back, targeting the weak part of their spine. + diff --git a/common/src/combat.rs b/common/src/combat.rs index cd51ab262e..06b0efe7e3 100644 --- a/common/src/combat.rs +++ b/common/src/combat.rs @@ -655,6 +655,14 @@ impl Attack { CombatRequirement::TargetPoised => { target.char_state.map_or(false, |cs| cs.is_stunned()) }, + CombatRequirement::BehindTarget => { + const REQUIRED_ANGLE: f32 = 45.0; + if let Some(ori) = target.ori { + ori.look_vec().angle_between(dir.with_z(0.0)) < REQUIRED_ANGLE + } else { + false + } + }, }); if requirements_met { is_applied = true; @@ -1014,6 +1022,7 @@ pub enum CombatRequirement { Combo(u32), TargetHasBuff(BuffKind), TargetPoised, + BehindTarget, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] @@ -1630,11 +1639,19 @@ pub fn compute_poise_resilience( } /// Used to compute the precision multiplier achieved by flanking a target -pub fn precision_mult_from_flank(attack_dir: Vec3, target_ori: Option<&Ori>) -> Option { +pub fn precision_mult_from_flank( + attack_dir: Vec3, + target_ori: Option<&Ori>, + precision_flank_multiplier: f32, +) -> Option { let angle = target_ori.map(|t_ori| t_ori.look_dir().angle_between(attack_dir)); match angle { - Some(angle) if angle < FULL_FLANK_ANGLE => Some(MAX_BACK_FLANK_PRECISION), - Some(angle) if angle < PARTIAL_FLANK_ANGLE => Some(MAX_SIDE_FLANK_PRECISION), + Some(angle) if angle < FULL_FLANK_ANGLE => { + Some(MAX_BACK_FLANK_PRECISION * precision_flank_multiplier) + }, + Some(angle) if angle < PARTIAL_FLANK_ANGLE => { + Some(MAX_SIDE_FLANK_PRECISION * precision_flank_multiplier) + }, Some(_) | None => None, } } diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index 4dfec94f0a..d6fbef19b6 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -1110,8 +1110,10 @@ impl Default for CharacterAbility { angle: 15.0, multi_target: None, damage_effect: None, + attack_effect: None, simultaneous_hits: 1, custom_combo: None, + precision_flank_multiplier: 1.0, }, ori_modifier: 1.0, frontend_specifier: None, diff --git a/common/src/comp/melee.rs b/common/src/comp/melee.rs index 341bc3f704..f808e8d35d 100644 --- a/common/src/comp/melee.rs +++ b/common/src/comp/melee.rs @@ -23,6 +23,7 @@ pub struct Melee { pub multi_target: Option, pub break_block: Option<(Vec3, Option)>, pub simultaneous_hits: u32, + pub precision_flank_multiplier: f32, } #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -51,6 +52,7 @@ impl Component for Melee { } fn default_simultaneous_hits() -> u32 { 1 } +fn default_precision_flank_multiplier() -> f32 { 1.0 } #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Scaled { @@ -73,9 +75,12 @@ pub struct MeleeConstructor { pub angle: f32, pub multi_target: Option, pub damage_effect: Option, + pub attack_effect: Option<(CombatEffect, CombatRequirement)>, #[serde(default = "default_simultaneous_hits")] pub simultaneous_hits: u32, pub custom_combo: Option, + #[serde(default = "default_precision_flank_multiplier")] + pub precision_flank_multiplier: f32, } #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -354,6 +359,14 @@ impl MeleeConstructor { }, }; + let attack = if let Some((effect, requirement)) = self.attack_effect { + let effect = AttackEffect::new(Some(GroupTarget::OutOfGroup), effect) + .with_requirement(requirement); + attack.with_effect(effect) + } else { + attack + }; + let attack = match self.custom_combo { None => attack.with_combo_increment(), Some(CustomCombo { @@ -377,6 +390,7 @@ impl MeleeConstructor { multi_target: self.multi_target, break_block: None, simultaneous_hits: self.simultaneous_hits, + precision_flank_multiplier: self.precision_flank_multiplier, } } diff --git a/common/src/states/finisher_melee.rs b/common/src/states/finisher_melee.rs index 98324a3181..b38faf6f11 100644 --- a/common/src/states/finisher_melee.rs +++ b/common/src/states/finisher_melee.rs @@ -74,9 +74,11 @@ impl CharacterBehavior for Data { c.exhausted = true; } - self.static_data - .combo_consumption - .consume(data, output_events); + self.static_data.combo_consumption.consume( + data, + output_events, + self.static_data.minimum_combo, + ); let mut melee_constructor = self.static_data.melee_constructor; if let Some(scaling) = self.static_data.scaling { diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index 83b00220be..9c0a53ca92 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -1732,14 +1732,16 @@ pub enum ComboConsumption { #[default] All, Half, + Cost, } impl ComboConsumption { - pub fn consume(&self, data: &JoinData, output_events: &mut OutputEvents) { + pub fn consume(&self, data: &JoinData, output_events: &mut OutputEvents, cost: u32) { let combo = data.combo.map_or(0, |c| c.counter()); let to_consume = match self { Self::All => combo, Self::Half => (combo + 1) / 2, + Self::Cost => cost, }; output_events.emit_server(ComboChangeEvent { entity: data.entity, diff --git a/common/systems/src/beam.rs b/common/systems/src/beam.rs index bf299bd98b..a2e24b837f 100644 --- a/common/systems/src/beam.rs +++ b/common/systems/src/beam.rs @@ -263,6 +263,7 @@ impl<'a> System<'a> for Sys { let precision_from_flank = combat::precision_mult_from_flank( beam.bezier.ctrl - beam.bezier.start, target_info.ori, + 1.0, ); let precision_from_time = { diff --git a/common/systems/src/melee.rs b/common/systems/src/melee.rs index 2a6419e9ea..30c6af5fa9 100644 --- a/common/systems/src/melee.rs +++ b/common/systems/src/melee.rs @@ -248,6 +248,7 @@ impl<'a> System<'a> for Sys { .try_normalized() .unwrap_or(ori.look_vec()), target_ori, + melee_attack.precision_flank_multiplier, ); let precision_from_poise = { diff --git a/common/systems/src/projectile.rs b/common/systems/src/projectile.rs index d0df98f3d8..e5e4c818b3 100644 --- a/common/systems/src/projectile.rs +++ b/common/systems/src/projectile.rs @@ -390,7 +390,7 @@ fn dispatch_hit( .map_or(false, |i| i.projectiles); let precision_from_flank = - combat::precision_mult_from_flank(*projectile_dir, target_info.ori); + combat::precision_mult_from_flank(*projectile_dir, target_info.ori, 1.0); let precision_from_head = { // This performs a cylinder and line segment intersection check. The cylinder is diff --git a/voxygen/anim/src/character/finishermelee.rs b/voxygen/anim/src/character/finishermelee.rs index e23f948a25..b233ca0f52 100644 --- a/voxygen/anim/src/character/finishermelee.rs +++ b/voxygen/anim/src/character/finishermelee.rs @@ -1,6 +1,6 @@ use super::{ super::{vek::*, Animation}, - CharacterSkeleton, SkeletonAttr, + hammer_start, twist_back, twist_forward, CharacterSkeleton, SkeletonAttr, }; use common::states::utils::{AbilityInfo, StageSection}; use core::f32::consts::{PI, TAU}; @@ -440,6 +440,28 @@ impl Animation for FinisherMeleeAnimation { next.control.orientation.rotate_z(move2 * -3.5); next.control.position += Vec3::new(move2 * 14.0, move2 * 6.0, 0.0); }, + Some("common.abilities.hammer.spine_cracker") => { + hammer_start(&mut next, s_a); + let (move1, move2, move3) = match stage_section { + Some(StageSection::Buildup) => (anim_time, 0.0, 0.0), + Some(StageSection::Action) => (1.0, anim_time, 0.0), + Some(StageSection::Recover) => (1.0, 1.0, anim_time), + _ => (0.0, 0.0, 0.0), + }; + let pullback = 1.0 - move3; + let move1 = move1 * pullback; + let move2 = move2 * pullback; + + twist_back(&mut next, move1, 1.9, 1.5, 0.5, 1.2); + next.head.position += Vec3::new(-2.0, 2.0, 0.0) * move1; + next.control.orientation.rotate_x(move1 * 1.8); + next.control.position += Vec3::new(0.0, 0.0, 8.0) * move1; + next.control.orientation.rotate_y(move1 * 0.4); + + twist_forward(&mut next, move2, 2.1, 1.6, 0.4, 1.3); + next.control.orientation.rotate_z(move2 * 1.6); + next.control.position += Vec3::new(-16.0, 12.0, -8.0) * move2; + }, _ => {}, } diff --git a/voxygen/src/audio/sfx/event_mapper/combat/tests.rs b/voxygen/src/audio/sfx/event_mapper/combat/tests.rs index b563992abe..0dcd6e2e1d 100644 --- a/voxygen/src/audio/sfx/event_mapper/combat/tests.rs +++ b/voxygen/src/audio/sfx/event_mapper/combat/tests.rs @@ -86,9 +86,11 @@ fn maps_basic_melee() { range: 3.5, angle: 15.0, damage_effect: None, + attack_effect: None, multi_target: None, simultaneous_hits: 1, - combo_gain: 1, + custom_combo: None, + precision_flank_multiplier: 1.0, }, ori_modifier: 1.0, ability_info: empty_ability_info(), diff --git a/voxygen/src/hud/img_ids.rs b/voxygen/src/hud/img_ids.rs index 1651952611..d335a632c9 100644 --- a/voxygen/src/hud/img_ids.rs +++ b/voxygen/src/hud/img_ids.rs @@ -323,6 +323,7 @@ image_ids! { hammer_heavy_whorl: "voxygen.element.skills.hammer.heavy_whorl", hammer_intercept: "voxygen.element.skills.hammer.intercept", hammer_retaliate: "voxygen.element.skills.hammer.retaliate", + hammer_spine_cracker: "voxygen.element.skills.hammer.spine_cracker", // Skilltree Icons health_plus_skill: "voxygen.element.skills.skilltree.health_plus", energy_plus_skill: "voxygen.element.skills.skilltree.energy_plus", diff --git a/voxygen/src/hud/util.rs b/voxygen/src/hud/util.rs index dc94318c05..4519afac59 100644 --- a/voxygen/src/hud/util.rs +++ b/voxygen/src/hud/util.rs @@ -632,6 +632,7 @@ pub fn ability_image(imgs: &img_ids::Imgs, ability_id: &str) -> image::Id { "common.abilities.hammer.intercept" => imgs.hammer_intercept, "common.abilities.hammer.dual_intercept" => imgs.hammer_intercept, "common.abilities.hammer.retaliate" => imgs.hammer_retaliate, + "common.abilities.hammer.spine_cracker" => imgs.hammer_spine_cracker, // Bow "common.abilities.bow.charged" => imgs.bow_m1, "common.abilities.bow.repeater" => imgs.bow_m2,