diff --git a/src/main/java/net/minestom/server/entity/ai/goal/RangedAttackGoal.java b/src/main/java/net/minestom/server/entity/ai/goal/RangedAttackGoal.java index 8f45a1797..ad77821ac 100644 --- a/src/main/java/net/minestom/server/entity/ai/goal/RangedAttackGoal.java +++ b/src/main/java/net/minestom/server/entity/ai/goal/RangedAttackGoal.java @@ -4,12 +4,16 @@ import net.minestom.server.entity.Entity; import net.minestom.server.entity.EntityCreature; import net.minestom.server.entity.ai.GoalSelector; import net.minestom.server.entity.pathfinding.Navigator; +import net.minestom.server.entity.type.Projectile; +import net.minestom.server.entity.type.projectile.EntityArrow; import net.minestom.server.utils.Position; import net.minestom.server.utils.time.CooldownUtils; import net.minestom.server.utils.time.TimeUnit; import net.minestom.server.utils.validate.Check; import org.jetbrains.annotations.NotNull; +import java.util.function.BiFunction; + /** * Created by k.shandurenko on 22.02.2021 */ @@ -21,6 +25,9 @@ public class RangedAttackGoal extends GoalSelector { private final int attackRangeSquared; private final int desirableRangeSquared; private final boolean comeClose; + private final double spread; + + private BiFunction projectileGenerator; private boolean stop; @@ -30,18 +37,24 @@ public class RangedAttackGoal extends GoalSelector { * @param attackRange the allowed range the entity can shoot others. * @param desirableRange the desirable range: the entity will try to stay no further than this distance. * @param comeClose whether entity should go as close as possible to the target whether target is not in line of sight. + * @param spread shot spread (0 for best accuracy). * @param timeUnit the unit of the delay. */ - public RangedAttackGoal(@NotNull EntityCreature entityCreature, int delay, int attackRange, int desirableRange, boolean comeClose, @NotNull TimeUnit timeUnit) { + public RangedAttackGoal(@NotNull EntityCreature entityCreature, int delay, int attackRange, int desirableRange, boolean comeClose, double spread, @NotNull TimeUnit timeUnit) { super(entityCreature); this.delay = delay; this.timeUnit = timeUnit; this.attackRangeSquared = attackRange * attackRange; this.desirableRangeSquared = desirableRange * desirableRange; this.comeClose = comeClose; + this.spread = spread; Check.argCondition(desirableRange <= attackRange, "Desirable range can not exceed attack range!"); } + public void setProjectileGenerator(BiFunction projectileGenerator) { + this.projectileGenerator = projectileGenerator; + } + @Override public boolean shouldStart() { return findAndUpdateTarget() != null; @@ -66,7 +79,15 @@ public class RangedAttackGoal extends GoalSelector { if (distanceSquared <= this.attackRangeSquared) { if (!CooldownUtils.hasCooldown(time, this.lastShot, this.timeUnit, this.delay)) { if (this.entityCreature.hasLineOfSight(target)) { + Position to = target.getPosition().clone().add(0D, target.getEyeHeight(), 0D); + BiFunction projectileGenerator = this.projectileGenerator; + if (projectileGenerator == null) { + projectileGenerator = EntityArrow::new; + } + Projectile projectile = projectileGenerator.apply(this.entityCreature, new Position(0D, 0D, 0D)); + + Projectile.shoot(projectile, this.entityCreature, to, this.spread); this.lastShot = time; } else { comeClose = this.comeClose; diff --git a/src/main/java/net/minestom/server/entity/type/Projectile.java b/src/main/java/net/minestom/server/entity/type/Projectile.java index a211ec296..91c825f31 100644 --- a/src/main/java/net/minestom/server/entity/type/Projectile.java +++ b/src/main/java/net/minestom/server/entity/type/Projectile.java @@ -1,4 +1,55 @@ package net.minestom.server.entity.type; +import net.minestom.server.entity.Entity; +import net.minestom.server.event.entity.EntityShootEvent; +import net.minestom.server.utils.Position; +import net.minestom.server.utils.Vector; +import net.minestom.server.utils.validate.Check; +import org.jetbrains.annotations.NotNull; + +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + public interface Projectile { + + static void shoot(@NotNull Projectile projectile, @NotNull Entity shooter, Position to, double spread) { + EntityShootEvent event = new EntityShootEvent(shooter, projectile, to, spread); + shooter.callCancellableEvent(EntityShootEvent.class, event, () -> { + Position from = shooter.getPosition().clone().add(0D, shooter.getEyeHeight(), 0D); + shoot(projectile, from, to, event.getSpread()); + }); + } + + @SuppressWarnings("ConstantConditions") + static void shoot(@NotNull Projectile projectile, @NotNull Position from, @NotNull Position to, double spread) { + Check.argCondition(projectile instanceof Entity, "Projectile must be an instance of Entity!"); + Entity proj = (Entity) projectile; + double dx = to.getX() - from.getX(); + double dy = to.getY() - from.getY(); + double dz = to.getZ() - from.getZ(); + double xzLength = Math.sqrt(dx * dx + dz * dz); + dy += xzLength * 0.20000000298023224D; + + double length = Math.sqrt(dx * dx + dy * dy + dz * dz); + dx /= length; + dy /= length; + dz /= length; + Random random = ThreadLocalRandom.current(); + spread *= 0.007499999832361937D; + dx += random.nextGaussian() * spread; + dy += random.nextGaussian() * spread; + dz += random.nextGaussian() * spread; + dx *= 2; + dy *= 2; + dz *= 2; + Vector velocity = proj.getVelocity(); + velocity.setX(dx); + velocity.setY(dy); + velocity.setZ(dz); + xzLength = Math.sqrt(dx * dx + dz * dz); + double yaw = Math.max(Math.abs(dx), Math.abs(dz)); + double pitch = Math.max(Math.abs(dy), Math.abs(xzLength)); + proj.setView((float) (yaw * Math.toDegrees(1D)), (float) (pitch * Math.toDegrees(1D))); + } + } diff --git a/src/main/java/net/minestom/server/event/entity/EntityShootEvent.java b/src/main/java/net/minestom/server/event/entity/EntityShootEvent.java new file mode 100644 index 000000000..440268c07 --- /dev/null +++ b/src/main/java/net/minestom/server/event/entity/EntityShootEvent.java @@ -0,0 +1,74 @@ +package net.minestom.server.event.entity; + +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.type.Projectile; +import net.minestom.server.event.CancellableEvent; +import net.minestom.server.event.EntityEvent; +import net.minestom.server.utils.Position; +import org.jetbrains.annotations.NotNull; + +/** + * Called with {@link Projectile#shoot(Projectile, Entity, Position, double)}. + */ +public class EntityShootEvent extends EntityEvent implements CancellableEvent { + + private final Projectile projectile; + private final Position to; + private double spread; + + private boolean cancelled; + + public EntityShootEvent(@NotNull Entity entity, @NotNull Projectile projectile, @NotNull Position to, double spread) { + super(entity); + this.projectile = projectile; + this.to = to; + this.spread = spread; + } + + /** + * Gets the projectile. + * + * @return the projectile. + */ + public Projectile getProjectile() { + return this.projectile; + } + + /** + * Gets the position projectile was shot to. + * + * @return the position projectile was shot to. + */ + public Position getTo() { + return this.to; + } + + /** + * Gets shot spread. + * + * @return shot spread. + */ + public double getSpread() { + return this.spread; + } + + /** + * Sets shot spread. + * + * @param spread shot spread. + */ + public void setSpread(double spread) { + this.spread = spread; + } + + @Override + public boolean isCancelled() { + return this.cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } + +}