package net.minecraft.world.entity.projectile; import com.google.common.base.MoreObjects; import it.unimi.dsi.fastutil.doubles.DoubleDoubleImmutablePair; import java.util.Objects; import java.util.UUID; import java.util.function.Consumer; import net.minecraft.core.BlockPos; import net.minecraft.core.UUIDUtil; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.ClientGamePacketListener; import net.minecraft.network.protocol.game.ClientboundAddEntityPacket; import net.minecraft.server.level.ServerEntity; import net.minecraft.server.level.ServerLevel; import net.minecraft.tags.EntityTypeTags; import net.minecraft.util.Mth; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntitySelector; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.TraceableEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; 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.state.BlockState; import net.minecraft.world.level.gameevent.GameEvent; import net.minecraft.world.level.gameevent.GameEvent.Context; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.EntityHitResult; import net.minecraft.world.phys.HitResult; import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.HitResult.Type; import org.jetbrains.annotations.Nullable; public abstract class Projectile extends Entity implements TraceableEntity { private static final boolean DEFAULT_LEFT_OWNER = false; private static final boolean DEFAULT_HAS_BEEN_SHOT = false; @Nullable private UUID ownerUUID; @Nullable private Entity cachedOwner; private boolean leftOwner = false; private boolean hasBeenShot = false; @Nullable private Entity lastDeflectedBy; Projectile(EntityType entityType, Level level) { super(entityType, level); } public void setOwner(@Nullable Entity owner) { if (owner != null) { this.ownerUUID = owner.getUUID(); this.cachedOwner = owner; } } @Nullable @Override public Entity getOwner() { if (this.cachedOwner != null && !this.cachedOwner.isRemoved()) { return this.cachedOwner; } else if (this.ownerUUID != null) { this.cachedOwner = this.findOwner(this.ownerUUID); return this.cachedOwner; } else { return null; } } @Nullable protected Entity findOwner(UUID entityUuid) { return this.level() instanceof ServerLevel serverLevel ? serverLevel.getEntity(entityUuid) : null; } public Entity getEffectSource() { return MoreObjects.firstNonNull(this.getOwner(), this); } @Override protected void addAdditionalSaveData(CompoundTag tag) { tag.storeNullable("Owner", UUIDUtil.CODEC, this.ownerUUID); if (this.leftOwner) { tag.putBoolean("LeftOwner", true); } tag.putBoolean("HasBeenShot", this.hasBeenShot); } protected boolean ownedBy(Entity entity) { return entity.getUUID().equals(this.ownerUUID); } @Override protected void readAdditionalSaveData(CompoundTag tag) { this.setOwnerThroughUUID((UUID)tag.read("Owner", UUIDUtil.CODEC).orElse(null)); this.leftOwner = tag.getBooleanOr("LeftOwner", false); this.hasBeenShot = tag.getBooleanOr("HasBeenShot", false); } protected void setOwnerThroughUUID(@Nullable UUID uuid) { if (!Objects.equals(this.ownerUUID, uuid)) { this.ownerUUID = uuid; this.cachedOwner = uuid != null ? this.findOwner(uuid) : null; } } @Override public void restoreFrom(Entity entity) { super.restoreFrom(entity); if (entity instanceof Projectile projectile) { this.ownerUUID = projectile.ownerUUID; this.cachedOwner = projectile.cachedOwner; } } @Override public void tick() { if (!this.hasBeenShot) { this.gameEvent(GameEvent.PROJECTILE_SHOOT, this.getOwner()); this.hasBeenShot = true; } if (!this.leftOwner) { this.leftOwner = this.checkLeftOwner(); } super.tick(); } private boolean checkLeftOwner() { Entity entity = this.getOwner(); if (entity != null) { AABB aABB = this.getBoundingBox().expandTowards(this.getDeltaMovement()).inflate(1.0); return entity.getRootVehicle().getSelfAndPassengers().filter(EntitySelector.CAN_BE_PICKED).noneMatch(entityx -> aABB.intersects(entityx.getBoundingBox())); } else { return true; } } public Vec3 getMovementToShoot(double x, double y, double z, float velocity, float inaccuracy) { return new Vec3(x, y, z) .normalize() .add(this.random.triangle(0.0, 0.0172275 * inaccuracy), this.random.triangle(0.0, 0.0172275 * inaccuracy), this.random.triangle(0.0, 0.0172275 * inaccuracy)) .scale(velocity); } /** * Similar to setArrowHeading, it's point the throwable entity to a x, y, z direction. */ public void shoot(double x, double y, double z, float velocity, float inaccuracy) { Vec3 vec3 = this.getMovementToShoot(x, y, z, velocity, inaccuracy); this.setDeltaMovement(vec3); this.hasImpulse = true; double d = vec3.horizontalDistance(); this.setYRot((float)(Mth.atan2(vec3.x, vec3.z) * 180.0F / (float)Math.PI)); this.setXRot((float)(Mth.atan2(vec3.y, d) * 180.0F / (float)Math.PI)); this.yRotO = this.getYRot(); this.xRotO = this.getXRot(); } public void shootFromRotation(Entity shooter, float x, float y, float z, float velocity, float inaccuracy) { float f = -Mth.sin(y * (float) (Math.PI / 180.0)) * Mth.cos(x * (float) (Math.PI / 180.0)); float g = -Mth.sin((x + z) * (float) (Math.PI / 180.0)); float h = Mth.cos(y * (float) (Math.PI / 180.0)) * Mth.cos(x * (float) (Math.PI / 180.0)); this.shoot(f, g, h, velocity, inaccuracy); Vec3 vec3 = shooter.getKnownMovement(); this.setDeltaMovement(this.getDeltaMovement().add(vec3.x, shooter.onGround() ? 0.0 : vec3.y, vec3.z)); } @Override public void onAboveBubbleColumn(boolean downwards, BlockPos pos) { double d = downwards ? -0.03 : 0.1; this.setDeltaMovement(this.getDeltaMovement().add(0.0, d, 0.0)); sendBubbleColumnParticles(this.level(), pos); } @Override public void onInsideBubbleColumn(boolean downwards) { double d = downwards ? -0.03 : 0.06; this.setDeltaMovement(this.getDeltaMovement().add(0.0, d, 0.0)); this.resetFallDistance(); } public static T spawnProjectileFromRotation( Projectile.ProjectileFactory factory, ServerLevel level, ItemStack spawnedFrom, LivingEntity owner, float z, float velocity, float innaccuracy ) { return spawnProjectile( factory.create(level, owner, spawnedFrom), level, spawnedFrom, projectile -> projectile.shootFromRotation(owner, owner.getXRot(), owner.getYRot(), z, velocity, innaccuracy) ); } public static T spawnProjectileUsingShoot( Projectile.ProjectileFactory factory, ServerLevel level, ItemStack spawnedFrom, LivingEntity owner, double x, double y, double z, float velocity, float inaccuracy ) { return spawnProjectile(factory.create(level, owner, spawnedFrom), level, spawnedFrom, projectile -> projectile.shoot(x, y, z, velocity, inaccuracy)); } public static T spawnProjectileUsingShoot( T projectile, ServerLevel level, ItemStack spawnedFrom, double x, double y, double z, float velocity, float inaccuracy ) { return spawnProjectile(projectile, level, spawnedFrom, projectile2 -> projectile.shoot(x, y, z, velocity, inaccuracy)); } public static T spawnProjectile(T projectile, ServerLevel level, ItemStack spawnedFrom) { return spawnProjectile(projectile, level, spawnedFrom, projectilex -> {}); } public static T spawnProjectile(T projectile, ServerLevel level, ItemStack stack, Consumer adapter) { adapter.accept(projectile); level.addFreshEntity(projectile); projectile.applyOnProjectileSpawned(level, stack); return projectile; } public void applyOnProjectileSpawned(ServerLevel level, ItemStack spawnedFrom) { EnchantmentHelper.onProjectileSpawned(level, spawnedFrom, this, item -> {}); if (this instanceof AbstractArrow abstractArrow) { ItemStack itemStack = abstractArrow.getWeaponItem(); if (itemStack != null && !itemStack.isEmpty() && !spawnedFrom.getItem().equals(itemStack.getItem())) { EnchantmentHelper.onProjectileSpawned(level, itemStack, this, abstractArrow::onItemBreak); } } } protected ProjectileDeflection hitTargetOrDeflectSelf(HitResult hitResult) { if (hitResult.getType() == Type.ENTITY) { EntityHitResult entityHitResult = (EntityHitResult)hitResult; Entity entity = entityHitResult.getEntity(); ProjectileDeflection projectileDeflection = entity.deflection(this); if (projectileDeflection != ProjectileDeflection.NONE) { if (entity != this.lastDeflectedBy && this.deflect(projectileDeflection, entity, this.getOwner(), false)) { this.lastDeflectedBy = entity; } return projectileDeflection; } } else if (this.shouldBounceOnWorldBorder() && hitResult instanceof BlockHitResult blockHitResult && blockHitResult.isWorldBorderHit()) { ProjectileDeflection projectileDeflection2 = ProjectileDeflection.REVERSE; if (this.deflect(projectileDeflection2, null, this.getOwner(), false)) { this.setDeltaMovement(this.getDeltaMovement().scale(0.2)); return projectileDeflection2; } } this.onHit(hitResult); return ProjectileDeflection.NONE; } protected boolean shouldBounceOnWorldBorder() { return false; } public boolean deflect(ProjectileDeflection deflection, @Nullable Entity entity, @Nullable Entity owner, boolean deflectedByPlayer) { deflection.deflect(this, entity, this.random); if (!this.level().isClientSide) { this.setOwner(owner); this.onDeflection(entity, deflectedByPlayer); } return true; } protected void onDeflection(@Nullable Entity entity, boolean deflectedByPlayer) { } protected void onItemBreak(Item item) { } /** * Called when this EntityFireball hits a block or entity. */ protected void onHit(HitResult result) { Type type = result.getType(); if (type == Type.ENTITY) { EntityHitResult entityHitResult = (EntityHitResult)result; Entity entity = entityHitResult.getEntity(); if (entity.getType().is(EntityTypeTags.REDIRECTABLE_PROJECTILE) && entity instanceof Projectile projectile) { projectile.deflect(ProjectileDeflection.AIM_DEFLECT, this.getOwner(), this.getOwner(), true); } this.onHitEntity(entityHitResult); this.level().gameEvent(GameEvent.PROJECTILE_LAND, result.getLocation(), Context.of(this, null)); } else if (type == Type.BLOCK) { BlockHitResult blockHitResult = (BlockHitResult)result; this.onHitBlock(blockHitResult); BlockPos blockPos = blockHitResult.getBlockPos(); this.level().gameEvent(GameEvent.PROJECTILE_LAND, blockPos, Context.of(this, this.level().getBlockState(blockPos))); } } /** * Called when the arrow hits an entity */ protected void onHitEntity(EntityHitResult result) { } protected void onHitBlock(BlockHitResult result) { BlockState blockState = this.level().getBlockState(result.getBlockPos()); blockState.onProjectileHit(this.level(), blockState, result, this); } protected boolean canHitEntity(Entity target) { if (!target.canBeHitByProjectile()) { return false; } else { Entity entity = this.getOwner(); return entity == null || this.leftOwner || !entity.isPassengerOfSameVehicle(target); } } protected void updateRotation() { Vec3 vec3 = this.getDeltaMovement(); double d = vec3.horizontalDistance(); this.setXRot(lerpRotation(this.xRotO, (float)(Mth.atan2(vec3.y, d) * 180.0F / (float)Math.PI))); this.setYRot(lerpRotation(this.yRotO, (float)(Mth.atan2(vec3.x, vec3.z) * 180.0F / (float)Math.PI))); } protected static float lerpRotation(float currentRotation, float targetRotation) { while (targetRotation - currentRotation < -180.0F) { currentRotation -= 360.0F; } while (targetRotation - currentRotation >= 180.0F) { currentRotation += 360.0F; } return Mth.lerp(0.2F, currentRotation, targetRotation); } @Override public Packet getAddEntityPacket(ServerEntity entity) { Entity entity2 = this.getOwner(); return new ClientboundAddEntityPacket(this, entity, entity2 == null ? 0 : entity2.getId()); } @Override public void recreateFromPacket(ClientboundAddEntityPacket packet) { super.recreateFromPacket(packet); Entity entity = this.level().getEntity(packet.getData()); if (entity != null) { this.setOwner(entity); } } @Override public boolean mayInteract(ServerLevel level, BlockPos pos) { Entity entity = this.getOwner(); return entity instanceof Player ? entity.mayInteract(level, pos) : entity == null || level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); } public boolean mayBreak(ServerLevel level) { return this.getType().is(EntityTypeTags.IMPACT_PROJECTILES) && level.getGameRules().getBoolean(GameRules.RULE_PROJECTILESCANBREAKBLOCKS); } @Override public boolean isPickable() { return this.getType().is(EntityTypeTags.REDIRECTABLE_PROJECTILE); } @Override public float getPickRadius() { return this.isPickable() ? 1.0F : 0.0F; } public DoubleDoubleImmutablePair calculateHorizontalHurtKnockbackDirection(LivingEntity entity, DamageSource damageSource) { double d = this.getDeltaMovement().x; double e = this.getDeltaMovement().z; return DoubleDoubleImmutablePair.of(d, e); } @Override public int getDimensionChangingDelay() { return 2; } @Override public boolean hurtServer(ServerLevel level, DamageSource damageSource, float amount) { if (!this.isInvulnerableToBase(damageSource)) { this.markHurt(); } return false; } @FunctionalInterface public interface ProjectileFactory { T create(ServerLevel serverLevel, LivingEntity livingEntity, ItemStack itemStack); } }