Dive melee now scales its attack off of the entity's vertical speed.

Parries now cause the attacker to effectively have a recover that is either twice as long or 0.5s longer, whichever is more.
Counters now deal twice as much damage to the target if the target is in the buildup portion of an ability.
This commit is contained in:
Sam 2022-09-06 21:23:12 -04:00
parent 356c26cc66
commit a8212d6f41
15 changed files with 313 additions and 24 deletions

View File

@ -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,

View File

@ -10,6 +10,7 @@ ComboMelee2(
),
range: 6.0,
angle: 5.0,
damage_effect: Some(BuildupsVulnerable),
),
buildup_duration: 0.05,
swing_duration: 0.1,

View File

@ -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

View File

@ -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() {

View File

@ -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"))]

View File

@ -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;
}
}

View File

@ -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,

View File

@ -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<DurationsInfo> {
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<Duration>,
pub action: Option<Duration>,
pub recover: Option<Duration>,
pub movement: Option<Duration>,
pub charge: Option<Duration>,
}
impl Default for CharacterState {

View File

@ -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,

View File

@ -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<Duration> {
&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

View File

@ -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),
);

View File

@ -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<EcsEntity>) {
pub fn handle_parry_hook(server: &Server, defender: EcsEntity, attacker: Option<EcsEntity>) {
let ecs = &server.state.ecs();
let server_eventbus = ecs.read_resource::<EventBus<ServerEvent>>();
// Reset character state of defender
if let Some(mut char_state) = ecs
.write_storage::<comp::CharacterState>()
@ -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::<comp::CharacterState>().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::<Uid>().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<f32>) {

View File

@ -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"
},

View File

@ -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<s
BuffKind::Wet { .. } => 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"),
}
}

View File

@ -184,7 +184,8 @@ pub fn consumable_desc(effects: &[Effect], i18n: &Localization) -> Vec<String> {
| 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<String> {
| 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