package net.minecraft.world.entity.npc; import com.google.common.collect.Lists; import java.util.ArrayList; import net.minecraft.advancements.CriteriaTriggers; import net.minecraft.core.particles.ParticleOptions; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.NbtOps; 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.ServerPlayer; import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundEvents; import net.minecraft.util.Mth; import net.minecraft.world.DifficultyInstance; import net.minecraft.world.SimpleContainer; 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.SlotAccess; import net.minecraft.world.entity.SpawnGroupData; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.trading.Merchant; import net.minecraft.world.item.trading.MerchantOffer; import net.minecraft.world.item.trading.MerchantOffers; import net.minecraft.world.level.Level; import net.minecraft.world.level.ServerLevelAccessor; import net.minecraft.world.level.pathfinder.PathType; import net.minecraft.world.level.portal.TeleportTransition; import net.minecraft.world.phys.Vec3; import org.jetbrains.annotations.Nullable; public abstract class AbstractVillager extends AgeableMob implements InventoryCarrier, Npc, Merchant { private static final EntityDataAccessor DATA_UNHAPPY_COUNTER = SynchedEntityData.defineId(AbstractVillager.class, EntityDataSerializers.INT); public static final int VILLAGER_SLOT_OFFSET = 300; private static final int VILLAGER_INVENTORY_SIZE = 8; @Nullable private Player tradingPlayer; @Nullable protected MerchantOffers offers; private final SimpleContainer inventory = new SimpleContainer(8); public AbstractVillager(EntityType entityType, Level level) { super(entityType, level); this.setPathfindingMalus(PathType.DANGER_FIRE, 16.0F); this.setPathfindingMalus(PathType.DAMAGE_FIRE, -1.0F); } @Override public SpawnGroupData finalizeSpawn( ServerLevelAccessor level, DifficultyInstance difficulty, EntitySpawnReason spawnReason, @Nullable SpawnGroupData spawnGroupData ) { if (spawnGroupData == null) { spawnGroupData = new AgeableMob.AgeableMobGroupData(false); } return super.finalizeSpawn(level, difficulty, spawnReason, spawnGroupData); } public int getUnhappyCounter() { return this.entityData.get(DATA_UNHAPPY_COUNTER); } public void setUnhappyCounter(int unhappyCounter) { this.entityData.set(DATA_UNHAPPY_COUNTER, unhappyCounter); } @Override public int getVillagerXp() { return 0; } @Override protected void defineSynchedData(Builder builder) { super.defineSynchedData(builder); builder.define(DATA_UNHAPPY_COUNTER, 0); } @Override public void setTradingPlayer(@Nullable Player tradingPlayer) { this.tradingPlayer = tradingPlayer; } @Nullable @Override public Player getTradingPlayer() { return this.tradingPlayer; } public boolean isTrading() { return this.tradingPlayer != null; } @Override public MerchantOffers getOffers() { if (this.level().isClientSide) { throw new IllegalStateException("Cannot load Villager offers on the client"); } else { if (this.offers == null) { this.offers = new MerchantOffers(); this.updateTrades(); } return this.offers; } } @Override public void overrideOffers(@Nullable MerchantOffers offers) { } @Override public void overrideXp(int xp) { } @Override public void notifyTrade(MerchantOffer offer) { offer.increaseUses(); this.ambientSoundTime = -this.getAmbientSoundInterval(); this.rewardTradeXp(offer); if (this.tradingPlayer instanceof ServerPlayer) { CriteriaTriggers.TRADE.trigger((ServerPlayer)this.tradingPlayer, this, offer.getResult()); } } protected abstract void rewardTradeXp(MerchantOffer offer); @Override public boolean showProgressBar() { return true; } @Override public void notifyTradeUpdated(ItemStack stack) { if (!this.level().isClientSide && this.ambientSoundTime > -this.getAmbientSoundInterval() + 20) { this.ambientSoundTime = -this.getAmbientSoundInterval(); this.makeSound(this.getTradeUpdatedSound(!stack.isEmpty())); } } @Override public SoundEvent getNotifyTradeSound() { return SoundEvents.VILLAGER_YES; } protected SoundEvent getTradeUpdatedSound(boolean isYesSound) { return isYesSound ? SoundEvents.VILLAGER_YES : SoundEvents.VILLAGER_NO; } public void playCelebrateSound() { this.makeSound(SoundEvents.VILLAGER_CELEBRATE); } @Override public void addAdditionalSaveData(CompoundTag tag) { super.addAdditionalSaveData(tag); if (!this.level().isClientSide) { MerchantOffers merchantOffers = this.getOffers(); if (!merchantOffers.isEmpty()) { tag.store("Offers", MerchantOffers.CODEC, this.registryAccess().createSerializationContext(NbtOps.INSTANCE), merchantOffers); } } this.writeInventoryToTag(tag, this.registryAccess()); } @Override public void readAdditionalSaveData(CompoundTag tag) { super.readAdditionalSaveData(tag); this.offers = (MerchantOffers)tag.read("Offers", MerchantOffers.CODEC, this.registryAccess().createSerializationContext(NbtOps.INSTANCE)).orElse(null); this.readInventoryFromTag(tag, this.registryAccess()); } @Nullable @Override public Entity teleport(TeleportTransition teleportTransition) { this.stopTrading(); return super.teleport(teleportTransition); } protected void stopTrading() { this.setTradingPlayer(null); } @Override public void die(DamageSource damageSource) { super.die(damageSource); this.stopTrading(); } protected void addParticlesAroundSelf(ParticleOptions particleOption) { for (int i = 0; i < 5; i++) { double d = this.random.nextGaussian() * 0.02; double e = this.random.nextGaussian() * 0.02; double f = this.random.nextGaussian() * 0.02; this.level().addParticle(particleOption, this.getRandomX(1.0), this.getRandomY() + 1.0, this.getRandomZ(1.0), d, e, f); } } @Override public boolean canBeLeashed() { return false; } @Override public SimpleContainer getInventory() { return this.inventory; } @Override public SlotAccess getSlot(int slot) { int i = slot - 300; return i >= 0 && i < this.inventory.getContainerSize() ? SlotAccess.forContainer(this.inventory, i) : super.getSlot(slot); } protected abstract void updateTrades(); /** * Adds limited numbers of trades to the given {@link net.minecraft.world.item.trading.MerchantOffers}. */ protected void addOffersFromItemListings(MerchantOffers givenMerchantOffers, VillagerTrades.ItemListing[] newTrades, int maxNumbers) { ArrayList arrayList = Lists.newArrayList(newTrades); int i = 0; while (i < maxNumbers && !arrayList.isEmpty()) { MerchantOffer merchantOffer = ((VillagerTrades.ItemListing)arrayList.remove(this.random.nextInt(arrayList.size()))).getOffer(this, this.random); if (merchantOffer != null) { givenMerchantOffers.add(merchantOffer); i++; } } } @Override public Vec3 getRopeHoldPosition(float partialTicks) { float f = Mth.lerp(partialTicks, this.yBodyRotO, this.yBodyRot) * (float) (Math.PI / 180.0); Vec3 vec3 = new Vec3(0.0, this.getBoundingBox().getYsize() - 1.0, 0.2); return this.getPosition(partialTicks).add(vec3.yRot(-f)); } @Override public boolean isClientSide() { return this.level().isClientSide; } @Override public boolean stillValid(Player player) { return this.getTradingPlayer() == player && this.isAlive() && player.canInteractWithEntity(this, 4.0); } }