package net.minecraft.world.entity.animal; import com.mojang.serialization.Dynamic; import java.util.function.Predicate; import net.minecraft.core.BlockPos; import net.minecraft.network.protocol.game.ClientboundEntityPositionSyncPacket; import net.minecraft.network.protocol.game.DebugPackets; import net.minecraft.network.syncher.EntityDataAccessor; import net.minecraft.network.syncher.EntityDataSerializers; import net.minecraft.network.syncher.SynchedEntityData; 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.ItemTags; import net.minecraft.util.Mth; import net.minecraft.util.profiling.Profiler; import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.AgeableMob; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntitySpawnReason; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.Leashable; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.ai.Brain; import net.minecraft.world.entity.ai.Brain.Provider; import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.entity.ai.attributes.AttributeSupplier.Builder; import net.minecraft.world.entity.ai.control.BodyRotationControl; import net.minecraft.world.entity.ai.control.FlyingMoveControl; import net.minecraft.world.entity.ai.control.LookControl; import net.minecraft.world.entity.ai.goal.FloatGoal; import net.minecraft.world.entity.ai.goal.TemptGoal.ForNonPathfinders; import net.minecraft.world.entity.ai.navigation.FlyingPathNavigation; import net.minecraft.world.entity.ai.navigation.PathNavigation; import net.minecraft.world.entity.monster.Ghast; import net.minecraft.world.entity.monster.Ghast.GhastMoveControl; import net.minecraft.world.entity.monster.Ghast.RandomFloatAroundGoal; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelReader; import net.minecraft.world.level.biome.Biome.Precipitation; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.storage.ValueInput; import net.minecraft.world.level.storage.ValueOutput; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.Vec2; import net.minecraft.world.phys.Vec3; import org.jetbrains.annotations.Nullable; public class HappyGhast extends Animal { public static final float BABY_SCALE = 0.2375F; public static final int WANDER_GROUND_DISTANCE = 16; public static final int SMALL_RESTRICTION_RADIUS = 32; public static final int LARGE_RESTRICTION_RADIUS = 64; public static final int RESTRICTION_RADIUS_BUFFER = 16; public static final int FAST_HEALING_TICKS = 20; public static final int SLOW_HEALING_TICKS = 600; public static final int MAX_PASSANGERS = 4; private static final int STILL_TIMEOUT_ON_LOAD_GRACE_PERIOD = 60; private static final int MAX_STILL_TIMEOUT = 10; public static final float SPEED_MULTIPLIER_WHEN_PANICKING = 2.0F; public static final Predicate IS_FOOD = itemStack -> itemStack.is(ItemTags.HAPPY_GHAST_FOOD); private int leashHolderTime = 0; private int serverStillTimeout; private static final EntityDataAccessor IS_LEASH_HOLDER = SynchedEntityData.defineId(HappyGhast.class, EntityDataSerializers.BOOLEAN); private static final EntityDataAccessor STAYS_STILL = SynchedEntityData.defineId(HappyGhast.class, EntityDataSerializers.BOOLEAN); private static final float MAX_SCALE = 1.0F; public HappyGhast(EntityType entityType, Level level) { super(entityType, level); this.moveControl = new GhastMoveControl(this, true, this::isOnStillTimeout); this.lookControl = new HappyGhast.HappyGhastLookControl(); } private void setServerStillTimeout(int serverStillTimeout) { if (this.serverStillTimeout <= 0 && serverStillTimeout > 0 && this.level() instanceof ServerLevel serverLevel) { this.syncPacketPositionCodec(this.getX(), this.getY(), this.getZ()); serverLevel.getChunkSource().chunkMap.broadcast(this, ClientboundEntityPositionSyncPacket.of(this)); } this.serverStillTimeout = serverStillTimeout; this.syncStayStillFlag(); } private PathNavigation createBabyNavigation(Level level) { return new HappyGhast.BabyFlyingPathNavigation(this, level); } @Override protected void registerGoals() { this.goalSelector.addGoal(3, new HappyGhast.HappyGhastFloatGoal()); this.goalSelector .addGoal( 4, new ForNonPathfinders( this, 1.0, itemStack -> !this.isWearingBodyArmor() && !this.isBaby() ? itemStack.is(ItemTags.HAPPY_GHAST_TEMPT_ITEMS) : IS_FOOD.test(itemStack), false, 7.0 ) ); this.goalSelector.addGoal(5, new RandomFloatAroundGoal(this, 16)); } private void adultGhastSetup() { this.moveControl = new GhastMoveControl(this, true, this::isOnStillTimeout); this.lookControl = new HappyGhast.HappyGhastLookControl(); this.navigation = this.createNavigation(this.level()); if (this.level() instanceof ServerLevel serverLevel) { this.removeAllGoals(goal -> true); this.registerGoals(); ((Brain)this.brain).stopAll(serverLevel, this); this.brain.clearMemories(); } } private void babyGhastSetup() { this.moveControl = new FlyingMoveControl(this, 180, true); this.lookControl = new LookControl(this); this.navigation = this.createBabyNavigation(this.level()); this.setServerStillTimeout(0); this.removeAllGoals(goal -> true); } @Override protected void ageBoundaryReached() { if (this.isBaby()) { this.babyGhastSetup(); } else { this.adultGhastSetup(); } super.ageBoundaryReached(); } public static Builder createAttributes() { return Animal.createAnimalAttributes() .add(Attributes.MAX_HEALTH, 20.0) .add(Attributes.TEMPT_RANGE, 16.0) .add(Attributes.FLYING_SPEED, 0.05) .add(Attributes.MOVEMENT_SPEED, 0.05) .add(Attributes.FOLLOW_RANGE, 16.0) .add(Attributes.CAMERA_DISTANCE, 8.0); } @Override protected float sanitizeScale(float scale) { return Math.min(scale, 1.0F); } @Override protected void checkFallDamage(double y, boolean onGround, BlockState state, BlockPos pos) { } @Override public boolean onClimbable() { return false; } @Override public void travel(Vec3 travelVector) { float f = (float)this.getAttributeValue(Attributes.FLYING_SPEED) * 5.0F / 3.0F; this.travelFlying(travelVector, f, f, f); } @Override public float getWalkTargetValue(BlockPos pos, LevelReader level) { if (!level.isEmptyBlock(pos)) { return 0.0F; } else { return level.isEmptyBlock(pos.below()) && !level.isEmptyBlock(pos.below(2)) ? 10.0F : 5.0F; } } @Override public boolean canBreatheUnderwater() { return this.isBaby() ? true : super.canBreatheUnderwater(); } @Override protected boolean shouldStayCloseToLeashHolder() { return false; } @Override protected void playStepSound(BlockPos pos, BlockState state) { } @Override public float getVoicePitch() { return 1.0F; } @Override public SoundSource getSoundSource() { return SoundSource.NEUTRAL; } @Override public int getAmbientSoundInterval() { int i = super.getAmbientSoundInterval(); return this.isVehicle() ? i * 6 : i; } @Override protected SoundEvent getAmbientSound() { return this.isBaby() ? SoundEvents.GHASTLING_AMBIENT : SoundEvents.HAPPY_GHAST_AMBIENT; } @Override protected SoundEvent getHurtSound(DamageSource damageSource) { return this.isBaby() ? SoundEvents.GHASTLING_HURT : SoundEvents.HAPPY_GHAST_HURT; } @Override protected SoundEvent getDeathSound() { return this.isBaby() ? SoundEvents.GHASTLING_DEATH : SoundEvents.HAPPY_GHAST_DEATH; } @Override public int getMaxSpawnClusterSize() { return 1; } @Nullable @Override public AgeableMob getBreedOffspring(ServerLevel level, AgeableMob otherParent) { return EntityType.HAPPY_GHAST.create(level, EntitySpawnReason.BREEDING); } @Override public boolean canFallInLove() { return false; } @Override public float getAgeScale() { return this.isBaby() ? 0.2375F : 1.0F; } @Override public boolean isFood(ItemStack stack) { return IS_FOOD.test(stack); } @Override public boolean canUseSlot(EquipmentSlot slot) { return slot != EquipmentSlot.BODY ? super.canUseSlot(slot) : this.isAlive() && !this.isBaby(); } @Override protected boolean canDispenserEquipIntoSlot(EquipmentSlot slot) { return slot == EquipmentSlot.BODY; } @Override public InteractionResult mobInteract(Player player, InteractionHand hand) { if (this.isBaby()) { return super.mobInteract(player, hand); } else { ItemStack itemStack = player.getItemInHand(hand); if (!itemStack.isEmpty()) { InteractionResult interactionResult = itemStack.interactLivingEntity(player, this, hand); if (interactionResult.consumesAction()) { return interactionResult; } } if (this.isWearingBodyArmor() && !player.isSecondaryUseActive()) { this.doPlayerRide(player); return InteractionResult.SUCCESS; } else { return super.mobInteract(player, hand); } } } private void doPlayerRide(Player player) { if (!this.level().isClientSide) { player.startRiding(this); } } @Override protected void addPassenger(Entity passenger) { if (!this.isVehicle()) { this.level().playSound(null, this.getX(), this.getY(), this.getZ(), SoundEvents.HARNESS_GOGGLES_DOWN, this.getSoundSource(), 1.0F, 1.0F); } super.addPassenger(passenger); if (!this.level().isClientSide) { if (!this.scanPlayerAboveGhast()) { this.setServerStillTimeout(0); } else if (this.serverStillTimeout > 10) { this.setServerStillTimeout(10); } } } @Override protected void removePassenger(Entity passenger) { super.removePassenger(passenger); if (!this.level().isClientSide) { this.setServerStillTimeout(10); } if (!this.isVehicle()) { this.clearHome(); this.level().playSound(null, this.getX(), this.getY(), this.getZ(), SoundEvents.HARNESS_GOGGLES_UP, this.getSoundSource(), 1.0F, 1.0F); } } @Override protected boolean canAddPassenger(Entity passenger) { return this.getPassengers().size() < 4; } @Nullable @Override public LivingEntity getControllingPassenger() { return (LivingEntity)(this.isWearingBodyArmor() && !this.isOnStillTimeout() && this.getFirstPassenger() instanceof Player player ? player : super.getControllingPassenger()); } @Override protected Vec3 getRiddenInput(Player player, Vec3 travelVector) { float f = player.xxa; float g = 0.0F; float h = 0.0F; if (player.zza != 0.0F) { float i = Mth.cos(player.getXRot() * (float) (Math.PI / 180.0)); float j = -Mth.sin(player.getXRot() * (float) (Math.PI / 180.0)); if (player.zza < 0.0F) { i *= -0.5F; j *= -0.5F; } h = j; g = i; } if (player.isJumping()) { h += 0.5F; } return new Vec3(f, h, g).scale(3.9F * this.getAttributeValue(Attributes.FLYING_SPEED)); } protected Vec2 getRiddenRotation(LivingEntity entity) { return new Vec2(entity.getXRot() * 0.5F, entity.getYRot()); } @Override protected void tickRidden(Player player, Vec3 travelVector) { super.tickRidden(player, travelVector); Vec2 vec2 = this.getRiddenRotation(player); float f = this.getYRot(); float g = Mth.wrapDegrees(vec2.y - f); float h = 0.08F; f += g * 0.08F; this.setRot(f, vec2.x); this.yRotO = this.yBodyRot = this.yHeadRot = f; } @Override protected Provider brainProvider() { return HappyGhastAi.brainProvider(); } @Override protected Brain makeBrain(Dynamic dynamic) { return HappyGhastAi.makeBrain(this.brainProvider().makeBrain(dynamic)); } @Override protected void customServerAiStep(ServerLevel level) { if (this.isBaby()) { ProfilerFiller profilerFiller = Profiler.get(); profilerFiller.push("happyGhastBrain"); ((Brain)this.brain).tick(level, this); profilerFiller.pop(); profilerFiller.push("happyGhastActivityUpdate"); HappyGhastAi.updateActivity(this); profilerFiller.pop(); } this.checkRestriction(); super.customServerAiStep(level); } @Override public void tick() { super.tick(); if (!this.level().isClientSide()) { if (this.leashHolderTime > 0) { this.leashHolderTime--; } this.setLeashHolder(this.leashHolderTime > 0); if (this.serverStillTimeout > 0) { if (this.tickCount > 60) { this.serverStillTimeout--; } this.setServerStillTimeout(this.serverStillTimeout); } if (this.scanPlayerAboveGhast()) { this.setServerStillTimeout(10); } } } @Override public void aiStep() { if (!this.level().isClientSide) { this.setRequiresPrecisePosition(this.isOnStillTimeout()); } super.aiStep(); this.continuousHeal(); } private int getHappyGhastRestrictionRadius() { return !this.isBaby() && this.getItemBySlot(EquipmentSlot.BODY).isEmpty() ? 64 : 32; } private void checkRestriction() { if (!this.isLeashed() && !this.isVehicle()) { int i = this.getHappyGhastRestrictionRadius(); if (!this.hasHome() || !this.getHomePosition().closerThan(this.blockPosition(), i + 16) || i != this.getHomeRadius()) { this.setHomeTo(this.blockPosition(), i); } } } private void continuousHeal() { if (this.level() instanceof ServerLevel serverLevel && this.isAlive() && this.deathTime == 0 && this.getMaxHealth() != this.getHealth()) { boolean bl = serverLevel.dimensionType().natural() && (this.isInClouds() || serverLevel.precipitationAt(this.blockPosition()) != Precipitation.NONE); if (this.tickCount % (bl ? 20 : 600) == 0) { this.heal(1.0F); } } } @Override protected void sendDebugPackets() { super.sendDebugPackets(); DebugPackets.sendEntityBrain(this); } @Override protected void defineSynchedData(net.minecraft.network.syncher.SynchedEntityData.Builder builder) { super.defineSynchedData(builder); builder.define(IS_LEASH_HOLDER, false); builder.define(STAYS_STILL, false); } private void setLeashHolder(boolean leashHolder) { this.entityData.set(IS_LEASH_HOLDER, leashHolder); } public boolean isLeashHolder() { return this.entityData.get(IS_LEASH_HOLDER); } private void syncStayStillFlag() { this.entityData.set(STAYS_STILL, this.serverStillTimeout > 0); } public boolean staysStill() { return this.entityData.get(STAYS_STILL); } @Override public boolean supportQuadLeashAsHolder() { return true; } @Override public Vec3[] getQuadLeashHolderOffsets() { return Leashable.createQuadLeashOffsets(this, -0.03125, 0.4375, 0.46875, 0.03125); } @Override public Vec3 getLeashOffset() { return Vec3.ZERO; } @Override public double leashElasticDistance() { return 10.0; } @Override public double leashSnapDistance() { return 16.0; } @Override public void onElasticLeashPull() { super.onElasticLeashPull(); this.getMoveControl().setWait(); } @Override public void notifyLeashHolder(Leashable leashHolder) { if (leashHolder.supportQuadLeash()) { this.leashHolderTime = 5; } } @Override public void addAdditionalSaveData(ValueOutput output) { super.addAdditionalSaveData(output); output.putInt("still_timeout", this.serverStillTimeout); } @Override public void readAdditionalSaveData(ValueInput input) { super.readAdditionalSaveData(input); this.setServerStillTimeout(input.getIntOr("still_timeout", 0)); } public boolean isOnStillTimeout() { return this.staysStill() || this.serverStillTimeout > 0; } private boolean scanPlayerAboveGhast() { AABB aABB = this.getBoundingBox(); AABB aABB2 = new AABB(aABB.minX - 1.0, aABB.maxY - 1.0E-5F, aABB.minZ - 1.0, aABB.maxX + 1.0, aABB.maxY + aABB.getYsize() / 2.0, aABB.maxZ + 1.0); for (Player player : this.level().players()) { if (!player.isSpectator()) { Entity entity = player.getRootVehicle(); if (!(entity instanceof HappyGhast) && aABB2.contains(entity.position())) { return true; } } } return false; } @Override protected BodyRotationControl createBodyControl() { return new HappyGhast.HappyGhastBodyRotationControl(); } @Override public boolean canBeCollidedWith(@Nullable Entity entity) { if (!this.isBaby() && this.isAlive()) { if (this.level().isClientSide() && entity instanceof Player && entity.position().y >= this.getBoundingBox().maxY) { return true; } else { return this.isVehicle() && entity instanceof HappyGhast ? true : this.isOnStillTimeout(); } } else { return false; } } @Override public boolean isFlyingVehicle() { return !this.isBaby(); } static class BabyFlyingPathNavigation extends FlyingPathNavigation { public BabyFlyingPathNavigation(HappyGhast ghast, Level level) { super(ghast, level); this.setCanOpenDoors(false); this.setCanFloat(true); this.setRequiredPathLength(48.0F); } @Override protected boolean canMoveDirectly(Vec3 posVec31, Vec3 posVec32) { return isClearForMovementBetween(this.mob, posVec31, posVec32, false); } } class HappyGhastBodyRotationControl extends BodyRotationControl { public HappyGhastBodyRotationControl() { super(HappyGhast.this); } @Override public void clientTick() { if (HappyGhast.this.isVehicle()) { HappyGhast.this.yHeadRot = HappyGhast.this.getYRot(); HappyGhast.this.yBodyRot = HappyGhast.this.yHeadRot; } super.clientTick(); } } class HappyGhastFloatGoal extends FloatGoal { public HappyGhastFloatGoal() { super(HappyGhast.this); } @Override public boolean canUse() { return !HappyGhast.this.isOnStillTimeout() && super.canUse(); } } class HappyGhastLookControl extends LookControl { HappyGhastLookControl() { super(HappyGhast.this); } @Override public void tick() { if (HappyGhast.this.isOnStillTimeout()) { float f = wrapDegrees90(HappyGhast.this.getYRot()); HappyGhast.this.setYRot(HappyGhast.this.getYRot() - f); HappyGhast.this.setYHeadRot(HappyGhast.this.getYRot()); } else if (this.lookAtCooldown > 0) { this.lookAtCooldown--; double d = this.wantedX - HappyGhast.this.getX(); double e = this.wantedZ - HappyGhast.this.getZ(); HappyGhast.this.setYRot(-((float)Mth.atan2(d, e)) * (180.0F / (float)Math.PI)); HappyGhast.this.yBodyRot = HappyGhast.this.getYRot(); HappyGhast.this.yHeadRot = HappyGhast.this.yBodyRot; } else { Ghast.faceMovementDirection(this.mob); } } public static float wrapDegrees90(float degrees) { float f = degrees % 90.0F; if (f >= 45.0F) { f -= 90.0F; } if (f < -45.0F) { f += 90.0F; } return f; } } }