Merge pull request #146 from RinesThaix/goals

Arrows and RangedAttackGoal
This commit is contained in:
TheMode 2021-02-22 13:58:43 +01:00 committed by GitHub
commit 71f1e51df3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 596 additions and 6 deletions

View File

@ -91,7 +91,8 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
protected Entity vehicle;
// Velocity
protected Vector velocity = new Vector(); // Movement in block per second
protected Vector velocity = new Vector(); // Movement in block per second
protected boolean hasPhysics = true;
protected double gravityDragPerTick;
protected double gravityAcceleration;
@ -483,7 +484,11 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
getVelocity().getZ() / tps
);
this.onGround = CollisionUtils.handlePhysics(this, deltaPos, newPosition, newVelocityOut);
if (this.hasPhysics) {
this.onGround = CollisionUtils.handlePhysics(this, deltaPos, newPosition, newVelocityOut);
} else {
newVelocityOut = deltaPos;
}
// World border collision
final Position finalVelocityPosition = CollisionUtils.applyWorldBorder(instance, position, newPosition);
@ -862,6 +867,16 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
return getPosition().getDistance(entity.getPosition());
}
/**
* Gets the distance squared between two entities.
*
* @param entity the entity to get the distance from
* @return the distance squared between this and {@code entity}
*/
public double getDistanceSquared(@NotNull Entity entity) {
return getPosition().getDistanceSquared(entity.getPosition());
}
/**
* Gets the entity vehicle or null.
*

View File

@ -22,6 +22,7 @@ import net.minestom.server.sound.Sound;
import net.minestom.server.sound.SoundCategory;
import net.minestom.server.utils.BlockPosition;
import net.minestom.server.utils.Position;
import net.minestom.server.utils.Vector;
import net.minestom.server.utils.block.BlockIterator;
import net.minestom.server.utils.time.CooldownUtils;
import net.minestom.server.utils.time.TimeUnit;
@ -590,6 +591,30 @@ public abstract class LivingEntity extends Entity implements EquipmentHandler {
return blocks;
}
/**
* Checks whether the current entity has line of sight to the given one.
* If so, it doesn't mean that the given entity is IN line of sight of the current,
* but the current one can rotate so that it will be true.
*
* @param entity the entity to be checked.
* @return if the current entity has line of sight to the given one.
*/
public boolean hasLineOfSight(Entity entity) {
Vector start = getPosition().toVector().add(0D, getEyeHeight(), 0D);
Vector end = entity.getPosition().toVector().add(0D, getEyeHeight(), 0D);
Vector direction = end.subtract(start);
int maxDistance = (int) Math.ceil(direction.length());
Iterator<BlockPosition> it = new BlockIterator(start, direction.normalize(), 0D, maxDistance);
while (it.hasNext()) {
Block block = Block.fromStateId(getInstance().getBlockStateId(it.next()));
if (!block.isAir() && !block.isLiquid()) {
return false;
}
}
return true;
}
/**
* Gets the target (not-air) {@link BlockPosition} of the entity.
*

View File

@ -0,0 +1,121 @@
package net.minestom.server.entity.ai.goal;
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;
public class RangedAttackGoal extends GoalSelector {
private long lastShot;
private final int delay;
private final TimeUnit timeUnit;
private final int attackRangeSquared;
private final int desirableRangeSquared;
private final boolean comeClose;
private final double power;
private final double spread;
private BiFunction<Entity, Position, Projectile> projectileGenerator;
private boolean stop;
/**
* @param entityCreature the entity to add the goal to.
* @param delay the delay between each shots.
* @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 power shot power (1 for normal).
* @param timeUnit the unit of the delay.
*/
public RangedAttackGoal(@NotNull EntityCreature entityCreature, int delay, int attackRange, int desirableRange, boolean comeClose, double power, 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.power = power;
this.spread = spread;
Check.argCondition(desirableRange > attackRange, "Desirable range can not exceed attack range!");
}
public void setProjectileGenerator(BiFunction<Entity, Position, Projectile> projectileGenerator) {
this.projectileGenerator = projectileGenerator;
}
@Override
public boolean shouldStart() {
return findAndUpdateTarget() != null;
}
@Override
public void start() {
Entity target = findAndUpdateTarget();
Check.notNull(target, "The target is not expected to be null!");
this.entityCreature.getNavigator().setPathTo(target.getPosition());
}
@Override
public void tick(long time) {
Entity target = findAndUpdateTarget();
if (target == null) {
this.stop = true;
return;
}
double distanceSquared = this.entityCreature.getDistanceSquared(target);
boolean comeClose = false;
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<Entity, Position, Projectile> 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.power, this.spread);
this.lastShot = time;
} else {
comeClose = this.comeClose;
}
}
}
Navigator navigator = this.entityCreature.getNavigator();
Position pathPosition = navigator.getPathPosition();
if (!comeClose && distanceSquared <= this.desirableRangeSquared) {
if (pathPosition != null) {
navigator.setPathTo(null);
}
return;
}
Position targetPosition = target.getPosition();
if (pathPosition == null || !pathPosition.isSimilar(targetPosition)) {
navigator.setPathTo(targetPosition);
}
}
@Override
public boolean shouldEnd() {
return this.stop;
}
@Override
public void end() {
// Stop following the target
this.entityCreature.getNavigator().setPathTo(null);
}
}

View File

@ -1,4 +1,58 @@
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 power, double spread) {
Check.argCondition(!(projectile instanceof Entity), "Projectile must be an instance of Entity!");
EntityShootEvent event = new EntityShootEvent(shooter, projectile, to, power, spread);
shooter.callEvent(EntityShootEvent.class, event);
if (event.isCancelled()) {
Entity proj = (Entity) projectile;
proj.remove();
return;
}
Position from = shooter.getPosition().clone().add(0D, shooter.getEyeHeight(), 0D);
shoot(projectile, from, to, event.getPower(), event.getSpread());
}
@SuppressWarnings("ConstantConditions")
static void shoot(@NotNull Projectile projectile, @NotNull Position from, @NotNull Position to, double power, 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;
Vector velocity = proj.getVelocity();
velocity.setX(dx);
velocity.setY(dy);
velocity.setZ(dz);
velocity.multiply(20 * power);
proj.setView(
(float) Math.toDegrees(Math.atan2(dx, dz)),
(float) Math.toDegrees(Math.atan2(dy, Math.sqrt(dx * dx + dz * dz)))
);
}
}

View File

@ -0,0 +1,177 @@
package net.minestom.server.entity.type.projectile;
import net.minestom.server.entity.*;
import net.minestom.server.entity.damage.DamageType;
import net.minestom.server.entity.type.Projectile;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.block.Block;
import net.minestom.server.utils.BlockPosition;
import net.minestom.server.utils.Position;
import net.minestom.server.utils.Vector;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.Optional;
import java.util.stream.Collectors;
public class EntityAbstractArrow extends ObjectEntity implements Projectile {
private final static byte CRITICAL_BIT = 0x01;
private final static byte NO_CLIP_BIT = 0x02;
private final Entity shooter;
EntityAbstractArrow(@Nullable Entity shooter, @NotNull EntityType entityType, @NotNull Position spawnPosition) {
super(entityType, spawnPosition);
this.shooter = shooter;
super.hasPhysics = false;
setBoundingBox(.5F, .5F, .5F);
}
@Override
public void tick(long time) {
Position posBefore = getPosition().clone();
super.tick(time);
Position posNow = getPosition().clone();
if (isStuck(posBefore, posNow)) {
if (super.onGround) {
return;
}
super.onGround = true;
getVelocity().zero();
sendPacketToViewersAndSelf(getVelocityPacket());
setNoGravity(true);
} else {
if (!super.onGround) {
return;
}
super.onGround = false;
setNoGravity(false);
}
}
/**
* Checks whether an arrow is stuck in block / hit an entity.
*
* @param pos position right before current tick.
* @param posNow position after current tick.
* @return if an arrow is stuck in block / hit an entity.
*/
@SuppressWarnings("ConstantConditions")
private boolean isStuck(Position pos, Position posNow) {
if (pos.isSimilar(posNow)) {
return true;
}
Instance instance = getInstance();
Chunk chunk = null;
Collection<Entity> entities = null;
/*
What we're about to do is to discretely jump from the previous position to the new one.
For each point we will be checking blocks and entities we're in.
*/
double part = .25D; // half of the bounding box
Vector dir = posNow.toVector().subtract(pos.toVector());
int parts = (int) Math.ceil(dir.length() / part);
Position direction = dir.normalize().multiply(part).toPosition();
for (int i = 0; i < parts; ++i) {
// If we're at last part, we can't just add another direction-vector, because we can exceed end point.
if (i == parts - 1) {
pos.setX(posNow.getX());
pos.setY(posNow.getY());
pos.setZ(posNow.getZ());
} else {
pos.add(direction);
}
BlockPosition bpos = pos.toBlockPosition();
Block block = instance.getBlock(bpos.getX(), bpos.getY() - 1, bpos.getZ());
if (!block.isAir() && !block.isLiquid()) {
teleport(pos);
return true;
}
Chunk currentChunk = instance.getChunkAt(pos);
if (currentChunk != chunk) {
chunk = currentChunk;
entities = instance.getChunkEntities(chunk)
.stream()
.filter(entity -> entity instanceof LivingEntity)
.collect(Collectors.toSet());
}
/*
We won't check collisions with entities for first ticks of arrow's life, because it spawns in the
shooter and will immediately damage him.
*/
if (getAliveTicks() < 3) {
continue;
}
Optional<Entity> victimOptional = entities.stream()
.filter(entity -> entity.getBoundingBox().intersect(pos.getX(), pos.getY(), pos.getZ()))
.findAny();
if (victimOptional.isPresent()) {
LivingEntity victim = (LivingEntity) victimOptional.get();
victim.setArrowCount(victim.getArrowCount() + 1);
victim.damage(DamageType.fromProjectile(this.shooter, this), 2F);
remove();
return super.onGround;
}
}
return false;
}
public void setCritical(boolean value) {
modifyMask(CRITICAL_BIT, value);
}
public boolean isCritical() {
return (getMask() & CRITICAL_BIT) != 0;
}
public void setNoClip(boolean value) {
modifyMask(NO_CLIP_BIT, value);
}
public boolean isNoClip() {
return (getMask() & NO_CLIP_BIT) != 0;
}
public void setPiercingLevel(byte value) {
this.metadata.setIndex((byte) 8, Metadata.Byte(value));
}
public byte getPiercingLevel() {
return this.metadata.getIndex((byte) 8, (byte) 0);
}
private byte getMask() {
return this.metadata.getIndex((byte) 7, (byte) 0);
}
private void setMask(byte mask) {
this.metadata.setIndex((byte) 7, Metadata.Byte(mask));
}
private void modifyMask(byte bit, boolean value) {
byte mask = getMask();
boolean isPresent = (mask & bit) == bit;
if (isPresent == value) {
return;
}
if (value) {
mask |= bit;
} else {
mask &= ~bit;
}
setMask(mask);
}
@Override
public int getObjectData() {
return this.shooter == null ? 0 : this.shooter.getEntityId() + 1;
}
}

