// Script by StardustSpy

// todo 

::ROOT <- getroottable()
::MAX_CLIENTS <- MaxClients().tointeger() // get all players that can spawn in a server

if ( !( "ConstantNamingConvention" in ROOT ) )
	foreach( a, b in Constants )
		foreach( k, v in b )
			ROOT[ k ] <- v != null ? v : 0

foreach ( key, value in CEntities ) 
{
    if ( key == "IsValid" ) continue;
    // Doesn't work directly with `value` due to squirrel's broken closures
    local method = value;
    getroottable()[ key ] <- function( ... ) {
        local entity = method.acall( [ Entities ].extend( vargv ) );
        if ( entity )
            NetProps.SetPropBool( entity, "m_bForcePurgeFixedupStrings", true );
        return entity;
    }
}

foreach( _class in [  "NetProps", "Entities", "EntityOutputs", "NavMesh"  ] )
    foreach( k, v in ROOT[  _class  ].getclass() )
        if ( !( k in ROOT ) && k != "IsValid" ) 
            ROOT[  k  ] <- ROOT[  _class  ][  k  ].bindenv( ROOT[  _class  ] )

// Helpful references to important entities
::gamerules <- FindByClassname( null, "tf_gamerules" )
::player_manager <- FindByClassname( null, "tf_player_manager" )
::WORLD_SPAWN <- FindByClassname( null, "worldspawn" )
::mvm_logic_entity <- FindByClassname( null, "tf_objective_resource" )

//control
const BLU_CAP_TIME = 30
const RED_CAP_TIME = 5

// mdl

// sound

// Helpful consts 
const EFL_USER = 1048576
const SLOT_COUNT = 7
const STRING_NETPROP_ITEMDEF = "m_AttributeManager.m_Item.m_iItemDefinitionIndex"
const DEG2RAD   = 0.0174532924
const RAD2DEG   = 57.295779513
const FLT_SMALL = 0.0000001
const FLT_MIN   = 1.175494e-38
const FLT_MAX   = 3.402823466e+38
const INT_MIN   = -2147483648
const INT_MAX   = 2147483647
const TF_BOT_FAKE_CLIENT = 1337

const DMG_CRITICAL = 1048576
const FAKE_MINI_SENTRY_MAXIMUM = 8
const SINGLE_TICK = 0.015
const REMOVE_ENTRY = false
const KEEP_ENTRY = true

const STRING_NETPROP_INIT = "m_AttributeManager.m_Item.m_bInitialized"
const STRING_NETPROP_ATTACH = "m_bValidatedAttachedEntity"

const OF_ALLOW_REPEAT_PLACEMENT      = 1
const OF_MUST_BE_BUILT_ON_ATTACHMENT = 2
const OF_DOESNT_HAVE_A_MODEL         = 4
const OF_PLAYER_DESTRUCTION          = 8

// attack
const BOSS_ATTACK_FIREBALL = 1
const BOSS_ATTACK_SCYTHE = 2

//redefine EFlags
const EFL_USER 			  = 1048576 // EFL_IS_BEING_LIFTED_BY_BARNACLE
const EFL_CUSTOM_WEARABLE = 1073741824 //EFL_NO_PHYSCANNON_INTERACTION
const EFL_PROJECTILE 	  = 2097152 //EFL_NO_ROTORWASH_PUSH
const EFL_BOT 			  = 268435456 //EFL_NO_MEGAPHYSCANNON_RAGDOLL
const EFL_SPAWNTEMPLATE   = 67108864 //EFL_DONTWALKON

// screenfade flags
const FFADE_IN = 1
const FFADE_OUT = 2
const FFADE_MODULATE = 4
const FFADE_STAYOUT = 8
const FFADE_PURGE =	16 

//buttons

::KEY_PRIMARY_FIRE <- Constants.FButtons.IN_ATTACK
::KEY_SECONDARY_FIRE <- Constants.FButtons.IN_ATTACK2
::KEY_SPECIAL_FIRE <- Constants.FButtons.IN_ATTACK3
::KEY_RELOAD <- Constants.FButtons.IN_RELOAD
::KEY_JUMP <- Constants.FButtons.IN_JUMP
::KEY_ABILITY <- Constants.FButtons.IN_SPEED

// trigger ent
const SF_TRIGGER_ALLOW_CLIENTS                = 1
const SF_TRIGGER_ALLOW_NPCS                   = 2
const SF_TRIGGER_ALLOW_PUSHABLES              = 4
const SF_TRIGGER_ALLOW_PHYSICS                = 8
const SF_TRIGGER_ONLY_PLAYER_ALLY_NPCS        = 16
const SF_TRIGGER_ONLY_CLIENTS_IN_VEHICLES     = 32
const SF_TRIGGER_ALLOW_ALL                    = 64
const SF_TRIG_PUSH_ONCE                       = 128
const SF_TRIG_PUSH_AFFECT_PLAYER_ON_LADDER    = 256
const SF_TRIGGER_ONLY_CLIENTS_OUT_OF_VEHICLES = 512
const SF_TRIG_TOUCH_DEBRIS                    = 1024
const SF_TRIGGER_ONLY_NPCS_IN_VEHICLES        = 2048
const SF_TRIGGER_DISALLOW_BOTS                = 4096

// from popext
const STRING_NETPROP_ITEMDEF 	  	    = "m_AttributeManager.m_Item.m_iItemDefinitionIndex"
const STRING_NETPROP_INIT 	 	  	    = "m_AttributeManager.m_Item.m_bInitialized"
const STRING_NETPROP_ATTACH  	  	    = "m_bValidatedAttachedEntity"
const STRING_NETPROP_PURGESTRINGS 	    = "m_bForcePurgeFixedupStrings"
const STRING_NETPROP_MYWEAPONS    	    = "m_hMyWeapons"
const STRING_NETPROP_AMMO		  	    = "m_iAmmo"
const STRING_NETPROP_NAME		  	    = "m_iName"
const STRING_NETPROP_CLASSNAME    	    = "m_iClassname"
const STRING_NETPROP_MODELINDEX   	    = "m_nModelIndex"
const STRING_NETPROP_POPNAME    		= "m_iszMvMPopfileName"
const STRING_NETPROP_MDLINDEX_OVERRIDES = "m_nModelIndexOverrides"

// screenshake

const SHAKE_START = 0
const SHAKE_STOP = 1

// restrict
::RESTRICT_MELEE <- 1
::RESTRICT_PRIMARY <- 2
::RESTRICT_SECONDARY <- 4

// damage type
::TF_DMG_BURN <- 8
::TF_DMG_BLAST <- 64
::TF_DMG_BULLET <- 2
::TF_DMG_MELEE <- 128
::TF_DMG_NO_FORCE <- 2048
::TF_DMG_CRITICAL <- 1048576
::TF_DMG_FALL_OFF <- 2097152
::TF_DMG_HALF_FALLOFF <- 262144
::TF_DMG_MELEE <- 134217728 // DMG_BLAST_SURFACE

// sound
::SND_NOFLAGS <- 0
::SND_CHANGE_VOL <- 1
::SND_CHANGE_PITCH <- 2
::SND_STOP <- 4
::SND_SPAWNING <- 8
::SND_DELAY <- 16
::SND_STOP_LOOPING <- 32
::SND_SPEAKER <- 64
::SND_SHOULDPAUSE <- 128
::SND_IGNORE_PHONEMES <- 256
::SND_IGNORE_NAME <- 512
::SND_DO_NOT_OVERWRITE_EXISTING_ON_CHANNEL <- 1024

// stun
::TF_STUN_NONE <- 0
::TF_STUN_MOVEMENT <- 1
::TF_STUN_CONTROLS <- 2
::TF_STUN_MOVEMENT_FORWARD_ONLY <- 4
::TF_STUN_SPECIAL_SOUND <- 8
::TF_STUN_DODGE_COOLDOWN <- 16
::TF_STUN_NO_EFFECTS <- 32
::TF_STUN_LOSER_STATE <- 64
::TF_STUN_BY_TRIGGER <- 128
::TF_STUN_SOUND <- 256

//Teams
::TEAM_SPECTATOR <- 1
::TEAM_RED <- 2
::TEAM_BLU <- 3

::TF_NAV_SPAWN_ROOM_RED <- 2
::TF_NAV_SPAWN_ROOM_BLUE <- 4

// stun
::STUN_MOVE <- 1
::STUN_MOVE_FORWARD_ONLY <- 4

// Multi
::WEAPON_BASE_JUMPER <- 1101
::WEAPON_RESERVE_SHOOTER <- 415
::WEAPON_PAIN_TRAIN <- 154
::WEAPON_SHOTGUN_PRIMARY <- 9

// Scout
::WEAPON_FORCE_A_NATURE <- 45
::WEAPON_BACK_SCATTER <- 1103
::WEAPON_BABY_FACES_BLASTER <- 772

::WEAPON_BONK_ATOMIC_PUNCH <- 46
::WEAPON_CRIT_A_COLA <- 163
::WEAPON_MAD_MILK <- 222
::WEAPON_PRETTY_BOY_POCKET_PISTOL <- 773
::WEAPON_FLYING_GUILLOTINE <- 812

::WEAPON_FANOWAR <- 355
::WEAPON_SANDMAN <- 44
::WEAPON_WRAP_ASSASSIN <- 648

// Soldier
::WEAPON_BEGGARS_BAZOOKA <- 730
::WEAPON_LIBERTY_LAUNCHER <- 414
::WEAPON_AIR_STRIKE <- 1104
::WEAPON_ROCKET_JUMPER <- 237

::WEAPON_BUFF_BANNER <- 129
::WEAPON_BATTALIONS_BACKUP <- 226
::WEAPON_CONCHEROR <- 354
::WEAPON_BUFF_BANNER_FESTIVE <- 1001
::WEAPON_GUNBOATS <- 133
::WEAPON_MANTREADS <- 444
::WEAPON_RIGHTEOUS_BISON <- 442

// Pyro
::WEAPON_DRAGONS_FURY <- 1178
::WEAPON_RAINBLOWER <- 741
::WEAPON_PHLOGISTINATOR <- 594
::WEAPON_DEGREASER <- 215

::WEAPON_MANN_MELTER <- 595
::WEAPON_SHARPENED_VOLCANO_FRAGMENT <- 348

// Demo
::WEAPON_ALI_BABAS_WEE_BOOTIES <- 405
::WEAPON_BOOTLEGGER <- 608
::WEAPON_IRON_BOMBER <- 1151
::WEAPON_LOCH_N_LOAD <- 308

::WEAPON_CHARGIN_TARGE <- 131
::WEAPON_TIDE_TURNER <- 1099
::WEAPON_SPLENDID_SCREEN <- 406
::WEAPON_STICKY_JUMPER <- 265
::WEAPON_CHARGIN_TARGE_FESTIVE <- 1144

::WEAPON_ULLAPOOL_CABER <- 307
::WEAPON_PERSIAN_PERSUADER <- 404
// heavy
::WEAPON_NATASCHA <- 41

// Engineer

::WEAPON_BUILDER <- 25
::WEAPON_BUILDER_DESTROY <- 26
::WEAPON_BOX <- 28
::WEAPON_WIDOWMAKER <- 527
::WEAPON_POMSON <- 588

// medic
::WEAPON_VITA_SAW <- 173
::WEAPON_SOLEMN_VOW <- 413

// Sniper
::WEAPON_DARWIN_DANGER_SHIELD <- 231
::WEAPON_COZY_CAMPER <- 642 
::WEAPON_RAZORBACK <- 57
::WEAPON_CLASSIC <- 1098
::WEAPON_CLEANERS_CARBINE <- 751

// Spy
::WEAPON_BUILDER_SAPPER <- 27
::WEAPON_INVIS_WATCH <- 30
::WEAPON_ENFORCER <- 460
::WEAPON_SPYCICLE <- 649

// FuncType
::FUNC_THINK <- 0
::FUNC_DAMAGE <- 1

PrecacheSound("ui/item_metal_scrap_pickup.wav")
PrecacheSound("ui/item_metal_scrap_drop.wav")
PrecacheSound("misc/halloween/spell_overheal.wav")
PrecacheSound("weapons/cow_mangler_explosion_charge_06.wav")

PrecacheModel("models/flags/toolbox_flag.mdl")
PrecacheModel("models/props_gameplay/cap_circle_320.mdl")
PrecacheModel("models/props_vehicles/mining_car_metal.mdl")
PrecacheModel("models/props_vehicles/mining_cart_supplies001.mdl")
PrecacheModel("models/props_vehicles/mining_cart_supplies002.mdl")
PrecacheModel("models/weapons/w_models/w_medigun_overhealer.mdl")

PrecacheModel("models/props_frontline/tank.mdl")

