package net.minecraft.world.entity.monster; import com.google.common.annotations.VisibleForTesting; import java.util.UUID; import net.minecraft.advancements.CriteriaTriggers; import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; import net.minecraft.core.UUIDUtil; import net.minecraft.core.component.DataComponentGetter; import net.minecraft.core.component.DataComponentType; import net.minecraft.core.component.DataComponents; import net.minecraft.core.registries.BuiltInRegistries; 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.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundEvents; import net.minecraft.world.DifficultyInstance; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.effect.MobEffectInstance; import net.minecraft.world.effect.MobEffects; import net.minecraft.world.entity.ConversionParams; import net.minecraft.world.entity.EntitySpawnReason; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.SlotAccess; import net.minecraft.world.entity.SpawnGroupData; import net.minecraft.world.entity.ai.gossip.GossipContainer; import net.minecraft.world.entity.ai.village.ReputationEventType; import net.minecraft.world.entity.npc.Villager; import net.minecraft.world.entity.npc.VillagerData; import net.minecraft.world.entity.npc.VillagerDataHolder; import net.minecraft.world.entity.npc.VillagerType; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.item.enchantment.EnchantmentEffectComponents; import net.minecraft.world.item.enchantment.EnchantmentHelper; import net.minecraft.world.item.trading.MerchantOffers; import net.minecraft.world.level.Level; import net.minecraft.world.level.ServerLevelAccessor; import net.minecraft.world.level.block.BedBlock; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.state.BlockState; import org.jetbrains.annotations.Nullable; public class ZombieVillager extends Zombie implements VillagerDataHolder { private static final EntityDataAccessor DATA_CONVERTING_ID = SynchedEntityData.defineId(ZombieVillager.class, EntityDataSerializers.BOOLEAN); private static final EntityDataAccessor DATA_VILLAGER_DATA = SynchedEntityData.defineId( ZombieVillager.class, EntityDataSerializers.VILLAGER_DATA ); private static final int VILLAGER_CONVERSION_WAIT_MIN = 3600; private static final int VILLAGER_CONVERSION_WAIT_MAX = 6000; private static final int MAX_SPECIAL_BLOCKS_COUNT = 14; private static final int SPECIAL_BLOCK_RADIUS = 4; private static final int NOT_CONVERTING = -1; private static final int DEFAULT_XP = 0; private int villagerConversionTime; @Nullable private UUID conversionStarter; @Nullable private GossipContainer gossips; @Nullable private MerchantOffers tradeOffers; private int villagerXp = 0; public ZombieVillager(EntityType entityType, Level level) { super(entityType, level); BuiltInRegistries.VILLAGER_PROFESSION.getRandom(this.random).ifPresent(reference -> this.setVillagerData(this.getVillagerData().withProfession(reference))); } @Override protected void defineSynchedData(Builder builder) { super.defineSynchedData(builder); builder.define(DATA_CONVERTING_ID, false); builder.define(DATA_VILLAGER_DATA, Villager.createDefaultVillagerData()); } @Override public void addAdditionalSaveData(CompoundTag tag) { super.addAdditionalSaveData(tag); tag.store("VillagerData", VillagerData.CODEC, this.getVillagerData()); tag.storeNullable("Offers", MerchantOffers.CODEC, this.registryAccess().createSerializationContext(NbtOps.INSTANCE), this.tradeOffers); tag.storeNullable("Gossips", GossipContainer.CODEC, this.gossips); tag.putInt("ConversionTime", this.isConverting() ? this.villagerConversionTime : -1); tag.storeNullable("ConversionPlayer", UUIDUtil.CODEC, this.conversionStarter); tag.putInt("Xp", this.villagerXp); } @Override public void readAdditionalSaveData(CompoundTag tag) { super.readAdditionalSaveData(tag); this.entityData.set(DATA_VILLAGER_DATA, (VillagerData)tag.read("VillagerData", VillagerData.CODEC).orElseGet(Villager::createDefaultVillagerData)); this.tradeOffers = (MerchantOffers)tag.read("Offers", MerchantOffers.CODEC, this.registryAccess().createSerializationContext(NbtOps.INSTANCE)).orElse(null); this.gossips = (GossipContainer)tag.read("Gossips", GossipContainer.CODEC).orElse(null); int i = tag.getIntOr("ConversionTime", -1); if (i != -1) { UUID uUID = (UUID)tag.read("ConversionPlayer", UUIDUtil.CODEC).orElse(null); this.startConverting(uUID, i); } else { this.getEntityData().set(DATA_CONVERTING_ID, false); this.villagerConversionTime = -1; } this.villagerXp = tag.getIntOr("Xp", 0); } @Override public void tick() { if (!this.level().isClientSide && this.isAlive() && this.isConverting()) { int i = this.getConversionProgress(); this.villagerConversionTime -= i; if (this.villagerConversionTime <= 0) { this.finishConversion((ServerLevel)this.level()); } } super.tick(); } @Override public InteractionResult mobInteract(Player player, InteractionHand hand) { ItemStack itemStack = player.getItemInHand(hand); if (itemStack.is(Items.GOLDEN_APPLE)) { if (this.hasEffect(MobEffects.WEAKNESS)) { itemStack.consume(1, player); if (!this.level().isClientSide) { this.startConverting(player.getUUID(), this.random.nextInt(2401) + 3600); } return InteractionResult.SUCCESS_SERVER; } else { return InteractionResult.CONSUME; } } else { return super.mobInteract(player, hand); } } @Override protected boolean convertsInWater() { return false; } @Override public boolean removeWhenFarAway(double distanceToClosestPlayer) { return !this.isConverting() && this.villagerXp == 0; } /** * Returns whether this zombie is in the process of converting to a villager */ public boolean isConverting() { return this.getEntityData().get(DATA_CONVERTING_ID); } /** * Starts conversion of this zombie villager to a villager */ private void startConverting(@Nullable UUID conversionStarter, int villagerConversionTime) { this.conversionStarter = conversionStarter; this.villagerConversionTime = villagerConversionTime; this.getEntityData().set(DATA_CONVERTING_ID, true); this.removeEffect(MobEffects.WEAKNESS); this.addEffect(new MobEffectInstance(MobEffects.STRENGTH, villagerConversionTime, Math.min(this.level().getDifficulty().getId() - 1, 0))); this.level().broadcastEntityEvent(this, (byte)16); } @Override public void handleEntityEvent(byte id) { if (id == 16) { if (!this.isSilent()) { this.level() .playLocalSound( this.getX(), this.getEyeY(), this.getZ(), SoundEvents.ZOMBIE_VILLAGER_CURE, this.getSoundSource(), 1.0F + this.random.nextFloat(), this.random.nextFloat() * 0.7F + 0.3F, false ); } } else { super.handleEntityEvent(id); } } private void finishConversion(ServerLevel level) { this.convertTo( EntityType.VILLAGER, ConversionParams.single(this, false, false), villager -> { for (EquipmentSlot equipmentSlot : this.dropPreservedEquipment( level, itemStack -> !EnchantmentHelper.has(itemStack, EnchantmentEffectComponents.PREVENT_ARMOR_CHANGE) )) { SlotAccess slotAccess = villager.getSlot(equipmentSlot.getIndex() + 300); slotAccess.set(this.getItemBySlot(equipmentSlot)); } villager.setVillagerData(this.getVillagerData()); if (this.gossips != null) { villager.setGossips(this.gossips); } if (this.tradeOffers != null) { villager.setOffers(this.tradeOffers.copy()); } villager.setVillagerXp(this.villagerXp); villager.finalizeSpawn(level, level.getCurrentDifficultyAt(villager.blockPosition()), EntitySpawnReason.CONVERSION, null); villager.refreshBrain(level); if (this.conversionStarter != null) { Player player = level.getPlayerByUUID(this.conversionStarter); if (player instanceof ServerPlayer) { CriteriaTriggers.CURED_ZOMBIE_VILLAGER.trigger((ServerPlayer)player, this, villager); level.onReputationEvent(ReputationEventType.ZOMBIE_VILLAGER_CURED, player, villager); } } villager.addEffect(new MobEffectInstance(MobEffects.NAUSEA, 200, 0)); if (!this.isSilent()) { level.levelEvent(null, 1027, this.blockPosition(), 0); } } ); } @VisibleForTesting public void setVillagerConversionTime(int villagerConversionTime) { this.villagerConversionTime = villagerConversionTime; } private int getConversionProgress() { int i = 1; if (this.random.nextFloat() < 0.01F) { int j = 0; BlockPos.MutableBlockPos mutableBlockPos = new BlockPos.MutableBlockPos(); for (int k = (int)this.getX() - 4; k < (int)this.getX() + 4 && j < 14; k++) { for (int l = (int)this.getY() - 4; l < (int)this.getY() + 4 && j < 14; l++) { for (int m = (int)this.getZ() - 4; m < (int)this.getZ() + 4 && j < 14; m++) { BlockState blockState = this.level().getBlockState(mutableBlockPos.set(k, l, m)); if (blockState.is(Blocks.IRON_BARS) || blockState.getBlock() instanceof BedBlock) { if (this.random.nextFloat() < 0.3F) { i++; } j++; } } } } } return i; } @Override public float getVoicePitch() { return this.isBaby() ? (this.random.nextFloat() - this.random.nextFloat()) * 0.2F + 2.0F : (this.random.nextFloat() - this.random.nextFloat()) * 0.2F + 1.0F; } @Override public SoundEvent getAmbientSound() { return SoundEvents.ZOMBIE_VILLAGER_AMBIENT; } @Override public SoundEvent getHurtSound(DamageSource damageSource) { return SoundEvents.ZOMBIE_VILLAGER_HURT; } @Override public SoundEvent getDeathSound() { return SoundEvents.ZOMBIE_VILLAGER_DEATH; } @Override public SoundEvent getStepSound() { return SoundEvents.ZOMBIE_VILLAGER_STEP; } @Override protected ItemStack getSkull() { return ItemStack.EMPTY; } public void setTradeOffers(MerchantOffers tradeOffers) { this.tradeOffers = tradeOffers; } public void setGossips(GossipContainer gossips) { this.gossips = gossips; } @Nullable @Override public SpawnGroupData finalizeSpawn( ServerLevelAccessor level, DifficultyInstance difficulty, EntitySpawnReason spawnReason, @Nullable SpawnGroupData spawnGroupData ) { this.setVillagerData(this.getVillagerData().withType(level.registryAccess(), VillagerType.byBiome(level.getBiome(this.blockPosition())))); return super.finalizeSpawn(level, difficulty, spawnReason, spawnGroupData); } @Override public void setVillagerData(VillagerData data) { VillagerData villagerData = this.getVillagerData(); if (!villagerData.profession().equals(data.profession())) { this.tradeOffers = null; } this.entityData.set(DATA_VILLAGER_DATA, data); } @Override public VillagerData getVillagerData() { return this.entityData.get(DATA_VILLAGER_DATA); } public int getVillagerXp() { return this.villagerXp; } public void setVillagerXp(int villagerXp) { this.villagerXp = villagerXp; } @Nullable @Override public T get(DataComponentType component) { return component == DataComponents.VILLAGER_VARIANT ? castComponentValue((DataComponentType)component, this.getVillagerData().type()) : super.get(component); } @Override protected void applyImplicitComponents(DataComponentGetter componentGetter) { this.applyImplicitComponentIfPresent(componentGetter, DataComponents.VILLAGER_VARIANT); super.applyImplicitComponents(componentGetter); } @Override protected boolean applyImplicitComponent(DataComponentType component, T value) { if (component == DataComponents.VILLAGER_VARIANT) { Holder holder = castComponentValue(DataComponents.VILLAGER_VARIANT, value); this.setVillagerData(this.getVillagerData().withType(holder)); return true; } else { return super.applyImplicitComponent(component, value); } } }