local BOTS_ATTRIBUTES = { -- ["collect currency on kill"] = 1, -- ["ammo regen"] = 10, } local ROBOT_MODEL = { Scout = "models/bots/scout/bot_scout.mdl", Soldier = "models/bots/soldier/bot_soldier.mdl", Pyro = "models/bots/pyro/bot_pyro.mdl", Demoman = "models/bots/demo/bot_demo.mdl", Heavyweapons = "models/bots/heavy/bot_heavy.mdl", Engineer = "models/bots/engineer/bot_engineer.mdl", Medic = "models/bots/medic/bot_medic.mdl", Sniper = "models/bots/sniper/bot_sniper.mdl", Spy = "models/bots/spy/bot_spy.mdl", } local GIANT_ROBOT_MODEL = { Scout = "models/bots/scout_boss/bot_scout_boss.mdl", Soldier = "models/bots/soldier_boss/bot_soldier_boss.mdl", Pyro = "models/bots/pyro_boss/bot_pyro_boss.mdl", Demoman = "models/bots/demo_boss/bot_demo_boss.mdl", Heavyweapons = "models/bots/heavy_boss/bot_heavy_boss.mdl", -- no model, default to normal model Engineer = "models/bots/engineer/bot_engineer.mdl", Medic = "models/bots/medic/bot_medic.mdl", Sniper = "models/bots/sniper/bot_sniper.mdl", Spy = "models/bots/spy/bot_spy.mdl", } local CLASS_INDEX_INTERNAL = { [1] = "Scout", [3] = "Soldier", [7] = "Pyro", [4] = "Demoman", [6] = "Heavyweapons", [9] = "Engineer", [5] = "Medic", [2] = "Sniper", [8] = "Spy", } local BOT_SET_DIFFICULTY_VSCRIPT = "activator.SetDifficulty(%s)" local BOT_ADD_ATTRIBUTE_VSCRIPT = "activator.AddBotAttribute(%s)" local BOT_SET_MISSION_VSCRIPT = "activator.SetMission(%s, true)" local BOT_SET_CLASS_AND_RESPAWN_VSCRIPT = "activator.ForceChangeTeam(Constants.ETFTeam.TF_TEAM_BLUE, true); activator.SetPlayerClass(%s);activator.ForceRegenerateAndRespawn()" local BOT_FORCE_RESPAWN_VSCRIPT = "activator.ForceRegenerateAndRespawn()" local BOT_SET_MAX_VISION_RANGE_VSCRIPT = "activator.SetMaxVisionRangeOverride(%s)" local BOT_SET_WEPRESTRICTION_VSCRIPT = "activator.AddWeaponRestriction(%s)" local BOT_ADD_BOT_TAG_VSCRIPT = "activator.AddBotTag(%s)" local BOT_FORCE_CHANGE_TEAM_VSCRIPT = "activator.ForceChangeTeam(%s, true)" local BOT_CLEAR_RESTRICTIONS_VSCRIPT = "activator.ClearAllWeaponRestrictions()" local BOT_CLEAR_BOT_TAGS_VSCRIPT = "activator.ClearAllBotTags()" local BOT_CLEAR_ALL_ATTRIBUTES_VSCRIPT = "activator.ClearAllBotAttributes()" local PRINT_ON_BOT_SPAWN = false -- utils local function getRandomSpawnOrigin(teamNum) local possibleTeamSpawns = {} for _, teamSpawn in pairs(ents.FindAllByClass("info_player_teamspawn")) do if teamSpawn.m_bDisabled == false and teamSpawn.m_iTeamNum == teamNum then table.insert(possibleTeamSpawns, teamSpawn) end end local chosenTeamSpawn = possibleTeamSpawns[math.random(#possibleTeamSpawns)] return chosenTeamSpawn:GetAbsOrigin() end local function getSpawnOriginFromName(targetName) local teamSpawn = ents.FindByName(targetName) if not teamSpawn or not IsValid(teamSpawn) then util.PrintToChatAll("[SPAWNBOT][ERROR]: NO TEAM SPAWN FOUND FOR 'WHERE' " .. targetName) end return teamSpawn:GetAbsOrigin() end -- bot local function findFreeBot() local chosen for _, bot in pairs(ents.GetAllPlayers()) do if not bot:IsRealPlayer() and not bot:IsAlive() and (bot.m_iTeamNum == TEAM_SPECTATOR or bot.m_iTeamNum == TEAM_UNASSIGNED) and bot:GetPlayerName() ~= "Demo-Bot" then chosen = bot break end end return chosen end local function applyName(bot, displayName) bot.m_szNetname = displayName bot:SetFakeClientConVar("name", displayName) end local function basicInitBot(bot, owner) local callbacks = {} for name, _ in pairs(bot:GetAllAttributeValues()) do bot:SetAttributeValue(name, nil) end bot:RunScriptCode(BOT_CLEAR_RESTRICTIONS_VSCRIPT, bot, bot) bot:RunScriptCode(BOT_CLEAR_ALL_ATTRIBUTES_VSCRIPT, bot, bot) bot:RunScriptCode(BOT_CLEAR_BOT_TAGS_VSCRIPT, bot, bot) -- remove potential lingering health bot:SetAttributeValue("hidden maxhealth non buffed", nil) -- default attributes for name, value in pairs(BOTS_ATTRIBUTES) do bot:SetAttributeValue(name, value) end local function removeCallbacks(callbacks) if not IsValid(bot) then return end for _, callbackId in pairs(callbacks) do bot:RemoveCallback(callbackId) end end callbacks.spawned = bot:AddCallback(ON_SPAWN, function() removeCallbacks(callbacks) end) 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 bot.m_bUseBossHealthBar = false bot:RunScriptCode(BOT_CLEAR_RESTRICTIONS_VSCRIPT, bot, bot) bot:RunScriptCode(BOT_CLEAR_ALL_ATTRIBUTES_VSCRIPT, bot, bot) bot:RunScriptCode(BOT_CLEAR_BOT_TAGS_VSCRIPT, bot, bot) bot:ResetFakeSendProp("m_iTeamNum") timer.Simple(0.1, function() if not bot:IsAlive() then bot:RunScriptCode((BOT_FORCE_CHANGE_TEAM_VSCRIPT):format(TEAM_SPECTATOR), bot, bot) -- (royal): this needs to happen here, or sentry buster will crash the server on death bot:RunScriptCode((BOT_SET_MISSION_VSCRIPT):format(tostring(BOT_MISSIONS.NO_MISSION)), bot, bot) end end) -- allow external on_death callbacks to run timer.Simple(0.015, function() removeCallbacks(callbacks) end) end) return callbacks end local function applyRobotModel(bot, templateInfo, isGiant) local robotModel if isGiant then robotModel = GIANT_ROBOT_MODEL[templateInfo.Class] else robotModel = ROBOT_MODEL[templateInfo.Class] end if not robotModel then util.PrintToChatAll("[SPAWNBOT]: " .. "CLASS" .. templateInfo.Class .. " HAS NO ASSOCIATED ROBOT MODEL") return end bot:SetCustomModelWithClassAnimations(robotModel) end function GetFreeBotSlotsCount() local freeSlots = 0 for _, bot in pairs(ents.GetAllPlayers()) do if not bot:IsRealPlayer() and not bot:IsAlive() and (bot.m_iTeamNum == TEAM_SPECTATOR or bot.m_iTeamNum == TEAM_UNASSIGNED) and bot:GetPlayerName() ~= "Demo-Bot" then freeSlots = freeSlots + 1 end end return freeSlots end function GetTotalBotSlotsCount() local totalSlots = 0 for _, bot in pairs(ents.GetAllPlayers()) do if not bot:IsRealPlayer() and bot:GetPlayerName() ~= "Demo-Bot" then totalSlots = totalSlots + 1 end end return totalSlots end function GetActiveBotsCount() local activeBotsCount = 0 for _, bot in pairs(ents.GetAllPlayers()) do if not bot:IsRealPlayer() and bot:GetPlayerName() ~= "Demo-Bot" then if bot:IsAlive() and (bot.m_iTeamNum ~= TEAM_SPECTATOR and bot.m_iTeamNum ~= TEAM_UNASSIGNED) then activeBotsCount = activeBotsCount + 1 end end end return activeBotsCount end local curSpawnId = 0 function SpawnBot(templateInfo, teamSpawnTargetName) local botSpawn = findFreeBot() if not botSpawn then util.PrintToChatAll("[SPAWNBOT]: " .. "FAILED TO FIND FREE BOT") return end if not templateInfo.ClassIndex then local internalClassIndex for i, v in pairs(CLASS_INDEX_INTERNAL) do if v == templateInfo.Class then internalClassIndex = i break end end templateInfo.ClassIndex = internalClassIndex end if PRINT_ON_BOT_SPAWN then util.PrintToChatAll( "[SPAWNBOT]: " .. "spawned bot of class " .. templateInfo.Class .. " at tick count " .. tostring(TickCount()) ) end local isGiant = templateInfo.Attributes and TableContains(templateInfo.Attributes, BOT_ATTRIBUTES.MINIBOSS) -- botSpawn:RunScriptCode((BOT_SET_CLASS_AND_RESPAWN_VSCRIPT):format(templateInfo.ClassIndex), botSpawn, botSpawn) local spawnOrigin if not teamSpawnTargetName or teamSpawnTargetName == "_random" then spawnOrigin = getRandomSpawnOrigin(TEAM_BLUE) else spawnOrigin = getSpawnOriginFromName(teamSpawnTargetName) end -- model timer.Simple(0.1, function() if templateInfo.UseBusterModel then botSpawn:SetCustomModelWithClassAnimations("models/bots/demo/bot_sentry_buster.mdl") elseif templateInfo.Model then botSpawn:SetCustomModelWithClassAnimations(templateInfo.Model) else applyRobotModel(botSpawn, templateInfo, isGiant) end end) -- apply class botSpawn:RunScriptCode((BOT_FORCE_CHANGE_TEAM_VSCRIPT):format(TEAM_BLUE), botSpawn, botSpawn) botSpawn:SwitchClassInPlace(templateInfo.Class) botSpawn:RunScriptCode(BOT_FORCE_RESPAWN_VSCRIPT, botSpawn, botSpawn) botSpawn:SetAbsOrigin(spawnOrigin) local callbacks = basicInitBot(botSpawn) -- apply behavior if isGiant then botSpawn.m_bIsMiniBoss = 1 else botSpawn.m_bIsMiniBoss = 0 end if templateInfo.ClassIcon then botSpawn.m_iszClassIcon = templateInfo.ClassIcon else botSpawn.m_iszClassIcon = "" -- don't remove from wave on death end local skill = templateInfo.Skill or 0 botSpawn:RunScriptCode((BOT_SET_DIFFICULTY_VSCRIPT):format(tostring(skill)), botSpawn, botSpawn) if not templateInfo.Skill then print("no skill defined for bot, defaulting to easy") end if templateInfo.Attributes then for _, attribute in pairs(templateInfo.Attributes) do botSpawn:RunScriptCode((BOT_ADD_ATTRIBUTE_VSCRIPT):format(tostring(attribute)), botSpawn, botSpawn) if attribute == BOT_ATTRIBUTES.SPAWN_WITH_FULL_CHARGE then botSpawn.m_flRageMeter = 100 -- 0 to 100 botSpawn.m_flHypeMeter = 100 -- 0 to 100 for _, item in pairs(botSpawn:GetAllItems()) do if item:IsWeapon() then if item.m_flChargeLevel then item.m_flChargeLevel = 1 -- 0 to 1 end -- cleaner's carbine if item.m_flMinicritCharge then item.m_flMinicritCharge = 100 -- 0 to 100 end end end elseif attribute == BOT_ATTRIBUTES.AUTO_JUMP then if templateInfo.AutoJumpMin and templateInfo.AutoJumpMax then botSpawn:RunScriptCode( ("activator.SetAutoJump(%d, %d)"):format(templateInfo.AutoJumpMin, templateInfo.AutoJumpMax), botSpawn, botSpawn ) else util.PrintToChatAll( "[SPAWNBOT][ERROR]: TEMPLATE HAS AUTO_JUMP BUT NO AutoJumpMin or AutoJumpMax" .. templateInfo.Class ) end elseif attribute == BOT_ATTRIBUTES.USE_BOSS_HEALTH_BAR then botSpawn.m_bUseBossHealthBar = true end end end local maxVisionRange = templateInfo.MaxVisionRange or 100000000 if templateInfo.MaxVisionRange then botSpawn:RunScriptCode((BOT_SET_MAX_VISION_RANGE_VSCRIPT):format(tostring(maxVisionRange)), botSpawn, botSpawn) end -- apply tags if templateInfo.Tags then for _, tag in pairs(templateInfo.Tags) do botSpawn:RunScriptCode((BOT_ADD_BOT_TAG_VSCRIPT):format('"' .. tostring(tag) .. '"'), botSpawn, botSpawn) end end -- apply weapon restriction botSpawn:RunScriptCode(BOT_CLEAR_RESTRICTIONS_VSCRIPT, botSpawn, botSpawn) if templateInfo.WeaponRestrictions then botSpawn:RunScriptCode( (BOT_SET_WEPRESTRICTION_VSCRIPT):format(tostring(templateInfo.WeaponRestrictions)), botSpawn, botSpawn ) end -- mission local botMission = templateInfo.Mission if not templateInfo.Mission and not templateInfo.IgnoreDefaultMission then if templateInfo.Class == "Sniper" then botMission = BOT_MISSIONS.MISSION_SNIPER elseif templateInfo.Class == "Spy" then botMission = BOT_MISSIONS.MISSION_SPY end end if botMission then botSpawn:RunScriptCode((BOT_SET_MISSION_VSCRIPT):format(tostring(botMission)), botSpawn, botSpawn) else botSpawn:RunScriptCode((BOT_SET_MISSION_VSCRIPT):format(tostring(BOT_MISSIONS.NO_MISSION)), botSpawn, botSpawn) -- if templateInfo.Attributes and TableContains(templateInfo.Attributes, BOT_ATTRIBUTES.AGGRESSIVE) then -- botSpawn:BotCommand("switch_action Mobber") -- else -- if templateInfo.Class == "Medic" then -- botSpawn:BotCommand("switch_action Medic") -- else -- botSpawn:BotCommand("switch_action FetchFlag") -- end -- end end -- stats if templateInfo.Health then botSpawn:SetAttributeValue("hidden maxhealth non buffed", templateInfo.Health - botSpawn.m_iMaxHealth) end if templateInfo.Conds then for _, id in pairs(templateInfo.Conds) do botSpawn:AddCond(id) end end local scale = templateInfo.Scale or 1 if isGiant and not templateInfo.Scale then scale = 1.75 end botSpawn:RunScriptCode(("activator.SetScaleOverride(%s)"):format(tostring(scale)), botSpawn) if templateInfo.Items then for _, itemName in pairs(templateInfo.Items) do botSpawn:GiveItem(itemName) end end local itemsDictCache = {} for _, item in pairs(botSpawn:GetAllItems()) do itemsDictCache[item:GetItemName():lower()] = item end if templateInfo.ItemAttributes then for itemName, itemAttributes in pairs(templateInfo.ItemAttributes) do local item = itemsDictCache[itemName:lower()] if item then for name, value in pairs(itemAttributes) do item:SetAttributeValue(name, value) end else print("no item found for itemName " .. itemName) end end end if templateInfo.CharacterAttributes then for name, value in pairs(templateInfo.CharacterAttributes) do botSpawn:SetAttributeValue(name, value) end end if templateInfo.Name then applyName(botSpawn, templateInfo.Name) else applyName(botSpawn, templateInfo.Class) end curSpawnId = curSpawnId + 1 botSpawn.SpawnBotId = curSpawnId if templateInfo.ClassIndex == TF_CLASS_ENGINEER then EngieBotSpawned(botSpawn, templateInfo, callbacks) end CheckSpawnTeleport(botSpawn, teamSpawnTargetName) return botSpawn end