package net.minecraft.world.entity.monster; import java.util.EnumSet; import java.util.Optional; import java.util.UUID; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.component.DataComponents; 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.resources.RegistryOps; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerLevel; import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundEvents; import net.minecraft.tags.BlockTags; import net.minecraft.tags.DamageTypeTags; import net.minecraft.tags.FluidTags; import net.minecraft.util.Mth; import net.minecraft.util.RandomSource; import net.minecraft.util.TimeUtil; import net.minecraft.util.valueproviders.UniformInt; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.NeutralMob; import net.minecraft.world.entity.ai.attributes.AttributeInstance; import net.minecraft.world.entity.ai.attributes.AttributeModifier; import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.entity.ai.attributes.AttributeSupplier.Builder; import net.minecraft.world.entity.ai.goal.FloatGoal; import net.minecraft.world.entity.ai.goal.Goal; import net.minecraft.world.entity.ai.goal.LookAtPlayerGoal; import net.minecraft.world.entity.ai.goal.MeleeAttackGoal; import net.minecraft.world.entity.ai.goal.RandomLookAroundGoal; import net.minecraft.world.entity.ai.goal.WaterAvoidingRandomStrollGoal; import net.minecraft.world.entity.ai.goal.Goal.Flag; import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; import net.minecraft.world.entity.ai.goal.target.ResetUniversalAngerTargetGoal; import net.minecraft.world.entity.ai.targeting.TargetingConditions; import net.minecraft.world.entity.ai.targeting.TargetingConditions.Selector; import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.projectile.AbstractThrownPotion; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.item.alchemy.PotionContents; import net.minecraft.world.item.alchemy.Potions; import net.minecraft.world.item.enchantment.EnchantmentHelper; import net.minecraft.world.item.enchantment.providers.VanillaEnchantmentProviders; import net.minecraft.world.level.ClipContext; import net.minecraft.world.level.GameRules; import net.minecraft.world.level.Level; import net.minecraft.world.level.ClipContext.Fluid; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.gameevent.GameEvent; import net.minecraft.world.level.gameevent.GameEvent.Context; import net.minecraft.world.level.pathfinder.PathType; import net.minecraft.world.level.storage.loot.parameters.LootContextParams; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.Vec3; import org.jetbrains.annotations.Nullable; public class EnderMan extends Monster implements NeutralMob { private static final ResourceLocation SPEED_MODIFIER_ATTACKING_ID = ResourceLocation.withDefaultNamespace("attacking"); private static final AttributeModifier SPEED_MODIFIER_ATTACKING = new AttributeModifier( SPEED_MODIFIER_ATTACKING_ID, 0.15F, AttributeModifier.Operation.ADD_VALUE ); private static final int DELAY_BETWEEN_CREEPY_STARE_SOUND = 400; private static final int MIN_DEAGGRESSION_TIME = 600; private static final EntityDataAccessor> DATA_CARRY_STATE = SynchedEntityData.defineId( EnderMan.class, EntityDataSerializers.OPTIONAL_BLOCK_STATE ); private static final EntityDataAccessor DATA_CREEPY = SynchedEntityData.defineId(EnderMan.class, EntityDataSerializers.BOOLEAN); private static final EntityDataAccessor DATA_STARED_AT = SynchedEntityData.defineId(EnderMan.class, EntityDataSerializers.BOOLEAN); private int lastStareSound = Integer.MIN_VALUE; private int targetChangeTime; private static final UniformInt PERSISTENT_ANGER_TIME = TimeUtil.rangeOfSeconds(20, 39); private int remainingPersistentAngerTime; @Nullable private UUID persistentAngerTarget; public EnderMan(EntityType entityType, Level level) { super(entityType, level); this.setPathfindingMalus(PathType.WATER, -1.0F); } @Override protected void registerGoals() { this.goalSelector.addGoal(0, new FloatGoal(this)); this.goalSelector.addGoal(1, new EnderMan.EndermanFreezeWhenLookedAt(this)); this.goalSelector.addGoal(2, new MeleeAttackGoal(this, 1.0, false)); this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0, 0.0F)); this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F)); this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); this.goalSelector.addGoal(10, new EnderMan.EndermanLeaveBlockGoal(this)); this.goalSelector.addGoal(11, new EnderMan.EndermanTakeBlockGoal(this)); this.targetSelector.addGoal(1, new EnderMan.EndermanLookForPlayerGoal(this, this::isAngryAt)); this.targetSelector.addGoal(2, new HurtByTargetGoal(this)); this.targetSelector.addGoal(3, new NearestAttackableTargetGoal(this, Endermite.class, true, false)); this.targetSelector.addGoal(4, new ResetUniversalAngerTargetGoal<>(this, false)); } public static Builder createAttributes() { return Monster.createMonsterAttributes() .add(Attributes.MAX_HEALTH, 40.0) .add(Attributes.MOVEMENT_SPEED, 0.3F) .add(Attributes.ATTACK_DAMAGE, 7.0) .add(Attributes.FOLLOW_RANGE, 64.0) .add(Attributes.STEP_HEIGHT, 1.0); } @Override public void setTarget(@Nullable LivingEntity livingEntity) { super.setTarget(livingEntity); AttributeInstance attributeInstance = this.getAttribute(Attributes.MOVEMENT_SPEED); if (livingEntity == null) { this.targetChangeTime = 0; this.entityData.set(DATA_CREEPY, false); this.entityData.set(DATA_STARED_AT, false); attributeInstance.removeModifier(SPEED_MODIFIER_ATTACKING_ID); } else { this.targetChangeTime = this.tickCount; this.entityData.set(DATA_CREEPY, true); if (!attributeInstance.hasModifier(SPEED_MODIFIER_ATTACKING_ID)) { attributeInstance.addTransientModifier(SPEED_MODIFIER_ATTACKING); } } } @Override protected void defineSynchedData(net.minecraft.network.syncher.SynchedEntityData.Builder builder) { super.defineSynchedData(builder); builder.define(DATA_CARRY_STATE, Optional.empty()); builder.define(DATA_CREEPY, false); builder.define(DATA_STARED_AT, false); } @Override public void startPersistentAngerTimer() { this.setRemainingPersistentAngerTime(PERSISTENT_ANGER_TIME.sample(this.random)); } @Override public void setRemainingPersistentAngerTime(int remainingPersistentAngerTime) { this.remainingPersistentAngerTime = remainingPersistentAngerTime; } @Override public int getRemainingPersistentAngerTime() { return this.remainingPersistentAngerTime; } @Override public void setPersistentAngerTarget(@Nullable UUID persistentAngerTarget) { this.persistentAngerTarget = persistentAngerTarget; } @Nullable @Override public UUID getPersistentAngerTarget() { return this.persistentAngerTarget; } public void playStareSound() { if (this.tickCount >= this.lastStareSound + 400) { this.lastStareSound = this.tickCount; if (!this.isSilent()) { this.level().playLocalSound(this.getX(), this.getEyeY(), this.getZ(), SoundEvents.ENDERMAN_STARE, this.getSoundSource(), 2.5F, 1.0F, false); } } } @Override public void onSyncedDataUpdated(EntityDataAccessor dataAccessor) { if (DATA_CREEPY.equals(dataAccessor) && this.hasBeenStaredAt() && this.level().isClientSide) { this.playStareSound(); } super.onSyncedDataUpdated(dataAccessor); } @Override public void addAdditionalSaveData(CompoundTag tag) { super.addAdditionalSaveData(tag); BlockState blockState = this.getCarriedBlock(); if (blockState != null) { RegistryOps registryOps = this.registryAccess().createSerializationContext(NbtOps.INSTANCE); tag.store("carriedBlockState", BlockState.CODEC, registryOps, blockState); } this.addPersistentAngerSaveData(tag); } @Override public void readAdditionalSaveData(CompoundTag tag) { super.readAdditionalSaveData(tag); RegistryOps registryOps = this.registryAccess().createSerializationContext(NbtOps.INSTANCE); this.setCarriedBlock((BlockState)tag.read("carriedBlockState", BlockState.CODEC, registryOps).filter(blockState -> !blockState.isAir()).orElse(null)); this.readPersistentAngerSaveData(this.level(), tag); } boolean isBeingStaredBy(Player player) { return !LivingEntity.PLAYER_NOT_WEARING_DISGUISE_ITEM.test(player) ? false : this.isLookingAtMe(player, 0.025, true, false, new double[]{this.getEyeY()}); } @Override public void aiStep() { if (this.level().isClientSide) { for (int i = 0; i < 2; i++) { this.level() .addParticle( ParticleTypes.PORTAL, this.getRandomX(0.5), this.getRandomY() - 0.25, this.getRandomZ(0.5), (this.random.nextDouble() - 0.5) * 2.0, -this.random.nextDouble(), (this.random.nextDouble() - 0.5) * 2.0 ); } } this.jumping = false; if (!this.level().isClientSide) { this.updatePersistentAnger((ServerLevel)this.level(), true); } super.aiStep(); } @Override public boolean isSensitiveToWater() { return true; } @Override protected void customServerAiStep(ServerLevel level) { if (level.isBrightOutside() && this.tickCount >= this.targetChangeTime + 600) { float f = this.getLightLevelDependentMagicValue(); if (f > 0.5F && level.canSeeSky(this.blockPosition()) && this.random.nextFloat() * 30.0F < (f - 0.4F) * 2.0F) { this.setTarget(null); this.teleport(); } } super.customServerAiStep(level); } /** * Teleport the enderman to a random nearby position */ protected boolean teleport() { if (!this.level().isClientSide() && this.isAlive()) { double d = this.getX() + (this.random.nextDouble() - 0.5) * 64.0; double e = this.getY() + (this.random.nextInt(64) - 32); double f = this.getZ() + (this.random.nextDouble() - 0.5) * 64.0; return this.teleport(d, e, f); } else { return false; } } /** * Teleport the enderman to another entity */ boolean teleportTowards(Entity target) { Vec3 vec3 = new Vec3(this.getX() - target.getX(), this.getY(0.5) - target.getEyeY(), this.getZ() - target.getZ()); vec3 = vec3.normalize(); double d = 16.0; double e = this.getX() + (this.random.nextDouble() - 0.5) * 8.0 - vec3.x * 16.0; double f = this.getY() + (this.random.nextInt(16) - 8) - vec3.y * 16.0; double g = this.getZ() + (this.random.nextDouble() - 0.5) * 8.0 - vec3.z * 16.0; return this.teleport(e, f, g); } /** * Teleport the enderman */ private boolean teleport(double x, double y, double z) { BlockPos.MutableBlockPos mutableBlockPos = new BlockPos.MutableBlockPos(x, y, z); while (mutableBlockPos.getY() > this.level().getMinY() && !this.level().getBlockState(mutableBlockPos).blocksMotion()) { mutableBlockPos.move(Direction.DOWN); } BlockState blockState = this.level().getBlockState(mutableBlockPos); boolean bl = blockState.blocksMotion(); boolean bl2 = blockState.getFluidState().is(FluidTags.WATER); if (bl && !bl2) { Vec3 vec3 = this.position(); boolean bl3 = this.randomTeleport(x, y, z, true); if (bl3) { this.level().gameEvent(GameEvent.TELEPORT, vec3, Context.of(this)); if (!this.isSilent()) { this.level().playSound(null, this.xo, this.yo, this.zo, SoundEvents.ENDERMAN_TELEPORT, this.getSoundSource(), 1.0F, 1.0F); this.playSound(SoundEvents.ENDERMAN_TELEPORT, 1.0F, 1.0F); } } return bl3; } else { return false; } } @Override protected SoundEvent getAmbientSound() { return this.isCreepy() ? SoundEvents.ENDERMAN_SCREAM : SoundEvents.ENDERMAN_AMBIENT; } @Override protected SoundEvent getHurtSound(DamageSource damageSource) { return SoundEvents.ENDERMAN_HURT; } @Override protected SoundEvent getDeathSound() { return SoundEvents.ENDERMAN_DEATH; } @Override protected void dropCustomDeathLoot(ServerLevel level, DamageSource damageSource, boolean recentlyHit) { super.dropCustomDeathLoot(level, damageSource, recentlyHit); BlockState blockState = this.getCarriedBlock(); if (blockState != null) { ItemStack itemStack = new ItemStack(Items.DIAMOND_AXE); EnchantmentHelper.enchantItemFromProvider( itemStack, level.registryAccess(), VanillaEnchantmentProviders.ENDERMAN_LOOT_DROP, level.getCurrentDifficultyAt(this.blockPosition()), this.getRandom() ); net.minecraft.world.level.storage.loot.LootParams.Builder builder = new net.minecraft.world.level.storage.loot.LootParams.Builder((ServerLevel)this.level()) .withParameter(LootContextParams.ORIGIN, this.position()) .withParameter(LootContextParams.TOOL, itemStack) .withOptionalParameter(LootContextParams.THIS_ENTITY, this); for (ItemStack itemStack2 : blockState.getDrops(builder)) { this.spawnAtLocation(level, itemStack2); } } } public void setCarriedBlock(@Nullable BlockState state) { this.entityData.set(DATA_CARRY_STATE, Optional.ofNullable(state)); } @Nullable public BlockState getCarriedBlock() { return (BlockState)this.entityData.get(DATA_CARRY_STATE).orElse(null); } @Override public boolean hurtServer(ServerLevel level, DamageSource damageSource, float amount) { if (this.isInvulnerableTo(level, damageSource)) { return false; } else { AbstractThrownPotion abstractThrownPotion2 = damageSource.getDirectEntity() instanceof AbstractThrownPotion abstractThrownPotion ? abstractThrownPotion : null; if (!damageSource.is(DamageTypeTags.IS_PROJECTILE) && abstractThrownPotion2 == null) { boolean bl = super.hurtServer(level, damageSource, amount); if (!(damageSource.getEntity() instanceof LivingEntity) && this.random.nextInt(10) != 0) { this.teleport(); } return bl; } else { boolean bl = abstractThrownPotion2 != null && this.hurtWithCleanWater(level, damageSource, abstractThrownPotion2, amount); for (int i = 0; i < 64; i++) { if (this.teleport()) { return true; } } return bl; } } } private boolean hurtWithCleanWater(ServerLevel level, DamageSource damageSource, AbstractThrownPotion potion, float damageAmount) { ItemStack itemStack = potion.getItem(); PotionContents potionContents = itemStack.getOrDefault(DataComponents.POTION_CONTENTS, PotionContents.EMPTY); return potionContents.is(Potions.WATER) ? super.hurtServer(level, damageSource, damageAmount) : false; } public boolean isCreepy() { return this.entityData.get(DATA_CREEPY); } public boolean hasBeenStaredAt() { return this.entityData.get(DATA_STARED_AT); } public void setBeingStaredAt() { this.entityData.set(DATA_STARED_AT, true); } @Override public boolean requiresCustomPersistence() { return super.requiresCustomPersistence() || this.getCarriedBlock() != null; } static class EndermanFreezeWhenLookedAt extends Goal { private final EnderMan enderman; @Nullable private LivingEntity target; public EndermanFreezeWhenLookedAt(EnderMan enderman) { this.enderman = enderman; this.setFlags(EnumSet.of(Flag.JUMP, Flag.MOVE)); } @Override public boolean canUse() { this.target = this.enderman.getTarget(); if (this.target instanceof Player player) { double d = this.target.distanceToSqr(this.enderman); return d > 256.0 ? false : this.enderman.isBeingStaredBy(player); } else { return false; } } @Override public void start() { this.enderman.getNavigation().stop(); } @Override public void tick() { this.enderman.getLookControl().setLookAt(this.target.getX(), this.target.getEyeY(), this.target.getZ()); } } static class EndermanLeaveBlockGoal extends Goal { private final EnderMan enderman; public EndermanLeaveBlockGoal(EnderMan enderman) { this.enderman = enderman; } @Override public boolean canUse() { if (this.enderman.getCarriedBlock() == null) { return false; } else { return !getServerLevel(this.enderman).getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) ? false : this.enderman.getRandom().nextInt(reducedTickDelay(2000)) == 0; } } @Override public void tick() { RandomSource randomSource = this.enderman.getRandom(); Level level = this.enderman.level(); int i = Mth.floor(this.enderman.getX() - 1.0 + randomSource.nextDouble() * 2.0); int j = Mth.floor(this.enderman.getY() + randomSource.nextDouble() * 2.0); int k = Mth.floor(this.enderman.getZ() - 1.0 + randomSource.nextDouble() * 2.0); BlockPos blockPos = new BlockPos(i, j, k); BlockState blockState = level.getBlockState(blockPos); BlockPos blockPos2 = blockPos.below(); BlockState blockState2 = level.getBlockState(blockPos2); BlockState blockState3 = this.enderman.getCarriedBlock(); if (blockState3 != null) { blockState3 = Block.updateFromNeighbourShapes(blockState3, this.enderman.level(), blockPos); if (this.canPlaceBlock(level, blockPos, blockState3, blockState, blockState2, blockPos2)) { level.setBlock(blockPos, blockState3, 3); level.gameEvent(GameEvent.BLOCK_PLACE, blockPos, Context.of(this.enderman, blockState3)); this.enderman.setCarriedBlock(null); } } } private boolean canPlaceBlock( Level level, BlockPos destinationPos, BlockState carriedState, BlockState destinationState, BlockState belowDestinationState, BlockPos belowDestinationPos ) { return destinationState.isAir() && !belowDestinationState.isAir() && !belowDestinationState.is(Blocks.BEDROCK) && belowDestinationState.isCollisionShapeFullBlock(level, belowDestinationPos) && carriedState.canSurvive(level, destinationPos) && level.getEntities(this.enderman, AABB.unitCubeFromLowerCorner(Vec3.atLowerCornerOf(destinationPos))).isEmpty(); } } static class EndermanLookForPlayerGoal extends NearestAttackableTargetGoal { private final EnderMan enderman; /** * The player */ @Nullable private Player pendingTarget; private int aggroTime; private int teleportTime; private final TargetingConditions startAggroTargetConditions; private final TargetingConditions continueAggroTargetConditions = TargetingConditions.forCombat().ignoreLineOfSight(); private final Selector isAngerInducing; public EndermanLookForPlayerGoal(EnderMan enderman, @Nullable Selector selector) { super(enderman, Player.class, 10, false, false, selector); this.enderman = enderman; this.isAngerInducing = (livingEntity, serverLevel) -> (enderman.isBeingStaredBy((Player)livingEntity) || enderman.isAngryAt(livingEntity, serverLevel)) && !enderman.hasIndirectPassenger(livingEntity); this.startAggroTargetConditions = TargetingConditions.forCombat().range(this.getFollowDistance()).selector(this.isAngerInducing); } @Override public boolean canUse() { this.pendingTarget = getServerLevel(this.enderman).getNearestPlayer(this.startAggroTargetConditions.range(this.getFollowDistance()), this.enderman); return this.pendingTarget != null; } @Override public void start() { this.aggroTime = this.adjustedTickDelay(5); this.teleportTime = 0; this.enderman.setBeingStaredAt(); } @Override public void stop() { this.pendingTarget = null; super.stop(); } @Override public boolean canContinueToUse() { if (this.pendingTarget != null) { if (!this.isAngerInducing.test(this.pendingTarget, getServerLevel(this.enderman))) { return false; } else { this.enderman.lookAt(this.pendingTarget, 10.0F, 10.0F); return true; } } else { if (this.target != null) { if (this.enderman.hasIndirectPassenger(this.target)) { return false; } if (this.continueAggroTargetConditions.test(getServerLevel(this.enderman), this.enderman, this.target)) { return true; } } return super.canContinueToUse(); } } @Override public void tick() { if (this.enderman.getTarget() == null) { super.setTarget(null); } if (this.pendingTarget != null) { if (--this.aggroTime <= 0) { this.target = this.pendingTarget; this.pendingTarget = null; super.start(); } } else { if (this.target != null && !this.enderman.isPassenger()) { if (this.enderman.isBeingStaredBy((Player)this.target)) { if (this.target.distanceToSqr(this.enderman) < 16.0) { this.enderman.teleport(); } this.teleportTime = 0; } else if (this.target.distanceToSqr(this.enderman) > 256.0 && this.teleportTime++ >= this.adjustedTickDelay(30) && this.enderman.teleportTowards(this.target)) { this.teleportTime = 0; } } super.tick(); } } } static class EndermanTakeBlockGoal extends Goal { private final EnderMan enderman; public EndermanTakeBlockGoal(EnderMan enderman) { this.enderman = enderman; } @Override public boolean canUse() { if (this.enderman.getCarriedBlock() != null) { return false; } else { return !getServerLevel(this.enderman).getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) ? false : this.enderman.getRandom().nextInt(reducedTickDelay(20)) == 0; } } @Override public void tick() { RandomSource randomSource = this.enderman.getRandom(); Level level = this.enderman.level(); int i = Mth.floor(this.enderman.getX() - 2.0 + randomSource.nextDouble() * 4.0); int j = Mth.floor(this.enderman.getY() + randomSource.nextDouble() * 3.0); int k = Mth.floor(this.enderman.getZ() - 2.0 + randomSource.nextDouble() * 4.0); BlockPos blockPos = new BlockPos(i, j, k); BlockState blockState = level.getBlockState(blockPos); Vec3 vec3 = new Vec3(this.enderman.getBlockX() + 0.5, j + 0.5, this.enderman.getBlockZ() + 0.5); Vec3 vec32 = new Vec3(i + 0.5, j + 0.5, k + 0.5); BlockHitResult blockHitResult = level.clip(new ClipContext(vec3, vec32, net.minecraft.world.level.ClipContext.Block.OUTLINE, Fluid.NONE, this.enderman)); boolean bl = blockHitResult.getBlockPos().equals(blockPos); if (blockState.is(BlockTags.ENDERMAN_HOLDABLE) && bl) { level.removeBlock(blockPos, false); level.gameEvent(GameEvent.BLOCK_DESTROY, blockPos, Context.of(this.enderman, blockState)); this.enderman.setCarriedBlock(blockState.getBlock().defaultBlockState()); } } } }