rott/kf_sources/KFMod/Classes/KFFriendlyAI.uc
2026-07-14 20:27:09 +07:00

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
{
}