local REDEEMER_HIT_DEBOUNCE = 10
local REDEEMER_HIT_DAMAGE = 25
-- local REDEEMER_HIT_DAMAGE_ADDITION = 10 -- increased each hit
-- local REDEEMER_HIT_DAMAGE_ADDITION_CAP = 50

local REDEEMER_HIT_DAMAGE_ADDITION_MULT_ADDITIVE = 1 -- increased each hit
local REDEEMER_HIT_DAMAGE_ADDITION_CAP_ADDITIVE = 5

local WINGER_COOLDOWN = 3

local DRONES_CAP = 2

--This number * 100 is the damage required to fully charge, not counting natural recharge rate
local BUSTER_TRANSFORMER_CHARGE_DIVISOR = 50
local BUSTER_TRANSFORMER_GIANT_DAMAGE = 2000
local BUSTER_TRANSFORMER_TANK_DAMAGE = 2000
local BUSTER_TRANSFORMER_BLAST_RADIUS = 256

local PHD_THRESHOLD = {
	["Small"] = -100,
	["Medium"] = 1,
	["Medium2"] = 1.8,
	["Large"] = 2.4,
	["Nuke"] = 4,
}

local PHD_EXPLOSIONS = {
	["Small"] = { Particle = "hammer_impact_button", Radius = 144, Damage = 50 },
	["Medium"] = { Particle = "ExplosionCore_buildings", Radius = 144, Damage = 125 },
	["Medium2"] = { Particle = "ExplosionCore_Wall", Radius = 144, Damage = 200 },
	["Large"] = { Particle = "ExplosionCore_Wall", Radius = 200, Damage = 250 }, --asplode_hoodoo, adjusted for minicrits
	["Nuke"] = { Particle = "skull_island_explosion", Radius = 600, Damage = 700 },
}

local PHD_FEEDBACK_CONDS = {
	-- ["Medium"] = TF_COND_SNIPERCHARGE_RAGE_BUFF,
	["Large"] = TF_COND_OFFENSEBUFF,
	["Nuke"] = TF_COND_CRITBOOSTED_CARD_EFFECT,
}

local PARRY_TIME = 0.8

-- local SCAVENGER_EXPLOSION_BASE_DAMAGE = 85
-- local SCAVENGER_EXPLOSION_BASE_DAMAGE_BOSS = 175

local classIndices_Internal = {
	[1] = "Scout",
	[3] = "Soldier",
	[7] = "Pyro",
	[4] = "Demoman",
	[6] = "Heavy",
	[9] = "Engineer",
	[5] = "Medic",
	[2] = "Sniper",
	[8] = "Spy",
}

local function findInTable(table, value)
	for i, v in pairs(table) do
		if v == value then
			return i
		end
	end
end

local function dictionaryLength(dictionary)
	local length = 0

	for i, _ in pairs(dictionary) do
		length = length + 1
	end

	return length
end

local callbacks = {}
local weaponsData = {}
local weaponTimers = {}

local CUSTOM_WEAPONS_INDICES = { "Parry", "Drone", "PHD", "SmolShield", "WingerDash", "Scavenger", "Diver", "bt" }
for _, weaponIndex in pairs(CUSTOM_WEAPONS_INDICES) do
	callbacks[weaponIndex] = {}
	weaponsData[weaponIndex] = {}
	weaponTimers[weaponIndex] = {}
end

function ClearCallbacks(index, activator, handle)
	handle = handle or activator:GetHandleIndex()

	local weaponCallbacks = callbacks[index][handle]

	if not weaponCallbacks then
		return
	end

	if activator and IsValid(activator) then
		for _, callbackId in pairs(weaponCallbacks) do
			activator:RemoveCallback(callbackId)
		end
	end

	callbacks[index][handle] = nil
end

function ClearData(index, activator, handle)
	handle = handle or activator:GetHandleIndex()

	local weaponData = weaponsData[index][handle]

	if not weaponData then
		return
	end

	weaponsData[index][handle] = nil
end

function ClearTimers(index, activator, handle)
	handle = handle or activator:GetHandleIndex()

	local weaponTimer = weaponTimers[index][handle]

	if not weaponTimer then
		return
	end

	for _, timerId in pairs(weaponTimer) do
		timer.Stop(timerId)
	end

	weaponTimers[index][handle] = nil
end

function NoMercyFailSafe(exEntName, activator, caller)
	local primary = activator:GetPlayerItemBySlot(0)

	if primary == nil then
		return
	end

	local exEnt = ents.FindByName(exEntName)

    if not exEnt then
        return
    end

	exEnt:Remove()
	caller:Remove()
	activator.m_flGravity = 0
end

function InstaLvLRefunded(_, activator)
	activator.InstantLevel = false
end
function InstaLvLPurchase(_, activator)
	activator.InstantLevel = true
end

function BuildingBuilt(_, building)
	if building.m_iHighestUpgradeLevel >= 2 then
		return
	end

	if building.m_bMiniBuilding ~= 0 then
		return
	end

	if building.m_bDisposableBuilding ~= 0 then
		return
	end

	local owner = building.m_hBuilder

	if not owner.InstantLevel then
		return
	end

	building.m_iHighestUpgradeLevel = 2
end

local cooldown_players = {}

local function _getPlayerCameraAngleUp(player)
	local pitch = player["m_angEyeAngles[0]"]

	return Vector(0, 0, -pitch)
end

--Used by Buster Transformer, copied from Collateral Damage
local function fireTraceBetweenTwoEntities(pos1, pos2)
    DefaultTraceInfo = {
        start = pos1, -- Start position vector. Can also be set to entity, in this case the trace will start from entity eyes position
        endpos = pos2, -- End position vector. If nil, the trace will be fired in `angles` direction with `distance` length
        mask = MASK_SOLID, -- Solid type mask, see MASK_* globals
        collisiongroup = COLLISION_GROUP_PLAYER, -- Pretend the trace to be fired by an entity belonging to this group. See COLLISION_GROUP_* globals
        filter = nil -- Entity to ignore. If nil, filters start entity. Can be a single entity, table of entities, or a function with a single entity parameter that returns true if trace should hit the entity, false otherwise.
    }
    trace = util.Trace(DefaultTraceInfo)
    return trace.Entity
end

