printl("Bot Attributes Loaded") const SPELL_COOLDOWN = 5; const BOSS_SPELL_COOLDOWN = 30; const CHALICE_SPAWN_HEALTH_THRESHOLD_1 = 75000; //these ended up going unused const CHALICE_SPAWN_HEALTH_THRESHOLD_2 = 55000; const CHALICE_SPAWN_HEALTH_THRESHOLD_3 = 30000; local wavenum = 0, cooldowntime = 0, spellsgiven = 0, maxspells = 0, spawned = false; local gamerules = FindByClassname(null, "tf_gamerules"); local resource = FindByClassname(null, "tf_objective_resource"); local spell, thrower; local ctable = ["scout", "sniper", "soldier", "demo", "medic", "heavy", "pyro", "spy", "engineer"]; // local zombieindex = [5617, 5625, 5618, 5620, 5622, 5619, 5624, 5623, 5621]; // local clientcommand = Entities.CreateByClassname("point_clientcommand"); // Entities.DispatchSpawn(clientcommand); ::BotAttribs <- {} ::BotAttribs.OnGameEvent_mvm_begin_wave <- function(params) { wavenum = params.wave_index + 1 printl(format("Wave: %d", wavenum) ); //crit border for boss if (wavenum == 5) SetPropIntArray(resource, "m_nMannVsMachineWaveClassFlags", 24, 5); //kill leftover spells for (local spellbook; spellbook = FindByClassname(spellbook, "tf_spell_pickup"); ) spellbook.Kill(); } ::GiveWeapon <- function(player, classname, itemid, model) { if (model != null && (classname == "tf_wearable" || classname == "tf_wearable_demoshield" || classname == "tf_wearable_razorback")) { local wearable = Entities.CreateByClassname(classname); NetProps.SetPropInt(wearable, "m_nModelIndex", PrecacheModel(model)); NetProps.SetPropBool(wearable, "m_bValidatedAttachedEntity", true); NetProps.SetPropBool(wearable, "m_AttributeManager.m_Item.m_bInitialized", true); NetProps.SetPropEntity(wearable, "m_hOwnerEntity", player); wearable.SetOwner(player); wearable.DispatchSpawn(); EntFireByHandle(wearable, "SetParent", "!activator", 0.0, player, player); NetProps.SetPropInt(wearable, "m_fEffects", 129); // EF_BONEMERGE|EF_BONEMERGE_FASTCULL for (local i = 0; i < 7; i++) { local heldWeapon = GetPropEntityArray(player, "m_hMyWeapons", i); if (heldWeapon == null) continue; if (heldWeapon.GetSlot() != wearable.GetSlot()) continue; heldWeapon.Destroy(); SetPropEntityArray(player, "m_hMyWeapons", wearable, i); break; } return wearable; // player.Weapon_Equip(wearable); } else { local weapon = Entities.CreateByClassname(classname); // printl(weapon) SetPropInt(weapon, "m_AttributeManager.m_Item.m_iItemDefinitionIndex", itemid); SetPropBool(weapon, "m_AttributeManager.m_Item.m_bInitialized", true); SetPropBool(weapon, "m_bValidatedAttachedEntity", true); Entities.DispatchSpawn(weapon); for (local i = 0; i < 7; i++) { local heldWeapon = GetPropEntityArray(player, "m_hMyWeapons", i); if (heldWeapon == null) continue; if (heldWeapon.GetSlot() != weapon.GetSlot()) continue; heldWeapon.Destroy(); SetPropEntityArray(player, "m_hMyWeapons", weapon, i); break; } //this giveweapon function is only used for fist zombies so whatever weapon.AddAttribute("crit vs non burning players", 1, 0.0); weapon.AddAttribute("crit vs burning players", 1, 0.0); weapon.AddAttribute("crits_become_minicrits", 1, 0.0); weapon.ReapplyProvision(); if (GetPropInt(weapon, "m_AttributeManager.m_Item.m_iItemDefinitionIndex") == 939); // weapon.DisableDraw(); //EF_NODRAW bat outta hell // SetPropInt(weapon, "m_fEffects", EF_NODRAW); //neither of these work player.Weapon_Equip(weapon); return weapon; } } //moved to it's own function because of very retarded jank ::TagCheck <- function(bot) { //make front/boss bots glow if (bot.HasBotTag("bot_front") || bot.HasBotTag("bot_boss")) SetPropBool(bot, "m_bGlowEnabled", true); if (bot.HasBotTag("bot_dummy") && IsMITM()) { bot.RemoveCustomAttribute("dmg taken increased"); bot.AddCustomAttribute("health drain", -9999, -1); } switch(wavenum) { //WAVE 1 //speed boost/minicrits for w1 bots //also give them fists/bat outta hell case 1: // bot.AddCond(19) if (!GetPropBool(bot, "m_bIsMiniBoss")) { if (bot.GetPlayerClass() != TF_CLASS_PYRO) GiveWeapon(bot, "tf_weapon_fists", ITEMINDEX_FISTS, null); else GiveWeapon(bot, "tf_weapon_fireaxe", ITEMINDEX_THE_BAT_OUTTA_HELL, null); bot.AddCond(TF_COND_SPEED_BOOST); } // bot.AddCondEx(32, 5.0, null) break; //WAVE 2 //spells for pumpkin bombers case 2: if (!bot.HasBotTag("bot_spell")) return; GiveWeapon(bot, "tf_weapon_bottle", ITEMINDEX_THE_BAT_OUTTA_HELL, null); break; //WAVE 3 //spells for bowmen, resist for first bot case 3: //resist fos if (bot.HasBotTag("bot_resist")) bot.AddCond(TF_COND_RUNE_RESIST); //stays spun-up if (bot.HasBotTag("bot_altfire")) bot.PressAltFireButton(9999); break; //WAVE 4 case 4: // if (bot.HasBotAttribute(AGGRESSIVE) && bot.GetPlayerClass() == TF_CLASS_MEDIC) // EntFireByHandle(bot, "RunScriptCode", "self.RemoveBotAttribute(2)", 5, bot, bot) if (!bot.HasBotTag("bot_speedboost")) return; bot.AddCond(TF_COND_SPEED_BOOST); break; //WAVE 5 case 5: if (!bot.HasBotTag("bot_fist")) return; if (bot.GetPlayerClass() != TF_CLASS_PYRO) GiveWeapon(bot, "tf_weapon_fists", ITEMINDEX_FISTS, null); else GiveWeapon(bot, "tf_weapon_fireaxe", ITEMINDEX_THE_BAT_OUTTA_HELL, null); } } //wave-specific case switch for custom bot behavior/conds //FAT AND UGLY ::BotAttribs.OnGameEvent_post_inventory_application <- function(params) { if (!IsPlayerABot(GetPlayerFromUserID(params.userid) || GetPlayerFromUserID(params.userid).GetTeam() != TF_TEAM_PVE_INVADERS)) return; local bot = GetPlayerFromUserID(params.userid); local cstring = ctable[bot.GetPlayerClass() - 1]; //kill romevision for (local child = bot.FirstMoveChild(); child != null; child = child.NextMovePeer()) { if (child.GetClassname() == "tf_wearable" && (child.GetModelName().find("tw_medibot_") || child.GetModelName().find(format("tw_%s", cstring)) ) ) EntFireByHandle(child, "Kill", "", 0.0, null, null) } //human playermodel EntFireByHandle(bot, "SetCustomModelWithClassAnimations", format("models/player/%s.mdl", cstring), 0.0, null, null); //I don't know why and I don't want to know why //apparently, this needs a delay if EventPopFile Halloween is not active EntFireByHandle(bot, "RunScriptCode", "TagCheck(self)", 0.1, bot, bot) } //abandon all hope ye who enter here //maxspells determines how many spells are randomly distributed to bots at once //there's a 50/50 chance bots will be given a spell //if spellsgiven increments enough to reach maxspells, spellsgiven resets to 0 //after spellsgiven resets, SPELL_COOLDOWN determines the time in seconds before spells can be distributed again ::ApplySpells <- function(bot, spellbook, spellcharge, numcharges) { SetPropInt(spellbook, "m_iSelectedSpellIndex", spellcharge); //pumpkin bombs SetPropInt(spellbook, "m_iSpellCharges", numcharges); spellsgiven++; // printl(format("%s Has spell charge on %s. Spell type %d with %d charges", GetPropString(bot, "m_szNetname"), spellbook.GetClassname(), GetPropInt(spellbook, "m_iSelectedSpellIndex"), GetPropInt(spellbook, "m_iSpellCharges")); //MITM compatibility if (IsMITM()) return; spellbook.AddAttribute("is_passive_weapon", 1, 0.1); //force equip spellbook temporarily. spellbook.ReapplyProvision(); //is_passive_weapon could just be applied directly in ItemAttributes but that's ugly and breaks animations EntFireByHandle(spellbook, "RunScriptCode", "self.RemoveAttribute(`is_passive_weapon`)", 1, spellbook, spellbook); EntFireByHandle(spellbook, "RunScriptCode", "self.ReapplyProvision()", 1.1, spellbook, spellbook); } //this think function also deals with w5 boss locomotion ::SpellThink <- function() { spellsgiven = 0; for (local i = 1; i <= MAX_PLAYERS; i++) { //randomize bot selection local bot = PlayerInstanceFromIndex(RandomInt(1, MAX_PLAYERS)); // if (IsPlayerABot(bot) && wavenum == 5 && bot.HasBotTag("bot_boss")) // { // local locomotion = bot.GetLocomotionInterface(); // locomotion.Approach(Vector(0, 0, 0), 999.0); // locomotion.DriveTo(Vector(0, 0, 0)); // printl(locomotion.IsAttemptingToMove()); // } if (!IsPlayerABot(bot) || bot.GetTeam() != TF_TEAM_PVE_INVADERS) continue; if (!bot.HasBotTag("bot_spell") || cooldowntime > Time()) continue; // printl(cooldowntime + " : " + Time()) local spellbook = GetPropEntityArray(bot, "m_hMyWeapons", 3); if (spellbook == null) continue; // if (spellbook.GetSlot() != 5) return; if (spellbook != null) { local rand = RandomInt(1, 2) if (rand != 1) return; if (wavenum == 2) { ApplySpells(bot, spellbook, 3, 1) maxspells = 2 } if (wavenum == 3) { ApplySpells(bot, spellbook, RandomInt(4, 6), 1) maxspells = 4 } if (wavenum == 5) { ApplySpells(bot, spellbook, 11, 2) maxspells = 1 } } if (spellsgiven >= maxspells) { if (wavenum == 5) cooldowntime = Time() + BOSS_SPELL_COOLDOWN; else cooldowntime = Time() + SPELL_COOLDOWN; } } // printl(format("%d : %d", Time(), cooldowntime)) } //check boss health for chalice spawn ::BotAttribs.OnGameEvent_player_hurt <- function(params) { if (spawned || wavenum != 5 || !IsPlayerABot(GetPlayerFromUserID(params.userid)) || GetPlayerFromUserID(params.userid).GetTeam() != TF_TEAM_PVE_INVADERS || params.health > CHALICE_SPAWN_HEALTH_THRESHOLD_1) return; if (!GetPlayerFromUserID(params.userid).HasBotTag("bot_boss")) return; if (!spawned && params.health <= CHALICE_SPAWN_HEALTH_THRESHOLD_1); { DoEntFire("tf_gamerules", "CallScriptFunction", "SpawnBossChalices", 2.0, null, null); DoEntFire("tf_gamerules", "CallScriptFunction", "SpawnBossChalices", 4.0, null, null); DoEntFire("tf_gamerules", "CallScriptFunction", "SpawnBossChalices", 6.0, null, null); DoEntFire("tf_gamerules", "CallScriptFunction", "SpawnBossChalices", 8.0, null, null); DoEntFire("tf_gamerules", "CallScriptFunction", "SpawnBossChalices", 10.0, null, null); DoEntFire("tf_gamerules", "CallScriptFunction", "SpawnBossChalices", 12.0, null, null); DoEntFire("tf_gamerules", "CallScriptFunction", "SpawnBossChalices", 14.0, null, null); spawned = true; } } //rare chance to drop spells on death ::BotAttribs.OnGameEvent_player_death <- function(params) { if (!IsPlayerABot(GetPlayerFromUserID(params.userid) || GetPlayerFromUserID(params.userid).GetTeam() != TF_TEAM_PVE_INVADERS)) return; local bot = GetPlayerFromUserID(params.userid); // for (local child = bot.FirstMoveChild(); child != null; child = child.NextMovePeer()) // if (child.GetClassname() == "tf_wearable" && (child.GetModelName())) // print(child.GetModelName()) // EntFireByHandle(child, "Kill", "", 0.0, null, null) // for (local child = bot.FirstMoveChild(); child != null; child = child.NextMovePeer()) // { // if (child == null) continue; // if (child.GetClassname() == "tf_wearable") child.Destroy(); // } //this doesn't account for wave resets, should be done in player_spawn or post_inventory_application instead SetPropBool(bot, "m_bGlowEnabled", false); // bot.ClearAllBotTags(); if (RandomInt(1, 10) != 1) return; if (bot.GetMaxHealth() > 1200) spell = SpawnEntityFromTable("tf_spell_pickup", { origin = bot.GetLocalOrigin() TeamNum = 2 tier = 1 "OnPlayerTouch": "!self,Kill,,0,-1" }); else spell = SpawnEntityFromTable("tf_spell_pickup", {origin = bot.GetLocalOrigin() TeamNum = 2 tier = 0 "OnPlayerTouch": "!self,Kill,,0,-1" }); //spectate bots immediately, bypass the delay //we need a small delay or else red player death sounds can sometimes play // bot.ForceChangeTeam(1, false); EntFireByHandle(bot, "RunScriptCode", "self.ForceChangeTeam(1, false)", 0.1, bot, bot); } gamerules.ValidateScriptScope(); gamerules.GetScriptScope().SpellThink <- SpellThink; AddThinkToEnt(gamerules, "SpellThink"); __CollectGameEventCallbacks(::BotAttribs);