feat: add entity knockback & damage to Explosions

Not 100% vanilla accurate but better than nothing
This commit is contained in:
Alexander Medvedev
2026-02-09 23:01:25 +01:00
parent 1cf5ac603c
commit 9435bbfb6a
7 changed files with 192 additions and 12 deletions

View File

@@ -23,8 +23,8 @@ impl LookControl {
pub fn look_at_entity(&mut self, mob: &dyn Mob, entity: &Arc<dyn EntityBase>) {
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(

View File

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

View File

@@ -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<f64>) {
self.set_velocity(self.velocity.load() + velocity).await;
}
pub async fn set_velocity(&self, velocity: Vector3<f64>) {
self.velocity.store(velocity);
self.send_velocity().await;
@@ -2018,6 +2030,15 @@ impl Entity {
.await;
}
pub fn get_eye_pos(&self) -> Vector3<f64> {
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)
}

View File

@@ -115,4 +115,8 @@ impl EntityBase for TNTEntity {
fn as_nbt_storage(&self) -> &dyn NBTStorage {
self
}
fn is_immune_to_explosion(&self) -> bool {
true
}
}

View File

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

View File

@@ -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<World>) {
// 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<f64>,
entity: &Entity,
world: &Arc<World>,
) -> 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<World>) -> 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
}
}

View File

@@ -2297,6 +2297,24 @@ impl World {
None
}
// Gets all entities at a Box
pub fn get_all_at_box(&self, aabb: &BoundingBox) -> Vec<Arc<dyn EntityBase>> {
let entities_guard = self.entities.load();
let players_guard = self.players.load();
entities_guard
.iter()
.map(|e| e.clone() as Arc<dyn EntityBase>)
.chain(
players_guard
.iter()
.map(|p| p.clone() as Arc<dyn EntityBase>),
)
.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<Arc<dyn EntityBase>> {
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<Arc<Player>> {
let players_guard = self.players.load();
players_guard
@@ -3296,19 +3316,19 @@ impl World {
}
async fn ray_outline_check(
self: &Arc<Self>,
&self,
block_pos: &BlockPos,
from: Vector3<f64>,
to: Vector3<f64>,
) -> (bool, Option<BlockDirection>) {
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());