/**
* SkillAPI
* com.sucy.skill.api.projectile.CustomProjectile
*
* The MIT License (MIT)
*
* Copyright (c) 2014 Steven Sucy
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software") to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.sucy.skill.api.projectile;
import com.sucy.skill.SkillAPI;
import com.sucy.skill.api.Settings;
import com.sucy.skill.api.particle.target.Followable;
import com.sucy.skill.log.Logger;
import mc.promcteam.engine.utils.Reflex;
import mc.promcteam.engine.utils.reflection.ReflectionManager;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.event.Event;
import org.bukkit.metadata.MetadataValue;
import org.bukkit.metadata.Metadatable;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.util.Vector;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.*;
import java.util.function.Predicate;
/**
* Base class for custom projectiles
*/
public abstract class CustomProjectile extends BukkitRunnable implements Metadatable, Followable {
private static final Vector X_VEC = new Vector(1, 0, 0);
private static final double DEGREE_TO_RAD = Math.PI / 180;
private static final Vector vel = new Vector();
private static Constructor> aabbConstructor;
private static Method getEntities;
private static Method getBukkitEntity;
private static final Predicate JAVA_PREDICATE = CustomProjectile::isLivingEntity;
private static final com.google.common.base.Predicate GUAVA_PREDICATE = CustomProjectile::isLivingEntity;
private static Method getEntitiesGuava;
private static Method getHandle;
static {
try {
Class> aabbClass =
ReflectionManager.MINOR_VERSION >= 17 ? Reflex.getClass("net.minecraft.world.phys.AxisAlignedBB")
: Reflex.getNMSClass("AxisAlignedBB");
Class> entityClass =
ReflectionManager.MINOR_VERSION >= 17 ? Reflex.getClass("net.minecraft.world.entity.Entity")
: Reflex.getNMSClass("Entity");
aabbConstructor = aabbClass.getConstructor(double.class,
double.class,
double.class,
double.class,
double.class,
double.class);
getBukkitEntity = entityClass.getDeclaredMethod("getBukkitEntity");
getHandle = Reflex.getCraftClass("CraftWorld").getDeclaredMethod("getHandle");
Class> worldClass =
ReflectionManager.MINOR_VERSION >= 17 ? Reflex.getClass("net.minecraft.world.level.World")
: Reflex.getNMSClass("World");
try {
getEntities = worldClass.getDeclaredMethod(ReflectionManager.MINOR_VERSION >= 18 ? "a" : "getEntities",
entityClass, aabbClass, Predicate.class);
} catch (Exception e) {
getEntitiesGuava = worldClass.getDeclaredMethod(ReflectionManager.MINOR_VERSION >= 18
? "a"
: "getEntities", entityClass, aabbClass, com.google.common.base.Predicate.class);
}
} catch (Exception ex) {
Logger.log("Unable to use reflection for accurate collision - will resort to simple radius check");
ex.printStackTrace();
}
}
private final HashMap> metadata = new HashMap<>();
private final Set hit = new HashSet<>();
private final LivingEntity thrower;
protected ProjectileCallback callback;
protected final Settings settings;
protected boolean enemy = true;
protected boolean ally = false;
private boolean valid = true;
/**
* Constructs a new custom projectile and starts its timer task
*
* @param thrower entity firing the projectile
*/
public CustomProjectile(LivingEntity thrower, Settings settings) {
this.thrower = thrower;
this.settings = settings;
runTaskTimer(SkillAPI.inst(), 1, 1);
}
private static boolean isLivingEntity(Object thing) {
try {
return getBukkitEntity.invoke(thing) instanceof LivingEntity;
} catch (Exception ex) {
return false;
}
}
/**
* Calculates the directions for projectiles spread from
* the centered direction using the given angle and
* number of projectiles to be fired.
*
* @param dir center direction of the spread
* @param angle angle which to spread at
* @param amount amount of directions to calculate
* @return the list of calculated directions
*/
public static List calcSpread(Vector dir, double angle, int amount) {
// Special cases
if (amount <= 0) {
return new ArrayList<>();
}
ArrayList list = new ArrayList<>();
// One goes straight if odd amount
if (amount % 2 == 1) {
list.add(dir);
amount--;
}
if (amount <= 0) {
return list;
}
// Get the base velocity
Vector base = dir.clone();
base.setY(0);
base.normalize();
vel.setX(1);
vel.setY(0);
vel.setZ(0);
// Get the vertical angle
double vBaseAngle = Math.acos(Math.max(-1, Math.min(base.dot(dir), 1)));
if (dir.getY() < 0) {
vBaseAngle = -vBaseAngle;
}
double hAngle = Math.acos(Math.max(-1, Math.min(1, base.dot(X_VEC)))) / DEGREE_TO_RAD;
if (dir.getZ() < 0) {
hAngle = -hAngle;
}
// Calculate directions
double angleIncrement = angle / (amount - 1);
for (int i = 0; i < amount / 2; i++) {
for (int direction = -1; direction <= 1; direction += 2) {
// Initial calculations
double bonusAngle = angle / 2 * direction - angleIncrement * i * direction;
double totalAngle = hAngle + bonusAngle;
double vAngle = vBaseAngle * Math.cos(bonusAngle * DEGREE_TO_RAD);
double x = Math.cos(vAngle);
// Get the velocity
vel.setX(x * Math.cos(totalAngle * DEGREE_TO_RAD));
vel.setY(Math.sin(vAngle));
vel.setZ(x * Math.sin(totalAngle * DEGREE_TO_RAD));
// Launch the projectile
list.add(vel.clone());
}
}
return list;
}
/**
* Calculates the locations to spawn projectiles to rain them down
* over a given location.
*
* @param loc the center location to rain on
* @param radius radius of the circle
* @param height height above the target to use
* @param amount amount of locations to calculate
* @return list of locations to spawn projectiles
*/
public static List calcRain(Location loc, double radius, double height, int amount) {
List list = new ArrayList<>();
if (amount <= 0) {
return list;
}
loc.add(0, height, 0);
// One would be in the center
list.add(loc);
amount--;
// Calculate locations
int tiers = (amount + 7) / 8;
for (int i = 0; i < tiers; i++) {
double rad = radius * (tiers - i) / tiers;
int tierNum = Math.min(amount, 8);
double increment = 360d / tierNum;
double angle = (i % 2) * 22.5;
for (int j = 0; j < tierNum; j++) {
double dx = Math.cos(angle) * rad;
double dz = Math.sin(angle) * rad;
Location l = loc.clone();
l.add(dx, 0, dz);
list.add(l);
angle += increment;
}
amount -= tierNum;
}
return list;
}
/**
* Retrieves the location of the projectile
*
* @return location of the projectile
*/
public abstract Location getLocation();
/**
* Checks whether the projectile is still active
*
* @return true if active, false otherwise
*/
@Override
public boolean isValid() {
return valid;
}
/**
* Handles expiring due to range or leaving loaded chunks
*/
protected abstract Event expire();
/**
* Handles landing on terrain
*/
protected abstract Event land();
/**
* Handles hitting an entity
*
* @param entity entity the projectile hit
*/
protected abstract Event hit(LivingEntity entity);
/**
* @return true if the projectile has landed on terrain, false otherwise
*/
protected abstract boolean landed();
/**
* @return squared radius for colliding
*/
protected abstract double getCollisionRadius();
protected abstract Vector getVelocity();
protected abstract void setVelocity(Vector vel);
/**
* Checks whether the projectile is still valid.
* Invalid would mean landing on the ground or leaving the loaded chunks.
*/
protected boolean isTraveling() {
// Leaving a loaded chunk
if (!getLocation().getChunk().isLoaded()) {
cancel();
Bukkit.getPluginManager().callEvent(expire());
return false;
}
// Hitting a solid block
if (landed()) {
this.applyLanded();
return false;
}
return true;
}
public void applyLanded() {
if (valid) {
cancel();
Bukkit.getPluginManager().callEvent(land());
if (callback != null)
callback.callback(this, null);
}
}
/**
* Checks if the projectile collides with a given list of entities
* Returns true if another check should happen, false other wise
*/
protected boolean checkCollision(final boolean pierce) {
for (LivingEntity entity : getColliding()) {
if (entity == thrower || hit.contains(entity.getEntityId())) {
continue;
}
hit.add(entity.getEntityId());
boolean ally = SkillAPI.getSettings().isAlly(getShooter(), entity);
if (ally && !this.ally) continue;
if (!ally && !this.enemy) continue;
if (!SkillAPI.getSettings().isValidTarget(entity)) continue;
Bukkit.getPluginManager().callEvent(hit(entity));
if (callback != null)
callback.callback(this, entity);
if (!pierce) {
cancel();
return false;
}
}
return true;
}
/**
* @return list of entities colliding with the projectile
*/
private List getColliding() {
// Reflection for nms collision
List result = new ArrayList<>(1);
try {
Object nmsWorld = getHandle.invoke(getLocation().getWorld());
Object predicate = getEntities == null ? GUAVA_PREDICATE : JAVA_PREDICATE;
Object list = (getEntities == null ? getEntitiesGuava : getEntities)
.invoke(nmsWorld, null, getBoundingBox(), predicate);
for (Object item : (List) list) {
result.add((LivingEntity) getBukkitEntity.invoke(item));
}
}
// Fallback when reflection fails
catch (Exception ex) {
double radiusSq = getCollisionRadius();
radiusSq *= radiusSq;
for (LivingEntity entity : getNearbyEntities()) {
if (entity == thrower)
continue;
if (getLocation().distanceSquared(entity.getLocation()) < radiusSq)
result.add(entity);
}
}
return result;
}
/**
* @return NMS bounding box of the projectile
*/
private Object getBoundingBox() throws Exception {
Location loc = getLocation();
double rad = getCollisionRadius();
return aabbConstructor.newInstance(
loc.getX() - rad, loc.getY() - rad, loc.getZ() - rad,
loc.getX() + rad, loc.getY() + rad, loc.getZ() + rad
);
}
/**
* @return list of nearby living entities
*/
private List getNearbyEntities() {
List list = new ArrayList();
Location loc = getLocation();
double radius = getCollisionRadius();
int minX = (int) (loc.getX() - radius) >> 4;
int maxX = (int) (loc.getX() + radius) >> 4;
int minZ = (int) (loc.getZ() - radius) >> 4;
int maxZ = (int) (loc.getZ() + radius) >> 4;
for (int i = minX; i <= maxX; i++)
for (int j = minZ; j < maxZ; j++)
for (Entity entity : loc.getWorld().getChunkAt(i, j).getEntities())
if (entity instanceof LivingEntity)
list.add((LivingEntity) entity);
return list;
}
/**
* Sets whether the projectile can hit allies or enemies
*
* @param ally whether allies can be hit
* @param enemy whether enemies can be hit
*/
public void setAllyEnemy(boolean ally, boolean enemy) {
this.ally = ally;
this.enemy = enemy;
}
/**
* Retrieves the entity that shot the projectile
*
* @return the entity that shot the projectile
*/
public LivingEntity getShooter() {
return thrower;
}
/**
* Marks the projectile as invalid when the associated task is cancelled
*/
@Override
public void cancel() {
super.cancel();
valid = false;
}
/**
* Sets a bit of metadata onto the projectile.
*
* @param key the key for the metadata
* @param meta the metadata to set
*/
@Override
public void setMetadata(String key, MetadataValue meta) {
boolean hasMeta = hasMetadata(key);
List list = hasMeta ? getMetadata(key) : new ArrayList();
list.add(meta);
if (!hasMeta) {
metadata.put(key, list);
}
}
/**
* Retrieves a metadata value from the projectile.
* If no metadata was set with the key, this will instead return null
*
* @param key the key for the metadata
* @return the metadata value
*/
@Override
public List getMetadata(String key) {
return metadata.get(key);
}
/**
* Checks whether this has a metadata set for the key.
*
* @param key the key for the metadata
* @return whether there is metadata set for the key
*/
@Override
public boolean hasMetadata(String key) {
return metadata.containsKey(key);
}
/**
* Removes a metadata value from the object.
* If no metadata is set for the key, this will do nothing.
*
* @param key the key for the metadata
* @param plugin plugin to remove the metadata for
*/
@Override
public void removeMetadata(String key, Plugin plugin) {
metadata.remove(key);
}
/**
* Sets the callback handler for the projectile
*
* @param callback callback handler
*/
public void setCallback(ProjectileCallback callback) {
this.callback = callback;
}
}