package net.minecraft.world.entity; import com.google.common.collect.Maps; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.UUID; import net.minecraft.core.UUIDUtil; import net.minecraft.core.component.DataComponentGetter; import net.minecraft.core.component.DataComponentType; import net.minecraft.core.component.DataComponents; import net.minecraft.core.particles.ColorParticleOption; import net.minecraft.core.particles.ParticleOptions; import net.minecraft.core.particles.ParticleTypes; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.NbtOps; import net.minecraft.nbt.Tag; import net.minecraft.network.syncher.EntityDataAccessor; import net.minecraft.network.syncher.EntityDataSerializers; import net.minecraft.network.syncher.SynchedEntityData; import net.minecraft.network.syncher.SynchedEntityData.Builder; import net.minecraft.resources.RegistryOps; import net.minecraft.server.level.ServerLevel; import net.minecraft.util.ARGB; import net.minecraft.util.Mth; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.effect.MobEffectInstance; import net.minecraft.world.item.alchemy.PotionContents; import net.minecraft.world.level.Level; import net.minecraft.world.level.material.PushReaction; import org.jetbrains.annotations.Nullable; public class AreaEffectCloud extends Entity implements TraceableEntity { private static final int TIME_BETWEEN_APPLICATIONS = 5; private static final EntityDataAccessor DATA_RADIUS = SynchedEntityData.defineId(AreaEffectCloud.class, EntityDataSerializers.FLOAT); private static final EntityDataAccessor DATA_WAITING = SynchedEntityData.defineId(AreaEffectCloud.class, EntityDataSerializers.BOOLEAN); private static final EntityDataAccessor DATA_PARTICLE = SynchedEntityData.defineId(AreaEffectCloud.class, EntityDataSerializers.PARTICLE); private static final float MAX_RADIUS = 32.0F; private static final int DEFAULT_AGE = 0; private static final int DEFAULT_DURATION_ON_USE = 0; private static final float DEFAULT_RADIUS_ON_USE = 0.0F; private static final float DEFAULT_RADIUS_PER_TICK = 0.0F; private static final float DEFAULT_POTION_DURATION_SCALE = 1.0F; private static final float MINIMAL_RADIUS = 0.5F; private static final float DEFAULT_RADIUS = 3.0F; public static final float DEFAULT_WIDTH = 6.0F; public static final float HEIGHT = 0.5F; public static final int INFINITE_DURATION = -1; public static final int DEFAULT_LINGERING_DURATION = 600; private static final int DEFAULT_WAIT_TIME = 20; private static final int DEFAULT_REAPPLICATION_DELAY = 20; private static final ColorParticleOption DEFAULT_PARTICLE = ColorParticleOption.create(ParticleTypes.ENTITY_EFFECT, -1); private PotionContents potionContents = PotionContents.EMPTY; private float potionDurationScale = 1.0F; private final Map victims = Maps.newHashMap(); private int duration = -1; private int waitTime = 20; private int reapplicationDelay = 20; private int durationOnUse = 0; private float radiusOnUse = 0.0F; private float radiusPerTick = 0.0F; @Nullable private LivingEntity owner; @Nullable private UUID ownerUUID; public AreaEffectCloud(EntityType entityType, Level level) { super(entityType, level); this.noPhysics = true; } public AreaEffectCloud(Level level, double x, double y, double z) { this(EntityType.AREA_EFFECT_CLOUD, level); this.setPos(x, y, z); } @Override protected void defineSynchedData(Builder builder) { builder.define(DATA_RADIUS, 3.0F); builder.define(DATA_WAITING, false); builder.define(DATA_PARTICLE, DEFAULT_PARTICLE); } public void setRadius(float radius) { if (!this.level().isClientSide) { this.getEntityData().set(DATA_RADIUS, Mth.clamp(radius, 0.0F, 32.0F)); } } @Override public void refreshDimensions() { double d = this.getX(); double e = this.getY(); double f = this.getZ(); super.refreshDimensions(); this.setPos(d, e, f); } public float getRadius() { return this.getEntityData().get(DATA_RADIUS); } public void setPotionContents(PotionContents potionContents) { this.potionContents = potionContents; this.updateColor(); } public void setPotionDurationScale(float potionDurationScale) { this.potionDurationScale = potionDurationScale; } private void updateColor() { ParticleOptions particleOptions = this.entityData.get(DATA_PARTICLE); if (particleOptions instanceof ColorParticleOption colorParticleOption) { int i = this.potionContents.equals(PotionContents.EMPTY) ? 0 : this.potionContents.getColor(); this.entityData.set(DATA_PARTICLE, ColorParticleOption.create(colorParticleOption.getType(), ARGB.opaque(i))); } } public void addEffect(MobEffectInstance effectInstance) { this.setPotionContents(this.potionContents.withEffectAdded(effectInstance)); } public ParticleOptions getParticle() { return this.getEntityData().get(DATA_PARTICLE); } public void setParticle(ParticleOptions particleOption) { this.getEntityData().set(DATA_PARTICLE, particleOption); } /** * Sets if the cloud is waiting. While waiting, the radius is ignored and the cloud shows fewer particles in its area. */ protected void setWaiting(boolean waiting) { this.getEntityData().set(DATA_WAITING, waiting); } /** * Returns {@code true} if the cloud is waiting. While waiting, the radius is ignored and the cloud shows fewer particles in its area. */ public boolean isWaiting() { return this.getEntityData().get(DATA_WAITING); } public int getDuration() { return this.duration; } public void setDuration(int duration) { this.duration = duration; } @Override public void tick() { super.tick(); if (this.level() instanceof ServerLevel serverLevel) { this.serverTick(serverLevel); } else { this.clientTick(); } } private void clientTick() { boolean bl = this.isWaiting(); float f = this.getRadius(); if (!bl || !this.random.nextBoolean()) { ParticleOptions particleOptions = this.getParticle(); int i; float g; if (bl) { i = 2; g = 0.2F; } else { i = Mth.ceil((float) Math.PI * f * f); g = f; } for (int j = 0; j < i; j++) { float h = this.random.nextFloat() * (float) (Math.PI * 2); float k = Mth.sqrt(this.random.nextFloat()) * g; double d = this.getX() + Mth.cos(h) * k; double e = this.getY(); double l = this.getZ() + Mth.sin(h) * k; if (particleOptions.getType() == ParticleTypes.ENTITY_EFFECT) { if (bl && this.random.nextBoolean()) { this.level().addAlwaysVisibleParticle(ColorParticleOption.create(ParticleTypes.ENTITY_EFFECT, -1), d, e, l, 0.0, 0.0, 0.0); } else { this.level().addAlwaysVisibleParticle(particleOptions, d, e, l, 0.0, 0.0, 0.0); } } else if (bl) { this.level().addAlwaysVisibleParticle(particleOptions, d, e, l, 0.0, 0.0, 0.0); } else { this.level().addAlwaysVisibleParticle(particleOptions, d, e, l, (0.5 - this.random.nextDouble()) * 0.15, 0.01F, (0.5 - this.random.nextDouble()) * 0.15); } } } } private void serverTick(ServerLevel level) { if (this.duration != -1 && this.tickCount >= this.waitTime + this.duration) { this.discard(); } else { boolean bl = this.isWaiting(); boolean bl2 = this.tickCount < this.waitTime; if (bl != bl2) { this.setWaiting(bl2); } if (!bl2) { float f = this.getRadius(); if (this.radiusPerTick != 0.0F) { f += this.radiusPerTick; if (f < 0.5F) { this.discard(); return; } this.setRadius(f); } if (this.tickCount % 5 == 0) { this.victims.entrySet().removeIf(entry -> this.tickCount >= (Integer)entry.getValue()); if (!this.potionContents.hasEffects()) { this.victims.clear(); } else { List list = new ArrayList(); this.potionContents.forEachEffect(list::add, this.potionDurationScale); List list2 = this.level().getEntitiesOfClass(LivingEntity.class, this.getBoundingBox()); if (!list2.isEmpty()) { for (LivingEntity livingEntity : list2) { if (!this.victims.containsKey(livingEntity) && livingEntity.isAffectedByPotions() && !list.stream().noneMatch(livingEntity::canBeAffected)) { double d = livingEntity.getX() - this.getX(); double e = livingEntity.getZ() - this.getZ(); double g = d * d + e * e; if (g <= f * f) { this.victims.put(livingEntity, this.tickCount + this.reapplicationDelay); for (MobEffectInstance mobEffectInstance : list) { if (mobEffectInstance.getEffect().value().isInstantenous()) { mobEffectInstance.getEffect().value().applyInstantenousEffect(level, this, this.getOwner(), livingEntity, mobEffectInstance.getAmplifier(), 0.5); } else { livingEntity.addEffect(new MobEffectInstance(mobEffectInstance), this); } } if (this.radiusOnUse != 0.0F) { f += this.radiusOnUse; if (f < 0.5F) { this.discard(); return; } this.setRadius(f); } if (this.durationOnUse != 0 && this.duration != -1) { this.duration = this.duration + this.durationOnUse; if (this.duration <= 0) { this.discard(); return; } } } } } } } } } } } public float getRadiusOnUse() { return this.radiusOnUse; } public void setRadiusOnUse(float radiusOnUse) { this.radiusOnUse = radiusOnUse; } public float getRadiusPerTick() { return this.radiusPerTick; } public void setRadiusPerTick(float radiusPerTick) { this.radiusPerTick = radiusPerTick; } public int getDurationOnUse() { return this.durationOnUse; } public void setDurationOnUse(int durationOnUse) { this.durationOnUse = durationOnUse; } public int getWaitTime() { return this.waitTime; } public void setWaitTime(int waitTime) { this.waitTime = waitTime; } public void setOwner(@Nullable LivingEntity owner) { this.owner = owner; this.ownerUUID = owner == null ? null : owner.getUUID(); } @Nullable public LivingEntity getOwner() { if (this.owner != null && !this.owner.isRemoved()) { return this.owner; } else { if (this.ownerUUID != null && this.level() instanceof ServerLevel serverLevel) { this.owner = serverLevel.getEntity(this.ownerUUID) instanceof LivingEntity livingEntity ? livingEntity : null; } return this.owner; } } @Override protected void readAdditionalSaveData(CompoundTag tag) { this.tickCount = tag.getIntOr("Age", 0); this.duration = tag.getIntOr("Duration", -1); this.waitTime = tag.getIntOr("WaitTime", 20); this.reapplicationDelay = tag.getIntOr("ReapplicationDelay", 20); this.durationOnUse = tag.getIntOr("DurationOnUse", 0); this.radiusOnUse = tag.getFloatOr("RadiusOnUse", 0.0F); this.radiusPerTick = tag.getFloatOr("RadiusPerTick", 0.0F); this.setRadius(tag.getFloatOr("Radius", 3.0F)); this.ownerUUID = (UUID)tag.read("Owner", UUIDUtil.CODEC).orElse(null); RegistryOps registryOps = this.registryAccess().createSerializationContext(NbtOps.INSTANCE); this.setParticle((ParticleOptions)tag.read("Particle", ParticleTypes.CODEC, registryOps).orElse(DEFAULT_PARTICLE)); this.setPotionContents((PotionContents)tag.read("potion_contents", PotionContents.CODEC, registryOps).orElse(PotionContents.EMPTY)); this.potionDurationScale = tag.getFloatOr("potion_duration_scale", 1.0F); } @Override protected void addAdditionalSaveData(CompoundTag tag) { tag.putInt("Age", this.tickCount); tag.putInt("Duration", this.duration); tag.putInt("WaitTime", this.waitTime); tag.putInt("ReapplicationDelay", this.reapplicationDelay); tag.putInt("DurationOnUse", this.durationOnUse); tag.putFloat("RadiusOnUse", this.radiusOnUse); tag.putFloat("RadiusPerTick", this.radiusPerTick); tag.putFloat("Radius", this.getRadius()); RegistryOps registryOps = this.registryAccess().createSerializationContext(NbtOps.INSTANCE); tag.store("Particle", ParticleTypes.CODEC, registryOps, this.getParticle()); tag.storeNullable("Owner", UUIDUtil.CODEC, this.ownerUUID); if (!this.potionContents.equals(PotionContents.EMPTY)) { tag.store("potion_contents", PotionContents.CODEC, registryOps, this.potionContents); } if (this.potionDurationScale != 1.0F) { tag.putFloat("potion_duration_scale", this.potionDurationScale); } } @Override public void onSyncedDataUpdated(EntityDataAccessor dataAccessor) { if (DATA_RADIUS.equals(dataAccessor)) { this.refreshDimensions(); } super.onSyncedDataUpdated(dataAccessor); } @Override public PushReaction getPistonPushReaction() { return PushReaction.IGNORE; } @Override public EntityDimensions getDimensions(Pose pose) { return EntityDimensions.scalable(this.getRadius() * 2.0F, 0.5F); } @Override public final boolean hurtServer(ServerLevel level, DamageSource damageSource, float amount) { return false; } @Nullable @Override public T get(DataComponentType component) { if (component == DataComponents.POTION_CONTENTS) { return castComponentValue((DataComponentType)component, this.potionContents); } else { return component == DataComponents.POTION_DURATION_SCALE ? castComponentValue((DataComponentType)component, this.potionDurationScale) : super.get(component); } } @Override protected void applyImplicitComponents(DataComponentGetter componentGetter) { this.applyImplicitComponentIfPresent(componentGetter, DataComponents.POTION_CONTENTS); this.applyImplicitComponentIfPresent(componentGetter, DataComponents.POTION_DURATION_SCALE); super.applyImplicitComponents(componentGetter); } @Override protected boolean applyImplicitComponent(DataComponentType component, T value) { if (component == DataComponents.POTION_CONTENTS) { this.setPotionContents(castComponentValue(DataComponents.POTION_CONTENTS, value)); return true; } else if (component == DataComponents.POTION_DURATION_SCALE) { this.setPotionDurationScale(castComponentValue(DataComponents.POTION_DURATION_SCALE, value)); return true; } else { return super.applyImplicitComponent(component, value); } } }