View File

@ -0,0 +1,25 @@
package net.minestom.server.entity.type.projectile;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.Metadata;
import net.minestom.server.utils.Position;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class EntityArrow extends EntityAbstractArrow {
public EntityArrow(@Nullable Entity shooter, @NotNull Position spawnPosition) {
super(shooter, EntityType.ARROW, spawnPosition);
}
public void setColor(int value) {
this.metadata.setIndex((byte) 9, Metadata.VarInt(value));
}
public int getColor() {
return this.metadata.getIndex((byte) 9, -1);
}
}

View File

@ -0,0 +1,15 @@
package net.minestom.server.entity.type.projectile;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityType;
import net.minestom.server.utils.Position;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class EntitySpectralArrow extends EntityAbstractArrow {
public EntitySpectralArrow(@Nullable Entity shooter, @NotNull Position spawnPosition) {
super(shooter, EntityType.SPECTRAL_ARROW, spawnPosition);
}
}

View File

@ -0,0 +1,94 @@
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 power;
private double spread;
private boolean cancelled;
public EntityShootEvent(@NotNull Entity entity, @NotNull Projectile projectile, @NotNull Position to, double power, double spread) {
super(entity);
this.projectile = projectile;
this.to = to;
this.power = power;
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;
}
/**
* Gets shot power.
*
* @return shot power.
*/
public double getPower() {
return this.power;
}
/**
* Sets shot power.
*
* @param power shot power.
*/
public void setPower(double power) {
this.power = power;
}
@Override
public boolean isCancelled() {
return this.cancelled;
}
@Override
public void setCancelled(boolean cancel) {
this.cancelled = cancel;
}
}

