package net.minecraft.world.entity.npc; import java.util.EnumSet; import net.minecraft.core.BlockPos; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.NbtUtils; import net.minecraft.server.level.ServerLevel; import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundEvents; import net.minecraft.stats.Stats; 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.EntityType; import net.minecraft.world.entity.ExperienceOrb; import net.minecraft.world.entity.Mob; import net.minecraft.world.entity.ai.goal.AvoidEntityGoal; import net.minecraft.world.entity.ai.goal.FloatGoal; import net.minecraft.world.entity.ai.goal.Goal; import net.minecraft.world.entity.ai.goal.InteractGoal; import net.minecraft.world.entity.ai.goal.LookAtPlayerGoal; import net.minecraft.world.entity.ai.goal.LookAtTradingPlayerGoal; import net.minecraft.world.entity.ai.goal.MoveTowardsRestrictionGoal; import net.minecraft.world.entity.ai.goal.PanicGoal; import net.minecraft.world.entity.ai.goal.TradeWithPlayerGoal; import net.minecraft.world.entity.ai.goal.UseItemGoal; import net.minecraft.world.entity.ai.goal.WaterAvoidingRandomStrollGoal; import net.minecraft.world.entity.ai.goal.Goal.Flag; import net.minecraft.world.entity.monster.Evoker; import net.minecraft.world.entity.monster.Illusioner; import net.minecraft.world.entity.monster.Pillager; import net.minecraft.world.entity.monster.Vex; import net.minecraft.world.entity.monster.Vindicator; import net.minecraft.world.entity.monster.Zoglin; import net.minecraft.world.entity.monster.Zombie; import net.minecraft.world.entity.npc.VillagerTrades.ItemListing; import net.minecraft.world.entity.player.Player; import net.minecraft.world.flag.FeatureFlags; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.item.alchemy.PotionContents; import net.minecraft.world.item.alchemy.Potions; import net.minecraft.world.item.component.Consumable; import net.minecraft.world.item.trading.MerchantOffer; import net.minecraft.world.item.trading.MerchantOffers; import net.minecraft.world.level.Level; import net.minecraft.world.phys.Vec3; import org.apache.commons.lang3.tuple.Pair; import org.jetbrains.annotations.Nullable; public class WanderingTrader extends AbstractVillager implements Consumable.OverrideConsumeSound { private static final int NUMBER_OF_TRADE_OFFERS = 5; @Nullable private BlockPos wanderTarget; private int despawnDelay; public WanderingTrader(EntityType entityType, Level level) { super(entityType, level); } @Override protected void registerGoals() { this.goalSelector.addGoal(0, new FloatGoal(this)); this.goalSelector .addGoal( 0, new UseItemGoal<>( this, PotionContents.createItemStack(Items.POTION, Potions.INVISIBILITY), SoundEvents.WANDERING_TRADER_DISAPPEARED, wanderingTrader -> this.level().isNight() && !wanderingTrader.isInvisible() ) ); this.goalSelector .addGoal( 0, new UseItemGoal<>( this, new ItemStack(Items.MILK_BUCKET), SoundEvents.WANDERING_TRADER_REAPPEARED, wanderingTrader -> this.level().isDay() && wanderingTrader.isInvisible() ) ); this.goalSelector.addGoal(1, new TradeWithPlayerGoal(this)); this.goalSelector.addGoal(1, new AvoidEntityGoal(this, Zombie.class, 8.0F, 0.5, 0.5)); this.goalSelector.addGoal(1, new AvoidEntityGoal(this, Evoker.class, 12.0F, 0.5, 0.5)); this.goalSelector.addGoal(1, new AvoidEntityGoal(this, Vindicator.class, 8.0F, 0.5, 0.5)); this.goalSelector.addGoal(1, new AvoidEntityGoal(this, Vex.class, 8.0F, 0.5, 0.5)); this.goalSelector.addGoal(1, new AvoidEntityGoal(this, Pillager.class, 15.0F, 0.5, 0.5)); this.goalSelector.addGoal(1, new AvoidEntityGoal(this, Illusioner.class, 12.0F, 0.5, 0.5)); this.goalSelector.addGoal(1, new AvoidEntityGoal(this, Zoglin.class, 10.0F, 0.5, 0.5)); this.goalSelector.addGoal(1, new PanicGoal(this, 0.5)); this.goalSelector.addGoal(1, new LookAtTradingPlayerGoal(this)); this.goalSelector.addGoal(2, new WanderingTrader.WanderToPositionGoal(this, 2.0, 0.35)); this.goalSelector.addGoal(4, new MoveTowardsRestrictionGoal(this, 0.35)); this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 0.35)); this.goalSelector.addGoal(9, new InteractGoal(this, Player.class, 3.0F, 1.0F)); this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Mob.class, 8.0F)); } @Nullable @Override public AgeableMob getBreedOffspring(ServerLevel level, AgeableMob otherParent) { return null; } @Override public boolean showProgressBar() { return false; } @Override public InteractionResult mobInteract(Player player, InteractionHand hand) { ItemStack itemStack = player.getItemInHand(hand); if (!itemStack.is(Items.VILLAGER_SPAWN_EGG) && this.isAlive() && !this.isTrading() && !this.isBaby()) { if (hand == InteractionHand.MAIN_HAND) { player.awardStat(Stats.TALKED_TO_VILLAGER); } if (!this.level().isClientSide) { if (this.getOffers().isEmpty()) { return InteractionResult.CONSUME; } this.setTradingPlayer(player); this.openTradingScreen(player, this.getDisplayName(), 1); } return InteractionResult.SUCCESS; } else { return super.mobInteract(player, hand); } } @Override protected void updateTrades() { if (this.level().enabledFeatures().contains(FeatureFlags.TRADE_REBALANCE)) { this.experimentalUpdateTrades(); } else { ItemListing[] itemListings = VillagerTrades.WANDERING_TRADER_TRADES.get(1); ItemListing[] itemListings2 = VillagerTrades.WANDERING_TRADER_TRADES.get(2); if (itemListings != null && itemListings2 != null) { MerchantOffers merchantOffers = this.getOffers(); this.addOffersFromItemListings(merchantOffers, itemListings, 5); int i = this.random.nextInt(itemListings2.length); ItemListing itemListing = itemListings2[i]; MerchantOffer merchantOffer = itemListing.getOffer(this, this.random); if (merchantOffer != null) { merchantOffers.add(merchantOffer); } } } } private void experimentalUpdateTrades() { MerchantOffers merchantOffers = this.getOffers(); for (Pair pair : VillagerTrades.EXPERIMENTAL_WANDERING_TRADER_TRADES) { ItemListing[] itemListings = pair.getLeft(); this.addOffersFromItemListings(merchantOffers, itemListings, pair.getRight()); } } @Override public void addAdditionalSaveData(CompoundTag tag) { super.addAdditionalSaveData(tag); tag.putInt("DespawnDelay", this.despawnDelay); if (this.wanderTarget != null) { tag.put("wander_target", NbtUtils.writeBlockPos(this.wanderTarget)); } } @Override public void readAdditionalSaveData(CompoundTag tag) { super.readAdditionalSaveData(tag); if (tag.contains("DespawnDelay", 99)) { this.despawnDelay = tag.getInt("DespawnDelay"); } NbtUtils.readBlockPos(tag, "wander_target").ifPresent(blockPos -> this.wanderTarget = blockPos); this.setAge(Math.max(0, this.getAge())); } @Override public boolean removeWhenFarAway(double distanceToClosestPlayer) { return false; } @Override protected void rewardTradeXp(MerchantOffer offer) { if (offer.shouldRewardExp()) { int i = 3 + this.random.nextInt(4); this.level().addFreshEntity(new ExperienceOrb(this.level(), this.getX(), this.getY() + 0.5, this.getZ(), i)); } } @Override protected SoundEvent getAmbientSound() { return this.isTrading() ? SoundEvents.WANDERING_TRADER_TRADE : SoundEvents.WANDERING_TRADER_AMBIENT; } @Override protected SoundEvent getHurtSound(DamageSource damageSource) { return SoundEvents.WANDERING_TRADER_HURT; } @Override protected SoundEvent getDeathSound() { return SoundEvents.WANDERING_TRADER_DEATH; } @Override public SoundEvent getConsumeSound(ItemStack stack) { return stack.is(Items.MILK_BUCKET) ? SoundEvents.WANDERING_TRADER_DRINK_MILK : SoundEvents.WANDERING_TRADER_DRINK_POTION; } @Override protected SoundEvent getTradeUpdatedSound(boolean isYesSound) { return isYesSound ? SoundEvents.WANDERING_TRADER_YES : SoundEvents.WANDERING_TRADER_NO; } @Override public SoundEvent getNotifyTradeSound() { return SoundEvents.WANDERING_TRADER_YES; } public void setDespawnDelay(int despawnDelay) { this.despawnDelay = despawnDelay; } public int getDespawnDelay() { return this.despawnDelay; } @Override public void aiStep() { super.aiStep(); if (!this.level().isClientSide) { this.maybeDespawn(); } } private void maybeDespawn() { if (this.despawnDelay > 0 && !this.isTrading() && --this.despawnDelay == 0) { this.discard(); } } public void setWanderTarget(@Nullable BlockPos wanderTarget) { this.wanderTarget = wanderTarget; } @Nullable BlockPos getWanderTarget() { return this.wanderTarget; } class WanderToPositionGoal extends Goal { final WanderingTrader trader; final double stopDistance; final double speedModifier; WanderToPositionGoal(final WanderingTrader trader, final double stopDistance, final double speedModifier) { this.trader = trader; this.stopDistance = stopDistance; this.speedModifier = speedModifier; this.setFlags(EnumSet.of(Flag.MOVE)); } @Override public void stop() { this.trader.setWanderTarget(null); WanderingTrader.this.navigation.stop(); } @Override public boolean canUse() { BlockPos blockPos = this.trader.getWanderTarget(); return blockPos != null && this.isTooFarAway(blockPos, this.stopDistance); } @Override public void tick() { BlockPos blockPos = this.trader.getWanderTarget(); if (blockPos != null && WanderingTrader.this.navigation.isDone()) { if (this.isTooFarAway(blockPos, 10.0)) { Vec3 vec3 = new Vec3(blockPos.getX() - this.trader.getX(), blockPos.getY() - this.trader.getY(), blockPos.getZ() - this.trader.getZ()).normalize(); Vec3 vec32 = vec3.scale(10.0).add(this.trader.getX(), this.trader.getY(), this.trader.getZ()); WanderingTrader.this.navigation.moveTo(vec32.x, vec32.y, vec32.z, this.speedModifier); } else { WanderingTrader.this.navigation.moveTo(blockPos.getX(), blockPos.getY(), blockPos.getZ(), this.speedModifier); } } } private boolean isTooFarAway(BlockPos pos, double distance) { return !pos.closerToCenterThan(this.trader.position(), distance); } } }