::SalvageGamemode <- 
{
    Test = printl( "----- Successfully Loaded <SalvageGamemode> Scripts -----" )

    // script1 = IncludeScript("curses/camera_fixer", getroottable())

    Player_Cleanup_Table = []
    Robots_Cleanup_Table = []
    PreservedCleanupArray = []
    MapNavAreas = {}
    TakeDamageFuncTable = {}
    aryNoGiveExplanation = []
    PrecacheParticleTable = {}

    inVictoryStage = 0

    MaxAmmoTable = 
    {
		[ TF_CLASS_SCOUT ] = 
        {
			[ "tf_weapon_scattergun" ]            = 32,
			[ "tf_weapon_handgun_scout_primary" ] = 32,
			[ "tf_weapon_soda_popper" ]           = 32,
			[ "tf_weapon_pep_brawler_blaster" ]   = 32,

			[ "tf_weapon_handgun_scout_secondary" ] = 36,
			[ "tf_weapon_pistol" ]                  = 36,
		},
		[ TF_CLASS_SOLDIER ] =
        {
			[ "tf_weapon_rocketlauncher" ]           = 20,
			[ "tf_weapon_rocketlauncher_directhit" ] = 20,
			[ "tf_weapon_rocketlauncher_airstrike" ] = 20,
			[ WEAPON_ROCKET_JUMPER ] = 60,

			[ "tf_weapon_shotgun_soldier" ] = 32,
			[ "tf_weapon_shotgun" ]         = 32,
		},
		[ TF_CLASS_PYRO ] = 
        {
			[ "tf_weapon_flamethrower" ]            = 200,
			[ "tf_weapon_rocketlauncher_fireball" ] = 40,

			[ "tf_weapon_shotgun_pyro" ] = 32,
			[ "tf_weapon_shotgun" ]      = 32,
			[ "tf_weapon_flaregun" ]     = 16,
		},
		[ TF_CLASS_DEMOMAN ] = 
        {
			[ "tf_weapon_grenadelauncher" ] = 16,
			[ "tf_weapon_cannon" ]          = 16,

			[ "tf_weapon_pipebomblauncher" ] = 24,
			[ WEAPON_STICKY_JUMPER ] = 72,
		},
		[ TF_CLASS_HEAVYWEAPONS ] = 
        {
			[ "tf_weapon_minigun" ]     = 200,

			[ "tf_weapon_shotgun_hwg" ] = 32,
			[ "tf_weapon_shotgun" ]     = 32,
		},
		[ TF_CLASS_ENGINEER ] = 
        {
			[ "tf_weapon_shotgun" ]                 = 32,
			[ "tf_weapon_sentry_revenge" ]          = 32,
			[ "tf_weapon_shotgun_building_rescue" ] = 16,
			[ WEAPON_SHOTGUN_PRIMARY ] = 32,

			[ "tf_weapon_pistol" ] = 200,
		},
		[ TF_CLASS_MEDIC ] = 
        {
			[ "tf_weapon_syringegun_medic" ] = 150,
			[ "tf_weapon_crossbow" ]         = 38,
		},
		[ TF_CLASS_SNIPER ] = 
        {
			[ "tf_weapon_sniperrifle" ]         = 25,
			[ "tf_weapon_sniperrifle_decap" ]   = 25,
			[ "tf_weapon_sniperrifle_classic" ] = 25,
			[ "tf_weapon_compound_bow" ]        = 12,

			[ "tf_weapon_smg" ]         = 75,
			[ "tf_weapon_charged_smg" ] = 75,
		},
		[ TF_CLASS_SPY ] = 
        {
			[ "tf_weapon_revolver" ] = 24,
		},
	}

    Classes = [ "", "Scout", "Sniper", "Soldier", "Demo", "Medic", "Heavy", "Pyro", "Spy", "Engineer", "civilian" ] //make element 0 a dummy string instead of doing array + 1 everywhere
    
    // Entities

    function ProgressWinStage()
    {
        inVictoryStage += 1
        // printl("current win stage: "+inVictoryStage)
    }

    function StartFailTimer()
    {
        printl("spawned fail timer")
        local fail_timer = SpawnEntityFromTable("prop_dynamic",
        {
            model = "models/props_vehicles/mining_car_metal.mdl"
            origin = "0 0 0"
            angles = "0 0 0"
            targetname = "fail_timer"
            modelscale = 1
            skin = 1
        })

        fail_timer.DisableDraw()
        fail_timer.ValidateScriptScope()
        local scope = fail_timer.GetScriptScope()
        scope.flFailTime <- 0
        scope.spawnTime <- Time()
        scope.bWarnStage <- 0

        scope.TimerThink <- function() 
        { 
            local cur_time = Time()
            local cur_origin = self.GetOrigin()
            local time_in_minutes = 15
            local total_time = time_in_minutes * 60
            local half_time = total_time * 0.5
            local quarter_time = total_time * 0.25
            local fail_trigger = FindByName(null, "boss_deploy_relay")

            local time_elapsed = cur_time - spawnTime
            local time_remaining = total_time - time_elapsed

            // Warn when 50% time remaining (15s left)
            if (time_remaining <= half_time && bWarnStage == 0)
            {
                local remaining_seconds = time_remaining
                local minutes = floor(remaining_seconds / 60)
                local seconds = floor(remaining_seconds % 60 + 0.5)  // Round to nearest integer
                bWarnStage++
                
                local time_text = ""
                if (seconds > 0)
                    time_text = minutes + "m " + seconds + "s left"
                else
                    time_text = minutes + "m left"
                
                SalvageGamemode.MiscPrintFunc(null, "F52727", "OBJECTIVE WARNING: YOU HAVE " + time_text.tolower() + " TO WIN.")
            }

            // Warn when 25% time remaining (7.5s left)
            if (time_remaining <= quarter_time && bWarnStage == 1)
            {
                local remaining_seconds = time_remaining
                local minutes = floor(remaining_seconds / 60)
                local seconds = floor(remaining_seconds % 60 + 0.5)  // Round to nearest integer
                bWarnStage++
                
                local time_text = ""
                if (seconds > 0)
                    time_text = minutes + "m " + seconds + "s left"
                else
                    time_text = minutes + "m left"
                
                SalvageGamemode.MiscPrintFunc(null, "F52727", "OBJECTIVE WARNING: YOU HAVE " + time_text.tolower() + " TO WIN.")
            }

            // stupid 
            if ((spawnTime + total_time ) <= cur_time && bWarnStage == 2)
            {
                bWarnStage++
                fail_trigger.AcceptInput("trigger", "", null, null)
            }

        }
        

        AddThinkToEnt(fail_timer, "TimerThink")
    }

    function SetupObjectiveWave2()
    {
        local trigger_capture1 = SpawnEntityFromTable("trigger_capture_area",
        {
            area_cap_point = "CapZone_A"
            area_time_to_cap = "30"
            StartDisabled = "0"
            targetname = "capture_a"
            team_cancap_2 = "1"
            team_cancap_3 = "1"
            team_numcap_2 = "1"
            team_numcap_3 = "1"
            team_spawn_2 = "0"
            team_spawn_3 = "0"
            team_startcap_2 = "1"
            team_startcap_3 = "1"

            origin = "2102 -3648 0"
            spawnflags = 1

            // If setting output like this to -1, it crashes the server
            // To remedy this, we'll dynamically add outputs when needed 
            
            // red is capping, shorten the timer
            "OnStartTeam1#1" : "!self,RunScriptCode,SalvageGamemode.SetControlPointCapRate( self RED_CAP_TIME TF_TEAM_RED `CapZone_A` ),0,1"  // red

            // blu capping: raise timer
            "OnStartTeam2#1" : "!self,RunScriptCode,SalvageGamemode.SetControlPointCapRate( self BLU_CAP_TIME TF_TEAM_BLUE `CapZone_A` ),0,1"  // blu

            // when RED caps: give the dummy his capture value, add output setcontrol

            "OnCapTeam1#1" : "!self,RunScriptCode,SalvageGamemode.EnableDummyCapture( self `capture_a` ),0.1,1"
            "OnCapTeam1#2" : "!self,AddOutput,OnStartTeam2 !self:RunScriptCode:SalvageGamemode.SetControlPointCapRate( self BLU_CAP_TIME TF_TEAM_BLUE `CapZone_A` ):0:1,0,-1"
            "OnCapTeam1#3" : "objective_a,skin,1,0,-1"
            "OnCapTeam1#4" : "glow_a,SetGlowColor,184 56 59 255,0,-1"
            "OnCapTeam1#5" : "dtator_point,runscriptcode,SalvageGamemode.CheckWaveProgression( self ),0,1"

            // when BLU caps: addoutput setcontrol
            "OnCapTeam2#1" : "!self,AddOutput,OnStartTeam1 !self:RunScriptCode:SalvageGamemode.SetControlPointCapRate( self RED_CAP_TIME TF_TEAM_RED `CapZone_A` ):0:1,0,-1"
            "OnCapTeam2#2" : "objective_a,skin,2,0,-1"
            "OnCapTeam2#3" : "glow_a,SetGlowColor,88 133 162 255,0,-1"

            // if any RED player is standing on the point, stop capturing
            "OnNumCappersChanged2#1" : "!self,RunScriptCode,SalvageGamemode.OverrideCapture( self `capture_a` ),0,-1"  // blu
        })
        trigger_capture1.SetSolid(SOLID_BBOX)
        trigger_capture1.SetSize(Vector(-160, -128, -25), Vector(160, 128, 25))
        
        local point1 = SpawnEntityFromTable("team_control_point", 
        {
            angles = "0 0 0"
            point_default_owner = "0"
            point_group = "0"
            point_index = "0"
            point_printname = "Zone A!"
            point_start_locked = "0"
            point_warn_on_cap = "0"
            point_warn_sound = "ControlPoint.CaptureWarn"
            random_owner_on_restart = "0"
            spawnflags = "4"
            targetname = "CapZone_A"
            team_bodygroup_0 = "3"
            team_bodygroup_2 = "1"
            team_bodygroup_3 = "1"
            team_icon_0 = "sprites/obj_icons/icon_obj_neutral"
            team_icon_2 = "sprites/obj_icons/icon_obj_red"
            team_icon_3 = "sprites/obj_icons/icon_obj_blu"
            team_model_0 = "models/effects/cappoint_hologram.mdl"
            team_model_2 = "models/effects/cappoint_hologram.mdl"
            team_model_3 = "models/effects/cappoint_hologram.mdl"
            team_overlay_3 = "sprites/obj_icons/icon_obj_a"
            team_overlay_2 = "sprites/obj_icons/icon_obj_a"
            team_overlay_0 = "sprites/obj_icons/icon_obj_a"
            team_timedpoints_2 = "0"
            team_timedpoints_3 = "0"
            origin = "2102 -3648 0"

            team_previouspoint_2_0 = "CapZone_A"
            team_previouspoint_3_0 = "CapZone_A"
        })

        local objective_area1 = SpawnEntityFromTable("prop_dynamic",
        {
            model = "models/props_gameplay/cap_circle_320.mdl"
            origin = "2102 -3648 0"
            angles = "0 0 0"
            targetname = "objective_a"
            modelscale = 1
            skin = 0
        })

        local objective_glow1 = SpawnEntityFromTable("tf_glow", 
        {
            targetname = "glow_a"
            GlowColor = "128 128 128 255"
            target = "objective_a"
        })

        EntFireByHandle(trigger_capture1, "SetControlPoint", "CapZone_A", 0, null, null)
        EntFireByHandle(trigger_capture1, "SetLocked", "", 0.1, null, null)
        EntFireByHandle(point1, "SetUnlockTime", "", 0.1, null, null)

        // point2

        local trigger_capture2 = SpawnEntityFromTable("trigger_capture_area",
        {
            area_cap_point = "CapZone_B"
            area_time_to_cap = "30"
            StartDisabled = "0"
            targetname = "capture_b"
            team_cancap_2 = "1"
            team_cancap_3 = "1"
            team_numcap_2 = "1"
            team_numcap_3 = "1"
            team_spawn_2 = "0"
            team_spawn_3 = "0"
            team_startcap_2 = "1"
            team_startcap_3 = "1"

            origin = "1740 -1624 352"
            spawnflags = 1

            // If setting output like this to -1, it crashes the server
            // To remedy this, we'll dynamically add outputs when needed 
            
            // red is capping, shorten the timer
            "OnStartTeam1#1" : "!self,RunScriptCode,SalvageGamemode.SetControlPointCapRate( self RED_CAP_TIME TF_TEAM_RED `CapZone_B` ),0,1"  // red

            // blu capping: raise timer
            "OnStartTeam2#1" : "!self,RunScriptCode,SalvageGamemode.SetControlPointCapRate( self BLU_CAP_TIME TF_TEAM_BLUE `CapZone_B` ),0,1"  // blu

            // when RED caps: give the dummy his capture value, add output setcontrol

            "OnCapTeam1#1" : "!self,RunScriptCode,SalvageGamemode.EnableDummyCapture( self `capture_b` ),0.1,1"
            "OnCapTeam1#2" : "!self,AddOutput,OnStartTeam2 !self:RunScriptCode:SalvageGamemode.SetControlPointCapRate( self BLU_CAP_TIME TF_TEAM_BLUE `CapZone_B` ):0:1,0,-1"
            "OnCapTeam1#3" : "objective_b,skin,1,0,-1"
            "OnCapTeam1#4" : "glow_b,SetGlowColor,184 56 59 255,0,-1"
            "OnCapTeam1#5" : "dtator_point,runscriptcode,SalvageGamemode.CheckWaveProgression( self ),0,1"

            // when BLU caps: addoutput setcontrol
            "OnCapTeam2#1" : "!self,AddOutput,OnStartTeam1 !self:RunScriptCode:SalvageGamemode.SetControlPointCapRate( self RED_CAP_TIME TF_TEAM_RED `CapZone_B` ):0:1,0,-1"
            "OnCapTeam2#2" : "objective_b,skin,2,0,-1"
            "OnCapTeam2#3" : "glow_b,SetGlowColor,88 133 162 255,0,-1"

            // if any RED player is standing on the point, stop capturing
            "OnNumCappersChanged2#1" : "!self,RunScriptCode,SalvageGamemode.OverrideCapture( self `capture_b` ),0,-1"  // blu
        })
        trigger_capture2.SetSolid(SOLID_BBOX)
        trigger_capture2.SetSize(Vector(-160, -128, -25), Vector(160, 128, 25))
        
        local point2 = SpawnEntityFromTable("team_control_point", 
        {
            angles = "0 0 0"
            point_default_owner = "0"
            point_group = "0"
            point_index = "1" // change this
            point_printname = "Zone B!"
            point_start_locked = "0"
            point_warn_on_cap = "0"
            point_warn_sound = "ControlPoint.CaptureWarn"
            random_owner_on_restart = "0"
            spawnflags = "4"
            targetname = "CapZone_B"
            team_bodygroup_0 = "3"
            team_bodygroup_2 = "1"
            team_bodygroup_3 = "1"
            team_icon_0 = "sprites/obj_icons/icon_obj_neutral"
            team_icon_2 = "sprites/obj_icons/icon_obj_red"
            team_icon_3 = "sprites/obj_icons/icon_obj_blu"
            team_model_0 = "models/effects/cappoint_hologram.mdl"
            team_model_2 = "models/effects/cappoint_hologram.mdl"
            team_model_3 = "models/effects/cappoint_hologram.mdl"
            team_timedpoints_2 = "0"
            team_timedpoints_3 = "0"
            origin = "1740 -1624 352"

            team_overlay_3 = "sprites/obj_icons/icon_obj_b"
            team_overlay_2 = "sprites/obj_icons/icon_obj_b"
            team_overlay_0 = "sprites/obj_icons/icon_obj_b"

            team_previouspoint_2_0 = "CapZone_B"
            team_previouspoint_3_0 = "CapZone_B"
        })

        local objective_area2 = SpawnEntityFromTable("prop_dynamic",
        {
            model = "models/props_gameplay/cap_circle_320.mdl"
            origin = "1740 -1624 352"
            angles = "0 0 0"
            targetname = "objective_b"
            modelscale = 1
            skin = 0
        })

        local objective_glow2 = SpawnEntityFromTable("tf_glow", 
        {
            targetname = "glow_b"
            GlowColor = "128 128 128 255"
            target = "objective_b"
        })

        EntFireByHandle(trigger_capture2, "SetControlPoint", "CapZone_B", 0, null, null)
        EntFireByHandle(trigger_capture2, "SetLocked", "", 0.1, null, null)
        EntFireByHandle(point2, "SetUnlockTime", "", 0.1, null, null)
        SalvageGamemode.SendAnnotation( "Capture ALL control points to begin the wave!", 1, 8, Vector(1740, -1624, 352) )


        // point 3

        local trigger_capture3 = SpawnEntityFromTable("trigger_capture_area",
        {
            area_cap_point = "CapZone_B"
            area_time_to_cap = "30"
            StartDisabled = "0"
            targetname = "capture_c"
            team_cancap_2 = "1"
            team_cancap_3 = "1"
            team_numcap_2 = "1"
            team_numcap_3 = "1"
            team_spawn_2 = "0"
            team_spawn_3 = "0"
            team_startcap_2 = "1"
            team_startcap_3 = "1"

            origin = "1550 571 276"
            spawnflags = 1

            // If setting output like this to -1, it crashes the server
            // To remedy this, we'll dynamically add outputs when needed 
            
            // red is capping, shorten the timer
            "OnStartTeam1#1" : "!self,RunScriptCode,SalvageGamemode.SetControlPointCapRate( self RED_CAP_TIME TF_TEAM_RED `CapZone_C` ),0,1"  // red

            // blu capping: raise timer
            "OnStartTeam2#1" : "!self,RunScriptCode,SalvageGamemode.SetControlPointCapRate( self BLU_CAP_TIME TF_TEAM_BLUE `CapZone_C` ),0,1"  // blu

            // when RED caps: give the dummy his capture value, add output setcontrol

            "OnCapTeam1#1" : "!self,RunScriptCode,SalvageGamemode.EnableDummyCapture( self `capture_c` ),0.1,1"
            "OnCapTeam1#2" : "!self,AddOutput,OnStartTeam2 !self:RunScriptCode:SalvageGamemode.SetControlPointCapRate( self BLU_CAP_TIME TF_TEAM_BLUE `CapZone_C` ):0:1,0,-1"
            "OnCapTeam1#3" : "objective_c,skin,1,0,-1"
            "OnCapTeam1#4" : "glow_c,SetGlowColor,184 56 59 255,0,-1" 
            "OnCapTeam1#5" : "dtator_point,runscriptcode,SalvageGamemode.CheckWaveProgression( self ),0,1"

            // when BLU caps: addoutput setcontrol
            "OnCapTeam2#1" : "!self,AddOutput,OnStartTeam1 !self:RunScriptCode:SalvageGamemode.SetControlPointCapRate( self RED_CAP_TIME TF_TEAM_RED `CapZone_C` ):0:1,0,-1"
            "OnCapTeam2#2" : "objective_c,skin,2,0,-1"
            "OnCapTeam2#3" : "glow_c,SetGlowColor,88 133 162 255,0,-1"

            // if any RED player is standing on the point, stop capturing
            "OnNumCappersChanged2#1" : "!self,RunScriptCode,SalvageGamemode.OverrideCapture( self `capture_c` ),0,-1"  // blu
        })
        trigger_capture3.SetSolid(SOLID_BBOX)
        trigger_capture3.SetSize(Vector(-160, -128, -25), Vector(160, 128, 25))
        
        local point3 = SpawnEntityFromTable("team_control_point", 
        {
            angles = "0 0 0"
            point_default_owner = "0"
            point_group = "0"
            point_index = "2" // change this
            point_printname = "Zone C!"
            point_start_locked = "0"
            point_warn_on_cap = "0"
            point_warn_sound = "ControlPoint.CaptureWarn"
            random_owner_on_restart = "0"
            spawnflags = "4"
            targetname = "CapZone_C"
            team_bodygroup_0 = "3"
            team_bodygroup_2 = "1"
            team_bodygroup_3 = "1"
            team_icon_0 = "sprites/obj_icons/icon_obj_neutral"
            team_icon_2 = "sprites/obj_icons/icon_obj_red"
            team_icon_3 = "sprites/obj_icons/icon_obj_blu"
            team_model_0 = "models/effects/cappoint_hologram.mdl"
            team_model_2 = "models/effects/cappoint_hologram.mdl"
            team_model_3 = "models/effects/cappoint_hologram.mdl"
            team_timedpoints_2 = "0"
            team_timedpoints_3 = "0"
            origin = "1550 571 276"

            team_overlay_3 = "sprites/obj_icons/icon_obj_c"
            team_overlay_2 = "sprites/obj_icons/icon_obj_c"
            team_overlay_0 = "sprites/obj_icons/icon_obj_c"

            team_previouspoint_2_0 = "CapZone_C"
            team_previouspoint_3_0 = "CapZone_C"
        })

        local objective_area3 = SpawnEntityFromTable("prop_dynamic",
        {
            model = "models/props_gameplay/cap_circle_320.mdl"
            origin = "1550 571 276"
            angles = "0 0 0"
            targetname = "objective_c"
            modelscale = 1
            skin = 0
        })

        local objective_glow3 = SpawnEntityFromTable("tf_glow", 
        {
            targetname = "glow_c"
            GlowColor = "128 128 128 255"
            target = "objective_c"
        })

        EntFireByHandle(trigger_capture3, "SetControlPoint", "CapZone_C", 0, null, null)
        EntFireByHandle(trigger_capture3, "SetLocked", "", 0.1, null, null)
        EntFireByHandle(point3, "SetUnlockTime", "", 0.1, null, null)

        // spawn last because hud elements wont appear otherwise
        local objective_master = SpawnEntityFromTable("team_control_point_master",
        {
            caplayout = "1,0 2"
            cpm_restrict_team_cap_win = "1"
            custom_position_x = "0.6"
            custom_position_y = "-1"
            partial_cap_points_rate = "0"
            play_all_rounds = "0"
            score_style = "0"
            switch_teams = "0"
            targetname = "dtator_point"
            team_base_icon_2 = "sprites/obj_icons/icon_base_red"
            team_base_icon_3 = "sprites/obj_icons/icon_base_blu"
            origin = "0 0 0"
        })
        
        for (local i; i = FindByClassname(i,"info_player_teamspawn");)
        {
            if (!(startswith( "spawnbot_", i.GetName() )) )
            {
                EntFireByHandle(i, "disable", "", 0.1, null, null)
            }
        }

        objective_master.ValidateScriptScope()
        local scope = objective_master.GetScriptScope()
        objective_master.GetScriptScope().inCappingStage <- 0
        objective_master.GetScriptScope().flBluWinProgression <- 0
        objective_master.GetScriptScope().flTimestampAdd <- 0
        objective_master.GetScriptScope().lose_bool <- false

        objective_master.GetScriptScope().controlObjectiveThink <- function()
        {
            local points_loss = 100
            local cur_time = Time()

            if ( ( flTimestampAdd + 0.5 ) <= cur_time )
            {
                printl("trigger")
                local points_count = 0

                for (local control_point; control_point = FindByClassname(control_point, "team_control_point");)
                {
                    if ( control_point.GetTeam() == TF_TEAM_BLUE )
                    {
                        points_count++
                    }
                }

                local progress_add = RandomFloat(0.65, 1.75)
                local progress_mult = progress_add * points_count
                local rounding = SalvageGamemode.Round( progress_mult, 2 ) 

                flBluWinProgression += rounding

                if ( flBluWinProgression >= points_loss )
                {
                    flBluWinProgression = points_loss

                    if ( lose_bool == false )
                    {
                        lose_bool = true

                        local hatch_ent = FindByName(null, "boss_deploy_relay")
                        hatch_ent.AcceptInput("trigger", "", null, null)
                    }
                }

                flTimestampAdd = cur_time
            }

            return -1
        }
        AddThinkToEnt(objective_master, "controlObjectiveThink")
    }

    function SetupObjectiveWave1()
    {
        //  4086.2 -1137.6 448.0
        // printl("begin objective")

        local objective_area = SpawnEntityFromTable("prop_dynamic",
        {
            model = "models/props_gameplay/cap_circle_320.mdl"
            origin = "4117 -1180 460"
            angles = "0 0 0"
            targetname = "objective"
            modelscale = 1
            skin = 1
        })

        local objective_glow = SpawnEntityFromTable("tf_glow", 
        {
            GlowColor = "201 36 36 255"
            target = "objective"
        })

        local minecart_red = SpawnEntityFromTable("prop_dynamic",
        {
            model = "models/props_vehicles/mining_car_metal.mdl"
            origin = "4117 -1180 448"
            angles = "0 0 0"
            targetname = "objective2"
            modelscale = 1
            skin = 1
        })

        //models/props_vehicles/mining_car_metal.mdl

        printl("Spawned objective: "+objective_area)

        objective_area.ValidateScriptScope()
        local scope = objective_area.GetScriptScope()
        scope.flCollectStamp <- 0
        scope.hCartModel <- minecart_red
        scope.hCartStage <- 0

        for (local player; player = FindByClassname(player, "player");)
        {
            if (player.IsBotOfType(1337) && player.HasBotTag("bot_scrap_healthbar"))
            {
                player.SetHealth(100)
            }
        }

        scope.ObjectiveThink <- function() 
        { 
            local cur_time = Time()
            local cur_origin = self.GetOrigin()

            if ( SalvageGamemode.inVictoryStage == 2 )
            {
                // local win_red = SpawnEntityFromTable("game_round_win",
                // {
                //     force_map_reset = "1"
                //     switch_teams = "0"
                //     targetname = "red_win"
                //     TeamNum = 2
                //     origin = "4928 -992 577"
                // })
                local win_ent = CreateByClassname("point_populator_interface")
                DispatchSpawn(win_ent)

                
                win_ent.AcceptInput("$FinishWave", "", null, null)
                win_ent.Destroy()
                hCartModel.Destroy()
                self.Destroy()

                return -1
            }

            function ProgressScrapCollection()
            {
                for (local player; player = FindByClassname(player, "player");)
                {
                    if (player.IsBotOfType(1337) && player.HasBotTag("bot_scrap_healthbar"))
                    {
                        local cur_health = player.GetHealth()
                        local max_health = player.GetMaxHealth()

                        player.SetHealth( cur_health + 100 ) // 100 scrap to win

                        // first 25%
                        if ( cur_health >= (max_health * 0.25) && hCartStage == 0 ) 
                        {
                            hCartStage = 1
                            hCartModel.SetModelSimple("models/props_vehicles/mining_cart_supplies001.mdl")
                        }

                        // at 25%
                        if ( cur_health >= (max_health * 0.75) && hCartStage == 1 ) 
                        {
                            hCartStage = 2
                            hCartModel.SetModelSimple("models/props_vehicles/mining_cart_supplies002.mdl")
                        }

                        // win :D
                        if ( cur_health >= max_health && hCartStage == 2 ) 
                        {
                            hCartStage = 3
                            SalvageGamemode.ProgressWinStage()
                        }
                    }
                }
            }


            if ((flCollectStamp + 0.5 ) <= cur_time)
            {
                printl("collect")

                for (local player; player = FindByClassnameWithin(player, "player", cur_origin, 320);)
                {
                    if ( player.GetTeam() == TF_TEAM_RED )
                    {
                        local scope_p = player.GetScriptScope()

                        if ( scope_p.inScrapAmount >= 1 )
                        {
                            scope_p.inScrapAmount -= 1
                            ProgressScrapCollection()
                            

                            local sound_range = ( 40 + ( 20 * log10( 4000 / 36.0 ) ) ).tointeger();

                            local channel = RandomInt(40, 80)

                            EmitSoundEx
                            ({
                                sound_name = "ui/item_metal_scrap_drop.wav",
                                origin = cur_origin,
                                volume = 1,
                                sound_level = sound_range,
                                channel = channel,
                                entity = player,
                                filter_type = RECIPIENT_FILTER_SINGLE_PLAYER
                            });
                            EmitSoundEx
                            ({
                                sound_name = "ui/item_metal_scrap_drop.wav",
                                origin = cur_origin,
                                volume = 1,
                                sound_level = sound_range,
                                channel = channel,
                                entity = player,
                                filter_type = RECIPIENT_FILTER_SINGLE_PLAYER
                            });
                        }
                        printl("player: "+player)
                    }
                }
                flCollectStamp = cur_time
            }

            return -1 
        }
        
        AddThinkToEnt(objective_area, "ObjectiveThink")

    }

    // ON TAKE DAMAGE

    // Thinks

    function ControlPointThink()
    {
        local cur_wave = GetPropInt( mvm_logic_entity, "m_nMannVsMachineWaveCount" )
        local in_wave = GetPropInt( mvm_logic_entity, "m_bMannVsMachineBetweenWaves" )

        // wave 2 and wave is active
        if ( cur_wave == 2 && in_wave == 0 )
        {
            local entity_counter = FindByName(null, "dtator_point")

            if ( entity_counter == null || !entity_counter.IsValid() )
            {
                return
            }

            entity_counter.ValidateScriptScope()
            
            local cur_progress = entity_counter.GetScriptScope().flBluWinProgression

            SalvageGamemode.DisplayHUD( self, "BLU Control Progress: "+cur_progress+"/100", 1, 0.05, 0.35, 0, false)
        }
    }

    function ScrapThink()
    {
        local cur_wave = GetPropInt( mvm_logic_entity, "m_nMannVsMachineWaveCount" )
        local in_wave = GetPropInt( mvm_logic_entity, "m_bMannVsMachineBetweenWaves" )

        // wave 1 and wave is active
        if ( cur_wave == 1 && in_wave == 0 )
        {
            SalvageGamemode.DisplayHUD( self, "Scrap Collected: "+inScrapAmount, 1, 0.05, 0.35, 0, false)
        }
    }

    function ApplyProjectileThink()
    {
        for ( local projectile; projectile = FindByClassname( projectile, "tf_projectile*" ); ) 
        {
            projectile.ValidateScriptScope()
            local scope = projectile.GetScriptScope()

            if ( ( "HasThink" in scope ) )
            {
                continue
            }

            local owner_netprop = GetPropEntity( projectile, "m_hOwner" )
            local thrower_netprop = GetPropEntity( projectile, "m_hThrower" )
            local owner_launcher = GetPropEntity( projectile, "m_hLauncher" )
            local owner_entity = GetPropEntity( projectile, "m_hOwnerEntity" )

            local AddThinkBool = false

            // iterating over table is slow
            if ( owner_netprop != null )
            {
                AddThinkBool = true
            }
            else if ( thrower_netprop != null )
            {
                AddThinkBool = true
            }
            else if ( owner_launcher != null )
            {
                AddThinkBool = true
            }
            else if ( owner_entity != null )
            {
                AddThinkBool = true
            }

            if ( AddThinkBool == false )
            {
                return
            }

            // // printl( "applying for: "+projectile )

            scope.ProjectileThink <- function() 
            { 
                foreach ( name, func in ThinkTable_Projectile ) 
                {
                    if ( func )
                    {
                        func.call( this )
                    }
                }

                return -1 
            }
            
            scope.HasThink <- 1
            scope.flCurLifetime <- 0
            scope.flActualLifetime <- 0
            scope.flBeginLifetime <- Time()
            scope.ThinkTable_Projectile <- {}

            AddThinkToEnt( projectile, "ProjectileThink" )
        }
    }

    function HomingProjectile()
    {
        if ( hLockTarget == null || !hLockTarget.IsValid() || !hLockTarget.IsAlive() )
        {
            return
        }

        if ( self == null || !self.IsValid() )
        {
            return 
        }


        local vecTargetOrigin = hLockTarget.GetAttachmentOrigin( hLockTarget.LookupAttachment( "flag" ) )
        local vecProjectileOrigin = self.GetOrigin()
        local vecCurrentVelocity = self.GetVelocity()
        
        // Calculate direction to target
        local vecToTarget = vecTargetOrigin - vecProjectileOrigin
        local flTargetDistance = vecToTarget.Length()
        if ( flTargetDistance > 0 )
            vecToTarget = vecToTarget * ( 1.0 / flTargetDistance ) // Normalize
        
        // Get current direction
        local vecCurrentDirection = Vector( 0, 0, 0 )
        local flCurrentSpeed = vecCurrentVelocity.Length()
        if ( flCurrentSpeed > 0 )
            vecCurrentDirection = vecCurrentVelocity * ( 1.0 / flCurrentSpeed ) // Normalize
        
        // Calculate homing factors ( 360 strength and 90 turn speed = perfect tracking )
        local flHomingFactor = iHomingStrength / 360.0  // 1.0 = perfect homing
        local flTurnSpeedFactor = iHomingTurnSpeed / 90.0  // 1.0 = perfect turning
        
        // Calculate how much we can turn this frame
        local flFrameTime = FrameTime()
        local flMaxTurnThisFrame = flTurnSpeedFactor * flFrameTime * 5.0 // Adjust multiplier as needed
        flMaxTurnThisFrame = ( flMaxTurnThisFrame > 1.0 ) ? 1.0 : flMaxTurnThisFrame
        
        // Calculate desired direction based on homing strength
        local vecDesiredDirection = vecCurrentDirection * ( 1.0 - flHomingFactor ) + vecToTarget * flHomingFactor
        local flDesiredLength = vecDesiredDirection.Length()
        if ( flDesiredLength > 0 )
            vecDesiredDirection = vecDesiredDirection * ( 1.0 / flDesiredLength ) // Normalize
        
        // Apply turn speed limitation
        local vecFinalDirection = vecCurrentDirection * ( 1.0 - flMaxTurnThisFrame ) + vecDesiredDirection * flMaxTurnThisFrame
        local flFinalLength = vecFinalDirection.Length()
        if ( flFinalLength > 0 )
            vecFinalDirection = vecFinalDirection * ( 1.0 / flFinalLength ) // Normalize
        
        // Set velocity at constant speed
        local vecNewVelocity = vecFinalDirection * iMaxVelocity
        self.SetAbsVelocity( vecNewVelocity )

        // Turn projectile toward target 

        local vecTurn = SalvageGamemode.VectorAngles( vecFinalDirection )
        self.SetAbsAngles( vecTurn )
    }

    function HomingProjectileAlly()
    {
        // Check if target is still valid
        if ( !("hLockTarget" in this) || hLockTarget == null || !hLockTarget.IsValid() || !hLockTarget.IsAlive() )
        {
            return // Stop homing if target is invalid
        }
        
        // Check if projectile is still valid
        if ( self == null || !self.IsValid() )
        {
            return
        }

        // force heal
        if ( hLockTarget.GetHealth() == hLockTarget.GetMaxHealth() )
        {
            printl("force damage to heal")
            hLockTarget.SetHealth( hLockTarget.GetMaxHealth() - 1 )
        }
        
        // Get positions
        local vecTargetOrigin = hLockTarget.GetAttachmentOrigin(hLockTarget.LookupAttachment("flag"))
        local vecProjectileOrigin = self.GetOrigin()
        local vecCurrentVelocity = self.GetVelocity()
        
        // Calculate direction to target
        local vecToTarget = vecTargetOrigin - vecProjectileOrigin
        local flTargetDistance = vecToTarget.Length()
        if ( flTargetDistance > 0 )
            vecToTarget = vecToTarget * (1.0 / flTargetDistance) // Normalize
        
        // Get current direction
        local flCurrentSpeed = vecCurrentVelocity.Length()
        local vecCurrentDirection = Vector(0, 0, 0)
        if ( flCurrentSpeed > 0 )
            vecCurrentDirection = vecCurrentVelocity * (1.0 / flCurrentSpeed) // Normalize
        
        // Calculate homing factors
        local flHomingFactor = iHomingStrength / 360.0  // 1.0 = perfect homing
        local flTurnSpeedFactor = iHomingTurnSpeed / 90.0  // 1.0 = instant turn
        
        // Calculate turn rate based on frame time
        local flFrameTime = FrameTime()
        local flMaxTurnThisFrame = flTurnSpeedFactor * flFrameTime * 5.0
        flMaxTurnThisFrame = (flMaxTurnThisFrame > 1.0) ? 1.0 : flMaxTurnThisFrame
        
        // Calculate desired direction with homing strength
        local vecDesiredDirection = vecCurrentDirection * (1.0 - flHomingFactor) + vecToTarget * flHomingFactor
        local flDesiredLength = vecDesiredDirection.Length()
        if ( flDesiredLength > 0 )
            vecDesiredDirection = vecDesiredDirection * (1.0 / flDesiredLength)
        
        // Apply turn speed limitation
        local vecFinalDirection = vecCurrentDirection * (1.0 - flMaxTurnThisFrame) + vecDesiredDirection * flMaxTurnThisFrame
        local flFinalLength = vecFinalDirection.Length()
        if ( flFinalLength > 0 )
            vecFinalDirection = vecFinalDirection * (1.0 / flFinalLength)
        
        // Set new velocity at constant speed
        local vecNewVelocity = vecFinalDirection * iMaxVelocity
        self.SetAbsVelocity(vecNewVelocity)
        
        // Rotate projectile to face direction
        local vecAngles = SalvageGamemode.VectorAngles(vecFinalDirection)
        self.SetAbsAngles(vecAngles)
    }

    // MISSION SPECIFIC FUNCTIONS

    function SetupRailgun( player )
    {
        local primary = SalvageGamemode.GetItemInSlot(player, 0)
        local attribute_special = primary.GetAttribute("kill eater user 1", 0)

        if ( attribute_special == 0 )
        {
            return
        }

        function RailgunWep()
        {
            local primary = SalvageGamemode.GetItemInSlot(self, 0)
            local attribute_special = primary.GetAttribute("kill eater user 1", 0)
            local cur_wep = self.GetActiveWeapon()

            if ( cur_wep != primary || attribute_special == 0 )
                return 

            local buttons = GetPropInt(self, "m_nButtons");
            local buttons_changed = buttons_last ^ buttons;
            local buttons_pressed = buttons_changed & buttons;
            local buttons_released = buttons_changed & (~buttons);
            local inspect_stage = GetPropInt(primary, "m_nInspectStage")
            local cur_time = Time()

            local string = "\n+Alt-Fire: Powerful, accurate projectile \nwith pierce & headshot.\nDeals damage based on charge.\nReduced charge gain per hit\nvs Giants, Tanks and Buildings."
            local max_charge = 100

            if ( inspect_stage != -1 )
            {
                printl("added inspect to table")
                stringTable.string_8888 <- string
            }

            if ( flRailgunEnergy > max_charge )
            {
                flRailgunEnergy = 100
            }
            SalvageGamemode.DisplayHUD( self, "Primary Charge: "+flRailgunEnergy+"/"+max_charge, 1, 0.05, 0.35, 0, false)

            if ( buttons_pressed & KEY_SECONDARY_FIRE && ( flAttribTime + 1.5 ) <= cur_time)
            {
                printl("alt fire!")

                if ( flRailgunEnergy > 0 )
                {
                    printl("successful alt fire")
                    flAttribTime = cur_time

                    local dmg_bonus_max = 35.0
                    local dmg_bonus_min = 1.5
                    local dmg_calc = SalvageGamemode.ScaleValue(dmg_bonus_min, dmg_bonus_max, flRailgunEnergy, 100)
                    dmg_calc = dmg_calc * SalvageGamemode.MULT_DAMAGE(self, primary)
                    primary.AddAttribute("weapon spread bonus", 0, -1)
                    primary.AddAttribute("sniper fires tracer", 1, -1)
                    primary.AddAttribute("card: damage bonus", dmg_calc, -1)
                    primary.AddAttribute("dmg pierces resists absorbs", 1, -1)
                    primary.AddAttribute("revolver use hit locations", 1, -1)
                    primary.AddAttribute("projectile penetration", 1, -1)
                    // self.AddCustomAttribute("weapon spread bonus", 0, 0.15)
                    
                    primary.SetClip1( primary.Clip1() + 1)
                    SetPropFloat( primary, "m_flNextPrimaryAttack", 0 )
                    
                    // EntFireByHandle(primary, "RunScriptCode", "self.PrimaryAttack()", 0.1, primary, primary)
                    primary.PrimaryAttack()

                    local muzzle = primary.GetAttachmentOrigin( primary.LookupAttachment("muzzle") )
                    self.AddCondEx( 46, 0.25, null ) // focus

                    local sound_range = ( 40 + ( 20 * log10( 4000 / 36.0 ) ) ).tointeger();

                    local channel = RandomInt(40, 80)

                    EmitSoundEx
                    ({
                        sound_name = "weapons/cow_mangler_explosion_charge_06.wav",
                        origin = self.GetOrigin(),
                        volume = 1,
                        sound_level = sound_range,
                        channel = channel,
                        entity = self,
                        filter_type = RECIPIENT_FILTER_SINGLE_PLAYER
                    });

                    // tracer logic: old code copied cuz im lazy
                    local tracer_bullet = SpawnEntityFromTable("info_particle_system", 
                    {
                        effect_name = "sniper_dxhr_rail_red", 
                        targetname = "bolt_zap_init_", 
                        origin = self.EyePosition(), 
                        angles = self.EyeAngles()    
                    });

                    // Calculate the target position based on the player's eye angles
                    local eye_angles = self.EyeAngles();  // Get player's eye angles
                    local forward_vector = eye_angles.Forward(); // Forward direction based on eye angles

                    // The distance for tracer_targ can be adjusted by changing the multiplier
                    local tracer_targ_position = self.EyePosition() + forward_vector * 1000; 

                    local tracer_targ = SpawnEntityFromTable("info_target", 
                    {
                        targetname = "zap_targ_init", 
                        spawnflags = 1,
                        origin = tracer_targ_position, 
                        angles = Vector(0, 0, 0)      
                    });

                    // Set angles and control points for the tracer bullet
                    tracer_bullet.SetAbsAngles(self.EyeAngles());  // Align the bullet with the player's view angles
                    SetPropEntityArray(tracer_bullet, "m_hControlPointEnts", tracer_targ, 0);  // Set the target for the tracer bullet

                    // Fire the tracer and handle cleanup
                    tracer_bullet.AcceptInput( "Start", "", null, null)
                    tracer_targ.AcceptInput("setparent", "!activator", tracer_bullet, tracer_targ);  // Parent the target to the bullet

                    // Set a delay to stop and clean up the tracer and target
                    EntFireByHandle(tracer_bullet, "Stop", "", 0.3, null, null);  // Stop the effect after 0.3 seconds
                    EntFireByHandle(tracer_bullet, "Kill", "", 0.6, null, null);  // Remove the bullet after 0.6 seconds
                    EntFireByHandle(tracer_targ, "Kill", "", 0.6, null, null);    // Remove the target after 0.6 seconds

                    // explosive damage

                    // local targets = SalvageGamemode.GetEnemiesWithinArea( self, 550, tracer_targ_position )
                    // local damage = 55.0
                    // local dmg_mult_min = 1.0
                    // local dmg_mult_max = 2.0
                    // local dmg_total = SalvageGamemode.ScaleValue(dmg_mult_min, dmg_mult_max, flRailgunEnergy, 100)

                    // foreach (i, player in targets)
                    // {
                    //     printl("attacking enemy: "+ player)

                    //     if ( bEnableHeadshotExplosion == false )
                    //         continue
                        
                    //     player.TakeDamageCustom
                    //     ( 
                    //         primary, 
                    //         self, 
                    //         primary, 
                    //         Vector( 0, 0, 0 ), 
                    //         player.GetOrigin(), 
                    //         damage * dmg_total, 
                    //         DMG_PREVENT_PHYSICS_FORCE | TF_DMG_BULLET,
                    //         TF_DMG_CUSTOM_PLASMA_CHARGED
                    //     )
                    // }


                    flRailgunEnergy = 0
                    bEnableHeadshotExplosion = false
                }
            }

            if ( (flAttribTime + 0.5 ) >= cur_time )
            {
                printl("need remove")
                primary.RemoveAttribute("weapon spread bonus")
                primary.RemoveAttribute("sniper fires tracer")
                primary.RemoveAttribute("card: damage bonus")
                primary.RemoveAttribute("dmg pierces resists absorbs")
                primary.RemoveAttribute("revolver use hit locations")
                primary.RemoveAttribute("projectile penetration")
            }
            
            buttons_last = buttons
        }
        printl( primary )
        SetPropInt(primary, "m_iPrimaryAmmoType", 1);

        GetEntityThinkTable( player ).RailgunWep <- RailgunWep
        player.GetScriptScope().flRailgunEnergy <- 0
        player.GetScriptScope().flAttribTime <- 0
        player.GetScriptScope().bEnableHeadshotExplosion <- false
    }

    function ChargeRailgun()
    {
        printl("testing rail")
        if ( "flRailgunEnergy" in attacker.GetScriptScope() )
        {
            printl("railing")

            local dmg_full_charge = 800.0
            local charge_per_hit = (damage / dmg_full_charge) * 100.0
            local rounding = SalvageGamemode.Round(charge_per_hit, 2)
            
            if ( !victim.IsPlayer() || victim.IsMiniBoss() )
            {
                rounding = rounding * 0.65
            }

            attacker.GetScriptScope().flRailgunEnergy += rounding

            // if ( GetPropInt( victim, "m_LastHitGroup" ) == HITGROUP_HEAD && weapon.GetAttribute("revolver use hit locations", 0 ) != 0 )
            // {
            //     printl("allow explosive headshot")
            //     attacker.GetScriptScope().bEnableHeadshotExplosion = true
            // }
        }
    }

    function CauseHeadshot()
    {
        if ( !victim.IsPlayer() || GetPropInt( victim, "m_LastHitGroup" ) != HITGROUP_HEAD || !(attacker.IsBotOfType(TF_BOT_TYPE) && attacker.HasBotTag("can_headshot")) )
			return

        params.damage_type = params.damage_type | DMG_CRITICAL
        params.damage_stats = TF_DMG_CUSTOM_HEADSHOT
    }

    function PsychoBuff()
    {
        local cur_time = Time()
        local primary = SalvageGamemode.GetItemInSlot(self, 0)
        // if cur time is above our timestamp, no boost
        if ( (inPsychoboostTime + 5 ) >= cur_time )
        {
            self.AddCondEx( TF_COND_SODAPOPPER_HYPE, -1, null )
            self.AddCondEx( TF_COND_SPEED_BOOST, -1, null)
            self.AddCustomAttribute("fire rate bonus", 0.5, -1)
            // if we are a minigun heavy, dont allow this
            if ( !(self.GetPlayerClass() == TF_CLASS_HEAVYWEAPONS && self.GetActiveWeapon() == primary) )
            {
                self.AddCustomAttribute("faster reload rate", -1, -1)
            }
            self.AddCustomAttribute("CARD: move speed bonus", 1.2, -1)
        }
        else 
        {
            self.RemoveCondEx( TF_COND_SODAPOPPER_HYPE, true )
            self.RemoveCondEx( TF_COND_SPEED_BOOST, true )
            self.RemoveCustomAttribute("fire rate bonus")
            self.RemoveCustomAttribute("faster reload rate")
            self.RemoveCustomAttribute("CARD: move speed bonus")
        }
    }

    function CheckWaveProgression( entity )
    {
        entity.ValidateScriptScope()
        local scope = entity.GetScriptScope()
        scope.inCappingStage++

        printl("progression: "+scope.inCappingStage)

        if ( scope.inCappingStage == 3 )
        {
            local wave_delay = 8
            printl("begin wave!")
            for (local i; i = FindByClassname(i,"info_player_teamspawn");)
            {
                if (!(startswith( "spawnbot_", i.GetName() )) )
                {
                    EntFireByHandle(i, "enable", "", wave_delay, null, null)
                }
            }

            SalvageGamemode.PlaySoundToTeam( TF_TEAM_RED, "vo/announcer_begins_5sec.mp3",3, 4, 1)
            SalvageGamemode.PlaySoundToTeam( TF_TEAM_RED, "vo/announcer_begins_4sec.mp3",4, 4, 1)
            SalvageGamemode.PlaySoundToTeam( TF_TEAM_RED, "vo/announcer_begins_3sec.mp3",5, 4, 1)
            SalvageGamemode.PlaySoundToTeam( TF_TEAM_RED, "vo/announcer_begins_2sec.mp3",6, 4, 1)
            SalvageGamemode.PlaySoundToTeam( TF_TEAM_RED, "vo/announcer_begins_1sec.mp3",7, 4, 1)
        }
    }

    function EnableDummyCapture( ent, tag )
    {
        printl("enabling dummy capture...")
        

        for (local player; player = FindByClassname(player, "player");)
        {
            if ( player.GetTeam() == TEAM_SPECTATOR )
                continue
            
            if ( player.IsBotOfType( TF_BOT_TYPE) && player.HasBotTag(tag) )
            {
                printl("found bot: "+player+" removing tag....")
                player.RemoveCustomAttribute("increase player capture value")

                player.ValidateScriptScope()
                local scope = player.GetScriptScope()

                printl("attempting to add entity")
                if ( !("CaptureEntity" in scope) )
                {
                    printl("adding to scope")

                    local ent_origin = ent.GetOrigin()

                    player.Teleport( true, ent_origin, false, QAngle(), false, Vector() )
                    scope.CaptureEntity <- ent
                    GetEntityThinkTable( player ).DummyThink <- function()
                    {
                        local player_list = []

                        local ent_origin = CaptureEntity.GetOrigin()
                        local cur_origin = self.GetOrigin()
                        
                        // Collect list of all players
                        for (local player; player = FindByClassname(player, "player");)
                        {
                            if ( player.GetTeam() == TEAM_SPECTATOR || SalvageGamemode.CheckEntityIntersection(CaptureEntity, player) == false )
                                continue
                            
                            player_list.append(player)
                        }
                        
                        // check what team all the cappers are on
                        local force_stop_cap = false 
                        foreach ( i, player in player_list )
                        {
                            if ( player.GetTeam() == TEAM_RED )
                            {
                                force_stop_cap = true
                                break
                            }
                        }

                        if ( force_stop_cap == false )
                        {
                            self.RemoveCustomAttribute("increase player capture value")
                        }
                        else 
                        {
                            self.AddCustomAttribute("increase player capture value", -1, -1)
                        }
                    }
                }
            }
        }
    }

    function InspectAttributes()
    {
        local wep = self.GetActiveWeapon()
        local inspect_stage = GetPropInt(wep, "m_nInspectStage")
        local inspect_string = "ACTIVE WEAPON ATTRIBUTES:"

        // combine into one message

        foreach (i, entry in stringTable)
        {
            inspect_string += entry
        }

        // display

        if ( inspect_stage != -1  )
        {
            SalvageGamemode.DisplayHUD( self, inspect_string, 1, 0.05, 0.35, 0, false)
        }
        else 
        {
            stringTable = SalvageGamemode.IterateArray( stringTable, function(slot, entry)
            {
                return REMOVE_ENTRY
            })
        }
    }

    function CheckCustomAttributes( player )
    {
        if ( player.GetTeam() == TEAM_SPECTATOR || GetEntityThinkTable( player ) == null )
            return

        printl("Checking custom attribs of: "+player)
        printl( "table: "+GetEntityThinkTable( player ) )
        GetEntityThinkTable( player ).InspectAttributes <- InspectAttributes
        player.GetScriptScope().strInspectString <- ""
        player.GetScriptScope().stringTable <- {}

        local cur_loadout = SalvageGamemode.GetLoadout( player )

        SalvageGamemode.PrintTable( GetEntityThinkTable( player ) )

        local attributes_custom = 
        {
            // weapon cannot be deployed
            "cannot_transmute" : function()
            {
                local wep = self.GetActiveWeapon()
                local attribute_special = wep.GetAttribute("cannot_transmute", 0)
                local primary = SalvageGamemode.GetItemInSlot(self, 0)

                if ( attribute_special == 0 )
                    return 


                printl("currently active")

                self.Weapon_Switch(primary)
            }
            // Patient heal bonus
            "strange restriction type 1" : function()
            {
                local wep = self.GetActiveWeapon()
                local attribute_special = wep.GetAttribute("strange restriction type 1", 0)

                if ( attribute_special == 0 )
                    return 

                local attrib_mult = (0.15 * attribute_special ) + 1
                local percent = (attrib_mult * 100 ) - 100
                local heal_target = self.GetHealTarget()
                local inspect_stage = GetPropInt(wep, "m_nInspectStage")
                local string = "\n+Patient moves "+percent+"% faster"
                local id = 0

                if ( inspect_stage != -1 )
                {
                    printl("added inspect to table")
                    stringTable.string_1 <- string
                }

                if ( heal_target != null )
                {
                    heal_target.AddCustomAttribute( "SET BONUS: move speed set bonus", attrib_mult, 0.5 )
                }
            }
            // Heal patient max health // super savior
            "strange restriction type 2" : function()
            {
                local wep = self.GetActiveWeapon()
                local attribute_special = wep.GetAttribute("strange restriction type 2", 0)

                if ( attribute_special == 0 )
                    return 

                if ( !("flMedicPulseCooldown" in self.GetScriptScope() ))
                {
                    printl("added medic pulse cooldown")
                    self.GetScriptScope().flMedicPulseCooldown <- 0
                    self.GetScriptScope().buttons_last <- 0
                }

                // bug here
                local buttons = GetPropInt(self, "m_nButtons");
                local buttons_changed = buttons_last ^ buttons;
                local buttons_pressed = buttons_changed & buttons;
                local buttons_released = buttons_changed & (~buttons);

                local heal_target = self.GetHealTarget()
                local inspect_stage = GetPropInt(wep, "m_nInspectStage")
                local cur_time = Time()

                local uber_saved_per_tick = 5
                local base_uber_spend = 35
                local uber_spent = base_uber_spend - 
                ( 
                    (uber_saved_per_tick * attribute_special) - uber_saved_per_tick 
                )

                local base_health_heal = 12.5
                local heal_percent = base_health_heal * attribute_special

                local base_cooldown = 30
                local string = "\n+Reload key: Spend "+uber_spent+"% Ubercharge to \nINSTANTLY heal "+heal_percent+"% of patient\nmax health. "+base_cooldown+"s cooldown."

                local secondary = SalvageGamemode.GetItemInSlot(self, 1)
                local cur_uber = GetPropFloat(secondary, "m_flChargeLevel")

                local time = ceil((flMedicPulseCooldown + base_cooldown) - cur_time)

                if ( inspect_stage != -1 )
                {
                    printl("added inspect to table")
                    stringTable.string_1 <- string
                }
                else 
                {
                    if ( time >= 0)
                    {
                        SalvageGamemode.DisplayHUD( self, "Alt-Fire Cooldown: "+time, 1, 0.1, 0.55, 0, false)
                    }
                    else 
                    {
                        SalvageGamemode.DisplayHUD( self, "Alt-Fire: Spend "+base_uber_spend+"% Uber to\nHeal Patient.", 1, 0.05, 0.35, 0, false)
                    }
                }

                // heal target will be null if our medigun isnt active
                if ( buttons_pressed & KEY_SECONDARY_FIRE )
                {
                    printl("ability trigger")

                    if ( heal_target != null )
                    {
                        cur_uber = cur_uber * 100
                        printl("cur uber: "+cur_uber)
                        // allow ability if:
                        // 1) were below 100% uber
                        // 2) we ourselves arent ubered
                        // 3) our cooldown is ready
                        if 
                        ( 
                            cur_uber < 100
                            && cur_uber >= uber_spent
                            && !self.InCond( TF_COND_INVULNERABLE )
                            && ( flMedicPulseCooldown + base_cooldown ) <= cur_time
                        )
                        {

                            local patient_health = heal_target.GetHealth()
                            local patient_max_health = heal_target.GetMaxHealth()

                            // Amount to add
                            local health_heal = patient_max_health * (heal_percent / 100)

                            // Cap it so we don't exceed max health
                            local health_room = patient_max_health - patient_health
                            if (health_heal > health_room)
                            {
                                health_heal = health_room
                            }

                            printl("health gain: " + health_heal)
                            heal_target.SetHealth(patient_health + health_heal)

                            // Subtract uber, clamped to 0
                            local cur_uber = GetPropFloat(secondary, "m_flChargeLevel")
                            local uber_set = cur_uber - uber_spent
                            if (uber_set < 0.0) uber_set = 0.0
                            SetPropFloat(secondary, "m_flChargeLevel", uber_set)

                            SalvageGamemode.DispatchParticleEffectEx( "spell_overheal_red", heal_target.GetOrigin(), Vector(), [-1, 0.5], null, null )
                            SalvageGamemode.DispatchParticleEffectEx( "medic_healradius_red_buffed", heal_target.GetOrigin(), Vector(), [-1, 0.5], null, null )
                            local sound_range = ( 40 + ( 20 * log10( 4000 / 36.0 ) ) ).tointeger();
                            local channel = RandomInt(40, 80)

                            EmitSoundEx
                            ({
                                sound_name = "misc/halloween/spell_overheal.wav",
                                origin = heal_target.GetOrigin(),
                                volume = 1,
                                sound_level = sound_range,
                                channel = channel,
                                entity = heal_target,
                                filter_type = RECIPIENT_FILTER_SINGLE_PLAYER
                            });
                            EmitSoundEx
                            ({
                                sound_name = "misc/halloween/spell_overheal.wav",
                                origin = self.GetOrigin(),
                                volume = 1,
                                sound_level = sound_range,
                                channel = channel,
                                entity = self,
                                filter_type = RECIPIENT_FILTER_SINGLE_PLAYER
                            });

                            local uber_pause_time = 5.0

                            self.AddCustomAttribute("ubercharge rate penalty", 0.01, uber_pause_time )

                            local overlay = "effects/stealth_overlay"
                            PrecacheModel(overlay)
                            self.SetScriptOverlayMaterial(overlay)
                            EntFireByHandle(self, "runscriptcode", "self.SetScriptOverlayMaterial(null)", uber_pause_time, null, null)

                            flMedicPulseCooldown = cur_time
                        }
                    }
                    else 
                    {
                        // sound
                    }
                }
                

                buttons_last = buttons
            }
            // Force partial clip reload
            "is giger counter" : function()
            {
                local wep = self.GetActiveWeapon()
                local attribute_special = wep.GetAttribute("is giger counter", 0)

                if ( wep.IsMeleeWeapon() || attribute_special == 0 )
                    return 
                
                local clip = wep.Clip1()
                local max_clip = wep.GetMaxClip1()
                local clip_allow_fire = max_clip * 0.25
                local min_clip = SalvageGamemode.Round( clip_allow_fire, 0 ) 
                local cur_time = Time()
                local inspect_stage = GetPropInt(wep, "m_nInspectStage")
                local string = "\n-Must partially reload before firing"
                local id = 1

                local reload_penalty = 3

                if ( inspect_stage != -1 )
                {
                    printl("added inspect to table")
                    stringTable.string_2 <- string
                }

                if ( clip < min_clip )
                {
                    self.AddCustomAttribute( "reload time increased hidden", reload_penalty, 0.25 )
                }
                else if ( clip < max_clip )
                {
                    self.AddCustomAttribute("reload time increased hidden", 0.33334, 0.25 )
                }
            }

            //SetPropInt(custom_wep, "m_iPrimaryAmmoType", 1);
        }

        foreach ( i, item in cur_loadout )
        {
            foreach (str, f in attributes_custom )
            {
                // printl("str is: "+str)
                // printl("f is: "+f)
                // printl("item: "+item)
                local attribute_special = item.GetAttribute(str, 0)
                local function_name = UniqueString("specialAttribute_")
                // printl("attrib is: "+attribute_special)
                // printl("string is: "+function_name)

                if ( attribute_special != 0 )
                {
                    GetEntityThinkTable( player )[function_name] <- f
                }
            }
        }
    }

    function OverrideCapture( ent, tag )
    {
        printl("overriding capture....")
        local capper_entity = null
        local player_list = []

        // Collect list of all players
        for (local player; player = FindByClassname(player, "player");)
        {
            if ( player.GetTeam() == TEAM_SPECTATOR )
                continue
            // printl("found player: "+player)

            player_list.append(player)
        }
        
        // remove anything that isnt capturing
        player_list = SalvageGamemode.IterateArray( player_list, function(slot, entry)
        {
            // printl("ent is: "+ent)
            if ( SalvageGamemode.CheckEntityIntersection(ent, entry) == false )
            {
                // printl("not keeping: "+entry)
                return REMOVE_ENTRY
            }
            else
            {
                // printl("keeping: "+entry)
                return KEEP_ENTRY
            }
        })

        // check what team all the cappers are on
        local force_stop_cap = false 
        foreach ( i, player in player_list )
        {
            if ( player.GetTeam() == TEAM_RED )
            {
                force_stop_cap = true
            }

            if ( force_stop_cap == true )
            {
                break
            }
        }

        // if we find a red player, look for the bot with the given tag
        // then disable his capture rate
        for (local player; player = FindByClassname(player, "player");)
        {
            if ( player.GetTeam() == TEAM_SPECTATOR )
                continue
            
            if ( force_stop_cap == true && player.IsBotOfType( TF_BOT_TYPE) && player.HasBotTag(tag) )
            {
                // printl("red needs to cap, stop")
                player.AddCustomAttribute("increase player capture value", -1, -1)
            }
            else if ( force_stop_cap == false && player.IsBotOfType( TF_BOT_TYPE) && player.HasBotTag(tag) )
            {
                // printl("no cappers, reset")
                player.RemoveCustomAttribute("increase player capture value")
            }
        }
    }

    function SetControlPointCapRate( ent, time, team, zone )
    {
        SetPropFloat(ent, "m_flCapTime", time)
        ent.AcceptInput("SetControlPoint", zone, null, null)
    }

    // UTILITY FUNCTIONS

    function ScaleValue(min, max, given_scale, max_scale)
    {
        return min + ((max - min) * (given_scale.tofloat() / max_scale.tofloat()));
    }

    function PlayerBonemergeModel( player, model ) 
    {

        local scope = player.GetScriptScope()

        if ( "bonemerge_model" in scope && scope.bonemerge_model && scope.bonemerge_model.IsValid() )
            scope.bonemerge_model.Kill()

        local bonemerge_model = CreateByClassname( "tf_wearable" )
        SetPropString( bonemerge_model, STRING_NETPROP_NAME, "__popext_bonemerge_model" )
        SetPropInt( bonemerge_model, STRING_NETPROP_MODELINDEX, PrecacheModel( model ) )
        SetPropBool( bonemerge_model, STRING_NETPROP_ATTACH, true )
        SetPropEntity( bonemerge_model, "m_hOwner", player )
        bonemerge_model.SetTeam( player.GetTeam() )
        bonemerge_model.SetOwner( player )
        DispatchSpawn( bonemerge_model )
        SetPropBool( bonemerge_model, STRING_NETPROP_PURGESTRINGS, true )
        EntFireByHandle( bonemerge_model, "SetParent", "!activator", -1, player, player )
        SetPropInt( bonemerge_model, "m_fEffects", EF_BONEMERGE|EF_BONEMERGE_FASTCULL )
        scope.bonemerge_model <- bonemerge_model

        SetPropInt( player, "m_nRenderMode", kRenderTransColor )
        SetPropInt( player, "m_clrRender", 0 )

        function BonemergeModelThink() {

            if ( bonemerge_model.IsValid() && ( player.IsTaunting() || bonemerge_model.GetMoveParent() != player ) )
                bonemerge_model.AcceptInput( "SetParent", "!activator", player, player )
            return -1
        }
        GetEntityThinkTable( player ).BonemergeModelThink <- BonemergeModelThink
    }

    function PlaySoundToTeam(team, sound, delay, delay_stop, volume = 1)
    {
        PrecacheSound(sound)
        for (local enemy; enemy = FindByClassname(enemy, "player");)
        {
            if (enemy.GetTeam() == team && enemy.GetTeam() != TEAM_SPECTATOR)
            {
                local sound_range = (40 + (20 * log10(3000 / 36.0))).tointeger();
                if (delay != -1)
                {
                    local script_code = format("EmitSoundEx({sound_name = \"%s\", origin = self.GetOrigin(), volume = %f, entity = self, filter = RECIPIENT_FILTER_SINGLE_PLAYER})", sound, volume)
                    EntFireByHandle(enemy, "RunScriptCode", script_code, delay, null, null)
                    
                    // Add sound stop after delay_stop
                    if (delay_stop != -1)
                    {
                        local stop_script = format("EmitSoundEx({sound_name = \"%s\", flags = SND_STOP, entity = self, filter = RECIPIENT_FILTER_GLOBAL})", sound)
                        EntFireByHandle(enemy, "RunScriptCode", stop_script, delay + delay_stop, null, null)
                    }
                }
                else
                {
                    EmitSoundEx
                    ({
                        sound_name = sound,
                        origin = enemy.GetOrigin(),
                        volume = volume,
                        sound_level = sound_range,
                        entity = enemy,
                        filter = RECIPIENT_FILTER_SINGLE_PLAYER
                    });
                    
                    // Add immediate sound stop after delay_stop
                    if (delay_stop != -1)
                    {
                        local stop_script = format("EmitSoundEx({sound_name = \"%s\", flags = SND_STOP, entity = self, filter = RECIPIENT_FILTER_GLOBAL})", sound)
                        EntFireByHandle(enemy, "RunScriptCode", stop_script, delay_stop, null, null)
                    }
                }
            }
        }
    }

    function RunWithDelay(func, delay = 0.0)
    {
        local world_spawn_scope = WORLD_SPAWN.GetScriptScope()
        local func_name = UniqueString()
        world_spawn_scope[func_name] <- function[this]()
        {
            delete world_spawn_scope[func_name]
            func()
        }
        
        EntFireByHandle(WORLD_SPAWN, "CallScriptFunction", func_name, delay, null, null)
        return func_name
    }

    function GetAllRealPlayers()
    {
        local total_players = []

        for ( local player; player = FindByClassname( player, "player" ); )
        {
            if ( player.IsBotOfType(TF_BOT_TYPE) || player in total_players )
            {
                continue
            }

            total_players.append(player)
        }

        return total_players
    }

    // copied from seel_ins

    function FindIndexOfIcon( name, entity )
    {
        local i = 0 //Check: do I start at 0 or at 1?
        local two = ""
        local i2 = 0
        for( i; i < 24; i+=1 ) //Check: and thus, do I end at 24, or 23?
        {
            if ( i >= 12 ) two = "2.",i2 = 12;

            local curIconName = GetPropStringArray( entity, "m_iszMannVsMachineWaveClassNames"+two, i-i2 )
            if ( curIconName == name )
                return i;
        }
        // printf( "Icon name '%s' not found!\n",name )
        return null;
    }

    function ChangeIconByIndex( index, name, entity )
    {
        local two = ""
        local i2 = 0
        if ( index >= 12 ) two = "2.",i2 = 12; //Was this dot ever fixed? Getting fixed? Hopefully.
        SetPropStringArray( entity, "m_iszMannVsMachineWaveClassNames"+two, name, index-i2 )
    }

    function ChangeIconByName( oldName, newName, entity )
    {
        local iconIndex = FindIndexOfIcon( oldName, entity )
        if ( iconIndex == null ) return;

        // printf( "%s found at %d! Changing to %s...\n",oldName,iconIndex,newName )
        ChangeIconByIndex( iconIndex,newName, entity )
    }

    function GetIconFlags(iconName)
	{
		local iconIndex = FindIndexOfIcon(iconName, mvm_logic_entity)
		if (iconIndex == null) return null;

		local two = ""
		local i2 = 0
		if (iconIndex >= 12) two = "2.",i2 = 12; //Was this dot ever fixed? Getting fixed? Hopefully.
		local flags = GetPropIntArray(mvm_logic_entity, "m_nMannVsMachineWaveClassFlags"+two, iconIndex)
		return flags
	}


    function ChangeIconFlags(iconName,flags)
	{
		local iconIndex = FindIndexOfIcon(iconName, mvm_logic_entity)

		if (iconIndex == null) return;

		local oldFlags = GetIconFlags(iconName)
		printf("%s found at %d! Changing its flags from %d to %d...\n",iconName,iconIndex,oldFlags,flags)

		local two = ""
		local i2 = 0
		if (iconIndex >= 12) two = "2.",i2 = 12; //Was this dot ever fixed? Getting fixed? Hopefully.

		SetPropIntArray(mvm_logic_entity, "m_nMannVsMachineWaveClassFlags"+two, flags, iconIndex-i2)
	}

    function Ignite( hPlayer, duration = 10.0, damage = 1 ) 
    {
        local ignite_fix = SpawnEntityFromTable( "trigger_ignite", 
        {
            targetname = "__ignite_utilscript"
            burn_duration = duration
            damage = damage
            spawnflags = SF_TRIGGER_ALLOW_CLIENTS
        })
        EntFireByHandle( ignite_fix, "StartTouch", "", -1, hPlayer, hPlayer )
        EntFireByHandle( ignite_fix, "EndTouch", "", SINGLE_TICK, hPlayer, hPlayer )
        EntFireByHandle( ignite_fix, "Kill", "", SINGLE_TICK * 2, hPlayer, hPlayer )
    }

    function GetRandomEnemy( hPlayer )
    {
        local enemies = [];

        // // printl( "player: "+hPlayer )
        // // printl( "team: "+hPlayer.GetTeam() )

        for ( local player; player = FindByClassname( player, "player" ); )
        {
            if ( player.GetTeam() == hPlayer.GetTeam() || player.GetTeam() == TEAM_SPECTATOR )
                continue

            // // // printl( "Entity "+player+" belongs to team: "+player.GetTeam() )
            // // // printl( "caller "+hPlayer+" belongs to team: "+hPlayer.GetTeam() )

            enemies.append( player )
        }

        // // SalvageGamemode.PrintTable( enemies )

        if ( enemies.len() == 0 )
        {
            return null; 
        }

        return enemies[  RandomInt( 0, enemies.len() - 1 )  ]
    }

    function GiveCosmetic( hPlayer, iCosmeticID, stModel = null )
    {
        local dummy = CreateByClassname( "tf_weapon_parachute" )
        SetPropInt( dummy, STRING_NETPROP_ITEMDEF, WEAPON_BASE_JUMPER )
        SetPropBool( dummy, STRING_NETPROP_INIT, true )
        dummy.SetTeam( hPlayer.GetTeam() )
        DispatchSpawn( dummy )
        hPlayer.Weapon_Equip( dummy )

        local wearable = GetPropEntity( dummy, "m_hExtraWearable" )
	    dummy.Kill()

        SetPropInt( wearable, STRING_NETPROP_ITEMDEF, iCosmeticID )
        SetPropBool( wearable, STRING_NETPROP_INIT, true )
        SetPropBool( wearable, STRING_NETPROP_ATTACH, true )

        DispatchSpawn( wearable )

        if ( stModel ) 
        {
            wearable.SetModelSimple( stModel )
        }

        // avoid infinite loop
        hPlayer.AddEFlags( EFL_CUSTOM_WEARABLE )

        SendGlobalGameEvent( "post_inventory_application",  { userid = GetPlayerUserID( hPlayer ) } )

	    hPlayer.RemoveEFlags( EFL_CUSTOM_WEARABLE )

        PreservedCleanupArray.append( wearable )

        return wearable
    }   

    function DisplayHUD( hPlayer, strMessage, strMsgId, flX, flY, inPriority, bForceTxtChange = false)
    {
        local scope = hPlayer.GetScriptScope()
        // Initiate extra HUD elements

        if ( !( "player_text" in scope ) || !("aryAbilityCooldownTable" in scope) || !scope.player_text.IsValid() || scope.player_text == null )
        {
            printl("Initilaising extended HUD...")
            scope.player_text <- SpawnEntityFromTable( "game_text",
            {
                message = "",
                channel = 1,
                fadein = 0,
                fadeout = 0,
                holdtime = 2.0,
                spawnflags = 0,
                x = flX,
                y = flY,
                effect = 0,
                color = "255 255 255",
                targetname = "__gametext_hud_element"
            });
            scope.player_text.ValidateScriptScope()
            scope.player_text.GetScriptScope().owner <- hPlayer
            scope.aryAbilityCooldownTable <- []
            scope.inMessageIndex <- 0  // Add counter for ordering
        }

        // Check if strMessage already exists in the array
        local already_exists = false
        
        // Check if the first 8 characters are the same
        // No first 8 characters should be exactly the same
        // if that becomes the case, we can increase character count

        
        foreach (i, entry in scope.aryAbilityCooldownTable)
        {
            local string = entry.id

            if ( string == strMsgId )
            {
                already_exists = true
                break
            }
        }
        
        // Only add if it doesn't already exist
        if (!already_exists)
        {
            if ( !( "inMessageIndex" in scope ) )
            {
                scope.inMessageIndex <- 0
            }

            scope.aryAbilityCooldownTable.append
            ({
                msg = strMessage, 
                priority = inPriority,
                id = strMsgId,
                order = scope.inMessageIndex  // Add order index
            })
            scope.inMessageIndex++
        }
        else
        {
            // update text if first 8 characters are the same
            local msg_count = scope.aryAbilityCooldownTable.len()
            for (local i = 0; i < msg_count; i++ )
            {
                local txt = scope.aryAbilityCooldownTable[i]
                local string = txt.id

                // change the message here
                if ( string == strMsgId )
                {
                    scope.aryAbilityCooldownTable[i].msg = strMessage
                    scope.aryAbilityCooldownTable[i].priority = inPriority
                }
            }
        }

        scope.aryAbilityCooldownTable.sort(@(a, b) b.order <=> a.order)
                
        // Find the highest priority in the array
        local highest_priority = -999999
        foreach (entry in scope.aryAbilityCooldownTable)
        {
            if (entry.priority > highest_priority)
                highest_priority = entry.priority
        }
        
        // Remove all messages with lower priority than the highest
        scope.aryAbilityCooldownTable = SalvageGamemode.IterateArray(scope.aryAbilityCooldownTable, function(slot, entry)
        {
            if (entry.priority < highest_priority)
            {
                return REMOVE_ENTRY
            }
            else
            {
                return KEEP_ENTRY
            }
        })
        
        // Compile all strings into one message
        local final_msg = ""
        foreach (i, entry in scope.aryAbilityCooldownTable)
        {
            final_msg += entry.msg + "\n"
        }

        if (scope.player_text == null || !scope.player_text.IsValid())
        {
            printl("not valid? "+scope.player_text)
            return
        }

        scope.player_text.KeyValueFromString( "message", final_msg);
        scope.player_text.KeyValueFromString( "x", flX.tostring() );
        scope.player_text.KeyValueFromString( "y", flY.tostring() );
        scope.player_text.KeyValueFromString( "color", "18 255 21" );
        scope.player_text.AcceptInput( "Display", "", hPlayer, hPlayer );
        if ( bForceTxtChange == true )
        {
            scope.aryAbilityCooldownTable.clear()
        }
    }

    function IsMiniCritBoosted( hPlayer )
    {
        local minicrit_conds = 
        [ 
            TF_COND_OFFENSEBUFF
            TF_COND_ENERGY_BUFF
            TF_COND_NOHEALINGDAMAGEBUFF
            TF_COND_MINICRITBOOSTED_ON_KILL
         ]

        foreach ( cond in minicrit_conds )
        {
            if ( hPlayer.GetClassname() != "player" )
                continue
            
            if ( hPlayer.InCond( cond ) )
            {
                return true
            }
        }

        return false
    }



    function ExtendRespawnTime( victim, duration, force_kill )
    {
        local SINGLE_TICK = 0.015
        local victim_origin = victim.GetOrigin()

        local trigger = SpawnEntityFromTable( "trigger_player_respawn_override", 
        {
            origin = victim_origin,
            targetname = "__giant_respawn_trigger",
            spawnflags = 1, 
            RespawnTime = duration
        })

        local victim_class = GetPropInt( victim, "m_Shared.m_iDesiredPlayerClass" )

        EntFireByHandle( trigger, "StartTouch", "", SINGLE_TICK * 3, victim, victim )
        EntFireByHandle( trigger, "EndTouch", "", SINGLE_TICK * 7, victim, victim )
        EntFireByHandle( trigger, "Kill", "", SINGLE_TICK * 8, victim, victim )
        if ( force_kill == true )
        {
            EntFireByHandle( victim, "RunScriptCode", "self.ForceRegenerateAndRespawn()", SINGLE_TICK, null, null )
            EntFireByHandle( victim, "RunScriptCode", "self.SetAbsOrigin( activator.GetOrigin() )", SINGLE_TICK * 2, trigger, null )

            SetPropInt( victim, "m_Shared.m_iDesiredPlayerClass", victim_class );

            EntFireByHandle( victim, "RunScriptCode", "self.TakeDamage( 99999, DMG_DISSOLVE, null )", SINGLE_TICK * 6, null, null )
        }
    }

    function ShowModelToPlayer( player, model = [ "models/player/heavy.mdl", 0 ], pos = Vector(), ang = QAngle(), duration = INT_MAX ) 
    {
		PrecacheModel( model[ 0 ] )
		local proxy_entity = CreateByClassname( "obj_teleporter" ) // use obj_teleporter to set bodygroups.  not using SpawnEntityFromTable as that creates spawning noises
		proxy_entity.SetAbsOrigin( pos )
		proxy_entity.SetAbsAngles( ang )
		DispatchSpawn( proxy_entity )

		proxy_entity.SetModel( model[ 0 ] )
		proxy_entity.SetSkin( model[ 1 ] )
		proxy_entity.AddEFlags( EFL_NO_THINK_FUNCTION ) // EFL_NO_THINK_function prevents the entity from disappearing
		proxy_entity.SetSolid( SOLID_NONE )

		SetPropBool( proxy_entity, "m_bPlacing", true )
		SetPropInt( proxy_entity, "m_fObjectFlags", OF_MUST_BE_BUILT_ON_ATTACHMENT ) // sets "attachment" flag, prevents entity being snapped to player feet

		// m_hBuilder is the player who the entity will be networked to only
		SetPropEntity( proxy_entity, "m_hBuilder", player )
		EntFireByHandle( proxy_entity, "Kill", "", duration, player, player )
		return proxy_entity
	}

    function GetWeaponMaxAmmo( player, wep ) 
    {
		if ( wep == null ) return

		local slot = wep.GetSlot()
		local classname = wep.GetClassname()
		local itemid =  GetPropInt( wep, STRING_NETPROP_ITEMDEF )

		local table = SalvageGamemode.MaxAmmoTable[ player.GetPlayerClass() ]

		if ( !( itemid in table ) && !( classname in table ) )
			return -1

		local base_max = ( itemid in table ) ? table[ itemid ] : table[ classname ]

		return base_max
	}

    function CheckEntityIntersection( ent1, ent2 )
    {
        if ( !ent1.IsValid() || !ent2.IsValid() || ent1 == null || ent2 == null)
            return false;
        // Player bounds
        local p_mins = ent2.GetOrigin() + ent2.GetBoundingMins()
        local p_maxs = ent2.GetOrigin() + ent2.GetBoundingMaxs()

        // Prop bounds
        local prop_mins = ent1.GetOrigin() + ent1.GetBoundingMins()
        local prop_maxs = ent1.GetOrigin() + ent1.GetBoundingMaxs()

        // AABB intersection check
        if ( p_mins.x <= prop_maxs.x && p_maxs.x >= prop_mins.x &&
            p_mins.y <= prop_maxs.y && p_maxs.y >= prop_mins.y &&
            p_mins.z <= prop_maxs.z && p_maxs.z >= prop_mins.z )
        {
            return true;
        }

        return false;
    }

    function SendAnnotation( text, delay, lifetime, origin )
    {
        local annotate = format( "SendGlobalGameEvent( \"show_annotation\", {text = \"%s\", lifetime = %s, worldPosX = \"%s\", worldPosY = \"%s\", worldPosZ = \"%s\", id = 2, play_sound = \"\", show_distance = false, show_effect = false})", text.tostring(), lifetime.tostring(), origin.x.tostring(), origin.y.tostring(), origin.z.tostring() )
        EntFireByHandle( gamerules, "RunScriptCode", annotate, delay, null, null )
    }

    function VectorAngles( forward ) 
    {
		local yaw, pitch
		if ( forward.y == 0.0 && forward.x == 0.0 ) 
        {
			yaw = 0.0
			if ( forward.z > 0.0 )
				pitch = 270.0
			else
				pitch = 90.0
		}
		else 
        {
			yaw = ( atan2( forward.y, forward.x ) * 180.0 / Pi )
			if ( yaw < 0.0 )
				yaw += 360.0
			pitch = ( atan2( -forward.z, forward.Length2D() ) * 180.0 / Pi )
			if ( pitch < 0.0 )
				pitch += 360.0
		}

		return QAngle( pitch, yaw, 0.0 )
	}

    function NormalizeVector( vec )
    {
        // Calculate the magnitude ( length ) of the vector
        local magnitude = sqrt( vec.x * vec.x + vec.y * vec.y + vec.z * vec.z );
        
        // If the magnitude is greater than zero, normalize the vector
        if ( magnitude > 0 )
        {
            vec.x = vec.x / magnitude;
            vec.y = vec.y / magnitude;
            vec.z = vec.z / magnitude;
        }
        
        return vec;
    }

    /* 
        Iterates over array. Within array, return REMOVE_ENTRY to remove array
        aryPlayersCursed = SalvageGamemode.IterateArray( aryPlayersCursed, function( entry )
        {
            //// // printl( entry )
            local player = entry.cursed
            local duration = entry.duration

            if ( cur_time >= duration )
            {
                //// // printl( "removing entry" )
                return REMOVE_ENTRY
            }
            else 
            {
                return KEEP_ENTRY
            }
        })
    */


    function IterateArray( array, callback )
    {
        array = array.filter( @( i, v ) callback( i, v ) == KEEP_ENTRY )
        
        return array
    }

    function GetEnemiesWithinArea( hPlayer, radius, origin )
    {
        local enemies = []

        local classnames = 
        {
            "player" : 1
            "obj_teleporter" : 1
            "obj_sentrygun" : 1
            "obj_dispenser" : 1
            "tank_boss" : 1
        }

        foreach ( string, value in classnames )
        {
            for ( local entity; entity = FindByClassnameWithin( entity, string, origin, radius ); )
            {
                // // // printl( "entity team: within area "+entity.GetTeam() )
                // // // printl( "player team: within area "+hPlayer.GetTeam() )
                if ( entity.GetTeam() != hPlayer.GetTeam() && entity.GetTeam() != TEAM_SPECTATOR )
                {
                    enemies.append( entity )
                }
            }
        }

        return enemies
    }

    // Fire a projectile 
    function FireProjectile( attacker, classname, item_id, attribute_table, bKillItem = true )
    {
        local SpecialWeaponAccommodations =
        {
            "tf_weapon_bat_wood" : "secondary",
            "tf_weapon_bat_giftwrap" : "secondary"
            "tf_weapon_pipebomblauncher" : "sticky"
        }
        
        // Handle Huntsman special case - replace with Crusader's Crossbow
        local actual_classname = classname
        local actual_item_id = item_id
        if ( classname == "tf_weapon_compound_bow" )
        {
            actual_classname = "tf_weapon_crossbow"
            actual_item_id = 305 // Crusader's Crossbow item ID
            if ( attribute_table != null )
            {
                attribute_table[ "override projectile type" ] <- 8
                // crossbow at base does 60% less damage then huntsman
                attribute_table[ "damage bonus HIDDEN" ] <- 1.6
            }
            else 
            {
                attribute_table = 
                {
                    "override projectile type" : 8
                    "damage bonus HIDDEN" : 1.6
                }
            }
        }
        if ( classname == "tf_weapon_cannon" )
        {
            if ( attribute_table != null )
            {
                attribute_table[ "grenade launcher mortar mode" ] <- 0
            }
            else 
            {
                attribute_table = 
                {
                    "grenade launcher mortar mode" : 0
                }
            }
        }
        
        // Store original values to restore later
        local original_ammo = GetPropIntArray( attacker, "m_iAmmo", 1 )
        local original_charge = GetPropFloat( attacker, "m_Shared.m_flItemChargeMeter" )
        local original_lag_comp = GetPropBool( attacker, "m_bLagCompensation" )
        
        // Create fake weapon
        local hFakeWeapon = CreateByClassname( actual_classname )
        if ( !hFakeWeapon || !hFakeWeapon.IsValid() )
        {
            //// // printl( "Failed to create weapon: " + actual_classname )
            return null
        }
        
        // Setup weapon properties
        SetPropBool( hFakeWeapon, "m_AttributeManager.m_Item.m_bInitialized", true )
        SetPropInt( hFakeWeapon, STRING_NETPROP_ITEMDEF, actual_item_id )
        SetPropEntity( hFakeWeapon, "m_hOwner", attacker )
        hFakeWeapon.SetOwner( attacker )
        hFakeWeapon.DispatchSpawn()
        
        // Apply attributes to the weapon
        foreach ( attribute, value in attribute_table )
        {
            hFakeWeapon.AddAttribute( attribute, value, -1 )
        }
        
        // Setup attacker for firing
        SetPropIntArray( attacker, "m_iAmmo", 99, 1 )
        SetPropFloat( attacker, "m_Shared.m_flItemChargeMeter", 100.0 )
        SetPropBool( attacker, "m_bLagCompensation", false )
        SetPropFloat( hFakeWeapon, "m_flNextPrimaryAttack", 0 )
        SetPropFloat( hFakeWeapon, "m_flNextSecondaryAttack", 0 )
        
        // Determine which attack to use
        local attack_type = "primary"
        if ( classname in SpecialWeaponAccommodations )
        {
            attack_type = SpecialWeaponAccommodations[ classname ]
        }
        
        // Fire the weapon
        if ( attack_type == "secondary" )
        {
            hFakeWeapon.SecondaryAttack()
        }
        else if ( attack_type == "sticky" )
        {
            hFakeWeapon.AddAttribute("stickybomb charge rate", -1, -1)
            hFakeWeapon.AddAttribute("projectile range increased", 1.3, -1)

            hFakeWeapon.PrimaryAttack()
            EntFireByHandle(hFakeWeapon, "runscriptcode", "self.PrimaryAttack()", 0.1, null, null)
            EntFireByHandle(hFakeWeapon, "runscriptcode", "self.SecondaryAttack()", 1, null, null)
        }
        else 
        {
            hFakeWeapon.PrimaryAttack()
        }
        
        // Clean up fake weapon

        if ( bKillItem == false )
        {
            hFakeWeapon.Kill()
        }
        
        // Revert attacker changes
        SetPropBool( attacker, "m_bLagCompensation", original_lag_comp )
        SetPropIntArray( attacker, "m_iAmmo", original_ammo, 1 )
        SetPropFloat( attacker, "m_Shared.m_flItemChargeMeter", original_charge )
        
        return hFakeWeapon
    }

    function GiveWeapon( player, className, itemID )
	{
		if ( typeof itemID == "string" && className == "tf_wearable" )
		{
			CTFBot.GenerateAndWearItem.call( player, itemID )
			return
		}
		local weapon = CreateByClassname( className )
		SetPropInt( weapon, STRING_NETPROP_ITEMDEF, itemID )
		SetPropBool( weapon, "m_AttributeManager.m_Item.m_bInitialized", true )
		SetPropBool( weapon, "m_bValidatedAttachedEntity", true )
		weapon.SetTeam( player.GetTeam() )
		DispatchSpawn( weapon )

		// remove existing weapon in same slot
		for ( local i = 0; i < SLOT_COUNT; i++ )
		{
			local heldWeapon = GetPropEntityArray( player, "m_hMyWeapons", i )
			if ( heldWeapon == null || heldWeapon.GetSlot() != weapon.GetSlot() )
				continue
			heldWeapon.Destroy()
			SetPropEntityArray( player, "m_hMyWeapons", null, i )
			break
		}

		player.Weapon_Equip( weapon )
		player.Weapon_Switch( weapon )

		return weapon
	}

    function SetEntityColor( entity, r, g, b, a )
    {
        local color = r | ( g << 8 ) | ( b << 16 ) | ( a << 24 )
        SetPropInt( entity, "m_clrRender", color )
    }

    function NormalizeAngle( target ) 
    {
		target %= 360.0
		if ( target > 180.0 )
			target -= 360.0
		else if ( target < -180.0 )
			target += 360.0
		return target
	}

    function VectorAngles( forward )
    {
		local yaw, pitch
		if ( forward.y == 0.0 && forward.x == 0.0 ) 
        {
			yaw = 0.0
			if ( forward.z > 0.0 )
				pitch = 270.0
			else
				pitch = 90.0
		}
		else {
			yaw = ( atan2( forward.y, forward.x ) * 180.0 / Pi )
			if ( yaw < 0.0 )
				yaw += 360.0
			pitch = ( atan2( -forward.z, forward.Length2D() ) * 180.0 / Pi )
			if ( pitch < 0.0 )
				pitch += 360.0
		}

		return QAngle( pitch, yaw, 0.0 )
	}


	function ApproachAngle( target, value, speed ) 
    {
		target = NormalizeAngle( target )
		value = NormalizeAngle( value )
		local delta = NormalizeAngle( target - value )
		if ( delta > speed )
			return value + speed
		else if ( delta < -speed )
			return value - speed
		return target
	}

    function RemapValClamped( v, A, B, C, D ) 
    {
		if ( A == B ) {
			if ( v >= B )
				return D
			return C
		}
		local cv = ( v - A ) / ( B - A )
		if ( cv <= 0.0 )
			return C
		if ( cv >= 1.0 )
			return D
		return C + ( D - C ) * cv
	}

    function LookAt( player, target_pos, min_rate, max_rate ) 
    {
        local cur_eye_ang = player.EyeAngles()
        local cur_eye_pos = player.EyePosition()
		local cur_eye_fwd = player.EyeAngles().Forward()
		local dt  = FrameTime()
		local dir = target_pos - cur_eye_pos
		dir.Norm()
		local dot = cur_eye_fwd.Dot( dir )

		local desired_angles = SalvageGamemode.VectorAngles( dir )

		local rate_x = SalvageGamemode.RemapValClamped( fabs( SalvageGamemode.NormalizeAngle( cur_eye_ang.x ) - SalvageGamemode.NormalizeAngle( desired_angles.x ) ), 0.0, 180.0, min_rate, max_rate )
		local rate_y = SalvageGamemode.RemapValClamped( fabs( SalvageGamemode.NormalizeAngle( cur_eye_ang.y ) - SalvageGamemode.NormalizeAngle( desired_angles.y ) ), 0.0, 180.0, min_rate, max_rate )

		if ( dot > 0.7 ) {
			local t = SalvageGamemode.RemapValClamped( dot, 0.7, 1.0, 1.0, 0.05 )
			local d = sin( 1.57 * t ) // pi/2
			rate_x *= d
			rate_y *= d
		}

		cur_eye_ang.x = SalvageGamemode.NormalizeAngle( SalvageGamemode.ApproachAngle( desired_angles.x, cur_eye_ang.x, rate_x * dt ) )
		cur_eye_ang.y = SalvageGamemode.NormalizeAngle( SalvageGamemode.ApproachAngle( desired_angles.y, cur_eye_ang.y, rate_y * dt ) )

        if (player.GetClassname() == "player")
        {
            player.SnapEyeAngles( cur_eye_ang )
        }
        else 
        {
            player.SetAbsAngles( cur_eye_ang )
        }
	}

    function GetPlayerUserID( player )
    {
        return GetPropIntArray( player_manager, "m_iUserID", player.entindex() )
    }

    // todo: check romevision
    function PlayAnimationOnPlayer( player, sequence_name, duration, flAnimSpeed, frame_start = 0 )
    {
        local cur_model = player.GetModelName()
        local cur_loadout = SalvageGamemode.GetLoadout( player )
        local player_scale = player.GetModelScale()
        local player_skin = player.GetSkin()
        local fake_items = []

        local cur_origin = player.GetOrigin()
        local cur_angles = player.GetAbsAngles() // use absangles since eyeposition can cause anim to turn up or down weirdly

        player.SetCustomModelWithClassAnimations( "models/empty.mdl" )

        // prevent from doing anything such as turning
        player.SetMoveType( MOVETYPE_NOCLIP + MOVETYPE_FLY, MOVECOLLIDE_DEFAULT ) // completely stops a player in place
        player.AddCondEx( TF_COND_FREEZE_INPUT, duration, null ) // prevent from doing anything such as turning
        player.AddCondEx( TF_COND_STEALTHED_USER_BUFF, duration, null )

        local anim = SpawnEntityFromTable( "prop_dynamic",
        {
            model = cur_model
            origin = cur_origin
            angles = cur_angles
            targetname = "fake_anim"
            DefaultAnim = sequence_name
            modelscale = player_scale
            skin = player_skin + 3
        })
        anim.SetPlaybackRate( flAnimSpeed )
        SetPropBool( anim, "m_bClientSideAnimation", false )
        SetPropFloat( anim, "m_flCycle", frame_start ) 

        if ( GetPropBool( player, "m_bGlowEnabled" ) == true )
        {
            SetPropBool( player, "m_bGlowEnabled", false )
            SetPropBool( anim, "m_bGlowEnabled", true )
            EntFireByHandle( player, "RunScriptCode", "SetPropBool( self, `m_bGlowEnabled`, true )", duration, null, null )
        }

        for ( local item; item = FindByClassname( item, "tf_wearable" ); )
        {
            if ( item.GetOwner() == player )
            {
                if ( item.GetModelName() == "" )
                {
                    // //// // printl( "null item" )
                    continue
                }

                PrecacheModel( item.GetModelName() )

                if ( startswith( item.GetModelName(), "models/workshop/player/items/pyro/tw_" ) )
                {
                    continue
                }

                // //// // printl( "Using: "+item.GetModelName() )

                local fake_cosmetic = SpawnEntityFromTable( "prop_dynamic_ornament",
                {
                    model = item.GetModelName()
                    origin = cur_origin
                    angles = cur_angles
                    targetname = "fake_hat"
                    // modelscale = player_scale
                    skin = player_skin
                })

                fake_cosmetic.AcceptInput( "SetAttached", "!activator", anim, anim )

                PreservedCleanupArray.append( fake_cosmetic )
            }
        }

        EntFireByHandle( anim, "Kill", "", duration, null, null )
        EntFireByHandle( player, "RunScriptCode", "self.SetMoveType( 2, MOVECOLLIDE_DEFAULT )", duration, null, null )

        local model_revert = format( "self.SetCustomModelWithClassAnimations( \"%s\" )", cur_model )
        EntFireByHandle( player, "RunScriptCode", model_revert, duration, null, null )
        EntFireByHandle( player, "RunScriptCode", "SalvageGamemode.UnstuckEntity( self )", duration, null, null )
    }

    function DispatchParticleEffectEx( name, origin, angles, start_end, entity, attachment )
    {
        if ( !( "PrecacheParticleTable" in SalvageGamemode ) )
        {
            SalvageGamemode.PrecacheParticleTable <- {}
            PrecacheEntityFromTable({ classname = "info_particle_system", effect_name = name })
            SalvageGamemode.PrecacheParticleTable.name <- name
        }

        foreach ( key, string in SalvageGamemode.PrecacheParticleTable )
        {
            if ( string == name )
            {
                //// // printl( "already precached" )
                break
            }

            PrecacheEntityFromTable({ classname = "info_particle_system", effect_name = name })
            SalvageGamemode.PrecacheParticleTable.name <- string
        }

        local start = start_end[ 0 ]
        local end = start_end[ 1 ]

        local particle = SpawnEntityFromTable( "info_particle_system", 
        {
            effect_name = name, 
            targetname = "_efx_temporary_"+RandomFloat( -9999, 9999 )
            origin = origin
            angles = angles
        }) 
        particle.ValidateScriptScope()
        local scope = particle.GetScriptScope()
        scope.stCurParticle <- name

        function CleanUpOnDeath()
        {
            // local parent = self.GetRootMoveParent()

            if ( !parent.IsAlive() )
            {
                SetPropString( self, "m_iszScriptThinkFunction", "" )
                AddThinkToEnt( self, null )

                EntFireByHandle( self, "Kill", "", -1, null, null )
            }

            return -1
        }

        particle.ValidateScriptScope()

        EntFireByHandle( particle, "Start", "", start, null, null );

        if ( end != -1 )
        {
            EntFireByHandle( particle, "Stop", "", end, null, null );
            EntFireByHandle( particle, "Kill", "", end + SINGLE_TICK, null, null );
        }

        if ( entity != null )
        {   
            particle.AcceptInput( "SetParent", "!activator", entity, entity )
            particle.GetScriptScope().parent <- entity

            /* 
                If we try and kill our particles in a think, we complain about null instance
                Despite my best efforts to check for null instance, it will complain anyway
                I did find a solution through using EntFireByHandle, but i for some reason
                changed the code, and attempts to restore it just caused the same issue.
                So i lost the solution...
            */

            if ( !entity.IsPlayer() )
            {
                SalvageGamemode.SetDestroyCallback( particle, function()
                {
                    for ( local child = self.FirstMoveChild(); child != null; child = child.NextMovePeer() )
                    {
                        //// // printl( "killing: "+child )
                        child.Kill()
                    }
                })
            }
            else 
            {
                // needs to be added in scope to work
                particle.GetScriptScope().CleanUpOnDeath <- CleanUpOnDeath
                AddThinkToEnt( particle, "CleanUpOnDeath" )
            }
        } 

        if ( attachment != null )
        {   
            SetPropEntityArray( particle, "m_hControlPointEnts", attachment, 0 );
        } 

        return particle
    }

    function SetDestroyCallback( entity, callback )
    {
        entity.ValidateScriptScope()
        local scope = entity.GetScriptScope()
        scope.setdelegate({}.setdelegate({
                parent   = scope.getdelegate()
                id       = entity.GetScriptId()
                index    = entity.entindex()
                callback = callback
                _get = function( k )
                {
                    return parent[ k ]
                }
                _delslot = function( k )
                {
                    if ( k == id )
                    {
                        entity = EntIndexToHScript( index )
                        local scope = entity.GetScriptScope()
                        scope.self <- entity
                        callback.pcall( scope )
                    }
                    delete parent[ k ]
                }
            })
        )
    }

    function UnstuckEntity( entity )
    {
        ::MASK_PLAYERSOLID <- 33636363
        local origin = entity.GetOrigin();
        local trace = {
            start = origin,
            end = origin,
            hullmin = entity.GetBoundingMins(),
            hullmax = entity.GetBoundingMaxs(),
            mask = MASK_PLAYERSOLID,
            ignore = entity
        }
        TraceHull( trace );
        if ( "startsolid" in trace )
        {
            local dirs = [ Vector( 1, 0, 0 ), Vector( -1, 0, 0 ), Vector( 0, 1, 0 ), Vector( 0, -1, 0 ), Vector( 0, 0, 1 ), Vector( 0, 0, -1 ) ];
            for ( local i = 16; i <= 96; i += 16 )
            {
                foreach ( dir in dirs )
                {
                    trace.start = origin + dir * i;
                    trace.end = trace.start;
                    delete trace.startsolid;
                    TraceHull( trace );
                    if ( !( "startsolid" in trace ) )
                    {
                        entity.SetAbsOrigin( trace.end );
                        return true;
                    }
                }
            }
            return false;
        }
        return true;
    }

    function AddFunctionListToTable( table, reference )
    {
        if ( reference == null )
        {
            return
        }
        foreach ( name, func in table )
        {
            printl("adding: "+name)
            if ( !( name in reference ) )  // check by KEY not value
            {
                reference[ name ] <- func
            }
        }
    }

    function Round( num, decimals ) 
    {
        if ( decimals <= 0 )
            return floor( num + 0.5 )

        local mod = pow( 10, decimals )
        return floor( ( num * mod ) + 0.5 ) / mod
    }

    // Return true if the dist between the 2 vecs is smaller then the given dist
    function IsWithinRange( vec1, vec2, dist )
    {
        if ( ( ( vec1 - vec2 ).Length() ) < dist )
        {
            return true
        }

        return false
    }

    function GetDist( vec1, vec2 )
    {
        return ( ( vec1 - vec2 ).Length() )
    }

    function ShowHudHint ( text = "This is a hud hint", player = null, duration = 5.0 ) 
    {
		local hudhint = FindByName( null, "__utilhudhint" )

		local flags = player == null ? 1 : 0

		if ( !hudhint ) hudhint = SpawnEntityFromTable( "env_hudhint", { targetname = "__utilhudhint", spawnflags = flags, message = text })

		hudhint.KeyValueFromString( "message", text )

		EntFireByHandle( hudhint, "ShowHudHint", "", -1, player, player )
		EntFireByHandle( hudhint, "HideHudHint", "", duration, player, player )
	}

    function MiscPrintFunc( player, color, msg ) 
    {
        ClientPrint( player,3, format( "\x07%s%s", color, msg ) )
    }

    function RunMessageBroadcast( WaveNum, player )
    {
        local scope = player.GetScriptScope()
        local bool = GetPropInt( mvm_logic_entity, "m_bMannVsMachineBetweenWaves" )

        if ( player.IsBotOfType( TF_BOT_TYPE ) )
            return 

        // //// // printl( "bool is: "+bool )

        // Check if wave is progressing
        // Check if player is not in aryNoGiveExplanation
        // If either are true, dont print
        // 0 = wave hasnt started
        if ( aryNoGiveExplanation.find( player ) != null || bool == 0 )
        {
            //// // printl( "Player has seen broadcast" )
            return
        }

        SalvageGamemode.aryNoGiveExplanation.append( player )
        // // SalvageGamemode.PrintTable( scope )
        
        switch( WaveNum )
        {
            case 1:

            break;

            case 2:

            break;

            case 3:

            break;

            case 4:

            break;

            case 5:

            break;

            case 6:

            break;

            case 7:

            break;

            case 8:

            break;

            default:
                // SalvageGamemode.MiscPrintFunc( player, "ff008b", "BROADCASTING ERROR" )
            break;
        }
    }

    function MissionSetup()
    {
        printl("setting up mission")
        for (local player; player = FindByClassname(player, "player");)
        {
            if ( !player in SalvageGamemode.aryNoGiveExplanation )
            {   
                SalvageGamemode.RunMessageBroadcast( cur_wave, player )
            }
        }
        
        // Clear aryNoGiveExplanation so that players dont see explanation text again
        SalvageGamemode.aryNoGiveExplanation.clear()

        local FuncList_OnTakeDamage = 
        {
            "CauseHeadshot" : CauseHeadshot
            "ChargeRailgun" : ChargeRailgun
        }

        AddFunctionListToTable( FuncList_OnTakeDamage, SalvageGamemode.TakeDamageFuncTable )
    }

    function MULT_DAMAGE( player, weapon )
    {
        //////// // printl( "Player: "+player )
        //////// // printl( "Weapon: "+weapon )
        local d_bonus = player.GetCustomAttribute( "damage bonus", 1 )
        local d_bonus_wep = weapon.GetAttribute( "damage bonus", 1 )
        local d_penalty = player.GetCustomAttribute( "damage penalty", 1 )
        local d_penalty_wep = weapon.GetAttribute( "damage penalty", 1 )
        local d_hidden = player.GetCustomAttribute( "damage bonus HIDDEN", 1 )
        local d_hidden_wep = weapon.GetAttribute( "damage bonus HIDDEN", 1 )

        local d_card_wep = weapon.GetAttribute( "CARD: damage bonus", 1 )
        local d_card = player.GetCustomAttribute( "CARD: damage bonus", 1 )

        local d_special_wep = weapon.GetAttribute( "dmg penalty vs players", 1 )
        local d_special = player.GetCustomAttribute( "dmg penalty vs players", 1 )

        local total = 
        ( 
            d_bonus 
            * d_bonus_wep 
            * d_penalty 
            * d_penalty_wep 
            * d_hidden 
            * d_hidden_wep 
            * d_card_wep 
            * d_card
            * d_special_wep
            * d_special
        )
        
        return total
        
    }

    // Get an entity's think table
    function GetEntityThinkTable( entity )
    {
        entity.ValidateScriptScope();
        local scope = entity.GetScriptScope();
        local key = null

        foreach ( k, v in scope )
        {
            if ( startswith( k, "ThinkTable_" ) )
            {
                key = k
                break
            }
        }

        if ( key == null )
        {
            //// // printl( "WARNING: NO THINK TABLE: "+entity )
            return
        }

        if ( !( key in scope ) )
        {
            // If the table doesn't exist yet, create it
            scope[ key ] <- {};
        }

        return scope[ key ];
    }

    // Get a players loadout, and return it as an array
    function GetLoadout( player )
    {
        local loadout = []

        for ( local i = 0; i < SLOT_COUNT; i++ ) 
        {
            local wep = GetPropEntityArray( player, "m_hMyWeapons", i )
            if ( wep == null ) continue

            loadout.append( wep )
            
        }

        local itemdef_getslot = 
        [ 
            {wep = WEAPON_MANTREADS, slot = 1},
            {wep = WEAPON_GUNBOATS, slot = 1},

            {wep = WEAPON_BOOTLEGGER, slot = 0},
            {wep = WEAPON_ALI_BABAS_WEE_BOOTIES, slot = 0},

            {wep = WEAPON_CHARGIN_TARGE, slot = 1},
            {wep = WEAPON_SPLENDID_SCREEN, slot = 1},
            {wep = WEAPON_TIDE_TURNER, slot = 1},
            {wep = WEAPON_CHARGIN_TARGE_FESTIVE, slot = 1},

            {wep = WEAPON_DARWIN_DANGER_SHIELD, slot = 1},
            {wep = WEAPON_COZY_CAMPER, slot = 1},
            {wep = WEAPON_RAZORBACK, slot = 1},

            //{wep = WEAPON_BASE_JUMPER, slot = ( player.GetPlayerClass() == Demoman ? 0 : 1 )}
        ];


        local children_table = []
        
        for ( local child = player.FirstMoveChild(); child != null; child = child.NextMovePeer() )
        {
            children_table.append( child )
        }

        foreach ( child in children_table ) 
        {
            ////////////// // printl( "child is: "+child )

            foreach ( entry in itemdef_getslot )
            {
                local id = entry.wep 

                if ( GetPropInt( child, STRING_NETPROP_ITEMDEF ) == id && loadout.find( child ) == null )
                {
                    loadout.append( child )
                }
                else 
                {
                    continue
                }
            }
        }
    
        return loadout
    }

    // Get an item in a players slot
    function GetItemInSlot( player, slot ) 
    {
        local item
        for ( local i = 0; i < SLOT_COUNT; i++ ) 
        {
            local wep = GetPropEntityArray( player, "m_hMyWeapons", i )
            if ( wep == null || wep.GetSlot() != slot ) continue

            item = wep
            break
        }

        local itemdef_getslot = 
        [ 
            {wep = WEAPON_MANTREADS, slot = 1},
            {wep = WEAPON_GUNBOATS, slot = 1},

            {wep = WEAPON_BOOTLEGGER, slot = 0},
            {wep = WEAPON_ALI_BABAS_WEE_BOOTIES, slot = 0},

            {wep = WEAPON_CHARGIN_TARGE, slot = 1},
            {wep = WEAPON_SPLENDID_SCREEN, slot = 1},
            {wep = WEAPON_TIDE_TURNER, slot = 1},
            {wep = WEAPON_CHARGIN_TARGE_FESTIVE, slot = 1},

            {wep = WEAPON_DARWIN_DANGER_SHIELD, slot = 1},
            {wep = WEAPON_COZY_CAMPER, slot = 1},
            {wep = WEAPON_RAZORBACK, slot = 1},

            //{wep = WEAPON_BASE_JUMPER, slot = ( player.GetPlayerClass() == Demoman ? 0 : 1 )}
        ];

        // Didn't find anything in slot, check for passive items
        if ( item == null )
        {
            local children_table = []
            
            for ( local child = player.FirstMoveChild(); child != null; child = child.NextMovePeer() )
            {
                children_table.append( child )
            }

            foreach ( child in children_table ) 
            {
                ////////////// // printl( "child is: "+child )

                foreach ( entry in itemdef_getslot )
                {
                    local id = entry.wep 
                    local wep_slot = entry.slot

                    // //////////// // printl( "id is: "+id )
                    // //////////// // printl( "slot: "+slot )

                    if ( wep_slot != slot )
                    {
                        // //////////// // printl( "not slot" )
                        continue
                    }

                    if ( GetPropInt( child, STRING_NETPROP_ITEMDEF ) == id )
                    {
                        // //////////// // printl( "------------- Found a passive weapon: "+child )
                        item = child
                        return item // must return item here, break doesnt return correct item 
                    }
                }
            }
        }

        return item
    }

    // cleanup
    function Cleanup()
    {
        ////// // printl( "cleaning file..." )
       
        for ( local player; player = FindByClassname( player, "player" ); )
        {
            PlayerCleanupEx( player )
        }

        PreservedCleanupArray = IterateArray( PreservedCleanupArray, function( slot, entry )
        {
            local ent = entry

            if ( !ent.IsValid() || ent == null )
            {
                return REMOVE_ENTRY
            }

            ent.Kill()
            return REMOVE_ENTRY
        })

        // keep this at the end of this function
        delete ::SalvageGamemode
    }


    function PrintTable( table ) 
    {
        if ( table == null ) return;

        this.DoPrintTable( table, 0 )
    }

    function DoPrintTable( table, indent ) 
    {
        local line = ""
        for ( local i = 0; i < indent; i++ ) {
            line += " "
        }
        line += typeof table == "array" ? "[ " : "{";

        ClientPrint( null, 2, line )

        indent += 2
        foreach( k, v in table ) {
            line = ""
            for ( local i = 0; i < indent; i++ ) {
                line += " "
            }
            line += k.tostring()
            line += " = "

            if ( typeof v == "table" || typeof v == "array" ) {
                ClientPrint( null, 2, line )
                this.DoPrintTable( v, indent )
            }
            else {
                try {
                    line += v.tostring()
                }
                catch ( e ) {
                    line += typeof v
                }

                ClientPrint( null, 2, line )
            }
        }
        indent -= 2

        line = ""
        for ( local i = 0; i < indent; i++ ) {
            line += " "
        }
        line += typeof table == "array" ? " ]" : "}";

        ClientPrint( null, 2, line )
    }

    function PlayerCleanupEx( player )
    {
        // //////// // printl( "Use special cleanup" )

        player.ValidateScriptScope()
        local scope = player.GetScriptScope()


        // These are in the players scope already, we must ignore them
        local ignore_table = 
        {
            "self"      : null,
            "__vname"   : null,
            "__vrefs"   : null
        };

        // Anything we put in the players scope is cleaned here
        foreach ( think, v in scope )
        {
            if ( !( think in ignore_table ) )
            {
                // Delete anything found to be an entity
                if ( typeof( v ) == "instance" && v.IsValid() )
                {
                    v.Kill()
                }
                
                // Delete other key values
                delete scope[ think ]
            }
        }

        SetPropString( player, "m_iszScriptThinkFunction", "" )
        AddThinkToEnt( player, null )
    }
    
    function PlayerSpawnEx( player )
    {
        //////// // printl( "Called PlayerSpawnEx" )
        player.ValidateScriptScope()
        // Check if the player is a bot. This ignores Source TV spectators
        local bot_check = player.IsBotOfType( TF_BOT_TYPE ) ? true : false
        local scope = player.GetScriptScope() 
        local cur_time = Time()
        player.SetScriptOverlayMaterial(null)

        // //////// // printl( "Is Player a Bot? "+bot_check+" for: "+player )

        local key = bot_check ? 
        [  "RobotThink", "ThinkTable_Robots", "Robots_Cleanup_Table" ] : 
        [  "PlayerThink", "ThinkTable_Player", "Player_Cleanup_Table" ]

        // Directly reference our tables 
        local cleanup_map =
        {
            "Player_Cleanup_Table" : Player_Cleanup_Table,
            "Robots_Cleanup_Table" : Robots_Cleanup_Table,
        }


        // key 0 is first arg ( i.e. RobotThink )
        // key 1 is second arg ( i.e. ThinkTable_Robots )
        

        scope[ key[ 1 ] ] <- {}
        scope.PlayerCleanupEx <- SalvageGamemode.PlayerCleanupEx
        
        scope[ key[ 0 ] ] <- function() 
        { 
            foreach ( name, func in this[ key[ 1 ] ] ) 
            {
                if ( typeof func == "function" )
                {
                    // //////// // printl( "Running func: "+name )
                    func.call( scope );
                }
            }
            
            return -1 
        }

        // append our player into the correct cleanup table
        cleanup_map[ key[ 2 ] ].append( player )

        AddThinkToEnt( player, key[ 0 ] )

        GetEntityThinkTable( player ).ApplyProjectileThink <- SalvageGamemode.ApplyProjectileThink
        player.GetScriptScope().buttons_last <- 0
        CheckCustomAttributes( player )

        if ( bot_check == true )
        {
            local bot_tags = {}
            player.GetAllBotTags( bot_tags )

            foreach ( i, tag in bot_tags )
            {
                switch( tag )
                {
                    case "bot_zombie":

                        local plyclass = 
                        {
                            [ 1 ] = { [ "scout" ] = 5617 },
                            [ 2 ] = { [ "sniper" ] = 5625 },
                            [ 3 ] = { [ "soldier" ] = 5618 },
                            [ 4 ] = { [ "demo" ] = 5620 },
                            [ 5 ] = { [ "medic" ] = 5622 },
                            [ 6 ] = { [ "heavy" ] = 5619 },
                            [ 7 ] = { [ "pyro" ] = 5624 },
                            [ 8 ] = { [ "spy" ] = 5623 },
                            [ 9 ] = { [ "engineer" ] = 5621 }
                        }

                        local table = plyclass[ player.GetPlayerClass() ]
                        local zombie_class = table.keys()[ 0 ]
                        local zombie_id = table[ zombie_class ]
                        local zombie_model = format( "models/player/items/%s/%s_zombie.mdl", zombie_class, zombie_class )
                        PrecacheModel( zombie_model )

                        SalvageGamemode.GiveCosmetic( player, zombie_id, zombie_model )

                        SetPropBool( player, "m_bForcedSkin", true )
                        SetPropInt( player, "m_nForcedSkin", player.GetSkin() + 4 )
                        SetPropInt( player, "m_iPlayerSkinOverride", 1 )

                        EntFireByHandle( player, "SetCustomModelWithClassAnimations", format( "models/player/%s.mdl", zombie_class ), -1, null, null )

                    break;

                    case "tag_alwaysfirealt":

                        function AlwaysFireAlt()
                        {
                            //// // printl( "firing alt" )
                            self.PressAltFireButton( -1 )
                        }
                        GetEntityThinkTable( player ).AlwaysFireAlt <- AlwaysFireAlt
                        
                    break;

                    case "use_human_anims":

                        local plyclass = 
                        {
                            [ 1 ] = { [ "scout" ] = 5617 },
                            [ 2 ] = { [ "sniper" ] = 5625 },
                            [ 3 ] = { [ "soldier" ] = 5618 },
                            [ 4 ] = { [ "demo" ] = 5620 },
                            [ 5 ] = { [ "medic" ] = 5622 },
                            [ 6 ] = { [ "heavy" ] = 5619 },
                            [ 7 ] = { [ "pyro" ] = 5624 },
                            [ 8 ] = { [ "spy" ] = 5623 },
                            [ 9 ] = { [ "engineer" ] = 5621 }
                        }

                        local table = plyclass[ player.GetPlayerClass() ]
                        local zombie_class = table.keys()[ 0 ]
                        local zombie_id = table[ zombie_class ]
                        local model = format( "models/bots/%s/bot_%s.mdl", zombie_class, zombie_class )
                        PrecacheModel( model )
                        local get_class_string = [ "", "scout", "sniper", "soldier", "demo", "medic", "heavy", "pyro", "spy", "engineer", "" ]
                        local get_cur_class = get_class_string[ player.GetPlayerClass() ]

                        EntFireByHandle( player, "SetCustomModelWithClassAnimations", format( "models/player/%s.mdl", get_cur_class ), SINGLE_TICK, null, null )
                        EntFireByHandle( player, "runscriptcode", format( "SalvageGamemode.PlayerBonemergeModel( self, `%s` )", model ), SINGLE_TICK, null, null)

                    break

                    case "tag_medic_crossboost":
                        
                        function MedicCrossboost()
                        {
                            local primary = SalvageGamemode.GetItemInSlot(self, 0)
                            local cur_time = Time()
                            
                            // Clear target list when reloading (clip is full)
                            if ( (flClearTime + 2) <= cur_time && aryAlliesTargtted.len() > 0 )
                            {
                                flClearTime = cur_time
                                if ( primary.Clip1() == primary.GetMaxClip1() )
                                {
                                    aryAlliesTargtted.clear()
                                }
                            }
                            
                            // Find all our projectiles and assign targets
                            for ( local projectile; projectile = FindByClassname(projectile, "tf_projectile*"); ) 
                            {
                                local owner_launcher = GetPropEntity(projectile, "m_hLauncher")
                                
                                if ( owner_launcher == primary )
                                {
                                    projectile.ValidateScriptScope()
                                    local scope = projectile.GetScriptScope()
                                    
                                    // Only assign target once per projectile
                                    if ( !("hLockTarget" in scope) )
                                    {
                                        // Collect all valid teammates not already targeted
                                        local available_allies = []
                                        for ( local player; player = FindByClassname(player, "player"); )
                                        {
                                            if 
                                            ( 
                                                player.GetTeam() == self.GetTeam() 
                                                && player != self 
                                                && player.GetPlayerClass() != TF_CLASS_MEDIC 
                                                && player.IsAlive() 
                                            )
                                            {
                                                local already_targeted = false
                                                foreach ( targeted_player in aryAlliesTargtted )
                                                {
                                                    if ( targeted_player == player )
                                                    {
                                                        already_targeted = true
                                                        break
                                                    }
                                                }
                                                if ( !already_targeted )
                                                {
                                                    available_allies.append(player)
                                                }
                                            }
                                        }
                                        
                                        // Pick a random ally if any are available
                                        if ( available_allies.len() > 0 )
                                        {
                                            local random_index = RandomInt(0, available_allies.len() - 1)
                                            local chosen_ally = available_allies[random_index]
                                            
                                            // Set up projectile scope
                                            scope.hLockTarget <- chosen_ally
                                            scope.iHomingStrength <- 360
                                            scope.iHomingTurnSpeed <- 180
                                            scope.iMaxVelocity <- 870
                                            
                                            // Add to targeted list
                                            aryAlliesTargtted.append(chosen_ally)
                                            
                                            // Assign and start the homing think function
                                            SalvageGamemode.GetEntityThinkTable( projectile ).HomingProjectileAlly <- SalvageGamemode.HomingProjectileAlly
                                            
                                            // printl("Projectile locked onto ally: " + chosen_ally)
                                        }
                                        else
                                        {
                                            // printl("No available allies! All " + aryAlliesTargtted.len() + " are targeted")
                                            aryAlliesTargtted.clear()
                                        }
                                    }
                                }
                            }
                        }

                        player.GetScriptScope().aryAlliesTargtted <- []
                        player.GetScriptScope().flClearTime <- 0
                        GetEntityThinkTable( player ).MedicCrossboost <- MedicCrossboost
                    break;

                    case "tag_carry":

                        function CarryScrubs()
                        {
                            // printl( "carry scrub" )

                            local cur_time = Time()
                            local secondary = SalvageGamemode.GetItemInSlot(self, 1)
                            local wep = self.GetActiveWeapon()
                            local sticky_count = GetPropInt(secondary, "m_iPipebombCount")


                            if ( ( flSwitchToSpam + flSpamRandomizer ) <= cur_time )
                            {
                                printl("switch mode")
                                flSwitchToSpam = cur_time
                                flSpamRandomizer = RandomFloat(4, 9)

                                if ( bSetSpammer == true )
                                {
                                    bSetSpammer = false
                                }
                                else 
                                {
                                    bSetSpammer = true
                                }
                            }

                            if ( bSetSpammer == true )
                            {
                                self.PressAltFireButton(0.5)
                                flDetonateCountdown = cur_time
                                flRandomStickyDetonate = RandomFloat(5, 8)
                                self.RemoveBotAttribute( SUPPRESS_FIRE )
                            }
                            else if ( sticky_count == 8 )
                            {
                                self.AddBotAttribute( SUPPRESS_FIRE )
                                if ( (flDetonateCountdown + flRandomStickyDetonate) <= cur_time )
                                {
                                    self.PressAltFireButton(0.5)
                                    wep.SecondaryAttack()
                                    flDetonateCountdown = cur_time
                                    flRandomStickyDetonate = RandomFloat(5, 8)
                                    self.RemoveBotAttribute( SUPPRESS_FIRE )
                                }
                            }
                        }
                        
                        player.GetScriptScope().flDetonateCountdown <- 0
                        player.GetScriptScope().bSetSpammer <- false
                        player.GetScriptScope().flSwitchToSpam <- 0
                        player.GetScriptScope().flSpamRandomizer <- RandomFloat(4, 9)
                        player.GetScriptScope().flRandomStickyDetonate <- RandomFloat(5, 8)
                        GetEntityThinkTable( player ).CarryScrubs <- CarryScrubs
                        
                    break;

                    case "tag_medic":

    
                        function PocketMedic()
                        {
                            local cur_time = Time()
                            local secondary = SalvageGamemode.GetItemInSlot(self, 1)
                            local wep = self.GetActiveWeapon()

                            // if ( bFixMedigun == false )
                            // {
                            //     bFixMedigun = true 
                            //     // SalvageGamemode.PlayerBonemergeModel( secondary, "models/weapons/w_models/w_medigun_overhealer.mdl" )
                            //     local bonemerge_model = CreateByClassname( "tf_wearable" )
                            //     SetPropString( bonemerge_model, STRING_NETPROP_NAME, "_model_mergher" )
                            //     SetPropInt( bonemerge_model, STRING_NETPROP_MODELINDEX, PrecacheModel(  "models/weapons/w_models/w_medigun_overhealer.mdl" ) )
                            //     SetPropBool( bonemerge_model, STRING_NETPROP_ATTACH, true )
                            //     SetPropEntity( bonemerge_model, "m_hOwner", secondary )
                            //     bonemerge_model.SetTeam( secondary.GetTeam() )
                            //     bonemerge_model.SetOwner( secondary )
                            //     DispatchSpawn( bonemerge_model )
                            //     SetPropBool( bonemerge_model, STRING_NETPROP_PURGESTRINGS, true )
                            //     EntFireByHandle( bonemerge_model, "SetParent", "!activator", -1, secondary, secondary )
                            //     SetPropInt( bonemerge_model, "m_fEffects", EF_BONEMERGE|EF_BONEMERGE_FASTCULL )
                            //     scope.bonemerge_model <- bonemerge_model

                            //     SetPropInt( secondary, "m_nRenderMode", kRenderTransColor )
                            //     SetPropInt( secondary, "m_clrRender", 0 )

                            //     //"models/weapons/w_models/w_medigun_overhealer.mdl"
                            // }
                            
                            if ( !self.InCond( TF_COND_INVULNERABLE_USER_BUFF ))
                            {
                                if ( (flShieldCountdown + 5 ) <= cur_time )
                                {
                                    if ( secondary.GetAttribute("generate rage on heal", 0) == 0 )
                                    {
                                        secondary.AddAttribute("generate rage on heal", 2, -1)

                                        SetPropFloat(self, "m_Shared.m_flRageMeter", 100)
                                    }

                                    if ( (flShieldRecharge + 20 ) <= cur_time )
                                    {
                                        flShieldRecharge = cur_time
                                        SetPropFloat(self, "m_Shared.m_flRageMeter", 100)
                                    }
                                    self.PressSpecialFireButton(-1)
                                }
                            }
                            else 
                            {
                                flShieldCountdown = cur_time
                            }
                        }
                        
                        player.GetScriptScope().flShieldCountdown <- Time()
                        player.GetScriptScope().flShieldRecharge <- Time()
                        player.GetScriptScope().bFixMedigun <- false
                        GetEntityThinkTable( player ).PocketMedic <- PocketMedic
                        
                    break;

                    case "tag_full_head":

                        SetPropInt( player, "m_Shared.m_iDecapitations", 5 )
                        
                    break;

                    case "tag_disabledraw":

                        player.DisableDraw()
                        player.SetIsMiniBoss(true)
                        
                    break;

                    case "capture_b":

                        local entity = FindByName(null, "capture_b")
                        local ent_origin = entity.GetOrigin()
                        SalvageGamemode.EnableDummyCapture( entity "capture_b" )
                        
                        // for some reason, when wave starts, point wont cap unless 
                        // red touches, force here
                        player.AddCondEx(43, 0.1, null)
                    break;

                    case "capture_c":

                        local entity = FindByName(null, "capture_c")
                        local ent_origin = entity.GetOrigin()
                        SalvageGamemode.EnableDummyCapture( entity "capture_c" )

                        // for some reason, when wave starts, point wont cap unless 
                        // red touches, force here
                        player.AddCondEx(43, 0.1, null)
                    break;

                    case "capture_a":
                        printl("a cap entity spawned")
                        local entity = FindByName(null, "capture_a")
                        local ent_origin = entity.GetOrigin()
                        SalvageGamemode.EnableDummyCapture( entity "capture_a" )
                        
                        // for some reason, when wave starts, point wont cap unless 
                        // red touches, force here
                        player.AddCondEx(43, 0.1, null)
                    break;

                    case "bot_basher":

                        function ShieldBashDemo()
                        {
                            if ( self.InCond(TF_COND_SHIELD_CHARGE) )
                            {
                                self.AddCustomAttribute("move speed bonus", 1.2, -1)
                            }
                            else 
                            {
                                self.AddCustomAttribute("move speed bonus", 0.5, -1)
                            }
                        }
                        GetEntityThinkTable( player ).ShieldBashDemo <- ShieldBashDemo

                    break;

                    // case "bot_medshield":

                    //     function GenerateShield()
                    //     {
                    //         local cur_origin = self.GetOrigin()
                    //         local eye_angles = self.EyeAngles()
                    //         local eye_forward = self.EyePosition() + self.EyeAngles().Forward() * 32.0 + Vector(0, 0, -95)

                    //         SetPropEntity( hShield, "m_hOwner", self )
                    //         hShield.SetModelSimple("models/props_mvm/mvm_player_shield.mdl")
                    //         hShield.SetSkin(1)
                    //         hShield.SetTeam( self.GetTeam() )
                    //         hShield.SetOwner( self )
                    //         hShield.SetAbsOrigin(eye_forward)
                    //         hShield.SetAbsAngles(eye_angles)
                    //     }
                    //     player.GetScriptScope().hShield <- SpawnEntityFromTable( "entity_medigun_shield", 
                    //     {
                    //         targetname = "projectile_shield"
                    //         origin = "0 0 0"
                    //         spawnflags = "3"
                    //         teamnum = "2"
                    //         skin = "2"
                    //     })
                        
                    //     GetEntityThinkTable( player ).GenerateShield <- GenerateShield

                    // break;

                    case "bot_engineer_avenger":

                        function AllyAvenger()
                        {
                            local cur_wep = self.GetActiveWeapon()
                            local upgrade_fire_rate_count = 5
                            for (local player; player = FindByClassname(player, "player");)
                            {
                                if 
                                ( 
                                    player.GetTeam() == self.GetTeam() 
                                    && aryDying.find(player) == null 
                                    && !player.IsAlive()
                                )
                                {
                                    printl("found dying ally: "+player)
                                    local particle = SalvageGamemode.DispatchParticleEffectEx( "spell_lightningball_hit_zap_blue", player.GetOrigin(), Vector(), [-1, 0.5], null, cur_wep )
                                    aryDying.append(player)
                                    inDeathCount++

                                    // Gains a critboost when an ally falls
                                    self.AddCondEx( TF_COND_CRITBOOSTED_USER_BUFF, 3, null )
                                }
                            }

                            aryDying = SalvageGamemode.IterateArray( aryDying, function( slot, entry )
                            {
                                local ally = entry

                                if ( ally.IsAlive() )
                                {
                                    return REMOVE_ENTRY
                                }
                                else 
                                {
                                    return KEEP_ENTRY
                                }
                            })

                            local fire_rate_threshold = floor(inDeathCount / upgrade_fire_rate_count)
                            flFiringSpeed = 1.0 - (fire_rate_threshold * 0.1)
                            
                            // Cap at minimum of 0.1
                            if ( flFiringSpeed < 0.1 )
                            {
                                flFiringSpeed = 0.1
                            }

                            if ( flFiringSpeed < 0.6 )
                            {
                                self.AddCustomAttribute("bullets per shot bonus", 3, -1)
                            }

                            if ( flFiringSpeed < 0.3 )
                            {
                                self.AddCustomAttribute("bullets per shot bonus", 6, -1)
                            }

                            self.AddCustomAttribute("fire rate bonus HIDDEN", flFiringSpeed, -1)
                            
                            printl("Deaths: " + inDeathCount + " | Fire speed: " + flFiringSpeed)

                            
                        }
                        player.GetScriptScope().inDeathCount <- 0
                        player.GetScriptScope().flFiringSpeed <- 1
                        player.GetScriptScope().aryDying <- []
                        
                        GetEntityThinkTable( player ).AllyAvenger <- AllyAvenger

                    break;

                    case "tag_melee_closerange":

                        function UseMeleeWhenClose()
                        {
                            local cur_origin = self.GetOrigin()
                            local model_scale = self.GetModelScale()
                            local melee_range = 64 * model_scale
                            local primary = SalvageGamemode.GetItemInSlot( self, 0 )
                            local secondary = SalvageGamemode.GetItemInSlot( self, 1 )
                            local melee = SalvageGamemode.GetItemInSlot( self, 2 )
                            if ( melee == null )
                            {
                                return
                            }

                            local range_mult = melee.GetAttribute( "melee range multiplier", 1 )
                            local sword_additive = melee.GetAttribute( "is_a_sword", 0 )

                            // check if we have a sword for extended range
                            if ( sword_additive != 0 )
                            {
                                melee_range += sword_additive
                            }

                            melee_range *= range_mult

                            local aryEnemyList = SalvageGamemode.GetEnemiesWithinArea( self, melee_range, cur_origin + ( Vector( 0, 0, 25 ) * model_scale ) )
                            
                            if ( aryEnemyList.len() > 0 && bSwitchBool == true ) 
                            {
                                // //// // printl( "SWITCH MELEE" )
                                self.AddWeaponRestriction( RESTRICT_MELEE )
                                self.RemoveWeaponRestriction( RESTRICT_PRIMARY )
                                bSwitchBool = false
                            }
                            else if ( aryEnemyList.len() == 0 && bSwitchBool == false )
                            {
                                // //// // printl( "SWITCH PRIMARY" )
                                self.RemoveWeaponRestriction( RESTRICT_MELEE )
                                self.AddWeaponRestriction( RESTRICT_PRIMARY )
                                bSwitchBool = true
                            }
                        }

                        // Set up the think function
                        player.GetScriptScope().bSwitchBool <- false
                        GetEntityThinkTable( player ).UseMeleeWhenClose <- UseMeleeWhenClose
                        
                    break;

                    default:
                        //// // printl( "No eligible tags" )
                    break;
                }
            }
        }
        else 
        {
            // get each item in our loadout
            
        }

        // MISSION SPECIFIC

    }  

    // Event Hooks

    // mandatory events
    function OnGameEvent_recalculate_holidays( _ ) 
    { 
        if ( GetRoundState() == 3 ) 
        {
            Cleanup()
        } 
    }
    function OnGameEvent_mvm_wave_complete( _ ) 
    {
        Cleanup() 
    }

    function OnGameEvent_player_team ( params )
    {
        local player = GetPlayerFromUserID( params.userid )
        local old_team = params.oldteam

        if ( player.GetTeam() == TEAM_SPECTATOR )
        {
            PlayerCleanupEx( player )
        }
    }

    function OnGameEvent_mvm_begin_wave ( params ) 
    {
        // StartFailTimer()
    }

    function OnGameEvent_post_inventory_application ( params )
    {
        local player = GetPlayerFromUserID( params.userid )
        player.ValidateScriptScope()
        local scope = player.GetScriptScope()

        printl(player)
    }

    function OnGameEvent_crossbow_heal ( params )
    {
        local healer = GetPlayerFromUserID( params.healer )
        local patient = GetPlayerFromUserID( params.target )

        patient.ValidateScriptScope()

        patient.GetScriptScope().inPsychoboostTime <- Time()
        SalvageGamemode.GetEntityThinkTable( patient ).PsychoBuff <- SalvageGamemode.PsychoBuff
    }

    function OnGameEvent_player_spawn ( params )
    {
        local player = GetPlayerFromUserID( params.userid )

        player.ValidateScriptScope()
        local scope = player.GetScriptScope()
        local bInWave = GetPropBool( mvm_logic_entity, "m_bMannVsMachineBetweenWaves" ) 

        EntFireByHandle( player, "RunScriptCode", "SalvageGamemode.PlayerSpawnEx( self )", 0, player, player )
    }

    function OnGameEvent_player_death ( params )
    {
        local victim = GetPlayerFromUserID( params.userid )
        local attacker = GetPlayerFromUserID( params.attacker ) 
        local weapon = EntIndexToHScript( params.inflictor_entindex )
        local assister = GetPlayerFromUserID( params.assister ) 

    
        if ( attacker == null )
        {
            return
        }

        if ( victim.IsBotOfType( 1337 ) && victim.HasBotTag("tag_scrapper"))
        {
            local gib_model_list = 
            [
                "models/flags/toolbox_flag.mdl" 
            ]

            local gib_choice = RandomInt(0, gib_model_list.len() - 1)
            local model_scrap = gib_model_list[gib_choice]

            local pickup = SpawnEntityFromTable( "tf_halloween_pickup", 
            {
                targetname = "__scrap"
                origin = victim.GetOrigin()
                powerup_model = model_scrap
                pickup_sound = "ui/item_metal_scrap_pickup.wav"
                pickup_particle = "tada_default"
                angles = "0 0 0"
                AutoMaterialize = 1
                fademaxdist = 0
                fademindist = -1
                TeamNum = 2
                StartDisabled = 0

                // OnPlayerTouch = "!activator,RunScriptCode,printl(activator),0,-1"
                // OnPlayerTouch = "!activator,RunScriptCode,activator.GetScriptScope().inScrapAmount += 1,0,-1"
                // OnPlayerTouch = "!activator,RunScriptCode,printl(activator.GetScriptScope().inScrapAmount),0,-1"
                // OnPlayerTouch = "!self,RunScriptCode,self.GetScriptScope().inScrapAmount += 1,0,-1"
                OnRedPickup = "!self,Kill,,0,1"
            })
            pickup.SetCycle(RandomFloat(0, 1))
            // AddOutput(pickup, "OnPlayerTouch", "!self", "RunScriptCode", "printl(`self: `+self)", -1, 1)
            // AddOutput(pickup, "OnPlayerTouch", "!activator", "RunScriptCode", "printl(`activ: `+activator)", -1, 1)

            AddOutput(pickup, "OnPlayerTouch", "!self", "RunScriptCode", "self.GetScriptScope().hPickUpper <- activator", -1, 1)
            pickup.ValidateScriptScope()

            SalvageGamemode.SetDestroyCallback( pickup, function()
            {
                printl("on kill self: "+hPickUpper)

                if ( hPickUpper == null || !hPickUpper.IsValid())
                {
                    return
                }

                hPickUpper.GetScriptScope().inScrapAmount += 1
                local sound_range = ( 40 + ( 20 * log10( 4000 / 36.0 ) ) ).tointeger();

                local channel = RandomInt(40, 80)
                EmitSoundEx
                ({
                    sound_name = "ui/item_metal_scrap_pickup.wav",
                    origin = self.GetOrigin(),
                    volume = 1,
                    sound_level = sound_range,
                    channel = channel,
                    entity = hPickUpper,
                    filter_type = RECIPIENT_FILTER_SINGLE_PLAYER
                });
                EmitSoundEx
                ({
                    sound_name = "ui/item_metal_scrap_pickup.wav",
                    origin = self.GetOrigin(),
                    volume = 1,
                    sound_level = sound_range,
                    channel = channel,
                    entity = hPickUpper,
                    filter_type = RECIPIENT_FILTER_SINGLE_PLAYER
                });
            })

            // AddOutput(pickup, "OnRedPickup", "!activator", "RunScriptCode", "printl(`yay`)", 0, 1)
            pickup.SetSkin(1)
            pickup.SetSequence(1)
        }

        if ( !( params.death_flags & 32 ) )
        {
            PlayerCleanupEx( victim )
        }
    }

    function OnGameEvent_player_disconnect ( params )
    {
        local player = GetPlayerFromUserID( params.userid )
        if ( !player )
        {
            return
        }
        player.ValidateScriptScope()
        local playerscope = player.GetScriptScope()

        player.AcceptInput( "RunScriptCode", "SalvageGamemode.PlayerCleanupEx( self )", player, player )
    }

    function OnGameEvent_player_say ( params ) 
    {
        local text = params.text 
        local player = GetPlayerFromUserID( params.userid )
        local scope = player.GetScriptScope()
        local cur_wave = GetPropInt( mvm_logic_entity, "m_nMannVsMachineWaveCount" )

    }

    function OnGameEvent_player_builtobject ( params )
    {
        local player = GetPlayerFromUserID( params.userid )

        local sentry = EntIndexToHScript( params.index )

        local cur_loadout = SalvageGamemode.GetLoadout( player )

        local is_gunslinger = false

        foreach ( i, item in cur_loadout )
        {

            if ( GetPropInt(item, STRING_NETPROP_ITEMDEF) == 142 ) // gunslinger
            {
                is_gunslinger = true
                break
            }
        }

        if ( is_gunslinger == true && player.GetTeam() == TF_TEAM_BLUE)
        {
            sentry.SetModelScale(0.8, 0)
            SetPropBool(sentry, "m_bMiniBuilding", true)
            SetPropInt(sentry, "m_iHighestUpgradeLevel", 1)
            SetPropInt(sentry, "m_iObjectMode", 1)
            sentry.KeyValueFromString("defaultupgrade", "0")
            sentry.SetHealth(100)
            sentry.SetMaxHealth(100)
        }
    }

    function OnGameEvent_teamplay_broadcast_audio ( params )
    {
        local tank_audio = "Announcer.MVM_Tank_Alert_Spawn"
        local tank_audio_alt = "Announcer.MVM_Tank_Alert_Multiple"
        if ( params.sound == tank_audio || params.sound == tank_audio_alt )
        {
            for (local tank; tank = FindByClassname(tank, "tank_boss");)
            {
                tank.ValidateScriptScope()
                local scope = tank.GetScriptScope()
                local health = tank.GetMaxHealth()
                local s = health.tostring()
                local d = s.len()
                if (d > 6) s = s.slice(0, d - 6) + "m"
                else if (d > 3) s = s.slice(0, d - 3) + "k"

                if ( !( "iTankHealthMsg" in scope) )
                {
                    scope.iTankHealthMsg <- 1

                    MiscPrintFunc( null, "99CCFF", "Tank deployed with "+s+ " ("+health+") health!" ) 
                }
            }
        }
    }

    // Runs whenever anything takes damage
    function OnScriptHook_OnTakeDamage ( params )
    {
        local victim = params.const_entity
        local weapon = params.weapon
        local attacker = params.attacker
        local entity = params.inflictor
        local damage = params.damage
        local dmgtype = params.damage_type
        local dmg_special = params.damage_stats
        local damage_force = params.damage_force
        local damage_position = params.damage_position
        local inflictor = params.inflictor
        local force_friendly_fire = params.force_friendly_fire

        local params_table = 
        {
            victim = victim,
            weapon = weapon,
            attacker = attacker,
            entity = entity,
            damage = damage,
            dmgtype = dmgtype,
            dmg_special = dmg_special
            damage_force = damage_force
            damage_position = damage_position
            inflictor = inflictor
            force_friendly_fire = force_friendly_fire
        };

        ////// // printl( "entity: "+entity )
        local classnames = 
        {
            "player" : 1
            "obj_teleporter" : 1
            "obj_sentrygun" : 1
            "obj_dispenser" : 1
            "tank_boss" : 1
        }

        local uber_conds = 
        [ 
            5, // stock
            14, // bonk
            51, // mvm spawn
            52, // canteen
            57, // wheel of fate
        ]

        local run_damage_table = true

        if ( victim.GetTeam() == TEAM_SPECTATOR )
        {
            // //////// // printl( "Spectator shouldnt run for" )
            run_damage_table = false
        }

        foreach ( cond in uber_conds )
        {
            if ( victim.GetClassname() != "player" )
                continue
            
            if ( victim.InCond( cond ) )
            {
                run_damage_table = false
            }
        }

        foreach ( name, func_table in SalvageGamemode.TakeDamageFuncTable ) 
        {
            if ( run_damage_table == false )
                continue

            func_table.call( params_table )
        }
        
        // needed to read anything we update in our takedamagetable functions
        params.const_entity = params_table.victim;
        params.weapon = params_table.weapon;
        params.attacker = params_table.attacker;
        params.inflictor = params_table.entity;
        params.damage = params_table.damage;
        params.damage_type = params_table.dmgtype;
        params.damage_stats = params_table.dmg_special;
        params.damage_force = params_table.damage_force;
        params.damage_position = params_table.damage_position;
        params.inflictor = params_table.inflictor;
        params.force_friendly_fire = params_table.force_friendly_fire
    }
};

for ( local i = 1, player; i <= MaxClients(); i++ )
{
    if ( player = PlayerInstanceFromIndex( i ), player && player.GetTeam() == TEAM_RED )
    {
        EntFireByHandle( player, "RunScriptCode", "SalvageGamemode.PlayerSpawnEx( self )", -1, player, player )
    }
}

GetAllAreas( SalvageGamemode.MapNavAreas )
__CollectGameEventCallbacks( SalvageGamemode ) // the OnGameEvent/OnScriptHook outputs need this