steel_core/entity/projectile/
thrown_potion.rs1use 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
23pub const SPLASH_RANGE_SQ: f64 = 16.0;
25pub const SPLASH_HORIZONTAL_REACH: f64 = 4.0;
27pub const SPLASH_VERTICAL_REACH: f64 = 2.0;
29const DEFAULT_GRAVITY: f64 = 0.05;
31
32pub trait AbstractThrownPotion: ThrowableItemProjectile {
34 fn thrown_potion_default_gravity(&self) -> f64 {
36 DEFAULT_GRAVITY
37 }
38
39 fn on_hit_as_potion(
41 self: &Arc<Self>,
42 world: &Arc<World>,
43 potion_item: &ItemStack,
44 hit: &ProjectileHit,
45 );
46
47 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 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 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 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 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 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
180fn 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
190fn 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}