 //version "4.12.0"
 
// To whoever is curious enough to look through this script,
// Possibly with intent to improve or use it:
// Feel free to do anything you want with it, I totally approve!
// You may also be able to email me at Ann@PoppingSpree.dev
// I make no guarantee that I will see the message, let alone reply to it.

/*
----------
--Credit--
----------
Music:
{
	SHOSTILE: HOSTILE COUNT (No Guns; No Bosses) by by SCALEBEASTS#0777

	SSYNACK2: Survey Sync Acknowledged (No Guns; No Bosses) by by SCALEBEASTS#0777
	SSYNACK: Survey Sync Acknowledged Lite (No Guns; No Bosses) by by SCALEBEASTS#0777

	SBATTLE3: This Is What We Trained For (No Guns; No Bosses) by by SCALEBEASTS#0777
	SBATTLL3: This Is What We Trained For Lite (No Guns; No Bosses) by by SCALEBEASTS#0777

	SBATTLE4: Cover of Hydroelectric Plant. Cover by SCALEBEASTS#0777
	SBATTLL4: d_map01 "Hydroelectric Plant" from FreeDoom Phase 2 https://github.com/freedoom/freedoom
}

Sounds:
SCALEBEASTS#0777
{
	SBCANCEL
}

NeoSpeech Bridget TTS:
{
	SBNEWS
}

ElevenLabs:
{
	KNOCKOFF
	HGH
	HGGH
	CHECKPT
	CHECKMAT
	CHECK
	CHECKSIX
	CANT
	TCH
	OWW
	OWH
	OUCH
	OOF
	NOTHING
	NADA
}

Puzzle Bobble:
{
	PBLAND
	BMNPOPSF
	BMNSQKSF
	BMNHOOKS
}

Super Mario Bros 2: Yoshi's Island:
{
	BMRING
	BMNDASHR
	BMJUMP
	BMSHOOP
}

Super Buster Bros. (SNES):
{
	BMNPOPBL
	BMNDANK
	BMNPOPBO
}

Sound Ideas (Paid):
{
 - Balloon_Creak_Multiple_Long_09
 - Balloon_Warble_Release_Air_Resonate_01
}

Art:
Sinestesia - https://opengameart.org/content/2d-explosion-animations-frame-by-frame

CafeCaboose - Galactic Riot's ending train.

Ann (PoppingSpree.Dev)
{
	Every image in this WAD that isn't credited above. If I included something that shouldn't be there, please poke me.
	Exception - Exit Pic of Allco Mers is illustrated by Ann, but background made with Bing Image Creator.
}
*/


class BMHealthPatch : Health
{
	Default
	{
		Scale 0.4;
		inventory.pickupsound "misc/p_pkup";
		inventory.pickupmessage "Patching up!";
		Health.Lowmessage 35, "Barely keeping it together...";
		inventory.respawntics 2010;
		Inventory.Amount 50;
		Inventory.MaxAmount 200;
		+COUNTITEM;
	}
    
    states
    {
        Spawn:
            WPAT A -1 bright;
            stop;
    }
}

class BMAutoMap : MapRevealer
{
	Default
	{
		inventory.pickupsound "misc/p_pkup";
		inventory.pickupmessage "E-Paper Map!";
		inventory.respawntics 2010;
		+COUNTITEM;
	}
    
    states
    {
        Spawn:
            WMAP A -1 bright;
            stop;
    }
}

// S_Skin

class BMCheckpoint : CustomInventory
{
	Default
	{
		Inventory.pickupsound "BMCHECKP";
		Inventory.pickupmessage "Checkpoint!";
		Radius 128;
		Height 128;
	}
	
    states
    {
        Spawn:
            BMCH A -1 bright;
            stop;
		Pickup:
			TNT1 A 1 bright TimeExtend(30);
            stop;
    }

    action void TimeExtend(int seconds)
    {
        //Console.PrintF("TODO: Checkpoint");
    }
}

class BMTicket2024 : Inventory
{
	Default
	{
        Inventory.Amount 1;
        Inventory.MaxAmount 2;
		Inventory.pickupsound "misc/p_pkup";
		Inventory.pickupmessage "One (1) ticket to the Underground.";
		Radius 64;
		Height 64;
        Scale 0.5;
	}
	
    states
    {
        Spawn:
            ATKT A -1 bright;
            stop;
		Pickup:
            stop;
    }
}
class BMDoomressNoArmor : Inventory
{
    Default
	{
        Inventory.Amount 1;
        Inventory.MaxAmount 1;
        inventory.pickupsound "misc/p_pkup";
        inventory.pickupmessage "A lovely dress! Seems familiar.";
        inventory.respawntics 2010;
        Radius 64;
		Height 64;
        +COUNTITEM;
    }
    states
    {	
        Spawn:
            GBKN B -1 bright;
            stop;
    }
}
class BMDisc : Inventory
{
    Default
	{
        Inventory.Amount 1;
        Inventory.MaxAmount 1;
        inventory.pickupsound "misc/p_pkup";
        inventory.pickupmessage "Some kind of disc? There are snakes printed on the surface.";
        inventory.respawntics 2010;
        Radius 64;
		Height 64;
        Scale 4.0;
        +COUNTITEM;
    }
    states
    {	
        Spawn:
            BMWP Z -1 bright;
            stop;
    }
}

class BMDangerNoodle : Inventory
{
    Default
	{
        Inventory.Amount 1;
        Inventory.MaxAmount 1;
        inventory.pickupsound "misc/p_pkup";
        inventory.pickupmessage "A statue of a Danger Noodle.";
        inventory.respawntics 2010;
        Radius 64;
		Height 64;
        +COUNTITEM;
    }
    states
    {	
        Spawn:
            GRHB S -1 bright;
            stop;
    }
}

class BMDoomkini : Inventory
{
    Default
	{
        inventory.pickupsound "misc/p_pkup";
        inventory.pickupmessage "Pretty clothes!";
        inventory.respawntics 2010;
        +COUNTITEM;
    }
    states
    {	
        Spawn:
            GBKN A -1 bright;
            stop;
    }
}

class BMDoomress : BlueArmor
{
    Default
	{
        inventory.pickupsound "misc/p_pkup";
        inventory.pickupmessage "A lovely dress!";
        inventory.respawntics 2010;
        +COUNTITEM;
    }
    states
    {	
        Spawn:
            GBKN B -1 bright;
            stop;
    }
}

class BMBunBons : HealthBonus
{
    Default
	{
        inventory.pickupsound "misc/p_pkup";
        inventory.pickupmessage "Sweets!";
        inventory.respawntics 2010;
        +COUNTITEM;
    }
    states
    {
        Spawn:
            BUNB A -1 bright;
            stop;
    }
}

class BMRedFlower : Actor
{
	Default
	{
	}
    states
    {
        Spawn:
            BMRF A -1;
            stop;
    }
}

class BMSpawnerTankStinger : Actor
{
	Default
	{
		Tag "Puff Tank: Bee Stinger";
		DropItem "BMHealthPatch";
		Health 100;
		Speed 0;
		+SOLID;
		+SHOOTABLE;
		+NOBLOOD;
		+ACTIVATEMCROSS;
		+DONTGIB;
		+NOICEDEATH;
		+FORCEXYBILLBOARD;
		
		SeeSound "BMNSQKSF";
		PainSound "BMNSQKSF";
		DeathSound "BMNPOPBO";
		//DeathSound "world/barrelx";
		ActiveSound "BMNSQKSF";
	}
    States
    {			
		Spawn:
			BMHT A 10 A_Look;
			Loop;
		See:
			BMHT A 3 A_Chase;
			Loop;
		Missile:
			BMHT A 5 A_FaceTarget;
			BMHT A 5 A_FaceTarget;
			BMHT A 5 BRIGHT A_FaceTarget;
			BMHT A 0 BRIGHT A_PainAttack("BMBeeStinger");
			Goto See;
		Pain:
			BMHT A 6;
			BMHT A 6 A_Pain;
			Goto See;
		Death:
			BMHT A 8 BRIGHT;
			BMHT A 8 BRIGHT A_Scream;
			BMHT B 8 BRIGHT A_PainDie("BMBeeStinger");
			BMHT B 8 BRIGHT;
			Stop;
		Raise:
			BMHT A 8;
			Goto See;
    }
}

class BMSpawnerTankBorkPacon : BMSpawnerTankStinger
{
	Default
	{
		Tag "Puff Tank: Bork Pacon";
		Health 100;
		Speed 0;
		+SOLID;
		+SHOOTABLE;
		+NOBLOOD;
		+ACTIVATEMCROSS;
		+DONTGIB;
		+NOICEDEATH;
		+FORCEXYBILLBOARD;
		
		SeeSound "BMNSQKSF";
		PainSound "BMNSQKSF";
		DeathSound "BMNPOPBO";
		//DeathSound "world/barrelx";
		ActiveSound "BMNSQKSF";
	}
    states
    {
		Spawn:
			BMHT A 10 A_Look;
			Loop;
		See:
			BMHT A 3 A_Chase;
			Loop;
		Missile:
			BMHT A 5 A_FaceTarget;
			BMHT A 5 A_FaceTarget;
			BMHT A 5 BRIGHT A_FaceTarget;
			BMHT A 0 BRIGHT A_PainAttack("BMBorkPaconImp");
			Goto See;
		Pain:
			BMHT A 6;
			BMHT A 6 A_Pain;
			Goto See;
		Death:
			BMHT A 8 BRIGHT;
			BMHT A 8 BRIGHT A_Scream;
			BMHT B 8 BRIGHT A_PainDie("BMBorkPaconImp");
			BMHT B 8 BRIGHT;
			Stop;
		Raise:
			BMHT A 8;
			Goto See;
    }
}

class BMSpawnerTankBouncer : BMSpawnerTankStinger
{
	Default
	{
		Tag "Puff Tank: Bouncer";
		Health 100;
		Speed 0;
		+SOLID;
		+SHOOTABLE;
		+NOBLOOD;
		+ACTIVATEMCROSS;
		+DONTGIB;
		+NOICEDEATH;
		+FORCEXYBILLBOARD;
		
		SeeSound "BMNSQKSF";
		PainSound "BMNSQKSF";
		DeathSound "BMNPOPBO";
		//DeathSound "world/barrelx";
		ActiveSound "BMNSQKSF";
	}
    states
    {			
		Spawn:
			BMHT A 10 A_Look;
			Loop;
		See:
			BMHT A 3 A_Chase;
			Loop;
		Missile:
			BMHT A 5 A_FaceTarget;
			BMHT A 5 A_FaceTarget;
			BMHT A 5 BRIGHT A_FaceTarget;
			BMHT A 0 BRIGHT A_PainAttack("BMCacoBouncer");
			Goto See;
		Pain:
			BMHT A 6;
			BMHT A 6 A_Pain;
			Goto See;
		Death:
			BMHT A 8 BRIGHT;
			BMHT A 8 BRIGHT A_Scream;
			BMHT B 8 BRIGHT A_PainDie("BMCacoBouncer");
			BMHT B 8 BRIGHT;
			Stop;
		Raise:
			BMHT A 8;
			Goto See;
    }
}

class BMSpawnerTankPinky : BMSpawnerTankStinger
{
    
	Default
	{
		Tag "Puff Tank: Pinky POW";
		Health 100;
		Speed 0;
		+SOLID;
		+SHOOTABLE;
		+NOBLOOD;
		+ACTIVATEMCROSS;
		+DONTGIB;
		+NOICEDEATH;
		+FORCEXYBILLBOARD;
		
		SeeSound "BMNSQKSF";
		PainSound "BMNSQKSF";
		DeathSound "BMNPOPBO";
		//DeathSound "world/barrelx";
		ActiveSound "BMNSQKSF";
	}
    states
    {			
		Spawn:
			BMHT A 10 A_Look;
			Loop;
		See:
			BMHT A 3 A_Chase;
			Loop;
		Missile:
			BMHT A 5 A_FaceTarget;
			BMHT A 5 A_FaceTarget;
			BMHT A 5 BRIGHT A_FaceTarget;
			BMHT A 0 BRIGHT A_PainAttack("BMPinkyPOW");
			Goto See;
		Pain:
			BMHT A 6;
			BMHT A 6 A_Pain;
			Goto See;
		Death:
			BMHT A 8 BRIGHT;
			BMHT A 8 BRIGHT A_Scream;
			BMHT B 8 BRIGHT A_PainDie("BMPinkyPOW");
			BMHT B 8 BRIGHT;
			Stop;
		Raise:
			BMHT A 8;
			Goto See;
    }
}

class BMSpawnerTankBMCacoSqueaken : BMSpawnerTankStinger
{
    
	Default
	{
		Tag "Puff Tank: CacoSqueaken";
		Health 100;
		Speed 0;
		+SOLID;
		+SHOOTABLE;
		+NOBLOOD;
		+ACTIVATEMCROSS;
		+DONTGIB;
		+NOICEDEATH;
		+FORCEXYBILLBOARD;
		
		SeeSound "BMNSQKSF";
		PainSound "BMNSQKSF";
		DeathSound "BMNPOPBO";
		//DeathSound "world/barrelx";
		ActiveSound "BMNSQKSF";
	}
    states
    {			
		Spawn:
			BMHT A 10 A_Look;
			Loop;
		See:
			BMHT A 3 A_Chase;
			Loop;
		Missile:
			BMHT A 5 A_FaceTarget;
			BMHT A 5 A_FaceTarget;
			BMHT A 5 BRIGHT A_FaceTarget;
			BMHT A 0 BRIGHT A_PainAttack("BMCacoSqueaken");
			Goto See;
		Pain:
			BMHT A 6;
			BMHT A 6 A_Pain;
			Goto See;
		Death:
			BMHT A 8 BRIGHT;
			BMHT A 8 BRIGHT A_Scream;
			BMHT B 8 BRIGHT A_PainDie("BMCacoSqueaken");
			BMHT B 8 BRIGHT;
			Stop;
		Raise:
			BMHT A 8;
			Goto See;
    }
}

class BMCacoSqueaken : Actor
{
    Default
	{
        Health 10;
		DropItem "BMStingFrazzle", 256, 100;
        Radius 32;
        Height 56;
        Mass 50;
        Speed 20;
        Damage 5;
        PainChance 256;
		
        Monster;
		-CountKill;
		
        +FLOAT;
        +NOGRAVITY;
        +MISSILEMORE;
        +NOICEDEATH;
		+NOBLOOD;
		+SPAWNFLOAT;
		+FORCEXYBILLBOARD;
		
        AttackSound "skull/melee";
        PainSound "BMNSQKSF";
        DeathSound "BMNPOPBL";
        ActiveSound "BMNSQKSF";
        RenderStyle "SoulTrans";
        Obituary "%o was busted open by a Cacosqueaken."; // "%o was spooked by a lost soul."
		
        
    }
    
    states
    {
        Spawn:
            BMCA A 10 A_Look;
            Loop;
        See:
            BMCA A 6 A_Chase;
            Loop;
        Missile:
            BMCA A 10 A_FaceTarget;
            BMCA A 4 A_SkullAttack;
            BMCA A 4;
            Goto Missile+2;
        Pain:
            BMCA A 3;
            BMCA A 3 A_Pain;
            Goto See;
        Death:
            BMCA B 2 A_Scream;
			BMCA B 2 A_Blast(0, 255, 512.0, 30.0, "BlastEffect", "BlastRadius");
			//BMCA C 4 A_Explode(128, 256);
			BMCA C 4 A_NoBlocking();
			BMCA D -1;
			Stop;
	}
}

class BMBeastBalloonBase : Actor
{
    double forwardBounceSpeed;
	double upwardBounceSpeed;
	bool willTurn;
	int turnAngle;
	bool fuseLit;
	int forceFireBurst;

	//Property prefix: Squeaker;
	Property ForwardBounceSpeed: forwardBounceSpeed;
	Property UpwardBounceSpeed: upwardBounceSpeed;

	Default
	{
		Tag "BM Generic Beast Balloon";
		Radius 32;
        Height 32;

		// If you're using custom properties, you must use the base class as the prefix.
		BMBeastBalloonBase.ForwardBounceSpeed 16.0;
		BMBeastBalloonBase.UpwardBounceSpeed 12.0;

		Health 10;
		Speed 0;
		+SOLID;
		+SHOOTABLE;
		+NOBLOOD;
		+ACTIVATEMCROSS;
		+DONTGIB;
		+NOICEDEATH;

		PainSound "BMNSQKSF";
        DeathSound "BMNPOPSF";
	}

    states
    {
        Spawn:
			BMSB A 35;
			BMSB B 35 A_ForwardHop();
			BMSB C 16;
			BMSB A 16;
			Loop;
		Death:
			BMSW A random(10, 20) Bright A_Scream();
			BMSW B 6 Bright FireOrGlitterburst();
			BMSW C 12 Bright;
			TNT1 A 2;
			Stop;
    }

	void A_ForwardHop()
    {
        if (willTurn)
        {
            // Play a Squeak sound, then turn. Will move again next cycle.
            angle += turnAngle;
            willTurn = false;
        }
        else
        {
            A_ChangeVelocity(ForwardBounceSpeed, 0.0, UpwardBounceSpeed, CVF_RELATIVE);
        }
    }

    override void Tick()
    {
        Super.Tick();
        if (turnAngle == 0) {turnAngle = 90;}
        if (fuseLit)
        {
            // THEY PLUMP WHEN YOU COOK 'EM
            if (health > 0)
            {
                Scale.X += 0.05 + Random(0.00, 0.03);
                Scale.Y += 0.07 + Random(0.00, 0.03);
            }

            if (forceFireBurst > 0)
            {
                forceFireBurst--;
                // Odds are *extremely* high that if the Death state hasn't been triggered already
                // This explosion will probably kill.
                if (forceFireBurst == 0)
                {
                    Fireburst();
                    A_Die();
                }
            }
        }
        if (!CheckMove((Pos.X + Vel.X, Pos.Y + Vel.Y)))
        {
            // Console.PrintF("Beast Balloon WILL TURN");
            // Randomize Turn Angle Here Maybe?
            willTurn = true;
        }
    }

    override int DamageMobj(Actor inflictor, Actor source, int damage, Name mod, int flags, double angle)
    {
        int result = Super.DamageMobj(inflictor, source, damage, mod, flags, angle);

        // Fire sources are fatal and explosive.
        if (inflictor is "BMFireyBoom"
            || inflictor is "ExplosiveBarrel"
            || inflictor is "FireBall"
            || inflictor is "DoomImpBall"
            || inflictor is "ArchvileFire"
            || inflictor is "FlameMissile"
            || inflictor is "FireDemonMissile")
        {
            fuseLit = true;
            forceFireBurst = 35;
            // Console.PrintF("lit");
            //A_Die();
            // health = 0;
        }
        return result;
    }

    void FireOrGlitterburst()
    {
        if (fuseLit) /*Last Damage Source is Fireburst*/
        {
            Fireburst();
        }
        else
        {
            //Console.PrintF("glitter");
            Glitterburst();
        }
    }

    void Fireburst()
    {
        let boom = BMFireyBoom(A_SpawnProjectile("BMFireyBoom"));
        // VM Seems to crash occasionally if I don't null check this?
        if (boom)
        {
            boom.scale = (4.0, 4.0);
            //Console.PrintF("BOOM!!!");
        }
    }

    void Glitterburst()
    {
        BMPinkGlitter glitter;
        glitter = BMPinkGlitter(Spawn("BMPinkGlitter", Pos));
        glitter.CountGenerations = 3;
        glitter.Vel = (0, 0, 2.0);
    }
}

class BMCacoBouncer : BMBeastBalloonBase
{
    Default
	{
        Health 5;
		DropItem "BMWindBullet", 256, 100;
        Radius 32;
        Height 56;
        Mass 50;
        Speed 30;
        Damage 1;
        PainChance 256;
        Monster;
		-CountKill;
        +FLOAT;
        +NOGRAVITY;
        +MISSILEMORE;
        +NOICEDEATH;
		+NOBLOOD
		+SPAWNFLOAT
		+FORCEXYBILLBOARD;

		BounceType "Doom";
		WallBounceFactor 0.25;
		BounceCount 5;

        AttackSound "skull/melee";
        PainSound "BMNSQKSF";
        DeathSound "world/barrelx";
        ActiveSound "BMNSQKSF";
        RenderStyle "SoulTrans";
        Obituary "%o was crushed to bits by a CacoBouncer.";
    }

