package net.minecraft.world.entity.animal; import net.minecraft.advancements.CriteriaTriggers; import net.minecraft.core.BlockPos; 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.server.level.ServerPlayer; import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundSource; import net.minecraft.stats.Stats; import net.minecraft.tags.FluidTags; import net.minecraft.tags.ItemTags; import net.minecraft.util.Mth; import net.minecraft.util.RandomSource; import net.minecraft.world.DifficultyInstance; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.AgeableMob; import net.minecraft.world.entity.EntityAttachment; import net.minecraft.world.entity.EntityAttachments; import net.minecraft.world.entity.EntityDimensions; import net.minecraft.world.entity.EntitySpawnReason; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.ExperienceOrb; import net.minecraft.world.entity.LightningBolt; import net.minecraft.world.entity.MoverType; 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.goal.BreedGoal; import net.minecraft.world.entity.ai.goal.Goal; import net.minecraft.world.entity.ai.goal.LookAtPlayerGoal; import net.minecraft.world.entity.ai.goal.MoveToBlockGoal; import net.minecraft.world.entity.ai.goal.PanicGoal; import net.minecraft.world.entity.ai.goal.RandomStrollGoal; import net.minecraft.world.entity.ai.goal.TemptGoal; import net.minecraft.world.entity.ai.navigation.AmphibiousPathNavigation; import net.minecraft.world.entity.ai.navigation.PathNavigation; import net.minecraft.world.entity.ai.targeting.TargetingConditions.Selector; import net.minecraft.world.entity.ai.util.DefaultRandomPos; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.level.GameRules; import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelAccessor; import net.minecraft.world.level.LevelReader; import net.minecraft.world.level.ServerLevelAccessor; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.TurtleEggBlock; 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.phys.Vec3; import org.jetbrains.annotations.Nullable; public class Turtle extends Animal { private static final EntityDataAccessor HAS_EGG = SynchedEntityData.defineId(Turtle.class, EntityDataSerializers.BOOLEAN); private static final EntityDataAccessor LAYING_EGG = SynchedEntityData.defineId(Turtle.class, EntityDataSerializers.BOOLEAN); private static final float BABY_SCALE = 0.3F; private static final EntityDimensions BABY_DIMENSIONS = EntityType.TURTLE .getDimensions() .withAttachments(EntityAttachments.builder().attach(EntityAttachment.PASSENGER, 0.0F, EntityType.TURTLE.getHeight(), -0.25F)) .scale(0.3F); private static final boolean DEFAULT_HAS_EGG = false; int layEggCounter; public static final Selector BABY_ON_LAND_SELECTOR = (livingEntity, serverLevel) -> livingEntity.isBaby() && !livingEntity.isInWater(); BlockPos homePos = BlockPos.ZERO; @Nullable BlockPos travelPos; boolean goingHome; public Turtle(EntityType entityType, Level level) { super(entityType, level); this.setPathfindingMalus(PathType.WATER, 0.0F); this.setPathfindingMalus(PathType.DOOR_IRON_CLOSED, -1.0F); this.setPathfindingMalus(PathType.DOOR_WOOD_CLOSED, -1.0F); this.setPathfindingMalus(PathType.DOOR_OPEN, -1.0F); this.moveControl = new Turtle.TurtleMoveControl(this); } public void setHomePos(BlockPos homePos) { this.homePos = homePos; } public boolean hasEgg() { return this.entityData.get(HAS_EGG); } void setHasEgg(boolean hasEgg) { this.entityData.set(HAS_EGG, hasEgg); } public boolean isLayingEgg() { return this.entityData.get(LAYING_EGG); } void setLayingEgg(boolean isLayingEgg) { this.layEggCounter = isLayingEgg ? 1 : 0; this.entityData.set(LAYING_EGG, isLayingEgg); } @Override protected void defineSynchedData(Builder builder) { super.defineSynchedData(builder); builder.define(HAS_EGG, false); builder.define(LAYING_EGG, false); } @Override public void addAdditionalSaveData(CompoundTag tag) { super.addAdditionalSaveData(tag); tag.store("home_pos", BlockPos.CODEC, this.homePos); tag.putBoolean("has_egg", this.hasEgg()); } @Override public void readAdditionalSaveData(CompoundTag tag) { this.setHomePos((BlockPos)tag.read("home_pos", BlockPos.CODEC).orElse(this.blockPosition())); super.readAdditionalSaveData(tag); this.setHasEgg(tag.getBooleanOr("has_egg", false)); } @Nullable @Override public SpawnGroupData finalizeSpawn( ServerLevelAccessor level, DifficultyInstance difficulty, EntitySpawnReason spawnReason, @Nullable SpawnGroupData spawnGroupData ) { this.setHomePos(this.blockPosition()); return super.finalizeSpawn(level, difficulty, spawnReason, spawnGroupData); } public static boolean checkTurtleSpawnRules( EntityType entityType, LevelAccessor level, EntitySpawnReason spawnReason, BlockPos pos, RandomSource random ) { return pos.getY() < level.getSeaLevel() + 4 && TurtleEggBlock.onSand(level, pos) && isBrightEnoughToSpawn(level, pos); } @Override protected void registerGoals() { this.goalSelector.addGoal(0, new Turtle.TurtlePanicGoal(this, 1.2)); this.goalSelector.addGoal(1, new Turtle.TurtleBreedGoal(this, 1.0)); this.goalSelector.addGoal(1, new Turtle.TurtleLayEggGoal(this, 1.0)); this.goalSelector.addGoal(2, new TemptGoal(this, 1.1, itemStack -> itemStack.is(ItemTags.TURTLE_FOOD), false)); this.goalSelector.addGoal(3, new Turtle.TurtleGoToWaterGoal(this, 1.0)); this.goalSelector.addGoal(4, new Turtle.TurtleGoHomeGoal(this, 1.0)); this.goalSelector.addGoal(7, new Turtle.TurtleTravelGoal(this, 1.0)); this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F)); this.goalSelector.addGoal(9, new Turtle.TurtleRandomStrollGoal(this, 1.0, 100)); } public static net.minecraft.world.entity.ai.attributes.AttributeSupplier.Builder createAttributes() { return Animal.createAnimalAttributes().add(Attributes.MAX_HEALTH, 30.0).add(Attributes.MOVEMENT_SPEED, 0.25).add(Attributes.STEP_HEIGHT, 1.0); } @Override public boolean isPushedByFluid() { return false; } @Override public int getAmbientSoundInterval() { return 200; } @Nullable @Override protected SoundEvent getAmbientSound() { return !this.isInWater() && this.onGround() && !this.isBaby() ? SoundEvents.TURTLE_AMBIENT_LAND : super.getAmbientSound(); } @Override protected void playSwimSound(float volume) { super.playSwimSound(volume * 1.5F); } @Override protected SoundEvent getSwimSound() { return SoundEvents.TURTLE_SWIM; } @Nullable @Override protected SoundEvent getHurtSound(DamageSource damageSource) { return this.isBaby() ? SoundEvents.TURTLE_HURT_BABY : SoundEvents.TURTLE_HURT; } @Nullable @Override protected SoundEvent getDeathSound() { return this.isBaby() ? SoundEvents.TURTLE_DEATH_BABY : SoundEvents.TURTLE_DEATH; } @Override protected void playStepSound(BlockPos pos, BlockState state) { SoundEvent soundEvent = this.isBaby() ? SoundEvents.TURTLE_SHAMBLE_BABY : SoundEvents.TURTLE_SHAMBLE; this.playSound(soundEvent, 0.15F, 1.0F); } @Override public boolean canFallInLove() { return super.canFallInLove() && !this.hasEgg(); } @Override protected float nextStep() { return this.moveDist + 0.15F; } @Override public float getAgeScale() { return this.isBaby() ? 0.3F : 1.0F; } @Override protected PathNavigation createNavigation(Level level) { return new Turtle.TurtlePathNavigation(this, level); } @Nullable @Override public AgeableMob getBreedOffspring(ServerLevel level, AgeableMob otherParent) { return EntityType.TURTLE.create(level, EntitySpawnReason.BREEDING); } @Override public boolean isFood(ItemStack stack) { return stack.is(ItemTags.TURTLE_FOOD); } @Override public float getWalkTargetValue(BlockPos pos, LevelReader level) { if (!this.goingHome && level.getFluidState(pos).is(FluidTags.WATER)) { return 10.0F; } else { return TurtleEggBlock.onSand(level, pos) ? 10.0F : level.getPathfindingCostFromLightLevels(pos); } } @Override public void aiStep() { super.aiStep(); if (this.isAlive() && this.isLayingEgg() && this.layEggCounter >= 1 && this.layEggCounter % 5 == 0) { BlockPos blockPos = this.blockPosition(); if (TurtleEggBlock.onSand(this.level(), blockPos)) { this.level().levelEvent(2001, blockPos, Block.getId(this.level().getBlockState(blockPos.below()))); this.gameEvent(GameEvent.ENTITY_ACTION); } } } @Override protected void ageBoundaryReached() { super.ageBoundaryReached(); if (!this.isBaby() && this.level() instanceof ServerLevel serverLevel && serverLevel.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) { this.spawnAtLocation(serverLevel, Items.TURTLE_SCUTE, 1); } } @Override public void travel(Vec3 travelVector) { if (this.isInWater()) { this.moveRelative(0.1F, travelVector); this.move(MoverType.SELF, this.getDeltaMovement()); this.setDeltaMovement(this.getDeltaMovement().scale(0.9)); if (this.getTarget() == null && (!this.goingHome || !this.homePos.closerToCenterThan(this.position(), 20.0))) { this.setDeltaMovement(this.getDeltaMovement().add(0.0, -0.005, 0.0)); } } else { super.travel(travelVector); } } @Override public boolean canBeLeashed() { return false; } @Override public void thunderHit(ServerLevel level, LightningBolt lightning) { this.hurtServer(level, this.damageSources().lightningBolt(), Float.MAX_VALUE); } @Override public EntityDimensions getDefaultDimensions(Pose pose) { return this.isBaby() ? BABY_DIMENSIONS : super.getDefaultDimensions(pose); } static class TurtleBreedGoal extends BreedGoal { private final Turtle turtle; TurtleBreedGoal(Turtle turtle, double speedModifier) { super(turtle, speedModifier); this.turtle = turtle; } @Override public boolean canUse() { return super.canUse() && !this.turtle.hasEgg(); } @Override protected void breed() { ServerPlayer serverPlayer = this.animal.getLoveCause(); if (serverPlayer == null && this.partner.getLoveCause() != null) { serverPlayer = this.partner.getLoveCause(); } if (serverPlayer != null) { serverPlayer.awardStat(Stats.ANIMALS_BRED); CriteriaTriggers.BRED_ANIMALS.trigger(serverPlayer, this.animal, this.partner, null); } this.turtle.setHasEgg(true); this.animal.setAge(6000); this.partner.setAge(6000); this.animal.resetLove(); this.partner.resetLove(); RandomSource randomSource = this.animal.getRandom(); if (getServerLevel(this.level).getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) { this.level.addFreshEntity(new ExperienceOrb(this.level, this.animal.getX(), this.animal.getY(), this.animal.getZ(), randomSource.nextInt(7) + 1)); } } } static class TurtleGoHomeGoal extends Goal { private final Turtle turtle; private final double speedModifier; private boolean stuck; private int closeToHomeTryTicks; private static final int GIVE_UP_TICKS = 600; TurtleGoHomeGoal(Turtle turtle, double speedModifier) { this.turtle = turtle; this.speedModifier = speedModifier; } @Override public boolean canUse() { if (this.turtle.isBaby()) { return false; } else if (this.turtle.hasEgg()) { return true; } else { return this.turtle.getRandom().nextInt(reducedTickDelay(700)) != 0 ? false : !this.turtle.homePos.closerToCenterThan(this.turtle.position(), 64.0); } } @Override public void start() { this.turtle.goingHome = true; this.stuck = false; this.closeToHomeTryTicks = 0; } @Override public void stop() { this.turtle.goingHome = false; } @Override public boolean canContinueToUse() { return !this.turtle.homePos.closerToCenterThan(this.turtle.position(), 7.0) && !this.stuck && this.closeToHomeTryTicks <= this.adjustedTickDelay(600); } @Override public void tick() { BlockPos blockPos = this.turtle.homePos; boolean bl = blockPos.closerToCenterThan(this.turtle.position(), 16.0); if (bl) { this.closeToHomeTryTicks++; } if (this.turtle.getNavigation().isDone()) { Vec3 vec3 = Vec3.atBottomCenterOf(blockPos); Vec3 vec32 = DefaultRandomPos.getPosTowards(this.turtle, 16, 3, vec3, (float) (Math.PI / 10)); if (vec32 == null) { vec32 = DefaultRandomPos.getPosTowards(this.turtle, 8, 7, vec3, (float) (Math.PI / 2)); } if (vec32 != null && !bl && !this.turtle.level().getBlockState(BlockPos.containing(vec32)).is(Blocks.WATER)) { vec32 = DefaultRandomPos.getPosTowards(this.turtle, 16, 5, vec3, (float) (Math.PI / 2)); } if (vec32 == null) { this.stuck = true; return; } this.turtle.getNavigation().moveTo(vec32.x, vec32.y, vec32.z, this.speedModifier); } } } static class TurtleGoToWaterGoal extends MoveToBlockGoal { private static final int GIVE_UP_TICKS = 1200; private final Turtle turtle; TurtleGoToWaterGoal(Turtle turtle, double speedModifier) { super(turtle, turtle.isBaby() ? 2.0 : speedModifier, 24); this.turtle = turtle; this.verticalSearchStart = -1; } @Override public boolean canContinueToUse() { return !this.turtle.isInWater() && this.tryTicks <= 1200 && this.isValidTarget(this.turtle.level(), this.blockPos); } @Override public boolean canUse() { if (this.turtle.isBaby() && !this.turtle.isInWater()) { return super.canUse(); } else { return !this.turtle.goingHome && !this.turtle.isInWater() && !this.turtle.hasEgg() ? super.canUse() : false; } } @Override public boolean shouldRecalculatePath() { return this.tryTicks % 160 == 0; } @Override protected boolean isValidTarget(LevelReader level, BlockPos pos) { return level.getBlockState(pos).is(Blocks.WATER); } } static class TurtleLayEggGoal extends MoveToBlockGoal { private final Turtle turtle; TurtleLayEggGoal(Turtle turtle, double speedModifier) { super(turtle, speedModifier, 16); this.turtle = turtle; } @Override public boolean canUse() { return this.turtle.hasEgg() && this.turtle.homePos.closerToCenterThan(this.turtle.position(), 9.0) ? super.canUse() : false; } @Override public boolean canContinueToUse() { return super.canContinueToUse() && this.turtle.hasEgg() && this.turtle.homePos.closerToCenterThan(this.turtle.position(), 9.0); } @Override public void tick() { super.tick(); BlockPos blockPos = this.turtle.blockPosition(); if (!this.turtle.isInWater() && this.isReachedTarget()) { if (this.turtle.layEggCounter < 1) { this.turtle.setLayingEgg(true); } else if (this.turtle.layEggCounter > this.adjustedTickDelay(200)) { Level level = this.turtle.level(); level.playSound(null, blockPos, SoundEvents.TURTLE_LAY_EGG, SoundSource.BLOCKS, 0.3F, 0.9F + level.random.nextFloat() * 0.2F); BlockPos blockPos2 = this.blockPos.above(); BlockState blockState = Blocks.TURTLE_EGG.defaultBlockState().setValue(TurtleEggBlock.EGGS, this.turtle.random.nextInt(4) + 1); level.setBlock(blockPos2, blockState, 3); level.gameEvent(GameEvent.BLOCK_PLACE, blockPos2, Context.of(this.turtle, blockState)); this.turtle.setHasEgg(false); this.turtle.setLayingEgg(false); this.turtle.setInLoveTime(600); } if (this.turtle.isLayingEgg()) { this.turtle.layEggCounter++; } } } @Override protected boolean isValidTarget(LevelReader level, BlockPos pos) { return !level.isEmptyBlock(pos.above()) ? false : TurtleEggBlock.isSand(level, pos); } } static class TurtleMoveControl extends MoveControl { private final Turtle turtle; TurtleMoveControl(Turtle turtle) { super(turtle); this.turtle = turtle; } private void updateSpeed() { if (this.turtle.isInWater()) { this.turtle.setDeltaMovement(this.turtle.getDeltaMovement().add(0.0, 0.005, 0.0)); if (!this.turtle.homePos.closerToCenterThan(this.turtle.position(), 16.0)) { this.turtle.setSpeed(Math.max(this.turtle.getSpeed() / 2.0F, 0.08F)); } if (this.turtle.isBaby()) { this.turtle.setSpeed(Math.max(this.turtle.getSpeed() / 3.0F, 0.06F)); } } else if (this.turtle.onGround()) { this.turtle.setSpeed(Math.max(this.turtle.getSpeed() / 2.0F, 0.06F)); } } @Override public void tick() { this.updateSpeed(); if (this.operation == MoveControl.Operation.MOVE_TO && !this.turtle.getNavigation().isDone()) { double d = this.wantedX - this.turtle.getX(); double e = this.wantedY - this.turtle.getY(); double f = this.wantedZ - this.turtle.getZ(); double g = Math.sqrt(d * d + e * e + f * f); if (g < 1.0E-5F) { this.mob.setSpeed(0.0F); } else { e /= g; float h = (float)(Mth.atan2(f, d) * 180.0F / (float)Math.PI) - 90.0F; this.turtle.setYRot(this.rotlerp(this.turtle.getYRot(), h, 90.0F)); this.turtle.yBodyRot = this.turtle.getYRot(); float i = (float)(this.speedModifier * this.turtle.getAttributeValue(Attributes.MOVEMENT_SPEED)); this.turtle.setSpeed(Mth.lerp(0.125F, this.turtle.getSpeed(), i)); this.turtle.setDeltaMovement(this.turtle.getDeltaMovement().add(0.0, this.turtle.getSpeed() * e * 0.1, 0.0)); } } else { this.turtle.setSpeed(0.0F); } } } static class TurtlePanicGoal extends PanicGoal { TurtlePanicGoal(Turtle turtle, double speedModifier) { super(turtle, speedModifier); } @Override public boolean canUse() { if (!this.shouldPanic()) { return false; } else { BlockPos blockPos = this.lookForWater(this.mob.level(), this.mob, 7); if (blockPos != null) { this.posX = blockPos.getX(); this.posY = blockPos.getY(); this.posZ = blockPos.getZ(); return true; } else { return this.findRandomPosition(); } } } } static class TurtlePathNavigation extends AmphibiousPathNavigation { TurtlePathNavigation(Turtle turtle, Level level) { super(turtle, level); } @Override public boolean isStableDestination(BlockPos pos) { return this.mob instanceof Turtle turtle && turtle.travelPos != null ? this.level.getBlockState(pos).is(Blocks.WATER) : !this.level.getBlockState(pos.below()).isAir(); } } static class TurtleRandomStrollGoal extends RandomStrollGoal { private final Turtle turtle; TurtleRandomStrollGoal(Turtle turtle, double speedModifier, int interval) { super(turtle, speedModifier, interval); this.turtle = turtle; } @Override public boolean canUse() { return !this.mob.isInWater() && !this.turtle.goingHome && !this.turtle.hasEgg() ? super.canUse() : false; } } static class TurtleTravelGoal extends Goal { private final Turtle turtle; private final double speedModifier; private boolean stuck; TurtleTravelGoal(Turtle turtle, double speedModifier) { this.turtle = turtle; this.speedModifier = speedModifier; } @Override public boolean canUse() { return !this.turtle.goingHome && !this.turtle.hasEgg() && this.turtle.isInWater(); } @Override public void start() { int i = 512; int j = 4; RandomSource randomSource = this.turtle.random; int k = randomSource.nextInt(1025) - 512; int l = randomSource.nextInt(9) - 4; int m = randomSource.nextInt(1025) - 512; if (l + this.turtle.getY() > this.turtle.level().getSeaLevel() - 1) { l = 0; } this.turtle.travelPos = BlockPos.containing(k + this.turtle.getX(), l + this.turtle.getY(), m + this.turtle.getZ()); this.stuck = false; } @Override public void tick() { if (this.turtle.travelPos == null) { this.stuck = true; } else { if (this.turtle.getNavigation().isDone()) { Vec3 vec3 = Vec3.atBottomCenterOf(this.turtle.travelPos); Vec3 vec32 = DefaultRandomPos.getPosTowards(this.turtle, 16, 3, vec3, (float) (Math.PI / 10)); if (vec32 == null) { vec32 = DefaultRandomPos.getPosTowards(this.turtle, 8, 7, vec3, (float) (Math.PI / 2)); } if (vec32 != null) { int i = Mth.floor(vec32.x); int j = Mth.floor(vec32.z); int k = 34; if (!this.turtle.level().hasChunksAt(i - 34, j - 34, i + 34, j + 34)) { vec32 = null; } } if (vec32 == null) { this.stuck = true; return; } this.turtle.getNavigation().moveTo(vec32.x, vec32.y, vec32.z, this.speedModifier); } } } @Override public boolean canContinueToUse() { return !this.turtle.getNavigation().isDone() && !this.stuck && !this.turtle.goingHome && !this.turtle.isInLove() && !this.turtle.hasEgg(); } @Override public void stop() { this.turtle.travelPos = null; super.stop(); } } }