local SQUAD_DEBUG_MODE = false local TEST_BOMB_SQUAD_FAILSAFE = false local function cancelSquadMedicBehavior(squadMember, timerId, memberTemplate) timer.Stop(timerId) RemoveTimerFromWaveActive(timerId) squadMember:RunScriptCode("activator.ClearAllWeaponRestrictions()", squadMember, squadMember) if memberTemplate.WeaponRestrictions then squadMember:RunScriptCode( ("activator.AddWeaponRestriction(%s)"):format(tostring(memberTemplate.WeaponRestrictions)), squadMember, squadMember ) end if not memberTemplate.Attributes or not TableContains(memberTemplate.Attributes, BOT_ATTRIBUTES.IGNORE_ENEMIES) then squadMember:RunScriptCode( ("activator.RemoveBotAttribute(%s)"):format(BOT_ATTRIBUTES.IGNORE_ENEMIES), squadMember, squadMember ) end squadMember:BotCommand("stop interrupt action") squadMember:BotCommand("clear_interrupt_action_queue") end local function cancelStickToLeaderBehavior(squadMember, timerId, memberTemplate) timer.Stop(timerId) RemoveTimerFromWaveActive(timerId) squadMember:BotCommand("stop interrupt action") squadMember:BotCommand("clear_interrupt_action_queue") end local function hasBomb(squadMember) for _, bomb in pairs(ents.FindAllByClass("item_teamflag")) do local bombOwner = bomb.m_hPrevOwner if bombOwner and squadMember == bombOwner then return true end end return false end function SquadMedicBehavior(squadMember, squadLeader, memberTemplate) local squadLeaderSpawnBotId = squadLeader.SpawnBotId local squadMemberSpawnBotId = squadMember.SpawnBotId squadMember:RunScriptCode("activator.AddWeaponRestriction(4)", squadMember, squadMember) squadMember:RunScriptCode( ("activator.AddBotAttribute(%s)"):format(BOT_ATTRIBUTES.IGNORE_ENEMIES), squadMember, squadMember ) local lastLeaderHealth = squadLeader.m_iHealth local lastMemberHealth = squadMember.m_iHealth local infoTargetName = tostring(squadLeader:GetHandleIndex()) .. "squadLeaderPoint" local infoTarget = ents.FindByName(infoTargetName) if not infoTarget or not IsValid(infoTarget) then infoTarget = Entity("info_target") infoTarget:SetAbsOrigin(squadLeader:GetAbsOrigin()) infoTarget:SetFakeParent(squadLeader) infoTarget:SetName(infoTargetName) end timer.Simple(0.1, function() local interruptAction = ("interrupt_action -posent %s -lookposent %s -killlook -waituntildone -alwayslook -distance 150"):format( infoTargetName, infoTargetName ) squadMember:BotCommand(interruptAction) end) local timerId timerId = timer.Create(0.15, function() if not squadLeader:IsAlive() or squadLeader.SpawnBotId ~= squadLeaderSpawnBotId then if IsValid(infoTarget) then infoTarget:Remove() end cancelSquadMedicBehavior(squadMember, timerId, memberTemplate) return end if not squadMember:IsAlive() or squadMember.SpawnBotId ~= squadMemberSpawnBotId then if IsValid(infoTarget) then infoTarget:Remove() end cancelSquadMedicBehavior(squadMember, timerId, memberTemplate) return end local medigun = squadMember:GetPlayerItemBySlot(LOADOUT_POSITION_SECONDARY) if medigun.m_hHealingTarget ~= squadLeader then -- medigun.m_hHealingTarget = squadLeader -- medigun.m_hLastHealingTarget = nil squadMember:RunScriptCode("activator.PressFireButton(0.05)", squadMember, squadMember) else squadMember:RunScriptCode("activator.PressFireButton(9999999999)", squadMember, squadMember) end local medicThreshold = squadMember:GetAttributeValue("bot medic uber health threshold") or 75 if squadMember.m_iHealth <= medicThreshold or (squadLeader.m_iHealth / squadLeader.m_iMaxHealth) <= 0.5 then if squadLeader.m_iHealth < lastLeaderHealth or squadMember.m_iHealth < lastMemberHealth then -- uber squadMember:RunScriptCode("activator.PressAltFireButton(0.1)", squadMember, squadMember) end end lastLeaderHealth = squadLeader.m_iHealth lastMemberHealth = squadMember.m_iHealth end, 0) table.insert(ActiveSubwaveTimers, timerId) end function StickToSquadLeader(squadMember, squadLeader, memberTemplate) local squadLeaderSpawnBotId = squadLeader.SpawnBotId local squadMemberSpawnBotId = squadMember.SpawnBotId if SQUAD_DEBUG_MODE then util.PrintToChatAll("applying stick to squad leader behavior") end local timerId timerId = timer.Create(0.2, function() if not squadLeader:IsAlive() or squadLeader.SpawnBotId ~= squadLeaderSpawnBotId then cancelStickToLeaderBehavior(squadMember, timerId, memberTemplate) return end if not squadMember:IsAlive() or squadMember.SpawnBotId ~= squadMemberSpawnBotId then cancelStickToLeaderBehavior(squadMember, timerId, memberTemplate) return end -- pause behavior if we have bomb if hasBomb(squadMember) then return end local leaderPos = squadLeader:GetAbsOrigin() + Vector(0, 0, 5) -- local memberPos = squadMember:GetAbsOrigin() -- if leaderPos:Distance(memberPos) < 250 then -- return -- end local interruptAction = ("interrupt_action -pos %s %s %s -distance 150 -duration 0.1"):format( leaderPos[1], leaderPos[2], leaderPos[3] ) squadMember:BotCommand(interruptAction) end, 0) table.insert(ActiveSubwaveTimers, timerId) end -- (royal): as all bots in a squad spawn in at the exact same tick, sometimes a squad member may be assigned the bomb instead of the squad leader -- this fixes that by forcefully sending the bomb back to the squad leader function SquadBombFailsafe(squadBots, squadLeader, attempts) if attempts >= #squadBots * 4 then return end for _, bomb in pairs(ents.FindAllByClass("item_teamflag")) do local bombOwner = bomb.m_hPrevOwner if bombOwner then for _, squadMember in pairs(squadBots) do if squadMember == bombOwner and squadMember ~= squadLeader then util.PrintToChatAll("[SQUAD-BEHAVIORS]: detected bomb on a squad member, moving to squad leader") -- (royal): it's not guaranteed that the leader will successfully pick up the bomb when it's teleported to them, sometimes medics pick it up bomb:ForceDrop() bomb:SetAbsOrigin(squadLeader:GetAbsOrigin() + Vector(0, 0, 10)) local timerId timerId = timer.Simple(0.05, function() RemoveTimerFromWaveActive(timerId) attempts = attempts + 1 SquadBombFailsafe(squadBots, squadLeader, attempts) end) table.insert(ActiveSubwaveTimers, timerId) break end end end end end function HandleSquad(squadBots, squadBotTemplates) -- TODO: squad bundling behavior local squadLeader = squadBots[1] local squadLeaderSpawnBotId = squadLeader.SpawnBotId for i, squadMember in pairs(squadBots) do if squadMember ~= squadLeader then if INTERNAL_CLASS_ID_TO_NAME[squadMember.m_iClass] == CLASSES.Medic then SquadMedicBehavior(squadMember, squadLeader, squadBotTemplates[i]) else -- stick to squad leader StickToSquadLeader(squadMember, squadLeader, squadBotTemplates[i]) end end -- (royal): remove dead bot from squad array local callbacks = {} local function clearCallbacks() for _, callbackId in pairs(callbacks) do squadMember:RemoveCallback(callbackId) end end callbacks.spawned = squadMember:AddCallback(ON_SPAWN, function() clearCallbacks() end) callbacks.died = squadMember:AddCallback(ON_DEATH, function() clearCallbacks() for j, squadBot in pairs(squadBots) do if squadBot == squadMember then table.remove(squadBots, j) table.remove(squadBotTemplates, j) break end end end) end local timerId timerId = timer.Simple(1, function() RemoveTimerFromWaveActive(timerId) if TEST_BOMB_SQUAD_FAILSAFE then for _, bomb in pairs(ents.FindAllByClass("item_teamflag")) do local bombOwner = bomb.m_hPrevOwner if bombOwner then bomb:ForceDrop() bomb:SetAbsOrigin(squadLeader:GetAbsOrigin()) end end util.PrintToChatAll("[SQUAD-BEHAVIORS]: running TEST_BOMB_SQUAD_FAILSAFE") end if squadLeader:IsAlive() and squadLeader.SpawnBotId == squadLeaderSpawnBotId then SquadBombFailsafe(squadBots, squadLeader, 0) end end) table.insert(ActiveSubwaveTimers, timerId) end