package net.minecraft.world.entity.animal; import com.mojang.serialization.Codec; import io.netty.buffer.ByteBuf; import java.util.EnumSet; import java.util.List; import java.util.Optional; import java.util.function.IntFunction; import java.util.function.Predicate; import java.util.stream.Stream; import net.minecraft.advancements.CriteriaTriggers; import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; import net.minecraft.core.component.DataComponentGetter; import net.minecraft.core.component.DataComponentType; import net.minecraft.core.component.DataComponents; import net.minecraft.core.particles.ItemParticleOption; import net.minecraft.core.particles.ParticleTypes; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.StreamCodec; 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.stats.Stats; import net.minecraft.tags.BiomeTags; import net.minecraft.tags.BlockTags; import net.minecraft.tags.FluidTags; import net.minecraft.tags.ItemTags; import net.minecraft.util.ByIdMap; import net.minecraft.util.Mth; import net.minecraft.util.RandomSource; import net.minecraft.util.StringRepresentable; import net.minecraft.util.ByIdMap.OutOfBoundsStrategy; import net.minecraft.world.DifficultyInstance; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.AgeableMob; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityDimensions; import net.minecraft.world.entity.EntityReference; import net.minecraft.world.entity.EntitySelector; import net.minecraft.world.entity.EntitySpawnReason; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.ExperienceOrb; 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.TamableAnimal; import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.entity.ai.control.LookControl; import net.minecraft.world.entity.ai.control.MoveControl; import net.minecraft.world.entity.ai.goal.AvoidEntityGoal; import net.minecraft.world.entity.ai.goal.BreedGoal; import net.minecraft.world.entity.ai.goal.ClimbOnTopOfPowderSnowGoal; import net.minecraft.world.entity.ai.goal.FleeSunGoal; import net.minecraft.world.entity.ai.goal.FloatGoal; import net.minecraft.world.entity.ai.goal.FollowParentGoal; import net.minecraft.world.entity.ai.goal.Goal; import net.minecraft.world.entity.ai.goal.JumpGoal; import net.minecraft.world.entity.ai.goal.LeapAtTargetGoal; import net.minecraft.world.entity.ai.goal.LookAtPlayerGoal; import net.minecraft.world.entity.ai.goal.MeleeAttackGoal; import net.minecraft.world.entity.ai.goal.MoveToBlockGoal; import net.minecraft.world.entity.ai.goal.PanicGoal; import net.minecraft.world.entity.ai.goal.StrollThroughVillageGoal; 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.NearestAttackableTargetGoal; import net.minecraft.world.entity.ai.targeting.TargetingConditions; import net.minecraft.world.entity.ai.targeting.TargetingConditions.Selector; import net.minecraft.world.entity.animal.wolf.Wolf; import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.entity.monster.Monster; 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.biome.Biome; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.CaveVines; import net.minecraft.world.level.block.SweetBerryBushBlock; 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 Fox extends Animal { private static final EntityDataAccessor DATA_TYPE_ID = SynchedEntityData.defineId(Fox.class, EntityDataSerializers.INT); private static final EntityDataAccessor DATA_FLAGS_ID = SynchedEntityData.defineId(Fox.class, EntityDataSerializers.BYTE); private static final int FLAG_SITTING = 1; public static final int FLAG_CROUCHING = 4; public static final int FLAG_INTERESTED = 8; public static final int FLAG_POUNCING = 16; private static final int FLAG_SLEEPING = 32; private static final int FLAG_FACEPLANTED = 64; private static final int FLAG_DEFENDING = 128; private static final EntityDataAccessor>> DATA_TRUSTED_ID_0 = SynchedEntityData.defineId( Fox.class, EntityDataSerializers.OPTIONAL_LIVING_ENTITY_REFERENCE ); private static final EntityDataAccessor>> DATA_TRUSTED_ID_1 = SynchedEntityData.defineId( Fox.class, EntityDataSerializers.OPTIONAL_LIVING_ENTITY_REFERENCE ); static final Predicate ALLOWED_ITEMS = itemEntity -> !itemEntity.hasPickUpDelay() && itemEntity.isAlive(); private static final Predicate TRUSTED_TARGET_SELECTOR = entity -> !(entity instanceof LivingEntity livingEntity) ? false : livingEntity.getLastHurtMob() != null && livingEntity.getLastHurtMobTimestamp() < livingEntity.tickCount + 600; static final Predicate STALKABLE_PREY = entity -> entity instanceof Chicken || entity instanceof Rabbit; private static final Predicate AVOID_PLAYERS = entity -> !entity.isDiscrete() && EntitySelector.NO_CREATIVE_OR_SPECTATOR.test(entity); private static final int MIN_TICKS_BEFORE_EAT = 600; private static final EntityDimensions BABY_DIMENSIONS = EntityType.FOX.getDimensions().scale(0.5F).withEyeHeight(0.2975F); private static final Codec>> TRUSTED_LIST_CODEC = EntityReference.codec().listOf(); private static final boolean DEFAULT_SLEEPING = false; private static final boolean DEFAULT_SITTING = false; private static final boolean DEFAULT_CROUCHING = false; private Goal landTargetGoal; private Goal turtleEggTargetGoal; private Goal fishTargetGoal; private float interestedAngle; private float interestedAngleO; float crouchAmount; float crouchAmountO; private int ticksSinceEaten; public Fox(EntityType entityType, Level level) { super(entityType, level); this.lookControl = new Fox.FoxLookControl(); this.moveControl = new Fox.FoxMoveControl(); this.setPathfindingMalus(PathType.DANGER_OTHER, 0.0F); this.setPathfindingMalus(PathType.DAMAGE_OTHER, 0.0F); this.setCanPickUpLoot(true); this.getNavigation().setRequiredPathLength(32.0F); } @Override protected void defineSynchedData(Builder builder) { super.defineSynchedData(builder); builder.define(DATA_TRUSTED_ID_0, Optional.empty()); builder.define(DATA_TRUSTED_ID_1, Optional.empty()); builder.define(DATA_TYPE_ID, Fox.Variant.DEFAULT.getId()); builder.define(DATA_FLAGS_ID, (byte)0); } @Override protected void registerGoals() { this.landTargetGoal = new NearestAttackableTargetGoal( this, Animal.class, 10, false, false, (livingEntity, serverLevel) -> livingEntity instanceof Chicken || livingEntity instanceof Rabbit ); this.turtleEggTargetGoal = new NearestAttackableTargetGoal(this, Turtle.class, 10, false, false, Turtle.BABY_ON_LAND_SELECTOR); this.fishTargetGoal = new NearestAttackableTargetGoal( this, AbstractFish.class, 20, false, false, (livingEntity, serverLevel) -> livingEntity instanceof AbstractSchoolingFish ); this.goalSelector.addGoal(0, new Fox.FoxFloatGoal()); this.goalSelector.addGoal(0, new ClimbOnTopOfPowderSnowGoal(this, this.level())); this.goalSelector.addGoal(1, new Fox.FaceplantGoal()); this.goalSelector.addGoal(2, new Fox.FoxPanicGoal(2.2)); this.goalSelector.addGoal(3, new Fox.FoxBreedGoal(1.0)); this.goalSelector .addGoal( 4, new AvoidEntityGoal( this, Player.class, 16.0F, 1.6, 1.4, livingEntity -> AVOID_PLAYERS.test(livingEntity) && !this.trusts(livingEntity) && !this.isDefending() ) ); this.goalSelector.addGoal(4, new AvoidEntityGoal(this, Wolf.class, 8.0F, 1.6, 1.4, livingEntity -> !((Wolf)livingEntity).isTame() && !this.isDefending())); this.goalSelector.addGoal(4, new AvoidEntityGoal(this, PolarBear.class, 8.0F, 1.6, 1.4, livingEntity -> !this.isDefending())); this.goalSelector.addGoal(5, new Fox.StalkPreyGoal()); this.goalSelector.addGoal(6, new Fox.FoxPounceGoal()); this.goalSelector.addGoal(6, new Fox.SeekShelterGoal(1.25)); this.goalSelector.addGoal(7, new Fox.FoxMeleeAttackGoal(1.2F, true)); this.goalSelector.addGoal(7, new Fox.SleepGoal()); this.goalSelector.addGoal(8, new Fox.FoxFollowParentGoal(this, 1.25)); this.goalSelector.addGoal(9, new Fox.FoxStrollThroughVillageGoal(32, 200)); this.goalSelector.addGoal(10, new Fox.FoxEatBerriesGoal(1.2F, 12, 1)); this.goalSelector.addGoal(10, new LeapAtTargetGoal(this, 0.4F)); this.goalSelector.addGoal(11, new WaterAvoidingRandomStrollGoal(this, 1.0)); this.goalSelector.addGoal(11, new Fox.FoxSearchForItemsGoal()); this.goalSelector.addGoal(12, new Fox.FoxLookAtPlayerGoal(this, Player.class, 24.0F)); this.goalSelector.addGoal(13, new Fox.PerchAndSearchGoal()); this.targetSelector .addGoal( 3, new Fox.DefendTrustedTargetGoal( LivingEntity.class, false, false, (livingEntity, serverLevel) -> TRUSTED_TARGET_SELECTOR.test(livingEntity) && !this.trusts(livingEntity) ) ); } @Override public void aiStep() { if (!this.level().isClientSide && this.isAlive() && this.isEffectiveAi()) { this.ticksSinceEaten++; ItemStack itemStack = this.getItemBySlot(EquipmentSlot.MAINHAND); if (this.canEat(itemStack)) { if (this.ticksSinceEaten > 600) { ItemStack itemStack2 = itemStack.finishUsingItem(this.level(), this); if (!itemStack2.isEmpty()) { this.setItemSlot(EquipmentSlot.MAINHAND, itemStack2); } this.ticksSinceEaten = 0; } else if (this.ticksSinceEaten > 560 && this.random.nextFloat() < 0.1F) { this.playEatingSound(); this.level().broadcastEntityEvent(this, (byte)45); } } LivingEntity livingEntity = this.getTarget(); if (livingEntity == null || !livingEntity.isAlive()) { this.setIsCrouching(false); this.setIsInterested(false); } } if (this.isSleeping() || this.isImmobile()) { this.jumping = false; this.xxa = 0.0F; this.zza = 0.0F; } super.aiStep(); if (this.isDefending() && this.random.nextFloat() < 0.05F) { this.playSound(SoundEvents.FOX_AGGRO, 1.0F, 1.0F); } } @Override protected boolean isImmobile() { return this.isDeadOrDying(); } private boolean canEat(ItemStack stack) { return stack.has(DataComponents.FOOD) && this.getTarget() == null && this.onGround() && !this.isSleeping(); } @Override protected void populateDefaultEquipmentSlots(RandomSource random, DifficultyInstance difficulty) { if (random.nextFloat() < 0.2F) { float f = random.nextFloat(); ItemStack itemStack; if (f < 0.05F) { itemStack = new ItemStack(Items.EMERALD); } else if (f < 0.2F) { itemStack = new ItemStack(Items.EGG); } else if (f < 0.4F) { itemStack = random.nextBoolean() ? new ItemStack(Items.RABBIT_FOOT) : new ItemStack(Items.RABBIT_HIDE); } else if (f < 0.6F) { itemStack = new ItemStack(Items.WHEAT); } else if (f < 0.8F) { itemStack = new ItemStack(Items.LEATHER); } else { itemStack = new ItemStack(Items.FEATHER); } this.setItemSlot(EquipmentSlot.MAINHAND, itemStack); } } @Override public void handleEntityEvent(byte id) { if (id == 45) { ItemStack itemStack = this.getItemBySlot(EquipmentSlot.MAINHAND); if (!itemStack.isEmpty()) { for (int i = 0; i < 8; i++) { Vec3 vec3 = new Vec3((this.random.nextFloat() - 0.5) * 0.1, Math.random() * 0.1 + 0.1, 0.0) .xRot(-this.getXRot() * (float) (Math.PI / 180.0)) .yRot(-this.getYRot() * (float) (Math.PI / 180.0)); this.level() .addParticle( new ItemParticleOption(ParticleTypes.ITEM, itemStack), this.getX() + this.getLookAngle().x / 2.0, this.getY(), this.getZ() + this.getLookAngle().z / 2.0, vec3.x, vec3.y + 0.05, vec3.z ); } } } else { super.handleEntityEvent(id); } } public static net.minecraft.world.entity.ai.attributes.AttributeSupplier.Builder createAttributes() { return Animal.createAnimalAttributes() .add(Attributes.MOVEMENT_SPEED, 0.3F) .add(Attributes.MAX_HEALTH, 10.0) .add(Attributes.ATTACK_DAMAGE, 2.0) .add(Attributes.SAFE_FALL_DISTANCE, 5.0) .add(Attributes.FOLLOW_RANGE, 32.0); } @Nullable public Fox getBreedOffspring(ServerLevel serverLevel, AgeableMob ageableMob) { Fox fox = EntityType.FOX.create(serverLevel, EntitySpawnReason.BREEDING); if (fox != null) { fox.setVariant(this.random.nextBoolean() ? this.getVariant() : ((Fox)ageableMob).getVariant()); } return fox; } public static boolean checkFoxSpawnRules(EntityType entityType, LevelAccessor level, EntitySpawnReason spawnReason, BlockPos pos, RandomSource random) { return level.getBlockState(pos.below()).is(BlockTags.FOXES_SPAWNABLE_ON) && isBrightEnoughToSpawn(level, pos); } @Nullable @Override public SpawnGroupData finalizeSpawn( ServerLevelAccessor level, DifficultyInstance difficulty, EntitySpawnReason spawnReason, @Nullable SpawnGroupData spawnGroupData ) { Holder holder = level.getBiome(this.blockPosition()); Fox.Variant variant = Fox.Variant.byBiome(holder); boolean bl = false; if (spawnGroupData instanceof Fox.FoxGroupData foxGroupData) { variant = foxGroupData.variant; if (foxGroupData.getGroupSize() >= 2) { bl = true; } } else { spawnGroupData = new Fox.FoxGroupData(variant); } this.setVariant(variant); if (bl) { this.setAge(-24000); } if (level instanceof ServerLevel) { this.setTargetGoals(); } this.populateDefaultEquipmentSlots(level.getRandom(), difficulty); return super.finalizeSpawn(level, difficulty, spawnReason, spawnGroupData); } private void setTargetGoals() { if (this.getVariant() == Fox.Variant.RED) { this.targetSelector.addGoal(4, this.landTargetGoal); this.targetSelector.addGoal(4, this.turtleEggTargetGoal); this.targetSelector.addGoal(6, this.fishTargetGoal); } else { this.targetSelector.addGoal(4, this.fishTargetGoal); this.targetSelector.addGoal(6, this.landTargetGoal); this.targetSelector.addGoal(6, this.turtleEggTargetGoal); } } @Override protected void playEatingSound() { this.playSound(SoundEvents.FOX_EAT, 1.0F, 1.0F); } @Override public EntityDimensions getDefaultDimensions(Pose pose) { return this.isBaby() ? BABY_DIMENSIONS : super.getDefaultDimensions(pose); } public Fox.Variant getVariant() { return Fox.Variant.byId(this.entityData.get(DATA_TYPE_ID)); } private void setVariant(Fox.Variant variant) { this.entityData.set(DATA_TYPE_ID, variant.getId()); } @Nullable @Override public T get(DataComponentType component) { return component == DataComponents.FOX_VARIANT ? castComponentValue((DataComponentType)component, this.getVariant()) : super.get(component); } @Override protected void applyImplicitComponents(DataComponentGetter componentGetter) { this.applyImplicitComponentIfPresent(componentGetter, DataComponents.FOX_VARIANT); super.applyImplicitComponents(componentGetter); } @Override protected boolean applyImplicitComponent(DataComponentType component, T value) { if (component == DataComponents.FOX_VARIANT) { this.setVariant(castComponentValue(DataComponents.FOX_VARIANT, value)); return true; } else { return super.applyImplicitComponent(component, value); } } Stream> getTrustedEntities() { return Stream.concat(this.entityData.get(DATA_TRUSTED_ID_0).stream(), this.entityData.get(DATA_TRUSTED_ID_1).stream()); } void addTrustedEntity(LivingEntity entity) { this.addTrustedEntity(new EntityReference<>(entity)); } private void addTrustedEntity(EntityReference entityReference) { if (this.entityData.get(DATA_TRUSTED_ID_0).isPresent()) { this.entityData.set(DATA_TRUSTED_ID_1, Optional.of(entityReference)); } else { this.entityData.set(DATA_TRUSTED_ID_0, Optional.of(entityReference)); } } @Override public void addAdditionalSaveData(CompoundTag tag) { super.addAdditionalSaveData(tag); tag.store("Trusted", TRUSTED_LIST_CODEC, this.getTrustedEntities().toList()); tag.putBoolean("Sleeping", this.isSleeping()); tag.store("Type", Fox.Variant.CODEC, this.getVariant()); tag.putBoolean("Sitting", this.isSitting()); tag.putBoolean("Crouching", this.isCrouching()); } @Override public void readAdditionalSaveData(CompoundTag tag) { super.readAdditionalSaveData(tag); this.clearTrusted(); ((List)tag.read("Trusted", TRUSTED_LIST_CODEC).orElse(List.of())).forEach(this::addTrustedEntity); this.setSleeping(tag.getBooleanOr("Sleeping", false)); this.setVariant((Fox.Variant)tag.read("Type", Fox.Variant.CODEC).orElse(Fox.Variant.DEFAULT)); this.setSitting(tag.getBooleanOr("Sitting", false)); this.setIsCrouching(tag.getBooleanOr("Crouching", false)); if (this.level() instanceof ServerLevel) { this.setTargetGoals(); } } private void clearTrusted() { this.entityData.set(DATA_TRUSTED_ID_0, Optional.empty()); this.entityData.set(DATA_TRUSTED_ID_1, Optional.empty()); } public boolean isSitting() { return this.getFlag(1); } public void setSitting(boolean sitting) { this.setFlag(1, sitting); } public boolean isFaceplanted() { return this.getFlag(64); } void setFaceplanted(boolean faceplanted) { this.setFlag(64, faceplanted); } boolean isDefending() { return this.getFlag(128); } void setDefending(boolean defending) { this.setFlag(128, defending); } @Override public boolean isSleeping() { return this.getFlag(32); } void setSleeping(boolean sleeping) { this.setFlag(32, sleeping); } private void setFlag(int flagId, boolean value) { if (value) { this.entityData.set(DATA_FLAGS_ID, (byte)(this.entityData.get(DATA_FLAGS_ID) | flagId)); } else { this.entityData.set(DATA_FLAGS_ID, (byte)(this.entityData.get(DATA_FLAGS_ID) & ~flagId)); } } private boolean getFlag(int flagId) { return (this.entityData.get(DATA_FLAGS_ID) & flagId) != 0; } @Override protected boolean canDispenserEquipIntoSlot(EquipmentSlot slot) { return slot == EquipmentSlot.MAINHAND && this.canPickUpLoot(); } @Override public boolean canHoldItem(ItemStack stack) { ItemStack itemStack = this.getItemBySlot(EquipmentSlot.MAINHAND); return itemStack.isEmpty() || this.ticksSinceEaten > 0 && stack.has(DataComponents.FOOD) && !itemStack.has(DataComponents.FOOD); } private void spitOutItem(ItemStack stack) { if (!stack.isEmpty() && !this.level().isClientSide) { ItemEntity itemEntity = new ItemEntity(this.level(), this.getX() + this.getLookAngle().x, this.getY() + 1.0, this.getZ() + this.getLookAngle().z, stack); itemEntity.setPickUpDelay(40); itemEntity.setThrower(this); this.playSound(SoundEvents.FOX_SPIT, 1.0F, 1.0F); this.level().addFreshEntity(itemEntity); } } private void dropItemStack(ItemStack stack) { ItemEntity itemEntity = new ItemEntity(this.level(), this.getX(), this.getY(), this.getZ(), stack); this.level().addFreshEntity(itemEntity); } @Override protected void pickUpItem(ServerLevel level, ItemEntity entity) { ItemStack itemStack = entity.getItem(); if (this.canHoldItem(itemStack)) { int i = itemStack.getCount(); if (i > 1) { this.dropItemStack(itemStack.split(i - 1)); } this.spitOutItem(this.getItemBySlot(EquipmentSlot.MAINHAND)); this.onItemPickup(entity); this.setItemSlot(EquipmentSlot.MAINHAND, itemStack.split(1)); this.setGuaranteedDrop(EquipmentSlot.MAINHAND); this.take(entity, itemStack.getCount()); entity.discard(); this.ticksSinceEaten = 0; } } @Override public void tick() { super.tick(); if (this.isEffectiveAi()) { boolean bl = this.isInWater(); if (bl || this.getTarget() != null || this.level().isThundering()) { this.wakeUp(); } if (bl || this.isSleeping()) { this.setSitting(false); } if (this.isFaceplanted() && this.level().random.nextFloat() < 0.2F) { BlockPos blockPos = this.blockPosition(); BlockState blockState = this.level().getBlockState(blockPos); this.level().levelEvent(2001, blockPos, Block.getId(blockState)); } } this.interestedAngleO = this.interestedAngle; if (this.isInterested()) { this.interestedAngle = this.interestedAngle + (1.0F - this.interestedAngle) * 0.4F; } else { this.interestedAngle = this.interestedAngle + (0.0F - this.interestedAngle) * 0.4F; } this.crouchAmountO = this.crouchAmount; if (this.isCrouching()) { this.crouchAmount += 0.2F; if (this.crouchAmount > 3.0F) { this.crouchAmount = 3.0F; } } else { this.crouchAmount = 0.0F; } } @Override public boolean isFood(ItemStack stack) { return stack.is(ItemTags.FOX_FOOD); } @Override protected void onOffspringSpawnedFromEgg(Player player, Mob child) { ((Fox)child).addTrustedEntity(player); } public boolean isPouncing() { return this.getFlag(16); } public void setIsPouncing(boolean isPouncing) { this.setFlag(16, isPouncing); } public boolean isJumping() { return this.jumping; } public boolean isFullyCrouched() { return this.crouchAmount == 3.0F; } public void setIsCrouching(boolean isCrouching) { this.setFlag(4, isCrouching); } @Override public boolean isCrouching() { return this.getFlag(4); } public void setIsInterested(boolean isInterested) { this.setFlag(8, isInterested); } public boolean isInterested() { return this.getFlag(8); } public float getHeadRollAngle(float partialTick) { return Mth.lerp(partialTick, this.interestedAngleO, this.interestedAngle) * 0.11F * (float) Math.PI; } public float getCrouchAmount(float partialTick) { return Mth.lerp(partialTick, this.crouchAmountO, this.crouchAmount); } @Override public void setTarget(@Nullable LivingEntity livingEntity) { if (this.isDefending() && livingEntity == null) { this.setDefending(false); } super.setTarget(livingEntity); } void wakeUp() { this.setSleeping(false); } void clearStates() { this.setIsInterested(false); this.setIsCrouching(false); this.setSitting(false); this.setSleeping(false); this.setDefending(false); this.setFaceplanted(false); } boolean canMove() { return !this.isSleeping() && !this.isSitting() && !this.isFaceplanted(); } @Override public void playAmbientSound() { SoundEvent soundEvent = this.getAmbientSound(); if (soundEvent == SoundEvents.FOX_SCREECH) { this.playSound(soundEvent, 2.0F, this.getVoicePitch()); } else { super.playAmbientSound(); } } @Nullable @Override protected SoundEvent getAmbientSound() { if (this.isSleeping()) { return SoundEvents.FOX_SLEEP; } else { if (!this.level().isBrightOutside() && this.random.nextFloat() < 0.1F) { List list = this.level().getEntitiesOfClass(Player.class, this.getBoundingBox().inflate(16.0, 16.0, 16.0), EntitySelector.NO_SPECTATORS); if (list.isEmpty()) { return SoundEvents.FOX_SCREECH; } } return SoundEvents.FOX_AMBIENT; } } @Nullable @Override protected SoundEvent getHurtSound(DamageSource damageSource) { return SoundEvents.FOX_HURT; } @Nullable @Override protected SoundEvent getDeathSound() { return SoundEvents.FOX_DEATH; } boolean trusts(LivingEntity entity) { return this.getTrustedEntities().anyMatch(entityReference -> entityReference.matches(entity)); } @Override protected void dropAllDeathLoot(ServerLevel level, DamageSource damageSource) { ItemStack itemStack = this.getItemBySlot(EquipmentSlot.MAINHAND); if (!itemStack.isEmpty()) { this.spawnAtLocation(level, itemStack); this.setItemSlot(EquipmentSlot.MAINHAND, ItemStack.EMPTY); } super.dropAllDeathLoot(level, damageSource); } public static boolean isPathClear(Fox fox, LivingEntity livingEntity) { double d = livingEntity.getZ() - fox.getZ(); double e = livingEntity.getX() - fox.getX(); double f = d / e; int i = 6; for (int j = 0; j < 6; j++) { double g = f == 0.0 ? 0.0 : d * (j / 6.0F); double h = f == 0.0 ? e * (j / 6.0F) : g / f; for (int k = 1; k < 4; k++) { if (!fox.level().getBlockState(BlockPos.containing(fox.getX() + h, fox.getY() + k, fox.getZ() + g)).canBeReplaced()) { return false; } } } return true; } @Override public Vec3 getLeashOffset() { return new Vec3(0.0, 0.55F * this.getEyeHeight(), this.getBbWidth() * 0.4F); } class DefendTrustedTargetGoal extends NearestAttackableTargetGoal { @Nullable private LivingEntity trustedLastHurtBy; @Nullable private LivingEntity trustedLastHurt; private int timestamp; public DefendTrustedTargetGoal(final Class targetType, final boolean mustSee, final boolean mustReach, @Nullable final Selector selector) { super(Fox.this, targetType, 10, mustSee, mustReach, selector); } @Override public boolean canUse() { if (this.randomInterval > 0 && this.mob.getRandom().nextInt(this.randomInterval) != 0) { return false; } else { ServerLevel serverLevel = getServerLevel(Fox.this.level()); for (EntityReference entityReference : Fox.this.getTrustedEntities().toList()) { LivingEntity livingEntity = (LivingEntity)entityReference.getEntity(serverLevel, LivingEntity.class); if (livingEntity != null) { this.trustedLastHurt = livingEntity; this.trustedLastHurtBy = livingEntity.getLastHurtByMob(); int i = livingEntity.getLastHurtByMobTimestamp(); return i != this.timestamp && this.canAttack(this.trustedLastHurtBy, this.targetConditions); } } return false; } } @Override public void start() { this.setTarget(this.trustedLastHurtBy); this.target = this.trustedLastHurtBy; if (this.trustedLastHurt != null) { this.timestamp = this.trustedLastHurt.getLastHurtByMobTimestamp(); } Fox.this.playSound(SoundEvents.FOX_AGGRO, 1.0F, 1.0F); Fox.this.setDefending(true); Fox.this.wakeUp(); super.start(); } } class FaceplantGoal extends Goal { int countdown; public FaceplantGoal() { this.setFlags(EnumSet.of(Flag.LOOK, Flag.JUMP, Flag.MOVE)); } @Override public boolean canUse() { return Fox.this.isFaceplanted(); } @Override public boolean canContinueToUse() { return this.canUse() && this.countdown > 0; } @Override public void start() { this.countdown = this.adjustedTickDelay(40); } @Override public void stop() { Fox.this.setFaceplanted(false); } @Override public void tick() { this.countdown--; } } public class FoxAlertableEntitiesSelector implements Selector { @Override public boolean test(LivingEntity livingEntity, ServerLevel serverLevel) { if (livingEntity instanceof Fox) { return false; } else if (livingEntity instanceof Chicken || livingEntity instanceof Rabbit || livingEntity instanceof Monster) { return true; } else if (livingEntity instanceof TamableAnimal) { return !((TamableAnimal)livingEntity).isTame(); } else if (livingEntity instanceof Player player && (player.isSpectator() || player.isCreative())) { return false; } else { return Fox.this.trusts(livingEntity) ? false : !livingEntity.isSleeping() && !livingEntity.isDiscrete(); } } } abstract class FoxBehaviorGoal extends Goal { private final TargetingConditions alertableTargeting = TargetingConditions.forCombat() .range(12.0) .ignoreLineOfSight() .selector(Fox.this.new FoxAlertableEntitiesSelector()); protected boolean hasShelter() { BlockPos blockPos = BlockPos.containing(Fox.this.getX(), Fox.this.getBoundingBox().maxY, Fox.this.getZ()); return !Fox.this.level().canSeeSky(blockPos) && Fox.this.getWalkTargetValue(blockPos) >= 0.0F; } protected boolean alertable() { return !getServerLevel(Fox.this.level()) .getNearbyEntities(LivingEntity.class, this.alertableTargeting, Fox.this, Fox.this.getBoundingBox().inflate(12.0, 6.0, 12.0)) .isEmpty(); } } class FoxBreedGoal extends BreedGoal { public FoxBreedGoal(final double speedModifier) { super(Fox.this, speedModifier); } @Override public void start() { ((Fox)this.animal).clearStates(); ((Fox)this.partner).clearStates(); super.start(); } @Override protected void breed() { ServerLevel serverLevel = this.level; Fox fox = (Fox)this.animal.getBreedOffspring(serverLevel, this.partner); if (fox != null) { ServerPlayer serverPlayer = this.animal.getLoveCause(); ServerPlayer serverPlayer2 = this.partner.getLoveCause(); ServerPlayer serverPlayer3 = serverPlayer; if (serverPlayer != null) { fox.addTrustedEntity(serverPlayer); } else { serverPlayer3 = serverPlayer2; } if (serverPlayer2 != null && serverPlayer != serverPlayer2) { fox.addTrustedEntity(serverPlayer2); } if (serverPlayer3 != null) { serverPlayer3.awardStat(Stats.ANIMALS_BRED); CriteriaTriggers.BRED_ANIMALS.trigger(serverPlayer3, this.animal, this.partner, fox); } this.animal.setAge(6000); this.partner.setAge(6000); this.animal.resetLove(); this.partner.resetLove(); fox.setAge(-24000); fox.snapTo(this.animal.getX(), this.animal.getY(), this.animal.getZ(), 0.0F, 0.0F); serverLevel.addFreshEntityWithPassengers(fox); this.level.broadcastEntityEvent(this.animal, (byte)18); if (serverLevel.getGameRules().getBoolean(GameRules.RULE_DOMOBLOOT)) { this.level .addFreshEntity(new ExperienceOrb(this.level, this.animal.getX(), this.animal.getY(), this.animal.getZ(), this.animal.getRandom().nextInt(7) + 1)); } } } } public class FoxEatBerriesGoal extends MoveToBlockGoal { private static final int WAIT_TICKS = 40; protected int ticksWaited; public FoxEatBerriesGoal(final double speedModifier, final int searchRange, final int verticalSearchRange) { super(Fox.this, speedModifier, searchRange, verticalSearchRange); } @Override public double acceptedDistance() { return 2.0; } @Override public boolean shouldRecalculatePath() { return this.tryTicks % 100 == 0; } @Override protected boolean isValidTarget(LevelReader level, BlockPos pos) { BlockState blockState = level.getBlockState(pos); return blockState.is(Blocks.SWEET_BERRY_BUSH) && (Integer)blockState.getValue(SweetBerryBushBlock.AGE) >= 2 || CaveVines.hasGlowBerries(blockState); } @Override public void tick() { if (this.isReachedTarget()) { if (this.ticksWaited >= 40) { this.onReachedTarget(); } else { this.ticksWaited++; } } else if (!this.isReachedTarget() && Fox.this.random.nextFloat() < 0.05F) { Fox.this.playSound(SoundEvents.FOX_SNIFF, 1.0F, 1.0F); } super.tick(); } protected void onReachedTarget() { if (getServerLevel(Fox.this.level()).getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) { BlockState blockState = Fox.this.level().getBlockState(this.blockPos); if (blockState.is(Blocks.SWEET_BERRY_BUSH)) { this.pickSweetBerries(blockState); } else if (CaveVines.hasGlowBerries(blockState)) { this.pickGlowBerry(blockState); } } } private void pickGlowBerry(BlockState state) { CaveVines.use(Fox.this, state, Fox.this.level(), this.blockPos); } private void pickSweetBerries(BlockState state) { int i = (Integer)state.getValue(SweetBerryBushBlock.AGE); state.setValue(SweetBerryBushBlock.AGE, 1); int j = 1 + Fox.this.level().random.nextInt(2) + (i == 3 ? 1 : 0); ItemStack itemStack = Fox.this.getItemBySlot(EquipmentSlot.MAINHAND); if (itemStack.isEmpty()) { Fox.this.setItemSlot(EquipmentSlot.MAINHAND, new ItemStack(Items.SWEET_BERRIES)); j--; } if (j > 0) { Block.popResource(Fox.this.level(), this.blockPos, new ItemStack(Items.SWEET_BERRIES, j)); } Fox.this.playSound(SoundEvents.SWEET_BERRY_BUSH_PICK_BERRIES, 1.0F, 1.0F); Fox.this.level().setBlock(this.blockPos, state.setValue(SweetBerryBushBlock.AGE, 1), 2); Fox.this.level().gameEvent(GameEvent.BLOCK_CHANGE, this.blockPos, Context.of(Fox.this)); } @Override public boolean canUse() { return !Fox.this.isSleeping() && super.canUse(); } @Override public void start() { this.ticksWaited = 0; Fox.this.setSitting(false); super.start(); } } class FoxFloatGoal extends FloatGoal { public FoxFloatGoal() { super(Fox.this); } @Override public void start() { super.start(); Fox.this.clearStates(); } @Override public boolean canUse() { return Fox.this.isInWater() && Fox.this.getFluidHeight(FluidTags.WATER) > 0.25 || Fox.this.isInLava(); } } static class FoxFollowParentGoal extends FollowParentGoal { private final Fox fox; public FoxFollowParentGoal(Fox fox, double speedModifier) { super(fox, speedModifier); this.fox = fox; } @Override public boolean canUse() { return !this.fox.isDefending() && super.canUse(); } @Override public boolean canContinueToUse() { return !this.fox.isDefending() && super.canContinueToUse(); } @Override public void start() { this.fox.clearStates(); super.start(); } } public static class FoxGroupData extends AgeableMob.AgeableMobGroupData { public final Fox.Variant variant; public FoxGroupData(Fox.Variant variant) { super(false); this.variant = variant; } } class FoxLookAtPlayerGoal extends LookAtPlayerGoal { public FoxLookAtPlayerGoal(final Mob mob, final Class lookAtType, final float lookDistance) { super(mob, lookAtType, lookDistance); } @Override public boolean canUse() { return super.canUse() && !Fox.this.isFaceplanted() && !Fox.this.isInterested(); } @Override public boolean canContinueToUse() { return super.canContinueToUse() && !Fox.this.isFaceplanted() && !Fox.this.isInterested(); } } public class FoxLookControl extends LookControl { public FoxLookControl() { super(Fox.this); } @Override public void tick() { if (!Fox.this.isSleeping()) { super.tick(); } } @Override protected boolean resetXRotOnTick() { return !Fox.this.isPouncing() && !Fox.this.isCrouching() && !Fox.this.isInterested() && !Fox.this.isFaceplanted(); } } class FoxMeleeAttackGoal extends MeleeAttackGoal { public FoxMeleeAttackGoal(final double speedModifier, final boolean followingTargetEvenIfNotSeen) { super(Fox.this, speedModifier, followingTargetEvenIfNotSeen); } @Override protected void checkAndPerformAttack(LivingEntity target) { if (this.canPerformAttack(target)) { this.resetAttackCooldown(); this.mob.doHurtTarget(getServerLevel(this.mob), target); Fox.this.playSound(SoundEvents.FOX_BITE, 1.0F, 1.0F); } } @Override public void start() { Fox.this.setIsInterested(false); super.start(); } @Override public boolean canUse() { return !Fox.this.isSitting() && !Fox.this.isSleeping() && !Fox.this.isCrouching() && !Fox.this.isFaceplanted() && super.canUse(); } } class FoxMoveControl extends MoveControl { public FoxMoveControl() { super(Fox.this); } @Override public void tick() { if (Fox.this.canMove()) { super.tick(); } } } class FoxPanicGoal extends PanicGoal { public FoxPanicGoal(final double speedModifier) { super(Fox.this, speedModifier); } @Override public boolean shouldPanic() { return !Fox.this.isDefending() && super.shouldPanic(); } } public class FoxPounceGoal extends JumpGoal { @Override public boolean canUse() { if (!Fox.this.isFullyCrouched()) { return false; } else { LivingEntity livingEntity = Fox.this.getTarget(); if (livingEntity != null && livingEntity.isAlive()) { if (livingEntity.getMotionDirection() != livingEntity.getDirection()) { return false; } else { boolean bl = Fox.isPathClear(Fox.this, livingEntity); if (!bl) { Fox.this.getNavigation().createPath(livingEntity, 0); Fox.this.setIsCrouching(false); Fox.this.setIsInterested(false); } return bl; } } else { return false; } } } @Override public boolean canContinueToUse() { LivingEntity livingEntity = Fox.this.getTarget(); if (livingEntity != null && livingEntity.isAlive()) { double d = Fox.this.getDeltaMovement().y; return (!(d * d < 0.05F) || !(Math.abs(Fox.this.getXRot()) < 15.0F) || !Fox.this.onGround()) && !Fox.this.isFaceplanted(); } else { return false; } } @Override public boolean isInterruptable() { return false; } @Override public void start() { Fox.this.setJumping(true); Fox.this.setIsPouncing(true); Fox.this.setIsInterested(false); LivingEntity livingEntity = Fox.this.getTarget(); if (livingEntity != null) { Fox.this.getLookControl().setLookAt(livingEntity, 60.0F, 30.0F); Vec3 vec3 = new Vec3(livingEntity.getX() - Fox.this.getX(), livingEntity.getY() - Fox.this.getY(), livingEntity.getZ() - Fox.this.getZ()).normalize(); Fox.this.setDeltaMovement(Fox.this.getDeltaMovement().add(vec3.x * 0.8, 0.9, vec3.z * 0.8)); } Fox.this.getNavigation().stop(); } @Override public void stop() { Fox.this.setIsCrouching(false); Fox.this.crouchAmount = 0.0F; Fox.this.crouchAmountO = 0.0F; Fox.this.setIsInterested(false); Fox.this.setIsPouncing(false); } @Override public void tick() { LivingEntity livingEntity = Fox.this.getTarget(); if (livingEntity != null) { Fox.this.getLookControl().setLookAt(livingEntity, 60.0F, 30.0F); } if (!Fox.this.isFaceplanted()) { Vec3 vec3 = Fox.this.getDeltaMovement(); if (vec3.y * vec3.y < 0.03F && Fox.this.getXRot() != 0.0F) { Fox.this.setXRot(Mth.rotLerp(0.2F, Fox.this.getXRot(), 0.0F)); } else { double d = vec3.horizontalDistance(); double e = Math.signum(-vec3.y) * Math.acos(d / vec3.length()) * 180.0F / (float)Math.PI; Fox.this.setXRot((float)e); } } if (livingEntity != null && Fox.this.distanceTo(livingEntity) <= 2.0F) { Fox.this.doHurtTarget(getServerLevel(Fox.this.level()), livingEntity); } else if (Fox.this.getXRot() > 0.0F && Fox.this.onGround() && (float)Fox.this.getDeltaMovement().y != 0.0F && Fox.this.level().getBlockState(Fox.this.blockPosition()).is(Blocks.SNOW)) { Fox.this.setXRot(60.0F); Fox.this.setTarget(null); Fox.this.setFaceplanted(true); } } } class FoxSearchForItemsGoal extends Goal { public FoxSearchForItemsGoal() { this.setFlags(EnumSet.of(Flag.MOVE)); } @Override public boolean canUse() { if (!Fox.this.getItemBySlot(EquipmentSlot.MAINHAND).isEmpty()) { return false; } else if (Fox.this.getTarget() != null || Fox.this.getLastHurtByMob() != null) { return false; } else if (!Fox.this.canMove()) { return false; } else if (Fox.this.getRandom().nextInt(reducedTickDelay(10)) != 0) { return false; } else { List list = Fox.this.level().getEntitiesOfClass(ItemEntity.class, Fox.this.getBoundingBox().inflate(8.0, 8.0, 8.0), Fox.ALLOWED_ITEMS); return !list.isEmpty() && Fox.this.getItemBySlot(EquipmentSlot.MAINHAND).isEmpty(); } } @Override public void tick() { List list = Fox.this.level().getEntitiesOfClass(ItemEntity.class, Fox.this.getBoundingBox().inflate(8.0, 8.0, 8.0), Fox.ALLOWED_ITEMS); ItemStack itemStack = Fox.this.getItemBySlot(EquipmentSlot.MAINHAND); if (itemStack.isEmpty() && !list.isEmpty()) { Fox.this.getNavigation().moveTo((Entity)list.get(0), 1.2F); } } @Override public void start() { List list = Fox.this.level().getEntitiesOfClass(ItemEntity.class, Fox.this.getBoundingBox().inflate(8.0, 8.0, 8.0), Fox.ALLOWED_ITEMS); if (!list.isEmpty()) { Fox.this.getNavigation().moveTo((Entity)list.get(0), 1.2F); } } } class FoxStrollThroughVillageGoal extends StrollThroughVillageGoal { public FoxStrollThroughVillageGoal(final int unused32, final int interval) { super(Fox.this, interval); } @Override public void start() { Fox.this.clearStates(); super.start(); } @Override public boolean canUse() { return super.canUse() && this.canFoxMove(); } @Override public boolean canContinueToUse() { return super.canContinueToUse() && this.canFoxMove(); } private boolean canFoxMove() { return !Fox.this.isSleeping() && !Fox.this.isSitting() && !Fox.this.isDefending() && Fox.this.getTarget() == null; } } class PerchAndSearchGoal extends Fox.FoxBehaviorGoal { private double relX; private double relZ; private int lookTime; private int looksRemaining; public PerchAndSearchGoal() { this.setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK)); } @Override public boolean canUse() { return Fox.this.getLastHurtByMob() == null && Fox.this.getRandom().nextFloat() < 0.02F && !Fox.this.isSleeping() && Fox.this.getTarget() == null && Fox.this.getNavigation().isDone() && !this.alertable() && !Fox.this.isPouncing() && !Fox.this.isCrouching(); } @Override public boolean canContinueToUse() { return this.looksRemaining > 0; } @Override public void start() { this.resetLook(); this.looksRemaining = 2 + Fox.this.getRandom().nextInt(3); Fox.this.setSitting(true); Fox.this.getNavigation().stop(); } @Override public void stop() { Fox.this.setSitting(false); } @Override public void tick() { this.lookTime--; if (this.lookTime <= 0) { this.looksRemaining--; this.resetLook(); } Fox.this.getLookControl() .setLookAt(Fox.this.getX() + this.relX, Fox.this.getEyeY(), Fox.this.getZ() + this.relZ, Fox.this.getMaxHeadYRot(), Fox.this.getMaxHeadXRot()); } private void resetLook() { double d = (Math.PI * 2) * Fox.this.getRandom().nextDouble(); this.relX = Math.cos(d); this.relZ = Math.sin(d); this.lookTime = this.adjustedTickDelay(80 + Fox.this.getRandom().nextInt(20)); } } class SeekShelterGoal extends FleeSunGoal { private int interval = reducedTickDelay(100); public SeekShelterGoal(final double speedModifier) { super(Fox.this, speedModifier); } @Override public boolean canUse() { if (!Fox.this.isSleeping() && this.mob.getTarget() == null) { if (Fox.this.level().isThundering() && Fox.this.level().canSeeSky(this.mob.blockPosition())) { return this.setWantedPos(); } else if (this.interval > 0) { this.interval--; return false; } else { this.interval = 100; BlockPos blockPos = this.mob.blockPosition(); return Fox.this.level().isBrightOutside() && Fox.this.level().canSeeSky(blockPos) && !((ServerLevel)Fox.this.level()).isVillage(blockPos) && this.setWantedPos(); } } else { return false; } } @Override public void start() { Fox.this.clearStates(); super.start(); } } class SleepGoal extends Fox.FoxBehaviorGoal { private static final int WAIT_TIME_BEFORE_SLEEP = reducedTickDelay(140); private int countdown = Fox.this.random.nextInt(WAIT_TIME_BEFORE_SLEEP); public SleepGoal() { this.setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK, Flag.JUMP)); } @Override public boolean canUse() { return Fox.this.xxa == 0.0F && Fox.this.yya == 0.0F && Fox.this.zza == 0.0F ? this.canSleep() || Fox.this.isSleeping() : false; } @Override public boolean canContinueToUse() { return this.canSleep(); } private boolean canSleep() { if (this.countdown > 0) { this.countdown--; return false; } else { return Fox.this.level().isBrightOutside() && this.hasShelter() && !this.alertable() && !Fox.this.isInPowderSnow; } } @Override public void stop() { this.countdown = Fox.this.random.nextInt(WAIT_TIME_BEFORE_SLEEP); Fox.this.clearStates(); } @Override public void start() { Fox.this.setSitting(false); Fox.this.setIsCrouching(false); Fox.this.setIsInterested(false); Fox.this.setJumping(false); Fox.this.setSleeping(true); Fox.this.getNavigation().stop(); Fox.this.getMoveControl().setWantedPosition(Fox.this.getX(), Fox.this.getY(), Fox.this.getZ(), 0.0); } } class StalkPreyGoal extends Goal { public StalkPreyGoal() { this.setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK)); } @Override public boolean canUse() { if (Fox.this.isSleeping()) { return false; } else { LivingEntity livingEntity = Fox.this.getTarget(); return livingEntity != null && livingEntity.isAlive() && Fox.STALKABLE_PREY.test(livingEntity) && Fox.this.distanceToSqr(livingEntity) > 36.0 && !Fox.this.isCrouching() && !Fox.this.isInterested() && !Fox.this.jumping; } } @Override public void start() { Fox.this.setSitting(false); Fox.this.setFaceplanted(false); } @Override public void stop() { LivingEntity livingEntity = Fox.this.getTarget(); if (livingEntity != null && Fox.isPathClear(Fox.this, livingEntity)) { Fox.this.setIsInterested(true); Fox.this.setIsCrouching(true); Fox.this.getNavigation().stop(); Fox.this.getLookControl().setLookAt(livingEntity, Fox.this.getMaxHeadYRot(), Fox.this.getMaxHeadXRot()); } else { Fox.this.setIsInterested(false); Fox.this.setIsCrouching(false); } } @Override public void tick() { LivingEntity livingEntity = Fox.this.getTarget(); if (livingEntity != null) { Fox.this.getLookControl().setLookAt(livingEntity, Fox.this.getMaxHeadYRot(), Fox.this.getMaxHeadXRot()); if (Fox.this.distanceToSqr(livingEntity) <= 36.0) { Fox.this.setIsInterested(true); Fox.this.setIsCrouching(true); Fox.this.getNavigation().stop(); } else { Fox.this.getNavigation().moveTo(livingEntity, 1.5); } } } } public static enum Variant implements StringRepresentable { RED(0, "red"), SNOW(1, "snow"); public static final Fox.Variant DEFAULT = RED; public static final StringRepresentable.EnumCodec CODEC = StringRepresentable.fromEnum(Fox.Variant::values); private static final IntFunction BY_ID = ByIdMap.continuous(Fox.Variant::getId, values(), OutOfBoundsStrategy.ZERO); public static final StreamCodec STREAM_CODEC = ByteBufCodecs.idMapper(BY_ID, Fox.Variant::getId); private final int id; private final String name; private Variant(final int id, final String name) { this.id = id; this.name = name; } @Override public String getSerializedName() { return this.name; } public int getId() { return this.id; } public static Fox.Variant byId(int id) { return (Fox.Variant)BY_ID.apply(id); } public static Fox.Variant byBiome(Holder biome) { return biome.is(BiomeTags.SPAWNS_SNOW_FOXES) ? SNOW : RED; } } }