1
0
KF2-Dev-Scripts/KFGame/Classes/KFSpawner.uc

447 lines
13 KiB
Ucode
Raw Normal View History

2020-12-13 15:01:13 +00:00
//=============================================================================
// 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<EditCondition=bIsTriggeredSpawner>;
/** If > 0, automatically deactivate this spawner after some time */
var() float MaxStayActiveTime<EditCondition=bIsTriggeredSpawner>;
/** Minimum time before getting re-activated */
var() float CooldownTime<EditCondition=bIsTriggeredSpawner>;
/** Time between individual spawns within a squad */
var() float SpawnInterval<EditCondition=bIsTriggeredSpawner>;
/** 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<EditCondition=bIsTriggeredSpawner>;
/** If > 0, players must be touching the trigger for some amount of time before it can be used */
var() float MinReacquireTouchTime<EditCondition=bIsTriggeredSpawner>;
var transient float ReacquireTouchTimeLeft;
/** Number of zeds required to be touching the ActorToReacquirePlayerTouch for this spawning to be active */
var() int NumTouchingZedsRequired<EditCondition=bIsTriggeredSpawner>;
/** 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<KFSpawner> ChildSpawners;
/** list of actor classes waiting to spawn */
var transient array< class<KFPawn_Monster> > 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<KFPawn_Monster> > 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<KFPawn_Monster> > 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<KFPawn_Monster> SpawnClass, optional int NumSpawns=1, optional bool bImmediate)
{
local array< class<KFPawn_Monster> > 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)
}