local BOT_SET_SKILL_EASY = "activator.SetDifficulty(0);" local BOT_SET_SKILL_NORMAL = "activator.SetDifficulty(1);" local BOT_SET_SKILL_HARD = "activator.SetDifficulty(2);" local BOT_SET_SKILL_Expert = "activator.SetDifficulty(3);" local function getEyeAngles(player) local pitch = player["m_angEyeAngles[0]"] local yaw = player["m_angEyeAngles[1]"] return Vector(pitch, yaw, 0) end local function getPlayerByNetIndex(netIndex) local allPlayers = ents.GetAllPlayers() for _, player in pairs(allPlayers) do if netIndex == player:GetNetIndex() then return player end end end local function getPlayerByHandleIndex(handle) local allPlayers = ents.GetAllPlayers() for _, player in pairs(allPlayers) do if handle == player:GetHandleIndex() then return player end end end local CRITBOOST_CONDS = { 11, 33, 34, 37, 38, 39, 40, 44, 56, 105 } local function isCritboosted(player) for _, index in pairs(CRITBOOST_CONDS) do if player:InCond(index) then return true end end return false end local function ghostShieldStart(player) local playerSecondary = player:GetPlayerItemBySlot(1) if not playerSecondary then return end if playerSecondary:GetItemName() == "The Spectral Screen" then playerSecondary:SetAttributeValue("not solid to players", 1) player:AddCond(66) return true end end local function ghostShieldEnd(player) local playerSecondary = player:GetPlayerItemBySlot(1) if not playerSecondary then return end if playerSecondary:GetItemName() == "The Spectral Screen" then playerSecondary:SetAttributeValue("not solid to players", 0) player:RemoveCond(66) player:AddCond(66, 2, player) local playerLocation = player:GetAbsOrigin() local playerTeamnum = player.m_iTeamNum local sufferingTable = { Attacker = player, -- Attacker Inflictor = nil, -- Direct cause of damage, usually a projectile Weapon = playerSecondary, Damage = 9000, DamageType = DMG_MELEE, -- Damage type, see DMG_* globals. Can be combined with | operator DamageCustom = TF_DMG_CUSTOM_CHARGE_IMPACT, -- Custom damage type, see TF_DMG_* globals CritType = 2, -- Crit type, 0 = no crit, 1 = mini crit, 2 = normal crit DamagePosition = nil, -- Where the target was hit at DamageForce = nil, -- Knockback force of the attack ReportedPosition = playerLocation -- Where the attacker attacked from } local stuckRadius = 100 * player.m_flModelScale for _, enemy in pairs(ents.GetAllPlayers()) do if enemy.m_iTeamNum ~= playerTeamnum and enemy:IsAlive() and enemy:GetAbsOrigin():Distance(playerLocation) <= stuckRadius and enemy.m_bIsMiniBoss == 0 then enemy:TakeDamage(sufferingTable) timer.Simple(0.01, function() if enemy:IsAlive() then enemy:Suicide() end end) elseif enemy.m_iTeamNum ~= playerTeamnum and enemy:IsAlive() and enemy:GetAbsOrigin():Distance(playerLocation) <= stuckRadius and enemy.m_bIsMiniBoss == 1 then player:Suicide() end end for _, building in pairs(ents.FindAllByClass("obj_*")) do if building.m_iTeamNum ~= playerTeamnum and building:GetAbsOrigin():Distance(playerLocation) <= stuckRadius then building:TakeDamage(sufferingTable) end end return true end end --These are expected to be called externally, by other lua scripts function SCROOBUTIL_RegisterChargeStartHandler(handler) table.insert(chargeStartHandlers,handler) end function SCROOBUTIL_RegisterChargeEndHandler(handler) table.insert(chargeEndHandlers,handler) end --tables those two functions interact with local chargeStartHandlers = { ghostShieldStart, } local chargeEndHandlers = { ghostShieldEnd, } local chargingPlayers = {} --fire input on effect doesn't work on shield charges, this is painful alternative to that, might hook bot gimmicks into this as well. local chargeDetectorLoop = timer.Create(0.015, function() local rebuiltChargingTable = {} --search for new charging demos for _, player in pairs(ents.GetAllPlayers()) do if player:InCond(17) == true then local alreadyInTable = false --verify they aren't already in the table for _, charger in pairs(chargingPlayers) do if player == charger then alreadyInTable = true end end if alreadyInTable == false then --Break out of the handler matching loop early if we find a valid start handler local handlerFound = false for _, handler in pairs(chargeStartHandlers) do handlerFound = handler(player) if handlerFound == true then break end end table.insert(rebuiltChargingTable, player) end end end --review current players for _, player in pairs(chargingPlayers) do if player:InCond(17) == false then --Break out of the handler matching loop early if we find a valid end handler local handlerFound = false for _, handler in pairs(chargeEndHandlers) do handlerFound = handler(player) if handlerFound == true then break end end else table.insert(rebuiltChargingTable, player) end end chargingPlayers = {} for _, player in pairs(rebuiltChargingTable) do table.insert(chargingPlayers, player) end --PrintTable(chargingPlayers) end, 0) local function getAllNonPlayerDamageableEntities() local nextbotCombatCharacterTable = {} for _, entity in pairs (ents.FindAllByClass("eyeball_boss")) do table.insert(nextbotCombatCharacterTable, entity) end for _, entity in pairs (ents.FindAllByClass("headless_hatman")) do table.insert(nextbotCombatCharacterTable, entity) end for _, entity in pairs (ents.FindAllByClass("merasmus")) do table.insert(nextbotCombatCharacterTable, entity) end for _, entity in pairs (ents.FindAllByClass("tank_boss")) do table.insert(nextbotCombatCharacterTable, entity) end for _, entity in pairs (ents.FindAllByClass("tf_zombie")) do table.insert(nextbotCombatCharacterTable, entity) end for _, entity in pairs (ents.FindAllByClass("obj_*")) do table.insert(nextbotCombatCharacterTable, entity) end return nextbotCombatCharacterTable end local function skeletonResist(parent, damageinfo) if parent.m_iTeamNum ~= 2 then return true end damageinfo.Damage = (damageinfo.Damage / 3) return true end local function spawnSkeletons(owner, angleIncrement, skeletonOrigin, skeletonCount) local pivotedAngle = 0 local skeletonTable = { TeamNum = owner.m_iTeamNum, origin = tostring(skeletonOrigin), angles = tostring(Vector(0, 0, 0)), } if skeletonCount > 1 then for i=0,(skeletonCount - 1),1 do local skellington = ents.CreateWithKeys("tf_projectile_spellspawnzombie", skeletonTable, true, true) skellington.m_hThrower = owner skellington:SetAbsAngles(Vector(0, pivotedAngle, 0)) skellington:SetForwardVelocity(400) pivotedAngle = pivotedAngle + angleIncrement end else local skellington = ents.CreateWithKeys("tf_projectile_spellspawnzombie", skeletonTable, true, true) skellington.m_hThrower = owner end end ents.AddCreateCallback("tf_zombie", function(skellington) skellington:AddCallback(ON_DAMAGE_RECEIVED_PRE, function(_, damageinfo) return skeletonResist(skellington, damageinfo) end) end) -- This code was originally mince's, it has been changed for my purposes and to make it function function OnHealthkitTouch(entity, other, hitPos, hitNormal) -- Only allow players if not IsValid(other) or not other:IsPlayer() then return end local owner = entity.m_hOwnerEntity; if not IsValid(owner) or other == owner or owner:IsPlayer() == false then return end local food = owner:GetPlayerItemBySlot(1) if not food then return end if food:GetItemName("The Dalokohs Bar") and food:GetAttributeValue("dmg pierces resists absorbs", true) == 1 then other:AddCond(26, 6, owner) end entity:Remove() end -- Called when a healthkit spawns function OnHealthkitSpawned(entity) entity:AddCallback(ON_TOUCH, OnHealthkitTouch) end ents.AddCreateCallback("item_healthkit_*", OnHealthkitSpawned) --mince code ends here AddEventCallback("post_inventory_application", function(eventTable) local playerNetID = eventTable.userid local activator = ents.GetPlayerByUserId(playerNetID) if activator.m_iClass == 9 and activator:IsRealPlayer() == true then local disposableSentryTotal = 0 local disposableSentryLimit = activator:GetAttributeValueByClass("engy_disposable_sentries", 0) local buildingTable = ents.FindAllByClass("obj_*") for _, entity in pairs(buildingTable) do if entity.m_hBuilder == activator then local entityRemainingHealthMultiplier = (entity.m_iHealth / entity.m_iMaxHealth) --object type is placed in a variable because it will be accessed multiple times local buildingObjectType = entity.m_iObjectType local buildingBaseHealth = 216 --if the activator has the gunslinger mini sentry attribute, or you're a disposable building, and you're a sentry, your base health is 100 if (activator:GetAttributeValue("mod wrench builds minisentry", true) or entity.m_bDisposableBuilding == 1) and buildingObjectType == 2 then buildingBaseHealth = 100 if entity.m_bDisposableBuilding == 1 then disposableSentryTotal = disposableSentryTotal + 1 end elseif entity.m_iUpgradeLevel == 1 then buildingBaseHealth = 150 elseif entity.m_iUpgradeLevel == 2 then buildingBaseHealth = 180 end --if none of the above checks pass, we will assume that we are a level 3 building local buildingMaxHealth = activator:GetAttributeValueByClass("mult_engy_building_health", 1) * buildingBaseHealth local damageCalibration = -1 local sufferingTable = { Attacker = nil, -- Attacker Inflictor = nil, -- Direct cause of damage, usually a projectile Weapon = nil, Damage = damageCalibration, DamageType = nil, -- Damage type, see DMG_* globals. Can be combined with | operator DamageCustom = nil, -- Custom damage type, see TF_DMG_* globals CritType = 0, -- Crit type, 0 = no crit, 1 = mini crit, 2 = normal crit DamagePosition = nil, -- Where the target was hit at DamageForce = nil, -- Knockback force of the attack ReportedPosition = nil -- Where the attacker attacked from } entity.m_iMaxHealth = buildingMaxHealth entity.m_iHealth = buildingMaxHealth * entityRemainingHealthMultiplier entity:TakeDamage(sufferingTable) if disposableSentryTotal > disposableSentryLimit and entity.m_bDisposableBuilding == 1 then damageCalibration = 9000 entity:TakeDamage(sufferingTable) end end end end end) function necrolanderCancelHeadBuffs(damage, activator, caller) local headStorage = activator.m_iDecapitations if headStorage > 10 then headStorage = 4 activator.m_iDecapitations = 10 elseif headStorage > 4 then headStorage = 4 end local pivotedAngle = 0 local skellingtonOrigin = (caller:GetAbsOrigin() + Vector(0,0,20)) local skeletonTable = { TeamNum = activator.m_iTeamNum, Owner = activator, origin = tostring(skellingtonOrigin), angles = tostring(Vector(0, 0, 0)), } if caller.m_bIsMiniBoss == 1 then spawnSkeletons(activator, 120, skellingtonOrigin, 3) activator.m_iDecapitations = activator.m_iDecapitations + 2 if activator.m_iDecapitations > 10 then activator.m_iDecapitations = 10 end elseif caller.m_bIsMiniBoss ~= 1 and caller.m_flModelScale > 1 then spawnSkeletons(activator, 0, skellingtonOrigin, 1) activator.m_iDecapitations = activator.m_iDecapitations + 1 if activator.m_iDecapitations > 10 then activator.m_iDecapitations = 10 end end --0.079207920792 is the derived per-head speed buff in attribute form, taken by dividing demo's base speed --by his speed increase with one extra head, then subtracting that value from 1 activator:SetAttributeValue("move speed penalty", (1 - (0.079207920792 * headStorage))) activator:SetAttributeValue("hidden maxhealth non buffed", (-15 * headStorage)) activator:SetAttributeValue("cannot giftwrap", activator.m_iDecapitations) end function ammoOnKillMedium(damage, activator, caller) local callerOrigin = caller:GetAbsOrigin() local ammoPack = ents.CreateWithKeys("item_ammopack_medium",{ ["origin"] = callerOrigin[1] .. " " .. callerOrigin[2] .. " " .. (callerOrigin[3] + 10), }) timer.Simple(10, function() if ammoPack:IsValid() then ammoPack:Remove() end end) end local function simulateMeleeKill(assister, victim) local assisterMelee = assister:GetPlayerItemBySlot(2) local zatoHeal = assisterMelee:GetAttributeValueByClass("restore_health_on_kill", 0) * (175 + assister:GetAttributeValueByClass("add_maxhealth_nonbuffed", 0) + assister:GetAttributeValueByClass("add_maxhealth", 0)) local healOnKill = assisterMelee:GetAttributeValueByClass("heal_on_kill", 0) + zatoHeal local critsOnKill = assisterMelee:GetAttributeValueByClass("add_onkill_critboost_time", 0) local refillCharge = assister:GetAttributeValueByClass("kill_refills_meter", 0) if assisterMelee:GetAttributeValue("decapitate type", true) == 1 then healOnKill = healOnKill + 15 assister.m_iDecapitations = assister.m_iDecapitations + 1 end if assisterMelee:GetItemName() == "The Necrolander" then necrolanderCancelHeadBuffs(0, assister, victim) end local caberStorageAttribute = assisterMelee:GetAttributeValue("tool needs giftwrap", true) if assisterMelee:GetItemName() == "The Ullapool Caber" and caberStorageAttribute ~= nil then if victim.m_bIsMiniBoss == 1 then assisterMelee:SetAttributeValue("tool needs giftwrap", 5) else assisterMelee:SetAttributeValue("tool needs giftwrap", caberStorageAttribute + 1) end if assisterMelee:GetAttributeValue("tool needs giftwrap", true) >= 5 and assisterMelee.m_iDetonated == 1 then timer.Simple(0.1, function() assister:PlaySoundToSelf("items/pumpkin_pickup.wav") assister:PlaySoundToSelf("items/spawn_item.wav") assisterMelee.m_iDetonated = 0 end) end end if assisterMelee:GetAttributeValue("fire input on kill", true) == "popscript^$ammoOnKillMedium^" then ammoOnKillMedium(0, assister, victim) end assister.m_flChargeMeter = assister.m_flChargeMeter + (refillCharge * 100) if assister.m_flChargeMeter > 100 then assister.m_flChargeMeter = 100 end assister.m_iHealth = assister.m_iHealth + healOnKill if assister.m_iHealth > (175 + (assister:GetAttributeValueByClass("add_maxhealth_nonbuffed", 0) + assister:GetAttributeValueByClass("add_maxhealth", 0)) * 1.5) then assister.m_iHealth = (175 + (assister:GetAttributeValueByClass("add_maxhealth_nonbuffed", 0) + assister:GetAttributeValueByClass("add_maxhealth", 0)) * 1.5) end assister:AddCond(33, critsOnKill) end AddEventCallback("arrow_impact", function(eventTable) local shooter = getPlayerByNetIndex(eventTable.shooter) local target = getPlayerByNetIndex(eventTable.attachedEntity) if target == nil or target:IsPlayer() == false or target.m_iTeamNum ~= shooter.m_iTeamNum or shooter.m_iClass ~= 5 then return end local primary = shooter:GetPlayerItemBySlot(0) if primary:GetAttributeValue("cannot giftwrap", true) == 1 then target:AddCond(29, 5, shooter) end end) AddEventCallback("player_used_powerup_bottle", function(eventTable) local allPlayers = ents.GetAllPlayers() local playerNetID = eventTable.player local activator = getPlayerByNetIndex(playerNetID) --Type 1: crit --Type 2: uber --Type 3: recall --Type 4: ammo --Type 5: building upgrade if eventTable.type == 3 and activator:GetPlayerItemBySlot(2):GetItemName() == "The Necrolander" then activator:SetAttributeValue("move speed penalty", 1) activator:SetAttributeValue("hidden maxhealth non buffed", 0) activator:SetAttributeValue("cannot giftwrap", 0) end end) AddEventCallback("player_hurt", function(eventTable) local attacker = ents.GetPlayerByUserId(eventTable.attacker) if not attacker then return end local victim = ents.GetPlayerByUserId(eventTable.userid) local primary = attacker:GetPlayerItemBySlot(0) if not primary then return end if eventTable.bonuseffect == 2.0 and primary:GetAttributeValue("tool needs giftwrap", true) == 1 then local sufferingDamage = 180 * primary:GetAttributeValueByClass("mult_dmg", 1) timer.Simple(0.05, function() if victim:IsAlive() == true then local sufferingTable = { Attacker = attacker, -- Attacker Inflictor = nil, -- Direct cause of damage, usually a projectile Weapon = nil, Damage = sufferingDamage, DamageType = nil, -- Damage type, see DMG_* globals. Can be combined with | operator DamageCustom = nil, -- Custom damage type, see TF_DMG_* globals CritType = 2, -- Crit type, 0 = no crit, 1 = mini crit, 2 = normal crit DamagePosition = nil, -- Where the target was hit at DamageForce = nil, -- Knockback force of the attack ReportedPosition = nil -- Where the attacker attacked from } victim:TakeDamage(sufferingTable) end end) return end if primary:GetItemName() == "The Metal Popper" and attacker:InCond(12) == false and attacker:GetAttributeValue("is miniboss", true) == nil then --fuck the hype meter's native charge, thing is almost impossible to work with --1500 damage to charge attacker.m_flRageMeter = attacker.m_flRageMeter + (0.1 * eventTable.damageamount) if attacker.m_flRageMeter > 100 then attacker.m_flRageMeter = 100 end attacker.m_flHypeMeter = attacker.m_flRageMeter return end --if you are wondering what eventTable.weaponid is, it is this https://github.com/ValveSoftware/source-sdk-2013/blob/11a677c/src/game/shared/tf/tf_shareddefs.h#L405-L524 --A giant enum that contains a bunch of weapons referenced by classname. The flamethrower is number 25, it is checked because there was a bug --that caused minicrit afterburn and detonator combos to hit like a nuclear furnace due to a false positive with this check. --This check is now relatively airtight because minicrits become crits works correctly on normal flames, so it should only be possible --for the broken case of reflected rockets to trip it. local minicritsBecomeCrits = primary:GetAttributeValue("minicrits become crits", true) if attacker.m_iClass == 7 and minicritsBecomeCrits ~= nil and minicritsBecomeCrits == 1 and eventTable.minicrit == 1 and eventTable.weaponid == 25 then local sufferingDamage = 149 * primary:GetAttributeValueByClass("mult_dmg", 1) timer.Simple(0.05, function() if victim:IsAlive() == true then local sufferingTable = { Attacker = attacker, -- Attacker Inflictor = nil, -- Direct cause of damage, usually a projectile Weapon = nil, Damage = sufferingDamage, DamageType = nil, -- Damage type, see DMG_* globals. Can be combined with | operator DamageCustom = nil, -- Custom damage type, see TF_DMG_* globals CritType = 2, -- Crit type, 0 = no crit, 1 = mini crit, 2 = normal crit DamagePosition = nil, -- Where the target was hit at DamageForce = nil, -- Knockback force of the attack ReportedPosition = nil -- Where the attacker attacked from } victim:TakeDamage(sufferingTable) end end) return end if not victim then return end local victimMelee = victim:GetPlayerItemBySlot(2) if not victimMelee then return end if victim:InCond(51) and (victim.m_iClass == 1 or victim.m_iClass == 7) and victimMelee:GetAttributeValue("tool needs giftwrap", true) ~= nil then victim:AddCond(52, 0.2) victim:AddCond(16, 1) victim:RemoveCond(51) victim:RemoveCond(15) victim:PlaySoundToSelf("items/pumpkin_pickup.wav") --find something better at some point. local minibossMult = 1 if attacker.m_bIsMiniBoss == 1 then minibossMult = 5 end local sufferingTable = { Attacker = victim, -- Attacker Inflictor = nil, -- Direct cause of damage, usually a projectile Weapon = nil, Damage = (50 * minibossMult), DamageType = DMG_GENERIC, -- Damage type, see DMG_* globals. Can be combined with | operator DamageCustom = TF_DMG_CUSTOM_PUMPKIN_BOMB, -- Custom damage type, see TF_DMG_* globals CritType = 0, -- Crit type, 0 = no crit, 1 = mini crit, 2 = normal crit DamagePosition = victim:GetAbsOrigin(), -- Where the target was hit at DamageForce = nil, -- Knockback force of the attack ReportedPosition = victim:GetAbsOrigin() -- Where the attacker attacked from } attacker:TakeDamage(sufferingTable) victim:AddCond(32, 5) if victim.m_iClass == 1 then victim:SetAttributeValue("halloween fire rate bonus", 0.5) victim:SetAttributeValue("halloween reload time decreased", 0.5) timer.Simple(5, function() victim:SetAttributeValue("halloween fire rate bonus", 1) victim:SetAttributeValue("halloween reload time decreased", 1) end) else victim:SetAttributeValue("CARD: damage bonus", 2) timer.Simple(5, function() victim:SetAttributeValue("CARD: damage bonus", 1) end) end end end) AddEventCallback("player_death", function(eventTable) local victim = ents.GetPlayerByUserId(eventTable.userid) local victimMelee = victim:GetPlayerItemBySlot(2) if victimMelee:GetItemName() == "The Necrolander" then --0.079207920792 is the derived per-head speed buff in attribute form, taken by dividing demo's base speed --by his speed increase with one extra head, then subtracting that value from 1 local headsRemaining = victim:GetAttributeValue("cannot giftwrap", false) local healthCorrection = victim:GetAttributeValue("hidden maxhealth non buffed", false) if healthCorrection == nil then healthCorrection = 0 end local reanimator --we wait a bit after the death occurs to ensure the reanimator spawned. timer.Simple(0.1, function() if victim:IsAlive() == false then if headsRemaining == nil then headsRemaining = 0 end local reanimatorTable = ents.FindAllByClass("entity_revive_marker") for _, entity in pairs(reanimatorTable) do if entity.m_hOwner == victim then reanimator = entity entity.m_iHealth = (50 * headsRemaining) entity.m_iMaxHealth = entity.m_iMaxHealth + 14 + (healthCorrection * 0.5) end end if reanimator.m_iHealth >= reanimator.m_iMaxHealth then victim:Print(PRINT_TARGET_CENTER, "MET HEAD REQUIREMENT: (" .. math.tointeger(headsRemaining) .. "/" .. math.ceil(reanimator.m_iMaxHealth / 50) .. "), YOU'LL BE BACK") else victim:Print(PRINT_TARGET_CENTER, "FAILED TO MEET HEAD REQUIREMENT: (" .. math.tointeger(headsRemaining) .. "/" .. math.ceil(reanimator.m_iMaxHealth / 50) .. ")") end end end) timer.Simple(2, function() if victim:IsAlive() == false and IsValid(reanimator) then if reanimator.m_iHealth >= reanimator.m_iMaxHealth then victim:SetAbsOrigin(reanimator:GetAbsOrigin()) victim:SwitchClassInPlace(4) local victimLocation = victim:GetAbsOrigin() local sufferingTable = { Attacker = victim, -- Attacker Inflictor = nil, -- Direct cause of damage, usually a projectile Weapon = victimMelee, Damage = 9000, DamageType = DMG_MELEE, -- Damage type, see DMG_* globals. Can be combined with | operator DamageCustom = TF_DMG_CUSTOM_PUMPKIN_BOMB, -- Custom damage type, see TF_DMG_* globals CritType = 2, -- Crit type, 0 = no crit, 1 = mini crit, 2 = normal crit DamagePosition = nil, -- Where the target was hit at DamageForce = nil, -- Knockback force of the attack ReportedPosition = victim:GetAbsOrigin() -- Where the attacker attacked from } local stuckRadius = 50 * victim.m_flModelScale for _, player in pairs(ents.GetAllPlayers()) do if player.m_iTeamNum ~= 2 and player:IsAlive() and player:GetAbsOrigin():Distance(victimLocation) <= stuckRadius and player.m_bIsMiniBoss == 0 then player:TakeDamage(sufferingTable) timer.Simple(0.01, function() if player:IsAlive() then player:Suicide() end end) elseif player.m_iTeamNum ~= 2 and player:IsAlive() and player:GetAbsOrigin():Distance(victimLocation) <= stuckRadius and player.m_bIsMiniBoss == 1 then victim:Suicide() end end local fakeEventTable = { entindex = victim, } --scoreboard credit, also increments revives counter like when a med does it. FireEvent("revive_player_complete", fakeEventTable) victim:PlaySoundToSelf("misc/halloween/spell_skeleton_horde_cast.wav") victim:PlaySoundToSelf("mvm/mvm_revive.wav") victim:AddCond(66, 0.1) victim:AddCond(57, 1) victim:AddCond(26, 3) end end end) victim:SetAttributeValue("move speed penalty", 1) victim:SetAttributeValue("hidden maxhealth non buffed", 0) victim:SetAttributeValue("cannot giftwrap", 0) end if victimMelee:GetItemName() == "The Equalizer" and victimMelee:GetAttributeValue("tool needs giftwrap", true) then local victimOrigin = victim:GetAbsOrigin() local sufferingTable = { Attacker = victim, -- Attacker Inflictor = nil, -- Direct cause of damage, usually a projectile Weapon = nil, Damage = 200, DamageType = DMG_BLAST, -- Damage type, see DMG_* globals. Can be combined with | operator DamageCustom = TF_DMG_CUSTOM_PUMPKIN_BOMB, -- Custom damage type, see TF_DMG_* globals CritType = 2, -- Crit type, 0 = no crit, 1 = mini crit, 2 = normal crit DamagePosition = victimOrigin, -- Where the target was hit at DamageForce = nil, -- Knockback force of the attack ReportedPosition = victimOrigin -- Where the attacker attacked from } local ExplodeSound = ents.CreateWithKeys("ambient_generic",{ ["health"] = "10", ["message"] = "Cart.Explode", ["pitch"] = "85", ["pitchstart"] = "85", ["radius"] = "750", ["spawnflags"] = "48", ["origin"] = victimOrigin[1] .. " " .. victimOrigin[2] .. " " .. victimOrigin[3], }) local Explode = ents.CreateWithKeys("info_particle_system",{ ["start_active"] = "0", ["flag_as_weather"] = "0", ["effect_name"] = "hightower_explosion", ["origin"] = victimOrigin[1] .. " " .. victimOrigin[2] .. " " .. victimOrigin[3], ["angles"] = Vector(0,0,0), }) ExplodeSound:AcceptInput("PlaySound") Explode:AcceptInput("Start") local blastRadius = 400 for _, player in pairs(ents.GetAllPlayers()) do if player.m_iTeamNum ~= victim.m_iTeamNum and player:IsAlive() and (player:GetAbsOrigin()):Distance(victimOrigin) <= blastRadius then player:TakeDamage(sufferingTable) end end for _, entity in pairs(getAllNonPlayerDamageableEntities()) do if entity.m_iTeamNum ~= victim.m_iTeamNum and (entity:GetAbsOrigin()):Distance(victimOrigin) <= blastRadius then entity:TakeDamage(sufferingTable) end end timer.Simple(1, function() Explode:AcceptInput("Stop") Explode:Remove() ExplodeSound:Remove() end) end local attacker = ents.GetPlayerByUserId(eventTable.attacker) if not attacker then return end local attackerPrimary = attacker:GetPlayerItemBySlot(0) if attackerPrimary and attackerPrimary:IsValid() then if attackerPrimary:GetItemName() == "The Overload" and eventTable.weapon ~= "tf_projectile_pipe" and attacker:IsAlive() == true then local ammoToReload = 0 local clipBonusMult = attackerPrimary:GetAttributeValueByClass("mult_clipsize", 1) local clipBonusAtomic = attackerPrimary:GetAttributeValueByClass("mult_clipsize_upgrade_atomic", 0) local maxClip = math.ceil(((4 * clipBonusMult) + clipBonusAtomic) * 1.5) --do nothing if we're full, used to prevent sound cue spam if attackerPrimary.m_iClip1 >= maxClip then return end if victim.m_bIsMiniBoss == 1 then ammoToReload = 4 elseif victim.m_flModelScale > 1 then ammoToReload = 2 else ammoToReload = 1 end attackerPrimary.m_iClip1 = attackerPrimary.m_iClip1 + ammoToReload --it would be more expensive to check if we're at max, then only play the sound, instead of just --setting it to max regardless of whether or not it is over the max clip if attackerPrimary.m_iClip1 >= maxClip then attackerPrimary.m_iClip1 = maxClip attacker:PlaySoundToSelf("items/pumpkin_pickup.wav") attacker:PlaySoundToSelf("items/spawn_item.wav") end end end local attackerMelee = attacker:GetPlayerItemBySlot(2) if not attackerMelee then return end local attackerWeaponName = eventTable.weapon if not (attackerWeaponName) then return end if (attackerWeaponName == "tide_turner" or attackerWeaponName == "demoshield" or attackerWeaponName == "splendid_screen") and attacker:GetPlayerItemBySlot(1):GetAttributeValue("tool needs giftwrap", true) == 1 then simulateMeleeKill(attacker, victim) end local attackerMeleeArbitraryStorageAttribute = attackerMelee:GetAttributeValue("tool needs giftwrap", true) if attackerMeleeArbitraryStorageAttribute == nil then return end if attackerMelee:GetItemName() == "The Ullapool Caber" and attackerMeleeArbitraryStorageAttribute >= 1 then if victim.m_bIsMiniBoss == 1 then attackerMelee:SetAttributeValue("tool needs giftwrap", 5) else attackerMelee:SetAttributeValue("tool needs giftwrap", attackerMeleeArbitraryStorageAttribute + 1) end if attackerMelee:GetAttributeValue("tool needs giftwrap", true) >= 5 and attackerMelee.m_iDetonated == 1 then timer.Simple(0.1, function() attacker:PlaySoundToSelf("items/pumpkin_pickup.wav") attacker:PlaySoundToSelf("items/spawn_item.wav") attackerMelee.m_iDetonated = 0 end) end end local assister = ents.GetPlayerByUserId(eventTable.assister) if not assister then return end local assisterSecondary = assister:GetPlayerItemBySlot(1) if not assisterSecondary then return end if assisterSecondary.m_iClassname == "tf_wearable_demoshield" and assisterSecondary:GetAttributeValue("tool needs giftwrap", true) == 1 then simulateMeleeKill(assister, victim) end end) --Hack to get around fire input on effect not working on shield charges --Used rejuvenator_hit_logic as a base function launcherReloadOnKill(damage, activator, caller) if caller.m_iTeamNum == activator.m_iTeamNum then return end local upgradeLevel = nil --Look through weapon slots to see if anyone has the vitasaw attribute, then use that value as the upgrade level if activator:GetPlayerItemBySlot(0):GetAttributeValue("preserve ubercharge", true) ~= nil then upgradeLevel = activator:GetPlayerItemBySlot(0):GetAttributeValue("preserve ubercharge", true) elseif activator:GetPlayerItemBySlot(1):GetAttributeValue("preserve ubercharge", true) ~= nil then upgradeLevel = activator:GetPlayerItemBySlot(1):GetAttributeValue("preserve ubercharge", true) elseif activator:GetPlayerItemBySlot(2):GetAttributeValue("preserve ubercharge", true) ~= nil then upgradeLevel = activator:GetPlayerItemBySlot(2):GetAttributeValue("preserve ubercharge", true) end --if we have no assigned level, give up if upgradeLevel == nil then return end local BASE_CLIP = 4 local primary = activator:GetPlayerItemBySlot(0) --in case of pyro airblasts or other owner changing events --why the fuck are the airstrike, mangler, and direct hit different classes? --I can kind of understand the loose cannon having its own class, but those motherfuckers all work basically the same, except maybe the mangler --BUT NOOOOO I'M NOT ALLOWED TO HAVE READABLE IF STATEMENTS! --if primary.m_iClassname ~= "tf_weapon_grenadelauncher" and primary.m_iClassname ~= "tf_weapon_rocketlauncher" and primary.m_iClassName ~= "tf_weapon_rocketlauncher_airstrike" and primary.m_iClassName ~= "tf_weapon_rocketlauncher_directhit" and primary.m_iClassName ~= "tf_weapon_cannon" and primary.m_iClassName ~= "tf_weapon_particle_cannon" then -- return --end --fuck this class spaghetti, we're going to assume that no flamethrowers will ever have the uber retaining upgrade on them, so they'll get filtered by the upgrade level check instead if primary:GetItemName() ~= "The Cow Mangler 5000" then local clipBonusMult = primary:GetAttributeValueByClass("mult_clipsize", 1) local clipBonusAtomic = primary:GetAttributeValueByClass("mult_clipsize_upgrade_atomic", 0) local airstrikeKills = 0 local overloadRefundAmount = 0 if primary:GetItemName() == "The Air Strike" then airstrikeKills = activator.m_Shared.m_iDecapitations + 1 end local maxClip = (BASE_CLIP * clipBonusMult) + clipBonusAtomic + airstrikeKills if primary.m_iClip1 >= maxClip then return end if activator:IsRealPlayer() == true then activator.m_iAmmo[TF_AMMO_PRIMARY] = activator.m_iAmmo[TF_AMMO_PRIMARY] - upgradeLevel if activator.m_iAmmo[TF_AMMO_PRIMARY] < 0 then --if this call is tripped primary ammo will be negative upgradeLevel = upgradeLevel + activator.m_iAmmo[TF_AMMO_PRIMARY] activator.m_iAmmo[TF_AMMO_PRIMARY] = 0 end end primary.m_iClip1 = primary.m_iClip1 + upgradeLevel if primary.m_iClip1 > maxClip then overloadRefundAmount = primary.m_iClip1 - maxClip activator.m_iAmmo[TF_AMMO_PRIMARY] = activator.m_iAmmo[TF_AMMO_PRIMARY] + overloadRefundAmount primary.m_iClip1 = maxClip end else local clipBonusMult = primary:GetAttributeValueByClass("mult_clipsize_upgrade", 1) local maxClip = (BASE_CLIP * 5) * clipBonusMult primary.m_flEnergy = primary.m_flEnergy + (upgradeLevel * 5) if primary.m_flEnergy > maxClip then primary.m_flEnergy = maxClip end end end function targeMarkAndBatts(damage, activator, caller) caller:AddCond(30, 10, activator) activator:AddCond(26, 5, activator) end function shieldAddcondOnHitFix(damage, activator, caller) local shield = activator:GetPlayerItemBySlot(1) local selfCond = shield:GetAttributeValue("self add cond on hit", true) local enemyCond = shield:GetAttributeValue("add cond on hit", true) local enemyCondDuration = shield:GetAttributeValue("add cond on hit duration", true) local selfCondDuration = shield:GetAttributeValue("self add cond on hit duration", true) local healOnHit = shield:GetAttributeValueByClass("add_onhit_addhealth", 0) activator.m_iHealth = activator.m_iHealth + healOnHit if selfCond and selfCondDuration then activator:AddCond(selfCond, selfCondDuration, activator) end if enemyCond and enemyCondDuration then caller:AddCond(enemyCond, enemyCondDuration, activator) end end --Used rejuvenator_hit_logic as a base function chargeBackOnKill(damage, activator, caller) local upgradeLevel = (activator:GetPlayerItemBySlot(0):GetAttributeValue("mult airblast refire time")) - 1 local primary = activator:GetPlayerItemBySlot(0) activator.m_flChargeMeter = activator.m_flChargeMeter + 20 * upgradeLevel if activator.m_flChargeMeter > 100 then activator.m_flChargeMeter = 100 end end --Used rejuvenator_hit_logic as a base function chargeBackOnHit(damage, activator, caller) if caller.m_bIsMiniBoss ~= 0 then return end upgradeLevel = activator:GetPlayerItemBySlot(1):GetAttributeValue("throwable detonation time") or 1 activator:RemoveCond(17) activator:AddCond(17, 2.5, activator) activator.m_flChargeMeter = 20 * upgradeLevel end --Used rejuvenator_hit_logic as a base function pomsonEMPhit(damage, activator, caller) local realMaxHealth = caller.m_iMaxHealth + caller:GetAttributeValueByClass("add_maxhealth_nonbuffed", 0) + caller:GetAttributeValueByClass("add_maxhealth", 0) if caller.m_iHealth > realMaxHealth * 0.5 or caller:IsRealPlayer() == true or caller:IsPlayer() == false then return end if caller.m_iHealth > realMaxHealth * 0.15 and caller.m_bIsMiniBoss == 1 then return end if realMaxHealth >= 6000 then return end local upgradeLevel = (activator:GetPlayerItemBySlot(0):GetAttributeValue("disguise speed penalty")) or 0.25 caller:AddCond(71, (upgradeLevel * 2), activator) end --Used rejuvenator_hit_logic as a base function launcherReloadOnKillSecondary(damage, activator, caller) if caller.m_iTeamNum == activator.m_iTeamNum then return end local upgradeLevel = nil --Look through weapon slots to see if anyone has the vitasaw attribute, then use that value as the upgrade level if activator:GetPlayerItemBySlot(1):GetAttributeValue("saxxy award category", true) ~= nil then upgradeLevel = activator:GetPlayerItemBySlot(1):GetAttributeValue("saxxy award category", true) elseif activator:GetPlayerItemBySlot(1):GetAttributeValue("saxxy award category", true) ~= nil then upgradeLevel = activator:GetPlayerItemBySlot(1):GetAttributeValue("saxxy award category", true) elseif activator:GetPlayerItemBySlot(2):GetAttributeValue("saxxy award category", true) ~= nil then upgradeLevel = activator:GetPlayerItemBySlot(2):GetAttributeValue("saxxy award category", true) end --if we have no assigned level, give up if upgradeLevel == nil then return end local BASE_CLIP = 8 local secondary = activator:GetPlayerItemBySlot(1) --in case of pyro airblasts or other owner changing events --why the fuck are the airstrike, mangler, and direct hit different classes? --I can kind of understand the loose cannon having its own class, but those motherfuckers all work basically the same, except maybe the mangler --BUT NOOOOO I'M NOT ALLOWED TO HAVE READABLE IF STATEMENTS! --if secondary.m_iClassname ~= "tf_weapon_grenadelauncher" and secondary.m_iClassname ~= "tf_weapon_rocketlauncher" and secondary.m_iClassName ~= "tf_weapon_rocketlauncher_airstrike" and secondary.m_iClassName ~= "tf_weapon_rocketlauncher_directhit" and secondary.m_iClassName ~= "tf_weapon_cannon" and secondary.m_iClassName ~= "tf_weapon_particle_cannon" then -- return --end --fuck this class spaghetti, we're going to assume that no flamethrowers will ever have the uber retaining upgrade on them, so they'll get filtered by the upgrade level check instead local clipBonusMult = secondary:GetAttributeValueByClass("mult_clipsize", 1) local clipBonusAtomic = secondary:GetAttributeValueByClass("mult_clipsize_upgrade_atomic", 0) local overloadRefundAmount = 0 local maxClip = (BASE_CLIP * clipBonusMult) + clipBonusAtomic if activator:IsRealPlayer() == true then activator.m_iAmmo[TF_AMMO_secondary] = activator.m_iAmmo[TF_AMMO_secondary] - upgradeLevel if activator.m_iAmmo[TF_AMMO_secondary] < 0 then --if this call is tripped secondary ammo will be negative upgradeLevel = upgradeLevel + activator.m_iAmmo[TF_AMMO_secondary] activator.m_iAmmo[TF_AMMO_secondary] = 0 end end secondary.m_iClip1 = secondary.m_iClip1 + upgradeLevel if secondary.m_iClip1 > maxClip then overloadRefundAmount = secondary.m_iClip1 - maxClip activator.m_iAmmo[TF_AMMO_secondary] = activator.m_iAmmo[TF_AMMO_secondary] + overloadRefundAmount secondary.m_iClip1 = maxClip end end function scoutMeleeParryOnAttack(projectile, activator, melee) local parryWindow = melee:GetAttributeValue("tool needs giftwrap", true) activator.m_iHealth = activator.m_iHealth - 50 if activator.m_iHealth <= 0 then activator:Suicide() return end activator:StunPlayer(5, 0.5, TF_STUNFLAG_SLOWDOWN, activator) activator:AddCond(51, parryWindow, activator) end function demoCaberClearStorageOnHit(activator, projectile, self) if self.m_iDetonated ~= 0 then --self:SetAttributeValue("tool needs giftwrap", 5) return end local caberHolder = self.m_hOwner if not util.IsLagCompensationActive() then util.StartLagCompensation(caberHolder) end --ripped from my med hunter script, hybrid of red sniper laser check and some original work on my end local TraceLineOfSight = { start = caberHolder, -- Start position vector. Can also be set to entity, in this case the trace will start from entity eyes position distance = 65, -- Used if endpos is nil angles = getEyeAngles(caberHolder), -- Used if endpos is nil 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 } local lineOfSightTraceTable = util.Trace(TraceLineOfSight) local hitEntity = lineOfSightTraceTable.Entity if hitEntity ~= nil then self:SetAttributeValue("tool needs giftwrap", 1) end util.FinishLagCompensation(activator) end function unfuckBisonGLProjSpeed(activator, projectile, self) local projSpeedBonus = (self:GetAttributeValueByClass("mult_projectile_speed", 1)) * 10 local currentVelocity = activator.m_vecAbsVelocity local editedVelocity = Vector(currentVelocity[1] * projSpeedBonus,currentVelocity[2] * projSpeedBonus,0) activator:AddOutput("BaseVelocity " .. editedVelocity[1] .. " " .. editedVelocity[2] .. " " .. 0) activator:SetForwardVelocity(1200 * projSpeedBonus) end function applyBrainrot(victim, duration, damagePenalty) --this is to stop more than one jammer banner from permanently lobotomizing a bot if victim:GetAttributeValue("grenades3_resupply_denied", true) ~= nil and victim:GetAttributeValue("grenades3_resupply_denied", true) >= 1 then return end local preBrainrotBotSkill = victim.m_nBotSkill local preBrainrotDamagePenalty = victim:GetAttributeValue("damage penalty", false) if preBrainrotDamagePenalty == nil then preBrainrotDamagePenalty = 1 end victim.m_nBotSkill = 0 victim:RunScriptCode(BOT_SET_SKILL_EASY, victim, victim) victim:SetAttributeValue("grenades3_resupply_denied", 1) victim:SetAttributeValue("damage penalty", (preBrainrotDamagePenalty - damagePenalty)) local painParticle = ents.CreateWithKeys("info_particle_system", { effect_name = "medic_resist_bullet", start_active = 1, flag_as_weather = 0, }, true, true) painParticle:SetAbsOrigin(victim:GetAbsOrigin()) painParticle:Start() painParticle:AcceptInput("SetParent", "!activator", victim, nil) timer.Simple(duration, function() painParticle:Remove() --if our skill was changed by something else, back off and don't fuck them up. if victim.m_nBotSkill == 0 then victim.m_nBotSkill = preBrainrotBotSkill --If only lua had switch statements if preBrainrotBotSkill == 1 then victim:RunScriptCode(BOT_SET_SKILL_NORMAL, victim, victim) elseif preBrainrotBotSkill == 2 then victim:RunScriptCode(BOT_SET_SKILL_HARD, victim, victim) elseif preBrainrotBotSkill == 3 then victim:RunScriptCode(BOT_SET_SKILL_EXPERT, victim, victim) end end victim:SetAttributeValue("damage penalty", preBrainrotDamagePenalty) victim:SetAttributeValue("grenades3_resupply_denied", 0) end) end --The stun on hit rafmod attribute hasn't been working well, this is a substitute for it function fakeRocketSpecStun(damage, activator, caller) if not caller:IsPlayer() then return end caller:StunPlayer(1, 1, TF_STUNFLAG_SLOWDOWN, activator) end function knifeRocketSpecStunOnBackstab(damage, activator, caller) --this is not a good way to detect backstabs, but it will have to do. local spyLookDirection = activator["m_angEyeAngles[1]"] local callerLookDirection = caller["m_angEyeAngles[1]"] --if the target is not facing you, trigger the effect. Should be identical to actual backstab hit detection (which is why it is bad). if (callerLookDirection - spyLookDirection) < 90 or callerLookDirection - spyLookDirection > 270 then fakeRocketSpecStun(daamge, activator, caller) end end function soldierEarrapeBannerActivateBrainrot(condition, caller, activator) --stored here for optimization purposes, doing up to 22 gets within a 0.2 second tick would be quite bad local ourTeamNum = activator.m_iTeamNum --the liberty launcher's support upgrade exists, and going forward I might have more buff range increases in the future --The +0.95 is to cancel the penalty on the banner itself when calculating this local sufferingRadius = 450 if activator:GetAttributeValue("mod soldier buff range", true) ~= nil then sufferingRadius = 450 * (activator:GetAttributeValue("mod soldier buff range", true) + 0.95) end local playerLocation = activator:GetAbsOrigin() --SetModelScale for _, player in pairs(ents.GetAllPlayers()) do if player.m_iTeamNum ~= ourTeamNum and player:IsAlive() and player:GetAbsOrigin():Distance(playerLocation) <= sufferingRadius then applyBrainrot(player,1,0.25) end end end function redTapeBrainrot(condition, caller, activator) local sapperUpgradeLevel = caller:GetAttributeValueByClass("robo_sapper", 0) local upgradeDurationIncrease = 0 if sapperUpgradeLevel == 2 then upgradeDurationIncrease = 1.5 elseif sapperUpgradeLevel >= 3 then upgradeDurationIncrease = 3 end --base duration is 4, but there's a 1 second reactivation time after the sapper breaks, which is accounted for here. local duration = 5 + upgradeDurationIncrease applyBrainrot(activator,duration,0.25) end function blastKnife(damage, activator, caller) --this is not a good way to detect backstabs, but it will have to do. local spyLookDirection = activator["m_angEyeAngles[1]"] local callerLookDirection = caller["m_angEyeAngles[1]"] --if the target is not facing you, trigger the effect. Should be identical to actual backstab hit detection (which is why it is bad). if (callerLookDirection - spyLookDirection) < 90 or callerLookDirection - spyLookDirection > 270 then local callerOrigin = caller:GetAbsOrigin() local melee = activator:GetPlayerItemBySlot(2) local damageBonusMult = melee:GetAttributeValueByClass("armor_piercing", 1) local mimicDamage = 2 * damageBonusMult local Explode = ents.CreateWithKeys("info_particle_system",{ ["start_active"] = "0", ["flag_as_weather"] = "0", ["effect_name"] = "hightower_explosion", ["origin"] = callerOrigin[1] .. " " .. callerOrigin[2] .. " " .. callerOrigin[3], ["angles"] = Vector(0,0,0), }) --ejection force, throws player out of bot and into the sky activator:AddOutput("BaseVelocity 0 0 700") activator:SetForwardVelocity("-1000") Explode:AcceptInput("Start") local sufferingTableSelf = { Attacker = activator, -- Attacker Inflictor = nil, -- Direct cause of damage, usually a projectile Weapon = nil, Damage = 50, DamageType = DMG_BLAST, -- Damage type, see DMG_* globals. Can be combined with | operator DamageCustom = TF_DMG_CUSTOM_PUMPKIN_BOMB, -- Custom damage type, see TF_DMG_* globals CritType = 2, -- Crit type, 0 = no crit, 1 = mini crit, 2 = normal crit DamagePosition = callerOrigin, -- Where the target was hit at DamageForce = nil, -- Knockback force of the attack ReportedPosition = activator:GetAbsOrigin() -- Where the attacker attacked from } activator:TakeDamage(sufferingTableSelf) local sufferingTable = { Attacker = activator, -- Attacker Inflictor = nil, -- Direct cause of damage, usually a projectile Weapon = nil, Damage = mimicDamage, DamageType = DMG_BLAST, -- Damage type, see DMG_* globals. Can be combined with | operator DamageCustom = TF_DMG_CUSTOM_PUMPKIN_BOMB, -- Custom damage type, see TF_DMG_* globals CritType = 2, -- Crit type, 0 = no crit, 1 = mini crit, 2 = normal crit DamagePosition = callerOrigin, -- Where the target was hit at DamageForce = nil, -- Knockback force of the attack ReportedPosition = activator:GetAbsOrigin() -- Where the attacker attacked from } local blastRadius = 300 * (1 + (melee:GetAttributeValueByClass("armor_piercing", 1) / 100)) for _, player in pairs(ents.GetAllPlayers()) do if player.m_iTeamNum ~= activator.m_iTeamNum and player:IsAlive() and (player:GetAbsOrigin()):Distance(callerOrigin) <= blastRadius then player:TakeDamage(sufferingTable) end end for _, entity in pairs(getAllNonPlayerDamageableEntities()) do if entity.m_iTeamNum ~= activator.m_iTeamNum and (entity:GetAbsOrigin()):Distance(callerOrigin) <= blastRadius then entity:TakeDamage(sufferingTable) end end timer.Simple(1, function() Explode:AcceptInput("Stop") Explode:Remove() end) end end function brainrotOnHit(damage, activator, caller) applyBrainrot(caller,1.0,0.25) end function demoSelfLauncher(projectile, activator, weapon) projectile:SetFakeParent(activator) activator:AddOutput("BaseVelocity 0 0 205") activator:SetForwardVelocity(805) end function demoShieldSuicideBomb(damage, activator, caller) activatorOrigin = activator:GetAbsOrigin() bombOrigin = "" .. activatorOrigin[1] .. " " .. activatorOrigin[2] .. " " .. activatorOrigin[3] .. "" local ExplodeSound = ents.CreateWithKeys("ambient_generic",{ ["health"] = "10", ["message"] = "Cart.Explode", ["pitch"] = "85", ["pitchstart"] = "85", ["radius"] = "750", ["spawnflags"] = "48", ["origin"] = bombOrigin, }) local ExplodeEffect = ents.CreateWithKeys("info_particle_system",{ ["start_active"] = "0", ["flag_as_weather"] = "0", ["effect_name"] = "hightower_explosion", ["origin"] = bombOrigin, }) ExplodeEffect:AcceptInput("Start") ExplodeSound:AcceptInput("PlaySound") explosionDamage = 85 * activator:GetPlayerItemBySlot(2):GetAttributeValueByClass("mult_dmg", 1) local function inflictDamage(player, damage) local victimDamage = damage --This means the victim will never be killed by the explosion, they will instead be stunned for 10 seconds. if victimDamage > player.m_iHealth then victimDamage = victimDamage - (player.m_iHealth + 1) player:StunPlayer(10, 1, TF_STUNFLAG_BONKSTUCK, activator) end local shield = activator:GetPlayerItemBySlot(1) local sufferingTable = { Attacker = activator, -- Attacker Inflictor = nil, -- Direct cause of damage, usually a projectile Weapon = nil, Damage = victimDamage, DamageType = DMG_BLAST, -- Damage type, see DMG_* globals. Can be combined with | operator DamageCustom = TF_DMG_CUSTOM_PUMPKIN_BOMB, -- Custom damage type, see TF_DMG_* globals CritType = 0, -- Crit type, 0 = no crit, 1 = mini crit, 2 = normal crit DamagePosition = nil, -- Where the target was hit at DamageForce = nil, -- Knockback force of the attack ReportedPosition = nil -- Where the attacker attacked from } player:TakeDamage(sufferingTable) end inflictDamage(caller, (explosionDamage - (damage + 1))) for _, player in pairs(ents.GetAllPlayers()) do if player.m_iTeamNum ~= activator.m_iTeamNum and player:IsAlive() and (player:GetAbsOrigin()):Distance(activatorOrigin) <= 400 and player ~= caller then inflictDamage(player, explosionDamage) end end local sufferingTableNonPlayer = { Attacker = activator, -- Attacker Inflictor = nil, -- Direct cause of damage, usually a projectile Weapon = nil, Damage = explosionDamage, DamageType = DMG_BLAST, -- Damage type, see DMG_* globals. Can be combined with | operator DamageCustom = TF_DMG_CUSTOM_PUMPKIN_BOMB, -- Custom damage type, see TF_DMG_* globals CritType = 0, -- Crit type, 0 = no crit, 1 = mini crit, 2 = normal crit DamagePosition = nil, -- Where the target was hit at DamageForce = nil, -- Knockback force of the attack ReportedPosition = nil -- Where the attacker attacked from } for _, entity in pairs(getAllNonPlayerDamageableEntities()) do if entity.m_iTeamNum ~= activator.m_iTeamNum and (entity:GetAbsOrigin()):Distance(activatorOrigin) <= 400 then entity:TakeDamage(sufferingTableNonPlayer) end end timer.Simple(0.25, function() ExplodeEffect:AcceptInput("Stop") ExplodeEffect:Remove() ExplodeSound:Remove() end) end local SCOUT_MECH_giantCharacterAttributes = { ["is miniboss"] = 1, ["not solid to players"] = 1, ["SET BONUS: special dsp"] = 38, --made an entire function to stop this from erasing speed boost upgrades ["SET BONUS: move speed set bonus"] = 0.75, ["damage force reduction"] = 0.4, ["airblast vulnerability multiplier"] = 0.4, ["override footstep sound set"] = 5, ["model scale"] = 1.75, ["damage bonus"] = 1.5, ["max health additive bonus"] = 875, ["effect bar recharge rate increased"] = 0.5, ["no revive"] = 1, ["health from healers reduced"] = 0.5, ["voice pitch scale"] = 0.6, ["ammo regen"] = 30, } local function applyAttributesOverExisting(target, newAttributes) local checkItemDefinition = true if target:IsPlayer() == true then checkItemDefinition = false end for name, value in pairs(newAttributes) do --detonates if an attribute takes a string arg, just don't do that if target:GetAttributeValue(name, checkItemDefinition) ~= nil then target:SetAttributeValue(name, (value + target:GetAttributeValue(name, checkItemDefinition))) else target:SetAttributeValue(name, value) end end end function engineerShotgunMaintainOnKill(damage, activator, caller) --local sentryTable = ents.FindAllByClass("obj_sentrygun") --PrintTable(sentryTable) --local dispenserTable = ents.FindAllByClass("obj_dispenser") --PrintTable(dispenserTable) --local teleporterTable = ents.FindAllByClass("obj_teleporter") --PrintTable(teleporterTable) local buildingTable = ents.FindAllByClass("obj_*") --for _, entity in pairs (sentryTable) do -- table.insert(buildingTable, entity) --end --for _, entity in pairs (dispenserTable) do -- table.insert(buildingTable, entity) --end --for _, entity in pairs (teleporterTable) do -- table.insert(buildingTable, entity) --end --PrintTable(buildingTable) for _, entity in pairs(buildingTable) do if entity.m_hBuilder == activator then --object type is placed in a variable because it will be accessed multiple times local buildingObjectType = entity.m_iObjectType local buildingBaseHealth = 216 --if the activator has the gunslinger mini sentry attribute, or you're a disposable building, and you're a sentry, your base health is 100 if (activator:GetAttributeValue("mod wrench builds minisentry", true) or entity.m_bDisposableBuilding == 1) and buildingObjectType == 2 then buildingBaseHealth = 100 elseif entity.m_iUpgradeLevel == 1 then buildingBaseHealth = 150 elseif entity.m_iUpgradeLevel == 2 then buildingBaseHealth = 180 end --if none of the above checks pass, we will assume that we are a level 3 building local buildingMaxHealth = activator:GetAttributeValueByClass("mult_engy_building_health", 1) * buildingBaseHealth if entity.m_bBuilding == 1 then entity.m_flPercentageConstructed = entity.m_flPercentageConstructed + 0.3 entity.m_iHealth = entity.m_iHealth + buildingMaxHealth * 0.3 if entity.m_flPercentageConstructed >= 1 then entity.m_bBuilding = 0 end else entity.m_iHealth = entity.m_iHealth + buildingMaxHealth * 0.1 end if entity.m_iHealth > buildingMaxHealth then entity.m_iHealth = buildingMaxHealth end if buildingObjectType == 2 then if entity.m_iUpgradeLevel ~= 1 then --level 2 and 3 sentries have 200 bullet ammo in them while level 1s and minis have 150 entity.m_iAmmoShells = entity.m_iAmmoShells + 20 --level 2s don't shoot rockets, but these calculations do nothing bad by changing their rockets netprop --before they fire them, so I don't care. entity.m_iAmmoRockets = entity.m_iAmmoRockets + 2 if entity.m_iAmmoShells > 200 then entity.m_iAmmoShells = 200 end if entity.m_iAmmoRockets > 20 then entity.m_iAmmoRockets = 20 end else entity.m_iAmmoShells = entity.m_iAmmoShells + 15 if entity.m_iAmmoShells > 150 then entity.m_iAmmoShells = 150 end end end end end end --custom weapon functions, past scripts will probably be merged here function scoutMechPrimaryCall(condition, caller, activator) if activator:GetAttributeValue("is miniboss", false) == 1 then return end local activatorOrigin = activator:GetAbsOrigin() local activatorAttributes activator:GetAllAttributeValues(false) activator.m_flRageMeter = 0 --turn player into mech applyAttributesOverExisting(activator, SCOUT_MECH_giantCharacterAttributes) local teleParticle = ents.CreateWithKeys("info_particle_system", { effect_name = "teleportedin_red", start_active = 1, flag_as_weather = 0, }, true, true) teleParticle:SetAbsOrigin(activator:GetAbsOrigin()) teleParticle:Start() local teleParticle2 = ents.CreateWithKeys("info_particle_system", { effect_name = "wrenchmotron_teleport_beam", start_active = 1, flag_as_weather = 0, }, true, true) teleParticle2:SetAbsOrigin(activator:GetAbsOrigin()) teleParticle2:Start() timer.Simple(1, function() teleParticle:Remove() teleParticle2:Remove() end) local function SCOUT_MECH_playerEjectAndReset(activator, activatorAttributes) activator:SetCustomModelWithClassAnimations("models/player/scout.mdl") activator:AddCond(52, 2, activator) --this is an incredibly messy way to do this, but I don't think the loss in performance is significant enough for me to care --This nils out all giant attributes, then reapplies the player's upgrade attributes for name, value in pairs(SCOUT_MECH_giantCharacterAttributes) do activator:SetAttributeValue(name, nil) end if activatorAttributes then for name, value in pairs(activatorAttributes) do activator:SetAttributeValue(name, value) end end if activator.m_iHealth < 125 then activator:AddHealth((125 - activator.m_iHealth)) end if not activator:IsAlive() then activator:SetAttributeValue("not solid to players", 0) return end --fixes a bug where you can eject in a giant's asshole and instakill them activator:SetAttributeValue("not solid to players", 1) activatorOrigin = activator:GetAbsOrigin() dummyOrigin = "" .. activatorOrigin[1] .. " " .. activatorOrigin[2] .. " " .. activatorOrigin[3] .. "" dummyAngles = "0 " .. activator["m_angEyeAngles[1]"] .. " 0" local giantBodyDummy = ents.CreateWithKeys("prop_dynamic",{ ["disableshadows"] = "1", ["model"] = "models/bots/scout_boss/bot_scout_boss.mdl", ["DefaultAnim"] = "PRIMARY_stun_middle", ["origin"] = dummyOrigin, ["angles"] = dummyAngles, ["modelscale"] = "1.75", }) local giantBodyDummyExplodeSound = ents.CreateWithKeys("ambient_generic",{ ["health"] = "10", ["message"] = "Cart.Explode", ["pitch"] = "85", ["pitchstart"] = "85", ["radius"] = "750", ["spawnflags"] = "48", ["origin"] = dummyOrigin, ["angles"] = dummyAngles, }) local giantBodyDummyExplode = ents.CreateWithKeys("info_particle_system",{ ["start_active"] = "0", ["flag_as_weather"] = "0", ["effect_name"] = "hightower_explosion", ["origin"] = dummyOrigin, ["angles"] = dummyAngles, }) --ejection force, throws player out of bot and into the sky activator:SetAbsOrigin(Vector(activatorOrigin[1], activatorOrigin[2], activatorOrigin[3] + 24)) activator:AddOutput("BaseVelocity 0 0 700") timer.Simple(0.75, function() giantBodyDummyExplode:AcceptInput("Start") giantBodyDummyExplodeSound:AcceptInput("PlaySound") local sufferingTable = { Attacker = activator, -- Attacker Inflictor = nil, -- Direct cause of damage, usually a projectile Weapon = nil, Damage = 150 * activator:GetPlayerItemBySlot(0):GetAttributeValueByClass("mult_dmg", 1), DamageType = DMG_BLAST, -- Damage type, see DMG_* globals. Can be combined with | operator DamageCustom = TF_DMG_CUSTOM_PUMPKIN_BOMB, -- Custom damage type, see TF_DMG_* globals CritType = 2, -- Crit type, 0 = no crit, 1 = mini crit, 2 = normal crit DamagePosition = nil, -- Where the target was hit at DamageForce = nil, -- Knockback force of the attack ReportedPosition = nil -- Where the attacker attacked from } for _, player in pairs(ents.GetAllPlayers()) do if player.m_iTeamNum ~= activator.m_iTeamNum and player:IsAlive() and (player:GetAbsOrigin()):Distance(activatorOrigin) <= 300 then if player:TakeDamage(sufferingTable) ~= 0 then activator.m_flRageMeter = 0 activator.m_flHypeMeter = activator.m_flRageMeter end end end for _, entity in pairs(getAllNonPlayerDamageableEntities()) do if entity.m_iTeamNum ~= activator.m_iTeamNum and (entity:GetAbsOrigin()):Distance(activatorOrigin) <= 300 then if entity:TakeDamage(sufferingTable) ~= 0 then activator.m_flRageMeter = 0 activator.m_flHypeMeter = activator.m_flRageMeter end end end giantBodyDummy:Remove() activator:SetAttributeValue("not solid to players", 0) timer.Simple(1, function() giantBodyDummyExplode:AcceptInput("Stop") giantBodyDummyExplode:Remove() giantBodyDummyExplodeSound:Remove() activator:SetAttributeValue("not solid to players", 0) end) end) end activator:SetCustomModelWithClassAnimations("models/bots/scout_boss/bot_scout_boss.mdl") if activator.m_iHealth < 1000 then activator:AddHealth((1000 - activator.m_iHealth)) end local timeLeft = 100 activator:PlaySoundToSelf("=50|metal_popper_giant_mode_active_1.mp3") local logicLoop logicLoop = timer.Create(0.2, function() timeLeft = timeLeft - 2 activator.m_flHypeMeter = timeLeft if activator:IsAlive() == false or timeLeft <= 0 or (activator:InCond(7) and activator.m_hActiveWeapon.m_iClassname ~= "tf_weapon_lunchbox_drink") then timer.Stop(logicLoop) SCOUT_MECH_playerEjectAndReset(activator, activatorAttributes) return end if timeLeft == 80 then activator:PlaySoundToSelf("=40|metal_popper_giant_mode_active_2.mp3") elseif timeLeft == 60 then activator:PlaySoundToSelf("=40|metal_popper_giant_mode_active_3.mp3") elseif timeLeft == 40 then activator:PlaySoundToSelf("=40|metal_popper_giant_mode_active_4.mp3") elseif timeLeft == 26 then activator:PlaySoundToSelf("=50|metal_popper_giant_mode_active_5.mp3") end end, 50) end function pyroHealthKitOnKill(damage, activator, caller) local callerOrigin = caller:GetAbsOrigin() local ammoPack = ents.CreateWithKeys("item_healthkit_medium",{ ["origin"] = callerOrigin[1] .. " " .. callerOrigin[2] .. " " .. (callerOrigin[3] + 10), }) timer.Simple(10, function() if ammoPack:IsValid() then ammoPack:Remove() end end) end function scoutAmmoOnKill(damage, activator, caller) local callerOrigin = caller:GetAbsOrigin() local ammoPack = ents.CreateWithKeys("item_ammopack_small",{ ["origin"] = callerOrigin[1] .. " " .. callerOrigin[2] .. " " .. (callerOrigin[3] + 10), }) timer.Simple(10, function() if ammoPack:IsValid() then ammoPack:Remove() end end) end function dalokasEat(tauntIndex, activator) if tauntIndex == "scenes/player/heavy/low/taunt04.vcd" then activator:AddCond(26, 12, activator) end end function bananaEat(tauntIndex, activator) local realMaxHealth = activator.m_iMaxHealth + activator:GetAttributeValueByClass("add_maxhealth_nonbuffed", 0) + activator:GetAttributeValueByClass("add_maxhealth", 0) if tauntIndex == "scenes/player/heavy/low/taunt04.vcd" then timer.Simple(0.8, function() activator:RemoveCond(7) activator.m_iHealth = activator.m_iHealth + 200 if activator.m_iHealth > (realMaxHealth * 1.5) then activator.m_iHealth = realMaxHealth end end) end end function eurekaInstaTeleport(tauntIndex, activator) if tauntIndex == "scenes/player/engineer/low/taunt_drg_melee.vcd" then activator:AddCond(52, 2.7) end end function necrolanderTaunt(tauntIndex, activator) if tauntIndex == "scenes/player/demoman/low/taunt09.vcd" and activator.m_iDecapitations >= 4 then activator.m_iDecapitations = activator.m_iDecapitations - 4 activator:AddCond(57, 5) activator:AddCond(56, 5) local melee = activator:GetPlayerItemBySlot(2) local pivotedAngle = 0 local skellingtonOrigin = (activator:GetAbsOrigin() + Vector(0,0,20)) spawnSkeletons(activator, 60, skellingtonOrigin, 6) local headStorage = activator.m_iDecapitations if headStorage > 10 then headStorage = 4 activator.m_iDecapitations = 10 elseif headStorage > 4 then headStorage = 4 end --0.079207920792 is the derived per-head speed buff in attribute form, taken by dividing demo's base speed --by his speed increase with one extra head, then subtracting that value from 1. activator:SetAttributeValue("move speed penalty", (1 - (0.079207920792 * headStorage))) activator:SetAttributeValue("hidden maxhealth non buffed", (-15 * headStorage)) activator:SetAttributeValue("cannot giftwrap", activator.m_iDecapitations) end end function heaterExplodeOnKill(damage, activator, caller) local sufferingTable = { Attacker = activator, -- Attacker Inflictor = nil, -- Direct cause of damage, usually a projectile Weapon = activator:GetPlayerItemBySlot(0), Damage = 20, DamageType = DMG_BURN, -- Damage type, see DMG_* globals. Can be combined with | operator DamageCustom = nil, -- Custom damage type, see TF_DMG_* globals CritType = 2, -- Crit type, 0 = no crit, 1 = mini crit, 2 = normal crit DamagePosition = nil, -- Where the target was hit at DamageForce = nil, -- Knockback force of the attack ReportedPosition = nil -- Where the attacker attacked from } local activatorOrigin = activator:GetAbsOrigin() for _, player in pairs(ents.GetAllPlayers()) do if player.m_iTeamNum ~= activator.m_iTeamNum and player:IsAlive() and (player:GetAbsOrigin()):Distance(activatorOrigin) <= 200 then player:TakeDamage(sufferingTable) player:IgnitePlayerDuration(10, activator) end end for _, entity in pairs(getAllNonPlayerDamageableEntities()) do if entity.m_iTeamNum ~= activator.m_iTeamNum and (entity:GetAbsOrigin()):Distance(activatorOrigin) <= 200 then entity:TakeDamage(sufferingTable) end end end function steakGasExplosion(tauntIndex, activator) if tauntIndex == "scenes/player/heavy/low/taunt04.vcd" then activatorTeamNum = activator.m_iTeamNum activatorOrigin = activator:GetAbsOrigin() local blastRadius = 400 --we don't check for non player damageable entities because they cannot be afflicted with gas for _, player in pairs(ents.GetAllPlayers()) do if player.m_iTeamNum ~= activatorTeamNum and player:IsAlive() and (player:GetAbsOrigin()):Distance(activatorOrigin) <= blastRadius then player:AddCond(123, 22) end end end end function manmelterExtinguish(damage, activator, caller) if not caller:IsPlayer() then return end --don't do anything to teammates --checked before the afterburn check as a failsafe in case it actually lights a teammate if activator.m_iTeamNum == caller.m_iTeamNum then if caller:InCond(22) then caller:AddHealth(damage, false) caller:RemoveCond(22) activator.m_iRevengeCrits = activator.m_iRevengeCrits + 1 activator:GetPlayerItemBySlot(1):SetAttributeValue("add cond when active", 11) --refund crits used to extinguish teammates if damage >= 90 then activator.m_iRevengeCrits = activator.m_iRevengeCrits + 1 end end return end --to stop afterburn from auto extinguishing or making them burn forever if not caller:InCond(22) then caller:IgnitePlayerDuration(7.5, activator) return end --implicitly calls incond caller:RemoveCond(22) activator.m_iRevengeCrits = activator.m_iRevengeCrits + 1 activator:GetPlayerItemBySlot(1):SetAttributeValue("add cond when active", 11) end function manmelterUnfuckCrit(activator, projectile, self) if projectile.m_iRevengeCrits > 1 then activator.m_bCritical = 1 elseif projectile.m_iRevengeCrits == 1 then activator.m_bCritical = 1 --just in case self:SetAttributeValue("add cond when active", nil) else self:SetAttributeValue("add cond when active", nil) end end function axtinguisherExplosion(damage, activator, caller) if not caller:InCond(22) then return end --implicitly calls incond caller:RemoveCond(22) local callerOrigin = caller:GetAbsOrigin() for _, player in pairs(ents.GetAllPlayers()) do if player.m_iTeamNum ~= activator.m_iTeamNum and player:IsAlive() and (player:GetAbsOrigin()):Distance(callerOrigin) <= 300 then local sufferingDamage = 65 if player:InCond(22) then sufferingDamage = 123 --abstraction, because we don't know how long they've been burning, so we assume max damage. end sufferingDamage = sufferingDamage * activator:GetPlayerItemBySlot(2):GetAttributeValueByClass("mult_dmg", 1) timer.Simple(0.01, function() if player:IsAlive() == true then local sufferingTable = { Attacker = activator, -- Attacker Inflictor = nil, -- Direct cause of damage, usually a projectile Weapon = activator:GetPlayerItemBySlot(2), Damage = sufferingDamage, DamageType = DMG_MELEE, -- Damage type, see DMG_* globals. Can be combined with | operator DamageCustom = nil, -- Custom damage type, see TF_DMG_* globals CritType = 2, -- Crit type, 0 = no crit, 1 = mini crit, 2 = normal crit DamagePosition = nil, -- Where the target was hit at DamageForce = nil, -- Knockback force of the attack ReportedPosition = nil -- Where the attacker attacked from } if player:InCond(22) and sufferingDamage >= player.m_iHealth then player:RemoveCond(22) activator:AddCond(32, 5) end player:TakeDamage(sufferingTable) end end) end end for _, entity in pairs(getAllNonPlayerDamageableEntities()) do local sufferingTable = { Attacker = activator, -- Attacker Inflictor = nil, -- Direct cause of damage, usually a projectile Weapon = activator:GetPlayerItemBySlot(2), Damage = 65 * activator:GetPlayerItemBySlot(2):GetAttributeValueByClass("mult_dmg", 1), --these entities can never be on fire, so it will always be 65 * the damage bonus DamageType = DMG_MELEE, -- Damage type, see DMG_* globals. Can be combined with | operator DamageCustom = nil, -- Custom damage type, see TF_DMG_* globals CritType = 2, -- Crit type, 0 = no crit, 1 = mini crit, 2 = normal crit DamagePosition = nil, -- Where the target was hit at DamageForce = nil, -- Knockback force of the attack ReportedPosition = nil -- Where the attacker attacked from } if entity.m_iTeamNum ~= activator.m_iTeamNum and (entity:GetAbsOrigin()):Distance(callerOrigin) <= 300 then entity:TakeDamage(sufferingTable) end end end --thank you royal local function removeCallbacks(player, callbacks) if not IsValid(player) then return end for _, callbackId in pairs(callbacks) do player:RemoveCallback(callbackId) end end function checkIfMeleeHitAllyCiv(param, activator, calller) if not util.IsLagCompensationActive() then util.StartLagCompensation(activator) end --ripped from my med hunter script, hybrid of red sniper laser check and some original work on my end local TraceLineOfSight = { start = activator, -- Start position vector. Can also be set to entity, in this case the trace will start from entity eyes position distance = 100, -- Used if endpos is nil angles = getEyeAngles(activator), -- Used if endpos is nil 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 } local lineOfSightTraceTable = util.Trace(TraceLineOfSight) local hitEntity = lineOfSightTraceTable.Entity if IsValid(hitEntity) and hitEntity:IsPlayer() and hitEntity.m_iTeamNum == activator.m_iTeamNum then local healedMaxHealth = hitEntity.m_iMaxHealth + hitEntity:GetAttributeValueByClass("add_maxhealth_nonbuffed", 0) + hitEntity:GetAttributeValueByClass("add_maxhealth", 0) hitEntity.m_iHealth = hitEntity.m_iHealth + (100 * activator:GetAttributeValueByClass("mult_repair_value", 1)) hitEntity:TakeDamage({ -- make the bot take 1 damage so the sentry healthbar updates. Damage = 1, Attacker = activator, Weapon = none, }) hitEntity:TakeDamage({ -- make the bot take -1 damage to nullify effect of earlier damage, could just add 1 to heal value, but I won't Damage = -1, Attacker = activator, Weapon = none, }) if hitEntity.m_iHealth < healedMaxHealth then activator:PlaySoundToSelf("Weapon_Wrench.HitBuilding_Success") hitEntity:PlaySoundToSelf("Weapon_Wrench.HitBuilding_Success") else activator:PlaySoundToSelf("Weapon_Wrench.HitBuilding_Failure") hitEntity:PlaySoundToSelf("Weapon_Wrench.HitBuilding_Failure") end if hitEntity.m_iHealth > healedMaxHealth then hitEntity.m_iHealth = healedMaxHealth end end util.FinishLagCompensation(activator) end function checkIfMeleeHitGauntletEmbodiment(param, activator, calller) if not util.IsLagCompensationActive() then util.StartLagCompensation(activator) end --ripped from my med hunter script, hybrid of red sniper laser check and some original work on my end local melee = activator:GetPlayerItemBySlot(2) local meleeDamageBonus if not melee:GetAttributeValue("CARD: damage bonus", true) then meleeDamageBonus = 1 else meleeDamageBonus = melee:GetAttributeValue("CARD: damage bonus", true) end melee:SetAttributeValue("CARD: damage bonus", 1) --miss penalty, is overwritten by either ally hit forgiveness or enemy hit bonus. Or isn't overwritten if neither of those are triggered melee:SetAttributeValue("fire rate bonus", 1) local TraceLineOfSight = { start = activator, -- Start position vector. Can also be set to entity, in this case the trace will start from entity eyes position distance = 100, -- Used if endpos is nil angles = getEyeAngles(activator), -- Used if endpos is nil 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 } local lineOfSightTraceTable = util.Trace(TraceLineOfSight) local hitEntity = lineOfSightTraceTable.Entity if IsValid(hitEntity) and (hitEntity:IsPlayer() or hitEntity:IsObject() or hitEntity:IsNPC()) and hitEntity.m_iTeamNum ~= activator.m_iTeamNum then melee:SetAttributeValue("CARD: damage bonus", (meleeDamageBonus + 0.05)) meleeDamageBonus = meleeDamageBonus + 0.05 if meleeDamageBonus > 2 then melee:SetAttributeValue("CARD: damage bonus", 2) end --if we hit a teammate, forgive the player elseif IsValid(hitEntity) and hitEntity.m_iTeamNum == activator.m_iTeamNum then melee:SetAttributeValue("CARD: damage bonus", meleeDamageBonus) --if we miss, we reduce our bonus slightly else melee:SetAttributeValue("CARD: damage bonus", (meleeDamageBonus - 0.1)) meleeDamageBonus = meleeDamageBonus - 0.1 end if meleeDamageBonus < 1 then meleeDamageBonus = 1 melee:SetAttributeValue("CARD: damage bonus", 1) end if meleeDamageBonus ~= 1 then melee:SetAttributeValue("fire rate bonus", 1 - (meleeDamageBonus * 0.25)) end util.FinishLagCompensation(activator) end function checkIfMeleeHitAllyWrench(param, activator, calller) if not util.IsLagCompensationActive() then util.StartLagCompensation(activator) end local melee = activator:GetPlayerItemBySlot(2) local meleeRangeMult = melee:GetAttributeValue("melee range multiplier", true) if meleeRangeMult == nil then meleeRangeMult = 1.0 end --ripped from my med hunter script, hybrid of red sniper laser check and some original work on my end local TraceLineOfSight = { start = activator, -- Start position vector. Can also be set to entity, in this case the trace will start from entity eyes position distance = (100 * meleeRangeMult), -- Used if endpos is nil angles = getEyeAngles(activator), -- Used if endpos is nil 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 } local lineOfSightTraceTable = util.Trace(TraceLineOfSight) local hitEntity = lineOfSightTraceTable.Entity if IsValid(hitEntity) and hitEntity:IsPlayer() and hitEntity.m_iTeamNum == activator.m_iTeamNum then local healedMaxHealth = hitEntity.m_iMaxHealth + hitEntity:GetAttributeValueByClass("add_maxhealth_nonbuffed", 0) + hitEntity:GetAttributeValueByClass("add_maxhealth", 0) --code to consume metal taken from b8's tank repair script local METAL_TO_HEALTH_RATIO = 8 --Metal before hit local oldMetal = activator.m_iAmmo[TF_AMMO_METAL] --metal after hit, tracked so you can't go into negative metal local newMetal = activator.m_iAmmo[TF_AMMO_METAL] - 20 if newMetal < 0 then newMetal = 0 end local metalLost = (oldMetal - newMetal) local healthHealed = metalLost * METAL_TO_HEALTH_RATIO * activator:GetAttributeValueByClass("mult_repair_value", 1) hitEntity:PlaySoundToSelf("Weapon_Wrench.HitBuilding_Success") hitEntity:Print(PRINT_TARGET_CENTER, "WRENCHED BY " .. activator.m_szNetname .. " HEALED FOR " .. healthHealed .. " HEALTH") local fakeEventTable = { patient = hitEntity:GetUserId(), healer = activator:GetUserId(), amount = healthHealed } --scoreboard credit. FireEvent("player_healed", fakeEventTable) activator.m_iHealPoints = activator.m_iHealPoints + healthHealed hitEntity.m_iHealth = hitEntity.m_iHealth + healthHealed if hitEntity.m_iHealth < healedMaxHealth and metalLost > 0 then activator:PlaySoundToSelf("Weapon_Wrench.HitBuilding_Success") activator.m_iAmmo[TF_AMMO_METAL] = newMetal --so we don't overwrite the ammo regen on a bot out of existence. if hitEntity:IsBot() == false then hitEntity:SetAttributeValue("ammo regen", 100) timer.Simple(5, function() hitEntity:SetAttributeValue("ammo regen", 0) end) end else activator:PlaySoundToSelf("Weapon_Wrench.HitBuilding_Failure") end if hitEntity.m_iHealth > healedMaxHealth then hitEntity.m_iHealth = healedMaxHealth end end util.FinishLagCompensation(activator) end function builtToDestroyKill(damage, activator, caller) local maxMetal = 200 * activator:GetAttributeValueByClass("mult_maxammo_metal", 1) if caller.m_bIsMiniBoss == 1 then activator.m_iAmmo[TF_AMMO_METAL] = activator.m_iAmmo[TF_AMMO_METAL] + 100 else activator.m_iAmmo[TF_AMMO_METAL] = activator.m_iAmmo[TF_AMMO_METAL] + 35 end if activator.m_iAmmo[TF_AMMO_METAL] > maxMetal then activator.m_iAmmo[TF_AMMO_METAL] = maxMetal end end function builtToDestroyBuildingBuilt(_, building) if not IsValid(building) then return end timer.Simple(0.1, function() if building.m_iHighestUpgradeLevel ~= 2 then building.m_iUpgradeMetal = 200 end end) end AddEventCallback("object_detonated", function(eventTable) local detonator = ents.GetPlayerByUserId(eventTable.userid) local detonatorWrench = detonator:GetPlayerItemBySlot(2) if detonatorWrench:GetItemName() ~= "The Built To Destroy" then return end local buildingTable = ents.FindAllByClass("obj_*") local buildingType = eventTable.objecttype local buildingIndex = eventTable.index local explodedBuilding for _, entity in pairs(buildingTable) do if entity:GetNetIndex() == buildingIndex then explodedBuilding = entity break end end if explodedBuilding then local explosionOrigin = explodedBuilding:GetAbsOrigin() local Explode = ents.CreateWithKeys("info_particle_system",{ ["start_active"] = "0", ["flag_as_weather"] = "0", ["effect_name"] = "rd_robot_explosion", ["origin"] = explosionOrigin[1] .. " " .. explosionOrigin[2] .. " " .. explosionOrigin[3], ["angles"] = Vector(0,0,0), }) if buildingType == 0 then local ExplodeHeal = ents.CreateWithKeys("info_particle_system",{ ["start_active"] = "0", ["flag_as_weather"] = "0", ["effect_name"] = "spell_overheal_red", ["origin"] = explosionOrigin[1] .. " " .. explosionOrigin[2] .. " " .. explosionOrigin[3], ["angles"] = Vector(0,0,0), }) ExplodeHeal:AcceptInput("Start") timer.Simple(0.5, function() ExplodeHeal:AcceptInput("Stop") ExplodeHeal:Remove() end) end Explode:AcceptInput("Start") timer.Simple(0.5, function() Explode:AcceptInput("Stop") Explode:Remove() end) local damageAmount = 75 * detonator:GetAttributeValueByClass("mult_engy_building_health", 1) if damageAmount > 500 then damageAmount = 500 end local damageAmountSelf = 50 if explodedBuilding.m_bBuilding == 1 then local constructionAmount = explodedBuilding.m_flPercentageConstructed damageAmount = (damageAmount * constructionAmount) damageAmountSelf = (damageAmountSelf * constructionAmount) end local sufferingTable = { Attacker = detonator, -- Attacker Inflictor = nil, -- Direct cause of damage, usually a projectile Weapon = detonatorWrench, Damage = damageAmount, DamageType = DMG_BLAST, -- Damage type, see DMG_* globals. Can be combined with | operator DamageCustom = nil, -- Custom damage type, see TF_DMG_* globals CritType = 0, -- Crit type, 0 = no crit, 1 = mini crit, 2 = normal crit DamagePosition = explosionOrigin, -- Where the target was hit at DamageForce = nil, -- Knockback force of the attack ReportedPosition = explosionOrigin -- Where the attacker attacked from } local sufferingTableSelf = { Attacker = detonator, -- Attacker Inflictor = nil, -- Direct cause of damage, usually a projectile Weapon = detonatorWrench, Damage = damageAmountSelf, DamageType = DMG_BLAST, -- Damage type, see DMG_* globals. Can be combined with | operator DamageCustom = nil, -- Custom damage type, see TF_DMG_* globals CritType = 0, -- Crit type, 0 = no crit, 1 = mini crit, 2 = normal crit DamagePosition = explosionOrigin, -- Where the target was hit at DamageForce = nil, -- Knockback force of the attack ReportedPosition = explosionOrigin -- Where the attacker attacked from } if buildingType ~= 0 then for _, player in pairs(ents.GetAllPlayers()) do if player.m_iTeamNum ~= detonator.m_iTeamNum and player:IsAlive() and (player:GetAbsOrigin()):Distance(explosionOrigin) <= 400 then player:TakeDamage(sufferingTable) elseif player == detonator and player:IsAlive() and (player:GetAbsOrigin()):Distance(explosionOrigin) <= 400 then player:TakeDamage(sufferingTableSelf) end end else for _, player in pairs(ents.GetAllPlayers()) do if player.m_iTeamNum ~= detonator.m_iTeamNum and player:IsAlive() and (player:GetAbsOrigin()):Distance(explosionOrigin) <= 400 then player:TakeDamage(sufferingTable) elseif player == detonator and player:IsAlive() and (player:GetAbsOrigin()):Distance(explosionOrigin) <= 400 then player:AddHealth(damageAmount - damageAmountSelf, false) local fakeEventTable = { patient = player:GetUserId(), healer = detonator:GetUserId(), amount = (damageAmount - damageAmountSelf) } --scoreboard credit. FireEvent("player_healed", fakeEventTable) detonator:PlaySoundToSelf("Weapon_Arrow.ImpactFleshCrossbowHeal") detonator:Print(PRINT_TARGET_CENTER, "HEAL SHRAPNEL FROM YOURSELF, HEALED FOR " .. math.floor((damageAmount - damageAmountSelf)) .. " HEALTH") detonator.m_iHealPoints = detonator.m_iHealPoints + (damageAmount - damageAmountSelf) elseif player.m_iTeamNum == detonator.m_iTeamNum and player:IsAlive() and (player:GetAbsOrigin()):Distance(explosionOrigin) <= 400 then player:AddHealth(damageAmount, false) local fakeEventTable = { patient = player:GetUserId(), healer = detonator:GetUserId(), amount = damageAmount } player:PlaySoundToSelf("Weapon_Arrow.ImpactFleshCrossbowHeal") player:Print(PRINT_TARGET_CENTER, "HEAL SHRAPNEL FROM " .. detonator.m_szNetname .. " HEALED FOR " .. math.floor(damageAmount) .. " HEALTH") --scoreboard credit. FireEvent("player_healed", fakeEventTable) detonator.m_iHealPoints = detonator.m_iHealPoints + damageAmount --if you aren't an engineer, gain ammo from the explosion if player.m_iClass ~= 9 then local playerOrigin = player:GetAbsOrigin() local ammoPack = ents.CreateWithKeys("item_ammopack_small",{ ["origin"] = playerOrigin[1] .. " " .. playerOrigin[2] .. " " .. (playerOrigin[3] + 10), }) --prevents edge cases where players standing inside each other could get more --ammo than they should. --callback is destroyed when the entity is removed ammoPack:AddCallback(ON_SHOULD_COLLIDE, function(_, collided) if collided ~= player then return false else return true end end) timer.Simple(0.1, function() if ammoPack:IsValid() then ammoPack:Remove() end end) end end end end for _, entity in pairs(getAllNonPlayerDamageableEntities()) do if entity.m_iTeamNum ~= detonator.m_iTeamNum and (entity:GetAbsOrigin()):Distance(explosionOrigin) <= 400 then entity:TakeDamage(sufferingTable) end end end end) function checkIfMeleeHitAllyDisciplinary(param, activator, calller) if not util.IsLagCompensationActive() then util.StartLagCompensation(activator) end --ripped from red_sniper_laser.lua local melee = activator:GetPlayerItemBySlot(2) local meleeRangeMult = melee:GetAttributeValue("melee range multiplier", true) if meleeRangeMult == nil then meleeRangeMult = 1.0 end --ripped from my med hunter script, hybrid of red sniper laser check and some original work on my end local TraceLineOfSight = { start = activator, -- Start position vector. Can also be set to entity, in this case the trace will start from entity eyes position distance = (100 * meleeRangeMult), -- Used if endpos is nil angles = getEyeAngles(activator), -- Used if endpos is nil 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 } local lineOfSightTraceTable = util.Trace(TraceLineOfSight) local hitEntity = lineOfSightTraceTable.Entity if IsValid(hitEntity) and hitEntity:IsPlayer() and hitEntity.m_iTeamNum == activator.m_iTeamNum then hitEntity:AddCond(29, 5, activator) activator:AddCond(29, 5, activator) end util.FinishLagCompensation(activator) end function checkIfMeleeHitAllyVitasaw(param, activator, calller) if not util.IsLagCompensationActive() then util.StartLagCompensation(activator) end if activator.m_iDecapitations <= 0 then activator.m_iDecapitations = 0 return end local melee = activator:GetPlayerItemBySlot(2) local meleeRangeMult = melee:GetAttributeValue("melee range multiplier", true) if meleeRangeMult == nil then meleeRangeMult = 1.0 end --ripped from my med hunter script, hybrid of red sniper laser check and some original work on my end local TraceLineOfSight = { start = activator, -- Start position vector. Can also be set to entity, in this case the trace will start from entity eyes position distance = (100 * meleeRangeMult), -- Used if endpos is nil angles = getEyeAngles(activator), -- Used if endpos is nil 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 } local lineOfSightTraceTable = util.Trace(TraceLineOfSight) local hitEntity = lineOfSightTraceTable.Entity if IsValid(hitEntity) and hitEntity:IsPlayer() and hitEntity.m_iTeamNum == activator.m_iTeamNum and hitEntity.m_bIsMiniBoss == 0 then activator.m_iDecapitations = activator.m_iDecapitations - 1 local healedMaxHealth = (hitEntity.m_iMaxHealth * 1.5 * hitEntity:GetAttributeValueByClass("mult_patient_overheal_penalty", 1) * hitEntity:GetAttributeValueByClass("mult_patient_overheal_penalty_active", 1) * activator:GetAttributeValueByClass("mult_medigun_overheal_amount", 1)) healedMaxHealth = healedMaxHealth * ((activator:GetAttributeValueByClass("overheal_expert", 0) * 0.25) + 1) local actualHealth = hitEntity.m_iMaxHealth + hitEntity:GetAttributeValueByClass("add_maxhealth_nonbuffed", 0) + hitEntity:GetAttributeValueByClass("add_maxhealth", 0) --There are cases, like with razorback snipers where a 0 can be dumped into this equation, and if this code wasn't here, it would basically instakill the patient when you try to heal them. if healedMaxHealth < actualHealth then healedMaxHealth = actualHealth end hitEntity:PlaySoundToSelf("Weapon_Arrow.ImpactFleshCrossbowHeal") activator:PlaySoundToSelf("Weapon_Arrow.ImpactFleshCrossbowHeal") local fakeEventTable = { patient = hitEntity:GetUserId(), healer = activator:GetUserId(), amount = (healedMaxHealth - hitEntity.m_iHealth) } --scoreboard credit. FireEvent("player_healed", fakeEventTable) activator.m_iHealPoints = activator.m_iHealPoints + healthHealed hitEntity.m_iHealth = healedMaxHealth end util.FinishLagCompensation(activator) end --Heavily modified version of build_ally_bot_old.lua by royal local PlayersWithCallbacks = {} local BOTS_ATTRIBUTES = { -- ["not solid to players"] = 1, -- prevents bot from taking teleporter ["collect currency on kill"] = 1, ["ammo regen"] = 10, ["fire input on kill"] = "popscript^$virusKnifeKill^", ["weapon always gib"] = 1, ["health regen"] = 5, ["health from packs increased"] = 5, ["armor piercing"] = 40, ["move speed bonus"] = 1.5, ["model scale"] = 0.75, ["voice pitch scale"] = 1.50, ["ignored by enemy sentries"] = 1, ["not solid to players"] = 1, ["max health additive bonus"] = -25, ["special item description"] = "IamASpy", } local BOTS_ATTRIBUTES_GIGA = { -- ["not solid to players"] = 1, -- prevents bot from taking teleporter ["collect currency on kill"] = 1, ["ammo regen"] = 10, ["fire input on kill"] = "popscript^$virusKnifeKill^", ["weapon always gib"] = 1, ["health regen"] = 25, ["health from packs increased"] = 20, ["armor piercing"] = 60, ["move speed bonus"] = 4.0, ["voice pitch scale"] = 1, ["add cloak on kill"] = 20, ["ignored by enemy sentries"] = 1, ["max health additive bonus"] = 175, ["special item description"] = "IamASpy", } local BOTS_ATTRIBUTES_CLONE_BASE = { -- ["not solid to players"] = 1, -- prevents bot from taking teleporter ["collect currency on kill"] = 1, ["ammo regen"] = 10, ["ignored by enemy sentries"] = 1, ["cannot taunt"] = 1, } local BOTS_ATTRIBUTES_DECOY = { -- ["not solid to players"] = 1, -- prevents bot from taking teleporter ["health regen"] = 1000, ["move speed bonus"] = 0, ["dmg taken increased"] = 0, ["damage force reduction"] = 0, ["airblast vulnerability multiplier"] = 0, ["always gib"] = 1, ["damage bonus"] = 0, ["cannot be backstabbed"] = 1, ["cannot taunt"] = 1, ["use robot voice"] = 1, ["always allow taunt"] = 1, ["ignored by enemy sentries"] = 1, } -- we can't expect lua to do all the work - joshua graham -- local BOT_SETUP_VSCRIPT = "activator.SetDifficulty(3); activator.SetMaxVisionRangeOverride(0.1)" -- 16 -- disable dodge local BOT_SETUP_VSCRIPT = "activator.SetDifficulty(0); activator.SetMaxVisionRangeOverride(100000); activator.AddBotAttribute(16)" local BOT_CLEAR_RESTRICTIONS_VSCRIPT = "activator.ClearAllWeaponRestrictions()" local activeBots = {} -- bots alive local activeBuiltBots = {} -- bot built by player local activeBuiltBotsOwner = {} local lingeringBuiltBots = {} local inWave = false local PACK_ITEMS = { "item_currencypack_small", "item_currencypack_medium", "item_currencypack_large", "item_currencypack_custom", } local numActiveBots = 0 local botCheckCounter = 0 function OnWaveInit() inWave = false for _, bot in pairs(activeBuiltBots) do bot:Suicide() bot.m_iTeamNum = 1 end for _, player in pairs(ents.GetAllPlayers()) do if player:IsRealPlayer() == false and player.m_iTeamNum == 2 and player:IsAlive() == true then player:Suicide() player.m_iTeamNum = 1 end end timer.Simple(0.1, function() numActiveBots = 0 end) activeBuiltBots = {} end function OnWaveStart() inWave = true end local function checkOnHit(parent, damageinfo) local attacker = damageinfo.Attacker if not attacker then return end local handle = attacker:GetHandleIndex() local owner = activeBuiltBotsOwner[handle] if not owner then return end if parent == owner then return end damageinfo.Attacker = owner return true end ents.AddCreateCallback("tank_boss", function(tank) tank:AddCallback(ON_DAMAGE_RECEIVED_PRE, function(_, damageinfo) local attacker = damageinfo.Attacker if attacker and attacker:IsRealPlayer() then local attackerPrimary = attacker:GetPlayerItemBySlot(0) local isUpgraded = attackerPrimary:GetAttributeValue("cannot giftwrap", true) if attackerPrimary and attackerPrimary:GetItemName() == "The Backburner" and isUpgraded ~= nil and isUpgraded == 1 then local attackerLookDirection = attacker["m_angEyeAngles[1]"] local tankDirection = tank:GetAbsAngles()[2] --lua arrays start at 1, so this is the tank's yaw. --if the target is not facing you, trigger the effect. Should be identical to actual backstab hit detection (which is why it is bad). if (tankDirection - attackerLookDirection) < 90 or (tankDirection - attackerLookDirection) > 270 then if isCritboosted(attacker) == false then --damageinfo.Damage = damageinfo.Damage * 3 damageinfo.CritType = 2 damageinfo.DamageType = damageinfo.DamageType|DMG_CRITICAL return true --squashes check on hit, but if we're doing this we aren't a bot to begin with, so that doesn't matter. end end end end return checkOnHit(tank, damageinfo) end) end) -- convert damage dealt by bots to owner -- and nullify damage taken by built bot during prewave local function addGlobalCallbacks(player) player:AddCallback(ON_DAMAGE_RECEIVED_PRE, function(_, damageinfo) return checkOnHit(player, damageinfo) end) -- failsafe for spawning bot in setup player:AddCallback(ON_SPAWN, function() if player:IsRealPlayer() then return end timer.Simple(0.1, function() if inWave then return end local owner = activeBuiltBots[player:GetHandleIndex()] if not owner then return end if player.m_iTeamNum == owner.m_iTeamNum then player:Suicide() end end) end) end AddEventCallback("player_spawn", function(eventTable) local spawnedPlayer = ents.GetPlayerByUserId(eventTable.userid) local revisedTable = {} local spawnedPlayerInTable = false for _, player in pairs(PlayersWithCallbacks) do --if IsValid(player) then -- table.insert(revisedTable, player) --end if player == spawnedPlayer then spawnedPlayerInTable = true end end --if #revisedTable ~= #PlayersWithCallbacks then -- PlayersWithCallbacks = {} -- for _, player in pairs(revisedTable) do -- table.insert(PlayersWithCallbacks, player) -- end --end if spawnedPlayerInTable == false then addGlobalCallbacks(spawnedPlayer) table.insert(PlayersWithCallbacks, spawnedPlayer) end end) --function OnPlayerConnected(player) -- --end local function applyName(bot, name, owner) local displayName = name .. " (" .. owner.m_szNetname .. ")" bot.m_szNetname = displayName bot:SetFakeClientConVar("name", displayName) end local function setupBot(bot, owner, handle, isDecoy) local callbacks = {} local botHandle = bot:GetHandleIndex() bot.m_iTeamNum = owner.m_iTeamNum bot.m_iszClassIcon = "" -- don't remove from wave on death local armorPenValue = owner:GetPlayerItemBySlot(2):GetAttributeValue("armor piercing", true) if armorPenValue == nil then armorPenValue = 0 end if isDecoy == 1 then applyName(bot, "Spy", owner) for name, value in pairs(BOTS_ATTRIBUTES) do bot:SetAttributeValue(name, value) end local currentArmorPen = bot:GetAttributeValue("armor piercing", false) bot:SetAttributeValue("armor piercing", currentArmorPen + armorPenValue) elseif isDecoy == 2 then applyName(bot, "Spy", owner) for name, value in pairs(BOTS_ATTRIBUTES_GIGA) do bot:SetAttributeValue(name, value) end local currentArmorPen = bot:GetAttributeValue("armor piercing", false) bot:SetAttributeValue("armor piercing", currentArmorPen + armorPenValue) elseif isDecoy == 3 then applyName(bot, "Clone", owner) for name, value in pairs(BOTS_ATTRIBUTES_CLONE_BASE) do bot:SetAttributeValue(name, value) end else applyName(bot, "Decoy", owner) for name, value in pairs(BOTS_ATTRIBUTES_DECOY) do bot:SetAttributeValue(name, value) end end owner.BuiltBotHandle = tostring(botHandle) activeBots[botHandle] = true activeBuiltBots[handle] = bot lingeringBuiltBots[botHandle] = true activeBuiltBotsOwner[botHandle] = owner callbacks.died = bot:AddCallback(ON_DEATH, function() -- attributes applied to bot spawned through script are not cleared automatically on death for name, _ in pairs(bot:GetAllAttributeValues()) do bot:SetAttributeValue(name, nil) end owner.BuiltBotHandle = false activeBots[botHandle] = nil activeBuiltBots[handle] = nil activeBuiltBotsOwner[botHandle] = nil bot:ResetFakeSendProp("m_iTeamNum") bot.m_iTeamNum = 1 removeCallbacks(bot, callbacks) end) return callbacks end local function findFreeBot() local chosen for _, bot in pairs(ents.GetAllPlayers()) do if not bot:IsRealPlayer() and not bot:IsAlive() and (bot.m_iTeamNum == 1 or bot.m_iTeamNum == 0) and bot:GetPlayerName() ~= "Demo-Bot" then chosen = bot break end end return chosen end function virusKnifeKill(damage, activator, caller) local owner = activator local handle = owner:GetHandleIndex() local origin = caller:GetAbsOrigin() --local ownerSapper = owner:GetPlayerItemBySlot(4) --sapper has a 15 second cooldown, 30% of 15 is 2 --ownerSapper.m_flEffectBarRegenTime = ownerSapper.m_flEffectBarRegenTime - 2 local botSpawn = findFreeBot() if not botSpawn or numActiveBots >= 10 then owner:Print(PRINT_TARGET_CENTER, "GLOBAL BOT LIMIT REACHED") return end numActiveBots = numActiveBots + 1 botCheckCounter = botCheckCounter + 1 if botCheckCounter >= 9 then botCheckCounter = 0 for _, player in pairs(ents.GetAllPlayers()) do if player:IsRealPlayer() == false and player.m_iTeamNum == 2 and player:IsAlive() == true and player:GetAttributeValue("special item description", false) == "IamNOTaSpy" and player.m_iClass == 8 then botSpawn:Suicide() botSpawn.m_iTeamNum = 1 end end end local gigaSpyValue = 1 if caller.m_bIsMiniBoss == 1 then gigaSpyValue = 2 end local callbacks = setupBot(botSpawn, owner, handle, gigaSpyValue) timer.Simple(0, function() botSpawn:SetAbsOrigin(origin) botSpawn:SwitchClassInPlace("Spy") --25 botSpawn:AddCond(51, 1, activator) botSpawn:AddCond(32, 10, activator) botSpawn:SetCustomModelWithClassAnimations("models/bots/spy/bot_spy.mdl") botSpawn:RunScriptCode(BOT_SETUP_VSCRIPT, botSpawn, botSpawn) -- botSpawn:RunScriptCode((BOT_SET_WEPRESTRICTION_VSCRIPT):format("0"), botSpawn, botSpawn) botSpawn:RunScriptCode(BOT_CLEAR_RESTRICTIONS_VSCRIPT, botSpawn, botSpawn) local teleParticle = ents.CreateWithKeys("info_particle_system", { effect_name = "teleportedin_red", start_active = 1, flag_as_weather = 0, }, true, true) teleParticle:SetAbsOrigin(botSpawn:GetAbsOrigin()) teleParticle:Start() timer.Simple(1, function () teleParticle:Remove() end) local spawnedCallback spawnedCallback = botSpawn:AddCallback(ON_SPAWN, function() botSpawn:ResetFakeSendProp("m_iTeamNum") lingeringBuiltBots[handle] = nil botSpawn:RemoveCallback(spawnedCallback) spawnedCallback = nil end) end) --14 local logicLoop local spyTerminated = false --checks every half second rather than every tick because this isn't code that needs to be updated constantly --forcibly uncloak any bot that hasn't teleported after 5 seconds timer.Simple(5, function() if botSpawn:InCond(4) then botSpawn.m_flCloakMeter = 0 botSpawn:RemoveCond(4) end end) logicLoop = timer.Create(0.5, function() --name check is performed in case the spy was killed and respawned between the half second of the check if botSpawn:IsAlive() == false and spyTerminated == false then timer.Stop(logicLoop) numActiveBots = numActiveBots - 1 spyTerminated = true botSpawn.m_iTeamNum = 1 return end if owner:IsValid() == false and spyTerminated == false then timer.Stop(logicLoop) botSpawn:Suicide() numActiveBots = numActiveBots - 1 spyTerminated = true botSpawn.m_iTeamNum = 1 return end end, 41) --These motherfuckers keep living forever, this is the ultimate solution to that issue --No ambiguity with iteration times, no constant checks, no clause they can slip through --They. Will. FUCKING. DIE. timer.Simple(21, function() botSpawn:SetAttributeValue("special item description", "IamNOTaSpy") if botSpawn:IsAlive() == true and spyTerminated == false then botSpawn:Suicide() numActiveBots = numActiveBots - 1 botSpawn.m_iTeamNum = 1 botSpawn:SetAttributeValue("special item description", nil) end if numActiveBots < 0 then numActiveBots = 0 end end) end function cloneSpawn(activator) local owner = activator local handle = owner:GetHandleIndex() local origin = activator:GetAbsOrigin() local botSpawn = findFreeBot() if not botSpawn then owner:Print(PRINT_TARGET_CENTER, "GLOBAL BOT LIMIT REACHED") return end local callbacks = setupBot(botSpawn, owner, handle, 3) timer.Simple(0, function() botSpawn:SetAbsOrigin(origin) botSpawn:SwitchClassInPlace("Soldier") --25 botSpawn:AddCond(51, 2, activator) local ownerPrimary = owner:GetPlayerItemBySlot(0) local ownerSecondary = owner:GetPlayerItemBySlot(1) local ownerMelee = owner:GetPlayerItemBySlot(2) if ownerPrimary then botSpawn:GiveItem(ownerPrimary:GetItemName(), ownerPrimary:GetAllAttributeValues(true), false, true) end if ownerSecondary then botSpawn:GiveItem(ownerSecondary:GetItemName(), ownerSecondary:GetAllAttributeValues(true), false, true) end if ownerMelee then botSpawn:GiveItem(ownerMelee:GetItemName(), ownerMelee:GetAllAttributeValues(true), false, true) end local ownerBodyUpgrades = owner:GetAllAttributeValues() for name, value in pairs(ownerBodyUpgrades) do botSpawn:SetAttributeValue(name, value) end botSpawn:SetCustomModelWithClassAnimations("models/bots/soldier/bot_soldier.mdl") botSpawn:RunScriptCode(BOT_SETUP_VSCRIPT, botSpawn, botSpawn) -- botSpawn:RunScriptCode((BOT_SET_WEPRESTRICTION_VSCRIPT):format("0"), botSpawn, botSpawn) botSpawn:RunScriptCode(BOT_CLEAR_RESTRICTIONS_VSCRIPT, botSpawn, botSpawn) local teleParticle = ents.CreateWithKeys("info_particle_system", { effect_name = "teleportedin_red", start_active = 1, flag_as_weather = 0, }, true, true) teleParticle:SetAbsOrigin(botSpawn:GetAbsOrigin()) teleParticle:Start() timer.Simple(1, function () teleParticle:Remove() end) local spawnedCallback spawnedCallback = botSpawn:AddCallback(ON_SPAWN, function() botSpawn:ResetFakeSendProp("m_iTeamNum") lingeringBuiltBots[handle] = nil botSpawn:RemoveCallback(spawnedCallback) spawnedCallback = nil end) end) local logicLoop logicLoop = timer.Create(0.2, function() if botSpawn:IsAlive() == false or botSpawn.m_szNetname ~= "Clone (" .. owner.m_szNetname .. ")" or owner:IsValid() == false then timer.Stop(logicLoop) owner.m_flHypeMeter = owner.m_flHypeMeter - 1 if owner.m_flHypeMeter < 0 then owner.m_flHypeMeter = 0 end return end if inWave == false then timer.Stop(logicLoop) botSpawn:Suicide() return end if owner:InCond(TF_COND_TAUNTING) then botSpawn:SetAttributeValue("cannot taunt", nil) botSpawn["$Taunt"](botSpawn) botSpawn:SetAttributeValue("cannot taunt", 1) end local pos = owner:GetAbsOrigin() local distance = pos:Distance(botSpawn:GetAbsOrigin()) if distance >= 400 then botSpawn:AddCond(TF_COND_SPEED_BOOST, 1) end local stringStart = "interrupt_action -switch_action Default" -- don't move if already close, or if you are told to not move if distance <= 150 then botSpawn:BotCommand(stringStart .. " -duration 0.1") return end local interruptAction = ("%s -pos %s %s %s -duration 0.1"):format(stringStart, pos[1], pos[2], pos[3]) botSpawn:BotCommand(interruptAction) end, 0) end function decoyWatchActivate(condition, activator) local owner = activator local origin = owner:GetAbsOrigin() for _, player in pairs(ents.GetAllPlayers()) do if player:IsRealPlayer() == false and player.m_iTeamNum == 2 and player:IsAlive() == true then local handle = player:GetHandleIndex() local ownerVerify = activeBuiltBotsOwner[handle] if ownerVerify then local botOrigin = player:GetAbsOrigin() local botAngles = player:GetAbsAngles() local ownerAngles = owner:GetAbsAngles() player:SetAbsOrigin(origin) owner:SetAbsOrigin(botOrigin) player:SnapEyeAngles(ownerAngles) owner:SnapEyeAngles(botAngles) player:SetLocalVelocity(Vector(0,0,0)) owner:SetLocalVelocity(Vector(0,0,0)) local teleParticle = ents.CreateWithKeys("info_particle_system", { effect_name = "teleportedin_red", start_active = 1, flag_as_weather = 0, }, true, true) teleParticle:SetAbsOrigin(origin) teleParticle:Start() local teleParticle2 = ents.CreateWithKeys("info_particle_system", { effect_name = "teleportedin_red", start_active = 1, flag_as_weather = 0, }, true, true) teleParticle2:SetAbsOrigin(botOrigin) teleParticle2:Start() timer.Simple(1, function () teleParticle:Remove() teleParticle2:Remove() end) owner:Print(PRINT_TARGET_CENTER, "SWAPPED WITH DECOY") owner:RemoveCond(4) return end end end if owner.m_flCloakMeter < 99.5 then owner:PlaySoundToSelf("Player.DenyWeaponSelection") owner:RemoveCond(4) return end local handle = owner:GetHandleIndex() local botSpawn = findFreeBot() if not botSpawn then owner:Print(PRINT_TARGET_CENTER, "GLOBAL BOT LIMIT REACHED, CLOAK REFUNDED") owner:PlaySoundToSelf("Player.DenyWeaponSelection") owner.m_flCloakMeter = 100 owner:RemoveCond(4) return end if not inWave then owner:Print(PRINT_TARGET_CENTER, "WAIT UNTIL THE WAVE STARTS TO SPAWN DECOYS, CLOAK REFUNDED") owner:PlaySoundToSelf("Player.DenyWeaponSelection") owner.m_flCloakMeter = 100 owner:RemoveCond(4) return end local callbacks = setupBot(botSpawn, owner, handle, true) owner.m_flCloakMeter = 0 timer.Simple(0, function() botSpawn:SetAbsOrigin(origin) botSpawn:SwitchClassInPlace("Sniper") botSpawn:SetCustomModelWithClassAnimations("models/bots/sniper/bot_sniper.mdl") botSpawn:RunScriptCode(BOT_SETUP_VSCRIPT, botSpawn, botSpawn) -- botSpawn:RunScriptCode((BOT_SET_WEPRESTRICTION_VSCRIPT):format("0"), botSpawn, botSpawn) botSpawn:RunScriptCode(BOT_CLEAR_RESTRICTIONS_VSCRIPT, botSpawn, botSpawn) local teleParticle = ents.CreateWithKeys("info_particle_system", { effect_name = "teleportedin_red", start_active = 1, flag_as_weather = 0, }, true, true) teleParticle:SetAbsOrigin(botSpawn:GetAbsOrigin()) botSpawn:AddCond(4, 0.25, botSpawn) teleParticle:Start() timer.Simple(1, function () teleParticle:Remove() end) local spawnedCallback spawnedCallback = botSpawn:AddCallback(ON_SPAWN, function() botSpawn:ResetFakeSendProp("m_iTeamNum") lingeringBuiltBots[handle] = nil botSpawn:RemoveCallback(spawnedCallback) spawnedCallback = nil end) end) timer.Simple(12, function() botSpawn:SetAttributeValue("cannot taunt", 0) botSpawn["$Taunt"](botSpawn) timer.Simple(1, function() botSpawn:AddCond(4, 1, botSpawn) botSpawn:AddCond(66, 99, botSpawn) end) timer.Simple(2, function() botSpawn:Suicide() end) end) end --Admin functions, for use during tests. Should probably be split off into its own file to save some server memory, but --at the moment it isn't anything gigantic --This function has no right to exist, I just got jealous of hellmet's robot conversion binds function becomeMonochrome(target) local modelUseInt = 1 if not target then return end local itIsI --target is a string literal local allPlayers = ents.GetAllPlayers() for _, player in pairs(allPlayers) do if player.m_szNetname == target then itIsI = player break end end if not itIsI then return end local storedClass = itIsI.m_iClass local monochrome_Character_Attributes = { ["move speed bonus"] = 0.5, ["damage force reduction"] = 0.01, ["airblast vulnerability multiplier"] = 0.01, ["override footstep sound set"] = 3, ["crit mod disabled"] = 0, ["health from packs decreased"] = 0.01, ["not solid to players"] = 1, ["cancel falling damage"] = 1, ["dmg taken mult from special damage type 2"] = 1.75, ["is miniboss"] = 1, ["model scale"] = 1.75, --Average of his HP between short circuit and deprecated designs ["max health additive bonus"] = 64800, ["voice pitch scale"] = 0, } local monochrome_Launcher_Attributes = { ["faster reload rate"] = -0.8, ["fire rate bonus"] = 0.4, ["collect currency on kill"] = 1, ["paintkit_proto_def_index"] = 420, ["set_item_texture_wear"] = 0, ["add cond when active"] = 36, ["crit vs burning players"] = 1, ["crit vs non burning players"] = 1, ["projectile trail particle"] = "eyeboss_projectile", --If this command is being invoked, you're probably in one of my missions, therefore, I can --safely have this. If you want to know what it is, see bot_pale_burst.lua, or robot_scroob.pop ["fire input on attack"] = "popscript^$paleBurstLogicOnAttack^", ["mod projectile heat follow crosshair"] = 1, ["mod projectile heat seek power"] = 100, ["mod projectile heat aim error"] = 360, ["mod projectile heat aim time"] = 0.7, ["mult dmg vs giants"] = 1.5, } local monochrome_Bison_Attributes = { ["faster reload rate"] = 0.01, ["fire rate bonus"] = 0.35, ["collect currency on kill"] = 1, ["passive reload"] = 1, ["mult dmg vs giants"] = 1.5, ["set item tint rgb"] = 16777215, } itIsI:SwitchClassInPlace(3) --not everyone is going to have the grey gsoldier model precached if modelUseInt == 1 then itIsI:SetCustomModelWithClassAnimations("models/bots/soldier_boss/bot_soldier_gray_boss.mdl") else itIsI:SetCustomModelWithClassAnimations("models/bots/soldier_boss/bot_soldier_boss.mdl") end --This was a godawful idea --itIsI.m_szNetname = "Monochrome" for _, player in pairs(allPlayers) do --I don't want to have 45 or 22 * number of messages, timers kicking around if player:IsRealPlayer() then player:AcceptInput("$DisplayTextChat", "{EEEEEE}Monochrome{reset} : {EEEEEE}this{66FF66}.longWindedIntro({FF0000}allPlayers{66FF66})") player:PlaySoundToSelf("mvm/mvm_tele_deliver.wav") player:PlaySoundToSelf("siren2.wav") --These are all started in sequence, so they need to be offset by 2 seconds each timer.Simple(2, function() player:AcceptInput("$DisplayTextChat", "{EEEEEE}Monochrome{reset} : {FF0000}allPlayers{66FF66}.m_ICareLevel < Double.NEGATIVE_INFINITY") end) timer.Simple(4, function() player:AcceptInput("$DisplayTextChat", "{EEEEEE}Monochrome{reset} : {FF9999}FINE") end) timer.Simple(4.5, function() player:AcceptInput("$DisplayTextChat", "{EEEEEE}Monochrome{reset} : {FF7777}ENGAGE") end) end end itIsI:AddCond(66, 0.4) itIsI:AddCond(TF_COND_MVM_BOT_STUN_RADIOWAVE, 0.5) itIsI:AddCond(5, 2) local launcher = itIsI:GiveItem("Upgradeable TF_WEAPON_ROCKETLAUNCHER") local bison = itIsI:GiveItem("The Righteous Bison") local itemOverwriter = itIsI:GiveItem("The Patriot's Pouches") itemOverwriter:Remove() itemOverwriter = itIsI:GiveItem("Shortness Of Breath") itemOverwriter:Remove() itIsI:GiveItem("Tyrantium Helmet"):SetAttributeValue("set item tint rgb", 16777215) --I don't hate myself enough to copypaste 2 bajillion SetAttributeValue lines for name, value in pairs(monochrome_Character_Attributes) do itIsI:SetAttributeValue(name, value); end for name, value in pairs(monochrome_Launcher_Attributes) do launcher:SetAttributeValue(name, value); end for name, value in pairs(monochrome_Bison_Attributes) do bison:SetAttributeValue(name, value); end local callbacks = {} callbacks.yeeowch = itIsI:AddCallback(ON_DEATH, function() for _, player in pairs(allPlayers) do player:AcceptInput("$DisplayTextChat", "{EEEEEE}*DEAD* Monochrome{reset} : {66FF66}print(\"{FF9999}FUCK{66FF66}\")") end itIsI.m_szNetname = target timer.Simple(0.05, function() removeCallbacks(itIsI, callbacks) for name, value in pairs(monochrome_Character_Attributes) do itIsI:SetAttributeValue(name, nil); end itIsI:SwitchClassInPlace(storedClass) if storedClass == 3 and itIsI.m_iTeamNum == 2 then itIsI:SetCustomModelWithClassAnimations("models/player/soldier.mdl") elseif itIsI.m_iTeamNum ~= 2 then itIsI:SetCustomModelWithClassAnimations("models/bots/soldier/bot_soldier.mdl") end end) end) end