/** 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.npc; import net.minecraft.enchantment.EnchantmentHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityAgeable; import net.minecraft.entity.EntityLiving; import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.SharedMonsterAttributes; import net.minecraft.entity.ai.EntityAIAttackOnCollide; import net.minecraft.entity.ai.EntityAIFollowGolem; import net.minecraft.entity.ai.EntityAIHurtByTarget; import net.minecraft.entity.ai.EntityAILookAtTradePlayer; import net.minecraft.entity.ai.EntityAIMoveThroughVillage; import net.minecraft.entity.ai.EntityAIMoveTowardsRestriction; import net.minecraft.entity.ai.EntityAIMoveTowardsTarget; import net.minecraft.entity.ai.EntityAIPlay; import net.minecraft.entity.ai.EntityAISwimming; import net.minecraft.entity.ai.EntityAITradePlayer; import net.minecraft.entity.ai.EntityAIVillagerMate; import net.minecraft.entity.ai.EntityAIWander; import net.minecraft.entity.ai.EntityAIWatchClosest; import net.minecraft.entity.ai.EntityAIWatchClosest2; import net.minecraft.entity.monster.EntityCreeper; import net.minecraft.entity.monster.IMob; import net.minecraft.entity.passive.EntityVillager; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.init.Items; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.BlockPos; import net.minecraft.util.ChatComponentTranslation; import net.minecraft.util.DamageSource; import net.minecraft.util.MathHelper; import net.minecraft.village.MerchantRecipe; import net.minecraft.village.MerchantRecipeList; import net.minecraft.village.Village; import net.minecraft.world.World; import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.fml.relauncher.SideOnly; import zeldaswordskills.api.entity.ISongEntity; import zeldaswordskills.entity.ZSSEntities; import zeldaswordskills.entity.ZSSEntityInfo; import zeldaswordskills.entity.ai.GenericAIDefendVillage; import zeldaswordskills.entity.ai.IVillageDefender; import zeldaswordskills.entity.buff.Buff; import zeldaswordskills.entity.player.ZSSPlayerSongs; import zeldaswordskills.entity.player.quests.QuestBiggoronSword; import zeldaswordskills.entity.player.quests.ZSSQuests; import zeldaswordskills.item.ZSSItems; import zeldaswordskills.ref.Sounds; import zeldaswordskills.songs.AbstractZeldaSong; import zeldaswordskills.songs.ZeldaSongs; import zeldaswordskills.util.MerchantRecipeHelper; import zeldaswordskills.util.PlayerUtils; import zeldaswordskills.util.TimedAddItem; import zeldaswordskills.util.TimedChatDialogue; public class EntityGoron extends EntityVillager implements IVillageDefender, ISongEntity { /** Villager trade for Biggoron's Sword */ private static final MerchantRecipe BIGGORONS_TRADE = new MerchantRecipe(new ItemStack(ZSSItems.masterOre,3), new ItemStack(Items.diamond,4), new ItemStack(ZSSItems.swordBiggoron)); /** The Goron's village, since EntityVillager.villageObj cannot be accessed */ protected Village village; /** Flag for handling attack timer during client-side health update */ private static final Byte ATTACK_FLAG = (byte) 4; /** Replacement for EntityLivingBase#attackTime */ private int attackTimer; /** Timer for health regeneration, similar to players when satiated */ private int regenTimer; /** Flag to allow attributes to update to adult status */ private boolean wasChild; public EntityGoron(World world) { this(world, 0); } public EntityGoron(World world, int profession) { super(world, profession); setSize(1.5F, 2.8F); //setCanPickUpLoot(true); tasks.taskEntries.clear(); tasks.addTask(0, new EntityAISwimming(this)); //tasks.addTask(1, new EntityAIAvoidEntity(this, EntityZombie.class, 8.0F, 0.6D, 0.6D)); tasks.addTask(1, new EntityAITradePlayer(this)); tasks.addTask(1, new EntityAILookAtTradePlayer(this)); tasks.addTask(2, new EntityAIAttackOnCollide(this, EntityPlayer.class, 1.0D, false)); tasks.addTask(2, new EntityAIAttackOnCollide(this, 1.0D, false)); tasks.addTask(3, new EntityAIMoveTowardsTarget(this, getEntityAttribute(SharedMonsterAttributes.movementSpeed).getAttributeValue(), 16.0F)); tasks.addTask(4, new EntityAIMoveThroughVillage(this, 0.6D, true)); //tasks.addTask(2, new EntityAIMoveIndoors(this)); //tasks.addTask(3, new EntityAIRestrictOpenDoor(this)); //tasks.addTask(4, new EntityAIOpenDoor(this, true)); tasks.addTask(5, new EntityAIMoveTowardsRestriction(this, 1.0D)); tasks.addTask(6, new EntityAIVillagerMate(this)); tasks.addTask(7, new EntityAIFollowGolem(this)); tasks.addTask(8, new EntityAIPlay(this, 0.32D)); tasks.addTask(9, new EntityAIWatchClosest2(this, EntityPlayer.class, 3.0F, 1.0F)); tasks.addTask(9, new EntityAIWatchClosest2(this, EntityVillager.class, 5.0F, 0.02F)); tasks.addTask(9, new EntityAIWander(this, 0.6D)); tasks.addTask(10, new EntityAIWatchClosest(this, EntityLiving.class, 8.0F)); targetTasks.addTask(1, new GenericAIDefendVillage(this)); targetTasks.addTask(2, new EntityAIHurtByTarget(this, true)); //targetTasks.addTask(3, new EntityAINearestAttackableTarget(this, EntityLiving.class, 0, false, true, IMob.mobSelector)); } @Override protected void applyEntityAttributes() { super.applyEntityAttributes(); getEntityAttribute(SharedMonsterAttributes.maxHealth).setBaseValue(100.0D); getEntityAttribute(SharedMonsterAttributes.movementSpeed).setBaseValue(0.3D); getEntityAttribute(SharedMonsterAttributes.knockbackResistance).setBaseValue(0.75D); getAttributeMap().registerAttribute(SharedMonsterAttributes.attackDamage).setBaseValue(2.0D); isImmuneToFire = true; ZSSEntityInfo.get(this).applyBuff(Buff.RESIST_FIRE, Integer.MAX_VALUE, 100); } /** * Updates Goron attributes (maxHealth, etc.) as appropriate for growing age; * necessary because NBT is read AFTER applyEntityAttributes(), as well as for * updating after transition from childhood to adulthood * @notes Passes return value of {@link #isChild()} to {@link #updateEntityAttributes(boolean)} */ private void updateEntityAttributes() { updateEntityAttributes(isChild()); } /** * As {@link #updateEntityAttributes} with parameter to force child values for use * when spawning child entities, as their growing age is not yet set. * @param isChild Whether the entity is a child; wasChild is set to this value */ private void updateEntityAttributes(boolean isChild) { getEntityAttribute(SharedMonsterAttributes.maxHealth).setBaseValue((isChild ? 40.0D : 100.0D)); getEntityAttribute(SharedMonsterAttributes.movementSpeed).setBaseValue((isChild ? 0.5D : 0.3D)); getEntityAttribute(SharedMonsterAttributes.knockbackResistance).setBaseValue((isChild ? 0.25D : 0.75D)); getEntityAttribute(SharedMonsterAttributes.attackDamage).setBaseValue((isChild ? 2.0D : 8.0D)); wasChild = isChild; } @Override public void onLivingUpdate() { updateArmSwingProgress(); super.onLivingUpdate(); if (attackTimer > 0) { --attackTimer; } if (getAITarget() instanceof EntityPlayer || getAttackTarget() instanceof EntityPlayer) { if (ticksExisted > getRevengeTimer() + 30 || recentlyHit < 70) { setRevengeTarget(null); setAttackTarget(null); attackingPlayer = null; } } if (!worldObj.isRemote) { updateHealth(); } } @Override public void updateAITick() { super.updateAITick(); if (!hasHome()) { village = null; } else if (village == null) { village = worldObj.getVillageCollection().getNearestVillage(new BlockPos(this), 32); } // Copied from EntityIronGolem: if (village == null) { detachHome(); } else { BlockPos blockpos = village.getCenter(); setHomePosAndDistance(blockpos, (int)((float)village.getVillageRadius() * 0.6F)); } } /** * Checks if Goron should regenerate some health */ protected void updateHealth() { if (getHealth() < getMaxHealth()) { if (++regenTimer > 399) { heal(1.0F); regenTimer = 0; } } if (wasChild && !isChild()) { updateEntityAttributes(); } } @Override public MerchantRecipeList getRecipes(EntityPlayer player) { MerchantRecipeList list = super.getRecipes(player); // Make sure Biggoron's Sword trade always removed and only re-add it if appropriate MerchantRecipeHelper.removeTrade(list, BIGGORONS_TRADE, false, true); if (("Biggoron").equals(this.getCustomNameTag()) && ZSSQuests.get(player).hasCompleted(QuestBiggoronSword.class)) { list.add(0, BIGGORONS_TRADE); } return list; } @Override public boolean interact(EntityPlayer player) { if (worldObj.isRemote) { return super.interact(player); } boolean isDarunia = ("Darunia").equals(getCustomNameTag()); boolean isCured = ZSSPlayerSongs.get(player).hasCuredNpc("Darunia"); if ((isDarunia && !isCured) || rand.nextInt(4) == 0) { String chat = (("Darunia").equals(getCustomNameTag()) ? "chat.zss.darunia." : "chat.zss.goron."); if (isCured) { PlayerUtils.sendTranslatedChat(player, chat + "cured." + rand.nextInt(4)); } else { PlayerUtils.sendTranslatedChat(player, chat + "depressed." + rand.nextInt(4)); } return true; } return super.interact(player); } @Override public boolean attackEntityFrom(DamageSource source, float amount) { if (isEntityInvulnerable(source)) { return false; } else if (super.attackEntityFrom(source, amount)) { Entity entity = source.getEntity(); if (entity != this && entity instanceof EntityLivingBase && riddenByEntity != entity && ridingEntity != entity) { setAttackTarget((EntityLivingBase) entity); } return true; } else { return false; } } @Override @SideOnly(Side.CLIENT) public void handleStatusUpdate(byte flag) { if (flag == ATTACK_FLAG) { // matches golem's value for rendering; not the same as value on server attackTimer = 10; } else { super.handleStatusUpdate(flag); } } @Override public boolean attackEntityAsMob(Entity entity) { if (attackTimer > 0) { return false; } float amount = (float) getEntityAttribute(SharedMonsterAttributes.attackDamage).getAttributeValue(); int knockback = 0; attackTimer = 20; // set to 20 in EntityMob#attackEntity, but seems to be unnecessary due to AI worldObj.setEntityState(this, ATTACK_FLAG); if (entity instanceof EntityLivingBase) { // func_152377_a applies attack damage modifier of the held weapon vs. the attack creature's type (e.g. UNDEAD) amount += EnchantmentHelper.getModifierForCreature(this.getHeldItem(), ((EntityLivingBase) entity).getCreatureAttribute()); knockback += EnchantmentHelper.getKnockbackModifier(this); } boolean flag = entity.attackEntityFrom(DamageSource.causeMobDamage(this), amount); if (flag) { if (knockback > 0) { float f = (float) knockback * 0.5F; double dx = -MathHelper.sin(rotationYaw * (float) Math.PI / 180.0F) * f; double dz = MathHelper.cos(rotationYaw * (float) Math.PI / 180.0F) * f; entity.addVelocity(dx, 0.1D, dz); motionX *= 0.6D; motionZ *= 0.6D; } int fire = EnchantmentHelper.getFireAspectModifier(this); if (fire > 0) { entity.setFire(fire * 4); } if (entity instanceof EntityLivingBase) { EnchantmentHelper.applyThornEnchantments((EntityLivingBase) entity, this); } EnchantmentHelper.applyArthropodEnchantments(this, entity); } return flag; } @Override public boolean canBreatheUnderwater() { return true; } @Override public boolean canAttackClass(Class<? extends EntityLivingBase> cls) { return cls != EntityCreeper.class && super.canAttackClass(cls); } @Override protected void collideWithEntity(Entity entity) { if (entity instanceof EntityLivingBase) { collideWtihEntityLivingBase((EntityLivingBase) entity); } super.collideWithEntity(entity); } protected void collideWtihEntityLivingBase(EntityLivingBase entity) { if (entity instanceof IMob && canAttackClass(entity.getClass()) && getRNG().nextInt(20) == 0) { setAttackTarget((EntityLivingBase) entity); } } public int getAttackTimer() { return attackTimer; } @Override public int getTotalArmorValue() { return super.getTotalArmorValue() + (isChild() ? 4 : 10); } // TODO update sounds to Goron-specific sounds @Override protected String getLivingSound() { return isTrading() ? Sounds.VILLAGER_HAGGLE : Sounds.VILLAGER_IDLE; } @Override protected String getHurtSound() { return Sounds.VILLAGER_HIT; } @Override protected String getDeathSound() { return Sounds.VILLAGER_DEATH; } /** * Creates a child entity for {@link EntityAgeable#createChild(EntityAgeable)} * @return class-specific instance, rather than generic EntityAgeable */ @Override public EntityGoron createChild(EntityAgeable entity) { EntityGoron goron = new EntityGoron(worldObj); goron.onInitialSpawn(worldObj.getDifficultyForLocation(new BlockPos(entity)), null); goron.updateEntityAttributes(true); return goron; } @Override public Village getVillageToDefend() { return village; } @Override public boolean onSongPlayed(EntityPlayer player, AbstractZeldaSong song, int power, int affected) { if (("Darunia").equals(getCustomNameTag())) { playLivingSound(); if (song == ZeldaSongs.songSaria) { if (ZSSPlayerSongs.get(player).hasCuredNpc("Darunia")) { PlayerUtils.sendTranslatedChat(player, "chat.zss.song.saria.darunia.thanks"); } else if (power < 5) { PlayerUtils.sendTranslatedChat(player, "chat.zss.song.saria.darunia.weak"); } else if (ZSSPlayerSongs.get(player).onCuredNpc("Darunia")) { ItemStack gift = new ItemStack(ZSSItems.gauntletsSilver); new TimedChatDialogue(player, 0, 1500, new ChatComponentTranslation("chat.zss.song.saria.darunia.0"), new ChatComponentTranslation("chat.zss.song.saria.darunia.1"), new ChatComponentTranslation("chat.zss.song.saria.darunia.2", new ChatComponentTranslation(gift.getUnlocalizedName() + ".name"))); new TimedAddItem(player, gift, 3000, Sounds.SUCCESS); } else { PlayerUtils.sendTranslatedChat(player, "chat.zss.song.saria.darunia.thanks"); } } else if (ZSSPlayerSongs.get(player).hasCuredNpc("Darunia")) { PlayerUtils.sendTranslatedChat(player, "chat.zss.darunia.cured.wrong"); } else { PlayerUtils.sendTranslatedChat(player, "chat.zss.darunia.wrong"); } return true; } else if (song == ZeldaSongs.songSaria && affected == 0) { playLivingSound(); if (ZSSPlayerSongs.get(player).hasCuredNpc("Darunia")) { PlayerUtils.sendTranslatedChat(player, "chat.zss.song.saria.goron"); } else { PlayerUtils.sendTranslatedChat(player, "chat.zss.song.saria.goron.depressed"); } return true; } return false; } @Override public void readEntityFromNBT(NBTTagCompound compound) { super.readEntityFromNBT(compound); updateEntityAttributes(); } /** * Attempts to spawn a goron on the coat-tails of a villager joining the world * Sets the 'zssFirstJoinFlag' in the villager's NBT data to prevent processing * again when the game is restarted. * @param villager The villager currently spawning * @result Depending on the current ratio of villagers to gorons, a goron will be spawned near the villager */ public static void doVillagerSpawn(EntityVillager villager, World world) { NBTTagCompound compound = villager.getEntityData(); if (!villager.isChild() && !compound.hasKey("zssFirstJoinFlag")) { compound.setBoolean("zssFirstJoinFlag", true); int ratio = ZSSEntities.getGoronRatio(); if (ratio > 0 && world.rand.nextInt(ratio) == 0) { try { if (world.villageCollectionObj.getVillageList().isEmpty() || world.getVillageCollection().getNearestVillage(new BlockPos(villager), 32) != null) { EntityGoron goron = new EntityGoron(world, world.rand.nextInt(5)); double posX = villager.posX + world.rand.nextInt(8) - 4; double posZ = villager.posZ + world.rand.nextInt(8) - 4; goron.setLocationAndAngles(posX, villager.posY + 1, posZ, villager.rotationYaw, villager.rotationPitch); if (goron.getCanSpawnHere()) { world.spawnEntityInWorld(goron); } } } catch (NullPointerException e) { ; // catches null pointer from block not found during world load (super-flat only?) } } } } }