1999 lines
58 KiB
Ucode
1999 lines
58 KiB
Ucode
//=============================================================================
|
|
// KFAIController_ZedPatriarch
|
|
//=============================================================================
|
|
// The Patriarch's AIController
|
|
//=============================================================================
|
|
// Killing Floor 2
|
|
// Copyright (C) 2015 Tripwire Interactive LLC
|
|
//=============================================================================
|
|
|
|
class KFAIController_ZedPatriarch extends KFAIController_ZedBoss;
|
|
|
|
/** Cached reference to patriarch pawn */
|
|
var KFPawn_ZedPatriarch MyPatPawn;
|
|
|
|
/*********************************************************************************************
|
|
* Enemy Handling
|
|
**********************************************************************************************/
|
|
|
|
/** Whether attack evaluation is enabled or not */
|
|
var bool bCanEvaluateAttacks;
|
|
|
|
/** How long the Patriarch should wait to start sprinting after losing sight of his enemy */
|
|
var float LostSightSprintDelay;
|
|
|
|
/** How long to wait before ticking off aggro damage */
|
|
var float AggroFalloffWaitTime;
|
|
|
|
/** How much damage to tick off of accumulated aggro, per second */
|
|
var float AggroFalloffPerSecond;
|
|
|
|
/** The threshold at which aggro from a visible player becomes critical */
|
|
var float VisibleAggroDmgThreshold;
|
|
|
|
/** The threshold at which aggro from a hidden player becomes critical */
|
|
var float HiddenAggroDmgThreshold;
|
|
|
|
/**
|
|
* Struct to track AI knowledge of visible enemies
|
|
*/
|
|
struct Patriarch_TrackedEnemyInfo
|
|
{
|
|
/** Enemy we're tracking */
|
|
var KFPawn TrackedEnemy;
|
|
/** Last time enemy was visible */
|
|
var float LastTimeVisible;
|
|
/** Last location enemy was visible */
|
|
var vector LastVisibleLocation;
|
|
/** The last time we took damage from this enemy */
|
|
var float LastTakeDamageTime;
|
|
/** Accumulated damage towards this enemy's aggro rating */
|
|
var int AggroDamage;
|
|
};
|
|
|
|
/** List of tracked enemies that have recently been seen */
|
|
var array<Patriarch_TrackedEnemyInfo> RecentlySeenEnemyList;
|
|
|
|
/** List of tracked enemies that are hidden */
|
|
var array<Patriarch_TrackedEnemyInfo> HiddenEnemies;
|
|
|
|
/** List of enemies that have been minigun attacked */
|
|
var array<KFPawn> LastMinigunEnemies;
|
|
|
|
/** Minimum distance for a minigun attack */
|
|
var float MinMinigunRangeSQ;
|
|
|
|
/** Maximum distance for a minigun attack */
|
|
var float MaxMinigunRangeSQ;
|
|
|
|
/** Maximum distance enemies are allowed to be for a fan fire attack */
|
|
var float MaxFanFireRangeSQ;
|
|
|
|
/** List of enemies that have been charge attacked */
|
|
var array<KFPawn> LastChargedPlayers;
|
|
|
|
/** Min distance an enemy can be for a charge */
|
|
var float MinChargeRangeSQ;
|
|
|
|
/** Are we doing a charge attack? */
|
|
var bool bDoingChargeAttack;
|
|
|
|
/** Last time we executed a charge attack */
|
|
var float LastChargeAttackTime;
|
|
|
|
/** If we were charging a player, but were interrupted by an obstruction */
|
|
var bool bWantsToCharge;
|
|
|
|
/** Cached charge target */
|
|
var Pawn CachedChargeTarget;
|
|
|
|
/** List of enemies that have been tentacle grabbed */
|
|
var array<KFPawn> LastGrabbedPlayers;
|
|
|
|
/** Last time we executed a tentacle grab */
|
|
var float LastGrabAttackTime;
|
|
|
|
/** Minimum distance to be able to use a tentacle attack */
|
|
var float MinTentacleRangeSQ;
|
|
|
|
/** List of enemies that have been missile attacked */
|
|
var array<KFPawn> LastMissileEnemies;
|
|
|
|
/** Last time we executed a missile attack */
|
|
var float LastMissileAttackTime;
|
|
|
|
/** Min distance an enemy can be for a missile attack */
|
|
var float MinMissileRangeSQ;
|
|
|
|
/** Max distance an enemy can be for a missile attack */
|
|
var float MaxMissileRangeSQ;
|
|
|
|
/** Last time we executed a mortar attack */
|
|
var float LastMortarAttackTime;
|
|
|
|
/** Last time we successfully hit an enemy with a melee attack */
|
|
var float LastSuccessfulAttackTime;
|
|
|
|
/** Variable used for determining if the Patriarch should pick a minigun or missile attack */
|
|
var bool bHadMinigunAttack;
|
|
|
|
/* How often to update RecentlySeenEnemyList */
|
|
var() float RecentSeenEnemyListUpdateInterval;
|
|
|
|
/* Last time we updated the RecentlySeenEnemyList*/
|
|
var float LastRecentSeenEnemyListUpdateTime;
|
|
|
|
/** Last time we finished a melee attack */
|
|
var float LastAttackMoveFinishTime;
|
|
|
|
/** How long to wait before evaluating special attacks */
|
|
var float NextAttackCheckTime;
|
|
|
|
/** Last time we activated a battle phase sprint */
|
|
var float LastSprintTime;
|
|
|
|
/** Whether we should sprint until we complete a melee attack */
|
|
var bool bSprintUntilAttack;
|
|
|
|
/** The last time we changed to a new target */
|
|
var float LastRetargetTime;
|
|
|
|
/** How long to wait before attempting to find a new target */
|
|
var float RetargetWaitTime;
|
|
|
|
/*********************************************************************************************
|
|
* Paternal Instinct
|
|
**********************************************************************************************/
|
|
|
|
/** True if we've entered into rage mode */
|
|
var bool bRaging;
|
|
|
|
/** Whether we've raged this phase or not */
|
|
var bool bRagedThisPhase;
|
|
|
|
/** Maximum number of attacks to do before ending rage mode */
|
|
var int MaxRageAttacks;
|
|
|
|
/** Current tally of rage attacks done */
|
|
var int RageAttackCount;
|
|
|
|
/** Maximum distance an enemy can be to be considered for a rage attack */
|
|
var float MaxRageRangeSQ;
|
|
|
|
/** Time before forcing rage mode to end */
|
|
var float RageTimeOut;
|
|
|
|
/** Players that have already been attacked in this rage state */
|
|
var array<KFPawn> RageAttackedTargets;
|
|
|
|
/*********************************************************************************************
|
|
* Flee And Heal
|
|
**********************************************************************************************/
|
|
|
|
/** The amount of damage to do to bumped humans when fleeing */
|
|
var int HumanBumpDamage;
|
|
|
|
/** How much momentum to apply to humans when bumping them */
|
|
var int HumanBumpMomentum;
|
|
|
|
/** Do we have a pending flee? */
|
|
var bool bWantsToFlee;
|
|
|
|
/** Whether we are currently fleeing or not */
|
|
var bool bFleeing;
|
|
|
|
/** The health threshold at which a flee and heal should be triggered */
|
|
var float FleeHealthThreshold;
|
|
|
|
/** The maximum amount of time that can be spent in the flee state */
|
|
var float MaxFleeDuration;
|
|
|
|
/** The maximum distance allowed to flee before command pops */
|
|
var float MaxFleeDistance;
|
|
|
|
/** The start time of the last flee */
|
|
var float FleeStartTime;
|
|
|
|
/** The total cumulative time spent fleeing */
|
|
var float TotalFleeTime;
|
|
|
|
/** Whether the flee was interrupted by the targeting loop */
|
|
var bool bFleeInterrupted;
|
|
|
|
/*********************************************************************************************
|
|
* Initialization, Pawn Possession, and Destruction
|
|
********************************************************************************************* */
|
|
|
|
/** Set MyPatPawn to avoid casting */
|
|
event Possess( Pawn inPawn, bool bVehicleTransition )
|
|
{
|
|
if( KFPawn_ZedPatriarch(inPawn) != none )
|
|
{
|
|
MyPatPawn = KFPawn_ZedPatriarch( inPawn );
|
|
}
|
|
else
|
|
{
|
|
`warn( GetFuncName()$"() attempting to possess "$inPawn$", but it's not a KFPawn_ZedPatriarchBase class! MyPatPawn variable will not be valid." );
|
|
}
|
|
|
|
// Initialize retarget time
|
|
LastRetargetTime = WorldInfo.TimeSeconds;
|
|
|
|
// Initialize sprint time
|
|
LastSprintTime = WorldInfo.TimeSeconds;
|
|
LastSuccessfulAttackTime = WorldInfo.TimeSeconds;
|
|
LastGrabAttackTime = WorldInfo.TimeSeconds;
|
|
LastMissileAttackTime = WorldInfo.TimeSeconds;
|
|
LastMortarAttackTime = WorldInfo.TimeSeconds;
|
|
|
|
// Wait a bit before evaluating special attacks
|
|
NextAttackCheckTime = 2.5f + fRand();
|
|
|
|
// Start evaluating on next tick
|
|
bCanEvaluateAttacks = true;
|
|
|
|
super.Possess( inPawn, bVehicleTransition );
|
|
}
|
|
|
|
function PawnDied( Pawn InPawn )
|
|
{
|
|
if( MyPatPawn != none )
|
|
{
|
|
//`DialogManager.PlayBossDeathDialog( MyHansPawn );
|
|
MyPatPawn = None;
|
|
}
|
|
Super.PawnDied( InPawn );
|
|
}
|
|
|
|
/** Clean up all internal objects and references when the AI is destroyed
|
|
TBD: Look into using Unpossessed() to do this! */
|
|
simulated event Destroyed()
|
|
{
|
|
if( MyPatPawn != none )
|
|
{
|
|
MyPatPawn.ClearMortarTargets();
|
|
}
|
|
MyPatPawn = None;
|
|
MyKFGameInfo.SpawnManager.StopSummoningBossMinions();
|
|
|
|
super.Destroyed();
|
|
}
|
|
|
|
/*********************************************************************************************
|
|
* Enemy and Attack Handling
|
|
********************************************************************************************* */
|
|
|
|
/** We can't allow aggro enemy switches during certain special moves */
|
|
function bool IsAggroEnemySwitchAllowed()
|
|
{
|
|
return super.IsAggroEnemySwitchAllowed()
|
|
&& !MyPatPawn.IsDoingSpecialMove(SM_StandAndShootAttack)
|
|
&& !MyPatPawn.IsDoingSpecialMove(SM_HoseWeaponAttack)
|
|
&& !MyPatPawn.IsDoingSpecialMove(SM_GrappleAttack);
|
|
}
|
|
|
|
/** Whether enemy switch commands can be run */
|
|
function bool CanSwitchEnemies()
|
|
{
|
|
return !bWantsToFlee
|
|
&& !bFleeing
|
|
&& MyPatPawn != none
|
|
&& !MyPatPawn.bIsCloaking
|
|
&& !MyPatPawn.IsDoingSpecialMove(SM_Heal)
|
|
&& !MyPatPawn.IsDoingSpecialMove(SM_StandAndShootAttack)
|
|
&& !MyPatPawn.IsDoingSpecialMove(SM_SonicAttack)
|
|
&& !MyPatPawn.IsDoingSpecialMove(SM_GrappleAttack);
|
|
}
|
|
|
|
/** Returns an aggro rating on a scale of 0.0f to 1.0f */
|
|
function float GetAggroRating( KFPawn KFP )
|
|
{
|
|
local int EnemyIndex;
|
|
local float AggroDmg;
|
|
|
|
EnemyIndex = RecentlySeenEnemyList.Find( 'TrackedEnemy', KFP );
|
|
if( EnemyIndex != INDEX_NONE )
|
|
{
|
|
AggroDmg = RecentlySeenEnemyList[EnemyIndex].AggroDamage;
|
|
if( AggroDmg > 0.f )
|
|
{
|
|
return fMin( AggroDmg/VisibleAggroDmgThreshold, 1.f );
|
|
}
|
|
else
|
|
{
|
|
return 0.f;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
EnemyIndex = HiddenEnemies.Find( 'TrackedEnemy', KFP );
|
|
if( EnemyIndex != INDEX_NONE )
|
|
{
|
|
AggroDmg = HiddenEnemies[EnemyIndex].AggroDamage;
|
|
if( AggroDmg > 0.f )
|
|
{
|
|
return fMin( AggroDmg/HiddenAggroDmgThreshold, 1.f );
|
|
}
|
|
else
|
|
{
|
|
return 0.f;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return 0.5f;
|
|
}
|
|
}
|
|
|
|
return 1.f;
|
|
}
|
|
|
|
/** Overridden to stop retargeting enemies when fleeing or cloaked */
|
|
event bool FindNewEnemy()
|
|
{
|
|
if( !CanSwitchEnemies() )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return super.FindNewEnemy();
|
|
}
|
|
|
|
/** Overridden to stop retargeting enemies when fleeing or cloaked */
|
|
event bool SetEnemy( Pawn NewEnemy )
|
|
{
|
|
if( !CanSwitchEnemies() )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return super.SetEnemy( NewEnemy );
|
|
}
|
|
|
|
/** Forces the enemy to the new pawn, bypassing CanSwitchEnemies() */
|
|
function ForceSetEnemy( Pawn NewEnemy )
|
|
{
|
|
super.SetEnemy( NewEnemy );
|
|
}
|
|
|
|
/** Overridden to stop retargeting enemies when fleeing or cloaked */
|
|
event ChangeEnemy( Pawn NewEnemy, optional bool bCanTaunt = true )
|
|
{
|
|
local Pawn OldEnemy;
|
|
|
|
if( !CanSwitchEnemies() )
|
|
{
|
|
return;
|
|
}
|
|
|
|
OldEnemy = Enemy;
|
|
|
|
super.ChangeEnemy( NewEnemy, !MyPatPawn.bIsCloaking && bCanTaunt );
|
|
|
|
if( OldEnemy != Enemy )
|
|
{
|
|
LastRetargetTime = WorldInfo.TimeSeconds;
|
|
}
|
|
}
|
|
|
|
/** Sets the enemy to the best target, based on several factors */
|
|
function bool SetBestTarget( out array<KFPawn> RecentTargets,
|
|
optional float MinDistSQ=-1.f,
|
|
optional float MaxDistSQ=-1.f,
|
|
optional float ClampFOV=-1.f,
|
|
optional bool bPreferFurtherTargets,
|
|
optional bool bIsWeaponAttack )
|
|
{
|
|
local KFPawn_Human KFPH;
|
|
local KFPawn KFP, BestTarget;
|
|
local int i, FoundIndex, BestIndex;
|
|
local float TempRating, BestRating;
|
|
local vector TempDist;
|
|
|
|
// Filter out dead/absent players
|
|
for( i = 0; i < RecentTargets.Length; ++i )
|
|
{
|
|
if( RecentTargets[i] == none || !RecentTargets[i].IsAliveAndWell() )
|
|
{
|
|
RecentTargets.Remove( i, 1 );
|
|
i--;
|
|
}
|
|
}
|
|
|
|
// If we've had no previously charged players or in solo play, charge enemy
|
|
if( RecentTargets.Length == 0 || WorldInfo.NetMode == NM_StandAlone )
|
|
{
|
|
// Fallback
|
|
if( Enemy == none )
|
|
{
|
|
//if( !bPreferFurtherTargets )
|
|
//{
|
|
ChangeEnemy( GetClosestEnemy(), false );
|
|
//}
|
|
}
|
|
|
|
// Can't get an enemy for whatever reason
|
|
if( Enemy == none )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Initial rating value is based on distance
|
|
TempRating = VSizeSQ( MyPatPawn.Location - Enemy.Location );
|
|
|
|
// Early out if we're outside distance limits
|
|
if( (MinDistSQ >= 0.f && TempRating < MinDistSQ) || (MaxDistSQ > 0.f && TempRating > MaxDistSQ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Even if we can see our targets, make sure that our gun has clearance
|
|
if( bIsWeaponAttack && !IsWeaponArmClear(Enemy.Location) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
KFP = KFPawn( Enemy );
|
|
FoundIndex = RecentTargets.Find( KFP );
|
|
if( FoundIndex == INDEX_NONE )
|
|
{
|
|
RecentTargets[RecentTargets.Length] = KFP;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Try to find a better target
|
|
foreach WorldInfo.AllPawns( class'KFPawn_Human', KFPH )
|
|
{
|
|
if( KFPH != none && KFPH.IsAliveAndWell() )
|
|
{
|
|
// Initial rating value is based on distance
|
|
TempDist = KFPH.Location - MyPatPawn.Location;
|
|
TempRating = VSizeSQ( TempDist );
|
|
|
|
// Filter based on distance limits, if we have any
|
|
if( (MinDistSQ >= 0.f && TempRating < MinDistSQ) || (MaxDistSQ > 0.f && TempRating > MaxDistSQ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Filter out pawns not in the clamp FOV, if we have one
|
|
if( vector(Pawn.Rotation) dot Normal(TempDist) < ClampFOV )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Even if we can see our targets, make sure that our gun has clearance
|
|
if( bIsWeaponAttack && !IsWeaponArmClear(KFPH.Location) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Recently attacked players get lowest priority
|
|
FoundIndex = RecentTargets.Find( KFPH );
|
|
if( FoundIndex != INDEX_NONE )
|
|
{
|
|
// Scale priority based on position in list -- the higher the more recent
|
|
TempRating *= bPreferFurtherTargets ?
|
|
( 2.5f + 2.5f*(FMax(float(FoundIndex),1.f)/RecentTargets.Length) ) :
|
|
( 1.f - 0.5f*(FMax(float(FoundIndex),1.f)/RecentTargets.Length) );
|
|
}
|
|
|
|
// Lower priority for players that haven't been seen recently
|
|
if( RecentlySeenEnemyList.Find('TrackedEnemy', KFPH) == INDEX_NONE )
|
|
{
|
|
TempRating *= bPreferFurtherTargets ? 0.5f : 2.0f;
|
|
}
|
|
|
|
// Apply aggro rating
|
|
TempRating *= bPreferFurtherTargets ? 0.5f + 0.5f*GetAggroRating(KFPH) : 1.0f - 0.5f*GetAggroRating(KFPH);
|
|
|
|
if( BestRating == 0.f || ((bPreferFurtherTargets && TempRating > BestRating)
|
|
|| (!bPreferFurtherTargets && TempRating < BestRating)) )
|
|
{
|
|
BestTarget = KFPH;
|
|
BestRating = TempRating;
|
|
BestIndex = FoundIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
if( BestTarget != none )
|
|
{
|
|
// Set our enemy
|
|
ChangeEnemy( BestTarget, false );
|
|
|
|
// Shuffle target out of current spot if chosen from array
|
|
if( BestIndex != INDEX_NONE )
|
|
{
|
|
RecentTargets.Remove( BestIndex, 1 );
|
|
}
|
|
|
|
// Place target at end of array
|
|
RecentTargets[RecentTargets.Length] = BestTarget;
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/** Override to tick the ranged combat system */
|
|
simulated function Tick( FLOAT DeltaTime )
|
|
{
|
|
Super.Tick(DeltaTime);
|
|
|
|
UpdateRecentlySeenEnemyList();
|
|
EvaluateAttacks( DeltaTime );
|
|
}
|
|
|
|
/** NPC has seen a player - use SeeMonster for similar notifications about seeing any pawns (currently not in use) */
|
|
event SeePlayer( Pawn Seen )
|
|
{
|
|
local int EnemyListIndex, HiddenEnemyIndex;
|
|
local Patriarch_TrackedEnemyInfo NewTrackedEnemy;
|
|
local KFPawn KFP;
|
|
|
|
Super.SeePlayer(Seen);
|
|
|
|
// Evaluate sprinting when visibility changes
|
|
EvaluateSprinting();
|
|
|
|
// Reject potential enemy if it's invalid or not on our team, or if it's already my current enemy, or if my pawn is dead or invalid
|
|
if( Seen == none || !Seen.IsAliveAndWell() || Pawn.IsSameTeam( Seen ) ||
|
|
Pawn == none || !Pawn.IsAliveAndWell() || !Seen.CanAITargetThisPawn(self) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
KFP = KFPawn(Seen);
|
|
|
|
// Add or update recently seen enemy in the list for tracking ranged enemies
|
|
if( KFP != none )
|
|
{
|
|
EnemyListIndex = RecentlySeenEnemyList.Find('TrackedEnemy', KFP);
|
|
if( EnemyListIndex == INDEX_NONE )
|
|
{
|
|
HiddenEnemyIndex = HiddenEnemies.Find('TrackedEnemy', KFP);
|
|
if( HiddenEnemyIndex != INDEX_NONE )
|
|
{
|
|
HiddenEnemies.Remove(HiddenEnemyIndex, 1);
|
|
}
|
|
NewTrackedEnemy.TrackedEnemy = KFP;
|
|
NewTrackedEnemy.LastTimeVisible = WorldInfo.TimeSeconds;
|
|
NewTrackedEnemy.LastVisibleLocation = Seen.Location;
|
|
|
|
// Delay attacks a bit if we are just acquiring an enemy after having not seen anyone
|
|
if( RecentlySeenEnemyList.Length == 0 )
|
|
{
|
|
NextAttackCheckTime = 1.5f + fRand();
|
|
}
|
|
|
|
RecentlySeenEnemyList[RecentlySeenEnemyList.Length] = NewTrackedEnemy;
|
|
}
|
|
else
|
|
{
|
|
RecentlySeenEnemyList[EnemyListIndex].LastTimeVisible = WorldInfo.TimeSeconds;
|
|
RecentlySeenEnemyList[EnemyListIndex].LastVisibleLocation = Seen.Location;
|
|
}
|
|
|
|
LastEnemySightedTime = WorldInfo.TimeSeconds;
|
|
}
|
|
}
|
|
|
|
/** Update the ranged enemy visibility tracking */
|
|
function UpdateRecentlySeenEnemyList()
|
|
{
|
|
local int i;
|
|
local bool bWantsNewEnemy;
|
|
|
|
if( LastRecentSeenEnemyListUpdateTime == 0 || `TimeSince(LastRecentSeenEnemyListUpdateTime) > RecentSeenEnemyListUpdateInterval )
|
|
{
|
|
LastRecentSeenEnemyListUpdateTime = WorldInfo.TimeSeconds;
|
|
|
|
for( i = RecentlySeenEnemyList.Length-1; i >= 0; i-- )
|
|
{
|
|
bWantsNewEnemy = false;
|
|
|
|
if( RecentlySeenEnemyList[i].TrackedEnemy == none
|
|
|| !RecentlySeenEnemyList[i].TrackedEnemy.IsAliveAndWell()
|
|
|| !RecentlySeenEnemyList[i].TrackedEnemy.CanAITargetThisPawn(self)
|
|
|| `TimeSince(RecentlySeenEnemyList[i].LastTimeVisible) > 0.5f )
|
|
{
|
|
// If enemy is still valid but out of view, add it to hidden enemies
|
|
if( RecentlySeenEnemyList[i].TrackedEnemy != none && RecentlySeenEnemyList[i].TrackedEnemy.IsAliveAndWell() )
|
|
{
|
|
HiddenEnemies[HiddenEnemies.Length] = RecentlySeenEnemyList[i];
|
|
}
|
|
|
|
// Try to get a new enemy
|
|
if( Enemy == RecentlySeenEnemyList[i].TrackedEnemy )
|
|
{
|
|
if( RecentlySeenEnemyList.Length > 1 )
|
|
{
|
|
bWantsNewEnemy = true;
|
|
}
|
|
else
|
|
{
|
|
FindNewEnemy();
|
|
}
|
|
}
|
|
|
|
RecentlySeenEnemyList.Remove(i, 1);
|
|
|
|
if( bWantsNewEnemy )
|
|
{
|
|
ChangeEnemy( RecentlySeenEnemyList[Rand(RecentlySeenEnemyList.Length)].TrackedEnemy );
|
|
}
|
|
}
|
|
else if( CanSee(RecentlySeenEnemyList[i].TrackedEnemy) )
|
|
{
|
|
if( `TimeSince(RecentlySeenEnemyList[i].LastTakeDamageTime) > AggroFalloffWaitTime )
|
|
{
|
|
RecentlySeenEnemyList[i].AggroDamage -= fMax( AggroFalloffPerSecond * (RecentSeenEnemyListUpdateInterval/1.f), 0.f );
|
|
}
|
|
RecentlySeenEnemyList[i].LastVisibleLocation = RecentlySeenEnemyList[i].TrackedEnemy.Location;
|
|
RecentlySeenEnemyList[i].LastTimeVisible = WorldInfo.TimeSeconds;
|
|
}
|
|
}
|
|
|
|
for( i = HiddenEnemies.Length-1; i >= 0; i-- )
|
|
{
|
|
if( `TimeSince(HiddenEnemies[i].LastTakeDamageTime) > AggroFalloffWaitTime )
|
|
{
|
|
HiddenEnemies[i].AggroDamage -= fMax( AggroFalloffPerSecond * (RecentSeenEnemyListUpdateInterval/1.f), 0.f );
|
|
}
|
|
if( HiddenEnemies[i].TrackedEnemy == none
|
|
|| !HiddenEnemies[i].TrackedEnemy.IsAliveAndWell()
|
|
|| !HiddenEnemies[i].TrackedEnemy.CanAITargetThisPawn(self) )
|
|
{
|
|
HiddenEnemies.Remove(i, 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Evaluates whether or not the Patriarch can do a special attack */
|
|
function EvaluateAttacks( float DeltaTime )
|
|
{
|
|
local bool bCanFireMinigun, bCanFireMissile, bCanFireMortar, bShouldFireMortar, bShouldFireMissile, bMortarBarrage;
|
|
|
|
if( !bCanEvaluateAttacks )
|
|
{
|
|
return;
|
|
}
|
|
|
|
//Don't attack while we're in theatrics
|
|
if (CommandList != none && CommandList.class == class'AICommand_BossTheatrics')
|
|
{
|
|
return;
|
|
}
|
|
|
|
NextAttackCheckTime -= DeltaTime;
|
|
|
|
if( bWantsToCharge || bWantsToFlee || bFleeing || MyPatPawn == none || MyPatPawn.IsDoingSpecialMove() || NextAttackCheckTime > 0.f )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Evaluate grab attack
|
|
if ( MyPatPawn.CanTentacleGrab()
|
|
&& RecentlySeenEnemyList.Length > 0
|
|
&& `TimeSince(LastGrabAttackTime) > MyPatPawn.TentacleGrabCooldownTime
|
|
&& (!MyPatPawn.bIsCloaking || fRand() < 0.25f) )
|
|
{
|
|
if( SetBestTarget(LastGrabbedPlayers, MinTentacleRangeSQ, Square(class'KFSM_Patriarch_Grapple'.default.MaxRange*0.8f*MyPatPawn.GetAttackRangeScale()), 0.4f, true, true) )
|
|
{
|
|
MyPatPawn.SetCloaked( false );
|
|
class'AICommand_Patriarch_Grab'.static.TentacleGrab( self );
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Evaluate charge attack
|
|
if( !MyPatPawn.bIsCloaking
|
|
&& MyPatPawn.CanChargeAttack()
|
|
&& (bHadMinigunAttack || fRand() < 0.75f)
|
|
&& `TimeSince(LastChargeAttackTime) > MyPatPawn.ChargeAttackCooldownTime )
|
|
{
|
|
// Make sure we have a target
|
|
if( SetBestTarget(LastChargedPlayers, MinChargeRangeSQ) )
|
|
{
|
|
// Cache our charge target
|
|
CachedChargeTarget = Enemy;
|
|
|
|
bDoingChargeAttack = true;
|
|
bSprintUntilAttack = true;
|
|
MyPatPawn.SetSprinting( true );
|
|
MyPatPawn.SetCloaked( true );
|
|
|
|
// Delay ranged attack checking a bit
|
|
NextAttackCheckTime = 2.5f + fRand();
|
|
|
|
SetTimer( 2.f, false, nameOf(Timer_SearchForChargeObstructions) );
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Evaluate weapon attack
|
|
bCanFireMinigun = (!MyPatPawn.CanChargeAttack() || fRand() < 0.5f)
|
|
&& (!MyPatPawn.bIsCloaking || fRand() < 0.75f)
|
|
&& RecentlySeenEnemyList.Length > 0
|
|
&& `TimeSince(LastSuccessfulAttackTime) > MyPatPawn.MinigunAttackCooldownTime;
|
|
bCanFireMissile = (!MyPatPawn.CanChargeAttack() || fRand() < 0.5f)
|
|
&& (!MyPatPawn.bIsCloaking || fRand() < 0.75f)
|
|
&& MyPatPawn.CanMissileAttack() && RecentlySeenEnemyList.Length > 0
|
|
&& `TimeSince(LastMissileAttackTime) > MyPatPawn.MissileAttackCooldownTime;
|
|
bCanFireMortar = !MyPatPawn.bIsCloaking
|
|
&& MyPatPawn.CanMortarAttack()
|
|
&& `TimeSince(LastMortarAttackTime) > MyPatPawn.MortarAttackCooldownTime;
|
|
|
|
if( bCanFireMissile || bCanFireMinigun || bCanFireMortar )
|
|
{
|
|
// Decide if we can fire our mortar attack
|
|
bShouldFireMortar = bCanFireMortar
|
|
&& (!bCanFireMissile || fRand() < 0.5f)
|
|
&& (!bCanFireMinigun || fRand() < 0.75f)
|
|
&& IsCeilingClear();
|
|
|
|
if( bShouldFireMortar )
|
|
{
|
|
// Random chance to do an area denial
|
|
if( MyPatPawn.CanDoMortarBarrage() && fRand() < 0.2f )
|
|
{
|
|
bMortarBarrage = true;
|
|
bShouldFireMortar = MyPatPawn.CollectMortarTargets( true, true );
|
|
}
|
|
else if( HiddenEnemies.Length > 0 )
|
|
{
|
|
bMortarBarrage = false;
|
|
bShouldFireMortar = SomeEnemiesAreHidden() && MyPatPawn.CollectMortarTargets( true );
|
|
}
|
|
else
|
|
{
|
|
bShouldFireMortar = false;
|
|
}
|
|
}
|
|
|
|
// Decide whether we should fire a missile or the minigun
|
|
bShouldFireMissile = !bShouldFireMortar && bCanFireMissile
|
|
&& (((bCanFireMinigun || !bHadMinigunAttack) && fRand() < 0.2f) || fRand() < 0.6f)
|
|
&& SetBestTarget( LastMissileEnemies, MinMissileRangeSQ, MaxMissileRangeSQ*MyPatPawn.GetAttackRangeScale(), 0.5f, true, true );
|
|
|
|
// No other attacks, find a minigun target!
|
|
if( bCanFireMinigun && !bShouldFireMissile && !bShouldFireMortar )
|
|
{
|
|
bCanFireMinigun = SetBestTarget( LastMinigunEnemies, MinMinigunRangeSQ, MaxMinigunRangeSQ*MyPatPawn.GetAttackRangeScale(), 0.25f, false, true );
|
|
}
|
|
|
|
if( bShouldFireMortar )
|
|
{
|
|
class'AICommand_Patriarch_MortarAttack'.static.FireMortar( self, bMortarBarrage );
|
|
return;
|
|
}
|
|
else if( bShouldFireMissile )
|
|
{
|
|
bHadMinigunAttack = false;
|
|
MyPatPawn.SetCloaked( false );
|
|
class'AICommand_Patriarch_MissileAttack'.static.FireMissiles( self );
|
|
return;
|
|
}
|
|
else if( bCanFireMinigun )
|
|
{
|
|
bHadMinigunAttack = true;
|
|
MyPatPawn.SetCloaked( false );
|
|
class'AICommand_Patriarch_MinigunBarrage'.static.MinigunBarrage( self );
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Evaluate attacks every half second
|
|
NextAttackCheckTime = 0.5f;
|
|
}
|
|
|
|
/** Returns true if the path from the weapon arm to the target location is clear */
|
|
function bool IsWeaponArmClear( vector EndTrace )
|
|
{
|
|
local vector StartTrace;
|
|
|
|
MyPatPawn.Mesh.GetSocketWorldLocationAndRotation( 'MissileCenter', StartTrace );
|
|
|
|
return MyPatPawn.FastTrace( EndTrace, StartTrace,, true );
|
|
}
|
|
|
|
/**
|
|
* Adjusts aim to always point at the enemy we're targeting
|
|
* @param W, weapon about to fire
|
|
* @param StartFireLoc, world location of weapon fire start trace, or projectile spawn loc.
|
|
*/
|
|
function Rotator GetAdjustedAimFor( Weapon W, vector StartFireLoc )
|
|
{
|
|
if( Enemy != none )
|
|
{
|
|
return rotator(Enemy.Location - StartFireLoc);
|
|
}
|
|
|
|
return super.GetAdjustedAimFor( W, StartFireLoc );
|
|
}
|
|
|
|
function DoStrike()
|
|
{
|
|
local name AttackName;
|
|
|
|
if( MyPatPawn != none && MyPatPawn.PawnAnimInfo != none )
|
|
{
|
|
AttackName = MyPatPawn.PawnAnimInfo.Attacks[PendingAnimStrikeIndex].Tag;
|
|
|
|
// @todo: figure out a way to make this less hard-coded? (see also KFDialogManager::PlayHansKilledDialog)
|
|
if( AttackName == 'Radial' )
|
|
{
|
|
`DialogManager.PlayPattyWhirlwindDialog( MyPatPawn );
|
|
}
|
|
}
|
|
|
|
super.DoStrike();
|
|
}
|
|
|
|
/*********************************************************************************************
|
|
* Sprinting
|
|
**********************************************************************************************/
|
|
|
|
/** Evaluate if we should start/stop sprinting, and then set the sprinting flag */
|
|
function EvaluateSprinting()
|
|
{
|
|
if( MyKFPawn != none && MyKFPawn.IsAliveAndWell() && Enemy != none )
|
|
{
|
|
if( ShouldSprint() )
|
|
{
|
|
MyKFPawn.SetSprinting( true );
|
|
}
|
|
else
|
|
{
|
|
MyKFPawn.SetSprinting( false );
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Timer function called during latent moves that determines whether NPC should sprint or stop sprinting */
|
|
function bool ShouldSprint()
|
|
{
|
|
if( Enemy != none && MyPatPawn != none && !MyPatPawn.bIsHeadless )
|
|
{
|
|
// Don't allow sprinting in minigun attack
|
|
if( MyPatPawn.IsDoingSpecialMove(SM_HoseWeaponAttack) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Always sprint if fleeing
|
|
if( bFleeing )
|
|
{
|
|
//`log(self@GetFuncName()$" bInFleeAndHealMode should sprint!");
|
|
return true;
|
|
}
|
|
|
|
// Always sprint if cloaked
|
|
if( MyPatPawn.bIsCloaking )
|
|
{
|
|
//`log(self@GetFuncName()$" bIsCloaking should sprint!");
|
|
return true;
|
|
}
|
|
|
|
// Always sprint if raging
|
|
if( bRaging )
|
|
{
|
|
//`log(self@GetFuncName()$" In Paternal Instinct mode, should sprint!");
|
|
return true;
|
|
}
|
|
|
|
// EMP check always comes after any forced sprint state
|
|
if( MyPatPawn.bEmpPanicked )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Sprint until we attack
|
|
if( bSprintUntilAttack )
|
|
{
|
|
//`log(self@GetFuncName()$" sprint until attack, should sprint!");
|
|
return true;
|
|
}
|
|
|
|
// Sprint if we can't see our enemy
|
|
if( LastEnemySightedTime == 0 || `TimeSince(LastEnemySightedTime) > LostSightSprintDelay )
|
|
{
|
|
//`log(self@GetFuncName()$" don't see any enemy should sprint = true! LastEnemySightedTime: "$LastEnemySightedTime$" TimeSince(LastEnemySightedTime): "$`TimeSince(LastEnemySightedTime));
|
|
return true;
|
|
}
|
|
|
|
//`log(self@GetFuncName()$" Generic should sprint depending on phase: "$MyPatPawn.DesireSprintingInThisPhase());
|
|
if( MyPatPawn.DesireSprintingInThisPhase() && `TimeSince(LastSprintTime) > MyPatPawn.SprintCooldownTime )
|
|
{
|
|
bSprintUntilAttack = true;
|
|
return true;
|
|
}
|
|
}
|
|
//`log(self@GetFuncName()$" Generic should sprint = false!");
|
|
return false;
|
|
}
|
|
|
|
/** Disallow sprinting if we're in a minigun */
|
|
function bool CanSetSprinting( bool bNewSprintStatus )
|
|
{
|
|
return super.CanSetSprinting( bNewSprintStatus ) && ( !bNewSprintStatus || !MyPatPawn.IsDoingSpecialMove(SM_HoseWeaponAttack) );
|
|
}
|
|
|
|
/*********************************************************************************************
|
|
* Mortar
|
|
**********************************************************************************************/
|
|
|
|
/** Returns true if the ceiling is clear for a mortar attack */
|
|
function bool IsCeilingClear()
|
|
{
|
|
local vector TraceStart, TraceEnd, Extent;
|
|
|
|
TraceStart = MyPatPawn.Location + vect(0,0,1)*MyPatPawn.GetCollisionHeight();
|
|
TraceEnd = TraceStart + vect(0,0,1)*500.f;
|
|
Extent.X = MyPatPawn.GetCollisionRadius() * 2.f;
|
|
Extent.Y = Extent.X;
|
|
Extent.Z = 1.f;
|
|
|
|
return MyPatPawn.FastTrace( TraceEnd, TraceStart, Extent, true );
|
|
}
|
|
|
|
/** Returns true if a certain percentage of enemies are hidden */
|
|
function bool SomeEnemiesAreHidden()
|
|
{
|
|
local KFPawn KFP;
|
|
local float TargetDist;
|
|
local int i, NumValidHiddenEnemies;
|
|
|
|
for( i = 0; i < HiddenEnemies.Length; ++i )
|
|
{
|
|
KFP = HiddenEnemies[i].TrackedEnemy;
|
|
if( !KFP.IsAliveAndWell() || MyPatPawn.IsSameTeam(KFP) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Too close or too far
|
|
TargetDist = VSizeSQ(KFP.Location - MyPatPawn.Location);
|
|
if( TargetDist < MyPatPawn.MinMortarRangeSQ || TargetDist > MyPatPawn.MaxMortarRangeSQ )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
NumValidHiddenEnemies++;
|
|
|
|
if( NumValidHiddenEnemies >= 2
|
|
|| NumValidHiddenEnemies >= (Max(HiddenEnemies.Length + RecentlySeenEnemyList.Length, 1)/2) )
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/*********************************************************************************************
|
|
* Notifications
|
|
**********************************************************************************************/
|
|
|
|
/** Overloaded to handle door usage in cloaked state */
|
|
function NotifyAttackDoor( KFDoorActor Door )
|
|
{
|
|
// We need to count up the total flee time so infinite fleeing isn't possible
|
|
if( bFleeing || bWantsToFlee )
|
|
{
|
|
if( bFleeing )
|
|
{
|
|
bFleeInterrupted = true;
|
|
bFleeing = false;
|
|
TotalFleeTime = TotalFleeTime + (WorldInfo.TimeSeconds - FleeStartTime);
|
|
bWantsToFlee = true;
|
|
}
|
|
|
|
// Kill our flee and move commands
|
|
AbortCommand( CommandList );
|
|
|
|
// Allow melee again
|
|
EnableMeleeRangeEventProbing();
|
|
|
|
// Restart default command
|
|
BeginCombatCommand( GetDefaultCommand(), "Restarting default command" );
|
|
}
|
|
else if( MyPatPawn.bIsCloaking )
|
|
{
|
|
MyPatPawn.SetCloaked( false );
|
|
bWantsToCharge = true;
|
|
}
|
|
|
|
super.NotifyAttackDoor( Door );
|
|
}
|
|
|
|
/** Overloaded to handle door usage in cloaked state */
|
|
function bool DoorFinished()
|
|
{
|
|
local bool bSuperFinished;
|
|
|
|
bSuperFinished = super.DoorFinished();
|
|
|
|
if( bWantsToFlee && !bFleeing )
|
|
{
|
|
if( MyPatPawn.IsDoingSpecialMove() )
|
|
{
|
|
MyPatPawn.EndSpecialMove();
|
|
}
|
|
Flee();
|
|
}
|
|
else if( bWantsToCharge )
|
|
{
|
|
ChangeEnemy( CachedChargeTarget, false );
|
|
bDoingChargeAttack = true;
|
|
bSprintUntilAttack = true;
|
|
bWantsToCharge = false;
|
|
MyPatPawn.SetSprinting( true );
|
|
MyPatPawn.SetCloaked( true );
|
|
SetTimer( 2.f, false, nameOf(Timer_SearchForChargeObstructions) );
|
|
|
|
// Delay ranged attack checking a bit
|
|
NextAttackCheckTime = 2.5f + fRand();
|
|
}
|
|
|
|
return bSuperFinished;
|
|
}
|
|
|
|
/** Overloaded to decloak patty and set pending flee */
|
|
function NotifyAttackActor( Actor A )
|
|
{
|
|
// do nothing for now, causes issues
|
|
|
|
/**
|
|
// We need to count up the total flee time so infinite fleeing isn't possible
|
|
if( bFleeing )
|
|
{
|
|
TotalFleeTime = TotalFleeTime + (WorldInfo.TimeSeconds - FleeStartTime);
|
|
bWantsToFlee = true;
|
|
bFleeInterrupted = true;
|
|
bFleeing = false;
|
|
|
|
// Kill our flee command
|
|
AbortCommand( FindCommandOfClass(class'AICommand_Flee') );
|
|
|
|
// Allow melee again
|
|
EnableMeleeRangeEventProbing();
|
|
}
|
|
else if( MyPatPawn.bIsCloaking )
|
|
{
|
|
MyPatPawn.SetCloaked( false );
|
|
bWantsToCharge = true;
|
|
}
|
|
|
|
super.NotifyAttackActor( A );**/
|
|
}
|
|
|
|
/** Command finished. Used to catch instances where the flee command is interrupted by another command */
|
|
function NotifyCommandFinished( AICommand FinishedCommand )
|
|
{
|
|
if( !bWantsToFlee && bFleeing && PendingDoor == none && (ActorEnemy == none || ActorEnemy.bPendingDelete) && AICommand_Flee(FinishedCommand) != none )
|
|
{
|
|
// Add to our total flee time
|
|
TotalFleeTime = TotalFleeTime + (WorldInfo.TimeSeconds - FleeStartTime);
|
|
|
|
// Abort the flee command
|
|
AbortCommand( FinishedCommand );
|
|
|
|
// Cancel any special moves
|
|
if( MyPatPawn.IsDoingSpecialMove() )
|
|
{
|
|
MyPatPawn.EndSpecialMove();
|
|
}
|
|
|
|
// Delay flee by a tiny bit to allow command to finish up
|
|
SetTimer( 0.06f, false, nameOf(Flee) );
|
|
}
|
|
}
|
|
|
|
function NotifySpecialMoveEnded( KFSpecialMove SM )
|
|
{
|
|
super.NotifySpecialMoveEnded(SM);
|
|
|
|
bFleeInterrupted = false;
|
|
|
|
if( !bWantsToFlee )
|
|
{
|
|
if( SM.Handle == 'KFSM_DoorMeleeAttack'
|
|
|| SM.Handle == 'KFSM_MeleeAttack'
|
|
|| SM.Handle == 'KFSM_Patriarch_Grapple'
|
|
|| SM.Handle == 'KFSM_Patriarch_MinigunBarrage' )
|
|
{
|
|
if( PendingDoor == none && bWantsToCharge && CachedChargeTarget != none && CachedChargeTarget.IsAliveAndWell() )
|
|
{
|
|
ChangeEnemy( CachedChargeTarget, false );
|
|
bDoingChargeAttack = true;
|
|
bSprintUntilAttack = true;
|
|
bWantsToCharge = false;
|
|
MyPatPawn.SetSprinting( true );
|
|
MyPatPawn.SetCloaked( true );
|
|
SetTimer( 2.f, false, nameOf(Timer_SearchForChargeObstructions) );
|
|
|
|
// Delay ranged attack checking a bit
|
|
NextAttackCheckTime = 2.5f + fRand();
|
|
}
|
|
else if( bRaging )
|
|
{
|
|
RageAttackCount++;
|
|
UpdateRageState();
|
|
}
|
|
else if( bSprintUntilAttack )
|
|
{
|
|
if( bDoingChargeAttack )
|
|
{
|
|
LastChargeAttackTime = WorldInfo.TimeSeconds;
|
|
}
|
|
|
|
LastSprintTime = WorldInfo.TimeSeconds;
|
|
bSprintUntilAttack = false;
|
|
bWantsToCharge = false;
|
|
CachedChargeTarget = none;
|
|
ClearTimer( nameOf(Timer_SearchForChargeObstructions) );
|
|
}
|
|
|
|
// Retarget if it's been enough time since we changed targets
|
|
if( SM.Handle == 'KFSM_MeleeAttack' && `TimeSince(LastRetargetTime) > RetargetWaitTime )
|
|
{
|
|
CheckForEnemiesInFOV( 3000.f, -1.f, 1.f, true );
|
|
}
|
|
}
|
|
else if( SM.Handle == 'KFSM_Patriarch_Heal' )
|
|
{
|
|
// Get a new enemy
|
|
ChangeEnemy( GetClosestEnemy(), true );
|
|
|
|
// Start moving to enemy
|
|
SetEnemyMoveGoal( self, true );
|
|
|
|
// Enable attacks again
|
|
NextAttackCheckTime = fRand();
|
|
}
|
|
|
|
// Use special move handlers, AI commands have a slight delay before popping
|
|
if( SM.Handle == 'KFSM_Patriarch_MinigunBarrage' )
|
|
{
|
|
NextAttackCheckTime = 2.25f+fRand();
|
|
CheckForEnemiesInFOV( 2000.f, -1.f, 1.f );
|
|
}
|
|
else if( SM.Handle == 'KFSM_Patriarch_MissileAttack' )
|
|
{
|
|
LastMissileAttackTime = WorldInfo.TimeSeconds;
|
|
NextAttackCheckTime = 2.25f+fRand();
|
|
CheckForEnemiesInFOV( 2000.f, -1.f, 1.f );
|
|
}
|
|
else if( SM.Handle == 'KFSM_Patriarch_MortarAttack' )
|
|
{
|
|
LastMortarAttackTime = WorldInfo.TimeSeconds;
|
|
NextAttackCheckTime = 2.25f+fRand();
|
|
CheckForEnemiesInFOV( 2000.f, -1.f, 1.f );
|
|
}
|
|
else if( SM.Handle == 'KFSM_Patriarch_Grapple' )
|
|
{
|
|
// We don't want the grab to retarget so we can kick our enemy in the face
|
|
LastSuccessfulAttackTime = WorldInfo.TimeSeconds;
|
|
LastGrabAttackTime = WorldInfo.TimeSeconds;
|
|
NextAttackCheckTime = 2.25f+fRand();
|
|
}
|
|
|
|
// Turn ranged attack eval back on
|
|
bCanEvaluateAttacks = true;
|
|
|
|
// Turn melee attack probing back on
|
|
EnableMeleeRangeEventProbing();
|
|
}
|
|
else if( PendingDoor == none && !bFleeing )
|
|
{
|
|
bSprintUntilAttack = false;
|
|
Flee();
|
|
}
|
|
|
|
// Evaluate sprinting whenever we finish a special move so sprinting will be snappy!
|
|
EvaluateSprinting();
|
|
|
|
// Reset charge attack state
|
|
bDoingChargeAttack = false;
|
|
}
|
|
|
|
/** Reset minigun timer if we've landed a successful melee attack */
|
|
function NotifyMeleeDamageDealt()
|
|
{
|
|
super.NotifyMeleeDamageDealt();
|
|
|
|
LastSuccessfulAttackTime = WorldInfo.TimeSeconds;
|
|
}
|
|
|
|
/** If a monster other than the Patriarch is killed, and the Patriarch sees it, rage out */
|
|
function NotifyKilled( Controller Killer, Controller Killed, pawn KilledPawn, class<DamageType> damageType )
|
|
{
|
|
if( GetIsInZedVictoryState() )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if( self == Killer && Killed.GetTeamNum() != GetTeamNum() )
|
|
{
|
|
`DialogManager.PlayPattyKilledDialog( MyPatPawn, damageType );
|
|
}
|
|
else if( !bWantsToFlee && !bFleeing && !bRagedThisPhase && MyPatPawn.MaxRageAttacks > 0
|
|
&& !MyPatPawn.IsDoingSpecialMove(SM_Heal) && Killed != self && Killed.GetTeamNum() == GetTeamNum() )
|
|
{
|
|
if( CanSee(KilledPawn) )
|
|
{
|
|
if( Killer.Pawn != none )
|
|
{
|
|
ChangeEnemy( Killer.Pawn, false );
|
|
}
|
|
StartPaternalInstinct();
|
|
}
|
|
}
|
|
else if( Killed.Pawn == Enemy && MyPatPawn.IsDoingSpecialMove(SM_HoseWeaponAttack) )
|
|
{
|
|
// Allow minigun kills to either force a target switch or end the move
|
|
KFSM_Patriarch_MinigunBarrage( MyPatPawn.SpecialMoves[SM_HoseWeaponAttack] ).Timer_SearchForMinigunTargets();
|
|
}
|
|
|
|
super.NotifyKilled( Killer, Killed, KilledPawn, damageType );
|
|
}
|
|
|
|
/** Notification from the pawn that it has taken damage */
|
|
function NotifyTakeHit( Controller InstigatedBy, vector HitLocation, int Damage, class<DamageType> damageType, vector Momentum )
|
|
{
|
|
local KFPawn EnemyPawn;
|
|
local int EnemyIndex;
|
|
local int pawnDmg;
|
|
local vector pawnRotToVec;
|
|
|
|
// Aggro system
|
|
if( InstigatedBy.Pawn != none )
|
|
{
|
|
EnemyPawn = KFPawn(InstigatedBy.Pawn);
|
|
if( EnemyPawn != none )
|
|
{
|
|
EnemyIndex = RecentlySeenEnemyList.Find( 'TrackedEnemy', EnemyPawn );
|
|
if( EnemyIndex != INDEX_NONE )
|
|
{
|
|
pawnDmg = Damage;
|
|
pawnRotToVec = vector(MyPatPawn.Rotation);
|
|
pawnRotToVec.Z = 0.f;
|
|
if( pawnRotToVec dot Normal2D(EnemyPawn.Location - MyPatPawn.Location) < -0.25f )
|
|
{
|
|
// Aggro damage scales by 2 if it came from behind
|
|
pawnDmg *= 2;
|
|
}
|
|
RecentlySeenEnemyList[EnemyIndex].AggroDamage += pawnDmg;
|
|
RecentlySeenEnemyList[EnemyIndex].LastTakeDamageTime = WorldInfo.TimeSeconds;
|
|
}
|
|
else
|
|
{
|
|
EnemyIndex = HiddenEnemies.Find( 'TrackedEnemy', EnemyPawn );
|
|
if( EnemyIndex != INDEX_NONE )
|
|
{
|
|
pawnDmg = Damage;
|
|
pawnRotToVec = vector(MyPatPawn.Rotation);
|
|
pawnRotToVec.Z = 0.f;
|
|
if( pawnRotToVec dot Normal2D(EnemyPawn.Location - MyPatPawn.Location) < -0.25f )
|
|
{
|
|
// Aggro damage scales by 2 if it came from behind
|
|
pawnDmg *= 2;
|
|
}
|
|
HiddenEnemies[EnemyIndex].AggroDamage += pawnDmg;
|
|
HiddenEnemies[EnemyIndex].LastTakeDamageTime = WorldInfo.TimeSeconds;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// When our health gets low, summon zeds and escape from the battle to heal
|
|
if( !bWantsToFlee && !bFleeing && MyPatPawn != None && !MyPatPawn.bHealedThisPhase && MyPatPawn.CanSummonChildren()
|
|
&& !MyPatPawn.IsDoingSpecialMove(SM_Heal) )
|
|
{
|
|
if( !bSummonedThisPhase && GetHealthPercentage() < FleeHealthThreshold+0.075f )
|
|
{
|
|
bSummonedThisPhase = true;
|
|
MyAIDirector.bForceFrustration = true;
|
|
MyPatPawn.SummonChildren();
|
|
|
|
// Fallback so zeds can't spawn forever
|
|
SetTimer( 30.f, false, nameOf(Timer_StopSummoningZeds) );
|
|
}
|
|
|
|
if( !MyPatPawn.IsDoingSpecialMove(SM_Taunt) && GetHealthPercentage() < FleeHealthThreshold )
|
|
{
|
|
// Force any special move to end
|
|
if( MyPatPawn.IsDoingSpecialMove() )
|
|
{
|
|
MyPatPawn.EndSpecialMove();
|
|
}
|
|
|
|
// Prevent timeout from interrupting flee
|
|
if( GetActiveCommand().IsA('AICommand_SpecialMove') )
|
|
{
|
|
AICommand_SpecialMove(GetActiveCommand()).ClearTimeout();
|
|
}
|
|
|
|
TotalFleeTime = 0.f;
|
|
bCanEvaluateAttacks = false;
|
|
bWantsToFlee = true;
|
|
EndPanicWander();
|
|
NextBattlePhase();
|
|
class'AICommand_TauntEnemy'.static.Taunt( self, Enemy, TAUNT_Enraged, class'KFSM_Patriarch_Taunt' );
|
|
MyPatPawn.SetFleeAndHealMode( true );
|
|
}
|
|
}
|
|
|
|
Super.NotifyTakeHit(InstigatedBy, HitLocation, Damage, DamageType, Momentum);
|
|
}
|
|
|
|
/*********************************************************************************************
|
|
* Paternal Instinct
|
|
**********************************************************************************************/
|
|
|
|
/** Enter Paternal Instinct mode. Rage out! */
|
|
function StartPaternalInstinct()
|
|
{
|
|
//local KFPawn MissileEnemy;
|
|
|
|
// Play a dialog event
|
|
`DialogManager.PlayPattyChildKilledDialog( MyPatPawn );
|
|
|
|
// Let the rest of code know we're raging
|
|
bRaging = true;
|
|
bRagedThisPhase = true;
|
|
|
|
// Zero our attack count
|
|
RageAttackCount = 0;
|
|
|
|
// Set the maximum number of attacks via phase and difficulty
|
|
MaxRageAttacks = MyPatPawn.MaxRageAttacks + Max(0, MyKFGameInfo.GetModifiedGameDifficulty() - 1);
|
|
|
|
// Always sprint when raging
|
|
MyPatPawn.SetSprinting( true );
|
|
|
|
// Set our timeout timer
|
|
SetTimer( RageTimeOut, false, nameOf(Timer_RageTimeOut) );
|
|
|
|
/*// Do our mortar rage if the ceiling is clear
|
|
if( !MyPatPawn.IsDoingSpecialMove(SM_StandAndShootAttack) && !MyPatPawn.IsDoingSpecialMove(SM_SonicAttack) )
|
|
{
|
|
if( IsCeilingClear() && CollectMortarTargets(true, true) )
|
|
{
|
|
if( MyPatPawn.IsDoingSpecialMove() )
|
|
{
|
|
MyPatPawn.EndSpecialMove();
|
|
}
|
|
|
|
class'AICommand_Patriarch_MortarAttack'.static.FireMortar( self, true );
|
|
bCanEvaluateAttacks = false;
|
|
}
|
|
else if( RecentlySeenEnemyList.Length > 0 )
|
|
{
|
|
if( MyPatPawn.IsDoingSpecialMove() )
|
|
{
|
|
MyPatPawn.EndSpecialMove();
|
|
}
|
|
|
|
// See if our enemy was visible recently, if not then choose a random visible one
|
|
MissileEnemy = KFPawn(Enemy);
|
|
if( RecentlySeenEnemyList.Find('TrackedEnemy', MissileEnemy) == INDEX_NONE )
|
|
{
|
|
MissileEnemy = RecentlySeenEnemyList[Rand(RecentlySeenEnemyList.Length)].TrackedEnemy;
|
|
}
|
|
|
|
ChangeEnemy( MissileEnemy, false );
|
|
class'AICommand_Patriarch_MissileAttack'.static.FireMissiles( self );
|
|
bCanEvaluateAttacks = false;
|
|
}
|
|
}*/
|
|
}
|
|
|
|
/** Determine if rage state should continue, if so try to get a new enemy */
|
|
function UpdateRageState()
|
|
{
|
|
local float DistSQ, BestDistSQ;
|
|
local KFPawn KFP, BestTarget;
|
|
local int i;
|
|
|
|
// Stop raging if we've cleared the attack threshold
|
|
if( RageAttackCount >= MaxRageAttacks )
|
|
{
|
|
bRaging = false;
|
|
bCanEvaluateAttacks = true;
|
|
RageAttackCount = 0;
|
|
RageAttackedTargets.Length = 0;
|
|
ClearTimer( nameOf(Timer_RageTimeOut) );
|
|
|
|
// See if we can still sprint or not
|
|
EvaluateSprinting();
|
|
}
|
|
else
|
|
{
|
|
// Find a (hopefully new) enemy to rage out on
|
|
foreach WorldInfo.AllPawns( class'KFPawn', KFP )
|
|
{
|
|
if( !KFP.IsAliveAndWell() || MyPatPawn.IsSameTeam(KFP) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Prefer players that haven't been attacked yet
|
|
if( RageAttackedTargets.Find(KFP) != INDEX_NONE )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Filter out players that are too far
|
|
DistSQ = VSizeSQ( KFP.Location - MyPatPawn.Location );
|
|
if( DistSQ > MaxRageRangeSQ )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Favor closer targets
|
|
if( BestDistSQ == 0.f || DistSQ < BestDistSQ )
|
|
{
|
|
BestDistSQ = DistSQ;
|
|
BestTarget = KFP;
|
|
}
|
|
}
|
|
|
|
if( BestTarget == none )
|
|
{
|
|
// Most recent targets are at the end of the array, start at the top
|
|
for( i = 0; i < RageAttackedTargets.Length; ++i )
|
|
{
|
|
KFP = RageAttackedTargets[i];
|
|
|
|
// Remove any enemies that are no longer valid
|
|
if( KFP == none || !KFP.IsAliveAndWell() )
|
|
{
|
|
RageAttackedTargets.Remove( i, 1 );
|
|
--i;
|
|
continue;
|
|
}
|
|
|
|
// Get the first target that's within range
|
|
DistSQ = VSizeSQ( KFP.Location - MyPatPawn.Location );
|
|
if( DistSQ > MaxRageRangeSQ )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
BestTarget = KFP;
|
|
RageAttackedTargets.Remove(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( BestTarget != none )
|
|
{
|
|
RageAttackedTargets[RageAttackedTargets.Length] = BestTarget;
|
|
ChangeEnemy( BestTarget );
|
|
}
|
|
else
|
|
{
|
|
// End rage state if we couldn't get a valid target
|
|
RageAttackCount = MaxRageAttacks;
|
|
UpdateRageState();
|
|
}
|
|
}
|
|
}
|
|
|
|
/** If we've raged for too long, end Paternal Instinct */
|
|
function Timer_RageTimeOut()
|
|
{
|
|
// End rage state if it's been too long
|
|
RageAttackCount = MaxRageAttacks;
|
|
UpdateRageState();
|
|
}
|
|
|
|
/*********************************************************************************************
|
|
* Pathfinding
|
|
**********************************************************************************************/
|
|
|
|
function bool AmIAllowedToSuicideWhenStuck()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/*********************************************************************************************
|
|
* Zed Summoning
|
|
**********************************************************************************************/
|
|
|
|
/** Stop summoning children */
|
|
function Timer_StopSummoningZeds()
|
|
{
|
|
// Allow summoning of children for this phase
|
|
bSummonedThisPhase = false;
|
|
|
|
super.Timer_StopSummoningZeds();
|
|
}
|
|
|
|
/*********************************************************************************************
|
|
* Flee And Heal
|
|
**********************************************************************************************/
|
|
|
|
/** Initiate the Patriarch's next battle phase */
|
|
function NextBattlePhase()
|
|
{
|
|
bRagedThisPhase = false;
|
|
|
|
// Play a dialog event
|
|
`DialogManager.PlayPattyBattlePhaseDialog( MyPatPawn, MyPatPawn.CurrentBattlePhase );
|
|
|
|
if( MyPatPawn != None )
|
|
{
|
|
MyPatPawn.IncrementBattlePhase();
|
|
}
|
|
}
|
|
|
|
/** Custom target searching when fleeing -- we only want to attack targets that are blocking our flee path */
|
|
function Timer_SearchForFleeObstructions()
|
|
{
|
|
local KFPawn ObstructingEnemy;
|
|
|
|
if( !bFleeing || bWantsToFlee || MyPatPawn.IsDoingSpecialMove() )
|
|
{
|
|
SetTimer( 0.25f, false, nameOf(Timer_SearchForFleeObstructions) );
|
|
return;
|
|
}
|
|
|
|
// See if there's someone blocking us
|
|
ObstructingEnemy = CheckForEnemiesInFOV( AttackRange * 1.1f, 0.4f, 1.f, false, false );
|
|
if( ObstructingEnemy != none )
|
|
{
|
|
// We need to count up the total flee time so infinite fleeing isn't possible
|
|
TotalFleeTime = TotalFleeTime + (WorldInfo.TimeSeconds - FleeStartTime);
|
|
bFleeInterrupted = true;
|
|
bFleeing = false;
|
|
|
|
// Temporarily end flee state
|
|
MyPatPawn.SetCloaked( false );
|
|
|
|
// Kill our flee and move commands
|
|
AbortCommand( CommandList );
|
|
|
|
// Set our new enemy
|
|
ChangeEnemy( ObstructingEnemy, false );
|
|
|
|
// Set our pending flee
|
|
bWantsToFlee = true;
|
|
|
|
// Sprint to new enemy
|
|
bSprintUntilAttack = true;
|
|
SetEnemyMoveGoal( self, true );
|
|
EnableMeleeRangeEventProbing();
|
|
|
|
// Restart default command
|
|
BeginCombatCommand( GetDefaultCommand(), "Restarting default command" );
|
|
|
|
// Give the patty a little bit of time to flee after this attack
|
|
SetTimer( 3.0f, false, nameOf(Timer_SearchForFleeObstructions) );
|
|
}
|
|
else
|
|
{
|
|
SetTimer( 0.25f, false, nameOf(Timer_SearchForFleeObstructions) );
|
|
}
|
|
}
|
|
|
|
/** Searches for enemies that are blocking the path to my charge target */
|
|
function Timer_SearchForChargeObstructions()
|
|
{
|
|
local KFPawn ObstructingEnemy;
|
|
|
|
if( bWantsToCharge || MyPatPawn.IsDoingSpecialMove() )
|
|
{
|
|
SetTimer( 0.25f, false, nameOf(Timer_SearchForChargeObstructions) );
|
|
return;
|
|
}
|
|
|
|
// See if there's someone blocking us
|
|
ObstructingEnemy = CheckForEnemiesInFOV( AttackRange * 1.1f, 0.4f, 1.f, false, false );
|
|
if( ObstructingEnemy != none )
|
|
{
|
|
// Set our new enemy
|
|
MyPatPawn.SetCloaked( false );
|
|
ChangeEnemy( ObstructingEnemy, false );
|
|
|
|
// Set pending charge
|
|
bWantsToCharge = true;
|
|
}
|
|
else
|
|
{
|
|
SetTimer( 0.25f, false, nameOf(Timer_SearchForChargeObstructions) );
|
|
}
|
|
}
|
|
|
|
/** Bump damage for human players */
|
|
function DoHeavyBump( Actor Other, vector HitNormal )
|
|
{
|
|
local KFPawn_Human KFPH;
|
|
|
|
// Only bump human pawns when we're not in the middle of an attack
|
|
if( IsTimerActive(nameOf(Timer_SearchForFleeObstructions)) )
|
|
{
|
|
KFPH = KFPawn_Human( Other );
|
|
if( KFPH != none )
|
|
{
|
|
KFPH.TakeDamage( HumanBumpDamage, self, KFPH.Location, HitNormal*HumanBumpMomentum, MyPatPawn.GetBumpAttackDamageType() );
|
|
return;
|
|
}
|
|
}
|
|
|
|
DoHeavyZedBump( Other, HitNormal );
|
|
}
|
|
|
|
/** Effects and damage from a zed sprinting and bumping other monsters */
|
|
function bool DoHeavyZedBump( Actor Other, vector HitNormal )
|
|
{
|
|
local int BumpEffectDamage;
|
|
local KFPawn_Monster BumpedMonster;
|
|
|
|
/** If we bumped into a glass window, break it */
|
|
if( Other.bCanBeDamaged && KFFracturedMeshGlass(Other) != none )
|
|
{
|
|
KFFracturedMeshGlass(Other).BreakOffAllFragments();
|
|
return true;
|
|
}
|
|
|
|
/*if( Other.IsA('KFDestructibleActor') && !GetActiveCommand().IsA('AICommand_Melee') && Other.bCollideActors && !MyPatPawn.IsDoingSpecialMove() )
|
|
{
|
|
AIZeroMovementVariables();
|
|
DisableBump(2.f);
|
|
NotifyAttackActor( Other );
|
|
return true;
|
|
}*/
|
|
|
|
BumpedMonster = KFPawn_Monster(Other);
|
|
if( BumpedMonster == none || !BumpedMonster.IsAliveAndWell() || BumpedMonster.ZedBumpDamageScale <= 0 )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if( MyPatPawn == none || !MyPatPawn.IsAliveAndWell() )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Patriarch knocks guys out of the way always if he is in hunt and heal
|
|
if( MyPatPawn.bIsSprinting && !MyKFPawn.IsDoingSpecialMove() )
|
|
{
|
|
BumpEffectDamage = ZedBumpEffectThreshold * BumpedMonster.ZedBumpDamageScale;
|
|
|
|
// If the Bumped Zed is near death, play either a knockdown or an immediate obliteration
|
|
if( BumpedMonster.Health - BumpEffectDamage <= 0 )
|
|
{
|
|
// Chance to obliterate if at low health
|
|
if( FRand() < ZedBumpObliterationEffectChance )
|
|
{
|
|
BumpedMonster.TakeDamage(BumpEffectDamage, self, BumpedMonster.Location, vect(0,0,0), MyKFPawn.GetBumpAttackDamageType());
|
|
}
|
|
else
|
|
{
|
|
BumpedMonster.Knockdown( , vect(1,1,1), Pawn.Location, 1000, 100 );
|
|
}
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
// otherwise deal damage and stumble the zed
|
|
BumpedMonster.TakeDamage(BumpEffectDamage, self, BumpedMonster.Location, vect(0,0,0), MyKFPawn.GetBumpAttackDamageType());
|
|
BumpedMonster.DoSpecialMove(SM_Stumble,,, class'KFSM_Stumble'.static.PackBodyHitSMFlags(BumpedMonster, HitNormal));
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* Starts Flee AICommand, with optional duration and distance */
|
|
function DoFleeFrom( actor FleeFrom,
|
|
optional float FleeDuration,
|
|
optional float FleeDistance,
|
|
optional bool bShouldStopAtGoal=false,
|
|
optional bool bFromFear=false )
|
|
{
|
|
if( !bFromFear || !MyPatPawn.bInFleeAndHealMode )
|
|
{
|
|
super.DoFleeFrom( FleeFrom, FleeDuration, FleeDistance, bShouldStopAtGoal, bFromFear );
|
|
}
|
|
}
|
|
|
|
/** Sets flee target if there is no enemy, starts flee command */
|
|
function Flee()
|
|
{
|
|
local Actor FleeFromTarget;
|
|
local float FleeDuration;
|
|
local AICommand_SpecialMove AICSM;
|
|
|
|
// Reset flee state
|
|
bFleeing = false;
|
|
bWantsToFlee = false;
|
|
bFleeInterrupted = false;
|
|
|
|
// We need a target to flee from
|
|
if( Enemy == None )
|
|
{
|
|
SetEnemy( GetClosestEnemy() );
|
|
}
|
|
|
|
// Try to get an enemy, if not just choose a nearby navigation point (always flee!)
|
|
if( Enemy != None )
|
|
{
|
|
FleeFromTarget = Enemy;
|
|
}
|
|
else
|
|
{
|
|
FleeFromTarget = class'NavigationPoint'.static.GetNearestNavToActor( MyPatPawn );
|
|
}
|
|
|
|
// If we somehow ended up taking a ton of damage in rage state, turn it off
|
|
if( bRaging )
|
|
{
|
|
RageAttackCount = MaxRageAttacks;
|
|
UpdateRageState();
|
|
}
|
|
|
|
// Prevent timeout from interrupting flee
|
|
AICSM = FindCommandOfClass(class'AICommand_SpecialMove');
|
|
if( AICSM != none )
|
|
{
|
|
AICSM.ClearTimeout();
|
|
}
|
|
|
|
// Abort all commands
|
|
EndPanicWander();
|
|
AbortCommand( CommandList );
|
|
|
|
// Perform flee
|
|
bFleeing = true;
|
|
bCanEvaluateAttacks = false;
|
|
MyPatPawn.SetCloaked( true );
|
|
SetSprintingDisabled( false );
|
|
MyPatPawn.SetSprinting( true );
|
|
DisableMeleeRangeEventProbing();
|
|
|
|
FleeDuration = fMax( MaxFleeDuration - TotalFleeTime, 6.f );
|
|
//`log("[FLEE] FleeDuration:"@FleeDuration);
|
|
//`log("[FLEE] FleeStartTime:"@WorldInfo.TimeSeconds);
|
|
FleeStartTime = WorldInfo.TimeSeconds;
|
|
DoFleeFrom( FleeFromTarget, FleeDuration, MaxFleeDistance + Rand(MaxFleeDistance * 0.25f), true );
|
|
EvaluateSprinting();
|
|
|
|
// Constantly make sure we don't have a player trying to block us
|
|
if( !IsTimerActive(nameOf(Timer_SearchForFleeObstructions)) )
|
|
{
|
|
SetTimer( 2.f, false, nameOf(Timer_SearchForFleeObstructions) );
|
|
}
|
|
}
|
|
|
|
/** We have finished fleeing for one reason or another, notify pawn to heal */
|
|
function NotifyFleeFinished( optional bool bAcquireNewEnemy=true )
|
|
{
|
|
// If this was not a flee for healing, don't do additional cleanup
|
|
if( !MyPatPawn.bInFleeAndHealMode )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if( MyPatPawn != None )
|
|
{
|
|
MyPatPawn.SetCloaked( false );
|
|
|
|
// Delay stop summoning, to give paternal instinct a chance to trigger
|
|
if( IsTimerActive(nameOf(Timer_StopSummoningZeds)) )
|
|
{
|
|
SetTimer( 4.f, false, nameOf(Timer_StopSummoningZeds) );
|
|
}
|
|
|
|
// Stop searching for targets
|
|
ClearTimer( nameOf(Timer_SearchForFleeObstructions) );
|
|
|
|
// Flee debug
|
|
//`log("[HEAL] FleeDuration:"@fMax(MaxFleeDuration - TotalFleeTime, 6.f));
|
|
//`log("[HEAL] FleeEndTime:"@WorldInfo.TimeSeconds);
|
|
//scripttrace();
|
|
|
|
// End flee state
|
|
bWantsToFlee = false;
|
|
bFleeing = false;
|
|
|
|
// Heal
|
|
MyPatPawn.DoSpecialMove( SM_Heal,,, class'KFSM_Patriarch_Heal'.static.PackSMFlags(MyPatPawn.CurrentBattlePhase-1) );
|
|
}
|
|
|
|
// Allow melee again
|
|
EnableMeleeRangeEventProbing();
|
|
|
|
// Restart default command
|
|
BeginCombatCommand( GetDefaultCommand(), "Restarting default command" );
|
|
}
|
|
|
|
/** Forces a heal regardless of what state we're in */
|
|
function ForceHeal()
|
|
{
|
|
if( bFleeing )
|
|
{
|
|
bFleeing = false;
|
|
bWantsToFlee = false;
|
|
|
|
// Kill our flee command
|
|
AbortCommand( FindCommandOfClass(class'AICommand_Flee') );
|
|
}
|
|
else
|
|
{
|
|
// Make sure hans doesn't try to flee again
|
|
bWantsToFlee = false;
|
|
}
|
|
|
|
// End flee as normal
|
|
NotifyFleeFinished( false );
|
|
}
|
|
|
|
/** Victory */
|
|
function EnterZedVictoryState()
|
|
{
|
|
bCanEvaluateAttacks = false;
|
|
bRaging = false;
|
|
MyPatPawn.SetCloaked( false );
|
|
bWantsToFlee = false;
|
|
bFleeing = false;
|
|
bFleeInterrupted = false;
|
|
|
|
if( IsTimerActive(nameOf(Timer_SearchForFleeObstructions)) )
|
|
{
|
|
ClearTimer( nameOf(Timer_SearchForFleeObstructions) );
|
|
}
|
|
|
|
if( IsTimerActive(nameOf(Timer_SearchForChargeObstructions)) )
|
|
{
|
|
ClearTimer( nameOf(Timer_SearchForChargeObstructions) );
|
|
}
|
|
|
|
if( IsTimerActive(nameOf(Timer_RageTimeOut)) )
|
|
{
|
|
ClearTimer( nameOf(Timer_RageTimeOut) );
|
|
}
|
|
|
|
MyKFGameInfo.SpawnManager.StopSummoningBossMinions();
|
|
super.EnterZedVictoryState();
|
|
|
|
KFWeapon(MyPatPawn.Weapon).GotoState( 'Inactive' );
|
|
}
|
|
|
|
state ZedVictory
|
|
{
|
|
ignores NotifyTakeHit, NotifyKilled, NotifySpecialMoveEnded, NotifyFleeFinished, SeePlayer, UpdateRageState, CheckForEnemiesInFOV,
|
|
EvaluateSprinting, ChangeEnemy, SetEnemy, FindNewEnemy, EvaluateAttacks, UpdateRecentlySeenEnemyList, Timer_SearchForFleeObstructions,
|
|
Timer_SearchForChargeObstructions;
|
|
|
|
Begin:
|
|
Sleep(0.1f);
|
|
class'AICommand_BossTheatrics'.static.DoTheatrics( self, THEATRIC_Victory, -1 );
|
|
}
|
|
|
|
/*********************************************************************************************
|
|
* Dialog
|
|
********************************************************************************************* */
|
|
|
|
function PlayDamagePlayerDialog( class<DamageType> DmgType )
|
|
{
|
|
//`DialogManager.PlayHansDamagePlayerDialog( MyHansPawn, DmgType );
|
|
}
|
|
|
|
/*********************************************************************************************
|
|
* Debug
|
|
********************************************************************************************* */
|
|
|
|
simulated function DrawDebug( KFHUDBase HUD, name Category )
|
|
{
|
|
local Canvas C;
|
|
|
|
super.DrawDebug( HUD, Category );
|
|
|
|
if( MyPatPawn == None || Category != 'All' )
|
|
{
|
|
return;
|
|
}
|
|
|
|
C = HUD.Canvas;
|
|
// Patriarch info
|
|
C.SetDrawColor( 255, 255, 255, 255 );
|
|
DrawDebugText( HUD, "************************************************************" );
|
|
C.SetDrawColor( 0, 0, 255, 255 );
|
|
DrawDebugText( HUD, "PATRIARCH STATUS" );
|
|
C.SetDrawColor( 0, 255, 0, 255);
|
|
DrawDebugText( HUD, "BattlePhase:"@MyPatPawn.CurrentBattlePhase );
|
|
}
|
|
|
|
|
|
`if(`notdefined(ShippingPC))
|
|
/** Debug command to advance battle phase */
|
|
function DebugNextPhase()
|
|
{
|
|
// Force any special move to end
|
|
if( MyPatPawn.IsDoingSpecialMove() )
|
|
{
|
|
MyPatPawn.EndSpecialMove();
|
|
}
|
|
|
|
bCanEvaluateAttacks = false;
|
|
bWantsToFlee = true;
|
|
NextBattlePhase();
|
|
//MyPatPawn.DoSpecialMove( SM_Heal,,, class'KFSM_Patriarch_Heal'.static.PackSMFlags(MyPatPawn.CurrentBattlePhase-1) );
|
|
class'AICommand_TauntEnemy'.static.Taunt( self, Enemy, TAUNT_Enraged, class'KFSM_Patriarch_Taunt' );
|
|
MyPatPawn.SetFleeAndHealMode( true );
|
|
}
|
|
`endif
|
|
|
|
DefaultProperties
|
|
{
|
|
Steering=none
|
|
DefaultCommandClass=class'KFGameContent.AICommand_Base_Patriarch'
|
|
MeleeCommandClass=class'KFGameContent.AICommand_Base_Patriarch'
|
|
bRepathOnInvalidStrike=true
|
|
LastRecentSeenEnemyListUpdateTime=0.1
|
|
LostSightSprintDelay=5.0
|
|
bSprintUntilAttack=true
|
|
bHadMinigunAttack=false
|
|
bCanDoHeavyBump=true
|
|
EvadeGrenadeChance=1.0f
|
|
RetargetWaitTime=5.f
|
|
AggroFalloffWaitTime=1.f
|
|
AggroFalloffPerSecond=25.f
|
|
VisibleAggroDmgThreshold=260.f
|
|
HiddenAggroDmgThreshold=200.f
|
|
|
|
// Run over warning
|
|
bUseRunOverWarning=true
|
|
MinRunOverSpeed=360.f
|
|
MinRunOverWarningAim=0.85f
|
|
|
|
// Special attacks
|
|
MinMinigunRangeSQ=160000.f
|
|
MaxMinigunRangeSQ=16000000.f
|
|
MaxFanFireRangeSQ=490000.f
|
|
MinChargeRangeSQ=810000.f
|
|
MinTentacleRangeSQ=90000.f
|
|
MinMissileRangeSQ=360000.f
|
|
MaxMissileRangeSQ=16000000.f
|
|
|
|
// Flee
|
|
HumanBumpDamage=10.f
|
|
HumanBumpMomentum=8000.f
|
|
FleeHealthThreshold=0.35f
|
|
MaxFleeDuration=25.f
|
|
MaxFleeDistance=20000.f
|
|
|
|
// Rage
|
|
MaxRageRangeSQ=1440000.f
|
|
RageTimeOut=16.f
|
|
|
|
// ---------------------------------------------
|
|
// Danger Evasion Settings
|
|
DangerEvadeSettings.Empty
|
|
|
|
//Aim Blocks
|
|
DangerEvadeSettings(0)={(ClassName="KFWeap_Rifle_Winchester1894",
|
|
Cooldowns=(0.0, 0.0, 0.3, 0.2), // Normal, Hard, Suicidal, HoE
|
|
BlockChances=(0.0, 0.0, 1.0, 1.0))}
|
|
DangerEvadeSettings(1)={(ClassName="KFWeap_Bow_Crossbow",
|
|
Cooldowns=(0.0, 0.0, 0.3, 0.2), // Normal, Hard, Suicidal, HoE
|
|
BlockChances=(0.0, 0.0, 1.0, 1.0))}
|
|
DangerEvadeSettings(2)={(ClassName="KFWeap_Rifle_M14EBR",
|
|
Cooldowns=(0.0, 0.0, 0.3, 0.2), // Normal, Hard, Suicidal, HoE
|
|
BlockChances=(0.0, 0.0, 1.0, 1.0))}
|
|
DangerEvadeSettings(3)={(ClassName="KFWeap_Rifle_RailGun",
|
|
Cooldowns=(0.0, 0.0, 0.3, 0.2), // Normal, Hard, Suicidal, HoE
|
|
BlockChances=(0.0, 0.0, 1.0, 1.0))}
|
|
DangerEvadeSettings(4)={(ClassName="KFWeap_Bow_CompoundBow",
|
|
Cooldowns=(0.0, 0.0, 0.3, 0.2), // Normal, Hard, Suicidal, HoE
|
|
BlockChances=(0.0, 0.0, 1.0, 1.0))}
|
|
}
|