From b024c3ae3a9bb5c8ed941dd96f620e555c971a80 Mon Sep 17 00:00:00 2001 From: Maxime Chaffraix <160416029+Sawlyer@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:25:36 +0200 Subject: [PATCH] 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. --- pumpkin/src/entity/combat.rs | 58 ++++++++++++++++++++++---- pumpkin/src/entity/living.rs | 8 +++- pumpkin/src/entity/mod.rs | 8 +++- pumpkin/src/entity/player.rs | 2 +- pumpkin/src/entity/projectile/arrow.rs | 2 +- 5 files changed, 66 insertions(+), 12 deletions(-) diff --git a/pumpkin/src/entity/combat.rs b/pumpkin/src/entity/combat.rs index f85295ee2..c4c9a8865 100644 --- a/pumpkin/src/entity/combat.rs +++ b/pumpkin/src/entity/combat.rs @@ -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, 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); + } +} diff --git a/pumpkin/src/entity/living.rs b/pumpkin/src/entity/living.rs index fa0849aea..a515c8717 100644 --- a/pumpkin/src/entity/living.rs +++ b/pumpkin/src/entity/living.rs @@ -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(); } } diff --git a/pumpkin/src/entity/mod.rs b/pumpkin/src/entity/mod.rs index 9e9cc025d..6f98d7686 100644 --- a/pumpkin/src/entity/mod.rs +++ b/pumpkin/src/entity/mod.rs @@ -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; } diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 95609f954..a22c14f9f 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -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); } } diff --git a/pumpkin/src/entity/projectile/arrow.rs b/pumpkin/src/entity/projectile/arrow.rs index 1fa2e8c26..85c5716b7 100644 --- a/pumpkin/src/entity/projectile/arrow.rs +++ b/pumpkin/src/entity/projectile/arrow.rs @@ -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, ); }