

/*
* Version 0.22.0
*
* This version designed for RAMP 2024 so all scripts have to be on 1 page.
* If released as a standalone see the "//filename: " comments on how it will be
* split up.
*
* Filenames:-
* ZSCRIPT/ENEMIES
* ZSCRIPT/WEAPPONS
* ZSCRIPT/PLAYER
*
* Change log
	0.22.0:
		Augmented the lancer gun and slightly nerfed crowd control gun
	0.21.2:
		Fixed breaking the use button from last fix
	0.21.1:
		Prevented map restarting if pressing the use key if died still with lives left
	0.21.0:
		Gave spider boss more health. Removed console debug messages
	0.20.0:
		Added difficulty settings in. Tweaked Barrage Rifle bullet speed
	0.19.0:
		Added player invincibility when completing section. Boss explodes more now
	0.18.0:
		Tweaked boss spider and chaingunner firing
	0.17.0:
		Worked on boss spider. Explosions on lowering arena barriers
	0.16.0:
		Replaced the spider mastermind with a non-hitscan version
	0.15.0:
		Spawning in enemies when "arena boss" is attacked
	0.14.0:
		When all enemies of the same ID are killed it lowers sector with same ID
	0.13.0:
		Added flash when hit and explosions when lose weapon/life
	0.12.0:
		Added temporary invunerability after losing weapon/life
	0.11.0:
		Added HUD
	0.10.0:
		Recoloured and renamed Lancer Gun. Guns now destroy correctly
	0.9.0:
		Sorted out weapon projectile sprites and sounds
	0.8.0:
		3 Non-boss hitscanners replaced using an event
	0.7.0:
		Player now has lives and is resurrected
	0.6.0:
		Weapons now absorb damage and break
	0.5.0:
		Added Micro Missile Gun
		Fixed Lancer Gun not showing when summoned
	0.4.0:
		Added Lancer Gun
	0.3.0:
		Added Crowd Control Gun
	0.2.0:
		Added Barrage Rifle
	0.1.0:
		Added non-hitscan version of the hitscan former humans
*/

// Constants

////////////////////////////////////////////////////////////////////////////////
//filename: ZSCRIPT/ENEMIES
////////////////////////////////////////////////////////////////////////////////


// What some of the hitscanner enemies fire instead
// Place holder graphics and sound
class BulletProjectile : Actor
{
	Default
	{
		Radius 6;
		Height 16;
		Speed 20;
		FastSpeed 30;
		Damage 5;
		Projectile ;
		+RANDOMIZE
		+ZDOOMTRANS
		RenderStyle "Add";
		Alpha 1;
		// SeeSound "grunt/attack";
		DeathSound "baron/shotx";
		Decal "BulletChip";
	}
	States
	{
	Spawn:
		SUIE AB 4 BRIGHT;
		Loop;
	Death:
		SUIE CDE 6 BRIGHT;
		Stop;
	}
}


//===========================================================================
//
// Zombie man
//
//===========================================================================
class ZombieManNonHitScan : ZombieMan
{
	Default
	{
		DropItem "";
		AttackSound "grunt/attack";
	}
	States
	{
	Missile:
		POSS E 10 A_FaceTarget;
		POSS F 8 A_BulletProjectile;
		POSS E 8;
		Goto See;
	}
	
	void A_BulletProjectile()
	{
		if (target)
		{
			A_StartSound(AttackSound, CHAN_WEAPON);
			A_FaceTarget();
			SpawnMissile (target, "BulletProjectile");
		}
	}
}


//===========================================================================
//
// Sergeant / Shotgun guy
//
//===========================================================================
class ShotgunGuyNonHitScan : ShotgunGuy
{
	Default
	{
		AttackSound "shotguy/attack";
		DropItem "";
	}
	States
	{
	Missile:
		SPOS E 10 A_FaceTarget;
		SPOS F 10 BRIGHT A_ShotgunProjectile;
		SPOS E 10;
		Goto See;
	}
	
	private void A_ShotgunProjectile()
	{
		if (target)
		{
			A_StartSound(AttackSound, CHAN_WEAPON);
			A_FaceTarget();
			double bangle = angle;
			double slope = AimLineAttack(bangle, MISSILERANGE);
		
			for (int i=0 ; i<3 ; i++)
			{
				double ang = bangle + Random2[SPosAttack]() * (22.5/256);
				int damage = Random[SPosAttack](1, 5) * 3;
				Actor missile = SpawnMissile (target, "BulletProjectile");
				if (missile)
				{
					missile.Angle = ang;
					missile.VelFromAngle();
				}
			}
		}
    }
}

//===========================================================================
//
// Chaingunner
//
//===========================================================================
class ChaingunGuyNonHitScan : ChaingunGuy
{
	Default
	{
		AttackSound "chainguy/attack";
		Dropitem "";
	}
	States
	{
	Missile:
		CPOS E 10 A_FaceTarget;
		CPOS F 4 BRIGHT A_ChaingunProjectile;
		CPOS E 4 BRIGHT;
		CPOS F 1 A_ChaingunProjectileRefire;
		Goto Missile+1;
	}
	
	private void A_ChaingunProjectile()
	{
		if (target)
		{
			if (bStealth) visdir = 1;
			A_StartSound(AttackSound, CHAN_WEAPON);
			A_FaceTarget();
			double slope = AimLineAttack(angle, MISSILERANGE);
			double ang = angle + Random2[CPosAttack]() * (22.5/256);
			int damage = Random[CPosAttack](1, 5) * 3;
			SpawnMissile (target, "BulletProjectile");
		}
	}
	
	// Might be redundant
	void A_ChaingunProjectileRefire()
	{
		// keep firing unless target got out of sight
		A_FaceTarget();
		if (Random[CPosRefire](0, 255) >= 40)
		{
			if (!target
				|| HitFriend()
				|| target.health <= 0
				|| !CheckSight(target, SF_SEEPASTBLOCKEVERYTHING|SF_SEEPASTSHOOTABLELINES))
			{
				SetState(SeeState);
			}
		}
	}
}

//===========================================================================
//
// SpiderMastermind
//
//===========================================================================
class SpiderMastermindNonHitScan : SpiderMastermind
{
	Default
	{
		AttackSound "spider/attack";
	}
	
	States
	{
	Missile:
		SPID A 20 BRIGHT A_FaceTarget;
		SPID G 4 BRIGHT A_SPosProjectileAttackUseAtkSound;
		SPID H 4 BRIGHT A_SposProjectileAttackUseAtkSound;
		SPID H 1 BRIGHT A_SpidProjectileRefire;
		Goto Missile+1;
	}
	
	void A_SpidProjectileRefire()
	{
		// keep firing unless target got out of sight
		A_FaceTarget();
		if (Random[CPosRefire](0, 255) >= 10)
		{
			if (!target
				|| HitFriend()
				|| target.health <= 0
				|| !CheckSight(target, SF_SEEPASTBLOCKEVERYTHING|SF_SEEPASTSHOOTABLELINES))
			{
				SetState(SeeState);
			}
		}
	}
	
	void A_SPosProjectileAttackInternal()
	{
		if (target)
		{
			A_FaceTarget();
			double bangle = angle;
			double slope = AimLineAttack(bangle, MISSILERANGE);
		
			for (int i=0 ; i<3 ; i++)
			{
				double ang = bangle + Random2[SPosAttack]() * (22.5/256);
				int damage = Random[SPosAttack](1, 5) * 3;
				Actor missile = SpawnMissile (target, "BulletProjectile");
				if (missile)
				{
					missile.Angle = ang;
					missile.VelFromAngle();
				}
			}
		}
    }
	
	void A_SPosProjectileAttackUseAtkSound()
	{
		if (target)
		{
			A_StartSound(AttackSound, CHAN_WEAPON);
			A_SPosProjectileAttackInternal();
		}
	}
	
}
	
//===========================================================================
//
// Final Boss
// Make a special version of the spider mastermind with different "stages"
// Attack patterns get tweaked as stages go on
// Tag a normal spider mastermind to turn it into a boss
//===========================================================================

