From 9435bbfb6af39d27a090d2dc37a30ee71ab6e58a Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Mon, 9 Feb 2026 23:01:25 +0100 Subject: [PATCH] feat: add entity knockback & damage to Explosions Not 100% vanilla accurate but better than nothing --- pumpkin/src/entity/ai/control/look_control.rs | 4 +- pumpkin/src/entity/falling.rs | 10 ++ pumpkin/src/entity/mod.rs | 27 +++- pumpkin/src/entity/tnt.rs | 4 + pumpkin/src/net/java/mod.rs | 2 +- pumpkin/src/world/explosion.rs | 131 +++++++++++++++++- pumpkin/src/world/mod.rs | 26 +++- 7 files changed, 192 insertions(+), 12 deletions(-) diff --git a/pumpkin/src/entity/ai/control/look_control.rs b/pumpkin/src/entity/ai/control/look_control.rs index 144e8ef57..7d97c84b9 100644 --- a/pumpkin/src/entity/ai/control/look_control.rs +++ b/pumpkin/src/entity/ai/control/look_control.rs @@ -23,8 +23,8 @@ impl LookControl { pub fn look_at_entity(&mut self, mob: &dyn Mob, entity: &Arc) { let entity = entity.get_entity(); - let pos = entity.pos.load(); - self.look_at(mob, pos.x, entity.get_eye_y(), pos.z); + let pos = entity.get_eye_pos(); + self.look_at(mob, pos.x, pos.y, pos.z); } pub fn look_at_entity_with_range( diff --git a/pumpkin/src/entity/falling.rs b/pumpkin/src/entity/falling.rs index 7657ad4f6..fd80e6067 100644 --- a/pumpkin/src/entity/falling.rs +++ b/pumpkin/src/entity/falling.rs @@ -1,3 +1,4 @@ +use pumpkin_data::damage::DamageType; use pumpkin_data::entity::EntityType; use pumpkin_data::meta_data_type::MetaDataType; use pumpkin_data::{Block, tracked_data::TrackedData}; @@ -110,6 +111,15 @@ impl EntityBase for FallingEntity { self } + fn damage<'a>( + &'a self, + _caller: &'a dyn EntityBase, + _amount: f32, + _damage_type: DamageType, + ) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { false }) + } + fn get_gravity(&self) -> f64 { 0.04 } diff --git a/pumpkin/src/entity/mod.rs b/pumpkin/src/entity/mod.rs index 07ae01393..cd5594232 100644 --- a/pumpkin/src/entity/mod.rs +++ b/pumpkin/src/entity/mod.rs @@ -146,6 +146,11 @@ pub trait EntityBase: Send + Sync + NBTStorage { true } + /// Whether the entity is immune from explosion knockback and damage + fn is_immune_to_explosion(&self) -> bool { + false + } + fn get_gravity(&self) -> f64 { 0.0 } @@ -194,9 +199,12 @@ pub trait EntityBase: Send + Sync + NBTStorage { cause: Option<&'a dyn EntityBase>, ) -> EntityBaseFuture<'a, bool> { Box::pin(async move { - caller - .damage_with_context(caller, amount, damage_type, position, source, cause) - .await + if caller.get_living_entity().is_some() { + return caller + .damage_with_context(caller, amount, damage_type, position, source, cause) + .await; + } + false }) } @@ -481,6 +489,10 @@ impl Entity { } } + pub async fn add_velocity(&self, velocity: Vector3) { + self.set_velocity(self.velocity.load() + velocity).await; + } + pub async fn set_velocity(&self, velocity: Vector3) { self.velocity.store(velocity); self.send_velocity().await; @@ -2018,6 +2030,15 @@ impl Entity { .await; } + pub fn get_eye_pos(&self) -> Vector3 { + let pos = self.pos.load(); + Vector3::new( + pos.x, + pos.y + f64::from(self.entity_dimension.load().eye_height), + pos.z, + ) + } + pub fn get_eye_y(&self) -> f64 { self.pos.load().y + f64::from(self.entity_dimension.load().eye_height) } diff --git a/pumpkin/src/entity/tnt.rs b/pumpkin/src/entity/tnt.rs index bde4937da..195f14fc5 100644 --- a/pumpkin/src/entity/tnt.rs +++ b/pumpkin/src/entity/tnt.rs @@ -115,4 +115,8 @@ impl EntityBase for TNTEntity { fn as_nbt_storage(&self) -> &dyn NBTStorage { self } + + fn is_immune_to_explosion(&self) -> bool { + true + } } diff --git a/pumpkin/src/net/java/mod.rs b/pumpkin/src/net/java/mod.rs index dfb67b962..0ecc7c961 100644 --- a/pumpkin/src/net/java/mod.rs +++ b/pumpkin/src/net/java/mod.rs @@ -171,7 +171,7 @@ impl JavaClient { } Err(error) => { let text = format!("Error while reading incoming packet {error}"); - log::error!( + log::debug!( "Failed to read incoming packet with id {}: {}", packet.id, error diff --git a/pumpkin/src/world/explosion.rs b/pumpkin/src/world/explosion.rs index 080261ef0..04859754b 100644 --- a/pumpkin/src/world/explosion.rs +++ b/pumpkin/src/world/explosion.rs @@ -1,11 +1,12 @@ use std::sync::Arc; -use pumpkin_data::{Block, BlockState}; -use pumpkin_util::math::{position::BlockPos, vector3::Vector3}; +use pumpkin_data::{Block, BlockState, damage::DamageType, entity::EntityType}; +use pumpkin_util::math::{boundingbox::BoundingBox, position::BlockPos, vector3::Vector3}; use rustc_hash::FxHashMap; use crate::{ block::{ExplodeArgs, drop_loot}, + entity::{Entity, EntityBase}, world::loot::LootContextParameters, }; @@ -77,10 +78,133 @@ impl Explosion { map } + async fn damage_entities(&self, world: &Arc) { + // Explosion is too small + if self.power < 1.0e-5 { + return; + } + + let radius = self.power as f64 * 2.0; + let min_x = (self.pos.x - radius - 1.0).floor() as i32; + let max_x = (self.pos.x + radius + 1.0).floor() as i32; + let min_y = (self.pos.y - radius - 1.0).floor() as i32; + let max_y = (self.pos.y + radius + 1.0).floor() as i32; + let min_z = (self.pos.z - radius - 1.0).floor() as i32; + let max_z = (self.pos.z + radius + 1.0).floor() as i32; + + let search_box = BoundingBox::new( + Vector3::new(min_x as f64, min_y as f64, min_z as f64), + Vector3::new(max_x as f64, max_y as f64, max_z as f64), + ); + + let entities = world.get_all_at_box(&search_box); + + for entity_base in entities { + if entity_base.is_immune_to_explosion() { + continue; + } + let entity = entity_base.get_entity(); + + let distance = (entity.pos.load().squared_distance_to_vec(&self.pos)).sqrt() / radius; + if distance > 1.0 { + continue; + } + + let exposure = Self::calculate_exposure(&self.pos, entity, world).await as f64; + if exposure == 0.0 { + continue; + } + + let damage_multiplier = (1.0 - distance) * exposure; + let damage = (f64::midpoint(damage_multiplier * damage_multiplier, damage_multiplier) + * 7.0 + * self.power as f64 + + 1.0) as f32; + + // TODO: damage type + entity + .damage(entity_base.as_ref(), damage, DamageType::EXPLOSION) + .await; + + // Calculate and apply knockback + let dir_pos = if entity.entity_type == &EntityType::TNT { + entity.pos.load() + } else { + entity.get_eye_pos() + }; + let direction = (dir_pos - self.pos).normalize(); + // TODO + let knockback_resistance = 0.0; + + let knockback_multiplier = (1.0 - distance) * exposure * (1.0 - knockback_resistance); + let knockback = direction * knockback_multiplier; + entity.add_velocity(knockback).await; + } + } + + async fn calculate_exposure( + explosion_pos: &Vector3, + entity: &Entity, + world: &Arc, + ) -> f32 { + let bbox = entity.bounding_box.load(); + + let step_x = 1.0 / ((bbox.max.x - bbox.min.x) * 2.0 + 1.0); + let step_y = 1.0 / ((bbox.max.y - bbox.min.y) * 2.0 + 1.0); + let step_z = 1.0 / ((bbox.max.z - bbox.min.z) * 2.0 + 1.0); + + if step_x < 0.0 || step_y < 0.0 || step_z < 0.0 { + return 0.0; + } + + let offset_x = (1.0 - (1.0 / step_x).floor() * step_x) / 2.0; + let offset_z = (1.0 - (1.0 / step_z).floor() * step_z) / 2.0; + + let mut visible_points = 0; + let mut total_points = 0; + + let mut k = 0.0; + while k <= 1.0 { + let mut l = 0.0; + while l <= 1.0 { + let mut m = 0.0; + while m <= 1.0 { + let n = bbox.min.x + (bbox.max.x - bbox.min.x) * k; + let o = bbox.min.y + (bbox.max.y - bbox.min.y) * l; + let p = bbox.min.z + (bbox.max.z - bbox.min.z) * m; + + let vec3d = Vector3::new(n + offset_x, o, p + offset_z); + + if world + .raycast(vec3d, *explosion_pos, async |pos, world_ref| { + let state = world_ref.get_block_state(pos).await; + !state.is_air() && !state.collision_shapes.is_empty() + }) + .await + .is_none() + { + visible_points += 1; + } + + total_points += 1; + m += step_z; + } + l += step_y; + } + k += step_x; + } + + if total_points == 0 { + return 0.0; + } + + visible_points as f32 / total_points as f32 + } + /// Returns the removed block count pub async fn explode(&self, world: &Arc) -> u32 { let blocks = self.get_blocks_to_destroy(world).await; - // TODO: Entity damage, fire + self.damage_entities(world).await; for (pos, (block, state)) in &blocks { world.set_block_state(pos, 0, BlockFlags::NOTIFY_ALL).await; @@ -104,6 +228,7 @@ impl Explosion { .await; } } + // TODO: fire blocks.len() as u32 } } diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index e6adb42a8..4ff13d836 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -2297,6 +2297,24 @@ impl World { None } + // Gets all entities at a Box + pub fn get_all_at_box(&self, aabb: &BoundingBox) -> Vec> { + let entities_guard = self.entities.load(); + let players_guard = self.players.load(); + + entities_guard + .iter() + .map(|e| e.clone() as Arc) + .chain( + players_guard + .iter() + .map(|p| p.clone() as Arc), + ) + .filter(|entity| entity.get_entity().bounding_box.load().intersects(aabb)) + .collect() + } + + // Gets all non Player entities at a Box pub fn get_entities_at_box(&self, aabb: &BoundingBox) -> Vec> { self.entities .load() @@ -2305,6 +2323,8 @@ impl World { .cloned() .collect() } + + // Gets all Player entities at a Box pub fn get_players_at_box(&self, aabb: &BoundingBox) -> Vec> { let players_guard = self.players.load(); players_guard @@ -3296,19 +3316,19 @@ impl World { } async fn ray_outline_check( - self: &Arc, + &self, block_pos: &BlockPos, from: Vector3, to: Vector3, ) -> (bool, Option) { let state = self.get_block_state(block_pos).await; - let bounding_boxes = state.get_block_outline_shapes(); - if state.outline_shapes.is_empty() { return (true, None); } + let bounding_boxes = state.get_block_outline_shapes(); + for shape in bounding_boxes { let world_min = shape.min.add(&block_pos.0.to_f64()); let world_max = shape.max.add(&block_pos.0.to_f64());