Skip to main content

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

1//! Lingering potion projectile that leaves an area effect cloud on impact
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::entity_type::EntityTypeRef;
11use steel_registry::item_stack::ItemStack;
12use steel_registry::items::ItemRef;
13use steel_registry::vanilla_entity_data::LingeringPotionEntityData;
14use steel_registry::{vanilla_entities, vanilla_items};
15use steel_utils::locks::SyncMutex;
16use steel_utils::{DowncastType, DowncastTypeKey};
17
18use crate::entity::damage::DamageSource;
19use crate::entity::entities::AreaEffectCloudEntity;
20use crate::entity::{
21    AbstractThrownPotion, Entity, EntityBase, EntityBaseLoad, EntitySyncedData, LivingEntity,
22    Projectile, ProjectileBase, ProjectileHit, SharedEntity, ThrowableItemProjectile,
23    ThrowableProjectile, apply_implicit_item_stack_components, next_entity_id,
24};
25use crate::world::{ClipHitResult, World};
26
27const CLOUD_RADIUS: f32 = 3.0;
28const CLOUD_RADIUS_ON_USE: f32 = -0.5;
29const CLOUD_DURATION: i32 = 600;
30const CLOUD_WAIT_TIME: i32 = 10;
31
32/// A thrown lingering potion.
33#[entity_behavior(class = "ThrownLingeringPotion")]
34pub struct LingeringPotionEntity {
35    /// Common entity fields (id, uuid, position, etc.).
36    base: EntityBase,
37    /// Vanilla entity type registered for this implementation.
38    entity_type: EntityTypeRef,
39    /// Synced data carrying the rendered item stack.
40    entity_data: SyncMutex<LingeringPotionEntityData>,
41    /// Shared `Projectile` state (owner / left-owner / has-been-shot).
42    projectile_base: ProjectileBase,
43}
44
45// SAFETY: This key is owned by Steel and uniquely identifies `LingeringPotionEntity`.
46unsafe impl DowncastType for LingeringPotionEntity {
47    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/lingering_potion");
48}
49
50impl LingeringPotionEntity {
51    /// Creates a new thrown lingering potion with no owner and the default rendered item.
52    #[must_use]
53    pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
54        Self {
55            base: EntityBase::new(id, position, entity_type.dimensions, world),
56            entity_type,
57            entity_data: SyncMutex::new(LingeringPotionEntityData::new()),
58            projectile_base: ProjectileBase::new(),
59        }
60    }
61
62    /// Creates a thrown lingering potion from saved base data.
63    #[must_use]
64    pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
65        Self {
66            base: EntityBase::from_load(load, entity_type.dimensions),
67            entity_type,
68            entity_data: SyncMutex::new(LingeringPotionEntityData::new()),
69            projectile_base: ProjectileBase::new(),
70        }
71    }
72}
73
74impl Entity for LingeringPotionEntity {
75    fn base(&self) -> &EntityBase {
76        &self.base
77    }
78
79    fn entity_type(&self) -> EntityTypeRef {
80        self.entity_type
81    }
82
83    fn tick(self: Arc<Self>) {
84        self.throwable_projectile_tick();
85    }
86
87    fn get_default_gravity(&self) -> f64 {
88        self.thrown_potion_default_gravity()
89    }
90
91    fn sound_source(&self) -> SoundSource {
92        SoundSource::Neutral
93    }
94
95    fn spawn_data(&self) -> i32 {
96        self.get_owner().map_or(0, |owner| owner.id())
97    }
98
99    fn restore_owner_reference(&self, owner: &SharedEntity) {
100        self.cache_owner_entity(owner);
101    }
102
103    fn projectile_owner_uuid(&self) -> Option<uuid::Uuid> {
104        self.owner_uuid()
105    }
106
107    fn projectile_owner(&self) -> Option<SharedEntity> {
108        self.get_owner()
109    }
110
111    fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
112        Some(&self.entity_data)
113    }
114
115    fn save_additional(&self, nbt: &mut NbtCompound) {
116        self.save_projectile(nbt);
117        self.save_throwable_item(nbt);
118    }
119
120    fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
121        self.load_projectile(nbt);
122        self.load_throwable_item(nbt);
123    }
124}
125
126impl Projectile for LingeringPotionEntity {
127    fn projectile_base(&self) -> &ProjectileBase {
128        &self.projectile_base
129    }
130
131    fn calculate_horizontal_hurt_knockback_direction(
132        &self,
133        hurt_entity: &dyn LivingEntity,
134        _damage_source: &DamageSource,
135    ) -> (f64, f64) {
136        self.thrown_potion_knockback_direction(hurt_entity)
137    }
138
139    fn on_hit(self: Arc<Self>, hit: &ProjectileHit) {
140        self.thrown_potion_on_hit(hit);
141    }
142
143    fn on_hit_block(self: Arc<Self>, hit: &ClipHitResult) {
144        self.thrown_potion_on_hit_block(hit);
145    }
146}
147
148impl ThrowableProjectile for LingeringPotionEntity {}
149
150impl ThrowableItemProjectile for LingeringPotionEntity {
151    fn get_default_item(&self) -> ItemRef {
152        &vanilla_items::LINGERING_POTION
153    }
154
155    fn set_item(&self, item: ItemStack) {
156        self.entity_data
157            .lock()
158            .throwable_item_projectile
159            .item_stack
160            .set(item);
161    }
162
163    fn get_item(&self) -> ItemStack {
164        self.entity_data
165            .lock()
166            .throwable_item_projectile
167            .item_stack
168            .get()
169            .clone()
170    }
171}
172
173impl AbstractThrownPotion for LingeringPotionEntity {
174    fn on_hit_as_potion(
175        self: &Arc<Self>,
176        world: &Arc<World>,
177        potion_item: &ItemStack,
178        hit: &ProjectileHit,
179    ) {
180        let position = match hit {
181            ProjectileHit::Entity(entity_hit) => entity_hit.entity.position(),
182            ProjectileHit::Block { .. } => self.position(),
183        };
184        let cloud = Arc::new(AreaEffectCloudEntity::new(
185            &vanilla_entities::AREA_EFFECT_CLOUD,
186            next_entity_id(),
187            position,
188            Arc::downgrade(world),
189        ));
190        cloud.set_owner(self.get_owner().as_ref());
191        cloud.set_radius(CLOUD_RADIUS);
192        cloud.set_radius_on_use(CLOUD_RADIUS_ON_USE);
193        cloud.set_duration(CLOUD_DURATION);
194        cloud.set_wait_time(CLOUD_WAIT_TIME);
195        cloud.set_radius_per_tick(-cloud.radius() / cloud.duration() as f32);
196
197        let cloud: SharedEntity = cloud;
198        apply_implicit_item_stack_components(&cloud, potion_item);
199        if let Err(error) = world.try_add_entity(cloud) {
200            log::debug!("world rejected a lingering potion cloud: {error}");
201        }
202    }
203}