526 lines
16 KiB
Java
526 lines
16 KiB
Java
package net.minecraft.world.entity.animal.horse;
|
|
|
|
import com.mojang.serialization.Codec;
|
|
import io.netty.buffer.ByteBuf;
|
|
import java.util.function.IntFunction;
|
|
import net.minecraft.Util;
|
|
import net.minecraft.core.BlockPos;
|
|
import net.minecraft.core.component.DataComponentGetter;
|
|
import net.minecraft.core.component.DataComponentType;
|
|
import net.minecraft.core.component.DataComponents;
|
|
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.server.level.ServerLevel;
|
|
import net.minecraft.sounds.SoundEvent;
|
|
import net.minecraft.sounds.SoundEvents;
|
|
import net.minecraft.tags.ItemTags;
|
|
import net.minecraft.util.ByIdMap;
|
|
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.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.EquipmentSlot;
|
|
import net.minecraft.world.entity.LivingEntity;
|
|
import net.minecraft.world.entity.Pose;
|
|
import net.minecraft.world.entity.SpawnGroupData;
|
|
import net.minecraft.world.entity.ai.attributes.AttributeSupplier.Builder;
|
|
import net.minecraft.world.entity.ai.goal.BreedGoal;
|
|
import net.minecraft.world.entity.ai.goal.FloatGoal;
|
|
import net.minecraft.world.entity.ai.goal.FollowParentGoal;
|
|
import net.minecraft.world.entity.ai.goal.LlamaFollowCaravanGoal;
|
|
import net.minecraft.world.entity.ai.goal.LookAtPlayerGoal;
|
|
import net.minecraft.world.entity.ai.goal.PanicGoal;
|
|
import net.minecraft.world.entity.ai.goal.RandomLookAroundGoal;
|
|
import net.minecraft.world.entity.ai.goal.RangedAttackGoal;
|
|
import net.minecraft.world.entity.ai.goal.RunAroundLikeCrazyGoal;
|
|
import net.minecraft.world.entity.ai.goal.TemptGoal;
|
|
import net.minecraft.world.entity.ai.goal.WaterAvoidingRandomStrollGoal;
|
|
import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal;
|
|
import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal;
|
|
import net.minecraft.world.entity.animal.Animal;
|
|
import net.minecraft.world.entity.animal.wolf.Wolf;
|
|
import net.minecraft.world.entity.monster.RangedAttackMob;
|
|
import net.minecraft.world.entity.player.Player;
|
|
import net.minecraft.world.entity.projectile.LlamaSpit;
|
|
import net.minecraft.world.entity.projectile.Projectile;
|
|
import net.minecraft.world.item.ItemStack;
|
|
import net.minecraft.world.item.Items;
|
|
import net.minecraft.world.level.Level;
|
|
import net.minecraft.world.level.ServerLevelAccessor;
|
|
import net.minecraft.world.level.block.Blocks;
|
|
import net.minecraft.world.level.block.state.BlockState;
|
|
import net.minecraft.world.phys.Vec3;
|
|
import org.jetbrains.annotations.Nullable;
|
|
|
|
public class Llama extends AbstractChestedHorse implements RangedAttackMob {
|
|
private static final int MAX_STRENGTH = 5;
|
|
private static final EntityDataAccessor<Integer> DATA_STRENGTH_ID = SynchedEntityData.defineId(Llama.class, EntityDataSerializers.INT);
|
|
private static final EntityDataAccessor<Integer> DATA_VARIANT_ID = SynchedEntityData.defineId(Llama.class, EntityDataSerializers.INT);
|
|
private static final EntityDimensions BABY_DIMENSIONS = EntityType.LLAMA
|
|
.getDimensions()
|
|
.withAttachments(EntityAttachments.builder().attach(EntityAttachment.PASSENGER, 0.0F, EntityType.LLAMA.getHeight() - 0.8125F, -0.3F))
|
|
.scale(0.5F);
|
|
boolean didSpit;
|
|
@Nullable
|
|
private Llama caravanHead;
|
|
@Nullable
|
|
private Llama caravanTail;
|
|
|
|
public Llama(EntityType<? extends Llama> entityType, Level level) {
|
|
super(entityType, level);
|
|
this.getNavigation().setRequiredPathLength(40.0F);
|
|
}
|
|
|
|
public boolean isTraderLlama() {
|
|
return false;
|
|
}
|
|
|
|
private void setStrength(int strength) {
|
|
this.entityData.set(DATA_STRENGTH_ID, Math.max(1, Math.min(5, strength)));
|
|
}
|
|
|
|
private void setRandomStrength(RandomSource random) {
|
|
int i = random.nextFloat() < 0.04F ? 5 : 3;
|
|
this.setStrength(1 + random.nextInt(i));
|
|
}
|
|
|
|
public int getStrength() {
|
|
return this.entityData.get(DATA_STRENGTH_ID);
|
|
}
|
|
|
|
@Override
|
|
public void addAdditionalSaveData(CompoundTag tag) {
|
|
super.addAdditionalSaveData(tag);
|
|
tag.store("Variant", Llama.Variant.LEGACY_CODEC, this.getVariant());
|
|
tag.putInt("Strength", this.getStrength());
|
|
}
|
|
|
|
@Override
|
|
public void readAdditionalSaveData(CompoundTag tag) {
|
|
this.setStrength(tag.getIntOr("Strength", 0));
|
|
super.readAdditionalSaveData(tag);
|
|
this.setVariant((Llama.Variant)tag.read("Variant", Llama.Variant.LEGACY_CODEC).orElse(Llama.Variant.DEFAULT));
|
|
}
|
|
|
|
@Override
|
|
protected void registerGoals() {
|
|
this.goalSelector.addGoal(0, new FloatGoal(this));
|
|
this.goalSelector.addGoal(1, new RunAroundLikeCrazyGoal(this, 1.2));
|
|
this.goalSelector.addGoal(2, new LlamaFollowCaravanGoal(this, 2.1F));
|
|
this.goalSelector.addGoal(3, new RangedAttackGoal(this, 1.25, 40, 20.0F));
|
|
this.goalSelector.addGoal(3, new PanicGoal(this, 1.2));
|
|
this.goalSelector.addGoal(4, new BreedGoal(this, 1.0));
|
|
this.goalSelector.addGoal(5, new TemptGoal(this, 1.25, itemStack -> itemStack.is(ItemTags.LLAMA_TEMPT_ITEMS), false));
|
|
this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.0));
|
|
this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 0.7));
|
|
this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 6.0F));
|
|
this.goalSelector.addGoal(9, new RandomLookAroundGoal(this));
|
|
this.targetSelector.addGoal(1, new Llama.LlamaHurtByTargetGoal(this));
|
|
this.targetSelector.addGoal(2, new Llama.LlamaAttackWolfGoal(this));
|
|
}
|
|
|
|
public static Builder createAttributes() {
|
|
return createBaseChestedHorseAttributes();
|
|
}
|
|
|
|
@Override
|
|
protected void defineSynchedData(net.minecraft.network.syncher.SynchedEntityData.Builder builder) {
|
|
super.defineSynchedData(builder);
|
|
builder.define(DATA_STRENGTH_ID, 0);
|
|
builder.define(DATA_VARIANT_ID, 0);
|
|
}
|
|
|
|
public Llama.Variant getVariant() {
|
|
return Llama.Variant.byId(this.entityData.get(DATA_VARIANT_ID));
|
|
}
|
|
|
|
private void setVariant(Llama.Variant variant) {
|
|
this.entityData.set(DATA_VARIANT_ID, variant.id);
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public <T> T get(DataComponentType<? extends T> component) {
|
|
return component == DataComponents.LLAMA_VARIANT ? castComponentValue((DataComponentType<T>)component, this.getVariant()) : super.get(component);
|
|
}
|
|
|
|
@Override
|
|
protected void applyImplicitComponents(DataComponentGetter componentGetter) {
|
|
this.applyImplicitComponentIfPresent(componentGetter, DataComponents.LLAMA_VARIANT);
|
|
super.applyImplicitComponents(componentGetter);
|
|
}
|
|
|
|
@Override
|
|
protected <T> boolean applyImplicitComponent(DataComponentType<T> component, T value) {
|
|
if (component == DataComponents.LLAMA_VARIANT) {
|
|
this.setVariant(castComponentValue(DataComponents.LLAMA_VARIANT, value));
|
|
return true;
|
|
} else {
|
|
return super.applyImplicitComponent(component, value);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean isFood(ItemStack stack) {
|
|
return stack.is(ItemTags.LLAMA_FOOD);
|
|
}
|
|
|
|
@Override
|
|
protected boolean handleEating(Player player, ItemStack stack) {
|
|
int i = 0;
|
|
int j = 0;
|
|
float f = 0.0F;
|
|
boolean bl = false;
|
|
if (stack.is(Items.WHEAT)) {
|
|
i = 10;
|
|
j = 3;
|
|
f = 2.0F;
|
|
} else if (stack.is(Blocks.HAY_BLOCK.asItem())) {
|
|
i = 90;
|
|
j = 6;
|
|
f = 10.0F;
|
|
if (this.isTamed() && this.getAge() == 0 && this.canFallInLove()) {
|
|
bl = true;
|
|
this.setInLove(player);
|
|
}
|
|
}
|
|
|
|
if (this.getHealth() < this.getMaxHealth() && f > 0.0F) {
|
|
this.heal(f);
|
|
bl = true;
|
|
}
|
|
|
|
if (this.isBaby() && i > 0) {
|
|
this.level().addParticle(ParticleTypes.HAPPY_VILLAGER, this.getRandomX(1.0), this.getRandomY() + 0.5, this.getRandomZ(1.0), 0.0, 0.0, 0.0);
|
|
if (!this.level().isClientSide) {
|
|
this.ageUp(i);
|
|
bl = true;
|
|
}
|
|
}
|
|
|
|
if (j > 0 && (bl || !this.isTamed()) && this.getTemper() < this.getMaxTemper() && !this.level().isClientSide) {
|
|
this.modifyTemper(j);
|
|
bl = true;
|
|
}
|
|
|
|
if (bl && !this.isSilent()) {
|
|
SoundEvent soundEvent = this.getEatingSound();
|
|
if (soundEvent != null) {
|
|
this.level()
|
|
.playSound(
|
|
null,
|
|
this.getX(),
|
|
this.getY(),
|
|
this.getZ(),
|
|
this.getEatingSound(),
|
|
this.getSoundSource(),
|
|
1.0F,
|
|
1.0F + (this.random.nextFloat() - this.random.nextFloat()) * 0.2F
|
|
);
|
|
}
|
|
}
|
|
|
|
return bl;
|
|
}
|
|
|
|
@Override
|
|
public boolean isImmobile() {
|
|
return this.isDeadOrDying() || this.isEating();
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public SpawnGroupData finalizeSpawn(
|
|
ServerLevelAccessor level, DifficultyInstance difficulty, EntitySpawnReason spawnReason, @Nullable SpawnGroupData spawnGroupData
|
|
) {
|
|
RandomSource randomSource = level.getRandom();
|
|
this.setRandomStrength(randomSource);
|
|
Llama.Variant variant;
|
|
if (spawnGroupData instanceof Llama.LlamaGroupData) {
|
|
variant = ((Llama.LlamaGroupData)spawnGroupData).variant;
|
|
} else {
|
|
variant = Util.getRandom(Llama.Variant.values(), randomSource);
|
|
spawnGroupData = new Llama.LlamaGroupData(variant);
|
|
}
|
|
|
|
this.setVariant(variant);
|
|
return super.finalizeSpawn(level, difficulty, spawnReason, spawnGroupData);
|
|
}
|
|
|
|
@Override
|
|
protected boolean canPerformRearing() {
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
protected SoundEvent getAngrySound() {
|
|
return SoundEvents.LLAMA_ANGRY;
|
|
}
|
|
|
|
@Override
|
|
protected SoundEvent getAmbientSound() {
|
|
return SoundEvents.LLAMA_AMBIENT;
|
|
}
|
|
|
|
@Override
|
|
protected SoundEvent getHurtSound(DamageSource damageSource) {
|
|
return SoundEvents.LLAMA_HURT;
|
|
}
|
|
|
|
@Override
|
|
protected SoundEvent getDeathSound() {
|
|
return SoundEvents.LLAMA_DEATH;
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
protected SoundEvent getEatingSound() {
|
|
return SoundEvents.LLAMA_EAT;
|
|
}
|
|
|
|
@Override
|
|
protected void playStepSound(BlockPos pos, BlockState state) {
|
|
this.playSound(SoundEvents.LLAMA_STEP, 0.15F, 1.0F);
|
|
}
|
|
|
|
@Override
|
|
protected void playChestEquipsSound() {
|
|
this.playSound(SoundEvents.LLAMA_CHEST, 1.0F, (this.random.nextFloat() - this.random.nextFloat()) * 0.2F + 1.0F);
|
|
}
|
|
|
|
@Override
|
|
public int getInventoryColumns() {
|
|
return this.hasChest() ? this.getStrength() : 0;
|
|
}
|
|
|
|
@Override
|
|
public boolean canUseSlot(EquipmentSlot slot) {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public int getMaxTemper() {
|
|
return 30;
|
|
}
|
|
|
|
@Override
|
|
public boolean canMate(Animal otherAnimal) {
|
|
return otherAnimal != this && otherAnimal instanceof Llama && this.canParent() && ((Llama)otherAnimal).canParent();
|
|
}
|
|
|
|
@Nullable
|
|
public Llama getBreedOffspring(ServerLevel serverLevel, AgeableMob ageableMob) {
|
|
Llama llama = this.makeNewLlama();
|
|
if (llama != null) {
|
|
this.setOffspringAttributes(ageableMob, llama);
|
|
Llama llama2 = (Llama)ageableMob;
|
|
int i = this.random.nextInt(Math.max(this.getStrength(), llama2.getStrength())) + 1;
|
|
if (this.random.nextFloat() < 0.03F) {
|
|
i++;
|
|
}
|
|
|
|
llama.setStrength(i);
|
|
llama.setVariant(this.random.nextBoolean() ? this.getVariant() : llama2.getVariant());
|
|
}
|
|
|
|
return llama;
|
|
}
|
|
|
|
@Nullable
|
|
protected Llama makeNewLlama() {
|
|
return EntityType.LLAMA.create(this.level(), EntitySpawnReason.BREEDING);
|
|
}
|
|
|
|
private void spit(LivingEntity target) {
|
|
LlamaSpit llamaSpit = new LlamaSpit(this.level(), this);
|
|
double d = target.getX() - this.getX();
|
|
double e = target.getY(0.3333333333333333) - llamaSpit.getY();
|
|
double f = target.getZ() - this.getZ();
|
|
double g = Math.sqrt(d * d + f * f) * 0.2F;
|
|
if (this.level() instanceof ServerLevel serverLevel) {
|
|
Projectile.spawnProjectileUsingShoot(llamaSpit, serverLevel, ItemStack.EMPTY, d, e + g, f, 1.5F, 10.0F);
|
|
}
|
|
|
|
if (!this.isSilent()) {
|
|
this.level()
|
|
.playSound(
|
|
null,
|
|
this.getX(),
|
|
this.getY(),
|
|
this.getZ(),
|
|
SoundEvents.LLAMA_SPIT,
|
|
this.getSoundSource(),
|
|
1.0F,
|
|
1.0F + (this.random.nextFloat() - this.random.nextFloat()) * 0.2F
|
|
);
|
|
}
|
|
|
|
this.didSpit = true;
|
|
}
|
|
|
|
void setDidSpit(boolean didSpit) {
|
|
this.didSpit = didSpit;
|
|
}
|
|
|
|
@Override
|
|
public boolean causeFallDamage(double fallDistance, float damageMultiplier, DamageSource damageSource) {
|
|
int i = this.calculateFallDamage(fallDistance, damageMultiplier);
|
|
if (i <= 0) {
|
|
return false;
|
|
} else {
|
|
if (fallDistance >= 6.0) {
|
|
this.hurt(damageSource, i);
|
|
this.propagateFallToPassengers(fallDistance, damageMultiplier, damageSource);
|
|
}
|
|
|
|
this.playBlockFallSound();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public void leaveCaravan() {
|
|
if (this.caravanHead != null) {
|
|
this.caravanHead.caravanTail = null;
|
|
}
|
|
|
|
this.caravanHead = null;
|
|
}
|
|
|
|
public void joinCaravan(Llama caravanHead) {
|
|
this.caravanHead = caravanHead;
|
|
this.caravanHead.caravanTail = this;
|
|
}
|
|
|
|
public boolean hasCaravanTail() {
|
|
return this.caravanTail != null;
|
|
}
|
|
|
|
public boolean inCaravan() {
|
|
return this.caravanHead != null;
|
|
}
|
|
|
|
@Nullable
|
|
public Llama getCaravanHead() {
|
|
return this.caravanHead;
|
|
}
|
|
|
|
@Override
|
|
protected double followLeashSpeed() {
|
|
return 2.0;
|
|
}
|
|
|
|
@Override
|
|
protected void followMommy(ServerLevel level) {
|
|
if (!this.inCaravan() && this.isBaby()) {
|
|
super.followMommy(level);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean canEatGrass() {
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public void performRangedAttack(LivingEntity target, float velocity) {
|
|
this.spit(target);
|
|
}
|
|
|
|
@Override
|
|
public Vec3 getLeashOffset() {
|
|
return new Vec3(0.0, 0.75 * this.getEyeHeight(), this.getBbWidth() * 0.5);
|
|
}
|
|
|
|
@Override
|
|
public EntityDimensions getDefaultDimensions(Pose pose) {
|
|
return this.isBaby() ? BABY_DIMENSIONS : super.getDefaultDimensions(pose);
|
|
}
|
|
|
|
@Override
|
|
protected Vec3 getPassengerAttachmentPoint(Entity entity, EntityDimensions dimensions, float partialTick) {
|
|
return getDefaultPassengerAttachmentPoint(this, entity, dimensions.attachments());
|
|
}
|
|
|
|
static class LlamaAttackWolfGoal extends NearestAttackableTargetGoal<Wolf> {
|
|
public LlamaAttackWolfGoal(Llama llama) {
|
|
super(llama, Wolf.class, 16, false, true, (livingEntity, serverLevel) -> !((Wolf)livingEntity).isTame());
|
|
}
|
|
|
|
@Override
|
|
protected double getFollowDistance() {
|
|
return super.getFollowDistance() * 0.25;
|
|
}
|
|
}
|
|
|
|
static class LlamaGroupData extends AgeableMob.AgeableMobGroupData {
|
|
public final Llama.Variant variant;
|
|
|
|
LlamaGroupData(Llama.Variant variant) {
|
|
super(true);
|
|
this.variant = variant;
|
|
}
|
|
}
|
|
|
|
static class LlamaHurtByTargetGoal extends HurtByTargetGoal {
|
|
public LlamaHurtByTargetGoal(Llama llama) {
|
|
super(llama);
|
|
}
|
|
|
|
@Override
|
|
public boolean canContinueToUse() {
|
|
if (this.mob instanceof Llama llama && llama.didSpit) {
|
|
llama.setDidSpit(false);
|
|
return false;
|
|
} else {
|
|
return super.canContinueToUse();
|
|
}
|
|
}
|
|
}
|
|
|
|
public static enum Variant implements StringRepresentable {
|
|
CREAMY(0, "creamy"),
|
|
WHITE(1, "white"),
|
|
BROWN(2, "brown"),
|
|
GRAY(3, "gray");
|
|
|
|
public static final Llama.Variant DEFAULT = CREAMY;
|
|
private static final IntFunction<Llama.Variant> BY_ID = ByIdMap.continuous(Llama.Variant::getId, values(), OutOfBoundsStrategy.CLAMP);
|
|
public static final Codec<Llama.Variant> CODEC = StringRepresentable.fromEnum(Llama.Variant::values);
|
|
@Deprecated
|
|
public static final Codec<Llama.Variant> LEGACY_CODEC = Codec.INT.xmap(BY_ID::apply, Llama.Variant::getId);
|
|
public static final StreamCodec<ByteBuf, Llama.Variant> STREAM_CODEC = ByteBufCodecs.idMapper(BY_ID, Llama.Variant::getId);
|
|
final int id;
|
|
private final String name;
|
|
|
|
private Variant(final int id, final String name) {
|
|
this.id = id;
|
|
this.name = name;
|
|
}
|
|
|
|
public int getId() {
|
|
return this.id;
|
|
}
|
|
|
|
public static Llama.Variant byId(int id) {
|
|
return (Llama.Variant)BY_ID.apply(id);
|
|
}
|
|
|
|
@Override
|
|
public String getSerializedName() {
|
|
return this.name;
|
|
}
|
|
}
|
|
}
|