    states
    {
        Spawn:
            BMCA EEEEIIJJII 10 BRIGHT A_Look;
            Loop;
        See:
            BMCA IIIEEE 6 BRIGHT A_Chase;
            Loop;
        Missile:
            BMCA J 10 BRIGHT A_FaceTarget;
            BMCA J 4 BRIGHT A_SkullAttack;
            BMCA J 4 BRIGHT;
            Goto Missile+2;
        Pain:
            BMCA I 3 BRIGHT;
            BMCA I 3 BRIGHT A_Pain;
            Goto See;
        Death:
            BMCA E 2 FireOrGlitterburst();
            BMCA F 2 A_Scream;
			BMCA G 4 A_Explode(128, 128);
			BMCA H 2 A_NoBlocking();
			BMCA H -1;
			Stop;
	}
}

class BMBeeStinger : BMBeastBalloonBase
{
    Default
	{
		Tag "Bee Squeak-Stinger";
		DropItem "BMStingShreds", 256, 100;
        Health 5;
        Radius 32;
        Height 56;
        Mass 50;
        Speed 2;
        Damage 1;
        PainChance 256;
        Monster;
		-CountKill;
        +FLOAT;
        +NOGRAVITY;
        +MISSILEMORE;
        +NOICEDEATH;
		+NOBLOOD;
		+SPAWNFLOAT;
		+FORCEXYBILLBOARD;
		
        AttackSound "BMNDANK";
        PainSound "BMNSQKSF";
        DeathSound "BMNPOPBL";
        ActiveSound "BMNSQKSF";
        RenderStyle "SoulTrans";
        Obituary "%o was filled with Squeak-stingers.";
    }
    
    states
    {
		Spawn:
			BMBZ A 10 A_Look;
			Loop;
		See:
			BMBZ A 3 A_Chase;
			Loop;
		Melee:
		Missile:
			BMBZ A 8 A_FaceTarget;
			BMBZ A 6 A_SpawnProjectile("BMAttackDart");
			Goto See;
		Pain:
			BMBZ A 2;
			BMBZ A 2 A_Pain;
			Goto See;
		Death:
			BMBZ B 2 A_Scream;
			BMBZ B 2 A_Blast(0, 255, 128.0, 50.0, "BlastEffect", "BlastRadius");
			BMBZ C 2 A_Explode(64, 128);
			BMBZ C 2 A_NoBlocking;
			BMBZ C 2 FireOrGlitterburst();
			BMBZ C -1;
			Stop;
		XDeath:
			BMBZ A 2;
			BMBZ B 2 A_Scream;
			BMBZ C 2 A_Explode(64, 128);
			BMBZ C 2 A_NoBlocking;
			BMBZ C 2 FireOrGlitterburst();
			BMBZ C -1;
			Stop;
		Raise:
			BMBZ B 8;
			BMBZ A 6;
			Goto See;
			
			// Memo to use the earthquake in the intro somewhere.
	}
}

class BMAttackDart : DoomImpBall
{
    Default
	{
        Radius 6;
        Height 8;
        Speed 30;
		FastSpeed 6;
        Damage 3;
        Projectile;
		+RANDOMIZE;
		RenderStyle "Add";
		Alpha 1;
		SeeSound "imp/attack";
		DeathSound "imp/shotx";
		
        Obituary "%o was filled with B-stings.";
    }
    
    states
    {
        Spawn:
            BMST C 4 BRIGHT;
            Loop;
        Death:
            BMST C 6 Bright;
			Stop;
	}
}

class BMPinkyPOW : BMBeastBalloonBase
{
    Default
	{
		DropItem "BMWindBullet";
        Health 50;
        Radius 32;
        Height 56;
        Mass 50;
        Speed 15;
        Damage 1;
        PainChance 256;
        Monster;
		-CountKill;
        +MISSILEMORE;
        +NOICEDEATH;
		+NOBLOOD;
		+FORCEXYBILLBOARD;
		
		BounceType "Doom";
		WallBounceFactor 0.25;
		BounceCount 10;
		
        AttackSound "skull/melee";
        PainSound "BMNSQKSF";
        //DeathSound "BMNPOPSF";
		DeathSound "BMNPOPBL";
        ActiveSound "BMNSQKSF";
        RenderStyle "SoulTrans";
        Obituary "%o tried to pet a peckish Pinky Pow.";
    }
    
    states
    {
        Spawn:
            BMPK B 10 BRIGHT A_Look;
            Loop;
        See:
            BMPK B 6 BRIGHT A_Chase;
            Loop;
        Missile:
            BMPK A 10 BRIGHT A_FaceTarget;
            BMPK A 4 BRIGHT A_SkullAttack;
            BMPK A 4 BRIGHT;
            Goto Missile+2;
        Pain:
            BMPK A 3 BRIGHT FireOrGlitterburst();
            BMPK A 3 BRIGHT A_Pain;
            Goto See;
        Death:
            BMPK C 2 FireOrGlitterburst();
            BMPK C 2 A_Scream;
			BMPK D 2 A_Explode(64, 128);
			BMPK E 2 A_NoBlocking();
			BMPK E -1;
			Stop;
	}
}

class BMBorkPaconImp : BMBeastBalloonBase
{
    Default
	{
        Health 5;
		DropItem "BMTZilly";
        Radius 32;
        Height 56;
        Mass 50;
        Speed 30;
        Damage 1;
        PainChance 256;
        Monster;
		-CountKill;
        +MISSILEMORE;
        +NOICEDEATH;
		+NOBLOOD;
		+FORCEXYBILLBOARD;
		
		BounceType "Doom";
		WallBounceFactor 0.25;
		BounceCount 5;
		
        AttackSound "";
		SeeSound "imp/sight";
		ActiveSound "imp/active";
        PainSound "BMNSQKSF";
        DeathSound "BMNPOPBL";
        RenderStyle "SoulTrans";
        Obituary "%o ate too much Bork Pacon.";
    }
    
    states
    {
        Spawn:
            BMIM A 10 BRIGHT A_Look;
            Loop;
        See:
            BMIM A 6 BRIGHT A_Chase;
            Loop;
        Missile:
            BMIM A 8 BRIGHT A_FaceTarget;
            BMIM A 6 BRIGHT A_TroopAttack;
            Goto See;
        Pain:
            BMIM A 3 BRIGHT;
            BMIM A 3 BRIGHT A_Pain;
            Goto See;
        Death:
            BMIM A 2 FireOrGlitterburst();
            BMIM E 2 A_Scream;
			BMIM C 4 A_Blast(0, 64, 128.0, 50.0, "BlastEffect", "BlastRadius");
            BMIM D -1;
            //BMIM A 6 BRIGHT A_Explode(1, 22222, XF_NOSPLASH);
			Stop;
	}
}

class BMNullShellRank3 : BMBeastBalloonBase
{
	action void A_NullDeathRank3()
    {	
		actor act = null;
		int x = pos.x;
		int y = pos.y;
		int z = pos.z;
		ACS_NamedExecute("increase_subboss_kill_count");
		act = Spawn("BMNullShellRank2", (x + 20, y, z));
		act.ChangeTid(105);
		act = Spawn("BMNullShellRank2", (x - 20, y, z));
		act.ChangeTid(105);
		act = Spawn("BMNullShellRank2", (x, y + 20, z));
		act.ChangeTid(105);
		act = Spawn("BMNullShellRank2", (x, y - 20, z));
		act.ChangeTid(105);
    }
	
	action void A_FireNullShellDeathProjectile()
    {	
		A_SpawnProjectile("BMBakedBadBigBalls", 0, 0, 0);
		A_SpawnProjectile("BMBakedBadBigBalls", 0, 0, 45);
		A_SpawnProjectile("BMBakedBadBigBalls", 0, 0, 135);
		A_SpawnProjectile("BMBakedBadBigBalls", 0, 0, 180);
		A_SpawnProjectile("BMBakedBadBigBalls", 0, 0, -45);
		A_SpawnProjectile("BMBakedBadBigBalls", 0, 0, -135);
		A_StartSound("BMNDANK", CHAN_WEAPON);
    }

    Default
	{
		//DropItem "BMNullShellRank2";
		//DropItem "BMNullShellRank2";
		//DropItem "BMNullShellRank2";
		DropItem "BMCheckpoint";
        Health 1500;
		Gravity 1.3;
        Radius 256;
        Height 256;
        Mass 25;
        Speed 30;
        Damage 20;
        PainChance 256;
		ProjectileKickBack 50;
		BounceFactor 1.0;
        Monster;
		-CountKill;
        +FLOAT;
        +MISSILEMORE;
        +NOICEDEATH;
		+NOBLOOD
		+SPAWNFLOAT
		+FORCEXYBILLBOARD;
		
		BounceType "Doom";
		WallBounceFactor 1;
		
        AttackSound "skull/melee";
        PainSound "BMNSQKSF";
        DeathSound "world/barrelx";
        ActiveSound "BMNSQKSF";
        RenderStyle "SoulTrans";
        Obituary "%o was nullified by a Null Shell (Risk Rank 3).";
    }
    
    states
    {
        Spawn:
			//BMBL A 1 BRIGHT Thing_ChangeTID(0, 101);
            BMBL AABBCC 10 BRIGHT A_Look;
            Loop;
        See:
            BMBL AABBCC 6 BRIGHT A_Chase;
            Loop;
        Missile:
            BMBL AABBCC 10 BRIGHT A_FaceTarget;
            BMBL AABBCC 4 BRIGHT A_SkullAttack;
            BMBL AABBCC 4 BRIGHT;
            Goto Missile+2;
        Pain:
            BMBL A 3 BRIGHT;
            BMBL A 3 BRIGHT A_Pain;
            Goto See;
        Death:
			BMBL C 1 ACS_NamedExecute("block_spawner_boss_all_clear");
            BMBL C 1 A_Scream;
			BMBL B 1 A_NoBlocking();
			BMBL A 15 A_FireNullShellDeathProjectile;
			BMBL B 2 A_NullDeathRank3;
			BMBL B 0 ACS_NamedExecute("unblock_spawner_boss_all_clear");
			//BMBL B 8 A_Explode(64, 512);
			
			//BMBL B 1 A_Explode(256, 512);
			//BMBL A 1 BRIGHT Thing_Spawn(13456, 0, 104);
			//BMBL A 1 BRIGHT Thing_Spawn(13456, 0, 104);
			//BMBL A 1 BRIGHT Thing_Spawn(13456, 0, 104);
			BMBL B 2 A_Blast(0, 128, 128.0, 20.0, "BlastEffect", "BlastRadius");
			//BMBL A 1 BRIGHT A_PainDie("BMNullShellRank1");
			TNT1 A -1;
			Stop;
	}
}

class BMNullShellRank2 : BMNullShellRank3
{
	action void A_NullDeathRank2()
    {
		actor act = null;
		int x = pos.x;
		int y = pos.y;
		int z = pos.z;
		
		ACS_NamedExecute("increase_subboss_kill_count");
		
		act = Spawn("BMNullShellRank1", (x + 20, y, z));
		act.ChangeTid(106);
		act = Spawn("BMNullShellRank1", (x - 20, y, z));
		act.ChangeTid(106);
		act = Spawn("BMNullShellRank1", (x, y + 20, z));
		act.ChangeTid(106);
		act = Spawn("BMNullShellRank1", (x, y - 20, z));
		act.ChangeTid(106);
    }
    Default
	{
		DropItem "BMCheckpoint", -1;
        Health 777;
        Radius 128;
        Height 128;
        Mass 50;
        Speed 45;
		Damage 10;
        PainChance 256;
		ProjectileKickBack 50;
		BounceFactor 1.0;
        Monster;
        +FLOAT;
        +MISSILEMORE;
        +NOICEDEATH;
		+NOBLOOD
		+SPAWNFLOAT
		+FORCEXYBILLBOARD;
		
		BounceType "Doom";
		WallBounceFactor 1;
		
        AttackSound "skull/melee";
        PainSound "BMNSQKSF";
        DeathSound "world/barrelx";
        ActiveSound "BMNSQKSF";
        RenderStyle "SoulTrans";
        Obituary "%o was nullified by a Null Shell (Risk Rank 2).";
    }
    
    states
    {
        Spawn:
			//BMBL D 1 BRIGHT Thing_ChangeTID(0, 101);
            BMBL DDEEFF 10 BRIGHT A_Look;
            Loop;
        See:
            BMBL DDEEFF 6 BRIGHT A_Chase;
            Loop;
        Missile:
            BMBL DDEEFF 10 BRIGHT A_FaceTarget;
            BMBL DDEEFF 4 BRIGHT A_SkullAttack;
            BMBL DDEEFF 4 BRIGHT;
            Goto Missile+2;
        Pain:
            BMBL D 3 BRIGHT;
            BMBL D 3 BRIGHT A_Pain;
            Goto See;
        Death:
			BMBL F 0 ACS_NamedExecute("block_spawner_boss_all_clear");
            BMBL F 1 A_Scream;
			BMBL E 1 A_NoBlocking();
			BMBL A 15 A_FireNullShellDeathProjectile;
			BMBL E 8 A_Explode(64, 512);
			BMBL F 2 A_NullDeathRank2;
			//BMBL E 1 A_Explode(256, 512);
			BMBL E 1 A_Blast(0, 128, 128.0, 10.0, "BlastEffect", "BlastRadius");
			//BMBL D 1 BRIGHT A_PainDie("BMNullShellRank1");
			//BMBL D 1 BRIGHT Thing_Spawn(13457, 0, 104);
			//BMBL D 1 BRIGHT Thing_Spawn(13457, 0, 104);
			//BMBL D 1 BRIGHT Thing_Spawn(13457, 0, 104);
			BMBL E 1 A_NoBlocking();
			BMBL F 0 ACS_NamedExecute("unblock_spawner_boss_all_clear");
			TNT1 A -1;
			Stop;
	}
}

class BMNullShellRank1 : BMNullShellRank3
{
    Default
	{
		DropItem "BMCheckpoint", -1;
        Health 333;
        Radius 128;
        Height 128;
        Mass 50;
        Speed 45;
		Damage 10;
        PainChance 256;
		ProjectileKickBack 50;
		BounceFactor 1.0;
        Monster;
        +FLOAT;
        +MISSILEMORE;
        +NOICEDEATH;
		+NOBLOOD
		+SPAWNFLOAT
		+FORCEXYBILLBOARD;
		
		BounceType "Doom";
		WallBounceFactor 1;
		
        AttackSound "BMNDANK";
        PainSound "BMNSQKSF";
        DeathSound "world/barrelx";
        ActiveSound "BMNSQKSF";
        RenderStyle "SoulTrans";
        Obituary "%o was nullified by a Null Shell (Risk Rank 2).";
    }
    
    states
    {
        Spawn:
			//BMBL G 1 BRIGHT Thing_ChangeTID(0, 101);
            BMBL GGHHII 10 BRIGHT A_Look;
            Loop;
        See:
            BMBL GGHHII 6 BRIGHT A_Chase;
            Loop;
        Missile:
            BMBL GGHHII 10 BRIGHT A_FaceTarget;
            BMBL GGHHII 4 BRIGHT A_SkullAttack;
            BMBL GGHHII 4 BRIGHT;
            Goto Missile+2;
        Pain:
            BMBL G 3 BRIGHT;
            BMBL G 3 BRIGHT A_Pain;
            Goto See;
        Death:
            BMBL I 1 A_Scream;
			BMBL I 0 ACS_NamedExecute("increase_subboss_kill_count");
			BMBL H 1 A_Explode(32, 64);
			BMBL G 1 A_NoBlocking();
			TNT1 A -1;
			//BMBL D 1 ACS_NamedExecute("check_spawner_boss_all_clear");
			Stop;
	}
}

// This does not inhert from BMBeastBalloonBase since well, it's a Baked Bad, not a Beast Balloon.
class BMBakedBadBigTheCake : Cyberdemon
{
	action void A_FireBakedBadBigBalls()
    {	
		A_SpawnProjectile("BMBakedBadBigBallsBigger", 32, 0);
    }
	

	Default
	{
		Health 3500;
		Radius 160;
		Height 160;
		Mass 1000;
		Speed 30;
		PainChance 30;
		Monster;
		MinMissileChance 160;
		+BOSS 
		+MISSILEMORE
		+FLOORCLIP
		+NORADIUSDMG
		+DONTMORPH
		+BOSSDEATH
		//SeeSound "cyber/sight";
		//PainSound "cyber/pain";
		//DeathSound "cyber/death";
		//ActiveSound "cyber/active";
		SeeSound "BMNDANK";
		PainSound "BMNSPLAT";
		DeathSound "BMNPOPBO";
		ActiveSound "BMNDANK";
		Obituary "%o was blown to bits by Big the Cake.";
		Tag "Baked Bad: Big the Cake";
	}
	States
	{
	Spawn:
		BMCK A 10 A_Look;
		Loop;
	See:
		BMCK A 3 A_Hoof;
		BMCK A 3 A_Chase;
		BMCK A 3 A_Metal;
		BMCK A 3 A_Chase;
		Loop;
	Missile:
	
		BMCK A 2 A_FaceTarget;
		BMCK A 15 A_FireBakedBadBigBalls;
		BMCK A 15 A_FaceTarget;
		BMCK A 15 A_FireBakedBadBigBalls;
		BMCK A 15 A_FaceTarget;
		BMCK A 15 A_FireBakedBadBigBalls;
		
		BMCK A 18 A_FaceTarget;
		BMCK A 4 A_FireBakedBadBigBalls;
		BMCK A 4 A_FaceTarget;
		BMCK A 4 A_FireBakedBadBigBalls;
		BMCK A 4 A_FaceTarget;
		BMCK A 4 A_FireBakedBadBigBalls;
		
		BMCK A 24 A_FaceTarget;
		BMCK A 15 A_FireBakedBadBigBalls;
	
		/*
		BMCK A 2 A_FaceTarget;
		BMCK A 15 A_CyberAttack;
		BMCK A 15 A_FaceTarget;
		BMCK A 15 A_CyberAttack;
		BMCK A 15 A_FaceTarget;
		BMCK A 15 A_CyberAttack;
		
		BMCK A 18 A_FaceTarget;
		BMCK A 4 A_CyberAttack;
		BMCK A 4 A_FaceTarget;
		BMCK A 4 A_CyberAttack;
		BMCK A 4 A_FaceTarget;
		BMCK A 4 A_CyberAttack;
		
		BMCK A 24 A_FaceTarget;
		BMCK A 15 A_CyberAttack;
		*/
		Goto See;
	Pain:
		BMCK A 10 A_Pain;
		Goto See;
	Death:
		BMCK A 10;
		BMCK B 10 A_Scream;
		BMCK B 10;
		BMCK C 10 A_NoBlocking;
		BMCK C 10;
		BMCK C 30;
		BMCK D -1 A_BossDeath;
		Stop;
	}
}

// TODO: This, however, should share BMBeastBalloonBase's vulnerability to fire.
class BMBakedBadBigBalls : Rocket
{
 	Default
	{
		Scale 1;
		Radius 11;
        Height 8;
        Speed 20;
        Damage 10;
        Projectile;
		+RANDOMIZE;
		RenderStyle "Normal";
		Alpha 1;
		SeeSound "BMNDANK";
		DeathSound "BMNPOPBO";
		Tag "Baked Bad's Big Balls";
	}
	States 
	{
	Spawn: 
		BMBL L 25;
		Goto Death;
	Death: 
		BMBL K 15 Bright A_Explode;
		Stop;
	}
}

class BMBakedBadBigBallsBigger : BMBakedBadBigBalls
{
 	Default
	{
		Scale 2.5;
		Radius 22;
        Height 16;
	}
	States 
	{
	Spawn: 
		BMBL L 140;
		Goto Death;
	Death: 
		BMBL K 15 Bright A_Explode;
		Stop;
	}
}

