/* * Author: Needles * https://steamcommunity.com/profiles/76561198026257137/ */ printl("*** MISSION"); IncludeScript("needles/debug.nut"); IncludeScript("needles/constants.nut"); // Don't set up the map if we are transitioning waves if ("Mission" in getroottable() && "doTransitionWave" in ::Mission && ::Mission.doTransitionWave == true) { ::Debug.Print("*** MISSION - skipping initialization..."); return; } ::Mission <- {}; ::Mission.RESPAWN_TIME_SETUP <- 5.0; ::Mission.RESPAWN_TIME_WAVE <- 99999.0; ::Mission.DEATH_TIME <- 240.0 // 4 minutes ::Mission.doTransitionWave <- false; ::Mission.inWave <- false; ::Mission.redSpawnListSetup <- []; ::Mission.redSpawnListWave <- []; ::Mission.blockingPropList <- []; ::Mission.timer_death <- null; ::Mission.SOUND_WARNING_PATH <- "vo/announcer_security_warning.mp3"; PrecacheSound(::Mission.SOUND_WARNING_PATH ); IncludeScript("needles/utils.nut"); IncludeScript("needles/math.nut"); IncludeScript("needles/events.nut"); IncludeScript("needles/damage.nut"); IncludeScript("needles/handler.nut"); IncludeScript("needles/hooks.nut"); IncludeScript("needles/timers.nut"); IncludeScript("needles/players.nut"); IncludeScript("needles/weapons.nut"); IncludeScript("needles/ent_tracker.nut"); IncludeScript("needles/filter.nut"); IncludeScript("needles/spawn_context.nut"); IncludeScript("needles/color.nut"); IncludeScript("needles/animated_sprite.nut"); IncludeScript("needles/brush.nut"); IncludeScript("needles/collision.nut"); IncludeScript("needles/storage.nut"); IncludeScript("needles/gui.nut"); IncludeScript("needles/respawn_override.nut"); IncludeScript("needles/currency.nut"); IncludeScript("needles/particle.nut"); IncludeScript("needles/attachment.nut"); IncludeScript("needles/highlight.nut"); IncludeScript("needles/spawngroup.nut"); IncludeScript("needles/patrol.nut"); IncludeScript("needles/map.nut"); IncludeScript("needles/fade.nut"); IncludeScript("needles/purge.nut"); IncludeScript("needles/death_tracker.nut"); IncludeScript("needles/missions/mvm_hoovydam_b10_rev_adv_insurgent/animated_sprite_data.nut"); // HANDLER REGISTRATION IncludeScript("needles/handler/burn.nut"); IncludeScript("needles/handler/jarate.nut"); IncludeScript("needles/handler/milk.nut"); IncludeScript("needles/handler/gas.nut"); IncludeScript("needles/handler/mark.nut"); IncludeScript("needles/handler/weapon_interaction.nut"); IncludeScript("needles/handler/fake_headshot.nut"); IncludeScript("needles/handler/damage_start.nut"); IncludeScript("needles/handler/damage_end.nut"); IncludeScript("needles/handler/core_boss.nut"); IncludeScript("needles/handler/scrap.nut"); IncludeScript("needles/handler/sap_target.nut"); IncludeScript("needles/handler/tutorial.nut"); IncludeScript("needles/handler/proximity_revive.nut"); IncludeScript("needles/handler/spawnpoint.nut"); ::Handler.SetPriority( [ "damage_start", "fake_headshot", "mission", "wave", "burn", "jarate", "milk", "gas", "mark", "weapon_interaction", "core_boss", "tutorial_console", "scrap", "sap_target", "proximity_reanimator", "spawnpoint", "damage_end", ]); // INITIALIZE PURGERS ::Mission.buildingPurge <- ::Purge.Purge(); ::Mission.buildingPurge.AddClassname("obj_sentrygun"); ::Mission.buildingPurge.AddClassname("obj_dispenser"); ::Mission.buildingPurge.AddClassname("obj_teleporter"); ::Mission.buildingPurge.event_onPurge.AddListener( function(params) { ::Debug.Print("*** MISSION - Purged building " + params.entity); } ); ::Mission.navPurge <- ::Purge.Purge(); ::Mission.navPurge.AddClassname("func_nav_prefer"); ::Mission.navPurge.AddClassname("func_nav_avoid"); ::Mission.navPurge.AddClassname("func_nav_prerequisite"); ::Mission.navPurge.AddClassname("func_tfbot_hint"); ::Mission.navPurge.event_onPurge.AddListener( function(params) { ::Debug.Print("*** MISSION - Purged nav " + params.entity); } ); // MISSION LOGIC ::EntTracker.AddClassname("tf_projectile_jar"); ::EntTracker.AddClassname("tf_projectile_jar_milk"); ::EntTracker.AddClassname("tf_projectile_jar_gas"); ::EntTracker.AddClassname("tf_projectile_arrow"); ::Storage.SetStoragePosition(Vector(-640.0, 2304.0, 128.0)); ::Scrap.ent_lightingOrigin = Entities.FindByName(null, "sentry_nest_hatch6"); // Random entity in broad daylight ::Map.forceMoneyCollectPoint = Vector(0.0, 0.0, 0.0); // @TODO - This is a huge mess! Do it again from the beginning. Simplify, only use 1 event please. ::DeathTracker.event_deadPlayerRespawn.AddListener( function(params) { // Do this at end of frame so that the respawnOverride has time to touch the player // @TODO - This may be a problem with the event system overall. In the future consider having ALL event callbacks delegated to end of frame processing. // @TODO - Is this the correct place for this? ::Utils.DoOnEndOfFrame( function() { if (::Mission.inWave == false) return; local player = ::Players.GetPlayerFromSteamID(params.steamid); local reviveData = ::ProximityRevive.Validate(player); if (reviveData != null && reviveData.isSpawnFromRevive == true) { ::Debug.Print("*** MISSION - Dead player with steamid " + params.steamid + " was spawned from a reanimator. Not killing player."); return; } ::Debug.Print("*** MISSION - Thought you could get away? To the depths with you! " + params.steamid); player.Teleport(true, params.originOfDeath, true, params.anglesOfDeath, true, Vector(0.0, 0.0,0.0)); player.TakeDamage(player.GetHealth(), 1, player); } ); } ) local MissionHandler = class extends ::Handler.BaseHandler { function OnEvent_WaveStart(params) { ::Debug.Print("*** MISSION - Wave start"); ::Mission.inWave = true; // No respawns ::RespawnOverride.SetRespawnOverride(::Mission.RESPAWN_TIME_WAVE); // Change spawns foreach(spawnpoint in ::Mission.redSpawnListSetup) ::SpawnContext.SetSpawnPointEnabled(spawnpoint, false); foreach(spawnpoint in ::Mission.redSpawnListWave) ::SpawnContext.SetSpawnPointEnabled(spawnpoint, true); // Kill buildings ::Mission.buildingPurge.Purge(); // Force red players outside for (local i = 1; i <= ::Players.Max(); i++) { local player = PlayerInstanceFromIndex(i); if (player == null) continue; if (::Players.IsPlayerValid(player) && player.GetTeam() == Constants.ETFTeam.TF_TEAM_RED) player.ForceRespawn(); } // Block spawn doors ::Mission.BlockSpawnDoors(); // Start the death countdown ::Mission.timer_death = ::Timers.AddTimer(::Mission.DEATH_TIME, function(params) { EntFire("sound_steam_whistle_red", "Volume", "10", 0.0); EntFire("sound_steam_whistle_red", "PlaySound", "", 0.0); EntFire("sound_steam_whistle_red", "StopSound", "", 9.0); ::Mission.handler_gatebotSpawnPoint.SetSpawnGroup(::SpawnGroup.GetSpawnGroupByTag("spawn_group_heavy_crit"), true); ::Mission.handler_gatebotSpawnPoint.SetIcon(::SpawnPoint.GetClassIconPath("heavy")); local ent_annotation = SpawnEntityFromTable("training_annotation", { origin = ::Mission.handler_gatebotSpawnPoint.self.GetOrigin() + Vector(0.0, 0.0, 64.0), display_text = "The gatebots are getting stronger!", lifetime = 10.0, }); EntFireByHandle(ent_annotation, "Show", "", 0.1, null, null); ::Debug.Print("*** WAVE - Death!"); ::Timers.AddTimer(2.35, function(params) { ::Debug.Print("*** WAVE - Warning!"); EmitSoundEx({ sound_name = ::Mission.SOUND_WARNING_PATH, channel = CHANNEL.CHAN_VOICE2, volume = 1.0, }); }, {}, 3); }); } function OnEvent_WaveEnd(params) { ::Debug.Print("*** MISSION - Wave end"); ::Mission.inWave = false; ::Mission.doTransitionWave = true; ::Debug.Print("*** MISSION - Wave transition..."); ::DeathTracker.Clear(); ::Timers.AddTimer(5.0, function(params) { ::Fade.event_onFade.ClearListeners(); ::Fade.event_onFade.AddListener( function(params) { ::Wave.InitializeWave(); ::Mission.FinalizeSetup(); } ); ::Fade.event_onFadeFull.AddListener( function(params) { for (local i = 1; i <= ::Players.Max(); i++) { local player = PlayerInstanceFromIndex(i); if (player == null) continue; if (::Players.IsPlayerAlive(player) == false) player.ForceRespawn(); } } ); ::Fade.Fade(::Color.Color(255, 255, 255, 255), 1.0, 0.0, 5.0); }); ::Mission.UnblockSpawnDoors(); ::Timers.RemoveTimer(::Mission.timer_death); } function OnEvent_PlayerDeathPost(params) { // Lose condition if (::Mission.inWave == false) return; local redPlayerCount = 0; local deadPlayerCount = 0; for (local i = 1; i <= ::Players.Max(); i++) { local player = PlayerInstanceFromIndex(i); if (player == null) continue; if (::Players.IsPlayerValid(player) == false) continue; if (player.GetTeam() == Constants.ETFTeam.TF_TEAM_RED) { redPlayerCount ++; if (::Players.IsPlayerAlive(player) == false) { deadPlayerCount ++; } } } if (deadPlayerCount >= redPlayerCount) { ::Debug.Print("*** MISSION - All red players are dead players!"); EntFire("bots_win", "RoundWin", "", -1.0); ::Mission.inWave = false; // @TODO - Possible cause of future bug? inWave is being set to false before the RoundWin is fired at end of frame. // This doesn't actually fix the wave-reset death bug. This bug is caused because the death tracker stays in scope until scripts are reset. Scripts are reset AFTER the map has finished setting up and all players have spawned. // Temporary solution. A better method may be to eliminate the inWave boolean and find a more robust way to detect if we are in setup or a wave. } } }; ::Handler.RegisterHandler("mission", @() MissionHandler()); ::Mission.handler_mission <- ::Handler.AddHandler(Entities.FindByClassname(null, "tf_gamerules"), "mission", null, -1.0); function Mission::FinalizeSetup() { for (local i = 1; i <= ::Players.Max(); i++) { local player = PlayerInstanceFromIndex(i); if (player == null) continue; ::SpawnContext.MovePlayerToSpawnPoint(player); } // Not in wave anymore ::Mission.inWave = false; ::Mission.doTransitionWave = false; // Respawns allowed ::RespawnOverride.SetRespawnOverride(::Mission.RESPAWN_TIME_SETUP); // Change red spawn points to setup foreach(spawnpoint in ::Mission.redSpawnListSetup) ::SpawnContext.SetSpawnPointEnabled(spawnpoint, true); foreach(spawnpoint in ::Mission.redSpawnListWave) ::SpawnContext.SetSpawnPointEnabled(spawnpoint, false); // @TODO - Does recompute_nav input work? ::Debug.Print("*** MISSION - Setup finalized"); } // @TODO - This is dumb // We could mark all the spawned props as dependencies of the wave handler, then when the handler is removed all the dependencies will be destroyed. function Mission::BlockSpawnDoors() { PrecacheModel("models/props_urban/urban_blast_door.mdl"); ::Mission.blockingPropList.append( SpawnEntityFromTable("prop_dynamic", { origin = Vector(-3200, -1162, 545), angles = QAngle(0.0, 180.0, 0.0), model = "models/props_urban/urban_blast_door.mdl", skin = 0, disableshadows = true, solid = Constants.ESolidType.SOLID_BBOX, }) ); ::Mission.blockingPropList.append( SpawnEntityFromTable("prop_dynamic", { origin = Vector(-1320, 2343, 304), angles = QAngle(0.0, 0.0, 0.0), model = "models/props_urban/urban_blast_door.mdl", skin = 1, disableshadows = true, solid = Constants.ESolidType.SOLID_BBOX, }) ); } function Mission::UnblockSpawnDoors() { foreach (entity in ::Mission.blockingPropList) { if (entity != null && entity.IsValid() == true) entity.Destroy(); } ::Mission.blockingPropList.clear(); } // TUTORIAL PrecacheModel("models/props_2fort/chimney006.mdl"); PrecacheModel("models/props_mvm/mvm_revive_tombstone.mdl"); PrecacheModel("models/bots/gameplay_cosmetic/light_soldier_on.mdl"); PrecacheModel("models/player/items/all_class/skull_soldier.mdl"); PrecacheModel("models/player/gibs/gibs_can.mdl"); ::Mission.tutorialList <- [ { model = "models/props_2fort/chimney006.mdl", scale = 0.25, modelOrigin = Vector(0.0, 0.0, 72.0), text = "destroy the robot core", textOrigin = Vector(-64, 0.0, 128.0), }, { model = "models/bots/gameplay_cosmetic/light_soldier_on.mdl", scale = 1.5, modelOrigin = Vector(0.0, 0.0, -32.0), text = "gatebots will try to capture the base\n don't let them" // Why can't you just let me align text to center!!! textOrigin = Vector(-112, 0.0, 128.0), }, { model = "models/player/items/all_class/skull_soldier.mdl", scale = 1.5, modelOrigin = Vector(0.0, 0.0, -32.0), text = "you will not respawn after dying" textOrigin = Vector(-96, 0.0, 128.0), }, { model = "models/props_mvm/mvm_revive_tombstone.mdl", scale = 0.5, modelOrigin = Vector(0.0, 0.0, 64.0), text = "any class can revive teammates\n stand next to the reanimator" textOrigin = Vector(-96, 0.0, 128.0), }, { model = "models/player/gibs/gibs_can.mdl", scale = 1.5, modelOrigin = Vector(-16.0, 0.0, 40.0), text = " robots will drop scrap on death\nscouts can collect them for overheal" textOrigin = Vector(-112, 0.0, 128.0), }, ]; ::Mission.hintList <- [ { model = "models/empty.mdl", scale = 1.0, modelOrigin = Vector(0.0, 0.0, 0.0), text = "team coordination is very important", textOrigin = Vector(0, 0.0, 0.0), }, { model = "models/empty.mdl", scale = 1.0, modelOrigin = Vector(0.0, 0.0, 0.0), text = "you revive teammates faster when ubered\nor when using a canteen", textOrigin = Vector(0, 0.0, 0.0), }, { model = "models/empty.mdl", scale = 1.0, modelOrigin = Vector(0.0, 0.0, 0.0), text = "the core can be sapped from behind", textOrigin = Vector(0, 0.0, 0.0), }, { model = "models/empty.mdl", scale = 1.0, modelOrigin = Vector(0.0, 0.0, 0.0), text = "the core can be disabled with the cow mangler", textOrigin = Vector(0, 0.0, 0.0), }, { model = "models/empty.mdl", scale = 1.0, modelOrigin = Vector(0.0, 0.0, 0.0), text = "snipers can headshot the core\nshoot its gun", textOrigin = Vector(0, 0.0, 0.0), }, { model = "models/empty.mdl", scale = 1.0, modelOrigin = Vector(0.0, 0.0, 0.0), text = "battalion's backup halves the core's damage", textOrigin = Vector(0, 0.0, 0.0), }, { model = "models/empty.mdl", scale = 1.0, modelOrigin = Vector(0.0, 0.0, 0.0), text = "if you take too long...\nthe gatebots will get stronger", textOrigin = Vector(0, 0.0, 0.0), }, ]; ::Mission.tutorialConsole <- ::Tutorial.BuildTutorialConsole(Vector(-1664, 2592, 304), QAngle(0.0, 0.0, 0.0)); EntFireByHandle(::Mission.tutorialConsole.self, "RunScriptCode", "::Mission.tutorialConsole.SetTutorialList(::Mission.tutorialList);", -1.0, null, null); // MAP SETUP // Switch spawn room teams local ent_funcRespawnRoom = null; while(ent_funcRespawnRoom = Entities.FindByClassname(ent_funcRespawnRoom, "func_respawnroom")) { local team = ent_funcRespawnRoom.GetTeam(); if (team == Constants.ETFTeam.TF_TEAM_RED) { ent_funcRespawnRoom.SetTeam(Constants.ETFTeam.TF_TEAM_BLUE); continue; } if (team == Constants.ETFTeam.TF_TEAM_BLUE) { ent_funcRespawnRoom.SetTeam(Constants.ETFTeam.TF_TEAM_RED); continue; } } // Stop red from losing if hatch is destroyed EntityOutputs.RemoveOutput(Entities.FindByName(null, "boss_deploy_relay"), "OnTrigger", "bots_win", "RoundWin", ""); // Get rid of the bombs local bomb = null; while (bomb = Entities.FindByClassname(bomb, "item_teamflag")) { bomb.SetAbsOrigin(::Const.INVALID_VECTOR); } // Get rid of holograms local hologram = null; ::Purge.QuickPurge(["prop_dynamic"], function(entity) { return (entity.GetModelName() == "models/props_mvm/robot_hologram.mdl" || entity.GetModelName() == "models/props_mvm/gatebot_hologram.mdl"); }, null); // Spawn some barrels to cover up the shitty nav PrecacheModel("models/props_hydro/water_barrel_cluster2.mdl"); SpawnEntityFromTable("prop_dynamic", { origin = Vector(736, 608, 320), angles = QAngle(0.0, 0.0, 0.0), model = "models/props_hydro/water_barrel_cluster2.mdl", disableshadows = true, solid = Constants.ESolidType.SOLID_VPHYSICS, }); SpawnEntityFromTable("prop_dynamic", { origin = Vector(664, 608, 320), angles = QAngle(0.0, 0.0, 0.0), model = "models/props_hydro/water_barrel_cluster2.mdl", disableshadows = true, solid = Constants.ESolidType.SOLID_VPHYSICS, }); SpawnEntityFromTable("prop_dynamic", { origin = Vector(592, 608, 320), angles = QAngle(0.0, 0.0, 0.0), model = "models/props_hydro/water_barrel_cluster2.mdl", disableshadows = true, solid = Constants.ESolidType.SOLID_VPHYSICS, }); // Block off the vent opening PrecacheModel("models/props_farm/air_intake.mdl") local ent_propDoor = SpawnEntityFromTable("prop_dynamic", { origin = Vector(752, 864, 432), angles = QAngle(0.0, 270.0, 0.0), model = "models/props_farm/air_intake.mdl", disableshadows = true, solid = Constants.ESolidType.SOLID_VPHYSICS, }); // health and ammo Entities.FindByClassnameNearest("item_healthkit_medium", Vector(-1088, 160, 320), 32.0).Destroy(); Entities.FindByClassnameNearest("item_ammopack_full", Vector(-1088, 232, 320), 32.0).Destroy(); Entities.FindByClassnameNearest("item_healthkit_medium", Vector(-288, -64, 320), 32.0).Destroy(); Entities.FindByClassnameNearest("item_ammopack_full", Vector(-288, -160, 320), 32.0).Destroy(); Entities.FindByClassnameNearest("item_healthkit_medium", Vector(-432, 2048, 256), 32.0).Destroy(); Entities.FindByClassnameNearest("item_ammopack_medium", Vector(-368, 2048, 256), 32.0).Destroy(); Entities.FindByClassnameNearest("item_healthkit_medium", Vector(-2272, 1504, 88), 32.0).Destroy(); Entities.FindByClassnameNearest("item_ammopack_medium", Vector(-2400, 1536, 88), 32.0).Destroy(); Entities.FindByClassnameNearest("item_ammopack_medium", Vector(-3296, 992, 96), 32.0).Destroy(); SpawnEntityFromTable("item_healthkit_medium", { origin = Vector(-736, 976, 256) AutoMaterialize = true, }); SpawnEntityFromTable("item_ammopack_full", { origin = Vector(-800, 976, 256) AutoMaterialize = true, }); SpawnEntityFromTable("item_healthkit_medium", { origin = Vector(-1200, 240, 256) AutoMaterialize = true, }); SpawnEntityFromTable("item_ammopack_medium", { origin = Vector(-1200, 168, 256) AutoMaterialize = true, }); SpawnEntityFromTable("item_healthkit_medium", { origin = Vector(-416, -64, 320) AutoMaterialize = true, }); SpawnEntityFromTable("item_ammopack_medium", { origin = Vector(-416, -160, 320) AutoMaterialize = true, }); SpawnEntityFromTable("item_healthkit_medium", { origin = Vector(-1008, 2240, 256) AutoMaterialize = true, }); SpawnEntityFromTable("item_ammopack_medium", { origin = Vector(-1008, 2176, 256) AutoMaterialize = true, }); SpawnEntityFromTable("item_healthkit_medium", { origin = Vector(-2560, 2000, 88) AutoMaterialize = true, }); SpawnEntityFromTable("item_ammopack_full", { origin = Vector(-2560, 1920, 88) AutoMaterialize = true, }); SpawnEntityFromTable("item_ammopack_medium", { origin = Vector(-3328, 768, 96) AutoMaterialize = true, }); // gate logic local ent_gate1DoorTrigger = Entities.FindByName(null, "gate1_door_trigger"); local ent_gate2DoorTrigger = Entities.FindByName(null, "gate2_door_trigger"); local ent_initB = Entities.FindByName(null, "initB"); // Do not change map-phase when the gate is captured EntityOutputs.RemoveOutput(ent_gate2DoorTrigger, "OnCapTeam2", "gate2_relay", "Trigger", ""); EntityOutputs.RemoveOutput(ent_gate1DoorTrigger, "OnCapTeam2", "gate1_relay", "Trigger", ""); // bots win when a gate is captured EntityOutputs.AddOutput(ent_gate2DoorTrigger, "OnCapTeam2", "gate2_door_alarm", "Disable", "", 0.0, 1); EntityOutputs.AddOutput(ent_gate2DoorTrigger, "OnCapTeam2", "bots_win", "RoundWin", "", 0.0, 1); EntityOutputs.AddOutput(ent_gate1DoorTrigger, "OnCapTeam2", "gate1_door_alarm", "Disable", "", 0.0, 1); EntityOutputs.AddOutput(ent_gate1DoorTrigger, "OnCapTeam2", "bots_win", "RoundWin", "", 0.0, 1); // close the lower bot spawn door PrecacheModel("models/props_moonbase/moon_frontdoor_floor01.mdl"); local ent_upgradeCenterProp = SpawnEntityFromTable("prop_dynamic", { origin = Vector(-912, 2416, 128), angles = QAngle(0.0, 270.0, 90.0), model = "models/props_moonbase/moon_frontdoor_floor01.mdl", disableshadows = true, solid = Constants.ESolidType.SOLID_VPHYSICS, });