/** 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 io.netty.buffer.ByteBuf; import net.minecraft.block.Block; import net.minecraft.block.material.Material; import net.minecraft.block.state.IBlockState; import net.minecraft.enchantment.EnchantmentHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.monster.EntityEnderman; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.entity.projectile.EntityArrow; import net.minecraft.init.Items; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.network.play.server.S2BPacketChangeGameState; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.BlockPos; import net.minecraft.util.DamageSource; import net.minecraft.util.EntityDamageSourceIndirect; import net.minecraft.util.EnumParticleTypes; import net.minecraft.util.MathHelper; import net.minecraft.util.MovingObjectPosition; import net.minecraft.util.Vec3; import net.minecraft.world.World; import net.minecraftforge.fml.common.registry.IEntityAdditionalSpawnData; import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.fml.relauncher.SideOnly; import zeldaswordskills.ref.Sounds; import zeldaswordskills.util.TargetUtils; /** * * Base custom Arrow class, operates exactly as EntityArrow except it provides an easy framework * from which to extend and manipulate, specifically by breaking up the onUpdate method into * multiple overridable steps * */ public class EntityArrowCustom extends EntityArrow implements IEntityAdditionalSpawnData { /** Watchable object index for whether this arrow is a homing arrow */ private static final int HOMING_DATAWATCHER_INDEX = 23; /** Watchable object index for target entity's id */ private static final int TARGET_DATAWATCHER_INDEX = 24; /** Shooter's name, if shooter is a player - based on EntityThrowable's code */ private String shooterName = null; // Private fields with no getters from EntityArrow; instead of repeating the fields here, // it would be better to use use Reflection to get/set the actual values in EntityArrow, // or use ASM to make them public protected Block inTile; protected int inData, xTile = -1, yTile = -1, zTile = -1; protected int ticksInGround = 0, ticksInAir = 0; protected boolean inGround = false; // Also private - has a setter, but no getter >.< private int knockbackStrength; /** The item to return when picked up */ private Item arrowItem = Items.arrow; /** Basic constructor is necessary */ public EntityArrowCustom(World world) { super(world); } /** Constructs an arrow at a position, but with no heading or velocity */ public EntityArrowCustom(World world, double x, double y, double z) { super(world, x, y, z); } /** Constructs an arrow with heading based on shooter and velocity, modified by the arrow's velocityFactor */ public EntityArrowCustom(World world, EntityLivingBase shooter, float velocity) { super(world); renderDistanceWeight = 10.0D; shootingEntity = shooter; if (shooter instanceof EntityPlayer) { canBePickedUp = 1; } setSize(0.5F, 0.5F); setLocationAndAngles(shooter.posX, shooter.posY + (double) shooter.getEyeHeight(), shooter.posZ, shooter.rotationYaw, shooter.rotationPitch); posX -= (double)(MathHelper.cos(rotationYaw / 180.0F * (float) Math.PI) * 0.16F); posY -= 0.10000000149011612D; posZ -= (double)(MathHelper.sin(rotationYaw / 180.0F * (float) Math.PI) * 0.16F); setPosition(posX, posY, posZ); motionX = (double)(-MathHelper.sin(rotationYaw / 180.0F * (float) Math.PI) * MathHelper.cos(rotationPitch / 180.0F * (float) Math.PI)); motionZ = (double)(MathHelper.cos(rotationYaw / 180.0F * (float) Math.PI) * MathHelper.cos(rotationPitch / 180.0F * (float) Math.PI)); motionY = (double)(-MathHelper.sin(rotationPitch / 180.0F * (float) Math.PI)); setThrowableHeading(motionX, motionY, motionZ, velocity * getVelocityFactor(), 1.0F); } /** * Constructs an arrow heading towards target's initial position with given velocity, but abnormal Y trajectory; * @param wobble amount of deviation from base trajectory, used by Skeletons and the like; set to 0.0F for no x/z deviation */ public EntityArrowCustom(World world, EntityLivingBase shooter, EntityLivingBase target, float velocity, float wobble) { super(world, shooter, target, velocity, wobble); setTarget(target); } @Override protected void entityInit() { super.entityInit(); dataWatcher.addObject(TARGET_DATAWATCHER_INDEX, -1); dataWatcher.addObject(HOMING_DATAWATCHER_INDEX, Byte.valueOf((byte) 0)); } /** * Returns true if this arrow is a homing arrow */ protected boolean isHomingArrow() { return (dataWatcher.getWatchableObjectByte(HOMING_DATAWATCHER_INDEX) & 1) != 0; } /** * Sets whether this arrow is a homing arrow or not */ public void setHomingArrow(boolean isHoming) { dataWatcher.updateObject(HOMING_DATAWATCHER_INDEX, Byte.valueOf((byte)(isHoming ? 1 : 0))); } /** * Returns the shootingEntity, or if null, tries to get the shooting player from the world based on shooterName, if available */ public Entity getShooter() { if (shootingEntity == null && shooterName != null) { shootingEntity = worldObj.getPlayerEntityByName(shooterName); } return shootingEntity; } /** * Returns this arrow's current target, if any (for homing arrows only) */ protected EntityLivingBase getTarget() { int id = dataWatcher.getWatchableObjectInt(TARGET_DATAWATCHER_INDEX); return (id > 0 ? (EntityLivingBase) worldObj.getEntityByID(id) : null); } /** * Sets this arrow's current target (for homing arrows only) */ public void setTarget(EntityLivingBase target) { dataWatcher.updateObject(TARGET_DATAWATCHER_INDEX, target != null ? target.getEntityId() : -1); } /** * Sets the arrow item that will be added to the player's inventory when picked up */ public EntityArrowCustom setArrowItem(Item item) { arrowItem = item; return this; } @Override public void onUpdate() { // This calls the Entity class' update method directly, circumventing EntityArrow super.onEntityUpdate(); updateAngles(); checkInGround(); if (arrowShake > 0) { --arrowShake; } if (inGround) { updateInGround(); } else { updateInAir(); } } /** * Sets the velocity to the args. Args: x, y, z */ @Override @SideOnly(Side.CLIENT) public void setVelocity(double x, double y, double z) { if (prevRotationPitch == 0.0F && prevRotationYaw == 0.0F) { ticksInGround = 0; } super.setVelocity(x, y, z); } /** * Called by a player entity when they collide with an entity */ @Override public void onCollideWithPlayer(EntityPlayer player) { if (!worldObj.isRemote && inGround && arrowShake <= 0) { boolean flag = canBePickedUp == 1 || canBePickedUp == 2 && player.capabilities.isCreativeMode; if (canBePickedUp == 1 && !player.inventory.addItemStackToInventory(new ItemStack(arrowItem, 1, 0))) { flag = false; } if (flag) { playSound(Sounds.POP, 0.2F, ((rand.nextFloat() - rand.nextFloat()) * 0.7F + 1.0F) * 2.0F); player.onItemPickup(this, 1); setDead(); } } } /** Returns the damage source this arrow will use against the entity struck */ protected DamageSource getDamageSource(Entity entity) { return new EntityDamageSourceIndirect("arrow", this, shootingEntity).setProjectile(); } /** Returns whether this arrow can target the entity; used for Endermen */ protected boolean canTargetEntity(Entity entity) { return (!(entity instanceof EntityEnderman)); } /** Returns the amount of knockback the arrow applies when it hits a mob */ public int getKnockbackStrength() { return knockbackStrength; } /** Sets the amount of knockback the arrow applies when it hits a mob. */ public void setKnockbackStrength(int value) { knockbackStrength = value; } /** * Updates yaw and pitch based on current motion */ protected void updateAngles() { if (prevRotationPitch == 0.0F && prevRotationYaw == 0.0F) { float f = MathHelper.sqrt_double(motionX * motionX + motionZ * motionZ); prevRotationYaw = rotationYaw = (float)(Math.atan2(motionX, motionZ) * 180.0D / Math.PI); prevRotationPitch = rotationPitch = (float)(Math.atan2(motionY, (double) f) * 180.0D / Math.PI); } } /** * Updates the arrow's position and angles */ protected void updatePosition() { posX += motionX; posY += motionY; posZ += motionZ; float f = MathHelper.sqrt_double(motionX * motionX + motionZ * motionZ); rotationYaw = (float)(Math.atan2(motionX, motionZ) * 180.0D / Math.PI); for (rotationPitch = (float)(Math.atan2(motionY, (double) f) * 180.0D / Math.PI); rotationPitch - prevRotationPitch < -180.0F; prevRotationPitch -= 360.0F) { ; } while (rotationPitch - prevRotationPitch >= 180.0F) { prevRotationPitch += 360.0F; } while (rotationYaw - prevRotationYaw < -180.0F) { prevRotationYaw -= 360.0F; } while (rotationYaw - prevRotationYaw >= 180.0F) { prevRotationYaw += 360.0F; } rotationPitch = prevRotationPitch + (rotationPitch - prevRotationPitch) * 0.2F; rotationYaw = prevRotationYaw + (rotationYaw - prevRotationYaw) * 0.2F; float motionFactor = 0.99F; if (isInWater()) { for (int i = 0; i < 4; ++i) { float f3 = 0.25F; worldObj.spawnParticle(EnumParticleTypes.WATER_BUBBLE, posX - motionX * (double) f3, posY - motionY * (double) f3, posZ - motionZ * (double) f3, motionX, motionY, motionZ); } motionFactor = 0.8F; } updateMotion(motionFactor, getGravityVelocity()); setPosition(posX, posY, posZ); } /** * Adjusts arrow's motion: multiplies each by factor, subtracts adjustY from motionY */ protected void updateMotion(float factor, float adjustY) { EntityLivingBase target = getTarget(); if (isHomingArrow() && target != null) { double d0 = target.posX - this.posX; double d1 = target.getEntityBoundingBox().minY + (double)(target.height) - this.posY; double d2 = target.posZ - this.posZ; setThrowableHeading(d0, d1, d2, getVelocityFactor() * 2.0F, 1.0F); } else { motionX *= (double) factor; motionY *= (double) factor; motionZ *= (double) factor; motionY -= (double) adjustY; } } /** * Checks if entity is colliding with a block and if so, sets inGround to true */ protected void checkInGround() { BlockPos pos = new BlockPos(xTile, yTile, zTile); IBlockState state = worldObj.getBlockState(pos); Block block = state.getBlock(); if (block.getMaterial() != Material.air) { block.setBlockBoundsBasedOnState(worldObj, pos); AxisAlignedBB axisalignedbb = block.getCollisionBoundingBox(worldObj, pos, state); if (axisalignedbb != null && axisalignedbb.isVecInside(new Vec3(posX, posY, posZ))) { inGround = true; } } } /** * If entity is in ground, updates ticks in ground or adjusts position if block no longer in world */ protected void updateInGround() { BlockPos pos = new BlockPos(xTile, yTile, zTile); IBlockState state = worldObj.getBlockState(pos); Block block = state.getBlock(); if (block == inTile && block.getMetaFromState(state) == inData) { ++ticksInGround; if (ticksInGround == 1200) { setDead(); } } else { inGround = false; motionX *= (double)(rand.nextFloat() * 0.2F); motionY *= (double)(rand.nextFloat() * 0.2F); motionZ *= (double)(rand.nextFloat() * 0.2F); ticksInGround = 0; ticksInAir = 0; } } /** * Checks for impacts, spawns trailing particles and updates entity position */ protected void updateInAir() { ++ticksInAir; MovingObjectPosition mop = TargetUtils.checkForImpact(worldObj, this, getShooter(), 0.3D, ticksInAir >= 5); if (mop != null) { onImpact(mop); } spawnTrailingParticles(); updatePosition(); doBlockCollisions(); } /** Returns the arrow's velocity factor */ protected float getVelocityFactor() { return 1.5F; } /** Default gravity adjustment for arrows seems to be 0.05F */ protected float getGravityVelocity() { return 0.05F; } /** The name of the particle to spawn for trailing particle effects */ protected EnumParticleTypes getParticle() { return EnumParticleTypes.CRIT; } /** * Returns whether trailing particles should spawn (vanilla returns isCritical()) */ protected boolean shouldSpawnParticles() { return (getIsCritical() && getParticle() != null); } /** * Spawns trailing particles, if any */ protected void spawnTrailingParticles() { if (shouldSpawnParticles()) { for (int i = 0; i < 4; ++i) { worldObj.spawnParticle(getParticle(), posX + motionX * (double) i / 4.0D, posY + motionY * (double) i / 4.0D, posZ + motionZ * (double) i / 4.0D, -motionX, -motionY + 0.2D, -motionZ); } } } /** * Called when custom arrow impacts an entity or block */ protected void onImpact(MovingObjectPosition mop) { if (mop.entityHit != null) { onImpactEntity(mop); } else { onImpactBlock(mop); } } /** * Called when custom arrow impacts another entity */ protected void onImpactEntity(MovingObjectPosition mop) { if (mop.entityHit != null) { // make sure shootingEntity is correct, e.g. if loaded from NBT shootingEntity = getShooter(); float dmg = calculateDamage(mop.entityHit); if (dmg > 0) { if (isBurning() && canTargetEntity(mop.entityHit)) { mop.entityHit.setFire(5); } if (mop.entityHit.attackEntityFrom(getDamageSource(mop.entityHit), dmg)) { if (mop.entityHit instanceof EntityLivingBase) { handlePostDamageEffects((EntityLivingBase) mop.entityHit); if (shootingEntity instanceof EntityPlayerMP && mop.entityHit != shootingEntity && mop.entityHit instanceof EntityPlayer) { ((EntityPlayerMP) shootingEntity).playerNetServerHandler.sendPacket(new S2BPacketChangeGameState(6, 0.0F)); } } playSound(Sounds.BOW_HIT, 1.0F, 1.2F / (rand.nextFloat() * 0.2F + 0.9F)); if (canTargetEntity(mop.entityHit)) { setDead(); } } else { motionX *= -0.10000000149011612D; motionY *= -0.10000000149011612D; motionZ *= -0.10000000149011612D; rotationYaw += 180.0F; prevRotationYaw += 180.0F; ticksInAir = 0; } } } } /** * Called when custom arrow impacts a block */ protected void onImpactBlock(MovingObjectPosition mop) { BlockPos pos = mop.getBlockPos(); xTile = pos.getX(); yTile = pos.getY(); zTile = pos.getZ(); IBlockState state = worldObj.getBlockState(pos); inTile = state.getBlock(); inData = inTile.getMetaFromState(state); motionX = (double)((float)(mop.hitVec.xCoord - posX)); motionY = (double)((float)(mop.hitVec.yCoord - posY)); motionZ = (double)((float)(mop.hitVec.zCoord - posZ)); float f2 = MathHelper.sqrt_double(motionX * motionX + motionY * motionY + motionZ * motionZ); posX -= motionX / (double) f2 * 0.05000000074505806D; posY -= motionY / (double) f2 * 0.05000000074505806D; posZ -= motionZ / (double) f2 * 0.05000000074505806D; playSound(Sounds.BOW_HIT, 1.0F, 1.2F / (rand.nextFloat() * 0.2F + 0.9F)); inGround = true; arrowShake = 7; setIsCritical(false); if (inTile.getMaterial() != Material.air) { inTile.onEntityCollidedWithBlock(worldObj, pos, state, this); } } /** * Returns amount of damage arrow will inflict to entity impacted */ protected float calculateDamage(Entity entityHit) { float velocity = MathHelper.sqrt_double(motionX * motionX + motionY * motionY + motionZ * motionZ); float dmg = (float)(velocity * getDamage()); if (getIsCritical()) { dmg += rand.nextInt(MathHelper.ceiling_double_int(dmg) / 2 + 2); } return dmg; } /** * Handles all secondary effects if entity hit was damaged, such as knockback, thorns, etc. */ protected void handlePostDamageEffects(EntityLivingBase entityHit) { if (!worldObj.isRemote) { entityHit.setArrowCountInEntity(entityHit.getArrowCountInEntity() + 1); } int k = getKnockbackStrength(); if (k > 0) { float f3 = MathHelper.sqrt_double(motionX * motionX + motionZ * motionZ); if (f3 > 0.0F) { double knockback = (double) k * 0.6000000238418579D / (double) f3; entityHit.addVelocity(motionX * knockback, 0.1D, motionZ * knockback); } } if (shootingEntity instanceof EntityLivingBase) { EnchantmentHelper.applyThornEnchantments((EntityLivingBase) entityHit, shootingEntity); EnchantmentHelper.applyArthropodEnchantments((EntityLivingBase) shootingEntity, entityHit); } } @Override public void writeEntityToNBT(NBTTagCompound compound) { //super.writeEntityToNBT(compound); // writes the same data compound.setShort("xTile", (short) xTile); compound.setShort("yTile", (short) yTile); compound.setShort("zTile", (short) zTile); compound.setShort("life", (short) ticksInGround); // vanilla arrow uses Byte for the block; use Integer instead for modded blocks compound.setInteger("inTile", Block.getIdFromBlock(inTile)); compound.setByte("inData", (byte) inData); compound.setByte("shake", (byte) arrowShake); compound.setByte("inGround", (byte)(inGround ? 1 : 0)); compound.setByte("pickup", (byte) canBePickedUp); compound.setDouble("damage", getDamage()); compound.setInteger("arrowId", Item.getIdFromItem(arrowItem)); if ((shooterName == null || shooterName.length() == 0) && shootingEntity instanceof EntityPlayer) { shooterName = shootingEntity.getName(); } compound.setString("shooter", shooterName == null ? "" : shooterName); compound.setInteger("target", getTarget() == null ? -1 : getTarget().getEntityId()); } @Override public void readEntityFromNBT(NBTTagCompound compound) { //super.readEntityFromNBT(compound); xTile = compound.getShort("xTile"); yTile = compound.getShort("yTile"); zTile = compound.getShort("zTile"); ticksInGround = compound.getShort("life"); // vanilla arrow uses Byte for the block; use Integer instead for modded blocks // otherwise, could call super to have parent get correct values for private fields inTile = Block.getBlockById(compound.getInteger("inTile")); inData = compound.getByte("inData") & 255; arrowShake = compound.getByte("shake") & 255; inGround = compound.getByte("inGround") == 1; setDamage(compound.getDouble("damage")); canBePickedUp = compound.getByte("pickup"); arrowItem = (compound.hasKey("arrowId") ? Item.getItemById(compound.getInteger("arrowId")) : Items.arrow); shooterName = compound.getString("shooter"); if (shooterName != null && shooterName.length() == 0) { shooterName = null; } dataWatcher.updateObject(TARGET_DATAWATCHER_INDEX, compound.hasKey("target") ? compound.getInteger("target") : -1); } @Override public void writeSpawnData(ByteBuf buffer) { buffer.writeInt(shootingEntity != null ? shootingEntity.getEntityId() : -1); } @Override public void readSpawnData(ByteBuf buffer) { // Replicate EntityArrow's special spawn packet handling from NetHandlerPlayClient#handleSpawnObject: Entity shooter = worldObj.getEntityByID(buffer.readInt()); if (shooter instanceof EntityLivingBase) { // why check for EntityLivingBase when shootingEntity can be an Entity? shootingEntity = (EntityLivingBase) shooter; } } }