vazkii.botania.common.entity.EntityDoppleganger.java Source code

Java tutorial

Introduction

Here is the source code for vazkii.botania.common.entity.EntityDoppleganger.java

Source

/**
 * This class was created by <Vazkii>. It's distributed as
 * part of the Botania Mod. Get the Source Code in github:
 * https://github.com/Vazkii/Botania
 *
 * Botania is Open Source and distributed under the
 * Botania License: http://botaniamod.net/license.php
 *
 * File Created @ [Jul 12, 2014, 3:47:45 PM (GMT)]
 */
package vazkii.botania.common.entity;

import io.netty.buffer.ByteBuf;
import net.minecraft.block.Block;
import net.minecraft.block.state.IBlockState;
import net.minecraft.client.Minecraft;
import net.minecraft.client.audio.MovingSound;
import net.minecraft.client.gui.ScaledResolution;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.client.renderer.texture.TextureMap;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLiving;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.SharedMonsterAttributes;
import net.minecraft.entity.ai.EntityAISwimming;
import net.minecraft.entity.ai.EntityAIWatchClosest;
import net.minecraft.entity.monster.EntitySkeleton;
import net.minecraft.entity.monster.EntityWitch;
import net.minecraft.entity.monster.EntityWitherSkeleton;
import net.minecraft.entity.monster.EntityZombie;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.init.Blocks;
import net.minecraft.init.Items;
import net.minecraft.init.MobEffects;
import net.minecraft.init.SoundEvents;
import net.minecraft.inventory.EntityEquipmentSlot;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.network.datasync.DataParameter;
import net.minecraft.network.datasync.DataSerializers;
import net.minecraft.network.datasync.EntityDataManager;
import net.minecraft.network.play.server.SPacketRemoveEntityEffect;
import net.minecraft.potion.Potion;
import net.minecraft.potion.PotionEffect;
import net.minecraft.tileentity.TileEntityBeacon;
import net.minecraft.util.DamageSource;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.EnumParticleTypes;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.SoundCategory;
import net.minecraft.util.math.AxisAlignedBB;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.math.RayTraceResult;
import net.minecraft.util.math.Vec3d;
import net.minecraft.util.text.Style;
import net.minecraft.util.text.TextComponentTranslation;
import net.minecraft.util.text.TextFormatting;
import net.minecraft.world.BossInfo;
import net.minecraft.world.BossInfoServer;
import net.minecraft.world.EnumDifficulty;
import net.minecraft.world.World;
import net.minecraft.world.WorldServer;
import net.minecraftforge.common.util.FakePlayer;
import net.minecraftforge.fml.common.registry.IEntityAdditionalSpawnData;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
import org.lwjgl.opengl.ARBShaderObjects;
import vazkii.botania.api.BotaniaAPI;
import vazkii.botania.api.boss.IBotaniaBoss;
import vazkii.botania.api.internal.ShaderCallback;
import vazkii.botania.api.lexicon.multiblock.Multiblock;
import vazkii.botania.api.lexicon.multiblock.MultiblockSet;
import vazkii.botania.api.lexicon.multiblock.component.MultiblockComponent;
import vazkii.botania.api.state.BotaniaStateProps;
import vazkii.botania.api.state.enums.PylonVariant;
import vazkii.botania.client.core.handler.BossBarHandler;
import vazkii.botania.client.core.helper.ShaderHelper;
import vazkii.botania.common.Botania;
import vazkii.botania.common.advancements.DopplegangerNoArmorTrigger;
import vazkii.botania.common.block.ModBlocks;
import vazkii.botania.common.core.handler.ModSounds;
import vazkii.botania.common.core.helper.Vector3;
import vazkii.botania.common.item.ModItems;
import vazkii.botania.common.lib.LibEntityNames;
import vazkii.botania.common.lib.LibMisc;
import vazkii.botania.common.network.PacketBotaniaEffect;
import vazkii.botania.common.network.PacketHandler;

