IncludeScript("popextensions/customattributes") PrecacheScriptSound("Announcer.MVM_Get_To_Upgrade") const EFL_USER = 1048576 if (!("ScriptLoadTable" in ROOT)) ::ScriptLoadTable <- {} if (!("ScriptUnloadTable" in ROOT)) ::ScriptUnloadTable <- {} ::MissionAttributes <- { CurAttrs = {} // Array storing currently modified attributes. ConVars = {} //table storing original convar values SoundsToReplace = {} ThinkTable = {} TakeDamageTable = {} TakeDamageTablePost = {} SpawnHookTable = {} DeathHookTable = {} // InitWaveTable = {} DisconnectTable = {} StartWaveTable = {} DebugText = false RaisedParseError = false PathNum = 0 // function InitWave() { // foreach (_, func in MissionAttributes.InitWaveTable) func() // foreach (attr, value in MissionAttributes.CurAttrs) printl(attr+" = "+value) // MissionAttributes.RaisedParseError = false // } function Cleanup() { MissionAttributes.ResetConvars() MissionAttributes.PathNum = 0 foreach (bot in PopExtUtil.BotArray) if (bot.GetTeam() == TF_TEAM_PVE_DEFENDERS) bot.ForceChangeTeam(TEAM_SPECTATOR, true) for (local wearable; wearable = FindByClassname(wearable, "tf_wearable");) if (wearable.GetOwner() == null || IsPlayerABot(wearable.GetOwner())) EntFireByHandle(wearable, "Kill", "", -1, null, null) MissionAttributes.DebugLog(format("Cleaned up mission attributes")) } Events = { function OnScriptHook_OnTakeDamage(params) { foreach (_, func in MissionAttributes.TakeDamageTable) func(params) } function OnGameEvent_player_hurt(params) { foreach (_, func in MissionAttributes.TakeDamageTablePost) func(params) } function OnGameEvent_player_death(params) { foreach (_, func in MissionAttributes.DeathHookTable) func(params) } function OnGameEvent_player_disconnect(params) { foreach (_, func in MissionAttributes.DisconnectTable) func(params) } function OnGameEvent_mvm_begin_wave(params) { foreach (_, func in MissionAttributes.StartWaveTable) func(params) } function OnGameEvent_player_team(params) { local player = GetPlayerFromUserID(params.userid) if (!player.IsBotOfType(1337) && params.team == TEAM_SPECTATOR && params.oldteam == TF_TEAM_PVE_INVADERS) { EntFireByHandle(player, "RunScriptCode", "PopExtUtil.ChangePlayerTeamMvM(self, TF_TEAM_PVE_INVADERS)", -1, null, null) EntFireByHandle(player, "RunScriptCode", "self.ForceRespawn()", SINGLE_TICK, null, null) } } // Hook all wave inits to reset parsing error counter. function OnGameEvent_recalculate_holidays(params) { if (GetRoundState() != GR_STATE_PREROUND) return foreach (player in PopExtUtil.PlayerArray) PopExtMain.PlayerCleanup(player) MissionAttributes.Cleanup() } function OnGameEvent_mvm_wave_complete(params) { MissionAttributes.Cleanup() } function OnGameEvent_mvm_mission_complete(params) { foreach (_, func in ScriptUnloadTable) func() MissionAttributes.ResetConvars() EntFire("_popext_missionattr_ent", "Kill") delete ::MissionAttributes } function OnGameEvent_teamplay_broadcast_audio(params) { if (MissionAttributes.SoundsToReplace.len() == 0) return if (params.sound in MissionAttributes.SoundsToReplace) { foreach (player in PopExtUtil.HumanArray) StopSoundOn(params.sound, player) if (MissionAttributes.SoundsToReplace[params.sound] == null) return EmitSoundEx({sound_name = MissionAttributes.SoundsToReplace[params.sound]}) } } } }; foreach (_, func in ScriptLoadTable) func() __CollectGameEventCallbacks(MissionAttributes.Events); // Mission Attribute Functions // ========================================================= // Function is called in popfile by mission maker to modify mission attributes. local MissionAttrEntity = FindByName(null, "_popext_missionattr_ent") if (MissionAttrEntity == null) MissionAttrEntity = SpawnEntityFromTable("info_teleport_destination", {targetname = "_popext_missionattr_ent"}); function MissionAttributes::SetConvar(convar, value, duration = 0, hideChatMessage = true) { local commentaryNode = FindByClassname(null, "point_commentary_node") if (commentaryNode == null && hideChatMessage) commentaryNode = SpawnEntityFromTable("point_commentary_node", {targetname = " IGNORE THIS ERROR \r"}) //save original values to restore later if (!(convar in MissionAttributes.ConVars)) MissionAttributes.ConVars[convar] <- Convars.GetStr(convar); if (Convars.GetStr(convar) != value) Convars.SetValue(convar, value) if (duration > 0) EntFire("tf_gamerules", "RunScriptCode", "MissionAttributes.SetConvar("+convar+","+MissionAttributes.ConVars[convar]+")", duration) if (commentaryNode != null) EntFireByHandle(commentaryNode, "Kill", "", -1, null, null) } function MissionAttributes::ResetConvars(hideChatMessage = true) { local commentaryNode = FindByClassname(null, "point_commentary_node") if (commentaryNode == null && hideChatMessage) commentaryNode = SpawnEntityFromTable("point_commentary_node", {targetname = " IGNORE THIS ERROR \r"}) foreach (convar, value in MissionAttributes.ConVars) Convars.SetValue(convar, value) MissionAttributes.ConVars.clear() if (commentaryNode != null) EntFireByHandle(commentaryNode, "Kill", "", -1, null, null) } local noromecarrier = false function MissionAttributes::MissionAttr(...) { local args = vargv local attr local value if (args.len() == 0) return else if (args.len() == 1) { attr = args[0] value = null } else { attr = args[0] value = args[1] } local success = true switch(attr) { // ========================================= // Replicates sigsegv-mvm: ForceHoliday. // Forces a tf_holiday for the mission. // Supported Holidays are: // 0 - None // 1 - Birthday // 2 - Halloween // 3 - Christmas // ========================================= case "ForceHoliday": // @param Holiday Holiday number to force. // @error TypeError If type is not an integer. // @error IndexError If invalid holiday number is passed. // Error Handling try (value.tointeger()) catch(_) {RaiseTypeError(attr, "int"); success = false; break} if (type(value) != "integer") {RaiseTypeError(attr, "int"); success = false; break} if (value < 0 || value > 11) {RaiseIndexError(attr, [0, 11]); success = false; break} // Set Holiday logic SetConvar("tf_forced_holiday", value) if (value == 0) break local ent = FindByName(null, "MissionAttrHoliday"); if (ent != null) ent.Kill(); SpawnEntityFromTable("tf_logic_holiday", { targetname = "MissionAttrHoliday", holiday_type = value }); break // ================================= // disable random crits for red bots // ================================= case "RedBotsNoRandomCrit": function MissionAttributes::RedBotsNoRandomCrit(params) { local player = GetPlayerFromUserID(params.userid) if (!player.IsBotOfType(1337) && player.GetTeam() != TF_TEAM_PVE_DEFENDERS) return PopExtUtil.AddAttributeToLoadout(player, "crit mod disabled hidden", 0) } MissionAttributes.SpawnHookTable.RedBotsNoRandomCrit <- MissionAttributes.RedBotsNoRandomCrit // ===================== // disable crit pumpkins // ===================== case "NoCrumpkins": local pumpkinIndex = PrecacheModel("models/props_halloween/pumpkin_loot.mdl") MissionAttributes.ThinkTable.NoCrumpkins <- function() { switch(value) { case 1: for (local pumpkin; pumpkin = FindByClassname(pumpkin, "tf_ammo_pack");) if (GetPropInt(pumpkin, "m_nModelIndex") == pumpkinIndex) EntFireByHandle(pumpkin, "Kill", "", -1, null, null) //should't do .Kill() in the loop, entfire kill is delayed to the end of the frame. } foreach (player in PopExtUtil.PlayerArray) if (player.InCond(TF_COND_CRITBOOSTED_PUMPKIN)) //TF_COND_CRITBOOSTED_PUMPKIN EntFireByHandle(player, "RunScriptCode", "self.RemoveCond(TF_COND_CRITBOOSTED_PUMPKIN)", -1, null, null) } break // =================== // disable reanimators // =================== case "NoReanimators": if (value < 1) return MissionAttributes.DeathHookTable.NoReanimators <- function(params) { for (local revivemarker; revivemarker = FindByClassname(revivemarker, "entity_revive_marker");) EntFireByHandle(revivemarker, "Kill", "", -1, null, null) } break // =========================== // allow standing on bot heads // =========================== case "StandableHeads": local movekeys = IN_FORWARD | IN_BACK | IN_LEFT | IN_RIGHT MissionAttributes.SpawnHookTable.StandableHeads <- function(params) { local player = GetPlayerFromUserID(params.userid) if (player.IsBotOfType(1337)) return player.GetScriptScope().PlayerThinkTable.StandableHeads <- function() { local groundent = GetPropEntity(player, "m_hGroundEntity") if (!groundent || !groundent.IsPlayer() || PopExtUtil.InButton(player, movekeys)) return player.SetAbsVelocity(Vector()) } } break // ================================================= // doesn't work until wave switches, won't work on W1 // ================================================= case "666Wavebar": MissionAttributes.StartWaveTable.EventWavebar <- function(params) { SetPropInt(PopExtUtil.ObjectiveResource, "m_nMannVsMachineWaveCount", value) } MissionAttributes.StartWaveTable.EventWavebar(null) break // =================================== // sets the wave number on the wavebar // =================================== case "WaveNum": MissionAttributes.StartWaveTable.SetWaveNum <- function(params) { SetPropInt(PopExtUtil.ObjectiveResource, "m_nMannVsMachineWaveCount", value) } MissionAttributes.StartWaveTable.SetWaveNum(null) break // ======================================= // sets the max wave number on the wavebar // ======================================= case "MaxWaveNum": MissionAttributes.StartWaveTable.SetMaxWaveNum <- function(params) { SetPropInt(PopExtUtil.ObjectiveResource, "m_nMannVsMachineMaxWaveCount", value) } MissionAttributes.StartWaveTable.SetMaxWaveNum(null) break // ========================================================= // UNFINISHED // ========================================================= case "NegativeDmgHeals": // function MissionAttributes::NegativeDmgHeals(params) { // local player = params.const_entity // local damage = params.damage // if (!player.IsPlayer() || damage > 0) return // if ((value == 2 && player.GetHealth() - damage > player.GetMaxHealth()) || //don't overheal is value is 2 // (value == 1 && player.GetHealth() - damage > player.GetMaxHealth() * 1.5)) return //don't go past max overheal if value is 1 // player.SetHealth(player.GetHealth() - damage) // } // MissionAttributes.TakeDamageTable.NegativeDmgHeals <- MissionAttributes.NegativeDmgHeals break // ============================================================== // Allows spies to place multiple sappers when item meter is full // ============================================================== case "MultiSapper": MissionAttributes.SpawnHookTable.MultiSapper <- function(params) { local player = GetPlayerFromUserID(params.userid) if (player.IsBotOfType(1337) || player.GetPlayerClass() < TF_CLASS_SPY) return player.GetScriptScope().BuiltObjectTable.MultiSapper <- function(params) { if (params.object != OBJ_ATTACHMENT_SAPPER) return local sapper = EntIndexToHScript(params.index) SetPropBool(sapper, "m_bDisposableBuilding", true) } } break // ========================================================= //all of these could just be set directly in the pop easily, however popfile's have a 4096 character limit for vscript so might as well save space case "NoRefunds": SetConvar("tf_mvm_respec_enabled", 0); break // ========================================================= case "RefundLimit": SetConvar("tf_mvm_respec_enabled", 1) SetConvar("tf_mvm_respec_limit", value) break // ========================================================= case "RefundGoal": SetConvar("tf_mvm_respec_enabled", 1) SetConvar("tf_mvm_respec_credit_goal", value) break // ========================================================= case "FixedBuybacks": SetConvar("tf_mvm_buybacks_method", 1) break // ========================================================= case "BuybacksPerWave": SetConvar("tf_mvm_buybacks_per_wave", value) break // ========================================================= case "NoBuybacks": SetConvar("tf_mvm_buybacks_method", value) SetConvar("tf_mvm_buybacks_per_wave", 0) break // ========================================================= case "DeathPenalty": SetConvar("tf_mvm_death_penalty", value) break // ========================================================= case "BonusRatioHalf": SetConvar("tf_mvm_currency_bonus_ratio_min", value) break // ========================================================= case "BonusRatioFull": SetConvar("tf_mvm_currency_bonus_ratio_max", value) break // ========================================================= case "UpgradeFile": EntFire("tf_gamerules", "SetCustomUpgradesFile", value) break // ========================================================= case "FlagEscortCount": SetConvar("tf_bot_flag_escort_max_count", value) break // ========================================================= case "BombMovementPenalty": SetConvar("tf_mvm_bot_flag_carrier_movement_penalty", value) break // ========================================================= case "MaxSkeletons": SetConvar("tf_max_active_zombie", value) break // ========================================================= case "TurboPhysics": SetConvar("sv_turbophysics", value) break // ========================================================= case "Accelerate": SetConvar("sv_accelerate", value) break // ========================================================= case "AirAccelerate": SetConvar("sv_airaccelerate", value) break // ========================================================= case "BotPushaway": SetConvar("tf_avoidteammates_pushaway", value) break // ========================================================= case "TeleUberDuration": SetConvar("tf_mvm_engineer_teleporter_uber_duration", value) break // ========================================================= case "RedMaxPlayers": SetConvar("tf_mvm_defenders_team_size", value) break // ========================================================= case "MaxVelocity": SetConvar("sv_maxvelocity", value) break // ========================================================= case "ConchHealthOnHitRegen": SetConvar("tf_dev_health_on_damage_recover_percentage", value) break // ========================================================= case "MarkForDeathLifetime": SetConvar("tf_dev_marked_for_death_lifetime", value) break // ========================================================= case "GrapplingHookEnable": SetConvar("tf_grapplinghook_enable", value) break // ========================================================= case "GiantScale": SetConvar("tf_mvm_miniboss_scale", value) break // ========================================================= case "VacNumCharges": SetConvar("weapon_medigun_resist_num_chunks", value) break // ========================================================= case "DoubleDonkWindow": SetConvar("tf_double_donk_window", value) break // ========================================================= case "ConchSpeedBoost": SetConvar("tf_whip_speed_increase", value) break // ========================================================= case "StealthDmgReduction": SetConvar("tf_stealth_damage_reduction", value) break // ========================================================= case "FlagCarrierCanFight": SetConvar("tf_mvm_bot_allow_flag_carrier_to_fight", value) break // ========================================================= case "HHHChaseRange": SetConvar("tf_halloween_bot_chase_range", value) break // ========================================================= case "HHHAttackRange": SetConvar("tf_halloween_bot_attack_range", value) break // ========================================================= case "HHHQuitRange": SetConvar("tf_halloween_bot_quit_range", value) break // ========================================================= case "HHHTerrifyRange": SetConvar("tf_halloween_bot_terrify_radius", value) break // ========================================================= case "HHHHealthBase": SetConvar("tf_halloween_bot_health_base", value) break // ========================================================= case "HHHHealthPerPlayer": SetConvar("tf_halloween_bot_health_per_player", value) break // ========================================================= case "SentryHintBombForwardRange": SetConvar("tf_bot_engineer_mvm_sentry_hint_bomb_forward_range", value) break // ========================================================= case "SentryHintBombBackwardRange": SetConvar("tf_bot_engineer_mvm_sentry_hint_bomb_backward_range", value) break // ========================================================= case "SentryHintMinDistanceFromBomb": SetConvar("tf_bot_engineer_mvm_hint_min_distance_from_bomb", value) break // ========================================================= case "NoBusterFF": if (value != 1 || value != 0 ) RaiseIndexError(attr) SetConvar("tf_bot_suicide_bomb_friendly_fire", value = 1 ? 0 : 1) break // ===================== // Disable sniper lasers // ===================== case "SniperHideLasers": if (value < 1) return function MissionAttributes::SniperHideLasers() { for (local dot; dot = FindByClassname(dot, "env_sniperdot");) if (dot.GetOwner().GetTeam() == TF_TEAM_PVE_INVADERS) EntFireByHandle(dot, "Kill", "", -1, null, null) return -1; } MissionAttributes.ThinkTable.SniperHideLasers <- MissionAttributes.SniperHideLasers break // =================================== // lose wave when all players are dead // =================================== case "TeamWipeWaveLoss": MissionAttributes.DeathHookTable.TeamWipeWaveLoss <- function(params) { if (!PopExtUtil.IsWaveStarted) return EntFire("tf_gamerules", "RunScriptCode", "if (PopExtUtil.CountAlivePlayers() == 0) EntFire(`__utilroundwin`, `RoundWin`)") } break // ================================================================================= // change sentry kill count per mini-boss kill. -4 will make giants count as 1 kill // ================================================================================= case "GiantSentryKillCountOffset": MissionAttributes.DeathHookTable.GiantSentryKillCount <- function(params) { local sentry = EntIndexToHScript(params.inflictor_entindex) local victim = GetPlayerFromUserID(params.userid) if (sentry == null) return if (sentry.GetClassname() != "obj_sentrygun" || !victim.IsMiniBoss()) return local kills = GetPropInt(sentry, "m_iKills") SetPropInt(sentry, "m_iKills", kills + value) } break // ======================================================================== // set reset time for flags (bombs). // accepts a key/value table of flag targetnames and their new return times // can also just accept a float value to apply to all flags // ======================================================================== case "FlagResetTime": MissionAttributes.FlagResetTime <- function() { for (local flag; flag = FindByClassname(flag, "item_teamflag");) { if (typeof value == "table") foreach (k, v in value) EntFire(k, "SetReturnTime", v) else if (typeof value == "integer" || typeof value == "float") EntFire("item_teamflag", "SetReturnTime", value) } } MissionAttributes.FlagResetTime() break // =========================== // enable bot headshots (jank) // =========================== case "BotHeadshots": if (value < 1) return MissionAttributes.TakeDamageTable.BotHeadshots <- function(params) { local player = params.attacker, victim = params.const_entity victim.ValidateScriptScope() local scope = victim.GetScriptScope() local wep = params.weapon //re-enable headshots for snipers and ambassador if ( !player.IsPlayer() || !victim.IsPlayer() || //check if non-bot victim player.GetPlayerClass() != TF_CLASS_SPY && player.GetPlayerClass() != TF_CLASS_SNIPER || //check if we're spy/sniper GetPropInt(victim, "m_LastHitGroup") != HITGROUP_HEAD || //check for headshot player.GetPlayerClass() == TF_CLASS_SNIPER && (wep.GetSlot() == SLOT_SECONDARY || PopExtUtil.GetItemIndex(wep) == ID_SYDNEY_SLEEPER) || //ignore sydney sleeper and SMGs GetPropFloat(wep, "m_flChargedDamage") < 150.0 && PopExtUtil.GetItemIndex(wep) == ID_CLASSIC || //check classic charge player.GetPlayerClass() == TF_CLASS_SPY && PopExtUtil.GetItemIndex(wep) != ID_AMBASSADOR || // check for ambassador victim.InCond(TF_COND_BLEEDING) && GetPropInt(victim, "m_iStunFlags") & TF_STUN_MOVEMENT //check for explosive headshot victim. ) return params.damage_type = params.damage_type | DMG_CRITICAL //DMG_USE_HITLOCATIONS doesn't actually work here, no headshot icon. params.damage_stats = TF_DMG_CUSTOM_HEADSHOT } break // ============================================================== // Uses bitflags to enable certain behavior // 1 = Robot animations (excluding sticky demo and jetpack pyro) // 2 = Human animations // 4 = Enable footstep sfx // 8 = Enable voicelines (WIP) // 16 = Enable viewmodels (WIP) // ============================================================== case "PlayersAreRobots": // TODO: Make PlayersAreRobots 16 and HandModelOverride incompatible // Doesn't work /* ScriptLoadTable.PlayersAreRobotsReset <- function() { DoEntFire("__bot_bonemerge_model", "Kill", "", -1, null, null) printl("TEST TEST TEST") foreach (player in PopExtUtil.HumanArray) { player.ValidateScriptScope() local scope = player.GetScriptScope() EntFireByHandle(player, "SetCustomModelWithClassAnimations", format("models/player/%s.mdl", PopExtUtil.Classes[player.GetPlayerClass()]), -1, null, null) SetPropInt(player, "m_clrRender", 0xFFFFFF) SetPropInt(player, "m_nRenderMode", 0) } delete ScriptLoadTable.PlayersAreRobotsReset } */ MissionAttributes.SpawnHookTable.PlayersAreRobots <- function(params) { local player = GetPlayerFromUserID(params.userid) if (player.IsBotOfType(1337)) return player.ValidateScriptScope() local scope = player.GetScriptScope() if ("wearable" in scope && scope.wearable != null) { scope.wearable.Destroy() scope.wearable <- null } local playerclass = player.GetPlayerClass() local class_string = PopExtUtil.Classes[playerclass] local model = format("models/bots/%s/bot_%s.mdl", class_string, class_string) if (value & 1) { //sticky anims and thruster anims are particularly problematic if (PopExtUtil.HasItemInLoadout(player, "tf_weapon_pipebomblauncher") || PopExtUtil.HasItemInLoadout(player, ID_THERMAL_THRUSTER)) { PopExtUtil.PlayerRobotModel(player, model) return } EntFireByHandle(player, "SetCustomModelWithClassAnimations", model, 1, null, null) PopExtUtil.SetEntityColor(player, 255, 255, 255, 255) SetPropInt(player, "m_nRenderMode", kRenderFxNone) //dangerous constant name lol } if (value & 2) { if (value & 1) value = value & 1 //incompatible flags PopExtUtil.PlayerRobotModel(player, model) } if (value & 4) { scope.stepside <- GetPropInt(player, "m_Local.m_nStepside") function StepThink() { if (self.GetPlayerClass() == TF_CLASS_MEDIC) return if (GetPropInt(self,"m_Local.m_nStepside") != stepside) EmitSoundOn("MVM.BotStep", self) scope.stepside = GetPropInt(self,"m_Local.m_nStepside") return -1 } if (!("StepThink" in scope.PlayerThinkTable)) scope.PlayerThinkTable.StepThink <- StepThink } else if ("StepThink" in scope.PlayerThinkTable) delete scope.PlayerThinkTable.StepThink if (value & 8) { MissionAttributes.ThinkTable.RobotVOThink <- function() { for (local ent; ent = FindByClassname(ent, "instanced_scripted_scene"); ) { if (ent.GetEFlags() & EFL_USER) continue ent.AddEFlags(EFL_USER) local owner = GetPropEntity(ent, "m_hOwner") if (owner != null && !owner.IsBotOfType(1337)) { local vcdpath = GetPropString(ent, "m_szInstanceFilename"); if (!vcdpath || vcdpath == "") return -1 local dotindex = vcdpath.find(".") local slashindex = null; for (local i = dotindex; i >= 0; --i) { if (vcdpath[i] == '/' || vcdpath[i] == '\\') { slashindex = i break } } owner.ValidateScriptScope() local scope = owner.GetScriptScope() scope.soundtable <- VCD_SOUNDSCRIPT_MAP[owner.GetPlayerClass()] scope.vcdname <- vcdpath.slice(slashindex+1, dotindex) if (scope.vcdname in scope.soundtable) { local soundscript = scope.soundtable[scope.vcdname]; if (typeof soundscript == "string") PopExtUtil.StopAndPlayMVMSound(owner, soundscript, 0); else if (typeof soundscript == "array") foreach (sound in soundscript) PopExtUtil.StopAndPlayMVMSound(owner, sound[1], sound[0]); } } } return -1; } } else if ("RobotVOThink" in scope.PlayerThinkTable) delete scope.PlayerThinkTable.RobotVOThink if (value & 16) { if ("HandModelOverride" in MissionAttributes.SpawnHookTable) return local vmodel = PopExtUtil.ROBOT_ARM_PATHS[player.GetPlayerClass()] local playervm = GetPropEntity(player, "m_hViewModel") playervm.GetOrigin() if (playervm == null) return if (playervm.GetModelName() != vmodel) playervm.SetModelSimple(vmodel) for (local i = 0; i < SLOT_COUNT; i++) { local wep = GetPropEntityArray(player, "m_hMyWeapons", i) if (wep == null || wep.GetModelName() == vmodel) continue wep.SetModelSimple(vmodel) wep.SetCustomViewModel(vmodel) } } else if ("RobotArmThink" in scope.PlayerThinkTable) delete scope.PlayerThinkTable.RobotArmThink } break // ======================================================= // Uses bitflags to change behavior: // 1 = Blu bots use human models. // 2 = Blu bots use zombie models. Overrides human models. // 4 = Red bots use human models. // 4 = Red bots use zombie models. Overrides human models. // ======================================================= case "BotsAreHumans": MissionAttributes.SpawnHookTable.BotsAreHumans <- function(params) { local player = GetPlayerFromUserID(params.userid) if (!player.IsBotOfType(1337)) return if (player.GetTeam() == TF_TEAM_PVE_INVADERS && value & 1) { EntFireByHandle(player, "SetCustomModelWithClassAnimations", format("models/player/%s.mdl", PopExtUtil.Classes[player.GetPlayerClass()]), -1, null, null) if (value & 2) activator.GenerateAndWearItem(format("Zombie %s",PopExtUtil.Classes[player.GetPlayerClass()])) } if (player.GetTeam() == TF_TEAM_PVE_DEFENDERS && value & 4) { EntFireByHandle(player, "SetCustomModelWithClassAnimations", format("models/player/%s.mdl", PopExtUtil.Classes[player.GetPlayerClass()]), -1, null, null) if (value & 8) activator.GenerateAndWearItem(format("Zombie %s",PopExtUtil.Classes[player.GetPlayerClass()])) } } break // ============================================================== // 1 = disables romevision on bots 2 = disables rome carrier tank // ============================================================== case "NoRome": local carrierPartsIndex = GetModelIndex("models/bots/boss_bot/carrier_parts.mdl") MissionAttributes.SpawnHookTable.NoRome <- function(params) { local bot = GetPlayerFromUserID(params.userid) EntFireByHandle(bot, "RunScriptCode", @" if (self.IsBotOfType(1337)) // if (!self.HasBotTag(`popext_forceromevision`)) //handle these elsewhere for (local child = self.FirstMoveChild(); child != null; child = child.NextMovePeer()) if (child.GetClassname() == `tf_wearable` && startswith(child.GetModelName(), `models/workshop/player/items/`+PopExtUtil.Classes[self.GetPlayerClass()]+`/tw`)) EntFireByHandle(child, `Kill`, ``, -1, null, null) ", -1, null, null) if (value < 2 || noromecarrier) return local carrier = FindByName(null, "botship_dynamic") //some maps have a targetname for it if (carrier == null) { for (local props; props = FindByClassname(props, "prop_dynamic");) { if (GetPropInt(props, "m_nModelIndex") != carrierPartsIndex) continue carrier = props break } } SetPropIntArray(carrier, "m_nModelIndexOverrides", carrierPartsIndex, 3) noromecarrier = true } break // ========================================================= case "SpellRateCommon": SetConvar("tf_spells_enabled", 1) MissionAttributes.DeathHookTable.SpellRateCommon <- function(params) { if (RandomFloat(0, 1) > value) return local bot = GetPlayerFromUserID(params.userid) if (!bot.IsBotOfType(1337) || bot.IsMiniBoss()) return local spell = SpawnEntityFromTable("tf_spell_pickup", {targetname = "_commonspell" origin = bot.GetLocalOrigin() TeamNum = 2 tier = 0 "OnPlayerTouch": "!self,Kill,,0,-1" }) } break // ========================================================= case "SpellRateGiant": SetConvar("tf_spells_enabled", 1) MissionAttributes.DeathHookTable.SpellRateGiant <- function(params) { if (RandomFloat(0, 1) > value) return local bot = GetPlayerFromUserID(params.userid) if (!bot.IsBotOfType(1337) || !bot.IsMiniBoss()) return local spell = SpawnEntityFromTable("tf_spell_pickup", {targetname = "_giantspell" origin = bot.GetLocalOrigin() TeamNum = 2 tier = 0 "OnPlayerTouch": "!self,Kill,,0,-1" }) } break // ========================================================= case "RareSpellRateCommon": SetConvar("tf_spells_enabled", 1) MissionAttributes.DeathHookTable.RareSpellRateCommon <- function(params) { if (RandomFloat(0, 1) > value) return local bot = GetPlayerFromUserID(params.userid) if (!bot.IsBotOfType(1337) || bot.IsMiniBoss()) return local spell = SpawnEntityFromTable("tf_spell_pickup", {targetname = "_commonspell" origin = bot.GetLocalOrigin() TeamNum = 2 tier = 1 "OnPlayerTouch": "!self,Kill,,0,-1" }) } break // ========================================================= case "RareSpellRateGiant": SetConvar("tf_spells_enabled", 1) MissionAttributes.DeathHookTable.RareSpellRateGiant <- function(params) { if (RandomFloat(0, 1) > value) return local bot = GetPlayerFromUserID(params.userid) if (!bot.IsBotOfType(1337) || !bot.IsMiniBoss()) return local spell = SpawnEntityFromTable("tf_spell_pickup", {targetname = "_giantspell" origin = bot.GetLocalOrigin() TeamNum = 2 tier = 1 "OnPlayerTouch": "!self,Kill,,0,-1" }) } break // =========================================================================================== //skeleton's spawned by bots or tf_zombie entities will no longer split into smaller skeletons // =========================================================================================== case "NoSkeleSplit": MissionAttributes.ThinkTable.NoSkeleSplit <- function() { //kill skele spawners before they split from tf_zombie_spawner for (local skelespell; skelespell = FindByClassname(skelespell, "tf_projectile_spellspawnzombie"); ) if (GetPropEntity(skelespell, "m_hThrower") == null) EntFireByHandle(skelespell, "Kill", "", -1, null, null) // m_hThrower does not change when the skeletons split for spell-casted skeles, just need to kill them after spawning for (local skeles; skeles = FindByClassname(skeles, "tf_zombie"); ) { //kill blu split skeles if (skeles.GetModelScale() == 0.5 && (skeles.GetOwner() == null || skeles.GetOwner().IsBotOfType(1337))) { EntFireByHandle(skeles, "Kill", "", -1, null, null) return } // if (skeles.GetTeam() == 5) { // skeles.SetTeam(TF_TEAM_PVE_INVADERS) // skeles.SetSkin(1) // } } } break // ==================================================================== // ready-up countdown time. Values below 9 will disable starting music // ==================================================================== case "WaveStartCountdown": MissionAttributes.ThinkTable.WaveStartCountdown <- function() { if (PopExtUtil.IsWaveStarted) return local roundtime = GetPropFloat(PopExtUtil.GameRules, "m_flRestartRoundTime") if (roundtime > Time() + value) { local ready = PopExtUtil.GetPlayerReadyCount() if (ready >= PopExtUtil.HumanArray.len() || (roundtime <= 12.0)) SetPropFloat(PopExtUtil.GameRules, "m_flRestartRoundTime", Time() + value) } } break // ======================================================================================================================= // array of arrays with xyz values to spawn path_tracks at // path_track names are ordered based on where they are listed in the array // example: //`ExtraTankPath`: [ // [`686 4000 392`, `667 4358 390`, `378 4515 366`, `-193 4250 289`], // starting node: extratankpath1_1 // [`640 5404 350`, `640 4810 350`, `640 4400 550`, `1100 4100 650`, `1636 3900 770`] //starting node: extratankpath2_1 //] // the last track in the list will have _lastnode appended to the targetname // ====================================================================================================================== case "ExtraTankPath": if (typeof value != "array") { MissionAttributes.RaiseValueError("ItemWhitelist", value, "Value must be array") success = false break } if (!("ExtraTankPathTracks" in MissionAttributes)) MissionAttributes.ExtraTankPathTracks <- [] foreach (path in value) { local tracks = [] MissionAttributes.PathNum++ foreach (i, pos in path) { local org = split(pos, " ") local track = SpawnEntityFromTable("path_track", { targetname = format("extratankpath%d_%d", MissionAttributes.PathNum, i+1) origin = Vector(org[0].tointeger(), org[1].tointeger(), org[2].tointeger()) }) tracks.append(track) } local lastnode = tracks[tracks.len() - 1] PopExtUtil.SetTargetname(lastnode, format("%s_lastnode", GetPropString(lastnode, "m_iName"))) tracks.append(null) //dummy value to put at the end for (local i = 0; i < tracks.len() - 1; i++) if (tracks[i] != null) SetPropEntity(tracks[i], "m_pnext", tracks[i+1]) } break // ======================================= // replace viewmodel arms with custom ones // ======================================= case "HandModelOverride": MissionAttributes.SpawnHookTable.HandModelOverride <- function(params) { local player = GetPlayerFromUserID(params.userid) if (player.IsBotOfType(1337)) return player.ValidateScriptScope() local scope = player.GetScriptScope() scope.PlayerThinkTable.ArmThink <- function() { local tfclass = player.GetPlayerClass() local class_string = PopExtUtil.Classes[tfclass] local vmodel = null local playervm = GetPropEntity(player, "m_hViewModel") if (typeof value == "string") vmodel = PopExtUtil.StringReplace(value, "%class", class_string); else if (typeof value == "array") { if (value.len() == 0) return if (tfclass >= value.len()) vmodel = PopExtUtil.StringReplace(value[0], "%class", class_string); else vmodel = value[tfclass] } else { // do we need to do anything special for thinks?? MissionAttributes.RaiseValueError("HandModelOverride", value, "Value must be string or array of strings") return } if (vmodel == null) return if (playervm.GetModelName() != vmodel) playervm.SetModelSimple(vmodel) for (local i = 0; i < SLOT_COUNT; i++) { local wep = GetPropEntityArray(player, "m_hMyWeapons", i) if (wep == null || (wep.GetModelName() == vmodel)) continue wep.SetModelSimple(vmodel) wep.SetCustomViewModel(vmodel) } } } break // =========================================================== // add cond to every player on spawn with an optional duration // =========================================================== case "AddCond": MissionAttributes.SpawnHookTable.AddCond <- function(params) { local player = GetPlayerFromUserID(params.userid) if (player.IsBotOfType(1337)) return if (typeof value == "array") { player.RemoveCondEx(value[0], true) EntFireByHandle(player, "RunScriptCode", format("self.AddCondEx(%d, %f, null)", value[0], value[1]), -1, null, null) } else if (typeof value == "integer") { player.RemoveCond(value) EntFireByHandle(player, "RunScriptCode", format("self.AddCond(%d)", value), -1, null, null) } } break // ====================================================== // add/modify player attributes, can be filtered by class // ====================================================== case "PlayerAttributes": //setting maxhealth attribs doesn't update current HP local healthattribs = { "max health additive bonus" : null, "max health additive penalty": null, "SET BONUS: max health additive bonus": null, "hidden maxhealth non buffed": null, } MissionAttributes.SpawnHookTable.PlayerAttributes <- function(params) { local player = GetPlayerFromUserID(params.userid) if (player.IsBotOfType(1337)) return if (typeof value != "table") { MissionAttributes.RaiseValueError("PlayerAttributes", value, "Value must be table") success = false return } local tfclass = player.GetPlayerClass() foreach (k, v in value) { if (typeof k == "string" && (typeof v == "integer" || typeof v == "float")) if (k in CustomAttributes.Attrs) CustomAttributes.AddAttr(player, k, v, player.GetActiveWeapon()) else EntFireByHandle(player, "RunScriptCode", format("self.AddCustomAttribute(`%s`, %1.8e, -1)", k, v.tofloat()), -1, null, null) if (!(tfclass in value)) continue local table = value[tfclass] foreach (k, v in table) { if (k in CustomAttributes.Attrs && !player.IsBotOfType(1337)) CustomAttributes.AddAttr(player, k, v, player.GetActiveWeapon()) else { local valformat = "" if (typeof v == "integer") valformat = format("self.AddCustomAttribute(`%s`, %d, -1)", k, v) else if (typeof v == "float") valformat = format("self.AddCustomAttribute(`%s`, %f, -1)", k, v) else if (typeof v == "string") { MissionAttributes.RaiseValueError("PlayerAttributes", v, "Cannot set string attributes!") success = false continue } EntFireByHandle(player, "RunScriptCode", valformat, -1, null, null) if (k in healthattribs) EntFireByHandle(player, "RunScriptCode", "self.SetHealth(self.GetMaxHealth())", -1, null, null) } } } } break // ====================================================================== // add/modify item attributes, can be filtered by item index or classname // ====================================================================== case "ItemAttributes": MissionAttributes.SpawnHookTable.ItemAttributes <- function(params) { local player = GetPlayerFromUserID(params.userid) if (player.IsBotOfType(1337)) return if (typeof value != "table") { MissionAttributes.RaiseValueError("ItemAttributes", value, "Value must be table") success = false return } foreach(k, v in value) { local wep = PopExtUtil.HasItemInLoadout(player, k) if (wep == null) continue foreach (a, b in v) { if (a in CustomAttributes.Attrs) CustomAttributes.AddAttr(player, a, b, wep) else { wep.AddAttribute(a, b, -1) wep.ReapplyProvision() } } } } break // ========================================================= // TODO: once we have our own giveweapon functions, finish this case "LoadoutControl": MissionAttributes.SpawnHookTable.LoadoutControl <- function(params) { local player = GetPlayerFromUserID(params.userid) if (player.IsBotOfType(1337)) return player.ValidateScriptScope() local scope = player.GetScriptScope() function HasVal(arr, val) foreach (v in arr) if (v == val) return true function IsInMultiList(arr, val) { if (arr.len() <= 0) return false local in_list = false foreach (a in arr) { if (HasVal(a, val)) { in_list = true break } } return in_list } for (local i = 0; i < SLOT_COUNT; ++i) { local wep = GetPropEntityArray(player, "m_hMyWeapons", i) if (wep == null) continue local tfclass = PopExtUtil.Classes[player.GetPlayerClass()] local slot = PopExtUtil.Slots[i] local index = PopExtUtil.GetItemIndex(wep) local cls = wep.GetClassname() local whitelists = [] local tables = [] tables.insert(0, value) if ("Whitelist" in value) whitelists.insert(0, value.Whitelist) if (tfclass in value) { tables.insert(0, value[tfclass]) if ("Whitelist" in value[tfclass]) whitelists.insert(0, value[tfclass].Whitelist) } if (tfclass in value && slot in value[tfclass]) { tables.insert(0, value[tfclass][slot]) if ("Whitelist" in value[tfclass][slot]) whitelists.insert(0, value[tfclass][slot].Whitelist) } if (whitelists.len() > 0) { local in_whitelist = IsInMultiList(whitelists, index) || IsInMultiList(whitelists, cls) if (!in_whitelist) { wep.Kill() continue } } foreach (table in tables) { local identifiers = [index, cls] local full_break = false foreach (id in identifiers) { if (id in table) { local value = table[id] if (value == null) { wep.Kill() full_break = true break } else if (value == "") { printl("GIVE STOCK ITEM, check with IsInMultiList") } else if (typeof value == "string" && value.len() > 0) { printl("INVALID VALUE "+value+ "FOR k: "+id) continue } else { try { local value = value.tointeger() printl("REPLACE ITEM WITH ITEMINDEX "+value) } catch (e) {} } } } if (full_break) break } } EntFireByHandle(player, "RunScriptCode", "PopExtUtil.SwitchToFirstValidWeapon(self)", SINGLE_TICK, null, null) } break case "SoundOverrides": if (typeof value != "table") MissionAttributes.RaiseValueError("SoundOverrides", value, "value must be a table") foreach (sound, override in value) if (override != null) MissionAttributes.SoundsToReplace[sound] <- override //catch-all for disabling non teamplay_broadcast_audio sfx MissionAttributes.ThinkTable.DisableSounds <- function() { foreach (sound, override in value) { if (override == null) { foreach (player in PopExtUtil.HumanArray) { StopSoundOn(sound, player) player.StopSound(sound) } } } } break case "NoThrillerTaunt": MissionAttributes.SpawnHookTable.NoThrillerTaunt <- function(params) { local player = GetPlayerFromUserID(params.userid) player.ValidateScriptScope() player.GetScriptScope().PlayerThinkTable.NoThrillerTaunt <- function() { if (self.IsTaunting()) { for (local scene; scene = FindByClassname(scene, "instanced_scripted_scene");) { local owner = GetPropEntity(scene, "m_hOwner"); if (owner == self) { local name = GetPropString(scene, "m_szInstanceFilename"); local thriller_name = self.GetPlayerClass() == TF_CLASS_MEDIC ? "taunt07" : "taunt06"; if (name.find(thriller_name) != null) { scene.Kill(); self.RemoveCond(TF_COND_TAUNTING); self.Taunt(TAUNT_BASE_WEAPON, 0); break; } } } } } } break // ===================================== // uses bitflags to enable random crits: // 1 - Blue humans // 2 - Blue robots // 4 - Red robots // ===================================== case "EnableRandomCrits": if (value == 0.0) return local user_chance = (args.len() > 2) ? args[2] : null // Simplified rare high moments local base_ranged_crit_chance = 0.0005 local max_ranged_crit_chance = 0.0020 local base_melee_crit_chance = 0.15 local max_melee_crit_chance = 0.60 // 4 kills to reach max chance local timed_crit_weapons = { "tf_weapon_handgun_scout_secondary" : null, "tf_weapon_pistol_scout" : null, "tf_weapon_flamethrower" : null, "tf_weapon_minigun" : null, "tf_weapon_pistol" : null, "tf_weapon_syringegun_medic" : null, "tf_weapon_smg" : null, "tf_weapon_charged_smg" : null, } local no_crit_weapons = { "tf_weapon_laser_pointer" : null, "tf_weapon_medigun" : null, "tf_weapon_sniperrifle" : null, } MissionAttributes.ThinkTable.EnableRandomCritsThink <- function() { if (!PopExtUtil.IsWaveStarted) return -1 foreach (player in PopExtUtil.PlayerArray) { if (!( (value & 1 && player.GetTeam() == TF_TEAM_PVE_INVADERS && !player.IsBotOfType(1337)) || (value & 2 && player.GetTeam() == TF_TEAM_PVE_INVADERS && player.IsBotOfType(1337)) || (value & 4 && player.GetTeam() == TF_TEAM_PVE_DEFENDERS && player.IsBotOfType(1337)) )) continue player.ValidateScriptScope() local scope = player.GetScriptScope() if (!("crit_weapon" in scope)) scope.crit_weapon <- null if (!("ranged_crit_chance" in scope) || !("melee_crit_chance" in scope)) { scope.ranged_crit_chance <- base_ranged_crit_chance scope.melee_crit_chance <- base_melee_crit_chance } if (!PopExtUtil.IsAlive(player) || player.GetTeam() == TEAM_SPECTATOR) continue local wep = player.GetActiveWeapon() local index = PopExtUtil.GetItemIndex(wep) local classname = wep.GetClassname() // Lose the crits if we switch weapons if (scope.crit_weapon != null && scope.crit_weapon != wep) player.RemoveCond(TF_COND_CRITBOOSTED_CTF_CAPTURE) // Wait for bot to use its crits if (scope.crit_weapon != null && player.InCond(TF_COND_CRITBOOSTED_CTF_CAPTURE)) continue // We handle melee weapons elsewhere in OnTakeDamage if (wep == null || wep.IsMeleeWeapon()) continue // Certain weapon types never receive random crits if (classname in no_crit_weapons || wep.GetSlot() > 2) continue // Ignore weapons with certain attributes // if (wep.GetAttribute("crit mod disabled", 1) == 0 || wep.GetAttribute("crit mod disabled hidden", 1) == 0) continue local crit_chance_override = (user_chance > 0) ? user_chance : null local chance_to_use = (crit_chance_override != null) ? crit_chance_override : scope.ranged_crit_chance // Roll for random crits if (RandomFloat(0, 1) < chance_to_use) { player.AddCond(TF_COND_CRITBOOSTED_CTF_CAPTURE) scope.crit_weapon <- wep // Detect weapon fire to remove our crits wep.ValidateScriptScope() wep.GetScriptScope().last_fire_time <- Time() wep.GetScriptScope().Think <- function() { local fire_time = NetProps.GetPropFloat(self, "m_flLastFireTime"); if (fire_time > last_fire_time) { local owner = self.GetOwner() owner.RemoveCond(TF_COND_CRITBOOSTED_CTF_CAPTURE) owner.ValidateScriptScope() local scope = owner.GetScriptScope() // Continuous fire weapons get 2 seconds of crits once they fire if (classname in timed_crit_weapons) { owner.AddCondEx(TF_COND_CRITBOOSTED_CTF_CAPTURE, 2, null) EntFireByHandle(owner, "RunScriptCode", format("crit_weapon <- null; ranged_crit_chance <- %f", base_ranged_crit_chance), 2, null, null) } else { scope.crit_weapon <- null scope.ranged_crit_chance <- base_ranged_crit_chance } NetProps.SetPropString(self, "m_iszScriptThinkFunction", "") } return -1 } AddThinkToEnt(wep, "Think") } } } MissionAttributes.DeathHookTable.EnableRandomCritsKill <- function(params) { local attacker = GetPlayerFromUserID(params.attacker) if (attacker == null || !attacker.IsBotOfType(1337)) return attacker.ValidateScriptScope() local scope = attacker.GetScriptScope() if (!("ranged_crit_chance" in scope) || !("melee_crit_chance" in scope)) return if (scope.ranged_crit_chance + base_ranged_crit_chance > max_ranged_crit_chance) scope.ranged_crit_chance <- max_ranged_crit_chance else scope.ranged_crit_chance <- scope.ranged_crit_chance + base_ranged_crit_chance if (scope.melee_crit_chance + base_melee_crit_chance > max_melee_crit_chance) scope.melee_crit_chance <- max_melee_crit_chance else scope.melee_crit_chance <- scope.melee_crit_chance + base_melee_crit_chance } MissionAttributes.TakeDamageTable.EnableRandomCritsTakeDamage <- function(params) { if (!("inflictor" in params)) return local attacker = params.inflictor if (attacker == null || !attacker.IsPlayer() || !attacker.IsBotOfType(1337)) return attacker.ValidateScriptScope() local scope = attacker.GetScriptScope() if (!("melee_crit_chance" in scope)) return // Already a crit if (params.damage_type & DMG_CRITICAL) return // Only Melee weapons local wep = attacker.GetActiveWeapon() if (!wep.IsMeleeWeapon()) return // Certain weapon types never receive random crits if (attacker.GetPlayerClass() == TF_CLASS_SPY) return // Ignore weapons with certain attributes // if (wep.GetAttribute("crit mod disabled", 1) == 0 || wep.GetAttribute("crit mod disabled hidden", 1) == 0) return // Roll our crit chance if (RandomFloat(0, 1) < scope.melee_crit_chance) { params.damage_type = params.damage_type | DMG_CRITICAL // We delay here to allow death code to run so the reset doesn't get overriden EntFireByHandle(attacker, "RunScriptCode", format("melee_crit_chance <- %f", base_melee_crit_chance), SINGLE_TICK, null, null) } } break // ======================================= // 1 = enables basic Reverse MvM behavior // 2 = blu players cannot pick up bombs // 4 = blu players have infinite ammo // 8 = blu spies have infinite cloak // 16 = blu players have spawn protection // 32 = blu players cannot attack in spawn // 64 = remove blu footsteps // ======================================= case "ReverseMVM": // Prevent bots on red team from hogging slots so players can always join and get switched to blue // TODO: Needs testing // also need to reset it //MissionAttributes.SetConvar("tf_mvm_defenders_team_size", 999) MissionAttributes.ThinkTable.ReverseMVMThink <- function() { // Enforce max team size local player_count = 0 local max_team_size = 6 foreach (player in PopExtUtil.HumanArray) { if (player_count + 1 > max_team_size && player.GetTeam() != TEAM_SPECTATOR && !player.IsBotOfType(1337)) { player.ForceChangeTeam(TEAM_SPECTATOR, false) continue } player_count++ } // Readying up starts the round if (!PopExtUtil.IsWaveStarted) { local ready = PopExtUtil.GetPlayerReadyCount() if (ready > 0 && ready >= PopExtUtil.HumanArray.len() && !("WaveStartCountdown" in MissionAttributes) && GetPropFloat(PopExtUtil.GameRules, "m_flRestartRoundTime") >= Time() + 12.0) SetPropFloat(PopExtUtil.GameRules, "m_flRestartRoundTime", Time() + 10.0) } } MissionAttributes.SpawnHookTable.ReverseMVMSpawn <- function(params) { local player = GetPlayerFromUserID(params.userid) if (player.IsBotOfType(1337)) return player.ValidateScriptScope() local scope = player.GetScriptScope() if ("ReverseMVMCurrencyThink" in scope.PlayerThinkTable) delete scope.PlayerThinkTable.ReverseMVMCurrencyThink if ("ReverseMVMPackThink" in scope.PlayerThinkTable) delete scope.PlayerThinkTable.ReverseMVMPackThink if ("ReverseMVMLaserThink" in scope.PlayerThinkTable) delete scope.PlayerThinkTable.ReverseMVMLaserThink if ("ReverseMVMDrainAmmoThink" in scope.PlayerThinkTable) delete scope.PlayerThinkTable.ReverseMVMDrainAmmoThink // Switch to blue team // TODO: Need to fix players getting stuck in spec on wave fail, mission complete, etc if (player.GetTeam() != TF_TEAM_PVE_INVADERS) { EntFireByHandle(player, "RunScriptCode", "PopExtUtil.ChangePlayerTeamMvM(self, TF_TEAM_PVE_INVADERS)", SINGLE_TICK, null, null) EntFireByHandle(player, "RunScriptCode", "self.ForceRespawn()", SINGLE_TICK, null, null) } // Kill any phantom lasers from respawning as engie (yes this is real) EntFireByHandle(player, "RunScriptCode", @" for (local ent; ent = FindByClassname(ent, `env_laserdot`);) if (ent.GetOwner() == self) ent.Destroy() ", 0.5, null, null) // Temporary solution for engie wrangler laser scope.handled_laser <- false scope.PlayerThinkTable.ReverseMVMLaserThink <- function() { if (!("laser_spawntime" in scope)) scope.laser_spawntime <- -1 if (!PopExtUtil.IsAlive(player) || player.GetTeam() == TEAM_SPECTATOR) return local wep = self.GetActiveWeapon() if (wep.GetClassname() == "tf_weapon_laser_pointer") { if (laser_spawntime == -1) laser_spawntime = Time() + 0.55 } else { if (laser_spawntime > -1) { laser_spawntime = -1 handled_laser = false } return } if (handled_laser) return if (Time() < laser_spawntime) return local laser = null for (local ent; ent = FindByClassname(ent, "env_laserdot");) { if (ent.GetOwner() == self) { laser = ent break } } for (local ent; ent = FindByClassname(ent, "obj_sentrygun");) { local builder = GetPropEntity(ent, "m_hBuilder") if (builder != self) continue if (!GetPropBool(ent, "m_bPlayerControlled") || laser == null) continue originalposition <- self.GetOrigin() originalvelocity <- self.GetAbsVelocity() originalmovetype <- self.GetMoveType() self.SetAbsOrigin(ent.GetOrigin() + Vector(0, 0, -32)) self.SetAbsVelocity(Vector()) self.SetMoveType(MOVETYPE_NOCLIP, MOVECOLLIDE_DEFAULT) EntFireByHandle(laser, "RunScriptCode", "self.SetTeam(TF_TEAM_PVE_DEFENDERS)", SINGLE_TICK, null, null) EntFireByHandle(self, "RunScriptCode", "self.SetAbsOrigin(originalposition); self.SetAbsVelocity(originalvelocity); self.SetMoveType(originalmovetype, MOVECOLLIDE_DEFAULT)", SINGLE_TICK, null, null) handled_laser = true return } if (!handled_laser && laser != null) laser.Destroy() } // Allow money collection local collectionradius = 0 scope.PlayerThinkTable.ReverseMVMCurrencyThink <- function() { // Save money netprops because we fuck it in the loop below local money = GetPropInt(PopExtUtil.ObjectiveResource, "m_nMvMWorldMoney") local prev_wave_money = GetPropInt(PopExtUtil.MvMStatsEnt, "m_previousWaveStats.nCreditsDropped") local current_wave_money = GetPropInt(PopExtUtil.MvMStatsEnt, "m_currentWaveStats.nCreditsDropped") // Find currency near us local origin = self.GetOrigin() self.GetPlayerClass() != TF_CLASS_SCOUT ? collectionradius = 72 : collectionradius = 288 for ( local ent; ent = FindByClassnameWithin(ent, "item_currencypack_*", origin, collectionradius); ) { // Move the money to the origin and respawn it to allow us to collect it after it touches the ground ent.SetAbsOrigin(Vector(0, 0, INT_MIN)) ent.DispatchSpawn() } // The money counters are fucked from what we did in the above loop, fix it here SetPropInt(PopExtUtil.ObjectiveResource, "m_nMvMWorldMoney", money) SetPropInt(PopExtUtil.MvMStatsEnt, "m_previousWaveStats.nCreditsDropped", prev_wave_money) SetPropInt(PopExtUtil.MvMStatsEnt, "m_currentWaveStats.nCreditsDropped", current_wave_money) } // Allow pack collection scope.PlayerThinkTable.ReverseMVMPackThink <- function() { local origin = self.GetOrigin() for ( local ent; ent = FindByClassnameWithin(ent, "item_*", origin, 40); ) { if (ent.GetEFlags() & EFL_USER) continue if (GetPropInt(ent, "m_fEffects") & EF_NODRAW) continue local classname = ent.GetClassname() if (startswith(classname, "item_currencypack_")) continue local refill = false local is_health = false local multiplier = 0.0 if (endswith(classname, "_small")) multiplier = 0.2 else if (endswith(classname, "_medium")) multiplier = 0.5 else if (endswith(classname, "_full")) multiplier = 1.0 if (startswith(classname, "item_ammopack_")) { local metal = GetPropIntArray(self, "m_iAmmo", TF_AMMO_METAL) local maxmetal = 200 if (metal < maxmetal) { local maxmult = PopExtUtil.Round(maxmetal * multiplier) local setvalue = (metal + maxmult > maxmetal) ? maxmetal : metal + maxmult SetPropIntArray(self, "m_iAmmo", setvalue, TF_AMMO_METAL) refill = true } for (local i = 0; i < SLOT_MELEE; ++i) { local wep = PopExtUtil.GetItemInSlot(self, i) local ammo = GetPropIntArray(self, "m_iAmmo", i+1) local maxammo = PopExtUtil.GetWeaponMaxAmmo(self, wep) if (ammo >= maxammo) continue else { local maxmult = PopExtUtil.Round(maxammo * multiplier) local setvalue = (ammo + maxmult > maxammo) ? maxammo : ammo + maxmult SetPropIntArray(self, "m_iAmmo", setvalue, i+1) refill = true } } } else if (startswith(classname, "item_healthkit_")) { is_health = true local hp = self.GetHealth() local maxhp = self.GetMaxHealth() if (hp >= maxhp) continue refill = true local maxmult = PopExtUtil.Round(maxhp * multiplier) local setvalue = (hp + maxmult > maxhp) ? maxhp : hp + maxmult self.ExtinguishPlayerBurning() SendGlobalGameEvent("player_healed", { patient = PopExtUtil.GetPlayerUserID(self) healer = 0 amount = setvalue - hp }) SendGlobalGameEvent("player_healonhit", { entindex = self.entindex() weapon_def_index = 65535 amount = setvalue - hp }) self.SetHealth(setvalue) } if (refill) { if (is_health) EmitSoundOnClient("HealthKit.Touch", self) else EmitSoundOnClient("AmmoPack.Touch", self) ent.AddEFlags(EFL_USER) EntFireByHandle(ent, "Disable", "", -1, null, null) EntFireByHandle(ent, "Enable", "", 10, null, null) EntFireByHandle(ent, "RunScriptCode", "self.RemoveEFlags(EFL_USER)", 10, null, null) } } } // Drain player ammo on weapon usage scope.PlayerThinkTable.ReverseMVMDrainAmmoThink <- function() { if (value & 4) return local buttons = NetProps.GetPropInt(self, "m_nButtons"); local wep = player.GetActiveWeapon() if (wep == null || wep.IsMeleeWeapon()) return wep.ValidateScriptScope() local scope = wep.GetScriptScope() if (!("nextattack" in scope)) { scope.nextattack <- -1 scope.lastattack <- -1 } local classname = wep.GetClassname() local itemid = PopExtUtil.GetItemIndex(wep) local sequence = wep.GetSequence() // These weapons have clips but either function fine or don't need to be handled if (classname == "tf_weapon_particle_cannon" || classname == "tf_weapon_raygun" || classname == "tf_weapon_flaregun_revenge" || classname == "tf_weapon_drg_pomson" || classname == "tf_weapon_medigun") return local clip = wep.Clip1() if (clip > -1) { if (!("lastclip" in scope)) scope.lastclip <- clip // We reloaded if (clip > scope.lastclip) { local difference = clip - scope.lastclip if (self.GetPlayerClass() == TF_CLASS_SPY && classname == "tf_weapon_revolver") { local maxammo = GetPropIntArray(self, "m_iAmmo", SLOT_SECONDARY + 1) SetPropIntArray(self, "m_iAmmo", maxammo - difference, SLOT_SECONDARY + 1) } else { local maxammo = GetPropIntArray(self, "m_iAmmo", wep.GetSlot() + 1) SetPropIntArray(self, "m_iAmmo", maxammo - difference, wep.GetSlot() + 1) } } scope.lastclip <- clip } else { if (classname == "tf_weapon_rocketlauncher_fireball") { local recharge = GetPropFloat(player, "m_Shared.m_flItemChargeMeter") if (recharge == 0) { local cost = (sequence == 13) ? 2 : 1 local maxammo = GetPropIntArray(self, "m_iAmmo", wep.GetSlot() + 1) if (maxammo - cost > -1) SetPropIntArray(self, "m_iAmmo", maxammo - cost, wep.GetSlot() + 1) } } else if (classname == "tf_weapon_flamethrower") { if (sequence == 12) return // Weapon deploy if (Time() < scope.nextattack) return local maxammo = GetPropIntArray(self, "m_iAmmo", wep.GetSlot() + 1) // The airblast sequence lasts 0.25s too long so we check against it here if (buttons & IN_ATTACK && sequence != 13) { if (maxammo - 1 > -1) { SetPropIntArray(self, "m_iAmmo", maxammo - 1, wep.GetSlot() + 1) scope.nextattack <- Time() + 0.105 } } else if (buttons & IN_ATTACK2) { local cost = 20 if (itemid == ID_BACKBURNER || itemid == ID_FESTIVE_BACKBURNER) // Backburner cost = 50 else if (itemid == ID_DEGREASER) // Degreaser cost = 25 if (maxammo - cost > -1) { SetPropIntArray(self, "m_iAmmo", maxammo - cost, wep.GetSlot() + 1) scope.nextattack <- Time() + 0.75 } } } else if (classname == "tf_weapon_flaregun") { local nextattack = GetPropFloat(wep, "m_flNextPrimaryAttack") if (Time() < nextattack) return local maxammo = GetPropIntArray(self, "m_iAmmo", wep.GetSlot() + 1) if (buttons & IN_ATTACK) { if (maxammo - 1 > -1) SetPropIntArray(self, "m_iAmmo", maxammo - 1, wep.GetSlot() + 1) } } else if (classname == "tf_weapon_minigun") { local nextattack = GetPropFloat(wep, "m_flNextPrimaryAttack") if (Time() < nextattack) return local maxammo = GetPropIntArray(self, "m_iAmmo", wep.GetSlot() + 1) if (sequence == 21) { if (maxammo - 1 > -1) SetPropIntArray(self, "m_iAmmo", maxammo - 1, wep.GetSlot() + 1) } else if (sequence == 25) { if (Time() < scope.nextattack) return if (itemid != ID_HUO_LONG_HEATMAKER && itemid != ID_GENUINE_HUO_LONG_HEATMAKER) return if (maxammo - 1 > -1) { SetPropIntArray(self, "m_iAmmo", maxammo - 1, wep.GetSlot() + 1) scope.nextattack <- Time() + 0.25 } } } else if (classname == "tf_weapon_mechanical_arm") { // Reset hack SetPropIntArray(self, "m_iAmmo", 0, TF_AMMO_GRENADES1) SetPropInt(wep, "m_iPrimaryAmmoType", 3) local nextattack1 = GetPropFloat(wep, "m_flNextPrimaryAttack") local nextattack2 = GetPropFloat(wep, "m_flNextSecondaryAttack") local maxmetal = GetPropIntArray(self, "m_iAmmo", TF_AMMO_METAL) if (buttons & IN_ATTACK) { if (Time() < nextattack1) return if (maxmetal - 5 > -1) { SetPropIntArray(self, "m_iAmmo", maxmetal - 5, TF_AMMO_METAL) // This prevents an exploit where you M1 and M2 in rapid succession to evade the 65 orb cost SetPropFloat(wep, "m_flNextSecondaryAttack", Time() + 0.25) } } else if (buttons & IN_ATTACK2) { if (Time() < nextattack2) return if (maxmetal - 65 > -1) { // Hack to get around the game blocking SecondaryAttack below 65 metal SetPropIntArray(self, "m_iAmmo", INT_MAX, TF_AMMO_GRENADES1) SetPropInt(wep, "m_iPrimaryAmmoType", TF_AMMO_GRENADES1) SetPropIntArray(self, "m_iAmmo", maxmetal - 65, TF_AMMO_METAL) } } } else if (itemid == ID_WIDOWMAKER) { // Widowmaker local nextattack = GetPropFloat(wep, "m_flNextPrimaryAttack") if (Time() < nextattack) return local maxmetal = GetPropIntArray(self, "m_iAmmo", TF_AMMO_METAL) if (buttons & IN_ATTACK) { if (maxmetal - 30 > -1) SetPropIntArray(self, "m_iAmmo", maxmetal - 30, TF_AMMO_METAL) } } else if (classname == "tf_weapon_sniperrifle" || itemid == ID_BAZAAR_BARGAIN || itemid == ID_CLASSIC) { local lastfire = GetPropFloat(wep, "m_flLastFireTime") if (scope.lastattack == lastfire) return scope.lastattack <- lastfire local maxammo = GetPropIntArray(self, "m_iAmmo", wep.GetSlot() + 1) if (scope.lastattack > 0 && scope.lastattack < Time()) { if (maxammo - 1 > -1) SetPropIntArray(self, "m_iAmmo", maxammo - 1, wep.GetSlot() + 1) } } } } if (player.GetPlayerClass() == TF_CLASS_ENGINEER) { scope.BuiltObjectTable.DrainMetal <- function(params) { if (value & 4) return local player = GetPlayerFromUserID(params.userid) local scope = player.GetScriptScope() local curmetal = GetPropIntArray(player, "m_iAmmo", TF_AMMO_METAL) if (!("buildings" in scope)) scope.buildings <- [-1, array(2), -1] // don't drain metal if this buildings entindex exists in the players scope if (scope.buildings.find(params.index) != null || scope.buildings[1].find(params.index) != null) return // add entindexes to player scope if (params.object == OBJ_TELEPORTER) if (GetPropInt(EntIndexToHScript(params.index), "m_iTeleportType") == TTYPE_ENTRANCE) scope.buildings[1][0] = params.index else scope.buildings[1][1] = params.index else scope.buildings[params.object] = params.index switch(params.object) { case OBJ_DISPENSER: SetPropIntArray(player, "m_iAmmo", curmetal - 100, TF_AMMO_METAL) break case OBJ_TELEPORTER: if (PopExtUtil.HasItemInLoadout(player, ID_EUREKA_EFFECT)) SetPropIntArray(player, "m_iAmmo", curmetal - 25, TF_AMMO_METAL) else SetPropIntArray(player, "m_iAmmo", curmetal - 50, TF_AMMO_METAL) break case OBJ_SENTRYGUN: if (PopExtUtil.HasItemInLoadout(player, ID_GUNSLINGER)) SetPropIntArray(player, "m_iAmmo", curmetal - 100, TF_AMMO_METAL) else SetPropIntArray(player, "m_iAmmo", curmetal - 130, TF_AMMO_METAL) break } if (GetPropIntArray(player, "m_iAmmo", TF_AMMO_METAL) < 0) EntFireByHandle(player, "RunScriptCode", "SetPropIntArray(player, `m_iAmmo`, 0, TF_AMMO_METAL)", -1, null, null) } } //bitflags //cannot pick up intel if (value & 2 && !IsPlayerABot(player)) player.AddCustomAttribute("cannot pick up intelligence", 1, -1) //infinite cloak if (value & 8 && player.GetPlayerClass() == TF_CLASS_SPY) //setting it to -FLT_MAX makes it display as +inf% player.AddCustomAttribute("cloak consume rate decreased", -FLT_MAX, -1) //spawnroom behavior. 16 = spawn protection 32 = can't attack if (value & 16 || value & 32) { scope.PlayerThinkTable.InRespawnThink <- function() { if (!PopExtUtil.IsPointInRespawnRoom(player.EyePosition())) return if (value & 16 && !player.InCond(TF_COND_INVULNERABLE_HIDE_UNLESS_DAMAGED)) player.AddCondEx(TF_COND_INVULNERABLE_HIDE_UNLESS_DAMAGED, 2.0, null) if (value & 32) player.AddCustomAttribute("no_attack", 1, 0.033) } } if (value & 64) { // player.AddCond(TF_COND_DISGUISED) scope.PlayerThinkTable.RemoveFootsteps <- function() { if (!player.IsMiniBoss()) player.SetIsMiniBoss(true) } } } break // Options to revert global fixes below: // View globalfixes.nut for more info // ========================================================= case "ReflectableDF": if ("DragonsFuryFix" in GlobalFixes.ThinkTable) delete GlobalFixes.ThinkTable.DragonsFuryFix break // ========================================================= case "RestoreYERNerf": if ("YERDisguiseFix" in GlobalFixes.TakeDamageTable) delete GlobalFixes.TakeDamageTable.YERDisguiseFix break // ========================================================= // Don't add attribute to clean-up list if it could not be found. default: ParseError(format("Could not find mission attribute '%s'", attr)) success = false } // Add attribute to clean-up list if its modification was successful. if (success) { MissionAttributes.DebugLog(format("Added mission attribute %s", attr)) MissionAttributes.CurAttrs[attr] <- value } } function MissionAttrThink() { try foreach (_, func in MissionAttributes.ThinkTable) func() catch(e) printl(e) return -1 } MissionAttrEntity.ValidateScriptScope(); MissionAttrEntity.GetScriptScope().MissionAttrThink <- MissionAttrThink AddThinkToEnt(MissionAttrEntity, "MissionAttrThink") // This only supports key : value pairs, if you want var args call MissionAttr directly function MissionAttrs(attrs = {}) { foreach (attr, value in attrs) MissionAttributes.MissionAttr(attr, value) } //super truncated version incase the pop character limit becomes an issue. function MAtrs(attrs = {}) { foreach (attr, value in attrs) MissionAttributes.MissionAttr(attr, value) } // Allow calling MissionAttributes::MissionAttr() directly with MissionAttr(). function MissionAttr(...) { MissionAttr.acall(vargv.insert(0, MissionAttributes)) } //super truncated version incase the pop character limit becomes an issue. function MAtr(...) { MissionAttr.acall(vargv.insert(0, MissionAttributes)) } // Logging Functions // ========================================================= // Generic debug message that is visible if PrintDebugText is true. // Example: Print a message that the script is working as expected. function MissionAttributes::DebugLog(LogMsg) { if (MissionAttributes.DebugText) { ClientPrint(null, HUD_PRINTCONSOLE, format("MissionAttr: %s.", LogMsg)) } } // TODO: implement a try catch raise system instead of this // Raises an error if the user passes an index that is out of range. // Example: Allowed values are 1-2, but user passed 3. function MissionAttributes::RaiseIndexError(attr, max = [0, 1]) ParseError(format("Index out of range for %s, value range: %d - %d", attr, max[0], max[1])) // Raises an error if the user passes an argument of the wrong type. // Example: Allowed values are strings, but user passed a float. function MissionAttributes::RaiseTypeError(attr, type) ParseError(format("Bad type for %s (should be %s)", attr, type)) // Raises an error if the user passes an invalid argument // Example: Attribute expects a bitwise operator but value cannot be evenly split into a power of 2 function MissionAttributes::RaiseValueError(attr, value, extra = "") ParseError(format("Bad value %s passed to %s. %s", value.tostring(), attr, extra)) // Raises a template parsing error, if nothing else fits. function MissionAttributes::ParseError(ErrorMsg) { if (!MissionAttributes.RaisedParseError) { MissionAttributes.RaisedParseError = true ClientPrint(null, HUD_PRINTTALK, "\x08FFB4B4FFIt is possible that a parsing error has occured. Check console for details.") } ClientPrint(null, HUD_PRINTCONSOLE, format("%s %s.\n", MATTR_ERROR, ErrorMsg)) foreach (player in PopExtUtil.HumanArray) { if (player == null) continue EntFireByHandle(ClientCommand, "Command", format("echo %s %s.\n", MATTR_ERROR, ErrorMsg), -1, player, player) } printf("%s %s.\n", MATTR_ERROR, ErrorMsg) } // Raises an exception. // Example: Script modification has not been performed correctly. User should never see one of these. function MissionAttributes::RaiseException(ExceptionMsg) { Assert(false, format("MissionAttr EXCEPTION: %s.", ExceptionMsg)) }