package net.minecraft.world.entity; import java.util.List; import java.util.Optional; import net.minecraft.core.BlockPos; import net.minecraft.nbt.CompoundTag; 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.SoundSource; import net.minecraft.tags.FluidTags; import net.minecraft.util.ExtraCodecs; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.enchantment.EnchantedItemInUse; import net.minecraft.world.item.enchantment.EnchantmentEffectComponents; import net.minecraft.world.item.enchantment.EnchantmentHelper; import net.minecraft.world.level.Level; import net.minecraft.world.level.entity.EntityTypeTest; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.Vec3; import org.jetbrains.annotations.Nullable; public class ExperienceOrb extends Entity { protected static final EntityDataAccessor DATA_VALUE = SynchedEntityData.defineId(ExperienceOrb.class, EntityDataSerializers.INT); private static final int LIFETIME = 6000; private static final int ENTITY_SCAN_PERIOD = 20; private static final int MAX_FOLLOW_DIST = 8; private static final int ORB_GROUPS_PER_AREA = 40; private static final double ORB_MERGE_DISTANCE = 0.5; private static final short DEFAULT_HEALTH = 5; private static final short DEFAULT_AGE = 0; private static final short DEFAULT_VALUE = 0; private static final int DEFAULT_COUNT = 1; private int age = 0; private int health = 5; private int count = 1; @Nullable private Player followingPlayer; private final InterpolationHandler interpolation = new InterpolationHandler(this); public ExperienceOrb(Level level, double x, double y, double z, int value) { this(EntityType.EXPERIENCE_ORB, level); this.setPos(x, y, z); if (!this.level().isClientSide) { this.setYRot((float)(this.random.nextDouble() * 360.0)); this.setDeltaMovement((this.random.nextDouble() * 0.2F - 0.1F) * 2.0, this.random.nextDouble() * 0.2 * 2.0, (this.random.nextDouble() * 0.2F - 0.1F) * 2.0); } this.setValue(value); } public ExperienceOrb(EntityType entityType, Level level) { super(entityType, level); } @Override protected Entity.MovementEmission getMovementEmission() { return Entity.MovementEmission.NONE; } @Override protected void defineSynchedData(Builder builder) { builder.define(DATA_VALUE, 0); } @Override protected double getDefaultGravity() { return 0.03; } @Override public void tick() { this.interpolation.interpolate(); if (this.firstTick && this.level().isClientSide) { this.firstTick = false; } else { super.tick(); boolean bl = !this.level().noCollision(this.getBoundingBox()); if (this.isEyeInFluid(FluidTags.WATER)) { this.setUnderwaterMovement(); } else if (!bl) { this.applyGravity(); } if (this.level().getFluidState(this.blockPosition()).is(FluidTags.LAVA)) { this.setDeltaMovement((this.random.nextFloat() - this.random.nextFloat()) * 0.2F, 0.2F, (this.random.nextFloat() - this.random.nextFloat()) * 0.2F); } if (this.tickCount % 20 == 1) { this.scanForMerges(); } this.followNearbyPlayer(); if (this.followingPlayer == null && !this.level().isClientSide && bl) { this.moveTowardsClosestSpace(this.getX(), (this.getBoundingBox().minY + this.getBoundingBox().maxY) / 2.0, this.getZ()); this.hasImpulse = true; } double d = this.getDeltaMovement().y; this.move(MoverType.SELF, this.getDeltaMovement()); this.applyEffectsFromBlocks(); float f = 0.98F; if (this.onGround()) { f = this.level().getBlockState(this.getBlockPosBelowThatAffectsMyMovement()).getBlock().getFriction() * 0.98F; } this.setDeltaMovement(this.getDeltaMovement().scale(f)); if (this.verticalCollisionBelow && d < -this.getGravity()) { this.setDeltaMovement(new Vec3(this.getDeltaMovement().x, -d * 0.4, this.getDeltaMovement().z)); } this.age++; if (this.age >= 6000) { this.discard(); } } } private void followNearbyPlayer() { if (this.followingPlayer == null || this.followingPlayer.isSpectator() || this.followingPlayer.distanceToSqr(this) > 64.0) { Player player = this.level().getNearestPlayer(this, 8.0); if (player != null && !player.isSpectator() && !player.isDeadOrDying()) { this.followingPlayer = player; } else { this.followingPlayer = null; } } if (this.followingPlayer != null) { Vec3 vec3 = new Vec3( this.followingPlayer.getX() - this.getX(), this.followingPlayer.getY() + this.followingPlayer.getEyeHeight() / 2.0 - this.getY(), this.followingPlayer.getZ() - this.getZ() ); double d = vec3.lengthSqr(); double e = 1.0 - Math.sqrt(d) / 8.0; this.setDeltaMovement(this.getDeltaMovement().add(vec3.normalize().scale(e * e * 0.1))); } } @Override public BlockPos getBlockPosBelowThatAffectsMyMovement() { return this.getOnPos(0.999999F); } private void scanForMerges() { if (this.level() instanceof ServerLevel) { for (ExperienceOrb experienceOrb : this.level() .getEntities(EntityTypeTest.forClass(ExperienceOrb.class), this.getBoundingBox().inflate(0.5), this::canMerge)) { this.merge(experienceOrb); } } } public static void award(ServerLevel level, Vec3 pos, int amount) { while (amount > 0) { int i = getExperienceValue(amount); amount -= i; if (!tryMergeToExisting(level, pos, i)) { level.addFreshEntity(new ExperienceOrb(level, pos.x(), pos.y(), pos.z(), i)); } } } private static boolean tryMergeToExisting(ServerLevel level, Vec3 pos, int amount) { AABB aABB = AABB.ofSize(pos, 1.0, 1.0, 1.0); int i = level.getRandom().nextInt(40); List list = level.getEntities(EntityTypeTest.forClass(ExperienceOrb.class), aABB, experienceOrbx -> canMerge(experienceOrbx, i, amount)); if (!list.isEmpty()) { ExperienceOrb experienceOrb = (ExperienceOrb)list.get(0); experienceOrb.count++; experienceOrb.age = 0; return true; } else { return false; } } private boolean canMerge(ExperienceOrb orb) { return orb != this && canMerge(orb, this.getId(), this.getValue()); } private static boolean canMerge(ExperienceOrb orb, int amount, int other) { return !orb.isRemoved() && (orb.getId() - amount) % 40 == 0 && orb.getValue() == other; } private void merge(ExperienceOrb orb) { this.count = this.count + orb.count; this.age = Math.min(this.age, orb.age); orb.discard(); } private void setUnderwaterMovement() { Vec3 vec3 = this.getDeltaMovement(); this.setDeltaMovement(vec3.x * 0.99F, Math.min(vec3.y + 5.0E-4F, 0.06F), vec3.z * 0.99F); } @Override protected void doWaterSplashEffect() { } @Override public final boolean hurtClient(DamageSource damageSource) { return !this.isInvulnerableToBase(damageSource); } @Override public final boolean hurtServer(ServerLevel level, DamageSource damageSource, float amount) { if (this.isInvulnerableToBase(damageSource)) { return false; } else { this.markHurt(); this.health = (int)(this.health - amount); if (this.health <= 0) { this.discard(); } return true; } } @Override public void addAdditionalSaveData(CompoundTag tag) { tag.putShort("Health", (short)this.health); tag.putShort("Age", (short)this.age); tag.putShort("Value", (short)this.getValue()); tag.putInt("Count", this.count); } @Override public void readAdditionalSaveData(CompoundTag tag) { this.health = tag.getShortOr("Health", (short)5); this.age = tag.getShortOr("Age", (short)0); this.setValue(tag.getShortOr("Value", (short)0)); this.count = (Integer)tag.read("Count", ExtraCodecs.POSITIVE_INT).orElse(1); } @Override public void playerTouch(Player player) { if (player instanceof ServerPlayer serverPlayer) { if (player.takeXpDelay == 0) { player.takeXpDelay = 2; player.take(this, 1); int i = this.repairPlayerItems(serverPlayer, this.getValue()); if (i > 0) { player.giveExperiencePoints(i); } this.count--; if (this.count == 0) { this.discard(); } } } } private int repairPlayerItems(ServerPlayer player, int value) { Optional optional = EnchantmentHelper.getRandomItemWith(EnchantmentEffectComponents.REPAIR_WITH_XP, player, ItemStack::isDamaged); if (optional.isPresent()) { ItemStack itemStack = ((EnchantedItemInUse)optional.get()).itemStack(); int i = EnchantmentHelper.modifyDurabilityToRepairFromXp(player.serverLevel(), itemStack, value); int j = Math.min(i, itemStack.getDamageValue()); itemStack.setDamageValue(itemStack.getDamageValue() - j); if (j > 0) { int k = value - j * value / i; if (k > 0) { return this.repairPlayerItems(player, k); } } return 0; } else { return value; } } /** * Returns the XP value of this XP orb. */ public int getValue() { return this.entityData.get(DATA_VALUE); } private void setValue(int value) { this.entityData.set(DATA_VALUE, value); } /** * Returns a number from 1 to 10 based on how much XP this orb is worth. This is used by RenderXPOrb to determine what texture to use. */ public int getIcon() { int i = this.getValue(); if (i >= 2477) { return 10; } else if (i >= 1237) { return 9; } else if (i >= 617) { return 8; } else if (i >= 307) { return 7; } else if (i >= 149) { return 6; } else if (i >= 73) { return 5; } else if (i >= 37) { return 4; } else if (i >= 17) { return 3; } else if (i >= 7) { return 2; } else { return i >= 3 ? 1 : 0; } } /** * Get a fragment of the maximum experience points value for the supplied value of experience points value. */ public static int getExperienceValue(int expValue) { if (expValue >= 2477) { return 2477; } else if (expValue >= 1237) { return 1237; } else if (expValue >= 617) { return 617; } else if (expValue >= 307) { return 307; } else if (expValue >= 149) { return 149; } else if (expValue >= 73) { return 73; } else if (expValue >= 37) { return 37; } else if (expValue >= 17) { return 17; } else if (expValue >= 7) { return 7; } else { return expValue >= 3 ? 3 : 1; } } @Override public boolean isAttackable() { return false; } @Override public SoundSource getSoundSource() { return SoundSource.AMBIENT; } @Override public InterpolationHandler getInterpolation() { return this.interpolation; } }