Class SchmupSpiderBoss : SpiderMastermindNonHitScan 
{
	// boss health gets changed depending on skill so will be different than
	// from SpawnHealth()
	int startHealth;
	int numberOfBulletsStage1;
	int numberOfBulletsStage2;
	int numberOfBulletsStage3;
	int numberOfBulletsStage4;
	int numberOfBulletsStage5;
	
	Default
	{	
		// Massively increase health
		health 20000;
		// Reduce pain chance by a significant amount
		PainChance 10;
		// Prevent other monsters attacking it
		+NOTARGET;
	}
	
	// Make the death more epic!
	States
	{	
	// Want to override 
	Missile:
		// Reduce telegraphing of attack
		SPID A 5 BRIGHT A_FaceTarget;
		SPID G 4 BRIGHT A_SPosProjectileAttackUseAtkSound;
		SPID H 4 BRIGHT A_SposProjectileAttackUseAtkSound;
		SPID H 1 BRIGHT A_SpidProjectileRefire;
		Goto Missile+1;
	Death:
		SPID J 80 A_Scream;
		SPID K 40 A_NoBlocking;
		SPID LMNOPQR 20 A_Quake(4,375,0,2048);
		SPID S 30;
		SPID S -1 A_BossDeath;
		Stop;
	}
	
	int getBossStage()
	{
	
		// stages determined by how much health left
		if (health < (startHealth / 4)) {
			return 4;
		}
		if (health < (startHealth / 2)) {
			return 3;
		}
		if (health < ((startHealth * 3) / 4)) {
			return 2;
		}
		
		// still on initial stage
		return 1;
	}
	
	// Overwrite generic non-hitscan spider mastermind with ones that change as
	// boss stage progresses
	void A_SpidProjectileRefire()
	{
		// keep firing unless target got out of sight
		A_FaceTarget();
		// Increase the chance that keeps on firing after going out of sight for
		// longer to prevent quick hiding them popping out again to stop firing
		int chanceOfStopFiring = 10;
		if (getBossStage() == 3) {
			chanceOfStopFiring = 20;
		}
		// do a greater than just in case adding an extra boss stage
		if (getBossStage() >= 4) {
			chanceOfStopFiring = 80;
		}
		
		if (Random[CPosRefire](0, 255) >= chanceOfStopFiring)
		{
			if (!target
				|| HitFriend()
				|| target.health <= 0
				|| !CheckSight(target, SF_SEEPASTBLOCKEVERYTHING|SF_SEEPASTSHOOTABLELINES))
			{
				SetState(SeeState);
			}
		}
	}
	
	void A_SPosProjectileAttackInternal()
	{
		if (target)
		{
			A_FaceTarget();
			double bangle = angle;
			double slope = AimLineAttack(bangle, MISSILERANGE);
		
			int bossStage = getBossStage();
			
			// Increase number of bullets per shot by  
			int numberOfBullets;
			switch(bossStage)
			{
				case 1:
				numberOfBullets = numberOfBulletsStage1;
				break;
				case 2:
				numberOfBullets = numberOfBulletsStage2;
				break;
				case 3:
				numberOfBullets = numberOfBulletsStage3;
				break;
				case 4:
				numberOfBullets = numberOfBulletsStage4;
				break;
				case 5:
				numberOfBullets = numberOfBulletsStage5;
				break;
			}
			
				
			for (int i=0 ; i < numberOfBullets ; i++)
			{
				// Change spread depending on boss stage
				float spread;
				switch(bossStage)
				{
					case 1:
					case 2:
					spread = 2;
					break;
					case 3:
					spread = 22.5;
					break;
					case 4:
					case 5:
					spread = 90.0;
					break;
				}
				double ang = bangle + Random2[SPosAttack]() * (spread/256);
				// console.printf("numberOfBullets %d ang %.2f", numberOfBullets, ang);
				int damage = Random[SPosAttack](1, 5) * 3;
				Actor missile = SpawnMissile (target, "BulletProjectile");
				if (missile)
				{
					missile.Angle = ang;
					missile.VelFromAngle();
				}
			}
		}
    }
	
	void A_SPosProjectileAttackUseAtkSound()
	{
		if (target)
		{
			A_StartSound(AttackSound, CHAN_WEAPON);
			A_SPosProjectileAttackInternal();
		}
	}
}


Class ReplaceHitScanners : EventHandler {

	// Replace all 4 hitscanners with their non-hitscan version
	// Have to first save the position and angle of the enemies and delete them
	// first otherwise the non-hitscan version will get added back onto the
	// stack of the hitscan version (because it inherited the non-hitscan
	// version).
	override void WorldLoaded(WorldEvent e) {

		// If spawn a non-hitscan enemy and then destroy a hit scan enemy it
		// be forever stuck in a loop because adding a new enemy to the stack
		// and start again.
		
		// Create a temporary place holder to store position and angle of hit
		// scanner
		Actor newActor;
		
		// Store all zombie men positions and angles
		Array <Actor> allZombieMen;
		
		// Device to loop through whichever class is wanted
		ThinkerIterator it;
		
		// Loop through all zombie men
		it = ThinkerIterator.Create("ZombieMan", Thinker.STAT_DEFAULT);
		ZombieMan hitScanZombieMan = ZombieMan(it.Next());
		while (hitScanZombieMan != null) {

			// console.printf("Found zombieman");
			// Temporarily store position and angle to add to array
			newActor = Actor.Spawn("Actor", hitScanZombieMan.pos);
			newActor.Angle = hitScanZombieMan.Angle;
			allZombieMen.push(newActor);
			
			hitScanZombieMan.Destroy();
			hitScanZombieMan = ZombieMan(it.Next());
		}
		
		// Put non-hit scan zombies back in their place
		for (int i = 0; i < allZombieMen.Size(); i++) {
			Actor zombieManNonHitscan = Actor.Spawn("ZombieManNonHitScan", allZombieMen[i].pos);
			zombieManNonHitscan.Angle = allZombieMen[i].Angle;
		}



		// Shotgun guys
		Array <Actor> allShotgunGuys;
		
		// Loop through all shotgun guys
		it = ThinkerIterator.Create("ShotgunGuy", Thinker.STAT_DEFAULT);
		ShotgunGuy hitScanShotgunGuy = ShotgunGuy(it.Next());
		while (hitScanShotgunGuy != null) {

			// console.printf("Found shotgun guy");
			// Temporarily store position and angle to add to array
			newActor = Actor.Spawn("Actor", hitScanShotgunGuy.pos);
			newActor.Angle = hitScanShotgunGuy.Angle;
			allShotgunGuys.push(newActor);
			
			hitScanShotgunGuy.Destroy();
			hitScanShotgunGuy = ShotgunGuy(it.Next());
		}
		
		// Put non-hit scan shotgun guys back in their place
		for (int i = 0; i < allShotgunGuys.Size(); i++) {
				Actor shotgunGuyNonHitscan = Actor.Spawn("ShotgunGuyNonHitScan", allShotgunGuys[i].pos);
				shotgunGuyNonHitscan.Angle = allShotgunGuys[i].Angle;
		}

		// Chaingun guys
		Array <Actor> allChaingunGuys;
		
		// Loop through all chaingun guys
		it = ThinkerIterator.Create("ChaingunGuy", Thinker.STAT_DEFAULT);
		ChaingunGuy hitScanChaingunGuy = ChaingunGuy(it.Next());
		while (hitScanChaingunGuy != null) {

			// console.printf("Found chaingun guy");
			//Temporarily store position and angle to add to array
			newActor = Actor.Spawn("Actor", hitScanChaingunGuy.pos);
			newActor.Angle = hitScanChaingunGuy.Angle;
			newActor.ChangeTid(hitScanChaingunGuy.TId);
			allChaingunGuys.push(newActor);
			
			hitScanChaingunGuy.Destroy();
			hitScanChaingunGuy = ChaingunGuy(it.Next());
		}
		
		// Put non-hit scan chaingun guys back in their place
		for (int i = 0; i < allChaingunGuys.Size(); i++) {
			Actor chaingunGuyNonHitscan = Actor.Spawn("ChaingunGuyNonHitScan", allChaingunGuys[i].pos);
			chaingunGuyNonHitscan.Angle = allChaingunGuys[i].Angle;
			chaingunGuyNonHitscan.ChangeTid(allChaingunGuys[i].TId);
		}
		
		////////////////////////////////////////////////////////////////////////
		// Special case for schmup level. 
		// If spider mastermind has a tag then upgrade to a boss instead
		////////////////////////////////////////////////////////////////////////
		// Spider Masterminds
		Array <Actor> allSpiderMasterMinds;
		
		// Loop through all spider masterminds
		it = ThinkerIterator.Create("SpiderMastermind", Thinker.STAT_DEFAULT);
		SpiderMasterMind hitScanSpiderMasterMind = SpiderMasterMind(it.Next());
		while (hitScanSpiderMasterMind != null) {

			// console.printf("Found SpiderMasterMind");
			// Temporarily store position and angle to add to array
			newActor = Actor.Spawn("Actor", hitScanSpiderMasterMind.pos);
			newActor.Angle = hitScanSpiderMasterMind.Angle;
			newActor.ChangeTid(hitScanSpiderMasterMind.TId);
			
			allSpiderMasterMinds.push(newActor);
			
			hitScanSpiderMasterMind.Destroy();
			hitScanSpiderMasterMind = SpiderMasterMind(it.Next());
		}
		
		// Put non-hit scan Spider Master Minds back in their place
		// Upgrade to schmup boss spider if got a tag
		for (int i = 0; i < allSpiderMasterMinds.Size(); i++) {
			
			Actor spiderMasterMindNonHitscan;
			if (allSpiderMasterMinds[i].TId) {
				spiderMasterMindNonHitscan = Actor.Spawn("SchmupSpiderBoss", allSpiderMasterMinds[i].pos);
			} else {
				spiderMasterMindNonHitscan = Actor.Spawn("spiderMasterMindNonHitScan", allSpiderMasterMinds[i].pos);
			}
			spiderMasterMindNonHitscan.Angle = allSpiderMasterMinds[i].Angle;
			spiderMasterMindNonHitscan.ChangeTid(allSpiderMasterMinds[i].TId);
			
		}
		

		
	}
	
}