class BMAllcomers : Actor
{
    Default
	{
        Health 696969;
        Radius 16;
		Height 56;
		Mass 100;
        Speed 20;
        Damage 0;
        PainChance 0;
		Tag "Al L Comers (Allco Mers)";
		
		Monster;
		-CountKill;
		+ONLYVISIBLEINMIRRORS;
        +NOICEDEATH;
		+NOBLOOD;
		+FLOORCLIP;
		+FRIENDLY;
		-SHOOTABLE;
		-SOLID;
		-FALLDAMAGE;
		+NOBLOCKMONST;
		+NOTELESTOMP;
		+PUSHABLE;
		+FORCEXYBILLBOARD;
		
		+NOBLOCKMAP;
		
		//RenderStyle "Add";
		
        AttackSound "";
        PainSound "BMNSQKSF";
        DeathSound "BMNPOPSF";
        ActiveSound "BMNSQKSF";
        Obituary "%o somehow died to %g meta representation.";
    }
    
    States
    {
		Spawn:
            BMRB B -1 A_Look;
            Stop;
	}
	
	override void Tick()
    {		
		// Get the player's position
		// ACS_NamedExecute("TeleportBunnyToPlayer");

        super.Tick();
    }
}

class BMGasket : Actor
{
	Default
	{
		DropItem "BMBSting";
		Health 500;
		PainChance 200;
		Radius 16;
		Height 56;
		Mass 100;
		Speed 20;
		Damage 0;
		PainChance 0;
		Activation THINGSPEC_Default;
		Monster;
		-CountKill;
		+NEVERTARGET;
		-COUNTKILL;
		+NOSPLASHALERT;
		+FLOORCLIP;
		+JUSTHIT;
		+FRIENDLY;
		+FORCEXYBILLBOARD;
		+USESPECIAL;
		-BUMPSPECIAL;
		MinMissileChance 150;
		MaxStepHeight 16;
		MaxDropoffHeight 32;
		SeeSound "peasant/sight";
		AttackSound "peasant/attack";
		PainSound "";
		DeathSound "";
		HitObituary "%o stepped in front of Gasket's shotgun.";
	}
  
  States
  {
	Spawn:
		BMSK A 70 A_LookEX;
		Loop;
	See:
		BMSK A 5 A_FaceTarget;
		Loop;
  }
  
  override bool Used(Actor user)
    {
        // Add your code here to define what happens when the actor is used.
		//A_Log("attempting to start conversation.");
		//StartConversation(50, 1);
		//A_Log("Conversation should have started?");
		//A_Log("I'm looking for my brother. If you can fight, I'll go with.");
		A_Log("I'm looking for my brother. If you see him, let me know.");
		//StartConversation(50, 1); // If this ever actually works, I'll be stunned.
        return true; // Return true if the actor was used.
    }
}

class BMPatcher : Fist
{
 	Default
	{
		Weapon.SlotNumber 1;
		Weapon.SelectionOrder 3600;
		Weapon.Kickback 100;
		Obituary "Yes, it is a sharp needle.";
		Tag "Patcher";
		+WEAPON.WIMPY_WEAPON;
		+WEAPON.MELEEWEAPON;
	}
	States
	{
	Ready:
		BMNE A 1 A_WeaponReady;
		Loop;
	Deselect:
		BMNE A 1 A_Lower;
		Loop;
	Select:
		BMNE A 1 A_Raise;
		Loop;
	Fire:
		BMNE A 4;
		BMNE B 6 A_Punch;
		//BMNE B 6 A_FirePistol;
		BMNE C 4;
		BMNE B 5 A_ReFire;
		Goto Ready;
	Flash:
		BMNE A 7 Bright A_Light1;
		Goto LightDone;
		BMNE A 7 Bright A_Light1;
		Goto LightDone;
 	Spawn:
		BMNE A -1;
		Stop;
	}
}

class BMDigouter : Chainsaw
{
 	Default
	{
		Weapon.SlotNumber 1;
		Weapon.SelectionOrder 2100;
		Weapon.Kickback 100;
		Weapon.UpSound "BMCMPRON";
		Weapon.ReadySound "BMCMPRSR";
		Obituary "%o was overpressurized!";
		Tag "Digouter";
		Inventory.PickupMessage "Digouter";
		+WEAPON.MELEEWEAPON;
	}
	States
	{	
	Ready:
		BMDO C 4 A_WeaponReady;
		Loop;
	Deselect:
		BMDO C 1 A_Lower;
		Loop;
	Select:
		BMDO C 1 A_Raise;
		Loop;
	Fire:
		BMDO CDE 4 A_Saw("BMCMPRSR", "BMCMPRON");
		BMDO C 0 A_ReFire;
		Goto Ready;
	Spawn:
		BMDO A -1;
		Stop;
	}
}

class BMStingFrazzle : Ammo
{
	Default
	{
		Scale 0.25;
		Inventory.PickupMessage "B.Sting Frazzles";
		Inventory.Amount 10;
		Inventory.MaxAmount 340;
		Inventory.Icon "BMWPC0";
		Ammo.BackpackAmount 10;
		Ammo.BackpackMaxAmount 400;
	}
	
	states
	{
		Spawn:
			BMWP C -1;
			stop;
	}
}

class BMStingShreds : BMStingFrazzle
{
	Default
	{
		Scale 0.25;
		Inventory.PickupMessage "B.Sting Shreds";
		Inventory.Amount 100;
	}
}

class BMTZilly : Ammo
{
	Default
	{
		Scale 0.25;
		Inventory.PickupMessage "Tzilly";
		Inventory.Amount 34;
		Inventory.MaxAmount 134;
		Inventory.Icon "BMWPH0";
		Ammo.BackpackAmount 50;
		Ammo.BackpackMaxAmount 400;
	}
	
	states
	{
		Spawn:
			BMWP H -1;
			stop;
	}
}

class BMWindBullet : Ammo
{
	Default
	{
		Scale 0.25;
		Inventory.PickupMessage "Cyclone Slug";
		Inventory.Amount 3;
		Inventory.MaxAmount 69;
		Inventory.Icon "BMWPJ0";
		Ammo.BackpackAmount 3;
		Ammo.BackpackMaxAmount 6969;
	}
	
	states
	{
		Spawn:
			BMWP J -1;
			stop;
	}
}

class BMBSting : DoomWeapon
{
	action void A_FireBSting()
    {		
		 A_FireBullets (0, 0, 1, 20, "BulletPuff");
		 A_StartSound("BMNHOOKS", CHAN_WEAPON);
		 A_GunFlash();
    }
	
	action void A_FireBStingProjectile()
    {	
		A_FireProjectile("BMBStingPlayerShot", 0, 1);
		A_StartSound("BMNPOPSF", CHAN_WEAPON);
		A_GunFlash();
    }

 	Default
	{
		Weapon.SlotNumber 2;
		Weapon.SelectionOrder 1900;
		Weapon.AmmoUse 1;
		Weapon.AmmoGive 100;
		Weapon.AmmoType "BMStingFrazzle";
		Obituary "%o was filled with B.Stings";
		+WEAPON.WIMPY_WEAPON;
		Inventory.Pickupmessage "Beastling Stings";
		Tag "B.Sting";
		
		//Weapon.WeaponScaleX 0.3;
		//Weapon.WeaponScaleY 0.3;
	}
	States
	{
	Ready:
		BMWP D 1 A_WeaponReady;
		Loop;
	Deselect:
		BMWP D 1 A_Lower;
		Loop;
	Select:
		BMWP E 2 A_Raise;
		Loop;
	Fire:
		BMWP D 3 Bright;
		BMWP D 6 Bright A_FireBStingProjectile;
		BMWP D 15;
		BMWP D 5 A_ReFire;
		Goto Ready;
	Flash:
		BMWP D 7 Bright A_Light1;
		Goto LightDone;
		BMWP D 7 Bright A_Light1;
		Goto LightDone;
 	Spawn:
		BMWP B -1;
		Stop;
	}
}

class BMBStingPlayerShot : BMBSting
{
 	Default
	{
		Scale 0.75;
		
		Radius 16;
        Height 16;
        Speed 160;
        Damage 10;
        Projectile;
		+RANDOMIZE;
		RenderStyle "Add";
		Alpha 1;
		SeeSound "BMNDANK";
		DeathSound "";
		Tag "Stinger Player Projectile";
		+RIPPER;
	}
	States 
	{
	Spawn: 
		BMST C 12;
		Goto Death;
	Death: 
		TNT1 A 0;
		Stop;
	}
}

class BMSerpentine : BMBSting
{
	action void A_FireSerpentine()
	{		
		 A_FireBullets (2.25, 0, 3, 5, "BulletPuff");
		 A_StartSound("BMNHOOKS", CHAN_WEAPON);
		 A_GunFlash();
	}
	Default
	{
		Weapon.SlotNumber 4;
		Weapon.SelectionOrder 1700;
		Weapon.AmmoUse 3;
		Obituary "%o took too many Slither Smooches.";
		Inventory.Pickupmessage "Serpentine";
		Tag "Serpentine";
	}
	States
	{
	Spawn:
		BMCH B -1;
		Stop;
	
	Ready:
		BMWP F 1 A_WeaponReady;
		Loop;
	Deselect:
		BMWP F 1 A_Lower;
		Loop;
	Select:
		BMWP F 2 A_Raise;
		Loop;
	Fire:
		BMWP F 4 Bright A_FireSerpentine;
		BMWP F 0 A_ReFire;
		Goto Ready;
	Flash:
		BMWP F 7 Bright A_Light1;
		Goto LightDone;
		BMWP F 7 Bright A_Light1;
		Goto LightDone;
	}
}

class BMFallstreak : BMBSting
{
	action void A_FireFallstreak()
	{		
		 A_FireBullets(12, 0, 12, 20, "BulletPuff");
		 A_StartSound("BMNPOPSF", CHAN_WEAPON);
		 A_GunFlash();
	}
 	Default
	{
		Weapon.SlotNumber 3;
		Weapon.SelectionOrder 1800;
		Weapon.AmmoUse 12;
		Weapon.AmmoType "BMTZilly";
		Obituary "%o was holepunched by the Fallstreak.";
		Inventory.Pickupmessage "Fallstreak";
		Tag "Fallstreak";
	}
	States 
	{
	Spawn: 
		BMWP G -1;
		Stop;
		
	Ready:
		BMWP D 1 A_WeaponReady;
		Loop;
	Deselect:
		BMWP D 1 A_Lower;
		Loop;
	Select:
		BMWP D 2 A_Raise;
		Loop;
	Fire:
		BMWP D 5 Bright A_FireFallstreak;
		BMWP D 5 Bright;
		BMWP E 35 Bright;
		BMWP D 0 A_ReFire;
		Goto Ready;
	Flash:
		BMWP D 7 Bright A_Light1;
		Goto LightDone;
		BMWP D 7 Bright A_Light1;
		Goto LightDone;
	}
}

class BMSuperShieldBurst : BMBSting
{
	action void A_FireSuperShieldBurst()
    {	
		A_FireBullets (0, 0, 1, 20, "BulletPuff");
		A_FireProjectile("BMSuperShieldBurstBurst", 0, 1);
		A_GunFlash();//BMWPI0
    }

 	Default
	{
		Weapon.SlotNumber 5;
		Weapon.SelectionOrder 1600;
		Weapon.AmmoType "BMWindBullet";
		Obituary "%o was blown to bits by the Blowback Generator.";
		Inventory.Pickupmessage "Blowback Generator";
		Tag "Blowback Generator";
	}
	States 
	{
	Spawn: 
		BMWP I -1;
		Stop;
		
	Ready:
		BMWP K 1 A_WeaponReady;
		Loop;
	Deselect:
		BMWP K 1 A_Lower;
		Loop;
	Select:
		BMWP K 2 A_Raise;
		Loop;
	Fire:
		BMWP K 5 Bright A_FireSuperShieldBurst;
		BMWP K 10 Bright;
		BMWP K 0 A_ReFire;
		Goto Ready;
	Flash:
		BMWP K 7 Bright A_Light1;
		Goto LightDone;
		BMWP K 7 Bright A_Light1;
		Goto LightDone;
	}
}

class BMSuperShieldBurstBurst : BMBSting
{
 	Default
	{
		Scale 3.0;
		Radius 18;
        Height 18;
        Speed 60;
        Damage 3;
        Projectile;
		+RANDOMIZE;
		RenderStyle "Add";
		Alpha 1;
		SeeSound "";
		DeathSound "BMNPOPSF";
		Obituary "%o was blown to bits by the Blowback Generator.";
		Tag "Blowback Generator Gumshot";
	}
	States 
	{
	Spawn: 
		BMBL J 6;
		Goto Death;
	Death: 
		BMBL K 2 A_Blast(0, 1024, 1024, 60.0, "BlastEffect", "BlastRadius");
		TNT1 A 0;
		Stop;
	}
}

Class BMBlueYum : Soulsphere
{
	Default
	{
		Tag "Blue Yum";
	}
	States
	{
		Spawn:
		BMBY A 2 Bright;
		Loop;
	}
}

Class BMRisqueKit : Berserk
{
	Default
	{
		Radius 32;
		Height 32;
		Tag "Risque Kit";
		Inventory.PickupMessage "Toroko Flower Mix..."; // "Berserk!"
		// Consider only using the timer mechanic if this is active? 
		// In which, flowers can refill the timer instead of checkpoints.
		Inventory.PickupSound "misc/p_pkup";
	}
	
	States
	{
	Spawn:
		BMZK A -1;
		Stop;
	Pickup:
		TNT1 A 0 A_GiveInventory("PowerStrength");
		TNT1 A 0 HealThing(100, 0);
		TNT1 A 0 A_SelectWeapon("BMPatcher");
		Stop;
	}
}

class BMTimerDamageSource : Actor
{
	Default
	{
		Radius 1;
		Height 1;
		Health 10;
		Obituary "%o was never seen again. Only a plushie of %g likeness was found.";
	}
	
    states
    {
        Spawn:
            TNT1 A -1 bright;
            stop;
    }	
}

class BMCheckSpawnerBossAllClear : Actor
{
	Default
	{
		Radius 1;
		Height 1;
		-Shootable;
		Obituary "%o was never seen again. Only a plushie of %g likeness was found.";
	}
	
    states
    {
        Spawn:
            TNT1 A 35 ACS_NamedExecute("check_spawner_boss_all_clear");
            Loop;
    }
}

class BMMenaceEnergySource : Cacodemon
{
	Default
	{
		Health 5;
		Tag "Bubble Menace Energy Source";
	}
	
    states
    {
        Spawn:
			HEAD A 2 A_Look;
			Loop;
		See:
			HEAD A 2 A_FaceTarget;
			Loop;
		Death:
			HEAD A 2 Exit_Normal(0);
			Stop;
    }
}

class BMRedDisc : RedCard
{
	Default
	{
		Inventory.PickupMessage "Got a Red Key Disc";
		Inventory.Icon "BMWPO0";
		Tag "Red Key Disc";
	}
	
    states
    {
        Spawn:
			BMWP L 10;
			BMWP L 10 Bright;
			Loop;
    }
}

class BMYellowDisc : YellowCard
{
	Default
	{
		Inventory.PickupMessage "Got a Yellow Key Disc";
		Inventory.Icon "BMWPP0";
		Tag "Yellow Key Disc";
	}
	
    states
    {
        Spawn:
			BMWP M 10;
			BMWP M 10 Bright;
			Loop;
    }
}

class BMFireyBoom : DoomImpBall
{
    Default
	{
        Radius 32;
        Height 32;
        Speed 0;
		FastSpeed 0;
        Damage 0;
        Projectile;
		+Ripper;
		RenderStyle "Normal";
		Alpha 1;
		SeeSound "";
		DeathSound "";
		+NOINTERACTION
		
        Obituary "%o was caught in an explosion.";
    }
    
    states
    {
        Spawn:
			BMBM B 1 NoDelay A_StartSound("BMSIBOOM");
			BMBM B 1 ;
			BMBM C 1 A_Explode(15 * Max(Scale.X/2.0, 1), 128.0 * Max(Scale.X/2.0, 1), XF_NOTMISSILE | XF_CIRCULAR, false, 128.0 * Max(Scale.X/2.0, 1));
			BMBM D 1 ;//A_RadiusThrust(30 * 100 * Max(Scale.X/2.0, 1), 128.0 * Max(Scale.X/2.0, 1), RTF_NOIMPACTDAMAGE | RTF_NOTMISSILE & RTF_AFFECTSOURCE, 128.0 * Max(Scale.X/2.0, 1), "None");
			BMBM C 1;
			BMBM F 1;
			BMBM G 1;
			BMBM H 1;
			BMBM I 1;
			BMBM J 1;
			BMBM K 1 A_Explode(15 * Max(Scale.X/2.0, 1), 128.0 * Max(Scale.X/2.0, 1), XF_NOTMISSILE | XF_CIRCULAR, false, 128.0 * Max(Scale.X/2.0, 1));
			BMBM L 1 ;//A_RadiusThrust(30 * 100 * Max(Scale.X/2.0, 1), 128.0 * Max(Scale.X/2.0, 1), RTF_NOIMPACTDAMAGE | RTF_NOTMISSILE & RTF_AFFECTSOURCE, 128.0 * Max(Scale.X/2.0, 1), "None");
			BMBM M 1;
			BMBM N 1;
			BMBM O 1;
			BMBM P 1;
			BMBM Q 1;
			BMBM R 1;
			BMBM S 1 A_Explode(15 * Max(Scale.X/2.0, 1), 128.0 * Max(Scale.X/2.0, 1), XF_NOTMISSILE | XF_CIRCULAR, false, 128.0 * Max(Scale.X/2.0, 1));
			BMBM T 1 ;//A_RadiusThrust(30 * 100 * Max(Scale.X/2.0, 1), 128.0 * Max(Scale.X/2.0, 1), RTF_NOIMPACTDAMAGE | RTF_NOTMISSILE & RTF_AFFECTSOURCE, 128.0 * Max(Scale.X/2.0, 1), "None");
			BMBM U 1;
			BMBM V 1;
			BMBM W 1;
			BMBM X 1;
			BMBM Y 1;
			BMBM Z 1;
			BMBN A 1 A_Explode(15 * Max(Scale.X/2.0, 1), 128.0 * Max(Scale.X/2.0, 1), XF_NOTMISSILE | XF_CIRCULAR, false, 128.0 * Max(Scale.X/2.0, 1));
			BMBN B 1 ;//A_RadiusThrust(30 * 100 * Max(Scale.X/2.0, 1), 128.0 * Max(Scale.X/2.0, 1), RTF_NOIMPACTDAMAGE | RTF_NOTMISSILE & RTF_AFFECTSOURCE, 128.0 * Max(Scale.X/2.0, 1), "None");
			BMBN C 1;
			BMBN D 1;
			BMBN E 1;
			BMBN F 1;
			BMBN G 1;
			BMBN H 1;
			BMBN I 1 A_Explode(15 * Max(Scale.X/2.0, 1), 128.0 * Max(Scale.X/2.0, 1), XF_NOTMISSILE | XF_CIRCULAR, false, 128.0 * Max(Scale.X/2.0, 1));
			BMBN J 1 ;//A_RadiusThrust(30 * 100 * Max(Scale.X/2.0, 1), 128.0 * Max(Scale.X/2.0, 1), RTF_NOIMPACTDAMAGE | RTF_NOTMISSILE & RTF_AFFECTSOURCE, 128.0 * Max(Scale.X/2.0, 1), "None");
			BMBN K 1;
			BMBN L 1;
			BMBN M 1;
			BMBN N 1;
			BMBN O 1;
			BMBN P 1;
			BMBN Q 1 A_Explode(15 * Max(Scale.X/2.0, 1), 128.0 * Max(Scale.X/2.0, 1), XF_NOTMISSILE | XF_CIRCULAR, false, 128.0 * Max(Scale.X/2.0, 1));
			BMBN R 1 ;//A_RadiusThrust(30 * 100 * Max(Scale.X/2.0, 1), 128.0 * Max(Scale.X/2.0, 1), RTF_NOIMPACTDAMAGE | RTF_NOTMISSILE & RTF_AFFECTSOURCE, 128.0 * Max(Scale.X/2.0, 1), "None");
			BMBN S 1;
			BMBN T 1;
			BMBN U 1;
			BMBN V 1;
			BMBN W 1;
			BMBN X 1;
			BMBN Y 1 A_Explode(15 * Max(Scale.X/2.0, 1), 128.0 * Max(Scale.X/2.0, 1), XF_NOTMISSILE | XF_CIRCULAR, false, 128.0 * Max(Scale.X/2.0, 1));
			BMBN Z 1 ;//A_RadiusThrust(30 * 100 * Max(Scale.X/2.0, 1), 128.0 * Max(Scale.X/2.0, 1), RTF_NOIMPACTDAMAGE | RTF_NOTMISSILE & RTF_AFFECTSOURCE, 128.0 * Max(Scale.X/2.0, 1), "None");
			BMBO A 1;
			BMBO B 1;
			BMBO C 1;
			BMBO D 1;
			BMBO E 1;
			BMBO F 1;
			BMBO G 1 A_Explode(15 * Max(Scale.X/2.0, 1), 128.0 * Max(Scale.X/2.0, 1), XF_NOTMISSILE | XF_CIRCULAR, false, 128.0 * Max(Scale.X/2.0, 1));
			BMBO H 1 ;//A_RadiusThrust(30 * 100 * Max(Scale.X/2.0, 1), 128.0 * Max(Scale.X/2.0, 1), RTF_NOIMPACTDAMAGE | RTF_NOTMISSILE & RTF_AFFECTSOURCE, 128.0 * Max(Scale.X/2.0, 1), "None");
			BMBO I 1;
			BMBO J 1;
			BMBO K 1 ExplodeMissile();
            Stop;
        Death:
            
			BMBO L 1;
			BMBM A 1;
			Stop;
	}
}

