/** Copyright (C) <2017> <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.entity.ai; import java.util.Set; import net.minecraft.block.Block; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityCreature; import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.ai.EntityAIBase; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.BlockPos; import net.minecraft.util.EnumParticleTypes; import net.minecraft.util.MathHelper; import net.minecraft.util.Vec3; import net.minecraft.world.World; import net.minecraft.world.WorldServer; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.entity.living.EnderTeleportEvent; import zeldaswordskills.entity.player.ZSSPlayerSkills; /** * * AI for entities that teleport, whether randomly or upon some certain condition. * * Also provides static teleportation methods to use for custom conditions, using * Forge events to allow the AI delay timer to be set even when not teleporting * directly through the AI, thus preventing crazy Ender-like teleportation (hopefully). * * Uses MutexBit 8. * */ public class EntityAITeleport extends EntityAIBase { /** The task owner, i.e. the teleporting entity */ public final EntityCreature entity; /** Maximum teleportation range; y range is half this value */ public final double range; /** Range squared */ public final double rangeSq; /** Minimum delay before next teleport attempt */ public final int minTeleportDelay; /** The number of ticks before next teleport attempt will be made */ protected int teleportDelay; /** Incrementing timer; entity will teleport randomly when timer reaches teleportDelay */ protected int delayTimer; /** Additional timer for triggered teleports, set externally */ protected int triggerTimer; /** True to require entity to land upon solid ground when teleporting */ public final boolean isGrounded; /** True to allow entity to teleport randomly */ public final boolean randomTele; /** True to teleport towards current target if target is beyond current range */ public final boolean approachTele; /** True to teleport away from attack target when distance is too close */ public final boolean fleeTele; /** True to teleport away shortly after taking damage */ public final boolean hurtTele; /** Optional bounding box defining limits of teleportation - entity will not teleport outside of the defined boundary */ protected AxisAlignedBB teleBounds; /** True when the AI is already in the process of teleporting */ protected boolean isTeleporting; /** * * @param entity The task owner, i.e. the teleporting entity * @param teleRange Maximum teleportation range; y range is half this value * @param delay The minimum time before the entity will next attempt to teleport randomly or to the current target * @param grounded True to require entity to land upon solid ground when teleporting * @param random True to allow entity to teleport randomly * @param approach True to teleport to current target when target is out of range * @param flee True to teleport away from attack target when distance is too close * @param hurt True to teleport away shortly after taking damage */ public EntityAITeleport(EntityCreature entity, double teleRange, int delay, boolean grounded, boolean random, boolean approach, boolean flee, boolean hurt) { this.entity = entity; this.range = teleRange; this.rangeSq = teleRange * teleRange; this.minTeleportDelay = delay; this.isGrounded = grounded; this.randomTele = random; this.approachTele = approach; this.fleeTele = flee; this.hurtTele = hurt; this.setMutexBits(8); // compatible with all vanilla AI tasks, but not EntityAIUseMagic } /** * Returns the boundary within which the entity is allowed to teleport */ public AxisAlignedBB getTeleBounds() { return teleBounds; } /** * Sets (or removes) the boundary within which the entity is allowed to teleport. * * It is important for an entity with restricted bounds to frequently check * {@link #invalidateBounds}, otherwise it is possible for the entity to be * completely unable to teleport when no longer with range of its bounds. * * @param newBounds null is allowed */ public void setTeleBounds(AxisAlignedBB newBounds) { this.teleBounds = newBounds; } /** * Schedules a teleport to occur after a certain number of ticks, though * the entity may teleport sooner if other conditions are met first. */ public void scheduleNextTeleport(int ticks) { triggerTimer = (triggerTimer > 0 ? Math.min(triggerTimer, Math.max(0, ticks)) : Math.max(0, ticks)); } /** * Checks if the entity is too far outside of the teleportation bounding box, * in which case the bounds will be set to NULL and the entity will no longer * be restricted in its teleportation (aka it is in an 'unbound' state). * * Can happen if the entity uses mundane means of travel, such as walking. * * When bounds are invalidated, either accept the unbound state, set up new * bounds, or remove the teleportation AI task. * * @param rangeSq Distance squared at which the bounds will become invalid * @return True if the teleportation bounds are still valid or if there were no bounds to begin with */ public boolean invalidateBounds(double rangeSq) { if (teleBounds != null) { double x = teleBounds.minX + ((teleBounds.maxX - teleBounds.minX) / 2.0D); double y = teleBounds.minY + ((teleBounds.maxY - teleBounds.minY) / 2.0D); double z = teleBounds.minZ + ((teleBounds.maxZ - teleBounds.minZ) / 2.0D); if (entity.getDistanceSq(x, y, z) > rangeSq) { setTeleBounds(null); return false; } } return true; } @Override public void resetTask() { delayTimer = 0; triggerTimer = 0; isTeleporting = false; } @Override public boolean shouldExecute() { if (isTeleporting) { return false; } else if (randomTele) { return entity.isEntityAlive(); } return (entity.getAttackTarget() != null); } @Override public void updateTask() { boolean flag = false; EntityLivingBase target = entity.getAttackTarget(); ++delayTimer; if (triggerTimer > 0 && --triggerTimer == 0) { flag = true; } else if (randomTele && delayTimer > teleportDelay) { flag = true; } else if (fleeTele && delayTimer > (teleportDelay / 2) && target != null && entity.getDistanceSqToEntity(target) < range) { flag = true; } else if (hurtTele && entity.hurtResistantTime > 10 && delayTimer > entity.hurtResistantTime) { flag = true; // hurt time > 10 should restrict teleports to once per time hit } else if (approachTele && target != null && entity.getDistanceSqToEntity(target) > rangeSq) { if (teleBounds == null || teleBounds.isVecInside(new Vec3(target.posX, target.posY, target.posZ))) { if (!entity.worldObj.isRemote) { isTeleporting = true; for (int i = 0; i < 64; ++i) { if (teleportToEntity(entity.worldObj, entity, target)) { break; } } } } } if (flag && !entity.worldObj.isRemote) { teleportRandomly(); } } /** * Attempts to teleport randomly until successful, up to 64 times */ public boolean teleportRandomly() { isTeleporting = true; for (int i = 0; i < 64; ++i) { if (teleportRandomly(entity.worldObj, entity, range, teleBounds, isGrounded)) { return true; } } return false; } /** * Returns true if the entity has not teleported too recently. Should be used * before calling any of the static methods such as {@link #teleportRandomly} */ public boolean canTeleport() { return !isTeleporting && delayTimer > teleportDelay; } /** * Returns whether the AI is currently in the process of teleporting */ public boolean isTeleporting() { return isTeleporting; } /** * Sets {@link #isTeleporting} to true; do this before calling any of the static teleportation methods */ public void setTeleporting() { isTeleporting = true; } /** * Called when the parent entity posts {@link PostEnderTeleport} event */ public void onPostTeleport(double originX, double originY, double originZ) { teleportDelay = minTeleportDelay + entity.worldObj.rand.nextInt(minTeleportDelay * 2) - entity.worldObj.rand.nextInt((minTeleportDelay / 2) + 1); delayTimer = 0; triggerTimer = 0; isTeleporting = false; } /** * Teleport the enderman to a random nearby position * @param range The maximum range of the teleport; y range is half this value * @return True if the entity successfully teleported */ public static boolean teleportRandomly(World world, EntityLivingBase entity, double range) { return teleportRandomly(world, entity, range, null, true); } /** * Teleport the enderman to a random nearby position * @param range The maximum range of the teleport; y range is half this value * @param restriction Optional bounding box defining teleportation borders - entity will not teleport outside these bounds * @param grounded True to require entity to land upon solid ground when teleporting * @return True if the entity successfully teleported */ public static boolean teleportRandomly(World world, EntityLivingBase entity, double range, AxisAlignedBB restriction, boolean grounded) { int rangeY = (int) range; if (range < 1.0D || rangeY < 1) { return false; } double x = entity.posX + (world.rand.nextDouble() - 0.5D) * range; double y = entity.posY + (double)(world.rand.nextInt(rangeY) - (rangeY / 2)); double z = entity.posZ + (world.rand.nextDouble() - 0.5D) * range; return teleportTo(world, entity, x, y, z, restriction, grounded, true); } /** * Teleport the entity to somewhere near the target entity's position with no * bounding box restriction and requiring entity to land upon the ground * @return True if the entity successfully teleported */ public static boolean teleportToEntity(World world, EntityLivingBase entity, Entity target) { return teleportToEntity(world, entity, target, null, true); } /** * Teleport the entity to somewhere near the target entity's position * @param restriction Optional bounding box defining teleportation borders - entity will not teleport outside these bounds * @param grounded True to require entity to land upon solid ground when teleporting * @return True if the entity successfully teleported */ public static boolean teleportToEntity(World world, EntityLivingBase entity, Entity target, AxisAlignedBB restriction, boolean grounded) { Vec3 vec3 = new Vec3(entity.posX - target.posX, entity.getEntityBoundingBox().minY + (double)(entity.height / 2.0F) - target.posY + (double) target.getEyeHeight(), entity.posZ - target.posZ); vec3 = vec3.normalize(); double d0 = 16.0D; double x = entity.posX + (world.rand.nextDouble() - 0.5D) * 8.0D - vec3.xCoord * d0; double y = entity.posY + (double)(world.rand.nextInt(16) - 8) - vec3.yCoord * d0; double z = entity.posZ + (world.rand.nextDouble() - 0.5D) * 8.0D - vec3.zCoord * d0; return teleportTo(world, entity, x, y, z, restriction, grounded, true); } /** * Teleport the entity to the position specified, adjusting y coordinate as necessary * @return True if the entity successfully teleported */ public static boolean teleportTo(World world, EntityLivingBase entity, double x, double y, double z) { return teleportTo(world, entity, x, y, z, null, true, true); } /** * Teleport the entity to the position specified, adjusting y coordinate as necessary * @param restriction Optional bounding box defining teleportation borders - entity will not teleport outside these bounds' * @param grounded True to require entity to land upon solid ground when teleporting * @param noLiquid True to prevent entity from teleporting into liquids * @return True if the entity successfully teleported */ public static boolean teleportTo(World world, EntityLivingBase entity, double x, double y, double z, AxisAlignedBB restriction, boolean grounded, boolean noLiquid) { EnderTeleportEvent event = new EnderTeleportEvent(entity, x, y, z, 0); if (MinecraftForge.EVENT_BUS.post(event)) { return false; } double d3 = entity.posX; double d4 = entity.posY; double d5 = entity.posZ; entity.posX = event.targetX; entity.posY = event.targetY; entity.posZ = event.targetZ; boolean flag = false; int i = MathHelper.floor_double(entity.posX); int j = MathHelper.floor_double(entity.posY); int k = MathHelper.floor_double(entity.posZ); boolean foundSolidBlock = !grounded; if (grounded) { while (!foundSolidBlock && j > 1) { Block block = entity.worldObj.getBlockState(new BlockPos(i, j - 1, k)).getBlock(); if (block.getMaterial().blocksMovement()) { foundSolidBlock = true; } else { --entity.posY; --j; } } } if (foundSolidBlock) { entity.setPosition(entity.posX, entity.posY, entity.posZ); if (restriction != null && !restriction.isVecInside(new Vec3(entity.posX, entity.posY, entity.posZ))) { flag = false; } else if (world.getCollidingBoundingBoxes(entity, entity.getEntityBoundingBox()).isEmpty() && (!noLiquid || !world.isAnyLiquid(entity.getEntityBoundingBox()))) { flag = true; } } if (!flag) { entity.setPosition(d3, d4, d5); return false; } else { if (entity instanceof EntityPlayer) { entity.setPositionAndUpdate(entity.posX, entity.posY, entity.posZ); } for (int l = 0; l < 128; ++l) { double d6 = (double) l / 127.0D; float f = (world.rand.nextFloat() - 0.5F) * 0.2F; float f1 = (world.rand.nextFloat() - 0.5F) * 0.2F; float f2 = (world.rand.nextFloat() - 0.5F) * 0.2F; double d7 = d3 + (entity.posX - d3) * d6 + (world.rand.nextDouble() - 0.5D) * (double) entity.width * 2.0D; double d8 = d4 + (entity.posY - d4) * d6 + world.rand.nextDouble() * (double) entity.height; double d9 = d5 + (entity.posZ - d5) * d6 + (world.rand.nextDouble() - 0.5D) * (double) entity.width * 2.0D; entity.worldObj.spawnParticle(EnumParticleTypes.PORTAL, d7, d8, d9, (double) f, (double) f1, (double) f2); } entity.worldObj.playSoundEffect(d3, d4, d5, "mob.endermen.portal", 1.0F, 1.0F); entity.playSound("mob.endermen.portal", 1.0F, 1.0F); MinecraftForge.EVENT_BUS.post(new PostEnderTeleport(entity, d3, d4, d5, 0)); return true; } } /** * Call to disengage any ILockOnTargets tracking the entity */ public static void disruptTargeting(EntityLivingBase entity) { if (entity.worldObj instanceof WorldServer) { Set<? extends EntityPlayer> players = ((WorldServer) entity.worldObj).getEntityTracker().getTrackingPlayers(entity); for (EntityPlayer player : players) { ZSSPlayerSkills skills = ZSSPlayerSkills.get(player); if (skills.getTargetingSkill() != null && skills.getTargetingSkill().getCurrentTarget() == entity) { skills.getTargetingSkill().setCurrentTarget(player, null); } } } } /** * Event posted after a successful ender teleport; not cancelable and changing fields has no effect */ public static class PostEnderTeleport extends EnderTeleportEvent { /** * Post teleport event with the original position of the entity; the entity has already been set to its new position. */ public PostEnderTeleport(EntityLivingBase entity, double originX, double originY, double originZ, float damage) { super(entity, originX, originY, originZ, damage); } } }