import javax.annotation.Nonnull;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class EntityDoppleganger extends EntityLiving implements IBotaniaBoss, IEntityAdditionalSpawnData {

    public static final float ARENA_RANGE = 12F;
    public static final int ARENA_HEIGHT = 5;

    private static final int SPAWN_TICKS = 160;
    private static final float MAX_HP = 320F;

    private static final int MOB_SPAWN_START_TICKS = 20;
    private static final int MOB_SPAWN_END_TICKS = 80;
    private static final int MOB_SPAWN_BASE_TICKS = 800;
    private static final int MOB_SPAWN_TICKS = MOB_SPAWN_BASE_TICKS + MOB_SPAWN_START_TICKS + MOB_SPAWN_END_TICKS;
    private static final int MOB_SPAWN_WAVES = 10;
    private static final int MOB_SPAWN_WAVE_TIME = MOB_SPAWN_BASE_TICKS / MOB_SPAWN_WAVES;

    private static final String TAG_INVUL_TIME = "invulTime";
    private static final String TAG_AGGRO = "aggro";
    private static final String TAG_SOURCE_X = "sourceX";
    private static final String TAG_SOURCE_Y = "sourceY";
    private static final String TAG_SOURCE_Z = "sourcesZ";
    private static final String TAG_MOB_SPAWN_TICKS = "mobSpawnTicks";
    private static final String TAG_HARD_MODE = "hardMode";
    private static final String TAG_PLAYER_COUNT = "playerCount";

    private static final DataParameter<Integer> INVUL_TIME = EntityDataManager.createKey(EntityDoppleganger.class,
            DataSerializers.VARINT);

    private static final BlockPos[] PYLON_LOCATIONS = { new BlockPos(4, 1, 4), new BlockPos(4, 1, -4),
            new BlockPos(-4, 1, 4), new BlockPos(-4, 1, -4) };

    private static final List<ResourceLocation> CHEATY_BLOCKS = Arrays.asList(
            new ResourceLocation("openblocks", "beartrap"), new ResourceLocation("thaumictinkerer", "magnet"));

    private boolean spawnLandmines = false;
    private boolean spawnPixies = false;
    private boolean anyWithArmor = false;
    private boolean aggro = false;
    private int tpDelay = 0;
    private int mobSpawnTicks = 0;
    private int playerCount = 0;
    private boolean hardMode = false;
    private BlockPos source = BlockPos.ORIGIN;
    private final List<UUID> playersWhoAttacked = new ArrayList<>();
    private final BossInfoServer bossInfo = (BossInfoServer) new BossInfoServer(
            new TextComponentTranslation("entity." + LibEntityNames.DOPPLEGANGER_REGISTRY + ".name"),
            BossInfo.Color.PINK, BossInfo.Overlay.PROGRESS).setCreateFog(true);;
    private UUID bossInfoUUID = bossInfo.getUniqueId();
    public EntityPlayer trueKiller = null;

    public EntityDoppleganger(World world) {
        super(world);
        setSize(0.6F, 1.8F);
        isImmuneToFire = true;
        experienceValue = 825;
        if (world.isRemote) {
            Botania.proxy.addBoss(this);
        }
    }

    public static MultiblockSet makeMultiblockSet() {
        Multiblock mb = new Multiblock();

        for (BlockPos p : PYLON_LOCATIONS)
            mb.addComponent(p.up(), ModBlocks.pylon.getDefaultState().withProperty(BotaniaStateProps.PYLON_VARIANT,
                    PylonVariant.GAIA));

        for (int i = 0; i < 3; i++)
            for (int j = 0; j < 3; j++)
                mb.addComponent(new BeaconComponent(new BlockPos(i - 1, 0, j - 1)));

        mb.addComponent(new BeaconBeamComponent(new BlockPos(0, 1, 0)));
        mb.setRenderOffset(new BlockPos(0, -1, 0));

        return mb.makeSet();
    }

    public static boolean spawn(EntityPlayer player, ItemStack stack, World world, BlockPos pos, boolean hard) {
        //initial checks
        if (!(world.getTileEntity(pos) instanceof TileEntityBeacon) || !isTruePlayer(player)
                || countGaiaGuardiansAround(world, pos) > 0)
            return false;

        //check difficulty
        if (world.getDifficulty() == EnumDifficulty.PEACEFUL) {
            if (!world.isRemote)
                player.sendMessage(new TextComponentTranslation("botaniamisc.peacefulNoob")
                        .setStyle(new Style().setColor(TextFormatting.RED)));
            return false;
        }

        //check pylons
        for (BlockPos coords : PYLON_LOCATIONS) {
            BlockPos pos_ = pos.add(coords);

            IBlockState state = world.getBlockState(pos_);
            if (state.getBlock() != ModBlocks.pylon
                    || state.getValue(BotaniaStateProps.PYLON_VARIANT) != PylonVariant.GAIA) {
                if (!world.isRemote)
                    player.sendMessage(new TextComponentTranslation("botaniamisc.needsCatalysts")
                            .setStyle(new Style().setColor(TextFormatting.RED)));
                return false;
            }
        }

        //check arena shape
        List<BlockPos> invalidArenaBlocks = checkArena(world, pos);
        if (!invalidArenaBlocks.isEmpty()) {
            if (world.isRemote) {
                Botania.proxy.setWispFXDepthTest(false);
                for (BlockPos pos_ : invalidArenaBlocks)
                    Botania.proxy.wispFX(pos_.getX() + 0.5, pos_.getY() + 0.5, pos_.getZ() + 0.5, 1F, 0.2F, 0.2F,
                            0.5F, 0F, 8);
                Botania.proxy.setWispFXDepthTest(true);
            } else {
                PacketHandler.sendTo((EntityPlayerMP) player, new PacketBotaniaEffect(
                        PacketBotaniaEffect.EffectType.ARENA_INDICATOR, pos.getX(), pos.getY(), pos.getZ()));

                player.sendMessage(new TextComponentTranslation("botaniamisc.badArena")
                        .setStyle(new Style().setColor(TextFormatting.RED)));
            }

            return false;
        }

        //all checks ok, spawn the boss
        if (!world.isRemote) {
            stack.shrink(1);

            EntityDoppleganger e = new EntityDoppleganger(world);
            e.setPosition(pos.getX() + 0.5, pos.getY() + 3, pos.getZ() + 0.5);
            e.setInvulTime(SPAWN_TICKS);
            e.setHealth(1F);
            e.source = pos;
            e.mobSpawnTicks = MOB_SPAWN_TICKS;
            e.hardMode = hard;

            int playerCount = e.getPlayersAround().size();
            e.playerCount = playerCount;
            e.getAttributeMap().getAttributeInstance(SharedMonsterAttributes.MAX_HEALTH)
                    .setBaseValue(MAX_HP * playerCount);
            if (hard)
                e.getAttributeMap().getAttributeInstance(SharedMonsterAttributes.ARMOR).setBaseValue(15);

            e.playSound(SoundEvents.ENTITY_ENDERDRAGON_GROWL, 10F, 0.1F);
            e.onInitialSpawn(world.getDifficultyForLocation(new BlockPos(e)), null);
            world.spawnEntity(e);
        }

        return true;
    }

    private static List<BlockPos> checkArena(World world, BlockPos beaconPos) {
        List<BlockPos> trippedPositions = new ArrayList<>();
        int range = (int) Math.ceil(ARENA_RANGE);
        BlockPos pos;

        for (int x = -range; x <= range; x++)
            for (int z = -range; z <= range; z++) {
                if (Math.abs(x) == 4 && Math.abs(z) == 4 || vazkii.botania.common.core.helper.MathHelper
                        .pointDistancePlane(x, z, 0, 0) > ARENA_RANGE)
                    continue; // Ignore pylons and out of circle

                for (int y = -1; y <= ARENA_HEIGHT; y++) {
                    if (x == 0 && y == 0 && z == 0)
                        continue; //this is the beacon

                    pos = beaconPos.add(x, y, z);

                    boolean expectedBlockHere = y == -1; //the floor
                    boolean isBlockHere = world.getBlockState(pos).getCollisionBoundingBox(world, pos) != null;

                    if (expectedBlockHere != isBlockHere) {
                        trippedPositions.add(pos);
                    }
                }
            }

        return trippedPositions;
    }

    @Override
    protected void initEntityAI() {
        tasks.addTask(0, new EntityAISwimming(this));
        tasks.addTask(1, new EntityAIWatchClosest(this, EntityPlayer.class, ARENA_RANGE * 1.5F));
    }

    @Override
    protected void entityInit() {
        super.entityInit();
        dataManager.register(INVUL_TIME, 0);
    }

    public int getInvulTime() {
        return dataManager.get(INVUL_TIME);
    }

    public BlockPos getSource() {
        return source;
    }

    public void setInvulTime(int time) {
        dataManager.set(INVUL_TIME, time);
    }

    @Override
    public void writeEntityToNBT(NBTTagCompound cmp) {
        super.writeEntityToNBT(cmp);
        cmp.setInteger(TAG_INVUL_TIME, getInvulTime());
        cmp.setBoolean(TAG_AGGRO, aggro);
        cmp.setInteger(TAG_MOB_SPAWN_TICKS, mobSpawnTicks);

        cmp.setInteger(TAG_SOURCE_X, source.getX());
        cmp.setInteger(TAG_SOURCE_Y, source.getY());
        cmp.setInteger(TAG_SOURCE_Z, source.getZ());

        cmp.setBoolean(TAG_HARD_MODE, hardMode);
        cmp.setInteger(TAG_PLAYER_COUNT, playerCount);
    }

    @Override
    public void readEntityFromNBT(NBTTagCompound cmp) {
        super.readEntityFromNBT(cmp);
        setInvulTime(cmp.getInteger(TAG_INVUL_TIME));
        aggro = cmp.getBoolean(TAG_AGGRO);
        mobSpawnTicks = cmp.getInteger(TAG_MOB_SPAWN_TICKS);

        int x = cmp.getInteger(TAG_SOURCE_X);
        int y = cmp.getInteger(TAG_SOURCE_Y);
        int z = cmp.getInteger(TAG_SOURCE_Z);
        source = new BlockPos(x, y, z);

        hardMode = cmp.getBoolean(TAG_HARD_MODE);
        if (cmp.hasKey(TAG_PLAYER_COUNT))
            playerCount = cmp.getInteger(TAG_PLAYER_COUNT);
        else
            playerCount = 1;

        if (this.hasCustomName()) {
            this.bossInfo.setName(this.getDisplayName());
        }
    }

    @Override
    public void setCustomNameTag(@Nonnull String name) {
        super.setCustomNameTag(name);
        this.bossInfo.setName(this.getDisplayName());
    }

    @Override
    public void heal(float amount) {
        if (getInvulTime() == 0) {
            super.heal(amount);
        }
    }

    @Override
    public void onKillCommand() {
        this.setHealth(0.0F);
    }

    @Override
    public boolean attackEntityFrom(@Nonnull DamageSource source, float par2) {
        Entity e = source.getTrueSource();
        if (e instanceof EntityPlayer && isTruePlayer(e) && getInvulTime() == 0) {
            EntityPlayer player = (EntityPlayer) e;

            if (!playersWhoAttacked.contains(player.getUniqueID()))
                playersWhoAttacked.add(player.getUniqueID());

            int cap = 25;
            return super.attackEntityFrom(source, Math.min(cap, par2));
        }

        return false;
    }

    private static final Pattern FAKE_PLAYER_PATTERN = Pattern.compile("^(?:\\[.*\\])|(?:ComputerCraft)$");

    public static boolean isTruePlayer(Entity e) {
        if (!(e instanceof EntityPlayer))
            return false;

        EntityPlayer player = (EntityPlayer) e;

        String name = player.getName();
        return !(player instanceof FakePlayer || FAKE_PLAYER_PATTERN.matcher(name).matches());
    }

    @Override
    protected void damageEntity(@Nonnull DamageSource source, float par2) {
        super.damageEntity(source, par2);

        Entity attacker = source.getImmediateSource();
        if (attacker != null) {
            Vector3 thisVector = Vector3.fromEntityCenter(this);
            Vector3 playerVector = Vector3.fromEntityCenter(attacker);
            Vector3 motionVector = thisVector.subtract(playerVector).normalize().multiply(0.75);

            if (getHealth() > 0) {
                motionX = -motionVector.x;
                motionY = 0.5;
                motionZ = -motionVector.z;
                tpDelay = 4;
                spawnPixies = aggro;
            }

            aggro = true;
        }
    }

    @Override
    public void onDeath(@Nonnull DamageSource source) {
        super.onDeath(source);
        EntityLivingBase entitylivingbase = getAttackingEntity();
        if (entitylivingbase instanceof EntityPlayerMP && !anyWithArmor) {
            DopplegangerNoArmorTrigger.INSTANCE.trigger((EntityPlayerMP) entitylivingbase, this, source);
        }

        playSound(SoundEvents.ENTITY_GENERIC_EXPLODE, 20F,
                (1F + (world.rand.nextFloat() - world.rand.nextFloat()) * 0.2F) * 0.7F);
        world.spawnParticle(EnumParticleTypes.EXPLOSION_HUGE, posX, posY, posZ, 1D, 0D, 0D);
    }

    @Override
    protected void applyEntityAttributes() {
        super.applyEntityAttributes();
        getEntityAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).setBaseValue(0.4);
        getEntityAttribute(SharedMonsterAttributes.MAX_HEALTH).setBaseValue(MAX_HP);
        getEntityAttribute(SharedMonsterAttributes.KNOCKBACK_RESISTANCE).setBaseValue(1.0);
    }

    @Override
    protected boolean canDespawn() {
        return false;
    }

    @Override
    public ResourceLocation getLootTable() {
        return new ResourceLocation(LibMisc.MOD_ID, hardMode ? "gaia_guardian_2" : "gaia_guardian");
    }

    @Override
    protected void dropLoot(boolean wasRecentlyHit, int lootingModifier, @Nonnull DamageSource source) {
        // Save true killer, they get extra loot
        if (wasRecentlyHit && source.getTrueSource() instanceof EntityPlayer) {
            trueKiller = (EntityPlayer) source.getTrueSource();
        }

        // Drop equipment and clear it so multiple calls to super don't do it again
        super.dropEquipment(wasRecentlyHit, lootingModifier);

        for (EntityEquipmentSlot e : EntityEquipmentSlot.values()) {
            setItemStackToSlot(e, ItemStack.EMPTY);
        }

        // Generate loot table for every single attacking player
        for (UUID u : playersWhoAttacked) {
            EntityPlayer player = world.getPlayerEntityByUUID(u);
            if (player == null)
                continue;

            EntityPlayer saveLastAttacker = attackingPlayer;
            double savePosX = posX;
            double savePosY = posY;
            double savePosZ = posZ;

            attackingPlayer = player; // Fake attacking player as the killer
            posX = player.posX; // Spoof pos so drops spawn at the player
            posY = player.posY;
            posZ = player.posZ;
            super.dropLoot(wasRecentlyHit, lootingModifier, DamageSource.causePlayerDamage(player));
            posX = savePosX;
            posY = savePosY;
            posZ = savePosZ;
            attackingPlayer = saveLastAttacker;
        }

        trueKiller = null;
    }

    @Override
    public void setDead() {
        if (world.isRemote) {
            Botania.proxy.removeBoss(this);
        }
        super.setDead();
    }

    public List<EntityPlayer> getPlayersAround() {
        float range = 15F;
        return world.getEntitiesWithinAABB(EntityPlayer.class,
                new AxisAlignedBB(source.getX() + 0.5 - range, source.getY() + 0.5 - range,
                        source.getZ() + 0.5 - range, source.getX() + 0.5 + range, source.getY() + 0.5 + range,
                        source.getZ() + 0.5 + range),
                player -> isTruePlayer(player) && !player.isSpectator());
    }

    private static int countGaiaGuardiansAround(World world, BlockPos source) {
        float range = 15F;
        List l = world.getEntitiesWithinAABB(EntityDoppleganger.class,
                new AxisAlignedBB(source.getX() + 0.5 - range, source.getY() + 0.5 - range,
                        source.getZ() + 0.5 - range, source.getX() + 0.5 + range, source.getY() + 0.5 + range,
                        source.getZ() + 0.5 + range));
        return l.size();
    }

    private void particles() {
        for (int i = 0; i < 360; i += 8) {
            float r = 0.6F;
            float g = 0F;
            float b = 0.2F;
            float m = 0.15F;
            float mv = 0.35F;

            float rad = i * (float) Math.PI / 180F;
            double x = source.getX() + 0.5 - Math.cos(rad) * ARENA_RANGE;
            double y = source.getY() + 0.5;
            double z = source.getZ() + 0.5 - Math.sin(rad) * ARENA_RANGE;

            Botania.proxy.wispFX(x, y, z, r, g, b, 0.5F, (float) (Math.random() - 0.5F) * m,
                    (float) (Math.random() - 0.5F) * mv, (float) (Math.random() - 0.5F) * m);
        }

        if (getInvulTime() > 10) {
            Vector3 pos = Vector3.fromEntityCenter(this).subtract(new Vector3(0, 0.2, 0));
            for (BlockPos arr : PYLON_LOCATIONS) {
                Vector3 pylonPos = new Vector3(source.getX() + arr.getX(), source.getY() + arr.getY(),
                        source.getZ() + arr.getZ());
                double worldTime = ticksExisted;
                worldTime /= 5;

                float rad = 0.75F + (float) Math.random() * 0.05F;
                double xp = pylonPos.x + 0.5 + Math.cos(worldTime) * rad;
                double zp = pylonPos.z + 0.5 + Math.sin(worldTime) * rad;

                Vector3 partPos = new Vector3(xp, pylonPos.y, zp);
                Vector3 mot = pos.subtract(partPos).multiply(0.04);

                float r = 0.7F + (float) Math.random() * 0.3F;
                float g = (float) Math.random() * 0.3F;
                float b = 0.7F + (float) Math.random() * 0.3F;

                Botania.proxy.wispFX(partPos.x, partPos.y, partPos.z, r, g, b, 0.25F + (float) Math.random() * 0.1F,
                        -0.075F - (float) Math.random() * 0.015F);
                Botania.proxy.wispFX(partPos.x, partPos.y, partPos.z, r, g, b, 0.4F, (float) mot.x, (float) mot.y,
                        (float) mot.z);
            }
        }
    }

    private void smashBlocksAround(int centerX, int centerY, int centerZ, int radius) {
        for (int dx = -radius; dx <= radius; dx++)
            for (int dy = -radius; dy <= radius + 1; dy++)
                for (int dz = -radius; dz <= radius; dz++) {
                    int x = centerX + dx;
                    int y = centerY + dy;
                    int z = centerZ + dz;

                    BlockPos pos = new BlockPos(x, y, z);
                    IBlockState state = world.getBlockState(pos);
                    Block block = state.getBlock();

                    if (state.getBlockHardness(world, pos) == -1)
                        continue;

                    if (CHEATY_BLOCKS.contains(block.getRegistryName())) {
                        world.destroyBlock(pos, true);
                    } else {
                        //don't break blacklisted blocks
                        if (BotaniaAPI.gaiaBreakBlacklist.contains(block))
                            continue;
                        //don't break the floor
                        if (y == source.getY() - 1)
                            continue;
                        //don't break blocks in pylon columns
                        if (Math.abs(source.getX() - x) == 4 && Math.abs(source.getZ() - z) == 4)
                            continue;

                        world.destroyBlock(pos, true);
                    }
                }
    }

    private void clearPotions(EntityPlayer player) {
        int posXInt = MathHelper.floor(posX);
        int posZInt = MathHelper.floor(posZ);

        List<Potion> potionsToRemove = player.getActivePotionEffects().stream()
                .filter(effect -> effect.getDuration() < 160 && effect.getIsAmbient()
                        && !effect.getPotion().isBadEffect())
                .map(PotionEffect::getPotion).distinct().collect(Collectors.toList());

        potionsToRemove.forEach(potion -> {
            player.removePotionEffect(potion);
            ((WorldServer) world).getPlayerChunkMap().getEntry(posXInt >> 4, posZInt >> 4)
                    .sendPacket(new SPacketRemoveEntityEffect(player.getEntityId(), potion));
        });
    }

    private void keepInsideArena(EntityPlayer player) {
        if (vazkii.botania.common.core.helper.MathHelper.pointDistanceSpace(player.posX, player.posY, player.posZ,
                source.getX() + 0.5, source.getY() + 0.5, source.getZ() + 0.5) >= ARENA_RANGE) {
            Vector3 sourceVector = new Vector3(source.getX() + 0.5, source.getY() + 0.5, source.getZ() + 0.5);
            Vector3 playerVector = Vector3.fromEntityCenter(player);
            Vector3 motion = sourceVector.subtract(playerVector).normalize();

            player.motionX = motion.x;
            player.motionY = 0.2;
            player.motionZ = motion.z;
            player.velocityChanged = true;
        }
    }

    private void spawnMobs(List<EntityPlayer> players) {
        for (int pl = 0; pl < playerCount; pl++) {
            for (int i = 0; i < 3 + world.rand.nextInt(2); i++) {
                EntityLiving entity = null;
                switch (world.rand.nextInt(2)) {
                case 0: {
                    entity = new EntityZombie(world);
                    if (world.rand.nextInt(hardMode ? 3 : 12) == 0) {
                        entity = new EntityWitch(world);
                    }
                    break;
                }
                case 1: {
                    entity = new EntitySkeleton(world);
                    if (world.rand.nextInt(8) == 0) {
                        entity = new EntityWitherSkeleton(world);
                    }
                    break;
                }
                case 3: {
                    if (!players.isEmpty()) {
                        for (int j = 0; j < 1 + world.rand.nextInt(hardMode ? 8 : 5); j++) {
                            EntityPixie pixie = new EntityPixie(world);
                            pixie.setProps(players.get(rand.nextInt(players.size())), this, 1, 8);
                            pixie.setPosition(posX + width / 2, posY + 2, posZ + width / 2);
                            pixie.onInitialSpawn(world.getDifficultyForLocation(new BlockPos(pixie)), null);
                            world.spawnEntity(pixie);
                        }
                    }
                    break;
                }
                }

                if (entity != null) {
                    if (!entity.isImmuneToFire())
                        entity.addPotionEffect(new PotionEffect(MobEffects.FIRE_RESISTANCE, 600, 0));
                    float range = 6F;
                    entity.setPosition(posX + 0.5 + Math.random() * range - range / 2, posY - 1,
                            posZ + 0.5 + Math.random() * range - range / 2);
                    entity.onInitialSpawn(world.getDifficultyForLocation(new BlockPos(entity)), null);
                    if (entity instanceof EntityWitherSkeleton && hardMode) {
                        entity.setItemStackToSlot(EntityEquipmentSlot.MAINHAND,
                                new ItemStack(ModItems.elementiumSword));
                    }
                    world.spawnEntity(entity);
                }
            }
        }
    }

    @Override
    public void onLivingUpdate() {
        super.onLivingUpdate();

        int invul = getInvulTime();

        if (world.isRemote) {
            particles();
            EntityPlayer player = Botania.proxy.getClientPlayer();
            if (getPlayersAround().contains(player))
                player.capabilities.isFlying &= player.capabilities.isCreativeMode;
            return;
        }

        bossInfo.setPercent(getHealth() / getMaxHealth());

        if (isRiding())
            dismountRidingEntity();

        if (world.getDifficulty() == EnumDifficulty.PEACEFUL)
            setDead();

        smashBlocksAround(MathHelper.floor(posX), MathHelper.floor(posY), MathHelper.floor(posZ), 1);

        List<EntityPlayer> players = getPlayersAround();

        if (players.isEmpty() && !world.playerEntities.isEmpty())
            setDead();
        else {
            for (EntityPlayer player : players) {
                for (EntityEquipmentSlot e : EntityEquipmentSlot.values()) {
                    if (e.getSlotType() == EntityEquipmentSlot.Type.ARMOR
                            && !player.getItemStackFromSlot(e).isEmpty()) {
                        anyWithArmor = true;
                        break;
                    }
                }

                //also see SleepingHandler
                if (player.isPlayerSleeping())
                    player.wakeUpPlayer(true, true, false);

                clearPotions(player);
                keepInsideArena(player);
                player.capabilities.isFlying &= player.capabilities.isCreativeMode;
            }
        }

        if (isDead)
            return;

        boolean spawnMissiles = hardMode && ticksExisted % 15 < 4;

        if (invul > 0 && mobSpawnTicks == MOB_SPAWN_TICKS) {
            if (invul < SPAWN_TICKS) {
                if (invul > SPAWN_TICKS / 2 && world.rand.nextInt(SPAWN_TICKS - invul + 1) == 0)
                    for (int i = 0; i < 2; i++)
                        spawnExplosionParticle();
            }

            setHealth(getHealth() + (getMaxHealth() - 1F) / SPAWN_TICKS);
            setInvulTime(invul - 1);

            motionY = 0;
        } else {
            if (aggro) {
                boolean dying = getHealth() / getMaxHealth() < 0.2;
                if (dying && mobSpawnTicks > 0) {
                    motionX = 0;
                    motionY = 0;
                    motionZ = 0;

                    int reverseTicks = MOB_SPAWN_TICKS - mobSpawnTicks;
                    if (reverseTicks < MOB_SPAWN_START_TICKS) {
                        motionY = 0.2;
                        setInvulTime(invul + 1);
                    }

                    if (reverseTicks > MOB_SPAWN_START_TICKS * 2 && mobSpawnTicks > MOB_SPAWN_END_TICKS
                            && mobSpawnTicks % MOB_SPAWN_WAVE_TIME == 0) {
                        spawnMobs(players);

                        if (hardMode && ticksExisted % 3 < 2) {
                            for (int i = 0; i < playerCount; i++)
                                spawnMissile();
                            spawnMissiles = false;
                        }
                    }

                    mobSpawnTicks--;
                    tpDelay = 10;
                } else if (tpDelay > 0) {
                    if (invul > 0)
                        setInvulTime(invul - 1);

                    tpDelay--;
                    if (tpDelay == 0 && getHealth() > 0) {
                        teleportRandomly();

                        if (spawnLandmines) {
                            int count = dying && hardMode ? 7 : 6;
                            for (int i = 0; i < count; i++) {
                                int x = source.getX() - 10 + rand.nextInt(20);
                                int y = (int) players.get(rand.nextInt(players.size())).posY;
                                int z = source.getZ() - 10 + rand.nextInt(20);

                                EntityMagicLandmine landmine = new EntityMagicLandmine(world);
                                landmine.setPosition(x + 0.5, y, z + 0.5);
                                landmine.summoner = this;
                                world.spawnEntity(landmine);
                            }

                        }

                        if (!players.isEmpty())
                            for (int pl = 0; pl < playerCount; pl++)
                                for (int i = 0; i < (spawnPixies ? world.rand.nextInt(hardMode ? 6 : 3) : 1); i++) {
                                    EntityPixie pixie = new EntityPixie(world);
                                    pixie.setProps(players.get(rand.nextInt(players.size())), this, 1, 8);
                                    pixie.setPosition(posX + width / 2, posY + 2, posZ + width / 2);
                                    pixie.onInitialSpawn(world.getDifficultyForLocation(new BlockPos(pixie)), null);
                                    world.spawnEntity(pixie);
                                }

                        tpDelay = hardMode ? dying ? 35 : 45 : dying ? 40 : 60;
                        spawnLandmines = true;
                        spawnPixies = false;
                    }
                }

                if (spawnMissiles)
                    spawnMissile();
            } else {
                if (!players.isEmpty())
                    damageEntity(DamageSource.causePlayerDamage(players.get(0)), 0);
            }
        }
    }

    @Override
    public boolean isNonBoss() {
        return false;
    }

    @Override
    public void addTrackingPlayer(EntityPlayerMP player) {
        super.addTrackingPlayer(player);
        bossInfo.addPlayer(player);
    }

    @Override
    public void removeTrackingPlayer(EntityPlayerMP player) {
        super.removeTrackingPlayer(player);
        bossInfo.removePlayer(player);
    }

    @Override
    protected void collideWithNearbyEntities() {
        if (getInvulTime() == 0)
            super.collideWithNearbyEntities();
    }

    @Override
    public boolean canBePushed() {
        return super.canBePushed() && getInvulTime() == 0;
    }

    private void spawnMissile() {
        EntityMagicMissile missile = new EntityMagicMissile(this, true);
        missile.setPosition(posX + (Math.random() - 0.5 * 0.1), posY + 2.4 + (Math.random() - 0.5 * 0.1),
                posZ + (Math.random() - 0.5 * 0.1));
        if (missile.findTarget()) {
            playSound(ModSounds.missile, 0.6F, 0.8F + (float) Math.random() * 0.2F);
            world.spawnEntity(missile);
        }
    }

    private void teleportRandomly() {
        //choose a location to teleport to
        double oldX = posX, oldY = posY, oldZ = posZ;
        double newX, newY, newZ;
        int tries = 0;

        do {
            newX = source.getX() + (rand.nextDouble() - .5) * ARENA_RANGE;
            newY = source.getY();
            newZ = source.getZ() + (rand.nextDouble() - .5) * ARENA_RANGE;
            tries++;
            //ensure it's inside the arena ring, and not just its bounding square
        } while (tries < 50 && vazkii.botania.common.core.helper.MathHelper.pointDistanceSpace(newX, newY, newZ,
                source.getX(), source.getY(), source.getZ()) > 12);

        if (tries == 50) {
            //failsafe: teleport to the beacon
            newX = source.getX() + .5;
            newY = source.getY() + 1.6;
            newZ = source.getZ() + .5;
        }

        //teleport there
        setPositionAndUpdate(newX, newY, newZ);

        //play sound
        world.playSound(null, oldX, oldY, oldZ, SoundEvents.ENTITY_ENDERMEN_TELEPORT, this.getSoundCategory(), 1.0F,
                1.0F);
        this.playSound(SoundEvents.ENTITY_ENDERMEN_TELEPORT, 1.0F, 1.0F);

        Random random = getRNG();

        //spawn particles along the path
        int particleCount = 128;
        for (int i = 0; i < particleCount; ++i) {
            double progress = i / (double) (particleCount - 1);
            float vx = (random.nextFloat() - 0.5F) * 0.2F;
            float vy = (random.nextFloat() - 0.5F) * 0.2F;
            float vz = (random.nextFloat() - 0.5F) * 0.2F;
            double px = oldX + (newX - oldX) * progress + (random.nextDouble() - 0.5D) * width * 2.0D;
            double py = oldY + (newY - oldY) * progress + random.nextDouble() * height;
            double pz = oldZ + (newZ - oldZ) * progress + (random.nextDouble() - 0.5D) * width * 2.0D;
            world.spawnParticle(EnumParticleTypes.PORTAL, px, py, pz, vx, vy, vz);
        }

        Vec3d oldPosVec = new Vec3d(oldX, oldY + height / 2, oldZ);
        Vec3d newPosVec = new Vec3d(newX, newY + height / 2, newZ);

        if (oldPosVec.squareDistanceTo(newPosVec) > 1) {
            //damage players in the path of the teleport
            for (EntityPlayer player : getPlayersAround()) {
                RayTraceResult rtr = player.getEntityBoundingBox().grow(0.25).calculateIntercept(oldPosVec,
                        newPosVec);
                if (rtr != null)
                    player.attackEntityFrom(DamageSource.causeMobDamage(this), 6);
            }

            //break blocks in the path of the teleport
            int breakSteps = (int) oldPosVec.distanceTo(newPosVec);
            if (breakSteps >= 2) {
                for (int i = 0; i < breakSteps; i++) {
                    float progress = i / (float) (breakSteps - 1);
                    int breakX = MathHelper.floor(oldX + (newX - oldX) * progress);
                    int breakY = MathHelper.floor(oldY + (newY - oldY) * progress);
                    int breakZ = MathHelper.floor(oldZ + (newZ - oldZ) * progress);

                    smashBlocksAround(breakX, breakY, breakZ, 1);
                }
            }
        }
    }

    @Override
    @SideOnly(Side.CLIENT)
    public ResourceLocation getBossBarTexture() {
        return BossBarHandler.defaultBossBar;
    }

    @SideOnly(Side.CLIENT)
    private static Rectangle barRect;
    @SideOnly(Side.CLIENT)
    private static Rectangle hpBarRect;

    @Override
    @SideOnly(Side.CLIENT)
    public Rectangle getBossBarTextureRect() {
        if (barRect == null)
            barRect = new Rectangle(0, 0, 185, 15);
        return barRect;
    }

    @Override
    @SideOnly(Side.CLIENT)
    public Rectangle getBossBarHPTextureRect() {
        if (hpBarRect == null)
            hpBarRect = new Rectangle(0, barRect.y + barRect.height, 181, 7);
        return hpBarRect;
    }

    @Override
    @SideOnly(Side.CLIENT)
    public int bossBarRenderCallback(ScaledResolution res, int x, int y) {
        GlStateManager.pushMatrix();
        int px = x + 160;
        int py = y + 12;

        Minecraft mc = Minecraft.getMinecraft();
        ItemStack stack = new ItemStack(Items.SKULL, 1, 3);
        mc.renderEngine.bindTexture(TextureMap.LOCATION_BLOCKS_TEXTURE);
        net.minecraft.client.renderer.RenderHelper.enableGUIStandardItemLighting();
        GlStateManager.enableRescaleNormal();
        mc.getRenderItem().renderItemIntoGUI(stack, px, py);
        net.minecraft.client.renderer.RenderHelper.disableStandardItemLighting();

        boolean unicode = mc.fontRenderer.getUnicodeFlag();
        mc.fontRenderer.setUnicodeFlag(true);
        mc.fontRenderer.drawStringWithShadow("" + playerCount, px + 15, py + 4, 0xFFFFFF);
        mc.fontRenderer.setUnicodeFlag(unicode);
        GlStateManager.popMatrix();

        return 5;
    }

    @Override
    public UUID getBossInfoUuid() {
        return bossInfoUUID;
    }

    @Override
    @SideOnly(Side.CLIENT)
    public int getBossBarShaderProgram(boolean background) {
        return background ? 0 : ShaderHelper.dopplegangerBar;
    }

    @SideOnly(Side.CLIENT)
    private ShaderCallback shaderCallback;

    @Override
    @SideOnly(Side.CLIENT)
    public ShaderCallback getBossBarShaderCallback(boolean background, int shader) {
        if (shaderCallback == null)
            shaderCallback = shader1 -> {
                int grainIntensityUniform = ARBShaderObjects.glGetUniformLocationARB(shader1, "grainIntensity");
                int hpFractUniform = ARBShaderObjects.glGetUniformLocationARB(shader1, "hpFract");

                float time = getInvulTime();
                float grainIntensity = time > 20 ? 1F : Math.max(hardMode ? 0.5F : 0F, time / 20F);

                ARBShaderObjects.glUniform1fARB(grainIntensityUniform, grainIntensity);
                ARBShaderObjects.glUniform1fARB(hpFractUniform, getHealth() / getMaxHealth());
            };

        return background ? null : shaderCallback;
    }

    @Override
    public void writeSpawnData(ByteBuf buffer) {
        buffer.writeInt(playerCount);
        buffer.writeBoolean(hardMode);
        buffer.writeLong(source.toLong());
        buffer.writeLong(bossInfoUUID.getMostSignificantBits());
        buffer.writeLong(bossInfoUUID.getLeastSignificantBits());
    }

    @Override
    @SideOnly(Side.CLIENT)
    public void readSpawnData(ByteBuf additionalData) {
        playerCount = additionalData.readInt();
        hardMode = additionalData.readBoolean();
        source = BlockPos.fromLong(additionalData.readLong());
        long msb = additionalData.readLong();
        long lsb = additionalData.readLong();
        bossInfoUUID = new UUID(msb, lsb);
        Minecraft.getMinecraft().getSoundHandler().playSound(new DopplegangerMusic(this));
    }

    @SideOnly(Side.CLIENT)
    private static class DopplegangerMusic extends MovingSound {
        private final EntityDoppleganger guardian;

        public DopplegangerMusic(EntityDoppleganger guardian) {
            super(guardian.hardMode ? ModSounds.gaiaMusic2 : ModSounds.gaiaMusic1, SoundCategory.RECORDS);
            this.guardian = guardian;
            this.xPosF = guardian.getSource().getX();
            this.yPosF = guardian.getSource().getY();
            this.zPosF = guardian.getSource().getZ();
            this.repeat = true;
        }

        @Override
        public void update() {
            if (!guardian.isEntityAlive()) {
                donePlaying = true;
            }
        }
    }

    private static class BeaconComponent extends MultiblockComponent {

        public BeaconComponent(BlockPos relPos) {
            super(relPos, Blocks.IRON_BLOCK.getDefaultState());
        }

        @Override
        public boolean matches(World world, BlockPos pos) {
            return world.getBlockState(pos).getBlock().isBeaconBase(world, pos,
                    pos.add(new BlockPos(-relPos.getX(), -relPos.getY(), -relPos.getZ())));
        }

    }

    private static class BeaconBeamComponent extends MultiblockComponent {

        public BeaconBeamComponent(BlockPos relPos) {
            super(relPos, Blocks.BEACON.getDefaultState());
        }

        @Override
        public boolean matches(World world, BlockPos pos) {
            return world.getTileEntity(pos) instanceof TileEntityBeacon;
        }
    }
}