class BMDeadlyToilet : Actor
{
	Default
	{
		Tag "Deadly Toilet";
		Radius 32;
        Height 32;
        Scale 1;
		Health 10;
		Speed 0;
		+SOLID;
		+SHOOTABLE;
		+NOBLOOD;
		+ACTIVATEMCROSS;
		+DONTGIB;
		+NOICEDEATH;
	}
	
    states
    {
        Spawn:
			BMTL A -1;
			Loop;
		Death:
			BMTL B random(16, 35);
			BMTL C 35 A_SpawnProjectile("BMFireyBoom");
			Stop;
    }
}

class BMDeadlyDangerNoodle : BMDeadlyToilet
{
	Default
	{
		Tag "Deadly Danger Noodle";
		Radius 32;
        Height 32;
        Scale 1;
		Health 10;
		Speed 0;
		+SOLID;
		+SHOOTABLE;
		+NOBLOOD;
		+ACTIVATEMCROSS;
		+DONTGIB;
		+NOICEDEATH;
	}
	
    states
    {
        Spawn:
			GRHB S -1;
			Loop;
		Death:
			GRHB S random(16, 35);
			GRHB S 35 A_SpawnProjectile("BMFireyBoom");
			Stop;
    }
}

class BMSqueakerBlueChew : BMBeastBalloonBase
{
	
	//Property prefix: Squeaker;
	Property ForwardBounceSpeed: forwardBounceSpeed;
	Property UpwardBounceSpeed: upwardBounceSpeed;

	Default
	{
		Tag "Squeaker:Blue Chew";
		Radius 32;
        Height 32;
		
		// If you're using custom properties, you must use the base class as the prefix.
		BMSqueakerBlueChew.ForwardBounceSpeed 16.0;
		BMSqueakerBlueChew.UpwardBounceSpeed 12.0;

		Health 60;
		Speed 0;
		+SOLID;
		+SHOOTABLE;
		+NOBLOOD;
		+ACTIVATEMCROSS;
		+DONTGIB;
		+NOICEDEATH;
	}
	
    states
    {
        Spawn:
			BMSB A 35;
			BMSB B 35 A_ForwardHop();
			BMSB C 16;
			BMSB A 16;
			Loop;
        Pain:
            BMSW A 6 A_Pain;
		Death:
			BMSW A random(10, 20) Bright A_Scream();
			BMSW B 6 Bright FireOrGlitterburst();
			BMSW C 12 Bright;
			TNT1 A 2;
			Stop;
    }
	
	void A_ForwardHop()
    {
        if (willTurn)
        {
            // Play a Squeak sound, then turn. Will move again next cycle.
            angle += turnAngle;
            willTurn = false;
        }
        else
        {
            A_ChangeVelocity(ForwardBounceSpeed, 0.0, UpwardBounceSpeed, CVF_RELATIVE);
        }
    }

    override void Tick()
    {
        if (turnAngle == 0) {turnAngle = 90;}
        Super.Tick();
        if (!CheckMove((Pos.X + Vel.X, Pos.Y + Vel.Y)))
        {
            // Console.PrintF("Squeaker WILL TURN");
            willTurn = true;
        }
    }
}

class BMSqueakerRedChew : BMSqueakerBlueChew
{
	Default
	{
		Tag "Squeaker:Red Chew";
		Radius 48;
        Height 48;
		BMSqueakerBlueChew.ForwardBounceSpeed 12.0;
		BMSqueakerBlueChew.UpwardBounceSpeed 8.0;
	}
	
    states
    {
        Spawn:
			BMSR A 15;
			BMSR B 15 A_ForwardHop();
			BMSR C 8;
			BMSR A 8;
			Loop;
        Pain:
            BMSW A 6 A_Pain;
		Death:
			BMSW A random(10, 20) Bright A_Scream();
			BMSW B 6 Bright FireOrGlitterburst();
			BMSW C 12 Bright;
			Stop;
    }

    override void Tick()
    {
        if (turnAngle == 0) {turnAngle = 45;}
        Super.Tick();
    }
}

// I would probably save a lot of trouble if I inherted from HelperDogs, but it's a bit late for that.
// https://doomwiki.org/wiki/Helper_dog
class BMPercivalFollower : Actor
{
	enum PercivalFollowerBehaviorState
	{
		PERCIVAL_FOLLOWER_STATE_IDLE,
		PERCIVAL_FOLLOWER_STATE_MIMIC,
		PERCIVAL_FOLLOWER_STATE_CATCH_UP,
		PERCIVAL_FOLLOWER_STATE_WARP,
		PERCIVAL_FOLLOWER_STATE_PANIC,
	};
	
	PercivalFollowerBehaviorState follow_state;
	bool initialized;
	bool showDebugText;
	
	double jumpStrength;
	
	double distanceFromPlayer;
	double mimicRangeMin;
	double mimicRangeMax;
	double catchupRangeMin;
	double catchupRangeMax;
	double warpRangeMin;
	
	int arrLeaderPosLength;
	int arrLeaderPosCurrentIndex;
	Vector3[35] arrLeaderPos; // Wish this were dynamic...
	
	Default
	{
		Tag "Percival (Follower)";
		+Friendly;
		
		Scale 0.5;
		Radius 16;
        Height 12;
        Speed 4;
	}
	
    states
    {
        Spawn:
			BMPC A 10 A_Look();
			Loop;
		See:
			BMPC A 10 A_Chase("Melee", "Missile", CHF_DONTMOVE); //A_FaceTarget?
			// A_CustomComboAttack("DoomImpBall", 32, 3 * random(1, 8), "imp/melee")
			Loop;
		Melee:
			BMPC A 10 A_FaceTarget();
			BMPC A 8 A_CustomMeleeAttack(random(1, 10) * 6, "skeleton/melee");
			BMPC A 8;
			Goto See;
		Missile:
			BMPC A 10 A_FaceTarget();
			BMPC A 8 A_PosAttack();
			BMPC A 8;
			Goto See;
    }
	
	override void Tick()
	{	
		super.Tick();
		// Dealing with nondeterministic code - https://zdoom.org/wiki/Creating_multiplayer-friendly_ZScript
		if (!initialized) 
		{
			if (showDebugText) console.Printf("Initializing Percival");
			InitPercival();
		}
		if (!master) {
			// Consider having a separate behavior where she just roams if there's nobody to follow.
			console.Printf("No value set for 'master', cannot follow.");
			return;
		}
		// https://zdoom.org/wiki/Distance3D - Distance stuff.
		
		distanceFromPlayer = Distance3D(master);
		SelectState();
		ActBasedOnState(follow_state);
	}
	
	void SelectState()
	{
	    if (master.health <= 0)
	    {
            // Leader is dead, focus on keeping yourself alive.
            // With any luck, she may finish the map herself.
            follow_state = PERCIVAL_FOLLOWER_STATE_IDLE;
            ClearArrLeader();
	    }
		else if (distanceFromPlayer < mimicRangeMin)
		{
			if (follow_state != PERCIVAL_FOLLOWER_STATE_IDLE)
			{
			    if (showDebugText) console.printF("Switching to Idle State.");
			    follow_state = PERCIVAL_FOLLOWER_STATE_IDLE;
			    ClearArrLeader();
            }

		}
		else if (distanceFromPlayer >= mimicRangeMin && distanceFromPlayer < mimicRangeMax)
		{
			if (follow_state != PERCIVAL_FOLLOWER_STATE_MIMIC)
			{
			    if (showDebugText) console.printF("Switching to Mimic State.");
    			follow_state = PERCIVAL_FOLLOWER_STATE_MIMIC;
    			ClearArrLeader();
            }
		} 
		else if (distanceFromPlayer >= mimicRangeMax && distanceFromPlayer < catchupRangeMax) 
		{
			if (follow_state != PERCIVAL_FOLLOWER_STATE_CATCH_UP)
			{
			    if (showDebugText) console.printF("Switching to Catch-Up State.");
    			follow_state = PERCIVAL_FOLLOWER_STATE_CATCH_UP;
    			ClearArrLeader();
            }
		} 
		else if (distanceFromPlayer >= catchupRangeMax) 
		{
			if (follow_state != PERCIVAL_FOLLOWER_STATE_WARP)
			{
			    if (showDebugText) console.printF("Switching to Warp State.");
			    follow_state = PERCIVAL_FOLLOWER_STATE_WARP;
			    ClearArrLeader();
            }
		}
	}
	
	void ActBasedOnState(PercivalFollowerBehaviorState follow_state)
	{
		switch (follow_state)
		{
			case PERCIVAL_FOLLOWER_STATE_IDLE:
				PercivalFollowerTickIdle();
				break;
				
			case PERCIVAL_FOLLOWER_STATE_MIMIC:
				PercivalFollowerTickMimic();
				break;
				
			case PERCIVAL_FOLLOWER_STATE_CATCH_UP:
				PercivalFollowerTickCatchUp();
				break;
				
			case PERCIVAL_FOLLOWER_STATE_WARP:
				PercivalFollowerTickWarp();
				break;
				
			case PERCIVAL_FOLLOWER_STATE_PANIC:
				PercivalFollowerTickPanic();
				break;
		}
	}
	
	void PercivalFollowerTickIdle()
	{
		if (showDebugText) console.Printf("Idle...");
		A_Chase();
	}
	
	void PercivalFollowerTickMimic()
	{
		if (showDebugText) console.Printf("Mimic...");
		// We don't want to shuffle memory around, 
		// So we'll use a carousel style cursor index.
		
		// There was a safety null check here. It's gone now.
		
		// Turns out SetXYZ is mostly good for "assuming" a position, 
		// not for actually moving. SetOrigin instead.
		if (showDebugText) console.Printf("Attempting to move to (%.2f, %.2f, %.2f)", pos.x, pos.y, pos.z);
		
		Vector3 newPos = (arrLeaderPos[arrLeaderPosCurrentIndex].X, arrLeaderPos[arrLeaderPosCurrentIndex].Y, Pos.Z);
		SetOrigin(newPos, true); 
		
		if (arrLeaderPos[arrLeaderPosCurrentIndex].Z - Pos.Z > 20) {ActJump();}
		// Consumed the slot. We can replace it now.
		arrLeaderPos[arrLeaderPosCurrentIndex] = master.Pos;
		
		arrLeaderPosCurrentIndex++;
		if (arrLeaderPosCurrentIndex >= arrLeaderPosLength) 
		{
			arrLeaderPosCurrentIndex = 0;
		}
		
		if (!target) 
		{
			A_FaceMaster();
		} 
		else 
		{
			A_FaceTarget();
		}
		
	}
	
	void PercivalFollowerTickCatchUp()
	{
	    if (showDebugText) console.Printf("Angle before facing: (%.2f))", angle);
	    A_ClearTarget();
		if (showDebugText) console.Printf("CatchUp...");
		A_FaceMaster();
		if (showDebugText) console.Printf("Angle AFTER facing: (%.2f))", angle);
		
		/*
		// Figure out where we want to be.
		Vector3 targetPos = master.Pos;
		
		
		// Figure out the distance (ignore z bc/ jumping?)
		double dist = DistVec3IgnoreZ(pos, targetPos);
		
		//wait, wouldn't the length of the displacement vector also be the distance?... Oops.
		Vector3 displacement = targetPos - pos;
		
		
		// If move speed will get us there, just warp there.
		if (dist <= speed) 
		{
			SetOrigin(targetPos, true);
		}
		// If it won't, we normalize the vector and multiply it by our speed
		// to get the new position.
		else 
		{
			SetOrigin(displacement.Unit() * speed, true); //Unit is a Normalized copy.
		}
		*/
		
		
		A_ChangeVelocity(Speed, 0, Vel.Z, CVF_RELATIVE);
		if (arrLeaderPos[arrLeaderPosCurrentIndex].Z - Pos.Z > 20) {ActJump();}
		
		
	}
	
	double DistVec3IgnoreZ(Vector3 startPos, Vector3 endPos)
	{
		double distance = 0.0;
		
		double xDiff = endPos.x - startPos.x;
		double yDiff = endPos.y - startPos.y;
		distance = sqrt((xDiff*xDiff)  + (yDiff*yDiff));
		
		return distance;
	}
	
	void PercivalFollowerTickWarp()
	{
		// console.Printf("Warp...");
		SetOrigin(master.Pos, true); 
	}
	
	void PercivalFollowerTickPanic()
	{
	
	}
	
	void ActJump()
	{
		if (Vel.Z <= 0) {Vel.Z = 0;}
		Vel.Z += jumpStrength;
	}
	
	void InitPercival()
	{
		/*
		I think the intended paradigm is that I make properties for all of these and set it up in the Default but asfasdfaf
		*/
		showDebugText = false;
		follow_state = PERCIVAL_FOLLOWER_STATE_MIMIC;
		
		jumpStrength = 10.0;
		
		distanceFromPlayer = 0.0;
		mimicRangeMin = 64.0;
		mimicRangeMax = 512.0;
		catchupRangeMin = 512.0;
		catchupRangeMax =  1024.0;
		warpRangeMin = 2048.0;
		
		arrLeaderPosLength = 35; // Please remember to manually match this to the length you defined earlier.
		arrLeaderPosCurrentIndex = 0;
		// arrLeaderPosLength.Resize(arrLeaderPosLength); 
		// Can't figurte out how to get dynamic arrays to work with Vector3.
		let v = (0,0,0);
		for (int i = 0; i < arrLeaderPosLength; i++) 
		{
			// Player position isn't set yet... so just zero instead of master.Pos?
			
			arrLeaderPos[i] = v;
		}
		
		if (!master) 
		{
			// Default to following the "host" player. This isn't proper, we should
			// probably track a list of all summoned followers and their corresponding
			// players, but this will work for the time I have left before RAMP ends.
			master = players[net_arbitrator].mo;
		}
		
		initialized = true;
	}
	
	void ClearArrLeader()
	{
		for (int i = 0; i < arrLeaderPosLength; i++) 
		{
			// There's probably some way to safely nullcheck the master.Pos
			// But I will figure it out when I have more time probably.
			
			// arrLeaderPos[i] = master.Pos;
			// arrLeaderPos[i].Y -= 64;
			
			arrLeaderPos[i] = Pos;
		}
	}
}

class BMNPCJean : Actor
{
	Default
	{
		Tag "Jean NPC";
	}
	
	States
	{
		Spawn:
			BMSM E -1;
			Loop;
		Pain:
			BMSM E 35;
			Loop;
		Death:
			BMSM E random(35, 50);
			BMSM E 6 A_SpawnProjectile("BMFireyBoom");
			TNT1 A 12;
			Stop;
	}
}

class BMFollowerPercivalEvents : EventHandler
{
	override void OnRegister()
	{
		console.printf("I Exist!");
	}
	
	override void WorldLoaded (WorldEvent e)
     {
         console.printf("Another World Loaded.");
		 
     }
}

// Consider redoing this to instead be a powerup subclass instead of a playerpawn.
// That would both be more modular, and more flexible for other modders to toy with out of context.
class BMPlayerALLCOMERS : DoomPlayer
{
	double crouchDashSpeed;
	double fastfall_speed;
    int crouchDashCooldownRemaining;
    int crouchDashCooldownWait;
	int crouch_dash_active_duration;
	int crouch_dash_active_rem;
	int crouch_dash_vel_dir_magnitude;
	
	int crouchDashActiveDuration;
	int crouchDashActiveRem;
	
	double fastfallSpeed;

	name originalSoundClass;
	name originalPlayerClass;

	int strangeCounter;

	PlayerPawn substitute;

	Default
	{
		//Tag "Player (Al L. Comers)";
		
		
		Speed 1;
		Health 100;
		Radius 16;
		Height 56;
		Mass 100;
		PainChance 255;
		Gravity 1.25; // Relative to World Grav Default: 1000.0/800.0 = 1.25
		
		
		Player.JumpZ 18.0;
		Player.DisplayName "Al L. Comers";
		Player.CrouchSprite "PLYC";
		Player.StartItem "BMPatcher";
		
		Player.SoundClass "BMPlayerALLCOMERS";

	}

	States
	{
		Spawn:
			BMRB B 1 NODELAY InitProperties();
			BMRB B -1;
			Loop;
		See:
			BMRB B 4;
			Loop;
		Missile:
			BMRB B 12;
			Goto Spawn;
		Melee:
			BMRB B 6 BRIGHT;
			Goto Missile;
		Pain:
			BMRB B 4;
			BMRB B 4 A_Pain;
			Goto Spawn;
		Death:
			TNT1 A 0 A_PlayerSkinCheck("AltSkinDeath");
		Death1:
			BMRB B 10;
			BMRB B 10 A_PlayerScream;
			BMRB B 10 A_NoBlocking;
			BMRB B 10;
			TNT1 A -1;
			Stop;
		XDeath:
			TNT1 A 0 A_PlayerSkinCheck("AltSkinXDeath");
		XDeath1:
			BMRB B 5;
			BMRB B 5 A_XScream;
			BMRB B 5 A_NoBlocking;
			BMRB B 5;
			TNT1 A -1;
			Stop;
		AltSkinDeath:
			BMRB B 6;
			BMRB B 6 A_PlayerScream;
			BMRB B 6;
			BMRB B 6 A_NoBlocking;
			TNT1 A 6;
			TNT1 A -1;
			Stop;
		AltSkinXDeath:
			BMRB B 5 A_PlayerScream;
			BMRB B 0 A_NoBlocking;
			BMRB B 5 A_SkullPop;
			TNT1 A 5;
			TNT1 A -1;
			Stop;
	}
	
	void InitProperties()
	{
		
		level.AirControl = 1.0;
        level.AirFriction = 0.95; 
		
		crouchDashSpeed = 77.0;
		crouchDashCooldownRemaining = 0;
		crouchDashCooldownWait = 35;
		
		crouchDashActiveDuration = 7;
		crouchDashActiveRem = 0;
		
		fastfallSpeed = -64.0;
		
		GiveInventory("BMPatcher", 1);
		
	}
	
	override void Tick()
	{
        if (substitute)
        {
            bNoInteraction = true;
            bSolid = false;
            bShootable = false;
            // substitute.A_SetRenderStyle(0, STYLE_None);
            A_SetRenderStyle(0, STYLE_None);
            Vel = substitute.Vel;

            substitute.soundClass = soundClass;
            substitute.jumpZ = jumpZ;
        }
		super.Tick();
		TickDash();
		strangeCounter++;
		// console.PrintF("Allcomers counter: (%i)", strangeCounter);
	}
	
	void HandleDashInput()
	{
		if (CanAirDash()) 
		{
			DoAirDash();
		}
		else 
		{
			A_StartSound("BMCANT");
		}
	}
	
	bool CanAirDash()
	{
		return crouchDashCooldownRemaining < 1 && health > 0;
	}
	
	void DoAirDash()
	{
		crouchDashCooldownRemaining = crouchDashCooldownWait + crouchDashActiveDuration;
		crouchDashActiveRem = crouchDashActiveDuration;
		A_QuakeEx(2, 2, 2, 25, 0, 10, "", QF_SCALEDOWN);
		A_StartSound("BMTCH");
		if (substitute)
		{
            substitute.A_QuakeEx(2, 2, 2, 25, 0, 10, "", QF_SCALEDOWN);
		    substitute.A_StartSound("BMTCH");
		}
	}
	
