package electroblob.wizardry.spell; import electroblob.wizardry.Wizardry; import electroblob.wizardry.data.IVariable; import electroblob.wizardry.data.Persistence; import electroblob.wizardry.data.WizardData; import electroblob.wizardry.item.ItemArtefact; import electroblob.wizardry.registry.Spells; import electroblob.wizardry.registry.WizardryItems; import electroblob.wizardry.util.ParticleBuilder; import electroblob.wizardry.util.ParticleBuilder.Type; import electroblob.wizardry.util.RayTracer; import electroblob.wizardry.util.SpellModifiers; import electroblob.wizardry.util.WizardryUtilities; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLiving; import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.item.EnumAction; import net.minecraft.network.play.server.SPacketEntityVelocity; import net.minecraft.util.EnumFacing; import net.minecraft.util.EnumHand; import net.minecraft.util.SoundEvent; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.RayTraceResult; import net.minecraft.util.math.Vec3d; import net.minecraft.world.World; import javax.annotation.Nullable; public class Grapple extends Spell { /** The speed at which the vine extends/retracts from the caster, in blocks per tick. */ public static final String EXTENSION_SPEED = "extension_speed"; /** The speed at which the vine reels in the caster or target, in blocks per tick. */ public static final String REEL_SPEED = "reel_speed"; public static final IVariable TARGET_KEY = new IVariable.Variable(Persistence.NEVER) .withTicker(Grapple::update); /** The distance from the target position at which the spell will stop reeling in entities. */ private static final double MINIMUM_REEL_DISTANCE = 3; /** The acceleration with which the vine reels in the caster or target. */ private static final double REEL_ACCELERATION = 0.3; /** The speed at which the caster or target is lowered when the caster is sneaking. */ private static final double PAYOUT_SPEED = 0.25; /** Once attached, the vine can stretch beyond the maximum range by this factor before breaking. */ private static final double STRETCH_LIMIT = 1.5; /** The distance between spawned particles. */ protected static final double PARTICLE_SPACING = 1.5; /** The maximum jitter (random position offset) for spawned particles. */ protected static final double PARTICLE_JITTER = 0.04; public Grapple(){ super("grapple", EnumAction.NONE, true); addProperties(RANGE, EXTENSION_SPEED, REEL_SPEED); } @Override public boolean canBeCastByNPCs(){ return super.canBeCastByNPCs(); } @Override public boolean canBeCastByDispensers(){ return true; } @Override protected SoundEvent[] createSounds(){ return this.createSoundsWithSuffixes("shoot", "attach", "pull", "release"); } @Override public boolean cast(World world, EntityPlayer caster, EnumHand hand, int ticksInUse, SpellModifiers modifiers){ WizardData data = WizardData.get(caster); Vec3d origin = new Vec3d(caster.posX, caster.getEntityBoundingBox().minY + caster.getEyeHeight(), caster.posZ); float extensionSpeed = getProperty(EXTENSION_SPEED).floatValue() * modifiers.get(SpellModifiers.POTENCY); RayTraceResult hit = data.getVariable(TARGET_KEY); // Initial targeting if(hit == null){ hit = findTarget(world, caster, origin, caster.getLookVec(), modifiers); data.setVariable(TARGET_KEY, hit); caster.swingArm(hand); // This condition prevents the sound playing every tick after a missed shot has finished extending if(hit.typeOfHit != RayTraceResult.Type.MISS || ticksInUse * extensionSpeed < getProperty(RANGE).floatValue() * modifiers.get(WizardryItems.range_upgrade)){ this.playSound(world, caster, ticksInUse, -1, modifiers, "shoot"); } } Vec3d target = hit.hitVec; if(hit.entityHit instanceof EntityLivingBase){ // If the target is an entity, we need to use the entity's centre rather than the original hit position // because the entity will have moved! target = new Vec3d(hit.entityHit.posX, hit.entityHit.getEntityBoundingBox().minY + hit.entityHit.height/2, hit.entityHit.posZ); } double distance = origin.distanceTo(target); Vec3d direction = target.subtract(origin).normalize(); double maxLength = getProperty(RANGE).floatValue() * modifiers.get(WizardryItems.range_upgrade) * STRETCH_LIMIT; // If the vine stretched too far if(distance > maxLength){ if(world.isRemote && (ticksInUse-1) * extensionSpeed < distance){ spawnLeafParticles(world, origin.subtract(0, SpellRay.Y_OFFSET, 0), direction, distance); } data.setVariable(TARGET_KEY, null); return false; // The spell is finished } boolean extending = ticksInUse * extensionSpeed < distance; if(extending){ // Extension if(world.isRemote){ // world.getTotalWorldTime() - ticksInUse generates a constant but unique seed each time the spell is cast ParticleBuilder.create(Type.VINE).entity(caster).pos(0, caster.getEyeHeight() - SpellRay.Y_OFFSET, 0) .target(origin.add(direction.scale(ticksInUse * extensionSpeed))).tvel(direction.scale(extensionSpeed)) .seed(world.getTotalWorldTime() - ticksInUse).spawn(world); } }else{ // Retraction Vec3d velocity = direction.scale(getProperty(REEL_SPEED).floatValue() * modifiers.get(SpellModifiers.POTENCY)); int retractTime = ticksInUse - (int)(distance/extensionSpeed); switch(hit.typeOfHit){ case BLOCK: // Payout if(caster.isSneaking() && ItemArtefact.isArtefactActive(caster, WizardryItems.charm_abseiling)) velocity = new Vec3d(velocity.x, distance < maxLength-1 ? -PAYOUT_SPEED : distance-maxLength+1, velocity.z); // Reel the caster towards the block hit double ax = (velocity.x - caster.motionX) * REEL_ACCELERATION; double ay = (velocity.y - caster.motionY) * REEL_ACCELERATION; double az = (velocity.z - caster.motionZ) * REEL_ACCELERATION; caster.addVelocity(ax, ay, az); if(caster.motionY > 0 && !Wizardry.settings.replaceVanillaFallDamage) caster.fallDistance = 0; // Reset fall distance if the caster moves upwards if(world.isRemote){ ParticleBuilder.create(Type.VINE).entity(caster).pos(0, caster.getEyeHeight() - SpellRay.Y_OFFSET, 0) .target(target).seed(world.getTotalWorldTime() - ticksInUse).spawn(world); } if(retractTime == 1){ // Just hit this.playSound(world, caster, ticksInUse, -1, modifiers, "pull"); this.playSound(world, hit.hitVec, ticksInUse, -1, modifiers, "attach"); } break; case ENTITY: // Payout if(caster.isSneaking() && ItemArtefact.isArtefactActive(caster, WizardryItems.charm_abseiling)) velocity = new Vec3d(velocity.x, distance < maxLength-1 ? PAYOUT_SPEED : maxLength-1-distance, velocity.z); // Reel the entity hit towards the caster Entity entity = hit.entityHit; if(distance > MINIMUM_REEL_DISTANCE){ double ax1 = (-velocity.x - entity.motionX) * REEL_ACCELERATION; double ay1 = (-velocity.y - entity.motionY) * REEL_ACCELERATION; double az1 = (-velocity.z - entity.motionZ) * REEL_ACCELERATION; entity.addVelocity(ax1, ay1, az1); // Player motion is handled on that player's client so needs packets if(entity instanceof EntityPlayerMP){ ((EntityPlayerMP)entity).connection.sendPacket(new SPacketEntityVelocity(entity)); } } if(world.isRemote){ ParticleBuilder.create(Type.VINE).entity(caster).pos(0, caster.getEyeHeight() - SpellRay.Y_OFFSET, 0) .target(entity).seed(world.getTotalWorldTime() - ticksInUse).spawn(world); } if(retractTime == 1){ // Just hit this.playSound(world, caster, ticksInUse, -1, modifiers, "pull"); this.playSound(world, entity.posX, entity.posY, entity.posZ, ticksInUse, -1, modifiers, "attach"); } break; default: // Missed if(world.isRemote && (ticksInUse-1) * extensionSpeed < distance){ spawnLeafParticles(world, origin.subtract(0, SpellRay.Y_OFFSET, 0), direction, distance); } //caster.resetActiveHand(); data.setVariable(TARGET_KEY, null); return false; // The spell is finished } } return true; } @Override public boolean cast(World world, EntityLiving caster, EnumHand hand, int ticksInUse, EntityLivingBase target, SpellModifiers modifiers){ Vec3d origin = new Vec3d(caster.posX, caster.getEntityBoundingBox().minY + caster.getEyeHeight(), caster.posZ); // If the target is an entity, we need to use the entity's centre rather than the original hit position // because the entity will have moved! Vec3d targetVec = new Vec3d(target.posX, target.getEntityBoundingBox().minY + target.height/2, target.posZ); double distance = origin.distanceTo(targetVec); // Can't cast the spell at all if the target is too far away if(ticksInUse <= 1 && distance > getProperty(RANGE).floatValue() * modifiers.get(WizardryItems.range_upgrade)) return false; Vec3d vec = targetVec.subtract(origin).normalize(); float extensionSpeed = getProperty(EXTENSION_SPEED).floatValue() * modifiers.get(SpellModifiers.POTENCY); // If the vine stretched too far if(distance > getProperty(RANGE).floatValue() * modifiers.get(WizardryItems.range_upgrade) * STRETCH_LIMIT){ if(world.isRemote && (ticksInUse-1) * extensionSpeed < distance){ spawnLeafParticles(world, origin.subtract(0, SpellRay.Y_OFFSET, 0), vec, distance); } return false; } Vec3d hookPosition; if(ticksInUse * extensionSpeed < distance){ // Extension hookPosition = origin.add(vec.scale(ticksInUse * extensionSpeed)); }else{ // Retraction Vec3d velocity = vec.scale(getProperty(REEL_SPEED).floatValue() * modifiers.get(SpellModifiers.POTENCY)); // Reel the entity hit towards the caster if(distance > MINIMUM_REEL_DISTANCE){ double ax1 = (-velocity.x - target.motionX) * REEL_ACCELERATION; double ay1 = (-velocity.y - target.motionY) * REEL_ACCELERATION; double az1 = (-velocity.z - target.motionZ) * REEL_ACCELERATION; target.addVelocity(ax1, ay1, az1); // Player motion is handled on that player's client so needs packets if(target instanceof EntityPlayerMP){ ((EntityPlayerMP)target).connection.sendPacket(new SPacketEntityVelocity(target)); } } hookPosition = targetVec; } if(world.isRemote){ // world.getTotalWorldTime() - ticksInUse generates a constant but unique seed each time the spell is cast ParticleBuilder.create(Type.VINE).pos(origin).target(hookPosition).tvel(vec.scale(extensionSpeed)) .seed(world.getTotalWorldTime() - ticksInUse).spawn(world); } return true; } @Override public boolean cast(World world, double x, double y, double z, EnumFacing direction, int ticksInUse, int duration, SpellModifiers modifiers){ Vec3d origin = new Vec3d(x, y, z); RayTraceResult result = findTarget(world, null, origin, new Vec3d(direction.getDirectionVec()), modifiers); if(result.entityHit instanceof EntityLivingBase){ Entity entity = result.entityHit; // If the target is an entity, we need to use the entity's centre rather than the original hit position // because the entity will have moved! Vec3d target = new Vec3d(entity.posX, entity.getEntityBoundingBox().minY + entity.height/2, entity.posZ); double distance = origin.distanceTo(target); Vec3d vec = target.subtract(origin).normalize(); float extensionSpeed = getProperty(EXTENSION_SPEED).floatValue() * modifiers.get(SpellModifiers.POTENCY); // If the vine stretched too far if(distance > getProperty(RANGE).floatValue() * modifiers.get(WizardryItems.range_upgrade) * STRETCH_LIMIT){ if(world.isRemote && (ticksInUse-1) * extensionSpeed < distance){ spawnLeafParticles(world, origin.subtract(0, SpellRay.Y_OFFSET, 0), vec, distance); } return false; } Vec3d hookPosition; if(ticksInUse * extensionSpeed < distance){ // Extension hookPosition = origin.add(vec.scale(ticksInUse * extensionSpeed)); }else{ // Retraction Vec3d velocity = vec.scale(getProperty(REEL_SPEED).floatValue() * modifiers.get(SpellModifiers.POTENCY)); // Reel the entity hit towards the caster if(distance > MINIMUM_REEL_DISTANCE){ double ax1 = (-velocity.x - entity.motionX) * REEL_ACCELERATION; double ay1 = (-velocity.y - entity.motionY) * REEL_ACCELERATION; double az1 = (-velocity.z - entity.motionZ) * REEL_ACCELERATION; entity.addVelocity(ax1, ay1, az1); // Player motion is handled on that player's client so needs packets if(entity instanceof EntityPlayerMP){ ((EntityPlayerMP)entity).connection.sendPacket(new SPacketEntityVelocity(entity)); } } hookPosition = target; } if(world.isRemote){ // world.getTotalWorldTime() - ticksInUse generates a constant but unique seed each time the spell is cast ParticleBuilder.create(Type.VINE).pos(origin).target(hookPosition).seed(world.getTotalWorldTime() - ticksInUse).spawn(world); } return true; } return false; } @Override public void finishCasting(World world, @Nullable EntityLivingBase caster, double x, double y, double z, EnumFacing facing, int duration, SpellModifiers modifiers){ Vec3d origin = null; Vec3d direction = null; Vec3d target = null; if(caster != null){ origin = new Vec3d(caster.posX, caster.getEntityBoundingBox().minY + caster.getEyeHeight(), caster.posZ); if(caster instanceof EntityPlayer){ WizardData data = WizardData.get((EntityPlayer)caster); if(data != null){ RayTraceResult hit = data.getVariable(TARGET_KEY); if(hit != null) target = hit.hitVec; } }else if(caster instanceof EntityLiving){ Entity entity = ((EntityLiving)caster).getAttackTarget(); if(entity != null) target = new Vec3d(entity.posX, entity.getEntityBoundingBox().minY + entity.height/2, entity.posZ); } if(target != null) direction = target.subtract(origin).normalize(); this.playSound(world, caster, duration, duration, modifiers, "release"); }else if(!Double.isNaN(x) && !Double.isNaN(y) && !Double.isNaN(z)){ origin = new Vec3d(x, y, z); direction = new Vec3d(facing.getDirectionVec()); RayTraceResult result = findTarget(world, null, origin, direction, modifiers); target = result.hitVec; this.playSound(world, origin, duration, duration, modifiers, "release"); } if(world.isRemote && origin != null && direction != null){ float extensionSpeed = getProperty(EXTENSION_SPEED).floatValue() * modifiers.get(SpellModifiers.POTENCY); double distance = Math.min(target.subtract(origin).length(), duration * extensionSpeed); spawnLeafParticles(world, origin, direction, distance); } } private void spawnLeafParticles(World world, Vec3d origin, Vec3d direction, double distance){ // Copied from SpellRay for(double d = PARTICLE_SPACING; d <= distance; d += PARTICLE_SPACING){ double x = origin.x + d*direction.x;// + PARTICLE_JITTER * (world.rand.nextDouble()*2 - 1); double y = origin.y + d*direction.y;// + PARTICLE_JITTER * (world.rand.nextDouble()*2 - 1); double z = origin.z + d*direction.z;// + PARTICLE_JITTER * (world.rand.nextDouble()*2 - 1); ParticleBuilder.create(Type.LEAF, world.rand, x, y, z, PARTICLE_JITTER, true).time(25 + world.rand.nextInt(5)).spawn(world); } } private RayTraceResult findTarget(World world, @Nullable EntityLivingBase caster, Vec3d origin, Vec3d direction, SpellModifiers modifiers){ double range = getProperty(RANGE).floatValue() * modifiers.get(WizardryItems.range_upgrade); Vec3d endpoint = origin.add(direction.scale(range)); RayTraceResult result = RayTracer.rayTrace(world, origin, endpoint, 0, false, true, false, Entity.class, RayTracer.ignoreEntityFilter(caster)); // Non-solid blocks (or if the result is null) count as misses if(result == null || result.typeOfHit == RayTraceResult.Type.BLOCK && !world.getBlockState(result.getBlockPos()).getMaterial().isSolid()){ return new RayTraceResult(RayTraceResult.Type.MISS, endpoint, EnumFacing.DOWN, new BlockPos(endpoint)); // Immovable entities count as misses too, but the endpoint is the hit vector instead }else if(result.entityHit != null && !result.entityHit.canBePushed()){ return new RayTraceResult(RayTraceResult.Type.MISS, result.hitVec, EnumFacing.DOWN, new BlockPos(endpoint)); } // If the ray trace missed, result.hitVec will be the endpoint anyway - neat! return result; } private static RayTraceResult update(EntityPlayer player, RayTraceResult grapplingTarget){ if(grapplingTarget != null && (!WizardryUtilities.isCasting(player, Spells.grapple) || (grapplingTarget.entityHit != null && !grapplingTarget.entityHit.isEntityAlive()))){ return null; } return grapplingTarget; } }