--Also copied from Collateral Damage holy
local function getAllPlayersInBusterExplosion(blastCenter, blastRadius)

    local entsInBlastRadius = ents.FindInSphere(blastCenter, blastRadius)
    local playersCaughtInBlast = {}

    for _, blastVictim in pairs(entsInBlastRadius) do

        if blastVictim:IsPlayer() == false then
            goto continue
        end
        
        if blastVictim:IsAlive() == false then
            goto continue
        end

		if blastVictim.m_iTeamNum ~= 3 then
			goto continue
		end
        
        -- Jank af, didnt use

        -- local hitEntityFromBlast = fireTraceBetweenTwoEntities(blastCenter, blastVictim)
        -- -- -- print("Hit a: " .. hitEntityFromBlast:GetClassname())
        -- if hitEntityFromBlast:GetClassname() ~= "player" then
        --     -- print("Blast victim was behind cover!")
        --     goto continue
        -- end

        playersCaughtInBlast[#playersCaughtInBlast+1] = blastVictim

        ::continue::
    end

    return playersCaughtInBlast
end

local function getAllSentriesInBusterExplosion(blastCenter, blastRadius)

    local entsInBlastRadius = ents.FindInSphere(blastCenter, blastRadius)
    local sentriesCaughtInBlast = {}

    for _, blastVictim in pairs(entsInBlastRadius) do

        if blastVictim:GetClassname() ~= "obj_sentrygun" then
            goto continue
        end

		if blastVictim.m_iTeamNum ~= 3 then
			goto continue
		end

        sentriesCaughtInBlast[#sentriesCaughtInBlast+1] = blastVictim

        ::continue::
    end

    return sentriesCaughtInBlast
end

--I'm sure these can be bundled into one function but I cba for now
local function getAllTanksInBusterExplosion(blastCenter, blastRadius)

    local entsInBlastRadius = ents.FindInSphere(blastCenter, blastRadius)
    local tanksCaughtInBlast = {}

    for _, blastVictim in pairs(entsInBlastRadius) do

        if blastVictim:GetClassname() ~= "tank_boss" and blastVictim:GetClassname() ~= "base_boss" then
            goto continue
        end

        tanksCaughtInBlast[#tanksCaughtInBlast+1] = blastVictim

        ::continue::
    end

    return tanksCaughtInBlast
end

local function _dash(player)
	local handle = player:GetHandleIndex()

	if cooldown_players[handle] then
		return
	end

	cooldown_players[handle] = true

	local secondary = player:GetPlayerItemBySlot(1)
	secondary:SetAttributeValue("add cond when active", nil)

	timer.Simple(WINGER_COOLDOWN, function()
		cooldown_players[handle] = nil

		if IsValid(secondary) then
			secondary:SetAttributeValue("add cond when active", TF_COND_SNIPERCHARGE_RAGE_BUFF)
		end
	end)

	local angle = _getPlayerCameraAngleUp(player)

	player:SetForwardVelocity(1000)
	player:AddOutput("BaseVelocity " .. tostring(angle * 10))
end

function WingerDashPurchased(_, activator)
	local handle = activator:GetHandleIndex()
	callbacks.WingerDash[handle] = {}

	local dashCallbacks = callbacks.WingerDash[handle]

	dashCallbacks.input = activator:AddCallback(ON_KEY_PRESSED, function(_, key)
		if key ~= IN_ATTACK2 then
			return
		end

		local secondary = activator:GetPlayerItemBySlot(1)
		local secondaryHandle = secondary:GetHandleIndex()

		if activator.m_hActiveWeapon:GetHandleIndex() ~= secondaryHandle then
			return
		end

		_dash(activator)
	end)

	dashCallbacks.died = activator:AddCallback(ON_DEATH, function()
		local secondary = activator:GetPlayerItemBySlot(1)

		if secondary:GetAttributeValue("cannot giftwrap") then
			return
		end

		WingerDashRefunded(_, activator)
	end)

	dashCallbacks.spawned = activator:AddCallback(ON_SPAWN, function()
		local secondary = activator:GetPlayerItemBySlot(1)

		if secondary:GetAttributeValue("cannot giftwrap") then
			return
		end

		WingerDashRefunded(_, activator)
	end)
end

function WingerDashRefunded(_, activator)
	if IsValid(activator) then
		ClearCallbacks("WingerDash", activator, activator:GetHandleIndex())
	end
end

local function _parry(activator)
	local handle = activator:GetHandleIndex()

	weaponsData.Parry[handle] = true

	timer.Simple(PARRY_TIME, function()
		weaponsData.Parry[handle] = nil
	end)
end

function ParryAddictionEquip(_, activator)
	-- fix weird quirk with template being spawned after you switch to a different class
	if classIndices_Internal[activator:DumpProperties().m_iClass] ~= "Demoman" then
		return
	end

	print("parry addiction equipped")
	local handle = activator:GetHandleIndex()

	if callbacks.Parry[handle] then
		ParryAddictionUnequip(activator, handle)
	end

	callbacks.Parry[handle] = {}

	local parryCallbacks = callbacks.Parry[handle]

	-- on key press
	parryCallbacks.keyPress = activator:AddCallback(7, function(_, key)
		if key ~= IN_ATTACK2 then
			return
		end

		if activator.m_flChargeMeter < 100 then
			return
		end

		if weaponsData.Parry[handle] then
			activator.m_flChargeMeter = 0
			-- activator:AcceptInput("$SetProp$m_flChargeMeter", "0")
			return
		end

		activator:AddCond(46, PARRY_TIME)

		activator.m_flChargeMeter = 0
		-- activator:AcceptInput("$SetProp$m_flChargeMeter", "0")

		_parry(activator)
	end)

	-- on damage
	parryCallbacks.onDamagePre = activator:AddCallback(3, function(_, damageInfo)
		if not weaponsData.Parry[handle] then
			return
		end

		if not damageInfo.Attacker then
			-- negate damage, don't try deflecting
			damageInfo.Damage = 0
			return true
		end

		if damageInfo.Attacker == activator then
			return
		end

		local deflectDmg = damageInfo.Damage * 2

		local deflectDmgInfo = {
			Attacker = activator,
			Inflictor = nil,
			Weapon = nil,
			Damage = deflectDmg,
			DamageType = 0,
			DamageCustom = 0,
			DamagePosition = Vector(0, 0, 0),
			DamageForce = Vector(0, 0, 0),
			ReportedPosition = Vector(0, 0, 0),
		}

		-- negate damage
		damageInfo.Damage = 0

		-- deflect
		damageInfo.Attacker:TakeDamage(deflectDmgInfo)

		return true
	end)

	parryCallbacks.onRemoved = activator:AddCallback(ON_REMOVE, function()
		ParryAddictionUnequip(activator, handle)
	end)

	parryCallbacks.onDeath = activator:AddCallback(9, function()
		ParryAddictionUnequip(activator, handle)
	end)

	parryCallbacks.onSpawn = activator:AddCallback(1, function()
		ParryAddictionUnequip(activator, handle)
	end)
end

function ParryAddictionUnequip(activator, handle)
	if not IsValid(activator) then
		activator = nil
	end

	ClearCallbacks("Parry", activator, handle)
	ClearData("Parry", activator, handle)
end


local noReprogram = {}
function _OnWaveSpawnBot_CustomWeapon(bot, _, tags)
	noReprogram[bot] = nil

	if not findInTable(tags, "no_reprogram") then
		return
	end

	noReprogram[bot] = true
end

local controlled = {}
function SeducerHit(_, activator, caller)
	if noReprogram[caller] then
		return
	end

	if not caller:IsPlayer() then
		return
	end

	if caller:IsRealPlayer() then
		return
	end

	if caller.m_bIsMiniBoss ~= 0 then
		return
	end

	if caller:InCond(TF_COND_INVULNERABLE_HIDE_UNLESS_DAMAGED) ~= 0 then
		return
	end

	if caller.m_iTeamNum == activator.m_iTeamNum then
		return
	end

	local handle = activator:GetHandleIndex()

	if controlled[handle] then
		return
	end

	controlled[handle] = caller

	caller:AddCond(TF_COND_REPROGRAMMED)
	caller:AddCond(TF_COND_CRITBOOSTED_CARD_EFFECT)

	local secondary = activator:GetPlayerItemBySlot(1)
	secondary:SetAttributeValue("add cond when active", nil)

	timer.Simple(8, function()
		controlled[handle] = nil
		caller:Suicide()

		if IsValid(secondary) then
			secondary:SetAttributeValue("add cond when active", TF_COND_SNIPERCHARGE_RAGE_BUFF)
		end
	end)
end


local redeemerDebounces = {} --value is debounce players

-- redeemer
ents.AddCreateCallback("tf_projectile_rocket", function(entity)
	timer.Simple(0, function()
		if not IsValid(entity) then
			return
		end

		local owner = entity.m_hOwnerEntity

		if not owner then
			return
		end

		if not owner.hasRedeemer or owner.hasRedeemer ~= 1 then
			return
		end

		-- not scout, just in case
		if owner.m_iClass ~= 1 then
			return
		end

		local extraDamageMult = 1

		local handle = entity:GetHandleIndex()

		redeemerDebounces[handle] = {}

		entity:AddCallback(0, function()
			redeemerDebounces[handle] = nil
		end)

		entity:AddCallback(16, function(_, target)
			if not target or not target:IsPlayer() then
				return true
			end

			if target == owner then
				return
			end

			local targetHandle = target:GetHandleIndex()

			local nextAllowedDamageTickOnTarget = redeemerDebounces[handle][targetHandle] or -1

			if CurTime() < nextAllowedDamageTickOnTarget then
				return false
			end

			local targetTeamnum = target._iTeamNum

			if targetTeamnum == owner.m_iTeamNum then
				return false
			end

			redeemerDebounces[handle][targetHandle] = CurTime() + REDEEMER_HIT_DEBOUNCE

			local secondary = owner:GetPlayerItemBySlot(1)

			local baseDamage = REDEEMER_HIT_DAMAGE
			local damageMult = secondary:GetAttributeValue("damage bonus") or 1

			local visualHitPost = target:GetAbsOrigin() + Vector(0, 0, 50)

			local damageInfo = {
				Attacker = owner,
				Inflictor = nil,
				Weapon = secondary,
				Damage = baseDamage * damageMult * extraDamageMult,
				DamageType = DMG_SHOCK,
				DamageCustom = 0,
				DamagePosition = visualHitPost,
				DamageForce = Vector(0, 0, 0),
				ReportedPosition = visualHitPost,
			}

			local dmg = target:TakeDamage(damageInfo)

			extraDamageMult = extraDamageMult + REDEEMER_HIT_DAMAGE_ADDITION_MULT_ADDITIVE

			if extraDamageMult > REDEEMER_HIT_DAMAGE_ADDITION_CAP_ADDITIVE then
				extraDamageMult = REDEEMER_HIT_DAMAGE_ADDITION_CAP_ADDITIVE
			end

			return false
		end)
	end)
end)


-- drone
function DroneFired(sentryName, projectile)
	local owner = projectile.m_hOwnerEntity

	local sentryEnt = ents.FindByName(sentryName)

	if owner.m_iTeamNum == 3 then
		timer.Simple(0, function()
			sentryEnt.m_nSkin = 1
		end)

		sentryEnt.m_iTeamNum = 3
	end

	timer.Simple(0.6, function()
		sentryEnt.m_hBuilder = owner
	end)

	local firerateUpgrade = owner:GetPlayerItemBySlot(LOADOUT_POSITION_PDA):GetAttributeValue("engy sentry fire rate increased") or 1

	local primary = owner:GetPlayerItemBySlot(0)
	local mothershipUpgrade = primary:GetAttributeValue("throwable damage")

	if mothershipUpgrade then
		sentryEnt.m_flModelScale = 2.5
		timer.Simple(0.1, function()
			sentryEnt:SetHealth(250)
		end)
		sentryEnt["$attributeoverride"] = 1
		sentryEnt["$damagemult"] = 0.8
		sentryEnt["$fireratemult"] = firerateUpgrade + 1
		-- sentryEnt["$rangemult"] = 0.5
		sentryEnt["$bulletweapon"] = "Upgradeable TF_WEAPON_ROCKETLAUNCHER"
	end

	local ownerHandle = owner:GetHandleIndex()

	local dronesData = weaponsData.Drone[ownerHandle]

	local dronesList = dronesData.DronesList

	if dictionaryLength(dronesList) >= dronesData.DronesCap then
		-- remove first drone
		-- automatically cleared from array
		dronesList[1]:Remove()
	end

	table.insert(dronesList, projectile)

	projectile:AddCallback(ON_REMOVE, function()
		if sentryEnt and IsValid(sentryEnt) then
			util.ParticleEffect("ExplosionCore_buildings", sentryEnt:GetAbsOrigin(), Vector(0, 0, 0))
			sentryEnt:Remove()
		end

		table.remove(dronesList, findInTable(dronesList, projectile))

		local stationaryId = dronesData.DronesStationaryIds[projectile]

		if stationaryId then
			dronesData.DronesStationaryIds[projectile] = nil
			timer.Stop(stationaryId)
		end
	end)
end

function UpdateDroneCap(_, activator)
	local handle = activator:GetHandleIndex()

	local dronesData = weaponsData.Drone[handle]

	-- remove any existing drones
	for _, id in pairs(dronesData.DronesStationaryIds) do
		timer.Stop(id)
	end

	for _, projectile in pairs(dronesData.DronesList) do
		projectile:Remove()
	end

	local droneCount = DRONES_CAP

	local primary = activator:GetPlayerItemBySlot(0)
	local mothershipUpgrade = primary:GetAttributeValue("throwable damage")
	local droneCountUpgrade = primary:GetAttributeValue("throwable fire speed")

	if mothershipUpgrade then
		droneCount = 1
	elseif droneCountUpgrade then
		droneCount = droneCount + 1
	end

	local melee = activator:GetPlayerItemBySlot(LOADOUT_POSITION_MELEE)
	if droneCountUpgrade then
		melee:SetAttributeValue("mod wrench builds minisentry", 1)
	else
		melee:SetAttributeValue("mod wrench builds minisentry", nil)
	end

	dronesData.DronesCap = droneCount
end

local notificationsShown = {}

function DroneWalkerEquip(_, activator)
	-- fix weird quirk with template being spawned after you switch to a different class
	if classIndices_Internal[activator.m_iClass] ~= "Engineer" then
		return
	end

	print("drone walker equipped")
	local handle = activator:GetHandleIndex()

	if callbacks.Drone[handle] then
		DroneWalkerUnequip(activator, handle)
	end

	if not notificationsShown[handle] then
		notificationsShown[handle] = true
		activator["$DisplayTextCenter"](activator, "Press alt-fire to make drones stationary")
	end

	-- local meleeWeapon = activator:GetPlayerItemBySlot(2)
	-- local gunslingerEquipped = meleeWeapon.m_iClassname == "tf_weapon_robot_arm"

	-- if gunslingerEquipped then
	-- 	primary:SetAttributeValue("always crit", 1)
	-- 	-- primary:SetAttributeValue("engy sentry damage bonus", 1.25)
	-- else
	-- 	primary:SetAttributeValue("always crit", nil)
	-- 	-- primary:SetAttributeValue("engy sentry damage bonus", nil)
	-- end

	callbacks.Drone[handle] = {}
	weaponsData.Drone[handle] = {
		DronesList = {},

		DronesStationaryIds = {},

		-- DronesCap = not gunslingerEquipped and DRONES_CAP or DRONES_CAP + 1,
		DronesCap = DRONES_CAP,

		-- Buffed = gunslingerEquipped,
	}

	UpdateDroneCap(_, activator)

	local droneCallbacks = callbacks.Drone[handle]
	local dronesData = weaponsData.Drone[handle]

	-- on key press
	droneCallbacks.keyPress = activator:AddCallback(ON_KEY_PRESSED, function(_, key)
		if key ~= IN_ATTACK2 then
			return
		end

		if activator.m_hActiveWeapon.m_iClassname ~= "tf_weapon_shotgun_building_rescue" then
			return
		end

		if dictionaryLength(dronesData.DronesStationaryIds) > 0 then
			for projectile, id in pairs(dronesData.DronesStationaryIds) do
				timer.Stop(id)
				dronesData.DronesStationaryIds[projectile] = nil
			end

			return
		end

		for _, projectile in pairs(dronesData.DronesList) do
			local origin = projectile:GetAbsOrigin()

			projectile:SetLocalVelocity(Vector(0, 0, 0))

			dronesData.DronesStationaryIds[projectile] = timer.Create(0, function()
				projectile:SetAbsOrigin(origin)
			end, 0)
		end
	end)

	droneCallbacks.onRemoved = activator:AddCallback(ON_REMOVE, function()
		DroneWalkerUnequip(activator, handle)
	end)

	droneCallbacks.onDeath = activator:AddCallback(ON_DEATH, function()
		DroneWalkerUnequip(activator, handle)
	end)

	droneCallbacks.onSpawn = activator:AddCallback(ON_SPAWN, function()
		DroneWalkerUnequip(activator, handle)
	end)
end

function DroneWalkerUnequip(activator, handle)
	if not IsValid(activator) then
		activator = nil
	end

	local dronesData = weaponsData.Drone[handle]

	-- remove any existing drones
	for _, id in pairs(dronesData.DronesStationaryIds) do
		timer.Stop(id)
	end

	for _, projectile in pairs(dronesData.DronesList) do
		projectile:Remove()
	end

	ClearCallbacks("Drone", activator, handle)
	ClearData("Drone", activator, handle)
end

function BTEquip(_, activator)
	-- fix weird quirk with template being spawned after you switch to a different class
	if classIndices_Internal[activator.m_iClass] ~= "Demoman" then
		return
	end

	print("buster transformer equipped")
	activator.m_flChargeMeter = 0
	local handle = activator:GetHandleIndex()

	timer.Simple(1, function()
		--Persian and Booties are not allowed to charge this
		local primaryWeapon = activator:GetPlayerItemBySlot(0)
		local meleeWeapon = activator:GetPlayerItemBySlot(2)

		--No booties passive with this weapon sori
		primaryWeapon:SetAttributeValue("kill refills meter", "0")

		--No persian passive either, unless... yknow i might consider it it's such a garbage weapon
		meleeWeapon:SetAttributeValue("charge meter on hit", "0")
		meleeWeapon:SetAttributeValue("ammo gives charge", "0")

		--Failsafe
		activator:SetAttributeValue("max health additive bonus", "0")
		activator:SetAttributeValue("no_attack", "0")
		activator:SetAttributeValue("move speed penalty", "1")
		activator:SetAttributeValue("damage force reduction", "1")
		activator:SetAttributeValue("always allow taunt", "0")
		activator:SetAttributeValue("cannot be backstabbed", "0")
		activator:SetAttributeValue("not solid to players", "0")

		activator:AcceptInput("AddOutput", "modelscale 1")
		activator:AcceptInput("SetForcedTauntCam", "0")
		activator:RemoveCond(7)
		activator:RemoveCond(32)
		activator:RemoveCond(41)
		--Reset model
		activator:SetCustomModel("")
	end)
	

	if callbacks.bt[handle] then
		BTUnequip(activator, handle)
	end

	callbacks.bt[handle] = {}
	weaponTimers.bt[handle] = {}
	weaponsData.bt[handle] = {}

	local btCallbacks = callbacks.bt[handle]
	local btTimers = weaponTimers.bt[handle]
	local btData = weaponsData.bt[handle]

	btCallbacks.removed = activator:AddCallback(ON_REMOVE, function()
		BTUnequip(activator, handle)
	end)

	btCallbacks.died = activator:AddCallback(ON_DEATH, function()
		BTUnequip(activator, handle)
		timer.Stop(btTimers.detonatorLogicParticle)
		timer.Stop(btTimers.detonatorLogicMain)
		ClearTimers("bt", activator, handle)
	end)

	btCallbacks.spawned = activator:AddCallback(ON_SPAWN, function()
		BTUnequip(activator, handle)
	end)

	btCallbacks.onmouse2 = activator:AddCallback(ON_KEY_PRESSED, function(ent, key)
		
		--mouse2 is 2048
		if key ~= 2048 or activator.m_flChargeMeter < 100 then
			return
		end

		activator.m_flChargeMeter = 0
		--Locked to melee
		activator:AddCond(41)
		--Speed boost
		activator:AddCond(32)
		--Force switch to melee? idk this was in the original caber buster template
		activator:AddCond(85)
		timer.Simple(0.015, function()
			activator:RemoveCond(85)
		end)
		timer.Simple(0.11, function()
			util.ParticleEffect("drg_wrenchmotron_teleport", activator:GetAbsOrigin(), Vector(0,0,0), activator)
			activator:PlaySound("misc/halloween/merasmus_spell.wav")
			activator:AcceptInput("$DisplayTextCenter", "TAUNT to explode!")

			--Surely there's a better way to do this
			activator:AcceptInput("AddOutput", "modelscale 1.75")
			activator:AcceptInput("SetForcedTauntCam", "1")
			activator:SetAttributeValue("max health additive bonus", "225")
			activator:SetAttributeValue("no_attack", "1")
			activator:SetAttributeValue("move speed penalty", "2")
			--activator:SetAttributeValue("no resupply", "1") --Buying smth from the upgrade station refills charge grahhh
			activator:SetAttributeValue("damage force reduction", "0.01") --Not fun getting knocked back around
			activator:SetAttributeValue("always allow taunt", "1")
			activator:SetAttributeValue("cannot be backstabbed", "1")
			activator:SetAttributeValue("not solid to players", "1") --Not fun being solid to players fr
		end)
		timer.Simple(0.2, function()
			activator:SetCustomModel("models/bots/demo/red_sentry_buster.mdl")
		end)
		timer.Simple(0.3, function()
			activator:AddHealth(400, false)
		end)

		btTimers.tauntChecker = timer.Create(0.1, function()
			
			if activator:InCond(TF_COND_TAUNTING) == 0 then
				goto noTaunt
			end

			timer.Stop(btTimers.tauntChecker)
			activator:SetAttributeValue("move speed penalty", "0.0001")
			activator:PlaySound("mvm/sentrybuster/mvm_sentrybuster_spin.wav")

			btTimers.detonatorLogicParticle = timer.Create(1.97, function()
				util.ParticleEffect("asplode_hoodoo", activator:GetAbsOrigin(), Vector(0,0,0), activator)
			end)

			btTimers.detonatorLogicMain = timer.Create(1.95, function()
				
				activator:PlaySound("mvm/sentrybuster/mvm_sentrybuster_explode.wav")

				--Is there no remove attribute?
				activator:SetAttributeValue("max health additive bonus", "0")
				activator:SetAttributeValue("no_attack", "0")
				activator:SetAttributeValue("move speed penalty", "1")
				--activator:SetAttributeValue("no resupply", "0")
				activator:SetAttributeValue("damage force reduction", "1")
				activator:SetAttributeValue("always allow taunt", "0")
				activator:SetAttributeValue("cannot be backstabbed", "0")
				activator:SetAttributeValue("not solid to players", "0")

				activator:AcceptInput("AddOutput", "modelscale 1")
				activator:AcceptInput("SetForcedTauntCam", "0")
				--Get out of taunting! No more speed boost and melee lock as well.
				activator:RemoveCond(7)
				activator:RemoveCond(32)
				activator:RemoveCond(41)
				--Temporary uber after exploding so that you dont instantly die
				activator:AddCond(52,2)
				--Reset model
				activator:SetCustomModel("")

				--Borrowed from collateral damage whatadeal
				--I'm sure this can be bundled into one function but I cba for now
				for _, blastVictim in pairs(getAllPlayersInBusterExplosion(activator:GetAbsOrigin(), BUSTER_TRANSFORMER_BLAST_RADIUS)) do
					BusterTakeDamage = {
						Attacker = activator, -- Attacker
						Inflictor = activator:GetPlayerItemBySlot(2), -- Direct cause of damage, usually a projectile
						Weapon = activator:GetPlayerItemBySlot(2),
						Damage = 9999,
						DamageType = DMG_BLAST, -- Damage type, see DMG_* globals. Can be combined with | operator
						DamageCustom = TF_DMG_CUSTOM_AIR_STICKY_BURST, -- Custom damage type, see TF_DMG_* globals
						DamagePosition = activator:GetAbsOrigin(), -- Where the target was hit at
						DamageForce = Vector(0,0,0), -- Knockback force of the attack
						ReportedPosition = activator:GetAbsOrigin() -- Where the attacker attacked from
					}
			
					if blastVictim.m_bIsMiniBoss == 1 then
						BusterTakeDamage.Damage = BUSTER_TRANSFORMER_GIANT_DAMAGE
						blastVictim:TakeDamage(BusterTakeDamage)
						goto continue
					end
					
					blastVictim:TakeDamage(BusterTakeDamage)
			
					::continue::
				end
			
				for _, blastVictim in pairs(getAllSentriesInBusterExplosion(activator:GetAbsOrigin(), BUSTER_TRANSFORMER_BLAST_RADIUS)) do
					BusterTakeDamage = {
						Attacker = activator, -- Attacker
						Inflictor = activator:GetPlayerItemBySlot(2), -- Direct cause of damage, usually a projectile
						Weapon = activator:GetPlayerItemBySlot(2),
						Damage = 9999,
						DamageType = DMG_BLAST, -- Damage type, see DMG_* globals. Can be combined with | operator
						DamageCustom = TF_DMG_CUSTOM_AIR_STICKY_BURST, -- Custom damage type, see TF_DMG_* globals
						DamagePosition = activator:GetAbsOrigin(), -- Where the target was hit at
						DamageForce = Vector(0,0,0), -- Knockback force of the attack
						ReportedPosition = activator:GetAbsOrigin() -- Where the attacker attacked from
					}
					blastVictim:TakeDamage(BusterTakeDamage)
			
					::continue::
				end

				for _, blastVictim in pairs(getAllTanksInBusterExplosion(activator:GetAbsOrigin(), BUSTER_TRANSFORMER_BLAST_RADIUS)) do
					BusterTakeDamage = {
						Attacker = activator, -- Attacker
						Inflictor = activator:GetPlayerItemBySlot(2), -- Direct cause of damage, usually a projectile
						Weapon = activator:GetPlayerItemBySlot(2),
						Damage = BUSTER_TRANSFORMER_TANK_DAMAGE,
						DamageType = DMG_BLAST, -- Damage type, see DMG_* globals. Can be combined with | operator
						DamageCustom = TF_DMG_CUSTOM_AIR_STICKY_BURST, -- Custom damage type, see TF_DMG_* globals
						DamagePosition = activator:GetAbsOrigin(), -- Where the target was hit at
						DamageForce = Vector(0,0,0), -- Knockback force of the attack
						ReportedPosition = activator:GetAbsOrigin() -- Where the attacker attacked from
					}
					blastVictim:TakeDamage(BusterTakeDamage)
			
					::continue::
				end

				activator.m_flChargeMeter = 0
			end)
			::noTaunt::
		end, 0)
		
	end)
end

AddEventCallback('player_hurt', function(event)

	if event.attacker == 0 then
		return
	end

	local playerAttacker = ents.GetPlayerByUserId(event.attacker)

	--With the current guard clauses you actually build up charge by damaging yourself
	--But seeing as it is 1% charge per 50 damage dealt, I decided it doesn't matter
	if playerAttacker.m_iTeamNum == 3 then
		return
	end

	-- print("Attacker was red!")

	local weaponUsed = playerAttacker:GetPlayerItemBySlot(1)

	if not weaponUsed:GetAttributeValue("dmg taken from blast reduced") then
		return
	end

	--The weapon's base 0.95 is turned into some dumb number like 0.94999... thanks float! This workaround is needed
	if weaponUsed:GetAttributeValue("dmg taken from blast reduced") > 0.94 and weaponUsed:GetAttributeValue("dmg taken from blast reduced") < 0.96 then
		-- print("And BT was equipped!")

		--+0.01 hack job for floats
		playerAttacker.m_flChargeMeter = playerAttacker.m_flChargeMeter + ((event.damageamount + 0.01) / BUSTER_TRANSFORMER_CHARGE_DIVISOR)
	end
end)


function PHDEquip(_, activator)
	-- fix weird quirk with template being spawned after you switch to a different class
	if classIndices_Internal[activator.m_iClass] ~= "Soldier" then
		return
	end

	print("phd jumper equipped")
	local handle = activator:GetHandleIndex()

	if callbacks.PHD[handle] then
		PHDUnequip(activator, handle)
	end

	callbacks.PHD[handle] = {}
	weaponTimers.PHD[handle] = {}
	weaponsData.PHD[handle] = {
		JumpStartTime = false,
	}

	local phdCallbacks = callbacks.PHD[handle]
	local phdTimers = weaponTimers.PHD[handle]
	local phdData = weaponsData.PHD[handle]

	phdCallbacks.removed = activator:AddCallback(ON_REMOVE, function()
		PHDUnequip(activator, handle)
	end)

	phdCallbacks.died = activator:AddCallback(ON_DEATH, function()
		PHDUnequip(activator, handle)
	end)

	phdCallbacks.spawned = activator:AddCallback(ON_SPAWN, function()
		PHDUnequip(activator, handle)
	end)

	local timeSpentParachuting = 0

	local function getCurrentThreshold(timeDiff)
		local currentThreshold = { nil, -10000 }

		for thresHoldName, timeRequired in pairs(PHD_THRESHOLD) do
			if timeRequired > currentThreshold[2] and timeDiff > timeRequired then
				currentThreshold = { thresHoldName, timeRequired }
			end
		end

		local chosenThreshold = currentThreshold[1]

		return chosenThreshold
	end

	local primary = activator:GetPlayerItemBySlot(0)

	phdTimers.rocketJumpCheck = timer.Create(0.1, function()
		local jumping = activator:InCond(TF_COND_BLASTJUMPING)

		local timeDiff
		local chosenThreshold

		if phdData.JumpStartTime then
			timeDiff = CurTime() - phdData.JumpStartTime - timeSpentParachuting
			chosenThreshold = getCurrentThreshold(timeDiff)

			local parachuting = activator:InCond(TF_COND_PARACHUTE_ACTIVE)

			if parachuting ~= 0 then
				timeSpentParachuting = timeSpentParachuting + 0.1
			end

			if PHD_FEEDBACK_CONDS[chosenThreshold] then
				primary:SetAttributeValue("add cond when active", PHD_FEEDBACK_CONDS[chosenThreshold])
			end
		end

		if jumping == 0 then
			if phdData.JumpStartTime then
				timeSpentParachuting = 0

				local activatorOrigin = activator:GetAbsOrigin()

				local explosionData = PHD_EXPLOSIONS[chosenThreshold]

				util.ParticleEffect(explosionData.Particle, activatorOrigin, Vector(0, 0, 0))
				local radius = explosionData.Radius
				local damage = explosionData.Damage

				local enemiesInRange = ents.FindInSphere(activatorOrigin, radius) --ents.GetAllPlayers()

				local damageMult = primary:GetAttributeValue("damage bonus") or 1

				for _, enemy in pairs(enemiesInRange) do
					if not enemy:IsCombatCharacter() then
						goto continue
					end

					if enemy.m_iTeamNum == activator.m_iTeamNum then
						goto continue
					end

					local damageInfo = {
						Attacker = activator,
						Inflictor = nil,
						Weapon = primary,
						Damage = damage * damageMult,
						DamageType = DMG_BLAST,
						DamageCustom = TF_DMG_CUSTOM_NONE,
						DamagePosition = enemy:GetAbsOrigin(), -- Where the target was hit at
						DamageForce = Vector(0, 0, 0), -- Knockback force of the attack
						ReportedPosition = activatorOrigin, -- Where the attacker attacked from
					}

					enemy:TakeDamage(damageInfo)

					::continue::
				end

				phdData.JumpStartTime = false
			end

			primary:SetAttributeValue("add cond when active", nil)

			return
		end

		if phdData.JumpStartTime then
			return
		end

		phdData.JumpStartTime = CurTime()
	end, 0)
end

function BTUnequip(activator, handle)
	if not IsValid(activator) then
		activator = nil
	end

	ClearCallbacks("bt", activator, handle)
	ClearData("bt", activator, handle)
	ClearTimers("bt", activator, handle)

	--Is there no remove attribute?
	--We need to reset all of these in the event the player dies as a sentry buster
	activator:SetAttributeValue("max health additive bonus", "0")
	activator:SetAttributeValue("no_attack", "0")
	activator:SetAttributeValue("move speed penalty", "1")
	--activator:SetAttributeValue("no resupply", "0")
	activator:SetAttributeValue("damage force reduction", "1")
	activator:SetAttributeValue("always allow taunt", "0")
	activator:SetAttributeValue("cannot be backstabbed", "0")
	activator:SetAttributeValue("not solid to players", "0")

	activator:AcceptInput("AddOutput", "modelscale 1")
	activator:AcceptInput("SetForcedTauntCam", "0")
	activator:RemoveCond(7)
	activator:RemoveCond(32)
	activator:RemoveCond(41)
	--Reset model
	activator:SetCustomModel("")
end

function PHDUnequip(activator, handle)
	if not IsValid(activator) then
		activator = nil
	end

	ClearCallbacks("PHD", activator, handle)
	ClearData("PHD", activator, handle)
	ClearTimers("PHD", activator, handle)
end

local function weaponMimic(properties, positional)
	local mimic = ents.CreateWithKeys("tf_point_weapon_mimic", properties)
	mimic:SetAbsOrigin(positional.Origin)
	mimic:SetAbsAngles(positional.Angles)

	return mimic
end

local function getEyeAngles(player)
	local pitch = player["m_angEyeAngles[0]"]
	local yaw = player["m_angEyeAngles[1]"]

	return Vector(pitch, yaw, 0)
end

-- thunderdome
function ThunderdomeEquipped(_, activator)
	local hasRedSun = activator:GetPlayerItemBySlot(LOADOUT_POSITION_PRIMARY):GetAttributeValue("throwable damage")
	local hasCommunist = activator:GetPlayerItemBySlot(LOADOUT_POSITION_PRIMARY):GetAttributeValue("throwable fire speed")

	local spawnflags = 65
	if hasRedSun or hasCommunist then
		spawnflags = 64
	end

	local namePrefix = "thunderdome"..tostring(activator:GetHandleIndex())

	local allEntities = {}

	for i = 1, 2 do
		local rotate = ents.CreateWithKeys("func_rotating", {
			mins = Vector(-0.1, -0.1, -0.1),
			maxs = Vector(0.1, 0.1, 0.1),
			dmg = 0,
			fanfriction = 100,
			maxspeed = 60,
			spawnflags = spawnflags,
			volume = 0,
			solid = 0,
		})

		rotate:SetName(namePrefix..tostring(rotate:GetHandleIndex()))
		if not hasCommunist and not hasRedSun then
			rotate["$positiononly"] = 1
		end
		rotate:SetFakeParent(activator)

		if (hasRedSun or hasCommunist) and i == 2 then
			break
		end

		local shieldOffset = i == 1 and Vector(100, 0, 0) or Vector(-100, 0, 0)
		local shieldAngles = i == 1 and Vector(0, 0, 0) or Vector(-180, 0, -180)

		if hasRedSun then
			shieldOffset = Vector(100, 0, 0)
			shieldAngles = Vector(0, 0, 0)
		elseif hasCommunist then
			shieldOffset = Vector(-50, 0, 0)
			shieldAngles = Vector(-180, 0, -180)
		end

		local shield = ents.CreateWithKeys("entity_medigun_shield", {
			angles = shieldAngles,
			spawnflags = 1,
			teamnum = activator.m_iTeamNum,
		}, true, true)


		shield["$fakeparentoffset"] = shieldOffset
		shield["$fakeparentrotation"] = shieldAngles

		shield:SetModel("models/props_mvm/mvm_comically_small_player_shield.mdl")
		shield:SetFakeParent(rotate)
		local shieldName = namePrefix..tostring(shield:GetHandleIndex().."Shield")
		shield:SetName(shieldName)

		RegisterShieldThunderdome(shieldName, activator)

		table.insert(allEntities, rotate)
		table.insert(allEntities, shield)
	end

	local thunderdomeCallbacks = {}
	thunderdomeCallbacks[1] = activator:AddCallback(ON_REMOVE, function()
		for _, ent in pairs(thunderdomeCallbacks) do
			ent:Remove()
		end
	end)
	thunderdomeCallbacks[2] = activator:AddCallback(ON_SPAWN, function()
		for _, callback in pairs(thunderdomeCallbacks) do
			activator:RemoveCallback(callback)
		end
	end)
end

function ThunderdomeUnequipped(_, activator)
	local namePrefix = "thunderdome"..tostring(activator:GetHandleIndex())

	for _, ent in pairs(ents.FindAllByName(namePrefix.."*")) do
		ent:Remove()
	end
end

function ThunderdomeRefresh(_, activator)
	ThunderdomeUnequipped(nil, activator)
	ThunderdomeEquipped(nil, activator)
end

-- explosive arrow
local function spawnArrowTip(owner, arrowOrigin, attachEnt)
	local detonatePosition = Entity("info_target")
	local detonatePositionName = "detonatePosition"..tostring(detonatePosition:GetHandleIndex())
	detonatePosition:SetName(detonatePositionName)
	detonatePosition:SetAbsOrigin(arrowOrigin)

	local flickerParticle = owner.m_iTeamNum == 2 and "stickybomb_pulse_red" or "stickybomb_pulse_blue"
	local flicker = ents.CreateWithKeys("info_particle_system", {
		effect_name = flickerParticle,
		start_active = 0,
		flag_as_weather = 0,
	}, true, true)
	flicker:SetAbsOrigin(arrowOrigin)
	flicker:SetFakeParent(detonatePosition)


	local sound = ents.CreateWithKeys("ambient_generic", {
		message = "weapons/stickybomblauncher_det.wav",
		radius = 4000,
		health = 5,
		spawnflags = 48,
	})
	sound:SetAbsOrigin(arrowOrigin)
	sound:SetFakeParent(detonatePosition)

	timer.Simple(1, function ()
		flicker:Start()
		sound:PlaySound()

		timer.Simple(1, function ()
			if IsValid(flicker) then
				flicker:Remove()
			end
			if IsValid(sound) then
				sound:Remove()
			end
		end)
	end)

	local damageMult = owner:GetPlayerItemBySlot(LOADOUT_POSITION_PRIMARY):GetAttributeValue("damage bonus") or 1

	local mimic = weaponMimic({
		TeamNum = owner.m_iTeamNum,
		WeaponType = 3,

		SpeedMax = 0,
		SpeedMin = 0,

		spawnflags = 3,
		SplashRadius = 144,
		Damage = 100 * damageMult,
	},
	{
		Origin = owner:GetAbsOrigin() + Vector(0, 0, 50),
		Angles = getEyeAngles(owner),
	})

	mimic["$SetOwner"](mimic, owner)

	mimic:SetAbsOrigin(arrowOrigin)
	mimic:SetFakeParent(detonatePosition)

	timer.Simple(2, function()
		mimic:FireOnce()
		timer.Simple(0.015, function()
			mimic:DetonateStickies()
		end)
		timer.Simple(1, function()
			mimic:Remove()
		end)
	end)

	for _, ent in pairs({mimic, flicker, sound}) do
		local entName = "detonateComponent"..tostring(ent:GetHandleIndex())
		ent:SetName(entName)
	end

	if attachEnt then
		local offset = arrowOrigin - attachEnt:GetAbsOrigin()
		detonatePosition["$fakeparentoffset"] = offset
		detonatePosition:SetFakeParent(attachEnt)
	end

	timer.Simple(5, function ()
		detonatePosition:Remove()
	end)
end

ents.AddCreateCallback("tf_projectile_arrow", function(arrow)
    timer.Simple(0, function()
		local owner = arrow.m_hOwnerEntity
        local primary = owner:GetPlayerItemBySlot(LOADOUT_POSITION_PRIMARY)

		if classIndices_Internal[owner.m_iClass] ~= "Sniper" then
			return
		end

        if not primary:GetAttributeValue("throwable healing") then
            return
        end

		local shouldPenetrate = primary:GetAttributeValue("projectile penetration")

        -- print("explosive arrow fired")

		local collided = false
		local touchedEnts = {}

        arrow:AddCallback(ON_TOUCH, function(_, other)
			if touchedEnts[other:GetHandleIndex()] then
				return
			end

            if not other:IsPlayer() and not other:IsNPC() and other.m_iClassname ~= "entity_medigun_shield" then
                return
            end

			if not shouldPenetrate then
				collided = true
			end

			touchedEnts[other:GetHandleIndex()] = true

            -- print("collided with attachable entity")
			local origin = arrow:GetAbsOrigin()
			spawnArrowTip(owner, origin, other)
        end)

        arrow:AddCallback(ON_REMOVE, function()
			if collided then
				return
			end

            -- print("collided with surface")
			local origin = arrow:GetAbsOrigin()
			spawnArrowTip(owner, origin)
        end)
    end)
end)

-- wormhole Diver

-- disable collision with func_respawnroomvisualizer
for _, visualizer in pairs(ents.FindAllByClass("func_respawnroomvisualizer")) do
	visualizer:AddCallback(ON_SHOULD_COLLIDE, function(_, other)
		if other.IsWormHole then
			other:Remove()
		end
	end)
end

function WormholeDiverShot(_, projectile)
	if not projectile then
		return
	end
	projectile.IsWormHole = true
end

local diverCallbacks = {}
function WormholeDiverEquip(_, activator)
	-- fix weird quirk with template being spawned after you switch to a different class
	if classIndices_Internal[activator:DumpProperties().m_iClass] ~= "Pyro" then
		return
	end

	local handle = activator:GetHandleIndex()

	diverCallbacks[handle] = {}
	local playerCallbacks = diverCallbacks[handle]

	-- on key press
	playerCallbacks.keyPress = activator:AddCallback(ON_KEY_PRESSED, function(_, key)
		if key ~= IN_ATTACK2 then
			return
		end

		if activator.m_hActiveWeapon ~= activator:GetPlayerItemBySlot(LOADOUT_POSITION_SECONDARY) then
			return
		end

		for _, flare in pairs(ents.FindAllByClass("tf_projectile_flare")) do
			if flare.m_hOwnerEntity == activator then
				local flareOrigin = flare:GetAbsOrigin()
				local activatorOrigin = activator:GetAbsOrigin()

				-- too short, don't bother
				-- prevents clipping
				if flareOrigin:Distance(activatorOrigin) <= 120 then
					break
				end

				local DefaultTraceInfo = {
					start = activator:GetAbsOrigin(),
					endpos = flareOrigin,
					mask = (CONTENTS_PLAYERCLIP),
					collisiongroup = COLLISION_GROUP_PLAYER,
					-- mins = Vector(-150, -150, -0),
					-- maxs = Vector(150, 150, 0),
				}

				-- trace for player clip
				local trace = util.Trace(DefaultTraceInfo)

				if not trace.Entity then
					activator:SetAbsOrigin(flareOrigin)
				end
				-- activator:SetAbsOrigin(trace.HitPos or flareOrigin)
				flare:Remove()

				break
			end
		end
	end)
end

function WormholeDiverUnequip(_, activator)
	for _, id in pairs(diverCallbacks[activator:GetHandleIndex()]) do
		activator:RemoveCallback(id)
	end
end