View File

@ -345,23 +345,19 @@ public class BlockIterator implements Iterator<BlockPosition> {
thirdError -= gridSize;
secondError -= gridSize;
currentBlock = 2;
return;
} else if (secondError > 0) {
blockQueue[1] = blockQueue[0].getRelative(mainFace);
blockQueue[0] = blockQueue[1].getRelative(secondFace);
secondError -= gridSize;
currentBlock = 1;
return;
} else if (thirdError > 0) {
blockQueue[1] = blockQueue[0].getRelative(mainFace);
blockQueue[0] = blockQueue[1].getRelative(thirdFace);
thirdError -= gridSize;
currentBlock = 1;
return;
} else {
blockQueue[0] = blockQueue[0].getRelative(mainFace);
currentBlock = 0;
return;
}
}

View File

@ -42,6 +42,7 @@ public class Main {
commandManager.register(new PotionCommand());
commandManager.register(new TitleCommand());
commandManager.register(new BookCommand());
commandManager.register(new ShootCommand());
commandManager.setUnknownCommandCallback((sender, command) -> sender.sendMessage("unknown command"));

View File

@ -0,0 +1,67 @@
package demo.commands;
import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.Arguments;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.arguments.ArgumentType;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.type.Projectile;
import net.minestom.server.entity.type.projectile.EntityArrow;
import net.minestom.server.entity.type.projectile.EntitySpectralArrow;
import java.util.concurrent.ThreadLocalRandom;
public class ShootCommand extends Command {
public ShootCommand() {
super("shoot");
setCondition(this::condition);
setDefaultExecutor(this::defaultExecutor);
var typeArg = ArgumentType.Word("type").from("default", "spectral", "colored");
setArgumentCallback(this::onTypeError, typeArg);
addSyntax(this::onShootCommand, typeArg);
}
private boolean condition(CommandSender sender, String commandString) {
if (!sender.isPlayer()) {
sender.sendMessage("The command is only available for player");
return false;
}
return true;
}
private void defaultExecutor(CommandSender sender, Arguments args) {
sender.sendMessage("Correct usage: shoot [default/spectral/colored]");
}
private void onTypeError(CommandSender sender, ArgumentSyntaxException exception) {
sender.sendMessage("SYNTAX ERROR: '" + exception.getInput() + "' should be replaced by 'default', 'spectral' or 'colored'");
}
private void onShootCommand(CommandSender sender, Arguments args) {
Player player = (Player) sender;
String mode = args.getWord("type");
Projectile projectile;
var pos = player.getPosition().clone().add(0D, player.getEyeHeight(), 0D);
switch (mode) {
case "default":
projectile = new EntityArrow(player, pos);
break;
case "spectral":
projectile = new EntitySpectralArrow(player, pos);
break;
case "colored":
projectile = new EntityArrow(player, pos);
((EntityArrow) projectile).setColor(ThreadLocalRandom.current().nextInt());
break;
default:
return;
}
((Entity) projectile).setInstance(player.getInstance());
var dir = pos.getDirection().multiply(30D);
pos = pos.clone().add(dir.getX(), dir.getY(), dir.getZ());
Projectile.shoot(projectile, player, pos, 1D, 0D);
}
}