private void DrawBigPortal(SpriteBatch spriteBatch, Texture2D texture, BigPortalInfo info)
        {
            if (!info.visible)
            {
                return;
            }

            spriteBatch.Draw(texture, info.center - Main.screenPosition, null, Color.White * info.alpha, info.rotation, texture.Size() / 2f, info.scale, SpriteEffects.None, 0);
        }
        private void SpawnBigPortal(Vector2 center, ref BigPortalInfo info, bool fast = false)
        {
            info.center   = center;
            info.visible  = true;
            info.scale    = 8f / 240f;           //Initial size of 8 pxiels
            info.alpha    = info.scale;
            info.rotation = 0f;
            info.fast     = fast;

            Main.PlaySound(SoundID.Item84.WithVolume(0.9f), info.center);
        }
        public override void AI()
        {
            npc.defense = npc.defDefense;

            if (!spawned)
            {
                //Laugh after a second
                if (AI_Timer == 0)
                {
                    targetZAxisRotation = 0f;
                    zAxisRotation       = 0f;
                    targetAlpha         = 255f;

                    ignoreRetargetPlayer = true;

                    AI_Timer  = 60 + 100;
                    AI_Attack = FadeIn;
                    npc.TargetClosest();

                    npc.netUpdate = true;
                }
                else if (AI_Timer <= 100)
                {
                    spawned     = true;
                    targetAlpha = 0f;

                    Main.PlaySound(SoundID.Zombie, npc.Center, 105);                      //Cultist laugh sound

                    npc.netUpdate = true;
                }
                else
                {
                    targetAlpha -= 255f / 60f;
                }
            }

            //Player is dead/not connected?  Target a new one
            //That player is also dead/not connected?  Begin the "fade away" animation and despawn
            Player player = npc.target >= 0 && npc.target < Main.maxPlayers ? Main.player[npc.target] : null;

            if (!ignoreRetargetPlayer && (npc.target < 0 || npc.target >= Main.maxPlayers || player.dead || !player.active))
            {
                npc.TargetClosest();

                player = npc.target >= 0 && npc.target < Main.maxPlayers ? Main.player[npc.target] : null;

                if (npc.target < 0 || npc.target >= Main.maxPlayers || player.dead || !player.active)
                {
                    //Go away
                    AI_Attack         = FadeAway;
                    AI_Timer          = -1;
                    AI_AttackProgress = -1;

                    hadNoPlayerTargetForLongEnough = true;

                    npc.netUpdate = true;
                }
                else if (hadNoPlayerTargetForLongEnough)
                {
                    hadNoPlayerTargetForLongEnough = false;

                    //Start back in the idle phase
                    SetAttack(Attack_DoNothing);

                    npc.netUpdate = true;
                }
            }

            npc.defense = npc.defDefense;

            switch ((int)AI_Attack)
            {
            case FadeIn:
                //Do the laughing animation
                if (AI_Timer > 0 && AI_Timer <= 100)
                {
                    npc.velocity *= 1f - 3f / 60f;
                }
                else if (AI_Timer <= 0)
                {
                    //Transition to the next subphase
                    SetAttack(Attack_DoNothing);

                    npc.dontTakeDamage   = false;
                    ignoreRetargetPlayer = false;
                }
                else
                {
                    npc.velocity = new Vector2(0, 1.5f);

                    SpawnDusts();
                }

                break;

            case FadeAway:
                npc.velocity *= 1f - 3f / 60f;

                //Spawn dusts as the boss fades away, then despawn it once fully invisible
                SpawnDusts();

                targetAlpha += 255f / 180f;

                if (targetAlpha >= 255)
                {
                    npc.active = false;
                    return;
                }
                break;

            case Attack_DoNothing:
                inertia           = DefaultInertia;
                zAxisLerpStrength = DefaultZAxisLerpStrength;

                FloatTowardsTarget(player);

                if (AI_Timer <= 0)
                {
                    //Prevent the same attack from being rolled twice in a row
                    int attack;
                    do
                    {
                        attack = Main.rand.Next(Attack_SummonMeteors, Attack_ChargeAtPlayer + 1);
                    }while(attack == oldAttack);

                    SetAttack(attack);
                    npc.netUpdate = true;

                    oldAttack = (int)AI_Attack;
                }

                break;

            case Attack_SummonMeteors:
                npc.defense = npc.defDefense + 40;

                //Face forwards
                targetZAxisRotation = 0f;

                npc.velocity *= 1f - 4.67f / 60f;

                if (Math.Abs(npc.velocity.X) < 0.02f)
                {
                    npc.velocity.X = 0f;
                }
                if (Math.Abs(npc.velocity.Y) < 0.02f)
                {
                    npc.velocity.Y = 0f;
                }

                int max = Main.expertMode ? PortalTimerMax : (int)(PortalTimerMax * 1.5f);

                if ((int)AI_Timer == max - 1)
                {
                    Main.PlaySound(SoundID.Zombie, npc.Center, 99);
                }
                else if ((int)AI_Timer == max - 24)
                {
                    //Wait until the animation frame changes to the one facing forwards
                    if (GetAnimationSetFrame() == Animation_LookFront_JawOpen)
                    {
                        Main.PlaySound(SoundID.Zombie, npc.Center, 93);
                        AI_AttackProgress++;
                    }
                    else
                    {
                        AI_Timer++;
                    }
                }

                if (AI_AttackProgress == 1)
                {
                    //Spawn portals near and above the player
                    Vector2 orig = player.Center - new Vector2(0, 15 * 16);

                    int count = Main.expertMode ? 4 : 2;

                    for (int i = 0; i < count; i++)
                    {
                        Vector2 spawn = orig + new Vector2(Main.rand.NextFloat(-1, 1) * 40 * 16, Main.rand.NextFloat(-1, 1) * 6 * 16);

                        Projectile.NewProjectile(spawn, Vector2.Zero, ModContent.ProjectileType <Portal>(), MiscUtils.TrueDamage(Main.expertMode ? 140 : 90), 0f, Main.myPlayer);

                        // TODO: netcode
                    }

                    AI_AttackProgress++;
                }
                else if (AI_AttackProgress == 2 && AI_Timer == 0)
                {
                    SetAttack(Attack_DoNothing);
                }

                break;

            case Attack_SummonLesserDemons:
                FloatTowardsTarget(player);

                if (AI_AttackProgress < 3)
                {
                    if (AI_Timer % 75 == 0)
                    {
                        Vector2 spawn = npc.Center + new Vector2(Main.rand.NextFloat(-1, 1) * 22 * 16, Main.rand.NextFloat(-1, 1) * 10 * 16);

                        NPC.NewNPC((int)spawn.X, (int)spawn.Y, ModContent.NPCType <MiniCraterDemon>(), ai3: npc.whoAmI);

                        //Exhale sound
                        Main.PlaySound(SoundID.Zombie, npc.Center, 93);

                        // TODO: netcode

                        AI_AttackProgress++;
                    }
                }
                else if (AI_Timer <= 0)
                {
                    //Go to next attack immediately
                    SetAttack(Attack_DoNothing);
                }
                else if (CountAliveLesserDemons() == 0)
                {
                    //All the minions have been killed.  Transition to the next subphase immediately
                    AI_Timer = 1;
                }

                break;

            case Attack_ChargeAtPlayer:
                inertia = DefaultInertia * 0.1f;

                float chargeVelocity = Main.expertMode ? 26f : 17f;
                int   repeatRelative = AI_AttackProgress >= Attack_ChargeAtPlayer_RepeatStart
                                                ? ((int)AI_AttackProgress - Attack_ChargeAtPlayer_RepeatStart) % Attack_ChargeAtPlayer_RepeatSubphaseCount
                                                : -1;
                int repeatCount = AI_AttackProgress >= Attack_ChargeAtPlayer_RepeatStart
                                                ? ((int)AI_AttackProgress - Attack_ChargeAtPlayer_RepeatStart) / Attack_ChargeAtPlayer_RepeatSubphaseCount
                                                : 0;

                if (AI_AttackProgress == 0)
                {
                    if (bigPortal.visible)
                    {
                        bigPortal = new BigPortalInfo();
                    }
                    if (bigPortal2.visible)
                    {
                        bigPortal2 = new BigPortalInfo();
                    }

                    //Wait until initial wait is done
                    if (AI_Timer <= 0)
                    {
                        AI_AttackProgress++;

                        //Spawn portal -- must be close to boss and player
                        Vector2     spawn;
                        const float playerDistMax = 40 * 16;
                        int         tries         = 0;
                        bool        success       = false;
                        do
                        {
                            spawn = npc.Center + Main.rand.NextVector2Unit() * 40 * 16;
                            tries++;
                        }while(tries < 1000 && (success = player.DistanceSQ(spawn) >= playerDistMax * playerDistMax));

                        if (!success && tries == 1000)
                        {
                            //Failsafe: put the portal directly on the target player
                            spawn = player.Center;
                        }

                        SpawnBigPortal(spawn, ref bigPortal);
                    }
                }
                else if (AI_AttackProgress == 1)
                {
                    float       dist       = npc.DistanceSQ(bigPortal.center);
                    const float portalDist = 5;
                    bool        tooFar     = dist > portalDist * portalDist;

                    //Update the portal
                    if (bigPortal.scale > 0.98f)
                    {
                        bigPortal.scale = 1f;
                        bigPortal.alpha = 1f;
                    }
                    else
                    {
                        bigPortal.scale = MiscUtils.ScaleLogarithmic(bigPortal.scale, 1f, 2.7219f, 1f / 60f);
                        bigPortal.alpha = bigPortal.scale;
                    }

                    if (tooFar)
                    {
                        //Float toward first portal
                        movementTarget = bigPortal.center;

                        FloatTowardsTarget(player, minimumDistanceThreshold: 0);
                    }
                    else
                    {
                        npc.Center     = bigPortal.center;
                        npc.velocity   = Vector2.Zero;
                        movementTarget = null;

                        //If the portal hasn't gotten to the full size yet, wait for it to do so
                        if (bigPortal.scale >= 1f)
                        {
                            AI_AttackProgress++;

                            UpdateScale(1f);

                            AI_Timer = 45;
                        }
                    }
                }
                else if (repeatRelative == 0)
                {
                    //Shrink (factor of 4.3851 makes it reach 0.01 in around 60 ticks, 12.2753 ~= 20 ticks)
                    const float epsilon = 0.01f;
                    if (npc.scale > epsilon)
                    {
                        UpdateScale(MiscUtils.ScaleLogarithmic(npc.scale, 0f, repeatCount == 0 ? 4.3851f : 12.2753f, 1f / 60f));
                    }

                    targetAlpha = 255f * (1f - npc.scale);

                    if (AI_Timer <= 0)
                    {
                        //Shrink the portal, but a bit slower
                        bigPortal.scale = MiscUtils.ScaleLogarithmic(bigPortal.scale, 0f, repeatCount == 0 ? 2.7219f : 9.2153f, 1f / 60f);
                        bigPortal.alpha = bigPortal.scale;
                    }

                    if (npc.scale < 0.4f)
                    {
                        hideMapIcon        = true;
                        npc.dontTakeDamage = true;
                    }

                    if (npc.scale < epsilon)
                    {
                        UpdateScale(epsilon);

                        targetAlpha = 255f;

                        //Sanity check
                        targetZAxisRotation = 0;
                        zAxisRotation       = 0;
                    }

                    if (bigPortal.scale < epsilon)
                    {
                        bigPortal.scale   = 0f;
                        bigPortal.visible = false;

                        AI_AttackProgress++;

                        if (repeatCount == 0)
                        {
                            Main.PlaySound(SoundID.Zombie, npc.Center, 105);                                      //Cultist laugh sound
                        }
                        //Wait for a random amount of time
                        AI_Timer      = Main.expertMode ? Main.rand.Next(40, 90 + 1) : Main.rand.Next(100, 220 + 1);
                        npc.netUpdate = true;

                        movementTarget = null;
                    }
                }
                else if (repeatRelative == 1)
                {
                    //Wait, then spawn a portal
                    if (AI_Timer <= 0)
                    {
                        Vector2 offset = Main.rand.NextVector2Unit() * 30 * 16;

                        //Second portal is where the boss will end up
                        SpawnBigPortal(player.Center + offset, ref bigPortal, fast: true);
                        SpawnBigPortal(player.Center - offset, ref bigPortal2, fast: true);
                        bigPortal2.visible = false;

                        npc.Center   = bigPortal.center;
                        npc.velocity = Vector2.Zero;

                        AI_AttackProgress++;
                    }
                }
                else if (repeatRelative == 2)
                {
                    //Make the portal grow FAST (9.9583 results in around 26 ticks)
                    bigPortal.scale = MiscUtils.ScaleLogarithmic(bigPortal.scale, 1f, 9.9583f, 1f / 60f);
                    bigPortal.alpha = bigPortal.scale;

                    if (bigPortal.scale >= 0.99f)
                    {
                        bigPortal.scale = 1f;

                        AI_AttackProgress++;
                        AI_Timer = 40;
                    }

                    bigPortal.alpha = bigPortal.scale;
                }
                else if (repeatRelative == 3)
                {
                    //Make the boss charge at the player after fading in
                    zAxisLerpStrength = DefaultZAxisLerpStrength * 2.7f;

                    //15.7025 ~= 16 ticks to go from 0.01 to 1
                    if (npc.scale < 0.99f)
                    {
                        UpdateScale(MiscUtils.ScaleLogarithmic(npc.scale, 1, 15.7025f, 1f / 60f));
                    }
                    else
                    {
                        UpdateScale(1f);
                    }

                    if (npc.scale > 0.4f)
                    {
                        npc.dontTakeDamage = false;
                        hideMapIcon        = false;
                    }

                    targetAlpha = 255f * (1f - npc.scale);

                    SetTargetZAxisRotation(player, out _);

                    if (AI_Timer <= 0 && npc.scale == 1f)
                    {
                        AI_AttackProgress++;
                        AI_Timer = Main.expertMode ? 30 : 60;

                        npc.Center   = bigPortal.center;
                        npc.velocity = npc.DirectionTo(player.Center) * chargeVelocity;
                        Main.PlaySound(SoundID.ForceRoar, (int)npc.Center.X, (int)npc.Center.Y, -1);

                        if (repeatCount >= (Main.expertMode ? Main.rand.Next(5, 8) : Main.rand.Next(2, 5)))
                        {
                            //Stop the repetition
                            bigPortal2.visible = false;
                            bigPortal2.scale   = 8f / 240f;

                            SetAttack(Attack_PostCharge);
                        }
                    }
                }
                else if (repeatRelative == 4)
                {
                    //Second portal appears once the boss is within 22 update ticks of its center
                    float activeDist = chargeVelocity * 22;
                    if (AI_Timer < 0 && npc.DistanceSQ(bigPortal2.center) <= activeDist * activeDist)
                    {
                        bigPortal2.visible = true;
                    }

                    //First portal disappears once the boss leaves within 22 update ticks of its center
                    if (npc.DistanceSQ(bigPortal.center) > activeDist * activeDist)
                    {
                        bigPortal.scale = MiscUtils.ScaleLogarithmic(bigPortal.scale, 0f, 15.2753f, 1f / 60f);
                        bigPortal.alpha = bigPortal.scale;

                        if (bigPortal.scale <= 0.01f)
                        {
                            bigPortal.scale   = 0f;
                            bigPortal.alpha   = 0f;
                            bigPortal.visible = false;
                        }
                    }

                    if (bigPortal2.visible)
                    {
                        bigPortal2.scale = MiscUtils.ScaleLogarithmic(bigPortal2.scale, 1f, 15.2753f, 1f / 60f);
                        bigPortal2.alpha = bigPortal2.scale;

                        if (bigPortal2.scale >= 0.99f)
                        {
                            bigPortal2.scale = 1f;
                            bigPortal2.alpha = 1f;
                        }
                    }

                    const float portalEnterDist = 5, portalTargetDist = 4 * 16;

                    if (AI_Timer >= 0)
                    {
                        if (AI_Timer == 0)
                        {
                            bigPortal2.center = npc.Center + Vector2.Normalize(npc.velocity) * (activeDist + 3 * 16);
                        }
                    }
                    else
                    {
                        //Make sure the boss snaps to the center of the portal before repeating the logic
                        float dist = npc.DistanceSQ(bigPortal2.center);
                        if (dist < portalEnterDist * portalEnterDist)
                        {
                            UpdateScale(1f);

                            npc.Center   = bigPortal2.center;
                            npc.velocity = Vector2.Zero;

                            targetAlpha = 0;

                            movementTarget = null;

                            if (bigPortal2.scale >= 1f)
                            {
                                AI_AttackProgress++;

                                Utils.Swap(ref bigPortal, ref bigPortal2);
                            }
                        }
                        else if (dist < portalTargetDist * portalTargetDist)
                        {
                            //Float to the center
                            movementTarget = bigPortal2.center;
                            inertia        = DefaultInertia * 0.1f;

                            float oldZ = targetZAxisRotation;
                            FloatTowardsTarget(player, minimumDistanceThreshold: 0);
                            targetZAxisRotation = oldZ;
                        }
                    }
                }
                break;

            case Attack_PostCharge:
                if (npc.velocity == Vector2.Zero && !bigPortal.visible)
                {
                    SetAttack(Attack_DoNothing);
                }
                else if (AI_Timer <= 0)
                {
                    //Charge has ended.  Make the portal fade away and slow the boss down
                    npc.velocity *= 1f - 8.5f / 60f;

                    //5.9192 ~= 45 ticks to reach 0.01 scale
                    bigPortal.scale = MiscUtils.ScaleLogarithmic(bigPortal.scale, 0f, 5.9192f, 1f / 60f);

                    if (Math.Abs(npc.velocity.X) < 0.05f && Math.Abs(npc.velocity.Y) <= 0.05f)
                    {
                        npc.velocity = Vector2.Zero;
                    }

                    if (bigPortal.scale <= 0.01f)
                    {
                        bigPortal = new BigPortalInfo();
                    }
                }
                break;
            }

            AI_Timer--;
            AI_AnimationCounter++;

            if (AI_Attack != FadeAway && targetAlpha > 0)
            {
                targetAlpha -= 255f / 60f;

                if (targetAlpha < 0)
                {
                    targetAlpha = 0;
                }
            }

            npc.alpha = (int)targetAlpha;

            if (Math.Abs(zAxisRotation - targetZAxisRotation) < 0.02f)
            {
                zAxisRotation = targetZAxisRotation;
            }
            else
            {
                zAxisRotation = MathHelper.Lerp(zAxisRotation, targetZAxisRotation, zAxisLerpStrength / 60f);
            }

            //We don't want sprite flipping
            npc.spriteDirection = -1;

            bigPortal.Update();
            bigPortal2.Update();
        }