/**
Copyright (C) <2015> <coolAlias>
This file is part of coolAlias' Zelda Sword Skills Minecraft Mod; as such,
you can redistribute it and/or modify it under the terms of the GNU
General Public License as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package zeldaswordskills.util;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.block.state.IBlockState;
import net.minecraft.client.Minecraft;
import net.minecraft.enchantment.Enchantment;
import net.minecraft.enchantment.EnchantmentHelper;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.IEntityOwnable;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.projectile.EntityArrow;
import net.minecraft.item.ItemStack;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.BlockPos;
import net.minecraft.util.MathHelper;
import net.minecraft.util.MovingObjectPosition;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
/**
*
* A collection of methods related to target acquisition
*
*/
public class TargetUtils
{
/** Maximum range within which to search for targets */
private static final int MAX_DISTANCE = 256;
/** Max distance squared, used for comparing target distances (avoids having to call sqrt) */
private static final double MAX_DISTANCE_SQ = MAX_DISTANCE * MAX_DISTANCE;
// TODO write general MovingObjectPosition method, then have specific methods return blockHit or entityHit from that
// TODO methods for acquiring multiple targets (beam, sphere, etc) with optional number of targets to acquire
/** Returns the player's current reach distance, taking held item into account if applicable */
// Packet7UseEntity uses 36.0D for determining if an attack should hit, or 9.0D if the entity cannot be seen
// EntityRenderer uses 36.0D for creative mode, otherwise 9.0D, in calculating whether mouseover entity should be null
// but using this exactly results in some attacks that in reality hit, being counted as a miss
// Unlike Creative Mode, the mouseover is always null when an attack should miss when in Survival
public static double getReachDistanceSq(EntityPlayer player) {
return 38.5D; // seems to be just about right for Creative Mode hit detection
}
/**
* Returns true if current target is within the player's reach distance; does NOT check mouse over
*/
public static boolean canReachTarget(EntityPlayer player, Entity target) {
return (player.canEntityBeSeen(target) && player.getDistanceSqToEntity(target) < getReachDistanceSq(player));
}
/**
* Returns MovingObjectPosition of Entity or Block impacted, or null if nothing was struck
* @param entity The entity checking for impact, e.g. an arrow
* @param shooter An entity not to be collided with, generally the shooter
* @param hitBox The amount by which to expand the collided entities' bounding boxes when checking for impact (may be negative)
* @param flag Optional flag to allow collision with shooter, e.g. (ticksInAir >= 5)
*/
public static MovingObjectPosition checkForImpact(World world, Entity entity, Entity shooter, double hitBox, boolean flag) {
double posY = entity.posY + (entity.height / 2); // fix for Dash
Vec3 vec3 = new Vec3(entity.posX, posY, entity.posZ);
Vec3 vec31 = new Vec3(entity.posX + entity.motionX, posY + entity.motionY, entity.posZ + entity.motionZ);
MovingObjectPosition mop = world.rayTraceBlocks(vec3, vec31, false, true, false);
vec3 = new Vec3(entity.posX, posY, entity.posZ);
vec31 = new Vec3(entity.posX + entity.motionX, posY + entity.motionY, entity.posZ + entity.motionZ);
if (mop != null) {
vec31 = new Vec3(mop.hitVec.xCoord, mop.hitVec.yCoord, mop.hitVec.zCoord);
}
Entity target = null;
List<Entity> list = world.getEntitiesWithinAABBExcludingEntity(entity, entity.getEntityBoundingBox().addCoord(entity.motionX, entity.motionY, entity.motionZ).expand(1.0D, 1.0D, 1.0D));
double d0 = 0.0D;
for (int i = 0; i < list.size(); ++i) {
Entity entity1 = (Entity) list.get(i);
if (entity1.canBeCollidedWith() && (entity1 != shooter || flag)) {
AxisAlignedBB axisalignedbb = entity1.getEntityBoundingBox().expand(hitBox, hitBox, hitBox);
MovingObjectPosition mop1 = axisalignedbb.calculateIntercept(vec3, vec31);
if (mop1 != null) {
double d1 = vec3.distanceTo(mop1.hitVec);
if (d1 < d0 || d0 == 0.0D) {
target = entity1;
d0 = d1;
}
}
}
}
if (target != null) {
mop = new MovingObjectPosition(target);
}
if (mop != null && mop.entityHit instanceof EntityPlayer) {
EntityPlayer player = (EntityPlayer) mop.entityHit;
if (player.capabilities.disableDamage || (shooter instanceof EntityPlayer
&& !((EntityPlayer) shooter).canAttackPlayer(player)))
{
mop = null;
}
}
return mop;
}
/**
* Returns true if the entity is directly in the crosshairs
*/
@SideOnly(Side.CLIENT)
public static boolean isMouseOverEntity(Entity entity) {
MovingObjectPosition mop = Minecraft.getMinecraft().objectMouseOver;
return (mop != null && mop.entityHit == entity);
}
/**
* Returns the Entity that the mouse is currently over, or null
*/
@SideOnly(Side.CLIENT)
public static Entity getMouseOverEntity() {
MovingObjectPosition mop = Minecraft.getMinecraft().objectMouseOver;
return (mop == null ? null : mop.entityHit);
}
/** Returns the EntityLivingBase closest to the point at which the seeker is looking and within the distance and radius specified */
public static final EntityLivingBase acquireLookTarget(EntityLivingBase seeker, int distance, double radius) {
return acquireLookTarget(seeker, distance, radius, false);
}
/**
* Returns the EntityLivingBase closest to the point at which the entity is looking and within the distance and radius specified
* @param distance max distance to check for target, in blocks; negative value will check to MAX_DISTANCE
* @param radius max distance, in blocks, to search on either side of the vector's path
* @param closestToEntity if true, the target closest to the seeker and still within the line of sight search radius is returned
* @return the entity the seeker is looking at or null if no entity within sight search range
*/
public static final EntityLivingBase acquireLookTarget(EntityLivingBase seeker, int distance, double radius, boolean closestToSeeker) {
if (distance < 0 || distance > MAX_DISTANCE) {
distance = MAX_DISTANCE;
}
EntityLivingBase currentTarget = null;
double currentDistance = MAX_DISTANCE_SQ;
Vec3 vec3 = seeker.getLookVec();
double targetX = seeker.posX;
double targetY = seeker.posY + seeker.getEyeHeight() - 0.10000000149011612D;
double targetZ = seeker.posZ;
double distanceTraveled = 0;
while ((int) distanceTraveled < distance) {
targetX += vec3.xCoord;
targetY += vec3.yCoord;
targetZ += vec3.zCoord;
distanceTraveled += vec3.lengthVector();
AxisAlignedBB bb = new AxisAlignedBB(targetX-radius, targetY-radius, targetZ-radius, targetX+radius, targetY+radius, targetZ+radius);
List<EntityLivingBase> list = seeker.worldObj.getEntitiesWithinAABB(EntityLivingBase.class, bb);
for (EntityLivingBase target : list) {
if (target != seeker && target.canBeCollidedWith() && isTargetInSight(vec3, seeker, target)) {
double newDistance = (closestToSeeker ? target.getDistanceSqToEntity(seeker) : target.getDistanceSq(targetX, targetY, targetZ));
if (newDistance < currentDistance) {
currentTarget = target;
currentDistance = newDistance;
}
}
}
}
return currentTarget;
}
/**
* Similar to the single entity version, but this method returns a List of all EntityLivingBase entities
* that are within the entity's field of vision, up to a certain range and distance away
*/
public static final List<EntityLivingBase> acquireAllLookTargets(EntityLivingBase seeker, int distance, double radius) {
if (distance < 0 || distance > MAX_DISTANCE) {
distance = MAX_DISTANCE;
}
List<EntityLivingBase> targets = new ArrayList<EntityLivingBase>();
Vec3 vec3 = seeker.getLookVec();
double targetX = seeker.posX;
double targetY = seeker.posY + seeker.getEyeHeight() - 0.10000000149011612D;
double targetZ = seeker.posZ;
double distanceTraveled = 0;
while ((int) distanceTraveled < distance) {
targetX += vec3.xCoord;
targetY += vec3.yCoord;
targetZ += vec3.zCoord;
distanceTraveled += vec3.lengthVector();
AxisAlignedBB bb = new AxisAlignedBB(targetX-radius, targetY-radius, targetZ-radius, targetX+radius, targetY+radius, targetZ+radius);
List<EntityLivingBase> list = seeker.worldObj.getEntitiesWithinAABB(EntityLivingBase.class, bb);
for (EntityLivingBase target : list) {
if (target != seeker && target.canBeCollidedWith() && isTargetInSight(vec3, seeker, target)) {
if (!targets.contains(target)) {
targets.add(target);
}
}
}
}
return targets;
}
/**
* Returns whether the target is in the seeker's field of view based on relative position
* @param fov seeker's field of view; a wider angle returns true more often
*/
public static final boolean isTargetInFrontOf(Entity seeker, Entity target, float fov) {
// thanks again to Battlegear2 for the following code snippet
double dx = target.posX - seeker.posX;
double dz;
for (dz = target.posZ - seeker.posZ; dx * dx + dz * dz < 1.0E-4D; dz = (Math.random() - Math.random()) * 0.01D) {
dx = (Math.random() - Math.random()) * 0.01D;
}
while (seeker.rotationYaw > 360) { seeker.rotationYaw -= 360; }
while (seeker.rotationYaw < -360) { seeker.rotationYaw += 360; }
float yaw = (float)(Math.atan2(dz, dx) * 180.0D / Math.PI) - seeker.rotationYaw;
yaw = yaw - 90;
while (yaw < -180) { yaw += 360; }
while (yaw >= 180) { yaw -= 360; }
return yaw < fov && yaw > -fov;
}
/**
* Returns true if the target's position is within the area that the seeker is facing and the target can be seen
*/
public static final boolean isTargetInSight(EntityLivingBase seeker, Entity target) {
return isTargetInSight(seeker.getLookVec(), seeker, target);
}
/**
* Returns true if the target's position is within the area that the seeker is facing and the target can be seen
*/
private static final boolean isTargetInSight(Vec3 vec3, EntityLivingBase seeker, Entity target) {
return seeker.canEntityBeSeen(target) && isTargetInFrontOf(seeker, target, 60);
}
/**
* Applies all vanilla modifiers to passed in arrow (e.g. enchantment bonuses, critical, etc)
* @param charge should be a value between 0.0F and 1.0F, inclusive
*/
public static final void applyArrowSettings(EntityArrow arrow, ItemStack bow, float charge) {
if (charge < 0.0F) { charge = 0.0F; }
if (charge > 1.0F) { charge = 1.0F; }
if (charge == 1.0F) { arrow.setIsCritical(true); }
int k = EnchantmentHelper.getEnchantmentLevel(Enchantment.power.effectId, bow);
if (k > 0) { arrow.setDamage(arrow.getDamage() + (double) k * 0.5D + 0.5D); }
int l = EnchantmentHelper.getEnchantmentLevel(Enchantment.punch.effectId, bow);
if (l > 0) { arrow.setKnockbackStrength(l); }
if (EnchantmentHelper.getEnchantmentLevel(Enchantment.flame.effectId, bow) > 0) {
arrow.setFire(100);
}
}
/**
* Sets an entity's motion along the given vector at the given velocity, with wobble being
* an amount of variation applied to the course.
* @param wobble set to 0.0F for a true heading
* @param backwards if true, will set the entity's rotation to the opposite direction
*/
public static void setEntityHeading(Entity entity, double vecX, double vecY, double vecZ, float velocity, float wobble, boolean backwards) {
float vectorLength = MathHelper.sqrt_double(vecX * vecX + vecY * vecY + vecZ * vecZ);
vecX /= vectorLength;
vecY /= vectorLength;
vecZ /= vectorLength;
vecX += entity.worldObj.rand.nextGaussian() * (entity.worldObj.rand.nextBoolean() ? -1 : 1) * 0.007499999832361937D * wobble;
vecY += entity.worldObj.rand.nextGaussian() * (entity.worldObj.rand.nextBoolean() ? -1 : 1) * 0.007499999832361937D * wobble;
vecZ += entity.worldObj.rand.nextGaussian() * (entity.worldObj.rand.nextBoolean() ? -1 : 1) * 0.007499999832361937D * wobble;
vecX *= velocity;
vecY *= velocity;
vecZ *= velocity;
entity.motionX = vecX;
entity.motionY = vecY;
entity.motionZ = vecZ;
float f = MathHelper.sqrt_double(vecX * vecX + vecZ * vecZ);
entity.prevRotationYaw = entity.rotationYaw = (backwards ? -1 : 1) * (float)(Math.atan2(vecX, vecZ) * 180.0D / Math.PI);
entity.prevRotationPitch = entity.rotationPitch = (backwards ? -1 : 1) * (float)(Math.atan2(vecY, f) * 180.0D / Math.PI);
}
/**
* Returns true if the entity is considered friendly to the player (or IS the player)
*/
public static boolean isOnTeam(EntityPlayer player, EntityLivingBase entity) {
if (entity == player) {
return true;
} else if (player.isOnSameTeam(entity)) {
return true;
} else if (entity instanceof IEntityOwnable) {
return ((IEntityOwnable) entity).getOwner() == player;
} else {
return false;
}
}
/**
* Returns true if the entity has an unimpeded view of the sky
*/
public static boolean canEntitySeeSky(World world, Entity entity) {
BlockPos pos = new BlockPos(entity);
while (pos.getY() < world.getActualHeight()) {
if (!world.isAirBlock(pos)) {
return false;
}
pos = pos.up();
}
return true;
}
/**
* Whether the entity is currently standing in any liquid
*/
public static boolean isInLiquid(Entity entity) {
IBlockState state = entity.worldObj.getBlockState(new BlockPos(entity));
return state.getBlock().getMaterial().isLiquid();
}
/**
* Knocks the pushed entity back slightly as though struck by the pushing entity
*/
public static final void knockTargetBack(EntityLivingBase pushedEntity, EntityLivingBase pushingEntity) {
if (pushedEntity.canBePushed()) {
double dx = pushedEntity.posX - pushingEntity.posX;
double dz;
for (dz = pushedEntity.posZ - pushingEntity.posZ; dx * dx + dz * dz < 1.0E-4D; dz = (Math.random() - Math.random()) * 0.01D){
dx = (Math.random() - Math.random()) * 0.01D;
}
pushedEntity.knockBack(pushingEntity, 0, -dx, -dz);
}
}
}