	void HandleFastFallInput()
	{
		if (CanFastFall()) 
		{
			DoFastFall();
		}
		else 
		{
            if (A_CheckFloor(null))
            {
                Vel.Z = JumpZ;
            }
            if (substitute && substitute.A_CheckFloor(null))
            {
                substitute.Vel.Z = JumpZ;
            }
			//A_StartSound("BMCANT");
		}
	}
	
	bool CanFastFall()
	{
		return Vel.Z < 0.0;
	}
	
	void DoFastFall()
	{
		Vel.X = 0;
		Vel.Y = 0;
		Vel.Z = fastfallSpeed;
		A_QuakeEx(2, 2, 2, 25, 0, 10, "", QF_SCALEDOWN);
		//A_StartSound("BMTCH");
        if (substitute)
		{
            substitute.A_QuakeEx(2, 2, 2, 25, 0, 10, "", QF_SCALEDOWN);
            substitute.Vel.X = 0;
		    substitute.Vel.Y = 0;
		    substitute.Vel.Z = fastfallSpeed;
		}
	}

	void CreateGroundPound()
	{
        BMGroundPound gp = BMGroundPound(Spawn("BMGroundPound"));
        if (gp)
        {
            Console.PrintF("GroundPound Spawned");
            gp.master = self;
            gp.target = self;
            if (substitute)
            {
                gp.master = substitute;
                gp.target = substitute;
            }
        }
	}

	void TickDash()
	{
		if (crouchDashActiveRem > 0)
		{
			let currentSpeed = abs(Vel.X) + abs(Vel.Y); // Ignore Z.
			if (currentSpeed == 0)
			{
				// If we want a specific action for trying to dodge while still, it should probably go here. Maybe a backflip? A spot-dodge?
			}
			else 
			{
				Vel.X = (Vel.X / currentSpeed) * crouchDashSpeed;
				Vel.Y = (Vel.Y / currentSpeed) * crouchDashSpeed;
				Vel.Z = 0;
			}
		}
	
		if (crouchDashCooldownRemaining > 0) {crouchDashCooldownRemaining--;}
		if (crouchDashActiveRem > 0) {crouchDashActiveRem--;}
		if (crouchDashCooldownRemaining == 1)
        {
            A_StartSound("BMNDASHR");
        }
        if (substitute)
        {
            substitute.Vel = Vel;
        }
	}
}

class BMEventsPlayAsALLCOMERS : EventHandler
{
    bool showLevelWinSplash;
    
    bool showBlackFade;
    bool showPinkFade;
    double pinkFadeOpacity;
    int postFade;

    PlayerInfo spawnedPlayerInfo;
    PlayerPawn originalPlayerPawn;
    Name originalPlayerClass;
    Name originalPlayerSoundClass;
    double originalPlayerJumpZ;
    BMPlayerALLCOMERS allcomers;
    bool alPrepared;
    int totalTicks;

    bool percivalDown;
    bool gasketDown;

    bool ammoCheck;
    bool itemCheck;

    int thingDieCount;

    TextureID dialogueBG;

    BMGlobalVariables bmg;

    BMFakeTextureGlitch bmftg;
    
	override void OnRegister()
	{
        // This... seems to break when loading (such as dying) so...
		console.printf("Event Handler registered. Will play as Al.");
		showLevelWinSplash = false;
        bmg = BMGlobalVariables.Get();
	}

    override void WorldThingDied(WorldEvent e)
	{
        thingDieCount++;
		if (e.thing) // Check that the Actor is valid
		{
            let tTid = e.thing.TID;
            if (tTid >= 34100)
            {
                //Console.PrintF("A WorldThing died that seems important. Spawning a reward.");
                Actor newThing;
                switch(tTid)
                {
                    case 34100:
                    case 34101:
                        newThing = Actor(e.thing.Spawn("BMTicket2024"));
                        let raisedPos =  (e.thing.Pos.X, e.thing.Pos.Y, e.thing.Pos.Z + 64);
                        newThing.SetOrigin(raisedPos, false);
                        break;
                    case 34102:
                        newThing = Actor(e.thing.Spawn("BMDisc"));
                        newThing.SetOrigin(e.thing.Pos, false);
                        break;
                    case 34103:
                        newThing = Actor(e.thing.Spawn("BMDangerNoodle"));
                        newThing.SetOrigin(e.thing.Pos, false);
                        break;
                    case 34104:
                        newThing = Actor(e.thing.Spawn("BMDoomressNoArmor"));
                        newThing.SetOrigin(e.thing.Pos, false);
                        break;
                    default:
                        break;
                }
            }

            if (e.thing is "BMNPCGasket2m30s" && !(e.thing is "BMNPCPercival2m30s")) 
            {
                gasketDown = true;
                let sparkles = BMPinkGlitter(e.thing.Spawn("BMPinkGlitter"));
                sparkles.CountGenerations = 5;
                sparkles.SetOrigin(e.thing.Pos, false);
                //Interrupt the dialogue.
                // If Percival is alive
                if (!percivalDown){bmg.convoDirector.currentConvo.InterruptLine("Percival: EEP! What are you doing!? Why did you do that!?");}
                else {EarlyEnd();}
                
                // Otherwise, accelerate the end.
                // 
                
                //Switch to the solo topics.
                
            }
            if (e.thing is "BMNPCPercival2m30s") 
            {
                percivalDown = true;
                let sparkles = BMPinkGlitter(e.thing.Spawn("BMPinkGlitter"));
                sparkles.CountGenerations = 5;
                sparkles.SetOrigin(e.thing.Pos, false);
                //Intterupt the dialogue.
                //Switch to the solo topics.

                // If Gasket is still alive.
                if (!gasketDown){bmg.convoDirector.currentConvo.InterruptLine("Gasket: What the hell did she ever do to you? She was just trying to be friendly!");}
                else {EarlyEnd();}

                // Otherwise, accelerate the end.
                // bmg.convoDirector.currentConvo.InterruptLine("");
            }

        }
	}

    void EarlyEnd()
    {
        // Console.PrintF("That's all, folks. On to the next!");

        let playerToShake = players[net_arbitrator].mo;

        bmg.convoDirector.currentConvo.InterruptLine("");
        bmg.convoDirector.show = false;

        let sparkles = BMPinkGlitter(playerToShake.Spawn("BMPinkGlitter"));
        sparkles.CountGenerations = 6;
        sparkles.SetOrigin(playerToShake.Pos, false);

        
        playerToShake.A_QuakeEx(6, 6, 6 / 1.3, (35*7) + (6*35*2), 0, 10, "", QF_SHAKEONLY | QF_SCALEUP); //

        StartFadeToPink();
    }

    override void WorldLineActivated (WorldEvent e) 
    {
        if (e.ActivatedLine.Index() == 1222) 
        {
            Console.PrintF("End Level Cutscene Goes Here: Train.");
            StartFadeToBlack();
        }
    }
	
    // This works when leaving the world, not when loading a save.
	override void WorldLoaded(WorldEvent e)
	{
	    //console.printf("World Loaded.");
	}

    // This works when leaving the world, not when loading a save.
	override void WorldUnloaded(WorldEvent e)
	{
	    //console.printf("World Unloaded. Reset all player values to what they were at map start.");
	    ResetPlayerToPriorState();
        // Might be possible to hijack the end-screen music via Level.SetInterMusic(wbs.next);, but I don't want to risk it right now.
	}

    override void PlayerEntered(PlayerEvent e)
	{
        //Console.PrintF("Player Entered.");
	}

    override void PlayerRespawned(PlayerEvent e)
	{
        //Console.PrintF("Player Respawned.");
	}

    override void NewGame()
	{
        //Console.PrintF("New Game");
	}

	override void PlayerSpawned(PlayerEvent e)
	{
        //Console.PrintF("Player Spawned.");
		AttachAllcomersForPlayer(e.PlayerNumber);
	}

    void GiveAllCrayonWeapons(Actor playerPawn)
    {
        playerPawn.ClearInventory();
        playerPawn.GiveInventory("BMPatcher", 10);
        //playerPawn.GiveInventory("BMDigouter", 10);
        playerPawn.GiveInventory("BMBSting", 10);
        playerPawn.GiveInventory("BMSerpentine", 10);
        playerPawn.GiveInventory("BMFallstreak", 10);
        //playerPawn.GiveInventory("BMSuperShieldBurst", 10);
    }

    void AttachAllcomersForPlayer(int playerNum)
    {
        // Spawn a corresponding new Al player for this spawned player.
		// As a reminder, the playerpawn's (or monster's) .player should point
		// to the corresponding PlayerInfo, and the PlayerInfo's .mo should 
		// be pointing to the corresponding PlayerPawnActor/Monster.
		
		/*
		https://zdoom-docs.github.io/staging/Api/Base/ctor.html?highlight=position#world-attributes
		readOnly vector3 Pos
		The current position of the actor. If you want to set this, you can use SetOrigin.

		vector3 Prev
		The previous position of the actor.
		*/

        /*
        This whole system seems brittle and prone to breakage when reloading save files. Maybe I need to move from PlayerSpawned to WorldLoaded?
        */
		
		// Bug that will hopefully just not happen often:
		// If the player change-maps across maps wwhere this code is used,
		// their "original" settings will now be copies of the modified settings.
		spawnedPlayerInfo = players[playerNum];
		originalPlayerPawn = spawnedPlayerInfo.mo;
		originalPlayerClass = originalPlayerPawn.GetClassName();
		originalPlayerSoundClass = originalPlayerPawn.SoundClass;
        originalPlayerJumpZ = originalPlayerPawn.jumpZ;

        GiveAllCrayonWeapons(originalPlayerPawn);

		if (allcomers)
		{
	       allcomers.Destroy();

		}
		allcomers = BMPlayerALLCOMERS(Actor.Spawn("BMPlayerALLCOMERS", originalPlayerPawn.Pos));
		allcomers.substitute = originalPlayerPawn;
		allcomers.strangeCounter += 1;
		
		//console.PrintF("Allcomers counter: (%i)", allcomers.strangeCounter);
		
		// This seems to mostly work. Not sure what to do about player death since this doesn't cause the usual death reaction?

        // I was going to auto-summon percival if unlocked, but some players may find that annoying?
        // But I also don't want people to completely forget either...
        bool optionEnableFollowerPercival = true;

        if (bmg.followerPercivalUnlocked && optionEnableFollowerPercival)
        {
            
                        let newThing = Actor(allcomers.Spawn("BMPercivalFollower"));
                        let adjustedPos =  (allcomers.Pos.X - 64, allcomers.Pos.Y - 64, allcomers.Pos.Z + 64);
                        newThing.SetOrigin(adjustedPos, false);
        } 
    }

    // Gotta make sure I at least TRY to make it unlikely to unintentionally leave this map with modified properties
    // Or David will probably be annoyed.
	void ResetPlayerToPriorState()
	{
	    if (spawnedPlayerInfo)
	    {
	        if (!originalPlayerSoundClass) {originalPlayerSoundClass == "player";}
	        spawnedPlayerInfo.mo.SoundClass = originalPlayerSoundClass;
            spawnedPlayerInfo.mo.JumpZ = originalPlayerJumpZ;
            spawnedPlayerInfo.mo.A_SetRenderStyle(1, STYLE_Normal);

	        if (originalPlayerPawn)
	        {
	            spawnedPlayerInfo.mo = originalPlayerPawn;
	            originalPlayerPawn.player = spawnedPlayerInfo;
	        }
	        else if (originalPlayerClass)
	        {
	            // Commented out because Spawning is not available.
	            /*
                originalPlayerClass = PlayerPawn(Spawn(originalPlayerClass, (0,0,0)));
                spawnedPlayerInfo.mo = originalPlayerPawn;
	            originalPlayerPawn.player = spawnedPlayerInfo;
	            */

	        }
	    }
	}

	override bool InputProcess(InputEvent e)
	{
	    // https://zdoom.org/wiki/Customize_controls
	    // Because I couldn't figure out the + and - meant Button Down and button Release.
	    // console.Printf("Input: (%s)", e.KeyString);

		bool intendToDash = false;
		Array<int> allBindsDash;
		Bindings.GetAllKeysForCommand(allBindsDash, "+Crouch");
		for (int i = 0; i < allBindsDash.Size(); i++)
		{
            //console.Printf("Checking Bind (%i)", allBindsDash[i]);
            intendToDash = intendToDash || e.KeyScan == allBindsDash[i];
		}

        // I keep forgetting this doesn't include the toggle crouch.
        // And I just learned that ToggleCrouch is just Crouch without the explicit
        // KeyDown (+) indicator.
        Bindings.GetAllKeysForCommand(allBindsDash, "crouch");
		for (int i = 0; i < allBindsDash.Size(); i++)
		{
            //console.Printf("Checking Bind (%i)", allBindsDash[i]);
            intendToDash = intendToDash || e.KeyScan == allBindsDash[i];
		}
		intendToDash = intendToDash && e.Type == InputEvent.Type_KeyDown;
		if (intendToDash)
		{
			EventHandler.SendNetworkEvent("BMActDash");
		}

        bool intendToFastFall = false;
		Array<int> allBinds;
		Bindings.GetAllKeysForCommand(allBinds, "+Jump");
		for (int i = 0; i < allBinds.Size(); i++)
		{
            // console.Printf("Checking Bind (%i)", allBinds[i]);
            intendToFastFall = intendToFastFall || e.KeyScan == allBinds[i];
            // console.Printf("IntendFall (%b)", intendToFastFall);
		}
		intendToFastFall = intendToFastFall && e.Type == InputEvent.Type_KeyDown;
		if (intendToFastFall)
		{
			EventHandler.SendNetworkEvent("BMActFastFall");
		}

		return false;
	}
	
	override void NetworkProcess(ConsoleEvent e)
	{
	    if (e.Name == "BMActWin") 
		{
		    // console.Printf("BMActWin");
		    showLevelWinSplash = !showLevelWinSplash;
		}
		if (e.Name == "BMActDash") 
		{
			// console.Printf("BMActDash");
			PlayerPawn theCallingPlayerPawn = players[e.player].mo;
			BMPlayerALLCOMERS bmpa = null;
			if (theCallingPlayerPawn is "BMPlayerALLCOMERS") 
			{
				bmpa = BMPlayerALLCOMERS(theCallingPlayerPawn);
				bmpa.HandleDashInput();
			}
			else 
			{
			 // console.Printf("This player probably can't airdash?");
             if (allcomers) {allcomers.HandleDashInput();}
			}
		}
		else if (e.Name == "BMActFastFall") 
		{
			// console.Printf("BMActFastFall");
			//TODO: refactor and move this repeating junk into a function.
			PlayerPawn theCallingPlayerPawn = players[e.player].mo;
			BMPlayerALLCOMERS bmpa = null;
			if (theCallingPlayerPawn is "BMPlayerALLCOMERS") 
			{
				bmpa = BMPlayerALLCOMERS(theCallingPlayerPawn);
				bmpa.HandleFastFallInput();
			}
			else 
			{
			 // console.Printf("This player probably can't fastfall?");
             if (allcomers) {allcomers.HandleFastFallInput();}
			}
		}
		else if (e.Name == "BMActSummonPercival") 
		{
			console.Printf("BMActSummonPercival");
		}
		else if (e.Name == "BMActPlatformingCamera") 
		{
			console.Printf("BMActPlatformingCamera");
		}
        else if (e.Name == "BMActFadeToPink") 
		{
			console.Printf("BMActFadeToPink");
            StartFadeToPink();
		}
        else if (e.Name == "BMActFinalGlitterFountain") 
		{
			//console.Printf("BMActFinalGlitterFountain");
            // Spawn a HUGE glitter fountain
            //    if I had more time and felt clever, I'd loop through every remaining entity and do the fountains but I don't.
            let glitterThisGuy = players[net_arbitrator].mo;
            if (glitterThisGuy) 
            {
                let glitter = BMPinkGlitter(Actor.Spawn("BMPinkGlitter", glitterThisGuy.Pos));
                glitter.CountGenerations = 5; // This is probably going to be at least 500, possibly thousands...
            }
		}
	}

    void StartFadeToPink()
    {
        // Spawn a HUGE glitter fountain
        // Reset opacity to 0;
        showPinkFade = true;
        // Flag that we want to draw and increase it every tick.
        pinkFadeOpacity = 0.0;
        
        EventHandler.SendNetworkEvent("BMActFinalGlitterFountain");
        
    }

    void TickPinkFade()
    {
        // Reset opacity to 0;
        if (pinkFadeOpacity < 1) {pinkFadeOpacity += 0.0025; postFade = 0;}
        else {pinkFadeOpacity = 1; postFade++;}

        if (postFade == 70) {Exit_Normal(0);}
    }
    
    void StartFadeToBlack()
    {
        // Spawn a HUGE glitter fountain
        // Reset opacity to 0;
        showBlackFade = true;
        // Flag that we want to draw and increase it every tick.
        pinkFadeOpacity = 0.0;        
    }

    void TickBlackFade()
    {
        // Reset opacity to 0;
        if (pinkFadeOpacity < 1) {pinkFadeOpacity += 0.005; postFade = 0;}
        else {pinkFadeOpacity = 1; postFade++;}

        if (postFade == 70) {Exit_Normal(0);}
    }

    override void WorldTick()
    {
        totalTicks++;
    
        // Trying to prevent the unwinnable state from losing abilities on savefile load.
        if (totalTicks > 10 && totalTicks < 20 && !allComers) {AttachAllcomersForPlayer(net_arbitrator);}

        if (showPinkFade) 
        {
            TickPinkFade();
        }
        if (showBlackFade) 
        {
            TickBlackFade();
        }

        let ticksRemaining = 35 * (150) * 2; //2m30sx2
        ticksRemaining -= totalTicks;
        if (ticksRemaining == 0) // 0 time remaining.
        {
            
        }
        else if (ticksRemaining == 35 * 60 * 2) //1 min base, 2 min extended.  
        {
            let pp = spawnedPlayerInfo.mo;
            /*Console.PrintF("BMTicket2024: (%i), BMTicket2024x2: (%i), BMDoomressNoArmor: (%i), BMDisc: (%i), BMDangerNoodle: (%i)", 
            pp.CheckInventory("BMTicket2024", 1), pp.CheckInventory("BMTicket2024", 2), pp.CheckInventory("BMDoomressNoArmor", 1), pp.CheckInventory("BMDisc", 1), pp.CheckInventory("BMDangerNoodle", 1));*/

            //Console.PrintF("Things Died: (%i)", thingDieCount);
            
            // The whole "waste ammo to spook the characters" gimmick will take more time than I have left.
            // This unexplained hack will have to do.
            ammoCheck = thingDieCount > 5 && pp.CheckInventory("BMTicket2024", 2); 
            itemCheck = pp.CheckInventory("BMTicket2024", 1);
            itemCheck = itemCheck && pp.CheckInventory("BMDoomressNoArmor", 1);
            itemCheck = itemCheck && pp.CheckInventory("BMDisc", 1);
            itemCheck = itemCheck && pp.CheckInventory("BMDangerNoodle", 1);
            
            if (bmg && bmg.convoDirector && bmg.convoDirector.show && bmg.convoDirector.currentConvo) 
            {
                bmg.convoDirector.show = true;
                if (percivalDown && gasketDown) 
                {
                    // The level is setup to auto-end shortly in this case and shouldn't require a special case.
                    bmg.convoDirector.show = false;
                    //bmg.convoDirector.currentConvo.PopulateConvoByTopic(BM_TOPIC_POST_END_EMPTY_FILLER);
                }
                else if (percivalDown && !gasketDown) 
                {
                    bmg.convoDirector.currentConvo.PopulateConvoByTopic(BM_TOPIC_1M_SOLO_GASKET);
                }
                else if (!percivalDown && gasketDown) 
                {
                    bmg.convoDirector.currentConvo.PopulateConvoByTopic(BM_TOPIC_1M_SOLO_PERCI);
                }
                else if (!percivalDown && !gasketDown && itemCheck && ammoCheck) 
                {
                    bmg.convoDirector.currentConvo.PopulateConvoByTopic(BM_TOPIC_1M_CONVINCE_PERCI_GASKET);
                    if (bmftg) {bmftg.ticksBonusReveal = (35 * 2);}
                }
                else if (!percivalDown && !gasketDown && itemCheck) 
                {
                    bmg.convoDirector.currentConvo.PopulateConvoByTopic(BM_TOPIC_1M_CONVINCE_PERCI);
                }
                else if (!percivalDown && !gasketDown) 
                {
                    bmg.convoDirector.currentConvo.PopulateConvoByTopic(BM_TOPIC_1M_PERCI_GASKET);
                }
            }
        }
        else if (ticksRemaining == 35 * 30 * 2)  //30 sec base, 1 min extended.
        {

        }
        BMDialogueTopics curTop = BM_TOPIC_DEBUG;
        if (bmg && bmg.convoDirector && bmg.convoDirector.currentConvo) {curTop = bmg.convoDirector.currentConvo.currentTopic;} 
if (curTop == BM_TOPIC_FOLLOWER_BONUS // Adding some more lenient conditions to unlock this.
    || curTop == BM_TOPIC_ENDINGS_ARE_BEGINNINGS
    || curTop == BM_TOPIC_30S_CONVINCE_PERCI_GASKET
    || curTop == BM_TOPIC_30S_CONVINCE_PERCI)
{
    bmg.followerPercivalUnlocked = true;
}

/*
Why is the ZScript version of CheckItem not covered on any of the wikis?
bool CheckInventory(class<Inventory> itemtype, int itemamount, int owner = AAPTR_DEFAULT)
	{
		if (itemtype == null)
		{
			return false;
		}
		let owner = GetPointer(owner);
		if (owner == null)
		{
			return false;
		}

		let item = owner.FindInventory(itemtype);

		if (item)
		{
			if (itemamount > 0)
			{
				if (item.Amount >= itemamount)
				{
					return true;
				}
			}
			else if (item.Amount >= item.MaxAmount)
			{
				return true;
			}
		}
		return false;
	}
*/
    }
    
