diff --git a/src/main/java/electroblob/wizardry/client/WizardryClientEventHandler.java b/src/main/java/electroblob/wizardry/client/WizardryClientEventHandler.java index e9ea54fb..ce053d6c 100644 --- a/src/main/java/electroblob/wizardry/client/WizardryClientEventHandler.java +++ b/src/main/java/electroblob/wizardry/client/WizardryClientEventHandler.java @@ -208,7 +208,6 @@ public final class WizardryClientEventHandler { Minecraft mc = Minecraft.getMinecraft(); WizardData properties = WizardData.get(mc.player); - RayTraceResult rayTrace = WizardryUtilities.standardEntityRayTrace(mc.world, mc.player, 16, false); RenderManager renderManager = event.getRenderer().getRenderManager(); ItemStack wand = mc.player.getHeldItemMainhand(); @@ -218,45 +217,51 @@ public final class WizardryClientEventHandler { } // Target selection pointer - if(mc.player.isSneaking() && wand.getItem() instanceof ItemWand && rayTrace != null && !(event.getEntity() instanceof EntityArmorStand) - && rayTrace.entityHit == event.getEntity() && properties != null && properties.selectedMinion != null){ + if(mc.player.isSneaking() && wand.getItem() instanceof ItemWand && WizardryUtilities.isLiving(event.getEntity()) + && properties != null && properties.selectedMinion != null){ + + // -> Moved this in here so it isn't called every tick + RayTraceResult rayTrace = WizardryUtilities.standardEntityRayTrace(mc.world, mc.player, 16, false); + + if(rayTrace != null && rayTrace.entityHit == event.getEntity()){ - Tessellator tessellator = Tessellator.getInstance(); - BufferBuilder buffer = tessellator.getBuffer(); + Tessellator tessellator = Tessellator.getInstance(); + BufferBuilder buffer = tessellator.getBuffer(); - GlStateManager.pushMatrix(); + GlStateManager.pushMatrix(); - GlStateManager.disableCull(); - GlStateManager.disableLighting(); - OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240f, 240f); - // Disabling depth test allows it to be seen through everything. - GlStateManager.disableDepth(); - GlStateManager.color(1, 1, 1, 1); + GlStateManager.disableCull(); + GlStateManager.disableLighting(); + OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240f, 240f); + // Disabling depth test allows it to be seen through everything. + GlStateManager.disableDepth(); + GlStateManager.color(1, 1, 1, 1); - GlStateManager.translate(event.getX(), event.getY() + event.getEntity().height + 0.5, event.getZ()); + GlStateManager.translate(event.getX(), event.getY() + event.getEntity().height + 0.5, event.getZ()); - // This counteracts the reverse rotation behaviour when in front f5 view. - // Fun fact: this is a bug with vanilla too! Look at a snowball in front f5 view, for example. - float yaw = mc.gameSettings.thirdPersonView == 2 ? renderManager.playerViewX : -renderManager.playerViewX; - GlStateManager.rotate(180 - renderManager.playerViewY, 0.0F, 1.0F, 0.0F); - GlStateManager.rotate(yaw, 1.0F, 0.0F, 0.0F); + // This counteracts the reverse rotation behaviour when in front f5 view. + // Fun fact: this is a bug with vanilla too! Look at a snowball in front f5 view, for example. + float yaw = mc.gameSettings.thirdPersonView == 2 ? renderManager.playerViewX : -renderManager.playerViewX; + GlStateManager.rotate(180 - renderManager.playerViewY, 0.0F, 1.0F, 0.0F); + GlStateManager.rotate(yaw, 1.0F, 0.0F, 0.0F); - buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX); + buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX); - mc.renderEngine.bindTexture(targetPointerTexture); + mc.renderEngine.bindTexture(targetPointerTexture); - buffer.pos(-0.2, 0.24, 0).tex(0, 0).endVertex(); - buffer.pos(0.2, 0.24, 0).tex(9f / 16f, 0).endVertex(); - buffer.pos(0.2, -0.24, 0).tex(9f / 16f, 11f / 16f).endVertex(); - buffer.pos(-0.2, -0.24, 0).tex(0, 11f / 16f).endVertex(); + buffer.pos(-0.2, 0.24, 0).tex(0, 0).endVertex(); + buffer.pos(0.2, 0.24, 0).tex(9f / 16f, 0).endVertex(); + buffer.pos(0.2, -0.24, 0).tex(9f / 16f, 11f / 16f).endVertex(); + buffer.pos(-0.2, -0.24, 0).tex(0, 11f / 16f).endVertex(); - tessellator.draw(); + tessellator.draw(); - GlStateManager.enableCull(); - GlStateManager.enableLighting(); - GlStateManager.enableDepth(); + GlStateManager.enableCull(); + GlStateManager.enableLighting(); + GlStateManager.enableDepth(); - GlStateManager.popMatrix(); + GlStateManager.popMatrix(); + } } // Summoned creature selection pointer diff --git a/src/main/java/electroblob/wizardry/spell/SpellRay.java b/src/main/java/electroblob/wizardry/spell/SpellRay.java index 79b2f310..41c65e52 100644 --- a/src/main/java/electroblob/wizardry/spell/SpellRay.java +++ b/src/main/java/electroblob/wizardry/spell/SpellRay.java @@ -70,6 +70,8 @@ public abstract class SpellRay extends Spell { protected boolean ignoreEntities = false; /** Whether liquids count as blocks when raytracing. Defaults to false. */ protected boolean hitLiquids = false; + /** The aim assist to use when raytracing. Defaults to 0. */ + protected float aimAssist = 0; public SpellRay(String name, Tier tier, Element element, SpellType type, int cost, int cooldown, boolean isContinuous, double baseRange, SoundEvent sound){ this(Wizardry.MODID, name, tier, element, type, cost, cooldown, isContinuous, baseRange, sound); @@ -147,6 +149,17 @@ public abstract class SpellRay extends Spell { return this; } + /** + * Sets the aim assist to use when raytracing. + * @param particleVelocity The aim assist to use when raytracing. See {@link WizardryUtilities#rayTrace(World, Vec3d, + * Vec3d, float, boolean, Class, java.util.function.Predicate)} for more details. + * @return The spell instance, allowing this method to be chained onto the constructor. + */ + public SpellRay aimAssist(float aimAssist){ + this.aimAssist = aimAssist; + return this; + } + @Override public boolean canBeCastByNPCs(){ return true; } // Finally everything in here is standardised and written in a form that's actually readable - it was long overdue! @@ -157,49 +170,7 @@ public abstract class SpellRay extends Spell { Vec3d origin = new Vec3d(caster.posX, caster.getEntityBoundingBox().minY + caster.getEyeHeight() - Y_OFFSET, caster.posZ); - double range = baseRange * modifiers.get(WizardryItems.range_upgrade); - - // The first method will hit the first block it touches, passing through entities as if they weren't there. - // The second method will hit the first thing it touches, whether that's a block or an entity. - // Note that when it hits a block, the exact position hit is returned in hitVec, whereas when it hits an - // entity, it just returns the entity's position in hitVec. - RayTraceResult rayTrace = ignoreEntities - ? WizardryUtilities.standardBlockRayTrace(world, caster, range, hitLiquids) - : WizardryUtilities.standardEntityRayTrace(world, caster, range, hitLiquids); - - boolean flag = false; - - if(rayTrace != null){ - // Doesn't matter which way round these are, they're mutually exclusive - if(rayTrace.typeOfHit == RayTraceResult.Type.ENTITY){ - - Entity target = rayTrace.entityHit; - // Do whatever the spell does when it hits an entity - flag = onEntityHit(world, target, caster, ticksInUse, modifiers); - // If the spell succeeded, clip the particles to the correct distance so they don't go through the entity - if(flag){ - // The most pragmatic solution is to use the target's centre point for reasons explained earlier - double dx = origin.x - target.posX; - double dy = origin.y - (target.getEntityBoundingBox().minY + target.height/2); - double dz = origin.z - target.posZ; - range = MathHelper.sqrt(dx*dx + dy*dy + dz*dz); - } - - }else if(rayTrace.typeOfHit == RayTraceResult.Type.BLOCK){ - // Do whatever the spell does when it hits an block - flag = onBlockHit(world, rayTrace.getBlockPos(), rayTrace.sideHit, caster, ticksInUse, modifiers); - // If the spell succeeded, clip the particles to the correct distance so they don't go through the block - if(flag) range = origin.distanceTo(rayTrace.hitVec); - } - } - - // If flag is false, either the spell missed or the relevant entity/block hit method returned false - if(!flag && !onMiss(world, caster, ticksInUse, modifiers)) return false; - - // Particle spawning - if(world.isRemote){ - spawnParticleRay(world, origin, look, range); - } + if(!shootSpell(world, origin, look, caster, ticksInUse, modifiers)) return false; if(!isContinuous) caster.swingArm(hand); // Bit of a dirty fix but I don't think it'll be a problem! if(sound != null) WizardryUtilities.playSoundAtPlayer(caster, sound, volume, pitch + pitchVariation * (world.rand.nextFloat() - 0.5f)); @@ -253,6 +224,46 @@ public abstract class SpellRay extends Spell { return true; } + /** Player and dispenser casting are almost identical so this takes care of the shared stuff. */ + private boolean shootSpell(World world, Vec3d origin, Vec3d direction, @Nullable EntityPlayer caster, int ticksInUse, SpellModifiers modifiers){ + + double range = baseRange * modifiers.get(WizardryItems.range_upgrade); + Vec3d endpoint = origin.add(direction.scale(range)); + + // The first method will hit the first block it touches, passing through entities as if they weren't there. + // The second method will hit the first thing it touches, whether that's a block or an entity. This method now + // returns the exact hit position for entities as well as blocks. + RayTraceResult rayTrace = WizardryUtilities.rayTrace(world, origin, endpoint, aimAssist, hitLiquids, Entity.class, e -> e == caster); + + boolean flag = false; + + if(rayTrace != null){ + // Doesn't matter which way round these are, they're mutually exclusive + if(rayTrace.typeOfHit == RayTraceResult.Type.ENTITY){ + // Do whatever the spell does when it hits an entity + flag = onEntityHit(world, rayTrace.entityHit, caster, ticksInUse, modifiers); + // If the spell succeeded, clip the particles to the correct distance so they don't go through the entity + if(flag) range = origin.distanceTo(rayTrace.hitVec); + + }else if(rayTrace.typeOfHit == RayTraceResult.Type.BLOCK){ + // Do whatever the spell does when it hits an block + flag = onBlockHit(world, rayTrace.getBlockPos(), rayTrace.sideHit, caster, ticksInUse, modifiers); + // If the spell succeeded, clip the particles to the correct distance so they don't go through the block + if(flag) range = origin.distanceTo(rayTrace.hitVec); + } + } + + // If flag is false, either the spell missed or the relevant entity/block hit method returned false + if(!flag && !onMiss(world, caster, ticksInUse, modifiers)) return false; + + // Particle spawning + if(world.isRemote){ + spawnParticleRay(world, origin, direction, range); + } + + return true; + } + // Private helper method, no-one will need to override it since it's pretty much the whole point of this class. private void spawnParticleRay(World world, Vec3d origin, Vec3d direction, double distance){ diff --git a/src/main/java/electroblob/wizardry/util/WizardryUtilities.java b/src/main/java/electroblob/wizardry/util/WizardryUtilities.java index d9578fd6..d334e2a2 100644 --- a/src/main/java/electroblob/wizardry/util/WizardryUtilities.java +++ b/src/main/java/electroblob/wizardry/util/WizardryUtilities.java @@ -4,13 +4,13 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Random; import java.util.UUID; import java.util.function.Function; +import java.util.function.Predicate; import javax.annotation.Nullable; @@ -553,118 +553,145 @@ public final class WizardryUtilities { // =============================================================================================================== /** - * Does a block ray trace (NOT entities) from an entity's eyes (i.e. properly...) + * Helper method which performs a ray trace for blocks only from an entity's eye position in the direction + * they are looking, over a specified range, using {@link World#rayTraceBlocks(Vec3d, Vec3d, boolean)}. + * + * @param world The world in which to perform the ray trace. + * @param entity The entity from which to perform the ray trace. The ray trace will start from this entity's eye + * position and proceed in the direction the entity is looking. + * @param range The distance over which the ray trace will be performed. + * @param hitLiquids True to return hits on the surfaces of liquids, false to ignore liquid blocks as if they were + * not there. + * @return A {@link RayTraceResult} representing the object that was hit, which may be either a block or nothing. + * Returns {@code null} only if the origin and endpoint are within the same block. */ @Nullable public static RayTraceResult standardBlockRayTrace(World world, EntityLivingBase entity, double range, boolean hitLiquids){ - - Vec3d start = new Vec3d(entity.posX, entity.getEntityBoundingBox().minY + entity.getEyeHeight(), entity.posZ); - Vec3d look = entity.getLookVec(); - Vec3d end = start.addVector(look.x * range, look.y * range, look.z * range); - return world.rayTraceBlocks(start, end, hitLiquids); + // This method does not apply an offset like ray spells do, since it is not desirable in most other use cases. + Vec3d origin = new Vec3d(entity.posX, entity.getEntityBoundingBox().minY + entity.getEyeHeight(), entity.posZ); + Vec3d endpoint = origin.add(entity.getLookVec().scale(range)); + return world.rayTraceBlocks(origin, endpoint, hitLiquids); } /** - * Helper method which does a rayTrace for entities from an entity's eye level in the direction they are looking - * with a specified range, using the tracePath method. Tidies up the code a bit. Border size defaults to 1. + * Helper method which performs a ray trace for blocks and entities from an entity's eye position in the direction + * they are looking, over a specified range, using {@link WizardryUtilities#rayTrace(World, Vec3d, Vec3d, float, + * boolean, Class, Predicate)}. Aim assist is zero, the entity type is simply {@code Entity} (all entities), and the + * filter removes the given entity and allows all others. * - * @param world - * @param entity - * @param range - * @return + * @param world The world in which to perform the ray trace. + * @param entity The entity from which to perform the ray trace. The ray trace will start from this entity's eye + * position and proceed in the direction the entity is looking. This entity will be ignored when ray tracing. + * @param range The distance over which the ray trace will be performed. + * @param hitLiquids True to return hits on the surfaces of liquids, false to ignore liquid blocks as if they were + * not there. + * @return A {@link RayTraceResult} representing the object that was hit, which may be an entity, a block or + * nothing. Returns {@code null} only if the origin and endpoint are within the same block and no entity was hit. */ @Nullable - public static RayTraceResult standardEntityRayTrace(World world, EntityLivingBase entity, double range, boolean hitLiquids){ - return standardEntityRayTrace(world, entity, range, 1, hitLiquids); + public static RayTraceResult standardEntityRayTrace(World world, Entity entity, double range, boolean hitLiquids){ + // This method does not apply an offset like ray spells do, since it is not desirable in most other use cases. + Vec3d origin = new Vec3d(entity.posX, entity.getEntityBoundingBox().minY + entity.getEyeHeight(), entity.posZ); + Vec3d endpoint = origin.add(entity.getLookVec().scale(range)); + return WizardryUtilities.rayTrace(world, origin, endpoint, 0, hitLiquids, Entity.class, e -> e == entity); } /** - * Helper method which does a rayTrace for entities from a entity's eye level in the direction they are looking with - * a specified range and radius, using the tracePath method. Tidies up the code a bit. + * Performs a ray trace for blocks and entities, starting at the given origin and finishing at the given endpoint. + * As of wizardry 4.2, the ray tracing methods have been rewritten to be more user-friendly and implement proper + * aim assist. + *

+ * N.B. It is possible to ignore entities entirely by passing in a {@code Predicate} that is always false; + * however, in this specific case it is more efficient to use + * {@link World#rayTraceBlocks(Vec3d, Vec3d, boolean, boolean, boolean)} or one of its overloads. * - * @param world - * @param entity - * @param range - * @param borderSize - * @return - */ - @Nullable - public static RayTraceResult standardEntityRayTrace(World world, EntityLivingBase entity, double range, float borderSize, boolean hitLiquids){ - double dx = entity.getLookVec().x * range; - double dy = entity.getLookVec().y * range; - double dz = entity.getLookVec().z * range; - HashSet hashset = new HashSet(1); - hashset.add(entity); - return WizardryUtilities.tracePath(world, (float)entity.posX, - (float)(entity.getEntityBoundingBox().minY + entity.getEyeHeight()), (float)entity.posZ, - (float)(entity.posX + dx), (float)(entity.posY + entity.getEyeHeight() + dy), (float)(entity.posZ + dz), - borderSize, hashset, false, hitLiquids); - } - - /** - * Method for ray tracing entities. You can also use this for seeking. + * @param The type of entities to include; all other entities will be ignored. + * @param world The world in which to perform the ray trace. + * @param origin A vector representing the coordinates of the start point of the ray trace. + * @param endpoint A vector representing the coordinates of the finish point of the ray trace. + * @param aimAssist In addition to direct hits, the ray trace will also hit entities that are up to this distance + * from its path. For a normal ray trace, this should be 0. Values greater than 0 will give an 'aim assist' effect. + * @param hitLiquids Whether liquids should be ignored when ray tracing blocks + * @param entityType The class of entities to include; all other entities will be ignored. + * @param filter A {@link Predicate} which filters out entities that can be ignored; often used to exclude the + * player that is performing the ray trace. * - * @param world - * @param x startX - * @param y startY - * @param z startZ - * @param tx endX - * @param ty endY - * @param tz endZ - * @param borderSize extra area to examine around line for entities - * @param excluded any excluded entities (the player, etc) - * @return a RayTraceResult of either the block hit (no entity hit), the entity hit (hit an entity), or null for - * nothing hit + * @return A {@link RayTraceResult} representing the object that was hit, which may be an entity, a block or + * nothing. Returns {@code null} only if the origin and endpoint are within the same block and no entity was hit. + * + * @see WizardryUtilities#standardEntityRayTrace(World, Entity, double, boolean) + * @see WizardryUtilities#standardBlockRayTrace(World, EntityLivingBase, double, boolean) */ + // Interestingly enough, aimAssist can be negative, which means hits have to be in the middle of entities! @Nullable - public static RayTraceResult tracePath(World world, float x, float y, float z, float tx, float ty, float tz, - float borderSize, HashSet excluded, boolean collideablesOnly, boolean hitLiquids){ + public static RayTraceResult rayTrace(World world, Vec3d origin, Vec3d endpoint, float aimAssist, + boolean hitLiquids, Class entityType, Predicate filter){ + + // 1 is the standard amount of extra search volume, and aim assist needs to increase this further as well as + // expanding the entities' bounding boxes. + float borderSize = 1 + aimAssist; - Vec3d startVec = new Vec3d(x, y, z); - // Vec3d lookVec = new Vec3d(tx-x, ty-y, tz-z); - Vec3d endVec = new Vec3d(tx, ty, tz); - float minX = x < tx ? x : tx; - float minY = y < ty ? y : ty; - float minZ = z < tz ? z : tz; - float maxX = x > tx ? x : tx; - float maxY = y > ty ? y : ty; - float maxZ = z > tz ? z : tz; - AxisAlignedBB bb = new AxisAlignedBB(minX, minY, minZ, maxX, maxY, maxZ).grow(borderSize, borderSize, borderSize); - List allEntities = world.getEntitiesWithinAABBExcludingEntity(null, bb); - RayTraceResult blockHit = world.rayTraceBlocks(startVec, endVec, hitLiquids); - startVec = new Vec3d(x, y, z); - endVec = new Vec3d(tx, ty, tz); - float maxDistance = (float)endVec.distanceTo(startVec); - if(blockHit != null){ - maxDistance = (float)blockHit.hitVec.distanceTo(startVec); + // The AxisAlignedBB constructor accepts min/max coords in either order. + AxisAlignedBB searchVolume = new AxisAlignedBB(origin.x, origin.y, origin.z, endpoint.x, endpoint.y, endpoint.z) + .grow(borderSize, borderSize, borderSize); + + // Gets all of the entities in the bounding box that could be collided with. + List entities = world.getEntitiesWithinAABB(entityType, searchVolume); + // Applies the given filter to remove entities that should be ignored. + entities.removeIf(filter); + + // Finds the first block hit by the ray trace, if any. + RayTraceResult result = world.rayTraceBlocks(origin, endpoint, hitLiquids); + + // Clips the entity search range to the part of the ray trace before the block hit, if it hit a block. + if(result != null){ + endpoint = result.hitVec; } + + // Search variables Entity closestHitEntity = null; - float closestHit = maxDistance; - float currentHit = 0.f; - AxisAlignedBB entityBb;// = ent.getBoundingBox(); + Vec3d closestHitPosition = endpoint; + AxisAlignedBB entityBounds; RayTraceResult intercept; - for(Entity ent : allEntities){ - if((ent.canBeCollidedWith() || !collideablesOnly) - && ((excluded != null && !excluded.contains(ent)) || excluded == null)){ - float entBorder = ent.getCollisionBorderSize(); - entityBb = ent.getEntityBoundingBox(); - if(entityBb != null){ - entityBb = entityBb.grow(entBorder, entBorder, entBorder); - intercept = entityBb.calculateIntercept(startVec, endVec); - if(intercept != null){ - currentHit = (float)intercept.hitVec.distanceTo(startVec); - if(currentHit < closestHit || currentHit == 0){ - closestHit = currentHit; - closestHitEntity = ent; - } + + // Iterates through all the entities + for(Entity entity : entities){ + + entityBounds = entity.getEntityBoundingBox(); + + if(entityBounds != null){ + + // This is zero for everything except fireballs... + float entityBorderSize = entity.getCollisionBorderSize(); + // ... meaning the following line does nothing in all other cases. + // -> Added the non-zero check to prevent unnecessary AABB object creation. + if(entityBorderSize != 0) entityBounds = entityBounds.grow(entityBorderSize, entityBorderSize, entityBorderSize); + + // Aim assist expands the bounding box to hit entities within the specified distance of the ray trace. + if(aimAssist != 0) entityBounds = entityBounds.grow(aimAssist, aimAssist, aimAssist); + + // Finds the first point at which the ray trace intercepts the entity's bounding box, if any. + intercept = entityBounds.calculateIntercept(origin, endpoint); + + // If the ray trace hit the entity... + if(intercept != null){ + // Decides whether the entity that was hit is the closest so far, and if so, overwrites the old one. + float currentHitDistance = (float)intercept.hitVec.distanceTo(origin); + float closestHitDistance = (float)closestHitPosition.distanceTo(origin); + if(currentHitDistance < closestHitDistance){ + closestHitEntity = entity; + closestHitPosition = intercept.hitVec; } } } } + + // If the ray trace hit an entity, return that entity; otherwise return the result of the block ray trace. if(closestHitEntity != null){ - blockHit = new RayTraceResult(closestHitEntity); + result = new RayTraceResult(closestHitEntity, closestHitPosition); } - return blockHit; + + return result; } // SECTION Rendering and GUIs