L4D2 Decrypted mutations

From Valve Developer Community
Jump to: navigation, search

Contents

SquirrelLeft 4 Dead 2 These are all official L4D2 mutation scripts and their included scripts. Most scripts have been decrypted on February 26th 2021 with L4D2 VScript Editor Beta 0.5 by Cynick and may be updated at any time.

Valve Mutations

L4d1


//============ Copyright (c) Valve Corporation, All rights reserved. ==========
//
//
//=============================================================================
Msg("Activating Mutation L4D1\n");

if ( !IsModelPrecached( "models/infected/smoker_l4d1.mdl" ) )
	PrecacheModel( "models/infected/smoker_l4d1.mdl" );
if ( !IsModelPrecached( "models/infected/boomer_l4d1.mdl" ) )
	PrecacheModel( "models/infected/boomer_l4d1.mdl" );
if ( !IsModelPrecached( "models/infected/hunter_l4d1.mdl" ) )
	PrecacheModel( "models/infected/hunter_l4d1.mdl" );
if ( !IsModelPrecached( "models/infected/hulk_l4d1.mdl" ) )
	PrecacheModel( "models/infected/hulk_l4d1.mdl" );

DirectorOptions <-
{
	ActiveChallenge = 1

	SpitterLimit = 0
	JockeyLimit = 0
	ChargerLimit = 0

	EscapeSpawnTanks = true

	weaponsToConvert =
	{
		weapon_shotgun_spas				= "weapon_autoshotgun_spawn"
		weapon_defibrillator			= "weapon_pain_pills_spawn"
		weapon_ammo_pack				= "weapon_first_aid_kit_spawn"
		weapon_sniper_awp				= "weapon_hunting_rifle_spawn"
		weapon_sniper_military			= "weapon_hunting_rifle_spawn"
		weapon_sniper_scout				= "weapon_hunting_rifle_spawn"
		weapon_vomitjar					= "weapon_molotov_spawn"
		weapon_adrenaline				= "weapon_pain_pills_spawn"
		weapon_pistol_magnum			= "weapon_pistol_spawn"
		weapon_shotgun_chrome			= "weapon_pumpshotgun_spawn"
		weapon_rifle_ak47				= "weapon_rifle_spawn"
		weapon_rifle_desert				= "weapon_rifle_spawn"
		weapon_rifle_sg552				= "weapon_rifle_spawn"
		weapon_smg_mp5					= "weapon_smg_spawn"
		weapon_smg_silenced				= "weapon_smg_spawn"
	}

	function ConvertWeaponSpawn( classname )
	{
		if ( classname in weaponsToConvert )
		{
			return weaponsToConvert[classname];
		}
		return 0;
	}

	weaponsToRemove =
	{
		weapon_rifle_m60 = 0
		weapon_grenade_launcher = 0
		weapon_chainsaw = 0
		weapon_melee = 0
		weapon_upgradepack_explosive = 0
		weapon_upgradepack_incendiary = 0
		upgrade_item = 0
	}

	function AllowWeaponSpawn( classname )
	{
		if ( classname in weaponsToRemove )
		{
			return false;
		}
		return true;
	}

	function ShouldAvoidItem( classname )
	{
		if ( classname in weaponsToRemove )
		{
			return true;
		}
		return false;
	}
}

function OnGameEvent_round_start_post_nav( params )
{
	EntFire( "worldspawn", "AddOutput", "timeofday 0" );
	EntFire( "weapon_*", "AddOutput", "skin 0" );
	EntFire( "weapon_*", "AddOutput", "weaponskin -1" );
	EntFire( "trigger_upgrade_laser_sight", "Kill" );

	foreach( wep, val in DirectorOptions.weaponsToRemove )
		EntFire( wep + "_spawn", "Kill" );
	foreach( wep, val in DirectorOptions.weaponsToConvert )
	{
		for ( local wep_spawner; wep_spawner = Entities.FindByClassname( wep_spawner, wep + "_spawn" ); )
		{
			local spawnTable =
			{
				origin = wep_spawner.GetOrigin(),
				angles = wep_spawner.GetAngles().ToKVString(),
				targetname = wep_spawner.GetName(),
				count = NetProps.GetPropInt( wep_spawner, "m_itemCount" ),
				spawnflags = NetProps.GetPropInt( wep_spawner, "m_spawnflags" )
			}
			wep_spawner.Kill();
			SpawnEntityFromTable(val, spawnTable);
		}
	}

	if ( Director.IsL4D1Campaign() )
	{
		DirectorOptions.WaterSlowsMovement <- false;

		if ( SessionState.ModeName == "l4d1coop" || SessionState.ModeName == "l4d1vs" )
		{
			if ( IsMissionFinalMap() )
			{
				if ( SessionState.MapName != "c7m3_port" )
				{
					local finale = Entities.FindByClassname( null, "trigger_finale" );
					if ( finale )
						NetProps.SetPropInt( finale, "m_type", 0 );
				}
			}
			else
			{
				if ( SessionState.MapName == "c10m4_mainstreet" )
				{
					local relay = Entities.FindByName( null, "forklift_relay" );
					if ( relay )
					{
						EntityOutputs.RemoveOutput( relay, "OnTrigger", "director", "BeginScript", "c10m4_onslaught" );
						EntityOutputs.AddOutput( relay, "OnTrigger", "director", "ForcePanicEvent", "", 9.0, -1 );
					}
					EntFire( "onslaught1", "Kill" );
				}
				else if ( SessionState.MapName == "c11m4_terminal" )
				{
					local van = Entities.FindByName( null, "van_button" );
					if ( van )
					{
						EntityOutputs.RemoveOutput( van, "OnPressed", "@director", "", "" );
						EntityOutputs.AddOutput( van, "OnPressed", "@director", "ForcePanicEvent", "", 3.0, -1 );
					}
					local relay = Entities.FindByName( null, "alarm_on_relay" );
					if ( relay )
					{
						EntityOutputs.RemoveOutput( relay, "OnTrigger", "@director", "", "" );
						EntityOutputs.RemoveOutput( relay, "OnTrigger", "alarm_safety_relay", "", "" );
						EntityOutputs.AddOutput( relay, "OnTrigger", "@director", "ForcePanicEvent", "", 0.0, -1 );
						EntityOutputs.AddOutput( relay, "OnTrigger", "alarm_off_relay", "Trigger", "", 15.0, -1 );
					}
					EntFire( "van_follow_trigger", "Kill" );
					EntFire( "van_endscript_relay", "Kill" );
					EntFire( "onslaught_hint_trigger", "Kill" );
				}
				else if ( SessionState.MapName == "c12m3_bridge" )
				{
					local relay = Entities.FindByName( null, "train_engine_relay" );
					if ( relay )
					{
						EntityOutputs.RemoveOutput( relay, "OnTrigger", "director", "BeginScript", "c12m3_onslaught" );
						EntityOutputs.AddOutput( relay, "OnTrigger", "director", "ForcePanicEvent", "", 2.0, -1 );
					}
					EntFire( "zombie_spawn1", "Kill" );
					EntFire( "onslaught_hint_template", "Kill" );
				}
				else if ( SessionState.MapName == "c12m4_barn" )
					EntFire( "window_trigger", "Kill" );
			}
		}
	}
}

if ( HasPlayerControlledZombies() )
{
	if ( !IsModelPrecached( "models/v_models/weapons/v_claw_smoker_l4d1.mdl" ) )
		PrecacheModel( "models/v_models/weapons/v_claw_smoker_l4d1.mdl" );
	if ( !IsModelPrecached( "models/v_models/weapons/v_claw_boomer_l4d1.mdl" ) )
		PrecacheModel( "models/v_models/weapons/v_claw_boomer_l4d1.mdl" );
	if ( !IsModelPrecached( "models/v_models/weapons/v_claw_hunter_l4d1.mdl" ) )
		PrecacheModel( "models/v_models/weapons/v_claw_hunter_l4d1.mdl" );
	if ( !IsModelPrecached( "models/v_models/weapons/v_claw_hulk_l4d1.mdl" ) )
		PrecacheModel( "models/v_models/weapons/v_claw_hulk_l4d1.mdl" );

	function OnGameEvent_item_pickup( params )
	{
		local player = GetPlayerFromUserID( params["userid"] );

		if ( ( !player ) || ( player.IsSurvivor() ) )
			return;

		local modelName = player.GetModelName();
		if ( ( modelName.find( "l4d1" ) != null ) || ( modelName == "models/infected/hulk_dlc3.mdl" ) )
			return;

		local function SetClawModel( modelName )
		{
			local claw = player.GetActiveWeapon();
			local viewmodel = NetProps.GetPropEntity( player, "m_hViewModel" );

			if ( ( !claw ) || ( !viewmodel ) )
				return;

			claw.SetModel( modelName );
			NetProps.SetPropInt( viewmodel, "m_nModelIndex", NetProps.GetPropInt( claw, "m_nModelIndex" ) );
			NetProps.SetPropString( viewmodel, "m_ModelName", modelName );
		}

		switch( player.GetZombieType() )
		{
			case 1:
			{
				player.SetModel( "models/infected/smoker_l4d1.mdl" );
				SetClawModel( "models/v_models/weapons/v_claw_smoker_l4d1.mdl" );
				break;
			}
			case 2:
			{
				player.SetModel( "models/infected/boomer_l4d1.mdl" );
				SetClawModel( "models/v_models/weapons/v_claw_boomer_l4d1.mdl" );
				break;
			}
			case 3:
			{
				player.SetModel( "models/infected/hunter_l4d1.mdl" );
				SetClawModel( "models/v_models/weapons/v_claw_hunter_l4d1.mdl" );
				break;
			}
			case 8:
			{
				player.SetModel( "models/infected/hulk_l4d1.mdl" );
				SetClawModel( "models/v_models/weapons/v_claw_hulk_l4d1.mdl" );
				break;
			}
			default:
				break;
		}
	}
}

function OnGameEvent_player_spawn( params )
{
	local player = GetPlayerFromUserID( params["userid"] );

	if ( ( !player ) || ( player.IsSurvivor() ) )
		return;

	local modelName = player.GetModelName();
	if ( ( modelName.find( "l4d1" ) != null ) || ( modelName == "models/infected/hulk_dlc3.mdl" ) )
		return;

	switch( player.GetZombieType() )
	{
		case 1:
		{
			player.SetModel( "models/infected/smoker_l4d1.mdl" );
			break;
		}
		case 2:
		{
			player.SetModel( "models/infected/boomer_l4d1.mdl" );
			break;
		}
		case 3:
		{
			player.SetModel( "models/infected/hunter_l4d1.mdl" );
			break;
		}
		case 8:
		{
			player.SetModel( "models/infected/hulk_l4d1.mdl" );
			break;
		}
		default:
			break;
	}
}

function OnGameEvent_player_death( params )
{
	if ( !("userid" in params) )
		return;

	local victim = GetPlayerFromUserID( params["userid"] );

	if ( ( !victim ) || ( !victim.IsSurvivor() ) )
		return;

	local prevRagdoll = NetProps.GetPropEntity( victim, "m_hRagdoll" );
	if ( prevRagdoll != null )
		return;

	local clOrigin = victim.GetOrigin();

	local ragdoll = null;
	// cs_ragdoll can crash if proper netprops aren't set, some future-proofing
	// get rid of uninitialized ragdoll if something goes wrong here
	try
	{
		ragdoll = SpawnEntityFromTable( "cs_ragdoll", {} )
		NetProps.SetPropVector( ragdoll, "m_vecOrigin", clOrigin );
		NetProps.SetPropVector( ragdoll, "m_vecRagdollOrigin", clOrigin );
		NetProps.SetPropInt( ragdoll, "m_nModelIndex", NetProps.GetPropInt( victim, "m_nModelIndex" ) );
		NetProps.SetPropInt( ragdoll, "m_iTeamNum", NetProps.GetPropInt( victim, "m_iTeamNum" ) );
		NetProps.SetPropEntity( ragdoll, "m_hPlayer", victim );
		NetProps.SetPropInt( ragdoll, "m_iDeathPose", NetProps.GetPropInt( victim, "m_nSequence" ) );
		NetProps.SetPropInt( ragdoll, "m_iDeathFrame", NetProps.GetPropInt( victim, "m_flAnimTime" ) );
		NetProps.SetPropInt( ragdoll, "m_bClientSideAnimation", 1 );
		NetProps.SetPropInt( ragdoll, "m_iTeamNum", NetProps.GetPropInt( victim, "m_iTeamNum" ) );
		NetProps.SetPropInt( ragdoll, "m_nForceBone", NetProps.GetPropInt( victim, "m_nForceBone" ) );
		NetProps.SetPropInt( ragdoll, "m_ragdollType", 4 );
		NetProps.SetPropInt( ragdoll, "m_survivorCharacter", NetProps.GetPropInt( victim, "m_survivorCharacter" ) );
		NetProps.SetPropEntity( victim, "m_hRagdoll", ragdoll );

		//EntFire( "survivor_death_model", "Kill" );
		// EntFire is too slow and you can see one-frame image of death model
		for ( local body; body = Entities.FindByClassname( body, "survivor_death_model" ); )
		{
			body.Kill();
		}
	}
	catch (err)
	{
		if ( ragdoll != null && ragdoll.IsValid() )
			ragdoll.Kill();

		EntFire( "survivor_death_model", "BecomeRagdoll" );
	}
}

L4d1coop - L4D1 Co-op


//============ Copyright (c) Valve Corporation, All rights reserved. ==========
//
//
//=============================================================================

IncludeScript( "l4d1" )

L4d1vs - L4D1 Versus


//============ Copyright (c) Valve Corporation, All rights reserved. ==========
//
//
//=============================================================================

IncludeScript( "l4d1" )

L4d1survival - L4D1 Survival


//============ Copyright (c) Valve Corporation, All rights reserved. ==========
//
//
//=============================================================================

IncludeScript( "l4d1" )

Mutation1 - Last Man on Earth


//-----------------------------------------------------
Msg("Activating Mutation 1\n");


DirectorOptions <-
{
	ActiveChallenge = 1

	cm_NoSurvivorBots = 1
	cm_CommonLimit = 0
	cm_DominatorLimit = 1
	cm_MaxSpecials = 2
	cm_SpecialRespawnInterval = 60
	cm_AutoReviveFromSpecialIncap = 1
	cm_AllowPillConversion = 0

	BoomerLimit = 0
	MobMaxPending = 0
	SurvivorMaxIncapacitatedCount = 1
	SpecialInitialSpawnDelayMin = 5
	SpecialInitialSpawnDelayMax = 30
	TankHitDamageModifierCoop = 0.5
	
	// convert items that aren't useful
	weaponsToConvert =
	{
		weapon_pipe_bomb =	"weapon_molotov_spawn"
		weapon_vomitjar =	"weapon_molotov_spawn"
		weapon_defibrillator =	"weapon_first_aid_kit_spawn"

		weapon_smg =		"weapon_rifle_spawn"
		weapon_pumpshotgun =	"weapon_autoshotgun_spawn"
		weapon_smg_silenced =	"weapon_rifle_desert_spawn"
		weapon_shotgun_chrome =	"weapon_shotgun_spas_spawn"
		weapon_smg_mp5 =	"weapon_rifle_sg552_spawn"
	}

	function ConvertWeaponSpawn( classname )
	{
		if ( classname in weaponsToConvert )
		{
			return weaponsToConvert[classname];
		}
		return 0;
	}	
}

function OnGameEvent_round_start_post_nav( params )
{
	local spawner = null;
	while ( spawner = Entities.FindByClassname( spawner, "info_zombie_spawn" ) )
	{
		if ( spawner.IsValid() )
		{
			local population = NetProps.GetPropString( spawner, "m_szPopulation" );
			
			if ( population == "boomer" || population == "hunter" || population == "smoker" || population == "jockey"
				|| population == "charger" || population == "spitter" || population == "new_special" || population == "church"
					|| population == "tank" || population == "witch" || population == "witch_bride" || population == "river_docks_trap" )
				continue;
			else
				spawner.Kill();
		}
	}
	
	if ( Director.GetMapName() == "c5m5_bridge" || Director.GetMapName() == "c6m3_port" )
		DirectorOptions.cm_MaxSpecials = 0;
	
	foreach( wep, val in DirectorOptions.weaponsToConvert )
	{
		local wep_spawner = null;
		while ( wep_spawner = Entities.FindByClassname( wep_spawner, wep + "_spawn" ) )
		{
			if ( wep_spawner.IsValid() )
			{
				local spawnTable =
				{
					origin = wep_spawner.GetOrigin(),
					angles = wep_spawner.GetAngles().ToKVString(),
					targetname = wep_spawner.GetName(),
					count = NetProps.GetPropInt( wep_spawner, "m_itemCount" ),
					spawnflags = NetProps.GetPropInt( wep_spawner, "m_spawnflags" )
				}
				wep_spawner.Kill();
				SpawnEntityFromTable(val, spawnTable);
			}
		}
	}
}

function OnGameEvent_finale_start( params )
{
	if ( Director.GetMapName() == "c6m3_port" )
		DirectorOptions.cm_MaxSpecials = 2;
}

function OnGameEvent_gauntlet_finale_start( params )
{
	if ( Director.GetMapName() == "c5m5_bridge" )
		DirectorOptions.cm_MaxSpecials = 2;
}

function Update()
{
	if ( Director.GetCommonInfectedCount() > 0 )
	{
		local infected = null;
		while ( infected = Entities.FindByClassname( infected, "infected" ) )
		{
			if ( infected.IsValid() )
				infected.Kill();
		}
	}
}

Mutation2 - Headshot!


//-----------------------------------------------------
Msg("Activating Mutation 2\n");


DirectorOptions <-
{
	ActiveChallenge = 1

	cm_HeadshotOnly = 1
}

Mutation3 - Bleed Out


//-----------------------------------------------------
Msg("Activating Mutation 3\n");


DirectorOptions <-
{
	ActiveChallenge = 1

	weaponsToConvert =
	{
		weapon_first_aid_kit = "weapon_pain_pills_spawn"
	}

	function ConvertWeaponSpawn( classname )
	{
		if ( classname in weaponsToConvert )
		{
			return weaponsToConvert[classname];
		}
		return 0;
	}	

	// Challenge vars
	cm_TempHealthOnly = 1
	cm_AllowPillConversion = 0
	cm_ShouldHurry = 1
	cm_MaxSpecials = 4
	cm_SpecialRespawnInterval = 45
	cm_WanderingZombieDensityModifier = 0.003

	// Global vars
	MobSpawnMinTime = 5
	MobSpawnMaxTime = 10
	MobMinSize = 10
	MobMaxSize = 15
	MobMaxPending = 30
	SustainPeakMinTime = 10
	SustainPeakMaxTime = 15
	IntensityRelaxThreshold = 0.95
	RelaxMinInterval = 3
	RelaxMaxInterval = 5
	RelaxMaxFlowTravel = 50
	PreferredMobDirection = SPAWN_BEHIND_SURVIVORS

	TempHealthDecayRate = 0.001
	function RecalculateHealthDecay()
	{
		if ( Director.HasAnySurvivorLeftSafeArea() )
		{
			TempHealthDecayRate = 0.27 // pain_pills_decay_rate default
		}
	}
}

function Update()
{
	DirectorOptions.RecalculateHealthDecay();
}

Mutation4 - Hard Eight


//-----------------------------------------------------
Msg("Activating Mutation 4\n");


DirectorOptions <-
{
	ActiveChallenge = 1

	cm_SpecialRespawnInterval = 15
	cm_MaxSpecials = 8
	cm_BaseSpecialLimit = 2

	DominatorLimit = 8
}

Mutation5 - Four Swordsmen


//-----------------------------------------------------
Msg("Activating Mutation 5\n");


DirectorOptions <-
{
	ActiveChallenge = 1

	cm_CommonLimit = 0
	cm_DominatorLimit = 8
	cm_MaxSpecials = 8
	cm_SpecialRespawnInterval = 15

	SpecialInitialSpawnDelayMin = 5
	SpecialInitialSpawnDelayMax = 30
	
	// convert items that aren't useful
	weaponsToConvert =
	{
		weapon_pipe_bomb =	"weapon_molotov_spawn"
		weapon_vomitjar =	"weapon_molotov_spawn"
		weapon_defibrillator =	"weapon_first_aid_kit_spawn"
	}

	function ConvertWeaponSpawn( classname )
	{
		if ( classname in weaponsToConvert )
		{
			return weaponsToConvert[classname];
		}
		return 0;
	}	

	weaponsToRemove =
	{
		weapon_pistol = 0
		weapon_pistol_magnum = 0
		weapon_smg = 0
		weapon_pumpshotgun = 0
		weapon_autoshotgun = 0
		weapon_rifle = 0
		weapon_hunting_rifle = 0
		weapon_smg_silenced = 0
		weapon_shotgun_chrome = 0
		weapon_rifle_desert = 0
		weapon_sniper_military = 0
		weapon_shotgun_spas = 0
		weapon_grenade_launcher = 0
		weapon_rifle_ak47 = 0
		weapon_smg_mp5 = 0
		weapon_rifle_sg552 = 0
		weapon_sniper_awp = 0
		weapon_sniper_scout = 0
		weapon_rifle_m60 = 0
		weapon_melee = 0
		weapon_chainsaw = 0
		weapon_upgradepack_incendiary = 0
		weapon_upgradepack_explosive = 0
		ammo = 0
		upgrade_item = 0
	}

	function AllowWeaponSpawn( classname )
	{
		if ( classname in weaponsToRemove )
		{
			return false;
		}
		return true;
	}

	function ShouldAvoidItem( classname )
	{
		if ( ( classname != "weapon_melee" ) && ( classname in weaponsToRemove ) )
		{
			return true;
		}
		return false;
	}

	DefaultItems =
	[
		"katana",
	]

	function GetDefaultItem( idx )
	{
		if ( idx < DefaultItems.len() )
		{
			return DefaultItems[idx];
		}
		return 0;
	}
}

function OnGameEvent_round_start_post_nav( params )
{
	local spawner = null;
	while ( spawner = Entities.FindByClassname( spawner, "info_zombie_spawn" ) )
	{
		if ( spawner.IsValid() )
		{
			local population = NetProps.GetPropString( spawner, "m_szPopulation" );
			
			if ( population == "boomer" || population == "hunter" || population == "smoker" || population == "jockey"
				|| population == "charger" || population == "spitter" || population == "new_special" || population == "church"
					|| population == "tank" || population == "witch" || population == "witch_bride" || population == "river_docks_trap" )
				continue;
			else
				spawner.Kill();
		}
	}
	
	if ( Director.GetMapName() == "c5m5_bridge" || Director.GetMapName() == "c6m3_port" )
		DirectorOptions.cm_MaxSpecials = 0;
	
	EntFire( "weapon_spawn", "Kill" );
	foreach( wep, val in DirectorOptions.weaponsToRemove )
		EntFire( wep + "_spawn", "Kill" );
}

function OnGameEvent_finale_start( params )
{
	if ( Director.GetMapName() == "c6m3_port" )
		DirectorOptions.cm_MaxSpecials = 8;
}

function OnGameEvent_gauntlet_finale_start( params )
{
	if ( Director.GetMapName() == "c5m5_bridge" )
		DirectorOptions.cm_MaxSpecials = 8;
}

function Update()
{
	if ( Director.GetCommonInfectedCount() > 0 )
	{
		local infected = null;
		while ( infected = Entities.FindByClassname( infected, "infected" ) )
		{
			if ( infected.IsValid() )
				infected.Kill();
		}
	}
}

Mutation6

This 'mutation' is missing from gamemodes.txt since release, apparently. The .nuc was removed from the game files between 2.0.2.7 and 2.0.4.5.


//-----------------------------------------------------
Msg("Activating Mutation 6\n");


DirectorOptions <-
{
	ActiveChallenge = 1

	cm_MaxSpecials = 0
	cm_CommonLimit = 0
	cm_WitchLimit = 0
	cm_TankLimit = 0
	cm_ProhibitBosses = 1
	cm_NoRescueClosets = 1
}

Mutation7 - Chainsaw Massacre


//-----------------------------------------------------
Msg("Activating Mutation 7\n");


DirectorOptions <-
{
	ActiveChallenge = 1

	cm_InfiniteFuel = 1
	cm_ShouldHurry = 1

	weaponsToRemove =
	{
		weapon_pistol = 0
		weapon_pistol_magnum = 0
		weapon_smg = 0
		weapon_pumpshotgun = 0
		weapon_autoshotgun = 0
		weapon_rifle = 0
		weapon_hunting_rifle = 0
		weapon_smg_silenced = 0
		weapon_shotgun_chrome = 0
		weapon_rifle_desert = 0
		weapon_sniper_military = 0
		weapon_shotgun_spas = 0
		weapon_grenade_launcher = 0
		weapon_rifle_ak47 = 0
		weapon_smg_mp5 = 0
		weapon_rifle_sg552 = 0
		weapon_sniper_awp = 0
		weapon_sniper_scout = 0
		weapon_rifle_m60 = 0
		weapon_melee = 0
		weapon_chainsaw = 0
		weapon_upgradepack_incendiary = 0
		weapon_upgradepack_explosive = 0
		ammo = 0
		upgrade_item = 0
	}

	function AllowWeaponSpawn( classname )
	{
		if ( classname in weaponsToRemove )
		{
			return false;
		}
		return true;
	}

	function ShouldAvoidItem( classname )
	{
		if ( ( classname != "weapon_chainsaw" ) && ( classname in weaponsToRemove ) )
		{
			return true;
		}
		return false;
	}

	DefaultItems =
	[
		"weapon_chainsaw",
		"weapon_pistol",
	]

	function GetDefaultItem( idx )
	{
		if ( idx < DefaultItems.len() )
		{
			return DefaultItems[idx];
		}
		return 0;
	}
}

function OnGameEvent_round_start_post_nav( params )
{
	EntFire( "weapon_spawn", "Kill" );
	foreach( wep, val in DirectorOptions.weaponsToRemove )
		EntFire( wep + "_spawn", "Kill" );
}

Mutation8 - Iron Man


//-----------------------------------------------------
Msg("Activating Mutation 8\n");


DirectorOptions <- 
{
	ActiveChallenge = 1

	cm_AllowSurvivorRescue = 0

	weaponsToRemove =
	{
		ammo = 0
	}

	function AllowWeaponSpawn( classname )
	{
		if ( classname in weaponsToRemove )
		{
			return false;
		}
		return true;
	}	
}

Mutation9 - Last Gnome On Earth


//-----------------------------------------------------
Msg("Activating Mutation 9\n");


DirectorOptions <- 
{
	ActiveChallenge = 1

	cm_VIPTarget = 1
	cm_ShouldHurry = 1
}

Mutation10 - Room For One


//-----------------------------------------------------
Msg("Activating Mutation 10\n");


DirectorOptions <-
{
	ActiveChallenge = 1

	cm_FirstManOut = 1
}

Mutation11 - Healthpackalypse!


//-----------------------------------------------------
Msg("Activating Mutation 11\n");

DirectorOptions <-
{
	ActiveChallenge = 1

	weaponsToRemove =
	{
		weapon_first_aid_kit = 0
		weapon_pain_pills = 0
		weapon_adrenaline = 0
		weapon_defibrillator = 0
	}

	function AllowWeaponSpawn( classname )
	{
		if ( classname in weaponsToRemove )
		{
			return false;
		}
		return true;
	}		
}

Mutation12 - Realism Versus


//-----------------------------------------------------
Msg("Activating Mutation 12\n");


DirectorOptions <-
{
	ActiveChallenge = 1

	cm_AllowPillConversion = 0

	ZombieGhostDelayMin = 15
	ZombieGhostDelayMax = 25

	weaponsToRemove =
	{
		weapon_first_aid_kit = 0
		weapon_defibrillator = 0
	}

	function AllowWeaponSpawn( classname )
	{
		if ( classname in weaponsToRemove )
		{
			return false;
		}
		return true;
	}		

	DefaultItems =
	[
		"weapon_first_aid_kit",
		"weapon_pistol",
	]

	function GetDefaultItem( idx )
	{
		if ( idx < DefaultItems.len() )
		{
			return DefaultItems[idx];
		}
		return 0;
	}	
}

Mutation13 - Follow the Liter


//-----------------------------------------------------
Msg("Activating Mutation 13\n");


DirectorOptions <- 
{
	ActiveChallenge = 1

	cm_SingleScavengeCluster = 1

	ScavengeScoreBonusTime = 30
}

Mutation14 - Gib Fest


//-----------------------------------------------------
Msg("Activating Mutation 14\n");


DirectorOptions <-
{
	ActiveChallenge = 1

	weaponsToRemove =
	{
		weapon_pistol = 0
		weapon_pistol_magnum = 0
		weapon_smg = 0
		weapon_pumpshotgun = 0
		weapon_autoshotgun = 0
		weapon_rifle = 0
		weapon_hunting_rifle = 0
		weapon_smg_silenced = 0
		weapon_shotgun_chrome = 0
		weapon_rifle_desert = 0
		weapon_sniper_military = 0
		weapon_shotgun_spas = 0
		weapon_grenade_launcher = 0
		weapon_rifle_ak47 = 0
		weapon_smg_mp5 = 0
		weapon_rifle_sg552 = 0
		weapon_sniper_awp = 0
		weapon_sniper_scout = 0
		weapon_rifle_m60 = 0
		weapon_melee = 0
		weapon_chainsaw = 0
		ammo = 0
	}

	function AllowWeaponSpawn( classname )
	{
		if ( classname in weaponsToRemove )
		{
			return false;
		}
		return true;
	}

	function ShouldAvoidItem( classname )
	{
		if ( ( classname != "weapon_rifle_m60" && classname != "weapon_pistol_magnum" ) && ( classname in weaponsToRemove ) )
		{
			return true;
		}
		return false;
	}

	DefaultItems =
	[
		"weapon_rifle_m60",
		"weapon_pistol_magnum",
	]

	function GetDefaultItem( idx )
	{
		if ( idx < DefaultItems.len() )
		{
			return DefaultItems[idx];
		}
		return 0;
	}
}

function OnGameEvent_round_start_post_nav( params )
{
	EntFire( "weapon_spawn", "Kill" );
	foreach( wep, val in DirectorOptions.weaponsToRemove )
		EntFire( wep + "_spawn", "Kill" );
}

Mutation15 - Versus Survival


//-----------------------------------------------------
Msg("Activating Mutation 15\n");

DirectorOptions <-
{
	ActiveChallenge = 1

	cm_ProhibitBosses = false
	cm_CommonLimit = 25
	cm_TankLimit = 1
	ZombieTankHealth = 2667

	SurvivalSetupTime = 90

	weaponsToRemove =
	{
		weapon_upgradepack_explosive = 0
	}

	function AllowWeaponSpawn( classname )
	{
		if ( classname in weaponsToRemove )
		{
			return false;
		}
		return true;
	}
}

function OnGameEvent_survival_round_start( params )
{
	local validStartNames = { func_button_timed = "OnTimeUp", func_button = "OnPressed", script_func_button = "OnPressed", trigger_finale = "UseStart", prop_door_rotating = "OnOpen" };
	local function GetSurvivalStartEntity()
	{
		local validIO = { logic_relay = { input = "Trigger", output = "OnTrigger" }, logic_timer = { input = "Enable", output = "OnTimer" } };
		local function CheckOutputs( entity, outputName )
		{
			local ent = entity;
			local output = outputName;
			while ( ent )
			{
				local target = null;
				local targetOutput = "";
				local nElements = EntityOutputs.GetNumElements( ent, output );
				for ( local i = 0; i < nElements; i++ )
				{
					local outputs = {};
					EntityOutputs.GetOutputTable( ent, output, outputs, i );
					local outputTarget = Entities.FindByName( null, outputs.target );
					if ( !outputTarget )
						continue;

					if ( outputTarget.GetClassname() == "info_director" && outputs.input.find( "PanicEvent" ) != null )
						return outputTarget;
					else
					{
						if ( (outputTarget.GetClassname() in validIO) && (outputs.input == validIO[outputTarget.GetClassname()].input) )
						{
							target = outputTarget;
							targetOutput = validIO[outputTarget.GetClassname()].output;
						}
					}
				}
				ent = target;
				output = targetOutput;
			}
			return null;
		}

		foreach( classname, output in validStartNames )
		{
			for ( local ent; ent = Entities.FindByClassname( ent, classname ); )
			{
				if ( !EntityOutputs.HasAction( ent, output ) )
					continue;

				local target = CheckOutputs( ent, output );
				if ( (!target) || (target.GetClassname() != "info_director") )
					continue;

				return ent;
			}
		}
		return null;
	}

	EntFire( "info_director", "PanicEvent" );

	local startEntity = GetSurvivalStartEntity();
	if ( startEntity )
	{
		if ( startEntity.GetClassname() == "func_button_timed" || startEntity.GetClassname() == "trigger_finale" )
		{
			local nElements = EntityOutputs.GetNumElements( startEntity, validStartNames[startEntity.GetClassname()] );
			for ( local i = 0; i < nElements; i++ )
			{
				local outputs = {};
				EntityOutputs.GetOutputTable( startEntity, validStartNames[startEntity.GetClassname()], outputs, i );
				EntFire( outputs.target, outputs.input, outputs.parameter, outputs.delay );
			}
		}
		else
		{
			EntFire( startEntity.GetName(), "Unlock" );
			EntFire( startEntity.GetName(), "Press" );
			EntFire( startEntity.GetName(), "Open" );
		}
	}
}

function OnGameEvent_round_start_post_nav( params )
{
	local bRemoveClips = false;
	switch( SessionState.MapName )
	{
		// For comments and elaboration regarding these map fixes, see:
		//	Collection of Versus Survival & Taaannnk! Mutation stuck spawn fixes
		//	https://gist.github.com/Tsuey/e8996ad10bb7fb72e7d6b2aaa7bcd5db
		// Most of these fixes should eventually be replaced with a fuller clip brush re-work of all base maps for modders/PvP,
		// such as we already did for TLS at least with regard to new player SI areas opened on c8m3 (seen with ShowUpdate tool).

		case "c1m2_streets":
		{
			EntFire( g_UpdateName + "_yeswayturnpike_clipb", "Kill", null, 30 );		// Delay oddly required
			bRemoveClips = true;
			break;
		}
		case "c3m1_plankcountry":
		{
			EntFire( g_UpdateName + "_meticulous_funcinfclip01", "Kill", null, 30 );	// Delay oddly required
			make_axiswarp( "_axiswarp_semitrailer", "y-", 96, "-550 -8 0", "740 8 256", "-9505 10720 155" );
			bRemoveClips = true;
			break;
		}
		case "c5m4_quarter":
		{
			bRemoveClips = true;
			break;
		}
		case "c8m2_subway":
		{
			make_prop( "dynamic", "_cosmetic_oobstep", "models/props/cs_office/vending_machine.mdl", "7366 3801 270", "90 0 0", "shadow_no", "solid_no" );
			break;
		}
		case "c9m2_lots":
		{
			kill_entity( Entities.FindByClassnameNearest( "prop_dynamic", Vector( 8521, 5815, 348 ), 1 ) );
			break;
		}
		case "c10m2_drainage":
		{
			kill_entity( Entities.FindByClassnameNearest( "prop_dynamic", Vector( -8972, -7890, -320 ), 1 ) );
			make_ladder( "_ladder_versussurvivalfence1_cloned_tunnelmid", "-9478.5 -7280 -384", "155 45 -410" );
			make_ladder( "_ladder_versussurvivalfence2_cloned_tunnelmid", "-9478.5 -7280 -384", "155 0 -410" );
			make_ladder( "_ladder_versussurvivalfence3_cloned_tunnelmid", "-9478.5 -7280 -384", "155 -45 -410" );
			bRemoveClips = true;
			break;
		}
		case "c10m4_mainstreet":
		{
			make_clip( "_tankstuck_endbarricade", "SI Players", 1, "-400 -900 0", "400 900 1700", "3822 -3970 0" );
			make_clip( "_tankstuck_excesscorner", "SI Players", 1, "-200 -100 0", "200 100 1700", "-2520 -5048 -64" );
			break;
		}
		case "c11m2_offices":
		{
			kill_funcinfclip( 1043.94 );	// Final street left barricade
			break;
		}
		case "c12m3_bridge":
		{
			kill_funcinfclip( 311.003 );	// Wrongway at end of train tunnel
			break;
		}
		default:
			break;
	}

	if ( bRemoveClips )
		EntFire( "func_playerinfected_clip", "Kill" );
}

Mutation16 - Hunting Party


//-----------------------------------------------------
Msg("Activating Mutation 16\n");


DirectorOptions <-
{
	ActiveChallenge = 1

	cm_SpecialSlotCountdownTime = 15
	cm_SpecialRespawnInterval = 1
	cm_MaxSpecials = 4

	HunterLimit = 4
	BoomerLimit = 0
	SmokerLimit = 0
	SpitterLimit = 0
	ChargerLimit = 0
	JockeyLimit = 0
	
	DominatorLimit = 4
}

Mutation17 - Lone Gunman


//-----------------------------------------------------
Msg("Activating Mutation 17\n");


DirectorOptions <-
{
	ActiveChallenge = 1

	cm_NoSurvivorBots = 1

	cm_CommonLimit = 10
	cm_BaseCommonAttackDamage = 15
	cm_WanderingZombieDensityModifier = 0.015

	HunterLimit = 0
	SmokerLimit = 0
	ChargerLimit = 0
	JockeyLimit = 0
	SpitterLimit = 0

	weaponsToRemove =
	{
		weapon_pistol = 0
		weapon_smg = 0
		weapon_pumpshotgun = 0
		weapon_autoshotgun = 0
		weapon_rifle = 0
		weapon_hunting_rifle = 0
		weapon_smg_silenced = 0
		weapon_shotgun_chrome = 0
		weapon_rifle_desert = 0
		weapon_sniper_military = 0
		weapon_shotgun_spas = 0
		weapon_grenade_launcher = 0
		weapon_rifle_ak47 = 0
		weapon_smg_mp5 = 0
		weapon_rifle_sg552 = 0
		weapon_sniper_awp = 0
		weapon_sniper_scout = 0
		weapon_rifle_m60 = 0
		weapon_melee = 0
		weapon_chainsaw = 0
		weapon_rifle_m60 = 0
		ammo = 0
		upgrade_item = 0
	}

	function AllowWeaponSpawn( classname )
	{
		if ( classname in weaponsToRemove )
		{
			return false;
		}
		return true;
	}

	DefaultItems =
	[
		"weapon_pistol_magnum",
	]

	function GetDefaultItem( idx )
	{
		if ( idx < DefaultItems.len() )
		{
			return DefaultItems[idx];
		}
		return 0;
	}
}

function OnGameEvent_round_start_post_nav( params )
{
	EntFire( "weapon_spawn", "Kill" );
	foreach( wep, val in DirectorOptions.weaponsToRemove )
		EntFire( wep + "_spawn", "Kill" );
}

Mutation18 - Bleed Out Versus


//-----------------------------------------------------
Msg("Activating Mutation 18\n");


DirectorOptions <-
{
	ActiveChallenge = 1

	weaponsToConvert =
	{
		weapon_first_aid_kit = "weapon_pain_pills_spawn"
	}

	function ConvertWeaponSpawn( classname )
	{
		if ( classname in weaponsToConvert )
		{
			return weaponsToConvert[classname];
		}
		return 0;
	}	

	// Challenge vars
	cm_TempHealthOnly = 1
	cm_AllowPillConversion = 0
	cm_ShouldHurry = 1

	TempHealthDecayRate = 0.001
	function RecalculateHealthDecay()
	{
		if ( Director.HasAnySurvivorLeftSafeArea() )
		{
			TempHealthDecayRate = 0.27 // pain_pills_decay_rate default
		}
	}
}

function Update()
{
	DirectorOptions.RecalculateHealthDecay();
}

Mutation19 - Taaannnk!!


//-----------------------------------------------------
Msg("Activating Mutation 19\n");

DirectorOptions <-
{
	ActiveChallenge = 1

//	cm_frustrationTimer = 0
	cm_TankRun = true
	cm_ShouldHurry = true
	cm_AutoSpawnInfectedGhosts = true

	PreferredSpecialDirection = SPAWN_BEHIND_SURVIVORS
	SpawnBehindSurvivorsDistance = 2000
	TankRunSpawnDelay = 15

	MobMaxPending = 0

	// Always convert to the TANK!!!
	function ConvertZombieClass( iClass )
	{
		return 8;
	}

	weaponsToConvert =
	{
		weapon_first_aid_kit = "weapon_pain_pills_spawn"
	}

	function ConvertWeaponSpawn( classname )
	{
		if ( classname in weaponsToConvert )
		{
			return weaponsToConvert[classname];
		}
		return 0;
	}
}

function OnGameEvent_round_start_post_nav( params )
{
	local function CheckOutputs( entity, outputName )
	{
		local ent = entity;
		local output = outputName;
		while ( ent )
		{
			local target = null;
			local targetOutput = "";
			local nElements = EntityOutputs.GetNumElements( ent, output );
			for ( local i = 0; i < nElements; i++ )
			{
				local outputs = {};
				EntityOutputs.GetOutputTable( ent, output, outputs, i );
				local outputTarget = Entities.FindByName( null, outputs.target );
				if ( !outputTarget )
					continue;

				if ( outputs.input == "Unlock" || outputs.input == "Enable" || outputs.input == "SetValueTest" || outputs.input == "MoveToFloor" )
					return outputTarget;
				else
				{
					if ( outputTarget.GetClassname() == "logic_relay" && outputs.input == "Trigger" )
					{
						target = outputTarget;
						targetOutput = "OnTrigger";
					}
				}
			}
			ent = target;
			output = targetOutput;
		}
		return null;
	}
	for ( local trigger; trigger = Entities.FindByClassname( trigger, "trigger_multiple" ); )
	{
		if ( NetProps.GetPropInt( trigger, "m_bAllowIncapTouch" ) == 0 || !EntityOutputs.HasAction( trigger, "OnEntireTeamStartTouch" ) )
			continue;

		NetProps.SetPropInt( trigger, "m_bAllowIncapTouch", 0 );
		local target = CheckOutputs( trigger, "OnEntireTeamStartTouch" );
		if ( !target )
			continue;

		if ( target.GetClassname() == "logic_relay" )
		{
			for ( local button; button = Entities.FindByClassname( button, "func_button" ); )
			{
				local nElements = EntityOutputs.GetNumElements( button, "OnPressed" );
				for ( local i = 0; i < nElements; i++ )
				{
					local outputs = {};
					EntityOutputs.GetOutputTable( button, "OnPressed", outputs, i );
					if ( outputs.target == target.GetName() )
					{
						target = button;
						break;
					}
				}
			}
		}
		else if ( target.GetClassname() == "logic_branch" )
		{
			local foundListener = false;
			for ( local listener; listener = Entities.FindByClassname( listener, "logic_branch_listener" ); )
			{
				for ( local i = 0; i < 16; i++ )
				{
					if ( NetProps.GetPropString( listener, "m_nLogicBranchNames[" + i + "]" ) == target.GetName() )
					{
						foundListener = true;
						break;
					}
				}

				if ( foundListener )
				{
					local nElements = EntityOutputs.GetNumElements( listener, "OnAllTrue" );
					for ( local i = 0; i < nElements; i++ )
					{
						local outputs = {};
						EntityOutputs.GetOutputTable( listener, "OnAllTrue", outputs, i );
						if ( outputs.input == "Unlock" )
						{
							target = Entities.FindByName( null, outputs.target );
							break;
						}
					}
					break;
				}
			}
		}

		local validOutputs = { func_button_timed = "OnTimeUp", func_button = "OnPressed", script_func_button = "OnPressed", prop_door_rotating = "OnOpen" };
		if ( !target.GetClassname() in validOutputs )
			continue;

		target.ValidateScriptScope();
		local targetScope = target.GetScriptScope();
		targetScope.OverrideTrigger <- trigger;
		targetScope.UseEnt <- function()
		{
			for ( local player; player = Entities.FindByClassname( player, "player" ); )
			{
				if ( player.IsSurvivor() && player.IsIncapacitated() )
				{
					if ( !OverrideTrigger.IsTouching( player ) )
						player.TakeDamage( player.GetHealth(), 0, null );
				}
			}
			self.DisconnectOutput( validOutputs[target.GetClassname()], "UseEnt" );
		}
		target.ConnectOutput( validOutputs[target.GetClassname()], "UseEnt" );
	}

	local bRemoveClips = false;
	switch( SessionState.MapName )
	{
		// For comments and elaboration regarding these map fixes, see:
		//	Collection of Versus Survival & Taaannnk! Mutation stuck spawn fixes
		//	https://gist.github.com/Tsuey/e8996ad10bb7fb72e7d6b2aaa7bcd5db
		// Most of these fixes should eventually be replaced with a fuller clip brush re-work of all base maps for modders/PvP,
		// such as we already did for TLS at least with regard to new player SI areas opened on c8m3 (seen with ShowUpdate tool).

		case "c1m2_streets":
		{
			EntFire( g_UpdateName + "_yeswayturnpike_clipb", "Kill", null, 30 );		// Delay oddly required
			bRemoveClips = true;
			break;
		}
		case "c2m2_fairgrounds":
		{
			bRemoveClips = true;
			break;
		}
		case "c3m1_plankcountry":
		{
			EntFire( g_UpdateName + "_meticulous_funcinfclip01", "Kill", null, 30 );	// Delay oddly required
			make_axiswarp( "_axiswarp_semitrailer", "y-", 96, "-550 -8 0", "740 8 256", "-9505 10720 155" );
			bRemoveClips = true;
			break;
		}
		case "c5m2_park":
		{
			make_clip( "_tankstuck_startroof", "SI Players", 1, "-106 -240 0", "86 240 196", "-2936 -816 -58" );
			break;
		}
		case "c5m4_quarter":
		{
			bRemoveClips = true;
			break;
		}
		case "c6m1_riverbank":
		{
			make_clip( "_tankstuck_startfence", "SI Players", 1, "-251 -760 0", "173 216 1513", "-261 2872 87" );
			break;
		}
		case "c8m1_apartment":
		{
			make_clip( "_tankstuck_alleystart", "SI Players", 1, "-105 -711 0", "823 57 4132", "2473 455 16" );
			make_clip( "_tankstuck_alleymiddle", "SI Players", 1, "-127 -257 0", "209 256 4468", "-129 3584 16" );
			kill_funcinfclip( 1060.85 );	// Delete clip that blocks the rubble and a cool unique spawn
			break;
		}
		case "c8m2_subway":
		{
			make_clip( "_tankstuck_rubblestart", "SI Players", 1, "-139 -107 0", "119 99 177", "1621 3617 -337", "0 42 0" );
			make_clip( "_tankstuck_rubblemiddle", "SI Players", 1, "-189 -145 0", "300 179 240", "6766 5299 -336", "0 -68 0" );
			make_clip( "_tankstuck_rubblefinal", "SI Players", 1, "-189 -253 0", "140 320 176", "8249 3222 -336", "0 -53 0" );
			make_clip( "_tankstuck_endalley1", "SI Players", 1, "-480 -855 0", "65 293 1400", "10303 3479 16" );
			make_clip( "_tankstuck_endalley2", "SI Players", 1, "-373 -96 0", "395 99 1400", "11637 5342 16" );
			make_clip( "_tankstuck_widestreet", "SI Players", 1, "-842 0 0", "386 555 1400", "10110 6784 8" );
			break;
		}
		case "c8m3_sewers":
		{
			make_clip( "_tankstuck_startrooftops", "SI Players", 1, "-1749 -380 0", "219 332 750", "12261 4212 464" );
			kill_funcinfclip( 606.217 );	// Delete clip behind a fence and inaccessible ladder
			kill_funcinfclip( 733.138 );	// Delete clip for end area backstreet wrongway sign left
			kill_funcinfclip( 762.564 );	// Delete clip for end area backstreet wrongway sign right
			break;
		}
		case "c9m2_lots":
		{
			make_clip( "_tankstuck_startback", "SI Players", 1, "-120 -900 0", "120 900 1370", "-835 -110 -223" );
			break;
		}
		case "c10m1_caves":
		{
			kill_funcinfclip( 1145.18 );	// Spawn left 1st
			kill_funcinfclip( 1127.68 );	// Spawn left 2nd
			kill_funcinfclip( 1110.42 );	// Spawn left 3rd
			kill_funcinfclip( 1059.74 );	// Spawn left 4th
			kill_funcinfclip( 1054.83 );	// Spawn left 5th
			kill_funcinfclip( 1216.59 );	// Tunnelside
			kill_funcinfclip( 1000.2 );	// Cliffside
			kill_funcinfclip( 597.979 );	// End cave connection to map 2 (easter egg spot)
			make_clip( "_tankstuck_tunnelside", "SI Players", 1, "-1700 -200 -1337", "1700 1024 610", "-13902 -10396 656", "0 45 0" );
			break;
		}
		case "c10m2_drainage":
		{
			bRemoveClips = true;
			break;
		}
		case "c10m3_ranchhouse":
		{
			kill_funcinfclip( 668.433 );	// Wrongway in beginning area
			kill_funcinfclip( 2248.58 );	// Treeline after bus/shed
			make_clip( "_tankstuck_cornerpath", "SI Players", 1, "-700 -800 -145", "800 790 1360", "-13598 -9362 68" );
			make_clip( "_tankstuck_mountainwedge", "SI Players", 1, "-100 -100 0", "100 100 1300", "-8388 -3525 300" );
			break;
		}
		case "c10m4_mainstreet":
		{
			make_clip( "_tankstuck_houserow", "SI Players", 1, "-2260 -440 0", "3450 900 1850", "68 158 -100" );
			make_clip( "_tankstuck_endbarricade", "SI Players", 1, "-400 -900 0", "400 900 1700", "3822 -3970 0" );
			make_clip( "_tankstuck_excesscorner", "SI Players", 1, "-200 -100 0", "200 100 1700", "-2520 -5048 -64" );
			break;
		}
		case "c11m1_greenhouse":
		{
			kill_funcinfclip( 1154.43 );	// End area fence barricade
			break;
		}
		case "c11m2_offices":
		{
			make_clip( "_tankstuck_fencenavarea", "SI Players", 1, "-64 -64 0", "64 64 1400", "10314 3862 16" );
			kill_funcinfclip( 1043.94 );	// Final street left barricade
			kill_funcinfclip( 1493.36 );	// Final street right barricade
			break;
		}
		case "c11m3_garage":
		{
			make_clip( "_tankstuck_startfence", "SI Players", 1, "-1200 -80 0", "720 58 1800", "-4947 -3786 16" );
			break;
		}
		case "c12m1_hilltop":
		{
			make_clip( "_tankstuck_wrongwayone", "SI Players", 1, "-320 -800 0", "0 750 1740", "-11967 -10090 450" );
			make_clip( "_tankstuck_wrongwaytwo", "SI Players", 1, "-610 0 0", "180 460 1740", "-11604 -9345 450" );
			break;
		}
		case "c12m2_traintunnel":
		{
			make_clip( "_tankstuck_tunnelend", "SI Players", 1, "-320 -800 -120", "450 258 240", "-1551 -10785 50" );
			break;
		}
		case "c12m3_bridge":
		{
			kill_funcinfclip( 311.003 );	// Wrongway at end of train tunnel
			break;
		}
		case "c12m4_barn":
		{
			make_clip( "_tankstuck_safeback", "SI Players", 1, "-170 -640 -100", "170 225 1700", "7704 -11710 425" );
			break;
		}
		default:
			break;
	}

	if ( bRemoveClips )
		EntFire( "func_playerinfected_clip", "Kill" );
}

Mutation20 - Healing Gnome


//-----------------------------------------------------
Msg("Activating Mutation 20\n");


DirectorOptions <-
{
	ActiveChallenge = 1

	weaponsToRemove =
	{
		weapon_first_aid_kit = 0
		weapon_pain_pills = 0
		weapon_adrenaline = 0
	}

	function AllowWeaponSpawn( classname )
	{
		if ( classname in weaponsToRemove )
		{
			return false;
		}
		return true;
	}

	// Challenge vars
	cm_TempHealthOnly = 1
	cm_AllowPillConversion = 0
	cm_HealingGnome = 1
	
	TempHealthDecayRate = 0.001
	function RecalculateHealthDecay()
	{
		if ( Director.HasAnySurvivorLeftSafeArea() )
		{
			TempHealthDecayRate = 0.27 // pain_pills_decay_rate default
		}
	}
}

function Update()
{
	DirectorOptions.RecalculateHealthDecay();
}

RocketDude

Technically, RocketDude was made by community members for the "The Last Stand" update, but listed as official because it is an official mutation.


//****************************************************************************************
//																						//
//									rocketDude.nut (mainfile)							//
//																						//
//****************************************************************************************

Msg("Activating RocketDude By ReneTM \n")

::rocketdude_version	<- "v1.7.7 build: 17:07:07 Feb 07 2021"
::mapName				<- Director.GetMapName().tolower()
::survivorSet			<- Director.GetSurvivorSet()
local grenadeData = {}




// Different scripts 2 include
// ----------------------------------------------------------------------------------------------------------------------------

IncludeScript("rocketdude/rd_debug")
IncludeScript("rocketdude/rd_utils")
IncludeScript("rocketdude/rd_melee_getter")
IncludeScript("rocketdude/rd_director")
IncludeScript("rocketdude/rd_damage_controll")
IncludeScript("rocketdude/rd_detonation_analysis")
IncludeScript("rocketdude/rd_meds")
IncludeScript("rocketdude/rd_events")
IncludeScript("rocketdude/rd_last_chance")
IncludeScript("rocketdude/rd_decals")
IncludeScript("rocketdude/rd_map_specifics")
IncludeScript("rocketdude/rd_custom_map_support")
IncludeScript("rocketdude/rd_saferoom_timer")
IncludeScript("rocketdude/rd_speedrun_mode")
IncludeScript("rocketdude/rd_mode_description")
IncludeScript("rocketdude/rd_hud_controller")




// Precache models and sounds
// ----------------------------------------------------------------------------------------------------------------------------

precacheSurvivorModels()

precacheRocketDudeModels()

precacheSounds()




// Creates a bullet time when the previous one is atleast 32 seconds ago.
// When this condition is met bullet time will occur with a probability of 3% 
// ----------------------------------------------------------------------------------------------------------------------------

local lastBulletTime = Time()
local lastDiceTime = Time()

::GLOBALS <-
{
	allowBulletTime = false
}

function bulletTime(){
	if(GLOBALS.allowBulletTime){
		if(Time() > lastBulletTime + 32){
			if(Time() >= lastDiceTime + 2){
				if(rollDice(3)){
					lastBulletTime = Time()
					DoEntFire( "!self", "Start", "", 0, timeScaler, timeScaler )
					DoEntFire( "!self", "Stop", "", 1, timeScaler, timeScaler )
				}
				lastDiceTime = Time()
			}
		}
	}
}

function saveGlobals(){
	SaveTable("GLOBAL_SAVINGS", GLOBALS)
}

function restoreGlobals(){
	RestoreTable("GLOBAL_SAVINGS", GLOBALS)
}




// When an instance of a grenade launcher projectile stops existing "doRocketJump" gets called
// ----------------------------------------------------------------------------------------------------------------------------

function grenadeExplodeEvent( impact ){
	foreach( player in getSurvivorsInRange( impact ) ){
		if(player.IsValid()){
			doRocketJump(impact, player)
		}
	}
}




// Returns an array of players within the blast radius
// ----------------------------------------------------------------------------------------------------------------------------

function getSurvivorsInRange(pos){
	local player = null
	while(player = Entities.FindByClassnameWithin(player, "player", pos, 160)){
		if(!player.IsDead()){
			if(!player.IsIncapacitated()){
				yield player
			}
		}
	}
}




// Compares current position of a projectile with the previous one. Force it to explode when it got stuck on prop_dynamic
// ----------------------------------------------------------------------------------------------------------------------------

function dynamicPropCheck(grenade){
	if(grenade in grenadeData){
		if(grenade.IsValid()){
			if((grenadeData[grenade] - grenade.GetOrigin()).Length() < 1){
				NetProps.SetPropInt(grenade, "m_takedamage", 1)
				grenade.TakeDamage(1337, 2, null)
			}	
		}
	}
}




// Get's fired 'OnGameplayStart' (usually after every single loadingscreen)
// ----------------------------------------------------------------------------------------------------------------------------

function OnGameplayStart(){
	
	/* CREATE MUSHROOMS */
	if(IsValveMap()){
		spawnMushrooms()
	}
	
	/* SPAWN MAP SIDED MUSHROOMS */
	spawnMapSidedMushrooms()
	
	/* SET PLAYERS MAX HEALTH AND CURRENT HEALTH TO 200 */
	setPlayersHealth()
	
	/* GENERAL SETTINGS */
	checkCvars()

	/* CREATE BULLET TIME */
	createBulletTimerEntity()

	/* KILL ALL DEATH CAMS */
	removeDeathFallCameras()
	
	/* CREATE THINK TIMER */
	createThinkTimer()

	/* RESTORE SETTINGS LIKE BULLET TIME 1/0 */
	restoreGlobals()
}

::fixEntitiesRemoved <- false

::killFixEntities <- function(){
	if(!fixEntitiesRemoved){
		EntFire( "anv_mapfixes*", "Kill" )
		EntFire( "env_player_blocker", "Kill" )
		EntFire( "rene_relay", "Trigger" )
		fixEntitiesRemoved = true
	}
}




// Called for every player within the detonation radius, it will launch the player in the calculated direction
// ----------------------------------------------------------------------------------------------------------------------------

function doRocketJump(detonationPos, player){

	local hitSurface = getSurfaceValue(detonationPos, player)
	local ignoreDistance = 160
	local playerEyes = player.EyePosition()
	local finalVector = null
	
	local detonationDistance = (detonationPos - playerEyes).Length()
	local playerIsMidAir = NetProps.GetPropInt(player, "m_hGroundEntity") & 1
	local midAirFactor = 0
	local boostdirection = (detonationPos - playerEyes) * - 1
	local eyeToSurface = ((detonationPos - playerEyes).Length())
	local distanceFactor = (160 - eyeToSurface) / 10
	
	// WALL OR FLOOR ?
	if(hitSurface == "WALL"){
		distanceFactor *= 1.5
	}else{
		distanceFactor *= 1.4
	}
	
	if(detonationDistance <= ignoreDistance){
		if(playerIsMidAir){
			midAirFactor = 1
			finalVector = (player.GetVelocity() + boostdirection * midAirFactor * distanceFactor) 
		}else{
			midAirFactor = 0.5
			finalVector = (player.GetVelocity() + boostdirection * midAirFactor * distanceFactor)
		}
		player.SetVelocity(finalVector)
	}
}




// 1. Save current origin of projectiles
// 2. Check if projectile is stuck to prop_dynamic
// 3. Projectiles from bots are ignored ( just in case )
// 4. If the player enabled auto bhop ( by using green mushroom ) use another projectile model and set it to glowing
// 5. Get rockets which travel in a straight line
// 6. Check for invalid rockets and fire grenadeExplodeEvent
// ----------------------------------------------------------------------------------------------------------------------------

local grenadeColor = GetColorInt(Vector(64,64,64))
local grenadeGlowColor = GetColorInt(Vector(255,16,16))

function ProjectileGenerator(){
	local proj = null
	while(proj = Entities.FindByClassname(proj, "grenade_launcher_projectile")){
		yield proj
	}
}

function GrenadeListener(){
	local ent = null
	foreach(ent in ProjectileGenerator()){
		dynamicPropCheck(ent)
		if(!IsPlayerABot(NetProps.GetPropEntity(ent, "m_hThrower"))){
			grenadeData[ent] <- ent.GetOrigin()
		}
	
		local scope = GetValidatedScriptScope(ent)
	
		// Projectile model and color
		if(!("modelChanged" in scope)){
			if(NetProps.GetPropEntity(ent, "m_hThrower") in bunnyPlayers){
				ent.SetModel("models/w_models/weapons/w_rd_grenade_scale_x4_burn.mdl")
			}else{
				ent.SetModel("models/w_models/weapons/w_rd_grenade_scale_x4.mdl")
			}
			NetProps.SetPropInt(ent, "m_clrRender", grenadeColor)
			scope["modelChanged"] <- true
		}

		if(!("creationTimestamp" in scope)){
			scope["creationTimestamp"] <- Time()
		}
	
		// Projectile Glow
		if(Time() > scope["creationTimestamp"] + 0.12){
			if(!("glowEnabled" in scope)){
				if(NetProps.GetPropEntity(ent, "m_hThrower") in bunnyPlayers){
					NetProps.SetPropInt(ent, "m_Glow.m_glowColorOverride", grenadeGlowColor)
					NetProps.SetPropInt(ent, "m_Glow.m_nGlowRangeMin", 32)
					NetProps.SetPropInt(ent, "m_Glow.m_nGlowRange", 8192)
					NetProps.SetPropInt(ent, "m_Glow.m_iGlowType", 3)
					NetProps.SetPropInt(ent, "m_Glow.m_bFlashing", 1)
					scope["glowEnabled"] <- true
				}
			}
		}
		
		// Let rockets travel in a straight line like in TF2
		// Another method would be to change the ent´s gravity.
		// But this would result in reflecting projectiles
		// NetProps.SetPropFloat(nade, "m_flGravity", 0.00000001)
		
		if(!("first_dir_vec" in scope)){
			scope["first_dir_vec"] <- ent.GetVelocity()
			ent.SetVelocity(scope["first_dir_vec"])
		}else{
			ent.SetVelocity(scope["first_dir_vec"])
		}
	}

	// Check if saved instances are still valid. Fire "grenadeExplodeEvent" when this is not the case anymore 
	foreach(grenade, origin in grenadeData){
		if(!grenade.IsValid()){
			grenadeExplodeEvent(origin)
			grenadeData.rawdelete(grenade)
		}
	}
}




// Disable player´s ledge hang, set his max health to 200 and disable fall damage crack
// ----------------------------------------------------------------------------------------------------------------------------

local PlayerSettingsCheckTime = Time()
function PlayerSettings(){
	if(Time() > PlayerSettingsCheckTime + 4){
		foreach(player in GetSurvivors()){
			DoEntFire("!self", "DisableLedgeHang", "", 0.0, player, player)
			DoEntFire("!self", "ignorefalldamagewithoutreset", "99999", 0.0, player, player)
			//
			if(NetProps.GetPropInt(player, "m_iMaxHealth") != 200){
				NetProps.SetPropInt(player, "m_iMaxHealth", 200)
			}
			PlayerSettingsCheckTime = Time()
		}
	}
}


function setPlayersHealth(){
	foreach(player in GetSurvivors()){
		NetProps.SetPropInt(player, "m_iMaxHealth", 200)
		NetProps.SetPropInt(player, "m_iHealth", 200)
	}
}




// While the player´s primary and secondary weapon still have infinite ammo we want to remove used items from player inventory
// Since players are able to receive throwables from mushrooms we need to check if any survivor is standing right on a mushroom
// ----------------------------------------------------------------------------------------------------------------------------

function projectileListener(){
	local projectiles = [ "vomitjar_projectile", "molotov_projectile", "pipe_bomb_projectile" ]
	foreach(projectile in projectiles){
		local throwable = null
		while(throwable = Entities.FindByClassname(throwable, projectile)){
			if(throwable.IsValid()){
				local scope = GetValidatedScriptScope(throwable)
				if(!("inv_edited" in scope)){
					local invTable = {}
					local player = NetProps.GetPropEntity(throwable, "m_hThrower")
					// Special case for c4m3_sugarmill_b ( mapsided projectiles which destroys stuff )
					if(player == null || player.GetClassname() == "pipe_bomb_projectile"){
						return
					}
					GetInvTable(player, invTable)
					if("slot2" in invTable){
						invTable.slot2.Kill()
						scope["inv_edited"] <- true
					}

					foreach(trigger in medkit_triggers){
						DoEntFire("!self", "TouchTest", "", 0.03, trigger, trigger)
					}
				}
			}
		}
	}
}




// Check if mushrooms can be reactivated
// ----------------------------------------------------------------------------------------------------------------------------

function updateMushroomTrigger(){
	foreach(trigger in medkit_triggers){
		
		local scope = GetValidatedScriptScope(trigger)

		if(Time() >= scope.usetime + scope.restoreTime){
			if(scope.usable == false){
				scope.usetime = Time() - scope.restoreTime
				setMedVisibility(1, scope.model)
				scope.usable = true
				DoEntFire("!self", "TouchTest", "", 0, trigger, trigger)
			}
		}
	}
}




// Hold space for auto-bhop ( if player used bhop mushroom )
// ----------------------------------------------------------------------------------------------------------------------------

::bunnyPlayers <- {}

function autobhop(){
	foreach(player,ent in bunnyPlayers){
		if(ent.IsValid()){
			if(!(NetProps.GetPropInt(ent, "m_fFlags") & 1) && NetProps.GetPropInt(ent, "movetype") == 2){
				if(ent.GetButtonMask() & 2){
					ent.OverrideFriction(0.033, 0)
				}
				NetProps.SetPropInt(ent, "m_afButtonDisabled", NetProps.GetPropInt(ent, "m_afButtonDisabled") | 2)
			}else{
				NetProps.SetPropInt(ent, "m_afButtonDisabled", NetProps.GetPropInt(ent, "m_afButtonDisabled") & ~2)
			}
		}else{
			bunnyPlayers.rawdelete(player)
		}
	}
}








// Get's fired every tick from a timer
// ----------------------------------------------------------------------------------------------------------------------------

function Think(){
	checkCvars()
	GrenadeListener()
	PlayerSettings()
	projectileListener()
	updateMushroomTrigger()
	autobhop()
	PlayerFunctions()
	lastChanceCountDown()
	tankrockListener()
	safeRoomTimer()
}

RocketDude Additional Scripts

These scripts are all included in the main RocketDude script.

rd_custom_map_support

//****************************************************************************************
//																						//
//								rd_custom_map_support.nut								//
//																						//
//****************************************************************************************




// Spawn mushrooms from map entities 
// ----------------------------------------------------------------------------------------------------------------------------

spawnMapSidedMushrooms <- function(){
	
	local mushroomTarget = null

	local types = [ "tiny", "small", "medium", "large", "bh", "exp", "item" ]
	
	foreach(type in types){
		while(mushroomTarget = Entities.FindByName(mushroomTarget, "rd_mushroom_" + type)){
			createRD_Medkit(mushroomTarget.GetOrigin(), mushroomTarget.GetAngles().ToKVString(), false, shroomProperties[type])
		}
	}
	
	foreach(type in types){
		while(mushroomTarget = Entities.FindByName(mushroomTarget, "rd_mushroom_" + type + "_rotating")){
			createRD_Medkit(mushroomTarget.GetOrigin(), mushroomTarget.GetAngles().ToKVString(), true, shroomProperties[type])
		}
	}
}




// Timers and stats
// ----------------------------------------------------------------------------------------------------------------------------

::resetPlayerStats <- function(){
	if(activator.IsValid()){
		if(activator in PlayerTimeData){
			if(activator.GetZombieType() == ZombieTypes.SURVIVOR){
				local prevBest = PlayerTimeData[activator].time_best
				PlayerTimeData[activator] <- { timerActive = false, finished = false, startTime = Time(), endTime = Time(), time_best = prevBest, ticks = 0, seconds = 0 }
				ClientPrint(activator, 5, BLUE + activator.GetPlayerName() + " | 00:00")
				EmitAmbientSoundOn("ui/littlereward.wav", 0.5, 100, 110, activator)
			}
		}
	}
}

::startTimer <- function(){
	if(activator.IsValid()){
		if(activator in PlayerTimeData){
			if(activator.GetZombieType() == ZombieTypes.SURVIVOR){
				resetPlayerStats()
				PlayerTimeData[activator].timerActive = true
			}
		}
	}
}

::mapFinished <- function(){
	
	if(PlayerTimeData[activator].finished == true){
		return
	}
	
	printFinalGroundTime(activator)
	PlayerTimeData[activator].finished = true
	resetPlayerStats()
	
	local playerSpawn = Entities.FindByName(null, "rd_player_start")
	if(playerSpawn != null){
		activator.SetOrigin(playerSpawn.GetOrigin())
	}else{
		ClientPrint(null, 5, "ERROR: ENTITY 'rd_player_start' NOT FOUND")
	}
}
rd_damage_controll

//****************************************************************************************
//																						//
//									rd_damage_controll.nut								//
//																						//
//****************************************************************************************



// Fix grenade launcher applying damage to the player on multiple ticks 
// ----------------------------------------------------------------------------------------------------------------------------

lastDamageTimes <- {}

function AllowGrenadeLauncherDamage(player){
	
	if(!(player in lastDamageTimes)){
		lastDamageTimes[player] <- Time() - 1
	}
	
	if( Time() > lastDamageTimes[player] + 0.06 ){
			lastDamageTimes[player] <- Time()
			return true
	}else{
		lastDamageTimes[player] <- Time()
		return false
	}
}




// When to allow damage
// ----------------------------------------------------------------------------------------------------------------------------

function AllowTakeDamage(damageTable){
	local damageType = damageTable["DamageType"]
	local attacker = damageTable["Attacker"]
	local victim = damageTable["Victim"]
	local damageDone = damageTable["DamageDone"]
	
	// TANK ROCK
	if(damageTable.Victim.GetClassname() == "tank_rock"){
		if(attacker != null && attacker.IsPlayer() && attacker.GetZombieType() == 9){
			
			local victimScope = GetValidatedScriptScope(victim)

			if(!("dealtDamage" in victim.GetScriptScope())){
				victimScope["dealtDamage"] <- damageDone
			}else{
				victimScope["dealtDamage"] += damageDone
			}
			if(victimScope["dealtDamage"] >= 50){
				victimScope["dealtDamage"] <- -500
				if(attacker.IsIncapacitated()){
					if(!missionFailed){
						if(last_chance_active){
							stopLastChanceMode()
							attacker.ReviveFromIncap()
						}else{
							if(!allSurvivorsIncap()){
								attacker.ReviveFromIncap()
							}else{
								ClientPrint(null, 5, BLUE + "Time to say goodbye")
							}
						}
						EmitAmbientSoundOn("player/orch_hit_csharp_short", 1, 100, 100, attacker);	
					}
				}else{
					if((attacker.GetHealth() + 5) <= 200){
							attacker.SetHealth(attacker.GetHealth() + 5)
					}else{
						attacker.SetHealth(200)
					}
					attacker.UseAdrenaline(1)
				}
			}
		}
	return true
	}


	// DISABLE FALL DAMAGE
	if(damageType == 32){
		return false
	}
	
	// DISABLE FF
	if(attacker.IsPlayer()){
		if(victim.IsPlayer()){
			if(attacker.GetZombieType() == 9 && victim.GetZombieType() == 9){
				if(IsPlayerABot(attacker)){
					return false
				}
				
				if(damageTable.Weapon == null){
					if(!AllowGrenadeLauncherDamage(victim)){
						return false
					}else{
						damageTable.DamageDone = 1
						return true
					}
				}
			}
		}
	}
	return true
}
rd_debug

//****************************************************************************************
//																						//
//										rd_debug.nut									//
//																						//
//****************************************************************************************

// Check if dev mode is active
// ----------------------------------------------------------------------------------------------------------------------------

::DevModeActive <- function(){
	return Convars.GetFloat("developer")
}




// Switch from mushroom to mushroom
// ----------------------------------------------------------------------------------------------------------------------------

::debugMushroom <- null;

::GoToNextMushroom <- function(ent){
	if(!DevModeActive()){
		return
	}
	
	if(debugMushroom = Entities.FindByModel(debugMushroom, "models/props_collectables/mushrooms_glowing.mdl")){
		NetProps.SetPropInt(ent, "m_MoveType", 8)
		ent.SetOrigin(debugMushroom.GetOrigin() + Vector(0,0,64))
	}else{
		NetProps.SetPropInt(ent, "m_MoveType", 2)
	}
}
rd_decals

//****************************************************************************************
//																						//
//										rd_decals.nut									//
//																						//
//****************************************************************************************




// Will place decals depending on map
// ----------------------------------------------------------------------------------------------------------------------------

::placeRocketDudeDecals <- function(){
	local texture = "decals/rocketdude/rd_logo_glow"
	
	if(mapName in RocketDudeDecals){
		foreach(pos in RocketDudeDecals[mapName]){
			if(mapName == "c14m1_junkyard" || mapName == "c14m2_lighthouse"){
				texture = "decals/rocketdude/rd_logo_glow_big"
			}
			applyDecalAt(pos, texture)
		}
	}
}




// Applies a decal with texture X on position Y
// ----------------------------------------------------------------------------------------------------------------------------

::applyDecalAt <- function (pos, tex){
	local decal = SpawnEntityFromTable( "infodecal", { targetname = "rd_decal", texture = tex, LowPriority = 0, origin = pos } )
	DoEntFire( "!self", "Activate", "", 0.0, decal, decal )
}




// Decal data for maps c1 - c14
// ----------------------------------------------------------------------------------------------------------------------------

::RocketDudeDecals <-
{
	c1m1_hotel			= [ Vector(2064.64,6783.97,2882) ]
	c1m2_streets		= [ Vector(-1174.98,728.091,912.947) ]
	c1m3_mall			= [ Vector(1776.31,-2015.99,642.575) ]
	c1m4_atrium			= [ Vector(-3927.97,-3403.11,856.501) ]

	c2m1_highway		= [ Vector(1338.82,-1736.51,-1087.55) ]
	c2m2_fairgrounds	= [ Vector(-495.969,-1238.1,187.207) ]
	c2m3_coaster		= [ Vector(303.034,3328.03,349.983) ]
	c2m4_barns 			= [ Vector(3717.22,576.031,-127.915) ]
	c2m5_concert 		= [ Vector(-4368.03,2687,236.19) ]

	c3m1_plankcountry 	= [ Vector(-3712,4349.86,-74.6688) ]
	c3m2_swamp 			= [ Vector(-3923.97,4650.4,85.4511) ]
	c3m3_shantytown 	= [ Vector(-5602.41,-1635.03,258.268) ]
	c3m4_plantation 	= [ Vector(1853.52,76.6953,600.031) ]

	c4m1_milltown_a 	= [ Vector(1643.17,4338.16,433.805) ]
	c4m2_sugarmill_a 	= [ Vector(2966.24,-3271.43,1200.15) ]
	c4m3_sugarmill_b 	= [ Vector(1424.04,-6088.03,416.549) ]
	c4m4_milltown_b 	= [ Vector(1437.01,6364.88,334.595) ]
	c4m5_milltown_escape = [ Vector(-5874.37,8072.03,377.766) ]

	c5m1_waterfront		= [ Vector(-2144.03,-2508.57,-334.026) ]
	c5m2_park			= [ Vector(-10102.6,-5538.72,48.0313) ]
	c5m3_cemetery		= [ Vector(3511.2,3503.97,258.566) ]
	c5m4_quarter		= [ Vector(-2060.97,3392.03,132.305) ]
	c5m5_bridge			= [ Vector(14288.3,6326.82,790.031) ]

	c6m1_riverbank		= [ Vector(1301.35,1616.03,578.07) ]
	c6m2_bedlam			= [ Vector(3964.55,4096.03,-549.714) ]
	c6m3_port			= [ Vector(-1258.88,-307.692,480.031) ]

	c7m1_docks			= [ Vector(2835.56,901.987,448.031) ]
	c7m2_barge 			= [ Vector(-10349.8,415.969,341.246) ]
	c7m3_port 			= [ Vector(-2223.97,-537.898,-32.6826) ]

	c8m1_apartment 		= [ Vector(1710.64,4568.03,264.95) ]
	c8m2_subway 		= [ Vector(9519.12,3772.03,71.1739) ]
	c8m3_sewers 		= [ Vector(15280,15322.9,72.8583) ]
	c8m4_interior 		= [ Vector(13064,14173,632.224) ]
	c8m5_rooftop		= [ Vector(7291.74,8976.03,470.528) ]

	c9m1_alleys			= [ Vector(1466.98,-1663.97,-161.193) ]
	c9m2_lots			= [ Vector(4135.19,1263.97,252.13) ]

	c10m1_caves			= [ Vector(-12608,-9505.11,27.5156) ]
	c10m2_drainage		= [ Vector(-9007.97,-9040.36,-377.031) ]
	c10m3_ranchhouse	= [ Vector(-9690.9,-7808.03,545.451) ]
	c10m4_mainstreet	= [ Vector(-1181.2,-4732.03,253.471) ]
	c10m5_houseboat		= [ Vector(2239.22,4086.55,72.0313) ]

	c11m1_greenhouse	= [ Vector(3248.03,961.976,69.2323) ]
	c11m2_offices		= [ Vector(5168.03,137.882,69.3103) ]
	c11m3_garage		= [ Vector(-2448.03,1375.55,217.825) ]
	c11m4_terminal		= [ Vector(1992.03,3128.09,513.913) ]
	c11m5_runway		= [ Vector(-7215.97,14444.1,623.558) ]

	c12m1_hilltop		= [ Vector(-7670.79,-7375.85,1478.86) ] 
	c12m2_traintunnel	= [ Vector(-7996.38,-7685.76,1329.54) ] 
	c12m3_bridge		= [ Vector(1828.55,-12166.7,484.031) ]
	c12m4_barn			= [ Vector(9825.65,-6235.72,861.689) ] 
	c12m5_cornfield		= [ Vector(4736,1515.6,362.583) ]

	c13m1_alpinecreek	= [ Vector(-3956.75,3989.21,461.736) ]
	c13m2_southpinestream = [ Vector(1329.92,993.69,170.069) ]
	c13m3_memorialbridge = [ Vector(6804.03,-4099.88,1720.87) ] 
	c13m4_cutthroatcreek = [ Vector(-731.623,1533.44,-48.6967) ] 

	c14m1_junkyard 		= [ Vector(-3616.64,-9215.38,165.2) ]
	c14m2_lighthouse	= [ Vector(340.733,-240.909,394.865) ] 

}
rd_detonation_analysis

//****************************************************************************************
//																						//
//								rd_detonation_analysis.nut								//
//																						//
//****************************************************************************************




// Distance between player eyepos and wall < distance between player eyepos to floor/ceiling...
// Thats why we want to have a higher value when the hit surface was a wall.
// ----------------------------------------------------------------------------------------------------------------------------

function getSurfaceValue(detonationPos,player){
	
	local traceTableX1 = { start = detonationPos+Vector(2,0,0), end = detonationPos+Vector(-2,0,0), ignore = player, mask = TRACE_MASK_PLAYER_SOLID }
	local traceTableX2 = { start = detonationPos+Vector(-2,0,0), end = detonationPos+Vector(2,0,0), ignore = player, mask = TRACE_MASK_PLAYER_SOLID }

	local traceTableY1 = { start = detonationPos+Vector(0,2,0), end = detonationPos+Vector(0,-2,0), ignore = player, mask = TRACE_MASK_PLAYER_SOLID }
	local traceTableY2 = { start = detonationPos+Vector(0,-2,0), end = detonationPos+Vector(0,2,0), ignore = player, mask = TRACE_MASK_PLAYER_SOLID }

	local traceTableZ1 = { start = detonationPos+Vector(0,0,2), end = detonationPos+Vector(0,0,-2), ignore = player, mask = TRACE_MASK_PLAYER_SOLID }
	local traceTableZ2 = { start = detonationPos+Vector(0,0,-2), end = detonationPos+Vector(0,0,2), ignore = player, mask = TRACE_MASK_PLAYER_SOLID }

	TraceLine(traceTableX1)
	TraceLine(traceTableX2)
	TraceLine(traceTableY1)
	TraceLine(traceTableY2)
	TraceLine(traceTableZ1)
	TraceLine(traceTableZ2)

	// WALL
	if("hit" in traceTableX1 && traceTableX1.hit == true ||  "hit" in traceTableX2 && traceTableX2.hit == true ){
		return "WALL"
	}
	// WALL
	else if("hit" in traceTableY1 && traceTableY1.hit == true ||  "hit" in traceTableY2 && traceTableY2.hit == true ){
		return "WALL"
	}
	// CEILING/FLOOR
	else if("hit" in traceTableZ1 && traceTableZ1.hit == true ||  "hit" in traceTableZ2 && traceTableZ2.hit == true ){
		return "CLOOR"
	}else{
		return "WALL" // Just in case...
	}
}
rd_director

//****************************************************************************************
//																						//
//										rd_director.nut									//
//																						//
//****************************************************************************************




MutationOptions <-
{
	// General
	cm_NoSurvivorBots	= 1
	
	// Special Infected
	MaxSpecials			= 6

	// Convert items
	weaponsToConvert =
	{
		weapon_first_aid_kit =	"weapon_pain_pills_spawn"
	}

	function ConvertWeaponSpawn(classname){
		if (classname in weaponsToConvert){
			return weaponsToConvert[classname]
		}
		return 0
	}	
	
	
	// Controll which weapons are allowed to be spawned
	weaponsToPreserve =
	{
		weapon_pain_pills		= 0
		weapon_adrenaline		= 0
		weapon_melee			= 0
		weapon_first_aid_kit	= 0
		weapon_gascan			= 0
		weapon_pistol_magnum	= 0
		weapon_grenade_launcher = 0
	}

	function AllowWeaponSpawn(classname){
		if(!IsValveMap()){
			if(classname in weaponsToPreserve){
				return true;
			}
		}
		
		if (classname in weaponsToPreserve){
			return true;
		}
		return false;
	}
	
	// Avoid fallen survivors carrying items
	function AllowFallenSurvivorItem(item){
		return false
	}
	
	// Get default items for survivors
	DefaultItems =
	[
		"weapon_grenade_launcher",
		RandomInt(0, 1) ? getAvailableMelee("Sharp") : "weapon_pistol_magnum"
	]

	function GetDefaultItem( idx ){
		if ( idx < DefaultItems.len() ){
			return DefaultItems[idx]
		}
		return 0
	}
}
rd_events

//****************************************************************************************
//																						//
//										rd_events.nut									//
//																						//
//****************************************************************************************




::devs <- {}
	
devs["STEAM_1:0:26359107"] <- { name = "ReneTM", role = "creator" }
devs["STEAM_1:0:16327272"] <- { name = "Derdoron", role = "Beta Tester" }




// Chat commands
// ----------------------------------------------------------------------------------------------------------------------------

function OnGameEvent_player_say(params){

	local text,ent = null
	
	if("userid" in params && params.userid == 0){
		return
	}
	
	text = strip(params["text"].tolower())
	ent = GetPlayerFromUserID(params["userid"])

	if(text.len() < 1){
		return
	}
	
	local steamID = ent.GetNetworkIDString()
	local scope = GetValidatedScriptScope(ent)

	switch(text){

		case "!version" :
		if(steamID in devs){
			ClientPrint(null, 5, BLUE + "RocketDude " + rocketdude_version)
		}
		break
		
		case "!countdown" :
		startSafeRoomTimer(ent)
		break
		
		case "!r" :
		restartFromSaferoom(ent)
		break
		
		case "!saveangles" :
		savePlayerEyeAngles(ent)
		break
		
		case "!speedrunmode" :
		speedrunModeToggle(ent)
		break
		
		case "!hud" :
		ChangeHudState(ent)
		break
		
		case "!stats" :
		outputStats(ent)
		break
		
		case "!info" :
		printMutationInfo(ent)
		break
		
		case "!g2mushroom" :
		GoToNextMushroom(ent)
		break
	}
}




function OnGameEvent_player_first_spawn(params){
	
	local orangestar = ORANGE + "★"

	if(params["isbot"] == 0){
		local player = GetPlayerFromUserID(params.userid)
		local steamID = player.GetNetworkIDString()

		if(steamID in devs){
			local invTable = {}
			GetInvTable(player, invTable)
			if("slot1" in invTable && invTable.slot1.GetClassname() == "weapon_melee"){
				if(NetProps.GetPropString(invTable.slot1, "m_strMapSetScriptName") != "crowbar"){
					invTable.slot1.Kill()
					player.GiveItemWithSkin("crowbar", 1)
				}else{
					NetProps.SetPropInt(invTable.slot1, "m_nSkin", 1)
				}
			}
			ClientPrint(null, 5, orangestar + GREEN + " RocketDude " + devs[steamID].role + BLUE + " " + player.GetPlayerName() + WHITE + " joined the game.")
		}
	}
}




// "The right man in the wrong place can make all the difference in the world"
// ----------------------------------------------------------------------------------------------------------------------------

function OnGameEvent_player_changename(params){
	local player = GetPlayerFromUserID(params["userid"])
	local invTable = {}
	GetInvTable(player, invTable)
	
	if("newname" in params && params["newname"] == "Dr. Gordon Freeman"){
		if("slot1" in invTable && invTable.slot1.GetClassname() == "weapon_melee"){
			if(NetProps.GetPropString(invTable.slot1, "m_strMapSetScriptName") == "crowbar"){
				NetProps.SetPropInt(invTable.slot1, "m_nSkin", 1)
			}
		}
	}
}




function OnGameEvent_player_spawn(params){

	local player = GetPlayerFromUserID(params["userid"])
	
	if(player.GetZombieType() == 9 && !IsPlayerABot(player)){
		placeRocketDudeDecals()
		teleportToSurvivor(player)
		
		DoEntFire("!self", "DisableLedgeHang", "", 0.0, player, player)
		DoEntFire("!self", "ignorefalldamagewithoutreset", "99999", 0.0, player, player)

		if(NetProps.GetPropInt(player, "m_iMaxHealth") != 200){
			NetProps.SetPropInt(player, "m_iMaxHealth", 200)
		}
	}
}




function teleportToSurvivor(player){
	local target = getClosestSurvivorTo(player)
	
	if(target == null){
		return
	}
	
	if((player.GetOrigin() - target.GetOrigin()).Length() < 256){
		return
	}
	
	if(target.IsValid()){
		if(!target.IsDead() && !target.IsDying()){
			if(Director.HasAnySurvivorLeftSafeArea()){
				player.SetOrigin(target.GetOrigin())
			}
		}
	}
}




function OnGameEvent_player_incapacitated(params){
	
	if("attackerentid" in params){
		if(EntIndexToHScript(params.attackerentid).GetClassname() == "trigger_hurt"){
			return
		}
	}

	if(!lastChanceUsed){
		lastChanceSwitch(params)
	}
}




// When a survivor stands full hp in a mushroom trigger volume the function is unlocked
// again and the survivor gets hurt a touchtest should be done
// ----------------------------------------------------------------------------------------------------------------------------

function OnGameEvent_player_hurt(params){
	if(GetPlayerFromUserID(params.userid).GetZombieType() == 9){
		foreach(trigger in medkit_triggers){
			DoEntFire("!self", "TouchTest", "", 0, trigger, trigger)
		}
	}
}




// Disable glows when survivors enter "last chance mode" but fail
// ----------------------------------------------------------------------------------------------------------------------------

function OnGameEvent_mission_lost(params){
	disableInfectedGlows()
}




// Called when any player dies. Purpose mostly for bullet time, reviving survivors from being incap and "last chance"
// ----------------------------------------------------------------------------------------------------------------------------

function OnGameEvent_player_death(params){
	
	local victim = null
	local attacker = null
	local victimClass = null
	
	if("userid" in params){
		victim = GetPlayerFromUserID(params["userid"])
	} else if("entityid" in params){
		victim = EntIndexToHScript(params["entityid"])
	}
	if("attacker" in params){
		attacker = GetPlayerFromUserID(params["attacker"])
	} else if("attackerentid" in params){
		attacker = EntIndexToHScript(params["attackerentid"])
	}
	
	if(!lastChanceUsed){
		lastChanceSwitch(params)	
	}
	
	if(victim.IsPlayer() && victim.GetZombieType() == 9){
		ClientPrint(null, 5, BLUE + victim.GetPlayerName() + WHITE + " did not finish this map. The map finished them.")
	}
	

	if(victim.GetClassname() != "infected"){
		if(victim.GetClassname() == "witch" || victim.GetZombieType() != 9){	// Killed witch or any Special infected or tank
			if(attacker != null && attacker.IsPlayer()){						// Dont do anything when the map is the killer
				if(attacker.GetZombieType() == 9){
					if(attacker.IsIncapacitated()){
						if(!missionFailed){
							if(last_chance_active){
								stopLastChanceMode()
								attacker.ReviveFromIncap()
							}else{
								if(!allSurvivorsIncap()){
									attacker.ReviveFromIncap()
								}else{
									ClientPrint(null, 5, BLUE + "Time to say goodbye")
								}
							}
							EmitAmbientSoundOn("player/orch_hit_csharp_short", 1, 100, 100, attacker)
						}
					}
				}
			}
		}
	}else{
		if(attacker != null && attacker.GetClassname() == "player" && attacker.GetZombieType() == 9){
			bulletTime()
		}
	}
}




// Set tank's health in relation to the current difficulty
// ----------------------------------------------------------------------------------------------------------------------------

function OnGameEvent_tank_spawn(params){
	local tank = EntIndexToHScript(params.tankid)
	local health = 0
	switch(Convars.GetStr("z_difficulty").tolower()){
		case "easy" :
			health = 8000;	break
		case "normal" :
			health = 16000;	break
		case "hard" :
			health = 32000;	break
		case "impossible" :
			health = 64000;	break
	}
	tank.SetMaxHealth(health)
	tank.SetHealth(health)
}




// Set witch health in relation to the current difficulty
// ----------------------------------------------------------------------------------------------------------------------------

function OnGameEvent_witch_spawn(params){
	local witch = EntIndexToHScript(params.witchid)
	local health = 0
	
	switch(Convars.GetStr("z_difficulty").tolower()){
		case "easy" :
			health = 2048;	break
		case "normal" :
			health = 4096;	break
		case "hard" :
			health = 8192;	break
		case "impossible" :
			health = 16384; break
	}
	witch.SetMaxHealth(health)
	witch.SetHealth(health)
}




// Avoid multiple rocketlaunchers on the ground 
// ----------------------------------------------------------------------------------------------------------------------------

function OnGameEvent_item_pickup(params){
	local player = GetPlayerFromUserID(params["userid"])
	local playerInv = {}
	if(player.GetZombieType() == 9 && !IsPlayerABot(player)){
		GetInvTable(player, playerInv)
		if("slot0" in playerInv){
			if(playerInv["slot0"].GetClassname() != "weapon_grenade_launcher"){
				playerInv["slot0"].Kill()
				player.GiveItem("weapon_grenade_launcher")
			}
		}
		
		// Gives devs a golden crowbar
		if(player.GetNetworkIDString() in devs){
			if("slot1" in playerInv){
				if(NetProps.GetPropString(playerInv.slot1, "m_strMapSetScriptName") == "crowbar"){
					NetProps.SetPropInt(playerInv.slot1, "m_nSkin", 1)
				}
			}
		}
	}
}




function OnGameEvent_weapon_drop(params){
	if("propid" in params){
		local droppedItem = EntIndexToHScript(params.propid)
		if(droppedItem.GetClassname() == "weapon_grenade_launcher"){
			droppedItem.Kill()
		}
	}
}

::entityChangesDone <- false

function OnGameEvent_player_left_checkpoint(params){
	if("userid" in params){
		local player = GetPlayerFromUserID(params["userid"])
		if(player.GetZombieType() == 9 && !IsPlayerABot(player) && !player.IsDead()){
			if(player in PlayerTimeData){
				if(!PlayerTimeData[player].finished){
					PlayerTimeData[player].timerActive = true
					PlayerTimeData[player].startTime = Time()
					PlayerTimeData[player].seconds = 0
					PlayerTimeData[player].ticks = 0
					EmitAmbientSoundOn("ui/beep07.wav", 0.5, 100, 107, player)
					ClientPrint(player, 5, BLUE + player.GetPlayerName() + " | 00:00")
				}
			}
		
			if(!IsPlayerABot(player)){
				if(!entityChangesDone){
					DoEntFire("worldspawn", "RunScriptCode", "killFixEntities()", 2, player, player)
					DoEntFire("worldspawn", "RunScriptCode", "mapSpecifics()", 4, player, player)
					entityChangesDone = true
				}
			}

			DoEntFire("!self", "DisableLedgeHang", "", 0.0, player, player)
			DoEntFire("!self", "ignorefalldamagewithoutreset", "99999", 0.0, player, player)

			if(NetProps.GetPropInt(player, "m_iMaxHealth") != 200){
				NetProps.SetPropInt(player, "m_iMaxHealth", 200)
			}
		}
	}
}




// Like in Portal 1 we want to challange the player to do as less steps as possible or atleast to spend as less time on the 
// ground as possible. Event "player_footstep" is non-functional atm...Valve please fix
// ----------------------------------------------------------------------------------------------------------------------------

function OnGameEvent_finale_vehicle_leaving(param){
	finalGroundTimeOutput()
}




function OnGameEvent_finale_win(param){
	finalGroundTimeOutput()
}




function finalGroundTimeOutput(){
	foreach(player,datatable in PlayerTimeData){
		if(player.IsValid()){
			if(!player.IsDead() && !player.IsDying() && !player.IsIncapacitated()){
				if(!PlayerTimeData[player].finished){
					printFinalGroundTime(player)
					ProcessSurvivorTime(player)
				}
			}
		}
	}
}




function ProcessSurvivorTime(ent){
	
	local tTable = PlayerTimeData[ent]
	tTable.endTime = Time()
	
	if(tTable.time_best == 0){
		tTable.time_best = tTable.endTime - tTable.startTime
	}else{
		if((tTable.endTime - tTable.startTime) < tTable.time_best){
			tTable.time_best = (tTable.endTime - tTable.startTime)
		}
	}
	tTable.finished = true
}




::printFinalGroundTime <- function(ent){
	if(!ent.IsDead() && !ent.IsDying() && !ent.IsIncapacitated()){
		local sec = PlayerTimeData[ent].seconds
		local fracs = PlayerTimeData[ent].ticks.tofloat()
		if(fracs > 0){
			fracs = (fracs / 30)
		}
		local groundTime = sec + fracs
		local time_curr = limitDecimalPlaces( Time() - (PlayerTimeData[ent].startTime).tofloat() )
		local midAirPercent = getMidAirPercentage(groundTime, time_curr)
		
		ClientPrint(null, 5, BLUE + ent.GetPlayerName() + WHITE + " finished this map in " + BLUE + time_curr + WHITE + " seconds and spent " + BLUE + midAirPercent + WHITE + " % midair")
		EmitAmbientSoundOn("ui/menu_invalid.wav", 0.75, 100, 110, ent)
		
		local diff = GetSpeedrunStats(ent, time_curr)

		// Speedrun tracker
		
		if( diff != null ){
			if( diff < 0){
				ClientPrint(null, 5, WHITE + "Thats a new personal record for " + mapName + GREEN + " ( "  + diff.tostring() + " seconds )")
				return
			}else if(diff > 0){
				ClientPrint(null, 5, ORANGE + "( +" + ( diff.tostring()) + " seconds )" )
				return
			}
		}
	}
}




::getMidAirPercentage <- function(groundTime, time_curr){
	local airTime = time_curr - groundTime
	local airPercentage = (( airTime / time_curr ) * 100).tofloat()
	return limitDecimalPlaces(airPercentage)
}




::limitDecimalPlaces <- function(var){
	return (var * 100).tointeger() / 100.0
}




// Check if the text from the file is a "number"
// ----------------------------------------------------------------------------------------------------------------------------

::isNumeric <- function(value){
	local newValue
	local dots = 0
	local numbers = ["0","1","2","3","4","5","6","7","8","9","."]
	for(local i=0; i < value.len(); i++){
		local checkChar = value.slice(i, i+1)
		if(checkChar == "."){
			dots++
		}
		if(dots > 1){
			return false
		}
		if(numbers.find(checkChar) == null){
			return false
		}
	}
	return true
}




// Record output for the local player
// ----------------------------------------------------------------------------------------------------------------------------

::GetSpeedrunStats <- function(player, newTime){
	
	// Restrict saving for the local player
	if(!(player == GetListenServerHost())){
		return null
	}
	
	local filePath = "rocketdude/speedrun/"
	local fileName = mapName + ".txt"
	
	local savedTime = FileToString(filePath + fileName)
	
	// Save a file when there is none
	if(savedTime == null || savedTime.len() == 0 || !isNumeric(savedTime)){
		StringToFile(filePath + fileName, newTime.tostring() )
		return null
	}
	
	try{
		savedTime = savedTime.tofloat()
	}catch(exception){
		return null
	}
	
	if(newTime < savedTime){
		StringToFile(filePath + fileName, newTime.tostring() )
	}
	
	return (newTime - savedTime)
}




// Typing sv_cheats 1 on local server would result in every cheat flagged variable reset
// ----------------------------------------------------------------------------------------------------------------------------

function OnGameEvent_server_cvar(param){
	if("cvarname" in param){
		local cvar = param.cvarname
		if(cvar == "sv_cheats"){
			checkCvars()
		}
	}
}




__CollectEventCallbacks(this, "OnGameEvent_", "GameEventCallbacks", RegisterScriptGameEventListener)
rd_hud_controller

//****************************************************************************************
//																						//
//									rd_hud_controller.nut								//
//																						//
//****************************************************************************************




// Flag and position tables
// ----------------------------------------------------------------------------------------------------------------------------

::HUDFlags <- {
	PRESTR = 1
	POSTSTR = 2
	BEEP = 4
	BLINK = 8
	AS_TIME = 16
	COUNTDOWN_WARN = 32
	NOBG = 64
	ALLOWNEGTIMER = 128
	ALIGN_LEFT = 256
	ALIGN_CENTER = 512
	ALIGN_RIGHT = 768
	TEAM_SURVIVORS = 1024
	TEAM_INFECTED = 2048
	TEAM_MASK = 3072
	NOTVISIBLE = 16384
}

::HUDPositions <- {
	LEFT_TOP = 0
	LEFT_BOT = 1
	MID_TOP = 2
	MID_BOT = 3
	RIGHT_TOP = 4
	RIGHT_BOT = 5
	TICKER = 6
	FAR_LEFT = 7
	FAR_RIGHT = 8
	MID_BOX = 9
	SCORE_TITLE = 10
	SCORE_1 = 11
	SCORE_2 = 12
	SCORE_3 = 13
	SCORE_4 = 14
}




// Main hud definition
// ----------------------------------------------------------------------------------------------------------------------------

RD_HUD <-
{
	Fields = 
	{
		timer = { slot = HUDPositions.MID_BOX, flags = HUDFlags.ALIGN_CENTER | HUDFlags.NOBG, name = "timer", datafunc = @()GetPlayerTimes() }
	}
}




// Returns one string for all survivors
// ----------------------------------------------------------------------------------------------------------------------------

::GetPlayerTimes <- function(){
	local str = ""
	
	foreach(ent in GetHumanSurvivors()){
		str += (GetCharacterDisplayName(ent) + ": ")
		if(ent in PlayerTimeData){
			if(!ent.IsDead() && !ent.IsDying()){
				if(PlayerTimeData[ent].timerActive){
					if(!PlayerTimeData[ent].finished){
						str += g_MapScript.TimeToDisplayString(Time() - PlayerTimeData[ent].startTime)
					}else{
						str += "Finished(" + g_MapScript.TimeToDisplayString(PlayerTimeData[ent].endTime - PlayerTimeData[ent].startTime) + ")"
					}
				}else{
					str += "0:00" 
				}
			}else{
				str += "(Dead)"
			}
		}
		str += "  "
	}
	return str
}




// Voting to disable the timer hud
// ----------------------------------------------------------------------------------------------------------------------------

function ChangeHudState(ent){
	local scope = GetValidatedScriptScope(ent)
	scope["speedrun_timer"] <- true
	
	foreach(player in GetHumanSurvivors()){
		local playerscope = GetValidatedScriptScope(player)
		if(!("speedrun_timer" in playerscope)){
			ClientPrint(null, 5, GREEN + ent.GetPlayerName() + WHITE + " voted to " + ( IsTimerHudActive() ? "disable" : "enable" ) + " the timer hud")
			return
		}
	}
	
	if(IsTimerHudActive()){
		DisableTimerHud()
	}else{
		EnableTimerHud()
	}
	
	foreach(player in GetHumanSurvivors()){
		local scope = GetValidatedScriptScope(player)
		if("speedrun_timer" in scope){
			player.GetScriptScope().rawdelete("speedrun_timer")
		}
	}
	
	ClientPrint(null, 5, WHITE + "Hud has been " + GREEN + ( IsTimerHudActive() ? "enabled" : "disabled") )
}




// Enable / disable the hud
// ----------------------------------------------------------------------------------------------------------------------------

function EnableTimerHud(){
	RD_HUD.Fields.timer.flags <- RD_HUD.Fields.timer.flags & ~HUDFlags.NOTVISIBLE
	HUDPlace(HUDPositions.MID_BOX, 0.0, 0.0, 1.0, 0.05)
}




function EnableTickerHud(){
	Ticker_AddToHud(RD_HUD, "!info in chat to print mutation details to your console", true)
	HUDPlace(HUDPositions.TICKER, 0.0, 0.0, 0.99, 0.25)
	Ticker_SetTimeout(16)
	Ticker_SetBlinkTime(16)
	RD_HUD.Fields.ticker.flags <- HUDFlags.ALIGN_CENTER | HUDFlags.NOBG | HUDFlags.BLINK
}




function DisableTimerHud(){
	RD_HUD.Fields.timer.flags <- RD_HUD.Fields.timer.flags | HUDFlags.NOTVISIBLE
}




// Checks if the timer hud is currently visible
// ----------------------------------------------------------------------------------------------------------------------------

function IsTimerHudActive(){
	return !(RD_HUD.Fields.timer.flags & HUDFlags.NOTVISIBLE)
}


EnableTimerHud()

EnableTickerHud()

HUDSetLayout(RD_HUD)
rd_last_chance

//****************************************************************************************
//																						//
//									rd_last_chance.nut									//
//																						//
//****************************************************************************************




// Basic functions to check if survivors are dead / capped
// ----------------------------------------------------------------------------------------------------------------------------

function allSurvivorsIncap(){
	foreach(survivor in GetSurvivors()){
		//if(!survivor.IsIncapacitated() && !survivor.IsHangingFromLedge() && !survivor.IsDead() && !survivor.IsDying() && !playerIsCapped(survivor)){  // 20200913
		if(!survivor.IsIncapacitated() && !survivor.IsDead() && !survivor.IsDying()){
			return false
		}
	}
	return true
}

function allSurvivorsDead(){
	foreach(survivor in GetSurvivors()){
		if(!survivor.IsDead() && !survivor.IsHangingFromLedge()){
			return false
		}
	}
	return true
}

function playerIsCapped(player){
	local infected = null;
	local keys = ["m_pummelVictim","m_carryVictim","m_pounceVictim","m_jockeyVictim","m_tongueVictim"]
	while(infected = Entities.FindByClassname(infected, "player")){
		if(infected.GetZombieType() != 9){
			foreach(key in keys){
				if(NetProps.GetPropEntity(infected, key) == player){
					return true
				}
			}
		}
	}
	return false
}




// When all survivors are incapacitated, give them 10 seconds to kill any special infected or boss infected ( BL-Style )
// ----------------------------------------------------------------------------------------------------------------------------

::last_chance_active	<- false
lastChanceCheckStamp	<- Time()
remainingReviveTime		<- 10
::missionFailed			<- false
::lastChanceUsed		<- false



// Called on player_death and player_incapacitated it will call enableLastChanceVision()
// when all survivors are incap but not all are dead
// ----------------------------------------------------------------------------------------------------------------------------

function lastChanceSwitch(params){
	if(!allSurvivorsDead()){
		if("userid" in params && GetPlayerFromUserID(params.userid).GetZombieType() == 9){
			if(allSurvivorsIncap() && !last_chance_active){
				enableLastChanceVision();
				lastChanceUsed = true
				last_chance_active = true
				remainingReviveTime = 10
				Convars.SetValue("director_no_death_check", 1)
				lastChanceCheckStamp = Time()
			}
		}
	}
}




// Called every tick, it will outout a countdown to timeout the survivors
// ----------------------------------------------------------------------------------------------------------------------------

function lastChanceCountDown(){
	if(last_chance_active){
		if(Time() >= lastChanceCheckStamp + 1 ){
			lastChanceCheckStamp = Time()
			if(allSurvivorsIncap()){
				if(remainingReviveTime == 0){
					last_chance_active = false
					Convars.SetValue("director_no_death_check", 0)
					ClientPrint(null, 5, BLUE + "Sorry, you don't have any time left to repopulate the world.")
					missionFailed = true
				}else{
					ClientPrint(null, 5, BLUE + remainingReviveTime)
				}
				remainingReviveTime --
			}else{
				last_chance_active = false
				Convars.SetValue("director_no_death_check", 0)
			}
		}
	}
}




// Will switch survivors vision to "black and white" and enables outline glows for special and boss infected
// ----------------------------------------------------------------------------------------------------------------------------

function enableLastChanceVision(){
	whiteScreen();
	local player = null
	local witch = null
	local rock = null
	
	while(player = Entities.FindByClassname(player, "player")){
		if(player.GetZombieType() != 9 && !player.IsDead() && !player.IsDying()){
			NetProps.SetPropInt(player, "m_Glow.m_iGlowType", 3)
			setInfectedGlowColor(player)
		}
		if(player.GetZombieType() == 9){
			player.SetReviveCount(2)
		}
	}
	while(witch = Entities.FindByClassname(witch, "witch")){
		if(witch.IsValid()){
			NetProps.SetPropInt(witch, "m_Glow.m_iGlowType", 3)
			setInfectedGlowColor(witch)
		}
	}
	while(rock = Entities.FindByClassname(rock, "tank_rock")){
		if(rock.IsValid()){
			NetProps.SetPropInt(rock, "m_Glow.m_iGlowType", 3)
			setInfectedGlowColor(rock)
		}
	}
}




// After a survivor gets a kill on any special or boss infected this should get called also
// ----------------------------------------------------------------------------------------------------------------------------

function stopLastChanceMode(){
	local player = null;
	local witch = null;
	local rock = null;
	while(player = Entities.FindByClassname(player, "player")){
		NetProps.SetPropInt(player, "m_Glow.m_iGlowType", 0)
		if(player.GetZombieType() == 9){
			player.SetReviveCount(1)
		}
	}
	while(witch = Entities.FindByClassname(witch, "witch")){
		NetProps.SetPropInt(witch, "m_Glow.m_iGlowType", 0)
	}
	while(rock = Entities.FindByClassname(rock, "tank_rock")){
		if(rock.IsValid()){
			NetProps.SetPropInt(rock, "m_Glow.m_iGlowType", 0)
		}
	}
}




// Called on mission_fail it will disable all infected glows of "last chance"
// ----------------------------------------------------------------------------------------------------------------------------

function disableInfectedGlows(){
	local player = null;
	local witch = null;
	while(player = Entities.FindByClassname(player, "player")){
		NetProps.SetPropInt(player, "m_Glow.m_iGlowType", 0)
	}
	while(witch = Entities.FindByClassname(witch, "witch")){
		NetProps.SetPropInt(witch, "m_Glow.m_iGlowType", 0)
	}
}




// Fade effect for entering the last chance vision
// ----------------------------------------------------------------------------------------------------------------------------

function whiteScreen(){
	foreach(player in GetHumanSurvivors()){
		ScreenFade(player, 200, 200, 200, 255, 1, 0.5, 1)
	}
}




// Change infected glows in "last_chance_mode" depending on difficulty
// ----------------------------------------------------------------------------------------------------------------------------

function setInfectedGlowColor(inf){
	local vector = null;
	switch(Convars.GetStr("z_difficulty").tolower()){
		case "easy"			: vector = Vector(255,45,180);	break;
		case "normal"		: vector = Vector(0,255,0);		break;
		case "hard"			: vector = Vector(200,0,0);		break;
		case "impossible"	: vector = Vector(120,120,120);	break;
		default				: vector = Vector(165,0,255);	break;
	}
	local color = vector.x
	color += 256 * vector.y
	color += 65536 * vector.z
	NetProps.SetPropInt(inf, "m_Glow.m_glowColorOverride", color)
}




// Returns vector color as int
// ----------------------------------------------------------------------------------------------------------------------------

function GetColorInt(col){
	
	if(typeof(col) == "Vector"){
		local color = col.x
		color += 256 * col.y
		color += 65536 * col.z
		return color
	}else if(typeof(col) == "string"){
		local colorArray = split(col, " ")
		local r = colorArray[0].tointeger()
		local g = colorArray[1].tointeger()
		local b = colorArray[2].tointeger()
		local color = r
		color += 256 * g
		color += 65536 * b
		return color
	}
}
rd_map_specifics

//****************************************************************************************
//																						//
//									rd_map_specifics.nut								//
//																						//
//****************************************************************************************




// Some maps require minor adjustments to improve gameplay. 
// ----------------------------------------------------------------------------------------------------------------------------

::mapSpecifics <- function(){
	switch(mapName){
		case "c1m1_hotel"				:	rd_specifics_c1m1();	break
		case "c1m2_streets"				:	rd_specifics_c1m2();	break
		case "c1m3_mall"				:	rd_specifics_c1m3();	break
		case "c5m5_bridge"				:	rd_specifics_c5m5();	break
		case "c7m3_port"				: 	rd_specifics_c7m3();	break
		case "c8m4_interior"			:	rd_specifics_c8m4();	break
		case "c8m5_rooftop"				:	rd_specifics_c8m5();	break
		case "c13m4_cutthroatcreek"		:	rd_specifics_c13m4();	break
		case "c14m1_junkyard"			:	rd_specifics_c14m1();	break
		case "c14m2_lighthouse"			:	rd_specifics_c14m2();	break
		default							:							break
	}
}




// Since we dont have any fall damage at all we should not stop players from just dropping down the hotel
// ----------------------------------------------------------------------------------------------------------------------------

::rd_specifics_c1m1 <- function(){
	local deathTriggers =
	[
		Vector(3200.000,5312.000,1648.000),
		Vector(2944.000,5888.000,1648.000),
		Vector(2936.000,6932.000,1648.000),
		Vector(1516.800,8000.000,1520.000),
		Vector(632.000,6944.000,1648.000),
		Vector(1600.000,4608.000,1648.000),
		Vector(0.000,5632.000,1648.000)
	]
	foreach(triggerPos in deathTriggers){
		local ent = null
		if(ent = Entities.FindByClassnameNearest( "trigger_hurt", triggerPos, 4 )){
			ent.Kill()
		}
	}
}




// Force start of parish finale
// ----------------------------------------------------------------------------------------------------------------------------

::rd_specifics_c5m5 <- function (){
	local ent = null
	if(ent = Entities.FindByClassname(null, "player")){
		DoEntFire( "radio_fake_button", "use", "", 1, ent, ent)
		DoEntFire( "finale", "use", "", 8, ent, ent)
		DoEntFire( "relay_init_heli", "Trigger", "", 12, ent, ent)
		DoEntFire( "relay_start_heli", "Trigger", "", 16, ent, ent)
	}
}




// Remove liftshaft_portal3_brush and rooftop_opening_clip
// ----------------------------------------------------------------------------------------------------------------------------

::rd_specifics_c8m5 <- function(){
	local ent = null
	if(ent = Entities.FindByClassnameNearest( "func_brush", Vector(7248.000000, 9168.000000, 7144.000000), 4)){
		ent.Kill()
	}
}




// Force start of cold stream finale
// ----------------------------------------------------------------------------------------------------------------------------

::rd_specifics_c13m4 <- function (){
	local ent = null
	if(ent = Entities.FindByClassname(null, "player")){
		DoEntFire( "startbldg_door_button", "use", "", 1, ent, ent)
		DoEntFire( "finale", "use", "", 12, ent, ent)
	}
}




// Change damage type of trigger_hurt ( water area in front of the end saferoom )
// ----------------------------------------------------------------------------------------------------------------------------

::rd_specifics_c14m1 <- function(){
	local ent = null
	if(ent = Entities.FindByClassnameNearest( "trigger_hurt_ghost", Vector(-4580,9352,-732), 4)){
		TriggerSetDamageType(ent, damageTypes.GENERIC)
	}
}




// Kill trigger which re-enables the ledgehang
// Change damage type of trigger_hurt ( water of rescue vehicle zone )
// ----------------------------------------------------------------------------------------------------------------------------

::rd_specifics_c14m2 <- function(){
	local ent = null
	
	if(ent = Entities.FindByClassnameNearest( "trigger_multiple", Vector(-4352,3928,1096), 4)){
		ent.Kill()
	}
	if(ent = Entities.FindByClassnameNearest( "trigger_hurt", Vector(-4608,7168,-256), 4)){
		TriggerSetDamageType(ent, damageTypes.GENERIC)
	}
	if(ent = Entities.FindByClassnameNearest("func_brush", Vector(275.000000, 930.000000, 1360.000000), 4)){
		ent.Kill()
	}
}




// Re-execute the elevator fix once
// ----------------------------------------------------------------------------------------------------------------------------


::rd_specifics_c8m4 <- function(){
	EntFire( "worldspawn", "RunScriptFile", "c8m4_elevatorfix" )
}




// Utils to manipulate entities
// ----------------------------------------------------------------------------------------------------------------------------

::TriggerSetDamageType <- function(ent, type){
	if(ent.GetClassname() == "trigger_hurt" || ent.GetClassname() == "trigger_hurt_ghost"){
		NetProps.SetPropInt(ent, "m_bitsDamageInflict", type)
	}
}




// Lets make c7m3 playable alone
// ----------------------------------------------------------------------------------------------------------------------------

::KillOldFinaleButton <- function(){
	local triggerFinale = null
		if(triggerFinale = Entities.FindByClassname(triggerFinale, "trigger_finale")){
			triggerFinale.Kill()
		}
	
	local br = null
	if(br = Entities.FindByClassname(null,"trigger_escape")){
		br.Kill()
	}
}




::rd_specifics_c7m3 <- function(){
	KillOldFinaleButton()
	EntFire("worldspawn", "RunScriptCode", "SpawnNewFinaleButton()", 1.0)
	EntFire("worldspawn", "RunScriptCode", "SpawnNewFinaleButton()", 1.0)
	EntFire("generator_start_model", "Disable", "", 1.5)
	EntFire("generator_start_model", "Enable", "", 2.0)
}




::SpawnNewFinaleButton <- function(){
	
	local keyvalues =
	{
		targetname = "generator_start_model",
		StartDisabled = "1",
		model = "models/props_vehicles/floodlight_generator_switch01.mdl",
		disableshadows = "1",
		UseDelay = "0",
		type = "2",
		origin = Vector(-456.187, -571.935, 2.57401),
		connections =
		{
			FinaleStart =
			{
				cmd1 = "bridge_ragdoll_fader�Enable��5�-1"
				cmd2 = "bridge_ragdoll_fader�Disable��8�-1"
				cmd3 = "bridge_move_sound�PlaySound��5�-1"
				cmd4 = "bridge_elevator�MoveToFloor�Bottom�5�-1"
				cmd5 = "store_brush�Enable��0�-1"
				cmd6 = "generator_brush�Enable��0�-1"
				//Editors Note: These � characters will not copy over properly. Websites cannot display them.
				//In Notepad++ replace them by selecting them, holding down ALT and entering 027 on Numblock. You'll get a black ESC character.	
			}
			FinaleEscapeStarted =
			{
				cmd1 = "relay_escape�Trigger��0�-1"
			}
		}
	}
	
	local ent = SpawnEntityFromTable("trigger_finale", keyvalues)
	NetProps.SetPropEntity(Entities.FindByName(null, "finale_start_button"), "m_sGlowEntity", ent)
}




// Convert all weapons in shop ( except deagle ) to weapon_grenade_launcher_spawn
// ----------------------------------------------------------------------------------------------------------------------------

::rd_specifics_c1m2 <- function(){

	local ent = null
	local weapons =
	[
		"weapon_pistol_spawn",
		"weapon_autoshotgun_spawn",
		"weapon_chainsaw_spawn",
		"weapon_hunting_rifle_spawn",
		"weapon_pumpshotgun_spawn",
		"weapon_rifle_ak47_spawn",
		"weapon_rifle_desert_spawn",
		"weapon_rifle_m60_spawn",
		"weapon_rifle_sg552_spawn",
		"weapon_rifle_spawn",
		"weapon_shotgun_chrome_spawn",
		"weapon_shotgun_spas_spawn",
		"weapon_smg_mp5_spawn",
		"weapon_smg_silenced_spawn",
		"weapon_smg_spawn",
		"weapon_sniper_awp_spawn",
		"weapon_sniper_military_spawn",
		"weapon_sniper_scout_spawn"
	]
	
	foreach(weapon in weapons){
		while(ent = Entities.FindByClassname(ent, weapon)){
			SpawnEntityFromTable("weapon_grenade_launcher_spawn",
			{
				weaponskin = -1
				angles = ent.GetAngles().ToKVString()
				origin = ent.GetOrigin()
				spawnflags = 2
				body = 0
				solid = 0
				count = 1
				disableshadows = 0
				skin = 0
				glowrange = 0
				model = "models/w_models/weapons/w_grenade_launcher.mdl"
			})
			ent.Kill()
		}
	}
}




// Makes event glass also breakable by the rocket launcher's projectiles
// ----------------------------------------------------------------------------------------------------------------------------

::rd_specifics_c1m3 <- function(){
	local multiFilterOld = Entities.FindByName(null, "filter_breakglass")
	
	if(multiFilterOld && multiFilterOld.IsValid()){
		multiFilterOld.Kill()
	}
	

	SpawnEntityFromTable("filter_activator_class",
	{
			targetname = "filter_grenade_launcher_projectile"
			Negated = 0
			filterclass = "grenade_launcher_projectile"
	})
	
	SpawnEntityFromTable("filter_multi",
	{
		targetname = "filter_breakglass"
		Filter01 = "filter_bullet"
		Filter02 = "filter_melee"
		Filter03 = "filter_grenade_launcher_projectile"
		//
		filtertype = 1
		Negated = 0
	})
}
rd_meds

//****************************************************************************************
//																						//
//										rd_meds.nut										//
//																						//
//****************************************************************************************



// All mushroom triggers to iterate over
// ----------------------------------------------------------------------------------------------------------------------------

::medkit_triggers	<- []




// Returns handle of the created prop_dynamic with a 'mushroom model'
// ----------------------------------------------------------------------------------------------------------------------------

::RD_healtkit_model <- function(pos, ang, shroomdata){
	local model = SpawnEntityFromTable("prop_dynamic_override",
	{
		// General
		targetname = "RD_HP_MODEL", origin = pos, angles = ang, body = 0, DefaultAnim = "idle", DisableBoneFollowers = 1,
		// Shadows and fade
		disablereceiveshadows = 1, disableshadows = 1, disableX360 = 0, ExplodeDamage = 0, ExplodeRadius = 0, fademaxdist = 0, fademindist = -1, fadescale = 0,
		// Glows
		glowbackfacemult = 1.0, glowcolor = shroomdata.glowColor, glowrange = shroomdata.glowRange, glowrangemin = 0, glowstate = shroomdata.glowstate, health = 0,
		// Model & Animation
		LagCompensate = 0, MaxAnimTime = 10, maxcpulevel = 0, maxgpulevel = 0, MinAnimTime = 5,
		mincpulevel = 0, mingpulevel = 0, model = "models/props_collectables/mushrooms_glowing.mdl", PerformanceMode = 0, pressuredelay = 0,
		RandomAnimation = 0, renderamt = 255, rendercolor = shroomdata.modelColor, renderfx = 0, rendermode = 0, SetBodyGroup = 0, skin = 0,
		solid = 0, spawnflags = 0, updatechildren = 0
	})
	model.SetModelScale(shroomdata.modelScaleMax, 0)
	return model
}




// Creates a survivor only filter for the mushrooms
// ----------------------------------------------------------------------------------------------------------------------------

SpawnEntityFromTable("filter_activator_team", { targetname = "RD_FILTER_SURVIVOR", origin = Vector(0,0,0), Negated = 0, filterteam = 2 } )
::worldspawn <- Entities.FindByClassname(null, "worldspawn")
::worldspawn.ValidateScriptScope()




// Returns handle of trigger to execute the healing function
// ----------------------------------------------------------------------------------------------------------------------------

::RD_healthkit_trigger <- function(pos, shroomdata){
	local triggerMin = shroomdata.triggerSize[0]
	local triggerMax = shroomdata.triggerSize[1]
	local zOffset = triggerMax.z
	local HP_Value = shroomdata.hp
	
	local triggerName = "RD_HP_TRIGGER"
	//
	local triggerTable =
	{
		targetname    = triggerName
		StartDisabled = 0
		spawnflags    = 1
		allowincap    = 1
		entireteam    = 0
		filtername    = "RD_FILTER_SURVIVOR"
		origin        = pos + Vector(0,0,zOffset)
	}

	local trigger = SpawnEntityFromTable( "trigger_multiple", triggerTable)
	//
	setTriggerSize(trigger,triggerMin,triggerMax)
	NetProps.SetPropInt(trigger, "m_Collision.m_nSolidType", 2)
	//
	EntFire( triggerName, "AddOutput", "OnStartTouch worldspawn:RunScriptCode:survivorMedKitTouch(activator):0:-1" )
	EntFire( triggerName, "AddOutput", "OnTouching worldspawn:RunScriptCode:survivorMedKitTouch(activator):0:-1" )
	//
	if(DevModeActive()){
		DebugDrawBox( triggerTable.origin, Vector(32,32,32), Vector(-32,-32,-32), 255, 255, 255, 0, 16)
	}
	return trigger
}




// Sets the trigger size in relation to the mushroom size
// ----------------------------------------------------------------------------------------------------------------------------

function setTriggerSize(trigger, vectorMins, vectorMaxs){
	if(trigger.IsValid()){
		if(typeof(vectorMins) == "Vector"){
			if(typeof(vectorMaxs) == "Vector"){
				NetProps.SetPropVector(trigger, "m_Collision.m_vecMins", vectorMins)
				NetProps.SetPropVector(trigger, "m_Collision.m_vecMaxs", vectorMaxs)
			}else{
				error("setTriggerSize error: vectorMaxs ment to be datatype vector")
			}
		}else{
			error("setTriggerSize error: vectorMins ment to be datatype vector")
		}
	}
}




// Creates a set of trigger and prop_dynamic_override ( healing mushroom )
// ----------------------------------------------------------------------------------------------------------------------------

::createRD_Medkit <- function(pos, ang, rotating, shroomdata){
	local HP_Val = shroomdata.hp

	local trigger = RD_healthkit_trigger(pos, shroomdata);
	local model = RD_healtkit_model(pos, ang, shroomdata);
	
	local mushroomTable =
	{
		model = model
		trigger = trigger
		restoreTime = shroomdata.restoreTime
		usetime = Time() - shroomdata.restoreTime
		usable = true
		hp = HP_Val
		modelScaleMin = shroomdata.modelScaleMin
		modelScaleMax = shroomdata.modelScaleMax
		glowstate = shroomdata.glowstate
		action = shroomdata.action
		flashColor = shroomdata.flashColor
	}
	medkit_triggers.append(trigger)
	
	addTableToEntityScope(trigger, mushroomTable)
	addTableToEntityScope(model, mushroomTable)
	
	if(rotating){
		AttachRotatorTo(model)
	}
}

::addTableToEntityScope <- function(ent, table){
	
	local scope = GetValidatedScriptScope(ent)
	
	foreach(key, value in table){
		scope[key] <- value
	}
}




// Mushrooms properties
// ----------------------------------------------------------------------------------------------------------------------------
::shroomProperties <-
{
	large 	= 	{ hp = 75, action = "HP", restoreTime = 16, modelColor = "255 185 0", flashColor = "255 185 0", glowRange = 256, glowstate = 3, glowColor = "255 185 0", GlowColorRelation = true, modelScaleMax = 7.0, modelScaleMin = 1.0, triggerSize = [ Vector(-28,-28,-28), Vector(28,28,28) ] }
	medium 	=	{ hp = 50, action = "HP", restoreTime = 16, modelColor = "220 0 255", flashColor = "220 0 255", glowRange = 256, glowstate = 3, glowColor = "220 0 255", GlowColorRelation = true, modelScaleMax = 5.0, modelScaleMin = 1.0, triggerSize = [ Vector(-16,-16,-16), Vector(16,16,16) ] }
	small	=	{ hp = 25, action = "HP", restoreTime = 16, modelColor = "0 105 255", flashColor = "0 105 255", glowRange = 256, glowstate = 3, glowColor = "0 105 255", GlowColorRelation = true, modelScaleMax = 3.0, modelScaleMin = 1.0, triggerSize = [ Vector(-16,-16,-16), Vector(16,16,16) ] }
	tiny	=	{ hp = 10, action = "HP", restoreTime = 16, modelColor = "255 255 255", flashColor = "255 255 255", glowRange = 256, glowstate = 3, glowColor = "255 255 255", GlowColorRelation = true, modelScaleMax = 2.0, modelScaleMin = 1.0, triggerSize = [ Vector(-16,-16,-16), Vector(16,16,16) ] }
	exp		=	{ hp = 0, action = "EXP", restoreTime = 8, modelColor = "0 0 0", flashColor = "0 0 0", glowRange = 128, glowstate = 3, glowColor = "255 0 0", GlowColorRelation = false, modelScaleMax = 4.0, modelScaleMin = 1.0, triggerSize = [ Vector(-16,-16,-16), Vector(16,16,16) ] }
	item	=	{ hp = 0, action = "ITEM", restoreTime = 16, modelColor = "0 0 255", flashColor = "0 0 255", glowRange = 512, glowstate = 3, glowColor = "0 0 255", GlowColorRelation = true, modelScaleMax = 4.0, modelScaleMin = 1.0, triggerSize = [ Vector(-16,-16,-16), Vector(16,16,16) ] }
	bh		=	{ hp = 0, action = "BH", restoreTime = 1, modelColor = "0 255 0", flashColor = "0 255 0", glowRange = 1024, glowstate = 3, glowColor = "0 255 0", GlowColorRelation = true, modelScaleMax = 4.0, modelScaleMin = 1.0, triggerSize = [ Vector(-16,-16,-16), Vector(16,16,16) ] }
}

foreach(dataset in shroomProperties){
	if(dataset.glowstate == 3 && dataset.GlowColorRelation){
		dataset.glowColor = getColorWithIntensity(dataset.modelColor, 77)
	}
}




// After a mushroom heals a player it should be invisible for 10 seconds
// ----------------------------------------------------------------------------------------------------------------------------

::setMedVisibility <- function(x, ent){
	
	local scope = GetValidatedScriptScope(ent)
	
	if(x == 0){
		NetProps.SetPropInt(ent, "m_Glow.m_iGlowType", 0)
		NetProps.SetPropInt(ent, "m_fEffects", NetProps.GetPropInt(ent, "m_fEffects") | (1 << 5))
		mushroomSizer(ent,scope.modelScaleMin, 0)
	}else{
		NetProps.SetPropInt(ent, "m_fEffects", 0)
		NetProps.SetPropInt(ent, "m_Glow.m_iGlowType", scope.glowstate)
		mushroomSizer(ent,scope.modelScaleMax, 0.1)
		EmitAmbientSoundOn("level/popup.wav", 1, 100, 100, ent)
	}
}




// Let's the mushroom "grow"
// ----------------------------------------------------------------------------------------------------------------------------

::mushroomSizer <- function(ent, scale, time){
	ent.SetModelScale(scale, time)
}




// Survivor touches a mushroom trigger
// ----------------------------------------------------------------------------------------------------------------------------

::survivorMedKitTouch <- function(player){
	
	// Has to be set when this fuction gets called via OnTouching because the activator is the trigger itself
	
	if(player.GetClassname() == "trigger_multiple"){
		player = Entities.FindByClassnameNearest("player", player.GetOrigin(), 256)
	}
	
	local playerPos = player.GetOrigin()
	local medkit_trigger = Entities.FindByNameNearest("RD_HP_TRIGGER", playerPos, 256)
	
	local scope = GetValidatedScriptScope(medkit_trigger)
	
	local HP_Val = scope.hp
	local action = scope.action
	local flashColor = split(scope.flashColor," ")
	
	local medkit_model = scope.model
	local mushroomUsed = false
	
	if(!IsPlayerABot(player)){
		if(!missionFailed){
			if(Time() >= scope.usetime + scope.restoreTime){
				if(action == "HP"){
					if(player.GetHealth() < player.GetMaxHealth() || player.IsIncapacitated()){
						healPlayer(player, HP_Val)
						mushroomUsed = true
					}
				}else if(action == "BH"){
					if(!(player in bunnyPlayers)){
						playerBecomesBunny(player)
						player.UseAdrenaline(7)
						mushroomUsed = true
					}
				}else if(action == "EXP"){
					executeExplosion(medkit_trigger)
					mushroomUsed = true
				}else if(action == "ITEM"){
					if(!player.IsIncapacitated()){
						local invTable = {}
						GetInvTable(player, invTable)
						if(!("slot2" in invTable)){
							giveRandomItem(player, medkit_trigger)
							mushroomUsed = true
						}
					}
				}
				if(mushroomUsed){
					scope.usetime = Time()
					scope.usable = false
					setMedVisibility(0, medkit_model)
					ScreenFade(player, flashColor[0].tointeger(), flashColor[1].tointeger(), flashColor[2].tointeger(), 128, 1.0, 0, 1)
				}
			}
		}
	}
}




// Sounds used by the mushrooms 
// ----------------------------------------------------------------------------------------------------------------------------

::mushroomSounds <-
{
	explosions =
	[
		"player/boomer/explode/explo_medium_09.wav",
		"player/boomer/explode/explo_medium_10.wav",
		"player/boomer/explode/explo_medium_14.wav"
	]
	misc =
	[
		"level/gnomeftw.wav",
		"player/laser_on.wav",
		"ui/menu_invalid.wav"
	]
}





::precacheSounds <- function(){
	foreach(collection in mushroomSounds){
		foreach(sound in collection){
			PrecacheSound(sound)
		}
	}
}




// Mushroom action ( explosion )
// ----------------------------------------------------------------------------------------------------------------------------

::executeExplosion <- function(ent){
	local location = ent.GetOrigin()
	local expTable =
	{
		origin = location
		fireballsprite = "sprites/zerogxplode.spr"
		ignoredClass = 0
		targetname = "exp"
		iMagnitude = 128
		spawnflags = 0
		iRadiusOverride = 128
		rendermode = 5
	}
	
	local exp = SpawnEntityFromTable("env_explosion", expTable)
	DoEntFire("!self", "explode", "", 0, exp, exp)
	local sounds = mushroomSounds.explosions
	EmitAmbientSoundOn(sounds[RandomInt(0, sounds.len() - 1)], 0.8, 100, 147, ent)
}




// Mushroom action ( bunny-hop )
// ----------------------------------------------------------------------------------------------------------------------------

::playerBecomesBunny <- function(player){
	if(!(player in bunnyPlayers)){
		if(player.IsValid()){
			bunnyPlayers[player] <- player
			ClientPrint(null, 5, "\x03" + player.GetPlayerName() + "\x01" + " is a bunny now.")
			EmitAmbientSoundOn("player/laser_on.wav", 1, 100, 180, player)
		}
	}
}




// Mushroom action ( random item )
// ----------------------------------------------------------------------------------------------------------------------------

::giveRandomItem <- function(player, shroom){
	local throwables = [ "weapon_pipe_bomb", "weapon_vomitjar", "weapon_molotov" ]
	player.GiveItem(throwables[RandomInt(0, throwables.len() - 1)])
	EmitAmbientSoundOn(mushroomSounds.misc[0], 0.5, 100, 170, shroom)
}




// Mushroom action ( healing )
// ----------------------------------------------------------------------------------------------------------------------------

::healPlayer <- function(player, val){
	
	local sndPitch = 100;
	switch(GetCharacterDisplayName(player)){
		case "Rochelle"	:	sndPitch = 115; break;
		case "Zoey"		: 	sndPitch = 135; break;
		default			: 	sndPitch = 100; break;
	}
	
	EmitAmbientSoundOn("player/items/pain_pills/pills_use_1.wav", 1, 100, sndPitch, player)
	StopAmbientSoundOn("player/heartbeatloop.wav", player) 
	player.UseAdrenaline(7)
	local newHP = player.GetHealth() + val
	local playerMaxHealth = player.GetMaxHealth()
	
	if(player.IsIncapacitated())
	{
		player.ReviveFromIncap()
		player.SetReviveCount(0)
		player.SetHealthBuffer(0)
		player.SetHealth(val)
	}
	else
	{
		if(newHP >= playerMaxHealth)
		{
			//NetProps.SetPropInt(player,"m_isGoingToDie",0)
			//NetProps.SetPropInt(player,"m_isIncapacitated",0)
			player.ReviveFromIncap()
			player.SetReviveCount(0)
			player.SetHealthBuffer(0)
			player.SetHealth(playerMaxHealth)
		}
		else
		{
			//NetProps.SetPropInt(player,"m_isGoingToDie",0)
			//NetProps.SetPropInt(player,"m_isIncapacitated",0)
			player.ReviveFromIncap()
			player.SetReviveCount(0)
			player.SetHealthBuffer(0)
			player.SetHealth(newHP)
		}
	}
}




// Called "OnGameplayStart" it will spawn mushrooms for the current map 
// ----------------------------------------------------------------------------------------------------------------------------

spawnMushrooms <- function(){
	if(mapName in mushroomPositions){
		foreach(DS in mushroomPositions[mapName]){
			createRD_Medkit(DS.origin, DS.angles, DS.rotating, shroomProperties[DS.type])
		}
	}
}




// When specified in mushroom definition the passed entity gets attached to a rotation entity
// ----------------------------------------------------------------------------------------------------------------------------

AttachRotatorTo <- function(ent){
	local pos = ent.GetOrigin()
	local rotName = UniqueString("_mushroom_rot")
	local rotator = SpawnEntityFromTable("func_rotating", { targetname = rotName, origin = pos, spawnflags = 67})
	NetProps.SetPropVector(rotator, "m_Collision.m_vecMins", Vector(1,1,1))
	NetProps.SetPropVector(rotator, "m_Collision.m_vecMaxs", Vector(-1,-1,-1))
	DoEntFire("!self", "SetParent", "!activator", 0.00, rotator, ent);

}
rd_melee_getter

//****************************************************************************************
//																						//
//									rd_melee_getter.nut									//
//																						//
//****************************************************************************************

sharpMeleeData <-
[
	{ model = "models/v_models/weapons/v_knife_t.mdl", itemName = "knife", alias = "knife" },
	{ model = "models/weapons/melee/v_crowbar.mdl", itemName = "crowbar", alias = "crowbar" },
	{ model = "models/weapons/melee/v_fireaxe.mdl", itemName = "fireaxe", alias = "fireaxe" },
	{ model = "models/weapons/melee/v_katana.mdl", itemName = "katana", alias = "katana" },
	{ model = "models/weapons/melee/v_machete.mdl", itemName = "machete", alias = "machete" },
	{ model = "models/weapons/melee/v_pitchfork.mdl", itemName = "pitchfork", alias = "pitchfork" }
]

bluntMeleeData <-
[
	{ model = "models/weapons/melee/v_riotshield.mdl", itemName = "riotshield" , alias = "riotshield" },
	{ model = "models/weapons/melee/v_shovel.mdl", itemName = "shovel", alias = "shovel" },
	{ model = "models/weapons/melee/v_bat.mdl", itemName = "baseball_bat", alias = "bat" },
	{ model = "models/weapons/melee/v_cricket_bat.mdl", itemName = "cricket_bat", alias = "cricket" },
	{ model = "models/weapons/melee/v_golfclub.mdl", itemName = "golfclub", alias = "golfclub" },
	{ model = "models/weapons/melee/v_tonfa.mdl", itemName = "tonfa", alias = "tonfa" },
	{ model = "models/weapons/melee/v_electric_guitar.mdl", itemName = "electric_guitar", alias = "guitar" },
	{ model = "models/weapons/melee/v_frying_pan.mdl", itemName = "frying_pan", alias = "pan" }
]




// Before giving the melee check if it's available for the current map
// ----------------------------------------------------------------------------------------------------------------------------

function GiveMelee(player, melee){

	if(meleeAliases == null){
		meleeAliases = getAvailableMeleeAliases()
	}

	local sharps = GetAvailableSharpMelees()
	local blunts = GetAvailableBluntMelees()

	if(sharps.find(melee) != null || blunts.find(melee) != null){
		player.GiveItem(melee)
	}else{
		ClientPrint(null, 5, "The " + melee + " is not available on current map.")
		ClientPrint(null, 5, "Take one of those: " + meleeAliases)
	}
}

meleeAliases <- null;
function getAvailableMeleeAliases(){
	local aliases = "";
	foreach(dataSet in sharpMeleeData){
		if(IsModelPrecached(dataSet.model)){
			aliases += dataSet.alias + ", "
		}
	}
		foreach(dataSet in bluntMeleeData){
		if(IsModelPrecached(dataSet.model)){
			aliases += dataSet.alias + ", "
		}
	}
	return aliases;
}

function GetAvailableSharpMelees(){
	local sharps = []
	foreach(dataSet in sharpMeleeData){
		if(IsModelPrecached(dataSet.model)){
			sharps.append(dataSet.itemName)
		}
	}
	return sharps;
}

function GetAvailableBluntMelees(){
	local blunts = []
	foreach(dataSet in bluntMeleeData){
		if(IsModelPrecached(dataSet.model)){
			blunts.append(dataSet.itemName)
		}
	}
	return blunts;
}




// Will give the player a random sharp/blunt weapon depending which melee is available on the current map
// ----------------------------------------------------------------------------------------------------------------------------

function getAvailableMelee(attribute){
	local sharps = GetAvailableSharpMelees()
	local blunts = GetAvailableBluntMelees()
	local melee = null;
	if(sharps.len() > 0 && blunts.len() > 0){
		if(attribute == "Sharp"){
			melee = sharps[RandomInt(0, sharps.len() - 1)]
		}else{
			melee = blunts[RandomInt(0, blunts.len() - 1)]
		}
		return melee;
	}else{
		return "bat"
	}
}
rd_mode_description

//****************************************************************************************
//																						//
//								rd_mode_description.nut									//
//																						//
//****************************************************************************************




// Output the most important facts about this mode to the players console
// ----------------------------------------------------------------------------------------------------------------------------

::printMutationInfo <- function(ent){
	
	local line = "_______________________________________________________________________________________________"
	local txt = [
	
		"-------------------------------- RocketDude by ReneTM --------------------------------"
		" "
		"In this mutation, there are no bot survivors and the human Survivors start with 200 health."
		"They are equipped with a Grenade Launcher with infinite ammo and clip size, and either a sharp melee weapon or a Magnum."
		"The Grenade Launcher's grenades do full damage to infected but only 1 to the survivors."
		"They travel in a straight line like in TF2. There are only pills and adrenaline shots to pickup."
		"All medkits and defibs are gone. There are different kinds of mushrooms spread over the map."
		"They can be picked up by walking over them. Some may be hard to reach."
		" "
		line
		" "
		" "
		" "
		" "
		"Green       : Server sided autobhop"
		"Dark Blue   : Random throwable if the players inventory allows it"
		"Black       : Explosive! I would not touch them"
		" "
		"Yellow      : +75 HP"
		"Pink        : +50 HP"
		"Blue        : +25 HP"
		"White       : +10 HP"
		" "
		"! Touching those health mushrooms also removes any tempoary health and \"black and white\" state !"
		line
		" "
		" "
		"Specialties:"
		" "
		"When all survivors are on the ground and the round would restart \"the last chance\" mode gets activated."
		"This mode being active, the survivors will have the chance, to get revived by killing any special or boss infected."
		"This mode can only be active once. Survivors will be black and white after this mode."
		"Destroying \"Skeeting\" tank-rocks mid-air will give you +5hp."
		"When you are incapped, but other survivors are still up, you can get revived by killing any special or boss infected."
		"Survivors are able to crawl while being incapped. Keep your eyes open for mushrooms."
		" "
		line
		" "
		" "
		"Chatcommands:"
		" "
		"!hud           -> Vote to enable/disable the timer hud"
		"!countdown     -> Vote to start a countdown while being in saferoom"
		" "
		"Local usage only:"
		"----------------"
		"!speedrunmode  -> Vote to toggle between speedrunmode which disables all infected and normal mode"
		"!saveangles    -> Annoyed by survivors looking the wrong direction? Save your eye angles :)"
		"!stats         -> Print players besttimes to console"
		"!r             -> Vote to jump back to the saferoom to retry"
	]
	
	foreach(line in txt){
		ClientPrint(ent, 5, GREEN + line)
	}
	for(local i = 0; i < 10; i++){
		ClientPrint(ent, 5, " ")
	}
	ClientPrint(ent, 5, GREEN + "Check the console for information!")
}
rd_saferoom_timer

//****************************************************************************************
//																						//
//									rd_saferoom_timer.nut								//
//																						//
//****************************************************************************************



::countDownActive		<- false
::countDownStamp		<- Time()
::countdownTime			<- 3

if(!IsSoundPrecached("ui/beep07.wav")){
	PrecacheSound("ui/beep07.wav")
}

if(!IsSoundPrecached("ui/beep22.wav")){
	PrecacheSound("ui/beep22.wav")
}




// Executed by the chatcommand !countdown it will check of everybody wants to make a countdown
// ----------------------------------------------------------------------------------------------------------------------------

::saferoomTimerUseStamp <- Time()

::startSafeRoomTimer <- function(ent){
	
	if(!AllInStartSafeRoom()){
		return
	}
	
	if(!(mapName in survivorSpawnPoints)){
		ClientPrint(null, 5, "This function is only available for maps of c1-c14.")
		return
	}
	
	if(NetProps.GetPropInt(Entities.FindByClassname(null, "terror_gamerules"), "m_bInIntro")){
		return
	}
	
	if(!(Time() > saferoomTimerUseStamp + 5)){
		ClientPrint(null, 5, "This function can only be used every 5 seconds.")
		return
	}

	local playerscope = GetValidatedScriptScope(ent)
	if(!("countdown_vote" in playerscope)){
		playerscope["countdown_vote"] <- true
		foreach(player in GetHumanSurvivors()){
			local playerscope = GetValidatedScriptScope(player)
			if(!("countdown_vote" in playerscope)){
				ClientPrint(null, 5, GREEN + ent.GetPlayerName() + WHITE + " voted to start a countdown.")
				return
			}
		}
	}
	
	if(!countDownActive){
		countDownActive = true
		countDownStamp = Time()
		FreezePlayers()
		SetPlayersStartPositions()
		saferoomTimerUseStamp = Time()
		foreach(player in GetHumanSurvivors()){
			player.ValidateScriptScope()
			player.GetScriptScope().rawdelete("countdown_vote")
		}
	}
}




// Gets called from Think() function
// ----------------------------------------------------------------------------------------------------------------------------

::safeRoomTimer <- function(){
	if(countDownActive){
		if(Time() > countDownStamp + 1){
			if(countdownTime > 0){
				printTimeToChat()
				countdownTime--
				countDownStamp = Time()
			}else{
				printGoToChat()
				countDownActive = false
				countdownTime = 3
			}
		}
	}
}




// Prints timer to chat and outputs sound to all players
// ----------------------------------------------------------------------------------------------------------------------------

::printTimeToChat <- function(){
	
	local players = GetHumanSurvivors()
	
	ClientPrint(null, 5,  GREEN + countdownTime)
	foreach(player in players){
		EmitAmbientSoundOn("ui/beep07.wav", 0.75, 100, 100, player)	
	}
}

::printGoToChat <- function(){

	local players = GetHumanSurvivors()

	ClientPrint(null, 5,  ORANGE + "GO!")
	UnfreezePlayers()
	foreach(player in players){
		EmitAmbientSoundOn("ui/beep22.wav", 0.75, 100, 100, player)
	}
}




// Check if all survivors are in the beginning saferoom ( Re-entering possible )
// ----------------------------------------------------------------------------------------------------------------------------

::AllInStartSafeRoom <- function(){
	foreach(player in GetHumanSurvivors()){
		if(ResponseCriteria.GetValue(player, "instartarea" ) == "0"){
			return false
		}
	}
	return true
}


::FreezePlayers <- function(){
	foreach(player in GetHumanSurvivors()){
		NetProps.SetPropInt(player, "movetype", 0)
	}
}

::UnfreezePlayers <- function(){
	foreach(player in GetHumanSurvivors()){
		NetProps.SetPropInt(player, "movetype", 2)
	}	
}




// Sets the origin of all present players to one of in the saferoom positioned info_survivor_position entities.
// This method is only ment to be used when all survivors are in the saferoom!
// ----------------------------------------------------------------------------------------------------------------------------

::SetPlayersStartPositions <- function(){

	local offsets = [ Vector(16,16,0), Vector(16,-16,0), Vector(-16,-16,0), Vector(-16,16,0) ]
	
	local counter = 0
	
	foreach(player in GetHumanSurvivors()){
		if(counter == 4){
			counter = 0
		}
		player.SetOrigin(survivorSpawnPoints[ mapName ] + offsets[counter])
		counter++
	}
}




// These are player origins for all beginning saferooms ( c1-c14 ). We spawn every player with an offset
// ----------------------------------------------------------------------------------------------------------------------------

::survivorSpawnPoints <-
{
	c1m1_hotel				= Vector(599.989,5630.53,2851.46)
	c1m2_streets			= Vector(2360.92,5124.73,452.031)
	c1m3_mall				= Vector(6628.92,-1433.91,28.0313)
	c1m4_atrium				= Vector(-2090.7,-4624.16,540.031)

	c2m1_highway			= Vector(10895.7,7873.87,-548.574)
	c2m2_fairgrounds		= Vector(1636.83,2719.81,8.03125)
	c2m3_coaster			= Vector(4343.23,2048.8,-59.9688)
 	c2m4_barns				= Vector(3116.18,3330.29,-183.969)
	c2m5_concert			= Vector(-825.861,2292.01,-251.969)

	c3m1_plankcountry		= Vector(-12546.3,10456.1,248.893)
	c3m2_swamp 				= Vector(-8160.61,7615.23,16.0313)
	c3m3_shantytown			= Vector(-5709.34,2143.82,140.031)
	c3m4_plantation			= Vector(-5011.12,-1672.01,-92.8118)
	
	c4m1_milltown_a			= Vector(-7073.8,7719.17,117.924)
	c4m2_sugarmill_a		= Vector(3628.58,-1679.04,236.531)
	c4m3_sugarmill_b		= Vector(-1803.96,-13698.4,134.031)
	c4m4_milltown_b			= Vector(3910.57,-1569.59,236.281)
	c4m5_milltown_escape 	= Vector(-3367.6,7853.06,124.031)
	
	c5m1_waterfront			= Vector(798.114,676.213,-477.969)
	c5m2_park				= Vector(-3966.59,-1261.61,-339.969)
	c5m3_cemetery			= Vector(6402.67,8410.15,4.03125)
	c5m4_quarter			= Vector(-3291.22,4889.5,72.0313)
	c5m5_bridge				= Vector(-12039.5,5833.28,132.031)

	c6m1_riverbank			= Vector(911.249,3774.63,98.0062)
	c6m2_bedlam				= Vector(3157.03,-1218.18,-291.969)
	c6m3_port				= Vector(-2410.05,-456.402,-251.969)
	
	c7m1_docks				= Vector(13838.7,2583.8,36.2599)
	c7m2_barge				= Vector(10726.2,2445.25,180.031)
	c7m3_port				= Vector(1123.67,3230.13,174.531)
	
	c8m1_apartment			= Vector(2004.74,913.544,436.031)
	c8m2_subway				= Vector(2907.06,3063.35,20.0313)
	c8m3_sewers				= Vector(10948.2,4729.64,20.0313)
	c8m4_interior			= Vector(12376.7,12567.9,20.0313)
	c8m5_rooftop			= Vector(5405.11,8388.72,5540.03)

	c9m1_alleys				= Vector(-9951.59,-8578.78,-2.66612)
	c9m2_lots				= Vector(278.24,-1301.6,-171.969)
	
	c10m1_caves				= Vector(-11745.4,-14862.1,-214.225)
	c10m2_drainage 			= Vector(-11014.6,-9103.6,-587.969)
	c10m3_ranchhouse		= Vector(-8263.9,-5550.87,-20.9688)
	c10m4_mainstreet		= Vector(-3082.28,-23.326,164.031)
	c10m5_houseboat			= Vector(2120.59,4742.14,-59.9688)
	
	c11m1_greenhouse		= Vector(6815.1,-672.996,772.031)
	c11m2_offices			= Vector(5278.98,2785.43,52.0313)
	c11m3_garage			= Vector(-5368.26,-3069.3,20.0313)
	c11m4_terminal			= Vector(-447.003,3577.39,300.031)
	c11m5_runway			= Vector(-6716.97,12066.8,156.031)
	
	c12m1_hilltop			= Vector(-8073.41,-15118.7,283.495)
	c12m2_traintunnel		= Vector(-6611.49,-6712.19,352.031)
	c12m3_bridge			= Vector(-927.759,-10382.7,-59.9688)
	c12m4_barn				= Vector(7754.14,-11374,444.031)
	c12m5_cornfield			= Vector(10480.9,-566.041,-24.9688)
	
	c13m1_alpinecreek		= Vector(-3012.43,-589.132,68.0313)
	c13m2_southpinestream	= Vector(8566.48,7505.06,500.031)
	c13m3_memorialbridge	= Vector(-4372.79,-5150.93,100.031)
	c13m4_cutthroatcreek	= Vector(-3387.78,-9284.08,364.031)
	
	c14m1_junkyard			= Vector(-4189.09,-10697.9,-296.294)
	c14m2_lighthouse		= Vector(2220.41,-1103.17,452.031)
}




// Prevent players from using restartFromSaferoom on a finale
// ----------------------------------------------------------------------------------------------------------------------------

::valveFinaleMaps <-
[
	"c1m4_atrium"
	"c2m5_concert"
	"c3m4_plantation"
	"c4m5_milltown_escape"
	"c5m5_bridge"
	"c6m3_port"
	"c7m3_port"
	"c8m5_rooftop"
	"c9m2_lots"
	"c10m5_houseboat"
	"c11m5_runway"
	"c12m5_cornfield"
	"c13m4_cutthroatcreek"
	"c14m2_lighthouse"
]




//insafespot
//incheckpoint
rd_speedrun_mode

//****************************************************************************************
//																						//
//										rd_speedrun_mode.nut							//
//																						//
//****************************************************************************************




// Playing locally allows the player to enable the speedrun mode.
// !speedrunmode to toggle it. !r to respawn in the saferoom
// ----------------------------------------------------------------------------------------------------------------------------

::speedrunModeEnabled <- false

::speedrunModeToggle <- function(ent){
	
	if(!OnLocalServer()){
		return
	}
	
	ent.ValidateScriptScope()
	local scope = ent.GetScriptScope()
	scope["speedrun_mode"] <- true
	
	foreach(player in GetHumanSurvivors()){
		local playerscope = player.GetScriptScope()
		if(!("speedrun_mode" in playerscope)){
			ClientPrint(null, 5, GREEN + ent.GetPlayerName() + WHITE + " voted to " + ( speedrunModeEnabled ? "disable" : "enable" ) + " the speedrun mode!")
			return
		}
	}
	
	speedrunModeEnabled = !speedrunModeEnabled
	
	local speedrunDirector =
	{
		ProhibitBosses	= [true, false]
		MobMaxSize		= [0, 30]
		CommonLimit		= [0, 30]
		MobSpawnMinTime	= [9999, 90]
		MobSpawnMaxTime	= [9999, 180]
		MaxSpecials		= [0, 6]
		SpecialRespawnInterval = [9999, 45]
	}
	
	foreach(var,valarr in speedrunDirector){
		if(speedrunModeEnabled){
			SessionOptions[var.tostring()] <- valarr[0]
			removeAllInfected()
		}else{
			SessionOptions[var.tostring()] <- valarr[1]
		}
	}
	
	foreach(player in GetHumanSurvivors()){
		player.GetScriptScope().rawdelete("speedrun_mode")
	}
	
	ClientPrint(null, 5, WHITE + "Speedrun mode " + GREEN + ( speedrunModeEnabled ? "enabled" : "disabled") )
}




// While being in speedrun mode the survivor can jump back to the saferoom to restart his run
// ----------------------------------------------------------------------------------------------------------------------------

::restartFromSaferoom <- function(ent){
	
	if(!speedrunModeEnabled){
		ClientPrint(null, 5, WHITE + "Jumping back to the saferoom is restricted to " + GREEN + "speedrunmode!")
		return
	}
	
	if(!(Director.GetMapName() in survivorSpawnPoints)){
		return
	}
	
	if(valveFinaleMaps.find(Director.GetMapName()) != null){
		ClientPrint(null, 5, WHITE + "Jumping back to the saferoom is disabled for finales")
		return
	}
	
	local scope = GetValidatedScriptScope(ent)
	
	scope["go_back_2_safe"] <- true
	
	foreach(player in GetHumanSurvivors()){
		local playerscope = player.GetScriptScope()
		if(!("go_back_2_safe" in playerscope)){
			ClientPrint(null, 5, GREEN + ent.GetPlayerName() + WHITE + " voted to go back to the saferoom!")
			return
		}
	}
	
	foreach(player in GetHumanSurvivors()){
		local playerscope = GetValidatedScriptScope(player)
		playerscope.rawdelete("go_back_2_safe")
		local prevBest = PlayerTimeData[player].time_best
		PlayerTimeData[player] <- { timerActive = false, finished = false, startTime = Time(), endTime = Time(), time_best = prevBest, ticks = 0, seconds = 0 }
		bunnyPlayers.rawdelete(player)
		NetProps.SetPropInt(player, "m_afButtonDisabled", NetProps.GetPropInt(player, "m_afButtonDisabled") & ~2)
		GLOBALS.allowBulletTime = false
		
		// Set player eye angles when there are any saved
		if(player in SavedplayerEyeAngles){
			player.SnapEyeAngles(SavedplayerEyeAngles[player].EyeAngles)
		}
		player.SetVelocity(Vector(0,0,256))
	}
	
	SetPlayersStartPositions()
	setPlayersHealth()
}




// Prints all times of known maps to the console
// ----------------------------------------------------------------------------------------------------------------------------

::outputStats <- function(ent){
	
	if(ent != GetListenServerHost()){
		return
	}
	
	foreach(mapname in valveMaps){
		local str = null
		str = FileToString("rocketdude/speedrun/" + mapname + ".txt")
		if(str != null){
			str = strip(str)
			if(isNumeric(str)){
				ClientPrint(ent, 5, GREEN + mapname + GetCharChain(" ", 32 - mapname.len()) + ": " + str)
			}else{
				ClientPrint(ent, 5, ORANGE + mapname + GetCharChain(" ", 32 - mapname.len()) + ": INVALID DATA IN TEXT FILE")
			}
		}else{
			ClientPrint(ent, 5, BLUE + mapname + GetCharChain(" ", 32 - mapname.len()) + ": NO RECORD YET")
		}
	}
	for(local i = 0; i < 10; i++){
		ClientPrint(ent, 5, " ")
	}
	ClientPrint(ent, 5, GREEN + "Check the console for your stats!")
}




// Removes all infected like nb_delete_all does
// ----------------------------------------------------------------------------------------------------------------------------

::removeAllInfected <- function(){
	local player = null
	local witch = null
	local common = null
	
	while(player = Entities.FindByClassname(player, "player")){
		if(IsPlayerABot(player) && player.GetZombieType() != 9){
			player.Kill()
		}
	}
	
	while(witch = Entities.FindByClassname(witch, "witch")){
		if(witch.IsValid()){
			witch.Kill()
		}
	}
	
	while(common = Entities.FindByClassname(common, "infected")){
		if(common.IsValid()){
			common.Kill()
		}
	}
}




// Save player angles for speedrunning
// ----------------------------------------------------------------------------------------------------------------------------

::SavedplayerEyeAngles <- {}

function savePlayerEyeAngles(ent){
	
	if(!OnLocalServer()){
		ClientPrint(null, 5, WHITE + "Saving player angles is restricted to local servers!")
		return
	}
	
	if(!speedrunModeEnabled){
		ClientPrint(null, 5, WHITE + "Enable speedrunmode first ( " + GREEN + "!speedrunmode" + WHITE + " in chat )!")
		return
	}
	
	if(ResponseCriteria.GetValue(ent, "instartarea" ) == "0"){
		ClientPrint(null, 5, WHITE + "Move to the start area to save your eye angles!")
		return
	}
	
	if(ent.IsValid()){
		SavedplayerEyeAngles[ent] <- { EyeAngles = ent.EyeAngles() }
		ClientPrint(null, 5, WHITE + "Your eye angles got " + GREEN + "saved!")
	}
}
rd_utils

//****************************************************************************************
//																						//
//										rd_utils.nut									//
//																						//
//****************************************************************************************


getroottable()["TRACE_MASK_ALL"] <- -1
getroottable()["TRACE_MASK_SHOT"] <- 1174421507
getroottable()["TRACE_MASK_VISION"] <- 33579073
getroottable()["TRACE_MASK_NPC_SOLID"] <- 33701899
getroottable()["TRACE_MASK_PLAYER_SOLID"] <- 33636363
getroottable()["TRACE_MASK_VISIBLE_AND_NPCS"] <- 33579137


getroottable()["WHITE"]		<- "\x01"
getroottable()["BLUE"]		<- "\x03"
getroottable()["ORANGE"]	<- "\x04"
getroottable()["GREEN"]		<- "\x05"


::ZombieTypes <-
{
	COMMON		= 0
	SMOKER		= 1
	BOOMER		= 2
	HUNTER		= 3
	SPITTER		= 4
	JOCKEY		= 5
	CHARGER		= 6
	WITCH		= 7
	TANK		= 8
	SURVIVOR	= 9
	MOB			= 10
	WITCHBRIDE	= 11
	MUDMEN		= 12
}

::TEAMS <-
{ 
	SPECTATOR	= 1
	SURVIVOR	= 2
	INFECTED	= 3
}

::damageTypes <-
{
	GENERIC			= 0
	CRUSH			= 1
	BULLET			= 2
	SLASH			= 4
	BURN			= 8
	VEHICLE			= 16
	FALL			= 32
	BLAST			= 64
	CLUB			= 128
	SHOCK			= 256
	SONIC			= 512
	ENERGYBEAM		= 1024
	DROWN			= 16384
	PARALYSE		= 32768
	NERVEGAS		= 65536
	POISON			= 131072
	RADIATION		= 262144
	DROWNRECOVER	= 524288
	ACID			= 1048576
	SLOWBURN		= 2097152
	REMOVENORAGDOLL	= 4194304
}


// Creates the think timer which calls "Think()" every tick
// ----------------------------------------------------------------------------------------------------------------------------

function createThinkTimer(){
	local timer = null
	while (timer = Entities.FindByName(null, "thinkTimer")){
		timer.Kill()
	}
	timer = SpawnEntityFromTable("logic_timer", { targetname = "thinkTimer", RefireTime = 0.01 })
	timer.ValidateScriptScope()
	timer.GetScriptScope()["scope"] <- this

	timer.GetScriptScope()["func"] <- function (){
		scope.Think()
	}
	timer.ConnectOutput("OnTimer", "func")
	EntFire("!self", "Enable", null, 0, timer)
}




// Remove all deathcams e.g on c8m5_rooftop
// ----------------------------------------------------------------------------------------------------------------------------

function removeDeathFallCameras(){
	local deathCam = null
	while (deathCam = Entities.FindByClassname(deathCam, "point_deathfall_camera")){
		deathCam.Kill()
	}
}




// All needed cvars
// ----------------------------------------------------------------------------------------------------------------------------

::cvars <-
{
	// Survivor settings
	sv_infinite_ammo = 1
	survivor_allow_crawling = 1
	survivor_crawl_speed = 45
	first_aid_kit_max_heal = 200
	survivor_respawn_with_guns = 0
	first_aid_heal_percent = 0.8
	z_grab_ledges_solo = 1
	z_tank_incapacitated_decay_rate = 5
	// Grenadelauncher settings
	grenadelauncher_velocity = 1100
	grenadelauncher_startpos_right = 0
	grenadelauncher_startpos_forward = 16
	grenadelauncher_vel_variance = 0
	grenadelauncher_vel_up = 0
	// Force settings
	phys_explosion_force = 4096
	melee_force_scalar = 16
	melee_force_scalar_combat_character = 512
	phys_pushscale = 512
	// Infected settings
	z_force_attack_from_sound_range = 512
	z_brawl_chance = 1
	// Medicals
	pain_pills_health_threshold = 199
	pain_pills_health_value = 100
	// Items
	sv_infected_riot_control_tonfa_probability = 0
	sv_infected_ceda_vomitjar_probability = 0
	// Votes
	sv_vote_creation_timer = 8
	sv_vote_plr_map_limit = 128
	// Misc
	z_spawn_flow_limit = 99999
	director_afk_timeout = 99999
	mp_allowspectators = 0
	// Disable Placeholder bots
	director_transition_timeout = 1
}




local cvarChangeTime = Time()

function checkCvars(){
	if(Time() > cvarChangeTime + 4){
		foreach(var, value in cvars){
			if(Convars.GetFloat(var) != value.tofloat()){
				Convars.SetValue(var, value)
			}
		}
		cvarChangeTime = Time()
	}
}




// Create a func_timescale entity for the "bullet time"
// ----------------------------------------------------------------------------------------------------------------------------

timeScaler <- null
function createBulletTimerEntity(){
	while (timeScaler = Entities.FindByName(null, "timeScaler")){
		timeScaler.Kill()
	}
	timeScaler = SpawnEntityFromTable("func_timescale",
		{
			targetname = "timeScaler"
			acceleration = 0.05
			angles = "0 0 0"
			origin = Vector(0, 0, 0)
			blendDataMultiplier = 3.0
			minBlendRate = 0.1
			desiredTimescale = 0.25
		}
	)
}




// We roll a dice with probability of X to decide if event Y will occur 
// ----------------------------------------------------------------------------------------------------------------------------

function rollDice(probability){
	local roll = RandomInt(1, 100)
	
	if(probability == 100){
		return true
	}else if(roll <= probability){
		return true
	}
	return false
}




// Returns the closest survivor in any radius
// ----------------------------------------------------------------------------------------------------------------------------

function getClosestSurvivorTo(ent){
	local survivor = null
	local previousDistance = 0.0
	local closest = null
	local currentDistance = null
	
	foreach(survivor in GetSurvivors()){
		if(survivor != ent){
			if(previousDistance == 0.0){
				previousDistance = (ent.GetOrigin() - survivor.GetOrigin()).Length()
				closest = survivor
			}else{
				currentDistance = (ent.GetOrigin() - survivor.GetOrigin()).Length()
				if(currentDistance < previousDistance){
					previousDistance = currentDistance
					closest = survivor
				}
			}
		}
	}
	return closest
}




// Returns array of all players (bots included)
// ----------------------------------------------------------------------------------------------------------------------------

function GetSurvivors(){
	local ent = null
	while (ent = Entities.FindByClassname(ent, "player")){
		if (ent.GetZombieType() == 9){
			yield ent
		}
	}
}


// Returns array of all survivors (bots excluded)
// ----------------------------------------------------------------------------------------------------------------------------

::GetHumanSurvivors <- function(){
	local ent = null
	while (ent = Entities.FindByClassname(ent, "player")){
		if (ent.GetZombieType() == 9 && !IsPlayerABot(ent)){
			yield ent
		}
	}
}




// Validates script scope and returns it
// ----------------------------------------------------------------------------------------------------------------------------

::GetValidatedScriptScope <- function(ent){
	ent.ValidateScriptScope()
	return ent.GetScriptScope()
}




// Precache survivor models so game wont crash due to the "cm_NoSurvivorBots = 1" bug...Valve please fix
// ----------------------------------------------------------------------------------------------------------------------------

function precacheSurvivorModels(){
	
	local path = "models/survivors/"
	local models =
	[
		"survivor_coach.mdl", "survivor_gambler.mdl", "survivor_manager.mdl", "survivor_mechanic.mdl",
		"survivor_namvet.mdl", "survivor_biker.mdl", "survivor_producer.mdl", "survivor_teenangst.mdl"
	]

	foreach(model in models){
		if (!IsModelPrecached( path + model)){
			PrecacheModel(path + model)
		}
	}
}




// Precache projectile models and mushroom
// ----------------------------------------------------------------------------------------------------------------------------

function precacheRocketDudeModels(){
	
	local models =
	[
		"models/props_collectables/mushrooms_glowing.mdl",
		"models/w_models/weapons/w_rd_grenade_scale_x4_burn.mdl",
		"models/w_models/weapons/w_rd_grenade_scale_x4.mdl"
	]
	
	foreach(model in models){
		if(!IsModelPrecached(model)){
			PrecacheModel(model)
		}
	}
}




// Check if the current map is a valve map
// ----------------------------------------------------------------------------------------------------------------------------

::IsValveMap <- function(){
	if (valveMaps.find(mapName) == null){
		return false
	}
	return true
}




// Array of maps c1 - c14
// ----------------------------------------------------------------------------------------------------------------------------

::valveMaps <- [
	// DEAD CENTER
	"c1m1_hotel"
	"c1m2_streets"
	"c1m3_mall"
	"c1m4_atrium"
	// DARK CARNIVAL
	"c2m1_highway"
	"c2m2_fairgrounds"
	"c2m3_coaster"
	"c2m4_barns"
	"c2m5_concert"
	// SWAMP FEVER
	"c3m1_plankcountry"
	"c3m2_swamp"
	"c3m3_shantytown"
	"c3m4_plantation"
	// HARD RAIN
	"c4m1_milltown_a"
	"c4m2_sugarmill_a"
	"c4m3_sugarmill_b"
	"c4m4_milltown_b"
	"c4m5_milltown_escape"
	// THE PARISH
	"c5m1_waterfront"
	"c5m1_waterfront_sndscape"
	"c5m2_park"
	"c5m3_cemetery"
	"c5m4_quarter"
	"c5m5_bridge"
	// THE PASSING
	"c6m1_riverbank"
	"c6m2_bedlam"
	"c6m3_port"
	// THE SACRIFICE
	"c7m1_docks"
	"c7m2_barge"
	"c7m3_port"
	// NO MERCY
	"c8m1_apartment"
	"c8m2_subway"
	"c8m3_sewers"
	"c8m4_interior"
	"c8m5_rooftop"
	// CRASH COURSE
	"c9m1_alleys"
	"c9m2_lots"
	// DEATH TOLL
	"c10m1_caves"
	"c10m2_drainage"
	"c10m3_ranchhouse"
	"c10m4_mainstreet"
	"c10m5_houseboat"
	// DEAD AIR
	"c11m1_greenhouse"
	"c11m2_offices"
	"c11m3_garage"
	"c11m4_terminal"
	"c11m5_runway"
	// BLOOD HARVEST
	"c12m1_hilltop"
	"c12m2_traintunnel"
	"c12m3_bridge"
	"c12m4_barn"
	"c12m5_cornfield"
	// COLD STREAM
	"c13m1_alpinecreek"
	"c13m2_southpinestream"
	"c13m3_memorialbridge"
	"c13m4_cutthroatcreek"
	// THE LAST STAND
	"c14m1_junkyard"
	"c14m2_lighthouse"
]




// Mushroom positions of maps ( c1 -14 )
// ----------------------------------------------------------------------------------------------------------------------------

::mushroomPositions <-
{
	c1m1_hotel =
	[
		{ origin = Vector(910.302,5475.81,2656.03), angles = "0 90 0", rotating = true, type = "bh" }
		{ origin = Vector(3430.85,7487.11,1664.03), angles = "0 90 0", rotating = false, type = "item" }
		{ origin = Vector(2168.26,5703.24,2464.03), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(2480.38,6217.6,2656.03), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(2311.68,7656.3,2464.03), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(1924.48,5762.97,1336.03), angles = "0 90 0", rotating = false, type = "medium" }
		{ origin = Vector(540.982,4832.06,1320.03), angles = "0 0 0", rotating = false, type = "large" }
	]
	c1m2_streets =
	[
		{ origin = Vector(1059.62,4854.1,704.031), angles = "0 90 0", rotating = true, type = "bh" }
		{ origin = Vector(-6132.59,-1135.09,472.031), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(-3897.52,2238.46,320.031), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(-1253.71,777.393,811.381), angles = "0 90 0", rotating = false, type = "tiny" }
		{ origin = Vector(-5343.9,-2082.83,456.031), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-8619.01,-2111.07,963.138), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-7171.31,-4490.4,1224.7), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(1598.73,4225.02,521.433), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(-2223.24,982.305,41.804), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(-4588.86,1475.42,440.031), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-8635.88,-4498.73,440.031), angles = "0 0 0", rotating = false, type = "large" }
	]
	c1m3_mall =
	[
		{ origin = Vector(6447.99,-2648.58,288.031), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(1294.77,-2343.76,325.803), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(675.635,-4834.9,536.031), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(3600.03,-2384.03,825.031), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(4008.45,-290.606,0.03125), angles = "0 90 0", rotating = false, type = "tiny" }
		{ origin = Vector(2266.34,-1561.81,536.031), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-198.189,-5201.84,415.031), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-1891.39,-4127.36,574.433), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(6997.87,-1362.59,152.031), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(3896.59,-2873.28,318.958), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(2754.07,-1866.65,280.031), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(2414.66,-2412.11,536.031), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(1584.63,-5446.67,364.031), angles = "0 90 0", rotating = false, type = "medium" }
		{ origin = Vector(-1239.21,-4472.65,318.958), angles = "0 0 0", rotating = false, type = "large" }
	]
	c1m4_atrium =
	[
		{ origin = Vector(-1806.74,-4825.39,621.031), angles = "0 90 0", rotating = true, type = "bh" }
		{ origin = Vector(-2388.27,-3878.41,824.031), angles = "0 90 0", rotating = false, type = "item" }
		{ origin = Vector(-3856.95,-3123.88,536.031), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(-2572.11,-5303.06,553.52), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-6031.35,-3306.58,792.031), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-3340.16,-4001.26,744.781), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(-5103.33,-3918.44,408.031), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(-4451.52,-3207.95,106.031), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-3209.12,-3865.85,107.617), angles = "0 90 0", rotating = false, type = "large" }
		{ origin = Vector(-5343.23,-4186.55,1080.03), angles = "0 0 0", rotating = false, type = "large" }
	]
	c2m1_highway =
	[
		{ origin = Vector(9530.42,8430.98,-176.474), angles = "0 90 0", rotating = true, type = "bh" }
		{ origin = Vector(2273.79,4287.25,-936.719), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(2188.58,3274.16,-807.969), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(3462.18,8450.17,-843.079), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-1168.83,2091.84,-1738.7), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(6915.73,7505.84,-675.791), angles = "0 90 0", rotating = false, type = "small" }
		{ origin = Vector(3015.77,6931.89,-899.608), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(2914.95,4895.79,-507.969), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(1163.45,2193.78,-1213.73), angles = "0 0 0", rotating = false, type = "large" }
		{ origin = Vector(-1334.14,-2035.05,-510.97), angles = "0 0 0", rotating = false, type = "large" }
	]
	c2m2_fairgrounds =
	[
		{ origin = Vector(3966.348633,-465.165253,48.42), angles = "0 90 0", rotating = true, type = "bh" }
		{ origin = Vector(-1985.2,-3877.99,32.0313), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(-3616.02,-721.635,0.03125), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(1576.99,1513.79,8.03125), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-3329.16,-4239.81,352.595), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(2710.8,516.643,200.031), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(-2126.08,318.905,128.031), angles = "0 90 0", rotating = false, type = "medium" }
		{ origin = Vector(-874.256,-1539.82,128.031), angles = "0 90 0", rotating = false, type = "medium" }
		{ origin = Vector(-1493.91,-4415.5,-1.77925), angles = "0 0 0", rotating = false, type = "large" }
	]
	c2m3_coaster =
	[
		{ origin = Vector(2048.36,3296.04,196.031), angles = "0 90 0", rotating = true, type = "bh" }
		{ origin = Vector(-1582.4,1617.42,128.281), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(-87.7301,3973.93,208.031), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(2797.84,1638.93,-35.5456), angles = "0 90 0", rotating = false, type = "tiny" }
		{ origin = Vector(-3968.39,1550.79,413.031), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-66.8414,3611.93,208.031), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(-2754.75,1143.2,620.031), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-3743.81,3732.83,544.031), angles = "0 0 0", rotating = false, type = "large" }
	]
	c2m4_barns =
	[
		{ origin = Vector(3138.24,3629.02,-3.96875), angles = "0 90 0", rotating = true, type = "bh" }
		{ origin = Vector(-2756.8,1308.9,-43.9688), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(1081.55,1024.68,-147.969), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(2166.15,1419.64,18.4938), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(-226.231,886.084,387.031), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(-1918.83,95.4541,32.0313), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-3042.29,1694.38,-255.969), angles = "0 0 0", rotating = false, type = "large" }
		{ origin = Vector(3715.38,662.404,-191.969), angles = "0 0 0", rotating = false, type = "tiny" }
	]
	c2m5_concert =
	[
		{ origin = Vector(-1096.98,2354.9,-255.969), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(-2308.02,3211.99,140.031), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(-2637.91,3340.46,312.678), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(-448.975,2760.9,-255.969), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-1979.98,2505.26,191.031), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-2679.04,2500.67,191.031), angles = "0 0 0", rotating = false, type = "large" }
	]
	c3m1_plankcountry =
	[
		{ origin = Vector(-10586.4,10170.8,571.008), angles = "0 90 0", rotating = true, type = "bh" }
		{ origin = Vector(-5590.18,6843.73,220.031), angles = "0 90 0", rotating = false, type = "item" }
		{ origin = Vector(-980.031,4902.7,144.16), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(-1067.97,4902.7,144.16), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(-1022.58,4902.7,144.16), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(-8121.03,7213.5,266.604), angles = "0 90 0", rotating = false, type = "tiny" }
		{ origin = Vector(-5392.27,5891.52,256.034), angles = "0 90 0", rotating = false, type = "small" }
		{ origin = Vector(-6212.27,7861.63,48.0313), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(-3386.78,6069.38,698.369), angles = "0 90 0", rotating = false, type = "medium" }
		{ origin = Vector(-2159.44,8640.77,253.1), angles = "0 0 0", rotating = false, type = "large" }
		{ origin = Vector(-1022.87,4946.13,332.031), angles = "0 0 0", rotating = false, type = "large" }
	]
	c3m2_swamp =
	[
		{ origin = Vector(-8114.13,5131.93,295.391), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(-1801.48,2818.93,47.6229), angles = "0 90 0", rotating = false, type = "item" }
		{ origin = Vector(-1884.05,3218.16,36.4844), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(1936.43,1250.89,19.2292), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(7614.55,3192.79,122.329), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-1895.61,3104.54,226.169), angles = "0 90 0", rotating = false, type = "small" }
		{ origin = Vector(4779.86,1102.69,41.265), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(8702.53,512.321,569.088), angles = "0 0 0", rotating = false, type = "large" }
	]
	c3m3_shantytown =
	[
		{ origin = Vector(-5293.03,1199.51,871.913), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(-4088.94,-3094.7,189.711), angles = "0 90 0", rotating = false, type = "item" }
		{ origin = Vector(-659.526,-2485.02,4.4949), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(-4605.58,-236.376,181.425), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(1532.24,-4869.25,24.0313), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(-2651.48,-931.619,74.849), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-5504.8,-3256.66,308.68), angles = "0 90 0", rotating = false, type = "medium" }
		{ origin = Vector(3013.28,-4477.09,86.1035), angles = "0 90 0", rotating = false, type = "large" }
	]
	c3m4_plantation =
	[
		{ origin = Vector(-3650.61,-1527.09,542.849), angles = "0 90 0", rotating = true, type = "bh" }
		{ origin = Vector(1665.36,900.156,127.237), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(1788.93,-415.403,224.031), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(2591.66,57.0649,224.031), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(2032.64,252.058,416.031), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(1664.07,314.551,416.031), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(1663.35,291.383,224.031), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(1320.5,251.85,416.031), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(2743.47,-3207.78,65.3052), angles = "0 90 0", rotating = false, type = "small" }
		{ origin = Vector(3040.81,2016.1,133.507), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-1426.21,-3443.73,187.907), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(1665.04,536.081,640.031), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(1867.68,-138.64,600.031), angles = "0 0 0", rotating = false, type = "large" }
	]
	c4m1_milltown_a =
	[
		{ origin = Vector(-5801.77,7494.31,1009.75), angles = "0 90 0", rotating = true, type = "bh" }
		{ origin = Vector(383.878,3381.04,368.031), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(-6356.01,7456.24,104.031), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(-1547.58,6908.92,200.773), angles = "0 90 0", rotating = false, type = "tiny" }
		{ origin = Vector(4152.82,1222.7,184.031), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-609.475,6187.33,296.031), angles = "0 90 0", rotating = false, type = "small" }
		{ origin = Vector(1482.28,4149.37,435.32), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(3617.5,2161.08,368.155), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(3338.04,179.84,586.974), angles = "0 0 0", rotating = false, type = "large" }
	]
	c4m2_sugarmill_a =
	[
		{ origin = Vector(4260,-3671.71,406.515), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(-719.081,-7283.16,441.979), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(-921.116,-8917.79,295.392), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(2429.61,-5696.4,124.659), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(4315.07,-4528.41,97.6315), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-10.6827,-12670.2,113.272), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(3212.06,-3036.58,1164.28), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(2725.17,-4272.82,329.469), angles = "0 90 0", rotating = false, type = "small" }
		{ origin = Vector(2581.79,-6098.72,100.829), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-345.992,-8559.92,624.281), angles = "0 90 0", rotating = false, type = "small" }
		{ origin = Vector(-1507.19,-13161,1141.23), angles = "0 0 0", rotating = false, type = "large" }
	]
	c4m3_sugarmill_b =
	[
		{ origin = Vector(-925.624,-13530.8,432.031), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(-53.2867,-6479.09,441.729), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(2434.5,-5315.34,194.031), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(2587.19,-6096.47,100.157), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(4114.54,-3217.8,406.265), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-1106.24,-8463.68,624.031), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(936.826,-6244.9,638.807), angles = "0 90 0", rotating = false, type = "medium" }
		{ origin = Vector(430.224,-4582.72,310.428), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(1741.85,-3929.74,737.02), angles = "0 0 0", rotating = false, type = "large" }
	]
	c4m4_milltown_b =
	[
		{ origin = Vector(3331.17,-847.188,586.974), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(665.963,2783.55,227.927), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(387.227,3489.37,336.38), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(4327.79,1780.41,363.761), angles = "0 90 0", rotating = false, type = "small" }
		{ origin = Vector(673.109,4776.85,131.802), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-1486.32,7441.23,366.277), angles = "0 0 0", rotating = false, type = "large" }
	]
	c4m5_milltown_escape =
	[
		{ origin = Vector(-5868.91,8159.17,348.031), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(-4641.69,7626.45,479.597), angles = "0 90 0", rotating = false, type = "item" }
		{ origin = Vector(-6338.14,7169.93,104.031), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(-5432.06,7068.65,100.536), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(-5315.37,8567.07,584.038), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-7088.42,7698.42,113.924), angles = "0 90 0", rotating = false, type = "small" }
		{ origin = Vector(-5815.44,6623.25,126.972), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-5801.8,7496.52,1009.75), angles = "0 0 0", rotating = false, type = "large" }
	]
	c5m1_waterfront =
	[
		{ origin = Vector(-335.683,64.8597,-52.3949), angles = "0 90 0", rotating = true, type = "bh" }
		{ origin = Vector(-1177.66,-2398.49,144.031), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(-2347.78,-553.647,-367.969), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(-2720.07,-1612.63,-13.3263), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-1904.66,-1864.33,-71.9454), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(-3068.68,-2336.01,-157.992), angles = "0 0 0", rotating = false, type = "medium" }
	]
	c5m2_park =
	[
		{ origin = Vector(-5054.91,-2216.52,-127.982), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(-9802.61,-5213.03,-79.9688), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(-8046.08,-6670.04,-247.969), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(-7940.64,-6671.62,-247.969), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(-7551.83,-416.524,-127.969), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-7166.92,-3491.44,44.5587), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(-8789.87,-5193.33,89.7595), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-8113.72,-5774.25,485.766), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-6815.91,-8437.09,250.906), angles = "0 0 0", rotating = false, type = "large" }
	]
	c5m3_cemetery =
	[
		{ origin = Vector(6127.99,7700.52,208.031), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(5907.05,1012.93,155.243), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(2837.89,2634.4,176.031), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(4728.95,4570.35,131.378), angles = "0 90 0", rotating = false, type = "tiny" }
		{ origin = Vector(4432.79,3202.69,154.031), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(6189.01,1337.86,-159.969), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(8768.77,-6591.84,756.031), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(7417.2,-8926.28,264.031), angles = "0 0 0", rotating = false, type = "large" }
	]
	c5m4_quarter =
	[
		{ origin = Vector(-2368.29,3616.46,64.0313), angles = "0 90 0", rotating = true, type = "bh" }
		{ origin = Vector(-639.981,1607.46,224.031), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(-569.655,1031.48,96.0313), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(-3650.66,3888.94,384.031), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(29.7625,-1607.78,287.837), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(-1193.22,1488.05,452.401), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-813.288,-2031.78,422.465), angles = "0 0 0", rotating = false, type = "large" }
	]
	c5m5_bridge =
	[
		{ origin = Vector(-6151.38,6420.53,765.699), angles = "0 90 0", rotating = true, type = "bh" }
		{ origin = Vector(-1954.61,6463.8,852.885), angles = "0 90 0", rotating = false, type = "item" }
		{ origin = Vector(2792.36,6336.46,790.031), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(2792.36,6450.22,790.031), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(2792.36,6211.1,790.031), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(-7031.1,6298.27,456.031), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(-12334.9,6552.2,453.565), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-3862.72,6225.25,897.47), angles = "0 90 0", rotating = false, type = "small" }
		{ origin = Vector(2290.68,6425.58,790.031), angles = "0 90 0", rotating = false, type = "medium" }
		{ origin = Vector(14201,6326.86,790.031), angles = "0 90 0", rotating = false, type = "large" }
	]
	c6m1_riverbank =
	[
		{ origin = Vector(669.412,3116.3,640.031), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(1168,4459.88,510.031), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(2095.14,1641.12,352.031), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(4520.78,2670.4,555.536), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(4461.7,2397.54,224.031), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(-4004.19,554.621,864.031), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-4223.99,1571.49,727.091), angles = "0 0 0", rotating = false, type = "large" }
	]
	c6m2_bedlam =
	[
		{ origin = Vector(2148.07,-1201.23,288.031), angles = "0 90 0", rotating = true, type = "bh" }
		{ origin = Vector(1750.27,3709.59,-275.002), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(1498.32,4839.88,-159.969), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(-224.862,1354.13,28.093), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(1213.93,1966.51,336.031), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(998.689,2701.23,96.0313), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(1589.86,4615.77,32.0313), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(2868.05,5702.03,-1063.97), angles = "0 90 0", rotating = false, type = "large" }
		{ origin = Vector(5883.71,4207.33,-890.867), angles = "0 0 0", rotating = false, type = "large" }
	]
	c6m3_port =
	[
		{ origin = Vector(-2248.68,-646.404,320.031), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(-1761.71,256.046,156.841), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(583.696,1765.25,160.031), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(-1184.55,-952.523,0.03125), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(-788.678,-952.523,0.03125), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(-1379.67,-952.523,0.03125), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(-374.426,-889.011,0.0760522), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(447.796,1824.08,160.031), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-901.026,2123.37,320.031), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(345.633,-358.962,184.031), angles = "0 0 0", rotating = false, type = "small" }
	]
	c7m1_docks =
	[
		{ origin = Vector(14063.8,2301.94,16.2378), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(10153.7,449.507,129.809), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(11410.8,-229.048,-63.9688), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(12001.8,-787.905,-35.1111), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(5208.64,552.395,382.031), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(4679.74,764.071,303.384), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(2516.95,-188.518,138.451), angles = "0 90 0", rotating = false, type = "medium" }
		{ origin = Vector(3404.88,1676.66,336.031), angles = "0 90 0", rotating = false, type = "large" }
	]
	c7m2_barge =
	[
		{ origin = Vector(9920.64,608.323,322.374), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(-3827.21,671.672,342.031), angles = "0 90 0", rotating = false, type = "item" }
		{ origin = Vector(2140.39,1436.81,132.031), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(5435.93,755.192,256.282), angles = "0 90 0", rotating = false, type = "small" }
		{ origin = Vector(10062.7,2095.96,305.796), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-593.486,2559.1,756.694), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-2894.79,725.009,576.031), angles = "0 90 0", rotating = false, type = "large" }
	]
	c7m3_port =
	[
		{ origin = Vector(1052.7,2623.67,544.031), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(-2137.34,-539.338,-95.9688), angles = "0 90 0", rotating = false, type = "item" }
		{ origin = Vector(680.945,2047.86,160.031), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(583.284,1770.19,160.758), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(-938.757,931.624,352.031), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(736.739,2264.6,640.031), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(1560.46,168.305,345.56), angles = "0 90 0", rotating = false, type = "large" }
	]
	c8m1_apartment =
	[
		{ origin = Vector(1811.17,1797.96,640.031), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(896.289,3030.33,637.176), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(2797.04,4191.32,15.8693), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(334.88,767.923,957.29), angles = "0 90 0", rotating = false, type = "small" }
		{ origin = Vector(2624.62,2212.92,945.241), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(1976.26,3944.68,608.031), angles = "0 0 0", rotating = false, type = "large" }
		{ origin = Vector(3493.26,4004.85,1436.03), angles = "0 0 0", rotating = false, type = "tiny" }
	]
	c8m2_subway =
	[
		{ origin = Vector(2937.46,4155.97,-178.67), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(8467.56,3832.97,376.031), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(4526.2,3801.29,-287.969), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(2204.75,3968.89,-335.969), angles = "0 90 0", rotating = false, type = "tiny" }
		{ origin = Vector(6775.15,2899.15,-178.67), angles = "0 90 0", rotating = false, type = "small" }
		{ origin = Vector(7567.9,3417.3,424.031), angles = "0 90 0", rotating = false, type = "small" }
		{ origin = Vector(8328.45,4596.1,1216.03), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(8805.87,5685.34,768.031), angles = "0 0 0", rotating = false, type = "large" }
	]
	c8m3_sewers =
	[
		{ origin = Vector(12631.2,5360.45,957.54), angles = "0 90 0", rotating = true, type = "bh" }
		{ origin = Vector(12712.9,6696.47,800.031), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(13137.6,7424.46,16.0313), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(11203.7,5091.59,712.031), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(11852.1,7923.79,276.031), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(13711,10120.7,-358.28), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(13068.6,10985.1,-191.969), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(13070.8,11448,-277.217), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(13788.6,11101.2,746.031), angles = "0 0 0", rotating = false, type = "large" }
		{ origin = Vector(13198.7,13927.2,5624.03), angles = "0 0 0", rotating = false, type = "large" }
	]
	c8m4_interior =
	[
		{ origin = Vector(12162.3,13317.7,42.4583), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(13687.1,14624.8,576.031), angles = "0 90 0", rotating = false, type = "item" }
		{ origin = Vector(12718.3,14651.8,424.772), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(13695.3,13991.1,5641.54), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(12314.1,13388.4,152.031), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(13439.6,15006.3,624.031), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(14045.5,14862.1,5920.03), angles = "0 0 0", rotating = false, type = "large" }
	]
	c8m5_rooftop =
	[
		{ origin = Vector(7487.75,9322.88,6314.71), angles = "0 90 0", rotating = true, type = "bh" }
		{ origin = Vector(5986.62,7845.9,6210.03), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(7303.02,8911.17,6092.24), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(7048.08,9024.08,6096.03), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(6960.91,9464.52,5644.03), angles = "0 90 0", rotating = false, type = "small" }
		{ origin = Vector(7022.35,7746.25,16.0313), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(7714.38,9340.58,5952.03), angles = "0 0 0", rotating = false, type = "large" }
	]
	c9m1_alleys =
	[
		{ origin = Vector(-8671.74,-9921.56,384.031), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(205.191,-6577.32,-45.685), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(-2334.43,-8371.1,0.03125), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(-7876.03,-10448,192.031), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-6194.29,-10207.2,348.124), angles = "0 90 0", rotating = false, type = "small" }
		{ origin = Vector(-2692.39,-9362.13,362.682), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-1326.74,-3292.39,445.932), angles = "0 0 0", rotating = false, type = "large" }
	]
	c9m2_lots =
	[
		{ origin = Vector(1621.53,-1232.15,186.989), angles = "0 90 0", rotating = true, type = "bh" }
		{ origin = Vector(6858.532,6943.091,223.031), angles = "0 90 0", rotating = false, type = "item" }
		{ origin = Vector(7559.25,6343.99,48.0313), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(7559.25,6680.94,48.0313), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(7535.98,6160.13,427.767), angles = "0 90 0", rotating = false, type = "tiny" }
		{ origin = Vector(3568.14,-492.286,35.807), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(1761.86,218.214,45.0175), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(7844.706,6643.726,375.036), angles = "0 90 0", rotating = false, type = "large" }
	]
	c10m1_caves =
	[
		{ origin = Vector(-11659.9,-13385.5,554.054), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(-12974.9,-6724.85,176.031), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(-12244.6,-5599.09,-73.601), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(-13089.6,-5241.27,-287.969), angles = "0 90 0", rotating = false, type = "tiny" }
		{ origin = Vector(-12349.5,-9805.34,496.031), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(-12974.9,-5860.65,176.031), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-10690.6,-4991.48,688.028), angles = "0 90 0", rotating = false, type = "large" }
	]
	c10m2_drainage =
	[
		{ origin = Vector(-11215.6,-8447.19,-273.116), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(-8220.77,-8322.58,-452.1), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(-9873.86,-7689.84,-376.396), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(-10213.5,-8154.52,-162.483), angles = "0 90 0", rotating = false, type = "tiny" }
		{ origin = Vector(-9872.4,-6733.67,-307.969), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(-7846.5,-6987.36,-457.214), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-8826.14,-7633.1,953.457), angles = "0 0 0", rotating = false, type = "large" }
	]
	c10m3_ranchhouse =
	[
		{ origin = Vector(-9839.35,-6342.99,89.6114), angles = "0 90 0", rotating = true, type = "bh" }
		{ origin = Vector(-4811.54,-1131.53,506.244), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(-9440.1,-2750.96,-38.9688), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(-6958,-1886.3,8.03125), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(-8059.92,-5811.83,400.031), angles = "0 90 0", rotating = false, type = "tiny" }
		{ origin = Vector(-12539.413,-6515.756,408.296), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(-9092.79,-3946.72,356.642), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-5081.95,-1693.59,626.119), angles = "0 0 0", rotating = false, type = "large" }
	]
	c10m4_mainstreet =
	[
		{ origin = Vector(-3068.25,-57.6381,1039.03), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(2790.76,-1699.64,336.031), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(-1375.73,-4670.05,-55.9688), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(-1425.11,-4670.65,-55.9688), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(137.127,-1897.99,112.031), angles = "0 90 0", rotating = false, type = "tiny" }
		{ origin = Vector(2756.35,-2412.78,336.031), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(1617.19,-4384.35,96.0313), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-673.274,-4583.52,176.031), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-1399.49,-4674.15,192.031), angles = "0 0 0", rotating = false, type = "large" }
	]
	c10m5_houseboat =
	[
		{ origin = Vector(2123.12,2634.09,326.222), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(4225.13,-5089.24,-170.896), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(2874.1,2413.57,-39.9688), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(3065.88,2759.15,-39.9688), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(3952.22,256.841,324.537), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(3854.66,4218.71,320.031), angles = "0 90 0", rotating = false, type = "small" }
		{ origin = Vector(4264.85,-4686.83,231.018), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(2195.19,-4720.26,-35.1493), angles = "0 0 0", rotating = false, type = "large" }
	]
	c11m1_greenhouse =
	[
		{ origin = Vector(6418.19,-690.905,831.396), angles = "0 90 0", rotating = true, type = "bh" }
		{ origin = Vector(3427.550,1374.021,819.144), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(3518.21,2217.03,183.338), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(3370.21,731.601,554.228), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(2910.57,2108.9,416.535), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(4351.72,-300.349,1116.8), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(3394.22,-871.949,1029.47), angles = "0 0 0", rotating = false, type = "large" }
	]
	c11m2_offices =
	[
		{ origin = Vector(5488.19,4064.33,434.733), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(7419.72,3330.88,1212.23), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(7513.56,5654.95,16.0313), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(5839.04,3168.24,523.664), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(6272.13,1022.59,16.0313), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(5447.53,3558.41,303.983), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(6623.48,4828.47,600.482), angles = "0 0 0", rotating = false, type = "large" }
	]
	c11m3_garage =
	[
		{ origin = Vector(-3838.76,-3139.08,936.54), angles = "0 90 0", rotating = true, type = "bh" }
		{ origin = Vector(-5872.75,-1719.78,512.031), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(-3488.1,2854.93,32.0313), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(-3647.99,2854.92,31.0313), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(-3565.8,2854.83,32.0313), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(-4952.03,-2623.2,352.031), angles = "0 90 0", rotating = false, type = "tiny" }
		{ origin = Vector(-6805.34,-1273.95,550.072), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(-5239.47,124.543,1301.27), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-2881.81,3153.91,160.031), angles = "0 90 0", rotating = false, type = "large" }
	]
	c11m4_terminal =
	[
		{ origin = Vector(-93.3845,4457.05,144.031), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(98.4939,3620.52,16.0313), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(1163.08,4456.62,296.031), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(710.154,5631.1,296.031), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(541.737,3785.52,536.031), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(471.801,2959.14,348.031), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(2130.92,1586.85,448.031), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(2780.61,6941.86,313.031), angles = "0 0 0", rotating = false, type = "large" }
	]
	c11m5_runway =
	[
		{ origin = Vector(-6838.83,9651.14,568.031), angles = "0 90 0", rotating = true, type = "bh" }
		{ origin = Vector(-5431.83,11812.2,62.8417), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(-5418.15,10341.3,60.0313), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-6900.6,11531.3,-191.969), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(-6891.96,10827.7,-191.969), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-5795.82,9010.43,176.974), angles = "0 0 0", rotating = false, type = "large" }
	]
	c12m1_hilltop =
	[
		{ origin = Vector(-8089.921,-15117.432,277.785), angles = "0 90 0", rotating = true, type = "bh" }
		{ origin = Vector(-6446.82,-8719.63,1015.15), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(-6553.63,-7570.19,384.031), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(-6418.22,-7523.43,365.19), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(-6367.87,-7414.88,377.974), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(-10427.594,-13344.635,746.753), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-10980.1,-10901.5,938.975), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(-8990.35,-8981.87,1062.03), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-7808.35,-9486.53,992.031), angles = "0 0 0", rotating = false, type = "large" }
	]
	c12m2_traintunnel =
	[
		{ origin = Vector(-8229.1,-7517.71,160.353), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(-7719.68,-8901.63,304.031), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(-4604.06,-8322.47,-63.5097), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(-4175.46,-8323.85,-63.9688), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(-3711.27,-8392.14,-63.1514), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(-8740.09,-7214.79,200.031), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-8547.49,-8900.32,304.031), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(-7563.7,-8612.52,826.025), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-4185.79,-8717.72,232.031), angles = "0 90 0", rotating = false, type = "large" }
	]
	c12m3_bridge =
	[
		{ origin = Vector(-1137.81,-10944.3,160.031), angles = "0 90 0", rotating = true, type = "bh" }
		{ origin = Vector(1827.29,-12229.5,484.031), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(1913.03,-12602.3,-31.9688), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(2030.97,-12602.3,-31.9688), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(1970.48,-12602.3,-31.9688), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(-952.414,-10439.4,72.0312), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(3342.01,-14299.5,169.088), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(4914.05,-13113.9,1141.33), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(5932.44,-13851.9,272.05), angles = "0 0 0", rotating = false, type = "large" }
	]
	c12m4_barn =
	[
		{ origin = Vector(9334.13,-9355.23,932.537), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(11022.1,-4584.55,324.481), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(10382,-2600.35,-63.9688), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(10526,-2600.35,-63.9688), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(10453.7,-2600.35,-63.9688), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(7488.13,-10687,897.565), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(10616.6,-7429.15,274.641), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(9688.78,-4243.98,722.381), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(10453.7,-1712.4,268.031), angles = "0 0 0", rotating = false, type = "large" }
	]
	c12m5_cornfield =
	[
		{ origin = Vector(10059.6,821.12,462.47), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(7138.16,270.204,596.031), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(8960.78,3370.82,201.741), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(9060.78,3370.82,204.697), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(9272.22,3547.45,961.057), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(8446.37,422.173,590.031), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(6823.95,1191.26,794.031), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(7186.65,2650.19,1034.12), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(7536.12,2649.97,1034.12), angles = "0 0 0", rotating = false, type = "large" }
	]
	c13m1_alpinecreek =
	[
		{ origin = Vector(-2778.1,1140.71,255.406), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(1200.27,1406.95,1601.38), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(992.23,183.899,585.485), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(1120.63,183.726,585.398), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(-2876.72,2797.21,1273), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-2332,3247.16,976.031), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(869.342,2451.07,805.031), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(880.042,-464,476.011), angles = "0 90 0", rotating = false, type = "large" }
	]
	c13m2_southpinestream =
	[
		{ origin = Vector(7961.18,6390.28,585.92), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(6728,2925.41,1216.03), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(78.6595,8679.27,-404.969), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(8107.63,4172.91,649.555), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(5677.56,2211.83,1090), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(4905.09,2577.37,1120.03), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-356.712,4977.07,272.031), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-348.279,6151.25,302.031), angles = "0 0 0", rotating = false, type = "large" }
	]
	c13m3_memorialbridge =
	[
		{ origin = Vector(-2169.22,-4092.02,1758.03), angles = "0 90 0", rotating = true, type = "bh" }
		{ origin = Vector(-2169.54,-4092.02,2201.03), angles = "0 90 0", rotating = false, type = "item" }
		{ origin = Vector(1003.74,-4472.38,590.031), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(-3844.47,-4095.34,896.031), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(6777.83,-4114.74,2201.03), angles = "0 90 0", rotating = false, type = "small" }
		{ origin = Vector(3687.47,-4074.24,2201.03), angles = "0 90 0", rotating = false, type = "medium" }
		{ origin = Vector(3686.79,-4095.17,896.031), angles = "0 90 0", rotating = false, type = "large" }
	]
	c13m4_cutthroatcreek =
	[
		{ origin = Vector(-3872.6,-8143.45,723.031), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(-772.295,3198.8,-101.969), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(-1174.01,4688.39,180.031), angles = "0 0 0", rotating = false, type = "exp" }
		{ origin = Vector(-3622.38,-5926.86,623.746), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-3916.44,-3227.55,360.898), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(-653.084,1568.25,18.0313), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-391.136,3752.91,88.0313), angles = "0 0 0", rotating = false, type = "large" }
	]
	c14m1_junkyard =
	[
		{ origin = Vector(-4227.94,-8819.65,103), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(-2505.24,1649.99,182.772), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(-4450.59,2204.69,-58.7463), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(-1669.74,-5730.32,-300.582), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(-2676.9,2190.81,-50.752), angles = "0 0 0", rotating = false, type = "small" }
		{ origin = Vector(-4814.390,2711.914,100.450), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-2407,7408.93,168.031), angles = "0 0 0", rotating = false, type = "large" }
	]
	c14m2_lighthouse =
	[
		{ origin = Vector(1319.83,344.463,830.588), angles = "0 0 0", rotating = true, type = "bh" }
		{ origin = Vector(-2479.24,5569.72,-101.291), angles = "0 0 0", rotating = false, type = "item" }
		{ origin = Vector(320.403,923.429,560.905), angles = "0 90 0", rotating = false, type = "exp" }
		{ origin = Vector(-4567.81,3575.97,1400.03), angles = "0 0 0", rotating = false, type = "tiny" }
		{ origin = Vector(479.378,946.381,696.031), angles = "0 90 0", rotating = false, type = "small" }
		{ origin = Vector(-981.672,2524.87,701.112), angles = "0 0 0", rotating = false, type = "medium" }
		{ origin = Vector(-4725,5825,-92.968), angles = "0 0 0", rotating = false, type = "large" }
	]
}




// Output count of mushroom statistics
// ----------------------------------------------------------------------------------------------------------------------------

::map_mushroom_stats <- {}

::outputMushroomData <- function(){
	
	map_mushroom_stats.clear()
	local count_total = 0
	local nl = "\n"
	
	foreach(mapname, mushroomArray in mushroomPositions){
		foreach(DS in mushroomArray){
			local type = DS.type
			if(!(mapname in map_mushroom_stats)){
				map_mushroom_stats[mapname] <- { bh = 0, exp = 0, item = 0, tiny = 0, small = 0, medium = 0, large = 0 }
			}
			map_mushroom_stats[mapname][type] += 1
			count_total += 1
		}
	}
	
	printl("- - - - Mushroom stats - - - -" + nl)
	//
	printl("Total count of placed mushrooms: " + count_total + " ( " + mushroomPositions[mapName].len() + " in this map )" + nl)
	//
	foreach(mapname, datatable in map_mushroom_stats){
		printl("================== " + mapname + " " + GetCharChain("=", 32 - mapname.len() ) + nl)
		printl("BunnyHop: " + datatable.bh + "  Explosive: " + datatable.exp + "  Item: " + datatable.item)
		printl("Tiny: " + datatable.tiny + "  Small: " + datatable.small + "  Medium: " + datatable.medium + "  Large: " + datatable.large + nl)
	}
}




// Returns a concatenated string of the passed string with length of num
// ----------------------------------------------------------------------------------------------------------------------------

::GetCharChain <- function(str, num){
	local txt = ""
	for(local i = 0; i < num; i++){
		txt += str
	}
	return txt
}




// Returns the slot the weapon belongs to
// ----------------------------------------------------------------------------------------------------------------------------

function getItemSlot(item){
	local className = item.GetClassname()
	
	local slot0 =
	[
		"weapon_grenade_launcher","weapon_rifle_m60",
		"weapon_rifle","weapon_rifle_desert","weapon_rifle_ak47",
		"weapon_rifle_sg552","weapon_smg_mp5",
		"weapon_shotgun_chrome","weapon_pumpshotgun",
		"weapon_shotgun_spas","weapon_autoshotgun",
		"weapon_smg","weapon_smg_silenced",
		"weapon_hunting_rifle","weapon_sniper_military",
		"weapon_sniper_scout","weapon_sniper_awp"
	]
	local slot1 = 
	[
		"weapon_melee","weapon_chainsaw",
		"weapon_pistol","weapon_pistol_magnum"
	]
	local slot2 =
	[
		"weapon_molotov","weapon_pipe_bomb","weapon_vomitjar"
	]
	local slot3 =
	[
		"weapon_first_aid_kit","weapon_defibrillator",
		"weapon_upgradepack_explosive","weapon_upgradepack_incendiary"
	]
	local slot4 =
	[
		"weapon_adrenaline","weapon_pain_pills"
	]
	local slot5 = 
	[
		"weapon_oxygentank","weapon_propanetank","weapon_gascan",
		"weapon_gnome","weapon_cola_bottles","weapon_fireworkcrate"
	]

	if(slot0.find(className) != null){
		return "slot0"
	}else if(slot1.find(className) != null){
		return "slot1"
	}else if(slot2.find(className) != null){
		return "slot2"
	}else if(slot3.find(className) != null){
		return "slot3"
	}else if(slot4.find(className) != null){
		return "slot4"
	}else if(slot5.find(className) != null){
		return "slot5"
	}else{
		return null
	}
}




// This will track timings of the survivor
// ----------------------------------------------------------------------------------------------------------------------------

::PlayerTimeData <- {}

function PlayerTimer(ent){

	if(!(ent in PlayerTimeData)){
		PlayerTimeData[ent] <- { timerActive = false, finished = false, startTime = Time(), endTime = Time(), time_best = 0, ticks = 0, seconds = 0 }
	}
	
	if(PlayerIsOnGround(ent)){
		if(ent in PlayerTimeData){
			if(!PlayerTimeData[ent].finished){
				if(PlayerTimeData[ent].ticks < 30){
					PlayerTimeData[ent].ticks += 1
				}else{
					PlayerTimeData[ent].ticks = 0
					PlayerTimeData[ent].seconds += 1
				}
			}
		}
	}
}




// Check if survivor reached the saferoom
// ----------------------------------------------------------------------------------------------------------------------------

function survivorSaferoomCheck(ent){
	if(!PlayerTimeData[ent].finished){
		if( ResponseCriteria.GetValue(ent, "incheckpoint" ) == "1" ){
			printFinalGroundTime(ent)
			ProcessSurvivorTime(ent)
		}
	}
}




// Iteration over all human player and pass them to different methods
// ----------------------------------------------------------------------------------------------------------------------------

function PlayerFunctions(){
	foreach(ent in GetHumanSurvivors()){
		if(ent.IsValid()){
			PlayerTimer(ent)
			survivorSaferoomCheck(ent)
		}
	}
}




// Returns true when player is on the ground
// ----------------------------------------------------------------------------------------------------------------------------

function PlayerIsOnGround(player){
	if(NetProps.GetPropInt(player, "m_fFlags") & 1){
		return true
	}
	return false
}




// Print a chat message in color x 
// ----------------------------------------------------------------------------------------------------------------------------

function toChat(color, message, sound){
	local player = Entities.FindByClassname(null, "player")
	switch(color)
	{
		case "white"	: color = "\x01" ; break
		case "blue"		: color = "\x03" ; break
		case "orange"	: color = "\x04" ; break
		case "green"	: color = "\x05" ; break
	}
	switch(sound)
	{
		case "reward"	: sound = "ui/littlereward.wav" ; break
		case "error"	: sound = "ui/beep_error01.wav" ; break
		case "click"	: sound = "ui/menu_click01.wav" ; break
	}
	ClientPrint(null, 5, color + message)
	if(sound != null){
		EmitAmbientSoundOn( sound, 1, 100, 100, player)
	}
}




// Will change the model of "tank_rock" to a log when it is a L4D1 map
// When "last chance mode" is active, the rock will start glowing
// ----------------------------------------------------------------------------------------------------------------------------

function tankrockListener(){
	local rock = null

	while(rock = Entities.FindByClassname(rock, "tank_rock")){
		if(rock.IsValid()){
			local scope = GetValidatedScriptScope(rock)
			
			if(survivorSet == 1){
				if(!("usesLog" in scope)){
					rock.SetModel("models/props_foliage/tree_trunk.mdl")
					scope["usesLog"] <- true
				}
			}
			
			// When last change mode is active we need to enable the glow on all new rocks
			if(last_chance_active){
				if(!("glowing" in scope)){
					NetProps.SetPropInt(rock, "m_Glow.m_iGlowType", 3)
					rock.GetScriptScope()["glowing"] <- true
				}
			}
		}
	}
}




// Returns the given color with changed intesity as string 
// ----------------------------------------------------------------------------------------------------------------------------

function getColorWithIntensity(color, intensity){	

	local values = split(color, " ")
	
	local rNew = values[0].tofloat()
	local gNew = values[1].tofloat()
	local bNew = values[2].tofloat()

	rNew	= (rNew / 100 * intensity).tointeger()
	gNew	= (gNew / 100 * intensity).tointeger()
	bNew	= (bNew / 100 * intensity).tointeger()

	return "" + rNew + " " + gNew + " " + bNew
}




// Is the current server a local one
// ----------------------------------------------------------------------------------------------------------------------------

function OnLocalServer(){
	if(GetListenServerHost() == null){
		return false
	}
	return true
}

Community Mutations

These mutations were made by the community, eventually added to the base game by Valve.

Community1 - Special Delivery


//-----------------------------------------------------
Msg("Activating Special Delivery\n");
Msg("Made by Rayman1103\n");

if ( !IsModelPrecached( "models/infected/smoker.mdl" ) )
	PrecacheModel( "models/infected/smoker.mdl" );
if ( !IsModelPrecached( "models/infected/smoker_l4d1.mdl" ) )
	PrecacheModel( "models/infected/smoker_l4d1.mdl" );
if ( !IsModelPrecached( "models/infected/boomer.mdl" ) )
	PrecacheModel( "models/infected/boomer.mdl" );
if ( !IsModelPrecached( "models/infected/boomer_l4d1.mdl" ) )
	PrecacheModel( "models/infected/boomer_l4d1.mdl" );
if ( !IsModelPrecached( "models/infected/boomette.mdl" ) )
	PrecacheModel( "models/infected/boomette.mdl" );
if ( !IsModelPrecached( "models/infected/hunter.mdl" ) )
	PrecacheModel( "models/infected/hunter.mdl" );
if ( !IsModelPrecached( "models/infected/hunter_l4d1.mdl" ) )
	PrecacheModel( "models/infected/hunter_l4d1.mdl" );
if ( !IsModelPrecached( "models/infected/limbs/exploded_boomette.mdl" ) )
{
	PrecacheModel( "models/infected/limbs/exploded_boomette.mdl" );
	::community1_no_female_boomers <- true;
}
if ( !IsModelPrecached( "models/infected/spitter.mdl" ) )
	PrecacheModel( "models/infected/spitter.mdl" );
if ( !IsModelPrecached( "models/infected/jockey.mdl" ) )
	PrecacheModel( "models/infected/jockey.mdl" );
if ( !IsModelPrecached( "models/infected/charger.mdl" ) )
	PrecacheModel( "models/infected/charger.mdl" );

MutationOptions <-
{
	ActiveChallenge = 1

	cm_CommonLimit = 0
	cm_DominatorLimit = 8
	cm_MaxSpecials = 8
	cm_ProhibitBosses = false
	cm_SpecialRespawnInterval = 0
	cm_AggressiveSpecials = false

	SpecialInitialSpawnDelayMin = 0
	SpecialInitialSpawnDelayMax = 5
	ShouldAllowSpecialsWithTank = true
	EscapeSpawnTanks = true
	MobMinSize = 0
	MobMaxSize = 0
	NoMobSpawns = true

	SmokerLimit = 2
	BoomerLimit = 2
	HunterLimit = 2
	SpitterLimit = 2
	JockeyLimit = 2
	ChargerLimit = 2

	// convert items that aren't useful
	weaponsToConvert =
	{
		weapon_pipe_bomb =	"weapon_molotov_spawn"
	}

	function ConvertWeaponSpawn( classname )
	{
		if ( classname in weaponsToConvert )
		{
			return weaponsToConvert[classname];
		}
		return 0;
	}
	
	DefaultItems =
	[
		"weapon_pistol_magnum",
	]

	function GetDefaultItem( idx )
	{
		if ( idx < DefaultItems.len() )
		{
			return DefaultItems[idx];
		}
		return 0;
	}
}

MutationState <-
{
	SIModelsBase = [ [ "models/infected/smoker.mdl", "models/infected/smoker_l4d1.mdl" ],
					[ "models/infected/boomer.mdl", "models/infected/boomer_l4d1.mdl", "models/infected/boomette.mdl" ],
						[ "models/infected/hunter.mdl", "models/infected/hunter_l4d1.mdl" ],
							[ "models/infected/spitter.mdl" ],
								[ "models/infected/jockey.mdl" ],
									[ "models/infected/charger.mdl" ] ]
	SIModels = [ [ "models/infected/smoker.mdl", "models/infected/smoker_l4d1.mdl" ],
				[ "models/infected/boomer.mdl", "models/infected/boomer_l4d1.mdl", "models/infected/boomette.mdl" ],
					[ "models/infected/hunter.mdl", "models/infected/hunter_l4d1.mdl" ],
						[ "models/infected/spitter.mdl" ],
							[ "models/infected/jockey.mdl" ],
								[ "models/infected/charger.mdl" ] ]
	ModelCheck = [ false, false, false, false, false, false ]
	LastBoomerModel = ""
	BoomersChecked = 0
	LeftSafeAreaThink = false
}

function LeftSafeAreaThink()
{
	local player = null;
	while ( player = Entities.FindByClassname( player, "player" ) )
	{
		if ( ( !player.IsValid() ) || ( NetProps.GetPropInt( player, "m_iTeamNum" ) != 2 ) )
			continue;
		
		if ( ResponseCriteria.GetValue( player, "instartarea" ) == "0" )
		{
			SessionOptions.cm_MaxSpecials = 8;
			SessionState.LeftSafeAreaThink = false;
			break;
		}
		else
			continue;
	}
}

function OnGameEvent_round_start_post_nav( params )
{
	local spawner = null;
	while ( spawner = Entities.FindByClassname( spawner, "info_zombie_spawn" ) )
	{
		if ( spawner.IsValid() )
		{
			local population = NetProps.GetPropString( spawner, "m_szPopulation" );
			
			if ( population == "boomer" || population == "hunter" || population == "smoker" || population == "jockey"
				|| population == "charger" || population == "spitter" || population == "new_special" || population == "church"
					|| population == "tank" || population == "witch" || population == "witch_bride" || population == "river_docks_trap" )
				continue;
			else
				spawner.Kill();
		}
	}
	
	if ( Director.GetMapName() == "c1m1_hotel" )
		DirectorOptions.cm_TankLimit <- 0;
	else if ( Director.GetMapName() == "c5m5_bridge" || Director.GetMapName() == "c6m3_port" )
		DirectorOptions.cm_MaxSpecials = 0;
	else if ( Director.GetMapName() == "c7m1_docks" )
		DirectorOptions.cm_ProhibitBosses = true;
}

function OnGameEvent_player_left_safe_area( params )
{
	local player = GetPlayerFromUserID( params["userid"] );
	if ( !player )
		return;
	
	local instartarea = ResponseCriteria.GetValue( player, "instartarea" );
	if ( instartarea == "1" )
	{
		SessionOptions.cm_MaxSpecials = 0;
		SessionState.LeftSafeAreaThink = true;
	}
}

function OnGameEvent_triggered_car_alarm( params )
{
	if ( !Director.IsTankInPlay() )
	{
		DirectorOptions.cm_AggressiveSpecials = true;
		ZSpawn( { type = 8 } );
		DirectorOptions.cm_AggressiveSpecials = false;
	}
	
	StartAssault();
}

function OnGameEvent_finale_start( params )
{
	if ( Director.GetMapName() == "c6m3_port" )
		DirectorOptions.cm_MaxSpecials = 8;
}

function OnGameEvent_gauntlet_finale_start( params )
{
	if ( Director.GetMapName() == "c5m5_bridge" )
		DirectorOptions.cm_MaxSpecials = 8;
}

function OnGameEvent_player_spawn( params )
{
	local player = GetPlayerFromUserID( params["userid"] );
	
	if ( ( !player ) || ( player.IsSurvivor() ) )
		return;
	
	local zombieType = player.GetZombieType();
	if ( zombieType > 6 )
		return;
	
	local modelName = player.GetModelName();
	
	if ( !SessionState.ModelCheck[ zombieType - 1 ] )
	{
		if ( (zombieType == 2) && !("community1_no_female_boomers" in getroottable()) )
		{
			if ( SessionState.LastBoomerModel != modelName )
			{
				SessionState.LastBoomerModel = modelName;
				SessionState.BoomersChecked++;
			}
			if ( SessionState.BoomersChecked > 1 )
				SessionState.ModelCheck[ zombieType - 1 ] = true;
		}
		else
			SessionState.ModelCheck[ zombieType - 1 ] = true;
		
		if ( SessionState.SIModelsBase[zombieType - 1].find( modelName ) == null )
		{
			SessionState.SIModelsBase[zombieType - 1].append( modelName );
			SessionState.SIModels[zombieType - 1].append( modelName );
		}
	}
	
	if ( SessionState.SIModelsBase[zombieType - 1].len() == 1 )
		return;
	
	local zombieModels = SessionState.SIModels[zombieType - 1];
	if ( zombieModels.len() == 0 )
		SessionState.SIModels[zombieType - 1].extend( SessionState.SIModelsBase[zombieType - 1] );
	local foundModel = zombieModels.find( modelName );
	if ( foundModel != null )
	{
		zombieModels.remove( foundModel );
		return;
	}
	
	local randomElement = RandomInt( 0, zombieModels.len() - 1 );
	local randomModel = zombieModels[ randomElement ];
	zombieModels.remove( randomElement );
	
	player.SetModel( randomModel );
}

function Update()
{
	if ( SessionState.LeftSafeAreaThink )
		LeftSafeAreaThink();
	if ( Director.GetCommonInfectedCount() > 0 )
	{
		local infected = null;
		while ( infected = Entities.FindByClassname( infected, "infected" ) )
		{
			if ( infected.IsValid() )
				infected.Kill();
		}
	}
}

Community2 - Flu Season


Msg("Activating Community Mutation 2\n");

DirectorOptions <-
{
	ActiveChallenge = 1
	RelaxMinInterval = 5
	RelaxMaxInterval = 10
	cm_SpecialRespawnInterval = 8
	SpecialInitialSpawnDelayMin = 1
	SpecialInitialSpawnDelayMax = 5

	cm_MaxSpecials = 8
	BoomerLimit = 4
	SpitterLimit = 4
	SmokerLimit = 0
	HunterLimit = 0
	ChargerLimit = 0
	JockeyLimit = 0

	ProhibitBosses = 1 //tanks still spawn at finales though
	MegaMobSize = 0

	// convert items that aren't useful
	weaponsToConvert =
	{
		weapon_vomitjar = 	"weapon_molotov_spawn"
	}

	function ConvertWeaponSpawn( classname )
	{
		if ( classname in weaponsToConvert )
		{
			return weaponsToConvert[classname];
		}
		return 0;
	}
}

function OnGameEvent_round_start_post_nav( params )
{
	local spawner = null;
	while ( spawner = Entities.FindByClassname( spawner, "info_zombie_spawn" ) )
	{
		if ( spawner.IsValid() )
		{
			local population = NetProps.GetPropString( spawner, "m_szPopulation" );
			
			if ( population == "boomer" || population == "spitter" || population == "church" || population == "river_docks_trap" )
				continue;
			else
				spawner.Kill();
		}
	}
}

Community3 - Riding My Survivor


Msg("Activating community mutation 3.\n");

DirectorOptions <-
{
	ActiveChallenge = 1
	
	cm_CommonLimit = 0
	
	BoomerLimit = 0
	ChargerLimit = 0
	HunterLimit = 0
	JockeyLimit = 4
	SmokerLimit = 0
	SpitterLimit = 0
	cm_MaxSpecials = 4
	
	cm_SpecialRespawnInterval = 20
	
	function ConvertZombieClass(id)
	{
		return 5;
	}
}

Community4 - Nightmare


//-----------------------------------------------------
Msg("Activating Nightmare\n");


DirectorOptions <-
{
	ActiveChallenge = 1

	cm_DominatorLimit = 8
	cm_MaxSpecials = 8
	cm_SpecialRespawnInterval = 30
	cm_AutoReviveFromSpecialIncap = 1
	cm_AllowPillConversion = 0
	cm_TankLimit = 4
	ProhibitBosses = 0

	BoomerLimit = 0
	SurvivorMaxIncapacitatedCount = 3
	SpecialInitialSpawnDelayMin = 5
	SpecialInitialSpawnDelayMax = 30
	TankHitDamageModifierCoop = 0.25	
}

Community5 - Death's Door


//-----------------------------------------------------
Msg("Activating Death's Door\n");
Msg("Made by Rayman1103\n");

MutationOptions <-
{
	cm_ShouldHurry = true
	cm_AllowPillConversion = false
	cm_AllowSurvivorRescue = false
	SurvivorMaxIncapacitatedCount = 0
	TempHealthDecayRate = 0.0

	function AllowFallenSurvivorItem( classname )
	{
		if ( classname == "weapon_first_aid_kit" )
			return false;

		return true;
	}

	weaponsToConvert =
	{
		weapon_first_aid_kit = "weapon_pain_pills_spawn"
		weapon_adrenaline = "weapon_pain_pills_spawn"
	}

	function ConvertWeaponSpawn( classname )
	{
		if ( classname in weaponsToConvert )
			return weaponsToConvert[classname];

		return 0;
	}

	DefaultItems =
	[
		"weapon_pistol",
		"weapon_pistol",
	]

	function GetDefaultItem( idx )
	{
		if ( idx < DefaultItems.len() )
			return DefaultItems[idx];

		return 0;
	}
}

function OnGameEvent_round_start( params )
{
	Convars.SetValue( "pain_pills_decay_rate", 0.0 );

	for ( local player; player = Entities.FindByClassname( player, "player" ); ) // only works in restarts, which is desired
	{
		if ( player.IsSurvivor() && !IsPlayerABot( player ) )
			player.GetScriptScope().HeartbeatOn = false;
	}
}

function OnGameEvent_player_left_safe_area( params )
{
	SessionOptions.TempHealthDecayRate = 0.27;
}

function OnGameEvent_player_hurt( params )
{
	local player = GetPlayerFromUserID( params["userid"] );
	if ( !player || !player.IsSurvivor() || player.IsHangingFromLedge() )
		return;

	if ( NetProps.GetPropInt( player, "m_bIsOnThirdStrike" ) == 0 )
	{
		local health = player.GetHealth();
		if ( health > 0 )
		{
			if ( !IsPlayerABot( player ) )
			{
				local scope = player.GetScriptScope();
				if ( !scope.HeartbeatOn && health < player.GetMaxHealth() / 4 )
				{
					EmitSoundOnClient( "Player.Heartbeat", player );
					scope.HeartbeatOn = true;
				}
			}
			if ( health == 1 )
			{
				NetProps.SetPropInt( player, "m_bIsOnThirdStrike", 1 );
				NetProps.SetPropInt( player, "m_isGoingToDie", 1 );
			}
		}
	}
}

function OnGameEvent_defibrillator_used( params )
{
	local player = GetPlayerFromUserID( params["subject"] );
	if ( !player )
		return;

	player.SetHealth( 1 );
	player.SetHealthBuffer( 99 );

	if ( !IsPlayerABot( player ) )
	{
		EmitSoundOnClient( "Player.Heartbeat", player );
		player.GetScriptScope().HeartbeatOn = true;
	}
	NetProps.SetPropInt( player, "m_bIsOnThirdStrike", 1 );
	NetProps.SetPropInt( player, "m_isGoingToDie", 1 );
}

function OnGameEvent_heal_success( params )
{
	local player = GetPlayerFromUserID( params["subject"] );
	if ( !player )
		return;

	if ( !IsPlayerABot( player ) )
	{
		local scope = player.GetScriptScope();
		if ( scope.HeartbeatOn && player.GetHealth() >= player.GetMaxHealth() / 4 )
		{
			StopSoundOn( "Player.Heartbeat", player );
			scope.HeartbeatOn = false;
		}
	}
}

function CheckHealthAfterLedgeHang( userid )
{
	local player = GetPlayerFromUserID( userid );
	if ( !player )
		return;

	local health = player.GetHealth();

	if ( !IsPlayerABot( player ) )
	{
		local scope = player.GetScriptScope();
		if ( !scope.HeartbeatOn && health < player.GetMaxHealth() / 4 )
		{
			EmitSoundOnClient( "Player.Heartbeat", player );
			scope.HeartbeatOn = true;
		}
	}
	if ( health == 1 )
	{
		NetProps.SetPropInt( player, "m_bIsOnThirdStrike", 1 );
		NetProps.SetPropInt( player, "m_isGoingToDie", 1 );
	}
}

function OnGameEvent_revive_success( params )
{
	local player = GetPlayerFromUserID( params["subject"] );
	if ( !params["ledge_hang"] || !player )
		return;

	if ( NetProps.GetPropInt( player, "m_bIsOnThirdStrike" ) == 0 )
		EntFire( "worldspawn", "RunScriptCode", "g_ModeScript.CheckHealthAfterLedgeHang(" + params["subject"] + ")" );
}

function OnGameEvent_player_spawn( params )
{
	local player = GetPlayerFromUserID( params["userid"] );
	if ( !player || !player.IsSurvivor() )
		return;

	if ( !IsPlayerABot( player ) )
	{
		player.ValidateScriptScope();
		local scope = player.GetScriptScope();
		if ( !("HeartbeatOn" in scope) )
			scope.HeartbeatOn <- false;
	}
}

function OnGameEvent_player_death( params )
{
	if ( !("userid" in params) )
		return;

	local player = GetPlayerFromUserID( params["userid"] );
	if ( !player || !player.IsSurvivor() )
		return;

	if ( !IsPlayerABot( player ) )
	{
		local scope = player.GetScriptScope();
		if ( scope.HeartbeatOn )
		{
			StopSoundOn( "Player.Heartbeat", player );
			scope.HeartbeatOn = false;
		}
	}
}

function OnGameEvent_player_bot_replace( params )
{
	local player = GetPlayerFromUserID( params["player"] );
	if ( !player || !player.IsSurvivor() ) // in case an infected player uses jointeam 1
		return;

	local scope = player.GetScriptScope();
	if ( scope.HeartbeatOn )
	{
		StopSoundOn( "Player.Heartbeat", player );
		scope.HeartbeatOn = false;
	}
}

function OnGameEvent_bot_player_replace( params )
{
	local player = GetPlayerFromUserID( params["player"] );
	if ( !player )
		return;

	if ( !player.IsDead() ) // in case jointeam 2 or sb_takecontrol was used on a dead bot
	{
		if ( player.GetHealth() < player.GetMaxHealth() / 4 )
			player.GetScriptScope().HeartbeatOn = true; // unreliable if sb_takecontrol was used
		else
			StopSoundOn( "Player.Heartbeat", player );
	}
}

function OnGameEvent_player_complete_sacrifice( params )
{
	local player = GetPlayerFromUserID( params["userid"] );
	if ( !player )
		return;

	NetProps.SetPropInt( player, "m_takedamage", 0 );
	NetProps.SetPropInt( player, "m_isIncapacitated", 1 );
}

if ( !Director.IsSessionStartMap() )
{
	function PlayerSpawnDeadAfterTransition( userid )
	{
		local player = GetPlayerFromUserID( userid );
		if ( !player )
			return;

		player.SetHealth( 24 );
		player.SetHealthBuffer( 26 );

		if ( !IsPlayerABot( player ) )
		{
			EmitSoundOnClient( "Player.Heartbeat", player );
			player.GetScriptScope().HeartbeatOn = true;
		}
	}

	function PlayerSpawnAliveAfterTransition( userid )
	{
		local player = GetPlayerFromUserID( userid );
		if ( !player )
			return;

		local oldHealth = player.GetHealth();
		local maxHeal = player.GetMaxHealth() / 2;
		local healAmount = 0;

		if ( oldHealth < maxHeal )
		{
			healAmount = floor( (maxHeal - oldHealth) * 0.8 + 0.5 );
			player.SetHealth( oldHealth + healAmount );
			local bufferHealth = player.GetHealthBuffer() - healAmount;
			if ( bufferHealth < 0.0 )
				bufferHealth = 0.0;
			player.SetHealthBuffer( bufferHealth );
		}
		NetProps.SetPropInt( player, "m_bIsOnThirdStrike", 0 );
		NetProps.SetPropInt( player, "m_isGoingToDie", 0 );
	}

	function OnGameEvent_player_transitioned( params )
	{
		local player = GetPlayerFromUserID( params["userid"] );
		if ( !player || !player.IsSurvivor() )
			return;

		if ( NetProps.GetPropInt( player, "m_lifeState" ) == 2 )
			EntFire( "worldspawn", "RunScriptCode", "g_ModeScript.PlayerSpawnDeadAfterTransition(" + params["userid"] + ")", 0.03 );
		else
			EntFire( "worldspawn", "RunScriptCode", "g_ModeScript.PlayerSpawnAliveAfterTransition(" + params["userid"] + ")" );
	}
}

Community6 - Confogl


// vim: set ts=4
// CompLite.nut (Confogl Mutation)
// Copyright (C) 2012 ProdigySim
// All rights reserved.
// =============================================================================

IncludeScript("globals", this);

CompLite = InitializeCompLite();

// Don't need to do anything else if we're not first load
if(CompLite.Globals.GetCurrentRound() > 0)
{
	Msg("CompLite Starting Round "+CompLite.Globals.GetCurrentRound()+" on ");
	local mi = CompLite.Globals.MapInfo;
	if(mi.isIntro) Msg("an intro map.\n");
	else Msg("a non-intro map.\n");

	Msg("Found "+mi.saferoomPoints.len()+" saferoom points.\n");
	Msg("Map has a scavenge event? " + mi.hasScavengeEvent + "\n");
	Msg("MapName: "+mi.mapname+"\n");

	return;
}

Msg("Activating Mutation CompLite v3.6\n");

DirectorOptions.ActiveChallenge <- 1
DirectorOptions.cm_ProhibitBosses <- 0
DirectorOptions.cm_AllowPillConversion <- 0

// Name shortening references
local g_Timer = CompLite.Globals.Timer;
local g_FrameTimer = CompLite.Globals.FrameTimer;
local g_MapInfo = CompLite.Globals.MapInfo;
local g_GSC = CompLite.Globals.GSC;
local g_GSM = CompLite.Globals.GSM;
local g_MobResetti = CompLite.Globals.MobResetti;
local Modules = CompLite.Modules;

// Uncomment to add a debug event listener
//g_GSC.AddListener(Modules.MsgGSL());

g_GSC.AddListener(Modules.SpitterControl(Director, DirectorOptions, Entities));
g_GSC.AddListener(Modules.MobControl(g_MobResetti));


// Give out hunting rifles on non-intro maps.
// But limit them to 1 of each.
g_GSC.AddListener(Modules.HRControl(Entities, CompLite.Globals, Director));


g_GSC.AddListener(
	Modules.BasicItemSystems(
		// AllowWeaponSpawn Limits
		// 0: Always remove
		// >0: Keep the first n instances, delete others
		// <-1: Delete the first n instances, keep others.
		{
			weapon_defibrillator = 0
			weapon_grenade_launcher = 0
			weapon_upgradepack_incendiary = 0
			weapon_upgradepack_explosive = 0
			weapon_chainsaw = 0
			//weapon_molotov = 1
			//weapon_pipe_bomb = 2
			//weapon_vomitjar = 1
			weapon_propanetank = 0
			weapon_oxygentank = 0
			weapon_rifle_m60 = 0
			//weapon_first_aid_kit = -5
			upgrade_item = 0
		},
		// Conversion Rules
		{
		weapon_autoshotgun	  = "weapon_pumpshotgun_spawn"
		weapon_shotgun_spas	 = "weapon_shotgun_chrome_spawn"
		weapon_rifle			= "weapon_smg_spawn"
		weapon_rifle_desert	 = "weapon_smg_spawn"
		weapon_rifle_sg552	  = "weapon_smg_mp5_spawn"
		weapon_rifle_ak47	   = "weapon_smg_silenced_spawn"
		weapon_hunting_rifle	= "weapon_smg_silenced_spawn"
		weapon_sniper_military  = "weapon_shotgun_chrome_spawn"
		weapon_sniper_awp	   = "weapon_shotgun_chrome_spawn"
		weapon_sniper_scout	 = "weapon_pumpshotgun_spawn"
		weapon_first_aid_kit	= "weapon_pain_pills_spawn"
		weapon_molotov = "weapon_molotov_spawn"
		weapon_pipe_bomb = "weapon_pipe_bomb_spawn"
		weapon_vomitjar = "weapon_vomitjar_spawn"
		},
		// Default item list
		[
			"weapon_pain_pills",
			"weapon_pistol",
			"weapon_hunting_rifle"
		]
	)
);

// Enforce various item spawns to be single pickup.
g_GSC.AddListener(
	Modules.EntKVEnforcer(Entities,
		// classnames
		[
			"weapon_adrenaline_spawn",
			"weapon_pain_pills_spawn",
			"weapon_melee_spawn",
			"weapon_molotov_spawn",
			"weapon_vomitjar_spawn",
			"weapon_pipebomb_spawn"
		],
		// models
		[],
		// key to enforce
		"count",
		// value to set it to
		1
	)
);

// Entity tracking/removal/limiting
g_GSC.AddListener(
	Modules.ItemControl(Entities, 
	// Limit to value by classname
		{
			weapon_adrenaline_spawn = 1
			weapon_pain_pills_spawn = 4
			witch = 1
			func_playerinfected_clip = 0
			weapon_molotov_spawn = 1
			weapon_pipe_bomb_spawn = 1
			weapon_vomitjar_spawn = 1
		},
	// Limit to value by model name
		{
			["models/props_junk/propanecanister001a.mdl"] = 0,
			["models/props_equipment/oxygentank01.mdl"] = 0,
			["models/props_junk/explosive_box001.mdl"] = 1
		},
	// Remove these items from all saferooms
		[
			"weapon_adrenaline_spawn",
			"weapon_pain_pills_spawn",
			//"weapon_melee_spawn",
			"weapon_molotov_spawn",
			"weapon_pipe_bomb_spawn",
			"weapon_vomitjar_spawn"
		],
		g_MapInfo
	)
);

// Limit melee weapons to 4
g_GSC.AddListener(Modules.MeleeWeaponControl(Entities, 4));

// Remove all gascans on non-scavenge maps
g_GSC.AddListener(Modules.GasCanControl(Entities, g_MapInfo));


Msg("GSC/M/L Script run.\n");

Confogl Additional Scripts

These scripts are included in the main Confogl and globals scripts.

globals

// Call this once on script start to initialize CompLite's libraries and hook them up to ChallengeScript events.
// parentTable: The parent table where the CompLite library namespace should be created. This 
//		should be a table which will not be wiped on roundstart. getroottable() is a good idea, defaults to getroottable();
// customNameSpace: The custom name to use for the CompLite Libraries' namespace. Should be passed as a string. Defaults to "CompLite"
// ChallengeScript: Pass a reference to the ChallengeScript table. Defaults to ::DirectorScript.MapScript.ChallengeScript
function InitializeCompLite(parentTable = getroottable() , customNameSpace = "CompLite", ChallengeScript = ::DirectorScript.MapScript.ChallengeScript )
{
	if(customNameSpace in parentTable)
	{
		local CompLite = parentTable[customNameSpace];
		CompLite.Globals.IncrementRoundNumber();
		CompLite.Globals.GSM.Reset();

		if(CompLite.Globals.GetCurrentRound() == 1)
		{
			CompLite.Globals.MapInfo.IdentifyMap(Entities);
		}

		ChallengeScript.DirectorOptions <- CompLite.ChallengeScript.DirectorOptions;
		ChallengeScript.Update <- CompLite.ChallengeScript.Update;
		return CompLite;
	}
	
	local CompLite = parentTable[customNameSpace] <- {};
	
	IncludeScript("gamestate_model", CompLite);
	IncludeScript("globaltimers", CompLite);
	IncludeScript("utils", CompLite);
	IncludeScript("modules", CompLite);
	
	CompLite.ChallengeScript <- {
		CompLite = CompLite
		DirectorOptions = {
			CompLite = CompLite
			function AllowWeaponSpawn( classname ) 
			{ 
				return CompLite.Globals.GSM.OnAllowWeaponSpawn(classname);
			}
			function ConvertWeaponSpawn( classname ) 
			{ 
				return CompLite.Globals.GSM.OnConvertWeaponSpawn(classname);
			}
			function GetDefaultItem( idx ) 
			{
				return CompLite.Globals.GSM.OnGetDefaultItem(idx);
			}
			function ConvertZombieClass( id ) 
			{ 
				return CompLite.Globals.GSM.OnConvertZombieClass(id);
			}
		}

		function Update()
		{
			CompLite.Globals.Timer.Update();
			CompLite.Globals.FrameTimer.Update();
			CompLite.Globals.GSM.DoFrameUpdate();
		}
	}

	CompLite.Globals <- CompLiteGlobals(CompLite, Director, CompLite.ChallengeScript.DirectorOptions);

	ChallengeScript.DirectorOptions <- CompLite.ChallengeScript.DirectorOptions;
	ChallengeScript.Update <- CompLite.ChallengeScript.Update;

	CompLite.Globals.MapInfo.IdentifyMap(Entities);

	return CompLite;
}

class CompLiteGlobals {
	constructor(NameSpace, director, dopts)
	{
		Timer = NameSpace.Timers.GlobalSecondsTimer();
		FrameTimer = NameSpace.Timers.GlobalFrameTimer();
		MapInfo = NameSpace.Utils.MapInfo();
		GSC = NameSpace.GameState.GameStateController();
		GSM = NameSpace.GameState.GameStateModel(GSC, director);
		MobResetti = NameSpace.Utils.ZeroMobReset(director, dopts, FrameTimer);
	}

	function IncrementRoundNumber() { m_iRoundNumber++; }
	function GetCurrentRound() { return m_iRoundNumber; }
	//public
	Timer = null;
	FrameTimer = null;
	MapInfo = null;
	GSM = null;
	GSC = null;
	MobResetti = null;
	
	// private
	m_iRoundNumber = 0;
}
gamestate_model

// vim: set ts=4
// L4D2 GameState Model for Mutation VScripts
// Copyright (C) 2012 ProdigySim
// All rights reserved.
// =============================================================================

// double include protection
if("GameState" in this) return;
GameState <- {
	ROUNDSTART_DELAY_INTERVAL = 2
};
IncludeScript("utils", this);

class GameState.GameStateModel
{
	constructor(controller, director)
	{
		m_controller = controller;
		m_pDirector = director;
	}

	function DoFrameUpdate()
	{
		if(m_bLastUpdateTankInPlay)
		{
			if(!m_pDirector.IsTankInPlay())
			{
				m_bLastUpdateTankInPlay = false;
				m_controller.TriggerTankLeavesPlay();
			}
		}
		else if(m_pDirector.IsTankInPlay())
		{
			m_bLastUpdateTankInPlay = true;
			m_controller.TriggerTankEntersPlay();
		}
		if(!m_bLastUpdateSafeAreaOpened && m_pDirector.HasAnySurvivorLeftSafeArea())
		{
			m_bLastUpdateSafeAreaOpened = true;
			m_controller.TriggerSafeAreaOpen();
		}
		if(!m_bRoundStarted && m_bHeardAWS && m_bHeardCWS && m_bHeardGDI 
			&& m_iRoundStartTime < Time()-m_roundstart_delay)
		{
			m_bRoundStarted = true;
			m_controller.TriggerRoundStart(GetCurrentRound());
		}
	}
	function OnAllowWeaponSpawn( classname )
	{
		m_bHeardAWS = true;
		m_iRoundStartTime = Time();
		return m_controller.TriggerAllowWeaponSpawn(classname);
	}
	function OnConvertWeaponSpawn( classname )
	{
		m_bHeardCWS = true;
		m_iRoundStartTime = Time();
		return m_controller.TriggerConvertWeaponSpawn(classname);
	}
	function OnGetDefaultItem( idx )
	{
		m_bHeardGDI = true;
		m_iRoundStartTime = Time();
		return m_controller.TriggerGetDefaultItem(idx);
	}
	function OnConvertZombieClass(id)
	{
		return m_controller.TriggerPCZSpawn(id);
	}

	function Reset()
	{
		m_bRoundStarted = false;
		m_bHeardAWS = false;
		m_bHeardCWS = false;
		m_bHeardGDI = false;
		m_iRoundStartTime = 0;
		m_bLastUpdateSafeAreaOpened = false;
	}

	// Check for various round-start even. ts before triggering OnRoundStart()
	m_bRoundStarted = false;
	m_bHeardAWS = false;
	m_bHeardCWS = false;
	m_bHeardGDI = false;
	m_iRoundStartTime = 0;

	m_bLastUpdateTankInPlay = false;
	m_bLastUpdateSafeAreaOpened = false;
	
	m_controller = null;
	m_pDirector = null;
	m_roundstart_delay = GameState.ROUNDSTART_DELAY_INTERVAL;
	static GetCurrentRound = Utils.GetCurrentRound;
};

class GameState.GameStateListener
{
	// Called on round start. These may be multiples of these triggered, unfortunately.
	function OnRoundStart(roundNumber) {}
	// Called when a player leaves saferoom or the saferoom timer counts down
	function OnSafeAreaOpened() {}
	// Called when tank spawns
	function OnTankEntersPlay() {}
	// Called when tank dies/leaves play
	function OnTankLeavesPlay() {}
	// Called when a player-controlled zombie is going to be spawned via ConvertZombieClass
	// This event will be chained--called on all Listeners with the modified id passed into successive calls.
	// id: SIClass id of the PCZ to be spawned
	// return another SIClass value to convert the PCZ spawn.
	function OnSpawnPCZ(id) {}
	// Called when a player-controlled zombie is going to be spawned via ConvertZombieClass
	// After conversions from OnSpawnPCZ have taken place
	// id: actual SIClass id to be spawned
	function OnSpawnedPCZ(id) {}
	// Called when DirectorOptions.GetDefaultItem() is called.
	// This event chain will notify all listeners, but only one return value will be used.
	// TODO: further abstraction to just have lists returned...
	// Should be at the beginning of the round normally.
	function OnGetDefaultItem(idx) {}
	// Called when DirectorOptions.AllowWeaponSpawn() is called. 
	// Should be at the beginning of the round normally, after conversions take place.
	// This event will stop being called on Listeners when one listener returns false.
	// classname: string classname of weapon to allow/disallow
	// return true to allow, false to disallow.
	function OnAllowWeaponSpawn(classname) {}
	// Called when DirectorOptions.ConvertWeaponSpawn is called
	// This event will be chained--called on all Listeners with the modified classname passed into successive calls.
	// classname: Classname of the weapon that would spawned
	// retun the classname that it should be converted to, or 0 for no conversion.
	function OnConvertWeaponSpawn(classname) {}
};

class GameState.GameStateController
{
	function AddListener(listener)
	{
		m_listeners.push(listener)
	}

	function TriggerRoundStart(roundNumber)
	{
		foreach(listener in m_listeners)
			listener.OnRoundStart(roundNumber);
	}
	function TriggerSafeAreaOpen()
	{
		foreach(listener in m_listeners)
			listener.OnSafeAreaOpened();
	}
	function TriggerTankEntersPlay()
	{
		foreach(listener in m_listeners)
			listener.OnTankEntersPlay();
	}
	function TriggerTankLeavesPlay()
	{
		foreach(listener in m_listeners)
			listener.OnTankLeavesPlay();
	}
	function TriggerPCZSpawn(id)
	{
		local retval = id;
		foreach(listener in m_listeners)
		{
			// Allow each listener to try to convert.
			// Not pretty in the long run but I'm okay with it.
			local ret = listener.OnSpawnPCZ(retval);
			if(ret != null) retval = ret;
		}

		// Simply notify everyone of the final value
		foreach(listener in m_listeners)
			listener.OnSpawnedPCZ(retval)
		return retval;
	}
	function TriggerAllowWeaponSpawn(classname)
	{
		foreach(listener in m_listeners)
		{
			// Cancel call chain once one listener returns false (says not to spawn it).
			if(listener.OnAllowWeaponSpawn(classname) == false) return false;
			// Interesting ! semantics
			//             null  false
			// !ret        true  true
			// ret==false  false true
			// ret==null   true  false
		}
		return true;
	}
	function TriggerConvertWeaponSpawn(classname)
	{
		local retcls = 0;
		foreach(listener in m_listeners)
		{
			local ret = listener.OnConvertWeaponSpawn(classname);
			if(ret != null && ret != 0) retcls = ret;
		}
		return retcls;
	}
	function TriggerGetDefaultItem(idx)
	{
		local retval = 0;
		foreach(listener in m_listeners)
		{
			local ret = listener.OnGetDefaultItem(idx);
			if(retval == 0 && ret != null) retval = ret;
		}
		return retval;
	}

	m_listeners = []
};
globaltimers

// vim: set ts=4
// Global Timers for L4D2 VScript Mutations
// Copyright (C) 2012 ProdigySim
// All rights reserved.
// =============================================================================


/* Usage:
	Create an instance of one of these timers, e.g.
	g_Timer = GlobalTimer()
	and/or
	g_FrameTimer = GlobalFrameTimer()
	
	Then run the timer's Update() function in your global "update" function.
	e.g. 
	function Update()
	{
		g_Timer.Update();
		g_FrameTimer.Update();
	}
	
	To add a timer, extend TimerCallback to create a callback.
	e.g.
	class MyCallback extends TimerCallback
	{
		function OnTimerElapsed()
		{
			Msg("My Timer has elapsed!!!\n");
		}
	}
	
	Then register it to the global timer of your choice,
	
 */

// double include protection
if("Timers" in this) return;

Timers <- {};

class Timers.TimerCallback
{
	/* OnTimerElapsed()
	Executed once the timer is elapsed in a GlobalTimer
	 */
	function OnTimerElapsed() {}
};

class Timers.GlobalTimer
{
	constructor()
	{
		m_callbacks = array(0);
		m_cbtimes = array(0);
	}
	// Returns the current time in some format that supports arithmetic operations
	// Overload this in your final class
	function GetCurrentTime() { assert(null) }
	/* Update()
	Checks to see which timers have elapsed.
	Please run on global frame Update() function
	 */
	function Update()
	{
		while(m_cbtimes.len() && m_cbtimes[0] <= GetCurrentTime())
		{
			//Msg("Executing timer at "+GetCurrentTime()+" ("+Time()+") elapsed "+m_cbtimes[0]+"\n");
			local cb = m_callbacks[0];
			m_callbacks.remove(0);
			m_cbtimes.remove(0);
			cb.OnTimerElapsed();
		}
	}
	
	/* AddTimer(time,timer)
	Register a new timed callback.
	time: Time in seconds to wait before executing the timer callback.
	timer: TimerCallback to execute once the timer has elapsed.
	 */
	function AddTimer(time, timer)
	{
		local cbtime = GetCurrentTime() + time;
		//Msg("Adding time at "+GetCurrentTime()+" ("+Time()+") for "+cbtime+"\n");
		// Insert sorted Ascending by end timestamp (cbtime) into callbacks list
		local i = 0;
		// TODO: Binary search
		while(i < m_callbacks.len() && m_cbtimes[i] < cbtime) i++;
		if(i == m_callbacks.len())
		{
			m_callbacks.push(timer);
			m_cbtimes.push(cbtime);
		}
		else
		{
			m_callbacks.insert(i,timer);
			m_cbtimes.insert(i,cbtime);
		}
	}

	m_callbacks = [];
	m_cbtimes = [];
};

class Timers.GlobalSecondsTimer extends Timers.GlobalTimer
{
	// Returns the current time in seconds (internal use)
	function GetCurrentTime() { return Time(); }
};

class Timers.GlobalFrameTimer extends Timers.GlobalTimer
{
	// Returns the current time in frames (internal use)
	function GetCurrentTime()
	{
		return m_curFrame;
	}
	/* Update()
	Checks to see which timers have elapsed and executes callbacks.
	Please run on global frame Update() function
	 */
	function Update()
	{
		// TODO: Integer overflow after 180+ hours on the round?
		m_curFrame++;
		baseclass.Update();
	}
	m_curFrame = 0;
	// Need a name reference to base class
	static baseclass = Timers.GlobalTimer;
};
utils

// vim: set ts=4
// Utilities for L4D2 Vscript Mutations
// Copyright (C) 2012 ProdigySim
// All rights reserved.
// =============================================================================


if("Utils" in this) return;
Utils <- {
	SIClass = {
		Smoker = 1
		Boomer = 2
		Hunter = 3
		Spitter = 4
		Jockey = 5
		Charger = 6
		Witch = 7
		Tank = 8
	}
	SIModels = [
		"", // null entry
		["models/infected/smoker.mdl"],
		["models/infected/boomer.mdl", "models/infected/boomette.mdl"],
		["models/infected/hunter.mdl"],
		["models/infected/spitter.mdl"],
		["models/infected/jockey.mdl"],
		["models/infected/charger.mdl"],
		["models/infected/witch.mdl", "models/infected/witch_bride.mdl"],
		["models/infected/hulk.mdl", "models/infected/hulk_dlc3.mdl"]
	]
	SurvivorModels = {
		coach = "models/survivors/survivor_coach.mdl"
		ellis = "models/survivors/survivor_mechanic.mdl"
		nick = "models/survivors/survivor_gambler.mdl"
		rochelle = "models/survivors/survivor_producer.mdl"
		louis = "models/survivors/survivor_manager.mdl"
		bill = "models/survivors/survivor_namvet.mdl"
		francis = "models/survivors/survivor_biker.mdl"
		zoey = "models/survivors/survivor_teenangst.mdl"
	}
	MeleeModels = [
		"models/weapons/melee/w_bat.mdl",
		"models/weapons/melee/w_chainsaw.mdl"
		"models/weapons/melee/w_cricket_bat.mdl",
		"models/weapons/melee/w_crowbar.mdl",
		"models/weapons/melee/w_didgeridoo.mdl",
		"models/weapons/melee/w_electric_guitar.mdl",
		"models/weapons/melee/w_fireaxe.mdl",
		"models/weapons/melee/w_frying_pan.mdl",
		"models/weapons/melee/w_golfclub.mdl",
		"models/weapons/melee/w_katana.mdl",
		"models/weapons/melee/w_machete.mdl",
		"models/weapons/melee/w_riotshield.mdl",
		"models/weapons/melee/w_tonfa.mdl"
	]
};
IncludeScript("globaltimers", this);

/* KeyReset
	Create a KeyReset to track the state of a key before you change its value, and
	reset it to the original value/state when you want to revert it.
	Can detect whether a key existed or not and will delete the key afterwards if it doesn't exists.
	
	e.g.
	myKeyReset = KeyReset(DirectorOptions, "JockeyLimit")
	
	then on some event...
	myKeyReset.set(0); // Set DirectorOptions.JockeyLimit to 0, storing the previous value/state
	
	and later...
	myKeyReset.unset(); // Reset DirectorOptions.JockeyLimit to whatever value it was before, or delete
	

 */

// Class that will detect the existence and old value of a key and store
// it for "identical" resetting at a later time.
// Assumes that while between Set() and Unset() calls no other entity will modify the
// value of this key.
class Utils.KeyReset
{
	constructor(owner, key)
	{
		m_owner = owner;
		m_key = key;
	}
	function set(val)
	{
		if(!m_bSet)
		{
			m_bExists = m_owner.rawin(m_key);
			if(m_bExists)
			{
				m_oldVal = m_owner.rawget(m_key);
			}
			m_bSet = true;
		}
		m_owner.rawset(m_key,val);
	}
	function unset()
	{
		if(!m_bSet) return;
		
		if(m_bExists)
		{
			m_owner.rawset(m_key,m_oldVal);
		}
		else
		{
			m_owner.rawdelete(m_key);
		}
		m_bSet = false;
	}
	m_owner = null;
	m_key = null;
	m_oldVal = null;
	m_bExists = false;
	m_bSet = false;
};


/* ZeroMobReset
	Class which handles resetting the mob timer without spawning CI.
	
	e.g.
	g_MobTimerCntl = ZeroMobReset(Director, DirectorOptions, g_FrameTimer);
	
	then later on some event
	g_MobTimerCntl.ZeroMobReset();
	

 */
// Can reset the mob spawn timer at any point without
// triggering an CI to spawn. Should not demolish any other state settings.
class Utils.ZeroMobReset extends Timers.TimerCallback
{
	// Initialize with Director, DirectorOptions, and a GlobalFrameTimer
	constructor(director, dopts, timer)
	{
		m_director = director;
		m_timer = timer;
		m_mobsizesetting = KeyReset(dopts, "MobSpawnSize");
	}
	/* ZeroMobReset()
	Resets the director's mob timer.
	Will trigger incoming horde music, but will not spawn any commons.
	 */
	function ZeroMobReset()
	{
		if(m_bResetInProgress) return;
		
		// set DirectorOptions.MobSpawnSize to 0 so the triggered
		// horde won't spawn CI
		m_mobsizesetting.set(0);
		m_director.ResetMobTimer();
		m_timer.AddTimer(1, this)
		m_bResetInProgress = true;
	}
	// Internal use only,
	// resets the mob size setting after the mob timer has been set
	function OnTimerElapsed()
	{
		m_mobsizesetting.unset();
		m_bResetInProgress = false;
	}
	m_bResetInProgress = false;
	m_director = null;
	m_timer = null;
	m_mobsizesetting = null;
	static KeyReset = Utils.KeyReset;
};

class Utils.Sphere {
	constructor(center, radius)
	{
		m_vecOrigin = center;
		m_flRadius = radius;
	}
	function GetOrigin()
	{
		return m_vecOrigin();
	}
	function GetRadius()
	{
		return m_flRadius;
	}
	// point: vector
	function ContainsPoint(point)
	{
		return (m_vecOrigin - point).Length() <= m_flRadius;
	}
	function ContainsEntity(entity)
	{
		return ContainsPoint(entity.GetOrigin());
	}
	m_vecOrigin = null;
	m_flRadius = null;
};

class Utils.MapInfo {
	function IdentifyMap(EntList)
	{
		isIntro = EntList.FindByName(null, "fade_intro") != null
			|| EntList.FindByName(null, "lcs_intro") != null;

		// also will become true in scavenge gamemode!
		hasScavengeEvent = EntList.FindByClassname(null, "point_prop_use_target") != null;

		saferoomPoints = [];

		if(isIntro)
		{
			local ent = EntList.FindByName(null, "survivorPos_intro_01");
			if(ent != null) saferoomPoints.push(ent.GetOrigin());
		}

		local ent = null;
		while((ent = EntList.FindByClassname(ent, "prop_door_rotating_checkpoint")) != null)
		{
			saferoomPoints.push(ent.GetOrigin());
		}

		if(IsMapC1M2(EntList)) mapname = "c1m2_streets";
		else mapname = "unknown";
	}
	function IsPointNearAnySaferoom(point, distance=2000.0)
	{
		// We actually check if any saferoom is near the point...
		local sphere = Sphere(point, distance);
		foreach(pt in saferoomPoints)
		{
			if(sphere.ContainsPoint(pt)) return true;
		}
		return false;
	}
	function IsEntityNearAnySaferoom(entity, distance=2000.0)
	{
		return IsPointNearAnySaferoom(entity.GetOrigin(), distance);
	}
	function IsMapC1M2(EntList)
	{
		// Identified by a entity with a given model at a given point
		local ent = EntList.FindByModel(null, "models/destruction_tanker/c1m2_cables_far.mdl");
		if(ent != null 
			&& (ent.GetOrigin() - Vector(-6856.0,-896.0,384.664)).Length() < 1.0) return true;
		return false;
	}
	isIntro = false
	isFinale = false
	hasScavengeEvent = false;
	saferoomPoints = null;
	mapname = null
	chapter = 0
	Sphere = Utils.Sphere;
};

class Utils.VectorClone {
	constructor(vec)
	{
		x=vec.x;
		y=vec.y;
		z=vec.z;
	}
	function ToVector()
	{
		return Vector(x,y,z);
	}
	x=0.0
	y=0.0
	z=0.0
};

class Utils.ItemInfo {
	constructor(ent)
	{
		m_vecOrigin = VectorClone(ent.GetOrigin());
		//m_vecForward = ent.GetForwardVector();
	}
	m_vecOrigin = null;
	//m_vecForward = null;
	static VectorClone = Utils.VectorClone;
};

Utils.KillEntity <- function (ent)
{
	DoEntFire("!self", "kill", "", 0, null, ent);
}

Utils.ArrayToTable <- function (arr)
{
	local tab = {};
	foreach(str in arr) tab[str] <- 0;
	return tab;
}

Utils.ArrayRemoveByValue <- function (arr, value)
{
	foreach(id,val in arr)
	{
		if(val == value)
		{
			arr.remove(id);
			break;
		}
	}
}

// return index on found
// return -1 on not found
Utils.ArraySearchByValue <- function (arr, value)
{
	foreach(id,val in arr)
	{
		if(val == value)
		{
			return id;
			break;
		}
	}
	return -1;
}

Utils.IsEntityInMoveHeirarchy <- function (moveChildEnt, moveParentCandidate)
{
	local curEnt = moveChildEnt;
	while(curEnt != null)
	{
		curEnt = curEnt.GetMoveParent();
		if(curEnt == moveParentCandidate) return true;
	}
	return false;
}

// TODO move/refactor...
Utils.GetCurrentRound <- function () 
{ 
	return ::CompLite.Globals.GetCurrentRound();
}
modules

// vim: set ts=4
// CompLite Mutation Modules
// Copyright (C) 2012 ProdigySim
// All rights reserved.
// =============================================================================

if("Modules" in this) return;
Modules <- {};
IncludeScript("gamestate_model", this);
IncludeScript("utils", this);


class Modules.MsgGSL extends GameState.GameStateListener
{
	function OnRoundStart(roundNumber) { Msg("MsgGSL: OnRoundStart("+roundNumber+")\n"); }
	function OnSafeAreaOpened() { Msg("MsgGSL: OnSafeAreaOpened()\n"); }
	function OnTankEntersPlay() { Msg("MsgGSL: OnTankEntersPlay()\n"); }
	function OnTankLeavesPlay() { Msg("MsgGSL: OnTankLeavesPlay()\n"); }
	function OnSpawnPCZ(id) { Msg("MsgGSL: OnSpawnPCZ("+id+")\n"); }
	function OnSpawnedPCZ(id) { Msg("MsgGSL: OnSpawnedPCZ("+id+")\n"); }
	function OnGetDefaultItem(idx)
	{
		if(idx == 0) 
		{
			Msg("MsgGSL: OnGetDefaultItem(0) #"+m_defaultItemCnt+"\n");
			m_defaultItemCnt++;
		}
	}
	// Too much spam for these
	/*
	function OnAllowWeaponSpawn(classname) {}
	function OnConvertWeaponSpawn(classname) {}
	*/
	m_defaultItemCnt = 0;
};

class Modules.SpitterControl extends GameState.GameStateListener
{
	constructor(director, director_opts, entlist)
	{
		m_pDirector = director;
		m_pSpitterLimit = KeyReset(director_opts, "SpitterLimit");
		m_pEntities = entlist;
		// Initialize to default order...
		SpawnLastUsed = [1, 2, 3, 5, 6];
	}
	function OnTankEntersPlay()
	{
		m_pSpitterLimit.set(0);
	}
	function OnTankLeavesPlay()
	{
		m_pSpitterLimit.unset();
	}
	// Check if there is an instance of this SI on the map
	function IsGivenSIClassSpawned(id)
	{
		foreach(mdl in SIModels[id])
		{
			if(m_pEntities.FindByModel(null, mdl) != null)
			{
				return true;
			}
		}
		return false;
	}
	function OnSpawnPCZ(id)
	{
		// If a spitter is going to be spawned during tank,
		if(id == SIClass.Spitter && m_pDirector.IsTankInPlay())
		{
			foreach(si in SpawnLastUsed)
			{
				// Note: Player keeps SI model until they receive a new spawn
				// (while dead, they have the model of their last SI class)
				if(!IsGivenSIClassSpawned(si)) return si;
			}
			// default to hunter if we really can't pick another class...
			return SIClass.Hunter;
		}
		// Msg("Spawning SI Class "+newClass+".\n");
		return id;
	}
	function OnSpawnedPCZ(id)
	{
		// Mark that this SI to be spawned is most recently spawned now.
		if(id != SIClass.Spitter && id <= SIClass.Charger && id >= SIClass.Smoker)
		{
			// Low index = least recent
			// High index = most recent
			// Remove the other instance of this class in our array
			ArrayRemoveByValue(SpawnLastUsed, id);
			SpawnLastUsed.push(id);
		}
	}
	// List of last spawned time for each SI class
	SpawnLastUsed = null;
	// reference to director options
	m_pSpitterLimit = null;
	m_pDirector = null;
	m_pEntities = null;
	static KeyReset = Utils.KeyReset;
	static SIClass = Utils.SIClass;
	static SIModels = Utils.SIModels;
	static ArrayRemoveByValue = Utils.ArrayRemoveByValue;
};


class Modules.MobControl extends GameState.GameStateListener
{
	constructor(mobresetti)
	{
		//m_dopts = director_opts;
		m_resetti = mobresetti;
	}
	function OnSafeAreaOpened() 
	{
		m_resetti.ZeroMobReset();
	}
	// These functions created major problems....
	/*
	function OnTankEntersPlay()
	{
		m_oldMinTime = m_dopts.MobSpawnMinTime;
		m_oldMaxTime = m_dopts.MobSpawnMaxTime;

		m_dopts.MobSpawnMinTime = 99999;
		m_dopts.MobSpawnMaxTime = 99999;

		m_resetti.ZeroMobReset();
	}
	function OnTankLeavesPlay()
	{
		m_dopts.MobSpawnMinTime = m_oldMinTime;
		m_dopts.MobSpawnMaxTime = m_oldMaxTime;

		m_resetti.ZeroMobReset();
	} 
	m_oldMinTime = 0;
	m_oldMaxTime = 0; 
	m_dopts = null; */
	m_resetti = null;
};

class Modules.BasicItemSystems extends GameState.GameStateListener
{
	constructor(removalTable, convertTable, defaultItemList)
	{
		m_removalTable = removalTable;
		m_convertTable = convertTable;
		m_defaultItemList = defaultItemList;
	}
	function OnAllowWeaponSpawn(classname)
	{
		if ( classname in m_removalTable )
		{
			if(m_removalTable[classname] > 0)
			{
				//Msg("Found a "+classname+" to keep, "+m_removalTable[classname]+" remain.\n");
				m_removalTable[classname]--
			}
			else if (m_removalTable[classname] < -1)
			{
				//Msg("Killing just one "+classname+"\n");
				m_removalTable[classname]++
				return false;
			}
			else if (m_removalTable[classname] == 0)
			{
				//Msg("Removed "+classname+"\n")
				return false;
			}
		}
		return true;
	}
	function OnConvertWeaponSpawn(classname)
	{
		if ( classname in m_convertTable )
		{
			//Msg("Converting"+classname+" to "+convertTable[classname]+"\n")
			return m_convertTable[classname];
		}
		return 0;
	}
	function OnGetDefaultItem(idx)
	{
		if ( idx < m_defaultItemList.len())
		{
			return m_defaultItemList[idx];
		}
		return 0;
	}
	m_removalTable = null;
	m_convertTable = null;
	m_defaultItemList = null;
};

class Modules.EntKVEnforcer extends GameState.GameStateListener
{
	constructor(EntList, classes, models, key, value)
	{
		m_pEntities = EntList;
		m_classes = classes;
		m_models = models;
		m_key = key;
		switch(typeof value)
		{
			case "bool":
				m_value = value.tointeger();
				m_setFunc = "__KeyValueFromInt";
				break;
			case "float":
				m_value = value.tointeger();
				m_setFunc = "__KeyValueFromInt";
				break;
			case "integer":
				m_value = value;
				m_setFunc = "__KeyValueFromInt";
				break;
			case "string":
				m_value = value;
				m_setFunc = "__KeyValueFromString";
				break;
			case "instance":
				if(value.getclass() == ::Vector)
				{
					m_value = value;
					m_setFunc = "__KeyValueFromVector";
					break;
				}
			default:
				m_value = null;
				m_setFunc = null;
				// Unsupported type!!!
				throw "Unsupported type "+(typeof value)+" used with EntKVEnforcer!";
		}
	}
	function OnRoundStart(roundNumber)
	{
		local ent = null;

		while((ent = m_pEntities.Next(ent)) != null)
		{
			if(ent.GetClassname() in m_classes)
			{
				ent[m_setFuncName].call(ent, m_key, m_value);
			}
		}

		foreach(mdl in m_models)
		{
			ent = null;
			while((ent = m_pEntities.FindByModel(mdl)) != null)
			{
				ent[m_setFuncName].call(ent, m_key, m_value);
			}
		}
	}
	m_pEntities = null;
	m_classes = null;
	m_models = null;
	m_key = null;
	m_value = null;
	m_setFunc = null;
};

class Modules.ItemControl extends GameState.GameStateListener
{
	constructor(entlist, removalTable, modelRemovalTable, saferoomRemoveList, mapinfo)
	{
		m_entlist = entlist;
		m_removalTable = removalTable;
		m_modelRemovalTable = modelRemovalTable;
		m_saferoomRemoveList = ArrayToTable(saferoomRemoveList);
		m_pMapInfo = mapinfo;
	}
	function OnFirstRound()
	{
		local ent = m_entlist.First();
		local classname = "";
		local tItemEnts = {};
		local saferoomEnts = [];

		// Create an empty array for each item in our list.
		foreach(key,val in m_removalTable)
		{
			tItemEnts[key] <- [];
		}

		while(ent != null)
		{
			classname = ent.GetClassname()
			if(classname in m_saferoomRemoveList && m_pMapInfo.IsEntityNearAnySaferoom(ent, 1600.0))
			{
				// Make a list of items which are in saferooms that need to be removed
				// and don't track these entities for other removal.
				saferoomEnts.push(ent.weakref());
			}
			else if(classname in m_removalTable)
			{
				tItemEnts[classname].push(ent.weakref());
			}
			ent=m_entlist.Next(ent);
		}

		local tModelEnts = {};

		foreach(mdl,limit in m_modelRemovalTable)
		{
			local thisMdlEnts = tModelEnts[mdl] <- [];
			ent = null;
			while((ent = m_entlist.FindByModel(ent, mdl)) != null)
			{
				// Only use this entity if it's not one of the saferoom entities we're going to remove.
				thisMdlEnts.push(ent.weakref());
			}
		}

		// Remove all targeted saferoom items before doing roundstart removals
		foreach(entity in saferoomEnts) KillEntity(entity);
		local KilledEntList = saferoomEnts;

		m_firstRoundEnts = {}
		foreach(classname,instances in tItemEnts)
		{
			local cnt = m_removalTable[classname].tointeger();
			local saved_ents = m_firstRoundEnts[classname] <- [];
			// We need to choose certain items to save
			while( instances.len() > 0 && saved_ents.len() < cnt )
			{
				local saveIdx = RandomInt(0,instances.len()-1);
				// Track this entity's info for future rounds.
				saved_ents.push(ItemInfo(instances[saveIdx]));
				// Remove this entity from the kill list
				instances.remove(saveIdx);
			}
			Msg("Killing "+instances.len()+" "+classname+", leaving "+saved_ents.len()+" on the map.\n");
			foreach(inst in instances)
			{
				KillEntity(inst);
				KilledEntList.push(inst.weakref());
			}
		}

		m_firstRoundModelEnts = {}
		foreach(model,instances in tModelEnts)
		{
			// Don't use killed ents!
			foreach(deadEnt in KilledEntList) ArrayRemoveByValue(instances, deadEnt);
			local limit = m_modelRemovalTable[model].tointeger();
			local saved_ents = m_firstRoundModelEnts[model] <- [];
			while( instances.len() > 0 && saved_ents.len() < limit )
			{
				local saveIdx = RandomInt(0,instances.len()-1);
				// Track this entity's info for future rounds.
				saved_ents.push(ItemInfo(instances[saveIdx]));
				// Remove this entity from the kill list
				instances.remove(saveIdx);
			}
			Msg("Killing "+instances.len()+" "+model+", leaving "+saved_ents.len()+" on the map.\n");
			foreach(inst in instances)
			{
				KillEntity(inst);
			}
		}
	}
	function OnLaterRounds()
	{
		local ent = m_entlist.First();
		local classname = "";
		local tItemEnts = {};

		foreach(key,val in m_removalTable)
		{
			tItemEnts[key] <- [];
		}
		while(ent != null)
		{
			classname = ent.GetClassname()
			if(classname in m_removalTable)
			{
				tItemEnts[classname].push(ent.weakref());
			}
			ent=m_entlist.Next(ent);
		}

		local tModelEnts = {};
		foreach(mdl,limit in m_modelRemovalTable)
		{
			local thisMdlEnts = tModelEnts[mdl] <- [];
			ent = null;
			while((ent = m_entlist.FindByModel(ent, mdl)) != null)
			{
				// Only use this entity if it's not one of the saferoom entities we're going to remove.
				thisMdlEnts.push(ent.weakref());
			}
		}

		foreach(classname,entList in tItemEnts)
		{
			local firstItems = m_firstRoundEnts[classname];
			// count to keep alive
			local cnt = firstItems.len();
			if(cnt > entList.len())
			{
				Msg("Warning! Not enough "+classname+" spawned this round to match R1! ("+entList.len()+" < "+cnt+")\n");
				cnt = entList.len();
			}

			for(local i = cnt; i < entList.len(); i++)
			{
				KillEntity(entList[i]);
			}

			for(local i = 0; i < cnt; i++)
			{
				local vec = VectorClone(firstItems[i].m_vecOrigin);

				// Hack. To avoid crashing the server by placing entities on top of each other here, 
				// we'll just offset the placement of each entity by 1 unit. 
				// Alternative solutions: 
				// 1. Check all tracked entity positions for conflicts and resolve through <insert algorithm here>
				// 2. Move all entities to offset by 1 unit, wait 1 frame, move entities to original (un-offset) position (same frame also crashes)
				// 3. Convert this code to kill/create entities instead of kill/shuffle entities (not possible atm)
				vec.z += 1.0;
				entList[i].SetOrigin(vec.ToVector());
				//entList[i].SetForwardVector(firstItems[i].m_vecForward.ToVector());
			}
			Msg("Restored "+cnt+" "+classname+", out of "+entList.len()+" on the map.\n");
		}

		foreach(model,entList in tModelEnts)
		{
			local firstItems = m_firstRoundModelEnts[model];
			// count to keep alive
			local cnt = firstItems.len();
			if(cnt > entList.len())
			{
				Msg("Warning! Not enough "+model+" spawned this round to match R1! ("+entList.len()+" < "+cnt+")\n");
				cnt = entList.len();
			}

			for(local i = cnt; i < entList.len(); i++)
			{
				KillEntity(entList[i]);
			}

			for(local i = 0; i < cnt; i++)
			{
				entList[i].SetOrigin(firstItems[i].m_vecOrigin.ToVector());
				//entList[i].SetForwardVector(firstItems[i].m_vecForward.ToVector());
			}
			Msg("Restored "+cnt+" "+model+", out of "+entList.len()+" on the map.\n");
		}
	}
	function OnRoundStart(roundNumber)
	{
		Msg("ItemControl OnRoundStart()\n");
		// This will run multiple times per round in certain cases...
		// Notably, on natural map switch (transition) e.g. chapter 1 ends, start chapter 2.
		// Just make sure you don't screw up anything...
		if(roundNumber == 1)
		{
			OnFirstRound();
		}
		else
		{
			OnLaterRounds();
		}


	}
	// pointer to global Entity List
	m_entlist = null;
	// point to global mapinfo
	m_pMapInfo = null;
	// Table of entity classname, limit value pairs
	m_removalTable = null;
	m_modelRemovalTable = null;
	m_saferoomRemoveList = null;

	m_firstRoundEnts = null;
	m_firstRoundModelEnts = null;
	static ArrayToTable = Utils.ArrayToTable;
	static ArrayRemoveByValue = Utils.ArrayRemoveByValue;
	static ItemInfo = Utils.ItemInfo;
	static KillEntity = Utils.KillEntity;
	static VectorClone = Utils.VectorClone;
};

class Modules.MeleeWeaponControl extends GameState.GameStateListener {
	constructor(entlist, melee_limit)
	{
		m_pEntities = entlist;
		m_maxSpawns = melee_limit;
	}
	function EntListToItemInfoList(entlist)
	{
		local infolist = [];
		foreach (ent in entlist)
		{
			infolist.push(ItemInfo(ent));
		}
		return infolist;
	}
	function OnFirstRound()
	{
		m_firstRoundMelees = {};
		local meleeEnts = {}
		local totalCount = 0;

		// Enumerate all melee weapon spawns by model
		foreach(mdl in MeleeModels)
		{
			// prep table for later
			m_firstRoundMelees[mdl] <- []

			local spawnlist = []
			local ent = null;
			while((ent = m_pEntities.FindByModel(ent, mdl)) != null)
			{
				spawnlist.push(ent.weakref());
				totalCount++;
			}
			meleeEnts[mdl] <- spawnlist;
		}

		if(totalCount < m_maxSpawns)
		{
			// There are less than m_maxSpawns melee weapons on the map,
			// so we record them all and we're done.
			foreach(mdl,spawnlist in meleeEnts)
			{
				m_firstRoundMelees[mdl] = EntListToItemInfoList(spawnlist);
			}
			Msg("Only "+totalCount+" melee weapons on the map to track.\n");
		}
		else
		{
			local savedCnt = 0;

			// Save m_maxSpawns of them
			while(savedCnt < m_maxSpawns)
			{
				local saveIdx = RandomInt(0,totalCount-1);

				// Iterate through the list until we've hit saveIdx melees.
				foreach(mdl, spawnlist in meleeEnts)
				{
					if(saveIdx < spawnlist.len())
					{
						// Save this item's spawn info.
						m_firstRoundMelees[mdl].push(ItemInfo(spawnlist[saveIdx]));
						spawnlist.remove(saveIdx);
						savedCnt++;
						totalCount--;
						break;
					}
					saveIdx -= spawnlist.len();
				}
			}

			// remove the remaining weapon spawns from the map.
			foreach(mdl, spawnlist in meleeEnts)
			{
				foreach(melee_ent in spawnlist)
				{
					KillEntity(melee_ent);
				}
			}
			Msg("Killing "+totalCount+" melee weapons and saving "+savedCnt+" out of "+(totalCount+savedCnt)+" on the map.\n");
		}

	}
	function OnOtherRounds()
	{
		local meleeEnts = {}
		local totalCount = 0;

		// Enumerate all melee weapon spawns by model
		foreach(mdl in MeleeModels)
		{
			local spawnlist = []
			local ent = null;
			while((ent = m_pEntities.FindByModel(ent, mdl)) != null)
			{
				spawnlist.push(ent.weakref());
				totalCount++;
			}
			meleeEnts[mdl] <- spawnlist;
		}

		foreach(mdl,infolist in m_firstRoundMelees)
		{
			local restoreCnt = infolist.len();

			// Make sure this model isn't unset in this round's ent table
			if(!(mdl in meleeEnts))
			{
				Msg("Warning! No "+ mdl +" exist on R2 for restoring!\n");
				continue;
			}

			local thisMdlEnts = meleeEnts[mdl];

			// Check that this round's ent list is long enough to spawn last round's melees
			if(thisMdlEnts.len() < restoreCnt)
			{
				restoreCnt = thisMdlEnts.len();
				Msg("Warning! Not as many of melee weapon ("+ mdl +") available on R2! ("+restoreCnt+" < "+infolist.len()+"\n");
			}

			Msg("Restoring "+restoreCnt+" "+mdl+" out of "+thisMdlEnts.len()+"\n");

			// Move restoreCnt melees of this model to their spots from R1.
			for(local i = 0; i < restoreCnt; i++)
			{
				local ent = thisMdlEnts[0];
				thisMdlEnts.remove(0);
				ent.SetOrigin(infolist[i].m_vecOrigin.ToVector());
				//ent.SetForwardVector(infolist[i].m_vecForward.ToVector());
			}
		}

		// Delete the remaining melees from this round.
		foreach(mdl,spawnlist in meleeEnts)
		{
			foreach(ent in spawnlist)
			{
				KillEntity(ent);
			}
		}
	}
	function OnRoundStart(roundNumber)
	{
		if(roundNumber == 1)
		{
			OnFirstRound();
		}
		else
		{
			OnOtherRounds();
		}
	}
	m_pEntities = null;
	m_maxSpawns = null;

	m_firstRoundMelees = null;

	static MeleeModels = Utils.MeleeModels;
	static ItemInfo = Utils.ItemInfo;
	static KillEntity = Utils.KillEntity;
};

class Modules.HRControl extends GameState.GameStateListener //, extends TimerCallback (no MI support)
{
	constructor(entlist, globals, director)
	{
		m_pEntities = entlist;
		m_pGlobals = globals;
		m_pDirector = director;
	}
	function QueueCheck(time)
	{
		if(!m_bChecking)
		{
			m_pGlobals.Timer.AddTimer(time, this);
			m_bChecking = true;
		}
	}
	function OnRoundStart(roundNumber)
	{
		QueueCheck(1.0);
	}
	function OnTimerElapsed()
	{
		m_bChecking=false;
		if(!m_pDirector.HasAnySurvivorLeftSafeArea()) QueueCheck(5.0);

		local ent = null;
		local hrList = [];
		while((ent = m_pEntities.FindByClassname(ent, "weapon_hunting_rifle")) != null)
		{
			hrList.push(ent.weakref());
		}

		if(!m_pGlobals.MapInfo.isIntro)
		{
			if(hrList.len() <= 1) return;
			hrList.remove(RandomInt(0,hrList.len()-1));
		}

		// Delete the rest
		foreach(hr in hrList)
		{
			KillEntity(hr);
		}
	}
	m_pEntities = null;
	m_pTimer = null;
	m_pGlobals = null;
	m_pDirector = null;
	m_bChecking = false;
	static KillEntity = Utils.KillEntity;
};

class Modules.GasCanControl extends GameState.GameStateListener {
	constructor(entList, mapinfo)
	{
		m_pEntities = entList;
		m_pMapInfo = mapinfo;
	}
	function OnRoundStart(roundNumber)
	{
		// Don't demolish gascans if the map has a scavenge event!
		// Unless it's c1m2 because it uses cola yo.
		if(m_pMapInfo.hasScavengeEvent && m_pMapInfo.mapname != "c1m2_streets") return;

		local ent = null;
		local list = [];
		while((ent = m_pEntities.FindByModel(ent, "models/props_junk/gascan001a.mdl")) != null)
		{
			list.push(ent.weakref());
		}
		Msg("Killing "+list.len()+" gas cans from the map.\n");
		foreach(can in list)
		{
			KillEntity(can);
		}
	}
	m_pEntities = null;
	m_pMapInfo = null;
	static KillEntity = Utils.KillEntity;
}

EMS Mutations

These have been introduced with the L4D2 EMS Update

Dash


////////////////////////////////
// "Dash" is another extended mutation demo mode to show some of the capabilities available in script
//
// the players must run near a sequence of waypoints (well, ok, lampposts) in order
// As they reach each one, we light up the next one, and also summon a new panic wave from the next locale
// Designers use the Entity Placement tool to lay down a sequence of waypoints
// Waypoint naming:
// If you name your waypoints sequentially, e.g., waypoint_001, waypoint_002 flagpole waypoints will be used.
// See scripted_c5m2_park_dash.nut, c5m2_park_entities_dash.txt for an example.
//
// If you prefix your waypoints with "short_" e.g., short_waypoint_001, short_waypoint_002, then shorter
// waypoints will be used.
// See scripted_c5m4_quarter_dash.nut, c5m4_quarter_entities_dash.txt for an example.
//
// When survivors reach the final waypoint, we do a TANK wave, and they get a final time based on when they finish
// 
// The waypoint itself has a fairly big script to detect nearby players, and track which ones have reached it
//   as well as to trigger visual indications of progress, and "waypoint finished" calls back to this script
// And this script on startup has to sort the waypoints to make the in-order list

// printl(" Loading Dash mode")

MutationOptions <-
{   
	// since we are going to move the spawn point to "ahead" on the track as we hit waypoints
	PreferredMobDirection = SPAWN_NEAR_POSITION
	PreferredMobPositionRange = 600

	// we should change this to zero
	PreferredMobPosition = Vector( 0,0,0 )

	SpawnSetRule = SPAWN_POSITIONAL
	SpawnSetRadius = 2000
	SpawnSetPosition = Vector( -7150, -3647, -97 )

	WanderingZombieDensityModifier = 0 // get rid of wanderers

	cm_ShouldEscortHumanPlayers = 1
	cm_AggressiveSpecials = 1

	BoomerLimit  = 0
	ChargerLimit = 0
	HunterLimit  = 0
	JockeyLimit  = 0
	SpitterLimit = 0
	SmokerLimit  = 0
	MaxSpecials  = 0
	CommonLimit  = 20
	MegaMobSize	 = 20
	TankLimit    = 0
	WitchLimit	 = 0

	function EndScriptedMode()
	{
		local finished = true;
		foreach ( val in g_RoundState.WaypointList )
		{
			if ( val.GetScriptScope().active == true )
			{
				finished = false
				break;
			}
		}

		if ( finished )
		{
			// Finished the course
			return 0 // SCENARIO_RESTART
		}
		else
		{
			return 1 // SCENARIO_SURVIVORS_DEAD
		}
	}

	/*  Not using this method anymore
	function GetScoreboardFilename( endreason )
	{
		switch ( endreason )
		{
			case 1:
				return "Resource/UI/scriptedmodeplaceholderlost.res"
			case 0:
				return "Resource/UI/scriptedmodeplaceholderwon.res"
			default:
				return "Resource/UI/scriptedmodeplaceholder.res"
		}
	}
	*/

	// challenge mode experiment - make sure old CM type stuff works fine in new mutation code
	cm_HeadshotOnly = 0

	DefaultItems =
	[
		"weapon_rifle_m60",
		"weapon_pistol_magnum",
		"adrenaline",
		"pipe_bomb"
	]

	// Set characters up with default items
	function GetDefaultItem( idx )
	{
		if ( idx < DefaultItems.len() )
		{
			return DefaultItems[idx];
		}
		return 0;
	}	
}

// SessionState for this mode - mostly about the waypoint tracking/which on next/etc/etc
MutationState <-
{
	StartActive = true	
	CurrentWaypoint = 0
	FinalWaypoint = 0
	JustHitWaypoint = false
	YouWon = false
	FirstWaypointHit = false
	TimerWaypoint = 0   // for a soon to be written more flexible setup
	MobWaypoint = 0
	CommonIncrement = 0
}

// callback for waypoint generation - as we spawn each one we add it to the list
function SetWaypointCB( waypointObj, rarity )
{
	local waypointScr = waypointObj.GetScriptScope()
	waypointScr.myID <- g_ModeScript.MutationState.FinalWaypoint++

	
	// if we're using a custom list set the initial touch count on this waypoint
	if( "CustomWaypointList" in g_MapScript )
		waypointScr.initialTouchCount = g_MapScript.CustomWaypointList[ g_RoundState.WaypointList.len() ].startingTouchCount

	g_RoundState.WaypointList.append(waypointObj)
}

// the startbox needs a custom precache function if you are using it
function Precache()
{
	Startbox_Precache()
}

// called on startup - setup a few callbacks if they arent there
// and then go ahead and teleport players to start, spawn the startbox, activate first waypoint
function OnGameplayStart()
{
	CheckOrSetMapCallback( "MapOverrideOptions", @() false )
	CheckOrSetMapCallback( "MapGameplayStart", @() false )

	Scoring_LoadTable( SessionState.MapName, SessionState.ModeName )

	//teleport players to the start point
	if (!TeleportPlayersToStartPoints( "gamemode_playerstart" ) )
		printl(" ** TeleportPlayersToStartPoints: Spawn point count or player count incorrect! Verify that there are 4 of each.")

	// create a start box
	if ( !SpawnStartBox( "startbox_origin" ) )
	{
		printl("Note: SpawnStartBox() called but there is no startbox_origin in map.")
	}

	// If the map script supplies a CustomWaypointList of points spawn the entities in that order
	if( "CustomWaypointList" in g_MapScript )
	{
		SpawnCustomWaypointList()
	}
	else
	{
		// Spawn waypoints in order
		SortAndSpawnWaypointList()
	}

	if (g_RoundState.WaypointList.len() > 0)
	{
		EntFire( g_RoundState.WaypointList[0].GetName(), "StartGlowing" )
		EntFire( g_RoundState.WaypointList[0].GetName(), "fireuser1" )
		g_RoundState.WaypointList[0].GetScriptScope().active = true
		SessionOptions.PreferredMobPosition = g_RoundState.WaypointList[0].GetOrigin()
	}

	SessionOptions.ScriptedStageType = STAGE_SETUP
	SessionOptions.ScriptedStageValue = 1000

	MapGameplayStart()
}

//=========================================================
// Uses the CustomWaypointList defined in the map script to
// spawn waypoints.  The info_item_position entities are
// collected in the order they appear in the list and then
// the waypoints are spawned in that order.
//=========================================================
function SpawnCustomWaypointList()
{
	// collect the info_item_position entities into a list
	local currentEnt  = null
	local tempWaypointList = []

	foreach( idx, val in CustomWaypointList )
	{
		currentEnt = Entities.FindByName( null, val.targetName )
		
		// store the waypoint if we found it, otherwise complain
		if( currentEnt )
		{
			tempWaypointList.append( currentEnt )
		}
		else
		{
			printl( " ** Waypoint Spawn ERROR**  I can't find the waypoint named: " + val.targetName )
		}
	}

	SpawnWaypointList( tempWaypointList )
}

//=========================================================
// Collect all the info_item_position entities that are named 
// "short_waypoint_*" OR "waypoint_*", put them into a list
// and sort it by suffix.  This will allow us to spawn all
// the waypoints in order so they can know their touch order
//=========================================================
function SortAndSpawnWaypointList()
{
	local currentWaypoint = Entities.FindByClassname( null, "info_item_position" )
	local tempWaypointList = []

	while( currentWaypoint )
	{
		local name = currentWaypoint.GetName()
		if( ( name.find( "waypoint_" ) == 0 ) || ( name.find( "short_waypoint_" ) == 0 ) )
		{
			tempWaypointList.append( currentWaypoint )
		}

		currentWaypoint = Entities.FindByClassname( currentWaypoint, "info_item_position" )
	}

	// sort the list by suffix	
	tempWaypointList.sort(@(a,b) a.GetName().slice(-4) <=> b.GetName().slice(-4) )

	SpawnWaypointList( tempWaypointList )
}

//=========================================================
// Spawn the provided list of waypoints on their positions
//=========================================================
function SpawnWaypointList( list )
{
	local waypointGroup = g_MapScript.GetEntityGroup( "DashWaypoint" )
	local shortWaypointGroup = g_MapScript.GetEntityGroup( "DashWaypointShort" )

	// set callbacks
	shortWaypointGroup.SpawnTables[ "waypoint" ].PostPlaceCB <- SetWaypointCB
	waypointGroup.SpawnTables[ "waypoint" ].PostPlaceCB <- SetWaypointCB

	// spawn the waypoints	
	foreach( idx, ent in list )
	{
		// does our waypoint start with "short_"?  If so, it is a short waypoint! Spawn it.
		if( ent.GetName().find( "short_" ) == 0  )
		{
			SpawnSingleAt( shortWaypointGroup, ent.GetOrigin(), ent.GetAngles() )
		}
		else // assuming it must be a normal waypoint since it isn't "short_"
		{
			SpawnSingleAt( waypointGroup, ent.GetOrigin(), ent.GetAngles() )
		}
	}
}

function DashDisplayScores( )
{
	local final_time = HUDReadTimer(1)
	local score_strings = Scoring_AddScoreAndBuildStrings( Scoring_MakeName(), final_time )
	HUDPlace( HUD_TICKER, 0.32, 0.27, 0.36, 0.20 )
	HUDPlace( HUD_FAR_RIGHT, 0.28, 0.50, 0.44, 0.06 )
	HUDPlace( HUD_FAR_LEFT,  0.28, 0.58, 0.44, 0.06 )
	HUDPlace( HUD_LEFT_BOT,  0.28, 0.66, 0.44, 0.06 )
	HUDPlace( HUD_RIGHT_BOT, 0.28, 0.74, 0.44, 0.06 )
	local ticker_str = score_strings.yourtime
	if ("finish" in score_strings)
		ticker_str = ticker_str + "\n" + score_strings.finish
	ticker_str = ticker_str + "\nBest Times So Far"
	Ticker_SetBlink( false )   // or true if you came in first? hmmm....
	Ticker_NewStr( ticker_str, 120 )
	foreach (idx, val in score_strings.topscores)
	{
		local hudfield = "score"+(idx+1)
		DashHUD.Fields[hudfield].dataval = val
		DashHUD.Fields[hudfield].flags = DashHUD.Fields[hudfield].flags & ~HUD_FLAG_NOTVISIBLE
	}
	Scoring_SaveTable( SessionState.MapName, SessionState.ModeName )
}

// this array holds the waypoints in order, up to SessionState.FinalWaypoint (though really, could just use len() for safety?
g_RoundState.WaypointList <- []

// called at basically each waypoint via ForceNextStage - or when a PANIC ends we just go to a delay
function GetNextStage()
{
	smDbgPrint("GetNextStage called")

	if ( SessionState.YouWon )
	{
		smDbgPrint("Really winningz, going to rezultz, yo!")
		SessionOptions.ScriptedStageType = STAGE_RESULTS
		SessionOptions.ScriptedStageValue = 10
		DashDisplayScores()
	}
	else if ( SessionState.JustHitWaypoint && SessionState.FinalWaypoint > 0 )
	{	
		if ( !SessionState.FirstWaypointHit ) // could be start line, or waypoint 0
		{  // hit firstone, kick things off
			smDbgPrint("Hit First Waypoint!")
			SessionState.FirstWaypointHit = true
			HUDManageTimers( 1 , TIMER_COUNTUP, 0)   // generalize this?

			// calculate the number of common infected to add each time a waypoint is completed
			SessionState.CommonIncrement = (100 - SessionOptions.CommonLimit) / SessionState.FinalWaypoint
		}  
		
		if( !MapOverrideOptions() )
		{
			// really, do we need this? or can we just always do panic here else a delay???
			local PercentageComplete = SessionState.CurrentWaypoint * 1.0 / SessionState.FinalWaypoint
		
			SessionState.JustHitWaypoint = false
			SessionOptions.ScriptedStageType = STAGE_PANIC
			SessionOptions.ScriptedStageValue = 1
			foreach (val in SpecialNames)
			{
				local limit = RandomInt(1,3) + PercentageComplete * 3
				SessionOptions[val + "Limit"] = limit
			}
			SessionOptions.MaxSpecials = RandomInt(2,6) + PercentageComplete * 3
			if (SessionOptions.CommonLimit < 100)
				SessionOptions.CommonLimit += SessionState.CommonIncrement

			if (SessionOptions.MegaMobSize < 50 )
				SessionOptions.MegaMobSize += SessionState.CommonIncrement

			if (SessionState.CurrentWaypoint == SessionState.FinalWaypoint - 1 )
			{
				Ticker_NewStr("One gate to go!")
				SessionOptions.TankLimit = 1 // how do i force a tank from script?
				SessionOptions.ScriptedStageType = STAGE_ESCAPE  // i guess i do an escape stage
			}
			else // display some help text on the ticker
			{
				switch( SessionState.CurrentWaypoint )
				{
					case 1:
						Ticker_NewStr( "Each time you activate a new waypoint more infected will attack.", 15 )
						break
					
					case 2:
						Ticker_NewStr( "You cleared the waypoint! Here come more infected!", 15 )
						break
				}
			}
		}
	}
	else
	{
		SessionOptions.ScriptedStageType = STAGE_DELAY
		SessionOptions.ScriptedStageValue = 1000
		SessionOptions.MaxSpecials = 0
	}

	smDbgPrint("Dash setting Mode " + SessionOptions.ScriptedStageType + " val " + SessionOptions.ScriptedStageValue)

	// we also want to positionally spawn at the mob target - though really i'd like to do some distance/scale stuff, too
	SessionOptions.SpawnSetPosition = SessionOptions.PreferredMobPosition
}

// because call DWD is where we active/deactive the waypoints, we can just treat the start line as secret pre-waypoint
function SurvivorLeftStartBox()
{
	SessionState.JustHitWaypoint = true   // and since the main loop never looks up into the array, that is o.k.
	Director.ForceNextStage()
}

// each time a waypoint is hit this is called to get us moving forward
// i.e. we need to activate the next waypoint, see if it is the final one, track counts, and so on
function DashWaypointDone( id )
{
	smDbgPrint("Dash Waypoint finished! " + id)

	// sanity check!
	if ( id != SessionState.CurrentWaypoint )
		printl("Hey! you think you are done with waypoint " + id + " but current is " + SessionState.CurrentWaypoint )

	EntFire( g_RoundState.WaypointList[id].GetName(), "StopGlowing" )
	g_RoundState.WaypointList[id].GetScriptScope().active = false
	// printl( "Deactivated waypoint " + id )
	local next_id = ++SessionState.CurrentWaypoint
	if ( next_id >= SessionState.FinalWaypoint )
	{
		// printl("Youz da Winner, yo!")
		SessionState.CurrentWaypoint--   // for safety
		SessionState.YouWon = true

		// stop the clock
		HUDManageTimers( 1, TIMER_STOP, 0 )
	}
	else
	{
		EntFire( g_RoundState.WaypointList[next_id].GetName(), "StartGlowing" )
		EntFire( g_RoundState.WaypointList[next_id].GetName(), "fireuser1" )
		//printl("Moving on to next waypoint " + next_id )
		g_RoundState.WaypointList[next_id].GetScriptScope().active = true
		SessionOptions.PreferredMobPosition = g_RoundState.WaypointList[next_id].GetOrigin()
	}
	SessionState.JustHitWaypoint = true
	Director.ForceNextStage()
}

// add Criteria for the new Response Rule speech criteria system
// i.e. this is to add data to the RR table the characters use to choose statements
function AddCriteria( criteriaTable )
{
	criteriaTable.PercentComplete <- SessionState.FinalWaypoint > 0 ? (SessionState.CurrentWaypoint*1.0)/SessionState.FinalWaypoint : 0
//	printl("Dash AddCriteria added " + criteriaTable.PercentComplete)
}

//-------------------------------------------------------
// Include all entity group interfaces needed for this mode
//-------------------------------------------------------
IncludeScript( "entitygroups/dash_waypoint_group" )
IncludeScript( "entitygroups/dash_waypoint_short_group" )

DashHUD <- {}

// HUD setup/control - our HUD is pretty simple in dash
function SetupModeHUD( )
{
	DashHUD =
	{
		Fields = 
		{
			waypoint = { slot = HUD_RIGHT_TOP, name = "waypoint", staticstring = "Waypoint: ", datafunc = @() SessionState.CurrentWaypoint },
			timer    = { slot = HUD_LEFT_TOP, name = "timer", staticstring = "Time: ", special = HUD_SPECIAL_TIMER1 },

			// this is kinda a lie! we are really going to move these w/HUDPlace for displaying final scores!
			// we will move the Ticker down to be a "header" - then need 4 slots for top 4
			score1   = { slot = HUD_FAR_RIGHT, name = "score1", dataval = "Score1", flags = HUD_FLAG_NOTVISIBLE | HUD_FLAG_ALIGN_LEFT },
			score2   = { slot = HUD_FAR_LEFT,  name = "score2", dataval = "Score2", flags = HUD_FLAG_NOTVISIBLE | HUD_FLAG_ALIGN_LEFT },
			score3   = { slot = HUD_LEFT_BOT,  name = "score3", dataval = "Score3", flags = HUD_FLAG_NOTVISIBLE | HUD_FLAG_ALIGN_LEFT },
			score4   = { slot = HUD_RIGHT_BOT, name = "score4", dataval = "Score4", flags = HUD_FLAG_NOTVISIBLE | HUD_FLAG_ALIGN_LEFT },
		}
	}

	Ticker_AddToHud( DashHUD, "Hurry past the poles, go for a best time" )
	HUDSetLayout( DashHUD )
}

Holdout


///////////////////////////////////////////////////////////////////////////////
// The Holdout game mode! Brought to you by the new Mutation system!
// A Holdout Is...
//   Mostly a demo of the new mutation system, but a game mode as well
//   A Continuous cycle of attack/cooldown/attack/cooldown
//   in code, implemented as 3 phases
//        PANIC - spawn some enemies around the players
//        CLEAROUT - wait until the enemies are (mostly) all gone
//        DELAY - a cooldown time before the next wave of enemies
// There are several "special behaviors" as this happens
//   The Escape wave can happen at a specified wave #, or as a result of in game action (timer running down, for instance)
//   There can be special events that take over based on the current Stage #

// This is a very Structured mode - it uses stagetables and other "helpers" in the mutation system
// You dont need to use these in your own mutations, though you are of course welcome to

// as a map, you should go implement the following functions, returning a "stageTable"
// or null to ignore
//   DoMapEventCheck()
//   DoMapSetup()
//   GetMapEscapeStage()
//   IsMapSpecificStage()    // dont like this! potential out of sync/parallel variables
//   GetMapSpecificStage()   //    maybe IsMap sets a "stageSpecific" that GetMap can use?
//   GetAttackStage()
//   GetMapClearoutStage()
//   GetMapDelayStage()
///////////////////////////////////////////////////////////////////////////////

//---------------------------------------------------------
// This is the SessionState - any non-director data all your session code wants to be able to look at
// So we hold data about what wave, some UI controls, and info about managing warnings
MutationState <-
{
	HUDWaveInfo = true           // do you want the Wave # in middle of UI
	HUDRescueTimer = false       // or would you rather have the rescue timer (cant have both)
	HUDTickerText = "Holdout as long as you can"
	RescueStarted = false
	RawStageNum = -1
	ForcedEscapeStage = -1
	ScriptedStageWave = 0
	CooldownEndWarningTime = 8.5 // seconds before end of cooldown to play warning
	CooldownEndWarningChance = 15 // percent chance to play warning
	CooldownEndWarningFrequency = 0 // how many waves to wait before playing another warning
	LastWaveCooldownWarningPlayed = -1 // the last wave that the cooldown warning played on
}

//---------------------------------------------------------
// the DirectorOptions defaults for Holdout - though the maps will override many of these per wave and phase
MutationOptions <-
{
	PreferredMobDirection = SPAWN_NO_PREFERENCE
	SpawnSetRule = SPAWN_ANYWHERE
	JournalString = ""
	SpecialInfectedAssault = 0
	AllowWitchesInCheckpoints = 1
	AllowCrescendoEvents = 0
	EnforceFinaleNavSpawnRules = 0
	IgnoreNavThreatAreas = 1
	ZombieDiscardRange = 10000

	WanderingZombieDensityModifier = 0
	BoomerLimit  = 0
 	ChargerLimit = 0
 	HunterLimit  = 0
	JockeyLimit  = 0
	SpitterLimit = 0
	SmokerLimit  = 0
	MaxSpecials  = 0
	CommonLimit  = 20
	TankLimit    = 0
	MegaMobMaxSize = 100
	MegaMobMinSize = 75
	PanicWavePauseMax = 5
	PanicWavePauseMin = 1
	AddToSpawnTimer = 0
	ShouldAllowMobsWithTank = true
	ShouldAllowSpecialsWithTank = true
	ZombieSpawnRange = 800
	ZombieTankHealth = 6000 // SP default is 4000
	BileMobSize = 20
	EscapeSpawnTanks = true
	ZombieDontClear = 1
	MegaMobSize = 50       // i have no idea why this defaults to not between min and max

	cm_ShouldEscortHumanPlayers = 1
	cm_AggressiveSpecials = 1
}

//=========================================================
// Utilities and helpers
//=========================================================

const PHASE_PANIC = 1
const PHASE_CLEAR = 2
const PHASE_DELAY = 0

function StageNumToWaveBase( stagenum )
{
	return (stagenum + 2) / 3   // the +2 is because of our initial startup raw phases...
}

function WaveBaseToStageNum( wavebase, substage = PHASE_DELAY )
{
	return wavebase * 3 + substage
}

// Are players permitted to pick up this object by pressing their USE key?
// return true for yes, false for no.
function CanPickupObject( object )
{
	// mines
	if ( object.GetName().find( "mine_1_body" ) )
	{
		return true
	}
	
	// resource drops
	if ( object.GetName().find( "prop_resource" ) )
	{
		return true
	}

	// check the map script for a PickupObject function for extra qualified items
	// TODO: we should do this more generally!
	local canPickup = false
	if( "PickupObject" in g_MapScript )
	{
		canPickup = g_MapScript.PickupObject( object )
	}
	
	return canPickup
}

// Called from other systems when they want the next combat wave to be the escape
function StartEscapeWave()
{
	SessionState.ForcedEscapeStage =  ( ( SessionState.RawStageNum + 2 ) / 3) * 3 + 1
	// printl( "Prepping Escape Wave for Stage " + SessionState.ForcedEscapeStage + " from " + SessionState.RawStageNum )
}

// show the final score and timing
function HoldoutDisplayScores( )
{
	local final_time = Time() - SessionState.RealStartTime
	local score_strings = Scoring_AddScoreAndBuildStrings( Scoring_MakeName(), final_time )
	HUDPlace( HUD_TICKER, 0.32, 0.27, 0.36, 0.20 )
	HUDPlace( HUD_FAR_RIGHT, 0.28, 0.50, 0.44, 0.06 )
	HUDPlace( HUD_FAR_LEFT,  0.28, 0.58, 0.44, 0.06 )
	HUDPlace( HUD_LEFT_BOT,  0.28, 0.66, 0.44, 0.06 )
	HUDPlace( HUD_RIGHT_BOT, 0.28, 0.74, 0.44, 0.06 )
	local ticker_str = score_strings.yourtime
	if ("finish" in score_strings)
		ticker_str = ticker_str + "\n" + score_strings.finish
	ticker_str = ticker_str + "\nBest Times So Far"
	Ticker_SetBlink( false )   // or true if you came in first?
	Ticker_NewStr( ticker_str, 120 )
	foreach (idx, val in score_strings.topscores)
	{
		local hudfield = "score"+(idx+1)
		HoldoutHUD.Fields[hudfield].dataval = val
		HoldoutHUD.Fields[hudfield].flags = HoldoutHUD.Fields[hudfield].flags & ~HUD_FLAG_NOTVISIBLE
	}
	Scoring_SaveTable( SessionState.MapName, SessionState.ModeName )
}

// if a chopper rescue, this is where we get called
function RescuedByCopter( )
{
	// for now
	HoldoutDisplayScores()
}

// @TODO - this is basically "dont shoot at mines" right? why is it here?
function BotQuery( queryflag, entity, defaultvalue )
{
	switch( queryflag )
	{
		case BOT_QUERY_NOTARGET:
		{
			local classname = entity.GetClassname()
			local targetname = entity.GetName()
			if ( targetname && targetname.find( "mine_1_body" ) )
			{
				return false;
			}
			return true;
		}
	}
	
	return defaultvalue;
}

//----------------------------------------------------
// Rather than checking every time to see if the map has defined each callback
// instead we are going to check for each callback on startup
//   if they arent there, we will insert a simple NOP lambda (if they are, we leave them)
// For what these callbacks do, check top of file where they are described
function OnGameplayStart()
{
	CheckOrSetMapCallback( "DoMapEventCheck", @() false )
	CheckOrSetMapCallback( "DoMapSetup", @() null)
	CheckOrSetMapCallback( "GetMapEscapeStage", @() null )
	CheckOrSetMapCallback( "IsMapSpecificStage", @() false )
	CheckOrSetMapCallback( "GetMapSpecificStage", @() null )
	CheckOrSetMapCallback( "GetAttackStage", @() null )
	CheckOrSetMapCallback( "GetMapClearoutStage", @() null )
	CheckOrSetMapCallback( "GetMapDelayStage", @() null )

	DoMapSetup()    // and then call the MapSpecific startup callback right away
}

//=========================================================
//=========================================================
function OnActivate()
{
	// @todo: put scoring back into holdout!
	Scoring_LoadTable( SessionState.MapName, SessionState.ModeName )	

	// this is so we can do some non-wave-based thinking
	ScriptedMode_AddSlowPoll( HoldoutSlowPollUpdate )
}

//=========================================================
//=========================================================
function OnShutdown()
{
	ScriptedMode_RemoveSlowPoll( HoldoutSlowPollUpdate )
}

HoldoutHUD <- {}
//=========================================================
// since we have this mutation specific HUD but individual maps can use it differently
// it is a bit more complicated than a normal HUD Setup
// - use SessionState to tell the system which extra fields you want (Wave #, Timer, etc)
// - check and warn if you try to set conflicting fields
function SetupModeHUD()
{
	HoldoutHUD =
	{
		Fields = 
		{
			cooldown_time = { slot = HUD_LEFT_TOP, name = "cooldown", special = HUD_SPECIAL_COOLDOWN, flags = HUD_FLAG_COUNTDOWN_WARN | HUD_FLAG_BEEP },
			supply        = { slot = HUD_RIGHT_TOP, name = "supply", staticstring = "Supplies: ", datafunc = @() g_MapScript.Resources.CurrentCount },

			// this is kinda a lie! we are really going to move these w/HUDPlace for displaying final scores!
			// we will move the Ticker down to be a "header" - then need 4 slots for top 4
			score1   = { slot = HUD_FAR_RIGHT, name = "score1", dataval = "Score1", flags = HUD_FLAG_NOTVISIBLE | HUD_FLAG_ALIGN_LEFT },
			score2   = { slot = HUD_FAR_LEFT,  name = "score2", dataval = "Score2", flags = HUD_FLAG_NOTVISIBLE | HUD_FLAG_ALIGN_LEFT },
			score3   = { slot = HUD_LEFT_BOT,  name = "score3", dataval = "Score3", flags = HUD_FLAG_NOTVISIBLE | HUD_FLAG_ALIGN_LEFT },
			score4   = { slot = HUD_RIGHT_BOT, name = "score4", dataval = "Score4", flags = HUD_FLAG_NOTVISIBLE | HUD_FLAG_ALIGN_LEFT },
		}
	}
	
	if ( "HUDRescueTimer" in SessionState && SessionState.HUDRescueTimer )
	{   
		RescueTimer_Init( HoldoutHUD, HUD_MID_BOX, HUD_MID_BOT )
 		if (SessionState.HUDWaveInfo)
 			printl("Warning: Cannot have both Rescuetimer and Wave HUD elements at once");
 	}
 	else if ( "HUDWaveInfo" in SessionState && SessionState.HUDWaveInfo )
 	{
		HoldoutHUD.Fields.wave <- {slot = HUD_MID_TOP, name = "wave", staticstring = "Wave: ", datafunc = @() SessionState.ScriptedStageWave }
	}

	if( "HUDTickerTimeout" in SessionState )
	{
		TickerTimeout = SessionState.HUDTickerTimeout
	}
	
 	if ( "HUDTickerText" in SessionState )
 	{
 		if ( SessionState.HUDTickerText.len() > 0 )
 		{
 			Ticker_AddToHud( HoldoutHUD, SessionState.HUDTickerText )
 		}
 	}

	HUDSetLayout( HoldoutHUD );
}

//=========================================================
// The scripted mode calls this function each time the generator turns on. 
// The generator usually turns on because someone poured a can of fuel into it.
//=========================================================
function OnGeneratorStart()
{
	g_MapScript.RescueTimer_Start()
}

//=========================================================
// Counterpart to OnGeneratorStart(), this is called when the generator sputters and stops
//  usually because it has run out of fuel.
//=========================================================
function OnGeneratorStop()
{
	g_MapScript.RescueTimer_Stop()
}

//=========================================================
// this is the base "system maintenance" for holdout mode timers and UI
// It checks things like has rescue started, should we do a warning about cooldown about to end, etc
//=========================================================
function HoldoutSlowPollUpdate()
{
	RescueTimer_Tick()

	if ( !SessionState.RescueStarted )
	{
		if ( "HUDRescueTimer" in SessionState && SessionState.HUDRescueTimer )
		{
			local seconds = g_MapScript.RescueTimer_Get()
		
			// start the rescue if the generator has run long enough
			if( seconds <= 0 )
			{
				SessionState.RescueStarted = true
				g_ModeScript.StartEscapeWave()
			}
		}
	}

	// CooldownEndWarningChance is the % chance that a random player will vocalize a warning
	// when a cooldown is close to ending
	if( "CooldownEndWarningChance" in SessionState )
	{
		local timeLeft = HUDReadTimer( HUD_SPECIAL_COOLDOWN )

		if ( ( timeLeft < SessionState.CooldownEndWarningTime ) && ( timeLeft > 0 ) && ( SessionState.LastWaveCooldownWarningPlayed + SessionState.CooldownEndWarningFrequency < SessionState.ScriptedStageWave ) )
		{
			SessionState.LastWaveCooldownWarningPlayed = SessionState.ScriptedStageWave

			// roll the dice...
			local chance = RandomInt(0, 100 )

			if( chance < SessionState.CooldownEndWarningChance )
			{
				// jackpot! collect the players into an array and select one at random to speak a line
				local playerEnt = null
				local playerArray = []

				while ( playerEnt = Entities.FindByClassname( playerEnt, "player" ) )
				{
					if (playerEnt.IsSurvivor() )
					{
						playerArray.append( playerEnt)
					}
				}

				local idx = RandomInt( 0, 3)

				if( idx < playerArray.len() )
				{
					// fire entity IO at "!activator" and pass the player ent as the activator
					EntFire( "!activator", "SpeakResponseConcept", "PlayerHurryUp", 0, playerArray[idx] )
				}
			}
		}
	}
}

function JournalFunc()
{
	DirectorOptions.JournalString = "{ resources = " + g_MapScript.Resources.CurrentCount
	if (SessionState.HUDRescueTimer)
		DirectorOptions.JournalString += ", rescue = " + RescueTimer_Get()
	if ("JournalMapFunc" in this)
		DirectorOptions.JournalString += JournalMapFunc()
	DirectorOptions.JournalString += " }"
}

//=========================================================
// A little digression of testing/demoing a "script based clearout"
//  You can just do C++ based clearout (to let the mob population clearout) of course
//  But from script, one can use a Poll function and GetInfectedStats to manage it yourself
//  See sm_utilities for the actual code/systems if you want to do something like it

// Whether to use the new Script based clearout or not - false here just means use the default c++ one 
::g_ScriptClearout <- true

// if you are going to use the new clearout - here is the table for determining it's behavior
holdout_ClearoutTable <-
{
	commons = 2
	specials = 0
	tanks = 0
	witches = 0
	plateautime = 5
	plateaucommons = 5
	plateauspecials = 1
	stopspecials = true
}

///////////////////////////////////////////////////////////////////////////////
//
// "Main Loop" of Holdout
// 
// GetNextStage gets called whenever the director finishes the last thing you told it to do
//   or if you do a ForceNextStage yourself
// In holdout, we use GetNextStage to manage the game, cycling through 3 types of Stage (as described at top)
// At each stage, we make some of our callbacks (that we checked and set up in OnGameplayStart)
// To determine what options we want for that stage - usually in the form of a "stagetable"
//   which is basically just a set of DirectorOptions parameters and a few extra fields
// And then we go and set all those fields in the director, set the stage type, and send the director off
//
///////////////////////////////////////////////////////////////////////////////

function GetNextStage()
{
	local use_stage = null

	local stageNum = ++SessionState.RawStageNum

	SessionState.ScriptedStageWave = ( stageNum + 2 ) / 3

	// first, special event management
	DoMapEventCheck()
	
	// fire game events as we begin and end cooldown states - initial map stage 0 is considered a cooldown state
	if( ( stageNum % 3 ) == PHASE_PANIC )
		FireScriptEvent( "on_cooldown_end", null )
	else if( ( ( stageNum % 3 ) == PHASE_DELAY ) || ( stageNum == 0 ) )
		FireScriptEvent( "on_cooldown_begin", null )

	// error check!
	if (SessionState.ForcedEscapeStage != -1 && stageNum > SessionState.ForcedEscapeStage)
		printl("Ummmmmm.... something has gone very wrong... we passed escape stage but are still picking next stages...");

	if ( stageNum == 1) // 1st real attack stage
	{
		SessionState.RealStartTime <- Time() 
		Director.ResetSpecialTimers()
	}

	//  now the generalized stage sequencing...
	if ( stageNum == 0 )
	{
		DirectorOptions.ScriptedStageType = STAGE_SETUP
		if ("HUDTickerText" in SessionState)
			Ticker_NewStr(SessionState.HUDTickerText)  // since the time it got set as start in HUD load is ages ago
	}
	else if ( stageNum == SessionState.ForcedEscapeStage )
	{
		use_stage = GetMapEscapeStage()
	}
	else if ( IsMapSpecificStage() )
		use_stage = GetMapSpecificStage()   // dont like this! potential out of sync/parallel variables
	else if ( ( stageNum % 3 ) == PHASE_PANIC )
	{
		use_stage = GetAttackStage()
		if (::g_ScriptClearout)
			ClearoutNotifyPanicStart( holdout_ClearoutTable )
	}
	else if ( ( stageNum % 3 ) == PHASE_CLEAR )
	{
		if (::g_ScriptClearout)
		{
			ClearoutStart( holdout_ClearoutTable )
			DirectorOptions.ScriptedStageType = STAGE_DELAY
			DirectorOptions.ScriptedStageValue = -1	// Infinite
		}
		else
			use_stage = GetMapClearoutStage()
	}
	else if ( ( stageNum % 3 ) == PHASE_DELAY )
	{
		use_stage = GetMapDelayStage()
	}

	// Put the special infected into assault mode - really want to do this sooner somehow? at maxspecials 0?
	switch ( DirectorOptions.ScriptedStageType )
	{
		case STAGE_CLEAROUT:
		case STAGE_DELAY:
		case STAGE_ESCAPE:
			DirectorOptions.SpecialInfectedAssault = 1
			break;
		default:
			DirectorOptions.SpecialInfectedAssault = 0
	}

	if ( use_stage != null )
	{
		if ( "stageDefaults" in g_MapScript )
			StageInfo_Execute( use_stage, stageDefaults )        // should use_stage just delegate?
		else
			StageInfo_Execute( use_stage )
		
		if ( "NextDelay" in use_stage )
			SessionState.NextDelayTime <- use_stage.NextDelay    // just overwrite, so we dont need to if check
		else
			SessionState.NextDelayTime <- 30                     // better default from somewhere?
	}

	smDbgPrint( "Setting type " + DirectorOptions.ScriptedStageType + " and val " + DirectorOptions.ScriptedStageValue )
}

ShootZones


ModeSpawns <-
[
	["ShootzoneTrigger", "shootzone_trigger_spawn_*", "shootzone_trigger_group", SPAWN_FLAGS.SPAWN],
	[ "ShootzoneSound", SPAWN_FLAGS.NOSPAWN ],
]


MutationState <-
{
	allPlayers = []
	playersInShootzone = []
	allShootzones = []
	enabledShootzones = []
	lastEnabledShootzoneID = -1
	FirstTime = true
}


MutationOptions <-
{
    CommonLimit = 30 // Maximum number of common zombies alive in the world at the same time
 	//MegaMobSize = 20 // Total number of common zombies in a mob. (never more than CommonLimit at one time)
 	//WanderingZombieDensityModifier = 0 // lets get rid of the wandering zombies
 	MaxSpecials  = 0 // removes all special infected from spawning
 	TankLimit    = 0 // removes all tanks from spawning
 	WitchLimit   = 0 // removes all witches from spawning
	//BoomerLimit  = 0
 	//ChargerLimit = 0
 	//HunterLimit  = 0
	//JockeyLimit  = 0
	//SpitterLimit = 0
	//SmokerLimit  = 0
}


ShootzonesHUD <-
{
	Fields = 
	{
		player1   = { slot = HUD_LEFT_TOP,  name = "player1", dataval = "OUT", flags = HUD_FLAG_NOTVISIBLE | HUD_FLAG_ALIGN_LEFT },
		player2   = { slot = HUD_LEFT_BOT,  name = "player2", dataval = "OUT", flags = HUD_FLAG_NOTVISIBLE | HUD_FLAG_ALIGN_LEFT },
		player3   = { slot = HUD_RIGHT_TOP, name = "player3", dataval = "OUT", flags = HUD_FLAG_NOTVISIBLE | HUD_FLAG_ALIGN_LEFT },
		player4   = { slot = HUD_RIGHT_BOT, name = "player4", dataval = "OUT", flags = HUD_FLAG_NOTVISIBLE | HUD_FLAG_ALIGN_LEFT },
	}
}


function ShootzoneThinkSpawnCB( entity, rarity )
{
	printl( "Think Spawn!!!" )

	//entity.ValidateScriptScope()
	entity.GetScriptScope().ShootzoneThink <- function()
	{
		g_ModeScript.RecomputePlayersInShootzones()
	}
}


function OnGameplayStart()
{
	printl( "Starting SHOOTZONES!" )
	printl( "Max Flow Distance: " + GetMaxFlowDistance() )

	//Add all the players
	local playerEnt = null
	while ( playerEnt = Entities.FindByClassname( playerEnt, "player" ) )
	{
		if ( playerEnt.IsSurvivor() )
		{
			playerEnt.ValidateScriptScope()
			playerEnt.GetScriptScope().currentShootzone <- -1
			//playerEnt.GetScriptScope().currentShootzone = -1
			SessionState.allPlayers.append( playerEnt)
		}
	}

	//Initialize the HUD
	foreach ( index, playerEnt in SessionState.allPlayers )
	{
		local hudField = "player" + ( index + 1 )
		ShootzonesHUD.Fields[hudField].dataval = SessionState.allPlayers[index].GetPlayerName() + ": OUT"
		ShootzonesHUD.Fields[hudField].flags = ShootzonesHUD.Fields[hudField].flags & ~HUD_FLAG_NOTVISIBLE
	}

	EnableShootzones()

	//ScriptedMode_AddUpdate( RecomputePlayersInShootzones.bindenv(this) )

	local shootzone_think =
	{
		function GetSpawnList()      { return [ EntityGroup.SpawnTables.shootzone_think ] }
		function GetEntityGroup()    { return EntityGroup }
		EntityGroup =
		{
			SpawnTables =
			{
				shootzone_think = 
				{
					initialSpawn = true
					SpawnInfo =
					{
						classname = "logic_script"
						angles = Vector( 0, 0, 90 )
						targetname = "shootzone_think"
						origin = Vector( 0, 0, 0 )
						vscripts = "NULL_SCRIPT_NAME"
						thinkfunction = "ShootzoneThink"
					}
				}
			} // SpawnTables
		} // EntityGroup
	}

	local shootzoneThinkGroup = shootzone_think.GetEntityGroup()
	shootzoneThinkGroup.SpawnTables[ "shootzone_think" ].PostPlaceCB <- ShootzoneThinkSpawnCB
	g_MapScript.SpawnSingleAt( shootzoneThinkGroup, Vector( 0, 0, 0 ) , QAngle( 0, 0, 0 ) )
	g_MapScript.SpawnSingleAt( g_MapScript.GetEntityGroup( "ShootzoneSound" ), Vector( 0, 0, 0 ) , QAngle( 0, 0, 0 ) )

}


function OnActivate()
{
}


function OnEntityGroupRegistered( name, group )
{
	if ( name == "ShootzoneTrigger" )
	{
		group.GetEntityGroup().SpawnTables[ "shootzone_script" ].PostPlaceCB <- ShootzoneScriptSpawnCB
	}
}


function ShootzoneScriptSpawnCB( entity, rarity )
{
	printl( "Script spawned: " + entity + ", name: " + entity.GetName() )

	local shootzoneScope = entity.GetScriptScope()

	//Set the id of the shootzone
	shootzoneScope.id = SessionState.allShootzones.len()
	printl( "Shootzone ID: " + shootzoneScope.id )
	
	//Flow distance for the shootzone
	shootzoneScope.flowDistance = GetFlowDistanceForPosition( shootzoneScope.origin )
	shootzoneScope.flowPercent = GetFlowPercentForPosition( shootzoneScope.origin, true )
	shootzoneScope.flowPercent1 = GetFlowPercentForPosition( shootzoneScope.origin, false )
	printl( "Flow Distance: " + shootzoneScope.flowDistance )
	printl( "Flow Percent: " + shootzoneScope.flowPercent )

	//Add the shootzone to the list of all the shootzones
	SessionState.allShootzones.append( entity )
	
}


// HUD setup/control - our HUD is pretty simple in dash
function SetupModeHUD( )
{
	HUDSetLayout( ShootzonesHUD )

	HUDPlace( HUD_LEFT_TOP, 0.01, 0.06, 0.2, 0.04 )
	HUDPlace( HUD_LEFT_BOT, 0.01, 0.11, 0.2, 0.04 )
	HUDPlace( HUD_RIGHT_TOP, 0.01, 0.16, 0.2, 0.04 )
	HUDPlace( HUD_RIGHT_BOT, 0.01, 0.21, 0.2, 0.04 )
}


function FindShootzoneClosestToPlayers()
{

}

function EnableShootzones()
{
	//Cycle through the shootzones and keep 3 enabled at all times
	if ( SessionState.allShootzones.len() < 3 )
	{
		foreach ( shootzone in SessionState.allShootzones )
		{
			shootzone.GetScriptScope().EnableShootzone()
			SessionState.enabledShootzones.append( shootzone )

			SessionState.lastEnabledShootzoneID = shootzone.GetScriptScope().id
		}
	}
	else
	{
		local index = SessionState.lastEnabledShootzoneID + 1
		while ( SessionState.enabledShootzones.len() < 3 )
		{
			if ( index >= SessionState.allShootzones.len() )
			{
				index = 0;
			}

			//Enable the next available shootzone in the list
			local currentShootzone = SessionState.allShootzones[index]
			if ( currentShootzone.GetScriptScope().isEnabled == false )
			{
				currentShootzone.GetScriptScope().EnableShootzone()
				SessionState.enabledShootzones.append( currentShootzone )

				SessionState.lastEnabledShootzoneID = currentShootzone.GetScriptScope().id
			}

			index++
		}
	}
}


function RecomputePlayersInShootzones()
{
//	if ( SessionState.FirstTime )
//	{
//		foreach ( shootzone in SessionState.allShootzones )
//		{
//			local shootzoneScope = shootzone.GetScriptScope()
//			DebugDrawText( shootzoneScope.origin, "Distance: " + shootzoneScope.flowDistance + "\nPercent: " + shootzoneScope.flowPercent + "%", false, 100000 )
//		}
//		
//		SessionState.FirstTime = false;
//	}

	//Keep track of players currently in a shootzone
	local prevShootzonePlayers = clone( SessionState.playersInShootzone )

	//Recompute which players are in an active shootzone
	SessionState.playersInShootzone.clear()

	foreach ( playerEnt in SessionState.allPlayers )
	{
		foreach ( shootzone in SessionState.enabledShootzones )
		{
			local shootzoneScope = shootzone.GetScriptScope()
			if ( shootzoneScope.isActive )
			{
				local playerToShootzone = playerEnt.GetOrigin() - shootzoneScope.origin
				local playerDistance = playerToShootzone.Length()

				if ( playerDistance < 150 )
				{
					playerEnt.GetScriptScope().currentShootzone = shootzoneScope.id
					SessionState.playersInShootzone.append( playerEnt )
					break;
				}
			}
		}	
	}

	//foreach ( shootzone in SessionState.allShootzones )
	//{
		//local shootzoneScope = shootzone.GetScriptScope()
		//local vecto = SessionState.allPlayers[0].GetOrigin() - shootzoneScope.origin
		//local dist = vecto.Length()
		//
		//if ( dist < 500 )
		//{
			//DebugDrawText( shootzoneScope.origin, "Distance: " + shootzoneScope.flowDistance + "\nPercent: " + shootzoneScope.flowPercent + "%\nPercent2: " + shootzoneScope.flowPercent1, false, 0.15 )
			//
			//DebugDrawLine( shootzoneScope.origin, SessionState.allPlayers[0].GetOrigin(), 255, 0, 0, false, 0.2 )
		//}
	//}
//
	foreach ( index, playerEnt in SessionState.allPlayers )
	{
		//This player just entered a shootzone
		if ( prevShootzonePlayers.find( playerEnt ) == null &&
			 SessionState.playersInShootzone.find( playerEnt ) != null )
		{
			//Start playing the sound for players that just entered the shootzone
			printl( playerEnt.GetPlayerName() + " entered shootzone " + playerEnt.GetScriptScope().currentShootzone )
			EmitSoundOnClient( "c2m4.BadMan1", playerEnt )
		}

		//This player just exited a shootzone
		if ( prevShootzonePlayers.find( playerEnt ) != null &&
			 SessionState.playersInShootzone.find( playerEnt ) == null )
		{
			//Stop playing the sound for players that just exited the shootzone
			printl( playerEnt.GetPlayerName() +" exited shootzone " + playerEnt.GetScriptScope().currentShootzone )

			playerEnt.GetScriptScope().currentShootzone = -1
			StopSoundOn( "c2m4.BadMan1", playerEnt )
		}

		local inOrOut = "OUT"
		foreach ( shootzonePlayer in SessionState.playersInShootzone )
		{
			if ( shootzonePlayer.GetPlayerName() == playerEnt.GetPlayerName() )
			{
				inOrOut = "IN"
				break;
			}
		}

		//Set the player HUD flags
		local hudField = "player" + ( index + 1 )
		ShootzonesHUD.Fields[hudField].dataval = SessionState.allPlayers[index].GetPlayerName() + ": " + inOrOut
		ShootzonesHUD.Fields[hudField].flags = ShootzonesHUD.Fields[hudField].flags & ~HUD_FLAG_NOTVISIBLE
	}
}


function ShootzoneTimedOut( shootzoneID )
{
	foreach ( index, shootzone in SessionState.enabledShootzones )
	{
		if ( shootzone.GetScriptScope().id == shootzoneID )
		{
			SessionState.enabledShootzones.remove( index )
			break
		}
	}

	EnableShootzones()
}


function AllowTakeDamage( damageTable )
{
	foreach ( playerEnt in SessionState.allPlayers )
	{
		//If the attacker is a player
		if ( playerEnt == damageTable.Attacker )
		{
			//If the attacking player is in a shootzone then do damage
			foreach ( shootzonePlayer in SessionState.playersInShootzone )
			{
				if ( shootzonePlayer == damageTable.Attacker )
				{
					//printl( "Damage done by shootzone player: " + shootzonePlayer.GetPlayerName() )
					return true
				}
			}

			//Attacking player is not in a shootzone
			//printl( "Attacking player is not in shootzone: " + playerEnt.GetPlayerName() )
			return false
		}
	}

	//Attacker is not a player
	return true
}

GunBrain


//=========================================================
//=========================================================

GunStatsTable <- null

TARGET_INVALID <- -1
TARGET_SURVIVOR <- 9

MAX_TRACK_PLAYERS <- 10 // note - to change this we would need to update the server index to generate our graphs. There are only 10 graph servers so you can't go past the index 9.

// we need a barebones HUD for our ticker that shows up at the begining of the game
function SetupModeHUD( )
{
	ModeHUD <-
	{
		Fields =
		{
		}
	}
	Ticker_AddToHud( ModeHUD, "" )

	// load the ModeHUD table
	HUDSetLayout( ModeHUD )
}

/////////////////////////////////////////////////
// Mod Command interface
/////////////////////////////////////////////////

GB_HELP_TEXT <-
[
	"About GunBrain - Damage stats are stored on the server for each player. The motd points to an htm file that contains graphs of the stats.",
	"Type these commands into the chat window to perform the following actions.",
	"---",
	"gb_block <playername> - Remove stats for this player and prevent them from recording stats in the future. (useful for Bots)",
	"gb_allow <playername> - Allow a stats blocked player to record stats again.",
	"gb_update - Update the files on disk with the most recent version of the stats (this automatically happens at level end).",
	"gb_dump - Dump all the stats to the console.",
	"gb_reset - Start fresh collecting stats.",
	"gb_backup - creates gunbrainstats.bak with the current data",
	"gb_restore - loads data from gunbrainstats.bak",
	"gb_bot_block - Block all bots from recording stats",
]

function InterceptChat( str, srcEnt )
{
	if ( str.find("gb_help") != null )
	{
		foreach( idx, Line in GB_HELP_TEXT )
		{
			Say( null, Line, true )
		}
	}
	else if( srcEnt != null )
	{
		if ( str.find("gb_block ") )
		{
			local commandStart = str.find("gb_block ")
			local name = str.slice( commandStart + 9 )
			name = name.slice(0,-1)
			CommandAddBlockPlayer( name.toupper() )
			SessionState.GBStatsDirty = true
		}
		else if ( str.find("gb_allow ") )
		{
			local commandStart = str.find("gb_allow ")
			local name = str.slice( commandStart + 9 )
			name = name.slice(0,-1)
			CommandRemoveBlockPlayer( name.toupper() )
		}
		else if ( str.find("gb_update") )
		{
			SessionState.GBStatsDirty = true
		}
		else if ( str.find("gb_dump") )
		{
			CommandDumpData()
		}
		else if ( str.find("gb_reset") )
		{
			CommandResetData()
			SessionState.GBStatsDirty = true
		}
		else if ( str.find("gb_backup") )
		{
			CommandBackupData()
		}
		else if ( str.find("gb_restore") )
		{
			CommandRestoreData()
			SessionState.GBStatsDirty = true
		}
		else if ( str.find("gb_bot_block") )
		{
			CommandAddBlockPlayer( "ELLIS" )
			CommandAddBlockPlayer( "COACH" )
			CommandAddBlockPlayer( "NICK" )
			CommandAddBlockPlayer( "ROCHELLE" )
			CommandAddBlockPlayer( "FRANCIS" )
			CommandAddBlockPlayer( "LOUIS" )
			CommandAddBlockPlayer( "ZOEY" )
			CommandAddBlockPlayer( "BILL" )
		}		
	}
}

function IsPlayerBlocked( playerName )
{
	foreach( index, name in GunStatsTable.Blocked )
	{
		if( name == playerName )
		{
			return true
		}
	}
	return false
}

function CommandAddBlockPlayer( playerName )
{
	Say( null, "Blocking stats for " + playerName, true )

	if ( GunStatsTable.Blocked.len() >= 64 )
		return

	if ( IsPlayerBlocked( playerName ) )
	{
		return
	}
	
	
	GunStatsTable.Blocked.push( playerName )

	foreach( index, Player in GunStatsTable.Players )
	{
		if( playerName == Player.name )
		{
			Say( null, "Removing stats for " + playerName, true )
			GunStatsTable.Players.remove(index)
			return
		}
	}
}

function CommandRemoveBlockPlayer( playerName )
{
	Say( null, "Removing block for " + playerName, true )
	foreach( index, name in GunStatsTable.Blocked )
	{
		if( name == playerName )
		{
			GunStatsTable.Blocked.remove(index)
			return
		}
	}
}

function CommandUpdateData()
{
	CommitDamageData()
}

function CommandDumpData()
{
	printl( TableToString( GunStatsTable ) )
}

function CommandResetData()
{
	GunStatsTable.clear()
	VerifyStatsTableStorage()
	printl( TableToString( GunStatsTable ) )
}

function CommandBackupData()
{
	Say( null, "Backing up data", true )
	StringToFile( "GunBrainData.bak" , TableToString( GunStatsTable ) )
}

function CommandRestoreData()
{
	GunStatsTable.clear()

	local saved_data = FileToString( "GunBrainData.bak" )
	if (saved_data != null)
	{
		local compileDataFunc = compilestring( "local temp_table = " + saved_data + " return temp_table" )
		GunStatsTable = compileDataFunc()
	}
	else
	{
		GunStatsTable <- {}
	}

	VerifyStatsTableStorage()
}

/////////////////////////////////////////////////
// Startup
/////////////////////////////////////////////////

function OnGameplayStart()
{
	printl("Running GunBrain the enhanced gun stats mod")
}

function OnActivate()
{
	printl( " ** On Activate" )	

	SessionState.GBStatsDirty <- false
	ScriptedMode_AddUpdate( StatsUpdate )
	SessionState.GBStatsTick <- 0

	Ticker_SetBlink( true )
	Ticker_NewStr( "Welcome to GunBrain! Chat <gb_help> or open the MotD for available commands", 15 )
	
	PrepareStats()

	CommitDamageData()
}

function PrepareStats()
{
	local saved_data = FileToString( "GunBrainData" )
	if (saved_data != null)
	{
		local compileDataFunc = compilestring( "local temp_table = " + saved_data + " return temp_table" )
		GunStatsTable = compileDataFunc()
	}
	else
	{
		GunStatsTable <- {}
	}

	VerifyStatsTableStorage()

	foreach( index, Player in GunStatsTable.Players )
	{
		printl( "    " + index + " = " + Player.name );
	}
}

/////////////////////////////////////////////////
// Data Management
/////////////////////////////////////////////////

function StatsUpdate()
{
	SessionState.GBStatsTick++

	if( SessionState.GBStatsDirty || SessionState.GBStatsTick == 30 )
	{
		g_ModeScript.CommandUpdateData()

		SessionState.GBStatsDirty = false
		SessionState.GBStatsTick = 0

		Say( null, "Updating Stats", true )
	}
}

function VerifyStatsTableStorage()
{
	if ( !( "Players" in GunStatsTable ) )
	{
		printl("Adding new players list")
		GunStatsTable.Players <- []
	}
	if ( !( "Blocked" in GunStatsTable ) )
	{
		GunStatsTable.Blocked <- []
	}
}

function CommitDamageData()
{
	StringToFile( "GunBrainData" , TableToString( GunStatsTable ) )

	local damageImgString = "<p>"
	
	foreach( idx, Line in GB_HELP_TEXT )
	{
		damageImgString = damageImgString + Line + "<br>"
	}

	damageImgString = damageImgString + "</p>"
	
	damageImgString = damageImgString + CreateOutgoingDamageChartString() + CreateIncomingDamageChartString()
	foreach( index, Player in GunStatsTable.Players )
	{
		damageImgString = damageImgString + CreatePlayerAccuracyChartString( index )
	}
	foreach( index, Player in GunStatsTable.Players )
	{
		damageImgString = damageImgString + CreatePlayerDamageChartString( index )
	}
	StringToFile( "gunbrainstats.htm" , damageImgString )
	SendToServerConsole( "motdfile ems\\gunbrainstats.htm" )
	ReloadMOTD() 
}

function OnShutdown()
{
	CommitDamageData()
}

/////////////////////////////////////////////////
// Damage Event handling
/////////////////////////////////////////////////

function AllowTakeDamage( damageTable )
{	
	// check to see if this wasn't a weapon
	if( !( "GetClassname" in damageTable.Weapon ) )
	{
		// don't store shove damage
	}
	else
	{
		if( damageTable.Attacker.GetClassname() == "player" && damageTable.Attacker.IsSurvivor() )
		{
			local target_type = TARGET_INVALID

			if ( damageTable.Victim.GetClassname() == "infected" )
			{
				target_type = ZOMBIE_NORMAL
			}
			else if ( damageTable.Victim.GetClassname() == "player" )
			{
				if( damageTable.Victim.IsSurvivor() )
				{
					target_type = TARGET_SURVIVOR
				}
				else
				{
					target_type = damageTable.Victim.GetZombieType()
				}
			}
			else if ( damageTable.Victim.GetClassname() == "witch" )
			{
				target_type = ZOMBIE_WITCH
			}
			
			// remove WEAPON_ from the name
			local weapName = damageTable.Weapon.GetClassname()
			weapName = weapName.slice( 7 )

			AddHit( damageTable.Attacker.GetPlayerName().toupper(), weapName.toupper(), damageTable.Victim.GetHealth() > 0 ? damageTable.Victim.GetHealth() : 0, damageTable.DamageDone, damageTable.DamageType & DMG_HEADSHOT, target_type )
		}
		else
		{
			printl("bad target")
		}
	}
	return true
}

function OnGameEvent_weapon_fire( params )
{
	if ( "count" in params )
	{
		local attacker = GetPlayerFromUserID( params.userid )
		local weapon = params.weapon
		local shots = params.count

		AddShots( attacker.GetPlayerName().toupper(), weapon.toupper(), shots )
	}
}

function OnGameEvent_player_hurt( params )
{
	local victim = GetPlayerFromUserID( params.userid )
	if ( !victim.IsSurvivor() )
	{
		return
	}

	if( IsPlayerBlocked( victim.GetPlayerName().toupper() ) )
		return

	if( "attackerentid" in params )
	{
		local attacker = EntIndexToHScript( params.attackerentid )

		local target_type = TARGET_INVALID

		if( params.attackerentid == 0 && params.attacker == 0 )
		{
			// looks like an invalid attacker
		}
		else if ( attacker.GetClassname() == "infected" )
		{
			target_type = ZOMBIE_NORMAL
		}
		else if ( attacker.GetClassname() == "player" )
		{
			if( attacker.IsSurvivor() )
			{
				target_type = TARGET_SURVIVOR
			}
			else
			{
				target_type = attacker.GetZombieType()
			}
		}
		else if ( attacker.GetClassname() == "witch" )
		{
			target_type = ZOMBIE_WITCH
		}

		local player = FindOrAddPlayerData( victim.GetPlayerName().toupper() )
		if ( player != null )
		{
			AddIncomingDamage( player, params.health, params.dmg_health, target_type )
		}
	}
}

/////////////////////////////////////////////////
// Stat recording.
/////////////////////////////////////////////////

function AddHit( playerName, weaponName, victimHealth, damage, headShot, target_type )
{
	if( IsPlayerBlocked( playerName ) )
		return
	
//	printl( "playerName " + playerName + " weaponName " + weaponName + " victimHealth " + victimHealth + " damage " + damage + " headShot " + headShot )

	local Weapon = FindOrAddWeaponData( playerName, weaponName )

	if ( Weapon != null )
	{
		if( target_type != TARGET_INVALID )
		{
			AddWeaponHit( Weapon, victimHealth, damage, headShot, target_type )
		}

		local player = FindOrAddPlayerData( playerName )
		if ( player != null ) 
		{
			AddTypeDamage( player, victimHealth, damage, target_type )
		}
	}
}

function AddShots( playerName, weaponName, shots )
{
	if( IsPlayerBlocked( playerName ) )
		return

//	printl( "playerName " + playerName + " weaponName " + weaponName + " shots " + shots )

	local Weapon = FindOrAddWeaponData( playerName, weaponName )
	if ( Weapon  != null )
	{
		Weapon.shots += shots
	}
}

function CreateNewPlayerTable( playerName )
{
	local player = { name=playerName, WeaponData = [], TypeDamage = [], DamageTaken = [] }
	player.TypeDamage.resize(10,0)
	player.DamageTaken.resize(10,0)
	return player
}

function CreateNewWeaponTable( weaponName )
{
	local weapon = { name=weaponName, shots = 0, hits = 0, effectiveDamage = 0, overKill = 0, deadDamage = 0, headShots = 0, FFHits = 0 }
	return weapon
}

function FindOrAddPlayerData( playerName )
{
	local playerIdx = -1

	foreach( indexP, Player in GunStatsTable.Players )
	{
		if( playerName == Player.name )
		{
			playerIdx = indexP
			break
		}
	}	

	if ( playerIdx == -1 )
	{
		if ( GunStatsTable.Players.len() >= MAX_TRACK_PLAYERS )
		{
			return null
		}
		else
		{
			local player = CreateNewPlayerTable( playerName )
			playerIdx = GunStatsTable.Players.len()
			GunStatsTable.Players.push( player )
		}
	}

	return GunStatsTable.Players[playerIdx]
}

function FindOrAddWeaponData( playerName, weaponName )
{
	local foundPlayer = null
	local bFoundWeapon = false

	local weapIdx = -1
	local playerIdx = -1

	foreach( indexP, Player in GunStatsTable.Players )
	{
		if( playerName == Player.name )
		{
			playerIdx = indexP
			foreach( indexW, Weapon in Player.WeaponData )
			{	
				if( weaponName == Weapon.name )
				{
					return Weapon
				}
			}
			break
		}
	}	

	if ( playerIdx == -1 )
	{
		if ( GunStatsTable.Players.len() >= MAX_TRACK_PLAYERS )
		{
			return null
		}
		else
		{
			local player = CreateNewPlayerTable( playerName )
			playerIdx = GunStatsTable.Players.len()
			GunStatsTable.Players.push( player )
		}
	}

	local weapon = CreateNewWeaponTable( weaponName )
	GunStatsTable.Players[playerIdx].WeaponData.push( weapon )
	return weapon
}

function AddIncomingDamage( player, victimHealth, damage, target_type )
{
	if ( target_type == TARGET_INVALID )
		return

	local effectiveDamage = damage
	if ( damage > victimHealth )
	{
		effectiveDamage = victimHealth
	}

	player.DamageTaken[target_type] += effectiveDamage
}

function AddTypeDamage( player, victimHealth, damage, target_type )
{
	if ( target_type == TARGET_INVALID )
		return

	local effectiveDamage = damage
	if ( damage > victimHealth )
	{
		effectiveDamage = victimHealth
	}

	player.TypeDamage[target_type] += effectiveDamage
}

function AddWeaponHit( weaponData, victimHealth, damage, headShot, target_type )
{
	if ( target_type == TARGET_SURVIVOR )
	{
		weaponData.FFHits++
		return
	}

	if ( weaponData.name == "MELEE" || weaponData.name == "CHAINSAW" )
	{
		weaponData.shots++
	}

//	printl( "victimHealth " + victimHealth + " damage " + damage + " headShot " + headShot + " target_type " + target_type )

	if ( damage < 0 )
		damage = 0
	
	local effectiveDamage = damage
	local overKillDamage = 0
	local deadDamage = 0

	if ( victimHealth <= 0 )
	{
		effectiveDamage = 0
		overKillDamage = 0
		deadDamage = damage
	}
	if ( damage > victimHealth )
	{
		effectiveDamage = victimHealth
		overKillDamage = damage - victimHealth
	}

	weaponData.hits++
	weaponData.effectiveDamage += effectiveDamage
	weaponData.overKill += overKillDamage
	weaponData.deadDamage += deadDamage

	if ( headShot > 0 )
	{
		weaponData.headShots++
	}

//	printl( "hits " + weaponData.hits + "  effectiveDamage " + weaponData.effectiveDamage + "  overKill " + weaponData.overKill + "  headShots " + weaponData.headShots + "  shots " + weaponData.shots )
}


/////////////////////////////////////////////////
// Chart URL building
/////////////////////////////////////////////////

function ComputeChartHeight( desiredHeight )
{
	local height = desiredHeight

	if( height < 220 )
	{
		height = 220
	}
	if( height > 500 )
	{
		height = 500
	}
	return height
}
function CreateOutgoingDamageChartString()
{
	local chart_url = "<img src="
	chart_url = chart_url + "\""
	chart_url = chart_url + "http://"
	chart_url = chart_url + 8
	chart_url = chart_url + ".chart.apis.google.com/chart"

	local target_names = ["Common","Smoker","Boomer","Hunter","Spitter","Jockey","Charger","Witch","Tank","Survivor"]

	{
		local line = "?chxl=1:"
		foreach( index, player in GunStatsTable.Players )
		{
			// player names are backwards so go in revers order
			line = line + "|" + GunStatsTable.Players[ (GunStatsTable.Players.len() - 1) - index ].name
		}
		chart_url = chart_url + line
	}
	
	chart_url = chart_url + "&chxr=0,0,100"
	chart_url = chart_url + "&chxt=x,y"
	chart_url = chart_url + "&chbh=a"
	chart_url = chart_url + "&chs=600x" + ComputeChartHeight( floor(( 30*( 1 + GunStatsTable.Players.len() ) ) ) )
	chart_url = chart_url + "&cht=bhs"
	chart_url = chart_url + "&chco=D1C304,43D948,B6D943,0E8F17,6CA0E0,04C7D1,0E04D1,D104B2,8A04D1,DE2E12"
	chart_url = chart_url + "&chds=0,1000,0,1000,0,1000,0,1000,0,1000,0,1000,0,1000,0,1000,0,1000,0,1000"

	local maxDamage = 100
	foreach( indexP, player in GunStatsTable.Players )
	{
		local playerDamageTotal = 0 
		foreach( index_target, damage in player.TypeDamage )
		{
			playerDamageTotal += damage
		}
		if( playerDamageTotal > maxDamage )
		{
			maxDamage = playerDamageTotal
		}
	}

	local damageScalar = 1000.0/maxDamage

	{		
		local line = "&chd=t"
		foreach( index_target, target in target_names )
		{
			if( index_target == 0 ) {	line = line + ":" } else { line = line + "|" }

			foreach( indexP, player in GunStatsTable.Players )
			{
				if( indexP == 0 ) {	line = line + "" } else { line = line + "," }

				local num = damageScalar*player.TypeDamage[index_target]
				line = line + "" + floor( num )
			}
		}
		chart_url = chart_url + line
	}

	{
		local line = "&chdl="
		foreach( index, name in target_names )
		{
			if( index != 0 ) { line = line + "|" }
			line = line + name
		}
		chart_url = chart_url + line
	}
	chart_url = chart_url + "&chtt=" + "Outgoing Player damage by Target"
	chart_url = chart_url + "\"" + "\\><br><br><br><br>"

	return chart_url
}

function CreateIncomingDamageChartString()
{
	local chart_url = "<img src="
	chart_url = chart_url + "\""
	chart_url = chart_url + "http://"
	chart_url = chart_url + 9
	chart_url = chart_url + ".chart.apis.google.com/chart"

	local target_names = ["Common","Smoker","Boomer","Hunter","Spitter","Jockey","Charger","Witch","Tank","Survivor"]

	{
		local line = "?chxl=1:"
		foreach( index, player in GunStatsTable.Players )
		{
			// player names are backwards so go in revers order
			line = line + "|" + GunStatsTable.Players[ (GunStatsTable.Players.len() - 1) - index ].name
		}
		chart_url = chart_url + line
	}

	chart_url = chart_url + "&chxr=0,0,100"
	chart_url = chart_url + "&chxt=x,y"
	chart_url = chart_url + "&chbh=a"
	chart_url = chart_url + "&chs=600x" + ComputeChartHeight( floor(( 30*( 1 + GunStatsTable.Players.len() ) ) ) )
	chart_url = chart_url + "&cht=bhs"
	chart_url = chart_url + "&chco=D1C304,43D948,B6D943,0E8F17,6CA0E0,04C7D1,0E04D1,D104B2,8A04D1,DE2E12"
	chart_url = chart_url + "&chds=0,1000,0,1000,0,1000,0,1000,0,1000,0,1000,0,1000,0,1000,0,1000,0,1000"

	local maxDamage = 100
	foreach( indexP, player in GunStatsTable.Players )
	{
		local playerDamageTotal = 0 
		foreach( index_target, damage in player.DamageTaken )
		{
			playerDamageTotal += damage
		}
		if( playerDamageTotal > maxDamage )
		{
			maxDamage = playerDamageTotal
		}
	}

	local damageScalar = 1000.0/maxDamage

	{		
		local line = "&chd=t"
		foreach( index_target, target in target_names )
		{
			if( index_target == 0 ) {	line = line + ":" } else { line = line + "|" }

			foreach( indexP, player in GunStatsTable.Players )
			{
				if( indexP == 0 ) {	line = line + "" } else { line = line + "," }

				local num = damageScalar*player.DamageTaken[index_target]
				line = line + "" + floor( num )
			}
		}
		chart_url = chart_url + line
	}

	{
		local line = "&chdl="
		foreach( index, name in target_names )
		{
			if( index != 0 ) { line = line + "|" }
			line = line + name
		}
		chart_url = chart_url + line
	}
	chart_url = chart_url + "&chtt=" + "Incoming Player damage by Source"
	chart_url = chart_url + "\"" + "\\><br><br><br><br>"

	return chart_url
}

function CreatePlayerAccuracyChartString( player_index )
{
	local chart_url = "<img src="
	chart_url = chart_url + "\""
	chart_url = chart_url + "http://"
	chart_url = chart_url + player_index
	chart_url = chart_url + ".chart.apis.google.com/chart"

	local Player = GunStatsTable.Players[player_index]
	{
		local line = "?chxl=1:"
		foreach( indexW, Weapon in Player.WeaponData )
		{
			// weapon names are listed backwards so store them such
			line = line + "|" + Player.WeaponData[ (Player.WeaponData.len() - 1) - indexW ].name
		}
		chart_url = chart_url + line
	}

	chart_url = chart_url + "&chxr=0,0,100"
	chart_url = chart_url + "&chxt=x,y"
	chart_url = chart_url + "&chbh=a"
	chart_url = chart_url + "&chs=600x" + ComputeChartHeight( floor(( 36*( 1.6 + GunStatsTable.Players[player_index].WeaponData.len() ) )) )
	chart_url = chart_url + "&cht=bhs"
	chart_url = chart_url + "&chco=3D7930,A2C180,DCBA80,DC5030"
	chart_url = chart_url + "&chds=0,1000,0,1000,0,1000,0,1000"

	{		
		local line = "&chd=t"
		foreach( indexW, Weapon in Player.WeaponData )
		{
			if( indexW == 0 ) {	line = line + ":" } else { line = line + "," }

			local num = ( Weapon.headShots / (0.001*(Weapon.shots + 1) ) )
			line = line + "" + floor( num )
		}
		line = line + "|"
		foreach( indexW, Weapon in Player.WeaponData )
		{
			if( indexW != 0 ) { line = line + "," }

			local num = ( (Weapon.hits - Weapon.headShots) / (0.001*(Weapon.shots + 0.01) ) )
			line = line + "" + floor( num )
		}
		line = line + "|"
		foreach( indexW, Weapon in Player.WeaponData )
		{
			if( indexW != 0 ) { line = line + "," }

			local num = ( ( Weapon.shots - ( Weapon.hits + Weapon.FFHits ) ) / (0.001*(Weapon.shots + 0.01) ) )
			line = line + "" + floor( num )
		}
		line = line + "|"
		foreach( indexW, Weapon in Player.WeaponData )
		{
			if( indexW != 0 ) { line = line + "," }

			local num = ( Weapon.FFHits / (0.001*(Weapon.shots + 0.01) ) )
			line = line + "" + floor( num )
		}
		chart_url = chart_url + line
	}

	chart_url = chart_url + "&chdl=Headshot|Non-Headshot Hit|Miss|Friendly Fire"
	chart_url = chart_url + "&chtt=" + Player.name + " Shot Hit Type"
	chart_url = chart_url + "\"" + "\\><br><br><br><br>"

	return chart_url
}

function CreatePlayerDamageChartString( player_index )
{
	local chart_url = "<img src="
	chart_url = chart_url + "\""
	chart_url = chart_url + "http://"
	chart_url = chart_url + player_index
	chart_url = chart_url + ".chart.apis.google.com/chart"

	local Player = GunStatsTable.Players[player_index]
	{
		local line = "?chxl=1:"
		foreach( indexW, Weapon in Player.WeaponData )
		{
			// weapon names are listed backwards so store them such
			line = line + "|" + Player.WeaponData[ (Player.WeaponData.len() - 1) - indexW ].name
		}
		chart_url = chart_url + line
	}
	
	chart_url = chart_url + "&chxr=0,0,75"
	chart_url = chart_url + "&chxt=x,y"
	chart_url = chart_url + "&chbh=a"
	chart_url = chart_url + "&chs=600x" + ComputeChartHeight( floor(( 36*( 1.6 + GunStatsTable.Players[player_index].WeaponData.len() ) )) )
	chart_url = chart_url + "&cht=bhs"
	chart_url = chart_url + "&chco=1B84E0,43C5CC,A4B3B0"
	chart_url = chart_url + "&chds=0,75,0,75,0,75"

	{		
		local line = "&chd=t"
		foreach( indexW, Weapon in Player.WeaponData )
		{
			if( indexW == 0 )
			{
				line = line + ":"
			}
			else
			{
				line = line + ","
			}
			line = line + ( Weapon.effectiveDamage / (Weapon.shots + 1) )
		}
		line = line + "|"
		foreach( indexW, Weapon in Player.WeaponData )
		{
			if( indexW != 0 )
			{
				line = line + ","
			}
			line = line + ( Weapon.overKill / (Weapon.shots + 1) )
		}
		line = line + "|"
		foreach( indexW, Weapon in Player.WeaponData )
		{
			if( indexW != 0 )
			{
				line = line + ","
			}
			line = line + ( Weapon.deadDamage / (Weapon.shots + 1) )
		}
		chart_url = chart_url + line
	}

	chart_url = chart_url + "&chdl=Effective+Damage|Overkill+Damage|Dead Target+Damage"
	chart_url = chart_url + "&chtt=" + Player.name + " Damage+Breakdown(per pellet)"
	chart_url = chart_url + "\"" + "\\><br><br><br><br>"

	return chart_url
}


/////////////////////////////////////////////////
// Table manipulation
/////////////////////////////////////////////////

// These two helper functions can call themselves and each other so if want either you would need both.
function TableToString( table )  
{
	local table_string = "{\n"
		
	foreach (idx, key in table)
	{
		if ( typeof(key) == "table" )
		{
			table_string = table_string + idx + "=\n" + TableToString( key ) + ",\n"
		}
		else if ( typeof(key) == "array" )
		{
			table_string = table_string + idx + "=\n" + ArrayToString( key ) + ",\n"
		}
		else if ( typeof(key) == "string" )
		{
			table_string = table_string + idx + "=\"" + key + "\",\n"
		}
		else
		{
			table_string = table_string + idx + "=" + key + ",\n"
		}
	}
	return table_string	+ "}"
}

function ArrayToString( array )
{
	local array_string = "[\n"
		
	foreach (idx, key in array)
	{
		if ( typeof(key) == "table" )
		{
			array_string = array_string + TableToString( key ) + ",\n"
		}
		else if ( typeof(key) == "array" )
		{
			array_string = array_string + ArrayToString( key ) + ",\n"
		}
		else if ( typeof(key) == "string" )
		{
			array_string = array_string + "\"" + key + "\",\n"
		}
		else
		{
			array_string = array_string + key + ",\n"
		}
	}
	return array_string	+ "]"
}

Tank Run


//-----------------------------------------------------
Msg("Activating Tank Run\n");

if ( !IsModelPrecached( "models/infected/hulk.mdl" ) )
	PrecacheModel( "models/infected/hulk.mdl" );
if ( !IsModelPrecached( "models/infected/hulk_dlc3.mdl" ) )
	PrecacheModel( "models/infected/hulk_dlc3.mdl" );
if ( !IsModelPrecached( "models/infected/hulk_l4d1.mdl" ) )
	PrecacheModel( "models/infected/hulk_l4d1.mdl" );

MutationOptions <-
{
	cm_TankRun = true
	cm_ShouldHurry = true
	cm_InfiniteFuel = true
	cm_AllowPillConversion = false
	cm_CommonLimit = 0
	cm_DominatorLimit = 0
	cm_MaxSpecials = 0
	cm_ProhibitBosses = true
	cm_AggressiveSpecials = true

	BoomerLimit = 0
	SmokerLimit = 0
	HunterLimit = 0
	ChargerLimit = 0
	SpitterLimit = 0
	JockeyLimit = 0
	cm_WitchLimit = 0
	cm_TankLimit = 8

	MobMinSize = 0
	MobMaxSize = 0
	NoMobSpawns = true
	EscapeSpawnTanks = false

	// convert items that aren't useful
	weaponsToConvert =
	{
		ammo = "upgrade_laser_sight"
	}

	function ConvertWeaponSpawn( classname )
	{
		if ( classname in weaponsToConvert )
		{
			return weaponsToConvert[classname];
		}
		return 0;
	}

	DefaultItems =
	[
		"weapon_pistol_magnum",
	]

	function GetDefaultItem( idx )
	{
		if ( idx < DefaultItems.len() )
		{
			return DefaultItems[idx];
		}
		return 0;
	}
}

MutationState <-
{
	TankModelsBase = [ "models/infected/hulk.mdl", "models/infected/hulk_dlc3.mdl", "models/infected/hulk_l4d1.mdl" ]
	TankModels = [ "models/infected/hulk.mdl", "models/infected/hulk_dlc3.mdl", "models/infected/hulk_l4d1.mdl" ]
	ModelCheck = false
	FinaleStarted = false
	TriggerRescue = false
	RescueDelay = 600
	LastAlarmTankTime = 0
	LastSpawnTime = 0
	SpawnInterval = 20
	DoubleTanks = false
	TankBiled = {}
	TanksDisabled = false
	TankHealth = 4000
	BileHurtTankThink = false
	SpawnTankThink = false
	TriggerRescueThink = false
	LeftSafeAreaThink = false
	CheckPrimaryWeaponThink = false
	FinaleType = -1
}

function GetNumTanks()
{
	local infStats = {};
	GetInfectedStats( infStats );
	return infStats.Tanks;
}

if ( IsMissionFinalMap() )
{
	local triggerFinale = Entities.FindByClassname( null, "trigger_finale" );
	if ( triggerFinale )
	{
		MutationState.FinaleType = NetProps.GetPropInt( triggerFinale, "m_type" );
		if ( NetProps.GetPropInt( triggerFinale, "m_bIsSacrificeFinale" ) )
		{
			function OnGameEvent_generator_started( params )
			{
				if ( !SessionState.FinaleStarted )
					return;

				HUDManageTimers( 0, TIMER_COUNTDOWN, HUDReadTimer( 0 ) - 30 );
				if ( GetNumTanks() < SessionOptions.cm_TankLimit )
					ZSpawn( { type = 8 } );
			}
		}
	}

	TankRunHUD <- {};
	function SetupModeHUD()
	{
		TankRunHUD =
		{
			Fields =
			{
				rescue_time = { slot = HUD_MID_TOP, name = "rescue", special = HUD_SPECIAL_TIMER0, flags = HUD_FLAG_COUNTDOWN_WARN | HUD_FLAG_BEEP | HUD_FLAG_ALIGN_CENTER | HUD_FLAG_NOTVISIBLE },
			}
		}
		HUDSetLayout( TankRunHUD );
	}

	if ( MutationState.FinaleType != 4 )
	{
		function GetNextStage()
		{
			if ( SessionState.TriggerRescue )
			{
				SessionOptions.ScriptedStageType = STAGE_ESCAPE;
				return;
			}
			if ( SessionState.FinaleStarted )
			{
				SessionOptions.ScriptedStageType = STAGE_DELAY;
				SessionOptions.ScriptedStageValue = -1;
			}
		}
	}

	function OnGameEvent_finale_start( params )
	{
		if ( SessionState.MapName == "c6m3_port" )
		{
			SessionOptions.cm_TankLimit = 8;
			SessionState.TanksDisabled = false;
			SessionState.SpawnTankThink = true;
		}

		if ( SessionState.FinaleType == 4 )
			return;

		HUDManageTimers( 0, TIMER_COUNTDOWN, SessionState.RescueDelay );
		TankRunHUD.Fields.rescue_time.flags = TankRunHUD.Fields.rescue_time.flags & ~HUD_FLAG_NOTVISIBLE;

		SessionState.DoubleTanks = true;
		SessionState.SpawnInterval = 40;
		SessionState.FinaleStarted = true;
		SessionState.TriggerRescueThink = true;
	}

	function OnGameEvent_gauntlet_finale_start( params )
	{
		if ( SessionState.MapName == "c5m5_bridge" || SessionState.MapName == "c13m4_cutthroatcreek" )
		{
			SessionOptions.cm_TankLimit = 8;
			SessionState.TanksDisabled = false;
			SessionState.SpawnTankThink = true;
		}
	}

	function OnGameEvent_finale_vehicle_leaving( params )
	{
		SessionState.SpawnTankThink = false;
	}
}

function AllowTakeDamage( damageTable )
{
	if ( !damageTable.Attacker || !damageTable.Victim || !damageTable.Inflictor )
		return true;

	if ( damageTable.Victim.IsPlayer() && damageTable.Attacker.IsPlayer() )
	{
		if ( damageTable.Attacker.IsSurvivor() && damageTable.Victim.GetZombieType() == 8 )
		{
			if ( damageTable.Inflictor.GetClassname() == "pipe_bomb_projectile" )
				damageTable.DamageDone = 500;

			if ( damageTable.Weapon )
			{
				local weaponClass = damageTable.Weapon.GetClassname();
				if ( weaponClass == "weapon_pistol" )
					damageTable.DamageDone = (damageTable.DamageDone * 1.25);
				else if ( weaponClass == "weapon_melee" )
					damageTable.DamageDone = (damageTable.DamageDone * 1.334);
			}
		}
	}

	return true;
}

function TriggerRescueThink()
{
	if ( HUDReadTimer( 0 ) <= 0 )
	{
		SessionState.TriggerRescue = true;
		SessionState.TriggerRescueThink = false;
		Director.ForceNextStage();

		TankRunHUD.Fields.rescue_time.flags = TankRunHUD.Fields.rescue_time.flags | HUD_FLAG_NOTVISIBLE;
		HUDManageTimers( 0, TIMER_DISABLE, 0 );
	}
}

function SpawnTankThink()
{
	if ( SessionOptions.cm_TankLimit == 0 )
		return;

	if ( (GetNumTanks() < SessionOptions.cm_TankLimit) && ((Time() - SessionState.LastSpawnTime) >= SessionState.SpawnInterval || SessionState.LastSpawnTime == 0) )
	{
		if ( ZSpawn( { type = 8 } ) )
		{
			if ( SessionState.DoubleTanks )
				ZSpawn( { type = 8 } );
			SessionState.LastSpawnTime = Time();
		}
	}
}

function LeftSafeAreaThink()
{
	for ( local player; player = Entities.FindByClassname( player, "player" ); )
	{
		if ( NetProps.GetPropInt( player, "m_iTeamNum" ) != 2 )
			continue;

		if ( ResponseCriteria.GetValue( player, "instartarea" ) == "0" )
		{
			SessionState.LeftSafeAreaThink = false;
			SessionState.SpawnTankThink = true;
			break;
		}
	}
}

function BileHurtTankThink()
{
	foreach( tank, survivor in SessionState.TankBiled )
	{
		tank.TakeDamage( 100, 0, survivor );
	}
}

function CheckDifficultyForTankHealth( difficulty )
{
	local health = [2000, 3000, 4000, 5000];
	SessionState.TankHealth = health[difficulty];
}

if ( Director.IsFirstMapInScenario() )
{
	function CheckPrimaryWeaponThink()
	{
		local startArea = null;
		local startPos = null;
		for ( local survivorSpawn; survivorSpawn = Entities.FindByClassname( survivorSpawn, "info_survivor_position" ); )
		{
			local area = NavMesh.GetNearestNavArea( survivorSpawn.GetOrigin(), 100, false, false );
			if ( (area) && (area.HasSpawnAttributes( 128 ) || area.HasSpawnAttributes( 2048 )) )
			{
				startArea = area;
				startPos = survivorSpawn.GetOrigin();
				break;
			}
		}

		if ( !startPos )
			return;

		for ( local weapon; weapon = Entities.FindByClassnameWithin( weapon, "weapon_*", startPos, 1200 ); )
		{
			if ( weapon.GetOwnerEntity() )
				continue;

			local weaponName = weapon.GetClassname();
			local primaryNames = [ "smg", "rifle", "shotgun", "sniper", "grenade" ];
			if ( weaponName == "weapon_spawn" )
			{
				weaponName = NetProps.GetPropString( weapon, "m_iszWeaponToSpawn" );
				if ( weaponName.find( "pistol" ) != null )
					continue;
				primaryNames.append( "any" );
			}

			foreach( name in primaryNames )
			{
				if ( weaponName.find( name ) != null )
					return;
			}
		}

		local itemNames = [ "weapon_pistol_spawn", "weapon_pistol_magnum_spawn", "weapon_spawn", "weapon_melee_spawn" ];
		foreach( name in itemNames )
		{
			local item = Entities.FindByClassnameNearest( name, startPos, 600 );
			if ( item )
			{
				local nearestArea = NavMesh.GetNearestNavArea( item.GetOrigin(), 100, false, false );
				if ( nearestArea )
					startArea = nearestArea;
				break;
			}
		}

		local w = [ "any_smg", "tier1_shotgun" ];
		for ( local i = 0; i < 2; i++ )
			SpawnEntityFromTable( "weapon_spawn", { spawn_without_director = 1, weapon_selection = w[i], count = 5, spawnflags = 3, origin = (startArea.FindRandomSpot() + Vector(0,0,50)), angles = Vector(RandomInt(0,90),RandomInt(0,90),90) } );
	}

	function OnGameplayStart()
	{
		SessionState.CheckPrimaryWeaponThink = true;
	}
}

function OnGameEvent_round_start_post_nav( params )
{
	for ( local spawner; spawner = Entities.FindByClassname( spawner, "info_zombie_spawn" ); )
	{
		local population = NetProps.GetPropString( spawner, "m_szPopulation" );

		if ( population == "tank" || population == "river_docks_trap" )
			continue;
		else
			spawner.Kill();
	}
	for ( local ammo; ammo = Entities.FindByModel( ammo, "models/props/terror/ammo_stack.mdl" ); )
		ammo.Kill();

	if ( SessionState.MapName == "c5m5_bridge" || SessionState.MapName == "c6m3_port" || SessionState.MapName == "c13m4_cutthroatcreek" )
	{
		SessionOptions.cm_TankLimit = 0;
		SessionState.TanksDisabled = true;
	}

	CheckDifficultyForTankHealth( GetDifficulty() );
	EntFire( "worldspawn", "RunScriptCode", "g_ModeScript.TankRunThink()", 1.0 );
}

function OnGameEvent_difficulty_changed( params )
{
	CheckDifficultyForTankHealth( params["newDifficulty"] );
}

function OnGameEvent_player_left_safe_area( params )
{
	if ( SessionState.TanksDisabled )
		return;

	local player = GetPlayerFromUserID( params["userid"] );
	if ( !player )
	{
		SessionState.SpawnTankThink = true;
		return;
	}

	if ( ResponseCriteria.GetValue( player, "instartarea" ) == "1" )
		SessionState.LeftSafeAreaThink = true;
	else
		SessionState.SpawnTankThink = true;
}

function OnGameEvent_player_disconnect( params )
{
	local player = GetPlayerFromUserID( params["userid"] );
	if ( !player )
		return;

	if ( player.GetZombieType() == 8 && player in SessionState.TankBiled )
	{
		SessionState.TankBiled.rawdelete( player );
		if ( SessionState.TankBiled.len() == 0 )
			SessionState.BileHurtTankThink = false;
	}
}

function OnGameEvent_mission_lost( params )
{
	SessionState.SpawnTankThink = false;
}

function OnGameEvent_player_now_it( params )
{
	local attacker = GetPlayerFromUserID( params["attacker"] );
	local victim = GetPlayerFromUserID( params["userid"] );

	if ( !attacker || !victim )
		return;

	if ( attacker.IsSurvivor() && victim.GetZombieType() == 8 )
	{
		if ( victim in SessionState.TankBiled )
			return;

		victim.SetFriction( 2.0 );
		SessionState.TankBiled.rawset( victim, attacker );
		if ( SessionState.TankBiled.len() == 1 )
			SessionState.BileHurtTankThink = true;
	}
}

function OnGameEvent_player_no_longer_it( params )
{
	local victim = GetPlayerFromUserID( params["userid"] );

	if ( !victim )
		return;

	if ( victim.GetZombieType() == 8 && victim in SessionState.TankBiled )
	{
		victim.SetFriction( 1.0 );
		SessionState.TankBiled.rawdelete( victim );
		if ( SessionState.TankBiled.len() == 0 )
			SessionState.BileHurtTankThink = false;
	}
}

function OnGameEvent_triggered_car_alarm( params )
{
	if ( (GetNumTanks() < SessionOptions.cm_TankLimit) && ((Time() - SessionState.LastAlarmTankTime) >= SessionState.SpawnInterval || SessionState.LastAlarmTankTime == 0) )
	{
		if ( ZSpawn( { type = 8 } ) )
			SessionState.LastAlarmTankTime = Time();
	}
}

function OnGameEvent_tank_spawn( params )
{
	local tank = GetPlayerFromUserID( params["userid"] );
	if ( !tank )
		return;

	tank.SetMaxHealth( SessionState.TankHealth );
	tank.SetHealth( SessionState.TankHealth );
	local modelName = tank.GetModelName();

	if ( !SessionState.ModelCheck )
	{
		SessionState.ModelCheck = true;

		if ( SessionState.TankModelsBase.find( modelName ) == null )
		{
			SessionState.TankModelsBase.append( modelName );
			SessionState.TankModels.append( modelName );
		}
	}

	local tankModels = SessionState.TankModels;
	if ( tankModels.len() == 0 )
		SessionState.TankModels.extend( SessionState.TankModelsBase );
	local foundModel = tankModels.find( modelName );
	if ( foundModel != null )
	{
		tankModels.remove( foundModel );
		return;
	}

	local randomElement = RandomInt( 0, tankModels.len() - 1 );
	local randomModel = tankModels[ randomElement ];
	tankModels.remove( randomElement );

	tank.SetModel( randomModel );
}

function OnGameEvent_tank_killed( params )
{
	local tank = GetPlayerFromUserID( params["userid"] );

	if ( tank in SessionState.TankBiled )
	{
		SessionState.TankBiled.rawdelete( tank );
		if ( SessionState.TankBiled.len() == 0 )
			SessionState.BileHurtTankThink = false;
	}

	if ( SessionState.FinaleStarted )
		HUDManageTimers( 0, TIMER_COUNTDOWN, HUDReadTimer( 0 ) - 10 );
}

function TankRunThink()
{
	if ( SessionState.LeftSafeAreaThink )
		LeftSafeAreaThink();
	if ( SessionState.SpawnTankThink )
		SpawnTankThink();
	if ( SessionState.TriggerRescueThink )
		TriggerRescueThink();
	if ( SessionState.BileHurtTankThink )
		BileHurtTankThink();
	if ( SessionState.CheckPrimaryWeaponThink )
	{
		CheckPrimaryWeaponThink();
		SessionState.CheckPrimaryWeaponThink = false;
	}
	if ( Director.GetCommonInfectedCount() > 0 )
	{
		for ( local infected; infected = Entities.FindByClassname( infected, "infected" ); )
			infected.Kill();
	}
	EntFire( "worldspawn", "RunScriptCode", "g_ModeScript.TankRunThink()", 1.0 );
}

See also

Intros

Documentations

Miscelleanous