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

7846 lines
260 KiB
Ucode
Raw Normal View History

2020-12-13 15:01:13 +00:00
//=============================================================================
// KFAIController
//=============================================================================
// Base AI Controller for KFMonsterPawns.
// A lot of this should really be in KFAIController_Monster, which hasn't been
// a priority because currently all KFAI pawns are monsters/player enemies.
// TODO: Move zed-specific code down to KFAIController_Monster.
//=============================================================================
// Killing Floor 2
// Copyright (C) 2015 Tripwire Interactive LLC
//=============================================================================
class KFAIController extends BaseAIController
native(AI);
`include(KFGame\KFGameAnalytics.uci);
/*********************************************************************************************
* Misc. Defines
********************************************************************************************* */
/** For convenience (LATENT_MOVETOWARD is already defined in Controller.uc) */
const LATENT_MOVETO = 501;
/*********************************************************************************************
* Misc. Defines
********************************************************************************************* */
/** If true, will call ScriptGetTeamNum() to determine team if there is no PRI for this AI */
var bool bAllowScriptTeamCheck;
/*********************************************************************************************
* Initialization
********************************************************************************************* */
/** Cached copy of gameinfo to avoid casting */
var transient KFGameInfo MyKFGameInfo;
/** Cached copy of AI pawn to avoid casting */
var transient KFPawn_Monster MyKFPawn;
/** GameInfo's current AIManager */
var transient KFAIDirector MyAIDirector;
/** Steering used to actually move the controlled pawn */
var instanced KFAISteering Steering;
/** Default Combat Command we should move into when spotting an enemy */
var class<AICommand> DefaultCommandClass;
/** command we should use when we want to pursue and melee an enemy */
var class<AICommand> MeleeCommandClass;
/*********************************************************************************************
* Behavior Properties
******************************************************************************************** */
/** (Default behavior) Will send NPC to move to nearest enemy after possession, from ActionIdle startup state */
var bool bIdleMoveToNearestEnemy;
/*********************************************************************************************
* General Movement
********************************************************************************************* */
/** Intermediate move goal used when pathfinding to MoveGoal */
var Actor IntermediateMoveGoal;
/** Used in tracking # of times AICommand_MoveToEnemy has failed */
var int FailedMoveToEnemyCount;
/** Acceptable distance offset from the goal, used by AICommand_MoveToGoal (TODO: Move this into AICommand_MoveToGoal if not
needed here) */
var float MoveOffset;
/** Actor to move toward when destination is an actor rather than a vector, used in latent movement (MOVETOWARD)*/
var Actor MoveGoal;
/** Location to move to when destination is a vector rather than an actor, used in latent movement (MOVETO) */
var BasedPosition MovePosition;
/** Focus override while performing a move */
var Actor MoveFocus;
/** Last time we researched for a destination after a delay, used by AICommand_MoveToGoal */
var float LastDetourCheckTime;
/** Was current MoveGoal successfully reached? */
var bool bReachedMoveGoal;
/** Forces movement command to build a new path when command resumes, regardless of current routecache */
var bool bReevaluatePath;
/** TRUE if my pawn is currently moving to its goal (that is, if an AICommand_MoveToGoal is active) */
var bool bMovingToGoal;
/** TRUE if my pawn is currently moving to its enemy (AICommand_MoveToEnemy is active) */
var bool bMovingToEnemy;
/** Currently moving directly to a goal, rather than still on my path */
var bool bDirectMoveToGoal;
/** Is the current movement goal interruptable? If you need to rely on this, re-confirm that it's working properly (haven't needed this) */
var bool bMoveGoalInterruptable;
/** AI should not update route cache - flag to prevent cache from being changed when pathing is used to pick locations (flank, hide, etc.) */
var bool bSkipRouteCacheUpdates;
/** Used by base Zed AICommand and AICommand_MoveToEnemy */
var bool bFailedToMoveToEnemy;
/** Used with HandlePathObstruction() event (see AICommand_MoveToGoal version, which is a function called by the controller's HPO event) */
var float LastObstructionTime;
/** Use to track when my pawn last transitioned from walk to sprint (or vice versa) to prevent changing too frequently */
var float LastSprintChangeTime;
/** Lock the AI to a specific rotation, see KFAIController::UpdatePawnRotation() */
var Rotator LockedRotation;
/** Cached version of CommandList that's an AICommand (instead of BaseAICommand) */
var AICommand CachedAICommandList;
/** Currently not turned, going to be deprecated */
var transient bool bAvoidChokePoints;
/** Used in determining how often (when moving along a path) to check if my goal is now directly reachable */
var transient float NextDirectPathCheckTime;
var float DirectPathCheckFrequency_Min;
var float DirectPathCheckFrequency_Max;
/** Used by AKFAIController::DirectPathToGoal */
var float DirectPathExtentModifier;
/** Should sprint if enemy distance is > X and < Y - older, might be able to get rid of this */
var Vector2D SprintWithinEnemyRange;
/** Modifier to rotation rate whenever not already modified by a special move */
var float RotationRateMultiplier;
/** Current closed door that I'm for (assuming bPreparingMove is true to delay any latent move) */
var KFDoorActor PendingDoor;
/** If true, this pawn will receuve NotifyOnAddToRouteCache() events for any NavigationPoints with bNotifyOnAddToRouteCache=true when they
are added to a path I'm building */
var bool bProbeNotifyOnAddToRouteCache;
/** Set to true by KFJumpSpot, used in MayFall() event but might be able to get rid of this, it's old */
var bool bPlannedJump;
/** Used by direct reachability checking code while NPC is pathfinding, Used by AKFAIController::HasDirectPathToGoal */
var float LastLOSCheckTime;
/** Used by direct reachability if MinTimeBetweenLOSChecks seconds have yet to pass */
var bool CachedLOSCheck;
/** Minimum time that must pass between checking direct reachability to goal */
var float MinTimeBetweenLOSChecks;
/** Used when failing a move in AICommand_MoveToGoal... testing */
var KFPathnode BlockedPath; // 2.21.2014
var bool bIgnoreBlockedPathList;
var float MaxBlockedPathDuration;
/** Turns PathLane code on/off (see ShouldUsePathlanes()) */
var bool bShouldUsePathLanes;
/** Turns corner cutting on off (see ShouldOffsetCorners()) */
var bool bShouldOffsetCorners;
/** If this is on, NPC will always be willing to accept partial paths when using movement AICommands */
var bool bAlwaysAcceptPartialPaths;
/** This monster has the capability to sprint in general */
var bool bCanSprint;
/** This monster has the capability to sprint when damaged */
var bool bCanSprintWhenDamaged;
/** This monster's ability to sprint is currently disabled */
var bool bSprintingDisabled;
var config Color PathNodeShowRouteCacheColor;
var config vector PathNodeShowRouteCacheCrossOffset;
var config float PathNodeShowRouteCacheCrossSize;
var config vector PathNodeShowRouteCacheNumberLabelOffset;
var config float PathNodeShowRouteCacheNumberLabelDuration;
// ----------------------------------------------------------------------- //
// DEPRECATED KF Cached version of movement Plugins
// ----------------------------------------------------------------------- //
var AIPluginMovement KfMovementPlugin;
var AIPluginLeap KfLeapPlugin;
var AIPluginStuckFix KfStuckFixPlugin;
//var class<AIPluginMovement> WallWalkingPluginClass;
var AIPluginMovement KfWallWalkingPlugIn;
//var bool bGoToEnemiesOutsideNavmesh;
//var Actor ActorFindingNavMeshPointCloseToo;
//var KFNavigationHandle MyKFNavigationHandle;
var MOVEMENT_PHASE_TYPE CurrentMovementPhase;
// failure
var config Color Move_failure_type_none_color;
var config Color Move_failure_type_no_nav_mesh_path_color;
var config Color Move_failure_type_same_intermediate_point_too_many_times_color;
var config Color MoveFailureTypeTargetOffNavMeshAndCanNotFindLocaitonNearThemICanMoveTo;
var float DefaultMaxTimeAllowedToStayStuckBeforeSuicide;
var float NoNavMeshPathMaxTimeAllowedToStayStuckBeforeSuicide;
var float SameIntermediatePointToManyTimesMaxTimeAllowedToStayStuckBeforeSuicide;
var float TargetOffNavMeshAndCanNotFindLocaitonNearThemICanMoveTooMaxTimeAllowedToStayStuckBeforeSuicide;
var float SameIntermediatePointToManyTimesDurationAfterStartedMovingAgaintToStopStuckCheck;
var float DefaultMinDistaceToHaveToMoveToBeConcideredStuckBeforeSuicide;
var float NoNavMeshPathMinDistaceToHaveToMoveToBeConcideredStuckBeforeSuicide;
var float SameIntermediatePointToManyTimesMinDistaceToHaveToMoveToBeConcideredStuckBeforeSuicide;
var MOVE_FAILURE_TYPE TypeOfMovementStuckOn;
// Pathnode icons
var Texture2D MovementPhaseTypePathNodeNormalMoveToIcon;
var Texture2D MovementPhaseTypePathNodeMoveFailedIcon;
var Texture2D MovementPhaseTypePathNodeMoveToPointIcon;
var Texture2D MovementPhaseTypePathNodeRotateToFocusIcon;
var Texture2D MovementPhaseTypePathNodeDelayMoveIcon;
// end Pathnode icons
var Texture2D MovementPhaseTypeUnknownIcon;
var Texture2D MovementPhaseTypeNavMeshNormalMoveToIcon;
var Texture2D MovementPhaseTypeFinalDestMoveTowardsIcon;
var Texture2D MovementPhaseTypeEMT_LOS_MoveTowardsIcon;
var Texture2D MovementPhaseTypeMovingToNavMeshUsingPathNodesIcon;
var Texture2D MovementPhaseTypeFALLBACK_REFUSED_TO_BE_EXPLOTIED_FIND_NEARBY_MESH_POINT_MOVE_TO_DIRECT_NON_PATH_POSIcon;
var Texture2D MovementPhaseTypeFALLBACK_FIND_NEARBY_MESH_POINT_MOVE_TO_DIRECT_NON_PATH_POSIcon;
var Texture2D TypeOfMovementStuckOnUnknownWhyIcon;
var Texture2D TypeOfMovementStuckOnMOVE_FAILURE_TYPE_NO_NAV_MESH_PATHIcon;
var Texture2D TypeOfMovementStuckOnMOVE_FAILURE_TYPE_SAME_INTERMEDIATE_POINT_TOO_MANY_TIMESIcon;
var Texture2D TypeOfMovementStuckOnMOVE_FAILURE_TYPE_TARGET_OFF_NAV_MESH_AND_CAN_NOT_FIND_LOCAITON_NEAR_THEM_I_CAN_MOVE_TOIcon;
var Texture2D TypeOfMovementStuckOnLookingForBetterIntermediateLoc;
var Texture2D TypeOfMovementStuckOnMoveToBetterIntermediate;
var config bool bConfigShowMovePointsDebugInfo;
var bool bShowMovePointsDebugInfo;
var config bool bConfigShowHighDetailCombatMovementDebugInfo;
var bool bShowHighDetailCombatMovementDebugInfo;
var config bool bConfigShowVisualStuckZedDebugInfo;
var bool bShowVisualStuckZedDebugInfo;
var config color ColorForCollisionRadiusForReducedZedOnZedPinchPointCollisionStateOff;
var config color ColorForCollisionRadiusForReducedZedOnZedPinchPointCollisionStateOn;
var config bool bConfigShowCollisionRadiusForReducedZedOnZedPinchPointCollisionState;
var bool bShowCollisionRadiusForReducedZedOnZedPinchPointCollisionState;
/** how far away to look for position on the nav mesh when I end up off the nav mesh, first try */
var float DistanceToCheckForClearPathOnNavMeshLocWhenOffNavMesh;
/** how far away to look for position on the nav mesh when I end up off the nav mesh, 2nd try */
var float DistanceToCheckForNonExploitedOnNavMeshLocWhenOffNavMesh;
/** how far away to look for position on the nav mesh when I end up on a dead end of the nav mesh */
var float DistanceToCheckForNonExploitedOnNavMeshLocWhenOnDeadEndOfNavMesh;
var config color DefaultColorOfValidLocationWhenLookingForLocationsOnNavMesh;
var config color DefaultColorOfSearchSphereWhenNoValidLocationsFoundWhenLookingForLocationsOnNavMesh;
var config color ColorOfValidLocationFoundFor1stTryLookingForLocationsOnNavMesh;
var config color ColorOfSearchSphereWhenNoValidLocationsFoundFor1stTryLookingForLocationsOnNavMesh;
var config color ColorOfValidLocationFoundFor2ndTryLookingForLocationsOnNavMesh;
var config color ColorOfSearchSphereWhenNoValidLocationsFoundFor2ndTryLookingForLocationsOnNavMesh;
var config color ColorOfValidLocationFoundFor3rdTryLookingForLocationsOnNavMesh;
var config color ColorOfSearchSphereWhenNoValidLocationsFoundFor3rdTryLookingForLocationsOnNavMesh;
var config color ColorOfValidLocationFoundForLookingForLocationsOnNavMeshWhenOnDeadEndOfNavMesh;
var config color ColorOfSearchSphereWhenNoValidLocationsFoundForTryLookingForLocationsOnNavMeshWhenOnDeadEndOfNavMesh;
/** how far away to look for position on the nav mesh when my enemy ends up off the nav mesh, first try */
var float DistanceToCheckForClearPathOnNavMeshLocWhenEnemyIsOffNavMesh;
/** how far away to look for position on the nav mesh when my enemy ends up off the nav mesh, 2nd try */
var float DistanceToCheckForNonExploitedOnNavMeshLocWhenEnemyIsOffNavMesh;
/** How far away can the door be from the closest nav loc so that I can reach it */
var float GeneralGoalDistanceForMovingToDoor;
/** If the enemy moves more than this distance away from where it was when we pathed, repath */
var float RecastEnemyRepathDistance;
/** DropEdgeLeapVelocity */
var Vector DropEdgeLeapVelocity;
/** MaxRangeToDropEdgeAllowedToLeadFrom */
var float MaxRangeToDropEdgeAllowedToLeadFrom;
/** DistanceDownRangeToFocusForDropEdgeLeap */
var float DistanceDownRangeToFocusForDropEdgeLeap;
var bool bShowLeapDownDebugArtifacts;
var config bool bConfigShowLeapDownDebugArtifacts;
var bool bShowDoorNavigationDebugArtifacts;
var config bool bConfigShowDoorNavigationDebugArtifacts;
var bool bShowDestructibleNavigationDebugArtifacts;
var config bool bConfigShowDestructibleNavigationDebugArtifacts;
var config float TimeToShowEdgeTypeForNavMeshPathting;
/** determiner whether given AI actor respects numerous leader person/distance restrictions */
var bool bMindLeader;
var vector LastLeaderPosOffset;
/*********************************************************************************************
* Tracking obstructions via NotifyHitWall
**********************************************************************************************/
var float LastNotifyHitWallTime;
var actor LastHitWall;
var vector LastWallHitNormal;
var int HitWallCount;
var bool LastPathFailTime;
struct native sBlockedPathInfo
{
var ReachSpec BlockedReachSpec;
var int BlockedCost; // 0 = Completely blocked
var float BlockedTime;
};
var array<sBlockedPathInfo> BlockedPathList;
// Is the AI locked to a specific rotation
var bool IsRotationLocked;
/** My pawn was spawned from an emerge portal, set to false by AICommandMove or first time UpdateRotation
is called in PHYS_Spider */
var bool bSpawnedByEmergeSpecialMove;
/** Will call TargetedByPlayer event if true when calling AKFAIController::IsTargetedByPlayer() */
var bool bUseTargetedByPlayerEvent;
/*********************************************************************************************
* Bump testing - only in use if bSpecialBumpHandling is true (see BumperSomewhereToGo, Tick)
********************************************************************************************* */
var bool bBumpedThisFrame;
var KFPawn LastBumper;
var float CurBumpVal;
var float BumpThreshold;
var float BumpDecayRate;
var float BumpGrowthRate;
var config bool bDoNotBlockFriendlyAI;
var config bool bSuperSpeedDoNotBlockFriendlyAI;
var bool bSpecialBumpHandling;
/*********************************************************************************************
* Stuck checking
**********************************************************************************************/
/** This last time this AI checked to see if it was stuck */
var float LastStuckCheckTime;
/** How much time we've spent within close range to the player */
var float TotalStuckCheckCloseRangeTime;
/** The last time we checked and found that we were within close range to the player */
var float LastStuckCheckCloseRangeTime;
/** How often to check and see if this AI is stuck */
var(StuckChecking) float StuckCheckInterval;
/** How slow a zed needs to be moving (squared) in the X/Y access to be considered possibly stuck */
var(StuckChecking) float StuckVelocityThreshholdSquared;
/** How stuck this zed thinks it might be */
var float StuckPossiblity;
/** The threshold to actually consider this zed stuck when it thinks it might be */
var(StuckChecking) float StuckPossiblityThreshhold;
/** The zed thinks it is stuck and is trying to get unstuck */
var bool bTryingToGetUnstuck;
/** The time at which this AI started thinking it was stuck */
var float LastStuckTime;
/** Is this AI allowed to teleport to move closer? */
var bool bCanTeleportCloser;
/** The minimum number of zeds remaining before teleportation is disabled for this zed */
var(Teleport) byte AIRemainingTeleportThreshold;
/** The last time this AI checked to see if it can teleport closer to it's enemy */
var float LastTeleportCheckTime;
/** The last time this AI teleported either because it was stuck or relocated */
var float LastTeleportTime;
/** How often to check and see if this AI can relocate teleport*/
var(Teleport) float TeleportCheckInterval;
/** How long to wait after teleporting to be able to teleport again*/
var(Teleport) float TeleportCooldown;
/** How long this zed must be hidden before it can relocate teleport. Must be at least 5, because hidden checks can be delayed as much as 5 seconds*/
var(Teleport) float HiddenRelocateTeleportThreshold;
/** How long this zed must ahve been alive before it can relocate teleport. */
var(Teleport) float PostSpawnRelocateTeleportCooldown;
/** The last location this zed teleported from */
var vector LastTeleportLocation;
/** This last time this AI finished a special move */
var float LastSpecialMoveEndTime;
/** Where this AI was the last time we did a stuck check */
var vector LastStuckCheckLocation;
/** How far this AI should have moved in 2d space since last check to not be considered stuck */
var float XYMoveStuckThresholdSquared;
/** How far this AI should have moved in Z space since last check to not be considered stuck */
var float ZMoveStuckThresholdSquared;
/** If I am closer than this distance to my enemy, then don't do any special stuck checking/handling */
var float StuckCheckEnemyDistThreshholdSquared;
/** If I am closer than this distance to my enemy, then don't reduce collision size with other zeds */
var float NavigationBumpTeamCollisionThreshholdSquared;
/** How long we've been falling with no z velocity */
var float FallingStuckNoZVelocityTime;
/** Number of times a walk move has failed to apply velocity while Pawn->Acceleration is nonzero */
var int NumFailedLatentWalkMoves;
/*********************************************************************************************
* StepAside
********************************************************************************************* */
/** Current step aside goal, for StepAside state */
var Pawn StepAsideGoal;
/** Distance to attempt to step aside when bumping into other players */
var float MaxStepAsideDist;
/** should we ignore stepasidefor calls? **/
var bool bIgnoreStepAside;
var float LastFailedToFindStepAsideLocation;
/** last pawn we bumped */
var Pawn LastBumpedPawn;
/** last time we bumped someone */
var float LastBumpTime;
/*********************************************************************************************
Combat
********************************************************************************************* */
/** The range a zed needs to be from a player to begin considering his attack, also this is the MAX range I will ever be able to do a strike, any strike at, if I can not get this close I am SOL */
var float AttackRange;
/** The next melee attack index we will attempt */
var byte PendingAnimStrikeIndex;
/** The range a zed needs to be from a player to perform a melee attack */
var float StrikeRange;
/** Used to get the StrikeRange as a value between the min and max PawnAnimInfoGroup's attacks */
var float StrikeRangePercentage;
/** Used to get the StrikeRange as a value between the min and max PawnAnimInfoGroup's attacks */
var bool bCanStrikeThroughEnemies;
/** Used by KFPawnAnimInfo to determine if an attack can be performed if legs are blocked (lunges, etc) */
var bool bIsBodyBlocked;
/** Desired melee distance when attacking a door */
var float DoorMeleeDistance;
/** Max height offset (normalized degrees) that zed is able to attack in melee from */
var float MaxMeleeHeightAngle;
/** Last time a melee attack was performed */
var float LastAttackTime_Melee;
/** Last time a we have taken damage */
var float LastDamageTime_Taken;
/** Last time a melee attack decision was evaluated */
var float LastMeleeAttackDecisionTime;
/** Last time an enemy was selected using SelectEnemy() */
var float LastSelectEnemyTime;
/** Last time an enemy was set using SelectEnemy() */
var float LastSetEnemyTime;
var float MinTimeBetweenStatusUpdates;
var float MaxTimeBetweenStatusUPdates;
/** Last time a latent move completed */
var float LastMoveFinishTime;
/** Used to prevent changing my enemy too frequently (see SetEnemy()) */
var float MinTimeBetweenEnemyChanges;
/** Last time we collided with a destructible */
var float LastDestructibleBumpTime;
/** Enemies are pawns - use this for temporary actor enemies (like destructibles that need to be melee-attacked) */
var Actor ActorEnemy;
/** Set when attacking a welded door */
var KFDoorActor DoorEnemy;
/** Current melee target, if any - stored here for separation steering.. don't want a Zed to be pushed away from his melee target. */
var KFPawn MeleeTarget;
/** Result of last enemy LineOfSightTo() check */
var bool bEnemyIsVisible;
/** Result of last call to SeePlayer */
var transient bool bIsVisibleToEnemy;
/** Result of previous call to SeePlayer */
var transient bool bWasVisibleToEnemy;
/** When last enemy LineOfSightTo() check was done */
var float EnemyVisibilityTime;
/** The last time an enemy switch occurred (only set if enemy was set to a new enemy) */
var float LastEnemySwitchTime;
/** Who the enemy was for the last LineOfSightTo() check */
var Pawn CachedVisibleEnemy;
/** Incoming projectile that I should evade away from (see ReceiveProjectileWarning()) */
var Projectile PendingEvadeProjectile;
/** The chance that this zed has to dodge an incoming grenade */
var float EvadeGrenadeChance;
/** If true, will continually rotate to enemy during non-rootmotion melee attacks */
var bool bUseDesiredRotationForMelee;
/** The last time we tried to get a new move */
var float LastGetStrikeTime;
/** How long we need to wait before picking another move */
var const float MaxGetStrikeTime;
/** The movement system has done its best to get me to my enemy but can not get me any closer, the attack behavior needs to come up with something */
var bool bIamAsClosesToTheEnemyAsICanGet;
/** Force frustration on, regardless of frustration threshold settings */
var(Frustration) bool bForceFrustration;
/** The minimum number of zeds remaining before frustration mode (sprint) is activated */
var(Frustration) byte FrustrationThreshold;
/** The amount of time to delay zeds from entering frustration mode after it's first detected */
var(Frustration) float FrustrationDelay;
var float LastFrustrationCheckTime;
/** Cached default to workaround an issue with frustration reset */
var transient bool bDefaultCanSprint;
var float GoalDistanceWhenMovingToLocationForMeleeStrikeWhenEnemyIsOffNavMesh;
struct native CooldownData
{
var() name Tag;
var() float ActivationTime;
var() float Duration;
};
/** List of active attack cooldown timers */
var array<CooldownData> CooldownTimers;
/** Store info for an overall attack cooldown timer so we can throttle all attacks by this zed in certain circumstances */
var() CooldownData OverallAttackCooldownTimer;
/** Attack cooldown time to use when throttling all attacks by this zed in certain circumstances */
var() float LowIntensityAttackCooldown;
//var KFAiBallisticProjectileFireBehavior BallisticProjectileFireBehavior;
var KFAiDirectProjectileFireBehavior DirectProjectileFireBehavior;
var KFAiLeapBehavior LeapBehavior;
//var KFAiDestructibleInterActionBehavior DestructibleInterActionBehavior;
var const vector BaseShapeOfProjectileForCalc;
/** The last time this AI did an AICommand_ShootBase attack. See AICommand_ShootBase for notes on why this was brought over here - Ramm */
var float LastShotTime;
/** Set to true if we want to knockdown and possibly obliterate other zeds on bump */
var const bool bCanDoHeavyBump;
/** The threshold of life to consider doing a special bump effect (ex. enraged or knockdown) */
var const int ZedBumpEffectThreshold;
/** The chance of obliterating a zed on an enraged bump */
var const float ZedBumpObliterationEffectChance;
2022-10-29 23:52:58 +00:00
// Only enabled while we didn't receive damage and we use Aggro for choosing Enemy
var bool CanForceEnemy;
var Pawn ForcedEnemy;
var Pawn LastForcedEnemy;
var float ForcedEnemyLastTime;
var float DamageRatioToChangeForcedEnemy;
var float TimeCanRestartForcedEnemy;
var float TimeCannotChangeFromForcedEnemy;
2020-12-13 15:01:13 +00:00
/*********************************************************************************************
Evasion / Blocking
********************************************************************************************* */
/** Struct containing info for forced evade directions and their chance of being selected */
struct native sForcedEvadeChanceInfo
{
/** Forward-Left */
var float FL;
/** Forward-Right */
var float FR;
};
/** Struct containing information for AI to evade danger */
struct native sDangerEvadeInfo
{
/** Projectile class to check against */
var name ClassName;
/** Cooldown period, per-difficulty. Difficulty ranges are 0 to 3, Normal-Hard-Suicidal-HoE */
var array<float> Cooldowns;
/** Maximum evade chance, per-difficulty */
var array<float> EvadeChances;
/** Forced evade chances per difficulty */
var array<sForcedEvadeChanceInfo> ForcedEvadeChances;
/** Minimum and maximum delay before AI reacts to a projectile of this type */
var array<vector2d> ReactionDelayRanges;
/** Chances to block instead of evade */
var array<float> BlockChances;
/** How much to multiply the chance by when in a solo game */
var float SoloChanceMultiplier;
/** Last time a projectile of this type was evaded */
var transient float LastEvadeTime;
};
/** Struct containing information for AI to evade on damage */
struct native sEvadeOnDamageInfo
{
var float Chance;
var float DamagedHealthPctToTrigger;
var sForcedEvadeChanceInfo ForcedEvadeChance;
structdefaultproperties
{
Chance=0.f
}
};
/** Settings used for danger evasion, set on a per-projectile or per-weapon basis */
var protected array<sDangerEvadeInfo> DangerEvadeSettings;
/** What percentage of max health AccumulatedEvadeDamage needs to reach before an evade is triggered */
var transient sEvadeOnDamageInfo EvadeOnDamageSettings;
/** How much damage we've accumulated towards our next evade trigger */
var transient protected int AccumulatedEvadeDamage;
/** How much damage we've accumulated towards our next block trigger */
var transient protected int AccumulatedBlockDamage;
/*********************************************************************************************
Aggro
********************************************************************************************* */
struct native sFriendlyDamageInfo
{
var Controller DamagerController;
var int Damage;
};
/** Array containing all friendly AI that have damaged us */
var array<sFriendlyDamageInfo> FriendlyDamageHistory;
var byte CurrentEnemysHistoryIndex;
/** The percentage of damage that needs to be dealt to a zed to make this controller consider attacking the player */
var const float AggroPlayerHealthPercentage;
/** The time it will take before we will reset our damage history from an attacking player */
var const float AggroPlayerResetTime;
/** The time it will take before we will reset our damage history from another zed */
var float MinDistanceToAggroZed;
/** The time it will take before we will reset our damage history from another zed */
var const float AggroZedResetTime;
/** The percentage of damage that needs to be taken from another zed to make this controller consider attacking him */
var const float AggroZedHealthPercentage;
/** How long to wait between aggro switches */
var const float AggroEnemySwitchWaitTime;
/*********************************************************************************************
AI Commands
********************************************************************************************* */
/** Allow/Disallow combat transitions (see CheckCombatTransition()) */
var bool bAllowCombatTransitions;
/** If true, NPC is polling range to enemy, used in TickMeleeCombatDecision() */
var bool bIsProbingMeleeRangeEvents;
/** Last time this NPC performed a taunt */
var float LastTauntTime;
/** The time until we can perform another walking taunt */
var float RepeatWalkingTauntTime;
var float NextTauntTime;
var float NextRandomTauntTime;
var config float TauntTeamFactor;
var config Vector2D TauntTimeDelay;
var config Vector2D RandomTauntTimeDelay;
struct native InvalidAnchorItem
{
/** Anchor that was invalid */
var() NavigationPoint InvalidNav;
/** Time of path search that invalidated the anchor */
var() float InvalidTime;
};
var transient array<InvalidAnchorItem> InvalidAnchorList;
/*********************************************************************************************
* @name GameConductor balance values
**********************************************************************************************/
/** Tracks the time that this controller first saw an enemy player */
var float TimeFirstSawPlayer;
/*********************************************************************************************
* Debugging Related
********************************************************************************************* */
var Vector ChargeLocation;
/** String displayed with KFSceneCaptureDebugCam */
var string RecentActionInfo;
/** Render AI debug info to screen */
var bool bDebug_DrawAIDebug;
/** Draw line to Pawn's anchor when true */
var config bool bDebug_DrawAnchor;
/** Draws sphere around Pawn when bPreparingMove is true */
var config bool bDebug_ShowPreparingMove;
/** Draws the ranges for pending and current melee attacks */
var config bool bDebug_ShowStrikeRange;
/** Draws the ranges for all melee attacks */
var config bool bDebug_ShowAllStrikeRange;
/** Log AI_Log events to AIProfiler */
var config bool bDebug_LogToProfiler;
/** Render line to intermediate goal and one to end-goal during MoveToward calls */
var config bool bDebug_DrawPath;
/** Render overhead debug text */
var config bool bDebug_DrawOverheadInfo;
/** Used for debugging StepAside command */
var Rotator Debug_StepRot;
/** Used for debugging StepAside command */
var BasedPosition Debug_StepLoc;
/** If true, AILog will use a unique log file for each NPC rather than dumping everything in Launch.log */
var config bool bUseUniqueAILogFile;
/** Allows postrendering of AI debug info */
var config bool bDebug_PostRenderInfo;
/** Renders Separation steering force between NPCs if enabled */
var config bool bDebug_DrawSeparationSteering;
/** If true, NPC is in debug mode (AICommand_Debug is active command) */
var bool bHasDebugCommand;
/** Used internally, set by KFAIDirector on possess if KFAIDirector.bDebugAllAI is true */
var bool bForceDebugCommand;
/** Renders attack anim info if true */
var config bool bDebug_DrawAttackAnimInfo;
/** Draw viewcone (used in seeplayer) */
var config bool bDebug_ShowViewCone;
var KFPawn ChargeTarget;
var string AIActionStatus;
var config color Debug_TextColorOverride;
var array<name> EnabledDebugTextCategories;
/** Used for tracking down Zeds who might get stuck when spawning from certain volumes */
var KFSpawnVolume MySpawnVolume;
var bool bDisablePartialPaths;
/** The melee attack index we are currently doing ONLY USED FOR DEBUGGING */
var byte DebugCurrentStrikeIndex;
/** If set to true, this AI is considered a player by KFAISpawnManager_Versus */
var bool bIsSimulatedPlayerController;
var Vector LocationAtStartOfStuckCheck;
var Vector LocationAtLastStuckCheck;
var AICommand CmdTriggeringHardCoreStuckChecking;
var AITickablePlugin PlugInTriggeringTriggeringHardCoreStuckChecking;
/*********************************************************************************************
* AICommand History Debugging - useful in tracking down command related bugs. Enable through
* config file and each NPC will dump its history of AICommands and related info. If enabled,
* history will be output when this controller is destroyed, the pawn dies, or optionally only
* when exiting the game.
********************************************************************************************* */
/** Used when dumping all NPC AICommand history to log file */
var config bool bDebugCommandHistory;
/** When true, each NPCs AICommand history is dumped to log file when exiting game */
var bool bDumpCommandHistoryOnExit;
/** True if my AICommand history was already dumped to the log file */
var bool bDumpedCommandHistory;
/** Used with DumpCommandHistory(), contains info about each AICommand instance executed (see bDebugCommandHistory) */
struct native KFAICmdHistoryItem
{
var class<AICommandBase> CmdClass;
var string CmdName;
var float TimeStamp; // World time command was started
var float Duration; // Total time spent in the command
var bool bAborted; // Command was aborted (which is often fine and intentional depending on the context) (TODO:Change to bytes)
var bool bFailure; // Command failed (this is also not necessarily bad, at times it's expected)
var bool bSuccess; // Command succeeded
var string VerboseString; // Optional extra info that can be added
};
var array<KFAICmdHistoryItem> KFAICommandHistory;
cpptext
{
virtual void Initialize();
virtual void JumpOverWall(FVector WallNormal, AActor* Wall);
virtual void StoreCommandHistory( UGameAICommand* Cmd );
virtual UReachSpec* PrepareForMove( ANavigationPoint *NavGoal, UReachSpec* Path );
virtual DWORD LineOfSightTo( const AActor* Other, INT bUseLOSFlag=0, const FVector* chkLocation = NULL, UBOOL bTryAlternateTargetLoc = FALSE );
virtual UBOOL AirControlFromWall( float DeltaTime, FVector& RealAcceleration );
virtual FVector DesiredDirection();
virtual void BeginDestroy();
virtual UBOOL Tick( FLOAT DeltaTime, enum ELevelTick TickType );
virtual UBOOL TickMeleeCombatDecision( FLOAT DeltaTime ) { return FALSE; }
virtual UBOOL ShouldOffsetCorners();
virtual UBOOL ShouldUsePathLanes();
virtual UBOOL ForceReached( ANavigationPoint *Nav, const FVector& TestPosition );
virtual void UpdatePawnRotation();
virtual void UpdateLatentMoveDestination();
virtual void AdjustFromWall( FVector Hitnormal, AActor* HitActor );
virtual void RouteCache_Empty();
virtual void RouteCache_AddItem( ANavigationPoint* Nav );
virtual void RouteCache_InsertItem( ANavigationPoint* Nav, INT Idx );
virtual void RouteCache_RemoveItem( ANavigationPoint* Nav );
virtual void RouteCache_RemoveIndex( INT Index, INT Count );
virtual void StoreKFAICommandHistory( UGameAICommand* Cmd );
virtual UBOOL DirectPathToGoal( FVector TargetLoc, FLOAT TargetGroundOffset, FCheckResult &HitList, FVector FromLocation = FVector(0,0,0), UBOOL bCheckActors=true );
virtual UBOOL ShouldIgnoreNavigationBlockingFor( const AActor* Other );
virtual FRotator SetRotationRate( FLOAT deltaTime );
virtual INT ControllerModifyCostForReachSpec( UReachSpec* Spec, INT Cost );
virtual void PostPhysWalking(FLOAT DeltaTime);
virtual void MoveToward(AActor* goal, AActor* viewfocus, FLOAT DesiredOffset, UBOOL bStrafe, UBOOL bShouldWalk);
// Debugging
#if __TW_AIDEBUGGING_
virtual void FailMove( const FString& Reason );
#endif
}
/*********************************************************************************************
* General native function declarations
********************************************************************************************* */
/** Experimental - if repeatedly bumped by a moving Zed in a very short timespan, this will temporarily
turn off the collision for the bumped-into Zed to let the moving Zed pass through. GOW has similar
code to let players pass through blocking friendly NPC teammates. */
native function bool BumperSomewhereToGo();
native function bool LineBlockedByActor( vector Start, vector End, ReachSpec Spec, actor Actor );
native function bool IsPawnBlockingLine( vector Start, vector End, optional float MinCollisionHeight );
//native function bool CanJumpTo( Actor JumpToActor );
native function vector ComputeTrajectoryByTime( vector StartPosition, vector EndPosition, float EndTime );
native function CleanUp(optional bool bBeingDestroyed);
/** Aborts a command and any children if it has any. */
native function bool AbortCommand( GameAICommand AbortCmd, optional class<GameAICommand> AbortClass );
/** Push a new command on to the stack */
native function PushCommand(GameAICommand NewCommand);
/** Pop a command from the stack */
native function PopCommand(GameAICommand ToBePoppedCommand);
/** AI should ignore notifications that would alter behavior - ie. seeing/hearing enemies, getting shot, etc */
native function bool IgnoreNotifies() const;
/** Returns true if CheckPawn is currently moving away from my pawn, optionally at MinSpeed */
native function bool IsPawnMovingAwayFromMe( Pawn CheckPawn, optional float MinSpeed );
/** Returns a KFPawn if there is one blocking the path to EnemyPawn */
native function Pawn GetPawnBlockingPathTo( Pawn EnemyPawn, optional bool bTestTeam, optional bool bCheckVisible );
/** Lock the AI pawn rotation to a specific rotation, to unlock the pawn pass in zero */
native function LockPawnRotationTo( Rotator NewRotation );
/** Unlock the AI pawn's rotation */
native function UnlockPawnRotation();
/** Checks to see if our line of fire is blocked by friendlies. Return True if it is blocked */
final native function bool IsFriendlyBlockingFireLine( vector FireStart );
/** Used for leading Zed's target */
native function vector CalcAimLocToHit( vector AimSpot, vector StartFireLoc, float inSpeed, float inMaxSpeed, optional Vector LeadTargetVelocity );
/** Used for leading Zed's target */
static native function float EstimateProjectileTimeToTarget( float Distance, float StartSpeed, float MaxSpeed );
static native function bool FastActorTrace( Actor OriginTestActor, vector TraceEnd, optional vector TraceStart, optional vector BoxExtent, optional bool bTraceComplex );
static native function Actor ActorBlockTest( Actor OriginTestActor, vector TraceEnd, optional vector TraceStart, optional vector BoxExtent, optional bool bTraceActors, optional bool bTraceComplex );
native function bool TestTrace( vector TraceEnd, optional vector TraceStart );
native function bool IsPawnInFireLine( Pawn CheckPawn, vector FireStart, vector FireLine );
native final function bool SuggestNewWanderPoint( out vector out_NewMovePt, vector TryThisDirFirst, optional float MoveDist = 1024.f );
/** Queries the PRI and returns our current team index */
simulated native function byte GetTeamNum();
protected native function bool IsLookingAtWall( optional float CheckDist=768.f );
/*********************************************************************************************
* Initializtion, Pawn Possession, and Destruction
********************************************************************************************* */
/*---------------------------------------------------------
Native Init/Destroy related function declarations
---------------------------------------------------------*/
/** Clean up all the subobjects for the controller */
native function CleanUpOnDestroy();
/** TODO: Make this a native function */
event SetChargeLocation( Actor Victim, Vector VictimLocation );
/** This might be useful if you want a Zed to be notified when the player is aiming at it. If
bUseTargetedByPlayerEvent is true for this KFAIC, when you call IsTargetedByPlayer(), this
event will be called. */
event TargetedByPlayer( KFPawn InPlayer );
event PostBeginPlay()
{
Super.PostBeginPlay();
//BallisticProjectileFireBehavior = new(self) class'KFAiBallisticProjectileFireBehavior';
//BallisticProjectileFireBehavior.MyAiCtrl = self;
DirectProjectileFireBehavior = new(self) class'KFAiDirectProjectileFireBehavior';
DirectProjectileFireBehavior.MyAiCtrl = self;
LeapBehavior = new(self) class'KFAiLeapBehavior';
LeapBehavior.MyAiCtrl = self;
//DestructibleInterActionBehavior = new(self) class'KFAiDestructibleInterActionBehavior';
//DestructibleInterActionBehavior.MyAiCtrl = self;
bIamAsClosesToTheEnemyAsICanGet = false;
//BallisticProjectileFireBehavior.SetUp();
DirectProjectileFireBehavior.SetUp();
LeapBehavior.SetUp();
// Set our frustration delay
FrustrationDelay = RandRange(default.FrustrationDelay, default.FrustrationDelay*2); //+ Rand(3) + (2.0*fRand());
//Set initial value so things spawning dependent on this have a safe starting point
LastEnemySightedTime = WorldInfo.TimeSeconds;
}
/** Called when this controller has possessed inPawn */
event Possess( Pawn inPawn, bool bVehicleTransition )
{
local KFAIDirector Director;
`AILog( GetFuncName()$"() possessing pawn "$inPawn, 'AIController' );
super.Possess( inPawn, bVehicleTransition );
// Currently only enabling SeePlayer during latent moves in AICommand_MoveToGoal
Disable( 'SeePlayer' );
if( Steering != none )
{
InitSteering();
}
Pawn.bCanJump = true;
Pawn.bCanWalkOffLedges = true;
MyKFGameInfo = KFGameInfo( WorldInfo.Game );
if( MyKFGameInfo != none )
{
// Register new possession with AIDirector
Director = MyKFGameInfo.GetAIDirector();
if( Director != none )
{
MyAIDirector = Director;
MyAIDirector.NotifyNewPossess( self );
}
}
// Transition to the idle state.
BeginCombatCommand( None, "Possessed" );
// Init AI Steering object in 1-3 seconds
SetTimer( RandRange(1, 3), false, nameof(StartSteering) );
SetTimer( RandRange(MinTimeBetweenStatusUpdates, MaxTimeBetweenStatusUpdates), false, nameof(Timer_EvaluateStatus), self );
// Set up any desired debug related options.
SetupDebug();
// Give myself a PlayerReplicationInfo if bEnableGameAnalytics is true so I'll be viewable as if I'm a player when playing
// back recorded gamestats files.
if ( !bIsPlayer && MyKFGameInfo.bEnableGameAnalytics && !bDeleteMe && (WorldInfo.NetMode != NM_Client) )
{
InitPlayerReplicationInfo();
}
bShowVisualStuckZedDebugInfo = MyAIDirector.bShowVisualStuckZedDebugInfo;
bShowMovePointsDebugInfo = MyAIDirector.bShowMovePointsDebugInfo;
bShowHighDetailCombatMovementDebugInfo = MyAiDirector.bShowHighDetailCombatMovementDebugInfo;
bShowCollisionRadiusForReducedZedOnZedPinchPointCollisionState = MyAIDirector.bShowCollisionRadiusForReducedZedOnZedPinchPointCollisionState;
bShowLeapDownDebugArtifacts = MyAIDirector.bShowLeapDownDebugArtifacts;
bShowDoorNavigationDebugArtifacts = MyAIDirector.bShowDoorNavigationDebugArtifacts;
bShowDestructibleNavigationDebugArtifacts = MyAiDirector.bShowDestructibleNavigationDebugArtifacts;
if( bUsePluginsForMovement )
{
//KfMovementPlugin = KFAIPluginMovement_Recast(MovementPlugin);
//KfLeapPlugin = KFAIPluginLeap(LeapPlugin);
//KfStuckFixPlugin = KFAIPluginStuckFix(StuckFixPlugin);
}
// Give our pawn a pending attack upon on possesion
if( MyKFPawn != none && MyKFPawn.PawnAnimInfo != none )
{
AttackRange = MyKFPawn.PawnAnimInfo.GetAttackRange( self );
}
}
/** Used to initialize a pending attack, and choose our next attack after once we've started the pending attack */
function UpdatePendingStrike()
{
local KFPawnAnimInfo KFPAI;
if( MyKFPawn == none || MyKFPawn.PawnAnimInfo == none )
{
return;
}
DebugCurrentStrikeIndex = PendingAnimStrikeIndex;
KFPAI = MyKFPawn.PawnAnimInfo;
// Make sure we always have an attack range
if( AttackRange == 0 )
{
AttackRange = KFPAI.GetAttackRange( self );
}
PendingAnimStrikeIndex = KFPAI.ChooseNextStrike(MyKFPawn, Enemy);
// Make sure our attack name matches the PawnAnimInfo's tag
if( PendingAnimStrikeIndex == 255 )
{
StrikeRange = 0;
return;
}
// Update the range we want to get to perform our strike
UpdateStrikeRange();
`AILog( GetFuncName()$"() using AttackRange of "$AttackRange$" for attack polling.", 'Command_Attack_Melee' );
return;
}
/** Find the min/max range of each Tag (attack category) defined in my KFPawn_AnimGroup. This is per-Tag,
* not per attack animation, and is used to pre-determine if any of the attacks for a Tag is within range. */
function UpdateStrikeRange()
{
local float CollisionRadius;
if( PendingAnimStrikeIndex != 255 )
{
CollisionRadius = MyKFPawn.CylinderComponent.CollisionRadius;
StrikeRange = MyKFPawn.PawnAnimInfo.GetMedianStrikeRange(PendingAnimStrikeIndex, StrikeRangePercentage, CollisionRadius);
`AILog( GetFuncName()$"() using StrikeRange of "$StrikeRange$" for attack polling.", 'Command_Attack_Melee' );
}
}
/* Temporarily block the specified NavigationPoint if it's in my current path. This will only affect my own navigation,
* it's not globally blocked for all NPCs. */
event bool CreateTemporaryBlockedPath( NavigationPoint Nav )
{
local int i, x, PointIdx;
`AILog( GetFuncName()$"() for "$Nav, 'PathWarning' );
for( i = 0; i < RouteCache.Length; ++i )
{
if( RouteCache[i] == Nav )
{
for( x = 0; x < Nav.PathList.Length; ++x )
{
//`AILog( GetFuncName()$" Nav to block: "$Nav$", Anchor: "$Pawn.Anchor$"... checking current end actor "$Nav.PathList[x].End.Actor, 'PathWarning' );
DebugLogRoute();
PointIdx = i + 1;
if( RouteCache.Length > PointIdx && Nav.PathList[x].End.Actor == RouteCache[PointIdx] )
{
`AILog( GetFuncName()$"() Found a match, calling AddBlockedReachSpec for spec "$Nav.PathList[x]$" on nav point "$RouteCache[PointIdx] );
//`Log( GetFuncName()$"() Found a match, calling AddBlockedReachSpec for spec "$Nav.PathList[x]$" on nav point "$RouteCache[i+1] );
`RecordAIBlockedPath( self, Nav, RouteCache[PointIdx], "TestBlockedPath from "$Nav$" to "$RouteCache[PointIdx] );
`RecordAIRedirectedPath( self, IntermediateMoveGoal, "[HPO]Path:"$IntermediateMoveGoal$" and "$RouteCache[PointIdx] );
AddBlockedReachSpec( Nav.PathList[x], 10000000 );
return true;
}
}
}
}
return false;
}
/* Temporarily block the specified NavigationPoint if it's in my current path. This will only affect my own navigation,
* it's not globally blocked for all NPCs. */
function bool CreateTemporaryBlockedReach( NavigationPoint Nav, ReachSpec Reach )
{
`AILog( GetFuncName()$"() for "$Nav@Reach , 'PathWarning' );
if( Reach != none && Reach.End.Actor == Nav )
{
//`AILog( GetFuncName()$" Nav to block: "$Nav$", Anchor: "$Pawn.Anchor$"... checking current end actor "$Nav.PathList[x].End.Actor, 'PathWarning' );
DebugLogRoute();
`AILog( GetFuncName()$"() Found a match, calling AddBlockedReachSpec for spec "$Reach$" to nav point "$Nav );
//`Log( GetFuncName()$"() Found a match, calling AddBlockedReachSpec for spec "$Reach$" to nav point "$Nav );
`RecordAIBlockedPath( self, Nav, NavigationPoint(Reach.End.Actor), "TestBlockedPath from "$Nav$" to "$Reach.End.Actor );
`RecordAIRedirectedPath( self, IntermediateMoveGoal, "[HPO]Path:"$IntermediateMoveGoal$" and "$RouteCache[1] );
AddBlockedReachSpec( Reach, 10000000 );
return true;
}
return false;
}
function Timer_EvaluateStatus()
{
if( `TimeSince(LastNotifyHitWallTime) > 3.f )
{
LastHitWall = none;
LastWallHitNormal = vect(0.f,0.f,0.f);
HitWallCount = 0;
}
if( bHasDebugCommand || MyKFPawn == none || MyKFPawn.Health <= 0 || MyKFPawn.IsDoingSpecialMove() )
{
return;
}
if( Enemy == None || !Enemy.IsAliveAndWell() )
{
FindNewEnemy();
}
SetTimer( RandRange(MinTimeBetweenStatusUpdates, MaxTimeBetweenStatusUpdates), false, nameof(Timer_EvaluateStatus), self );
}
/** Called after PostBeginPlay(), and sets the initial state for this controller, if not already in that state */
simulated event SetInitialState()
{
if( GetStateName() != InitialState )
{
GotoState( InitialState );
}
}
/** Returns the default AI Command class for this controller */
function class<AICommand> GetDefaultCommand()
{
return DefaultCommandClass;
}
/** Returns whether we should exit combat and return to the idle state, generally because we don't have any more enemies */
function bool ShouldReturnToIdle()
{
local bool bShouldReturn;
bShouldReturn = ( !bMovingToGoal && Enemy == None );
return bShouldReturn;
}
function SetBestAnchor()
{
local NavigationPoint BestAnchor;
local float Dist;
BestAnchor = Pawn.GetBestAnchor( Pawn, Pawn.Location, true, false, Dist );
if( BestAnchor != none )
{
Pawn.SetAnchor( BestAnchor );
}
}
/** So far, no KF2 NPCs really need to return to idle, but leaving this in for now. Can override in subclasses */
function ReturnToIdle() {}
/** Used to add path constraints that we want on all the time */
function AddBasePathConstraints() {}
/** Returns current health percentage */
simulated final function float GetHealthPercentage()
{
local Pawn P;
P = Pawn;
if( P == none )
{
return 1.f;
}
else
{
return ( P.Health / float(P.HealthMax) );
}
}
function PawnDied( Pawn InPawn )
{
if( MyKFPawn != none )
{
`AILog( GetFuncName()$"() InPawn: "$InPawn$" - Lifetime was "$`TimeSince(MyKFPawn.CreationTime), 'Damage' );
}
else
{
`AILog( GetFuncName()$"() InPawn: "$InPawn, 'Damage' );
}
if( bDumpCommandHistoryOnExit )
{
if( bDebugCommandHistory && !bDumpedCommandHistory )
{
bDumpedCommandHistory = true;
DumpCommandHistory();
}
}
if( MyAIDirector != none )
{
// Unregister NPC with the AIDirector
MyAIDirector.UnregisterAIMember( self );
}
if( MyKFPawn != none )
{
MyKFPawn = None;
}
if( KFPawn_Monster(Pawn) != none && KFGameInfo(WorldInfo.Game) != none &&
TimeFirstSawPlayer >= 0 )
{
KFGameInfo(WorldInfo.Game).GameConductor.HandleZedKill( FMax(`TimeSince(TimeFirstSawPlayer),0.0));
// Set this so we know we already logged a kill for our pawn
TimeFirstSawPlayer = -1;
}
Super.PawnDied( InPawn );
}
function bool AttemptToTeleport( optional float CheckRadius=512 )
{
local NavigationPoint ResNav;
`AILog( GetFuncName() );
ResNav = class'KFPathnode'.static.GetNearestValidFloorNavWithinRadiusToPawn(Pawn, CheckRadius);
if( ResNav != none && TeleportToLocation(Resnav.Location, Pawn.Rotation) )
{
return true;
}
return false;
}
event bool TeleportToLocation(vector NewLoc, rotator NewRot, optional bool bGiveNavZBuffer = true, optional bool bCancelCombat=true, optional bool bCancelMovement=true )
{
local vector OldLocation;
//debug
`AILog( GetFuncName()@bCancelCombat@bCancelMovement@NewLoc, 'PathError' );
//DrawDebugLine(Pawn.Location,NewLoc,255,0,0,TRUE);
//DrawDebugCoordinateSystem(NewLoc,rot(0,0,0),100.f,TRUE);
if (Pawn != None )
{
OldLocation = Pawn.Location;
}
if( bGiveNavZBuffer )
{
NewLoc.Z += 32.f;
}
if (Pawn != None &&
Pawn.SetLocation(NewLoc))
{
LastTeleportLocation = OldLocation;
LastTeleportTime = WorldInfo.TimeSeconds;
if( Pawn.Physics == PHYS_RigidBody )
{
Pawn.Mesh.SetRBPosition( NewLoc );
}
RouteCache_Empty();
if( bCancelMovement )
{
ClearMovementInfo( false );
}
if( bCancelCombat && CommandList != None )
{
BeginCombatCommand( None, "Teleported" );
}
Pawn.SetRotation(NewRot);
Pawn.SetMovementPhysics();
AIZeroMovementVariables();
return true;
}
//debug
`AILog( "TELEPORT FAILED?!", 'PathWarning' );
return false;
}
/** Clean up all internal objects and references when the AI is destroyed
TBD: Look into using Unpossessed() to do this! */
simulated event Destroyed()
{
local KFDebugLines KFDL;
KFDL = class'KFDebugLines'.static.GetDebugLines();
if( KFDL != none )
{
KFDL.RemoveOwnedDebugLines( name );
KFDL.RemoveOwnedDebugSpheres( name );
KFDL.RemoveOwnedDebugText3D( name );
// SetPostRendering( false );
}
AbortCommand(CommandList);
MyKFPawn = None;
if( MyAIDirector != none )
{
// Unregister NPC with the AIDirector
MyAIDirector.UnregisterAIMember( self );
}
// Get rid of steering object.
if ( Steering != None )
{
Steering.OnOwnerDestroy();
Steering = None;
}
// Unregister NPC with the AIDirector
if( MyAIDirector != none )
{
MyAIDirector.UnregisterAIMember( self );
}
super.Destroyed();
}
/*********************************************************************************************
* Combat
**********************************************************************************************/
/*---------------------------------------------------------
Native combat function declarations
---------------------------------------------------------*/
/** Do I have an enemy within the specified distance range? */
final native function bool HasEnemyWithinDistance( float Distance, optional out Pawn out_EnemyPawn, optional bool bExact );
/** If enemy or my pawn is moving, will return the time (seconds) until and distance (UU) the two pawns will be closest along their trajectories */
native function GetClosestTimeAndDistToPawn( KFPawn CheckPawn, out float out_ClosestTime, out float out_ClosestDist );
/** Have I moved closely enough to my enemy? */
native function bool IsCloseEnoughToEnemy();
/** Is test location within melee range? */
native function bool InMeleeRange( const Vector TestLocation, optional name AttackTag );
native function bool InAttackTagRange( const name AttackTag, const Vector TestLocation );
native function bool InAnyAttackTagRange( const Vector TestLocation, out name outAttackTag );
/** Is test location within charge attack range? */
native function bool InChargeRange( const Vector TestLocation );
/** Enable polling for medium/long range attack range checking */
final native simulated final function EnableMeleeRangeEventProbing();
/** Disable polling for medium/long range attack range checking */
final native simulated final function DisableMeleeRangeEventProbing();
/** Is medium/long range attack range checking currently enabled? */
final native simulated final function bool IsMeleeRangeEventProbingEnabled() const;
/** Calls StopLatentExecution() if currently performing a latent moveto or movetoward */
native function StopAllLatentMoveExecution();
/** Am I being targeted by a player (optionally returns first found) */
native function bool IsTargetedByPlayer( optional out KFPawn outThreateningPlayer );
2022-10-29 23:52:58 +00:00
function Pawn FindForcedEnemy()
{
local KFGameInfo KFGI;
local KFPlayerController_WeeklySurvival KFPC_WS;
local class<KFPawn_Monster> MyMonster;
KFGI = KFGameInfo(WorldInfo.Game);
if(KFGI != none && KFGI.OutbreakEvent != none && KFGI.OutbreakEvent.ActiveEvent.bVIPGameMode)
{
MyMonster = class<KFPawn_Monster>(Pawn.Class);
// If this monster is included on the vip targetting, force VIP as enemy
if (KFGI.OutbreakEvent.ActiveEvent.VIPTargetting.Find(MyMonster) != INDEX_NONE)
{
foreach WorldInfo.AllControllers(class'KFPlayerController_WeeklySurvival', KFPC_WS)
{
if (KFPC_WS.VIPGameData.IsVIP && KFPC_WS.Pawn.IsAliveAndWell() && KFPC_WS.Pawn.CanAITargetThisPawn(self))
{
return KFPC_WS.Pawn;
}
}
}
}
return none;
}
2020-12-13 15:01:13 +00:00
/**
* Originally from KF1, KFMonsterController.uc, added check to take # of Zeds targeting
* the threat into account.
*/
event bool FindNewEnemy()
{
local Pawn PotentialEnemy, BestEnemy;
local float BestDist, NewDist;
local int BestEnemyZedCount;
local int PotentialEnemyZedCount;
local bool bUpdateBestEnemy;
if( Pawn == none )
{
return false;
}
BestEnemy = none;
2022-10-29 23:52:58 +00:00
if (BestEnemy == none)
{
foreach WorldInfo.AllPawns( class'Pawn', PotentialEnemy )
2020-12-13 15:01:13 +00:00
{
2022-10-29 23:52:58 +00:00
if( !PotentialEnemy.IsAliveAndWell() || Pawn.IsSameTeam( PotentialEnemy ) ||
!PotentialEnemy.CanAITargetThisPawn(self) )
2020-12-13 15:01:13 +00:00
{
2022-10-29 23:52:58 +00:00
continue;
2020-12-13 15:01:13 +00:00
}
2022-10-29 23:52:58 +00:00
NewDist = VSizeSq( PotentialEnemy.Location - Pawn.Location );
if( BestEnemy == none || BestDist > NewDist )
2020-12-13 15:01:13 +00:00
{
2022-10-29 23:52:58 +00:00
// New best enemies do not care about the number of zeds around us yet
BestEnemyZedCount = INDEX_None;
2020-12-13 15:01:13 +00:00
bUpdateBestEnemy = true;
}
2022-10-29 23:52:58 +00:00
else
{
// Only update NumZedsTargetingBestEnemy if it's a new best enemy and the best enemy is further
if(BestEnemyZedCount == INDEX_None)
{
// Cache BestEnemyZedCount so we don't need to calculate it again
BestEnemyZedCount = NumberOfZedsTargetingPawn( BestEnemy );
}
2020-12-13 15:01:13 +00:00
2022-10-29 23:52:58 +00:00
PotentialEnemyZedCount = NumberOfZedsTargetingPawn( PotentialEnemy );
if( PotentialEnemyZedCount < BestEnemyZedCount )
{
BestEnemyZedCount = PotentialEnemyZedCount;
bUpdateBestEnemy = true;
}
}
if( bUpdateBestEnemy )
{
BestEnemy = PotentialEnemy;
BestDist = NewDist;
bUpdateBestEnemy = false;
}
2020-12-13 15:01:13 +00:00
}
}
2022-10-29 23:52:58 +00:00
2020-12-13 15:01:13 +00:00
if( Enemy != none && BestEnemy != none && BestEnemy == Enemy )
{
return false;
}
if( BestEnemy != none )
{
ChangeEnemy( BestEnemy );
return HasValidEnemy();
}
return false;
}
/** Returns true if our new enemy has enough zeds already attacking him, for him to be our target */
function bool GetHasAcceptableEnemyCount( Pawn NewEnemy )
{
local Float NumZedsTargetingCurrentEnemy, NumZedsTargetingNewEnemy;
local float RatioOfZedsOldEnemyToNewEnemy;
// Added some rudimentary checks to help zeds prioritize when to switch targets - Ramm
if( Enemy != none )
{
// Check for zeds targeting the enemies out to 10 meters
NumZedsTargetingCurrentEnemy = NumberOfZedsTargetingPawn( Enemy,false,1000 );
NumZedsTargetingNewEnemy = NumberOfZedsTargetingPawn( NewEnemy,false,1000 );
// New enemy already has a full dance card, stick with current enemy
if( NumZedsTargetingNewEnemy >= 3 )
{
`AILog(GetFuncName()$" New enemy already has a full dance card, stick with current enemy", 'SetEnemy');
return false;
}
// Nobody nearby targeting the new enemy, go for it
if( NumZedsTargetingNewEnemy < 1 )
{
`AILog(GetFuncName()$" accepting "$NewEnemy$" because it's closer than our current, and nobody nearby targeting the new enemy", 'SetEnemy');
return true;
}
else
{
RatioOfZedsOldEnemyToNewEnemy = NumZedsTargetingCurrentEnemy/NumZedsTargetingNewEnemy;
// Don't switch to the new enemy if there are too few zeds targeting the current one
if( RatioOfZedsOldEnemyToNewEnemy <= 0.5 )
{
`AILog(GetFuncName()$" not accepting "$NewEnemy$" because there are too few zeds targeting the current one", 'SetEnemy');
return false;
}
}
}
return true;
}
/** Set a new enemy, returns false potential enemy is a player and AI is set to ignore players */
event bool SetEnemyToZed( Pawn NewEnemy )
{
local float NewEnemyDist, TimeSinceLastEnemyChange;
`AILog( GetFuncName()$"(), desired new enemy: "$NewEnemy, 'SetEnemy' );
// 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( NewEnemy == none || !NewEnemy.IsAliveAndWell() ||
Pawn == none || !Pawn.IsAliveAndWell() ||
Pawn == NewEnemy || !NewEnemy.CanAITargetThisPawn(self) )
{
return false;
}
// Evaluate whether enough time has passed since I last changed my enemy (TODO: Need to be able to override this in some situations)
TimeSinceLastEnemyChange = `TimeSince(LastSetEnemyTime);
if( TimeSinceLastEnemyChange < MinTimeBetweenEnemyChanges )
{
`AILog( GetFuncName()$"() not evaluating because not enough time has passed ("$TimeSinceLastEnemyChange$") since last SetEnemy() call.", 'SetEnemy' );
return false;
}
// Accept new enemy if its distance (plus some padding) is closer than enemy distance
NewEnemyDist = VSize(NewEnemy.Location - Pawn.Location);
if( NewEnemyDist < MinDistanceToAggroZed )
{
if( !GetHasAcceptableEnemyCount(NewEnemy) )
{
return false;
}
`AILog( GetFuncName()$"() accepting "$NewEnemy$" because Enemy ("$Enemy$") dist is > 1.2 * NewEnemy dist", 'SetEnemy' );
ChangeEnemy( NewEnemy );
return true;
}
return false;
}
/** Set a new enemy, returns false potential enemy is a player and AI is set to ignore players */
event bool SetEnemy( Pawn NewEnemy )
{
local float EnemyDistSq, NewEnemyDistSq, TimeSinceLastEnemyChange;
`AILog( GetFuncName()$"(), desired new enemy: "$NewEnemy, 'SetEnemy' );
// 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( NewEnemy == none || !NewEnemy.IsAliveAndWell() || Pawn.IsSameTeam( NewEnemy ) ||
Pawn == none || !Pawn.IsAliveAndWell() || !NewEnemy.CanAITargetThisPawn(self) )
{
return false;
}
if(Enemy != none && NewEnemy == Enemy)
{
// Was attacking a door to get to enemy, but enemy has now become reachable
if( DoorEnemy != none && LineOfSightTo( Enemy ) )
{
ChangeEnemy( NewEnemy );
return true;
}
return false;
}
// Ignore cooldown time and accept the new enemy if I absolutely need one
if( Enemy == none || !Enemy.IsAliveAndWell() )
{
`AILog( GetFuncName()$"(), accepting new enemy "$NewEnemy$" because current enemy is none or dead", 'SetEnemy' );
ChangeEnemy( NewEnemy );
return true;
}
// Evaluate whether enough time has passed since I last changed my enemy (TODO: Need to be able to override this in some situations)
TimeSinceLastEnemyChange = `TimeSince(LastSetEnemyTime);
if( TimeSinceLastEnemyChange < MinTimeBetweenEnemyChanges )
{
`AILog( GetFuncName()$"() not evaluating because not enough time has passed ("$TimeSinceLastEnemyChange$") since last SetEnemy() call.", 'SetEnemy' );
return false;
}
// Give any AICommands on the stack an opportunity to reject changing my enemy.
if( CachedAICommandList != none && !CachedAICommandList.CanChangeEnemy(NewEnemy) )
{
`AILog( GetFuncName()$"() rejecting "$NewEnemy$" because an AICommand rejected it", 'SetEnemy' );
return false;
}
// From KF1 - if I already have an enemy, and I have LOS to my enemy and my enemy is closer than NewEnemy, then keep my current enemy.
if( LineOfSightTo(Enemy) && VSize(Enemy.Location-Pawn.Location) < VSize(NewEnemy.Location-Pawn.Location) )
{
`AILog( GetFuncName()$"() rejecting "$NewEnemy$" because I have LOS to current enemy ("$Enemy$") and current enemy is also closer to me", 'SetEnemy' );
return false;
}
// Accept new enemy if I can't see my current enemy (CanSee takes PeripheralVision into account)
if( !CanSee(Enemy) )
{
`AILog( GetFuncName()$"() accepting "$NewEnemy$" because I cannot see my current enemy "$Enemy, 'SetEnemy' );
ChangeEnemy( NewEnemy );
return true;
}
// Reject new enemy if I can't see it (CanSee takes PeripheralVision into account)
if( !CanSee(NewEnemy) )
{
`AILog( GetFuncName()$"() rejecting "$NewEnemy$" because I cannot see it, but I can see my current enemy "$Enemy, 'SetEnemy' );
return false;
}
// Reject new enemy if my current enemy is within my StrikeRange
EnemyDistSq = VSizeSq( Enemy.Location - Pawn.Location );
if( MyKFPawn != none && EnemyDistSq < Square(StrikeRange) )
{
`AILog( GetFuncName()$"() rejecting "$NewEnemy$" because current enemy ("$Enemy$") is within my desired melee range", 'SetEnemy' );
return false;
}
// Accept new enemy if its distance (plus some padding) is closer than enemy distance
NewEnemyDistSq = Square( 1.2f * VSize(NewEnemy.Location - Pawn.Location) );
if( EnemyDistSq > NewEnemyDistSq )
{
if( !GetHasAcceptableEnemyCount(NewEnemy) )
{
return false;
}
`AILog( GetFuncName()$"() accepting "$NewEnemy$" because Enemy ("$Enemy$") dist is > 1.2 * NewEnemy dist", 'SetEnemy' );
ChangeEnemy( NewEnemy );
return true;
}
return false;
if( MyKFPawn != none && Enemy != none && Enemy == NewEnemy )
{
MyKFPawn.LookAtPawn( NewEnemy );
}
return true;
}
function ChangeEnemy( Pawn NewEnemy, optional bool bCanTaunt = true )
{
2022-10-29 23:52:58 +00:00
local Pawn OldEnemy, NewForcedEnemy;
2020-12-13 15:01:13 +00:00
local KFGameInfo KFGI;
2022-10-29 23:52:58 +00:00
if (CanForceEnemy)
{
NewForcedEnemy = FindForcedEnemy();
}
else if (NewEnemy == LastForcedEnemy)
{
return; // Don't allow to change to the ForcedEnemy while we can't (we reenable that again from outside)
}
if (NewForcedEnemy != none)
{
ForcedEnemy = NewForcedEnemy;
if (Enemy != ForcedEnemy)
{
LastForcedEnemy = ForcedEnemy;
ForcedEnemyLastTime = WorldInfo.TimeSeconds;
}
NewEnemy = NewForcedEnemy;
}
else
{
ForcedEnemy = none;
}
2020-12-13 15:01:13 +00:00
// gameinfo hook that calls mutator hook
KFGI = KFGameInfo( WorldInfo.Game );
if( KFGI != none )
{
KFGI.OnAIChangeEnemy( self, NewEnemy );
}
if( Enemy != none && Enemy != NewEnemy )
{
if( bCanTaunt && MyKFPawn != none && MyKFPawn.CanDoSpecialMove(SM_WalkingTaunt) )
{
MyKFPawn.DoSpecialMove(SM_WalkingTaunt);
}
OldEnemy = Enemy;
if( OldEnemy != none )
{
`RecordAIChangedEnemy( self, NewEnemy, OldEnemy, "ChangeEnemy() " );
}
// Set our last enemy switch time for the aggro system
LastEnemySwitchTime = WorldInfo.TimeSeconds;
}
`AILog( GetFuncName()$"() set Enemy to "$NewEnemy, 'SetEnemy' );
Enemy = NewEnemy;
BroadcastEnemyKnowledge(NewEnemy, WSPM_Belief);
// Notify AI commands about the enemy change.
if( CachedAICommandList != none )
{
if( OldEnemy != none )
{
CachedAICommandList.NotifyEnemyChanged( OldEnemy );
}
else
{
CachedAICommandList.NotifyEnemyChanged();
}
}
// Update cooldown time.
LastSetEnemyTime = WorldInfo.TimeSeconds;
}
/** Search for enemies within a set FOV range */
function KFPawn CheckForEnemiesInFOV( float MaxRange,
float MinFOV,
float MaxFOV,
optional bool bForceRetarget,
optional bool bTauntNewEnemy=true )
{
local vector PawnDir, Projection;
local float FOVDot, TempDistSQ, BestDistSQ;
local KFPawn KFP, BestTarget;
if( Pawn == none )
{
return none;
}
PawnDir = vector( Pawn.Rotation );
foreach Pawn.OverlappingActors( class'KFPawn', KFP, MaxRange )
{
if( bForceRetarget && Enemy == KFP )
{
continue;
}
if( !KFP.IsAliveAndWell() || KFP.GetTeamNum() == GetTeamNum() || !KFP.CanAITargetThisPawn(Pawn.Controller))
{
continue;
}
Projection = KFP.Location - Pawn.Location;
// Only care about players within the FOV range
FOVDot = PawnDir dot Normal( Projection );
if( FOVDot < MinFOV || FOVDot > MaxFOV )
{
continue;
}
// Make sure enemy isn't obstructed
if( !`FastTracePhysX(KFP.Location, Pawn.Location) )
{
continue;
}
// Make sure enemy isn't too far away
TempDistSQ = VSizeSQ( Projection );
// Scales distance by aggro rating (0.0 - 1.0)
TempDistSQ *= 1.f - GetAggroRating( KFP );
if( BestTarget == none || TempDistSQ < BestDistSQ )
{
BestDistSQ = TempDistSQ;
BestTarget = KFP;
}
}
if( BestTarget != none && BestTarget != Enemy )
{
ChangeEnemy( BestTarget, bTauntNewEnemy );
return BestTarget;
}
return none;
}
/** Allows subclasses to assess aggro ratings on a pawn-by-pawn basis */
function float GetAggroRating( KFPawn KFP )
{
return 0.f;
}
/** Enable notifications sent from native code which signial when NPC is in range to perform certain attacks */
function EnableProbingMeleeRangeEvents( optional bool bForce )
{
if( bForce || !MyKFPawn.IsDoingSpecialMove() )
{
`AILog( GetFuncName()$"() setting bIsProbingMeleeRangeEvents to true, restoring ability to melee attack", 'AIController' );
EnableMeleeRangeEventProbing();
ClearTimer( nameof(EnableProbingMeleeRangeEvents) );
}
else
{
`AILog( GetFuncName()$"() waiting to restore ability to melee attack, will check again in 0.1 seconds", 'AIController' );
SetTimer( 0.25f, true, nameof(EnableProbingMeleeRangeEvents) );
}
}
/** Disable notifications sent from native code which signial when NPC is in range to perform certain attacks */
function ResetProbingMeleeRangeEvents( optional float DelayOverride )
{
local float Delay;
//DelayOverride = 0.5;
Delay = (DelayOverride>0.f?DelayOverride:0.07f);
`AILog( GetFuncName()$"() will be resetting bIsProbingMeleeRangeEvents to true in "$Delay$" seconds", 'Command_Attack_Melee' );
SetTimer( Delay, false, nameof(EnableProbingMeleeRangeEvents), self );
//EnableProbingMeleeRangeEvents();
}
/** Event called by TickMeleeCombatDecision() if MeleeRangeEventProbing is enabled and all basic melee checks have passed */
event ReadyToMelee();
/** Override in child classes for NPCs using the suicide AICommand (i.e, Husks) */
function bool IsSuicidal()
{
return false;
}
function bool IsDoingGrabSpecialMove()
{
return ( MyKFPawn != none && MyKFPawn.IsDoingSpecialMove(SM_GrappleAttack) );
}
function bool CanTargetBeGrabbed( KFPawn TargetKFP )
{
local KFAIController OtherKFAIC;
// Disallow if target is invalid, already being grabbed, or currently falling
if( TargetKFP == none || (TargetKFP.Health <= 0) || (TargetKFP.IsDoingSpecialMove(SM_GrappleVictim)) || TargetKFP.Physics == PHYS_Falling )
{
return false;
}
// Need to check that no other clots are performing initial grab move on player since SM_GRAB is run without the player in a special move
foreach WorldInfo.AllControllers( class'KFAIController', OtherKFAIC )
{
if( OtherKFAIC == self )
{
continue;
}
// If the the other Zed is doing a grab start or grab attack special move, and TargetKFP is also his enemy, disallow
// this grab. Already checked if player is doing a grapplevictim special move above, but the player isn't executing
// the victim special move until the other Zed's initial grab anim is complete (see KFSM_Clot_Grab - this is the move that determines
// whether the clot's grab should miss or succeed based on when the initial sequence is interrupted)... so instead check
// whether the starting-to-grab Zed has the same enemy.
// The grab special move assumes that the grab victim is also the grabber's enemy.
if( OtherKFAIC.MyKFPawn != none && OtherKFAIC.Enemy == TargetKFP && OtherKFAIC.IsDoingGrabSpecialMove() )
{
return false;
}
}
return true;
}
/**
* BeginCombatCommand() - Initiates general base combat command for NPC.
*
* @param CmdClass Command to start.
* @param Reason Debug string.
* @param bForced Init command regardless of combat transition status.
*/
function BeginCombatCommand( class<AICommand> CmdClass, optional coerce string Reason, optional bool bForced )
{
local class<AICommand_Base_Combat> CurClass;
local AICommand_Base_Combat CurCommand;
`AILog( GetFuncName()@CmdClass@"(CommandList:"$CommandList$") Reason: "$Reason$" bForced: "$bForced, 'InitAICommand' );
if( CommandList != None )
{
CurCommand = AICommand_Base_Combat(CommandList);
if( CurCommand != None )
{
CurClass = AICommand_Base_Combat(CommandList).Class;
}
}
if( !bForced )
{
if( CmdClass != None && !bAllowCombatTransitions )
{
`AILog( self$" Not allowing combat transition due to scripting", 'InitAICommand' );
return;
}
if( CommandList != None && !CommandList.AllowTransitionTo( CmdClass ) )
{
`AILog( self$" Current command stack rejected transiton", 'InitAICommand' );
return;
}
}
CleanOutCommandListOnCombatTransition();
if( CmdClass != None )
{
InitalizeBaseCommand( CmdClass );
}
else
{
// If moving from combat back to Idle
if( CurClass != None )
{
ReturnToIdle();
}
GotoState( 'Action_Idle', 'Begin' );
}
}
function InitalizeBaseCommand( class<AICommand> CmdClass )
{
CmdClass.static.InitCommand( self );
}
function CleanOutCommandListOnCombatTransition()
{
if( CommandList != None )
{
AbortCommand( CommandList ); //[NEWCODE] was commented out, isn't in gow3
}
}
/** Find the closest potential enemy to my pawn */
final function Pawn GetClosestEnemy( optional Pawn ExcludePawn )
{
local Pawn P, Best;
local float Dist, BestDist;
BestDist = 1000000.0;
foreach WorldInfo.AllPawns( class'Pawn', P )
{
if( !P.IsAliveAndWell() || (ExcludePawn != none && ExcludePawn == P) || !P.CanAITargetThisPawn(self) )
{
continue;
}
if( (P.Controller != None && P.Controller != self && P.IsAliveAndWell()) && !Pawn.IsSameTeam( P ) )
{
Dist = VSize( P.Location - Pawn.Location );
if( Dist < BestDist )
{
Best = P;
BestDist = Dist;
}
}
}
`AILog( GetFuncName()$"() returning closest enemy "$Best, 'SetEnemy' );
if( Best == none && ExcludePawn != none )
{
Best = ExcludePawn;
}
return Best;
}
/** Returns true if NPC is doing an attack-related special move. TODO: Needs refactor (remove IsA, etc.) */
final event bool IsDoingAttackSpecialMove()
{
local ESpecialMove KFSM;
if( MyKFPawn.IsDoingSpecialMove() )
{
KFSM = MyKFPawn.SpecialMove;
if( KFSM == SM_MeleeAttack || KFSM == SM_GrappleAttack || KFSM == SM_SonicAttack || KFSM == SM_MeleeAttackDoor
|| KFSM == SM_Suicide )
{
return true;
}
}
return false;
}
/** Check for a combat transition, called once the current behavior has completed */
function CheckCombatTransition()
{
local class<AICommand> NewCommand;
local string Reason;
local bool bTransition;
local AICommand_Base_Combat CurCommand;
`AILog( GetFuncName()$"() bAllowCombatTransitions: "$bAllowCombatTransitions, 'CombatTransitions' );
if (Pawn == None || !Pawn.IsAliveAndWell() || !bAllowCombatTransitions || IsDead() )
{
return;
}
`AILog( GetFuncName()@CommandList$" calling CheckInterruptCombatTransitions now", 'CombatTransitions' );
// First check for interrupt transitions
bTransition = !CheckInterruptCombatTransitions();
// If no interrupt occurred
if( bTransition )
{
`AILog( "bTransition was true from CheckInterruptCombatTransitions()", 'CombatTransitions' );
CurCommand = AICommand_Base_Combat(CommandList);
if( CurCommand != None &&
CurCommand.CheckTransition( NewCommand, Reason ) )
{
`AILog( GetFuncName()$"() Calling BeginCombatCommand for "$NewCommand, 'CombatTransitions' );
BeginCombatCommand( NewCommand, Reason );
}
else
if( CurCommand == None && (CommandList == none || CommandList.Class != DefaultCommandClass))
{
NewCommand = GetDefaultCommand();
if(NewCommand == none)
{
`AILog("WARNING! "$self$" has no default command specified!", 'CombatTransitions');
return;
}
`AILog( GetFuncName()$"() Calling BeginCombatCommand for 2 "$NewCommand, 'CombatTransitions' );
BeginCombatCommand(NewCommand, "No Command Specified" );
}
}
}
//function NotifyKilled(Controller Killer, Controller Killed, Pawn KilledPawn)
function NotifyKilled(Controller Killer, Controller Killed, pawn KilledPawn, class<DamageType> damageTyp)
{
// TODO: add a radius check so that zeds won't taunt over really far away guys
// or if there is another enemy really close!
if( Killer != none && Killer == self && KilledPawn != none && Pawn != none
&& !Pawn.IsSameTeam( KilledPawn ) && KilledPawn == Enemy )
{
class'AICommand_TauntEnemy'.static.Taunt(self, KilledPawn, TAUNT_EnemyKilled );
}
else if ( KilledPawn != none && KilledPawn == Enemy )
{
// Find a new enemy if our current enemy dies!
FindNewEnemy();
}
super.NotifyKilled(Killer, Killed, KilledPawn, damageTyp);
}
/** Returns true if we have a valid enemy targeted */
final function bool HasValidEnemy( optional Pawn TestEnemy )
{
if( TestEnemy == None )
{
TestEnemy = Enemy;
}
if( TestEnemy == None ||
!TestEnemy.IsValidEnemyTargetFor( PlayerReplicationInfo, true ) )
{
return false;
}
return true;
}
/** TODO: Refactor to always just use FindNewEnemy */
function bool SelectEnemy()
{
return FindNewEnemy();
}
/** TODO: Refactor to always just use FindNewEnemy */
function bool SelectTarget()
{
return FindNewEnemy();
}
/** 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;
}
BumpedMonster = KFPawn_Monster(Other);
if( BumpedMonster == none || !BumpedMonster.IsAliveAndWell() || BumpedMonster.ZedBumpDamageScale <= 0 )
{
return false;
}
if( MyKFPawn == none || !MyKFPawn.IsAliveAndWell() )
{
return false;
}
if( (MyKFPawn.bIsSprinting || MyKFPawn.IsEnraged()) )
{
BumpEffectDamage = ZedBumpEffectThreshold * BumpedMonster.ZedBumpDamageScale * (MyKFPawn.bIsSprinting ? 2 : 1);
// If the Bumped Zed is near death, play either a knockdown or an immediate obliteration
if( BumpedMonster.Health - BumpEffectDamage <= 0 )
{
BumpedMonster.TakeDamage(BumpEffectDamage, self, BumpedMonster.Location, vect(0, 0, 0), MyKFPawn.GetBumpAttackDamageType());
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;
}
/*********************************************************************************************
* MELEE
********************************************************************************************* */
/** Notification from AICommand_Attack_Melee that I've just completed an attack */
function NotifyMeleeAttackFinished();
/** Starts base melee AI command */
function BeginMeleeCommand( Pawn TargetPawn, optional coerce String Reason )
{
`AILog( GetFuncName()$"() TargetPawn: "$TargetPawn, 'Command_Attack_Melee' );
if( AICommand_Base_Zed(CommandList) == none )
{
// Optionally set my enemy to TargetPawn
if( TargetPawn != None )
{
SetEnemy( TargetPawn );
}
if( MeleeCommandClass != GetDefaultCommand() )
{
`AILog( GetFuncName()$"() calling BeginCombatCommand for "$MeleeCommandClass, 'Command_Attack_Melee' );
BeginCombatCommand( MeleeCommandClass, Reason, true );
}
}
}
/** Is current potential target valid? TODO: Conditions need refactoring */
function bool IsValidAttackTarget( Actor CheckActor )
{
if( CheckActor == none || CheckActor.bTearOff )
{
return false;
}
return true;
}
/**
* Used by movement commands to determine if NPC is close enough consider a strike
*/
function bool IsWithinAttackRange()
{
local float DistSqToEnemy;
// If my current enemy isn't valid, set my enemy to the closest one available
if( MyKFPawn == none || Enemy == none || !MyKFPawn.IsAliveAndWell() || !Enemy.IsAliveAndWell() )
{
if( Enemy == none || Enemy.Health <= 0 )
{
Enemy = none;
SetEnemy( GetClosestEnemy() );
return false;
}
return true;
}
if( MyKFGameInfo.NumPlayers > 1 )
{
// If at least 4 other KFPawns are within 350 units of my enemy, and I'm close enough to see my enemy...
if( KFPawn(Enemy).IsSurrounded(false, 4, 350.f) && CanSee(Enemy) )
{
// ... Try to find a new enemy and return false if I decided to change targets
if( FindNewEnemy() )
{
`AILog( GetFuncName()$" found that enemy is surrounded so changed enemy to "$Enemy, 'ReachedEnemy' );
return false;
}
}
}
if( MyKFPawn.PawnAnimInfo == none )
{
`warn( "No PawnAnimInfo defined for "$MyKFPawn );
return IsCloseEnoughToEnemy();
}
// Check distance from enemy versus my PawnAnimInfo's AttackRange value.
DistSqToEnemy = VSizeSq( Enemy.Location - Pawn.Location );
if( DistSqToEnemy <= AttackRange * AttackRange )
{
return true;
}
return false;
}
/** Check to see if this KFPawn is within allowed height difference for melee */
function bool WithinMeleeHeightDifference( KFPawn EnemyPawn )
{
local vector EnemyPawnGroundPos, MyPawnGroundPos;
local vector EnemyPawnCylinder, MyPawnCylinder;
EnemyPawnCylinder.Z = EnemyPawn.CylinderComponent.CollisionHeight;
MyPawnCylinder.Z = Pawn.CylinderComponent.CollisionHeight;
EnemyPawnGroundPos = EnemyPawn.Location - EnemyPawnCylinder;
MyPawnGroundPos = Pawn.Location - MyPawnCylinder;
return ( Abs(Normal(EnemyPawnGroundPos - MyPawnGroundPos).Z) < MaxMeleeHeightAngle );
}
/** Notification that a melee attack has ended or been aborted */
function EndOfMeleeAttackNotification();
/** Notification from melee attack command that it was aborted due to damage taken */
function NotifyCommandMeleeAttackAborted()
{
local AICommand_Attack_Melee MeleeCommand;
`AILog( GetFuncName()$"()", 'Command_Attack_Melee' );
// Push AICommand if it is defined. (jessnew)
if( CommandList != None && GetActiveCommand().IsA('AICommand_Attack_Melee') )
{
MeleeCommand = AICommand_Attack_Melee( GetActiveCommand() );
if( MeleeCommand != none )
{
`AILog( GetFuncName()$"() calling AbortCommand for "$MeleeCommand, 'Command_Attack_Melee' );
AbortCommand( MeleeCommand );
}
}
}
/** Updates time of last melee attack performed by this NPC (TODO: Need to update this) */
function UpdateLastMeleeTime( EAnimSlotStance BodyStance )
{
LastAttackTime_Melee = WorldInfo.TimeSeconds;
}
/** Chooses a melee attack animation by name */
function ChooseStrikeByName( name AttackName, optional bool bForce )
{
local int DesiredStrikeIndex;
local KFPawnAnimInfo KFPAI;
if( AttackName != '' && MyKFPawn != none && MyKFPawn.PawnAnimInfo != none )
{
KFPAI = MyKFPawn.PawnAnimInfo;
DesiredStrikeIndex = KFPAI.GetAttackIndexByTag(AttackName);
// Make sure our attack name matches the PawnAnimInfo's tag
if( DesiredStrikeIndex < 0 )
{
`warn("MeleeAttackRandList Tag "$ AttackName $" Could not be found in the PawnAnimInfo");
return;
}
// if we aren't forcing the attack, check to see if this is a valid attack
if ( bForce || KFPAI.CanDoAttackAnim(DesiredStrikeIndex, MyKFPawn, Enemy) )
{
// Updates the next time this attack can be used
KFPAI.UpdateAttackCooldown(self, DesiredStrikeIndex);
return;
}
}
`log(self@GetFuncName()@"failed to find a valid attack");
}
/** Override in child classes - return true if permitted to grab enemy */
event bool CanGrabAttack()
{
return false;
}
/** Override in child classes if pawn can grab */
event DoGrabAttack( optional Pawn NewEnemy, optional float InPostSpecialMoveSleepTime=0.f ) {}
/** Count how many Zeds are currently targeting P, optionally excluding myself from the count */
function int NumberOfZedsTargetingPawn( Pawn P, optional bool bExcludeMe=true, optional float MinDistanceToInclude )
{
local Controller C;
local int ZedCount;
if( P == none || !P.IsAliveAndWell() )
{
return 0;
}
foreach WorldInfo.AllControllers( class'Controller', C )
{
if( C.Enemy == P && C.Pawn != none && (C.Pawn != Pawn || !bExcludeMe) && C.Pawn.IsAliveAndWell() )
{
if( MinDistanceToInclude == 0.f || (MinDistanceToInclude > 0.f && (VSizeSq(C.Pawn.Location - P.Location) <= MinDistanceToInclude * MinDistanceToInclude)) )
{
ZedCount++;
}
}
}
return ZedCount;
}
/**
* How many other Zeds are currently targeting my enemy? Use to determine ahead of time if it'd be worthwhile
* to target players with fewer Zeds targeting them.
*/
function int OtherZedsTargetingMyEnemy()
{
local KFAIController KFAIC;
local int ZedCount;
if( Enemy == none || !Enemy.IsAliveAndWell() )
{
return 0;
}
foreach WorldInfo.AllControllers( class'KFAIController', KFAIC )
{
if( KFAIC != self && KFAIC.Pawn != none && KFAIC.Pawn.IsAliveAndWell() && KFAIC.Enemy != none && KFAIC.Enemy == Enemy )
{
ZedCount++;
}
}
return ZedCount;
}
/** Returns how many Zeds are out there, optionally how many are within the specified # of units to my pawn */
function int GetZedCount( optional float MinDistanceFromMyPawn=0.f )
{
local KFAIController KFAIC;
local int ZedCount;
foreach WorldInfo.AllControllers( class'KFAIController', KFAIC )
{
if( KFAIC.Pawn != none && KFAIC.Pawn.IsAliveAndWell() )
{
if( MinDistanceFromMyPawn > 0.f )
{
if( VSizeSq(KFAIC.Pawn.Location - Pawn.Location) <= (MinDistanceFromMyPawn*MinDistanceFromMyPawn) )
{
ZedCount++;
}
}
else
{
ZedCount++;
}
}
}
return ZedCount;
}
native final function bool CanSeeByPointsEx( Vector ViewLocation, Vector TestLocation, Rotator ViewRotation, out Vector outHitLoc, out Actor outHitActor );
/**
* True if MinimalCount Zeds are within CheckRadius units of aLocation, optionally requiring a Zed can currently see
* that location before counting him.
*/
function bool AreZedsNear( vector aLocation, optional bool bRequireCanSee=false, optional float CheckRadius=650.f, optional int MinimalCount=1 )
{
local int i, ZedCount;
local rotator ViewRot;
local vector ViewLoc;
if( MyAIDirector == none )
{
return false;
}
// Can use KFAIDirector's AIList rather than AllControllers, etc.
for( i = 0; i < MyAIDirector.AIList.Length; i++ )
{
if( ZedCount >= MinimalCount )
{
return true;
}
if( MyAIDirector.AIList[i] == self || MyAIDirector.AIList[i].MyKFPawn == none || MyAIDirector.AIList[i].MyKFPawn.IsUsingSuperSpeed() )
{
continue;
}
if( CheckRadius > 0.f && VSizeSq( MyAIDirector.AIList[i].Pawn.Location - aLocation ) < (CheckRadius * CheckRadius) )
{
if( bRequireCanSee )
{
MyAIDirector.AIList[i].MyKFPawn.GetActorEyesViewPoint( ViewLoc, ViewRot );
if( MyAIDirector.AIList[i].CanSeeByPoints( ViewLoc, aLocation, ViewRot ) )
{
ZedCount++;
}
else
{
continue;
}
}
else
{
ZedCount++;
}
}
}
return false;
}
/** Returns false if this attack is still on cooldown */
function bool CheckCooldownTimer(name CooldownTag)
{
local int i;
i = CooldownTimers.Find('Tag', CooldownTag);
if ( i != INDEX_None )
{
if ( `TimeSince(CooldownTimers[i].ActivationTime) < CooldownTimers[i].Duration )
{
return false;
}
}
return true;
}
/** Add (or update existing) cooldown timer */
function AddCooldownTimer(name CooldownTag, float CooldownTime)
{
local int i;
local CooldownData NewCooldown;
i = CooldownTimers.Find('Tag', CooldownTag);
if ( i == INDEX_None )
{
NewCooldown.Tag = CooldownTag;
i = CooldownTimers.AddItem(NewCooldown);
}
CooldownTimers[i].Duration = CooldownTime;
CooldownTimers[i].ActivationTime = WorldInfo.TimeSeconds;
}
/** Set overall attack cooldown timer */
function SetOverallCooldownTimer(float CooldownTime)
{
OverallAttackCooldownTimer.Duration = CooldownTime;
OverallAttackCooldownTimer.ActivationTime = WorldInfo.TimeSeconds;
}
/** Returns false if the overall attacks are still on cooldown */
function bool CheckOverallCooldownTimer()
{
if ( OverallAttackCooldownTimer.Duration > 0 &&
`TimeSince(OverallAttackCooldownTimer.ActivationTime) < OverallAttackCooldownTimer.Duration )
{
return false;
}
return true;
}
/*---------------------------------------------------------
Combat transitions
---------------------------------------------------------*/
/** Check for an interrupt transition, can override any normal transition */
function bool CheckInterruptCombatTransitions()
{
local AICommand_Base_Combat CurCommand;
`AILog( "---- "$GetFuncName()$"() IgnoreNotifies? : "$IgnoreNotifies(), 'CombatTransitions' );
// If dead, gone, or ignoring notifies don't bother
if( Pawn == None || !Pawn.IsAliveAndWell() || IsDead() || IgnoreNotifies() )
{
`AILog( "---- "$GetFuncName()$"() aborting early", 'CombatTransitions' );
return false;
}
// check that our enemy is still valid
if( Enemy != None && ( !Enemy.IsAliveAndWell() || Enemy.Controller == none ) )
{
`AILog( GetFuncName()$"() setting enemy to NONE because they have no controller or they are dead!", 'CombatTransitions' );
SetEnemy( None );
}
// If not already in combat then don't bother
CurCommand = AICommand_Base_Combat( CommandList );
if( CurCommand == None )
{
`AILog( "---- "$GetFuncName()$"() setting enemy aborting because current command ("$CurCommand$") isn't a combat based command", 'CombatTransitions' );
return false;
}
if( AICommand_Base_Combat(CommandList) != None && ShouldReturnToIdle() )
{
//`AILog( "---- "$GetFuncName()$"() setting enemy aborting because current command ("$CurCommand$") isn't a combat based command", 'CombatTransitions' );
`AILog( GetFuncName()$"() returning true because I have a combat command and ShouldReturnToIdle returned true", 'CombatTransitions' );
//BeginCombatCommand( None, "No Enemies" );
return true;
}
`AILog( GetFuncName()$"() returning false", 'CombatTransitions' );
return false;
}
/*********************************************************************************************
* Movement Methods
********************************************************************************************* */
/*---------------------------------------------------------
Native definitions
---------------------------------------------------------*/
/** Same as actor reachable, only allows the "no anchor check" option to be used from script */
native final function bool ActorReachableNoAnchorCheck( actor anActor );
// Returns true if next intermediate move target is a KFJumpPoint node
native function bool IsMovingToJumpPoint();
// Initializes NPC steering
native function InitSteering();
// Returns NPC's steering object
native function KFAISteering GetSteering();
// Calculates distance along current path, using NPC's routecache if valid
final native function float GetRouteCacheDistance();
// < -1 = Already passed LocB, nearly zero = nearly parallel
native function float CalcClosestPointTime( Vector LocA, Vector VelocityA, Vector LocB, Vector VelocityB );
// Used in checking direct reachability to goal while pawn is moving along a path
native function SetDirectPathCheckTime();
/** Is my pawn currently performing a latent move? */
function bool IsDoingLatentMove()
{
return (InLatentExecution(LATENT_MOVETOWARD) || InLatentExecution(LATENT_MOVETO));
}
/*********************************************************************************************
* MOVEMENT AND PATHFINDING
********************************************************************************************* */
/** Forcibly stops any active latent movement and execution and stops accel/velocity */
event StopAllLatentMovement( optional bool bForced )
{
StopAllLatentMoveExecution();
AIZeroMovementVariables();
}
/** Abort any movement commands and reset MoveTarget */
function ClearMovementInfo( optional bool bSafeAbort, optional string DebugMsg )
{
`AILog( GetFuncName()$"() Aborting movement commands and setting MoveTarget to none", 'Command_MoveToGoal' );
AbortMovementCommands();
AbortMovementPlugIns();
MoveTarget = None;
}
/** Abort the main movement commands, might change to abort others like StepAside */
event AbortMovementCommands( optional bool bSafeAbort, optional string DebugMsg )
{
`AILog( GetFuncName()$"() called, aborting MoveToEnemy and MoveToGoal commands if they are active", 'Command_MoveToGoal' );
if( DebugMsg != "" )
{
FindCommandOfClass(class'AICommand_MoveToEnemy').UpdateHistoryString(DebugMsg);
FindCommandOfClass(class'AICommand_MoveToGoal').UpdateHistoryString(DebugMsg);
}
AbortCommand( None, class'AICommand_MoveToEnemy' );
AbortCommand( None, class'AICommand_MoveToGoal' );
}
/** Abort the main movement commands, might change to abort others like StepAside */
event AbortMovementPlugIns( optional bool bSafeAbort, optional string DebugMsg )
{
`AILog( GetFuncName()$"() called, aborting MovementPlugin and LeapPlugin Plug Ins if they are active", 'Movement_Plugins' );
if( DebugMsg != "" )
{
if( KfMovementPlugin != none )
{
KfMovementPlugin.UpdateHistoryString(DebugMsg);
}
if( KfLeapPlugin != none )
{
KfLeapPlugin.UpdateHistoryString(DebugMsg);
}
if( StuckFixPlugin != none )
{
StuckFixPlugin.UpdateHistoryString(DebugMsg);
}
}
if( KfMovementPlugin != none )
{
KfMovementPlugin.AbortMove(false);
}
if( KfLeapPlugin != none )
{
KfLeapPlugin.AbortMove(false);
}
if( StuckFixPlugin != none )
{
StuckFixPlugin.AbortMove(false);
}
}
/** Stops latent move, and zeroes pawn Acceleration and Velocity if the pawn's not falling (or if bForce is true) */
function AIZeroMovementVariables( optional bool bForce )
{
`AILog( WorldInfo.TimeSeconds$" "$GetFuncName()$" setting movetimer to -1", 'PathWarning' );
MoveTimer = -1.f; // Keep an eye on this
if( Pawn != none )
{
if( !bForce && Pawn.Physics == PHYS_Falling )
{
return;
}
Pawn.ZeroMovementVariables();
}
}
function StopMovement(optional EActionPriority ActionPriority = AP_Logic)
{
Super.StopMovement(ActionPriority);
if( KfWallWalkingPlugIn != none )
{
KfWallWalkingPlugIn.AbortMove(false);
}
}
/** Called by AICommands to notify that a move is about to start */
event StartingMovement()
{
`AILog( self$" StartingMovement", 'AIController' );
}
/** Called by AICommands to notify that a move has ended */
event StoppingMovement()
{
`AILog( self$" StoppingMovement", 'AIController' );
}
/** Called by AICommands to determine if NPC is allowed to move */
event bool AllowedToMove()
{
// Disallow for SM attacks since movement will be handled by root motion
return ( !IsDoingAttackSpecialMove() );
}
/** Called by movetogoal when we arrive at our destination */
event ReachedMoveGoal()
{
`AILog( self$" Reached MoveGoal!", 'AIController' );
}
/** Called by movetogoal when we arrive at an intermediate point in our path (pathnode location, etc.) */
function ReachedIntermediateMoveGoal()
{
`AILog( self$" ReachedIntermediateMoveGoal!", 'AIController' );
}
/** TODO: Needs to be deprecated or revised */
function bool MoveIsInterruptable(optional bool bForce)
{
return true;
}
/** Check if given goal is valid */
function bool MoveGoalIsValid( optional Actor Goal )
{
if( Goal == None )
{
Goal = MoveGoal;
}
return (Goal != None);
}
/** Check if given point is valid */
function bool MovePointIsValid( optional Vector Point )
{
if( Point == vect(0,0,0) )
{
Point = GetBasedPosition( MovePosition );
}
return (Point != vect(0,0,0));
}
/*********************************************************************************************
* PATH BUILDING
********************************************************************************************* */
/** Build a path to goal actor */
event Actor GeneratePathTo( Actor Goal, optional float Distance, optional bool bAllowPartialPath )
{
local actor PathResult;
AddBasePathConstraints();
`AILog( GetFuncName()$"() Goal: "$Goal$" optional distance: "$Distance$" bAllowPartialPath: "$bAllowPartialPath, 'AIController' );
class'Path_TowardGoal'.static.TowardGoal( Pawn, Goal );
if( bDisablePartialPaths )
{
bAllowPartialPath = false;
}
class'Goal_AtActor'.static.AtActor( Pawn, Goal, Distance, bAllowPartialPath );
PathResult = FindPathToward( Goal );
Pawn.ClearConstraints();
return PathResult;
}
event bool GenerateNavMeshPathTo( Actor Goal, optional float WithinDistance, optional bool bAllowPartialPath )
{
if( NavigationHandle == None )
{
return false;
}
NavigationHandle.ClearConstraints();
class'NavMeshPath_Toward'.static.TowardGoal( NavigationHandle, Goal );
class'NavMeshGoal_At'.static.AtActor( NavigationHandle, Goal, WithinDistance, bAllowPartialPath );
return NavigationHandle.FindPath();
}
event bool GenerateNavMeshPathToLocation( Vector Goal, optional float WithinDistance, optional bool bAllowPartialPath )
{
if( NavigationHandle == None )
{
return FALSE;
}
NavigationHandle.ClearConstraints();
// NavMeshPathConstraint
class'NavMeshPath_Toward'.static.TowardPoint( NavigationHandle, Goal );
// NavMeshPathGoalEvaluator
class'NavMeshGoal_At'.static.AtLocation( NavigationHandle, Goal, WithinDistance, bAllowPartialPath );
return NavigationHandle.FindPath();
}
/** Generates a path to goal which avoids passing through closed and locked doors */
event Actor GenerateDoorlessPathTo( Actor Goal, optional float Distance, optional bool bAllowPartialPath )
{
local actor PathResult;
local KFPawn CheckPawn;
CheckPawn = KFPawn(Goal);
AddBasePathConstraints();
class'Path_TowardGoal'.static.TowardGoal( Pawn, CheckPawn );
class'Path_AvoidClosedDoors'.static.AvoidClosedDoors( Pawn, true );
class'Goal_Null'.static.GoUntilBust( Pawn, 2024 );
//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' );
}
return PathResult;
}
/** Manually block a troublesome path connection, or specify a cost which won't explicitly block it */
native function bool AddBlockedReachSpec( ReachSpec BlockedSpec, INT BlockedCost );
static function ShowAdvancedRouteEdgeDebugInfo( NavigationHandle KFNavigationHandle2Use, optional bool bShowPathCachePolys = false )
{
//local array<KFNavMeshMovementStepData> debugPathData;
//local array<string> listOfEdgeTrips;
//local int i;
//local Vector textOffset;
//if( bShowPathCachePolys )
//{
// //FlushPersistentDebugLines();
// KFNavigationHandle2Use.DrawPathCache(,true);
//}
//KFNavigationHandle2Use.FillArrayWithAllCurrentNavMeshMovementStepData(debugPathData);
//KFNavigationHandle2Use.FillArrayWithAllCurrentPathEdgeTrippyDebugInfo( listOfEdgeTrips );
//for( i = 0; i < KFNavigationHandle2Use.PathCache.EdgeList.Length; i++ )
//{
// //typeOfEdge = listOfEdgeTypes[i];
// textOffset = Vect(0,0,1);
// textOffset *= 10.0f + 10*i;
// DrawDebugString( debugPathData[i].EdgeCenter + textOffset, string(i) @ listOfEdgeTrips[i] /*string(typeOfEdge)*/, , debugPathData[i].EdgeColor, default.TimeToShowEdgeTypeForNavMeshPathting);
// DrawDebugLine( debugPathData[i].EdgeCenter , debugPathData[i].EdgeCenter + textOffset , debugPathData[i].EdgeColor.R, debugPathData[i].EdgeColor.G, debugPathData[i].EdgeColor.B, true );
// DrawDebugLine( debugPathData[i].EdgeCenter + textOffset , debugPathData[i].Poly0Center + textOffset , debugPathData[i].EdgeColor.R, debugPathData[i].EdgeColor.G, debugPathData[i].EdgeColor.B, true );
// if( i != 0 )
// {
// DrawDebugLine( debugPathData[i].EdgeCenter + textOffset, debugPathData[i-1].EdgeCenter + textOffset , debugPathData[i].EdgeColor.R, debugPathData[i].EdgeColor.G, debugPathData[i].EdgeColor.B, true );
// }
//}
}
/*********************************************************************************************
* AICOMMANDS
********************************************************************************************* */
/** Pauses the AI for specified duration (seconds), optionally stops movement, aborts AI commands, and delays pause until
pawn is on the ground */
function DoPauseAI( float InDuration, optional bool bStopMovement, optional bool bAbortCommands, optional bool bWaitForLanding )
{
local vector OldVelocity;
local bool bWasFalling;
if( bStopMovement )
{
if( Pawn != none && Pawn.Physics == PHYS_Falling )
{
OldVelocity = Pawn.Velocity;
bWasFalling = true;
}
AbortMovementCommands();
AbortMovementPlugIns();
// Set our old velocity if we zeroed our movement while falling (probably from being pushed by weapon physics)
if( Pawn != none && bWasFalling )
{
Pawn.Velocity = OldVelocity;
}
}
`AILog( GetFuncName()$"() Init AICommand_Pause Duration: "$InDuration$" Active command: "$GetActiveCommand(), 'Command_Pause' );
class'AICommand_Pause'.static.Pause( self, InDuration, bStopMovement );
}
/** Starts wander AICommand, with optional goal to wander toward or away from, wander duration (-1 = none), and maximum distance to wander */
function DoWander( optional actor WanderGoal, optional float WanderDuration=-1.f, optional bool bWanderAwayFromGoal, optional float MaxWanderDist=10000.f )
{
class'AICommand_Wander'.static.BeginWander( self, WanderDuration, WanderGoal, bWanderAwayFromGoal, MaxWanderDist );
}
/** Begins Hide AICommand, hiding from HideFrom, with an optional duration to control when the command expires */
/*function DoHideFrom( actor HideFrom, optional float HideDuration, optional float HideDistance )
{
class'AICommand_Hide'.static.HideFrom( self, HideFrom, HideDuration, HideDistance );
`DialogManager.PlaySpotRunAwayDialog( MyKFPawn );
}*/
/* 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 )
{
class'AICommand_Flee'.static.FleeFrom( self, FleeFrom, FleeDuration, FleeDistance, bShouldStopAtGoal );
}
/*
* SetMoveGoal() - main function used to initiate NPC movement toward a goal actor
* @param NewMoveGoal
* @param NewMoveFocus Optional focus override while moving
* @param bInterruptable Whether or not this move can be interrupted
* @param OffsetDist Optional offset distance from move point
* @param bIsValidCache Whether routecache is currently valid
* @param bInCanPathfind When false, NPC will only move if goal is directly reachable
* @param bForce Forces the move regardless of interruptability
* @param bAllowedToAttack Whether or not NPC is permitted to attack during this move
* @param bAllowPartialPath Whether or not to allow a partial path
*/
event SetMoveGoal( Actor NewMoveGoal, optional Actor NewMoveFocus,
optional bool bInterruptable, optional float OffsetDist,
optional bool bIsValidCache, optional bool bInCanPathfind = true,
optional bool bForce, optional bool bAllowedToAttack=true,
optional bool bAllowPartialPath )
{
// Sandbox
if( bAlwaysAcceptPartialPaths && MyKFPawn.Physics != PHYS_Spider )
{
bAllowPartialPath = true;
}
bMoveGoalInterruptable = bInterruptable;
MoveGoal = NewMoveGoal;
SetBasedPosition( MovePosition, vect(0,0,0) );
MoveFocus = NewMoveFocus;
MoveOffset = OffsetDist;
`AILog( GetFuncName()$"() initializing AICommand_MoveToGoal"@NewMoveGoal@NewMoveFocus@bInterruptable@bAllowedToAttack, 'InitAICommand' );
if( NewMoveGoal != None && (MoveIsInterruptable(bForce) || !bInterruptable) )
{
class'AICommand_MoveToGoal'.static.MoveToGoal( self, NewMoveGoal, NewMoveFocus, OffsetDist, bIsValidCache, bInCanPathfind, bAllowedToAttack, bAllowPartialPath );
}
else if( NewMoveGoal != none )
{
`AILog( GetFuncName() @"!! -- ignoring movegoal because I already have a moveaction, which is non-interruptable, and the new movegoal IS interruptable.. trumped", 'InitAICommand' );
}
}
/**
* SetMovePoint() - main function used to initiate NPC movement to a location
* @param NewMovePoint Move to this location
* @param NewMoveFocus Optional focus override while moving
* @param bInterruptable Whether or not this move can be interrupted
* @param OffsetDist Optional offset distance from move point
* @param bIsValidCache Whether routecache is currently valid
* @param bAllowedToAttack Whether or not NPC is permitted to attack during this move
* @param bAllowPartialPath Whether or not to allow a partial path
*/
final event SetMovePoint( Vector NewMovePoint, optional Actor NewMoveFocus,
optional bool bInterruptable, optional float OffsetDist,
optional bool bIsValidCache, optional bool bAllowedToAttack=true,
optional bool bAllowPartialPath, optional bool bCanPathfind=true )
{
`AILog( GetFuncName()$"() initializing AICommand_MoveToGoal"$NewMovePoint@NewMoveFocus@bInterruptable@bAllowedToAttack, 'InitAICommand' );
bReachedMoveGoal = false;
bMoveGoalInterruptable = bInterruptable;
MoveGoal = None;
SetBasedPosition( MovePosition, NewMovePoint );
MoveFocus = NewMoveFocus;
MoveOffset = OffsetDist;
if( NewMovePoint != vect(0,0,0) )
{
class'AICommand_MoveToGoal'.static.MoveToPoint( self, NewMovePoint, NewMoveFocus, OffsetDist, bIsValidCache, bCanPathfind, bAllowedToAttack, bAllowPartialPath );
}
}
/**
* SetEnemyMoveGoal() - initiates movement toward NPC's current enemy.
* @param bCompleteMove When false, a partial move is acceptable
* @param GoalDistance Optional offset distance (UU) from enemy
* @param AbandonDistance Distance (UU) from enemy that will forcibly abort the command
* @param bAllowedToAttack Whether or not NPC is permitted to attack during this move
*/
final event SetEnemyMoveGoal( Object Observer,
optional bool bCompleteMove,
optional float GoalDistance=0.f,
optional float AbandonDistance=0.f,
optional bool bAllowedToAttack=true)
{
`AILog( GetFuncName()$"() initializing AICommand_MoveToEnemy - setting enemy goal Dist:"@GoalDistance@"bCompleteMove?"@bCompleteMove@"bAllowedToAttack?"@bAllowedToAttack, 'InitAICommand' );
PreMoveToEnemy();
if( bUseNavMesh && bUsePluginsForMovement )
{
//MoveToEnemyRequest(Observer, AP_Logic, bAllowedToAttack, GoalDistance);
}
else
{
class'AICommand_MoveToEnemy'.static.MoveToEnemy( self, bCompleteMove, GoalDistance, AbandonDistance, bAllowedToAttack, Enemy );
}
}
// ----------------------------------------------------------------------- //
// UBTTaskGoto methods
// ----------------------------------------------------------------------- //
/** Begin a strike attack */
function DoStrike();
function bool CanDoStrike();
/** 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 );
/** Triggers an evade from the PendingEvadeProjectile, if possible and it's still on target */
final function DoProjectileEvade()
{
local byte BestDir;
//local KFDebugLines KFDL;
//KFDL = class'KFDebugLines'.static.GetDebugLines();
MyKFPawn.SetHeadTrackTarget( none );
if( MyKFPawn != none && (MyKFPawn.CanDoSpecialMove(SM_Evade) || MyKFPawn.CanDoSpecialMove(SM_Evade_Fear)) && PendingEvadeProjectile != none && !PendingEvadeProjectile.bDeleteMe &&
!IsZero(PendingEvadeProjectile.Velocity) && CanEvade() )
{
BestDir = GetBestEvadeDir( PendingEvadeProjectile.Location, PendingEvadeProjectile.Instigator );
if( BestDir != DIR_None )
{
DoEvade( BestDir, PendingEvadeProjectile,, 0.1f + FRand() * 0.2f, true );
}
}
PendingEvadeProjectile = None;
}
/** Called just prior to setting MoveToEnemy */
function PreMoveToEnemy();
function DoEvade( byte EvadeDir, optional actor EvadeActor, optional vector DangerInstigatorLocation=vect(0,0,0), optional float Delay, optional bool bFrightened, optional bool bTurnToThreat )
{
`AILog( GetFuncName()@EvadeDir@Pawn.Physics@Pawn.Anchor, 'Command_Evade' );
class'AICommand_Evade'.static.Evade( self, EvadeDir, Delay, bFrightened,, DangerInstigatorLocation );
}
function DoStumble( vector Momentum, EHitZoneBodyPart HitZoneLimb )
{
local AICommand_Attack_Melee MeleeCommand;
// Don't stumble if we're in a panic wander state
if( GetActiveCommand() != none && AICommand_PanicWander(GetActiveCommand()) != none )
{
return;
}
if( CommandList != None && GetActiveCommand().IsA('AICommand_Attack_Melee') )
{
MeleeCommand = AICommand_Attack_Melee( GetActiveCommand() );
if( MeleeCommand != none )
{
`AILog( GetFuncName()$"() Aborting Melee Command", 'InitAICommand' );
AbortCommand( MeleeCommand );
}
}
`AILog( GetFuncName()$"() Aborting movement commands", 'Command_MoveToGoal' );
AbortMovementCommands();
AbortMovementPlugIns();
if( CommandList == None || !GetActiveCommand().IsA( 'AICommand_Stumble' ) )
{
`AILog( GetFuncName()$"() Init AICommand_Stumble", 'InitAICommand' );
class'AICommand_Stumble'.static.Stumble( self, Momentum, HitZoneLimb );
}
}
function DoHeadlessWander()
{
class'AICommand_HeadlessWander'.Static.HeadlessWander( self );
}
/** Launch the AICommand to send the KFAIController into the EMP Wander state */
function DoPanicWander()
{
class'AICommand_PanicWander'.Static.PanicWander( self );
}
/** Stop doing the AICommand with the AI in the EMP Wander state */
function EndPanicWander()
{
local AICommand_PanicWander CurCommand;
if( GetActiveCommand() != None )
{
CurCommand = AICommand_PanicWander(GetActiveCommand());
if( CurCommand != None )
{
CurCommand.EndWander(self);
}
}
}
event AttitudeStateChanged( name NewState ){}
/*********************************************************************************************
* StepAside
* StepAside is a potentially useful AICommand that hasn't gotten much use, but should work.
* It's similar to evading, only it's not animation-based and instead performs a latent move
* to move the NPC a few steps away in a safe direction (usually left/right).
*
* Call StepAsideFor() to start the command. ChkPawn is the pawn to step aside from. If ChkPawn
* is my own pawn, a random stepaside direction will be chosen. See GetStepAsideLocation()
* in AICommand_StepAside.
*
********************************************************************************************* */
/** StepAsideFor() - starts the StepAside AI Command */
event bool StepAsideFor( Pawn ChkPawn, Vector HitNormal )
{
local bool bResult, bStepAside, bDelayStep;
local KFAIController AI;
local bool bNoFocus;
`AILog(GetFuncName()@ChkPawn);
if( !bIgnoreStepAside && /*(MyKFPawn.Physics != PHYS_Spider &&*/ !MyKFPawn.IsDoingSpecialMove() )
{
// step aside for players
bResult = true;
bStepAside = true;
// if we're dealing with another AI
AI = KFAIController(ChkPawn.Controller);
if( AI != None && (AI.StepAsideGoal == Pawn) )
{
`AILog("- other AI is stepping aside for us already");
bResult = true;
bStepAside = false;
}
// If we are moving (11/7 changed Velocity is zero check to InLatentExecution check)
if( Pawn.IsSameTeam(ChkPawn) && InLatentExecution(LATENT_MOVETOWARD) && (Pawn.Velocity DOT ChkPawn.Velocity) > 0.f )
{
bDelayStep = ShouldDelayStepAside(ChkPawn);
if( !bDelayStep )
{
`AILog( self$" - moving in the same direction as pawn we bumped" );
bResult = true;
bStepAside = false;
}
}
if( bStepAside && StepAsideGoal != ChkPawn )
{
// note: don't need to abort existing instances because stepaside is set bReplaceActiveSameClassInstance=true
if( GetActiveCommand().IsA( 'AICommand_Debug' ) )
{
bNoFocus = AICommand_Debug( GetActiveCommand() ).bNoFocus;
}
bResult = class'AICommand_StepAside'.static.StepAside( self, ChkPawn, bDelayStep, bNoFocus );
}
}
return bResult;
}
/** Checks to see if a step-aside should be delayed because my pawn's moving in the same general
direction as (or already in front of) the pawn set to be stepped aside from */
function bool ShouldDelayStepAside( Pawn GoalPawn )
{
local float VelDotVel, GoalDotVel;
local Vector VectToGoal, VelDir;
if( !IsZero(Pawn.Velocity) && !IsZero(GoalPawn.Velocity) )
{
// If moving in the same direction as pawn we bumped
VelDir = Normal(Pawn.Velocity);
VelDotVel = Normal(GoalPawn.Velocity) DOT VelDir;
if( VelDotVel > 0.3 )
{
// If pawn we bumped is in front of us already
VectToGoal = Normal(GoalPawn.Location - Pawn.Location);
GoalDotVel = VectToGoal DOT VelDir;
if( GoalDotVel > 0 )
{
// Just delay movement
return true;
}
}
}
return false;
}
/** Alerts NPC that current move has failed */
event FailedMove( optional string Reason )
{
`AILog( "FailedMove! Reason: "$Reason, 'PathWarning' );
`RecordAIMoveFailure(self,Pawn.Location,Pawn.Rotation,MoveGoal,"4 "$Reason);
}
/** Optionally overrides movefocus for upcoming move toward enemy */
function Actor MoveToEnemy_GetMoveFocus()
{
if( Enemy != none )
{
return Enemy;
}
return None;
}
function float GetEstimatedGroundSpeedForMoveTimout( bool bDoingLeadinOutWalk )
{
local float GroundSpeed;
GroundSpeed = Pawn.GroundSpeed;
if( MyKFPawn != none )
{
GroundSpeed = MyKFPawn.NormalGroundSpeed;
}
return GroundSpeed;
}
function float GetMoveTimeOutDuration(vector dest, bool bDoingLeadInOutWalk)
{
local float dist;
local float GroundSpeed;
local float Duration;
if( Pawn == none )
{
return 5.f;
}
else
{
GroundSpeed = Pawn.GroundSpeed;
}
if( MyKFPawn != none )
{
GroundSpeed = MyKFPawn.NormalGroundSpeed;
}
Dist = VSize( Pawn.Location - Dest );
if( Pawn.bIsWalking || bDoingLeadInOutWalk )
{
GroundSpeed *= Pawn.WalkingPct;
}
Duration = FMax(0.5f,2.0f * (Dist / GroundSpeed));
`AILog( GetFuncName()$"() returning "$Duration$" for dist of "$Dist, 'Command_MoveToGoal' );
return Duration;
}
/** Determines if we can even consider sprinting */
function SetCanSprint( bool bNewSprintStatus )
{
bCanSprint = bNewSprintStatus;
if ( !bCanSprint || bSprintingDisabled )
{
MyKFPawn.bIsSprinting = false;
}
}
/** Determines if we can't at the moment */
function SetSprintingDisabled( bool bNewSprintStatus )
{
bSprintingDisabled = bNewSprintStatus;
if ( MyKFPawn != none && (!bCanSprint || bSprintingDisabled) )
{
MyKFPawn.bIsSprinting = false;
}
}
/** Determines if we can sprint when damaged */
function SetCanSprintWhenDamaged( bool bNewSprintDamagedStatus )
{
bCanSprintWhenDamaged = bNewSprintDamagedStatus;
if ( (!bCanSprint && !bCanSprintWhenDamaged) || bSprintingDisabled )
{
MyKFPawn.bIsSprinting = false;
}
}
function bool CanSetSprinting( bool bNewSprintStatus )
{
if ( bNewSprintStatus && ( (!bCanSprint && !bCanSprintWhenDamaged) || bSprintingDisabled) )
{
return false;
}
return true;
}
/** Timer function called during latent moves that determines whether NPC should sprint or stop sprinting. This is often
overridden in child classes */
function bool ShouldSprint()
{
local float DistToEnemy;
local name AttackTag;
if( MyKFPawn != none && MyKFPawn.IsAliveAndWell() && Enemy != none && Enemy.IsAliveAndWell() )
{
// Don't allow sprinting when blocking attacks
if( MyKFPawn.IsDoingSpecialMove(SM_Block) )
{
return false;
}
// Sprint if we've reached the frustration threshold
if ( IsFrustrated() )
{
return true;
}
if( SprintWithinEnemyRange.X > 0.f && SprintWithinEnemyRange.Y > 0.f )
{
DistToEnemy = VSize( Enemy.Location - Pawn.Location );
if( DistToEnemy > SprintWithinEnemyRange.X && DistToEnemy < SprintWithinEnemyRange.Y )
{
return true;
}
else
{
return false;
}
}
else
{
return false; // Old bug undiscovered until now (6.17.14), keep an eye on npcs no longer sprinting when they should
}
}
if( MyKFPawn == none || Enemy == none /*|| !bDirectMoveToGoal*/ )
{
return false;
}
if( Enemy.Velocity dot ( Pawn.Location - Enemy.Location ) < 0.f )
{
//if( !InMeleeRange( Enemy.Location ) )
if( !MyKFPawn.InAnyAttackTagRange(Enemy.Location, AttackTag) )
{
return true;
}
}
else if( !MyKFPawn.InAnyAttackTagRange(Enemy.Location, AttackTag) && VSize( Pawn.Location - Enemy.Location ) > 300.f )
{
return true;
}
if( VSize( Enemy.Location - Pawn.Location ) <= 512.f )
{
return false;
}
return false;
}
/**
* Update sprint settings based on Frustration
*/
function UpdateSprintFrustration( optional byte bForceFrustrationState=255 )
{
if( FrustrationThreshold > 0 )
{
if( bForceFrustrationState == 1 || (IsFrustrated() && bForceFrustrationState != 0) )
{
bCanSprint = true;
}
else
{
// can't use true default as it's modified by difficulty
bCanSprint = bDefaultCanSprint;
}
}
}
function bool IsFrustrated()
{
// Forced frustration, controlled by AI director
if( MyAIDirector.bForceFrustration )
{
return true;
}
// Forced frustration, controlled by individual AI
if( bForceFrustration )
{
return true;
}
if( FrustrationThreshold > 0 && MyKFGameInfo.MyKFGRI != None && MyKFGameInfo.MyKFGRI.AIRemaining <= FrustrationThreshold )
{
if( LastFrustrationCheckTime == 0 )
{
LastFrustrationCheckTime = WorldInfo.TimeSeconds;
}
if( `TimeSince(LastFrustrationCheckTime) >= FrustrationDelay )
{
return true;
}
}
return false;
}
/*---------------------------------------------------------
Pathfinding events and related notifications
---------------------------------------------------------*/
/** Notification from AICommand::Popped that it has completed */
function NotifyCommandFinished( AICommand FinishedCommand );
/** Notification from the flee command that the flee completed successfully (was not interrupted) */
function NotifyFleeFinished( optional bool bAcquireNewEnemy=true );
/** Notification from the move command that we've arrived at an intermediate destination */
function NotifyReachedLatentMoveGoal();
event NotifyLatentPostPhysWalking()
{
if( CachedAICommandList != none )
{
/** Let the current AICommand, if any, handle this. */
CachedAICommandList.NotifyLatentPostPhysWalking();
}
}
`if(`__TW_PATHFINDING_)
event NotifyFailMove( string Reason )
{
//`RecordAIMoveFailure(self,Pawn.Location,Pawn.Rotation,MoveGoal,"5 "$Reason);
// if( GetActiveKFCommand() != none )
// {
// //GetActiveKFCommand().UpdateHistoryString( "[NotifyFailMove: "$Reason$"]" );
// }
`AILog( "NotifyFailMove, Reason: "$Reason, 'PathWarning' );
}
`endif //__TW_PATHFINDING_
/** Notification from gameinfo that paths have changed */
event NotifyPathChanged()
{
`AILog( GetFuncName()$"() Command: "$GetActiveCommand(), 'PathWarning' );
}
/** Returns true if the actor is currently directly reachable */
function bool IsValidDirectMoveGoal( Actor A )
{
// if( Pawn(A) != none && !FastTrace(A.Location, Pawn.Location, Pawn.GetCollisionExtent()) )
// {
// return false;
// }
if( ActorReachable(A) )
{
return true;
}
return false;
}
final function SetFailedPathToEnemy( Pawn TestEnemy, optional float Offset )
{
`RecordAIMoveFailure(self,Pawn.Location,Pawn.Rotation,MoveGoal,"7 SetFailedpathToEnemy");
`AILog( GetFuncName()$"() TestEnemy: "$TestEnemy$" Offset: "$Offset, 'PathWarning' );
// Below might come in handy, but haven't tested it enough.
// if( Pawn.Anchor != none )
// {
// `AILog( GetFuncName()$"() calling InvalidateAnchor (Anchor: "$Pawn.Anchor$")", 'PathWarning' );
// //InvalidateAnchor( Pawn.Anchor );
// }
}
/** Used by pathfinding code to allow NPC to detour to special navigation points (pickups, etc.) */
event bool AllowDetourTo( NavigationPoint N )
{
return true;
}
/** Forces NPC to re-evaluate in-progress move, to see if the goal is now directly reachable, otherwise
the pawn will need pathfind to the goal. */
event FindDirectPath()
{
if( CachedAICommandList != none )
{
/** Let the current AICommand, if any, handle this. */
CachedAICommandList.FindDirectPath();
}
SetDirectPathCheckTime();
}
/** Alert from movement code that path is blocked by an actor which has its bBlocksNavigation flag set to true.
Note that if ShouldIgnoreNavigationBlockingFor() returns true for BlockedBy, this event will not be called.
See UReachSpec::PrepareForMove() */
event bool HandlePathObstruction( Actor BlockedBy )
{
// local Pawn BlockPawn;
local bool bResult;
if( LastObstructionTime == 0.f || `TimeSince(LastObstructionTime) > 0.7f )
{
LastObstructionTime = WorldInfo.TimeSeconds;
`AILog( GetFuncName()@BlockedBy@`TimeSince(LastObstructionTime), 'HandlePathObstruction' );
// Let the current AICommand deal with the obstruction.
if( CachedAICommandList != None )
{
bResult = CommandList.HandlePathObstruction( BlockedBy );
}
}
return bResult;
}
function bool HandleZedBlockedPath();
event FailMove( string Reason )
{
//`RecordAIMoveFailure(self,Pawn.Location,Pawn.Rotation,MoveGoal,Reason);
`AILog( GetFuncName()$"() REASON: "$Reason, 'PathWarning' );
}
/** Event from latent movement code notifying that current move goal is not reachable */
event MoveUnreachable( Vector AttemptedDest, Actor AttemptedTarget )
{
`AILog( GetFuncName()$"()"$AttemptedDest@AttemptedTarget, 'PathWarning' );
if( CachedAICommandList != None )
{
CachedAICommandList.MoveUnreachable( AttemptedDest, AttemptedTarget );
}
if( KfMovementPlugin != none )
{
KfMovementPlugin.MoveUnreachable( AttemptedDest, AttemptedTarget );
}
}
/** Will cause AI to briefly pause and sets ReevaluatePath to true so if the AI is moving it will re-path */
event ForcePauseAndRepath( optional Actor InInstigator )
{
//`Log( GetFuncName()@InInstigator );
bReevaluatePath = true;
NotifyNeedRepath();
class'AICommand_Pause'.static.Pause( self, 0.25f + FRand() );
}
/** Notification that any existing pathing should be checked and rebuilt */
function NotifyNeedRepath()
{
`AILog( GetFuncName()$"()", 'PathWarning' );
if( CachedAICommandList != None )
{
CachedAICommandList.NotifyNeedRepath();
}
}
/*---------------------------------------------------------
Anchor handling
---------------------------------------------------------*/
/** Invalidates pawn's current anchor, making it unusable by pawn for a brief duration and sets current
anchor to none, forcing the pawn to find a new valid anchor on next pathfind attempt (with the
expectation that the newly invalidated anchor will be skipped since it's in the invalid anchor list */
final function InvalidateAnchor( NavigationPoint Nav )
{
local int Idx;
Idx = InvalidAnchorList.Find( 'InvalidNav', Nav );
`AILog( GetFuncName()@Nav@Idx, 'PathWarning' );
if( Idx < 0 )
{
Idx = InvalidAnchorList.Length;
InvalidAnchorList.Length = Idx + 1;
InvalidAnchorList[Idx].InvalidNav = Nav;
InvalidAnchorList[Idx].InvalidTime = WorldInfo.TimeSeconds;
`AILog( GetFuncName()$" Invalidating anchor ("$Nav$") - setting anchor to NONE", 'PathWarning' );
// Clear the old anchor so we need to get a new one
Pawn.SetAnchor( none );
}
}
final function NavigationPoint GetFallbackAnchor()
{
local NavigationPoint LastAnchor, ResultAnchor;
local float AnchorDist;
`AILog( GetFuncName()$"() trying to get fallback anchor", 'PathWarning' );
// block the current anchor if possible to prevent it from being reacquired as an anchor
if (Pawn.Anchor != None && !Pawn.Anchor.bBlocked)
{
`AILog( GetFuncName()$"() setting Pawn anchor ("$Pawn.Anchor$") to bBlocked to true!", 'PathWarning' );
LastAnchor = Pawn.Anchor;
LastAnchor.bBlocked = true;
}
// look for the nearest anchor
ResultAnchor = Pawn.GetBestAnchor( Pawn,Pawn.Location, true, true, AnchorDist );
// unblock the last anchor
if (LastAnchor != None)
{
LastAnchor.bBlocked = false;
if (LastAnchor == ResultAnchor)
{
`AILog( "ERROR! LastAnchor == ResultAnchor", 'PathWarning' );
}
}
if (ResultAnchor == None)
{
FailedToFindFallbackAnchor();
}
return ResultAnchor;
}
final function FailedToFindFallbackAnchor()
{
`AILog( self$" FailedToFindFallbackAnchor!", 'PathWarning' );
// Haven't actually gotten a pawn yet
if( Pawn == None )
return;
}
/** Returns true if Nav is found in the RouteCache (i.e, is it part of NPC's current path) */
function bool IsNavInRouteCache( NavigationPoint Nav )
{
local int i;
for( i = 0; i < RouteCache.Length; i++ )
{
if( RouteCache[i] == Nav )
{
return true;
}
}
return false;
}
/**
* Event called when specific navigation points (with bProbeNotifyOnAddToRouteCache true) are
* added to this controller's route cache.
*/
event NotifyOnAddToRouteCache( NavigationPoint Nav ) {}
/*********************************************************************************************
* FALLING, PHYS_FALLING
********************************************************************************************* */
// Called when NPC reaches a ledge, setting bCanJump to true here will allow NPC to continue move despite pending fall
event MayFall( bool bFloor, vector FloorNormal )
{
`AILog( "MayFall Event! bFloor: "$bFloor$" Phys: "$Pawn.Physics$" MoveTarget: "$MoveTarget$" Reachable: "$ActorReachable(Enemy), 'Falling' );
if( !bPlannedJump )
{
//msg( "MayFall Event! bFloor: "$bFloor$" Phys: "$Pawn.Physics$" MoveTarget: "$MoveTarget$" Reachable: "$ActorReachable(Enemy) );
Pawn.JumpZ = 100.f;
}
else
{
Pawn.JumpZ = Pawn.Default.JumpZ;
}
Pawn.bCanJump = true;
}
/** NPC has attempted to jump over an obstruction after bumping into it */
event JumpedOverWall( vector WallHitNormal, optional actor Wall )
{
`AiLog(GetFuncName() @ " WallHitNormal: " @ WallHitNormal @ " Wall: " @Wall );
/* Below untested - enable to lower jump over wall frequency */
//MyKFPawn.bCanJumpOverWalls = false;
//SetTimer( 2.f * FRand() + 0.25f, false, nameof(Timer_EnableJumpOverWalls), self );
`RecordAIWall( `StatID(AI_JUMPOVERWALL), self, Pawn.Location, Pawn.Rotation, Wall, "HN: "$string(WallHitNormal) );
}
/** Re-Enables pawn's ability to jump over obstructions */
function Timer_EnableJumpOverWalls()
{
MyKFPawn.bCanJumpOverWalls = true;
}
function Vector GetDropEdgeLeapVelocity()
{
return DropEdgeLeapVelocity;// Vect( 200.0f,0.0f, 20.0f );
}
/*********************************************************************************************
* Sight/Hearing/Bump
********************************************************************************************* */
function RecordLeapToWall()
{
//`RecordAILeapToWall( self, Pawn.Location, Pawn.Rotation, "Base: "$Pawn.Base );
}
function RecordHitWall( actor Wall )
{
//`RecordAIHitWall( self, Pawn.Location, Pawn.Rotation, Wall, "SS: "$MyKFPawn.IsUsingSuperSpeed()$" WallGeo:"$Wall.bWorldGeometry$" Crawlable:"$Wall.bCrawlable );
}
function bool NotifyBaseChange( actor NewBase, vector NewFloor )
{
if( CachedAICommandList != none )// && Enemy == Seen )
{
if( CachedAICommandList.NotifyBaseChange( NewBase, NewFloor ) )
{
return true;
}
}
return false;
}
/** Notification from ProcessLanded() that pawn has stopped falling */
event bool NotifyLanded( vector HitNormal, Actor FloorActor )
{
`AILog( GetFuncName() $ " - HitNormal: " $ HitNormal $ " - FloorActor: " $ FloorActor, 'LandedEvent' );
if( MyKFPawn != none )
{
MyKFPawn.BaseEyeHeight = MyKFPawn.default.BaseEyeHeight;
// Reset JumpZ (might not be necessary anymore)
Pawn.JumpZ = Pawn.Default.JumpZ;
// If movement AI is not active, reset acceleration to zero to prevent sliding
if( bPreparingMove )
{
Pawn.Acceleration = vect(0,0,0);
}
}
bPlannedJump = false;
// Reset NotifyJumpApex event notification, turned off by PHYS_Falling
bNotifyApex = Default.bNotifyApex;
if( CachedAICommandList != none )// && Enemy == Seen )
{
//`AILog( "NotifyLanded: "$FloorActor$" - while I'm moving to goal, notifying "$CommandList$" and letting it handle this event", 'NotifyLanded' );
return CachedAICommandList.NotifyLanded( HitNormal, FloorActor );
}
return super.NotifyLanded( HitNormal, FloorActor );
}
/** Notification that my current enemy is surrounded by other Zeds */
event bool EnemyIsSurrounded()
{
`AILog( "EnemyIsSurrounded! ("$Enemy$")", 'EnemyStatus' );
if( CachedAICommandList != none )// && Enemy == Seen )
{
//`AILog( "EnemyIsSurrounded", 'Command_MoveToEnemy' );
return CachedAICommandList.EnemyIsSurrounded();
}
return false;
}
/** Notification that a Husk is going to commit suicide, providing chance to move away */
function bool NotifyHuskSuicide( KFPawn_Monster Husk )
{
return false; // Temp disabled
if( CachedAICommandList != none )// && Enemy == Seen )
{
if( CachedAICommandList.NotifyHuskSuicide( Husk ) )
{
return true;
}
}
if( !IgnoreNotifies() )
{
// class'AICommand_Hide'.static.HideFrom( self, Husk,, false );
// class'AICommand_Scatter'.static.ScatterFrom( self, Husk,, true );
}
return false;
}
/*********************************************************************************************
* HitWall Handling
* NotifyHitWall is enabled by default. Note that if the pawn's bDirectHitWall flag is true,
* this will be bypassed and the pawn's HitWall event will be called directly.
********************************************************************************************* */
/** Timer which re-enables SeePlayer event polling */
final function EnableNotifyHitWall()
{
`AILog( GetFuncName()$"() Enabling NotifyHitWall event", 'HitWall' );
Enable( 'NotifyHitWall' );
}
/** Disable this NPC's SeePlayer polling for DisabledTime seconds */
final function DisableNotifyHitWall( optional float DisabledTime=0.2f )
{
//`AILog( GetFuncName()$"() disabling NotifyHitWall for "$DisabledTime$" seconds", 'HitWall' );
Disable( 'NotifyHitWall' );
if( DisabledTime > 0.f )
{
SetTimer( DisabledTime, false, nameof(EnableNotifyHitWall) );
}
}
/** Notification that I've run into a wall. If the pawn's bDirectHitWall flag is true, this will be bypassed and
the pawn's HitWall() event will be called directly. */
event bool NotifyHitWall( vector HitNormal, actor Wall )
{
local KFDestructibleActor KFDA;
local bool bEmerging;
LastWallHitNormal = HitNormal;
LastHitWall = Wall;
LastNotifyHitWallTime = WorldInfo.TimeSeconds;
`AiLog( GetFuncName() @ " Wall: " @ Wall @ " HitNormal: " @ HitNormal );
if( MyKFPawn != none && MyKFPawn.IsDoingSpecialMove(SM_Emerge) )
{
bEmerging = true;
}
if( !Wall.bStatic && Wall.bCollideActors && Wall.bCanBeDamaged )
{
KFDA = KFDestructibleActor( Wall );
if( KFDA != none )
{
MyKFPawn.HandleDestructibleBump( KFDA, HitNormal );
if( bEmerging )
{
return true;
}
if( ActorEnemy != KFDA
&& KFDA.bBlockActors
&& !MyKFPawn.IsDoingSpecialMove()
&& (LastDestructibleBumpTime == 0.f || `TimeSince(LastDestructibleBumpTime) > 1.f)
&& CanAttackDestructibles()
&& KFDA.HasAnyHealth()
&& !GetActiveCommand().IsA('AICommand_Melee') )
{
// FOV test
if( vector(MyKFPawn.Rotation) dot Normal(KFDA.Location - MyKFPawn.Location) >= 0.67f )
{
AIZeroMovementVariables();
NotifyAttackActor( Wall );
}
}
LastDestructibleBumpTime = WorldInfo.TimeSeconds;
}
}
/** I shouldn't be colliding if I'm emerging, so ignore notification but return true to prevent move adjusting in native code */
if( bEmerging )
{
return true;
}
/** Return whatever the current AICommand wants me to */
if( CachedAICommandList != none )
{
return CachedAICommandList.NotifyHitWall( HitNormal, Wall );
}
else
{
DisableNotifyHitWall(0.2f);
}
return Super.NotifyHitWall( HitNormal, Wall );
}
/** Pawn has bumped into a wall while falling - unusual given KF2 NPCs have very little air control */
event NotifyFallingHitWall( vector HitNormal, actor Wall )
{
if( CachedAICommandList != none )
{
`AILog( "NotifyFallingHitWall: "$Wall$" - while I'm moving to goal, notifying "$CommandList$" and letting it handle this event", 'HitWall' );
CachedAICommandList.NotifyFallingHitWall( HitNormal, Wall );
}
}
/*********************************************************************************************
* SEEPLAYER
* KF2 NPCs do not heavily rely on SeePlayer() and EnemyNotVisible(), since the NPCs are
* omniscient when it comes to choosing an enemy. If the NPCs were to begin in a state where
* they randomly wander about, they would then need to rely more on these events.
* Currently they are used for some miscellaneous things, like using SeePlayer() when
* attacking a door and using that notification to stop attacking the door and target the
* newly spotted enemy instead.
********************************************************************************************* */
/** Timer which re-enables SeePlayer event polling */
final event EnableSeePlayer()
{
`AILog( GetFuncName()$"() Enabling SeePlayer event", 'SeePlayer' );
if( !bHasDebugCommand )
{
Enable( 'SeePlayer' );
}
}
/** Disable this NPC's SeePlayer polling for DisabledTime seconds */
final event DisableSeePlayer( optional float DisabledTime )
{
`AILog( GetFuncName()$"() disabling SeePlayer polling for "$DisabledTime$" seconds", 'SeePlayer' );
Disable( 'SeePlayer' );
if( DisabledTime > 0.f )
{
SetTimer( DisabledTime, false, nameof(EnableSeePlayer) );
}
}
/** Timer which re-enables SeePlayer event polling */
final event EnableEnemyNotVisible()
{
`AILog( GetFuncName()$"() Enabling EnemyNotVisible event", 'EnemyNotVisible' );
Enable( 'EnemyNotVisible' );
}
/** Disable this NPC's SeePlayer polling for DisabledTime seconds */
final event DisableEnemyNotVisible( optional float DisabledTime=2.f )
{
`AILog( GetFuncName()$"() disabling EnemyNotVisible polling for "$DisabledTime$" seconds", 'EnemyNotVisible' );
Disable( 'EnemyNotVisible' );
if( DisabledTime > 0.f )
{
SetTimer( DisabledTime, false, nameof(EnableEnemyNotVisible) );
}
}
function bool IsPawnVisibleViaTrace( Pawn PawnToCheck, optional bool bUsePawnRotation )
{
local Vector TestLocation;
local Rotator Rot;
Rot = Pawn.Rotation;
TestLocation = PawnToCheck.GetPawnViewLocation();
if( !bUsePawnRotation )
{
Rot = Rotator( PawnToCheck.Location - Pawn.Location );
}
return CanSeeByPoints( Pawn.Location, TestLocation, Rot );
}
/*********************************************************************************************
* HEARING
* Use MakeNoise() to simulate noise for NPCs to "hear."
********************************************************************************************* */
/** Timer which re-enables HearNoise event polling. */
final event EnableHearNoise()
{
Enable( 'HearNoise' );
`AILog( GetFuncName()$"() Enabled HearNoise event", 'HearNoise' );
}
/** Disable this NPC's hearing for DisabledTime seconds */
final event DisableHearNoise( optional float DisabledTime=1.f )
{
if( DisabledTime <= 0.f )
{
DisabledTime = 1.f + FRand();
}
`AILog( GetFuncName()$"() disabling noise hearing for "$DisabledTime$" seconds", 'HearNoise' );
Disable( 'HearNoise' );
SetTimer( DisabledTime, false, nameof(EnableHearNoise) );
}
/*********************************************************************************************
* BUMP HANDLING
********************************************************************************************* */
/** Records bump related gameplay event */
function RecordBump( actor Other )
{
`RecordAIBump( `StatID(AI_BUMP), self, Pawn.Location, Pawn.Rotation, Other, "SSpeed: "$MyKFPawn.IsUsingSuperSpeed() );
}
/** Timer which re-enables Bump event polling */
final event EnableBump()
{
`AILog( GetFuncName()$" Enabling Bump Events" , 'BumpEvent' );
Enable( 'NotifyBump' );
}
/** Disable this NPC's Bump event polling for DisabledTime seconds */
final event DisableBump( optional float DisabledTime=0.5f )
{
DisabledTime = FMax(0.1f,DisabledTime);
`AILog( GetFuncName()$"() disabling Bump polling for "$DisabledTime$" seconds", 'BumpEvent' );
Disable( 'NotifyBump' );
if( DisabledTime > 0.f )
{
SetTimer( DisabledTime, false, nameof(EnableBump) );
}
}
simulated event Touch(Actor Other, PrimitiveComponent OtherComp, vector HitLocation, vector HitNormal)
{
`AILog( GetFuncName()$" - Other: "$Other$" OtherComp"$OtherComp$" - HitLocation: " $ HitLocation $ " - HitNormal: " $ HitNormal, 'TouchEvent' );
if( CachedAICommandList != none )
{
CachedAICommandList.NotifyTouch( Other, OtherComp, HitLocation, HitNormal);
}
if( KfMovementPlugin != none )
{
//KfMovementPlugin.NotifyTouch( Other, OtherComp, HitLocation, HitNormal);
}
Super.Touch(Other, OtherComp, HitLocation, HitNormal);
}
/** Override is currently only used if bSpecialBumpHandling is true in .ini file */
simulated function Tick( FLOAT DeltaTime )
{
Super.Tick(DeltaTime);
if (GetIsInZedVictoryState())
{
return;
}
if( bSpecialBumpHandling && Role == ROLE_Authority && MyKFPawn != None && MyKFPawn.Health >= 0 && !MyKFPawn.IsDoingSpecialMove() )
{
SpecialBumpHandling(DeltaTime);
}
// Regularly check to see if this zed is stuck
if( Role == ROLE_Authority && MyKFPawn != none && MyKFPawn.Health > 0 && ((`TimeSince(LastStuckCheckTime) > StuckCheckInterval
&& !MyKFPawn.IsDoingSpecialMove()) || (MyKFPawn.Physics == PHYS_Falling && MyKFPawn.Velocity.Z == 0)) )
{
EvaluateStuckPossibility(DeltaTime);
}
// Regularly check to see if this AI can teleport closer to the enemy
if( bCanTeleportCloser && PendingDoor == none && Role == ROLE_Authority && MyKFPawn != none && MyKFGameInfo.MyKFGRI != None
&& MyKFPawn.Health > 0 && `TimeSince(LastTeleportCheckTime) > TeleportCheckInterval && !MyKFPawn.IsDoingSpecialMove()
&& MyKFGameInfo.MyKFGRI.AIRemaining >= AIRemainingTeleportThreshold )
{
EvaluateTeleportPossibility(DeltaTime);
}
}
function EvaluateTeleportPossibility(float DeltaTime)
{
LastTeleportCheckTime = WorldInfo.TimeSeconds;
// Teleport if we have an enemy and we're superspeeding and we haven't teleported in a while
if( Enemy != none && MyKFPawn != none && MyKFPawn.IsUsingSuperSpeed() )
{
if( `TimeSince(MyKFPawn.LastLOSOrRelevantTime) > HiddenRelocateTeleportThreshold
&& `TimeSince(LastTeleportTime) > TeleportCooldown
&& `TimeSince(MyKFPawn.SpawnTime) > PostSpawnRelocateTeleportCooldown)
{
RelocateTeleport();
}
}
}
/** Evaluate if this zed might be getting stuck, and then call functions to handle it */
function EvaluateStuckPossibility(float DeltaTime)
{
local float DistToEnemySquared;
local vector OldStuckCheckLocation;
local bool bSeenRecently;
local int i;
LastStuckCheckTime = WorldInfo.TimeSeconds;
OldStuckCheckLocation = LastStuckCheckLocation;
LastStuckCheckLocation = Pawn.Location;
// Special big hack here. This fixes the bug where zeds would get stuck
// floating in air unable to move. Someday perhaps we'll track down the
// source of that issue and won't need this code - Ramm
if( MyKFPawn.Physics == PHYS_Falling && MyKFPawn.Velocity.Z == 0 )
{
//`log(self@GetFuncName()$" falling with no Z velocity!!!");
FallingStuckNoZVelocityTime += DeltaTime;
MyKFPawn.Velocity = MyKFPawn.GroundSpeed * Normal(GetDestinationPosition() - MyKFPawn.Location);
MyKFPawn.Acceleration = MyKFPawn.AccelRate * Normal(GetDestinationPosition() - MyKFPawn.Location);
// Continue on and try and get unstuck if this time is greather than the
// StuckCheckInterval, otherwise just return out and see if the hack will
// get us unstuck!
if( FallingStuckNoZVelocityTime < StuckCheckInterval )
{
return;
}
}
else
{
FallingStuckNoZVelocityTime = 0;
}
if( `TimeSince(LastDamageTime_Taken) < 5.0 )
{
StuckPossiblity = 0;
bTryingToGetUnstuck = false;
return;
}
// Don't check stuck if we recently melee'd
if( `TimeSince(LastAttackTime_Melee) < 5.0 )
{
StuckPossiblity=0;
bTryingToGetUnstuck=false;
return;
}
// Don't check stuck if we just teleported
if( `TimeSince(LastTeleportTime) < 5.0 )
{
StuckPossiblity=0;
bTryingToGetUnstuck=false;
return;
}
if( `TimeSince(LastSpecialMoveEndTime) < 5.0 )
{
StuckPossiblity=0;
bTryingToGetUnstuck=false;
return;
}
bSeenRecently = !MyKFPawn.IsUsingSuperSpeed();
// Don't do any stuck checking if we're close to an enemy
if( Enemy != none )
{
DistToEnemySquared = VSizeSq(Enemy.Location - Pawn.Location);
// Modified to only allow it to bypass the rest of the stuck checks for 5 seconds
// if we are down to only a few zeds. Would never run HandleStuck() on stuck zeds
// if any players were too close to them.
if( DistToEnemySquared < StuckCheckEnemyDistThreshholdSquared )
{
if( MyKFGameInfo.MyKFGRI.AIRemaining > 5 || TotalStuckCheckCloseRangeTime < 5.0f )
{
if( LastStuckCheckCloseRangeTime > 0.f )
{
TotalStuckCheckCloseRangeTime += `TimeSince(LastStuckCheckCloseRangeTime);
}
LastStuckCheckCloseRangeTime = WorldInfo.TimeSeconds;
StuckPossiblity=0;
bTryingToGetUnstuck=false;
return;
}
}
else
{
TotalStuckCheckCloseRangeTime = 0.f;
LastStuckCheckCloseRangeTime = 0.f;
}
}
// Don't check stuck if we're line of site pathing to the enemy
if( Enemy != none && MoveTarget == Enemy && RouteCache.Length <= 0 )
{
//`log(self@GetFuncName()$"I'm on final approach to enemy, just keep going");
StuckPossiblity=0;
bTryingToGetUnstuck=false;
return;
}
// Don't check stuck if we're waiting outside a welded door while it's being beaten down
for ( i=0; i<RouteCache.Length; i++ )
{
if ( RouteCache[i] != None )
{
if( KFDoorMarker(RouteCache[i]) != none && KFDoorMarker(RouteCache[i]).MyKFDoor.WeldedShut()
&& VSizeSq2d(RouteCache[i].Location - Pawn.Location) < 1000000 ) // 10 Meters
{
//`log(self@GetFuncName()$"I'm trying to pass through a welded door. Just chill here");
StuckPossiblity=0;
bTryingToGetUnstuck=false;
return;
}
}
}
// See if the zed isn't moving around in the XY plane while walking
if( MyKFPawn.Physics != PHYS_Falling && VSizeSq2d(MyKFPawn.Velocity) < StuckVelocityThreshholdSquared )
{
if( !IsZero(OldStuckCheckLocation) )
{
//`log(self$" XY Move since last check: "$VSizeSq2d(Pawn.Location - OldStuckCheckLocation)$" XYMoveStuckThresholdSquared "$XYMoveStuckThresholdSquared);
if( VSizeSq2d(Pawn.Location - OldStuckCheckLocation) > XYMoveStuckThresholdSquared )
{
StuckPossiblity=0;
bTryingToGetUnstuck=false;
return;
}
}
else
{
StuckPossiblity=0;
bTryingToGetUnstuck=false;
return;
}
StuckPossiblity += 0.5;
if( StuckPossiblity > StuckPossiblityThreshhold )
{
//`log(self$" thinks he's stuck!!!!!!!!!!!!!!!!! StuckPossiblity: "$StuckPossiblity$" 2d Velocity "$VSize2d(MyKFPawn.Velocity)$" Physics: "$Pawn.GetPhysicsName());
//`log(self$" MoveTarget: "$GetItemName(String(MoveTarget))$" MoveTimer: "$MoveTimer$" RouteGoal: "$GetItemName(string(RouteGoal))$" RouteDist: "$RouteDist$" bPreparingMove: "$bPreparingMove);
StuckPossiblity=0;
HandleStuck();
DumpCommandStack();
DumpCommandHistory();
}
else
{
//`log(self$" thinks he's getting stuck!!!!!!!!!!!!!!!!! StuckPossiblity: "$StuckPossiblity$" 2d Velocity "$VSize2d(MyKFPawn.Velocity)$" Physics: "$Pawn.GetPhysicsName());
//`log(self$" MoveTarget: "$GetItemName(String(MoveTarget))$" MoveTimer: "$MoveTimer$" RouteGoal: "$GetItemName(string(RouteGoal))$" RouteDist: "$RouteDist$" bPreparingMove: "$bPreparingMove);
}
}
// See if the zed is stuck while falling
else if( MyKFPawn.Physics == PHYS_Falling )
{
// Check if jumping up and down in the same place
if( MyKFPawn.Velocity.Z != 0 )
{
if( !IsZero(OldStuckCheckLocation) )
{
//`log(self$" XY Move since last check: "$VSizeSq2d(Pawn.Location - OldStuckCheckLocation)$" XYMoveStuckThresholdSquared "$XYMoveStuckThresholdSquared);
if( VSizeSq2d(Pawn.Location - OldStuckCheckLocation) < XYMoveStuckThresholdSquared )
{
if( bSeenRecently )
{
StuckPossiblity += 1.0;
}
else
{
StuckPossiblity += 0.5;
}
if( StuckPossiblity > StuckPossiblityThreshhold )
{
//`log(self$" is falling stuck because of XY!!!!!!!!!!!!!!!!! StuckPossiblity: "$StuckPossiblity$" Falling Velocity "$MyKFPawn.Velocity.Z$" Physics: "$Pawn.GetPhysicsName());
//`log(self$" MoveTarget: "$GetItemName(String(MoveTarget))$" MoveTimer: "$MoveTimer$" RouteGoal: "$GetItemName(string(RouteGoal))$" RouteDist: "$RouteDist$" bPreparingMove: "$bPreparingMove);
StuckPossiblity=0;
HandleStuck();
}
else
{
//`log(self$" thinks he's falling stuck because of XY!!!!!!!!!!!!!!!!! StuckPossiblity: "$StuckPossiblity$" Falling Velocity "$MyKFPawn.Velocity.Z$" Physics: "$Pawn.GetPhysicsName());
//`log(self$" MoveTarget: "$GetItemName(String(MoveTarget))$" MoveTimer: "$MoveTimer$" RouteGoal: "$GetItemName(string(RouteGoal))$" RouteDist: "$RouteDist$" bPreparingMove: "$bPreparingMove);
}
}
}
else
{
StuckPossiblity=0;
bTryingToGetUnstuck=false;
return;
}
}
// Falling, but no velocity so possibly stuck in place
else
{
if( bSeenRecently )
{
StuckPossiblity += 2.0;
}
else
{
StuckPossiblity += 1.0;
}
if( StuckPossiblity > StuckPossiblityThreshhold )
{
//`log(self$" is falling stuck!!!!!!!!!!!!!!!!! StuckPossiblity: "$StuckPossiblity$" Falling Velocity "$MyKFPawn.Velocity.Z$" Physics: "$Pawn.GetPhysicsName());
//`log(self$" MoveTarget: "$GetItemName(String(MoveTarget))$" MoveTimer: "$MoveTimer$" RouteGoal: "$GetItemName(string(RouteGoal))$" RouteDist: "$RouteDist$" bPreparingMove: "$bPreparingMove);
StuckPossiblity=0;
HandleStuck();
}
else
{
//`log(self$" thinks he's falling stuck!!!!!!!!!!!!!!!!! StuckPossiblity: "$StuckPossiblity$" Falling Velocity "$MyKFPawn.Velocity.Z$" Physics: "$Pawn.GetPhysicsName());
//`log(self$" MoveTarget: "$GetItemName(String(MoveTarget))$" MoveTimer: "$MoveTimer$" RouteGoal: "$GetItemName(string(RouteGoal))$" RouteDist: "$RouteDist$" bPreparingMove: "$bPreparingMove);
}
}
}
else
{
StuckPossiblity=0;
bTryingToGetUnstuck=false;
}
}
/** Handle a zed that thinks it's stuck. Teleport it to a spawn volume, or handle it some other way */
function HandleStuck()
{
local bool bSeenRecently;
if( !bTryingToGetUnstuck )
{
LastStuckTime = WorldInfo.TimeSeconds;
bTryingToGetUnstuck = true;
}
DumpCommandStack();
DumpCommandHistory();
bSeenRecently = !MyKFPawn.IsUsingSuperSpeed();
//`log(self@GetFuncName()$" bSeenRecently = "$bSeenRecently$" `TimeSince(LastLOSOrRelevantTime): "$`TimeSince(MyKFPawn.LastLOSOrRelevantTime)$" TimeSeconds: "$WorldInfo.TimeSeconds$" LastLOSOrRelevantTime: "$MyKFPawn.LastLOSOrRelevantTime);
if( bSeenRecently )
{
if( AmIAllowedToSuicideWhenStuck() )
{
//`log(self$" killed itself because it was stuck and being seen!!!");
//ScreenMessagePlayer(self$" killed itself because it was stuck and being seen!!!");
StuckSuicide();
}
else if( StuckTeleportToPathNode() )
{
//`log(self$" teleported because it was stuck, and seen, but couldn't suicide!!!");
//ScreenMessagePlayer(self$" teleported because it was stuck, and seen, but couldn't suicide!!!");
}
else if( StuckTeleportToSpawnVolume() )
{
//`log(self$" teleported to spawn volume because it was stuck, and seen, but couldn't suicide and couldn't teleport to pathnode!!!");
//ScreenMessagePlayer(self$" teleported to spawn volume because it was stuck, and seen, but couldn't suicide and couldn't teleport to pathnode!!!");
}
else
{
//`log(self$" stuck but can't teleport to spawn or nav or suicide and I've been seen!!!");
//ScreenMessagePlayer(self$" stuck but can't teleport to spawn or nav or suicide and I've been seen!!!");
}
}
else
{
if( StuckTeleportToSpawnVolume() )
{
//`log(self$" teleported to spawn volume because it was stuck!!!");
//ScreenMessagePlayer(self$" teleported to spawn volume because it was stuck!!!");
}
else if( StuckTeleportToPathNode() )
{
//`log(self$" teleported because it was stuck, but couldn't teleport to spawn volume!!!");
//ScreenMessagePlayer(self$" teleported because it was stuck, but couldn't teleport to spawn volume!!!");
}
else if( AmIAllowedToSuicideWhenStuck() )
{
//`log(self$" killed itself because it was stuck and couldn't teleport anywhere!!!");
//ScreenMessagePlayer(self$" killed itself because it was stuck and couldn't teleport anywhere!!!");
StuckSuicide();
}
else
{
//`log(self$" stuck but can't teleport to spawn or nav or suicide!!!");
//ScreenMessagePlayer(self$" stuck but can't teleport to spawn or nav or suicide!!!");
}
}
}
/** Kill this zed because it is stuck */
function StuckSuicide()
{
// Add back to the spawn queue!
if ( MyKFGameInfo != None && MyKFGameInfo.SpawnManager != none )
{
MyKFGameInfo.NumAISpawnsQueued -= 1;
MyKFGameInfo.MyKFGRI.AIRemaining++;
MyKFGameInfo.SpawnManager.LeftoverSpawnSquad[MyKFGameInfo.SpawnManager.LeftoverSpawnSquad.Length] = class<KFPawn_Monster>(Pawn.Class);
}
Pawn.Died( self, //Controller Killer,
class'DmgType_Suicided', // class<DamageType> damageType,
Pawn.Location); // vector HitLocation);
}
/** Teleport this zed to a spawn volume because it is stuck */
function bool StuckTeleportToSpawnVolume()
{
local array< class<KFPawn_Monster> > AIToTeleport;
local KFSpawnVolume KFSV;
local KFAISpawnManager SpawnManager;
local vector TeleportLocation;
AIToTeleport[AIToTeleport.Length] = MyKFPawn.Class;
SpawnManager = MyKFGameInfo.SpawnManager;
if ( SpawnManager != none )
{
SpawnManager.DesiredSquadType = MyKFPawn.default.MinSpawnSquadSizeType;
if( Enemy != none && Enemy.Controller != none )
{
KFSV = SpawnManager.GetBestSpawnVolume(AIToTeleport, Enemy.Controller,, True );
}
else
{
KFSV = SpawnManager.GetBestSpawnVolume(AIToTeleport,,, True );
}
if( KFSV != None )
{
TeleportLocation = KFSV.FindTeleportLocation( MyKFPawn.Class );
if( !IsZero( TeleportLocation ) )
{
if( TeleportToLocation(TeleportLocation, Pawn.Rotation, false) )
{
KFSV.HandleTeleportedTo();
//`log(self$" teleported to spawn volume "$KFSV$" because it was stuck!!!");
return true;
}
}
}
}
return false;
}
/** Teleport this zed to a path node because it is stuck */
function bool StuckTeleportToPathNode(optional float CheckRadius=512)
{
local NavigationPoint ResNav;
`AILog( GetFuncName() );
ResNav = class'KFPathnode'.static.GetNearestValidFloorNavWithinRadiusToPawn(Pawn, CheckRadius);
if( ResNav != none && TeleportToLocation(Resnav.Location, Pawn.Rotation) )
{
return true;
}
return false;
}
/** Teleport to a spawn volume closer to the enemy */
function RelocateTeleport()
{
local array< class<KFPawn_Monster> > AIToTeleport;
local KFSpawnVolume KFSV;
local KFAISpawnManager SpawnManager;
local vector TeleportLocation;
local float DistToEnemySquared;
AIToTeleport[AIToTeleport.Length] = MyKFPawn.Class;
SpawnManager = MyKFGameInfo.SpawnManager;
if ( SpawnManager != none )
{
SpawnManager.DesiredSquadType = MyKFPawn.default.MinSpawnSquadSizeType;
if( Enemy != none && Enemy.Controller != none )
{
DistToEnemySquared = VSizeSq(Pawn.Location - Enemy.Location);
KFSV = SpawnManager.GetBestSpawnVolume(AIToTeleport, Enemy.Controller,, True, DistToEnemySquared );
}
// else
// {
// KFSV = SpawnManager.GetBestSpawnVolume(AIToTeleport,, True );
// }
if( KFSV != None )
{
TeleportLocation = KFSV.FindTeleportLocation( MyKFPawn.Class );
if( !IsZero( TeleportLocation ) )
{
if( TeleportToLocation(TeleportLocation, Pawn.Rotation, false) )
{
KFSV.HandleTeleportedTo();
LastTeleportTime = WorldInfo.TimeSeconds;
//`log(self$" teleported to spawn volume "$KFSV$" because it hadn't seen an enemy for a while!!!");
return;
}
}
}
}
}
/** Return true if we should reduce zed on zed collision on bump when navigating */
function bool ShouldReduceZedOnZedCollisionOnBumpForNavigating()
{
local float DistToEnemySquared;
// Don't reduce zed on zed collision if we're close to an enemy
if( Enemy != none )
{
DistToEnemySquared = VSizeSq(Enemy.Location - Pawn.Location);
if( bEnemyIsVisible )
{
if( DistToEnemySquared < NavigationBumpTeamCollisionThreshholdSquared )
{
return false;
}
}
}
if( MyKFPawn != none )
{
// If noone can see us, reduce collision
if( MyKFPawn.IsUsingSuperSpeed() )
{
return true;
}
// If we think we may be stuck, reduce collision
if( StuckPossiblity > (StuckPossiblityThreshhold * 0.5) )
{
return true;
}
}
return false;
}
simulated function SpecialBumpHandling( float DeltaTime )
{
local float delta;
local Vector upvect;
if( MyKFPawn.CurrentChokePointTrigger == none && !ShouldReduceZedOnZedCollisionOnBumpForNavigating() )
{
if( MyKFPawn.bReducedZedOnZedPinchPointCollisionStateActive )
{
`AiLog( GetFuncName() @ " does not have a CurrentChokePointTrigger so turning turn collision on", 'SpecialBumpHandling' );
RestoreCollisionCylinderReducedPercentForSameTeamIgnoreBlockingBy();
}
}
else
{
//decay bump val
if( bBumpedThisFrame )
{
delta = fMax(0.7,abs( BumpThreshold - CurBumpVal ));
CurBumpVal = FInterpTo( CurBumpVal, BumpThreshold * 5.01f, DeltaTime, BumpGrowthRate / delta );
if(CurBumpVal > 0.3f)
{
//MessagePlayer(GetFuncname()@"HIT THRESHOLD! TURNING OFF COLLISION ZOMG");
`AiLog( GetFuncName() @ " CurBumpVal: " @ CurBumpVal @ " Has Gotten High enough to turn collision off - lastbumper: " @ lastbumper, 'SpecialBumpHandling' );
if( MyKFPawn.CurrentChokePointTrigger != none && MyKFPawn.CurrentChokePointTrigger.PartialReduceTeammateCollision() )
{
ReduceCollisionCylinderReducedPercentForSameTeamIgnoreBlockingBy(true);
}
else
{
ReduceCollisionCylinderReducedPercentForSameTeamIgnoreBlockingBy();
}
}
bBumpedThisFrame=false;
}
else
{
if(CurBumpVal > 0.01)
{
CurBumpVal = FInterpTo( CurBumpVal, 0.f, DeltaTime, BumpDecayRate / fMax(CurBumpVal,0.1) );
}
else if( MyKFPawn.bReducedZedOnZedPinchPointCollisionStateActive )
{
`AiLog( GetFuncName() @ " CurBumpVal: " @ CurBumpVal @ " Has Gotten low enough to turn collision on - LastBumper: " @ LastBumper, 'SpecialBumpHandling' );
RestoreCollisionCylinderReducedPercentForSameTeamIgnoreBlockingBy();
}
}
}
if( bShowCollisionRadiusForReducedZedOnZedPinchPointCollisionState && MyKFPawn != none )
{
upvect.X = 0.0;
upvect.Y = 0.0;
upvect.Z = MyKFPawn.CylinderComponent.CollisionHeight;
if( MyKFPawn.bReducedZedOnZedPinchPointCollisionStateActive )
{
DrawDebugCylinder( MyKFPawn.Location + upvect,
MyKFPawn.Location - upvect,
MyKFPawn.CylinderComponent.CollisionRadius * MyKFPawn.TeammateCollisionRadiusPercent,
10,
ColorForCollisionRadiusForReducedZedOnZedPinchPointCollisionStateOn.R,
ColorForCollisionRadiusForReducedZedOnZedPinchPointCollisionStateOn.G,
ColorForCollisionRadiusForReducedZedOnZedPinchPointCollisionStateOn.B,
false );
}
else
{
DrawDebugCylinder( MyKFPawn.Location + upvect,
MyKFPawn.Location - upvect,
MyKFPawn.CylinderComponent.CollisionRadius * MyKFPawn.TeammateCollisionRadiusPercent,
10,
ColorForCollisionRadiusForReducedZedOnZedPinchPointCollisionStateOff.R,
ColorForCollisionRadiusForReducedZedOnZedPinchPointCollisionStateOff.G,
ColorForCollisionRadiusForReducedZedOnZedPinchPointCollisionStateOff.B,
false );
}
}
}
/** Only used if bSpecialBumpHandling is true in .ini file */
function ReduceCollisionCylinderReducedPercentForSameTeamIgnoreBlockingBy(optional bool bPartialReduction)
{
if( MyKFPawn != None && MyKFPawn.Health > 0 )
{
//MyKFPawn.SetCollision(,false);
if( !MyKFPawn.bReducedZedOnZedPinchPointCollisionStateActive )
{
// Only make it a little smaller
if( bPartialReduction )
{
MyKFPawn.TeammateCollisionRadiusPercent = 0.4;
}
else
{
MyKFPawn.TeammateCollisionRadiusPercent = MyKFPawn.TeammateCollisionRadiusPercent / MyKFPawn.CylinderComponent.CollisionRadius;
}
`AiLog( GetFuncName() @ " - CollisionRadiusBeforeReducedZedOnZedPinchPointCollisionState: " @ MyKFPawn.default.TeammateCollisionRadiusPercent @ " - CollisionCylinderReducedPercentForSameTeamCollision: " @ MyKFPawn.TeammateCollisionRadiusPercent, 'CollisionToggle' );
MyKFPawn.bReducedZedOnZedPinchPointCollisionStateActive = true;
}
}
}
/** Only used if bSpecialBumpHandling is true in .ini file */
function RestoreCollisionCylinderReducedPercentForSameTeamIgnoreBlockingBy()
{
if( MyKFPawn != None && MyKFPawn.Health > 0 )
{
//MyKFPawn.SetCollision(,true);
MyKFPawn.TeammateCollisionRadiusPercent = MyKFPawn.default.TeammateCollisionRadiusPercent;
MyKFPawn.bReducedZedOnZedPinchPointCollisionStateActive = false;
`AiLog( GetFuncName() @ " - CollisionRadiusBeforeReducedZedOnZedPinchPointCollisionState: " @ MyKFPawn.default.TeammateCollisionRadiusPercent @ " - CollisionCylinderReducedPercentForSameTeamIgnoreBlockingBy: " @ MyKFPawn.TeammateCollisionRadiusPercent, 'CollisionToggle' );
}
else
{
`AiLog( GetFuncName() @ " MyKFPawn == None || MyKFPawn.Health <= 0 ", 'CollisionToggle' );
}
}
/** NPC has seen a player - use SeeMonster for similar notifications about seeing any pawns (currently not in use) */
event SeePlayer( Pawn Seen )
{
local float DistToEnemy, DistToSeen;
// if( bHasDebugCommand && AICommand_Debug(GetActiveCommand()).GetStateName() == 'DebugVision' )
// {
// AICommand(CommandList).NotifyEnemyBecameVisible( Seen );
// Enemy = Seen;
// Enable( 'EnemyNotVisible' );
// return;
// }
// Store the time this AI first saw a player
if( TimeFirstSawPlayer == 0 )
{
TimeFirstSawPlayer = Worldinfo.TimeSeconds;
}
if( MyKFPawn != none )
{
if( !MyKFPawn.IsAliveAndWell() )
{
// Disable SeePlayer indefinitely
DisableSeePlayer(0.f);
}
else if( MyKFPawn.IsDoingSpecialMove(SM_Emerge) )
{
DisableSeePlayer( 0.f );
return;
}
}
if ( Enemy == Seen )
{
CachedVisibleEnemy = Enemy;
EnemyVisibilityTime = WorldInfo.TimeSeconds;
bEnemyIsVisible = true;
EnableEnemyNotVisible();
if( MyKFPawn != none && MyKFPawn.IsAliveAndWell() )
{
`DialogManager.CheckSpotMonsterDialog( Enemy, MyKFPawn );
// Store the last time we saw the enemy
if( Enemy == Seen )
{
if( LastEnemySightedTime == 0 || `TimeSince(LastEnemySightedTime) > RepeatWalkingTauntTime )
{
if( MyKFPawn.CanDoSpecialMove(SM_WalkingTaunt) )
{
MyKFPawn.DoSpecialMove(SM_WalkingTaunt);
}
}
LastEnemySightedTime = WorldInfo.TimeSeconds;
}
}
return;
}
if( Enemy != none && Enemy != Seen )
{
//`log( self$" I already have an enemy: "$Enemy$" Visible? "$bEnemyIsVisible$" LOS to Seen? "$LineOfSightTo(Seen)$" CanSee Seen? "$CanSee(Seen) );
//`log( self$" Dist from Enemy: "$VSize(Enemy.Location - Pawn.Location)$" vs "$VSize( Seen.Location - Pawn.Location ) );
if( KFPawn(Seen) != none && KFPawn(Seen).IsDoingSpecialMove(SM_GrappleVictim) && NumberOfZedsTargetingPawn(Seen) <= 3 && !bEnemyIsVisible )
{
SetEnemy( Seen );
}
else if( LineOfSightTo(Seen) && !bEnemyIsVisible )
{
SetEnemy( Seen );
}
else if( CanSee(Seen) )
{
DistToEnemy = VSize(Enemy.Location - Pawn.Location);
DistToSeen = VSize(Seen.Location - Pawn.Location);
if( DistToEnemy > StrikeRange * 1.5 && DistToEnemy > 1.7 * DistToSeen )
{
SetEnemy( Seen );
}
}
}
else
{
SetEnemy( Seen );
}
// Store the last time we saw the enemy if we just switched to that enemy
if( Enemy == Seen )
{
LastEnemySightedTime = WorldInfo.TimeSeconds;
}
// Notify the AI command that we saw an enemy
if( CachedAICommandList != none )
{
`AILog( "SeePlayer: "$Seen$" - while I'm moving to goal, notifying "$CommandList$" and letting it handle this event", 'SeePlayer' );
CachedAICommandList.NotifyPlayerBecameVisible( Seen );
}
}
/** NPC has lost sight of current enemy */
event EnemyNotVisible()
{
// if( bHasDebugCommand && AICommand_Debug(GetActiveCommand()).GetStateName() == 'DebugVision' )
// {
// msg( "I CANNOT SEE YOU!" );
// AICommand(CommandList).NotifyEnemyNotVisible();
// return;
// }
// Let the command, if any, handle this
if( CommandList != none )
{
`AILog( GetFuncName()$"() - notifying "$CommandList$" and letting it handle this event", 'EnemyNotVisible' );
AICommand(CommandList).NotifyEnemyNotVisible();
}
if( Enemy != none && MyKFPawn != none )
{
MyKFPawn.ClearHeadTrackTarget( Enemy );
}
if( bEnemyIsVisible )
{
// Evalulate if we should switch enemies if we lose site of our current enemy
FindNewEnemy();
}
bEnemyIsVisible = false;
DisableEnemyNotVisible();
EnableSeePlayer();
bWasVisibleToEnemy = bIsVisibleToEnemy;
bIsVisibleToEnemy = false;
}
/** NPC has "heard" a noise (instigated by MakeNoise()) */
event HearNoise( float Loudness, Actor NoiseMaker, optional Name NoiseType )
{
return;
DisableHearNoise();
if( CachedAICommandList != none )// && Enemy == Seen )
{
//`AILog( "HearNoise: "$Other$" - notifying "$CommandList$" and letting it handle this event", 'BumpEvent' );
if( CachedAICommandList.NotifyHearNoise( Loudness, NoiseMaker, NoiseType ) )
{
return;
}
}
// Respond to MakeNoise from nearby player reloading
// if( NoiseType == 'PlayerReload' && FRand() < 0.75f && VSize(KFWeapon(NoiseMaker).Instigator.Location - Pawn.Location) < 1024.f )
// {
// `AILog( "HearNoise aborting movement commands", 'Command_MoveToGoal' );
// AbortMovementCommands();
//
// SetEnemy( KFWeapon(NoiseMaker).Instigator );
// }
// else
// {
// super.HearNoise( Loudness, NoiseMaker, NoiseType );
// }
`AILog( self$" HearNoise event, Loudness: "$Loudness$" NoiseMaker: "$NoiseMaker$" Type: "$NoiseType$" Dist: "$VSize( NoiseMaker.Location - Pawn.Location )$"... Disabling HearNoise", 'HearNoise' );
// Disable noise hearing, it will be re-enabled later.
DisableHearNoise();
}
event bool NotifyBump( Actor Other, vector HitNormal )
{
local AICommand_HeadlessWander HeadlessWander;
local KFPawn_Monster KFPM;
local bool bInPartialCollisionReductionTrigger;
local actor HitActor;
if( MyKFPawn != none && MyKFPawn.IsDoingSpecialMove(SM_Emerge) )
{
DisableBump( 0.25f );
return true;
}
KFPM = KFPawn_Monster( Other );
if( bSpecialBumpHandling )
{
if( MyKFPawn != none && KFPM != none && KFPM.Health > 0 )
{
MyKFPawn.HandleMonsterBump( KFPM, HitNormal );
if( !KFPM.IsDoingSpecialMove(SM_MeleeAttack) && KFPM.MyKFAIC != none && KFPM.MyKFAIC.DoorEnemy == none && !IsZero(KFPM.Acceleration) )
{
bInPartialCollisionReductionTrigger = MyKFPawn.CurrentChokePointTrigger != none && MyKFPawn.CurrentChokePointTrigger.PartialReduceTeammateCollision();
// Reduce the collision with other zeds if noone is watching, or we are in a choke point that allows it
if( MyKFPawn.CurrentChokePointTrigger != none
&& (MyKFPawn.CurrentChokePointTrigger.CanReduceTeammateCollision()
|| bInPartialCollisionReductionTrigger
|| ShouldReduceZedOnZedCollisionOnBumpForNavigating()) )
{
if( bInPartialCollisionReductionTrigger && Enemy != none )
{
// If this line doesn't hit our enemy, go ahead and let this bump count to reduce collision if we're standing in the trigger of a welded door!
HitActor = ActorBlockTest( Pawn, Enemy.Location + vect(0,0,1) * Enemy.BaseEyeHeight, MyKFPawn.Location + vect(0,0,1) * MyKFPawn.BaseEyeHeight,, true );
}
if( !IsWithinAttackRange() || (bInPartialCollisionReductionTrigger && (HitActor == none || HitActor != Enemy)) )
{
bBumpedThisFrame = true;
lastbumper = KFPM;
//msg( "BUMP! "$CurBumpVal );
return true;
//MessagePlayer(GetFuncname()@"BUMP! -"@CurBumpVal);
}
else
{
`AiLog( GetFuncName() @ " Bumped: " @ KFPM @ " but IsWithinAttackRange so not going to care about SpecialBumpHandling right now!!!", 'SpecialBumpHandling' );
}
}
else
{
`AiLog( GetFuncName() @ " Bumped: " @ KFPM @ " but not in a door trigger so not going to care about SpecialBumpHandling right now!!!", 'SpecialBumpHandling' );
}
}
}
}
DisableBump( 0.25f );
if (MyKFPawn != none && MyKFPawn.Physics == PHYS_Falling)
{
return false;
}
`RecordAIBump( `StatID(AI_BUMP), self, Pawn.Location, Pawn.Rotation, Other, "SSpeed: "$MyKFPawn != none ? MyKFPawn.IsUsingSuperSpeed() : False );
`AILog( "NotifyBump() into "$Other$" HitNormal: "$HitNormal$" MoveTarget: "$MoveTarget!=none?string(MoveTarget):"none", 'BumpEvent' );
DisableBump( 0.25f );
LastBumpedPawn = Pawn(Other);
if( MyKFPawn != none && MyKFPawn.bPlayShambling && LastBumpedPawn != none && LastBumpedPawn.IsAliveAndWell() )
{
HeadlessWander = AICommand_HeadlessWander(GetActiveCommand());
DisableBump( 0.3f );
if( HeadlessWander != none && !MyKFPawn.IsDoingSpecialMove() && `TimeSince( HeadlessWander.LastHeadlessAttackTime ) > 2.5f )
{
UpdatePendingStrike();
DoStrike();
}
}
if (Role == ROLE_Authority && bCanDoHeavyBump)
{
DoHeavyZedBump(Other, HitNormal);
return true;
}
// if( IgnoreNotifies() )
// {
// `AILog( "Bump ignoring notifies for "$GetActiveCommand(), 'BumpEvent' );
// return false;
// }
// Let any interested AICommands deal with this
if( CachedAICommandList != none )// && Enemy == Seen )
{
`AILog( "Bump: "$Other$" - notifying "$CommandList$" and letting it handle this event", 'BumpEvent' );
if( CachedAICommandList.NotifyBump(Other, HitNormal) )
{
return true;
}
}
if( KfMovementPlugin != none )
{
//if( KfMovementPlugin.NotifyBump(Other, HitNormal) )
//{
// return true;
//}
}
if( LastBumpedPawn == none || Pawn.IsSameTeam(LastBumpedPawn) || !LastBumpedPawn.IsAliveAndWell() || (Enemy != none && Enemy == Other) )
{
return false;
}
// From KF1
if( SetEnemy(LastBumpedPawn) )
{
return false;
}
return false;
}
function DrawFireAttackDebug()
{
//BallisticProjectileFireBehavior.DrawFireAttackDebug();
DirectProjectileFireBehavior.DrawFireAttackDebug();
LeapBehavior.DrawFireAttackDebug();
}
/** Called when we have dealt melee damage */
function NotifyMeleeDamageDealt();
/*********************************************************************************************
* SpecialMoves
********************************************************************************************* */
// Stub function
function SpecialMoveTimeout();
function NotifySpecialMoveEnded(KFSpecialMove SM)
{
local LatentActionObserver ObserverInterface;
`AILog(SM@"finished", 'SpecialMove');
// notify observers
ObserverInterface = LatentActionObserver(SM.AISpecialOwner);
if (ObserverInterface != none)
{
ObserverInterface.OnLatentFinished(self, SM, BTR_Success);
}
LastSpecialMoveEndTime = WorldInfo.TimeSeconds;
}
function NotifySpecialMoveStarted(KFSpecialMove SM);
/*********************************************************************************************
Steering related
********************************************************************************************* */
// Timer called after possession
simulated function StartSteering()
{
if( Steering != none )
{
`AILog( GetFuncName()$"() turning on separation steering, separating from class KFPawn_Monster", 'AISteering' );
Steering.SeparationOn( class'KFPawn_Monster' );
}
}
/*********************************************************************************************
* Debugging related functions
* For NPC Overhead debug text, see:
* To display a message to players, see: event msg()
* To set PlayersOnly and display a message to players, see: event PauseAndShowMsg()
* To edit AILog output, see: event AILog_Internal() and Globals.uci
* To set up a debug mode when NPC spawns, see: SetUpDebug()
*
*
********************************************************************************************* */
/*---------------------------------------------------------
Native function declarations
---------------------------------------------------------*/
/** Outputs the AICommand history array to the log file */
native function DumpCommandHistory();
native static function DrawDebugText( HUD HUD, String Text, optional color TextColor );
function DrawDebugTextToHud( HUD HUD, String Text, optional color TextColor )
{
DrawDebugText( HUD, Text, TextColor );
}
/** Assorted debug options - uncomment to enable */
function SetupDebug()
{
local KFAIDirector Director;
if( MyKFGameInfo != none )
{
Director = MyKFGameInfo.GetAIDirector();
if( Director != none )
{
bDebug_PostRenderInfo = (bDebug_DrawOverheadInfo || Director.bShowAINames) || bDebug_DrawAttackAnimInfo || bDebug_DrawAIDebug || (MyKFPawn != none && MyKFPawn.bDebug_DrawSprintingOverheadInfo);
if( Director.bShowAINames )
{
bDebug_DrawOverheadInfo = true;
MyKFPawn.SetDebugTextRendering(true);
//bDebug_DrawAIDebug = true;
//SetPostRendering( bDebug_PostRenderInfo );
}
}
}
//`if(`__TW_AIDEBUGGING_)
if( !IsTimerActive(nameof(Debug_CheckRecentMoveTime)) )
{
// Tracking stuck NPCs
SetTimer( 20.f + FRand(), true, nameof(Debug_CheckRecentMoveTime) );
}
//`endif
}
/** Is the AI currently in debug mode? (i.e, using AICommand_Debug) */
function bool IsInDebugMode()
{
local GameAICommand Cmd;
local AICommand_Debug DebugCommand;
for( Cmd = CommandList; Cmd != None; Cmd = Cmd.ChildCommand )
{
DebugCommand = AICommand_Debug(Cmd);
if( DebugCommand != none )
{
return true;
}
}
return false;
}
/*---------------------------------------------------------
Logging / Screen Messages
---------------------------------------------------------*/
/** Used to update RecentActionInfo string, which is contoniually displayed on-screen in some debug modes */
event SetRecentActionInfo( string Info )
{
RecentActionInfo = "["$GetStateName()$"]["$Info$"]";
}
simulated function OnDestroy(SeqAct_Destroy Action)
{
if (Pawn != None)
{
Pawn.OnDestroy(Action);
}
Super.OnDestroy(Action);
}
/** Sends message to local players using ClientMessage and log the text */
event Msg( string MsgTxt, optional bool bOutputToLog=true, optional name MessageType, optional float MessageDuration )
{
if( MyKFPawn != none )
{
MyKFPawn.KFMessagePlayer( MsgTxt, MessageType, MessageDuration );
if( bOutputToLog )
{
`AILog( MsgTxt, 'Msg' );
}
}
}
/** Main AI related logging function usually called from macros `AILog & `AILog_Ext or from native code */
event AILog_Internal( coerce string LogText, optional Name LogCategory, optional bool bForce, optional bool BugIt, optional bool bSkipExtraInfo )
{
`if(`notdefined(ShippingPC))
local AICommand ActiveCommand;
local int Idx, FileNameLength;
local String ActionStr, BorderStr, FinalStr, StatusStr, FileName, GoString, LocString;
local Engine Eng;
local KFGameReplicationInfo KFGRI;
if( MyKFPawn == none || MyKFPawn.Health <= 0 )
{
return;
}
Eng = class'Engine'.static.GetEngine();
if( (!bForce && !bAILogging) || Eng.bDisableAILogging )
{
return;
}
if( LogCategory == 'AICommand' || LogCategory == 'InitAICommand' || LogCategory == 'Command_Base' )
{
bSkipExtraInfo = true;
}
ActiveCommand = AICommand( GetActiveCommand() );
if( !bForce )
{
if( ActiveCommand != none && ActiveCommand.OverrideLogCategory != "" )
{
LogCategory = name( ActiveCommand.OverrideLogCategory );
}
for( Idx = 0; Idx < AILogFilter.Length; Idx++ )
{
if( AILogFilter[Idx] == LogCategory || (LogCategory == '' && AILogFilter[Idx] == 'Default') )
{
return;
}
}
}
BorderStr = "--------------------------------------------------------------------------------------------------";
if( MyKFPawn != none )
{
if( bUseUniqueAILogFile && AILogFile == None )
{
AILogFile = Spawn(class'FileLog');
FileName = string( MyKFPawn );
FileName = Repl(FileName,"KFPawn_","",false);
FileName = Repl(FileName,"Zed","",false);
if( WorldInfo.GRI != none )
{
KFGRI = KFGameReplicationInfo(WorldInfo.GRI);
FileName = "w"$KFGRI.WaveNum$"_"$FileName;
}
// Include the file extension in determining the length
FileNameLength = Len(Filename) + 6;
// make sure file name isn't too long for consoles
if(FileNameLength > 40)
{
// Make sure there is room for the .ailog part too
FileName = Right(Filename,34);
}
`log( "Created log file "$AILogFile$" name: "$FileName );
AILogFile.bKillDuringLevelTransition = true;
AILogFile.bFlushEachWrite = bFlushAILogEachLine;
// Use async unless flushing was requested
AILogFile.bWantsAsyncWrites = !bFlushAILogEachLine;
AILogFile.OpenLog(FileName,".ailog");
AILogFile.Logf( "========================================================================================================" );
AILogFile.Logf( "Log file for "$MyKFPawn$" created: ["$MyKFPawn.CreationTime$"] "$self$" Controlling "$MyKFPawn );
AILogFile.Logf( " " );
AILogFile.Logf( "Spawn Location Info: " );
AIBugItStringCreator( MyKFPawn.Location, MyKFPawn.Rotation, GoString, LocString );
AILogFile.Logf( GoString );
AILogFile.Logf( LocString );
AILogFile.Logf( "Difficulty:"$MyKFGameInfo.GetModifiedGameDifficulty());
AILogFile.Logf( "Original Health:"$MyKFPawn.HealthMax$" Adjusted Health:"$MyKFPawn.Health );
AILogFile.Logf( "Original GroundSpeed:"$MyKFPawn.default.GroundSpeed$" Adjusted GroundSpeed:"$MyKFPawn.GroundSpeed );
AILogFile.Logf( "Original SprintSpeed:"$MyKFPawn.default.SprintSpeed$" Adjusted SprintSpeed:"$MyKFPawn.SprintSpeed );
AILogFile.Logf( "Original HiddenSpeed:"$MyKFPawn.default.HiddenGroundSpeed$" Adjusted HiddenSpeed:"$MyKFPawn.HiddenGroundSpeed );
if( MyKFPawn.PawnAnimInfo != none )
{
AILogFile.Logf( "WeakAttackChance:"$MyKFPawn.PawnAnimInfo.WeakAttackChance );
AILogFile.Logf( "MediumAttackChance:"$MyKFPawn.PawnAnimInfo.MediumAttackChance );
AILogFile.Logf( "HardAttackChance:"$MyKFPawn.PawnAnimInfo.HardAttackChance );
}
AILogFile.Logf( "========================================================================================================" );
AILogFile.Logf( " " );
}
}
//ActionStr = String(GetStateName());
if( !bSkipExtraInfo )
{
if( ActiveCommand != none )
{
ActionStr = string( ActiveCommand.Name ); //$":"$String( ActiveCommand.GetStateName() );
}
if( MyKFPawn != none )
{
StatusStr = " [Phys:"$MyKFPawn.GetPhysicsName()$"][Velocity:"$VSize(MyKFPawn.Velocity)$"]";
if( bPreparingMove )
{
StatusStr = StatusStr$"[bPreparingMove:TRUE]";
}
if( MyKFPawn.Anchor != none )
{
StatusStr = StatusStr$"[Anchor:"$MyKFPawn.Anchor$"]";
}
if( MyKFPawn.IsUsingSuperSpeed() )
{
StatusStr = StatusStr$"[SUPERSPEED]";
}
if( Enemy != none )
{
StatusStr = StatusStr$"[EnemyDist:"$VSize(Enemy.Location - MyKFPawn.Location)$"]";
}
if( MyKFPawn.IsDoingSpecialMove() )
{
StatusStr = StatusStr$"[SM:"$MyKFPawn.SpecialMoves[MyKFPawn.SpecialMove]$"]";
}
}
else
{
FinalStr = "[NO PAWN]";
}
if( MyKFPawn.bCrawler && MyKFPawn.Base != none )
{
StatusStr = StatusStr$"[BASE:"$MyKFPawn.Base$"]";
}
if( PendingDoor != none )
{
StatusStr = StatusStr$"[PENDING DOOR:"$PendingDoor$"]";
}
if( DoorEnemy != none )
{
StatusStr = StatusStr$"[DOORENEMY:"$DoorEnemy$"]";
}
FinalStr = "["$WorldInfo.TimeSeconds$"]["$ActionStr$"]"$FinalStr;
}
else
{
FinalStr = "["$WorldInfo.TimeSeconds$"]"; //["$self$"]";
}
if( bDebug_LogToProfiler )
{
LogToProfiler( LogText, LogCategory );
}
if( AILogFile != none && bUseUniqueAILogFile )
{
if( !bSkipExtraInfo )
{
AILogFile.Logf( BorderStr );
AILogFile.Logf( FinalStr );
AILogFile.Logf( StatusStr );
AILogFile.Logf( LogText );
AILogFile.Logf( BorderStr );
}
else
{
AILogFile.Logf( FinalStr@LogText );
}
if( BugIt )
{
AIBugItStringCreator( MyKFPawn.Location, MyKFPawn.Rotation, GoString, LocString );
AILogFile.Logf( "BUGIT: "$GoString );
}
AILogFile.Logf( " " );
}
else
{
if( !bSkipExtraInfo )
{
`log( BorderStr );
}
`log( FinalStr );
`log( StatusStr );
`log( LogText );
if( !bSkipExtraInfo )
{
`log( BorderStr );
}
if( BugIt && AILogFile != none )
{
AIBugItStringCreator( MyKFPawn.Location, MyKFPawn.Rotation, GoString, LocString );
AILogFile.Logf( "BUGIT: "$GoString );
}
`log( " " );
}
//else
//{
// `AILog( Pawn@"["$WorldInfo.TimeSeconds$"]"@ActionStr$":"@LogText );
//}
`Log( Pawn@"["$WorldInfo.TimeSeconds$"]"@FinalStr$":"@LogText, bAILogToWindow );
`endif //`notdefined(ShippingPC)
}
function AIBugItStringCreator( const out Vector ViewLocation, const out Rotator ViewRotation, out String GoString, out String LocString )
{
GoString = "BugItGo " $ ViewLocation.X $ " " $ ViewLocation.Y $ " " $ ViewLocation.Z $ " " $ ViewRotation.Pitch $ " " $ ViewRotation.Yaw $ " " $ ViewRotation.Roll;
//`AILog( GoString, 'BugIt' );
LocString = "?BugLoc=" $ string(ViewLocation) $ "?BugRot=" $ string(ViewRotation);
//`AILog( LocString, 'BugIt' );
}
/** Debug event which will initiate PlayersOnly pause mode and display MsgTxt to player(s) */
event PauseAndShowMsg( optional string MsgTxt, optional vector TeleportToLocation )
{
local PlayerController PC;
local DebugCameraController DCC;
foreach WorldInfo.AllControllers( class'PlayerController', PC )
{
PC.ClientMessage( MsgTxt );
if( !WorldInfo.bPlayersOnly && !WorldInfo.bPlayersOnlyPending )
{
PC.CheatManager.PlayersOnly();
break;
}
}
if( TeleportToLocation != vect(0,0,0) )
{
foreach WorldInfo.AllControllers( class'DebugCameraController', DCC )
{
DCC.SetLocation( TeleportToLocation );
}
}
}
function ScreenMessagePlayer( coerce String Msg )
{
`if(`notdefined(FINAL_RELEASE))
local PlayerController PC;
foreach LocalPlayerControllers(class'PlayerController', PC)
{
PC.ClientMessage( Msg );
}
`endif
}
/*********************************************************************************************
* KFDebugLines & Path rendering in-game
********************************************************************************************* */
function KFDebugLines GetDebugLines()
{
return class'KFDebugLines'.static.GetDebugLines();
}
function KFDebug_ClearLines( optional int ID )
{
class'KFDebugLines'.static.KFDebug_ClearLines( name, ID );
}
function KFDebug_ClearSpheres( optional int ID )
{
class'KFDebugLines'.static.KFDebug_ClearSpheres( name, ID );
}
function KFDebug_ClearDebugText( optional int ID )
{
class'KFDebugLines'.static.KFDebug_ClearText( name, ID );
}
function KFDebug_DrawPath( optional int ID )
{
local KFDebugLines KFDL;
return;
KFDL = GetDebugLines();
if( KFDL != none )
{
KFDL.AddDebugLineFromOwner( name, MoveTarget.Location, 0, 255, 0, false, 20, ID );
KFDL.AddDebugSphere( MoveTarget.Location, 16, 8, 0, 100, 100, false, 20, name, ID );
}
}
function KFDebug_DrawMyPath()
{
local KFDebugLines KFDL;
local int i;
KFDL = GetDebugLines();
if( KFDL != none )
{
KFDL.RemoveOwnedDebugLines(name);
KFDL.RemoveOwnedDebugSpheres(name);
KFDL.RemoveOwnedDebugText3D(name);
for( i = 0; i < RouteCache.Length; i++ )
{
if( i < (RouteCache.Length - 1) )
{
KFDL.AddDebugLine( RouteCache[i].Location, RouteCache[i+1].Location, 113, 167, 207, false, 20, name, i );
}
KFDL.AddDebugSphere( RouteCache[i].Location, 24, 8, 113, 167, 207, true,, name, i );
KFDL.AddDebugText3D( RouteCache[i].Location + vect(0,0,32), string(RouteCache[i]), true, 113, 167, 207, true,,,, name, i );
}
}
}
function KFDebug_DrawPathTo( Actor A, optional int ID, optional int SecondaryID, optional int TextID )
{
local KFDebugLines KFDL;
if( bDebug_DrawPath )
{
KFDL = GetDebugLines();
if( KFDL != none )
{
KFDL.AddDebugLineFromOwner( name, A.Location, 113,167,207, false, 45, ID );
//KFDL.AddDebugLine( Pawn.Location, MoveTarget.Location, 0, 255, 0, false, 45, name, ID );
KFDL.AddDebugSphere( A.Location, 16, 8, 113,167,207, false, 45, name, ID );
KFDL.AddDebugLineFromOwner( name, MoveGoal.Location, 126,250,85, false, 45, SecondaryID );
KFDL.AddDebugSphere( MoveGoal.Location, 16, 8, 125, 250, 85, false, 45, name, SecondaryID );
KFDL.AddDebugText3D( A.Location + vect(0,0,32), string(A), true, 113, 167, 207, false, 45, A,, name, ID );
}
}
}
function KFDebug_DrawPathGoal( optional int ID )
{
local KFDebugLines KFDL;
return;
KFDL = GetDebugLines();
if( KFDL != none )
{
KFDL.AddDebugLineFromOwner( name, MoveGoal.Location, 0, 255, 0, false, 45, ID );
//KFDL.AddDebugLine( Pawn.Location, MoveTarget.Location, 0, 255, 0, false, 45, name, ID );
KFDL.AddDebugSphere( MoveGoal.Location, 16, 8, 0, 100, 100, false, 45, name, ID );
}
}
/** Logs text/category to file readable by AIProfiler */
native function LogToProfiler( string Text, name Category );
/**
*
*
*/
function ShowStuckNpcsToggle()
{
// start timer to check update
if( IsTimerActive(nameof(Debug_CheckStuckTimer)) )
{
ClearTimer( nameof(Debug_CheckStuckTimer) );
}
else
{
// record location at debug start
LocationAtStartOfStuckCheck = MyKFPawn.Location;
LocationAtLastStuckCheck = MyKFPawn.Location;
// Tracking stuck NPCs
SetTimer( 1.0f, true, nameof(Debug_CheckStuckTimer) );
}
}
function Debug_CheckStuckTimer()
{
local Vector displacementFromStartOfCheck;
local float distanceFromStartOfCheck;
local Vector displacementFromLastStuckUpdate;
local float distanceFromLastStuckUpdate;
displacementFromStartOfCheck = MyKFPawn.Location - LocationAtStartOfStuckCheck;
distanceFromStartOfCheck = VSize(displacementFromStartOfCheck);
displacementFromLastStuckUpdate = MyKFPawn.Location - LocationAtLastStuckCheck;
distanceFromLastStuckUpdate = VSize(displacementFromLastStuckUpdate);
LocationAtLastStuckCheck = MyKFPawn.Location;
if( distanceFromStartOfCheck < 1.0f )
{
DrawDebugLine( MyKFPawn.Location, Enemy.Location , 255, 0, 0, true );
}
else if( distanceFromLastStuckUpdate < 1.0f )
{
DrawDebugLine( MyKFPawn.Location, Enemy.Location , 255, 255, 0, true );
}
}
function float GetMaxTimeAllowedToStayStuckBasedOnStuckType( MOVE_FAILURE_TYPE TypeOfFailure )
{
local float maxTime;
if( TypeOfMovementStuckOn == MOVE_FAILURE_TYPE_NO_NAV_MESH_PATH )
{
maxTime = NoNavMeshPathMaxTimeAllowedToStayStuckBeforeSuicide;
}
else if( TypeOfMovementStuckOn == MOVE_FAILURE_TYPE_SAME_INTERMEDIATE_POINT_TOO_MANY_TIMES )
{
maxTime = SameIntermediatePointToManyTimesMaxTimeAllowedToStayStuckBeforeSuicide;
}
else if( TypeOfMovementStuckOn == MOVE_FAILURE_TYPE_TARGET_OFF_NAV_MESH_AND_CAN_NOT_FIND_LOCAITON_NEAR_THEM_I_CAN_MOVE_TO )
{
maxTime = TargetOffNavMeshAndCanNotFindLocaitonNearThemICanMoveTooMaxTimeAllowedToStayStuckBeforeSuicide;
}
else
{
maxTime = DefaultMaxTimeAllowedToStayStuckBeforeSuicide;
}
return maxTime;
}
/**
*
*
*/
function ToggleHardCoreStuckNpcs( AICommand CmdTriggering, AITickablePlugin PlugInTriggering )
{
// start timer to check update
local float timeToTrip;
if( IsTimerActive(nameof(HardCoreCheckStuckTimer)) )
{
ClearTimer( nameof(HardCoreCheckStuckTimer) );
CmdTriggeringHardCoreStuckChecking = none;
PlugInTriggeringTriggeringHardCoreStuckChecking = none;
}
else if( MyKFPawn != none )
{
// record location at debug start
LocationAtStartOfStuckCheck = MyKFPawn.Location;
LocationAtLastStuckCheck = MyKFPawn.Location;
CmdTriggeringHardCoreStuckChecking = CmdTriggering;
PlugInTriggeringTriggeringHardCoreStuckChecking = PlugInTriggering;
timeToTrip = GetMaxTimeAllowedToStayStuckBasedOnStuckType( TypeOfMovementStuckOn );
// Tracking stuck NPCs
SetTimer( timeToTrip, true, nameof(HardCoreCheckStuckTimer) );
}
}
function float GetMinDistaceToHaveToMoveToBeConcideredStuckByStuckType( MOVE_FAILURE_TYPE TypeOfFailure )
{
local float minDistance;
if( TypeOfMovementStuckOn == MOVE_FAILURE_TYPE_NO_NAV_MESH_PATH )
{
minDistance = NoNavMeshPathMinDistaceToHaveToMoveToBeConcideredStuckBeforeSuicide;
}
else if( TypeOfMovementStuckOn == MOVE_FAILURE_TYPE_SAME_INTERMEDIATE_POINT_TOO_MANY_TIMES )
{
minDistance = SameIntermediatePointToManyTimesMinDistaceToHaveToMoveToBeConcideredStuckBeforeSuicide;
}
else
{
minDistance = DefaultMinDistaceToHaveToMoveToBeConcideredStuckBeforeSuicide;
}
return minDistance;
}
function HardCoreCheckStuckTimer()
{
local Vector displacementFromStartOfCheck;
local float distanceFromStartOfCheck;
local Vector displacementFromLastStuckUpdate;
local float distanceFromLastStuckUpdate;
local color stuckLineColor;
local float minDistanceToMove;
displacementFromStartOfCheck = MyKFPawn.Location - LocationAtStartOfStuckCheck;
distanceFromStartOfCheck = VSize(displacementFromStartOfCheck);
displacementFromLastStuckUpdate = MyKFPawn.Location - LocationAtLastStuckCheck;
distanceFromLastStuckUpdate = VSize(displacementFromLastStuckUpdate);
LocationAtLastStuckCheck = MyKFPawn.Location;
minDistanceToMove = GetMinDistaceToHaveToMoveToBeConcideredStuckByStuckType( TypeOfMovementStuckOn );
if( distanceFromStartOfCheck <= minDistanceToMove )
{
if( bShowVisualStuckZedDebugInfo )
{
if( TypeOfMovementStuckOn == MOVE_FAILURE_TYPE_NO_NAV_MESH_PATH )
{
stuckLineColor = Move_failure_type_no_nav_mesh_path_color;
}
else if( TypeOfMovementStuckOn == MOVE_FAILURE_TYPE_SAME_INTERMEDIATE_POINT_TOO_MANY_TIMES )
{
stuckLineColor = Move_failure_type_same_intermediate_point_too_many_times_color;
}
else if( TypeOfMovementStuckOn == MOVE_FAILURE_TYPE_TARGET_OFF_NAV_MESH_AND_CAN_NOT_FIND_LOCAITON_NEAR_THEM_I_CAN_MOVE_TO )
{
stuckLineColor = MoveFailureTypeTargetOffNavMeshAndCanNotFindLocaitonNearThemICanMoveTo;
}
else
{
stuckLineColor = Move_failure_type_none_color;
}
DrawDebugLine( MyKFPawn.Location, Enemy.Location , stuckLineColor.R, stuckLineColor.G, stuckLineColor.B, true );
}
if( CmdTriggeringHardCoreStuckChecking != none )
{
CmdTriggeringHardCoreStuckChecking.NotifyNpcTerminallyStuck();
}
else if( PlugInTriggeringTriggeringHardCoreStuckChecking != none )
{
PlugInTriggeringTriggeringHardCoreStuckChecking.NotifyNpcTerminallyStuck();
}
}
else if( distanceFromLastStuckUpdate <= minDistanceToMove )
{
if( bShowVisualStuckZedDebugInfo )
{
DrawDebugLine( MyKFPawn.Location, Enemy.Location , 255, 255, 0, true );
}
if( CmdTriggeringHardCoreStuckChecking != none )
{
CmdTriggeringHardCoreStuckChecking.NotifyNpcInGrannyMode();
}
else if( PlugInTriggeringTriggeringHardCoreStuckChecking != none )
{
PlugInTriggeringTriggeringHardCoreStuckChecking.NotifyNpcInGrannyMode();
}
}
}
function bool AmIAllowedToSuicideWhenStuck()
{
return true;
}
function bool AmIAllowedToStillUsePathNodes()
{
return false;
}
//`if(`__TW_AIDEBUGGING_)
/** Timer that's tracking stuck NPCs */
function Debug_CheckRecentMoveTime()
{
// local float TimeSinceLastMove;
// local float TimerRate;
// TimeSinceLastMove = `TimeSince(LastMoveFinishTime);
// TimerRate = GetTimerRate(nameof(Debug_CheckRecentMoveTime));
//
// if( TimeSinceLastMove > TimerRate )
// {
// `AILog( "** WARNING ** "$self$" hasn't moved in over "$TimeSinceLastMove$" seconds! Dumping CommandStack and CommandHistory:", 'Critical' );
// DumpCommandStack();
// DumpCommandHistory();
// }
}
//`endif
/** Called from native code when AI is paused */
event Debug_AIPaused( bool bNewPause )
{
if( bNewPause )
{
AbortMovementCommands();
AbortMovementPlugIns();
Pawn.ZeroMovementVariables();
StopAllLatentMovement();
}
}
/** Dump AI command stack to the log file */
function DumpStack()
{
DumpCommandStack();
}
/** Adds self to Player HUD PostRenderedActors list. Actors in the list will have PostRenderFor() called. */
function SetPostRendering( bool bOn )
{
local PlayerController KFPC;
local KFHUDBase KFHUD;
foreach LocalPlayerControllers(class'PlayerController', KFPC)
{
KFHUD = KFHUDBase(KFPC.myHUD);
if( KFHUD != none )
{
if( bOn )
{
bPostRenderIfNotVisible = true;
KFHUD.SetShowOverlays( true );
KFHUD.AddPostRenderedActor(self);
}
else
{
bPostRenderIfNotVisible = default.bPostRenderIfNotVisible;
//KFHUD.SetShowOverlays( false );
KFHUD.RemovePostRenderedActor(self);
}
}
}
}
/**
Script function called by NativePostRenderFor().
*/
simulated event PostRenderFor(PlayerController PC, Canvas Canvas, vector CameraPosition, vector CameraDir)
{
if( MyKFPawn != none )
{
MyKFPawn.PostRenderFor(PC, Canvas, CameraPosition, CameraDir);
}
}
function DrawAttackInfo( KFHUDBase HUD )
{
local Canvas C;
local int i, x;
local string DisplayString;
local KFPawnAnimInfo KFPAG;
local name CurrentSeq, InRangeTag;
if( !bDebug_DrawAttackAnimInfo || MyKFPawn == none || MyKFPawn.PawnAnimInfo == none || Enemy == none )
{
return;
}
if( MyKFPawn.PawnAnimInfo == none )
{
return;
}
C = HUD.Canvas;
C.Font = class'Engine'.Static.GetTinyFont();
C.SetDrawColor( 255, 255, 255 );
C.SetPos( C.SizeX * 0.05f, C.SizeY * 0.22f );
DrawDebugText( HUD, "ATTACK ANIMATION INFO FOR "$Pawn.Name$" (Controller: "$self$")" );
C.SetDrawColor( 0, 0, 255, 255 );
DrawDebugText( HUD, "--------------------------------------------------------------------------" );
DrawDebugText( HUD, " " );
KFPAG = MyKFPawn.PawnAnimInfo;
if( MyKFPawn.InAnyAttackTagRange(Enemy.Location, InRangeTag) )
{
C.SetDrawColor( 255, 25, 0, 255 );
DrawDebugText( HUD, "Distance From Enemy: "$VSize(Pawn.Location - Enemy.Location)$"uu [WITHIN ATTACK RANGE]" );
C.SetDrawColor( 0, 255, 50, 255 );
}
else
{
C.SetDrawColor( 0, 255, 50, 255 );
DrawDebugText( HUD, "Distance From Enemy: "$VSize(Pawn.Location - Enemy.Location)$"uu" );
}
if( MyKFPawn.IsDoingSpecialMove() )
{
C.SetDrawColor( 255, 25, 50, 255 );
DrawDebugText( HUD,"[Doing Special Move: "$MyKFPawn.SpecialMove$"]" );
}
C.SetDrawColor( 0, 255, 50, 255 );
if( GetActiveCommand() != none )
{
DisplayString = "[Active Command: "$GetActiveCommand()$"]";
}
if( CommandList != none )
{
DisplayString = DisplayString$" [CommandList: "$CommandList$"]";
if( CommandList.ChildCommand != none )
{
DisplayString = DisplayString$" [ChildCommand: "$CommandList.ChildCommand$"]";
if( CommandList.ChildStatus != '' )
{
DisplayString = DisplayString$" [ChildStatus: "$CommandList.ChildStatus$"]";
}
}
}
if( DisplayString != "" )
{
DrawDebugText( HUD, DisplayString );
DisplayString = "";
}
CurrentSeq = MyKFPawn.BodyStanceNodes[EAS_FullBody].GetPlayedAnimation();
if( CurrentSeq != '' )
{
for( i = 0; i < KFPAG.Attacks.Length; i++ )
{
for( x = 0; x < KFPAG.Attacks[i].Anims.Length; x++ )
{
if( CurrentSeq == KFPAg.Attacks[i].Anims[x] )
{
DisplayString = "[FULLBODY] Playing "$string(CurrentSeq)$" [Tag "$KFPAG.Attacks[i].Tag$"] [Min: "$KFPAG.Attacks[i].MinDistance$"uu Max: "$KFPAG.Attacks[i].MaxDistance$"uu]";
break;
}
if( DisplayString != "" )
{
break;
}
}
}
if( DisplayString != "" )
{
DrawDebugText( HUD, DisplayString );
DisplayString = "";
}
}
}
/*********************************************************************************************
* Overhead Debug Text
********************************************************************************************* */
function bool IsDebugTextCategoryEnabled( name CategoryName )
{
return( EnabledDebugTextCategories.Find('All') != -1 || EnabledDebugTextCategories.Find(CategoryName) != -1 );
}
simulated function DrawDebug( KFHUDBase HUD, name Category )
{
local int CmdCnt;
local Canvas C;
local GameAICommand Cmd;
//local String Text;
local AICmdHistoryItem Item;
local bool bDrawDebugCommandStack, bDrawDebugCommandHistory;
local bool bDrawDebugAllPlugins, bDrawDebugPlugInHistory;
local string AddTxt, NullTxt;
local AICommand AC;
//local float Aggression;
bDrawDebugCommandStack = false;
bDrawDebugCommandHistory = false;
bDrawDebugAllPlugins = false;
if( Category == 'Default' || Category == 'All' || Category == 'PlugIns' )
{
bDrawDebugAllPlugins = true;
bDrawDebugPlugInHistory = true;
}
// if( Category == 'Default' || Category == 'All' || Category == 'OverheadNames' )
// {
// bDrawDebugCommandStack = false;
// bDrawDebugCommandHistory = false;
// Icon = Texture2D'ENG_EditorResources_TEX.AI.S_AI';
// DrawIconOverhead(HUD, Icon);
// return;
// }
// if( bDebug_ShowViewCone )
// {
// if( MyKFPawn != None )
// {
// tmp = MyKFPawn.GetPawnViewLocation();
// rot = MyKFPawn.GetBaseAimRotation();
// }
// DrawDebugCone(tmp ,vector( rot),Pawn .SightRadius, Acos(Pawn .PeripheralVision), Acos(Pawn .PeripheralVision),16,MakeColor(255,0,0,255));
// }
//return;
NullTxt = "None";
// Draw list of commands down the side of the screen
if( Pawn != None && Category == 'All' )
{
C = HUD.Canvas;
C.SetOrigin(0,0);
C.Font = class'Engine'.Static.GetSmallFont();
C.SetPos(C.SizeX * 0.05f, C.SizeY * 0.25f);
if( bEnemyIsVisible )
{
DrawDebugText( HUD, "bEnemyIsVisible: "$bEnemyIsVisible, MakeColor(0,255,0,255) );
}
else
{
DrawDebugText( HUD, "bEnemyIsVisible: "$bEnemyIsVisible );
}
// C.SetDrawColor(0, 255, 0, 255);
// if( InLatentExecution( LATENT_MOVETOWARD ) )
// {
// C.SetDrawColor(225, 255, 0, 255);
// DrawDebugText( HUD, String(self)$" IN LATENT MOVETOWARD" );
// C.SetDrawColor(225, 0, 0, 255);
// }
if( bDrawDebugCommandStack )
{
// WRITE OUT COMMAND STACK
C.SetDrawColor(255, 255, 255, 255);
DrawDebugText( HUD, "************************************************************" );
C.SetDrawColor(0, 0, 255, 255);
DrawDebugText( HUD, "COMMAND STACK" );
foreach AllCommands( class'GameAICommand', Cmd )
{
if( Cmd.ChildCommand == None )
{
C.SetDrawColor(225, 0, 0, 255);
}
else
{
C.SetDrawColor(0, 255, 0, 255);
}
DrawDebugText( HUD, String(Cmd.Name)@":"@Cmd.GetStateName() );
// Text = Cmd.GetDebugVerboseText();
// if( Len(Text) > 0 )
// {
// DrawDebugText( HUD, ".........."@Text );
// }
CmdCnt++;
}
if( CmdCnt == 0 )
{
DrawDebugText( HUD, "NO COMMANDS ACTIVE" );
}
}
if( bDrawDebugCommandHistory )
{
// WRITE OUT COMMAND HISTORY
// C.SetDrawColor(255, 255, 255, 255);
DrawDebugText( HUD, "************************************************************" );
//C.SetDrawColor(0, 0, 255, 255);
DrawDebugText( HUD, "COMMAND HISTORY (Count:"@CommandHistoryNum$")" );
CmdCnt = 0;
foreach CommandHistory( Item )
{
CmdCnt++;
// C.SetDrawColor(255, 0, 0, 255);
DrawDebugText( HUD, "Cmd"@CmdCnt$":"@String(Item.CmdClass)@"Time:"@Item.TimeStamp);
if( Len(Item.VerboseString) > 0 )
{
// C.SetDrawColor(255, 64, 64, 255);
DrawDebugText( HUD, ".............."@Item.VerboseString );
}
}
}
C.Font = class'Engine'.Static.GetSmallFont();
if( MyKFPawn != none )
{
//C.SetDrawColor(0, 255, 0, 255);
DrawDebugText( HUD, string(Pawn) );//, MakeColor(0,255,0,255) );
AddTxt = "ActiveCommand: ";
if( CachedAICommandList != None )
{
AC = CachedAICommandList;
if( AC != none )
{
AddTxt = AddTxt@AC;
if( AC.CachedChildCommand != none )
{
AddTxt = AddTxt@"Child1: "$AC.CachedChildCommand;
if( AC.CachedChildCommand.CachedChildCommand != none )
{
AddTxt = AddTxt@"Child2: "$AC.CachedChildCommand.CachedChildCommand;
}
}
}
}
else
{
AddTxt = AddTxt@NullTxt;
}
DrawDebugText( HUD, AddTxt );
if( IsDoingLatentMove() )
{
DrawDebugText( HUD, "Latent Action: MOVE", MakeColor(200,0,0) );
}
else
{
DrawDebugText( HUD, "Latent Action: None" );
}
DrawDebugText( HUD, AddTxt );
DrawDebugText( HUD, "Floor: "$Pawn.Floor$" DesiredRot: "$Pawn.DesiredRotation$" PawnRot: "$Pawn.Rotation$" KFAIC Rot: "$Rotation );
DrawDebugText( HUD, "Velocity: "$VSize(Pawn.Velocity)$" Accel: "$VSize(Pawn.Acceleration)$" Physics: "$Pawn.GetPhysicsName() );
DrawDebugText( HUD, "Direct Path: "$bDirectMoveToGoal$" bPreparingMove: "$bPreparingMove$" GroundSpeed: "$Pawn.GroundSpeed );
DrawDebugText( HUD, "bPreciseDest: "$bPreciseDestination$" bForceMaxAccel: "$MyKFPawn.bForceMaxAccel$" MoveOffset: "$MoveOffset );
AddTxt = "Anchor:";
if( Pawn.Anchor != none )
{
AddTxt = AddTxt@MyKFPawn.Anchor;
}
else
{
AddTxt = AddTxt@"None";
}
if( RouteCache.Length > 0 )
{
AddTxt = AddTxt@" RouteCache[0]:"$RouteCache[0];
}
AddTxt = "SpecialMove:";
if( MyKFPawn.IsDoingSpecialmove() )
{
AddTxt = AddTxt@MyKFPawn.SpecialMoves[MyKFPawn.SpecialMove];
}
else
{
AddTxt = AddTxt@NullTxt;
}
AddTxt = AddTxt@"MoveTarget:";
if( MoveTarget != none )
{
AddTxt = AddTxt@MoveTarget;
}
else
{
AddTxt = AddTxt@NullTxt;
}
AddTxt = AddTxt@"Enemy:";
if( Enemy != none )
{
AddTxt = AddTxt@Enemy;
}
else
{
AddTxt = AddTxt@NullTxt;
}
DrawDebugText( HUD, AddTxt );
AddTxt = "Sprinting: "$MyKFPawn.bIsSprinting$" FocalPoint: "$GetFocalPoint()$" Focus:";
if( Focus != none )
{
AddTxt = AddTxt@Focus;
}
else
{
AddTxt = AddTxt@NullTxt;
}
AddTxt = AddTxt@":";
if( MyKFPawn.bIsHeadTrackingActive && MyKFPawn.MyLookAtInfo.LookAtTarget != none )
{
AddTxt = AddTxt@MyKFPawn.MyLookAtInfo.LookAtTarget;
}
else
{
AddTxt = AddTxt@NullTxt;
}
DrawDebugText( HUD, AddTxt );
if( Enemy != none )
{
DrawDebugText( HUD, "EnemyDist: "$VSize(Enemy.Location - Pawn.Location)$" RouteCache: "$RouteCache.Length$" DestinationOffset: "$Pawn.DestinationOffset );
DrawDebugText( HUD, "MeleeEvents Enabled: "$bIsProbingMeleeRangeEvents$" Within Melee Range ["$StrikeRange$"]" );
DrawDebugText( HUD, "DotToEnemy: "$Normal(MyKFPawn.Location - Enemy.Location) dot vector(Enemy.Rotation) );
}
if( Pawn.IsDesiredRotationInUse() )
{
DrawDebugText( HUD, "DesiredRotation In Use: TRUE", MakeColor(255,0,0,255) );
}
if( Pawn.IsDesiredRotationLocked() )
{
DrawDebugText( HUD, "DesiredRotation is locked", MakeColor(255,0,0,255) );
}
if( bHasDebugCommand )
{
FindCommandOfClass(class'AICommand_Debug').DrawDebug( HUD, Category );
}
}
}
if( bDrawDebugAllPlugins )
{
if( KfMovementPlugin != none )
{
KfMovementPlugin.DrawDebugToHud( Hud, Category );
}
if( KfLeapPlugin != none )
{
KfLeapPlugin.DrawDebugToHud( Hud, Category );
}
if( KfMovementPlugin != none )
{
KfStuckFixPlugin.DrawDebugToHud( Hud, Category );
}
}
if( bDrawDebugPlugInHistory )
{
MyAiPlugInHistory.DrawDebugToHud( Hud, Category );
}
}
function BeginDebugCommand()
{
AbortCommand( None, class'AICommand' );
class'AICommand_Debug'.static.Debug( self );
}
function DoDebugTurnInPlace( KFPlayerController KFPC, optional bool bAllowMelee=false )
{
AbortCommand( None, class'AICommand' );
class'AICommand_DebugTurn'.static.DebugTurnInPlace( self, bAllowMelee, KFPC );
}
/*********************************************************************************************
* TakeDamage
********************************************************************************************* */
/** To override in subclasses */
function bool IsAggroEnemySwitchAllowed()
{
if( LastEnemySwitchTime > 0.f && `TimeSince(LastEnemySwitchTime) < AggroEnemySwitchWaitTime )
{
return false;
}
return true;
}
/**
*
* @param HUD Player HUD
*/
function DrawDebugOverheadMovementPhaseData( KFHUDBase HUD, Out Vector2d ScreenPos )
{
local Texture2D Icon;
local PlayerController PC;
local Canvas Canvas;
local Vector CameraLoc, ScreenLoc;
local Rotator CameraRot;
local float X, Y;
local float DOT;
if( MyKFPawn == none || MyKFPawn.Health <= 0 )
{
return;
}
if( HUD.ShouldDisplayDebug('AIPathing') || HUD.ShouldDisplayDebug('AllVerbose'))
{
HUD.DrawRoute(MyKFPawn);
}
Canvas = HUD.Canvas;
ScreenLoc = Canvas.Project( Pawn.Location + vect(0,0,1) * Pawn.GetCollisionHeight() * 1.5f );
if( ScreenLoc.X < 0 || ScreenLoc.X >= HUD.Canvas.ClipX || ScreenLoc.Y < 0 && ScreenLoc.Y >= HUD.Canvas.ClipY )
{
return;
}
PC = HUD.PlayerOwner;
Canvas.SetDrawColor(255,255,255);
PC.GetPlayerViewPoint( CameraLoc, CameraRot );
Dot = vector( CameraRot ) dot ( Pawn.Location - CameraLoc );
if( Dot < 0.5f )
{
return;
}
if( CurrentMovementPhase == MOVEMENT_PHASE_TYPE_PATHNODE_NORMAL_MOVE_TO )
{
Icon = MovementPhaseTypePathNodeNormalMoveToIcon;
}
else if( CurrentMovementPhase == MOVEMENT_PHASE_TYPE_PATHNODE_DELAY_MOVE )
{
Icon = MovementPhaseTypePathNodeDelayMoveIcon;
}
else if( CurrentMovementPhase == MOVEMENT_PHASE_TYPE_PATHNODE_POINT_MOVE_TO )
{
Icon = MovementPhaseTypePathNodeMoveToPointIcon;
}
else if( CurrentMovementPhase == MOVEMENT_PHASE_TYPE_PATHNODE_ROTATE_TO_FOCUS )
{
Icon = MovementPhaseTypePathNodeRotateToFocusIcon;
}
else if( CurrentMovementPhase == MOVEMENT_PHASE_TYPE_PATHNODE_FAILED_MOVE )
{
Icon = MovementPhaseTypePathNodeMoveFailedIcon;
}
else if( CurrentMovementPhase == MOVEMENT_PHASE_TYPE_NAV_MESH_NORMAL_MOVE_TO )
{
Icon = MovementPhaseTypeNavMeshNormalMoveToIcon;
}
else if( CurrentMovementPhase == MOVEMENT_PHASE_TYPE_FINAL_DEST_MOVE_TOWARDS )
{
Icon = MovementPhaseTypeFinalDestMoveTowardsIcon;
}
else if( CurrentMovementPhase == MOVEMENT_PHASE_TYPE_EMT_LOS_MOVE_TOWARDS )
{
Icon = MovementPhaseTypeEMT_LOS_MoveTowardsIcon;
}
else if( CurrentMovementPhase == MOVEMENT_PHASE_TYPE_MOVE_TOO_MESH_USING_PATH_NODES )
{
Icon = MovementPhaseTypeMovingToNavMeshUsingPathNodesIcon;
}
else if( CurrentMovementPhase == MOVEMENT_PHASE_TYPE_FALLBACK_FIND_NEARBY_MESH_POINT_MOVE_TO_DIRECT_NON_PATH_POS )
{
Icon = MovementPhaseTypeFALLBACK_FIND_NEARBY_MESH_POINT_MOVE_TO_DIRECT_NON_PATH_POSIcon;
}
else if( CurrentMovementPhase == MOVEMENT_PHASE_TYPE_FALLBACK_REFUSED_TO_BE_EXPLOTIED_FIND_NEARBY_MESH_POINT_MOVE_TO_DIRECT_NON_PATH_POS )
{
Icon = MovementPhaseTypeFALLBACK_REFUSED_TO_BE_EXPLOTIED_FIND_NEARBY_MESH_POINT_MOVE_TO_DIRECT_NON_PATH_POSIcon;
}
else if( CurrentMovementPhase == MOVEMENT_PHASE_TYPE_STUCK )
{
if( TypeOfMovementStuckOn == MOVE_FAILURE_TYPE_NO_NAV_MESH_PATH )
{
Icon = TypeOfMovementStuckOnMOVE_FAILURE_TYPE_NO_NAV_MESH_PATHIcon;
}
else if( TypeOfMovementStuckOn == MOVE_FAILURE_TYPE_SAME_INTERMEDIATE_POINT_TOO_MANY_TIMES )
{
Icon = TypeOfMovementStuckOnMOVE_FAILURE_TYPE_SAME_INTERMEDIATE_POINT_TOO_MANY_TIMESIcon;
}
else if( TypeOfMovementStuckOn == MOVE_FAILURE_TYPE_TARGET_OFF_NAV_MESH_AND_CAN_NOT_FIND_LOCAITON_NEAR_THEM_I_CAN_MOVE_TO )
{
Icon = TypeOfMovementStuckOnMOVE_FAILURE_TYPE_TARGET_OFF_NAV_MESH_AND_CAN_NOT_FIND_LOCAITON_NEAR_THEM_I_CAN_MOVE_TOIcon;
}
else if( TypeOfMovementStuckOn == MOVE_FAILURE_TYPE_LOOKING_FOR_BETTER_INTERMEDIATE_LOCATION )
{
Icon = TypeOfMovementStuckOnLookingForBetterIntermediateLoc;
}
else if( TypeOfMovementStuckOn == MOVE_FAILURE_TYPE_MOVING_TO_BETTER_INTERMEDIATE_LOCATION )
{
Icon = TypeOfMovementStuckOnMoveToBetterIntermediate;
}
else
{
Icon = TypeOfMovementStuckOnUnknownWhyIcon;
}
}
else
{
Icon = MovementPhaseTypeUnknownIcon;
}
if (Icon != None)
{
Canvas.SetPos( ScreenLoc.X - Icon.SizeX / 2, ScreenLoc.Y - Icon.SizeY / 2, ScreenLoc.Z );
Canvas.DrawTexture( Icon, 1.f );
X = ScreenLoc.X + Icon.SizeX/2 + 5;
Y = ScreenLoc.Y - Icon.SizeY/2;
}
else
{
X = ScreenLoc.X;
Y = ScreenLoc.Y;
}
Canvas.SetPos( X, Y );
ScreenPos.X = Canvas.CurX;
ScreenPos.Y = Canvas.CurY;
//Str = "#"$GetRightMost(self);
Canvas.Font = class'Engine'.Static.GetSmallFont();
if( KfMovementPlugin != none )
{
KfMovementPlugin.DrawDebugToHud( HUD, 'Recast' );
}
if( KfStuckFixPlugin != none )
{
KfStuckFixPlugin.DrawDebugToHud( HUD, 'Recast' );
}
if( KfLeapPlugin != none )
{
KfLeapPlugin.DrawDebugToHud( HUD, 'Recast' );
}
}
simulated function DrawBehaviorTreeIconOverhead(KFHUDBase HUD)
{
//@deprecated with __TW_BASEAI_LEAN_
}
/*********************************************************************************************
* Evade / Evasion / Blocking
**********************************************************************************************/
/** Returns a reaction delay only if the projectile/weapon can be evaded */
function bool GetDangerEvadeDelay( Name InstigatorClassName, out float ReactionDelay, out byte ForcedEvadeDir, out byte bShouldBlock )
{
local int Index, D;
local sBlockInfo BlockSettings;
Index = DangerEvadeSettings.Find( 'ClassName', InstigatorClassName );
if( Index != INDEX_NONE )
{
bShouldBlock = 0;
// Cache difficulty
D = WorldInfo.Game.GetModifiedGameDifficulty();
// Check cooldown
if( DangerEvadeSettings[Index].LastEvadeTime > 0.f
&& `TimeSince(DangerEvadeSettings[Index].LastEvadeTime) < DangerEvadeSettings[Index].Cooldowns[D] )
{
return false;
}
// Check evade chance
if( D >= DangerEvadeSettings[Index].EvadeChances.Length
|| fRand() > DangerEvadeSettings[Index].EvadeChances[D] * (WorldInfo.Game.NumPlayers == 1 ? DangerEvadeSettings[Index].SoloChanceMultiplier : 1.f) )
{
// Check block chance
if( D < DangerEvadeSettings[Index].BlockChances.Length )
{
BlockSettings = MyKFPawn.GetBlockSettings();
if( `TimeSince(MyKFPawn.LastBlockTime) >= BlockSettings.Cooldown
&& fRand() < DangerEvadeSettings[Index].BlockChances[D] * (WorldInfo.Game.NumPlayers == 1 ? BlockSettings.SoloChanceMultiplier : 1.f) )
{
// We are blocking
bShouldBlock = 1;
}
else
{
return false;
}
}
else
{
return false;
}
}
// Check forced evade chances
if( bShouldBlock == 0 )
{
ForcedEvadeDir = DIR_None;
if( DangerEvadeSettings[Index].ForcedEvadeChances.Length > D )
{
if( fRand() < DangerEvadeSettings[Index].ForcedEvadeChances[D].FL )
{
ForcedEvadeDir = DIR_ForwardLeft;
}
else if( fRand() < DangerEvadeSettings[Index].ForcedEvadeChances[D].FR )
{
ForcedEvadeDir = DIR_ForwardRight;
}
}
// Set our delay
ReactionDelay = RandRange( DangerEvadeSettings[Index].ReactionDelayRanges[D].X, DangerEvadeSettings[Index].ReactionDelayRanges[D].Y );
}
// Set our evade time for cooldowns
DangerEvadeSettings[Index].LastEvadeTime = WorldInfo.TimeSeconds;
return true;
}
return false;
}
/** Receives a melee warning including direction of attack and direction from pawn to attacker */
function ReceiveMeleeWarning( EPawnOctant MeleeDir, vector ProjectionToAttacker, Pawn Attacker )
{
local vector NormalDir;
local EPawnOctant AttackerDir;
local byte BestDir;
// Early out if blocking isn't possible
if( !MyKFPawn.CanBlock() )
{
return;
}
NormalDir = Normal( ProjectionToAttacker );
// No blocking attacks from behind
if( vector(MyKFPawn.Rotation) dot NormalDir < MyKFPawn.GetMinBlockFOV() )
{
return;
}
BestDir = DIR_None;
// Start with which direction attack is coming from
AttackerDir = class'KFPawn'.static.CalcQuadRegion( MyKFPawn.Rotation, NormalDir );
if( AttackerDir == DIR_Left || AttackerDir == DIR_Right )
{
BestDir = AttackerDir;
}
else if( AttackerDir == DIR_Forward )
{
// If we're being attacked from the front, use melee direction to determine block
BestDir = MeleeDir == DIR_Left ? DIR_Right : DIR_Left;
}
if( BestDir != DIR_None )
{
MyKFPawn.DoSpecialMove( SM_Block, MyKFPawn.bIsBlocking,, class'KFSM_Block'.static.PackBlockSMFlags(BestDir) );
}
}
/** Deprecated, doesn't work well for our purposes */
function ReceiveProjectileWarning( Projectile Proj );
/** Warns the AI that it needs to evade from a particular location */
function ReceiveLocationalWarning( vector DangerPoint, vector DangerInstigatorLocation, optional Object EvadeCauser )
{
local byte BestDir;
local float ReactionDelay;
local byte bWantsBlock;
if( MyKFPawn == none )
{
return;
}
if( CanEvade() || MyKFPawn.CanDoSpecialMove(SM_Block) )
{
// Make sure we're not evading something we can't even see
if( vector(MyKFPawn.Rotation) dot Normal(DangerInstigatorLocation - MyKFPawn.Location) < 0.1f )
{
return;
}
// Attempt to look up our danger evade settings
if( EvadeCauser != none )
{
// Returns false if it has no entry, is on cooldown, or it failed the evade chance check
if( !GetDangerEvadeDelay(EvadeCauser.class.name, ReactionDelay, BestDir, bWantsBlock) )
{
return;
}
}
else
{
// Make sure we always have a valid reaction delay
ReactionDelay = RandRange( 0.f, 0.2f );
}
// Do blocks first
if( bWantsBlock == 1 )
{
BestDir = class'KFPawn'.static.CalcQuadRegion( MyKFPawn.Rotation, DangerPoint - MyKFPawn.Location );
if( BestDir != DIR_None )
{
MyKFPawn.DoSpecialMove( SM_Block, MyKFPawn.bIsBlocking,, class'KFSM_Block'.static.PackBlockSMFlags(BestDir) );
}
return;
}
// Calculate evade direction if there was no forced direction
if( BestDir == DIR_None )
{
BestDir = GetBestEvadeDir( DangerPoint,, true );
}
if( BestDir != DIR_None )
{
DoEvade( BestDir,, DangerInstigatorLocation, ReactionDelay, true );
}
}
}
function NotifyTakeHit( Controller InstigatedBy, vector HitLocation, int Damage, class<DamageType> damageType, vector Momentum )
{
local sBlockInfo BlockSettings;
local byte BestDir;
super.NotifyTakeHit( InstigatedBy, HitLocation, Damage, damageType, Momentum );
if( MyKFPawn == none )
{
return;
}
UpdateLasDamageTime();
// See if we should trigger our block
if( !MyKFPawn.bIsBlocking )
{
BlockSettings = MyKFPawn.GetBlockSettings();
// If there's no blocking chance, early out
if( BlockSettings.Chance > 0.f )
{
AccumulatedBlockDamage += Damage;
// If accumulated damage has reached or exceeded the threshold, attempt to trigger a block
if( AccumulatedBlockDamage >= MyKFPawn.HealthMax * BlockSettings.DamagedHealthPctToTrigger
&& MyKFPawn.Physics == PHYS_Walking
&& MyKFPawn.IsCombatCapable()
&& MyKFPawn.CanBlock() )
{
AccumulatedBlockDamage = 0;
// Get direction we should block
BestDir = class'KFPawn'.static.CalcQuadRegion( MyKFPawn.Rotation, Momentum );
MyKFPawn.DoSpecialMove( SM_Block, MyKFPawn.bIsBlocking,, class'KFSM_Block'.static.PackBlockSMFlags(BestDir) );
return;
}
}
}
// See if we should trigger our evade
if( EvadeOnDamageSettings.Chance > 0.f )
{
AccumulatedEvadeDamage += Damage;
// If accumulated damage has reached or exceeded the threshold, attempt to trigger an evade
if( AccumulatedEvadeDamage >= MyKFPawn.HealthMax * EvadeOnDamageSettings.DamagedHealthPctToTrigger
&& fRand() < EvadeOnDamageSettings.Chance
&& CanEvade() )
{
AccumulatedEvadeDamage = 0;
// Get direction we should evade
BestDir = DIR_None;
if( EvadeOnDamageSettings.ForcedEvadeChance.FL > 0.f && fRand() < EvadeOnDamageSettings.ForcedEvadeChance.FL )
{
BestDir = DIR_ForwardLeft;
}
else if( EvadeOnDamageSettings.ForcedEvadeChance.FR > 0.f && fRand() < EvadeOnDamageSettings.ForcedEvadeChance.FR )
{
BestDir = DIR_ForwardRight;
}
// If we don't have a forced evade direction, pick one based on the hit location
if( BestDir == DIR_None )
{
BestDir = GetBestEvadeDir( HitLocation,, true );
}
if( BestDir != DIR_None )
{
DoEvade( BestDir,, (InstigatedBy != none && InstigatedBy.Pawn != none) ? InstigatedBy.Pawn.Location : vect(0,0,0), RandRange(0.f, 0.2f), true );
}
}
}
}
function UpdateLasDamageTime()
{
LastDamageTime_Taken = WorldInfo.TimeSeconds;
}
/** Notification that we've been damaged by a friendly AI */
function NotifyFriendlyAIDamageTaken( Controller DamagerController, int Damage, Actor DamageCauser, class<KFDamageType> DamageType )
{
local int Idx;
local Pawn BlockerPawn;
if (DamageType == none || DamageType.default.bIgnoreAggroOnDamage)
{
return;
}
// Retrieves the index and, if necessary, creates a new entry
Idx = UpdateFriendlyDamageHistory( DamagerController, Damage );
if( Idx == INDEX_NONE )
{
return;
}
if( IsAggroEnemySwitchAllowed()
&& DoorEnemy == none
&& PendingDoor == none
&& DamagerController.Pawn != Enemy
&& FriendlyDamageHistory[Idx].Damage >= float(Pawn.HealthMax) * AggroZedHealthPercentage )
{
BlockerPawn = GetPawnBlockingPathTo( DamagerController.Pawn );
if( BlockerPawn == none )
{
SetEnemyToZed( DamagerController.Pawn );
}
}
}
/** Updates our friendly damage history, and returns the index in the damage history array */
native function int UpdateFriendlyDamageHistory( Controller DamagerController, int Damage );
/** Called from KFProjectile after certain impact criteria is met, to warn AI of a pending explosion */
function DoProjectileWarning( KFProjectile KFProj )
{
if( MyKFPawn == none || MyKFPawn.Health <= 0 || (!MyKFPawn.CanDoSpecialMove(SM_Evade) && !MyKFPawn.CanDoSpecialMove(SM_Evade_Fear)) )
{
return;
}
HandleProjectileWarning( KFProj );
}
function HandleProjectileWarning( Projectile Proj )
{
if( MyKFPawn == none || MyKFPawn.Health <= 0 || (!MyKFPawn.CanDoSpecialMove(SM_Evade) && !MyKFPawn.CanDoSpecialMove(SM_Evade_Fear)) )
{
return;
}
if(CanEvadeGrenade() && Proj.IsA('KFProj_Grenade'))
{
// if we're looking in that direction and skilled enough, try an evasive maneuver. TODO: Add KFProjectile flag to check instead of IsA.
if ( !IsTimerActive(nameof(Timer_DoProjectileEvade)) &&
(vector(Pawn.Rotation) dot Normal(Proj.Instigator.Location - Pawn.Location) > 0.7) )
{
//TimeToImpact = Proj.GetTimeToLocation(Pawn.Location);
PendingEvadeProjectile = Proj;
MyKFPawn.SetHeadTrackTarget( PendingEvadeProjectile,, 1.f );
SetTimer( (0.265 + FRand() * 0.12f), false, nameof(Timer_DoProjectileEvade) );
}
}
}
function bool CanEvadeGrenade()
{
return (FRand() < EvadeGrenadeChance);
}
/** triggers an evade from the PendingEvadeProjectile, if possible and it's still on target */
final function Timer_DoProjectileEvade()
{
local byte BestDir;
/** When evading projectiles, headtracking is very briefly enabled when this timer is started, and ended here within
the timer. This helps to make the NPC reaction look a little more realistic, turning its head toward the threat right
before responding to it. */
MyKFPawn.SetHeadTrackTarget( none );
if( MyKFPawn != none && (MyKFPawn.CanDoSpecialMove(SM_Evade) || MyKFPawn.CanDoSpecialMove(SM_Evade_Fear)) && PendingEvadeProjectile != none && !PendingEvadeProjectile.bDeleteMe
&& !IsZero(PendingEvadeProjectile.Velocity) && CanEvade() )
{
/** Find the safest direction to evade in - away from the threat and hopefully not into a larger threat or obstruction. */
BestDir = GetBestEvadeDir( PendingEvadeProjectile.Location, PendingEvadeProjectile.Instigator );
if( BestDir != DIR_None )
{
DoEvade( BestDir, PendingEvadeProjectile,, 0.1f + FRand() * 0.2f, true );
}
}
PendingEvadeProjectile = None;
}
/** Returns true if my pawn is permitted to evade away from something. */
function bool CanEvade( optional bool bOverrideSpecialMove )
{
return MyKFPawn.Physics == PHYS_Walking
&& (MyKFPawn.CanDoSpecialMove(SM_Evade) || MyKFPawn.CanDoSpecialMove(SM_Evade_Fear))
&& (bOverrideSpecialMove || !MyKFPawn.IsDoingSpecialMove())
&& MyKFPawn.IsCombatCapable();
}
/** Pick best direction to evade */
final function byte GetBestEvadeDir( Vector DangerPoint, optional Pawn ThreatPawn, optional bool bUseFastTrace=true, optional bool bCross )
{
local vector X, Y, Z, VectToDanger, Offset, EvadeLocation, Extent;
local bool bLeftOpen, bRightOpen, bFrontOpen, bBackOpen;
local float DotX, CheckHeight;
local EPawnOctant EvadeDir;
if( bCross )
{
EvadeDir = class'KFPawn'.static.CalcOctagonRegion( MyKFPawn.GetViewRotation(), (Normal(ThreatPawn.Location - Pawn.Location) Cross vect(0,0,1)) );
return EvadeDir;
}
// Figure out which directions we can evade(Normal(IncomingKFP.Location - Pawn.Location) Cross vect(0,0,1))
EvadeDir = class'KFPawn'.static.CalcOctagonRegion( MyKFPawn.GetViewRotation(), normal(MyKFPawn.Location - DangerPoint) );
GetAxes( Pawn.Rotation, X, Y, Z );
switch( EvadeDir )
{
case DIR_ForwardLeft:
return DIR_ForwardLeft;
case DIR_ForwardRight:
return DIR_ForwardRight;
case DIR_BackwardLeft:
return DIR_BackwardLeft;
case DIR_BackwardRight:
return DIR_BackwardRight;
case DIR_Forward:
if( !bUseFastTrace || FastActorTrace( Pawn, Pawn.Location + 256.f * X, Pawn.Location, Pawn.GetCollisionExtent() * 0.5f ) )
{
return DIR_Forward;
}
break;
case DIR_Backward:
if( !bUseFastTrace || FastActorTrace( Pawn, Pawn.Location - 256.f * X, Pawn.Location, Pawn.GetCollisionExtent() * 0.5f ) )
{
return DIR_Backward;
}
break;
case DIR_Left:
if( !bUseFastTrace || FastActorTrace( Pawn, Pawn.Location - 256.f * Y, Pawn.Location, Pawn.GetCollisionExtent() * 0.5f ) )
{
return DIR_Left;
}
break;
case DIR_Right:
if( !bUseFastTrace || FastActorTrace( Pawn, Pawn.Location + 256.f * Y, Pawn.Location, Pawn.GetCollisionExtent() * 0.5f ) )
{
return DIR_Right;
}
break;
}
if( EvadeDir == DIR_None && ThreatPawn != none )
{
CheckHeight = (MyKFPawn.GetCollisionHeight() * 0.5) + MyKFPawn.MaxStepHeight;
GetAxes( MyKFPawn.Rotation, X, Y, Z );
VectToDanger = Normal(DangerPoint-Pawn.Location);
DotX = X DOT VectToDanger;
//DotY = Y DOT VectToDanger;
Extent = Pawn.GetCollisionExtent() * 0.5f;
// If dot is mostly forward or backward
if( DotX >= 0.7071f || DotX <= -0.7071f )
{
Offset = X * 300.f;
EvadeLocation = Pawn.Location - Offset;
bBackOpen = CanReachEvadeLocation( EvadeLocation, CheckHeight, Extent );
if( bBackOpen && DotX >= 0.7071 )
{
return DIR_Backward;
}
EvadeLocation = Pawn.Location + Offset;
bFrontOpen = CanReachEvadeLocation( EvadeLocation, CheckHeight, Extent );
if( bFrontOpen && DotX <= -0.7071 )
{
return DIR_Forward;
}
}
else
{
Offset = Y * 300.0f;
EvadeLocation = Pawn.Location + Offset;
bRightOpen = CanReachEvadeLocation( EvadeLocation, CheckHeight, Extent );
if( bRightOpen )
{
return DIR_Right;
}
EvadeLocation = Pawn.Location - Offset;
bLeftOpen = CanReachEvadeLocation( EvadeLocation, CheckHeight, Extent );
if( bLeftOpen )
{
return DIR_Left;
}
}
}
return DIR_None;
}
/** Checks to see if a potential location to evade to is obstructed */
function bool CanReachEvadeLocation( vector EvadeLocation, float CheckHeight, vector Extent )
{
if( `FastTracePhysX(EvadeLocation, Pawn.Location)
&& !FastTrace(EvadeLocation + (vect(0,0,-1) * CheckHeight), EvadeLocation, Extent) )
{
if( !IsPawnBlockingLine(Pawn.Location, EvadeLocation) )
{
return true;
}
}
return false;
}
/*********************************************************************************************
* Door handling (attacking, passing through)
********************************************************************************************* */
/** Notification that NPC needs to stop moving and wait for door */
event WaitForDoor( KFDoorActor Door )
{
`RecordAIDoor(`StatID(AI_WAITFORDOOR), self,Pawn.Location, Door, "Waiting at "$WorldInfo.TimeSeconds);
`AILog( GetFuncName()$"() Waiting for door "$Door$" to open or be destroyed", 'Doors' );
SetTimer( 5.f, true, nameof(Timer_WaitingForDoor) );
DoorEnemy = none;
PendingDoor = Door;
Door.bMonitorDoor = true;
bPreparingMove = true;
//MoveTimer = -1.0f; // To be enabled
StopAllLatentMovement(); // To be enabled
Pawn.Acceleration = vect(0,0,0);
// if( GetActiveCommand().IsA('AICommand_MoveToGoal') )
// {
// ForcePauseAndRepath();
// }
}
/** Debug timer set when NPC begins waiting for a door. Timer is cleared by DoorFinished(). Currently only
in use to find any NPCs who might become stuck indefinitely while waiting for a door to finish opening,
which hasn't been an issue lately. */
function Timer_WaitingForDoor()
{
`AILog( "** WARNING ** ["$GetFuncName()$"] I've been waiting for at least 10 seconds for door "$PendingDoor$" bPreparingMove: "$bPreparingMove$" (Command: "$CachedAICommandList$")", 'Doors' );
DoorFinished();
}
/** Door has finished opening, let the NPC move through - notification comes from the door itself if my
bMonitorDoor flag is true. */
function bool DoorFinished()
{
`AILog( GetFuncName()$"() door has finished opening or is destroyed!", 'Doors' );
if( Pawn == None || PendingDoor.MyMarker == None || PendingDoor.MyMarker.ProceedWithMove(Pawn) )
{
`RecordAIDoor(`StatID(AI_FINISHEDWAITFORDOOR), self,Pawn.Location, PendingDoor, "Waiting at "$WorldInfo.TimeSeconds);
`AILog( GetFuncName()$"() setting pending door to none, bPreparingMove to false. Proceeding with move.", 'Doors' );
PendingDoor = None;
// Notify command list that door is open
if( CachedAICommandList != None )
{
CachedAICommandList.NotifyDoorOpened();
}
ClearTimer( nameof(Timer_WaitingForDoor) );
return true;
}
`AILog( GetFuncName()$"() *** WARNING *** I received DoorFinished event but returned false! "$CachedAICommandList, 'Doors' );
return false;
}
/*********************************************************************************************
* ATTACKING DOORS/ACTORS INSTEAD OF PAWNS
* "Enemy" is a Pawn, so we also have "DoorEnemy" and "ActorEnemy".
*
* TODO: Get rid of DoorEnemy, and and replace it with ActorEnemy for attacking doors
**********************************************************************************************/
/** Notification that NPC needs to attack this door */
function NotifyAttackDoor( KFDoorActor Door )
{
local KFDoorMarker DoorMarker;
local int AttackerCount, QueuedCount;
local byte SMFlags;
DoorMarker = KFDoorMarker(Door.MyMarker);
if( DoorMarker != none )
{
Door.GetQueuedDoorAICounts( AttackerCount, QueuedCount );
// Force a repath if there are too many zeds queued at this door
if( AttackerCount + QueuedCount > 8 && DoorEnemy != Door )
{
DoorEnemy = none;
PendingDoor = none;
if( Enemy != none && Focus != Enemy )
{
Focus = none;
}
if( Enemy == none )
{
ChangeEnemy( GetClosestEnemy(), false );
}
// Update the cost of this door marker based on the number queued
DoorMarker.UpdatePathingCost( AttackerCount, QueuedCount );
// Tell our move commands that we're leaving
NotifyNeedRepath();
return;
}
else
{
// Update the cost of this door marker based on the number queued
DoorMarker.UpdatePathingCost( AttackerCount + (DoorEnemy != Door ? 1 : 0), QueuedCount );
}
}
`AILog( GetFuncName()$"() initializing AICommand_Attack_Melee, MoveTarget: "$MoveTarget$" Dist: "$VSize( Door.MyMarker.Location - Pawn.Location ), 'Doors' );
/** Sets the pending door for reference */
PendingDoor = Door;
/** DoorEnemy is used by attack code */
DoorEnemy = Door;
/** When opened (or destroyed), a KFDoorActor will notify any AIControllers (with "bMonitorDoor" true) that it's opened/closed status has changed */
Door.bMonitorDoor = true;
/** Prevents any latent movement while true */
bPreparingMove = true;
/** Stop moving and face the door */
StopAllLatentMovement();
AIZeroMovementVariables();
Focus = Door;
SMFlags = 255;
/** Try to get a valid door attack from my pawn's AnimInfo */
SMFlags = class'KFSM_DoorMeleeAttack'.static.PackSMFlags( MyKFPawn );
if( SMFlags != 255 )
{
class'AICommand_Attack_Melee'.static.Melee( self, Door, SMFlags );
}
else
{
class'AICommand_Attack_Melee'.static.Melee( self, Door );
}
}
/** Can be called to initiate a melee attack on a non-pawn attack target, like a destructible actor.
Currently called from FleshPound's Bump() if the bumped-into actor is destructible. */
function NotifyAttackActor( Actor A )
{
if( !MyKFPawn.IsDoingSpecialMove() )
{
ActorEnemy = A;
StopAllLatentMovement();
AIZeroMovementVariables();
bPreparingMove = true;
Focus = A;
class'AICommand_Attack_Melee'.static.Melee( self, A );
}
}
function bool CanAttackDestructibles()
{
return bCanDoHeavyBump;
}
/*********************************************************************************************
* Dead State
**********************************************************************************************/
State Dead
{
ignores SeePlayer, HearNoise, KilledBy;
function BeginState( name PreviousStateName )
{
`AILog( "DEAD", 'Damage' );
super.BeginState( PreviousStateName );
}
}
/*********************************************************************************************
* DebugState - Add "`DEBUGSTATE" to state declaration to make that state descend from
* this, which will then log any related state transitions for it, assuming you're
* calling the super version if you override any of these functions/events in your own
* state.
********************************************************************************************* */
state DebugState
{
function BeginState( Name PreviousStateName )
{
`AILog( "BEGINSTATE"@PreviousStateName, 'State' );
}
function EndState( Name NextStateName )
{
`AILog( "ENDSTATE"@NextStateName, 'State' );
}
function PushedState()
{
`AILog( "PUSHED", 'State' );
}
function PoppedState()
{
`AILog( "POPPED", 'State' );
}
function ContinuedState()
{
`AILog( "CONTINUED", 'State' );
}
function PausedState()
{
`AILog( "PAUSED", 'State' );
}
}
/** Log the NPC's routecache (list of navigation points to traverse for current path, if any) */
final function DebugLogRoute()
{
`if(`notdefined(ShippingPC))
local String RouteString;
local int i;
for( i = 0; i < RouteCache.Length; i++ )
{
RouteString = "DebugLogRoute()";
if( Pawn.Anchor != none )
{
RouteString = RouteString$" [Anchor:"$Pawn.Anchor$"][Dist:"$VSize(Pawn.Anchor.Location - Pawn.Location)$"]";
}
RouteString = RouteString$"[RC "$i$":"$RouteCache[i]$"]";
if( i + 1 < RouteCache.Length )
{
RouteString = RouteString$",";
}
}
if( RouteCache.Length == 0 )
{
RouteString = "Empty Route";
}
else
{
`AILog( "DebugLogRoute(): "$RouteString, 'PathWarning',,, true );
}
`endif
}
/*********************************************************************************************
* Action_Idle State - default state this controller uses while pawn is active
********************************************************************************************* */
state Action_Idle `DEBUGSTATE
{
event BeginState(Name PreviousStateName)
{
Super.BeginState( PreviousStateName );
Enable( 'SeePlayer' );
`AILog( "Entering Action_Idle() state, previous state was "$PreviousStateName, 'Action_Idle' );
if( Pawn != none )
{
`AILog( "Action_Idle BeginState() calling Pawn.ZeroMovementVariables()", 'Action_Idle' );
AIZeroMovementVariables();
}
}
function ContinuedState()
{
Super.ContinuedState();
if( Pawn != none )
{
`AILog( "Action_Idle ContinuedState() calling Pawn.ZeroMovementVariables()", 'Action_Idle' );
AIZeroMovementVariables();
}
}
event EndState( name NextStateName )
{
Super.EndState( NextStateName );
`AILog( "Ending Action_Idle state and going to "$NextStateName, 'Action_Idle' );
}
Begin:
Sleep(0.f);
// Wait until Emerge SpecialMove is complete before beginning state code
// 8/5/14: Wait for all special moves! (fixes knockdown bug)
// @todo: Needs AI review. Setting PHYS_Walking below is unsafe and causing
// special move errors. Doing this wait before WaitForLanding is also unsafe
if( MyKFPawn == none || MyKFPawn.Health <= 0 || MyKFPawn.IsDoingSpecialMove() )
{
Sleep( 0.1f );
Goto( 'Begin' );
}
`AILog( self$" -- Begin Label", 'Action_Idle' );
if( Pawn != none && Pawn.Physics == PHYS_Falling )
{
AIActionStatus = "Falling";
WaitForLanding();
}
if( !MyKFPawn.bCrawler || !bSpawnedByEmergeSpecialMove )
{
Pawn.SetPhysics( PHYS_Walking );
}
// Begins the debug command (can be customized) if flag is set.
if( bForceDebugCommand )
{
BeginDebugCommand();
// Stop state code execution here so that no other commands will be loaded.
Stop;
}
// Auto-acquire an enemy even if it's not visible to my pawn. This also will
// start up the MoveToEnemy command if a valid enemy is found.
Goto('IdleMoveToNearestEnemy');
IdleMoveToNearestEnemy:
if( bIdleMoveToNearestEnemy )
{
if( Physics == PHYS_Falling )
{
WaitForLanding();
}
//SetEnemy( GetClosestEnemy() );
if( !FindNewEnemy() )
{
// Fallback
SetEnemy( GetClosestEnemy() );
}
if( Enemy != none )
{
`AILog( self$" -- GetClosestEnemy() returned "$Enemy, 'Action_Idle' );
AIActionStatus = "Starting to hunt";
BeginCombatCommand( GetDefaultCommand(), "Initial aggressive move" );
Stop;
}
else
{
`AILog( self$" Pausing and going back to begin label - haven't found an enemy yet.", 'Action_Idle' );
AIActionStatus = "Can't find an enemy!";
// if( bSpawnedByEmergeSpecialMove || MySpawnVolume != none )
// {
// // Only sleep for a frame if spawned using Emerge special move
// Sleep( 0.f );
// }
// else
// {
Sleep( 0.5f + FRand() * 1.5f );
// }
}
}
else
{
`AILog( self$" Not moving yet because bIdleMoveToNearestEnemy is false", 'Action_Idle' );
Sleep( 0.25f + FRand() * 0.5f );
}
Goto('Begin');
}
function float GetMinDistanceToAnyPlayer()
{
local PlayerController PC;
local float MinRange;
local float CurRange;
MinRange = -1.f;
if( Pawn == none )
{
return -1.f;
}
ForEach WorldInfo.AllControllers( class'PlayerController', PC )
{
if ( PC != none && PC.ViewTarget != None )
{
CurRange = VSizeSq( PC.ViewTarget.Location - Pawn.Location );
if( CurRange < MinRange || MinRange < 0.f )
{
MinRange = CurRange;
}
}
}
return sqrt( MinRange );
}
function NotifyEnRaged( bool bEnraged );
//If this function returns true, use the AI controller's spawn enrage.
// If this is false, recommended to use the direct call into the pawn.
function bool SpawnEnraged()
{
return false;
}
function ResetKFAIC()
{
if( MyKFPawn != none && MyKFPawn.IsDoingSpecialMove() )
{
MyKFPawn.EndSPecialMove();
SetTimer( 0.1, false, nameof(ResetKFAIC), self );
return;
}
AIZeroMovementVariables();
StopLatentExecution();
StopAllLatentMovement();
Enemy = none;
DoorEnemy = none;
CachedVisibleEnemy = none;
MeleeTarget = none;
MoveTarget = none;
MoveGoal = none;
LastHitWall = none;
PendingDoor = none;
RouteGoal = none;
MoveFailureCount = 0;
FailedMoveToEnemyCount = 0;
HitWallCount = 0;
RouteCache.Length = 0;
LastTauntTime = 0.f;
LastNotifyHitWallTime = 0.f;
LastBumpTime = 0.f;
LastObstructionTime = 0.f;
MoveTimer = -1.f;
bHasDebugCommand = false;
bPreparingMove = false;
bAdjusting = false;
bReevaluatePath = false;
IsRotationLocked = false;
bEnemyIsVisible = false;
bReachedLatentMoveGoal = false;
LockedRotation = rot(0,0,0);
// Init AI Steering object in 1-3 seconds
SetTimer( RandRange(1, 3), false, nameof(StartSteering) );
BeginCombatCommand( None, "Reset" );
}
/*********************************************************************************************
* Victory
********************************************************************************************* */
/** Enter this state when the match is over and the zeds have won */
state ZedVictory
{
function bool GetIsInZedVictoryState()
{
return true;
}
function bool CanDoStrike()
{
return false;
}
event bool CanGrabAttack()
{
return false;
}
Begin:
DisableMeleeRangeEventProbing();
if(FRand() <= 0.6f)
{
if(FRand() <= 0.5f)
{
class'AICommand_TauntEnemy'.static.Taunt(self, none, TAUNT_EnemyKilled );
}
else
{
class'AICommand_TauntEnemy'.static.Taunt( self, none, TAUNT_Standard );
}
}
else
{
DoWander( Pawn,, true );
}
Sleep(0.f);
if( MyKFPawn == none || MyKFPawn.Health <= 0 || MyKFPawn.IsDoingSpecialMove() )
{
Sleep( 0.1f );
Goto( 'Begin' );
}
if( Pawn != none && Pawn.Physics == PHYS_Falling )
{
AIActionStatus = "Falling";
WaitForLanding();
}
Goto('Begin');
}
function bool GetIsInZedVictoryState()
{
return false;
}
function EnterZedVictoryState()
{
ClearMovementInfo();
if( CommandList != None )
{
AbortCommand( CommandList );
}
DisableMeleeRangeEventProbing();
ChangeEnemy(none);
MyKFPawn.SetSprinting( false );
GotoState( 'ZedVictory' , 'Begin');
}
/*********************************************************************************************
View and Rotation related functions
********************************************************************************************* */
// Use where the bot is actually looking from
simulated event GetPlayerViewPoint(out vector out_Location, out Rotator out_Rotation)
{
GetActorEyesViewPoint( out_Location, out_Rotation );
}
/*********************************************************************************************
* Enemy Threat Evaluations [EXPERIMENTAL, has not been in use, but if we want more intelligent
* NPC decisions regarding threat evaluation, the below two functions are worth trying out.
********************************************************************************************* */
/** Experimental - scores potential new enemy based on many conditions:
Distance
Is CheckPawn moving toward or away from me?
Am I currently visible to CheckPawn?
Have I recently taken damage from CheckPawn? */
event float EvaluateThreatFrom( Pawn CheckPawn, optional float EarlyOutScore )
{
local KFPawn Threat;
local float DistToThreat, ThreatScore;
local int RecentDamageFromThreat; //ZedsTargetingThreat;
local string outmsg;
local float modscore, Dot;
local vector Loc;
local rotator Rot;
local float DistScore;
local float ClosestDist;
local float Time;
Threat = KFPawn(CheckPawn);
RecentDamageFromThreat = MyKFPawn.RecentDamageFrom(Threat);
DistToThreat = VSize( Threat.Location - Pawn.Location );
// Count # of Zeds who are currently targeting CheckPawn and closer to CheckPawn than I am
//ZedsTargetingThreat = NumberOfZedsTargetingPawn( Threat, true, DistToThreat * 1.75f );
if( CheckPawn.IsHumanControlled() && CheckPawn.PlayerReplicationInfo != none )
{
outmsg = "["$CheckPawn.PlayerReplicationInfo.PlayerName$"] THREATSCORE";
}
else
{
outmsg = "["$CheckPawn$"] THREATSCORE";
}
if( DistToThreat < 2500.f )
{
ThreatScore += (2500.f - DistToThreat)/2500.f;
DistScore = ThreatScore;
}
if( MyKFPawn.IsPawnMovingTowardMe(Threat, 300.f) )
{
modscore = 0.12f * DistScore;
outmsg = outmsg$" [MOVING TOWARD ME SCORE: "$modscore$"]";
ThreatScore += 0.12f * DistScore;
GetClosestTimeAndDistToPawn(Threat, Time, ClosestDist);
if( Time > 0.f && Time < 2.5f && ClosestDist < 250.f )
{
modscore = (2.5f - Time)/2.5f;
ThreatScore += modscore;
outmsg = outmsg$" [TIME TO ENEMY: "$modscore$" TIME: "$Time$"]";
}
}
else if( MyKFPawn.IsPawnMovingAwayFromMe(Threat, 300.f) )
{
modscore = -0.15f * (1/DistScore);
outmsg = outmsg$" [MOVING AWAY SCORE: "$modscore$"]";
ThreatScore -= 0.15f * (1/DistScore);
}
if( Threat.Controller.CanSee(Pawn) )
{
Threat.Controller.GetPlayerViewPoint( Loc, Rot );
Dot = Normal( Pawn.Location - Loc ) dot vector(Rot);
if( Dot > 0.7f )
{
//modscore = 1.f - Dot;
modscore += Dot * DistScore;
ThreatScore += Dot * DistScore;
}
else
{
modscore = 0.1f;
ThreatScore -= 0.1f;
}
//msg( "[Dot: "$Dot$"] TEST: "$modscore$" versus further scaled "$modscore * (DistScore * 0.88f)$"... dist score was "$DistScore$" Dist: "$DistToThreat );
outmsg = outmsg$" [DOT: "$DOT$" AIMING AT SCORE: "$modscore$"]";
}
else
{
ThreatScore -= 0.12f * ( 1/DistScore );
modscore = 0.12f * (1/DistScore);
outmsg = outmsg$" [CANT SEE YOU PENALTY:"$modscore$" DIST SCORE WAS "$DistScore$"]";
}
if( RecentDamageFromThreat > 0 )
{
modscore = float(RecentDamageFromThreat)/float(MyKFPawn.HealthMax);
outmsg = outmsg$" [RECENT DAMAGE:"$RecentDamageFromThreat$" SCORE: "$modscore$"]";
ThreatScore += float(RecentDamageFromThreat)/float(MyKFPawn.HealthMax);
}
//VSize(Pawn.Velocity) > 0.f &&
//FLOAT DistanceRating = 1 - ActorToLookAt->LastKnownDistance/LookAtActorRadiusSq;
outmsg = "FINAL SCORE: "$ThreatScore$"..."$outmsg;
msg( Threat.PlayerReplicationInfo.PlayerName$": "$outmsg );
return ThreatScore;
}
/** if bDebug_ShowStrikeRange is true, draw the range of every zed around its pawn */
event DrawStrikeRanges()
{
local KFPawnAnimInfo KFPAI;
local byte i;
if( MyKFPawn != none && MyKFPawn.PawnAnimInfo != none )
{
KFPAI = MyKFPawn.PawnAnimInfo;
// Draw inactive ranges first
for( i = 0; i < MyKFPawn.PawnAnimInfo.Attacks.length; i++ )
{
if( i != PendingAnimStrikeIndex &&
(bDebug_ShowAllStrikeRange || KFPAI.AllowAttackByDifficulty(i) && KFPAI.AllowAttackByMovement( i, MyKFPawn )) )
{
// Give different colors for the different unused attacks
DrawStrikeRange(i, KFPAI, MakeColor(255 - i * 20, 0, i * 20, 255 ));
}
}
// Draw pending attack range
if( PendingAnimStrikeIndex != 255)
{
DrawStrikeRange(PendingAnimStrikeIndex, KFPAI, MakeColor(255,255,0,255));
}
// Draw the StrikeRange in black. This is the range between min and max multiplied by the StrikeRangePercentage
DrawStrikeRangeLines( MyKFPawn.Location, MyKFPawn.Rotation, 1.57, 0, StrikeRange, MakeColor(0,0,0,255) );
DrawStrikeRangeLines( MyKFPawn.Location, MyKFPawn.Rotation, 0, 1.57, StrikeRange, MakeColor(0,0,0,255) );
// Draw Current Attack
if( MyKFPawn != none && MyKFPawn.IsDoingSpecialMove(SM_MeleeAttack))
{
DrawStrikeRange(DebugCurrentStrikeIndex, KFPAI, MakeColor(0,255,0,255));
}
// Draw a line for the max hit range
DrawMaxHitRange(MakeColor(0,255,0,255));
}
}
function DrawStrikeRange( byte Index, KFPawnAnimInfo KFPAI, Color ZoneColor )
{
local float MinDistance, MaxDistance, AverageDistance, CollisionRadius;
local string TagString;
CollisionRadius = MyKFPawn.CylinderComponent.CollisionRadius;
MinDistance = ( KFPAI.Attacks[Index].MinDistance < CollisionRadius ) ? CollisionRadius : KFPAI.Attacks[Index].MinDistance;
MaxDistance = KFPAI.Attacks[Index].MaxDistance;
AverageDistance = KFPAI.GetMedianStrikeRange(Index, 0.5f, CollisionRadius);
TagString = String(KFPAI.Attacks[Index].Tag);
// Draws backwards
DrawStrikeRangeLines( MyKFPawn.Location, MyKFPawn.Rotation, 1.57, 0, MaxDistance, ZoneColor );
DrawStrikeRangeLines( MyKFPawn.Location, MyKFPawn.Rotation, 1.57, 0, MinDistance, ZoneColor );
// Draws forwards
DrawStrikeRangeLines( MyKFPawn.Location, MyKFPawn.Rotation, 0, 1.57, MaxDistance, ZoneColor );
DrawStrikeRangeLines( MyKFPawn.Location, MyKFPawn.Rotation, 0, 1.57, MinDistance, ZoneColor );
DrawAttackRangeText( Index, 40960, 2048, AverageDistance, TagString, ZoneColor);
}
event DrawStrikeRangeLines( Vector OrginOfStrikeZone,
Rotator RotOrginOfZone,
float StartAngleInRads,
float EndAngleInRads,
float RangeOfZone,
Color ZoneColor )
{
local Vector arcRightInnerLineEnd;
local Vector arcMidRightLineEnd;
local Vector arcFarRightLineEnd;
local Vector arcLeftInnerLineEnd;
local Vector arcMidLeftLineEnd;
local Vector arcFarLeftLineEnd;
local Rotator arcRightInnerEdge;
local Rotator arcMidRightEdge;
local Rotator arcFarRightEdge;
local Rotator arcLeftInnerEdge;
local Rotator arcMidLeftEdge;
local Rotator arcFarLeftEdge;
local int innerHorArcInUU;
local int farHorArcInUU;
local int midHorArcInUU;
local float verticalShiftForZoneLines;
verticalShiftForZoneLines = MyKFPawn.CylinderComponent.CollisionHeight;
OrginOfStrikeZone.Z = OrginOfStrikeZone.Z - verticalShiftForZoneLines;
innerHorArcInUU = StartAngleInRads * 10430.2192;
farHorArcInUU = EndAngleInRads * 10430.2192;
midHorArcInUU = ((farHorArcInUU - innerHorArcInUU) * 0.5) + innerHorArcInUU;
//
// Inner Right Edge
//
arcRightInnerEdge = MakeRotator(0, RotOrginOfZone.Yaw + innerHorArcInUU, 0);
arcRightInnerLineEnd = OrginOfStrikeZone+ Vector(arcRightInnerEdge) * RangeOfZone;
//
// far right edge
//
if( farHorArcInUU > 0 )
{
arcFarRightEdge = MakeRotator(0, RotOrginOfZone.Yaw + farHorArcInUU, 0);
arcFarRightLineEnd = OrginOfStrikeZone + Vector(arcFarRightEdge) * RangeOfZone;
//MyKFPawn.DrawDebugLine(OrginOfStrikeZone , arcFarRightLineEnd, ZoneColor.R, ZoneColor.G, ZoneColor.B );
}
else
{
midHorArcInUU = ( ( 32767 - innerHorArcInUU) * 0.5) + innerHorArcInUU;
arcFarRightEdge = MakeRotator(0, RotOrginOfZone.Yaw + 32767, 0);
arcFarRightLineEnd = OrginOfStrikeZone + Vector(arcFarRightEdge) * RangeOfZone;
}
//
// Mid right edge
//
arcMidRightEdge = MakeRotator(0, RotOrginOfZone.Yaw + midHorArcInUU, 0);
arcMidRightLineEnd = OrginOfStrikeZone + Vector(arcMidRightEdge) * RangeOfZone;
//
// Inner left Edge
//
arcLeftInnerEdge = MakeRotator(0, RotOrginOfZone.Yaw - innerHorArcInUU, 0);
arcLeftInnerLineEnd = OrginOfStrikeZone+ Vector(arcLeftInnerEdge) * RangeOfZone;
//
// Far left edge line
//
if( farHorArcInUU > 0 )
{
arcFarLeftEdge = MakeRotator(0, RotOrginOfZone.Yaw - farHorArcInUU, 0);
arcFarLeftLineEnd = OrginOfStrikeZone + Vector(arcFarLeftEdge) * RangeOfZone;
//MyKFPawn.DrawDebugLine( OrginOfStrikeZone , arcFarLeftLineEnd, ZoneColor.R, ZoneColor.G, ZoneColor.B );
}
else
{
arcFarLeftEdge = MakeRotator(0, RotOrginOfZone.Yaw - 32767, 0);
arcFarLeftLineEnd = OrginOfStrikeZone + Vector(arcFarLeftEdge) * RangeOfZone;
}
//
// Mid left edge line
//
arcMidLeftEdge = MakeRotator(0, RotOrginOfZone.Yaw - MidHorArcInUU, 0);
arcMidLeftLineEnd = OrginOfStrikeZone + Vector(arcMidLeftEdge) * RangeOfZone;
//
// connect far right to middle right to inner right
//
MyKFPawn.DrawDebugLine( arcFarRightLineEnd, arcMidRightLineEnd, ZoneColor.R, ZoneColor.G, ZoneColor.B );
MyKFPawn.DrawDebugLine( arcRightInnerLineEnd, arcMidRightLineEnd, ZoneColor.R, ZoneColor.G, ZoneColor.B );
//
// connect far left to middle left to inner left
//
MyKFPawn.DrawDebugLine( arcFarLeftLineEnd, arcMidLeftLineEnd, ZoneColor.R, ZoneColor.G, ZoneColor.B );
MyKFPawn.DrawDebugLine( arcLeftInnerLineEnd, arcMidLeftLineEnd, ZoneColor.R, ZoneColor.G, ZoneColor.B );
}
function DrawAttackRangeText( byte Index, float StartingAngle, float RotationInterval, float MedianMeleeRange, string Text, Color TextColor )
{
local Vector TextLocation;
local Rotator DrawDirection;
local int RotationOffset;
RotationOffset = RotationInterval * Index ;
StartingAngle = MyKFPawn.Rotation.Yaw - StartingAngle + RotationOffset;
DrawDirection = MakeRotator(0, StartingAngle, 0);
TextLocation = MyKFPawn.Location + Vector(DrawDirection) * MedianMeleeRange;
TextLocation.Z -= MyKFPawn.CylinderComponent.CollisionHeight;
// Draw the textfield in world
GetALocalPlayerController().AddDebugText(Text, WorldInfo, 0.02 * WorldInfo.TimeDilation, TextLocation, TextLocation, TextColor, true, true);
}
function DrawMaxHitRange( Color LineColor )
{
local Rotator ZedRotation;
local Vector LineStart, LineEnd;
// Draw the MeleeHitRange also
ZedRotation = MakeRotator(0, MyKFPawn.Rotation.Yaw, 0);
LineStart = MyKFPawn.Location;
LineStart.Z -= MyKFPawn.CylinderComponent.CollisionHeight;
LineEnd = LineStart + Vector(ZedRotation) * MyKFPawn.MeleeAttackHelper.GetMeleeRange();
MyKFPawn.DrawDebugLine( LineStart , LineEnd, LineColor.R, LineColor.G, LineColor.B );
}
/** Shows this KFAIController's AICommand stack on screen (see ShowAIInfo cheat) */
function GetCommandStack( HUD Hud, out array<string> OverheadTexts, out array<Color> OverheadColors )
{
local int CmdCnt;
local GameAICommand Cmd;
local String CommandText;
local bool bShowCommands, bShowMovement;
local int i;
local string T;
local AICommand_Wander WanderCommand;
if( HUD.ShouldDisplayDebug('AllVerbose') )
{
bShowCommands = true;
bShowMovement = true;
}
if( HUD.ShouldDisplayDebug('All') )
{
bShowCommands = true;
bShowMovement = true;
}
if( HUD.ShouldDisplayDebug('AICommands') )
{
bShowCommands = true;
}
if( HUD.ShouldDisplayDebug('AIMovement') || HUD.ShouldDisplayDebug('AIPathing') )
{
bShowMovement = true;
}
if( MyKFPawn == none )
{
return;
}
if ( bShowMovement || HUD.ShouldDisplayDebug('AI') || HUD.ShouldDisplayDebug('AIMovementVerbose') )
{
CommandText = "---------- AI General ----------\n";
if ( Enemy != None )
{
CommandText = CommandText@"STATE: "$GetStateName()$" Enemy "$Enemy.GetHumanReadableName()$"\n";
}
else
{
CommandText = CommandText@"STATE: "$GetStateName()$" NO Enemy "$"\n";
}
if( HUD.ShouldDisplayDebug('AI') || HUD.ShouldDisplayDebug('AIMovementVerbose') )
{
CommandText = CommandText@"Anchor: "$Pawn.Anchor$" Serpentine Dist: "$Pawn.SerpentineDist$" Time: "$Pawn.SerpentineTime$"\n";
if ( (Pawn != None) && (MoveTarget != None) && Pawn.ReachedDestination(MoveTarget) )
{
CommandText = CommandText@"Skill: "$Skill$" NAVIGATION MoveTarget: "$GetItemName(String(MoveTarget))$"(REACHED) MoveTimer: "$MoveTimer$"\n";
}
else
{
CommandText = CommandText@"Skill: "$Skill$" NAVIGATION MoveTarget: "$GetItemName(String(MoveTarget))$" MoveTimer: "$MoveTimer$"\n";
}
CommandText = CommandText@"Destination: "$GetDestinationPosition()$" Focus: "$GetItemName(string(Focus))$" Preparing Move: "$bPreparingMove$"\n";
CommandText = CommandText@"RouteGoal: "$GetItemName(string(RouteGoal))$" RouteDist: "$RouteDist$"\n";
for ( i=0; i<RouteCache.Length; i++ )
{
if ( RouteCache[i] == None )
{
if ( i > 5 )
T = T$"--"$GetItemName(string(RouteCache[i-1]));
break;
}
else if ( i < 5 )
T = T$GetItemName(string(RouteCache[i]))$"-";
}
CommandText = CommandText@"RouteCache: "$T$"\n";
}
OverheadTexts[OverheadTexts.Length] = CommandText;
OverheadColors[OverheadTexts.Length - 1] = MakeColor(0,200,50);
CommandText = "";
}
if( bShowMovement )
{
CommandText = "---------- AI MOVEMENT ----------\n";
if( IntermediateMoveGoal != none )
{
CommandText = CommandText@"IntermediateMoveGoal:"$IntermediateMoveGoal$"\n";
}
if( MoveTarget != none )
{
CommandText = CommandText@"MoveTarget:"$MoveTarget$"\n";
}
WanderCommand = FindCommandOfClass(class'AICommand_Wander');
if( WanderCommand != none )
{
CommandText = CommandText@"AIC Wander Timer:"$GetRemainingTimeForTimer('Timer_WanderDurationExpired', WanderCommand)$"\n";
}
OverheadTexts[OverheadTexts.Length] = CommandText;
}
if( bShowCommands )
{
CommandText = "---------- AI Commands ----------\n";
if( AIActionStatus != "" )
{
CommandText = CommandText@"Status:"$AIActionStatus$"\n";
}
}
if( bShowMovement || bShowCommands )
{
if( MyKFPawn.SpecialMove != SM_None )
{
CommandText = CommandText@"SpecialMove:"$MyKFPawn.SpecialMove$"\n";
}
else
{
CommandText = CommandText@"SpecialMove: NONE \n";
}
}
OverheadTexts[OverheadTexts.Length] = CommandText;
OverheadColors[OverheadTexts.Length - 1] = MakeColor(0,200,50);
if( bShowCommands )
{
CommandText = "-- AICommand History --";
OverheadTexts[OverheadTexts.Length] = CommandText;
OverheadColors[OverheadTexts.Length - 1] = MakeColor(0,255,0);
foreach AllCommands( class'GameAICommand', Cmd )
{
if( Cmd.ChildCommand == None )
{
CommandText = String(Cmd.Name)@":"@Cmd.GetStateName();
OverheadTexts[OverheadTexts.Length] = CommandText;
OverheadColors[OverheadTexts.Length - 1] = MakeColor(0,255,0);
}
else
{
CommandText = String(Cmd.Name)@":"@Cmd.GetStateName();
OverheadTexts[OverheadTexts.Length] = CommandText;
OverheadColors[OverheadTexts.Length - 1] = MakeColor(255,128,0);
}
CmdCnt++;
}
if( CmdCnt == 0 )
{
CommandText = "No Active Command";
OverheadTexts[OverheadTexts.Length] = CommandText;
OverheadColors[OverheadTexts.Length - 1] = MakeColor(0,200,50);
}
}
}
DefaultProperties
{
// ---------------------------------------------
// Combat
MeleeCommandClass=class'KFGame.AICommand_Base_Zed'
bAllowCombatTransitions=true
bIdleMoveToNearestEnemy=true
MinTimeBetweenEnemyChanges=5f
DoorMeleeDistance=200.f
MaxMeleeHeightAngle=0.64f
MaxGetStrikeTime=0.25f
MinDistanceToAggroZed=1500
PendingAnimStrikeIndex=255
bCanDoHeavyBump=false
ZedBumpEffectThreshold=270
ZedBumpObliterationEffectChance=0.4
AIRemainingTeleportThreshold=12.0
FrustrationThreshold=5.f
FrustrationDelay=2.5f
LastFrustrationCheckTime=0.f
LowIntensityAttackCooldown=2.0
//bUseOldAttackDecisions=true
2022-10-29 23:52:58 +00:00
CanForceEnemy=true
ForcedEnemy=none
LastForcedEnemy=none
ForcedEnemyLastTime=0.f
DamageRatioToChangeForcedEnemy=0.5f
TimeCanRestartForcedEnemy=10.f
TimeCannotChangeFromForcedEnemy=10.f
2020-12-13 15:01:13 +00:00
// ---------------------------------------------
// AI / Navigation
MaxBlockedPathDuration=30.f
bCanDoSpecial=true
InUseNodeCostMultiplier=3.f
bProbeNotifyOnAddToRouteCache=false // 7/1/14
DirectPathExtentModifier=1.f
DirectPathCheckFrequency_Min=0.1f
DirectPathCheckFrequency_Max=0.3f
bDisablePartialPaths=false
RepeatWalkingTauntTime=15
EvadeGrenadeChance=1.0f
// ---------------------------------------------
// Movement
RotationRate=(Pitch=50000,Yaw=50000,Roll=50000)
MaxStepAsideDist=128.0
// Begin Object Class=KFAISteering Name=Steering0
// End Object
// Steering=Steering0
RotationRateMultiplier=1.f
bIgnoreStepAside=true // 3.1.2014
MinTimeBetweenLOSChecks=4.f
//bNotifyApex=true
// optimization - simplify LineOfSight/SeePlayer line traces
bSkipExtraLOSChecks=true
bDumpCommandHistoryOnExit=false
bUseDesiredRotationForMelee=true
BumpThreshold=10f
BumpDecayRate=0.5f
BumpGrowthRate=10.0f
bNotifyFallingHitWall=true
// No navigation handle right now since we are doing path nodes
NavigationHandleClass=none//class'KFNavigationHandle'
CurrentMovementPhase = MOVEMENT_PHASE_TYPE_NONE;
//bGoToEnemiesOutsideNavmesh=true
//MovementPluginClass="KFAIPluginMovement_Recast"
//LeapPluginClass="KFAIPluginLeap"
//StuckFixPluginClass="KFAIPluginStuckFix"
KfWallWalkingPlugIn=None
//============================================
// Movement/Path Paramaters
//============================================
bAlwaysAcceptPartialPaths=true
bShouldUsePathLanes=true
bShouldOffsetCorners=false
//bDebugDisableSprinting=false
DistanceToCheckForClearPathOnNavMeshLocWhenOffNavMesh=512
DistanceToCheckForNonExploitedOnNavMeshLocWhenOffNavMesh=2056
DistanceToCheckForNonExploitedOnNavMeshLocWhenOnDeadEndOfNavMesh=512
DistanceToCheckForClearPathOnNavMeshLocWhenEnemyIsOffNavMesh=200
DistanceToCheckForNonExploitedOnNavMeshLocWhenEnemyIsOffNavMesh=500
GeneralGoalDistanceForMovingToDoor=256
RecastEnemyRepathDistance=512
DropEdgeLeapVelocity=(X=400,Y=0,Z=400)
MaxRangeToDropEdgeAllowedToLeadFrom=100
DistanceDownRangeToFocusForDropEdgeLeap=1000
//============================================
// General AI System Paramaters
//============================================
DefaultMaxTimeAllowedToStayStuckBeforeSuicide=5.0
NoNavMeshPathMaxTimeAllowedToStayStuckBeforeSuicide=1.0
SameIntermediatePointToManyTimesMaxTimeAllowedToStayStuckBeforeSuicide=5.0
TargetOffNavMeshAndCanNotFindLocaitonNearThemICanMoveTooMaxTimeAllowedToStayStuckBeforeSuicide=5.0
SameIntermediatePointToManyTimesDurationAfterStartedMovingAgaintToStopStuckCheck=10.0
DefaultMinDistaceToHaveToMoveToBeConcideredStuckBeforeSuicide=5.0
NoNavMeshPathMinDistaceToHaveToMoveToBeConcideredStuckBeforeSuicide=1.0
SameIntermediatePointToManyTimesMinDistaceToHaveToMoveToBeConcideredStuckBeforeSuicide=200.0
GoalDistanceWhenMovingToLocationForMeleeStrikeWhenEnemyIsOffNavMesh=0
bSpecialBumpHandling=true
bUseNavMesh=false
bUsePluginsForMovement=false
//
/////
//
MovementPhaseTypeUnknownIcon=Texture2D'AI_Debug_Helpers.UnknownMovementPhase'
MovementPhaseTypePathNodeNormalMoveToIcon=Texture2D'AI_Debug_Helpers.MapIcon_PathNodeMoveTo'
MovementPhaseTypePathNodeMoveFailedIcon=Texture2D'AI_Debug_Helpers.MapIcon_FailedMove'
MovementPhaseTypePathNodeMoveToPointIcon=Texture2D'AI_Debug_Helpers.MapIcon_MoveToAPoint'
MovementPhaseTypePathNodeRotateToFocusIcon=Texture2D'AI_Debug_Helpers.MapIcon_RotateToFocus'
MovementPhaseTypePathNodeDelayMoveIcon=Texture2D'AI_Debug_Helpers.MapIcon_AIDelay'
MovementPhaseTypeNavMeshNormalMoveToIcon=Texture2D'AI_Debug_Helpers.NavMeshMoveTo'
MovementPhaseTypeFinalDestMoveTowardsIcon=Texture2D'AI_Debug_Helpers.FinalDestMoveTowards'
MovementPhaseTypeEMT_LOS_MoveTowardsIcon=Texture2D'AI_Debug_Helpers.LOS_MoveTowards'
MovementPhaseTypeMovingToNavMeshUsingPathNodesIcon=Texture2D'AI_Debug_Helpers.WallToNavMesh'
MovementPhaseTypeFALLBACK_REFUSED_TO_BE_EXPLOTIED_FIND_NEARBY_MESH_POINT_MOVE_TO_DIRECT_NON_PATH_POSIcon=Texture2D'AI_Debug_Helpers.FindNearbyMeshPointRefusedToBeExplotied'
MovementPhaseTypeFALLBACK_FIND_NEARBY_MESH_POINT_MOVE_TO_DIRECT_NON_PATH_POSIcon=Texture2D'AI_Debug_Helpers.FindNearbyMeshPoint'
TypeOfMovementStuckOnUnknownWhyIcon=Texture2D'AI_Debug_Helpers.StuckUnknown'
TypeOfMovementStuckOnMOVE_FAILURE_TYPE_NO_NAV_MESH_PATHIcon=Texture2D'AI_Debug_Helpers.NoNavStuck'
TypeOfMovementStuckOnMOVE_FAILURE_TYPE_SAME_INTERMEDIATE_POINT_TOO_MANY_TIMESIcon=Texture2D'AI_Debug_Helpers.TriskelionStuck'
TypeOfMovementStuckOnMOVE_FAILURE_TYPE_TARGET_OFF_NAV_MESH_AND_CAN_NOT_FIND_LOCAITON_NEAR_THEM_I_CAN_MOVE_TOIcon=Texture2D'AI_Debug_Helpers.EnemyOffNavMeshAndCanDoNothing'
TypeOfMovementStuckOnLookingForBetterIntermediateLoc=Texture2D'AI_Debug_Helpers.LookingForBetterInter'
TypeOfMovementStuckOnMoveToBetterIntermediate=Texture2D'AI_Debug_Helpers.MoveToBetterIntermediate'
Role=ROLE_Authority
// Settings this to true so the base KFAIController gets a PRI, etc
bIsPlayer=true
// ---------------------------------------------
// Behaviors
BaseShapeOfProjectileForCalc=(X=1,Y=1,Z=1)
AggroPlayerHealthPercentage=0.1
AggroPlayerResetTime=10
AggroZedResetTime=30
AggroZedHealthPercentage=0.15
AggroEnemySwitchWaitTime=5.f
// Don't always assign an enemy in the BaseAIController native code. Let the script code handle it!!!
bAlwaysAssignEnemy=false
StuckCheckInterval=0.5
StuckVelocityThreshholdSquared=625 //(25 UUS)
StuckPossiblityThreshhold=5.0
XYMoveStuckThresholdSquared=625 //(25 UUS)
StuckCheckEnemyDistThreshholdSquared=1000000 // 10 meters
NavigationBumpTeamCollisionThreshholdSquared=1000000 // 10 meters
bCanTeleportCloser=true
TeleportCheckInterval=1.0
TeleportCooldown=7.0
HiddenRelocateTeleportThreshold=5.0
PostSpawnRelocateTeleportCooldown=10
StrikeRangePercentage=0.85
}