From d5cdba011a61f4dfeea3338fd078492a6dd45ddc Mon Sep 17 00:00:00 2001 From: Me <135455255+IcarussOne@users.noreply.github.com> Date: Fri, 10 Jan 2025 06:18:39 -0600 Subject: [PATCH] More boss improvements --- .../entity/EntityTFWinterWolf.java | 11 +- .../entity/ai/EntityAITFYetiRampage.java | 4 +- .../entity/boss/EntityTFHydra.java | 1521 +++++++-------- .../entity/boss/EntityTFKnightPhantom.java | 10 + .../entity/boss/EntityTFLich.java | 5 + .../entity/boss/EntityTFMinoshroom.java | 11 + .../entity/boss/EntityTFNaga.java | 1656 +++++++++-------- .../entity/boss/EntityTFSnowQueen.java | 919 ++++----- .../entity/boss/EntityTFUrGhast.java | 1140 ++++++------ .../entity/boss/EntityTFYetiAlpha.java | 21 +- 10 files changed, 2693 insertions(+), 2605 deletions(-) diff --git a/src/main/java/twilightforest/entity/EntityTFWinterWolf.java b/src/main/java/twilightforest/entity/EntityTFWinterWolf.java index 1537e4ca7a..38b7649f4a 100644 --- a/src/main/java/twilightforest/entity/EntityTFWinterWolf.java +++ b/src/main/java/twilightforest/entity/EntityTFWinterWolf.java @@ -1,8 +1,10 @@ package twilightforest.entity; import com.bobmowzie.mowziesmobs.server.potion.PotionHandler; +import com.bobmowzie.mowziesmobs.server.sound.MMSounds; import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.SharedMonsterAttributes; import net.minecraft.entity.ai.EntityAIAttackMelee; import net.minecraft.entity.ai.EntityAIHurtByTarget; @@ -10,7 +12,7 @@ import net.minecraft.entity.ai.EntityAISwimming; import net.minecraft.entity.ai.EntityAIWanderAvoidWater; import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.init.SoundEvents; +import net.minecraft.init.MobEffects; import net.minecraft.item.EnumDyeColor; import net.minecraft.network.datasync.DataParameter; import net.minecraft.network.datasync.DataSerializers; @@ -110,7 +112,7 @@ private void spawnBreathParticles() { } private void playBreathSound() { - playSound(SoundEvents.ENTITY_GHAST_SHOOT, rand.nextFloat() * 0.5F, rand.nextFloat() * 0.5F); + playSound(MMSounds.ENTITY_FROSTMAW_ICEBREATH_START, rand.nextFloat() * 0.75F, rand.nextFloat() * 1.5F); } @Override @@ -131,6 +133,11 @@ public void setBreathing(boolean flag) { @Override public void doBreathAttack(Entity target) { target.attackEntityFrom(DamageSource.causeMobDamage(this), BREATH_DAMAGE); + + if (target instanceof EntityLivingBase) { + ((EntityLivingBase)target).addPotionEffect(new PotionEffect(TFPotions.frosty, 5 * 20, 2)); // 5 seconds + ((EntityLivingBase)target).addPotionEffect(new PotionEffect(MobEffects.WEAKNESS, 5 * 20, 1)); // 5 seconds + } } @Override diff --git a/src/main/java/twilightforest/entity/ai/EntityAITFYetiRampage.java b/src/main/java/twilightforest/entity/ai/EntityAITFYetiRampage.java index 09cdf7b09f..8edd3963b3 100644 --- a/src/main/java/twilightforest/entity/ai/EntityAITFYetiRampage.java +++ b/src/main/java/twilightforest/entity/ai/EntityAITFYetiRampage.java @@ -80,12 +80,12 @@ public void updateTask() { this.yeti.destroyBlocksInAABB(this.yeti.getEntityBoundingBox().grow(1, 2, 1).offset(0, 2, 0)); // regular falling blocks - if (this.currentDuration % 20 == 0) { + if (this.currentDuration % 10 == 0) { this.yeti.makeRandomBlockFall(); } // blocks target players - if (this.currentDuration % 40 == 0) { + if (this.currentDuration % 20 == 0) { this.yeti.makeBlockAboveTargetFall(); } diff --git a/src/main/java/twilightforest/entity/boss/EntityTFHydra.java b/src/main/java/twilightforest/entity/boss/EntityTFHydra.java index 16521a23d0..fc17b8bc3d 100644 --- a/src/main/java/twilightforest/entity/boss/EntityTFHydra.java +++ b/src/main/java/twilightforest/entity/boss/EntityTFHydra.java @@ -44,761 +44,768 @@ public class EntityTFHydra extends EntityLiving implements IEntityMultiPart, IMob { - public static final ResourceLocation LOOT_TABLE = TwilightForestMod.prefix("entities/hydra"); - - private static final int TICKS_BEFORE_HEALING = 1000; - private static final int HEAD_RESPAWN_TICKS = 100; - private static final int HEAD_MAX_DAMAGE = 120; - private static final float ARMOR_MULTIPLIER = 8.0F; - private static final int MAX_HEALTH = 360; - private static float HEADS_ACTIVITY_FACTOR = 0.3F; - - private static final int SECONDARY_FLAME_CHANCE = 10; - private static final int SECONDARY_MORTAR_CHANCE = 16; - - private static final DataParameter DATA_SPAWNHEADS = EntityDataManager.createKey(EntityTFHydra.class, DataSerializers.BOOLEAN); - - private final Entity partArray[]; - - public final int numHeads = 7; - public final HydraHeadContainer[] hc = new HydraHeadContainer[numHeads]; - - public final MultiPartEntityPart body = new MultiPartEntityPart(this, "body", 4F, 4F); - private final MultiPartEntityPart leftLeg = new MultiPartEntityPart(this, "leg", 2F, 3F); - private final MultiPartEntityPart rightLeg = new MultiPartEntityPart(this, "leg", 2F, 3F); - private final MultiPartEntityPart tail = new MultiPartEntityPart(this, "tail", 4F, 4F); - private final BossInfoServer bossInfo = new BossInfoServer(getDisplayName(), BossInfo.Color.BLUE, BossInfo.Overlay.PROGRESS); - - private int ticksSinceDamaged = 0; - - public EntityTFHydra(World world) { - super(world); - - List parts = new ArrayList<>(); - parts.add(body); - parts.add(leftLeg); - parts.add(rightLeg); - parts.add(tail); - - for (int i = 0; i < numHeads; i++) { - hc[i] = new HydraHeadContainer(this, i, i < 3); - Collections.addAll(parts, hc[i].getNeckArray()); - } - - partArray = parts.toArray(new Entity[0]); - - this.ignoreFrustumCheck = true; - this.isImmuneToFire = true; - this.experienceValue = 511; - - setSize(16F, 12F); - setSpawnHeads(true); - } - - @Override - public void setCustomNameTag(String name) { - super.setCustomNameTag(name); - this.bossInfo.setName(this.getDisplayName()); - } - - @Override - protected void applyEntityAttributes() { - super.applyEntityAttributes(); - this.getEntityAttribute(SharedMonsterAttributes.MAX_HEALTH).setBaseValue(MAX_HEALTH); - this.getEntityAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).setBaseValue(0.28D); - } - - @Override - public void addTrackingPlayer(EntityPlayerMP player) { - super.addTrackingPlayer(player); - this.bossInfo.addPlayer(player); - } - - @Override - public void removeTrackingPlayer(EntityPlayerMP player) { - super.removeTrackingPlayer(player); - this.bossInfo.removePlayer(player); - } - - @Override - protected void despawnEntity() { - if (world.getDifficulty() == EnumDifficulty.PEACEFUL) { - world.setBlockState(getPosition().add(0, 2, 0), TFBlocks.boss_spawner.getDefaultState().withProperty(BlockTFBossSpawner.VARIANT, BossVariant.HYDRA)); - setDead(); - for (HydraHeadContainer container : hc) { - if (container.headEntity != null) { - container.headEntity.setDead(); - } - } - } else { - super.despawnEntity(); - } - } - - // [Vanilla Copy] from EntityLivingBase. Hydra doesn't like the one from EntityLiving for whatever reason - @Override - protected float updateDistance(float p_110146_1_, float p_110146_2_) - { - float f = MathHelper.wrapDegrees(p_110146_1_ - this.renderYawOffset); - this.renderYawOffset += f * 0.3F; - float f1 = MathHelper.wrapDegrees(this.rotationYaw - this.renderYawOffset); - boolean flag = f1 < -90.0F || f1 >= 90.0F; - - if (f1 < -75.0F) - { - f1 = -75.0F; - } - - if (f1 >= 75.0F) - { - f1 = 75.0F; - } - - this.renderYawOffset = this.rotationYaw - f1; - - if (f1 * f1 > 2500.0F) - { - this.renderYawOffset += f1 * 0.2F; - } - - if (flag) - { - p_110146_2_ *= -1.0F; - } - - return p_110146_2_; - } - - @Override - public void onLivingUpdate() { - if (hc[0].headEntity == null || hc[1].headEntity == null || hc[2].headEntity == null) { - // don't spawn if we're connected in multiplayer - if (!world.isRemote && shouldSpawnHeads()) { - for (int i = 0; i < numHeads; i++) { - hc[i].headEntity = new EntityTFHydraHead(this, "head" + i, 3F, 3F); - hc[i].headEntity.setPosition(this.posX, this.posY, this.posZ); - hc[i].setHeadPosition(); - world.spawnEntity(hc[i].headEntity); - } - - setSpawnHeads(false); - } - } - - body.onUpdate(); - - // update all heads (maybe we should change to only active ones - for (int i = 0; i < numHeads; i++) { - hc[i].onUpdate(); - } - - if (this.hurtTime > 0) { - for (int i = 0; i < numHeads; i++) { - hc[i].setHurtTime(this.hurtTime); - } - } - - this.ticksSinceDamaged++; - - if (!this.world.isRemote && this.ticksSinceDamaged > TICKS_BEFORE_HEALING && this.ticksSinceDamaged % 5 == 0) { - this.heal(1); - } - - // update fight variables for difficulty setting - setDifficultyVariables(); - - super.onLivingUpdate(); - - body.width = body.height = 6.0F; - tail.width = 6.0F; - tail.height = 2.0F; - - // set body part positions - float angle; - double dx, dy, dz; - - // body goes behind the actual position of the hydra - angle = (((renderYawOffset + 180) * 3.141593F) / 180F); - - dx = posX - MathHelper.sin(angle) * 3.0; - dy = posY + 0.1; - dz = posZ + MathHelper.cos(angle) * 3.0; - body.setPosition(dx, dy, dz); - - dx = posX - MathHelper.sin(angle) * 10.5; - dy = posY + 0.1; - dz = posZ + MathHelper.cos(angle) * 10.5; - tail.setPosition(dx, dy, dz); - - // destroy blocks - if (!this.world.isRemote) { - if (hurtTime == 0) { - this.collideWithEntities(this.world.getEntitiesWithinAABBExcludingEntity(this, this.body.getEntityBoundingBox()), this.body); - this.collideWithEntities(this.world.getEntitiesWithinAABBExcludingEntity(this, this.tail.getEntityBoundingBox()), this.tail); - } - - this.destroyBlocksInAABB(this.body.getEntityBoundingBox()); - this.destroyBlocksInAABB(this.tail.getEntityBoundingBox()); - - for (int i = 0; i < numHeads; i++) { - if (hc[i].headEntity != null && hc[i].isActive()) { - this.destroyBlocksInAABB(this.hc[i].headEntity.getEntityBoundingBox()); - } - } - - // smash blocks beneath us too - if (this.ticksExisted % 20 == 0) { - if (isUnsteadySurfaceBeneath()) { - this.destroyBlocksInAABB(this.getEntityBoundingBox().offset(0, -1, 0)); - - } - } - - bossInfo.setPercent(getHealth() / getMaxHealth()); - } - } - - @Override - protected void entityInit() { - super.entityInit(); - dataManager.register(DATA_SPAWNHEADS, false); - } - - private boolean shouldSpawnHeads() { - return dataManager.get(DATA_SPAWNHEADS); - } - - private void setSpawnHeads(boolean flag) { - dataManager.set(DATA_SPAWNHEADS, flag); - } - - @Override - public void writeEntityToNBT(NBTTagCompound compound) { - super.writeEntityToNBT(compound); - compound.setBoolean("SpawnHeads", shouldSpawnHeads()); - compound.setByte("NumHeads", (byte) countActiveHeads()); - } - - @Override - public void readEntityFromNBT(NBTTagCompound compound) { - super.readEntityFromNBT(compound); - setSpawnHeads(compound.getBoolean("SpawnHeads")); - activateNumberOfHeads(compound.getByte("NumHeads")); - if (this.hasCustomName()) { - this.bossInfo.setName(this.getDisplayName()); - } - } - - - // TODO modernize this more (old AI copypasta still kind of here) - private int numTicksToChaseTarget; - - @Override - protected void updateAITasks() { - moveStrafing = 0.0F; - moveForward = 0.0F; - float f = 48F; - - // kill heads that have taken too much damage - for (int i = 0; i < numHeads; i++) { - if (hc[i].isActive() && hc[i].getDamageTaken() > HEAD_MAX_DAMAGE) { - hc[i].setNextState(HydraHeadContainer.State.DYING); - hc[i].endCurrentAction(); - - // set this head and a random dead head to respawn - hc[i].setRespawnCounter(HEAD_RESPAWN_TICKS); - int otherHead = getRandomDeadHead(); - if (otherHead != -1) { - hc[otherHead].setRespawnCounter(HEAD_RESPAWN_TICKS); - } - } - } - - if (rand.nextFloat() < 0.7F) { - EntityPlayer entityplayer1 = world.getNearestAttackablePlayer(this, f, f); - - if (entityplayer1 != null) { - setAttackTarget(entityplayer1); - numTicksToChaseTarget = 100 + rand.nextInt(20); - } else { - randomYawVelocity = (rand.nextFloat() - 0.5F) * 20F; - } - } - - if (getAttackTarget() != null) { - faceEntity(getAttackTarget(), 10F, getVerticalFaceSpeed()); - - // have any heads not currently attacking switch to the primary target - for (int i = 0; i < numHeads; i++) { - if (!hc[i].isAttacking() && !hc[i].isSecondaryAttacking) { - hc[i].setTargetEntity(getAttackTarget()); - } - } - - // let's pick an attack - if (this.getAttackTarget().isEntityAlive()) { - float distance = this.getAttackTarget().getDistance(this); - - if (this.getEntitySenses().canSee(this.getAttackTarget())) { - this.attackEntity(this.getAttackTarget(), distance); - } - } - - if (numTicksToChaseTarget-- <= 0 || getAttackTarget().isDead || getAttackTarget().getDistanceSq(this) > (double) (f * f)) { - setAttackTarget(null); - } - } else { - if (rand.nextFloat() < 0.05F) { - randomYawVelocity = (rand.nextFloat() - 0.5F) * 20F; - } - - rotationYaw += randomYawVelocity; - rotationPitch = 0; - - // TODO: while we are idle, consider having the heads breathe fire on passive mobs - - // set idle heads to no target - for (int i = 0; i < numHeads; i++) { - if (hc[i].isIdle()) { - hc[i].setTargetEntity(null); - } - } - } - - // heads that are free at this point may consider attacking secondary targets - this.secondaryAttacks(); - } - - private void setDifficultyVariables() { - if (world.getDifficulty() != EnumDifficulty.HARD) { - EntityTFHydra.HEADS_ACTIVITY_FACTOR = 0.3F; - } else { - EntityTFHydra.HEADS_ACTIVITY_FACTOR = 0.5F; // higher is harder - } - } - - // TODO: make random - private int getRandomDeadHead() { - for (int i = 0; i < numHeads; i++) { - if (hc[i].canRespawn()) { - return i; - } - } - return -1; - } - - /** - * Used when re-loading from save. Assumes three heads are active and activates more if necessary. - */ - private void activateNumberOfHeads(int howMany) { - int moreHeads = howMany - this.countActiveHeads(); - - for (int i = 0; i < moreHeads; i++) { - int otherHead = getRandomDeadHead(); - if (otherHead != -1) { - // move directly into not dead - hc[otherHead].setNextState(HydraHeadContainer.State.IDLE); - hc[otherHead].endCurrentAction(); - } - } - } - - /** - * Count timers, and pick an attack against the entity if our timer says go - */ - private void attackEntity(Entity target, float distance) { - - int BITE_CHANCE = 10; - int FLAME_CHANCE = 100; - int MORTAR_CHANCE = 160; - - boolean targetAbove = target.getEntityBoundingBox().minY > this.getEntityBoundingBox().maxY; - - // three main heads can do these kinds of attacks - for (int i = 0; i < 3; i++) { - if (hc[i].isIdle() && !areTooManyHeadsAttacking(i)) { - if (distance > 4 && distance < 10 && rand.nextInt(BITE_CHANCE) == 0 && this.countActiveHeads() > 2 && !areOtherHeadsBiting(i)) { - hc[i].setNextState(HydraHeadContainer.State.BITE_BEGINNING); - } else if (distance > 0 && distance < 20 && rand.nextInt(FLAME_CHANCE) == 0) { - hc[i].setNextState(HydraHeadContainer.State.FLAME_BEGINNING); - } else if (distance > 8 && distance < 32 && !targetAbove && rand.nextInt(MORTAR_CHANCE) == 0) { - hc[i].setNextState(HydraHeadContainer.State.MORTAR_BEGINNING); - } - } - } - - // heads 4-7 can do everything but bite - for (int i = 3; i < numHeads; i++) { - if (hc[i].isIdle() && !areTooManyHeadsAttacking(i)) { - if (distance > 0 && distance < 20 && rand.nextInt(FLAME_CHANCE) == 0) { - hc[i].setNextState(HydraHeadContainer.State.FLAME_BEGINNING); - } else if (distance > 8 && distance < 32 && !targetAbove && rand.nextInt(MORTAR_CHANCE) == 0) { - hc[i].setNextState(HydraHeadContainer.State.MORTAR_BEGINNING); - } - } - } - } - - private boolean areTooManyHeadsAttacking(int testHead) { - int otherAttacks = 0; - - for (int i = 0; i < numHeads; i++) { - if (i != testHead && hc[i].isAttacking()) { - otherAttacks++; - - // biting heads count triple - if (hc[i].isBiting()) { - otherAttacks += 2; - } - } - } - - return otherAttacks >= 1 + (countActiveHeads() * HEADS_ACTIVITY_FACTOR); - } - - private int countActiveHeads() { - int count = 0; - - for (int i = 0; i < numHeads; i++) { - if (hc[i].isActive()) { - count++; - } - } - - return count; - } - - private boolean areOtherHeadsBiting(int testHead) { - for (int i = 0; i < numHeads; i++) { - if (i != testHead && hc[i].isBiting()) { - return true; - } - } - return false; - } - - /** - * Called sometime after the main attackEntity routine. Finds a valid secondary target and has an unoccupied head start an attack against it. - *

- * The center head (head 0) does not make secondary attacks - */ - private void secondaryAttacks() { - for (int i = 0; i < numHeads; i++) { - if (hc[i].headEntity == null) { - return; - } - } - - EntityLivingBase secondaryTarget = findSecondaryTarget(20); - - if (secondaryTarget != null) { - float distance = secondaryTarget.getDistance(this); - - for (int i = 1; i < numHeads; i++) { - if (hc[i].isActive() && hc[i].isIdle() && isTargetOnThisSide(i, secondaryTarget)) { - if (distance > 0 && distance < 20 && rand.nextInt(SECONDARY_FLAME_CHANCE) == 0) { - hc[i].setTargetEntity(secondaryTarget); - hc[i].isSecondaryAttacking = true; - hc[i].setNextState(HydraHeadContainer.State.FLAME_BEGINNING); - } else if (distance > 8 && distance < 32 && rand.nextInt(SECONDARY_MORTAR_CHANCE) == 0) { - hc[i].setTargetEntity(secondaryTarget); - hc[i].isSecondaryAttacking = true; - hc[i].setNextState(HydraHeadContainer.State.MORTAR_BEGINNING); - } - } - } - } - } - - /** - * Used to make sure heads don't attack across the whole body - */ - private boolean isTargetOnThisSide(int headNum, Entity target) { - double headDist = distanceSqXZ(hc[headNum].headEntity, target); - double middleDist = distanceSqXZ(this, target); - return headDist < middleDist; - } - - /** - * Square of distance between two entities with y not a factor, just x and z - */ - private double distanceSqXZ(Entity headEntity, Entity target) { - double distX = headEntity.posX - target.posX; - double distZ = headEntity.posZ - target.posZ; - return distX * distX + distZ * distZ; - } - - @Nullable - private EntityLivingBase findSecondaryTarget(double range) { - return this.world.getEntitiesWithinAABB(EntityLivingBase.class, new AxisAlignedBB(this.posX, this.posY, this.posZ, this.posX + 1, this.posY + 1, this.posZ + 1).grow(range, range, range)) - .stream() - .filter(e -> !(e instanceof EntityTFHydra || e instanceof EntityTFHydraPart)) - .filter(e -> e != getAttackTarget() && !isAnyHeadTargeting(e) && getEntitySenses().canSee(e)) - .min(Comparator.comparingDouble(this::getDistanceSq)).orElse(null); - } - - private boolean isAnyHeadTargeting(Entity targetEntity) { - for (int i = 0; i < numHeads; i++) { - if (hc[i].targetEntity != null && hc[i].targetEntity.equals(targetEntity)) { - return true; - } - } - - return false; - } - - // [VanillaCopy] based on EntityDragon.collideWithEntities - private void collideWithEntities(List entities, Entity part) { - double d0 = (part.getEntityBoundingBox().minX + part.getEntityBoundingBox().maxX) / 2.0D; - double d1 = (part.getEntityBoundingBox().minZ + part.getEntityBoundingBox().maxZ) / 2.0D; - - for (Entity entity : entities) { - if (entity instanceof EntityLivingBase) { - double d2 = entity.posX - d0; - double d3 = entity.posZ - d1; - double d4 = d2 * d2 + d3 * d3; - entity.addVelocity(d2 / d4 * 4.0D, 0.20000000298023224D, d3 / d4 * 4.0D); - } - } - } - - /** - * Check the surface immediately beneath us, if it is less than 80% solid - */ - private boolean isUnsteadySurfaceBeneath() { - int minX = MathHelper.floor(this.getEntityBoundingBox().minX); - int minZ = MathHelper.floor(this.getEntityBoundingBox().minZ); - int maxX = MathHelper.floor(this.getEntityBoundingBox().maxX); - int maxZ = MathHelper.floor(this.getEntityBoundingBox().maxZ); - int minY = MathHelper.floor(this.getEntityBoundingBox().minY); - - int solid = 0; - int total = 0; - - int dy = minY - 1; - - for (int dx = minX; dx <= maxX; ++dx) { - for (int dz = minZ; dz <= maxZ; ++dz) { - total++; - if (this.world.getBlockState(new BlockPos(dx, dy, dz)).getMaterial().isSolid()) { - solid++; - } - } - } - - return ((float) solid / (float) total) < 0.6F; - } - - private void destroyBlocksInAABB(AxisAlignedBB box) { - if (ForgeEventFactory.getMobGriefingEvent(world, this)) { - for (BlockPos pos : WorldUtil.getAllInBB(box)) { - if (EntityUtil.canDestroyBlock(world, pos, this)) { - world.destroyBlock(pos, false); - } - } - } - } - - @Override - public int getVerticalFaceSpeed() { - return 500; - } - - @Override - public boolean attackEntityFromPart(MultiPartEntityPart part, DamageSource source, float damage) { - return calculateRange(source) <= 400 && super.attackEntityFrom(source, Math.round(damage / 8.0F)); - } - - public boolean attackEntityFromPart(EntityTFHydraPart part, DamageSource source, float damage) { - // if we're in a wall, kill that wall - if (!world.isRemote && source == DamageSource.IN_WALL) { - destroyBlocksInAABB(part.getEntityBoundingBox()); - } - - HydraHeadContainer headCon = null; - - for (int i = 0; i < numHeads; i++) { - if (hc[i].headEntity == part) { - headCon = hc[i]; - } - } - - double range = calculateRange(source); - - if (range > 400) { - return false; - } - - // ignore hits on dying heads, it's weird - if (headCon != null && !headCon.isActive()) { - return false; - } - - boolean tookDamage; - if (headCon != null && headCon.getCurrentMouthOpen() > 0.5) { - tookDamage = super.attackEntityFrom(source, damage); - headCon.addDamage(damage); - } else { - int armoredDamage = Math.round(damage / ARMOR_MULTIPLIER); - tookDamage = super.attackEntityFrom(source, armoredDamage); - - if (headCon != null) { - headCon.addDamage(armoredDamage); - } - } - - if (tookDamage) { - this.ticksSinceDamaged = 0; - } - - return tookDamage; - } - - private double calculateRange(DamageSource damagesource) { - return damagesource.getTrueSource() != null ? getDistanceSq(damagesource.getTrueSource()) : -1; - } - - @Override - public boolean attackEntityFrom(DamageSource src, float damage) { - return src == DamageSource.OUT_OF_WORLD && super.attackEntityFrom(src, damage); - } - - /** - * We need to do this for the bounding boxes on the parts to become active - */ - @Override - public Entity[] getParts() { - return partArray; - } - - /** - * This is set as off for the hydra, which has an enormous bounding box, but set as on for the parts. - */ - @Override - public boolean canBeCollidedWith() { - return false; - } - - /** - * If this is on, the player pushes us based on our bounding box rather than it going by parts - */ - @Override - public boolean canBePushed() { - return false; - } - - @Override - protected void collideWithEntity(Entity entity) {} - - @Override - public void knockBack(Entity entity, float strength, double xRatio, double zRatio) {} - - @Override - protected SoundEvent getAmbientSound() { - return TFSounds.HYDRA_GROWL; - } - - @Override - protected SoundEvent getHurtSound(DamageSource source) { - return TFSounds.HYDRA_HURT; - } - - @Override - protected SoundEvent getDeathSound() { - return TFSounds.HYDRA_DEATH; - } - - @Override - protected float getSoundVolume() { - return 2F; - } - - @Override - public void onDeath(DamageSource cause) { - super.onDeath(cause); - // mark the lair as defeated - if (!world.isRemote) { - this.bossInfo.setPercent(0.0F); - TFWorld.markStructureConquered(world, new BlockPos(this), TFFeature.HYDRA_LAIR); - } - } - - @Override - public ResourceLocation getLootTable() { - return LOOT_TABLE; - } - - @Override - protected boolean canDespawn() { - return false; - } - - @Override - public boolean isBurning() { - return false; - } - - @Override - protected void onDeathUpdate() { - ++this.deathTime; - - // stop any head actions on death - if (deathTime == 1) { - for (int i = 0; i < numHeads; i++) { - hc[i].setRespawnCounter(-1); - if (hc[i].isActive()) { - hc[i].setNextState(HydraHeadContainer.State.IDLE); - hc[i].endCurrentAction(); - hc[i].setHurtTime(200); - } - } - } - - // heads die off one by one - if (this.deathTime <= 140 && this.deathTime % 20 == 0) { - int headToDie = (this.deathTime / 20) - 1; - - if (hc[headToDie].isActive()) { - hc[headToDie].setNextState(HydraHeadContainer.State.DYING); - hc[headToDie].endCurrentAction(); - } - } - - if (this.deathTime == 200) { - if (!this.world.isRemote && (this.isPlayer() || this.recentlyHit > 0 && this.canDropLoot() && this.world.getGameRules().getBoolean("doMobLoot"))) { - int i = this.getExperiencePoints(this.attackingPlayer); - i = ForgeEventFactory.getExperienceDrop(this, this.attackingPlayer, i); - while (i > 0) { - int j = EntityXPOrb.getXPSplit(i); - i -= j; - this.world.spawnEntity(new EntityXPOrb(this.world, this.posX, this.posY, this.posZ, j)); - } - } - - this.setDead(); - } - - for (int i = 0; i < 20; ++i) { - double vx = this.rand.nextGaussian() * 0.02D; - double vy = this.rand.nextGaussian() * 0.02D; - double vz = this.rand.nextGaussian() * 0.02D; - EnumParticleTypes particle = rand.nextInt(2) == 0 ? EnumParticleTypes.EXPLOSION_LARGE : EnumParticleTypes.EXPLOSION_NORMAL; - this.world.spawnParticle(particle, - this.posX + this.rand.nextFloat() * this.body.width * 2.0F - this.body.width, - this.posY + this.rand.nextFloat() * this.body.height, - this.posZ + this.rand.nextFloat() * this.body.width * 2.0F - this.body.width, - vx, vy, vz - ); - } - } - - @Override - public World getWorld() { - return this.world; - } - - @Override - public boolean isNonBoss() { - return false; - } + public static final ResourceLocation LOOT_TABLE = TwilightForestMod.prefix("entities/hydra"); + + private static final int TICKS_BEFORE_HEALING = 1000; + private static final int HEAD_RESPAWN_TICKS = 100; + private static final int HEAD_MAX_DAMAGE = 120; + private static final float ARMOR_MULTIPLIER = 8.0F; + private static final int MAX_HEALTH = 360; + private static float HEADS_ACTIVITY_FACTOR = 0.3F; + + private static final int SECONDARY_FLAME_CHANCE = 10; + private static final int SECONDARY_MORTAR_CHANCE = 16; + + private static final DataParameter DATA_SPAWNHEADS = EntityDataManager.createKey(EntityTFHydra.class, DataSerializers.BOOLEAN); + + private final Entity partArray[]; + + public final int numHeads = 7; + public final HydraHeadContainer[] hc = new HydraHeadContainer[numHeads]; + + public final MultiPartEntityPart body = new MultiPartEntityPart(this, "body", 4F, 4F); + private final MultiPartEntityPart leftLeg = new MultiPartEntityPart(this, "leg", 2F, 3F); + private final MultiPartEntityPart rightLeg = new MultiPartEntityPart(this, "leg", 2F, 3F); + private final MultiPartEntityPart tail = new MultiPartEntityPart(this, "tail", 4F, 4F); + private final BossInfoServer bossInfo = new BossInfoServer(getDisplayName(), BossInfo.Color.BLUE, BossInfo.Overlay.PROGRESS); + + private int ticksSinceDamaged = 0; + + public EntityTFHydra(World world) { + super(world); + + List parts = new ArrayList<>(); + parts.add(body); + parts.add(leftLeg); + parts.add(rightLeg); + parts.add(tail); + + for (int i = 0; i < numHeads; i++) { + hc[i] = new HydraHeadContainer(this, i, i < 3); + Collections.addAll(parts, hc[i].getNeckArray()); + } + + partArray = parts.toArray(new Entity[0]); + + this.ignoreFrustumCheck = true; + this.isImmuneToFire = true; + this.experienceValue = 511; + + setSize(16F, 12F); + setSpawnHeads(true); + } + + @Override + public void setCustomNameTag(String name) { + super.setCustomNameTag(name); + this.bossInfo.setName(this.getDisplayName()); + } + + @Override + protected void applyEntityAttributes() { + super.applyEntityAttributes(); + this.getEntityAttribute(SharedMonsterAttributes.MAX_HEALTH).setBaseValue(MAX_HEALTH); + this.getEntityAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).setBaseValue(0.28D); + } + + @Override + public void addTrackingPlayer(EntityPlayerMP player) { + super.addTrackingPlayer(player); + this.bossInfo.addPlayer(player); + } + + @Override + public void removeTrackingPlayer(EntityPlayerMP player) { + super.removeTrackingPlayer(player); + this.bossInfo.removePlayer(player); + } + + @Override + protected void despawnEntity() { + if (world.getDifficulty() == EnumDifficulty.PEACEFUL) { + world.setBlockState(getPosition().add(0, 2, 0), TFBlocks.boss_spawner.getDefaultState().withProperty(BlockTFBossSpawner.VARIANT, BossVariant.HYDRA)); + setDead(); + for (HydraHeadContainer container : hc) { + if (container.headEntity != null) { + container.headEntity.setDead(); + } + } + } else { + super.despawnEntity(); + } + } + + @Override + public boolean isPushedByWater() { + return false; + } + + // [Vanilla Copy] from EntityLivingBase. Hydra doesn't like the one from EntityLiving for whatever reason + @Override + protected float updateDistance(float p_110146_1_, float p_110146_2_) { + float f = MathHelper.wrapDegrees(p_110146_1_ - this.renderYawOffset); + this.renderYawOffset += f * 0.3F; + float f1 = MathHelper.wrapDegrees(this.rotationYaw - this.renderYawOffset); + boolean flag = f1 < -90.0F || f1 >= 90.0F; + + if (f1 < -75.0F) { + f1 = -75.0F; + } + + if (f1 >= 75.0F) { + f1 = 75.0F; + } + + this.renderYawOffset = this.rotationYaw - f1; + + if (f1 * f1 > 2500.0F) { + this.renderYawOffset += f1 * 0.2F; + } + + if (flag) { + p_110146_2_ *= -1.0F; + } + + return p_110146_2_; + } + + @Override + public void onLivingUpdate() { + if (hc[0].headEntity == null || hc[1].headEntity == null || hc[2].headEntity == null) { + // don't spawn if we're connected in multiplayer + if (!world.isRemote && shouldSpawnHeads()) { + for (int i = 0; i < numHeads; i++) { + hc[i].headEntity = new EntityTFHydraHead(this, "head" + i, 3F, 3F); + hc[i].headEntity.setPosition(this.posX, this.posY, this.posZ); + hc[i].setHeadPosition(); + world.spawnEntity(hc[i].headEntity); + } + + setSpawnHeads(false); + } + } + + body.onUpdate(); + + // update all heads (maybe we should change to only active ones + for (int i = 0; i < numHeads; i++) { + hc[i].onUpdate(); + } + + if (this.hurtTime > 0) { + for (int i = 0; i < numHeads; i++) { + hc[i].setHurtTime(this.hurtTime); + } + } + + this.ticksSinceDamaged++; + + if (!this.world.isRemote && this.ticksSinceDamaged > TICKS_BEFORE_HEALING && this.ticksSinceDamaged % 5 == 0) { + this.heal(1); + } + + // update fight variables for difficulty setting + setDifficultyVariables(); + + super.onLivingUpdate(); + + body.width = body.height = 6.0F; + tail.width = 6.0F; + tail.height = 2.0F; + + // set body part positions + float angle; + double dx, dy, dz; + + // body goes behind the actual position of the hydra + angle = (((renderYawOffset + 180) * 3.141593F) / 180F); + + dx = posX - MathHelper.sin(angle) * 3.0; + dy = posY + 0.1; + dz = posZ + MathHelper.cos(angle) * 3.0; + body.setPosition(dx, dy, dz); + + dx = posX - MathHelper.sin(angle) * 10.5; + dy = posY + 0.1; + dz = posZ + MathHelper.cos(angle) * 10.5; + tail.setPosition(dx, dy, dz); + + if (hurtTime == 0) { + this.collideWithEntities(this.world.getEntitiesWithinAABBExcludingEntity(this, this.body.getEntityBoundingBox()), this.body); + this.collideWithEntities(this.world.getEntitiesWithinAABBExcludingEntity(this, this.tail.getEntityBoundingBox()), this.tail); + } + + // destroy blocks + if (!this.world.isRemote) { + this.destroyBlocksInAABB(this.body.getEntityBoundingBox()); + this.destroyBlocksInAABB(this.tail.getEntityBoundingBox()); + + for (int i = 0; i < numHeads; i++) { + if (hc[i].headEntity != null && hc[i].isActive()) { + this.destroyBlocksInAABB(this.hc[i].headEntity.getEntityBoundingBox()); + } + } + + // smash blocks beneath us too + if (this.ticksExisted % 20 == 0) { + if (isUnsteadySurfaceBeneath()) { + this.destroyBlocksInAABB(this.getEntityBoundingBox().offset(0, -1, 0)); + + } + } + + bossInfo.setPercent(getHealth() / getMaxHealth()); + } + } + + @Override + protected void entityInit() { + super.entityInit(); + dataManager.register(DATA_SPAWNHEADS, false); + } + + private boolean shouldSpawnHeads() { + return dataManager.get(DATA_SPAWNHEADS); + } + + private void setSpawnHeads(boolean flag) { + dataManager.set(DATA_SPAWNHEADS, flag); + } + + @Override + public void writeEntityToNBT(NBTTagCompound compound) { + super.writeEntityToNBT(compound); + compound.setBoolean("SpawnHeads", shouldSpawnHeads()); + compound.setByte("NumHeads", (byte) countActiveHeads()); + } + + @Override + public void readEntityFromNBT(NBTTagCompound compound) { + super.readEntityFromNBT(compound); + setSpawnHeads(compound.getBoolean("SpawnHeads")); + activateNumberOfHeads(compound.getByte("NumHeads")); + if (this.hasCustomName()) { + this.bossInfo.setName(this.getDisplayName()); + } + } + + + // TODO modernize this more (old AI copypasta still kind of here) + private int numTicksToChaseTarget; + + @Override + protected void updateAITasks() { + moveStrafing = 0.0F; + moveForward = 0.0F; + float f = 48F; + + // kill heads that have taken too much damage + for (int i = 0; i < numHeads; i++) { + if (hc[i].isActive() && hc[i].getDamageTaken() > HEAD_MAX_DAMAGE) { + hc[i].setNextState(HydraHeadContainer.State.DYING); + hc[i].endCurrentAction(); + + // set this head and a random dead head to respawn + hc[i].setRespawnCounter(HEAD_RESPAWN_TICKS); + int otherHead = getRandomDeadHead(); + if (otherHead != -1) { + hc[otherHead].setRespawnCounter(HEAD_RESPAWN_TICKS); + } + } + } + + if (rand.nextFloat() < 0.7F) { + EntityPlayer entityplayer1 = world.getNearestAttackablePlayer(this, f, f); + + if (entityplayer1 != null) { + setAttackTarget(entityplayer1); + numTicksToChaseTarget = 100 + rand.nextInt(20); + } else { + randomYawVelocity = (rand.nextFloat() - 0.5F) * 20F; + } + } + + if (getAttackTarget() != null) { + faceEntity(getAttackTarget(), 10F, getVerticalFaceSpeed()); + + // have any heads not currently attacking switch to the primary target + for (int i = 0; i < numHeads; i++) { + if (!hc[i].isAttacking() && !hc[i].isSecondaryAttacking) { + hc[i].setTargetEntity(getAttackTarget()); + } + } + + // let's pick an attack + if (this.getAttackTarget().isEntityAlive()) { + float distance = this.getAttackTarget().getDistance(this); + + if (this.getEntitySenses().canSee(this.getAttackTarget())) { + this.attackEntity(this.getAttackTarget(), distance); + } + } + + if (numTicksToChaseTarget-- <= 0 || getAttackTarget().isDead || getAttackTarget().getDistanceSq(this) > (double) (f * f)) { + setAttackTarget(null); + } + } else { + if (rand.nextFloat() < 0.05F) { + randomYawVelocity = (rand.nextFloat() - 0.5F) * 20F; + } + + rotationYaw += randomYawVelocity; + rotationPitch = 0; + + // TODO: while we are idle, consider having the heads breathe fire on passive mobs + + // set idle heads to no target + for (int i = 0; i < numHeads; i++) { + if (hc[i].isIdle()) { + hc[i].setTargetEntity(null); + } + } + } + + // heads that are free at this point may consider attacking secondary targets + this.secondaryAttacks(); + } + + private void setDifficultyVariables() { + if (world.getDifficulty() != EnumDifficulty.HARD) { + EntityTFHydra.HEADS_ACTIVITY_FACTOR = 0.3F; + } else { + EntityTFHydra.HEADS_ACTIVITY_FACTOR = 0.5F; // higher is harder + } + } + + // TODO: make random + private int getRandomDeadHead() { + for (int i = 0; i < numHeads; i++) { + if (hc[i].canRespawn()) { + return i; + } + } + return -1; + } + + /** + * Used when re-loading from save. Assumes three heads are active and activates more if necessary. + */ + private void activateNumberOfHeads(int howMany) { + int moreHeads = howMany - this.countActiveHeads(); + + for (int i = 0; i < moreHeads; i++) { + int otherHead = getRandomDeadHead(); + if (otherHead != -1) { + // move directly into not dead + hc[otherHead].setNextState(HydraHeadContainer.State.IDLE); + hc[otherHead].endCurrentAction(); + } + } + } + + /** + * Count timers, and pick an attack against the entity if our timer says go + */ + private void attackEntity(Entity target, float distance) { + + int BITE_CHANCE = 10; + int FLAME_CHANCE = 100; + int MORTAR_CHANCE = 160; + + boolean targetAbove = target.getEntityBoundingBox().minY > this.getEntityBoundingBox().maxY; + + // three main heads can do these kinds of attacks + for (int i = 0; i < 3; i++) { + if (hc[i].isIdle() && !areTooManyHeadsAttacking(i)) { + if (distance > 4 && distance < 10 && rand.nextInt(BITE_CHANCE) == 0 && this.countActiveHeads() > 2 && !areOtherHeadsBiting(i)) { + hc[i].setNextState(HydraHeadContainer.State.BITE_BEGINNING); + } else if (distance > 0 && distance < 20 && rand.nextInt(FLAME_CHANCE) == 0) { + hc[i].setNextState(HydraHeadContainer.State.FLAME_BEGINNING); + } else if (distance > 8 && distance < 32 && !targetAbove && rand.nextInt(MORTAR_CHANCE) == 0) { + hc[i].setNextState(HydraHeadContainer.State.MORTAR_BEGINNING); + } + } + } + + // heads 4-7 can do everything but bite + for (int i = 3; i < numHeads; i++) { + if (hc[i].isIdle() && !areTooManyHeadsAttacking(i)) { + if (distance > 0 && distance < 20 && rand.nextInt(FLAME_CHANCE) == 0) { + hc[i].setNextState(HydraHeadContainer.State.FLAME_BEGINNING); + } else if (distance > 8 && distance < 32 && !targetAbove && rand.nextInt(MORTAR_CHANCE) == 0) { + hc[i].setNextState(HydraHeadContainer.State.MORTAR_BEGINNING); + } + } + } + } + + private boolean areTooManyHeadsAttacking(int testHead) { + int otherAttacks = 0; + + for (int i = 0; i < numHeads; i++) { + if (i != testHead && hc[i].isAttacking()) { + otherAttacks++; + + // biting heads count triple + if (hc[i].isBiting()) { + otherAttacks += 2; + } + } + } + + return otherAttacks >= 1 + (countActiveHeads() * HEADS_ACTIVITY_FACTOR); + } + + private int countActiveHeads() { + int count = 0; + + for (int i = 0; i < numHeads; i++) { + if (hc[i].isActive()) { + count++; + } + } + + return count; + } + + private boolean areOtherHeadsBiting(int testHead) { + for (int i = 0; i < numHeads; i++) { + if (i != testHead && hc[i].isBiting()) { + return true; + } + } + return false; + } + + /** + * Called sometime after the main attackEntity routine. Finds a valid secondary target and has an unoccupied head start an attack against it. + *

+ * The center head (head 0) does not make secondary attacks + */ + private void secondaryAttacks() { + for (int i = 0; i < numHeads; i++) { + if (hc[i].headEntity == null) { + return; + } + } + + EntityLivingBase secondaryTarget = findSecondaryTarget(20); + + if (secondaryTarget != null) { + float distance = secondaryTarget.getDistance(this); + + for (int i = 1; i < numHeads; i++) { + if (hc[i].isActive() && hc[i].isIdle() && isTargetOnThisSide(i, secondaryTarget)) { + if (distance > 0 && distance < 20 && rand.nextInt(SECONDARY_FLAME_CHANCE) == 0) { + hc[i].setTargetEntity(secondaryTarget); + hc[i].isSecondaryAttacking = true; + hc[i].setNextState(HydraHeadContainer.State.FLAME_BEGINNING); + } else if (distance > 8 && distance < 32 && rand.nextInt(SECONDARY_MORTAR_CHANCE) == 0) { + hc[i].setTargetEntity(secondaryTarget); + hc[i].isSecondaryAttacking = true; + hc[i].setNextState(HydraHeadContainer.State.MORTAR_BEGINNING); + } + } + } + } + } + + /** + * Used to make sure heads don't attack across the whole body + */ + private boolean isTargetOnThisSide(int headNum, Entity target) { + double headDist = distanceSqXZ(hc[headNum].headEntity, target); + double middleDist = distanceSqXZ(this, target); + return headDist < middleDist; + } + + /** + * Square of distance between two entities with y not a factor, just x and z + */ + private double distanceSqXZ(Entity headEntity, Entity target) { + double distX = headEntity.posX - target.posX; + double distZ = headEntity.posZ - target.posZ; + return distX * distX + distZ * distZ; + } + + @Nullable + private EntityLivingBase findSecondaryTarget(double range) { + return this.world.getEntitiesWithinAABB(EntityLivingBase.class, new AxisAlignedBB(this.posX, this.posY, this.posZ, this.posX + 1, this.posY + 1, this.posZ + 1).grow(range, range, range)) + .stream() + .filter(e -> !(e instanceof EntityTFHydra || e instanceof EntityTFHydraPart)) + .filter(e -> e != getAttackTarget() && !isAnyHeadTargeting(e) && getEntitySenses().canSee(e)) + .min(Comparator.comparingDouble(this::getDistanceSq)).orElse(null); + } + + private boolean isAnyHeadTargeting(Entity targetEntity) { + for (int i = 0; i < numHeads; i++) { + if (hc[i].targetEntity != null && hc[i].targetEntity.equals(targetEntity)) { + return true; + } + } + + return false; + } + + // [VanillaCopy] based on EntityDragon.collideWithEntities + private void collideWithEntities(List entities, Entity part) { + double d0 = (part.getEntityBoundingBox().minX + part.getEntityBoundingBox().maxX) / 2.0D; + double d1 = (part.getEntityBoundingBox().minZ + part.getEntityBoundingBox().maxZ) / 2.0D; + + for (Entity entity : entities) { + if (entity instanceof EntityLivingBase) { + double d2 = entity.posX - d0; + double d3 = entity.posZ - d1; + double d4 = d2 * d2 + d3 * d3; + entity.addVelocity(d2 / d4 * 4.0D, 0.20000000298023224D, d3 / d4 * 4.0D); + } + } + } + + /** + * Check the surface immediately beneath us, if it is less than 80% solid + */ + private boolean isUnsteadySurfaceBeneath() { + int minX = MathHelper.floor(this.getEntityBoundingBox().minX); + int minZ = MathHelper.floor(this.getEntityBoundingBox().minZ); + int maxX = MathHelper.floor(this.getEntityBoundingBox().maxX); + int maxZ = MathHelper.floor(this.getEntityBoundingBox().maxZ); + int minY = MathHelper.floor(this.getEntityBoundingBox().minY); + + int solid = 0; + int total = 0; + + int dy = minY - 1; + + for (int dx = minX; dx <= maxX; ++dx) { + for (int dz = minZ; dz <= maxZ; ++dz) { + total++; + if (this.world.getBlockState(new BlockPos(dx, dy, dz)).getMaterial().isSolid()) { + solid++; + } + } + } + + return ((float) solid / (float) total) < 0.6F; + } + + private void destroyBlocksInAABB(AxisAlignedBB box) { + if (ForgeEventFactory.getMobGriefingEvent(world, this)) { + for (BlockPos pos : WorldUtil.getAllInBB(box)) { + if (EntityUtil.canDestroyBlock(world, pos, this)) { + world.destroyBlock(pos, false); + } + } + } + } + + @Override + public int getVerticalFaceSpeed() { + return 500; + } + + @Override + public boolean attackEntityFromPart(MultiPartEntityPart part, DamageSource source, float damage) { + return calculateRange(source) <= 400 && super.attackEntityFrom(source, Math.round(damage / 8.0F)); + } + + public boolean attackEntityFromPart(EntityTFHydraPart part, DamageSource source, float damage) { + // if we're in a wall, kill that wall + if (!world.isRemote && source == DamageSource.IN_WALL) { + destroyBlocksInAABB(part.getEntityBoundingBox()); + } + + HydraHeadContainer headCon = null; + + for (int i = 0; i < numHeads; i++) { + if (hc[i].headEntity == part) { + headCon = hc[i]; + } + } + + double range = calculateRange(source); + + if (range > 400) { + return false; + } + + // ignore hits on dying heads, it's weird + if (headCon != null && !headCon.isActive()) { + return false; + } + + boolean tookDamage; + if (headCon != null && headCon.getCurrentMouthOpen() > 0.5) { + tookDamage = super.attackEntityFrom(source, damage); + headCon.addDamage(damage); + } else { + int armoredDamage = Math.round(damage / ARMOR_MULTIPLIER); + tookDamage = super.attackEntityFrom(source, armoredDamage); + + if (headCon != null) { + headCon.addDamage(armoredDamage); + } + } + + if (tookDamage) { + this.ticksSinceDamaged = 0; + } + + return tookDamage; + } + + private double calculateRange(DamageSource damagesource) { + return damagesource.getTrueSource() != null ? getDistanceSq(damagesource.getTrueSource()) : -1; + } + + @Override + public boolean attackEntityFrom(DamageSource src, float damage) { + return src == DamageSource.OUT_OF_WORLD && super.attackEntityFrom(src, damage); + } + + /** + * We need to do this for the bounding boxes on the parts to become active + */ + @Override + public Entity[] getParts() { + return partArray; + } + + /** + * This is set as off for the hydra, which has an enormous bounding box, but set as on for the parts. + */ + @Override + public boolean canBeCollidedWith() { + return false; + } + + /** + * If this is on, the player pushes us based on our bounding box rather than it going by parts + */ + @Override + public boolean canBePushed() { + return false; + } + + @Override + protected void collideWithEntity(Entity entity) { + } + + @Override + public void knockBack(Entity entity, float strength, double xRatio, double zRatio) { + } + + @Override + protected SoundEvent getAmbientSound() { + return TFSounds.HYDRA_GROWL; + } + + @Override + protected SoundEvent getHurtSound(DamageSource source) { + return TFSounds.HYDRA_HURT; + } + + @Override + protected SoundEvent getDeathSound() { + return TFSounds.HYDRA_DEATH; + } + + @Override + protected float getSoundVolume() { + return 2F; + } + + @Override + public void onDeath(DamageSource cause) { + super.onDeath(cause); + // mark the lair as defeated + if (!world.isRemote) { + this.bossInfo.setPercent(0.0F); + TFWorld.markStructureConquered(world, new BlockPos(this), TFFeature.HYDRA_LAIR); + } + } + + @Override + public ResourceLocation getLootTable() { + return LOOT_TABLE; + } + + @Override + protected boolean canDespawn() { + return false; + } + + @Override + public boolean isBurning() { + return false; + } + + @Override + protected boolean canBeRidden(Entity entity) { + return false; + } + + @Override + protected void onDeathUpdate() { + ++this.deathTime; + + // stop any head actions on death + if (deathTime == 1) { + for (int i = 0; i < numHeads; i++) { + hc[i].setRespawnCounter(-1); + if (hc[i].isActive()) { + hc[i].setNextState(HydraHeadContainer.State.IDLE); + hc[i].endCurrentAction(); + hc[i].setHurtTime(200); + } + } + } + + // heads die off one by one + if (this.deathTime <= 140 && this.deathTime % 20 == 0) { + int headToDie = (this.deathTime / 20) - 1; + + if (hc[headToDie].isActive()) { + hc[headToDie].setNextState(HydraHeadContainer.State.DYING); + hc[headToDie].endCurrentAction(); + } + } + + if (this.deathTime == 200) { + if (!this.world.isRemote && (this.isPlayer() || this.recentlyHit > 0 && this.canDropLoot() && this.world.getGameRules().getBoolean("doMobLoot"))) { + int i = this.getExperiencePoints(this.attackingPlayer); + i = ForgeEventFactory.getExperienceDrop(this, this.attackingPlayer, i); + while (i > 0) { + int j = EntityXPOrb.getXPSplit(i); + i -= j; + this.world.spawnEntity(new EntityXPOrb(this.world, this.posX, this.posY, this.posZ, j)); + } + } + + this.setDead(); + } + + for (int i = 0; i < 20; ++i) { + double vx = this.rand.nextGaussian() * 0.02D; + double vy = this.rand.nextGaussian() * 0.02D; + double vz = this.rand.nextGaussian() * 0.02D; + EnumParticleTypes particle = rand.nextInt(2) == 0 ? EnumParticleTypes.EXPLOSION_LARGE : EnumParticleTypes.EXPLOSION_NORMAL; + this.world.spawnParticle(particle, + this.posX + this.rand.nextFloat() * this.body.width * 2.0F - this.body.width, + this.posY + this.rand.nextFloat() * this.body.height, + this.posZ + this.rand.nextFloat() * this.body.width * 2.0F - this.body.width, + vx, vy, vz + ); + } + } + + @Override + public World getWorld() { + return this.world; + } + + @Override + public boolean isNonBoss() { + return false; + } } diff --git a/src/main/java/twilightforest/entity/boss/EntityTFKnightPhantom.java b/src/main/java/twilightforest/entity/boss/EntityTFKnightPhantom.java index 8c81943e44..96670fdcd5 100644 --- a/src/main/java/twilightforest/entity/boss/EntityTFKnightPhantom.java +++ b/src/main/java/twilightforest/entity/boss/EntityTFKnightPhantom.java @@ -155,6 +155,16 @@ protected boolean canDespawn() { return false; } + @Override + public boolean isPushedByWater() { + return false; + } + + @Override + protected boolean canBeRidden(Entity entity) { + return false; + } + @Override public boolean isEntityInvulnerable(DamageSource src) { return src == DamageSource.IN_WALL || super.isEntityInvulnerable(src); diff --git a/src/main/java/twilightforest/entity/boss/EntityTFLich.java b/src/main/java/twilightforest/entity/boss/EntityTFLich.java index b2cd103b43..c2dbed6d76 100644 --- a/src/main/java/twilightforest/entity/boss/EntityTFLich.java +++ b/src/main/java/twilightforest/entity/boss/EntityTFLich.java @@ -175,6 +175,11 @@ protected boolean canDespawn() { return false; } + @Override + public boolean isPushedByWater() { + return false; + } + @Override protected void despawnEntity() { if (world.getDifficulty() == EnumDifficulty.PEACEFUL && !isShadowClone()) { diff --git a/src/main/java/twilightforest/entity/boss/EntityTFMinoshroom.java b/src/main/java/twilightforest/entity/boss/EntityTFMinoshroom.java index 5ea88738b9..064848613a 100644 --- a/src/main/java/twilightforest/entity/boss/EntityTFMinoshroom.java +++ b/src/main/java/twilightforest/entity/boss/EntityTFMinoshroom.java @@ -2,6 +2,7 @@ import net.minecraft.block.Block; import net.minecraft.block.state.IBlockState; +import net.minecraft.entity.Entity; import net.minecraft.entity.SharedMonsterAttributes; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.inventory.EntityEquipmentSlot; @@ -162,6 +163,16 @@ protected boolean canDespawn() { return false; } + @Override + public boolean isPushedByWater() { + return false; + } + + @Override + protected boolean canBeRidden(Entity entity) { + return false; + } + @Override protected void despawnEntity() { if (world.getDifficulty() == EnumDifficulty.PEACEFUL) { diff --git a/src/main/java/twilightforest/entity/boss/EntityTFNaga.java b/src/main/java/twilightforest/entity/boss/EntityTFNaga.java index cf4589d5f8..3dbeacf224 100644 --- a/src/main/java/twilightforest/entity/boss/EntityTFNaga.java +++ b/src/main/java/twilightforest/entity/boss/EntityTFNaga.java @@ -53,825 +53,839 @@ public class EntityTFNaga extends EntityMob implements IEntityMultiPart { - public static final ResourceLocation LOOT_TABLE = TwilightForestMod.prefix("entities/naga"); - - private static final int TICKS_BEFORE_HEALING = 600; - private static final int MAX_SEGMENTS = 12; - private static final int LEASH_X = 46; - private static final int LEASH_Y = 7; - private static final int LEASH_Z = 46; - private static final double DEFAULT_SPEED = 0.3; - - private int currentSegmentCount = 0; // not including head - private final float healthPerSegment; - private final EntityTFNagaSegment[] bodySegments = new EntityTFNagaSegment[MAX_SEGMENTS]; - private AIMovementPattern movementAI; - private int ticksSinceDamaged = 0; - - private final BossInfoServer bossInfo = new BossInfoServer(getDisplayName(), BossInfo.Color.GREEN, BossInfo.Overlay.NOTCHED_10); - - private final AttributeModifier slowSpeed = new AttributeModifier("Naga Slow Speed", 0.25F, 0).setSaved(false); - private final AttributeModifier fastSpeed = new AttributeModifier("Naga Fast Speed", 0.50F, 0).setSaved(false); - - private static final DataParameter DATA_DAZE = EntityDataManager.createKey(EntityTFNaga.class, DataSerializers.BOOLEAN); - - public EntityTFNaga(World world) { - super(world); - this.setSize(1.75f, 3.0f); - this.stepHeight = 2; - this.healthPerSegment = getMaxHealth() / 10; - this.experienceValue = 217; - this.ignoreFrustumCheck = true; - - for (int i = 0; i < bodySegments.length; i++) { - bodySegments[i] = new EntityTFNagaSegment(this, i); - } - - this.goNormal(); - } - - @Override - protected void entityInit() { - super.entityInit(); - dataManager.register(DATA_DAZE, false); - } - - public boolean isDazed() { - return dataManager.get(DATA_DAZE); - } - - protected void setDazed(boolean daze) { - dataManager.set(DATA_DAZE, daze); - } - - private float getMaxHealthPerDifficulty() { - switch (world.getDifficulty()) { - case EASY: - return 120; - default: - case NORMAL: - return 200; - case HARD: - return 250; - } - } - - @Override - public void setCustomNameTag(String name) { - super.setCustomNameTag(name); - this.bossInfo.setName(this.getDisplayName()); - } - - @Override - protected boolean canDespawn() { - return false; - } - - @Override - protected void initEntityAI() { - this.tasks.addTask(1, new EntityAISwimming(this)); - this.tasks.addTask(2, new AIAttack(this)); - this.tasks.addTask(3, new AISmash(this)); - this.tasks.addTask(4, movementAI = new AIMovementPattern(this)); - this.tasks.addTask(8, new EntityAIWander(this, 1, 1) { - @Override - public void startExecuting() { - EntityTFNaga.this.goNormal(); - super.startExecuting(); - } - @Override - protected Vec3d getPosition() - { - return RandomPositionGenerator.findRandomTarget(this.entity, 30, 7); - } - }); - this.targetTasks.addTask(1, new EntityAIHurtByTarget(this, false)); - this.targetTasks.addTask(2, new EntityAINearestAttackableTarget<>(this, EntityPlayer.class, false)); - - this.moveHelper = new NagaMoveHelper(this); - } - - // Similar to EntityAIAttackMelee but simpler (no pathfinding) - static class AIAttack extends EntityAIBase { - - private final EntityTFNaga taskOwner; - private int attackTick = 20; - - AIAttack(EntityTFNaga taskOwner) { - this.taskOwner = taskOwner; - } - - @Override - public boolean shouldExecute() { - EntityLivingBase target = taskOwner.getAttackTarget(); - - return target != null - && target.getEntityBoundingBox().maxY > taskOwner.getEntityBoundingBox().minY - 2.5 - && target.getEntityBoundingBox().minY < taskOwner.getEntityBoundingBox().maxY + 2.5 - && taskOwner.getDistanceSq(target) <= 4.0D - && taskOwner.getEntitySenses().canSee(target); - - } - - @Override - public void updateTask() { - if (attackTick > 0) { - attackTick--; - } - } - - @Override - public void resetTask() { - attackTick = 20; - } - - @Override - public void startExecuting() { - taskOwner.attackEntityAsMob(taskOwner.getAttackTarget()); - attackTick = 20; - } - } - - static class AISmash extends EntityAIBase { - - private final EntityTFNaga taskOwner; - - AISmash(EntityTFNaga taskOwner) { - this.taskOwner = taskOwner; - } - - @Override - public boolean shouldExecute() { - return /*taskOwner.getAttackTarget() != null &&*/ taskOwner.collidedHorizontally && ForgeEventFactory.getMobGriefingEvent(taskOwner.world, taskOwner); - } - - @Override - public void startExecuting() { - // NAGA SMASH! - if (taskOwner.world.isRemote) return; - - AxisAlignedBB bb = taskOwner.getEntityBoundingBox(); - - int minx = MathHelper.floor(bb.minX - 0.75D); - int miny = MathHelper.floor(bb.minY + 1.01D); - int minz = MathHelper.floor(bb.minZ - 0.75D); - int maxx = MathHelper.floor(bb.maxX + 0.75D); - int maxy = MathHelper.floor(bb.maxY + 0.0D); - int maxz = MathHelper.floor(bb.maxZ + 0.75D); - - BlockPos min = new BlockPos(minx, miny, minz); - BlockPos max = new BlockPos(maxx, maxy, maxz); - - if (taskOwner.world.isAreaLoaded(min, max)) { - for (BlockPos pos : BlockPos.getAllInBox(min, max)) { - if (EntityUtil.canDestroyBlock(taskOwner.world, pos, taskOwner)) { - taskOwner.world.destroyBlock(pos, true); - } - } - } - } - } - - enum MovementState { - INTIMIDATE, - CRUMBLE, - CHARGE, - CIRCLE, - DAZE - } - - static class AIMovementPattern extends EntityAIBase { - - private final EntityTFNaga taskOwner; - private MovementState movementState; - private int stateCounter; - private boolean clockwise; - - AIMovementPattern(EntityTFNaga taskOwner) { - this.taskOwner = taskOwner; - setMutexBits(3); - resetTask(); - } - - @Override - public boolean shouldExecute() { - return taskOwner.getAttackTarget() != null; - } - - @Override - public void resetTask() { - movementState = MovementState.CIRCLE; - stateCounter = 15; - clockwise = false; - } - - @Override - public void updateTask() { - if (!taskOwner.getNavigator().noPath()) { - // If we still have an uncompleted path don't run yet - // This isn't in shouldExecute/shouldContinueExecuting because we don't want to reset the task - // todo 1.10 there's a better way to do this I think - taskOwner.setDazed(false); // Since we have a path, we shouldn't be dazed anymore. - return; - } - - switch (movementState) { - case INTIMIDATE: { - taskOwner.getNavigator().clearPath(); - taskOwner.getLookHelper().setLookPositionWithEntity(taskOwner.getAttackTarget(), 30F, 30F); - taskOwner.faceEntity(taskOwner.getAttackTarget(), 30F, 30F); - taskOwner.moveForward = 0.1f; - break; - } - case CRUMBLE: { - taskOwner.getNavigator().clearPath(); - taskOwner.crumbleBelowTarget(2); - taskOwner.crumbleBelowTarget(3); - break; - } - case CHARGE: { - BlockPos tpoint = taskOwner.findCirclePoint(clockwise, 14, Math.PI); - taskOwner.getNavigator().tryMoveToXYZ(tpoint.getX(), tpoint.getY(), tpoint.getZ(), 1); // todo 1.10 check speed - break; - } - case CIRCLE: { - // normal radius is 13 - double radius = stateCounter % 2 == 0 ? 12.0 : 14.0; - double rotation = 1; // in radians - - // hook out slightly before circling - if (stateCounter > 1 && stateCounter < 3) { - radius = 16; - } - - // head almost straight at the player at the end - if (stateCounter == 1) { - rotation = 0.1; - } - - BlockPos tpoint = taskOwner.findCirclePoint(clockwise, radius, rotation); - taskOwner.getNavigator().tryMoveToXYZ(tpoint.getX(), tpoint.getY(), tpoint.getZ(), 1); // todo 1.10 check speed - break; - } - case DAZE: { - taskOwner.setDazed(true); - break; - } - } - - stateCounter--; - if (stateCounter <= 0) { - transitionState(); - } - } - - private void transitionState() { - taskOwner.setDazed(false); - switch (movementState) { - case INTIMIDATE: { - clockwise = !clockwise; - - if (taskOwner.getAttackTarget().getEntityBoundingBox().minY > taskOwner.getEntityBoundingBox().maxY) { - doCrumblePlayer(); - } else { - doCharge(); - } - - break; - } - case CRUMBLE: - doCharge(); - break; - case CHARGE: - doCircle(); - break; - case CIRCLE: - doIntimidate(); - break; - case DAZE: - doCircle(); - break; - } - } - - private void doDaze() { - movementState = MovementState.DAZE; - taskOwner.getNavigator().clearPath(); - stateCounter = 60 + taskOwner.rand.nextInt(40); - } - - private void doCircle() { - movementState = MovementState.CIRCLE; - stateCounter += 10 + taskOwner.rand.nextInt(10); - taskOwner.goNormal(); - } - - private void doCrumblePlayer() { - movementState = MovementState.CRUMBLE; - stateCounter = 20 + taskOwner.rand.nextInt(20); - taskOwner.goSlow(); - } - - /** - * Charge the player. Although the count is 3, we actually charge only 2 times. - */ - private void doCharge() { - movementState = MovementState.CHARGE; - stateCounter = 3; - taskOwner.goFast(); - } - - private void doIntimidate() { - movementState = MovementState.INTIMIDATE; - taskOwner.playSound(TFSounds.NAGA_RATTLE, taskOwner.getSoundVolume() * 4F, taskOwner.getSoundPitch()); - - stateCounter += 15 + taskOwner.rand.nextInt(10); - taskOwner.goSlow(); - } - } - - @Override - public void onLivingUpdate() { - - super.onLivingUpdate(); - - if (world.isRemote || !ForgeEventFactory.getMobGriefingEvent(world, this)) return; - - AxisAlignedBB bb = this.getEntityBoundingBox(); - - int minx = MathHelper.floor(bb.minX - 0.75D); - int miny = MathHelper.floor(bb.minY + 1.01D); - int minz = MathHelper.floor(bb.minZ - 0.75D); - int maxx = MathHelper.floor(bb.maxX + 0.75D); - int maxy = MathHelper.floor(bb.maxY + 0.0D); - int maxz = MathHelper.floor(bb.maxZ + 0.75D); - - BlockPos min = new BlockPos(minx, miny, minz); - BlockPos max = new BlockPos(maxx, maxy, maxz); - - if (world.isAreaLoaded(min, max)) { - for (BlockPos pos : BlockPos.getAllInBox(min, max)) { - IBlockState state = world.getBlockState(pos); - if (state.getMaterial() == Material.LEAVES && EntityUtil.canDestroyBlock(world, pos, state, this)) { - world.destroyBlock(pos, true); - } - } - } - } - - @Override - protected void applyEntityAttributes() { - super.applyEntityAttributes(); - this.getEntityAttribute(SharedMonsterAttributes.MAX_HEALTH).setBaseValue(getMaxHealthPerDifficulty()); - this.getEntityAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).setBaseValue(DEFAULT_SPEED); - this.getEntityAttribute(SharedMonsterAttributes.ATTACK_DAMAGE).setBaseValue(5.0D); - this.getEntityAttribute(SharedMonsterAttributes.FOLLOW_RANGE).setBaseValue(80.0D); - } - - /** - * Determine how many segments, from 2-12, the naga should have, dependent on its current health - */ - private void setSegmentsPerHealth() { - int oldSegments = this.currentSegmentCount; - int newSegments = MathHelper.clamp((int) ((this.getHealth() / healthPerSegment) + (getHealth() > 0 ? 2 : 0)), 0, MAX_SEGMENTS); - this.currentSegmentCount = newSegments; - if (newSegments < oldSegments) { - for (int i = newSegments; i < oldSegments; i++) { - bodySegments[i].selfDestruct(); - } - } else if (newSegments > oldSegments) { - this.activateBodySegments(); - } - - if (!world.isRemote) { - double newSpeed = DEFAULT_SPEED - newSegments * (-0.2F / 12F); - if (newSpeed < 0) - newSpeed = 0; - this.getEntityAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).setBaseValue(newSpeed); - } - } - - @Override - public boolean canTriggerWalking() { - return false; - } - - @Override - public boolean isInLava() { - return false; - } - - @Override - public void onUpdate() { - if (deathTime > 0) { - for (int k = 0; k < 5; k++) { - double d = rand.nextGaussian() * 0.02D; - double d1 = rand.nextGaussian() * 0.02D; - double d2 = rand.nextGaussian() * 0.02D; - EnumParticleTypes explosionType = rand.nextBoolean() ? EnumParticleTypes.EXPLOSION_HUGE : EnumParticleTypes.EXPLOSION_NORMAL; - - world.spawnParticle(explosionType, (posX + rand.nextFloat() * width * 2.0F) - width, posY + rand.nextFloat() * height, (posZ + rand.nextFloat() * width * 2.0F) - width, d, d1, d2); - } - } - - // update health - this.ticksSinceDamaged++; - - if (!this.world.isRemote && this.ticksSinceDamaged > TICKS_BEFORE_HEALING && this.ticksSinceDamaged % 20 == 0) { - this.heal(1); - } - - setSegmentsPerHealth(); - - super.onUpdate(); - - // update bodySegments parts - for (EntityTFNagaSegment segment : bodySegments) { - this.world.updateEntityWithOptionalForce(segment, true); - } - - moveSegments(); - } - - @Override - protected void updateAITasks() { - super.updateAITasks(); - - if (getAttackTarget() != null && (getDistanceSq(getAttackTarget()) > 80 * 80 || !this.isEntityWithinHomeArea(getAttackTarget()))) { - setAttackTarget(null); - } - - // if we are very close to the path point, go to the next point, unless the path is finished - // TODO 1.10 this runs after the path navigator runs, is that okay? - double d = width * 4.0F; - Vec3d vec3d = hasPath() ? getNavigator().getPath().getPosition(this) : null; - - while (vec3d != null && vec3d.squareDistanceTo(posX, vec3d.y, posZ) < d * d) { - getNavigator().getPath().incrementPathIndex(); - - if (getNavigator().getPath().isFinished()) { - vec3d = null; - } else { - vec3d = getNavigator().getPath().getPosition(this); - } - } - - if (!isWithinHomeDistanceCurrentPosition()) { - setAttackTarget(null); - getNavigator().setPath(getNavigator().getPathToPos(getHomePosition()), 1.0F); - } - - // BOSS BAR! - this.bossInfo.setPercent(this.getHealth() / this.getMaxHealth()); - } - - static class NagaMoveHelper extends EntityMoveHelper { - - public NagaMoveHelper(EntityLiving naga) { - super(naga); - } - - @Override - public void onUpdateMoveHelper() { - // TF - slither! - MovementState currentState = ((EntityTFNaga) entity).movementAI.movementState; - if(currentState == MovementState.DAZE) { - this.entity.moveStrafing = 0F; - } else if (currentState != MovementState.CHARGE && currentState != MovementState.INTIMIDATE) { - this.entity.moveStrafing = MathHelper.cos(this.entity.ticksExisted * 0.3F) * 0.6F; - } else { - this.entity.moveStrafing *= 0.8F; - } - - super.onUpdateMoveHelper(); - } - } - - @Override - protected SoundEvent getAmbientSound() { - return TFSounds.NAGA_HISS; - } - - @Override - protected SoundEvent getHurtSound(DamageSource source) { - return TFSounds.NAGA_HURT; - } - - @Override - protected SoundEvent getDeathSound() { - return TFSounds.NAGA_HURT; - } - - @Override - public ResourceLocation getLootTable() { - return LOOT_TABLE; - } - - private void crumbleBelowTarget(int range) { - if (!ForgeEventFactory.getMobGriefingEvent(world, this)) return; - - int floor = (int) getEntityBoundingBox().minY; - int targetY = (int) getAttackTarget().getEntityBoundingBox().minY; - - if (targetY > floor) { - int dx = (int) getAttackTarget().posX + rand.nextInt(range) - rand.nextInt(range); - int dz = (int) getAttackTarget().posZ + rand.nextInt(range) - rand.nextInt(range); - int dy = targetY - rand.nextInt(range) + rand.nextInt(range > 1 ? range - 1 : range); - - if (dy <= floor) { - dy = targetY; - } - - BlockPos pos = new BlockPos(dx, dy, dz); - - if (EntityUtil.canDestroyBlock(world, pos, this)) { - // todo limit what can be broken - world.destroyBlock(pos, true); - - // sparkle!! - for (int k = 0; k < 20; k++) { - double d = rand.nextGaussian() * 0.02D; - double d1 = rand.nextGaussian() * 0.02D; - double d2 = rand.nextGaussian() * 0.02D; - - world.spawnParticle(EnumParticleTypes.CRIT, (posX + rand.nextFloat() * width * 2.0F) - width, posY + rand.nextFloat() * height, (posZ + rand.nextFloat() * width * 2.0F) - width, d, d1, d2); - } - } - } - } - - /** - * Sets the naga to move slowly, such as when he is intimidating the player - */ - private void goSlow() { - this.getEntityAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).removeModifier(slowSpeed); // if we apply this twice, we crash, but we can always remove it - this.getEntityAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).removeModifier(fastSpeed); - this.getEntityAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).applyModifier(slowSpeed); - } - - /** - * Normal speed, like when he is circling - */ - private void goNormal() { - this.getEntityAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).removeModifier(slowSpeed); - this.getEntityAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).removeModifier(fastSpeed); - } - - /** - * Fast, like when he is charging - */ - private void goFast() { - this.getEntityAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).removeModifier(slowSpeed); - this.getEntityAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).removeModifier(fastSpeed); - this.getEntityAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).applyModifier(fastSpeed); - } - - @Override - public boolean canBePushed() { - return false; - } - - /** - * Finds a point that allows us to circle the target clockwise. - */ - private BlockPos findCirclePoint(boolean clockwise, double radius, double rotation) { - EntityLivingBase toCircle = getAttackTarget(); - - // compute angle - double vecx = posX - toCircle.posX; - double vecz = posZ - toCircle.posZ; - float rangle = (float) (Math.atan2(vecz, vecx)); - - // add a little, so he circles (clockwise) - rangle += clockwise ? rotation : -rotation; - - // figure out where we're headed from the target angle - double dx = MathHelper.cos(rangle) * radius; - double dz = MathHelper.sin(rangle) * radius; - - double dy = Math.min(getEntityBoundingBox().minY, toCircle.posY); - - // add that to the target entity's position, and we have our destination - return new BlockPos(toCircle.posX + dx, dy, toCircle.posZ + dz); - } - - @Override - public boolean isEntityInvulnerable(DamageSource src) { - return src.getTrueSource() != null && !this.isEntityWithinHomeArea(src.getTrueSource()) // reject damage from outside of our home radius - || src.getImmediateSource() != null && !this.isEntityWithinHomeArea(src.getImmediateSource()) - || src.isFireDamage() || src.isExplosion() || super.isEntityInvulnerable(src); - } - - @Override - public boolean attackEntityFrom(DamageSource source, float amount) { - if (source != DamageSource.FALL && super.attackEntityFrom(source, amount)) { - this.ticksSinceDamaged = 0; - return true; - } else { - return false; - } - } - - @Override - public boolean attackEntityAsMob(Entity toAttack) { - if (movementAI.movementState == MovementState.CHARGE && toAttack instanceof EntityLivingBase && ((EntityLivingBase) toAttack).isActiveItemStackBlocking()) { - toAttack.addVelocity(motionX * 1.25D, 0.5D, motionZ * 1.25D); - motionX *= -1.5D; - motionY += 0.5D; - motionZ *= -1.5D; - if (toAttack instanceof EntityPlayerMP) - TFPacketHandler.CHANNEL.sendTo(new PacketThrowPlayer((float) toAttack.motionX, (float) toAttack.motionY, (float) toAttack.motionZ), (EntityPlayerMP) toAttack); - attackEntityFrom(DamageSource.GENERIC, 4F); - world.playSound(null, toAttack.getPosition(), SoundEvents.ITEM_SHIELD_BLOCK, SoundCategory.PLAYERS, 1.0F, 0.8F + this.world.rand.nextFloat() * 0.4F); - movementAI.doDaze(); - return false; - } - boolean result = super.attackEntityAsMob(toAttack); - - if (result) { - // charging, apply extra pushback - toAttack.addVelocity(-MathHelper.sin((rotationYaw * 3.141593F) / 180F) * 2.0F, 0.4F, MathHelper.cos((rotationYaw * 3.141593F) / 180F) * 2.0F); - } - - return result; - } - - @Override - public float getBlockPathWeight(BlockPos pos) { - if (!this.isWithinHomeDistanceFromPosition(pos)) { - return Float.MIN_VALUE; - } else { - return 0.0F; - } - } - - @Override - protected void despawnEntity() { - if (world.getDifficulty() == EnumDifficulty.PEACEFUL) { - if (hasHome()) { - world.setBlockState(getHomePosition(), TFBlocks.boss_spawner.getDefaultState().withProperty(BlockTFBossSpawner.VARIANT, BossVariant.NAGA)); - } - setDead(); - } else { - super.despawnEntity(); - } - } - - @Override - public void setDead() { - super.setDead(); - for (EntityTFNagaSegment seg : bodySegments) { - // must use this instead of setDead - // since multiparts are not added to the world tick list which is what checks isDead - this.world.removeEntityDangerously(seg); - } - } - - @Override - public boolean isWithinHomeDistanceFromPosition(BlockPos pos) { - if (this.getMaximumHomeDistance() == -1) { - return true; - } else { - int distX = Math.abs(this.getHomePosition().getX() - pos.getX()); - int distY = Math.abs(this.getHomePosition().getY() - pos.getY()); - int distZ = Math.abs(this.getHomePosition().getZ() - pos.getZ()); - - return distX <= LEASH_X && distY <= LEASH_Y && distZ <= LEASH_Z; - } - } - - private boolean isEntityWithinHomeArea(Entity entity) { - return isWithinHomeDistanceFromPosition(new BlockPos(entity)); - } - - private void activateBodySegments() { - for (int i = 0; i < currentSegmentCount; i++) { - EntityTFNagaSegment segment = bodySegments[i]; - segment.activate(); - segment.setLocationAndAngles(posX + 0.1 * i, posY + 0.5D, posZ + 0.1 * i, rand.nextFloat() * 360F, 0.0F); - for (int j = 0; j < 20; j++) { - double d0 = this.rand.nextGaussian() * 0.02D; - double d1 = this.rand.nextGaussian() * 0.02D; - double d2 = this.rand.nextGaussian() * 0.02D; - this.world.spawnParticle(EnumParticleTypes.EXPLOSION_NORMAL, - segment.posX + (double) (this.rand.nextFloat() * segment.width * 2.0F) - (double) segment.width - d0 * 10.0D, - segment.posY + (double) (this.rand.nextFloat() * segment.height) - d1 * 10.0D, - segment.posZ + (double) (this.rand.nextFloat() * segment.width * 2.0F) - (double) segment.width - d2 * 10.0D, - d0, d1, d2); - } - } - } - - /** - * Sets the heading (ha ha) of the bodySegments segments - */ - private void moveSegments() { - for (int i = 0; i < this.bodySegments.length; i++) { - Entity leader = i == 0 ? this : this.bodySegments[i - 1]; - double followX = leader.posX; - double followY = leader.posY; - double followZ = leader.posZ; - - // also weight the position so that the segments straighten out a little bit, and the front ones straighten more - float angle = (((leader.rotationYaw + 180) * 3.141593F) / 180F); - - - double straightenForce = 0.05D + (1.0 / (float) (i + 1)) * 0.5D; - - double idealX = -MathHelper.sin(angle) * straightenForce; - double idealZ = MathHelper.cos(angle) * straightenForce; - - - Vec3d diff = new Vec3d(bodySegments[i].posX - followX, bodySegments[i].posY - followY, bodySegments[i].posZ - followZ); - diff = diff.normalize(); - - // weight so segments drift towards their ideal position - diff = diff.add(idealX, 0, idealZ).normalize(); - - double f = 2.0D; - - double destX = followX + f * diff.x; - double destY = followY + f * diff.y; - double destZ = followZ + f * diff.z; - - bodySegments[i].setPosition(destX, destY, destZ); - - double distance = (double) MathHelper.sqrt(diff.x * diff.x + diff.z * diff.z); - - if (i == 0) { - // tilt segment next to head up towards head - diff = diff.add(0, -0.15, 0); - } - - bodySegments[i].setRotation((float) (Math.atan2(diff.z, diff.x) * 180.0D / Math.PI) + 90.0F, -(float) (Math.atan2(diff.y, distance) * 180.0D / Math.PI)); - } - } - - @Override - public void writeEntityToNBT(NBTTagCompound compound) { - if (hasHome()) { - BlockPos home = this.getHomePosition(); - compound.setTag("Home", new NBTTagIntArray(new int[]{home.getX(), home.getY(), home.getZ()})); - } - - super.writeEntityToNBT(compound); - } - - @Override - public void readEntityFromNBT(NBTTagCompound compound) { - super.readEntityFromNBT(compound); - - if (compound.hasKey("Home", Constants.NBT.TAG_INT_ARRAY)) { - int[] home = compound.getIntArray("Home"); - this.setHomePosAndDistance(new BlockPos(home[0], home[1], home[2]), 20); - } else { - this.detachHome(); - } - - if (this.hasCustomName()) { - this.bossInfo.setName(this.getDisplayName()); - } - } - - @Override - public void onDeath(DamageSource cause) { - super.onDeath(cause); - // mark the courtyard as defeated - if (!world.isRemote) { - this.bossInfo.setPercent(0.0F); - TFWorld.markStructureConquered(world, new BlockPos(this), TFFeature.NAGA_COURTYARD); - } - } - - @Override - public World getWorld() { - return this.world; - } - - @Override - public boolean attackEntityFromPart(MultiPartEntityPart part, DamageSource src, float damage) { - return attackEntityFrom(src, damage); - } - - @Override - public Entity[] getParts() { - return bodySegments; - } - - @Override - public void addTrackingPlayer(EntityPlayerMP player) { - super.addTrackingPlayer(player); - this.bossInfo.addPlayer(player); - } - - @Override - public void removeTrackingPlayer(EntityPlayerMP player) { - super.removeTrackingPlayer(player); - this.bossInfo.removePlayer(player); - } - - @Override - public boolean isNonBoss() { - return false; - } + public static final ResourceLocation LOOT_TABLE = TwilightForestMod.prefix("entities/naga"); + + private static final int TICKS_BEFORE_HEALING = 600; + private static final int MAX_SEGMENTS = 12; + private static final int LEASH_X = 46; + private static final int LEASH_Y = 7; + private static final int LEASH_Z = 46; + private static final double DEFAULT_SPEED = 0.5D; + + private int currentSegmentCount = 0; // not including head + private final float healthPerSegment; + private final EntityTFNagaSegment[] bodySegments = new EntityTFNagaSegment[MAX_SEGMENTS]; + private AIMovementPattern movementAI; + private int ticksSinceDamaged = 0; + + private final BossInfoServer bossInfo = new BossInfoServer(getDisplayName(), BossInfo.Color.GREEN, BossInfo.Overlay.NOTCHED_10); + + private final AttributeModifier slowSpeed = new AttributeModifier("Naga Slow Speed", 0.25F, 0).setSaved(false); + private final AttributeModifier fastSpeed = new AttributeModifier("Naga Fast Speed", 0.50F, 0).setSaved(false); + + private static final DataParameter DATA_DAZE = EntityDataManager.createKey(EntityTFNaga.class, DataSerializers.BOOLEAN); + + public EntityTFNaga(World world) { + super(world); + this.setSize(1.75f, 3.0f); + this.stepHeight = 2; + this.healthPerSegment = getMaxHealth() / 10; + this.experienceValue = 217; + this.ignoreFrustumCheck = true; + + for (int i = 0; i < bodySegments.length; i++) { + bodySegments[i] = new EntityTFNagaSegment(this, i); + } + + this.goNormal(); + } + + @Override + protected void entityInit() { + super.entityInit(); + dataManager.register(DATA_DAZE, false); + } + + public boolean isDazed() { + return dataManager.get(DATA_DAZE); + } + + protected void setDazed(boolean daze) { + dataManager.set(DATA_DAZE, daze); + } + + private float getMaxHealthPerDifficulty() { + switch (world.getDifficulty()) { + case EASY: + return 120; + default: + case NORMAL: + return 200; + case HARD: + return 250; + } + } + + @Override + public void setCustomNameTag(String name) { + super.setCustomNameTag(name); + this.bossInfo.setName(this.getDisplayName()); + } + + @Override + protected boolean canDespawn() { + return false; + } + + @Override + public boolean isPushedByWater() { + return false; + } + + @Override + protected boolean canBeRidden(Entity entity) { + return false; + } + + @Override + protected void initEntityAI() { + this.tasks.addTask(1, new EntityAISwimming(this)); + this.tasks.addTask(2, new AIAttack(this)); + this.tasks.addTask(3, new AISmash(this)); + this.tasks.addTask(4, movementAI = new AIMovementPattern(this)); + this.tasks.addTask(8, new EntityAIWander(this, 1, 1) { + @Override + public void startExecuting() { + EntityTFNaga.this.goNormal(); + super.startExecuting(); + } + + @Override + protected Vec3d getPosition() { + return RandomPositionGenerator.findRandomTarget(this.entity, 30, 7); + } + }); + this.targetTasks.addTask(1, new EntityAIHurtByTarget(this, false)); + this.targetTasks.addTask(2, new EntityAINearestAttackableTarget<>(this, EntityPlayer.class, false)); + + this.moveHelper = new NagaMoveHelper(this); + } + + // Similar to EntityAIAttackMelee but simpler (no pathfinding) + static class AIAttack extends EntityAIBase { + + private final EntityTFNaga taskOwner; + private int attackTick = 20; + + AIAttack(EntityTFNaga taskOwner) { + this.taskOwner = taskOwner; + } + + @Override + public boolean shouldExecute() { + EntityLivingBase target = taskOwner.getAttackTarget(); + + return target != null + && target.getEntityBoundingBox().maxY > taskOwner.getEntityBoundingBox().minY - 2.5 + && target.getEntityBoundingBox().minY < taskOwner.getEntityBoundingBox().maxY + 2.5 + && taskOwner.getDistanceSq(target) <= 4.0D + && taskOwner.getEntitySenses().canSee(target); + + } + + @Override + public void updateTask() { + if (attackTick > 0) { + attackTick--; + } + } + + @Override + public void resetTask() { + attackTick = 20; + } + + @Override + public void startExecuting() { + taskOwner.attackEntityAsMob(taskOwner.getAttackTarget()); + attackTick = 20; + } + } + + static class AISmash extends EntityAIBase { + + private final EntityTFNaga taskOwner; + + AISmash(EntityTFNaga taskOwner) { + this.taskOwner = taskOwner; + } + + @Override + public boolean shouldExecute() { + return /*taskOwner.getAttackTarget() != null &&*/ taskOwner.collidedHorizontally && ForgeEventFactory.getMobGriefingEvent(taskOwner.world, taskOwner); + } + + @Override + public void startExecuting() { + // NAGA SMASH! + if (taskOwner.world.isRemote) return; + + AxisAlignedBB bb = taskOwner.getEntityBoundingBox(); + + int minx = MathHelper.floor(bb.minX - 0.75D); + int miny = MathHelper.floor(bb.minY + 1.01D); + int minz = MathHelper.floor(bb.minZ - 0.75D); + int maxx = MathHelper.floor(bb.maxX + 0.75D); + int maxy = MathHelper.floor(bb.maxY + 0.0D); + int maxz = MathHelper.floor(bb.maxZ + 0.75D); + + BlockPos min = new BlockPos(minx, miny, minz); + BlockPos max = new BlockPos(maxx, maxy, maxz); + + if (taskOwner.world.isAreaLoaded(min, max)) { + for (BlockPos pos : BlockPos.getAllInBox(min, max)) { + if (EntityUtil.canDestroyBlock(taskOwner.world, pos, taskOwner)) { + taskOwner.world.destroyBlock(pos, true); + } + } + } + } + } + + enum MovementState { + INTIMIDATE, + CRUMBLE, + CHARGE, + CIRCLE, + DAZE + } + + static class AIMovementPattern extends EntityAIBase { + + private final EntityTFNaga taskOwner; + private MovementState movementState; + private int stateCounter; + private boolean clockwise; + + AIMovementPattern(EntityTFNaga taskOwner) { + this.taskOwner = taskOwner; + setMutexBits(3); + resetTask(); + } + + @Override + public boolean shouldExecute() { + return taskOwner.getAttackTarget() != null; + } + + @Override + public void resetTask() { + movementState = MovementState.CIRCLE; + stateCounter = 15; + clockwise = false; + } + + @Override + public void updateTask() { + if (!taskOwner.getNavigator().noPath()) { + // If we still have an uncompleted path don't run yet + // This isn't in shouldExecute/shouldContinueExecuting because we don't want to reset the task + // todo 1.10 there's a better way to do this I think + taskOwner.setDazed(false); // Since we have a path, we shouldn't be dazed anymore. + return; + } + + switch (movementState) { + case INTIMIDATE: { + taskOwner.getNavigator().clearPath(); + taskOwner.getLookHelper().setLookPositionWithEntity(taskOwner.getAttackTarget(), 30F, 30F); + taskOwner.faceEntity(taskOwner.getAttackTarget(), 30F, 30F); + taskOwner.moveForward = 0.1f; + break; + } + case CRUMBLE: { + taskOwner.getNavigator().clearPath(); + taskOwner.crumbleBelowTarget(2); + taskOwner.crumbleBelowTarget(3); + break; + } + case CHARGE: { + BlockPos tpoint = taskOwner.findCirclePoint(clockwise, 14, Math.PI); + taskOwner.getNavigator().tryMoveToXYZ(tpoint.getX(), tpoint.getY(), tpoint.getZ(), 1); // todo 1.10 check speed + break; + } + case CIRCLE: { + // normal radius is 13 + double radius = stateCounter % 2 == 0 ? 12.0 : 14.0; + double rotation = 1; // in radians + + // hook out slightly before circling + if (stateCounter > 1 && stateCounter < 3) { + radius = 16; + } + + // head almost straight at the player at the end + if (stateCounter == 1) { + rotation = 0.1; + } + + BlockPos tpoint = taskOwner.findCirclePoint(clockwise, radius, rotation); + taskOwner.getNavigator().tryMoveToXYZ(tpoint.getX(), tpoint.getY(), tpoint.getZ(), 1); // todo 1.10 check speed + break; + } + case DAZE: { + taskOwner.setDazed(true); + break; + } + } + + stateCounter--; + if (stateCounter <= 0) { + transitionState(); + } + } + + private void transitionState() { + taskOwner.setDazed(false); + switch (movementState) { + case INTIMIDATE: { + clockwise = !clockwise; + + if (taskOwner.getAttackTarget().getEntityBoundingBox().minY > taskOwner.getEntityBoundingBox().maxY) { + doCrumblePlayer(); + } else { + doCharge(); + } + + break; + } + case CRUMBLE: + doCharge(); + break; + case CHARGE: + doCircle(); + break; + case CIRCLE: + doIntimidate(); + break; + case DAZE: + doCircle(); + break; + } + } + + private void doDaze() { + movementState = MovementState.DAZE; + taskOwner.getNavigator().clearPath(); + stateCounter = 60 + taskOwner.rand.nextInt(40); + } + + private void doCircle() { + movementState = MovementState.CIRCLE; + stateCounter += 10 + taskOwner.rand.nextInt(10); + taskOwner.goNormal(); + } + + private void doCrumblePlayer() { + movementState = MovementState.CRUMBLE; + stateCounter = 20 + taskOwner.rand.nextInt(20); + taskOwner.goSlow(); + } + + /** + * Charge the player. Although the count is 3, we actually charge only 2 times. + */ + private void doCharge() { + movementState = MovementState.CHARGE; + stateCounter = 3; + taskOwner.goFast(); + } + + private void doIntimidate() { + movementState = MovementState.INTIMIDATE; + taskOwner.playSound(TFSounds.NAGA_RATTLE, taskOwner.getSoundVolume() * 4F, taskOwner.getSoundPitch()); + + stateCounter += 15 + taskOwner.rand.nextInt(10); + taskOwner.goSlow(); + } + } + + @Override + public void onLivingUpdate() { + + super.onLivingUpdate(); + + if (world.isRemote || !ForgeEventFactory.getMobGriefingEvent(world, this)) return; + + AxisAlignedBB bb = this.getEntityBoundingBox(); + + int minx = MathHelper.floor(bb.minX - 0.75D); + int miny = MathHelper.floor(bb.minY + 1.01D); + int minz = MathHelper.floor(bb.minZ - 0.75D); + int maxx = MathHelper.floor(bb.maxX + 0.75D); + int maxy = MathHelper.floor(bb.maxY + 0.0D); + int maxz = MathHelper.floor(bb.maxZ + 0.75D); + + BlockPos min = new BlockPos(minx, miny, minz); + BlockPos max = new BlockPos(maxx, maxy, maxz); + + if (world.isAreaLoaded(min, max)) { + for (BlockPos pos : BlockPos.getAllInBox(min, max)) { + IBlockState state = world.getBlockState(pos); + if (state.getMaterial() == Material.LEAVES && EntityUtil.canDestroyBlock(world, pos, state, this)) { + world.destroyBlock(pos, true); + } + } + } + } + + @Override + protected void applyEntityAttributes() { + super.applyEntityAttributes(); + this.getEntityAttribute(SharedMonsterAttributes.MAX_HEALTH).setBaseValue(getMaxHealthPerDifficulty()); + this.getEntityAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).setBaseValue(DEFAULT_SPEED); + this.getEntityAttribute(SharedMonsterAttributes.ATTACK_DAMAGE).setBaseValue(5.0D); + this.getEntityAttribute(SharedMonsterAttributes.FOLLOW_RANGE).setBaseValue(80.0D); + } + + /** + * Determine how many segments, from 2-12, the naga should have, dependent on its current health + */ + private void setSegmentsPerHealth() { + int oldSegments = this.currentSegmentCount; + int newSegments = MathHelper.clamp((int) ((this.getHealth() / healthPerSegment) + (getHealth() > 0 ? 2 : 0)), 0, MAX_SEGMENTS); + this.currentSegmentCount = newSegments; + if (newSegments < oldSegments) { + for (int i = newSegments; i < oldSegments; i++) { + bodySegments[i].selfDestruct(); + } + } else if (newSegments > oldSegments) { + this.activateBodySegments(); + } + + if (!world.isRemote) { + double newSpeed = DEFAULT_SPEED - newSegments * (-0.2F / 12F); + if (newSpeed < 0) + newSpeed = 0; + this.getEntityAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).setBaseValue(newSpeed); + } + } + + @Override + public boolean canTriggerWalking() { + return false; + } + + @Override + public boolean isInLava() { + return false; + } + + @Override + public void onUpdate() { + if (deathTime > 0) { + for (int k = 0; k < 5; k++) { + double d = rand.nextGaussian() * 0.02D; + double d1 = rand.nextGaussian() * 0.02D; + double d2 = rand.nextGaussian() * 0.02D; + EnumParticleTypes explosionType = rand.nextBoolean() ? EnumParticleTypes.EXPLOSION_HUGE : EnumParticleTypes.EXPLOSION_NORMAL; + + world.spawnParticle(explosionType, (posX + rand.nextFloat() * width * 2.0F) - width, posY + rand.nextFloat() * height, (posZ + rand.nextFloat() * width * 2.0F) - width, d, d1, d2); + } + } + + // update health + this.ticksSinceDamaged++; + + if (!this.world.isRemote && this.ticksSinceDamaged > TICKS_BEFORE_HEALING && this.ticksSinceDamaged % 20 == 0) { + this.heal(1); + } + + setSegmentsPerHealth(); + + super.onUpdate(); + + // update bodySegments parts + for (EntityTFNagaSegment segment : bodySegments) { + this.world.updateEntityWithOptionalForce(segment, true); + } + + moveSegments(); + } + + @Override + protected void updateAITasks() { + super.updateAITasks(); + + if (getAttackTarget() != null && (getDistanceSq(getAttackTarget()) > 80 * 80 || !this.isEntityWithinHomeArea(getAttackTarget()))) { + setAttackTarget(null); + } + + // if we are very close to the path point, go to the next point, unless the path is finished + // TODO 1.10 this runs after the path navigator runs, is that okay? + double d = width * 4.0F; + Vec3d vec3d = hasPath() ? getNavigator().getPath().getPosition(this) : null; + + while (vec3d != null && vec3d.squareDistanceTo(posX, vec3d.y, posZ) < d * d) { + getNavigator().getPath().incrementPathIndex(); + + if (getNavigator().getPath().isFinished()) { + vec3d = null; + } else { + vec3d = getNavigator().getPath().getPosition(this); + } + } + + if (!isWithinHomeDistanceCurrentPosition()) { + setAttackTarget(null); + getNavigator().setPath(getNavigator().getPathToPos(getHomePosition()), 1.0F); + } + + // BOSS BAR! + this.bossInfo.setPercent(this.getHealth() / this.getMaxHealth()); + } + + static class NagaMoveHelper extends EntityMoveHelper { + + public NagaMoveHelper(EntityLiving naga) { + super(naga); + } + + @Override + public void onUpdateMoveHelper() { + // TF - slither! + MovementState currentState = ((EntityTFNaga) entity).movementAI.movementState; + if (currentState == MovementState.DAZE) { + this.entity.moveStrafing = 0F; + } else if (currentState != MovementState.CHARGE && currentState != MovementState.INTIMIDATE) { + this.entity.moveStrafing = MathHelper.cos(this.entity.ticksExisted * 0.3F) * 0.6F; + } else { + this.entity.moveStrafing *= 0.8F; + } + + super.onUpdateMoveHelper(); + } + } + + @Override + protected SoundEvent getAmbientSound() { + return TFSounds.NAGA_HISS; + } + + @Override + protected SoundEvent getHurtSound(DamageSource source) { + return TFSounds.NAGA_HURT; + } + + @Override + protected SoundEvent getDeathSound() { + return TFSounds.NAGA_HURT; + } + + @Override + public ResourceLocation getLootTable() { + return LOOT_TABLE; + } + + private void crumbleBelowTarget(int range) { + if (!ForgeEventFactory.getMobGriefingEvent(world, this)) return; + + int floor = (int) getEntityBoundingBox().minY; + int targetY = (int) getAttackTarget().getEntityBoundingBox().minY; + + if (targetY > floor) { + int dx = (int) getAttackTarget().posX + rand.nextInt(range) - rand.nextInt(range); + int dz = (int) getAttackTarget().posZ + rand.nextInt(range) - rand.nextInt(range); + int dy = targetY - rand.nextInt(range) + rand.nextInt(range > 1 ? range - 1 : range); + + if (dy <= floor) { + dy = targetY; + } + + BlockPos pos = new BlockPos(dx, dy, dz); + + if (EntityUtil.canDestroyBlock(world, pos, this)) { + // todo limit what can be broken + world.destroyBlock(pos, true); + + // sparkle!! + for (int k = 0; k < 20; k++) { + double d = rand.nextGaussian() * 0.02D; + double d1 = rand.nextGaussian() * 0.02D; + double d2 = rand.nextGaussian() * 0.02D; + + world.spawnParticle(EnumParticleTypes.CRIT, (posX + rand.nextFloat() * width * 2.0F) - width, posY + rand.nextFloat() * height, (posZ + rand.nextFloat() * width * 2.0F) - width, d, d1, d2); + } + } + } + } + + /** + * Sets the naga to move slowly, such as when he is intimidating the player + */ + private void goSlow() { + this.getEntityAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).removeModifier(slowSpeed); // if we apply this twice, we crash, but we can always remove it + this.getEntityAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).removeModifier(fastSpeed); + this.getEntityAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).applyModifier(slowSpeed); + } + + /** + * Normal speed, like when he is circling + */ + private void goNormal() { + this.getEntityAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).removeModifier(slowSpeed); + this.getEntityAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).removeModifier(fastSpeed); + } + + /** + * Fast, like when he is charging + */ + private void goFast() { + this.getEntityAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).removeModifier(slowSpeed); + this.getEntityAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).removeModifier(fastSpeed); + this.getEntityAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).applyModifier(fastSpeed); + } + + @Override + public boolean canBePushed() { + return false; + } + + /** + * Finds a point that allows us to circle the target clockwise. + */ + private BlockPos findCirclePoint(boolean clockwise, double radius, double rotation) { + EntityLivingBase toCircle = getAttackTarget(); + + // compute angle + double vecx = posX - toCircle.posX; + double vecz = posZ - toCircle.posZ; + float rangle = (float) (Math.atan2(vecz, vecx)); + + // add a little, so he circles (clockwise) + rangle += clockwise ? rotation : -rotation; + + // figure out where we're headed from the target angle + double dx = MathHelper.cos(rangle) * radius; + double dz = MathHelper.sin(rangle) * radius; + + double dy = Math.min(getEntityBoundingBox().minY, toCircle.posY); + + // add that to the target entity's position, and we have our destination + return new BlockPos(toCircle.posX + dx, dy, toCircle.posZ + dz); + } + + @Override + public boolean isEntityInvulnerable(DamageSource src) { + return src.getTrueSource() != null && !this.isEntityWithinHomeArea(src.getTrueSource()) // reject damage from outside of our home radius + || src.getImmediateSource() != null && !this.isEntityWithinHomeArea(src.getImmediateSource()) + || src.isFireDamage() || src.isExplosion() || super.isEntityInvulnerable(src); + } + + @Override + public boolean attackEntityFrom(DamageSource source, float amount) { + if (source != DamageSource.FALL && super.attackEntityFrom(source, amount)) { + this.ticksSinceDamaged = 0; + return true; + } else { + return false; + } + } + + @Override + public boolean attackEntityAsMob(Entity toAttack) { + if (movementAI.movementState == MovementState.CHARGE && toAttack instanceof EntityLivingBase && ((EntityLivingBase) toAttack).isActiveItemStackBlocking()) { + toAttack.addVelocity(motionX * 1.25D, 0.5D, motionZ * 1.25D); + motionX *= -1.5D; + motionY += 0.5D; + motionZ *= -1.5D; + if (toAttack instanceof EntityPlayerMP) + TFPacketHandler.CHANNEL.sendTo(new PacketThrowPlayer((float) toAttack.motionX, (float) toAttack.motionY, (float) toAttack.motionZ), (EntityPlayerMP) toAttack); + attackEntityFrom(DamageSource.GENERIC, 4F); + world.playSound(null, toAttack.getPosition(), SoundEvents.ITEM_SHIELD_BLOCK, SoundCategory.PLAYERS, 1.0F, 0.8F + this.world.rand.nextFloat() * 0.4F); + movementAI.doDaze(); + return false; + } + + if (!this.isDazed()) { + boolean result = super.attackEntityAsMob(toAttack); + if (result) { + // charging, apply extra pushback + toAttack.addVelocity(-MathHelper.sin((rotationYaw * 3.141593F) / 180F) * 2.0F, 0.4F, MathHelper.cos((rotationYaw * 3.141593F) / 180F) * 2.0F); + } + + return result; + } + + return false; + } + + @Override + public float getBlockPathWeight(BlockPos pos) { + if (!this.isWithinHomeDistanceFromPosition(pos)) { + return Float.MIN_VALUE; + } else { + return 0.0F; + } + } + + @Override + protected void despawnEntity() { + if (world.getDifficulty() == EnumDifficulty.PEACEFUL) { + if (hasHome()) { + world.setBlockState(getHomePosition(), TFBlocks.boss_spawner.getDefaultState().withProperty(BlockTFBossSpawner.VARIANT, BossVariant.NAGA)); + } + setDead(); + } else { + super.despawnEntity(); + } + } + + @Override + public void setDead() { + super.setDead(); + for (EntityTFNagaSegment seg : bodySegments) { + // must use this instead of setDead + // since multiparts are not added to the world tick list which is what checks isDead + this.world.removeEntityDangerously(seg); + } + } + + @Override + public boolean isWithinHomeDistanceFromPosition(BlockPos pos) { + if (this.getMaximumHomeDistance() == -1) { + return true; + } else { + int distX = Math.abs(this.getHomePosition().getX() - pos.getX()); + int distY = Math.abs(this.getHomePosition().getY() - pos.getY()); + int distZ = Math.abs(this.getHomePosition().getZ() - pos.getZ()); + + return distX <= LEASH_X && distY <= LEASH_Y && distZ <= LEASH_Z; + } + } + + private boolean isEntityWithinHomeArea(Entity entity) { + return isWithinHomeDistanceFromPosition(new BlockPos(entity)); + } + + private void activateBodySegments() { + for (int i = 0; i < currentSegmentCount; i++) { + EntityTFNagaSegment segment = bodySegments[i]; + segment.activate(); + segment.setLocationAndAngles(posX + 0.1 * i, posY + 0.5D, posZ + 0.1 * i, rand.nextFloat() * 360F, 0.0F); + for (int j = 0; j < 20; j++) { + double d0 = this.rand.nextGaussian() * 0.02D; + double d1 = this.rand.nextGaussian() * 0.02D; + double d2 = this.rand.nextGaussian() * 0.02D; + this.world.spawnParticle(EnumParticleTypes.EXPLOSION_NORMAL, + segment.posX + (double) (this.rand.nextFloat() * segment.width * 2.0F) - (double) segment.width - d0 * 10.0D, + segment.posY + (double) (this.rand.nextFloat() * segment.height) - d1 * 10.0D, + segment.posZ + (double) (this.rand.nextFloat() * segment.width * 2.0F) - (double) segment.width - d2 * 10.0D, + d0, d1, d2); + } + } + } + + /** + * Sets the heading (ha ha) of the bodySegments segments + */ + private void moveSegments() { + for (int i = 0; i < this.bodySegments.length; i++) { + Entity leader = i == 0 ? this : this.bodySegments[i - 1]; + double followX = leader.posX; + double followY = leader.posY; + double followZ = leader.posZ; + + // also weight the position so that the segments straighten out a little bit, and the front ones straighten more + float angle = (((leader.rotationYaw + 180) * 3.141593F) / 180F); + + + double straightenForce = 0.05D + (1.0 / (float) (i + 1)) * 0.5D; + + double idealX = -MathHelper.sin(angle) * straightenForce; + double idealZ = MathHelper.cos(angle) * straightenForce; + + + Vec3d diff = new Vec3d(bodySegments[i].posX - followX, bodySegments[i].posY - followY, bodySegments[i].posZ - followZ); + diff = diff.normalize(); + + // weight so segments drift towards their ideal position + diff = diff.add(idealX, 0, idealZ).normalize(); + + double f = 2.0D; + + double destX = followX + f * diff.x; + double destY = followY + f * diff.y; + double destZ = followZ + f * diff.z; + + bodySegments[i].setPosition(destX, destY, destZ); + + double distance = (double) MathHelper.sqrt(diff.x * diff.x + diff.z * diff.z); + + if (i == 0) { + // tilt segment next to head up towards head + diff = diff.add(0, -0.15, 0); + } + + bodySegments[i].setRotation((float) (Math.atan2(diff.z, diff.x) * 180.0D / Math.PI) + 90.0F, -(float) (Math.atan2(diff.y, distance) * 180.0D / Math.PI)); + } + } + + @Override + public void writeEntityToNBT(NBTTagCompound compound) { + if (hasHome()) { + BlockPos home = this.getHomePosition(); + compound.setTag("Home", new NBTTagIntArray(new int[]{home.getX(), home.getY(), home.getZ()})); + } + + super.writeEntityToNBT(compound); + } + + @Override + public void readEntityFromNBT(NBTTagCompound compound) { + super.readEntityFromNBT(compound); + + if (compound.hasKey("Home", Constants.NBT.TAG_INT_ARRAY)) { + int[] home = compound.getIntArray("Home"); + this.setHomePosAndDistance(new BlockPos(home[0], home[1], home[2]), 20); + } else { + this.detachHome(); + } + + if (this.hasCustomName()) { + this.bossInfo.setName(this.getDisplayName()); + } + } + + @Override + public void onDeath(DamageSource cause) { + super.onDeath(cause); + // mark the courtyard as defeated + if (!world.isRemote) { + this.bossInfo.setPercent(0.0F); + TFWorld.markStructureConquered(world, new BlockPos(this), TFFeature.NAGA_COURTYARD); + } + } + + @Override + public World getWorld() { + return this.world; + } + + @Override + public boolean attackEntityFromPart(MultiPartEntityPart part, DamageSource src, float damage) { + return attackEntityFrom(src, damage); + } + + @Override + public Entity[] getParts() { + return bodySegments; + } + + @Override + public void addTrackingPlayer(EntityPlayerMP player) { + super.addTrackingPlayer(player); + this.bossInfo.addPlayer(player); + } + + @Override + public void removeTrackingPlayer(EntityPlayerMP player) { + super.removeTrackingPlayer(player); + this.bossInfo.removePlayer(player); + } + + @Override + public boolean isNonBoss() { + return false; + } } diff --git a/src/main/java/twilightforest/entity/boss/EntityTFSnowQueen.java b/src/main/java/twilightforest/entity/boss/EntityTFSnowQueen.java index fbeed3e331..12cbe7f9cf 100644 --- a/src/main/java/twilightforest/entity/boss/EntityTFSnowQueen.java +++ b/src/main/java/twilightforest/entity/boss/EntityTFSnowQueen.java @@ -59,472 +59,479 @@ public class EntityTFSnowQueen extends EntityMob implements IEntityMultiPart, IBreathAttacker { - public static final ResourceLocation LOOT_TABLE = TwilightForestMod.prefix("entities/snow_queen"); - private static final int MAX_SUMMONS = 6; - private static final DataParameter BEAM_FLAG = EntityDataManager.createKey(EntityTFSnowQueen.class, DataSerializers.BOOLEAN); - private static final DataParameter PHASE_FLAG = EntityDataManager.createKey(EntityTFSnowQueen.class, DataSerializers.BYTE); - private final BossInfoServer bossInfo = new BossInfoServer(getDisplayName(), BossInfo.Color.WHITE, BossInfo.Overlay.PROGRESS); - private static final int MAX_DAMAGE_WHILE_BEAMING = 25; - private static final float BREATH_DAMAGE = 4.0F; - - public enum Phase {SUMMON, DROP, BEAM} - - public final Entity[] iceArray = new Entity[7]; - - private int summonsRemaining = 0; - private int successfulDrops; - private int maxDrops; - private int damageWhileBeaming; - - public EntityTFSnowQueen(World world) { - super(world); - this.setSize(0.7F, 2.2F); - - for (int i = 0; i < this.iceArray.length; i++) { - this.iceArray[i] = new EntityTFSnowQueenIceShield(this); - } - - this.setCurrentPhase(Phase.SUMMON); - - this.isImmuneToFire = true; - this.experienceValue = 317; - } - - @Override - protected void initEntityAI() { - this.tasks.addTask(0, new EntityAISwimming(this)); - this.tasks.addTask(1, new EntityAITFHoverSummon(this, 1.0D)); - this.tasks.addTask(2, new EntityAITFHoverThenDrop(this, 50, 10)); - this.tasks.addTask(3, new EntityAITFHoverBeam(this, 60, 80)); - this.tasks.addTask(6, new EntityAIAttackMelee(this, 1.0D, true)); - this.tasks.addTask(8, new EntityAIWatchClosest(this, EntityPlayer.class, 8.0F)); - this.tasks.addTask(8, new EntityAILookIdle(this)); - this.targetTasks.addTask(1, new EntityAIHurtByTarget(this, true)); - this.targetTasks.addTask(2, new EntityAINearestAttackableTarget<>(this, EntityPlayer.class, true)); - } - - @Override - public boolean canBePushed() { - return false; - } - - @Override - protected void applyEntityAttributes() { - super.applyEntityAttributes(); - this.getEntityAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).setBaseValue(0.23D); - this.getEntityAttribute(SharedMonsterAttributes.ATTACK_DAMAGE).setBaseValue(7.0D); - this.getEntityAttribute(SharedMonsterAttributes.FOLLOW_RANGE).setBaseValue(40.0D); - this.getEntityAttribute(SharedMonsterAttributes.MAX_HEALTH).setBaseValue(200.0D); - this.getEntityAttribute(SharedMonsterAttributes.KNOCKBACK_RESISTANCE).setBaseValue(0.75D); - } - - @Override - protected void entityInit() { - super.entityInit(); - dataManager.register(BEAM_FLAG, false); - dataManager.register(PHASE_FLAG, (byte) 0); - } - - @Override - protected SoundEvent getAmbientSound() { - return TFSounds.ICE_AMBIENT; - } - - @Override - protected SoundEvent getHurtSound(DamageSource source) { - return TFSounds.ICE_HURT; - } - - @Override - protected SoundEvent getDeathSound() { - return TFSounds.ICE_DEATH; - } - - @Override - public ResourceLocation getLootTable() { - return LOOT_TABLE; - } - - @Override - public void onLivingUpdate() { - super.onLivingUpdate(); - - if (isBreathing()) playSound(MMSounds.ENTITY_FROSTMAW_ICEBREATH_START, rand.nextFloat() * 0.75F, rand.nextFloat() * 1.5F); - if (!world.isRemote) { - bossInfo.setPercent(getHealth() / getMaxHealth()); - } else { - spawnParticles(); - } - } - - private void spawnParticles() { - // make snow particles - for (int i = 0; i < 3; i++) { - float px = (this.rand.nextFloat() - this.rand.nextFloat()) * 0.3F; - float py = this.getEyeHeight() + (this.rand.nextFloat() - this.rand.nextFloat()) * 0.5F; - float pz = (this.rand.nextFloat() - this.rand.nextFloat()) * 0.3F; - - TwilightForestMod.proxy.spawnParticle(TFParticleType.SNOW_GUARDIAN, this.lastTickPosX + px, this.lastTickPosY + py, this.lastTickPosZ + pz, 0, 0, 0); - } - - // during drop phase, all the ice blocks should make particles - if (this.getCurrentPhase() == Phase.DROP) { - for (Entity ice : this.iceArray) { - float px = (this.rand.nextFloat() - this.rand.nextFloat()) * 0.5F; - float py = (this.rand.nextFloat() - this.rand.nextFloat()) * 0.5F; - float pz = (this.rand.nextFloat() - this.rand.nextFloat()) * 0.5F; - - TwilightForestMod.proxy.spawnParticle(TFParticleType.SNOW_WARNING, ice.lastTickPosX + px, ice.lastTickPosY + py, ice.lastTickPosZ + pz, 0, 0, 0); - } - } - - // when ice beaming, spew particles - if (isBreathing() && this.isEntityAlive()) { - Vec3d look = this.getLookVec(); - - double dist = 0.5; - double px = this.posX + look.x * dist; - double py = this.posY + 1.7F + look.y * dist; - double pz = this.posZ + look.z * dist; - - for (int i = 0; i < 10; i++) { - double dx = look.x; - double dy = 0;//look.y; - double dz = look.z; - - double spread = 2 + this.getRNG().nextDouble() * 2.5; - double velocity = 2.0 + this.getRNG().nextDouble() * 0.15; - - // beeeam - dx += this.getRNG().nextGaussian() * 0.0075D * spread; - dy += this.getRNG().nextGaussian() * 0.0075D * spread; - dz += this.getRNG().nextGaussian() * 0.0075D * spread; - dx *= velocity; - dy *= velocity; - dz *= velocity; - - TwilightForestMod.proxy.spawnParticle(TFParticleType.ICE_BEAM, px, py, pz, dx, dy, dz); - } - } - } - - @Override - public void onUpdate() { - super.onUpdate(); - - for (int i = 0; i < this.iceArray.length; i++) { - - this.iceArray[i].onUpdate(); - - if (i < this.iceArray.length - 1) { - // set block position - Vec3d blockPos = this.getIceShieldPosition(i); - - this.iceArray[i].setPosition(blockPos.x, blockPos.y, blockPos.z); - this.iceArray[i].rotationYaw = this.getIceShieldAngle(i); - } else { - // last block beneath - this.iceArray[i].setPosition(this.posX, this.posY - 1, this.posZ); - this.iceArray[i].rotationYaw = this.getIceShieldAngle(i); - } - - // collide things with the block - if (!world.isRemote) { - this.applyShieldCollisions(this.iceArray[i]); - } - } - - // death animation - if (deathTime > 0) { - for (int k = 0; k < 5; k++) { - double d = rand.nextGaussian() * 0.02D; - double d1 = rand.nextGaussian() * 0.02D; - double d2 = rand.nextGaussian() * 0.02D; - EnumParticleTypes explosionType = rand.nextBoolean() ? EnumParticleTypes.EXPLOSION_HUGE : EnumParticleTypes.EXPLOSION_NORMAL; - - world.spawnParticle(explosionType, (posX + rand.nextFloat() * width * 2.0F) - width, posY + rand.nextFloat() * height, (posZ + rand.nextFloat() * width * 2.0F) - width, d, d1, d2); - } - } - } - - @Override - protected boolean canDespawn() { - return false; - } - - @Override - protected void despawnEntity() { - if (world.getDifficulty() == EnumDifficulty.PEACEFUL) { - if (hasHome()) { - world.setBlockState(getHomePosition(), TFBlocks.boss_spawner.getDefaultState().withProperty(BlockTFBossSpawner.VARIANT, BossVariant.SNOW_QUEEN)); - } - setDead(); - } else { - super.despawnEntity(); - } - } - - @Override - public void onDeath(DamageSource cause) { - super.onDeath(cause); - // mark the tower as defeated - if (!world.isRemote) { - this.bossInfo.setPercent(0.0F); - TFWorld.markStructureConquered(world, new BlockPos(this), TFFeature.ICE_TOWER); - } - } - - // Immune to ice effects - @Override - public boolean isPotionApplicable(PotionEffect effect) { - return effect.getPotion() != TFPotions.frosty && effect.getPotion() != PotionHandler.FROZEN && super.isPotionApplicable(effect); - } - - private void applyShieldCollisions(Entity collider) { - List list = this.world.getEntitiesWithinAABBExcludingEntity(collider, collider.getEntityBoundingBox().grow(-0.2F, -0.2F, -0.2F)); - - for (Entity collided : list) { - if (collided.canBePushed()) { - applyShieldCollision(collider, collided); - } - } - } - - /** - * Do the effect where the shield hits something - */ - private void applyShieldCollision(Entity collider, Entity collided) { - if (collided != this) { - collided.applyEntityCollision(collider); - if (collided instanceof EntityLivingBase && super.attackEntityAsMob(collided)) { - ((EntityLivingBase)collided).knockBack(collided, 1.5F * 0.5F, (double)MathHelper.sin(this.rotationYaw * ((float)Math.PI / 180.0F)), (double)(-MathHelper.cos(this.rotationYaw * ((float)Math.PI / 180.0F)))); - this.playSound(SoundEvents.ENTITY_PLAYER_ATTACK_KNOCKBACK, 1.0F, 1.0F); - this.playSound(MMSounds.EFFECT_GEOMANCY_HIT_SMALL, 1.0F, 1.0F); - } - } - } - - @Override - protected void updateAITasks() { - super.updateAITasks(); - - // switch phases - if (this.getCurrentPhase() == Phase.SUMMON && this.getSummonsRemaining() == 0 && this.countMyMinions() <= 0) { - this.setCurrentPhase(Phase.DROP); - } - if (this.getCurrentPhase() == Phase.DROP && this.successfulDrops >= this.maxDrops) { - this.setCurrentPhase(Phase.BEAM); - } - if (this.getCurrentPhase() == Phase.BEAM && this.damageWhileBeaming >= MAX_DAMAGE_WHILE_BEAMING) { - this.setCurrentPhase(Phase.SUMMON); - } - } - - @Override - public boolean attackEntityFrom(DamageSource source, float damage) { - boolean result = super.attackEntityFrom(source, damage); - - if (result && this.getCurrentPhase() == Phase.BEAM) { - this.damageWhileBeaming += damage; - } - - Entity entity = source.getTrueSource(); - - if (entity != null && entity instanceof EntityLivingBase && ((EntityLivingBase)entity).isOnSameTeam(this)) { + public static final ResourceLocation LOOT_TABLE = TwilightForestMod.prefix("entities/snow_queen"); + private static final int MAX_SUMMONS = 6; + private static final DataParameter BEAM_FLAG = EntityDataManager.createKey(EntityTFSnowQueen.class, DataSerializers.BOOLEAN); + private static final DataParameter PHASE_FLAG = EntityDataManager.createKey(EntityTFSnowQueen.class, DataSerializers.BYTE); + private final BossInfoServer bossInfo = new BossInfoServer(getDisplayName(), BossInfo.Color.WHITE, BossInfo.Overlay.PROGRESS); + private static final int MAX_DAMAGE_WHILE_BEAMING = 25; + private static final float BREATH_DAMAGE = 4.0F; + + public enum Phase {SUMMON, DROP, BEAM} + + public final Entity[] iceArray = new Entity[7]; + + private int summonsRemaining = 0; + private int successfulDrops; + private int maxDrops; + private int damageWhileBeaming; + + public EntityTFSnowQueen(World world) { + super(world); + this.setSize(0.7F, 2.2F); + + for (int i = 0; i < this.iceArray.length; i++) { + this.iceArray[i] = new EntityTFSnowQueenIceShield(this); + } + + this.setCurrentPhase(Phase.SUMMON); + + this.isImmuneToFire = true; + this.experienceValue = 317; + } + + @Override + protected void initEntityAI() { + this.tasks.addTask(0, new EntityAISwimming(this)); + this.tasks.addTask(1, new EntityAITFHoverSummon(this, 1.0D)); + this.tasks.addTask(2, new EntityAITFHoverThenDrop(this, 50, 10)); + this.tasks.addTask(3, new EntityAITFHoverBeam(this, 60, 80)); + this.tasks.addTask(6, new EntityAIAttackMelee(this, 1.0D, true)); + this.tasks.addTask(8, new EntityAIWatchClosest(this, EntityPlayer.class, 8.0F)); + this.tasks.addTask(8, new EntityAILookIdle(this)); + this.targetTasks.addTask(1, new EntityAIHurtByTarget(this, true)); + this.targetTasks.addTask(2, new EntityAINearestAttackableTarget<>(this, EntityPlayer.class, true)); + } + + @Override + public boolean canBePushed() { + return false; + } + + @Override + protected void applyEntityAttributes() { + super.applyEntityAttributes(); + this.getEntityAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).setBaseValue(0.23D); + this.getEntityAttribute(SharedMonsterAttributes.ATTACK_DAMAGE).setBaseValue(7.0D); + this.getEntityAttribute(SharedMonsterAttributes.FOLLOW_RANGE).setBaseValue(40.0D); + this.getEntityAttribute(SharedMonsterAttributes.MAX_HEALTH).setBaseValue(200.0D); + this.getEntityAttribute(SharedMonsterAttributes.KNOCKBACK_RESISTANCE).setBaseValue(0.75D); + } + + @Override + protected void entityInit() { + super.entityInit(); + dataManager.register(BEAM_FLAG, false); + dataManager.register(PHASE_FLAG, (byte) 0); + } + + @Override + protected SoundEvent getAmbientSound() { + return TFSounds.ICE_AMBIENT; + } + + @Override + protected SoundEvent getHurtSound(DamageSource source) { + return TFSounds.ICE_HURT; + } + + @Override + protected SoundEvent getDeathSound() { + return TFSounds.ICE_DEATH; + } + + @Override + public ResourceLocation getLootTable() { + return LOOT_TABLE; + } + + @Override + public void onLivingUpdate() { + super.onLivingUpdate(); + + if (isBreathing()) + playSound(MMSounds.ENTITY_FROSTMAW_ICEBREATH_START, rand.nextFloat() * 0.75F, rand.nextFloat() * 1.5F); + if (!world.isRemote) { + bossInfo.setPercent(getHealth() / getMaxHealth()); + } else { + spawnParticles(); + } + } + + private void spawnParticles() { + // make snow particles + for (int i = 0; i < 3; i++) { + float px = (this.rand.nextFloat() - this.rand.nextFloat()) * 0.3F; + float py = this.getEyeHeight() + (this.rand.nextFloat() - this.rand.nextFloat()) * 0.5F; + float pz = (this.rand.nextFloat() - this.rand.nextFloat()) * 0.3F; + + TwilightForestMod.proxy.spawnParticle(TFParticleType.SNOW_GUARDIAN, this.lastTickPosX + px, this.lastTickPosY + py, this.lastTickPosZ + pz, 0, 0, 0); + } + + // during drop phase, all the ice blocks should make particles + if (this.getCurrentPhase() == Phase.DROP) { + for (Entity ice : this.iceArray) { + float px = (this.rand.nextFloat() - this.rand.nextFloat()) * 0.5F; + float py = (this.rand.nextFloat() - this.rand.nextFloat()) * 0.5F; + float pz = (this.rand.nextFloat() - this.rand.nextFloat()) * 0.5F; + + TwilightForestMod.proxy.spawnParticle(TFParticleType.SNOW_WARNING, ice.lastTickPosX + px, ice.lastTickPosY + py, ice.lastTickPosZ + pz, 0, 0, 0); + } + } + + // when ice beaming, spew particles + if (isBreathing() && this.isEntityAlive()) { + Vec3d look = this.getLookVec(); + + double dist = 0.5; + double px = this.posX + look.x * dist; + double py = this.posY + 1.7F + look.y * dist; + double pz = this.posZ + look.z * dist; + + for (int i = 0; i < 10; i++) { + double dx = look.x; + double dy = 0;//look.y; + double dz = look.z; + + double spread = 2 + this.getRNG().nextDouble() * 2.5; + double velocity = 2.0 + this.getRNG().nextDouble() * 0.15; + + // beeeam + dx += this.getRNG().nextGaussian() * 0.0075D * spread; + dy += this.getRNG().nextGaussian() * 0.0075D * spread; + dz += this.getRNG().nextGaussian() * 0.0075D * spread; + dx *= velocity; + dy *= velocity; + dz *= velocity; + + TwilightForestMod.proxy.spawnParticle(TFParticleType.ICE_BEAM, px, py, pz, dx, dy, dz); + } + } + } + + @Override + public void onUpdate() { + super.onUpdate(); + + for (int i = 0; i < this.iceArray.length; i++) { + + this.iceArray[i].onUpdate(); + + if (i < this.iceArray.length - 1) { + // set block position + Vec3d blockPos = this.getIceShieldPosition(i); + + this.iceArray[i].setPosition(blockPos.x, blockPos.y, blockPos.z); + this.iceArray[i].rotationYaw = this.getIceShieldAngle(i); + } else { + // last block beneath + this.iceArray[i].setPosition(this.posX, this.posY - 1, this.posZ); + this.iceArray[i].rotationYaw = this.getIceShieldAngle(i); + } + + // collide things with the block + if (!world.isRemote) { + this.applyShieldCollisions(this.iceArray[i]); + } + } + + // death animation + if (deathTime > 0) { + for (int k = 0; k < 5; k++) { + double d = rand.nextGaussian() * 0.02D; + double d1 = rand.nextGaussian() * 0.02D; + double d2 = rand.nextGaussian() * 0.02D; + EnumParticleTypes explosionType = rand.nextBoolean() ? EnumParticleTypes.EXPLOSION_HUGE : EnumParticleTypes.EXPLOSION_NORMAL; + + world.spawnParticle(explosionType, (posX + rand.nextFloat() * width * 2.0F) - width, posY + rand.nextFloat() * height, (posZ + rand.nextFloat() * width * 2.0F) - width, d, d1, d2); + } + } + } + + @Override + protected boolean canDespawn() { + return false; + } + + @Override + public boolean isPushedByWater() { + return false; + } + + @Override + protected boolean canBeRidden(Entity entity) { + return false; + } + + @Override + protected void despawnEntity() { + if (world.getDifficulty() == EnumDifficulty.PEACEFUL) { + if (hasHome()) { + world.setBlockState(getHomePosition(), TFBlocks.boss_spawner.getDefaultState().withProperty(BlockTFBossSpawner.VARIANT, BossVariant.SNOW_QUEEN)); + } + setDead(); + } else { + super.despawnEntity(); + } + } + + @Override + public void onDeath(DamageSource cause) { + super.onDeath(cause); + // mark the tower as defeated + if (!world.isRemote) { + this.bossInfo.setPercent(0.0F); + TFWorld.markStructureConquered(world, new BlockPos(this), TFFeature.ICE_TOWER); + } + } + + // Immune to ice effects + @Override + public boolean isPotionApplicable(PotionEffect effect) { + return effect.getPotion() != TFPotions.frosty && effect.getPotion() != PotionHandler.FROZEN && super.isPotionApplicable(effect); + } + + private void applyShieldCollisions(Entity collider) { + List list = this.world.getEntitiesWithinAABBExcludingEntity(collider, collider.getEntityBoundingBox().grow(-0.2F, -0.2F, -0.2F)); + + for (Entity collided : list) { + if (collided.canBePushed()) { + applyShieldCollision(collider, collided); + } + } + } + + /** + * Do the effect where the shield hits something + */ + private void applyShieldCollision(Entity collider, Entity collided) { + if (collided != this) { + collided.applyEntityCollision(collider); + if (collided instanceof EntityLivingBase && super.attackEntityAsMob(collided)) { + ((EntityLivingBase) collided).knockBack(collided, 1.5F * 0.5F, (double) MathHelper.sin(this.rotationYaw * ((float) Math.PI / 180.0F)), (double) (-MathHelper.cos(this.rotationYaw * ((float) Math.PI / 180.0F)))); + this.playSound(SoundEvents.ENTITY_PLAYER_ATTACK_KNOCKBACK, 1.0F, 1.0F); + this.playSound(MMSounds.EFFECT_GEOMANCY_HIT_SMALL, 1.0F, 1.0F); + } + } + } + + @Override + protected void updateAITasks() { + super.updateAITasks(); + + // switch phases + if (this.getCurrentPhase() == Phase.SUMMON && this.getSummonsRemaining() == 0 && this.countMyMinions() <= 0) { + this.setCurrentPhase(Phase.DROP); + } + if (this.getCurrentPhase() == Phase.DROP && this.successfulDrops >= this.maxDrops) { + this.setCurrentPhase(Phase.BEAM); + } + if (this.getCurrentPhase() == Phase.BEAM && this.damageWhileBeaming >= MAX_DAMAGE_WHILE_BEAMING) { + this.setCurrentPhase(Phase.SUMMON); + } + } + + @Override + public boolean attackEntityFrom(DamageSource source, float damage) { + boolean result = super.attackEntityFrom(source, damage); + + if (result && this.getCurrentPhase() == Phase.BEAM) { + this.damageWhileBeaming += damage; + } + + Entity entity = source.getTrueSource(); + + if (entity != null && entity instanceof EntityLivingBase && ((EntityLivingBase) entity).isOnSameTeam(this)) { return false; } - return result; - - } - - private Vec3d getIceShieldPosition(int idx) { - return this.getIceShieldPosition(getIceShieldAngle(idx), 1F); - } - - private float getIceShieldAngle(int idx) { - return 60F * idx + (this.ticksExisted * 5F); - } - - private Vec3d getIceShieldPosition(float angle, float distance) { - double dx = Math.cos((angle) * Math.PI / 180.0D) * distance; - double dz = Math.sin((angle) * Math.PI / 180.0D) * distance; - - return new Vec3d(this.posX + dx, this.posY + this.getShieldYOffset(), this.posZ + dz); - } - - private double getShieldYOffset() { - return 0.1F; - } - - @Override - public void fall(float distance, float damageMultiplier) { - } - - @Override - public World getWorld() { - return this.world; - } - - @Override - public boolean attackEntityFromPart(MultiPartEntityPart part, DamageSource source, float damage) { - return false; - } - - /** - * We need to do this for the bounding boxes on the parts to become active - */ - @Override - public Entity[] getParts() { - return iceArray; - } - - public void destroyBlocksInAABB(AxisAlignedBB box) { - if (ForgeEventFactory.getMobGriefingEvent(world, this)) { - for (BlockPos pos : WorldUtil.getAllInBB(box)) { - IBlockState state = world.getBlockState(pos); - if (state.getBlock() == Blocks.ICE || state.getBlock() == Blocks.PACKED_ICE) { - world.destroyBlock(pos, false); - } - } - } - } - - @Override - public boolean isBreathing() { - return dataManager.get(BEAM_FLAG); - } - - @Override - public void setBreathing(boolean flag) { - dataManager.set(BEAM_FLAG, flag); - } - - public Phase getCurrentPhase() { - return Phase.values()[dataManager.get(PHASE_FLAG)]; - } - - public void setCurrentPhase(Phase currentPhase) { - dataManager.set(PHASE_FLAG, (byte) currentPhase.ordinal()); - - // set variables for current phase - if (currentPhase == Phase.SUMMON) { - this.setSummonsRemaining(MAX_SUMMONS); - } - if (currentPhase == Phase.DROP) { - this.successfulDrops = 0; - this.maxDrops = 2 + this.rand.nextInt(3); - } - if (currentPhase == Phase.BEAM) { - this.damageWhileBeaming = 0; - } - } - - public int getSummonsRemaining() { - return summonsRemaining; - } - - public void setSummonsRemaining(int summonsRemaining) { - this.summonsRemaining = summonsRemaining; - } - - public void summonMinionAt(EntityLivingBase targetedEntity) { - EntityTFIceCrystal minion = new EntityTFIceCrystal(world); - minion.setPositionAndRotation(posX, posY, posZ, 0, 0); - - world.spawnEntity(minion); - this.playSound(MMSounds.ENTITY_FROSTMAW_ICEBALL_SHOOT, 1.0F, 0.75F); - this.playSound(MMSounds.ENTITY_FROSTMAW_FROZEN_CRASH, 0.8F, 1.5F); - - for (int i = 0; i < 100; i++) { - double attemptX; - double attemptY; - double attemptZ; - if (hasHome()) { - BlockPos home = getHomePosition(); - attemptX = home.getX() + rand.nextGaussian() * 7D; - attemptY = home.getY() + rand.nextGaussian() * 2D; - attemptZ = home.getZ() + rand.nextGaussian() * 7D; - } else { - attemptX = targetedEntity.posX + rand.nextGaussian() * 16D; - attemptY = targetedEntity.posY + rand.nextGaussian() * 8D; - attemptZ = targetedEntity.posZ + rand.nextGaussian() * 16D; - } - if (minion.attemptTeleport(attemptX, attemptY, attemptZ)) { - break; - } - } - - minion.setAttackTarget(targetedEntity); - minion.setToDieIn10Seconds(); // don't stick around - - this.summonsRemaining--; - } - - public int countMyMinions() { - return world.getEntitiesWithinAABB(EntityTFIceCrystal.class, new AxisAlignedBB(posX, posY, posZ, posX + 1, posY + 1, posZ + 1).grow(32.0D, 16.0D, 32.0D)).size(); - } - - public void incrementSuccessfulDrops() { - this.successfulDrops++; - } - - @Override - public void doBreathAttack(Entity target) { - target.attackEntityFrom(DamageSource.causeMobDamage(this), BREATH_DAMAGE); - - if (target instanceof EntityLivingBase) { - ((EntityLivingBase)target).addPotionEffect(new PotionEffect(TFPotions.frosty, 7 * 20, 4)); // 7 seconds - ((EntityLivingBase)target).addPotionEffect(new PotionEffect(MobEffects.WEAKNESS, 7 * 20, 3)); // 7 seconds - } - } - - @Override - public void setCustomNameTag(String name) { - super.setCustomNameTag(name); - this.bossInfo.setName(this.getDisplayName()); - } - - @Override - public void addTrackingPlayer(EntityPlayerMP player) { - super.addTrackingPlayer(player); - this.bossInfo.addPlayer(player); - } - - @Override - public void removeTrackingPlayer(EntityPlayerMP player) { - super.removeTrackingPlayer(player); - this.bossInfo.removePlayer(player); - } - - @Override - public void readEntityFromNBT(NBTTagCompound compound) { - super.readEntityFromNBT(compound); - if (this.hasCustomName()) - this.bossInfo.setName(this.getDisplayName()); - } - - @Override - public boolean isOnSameTeam(Entity entity) { + return result; + + } + + private Vec3d getIceShieldPosition(int idx) { + return this.getIceShieldPosition(getIceShieldAngle(idx), 1F); + } + + private float getIceShieldAngle(int idx) { + return 60F * idx + (this.ticksExisted * 5F); + } + + private Vec3d getIceShieldPosition(float angle, float distance) { + double dx = Math.cos((angle) * Math.PI / 180.0D) * distance; + double dz = Math.sin((angle) * Math.PI / 180.0D) * distance; + + return new Vec3d(this.posX + dx, this.posY + this.getShieldYOffset(), this.posZ + dz); + } + + private double getShieldYOffset() { + return 0.1F; + } + + @Override + public void fall(float distance, float damageMultiplier) { + } + + @Override + public World getWorld() { + return this.world; + } + + @Override + public boolean attackEntityFromPart(MultiPartEntityPart part, DamageSource source, float damage) { + return false; + } + + /** + * We need to do this for the bounding boxes on the parts to become active + */ + @Override + public Entity[] getParts() { + return iceArray; + } + + public void destroyBlocksInAABB(AxisAlignedBB box) { + if (ForgeEventFactory.getMobGriefingEvent(world, this)) { + for (BlockPos pos : WorldUtil.getAllInBB(box)) { + IBlockState state = world.getBlockState(pos); + if (state.getBlock() == Blocks.ICE || state.getBlock() == Blocks.PACKED_ICE) { + world.destroyBlock(pos, false); + } + } + } + } + + @Override + public boolean isBreathing() { + return dataManager.get(BEAM_FLAG); + } + + @Override + public void setBreathing(boolean flag) { + dataManager.set(BEAM_FLAG, flag); + } + + public Phase getCurrentPhase() { + return Phase.values()[dataManager.get(PHASE_FLAG)]; + } + + public void setCurrentPhase(Phase currentPhase) { + dataManager.set(PHASE_FLAG, (byte) currentPhase.ordinal()); + + // set variables for current phase + if (currentPhase == Phase.SUMMON) { + this.setSummonsRemaining(MAX_SUMMONS); + } + if (currentPhase == Phase.DROP) { + this.successfulDrops = 0; + this.maxDrops = 2 + this.rand.nextInt(3); + } + if (currentPhase == Phase.BEAM) { + this.damageWhileBeaming = 0; + } + } + + public int getSummonsRemaining() { + return summonsRemaining; + } + + public void setSummonsRemaining(int summonsRemaining) { + this.summonsRemaining = summonsRemaining; + } + + public void summonMinionAt(EntityLivingBase targetedEntity) { + EntityTFIceCrystal minion = new EntityTFIceCrystal(world); + minion.setPositionAndRotation(posX, posY, posZ, 0, 0); + + world.spawnEntity(minion); + this.playSound(MMSounds.ENTITY_FROSTMAW_ICEBALL_SHOOT, 1.0F, 0.75F); + this.playSound(MMSounds.ENTITY_FROSTMAW_FROZEN_CRASH, 0.8F, 1.5F); + + for (int i = 0; i < 100; i++) { + double attemptX; + double attemptY; + double attemptZ; + if (hasHome()) { + BlockPos home = getHomePosition(); + attemptX = home.getX() + rand.nextGaussian() * 7D; + attemptY = home.getY() + rand.nextGaussian() * 2D; + attemptZ = home.getZ() + rand.nextGaussian() * 7D; + } else { + attemptX = targetedEntity.posX + rand.nextGaussian() * 16D; + attemptY = targetedEntity.posY + rand.nextGaussian() * 8D; + attemptZ = targetedEntity.posZ + rand.nextGaussian() * 16D; + } + if (minion.attemptTeleport(attemptX, attemptY, attemptZ)) { + break; + } + } + + minion.setAttackTarget(targetedEntity); + minion.setToDieIn10Seconds(); // don't stick around + + this.summonsRemaining--; + } + + public int countMyMinions() { + return world.getEntitiesWithinAABB(EntityTFIceCrystal.class, new AxisAlignedBB(posX, posY, posZ, posX + 1, posY + 1, posZ + 1).grow(32.0D, 16.0D, 32.0D)).size(); + } + + public void incrementSuccessfulDrops() { + this.successfulDrops++; + } + + @Override + public void doBreathAttack(Entity target) { + target.attackEntityFrom(DamageSource.causeMobDamage(this), BREATH_DAMAGE); + + if (target instanceof EntityLivingBase) { + ((EntityLivingBase) target).addPotionEffect(new PotionEffect(TFPotions.frosty, 7 * 20, 4)); // 7 seconds + ((EntityLivingBase) target).addPotionEffect(new PotionEffect(MobEffects.WEAKNESS, 7 * 20, 3)); // 7 seconds + } + } + + @Override + public void setCustomNameTag(String name) { + super.setCustomNameTag(name); + this.bossInfo.setName(this.getDisplayName()); + } + + @Override + public void addTrackingPlayer(EntityPlayerMP player) { + super.addTrackingPlayer(player); + this.bossInfo.addPlayer(player); + } + + @Override + public void removeTrackingPlayer(EntityPlayerMP player) { + super.removeTrackingPlayer(player); + this.bossInfo.removePlayer(player); + } + + @Override + public void readEntityFromNBT(NBTTagCompound compound) { + super.readEntityFromNBT(compound); + if (this.hasCustomName()) + this.bossInfo.setName(this.getDisplayName()); + } + + @Override + public boolean isOnSameTeam(Entity entity) { if (entity == null) { return false; - } - else if (entity == this) { + } else if (entity == this) { return true; - } - else if (super.isOnSameTeam(entity)) { + } else if (super.isOnSameTeam(entity)) { return true; - } - else if (entity instanceof EntityTFSnowQueen || entity instanceof EntityTFIceMob) { + } else if (entity instanceof EntityTFSnowQueen || entity instanceof EntityTFIceMob) { return this.getTeam() == null && entity.getTeam() == null; - } - else { + } else { return false; } } - @Override - public boolean isNonBoss() { - return false; - } + @Override + public boolean isNonBoss() { + return false; + } } diff --git a/src/main/java/twilightforest/entity/boss/EntityTFUrGhast.java b/src/main/java/twilightforest/entity/boss/EntityTFUrGhast.java index 3426036d89..981eb224f9 100644 --- a/src/main/java/twilightforest/entity/boss/EntityTFUrGhast.java +++ b/src/main/java/twilightforest/entity/boss/EntityTFUrGhast.java @@ -43,569 +43,579 @@ public class EntityTFUrGhast extends EntityTFTowerGhast { - private static final DataParameter DATA_TANTRUM = EntityDataManager.createKey(EntityTFUrGhast.class, DataSerializers.BOOLEAN); - - //private static final int CRUISING_ALTITUDE = 235; // absolute cruising altitude - private static final int HOVER_ALTITUDE = 20; // how far, relatively, do we hover over ghast traps? - - private List trapLocations; - private int nextTantrumCry; - - private float damageUntilNextPhase = 10; // how much damage can we take before we toggle tantrum mode - private boolean noTrapMode; // are there no traps nearby? just float around - private final BossInfoServer bossInfo = new BossInfoServer(getDisplayName(), BossInfo.Color.RED, BossInfo.Overlay.PROGRESS); - - public EntityTFUrGhast(World world) { - super(world); - this.setSize(14.0F, 18.0F); - this.wanderFactor = 32.0F; - this.noClip = true; - this.setInTantrum(false); - this.experienceValue = 317; - this.moveHelper = new NoClipMoveHelper(this); - } - - @Override - public void setCustomNameTag(String name) { - super.setCustomNameTag(name); - this.bossInfo.setName(this.getDisplayName()); - } - - @Override - protected void applyEntityAttributes() { - super.applyEntityAttributes(); - this.getEntityAttribute(SharedMonsterAttributes.MAX_HEALTH).setBaseValue(250); - this.getEntityAttribute(SharedMonsterAttributes.FOLLOW_RANGE).setBaseValue(128.0D); - } - - @Override - protected void entityInit() { - super.entityInit(); - dataManager.register(DATA_TANTRUM, false); - } - - @Override - protected void initEntityAI() { - super.initEntityAI(); - trapLocations = new ArrayList(); - this.tasks.taskEntries.removeIf(e -> e.action instanceof EntityTFTowerGhast.AIHomedFly); - this.tasks.addTask(5, new AIWaypointFly(this)); - } - - static class AIWaypointFly extends EntityAIBase { - private final EntityTFUrGhast taskOwner; - - private final List pointsToVisit; - private int currentPoint = 0; - - AIWaypointFly(EntityTFUrGhast ghast) { - this.taskOwner = ghast; - pointsToVisit = createPath(); - setMutexBits(1); - } - - // [VanillaCopy] EntityGhast.AIRandomFly - @Override - public boolean shouldExecute() { - EntityMoveHelper entitymovehelper = this.taskOwner.getMoveHelper(); - - if (!entitymovehelper.isUpdating()) { - return true; - } else { - double d0 = entitymovehelper.getX() - this.taskOwner.posX; - double d1 = entitymovehelper.getY() - this.taskOwner.posY; - double d2 = entitymovehelper.getZ() - this.taskOwner.posZ; - double d3 = d0 * d0 + d1 * d1 + d2 * d2; - return d3 < 1.0D || d3 > 3600.0D; - } - } - - @Override - public boolean shouldContinueExecuting() { - return false; - } - - @Override - public void startExecuting() { - if (this.pointsToVisit.isEmpty()) { - pointsToVisit.addAll(createPath()); - } else { - if (this.currentPoint >= pointsToVisit.size()) { - this.currentPoint = 0; - - // when we're in tantrum mode, this is a good time to check if we need to spawn more ghasts - if (!taskOwner.checkGhastsAtTraps()) { - taskOwner.spawnGhastsAtTraps(); - } - } - - // TODO reintrodue wanderFactor somehow? Would need to change move helper or add extra fields here - - double x = pointsToVisit.get(currentPoint).getX(); - double y = pointsToVisit.get(currentPoint).getY() + HOVER_ALTITUDE; - double z = pointsToVisit.get(currentPoint).getZ(); - taskOwner.getMoveHelper().setMoveTo(x, y, z, 1.0F); - this.currentPoint++; - - // we have reached cruising altitude, time to turn noClip off - taskOwner.noClip = false; - } - } - - private List createPath() { - List potentialPoints = new ArrayList<>(); - BlockPos pos = new BlockPos(this.taskOwner); - - if (!this.taskOwner.noTrapMode) { - // make a copy of the trap locations list - potentialPoints.addAll(this.taskOwner.trapLocations); - } else { - potentialPoints.add(pos.add(20, -HOVER_ALTITUDE, 0)); - potentialPoints.add(pos.add(0, -HOVER_ALTITUDE, -20)); - potentialPoints.add(pos.add(-20, -HOVER_ALTITUDE, 0)); - potentialPoints.add(pos.add(0, -HOVER_ALTITUDE, 20)); - } - - Collections.shuffle(potentialPoints); - - if (this.taskOwner.noTrapMode) { - // if in no trap mode, head back to the middle when we're done - potentialPoints.add(pos.down(HOVER_ALTITUDE)); - } - - return potentialPoints; - } - } - - @Override - protected boolean canDespawn() { - return false; - } - - @Override - protected void despawnEntity() { - if (world.getDifficulty() == EnumDifficulty.PEACEFUL) { - if (hasHome()) { - world.setBlockState(getHomePosition(), TFBlocks.boss_spawner.getDefaultState().withProperty(BlockTFBossSpawner.VARIANT, BossVariant.UR_GHAST)); - } - setDead(); - } else { - super.despawnEntity(); - } - } - - @Override - public void onLivingUpdate() { - super.onLivingUpdate(); - - if (!world.isRemote) { - bossInfo.setPercent(getHealth() / getMaxHealth()); - } else { - if (this.isInTantrum()) { - TwilightForestMod.proxy.spawnParticle(TFParticleType.BOSS_TEAR, - this.posX + (this.rand.nextDouble() - 0.5D) * (double) this.width, - this.posY + this.rand.nextDouble() * (double) this.height - 0.25D, - this.posZ + (this.rand.nextDouble() - 0.5D) * (double) this.width, - 0, 0, 0 - ); - } - - // extra death explosions - if (deathTime > 0) { - for (int k = 0; k < 5; k++) { - - double d = rand.nextGaussian() * 0.02D; - double d1 = rand.nextGaussian() * 0.02D; - double d2 = rand.nextGaussian() * 0.02D; - - world.spawnParticle(rand.nextBoolean() ? EnumParticleTypes.EXPLOSION_HUGE : EnumParticleTypes.EXPLOSION_NORMAL, - (posX + rand.nextFloat() * width * 2.0F) - width, - posY + rand.nextFloat() * height, - (posZ + rand.nextFloat() * width * 2.0F) - width, - d, d1, d2 - ); - } - } - } - } - - @Override - public boolean isEntityInvulnerable(DamageSource src) { - return src == DamageSource.IN_WALL || super.isEntityInvulnerable(src); - } - - @Override - public void knockBack(Entity entityIn, float strength, double xRatio, double zRatio) { - // Don't take knockback - } - - @Override - public boolean attackEntityFrom(DamageSource source, float damage) { - // in tantrum mode take only 1/10 damage - if (this.isInTantrum()) { - damage /= 10; - } - - float oldHealth = getHealth(); - boolean attackSuccessful; - - if ("fireball".equals(source.getDamageType()) && source.getTrueSource() instanceof EntityPlayer) { - // 'hide' fireball attacks so that we don't take 1000 damage. - attackSuccessful = super.attackEntityFrom(DamageSource.causeThrownDamage(source.getTrueSource(), source.getImmediateSource()), damage); - } else { - attackSuccessful = super.attackEntityFrom(source, damage); - } - - float lastDamage = oldHealth - getHealth(); - - if (!world.isRemote) { - if (this.hurtTime == this.maxHurtTime) { - this.damageUntilNextPhase -= lastDamage; - - TwilightForestMod.LOGGER.debug("Urghast Attack successful, {} damage until phase switch.", this.damageUntilNextPhase); - - if (this.damageUntilNextPhase <= 0) { - this.switchPhase(); - } - } else { - TwilightForestMod.LOGGER.debug("Urghast Attack fail with {} type attack for {} damage", source.damageType, damage); - } - } - - return attackSuccessful; - } - - private void switchPhase() { - if (this.isInTantrum()) { - this.setInTantrum(false); - } else { - this.startTantrum(); - } - - resetDamageUntilNextPhase(); - } - - public void resetDamageUntilNextPhase() { - damageUntilNextPhase = 18; - } - - private void startTantrum() { - this.setInTantrum(true); - - // start raining - int rainTime = 300 * 20; - - WorldInfo worldInfo = world.getMinecraftServer().worlds[0].getWorldInfo(); // grab the overworld to set weather properly - - worldInfo.setCleanWeatherTime(0); - worldInfo.setRainTime(rainTime); - worldInfo.setThunderTime(rainTime); - worldInfo.setRaining(true); - worldInfo.setThundering(true); - - spawnGhastsAtTraps(); - } - - /** - * Spawn ghasts at two of the traps - */ - private void spawnGhastsAtTraps() { - // spawn ghasts around two of the traps - List ghastSpawns = new ArrayList(this.trapLocations); - Collections.shuffle(ghastSpawns); - - int numSpawns = Math.min(2, ghastSpawns.size()); - - for (int i = 0; i < numSpawns; i++) { - BlockPos spawnCoord = ghastSpawns.get(i); - spawnMinionGhastsAt(spawnCoord.getX(), spawnCoord.getY(), spawnCoord.getZ()); - } - } - - /** - * Spawn up to 6 minon ghasts around the indicated area - */ - private void spawnMinionGhastsAt(int x, int y, int z) { - int tries = 24; - int spawns = 0; - int maxSpawns = 6; - - int rangeXZ = 4; - int rangeY = 8; - - // lightning strike - this.world.addWeatherEffect(new EntityLightningBolt(world, x, y + 4, z, true)); - - - for (int i = 0; i < tries; i++) { - EntityTFMiniGhast minion = new EntityTFMiniGhast(world); - - double sx = x + ((rand.nextDouble() - rand.nextDouble()) * rangeXZ); - double sy = y + (rand.nextDouble() * rangeY); - double sz = z + ((rand.nextDouble() - rand.nextDouble()) * rangeXZ); - - minion.setLocationAndAngles(sx, sy, sz, this.world.rand.nextFloat() * 360.0F, 0.0F); - minion.makeBossMinion(); - - if (minion.getCanSpawnHere()) { - this.world.spawnEntity(minion); - minion.spawnExplosionParticle(); - } - - if (++spawns >= maxSpawns) { - break; - } - } - } - - @Override - protected void updateAITasks() { - super.updateAITasks(); - this.detachHome(); - - // despawn mini ghasts that are in our AABB - for (EntityTFMiniGhast ghast : world.getEntitiesWithinAABB(EntityTFMiniGhast.class, this.getEntityBoundingBox().grow(1, 1, 1))) { - ghast.spawnExplosionParticle(); - ghast.setDead(); - this.heal(2); - } - - // trap locations? - if (this.trapLocations.isEmpty() && !this.noTrapMode) { - this.scanForTrapsTwice(); - - if (this.trapLocations.isEmpty()) { - this.noTrapMode = true; - } - } - - if (this.isInTantrum()) { - setAttackTarget(null); - - // cry? - if (--this.nextTantrumCry <= 0) { - this.playSound(getHurtSound(null), this.getSoundVolume(), this.getSoundPitch()); - this.nextTantrumCry = 20 + rand.nextInt(30); - } - - if (this.ticksExisted % 10 == 0) { - doTantrumDamageEffects(); - } - } - } - - private void doTantrumDamageEffects() { - // harm player below - AxisAlignedBB below = this.getEntityBoundingBox().offset(0, -16, 0).grow(0, 16, 0); - - for (EntityPlayer player : world.getEntitiesWithinAABB(EntityPlayer.class, below)) { - if (world.canSeeSky(new BlockPos(player))) { - player.attackEntityFrom(DamageSource.ANVIL, 3); - } - } - - // also suck up mini ghasts - for (EntityTFMiniGhast ghast : world.getEntitiesWithinAABB(EntityTFMiniGhast.class, below)) { - ghast.motionY += 1; - } - } - - /** - * Check if there are at least 4 ghasts near at least 2 traps. Return false if not. - */ - private boolean checkGhastsAtTraps() { - int trapsWithEnoughGhasts = 0; - - for (BlockPos trap : this.trapLocations) { - AxisAlignedBB aabb = new AxisAlignedBB(trap, trap.add(1, 1, 1)).grow(8D, 16D, 8D); - - List nearbyGhasts = world.getEntitiesWithinAABB(EntityTFMiniGhast.class, aabb); - - if (nearbyGhasts.size() >= 4) { - trapsWithEnoughGhasts++; - } - } - - return trapsWithEnoughGhasts >= 1; - } - - @Override - protected void spitFireball() { - double offsetX = this.getAttackTarget().posX - this.posX; - double offsetY = this.getAttackTarget().getEntityBoundingBox().minY + (double) (this.getAttackTarget().height / 2.0F) - (this.posY + (double) (this.height / 2.0F)); - double offsetZ = this.getAttackTarget().posZ - this.posZ; - - EntityTFUrGhastFireball entityFireball = new EntityTFUrGhastFireball(this.world, this, offsetX, offsetY, offsetZ); - entityFireball.explosionPower = 1; - double shotSpawnDistance = 8.5D; - Vec3d lookVec = this.getLook(1.0F); - entityFireball.posX = this.posX + lookVec.x * shotSpawnDistance; - entityFireball.posY = this.posY + (double) (this.height / 2.0F) + lookVec.y * shotSpawnDistance; - entityFireball.posZ = this.posZ + lookVec.z * shotSpawnDistance; - this.world.spawnEntity(entityFireball); - - for (int i = 0; i < 2; i++) { - entityFireball = new EntityTFUrGhastFireball(this.world, this, offsetX + (rand.nextFloat() - rand.nextFloat()) * 8, offsetY, offsetZ + (rand.nextFloat() - rand.nextFloat()) * 8); - entityFireball.explosionPower = 1; - entityFireball.posX = this.posX + lookVec.x * shotSpawnDistance; - entityFireball.posY = this.posY + (double) (this.height / 2.0F) + lookVec.y * shotSpawnDistance; - entityFireball.posZ = this.posZ + lookVec.z * shotSpawnDistance; - this.world.spawnEntity(entityFireball); - } - - } - - /** - * Scan a few chunks around us for ghast trap blocks and if we find any, add them to our list - */ - private void scanForTrapsTwice() { - int scanRangeXZ = 48; - int scanRangeY = 32; - - scanForTraps(scanRangeXZ, scanRangeY, new BlockPos(this)); - - if (trapLocations.size() > 0) { - // average the location of the traps we've found, and scan again from there - int ax = 0, ay = 0, az = 0; - - for (BlockPos trapCoords : trapLocations) { - ax += trapCoords.getX(); - ay += trapCoords.getY(); - az += trapCoords.getZ(); - } - - ax /= trapLocations.size(); - ay /= trapLocations.size(); - az /= trapLocations.size(); - - scanForTraps(scanRangeXZ, scanRangeY, new BlockPos(ax, ay, az)); - } - } - - private void scanForTraps(int scanRangeXZ, int scanRangeY, BlockPos pos) { - for (int sx = -scanRangeXZ; sx <= scanRangeXZ; sx++) { - for (int sz = -scanRangeXZ; sz <= scanRangeXZ; sz++) { - for (int sy = -scanRangeY; sy <= scanRangeY; sy++) { - BlockPos trapCoords = pos.add(sx, sy, sz); - if (isTrapAt(trapCoords)) { - trapLocations.add(trapCoords); - } - } - } - } - } - - private boolean isTrapAt(BlockPos pos) { - IBlockState inactive = TFBlocks.tower_device.getDefaultState().withProperty(BlockTFTowerDevice.VARIANT, TowerDeviceVariant.GHASTTRAP_INACTIVE); - IBlockState active = TFBlocks.tower_device.getDefaultState().withProperty(BlockTFTowerDevice.VARIANT, TowerDeviceVariant.GHASTTRAP_ACTIVE); - return world.isBlockLoaded(pos) - && (world.getBlockState(pos) == inactive || world.getBlockState(pos) == active); - } - - @Override - public void addTrackingPlayer(EntityPlayerMP player) { - super.addTrackingPlayer(player); - this.bossInfo.addPlayer(player); - } - - @Override - public void removeTrackingPlayer(EntityPlayerMP player) { - super.removeTrackingPlayer(player); - this.bossInfo.removePlayer(player); - } - - @Override - public boolean isBurning() { - return false; - } - - @Override - public boolean canBePushed() { - return false; - } - - public boolean isInTantrum() { - return dataManager.get(DATA_TANTRUM); - } - - public void setInTantrum(boolean inTantrum) { - dataManager.set(DATA_TANTRUM, inTantrum); - resetDamageUntilNextPhase(); - } - - @Override - protected float getSoundVolume() { - return 16F; - } - - @Override - protected float getSoundPitch() { - return (this.rand.nextFloat() - this.rand.nextFloat()) * 0.2F + 0.5F; - } - - @Override - public void writeEntityToNBT(NBTTagCompound compound) { - compound.setBoolean("inTantrum", this.isInTantrum()); - super.writeEntityToNBT(compound); - } - - @Override - public void readEntityFromNBT(NBTTagCompound compound) { - super.readEntityFromNBT(compound); - this.setInTantrum(compound.getBoolean("inTantrum")); - if (this.hasCustomName()) { - this.bossInfo.setName(this.getDisplayName()); - } - } - - @Override - protected void onDeathUpdate() { - super.onDeathUpdate(); - if (this.deathTime == 20 && !world.isRemote) { - TFTreasure.darktower_boss.generateChest(world, findChestCoords(), false); - } - } - - @Override - public void onDeath(DamageSource cause) { - super.onDeath(cause); - // mark the tower as defeated - if (!world.isRemote) { - this.bossInfo.setPercent(0.0F); - TFWorld.markStructureConquered(world, findChestCoords(), TFFeature.DARK_TOWER); - } - } - - private BlockPos findChestCoords() { - if (trapLocations.size() > 0) { - // average the location of the traps we've found, and scan again from there - int ax = 0, ay = 0, az = 0; - - for (BlockPos trapCoords : trapLocations) { - ax += trapCoords.getX(); - ay += trapCoords.getY(); - az += trapCoords.getZ(); - } - - ax /= trapLocations.size(); - ay /= trapLocations.size(); - az /= trapLocations.size(); - - - return new BlockPos(ax, ay + 2, az); - } else { - return new BlockPos(this); - } - } - - // Don't attack (or even think about attacking) things while we're throwing a tantrum - @Override - protected boolean shouldAttack(EntityLivingBase living) { - return !this.isInTantrum(); - } - - @Override - public boolean isNonBoss() { - return false; - } + private static final DataParameter DATA_TANTRUM = EntityDataManager.createKey(EntityTFUrGhast.class, DataSerializers.BOOLEAN); + + //private static final int CRUISING_ALTITUDE = 235; // absolute cruising altitude + private static final int HOVER_ALTITUDE = 20; // how far, relatively, do we hover over ghast traps? + + private List trapLocations; + private int nextTantrumCry; + + private float damageUntilNextPhase = 10; // how much damage can we take before we toggle tantrum mode + private boolean noTrapMode; // are there no traps nearby? just float around + private final BossInfoServer bossInfo = new BossInfoServer(getDisplayName(), BossInfo.Color.RED, BossInfo.Overlay.PROGRESS); + + public EntityTFUrGhast(World world) { + super(world); + this.setSize(14.0F, 18.0F); + this.wanderFactor = 32.0F; + this.noClip = true; + this.setInTantrum(false); + this.experienceValue = 317; + this.moveHelper = new NoClipMoveHelper(this); + } + + @Override + public void setCustomNameTag(String name) { + super.setCustomNameTag(name); + this.bossInfo.setName(this.getDisplayName()); + } + + @Override + protected void applyEntityAttributes() { + super.applyEntityAttributes(); + this.getEntityAttribute(SharedMonsterAttributes.MAX_HEALTH).setBaseValue(250); + this.getEntityAttribute(SharedMonsterAttributes.FOLLOW_RANGE).setBaseValue(128.0D); + } + + @Override + protected void entityInit() { + super.entityInit(); + dataManager.register(DATA_TANTRUM, false); + } + + @Override + protected void initEntityAI() { + super.initEntityAI(); + trapLocations = new ArrayList(); + this.tasks.taskEntries.removeIf(e -> e.action instanceof EntityTFTowerGhast.AIHomedFly); + this.tasks.addTask(5, new AIWaypointFly(this)); + } + + static class AIWaypointFly extends EntityAIBase { + private final EntityTFUrGhast taskOwner; + + private final List pointsToVisit; + private int currentPoint = 0; + + AIWaypointFly(EntityTFUrGhast ghast) { + this.taskOwner = ghast; + pointsToVisit = createPath(); + setMutexBits(1); + } + + // [VanillaCopy] EntityGhast.AIRandomFly + @Override + public boolean shouldExecute() { + EntityMoveHelper entitymovehelper = this.taskOwner.getMoveHelper(); + + if (!entitymovehelper.isUpdating()) { + return true; + } else { + double d0 = entitymovehelper.getX() - this.taskOwner.posX; + double d1 = entitymovehelper.getY() - this.taskOwner.posY; + double d2 = entitymovehelper.getZ() - this.taskOwner.posZ; + double d3 = d0 * d0 + d1 * d1 + d2 * d2; + return d3 < 1.0D || d3 > 3600.0D; + } + } + + @Override + public boolean shouldContinueExecuting() { + return false; + } + + @Override + public void startExecuting() { + if (this.pointsToVisit.isEmpty()) { + pointsToVisit.addAll(createPath()); + } else { + if (this.currentPoint >= pointsToVisit.size()) { + this.currentPoint = 0; + + // when we're in tantrum mode, this is a good time to check if we need to spawn more ghasts + if (!taskOwner.checkGhastsAtTraps()) { + taskOwner.spawnGhastsAtTraps(); + } + } + + // TODO reintrodue wanderFactor somehow? Would need to change move helper or add extra fields here + + double x = pointsToVisit.get(currentPoint).getX(); + double y = pointsToVisit.get(currentPoint).getY() + HOVER_ALTITUDE; + double z = pointsToVisit.get(currentPoint).getZ(); + taskOwner.getMoveHelper().setMoveTo(x, y, z, 1.0F); + this.currentPoint++; + + // we have reached cruising altitude, time to turn noClip off + taskOwner.noClip = false; + } + } + + private List createPath() { + List potentialPoints = new ArrayList<>(); + BlockPos pos = new BlockPos(this.taskOwner); + + if (!this.taskOwner.noTrapMode) { + // make a copy of the trap locations list + potentialPoints.addAll(this.taskOwner.trapLocations); + } else { + potentialPoints.add(pos.add(20, -HOVER_ALTITUDE, 0)); + potentialPoints.add(pos.add(0, -HOVER_ALTITUDE, -20)); + potentialPoints.add(pos.add(-20, -HOVER_ALTITUDE, 0)); + potentialPoints.add(pos.add(0, -HOVER_ALTITUDE, 20)); + } + + Collections.shuffle(potentialPoints); + + if (this.taskOwner.noTrapMode) { + // if in no trap mode, head back to the middle when we're done + potentialPoints.add(pos.down(HOVER_ALTITUDE)); + } + + return potentialPoints; + } + } + + @Override + protected boolean canDespawn() { + return false; + } + + @Override + public boolean isPushedByWater() { + return false; + } + + @Override + protected boolean canBeRidden(Entity entity) { + return false; + } + + @Override + protected void despawnEntity() { + if (world.getDifficulty() == EnumDifficulty.PEACEFUL) { + if (hasHome()) { + world.setBlockState(getHomePosition(), TFBlocks.boss_spawner.getDefaultState().withProperty(BlockTFBossSpawner.VARIANT, BossVariant.UR_GHAST)); + } + setDead(); + } else { + super.despawnEntity(); + } + } + + @Override + public void onLivingUpdate() { + super.onLivingUpdate(); + + if (!world.isRemote) { + bossInfo.setPercent(getHealth() / getMaxHealth()); + } else { + if (this.isInTantrum()) { + TwilightForestMod.proxy.spawnParticle(TFParticleType.BOSS_TEAR, + this.posX + (this.rand.nextDouble() - 0.5D) * (double) this.width * 0.75D, + this.posY + this.rand.nextDouble() * (double) this.height * 0.5D, + this.posZ + (this.rand.nextDouble() - 0.5D) * (double) this.width * 0.75D, + 0, 0, 0 + ); + } + + // extra death explosions + if (deathTime > 0) { + for (int k = 0; k < 5; k++) { + + double d = rand.nextGaussian() * 0.02D; + double d1 = rand.nextGaussian() * 0.02D; + double d2 = rand.nextGaussian() * 0.02D; + + world.spawnParticle(rand.nextBoolean() ? EnumParticleTypes.EXPLOSION_HUGE : EnumParticleTypes.EXPLOSION_NORMAL, + (posX + rand.nextFloat() * width * 2.0F) - width, + posY + rand.nextFloat() * height, + (posZ + rand.nextFloat() * width * 2.0F) - width, + d, d1, d2 + ); + } + } + } + } + + @Override + public boolean isEntityInvulnerable(DamageSource src) { + return src == DamageSource.IN_WALL || src == DamageSource.IN_FIRE || src == DamageSource.ON_FIRE || super.isEntityInvulnerable(src); + } + + @Override + public void knockBack(Entity entityIn, float strength, double xRatio, double zRatio) { + // Don't take knockback + } + + @Override + public boolean attackEntityFrom(DamageSource source, float damage) { + // in tantrum mode take only 1/10 damage + if (this.isInTantrum()) { + damage /= 10; + } + + float oldHealth = getHealth(); + boolean attackSuccessful; + + if ("fireball".equals(source.getDamageType()) && source.getTrueSource() instanceof EntityPlayer) { + // 'hide' fireball attacks so that we don't take 1000 damage. + attackSuccessful = super.attackEntityFrom(DamageSource.causeThrownDamage(source.getTrueSource(), source.getImmediateSource()), damage); + } else { + attackSuccessful = super.attackEntityFrom(source, damage); + } + + float lastDamage = oldHealth - getHealth(); + + if (!world.isRemote) { + if (this.hurtTime == this.maxHurtTime) { + this.damageUntilNextPhase -= lastDamage; + + TwilightForestMod.LOGGER.debug("Urghast Attack successful, {} damage until phase switch.", this.damageUntilNextPhase); + + if (this.damageUntilNextPhase <= 0) { + this.switchPhase(); + } + } else { + TwilightForestMod.LOGGER.debug("Urghast Attack fail with {} type attack for {} damage", source.damageType, damage); + } + } + + return attackSuccessful; + } + + private void switchPhase() { + if (this.isInTantrum()) { + this.setInTantrum(false); + } else { + this.startTantrum(); + } + + resetDamageUntilNextPhase(); + } + + public void resetDamageUntilNextPhase() { + damageUntilNextPhase = 18; + } + + private void startTantrum() { + this.setInTantrum(true); + + // start raining + int rainTime = 300 * 20; + + WorldInfo worldInfo = world.getMinecraftServer().worlds[0].getWorldInfo(); // grab the overworld to set weather properly + + worldInfo.setCleanWeatherTime(0); + worldInfo.setRainTime(rainTime); + worldInfo.setThunderTime(rainTime); + worldInfo.setRaining(true); + worldInfo.setThundering(true); + + spawnGhastsAtTraps(); + } + + /** + * Spawn ghasts at two of the traps + */ + private void spawnGhastsAtTraps() { + // spawn ghasts around two of the traps + List ghastSpawns = new ArrayList(this.trapLocations); + Collections.shuffle(ghastSpawns); + + int numSpawns = Math.min(2, ghastSpawns.size()); + + for (int i = 0; i < numSpawns; i++) { + BlockPos spawnCoord = ghastSpawns.get(i); + spawnMinionGhastsAt(spawnCoord.getX(), spawnCoord.getY(), spawnCoord.getZ()); + } + } + + /** + * Spawn up to 6 minon ghasts around the indicated area + */ + private void spawnMinionGhastsAt(int x, int y, int z) { + int tries = 24; + int spawns = 0; + int maxSpawns = 6; + + int rangeXZ = 4; + int rangeY = 8; + + // lightning strike + this.world.addWeatherEffect(new EntityLightningBolt(world, x, y + 4, z, true)); + + + for (int i = 0; i < tries; i++) { + EntityTFMiniGhast minion = new EntityTFMiniGhast(world); + + double sx = x + ((rand.nextDouble() - rand.nextDouble()) * rangeXZ); + double sy = y + (rand.nextDouble() * rangeY); + double sz = z + ((rand.nextDouble() - rand.nextDouble()) * rangeXZ); + + minion.setLocationAndAngles(sx, sy, sz, this.world.rand.nextFloat() * 360.0F, 0.0F); + minion.makeBossMinion(); + + if (minion.getCanSpawnHere()) { + this.world.spawnEntity(minion); + minion.spawnExplosionParticle(); + } + + if (++spawns >= maxSpawns) { + break; + } + } + } + + @Override + protected void updateAITasks() { + super.updateAITasks(); + this.detachHome(); + + // despawn mini ghasts that are in our AABB + for (EntityTFMiniGhast ghast : world.getEntitiesWithinAABB(EntityTFMiniGhast.class, this.getEntityBoundingBox().grow(1, 1, 1))) { + ghast.spawnExplosionParticle(); + ghast.setDead(); + this.heal(2); + } + + // trap locations? + if (this.trapLocations.isEmpty() && !this.noTrapMode) { + this.scanForTrapsTwice(); + + if (this.trapLocations.isEmpty()) { + this.noTrapMode = true; + } + } + + if (this.isInTantrum()) { + setAttackTarget(null); + + // cry? + if (--this.nextTantrumCry <= 0) { + this.playSound(getHurtSound(null), this.getSoundVolume(), this.getSoundPitch()); + this.nextTantrumCry = 20 + rand.nextInt(30); + } + + if (this.ticksExisted % 10 == 0) { + doTantrumDamageEffects(); + } + } + } + + private void doTantrumDamageEffects() { + // harm player below + AxisAlignedBB below = this.getEntityBoundingBox().offset(0, -16, 0).grow(0, 16, 0); + + for (EntityPlayer player : world.getEntitiesWithinAABB(EntityPlayer.class, below)) { + if (world.canSeeSky(new BlockPos(player))) { + player.attackEntityFrom(DamageSource.ANVIL, 3); + } + } + + // also suck up mini ghasts + for (EntityTFMiniGhast ghast : world.getEntitiesWithinAABB(EntityTFMiniGhast.class, below)) { + ghast.motionY += 1; + } + } + + /** + * Check if there are at least 4 ghasts near at least 2 traps. Return false if not. + */ + private boolean checkGhastsAtTraps() { + int trapsWithEnoughGhasts = 0; + + for (BlockPos trap : this.trapLocations) { + AxisAlignedBB aabb = new AxisAlignedBB(trap, trap.add(1, 1, 1)).grow(8D, 16D, 8D); + + List nearbyGhasts = world.getEntitiesWithinAABB(EntityTFMiniGhast.class, aabb); + + if (nearbyGhasts.size() >= 4) { + trapsWithEnoughGhasts++; + } + } + + return trapsWithEnoughGhasts >= 1; + } + + @Override + protected void spitFireball() { + double offsetX = this.getAttackTarget().posX - this.posX; + double offsetY = this.getAttackTarget().getEntityBoundingBox().minY + (double) (this.getAttackTarget().height / 2.0F) - (this.posY + (double) (this.height / 2.0F)); + double offsetZ = this.getAttackTarget().posZ - this.posZ; + + EntityTFUrGhastFireball entityFireball = new EntityTFUrGhastFireball(this.world, this, offsetX, offsetY, offsetZ); + entityFireball.explosionPower = 1; + double shotSpawnDistance = 8.5D; + Vec3d lookVec = this.getLook(1.0F); + entityFireball.posX = this.posX + lookVec.x * shotSpawnDistance; + entityFireball.posY = this.posY + (double) (this.height / 2.0F) + lookVec.y * shotSpawnDistance; + entityFireball.posZ = this.posZ + lookVec.z * shotSpawnDistance; + this.world.spawnEntity(entityFireball); + + for (int i = 0; i < 2; i++) { + entityFireball = new EntityTFUrGhastFireball(this.world, this, offsetX + (rand.nextFloat() - rand.nextFloat()) * 8, offsetY, offsetZ + (rand.nextFloat() - rand.nextFloat()) * 8); + entityFireball.explosionPower = 1; + entityFireball.posX = this.posX + lookVec.x * shotSpawnDistance; + entityFireball.posY = this.posY + (double) (this.height / 2.0F) + lookVec.y * shotSpawnDistance; + entityFireball.posZ = this.posZ + lookVec.z * shotSpawnDistance; + this.world.spawnEntity(entityFireball); + } + + } + + /** + * Scan a few chunks around us for ghast trap blocks and if we find any, add them to our list + */ + private void scanForTrapsTwice() { + int scanRangeXZ = 48; + int scanRangeY = 32; + + scanForTraps(scanRangeXZ, scanRangeY, new BlockPos(this)); + + if (trapLocations.size() > 0) { + // average the location of the traps we've found, and scan again from there + int ax = 0, ay = 0, az = 0; + + for (BlockPos trapCoords : trapLocations) { + ax += trapCoords.getX(); + ay += trapCoords.getY(); + az += trapCoords.getZ(); + } + + ax /= trapLocations.size(); + ay /= trapLocations.size(); + az /= trapLocations.size(); + + scanForTraps(scanRangeXZ, scanRangeY, new BlockPos(ax, ay, az)); + } + } + + private void scanForTraps(int scanRangeXZ, int scanRangeY, BlockPos pos) { + for (int sx = -scanRangeXZ; sx <= scanRangeXZ; sx++) { + for (int sz = -scanRangeXZ; sz <= scanRangeXZ; sz++) { + for (int sy = -scanRangeY; sy <= scanRangeY; sy++) { + BlockPos trapCoords = pos.add(sx, sy, sz); + if (isTrapAt(trapCoords)) { + trapLocations.add(trapCoords); + } + } + } + } + } + + private boolean isTrapAt(BlockPos pos) { + IBlockState inactive = TFBlocks.tower_device.getDefaultState().withProperty(BlockTFTowerDevice.VARIANT, TowerDeviceVariant.GHASTTRAP_INACTIVE); + IBlockState active = TFBlocks.tower_device.getDefaultState().withProperty(BlockTFTowerDevice.VARIANT, TowerDeviceVariant.GHASTTRAP_ACTIVE); + return world.isBlockLoaded(pos) + && (world.getBlockState(pos) == inactive || world.getBlockState(pos) == active); + } + + @Override + public void addTrackingPlayer(EntityPlayerMP player) { + super.addTrackingPlayer(player); + this.bossInfo.addPlayer(player); + } + + @Override + public void removeTrackingPlayer(EntityPlayerMP player) { + super.removeTrackingPlayer(player); + this.bossInfo.removePlayer(player); + } + + @Override + public boolean isBurning() { + return false; + } + + @Override + public boolean canBePushed() { + return false; + } + + public boolean isInTantrum() { + return dataManager.get(DATA_TANTRUM); + } + + public void setInTantrum(boolean inTantrum) { + dataManager.set(DATA_TANTRUM, inTantrum); + resetDamageUntilNextPhase(); + } + + @Override + protected float getSoundVolume() { + return 16F; + } + + @Override + protected float getSoundPitch() { + return (this.rand.nextFloat() - this.rand.nextFloat()) * 0.2F + 0.5F; + } + + @Override + public void writeEntityToNBT(NBTTagCompound compound) { + compound.setBoolean("inTantrum", this.isInTantrum()); + super.writeEntityToNBT(compound); + } + + @Override + public void readEntityFromNBT(NBTTagCompound compound) { + super.readEntityFromNBT(compound); + this.setInTantrum(compound.getBoolean("inTantrum")); + if (this.hasCustomName()) { + this.bossInfo.setName(this.getDisplayName()); + } + } + + @Override + protected void onDeathUpdate() { + super.onDeathUpdate(); + if (this.deathTime == 20 && !world.isRemote) { + TFTreasure.darktower_boss.generateChest(world, findChestCoords(), false); + } + } + + @Override + public void onDeath(DamageSource cause) { + super.onDeath(cause); + // mark the tower as defeated + if (!world.isRemote) { + this.bossInfo.setPercent(0.0F); + TFWorld.markStructureConquered(world, findChestCoords(), TFFeature.DARK_TOWER); + } + } + + private BlockPos findChestCoords() { + if (trapLocations.size() > 0) { + // average the location of the traps we've found, and scan again from there + int ax = 0, ay = 0, az = 0; + + for (BlockPos trapCoords : trapLocations) { + ax += trapCoords.getX(); + ay += trapCoords.getY(); + az += trapCoords.getZ(); + } + + ax /= trapLocations.size(); + ay /= trapLocations.size(); + az /= trapLocations.size(); + + + return new BlockPos(ax, ay + 2, az); + } else { + return new BlockPos(this); + } + } + + // Don't attack (or even think about attacking) things while we're throwing a tantrum + @Override + protected boolean shouldAttack(EntityLivingBase living) { + return !this.isInTantrum(); + } + + @Override + public boolean isNonBoss() { + return false; + } } diff --git a/src/main/java/twilightforest/entity/boss/EntityTFYetiAlpha.java b/src/main/java/twilightforest/entity/boss/EntityTFYetiAlpha.java index 7432e2b017..0fd0a8439d 100644 --- a/src/main/java/twilightforest/entity/boss/EntityTFYetiAlpha.java +++ b/src/main/java/twilightforest/entity/boss/EntityTFYetiAlpha.java @@ -34,6 +34,7 @@ import twilightforest.TwilightForestMod; import twilightforest.biomes.TFBiomes; import twilightforest.client.particle.TFParticleType; +import twilightforest.entity.EntityTFTwilightWandBolt; import twilightforest.entity.IHostileMount; import twilightforest.entity.ai.EntityAITFThrowRider; import twilightforest.entity.ai.EntityAITFYetiRampage; @@ -174,6 +175,16 @@ public void onLivingUpdate() { } } + @Override + public boolean isPushedByWater() { + return false; + } + + @Override + protected boolean canBeRidden(Entity entity) { + return false; + } + // Immune to ice effects public boolean isPotionApplicable(PotionEffect effect) { return effect.getPotion() != TFPotions.frosty && effect.getPotion() != PotionHandler.FROZEN && super.isPotionApplicable(effect); @@ -197,12 +208,18 @@ public void setAttackTarget(@Nullable EntityLivingBase entity) { @Override public boolean attackEntityFrom(DamageSource source, float amount) { // no arrow damage when in ranged mode - if (!this.canRampage && !this.isTired() && source.isProjectile()) { + if (!this.canRampage && !this.isTired() && source.isProjectile() || source.getTrueSource() instanceof EntityTFTwilightWandBolt) { return false; } this.canRampage = true; - return super.attackEntityFrom(source, amount); + boolean flag = super.attackEntityFrom(source, amount); + + if (flag) { + this.canRampage = true; + } + + return flag; } @Nullable