mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
fix(entity): apply knockback resistance to living entities (#2498)
* fix(entity): apply knockback resistance to living entities Vanilla LivingEntity.knockback scales strength by 1.0 - KNOCKBACK_RESISTANCE, but Pumpkin never applied it, so mobs with high resistance (iron golem, warden, ravager) were knocked back like any other mob. Scale both knockback paths by the victim's resistance: the base hit knockback in LivingEntity, and the sprint/enchant bonus in handle_knockback, which now takes the victim as &dyn EntityBase so it can read the attribute. Fixes #1761 * docs(entity): clarify knockback resistance is not applied in apply_knockback The previous note read as if every caller had to pre-scale strength. Ender dragon knockback mirrors vanilla Entity.push, which ignores resistance, so spell out which callers scale and which don't.
This commit is contained in:
@@ -2,6 +2,7 @@ use std::sync::atomic::Ordering;
|
||||
|
||||
use crate::entity::EntityBase;
|
||||
use pumpkin_data::{
|
||||
attributes::Attributes,
|
||||
particle::Particle,
|
||||
sound::{Sound, SoundCategory},
|
||||
};
|
||||
@@ -61,13 +62,27 @@ impl AttackType {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_knockback(attacker: &Entity, victim: &Entity, strength: f64) {
|
||||
let yaw = attacker.yaw.load();
|
||||
victim.knockback(
|
||||
strength * 0.5,
|
||||
f64::from((yaw.to_radians()).sin()),
|
||||
f64::from(-(yaw.to_radians()).cos()),
|
||||
);
|
||||
/// Scales a knockback `strength` by a living entity's knockback resistance,
|
||||
/// mirroring vanilla `LivingEntity.knockback`: `strength *= 1.0 - resistance`.
|
||||
/// A resistance of 1.0 (iron golem, warden, ...) cancels the knockback entirely.
|
||||
pub fn knockback_after_resistance(strength: f64, resistance: f64) -> f64 {
|
||||
strength * (1.0 - resistance)
|
||||
}
|
||||
|
||||
pub fn handle_knockback(attacker: &Entity, victim: &dyn EntityBase, strength: f64) {
|
||||
let resistance = victim.get_living_entity().map_or(0.0, |living| {
|
||||
living.get_attribute_value(&Attributes::KNOCKBACK_RESISTANCE)
|
||||
});
|
||||
let strength = knockback_after_resistance(strength * 0.5, resistance);
|
||||
|
||||
if strength > 0.0 {
|
||||
let yaw = attacker.yaw.load();
|
||||
victim.get_entity().knockback(
|
||||
strength,
|
||||
f64::from((yaw.to_radians()).sin()),
|
||||
f64::from(-(yaw.to_radians()).cos()),
|
||||
);
|
||||
}
|
||||
|
||||
let velocity = attacker.velocity.load();
|
||||
attacker.velocity.store(velocity.multiply(0.6, 1.0, 0.6));
|
||||
@@ -116,3 +131,32 @@ pub async fn player_attack_sound(pos: &Vector3<f64>, world: &World, attack_type:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::knockback_after_resistance;
|
||||
|
||||
#[test]
|
||||
fn zero_resistance_keeps_full_strength() {
|
||||
assert_eq!(knockback_after_resistance(0.4, 0.0), 0.4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_resistance_cancels_knockback() {
|
||||
// Iron golem / warden have KNOCKBACK_RESISTANCE == 1.0.
|
||||
assert_eq!(knockback_after_resistance(0.4, 1.0), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_resistance_scales_strength() {
|
||||
// Ravager has KNOCKBACK_RESISTANCE == 0.75.
|
||||
assert!((knockback_after_resistance(0.4, 0.75) - 0.1).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn over_full_resistance_is_negative_so_callers_skip_it() {
|
||||
// Stacked armour modifiers can push resistance above 1.0; the result is
|
||||
// negative and callers guard on `strength > 0.0`.
|
||||
assert!(knockback_after_resistance(0.4, 1.2) < 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ use crate::block::OnLandedUponArgs;
|
||||
use crate::entity::attributes::AttributeInstance;
|
||||
use crate::entity::attributes::Modifier;
|
||||
use crate::entity::attributes::ModifierOperation;
|
||||
use crate::entity::combat::knockback_after_resistance;
|
||||
use crate::entity::mob::equipment::DEFAULT_EQUIPMENT_DROP_CHANCE;
|
||||
use crate::entity::mob::slime::SlimeEntity;
|
||||
use crate::entity::player::statistics::{CustomStatistic, StatisticCategory};
|
||||
@@ -2419,7 +2420,12 @@ impl EntityBase for LivingEntity {
|
||||
let target_pos = self.entity.pos.load();
|
||||
let dx = source_pos.x - target_pos.x;
|
||||
let dz = source_pos.z - target_pos.z;
|
||||
self.entity.apply_knockback(0.4, dx, dz);
|
||||
let resistance = self.get_attribute_value(&Attributes::KNOCKBACK_RESISTANCE);
|
||||
self.entity.apply_knockback(
|
||||
knockback_after_resistance(0.4, resistance),
|
||||
dx,
|
||||
dz,
|
||||
);
|
||||
self.entity.send_velocity();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1358,9 +1358,13 @@ impl Entity {
|
||||
/// Applies knockback to the entity, following vanilla Minecraft's mechanics.
|
||||
/// `LivingEntity.takeKnockback()`
|
||||
/// This function calculates the entity's new velocity based on the specified knockback strength and direction.
|
||||
///
|
||||
/// Knockback resistance is not applied here, because it is a `LivingEntity`
|
||||
/// attribute and this is an `Entity` method. Callers modelling vanilla's
|
||||
/// `LivingEntity.knockback` scale `strength` with
|
||||
/// `combat::knockback_after_resistance` first; callers modelling vanilla's raw
|
||||
/// `Entity.push` (such as the ender dragon) pass `strength` unscaled.
|
||||
pub fn apply_knockback(&self, strength: f64, mut x: f64, mut z: f64) {
|
||||
// TODO: strength *= 1 - Entity attribute knockback resistance
|
||||
|
||||
if strength <= 0.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1152,7 +1152,7 @@ impl Player {
|
||||
_ => {}
|
||||
}
|
||||
if config.knockback {
|
||||
combat::handle_knockback(attacker_entity, victim_entity, knockback_strength);
|
||||
combat::handle_knockback(attacker_entity, victim.as_ref(), knockback_strength);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -420,7 +420,7 @@ impl EntityBase for ArrowEntity {
|
||||
{
|
||||
crate::entity::combat::handle_knockback(
|
||||
owner_entity.get_entity(),
|
||||
target.get_entity(),
|
||||
target.as_ref(),
|
||||
f64::from(punch) * 0.6,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user