/** Copyright (C) <2015> <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 java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Set; import net.minecraft.block.material.Material; import net.minecraft.client.Minecraft; import net.minecraft.client.particle.EffectRenderer; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.SharedMonsterAttributes; import net.minecraft.entity.item.EntityItem; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.network.play.server.S12PacketEntityVelocity; import net.minecraft.util.BlockPos; import net.minecraft.util.DamageSource; import net.minecraft.util.EntityDamageSourceIndirect; import net.minecraft.util.EnumFacing; import net.minecraft.util.MovingObjectPosition; import net.minecraft.util.MovingObjectPosition.MovingObjectType; import net.minecraft.world.World; import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.fml.relauncher.SideOnly; import org.apache.commons.lang3.ArrayUtils; import zeldaswordskills.api.entity.MagicType; import zeldaswordskills.client.particle.FXCycloneRing; import zeldaswordskills.ref.Config; import zeldaswordskills.util.WorldUtils; public class EntityCyclone extends EntityMobThrowable { // Particle effect stuff: private static float maxPitch = 0.4f; private float yaw = 0; private float yawVelocity = 0.1f; private float pitch = 0; private static float pitchVelocity = 0; /** Watchable object index for cyclone's area of effect */ private static final int AREA_INDEX = 23; /** Keeps track of entities already affected so they don't get attacked twice */ private List<Integer> affectedEntities = new ArrayList<Integer>(); /** ItemStack version of captured drops is more efficient for NBT storage */ private List<ItemStack> capturedItems = new ArrayList<ItemStack>(); /** Whether this cyclone can destroy blocks */ private boolean canGrief = true; public EntityCyclone(World world) { super(world); setSize(1.0F, 2.0F); } public EntityCyclone(World world, EntityLivingBase entity) { super(world, entity); setSize(1.0F, 2.0F); } public EntityCyclone(World world, double x, double y, double z) { super(world, x, y, z); setSize(1.0F, 2.0F); } public EntityCyclone(World world, EntityLivingBase shooter, EntityLivingBase target, float velocity, float wobble) { super(world, shooter, target, velocity, wobble); setSize(1.0F, 2.0F); } @Override protected void entityInit() { super.entityInit(); dataWatcher.addObject(AREA_INDEX, 2.0F); } /** Returns the cyclone's area of effect */ public float getArea() { return dataWatcher.getWatchableObjectFloat(AREA_INDEX); } /** Sets the cyclone's area of effect radius; affects entities within (radius - 1, min. 0.5) */ public EntityCyclone setArea(float radius) { dataWatcher.updateObject(AREA_INDEX, radius); return this; } /** Disables the cyclone's ability to destroy blocks */ public EntityCyclone disableGriefing() { canGrief = false; return this; } /** Returns a tornado damage source */ protected DamageSource getDamageSource() { return new EntityDamageSourceIndirect("blast.wind", this, getThrower()).setProjectile().setMagicDamage(); } @Override public void applyEntityCollision(Entity entity) {} @Override public boolean handleWaterMovement() { return false; } @Override public boolean isInLava() { return false; } @Override protected float getVelocity() { return 0.75F; } @Override protected float getGravityVelocity() { return 0.0F; } @Override public void onUpdate() { super.onUpdate(); motionY += 0.01D; if (!worldObj.isRemote) { captureDrops(); attackNearbyEntities(); if (canGrief) { destroyLeaves(); } if (ticksExisted > 40) { setDead(); releaseDrops(); } else if (ticksExisted % MagicType.WIND.getSoundFrequency() == (MagicType.WIND.getSoundFrequency() - 1)) { worldObj.playSoundAtEntity(this, MagicType.WIND.getMovingSound(), MagicType.WIND.getSoundPitch(rand), MagicType.WIND.getSoundVolume(rand)); } } else { spawnParticleRing(); } } @Override protected void onImpact(MovingObjectPosition mop) { if (mop.typeOfHit == MovingObjectType.BLOCK) { BlockPos pos = mop.getBlockPos(); Material m = worldObj.getBlockState(pos).getBlock().getMaterial(); if (m == Material.leaves) { if (!worldObj.isRemote && canGrief && Config.canDekuDenude()) { worldObj.destroyBlock(pos, true); } } else if (m.blocksMovement()) { if (mop.sideHit == EnumFacing.UP) { posY = pos.getY() + 1; rotationPitch = 0.0F; motionY = 0.0D; } else { setDead(); releaseDrops(); } } } else if (mop.entityHit != null && (mop.entityHit != getThrower() || ticksExisted >= 5)) { if (getDamage() > 0.0F && !affectedEntities.contains(mop.entityHit.getEntityId())) { mop.entityHit.attackEntityFrom(getDamageSource(), getDamage()); affectedEntities.add(mop.entityHit.getEntityId()); } if (!(mop.entityHit instanceof EntityLivingBase) || rand.nextFloat() > ((EntityLivingBase) mop.entityHit).getAttributeMap().getAttributeInstance(SharedMonsterAttributes.knockbackResistance).getAttributeValue()) { mop.entityHit.motionX = this.motionX * 1.8D; mop.entityHit.motionY = this.motionY + 0.5D; mop.entityHit.motionZ = this.motionZ * 1.8D; mop.entityHit.rotationYaw += 30.0F * this.ticksExisted; if (mop.entityHit instanceof EntityPlayerMP && !worldObj.isRemote) { ((EntityPlayerMP) mop.entityHit).playerNetServerHandler.sendPacket(new S12PacketEntityVelocity(mop.entityHit)); } } } } /** * If cyclone inflicts damage, searches for entities within the area of effect and attacks them */ private void attackNearbyEntities() { if (getDamage() > 0.0F) { double d = Math.max(0.5D, getArea() - 1.0D); List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, getEntityBoundingBox().expand(d, d, d)); for (EntityLivingBase entity : entities) { if (affectedEntities.contains(entity.getEntityId()) || (entity == getThrower() && ticksExisted < 8)) { continue; } entity.attackEntityFrom(new EntityDamageSourceIndirect("tornado", this, getThrower()).setProjectile().setMagicDamage(), getDamage()); affectedEntities.add(entity.getEntityId()); } } } /** * Scans for and captures nearby EntityItems */ private void captureDrops() { if (!isDead) { double d = Math.max(0.5D, getArea() - 1.0D); List<EntityItem> items = worldObj.getEntitiesWithinAABB(EntityItem.class, getEntityBoundingBox().expand(d, d, d)); for (EntityItem item : items) { if (item.isEntityAlive()) { capturedItems.add(item.getEntityItem()); item.setDead(); } } } } /** * Releases all captured drops into the world as dropped items */ private void releaseDrops() { for (ItemStack stack : capturedItems) { WorldUtils.spawnItemWithRandom(worldObj, stack, posX, posY, posZ); } } /** * Checks for and destroys leaves each update tick */ private void destroyLeaves() { BlockPos pos; Set<BlockPos> affectedBlockPositions = WorldUtils.getAffectedBlocksList(worldObj, rand, getArea(), posX, posY, posZ, null); Iterator<BlockPos> iterator = affectedBlockPositions.iterator(); while (iterator.hasNext()) { pos = iterator.next(); Material m = worldObj.getBlockState(pos).getBlock().getMaterial(); if ((m == Material.leaves && canGrief && Config.canDekuDenude()) || m == Material.plants || m == Material.vine || m == Material.web) { worldObj.destroyBlock(pos, true); } } } /** Updates the cyclone swirling angles and spawns a new ring of particles. */ @SideOnly(Side.CLIENT) private void spawnParticleRing() { yaw += yawVelocity; if (yaw > 2*Math.PI) yaw -= 2*Math.PI; if (Math.random() < 0.1) { //if (pitchVelocity < 0.01) pitchVelocity = 0.2f; } pitch += pitchVelocity; if (pitch > maxPitch) pitch = maxPitch; if (pitchVelocity > 0) { pitchVelocity -= 0.05f; } else { pitchVelocity = 0; } if (pitch > 0) { pitch -= 0.07f; } else { pitch = 0; } // This was left from when Cyclone had a predetermined duration in Dota 2 Items: /*if (duration - elapsed < 0.5f && alpha > 0) { alpha -= 0.05f; }*/ //TODO: when destroying the cyclone, set the particles to start fading EffectRenderer effectRenderer = Minecraft.getMinecraft().effectRenderer; FXCycloneRing ring = new FXCycloneRing(worldObj, posX, posY + 0.1D, posZ, motionX, motionY, motionZ, yaw, pitch, 0.7f, effectRenderer); effectRenderer.addEffect(ring); } @Override public void writeEntityToNBT(NBTTagCompound compound) { super.writeEntityToNBT(compound); compound.setFloat("areaOfEffect", getArea()); compound.setIntArray("affectedEntities", ArrayUtils.toPrimitive(affectedEntities.toArray(new Integer[affectedEntities.size()]))); NBTTagList items = new NBTTagList(); for (ItemStack stack : capturedItems) { NBTTagCompound dropNBT = new NBTTagCompound(); stack.writeToNBT(dropNBT); items.appendTag(dropNBT); } compound.setTag("items", items); } @Override public void readEntityFromNBT(NBTTagCompound compound) { super.readEntityFromNBT(compound); setArea(compound.getFloat("areaOfEffect")); int[] entities = compound.getIntArray("affectedEntities"); for (int i = 0; i < entities.length; ++i) { affectedEntities.add(entities[i]); } NBTTagList items = compound.getTagList("items", compound.getId()); for (int i = 0; i < items.tagCount(); ++i) { capturedItems.add(ItemStack.loadItemStackFromNBT((NBTTagCompound) items.getCompoundTagAt(i))); } } }