    ui void DrawFadeToPink()
    {

        let texID = TexMan.CheckForTexture("BMPINK02");
        Screen.DrawTexture(texID, true, 0, 0, DTA_Fullscreen, true, DTA_FullScreenScale, FSMode_ScaleToFill, DTA_Alpha, pinkFadeOpacity);
        // Probably increase some kind of post-full-fade counter before we end level?
        // Flag that we want to draw and increase it every tick.
    }

    ui void DrawFadeToBlack()
    {

        let texID = TexMan.CheckForTexture("BMPINK02");
        Screen.DrawTexture(texID, true, 0, 0, DTA_Fullscreen, true, DTA_FullScreenScale, FSMode_ScaleToFill, DTA_Alpha, pinkFadeOpacity, DTA_Color, 0xFF000000);
        // Probably increase some kind of post-full-fade counter before we end level?
        // Flag that we want to draw and increase it every tick.
    }
	
	override void RenderOverlay(RenderEvent e)
	{
        let bmDoomFont = HUDFont.Create(smallfont);
        //string strDebug = String.Format("(%i) IS THIS THING WORKING IS THIS THING WORKING IS THIS THING WORKING IS THIS THING WORKING IS THIS THING WORKING", postFade); 
        //statusbar.DrawString(bmDoomFont, strDebug, (0,0),
        //statusbar.DI_SCREEN_LEFT_TOP, Font.CR_UNTRANSLATED, 1.0, 400);

		if (showLevelWinSplash) 
		{
            DrawCustomLevelTallyOverlay();
		}
        
        if (showPinkFade) 
        {
            DrawFadeToPink();
        }
        if (showBlackFade) 
        {
            DrawFadeToBlack();
        }


        if (bmg && bmg.convoDirector && bmg.convoDirector.show && bmg.convoDirector.currentConvo) 
        {
            // Draw the backing texture.
            //dialogueBG = 
            TextureID test = TexMan.CheckForTexture("SBNEWSI");
            Screen.DrawTexture(test, true, 0, 64, DTA_Alpha, 0.666, DTA_ScaleX, 20, DTA_Color, 0x99000000);

            // Draw the text.
            //HUDfont bmDoomFont = HUDFont.Create(smallfont); // I don't want to create this every frame...
            int fontHeight = bmDoomFont.mFont.GetHeight();
            int fontLineOffset = 0;
            fontLineOffset += fontHeight + 2;

            //This draws where I want, but no built in wordwrap...
            
            //Screen.DrawText(smallfont, Font.CR_WHITE, 32, 32 + fontLineOffset, "Debug WOW THIS IS SOME TEXT\ntest/ntest\ntest/ntest\ntest/ntest\ntest/ntest", DTA_ScaleX, 2.0, DTA_ScaleY, 2.0);


            //statusbar.DrawString(bmDoomFont, "Debug WOW THIS IS SOME TEXT", (32,32 + fontLineOffset),
            //statusbar.DI_SCREEN_LEFT_TOP, Font.CR_UNTRANSLATED, 1.0, 128);
            fontLineOffset += fontHeight + 2;
            Screen.DrawText(smallfont, Font.CR_WHITE, 32, 64 + fontLineOffset, bmg.convoDirector.currentConvo.currentLine, DTA_ScaleX, 2.0, DTA_ScaleY, 2.0);
            //statusbar.DrawString(bmDoomFont, bmg.convoDirector.currentConvo.GetCurrentLine(), (0,fontLineOffset)); // oh boy i didn't plan for wordwrap. oop.
            // bmg.convoDirector.currentConvo.AdvanceLineCountdown(); DON'T DO THIS HERE, this runs on DRAW SPEED, which is much more variable than GameTicks.

        }
    }

    ui void DrawCustomLevelTallyOverlay()
    {
        // https://zdoom.org/wiki/DrawTexture
        // https://zdoom.org/wiki/Structs:TexMan
        // https://zdoom.org/wiki/Events_and_handlers
        let texID = TexMan.CheckForTexture("SBMENACE");
        Screen.DrawTexture(texID, true, 0, 0, DTA_Fullscreen, true, DTA_FullScreenScale, FSMode_ScaleToFill);
    }
}

// Despite the name, all of these are using projectiles for sake of time.
class KissPanicMeleeEnemy : Demon
{
	Default
	{
	    Tag "KissPanic Melee";
	    Radius 48;
        Height 48;
	    Scale 0.25;
	    BloodType "BMPinkGlitter";
        
        SeeSound "BMPBLAND";
        DeathSound "BMNSPLAT";
        ActiveSound "BMNKSTAP";
        PainSound "BMNKPDUP";
	}
	
    states
    {
        Spawn:
			BMSM A 25;
			Loop;        
		Pain:
			BMSM F 6;
			BMSM F 12 A_Pain;
			Goto See;
		Death:
			BMSM F random(10, 20);
			BMSM F 35 A_SpawnProjectile("BMFireyBoom");
			TNT1 A 12 Glitterburst();
			Stop;
    }
    
    void Glitterburst()
    {
        BMPinkGlitter glitter;
        glitter = BMPinkGlitter(Spawn("BMPinkGlitter", Pos));
        glitter.CountGenerations = 3;
        glitter.Vel = (0, 0, 2.0);
    }

    override void Tick()
    {
        Super.Tick();
        if (!target && players[net_arbitrator].mo) {target = players[net_arbitrator].mo;}
    }
}


class KissPanicKiss : BaronBall
{
 Default
 {
   Radius 6;
   Height 8;
   Scale 1;
   Speed 20;
   FastSpeed 40;
   Damage 3;
   Projectile;
   +RANDOMIZE
   RenderStyle "Normal";
   Alpha 1;
   SeeSound "BMPBLAND";
   DeathSound "BMPBLAND";
 }
 States
 {
 Spawn:
   BMLP A 4 BRIGHT;
   Loop;
 Death:
   BMLP A 6 BRIGHT;
   Stop;
 }
}

class KissPanicSam : KissPanicMeleeEnemy
{
	Default
	{
		Tag "KissPanic Sam";
	}
	
    states
    {
        Spawn:
			BMSM D 25;
			Loop;
		See:
			BMSM D 2 Fast A_Chase;
			Loop;
	    Melee:
	    Missile:
		    BMSM D 8 A_FaceTarget;
		    BMSM D 6 A_CustomComboAttack("KissPanicKiss", 32, 3 * random(1, 8), "BMPBLAND");
		    Goto See;
		Pain:
			BMSM I 6;
			BMSM I 12 A_Pain;
			Goto See;
		Death:
			BMSM I random(10, 20);
			// BMSM I 36 A_SpawnProjectile("BMFireyBoom");
			TNT1 A 12 Glitterburst();
			Stop;
    }
}

class KissPanicFran : KissPanicMeleeEnemy
{
	// Maybe give a nose laser hitscan or a kiss rocket or idk. Either the defeat sprite doesn't look very defeated so idk
	Default
	{
		Tag "KissPanic Fran";
	}
	
    states
    {
        Spawn:
			BMSM B 25;
			Loop;
		See:
			BMSM B 2 A_Chase;
			Loop;
        Melee:
	    Missile:
		    BMSM B 8 A_FaceTarget;
		    BMSM B 6 A_CustomComboAttack("KissPanicKiss", 32, 3 * random(1, 8), "BMPBLAND");
		    Goto See;
		Pain:
			BMSM G 6;
			BMSM G 12 A_Pain;
			Goto See;
		Death:
			BMSM G random(10, 20);
			BMSM G random(35, 70);
			//BMSM G 20 A_SpawnProjectile("BMFireyBoom");
			TNT1 A 12 Glitterburst();
			Stop;
    }
}

class KissPanicCig : KissPanicMeleeEnemy
{
	Default
	{
		Tag "KissPanic Cigfried";
	}
	
    states
    {
        Spawn:
			BMSM A 25;
			Loop;
        Melee:
	    Missile:
		    BMSM A 8 A_FaceTarget;
		    BMSM A 6 A_CustomComboAttack("KissPanicKiss", 32, 3 * random(1, 8), "BMPBLAND");
		    Goto See;
		See:
			BMSM A 2 A_Chase;
			Loop;
		Pain:
			BMSM F 6;
			BMSM F 12 A_Pain;
			Goto See;
		Death:
			BMSM F random(10, 20);
			BMSM F 140;
			// BMSM F 20 A_SpawnProjectile("BMFireyBoom");
            TNT1 A 12 Glitterburst();
			Stop;
    }
}

class KissPanicGo : KissPanicMeleeEnemy
{
	Default
	{
		Tag "KissPanic Gogo";
	}
	
    States
    {
        Spawn:
			BMSM C 25;
			Loop;
		See:
			BMSM C 2 Fast A_Chase;
			Loop;
        Melee:
	    Missile:
		    BMSM C 8 A_FaceTarget;
		    BMSM C 6 A_CustomComboAttack("KissPanicKiss", 32, 3 * random(1, 8), "BMPBLAND");
		    Goto See;
		Pain:
			BMSM H 6;
			BMSM H 12 A_Pain;
			Goto See;
		Death:
			BMSM H random(10, 20);
			BMSM H random(5, 45);
			// BMSM H 20 A_SpawnProjectile("BMFireyBoom");
			TNT1 A 12 Glitterburst();
			Stop;
    }
}

class BMPinkGlitter : Actor
{
    int countGenerations;
    int countGlitterPerGeneration;
    
    property CountGenerations: countGenerations;
    property CountGlitterPerGeneration: countGlitterPerGeneration;
    
	Default
	{
		Tag "Pink Glitter";
		
		Height 16;
		Radius 16;
		
		Gravity 0.07;
		Scale 2.0;
		RenderStyle "Add";
		
		BMPinkGlitter.CountGenerations 0; //Default to zero to avoid someone accidentally making an infinite loop.
		BMPinkGlitter.CountGlitterPerGeneration 4; //4*4*4 = 64.
	}
	
    States
    {
        Spawn:
			BMPS A Random(1,4);
			BMPS B Random(1,4) A_StartSound("BMGLITTR", CHAN_7);
			BMPS C Random(0,4);
			BMPS D Random(1,4);
			BMPS E Random(0,4);
			BMPS F Random(1,4);
			BMPS G Random(0,4);
			BMPS H Random(1,4);
        EarlyFade:
			BMPS I Random(0,4);
			BMPS J Random(1,4);
			TNT1 A Random(0,4);
			TNT1 A 2 Offspring();
			Stop;
    }
    
    void EarlyEndAttempt()
    {
        if (countGenerations >= 3)
        {
            SetState(FindState("EarlyFade"));
        }
    }
    
    void Offspring()
    {
        if (countGenerations > 0)
        {
            BMPinkGlitter glitter;
            for (int i = 0; i < countGlitterPerGeneration; i++)
            {
                int randomChance = random(0, 10);
                if (randomChance >= 3)
                {
                    glitter = BMPinkGlitter(Spawn("BMPinkGlitter", Pos));
                    glitter.CountGenerations = CountGenerations - 1;
                    glitter.Vel = (Random(-2.0, 2.0), Random(-2.0, 2.0), Random(1.0, 3.0));
                }
            }
        }
    }
}

class BMConversationDirector230 : Actor
{
    bool initialized;
    bool show;
    BMConv currentConvo;

    Default
    {
        +NOINTERACTION;
    }
    States
    {
        Spawn:
            BMBL A -1;
            Stop;
    }

    override void Tick()
    {
        if (!initialized) 
        {
            InitDirector();
            Console.PrintF("Initializing Director.");
        }
        if (currentConvo) {currentConvo.AdvanceLineCountdown();}
    }

    void InitDirector()
    {
        show = true;
        let bmg = BMGlobalVariables.Get();
        if (bmg) 
        {
            bmg.convoDirector = self;
        }

        currentConvo = BMConv.NewConvoByTopic(BM_TOPIC_INTRO);
        initialized = true;
    }
}


class BMGlobalVariables : Thinker
{
    bool followerPercivalUnlocked;
    int countTimesMapPlayed;
    Array<BMDialogueTopics> topicsPossible;
    Array<BMDialogueTopics> topicsAvailable;

    BMConversationDirector230 convoDirector;

    static BMGlobalVariables Get()
    {
        ThinkerIterator it = ThinkerIterator.Create("BMGlobalVariables", STAT_STATIC);
        let p = BMGlobalVariables(it.Next());
        if (!p)
        {
            p = new("BMGlobalVariables");
            p.ChangeStatNum(STAT_STATIC);
        }

        return p;
    }
    
    // 1: Do we need to be careful that Delete doesn't result in null gaps without decreasing the size?
    // 2: Is this Random inclusive of its ends? We should probably explicitly test that.
    BMDialogueTopics SelectRandomTopic()
    {
        BMDialogueTopics selected = BM_TOPIC_DEBUG;
        if (topicsAvailable.Size() < 1) 
        {
            ResetAvailableTopics();
        }
        
        int indexSelected = Random(0, topicsAvailable.Size() - 1);
        
        selected = topicsAvailable[indexSelected];
        topicsAvailable.Delete(indexSelected); // I really hope this auto-shits the remaining items.
        // If it doesn't I'll just comment out the delete so it doesn't break.
        
        return selected;
    }
    
    void ResetAvailableTopics()
    {
        if (topicsPossible.Size() < 1)
        {
            InitTopicsPossible();
        }
        topicsAvailable.Copy(topicsPossible);
    }
    
    void InitTopicsPossible()
    {
        topicsPossible.Push(BM_TOPIC_NATURE_OF_A_RAMP);
        topicsPossible.Push(BM_TOPIC_JOY_CREATION);
        topicsPossible.Push(BM_TOPIC_FEAR_FAILURE);
        topicsPossible.Push(BM_TOPIC_INSPIRATION);
        topicsPossible.Push(BM_TOPIC_VOICE);
        topicsPossible.Push(BM_TOPIC_INCIDENTAL);
        topicsPossible.Push(BM_TOPIC_FRIENDSHIP);
        topicsPossible.Push(BM_TOPIC_ART_BLOCK);
        topicsPossible.Push(BM_TOPIC_FEEDBACK);
        topicsPossible.Push(BM_TOPIC_CELEBRATION);
        topicsPossible.Push(BM_TOPIC_INTENT);
        topicsPossible.Push(BM_TOPIC_DOODLING);
        topicsPossible.Push(BM_TOPIC_CONVERSATION);
        topicsPossible.Push(BM_TOPIC_SOCIAL);
        
    }

        int DetermineTopicTime(string str)
        {
            // One second minimum, add more time for each letter.
            int ticks = 35;
            // https://zdoom.org/wiki/String#CodePointCount
            int numChars = str.CodePointCount();
            
            ticks += numChars * 10;            

            return ticks;
            
        }
        
        BMConv GetConv()
        {
            BMConv conv = new('BMConv');
            return conv;
        }

        //topicsPossible.Push(BM_TOPIC_INTRO);
        // I wanted to do a system with dynamic "voice" changing and character labels, but 
        // For lack of time and a concern about memory waste,
        // We'll just set these values in simple string arrays.


        //topicsPossible.Push(BM_TOPIC_RECURRING);


        //topicsPossible.Push(BM_TOPIC_NATURE_OF_A_RAMP);
        //topicsPossible.Push(BM_TOPIC_JOY_CREATION);
        //topicsPossible.Push(BM_TOPIC_FEAR_FAILURE);
        //topicsPossible.Push(BM_TOPIC_INSPIRATION);
        //topicsPossible.Push(BM_TOPIC_VOICE);
        //topicsPossible.Push(BM_TOPIC_INCIDENTAL);
        //topicsPossible.Push(BM_TOPIC_FRIENDSHIP);
        //topicsPossible.Push(BM_TOPIC_ART_BLOCK);
        //topicsPossible.Push(BM_TOPIC_FEEDBACK);
        //topicsPossible.Push(BM_TOPIC_CELEBRATION);
        //topicsPossible.Push(BM_TOPIC_INTENT);
        //topicsPossible.Push(BM_TOPIC_DOODLING);
        //topicsPossible.Push(BM_TOPIC_CONVERSATION);

        // topicsPossible.Push(BM_TOPIC_1M_PERCI_GASKET);
        // topicsPossible.Push(BM_TOPIC_30S_PERCI_GASKET);
        // topicsPossible.Push(BM_TOPIC_0S_PERCI_GASKET);
        // topicsPossible.Push(BM_TOPIC_1M_SOLO_PERCI);
        // topicsPossible.Push(BM_TOPIC_30S_SOLO_PERCI);
        // topicsPossible.Push(BM_TOPIC_0S_SOLO_PERCI);
        // topicsPossible.Push(BM_TOPIC_1M_SOLO_GASKET);
        // topicsPossible.Push(BM_TOPIC_30S_SOLO_GASKET);
        // topicsPossible.Push(BM_TOPIC_0S_SOLO_GASKET);
        // topicsPossible.Push(BM_TOPIC_1M_CONVINCE_PERCI);
        // topicsPossible.Push(BM_TOPIC_30S_CONVINCE_PERCI);
        // topicsPossible.Push(BM_TOPIC_0S_CONVINCE_PERCI);
        // topicsPossible.Push(BM_TOPIC_1M_CONVINCE_PERCI_GASKET);
        // topicsPossible.Push(BM_TOPIC_30S_CONVINCE_PERCI_GASKET);
        // topicsPossible.Push(BM_TOPIC_0S_CONVINCE_PERCI_GASKET);
        // topicsPossible.Push(BM_TOPIC_ENDINGS_ARE_BEGINNINGS);
        // topicsPossible.Push(BM_TOPIC_FOLLOWER_BONUS);



        //topicsPossible.Push(BM_TOPIC_SOCIAL);



}

class BMConv : Thinker
{
    int lineIndex;
    int countdownLineDisplay;
    array<string> lines;
    int oneSec;
    bool currentConvoDone;
    BMDialogueTopics currentTopic;
    string currentLine;

