Parried debuff rework

This commit is contained in:
Papinha 2024-05-27 22:01:28 +00:00 committed by Samuel Keiffer
parent 246d90cf22
commit f17c5661ea
34 changed files with 225 additions and 36 deletions

View File

@ -63,6 +63,7 @@ pub const MAX_BLOCK_POISE_COST: f32 = 25.0;
pub const PARRY_BONUS_MULTIPLIER: f32 = 2.0;
pub const FALLBACK_BLOCK_STRENGTH: f32 = 5.0;
pub const BEHIND_TARGET_ANGLE: f32 = 45.0;
pub const BASE_PARRIED_POISE_PUNISHMENT: f32 = 100.0 / 3.5;
#[derive(Copy, Clone)]
pub struct AttackerInfo<'a> {
@ -76,6 +77,7 @@ pub struct AttackerInfo<'a> {
pub mass: Option<&'a Mass>,
}
#[derive(Copy, Clone)]
pub struct TargetInfo<'a> {
pub entity: EcsEntity,
pub uid: Uid,
@ -187,6 +189,7 @@ impl Attack {
defender: target.entity,
attacker: attacker.map(|a| a.entity),
source,
poise_multiplier: 2.0 - (damage_value / block_strength).min(1.0),
});
}
@ -301,10 +304,22 @@ impl Attack {
(matches!(attack_effect.target, Some(GroupTarget::OutOfGroup)) && !allow_friendly_fire)
&& (target_dodging || !permit_pvp)
};
let precision_mult = attacker
let from_precision_mult = attacker
.and_then(|a| a.stats)
.and_then(|s| s.precision_multiplier_override)
.or(precision_mult);
let from_precision_vulnerability_mult = target
.stats
.and_then(|s| s.precision_vulnerability_multiplier_override);
let precision_mult = match (from_precision_mult, from_precision_vulnerability_mult) {
(Some(a), Some(b)) => Some(a.max(b)),
(Some(a), None) | (None, Some(a)) => Some(a),
(None, None) => None,
};
let mut is_applied = false;
let mut accumulated_damage = 0.0;
let damage_modifier = attacker

View File

