steel_core/entity/entities/objects/projectiles/
splash_potion.rs1use 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
30const MIN_REMAINING_TICKS: i32 = 20;
32
33#[entity_behavior(class = "ThrownSplashPotion")]
35pub struct SplashPotionEntity {
36 base: EntityBase,
38 entity_type: EntityTypeRef,
40 entity_data: SyncMutex<SplashPotionEntityData>,
42 projectile_base: ProjectileBase,
44}
45
46unsafe impl DowncastType for SplashPotionEntity {
48 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/splash_potion");
49}
50
51impl SplashPotionEntity {
52 #[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 #[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 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}