// Creating a monster spawner to keep enemies filling up arenas.
// Uses the "SmallBloodPool" as a destination because they will be used out of 
// sight
class SchmupMonsterSpawner : Actor
{
	int ticsBetweenSpawn;
	int tagID;
	Array <String> monstersToSpawn;	
	int ticsSinceLastSpawn;
	Array <Actor> spawnPoint;	
	int lastSpawnPoint;
	
	// Simply to populate monster array. Doesn't actually start spawning
	// To increase chance of one enemy spawing just adding them in again
	SchmupMonsterSpawner Init(){
	
		// console.printf("SchmupMonsterSpawner initialised");
		
		monstersToSpawn.push("ZombieManNonHitScan");
		monstersToSpawn.push("DoomImp");
		monstersToSpawn.push("ShotgunGuyNonHitScan");
		monstersToSpawn.push("ChaingunGuyNonHitScan");
		monstersToSpawn.push("LostSoul");
		monstersToSpawn.push("Cacodemon");
		monstersToSpawn.push("Revenant");
		monstersToSpawn.push("Fatso");
		monstersToSpawn.push("Arachnotron");
		
		// Add different enemies and rate depending on game skill
		int skillSetting = G_SkillPropertyInt(SKILLP_ACSReturn);
		switch(skillSetting)
		{
		// Easy
		case 0:
		case 1:
			// console.printf("Easy skill");
			ticsBetweenSpawn = 100;
			monstersToSpawn.push("ZombieManNonHitScan");
			monstersToSpawn.push("ZombieManNonHitScan");
			monstersToSpawn.push("DoomImp");
			monstersToSpawn.push("DoomImp");
			monstersToSpawn.push("ShotgunGuyNonHitScan");
			monstersToSpawn.push("ZombieManNonHitScan");
			monstersToSpawn.push("ZombieManNonHitScan");
			monstersToSpawn.push("DoomImp");
			monstersToSpawn.push("DoomImp");
			monstersToSpawn.push("ShotgunGuyNonHitScan");
			break;
		// Medium
		case 2:
			// console.printf("Medium skill");
			ticsBetweenSpawn = 75;
			monstersToSpawn.push("ZombieManNonHitScan");
			monstersToSpawn.push("DoomImp");
			monstersToSpawn.push("ChaingunGuyNonHitScan");
			monstersToSpawn.push("Fatso");
			break;
		// Hard or harder
		default:
			// console.printf("Hard skill");
			ticsBetweenSpawn = 50;
			monstersToSpawn.push("ChaingunGuyNonHitScan");
			monstersToSpawn.push("Fatso");
			monstersToSpawn.push("ChaingunGuyNonHitScan");
			monstersToSpawn.push("Revenant");
			monstersToSpawn.push("Cacodemon");
			monstersToSpawn.push("Cacodemon");
			break;
		}
		
		return self;
	}
	
	static SchmupMonsterSpawner GetInstance() {
	
		// If already exists then grab it
		ThinkerIterator it = ThinkerIterator.Create("SchmupMonsterSpawner", Thinker.STAT_DEFAULT);
		SchmupMonsterSpawner t = SchmupMonsterSpawner(it.Next());
		if (t) {
			// console.printf("Existing SchmupMonsterSpawner found");
			return SchmupMonsterSpawner(t);
		}
		
		// If doesn't exist then create it and initiallise it
		//return new("SchmupMonsterSpawner").Init();
		t = SchmupMonsterSpawner(spawn("SchmupMonsterSpawner", (0,0,0), NO_REPLACE));
		t.Init();
		return t;
		
	}
	
	// Deactivate spawner by setting TagID to zero
	override void Tick()
	{
		// console.printf("SchmupMonsterSpawner instance exists");
		super.Tick();
		// If not set then don't do anything
		if (tagID) {
			// console.printf("tagID: %d", tagID);
			// See if it's time to spawn another monster
			if (ticsSinceLastSpawn >= ticsBetweenSpawn) {

				// console.printf("Spawn monster");
				
				// Roll a random spawn point that's not the last one 
				// Note arrays are zero based
				int randomSpawnPoint = random(0, spawnPoint.size() - 1);
				while (randomSpawnPoint == lastSpawnPoint) {
					// console.printf("lastSpawnPoint %d randomSpawnPoint %d", lastSpawnPoint, randomSpawnPoint);
					// roll again
					randomSpawnPoint = random(0, spawnPoint.size() - 1);
				}
				
				// Save the new spawn point to reduce chance of enemy spawning
				// on top of previous one
				lastSpawnPoint = randomSpawnPoint;
				
				// pick a random enemy to spawn
				int randomEnemy = random(0, monstersToSpawn.size() - 1);
				Actor newEnemy = Actor.Spawn(monstersToSpawn[randomEnemy], spawnPoint[randomSpawnPoint].pos);
				
				// console.printf("Spawn point %d enemy %s", randomSpawnPoint, monstersToSpawn[randomEnemy]);
				
				// CURRENTLY NOT WORKING. Can be fixed later
				// Alert the enemy
				// newEnemy.SetState(FindState("See"));
			
				ticsSinceLastSpawn = 0;
			} else {
				ticsSinceLastSpawn++;
			}
		}
	}
	
	// Update ID of spawn points or deactivates spawner if 0
	void UpdateThingID(int tagIdToChangeTo)
	{
		// don't redo spawn point array every time this is called
		if (tagIdToChangeTo != tagID) {
		
			// Change tag tags and redo spawn point array
			tagID = tagIdToChangeTo;
			// console.printf("SchmupMonsterSpawner tagID set to %d", tagID);
			
			// Uses small blood pool just as a location marker
			ThinkerIterator it = ThinkerIterator.Create("SmallBloodPool", Thinker.STAT_DEFAULT);
			Actor singleSpawnPoint = Actor(it.Next());
			while (singleSpawnPoint != null) {
				// Check it has a tag and that tag is what we want
				if (singleSpawnPoint.TId && singleSpawnPoint.TId == tagID) {
					// Add to array of spawn point locations
					// console.printf("Spawn point located: %.2f %.2f %.2f", singleSpawnPoint.pos.x, singleSpawnPoint.pos.y, singleSpawnPoint.pos.z);
					spawnPoint.push(singleSpawnPoint);
				}
				
				singleSpawnPoint = Actor(it.Next());
			}
			// console.printf("Spawn points found: %d", spawnPoint.size());
		}
	}
}


