minecraft-src/net/minecraft/world/entity/ExperienceOrb.java
2025-07-04 03:45:38 +03:00

362 lines
11 KiB
Java

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<Integer> 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<? extends ExperienceOrb> 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<ExperienceOrb> 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<EnchantedItemInUse> 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;
}
}