//============================================================================= // KFSpawner //============================================================================= // Placeable actor that can be used to script AI/Squad spawning //============================================================================= // Killing Floor 2 // Copyright (C) 2015 Tripwire Interactive LLC // - Andrew "Strago" Ladenberger //============================================================================= class KFSpawner extends Actor native placeable; /** Set to true if we're activating this from kismet. Set this to false if it is acting as a child spawner. This way any variables that the spawn will inherit from the parent will be greyed out */ var() bool bIsTriggeredSpawner; /** Largest type of squad that fits in this spawner */ var() ESquadType LargestSquadType; /** If > 0, automatically deactivate this spawner after some time */ var() float MaxStayActiveTime; /** Minimum time before getting re-activated */ var() float CooldownTime; /** Time between individual spawns within a squad */ var() float SpawnInterval; /** If set, yaw is randomized (useful for floor spawners) */ var() bool bRandomizeSpawnYawRot; /** The direction to spawn a zed if bRandomizeSpawnYawRot is true */ var transient int LastSpawnYawRot; /** If set, this spawner will auto-deactivate if this actor (e.g. Trigger, TriggerVolume) is not touching any players */ var() Actor ActorToReacquirePlayerTouch; /** If > 0, players must be touching the trigger for some amount of time before it can be used */ var() float MinReacquireTouchTime; var transient float ReacquireTouchTimeLeft; /** Number of zeds required to be touching the ActorToReacquirePlayerTouch for this spawning to be active */ var() int NumTouchingZedsRequired; /** Are enough zeds touching this actor to be able to spawn zeds here */ var transient bool bRequiredZedAmountTouching; /** Additional spawn points for pending zeds when this spawner is active */ var() array ChildSpawners; /** list of actor classes waiting to spawn */ var transient array< class > PendingSpawns; /** The animation direction of the last wall spawned zed */ var transient byte LastAnimDirection; /** Local timers for CanSpawnHere */ var protected transient float LastActivationTime; var protected transient float LastSpawnTime; /** Is this spawner active? */ var bool bIsActive; /** Is this spawner currently in the process of spawning? */ var bool bIsSpawning; /** Will log out/display spawning info for this spawn volume */ var bool bDebugSpawning; /********************************************************************************************* `* Effects ********************************************************************************************* */ enum EEmergeAnim { EMERGE_Floor, EMERGE_Wall248UU, EMERGE_WallHigh, EMERGE_Ceiling, EMERGE_None, }; /** Animation to play when exiting the spawner */ var() EEmergeAnim EmergeAnim; /** Reference to destructible object that we want to smash through */ var() KFDestructibleActor DestructibleToBreak; /********************************************************************************************* `* Debugging ********************************************************************************************* */ /** If set, do not activate child spawners */ var transient bool bIgnoreChildren; cpptext { /** Warn the mapper if a child KFSpawner has a "LargestSquadType" that's smaller than it's parent */ #if WITH_EDITOR virtual void CheckForErrors(); #endif /** Update SpawnInterval in tick */ virtual void TickSpecial(FLOAT DeltaTime); /** Spawns an AI from the pending spawns list */ UBOOL SpawnAI(UClass* ActorClass); /** If we have child spawners, randomely select a spawn location the list */ AKFSpawner* ChooseChildSpawner(); /** Gets a random direction to spawn our zed that's different than the last */ INT GetRandomYawRot(); } function OnToggle( SeqAct_Toggle Action ) { // On if( Action.InputLinks[0].bHasImpulse || (Action.InputLinks[2].bHasImpulse && !bIsActive) ) { ActivateSpawner(); } // Off else { DeactivateSpawner(); } } function ActivateSpawner() { local KFGameInfo KFGI; if ( !bIsActive ) { // restart reaquire touching actors interval if ( MinReacquireTouchTime > 0 && ActorToReacquirePlayerTouch != None ) { ReacquireTouchTimeLeft = MinReacquireTouchTime; SetTimer(1.0, true, nameof(ReacquirePlayerTouch)); } // restart reaquire touching actors interval if ( NumTouchingZedsRequired > 0 && ActorToReacquirePlayerTouch != None ) { SetTimer(1.0, true, nameof(ReacquireZedTouch)); } } bIsActive = true; LastActivationTime = WorldInfo.TimeSeconds; // register with the SpawnManager KFGI = KFGameInfo( WorldInfo.Game ); if ( KFGI != None && KFGI.SpawnManager != None ) { KFGI.SpawnManager.ActiveSpawner = self; } } function DeactivateSpawner() { bIsActive = false; LastActivationTime = -1.f; ClearTimer(nameof(ReacquirePlayerTouch)); ClearTimer(nameof(ReacquireZedTouch)); } function bool CanSpawnHere(ESquadType DesiredSquadType) { // check if activated if ( !bIsActive ) return false; // check valid squad type if ( DesiredSquadType < LargestSquadType ) return false; // check MaxStayActiveTime if ( MaxStayActiveTime > 0 && `TimeSince(LastActivationTime) > MaxStayActiveTime ) return false; // check CooldownTime if ( CooldownTime > 0 && LastSpawnTime > 0 && `TimeSince(LastSpawnTime) < CooldownTime ) return false; // check if player touch is still being acquired if ( ReacquireTouchTimeLeft > 0.f ) return false; // check if already spawning if ( PendingSpawns.Length > 0 ) return false; // Not enough touching zeds here! if( ActorToReacquirePlayerTouch != none && NumTouchingZedsRequired > 0 && bRequiredZedAmountTouching == false ) { return false; } return true; } /** Check if any players are still in the instigating trigger/triggervolume */ function ReacquirePlayerTouch() { local Pawn P; local bool bHasAlivePlayers; ForEach ActorToReacquirePlayerTouch.TouchingActors(class'Pawn', P) { if ( P.Controller != None && P.IsAliveAndWell() ) { // Human Team if( P.GetTeamNum() == 0 ) { bHasAlivePlayers = true; } } } // auto-deactivate if there are no living players in the area if ( !bHasAlivePlayers ) { `log(self@"ReacquirePlayerTouch is auto-deactivating this spawner", bDebugSpawning); ClearTimer(nameof(ReacquirePlayerTouch)); DeactivateSpawner(); } ReacquireTouchTimeLeft -= 1.f; if ( ReacquireTouchTimeLeft <= 0.f ) { `log(self@"ReacquirePlayerTouch completed successfully", bDebugSpawning); ClearTimer(nameof(ReacquirePlayerTouch)); } } /** Check if any zeds are still in the instigating trigger/triggervolume */ function ReacquireZedTouch() { local Pawn P; local int NumAliveZedsTouching; if( !bIsActive ) { return; } ForEach ActorToReacquirePlayerTouch.TouchingActors(class'Pawn', P) { if ( P.Controller != None && P.IsAliveAndWell() ) { // Zed Team if( P.GetTeamNum() != 0 ) { NumAliveZedsTouching++; } } } `log("NumAliveZedsTouching = "$NumAliveZedsTouching, bDebugSpawning); // See if enough zeds are touching to set the flag to true if( NumAliveZedsTouching >= NumTouchingZedsRequired ) { bRequiredZedAmountTouching = true; } else { bRequiredZedAmountTouching = false; } } /** Initiate spawning. Returns a list of classes that weren't able to be spawned here */ function int SpawnSquad( out array< class > SpawnList ) { if ( SpawnList.Length == 0 ) return 0; AddPendingSpawns( SpawnList ); LastSpawnTime = WorldInfo.TimeSeconds; bIsSpawning = PendingSpawns.Length > 0; return PendingSpawns.Length; } /** Validate and copy a list of classes to spawn into our PendingSpawn list */ function AddPendingSpawns( out array< class > SpawnList ) { local int i; local KFSpecialMoveHandler SMH; // if we don't need an animation, skip the SpecialMove (only valid if we're not using child spawners) if ( EmergeAnim == EMERGE_None && ChildSpawners.Length == 0 ) { PendingSpawns = SpawnList; SpawnList.Length = 0; return; } // Remove all monsters from the list that don't have emerge moves for ( i = SpawnList.length - 1; i >= 0 ; i-- ) { SMH = SpawnList[i].default.SpecialMoveHandler; if ( ESpecialMove(SM_Emerge) < SMH.SpecialMoveClasses.Length && SMH.SpecialMoveClasses[SM_Emerge] != None ) { PendingSpawns.AddItem( SpawnList[i] ); SpawnList.Remove( i, 1 ); } } } /** * Called when a new pawn is spawned using this spawner, * use to handle any special behaviors, such as playing * an animation, etc. */ event HandleSpawn(KFPawn NewSpawn, int SlotIdx) { local KFGameInfo KFGI; `if(`notdefined(ShippingPC)) local KFGameReplicationInfo KFGRI; `endif // instantly damage our linked destructible if ( DestructibleToBreak != None && !DestructibleToBreak.bShutDown ) { // use RadiusDamage so that we don't need a valid hit component DestructibleToBreak.TakeRadiusDamage(NewSpawn.Controller, 10000, 0, class'DmgType_Crushed', 0, Location, true, self); } // Handle emerge move/animation if ( EmergeAnim != EMERGE_None ) { NewSpawn.DoSpecialMove( SM_Emerge,,, class'KFSM_Emerge'.static.PackAnimFlag( EmergeAnim, LastAnimDirection ) ); if ( !NewSpawn.IsDoingSpecialMove(SM_Emerge) ) { `warn("SM_Emerge failed for"@NewSpawn); NewSpawn.Died( None, WorldInfo.KillZDamageType, Location ); HandleFailedSpawn(); } } `if(`notdefined(ShippingPC)) // Let the GRI know that a spawn volume was just used KFGRI = KFGameReplicationInfo(WorldInfo.GRI); if( KFGRI != none && KFGRI.bTrackingMapEnabled ) { KFGRI.AddRecentSpawnVolume(Location, true); } `endif // Recount number of living AI for wave gametype KFGI = KFGameInfo( WorldInfo.Game ); if ( KFGI != None ) { KFGI.RefreshMonsterAliveCount(); } } /** * Called when the spawner fails to spawn a pawn */ event HandleFailedSpawn() { local KFGameInfo KFGI; `if(`notdefined(ShippingPC)) local KFGameReplicationInfo KFGRI; `endif `warn(self@PendingSpawns.Length$" zeds failed to spawn at this portal spawn!!!"); // Removed failed spawns from the NumAISpawnsQueued; KFGI = KFGameInfo( WorldInfo.Game ); if ( KFGI != None ) { `log(self@GetFuncName()$" removing "$PendingSpawns.Length$" from NumAISpawnsQueued due to failed spawn. NumAISpawnsQueued: "$KFGI.NumAISpawnsQueued, bDebugSpawning); KFGI.NumAISpawnsQueued -= PendingSpawns.Length; KFGI.NumAIFinishedSpawning -= PendingSpawns.Length; } `if(`notdefined(ShippingPC)) // Let the GRI know that a portal spawn failed to spawn some AI KFGRI = KFGameReplicationInfo(WorldInfo.GRI); if( KFGRI != none && KFGRI.bTrackingMapEnabled ) { KFGRI.AddFailedSpawn(Location, true); } `endif if( bDebugSpawning ) { if ( KFGI != None && KFGI.SpawnManager != None ) { KFGI.SpawnManager.ActiveSpawner = self; if( bDebugSpawning ) { KFGI.SpawnManager.LogMonsterList(PendingSpawns, "Failed Pending Spawns For "$Self); } } } // If a zed failed to spawn, clear the list PendingSpawns.Length = 0; } /** Debugging */ function bool TestSpawn(class SpawnClass, optional int NumSpawns=1, optional bool bImmediate) { local array< class > TestSpawnList; while( NumSpawns > 0 ) { TestSpawnList.AddItem(SpawnClass); NumSpawns--; } SpawnSquad(TestSpawnList); if ( bImmediate ) { // don't wait for spawn interval, update spawn next tick LastSpawnTime -= SpawnInterval; } return bIsSpawning; } DefaultProperties { // Ensures we can randomize in all directions bIsTriggeredSpawner=true LastAnimDirection=255 LastSpawnYawRot=-1 SpawnInterval=0.5 CooldownTime=20.f LargestSquadType=EST_Medium Begin Object Class=StaticMeshComponent Name=PreviewMesh HiddenGame=TRUE StaticMesh=StaticMesh'ZED_Clot_MESH.ZED_Clot' Rotation=(Yaw=-16384) End Object Components.Add(PreviewMesh) }