// Handling map events like all particular monsters in area are killed
class SchmupMapEventsHandler : EventHandler
{

	// When an arena's bosses are cleared do explosions on progression
	int mapExplosionsLeft;
	int ticsBetweenExplosions;
	int ticsSinceLastExplosion;
	Array <Actor> explosionPoints;	
	int lastExplosionPoint;
	
	// Run exploding barrier if needed
	override void WorldTick()
	{
		super.WorldTick();
		
		// If should be doing explosions
		if (mapExplosionsLeft) {
		
			// can't use defaults on a non-actor class. Set here
			ticsBetweenExplosions = 6; // just over 4 a second
				
			// Should do another explosion?
			if (ticsSinceLastExplosion >= ticsBetweenExplosions) {
	
				// Roll a random explosion point that's not the last one 
				// Note arrays are zero based
				int randomExplosionPoint = random(0, explosionPoints.size() - 1);
				// Technically first time it won't do the first (0) spawn point
				// but not really a problem
				while (randomExplosionPoint == lastExplosionPoint) {
					// console.printf("lastExplosionPoint %d randomExplosionPoint %d", lastExplosionPoint, randomExplosionPoint);
					// roll again
					randomExplosionPoint = random(0, explosionPoints.size() - 1);
				}
				
				// Found explosion point that's not the same as last one. Save
				lastExplosionPoint = randomExplosionPoint;
				
				// Spawn an explosion at point
				Vector3 position;
				// Need to place above ground so will explode
				position.x = explosionPoints[randomExplosionPoint].pos.x;
				position.y = explosionPoints[randomExplosionPoint].pos.y;
				// Raise off ground so it actually explodes
				position.z = explosionPoints[randomExplosionPoint].pos.z + 8.0;
				Actor.Spawn("protectiveExplosion", position);
				
				// Use up an explosion
				mapExplosionsLeft--;
				
				// Start round again
				ticsSinceLastExplosion = 0;
			} 
			else {
				ticsSinceLastExplosion++;
			}
			
		}
		// Else just do nothing
		
	}
	
	// Things needed for the map. E.G. the monster spawner
	override void WorldLoaded(WorldEvent e) {
	
		//SchmupMonsterSpawner spawner = SchmupMonsterSpawner.GetInstance();
		
	}
	
	// TO DO. Find out how to run something like this just from a linedef action
	// Start the spawning in for the arena
	// Uses "SmallBloodPool" as a place marker for the spawn points
	override void WorldThingDamaged(WorldEvent e)
	{
		
		// If monster didn't have a tag ID then it wasn't a "boss". Just ignore
		if (e.Thing.TId) {
		
			// Thing has an ID
			// Get the spawner and give it the tag
			SchmupMonsterSpawner spawner = SchmupMonsterSpawner.GetInstance();
			spawner.UpdateThingID(e.Thing.TId);
			
		}
	}
	
	override void WorldThingDied(WorldEvent e)
	{
		// Most arenas are going to have 1 heavy/boss type enemy to defeat and 
		// then an opening lowers letting you get to the next arena
		
		// Using tag IDs on things. Only look for others of same class so not
		// running the iterator for everytime a monster dies
		if (e.Thing.TId) {
			// Thing has an ID
			int monsterThingID;
			monsterThingID = e.Thing.TId;
			//console.printf("Thing ID: %d died", monsterThingID);
			
			string monsterClassName;
			monsterClassName = e.Thing.getClassName();
			//console.printf("Thing class name: %s died", monsterClassName);

			// Loop through all monsters with same ID to find if others are 
			// still alive
			int numberWithSameID;
			numberWithSameID = 0;
			
			ThinkerIterator otherMonsterIterator;
			otherMonsterIterator = ThinkerIterator.Create(monsterClassName, Thinker.STAT_DEFAULT);
			Actor otherMonster = Actor(otherMonsterIterator.Next());
			while (otherMonster != null) {

				//console.printf("Found Thing with same class name with health: %d", otherMonster.health);
				if (otherMonster.TId == monsterThingID && otherMonster.health > 0) {
					//console.printf("Found Thing with ID: %d still alive", otherMonster.TId);
					numberWithSameID++;
					//console.printf("Number of things with same ID: %d still alive", numberWithSameID);
				}
				otherMonster = Actor(otherMonsterIterator.Next());
				
			}
			
			//console.printf("Number of %s with ID: %d is %d still alive", monsterClassName, monsterThingID, numberWithSameID);
			// All monsters of same ID now dead
			// lower the corresponding sector and stop the spawner
			if (numberWithSameID <= 0) {
			
				// Open up next section
				Floor_LowerToLowest(monsterThingID, 16);
				
				// Generate explosions at the explosion points
				// Make sure any previous map explosion points are cleared out
				explosionPoints.Clear();
				// Arm ready for explosions
				mapExplosionsLeft = 50;
				
				ThinkerIterator explosionPointsIterator;
				explosionPointsIterator = ThinkerIterator.Create("TeleportDest", Thinker.STAT_DEFAULT);
				Actor explosionPoint = Actor(explosionPointsIterator.Next());
				while (explosionPoint != null) {
					// console.printf("Found Teleport point");
					// Pick only the ones related to particular arena fight
					if (explosionPoint.TId == monsterThingID) {
					
						//console.printf("Found explosion point at %d %d %d", explosionPoint.pos.x, explosionPoint.pos.y, explosionPoint.pos.z);
						// Add to list of explosion points
						explosionPoints.push(explosionPoint);
						
					}
					explosionPoint = Actor(explosionPointsIterator.Next());
					
				}
				
				// Make players temporarily invunerable
				// console.printf("Make players invincible");
				foreach (player : players) {
					
					if (player.mo) {
						// console.printf("found PlayerPawn");
						player.mo.GiveInventory("TemporaryInvul", 1);
						// Increase time is invunerable
						TemporaryInvul tempInvul = TemporaryInvul(player.mo.FindInventory("TemporaryInvul"));
						tempInvul.timeToLive = 350;
					}
				}
				
				// Stop the monster spawner just by getting rid of it
				SchmupMonsterSpawner spawner = SchmupMonsterSpawner.GetInstance();
				spawner.destroy();
			}
			
			
		}
	}
}

////////////////////////////////////////////////////////////////////////////////
// Dedicated just for final boss
// Just hard coding Tag IDs for RAMP 2024
////////////////////////////////////////////////////////////////////////////////
class SchmupBossEventsHandler : EventHandler
{	
	// What level we currently at
	int bossStage;
	
	// for dramatic explosions when the boss dies
	vector3 bossLocation;
	int bossExplosionsLeft;
	int ticsBetweenExplosions;
	int ticsSinceLastExplosion;
	
	// Run exploding barrier if needed
	override void WorldTick()
	{
		super.WorldTick();
		
		// If should be doing explosions
		if (bossExplosionsLeft) {
		
			// can't use defaults on a non-actor class. Set here
			ticsBetweenExplosions = 6; // just over 4 a second
				
			// Should do another explosion?
			if (ticsSinceLastExplosion >= ticsBetweenExplosions) {
	
				// Spawn an explosion at point
				Vector3 position;
				// Place it randomly near the boss
				position.x = bossLocation.x + frandom(-64.0, 64.0);
				position.y = bossLocation.y + frandom(-64.0, 64.0);
				// Raise off ground so it actually explodes
				position.z = bossLocation.z + 8.0;
				Actor.Spawn("protectiveExplosion", position);
				
				// Use up an explosion
				bossExplosionsLeft--;
				
				// Start round again
				ticsSinceLastExplosion = 0;
			} 
			else {
				ticsSinceLastExplosion++;
			}
			
		}
		// Else just do nothing
		
	}
	
