1264 lines
33 KiB
Ucode
1264 lines
33 KiB
Ucode
class KFFriendlyAI extends MonsterController;
|
|
|
|
var KFHumanPawnEnemy KFHM;
|
|
var Actor MoveGoal;
|
|
var bool bStoppedFiring,bFireSuccess;
|
|
|
|
function Restart()
|
|
{
|
|
Super.Restart();
|
|
KFHM = KFHumanPawnEnemy(Pawn);
|
|
Accuracy = KFHM.Accuracy;
|
|
ReactionTime = KFHM.ReactionTime;
|
|
ReSetSkill();
|
|
}
|
|
function Possess(Pawn aPawn)
|
|
{
|
|
Super(AIController).Possess(aPawn);
|
|
InitializeSkill(DeathMatch(Level.Game).AdjustedDifficulty);
|
|
GoToState('InitilizingMe');
|
|
Disable('Tick');
|
|
}
|
|
|
|
State InitilizingMe
|
|
{
|
|
ignores SeePlayer, HearNoise, KilledBy, NotifyBump, HitWall, NotifyPhysicsVolumeChange, NotifyHeadVolumeChange, Falling, TakeDamage, ReceiveWarning;
|
|
|
|
function SetupFlags()
|
|
{
|
|
Pawn.MaxFallSpeed = 1.1 * Pawn.default.MaxFallSpeed; // so bots will accept a little falling damage for shorter routes
|
|
Pawn.SetMovementPhysics();
|
|
if( Pawn.Physics==PHYS_Walking )
|
|
Pawn.SetPhysics(PHYS_Falling);
|
|
bIsPlayer = True; // Fake player to make zombies attack me, and make me see zombies.
|
|
KFHM.GiveDefaultWeapon();
|
|
enable('NotifyBump');
|
|
Switch( KFHM.SoldierOrders )
|
|
{
|
|
Case ORDER_Guarding:
|
|
GoToState('Waiting');
|
|
Break;
|
|
Case ORDER_Wander:
|
|
GoToState('Wandering');
|
|
Break;
|
|
Case ORDER_Hunt:
|
|
GoToState('ActiveHunting');
|
|
Break;
|
|
Default:
|
|
GoToState('Patroling');
|
|
Break;
|
|
}
|
|
}
|
|
Begin:
|
|
Sleep(0.001);
|
|
SetupFlags();
|
|
}
|
|
|
|
// added CanAttack() check to calm trigger-happy bots
|
|
function bool FireWeaponAt(Actor A)
|
|
{
|
|
if ( A == None )
|
|
A = Enemy;
|
|
if ( (A == None) || (Focus != A) )
|
|
return false;
|
|
Target = A;
|
|
|
|
if ( Pawn.Weapon != None )
|
|
{
|
|
Pawn.Weapon.FillToInitialAmmo();
|
|
if( !Pawn.Weapon.CanAttack(A) )
|
|
return false;
|
|
if ( Pawn.Weapon.HasAmmo() )
|
|
return WeaponFireAgain(Pawn.Weapon.RefireRate(),false);
|
|
}
|
|
else return WeaponFireAgain(Pawn.RefireRate(),false);
|
|
|
|
return false;
|
|
}
|
|
function TimedFireWeaponAtEnemy()
|
|
{
|
|
if ( (Enemy == None) || FireWeaponAt(Enemy) )
|
|
SetCombatTimer();
|
|
else SetTimer(0.1, True);
|
|
}
|
|
function StopFiring()
|
|
{
|
|
if ( (Pawn != None) && Pawn.StopWeaponFiring() )
|
|
bStoppedFiring = true;
|
|
|
|
bCanFire = false;
|
|
bFire = 0;
|
|
bAltFire = 0;
|
|
}
|
|
|
|
function WanderOrCamp(bool bMayCrouch)
|
|
{
|
|
Switch( KFHM.SoldierOrders )
|
|
{
|
|
Case ORDER_Guarding:
|
|
GoToState('Waiting');
|
|
Break;
|
|
Case ORDER_Wander:
|
|
GoToState('Wandering');
|
|
Break;
|
|
Case ORDER_Hunt:
|
|
GoToState('ActiveHunting');
|
|
Break;
|
|
Default:
|
|
GoToState('Patroling');
|
|
Break;
|
|
}
|
|
}
|
|
|
|
state Wandering
|
|
{
|
|
ignores EnemyNotVisible,Timer;
|
|
|
|
function CancelCampFor(Controller C)
|
|
{
|
|
DirectedWander(Normal(Pawn.Location - C.Pawn.Location));
|
|
}
|
|
function bool Formation()
|
|
{
|
|
return true;
|
|
}
|
|
function HearNoise(float Loudness, Actor NoiseMaker)
|
|
{
|
|
if ( NoiseMaker.Instigator!=None && FastTrace(NoiseMaker.Location,Pawn.Location) && SetEnemy(NoiseMaker.Instigator,False) )
|
|
WhatToDoNext(3);
|
|
}
|
|
function PickDestination()
|
|
{
|
|
if ( TestDirection(VRand(),Destination) )
|
|
return;
|
|
TestDirection(VRand(),Destination);
|
|
}
|
|
function BeginState()
|
|
{
|
|
Enemy = None;
|
|
Pawn.bCanJump = false;
|
|
Pawn.bAvoidLedges = true;
|
|
Pawn.bStopAtLedges = true;
|
|
Pawn.SetWalking(true);
|
|
MinHitWall += 0.15;
|
|
}
|
|
function EndState()
|
|
{
|
|
MonitoredPawn = None;
|
|
MinHitWall -= 0.15;
|
|
if ( Pawn != None )
|
|
{
|
|
Pawn.bStopAtLedges = false;
|
|
Pawn.bAvoidLedges = false;
|
|
Pawn.SetWalking(false);
|
|
if (Pawn.JumpZ > 0)
|
|
Pawn.bCanJump = true;
|
|
}
|
|
}
|
|
event MonitoredPawnAlert()
|
|
{
|
|
WhatToDoNext(6);
|
|
}
|
|
|
|
Begin:
|
|
WaitForLanding();
|
|
Camping:
|
|
Pawn.Acceleration = vect(0,0,0);
|
|
Focus = None;
|
|
FocalPoint = Pawn.Location+VRand()*500;
|
|
NearWall(MINVIEWDIST);
|
|
FinishRotation();
|
|
Sleep(3 + FRand());
|
|
Moving:
|
|
WaitForLanding();
|
|
PickDestination();
|
|
MoveTo(Destination,,true);
|
|
if ( Pawn.bCanFly && (Physics == PHYS_Walking) )
|
|
SetPhysics(PHYS_Flying);
|
|
Goto('Begin');
|
|
}
|
|
state Waiting extends Wandering
|
|
{
|
|
Begin:
|
|
WaitForLanding();
|
|
Camping:
|
|
Pawn.Acceleration = vect(0,0,0);
|
|
Focus = None;
|
|
FocalPoint = Pawn.Location+vector(Pawn.Rotation)*2500;
|
|
NearWall(MINVIEWDIST);
|
|
Stop;
|
|
}
|
|
state ActiveHunting
|
|
{
|
|
ignores EnemyNotVisible,Timer;
|
|
|
|
function HearNoise(float Loudness, Actor NoiseMaker)
|
|
{
|
|
if ( NoiseMaker.Instigator!=None && FastTrace(NoiseMaker.Location,Pawn.Location) && SetEnemy(NoiseMaker.Instigator,False) )
|
|
WhatToDoNext(3);
|
|
}
|
|
function PickDestination()
|
|
{
|
|
if( MoveGoal==None )
|
|
MoveGoal = FindRandomDest();
|
|
if( MoveGoal!=None )
|
|
{
|
|
if( ActorReachable(MoveGoal) )
|
|
{
|
|
MoveTarget = MoveGoal;
|
|
MoveGoal = None;
|
|
Return;
|
|
}
|
|
if( FindBestPathToward(MoveGoal,True,True) )
|
|
Return;
|
|
MoveGoal = None;
|
|
}
|
|
if ( TestDirection(VRand(),Destination) )
|
|
return;
|
|
TestDirection(VRand(),Destination);
|
|
}
|
|
function BeginState()
|
|
{
|
|
Enemy = None;
|
|
Pawn.bCanJump = True;
|
|
Pawn.bAvoidLedges = False;
|
|
Pawn.bStopAtLedges = False;
|
|
Pawn.SetWalking(False);
|
|
}
|
|
|
|
Begin:
|
|
WaitForLanding();
|
|
PickDestination();
|
|
if( MoveTarget!=None )
|
|
MoveToward(MoveTarget,,,ShouldStrafeTo(MoveTarget));
|
|
else MoveTo(Destination);
|
|
if( FRand()<0.1 )
|
|
{
|
|
Pawn.Acceleration = vect(0,0,0);
|
|
Focus = None;
|
|
FocalPoint = Pawn.Location+VRand()*200;
|
|
Sleep(0.5+FRand()*1.5);
|
|
}
|
|
Goto'Begin';
|
|
}
|
|
|
|
function EnemyChanged(bool bNewEnemyVisible)
|
|
{
|
|
bEnemyAcquired = false;
|
|
SetEnemyInfo(bNewEnemyVisible);
|
|
}
|
|
|
|
function NotifyKilled(Controller Killer, Controller Killed, pawn KilledPawn)
|
|
{
|
|
if ( Enemy!=None && Enemy==KilledPawn )
|
|
{
|
|
Enemy = None;
|
|
WhatToDoNext(45);
|
|
}
|
|
}
|
|
|
|
function bool ShouldStrafeTo(Actor WayPoint)
|
|
{
|
|
local NavigationPoint N;
|
|
|
|
if ( Skill + StrafingAbility < 3 )
|
|
return false;
|
|
|
|
if ( WayPoint == Enemy )
|
|
return ( Skill + StrafingAbility < 5 * FRand() - 1 );
|
|
else if ( Pickup(WayPoint) == None )
|
|
{
|
|
N = NavigationPoint(WayPoint);
|
|
if ( (N == None) || N.bNeverUseStrafing )
|
|
return false;
|
|
if ( N.FearCost > 200 )
|
|
return true;
|
|
if ( N.bAlwaysUseStrafing && (FRand() < 0.8) )
|
|
return true;
|
|
}
|
|
if ( Pawn(WayPoint) != None )
|
|
return ( Skill + StrafingAbility < 5 * FRand() - 1 );
|
|
if ( Skill + StrafingAbility < 7 * FRand() - 1 )
|
|
return false;
|
|
if ( Enemy == None )
|
|
return ( FRand() < 0.4 );
|
|
if ( EnemyVisible() )
|
|
return ( FRand() < 0.85 );
|
|
return ( FRand() < 0.6 );
|
|
}
|
|
|
|
/* CheckIfShouldCrouch()
|
|
returns true if target position still can be shot from crouched position,
|
|
or if couldn't hit it from standing position either
|
|
*/
|
|
function CheckIfShouldCrouch(vector StartPosition, vector TargetPosition, float probability)
|
|
{
|
|
local actor HitActor;
|
|
local vector HitNormal,HitLocation, X,Y,Z, projstart;
|
|
|
|
if ( !Pawn.bCanCrouch || (!Pawn.bIsCrouched && (FRand() > probability))
|
|
|| (Skill < 3 * FRand())
|
|
|| KFHumanPawnEnemy(Pawn).RecommendSplashDamage() )
|
|
{
|
|
Pawn.bWantsToCrouch = false;
|
|
return;
|
|
}
|
|
|
|
GetAxes(Rotation,X,Y,Z);
|
|
projStart = KFHumanPawnEnemy(Pawn).GetFireStart(X,Y,Z);
|
|
projStart = projStart + StartPosition - Pawn.Location;
|
|
projStart.Z = projStart.Z - 1.8 * (Pawn.CollisionHeight - Pawn.CrouchHeight);
|
|
HitActor = Trace(HitLocation, HitNormal, TargetPosition , projStart, false);
|
|
if ( HitActor == None )
|
|
{
|
|
Pawn.bWantsToCrouch = true;
|
|
return;
|
|
}
|
|
|
|
projStart.Z = projStart.Z + 1.8 * (Pawn.Default.CollisionHeight - Pawn.CrouchHeight);
|
|
HitActor = Trace(HitLocation, HitNormal, TargetPosition , projStart, false);
|
|
if ( HitActor == None )
|
|
{
|
|
Pawn.bWantsToCrouch = false;
|
|
return;
|
|
}
|
|
Pawn.bWantsToCrouch = true;
|
|
}
|
|
|
|
|
|
|
|
//=============================================================================
|
|
function WhatToDoNext(byte CallingByte)
|
|
{
|
|
if ( ChooseAttackTime == Level.TimeSeconds )
|
|
ChooseAttackCounter++;
|
|
else
|
|
{
|
|
ChooseAttackTime = Level.TimeSeconds;
|
|
ChooseAttackCounter = 0;
|
|
}
|
|
ChoosingAttackLevel++;
|
|
ExecuteWhatToDoNext();
|
|
ChoosingAttackLevel--;
|
|
}
|
|
|
|
function bool WeaponFireAgain(float RefireRate, bool bFinishedFire)
|
|
{
|
|
if ( Target == None )
|
|
Target = Enemy;
|
|
if ( Target != None )
|
|
{
|
|
if ( !Pawn.IsFiring() )
|
|
{
|
|
if ( (Pawn.Weapon != None && Pawn.Weapon.bMeleeWeapon) || (!NeedToTurn(Target.Location) && Pawn.CanAttack(Target)) )
|
|
{
|
|
Focus = Target;
|
|
bCanFire = true;
|
|
bStoppedFiring = false;
|
|
if (Pawn.Weapon != None)
|
|
bFireSuccess = Pawn.Weapon.BotFire(bFinishedFire);
|
|
else
|
|
{
|
|
Pawn.ChooseFireAt(Target);
|
|
bFireSuccess = true;
|
|
}
|
|
return bFireSuccess;
|
|
}
|
|
else
|
|
{
|
|
bCanFire = false;
|
|
}
|
|
}
|
|
else if ( bCanFire && ShouldFireAgain(RefireRate) )
|
|
{
|
|
if ( (Target != None) && (Focus == Target) && !Target.bDeleteMe )
|
|
{
|
|
bStoppedFiring = false;
|
|
if (Pawn.Weapon != None)
|
|
bFireSuccess = Pawn.Weapon.BotFire(bFinishedFire);
|
|
else
|
|
{
|
|
Pawn.ChooseFireAt(Target);
|
|
bFireSuccess = true;
|
|
}
|
|
return bFireSuccess;
|
|
}
|
|
}
|
|
}
|
|
StopFiring();
|
|
return false;
|
|
}
|
|
|
|
function bool ShouldFireAgain(float RefireRate)
|
|
{
|
|
if ( FRand() < RefireRate )
|
|
return true;
|
|
if ( Pawn.FireOnRelease() && (Pawn.Weapon == None || !Pawn.Weapon.bMeleeWeapon) )
|
|
return false;
|
|
if ( Pawn(Target) != None )
|
|
return ( (Pawn.bStationary || Pawn(Target).bStationary) && (Pawn(Target).Health > 0) );
|
|
return false;
|
|
}
|
|
|
|
function FightEnemy(bool bCanCharge)
|
|
{
|
|
local vector X,Y,Z;
|
|
local float enemyDist;
|
|
local float AdjustedCombatStyle, Aggression;
|
|
local bool bFarAway, bOldForcedCharge;
|
|
|
|
if ( (Enemy == None) || (Pawn == None) )
|
|
{
|
|
WhatToDoNext(45);
|
|
Return;
|
|
}
|
|
|
|
if ( (Enemy == FailedHuntEnemy) && (Level.TimeSeconds == FailedHuntTime) )
|
|
{
|
|
if ( Enemy == FailedHuntEnemy )
|
|
{
|
|
GoalString = "FAILED HUNT - HANG OUT";
|
|
if ( EnemyVisible() )
|
|
bCanCharge = false;
|
|
}
|
|
}
|
|
|
|
if( !EnemyVisible() )
|
|
{
|
|
if( KFHM.bStationaryCombat )
|
|
{
|
|
Enemy = None;
|
|
WhatToDoNext(44);
|
|
}
|
|
else GoToState('Hunting');
|
|
Return;
|
|
}
|
|
else if( KFHM.bStationaryCombat )
|
|
{
|
|
GoalString = "Ranged Attack";
|
|
DoRangedAttackOn(Enemy);
|
|
return;
|
|
}
|
|
bOldForcedCharge = bMustCharge;
|
|
bMustCharge = false;
|
|
enemyDist = VSize(Pawn.Location - Enemy.Location);
|
|
AdjustedCombatStyle = CombatStyle;
|
|
Aggression = 1.5 * FRand() - 0.8 + 2 * AdjustedCombatStyle
|
|
+ FRand() * (Normal(Enemy.Velocity - Pawn.Velocity) Dot Normal(Enemy.Location - Pawn.Location));
|
|
if ( Enemy.Weapon != None )
|
|
Aggression += 2 * Enemy.Weapon.SuggestDefenseStyle();
|
|
if ( enemyDist > MAXSTAKEOUTDIST )
|
|
Aggression += 0.5;
|
|
|
|
// Bloodlust *__*
|
|
if ( (Enemy.Health / Enemy.HealthMax) <= 0.5 )
|
|
Aggression += 0.5;
|
|
|
|
if ( (Pawn.Physics == PHYS_Walking) || (Pawn.Physics == PHYS_Falling) )
|
|
{
|
|
if (Pawn.Location.Z > Enemy.Location.Z + TACTICALHEIGHTADVANTAGE)
|
|
Aggression = FMax(0.0, Aggression - 1.0 + AdjustedCombatStyle);
|
|
else if ( (Skill < 4) && (enemyDist > 0.65 * MAXSTAKEOUTDIST) )
|
|
{
|
|
bFarAway = true;
|
|
Aggression += 0.5;
|
|
}
|
|
else if (Pawn.Location.Z < Enemy.Location.Z - Pawn.CollisionHeight) // below enemy
|
|
Aggression += CombatStyle;
|
|
}
|
|
|
|
// see enemy - decide whether to charge it or strafe around/stand and fire
|
|
Target = Enemy;
|
|
if( KFHM.PreferMelee() || (bCanCharge && bOldForcedCharge) )
|
|
{
|
|
GoalString = "Charge";
|
|
DoCharge();
|
|
return;
|
|
}
|
|
if ( bCanCharge && bFarAway && (Aggression > 1) && (FRand() < 0.5) )
|
|
{
|
|
GoalString = "Charge closer";
|
|
DoCharge();
|
|
return;
|
|
}
|
|
if ( !KFHM.PreferMelee() && (FRand() > 0.17 * (skill - 1)) && !DefendMelee(enemyDist) )
|
|
{
|
|
GoalString = "Ranged Attack";
|
|
DoRangedAttackOn(Enemy);
|
|
return;
|
|
}
|
|
if ( bCanCharge )
|
|
{
|
|
if ( Aggression > 1 )
|
|
{
|
|
GoalString = "Charge 2";
|
|
DoCharge();
|
|
return;
|
|
}
|
|
}
|
|
if ( !Pawn.bCanStrafe )
|
|
{
|
|
GoalString = "Ranged Attack";
|
|
DoRangedAttackOn(Enemy);
|
|
return;
|
|
}
|
|
GoalString = "Do tactical move";
|
|
if ( !KFHM.RecommendSplashDamage() && KFHM.bCanDodgeDoubleJump && (FRand() < 0.7) && (FRand()*Skill > 3) )
|
|
{
|
|
GetAxes(Pawn.Rotation,X,Y,Z);
|
|
GoalString = "Try to Duck ";
|
|
if ( FRand() < 0.5 )
|
|
{
|
|
Y *= -1;
|
|
TryToDuck(Y, true);
|
|
}
|
|
else TryToDuck(Y, false);
|
|
}
|
|
DoTacticalMove();
|
|
}
|
|
|
|
function bool FindNewEnemy()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
function DoCharge()
|
|
{
|
|
if ( Enemy.PhysicsVolume.bWaterVolume )
|
|
{
|
|
if ( !Pawn.bCanSwim )
|
|
{
|
|
DoTacticalMove();
|
|
return;
|
|
}
|
|
}
|
|
else if ( !Pawn.bCanFly && !Pawn.bCanWalk )
|
|
{
|
|
DoTacticalMove();
|
|
return;
|
|
}
|
|
GotoState('Charging');
|
|
}
|
|
|
|
function SeeMonster( Pawn Seen )
|
|
{
|
|
SeePlayer(Seen);
|
|
}
|
|
|
|
state Charging
|
|
{
|
|
ignores SeePlayer, HearNoise;
|
|
|
|
/* MayFall() called by engine physics if walking and bCanJump, and
|
|
is about to go off a ledge. Pawn has opportunity (by setting
|
|
bCanJump to false) to avoid fall
|
|
*/
|
|
function MayFall()
|
|
{
|
|
if ( MoveTarget != Enemy )
|
|
return;
|
|
|
|
Pawn.bCanJump = ActorReachable(Enemy);
|
|
if ( !Pawn.bCanJump )
|
|
MoveTimer = -1.0;
|
|
}
|
|
|
|
function bool TryToDuck(vector duckDir, bool bReversed)
|
|
{
|
|
if ( FRand() < 0.6 )
|
|
return Global.TryToDuck(duckDir, bReversed);
|
|
if ( MoveTarget == Enemy )
|
|
return TryStrafe(duckDir);
|
|
}
|
|
|
|
function bool StrafeFromDamage(float Damage, class<DamageType> DamageType, bool bFindDest)
|
|
{
|
|
local vector sideDir;
|
|
|
|
sideDir = Normal( Normal(Enemy.Location - Pawn.Location) Cross vect(0,0,1) );
|
|
if ( (Pawn.Velocity Dot sidedir) > 0 )
|
|
sidedir *= -1;
|
|
|
|
return TryStrafe(sideDir);
|
|
}
|
|
|
|
function bool TryStrafe(vector sideDir)
|
|
{
|
|
local vector extent, HitLocation, HitNormal;
|
|
local actor HitActor;
|
|
|
|
Extent = Pawn.GetCollisionExtent();
|
|
HitActor = Trace(HitLocation, HitNormal, Pawn.Location + MINSTRAFEDIST * sideDir, Pawn.Location, false, Extent);
|
|
if (HitActor != None)
|
|
{
|
|
sideDir *= -1;
|
|
HitActor = Trace(HitLocation, HitNormal, Pawn.Location + MINSTRAFEDIST * sideDir, Pawn.Location, false, Extent);
|
|
}
|
|
if (HitActor != None)
|
|
return false;
|
|
|
|
if ( Pawn.Physics == PHYS_Walking )
|
|
{
|
|
HitActor = Trace(HitLocation, HitNormal, Pawn.Location + MINSTRAFEDIST * sideDir - MAXSTEPHEIGHT * vect(0,0,1), Pawn.Location + MINSTRAFEDIST * sideDir, false, Extent);
|
|
if ( HitActor == None )
|
|
return false;
|
|
}
|
|
Destination = Pawn.Location + 2 * MINSTRAFEDIST * sideDir;
|
|
GotoState('TacticalMove', 'DoStrafeMove');
|
|
return true;
|
|
}
|
|
|
|
function NotifyTakeHit(pawn InstigatedBy, vector HitLocation, int Damage, class<DamageType> damageType, vector Momentum)
|
|
{
|
|
local float pick;
|
|
local vector sideDir;
|
|
local bool bWasOnGround;
|
|
|
|
Super.NotifyTakeHit(InstigatedBy,HitLocation, Damage,DamageType,Momentum);
|
|
|
|
bWasOnGround = (Pawn.Physics == PHYS_Walking);
|
|
if ( Pawn.health <= 0 )
|
|
return;
|
|
if ( StrafeFromDamage(damage, damageType, true) )
|
|
return;
|
|
else if ( bWasOnGround && (MoveTarget == Enemy) &&
|
|
(Pawn.Physics == PHYS_Falling) ) //weave
|
|
{
|
|
pick = 1.0;
|
|
if ( bStrafeDir )
|
|
pick = -1.0;
|
|
sideDir = Normal( Normal(Enemy.Location - Pawn.Location) Cross vect(0,0,1) );
|
|
sideDir.Z = 0;
|
|
Pawn.Velocity += pick * Pawn.GroundSpeed * 0.7 * sideDir;
|
|
if ( FRand() < 0.2 )
|
|
bStrafeDir = !bStrafeDir;
|
|
}
|
|
}
|
|
|
|
event bool NotifyBump(actor Other)
|
|
{
|
|
if ( Other == Enemy )
|
|
{
|
|
DoRangedAttackOn(Enemy);
|
|
return false;
|
|
}
|
|
return Global.NotifyBump(Other);
|
|
}
|
|
|
|
function Timer()
|
|
{
|
|
enable('NotifyBump');
|
|
Target = Enemy;
|
|
if(ClearShot(Target.Location,true))
|
|
TimedFireWeaponAtEnemy();
|
|
}
|
|
|
|
function EnemyNotVisible()
|
|
{
|
|
// WhatToDoNext(15);
|
|
GotoState('Charging', 'Moving');
|
|
WhatToDoNext(15);
|
|
}
|
|
|
|
function EndState()
|
|
{
|
|
if ( (Pawn != None) && Pawn.JumpZ > 0 )
|
|
Pawn.bCanJump = true;
|
|
}
|
|
|
|
Begin:
|
|
if (Pawn.Physics == PHYS_Falling)
|
|
{
|
|
Focus = Enemy;
|
|
Destination = Enemy.Location;
|
|
WaitForLanding();
|
|
}
|
|
if ( Enemy == None )
|
|
WhatToDoNext(16);
|
|
WaitForAnim:
|
|
/*
|
|
if ( Pawn.bShotAnim )
|
|
{
|
|
Sleep(0.35);
|
|
Goto('WaitForAnim');
|
|
}
|
|
*/
|
|
if ( !FindBestPathToward(Enemy, false,true) )
|
|
GotoState('TacticalMove');
|
|
Moving:
|
|
MoveToward(MoveTarget,FaceActor(1),,ShouldStrafeTo(MoveTarget));
|
|
WhatToDoNext(17);
|
|
if ( bSoaking )
|
|
SoakStop("STUCK IN CHARGING!");
|
|
}
|
|
|
|
State WaitForAnim
|
|
{
|
|
ignores SeePlayer, HearNoise, KilledBy, NotifyBump, HitWall, NotifyPhysicsVolumeChange, NotifyHeadVolumeChange, Falling, TakeDamage, ReceiveWarning;
|
|
|
|
event AnimEnd(int Channel)
|
|
{
|
|
WhatToDoNext(99);
|
|
}
|
|
Begin:
|
|
Sleep(0.1);
|
|
WhatToDoNext(99);
|
|
}
|
|
|
|
function bool IsStrafing()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
function bool SetEnemy( Pawn NewEnemy, optional bool bHateMonster , optional float MonsterHateChanceOverride )
|
|
{
|
|
if ( (NewEnemy == None) || (NewEnemy.Health <= 0) || (NewEnemy.Controller == None) || (NewEnemy == Enemy) )
|
|
return false;
|
|
|
|
if( Monster(NewEnemy)!=None )
|
|
{
|
|
if( KFHM.AttitudeToSpecimen>=ATTITUDE_Friendly )
|
|
Return False;
|
|
else if( KFHM.AttitudeToSpecimen==ATTITUDE_Ignore && !bHateMonster )
|
|
Return False;
|
|
}
|
|
else if( KFHumanPawnEnemy(NewEnemy)!=None )
|
|
{
|
|
if( KFHM.AttitudeToPlayer>=ATTITUDE_Friendly && KFHumanPawnEnemy(NewEnemy).AttitudeToPlayer>=ATTITUDE_Friendly )
|
|
Return False;
|
|
else if( KFHM.AttitudeToPlayer<ATTITUDE_Friendly && KFHumanPawnEnemy(NewEnemy).AttitudeToPlayer<ATTITUDE_Friendly )
|
|
Return False;
|
|
}
|
|
else if( KFPawn(NewEnemy)!=None )
|
|
{
|
|
if( KFHM.AttitudeToPlayer>=ATTITUDE_Friendly )
|
|
Return False;
|
|
else if( KFHM.AttitudeToPlayer==ATTITUDE_Ignore && !bHateMonster )
|
|
Return False;
|
|
}
|
|
ChangeEnemy(NewEnemy,CanSee(NewEnemy));
|
|
return true;
|
|
}
|
|
|
|
function DoRangedAttackOn(Actor A)
|
|
{
|
|
Target = A;
|
|
GotoState('RangedAttack');
|
|
}
|
|
|
|
function bool ClearShot(Vector TargetLoc, bool bImmediateFire)
|
|
{
|
|
local bool bSeeTarget;
|
|
|
|
if ( VSize(Enemy.Location - TargetLoc) > MAXSTAKEOUTDIST )
|
|
return false;
|
|
|
|
bSeeTarget = FastTrace(TargetLoc, Pawn.Location + Pawn.EyeHeight * vect(0,0,1));
|
|
// if pawn is crouched, check if standing would provide clear shot
|
|
if ( !bImmediateFire && !bSeeTarget && Pawn.bIsCrouched )
|
|
bSeeTarget = FastTrace(TargetLoc, Pawn.Location + (Pawn.Default.EyeHeight + Pawn.Default.CollisionHeight - Pawn.CollisionHeight) * vect(0,0,1));
|
|
|
|
if ( !bSeeTarget || !FastTrace(TargetLoc , Enemy.Location + Enemy.BaseEyeHeight * vect(0,0,1)) );
|
|
return false;
|
|
if ( (Monster(Pawn).SplashDamage() && (VSize(Pawn.Location - TargetLoc) < Monster(Pawn).GetDamageRadius()))
|
|
|| !FastTrace(TargetLoc + vect(0,0,0.9) * Enemy.CollisionHeight, Pawn.Location) )
|
|
{
|
|
StopFiring();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
state RangedAttack
|
|
{
|
|
ignores SeePlayer, HearNoise, Bump;
|
|
|
|
function bool Stopped()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
function CancelCampFor(Controller C)
|
|
{
|
|
DoTacticalMove();
|
|
}
|
|
|
|
function StopFiring()
|
|
{
|
|
Global.StopFiring();
|
|
if ( bHasFired )
|
|
{
|
|
bHasFired = false;
|
|
WhatToDoNext(32);
|
|
}
|
|
}
|
|
|
|
function EnemyNotVisible()
|
|
{
|
|
//let attack animation complete
|
|
DoCharge();
|
|
// WhatToDoNext(33);
|
|
}
|
|
|
|
function Timer()
|
|
{
|
|
if ( KFHumanPawnEnemy(Pawn).PreferMelee() )
|
|
{
|
|
SetCombatTimer();
|
|
StopFiring();
|
|
Pawn.bWantsToCrouch = false;
|
|
WhatToDoNext(34);
|
|
}
|
|
else
|
|
TimedFireWeaponAtEnemy();
|
|
}
|
|
|
|
function DoRangedAttackOn(Actor A)
|
|
{
|
|
Target = A;
|
|
GotoState('RangedAttack');
|
|
}
|
|
|
|
function BeginState()
|
|
{
|
|
StopStartTime = Level.TimeSeconds;
|
|
bHasFired = false;
|
|
Pawn.Acceleration = vect(0,0,0); //stop
|
|
if ( Target == None )
|
|
Target = Enemy;
|
|
if ( Target == None )
|
|
log(GetHumanReadableName()$" no target in ranged attack");
|
|
}
|
|
|
|
Begin:
|
|
bHasFired = false;
|
|
GoalString = "Ranged attack";
|
|
Focus = Target;
|
|
Sleep(0.0);
|
|
if ( Enemy != None )
|
|
CheckIfShouldCrouch(Pawn.Location,Enemy.Location, 1);
|
|
if ( NeedToTurn(Target.Location) )
|
|
{
|
|
Focus = Target;
|
|
FinishRotation();
|
|
}
|
|
bHasFired = true;
|
|
if ( Target == Enemy )
|
|
TimedFireWeaponAtEnemy();
|
|
else
|
|
FireWeaponAt(Target);
|
|
Sleep(0.1);
|
|
if ( KFHumanPawnEnemy(Pawn).PreferMelee() || (Target == None) || (Target != Enemy) )
|
|
WhatToDoNext(35);
|
|
if ( Enemy != None )
|
|
CheckIfShouldCrouch(Pawn.Location,Enemy.Location, 1);
|
|
Focus = Target;
|
|
Sleep(FMax(Pawn.RangedAttackTime(),0.2 + (0.5 + 0.5 * FRand()) * 0.4 * (7 - Skill)));
|
|
WhatToDoNext(36);
|
|
if ( bSoaking )
|
|
SoakStop("STUCK IN RANGEDATTACK!");
|
|
}
|
|
|
|
function SetMaxDesiredSpeed()
|
|
{
|
|
if ( Pawn != None )
|
|
Pawn.MaxDesiredSpeed = 1;
|
|
}
|
|
|
|
state Hunting
|
|
{
|
|
ignores EnemyNotVisible;
|
|
|
|
/* MayFall() called by] engine physics if walking and bCanJump, and
|
|
is about to go off a ledge. Pawn has opportunity (by setting
|
|
bCanJump to false) to avoid fall
|
|
*/
|
|
function bool IsHunting()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
function MayFall()
|
|
{
|
|
Pawn.bCanJump = ( (MoveTarget == None) || (MoveTarget.Physics != PHYS_Falling) || !MoveTarget.IsA('Pickup') );
|
|
}
|
|
|
|
function SeePlayer(Pawn SeenPlayer)
|
|
{
|
|
if ( SeenPlayer == Enemy )
|
|
{
|
|
if ( Level.timeseconds - ChallengeTime > 7 )
|
|
{
|
|
ChallengeTime = Level.TimeSeconds;
|
|
//Monster(Pawn).PlayChallengeSound();
|
|
}
|
|
VisibleEnemy = Enemy;
|
|
EnemyVisibilityTime = Level.TimeSeconds;
|
|
bEnemyIsVisible = true;
|
|
Focus = Enemy;
|
|
WhatToDoNext(22);
|
|
}
|
|
else
|
|
Global.SeePlayer(SeenPlayer);
|
|
}
|
|
|
|
function Timer()
|
|
{
|
|
SetCombatTimer();
|
|
StopFiring();
|
|
}
|
|
|
|
function PickDestination()
|
|
{
|
|
local vector nextSpot, ViewSpot,Dir;
|
|
local float posZ;
|
|
local bool bCanSeeLastSeen;
|
|
|
|
// If no enemy, or I should see him but don't, then give up
|
|
if ( (Enemy == None) || (Enemy.Health <= 0) )
|
|
{
|
|
Enemy = None;
|
|
WhatToDoNext(23);
|
|
return;
|
|
}
|
|
|
|
if ( Pawn.JumpZ > 0 )
|
|
Pawn.bCanJump = true;
|
|
|
|
if ( ActorReachable(Enemy) )
|
|
{
|
|
Destination = Enemy.Location;
|
|
MoveTarget = None;
|
|
return;
|
|
}
|
|
|
|
ViewSpot = Pawn.Location + Pawn.BaseEyeHeight * vect(0,0,1);
|
|
bCanSeeLastSeen = bEnemyInfoValid && FastTrace(LastSeenPos, ViewSpot);
|
|
|
|
if ( FindBestPathToward(Enemy, true,true) )
|
|
return;
|
|
|
|
if ( bSoaking && (Physics != PHYS_Falling) )
|
|
SoakStop("COULDN'T FIND PATH TO ENEMY "$Enemy);
|
|
|
|
MoveTarget = None;
|
|
if ( !bEnemyInfoValid )
|
|
{
|
|
Enemy = None;
|
|
WhatToDoNext(26);
|
|
return;
|
|
}
|
|
|
|
Destination = LastSeeingPos;
|
|
bEnemyInfoValid = false;
|
|
if ( FastTrace(Enemy.Location, ViewSpot)
|
|
&& VSize(Pawn.Location - Destination) > Pawn.CollisionRadius )
|
|
{
|
|
SeePlayer(Enemy);
|
|
return;
|
|
}
|
|
|
|
posZ = LastSeenPos.Z + Pawn.CollisionHeight - Enemy.CollisionHeight;
|
|
nextSpot = LastSeenPos - Normal(Enemy.Velocity) * Pawn.CollisionRadius;
|
|
nextSpot.Z = posZ;
|
|
if ( FastTrace(nextSpot, ViewSpot) )
|
|
Destination = nextSpot;
|
|
else if ( bCanSeeLastSeen )
|
|
{
|
|
Dir = Pawn.Location - LastSeenPos;
|
|
Dir.Z = 0;
|
|
if ( VSize(Dir) < Pawn.CollisionRadius )
|
|
{
|
|
GoalString = "Stakeout 3 from hunt";
|
|
GotoState('StakeOut');
|
|
return;
|
|
}
|
|
Destination = LastSeenPos;
|
|
}
|
|
else
|
|
{
|
|
Destination = LastSeenPos;
|
|
if ( !FastTrace(LastSeenPos, ViewSpot) )
|
|
{
|
|
// check if could adjust and see it
|
|
if ( PickWallAdjust(Normal(LastSeenPos - ViewSpot)) || FindViewSpot() )
|
|
{
|
|
if ( Pawn.Physics == PHYS_Falling )
|
|
SetFall();
|
|
else
|
|
GotoState('Hunting', 'AdjustFromWall');
|
|
}
|
|
else
|
|
{
|
|
GoalString = "Stakeout 2 from hunt";
|
|
GotoState('StakeOut');
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function bool FindViewSpot()
|
|
{
|
|
local vector X,Y,Z;
|
|
|
|
GetAxes(Rotation,X,Y,Z);
|
|
|
|
// try left and right
|
|
|
|
if ( FastTrace(Enemy.Location, Pawn.Location + 2 * Y * Pawn.CollisionRadius) )
|
|
{
|
|
Destination = Pawn.Location + 2.5 * Y * Pawn.CollisionRadius;
|
|
return true;
|
|
}
|
|
|
|
if ( FastTrace(Enemy.Location, Pawn.Location - 2 * Y * Pawn.CollisionRadius) )
|
|
{
|
|
Destination = Pawn.Location - 2.5 * Y * Pawn.CollisionRadius;
|
|
return true;
|
|
}
|
|
if ( FRand() < 0.5 )
|
|
Destination = Pawn.Location - 2.5 * Y * Pawn.CollisionRadius;
|
|
else
|
|
Destination = Pawn.Location - 2.5 * Y * Pawn.CollisionRadius;
|
|
return true;
|
|
}
|
|
|
|
function EndState()
|
|
{
|
|
if ( (Pawn != None) && (Pawn.JumpZ > 0) )
|
|
Pawn.bCanJump = true;
|
|
}
|
|
|
|
AdjustFromWall:
|
|
MoveTo(Destination, MoveTarget);
|
|
|
|
Begin:
|
|
WaitForLanding();
|
|
if ( CanSee(Enemy) )
|
|
SeePlayer(Enemy);
|
|
WaitForAnim:
|
|
PickDestination();
|
|
if ( Level.timeseconds - ChallengeTime > 10 )
|
|
{
|
|
ChallengeTime = Level.TimeSeconds;
|
|
// Monster(Pawn).PlayChallengeSound();
|
|
}
|
|
|
|
SpecialNavig:
|
|
if (MoveTarget == None)
|
|
MoveTo(Destination);
|
|
else
|
|
MoveToward(MoveTarget,FaceActor(10),,(FRand() < 0.75) && ShouldStrafeTo(MoveTarget));
|
|
|
|
WhatToDoNext(27);
|
|
if ( bSoaking )
|
|
SoakStop("STUCK IN HUNTING!");
|
|
}
|
|
|
|
function bool Stopped()
|
|
{
|
|
return bPreparingMove;
|
|
}
|
|
|
|
function ExecuteWhatToDoNext()
|
|
{
|
|
|
|
bHasFired = false;
|
|
GoalString = "WhatToDoNext at "$Level.TimeSeconds;
|
|
if ( Pawn == None )
|
|
{
|
|
warn(GetHumanReadableName()$" WhatToDoNext with no pawn");
|
|
return;
|
|
}
|
|
|
|
if ( bPreparingMove )
|
|
{
|
|
Pawn.Acceleration = vect(0,0,0);
|
|
// GotoState('WaitForAnim');
|
|
return;
|
|
}
|
|
if (Pawn.Physics == PHYS_None)
|
|
Pawn.SetMovementPhysics();
|
|
if ( (Pawn.Physics == PHYS_Falling) && DoWaitForLanding() )
|
|
return;
|
|
if ( (Enemy != None) && ((Enemy.Health <= 0) || (Enemy.Controller == None)) )
|
|
Enemy = None;
|
|
|
|
if ( Level.Game.bGameEnded && (Enemy != None) && Enemy.Controller.bIsPlayer )
|
|
Enemy = None;
|
|
|
|
if ( (Enemy == None)) // || !EnemyVisible()
|
|
FindNewEnemy();
|
|
|
|
if ( Enemy != None )
|
|
ChooseAttackMode();
|
|
else
|
|
{
|
|
GoalString = "WhatToDoNext Wander or Camp at "$Level.TimeSeconds;
|
|
WanderOrCamp(true);
|
|
}
|
|
|
|
SwitchToBestWeapon();
|
|
}
|
|
|
|
function bool CheckPathToGoalAround(Pawn P)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
|
|
state Fallback extends MoveToGoalWithEnemy
|
|
{
|
|
|
|
function bool IsRetreating()
|
|
{
|
|
return ( (Pawn.Acceleration Dot (Pawn.Location - Enemy.Location)) > 0 );
|
|
}
|
|
|
|
event bool NotifyBump(actor Other)
|
|
{
|
|
local Pawn P;
|
|
|
|
Disable('NotifyBump');
|
|
if ( MoveTarget == Other )
|
|
{
|
|
if ( MoveTarget == Enemy && Pawn.HasWeapon() )
|
|
{
|
|
TimedFireWeaponAtEnemy();
|
|
DoRangedAttackOn(Enemy);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
P = Pawn(Other);
|
|
|
|
if ( (P == None) || (P.Controller == None) )
|
|
return false;
|
|
|
|
if ( !SameTeamAs(P.Controller) && (MoveTarget == RouteCache[0]) && (RouteCache[1] != None) && P.ReachedDestination(MoveTarget) )
|
|
{
|
|
MoveTimer = VSize(RouteCache[1].Location - Pawn.Location)/(Pawn.GroundSpeed * Pawn.DesiredSpeed) + 1;
|
|
MoveTarget = RouteCache[1];
|
|
}
|
|
|
|
SetEnemy(P);
|
|
|
|
if ( Enemy == Other )
|
|
{
|
|
Focus = Enemy;
|
|
TimedFireWeaponAtEnemy();
|
|
}
|
|
if ( CheckPathToGoalAround(P) )
|
|
return false;
|
|
|
|
AdjustAround(P);
|
|
return false;
|
|
}
|
|
|
|
function MayFall()
|
|
{
|
|
Pawn.bCanJump = ( MoveTarget != None && (MoveTarget.Physics != PHYS_Falling) );
|
|
}
|
|
|
|
function EnemyNotVisible()
|
|
{
|
|
if ( !FindNewEnemy() || (Enemy == None) )
|
|
WhatToDoNext(13);
|
|
else
|
|
{
|
|
enable('SeePlayer');
|
|
disable('EnemyNotVisible');
|
|
}
|
|
}
|
|
|
|
function EnemyChanged(bool bNewEnemyVisible)
|
|
{
|
|
bEnemyAcquired = false;
|
|
SetEnemyInfo(bNewEnemyVisible);
|
|
if ( bNewEnemyVisible )
|
|
{
|
|
disable('SeePlayer');
|
|
enable('EnemyNotVisible');
|
|
}
|
|
}
|
|
|
|
Begin:
|
|
WaitForLanding();
|
|
|
|
Moving:
|
|
MoveTarget = RouteCache[1];
|
|
MoveToward(MoveTarget,FaceActor(1),,ShouldStrafeTo(MoveTarget));
|
|
WhatToDoNext(14);
|
|
if ( bSoaking )
|
|
SoakStop("STUCK IN FALLBACK!");
|
|
goalstring = goalstring$" STUCK IN FALLBACK!";
|
|
}
|
|
|
|
/* ChooseAttackMode()
|
|
Handles tactical attacking state selection - choose which type of attack to do from here
|
|
*/
|
|
function ChooseAttackMode()
|
|
{
|
|
GoalString = " ChooseAttackMode last seen "$(Level.TimeSeconds - LastSeenTime);
|
|
// should I run away?
|
|
if ( (pawn.Health / pawn.HealthMax) <= 0.25 || VSize(location - enemy.Location) < 50 && pawn.Weapon != none && !pawn.Weapon.bMeleeWeapon)
|
|
{
|
|
GoalString = "Retreat";
|
|
GotoState('FallBack');
|
|
}
|
|
GoalString = "ChooseAttackMode FightEnemy";
|
|
FightEnemy(true);
|
|
}
|
|
|
|
function float AdjustAimError(float aimerror, float TargetDist, bool bDefendMelee, bool bInstantProj, bool bLeadTargetNow )
|
|
{
|
|
if ( (Pawn(Target) != None) && (Pawn(Target).Visibility < 2) )
|
|
aimerror *= 2.5;
|
|
|
|
// figure out the relative motion of the target across the bots view, and adjust aim error
|
|
// based on magnitude of relative motion
|
|
aimerror = aimerror * FMin(5,(12 - 11 *
|
|
(Normal(Target.Location - Pawn.Location) Dot Normal((Target.Location + 1.2 * Target.Velocity) - (Pawn.Location + Pawn.Velocity)))));
|
|
|
|
// if enemy is charging straight at bot with a melee weapon, improve aim
|
|
if ( bDefendMelee )
|
|
aimerror *= 0.5;
|
|
|
|
if ( Target.Velocity == vect(0,0,0) )
|
|
aimerror *= 0.6;
|
|
|
|
// aiming improves over time if stopped
|
|
if ( Stopped() && (Level.TimeSeconds > StopStartTime) )
|
|
{
|
|
if ( (Skill+Accuracy) > 4 )
|
|
aimerror *= 0.9;
|
|
aimerror *= FClamp((2 - 0.08 * FMin(skill,7) - FRand())/(Level.TimeSeconds - StopStartTime + 0.4),0.7,1.0);
|
|
}
|
|
|
|
// adjust aim error based on skill
|
|
if ( !bDefendMelee )
|
|
aimerror *= (3.3 - 0.37 * (FClamp(skill+Accuracy,0,8.5) + 0.5 * FRand()));
|
|
|
|
// Bots don't aim as well if recently hit, or if they or their target is flying through the air
|
|
if ( ((skill < 7) || (FRand()<0.5)) && (Level.TimeSeconds - Pawn.LastPainTime < 0.2) )
|
|
aimerror *= 1.3;
|
|
if ( (Pawn.Physics == PHYS_Falling) || (Target.Physics == PHYS_Falling) )
|
|
aimerror *= 1.6;
|
|
|
|
// Bots don't aim as well at recently acquired targets (because they haven't had a chance to lock in to the target)
|
|
if ( AcquireTime > Level.TimeSeconds - 0.5 - 0.6 * (7 - skill) )
|
|
{
|
|
aimerror *= 1.5;
|
|
if ( bInstantProj )
|
|
aimerror *= 1.5;
|
|
}
|
|
return (Rand(2 * aimerror) - aimerror);
|
|
}
|
|
|
|
defaultproperties
|
|
{
|
|
|
|
}
|