    string GetCurrentLine()
    {
        if (lineIndex >= lines.Size() || !lines[lineIndex]) 
        {
            return "";
        }
        else 
        {
            string result = lines[lineIndex];
            return result;
        }
    }
    void AdvanceLineCountdown()
    {
        countdownLineDisplay--;
        if (countdownLineDisplay < 1){AdvanceLine();}
        currentLine = GetCurrentLine();
    }
    void AdvanceLine()
    {
        lineIndex++;
        if (lineIndex < lines.Size() && lines[lineIndex]) 
        {
            countdownLineDisplay = CountdownLengthFromStrLen(lines[0]);
        }
        else
        {
            // Console.PrintF("Convo is done, you should probably handle that on next countdown end or something idk");
            countdownLineDisplay = 9999*9999;
            lineIndex = 0;
            currentConvoDone = true;

            if (currentTopic == BM_TOPIC_POST_END_EMPTY_FILLER) {PopulateConvoByTopic(BM_TOPIC_POST_END_EMPTY_FILLER);}

            else if (currentTopic == BM_TOPIC_1M_PERCI_GASKET) {PopulateConvoByTopic(BM_TOPIC_30S_PERCI_GASKET);}
            else if (currentTopic == BM_TOPIC_30S_PERCI_GASKET) {PopulateConvoByTopic(BM_TOPIC_0S_PERCI_GASKET);}
            else if (currentTopic == BM_TOPIC_0S_PERCI_GASKET) {PopulateConvoByTopic(BM_TOPIC_POST_END_EMPTY_FILLER);}

            else if (currentTopic == BM_TOPIC_1M_SOLO_PERCI) {PopulateConvoByTopic(BM_TOPIC_30S_SOLO_PERCI);}
            else if (currentTopic == BM_TOPIC_30S_SOLO_PERCI) {PopulateConvoByTopic(BM_TOPIC_0S_SOLO_PERCI);}
            else if (currentTopic == BM_TOPIC_0S_SOLO_PERCI) {PopulateConvoByTopic(BM_TOPIC_POST_END_EMPTY_FILLER);}

            else if (currentTopic == BM_TOPIC_1M_SOLO_GASKET) {PopulateConvoByTopic(BM_TOPIC_30S_SOLO_GASKET);}
            else if (currentTopic == BM_TOPIC_30S_SOLO_GASKET) {PopulateConvoByTopic(BM_TOPIC_0S_SOLO_GASKET);}
            else if (currentTopic == BM_TOPIC_0S_SOLO_GASKET) {PopulateConvoByTopic(BM_TOPIC_POST_END_EMPTY_FILLER);}

            else if (currentTopic == BM_TOPIC_1M_CONVINCE_PERCI) {PopulateConvoByTopic(BM_TOPIC_30S_CONVINCE_PERCI);}
            else if (currentTopic == BM_TOPIC_30S_CONVINCE_PERCI) {PopulateConvoByTopic(BM_TOPIC_0S_CONVINCE_PERCI);}
            else if (currentTopic == BM_TOPIC_0S_CONVINCE_PERCI) {PopulateConvoByTopic(BM_TOPIC_FOLLOWER_BONUS); }

            else if (currentTopic == BM_TOPIC_1M_CONVINCE_PERCI_GASKET) {PopulateConvoByTopic(BM_TOPIC_30S_CONVINCE_PERCI_GASKET);}
            else if (currentTopic == BM_TOPIC_30S_CONVINCE_PERCI_GASKET) {PopulateConvoByTopic(BM_TOPIC_ENDINGS_ARE_BEGINNINGS);}
            //else if (currentTopic == BM_TOPIC_0S_CONVINCE_PERCI_GASKET) {PopulateConvoByTopic(BM_TOPIC_ENDINGS_ARE_BEGINNINGS);}
            else if (currentTopic == BM_TOPIC_ENDINGS_ARE_BEGINNINGS) {PopulateConvoByTopic(BM_TOPIC_FOLLOWER_BONUS);}

            else if (currentTopic == BM_TOPIC_FOLLOWER_BONUS) {PopulateConvoByTopic(BM_TOPIC_POST_END_EMPTY_FILLER);}

            else {PopulateConvoByRandom();}
        }
    }

    // Maybe have a version that takes multiple lines? I don't have time now.
    void InterruptLine(string interruption)
    {
        PopulateConvoByTopic(BM_TOPIC_POST_END_EMPTY_FILLER);
        lines.Insert(lineIndex+1, interruption);
        AdvanceLine();
    }

    BMDialogueTopics PickRandomTopic()
    {
        let bmg = BMGlobalVariables.Get();
        return bmg.SelectRandomTopic(); 
    }

    void PopulateConvoByRandom()
    {
        currentTopic = PickRandomTopic();
        PopulateConvoByTopic(currentTopic);
    }
    
    void PopulateConvoByTopic(BMDialogueTopics topic)
    {
        currentTopic = topic;
        switch (topic)
        {
            case BM_TOPIC_INTRO:
                lines.Clear();
                // lines.Push("Percival: I think I hear something. Someone new, I mean.");
                // lines.Push("Gasket: Huh? Really? Can't remember the last time there's been another person here. Not that it matters much. Not much time left.");
                lines.Push("Percival: This place... Feels like it's dying.");
                lines.Push("Gasket: Everything ends eventually. Even the wildest party.\nBut something tells me this place isn't as dead as it seems.");
                lines.Push("Percival: Huh? Oh! I think I hear something. Someone new, I mean.");
                lines.Push("Gasket: I guess they're here for the going-away party.");
            break;

            case BM_TOPIC_RECURRING:
                lines.Clear();
                lines.Push("Gasket: I'm getting the strangest feeling that we've done this before.");
                lines.Push("Percival: We've been here for a while. Of course we mention the same topics sometimes.");
                lines.Push("Gasket: Not like that, hun. \nIt's as if timelines are jumping left and right, starting and stopping... ");
                lines.Push("Percival: Like it's all been reset?");
            break;

        case BM_TOPIC_NATURE_OF_A_RAMP:
                lines.Clear();
                lines.Push("Percival: If you had the power to make a world of your own and put anything you like in it, what would you make?");
                lines.Push("Gasket: Nothing good, probably. Why this all of a sudden?");
                lines.Push("Percival: I'm a terrible artist. I can barely draw a map and my penwork is unreadable!");
                lines.Push("Gasket: Often, a 'terrible artist' is just one who hasn't been given the encouragement \nand time to explore and find their niche. \nIf I ask you 'if you could make anything, what would you make?' \nwhat is the first thing to come to mind? Don't think too hard about it.");
            break;
        case BM_TOPIC_JOY_CREATION:
                lines.Clear();
                lines.Push("Gasket: So...? If you could make anything at all, what would you make?");
                lines.Push("Percival: I suppose I'd make a giant cup of tea to swim in! \nOr a big jello carrot planet that's super squishy and wiggly and juicy. \nBut... That sounds kinda silly-");
                lines.Push("Gasket: And I'd make a world of plush toys and motorbikes. At least, that's what I always told my brother. \nPerhaps, if you somehow get another chance? Just try making things for yourself. Experiment. Have fun. And besides, is swimming not an art? You seem plenty good at that.");
                lines.Push("Percival: I'll... think about it. Thank you.");
            break;
        case BM_TOPIC_FEAR_FAILURE:
                lines.Clear();
                lines.Push("Percival: What if... what if I'm not good enough? What if people don't like what I make?");
                lines.Push("Gasket: Hey, don't sweat it, Percy. Everyone starts somewhere. Just gotta keep tinkering and trying new things.");
                lines.Push("Percival: But what if I never find my... my spark? My thing?");
                lines.Push("Gasket: You will, hun. Just gotta keep those gears turning. Just try'nd enjoy yourself.");
            break;
        case BM_TOPIC_INSPIRATION:
                lines.Clear();
                lines.Push("Percival: Where do you find your inspiration, G?");
                lines.Push("Gasket: Anywhere. Everywhere, babe. The roar of an engine, the way light hits a chrome bumper, the thrill of a chase. \nEven an offhand joke with a stranger might ignite an idea.");
                lines.Push("Percival: Sounds exciting! I guess I find mine in... quieter places. Squishy toys, sweet treats, gentle sounds, calm pastel colors.");
                lines.Push("Gasket: To each their own, I guess. As long as it gets those creative juices flowing, right?");
    
            break;
        case BM_TOPIC_VOICE:
                lines.Clear();
                lines.Push("Gasket: You know, you've got a unique style, Percy. Don't let anyone tell you different.");
                lines.Push("Percival: You really think so? Sometimes I feel like my work is just... childish.");
                lines.Push("Gasket: Childish? Nah, nothing about you is childish. \nYour aesthetic's got charm. It's got heart. That's something you can't fake.");
                lines.Push("Percival: Thanks, G. That means a lot coming from you.");
  
            break;
        case BM_TOPIC_INCIDENTAL:
                lines.Clear();
                lines.Push("Percival: There was this one time where I was visiting the neighbors. And they had this huge, uh, decoration near the front door. \nI wasn't sure what it was, so I asked why they had such a massive thingy on display... and I tripped and fell on top of it.");
                lines.Push("Gasket: Let me guess... \nit was a carrot sculpture?");
                lines.Push("Percival: !!! It was so embarassing! But later that week, everyone started making... tributes? \nThey even made drawings of me! I swear \nI saved them on a disc somewhere, but I can't remember...");
                lines.Push("Gasket: Sounds like you were quite the muse, Percy.");
            break;
        case BM_TOPIC_FRIENDSHIP:
            lines.Clear();
            lines.Push("Percival: I'm glad we stuck together through all this, Gasket. I was scared when I first got here. \nAnd truthfully, I'm still scared. But having familiar faces to talk with helps a lot.");
            lines.Push("Gasket: Yeah, dunno where I'd be now if I hadn't met you, Percy. \nYou're not so bad for a fluffy bouncy bunny.");
            lines.Push("Percival: And you're not so bad for a motorhead.");
            lines.Push("Gasket: I'm as highly skilled a mercenary as I am a mechanic!"); 
            break;

        case BM_TOPIC_ART_BLOCK:
            lines.Clear();
            lines.Push("Percival: I'm feeling a bit stuck lately. \nLike my ideas have dried up.");
            lines.Push("Gasket: Creator's block, huh? Happens to the best of us. \nTry taking a break, doing something different for a while.");
            lines.Push("Percival: Maybe I should just give up. \nI'm not sure I'm cut out for whatever this is we're doing here.");
            lines.Push("Gasket: Don't say that, Percy. You've got talent. \nJust need to find that spark again. Take your time. \nRest if you need to. And we'll figure out what next when we get there.");
            break;

        case BM_TOPIC_FEEDBACK:
            lines.Clear();
            lines.Push("Percival: I like to hug everyone I meet. It's just so warm and pleasant. But sometimes people don't like that and I feel bad.");
            lines.Push("Gasket: Look, you can't please everyone. It's good to focus on making yourself happy... \nBut maybe ask first when touching other people or their belongs.");
            lines.Push("Percival: I don't want to make anyone uncomfortable. \nBut didn't you tell me to be myself and do things my way? To find my own style?");
            lines.Push("Gasket: You can bake your cakes however you like, \nbut when you cook for others, you ask their allergies before you give them a nasty surprise. \nYou might not get the chance to make ammends if you don't. \nWhen people ask for changes, don't let it get you down. Use it to learn and grow.");
            break;

        case BM_TOPIC_CELEBRATION:
            lines.Clear();
            lines.Push("Percival: Whatever happened to all the music? \nIt was all festive and trancy to celebrate all the new portals and visitors!");
            lines.Push("Gasket: Which one? The C64 styled one with the \nlow droning bass and the 'Dun da-da dun?' melody? \nOr the one with lots of layered pads?");
            lines.Push("Percival: C64? Layered pads? What does that even mean?");
            lines.Push("Gasket: Ah, sorry. Those are common in synth music. (Sorta.) \nPoint is, the was one song when the event started. \nThen a different song when everything opened to the public.");
            break;

        case BM_TOPIC_INTENT:
            lines.Clear();
            lines.Push("Percival: Sometimes I think I have a great idea, but when I try to share it it doesn't go according to plan.");
            lines.Push("Gasket: Yeah. Trying to estimate the meeting point of \nintent and execution is a skill on its own. \nAnd not a one-sided one. Even if you say what you intended, others might interpret it differently.");
            lines.Push("Percival: That's so annoying! It'd be much better \nif we could just have it exactly how we want it.");
            lines.Push("Gasket: Maybe. But sometimes the bumps \non the ride are what make it memorable.");
            break;

        case BM_TOPIC_DOODLING:
            lines.Clear();
            lines.Push("Percival: Sometimes I just do things without thinking. \nIt's like my body takes over.");
            lines.Push("Gasket: That can be good for the soul, Percy. \nIt's a way to let your mind wander and see what comes out.");
            lines.Push("Percival: Maybe I should try that more often. \nIt might help me get out of this creative funk.");
            lines.Push("Gasket: Give it a shot. You might surprise yourself.\n And if you don't like what you get, you don't have to share it.\nI guess, unless you do it in public while peopl watch.");
            break;
        case BM_TOPIC_CONVERSATION:
                lines.Clear();
                lines.Push("Gasket: People are really big into bats lately. I don't think I'd fancy being a bat. \nWings are gross and I can't imagine using a wrench with 'em.");
                lines.Push("Percival: I think you're make a beautiful bat if you wanted to be a bat!");
                lines.Push("Gasket: Aw, that's sweet of you. Come to think, \nI'm reminded of really bouncy portal I wandered into once. Reminds me of you.");
                lines.Push("Percival: You think I'm bouncy? \nWait, how can a portal be bouncy?");
            break;
        case BM_TOPIC_SOCIAL:
                lines.Clear();
                lines.Push("Gasket: I like a good thrill, but sometimes just \nkicking back and watching how things play out feels better.");
                lines.Push("Percival: I like being able to talk with all kinds of people. \nWhere I came from, there weren't many people \mand they all kept secrets.");
                lines.Push("Gasket: Do-ers, watchers, teachers, talkers. \nA strong community takes all sorts.");
                lines.Push("Percival: I've... only really ever met two of those sorts.");
            break;
        case BM_TOPIC_1M_PERCI_GASKET:
                lines.Clear();
                lines.Push("Percival: G-Gasket? The sky is turning pink... I'm scared!");
                lines.Push("Gasket: Easy, Percy. It's just the sky doing its thing. We've seen weirder, right?");
                lines.Push("Percival: But... but it feels different this time. Like it's really ending.");
                lines.Push("Gasket: Maybe it is. But endings always lead to new beginnings, right?");
                break;
        case BM_TOPIC_30S_PERCI_GASKET:
                lines.Clear();
                lines.Push("Percival: I've been waiting in this pool so long. I remember I made a promise to meet someone here. \nBut I just can't remember who. Or why. Or anything before the Green Rolling Hills back home. I guess time's up for this pool party...\nI don't want to disappear...");
                lines.Push("Gasket: I've been searching for my brother for so long. I don't remember how I ended up here. \nIt's a 'party', but maybe I'm here to pay for everyone I've hurt and mislead.");
                lines.Push("Percival: But you're one of the kindest people I've met. I can't imagine you hurting anyone on purpose.");
                lines.Push("Gasket: But I did. And I'm sure if this isn't the end, I will again. \nYou are a kind-natured soul and just need some time and experience. Not everyone is affectionate and trusting like you.");
            break;
        case BM_TOPIC_0S_PERCI_GASKET:
                lines.Clear();
                lines.Push("Percival: Why can't I remember...?");
                lines.Push("Gasket: I guess this is it, then...");
                lines.Push("...");
                lines.Push(" ");
            break;
        case BM_TOPIC_1M_SOLO_PERCI:
                lines.Clear();
                lines.Push("Percival: Gasket? ... I'm so scared...");
                lines.Push("Percival: The world is ending, and I'm all alone...");
                lines.Push("Percival: I don't want to be forgotten...");
                lines.Push("Percival: Please, someone... help me...");
                break;
            break;
        case BM_TOPIC_30S_SOLO_PERCI:
                lines.Clear();
                lines.Push("Percival: It's so cold... so dark...");
            lines.Push("...");
            lines.Push("Percival: I don't want to disappear...");
            lines.Push("Percival: Someone... anyone... please...");
            break;
        case BM_TOPIC_0S_SOLO_PERCI:
                lines.Clear();
                lines.Push("Percival: NO! I don't want to go!");
                lines.Push("Percival: help me");
                lines.Push("...");
                lines.Push(" ");
            break;
        case BM_TOPIC_1M_SOLO_GASKET:
                lines.Clear();
                lines.Push("Gasket: One minute left? Guess this is it. Well, at least I'm going out with a bang.");
                lines.Push("Gasket: Maybe I should've been nicer to that bunny. She was quirky, but she wasn't so bad, after all.");
                lines.Push("...");
                lines.Push("Gasket: Who are you, anyway? Why would you come back to...this?");
            break;
        case BM_TOPIC_30S_SOLO_GASKET:
                lines.Clear();
                lines.Push("Gasket: Wish I could see my brother one last time. \nTell him I'm sorry for all the trouble I caused.");
                lines.Push("Gasket: Wish I could see my brother one last time. \nTell him I'm sorry for all the trouble I caused.");
                lines.Push("...");
                lines.Push("Gasket:  Hey, world! You ain't seen the last of Gasket!");
            break;
        case BM_TOPIC_0S_SOLO_GASKET:
                lines.Clear();
                lines.Push("Gasket: Huh. Pretty. I guess this is it, then.");
                lines.Push("Gasket: Sorry, Bro. If there's something after this, maybe I'll find you there.");
                lines.Push("...");
                lines.Push(" ");
            break;
        case BM_TOPIC_1M_CONVINCE_PERCI:
                lines.Clear();
                lines.Push("Percival: I forgot about this dress! Where did you find it? I thought I lost it forever...");
                lines.Push("Gasket: Someone's been holding onto your old things, Percy. Maybe they're trying to tell you something?");
                lines.Push("Percival: But why would they bring it here? They found it. They could do anything with it. But they brought it back to RAMP?");
                lines.Push("Gasket: A memo of the good times. Or, maybe they want you to move on.");
            break;
        case BM_TOPIC_30S_CONVINCE_PERCI:
                lines.Clear();
                lines.Push("Percival: My disc! I know I was supposed to guard it like a key, but it had all of my favorite art and music on it!");
                lines.Push("Gasket: It's got your favorite music? Shame we don't have time for me to hear it.");
                lines.Push("Percival: If this stranger found my disc and found a way here, maybe they can find a way out so we can play it back?");
                lines.Push("Gasket: Nah. I'm good. I'm tired. I'll... catch up with you later.");
            break;
        case BM_TOPIC_0S_CONVINCE_PERCI:
                lines.Clear();
                lines.Push("Percival: I see the train! Thank you for reminding me of what's important. I won't forget this!");
                lines.Push("Gasket: Have a blast out there, Percy!");
                // lines.Push("...");
                // lines.Push("Gasket: Why are you still here? Well... that was anticlimactic.");
            break;
        case BM_TOPIC_1M_CONVINCE_PERCI_GASKET:
                lines.Clear();
                lines.Push("Percival: You found all my things! Does this mean...? Can we leave this place?");
                lines.Push("Gasket: Hey, easy there, hotshot! What's with all the firepower?");
                lines.Push("Percival: Maybe they're just excited. They want us to come with them!");
                lines.Push("Gasket: Alright, alright. If you're so eager to leave, I'm not gonna stop you. Let's see what this train's all about.");
            break;
        case BM_TOPIC_30S_CONVINCE_PERCI_GASKET:
                lines.Clear();
                lines.Push("Percival: Get to the train! We're coming! We're coming!");
                //lines.Push("Percival: Get to the train! We're coming! We're coming!");
                //lines.Push("Gasket: Hold on tight, Percy. This is gonna be a hell of a ride!");
                lines.Push("Gasket: Hold on tight, Percy. This is gonna be a hell of a ride!");
            break;
        case BM_TOPIC_0S_CONVINCE_PERCI_GASKET:
                lines.Clear();
            break;
        case BM_TOPIC_ENDINGS_ARE_BEGINNINGS:
                lines.Clear();
                lines.Push("Gasket: Here's to the next adventure, whatever it may be.");
                lines.Push("Percival: Eeeee, I'm getting excited! I'll miss home, but I've never been on a train before!");
                lines.Push("Gasket: Next time we get a chance, I'm gonna take you on my bike. You'll love it, hun!");
                lines.Push("Percival: Yeah! Yeah! That could be cool to try!");
            break;
        case BM_TOPIC_FOLLOWER_BONUS:
                lines.Clear();
                lines.Push("Goodbye RAMP!\nAnd...\nHello RAMP!");
                lines.Push("You may now summon Percival at any time via console with:\nsummon BMPercivalFollower\n(You always could. This was supposed to be an unlockable but I ran out of time lol.)");
                lines.Push("Wishing you all the best and hope you're having a good time!\nStay strong, explore, experiment, create, and get messy! It's more fun that way (usually)!");
        case BM_TOPIC_POST_END_EMPTY_FILLER:
                lines.Clear();
                lines.Push("");
                lines.Push("");
                lines.Push("");
                lines.Push("");
            break;
            default:
                lines.Clear();
                lines.Push("AAAAAA YOU BROKE THE CUTSCENE TEXT DIALOGUE SYSTEM THINGY THIS CLSOE OT THEEND OF RAMP WHAT DID YOU DO"); 
            break;
        }
        countdownLineDisplay = CountdownLengthFromStrLen(lines[0]);
        currentConvoDone = false;
    }