	override void WorldLoaded(WorldEvent e) {
	
		// Change the health and bullets of the final boss depending on skill
		int skillSetting = G_SkillPropertyInt(SKILLP_ACSReturn);

		ThinkerIterator it = ThinkerIterator.Create("SchmupSpiderBoss", Thinker.STAT_DEFAULT);
		SchmupSpiderBoss spiderBoss = SchmupSpiderBoss(it.Next());
		while (spiderBoss != null) {
		
			switch(skillSetting)
			{
			// Easy
			case 0:
			case 1:
				// console.printf("Easy skill");
				spiderBoss.startHealth = 10000;
				spiderBoss.health = 10000;
				spiderBoss.numberOfBulletsStage1 = 1;
				spiderBoss.numberOfBulletsStage2 = 1;
				spiderBoss.numberOfBulletsStage3 = 2;
				spiderBoss.numberOfBulletsStage4 = 2;
				spiderBoss.numberOfBulletsStage5 = 2;
				break;
			// Medium
			case 2:
				// console.printf("Medium skill");
				spiderBoss.startHealth = 20000;
				spiderBoss.health = 20000;
				spiderBoss.numberOfBulletsStage1 = 2;
				spiderBoss.numberOfBulletsStage2 = 2;
				spiderBoss.numberOfBulletsStage3 = 2;
				spiderBoss.numberOfBulletsStage4 = 3;
				spiderBoss.numberOfBulletsStage5 = 3;
				break;
			// Hard or harder
			default:
				// console.printf("Hard skill");
				spiderBoss.startHealth = 30000;
				spiderBoss.health = 30000;
				spiderBoss.numberOfBulletsStage1 = 2;
				spiderBoss.numberOfBulletsStage2 = 2;
				spiderBoss.numberOfBulletsStage3 = 3;
				spiderBoss.numberOfBulletsStage4 = 4;
				spiderBoss.numberOfBulletsStage5 = 4;
				break;
			}
			// Incase we actually decided to add more
			spiderBoss = SchmupSpiderBoss(it.Next());
		}
	}
	
	// Check if need to start next stage of boss fight
	override void WorldThingDamaged(WorldEvent e)
	{
		// Assume if SchmupBossSpider then has a TId anyway
		if (e.Thing.getClassName() == "SchmupSpiderBoss") {
			// console.printf("Boss health = %d", e.Thing.Health);
			
			// Check if need to proceed to next boss stage.
			SchmupSpiderBoss finalBoss = SchmupSpiderBoss(e.Thing);
			int newBossStage = finalBoss.getBossStage();
			// console.printf("Boss Stage should be %d", newBossStage);
			if (newBossStage != bossStage) {
				
				// console.printf("Change boss stage to %d", newBossStage);
				// Trigger next boss stage
				if (newBossStage == 2) {
					Floor_LowerToLowest(7501, 8);
				}
				if (newBossStage == 3) {
					Floor_LowerToLowest(7601, 16);
				}
				if (newBossStage == 4) {
					Floor_LowerToLowest(7701, 16);
					// Also start the extra spawner
					// Get the spawner and give it the tag
					// !!! Messy work around to add a second spawner
					SchmupMonsterSpawner spawner;
					spawner = SchmupMonsterSpawner(spawner.spawn("SchmupMonsterSpawner", (0,0,0), NO_REPLACE));
					spawner.Init();
					spawner.UpdateThingID(7701);
				}

				bossStage = newBossStage;
				
			}
			
			
		}
	}
	
	
	override void WorldThingDied(WorldEvent e)
	{
		if (e.Thing.getClassName() == "SchmupSpiderBoss") {
			// console.printf("Spider Boss Died");
			bossLocation = e.Thing.pos;
			bossExplosionsLeft = 50; 
			// The normal "all tagged monstered died" will destroy one spawner
			// Destroy the other spawner. Doesn't matter which one's which
			SchmupMonsterSpawner spawner = SchmupMonsterSpawner.GetInstance();
			spawner.destroy();
			
			// Shake the ground
			// e.Thing.A_Quake(4,12,0,400);
		}
	}
}


////////////////////////////////////////////////////////////////////////////////
// filename: ZSCRIPT/WEAPONS
////////////////////////////////////////////////////////////////////////////////

/*
* Keep asset names the same for now. Might be changing assets later
*/

// Basically make enemy and player bullets stand out
class PlayerBulletProjectile : BulletProjectile
{
	Default
	{
		Radius 6;
		Height 16;
		Speed 20;
		FastSpeed 30;
		Damage 5;
		DeathSound "baron/shotx";
		Decal "BulletChip";
	}
	States
	{
	Spawn:
		SUIB AB 4 BRIGHT;
		Loop;
	Death:
		SUIB CDE 6 BRIGHT;
		Stop;
	}
}

// Make Barrage rifle bullets faster
class BarrageRifleProjectile : PlayerBulletProjectile
{
	Default
	{
		Speed 40;
	}
}

// Passes through enemies
class LancerProjectile : Actor
{
	Default
	{
		Radius 6;
		Height 16;
		Speed 60;
		Damage 12;
		Projectile;
		+RANDOMIZE;
		+ZDOOMTRANS;
		+RIPPER;
		RenderStyle "Add";
		Alpha 1;
		DeathSound "baron/shotx";
		Decal "BaronScorch";
	}
	States
	{
	Spawn:
		SUIL A 1 BRIGHT;
		Loop;
	Death:
		SUIB CDE 6 BRIGHT;
		Stop;
	}
}

class MicroMissile : Actor
{
	Default
	{
		Radius 11;
		Height 8;
		Speed 20;
		Damage 5;
		Projectile;
		+RANDOMIZE
		+DEHEXPLOSION
		+ROCKETTRAIL
		+ZDOOMTRANS
		SeeSound "weapons/rocklf";
		DeathSound "weapons/rocklx";
		Obituary "$OB_MPROCKET";
		Decal "BaronScorch";
	}
	States
	{
	Spawn:
		SUIM AB 2 Bright;
		Loop;
	Death:
		// Change explosion from box to circular
		MISL B 8 Bright A_Explode(32, 128, XF_CIRCULAR, false, 0, 0);
		MISL C 6 Bright;
		MISL D 4 Bright;
		Stop;
	}
}



/*
* Schmup it up has 1 default gun plus 3 extra guns that can be broken. Make a 
* parent class for these 3
*/

class SchmupWeapon : Weapon
{
	Default
	{
		health 100;
	}

	// TO DO! Screen flash! See https://zdoom.org/wiki/Structs:PlayerInfo#Cyberdemon_alarm
	// Guns act like armour until they are "destroyed"
	// Note! The weapons are stacked on top of each other!
	// The last added weapon calls this function first. If no damage is absorbed
	// then it passes that damage onto the next weapon.
	override void AbsorbDamage (int damage, Name damageType, out int newdamage, Actor inflictor, Actor source, int flags)
	{
		//console.printf("current weapon %s", Owner.Player.ReadyWeapon.GetClassName());
		
		// Take health from the weapon player currently is using only!
		// If "Self" is not the currently selected weapon then "newdamage" is 
		// full and is then passed onto the next schmup weapon in the list to 
		// see if it should absorb damage.
		if (self.getClassName() == Owner.Player.ReadyWeapon.GetClassName()) {
		
			if(health > 0) {
			
				health = health - damage;
				//console.printf("%s health %d", self.getClassName(), self.health);
				// Weapon takes all the damage
				newdamage = 0;
				
				// Flash the screen a different colour to show you're taking 
				// damage. Don't do if temporarily invunerable
				Owner.A_SetBlend("99 99 99", 0.5, damage * 2);
				
				// Make a sound if not temporarily invunerable 
				if ( damage > 0 ) {
					Owner.A_StartSound("sounds/SUIHITWP", CHAN_WEAPON);
				}
				
			} else {
				// The weapon getting destroyed nullifies all the damage to 
				// prevent it passing onto next weapon/player
				newdamage = 0;
				//console.printf("%s destroyed", self.getClassName());
				
				// Give the player some invunerability to prevent them losing 
				// loads of health on the next weapon straight away
				Owner.Player.mo.GiveInventory("TemporaryInvul", 1);
				/*
				// Create explosions to clear nearby enemies
				Actor boom = Actor.Spawn("Rocket", Owner.pos, NO_REPLACE);
				if (boom) {
					boom.SetStateLabel ("Brainexplode");
				}*/
				
				Owner.Player.mo.GiveInventory("TemporaryExplosions", 1);
				// Remove the weapon
				DepleteOrDestroy();
			}
		}
	}
	
}

