1
0
KF2-Dev-Scripts/KFGame/Classes/KFAIController_Monster.uc
2020-12-13 18:01:13 +03:00

519 lines
18 KiB
Ucode

//=============================================================================
// KFAIController_Monster.uc
//=============================================================================
// Base AIController for KF2's Zeds
//=============================================================================
// Killing Floor 2
// Copyright (C) 2015 Tripwire Interactive LLC
//=============================================================================
class KFAIController_Monster extends KFAIController
dependson(KFAIController)
abstract
native(AI);
`include(KFGame\KFGameAnalytics.uci);
/** Zeds who can grab prefer to use a grab as their initial attack - if true, they've already done this */
var bool bCompletedInitialGrabAttack;
/** Clot won't perform grab until closer than this distance. TODO: If we keep this, change it to a % scale of MaxGrabDistance in KFSM_Clot_Grab */
var float MinDistanceToPerformGrabAttack;
/** Time frequency for grab attacks */
var float MinTimeBetweenGrabAttacks;
/** Last time a grab attack was performed */
var float LastAttackTime_Grab;
var bool bPathAroundDestructiblesICantBreak;
/** Determines if a zed should try to force a repath if they cannot execute a valid strike */
var bool bRepathOnInvalidStrike;
/*********************************************************************************************
* RunOverWarning (warns Zeds nearby that my pawn's about to run into them)
********************************************************************************************* */
/** Zed will transmit ReceiveRunOverWarning events to other Zeds if its about to run them over */
var bool bUseRunOverWarning;
/** Speed must be greater than this to transmit run over warning (if bUseRunOverWarning=true) */
var float MinRunOverSpeed;
/** Last time checked for pawns to transmit RunOverWarning to (if bUseRunOverWarning=true) */
var float LastRunOverWarningTime;
/** Minimum angle to victim required to transmit RunOverWarning (if bUseRunOverWarning=true) */
var float MinRunOverWarningAim;
/** When TRUE, this Zed will attempt to evade when warned of being run over */
var bool bEvadeOnRunOverWarning;
/** Scales the delay from the initial warning notification. Increase for zeds that are fast evaders, decrease for slow evaders */
var float RunOverEvadeDelayScale;
cpptext
{
UBOOL Tick( FLOAT DeltaTime, enum ELevelTick TickType );
// Called by native Tick() to evaluate if Zed in melee range of target - if so, will call InMeleeRange() event
virtual UBOOL TickMeleeCombatDecision( FLOAT DeltaTime );
// Supports "run over" warning notification to other NPCs - mainly for larger Zeds to use
// to give nearby Zeds a chance to get out of the way.
virtual void TickRunOverWarning( FLOAT DeltaSeconds );
}
/*********************************************************************************************
* Initialization, Pawn Possession, and Destruction
********************************************************************************************* */
/** Only spawning a PRI for gameplayevents! */
function InitPlayerReplicationInfo()
{
local KFGameInfo KFGI;
local string NPCName;
KFGI = KFGameInfo(WorldInfo.Game);
if( KFGI != none && KFGI.bEnableGameAnalytics )
{
PlayerReplicationInfo = Spawn(class'KFDummyReplicationInfo', self,, vect(0,0,0),rot(0,0,0));
if ( Pawn != none )
{
NPCName = string(Pawn.name);
NPCName = Repl(NPCName,"KFPawn_Zed","",false);
}
else
{
NPCName = string(self.name);
NPCName = Repl(NPCName,"KFAIController_Zed","",false);
}
PlayerReplicationInfo.PlayerName = NPCName;
/* __TW_ANALYTICS_ */
`RecordZedSpawn(self);
// don't call SetPlayerName() as that will broadcast entry messages but the GameInfo hasn't had a chance
// to potentionally apply a player/bot name yet
//PlayerReplicationInfo.PlayerName = class'GameInfo'.default.DefaultPlayerName;
}
}
/** Set MyKFPawn to avoid casting */
event Possess( Pawn inPawn, bool bVehicleTransition )
{
if( KFPawn_Monster(inPawn) != none )
{
MyKFPawn = KFPawn_Monster( inPawn );
}
else
{
`warn( GetFuncName()$"() attempting to possess "$inPawn$", but it's not a KFPawn_Monster class! MyKFPawn variable will not be valid." );
}
super.Possess( inPawn, bVehicleTransition );
SetPawnDefaults();
}
function SetPawnDefaults()
{
local float SprintChance;
local float SprintDamagedChance;
local float HiddenSpeedMod;
local float GameDifficulty;
local KFGameDifficultyInfo DifficultyInfo;
local KFGameInfo KFGI;
KFGI = KFGameInfo( WorldInfo.Game );
GameDifficulty = KFGI.GetModifiedGameDifficulty();
DifficultyInfo = KFGI.DifficultyInfo;
SprintChance = DifficultyInfo.GetCharSprintChanceByDifficulty( MyKFPawn, GameDifficulty );
SprintDamagedChance = DifficultyInfo.GetCharSprintWhenDamagedChanceByDifficulty( MyKFPawn, GameDifficulty );
HiddenSpeedMod = DifficultyInfo.GetAIHiddenSpeedModifier( KFGI.GetLivingPlayerCount() );
MyKFPawn.HiddenGroundSpeed = MyKFPawn.default.HiddenGroundSpeed * HiddenSpeedMod;
if ( MyKFPawn.PawnAnimInfo != none )
{
MyKFPawn.PawnAnimInfo.SetDifficultyValues( DifficultyInfo );
}
// Each zed has a chance he will sprint at a certain difficulty
// NOTE: Some zeds now bypass this check because they need to sprint under certain conditions regardless of
// difficulty! Search the code for bIsSprinting = true. Evil, yes, but necessary
SetCanSprint( FRand() <= SprintChance );
SetCanSprintWhenDamaged( FRand() <= SprintDamagedChance );
bDefaultCanSprint = bCanSprint;
if( KFGI.BaseMutator != None )
{
KFGI.BaseMutator.ModifyAI( Pawn );
}
}
/*********************************************************************************************
* Notifications & Events
********************************************************************************************* */
/** Re-Enables notifications from TickMeleeCombatDecision() */
function Timer_EnableMeleeRangeEventProbing()
{
if( !MyKFPawn.IsDoingSpecialMove() )
{
EnableMeleeRangeEventProbing();
}
else
{
// Re-Enable timer once at a time (added 7/2014)
SetTimer( 0.12f, false, nameof(Timer_EnableMeleeRangeEventProbing), self );
}
}
/** Notification that we have passed all our basic melee checks and are ready to attempt a melee attack */
event ReadyToMelee()
{
// Check script to see if a strike is allowed
if( CanDoStrike() )
{
// Update our next pending strike
UpdatePendingStrike();
LastGetStrikeTime = WorldInfo.TimeSeconds;
// Perform strike if we have a valid animation
if( PendingAnimStrikeIndex != 255 )
{
DoStrike();
return;
}
}
// Attempt to find another path to enemy
if( bRepathOnInvalidStrike && (bFailedToMoveToEnemy || (!bMovingToGoal && !bMovingToEnemy)) )
{
SetEnemyMoveGoal(self, true,,, true);
}
// If we can't attack, and are close to the enemy, do a taunt so we don't just stand there
else if( !CheckOverallCooldownTimer() && Enemy != none && Pawn != none && Pawn.IsAliveAndWell() )
{
if( VSize(Enemy.Location - Pawn.Location) < MyKFPawn.CylinderComponent.CollisionRadius * 3.0 )
{
if( MyKFPawn.CanDoSpecialMove(SM_Taunt) && `TimeSince(LastTauntTime) > 2.f )
{
`AILog( GetFuncName()$" starting taunt command", 'CantMelee' );
class'AICommand_TauntEnemy'.static.Taunt( self, KFPawn(Enemy), TAUNT_Standard );
}
}
}
}
/*********************************************************************************************
* Pathfinding
********************************************************************************************* */
/** Set up path constraints and attempt to build a path to Goal actor. Distance is an optional offset. */
event Actor GeneratePathTo( Actor Goal, optional float Distance, optional bool bAllowPartialPath )
{
local actor PathResult;
local int i;
if( bDisablePartialPaths )
{
bAllowPartialPath = false;
}
AddBasePathConstraints();
class'Path_TowardGoal'.static.TowardGoal( Pawn, Goal );
if( bPathAroundDestructiblesICantBreak )
{
/** NPC will build path around destructible objects not configured to accept bump damage */
class'Path_AroundDestructibles'.static.AvoidDestructibles( Pawn, true, true );
class'Goal_Null'.static.GoUntilBust( Pawn, 2024 );
}
else
{
class'Goal_AtActor'.static.AtActor( Pawn, Goal, Distance, bAllowPartialPath );
}
// Attempt to build the path.
PathResult = FindPathToward( Goal );
Pawn.ClearConstraints();
if( PathResult == None )
{
`AILog( GetFuncName()$"() failed to build a path to "$Goal$", offset distance was "$Distance$", bAllowPartialPath was "$bAllowPartialPath, 'PathWarning' );
}
if( bShowMovePointsDebugInfo )
{
for( i = 0; i < RouteCache.Length; i++ )
{
DrawDebugStar( RouteCache[i].Location, PathNodeShowRouteCacheCrossSize, PathNodeShowRouteCacheColor.R, PathNodeShowRouteCacheColor.G, PathNodeShowRouteCacheColor.B, true);
DrawDebugString( RouteCache[i].Location + vect(0,0,5), string(i), , PathNodeShowRouteCacheColor, PathNodeShowRouteCacheNumberLabelDuration);
if( i > 0 )
{
DrawDebugLine( RouteCache[i].Location, RouteCache[i-1].Location, PathNodeShowRouteCacheColor.R, PathNodeShowRouteCacheColor.G, PathNodeShowRouteCacheColor.B, true);
}
}
}
return PathResult;
}
/*********************************************************************************************
* Combat
**********************************************************************************************/
/** Can this pawn perform a grab attack? */
event bool CanGrabAttack()
{
local KFPawn_Human KFPH;
local KFPerk EnemyPerk;
local KFPawn KFPawnEnemy;
local float DistSq;
local vector Extent, HitLocation, HitNormal;
local Actor HitActor;
// If I'm dead, incapable of grabbing, or have no enemy, or my enemy is a player, or I'm busy doing a melee attack, refuse.
if( (MyKFPawn == none || !MyKFPawn.bCanGrabAttack || MyKFPawn.Health <= 0) || (Enemy == none) || (Enemy != none && Pawn.IsSameTeam(Enemy)) )
{
return false;
}
KFPawnEnemy = KFPawn( Enemy );
if( KFPawnEnemy == none || !KFPawnEnemy.CanBeGrabbed(MyKFPawn) )
{
return false;
}
// If I'm crippled, falling, busy doing an attack, or incapacitated, refuse.
if( MyKFPawn.bIsHeadless || (MyKFPawn.Physics == PHYS_Falling) || IsDoingAttackSpecialMove() || !MyKFPawn.IsCombatCapable() )
{
return false;
}
// Check for fakeout perk
KFPH = KFPawn_Human(Enemy);
if ( KFPH != none )
{
EnemyPerk = KFPH.GetPerk();
if ( EnemyPerk != none && EnemyPerk.CanNotBeGrabbed() )
{
return false;
}
}
if( !bCompletedInitialGrabAttack || (LastAttackTime_Grab == 0.f || (`TimeSince(LastAttackTime_Grab) > MinTimeBetweenGrabAttacks)) )
{
// Make sure the enemy's center of mass (location) is within my collision cylinder
if( Abs(Enemy.Location.Z - Pawn.Location.Z) > class'KFSM_GrappleCombined'.default.MaxVictimZOffset )
{
return false;
}
DistSq = VSizeSq(Enemy.Location - Pawn.Location);
if( DistSq > MinDistanceToPerformGrabAttack * MinDistanceToPerformGrabAttack || MyKFPawn.IsPawnMovingAwayFromMe(Enemy, 300.f) )
{
return false;
}
// Set our extent
Extent.X = Pawn.GetCollisionRadius() * 0.5f;
Extent.Y = Extent.X;
Extent.Z = Pawn.GetCollisionHeight() * 0.5f;
// Do the same kind of trace we do in KFSM_GrappleStart
HitActor = Trace(HitLocation, HitNormal, Enemy.Location, Pawn.Location, true, Extent);
if ( HitActor != None && HitActor != Enemy )
{
return false;
}
if( !CanTargetBeGrabbed(KFPawnEnemy) )
{
return false;
}
/** Makes Zed have high desire to grab as initial attack */
if( !MyKFPawn.IsDoingMeleeAttack() && (!bCompletedInitialGrabAttack || (FRand() < MyKFPawn.GrabAttackFrequency)) ) //&& !MyKFPawn.IsPawnMovingAwayFromMe(Enemy, 250.f) )
{
return true;
}
}
`AILog( GetFuncName()$"() returning FALSE", 'GrabAttack' );
return false;
}
function bool CanDoStrike()
{
local actor HitActor;
local vector TraceStepLocation;
// Used by KFPawnAnimInfo to determine if an attack can be performed if legs are blocked (lunges, etc)
bIsBodyBlocked = false;
// Check if a wall or another Zed is blocking my pawn from performing a melee attack, ignore zed collision if bCanStrikeThroughEnemies is true,
TraceStepLocation = Pawn.Location + (vect(0,0,-1) * (Pawn.CylinderComponent.CollisionHeight * 0.5f));
HitActor = ActorBlockTest( Pawn, Enemy.Location, TraceStepLocation,, !bCanStrikeThroughEnemies );
if( HitActor != none && HitActor != Enemy )
{
if( HitActor.bWorldGeometry )
{
// Set the body blocked flag so the anim info can check it
bIsBodyBlocked = true;
}
// Try again at eyeheight
HitActor = ActorBlockTest( Pawn, Enemy.Location + (vect(0,0,1) * Enemy.BaseEyeHeight), Pawn.Location + (vect(0,0,1) * Pawn.BaseEyeHeight),, !bCanStrikeThroughEnemies );
if( HitActor != None && HitActor != Enemy && (!bCanStrikeThroughEnemies || HitActor.bWorldGeometry) )
{
return false;
}
}
return true;
}
function DoStrike()
{
local byte StrikeFlags;
if( MyKFPawn != none && MyKFPawn.PawnAnimInfo != none )
{
StrikeFlags = MyKFPawn.PawnAnimInfo.GetStrikeFlags(PendingAnimStrikeIndex);
if( StrikeFlags != 255 )
{
`AILog( GetFuncName()$"() "$VSize(MyKFPawn.Location - Enemy.Location)$" units from enemy and I DO HAVE AN available attack!", 'Command_Attack_Melee' );
class'AICommand_Attack_Melee'.static.Melee( self, Enemy, StrikeFlags );
MyKFPawn.PawnAnimInfo.UpdateAttackCooldown(self, PendingAnimStrikeIndex);
UpdatePendingStrike();
}
else
{
`AILog( GetFuncName()$"() "$VSize(MyKFPawn.Location - Enemy.Location)$" units from enemy and I have no available attack!", 'Command_Attack_Melee' );
}
}
}
/** Perform a melee attack AICommand.. InTarget is optional actor to attack (door, etc.) */
function DoMeleeAttack( optional Pawn NewEnemy, optional Actor InTarget, optional byte AttackFlags )
{
/*
local AICommand AIC;
if( MyKFPawn != none && (!MyKFPawn.bIsHeadless && !MyKFPawn.bEmpPanicked && !IsMeleeRangeEventProbingEnabled()) || (MyKFPawn.IsDoingSpecialMove() && !MyKFPawn.IsDoingSpecialMove(SM_ChargeRun)) )
{
`AILog( GetFuncName()$"() skipping melee attack because "$Pawn$" is already busy.", 'Command_Attack_Melee' );
return;
}
AIC = AICommand( GetActiveCommand() );
if( AIC != none )
{
if( !AIC.bAllowedToAttack )
{
`AILog( GetFuncName()$"() refusing to do melee attack because "$AIC$" bAllowedToAttack is FALSE", 'Command_Attack_Melee' );
return;
}
if( AICommand_Pause(AIC) != none )
{
return;
}
if( AICommand_TauntEnemy(AIC) != none )
{
return;
}
}
if( MyKFPawn != none && MyKFPawn.PawnAnimInfo != none )
{
// Only Pack flags if 255 was initially passed in
if( AttackFlags == 255 )
{
AttackFlags = ChooseStrikeAnimation();
}
if( AttackFlags != 255 )
{
`AILog( GetFuncName()$"() Aborting movement commands and starting melee attack command", 'Command_Attack_Melee' );
class'AICommand_Attack_Melee'.static.Melee( self, InTarget, AttackFlags );
}
return;
}
if( !AICommand(CommandList).bAllowedToAttack )
{
`AILog( GetFuncName()$"() refusing to do melee attack because "$CommandList$" bAllowedToAttack is FALSE", 'Command_Attack_Melee' );
}
*/
}
/** Called when in melee range but enemy is blocked from me, probably by another Zed */
function bool HandleZedBlockedPath()
{
local actor HitActor;
local KFPawn_Monster HitMonster;
HitActor = ActorBlockTest( Pawn, Enemy.Location + vect(0,0,1) * (Enemy.BaseEyeHeight * 0.5f), MyKFPawn.Location + vect(0,0,1) * (MyKFPawn.BaseEyeHeight * 0.5f), MyKFPawn.GetCollisionExtent() * vect(0.2f,0.2f,0.2f), true );
if( HitActor == none || HitActor == Enemy )
{
return false;
}
// If we hit a monster check HandleEnemyBlocked, otherwise we're good to strike
HitMonster = KFPawn_Monster(HitActor);
if( HitMonster != none && HitMonster.Health > 0 )
{
if( MyKFPawn == none || MyKFPawn.Health <= 0 || MyKFPawn.IsDoingSpecialMove() )
{
return true;
}
`AILog( GetFuncName()$" ENEMY IS BLOCKED", 'ReachedEnemy' );
DisableMeleeRangeEventProbing();
SetTimer( 1.5f + (2.f*FRand()), false, nameof(Timer_EnableMeleeRangeEventProbing), self );
if( FindNewEnemy() )
{
ForcePauseAndRepath();
return true;
}
if( VSize(Enemy.Location - Pawn.Location) < AttackRange && bDirectMoveToGoal )
{
if( MyKFPawn.CanDoSpecialMove(SM_Taunt) && FRand() < 0.32 && `TimeSince(LastTauntTime) > 2.f )
{
`AILog( GetFuncName()$" starting taunt command", 'ReachedEnemy' );
class'AICommand_TauntEnemy'.static.Taunt( self, KFPawn(Enemy), TAUNT_Standard );
}
else
{
`AILog( GetFuncName()$" starting pauseAI command", 'ReachedEnemy' );
DoPauseAI( 1.f + (3.f * FRand()), true );
}
return true;
}
}
return false;
}
/** Notification I'm about to be run into by a Zed which has bUseRunOverWarning set to true */
event RunOverWarning( KFPawn IncomingKFP, float IncomingSpeedSquared, vector RunOverPoint )
{
local float Delay;
if( bEvadeOnRunOverWarning && CanEvade(true) )
{
Delay = ( VSize(IncomingKFP.Location - MyKFPawn.Location) / Sqrt(IncomingSpeedSquared) ) * RunOverEvadeDelayScale;
DoEvade( GetBestEvadeDir(RunOverPoint,, false), IncomingKFP,, Delay, true );
}
}
DefaultProperties
{
// ---------------------------------------------
// Combat
MeleeCommandClass=class'AICommand_Base_Zed'
DoorMeleeDistance=200.f
MinTimeBetweenGrabAttacks=5.f
MinDistanceToPerformGrabAttack=188.f
// ---------------------------------------------
// AI / Navigation
DefaultCommandClass=class'AICommand_Base_Zed'
SightCounterInterval=0.35f
bEvadeOnRunOverWarning=false
RunOverEvadeDelayScale=0.25f
bIsPlayer=false
}