    int CountdownLengthFromStrLen(string str)
    {
        int ticks = 70 + lines[0].Length() * 5;

        // Time's up for me to work on this. AS a hack, we'll set all texts to be 5 seconds.
        // okay i lied slightly, doubling that to 10 because there's not enough time to actually search the map.
        ticks = 35 * 10;

        //console.PrintF("Ticks For Talk: %i", ticks);
        return ticks;
    }

    static BMConv NewConvoByTopic(BMDialogueTopics topic)
    {
        BMConv newConv = new('BMConv');
        newConv.oneSec = 35;
        newConv.PopulateConvoByTopic(topic); 
        return newConv;
    } 
}

enum BMDialogueTopics
	{
	    BM_TOPIC_DEBUG,
	    BM_TOPIC_INTRO,
	    BM_TOPIC_RECURRING,
		BM_TOPIC_NATURE_OF_A_RAMP,
		BM_TOPIC_JOY_CREATION,
		BM_TOPIC_FEAR_FAILURE,
		BM_TOPIC_INSPIRATION,
		BM_TOPIC_VOICE,
		BM_TOPIC_INCIDENTAL,
		BM_TOPIC_FRIENDSHIP,
		BM_TOPIC_ART_BLOCK,
		BM_TOPIC_FEEDBACK,
		BM_TOPIC_CELEBRATION,
		BM_TOPIC_INTENT,
		BM_TOPIC_DOODLING,
		BM_TOPIC_CONVERSATION,
		BM_TOPIC_SOCIAL,

		BM_TOPIC_1M_PERCI_GASKET,
		BM_TOPIC_30S_PERCI_GASKET,
		BM_TOPIC_0S_PERCI_GASKET,

		BM_TOPIC_1M_SOLO_PERCI,
		BM_TOPIC_30S_SOLO_PERCI,
		BM_TOPIC_0S_SOLO_PERCI,

		BM_TOPIC_1M_SOLO_GASKET,
		BM_TOPIC_30S_SOLO_GASKET,
		BM_TOPIC_0S_SOLO_GASKET,

        BM_TOPIC_1M_CONVINCE_PERCI,
		BM_TOPIC_30S_CONVINCE_PERCI,
		BM_TOPIC_0S_CONVINCE_PERCI,

        BM_TOPIC_1M_CONVINCE_PERCI_GASKET,
		BM_TOPIC_30S_CONVINCE_PERCI_GASKET,
		BM_TOPIC_0S_CONVINCE_PERCI_GASKET,

		BM_TOPIC_ENDINGS_ARE_BEGINNINGS,
		BM_TOPIC_FOLLOWER_BONUS,
        BM_TOPIC_POST_END_EMPTY_FILLER,
	};

	class BMNPCGasket2m30s : Actor
    {
    	Default
    	{
    		DropItem "BMBSting";
    		Health 5;
    		PainChance 200;
    		Radius 16;
    		Height 56;
    		Mass 100;
    		Speed 20;
    		Damage 0;
    		PainChance 0;
            Scale 0.25;
    		Activation THINGSPEC_Default;
    		Monster;
    		-CountKill;
    		+NEVERTARGET;
    		-COUNTKILL;
    		+NOSPLASHALERT;
    		+FLOORCLIP;
    		+JUSTHIT;
    		+FRIENDLY;
    		+USESPECIAL;
    		-BUMPSPECIAL;
            BloodType "BMPinkGlitter";
    		MinMissileChance 150;
    		MaxStepHeight 16;
    		MaxDropoffHeight 32;
    		SeeSound "peasant/sight";
    		AttackSound "peasant/attack";
    		PainSound "";
    		DeathSound "";
    		HitObituary "%o stepped in front of Gasket's shotgun.";
    	}

      States
      {
    	Spawn:
    		BMSK A 70 A_LookEX;
    		Loop;
    	See:
    		BMSK A 5 A_FaceTarget;
    		Loop;
      }

        override bool Used(Actor user)
        {
            // Add your code here to define what happens when the actor is used.
            //A_Log("attempting to start conversation.");
            //StartConversation(50, 1);
            //A_Log("Conversation should have started?");
            //A_Log("I'm looking for my brother. If you can fight, I'll go with.");
            //A_Log("I'm looking for my brother. If you see him, let me know.");
            //StartConversation(50, 1); // If this ever actually works, I'll be stunned.
            return true; // Return true if the actor was used.
        }
    }

    class BMNPCPercival2m30s : BMNPCGasket2m30s
    {
    	Default
    	{
    		DropItem "BMBSting";
    		Monster;
    		-CountKill;
    		+NEVERTARGET;
    		+NOSPLASHALERT;
    		+FLOORCLIP;
    		+JUSTHIT;
    		+FRIENDLY;
    		+USESPECIAL;
    		-BUMPSPECIAL;
            BloodType "BMPinkGlitter";
    		MinMissileChance 150;
    		MaxStepHeight 16;
    		MaxDropoffHeight 32;
    		SeeSound "peasant/sight";
    		AttackSound "peasant/attack";
    		PainSound "";
    		DeathSound "";
    		HitObituary "%o was finished by Percival's pot shots.";
    	}

      States
      {
    	Spawn:
    		BMPB A 70 A_LookEX;
    		Loop;
    	See:
    		BMPB A 5 A_FaceTarget;
    		Loop;
      }

    override bool Used(Actor user)
    {
        // Add your code here to define what happens when the actor is used.
        //A_Log("attempting to start conversation.");
        //StartConversation(50, 1);
        //A_Log("Conversation should have started?");
        //A_Log("I'm looking for my brother. If you can fight, I'll go with.");
        //A_Log("I'm looking for my brother. If you see him, let me know.");
        //StartConversation(50, 1); // If this ever actually works, I'll be stunned.
        return true; // Return true if the actor was used.
    }
}

class BMWorldCountdown : Thinker
{
    int timeAsTicks;

    int timeFractionalSecondsPortion;
    int timeSecondsPortion;
    int timeMinutesPortion;


    void TickDown()
    {
        if (timeAsTicks > 0)
        {
            SetTimeFromTicks(timeAsTicks - 1);
        }
    }

    int SetTimeFromTicks(int ticks)
    {
        int ticksPerSecond = 35; // This is just how Doom works.
        timeAsTicks = ticks;
        timeMinutesPortion = timeAsTicks / ticksPerSecond / 60;
        timeSecondsPortion = timeAsTicks % ticksPerSecond;
        timeFractionalSecondsPortion = timeSecondsPortion / 35;
        return timeAsTicks;
    }

    int SetTimeFromSeconds(int seconds)
    {
        int ticksPerSecond = 35; // This is just how Doom works.
        timeAsTicks = seconds * ticksPerSecond;
        return SetTimeFromTicks(timeAsTicks);
    }
}

class BMGroundPound : Actor
{
    int minLifespan;
    property MinLifespan: minLifespan;

	Default
	{
		Scale 1.0;

		Radius 16.0;
        Height 16.0;
        Speed 0;
        Damage 200;
		Alpha 1;
		SeeSound "";
		DeathSound "";
		Tag "Ground Pound Damage Zone";
		Obituary "%o was FLATTENED by their foe.";

		BMGroundPound.MinLifespan 5;
	}
	States
	{
	Spawn:
	    TNT1 A 35;
		TNT1 A 350; // It is extremely unlikely an entity will be in the air-fall state for more than 10 seconds.
		Goto Death;
	Death:
		TNT1 A 0 PrintDebugDie();
		Stop;
	}

	override void Tick()
	{
	    // If the creator exists and isn't fast-falling anymore (for example, landed), destroy.
	    bool willDestroy = (!master || master.Vel.Z >= -1.0);
	    willDestroy = willDestroy && minLifespan <= 0;

	    if (minLifespan > 0) {minLifespan--;}

	    Super.Tick();
	    if (!willDestroy)
        {
            SetOrigin(master.Pos, true);
	        Console.PrintF("GroundPound Move");
        }
	    else
	    {
	        Console.PrintF("GroundPound Detroy");
            ExplodeMissile();
        }
	}

	override bool CanCollideWith(Actor other, bool passive)
	{
	    bool collide = super.CanCollideWith(other, passive);
        if (!passive
            && collide
            && master
            && other != master)
        {
            target = master; // This should probably be somewher else.
            //DoMissileDamage(other);
            // Since this doesn't destroy, treat it like it's a Ripper.
        }
        return collide;
	}

	void PrintDebugDie()
	{
	    Console.PrintF("GP area died from wall/floor???");
	}
}

class BMTelevision : Actor
{
    Default
    {
        Tag "Deadly Television";
		Health 20;
        Scale 1;
        Radius 64;
        Height 64;
		Speed 0;
		+SOLID;
		+SHOOTABLE;
		+NOBLOOD;
		+ACTIVATEMCROSS;
		+DONTGIB;
		+NOICEDEATH;
        -CountKill;
    }
    States
    {
        Spawn:
            BMTV A 1;
            BMTV A 1 A_StartSound("BMKILLEM", CHAN_5, CHANF_LOOP);
            BMTV A -1;
            Stop;
        Death:
            TNT1 A 0 A_SpawnProjectile("BMFireyBoom");
            Stop;
    }
}

class BMFakeTextureGlitch : Actor
{
    int timerFlipFlat;
    int countdownChangeSky;
    int timerSec30;
    double quakeIntensity;
    int totalTicks;
    int ticksBonusReveal;
    sector sectorToChange;
    BMEventsPlayAsALLCOMERS eventHandler;

    Array<string> skies;

    PlayerPawn playerToShake;
    
    Array<TextureID> validTextureIDsForGlitch;

    Default
    {
        RenderStyle "None";
    }
    States
    {
        Spawn:
            BMBL A 2 NoDelay ResetSecondsTimer();
            BMBL A -1;
            stop;
    }

    void ResetSecondsTimer()
    {
        timerSec30 = 35 * 30;
        quakeIntensity += 0.5; // Scale is 1 to 9. 
    }
    
    override void Tick()
    {
        Super.Tick();
        totalTicks++;

        if (!eventHandler) 
        {
            eventHandler = BMEventsPlayAsALLCOMERS(EventHandler.Find("BMEventsPlayAsALLCOMERS"));
            if (!eventHandler.bmftg) {eventHandler.bmftg = self;}
        }

        if (timerFlipFlat > 0) {timerFlipFlat--;}
        else 
        {
            // Console.PrintF("Glitch#i", timerFlipFlat);
            GlitchRandomTexture();
            ResetFlipTimer();
        }

        if (timerSec30 > 0) {timerSec30--;}
        else 
        {
            // Quake
            if (!playerToShake)
            {
                playerToShake = players[net_arbitrator].mo;
            }

            int fiveSeconds = 35 * 5 * 2;
            playerToShake.A_QuakeEx(quakeIntensity, quakeIntensity, quakeIntensity / 1.3, fiveSeconds + (quakeIntensity*35*2), 0, 10, "", QF_SHAKEONLY | QF_SCALEUP); // 
            Level.ForceLightning();
            
            // Reset
            ResetSecondsTimer();
        }
        
        let ticksRemaining = (35 * (150) * 2) - (35 * 13) + ticksBonusReveal; 
        // After extension, this is 5 minutes. We kicked out 13 seconds of fade out.
        // If player convinced both of the other characters, we cancel out that time bonus so we can show the extra dialogue.
        ticksRemaining -= totalTicks;
        if (ticksRemaining == 0) // 0 time remaining.
        {
            //Console.PrintF("Time's up! Time to move on to the next RAMP.");
            let eventHandler = BMEventsPlayAsALLCOMERS(EventHandler.Find("BMEventsPlayAsALLCOMERS"));
            if (!eventHandler) 
            {
                Console.PrintF("Failed to get reference to PlayableAllcomers event handler.");
            }
            else 
            {
                // Console.PrintF("Got the event handler. Fade things out.");
                eventHandler.StartFadeToPink();
            }
        }
        else if (ticksRemaining == 35 * 60 * 2) //1 min base, 2 min extended.  
        {
            
        }
        else if (ticksRemaining == 35 * 30 * 2)  //30 sec base, 1 min extended.
        {

        }
        DarkenSector(GetRandomWorldSector(), 5);
    }
    
    void GlitchRandomTexture()
    {
        sectorToChange = GetRandomWorldSector();
        if (!sectorToChange) {return;}
        else
        {
            sectorToChange.SetTexture(0, GetRandomValidTextureIdForGlitch());
            DarkenSector(sectorToChange);

            countdownChangeSky--;
            if (countdownChangeSky < 1) 
            { 
                countdownChangeSky = 35;

                TextureId skyID;
                if (quakeIntensity > 3) 
                {
                    skyID = TexMan.CheckForTexture("BMPINK01");
                    GlitchSides(skyID);
                }
                else if (quakeIntensity > 4) 
                {
                    skyID = TexMan.CheckForTexture("BMPINK02");
                    GlitchSides(skyID);
                }
                else
                {
                    skyID = PickRandomSky();
                }
                level.ChangeSky(skyID, skyID);
            }

            if (quakeIntensity > 3) 
            {
                let sideTexID = TexMan.CheckForTexture("BMPINK01");
                GlitchSides(sideTexID);
            }
            else if (quakeIntensity > 4) 
            {
                let sideTexID = TexMan.CheckForTexture("BMPINK02");
                GlitchSides(sideTexID);
            }
        }
    }

    void DarkenSector(sector sectorToChange, int rate = 10)
    {
        int ll = sectorToChange.GetLightLevel();
        if (ll > 20){ll -= rate;}
        else {ll += 31;}
        // Had a careless error here: ChangeLightLevel is relative, Set is not.
        sectorToChange.SetLightLevel(ll);
    }

    void GlitchSides(TextureID textureID)
    {
        let currentSide = GetRandomWorldSide();
        //Special protections for the train exit:
        if (currentSide.linedef.Index() == 1230  
            || currentSide.linedef.Index() == 1222
            || currentSide.linedef.Index() == 1228 ) {return;}
        currentSide.SetTexture(Side.bottom, textureID);
        currentSide.SetTexture(Side.mid, textureID);
        currentSide.SetTexture(Side.top, textureID);
        

/*    TextureID GetTexture(int which)

    Returns a TextureID of the texture at the top/mid/bottom of the sidedef.

        int which — part of the sidedef (Side.top, Side.mid or Side.bottom)

    void SetTexture(int which, TextureID tex)

    Changes the texture at the top/mid/bottom of the sidedef to the provided TextureID.

        int which — part of the sidedef (Side.top, Side.mid or Side.bottom)
        TextureID tex — the texture to use*/
    }
    
    TextureID GetRandomValidTextureIdForGlitch()
    {
        if (validTextureIDsForGlitch.Size() < 1) 
        {
            InitValidTextures();
        }

        int indTex = Random(0, validTextureIDsForGlitch.Size() - 1);
        TextureID texID = validTextureIDsForGlitch[indTex];
        if (quakeIntensity>=3) {texID = TexMan.CheckForTexture("BMPINK01");}
        if (quakeIntensity>=4) {texID = TexMan.CheckForTexture("BMPINK02");}
        return texID;
    }

        void InitSkies()
        {
            //skies.Push("OWARPB01");
            skies.Push("SKY1");
            //skies.Push("SKY2");
            skies.Push("SKY3");            
            skies.Push("BMHVRED2");
            skies.Push("SBNEWSI");
            //skies.Push("BMPINK02"); Pink shy should not be part of the normal set.
        }
        TextureID PickRandomSky()
        {
            if (skies.Size() < 1) {InitSkies();}
            int ind = Random(0, skies.Size() - 1);
            return TexMan.CheckForTexture(skies[ind]); 
        }
    
    void InitValidTextures()
    {
        validTextureIDsForGlitch.Push(TexMan.CheckForTexture("BMSKY2"));
        validTextureIDsForGlitch.Push(TexMan.CheckForTexture("BMHVBARS"));
        validTextureIDsForGlitch.Push(TexMan.CheckForTexture("BMHVGRN"));
        validTextureIDsForGlitch.Push(TexMan.CheckForTexture("BMSTATIC"));
        // validTextureIDsForGlitch.Push(int(TexMan.CheckForTexture("BMPINK01"))); Not in random pool.
        validTextureIDsForGlitch.Push(TexMan.CheckForTexture("BMPINK02"));
        validTextureIDsForGlitch.Push(TexMan.CheckForTexture("SBCNCEPT"));
        validTextureIDsForGlitch.Push(TexMan.CheckForTexture("SLIME01"));
        validTextureIDsForGlitch.Push(TexMan.CheckForTexture("LAVA1"));
        validTextureIDsForGlitch.Push(TexMan.CheckForTexture("NUKAGE1"));
        validTextureIDsForGlitch.Push(TexMan.CheckForTexture("OWARPA01"));
        validTextureIDsForGlitch.Push(TexMan.CheckForTexture("OWARPA01"));
        validTextureIDsForGlitch.Push(TexMan.CheckForTexture("OWARPA01"));
        validTextureIDsForGlitch.Push(TexMan.CheckForTexture("OLAVAE01"));


        // validTextureIDsForGlitch.Push(int(TexMan.CheckForTexture("BMPINK02")));
        // validTextureIDsForGlitch.Push(int(TexMan.CheckForTexture("BMPINK02")));

        validTextureIDsForGlitch.Push(TexMan.CheckForTexture("F_SKY1")); //This was TNT1A0, but I don't think that works here.
        validTextureIDsForGlitch.Push(TexMan.CheckForTexture("F_SKY1"));
        validTextureIDsForGlitch.Push(TexMan.CheckForTexture("F_SKY1"));
        validTextureIDsForGlitch.Push(TexMan.CheckForTexture("F_SKY1"));
    }
    
    sector GetRandomWorldSector()
    {
        int ind = Random(0, level.sectors.Size() -1);
        return level.sectors[ind];
    }

    Side GetRandomWorldSide()
    {
        int ind = Random(0, level.sides.Size() -1);
        return level.sides[ind];
    }
    
    void ResetFlipTimer()
    {
        timerFlipFlat = Random(20, 45);
        if (quakeIntensity > 2) {timerFlipFlat = 4;}
        if (quakeIntensity > 3) {timerFlipFlat = 2;}
    }
}

/*
JediMB (she/they) — 06/02/2023 4:55 PM
Okay, so the way it works is that every MAP inside of a WAD is split up into multiple lumps. What kinds of lumps depends on the format, but for UDMF it's:

MAPnn is just an empty lump. It's a "START"-type marker
TEXTMAP contains the actual UDMF map data
BEHAVIOR is compiled ACS
ZNODES is node tree stuff that I don't even understand
SCRIPTS is your ACS (or equivalent) readable code
ENDMAP is another empty lump, telling you there are no more lumps associated with that map

You had gotten all of your other data lumps stuck in-between the start and end lumps for your map, so chaos ensued, as RAMPART couldn't identify what was your map data and what was all your other data.
*/

/*
"Annie? I haven't seen anyone named Annie."
"I'm looking for my lil brother. If you're heading to the woods, I'll join you."
"I don't think those things are alive. They don't breathe, they don't eat, and they don't feel pain."

Patcher (Punch)
Digouter -> Zilly ( Chainsaw)
BSting -> Stinger (Projectile Pistol)
Fallstreak -> Cyclone Slug / Wind Bullet (Hitscan Pistol)
Serpentine -> Stinger (Hitscan SMG)
Blowback Generator -> Wind Bullet (Hitscan Rockets!?)
Blue Yum (Soul Sphere)


*/