// Assets from "butcher" gun from https://www.realm667.com
class BarrageRifle : Weapon
{
	Default
	{
		Inventory.PickupMessage "Picked up the Barrage Rifle!";
		Weapon.SelectionOrder 400;
		Weapon.SlotNumber 1;
	}
	
	States
	{
		Spawn: 
			BUTP A -1;
			Stop;
		Ready: 
			BUTC A 1 A_WeaponReady;
			Loop;
		Select:
			BUTC A 1 A_Raise(12);
			Loop;
		Deselect:
			BUTC A 1 A_Lower(12);
			Loop;
		Fire:
			BUTC A 0 A_Jump(128,4);
			BUTC B 1;
			BUTC A 0 A_StartSound("sounds/SUIBARRF");
			BUTC C 1 SpawnPlayerMissile("BarrageRifleProjectile");
			goto Ready;
			BUTC B 1;
			BUTC A 0 A_StartSound("sounds/SUIBARRF");
			BUTC D 1 SpawnPlayerMissile("BarrageRifleProjectile");
			goto Ready;
		Flash:
			BUTC A 6 bright A_Light1;
			Goto LightDone;
	}
	
	// Barrage rifle is different from other schmup weapons. If other weapons 
	// still exist then they will absorb damage equally. If they don't then the
	// player will start absorbing damage
	override void AbsorbDamage (int damage, Name damageType, out int newdamage, Actor inflictor, Actor source, int flags)
	{
		// tempInvul inventory item doesn't seem to take a few ticks before it 
		// starts working with the barrage rifle equipped !? Make sure it skips 
		// if has one
		TemporaryInvul currentTemporaryInvul;
		currentTemporaryInvul = TemporaryInvul(owner.FindInventory("TemporaryInvul"));
		if (currentTemporaryInvul) {
			// Skip rest of method
			newdamage = 0;
			return;
		}
		
		// No temporary invul so carry on
		
		//console.printf("---- BarrageRifle AbsorbDamage");
		// Check if player has this weapon equipped
		if (self.getClassName() == Owner.Player.ReadyWeapon.GetClassName()) {
		
			//console.printf("BarrageRifle Currently equipped");
			
			// Find if any other schmup weapons still exist and how many
			int hasCrowdControlGun;
			int hasLancerGun;
			int hasMicroMissileGun;
			
			hasCrowdControlGun = owner.CheckInventory("CrowdControlGun", 1);
			hasLancerGun = owner.CheckInventory("LancerGun", 1);
			hasMicroMissileGun = owner.CheckInventory("MicroMissileGun", 1);
			//console.printf("Weapons %d %d %d", hasCrowdControlGun, hasLancerGun, hasMicroMissileGun);
			
			int damageDivider;
			damageDivider = hasCrowdControlGun + hasLancerGun + hasMicroMissileGun;
			//console.printf("damageDivider %d", damageDivider);
					
			if (damageDivider) {
			
				// Still at least one schmup weapon left. 
				// Do usual schmup weapon flash
				Owner.A_SetBlend("99 99 99", 0.5, damage * 2);
				
				// Only want to destroy a maximum of one schmup weapon. Else do
				// no destroy more
				bool oneWeaponDestroyed;
				
				// Damage each weapon will receive
				int dividedDamage;
				
				// Get how much damage each one will receive.
				// Note because each of the other weapons would receive a max of
				// 100 then need to make total tha's spread accross all 3
				// the same.
				if (damage >= 100) {
					damage = 99;
				}
				
				dividedDamage = damage / damageDivider;
				//console.printf("dividedDamage %d", dividedDamage);
				
				// Note only want to destroy maximum one weapon at a time. Maybe
				// change which weapons are prioritised later
				if (hasLancerGun) {
				
					//console.printf("hasLancerGun");
					LancerGun playerLancerGun;
					playerLancerGun = LancerGun(owner.FindInventory("LancerGun"));
					if (playerLancerGun) {
						//console.printf("LancerGun has %d health left", playerLancerGun.health);
						if (playerLancerGun.health <= 0) {
							// gun will be destroyed.
							oneWeaponDestroyed = true;
							
							// Prevent other weapons taking damage/destroyed too quickly
							owner.GiveInventory("TemporaryInvul", 1);
							owner.GiveInventory("TemporaryExplosions", 1);
							
							playerLancerGun.DepleteOrDestroy();
							//console.printf("LancerGun Destroyed");
						}
						else {
							// Just damage gun. No need to use fancy functions
							playerLancerGun.health = playerLancerGun.health - dividedDamage;
						}
					}
				}
				
				//console.printf("oneWeaponDestroyed %d", oneWeaponDestroyed);

				if (hasMicroMissileGun) {
				
					//console.printf("hasMicroMissileGun");
					MicroMissileGun playerMicroMissileGun;
					playerMicroMissileGun = MicroMissileGun(owner.FindInventory("MicroMissileGun"));
					if (playerMicroMissileGun) {
						//console.printf("MicroMissileGun has %d health left", playerMicroMissileGun.health);
						if (playerMicroMissileGun.health <= 0 && !oneWeaponDestroyed) {
							// gun will be destroyed.
							oneWeaponDestroyed = true;
							
							// Prevent other weapons taking damage/destroyed too quickly
							owner.GiveInventory("TemporaryInvul", 1);
							owner.GiveInventory("TemporaryExplosions", 1);
							
							playerMicroMissileGun.DepleteOrDestroy();
							//console.printf("MicroMissileGun Destroyed");
						}
						else {
							// Just damage gun. No need to use fancy functions
							playerMicroMissileGun.health = playerMicroMissileGun.health - dividedDamage;
						}
					}
				}
				
				//console.printf("oneWeaponDestroyed %d", oneWeaponDestroyed);
				
				if (hasCrowdControlGun) {
				
					//console.printf("hasCrowdControlGun");
					CrowdControlGun playerCrowdControlGun;
					playerCrowdControlGun = CrowdControlGun(owner.FindInventory("CrowdControlGun"));
					if (playerCrowdControlGun) {
						//console.printf("CrowdControlGun has %d health left", playerCrowdControlGun.health);
						if (playerCrowdControlGun.health <= 0 && !oneWeaponDestroyed) {
							// gun will be destroyed.
							oneWeaponDestroyed = true;
							
							// Prevent other weapons taking damage/destroyed too quickly
							owner.GiveInventory("TemporaryInvul", 1);
							owner.GiveInventory("TemporaryExplosions", 1);
							
							playerCrowdControlGun.DepleteOrDestroy();
							//console.printf("CrowdControlGun Destroyed");
						}
						else {
							// Just damage gun. No need to use fancy functions
							playerCrowdControlGun.health = playerCrowdControlGun.health - dividedDamage;
						}
					}
				}
				
				//console.printf("oneWeaponDestroyed %d", oneWeaponDestroyed);
				// if a weapon has been destroyed deploy protective explosions
				
				// Weapons absorb all damage
				newdamage = 0;
			}
			else {
				// No other schmup weapons left. Player takes full damage
				newdamage = damage;
			}
				
		}
	}
}

