package net.minecraft.world.entity.monster; import com.google.common.annotations.VisibleForTesting; import java.util.EnumSet; import net.minecraft.core.BlockPos; import net.minecraft.core.particles.ParticleOptions; import net.minecraft.core.particles.ParticleTypes; import net.minecraft.nbt.CompoundTag; 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.server.level.ServerLevel; import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundSource; import net.minecraft.tags.BiomeTags; import net.minecraft.util.Mth; import net.minecraft.util.RandomSource; import net.minecraft.world.Difficulty; import net.minecraft.world.DifficultyInstance; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.effect.MobEffects; import net.minecraft.world.entity.ConversionParams; import net.minecraft.world.entity.ConversionType; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityDimensions; import net.minecraft.world.entity.EntitySpawnReason; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.Mob; import net.minecraft.world.entity.Pose; import net.minecraft.world.entity.SpawnGroupData; import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.entity.ai.control.MoveControl; import net.minecraft.world.entity.ai.control.MoveControl.Operation; import net.minecraft.world.entity.ai.goal.Goal; import net.minecraft.world.entity.ai.goal.Goal.Flag; import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; import net.minecraft.world.entity.animal.IronGolem; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.enchantment.EnchantmentHelper; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelAccessor; import net.minecraft.world.level.ServerLevelAccessor; import net.minecraft.world.level.WorldGenLevel; import net.minecraft.world.level.levelgen.WorldgenRandom; import net.minecraft.world.phys.Vec3; import net.minecraft.world.scores.PlayerTeam; import org.jetbrains.annotations.Nullable; public class Slime extends Mob implements Enemy { private static final EntityDataAccessor ID_SIZE = SynchedEntityData.defineId(Slime.class, EntityDataSerializers.INT); public static final int MIN_SIZE = 1; public static final int MAX_SIZE = 127; public static final int MAX_NATURAL_SIZE = 4; public float targetSquish; public float squish; public float oSquish; private boolean wasOnGround; public Slime(EntityType entityType, Level level) { super(entityType, level); this.fixupDimensions(); this.moveControl = new Slime.SlimeMoveControl(this); } @Override protected void registerGoals() { this.goalSelector.addGoal(1, new Slime.SlimeFloatGoal(this)); this.goalSelector.addGoal(2, new Slime.SlimeAttackGoal(this)); this.goalSelector.addGoal(3, new Slime.SlimeRandomDirectionGoal(this)); this.goalSelector.addGoal(5, new Slime.SlimeKeepOnJumpingGoal(this)); this.targetSelector .addGoal( 1, new NearestAttackableTargetGoal(this, Player.class, 10, true, false, (livingEntity, serverLevel) -> Math.abs(livingEntity.getY() - this.getY()) <= 4.0) ); this.targetSelector.addGoal(3, new NearestAttackableTargetGoal(this, IronGolem.class, true)); } @Override public SoundSource getSoundSource() { return SoundSource.HOSTILE; } @Override protected void defineSynchedData(Builder builder) { super.defineSynchedData(builder); builder.define(ID_SIZE, 1); } @VisibleForTesting public void setSize(int size, boolean resetHealth) { int i = Mth.clamp(size, 1, 127); this.entityData.set(ID_SIZE, i); this.reapplyPosition(); this.refreshDimensions(); this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(i * i); this.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(0.2F + 0.1F * i); this.getAttribute(Attributes.ATTACK_DAMAGE).setBaseValue(i); if (resetHealth) { this.setHealth(this.getMaxHealth()); } this.xpReward = i; } /** * Returns the size of the slime. */ public int getSize() { return this.entityData.get(ID_SIZE); } @Override public void addAdditionalSaveData(CompoundTag tag) { super.addAdditionalSaveData(tag); tag.putInt("Size", this.getSize() - 1); tag.putBoolean("wasOnGround", this.wasOnGround); } @Override public void readAdditionalSaveData(CompoundTag tag) { this.setSize(tag.getInt("Size") + 1, false); super.readAdditionalSaveData(tag); this.wasOnGround = tag.getBoolean("wasOnGround"); } public boolean isTiny() { return this.getSize() <= 1; } protected ParticleOptions getParticleType() { return ParticleTypes.ITEM_SLIME; } @Override protected boolean shouldDespawnInPeaceful() { return this.getSize() > 0; } @Override public void tick() { this.oSquish = this.squish; this.squish = this.squish + (this.targetSquish - this.squish) * 0.5F; super.tick(); if (this.onGround() && !this.wasOnGround) { float f = this.getDimensions(this.getPose()).width() * 2.0F; float g = f / 2.0F; for (int i = 0; i < f * 16.0F; i++) { float h = this.random.nextFloat() * (float) (Math.PI * 2); float j = this.random.nextFloat() * 0.5F + 0.5F; float k = Mth.sin(h) * g * j; float l = Mth.cos(h) * g * j; this.level().addParticle(this.getParticleType(), this.getX() + k, this.getY(), this.getZ() + l, 0.0, 0.0, 0.0); } this.playSound(this.getSquishSound(), this.getSoundVolume(), ((this.random.nextFloat() - this.random.nextFloat()) * 0.2F + 1.0F) / 0.8F); this.targetSquish = -0.5F; } else if (!this.onGround() && this.wasOnGround) { this.targetSquish = 1.0F; } this.wasOnGround = this.onGround(); this.decreaseSquish(); } protected void decreaseSquish() { this.targetSquish *= 0.6F; } /** * Gets the amount of time the slime needs to wait between jumps. */ protected int getJumpDelay() { return this.random.nextInt(20) + 10; } @Override public void refreshDimensions() { double d = this.getX(); double e = this.getY(); double f = this.getZ(); super.refreshDimensions(); this.setPos(d, e, f); } @Override public void onSyncedDataUpdated(EntityDataAccessor dataAccessor) { if (ID_SIZE.equals(dataAccessor)) { this.refreshDimensions(); this.setYRot(this.yHeadRot); this.yBodyRot = this.yHeadRot; if (this.isInWater() && this.random.nextInt(20) == 0) { this.doWaterSplashEffect(); } } super.onSyncedDataUpdated(dataAccessor); } @Override public EntityType getType() { return (EntityType)super.getType(); } @Override public void remove(Entity.RemovalReason reason) { int i = this.getSize(); if (!this.level().isClientSide && i > 1 && this.isDeadOrDying()) { float f = this.getDimensions(this.getPose()).width(); float g = f / 2.0F; int j = i / 2; int k = 2 + this.random.nextInt(3); PlayerTeam playerTeam = this.getTeam(); for (int l = 0; l < k; l++) { float h = (l % 2 - 0.5F) * g; float m = (l / 2 - 0.5F) * g; this.convertTo(this.getType(), new ConversionParams(ConversionType.SPLIT_ON_DEATH, false, false, playerTeam), EntitySpawnReason.TRIGGERED, slime -> { slime.setSize(j, true); slime.moveTo(this.getX() + h, this.getY() + 0.5, this.getZ() + m, this.random.nextFloat() * 360.0F, 0.0F); }); } } super.remove(reason); } @Override public void push(Entity entity) { super.push(entity); if (entity instanceof IronGolem && this.isDealsDamage()) { this.dealDamage((LivingEntity)entity); } } @Override public void playerTouch(Player player) { if (this.isDealsDamage()) { this.dealDamage(player); } } protected void dealDamage(LivingEntity livingEntity) { if (this.level() instanceof ServerLevel serverLevel && this.isAlive() && this.isWithinMeleeAttackRange(livingEntity) && this.hasLineOfSight(livingEntity)) { DamageSource damageSource = this.damageSources().mobAttack(this); if (livingEntity.hurtServer(serverLevel, damageSource, this.getAttackDamage())) { this.playSound(SoundEvents.SLIME_ATTACK, 1.0F, (this.random.nextFloat() - this.random.nextFloat()) * 0.2F + 1.0F); EnchantmentHelper.doPostAttackEffects(serverLevel, livingEntity, damageSource); } } } @Override protected Vec3 getPassengerAttachmentPoint(Entity entity, EntityDimensions dimensions, float partialTick) { return new Vec3(0.0, dimensions.height() - 0.015625 * this.getSize() * partialTick, 0.0); } /** * Indicates weather the slime is able to damage the player (based upon the slime's size) */ protected boolean isDealsDamage() { return !this.isTiny() && this.isEffectiveAi(); } protected float getAttackDamage() { return (float)this.getAttributeValue(Attributes.ATTACK_DAMAGE); } @Override protected SoundEvent getHurtSound(DamageSource damageSource) { return this.isTiny() ? SoundEvents.SLIME_HURT_SMALL : SoundEvents.SLIME_HURT; } @Override protected SoundEvent getDeathSound() { return this.isTiny() ? SoundEvents.SLIME_DEATH_SMALL : SoundEvents.SLIME_DEATH; } protected SoundEvent getSquishSound() { return this.isTiny() ? SoundEvents.SLIME_SQUISH_SMALL : SoundEvents.SLIME_SQUISH; } public static boolean checkSlimeSpawnRules(EntityType entityType, LevelAccessor level, EntitySpawnReason spawnReason, BlockPos pos, RandomSource random) { if (level.getDifficulty() != Difficulty.PEACEFUL) { if (EntitySpawnReason.isSpawner(spawnReason)) { return checkMobSpawnRules(entityType, level, spawnReason, pos, random); } if (level.getBiome(pos).is(BiomeTags.ALLOWS_SURFACE_SLIME_SPAWNS) && pos.getY() > 50 && pos.getY() < 70 && random.nextFloat() < 0.5F && random.nextFloat() < level.getMoonBrightness() && level.getMaxLocalRawBrightness(pos) <= random.nextInt(8)) { return checkMobSpawnRules(entityType, level, spawnReason, pos, random); } if (!(level instanceof WorldGenLevel)) { return false; } ChunkPos chunkPos = new ChunkPos(pos); boolean bl = WorldgenRandom.seedSlimeChunk(chunkPos.x, chunkPos.z, ((WorldGenLevel)level).getSeed(), 987234911L).nextInt(10) == 0; if (random.nextInt(10) == 0 && bl && pos.getY() < 40) { return checkMobSpawnRules(entityType, level, spawnReason, pos, random); } } return false; } @Override protected float getSoundVolume() { return 0.4F * this.getSize(); } @Override public int getMaxHeadXRot() { return 0; } /** * Returns {@code true} if the slime makes a sound when it jumps (based upon the slime's size) */ protected boolean doPlayJumpSound() { return this.getSize() > 0; } @Override public void jumpFromGround() { Vec3 vec3 = this.getDeltaMovement(); this.setDeltaMovement(vec3.x, this.getJumpPower(), vec3.z); this.hasImpulse = true; } @Nullable @Override public SpawnGroupData finalizeSpawn( ServerLevelAccessor level, DifficultyInstance difficulty, EntitySpawnReason spawnReason, @Nullable SpawnGroupData spawnGroupData ) { RandomSource randomSource = level.getRandom(); int i = randomSource.nextInt(3); if (i < 2 && randomSource.nextFloat() < 0.5F * difficulty.getSpecialMultiplier()) { i++; } int j = 1 << i; this.setSize(j, true); return super.finalizeSpawn(level, difficulty, spawnReason, spawnGroupData); } float getSoundPitch() { float f = this.isTiny() ? 1.4F : 0.8F; return ((this.random.nextFloat() - this.random.nextFloat()) * 0.2F + 1.0F) * f; } protected SoundEvent getJumpSound() { return this.isTiny() ? SoundEvents.SLIME_JUMP_SMALL : SoundEvents.SLIME_JUMP; } @Override public EntityDimensions getDefaultDimensions(Pose pose) { return super.getDefaultDimensions(pose).scale(this.getSize()); } static class SlimeAttackGoal extends Goal { private final Slime slime; private int growTiredTimer; public SlimeAttackGoal(Slime slime) { this.slime = slime; this.setFlags(EnumSet.of(Flag.LOOK)); } @Override public boolean canUse() { LivingEntity livingEntity = this.slime.getTarget(); if (livingEntity == null) { return false; } else { return !this.slime.canAttack(livingEntity) ? false : this.slime.getMoveControl() instanceof Slime.SlimeMoveControl; } } @Override public void start() { this.growTiredTimer = reducedTickDelay(300); super.start(); } @Override public boolean canContinueToUse() { LivingEntity livingEntity = this.slime.getTarget(); if (livingEntity == null) { return false; } else { return !this.slime.canAttack(livingEntity) ? false : --this.growTiredTimer > 0; } } @Override public boolean requiresUpdateEveryTick() { return true; } @Override public void tick() { LivingEntity livingEntity = this.slime.getTarget(); if (livingEntity != null) { this.slime.lookAt(livingEntity, 10.0F, 10.0F); } if (this.slime.getMoveControl() instanceof Slime.SlimeMoveControl slimeMoveControl) { slimeMoveControl.setDirection(this.slime.getYRot(), this.slime.isDealsDamage()); } } } static class SlimeFloatGoal extends Goal { private final Slime slime; public SlimeFloatGoal(Slime slime) { this.slime = slime; this.setFlags(EnumSet.of(Flag.JUMP, Flag.MOVE)); slime.getNavigation().setCanFloat(true); } @Override public boolean canUse() { return (this.slime.isInWater() || this.slime.isInLava()) && this.slime.getMoveControl() instanceof Slime.SlimeMoveControl; } @Override public boolean requiresUpdateEveryTick() { return true; } @Override public void tick() { if (this.slime.getRandom().nextFloat() < 0.8F) { this.slime.getJumpControl().jump(); } if (this.slime.getMoveControl() instanceof Slime.SlimeMoveControl slimeMoveControl) { slimeMoveControl.setWantedMovement(1.2); } } } static class SlimeKeepOnJumpingGoal extends Goal { private final Slime slime; public SlimeKeepOnJumpingGoal(Slime slime) { this.slime = slime; this.setFlags(EnumSet.of(Flag.JUMP, Flag.MOVE)); } @Override public boolean canUse() { return !this.slime.isPassenger(); } @Override public void tick() { if (this.slime.getMoveControl() instanceof Slime.SlimeMoveControl slimeMoveControl) { slimeMoveControl.setWantedMovement(1.0); } } } static class SlimeMoveControl extends MoveControl { private float yRot; private int jumpDelay; private final Slime slime; private boolean isAggressive; public SlimeMoveControl(Slime slime) { super(slime); this.slime = slime; this.yRot = 180.0F * slime.getYRot() / (float) Math.PI; } public void setDirection(float yRot, boolean aggressive) { this.yRot = yRot; this.isAggressive = aggressive; } public void setWantedMovement(double speed) { this.speedModifier = speed; this.operation = Operation.MOVE_TO; } @Override public void tick() { this.mob.setYRot(this.rotlerp(this.mob.getYRot(), this.yRot, 90.0F)); this.mob.yHeadRot = this.mob.getYRot(); this.mob.yBodyRot = this.mob.getYRot(); if (this.operation != Operation.MOVE_TO) { this.mob.setZza(0.0F); } else { this.operation = Operation.WAIT; if (this.mob.onGround()) { this.mob.setSpeed((float)(this.speedModifier * this.mob.getAttributeValue(Attributes.MOVEMENT_SPEED))); if (this.jumpDelay-- <= 0) { this.jumpDelay = this.slime.getJumpDelay(); if (this.isAggressive) { this.jumpDelay /= 3; } this.slime.getJumpControl().jump(); if (this.slime.doPlayJumpSound()) { this.slime.playSound(this.slime.getJumpSound(), this.slime.getSoundVolume(), this.slime.getSoundPitch()); } } else { this.slime.xxa = 0.0F; this.slime.zza = 0.0F; this.mob.setSpeed(0.0F); } } else { this.mob.setSpeed((float)(this.speedModifier * this.mob.getAttributeValue(Attributes.MOVEMENT_SPEED))); } } } } static class SlimeRandomDirectionGoal extends Goal { private final Slime slime; private float chosenDegrees; private int nextRandomizeTime; public SlimeRandomDirectionGoal(Slime slime) { this.slime = slime; this.setFlags(EnumSet.of(Flag.LOOK)); } @Override public boolean canUse() { return this.slime.getTarget() == null && (this.slime.onGround() || this.slime.isInWater() || this.slime.isInLava() || this.slime.hasEffect(MobEffects.LEVITATION)) && this.slime.getMoveControl() instanceof Slime.SlimeMoveControl; } @Override public void tick() { if (--this.nextRandomizeTime <= 0) { this.nextRandomizeTime = this.adjustedTickDelay(40 + this.slime.getRandom().nextInt(60)); this.chosenDegrees = this.slime.getRandom().nextInt(360); } if (this.slime.getMoveControl() instanceof Slime.SlimeMoveControl slimeMoveControl) { slimeMoveControl.setDirection(this.chosenDegrees, false); } } } }