Skip to main content

steel_core/entity/entities/objects/projectiles/
splash_potion.rs

1//! Splash potion projectile with distance scaled effects and water interactions
2
3use std::sync::{Arc, Weak};
4
5use glam::DVec3;
6use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
7use simdnbt::owned::NbtCompound;
8use steel_macros::entity_behavior;
9use steel_protocol::packets::game::SoundSource;
10use steel_registry::data_components::{PotionContents, vanilla_components};
11use steel_registry::entity_type::EntityTypeRef;
12use steel_registry::item_stack::ItemStack;
13use steel_registry::items::ItemRef;
14use steel_registry::vanilla_entity_data::SplashPotionEntityData;
15use steel_registry::{MobEffectInstance as RegistryMobEffectInstance, vanilla_items};
16use steel_utils::locks::SyncMutex;
17use steel_utils::{DowncastType, DowncastTypeKey};
18
19use crate::behavior::MOB_EFFECT_BEHAVIORS;
20use crate::entity::damage::DamageSource;
21use crate::entity::potion_contents::to_runtime_instance_icon_from_visibility;
22use crate::entity::{
23    AbstractThrownPotion, Entity, EntityBase, EntityBaseLoad, EntitySyncedData, LivingEntity,
24    Projectile, ProjectileBase, ProjectileHit, SPLASH_HORIZONTAL_REACH, SPLASH_RANGE_SQ,
25    SPLASH_VERTICAL_REACH, SharedEntity, ThrowableItemProjectile, ThrowableProjectile,
26    compute_margin,
27};
28use crate::world::{ClipHitResult, World};
29
30/// Scaled effects lasting 20 ticks or less are skipped
31const MIN_REMAINING_TICKS: i32 = 20;
32
33/// A thrown splash potion.
34#[entity_behavior(class = "ThrownSplashPotion")]
35pub struct SplashPotionEntity {
36    /// Common entity fields (id, uuid, position, etc.).
37    base: EntityBase,
38    /// Vanilla entity type registered for this implementation.
39    entity_type: EntityTypeRef,
40    /// Synced data carrying the rendered item stack.
41    entity_data: SyncMutex<SplashPotionEntityData>,
42    /// Shared `Projectile` state (owner / left-owner / has-been-shot).
43    projectile_base: ProjectileBase,
44}
45
46// SAFETY: This key is owned by Steel and uniquely identifies `SplashPotionEntity`.
47unsafe impl DowncastType for SplashPotionEntity {
48    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/splash_potion");
49}
50
51impl SplashPotionEntity {
52    /// Creates a new thrown splash potion with no owner and the default rendered item.
53    #[must_use]
54    pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
55        Self {
56            base: EntityBase::new(id, position, entity_type.dimensions, world),
57            entity_type,
58            entity_data: SyncMutex::new(SplashPotionEntityData::new()),
59            projectile_base: ProjectileBase::new(),
60        }
61    }
62
63    /// Creates a thrown splash potion from saved base data.
64    #[must_use]
65    pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
66        Self {
67            base: EntityBase::from_load(load, entity_type.dimensions),
68            entity_type,
69            entity_data: SyncMutex::new(SplashPotionEntityData::new()),
70            projectile_base: ProjectileBase::new(),
71        }
72    }
73
74    /// Applies one effect with distance falloff and duration scaling
75    fn apply_effect(
76        self: &Arc<Self>,
77        world: &World,
78        living: &dyn LivingEntity,
79        effect: &RegistryMobEffectInstance,
80        scale: f64,
81        duration_scale: f32,
82        owner: Option<&SharedEntity>,
83    ) {
84        let behavior = MOB_EFFECT_BEHAVIORS.get_behavior(effect.effect());
85        if let Some(instantaneous) = behavior.as_instantaneous() {
86            let this: SharedEntity = Arc::<Self>::clone(self);
87            instantaneous.apply_instantaneous(
88                world,
89                living,
90                effect.amplifier(),
91                Some(&this),
92                owner,
93                scale,
94            );
95            return;
96        }
97
98        let duration = effect.map_duration(|base_duration| {
99            (scale * f64::from(base_duration) * f64::from(duration_scale) + 0.5) as i32
100        });
101        let scaled_effect = to_runtime_instance_icon_from_visibility(effect, duration);
102        if scaled_effect.ends_within(MIN_REMAINING_TICKS) {
103            return;
104        }
105        let effect_source: &dyn Entity = owner.map_or(self.as_ref(), |owner| owner.as_ref());
106        living.add_mob_effect_with_source(scaled_effect, Some(effect_source));
107    }
108}
109
110impl Entity for SplashPotionEntity {
111    fn base(&self) -> &EntityBase {
112        &self.base
113    }
114
115    fn entity_type(&self) -> EntityTypeRef {
116        self.entity_type
117    }
118
119    fn tick(self: Arc<Self>) {
120        self.throwable_projectile_tick();
121    }
122
123    fn get_default_gravity(&self) -> f64 {
124        self.thrown_potion_default_gravity()
125    }
126
127    fn sound_source(&self) -> SoundSource {
128        SoundSource::Neutral
129    }
130
131    fn spawn_data(&self) -> i32 {
132        self.get_owner().map_or(0, |owner| owner.id())
133    }
134
135    fn restore_owner_reference(&self, owner: &SharedEntity) {
136        self.cache_owner_entity(owner);
137    }
138
139    fn projectile_owner_uuid(&self) -> Option<uuid::Uuid> {
140        self.owner_uuid()
141    }
142
143    fn projectile_owner(&self) -> Option<SharedEntity> {
144        self.get_owner()
145    }
146
147    fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
148        Some(&self.entity_data)
149    }
150
151    fn save_additional(&self, nbt: &mut NbtCompound) {
152        self.save_projectile(nbt);
153        self.save_throwable_item(nbt);
154    }
155
156    fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
157        self.load_projectile(nbt);
158        self.load_throwable_item(nbt);
159    }
160}
161
162impl Projectile for SplashPotionEntity {
163    fn projectile_base(&self) -> &ProjectileBase {
164        &self.projectile_base
165    }
166
167    fn calculate_horizontal_hurt_knockback_direction(
168        &self,
169        hurt_entity: &dyn LivingEntity,
170        _damage_source: &DamageSource,
171    ) -> (f64, f64) {
172        self.thrown_potion_knockback_direction(hurt_entity)
173    }
174
175    fn on_hit(self: Arc<Self>, hit: &ProjectileHit) {
176        self.thrown_potion_on_hit(hit);
177    }
178
179    fn on_hit_block(self: Arc<Self>, hit: &ClipHitResult) {
180        self.thrown_potion_on_hit_block(hit);
181    }
182}
183
184impl ThrowableProjectile for SplashPotionEntity {}
185
186impl ThrowableItemProjectile for SplashPotionEntity {
187    fn get_default_item(&self) -> ItemRef {
188        &vanilla_items::SPLASH_POTION
189    }
190
191    fn set_item(&self, item: ItemStack) {
192        self.entity_data
193            .lock()
194            .throwable_item_projectile
195            .item_stack
196            .set(item);
197    }
198
199    fn get_item(&self) -> ItemStack {
200        self.entity_data
201            .lock()
202            .throwable_item_projectile
203            .item_stack
204            .get()
205            .clone()
206    }
207}
208
209impl AbstractThrownPotion for SplashPotionEntity {
210    fn on_hit_as_potion(
211        self: &Arc<Self>,
212        world: &Arc<World>,
213        potion_item: &ItemStack,
214        hit: &ProjectileHit,
215    ) {
216        let contents = potion_item
217            .get_or_default(vanilla_components::POTION_CONTENTS, PotionContents::empty());
218        let duration_scale =
219            potion_item.get_or_default(vanilla_components::POTION_DURATION_SCALE, 1.0);
220        let mob_effects = contents.all_effects();
221
222        let potion_aabb = self
223            .bounding_box()
224            .translate(hit.location() - self.position());
225        let effect_aabb = potion_aabb.inflate_xyz(
226            SPLASH_HORIZONTAL_REACH,
227            SPLASH_VERTICAL_REACH,
228            SPLASH_HORIZONTAL_REACH,
229        );
230        let entities =
231            world.get_entities_in_aabb_matching(&effect_aabb, |entity| !entity.is_spectator());
232        if entities.is_empty() {
233            return;
234        }
235
236        let margin = compute_margin(self.tick_count());
237        let owner = self.get_owner();
238
239        for entity in entities {
240            let Some(living) = entity.as_living_entity() else {
241                continue;
242            };
243            if !living.is_affected_by_potions() {
244                continue;
245            }
246
247            let dist = potion_aabb.distance_to_sqr_aabb(entity.bounding_box().inflate(margin));
248            if dist >= SPLASH_RANGE_SQ {
249                continue;
250            }
251            let scale = 1.0 - dist.sqrt() / 4.0;
252
253            for effect in &mob_effects {
254                self.apply_effect(world, living, effect, scale, duration_scale, owner.as_ref());
255            }
256        }
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use std::sync::Arc;
263
264    use glam::DVec3;
265    use steel_registry::{init_vanilla_registry, vanilla_entities};
266    use steel_utils::{BlockPos, Direction};
267
268    use crate::behavior::init_behaviors;
269    use crate::entity::{Entity, Projectile, ProjectileHit};
270    use crate::test_support::test_world;
271    use crate::world::ClipHitResult;
272
273    use super::SplashPotionEntity;
274
275    #[test]
276    fn on_hit_discards_the_splash_potion() {
277        init_vanilla_registry();
278        init_behaviors();
279
280        let world = test_world();
281        let potion = Arc::new(SplashPotionEntity::new(
282            &vanilla_entities::SPLASH_POTION,
283            1,
284            DVec3::ZERO,
285            Arc::downgrade(world),
286        ));
287        let hit = ProjectileHit::Block {
288            location: DVec3::ZERO,
289            hit: ClipHitResult {
290                location: DVec3::ZERO,
291                direction: Direction::Up,
292                block_pos: BlockPos::new(0, 0, 0),
293                miss: false,
294                inside: false,
295                world_border_hit: false,
296            },
297        };
298
299        Arc::clone(&potion).on_hit(&hit);
300        assert!(potion.is_removed());
301    }
302}