package net.minecraft.world.entity.animal.allay; import com.google.common.collect.ImmutableList; import com.mojang.serialization.Dynamic; import java.util.Objects; import java.util.Optional; import java.util.UUID; import java.util.function.BiConsumer; import net.minecraft.core.BlockPos; import net.minecraft.core.GlobalPos; import net.minecraft.core.Holder; import net.minecraft.core.Vec3i; import net.minecraft.core.component.DataComponents; import net.minecraft.core.particles.ParticleTypes; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.NbtOps; import net.minecraft.nbt.Tag; 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.resources.RegistryOps; import net.minecraft.server.level.ServerLevel; import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundSource; import net.minecraft.tags.GameEventTags; import net.minecraft.tags.ItemTags; import net.minecraft.tags.TagKey; import net.minecraft.util.Mth; import net.minecraft.util.profiling.Profiler; import net.minecraft.util.profiling.ProfilerFiller; 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.entity.Entity; import net.minecraft.world.entity.EntitySpawnReason; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.Mob; import net.minecraft.world.entity.MoverType; import net.minecraft.world.entity.PathfinderMob; import net.minecraft.world.entity.ai.Brain; import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.entity.ai.attributes.AttributeSupplier.Builder; import net.minecraft.world.entity.ai.behavior.BehaviorUtils; import net.minecraft.world.entity.ai.control.FlyingMoveControl; import net.minecraft.world.entity.ai.memory.MemoryModuleType; import net.minecraft.world.entity.ai.memory.MemoryStatus; import net.minecraft.world.entity.ai.navigation.FlyingPathNavigation; import net.minecraft.world.entity.ai.navigation.PathNavigation; import net.minecraft.world.entity.ai.sensing.Sensor; import net.minecraft.world.entity.ai.sensing.SensorType; import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.entity.npc.InventoryCarrier; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.alchemy.PotionContents; import net.minecraft.world.item.enchantment.EnchantmentEffectComponents; import net.minecraft.world.item.enchantment.EnchantmentHelper; import net.minecraft.world.level.GameRules; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.gameevent.DynamicGameEventListener; import net.minecraft.world.level.gameevent.EntityPositionSource; import net.minecraft.world.level.gameevent.GameEvent; import net.minecraft.world.level.gameevent.GameEventListener; import net.minecraft.world.level.gameevent.PositionSource; import net.minecraft.world.level.gameevent.GameEvent.Context; import net.minecraft.world.level.gameevent.vibrations.VibrationSystem; import net.minecraft.world.phys.Vec3; import org.jetbrains.annotations.Nullable; public class Allay extends PathfinderMob implements InventoryCarrier, VibrationSystem { private static final Vec3i ITEM_PICKUP_REACH = new Vec3i(1, 1, 1); private static final int LIFTING_ITEM_ANIMATION_DURATION = 5; private static final float DANCING_LOOP_DURATION = 55.0F; private static final float SPINNING_ANIMATION_DURATION = 15.0F; private static final int DEFAULT_DUPLICATION_COOLDOWN = 0; private static final int DUPLICATION_COOLDOWN_TICKS = 6000; private static final int NUM_OF_DUPLICATION_HEARTS = 3; public static final int MAX_NOTEBLOCK_DISTANCE = 1024; private static final EntityDataAccessor DATA_DANCING = SynchedEntityData.defineId(Allay.class, EntityDataSerializers.BOOLEAN); private static final EntityDataAccessor DATA_CAN_DUPLICATE = SynchedEntityData.defineId(Allay.class, EntityDataSerializers.BOOLEAN); protected static final ImmutableList>> SENSOR_TYPES = ImmutableList.of( SensorType.NEAREST_LIVING_ENTITIES, SensorType.NEAREST_PLAYERS, SensorType.HURT_BY, SensorType.NEAREST_ITEMS ); protected static final ImmutableList> MEMORY_TYPES = ImmutableList.of( MemoryModuleType.PATH, MemoryModuleType.LOOK_TARGET, MemoryModuleType.NEAREST_VISIBLE_LIVING_ENTITIES, MemoryModuleType.WALK_TARGET, MemoryModuleType.CANT_REACH_WALK_TARGET_SINCE, MemoryModuleType.HURT_BY, MemoryModuleType.NEAREST_VISIBLE_WANTED_ITEM, MemoryModuleType.LIKED_PLAYER, MemoryModuleType.LIKED_NOTEBLOCK_POSITION, MemoryModuleType.LIKED_NOTEBLOCK_COOLDOWN_TICKS, MemoryModuleType.ITEM_PICKUP_COOLDOWN_TICKS, MemoryModuleType.IS_PANICKING ); public static final ImmutableList THROW_SOUND_PITCHES = ImmutableList.of( 0.5625F, 0.625F, 0.75F, 0.9375F, 1.0F, 1.0F, 1.125F, 1.25F, 1.5F, 1.875F, 2.0F, 2.25F, 2.5F, 3.0F, 3.75F, 4.0F ); private final DynamicGameEventListener dynamicVibrationListener; private VibrationSystem.Data vibrationData; private final VibrationSystem.User vibrationUser; private final DynamicGameEventListener dynamicJukeboxListener; private final SimpleContainer inventory = new SimpleContainer(1); @Nullable private BlockPos jukeboxPos; private long duplicationCooldown = 0L; private float holdingItemAnimationTicks; private float holdingItemAnimationTicks0; private float dancingAnimationTicks; private float spinningAnimationTicks; private float spinningAnimationTicks0; public Allay(EntityType entityType, Level level) { super(entityType, level); this.moveControl = new FlyingMoveControl(this, 20, true); this.setCanPickUpLoot(this.canPickUpLoot()); this.vibrationUser = new Allay.VibrationUser(); this.vibrationData = new VibrationSystem.Data(); this.dynamicVibrationListener = new DynamicGameEventListener<>(new VibrationSystem.Listener(this)); this.dynamicJukeboxListener = new DynamicGameEventListener<>( new Allay.JukeboxListener(this.vibrationUser.getPositionSource(), GameEvent.JUKEBOX_PLAY.value().notificationRadius()) ); } @Override protected Brain.Provider brainProvider() { return Brain.provider(MEMORY_TYPES, SENSOR_TYPES); } @Override protected Brain makeBrain(Dynamic dynamic) { return AllayAi.makeBrain(this.brainProvider().makeBrain(dynamic)); } @Override public Brain getBrain() { return (Brain)super.getBrain(); } public static Builder createAttributes() { return Mob.createMobAttributes() .add(Attributes.MAX_HEALTH, 20.0) .add(Attributes.FLYING_SPEED, 0.1F) .add(Attributes.MOVEMENT_SPEED, 0.1F) .add(Attributes.ATTACK_DAMAGE, 2.0); } @Override protected PathNavigation createNavigation(Level level) { FlyingPathNavigation flyingPathNavigation = new FlyingPathNavigation(this, level); flyingPathNavigation.setCanOpenDoors(false); flyingPathNavigation.setCanFloat(true); flyingPathNavigation.setRequiredPathLength(48.0F); return flyingPathNavigation; } @Override protected void defineSynchedData(net.minecraft.network.syncher.SynchedEntityData.Builder builder) { super.defineSynchedData(builder); builder.define(DATA_DANCING, false); builder.define(DATA_CAN_DUPLICATE, true); } @Override public void travel(Vec3 travelVector) { if (this.isInWater()) { this.moveRelative(0.02F, travelVector); this.move(MoverType.SELF, this.getDeltaMovement()); this.setDeltaMovement(this.getDeltaMovement().scale(0.8F)); } else if (this.isInLava()) { this.moveRelative(0.02F, travelVector); this.move(MoverType.SELF, this.getDeltaMovement()); this.setDeltaMovement(this.getDeltaMovement().scale(0.5)); } else { this.moveRelative(this.getSpeed(), travelVector); this.move(MoverType.SELF, this.getDeltaMovement()); this.setDeltaMovement(this.getDeltaMovement().scale(0.91F)); } } @Override public boolean hurtServer(ServerLevel level, DamageSource damageSource, float amount) { return this.isLikedPlayer(damageSource.getEntity()) ? false : super.hurtServer(level, damageSource, amount); } @Override protected boolean considersEntityAsAlly(Entity entity) { return this.isLikedPlayer(entity) || super.considersEntityAsAlly(entity); } private boolean isLikedPlayer(@Nullable Entity entity) { if (!(entity instanceof Player player)) { return false; } else { Optional optional = this.getBrain().getMemory(MemoryModuleType.LIKED_PLAYER); return optional.isPresent() && player.getUUID().equals(optional.get()); } } @Override protected void playStepSound(BlockPos pos, BlockState state) { } @Override protected void checkFallDamage(double y, boolean onGround, BlockState state, BlockPos pos) { } @Override protected SoundEvent getAmbientSound() { return this.hasItemInSlot(EquipmentSlot.MAINHAND) ? SoundEvents.ALLAY_AMBIENT_WITH_ITEM : SoundEvents.ALLAY_AMBIENT_WITHOUT_ITEM; } @Override protected SoundEvent getHurtSound(DamageSource damageSource) { return SoundEvents.ALLAY_HURT; } @Override protected SoundEvent getDeathSound() { return SoundEvents.ALLAY_DEATH; } @Override protected float getSoundVolume() { return 0.4F; } @Override protected void customServerAiStep(ServerLevel level) { ProfilerFiller profilerFiller = Profiler.get(); profilerFiller.push("allayBrain"); this.getBrain().tick(level, this); profilerFiller.pop(); profilerFiller.push("allayActivityUpdate"); AllayAi.updateActivity(this); profilerFiller.pop(); super.customServerAiStep(level); } @Override public void aiStep() { super.aiStep(); if (!this.level().isClientSide && this.isAlive() && this.tickCount % 10 == 0) { this.heal(1.0F); } if (this.isDancing() && this.shouldStopDancing() && this.tickCount % 20 == 0) { this.setDancing(false); this.jukeboxPos = null; } this.updateDuplicationCooldown(); } @Override public void tick() { super.tick(); if (this.level().isClientSide) { this.holdingItemAnimationTicks0 = this.holdingItemAnimationTicks; if (this.hasItemInHand()) { this.holdingItemAnimationTicks = Mth.clamp(this.holdingItemAnimationTicks + 1.0F, 0.0F, 5.0F); } else { this.holdingItemAnimationTicks = Mth.clamp(this.holdingItemAnimationTicks - 1.0F, 0.0F, 5.0F); } if (this.isDancing()) { this.dancingAnimationTicks++; this.spinningAnimationTicks0 = this.spinningAnimationTicks; if (this.isSpinning()) { this.spinningAnimationTicks++; } else { this.spinningAnimationTicks--; } this.spinningAnimationTicks = Mth.clamp(this.spinningAnimationTicks, 0.0F, 15.0F); } else { this.dancingAnimationTicks = 0.0F; this.spinningAnimationTicks = 0.0F; this.spinningAnimationTicks0 = 0.0F; } } else { VibrationSystem.Ticker.tick(this.level(), this.vibrationData, this.vibrationUser); if (this.isPanicking()) { this.setDancing(false); } } } @Override public boolean canPickUpLoot() { return !this.isOnPickupCooldown() && this.hasItemInHand(); } public boolean hasItemInHand() { return !this.getItemInHand(InteractionHand.MAIN_HAND).isEmpty(); } @Override protected boolean canDispenserEquipIntoSlot(EquipmentSlot slot) { return false; } private boolean isOnPickupCooldown() { return this.getBrain().checkMemory(MemoryModuleType.ITEM_PICKUP_COOLDOWN_TICKS, MemoryStatus.VALUE_PRESENT); } @Override protected InteractionResult mobInteract(Player player, InteractionHand hand) { ItemStack itemStack = player.getItemInHand(hand); ItemStack itemStack2 = this.getItemInHand(InteractionHand.MAIN_HAND); if (this.isDancing() && itemStack.is(ItemTags.DUPLICATES_ALLAYS) && this.canDuplicate()) { this.duplicateAllay(); this.level().broadcastEntityEvent(this, (byte)18); this.level().playSound(player, this, SoundEvents.AMETHYST_BLOCK_CHIME, SoundSource.NEUTRAL, 2.0F, 1.0F); this.removeInteractionItem(player, itemStack); return InteractionResult.SUCCESS; } else if (itemStack2.isEmpty() && !itemStack.isEmpty()) { ItemStack itemStack3 = itemStack.copyWithCount(1); this.setItemInHand(InteractionHand.MAIN_HAND, itemStack3); this.removeInteractionItem(player, itemStack); this.level().playSound(player, this, SoundEvents.ALLAY_ITEM_GIVEN, SoundSource.NEUTRAL, 2.0F, 1.0F); this.getBrain().setMemory(MemoryModuleType.LIKED_PLAYER, player.getUUID()); return InteractionResult.SUCCESS; } else if (!itemStack2.isEmpty() && hand == InteractionHand.MAIN_HAND && itemStack.isEmpty()) { this.setItemSlot(EquipmentSlot.MAINHAND, ItemStack.EMPTY); this.level().playSound(player, this, SoundEvents.ALLAY_ITEM_TAKEN, SoundSource.NEUTRAL, 2.0F, 1.0F); this.swing(InteractionHand.MAIN_HAND); for (ItemStack itemStack4 : this.getInventory().removeAllItems()) { BehaviorUtils.throwItem(this, itemStack4, this.position()); } this.getBrain().eraseMemory(MemoryModuleType.LIKED_PLAYER); player.addItem(itemStack2); return InteractionResult.SUCCESS; } else { return super.mobInteract(player, hand); } } public void setJukeboxPlaying(BlockPos jukeboxPos, boolean jukeboxPlaying) { if (jukeboxPlaying) { if (!this.isDancing()) { this.jukeboxPos = jukeboxPos; this.setDancing(true); } } else if (jukeboxPos.equals(this.jukeboxPos) || this.jukeboxPos == null) { this.jukeboxPos = null; this.setDancing(false); } } @Override public SimpleContainer getInventory() { return this.inventory; } @Override protected Vec3i getPickupReach() { return ITEM_PICKUP_REACH; } @Override public boolean wantsToPickUp(ServerLevel level, ItemStack stack) { ItemStack itemStack = this.getItemInHand(InteractionHand.MAIN_HAND); return !itemStack.isEmpty() && level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) && this.inventory.canAddItem(stack) && this.allayConsidersItemEqual(itemStack, stack); } private boolean allayConsidersItemEqual(ItemStack first, ItemStack second) { return ItemStack.isSameItem(first, second) && !this.hasNonMatchingPotion(first, second); } private boolean hasNonMatchingPotion(ItemStack first, ItemStack second) { PotionContents potionContents = first.get(DataComponents.POTION_CONTENTS); PotionContents potionContents2 = second.get(DataComponents.POTION_CONTENTS); return !Objects.equals(potionContents, potionContents2); } @Override protected void pickUpItem(ServerLevel level, ItemEntity entity) { InventoryCarrier.pickUpItem(level, this, this, entity); } @Override protected void sendDebugPackets() { super.sendDebugPackets(); DebugPackets.sendEntityBrain(this); } @Override public boolean isFlapping() { return !this.onGround(); } @Override public void updateDynamicGameEventListener(BiConsumer, ServerLevel> listenerConsumer) { if (this.level() instanceof ServerLevel serverLevel) { listenerConsumer.accept(this.dynamicVibrationListener, serverLevel); listenerConsumer.accept(this.dynamicJukeboxListener, serverLevel); } } public boolean isDancing() { return this.entityData.get(DATA_DANCING); } public void setDancing(boolean dancing) { if (!this.level().isClientSide && this.isEffectiveAi() && (!dancing || !this.isPanicking())) { this.entityData.set(DATA_DANCING, dancing); } } private boolean shouldStopDancing() { return this.jukeboxPos == null || !this.jukeboxPos.closerToCenterThan(this.position(), GameEvent.JUKEBOX_PLAY.value().notificationRadius()) || !this.level().getBlockState(this.jukeboxPos).is(Blocks.JUKEBOX); } public float getHoldingItemAnimationProgress(float partialTick) { return Mth.lerp(partialTick, this.holdingItemAnimationTicks0, this.holdingItemAnimationTicks) / 5.0F; } public boolean isSpinning() { float f = this.dancingAnimationTicks % 55.0F; return f < 15.0F; } public float getSpinningProgress(float partialTick) { return Mth.lerp(partialTick, this.spinningAnimationTicks0, this.spinningAnimationTicks) / 15.0F; } @Override public boolean equipmentHasChanged(ItemStack oldItem, ItemStack newItem) { return !this.allayConsidersItemEqual(oldItem, newItem); } @Override protected void dropEquipment(ServerLevel level) { super.dropEquipment(level); this.inventory.removeAllItems().forEach(itemStackx -> this.spawnAtLocation(level, itemStackx)); ItemStack itemStack = this.getItemBySlot(EquipmentSlot.MAINHAND); if (!itemStack.isEmpty() && !EnchantmentHelper.has(itemStack, EnchantmentEffectComponents.PREVENT_EQUIPMENT_DROP)) { this.spawnAtLocation(level, itemStack); this.setItemSlot(EquipmentSlot.MAINHAND, ItemStack.EMPTY); } } @Override public boolean removeWhenFarAway(double distanceToClosestPlayer) { return false; } @Override public void addAdditionalSaveData(CompoundTag tag) { super.addAdditionalSaveData(tag); this.writeInventoryToTag(tag, this.registryAccess()); RegistryOps registryOps = this.registryAccess().createSerializationContext(NbtOps.INSTANCE); tag.store("listener", VibrationSystem.Data.CODEC, registryOps, this.vibrationData); tag.putLong("DuplicationCooldown", this.duplicationCooldown); } @Override public void readAdditionalSaveData(CompoundTag tag) { super.readAdditionalSaveData(tag); this.readInventoryFromTag(tag, this.registryAccess()); RegistryOps registryOps = this.registryAccess().createSerializationContext(NbtOps.INSTANCE); this.vibrationData = (VibrationSystem.Data)tag.read("listener", VibrationSystem.Data.CODEC, registryOps).orElseGet(VibrationSystem.Data::new); this.setDuplicationCooldown(tag.getIntOr("DuplicationCooldown", 0)); } @Override protected boolean shouldStayCloseToLeashHolder() { return false; } private void updateDuplicationCooldown() { if (!this.level().isClientSide() && this.duplicationCooldown > 0L) { this.setDuplicationCooldown(this.duplicationCooldown - 1L); } } private void setDuplicationCooldown(long duplicationCooldown) { this.duplicationCooldown = duplicationCooldown; this.entityData.set(DATA_CAN_DUPLICATE, duplicationCooldown == 0L); } private void duplicateAllay() { Allay allay = EntityType.ALLAY.create(this.level(), EntitySpawnReason.BREEDING); if (allay != null) { allay.snapTo(this.position()); allay.setPersistenceRequired(); allay.resetDuplicationCooldown(); this.resetDuplicationCooldown(); this.level().addFreshEntity(allay); } } private void resetDuplicationCooldown() { this.setDuplicationCooldown(6000L); } private boolean canDuplicate() { return this.entityData.get(DATA_CAN_DUPLICATE); } private void removeInteractionItem(Player player, ItemStack stack) { stack.consume(1, player); } @Override public Vec3 getLeashOffset() { return new Vec3(0.0, this.getEyeHeight() * 0.6, this.getBbWidth() * 0.1); } @Override public void handleEntityEvent(byte id) { if (id == 18) { for (int i = 0; i < 3; i++) { this.spawnHeartParticle(); } } else { super.handleEntityEvent(id); } } private void spawnHeartParticle() { double d = this.random.nextGaussian() * 0.02; double e = this.random.nextGaussian() * 0.02; double f = this.random.nextGaussian() * 0.02; this.level().addParticle(ParticleTypes.HEART, this.getRandomX(1.0), this.getRandomY() + 0.5, this.getRandomZ(1.0), d, e, f); } @Override public VibrationSystem.Data getVibrationData() { return this.vibrationData; } @Override public VibrationSystem.User getVibrationUser() { return this.vibrationUser; } class JukeboxListener implements GameEventListener { private final PositionSource listenerSource; private final int listenerRadius; public JukeboxListener(final PositionSource listenerSource, final int listenerRadius) { this.listenerSource = listenerSource; this.listenerRadius = listenerRadius; } @Override public PositionSource getListenerSource() { return this.listenerSource; } @Override public int getListenerRadius() { return this.listenerRadius; } @Override public boolean handleGameEvent(ServerLevel level, Holder gameEvent, Context context, Vec3 pos) { if (gameEvent.is(GameEvent.JUKEBOX_PLAY)) { Allay.this.setJukeboxPlaying(BlockPos.containing(pos), true); return true; } else if (gameEvent.is(GameEvent.JUKEBOX_STOP_PLAY)) { Allay.this.setJukeboxPlaying(BlockPos.containing(pos), false); return true; } else { return false; } } } class VibrationUser implements VibrationSystem.User { private static final int VIBRATION_EVENT_LISTENER_RANGE = 16; private final PositionSource positionSource = new EntityPositionSource(Allay.this, Allay.this.getEyeHeight()); @Override public int getListenerRadius() { return 16; } @Override public PositionSource getPositionSource() { return this.positionSource; } @Override public boolean canReceiveVibration(ServerLevel level, BlockPos pos, Holder gameEvent, Context context) { if (Allay.this.isNoAi()) { return false; } else { Optional optional = Allay.this.getBrain().getMemory(MemoryModuleType.LIKED_NOTEBLOCK_POSITION); if (optional.isEmpty()) { return true; } else { GlobalPos globalPos = (GlobalPos)optional.get(); return globalPos.isCloseEnough(level.dimension(), Allay.this.blockPosition(), 1024) && globalPos.pos().equals(pos); } } } @Override public void onReceiveVibration( ServerLevel level, BlockPos pos, Holder gameEvent, @Nullable Entity entity, @Nullable Entity playerEntity, float distance ) { if (gameEvent.is(GameEvent.NOTE_BLOCK_PLAY)) { AllayAi.hearNoteblock(Allay.this, new BlockPos(pos)); } } @Override public TagKey getListenableEvents() { return GameEventTags.ALLAY_CAN_LISTEN; } } }