Skip to main content

steel_core/world/explosion/server/
entity_effects.rs

1//! Entity damage, knockback, and post-explosion callbacks.
2
3use glam::DVec3;
4use steel_registry::vanilla_entity_type_tags::EntityTypeTag;
5use steel_registry::{REGISTRY, TaggedRegistryExt as _, vanilla_attributes};
6use steel_utils::types::GameType;
7use steel_utils::{BlockPos, WorldAabb};
8
9use crate::behavior::BlockCollisionContext;
10use crate::entity::Entity;
11use crate::entity::entities::PrimedTntEntity;
12use crate::world::explosion::ExplosionDamageCalculator as _;
13use crate::world::raycast::ExplosionExposureRaycast;
14
15use super::ServerExplosion;
16use super::exposure::EntityExplosionExposure;
17
18const MIN_DAMAGE_RADIUS: f32 = 1.0e-5;
19const DAMAGE_RADIUS_SCALE: f32 = 2.0;
20const ENTITY_QUERY_PADDING: f64 = 1.0;
21const NORMALIZE_EPSILON: f64 = 1.0e-5_f32 as f64;
22
23impl ServerExplosion<'_> {
24    pub(super) fn hurt_entities(&mut self) {
25        if self.radius < MIN_DAMAGE_RADIUS {
26            return;
27        }
28
29        let double_radius = f64::from(self.radius * DAMAGE_RADIUS_SCALE);
30        let bounds = WorldAabb::from_min_max(
31            DVec3::new(
32                (self.center.x - double_radius - ENTITY_QUERY_PADDING).floor(),
33                (self.center.y - double_radius - ENTITY_QUERY_PADDING).floor(),
34                (self.center.z - double_radius - ENTITY_QUERY_PADDING).floor(),
35            ),
36            DVec3::new(
37                (self.center.x + double_radius + ENTITY_QUERY_PADDING).floor(),
38                (self.center.y + double_radius + ENTITY_QUERY_PADDING).floor(),
39                (self.center.z + double_radius + ENTITY_QUERY_PADDING).floor(),
40            ),
41        );
42        let source_id = self.source.as_ref().map(|source| source.id());
43        let entities = self.world.get_entities_in_aabb_matching(&bounds, |entity| {
44            source_id != Some(entity.id()) && !entity.is_spectator()
45        });
46        let builtin_entity_effects = self.damage_calculator.has_builtin_entity_effects();
47        let mut exposure_raycast =
48            ExplosionExposureRaycast::new(self.world.as_ref(), BlockCollisionContext::empty());
49        exposure_raycast.configure_clear_grid(
50            BlockPos::from(bounds.min_corner()),
51            BlockPos::from(bounds.max_corner()),
52        );
53        // The freshly constructed cache is already safe for the first target.
54        let mut reusable_from_previous_tnt = true;
55
56        for entity in entities {
57            // Exact Steel PrimedTNT rejects damage, keeps the base no-op explosion callback, and
58            // only accepts the impulse. With built-in entity effects, no block mutation can occur
59            // between these targets, so their static exposure shapes remain current.
60            let inert_primed_tnt = builtin_entity_effects
61                && steel_utils::Downcast::downcast_ref::<PrimedTntEntity>(entity.as_ref())
62                    .is_some();
63            if !reusable_from_previous_tnt || !inert_primed_tnt {
64                exposure_raycast.clear();
65            }
66            reusable_from_previous_tnt = inert_primed_tnt;
67
68            if entity.ignore_explosion(self) {
69                continue;
70            }
71            let distance = entity.position().distance(self.center) / double_radius;
72            if distance > 1.0 {
73                continue;
74            }
75
76            let delta = entity.explosion_damage_origin() - self.center;
77            let delta_length = delta.length();
78            let direction = if delta_length < NORMALIZE_EPSILON {
79                DVec3::ZERO
80            } else {
81                delta / delta_length
82            };
83            let should_damage = self
84                .damage_calculator
85                .should_damage_entity(self, entity.as_ref());
86            let knockback_multiplier = self.damage_calculator.knockback_multiplier(entity.as_ref());
87            let exposure = if !should_damage && knockback_multiplier == 0.0 {
88                0.0
89            } else {
90                EntityExplosionExposure::capture(entity.as_ref())
91                    .calculate_cached_with(&mut exposure_raycast, self.center)
92            };
93
94            if should_damage {
95                let amount =
96                    self.damage_calculator
97                        .entity_damage_amount(self, entity.as_ref(), exposure);
98                entity.hurt(self.world, &self.damage_source, amount);
99            }
100
101            let knockback_resistance = entity.as_living_entity().map_or(0.0, |living| {
102                living
103                    .attributes()
104                    .lock()
105                    .required_value(vanilla_attributes::EXPLOSION_KNOCKBACK_RESISTANCE)
106            });
107            let knockback_power = (1.0 - distance)
108                * f64::from(exposure)
109                * f64::from(knockback_multiplier)
110                * (1.0 - knockback_resistance);
111            let knockback = direction * knockback_power;
112            entity.push_impulse(knockback);
113
114            if REGISTRY.entity_types.is_in_tag(
115                entity.entity_type(),
116                &EntityTypeTag::REDIRECTABLE_PROJECTILE,
117            ) {
118                if let Some(projectile) = entity.as_projectile() {
119                    projectile.set_owner_entity(self.damage_source.causing_entity());
120                }
121            } else if let Some(player) = entity.as_player()
122                && !player.is_spectator()
123                && (player.game_mode() != GameType::Creative || !player.abilities.lock().flying)
124            {
125                self.hit_players.insert(player.id(), knockback);
126            }
127
128            entity.on_explosion_hit(self.source.as_deref());
129        }
130    }
131}