/** 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.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.SharedMonsterAttributes; import net.minecraft.entity.ai.EntityAIHurtByTarget; import net.minecraft.entity.ai.EntityAINearestAttackableTarget; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.init.Items; import net.minecraft.item.Item; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; import net.minecraft.util.EnumParticleTypes; import net.minecraft.util.MathHelper; import net.minecraft.util.Vec3; import net.minecraft.world.World; import net.minecraftforge.fml.common.eventhandler.Event.Result; import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.fml.relauncher.SideOnly; import zeldaswordskills.api.damage.EnumDamageType; import zeldaswordskills.api.damage.IPostDamageEffect; import zeldaswordskills.api.entity.IEntityBombEater; import zeldaswordskills.api.entity.IEntityBombIngestible; import zeldaswordskills.api.entity.IEntityCustomTarget; import zeldaswordskills.api.entity.ai.EntityAIDynamicAction; import zeldaswordskills.api.entity.ai.EntityAIDynamicProne; import zeldaswordskills.api.entity.ai.EntityAITargetBombs; import zeldaswordskills.api.entity.ai.EntityAction; import zeldaswordskills.api.entity.ai.IEntityDynamic; import zeldaswordskills.api.entity.ai.IEntityDynamicAI; import zeldaswordskills.entity.ZSSEntityInfo; import zeldaswordskills.item.ZSSItems; import zeldaswordskills.ref.Sounds; import zeldaswordskills.util.BiomeType; import zeldaswordskills.util.PlayerUtils; import zeldaswordskills.util.TargetUtils; public class EntityDekuBaba extends EntityDekuBase implements IEntityBombEater, IEntityDynamic, IEntityCustomTarget { /** * Returns array of default biomes in which this entity may spawn naturally */ public static String[] getDefaultBiomes() { return BiomeType.getBiomeArray(null, BiomeType.FOREST, BiomeType.JUNGLE, BiomeType.PLAINS, BiomeType.RIVER); } /** Health update flag signaling that a bomb was ingested */ public static final byte BOMB_INGESTED = EntityDekuBase.flag_index++; public static final EntityAction ACTION_SPROUT = new EntityAction(EntityDekuBase.flag_index++, 11, 5); public static final EntityAction ACTION_READY = new EntityAction(EntityDekuBase.flag_index++, 0, 5); public static final EntityAction ACTION_ATTACK = new EntityAction(EntityDekuBase.flag_index++, 16, 7); public static final EntityAction ACTION_BOMB = new EntityAction(EntityDekuBase.flag_index++, ACTION_ATTACK.duration, ACTION_ATTACK.action_frame); public static final EntityAction ACTION_PRONE = new EntityAction(EntityDekuBase.flag_index++, 60, 0); private static final Map<Integer, EntityAction> actionMap = new HashMap<Integer, EntityAction>(); protected static void registerAction(EntityAction action) { actionMap.put(action.id, action); } static { registerAction(ACTION_SPROUT); registerAction(ACTION_READY); registerAction(ACTION_ATTACK); registerAction(ACTION_BOMB); registerAction(ACTION_PRONE); } /** DataWatcher index for alertness level */ public static final int ALERTNESS_INDEX = 17; /** Datawatcher index for current target entity's ID */ public static final int TARGET_INDEX = 18; /** Datawatcher index for current custom target entity's ID */ public static final int CUSTOM_TARGET_INDEX = 19; /** Datawatcher index for current difficulty ID since client doesn't normally know about that */ public static final int DIFFICULTY_INDEX = 20; /** Datawatcher index for custom state, e.g. confused */ public static final int STATUS_INDEX = 21; /** Value for 'confused' status */ public static final int STATUS_BOMB = 1; /** Value for 'confused' status */ public static final int STATUS_CONFUSED = 2; /** Maximum alertness level */ public static final int MAX_ALERTNESS = 50; /** Time it takes for any ingested bomb to explode */ public static final int FUSE_TIME = 20; /** Current action should never be null and should always be the same on both client and server */ protected EntityAction action = ACTION_READY; protected final List<EntityAction> actionList = new ArrayList<EntityAction>(); /** Current action timer */ protected int action_timer; /** Set to true if struck while prone */ protected boolean prone; /** Ingested bomb 'timer' used for flashing effect */ protected int status_timer; /** Possibly non-living target entity */ protected Entity target; public EntityDekuBaba(World world) { super(world); actionList.add(action); // add base action so can use #set later } @Override protected void addAITasks() { this.tasks.addTask(1, new EntityAIDynamicProne<EntityDekuBaba>(this, ACTION_PRONE, 63)); this.tasks.addTask(2, new EntityAIDynamicAction<EntityDekuBaba>(this, ACTION_ATTACK, 4.0F, true)); this.tasks.addTask(4, new EntityAITargetBombs<EntityDekuBaba>(this, ACTION_BOMB, 3.0F, true)); this.targetTasks.addTask(1, new EntityAIHurtByTarget(this, true)); this.targetTasks.addTask(2, new EntityAINearestAttackableTarget<EntityPlayer>(this, EntityPlayer.class, 0, true, false, null)); } @Override public void entityInit() { super.entityInit(); dataWatcher.addObject(ALERTNESS_INDEX, 0); dataWatcher.addObject(TARGET_INDEX, -1); dataWatcher.addObject(CUSTOM_TARGET_INDEX, -1); dataWatcher.addObject(DIFFICULTY_INDEX, worldObj.getDifficulty().getDifficultyId()); dataWatcher.addObject(STATUS_INDEX, 0); } @Override protected String getLivingSound() { return isFullyAlert() ? Sounds.LEAF_RUSTLE : null; } @Override protected float getSoundVolume() { return 0.75F + rand.nextFloat() * 0.25F; } @Override protected float getSoundPitch() { return 0.5F; } /** * Alertness level of this Deku Baba; 0 is fully retracted, {@code ACTION_SPROUT#duration} is fully extended and ready to act. * Value may be up to {@value #MAX_ALERTNESS}. */ protected int getAlertness() { return dataWatcher.getWatchableObjectInt(ALERTNESS_INDEX); } /** * Returns true if the Deku Baba is fully alert, i.e. it can perform actions and take damage */ public boolean isFullyAlert() { return isEntityAlive() && getActionTime(ACTION_SPROUT.id) > ACTION_SPROUT.getDuration(getActionSpeed(ACTION_SPROUT.id)); } private void updateAlertness() { if (action == ACTION_PRONE) { return; } int alertness = getAlertness(); int i = (this.getCurrentTarget() == null ? -1 : 1); if (this.getCurrentTarget() != null && !this.canEntityBeSeen(this.getCurrentTarget())) { i = (this.rand.nextInt(4) == 0 ? -1 : 0); } int modified = MathHelper.clamp_int(alertness + i, 0, MAX_ALERTNESS); if (modified != alertness) { dataWatcher.updateObject(ALERTNESS_INDEX, modified); } } /** * Returns true if this baba is confused */ public boolean isConfused() { return dataWatcher.getWatchableObjectInt(STATUS_INDEX) == STATUS_CONFUSED; } /** * Sets the confused state of this baba */ public void setConfused(boolean confused) { dataWatcher.updateObject(STATUS_INDEX, confused ? STATUS_CONFUSED : 0); status_timer = (confused && status_timer == 0 ? rand.nextInt(80) + rand.nextInt(80) + 142 : 0); } /** * Whether the action ID is considered an 'attack' */ public boolean isAttack(int action_id) { return action_id == ACTION_ATTACK.id || action_id == ACTION_BOMB.id; } @Override public List<EntityAction> getActiveActions() { return actionList; } @Override public int getActionTime(int action_id) { if (action_id == ACTION_SPROUT.id) { return getAlertness(); } // action timer starts at max and decrements to 0, but needs to be returned as starting at 0 and increment to max EntityAction a = getActionById(action_id); if (a == action && action_timer > 0) { return a.getDuration(getActionSpeed(action_id)) - action_timer; } return 0; } @Override public float getActionSpeed(int action_id) { int i = getDifficultyModifier() - 2; if (isAttack(action_id)) { return 0.7F + (i * 0.15F); } else if (action_id == ACTION_PRONE.id) { return 1.0F + (i * 0.25F); } return 1.0F; } @Override public boolean canExecute(int action_id, IEntityDynamicAI ai) { if (isAttack(action_id)) { Entity entity = getCurrentTarget(); return action_timer == 0 && entity != null && canAttack() && TargetUtils.isTargetInFrontOf(this, entity, 15F); } else if (action_id == ACTION_PRONE.id) { return prone; } return true; } @Override public void beginAction(int action_id, IEntityDynamicAI ai) { if (isAttack(action_id)) { this.playLivingSound(); } setActionState(action_id); } @Override public void endAction(int action_id, IEntityDynamicAI ai) { if (action_id == ACTION_PRONE.id) { prone = false; } if (action_id == action.id) { setActionState(ACTION_READY.id); } } @Override public void performAction(int action_id, IEntityDynamicAI ai) { Entity target = getCurrentTarget(); if (isConfused()) { // do nothing } else if (action_id == ACTION_ATTACK.id) { if (target instanceof EntityLivingBase && canAttack() && TargetUtils.isTargetInFrontOf(this, target, 15F)) { attackEntityAsMob(target); } } else if (action_id == ACTION_BOMB.id) { if (target instanceof IEntityBombIngestible && canAttack() && TargetUtils.isTargetInFrontOf(this, target, 15F)) { IEntityBombIngestible bomb = (IEntityBombIngestible) getCurrentTarget(); if (ZSSEntityInfo.get(this).onBombIngested(bomb)) { dataWatcher.updateObject(STATUS_INDEX, STATUS_BOMB); worldObj.setEntityState(this, BOMB_INGESTED); double damage = this.getAttributeMap().getAttributeInstance(SharedMonsterAttributes.attackDamage).getAttributeValue(); bomb.setExplosionDamage((float) damage * worldObj.getDifficulty().getDifficultyId()); bomb.setFuseTime(FUSE_TIME); ZSSEntityInfo.get(this).refreshFuseTime(); } } } } /** Returns the world difficulty setting ID, between 0 and 3 */ public int getDifficultyModifier() { return dataWatcher.getWatchableObjectInt(DIFFICULTY_INDEX); } /** Counts up to {@value #FUSE_TIME} after ingesting a bomb */ public int getBombTimer() { return (status_timer > 0 && dataWatcher.getWatchableObjectInt(STATUS_INDEX) == STATUS_BOMB ? FUSE_TIME - status_timer : 0); } @Override public Result ingestBomb(IEntityBombIngestible bomb) { return Result.DENY; } @Override public boolean onBombIndigestion(IEntityBombIngestible bomb) { prone = true; // allows full damage to be applied and results in receiving Deku Nuts return true; } @Override public boolean doesIngestedBombExplode(IEntityBombIngestible bomb) { return true; } @Override public boolean isIngestedBombFatal(IEntityBombIngestible bomb) { return true; } @Override public Entity getCurrentTarget() { // prioritize custom target when available Entity entity = getCustomTarget(); if (entity == null) { entity = getAttackTarget(); } return entity; } @Override public Entity getCustomTarget() { if (target == null && dataWatcher.getWatchableObjectInt(CUSTOM_TARGET_INDEX) > -1) { target = worldObj.getEntityByID(dataWatcher.getWatchableObjectInt(CUSTOM_TARGET_INDEX)); if (target == null) { dataWatcher.updateObject(CUSTOM_TARGET_INDEX, -1); } } return target; } @Override public void setCustomTarget(Entity entity) { this.target = entity; dataWatcher.updateObject(CUSTOM_TARGET_INDEX, (entity == null ? -1 : entity.getEntityId())); } @Override public EntityLivingBase getAttackTarget() { if (super.getAttackTarget() == null && dataWatcher.getWatchableObjectInt(TARGET_INDEX) > -1) { Entity target = worldObj.getEntityByID(dataWatcher.getWatchableObjectInt(TARGET_INDEX)); if (target instanceof EntityLivingBase) { setAttackTarget((EntityLivingBase) target); } else { dataWatcher.updateObject(TARGET_INDEX, -1); } } return super.getAttackTarget(); } @Override public void setAttackTarget(EntityLivingBase entity) { super.setAttackTarget(entity); dataWatcher.updateObject(TARGET_INDEX, (entity == null ? -1 : entity.getEntityId())); } @Override protected boolean canAttack() { return isFullyAlert() && action != ACTION_PRONE; } @Override public boolean attackEntityAsMob(Entity entity) { boolean blocking = false; // item will no longer be in use after block: store current state if (entity instanceof EntityPlayer) { EntityPlayer player = (EntityPlayer) entity; if (PlayerUtils.isBlocking(player) && PlayerUtils.isShield(player.getHeldItem())) { blocking = true; } } boolean flag = super.attackEntityAsMob(entity); if (blocking) { prone = true; } return flag; } @Override public boolean attackEntityFrom(DamageSource source, float amount) { if (!isFullyAlert() && source.getSourceOfDamage() != null) { return false; } else if (isSourceFatal(source)) { // only give deku nuts if hit while already prone or attacking prone ^= (!isAttack(action.id)); return super.attackEntityFrom(source, getMaxHealth()); } else if (prone) { if (super.attackEntityFrom(source, getSlashDamage(source, amount))) { this.onProneAttack(source, amount); return true; } return false; } else if (didAttackCauseProne(source, amount)) { return true; } else if (isDamageEffective(source)) { return super.attackEntityFrom(source, amount); } return false; } /** * Called after a successful attack while prone * @param amount The original damage amount, not necessarily the actual amount inflicted */ protected void onProneAttack(DamageSource source, float amount) { if (this.getHealth() > 0.0F) { this.setActionState(ACTION_READY.id); action_timer = 10; // minor delay before next action } } /** * Returns true if the attack damaged the entity, thereby causing it to become prone */ protected boolean didAttackCauseProne(DamageSource source, float amount) { boolean flag = false; // Prevent DoT type effects from causing prone by requiring #getEntity() to be non-null if (prone || amount < 0.5F || source.getEntity() == null) { return false; } else if (source.isExplosion()) { // explosions can always cause baba to go prone and cause decent damage flag = super.attackEntityFrom(source, Math.max(0.5F, amount * 0.25F)); } else if (source instanceof IPostDamageEffect && ((IPostDamageEffect) source).getDuration(EnumDamageType.STUN) > 0) { // stun attacks do minimal damage, but can always cause baba to go prone amount = Math.max(amount * 0.25F, 0.5F); flag = super.attackEntityFrom(source, amount); } else if (isAttack(action.id) && getActionTime(action.id) > worldObj.getDifficulty().getDifficultyId()) { flag = super.attackEntityFrom(source, getSlashDamage(source, amount)); } if (flag && this.getHealth() > 0.0F) { // only set to prone if still alive if (!isConfused() && source.getSourceOfDamage() instanceof EntityPlayer && ((EntityPlayer) source.getSourceOfDamage()).getHeldItem() == null) { this.setConfused(true); } else { this.setConfused(false); this.prone = true; // flag for prone AI to execute } } return flag; } /** * Return the amount of damage to inflict upon striking the baba either while it is attacking or while it is prone * @param source Check {@link #isSlashing(DamageSource)} to see if it is a slashing weapon * @param amount The original damage amount */ protected float getSlashDamage(DamageSource source, float amount) { return isSlashing(source) ? (prone ? getMaxHealth() : getMaxHealth() / 2.0F) : amount; } protected boolean isDamageEffective(DamageSource source) { if (source.isFireDamage() || source.isExplosion() || source.isMagicDamage() || source == DamageSource.inWall || source == DamageSource.outOfWorld) { return true; } return false; } protected boolean isTargetValid(EntityLivingBase entity) { if (!entity.isEntityAlive()) { return false; } else if (entity instanceof EntityPlayer && ((EntityPlayer) entity).capabilities.disableDamage) { return false; } return entity.getDistanceSqToEntity(this) < 100.0D; } @Override public void onUpdate() { super.onUpdate(); if (!this.isEntityAlive()) { return; // reduces indentation below } // Set targets to null on both server and client, otherwise model freaks out in Creative if (this.target != null && !this.target.isEntityAlive()) { this.setCustomTarget(null); } if (this.getAttackTarget() != null && !isTargetValid(getAttackTarget())) { this.setAttackTarget(null); } updateAlertness(); if (!worldObj.isRemote) { if (worldObj.getDifficulty().getDifficultyId() != getDifficultyModifier()) { dataWatcher.updateObject(DIFFICULTY_INDEX, worldObj.getDifficulty().getDifficultyId()); } } else if (isConfused() && ticksExisted % 10 == 0) { Vec3 look = this.getLookVec(); for (int i = 0; i < 1; ++i) { worldObj.spawnParticle(EnumParticleTypes.SPELL, posX - look.xCoord * 0.5D + 0.1D * rand.nextGaussian(), this.getEntityBoundingBox().maxY + 0.25D + 0.15D * rand.nextGaussian(), posZ - look.zCoord * 0.5D + 0.1D * rand.nextGaussian(), 0.0D, 0.0D, 0.0D); } } if (status_timer > 0) { --status_timer; if (status_timer == 0 && !worldObj.isRemote) { dataWatcher.updateObject(STATUS_INDEX, 0); } } if (action_timer > 0 && isEntityAlive()) { --action_timer; if (action_timer == 0) { setActionState(ACTION_READY.id); } } if (getActionTime(ACTION_SPROUT.id) > ACTION_SPROUT.getActionFrame(1.0F)) { Entity target = this.getCurrentTarget(); if (target != null && action != ACTION_PRONE) { this.getLookHelper().setLookPositionWithEntity(target, 30.0F, 30.0F); // Manually update rotation yaw since the Deku Baba does not move double dx = target.posX - this.posX; double dz = target.posZ - this.posZ; double dy = target.posY - MathHelper.floor_double(this.getEntityBoundingBox().minY + 0.5D); double d = dx * dx + dy * dy + dz * dz; if (d >= 2.500000277905201E-7D) { float f = (float)(Math.atan2(dz, dx) * 180.0D / Math.PI) - 90.0F; this.rotationYaw = this.limitAngle(this.rotationYaw, f, 30.0F); } } } } @Override protected byte getCustomDeathFlag(DamageSource source) { if (prone || isSourceFatal(source)) { return super.getCustomDeathFlag(source); } return 0; } private float limitAngle(float angle, float target, float max) { target = MathHelper.clamp_float(MathHelper.wrapAngleTo180_float(target - angle), -max, max); return angle + target; } /** * Return an EntityAction based on an integer ID; may return null */ public EntityAction getActionById(int id) { return EntityDekuBaba.actionMap.get(id); } /** * Sets current action based on the given ID, return false if the ID is not a valid action */ protected boolean setActionState(int id) { EntityAction a = getActionById(id); if (a != null) { action = a; prone = (action == ACTION_PRONE); action_timer = action.getDuration(getActionSpeed(action.id)); if (!worldObj.isRemote) { worldObj.setEntityState(this, (byte) action.id); } actionList.set(0, action); return true; } return false; } @Override @SideOnly(Side.CLIENT) public void handleStatusUpdate(byte flag) { if (flag == BOMB_INGESTED) { status_timer = FUSE_TIME; } else if (!setActionState(flag)) { super.handleStatusUpdate(flag); } } @Override protected Item getDropItem() { return (prone ? Items.stick : ZSSItems.dekuNut); } @Override public void writeToNBT(NBTTagCompound compound) { super.writeToNBT(compound); compound.setInteger("statusIndex", dataWatcher.getWatchableObjectInt(STATUS_INDEX)); compound.setInteger("statusTimer", status_timer); } @Override public void readFromNBT(NBTTagCompound compound) { super.readFromNBT(compound); dataWatcher.updateObject(STATUS_INDEX, compound.getInteger("statusIndex")); status_timer = compound.getInteger("statusTimer"); } }