--This script continuously checks the location of the bomb, and both the robots and players to create a table of valid nodes that greys can be teleported to. --Individual grey bot scripts are expected to run GREYSPAWNER_getValidNodes, then pick a node to teleport to. function GreySpawnController() local SPAWN_NODE_VECTORS = { Vector(-108.08, 10.74, 288.98), Vector(-768.42, -1.98, 288.98), Vector(688.89, -74.24, 288.98), Vector(597.83, 358.67, 355), Vector(597.83, 748.68, 355), Vector(755.58, 1116.92, 419.2), Vector(417.68, 1138.72, 419.2), Vector(91.54, 1099.91, 419.2), Vector(177.73, 459, 419.2), Vector(-79.94, 500.18, 419.2), Vector(-370.31, 536.29, 419.2), Vector(112.28, 757.34, 419.2), Vector(-720.72, 1439.33, 328.03), Vector(-1335.29, 1166.83, 446.67), Vector(-1491.3, 1665.7, 476.28), Vector(-1491.3, 2282.4, 476.28), Vector(-1166.04, 1783.79, 467.3), Vector(-1166.04, 2152.15, 467.3), Vector(-816.08, 2392.37, 555.3), Vector(-732.82, 2392.37, 555.3), Vector(-784.83, 2816.94, 555.3), Vector(-772.71, 3333.64, 555.3), Vector(-580.46, 3668.54, 555.3), Vector(-1222.89, 3692.71, 555.3), Vector(-1132.85, 3234.52, 618.1), Vector(-1455.44, 3254.61, 618.1), Vector(-1455.44, 3675, 667.02), Vector(-2155.03, 3551.49, 476.28), Vector(-2555.87, 3356.44, 476.28), Vector(-3032.56, 3050.15, 555.3), Vector(-3028.88, 3500.47, 476.28), Vector(-2553.95, 3581.79, 476.28), Vector(-2075.97, 3887.96, 476.28), Vector(-2395.48, 3887.96, 476.28), Vector(-2395.48, 4226.32, 476.28), Vector(-1957.34, 4226.32, 476.28), Vector(-2253.49, 4541.35, 476.28), Vector(-1760.14, 4541.35, 476.28), Vector(-2532.65, 4165.79, 555.3), Vector(-2532.65, 3988.32, 555.3), Vector(-2532.65, 3795.77, 555.3), Vector(-3202.06, 3390.79, 476.28), Vector(-3202.06, 3778.9, 476.28), Vector(-3202.06, 4143.36, 476.28), Vector(-3202.06, 4419.65, 555.3), Vector(-3202.06, 4674.13, 555.3), Vector(-3587.7, 4866.06, 667.02), Vector(-3587.7, 4577.96, 667.02), Vector(-3955.48, 4577.96, 724.51), Vector(-4044.16, 4084.79, 724.51), Vector(-4044.16, 3538.81, 724.51), Vector(-4044.16, 3353.88, 789.46), Vector(-460.99, 4076.37, 535.3), Vector(-815.23, 4180.11, 535.3), Vector(-836.46, 3955.74, 535.3), Vector(-1072.72, 3985.84, 535.3), Vector(-1114.01, 4314.42, 535.3), Vector(-613.04, 4633.99, 535.3), Vector(49.79, 4486.96, 535.3), Vector(-271.41, 4501.6, 535.3), Vector(-271.41, 4289.6, 535.3), Vector(173.48, 4289.6, 535.3), Vector(-241.10, 3925.36, 535.3), Vector(375.2, 3896.01, 535.3), Vector(92.13, 3105.17, 716.11), Vector(92.13, 2523.62, 716.11), Vector(92.13, 2842.84, 716.11), Vector(-704, 482.66, 419.2), Vector(594.35, 3720.13, 716.11), Vector(110, 3542.75, 533.27), Vector(476.86, 2784.48, 581.78), Vector(-254.37, 2784.48, 581.78), Vector(-1816.27, 2558.08, 493.17), Vector(-796.36, -167.41, 269.26), Vector(-253.8, -208.54, 269.26), Vector(590.51, -256.33, 269.26), Vector(592.33, -643.10, 269.26), Vector(-292, 3631, 652.26), Vector(-292, 2918, 652.26), Vector(-384, 2063, 730.26), Vector(94.73, 2786.7, 275.57), } local SPAWN_NODE_VECTORS_PREVIEW = { Vector(-108.08, 10.74, 125.37), Vector(-768.42, -1.98, 113.37), Vector(688.89, -74.24, 104.63), Vector(597.83, 358.67, 189.12), Vector(597.83, 748.68, 285.26), Vector(755.58, 1116.92, 281.49), Vector(417.68, 1138.72, 281.49), Vector(91.54, 1099.91, 281.49), Vector(177.73, 459, 281.49), Vector(-79.94, 500.18, 281.49), Vector(-370.31, 536.29, 281.49), Vector(112.28, 757.34, 281.49), Vector(-720.72, 1439.33, 295.03), Vector(-1335.29, 1166.83, 274.42), Vector(-1491.3, 1665.7, 340.87), Vector(-1491.3, 2282.4, 407.41), Vector(-1166.04, 1783.79, 406.3), Vector(-1166.04, 2152.15, 406.3), Vector(-816.08, 2392.37, 405.65), Vector(-732.82, 2392.37, 405.65), Vector(-784.83, 2816.94, 405.65), Vector(-772.71, 3333.64, 405.65), Vector(-580.46, 3668.54, 405.65), Vector(-1222.89, 3692.71, 422.3), Vector(-1132.85, 3234.52, 491.1), Vector(-1455.44, 3254.61, 511.1), Vector(-1455.44, 3675, 565.02), Vector(-2155.03, 3551.49, 275.28), Vector(-2555.87, 3356.44, 275.28), Vector(-3032.56, 3050.15, 318.3), Vector(-3028.88, 3500.47, 275.28), Vector(-2553.95, 3581.79, 275.28), Vector(-2075.97, 3887.96, 275.28), Vector(-2395.48, 3887.96, 275.28), Vector(-2395.48, 4226.32, 275.28), Vector(-1957.34, 4226.32, 275.28), Vector(-2253.49, 4541.35, 275.28), Vector(-1760.14, 4541.35, 275.28), Vector(-2532.65, 4165.79, 318.3), Vector(-2532.65, 3988.32, 318.3), Vector(-2532.65, 3795.77, 318.3), Vector(-3202.06, 3390.79, 375.28), Vector(-3202.06, 3778.9, 375.28), Vector(-3202.06, 4143.36, 375.28), Vector(-3202.06, 4419.65, 318.3), Vector(-3202.06, 4674.13, 318.3), Vector(-3587.7, 4866.06, 540.02), Vector(-3587.7, 4577.96, 540.02), Vector(-3955.48, 4577.96, 609.51), Vector(-4044.16, 4084.79, 609.51), Vector(-4044.16, 3538.81, 609.51), Vector(-4044.16, 3353.88, 609.51), Vector(-460.99, 4076.37, 365.66), Vector(-815.23, 4180.11, 365.66), Vector(-836.46, 3955.74, 365.66), Vector(-1072.72, 3985.84, 365.66), Vector(-1114.01, 4314.42, 292), Vector(-613.04, 4633.99, 254.66), Vector(49.79, 4486.96, 292), Vector(-271.41, 4501.6, 292), Vector(-271.41, 4289.6, 292), Vector(173.48, 4289.6, 292), Vector(-241.10, 3925.36, 292), Vector(375.2, 3896.01, 292), Vector(92.13, 3105.17, 488.11), Vector(92.13, 2523.62, 488.11), Vector(92.13, 2842.84, 488.11), Vector(-704, 482.66, 254.2), Vector(594.35, 3720.13, 454.11), Vector(110, 3542.75, 276.27), Vector(476.86, 2784.48, 286.78), Vector(-254.37, 2784.48, 286.78), Vector(-1816.27, 2558.08, 384.17), Vector(-796.36, -167.41, 69.52), Vector(-253.8, -208.54, 75.84), Vector(590.51, -256.33, 77.61), Vector(592.33, -643.10, 86.71), Vector(-292, 3631, 315.13), Vector(-292, 2918, 269.26), Vector(-384, 2063, 348.18), Vector(94.73, 2786.7, 275.57), } function GREYSPAWNER_getSpawnNodeVectors() return SPAWN_NODE_VECTORS end function GREYSPAWNER_getSpawnNodeVectorsPreview() return SPAWN_NODE_VECTORS_PREVIEW end --For anyone who wants to use this script on a map other than mvm_sand function GREYSPAWNER_setSpawnNodeVectors(newVectors) SPAWN_NODE_VECTORS = newVectors end function GREYSPAWNER_setSpawnNodeVectorsPreview(newVectors) SPAWN_NODE_VECTORS = SPAWN_NODE_VECTORS_PREVIEW end --These functions are technically unnecessary, but they make it possible to make on-the-fly additions, edits, or removals from --the pool of spawn nodes without having to void the entire table. If you choose to use this script for something other than --supplying vectors to its paired client script, these will probably be helpful. function GREYSPAWNER_getSpawnNodeVectorByIndex(index) return SPAWN_NODE_VECTORS[index] end function GREYSPAWNER_setSpawnNodeVectorByIndex(index, newVector) SPAWN_NODE_VECTORS[index] = newVector end function GREYSPAWNER_addSpawnNodeVector(index, newVector) if index ~= nil then table.insert(SPAWN_NODE_VECTORS,index,newVector) else table.insert(SPAWN_NODE_VECTORS,newVector) end end function GREYSPAWNER_removeSpawnNodeVector(index) table.remove(SPAWN_NODE_VECTORS,index) end function GREYSPAWNER_getSpawnNodeVectorPreviewByIndex(index) return SPAWN_NODE_VECTORS_PREVIEW[index] end function GREYSPAWNER_setSpawnNodeVectorPreviewByIndex(index, newVector) SPAWN_NODE_VECTORS_PREVIEW[index] = newVector end function GREYSPAWNER_addSpawnNodeVectorPreview(index, newVector) if index ~= nil then table.insert(SPAWN_NODE_VECTORS_PREVIEW,index,newVector) else table.insert(SPAWN_NODE_VECTORS_PREVIEW,newVector) end end function GREYSPAWNER_removeSpawnNodeVectorPreview(index) table.remove(SPAWN_NODE_VECTORS_PREVIEW,index) end local spawnCheckingRadius = 1000 local spawnDenialRadiusBase = 300 local spawnDenialRadiusMultiplierRed = 2 local spawnDenialRadiusMultiplierBlue = 1 local priorityNodePlayerConsiderationDistance = 2000 local previewSkinSelection = 4 local previewParticleName = "medic_resist_bullet" --Consider both red and blue players for spawn denial (biased in favor of red), use the priority system, use a white skin for the indicator circle and a white particle local CONSTANT_PRESET_GREY = {1000,300,2,1,1500,4,"medic_resist_bullet"} --Ignore red players for spawn denial, do not use the priority system, use a red skin on the indicator circle and use a red particle local CONSTANT_PRESET_RED = {1000,300,0,1.5,0,5,"medic_healradius_red_buffed"} --Ignore blue players for spawn denial, use the priority system, use a blue skin on the indicator circle and a blue particle local CONSTANT_PRESET_BLUE = {1000,300,2,0,2000,1,"medic_healradius_blue_buffed"} function GREYSPAWNER_getSpawnCheckingRadius() return spawnCheckingRadius end function GREYSPAWNER_getSpawnDenialRadiusBase() return spawnDenialRadiusBase end function GREYSPAWNER_getSpawnDenialRadiusMultiplierRed() return spawnDenialRadiusMultiplierRed end function GREYSPAWNER_getSpawnDenialRadiusMultiplierBlue() return spawnDenialRadiusMultiplierBlue end function GREYSPAWNER_getPriorityNodePlayerConsiderationDistance() return priorityNodePlayerConsiderationDistance end function GREYSPAWNER_getPreviewSkinSelection() return previewSkinSelection end function GREYSPAWNER_getPreviewParticleName() return previewParticleName end --we use tonumber because args sent from source IO always come back as strings function GREYSPAWNER_setSpawnCheckingRadius(newRadius) spawnCheckingRadius = tonumber(newRadius) end function GREYSPAWNER_setSpawnDenialRadiusBase(newRadius) spawnDenialRadiusBase = tonumber(newRadius) end function GREYSPAWNER_setSpawnDenialRadiusMultiplierRed(newMult) spawnDenialRadiusMultiplierRed = tonumber(newMult) end function GREYSPAWNER_setSpawnDenialRadiusMultiplierBlue(newMult) spawnDenialRadiusMultiplierBlue = tonumber(newMult) end function GREYSPAWNER_setPriorityNodePlayerConsiderationDistance(newDistance) priorityNodePlayerConsiderationDistance = tonumber(newDistance) end function GREYSPAWNER_setPreviewSkinSelection(newIndex) previewSkinSelection = tonumber(newIndex) end function GREYSPAWNER_setPreviewParticleName(newParticle) previewParticleName = newParticle end function GREYSPAWNER_getAllConstants() local constantArray = {spawnCheckingRadius, spawnDenialRadiusBase, spawnDenialRadiusMultiplierRed, spawnDenialRadiusMultiplierBlue, priorityNodePlayerConsiderationDistance, previewSkinSelection, previewParticleName} return constantArray end function GREYSPAWNER_setAllConstants(newConstants) local sanitizedConstantArray = {} local currentConstants = GREYSPAWNER_getAllConstants() for i=1,7,1 do if newConstants[i] ~= nil then table.insert(sanitizedConstantArray,i,newConstants[i]) else table.insert(sanitizedConstantArray,i,currentConstants[i]) end end spawnCheckingRadius = sanitizedConstantArray[1] spawnDenialRadiusBase = sanitizedConstantArray[2] spawnDenialRadiusMultiplierRed = sanitizedConstantArray[3] spawnDenialRadiusMultiplierBlue = sanitizedConstantArray[4] priorityNodePlayerConsiderationDistance = sanitizedConstantArray[5] previewSkinSelection = sanitizedConstantArray[6] previewParticleName = sanitizedConstantArray[7] end local spawnWarningEnabled = false local spawnWarningBucket = 0 local soundCooldownBucket = 0 local callbacks = {} local validNodes = {} local priorityNodes = {} local targetTeam = {} function GREYSPAWNER_getValidNodes() return validNodes end function GREYSPAWNER_getPriorityNodes() return priorityNodes end --In case a function wants to pick its nodes more selectively function GREYSPAWNER_getTargetTeam() return targetTeam end function GREYSPAWNER_enableWarning(duration) spawnWarningBucket = spawnWarningBucket + duration end function GREYSPAWNER_requestSound(giantStatus) if soundCooldownBucket > 0 then return end local teleportSoundString = "=35|weapons/teleporter_send.wav" if giantStatus == 1 then teleportSoundString = "=35|mvm/mvm_tele_deliver.wav" end local allPlayers = ents.GetAllPlayers() for _, player in pairs(allPlayers) do player:AcceptInput("$PlaySoundToSelf", teleportSoundString) end soundCooldownBucket = 6 end local monitorNodes monitorNodes = timer.Create(0.5, function() --void old tables validNodes = {} priorityNodes = {} local potentialPriorityNodes = {} if spawnWarningBucket > 0 then spawnWarningBucket = spawnWarningBucket - 0.5 spawnWarningEnabled = true else spawnWarningBucket = 0 spawnWarningEnabled = false end if soundCooldownBucket > 0 then soundCooldownBucket = soundCooldownBucket - 0.5 else soundCooldownBucket = 0 end local allPlayers = ents.GetAllPlayers() --Table to contain the players targetTeam = {} local redTeam = {} local blueTeam = {} --Pick out our target players for _, player in pairs(allPlayers) do if player.m_iTeamNum == 2 then if player:IsAlive() == true then table.insert(targetTeam, player) table.insert(redTeam, player) end elseif player.m_iTeamNum == 3 then if player:IsAlive() == true then table.insert(targetTeam, player) table.insert(blueTeam, player) end end end --Origin of the bomb --there will always be a bomb, even if it isn't technically "active", it will still be spawned in. --are you using this script for something other than mvm? Too bad, plague of a thousand nil errors upon ye. --on a more serious note, a way to override the fulcrum position directly and skip finding the bomb is on the docket --for future updates (for example, forcing a mobber boss to be the fulcrum, so this will support entities other than teamflags). local bomb = ents.FindByClass("item_teamflag") local greyFulcrumOrigin --If the bomb is being actively carried, base grey spawns around its position if bomb.m_nFlagStatus == 1 then local bombOrigin = bomb:GetAbsOrigin() greyFulcrumOrigin = Vector(bombOrigin[1], bombOrigin[2], 0) --If the flag is either waiting in spawn (unlikely) or is dropped, base grey spawns around where both teams are precisely else --Compare distances between both red and blue, pick the two players who are closest, then average their positions to use as a substitute for the bomb's position. Ignores red spies for this calculation. local closestRedPlayerOrigin local closestBluePlayerOrigin local minRedDistance = 9999 for _, player in pairs(redTeam) do local redPlayerOrigin = player:GetAbsOrigin() local redPlayerOriginNoVertical = Vector(redPlayerOrigin[1], redPlayerOrigin[2], 0) local redPlayer = player for _, player in pairs(blueTeam) do local bluePlayerOrigin = player:GetAbsOrigin() local bluePlayerOriginNoVertical = Vector(bluePlayerOrigin[1], bluePlayerOrigin[2], 0) if redPlayerOriginNoVertical:Distance(bluePlayerOriginNoVertical) < minRedDistance and redPlayer.m_iClass ~= 8 then minRedDistance = redPlayerOriginNoVertical:Distance(bluePlayerOriginNoVertical) closestRedPlayerOrigin = redPlayerOriginNoVertical closestBluePlayerOrigin = bluePlayerOriginNoVertical end end end if closestRedPlayerOrigin == nil or closestBluePlayerOrigin == nil then local bombOrigin = bomb:GetAbsOrigin() greyFulcrumOrigin = Vector(bombOrigin[1], bombOrigin[2], 0) else greyFulcrumOrigin = Vector(((closestRedPlayerOrigin[1] + closestBluePlayerOrigin[1])/2),((closestRedPlayerOrigin[2] + closestBluePlayerOrigin[2])/2), 0) end end for index, Node in ipairs(SPAWN_NODE_VECTORS) do local nodeNoVertical = Vector(Node[1], Node[2], 0) local nodeToBombDistance = nodeNoVertical:Distance(greyFulcrumOrigin) --Will be set to false if the node fails the distance to player check, will never be considered --if the node is more than 1000 units away from the bomb. local nodeIsValid = true --If we're 1000 hammer units or less away from the bomb, but 600 hammer units away from any red players, and 300 away from any blue players, we are valid. if nodeToBombDistance <= spawnCheckingRadius then for _, player in pairs(targetTeam) do local playerOrigin = player:GetAbsOrigin() local noNoRadius = spawnDenialRadiusBase if player.m_iTeamNum == 2 then noNoRadius = spawnDenialRadiusBase * spawnDenialRadiusMultiplierRed else noNoRadius = spawnDenialRadiusBase * spawnDenialRadiusMultiplierBlue end local playerOriginNoVertical = Vector(playerOrigin[1], playerOrigin[2], 0) if playerOriginNoVertical:Distance(nodeNoVertical) < noNoRadius then nodeIsValid = false end end --If we're valid, spawn a pair of preview particles that live until the next preview cycle if nodeIsValid == true and spawnWarningEnabled == true then local previewNode = SPAWN_NODE_VECTORS_PREVIEW[index] --The origin tab under create with keys does not take a vector, it takes a wish.com version of a vector --manually set up with a string. local nodeOriginRetardedSyntax = "" .. previewNode[1] .. " " .. previewNode[2] .. " " .. previewNode[3] .. "" local teleWarnParticle = ents.CreateWithKeys("prop_dynamic",{ ["disableshadows"] = "1", ["model"] = "models/props_mvm/indicator/indicator_circle.mdl", ["defaultanim"] = "start", --["model"] = "models/bots/scout_boss/bot_scout_boss.mdl", ["origin"] = nodeOriginRetardedSyntax, ["modelscale"] = 1.75, ["skin"] = previewSkinSelection, }) local teleWarnParticle1 = ents.CreateWithKeys("info_particle_system", { effect_name = previewParticleName, start_active = 1, flag_as_weather = 0, }, true, true) teleWarnParticle1:SetAbsOrigin(previewNode) teleWarnParticle1:Start() --Look for priority nodes, priority nodes are nodes that are in the line of sight of players that are within the priority consideration distance. for _, player in pairs(redTeam) do local playerOrigin = player:GetAbsOrigin() local playerOriginNoVertical = Vector(playerOrigin[1], playerOrigin[2], 0) if playerOriginNoVertical:Distance(greyFulcrumOrigin) <= priorityNodePlayerConsiderationDistance then local pitch = player["m_angEyeAngles[0]"] local yaw = player["m_angEyeAngles[1]"] local playerEyeAngles = Vector(pitch, yaw, 0) --thank you ocet247 local tolerance = 0.5736 --cos(110/2) local delta = Vector(Node[1] - pitch, Node[2] - yaw, 0) --Normalize the vector delta = delta:Normalize() if playerEyeAngles:GetForward():Dot(delta) >= tolerance then table.insert(potentialPriorityNodes, Node) end end end --If we were able to find and potential priority nodes, do this giant chain of table iterations to eliminate --node(s) with the fewest occurances in the table. if type(next(potentialPriorityNodes)) ~= "nil" then --find all distinct values in the table local potentialPriorityNodesNoDuplicates = {} --means we are guaranteed to never touch a nil value. table.insert(potentialPriorityNodesNoDuplicates, potentialPriorityNodes[1]) for i, N in ipairs(potentialPriorityNodes) do local isDuplicate = false for index, Node in ipairs(potentialPriorityNodesNoDuplicates) do if Node == N then isDuplicate = true break end end if isDuplicate == false then table.insert(potentialPriorityNodesNoDuplicates, N) end end --count the occurances of each value local nodeOccuranceCounts = {} for i, N in ipairs(potentialPriorityNodesNoDuplicates) do table.insert(nodeOccuranceCounts, 0) for index, Node in ipairs(potentialPriorityNodesNoDuplicates) do if Node == N then nodeOccuranceCounts[i] = nodeOccuranceCounts[i] + 1 end end end local lowestNodes = {} local lowestCount = 9999 local highestCount = 0 --we track this to disable this ranking behavior if there is no true lowest entry for i=1,#nodeOccuranceCounts,1 do if nodeOccuranceCounts[i] < lowestCount then lowestNodes = {} table.insert(lowestNodes, i) lowestCount = nodeOccuranceCounts[i] elseif nodeOccuranceCounts[i] == lowestCount then table.insert(lowestNodes, i) end if nodeOccuranceCounts[i] > highestCount then highestCount = nodeOccuranceCounts[i] end end if highestCount ~= lowestCount then --build real table, excluding least common value, unless all are equal for index, Node in ipairs(potentialPriorityNodes) do local nodeIsNotLowest = true for slot, lowest in ipairs(lowestNodes) do if Node == lowestNodes[slot] then nodeIsNotLowest = false end if nodeIsNotLowest == true then table.insert(priorityNodes, Node) end end end else for index, Node in ipairs(potentialPriorityNodes) do table.insert(priorityNodes, Node) end end end --If we're being previewed, we are a valid grey spawn location, add it to the table table.insert(validNodes, Node) --Make the particles die by the next preview cycle timer.Simple(0.5, function() if teleWarnParticle:IsValid() then teleWarnParticle:Remove() teleWarnParticle1:Remove() end end) end end end end, 0) --Event callbacks cannot self terminate, they need another function to lower them into the steel local function finalizeCleanup(callbacks) for _, callbackId in pairs(callbacks) do RemoveEventCallback(callbackId) end end callbacks.waveReset = AddEventCallback("mvm_reset_stats", function () timer.Stop(monitorNodes) monitorNodes = nil finalizeCleanup(callbacks) end) end