// Assets from "repeater" gun from https://www.realm667.com
class CrowdControlGun : SchmupWeapon
{
	Default
	{
		Inventory.PickupMessage "Picked up the Crowd Control gun!";
		Weapon.SelectionOrder 100;
		Weapon.SlotNumber 2;
	}
	States
	{
		Spawn: 
			REPG I -1;
			Stop;
		Ready: 
			REPG A 1 A_WeaponReady;
			Loop;
		Select:
			REPG A 1 A_Raise(12);
			Loop;
		Deselect:
			REPG A 1 A_Lower(12);
			Loop;
		Fire:
			REPG A 0 A_StartSound("sounds/SUICCGUN");
			REPG E 3 {
				// Even out the spread but leave a little randomness in
				// Right
				A_FireProjectile("PlayerBulletProjectile", random(-31,-26),false,0,0,0,random(-1,1));
				//A_FireProjectile("PlayerBulletProjectile", random(-28,-26),false,0,0,0,random(-1,1));
				A_FireProjectile("PlayerBulletProjectile", random(-25,-23),false,0,0,0,random(-1,1));
				A_FireProjectile("PlayerBulletProjectile", random(-22,-20),false,0,0,0,random(-1,1));
				A_FireProjectile("PlayerBulletProjectile", random(-19,-17),false,0,0,0,random(-1,1));
				A_FireProjectile("PlayerBulletProjectile", random(-16,-14),false,0,0,0,random(-1,1));
				A_FireProjectile("PlayerBulletProjectile", random(-13,-11),false,0,0,0,random(-1,1));
				A_FireProjectile("PlayerBulletProjectile", random(-10,-8),false,0,0,0,random(-1,1));
				A_FireProjectile("PlayerBulletProjectile", random(-7,-5),false,0,0,0,random(-1,1));
				A_FireProjectile("PlayerBulletProjectile", random(-4,-2),false,0,0,0,random(-1,1));
				
				// Centre
				A_FireProjectile("PlayerBulletProjectile", random(-1,1),false,0,0,0,random(-1,1));
				
				// Left
				A_FireProjectile("PlayerBulletProjectile", random(31,26),false,0,0,0,random(-1,1));
				//A_FireProjectile("PlayerBulletProjectile", random(28,26),false,0,0,0,random(-1,1));
				A_FireProjectile("PlayerBulletProjectile", random(25,23),false,0,0,0,random(-1,1));
				A_FireProjectile("PlayerBulletProjectile", random(22,20),false,0,0,0,random(-1,1));
				A_FireProjectile("PlayerBulletProjectile", random(19,17),false,0,0,0,random(-1,1));
				A_FireProjectile("PlayerBulletProjectile", random(16,14),false,0,0,0,random(-1,1));
				A_FireProjectile("PlayerBulletProjectile", random(13,11),false,0,0,0,random(-1,1));
				A_FireProjectile("PlayerBulletProjectile", random(10,8),false,0,0,0,random(-1,1));
				A_FireProjectile("PlayerBulletProjectile", random(7,5),false,0,0,0,random(-1,1));
				A_FireProjectile("PlayerBulletProjectile", random(4,2),false,0,0,0,random(-1,1));
			}
			REPG F 3;
			REPG C 3;
			REPG D 3;
			goto Ready;
		Flash:
			REPG A 6 bright A_Light1;
			Goto LightDone;
	}

}


// Assets from "channeler" gun from https://www.realm667.com
class LancerGun : SchmupWeapon
{
	Default
	{
		Inventory.PickupMessage "Picked up the Crowd Control gun!";
		Weapon.SelectionOrder 300;
		Weapon.SlotNumber 3;
	}
	States
	{
		Spawn: 
			SUI3 X -1;
			Stop;
		Ready: 
			SUI3 A 1 A_WeaponReady;
			Loop;
		Select:
			SUI3 A 1 A_Raise(12);
			Loop;
		Deselect:
			SUI3 A 1 A_Lower(12);
			Loop;
		Fire:
			SUI3 A 0;
			SUI3 B 1;
			SUI3 C 2 A_StartSound("sounds/SUILANCE");
			SUI3 D 4 A_FireProjectile("LancerProjectile", 0,false,0,0,0,0);
			SUI3 B 2;
			goto Ready;
		Flash:
			SUI3 A 6 bright A_Light1;
			Goto LightDone;
	}

}

// Assets from "Prox Launcher" gun from https://www.realm667.com
class MicroMissileGun : SchmupWeapon
{
	Default
	{
		Inventory.PickupMessage "Picked up the Micro Missile gun!";
		Weapon.SelectionOrder 200;
		Weapon.SlotNumber 4;
	}
	States
	{
		Spawn: 
			PRXP A -1;
			Stop;
		Ready: 
			PRXL A 1 A_WeaponReady;
			Loop;
		Select:
			PRXL A 1 A_Raise(12);
			Loop;
		Deselect:
			PRXL A 1 A_Lower(12);
			Loop;
		Fire:
			PRXL A 0;
			PRXL B 1;
			PRXL B 0 A_StartSound("sounds/butcherfire");
			// Make it fire a burst of missiles rapidly one after another
			PRXL CBCBCBCB 1 A_FireProjectile("MicroMissile",  random(-6.0,6.0),false,0,0,0,random(-1.0,1.0));
			PRXL D 6;
			PRXL E 6;
			goto Ready;
		Flash:
			PRXL A 6 bright A_Light1;
			Goto LightDone;
	}

}

////////////////////////////////////////////////////////////////////////////////
// filename: ZSCRIPT/PLAYER
////////////////////////////////////////////////////////////////////////////////

Class SchmupLife : Inventory {

	int timeToResurrect;
	PlayerPawn playerToResurrect;
	
	Default {
		Inventory.PickupMessage "Picked up a life!";
		Inventory.MaxAmount 99;
		Inventory.Amount 1;
		+Bright;
	}
	
	states {
		Spawn:
			TFOG B 5;
			TFOG C 5;
			Loop;
	}
	
	override void Tick()
	{
		super.Tick();
		
		if (timeToResurrect > 0) {
			timeToResurrect--;
			
			// If going to resurrect
			if (timeToResurrect == 0) {
				//console.printf("Player should be resurrected");
				playerToResurrect = PlayerPawn(owner);
				playerToResurrect.player.Resurrect();
				playerToResurrect.TakeInventory("SchmupLife",1);
				
				// Barrage Rifle wasn't taken away
				playerToResurrect.GiveInventory("CrowdControlGun", 1);
				playerToResurrect.GiveInventory("LancerGun", 1);
				playerToResurrect.GiveInventory("MicroMissileGun", 1);
				
				// Protect the player just after resurrection
				playerToResurrect.GiveInventory("TemporaryInvul", 1);
			}
		}
	}
}

// Use to protect the player after weapon lose/resurrection
Class TemporaryInvul : Inventory {

	int timeToLive;
	property timeToLive: timeToLive;
	
	PlayerPawn playerToProtect;
	
	default {
		TemporaryInvul.timeToLive 105;
	}
	
	// While here, protect the player
	override void AbsorbDamage (int damage, Name damageType, out int newdamage, Actor inflictor, Actor source, int flags)
	{
		if (timeToLive > 0) {
			newdamage = 0;
		} 
	}
	
	// Decrease 
	override void Tick()
	{
		super.Tick();
		//console.printf("invul timeToLive %d", timeToLive);
		if (timeToLive > 0) {
			timeToLive--;
		} else {
			playerToProtect = PlayerPawn(owner);
			playerToProtect.TakeInventory("TemporaryInvul",1);
		}
		
	}
}

// Make a special rocket that drops and explodes straight away
Class protectiveExplosion : Rocket {

	default {
		gravity 1.0;
		-NOGRAVITY;
	}
	
	// Hide the original "rocket". Set to first explosion frame
	States
	{
	Spawn:
		MISL B 1 Bright;
		Loop;
	}
}

