/** 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.projectile; import net.minecraft.block.Block; import net.minecraft.block.BlockPane; import net.minecraft.block.material.Material; import net.minecraft.block.state.IBlockState; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.SharedMonsterAttributes; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.entity.projectile.EntityThrowable; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.BlockPos; import net.minecraft.util.DamageSource; import net.minecraft.util.EnumFacing; import net.minecraft.util.EnumParticleTypes; import net.minecraft.util.MovingObjectPosition; import net.minecraft.util.MovingObjectPosition.MovingObjectType; import net.minecraft.world.World; import net.minecraftforge.fml.common.eventhandler.Event.Result; import zeldaswordskills.api.block.IHookable; import zeldaswordskills.api.block.IHookable.HookshotType; import zeldaswordskills.api.damage.DamageUtils.DamageSourceBaseIndirect; import zeldaswordskills.api.item.ArmorIndex; import zeldaswordskills.entity.ZSSEntityInfo; import zeldaswordskills.entity.buff.Buff; import zeldaswordskills.item.ItemHookShot; import zeldaswordskills.item.ZSSItems; import zeldaswordskills.network.PacketDispatcher; import zeldaswordskills.network.client.UnpressKeyPacket; import zeldaswordskills.ref.Config; import zeldaswordskills.ref.Sounds; import zeldaswordskills.util.TargetUtils; /** * * The hookshot entity should travel up to 16 blocks, locking on to any impacted block if the * material is appropriate for the shot type. * * The entity returned from getThrower() is the entity that will travel to the location struck * so long as it is holding and using an ItemHookShot of the appropriate type. * */ public class EntityHookShot extends EntityThrowable { /** Watchable object index for thrower entity's id */ protected static final int THROWER_INDEX = 22; /** Watchable object index for target entity's id */ protected static final int TARGET_INDEX = 23; /** Watchable object index for hookshot's type */ protected static final int SHOTTYPE_INDEX = 24; /** Watchable object for if the player has reached the hookshot */ protected static final int IN_GROUND_INDEX = 25; /** * Watchable objects for hookshot's impact position - prevents client from occasionally trying to go to 0,0,0 (due to client values not being set) * Used instead of super's integer versions and set to the center of the face struck to prevent the player's motion from freaking out */ public static final int HIT_POS_X = 26, HIT_POS_Y = 27, HIT_POS_Z = 28; /** Needed since 1.8 to adjust the position used to determine whether the player has reached the hook or not */ public static final int SIDE_HIT = 29; /** Stops reeling player in when true */ protected boolean reachedHook = false; public EntityHookShot(World world) { super(world); } public EntityHookShot(World world, EntityLivingBase entity) { super(world, entity); } public EntityHookShot(World world, double x, double y, double z) { super(world, x, y, z); } @Override protected void entityInit() { super.entityInit(); setSize(0.25F, 0.25F); dataWatcher.addObject(THROWER_INDEX, ""); dataWatcher.addObject(TARGET_INDEX, -1); dataWatcher.addObject(SHOTTYPE_INDEX, HookshotType.WOOD_SHOT.ordinal()); dataWatcher.addObject(IN_GROUND_INDEX, (byte) 0); dataWatcher.addObject(HIT_POS_X, 0.0F); dataWatcher.addObject(HIT_POS_Y, 0.0F); dataWatcher.addObject(HIT_POS_Z, 0.0F); dataWatcher.addObject(SIDE_HIT, 0); } /** * Return's this entity's hookshot Type */ public HookshotType getType() { return HookshotType.values()[dataWatcher.getWatchableObjectInt(SHOTTYPE_INDEX) % HookshotType.values().length]; } /** * Sets the shot's type; returns itself for convenience */ public EntityHookShot setType(HookshotType type) { dataWatcher.updateObject(SHOTTYPE_INDEX, type.ordinal()); return this; } public int getMaxDistance() { return Config.getHookshotRange() * (getType().isExtended() ? 2 : 1); } public void setThrower(EntityPlayer player) { dataWatcher.updateObject(THROWER_INDEX, player != null ? player.getName() : ""); } protected Entity getTarget() { int id = dataWatcher.getWatchableObjectInt(TARGET_INDEX); return (id == -1 ? null : worldObj.getEntityByID(id)); } protected void setTarget(Entity entity) { dataWatcher.updateObject(TARGET_INDEX, entity != null ? entity.getEntityId() : -1); } @Override public EntityLivingBase getThrower() { String name = dataWatcher.getWatchableObjectString(THROWER_INDEX); return (name.equals("") ? null : worldObj.getPlayerEntityByName(name)); } public boolean isInGround() { return (dataWatcher.getWatchableObjectByte(IN_GROUND_INDEX) & 1) == 1; } protected void setInGround(boolean isInGround) { dataWatcher.updateObject(IN_GROUND_INDEX, isInGround ? (byte) 1 : (byte) 0); inGround = isInGround; } /** Returns a hookshot damage source */ protected DamageSource getDamageSource() { return new DamageSourceBaseIndirect("hookshot", this, getThrower()).setStunDamage(50, 1, true).setProjectile(); } /** * Returns true if the block at the given position can be grappled by this type of hookshot */ protected boolean canGrabBlock(Block block, BlockPos pos, EnumFacing face) { Material material = block.getMaterial(); Result result = Result.DEFAULT; if (block instanceof IHookable) { result = ((IHookable) block).canGrabBlock(getType(), worldObj, pos, face); material = ((IHookable) block).getHookableMaterial(getType(), worldObj, pos, face); } else if (Config.allowHookableOnly()) { return false; } switch (result) { case DEFAULT: switch (getType()) { case WOOD_SHOT: case WOOD_SHOT_EXT: return material == Material.wood; case CLAW_SHOT: case CLAW_SHOT_EXT: return material == Material.rock || (block instanceof BlockPane && material == Material.iron); case MULTI_SHOT: case MULTI_SHOT_EXT: return material == Material.wood || material == Material.rock || material == Material.ground || material == Material.grass || material == Material.clay; } default: return (result == Result.ALLOW); } } /** * Returns true if the hookshot can destroy the material type */ protected boolean canDestroyBlock(Block block, BlockPos pos, EnumFacing face) { Result result = Result.DEFAULT; if (block instanceof IHookable) { result = ((IHookable) block).canDestroyBlock(getType(), worldObj, pos, face); } else if (Config.allowHookableOnly()) { return false; } switch (result) { case DEFAULT: boolean isBreakable = block.getBlockHardness(worldObj, pos) >= 0.0F; boolean canPlayerEdit = false; if (getThrower() instanceof EntityPlayer) { canPlayerEdit = ((EntityPlayer) getThrower()).capabilities.allowEdit && Config.canHookshotBreakBlocks(); } Material m = block.getMaterial(); return (isBreakable && canPlayerEdit && (m == Material.glass || (m == Material.wood && getType().ordinal() / 2 == (HookshotType.CLAW_SHOT.ordinal() / 2)))); default: return result == Result.ALLOW; } } @Override protected float getVelocity() { return 1.25F; } @Override protected float getGravityVelocity() { return 0.0F; } @Override protected void onImpact(MovingObjectPosition mop) { if (mop.typeOfHit == MovingObjectType.BLOCK && getTarget() == null) { BlockPos pos = mop.getBlockPos(); IBlockState state = worldObj.getBlockState(pos); Block block = state.getBlock(); if (!block.getMaterial().blocksMovement()) { return; } if (block.getMaterial() != Material.air) { block.onEntityCollidedWithBlock(worldObj, pos, this); } if (!isInGround() && ticksExisted < getMaxDistance()) { motionX = motionY = motionZ = 0.0D; if (canGrabBlock(block, pos, mop.sideHit)) { setInGround(true); // adjusting posX/Y/Z here seems to make no difference to the rendering, even when client side makes same changes posX = (double) pos.getX() + 0.5D; posY = (double) pos.getY() + 0.5D; posZ = (double) pos.getZ() + 0.5D; switch (mop.sideHit) { case EAST: posX += 0.5D; break; case WEST: posX -= 0.515D; break; // a little extra to compensate for block border, otherwise renders black case SOUTH: posZ += 0.5D; break; case NORTH: posZ -= 0.515D; break; // a little extra to compensate for block border, otherwise renders black case UP: posY = pos.getY() + 1.0D; break; case DOWN: posY = pos.getY(); break; } // however, setting position as watched values and using these on the client works... weird dataWatcher.updateObject(HIT_POS_X, (float) posX); dataWatcher.updateObject(HIT_POS_Y, (float) posY); dataWatcher.updateObject(HIT_POS_Z, (float) posZ); dataWatcher.updateObject(SIDE_HIT, mop.sideHit.getIndex()); } else if (!worldObj.isRemote) { if (canDestroyBlock(block, pos, mop.sideHit)) { worldObj.destroyBlock(pos, false); } setDead(); } } if (!worldObj.isRemote) { worldObj.playSoundAtEntity(this, block.stepSound.getBreakSound(), 1.0F, 1.0F); } else { if (block.getRenderType() != -1) { int stateId = Block.getStateId(state); for (int i = 0; i < 10; ++i) { worldObj.spawnParticle(EnumParticleTypes.BLOCK_CRACK, posX, posY, posZ, rand.nextGaussian(), rand.nextGaussian(), rand.nextGaussian(), stateId); } } } } else if (mop.entityHit != null && getTarget() == null) { mop.entityHit.attackEntityFrom(getDamageSource(), 1.0F); worldObj.playSoundAtEntity(mop.entityHit, Sounds.WOOD_CLICK, 1.0F, 1.0F); EntityPlayer player = (getThrower() instanceof EntityPlayer ? (EntityPlayer) getThrower() : null); if (player != null && player.getCurrentArmor(ArmorIndex.WORN_BOOTS) != null && player.getCurrentArmor(ArmorIndex.WORN_BOOTS).getItem() == ZSSItems.bootsHeavy && player.isSneaking()) { setTarget(mop.entityHit); motionX = -motionX; motionY = -motionY; motionZ = -motionZ; } else { setDead(); } } } @Override public void onUpdate() { // Added DataWatcher to track inGround separately from EntityThrowable, and // avoid the super.onUpdate if the hookshot is in the ground; not sure what // changed from 1.6.4, but EntityThrowable's onUpdate is no longer working // acceptably for the Hookshot if (isInGround()) { super.onEntityUpdate(); } else { super.onUpdate(); } if (canUpdate()) { if ((ticksExisted > getMaxDistance() && !isInGround() && getTarget() == null) || ticksExisted > (getMaxDistance() * 8)) { if (worldObj.isRemote && getThrower() instanceof EntityPlayer && Config.enableHookshotSound) { ((EntityPlayer) getThrower()).playSound(Sounds.WOOD_CLICK, 1.0F, 1.0F); } setDead(); } else if (getTarget() != null) { pullTarget(); } else { pullThrower(); } } else { setDead(); } } @Override public void setDead() { super.setDead(); if (getThrower() instanceof EntityPlayerMP && !worldObj.isRemote) { PacketDispatcher.sendTo(new UnpressKeyPacket(UnpressKeyPacket.RMB), (EntityPlayerMP) getThrower()); } } /** * Returns true if the hookshot is allowed to update (i.e. thrower is holding correct item, etc.) */ protected boolean canUpdate() { EntityLivingBase thrower = getThrower(); if (!isDead && thrower instanceof EntityPlayer && ((EntityPlayer) thrower).isUsingItem()) { ItemStack stack = thrower.getHeldItem(); // getItemInUse() is client-side only return stack.getItem() instanceof ItemHookShot && ((ItemHookShot) stack.getItem()).getType(stack.getItemDamage()) == getType(); } else { return false; } } /** * Attempts to pull the thrower towards the hookshot's position; * canUpdate() should return true before this method is called */ protected void pullThrower() { EntityLivingBase thrower = getThrower(); if (thrower != null && isInGround()) { thrower.fallDistance = 0.0F; double hitPosY = dataWatcher.getWatchableObjectFloat(HIT_POS_Y); switch (EnumFacing.getFront(dataWatcher.getWatchableObjectInt(SIDE_HIT))) { case UP: break; // do nothing case DOWN: hitPosY -= thrower.getEyeHeight(); break; // subtract thrower's eye height default: hitPosY -= (thrower.getEyeHeight() / 1.5F); break; // subtract 1/3 of the thrower's eye height } double d = thrower.getDistanceSq(dataWatcher.getWatchableObjectFloat(HIT_POS_X), hitPosY, dataWatcher.getWatchableObjectFloat(HIT_POS_Z)); if (!reachedHook) { reachedHook = d < 1.0D; } if (reachedHook && thrower.isSneaking()) { thrower.motionX = thrower.motionZ = 0.0D; thrower.motionY = -0.15D; } else if (reachedHook && d < 1.0D) { thrower.motionX = thrower.motionY = thrower.motionZ = 0.0D; } else { double dx = 0.15D * (dataWatcher.getWatchableObjectFloat(HIT_POS_X) - thrower.posX); double dy = 0.15D * (hitPosY - thrower.posY); // + (this.height / 3.0F) double dz = 0.15D * (dataWatcher.getWatchableObjectFloat(HIT_POS_Z) - thrower.posZ); TargetUtils.setEntityHeading(thrower, dx, dy, dz, 1.0F, 1.0F, true); } } } /** * Pulls target to player; already checked if player is wearing Heavy Boots */ protected void pullTarget() { Entity target = getTarget(); EntityLivingBase thrower = getThrower(); if (target != null && thrower != null) { if (target instanceof EntityLivingBase) { ZSSEntityInfo.get((EntityLivingBase) target).removeBuff(Buff.STUN); } double d = target.getDistanceSq(thrower.posX, thrower.posY, thrower.posZ); if (!reachedHook) { reachedHook = d < 9.0D; } if (reachedHook && d < 9.0D) { target.motionX = target.motionY = target.motionZ = 0.0D; motionX = motionY = motionZ = 0.0D; } else { double dx = 0.15D * (thrower.posX - target.posX); double dy = 0.15D * (thrower.posY + (this.height / 3.0F) - target.posY); double dz = 0.15D * (thrower.posZ - target.posZ); if (target instanceof EntityLivingBase) { double resist = 1.0D - ((EntityLivingBase) target).getEntityAttribute(SharedMonsterAttributes.knockbackResistance).getAttributeValue(); dx *= resist; dy *= resist; dz *= resist; } TargetUtils.setEntityHeading(target, dx, dy, dz, 1.0F, 1.0F, true); } } } @Override public void writeEntityToNBT(NBTTagCompound compound) { super.writeEntityToNBT(compound); compound.setFloat("hitPosX", dataWatcher.getWatchableObjectFloat(HIT_POS_X)); compound.setFloat("hitPosY", dataWatcher.getWatchableObjectFloat(HIT_POS_Y)); compound.setFloat("hitPosZ", dataWatcher.getWatchableObjectFloat(HIT_POS_Z)); compound.setInteger("sideHit", dataWatcher.getWatchableObjectInt(SIDE_HIT)); compound.setByte("customInGround", (byte)(isInGround() ? 1 : 0)); compound.setByte("reachedHook", (byte)(reachedHook ? 1 : 0)); compound.setByte("shotType", (byte) getType().ordinal()); compound.setInteger("shotTarget", getTarget() != null ? getTarget().getEntityId() : -1); } @Override public void readEntityFromNBT(NBTTagCompound compound) { super.readEntityFromNBT(compound); dataWatcher.updateObject(HIT_POS_X, compound.getFloat("hitPosX")); dataWatcher.updateObject(HIT_POS_Y, compound.getFloat("hitPosY")); dataWatcher.updateObject(HIT_POS_Z, compound.getFloat("hitPosZ")); dataWatcher.updateObject(SIDE_HIT, compound.getInteger("sideHit")); dataWatcher.updateObject(IN_GROUND_INDEX, compound.getByte("customInGround")); reachedHook = (compound.getByte("reachedHook") == 1); dataWatcher.updateObject(THROWER_INDEX, compound.getString("ownerName")); dataWatcher.updateObject(SHOTTYPE_INDEX, HookshotType.values()[compound.getByte("shotType") % HookshotType.values().length]); dataWatcher.updateObject(TARGET_INDEX, compound.getInteger("shotTarget")); } }