// HOW IT WORKS: // PopExtPopulator.InitializeWave() is fired on wave init, this is the main function that parses the WaveSchedule table. // InitializeWave does the following: // - Spawns bot generators at the location provided in the "Where" keyvalue and sets the appropriate keyvalues // - attaches a think function to the generators that handles most of the spawning logic // - Adds icons to the wavebar // - fills out the WaveArray // Each element in WaveArray in another array that contains the WaveSpawn information // Each element in this nested array is a table where the key is the bot_generator, and the value is the keyvalue table for the wavespawn. // To access the bot_generator and the keyvalues from waves and individual wavespawns, use the PopExtPopulator.GetWavespawnInfo() function. // Major syntax differences compared to standard popfiles: // - Does not support unordered WaveSpawns, there is a fixed order of execution. If you wanna rearrange your wavespawns you can't just change the names/waits around // - CASE SENSITIVE! may change this in the future but it makes table look-ups easier for now. // - Tags and Items are applied to bots in a single array value // TODO: // - Tank, Halloween NPC, and SentryGun spawners // - RandomChoice and Squad spawners // - reliable icon decrementing const EFL_SPAWNER_PENDING = 1048576 // EFL_IS_BEING_LIFTED_BY_BARNACLE const EFL_SPAWNER_ACTIVE = 1073741824 //EFL_NO_PHYSCANNON_INTERACTION const EFL_SPAWNER_EXPENDED = 33554432 //EFL_DONTBLOCKLOS const MAX_WAVESPAWNS_PER_WAVE = 128 local PopulatorEnt = CreateByClassname( "info_teleport_destination" ) ::PopExtPopulator <- { WaveArray = [] PopulatorFunctions = { function Mission() { printl( "mission spawner" ) } } function DoEntityIO( type ) { if ( !( type in WaveSchedule[PopExtUtil.CurrentWaveNum] ) ) return local target = WaveSchedule[PopExtUtil.CurrentWaveNum][type].Target local action = "", param = "", delay = -1, activator = null, caller = null if ( "Param" in WaveSchedule[PopExtUtil.CurrentWaveNum][type] ) param = WaveSchedule[PopExtUtil.CurrentWaveNum][type].Param if ( "Delay" in WaveSchedule[PopExtUtil.CurrentWaveNum][type] ) delay = WaveSchedule[PopExtUtil.CurrentWaveNum][type].Delay if ( "Activator" in WaveSchedule[PopExtUtil.CurrentWaveNum][type] ) activator = WaveSchedule[PopExtUtil.CurrentWaveNum][type].Activator if ( "Caller" in WaveSchedule[PopExtUtil.CurrentWaveNum][type] ) caller = WaveSchedule[PopExtUtil.CurrentWaveNum][type].Caller typeof target == "instance" ? EntFireByHandle( target, action, param, delay, activator, caller ) : DoEntFire( target, action, param, delay, activator, caller ) } function InitializeWave() { //PopulatorEnt is a null instance when this is initially called, it is valid by the time another recalculate_holidays event fires, so this works reliably if ( !( "WaveSchedule" in getroottable() ) || PopulatorEnt.IsValid() ) return WaveArray = "CustomWaveOrder" in WaveSchedule ? WaveSchedule.CustomWaveOrder : array( WaveSchedule.len() ) foreach( wave, wavespawns in WaveSchedule ) { printl( "Wave: " + wave ) if ( !( "CustomWaveOrder" in WaveSchedule ) && typeof wave == "integer" ) WaveArray[wave] = array( wavespawns.len() ) if ( wave == "MissionAttrs" ) { MissionAttrs( wavespawns ) continue } //special support populators fire an additional function of the same name if ( wave in PopExtPopulator.PopulatorFunctions ) PopExtPopulator.PopulatorFunctions.wave() foreach ( wavespawn, keyvalues in wavespawns ) { printl( "\tWaveSpawn: " + wavespawn ) foreach( k, v in keyvalues ) { printl( "\t\t" + k + " : " + v ) if ( k.tolower() == "tfbot" ) { foreach( a, b in v ) printl( "\t\t\t" + a + " : " + b ) local icon = "" local playerclass = "" "Class" in v ? playerclass = ( typeof v.Class != "integer" ? v.Class : PopExtUtil.Classes[v.Class] ) : playerclass = PopExtUtil.Classes[RandomInt( 1, 9 )] // don't use bot_generators "auto" here, grab a random player class string instead so we can use playerclass for icon fallback "ClassIcon" in v ? icon = v.ClassIcon : icon = playerclass PopExt.AddCustomIcon( icon, keyvalues.TotalCount, ( "Attributes" in v && v.Attributes.find( ALWAYS_CRIT ) ) ? true : false, ( "Attributes" in v && v.Attributes.find( MINIBOSS ) ) ? true : false, ( "Support" in keyvalues ) ? true : false, ( "Support" in keyvalues && ( keyvalues.Support > 1 || ( typeof keyvalues.Support == "string" && keyvalues.Support.tolower() == "limited" ) ) ) ? true : false ) local generator = CreateByClassname( "bot_generator" ) // foreach ( w in WaveArray[wave] ) printl( w ) WaveArray[wave][wavespawn] = {} WaveArray[wave][wavespawn][generator] <- keyvalues // generator.KeyValueFromInt( "spawnOnlyWhenTriggered", "WaitBetweenSpawnsAfterDeath" in keyvalues || ( "SpawnCount" in keyvalues && keyvalues.SpawnCount > 1 ) ? 1 : 0 ) //we need manual control always for the spawn interval generator.KeyValueFromInt( "spawnOnlyWhenTriggered", 1 ) // generator.KeyValueFromFloat( "interval", "WaitBetweenSpawns" in keyvalues ? keyvalues.WaitBetweenSpawns.tofloat() : SINGLE_TICK ) generator.KeyValueFromFloat( "interval", SINGLE_TICK ) //this keyvalue is weird, just control the interval in the think function manually generator.KeyValueFromInt( "useTeamSpawnPoint", 0 ) generator.KeyValueFromInt( "maxActive", keyvalues.MaxActive.tointeger() ) generator.KeyValueFromInt( "count", keyvalues.TotalCount.tointeger() ) generator.KeyValueFromInt( "difficulty", "Skill" in keyvalues ? keyvalues.Skill.tointeger() : 1 ) generator.KeyValueFromInt( "disableDodge", "disableDodge" in keyvalues ? keyvalues.disableDodge.tointeger() : 0 ) generator.KeyValueFromString( "team", "TeamString" in keyvalues ? keyvalues.TeamString : "blue" ) generator.KeyValueFromString( "targetname", "Name" in keyvalues ? keyvalues.Name : format( "__popext_generator%d", generator.entindex() ) ) generator.KeyValueFromString( "class", playerclass ) local org = keyvalues.Where if ( typeof org == "string" ) { //check targetname first local spawnpoints = [] for ( local ent; ent = FindByName( ent, org ); ) spawnpoints.append( ent ) if ( spawnpoints.len() ) { org = spawnpoints[RandomInt( 0, spawnpoints.len() - 1 )].GetOrigin() return } //no targetnames found, assume KVString local split = split( org, " " ).apply( @( val ) val.tofloat() ) org = Vector( split[0], split[1], split[2] ) } generator.SetAbsOrigin( org ) AddOutput( generator, "OnExpended", "!self", "RunScriptCode", "self.AddEFlags( EFL_SPAWNER_EXPENDED )", -1, -1 ) generator.AddEFlags( EFL_SPAWNER_PENDING ) // generator.AddEFlags( EFL_SPAWNER_PENDING ) DispatchSpawn( generator ) generator.AcceptInput( "Disable", "", null, null ) generator.ValidateScriptScope() local genscope = generator.GetScriptScope() if ( "WaveSpawn" in genscope && "WaitBetweenSpawns" in genscope.WaveSpawn ) genscope.spawninterval <- genscope.WaveSpawn.WaitBetweenSpawns.tofloat() generator.GetScriptScope().GeneratorThink <- function() { if ( self.IsEFlagSet( EFL_SPAWNER_ACTIVE ) ) { local scope = self.GetScriptScope() if ( !( "spawninterval" in scope ) ) scope.spawninterval <- 0.0 if ( !( "nextspawn" in scope ) ) scope.nextspawn <- 0.0 if ( Time() < scope.nextspawn ) return //spawn 1 bot EntFireByHandle( self, "SpawnBot", "", -1, null, null ) //spawncount is > 1, spawn the SpawnCount number of bots - 1 since we already did 1 above if ( "SpawnCount" in genscope.WaveSpawn && genscope.WaveSpawn.SpawnCount > 1 ) for ( local i = 0; i < genscope.WaveSpawn.SpawnCount - 1; i ++ ) EntFireByHandle( self, "SpawnBot", "", -1, null, null ) scope.nextspawn <- Time() + scope.spawninterval } //we've finished spawning, look for other WaveSpawns that wait for this one and change their flag from PENDING to ACTIVE else if ( self.IsEFlagSet( EFL_SPAWNER_EXPENDED ) ) { for ( local g; g = FindByClassname( g, "bot_generator" ); ) { if ( !g.IsEFlagSet( EFL_SPAWNER_ACTIVE ) && "WaveSpawn" in g.GetScriptScope() && "WaitForAllSpawned" in g.GetScriptScope().WaveSpawn && self.GetName() == g.GetScriptScope().WaveSpawn.WaitForAllSpawned ) { self.AcceptInput( "Disable", "", null, null ) g.AddEFlags( EFL_SPAWNER_ACTIVE ) g.RemoveEFlags( EFL_SPAWNER_PENDING ) g.AcceptInput( "Enable", "", null, null ) } } } return -1 } generator.GetScriptScope().WaveSpawn <- keyvalues AddThinkToEnt( generator, "GeneratorThink" ) } } } } } function GetWavespawnInfo( wavenum = 0, wavespawn = 0 ) { // this function expects the actual wave number, not the array index ( wavenum 1 would get the array index 0 ) wavespawn -= 1 wavenum -= 1 //specific wave number passed, look at wavespawns at the wavenum index if ( wavenum > -1 ) { //valid wavespawn index passed, return only information about this wavespawn if ( wavespawn > -1 ) { return PopExtPopulator.WaveArray[wavenum][wavespawn] } //wave number passed, but no wavespawn, return every wavespawn in an array else { local allwavespawns = array( PopExtPopulator.WaveArray[wavenum].len() ) foreach( i, wavespawn in allwavespawns ) wavespawn = PopExtPopulator.WaveArray[wavenum][i] return allwavespawns } } //no wave number passed, put every wave in an array and put every wavespawn in another array at each wave else { // Create an array with each element being another MAX_WAVESPAWNS_PER_WAVE-length array. // If your mission has >MAX_WAVESPAWNS_PER_WAVE wavespawns on a single wave may god help you local allwaves = array( GetPropInt( PopExtUtil.ObjectiveResource, "m_nMannVsMachineMaxWaveCount" ), array( MAX_WAVESPAWNS_PER_WAVE, 0 ) ) foreach( i, wave in allwaves ) { //valid wavespawn index passed, just get this wavespawn index at every wave if ( wavespawn > -1 ) wave = PopExtPopulator.WaveArray[i] //no wavespawn index passed, get everything else { local allwavespawns = array( PopExtPopulator.WaveArray[wavenum].len(), [] ) foreach( j, _ in PopExtPopulator.WaveArray[i] ) wave[i] = PopExtPopulator.WaveArray[i][j] } } return allwaves } } } PopExtEvents.AddRemoveEventHook( "teamplay_round_start", "InitializeWave", function( params ) { PopExtPopulator.InitializeWave() PopExtPopulator.DoEntityIO( "InitWaveOutput" ) }, EVENT_WRAPPER_POPULATOR) PopExtEvents.AddRemoveEventHook( "mvm_begin_wave", "StartWave", function( params ) { //grab the first spawner and enable it on wave start local firstspawner = PopExtPopulator.WaveArray[PopExtUtil.CurrentWaveNum][0].keys()[0] EntFireByHandle( firstspawner, "Enable", "", -1, null, null ) PopExtPopulator.DoEntityIO( "StartWaveOutput" ) }, EVENT_WRAPPER_POPULATOR) PopExtEvents.AddRemoveEventHook( "mvm_wave_complete", "DoneOutput", function( params ) { PopExtPopulator.DoEntityIO( "DoneOutput" ) }, EVENT_WRAPPER_POPULATOR) PopExtEvents.AddRemoveEventHook( "player_death", "RemoveIcon", function( params ) { local player = GetPlayerFromUserID( params.userid ) if ( !player.IsBotOfType( TF_BOT_TYPE ) ) return local scope = player.GetScriptScope() PopExtUtil.PrintTable( scope ) local spawner = FindByName( null, scope.spawner ) PopExt.DecrementWaveIconSpawnCount( spawner.GetScriptScope().WaveSpawn.TFBot.ClassIcon, 0, 1 ) }) PopExtEvents.AddRemoveEventHook( "player_death", "WaitBetweenSpawnsAfterDeath", function( params ) { local player = GetPlayerFromUserID( params.userid ) if ( !player.IsBotOfType( TF_BOT_TYPE ) ) return local spawner = FindByName( null, player.GetScriptScope().spawner ) if ( "WaitBetweenSpawnsAfterDeath" in spawner.GetScriptScope().WaveSpawn ) { EntFireByHandle( spawner, "Enable", "", float delay, handle activator, handle caller ) EntFireByHandle( spawner, "SpawnBot", "", float delay, handle activator, handle caller ) } }) PopExtEvents.AddRemoveEventHook( "player_spawn", "WaitBetweenSpawnsAfterDeath", function( params ) { local player = GetPlayerFromUserID( params.userid ) if ( !player.IsBotOfType( TF_BOT_TYPE ) ) return //disable our spawner if we have WaitBetweenSpawnsAfterDeath EntFireByHandle( player, "RunScriptCode", @" local spawner = FindByName( null, self.GetScriptScope().spawner ) if ( `WaitBetweenSpawnsAfterDeath` in spawner.GetScriptScope().WaveSpawn ) EntFireByHandle(spawner, `Disable`, ``, -1, null, null ) ", SINGLE_TICK, null, null ) }) PopExtEvents.AddRemoveEventHook( "player_spawn", "SetSpawner", function( params ) { local player = GetPlayerFromUserID( params.userid ) if ( !player.IsBotOfType( TF_BOT_TYPE ) ) return local scope = player.GetScriptScope() local spawner = FindByClassnameNearest( "bot_generator", player.GetOrigin(), 256 ) scope.spawner <- spawner.GetName() scope.bot_attributes <- BECOME_SPECTATOR_ON_DEATH local additional_attributes = 0 //MVM hack: these keyvalues get removed by mvm logic from bot_generator spawned bots local spawner_keyvalues = { m_bDisableDodge = DISABLE_DODGE, m_bSuppressFire = SUPPRESS_FIRE, m_bRetainBuildings = RETAIN_BUILDINGS, m_iOnDeathAction = REMOVE_ON_DEATH //1 = remove } foreach ( key, value in spawner_keyvalues ) if ( NetProps.GetPropInt( spawner, key ) ) { if ( key == "m_iOnDeathAction" && NetProps.GetPropInt( spawner, key ) == 1 ) scope.bot_attributes = scope.bot_attributes |~ BECOME_SPECTATOR_ON_DEATH additional_attributes = additional_attributes | value } scope.bot_attributes = scope.bot_attributes | additional_attributes EntFireByHandle( player, "RunScriptCode", "self.AddBotAttribute( self.GetScriptScope().bot_attributes )", -1, null, null ) }) ::WaveSchedule <- { // CustomWaveOrder = [1,5,2,6,3] MissionAttrs = { WaveStartCountdown = 1 } Mission = { CooldownTime = 10 WaitBetweenSpawns = 5 TFBot = { } }, [1] = { //wave number AutoRelay = true, //set to true to automatically call wave_start_relay and wave_finished_relay without needing to define them InitWaveOutput = { Target = "bignet" Action = "RunScriptCode" Param = "Info( `Wave 1 loaded` )" }, // StartWaveOutput = { //not necessary with AutoRelay // Target = "wave_start_relay" // Action = "Trigger" // }, // DoneOutput = { // Target = "wave_finished_relay" // Action = "Trigger" // }, [1] = { //wavespawn Name = "wave1a" Where = "-198.163635 4760.567871 291.313049" //accepts KVString TotalCount = 15 MaxActive = 5 WaitBetweenSpawns = 3 Team = 3 TFBot = { Class = TF_CLASS_SCOUT Health = 150 ClassIcon = "scout_bat" Name = "Scout" Items = ["The Shortstop", "Bonk! Atomic Punch"] Tags = ["popext_addcond|11"] } }, [2] = { //wavespawn Name = "wave1b" Where = Vector( -1071.587646, 4216.348633, 200.016907 ) //accepts vector TotalCount = 10 MaxActive = 10 SpawnCount = 2 Team = 3 WaitForAllSpawned = "wave1a" ActionPoint = "action_point_test" RetainBuildings = true TFBot = { Class = TF_CLASS_SCOUT Health = 150 Name = "Scout" Items = ["The Shortstop", "Bonk! Atomic Punch"] Tags = ["popext_usehumananimations", "popext_addcond|11"] } }, [3] = { //wavespawn Name = "wave1c" Where = "spawnbot" //accepts targetname TotalCount = 100 MaxActive = 10 SpawnCount = 5 Team = 3 WaitForAllDead = "wave1b" RetainBuildings = true TFBot = { Class = "Engineer" Health = 275 Name = "Scout" Items = ["The Shortstop", "Bonk! Atomic Punch"] Tags = ["popext_usehumananimations", "popext_addcond|11"] } } } } PopExtPopulator.InitializeWave()