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:
Maxime Chaffraix
2026-07-30 14:25:36 +02:00
committed by GitHub
parent 98fd79b57e
commit b024c3ae3a
5 changed files with 66 additions and 12 deletions

View File

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

View File

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

View File

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

View File

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

View File

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