package net.minecraft.world.entity.npc; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.mojang.datafixers.util.Pair; import com.mojang.logging.LogUtils; import com.mojang.serialization.Dynamic; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.BiPredicate; import net.minecraft.core.BlockPos; import net.minecraft.core.GlobalPos; import net.minecraft.core.Holder; import net.minecraft.core.particles.ParticleTypes; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.NbtOps; import net.minecraft.nbt.Tag; import net.minecraft.network.chat.Component; 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.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundEvents; import net.minecraft.stats.Stats; import net.minecraft.tags.ItemTags; import net.minecraft.util.Mth; import net.minecraft.util.SpawnUtil; import net.minecraft.util.profiling.Profiler; import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.world.Difficulty; import net.minecraft.world.DifficultyInstance; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.SimpleContainer; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.effect.MobEffectInstance; import net.minecraft.world.effect.MobEffects; import net.minecraft.world.entity.AgeableMob; import net.minecraft.world.entity.ConversionParams; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntitySpawnReason; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.ExperienceOrb; import net.minecraft.world.entity.LightningBolt; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.Mob; import net.minecraft.world.entity.ReputationEventHandler; import net.minecraft.world.entity.SpawnGroupData; 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.behavior.VillagerGoalPackages; import net.minecraft.world.entity.ai.gossip.GossipContainer; import net.minecraft.world.entity.ai.gossip.GossipType; import net.minecraft.world.entity.ai.memory.MemoryModuleType; import net.minecraft.world.entity.ai.memory.MemoryStatus; import net.minecraft.world.entity.ai.memory.NearestVisibleLivingEntities; import net.minecraft.world.entity.ai.navigation.GroundPathNavigation; import net.minecraft.world.entity.ai.sensing.GolemSensor; import net.minecraft.world.entity.ai.sensing.Sensor; import net.minecraft.world.entity.ai.sensing.SensorType; import net.minecraft.world.entity.ai.village.ReputationEventType; import net.minecraft.world.entity.ai.village.poi.PoiManager; import net.minecraft.world.entity.ai.village.poi.PoiType; import net.minecraft.world.entity.ai.village.poi.PoiTypes; import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.entity.monster.Witch; import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.raid.Raid; import net.minecraft.world.entity.schedule.Activity; import net.minecraft.world.entity.schedule.Schedule; import net.minecraft.world.flag.FeatureFlags; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; 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.phys.AABB; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; public class Villager extends AbstractVillager implements ReputationEventHandler, VillagerDataHolder { private static final Logger LOGGER = LogUtils.getLogger(); private static final EntityDataAccessor DATA_VILLAGER_DATA = SynchedEntityData.defineId(Villager.class, EntityDataSerializers.VILLAGER_DATA); public static final int BREEDING_FOOD_THRESHOLD = 12; /** * Mapping between valid food items and their respective efficiency values. */ public static final Map FOOD_POINTS = ImmutableMap.of(Items.BREAD, 4, Items.POTATO, 1, Items.CARROT, 1, Items.BEETROOT, 1); private static final int TRADES_PER_LEVEL = 2; private static final int MAX_GOSSIP_TOPICS = 10; private static final int GOSSIP_COOLDOWN = 1200; private static final int GOSSIP_DECAY_INTERVAL = 24000; private static final int HOW_FAR_AWAY_TO_TALK_TO_OTHER_VILLAGERS_ABOUT_GOLEMS = 10; private static final int HOW_MANY_VILLAGERS_NEED_TO_AGREE_TO_SPAWN_A_GOLEM = 5; private static final long TIME_SINCE_SLEEPING_FOR_GOLEM_SPAWNING = 24000L; @VisibleForTesting public static final float SPEED_MODIFIER = 0.5F; private int updateMerchantTimer; private boolean increaseProfessionLevelOnUpdate; @Nullable private Player lastTradedPlayer; private boolean chasing; private int foodLevel; private final GossipContainer gossips = new GossipContainer(); private long lastGossipTime; private long lastGossipDecayTime; private int villagerXp; private long lastRestockGameTime; private int numberOfRestocksToday; private long lastRestockCheckDayTime; private boolean assignProfessionWhenSpawned; private static final ImmutableList> MEMORY_TYPES = ImmutableList.of( MemoryModuleType.HOME, MemoryModuleType.JOB_SITE, MemoryModuleType.POTENTIAL_JOB_SITE, MemoryModuleType.MEETING_POINT, MemoryModuleType.NEAREST_LIVING_ENTITIES, MemoryModuleType.NEAREST_VISIBLE_LIVING_ENTITIES, MemoryModuleType.VISIBLE_VILLAGER_BABIES, MemoryModuleType.NEAREST_PLAYERS, MemoryModuleType.NEAREST_VISIBLE_PLAYER, MemoryModuleType.NEAREST_VISIBLE_ATTACKABLE_PLAYER, MemoryModuleType.NEAREST_VISIBLE_WANTED_ITEM, MemoryModuleType.ITEM_PICKUP_COOLDOWN_TICKS, MemoryModuleType.WALK_TARGET, MemoryModuleType.LOOK_TARGET, MemoryModuleType.INTERACTION_TARGET, MemoryModuleType.BREED_TARGET, MemoryModuleType.PATH, MemoryModuleType.DOORS_TO_CLOSE, MemoryModuleType.NEAREST_BED, MemoryModuleType.HURT_BY, MemoryModuleType.HURT_BY_ENTITY, MemoryModuleType.NEAREST_HOSTILE, MemoryModuleType.SECONDARY_JOB_SITE, MemoryModuleType.HIDING_PLACE, MemoryModuleType.HEARD_BELL_TIME, MemoryModuleType.CANT_REACH_WALK_TARGET_SINCE, MemoryModuleType.LAST_SLEPT, MemoryModuleType.LAST_WOKEN, MemoryModuleType.LAST_WORKED_AT_POI, MemoryModuleType.GOLEM_DETECTED_RECENTLY ); private static final ImmutableList>> SENSOR_TYPES = ImmutableList.of( SensorType.NEAREST_LIVING_ENTITIES, SensorType.NEAREST_PLAYERS, SensorType.NEAREST_ITEMS, SensorType.NEAREST_BED, SensorType.HURT_BY, SensorType.VILLAGER_HOSTILES, SensorType.VILLAGER_BABIES, SensorType.SECONDARY_POIS, SensorType.GOLEM_DETECTED ); public static final Map, BiPredicate>> POI_MEMORIES = ImmutableMap.of( MemoryModuleType.HOME, (villager, holder) -> holder.is(PoiTypes.HOME), MemoryModuleType.JOB_SITE, (villager, holder) -> villager.getVillagerData().getProfession().heldJobSite().test(holder), MemoryModuleType.POTENTIAL_JOB_SITE, (villager, holder) -> VillagerProfession.ALL_ACQUIRABLE_JOBS.test(holder), MemoryModuleType.MEETING_POINT, (villager, holder) -> holder.is(PoiTypes.MEETING) ); public Villager(EntityType entityType, Level level) { this(entityType, level, VillagerType.PLAINS); } public Villager(EntityType entityType, Level level, VillagerType villagerType) { super(entityType, level); ((GroundPathNavigation)this.getNavigation()).setCanOpenDoors(true); this.getNavigation().setCanFloat(true); this.getNavigation().setRequiredPathLength(48.0F); this.setCanPickUpLoot(true); this.setVillagerData(this.getVillagerData().setType(villagerType).setProfession(VillagerProfession.NONE)); } @Override public Brain getBrain() { return (Brain)super.getBrain(); } @Override protected Provider brainProvider() { return Brain.provider(MEMORY_TYPES, SENSOR_TYPES); } @Override protected Brain makeBrain(Dynamic dynamic) { Brain brain = this.brainProvider().makeBrain(dynamic); this.registerBrainGoals(brain); return brain; } public void refreshBrain(ServerLevel serverLevel) { Brain brain = this.getBrain(); brain.stopAll(serverLevel, this); this.brain = brain.copyWithoutBehaviors(); this.registerBrainGoals(this.getBrain()); } private void registerBrainGoals(Brain villagerBrain) { VillagerProfession villagerProfession = this.getVillagerData().getProfession(); if (this.isBaby()) { villagerBrain.setSchedule(Schedule.VILLAGER_BABY); villagerBrain.addActivity(Activity.PLAY, VillagerGoalPackages.getPlayPackage(0.5F)); } else { villagerBrain.setSchedule(Schedule.VILLAGER_DEFAULT); villagerBrain.addActivityWithConditions( Activity.WORK, VillagerGoalPackages.getWorkPackage(villagerProfession, 0.5F), ImmutableSet.of(Pair.of(MemoryModuleType.JOB_SITE, MemoryStatus.VALUE_PRESENT)) ); } villagerBrain.addActivity(Activity.CORE, VillagerGoalPackages.getCorePackage(villagerProfession, 0.5F)); villagerBrain.addActivityWithConditions( Activity.MEET, VillagerGoalPackages.getMeetPackage(villagerProfession, 0.5F), ImmutableSet.of(Pair.of(MemoryModuleType.MEETING_POINT, MemoryStatus.VALUE_PRESENT)) ); villagerBrain.addActivity(Activity.REST, VillagerGoalPackages.getRestPackage(villagerProfession, 0.5F)); villagerBrain.addActivity(Activity.IDLE, VillagerGoalPackages.getIdlePackage(villagerProfession, 0.5F)); villagerBrain.addActivity(Activity.PANIC, VillagerGoalPackages.getPanicPackage(villagerProfession, 0.5F)); villagerBrain.addActivity(Activity.PRE_RAID, VillagerGoalPackages.getPreRaidPackage(villagerProfession, 0.5F)); villagerBrain.addActivity(Activity.RAID, VillagerGoalPackages.getRaidPackage(villagerProfession, 0.5F)); villagerBrain.addActivity(Activity.HIDE, VillagerGoalPackages.getHidePackage(villagerProfession, 0.5F)); villagerBrain.setCoreActivities(ImmutableSet.of(Activity.CORE)); villagerBrain.setDefaultActivity(Activity.IDLE); villagerBrain.setActiveActivityIfPossible(Activity.IDLE); villagerBrain.updateActivityFromSchedule(this.level().getDayTime(), this.level().getGameTime()); } @Override protected void ageBoundaryReached() { super.ageBoundaryReached(); if (this.level() instanceof ServerLevel) { this.refreshBrain((ServerLevel)this.level()); } } public static Builder createAttributes() { return Mob.createMobAttributes().add(Attributes.MOVEMENT_SPEED, 0.5); } public boolean assignProfessionWhenSpawned() { return this.assignProfessionWhenSpawned; } @Override protected void customServerAiStep(ServerLevel serverLevel) { ProfilerFiller profilerFiller = Profiler.get(); profilerFiller.push("villagerBrain"); this.getBrain().tick(serverLevel, this); profilerFiller.pop(); if (this.assignProfessionWhenSpawned) { this.assignProfessionWhenSpawned = false; } if (!this.isTrading() && this.updateMerchantTimer > 0) { this.updateMerchantTimer--; if (this.updateMerchantTimer <= 0) { if (this.increaseProfessionLevelOnUpdate) { this.increaseMerchantCareer(); this.increaseProfessionLevelOnUpdate = false; } this.addEffect(new MobEffectInstance(MobEffects.REGENERATION, 200, 0)); } } if (this.lastTradedPlayer != null) { serverLevel.onReputationEvent(ReputationEventType.TRADE, this.lastTradedPlayer, this); serverLevel.broadcastEntityEvent(this, (byte)14); this.lastTradedPlayer = null; } if (!this.isNoAi() && this.random.nextInt(100) == 0) { Raid raid = serverLevel.getRaidAt(this.blockPosition()); if (raid != null && raid.isActive() && !raid.isOver()) { serverLevel.broadcastEntityEvent(this, (byte)42); } } if (this.getVillagerData().getProfession() == VillagerProfession.NONE && this.isTrading()) { this.stopTrading(); } super.customServerAiStep(serverLevel); } @Override public void tick() { super.tick(); if (this.getUnhappyCounter() > 0) { this.setUnhappyCounter(this.getUnhappyCounter() - 1); } this.maybeDecayGossip(); } @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.isSleeping()) { return super.mobInteract(player, hand); } else if (this.isBaby()) { this.setUnhappy(); return InteractionResult.SUCCESS; } else { if (!this.level().isClientSide) { boolean bl = this.getOffers().isEmpty(); if (hand == InteractionHand.MAIN_HAND) { if (bl) { this.setUnhappy(); } player.awardStat(Stats.TALKED_TO_VILLAGER); } if (bl) { return InteractionResult.CONSUME; } this.startTrading(player); } return InteractionResult.SUCCESS; } } private void setUnhappy() { this.setUnhappyCounter(40); if (!this.level().isClientSide()) { this.makeSound(SoundEvents.VILLAGER_NO); } } private void startTrading(Player player) { this.updateSpecialPrices(player); this.setTradingPlayer(player); this.openTradingScreen(player, this.getDisplayName(), this.getVillagerData().getLevel()); } @Override public void setTradingPlayer(@Nullable Player tradingPlayer) { boolean bl = this.getTradingPlayer() != null && tradingPlayer == null; super.setTradingPlayer(tradingPlayer); if (bl) { this.stopTrading(); } } @Override protected void stopTrading() { super.stopTrading(); this.resetSpecialPrices(); } private void resetSpecialPrices() { if (!this.level().isClientSide()) { for (MerchantOffer merchantOffer : this.getOffers()) { merchantOffer.resetSpecialPriceDiff(); } } } @Override public boolean canRestock() { return true; } public void restock() { this.updateDemand(); for (MerchantOffer merchantOffer : this.getOffers()) { merchantOffer.resetUses(); } this.resendOffersToTradingPlayer(); this.lastRestockGameTime = this.level().getGameTime(); this.numberOfRestocksToday++; } private void resendOffersToTradingPlayer() { MerchantOffers merchantOffers = this.getOffers(); Player player = this.getTradingPlayer(); if (player != null && !merchantOffers.isEmpty()) { player.sendMerchantOffers( player.containerMenu.containerId, merchantOffers, this.getVillagerData().getLevel(), this.getVillagerXp(), this.showProgressBar(), this.canRestock() ); } } private boolean needsToRestock() { for (MerchantOffer merchantOffer : this.getOffers()) { if (merchantOffer.needsRestock()) { return true; } } return false; } private boolean allowedToRestock() { return this.numberOfRestocksToday == 0 || this.numberOfRestocksToday < 2 && this.level().getGameTime() > this.lastRestockGameTime + 2400L; } public boolean shouldRestock() { long l = this.lastRestockGameTime + 12000L; long m = this.level().getGameTime(); boolean bl = m > l; long n = this.level().getDayTime(); if (this.lastRestockCheckDayTime > 0L) { long o = this.lastRestockCheckDayTime / 24000L; long p = n / 24000L; bl |= p > o; } this.lastRestockCheckDayTime = n; if (bl) { this.lastRestockGameTime = m; this.resetNumberOfRestocks(); } return this.allowedToRestock() && this.needsToRestock(); } private void catchUpDemand() { int i = 2 - this.numberOfRestocksToday; if (i > 0) { for (MerchantOffer merchantOffer : this.getOffers()) { merchantOffer.resetUses(); } } for (int j = 0; j < i; j++) { this.updateDemand(); } this.resendOffersToTradingPlayer(); } private void updateDemand() { for (MerchantOffer merchantOffer : this.getOffers()) { merchantOffer.updateDemand(); } } private void updateSpecialPrices(Player player) { int i = this.getPlayerReputation(player); if (i != 0) { for (MerchantOffer merchantOffer : this.getOffers()) { merchantOffer.addToSpecialPriceDiff(-Mth.floor(i * merchantOffer.getPriceMultiplier())); } } if (player.hasEffect(MobEffects.HERO_OF_THE_VILLAGE)) { MobEffectInstance mobEffectInstance = player.getEffect(MobEffects.HERO_OF_THE_VILLAGE); int j = mobEffectInstance.getAmplifier(); for (MerchantOffer merchantOffer2 : this.getOffers()) { double d = 0.3 + 0.0625 * j; int k = (int)Math.floor(d * merchantOffer2.getBaseCostA().getCount()); merchantOffer2.addToSpecialPriceDiff(-Math.max(k, 1)); } } } @Override protected void defineSynchedData(net.minecraft.network.syncher.SynchedEntityData.Builder builder) { super.defineSynchedData(builder); builder.define(DATA_VILLAGER_DATA, new VillagerData(VillagerType.PLAINS, VillagerProfession.NONE, 1)); } @Override public void addAdditionalSaveData(CompoundTag compound) { super.addAdditionalSaveData(compound); VillagerData.CODEC.encodeStart(NbtOps.INSTANCE, this.getVillagerData()).resultOrPartial(LOGGER::error).ifPresent(tag -> compound.put("VillagerData", tag)); compound.putByte("FoodLevel", (byte)this.foodLevel); compound.put("Gossips", this.gossips.store(NbtOps.INSTANCE)); compound.putInt("Xp", this.villagerXp); compound.putLong("LastRestock", this.lastRestockGameTime); compound.putLong("LastGossipDecay", this.lastGossipDecayTime); compound.putInt("RestocksToday", this.numberOfRestocksToday); if (this.assignProfessionWhenSpawned) { compound.putBoolean("AssignProfessionWhenSpawned", true); } } @Override public void readAdditionalSaveData(CompoundTag compound) { super.readAdditionalSaveData(compound); if (compound.contains("VillagerData", 10)) { VillagerData.CODEC .parse(NbtOps.INSTANCE, compound.get("VillagerData")) .resultOrPartial(LOGGER::error) .ifPresent(villagerData -> this.entityData.set(DATA_VILLAGER_DATA, villagerData)); } if (compound.contains("FoodLevel", 1)) { this.foodLevel = compound.getByte("FoodLevel"); } ListTag listTag = compound.getList("Gossips", 10); this.gossips.update(new Dynamic<>(NbtOps.INSTANCE, listTag)); if (compound.contains("Xp", 3)) { this.villagerXp = compound.getInt("Xp"); } this.lastRestockGameTime = compound.getLong("LastRestock"); this.lastGossipDecayTime = compound.getLong("LastGossipDecay"); if (this.level() instanceof ServerLevel) { this.refreshBrain((ServerLevel)this.level()); } this.numberOfRestocksToday = compound.getInt("RestocksToday"); if (compound.contains("AssignProfessionWhenSpawned")) { this.assignProfessionWhenSpawned = compound.getBoolean("AssignProfessionWhenSpawned"); } } @Override public boolean removeWhenFarAway(double distanceToClosestPlayer) { return false; } @Nullable @Override protected SoundEvent getAmbientSound() { if (this.isSleeping()) { return null; } else { return this.isTrading() ? SoundEvents.VILLAGER_TRADE : SoundEvents.VILLAGER_AMBIENT; } } @Override protected SoundEvent getHurtSound(DamageSource damageSource) { return SoundEvents.VILLAGER_HURT; } @Override protected SoundEvent getDeathSound() { return SoundEvents.VILLAGER_DEATH; } public void playWorkSound() { this.makeSound(this.getVillagerData().getProfession().workSound()); } @Override public void setVillagerData(VillagerData data) { VillagerData villagerData = this.getVillagerData(); if (villagerData.getProfession() != data.getProfession()) { this.offers = null; } this.entityData.set(DATA_VILLAGER_DATA, data); } @Override public VillagerData getVillagerData() { return this.entityData.get(DATA_VILLAGER_DATA); } @Override protected void rewardTradeXp(MerchantOffer offer) { int i = 3 + this.random.nextInt(4); this.villagerXp = this.villagerXp + offer.getXp(); this.lastTradedPlayer = this.getTradingPlayer(); if (this.shouldIncreaseLevel()) { this.updateMerchantTimer = 40; this.increaseProfessionLevelOnUpdate = true; i += 5; } if (offer.shouldRewardExp()) { this.level().addFreshEntity(new ExperienceOrb(this.level(), this.getX(), this.getY() + 0.5, this.getZ(), i)); } } @Override public void setLastHurtByMob(@Nullable LivingEntity livingEntity) { if (livingEntity != null && this.level() instanceof ServerLevel) { ((ServerLevel)this.level()).onReputationEvent(ReputationEventType.VILLAGER_HURT, livingEntity, this); if (this.isAlive() && livingEntity instanceof Player) { this.level().broadcastEntityEvent(this, (byte)13); } } super.setLastHurtByMob(livingEntity); } @Override public void die(DamageSource damageSource) { LOGGER.info("Villager {} died, message: '{}'", this, damageSource.getLocalizedDeathMessage(this).getString()); Entity entity = damageSource.getEntity(); if (entity != null) { this.tellWitnessesThatIWasMurdered(entity); } this.releaseAllPois(); super.die(damageSource); } private void releaseAllPois() { this.releasePoi(MemoryModuleType.HOME); this.releasePoi(MemoryModuleType.JOB_SITE); this.releasePoi(MemoryModuleType.POTENTIAL_JOB_SITE); this.releasePoi(MemoryModuleType.MEETING_POINT); } private void tellWitnessesThatIWasMurdered(Entity murderer) { if (this.level() instanceof ServerLevel serverLevel) { Optional optional = this.brain.getMemory(MemoryModuleType.NEAREST_VISIBLE_LIVING_ENTITIES); if (!optional.isEmpty()) { ((NearestVisibleLivingEntities)optional.get()) .findAll(ReputationEventHandler.class::isInstance) .forEach(livingEntity -> serverLevel.onReputationEvent(ReputationEventType.VILLAGER_KILLED, murderer, (ReputationEventHandler)livingEntity)); } } } public void releasePoi(MemoryModuleType moduleType) { if (this.level() instanceof ServerLevel) { MinecraftServer minecraftServer = ((ServerLevel)this.level()).getServer(); this.brain.getMemory(moduleType).ifPresent(globalPos -> { ServerLevel serverLevel = minecraftServer.getLevel(globalPos.dimension()); if (serverLevel != null) { PoiManager poiManager = serverLevel.getPoiManager(); Optional> optional = poiManager.getType(globalPos.pos()); BiPredicate> biPredicate = (BiPredicate>)POI_MEMORIES.get(moduleType); if (optional.isPresent() && biPredicate.test(this, (Holder)optional.get())) { poiManager.release(globalPos.pos()); DebugPackets.sendPoiTicketCountPacket(serverLevel, globalPos.pos()); } } }); } } @Override public boolean canBreed() { return this.foodLevel + this.countFoodPointsInInventory() >= 12 && !this.isSleeping() && this.getAge() == 0; } private boolean hungry() { return this.foodLevel < 12; } private void eatUntilFull() { if (this.hungry() && this.countFoodPointsInInventory() != 0) { for (int i = 0; i < this.getInventory().getContainerSize(); i++) { ItemStack itemStack = this.getInventory().getItem(i); if (!itemStack.isEmpty()) { Integer integer = (Integer)FOOD_POINTS.get(itemStack.getItem()); if (integer != null) { int j = itemStack.getCount(); for (int k = j; k > 0; k--) { this.foodLevel = this.foodLevel + integer; this.getInventory().removeItem(i, 1); if (!this.hungry()) { return; } } } } } } } public int getPlayerReputation(Player player) { return this.gossips.getReputation(player.getUUID(), gossipType -> true); } private void digestFood(int qty) { this.foodLevel -= qty; } public void eatAndDigestFood() { this.eatUntilFull(); this.digestFood(12); } public void setOffers(MerchantOffers offers) { this.offers = offers; } private boolean shouldIncreaseLevel() { int i = this.getVillagerData().getLevel(); return VillagerData.canLevelUp(i) && this.villagerXp >= VillagerData.getMaxXpPerLevel(i); } private void increaseMerchantCareer() { this.setVillagerData(this.getVillagerData().setLevel(this.getVillagerData().getLevel() + 1)); this.updateTrades(); } @Override protected Component getTypeName() { return Component.translatable( this.getType().getDescriptionId() + "." + BuiltInRegistries.VILLAGER_PROFESSION.getKey(this.getVillagerData().getProfession()).getPath() ); } @Override public void handleEntityEvent(byte id) { if (id == 12) { this.addParticlesAroundSelf(ParticleTypes.HEART); } else if (id == 13) { this.addParticlesAroundSelf(ParticleTypes.ANGRY_VILLAGER); } else if (id == 14) { this.addParticlesAroundSelf(ParticleTypes.HAPPY_VILLAGER); } else if (id == 42) { this.addParticlesAroundSelf(ParticleTypes.SPLASH); } else { super.handleEntityEvent(id); } } @Nullable @Override public SpawnGroupData finalizeSpawn( ServerLevelAccessor serverLevelAccessor, DifficultyInstance difficultyInstance, EntitySpawnReason entitySpawnReason, @Nullable SpawnGroupData spawnGroupData ) { if (entitySpawnReason == EntitySpawnReason.BREEDING) { this.setVillagerData(this.getVillagerData().setProfession(VillagerProfession.NONE)); } if (entitySpawnReason == EntitySpawnReason.COMMAND || entitySpawnReason == EntitySpawnReason.SPAWN_ITEM_USE || EntitySpawnReason.isSpawner(entitySpawnReason) || entitySpawnReason == EntitySpawnReason.DISPENSER) { this.setVillagerData(this.getVillagerData().setType(VillagerType.byBiome(serverLevelAccessor.getBiome(this.blockPosition())))); } if (entitySpawnReason == EntitySpawnReason.STRUCTURE) { this.assignProfessionWhenSpawned = true; } return super.finalizeSpawn(serverLevelAccessor, difficultyInstance, entitySpawnReason, spawnGroupData); } @Nullable public Villager getBreedOffspring(ServerLevel level, AgeableMob otherParent) { double d = this.random.nextDouble(); VillagerType villagerType; if (d < 0.5) { villagerType = VillagerType.byBiome(level.getBiome(this.blockPosition())); } else if (d < 0.75) { villagerType = this.getVillagerData().getType(); } else { villagerType = ((Villager)otherParent).getVillagerData().getType(); } Villager villager = new Villager(EntityType.VILLAGER, level, villagerType); villager.finalizeSpawn(level, level.getCurrentDifficultyAt(villager.blockPosition()), EntitySpawnReason.BREEDING, null); return villager; } @Override public void thunderHit(ServerLevel level, LightningBolt lightning) { if (level.getDifficulty() != Difficulty.PEACEFUL) { LOGGER.info("Villager {} was struck by lightning {}.", this, lightning); Witch witch = this.convertTo(EntityType.WITCH, ConversionParams.single(this, false, false), witchx -> { witchx.finalizeSpawn(level, level.getCurrentDifficultyAt(witchx.blockPosition()), EntitySpawnReason.CONVERSION, null); witchx.setPersistenceRequired(); this.releaseAllPois(); }); if (witch == null) { super.thunderHit(level, lightning); } } else { super.thunderHit(level, lightning); } } @Override protected void pickUpItem(ServerLevel serverLevel, ItemEntity itemEntity) { InventoryCarrier.pickUpItem(serverLevel, this, this, itemEntity); } @Override public boolean wantsToPickUp(ServerLevel serverLevel, ItemStack itemStack) { Item item = itemStack.getItem(); return (itemStack.is(ItemTags.VILLAGER_PICKS_UP) || this.getVillagerData().getProfession().requestedItems().contains(item)) && this.getInventory().canAddItem(itemStack); } /** * Used by {@link net.minecraft.world.entity.ai.behavior.TradeWithVillager} to check if the villager can give some items from an inventory to another villager. */ public boolean hasExcessFood() { return this.countFoodPointsInInventory() >= 24; } public boolean wantsMoreFood() { return this.countFoodPointsInInventory() < 12; } /** * @return calculated food value from item stacks in this villager's inventory */ private int countFoodPointsInInventory() { SimpleContainer simpleContainer = this.getInventory(); return FOOD_POINTS.entrySet().stream().mapToInt(entry -> simpleContainer.countItem((Item)entry.getKey()) * (Integer)entry.getValue()).sum(); } /** * Returns {@code true} if villager has seeds, potatoes or carrots in inventory */ public boolean hasFarmSeeds() { return this.getInventory().hasAnyMatching(itemStack -> itemStack.is(ItemTags.VILLAGER_PLANTABLE_SEEDS)); } @Override protected void updateTrades() { VillagerData villagerData = this.getVillagerData(); Int2ObjectMap int2ObjectMap2; if (this.level().enabledFeatures().contains(FeatureFlags.TRADE_REBALANCE)) { Int2ObjectMap int2ObjectMap = (Int2ObjectMap)VillagerTrades.EXPERIMENTAL_TRADES .get(villagerData.getProfession()); int2ObjectMap2 = int2ObjectMap != null ? int2ObjectMap : (Int2ObjectMap)VillagerTrades.TRADES.get(villagerData.getProfession()); } else { int2ObjectMap2 = (Int2ObjectMap)VillagerTrades.TRADES.get(villagerData.getProfession()); } if (int2ObjectMap2 != null && !int2ObjectMap2.isEmpty()) { VillagerTrades.ItemListing[] itemListings = int2ObjectMap2.get(villagerData.getLevel()); if (itemListings != null) { MerchantOffers merchantOffers = this.getOffers(); this.addOffersFromItemListings(merchantOffers, itemListings, 2); } } } public void gossip(ServerLevel serverLevel, Villager target, long gameTime) { if ((gameTime < this.lastGossipTime || gameTime >= this.lastGossipTime + 1200L) && (gameTime < target.lastGossipTime || gameTime >= target.lastGossipTime + 1200L)) { this.gossips.transferFrom(target.gossips, this.random, 10); this.lastGossipTime = gameTime; target.lastGossipTime = gameTime; this.spawnGolemIfNeeded(serverLevel, gameTime, 5); } } private void maybeDecayGossip() { long l = this.level().getGameTime(); if (this.lastGossipDecayTime == 0L) { this.lastGossipDecayTime = l; } else if (l >= this.lastGossipDecayTime + 24000L) { this.gossips.decay(); this.lastGossipDecayTime = l; } } public void spawnGolemIfNeeded(ServerLevel serverLevel, long gameTime, int minVillagerAmount) { if (this.wantsToSpawnGolem(gameTime)) { AABB aABB = this.getBoundingBox().inflate(10.0, 10.0, 10.0); List list = serverLevel.getEntitiesOfClass(Villager.class, aABB); List list2 = list.stream().filter(villager -> villager.wantsToSpawnGolem(gameTime)).limit(5L).toList(); if (list2.size() >= minVillagerAmount) { if (!SpawnUtil.trySpawnMob( EntityType.IRON_GOLEM, EntitySpawnReason.MOB_SUMMONED, serverLevel, this.blockPosition(), 10, 8, 6, SpawnUtil.Strategy.LEGACY_IRON_GOLEM ) .isEmpty()) { list.forEach(GolemSensor::golemDetected); } } } } public boolean wantsToSpawnGolem(long gameTime) { return !this.golemSpawnConditionsMet(this.level().getGameTime()) ? false : !this.brain.hasMemoryValue(MemoryModuleType.GOLEM_DETECTED_RECENTLY); } @Override public void onReputationEventFrom(ReputationEventType type, Entity target) { if (type == ReputationEventType.ZOMBIE_VILLAGER_CURED) { this.gossips.add(target.getUUID(), GossipType.MAJOR_POSITIVE, 20); this.gossips.add(target.getUUID(), GossipType.MINOR_POSITIVE, 25); } else if (type == ReputationEventType.TRADE) { this.gossips.add(target.getUUID(), GossipType.TRADING, 2); } else if (type == ReputationEventType.VILLAGER_HURT) { this.gossips.add(target.getUUID(), GossipType.MINOR_NEGATIVE, 25); } else if (type == ReputationEventType.VILLAGER_KILLED) { this.gossips.add(target.getUUID(), GossipType.MAJOR_NEGATIVE, 25); } } @Override public int getVillagerXp() { return this.villagerXp; } public void setVillagerXp(int villagerXp) { this.villagerXp = villagerXp; } private void resetNumberOfRestocks() { this.catchUpDemand(); this.numberOfRestocksToday = 0; } public GossipContainer getGossips() { return this.gossips; } public void setGossips(Tag gossip) { this.gossips.update(new Dynamic<>(NbtOps.INSTANCE, gossip)); } @Override protected void sendDebugPackets() { super.sendDebugPackets(); DebugPackets.sendEntityBrain(this); } @Override public void startSleeping(BlockPos pos) { super.startSleeping(pos); this.brain.setMemory(MemoryModuleType.LAST_SLEPT, this.level().getGameTime()); this.brain.eraseMemory(MemoryModuleType.WALK_TARGET); this.brain.eraseMemory(MemoryModuleType.CANT_REACH_WALK_TARGET_SINCE); } @Override public void stopSleeping() { super.stopSleeping(); this.brain.setMemory(MemoryModuleType.LAST_WOKEN, this.level().getGameTime()); } private boolean golemSpawnConditionsMet(long gameTime) { Optional optional = this.brain.getMemory(MemoryModuleType.LAST_SLEPT); return optional.filter(long_ -> gameTime - long_ < 24000L).isPresent(); } }