steel_core/entity/entities/objects/effects/
area_effect_cloud.rs1use std::sync::{Arc, Weak};
4
5use glam::DVec3;
6use rustc_hash::FxHashMap;
7use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
8use simdnbt::owned::NbtCompound;
9use simdnbt::{FromNbtTag as _, ToNbtTag as _};
10use steel_macros::entity_behavior;
11use steel_registry::MobEffectInstance as RegistryMobEffectInstance;
12use steel_registry::blocks::behavior::PushReaction;
13use steel_registry::data_components::{PotionContents, vanilla_components};
14use steel_registry::entity_data::EntityPose;
15use steel_registry::entity_type::{EntityDimensions, EntityTypeRef};
16use steel_registry::item_stack::ItemStack;
17use steel_registry::particle_type::{ColorParticleOption, ParticleData};
18use steel_registry::vanilla_entity_data::AreaEffectCloudEntityData;
19use steel_registry::vanilla_particle_types;
20use steel_utils::locks::SyncMutex;
21use steel_utils::nbt::NbtValueInput as _;
22use steel_utils::{DowncastType, DowncastTypeKey, RgbColor};
23
24use crate::behavior::MOB_EFFECT_BEHAVIORS;
25use crate::entity::damage::DamageSource;
26use crate::entity::potion_contents::to_runtime_instance;
27use crate::entity::{
28 Entity, EntityBase, EntityBaseLoad, EntityReference, EntitySyncedData, LivingEntity,
29 RemovalReason, SharedEntity,
30};
31use crate::world::World;
32
33const TIME_BETWEEN_APPLICATIONS: i32 = 5;
35const MAX_RADIUS: f32 = 32.0;
36const MINIMAL_RADIUS: f32 = 0.5;
38const DEFAULT_RADIUS: f32 = 3.0;
39const HEIGHT: f32 = 0.5;
40const INFINITE_DURATION: i32 = -1;
42const DEFAULT_WAIT_TIME: i32 = 20;
43const DEFAULT_REAPPLICATION_DELAY: i32 = 20;
44const DEFAULT_POTION_DURATION_SCALE: f32 = 1.0;
45const INSTANTANEOUS_EFFECT_SCALE: f64 = 0.5;
47
48struct AreaEffectCloudState {
50 potion_contents: PotionContents,
51 potion_duration_scale: f32,
52 victims: FxHashMap<i32, i32>,
54 duration: i32,
55 wait_time: i32,
56 reapplication_delay: i32,
57 duration_on_use: i32,
58 radius_on_use: f32,
59 radius_per_tick: f32,
60 owner: Option<EntityReference>,
61}
62
63impl AreaEffectCloudState {
64 fn new() -> Self {
65 Self {
66 potion_contents: PotionContents::empty(),
67 potion_duration_scale: DEFAULT_POTION_DURATION_SCALE,
68 victims: FxHashMap::default(),
69 duration: INFINITE_DURATION,
70 wait_time: DEFAULT_WAIT_TIME,
71 reapplication_delay: DEFAULT_REAPPLICATION_DELAY,
72 duration_on_use: 0,
73 radius_on_use: 0.0,
74 radius_per_tick: 0.0,
75 owner: None,
76 }
77 }
78}
79
80#[entity_behavior(class = "AreaEffectCloud")]
85pub struct AreaEffectCloudEntity {
86 base: EntityBase,
87 entity_type: EntityTypeRef,
88 entity_data: SyncMutex<AreaEffectCloudEntityData>,
89 state: SyncMutex<AreaEffectCloudState>,
90}
91
92unsafe impl DowncastType for AreaEffectCloudEntity {
94 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/area_effect_cloud");
95}
96
97impl AreaEffectCloudEntity {
98 #[must_use]
100 pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
101 Self {
102 base: EntityBase::new(id, position, entity_type.dimensions, world),
103 entity_type,
104 entity_data: SyncMutex::new(AreaEffectCloudEntityData::new()),
105 state: SyncMutex::new(AreaEffectCloudState::new()),
106 }
107 }
108
109 #[must_use]
111 pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
112 Self {
113 base: EntityBase::from_load(load, entity_type.dimensions),
114 entity_type,
115 entity_data: SyncMutex::new(AreaEffectCloudEntityData::new()),
116 state: SyncMutex::new(AreaEffectCloudState::new()),
117 }
118 }
119
120 #[must_use]
122 pub fn radius(&self) -> f32 {
123 *self.entity_data.lock().radius.get()
124 }
125
126 pub fn set_radius(&self, radius: f32) {
128 self.entity_data
129 .lock()
130 .radius
131 .set(radius.clamp(0.0, MAX_RADIUS));
132 self.refresh_dimensions();
133 }
134
135 #[must_use]
137 pub fn is_waiting(&self) -> bool {
138 *self.entity_data.lock().waiting.get()
139 }
140
141 fn set_waiting(&self, waiting: bool) {
142 self.entity_data.lock().waiting.set(waiting);
143 }
144
145 pub fn set_potion_contents(&self, contents: PotionContents) {
147 let color = contents.get_color();
148 self.state.lock().potion_contents = contents;
149 self.update_particle(color);
150 }
151
152 pub fn set_potion_duration_scale(&self, scale: f32) {
154 self.state.lock().potion_duration_scale = scale;
155 }
156
157 fn update_particle(&self, potion_color: i32) {
158 let color = RgbColor::new(potion_color).with_alpha(u8::MAX);
159 self.entity_data.lock().particle.set(ParticleData::new(
160 &vanilla_particle_types::ENTITY_EFFECT,
161 ColorParticleOption::new(color),
162 ));
163 }
164
165 #[must_use]
167 pub fn duration(&self) -> i32 {
168 self.state.lock().duration
169 }
170
171 pub fn set_duration(&self, duration: i32) {
173 self.state.lock().duration = duration;
174 }
175
176 pub fn set_radius_on_use(&self, radius_on_use: f32) {
178 self.state.lock().radius_on_use = radius_on_use;
179 }
180
181 pub fn set_radius_per_tick(&self, radius_per_tick: f32) {
183 self.state.lock().radius_per_tick = radius_per_tick;
184 }
185
186 pub fn set_wait_time(&self, wait_time: i32) {
188 self.state.lock().wait_time = wait_time;
189 }
190
191 pub fn set_owner(&self, owner: Option<&SharedEntity>) {
193 self.state.lock().owner = owner
194 .filter(|owner| owner.as_living_entity().is_some())
195 .map(EntityReference::from_entity);
196 }
197
198 #[must_use]
200 pub fn get_owner(&self) -> Option<SharedEntity> {
201 let world = self.level()?;
202 let owner = self.state.lock().owner.clone()?;
203 owner
204 .get_entity(&world)
205 .filter(|owner| owner.as_living_entity().is_some())
206 }
207
208 fn server_tick(self: &Arc<Self>, world: &World) {
209 let tick_count = self.tick_count();
210 let (duration, wait_time, radius_per_tick) = {
211 let state = self.state.lock();
212 (state.duration, state.wait_time, state.radius_per_tick)
213 };
214 if duration != INFINITE_DURATION && tick_count.wrapping_sub(wait_time) >= duration {
215 self.set_removed(RemovalReason::Discarded);
216 return;
217 }
218
219 let should_wait = tick_count < wait_time;
220 if self.is_waiting() != should_wait {
221 self.set_waiting(should_wait);
222 }
223 if should_wait {
224 return;
225 }
226
227 let mut radius = self.radius();
228 if radius_per_tick != 0.0 {
229 radius += radius_per_tick;
230 if radius < MINIMAL_RADIUS {
231 self.set_removed(RemovalReason::Discarded);
232 return;
233 }
234 self.set_radius(radius);
235 }
236
237 if tick_count % TIME_BETWEEN_APPLICATIONS == 0 {
238 self.apply_effects(world, tick_count, radius);
239 }
240 }
241
242 fn apply_effects(self: &Arc<Self>, world: &World, tick_count: i32, mut radius: f32) {
244 let (effects, reapplication_delay) = {
245 let mut state = self.state.lock();
246 state.victims.retain(|_, until| tick_count < *until);
247 if !state.potion_contents.has_effects() {
248 state.victims.clear();
249 return;
250 }
251 let scale = state.potion_duration_scale;
252 let effects: Vec<_> = state
253 .potion_contents
254 .all_effects()
255 .iter()
256 .map(|effect| effect.with_scaled_duration(scale))
257 .collect();
258 (effects, state.reapplication_delay)
259 };
260
261 let entities = world.get_entities_in_aabb_matching(&self.bounding_box(), |entity| {
262 !entity.is_spectator() && entity.as_living_entity().is_some()
263 });
264 if entities.is_empty() {
265 return;
266 }
267
268 let this: SharedEntity = Arc::<Self>::clone(self);
269 let owner = self.get_owner();
270 let position = self.position();
271
272 for entity in entities {
273 let Some(living) = entity.as_living_entity() else {
274 continue;
275 };
276 if self.state.lock().victims.contains_key(&entity.id())
277 || !living.is_affected_by_potions()
278 || !effects.iter().any(|effect| {
279 living.can_be_affected(&to_runtime_instance(effect, effect.duration()))
280 })
281 {
282 continue;
283 }
284
285 let offset = entity.position() - position;
286 let dist = offset.x * offset.x + offset.z * offset.z;
287 if dist > f64::from(radius * radius) {
288 continue;
289 }
290
291 let (radius_on_use, duration_on_use) = {
292 let mut state = self.state.lock();
293 state
294 .victims
295 .insert(entity.id(), tick_count.wrapping_add(reapplication_delay));
296 (state.radius_on_use, state.duration_on_use)
297 };
298
299 for effect in &effects {
300 self.apply_effect(world, living, effect, &this, owner.as_ref());
301 }
302
303 if radius_on_use != 0.0 {
304 radius += radius_on_use;
305 if radius < MINIMAL_RADIUS {
306 self.set_removed(RemovalReason::Discarded);
307 return;
308 }
309 self.set_radius(radius);
310 }
311
312 if duration_on_use != 0 && self.add_duration(duration_on_use) {
313 self.set_removed(RemovalReason::Discarded);
314 return;
315 }
316 }
317 }
318
319 fn apply_effect(
320 &self,
321 world: &World,
322 living: &dyn LivingEntity,
323 effect: &RegistryMobEffectInstance,
324 this: &SharedEntity,
325 owner: Option<&SharedEntity>,
326 ) {
327 let behavior = MOB_EFFECT_BEHAVIORS.get_behavior(effect.effect());
328 if let Some(instantaneous) = behavior.as_instantaneous() {
329 instantaneous.apply_instantaneous(
330 world,
331 living,
332 effect.amplifier(),
333 Some(this),
334 owner,
335 INSTANTANEOUS_EFFECT_SCALE,
336 );
337 } else {
338 living.add_mob_effect_with_source(
339 to_runtime_instance(effect, effect.duration()),
340 Some(self as &dyn Entity),
341 );
342 }
343 }
344
345 fn add_duration(&self, delta: i32) -> bool {
347 let mut state = self.state.lock();
348 if state.duration == INFINITE_DURATION {
349 return false;
350 }
351 state.duration = state.duration.wrapping_add(delta);
352 state.duration <= 0
353 }
354}
355
356impl Entity for AreaEffectCloudEntity {
357 fn base(&self) -> &EntityBase {
358 &self.base
359 }
360
361 fn entity_type(&self) -> EntityTypeRef {
362 self.entity_type
363 }
364
365 fn tick(self: Arc<Self>) {
366 self.default_tick();
367 if let Some(world) = self.level() {
370 self.server_tick(&world);
371 }
372 }
373
374 fn no_physics(&self) -> bool {
375 true
376 }
377
378 fn piston_push_reaction(&self) -> PushReaction {
379 PushReaction::IgnoreEntity
380 }
381
382 fn dimensions_for_pose(&self, _pose: EntityPose) -> EntityDimensions {
383 EntityDimensions::with_default_eye_height(self.radius() * 2.0, HEIGHT)
384 }
385
386 fn hurt(&self, _world: &World, _source: &DamageSource, _amount: f32) -> bool {
387 false
388 }
389
390 fn restore_owner_reference(&self, owner: &SharedEntity) {
391 if let Some(reference) = &self.state.lock().owner {
392 reference.cache_entity(owner);
393 }
394 }
395
396 fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
397 Some(&self.entity_data)
398 }
399
400 fn apply_implicit_item_components(&self, item_stack: &ItemStack) {
401 if let Some(contents) = item_stack.get(vanilla_components::POTION_CONTENTS) {
402 self.set_potion_contents(contents.clone());
403 }
404 if let Some(scale) = item_stack.get(vanilla_components::POTION_DURATION_SCALE) {
405 self.set_potion_duration_scale(*scale);
406 }
407 }
408
409 fn save_additional(&self, nbt: &mut NbtCompound) {
410 nbt.insert("Age", self.tick_count());
411 let state = self.state.lock();
412 nbt.insert("Duration", state.duration);
413 nbt.insert("WaitTime", state.wait_time);
414 nbt.insert("ReapplicationDelay", state.reapplication_delay);
415 nbt.insert("DurationOnUse", state.duration_on_use);
416 nbt.insert("RadiusOnUse", state.radius_on_use);
417 nbt.insert("RadiusPerTick", state.radius_per_tick);
418 nbt.insert("Radius", self.radius());
419 if let Some(owner) = &state.owner {
420 owner.store(nbt, "Owner");
421 }
422 if state.potion_contents != PotionContents::empty() {
423 nbt.insert(
424 "potion_contents",
425 state.potion_contents.clone().to_nbt_tag(),
426 );
427 }
428 if state.potion_duration_scale != DEFAULT_POTION_DURATION_SCALE {
429 nbt.insert("potion_duration_scale", state.potion_duration_scale);
430 }
431 }
432
433 fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
434 self.base.set_tick_count(nbt.int_value("Age").unwrap_or(0));
435 {
436 let mut state = self.state.lock();
437 state.duration = nbt.int_value("Duration").unwrap_or(INFINITE_DURATION);
438 state.wait_time = nbt.int_value("WaitTime").unwrap_or(DEFAULT_WAIT_TIME);
439 state.reapplication_delay = nbt
440 .int_value("ReapplicationDelay")
441 .unwrap_or(DEFAULT_REAPPLICATION_DELAY);
442 state.duration_on_use = nbt.int_value("DurationOnUse").unwrap_or(0);
443 state.radius_on_use = nbt.float_value("RadiusOnUse").unwrap_or(0.0);
444 state.radius_per_tick = nbt.float_value("RadiusPerTick").unwrap_or(0.0);
445 state.owner = EntityReference::read(&nbt, "Owner");
446 state.potion_duration_scale = nbt
447 .float_value("potion_duration_scale")
448 .unwrap_or(DEFAULT_POTION_DURATION_SCALE);
449 }
450 self.set_radius(nbt.float_value("Radius").unwrap_or(DEFAULT_RADIUS));
451 self.set_potion_contents(
452 nbt.get("potion_contents")
453 .and_then(PotionContents::from_nbt_tag)
454 .unwrap_or_else(PotionContents::empty),
455 );
456 }
457}
458
459#[cfg(test)]
460mod tests;