/** 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.mobs; import java.util.List; import net.minecraft.entity.IEntityLivingData; import net.minecraft.entity.SharedMonsterAttributes; import net.minecraft.entity.monster.IMob; import net.minecraft.entity.passive.EntityBat; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.init.Items; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.potion.Potion; import net.minecraft.potion.PotionEffect; import net.minecraft.util.BlockPos; import net.minecraft.util.DamageSource; import net.minecraft.util.EntityDamageSource; import net.minecraft.util.EntityDamageSourceIndirect; import net.minecraft.util.MathHelper; import net.minecraft.world.DifficultyInstance; import net.minecraft.world.EnumDifficulty; import net.minecraft.world.EnumSkyBlock; import net.minecraft.world.World; import net.minecraft.world.biome.BiomeGenBase; import zeldaswordskills.api.block.IWhipBlock.WhipType; import zeldaswordskills.api.damage.DamageUtils.DamageSourceIce; import zeldaswordskills.api.damage.DamageUtils.DamageSourceShock; import zeldaswordskills.api.damage.IDamageSourceStun; import zeldaswordskills.api.entity.IEntityLootable; import zeldaswordskills.api.item.ArmorIndex; import zeldaswordskills.entity.IEntityVariant; import zeldaswordskills.entity.ZSSEntityInfo; import zeldaswordskills.entity.buff.Buff; import zeldaswordskills.entity.player.ZSSPlayerInfo; import zeldaswordskills.item.ItemTreasure.Treasures; import zeldaswordskills.item.ZSSItems; import zeldaswordskills.ref.Config; import zeldaswordskills.ref.Sounds; import zeldaswordskills.util.BiomeType; import com.google.common.collect.Lists; public class EntityKeese extends EntityBat implements IMob, IEntityLootable, IEntityVariant { /** The different varieties of Keese */ public static enum KeeseType { NORMAL(8.0F, BiomeType.PLAINS, BiomeType.FOREST), FIRE(12.0F, BiomeType.FIERY, BiomeType.JUNGLE), ICE(12.0F, BiomeType.COLD, BiomeType.TAIGA), THUNDER(12.0F, BiomeType.ARID, BiomeType.BEACH), CURSED(16.0F, null, null); // special spawn chance /** Default max health for this Keese Type */ public final float maxHealth; /** Biome in which this type spawns most frequently (or possibly exclusively) */ public final BiomeType favoredBiome; /** Secondary biome, if any, in which this type spawns most frequently (or possibly exclusively) */ public final BiomeType secondBiome; private KeeseType(float maxHealth, BiomeType favoredBiome, BiomeType secondBiome) { this.maxHealth = maxHealth; this.favoredBiome = favoredBiome; this.secondBiome = secondBiome; } } /** * Returns array of default biomes in which this entity may spawn naturally */ public static String[] getDefaultBiomes() { List<BiomeType> biomes = Lists.newArrayList(BiomeType.RIVER, BiomeType.MOUNTAIN); for (KeeseType type : KeeseType.values()) { if (type.favoredBiome != null) { biomes.add(type.favoredBiome); } if (type.secondBiome != null) { biomes.add(type.secondBiome); } } return BiomeType.getBiomeArray(null, biomes.toArray(new BiomeType[biomes.size()])); } /** Chunk coordinates toward which this Keese is currently heading */ private BlockPos currentFlightTarget; /** Replacement for removed 'attackTime' */ protected int attackTime; /** Data watcher index for this Keese's type */ private static final int TYPE_INDEX = 17; /** Data watcher index for shock time so entity can render appropriately */ private static final int SHOCK_INDEX = 18; /** Whether this Keese has spawned a swarm already */ private boolean swarmSpawned; public EntityKeese(World world) { super(world); setSize(0.5F, 0.9F); setIsBatHanging(true); setType(KeeseType.NORMAL); } @Override protected void entityInit() { super.entityInit(); dataWatcher.addObject(TYPE_INDEX, (byte)(KeeseType.NORMAL.ordinal())); dataWatcher.addObject(SHOCK_INDEX, 0); } /** Returns this Keese's type */ public KeeseType getType() { return KeeseType.values()[dataWatcher.getWatchableObjectByte(TYPE_INDEX) % KeeseType.values().length]; } /** Sets this Keese's type */ public void setType(KeeseType type) { dataWatcher.updateObject(TYPE_INDEX, (byte)(type.ordinal())); applyTypeTraits(); getEntityAttribute(SharedMonsterAttributes.maxHealth).setBaseValue(getType().maxHealth); setHealth(getMaxHealth()); } @Override public EntityKeese setType(int type) { setType(KeeseType.values()[type % KeeseType.values().length]); return this; } /** * Sets the Keese's type when spawned */ private void setTypeOnSpawn() { if (worldObj.provider.getDimensionId() == -1 && rand.nextFloat() < Config.getKeeseCursedChance()) { setType(KeeseType.CURSED); } else if (rand.nextFloat() < Config.getKeeseCursedChance()) { // second chance for both Hell and everywhere else setType(KeeseType.CURSED); } else if (Config.areMobVariantsAllowed() && rand.nextFloat() < Config.getMobVariantChance()) { setType(rand.nextInt(KeeseType.values().length)); } else { BiomeGenBase biome = worldObj.getBiomeGenForCoords(new BlockPos(this)); BiomeType biomeType = BiomeType.getBiomeTypeFor(biome); for (KeeseType t : KeeseType.values()) { if (t.favoredBiome == biomeType || t.secondBiome == biomeType) { setType(t); return; } } } } /** * Applies traits based on Keese's type */ private void applyTypeTraits() { ZSSEntityInfo info = ZSSEntityInfo.get(this); info.removeAllBuffs(); info.applyBuff(Buff.EVADE_UP, Integer.MAX_VALUE, 50); switch(getType()) { case CURSED: info.applyBuff(Buff.RESIST_FIRE, Integer.MAX_VALUE, 100); info.applyBuff(Buff.WEAKNESS_HOLY, Integer.MAX_VALUE, 100); experienceValue = 7; break; case FIRE: info.applyBuff(Buff.RESIST_FIRE, Integer.MAX_VALUE, 100); info.applyBuff(Buff.WEAKNESS_COLD, Integer.MAX_VALUE, 100); isImmuneToFire = true; experienceValue = 3; break; case ICE: info.applyBuff(Buff.RESIST_COLD, Integer.MAX_VALUE, 100); info.applyBuff(Buff.WEAKNESS_FIRE, Integer.MAX_VALUE, 100); experienceValue = 3; break; case THUNDER: info.applyBuff(Buff.RESIST_SHOCK, Integer.MAX_VALUE, 100); experienceValue = 5; break; default: experienceValue = 1; } } /** Whether this Keese type can shock */ protected boolean canShock() { return (getType() == KeeseType.THUNDER); } /** Returns the amount of time remaining for which this Keese is electrified */ public int getShockTime() { return dataWatcher.getWatchableObjectInt(SHOCK_INDEX); } /** Sets the amount of time this Keese will remain electrified */ public void setShockTime(int time) { dataWatcher.updateObject(SHOCK_INDEX, time); } /** Whether this Keese may spawn a swarm in the near future */ public boolean getSpawnSwarm() { return !swarmSpawned; } /** Disable or re-enable possibility of spawning a swarm */ public EntityKeese setSpawnSwarm(boolean spawnSwarm) { this.swarmSpawned = !spawnSwarm; return this; } /** * Returns amount of damage this type of Keese inflicts */ private float getDamage() { return 2.0F; } /** * Returns the DamageSource this type of Keese inflicts */ private DamageSource getDamageSource() { if (getShockTime() > 0) { return new DamageSourceShock("shock", this, worldObj.getDifficulty().getDifficultyId() * 50, 1.0F); } switch(getType()) { case FIRE: return new EntityDamageSource("mob", this).setFireDamage(); case ICE: return new DamageSourceIce("mob", this, 100, 0); default: return new EntityDamageSource("mob", this); } } @Override protected void addRandomDrop() { int rarity = rand.nextInt(8); if (getType() == KeeseType.CURSED) { switch(rarity) { case 0: entityDropItem(new ItemStack(ZSSItems.treasure,1,Treasures.EVIL_CRYSTAL.ordinal()), 0.0F); break; default: entityDropItem(new ItemStack(rand.nextInt(8) == 0 ? ZSSItems.smallHeart : ZSSItems.heartPiece), 0.0F); } } else { switch(rarity) { case 0: entityDropItem(new ItemStack(ZSSItems.treasure,1,Treasures.MONSTER_CLAW.ordinal()), 0.0F); break; default: entityDropItem(new ItemStack(rand.nextInt(3) == 1 ? Items.emerald : ZSSItems.smallHeart), 0.0F); } } } @Override public float getLootableChance(EntityPlayer player, WhipType whip) { return 0.2F; } @Override public ItemStack getEntityLoot(EntityPlayer player, WhipType whip) { if (rand.nextFloat() < (0.1F * (1 + whip.ordinal()))) { return new ItemStack(ZSSItems.treasure,1, (getType() == KeeseType.CURSED ? Treasures.EVIL_CRYSTAL.ordinal() : Treasures.MONSTER_CLAW.ordinal())); } return new ItemStack(rand.nextInt(3) > 0 ? Items.emerald : ZSSItems.smallHeart); } @Override public boolean onLootStolen(EntityPlayer player, boolean wasItemStolen) { return true; } @Override public boolean isHurtOnTheft(EntityPlayer player, WhipType whip) { return true; } @Override public void onCollideWithPlayer(EntityPlayer player) { if (attackTime == 0 && canEntityBeSeen(player) && getDistanceSqToEntity(player) < 1.5D && (player.getCurrentArmor(ArmorIndex.WORN_HELM) == null || player.getCurrentArmor(ArmorIndex.WORN_HELM).getItem() != ZSSItems.maskSkull) && player.attackEntityFrom(getDamageSource(), getDamage())) { attackTime = rand.nextInt(20) + 20; playSound(Sounds.BAT_HURT, 1.0F, (rand.nextFloat() - rand.nextFloat()) * 0.2F + 1.0F); int t = getShockTime(); if (t > 0) { setShockTime(Math.max(0, t - rand.nextInt(50) - 25)); } switch(getType()) { case CURSED: applyRandomCurse(player); break; case FIRE: if (rand.nextFloat() < 0.5F) { player.setFire(rand.nextInt(4) + 4); } break; default: } } } /** * Applies a random negative effect to the player, or possibly no effect */ private void applyRandomCurse(EntityPlayer player) { switch (rand.nextInt(16)) { case 0: ZSSEntityInfo.get(player).applyBuff(Buff.ATTACK_DOWN, rand.nextInt(500) + 100, rand.nextInt(51) + 50); break; case 1: ZSSEntityInfo.get(player).applyBuff(Buff.DEFENSE_DOWN, rand.nextInt(500) + 100, rand.nextInt(26) + 25); break; case 2: ZSSEntityInfo.get(player).applyBuff(Buff.EVADE_DOWN, rand.nextInt(500) + 100, rand.nextInt(51) + 50); break; case 3: ZSSEntityInfo.get(player).applyBuff(Buff.WEAKNESS_COLD, rand.nextInt(500) + 100, rand.nextInt(51) + 50); break; case 4: ZSSEntityInfo.get(player).applyBuff(Buff.WEAKNESS_FIRE, rand.nextInt(500) + 100, rand.nextInt(51) + 50); break; case 5: ZSSEntityInfo.get(player).applyBuff(Buff.WEAKNESS_MAGIC, rand.nextInt(500) + 100, rand.nextInt(51) + 50); break; case 6: ZSSEntityInfo.get(player).applyBuff(Buff.WEAKNESS_SHOCK, rand.nextInt(500) + 100, rand.nextInt(51) + 50); break; case 7: player.addPotionEffect(new PotionEffect(Potion.confusion.id, rand.nextInt(500) + 100, 1)); break; case 8: player.addPotionEffect(new PotionEffect(Potion.blindness.id, rand.nextInt(500) + 100, 1)); break; case 9: player.addPotionEffect(new PotionEffect(Potion.poison.id, rand.nextInt(100) + 50, rand.nextInt(9) / 8)); break; case 10: case 11: float drain = 6.0F + rand.nextInt(player.worldObj.getDifficulty().getDifficultyId() * 10); drain = Math.min(drain, ZSSPlayerInfo.get(player).getCurrentMagic()); if (ZSSPlayerInfo.get(player).getCurrentMagic() < 5.0F) { player.addPotionEffect(new PotionEffect(Potion.harm.id, 1, rand.nextInt(9) / 8)); } ZSSPlayerInfo.get(player).consumeMagic(drain); heal(drain / 2); break; default: } } @Override public void onUpdate() { super.onUpdate(); if (attackTime > 0) { --attackTime; } if (!swarmSpawned && !worldObj.isRemote) { swarmSpawned = true; if (rand.nextFloat() < Config.getKeeseSwarmChance()) { int n = Config.getKeeseSwarmSize() - rand.nextInt(Config.getKeeseSwarmSize()); for (int i = 0; i < n; ++i) { EntityKeese k = new EntityKeese(worldObj); double x = this.posX + rand.nextFloat() * 2.0F; double z = this.posZ + rand.nextFloat() * 2.0F; k.setPosition(x, this.posY, z); k.setTypeOnSpawn(); k.swarmSpawned = true; worldObj.spawnEntityInWorld(k); } } } int time = getShockTime(); if (time > 0) { setShockTime(time - 1); if (time % 8 > 6 && rand.nextInt(4) == 0) { worldObj.playSoundAtEntity(this, Sounds.SHOCK, getSoundVolume(), 1.0F / (rand.nextFloat() * 0.4F + 1.0F)); } } if (!worldObj.isRemote && worldObj.getDifficulty() == EnumDifficulty.PEACEFUL) { this.setDead(); } } @Override protected void updateAITasks() { if (ZSSEntityInfo.get(this).isBuffActive(Buff.STUN)) { // because Keese get moved twice per tick due to inherited EntityBat methods return; } super.updateAITasks(); if (!getIsBatHanging()) { if (currentFlightTarget != null && (!worldObj.isAirBlock(currentFlightTarget) || currentFlightTarget.getY() < 1)) { currentFlightTarget = null; } BlockPos pos = new BlockPos(this); if (currentFlightTarget == null || rand.nextInt(30) == 0 || currentFlightTarget.distanceSq(pos) < (attackingPlayer != null ? 1.0F : 4.0F)) { attackingPlayer = getLastAttacker() instanceof EntityPlayer ? (EntityPlayer) getLastAttacker() : worldObj.getClosestPlayerToEntity(this, 8.0D); if (attackingPlayer != null && !attackingPlayer.capabilities.isCreativeMode && (attackingPlayer.getCurrentArmor(ArmorIndex.WORN_HELM) == null || attackingPlayer.getCurrentArmor(ArmorIndex.WORN_HELM).getItem() != ZSSItems.maskSkull)) { currentFlightTarget = new BlockPos((int) attackingPlayer.posX, (int) attackingPlayer.posY + 1, (int) attackingPlayer.posZ); worldObj.playAuxSFXAtEntity(attackingPlayer, 1015, pos, 0); } else { currentFlightTarget = new BlockPos((int) posX + rand.nextInt(7) - rand.nextInt(7), (int) posY + rand.nextInt(6) - 2, (int) posZ + rand.nextInt(7) - rand.nextInt(7)); } } double d0 = (double) currentFlightTarget.getX() + 0.5D - posX; double d1 = (double) currentFlightTarget.getY() + 0.1D - posY; double d2 = (double) currentFlightTarget.getZ() + 0.5D - posZ; motionX += (Math.signum(d0) * 0.5D - motionX) * 0.10000000149011612D; motionY += (Math.signum(d1) * 0.699999988079071D - motionY) * 0.10000000149011612D; motionZ += (Math.signum(d2) * 0.5D - motionZ) * 0.10000000149011612D; float f = (float)(Math.atan2(motionZ, motionX) * 180.0D / Math.PI) - 90.0F; float f1 = MathHelper.wrapAngleTo180_float(f - rotationYaw); moveForward = 0.5F; rotationYaw += f1; if (attackingPlayer == null && rand.nextInt(100) == 0 && worldObj.getBlockState(pos.up()).getBlock().isNormalCube()) { setIsBatHanging(true); } else if (canShock() && getShockTime() == 0 && !ZSSEntityInfo.get(this).isBuffActive(Buff.STUN)) { if (attackingPlayer != null && ((recentlyHit > 0 && rand.nextInt(20) == 0) || rand.nextInt(300) == 0)) { setShockTime(rand.nextInt(50) + (worldObj.getDifficulty().getDifficultyId() * (rand.nextInt(20) + 10))); } } } } @Override public boolean attackEntityFrom(DamageSource source, float amount) { if (isEntityInvulnerable(source)) { return false; } else { if (!worldObj.isRemote && getIsBatHanging()) { setIsBatHanging(false); } if (getShockTime() > 0) { if (source instanceof EntityDamageSourceIndirect) { if (source.isMagicDamage()) { return super.attackEntityFrom(source, amount); } else if (source.isExplosion()) { ZSSEntityInfo.get(this).stun(20 + rand.nextInt((int)(amount * 5) + 1)); setShockTime(0); } else if (source instanceof IDamageSourceStun) { setShockTime(0); } // Hack to prevent infinite loop when attacked by other electrified mobs (other keese, chus, etc) } else if (source instanceof EntityDamageSource && source.getEntity() instanceof EntityPlayer && !source.damageType.equals("thorns")) { source.getEntity().attackEntityFrom(getDamageSource(), getDamage()); worldObj.playSoundAtEntity(this, Sounds.SHOCK, 1.0F, 1.0F / (rand.nextFloat() * 0.4F + 1.0F)); } return false; } return super.attackEntityFrom(source, amount); } } @Override public void readEntityFromNBT(NBTTagCompound compound) { super.readEntityFromNBT(compound); swarmSpawned = compound.getBoolean("SpawnedSwarm"); dataWatcher.updateObject(TYPE_INDEX, compound.getByte("KeeseType")); } @Override public void writeEntityToNBT(NBTTagCompound compound) { super.writeEntityToNBT(compound); compound.setBoolean("SpawnedSwarm", swarmSpawned); compound.setByte("KeeseType", dataWatcher.getWatchableObjectByte(TYPE_INDEX)); } @Override public IEntityLivingData onInitialSpawn(DifficultyInstance difficulty, IEntityLivingData data) { data = super.onInitialSpawn(difficulty, data); setTypeOnSpawn(); return data; } @Override public boolean getCanSpawnHere() { return (worldObj.getDifficulty() != EnumDifficulty.PEACEFUL && (posY < 64.0D || rand.nextInt(16) > 13) && isValidLightLevel() && !worldObj.isAnyLiquid(getEntityBoundingBox())); } /** * Copied from EntityMob */ protected boolean isValidLightLevel() { BlockPos blockpos = new BlockPos(posX, getEntityBoundingBox().minY, posZ); if (this.worldObj.getLightFor(EnumSkyBlock.SKY, blockpos) > rand.nextInt(32)) { return false; } int i = worldObj.getLightFromNeighbors(blockpos); if (worldObj.isThundering()) { int j = worldObj.getSkylightSubtracted(); worldObj.setSkylightSubtracted(10); i = worldObj.getLightFromNeighbors(blockpos); worldObj.setSkylightSubtracted(j); } return i <= rand.nextInt(8); } }