package slimeknights.tconstruct.library.entity; import com.google.common.collect.Multimap; import net.minecraft.block.Block; 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.ai.attributes.AttributeModifier; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.projectile.EntityArrow; import net.minecraft.init.SoundEvents; import net.minecraft.inventory.EntityEquipmentSlot; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.EnumFacing; import net.minecraft.util.EnumParticleTypes; import net.minecraft.util.math.AxisAlignedBB; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.MathHelper; import net.minecraft.util.math.RayTraceResult; import net.minecraft.util.math.Vec3d; import net.minecraft.world.World; import net.minecraftforge.common.capabilities.Capability; import net.minecraftforge.fml.common.network.ByteBufUtils; import net.minecraftforge.fml.common.registry.IEntityAdditionalSpawnData; import java.util.UUID; import javax.annotation.Nonnull; import javax.annotation.Nullable; import io.netty.buffer.ByteBuf; import slimeknights.tconstruct.common.Sounds; import slimeknights.tconstruct.library.capability.projectile.CapabilityTinkerProjectile; import slimeknights.tconstruct.library.capability.projectile.TinkerProjectileHandler; import slimeknights.tconstruct.library.events.ProjectileEvent; import slimeknights.tconstruct.library.tools.ToolCore; import slimeknights.tconstruct.library.tools.ranged.ILauncher; import slimeknights.tconstruct.library.tools.ranged.IProjectile; import slimeknights.tconstruct.library.traits.IProjectileTrait; import slimeknights.tconstruct.library.utils.AmmoHelper; import slimeknights.tconstruct.library.utils.TagUtil; import slimeknights.tconstruct.library.utils.Tags; import slimeknights.tconstruct.library.utils.ToolHelper; // have to base this on EntityArrow, otherwise minecraft does derp things because everything is handled based on class. public abstract class EntityProjectileBase extends EntityArrow implements IEntityAdditionalSpawnData { protected static final UUID PROJECTILE_POWER_MODIFIER = UUID.fromString("c6aefc21-081a-4c4a-b076-8f9d6cef9122"); public TinkerProjectileHandler tinkerProjectile = new TinkerProjectileHandler(); public boolean bounceOnNoDamage = true; public boolean defused = false; // if this is true it wont hit any entities anymore public EntityProjectileBase(World world) { super(world); init(); } public EntityProjectileBase(World world, double d, double d1, double d2) { this(world); this.setPosition(d, d1, d2); } public EntityProjectileBase(World world, EntityPlayer player, float speed, float inaccuracy, float power, ItemStack stack, ItemStack launchingStack) { this(world); this.shootingEntity = player; pickupStatus = player.isCreative() ? PickupStatus.CREATIVE_ONLY : PickupStatus.ALLOWED; // stuff from the arrow this.setLocationAndAngles(player.posX, player.posY + (double) player.getEyeHeight(), player.posZ, player.rotationYaw, player.rotationPitch); this.setPosition(this.posX, this.posY, this.posZ); //this.yOffset = 0.0F; this.motionX = -MathHelper.sin(this.rotationYaw / 180.0F * (float) Math.PI) * MathHelper.cos(this.rotationPitch / 180.0F * (float) Math.PI); this.motionZ = +MathHelper.cos(this.rotationYaw / 180.0F * (float) Math.PI) * MathHelper.cos(this.rotationPitch / 180.0F * (float) Math.PI); this.motionY = -MathHelper.sin(this.rotationPitch / 180.0F * (float) Math.PI); this.setThrowableHeading(this.motionX, this.motionY, this.motionZ, speed, inaccuracy); // our stuff tinkerProjectile.setItemStack(stack); tinkerProjectile.setLaunchingStack(launchingStack); tinkerProjectile.setPower(power); for(IProjectileTrait trait : tinkerProjectile.getProjectileTraits()) { trait.onLaunch(this, world, player); } } protected void init() { } @Override public boolean hasCapability(@Nonnull Capability<?> capability, @Nonnull EnumFacing facing) { return capability == CapabilityTinkerProjectile.PROJECTILE_CAPABILITY || super.hasCapability(capability, facing); } @Nonnull @Override public <T> T getCapability(@Nonnull Capability<T> capability, @Nonnull EnumFacing facing) { if(capability == CapabilityTinkerProjectile.PROJECTILE_CAPABILITY) { return (T) tinkerProjectile; } return super.getCapability(capability, facing); } public boolean isDefused() { return defused; } protected void defuse() { this.defused = true; } @Nonnull @Override protected ItemStack getArrowStack() { return tinkerProjectile.getItemStack(); } protected void playHitBlockSound(float speed, IBlockState state) { Material material = state.getMaterial(); if(material == Material.WOOD) { this.playSound(Sounds.wood_hit, 1f, 1f); } else if(material == Material.ROCK) { this.playSound(Sounds.stone_hit, 1f, 1f); } this.playSound(state.getBlock().getSoundType().getStepSound(), 0.8f, 1.0f); } protected void playHitEntitySound() { this.playSound(SoundEvents.ENTITY_ARROW_HIT, 1.0F, 1.2F / (this.rand.nextFloat() * 0.2F + 0.9F)); } /** * How deep the item enters stuff it hits. Best experiment. */ public double getStuckDepth() { return 0.4f; } protected void onEntityHit(Entity entityHit) { setDead(); } protected float getSpeed() { return MathHelper.sqrt(this.motionX * this.motionX + this.motionY * this.motionY + this.motionZ * this.motionZ); } public void onHitBlock(RayTraceResult raytraceResult) { BlockPos blockpos = raytraceResult.getBlockPos(); this.xTile = blockpos.getX(); this.yTile = blockpos.getY(); this.zTile = blockpos.getZ(); IBlockState iblockstate = this.getEntityWorld().getBlockState(blockpos); this.inTile = iblockstate.getBlock(); this.inData = this.inTile.getMetaFromState(iblockstate); this.motionX = (double) ((float) (raytraceResult.hitVec.xCoord - this.posX)); this.motionY = (double) ((float) (raytraceResult.hitVec.yCoord - this.posY)); this.motionZ = (double) ((float) (raytraceResult.hitVec.zCoord - this.posZ)); float speed = getSpeed(); this.posX -= this.motionX / (double) speed * 0.05000000074505806D; this.posY -= this.motionY / (double) speed * 0.05000000074505806D; this.posZ -= this.motionZ / (double) speed * 0.05000000074505806D; playHitBlockSound(speed, iblockstate); ProjectileEvent.OnHitBlock.fireEvent(this, speed, blockpos, iblockstate); this.inGround = true; this.arrowShake = 7; this.setIsCritical(false); if(iblockstate.getMaterial() != Material.AIR) { this.inTile.onEntityCollidedWithBlock(this.getEntityWorld(), blockpos, iblockstate, this); } defuse(); // defuse it so it doesn't hit stuff anymore, being weird } public void onHitEntity(RayTraceResult raytraceResult) { ItemStack item = tinkerProjectile.getItemStack(); ItemStack launcher = tinkerProjectile.getLaunchingStack(); boolean bounceOff = false; Entity entityHit = raytraceResult.entityHit; // deal damage if we have everything if(item != null && item.getItem() instanceof ToolCore && this.shootingEntity instanceof EntityLivingBase) { EntityLivingBase attacker = (EntityLivingBase) this.shootingEntity; //EntityLivingBase target = (EntityLivingBase) raytraceResult.entityHit; // find the actual itemstack in the players inventory ItemStack inventoryItem = AmmoHelper.getMatchingItemstackFromInventory(tinkerProjectile.getItemStack(), attacker, false); if(inventoryItem == null || inventoryItem.getItem() != item.getItem()) { // backup, use saved itemstack inventoryItem = item; } // for the sake of dealing damage we always ensure that the impact itemstack has the correct broken state // since the ammo stack can break while the arrow travels/if it's the last arrow boolean brokenStateDiffers = ToolHelper.isBroken(inventoryItem) != ToolHelper.isBroken(item); if(brokenStateDiffers) { toggleBroken(inventoryItem); } Multimap<String, AttributeModifier> projectileAttributes = null; // remove stats from held items if(!getEntityWorld().isRemote) { unequip(attacker, EntityEquipmentSlot.OFFHAND); unequip(attacker, EntityEquipmentSlot.MAINHAND); // apply stats from projectile if(item.getItem() instanceof IProjectile) { projectileAttributes = ((IProjectile) item.getItem()).getProjectileAttributeModifier(inventoryItem); if(launcher != null && launcher.getItem() instanceof ILauncher) { ((ILauncher) launcher.getItem()).modifyProjectileAttributes(projectileAttributes, tinkerProjectile.getLaunchingStack(), tinkerProjectile.getItemStack(), tinkerProjectile.getPower()); } // factor in power projectileAttributes.put(SharedMonsterAttributes.ATTACK_DAMAGE.getName(), new AttributeModifier(PROJECTILE_POWER_MODIFIER, "Weapon damage multiplier", tinkerProjectile.getPower() - 1f, 2)); attacker.getAttributeMap().applyAttributeModifiers(projectileAttributes); } } // deal the damage float speed = MathHelper.sqrt(this.motionX * this.motionX + this.motionY * this.motionY + this.motionZ * this.motionZ); bounceOff = !dealDamage(speed, inventoryItem, attacker, entityHit); if(brokenStateDiffers) { toggleBroken(inventoryItem); } // remove stats from projectile // apply stats from projectile if(!getEntityWorld().isRemote) { if(item.getItem() instanceof IProjectile) { attacker.getAttributeMap().removeAttributeModifiers(projectileAttributes); } // readd stats from held items equip(attacker, EntityEquipmentSlot.MAINHAND); equip(attacker, EntityEquipmentSlot.OFFHAND); } if(!bounceOff) { onEntityHit(entityHit); } } if(bounceOff) { if(!bounceOnNoDamage) { this.setDead(); } // bounce off if we didn't deal damage this.motionX *= -0.10000000149011612D; this.motionY *= -0.10000000149011612D; this.motionZ *= -0.10000000149011612D; this.rotationYaw += 180.0F; this.prevRotationYaw += 180.0F; this.ticksInAir = 0; // 1.9 /* if (!this.worldObj.isRemote && this.motionX * this.motionX + this.motionY * this.motionY + this.motionZ * this.motionZ < 0.0010000000474974513D) { if (this.canBePickedUp == EntityArrow.PickupStatus.ALLOWED) { this.entityDropItem(this.getArrowStack(), 0.1F); } this.setDead(); }*/ } playHitEntitySound(); } private void unequip(EntityLivingBase entity, EntityEquipmentSlot slot) { ItemStack stack = entity.getItemStackFromSlot(slot); if(stack != null) { entity.getAttributeMap().removeAttributeModifiers(stack.getAttributeModifiers(slot)); } } private void equip(EntityLivingBase entity, EntityEquipmentSlot slot) { ItemStack stack = entity.getItemStackFromSlot(slot); if(stack != null) { entity.getAttributeMap().applyAttributeModifiers(stack.getAttributeModifiers(slot)); } } private void toggleBroken(ItemStack stack) { NBTTagCompound tag = TagUtil.getToolTag(stack); tag.setBoolean(Tags.BROKEN, !tag.getBoolean(Tags.BROKEN)); TagUtil.setToolTag(stack, tag); } // returns true if it was successful public boolean dealDamage(float speed, ItemStack item, EntityLivingBase attacker, Entity target) { return ToolHelper.attackEntity(item, (ToolCore) item.getItem(), attacker, target, this); } @Override public void setVelocity(double p_70016_1_, double p_70016_3_, double p_70016_5_) { // don't do anything, we set it ourselves at spawn // Mojangs code has a hard cap of 3.9 speed, but our projectiles can go faster, which desyncs client and server speeds // Packet that's causing it: S12PacketEntityVelocity } @Override // this function is the same as the vanilla EntityArrow public void onUpdate() { // call the entity update routine // luckily we can call this directly and take the arrow-code, since we'd have to call super.onUpdate otherwise. Which would not work. onEntityUpdate(); for(IProjectileTrait trait : tinkerProjectile.getProjectileTraits()) { trait.onProjectileUpdate(this, getEntityWorld(), tinkerProjectile.getItemStack()); } // boioioiooioing if(this.arrowShake > 0) { --this.arrowShake; } // If we don't have our rotation set correctly, infer it from our motion direction if(this.prevRotationPitch == 0.0F && this.prevRotationYaw == 0.0F) { float f = MathHelper.sqrt(this.motionX * this.motionX + this.motionZ * this.motionZ); this.prevRotationYaw = this.rotationYaw = (float) (Math.atan2(this.motionX, this.motionZ) * 180.0D / Math.PI); this.prevRotationPitch = this.rotationPitch = (float) (Math.atan2(this.motionY, (double) f) * 180.0D / Math.PI); } // we previously hit something. Check if the block is still there. BlockPos blockpos = new BlockPos(this.xTile, this.yTile, this.zTile); IBlockState iblockstate = this.getEntityWorld().getBlockState(blockpos); if(iblockstate.getMaterial() != Material.AIR) { AxisAlignedBB axisalignedbb = iblockstate.getCollisionBoundingBox(this.getEntityWorld(), blockpos); if(axisalignedbb != Block.NULL_AABB && axisalignedbb.offset(blockpos).isVecInside(new Vec3d(this.posX, this.posY, this.posZ))) { this.inGround = true; } } if(this.inGround) { updateInGround(iblockstate); } else { updateInAir(); } } // Update while we're stuck in a block public void updateInGround(IBlockState state) { Block block = state.getBlock(); int meta = block.getMetaFromState(state); // check if it's still the same block if(block == this.inTile && meta == this.inData) { ++this.ticksInGround; if(this.ticksInGround >= 1200) { this.setDead(); } } else { this.inGround = false; this.motionX *= (double) (this.rand.nextFloat() * 0.2F); this.motionY *= (double) (this.rand.nextFloat() * 0.2F); this.motionZ *= (double) (this.rand.nextFloat() * 0.2F); this.ticksInGround = 0; this.ticksInAir = 0; } ++this.timeInGround; } // update while traveling public void updateInAir() { // tick tock this.timeInGround = 0; ++this.ticksInAir; // do a raytrace from old to new position Vec3d oldPos = new Vec3d(this.posX, this.posY, this.posZ); Vec3d newPos = new Vec3d(this.posX + this.motionX, this.posY + this.motionY, this.posZ + this.motionZ); RayTraceResult raytraceResult = this.getEntityWorld().rayTraceBlocks(oldPos, newPos, false, true, false); // raytrace messes with the positions. get new ones! (not anymore since vec3d is all final now?) //oldPos = Vec3d.createVectorHelper(this.posX, this.posY, this.posZ); // if we hit something, the collision point is our new position if(raytraceResult != null) { newPos = new Vec3d(raytraceResult.hitVec.xCoord, raytraceResult.hitVec.yCoord, raytraceResult.hitVec.zCoord); } //else //newPos = Vec3d.createVectorHelper(this.posX + this.motionX, this.posY + this.motionY, this.posZ + this.motionZ); Entity entity = this.findEntityOnPath(oldPos, newPos); // if we hit something, new collision point! if(entity != null) { raytraceResult = new RayTraceResult(entity); } // did we hit a player? if(raytraceResult != null && raytraceResult.entityHit != null && raytraceResult.entityHit instanceof EntityPlayer) { EntityPlayer entityplayer = (EntityPlayer) raytraceResult.entityHit; // can we attack said player? if(entityplayer.capabilities.disableDamage || this.shootingEntity instanceof EntityPlayer && !((EntityPlayer) this.shootingEntity).canAttackPlayer(entityplayer)) { raytraceResult = null; } // this check should probably done inside of the loop for accuracy.. } // time to hit the object if(raytraceResult != null) { if(raytraceResult.entityHit != null) { onHitEntity(raytraceResult); } else { onHitBlock(raytraceResult); } } // crithit particles if(this.getIsCritical()) { drawCritParticles(); } // MOVEMENT! yay. doMoveUpdate(); // Slowdown double slowdown = 1.0d - getSlowdown(); // bubblez if(this.isInWater()) { for(int l = 0; l < 4; ++l) { float f3 = 0.25F; this.getEntityWorld().spawnParticle(EnumParticleTypes.WATER_BUBBLE, this.posX - this.motionX * (double) f3, this.posY - this.motionY * (double) f3, this.posZ - this.motionZ * (double) f3, this.motionX, this.motionY, this.motionZ); } // more slowdown in water slowdown *= 0.60d; } // phshshshshshs if(this.isWet()) { this.extinguish(); } // minimalistic slowdown! this.motionX *= slowdown; this.motionY *= slowdown; this.motionZ *= slowdown; // gravity if(!this.hasNoGravity()) { this.motionY -= getGravity(); } for(IProjectileTrait trait : tinkerProjectile.getProjectileTraits()) { trait.onMovement(this, getEntityWorld(), slowdown); } this.setPosition(this.posX, this.posY, this.posZ); // tell blocks we collided with, that we collided with them! this.doBlockCollisions(); } @Nullable @Override protected Entity findEntityOnPath(@Nonnull Vec3d start, @Nonnull Vec3d end) { if(isDefused()) { return null; } return super.findEntityOnPath(start, end); } public void drawCritParticles() { for(int k = 0; k < 4; ++k) { this.getEntityWorld().spawnParticle(EnumParticleTypes.CRIT, this.posX + this.motionX * (double) k / 4.0D, this.posY + this.motionY * (double) k / 4.0D, this.posZ + this.motionZ * (double) k / 4.0D, -this.motionX, -this.motionY + 0.2D, -this.motionZ); } } protected void doMoveUpdate() { this.posX += this.motionX; this.posY += this.motionY; this.posZ += this.motionZ; double f2 = MathHelper.sqrt(this.motionX * this.motionX + this.motionZ * this.motionZ); this.rotationYaw = (float) (Math.atan2(this.motionX, this.motionZ) * 180.0D / Math.PI); this.rotationPitch = (float) (Math.atan2(this.motionY, f2) * 180.0D / Math.PI); // normalize rotations while(this.rotationPitch - this.prevRotationPitch < -180.0F) { this.prevRotationPitch -= 360.0F; } while(this.rotationPitch - this.prevRotationPitch >= 180.0F) { this.prevRotationPitch += 360.0F; } while(this.rotationYaw - this.prevRotationYaw < -180.0F) { this.prevRotationYaw -= 360.0F; } while(this.rotationYaw - this.prevRotationYaw >= 180.0F) { this.prevRotationYaw += 360.0F; } this.rotationPitch = this.prevRotationPitch + (this.rotationPitch - this.prevRotationPitch) * 0.2F; this.rotationYaw = this.prevRotationYaw + (this.rotationYaw - this.prevRotationYaw) * 0.2F; } /** * Factor for the slowdown. 0 = no slowdown, >0 = (1-slowdown)*speed slowdown, <0 = speedup */ public double getSlowdown() { return 0.01; } /** * Added to the y-velocity as gravitational pull. Otherwise stuff would simply float midair. */ public double getGravity() { return 0.05; } /** * Called by a player entity when they collide with an entity */ @Override public void onCollideWithPlayer(@Nonnull EntityPlayer player) { if(!this.getEntityWorld().isRemote && this.inGround && this.arrowShake <= 0) { boolean pickedUp = this.pickupStatus == EntityArrow.PickupStatus.ALLOWED || this.pickupStatus == EntityArrow.PickupStatus.CREATIVE_ONLY && player.capabilities.isCreativeMode; if(pickedUp) { pickedUp = tinkerProjectile.pickup(player, pickupStatus != PickupStatus.ALLOWED); } if(pickedUp) { this.playSound(SoundEvents.ENTITY_ITEM_PICKUP, 0.2F, ((this.rand.nextFloat() - this.rand.nextFloat()) * 0.7F + 1.0F) * 2.0F); player.onItemPickup(this, 1); this.setDead(); } } } /** NBT stuff **/ @Override public void writeEntityToNBT(NBTTagCompound tags) { super.writeEntityToNBT(tags); tags.setTag("item", tinkerProjectile.serializeNBT()); } @Override public void readEntityFromNBT(NBTTagCompound tags) { super.readEntityFromNBT(tags); tinkerProjectile.deserializeNBT(tags.getCompoundTag("item")); } @Override public void writeSpawnData(ByteBuf data) { data.writeFloat(rotationYaw); // shooting entity int id = shootingEntity == null ? this.getEntityId() : shootingEntity.getEntityId(); data.writeInt(id); // motion stuff. This has to be sent separately since MC seems to do hardcoded stuff to arrows with this data.writeDouble(this.motionX); data.writeDouble(this.motionY); data.writeDouble(this.motionZ); ByteBufUtils.writeItemStack(data, tinkerProjectile.getItemStack()); ByteBufUtils.writeItemStack(data, tinkerProjectile.getLaunchingStack()); data.writeFloat(tinkerProjectile.getPower()); } @Override public void readSpawnData(ByteBuf data) { rotationYaw = data.readFloat(); shootingEntity = getEntityWorld().getEntityByID(data.readInt()); this.motionX = data.readDouble(); this.motionY = data.readDouble(); this.motionZ = data.readDouble(); tinkerProjectile.setItemStack(ByteBufUtils.readItemStack(data)); tinkerProjectile.setLaunchingStack(ByteBufUtils.readItemStack(data)); tinkerProjectile.setPower(data.readFloat()); this.posX -= MathHelper.cos(this.rotationYaw / 180.0F * (float) Math.PI) * 0.16F; this.posY -= 0.10000000149011612D; this.posZ -= MathHelper.sin(this.rotationYaw / 180.0F * (float) Math.PI) * 0.16F; } }