407 lines
14 KiB
Java
407 lines
14 KiB
Java
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<? extends Projectile> 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 extends Projectile> T spawnProjectileFromRotation(
|
|
Projectile.ProjectileFactory<T> 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 extends Projectile> T spawnProjectileUsingShoot(
|
|
Projectile.ProjectileFactory<T> 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 extends Projectile> 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 extends Projectile> T spawnProjectile(T projectile, ServerLevel level, ItemStack spawnedFrom) {
|
|
return spawnProjectile(projectile, level, spawnedFrom, projectilex -> {});
|
|
}
|
|
|
|
public static <T extends Projectile> T spawnProjectile(T projectile, ServerLevel level, ItemStack stack, Consumer<T> 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<ClientGamePacketListener> 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 extends Projectile> {
|
|
T create(ServerLevel serverLevel, LivingEntity livingEntity, ItemStack itemStack);
|
|
}
|
|
}
|