Skip to main content

steel_core/entity/projectile/
thrown_potion.rs

1//! Shared impact and water interaction logic for thrown potions
2
3use std::sync::Arc;
4
5use steel_registry::blocks::block_state_ext::BlockStateExt as _;
6use steel_registry::blocks::properties::BlockStateProperties;
7use steel_registry::data_components::{PotionContents, vanilla_components};
8use steel_registry::item_stack::ItemStack;
9use steel_registry::potion::Potion;
10use steel_registry::vanilla_block_tags::BlockTag;
11use steel_registry::vanilla_potion_tags::PotionTag;
12use steel_registry::{level_events, vanilla_damage_types};
13use steel_utils::{BlockPos, BlockStateId, Direction, types::UpdateFlags};
14
15use crate::behavior::MOB_EFFECT_BEHAVIORS;
16use crate::behavior::blocks::{CampfireBlock, CandleBlock};
17use crate::entity::damage::DamageSource;
18use crate::entity::projectile::ThrowableItemProjectile;
19use crate::entity::{LivingEntity, ProjectileHit, RemovalReason, SharedEntity};
20use crate::world::ClipHitResult;
21use crate::world::World;
22
23/// Squared distance within which a splash reaches an entity.
24pub const SPLASH_RANGE_SQ: f64 = 16.0;
25/// How far the splash search box extends past the potion's box on the X and Z axes.
26pub const SPLASH_HORIZONTAL_REACH: f64 = 4.0;
27/// How far the splash search box extends past the potion's box on the Y axis.
28pub const SPLASH_VERTICAL_REACH: f64 = 2.0;
29/// Gravity of a thrown potion.
30const DEFAULT_GRAVITY: f64 = 0.05;
31
32/// Shared behavior for splash and lingering potion projectiles
33pub trait AbstractThrownPotion: ThrowableItemProjectile {
34    /// Returns the gravity of thrown potions (default is 0.05)
35    fn thrown_potion_default_gravity(&self) -> f64 {
36        DEFAULT_GRAVITY
37    }
38
39    /// The effect-specific area-of-effect (splash) or cloud-spawning (lingering) behavior.
40    fn on_hit_as_potion(
41        self: &Arc<Self>,
42        world: &Arc<World>,
43        potion_item: &ItemStack,
44        hit: &ProjectileHit,
45    );
46
47    /// Returns the horizontal direction from the potion to the hurt entity
48    fn thrown_potion_knockback_direction(&self, hurt_entity: &dyn LivingEntity) -> (f64, f64) {
49        let delta = hurt_entity.position() - self.position();
50        (delta.x, delta.z)
51    }
52
53    /// Handles impact effects emits the break event and discards the potion
54    fn thrown_potion_on_hit(self: Arc<Self>, hit: &ProjectileHit)
55    where
56        Self: Sized + 'static,
57    {
58        Arc::clone(&self).projectile_on_hit(hit);
59        let Some(world) = self.level() else {
60            return;
61        };
62
63        let potion_item = self.get_item();
64        let contents = potion_item
65            .get_or_default(vanilla_components::POTION_CONTENTS, PotionContents::empty());
66        self.affect_entities_around(&world, &contents);
67        if contents.has_effects() {
68            self.on_hit_as_potion(&world, &potion_item, hit);
69        }
70
71        let has_instant_effects = contents
72            .potion()
73            .is_some_and(|potion| potion_has_instant_effects(potion.value()));
74        let (event_type, sound_event) = if has_instant_effects {
75            (
76                level_events::PARTICLES_INSTANT_POTION_SPLASH,
77                level_events::SOUND_INSTANT_POTION_SPLASH,
78            )
79        } else {
80            (
81                level_events::PARTICLES_SPELL_POTION_SPLASH,
82                level_events::SOUND_SPELL_POTION_SPLASH,
83            )
84        };
85        world.level_event(
86            event_type,
87            self.block_position(),
88            contents.get_color(),
89            None,
90        );
91        if !self.is_silent() {
92            world.level_event(sound_event, self.block_position(), 0, None);
93        }
94        self.set_removed(RemovalReason::Discarded);
95    }
96
97    /// Hurts water-sensitive entities in range and extinguishes burning ones,
98    /// as allowed by the potion's tags.
99    fn affect_entities_around(self: &Arc<Self>, world: &Arc<World>, contents: &PotionContents)
100    where
101        Self: Sized + 'static,
102    {
103        let hurts_water_sensitive = contents.is_in_tag(&PotionTag::HURTS_WATER_SENSITIVE_ENTITIES);
104        let extinguishes = contents.is_in_tag(&PotionTag::EXTINGUISHES_ENTITIES);
105        // TODO: rehydrate nearby Axolotl entities for `REHYDRATES_AXOLOTLS` once
106        // Axolotl is implemented.
107        if !hurts_water_sensitive && !extinguishes {
108            return;
109        }
110
111        let aabb = self.bounding_box().inflate_xyz(
112            SPLASH_HORIZONTAL_REACH,
113            SPLASH_VERTICAL_REACH,
114            SPLASH_HORIZONTAL_REACH,
115        );
116        let potion_pos = self.position();
117
118        for entity in world.get_entities_in_aabb(&aabb) {
119            if entity.as_living_entity().is_none() {
120                continue;
121            }
122            let sensitive_to_water = entity.entity_type().flags.is_sensitive_to_water;
123            if !sensitive_to_water && !entity.is_on_fire() {
124                continue;
125            }
126            if potion_pos.distance_squared(entity.position()) >= SPLASH_RANGE_SQ {
127                continue;
128            }
129
130            if hurts_water_sensitive && sensitive_to_water {
131                let mut damage = DamageSource::environment(&vanilla_damage_types::INDIRECT_MAGIC)
132                    .with_direct_entity(Arc::<Self>::clone(self));
133                if let Some(owner) = self.get_owner() {
134                    damage = damage.with_causing_entity(owner);
135                }
136                entity.hurt(world, &damage, 1.0);
137            }
138
139            if extinguishes && entity.is_on_fire() && entity.is_alive() {
140                entity.extinguish_fire();
141            }
142        }
143    }
144
145    /// Handles block impact and douses nearby fire for `DOUSES_FIRE` potions.
146    fn thrown_potion_on_hit_block(&self, hit: &ClipHitResult) {
147        self.projectile_on_hit_block(hit);
148        let Some(world) = self.level() else {
149            return;
150        };
151
152        let contents = self
153            .get_item()
154            .get_or_default(vanilla_components::POTION_CONTENTS, PotionContents::empty());
155        if !contents.is_in_tag(&PotionTag::DOUSES_FIRE) {
156            return;
157        }
158
159        let block_effect_pos = hit.direction.relative(hit.block_pos);
160        self.douse_fire(&world, block_effect_pos);
161        self.douse_fire(&world, hit.direction.opposite().relative(block_effect_pos));
162        for direction in Direction::HORIZONTAL {
163            self.douse_fire(&world, direction.relative(block_effect_pos));
164        }
165    }
166
167    /// Puts out fire, lit campfires and lit candles at `pos`.
168    fn douse_fire(&self, world: &Arc<World>, pos: BlockPos) {
169        let state = world.get_block_state(pos);
170        if state.get_block().has_tag(&BlockTag::FIRE) {
171            world.destroy_block_by_entity(pos, false, self.as_entity_event_source());
172        } else if CandleBlock::is_lit(state) {
173            CandleBlock::extinguish(None, state, world, pos);
174        } else if CampfireBlock::is_lit_campfire(state) {
175            douse_lit_campfire(world, pos, state, self.get_owner());
176        }
177    }
178}
179
180/// Checks for instant effects in the base potion excluding custom effects
181fn potion_has_instant_effects(potion: &Potion) -> bool {
182    potion.effects.iter().any(|effect| {
183        MOB_EFFECT_BEHAVIORS
184            .get_behavior(effect.effect)
185            .as_instantaneous()
186            .is_some()
187    })
188}
189
190/// Plays the extinguish event, douses the campfire and clears `LIT`.
191fn douse_lit_campfire(
192    world: &Arc<World>,
193    pos: BlockPos,
194    state: BlockStateId,
195    owner: Option<SharedEntity>,
196) {
197    world.level_event(level_events::SOUND_EXTINGUISH_FIRE, pos, 0, None);
198    CampfireBlock::douse(owner.as_deref(), world, pos);
199    world.set_block(
200        pos,
201        state.set_value(&BlockStateProperties::LIT, false),
202        UpdateFlags::UPDATE_ALL,
203    );
204}