/**
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.mobs;
import java.util.List;
import net.minecraft.enchantment.EnchantmentHelper;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.IEntityLivingData;
import net.minecraft.entity.SharedMonsterAttributes;
import net.minecraft.entity.ai.EntityAIHurtByTarget;
import net.minecraft.entity.ai.EntityAILookIdle;
import net.minecraft.entity.ai.EntityAISwimming;
import net.minecraft.entity.ai.EntityAIWander;
import net.minecraft.entity.ai.EntityAIWatchClosest;
import net.minecraft.entity.monster.EntityMob;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.init.Items;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.BlockPos;
import net.minecraft.util.DamageSource;
import net.minecraft.util.EnumParticleTypes;
import net.minecraft.world.DifficultyInstance;
import net.minecraft.world.World;
import net.minecraft.world.biome.BiomeGenBase;
import zeldaswordskills.api.block.IWhipBlock.WhipType;
import zeldaswordskills.api.entity.IEntityLootable;
import zeldaswordskills.api.entity.MagicType;
import zeldaswordskills.entity.IEntityVariant;
import zeldaswordskills.entity.ZSSEntityInfo;
import zeldaswordskills.entity.ai.EntityAINearestAttackableTargetNight;
import zeldaswordskills.entity.ai.EntityAIRangedMagic;
import zeldaswordskills.entity.ai.EntityAITeleport;
import zeldaswordskills.entity.ai.IEntityTeleport;
import zeldaswordskills.entity.ai.IMagicUser;
import zeldaswordskills.entity.buff.Buff;
import zeldaswordskills.entity.projectile.EntityMagicSpell;
import zeldaswordskills.item.ItemTreasure.Treasures;
import zeldaswordskills.item.ZSSItems;
import zeldaswordskills.ref.Config;
import zeldaswordskills.ref.Sounds;
import zeldaswordskills.util.BiomeType;
import zeldaswordskills.util.WorldUtils;
import com.google.common.collect.Lists;
public class EntityWizzrobe extends EntityMob implements IEntityLootable, IEntityTeleport, IEntityVariant, IMagicUser
{
/** Separate Wizzrobe enum to allow more magic types without screwing up Wizzrobe spawn eggs and such */
public static enum WizzrobeType {
FIRE_WIZ(MagicType.FIRE, BiomeType.FIERY),
ICE_WIZ(MagicType.ICE, BiomeType.COLD),
LIGHTNING_WIZ(MagicType.LIGHTNING, BiomeType.MOUNTAIN),
WIND_WIZ(MagicType.WIND, BiomeType.FOREST);
/** The magic type this wizzrobe uses */
public final MagicType magicType;
/** Biome in which this type spawns most frequently (or possibly exclusively) */
public final BiomeType favoredBiome;
private WizzrobeType(MagicType type, BiomeType favoredBiome) {
this.magicType = type;
this.favoredBiome = favoredBiome;
}
}
/**
* Returns array of default biomes in which this entity may spawn naturally
*/
public static String[] getDefaultBiomes() {
List<BiomeType> biomes = Lists.newArrayList(BiomeType.ARID, BiomeType.JUNGLE, BiomeType.PLAINS, BiomeType.RIVER, BiomeType.TAIGA);
for (WizzrobeType type : WizzrobeType.values()) {
biomes.add(type.favoredBiome);
}
return BiomeType.getBiomeArray(null, biomes.toArray(new BiomeType[biomes.size()]));
}
/** Data watcher index for this Wizzrobe's type */
protected static final int TYPE_INDEX = 16;
/** Datawatcher index for current casting time, used for rendering arm positions */
protected static final int CASTING_TIME = 17;
/** Datawatcher index for current maximum casting time, used for rendering arm positions */
protected static final int MAX_CAST_TIME = 18;
/** The magic using AI instance, used to interrupt casting when hurt */
protected final EntityAIRangedMagic magicAI;
/** The teleportation AI instance, used to check if can teleport when attacked */
protected final EntityAITeleport teleportAI;
/** Target acquisition timer */
private int noTargetTime;
public EntityWizzrobe(World world) {
super(world);
magicAI = getMagicAI();
teleportAI = getNewTeleportAI();
tasks.addTask(1, new EntityAISwimming(this));
tasks.addTask(2, magicAI);
tasks.addTask(3, teleportAI);
tasks.addTask(4, new EntityAIWander(this, 1.0D));
tasks.addTask(5, new EntityAIWatchClosest(this, EntityPlayer.class, 8.0F));
tasks.addTask(5, new EntityAILookIdle(this));
targetTasks.addTask(1, new EntityAIHurtByTarget(this, false));
targetTasks.addTask(2, new EntityAINearestAttackableTargetNight<EntityPlayer>(this, EntityPlayer.class, 0, true, 0.5F));
experienceValue = 8; // normal mobs are 5
setSize(0.6F, 1.8F);
setType(WizzrobeType.WIND_WIZ);
}
@Override
public EntityAITeleport getTeleportAI() {
return teleportAI;
}
/**
* Returns the teleportation AI this Wizzrobe should use when being constructed
*/
protected EntityAITeleport getNewTeleportAI() {
return new EntityAITeleport(this, 16.0D, 60, true, true, true, true, true);
}
/**
* Returns the magic AI this Wizzrobe should use
*/
protected EntityAIRangedMagic getMagicAI() {
return new EntityAIRangedMagic(this, 20, 60, 16.0D);
}
@Override
protected void applyEntityAttributes() {
super.applyEntityAttributes();
getEntityAttribute(SharedMonsterAttributes.maxHealth).setBaseValue(30.0D);
getEntityAttribute(SharedMonsterAttributes.movementSpeed).setBaseValue(0.25D);
getEntityAttribute(SharedMonsterAttributes.knockbackResistance).setBaseValue(1.0D);
getEntityAttribute(SharedMonsterAttributes.followRange).setBaseValue(40.0D);
}
@Override
protected void entityInit() {
super.entityInit();
dataWatcher.addObject(TYPE_INDEX, (byte)(WizzrobeType.WIND_WIZ.ordinal()));
dataWatcher.addObject(CASTING_TIME, 0);
dataWatcher.addObject(MAX_CAST_TIME, 0);
}
/** Returns the Wizzrobe's Magic Type; this determines which spell will be cast */
public MagicType getMagicType() {
return getWizzrobeType().magicType;
}
/** Returns this Wizzrobe's type */
public WizzrobeType getWizzrobeType() {
return WizzrobeType.values()[dataWatcher.getWatchableObjectByte(TYPE_INDEX) % WizzrobeType.values().length];
}
/** Sets the Wizzrobe's type; this determines the Wizzrobe's Magic Type */
public void setType(WizzrobeType type) {
dataWatcher.updateObject(TYPE_INDEX, (byte)(type.ordinal()));
applyTypeTraits();
}
@Override
public EntityWizzrobe setType(int type) {
setType(WizzrobeType.values()[type % WizzrobeType.values().length]);
return this;
}
private void setTypeOnSpawn() {
if (Config.areMobVariantsAllowed() && rand.nextFloat() < Config.getMobVariantChance()) {
setType(rand.nextInt(WizzrobeType.values().length));
} else {
BiomeGenBase biome = worldObj.getBiomeGenForCoords(new BlockPos(this));
BiomeType biomeType = BiomeType.getBiomeTypeFor(biome);
for (WizzrobeType t : WizzrobeType.values()) {
if (t.favoredBiome == biomeType) {
setType(t);
return;
}
}
// no favored biome was found, set based on remaining biome types
if (biomeType != null) {
switch (biomeType) {
case ARID:
setType(rand.nextFloat() < 0.5F ? WizzrobeType.LIGHTNING_WIZ : WizzrobeType.FIRE_WIZ);
break;
case RIVER: // swamps and rivers are same as Jungle
case JUNGLE:
setType(rand.nextFloat() < 0.5F ? WizzrobeType.WIND_WIZ : WizzrobeType.LIGHTNING_WIZ);
break;
case TAIGA:
setType(rand.nextFloat() < 0.5F ? WizzrobeType.WIND_WIZ : WizzrobeType.ICE_WIZ);
break;
default:
}
}
}
}
protected void applyTypeTraits() {
ZSSEntityInfo info = ZSSEntityInfo.get(this);
info.removeAllBuffs();
info.applyBuff(Buff.RESIST_MAGIC, Integer.MAX_VALUE, 50);
info.applyBuff(Buff.RESIST_STUN, Integer.MAX_VALUE, 50);
switch (getMagicType()) {
case FIRE:
info.applyBuff(Buff.RESIST_FIRE, Integer.MAX_VALUE, 50);
info.applyBuff(Buff.WEAKNESS_COLD, Integer.MAX_VALUE, 100);
break;
case ICE:
info.applyBuff(Buff.RESIST_COLD, Integer.MAX_VALUE, 50);
info.applyBuff(Buff.WEAKNESS_FIRE, Integer.MAX_VALUE, 100);
break;
case LIGHTNING:
info.applyBuff(Buff.RESIST_SHOCK, Integer.MAX_VALUE, 50);
break;
case WIND:
break;
default:
}
}
/** The base time required to cast a spell */
protected int getBaseCastingTime() {
return 80;
}
/** Returns the current casting time for entity animations */
public int getCurrentCastingTime() {
return dataWatcher.getWatchableObjectInt(CASTING_TIME);
}
private void setCurrentCastingTime(int time) {
if (!worldObj.isRemote) {
dataWatcher.updateObject(CASTING_TIME, Math.max(0, time));
}
}
/** Returns the current maximum casting time for entity animations */
public int getMaxCastingTime() {
return dataWatcher.getWatchableObjectInt(MAX_CAST_TIME);
}
private void setMaxCastingTime(int time) {
if (!worldObj.isRemote) {
dataWatcher.updateObject(MAX_CAST_TIME, Math.max(0, time));
}
}
/**
* Sets teleportation boundary using {@link EntityAITeleport#setTeleBounds}
*/
public final void setTeleBounds(AxisAlignedBB newBounds) {
teleportAI.setTeleBounds(newBounds);
}
@Override
protected float getSoundVolume() {
return 0.4F;
}
@Override
protected boolean canTriggerWalking() {
return false;
}
@Override
public boolean attackEntityAsMob(Entity target) {
return false; // no damage on contact
}
@Override
public boolean attackEntityFrom(DamageSource source, float amount) {
if (!worldObj.isRemote) {
boolean wasReflected = false;
if (source.getSourceOfDamage() instanceof EntityMagicSpell) {
EntityMagicSpell spell = (EntityMagicSpell) source.getSourceOfDamage();
wasReflected = spell.getEntityData().getBoolean("isReflected");
if (spell.getThrower() == this && !wasReflected) {
return false;
}
}
if (!wasReflected && canTelevade(source) && teleportAI.teleportRandomly()) {
return false;
}
}
return super.attackEntityFrom(source, amount);
}
@Override
protected void damageEntity(DamageSource source, float amount) {
if (getCurrentCastingTime() > 0 && !isEntityInvulnerable(source) && amount >= getMinInterruptDamage()) {
float interruptChance = 1.0F - ((getMaxInterruptDamage() - amount) / getMaxInterruptDamage());
if (rand.nextFloat() < interruptChance) {
magicAI.interruptCasting();
teleportAI.scheduleNextTeleport(2); // teleport right away - no second attacks!
}
}
super.damageEntity(source, amount);
}
//@Override // method no longer exists in super classes
protected EntityLivingBase findPlayerToAttack() {
return (getBrightness(1.0F) < 0.5F ? worldObj.getClosestPlayerToEntity(this, 32.0D) : null);
}
/**
* Base spell damage
*/
protected float getBaseSpellDamage() {
return 4.0F;
}
/**
* Spell area of effect
*/
protected float getSpellAoE() {
return 1.25F;
}
/**
* Chance spell will be reflected when blocked by Mirror Shield
* @return negative values use default EntityMagicSpell handling
*/
protected float getReflectChance() {
return -1.0F; // use default handling
}
/**
* Minimum damage that can possibly interrupt spell-casting
*/
protected float getMinInterruptDamage() {
return 4.0F;
}
/**
* Amount of damage at which spell interruption is guaranteed
*/
protected float getMaxInterruptDamage() {
return 16.0F; // 4 damage has 25% interrupt chance
}
/**
* Returns true if Wizzrobe can attempt to teleport out of harm's way
*/
private boolean canTelevade(DamageSource source) {
if (getCurrentCastingTime() > 0 || !canEvadeSource(source)) {
return false;
}
return (teleportAI.canTeleport() || (!teleportAI.isTeleporting() && rand.nextFloat() < getTelevadeChance()));
}
/**
* Return true if the DamageSource is a kind that may be evaded
*/
protected boolean canEvadeSource(DamageSource source) {
return source.getEntity() != null;
}
/**
* Chance that Wizzrobe can teleport out of harm's way even if teleport AI cannot teleport
*/
protected float getTelevadeChance() {
return 0.5F;
}
@Override
public boolean canContinueCasting() {
ZSSEntityInfo info = ZSSEntityInfo.get(this);
if (info.isBuffActive(Buff.STUN)) {
if (worldObj.rand.nextInt(50) > info.getBuffAmplifier(Buff.STUN)) {
info.removeBuff(Buff.STUN);
}
return false;
}
return true;
}
@Override
public int beginSpellCasting(EntityLivingBase target) {
if (target == null) {
return 0;
}
int castTime = getBaseCastingTime() - (worldObj.getDifficulty().getDifficultyId() * 10);
castTime += (rand.nextInt(castTime) - rand.nextInt(castTime)) / 2;
setMaxCastingTime(castTime);
setCurrentCastingTime(castTime);
return castTime;
}
@Override
public void castPassiveSpell() {}
@Override
public void castRangedSpell(EntityLivingBase target, float range) {
float difficulty = (float) worldObj.getDifficulty().getDifficultyId();
EntityMagicSpell spell = new EntityMagicSpell(worldObj, this, target, 0.8F + (0.25F * difficulty), (float)(14 - (difficulty * 4)));
spell.setType(getMagicType());
spell.setArea(getSpellAoE());
spell.setDamageBypassesArmor();
spell.setDamage(getBaseSpellDamage() * difficulty);
spell.setReflectChance(getReflectChance());
WorldUtils.playSoundAtEntity(this, Sounds.WHOOSH, 0.4F, 0.5F);
if (!worldObj.isRemote) {
worldObj.spawnEntityInWorld(spell);
}
teleportAI.scheduleNextTeleport(rand.nextInt(5) + rand.nextInt(5) + 6);
}
@Override
public void stopCasting() {
setCurrentCastingTime(0);
}
@Override
public void onLivingUpdate() {
super.onLivingUpdate();
// update casting time for rendering and sound effects
int castTime = getCurrentCastingTime();
if (!worldObj.isRemote && castTime > 0) {
--castTime;
setCurrentCastingTime(castTime);
MagicType type = getMagicType();
if (castTime % type.getSoundFrequency() == 0) {
worldObj.playSoundAtEntity(this, type.getMovingSound(), type.getSoundVolume(rand) * 0.5F, type.getSoundPitch(rand));
}
}
// spawn some Enderman-like particles
for (int i = 0; i < 2; ++i) {
worldObj.spawnParticle(EnumParticleTypes.PORTAL,
posX + (rand.nextDouble() - 0.5D) * (double) width,
posY + rand.nextDouble() * (double) height - 0.25D,
posZ + (rand.nextDouble() - 0.5D) * (double) width,
(rand.nextDouble() - 0.5D) * 2.0D,
-rand.nextDouble(),
(rand.nextDouble() - 0.5D) * 2.0D);
}
}
@Override
public void onUpdate() {
super.onUpdate();
teleportAI.invalidateBounds(256.0D);
if (!onGround && motionY < 0.0D) {
motionY *= 0.6D;
}
if (isBurning() && getMagicType() == MagicType.FIRE) {
extinguish(); // immune to burning, but not all fire damage
}
if (teleportAI.getTeleBounds() == null && getAttackTarget() == null && ++noTargetTime > 400) {
noTargetTime = 0;
EntityLivingBase player = findPlayerToAttack();
if (player instanceof EntityPlayer && !worldObj.isRemote && canEntityBeSeen(player) && !((EntityPlayer) player).capabilities.disableDamage) {
setAttackTarget(player);
teleportAI.setTeleporting();
for (int i = 0; i < 64; ++i) {
if (EntityAITeleport.teleportToEntity(worldObj, this, player, null, teleportAI.isGrounded)) {
break;
}
}
}
}
if (worldObj.isDaytime() && ticksExisted % 20 == 0 && !isValidLightLevel()) {
despawnEntity();
}
}
@Override
public void fall(float distance, float damageMultiplier) {}
@Override
protected Item getDropItem() {
return Items.ender_pearl;
}
@Override
protected void addRandomDrop() {
ItemStack drop = getRareDrop(4);
if (drop != null) {
entityDropItem(drop, 0.0F);
}
}
/**
* @param modifier Applied to book enchantment level
*/
private ItemStack getRareDrop(int modifier) {
int rarity = rand.nextInt(8);
if (rarity == 0) {
return new ItemStack(ZSSItems.treasure, 1, Treasures.EVIL_CRYSTAL.ordinal());
} else if (rarity < 4) {
ItemStack book = new ItemStack(Items.book);
EnchantmentHelper.addRandomEnchantment(rand, book, rand.nextInt(8) + rand.nextInt(8) + modifier);
return book;
}
switch (getWizzrobeType()) {
case FIRE_WIZ: return new ItemStack(ZSSItems.arrowFire);
case ICE_WIZ: return new ItemStack(ZSSItems.arrowIce);
case LIGHTNING_WIZ: return new ItemStack(ZSSItems.arrowLight);
default: return null;
}
}
@Override
public float getLootableChance(EntityPlayer player, WhipType whip) {
return 0.2F;
}
@Override
public ItemStack getEntityLoot(EntityPlayer player, WhipType whip) {
if (rand.nextInt(10 - whip.ordinal()) == 0) {
return new ItemStack(ZSSItems.treasure, 1, Treasures.EVIL_CRYSTAL.ordinal());
}
return getRareDrop((3 * (whip.ordinal() + 1)));
}
@Override
public boolean onLootStolen(EntityPlayer player, boolean wasItemStolen) {
return true;
}
@Override
public boolean isHurtOnTheft(EntityPlayer player, WhipType whip) {
return true;
}
@Override
public boolean getCanSpawnHere() {
return !worldObj.isDaytime() && super.getCanSpawnHere() && worldObj.getTotalWorldTime() > Config.getTimeToSpawnWizzrobe();
}
@Override
public IEntityLivingData onInitialSpawn(DifficultyInstance difficulty, IEntityLivingData data) {
data = super.onInitialSpawn(difficulty, data);
setTypeOnSpawn();
return data;
}
@Override
public void writeEntityToNBT(NBTTagCompound compound) {
super.writeEntityToNBT(compound);
compound.setByte("WizzrobeType", dataWatcher.getWatchableObjectByte(TYPE_INDEX));
AxisAlignedBB box = teleportAI.getTeleBounds();
if (box != null) {
NBTTagCompound bounds = new NBTTagCompound();
bounds.setDouble("minX", box.minX);
bounds.setDouble("maxX", box.maxX);
bounds.setDouble("minY", box.minY);
bounds.setDouble("maxY", box.maxY);
bounds.setDouble("minZ", box.minZ);
bounds.setDouble("maxZ", box.maxZ);
compound.setTag("teleBounds", bounds);
}
}
@Override
public void readEntityFromNBT(NBTTagCompound compound) {
super.readEntityFromNBT(compound);
dataWatcher.updateObject(TYPE_INDEX, compound.getByte("WizzrobeType"));
if (compound.hasKey("teleBounds")) {
NBTTagCompound bounds = compound.getCompoundTag("teleBounds");
double minX = bounds.getDouble("minX");
double maxX = bounds.getDouble("maxX");
double minY = bounds.getDouble("minY");
double maxY = bounds.getDouble("maxY");
double minZ = bounds.getDouble("minZ");
double maxZ = bounds.getDouble("maxZ");
setTeleBounds(new AxisAlignedBB(minX, minY, minZ, maxX, maxY, maxZ));
}
}
}