// Use to protect the player after weapon lose/resurrection
Class TemporaryExplosions : Inventory {

	int numberOfExplosions;
	// Number of tics between explosions
	int delayBetweenExplosions;
	int ticsSinceLastExplosion;
	property numberOfExplosions: numberOfExplosions;
	property delayBetweenExplosions: delayBetweenExplosions;
	
	PlayerPawn playerToProtect;
	
	default {
		TemporaryExplosions.numberOfExplosions 10;
		TemporaryExplosions.delayBetweenExplosions 4;
	}
	
	// Decrease 
	override void Tick()
	{
		playerToProtect = PlayerPawn(owner);
		
		super.Tick();
		//console.printf("invul timeToLive %d", timeToLive);
		if (numberOfExplosions > 0) {
		
			if (ticsSinceLastExplosion >= delayBetweenExplosions) {
			
				vector3 position;
				// Want explosions to be far enough away from the player to not
				// affect their movement (note they are currently 
				// invinicible/dead)
				float generatedX;
				float generatedY;
				
				generatedX = 0;
				// Find a far enough X
				while (generatedX < 128 && generatedX > -128) {
					generatedX = frandom(-256.0, 256.0);
				}
				generatedY = 0;
				// Find a far enough X
				while (generatedY < 128 && generatedY > -128) {
					generatedY = frandom(-256.0, 256.0);
				}
				
				// Now place near player
				position.x = playerToProtect.pos.x + generatedX;
				position.y = playerToProtect.pos.y + generatedY;
				// Want it roughly on player's level
				position.z = playerToProtect.pos.z + frandom(8.0, 16.0);
				
				Actor boom = Actor.Spawn("protectiveExplosion", position, NO_REPLACE);
				
				numberOfExplosions--;
				ticsSinceLastExplosion = 0;
				
			} else {
				ticsSinceLastExplosion++;
			}
		} else {
			playerToProtect.TakeInventory("TemporaryExplosions",1);
		}
		
	}
}


class SchmupPlayerHandler : EventHandler
{

	SchmupLife lifeToLose;
	
	override void PlayerEntered(playerEvent e) 
	{
		let player = players[e.PlayerNumber].mo;
		if (player) 
		{
			// Remove normal Doom weapons
			player.TakeInventory("Fist",1);
			player.TakeInventory("Pistol",1);

			// Schmup Weapons 
			player.GiveInventory("SchmupLife", 5);
			player.GiveInventory("BarrageRifle", 1);
			player.GiveInventory("CrowdControlGun", 1);
			player.GiveInventory("LancerGun", 1);
			player.GiveInventory("MicroMissileGun", 1);
		}
	}
	
	override void PlayerDied(PlayerEvent e)
	{

		let player = players[e.PlayerNumber];
		if (player) 
		{
			// Explosions anyway even if no lives left
			player.mo.GiveInventory("TemporaryExplosions", 1);
			// Increase number of explosions
			TemporaryExplosions explosions;
			explosions = TemporaryExplosions(player.mo.FindInventory("TemporaryExplosions"));
			explosions.numberOfExplosions = explosions.numberOfExplosions * 4;
			
			lifeToLose = SchmupLife(player.mo.FindInventory("SchmupLife"));
			if (lifeToLose) {
				//console.printf("Player still has at least one life. Health %d", player.Health);
				// Should be a delay before reviving
				lifeToLose.timeToResurrect = 175;
				//console.printf("Health %d", player.Health);
			}
			// else just let player die like normal
		}
		
	}
	
	// Need to prevent player restarting the map instead of resurrecting
	override bool InputProcess(InputEvent e)
	{
		// console.printf("key %d pressed", e.KeyScan);
		int bind1, bind2 = Bindings.GetKeysForCommand("+use");
		// console.printf("bind1 %d bind2 %d", bind1, bind2);
		if ((e.KeyScan == bind1 || e.KeyScan == bind2) && e.Type == InputEvent.Type_KeyDown) {
			// console.printf("key %d pressed", e.KeyScan);
			
			PlayerPawn player = players[consoleplayer].mo;
			int schmupLives = player.CountInv("SchmupLife");
			
			// Has at least 1 schmup life left and is dead
			if (schmupLives && player.health <= 0) {
				// console.printf("Trying to press use while has schmup life");
				// Don't do anything else with the use key
				return true;
			}
		}
		
		// Either no lives left or wasn't the use key pressed
		return false;
	}
	
	// Draw HUD to the screen
	override void RenderOverlay(RenderEvent e)
	{
		// Don't draw over the automap!
		if (AutoMapActive) { 
			return; 
		}
		
		PlayerPawn player = players[consoleplayer].mo;
		
		int schmupLives = player.CountInv("SchmupLife");
		
		CrowdControlGun crowdControlGun = CrowdControlGun(player.FindInventory("CrowdControlGun"));
		LancerGun lancerGun = LancerGun(player.FindInventory("LancerGun"));
		MicroMissileGun microMissileGun = MicroMissileGun(player.FindInventory("MicroMissileGun"));
		
		Statusbar.BeginHUD();
		
		// Draw lives count
		HUDFont livesFont = HUDFont.Create(smallfont);
		// Draw Doom Guy's face next to the lives
		Statusbar.DrawImage("STFST01", (-55, 42), Statusbar.DI_SCREEN_RIGHT_TOP | Statusbar.DI_ITEM_RIGHT_TOP | Statusbar.DI_ITEM_RIGHT , scale: (0.5,0.5));
		// Draw number of lives
		String schmupLivesString = String.format("Lives: %d", schmupLives);
		Statusbar.DrawString(livesFont, schmupLivesString, (-5,45), Statusbar.DI_SCREEN_RIGHT_TOP | Statusbar.DI_TEXT_ALIGN_RIGHT, Font.CR_RED, scale: (1,1));
		
		// Draw guns' statuses. Show health if exists or show red X if destroyed
		// First get font for red X
		HUDFont destroyedXFont = HUDFont.Create(bigfont);
		
		// Draw Barrage Rifle (no health!)
		Statusbar.DrawImage("BUTPA0", (-175,8), Statusbar.DI_SCREEN_RIGHT_TOP | Statusbar.DI_ITEM_RIGHT_TOP | Statusbar.DI_ITEM_RIGHT, scale: (1,1));
		
		// Draw Crowd Control Gun
		Statusbar.DrawImage("REPGI0", (-120,8), Statusbar.DI_SCREEN_RIGHT_TOP | Statusbar.DI_ITEM_RIGHT_TOP | Statusbar.DI_ITEM_RIGHT, scale: (1,1));
		// Has gun been destroyed then draw X, else draw health bar
		if (!crowdControlGun) {
			Statusbar.DrawString(destroyedXFont, "X", (-120,8), Statusbar.DI_SCREEN_RIGHT_TOP | Statusbar.DI_TEXT_ALIGN_RIGHT, Font.CR_RED, scale: (4,2));
		} else {
			Statusbar.DrawBar("SUIHLFBF", "SUIHLFBE", crowdControlGun.health, 100, (-144,40), Statusbar.SHADER_HORZ, Statusbar.DI_SCREEN_RIGHT_TOP | Statusbar.DI_TEXT_ALIGN_RIGHT);
		}
		
		// Draw Lancer Gun
		Statusbar.DrawImage("SUI3X0", (-60,8), Statusbar.DI_SCREEN_RIGHT_TOP | Statusbar.DI_ITEM_RIGHT_TOP | Statusbar.DI_ITEM_RIGHT, scale: (1,1));
		if (!lancerGun) {
			Statusbar.DrawString(destroyedXFont, "X", (-60,8), Statusbar.DI_SCREEN_RIGHT_TOP | Statusbar.DI_TEXT_ALIGN_RIGHT, Font.CR_RED, scale: (4,2));
		} else {
			Statusbar.DrawBar("SUIHLFBF", "SUIHLFBE", lancerGun.health, 100, (-84,40), Statusbar.SHADER_HORZ, Statusbar.DI_SCREEN_RIGHT_TOP | Statusbar.DI_TEXT_ALIGN_RIGHT);
		}
		
		// Draw Micro Missile Gun
		Statusbar.DrawImage("PRXPA0", (-5,8), Statusbar.DI_SCREEN_RIGHT_TOP | Statusbar.DI_ITEM_RIGHT_TOP | Statusbar.DI_ITEM_RIGHT, scale: (1,1));
		if (!microMissileGun) {
			Statusbar.DrawString(destroyedXFont, "X", (-5,8), Statusbar.DI_SCREEN_RIGHT_TOP | Statusbar.DI_TEXT_ALIGN_RIGHT, Font.CR_RED, scale: (4,2));
		} else {
			Statusbar.DrawBar("SUIHLFBF", "SUIHLFBE", microMissileGun.health, 100, (-24,40), Statusbar.SHADER_HORZ, Statusbar.DI_SCREEN_RIGHT_TOP | Statusbar.DI_TEXT_ALIGN_RIGHT);
		}
		
	}
}