@ -2402,6 +2402,7 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState {
},
timer: Duration::default(),
stage_section: StageSection::Buildup,
is_parry: false,
}),
CharacterAbility::Roll {
energy_cost: _,

View File

@ -436,7 +436,10 @@ impl BuffKind {
BuffEffect::PoiseReduction(nn_scaling(data.strength)),
BuffEffect::PoiseDamageFromLostHealth(data.strength),
],
BuffKind::Parried => vec![BuffEffect::AttackSpeed(0.5)],
BuffKind::Parried => vec![
BuffEffect::RecoverySpeed(0.25),
BuffEffect::PrecisionVulnerabilityOverride(1.0),
],
//TODO: Handle potion sickness in a more general way.
BuffKind::PotionSickness => vec![
BuffEffect::HealReduction(data.strength),
@ -672,6 +675,8 @@ pub enum BuffEffect {
MovementSpeed(f32),
/// Modifies attack speed of target
AttackSpeed(f32),
/// Modifies recovery speed of target
RecoverySpeed(f32),
/// Modifies ground friction of target
GroundFriction(f32),
/// Reduces poise damage taken after armor is accounted for by this fraction
@ -686,6 +691,8 @@ pub enum BuffEffect {
AttackDamage(f32),
/// Overrides the precision multiplier applied to an attack
PrecisionOverride(f32),
/// Overrides the precision multiplier applied to an incoming attack
PrecisionVulnerabilityOverride(f32),
/// Changes body.
BodyChange(Body),
BuffImmunity(BuffKind),

View File

@ -61,11 +61,13 @@ pub struct Stats {
pub move_speed_modifier: f32,
pub jump_modifier: f32,
pub attack_speed_modifier: f32,
pub recovery_speed_modifier: f32,
pub friction_modifier: f32,
pub max_energy_modifiers: StatsModifier,
pub poise_damage_modifier: f32,
pub attack_damage_modifier: f32,
pub precision_multiplier_override: Option<f32>,
pub precision_vulnerability_multiplier_override: Option<f32>,
pub swim_speed_modifier: f32,
/// This adds effects to any attacks that the entity makes
pub effects_on_attack: Vec<AttackEffect>,
@ -93,11 +95,13 @@ impl Stats {
move_speed_modifier: 1.0,
jump_modifier: 1.0,
attack_speed_modifier: 1.0,
recovery_speed_modifier: 1.0,
friction_modifier: 1.0,
max_energy_modifiers: StatsModifier::default(),
poise_damage_modifier: 1.0,
attack_damage_modifier: 1.0,
precision_multiplier_override: None,
precision_vulnerability_multiplier_override: None,
swim_speed_modifier: 1.0,
effects_on_attack: Vec::new(),
mitigations_penetration: 0.0,

View File

@ -332,6 +332,7 @@ pub struct ParryHookEvent {
pub defender: EcsEntity,
pub attacker: Option<EcsEntity>,
pub source: AttackSource,
pub poise_multiplier: f32,
}
pub struct RequestSiteInfoEvent {

View File

@ -134,7 +134,11 @@ impl CharacterBehavior for Data {
if self.timer < self.static_data.recover_duration {
update.character = CharacterState::BasicAura(Data {
static_data: self.static_data.clone(),
timer: tick_attack_or_default(data, self.timer, None),
timer: tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
),
..*self
});
} else {

View File

@ -222,7 +222,11 @@ impl CharacterBehavior for Data {
StageSection::Recover => {
if self.timer < self.static_data.recover_duration {
update.character = CharacterState::BasicBeam(Data {
timer: tick_attack_or_default(data, self.timer, None),
timer: tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
),
..*self
});
} else {

View File

@ -51,6 +51,8 @@ pub struct Data {
pub timer: Duration,
/// What section the character stage is in
pub stage_section: StageSection,
// Whether there was parry
pub is_parry: bool,
}
impl CharacterBehavior for Data {
@ -100,10 +102,16 @@ impl CharacterBehavior for Data {
}
},
StageSection::Recover => {
if self.timer < self.static_data.recover_duration {
if (self.static_data.parry_window.recover || !self.is_parry)
&& self.timer < self.static_data.recover_duration
{
// Recovery
update.character = CharacterState::BasicBlock(Data {
timer: tick_attack_or_default(data, self.timer, None),
timer: tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
),
..*self
});
} else {

View File

@ -133,7 +133,11 @@ impl CharacterBehavior for Data {
if self.timer < self.static_data.recover_duration {
// Recovery
update.character = CharacterState::BasicMelee(Data {
timer: tick_attack_or_default(data, self.timer, None),
timer: tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
),
..*self
});
} else {

View File

@ -154,7 +154,11 @@ impl CharacterBehavior for Data {
} else if self.timer < self.static_data.recover_duration {
// Recovers
update.character = CharacterState::BasicRanged(Data {
timer: tick_attack_or_default(data, self.timer, None),
timer: tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
),
..*self
});
} else {

View File

@ -249,7 +249,11 @@ impl CharacterBehavior for Data {
if self.timer < self.static_data.recover_duration {
// Recovery
update.character = CharacterState::BasicSummon(Data {
timer: tick_attack_or_default(data, self.timer, None),
timer: tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
),
..*self
});
} else {

View File

@ -78,7 +78,11 @@ impl CharacterBehavior for Data {
if self.timer < self.static_data.recover_duration {
// Recovery
update.character = CharacterState::Blink(Data {
timer: tick_attack_or_default(data, self.timer, None),
timer: tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
),
..*self
});
} else {

View File

@ -206,7 +206,11 @@ impl CharacterBehavior for Data {
if self.timer < self.static_data.recover_duration {
// Recovers
update.character = CharacterState::ChargedMelee(Data {
timer: tick_attack_or_default(data, self.timer, None),
timer: tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
),
..*self
});
} else {

View File

@ -171,7 +171,11 @@ impl CharacterBehavior for Data {
if self.timer < self.static_data.recover_duration {
// Recovers
update.character = CharacterState::ChargedRanged(Data {
timer: tick_attack_or_default(data, self.timer, None),
timer: tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
),
..*self
});
} else {

View File

@ -234,7 +234,11 @@ impl CharacterBehavior for Data {
if self.timer < strike_data.recover_duration {
// Recovery
if let CharacterState::ComboMelee2(c) = &mut update.character {
c.timer = tick_attack_or_default(data, self.timer, None);
c.timer = tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
);
}
} else {
// Return to wielding

View File

@ -170,7 +170,11 @@ impl CharacterBehavior for Data {
if self.timer < self.static_data.recover_duration {
// Recover
update.character = CharacterState::DashMelee(Data {
timer: tick_attack_or_default(data, self.timer, None),
timer: tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
),
..*self
});
} else {

View File

@ -131,7 +131,11 @@ impl CharacterBehavior for Data {
if self.timer < self.static_data.recover_duration {
// Complete recovery delay before finishing state
if let CharacterState::DiveMelee(c) = &mut update.character {
c.timer = tick_attack_or_default(data, self.timer, None);
c.timer = tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
);
}
} else {
// Done

View File

@ -128,7 +128,11 @@ impl CharacterBehavior for Data {
if self.timer < self.static_data.recover_duration {
// Recovery
if let CharacterState::FinisherMelee(c) = &mut update.character {
c.timer = tick_attack_or_default(data, self.timer, None);
c.timer = tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
);
}
} else {
// Done

View File

@ -140,14 +140,22 @@ impl CharacterBehavior for Data {
);
update.character = CharacterState::LeapMelee(Data {
timer: tick_attack_or_default(data, self.timer, None),
timer: tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
),
exhausted: true,
..*self
});
} else if self.timer < self.static_data.recover_duration {
// Complete recovery delay before finishing state
update.character = CharacterState::LeapMelee(Data {
timer: tick_attack_or_default(data, self.timer, None),
timer: tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
),
..*self
});
} else {

View File

@ -248,7 +248,11 @@ impl CharacterBehavior for Data {
if self.timer < self.static_data.recover_duration {
// Recovers
update.character = CharacterState::LeapShockwave(Data {
timer: tick_attack_or_default(data, self.timer, None),
timer: tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
),
..*self
});
} else {

View File

@ -127,7 +127,11 @@ impl CharacterBehavior for Data {
if self.timer < self.static_data.recover_duration {
// Recover
if let CharacterState::RapidMelee(c) = &mut update.character {
c.timer = tick_attack_or_default(data, self.timer, None);
c.timer = tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
);
}
} else {
// Done

View File

@ -187,7 +187,11 @@ impl CharacterBehavior for Data {
if self.timer < self.static_data.recover_duration {
// Recover from attack
update.character = CharacterState::RepeaterRanged(Data {
timer: tick_attack_or_default(data, self.timer, None),
timer: tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
),
..*self
});
} else {

View File

@ -94,7 +94,11 @@ impl CharacterBehavior for Data {
if self.timer < self.static_data.recover_duration {
// Recovery
if let CharacterState::RiposteMelee(c) = &mut update.character {
c.timer = tick_attack_or_default(data, self.timer, None);
c.timer = tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
);
}
} else {
// Done

View File

@ -119,7 +119,11 @@ impl CharacterBehavior for Data {
{
// Recover
update.character = CharacterState::Roll(Data {
timer: tick_attack_or_default(data, self.timer, None),
timer: tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
),
..*self
});
} else {

View File

@ -170,7 +170,11 @@ impl CharacterBehavior for Data {
StageSection::Recover => {
if self.timer < self.static_data.recover_duration {
update.character = CharacterState::SelfBuff(Data {
timer: tick_attack_or_default(data, self.timer, None),
timer: tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
),
..*self
});
} else {

View File

@ -174,7 +174,11 @@ impl CharacterBehavior for Data {
if self.timer < self.static_data.recover_duration {
// Recovers
update.character = CharacterState::Shockwave(Data {
timer: tick_attack_or_default(data, self.timer, None),
timer: tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
),
..*self
});
} else {

View File

@ -95,7 +95,11 @@ impl CharacterBehavior for Data {
if self.timer < self.static_data.recover_duration {
// Recovery
if let CharacterState::SpriteInteract(c) = &mut update.character {
c.timer = tick_attack_or_default(data, self.timer, None);
c.timer = tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
);
}
} else {
// Create inventory manipulation event

View File

@ -183,7 +183,11 @@ impl CharacterBehavior for Data {
if self.timer < self.static_data.recover_duration {
// Recovery
update.character = CharacterState::SpriteSummon(Data {
timer: tick_attack_or_default(data, self.timer, None),
timer: tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
),
..*self
});
} else {

View File

@ -140,7 +140,11 @@ impl CharacterBehavior for Data {
if self.timer < self.static_data.recover_duration {
update.character = CharacterState::StaticAura(Data {
static_data: self.static_data.clone(),
timer: tick_attack_or_default(data, self.timer, None),
timer: tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
),
..*self
});
} else {

View File

@ -66,7 +66,11 @@ impl CharacterBehavior for Data {
if self.timer < self.static_data.recover_duration {
// Recovery
update.character = CharacterState::Stunned(Data {
timer: tick_attack_or_default(data, self.timer, None),
timer: tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
),
..*self
});
} else {

View File

@ -114,7 +114,11 @@ impl CharacterBehavior for Data {
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),
timer: tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
),
..*self
});
} else {

View File

@ -120,7 +120,11 @@ impl CharacterBehavior for Data {
// Recovery
update.character = CharacterState::UseItem(Data {
static_data: self.static_data.clone(),
timer: tick_attack_or_default(data, self.timer, None),
timer: tick_attack_or_default(
data,
self.timer,
Some(data.stats.recovery_speed_modifier),
),
..*self
});
} else {

View File

@ -755,6 +755,9 @@ fn execute_effect(
BuffEffect::AttackSpeed(speed) => {
stat.attack_speed_modifier *= *speed;
},
BuffEffect::RecoverySpeed(speed) => {
stat.recovery_speed_modifier *= *speed;
},
BuffEffect::GroundFriction(gf) => {
stat.friction_modifier *= *gf;
},
@ -781,6 +784,13 @@ fn execute_effect(
.map(|mult| mult.min(*val))
.or(Some(*val));
},
BuffEffect::PrecisionVulnerabilityOverride(val) => {
// Use higher of precision multiplier overrides
stat.precision_vulnerability_multiplier_override = stat
.precision_vulnerability_multiplier_override
.map(|mult| mult.max(*val))
.or(Some(*val));
},
BuffEffect::BodyChange(b) => {
// For when an entity is under the effects of multiple de/buffs that change the
// body, to avoid flickering between many bodies only change the body if the

View File

@ -18,7 +18,7 @@ use crate::{
#[cfg(feature = "worldgen")]
use common::rtsim::{Actor, RtSimEntity};
use common::{
combat::{self, AttackSource, DamageContributor, DeathEffect},
combat::{self, AttackSource, DamageContributor, DeathEffect, BASE_PARRIED_POISE_PUNISHMENT},
comp::{
self,
aura::{self, EnteredAuras},
@ -28,7 +28,7 @@ use common::{
item::flatten_counted_items,
loot_owner::LootOwnerKind,
Alignment, Auras, Body, CharacterState, Energy, Group, Health, Inventory, Object,
PickupItem, Player, Poise, Pos, Presence, PresenceKind, SkillSet, Stats,
PickupItem, Player, Poise, PoiseChange, Pos, Presence, PresenceKind, SkillSet, Stats,
BASE_ABILITY_LIMIT,
},
consts::TELEPORTER_RADIUS,
@ -1746,18 +1746,31 @@ impl ServerEvent for ParryHookEvent {
type SystemData<'a> = (
Read<'a, Time>,
Read<'a, EventBus<EnergyChangeEvent>>,
Read<'a, EventBus<PoiseChangeEvent>>,
Read<'a, EventBus<BuffEvent>>,
WriteStorage<'a, CharacterState>,
ReadStorage<'a, Uid>,
ReadStorage<'a, Stats>,
ReadStorage<'a, comp::Mass>,
ReadStorage<'a, Inventory>,
);
fn handle(
events: impl ExactSizeIterator<Item = Self>,
(time, energy_change_events, buff_events, mut character_states, uids, stats, masses): Self::SystemData<'_>,
(
time,
energy_change_events,
poise_change_events,
buff_events,
mut character_states,
uids,
stats,
masses,
inventories,
): Self::SystemData<'_>,
) {
let mut energy_change_emitter = energy_change_events.emitter();
let mut poise_change_emitter = poise_change_events.emitter();
let mut buff_emitter = buff_events.emitter();
for ev in events {
if let Some(mut char_state) = character_states.get_mut(ev.defender) {
@ -1773,6 +1786,7 @@ impl ServerEvent for ParryHookEvent {
entity: ev.defender,
change: c.static_data.energy_regen,
});
c.is_parry = true;
false
},
_ => false,
@ -1787,8 +1801,8 @@ impl ServerEvent for ParryHookEvent {
if let Some(attacker) = ev.attacker
&& matches!(ev.source, AttackSource::Melee)
{
// When attacker is parried, add the parried debuff for 2 seconds, which slows
// them
// When attacker is parried, the debuff lasts 2 seconds, the attacker takes
// poise damage, get precision vulnerability and get slower recovery speed
let data = buff::BuffData::new(1.0, Some(Secs(2.0)));
let source = if let Some(uid) = uids.get(ev.defender) {
BuffSource::Character { by: *uid }
@ -1812,6 +1826,27 @@ impl ServerEvent for ParryHookEvent {
entity: attacker,
buff_change: buff::BuffChange::Add(buff),
});
let attacker_poise_change = Poise::apply_poise_reduction(
ev.poise_multiplier.clamp(1.0, 2.0) * BASE_PARRIED_POISE_PUNISHMENT,
inventories.get(attacker),
&MaterialStatManifest::load().read(),
character_states.get(attacker),
stats.get(attacker),
);
poise_change_emitter.emit(PoiseChangeEvent {
entity: attacker,
change: PoiseChange {
amount: -attacker_poise_change,
impulse: Vec3::zero(),
by: uids
.get(ev.defender)
.map(|d| DamageContributor::new(*d, None)),
cause: Some(DamageSource::Melee),
time: *time,
},
});
}
}
}