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

1196 lines
32 KiB
Ucode

class KFInvasionBot extends InvasionBot;
// Shopping State
// General data for all code to use if needed
var float LastShopTime;
var float LastHealTime;
// Internal to state
var WeaponLocker TargetLocker;
var NavigationPoint ShoppingPath;
var vector MeLoc, LockLoc;
var float LockerDist;
var float LockerHeight;
var KFDoorMover TargetDoor;
var Syringe MySyringe;
var Inventory inv;
var Welder ActiveWelder;
var KFHumanPawn InjuredAlly;
var bool bHasChecked;
var int HealPoint; // Bots will not consider anyone above this threshold in need of medicial assistance
var float HealDist; // Bot will only travel so far to heal someone
// 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 )
{
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;
}
// There was so much cruft in the UT2K version function that isn't
// relevant to KF. Lets lighten the load
function Actor FaceActor(float StrafingModifier)
{
local actor SquadFace; //, N;
//TODO - do we need this?
SquadFace = Squad.SetFacingActor(self);
if ( SquadFace != None )
return SquadFace;
bRecommendFastMove = false;
if ( Enemy == none || Level.TimeSeconds - LastSeenTime > 4 - StrafingModifier)
return FaceMoveTarget();
// Gibber - trimmed this one down to happen regardless of skill level
if ( (Pawn.Weapon != None && Pawn.Weapon.bMeleeWeapon ) )
return FaceMoveTarget();
return Enemy;
}
function bool DefendMelee(float Dist)
{
return (Super.DefendMelee(Dist) || (KFMonster(Enemy) != None && Dist <800));
}
function bool FindInjuredAlly()
{
local controller c;
local KFHumanPawn aKFHPawn;
local float AllyDist;
local float BestDist;
InjuredAlly=none;
BestDist = HealDist;
if(FindMySyringe()==none)
return false;
if(!MySyringe.GetFireMode(0).AllowFire() )
return false;
for(c=level.ControllerList; c!=none; c=c.nextController)
{
aKFHPawn = kfHumanPawn(c.pawn);
// If he's dead. dont bother.
if (aKFHPawn!= none &&
aKFHPawn.Health <= 0)
return false;
if(aKFHPawn!=none && c!=self )
{
if(aKFHPawn.Health < aKFHPawn.HealthMax)
{
AllyDist = vsize(location - AKFHPawn.Location);
if(InjuredAlly==none && (AllyDist < HealDist) )
{
InjuredAlly = aKFHPawn;
BestDist = AllyDist;
}
else
{
// TODO: Weight it so that a seriously injured ally is given
// preference even if slightly further away
if(AllyDist < BestDist)
{
InjuredAlly = aKFHPawn;
BestDist = AllyDist;
}
}
}
}
}
return !(InjuredAlly == none);
}
function bool EnemyReallyScary()
{
// TODO: We might need deeper logic than this.
// This is enough to get Medic behaviour going though
if(enemy==none)
return false;
else
return ( vsize(location - enemy.Location) < HealDist);
}
// Gibber - stripping out vehicle stuff, adding shopping, better item scavaging
// using the syringe and all the other little tweaks
function ExecuteWhatToDoNext()
{
local float WeaponRating;
local Controller C;
local PlayerController AdminPC;
local KFHumanPawn AdminPawn;
local KFWeapon AdminWeapon;
bHasFired = false;
GoalString = "WhatToDoNext at "$Level.TimeSeconds;
if ( Pawn == None )
{
warn(GetHumanReadableName()$" WhatToDoNext with no pawn");
return;
}
// BOT FLASHLIGHT COMMANDS
// Find Admin, take commands from him
for ( C=Level.ControllerList; C!=None; C=C.NextController )
if ( C!= none && C.IsA('KFPlayerController') && PlayerController(C).PlayerReplicationInfo.bAdmin)
{
AdminPC = PlayerController(C);
if(AdminPC != none)
AdminPawn = KFHumanPawn(AdminPC.pawn);
if(AdminPawn != none)
AdminWeapon = KFWeapon(AdminPawn.Weapon);
}
// Any of this is only relevant if theres an admin, and he has a flashlight weapon
if (AdminWeapon != none && AdminWeapon.bTorchEnabled)
{
// If it's active....
if(AdminWeapon.Flashlight != none && AdminWeapon.Flashlight.bHaslight)
{
// If we have a flashlight weapon ourselves, then turn it on. If its already on, no change.
if(KFWeapon(pawn.weapon) != none &&
KFWeapon(pawn.weapon).bTorchEnabled)
{
if(KFWeapon(pawn.weapon).Flashlight == none || KFWeapon(pawn.weapon).Flashlight != none && !KFWeapon(pawn.weapon).Flashlight.bhaslight )
KFWeapon(pawn.Weapon).LightFire();
}
}
else if(AdminWeapon.Flashlight != none && !AdminWeapon.Flashlight.bHaslight)
{
// If we have a flashlight that's on , and his is off. turn ours off, too.
if(KFWeapon(pawn.weapon) != none && KFWeapon(pawn.weapon).bTorchEnabled)
{
if(KFWeapon(pawn.weapon).Flashlight != none && KFWeapon(pawn.weapon).Flashlight.bhaslight)
KFWeapon(pawn.Weapon).LightFire();
}
}
}
if ( Enemy == None )
{
if ( Level.Game.TooManyBots(self) )
{
if ( Pawn != None )
{
Pawn.Health = 0;
Pawn.Died( self, class'Suicided', Pawn.Location );
}
Destroy();
return;
}
BlockedPath = None;
bFrustrated = false;
if (Target == None || (Pawn(Target) != None && Pawn(Target).Health <= 0))
StopFiring();
}
if ( ScriptingOverridesAI() && ShouldPerformScript() )
return;
if (Pawn.Physics == PHYS_None)
Pawn.SetMovementPhysics();
if ( (Pawn.Physics == PHYS_Falling) && DoWaitForLanding() )
return;
if ( (StartleActor != None) && !StartleActor.bDeleteMe && (VSize(StartleActor.Location - Pawn.Location) < StartleActor.CollisionRadius) )
{
Startle(StartleActor);
return;
}
bIgnoreEnemyChange = true;
if ( (Enemy != None) && ((Enemy.Health <= 0) || (Enemy.Controller == None)) )
LoseEnemy();
if ( Enemy == None )
Squad.FindNewEnemyFor(self,false);
else if ( !Squad.MustKeepEnemy(Enemy) && !EnemyVisible() )
{
// decide if should lose enemy
// TODO - is losing enemies the right thing to do?
// do we need SquadAI?
if ( Squad.IsDefending(self) )
{
if ( LostContact(4) )
LoseEnemy();
}
else if ( LostContact(7) )
LoseEnemy();
}
bIgnoreEnemyChange = false;
if( Enemy==none && ShouldGoShopping() && GoShopping() ) { Return; }
// do nothing. All the magic is in the line above
else
{
if(FindInjuredAlly() && !EnemyReallyScary() && CanDoHeal() )
{
GoHealing();
return;
}
else if ( AssignSquadResponsibility() )
{
if ( Pawn == None )
return;
SwitchToBestWeapon();
return;
}
if ( ShouldPerformScript() )
return;
if ( Enemy != None )
ChooseAttackMode();
else
{
WeaponRating = Pawn.Weapon.CurrentRating/2000;
if ( FindInventoryGoal(WeaponRating) )
{
if ( InventorySpot(RouteGoal) == None )
GoalString = "fallback - inventory goal is not pickup but "$RouteGoal;
else
GoalString = "Fallback to better pickup "$InventorySpot(RouteGoal).markedItem$" hidden "$InventorySpot(RouteGoal).markedItem.bHidden;
GotoState('FallBack');
}
else
{
// No enemy and no ammo to grab. Guess all there is left to do is to chill out
GoalString = "WhatToDoNext Wander or Camp at "$Level.TimeSeconds;
WanderOrCamp(true);
}
}
}
SwitchToBestWeapon();
}
function bool DesperateForAmmo()
{
local float AmmoPercent;
local actor invit;
for(invit=self; invit!=none; invit=invit.Inventory )
{
if(Weapon(invit)!=none)
{
AmmoPercent= (1.0f * Weapon(invit).AmmoCharge[0])/Weapon(invit).AmmoClass[0].default.MaxAmmo;
if(AmmoPercent > 0.2) // 0.2 = arbitary low value
return false;
}
}
return true;
}
function bool ShouldGoShopping()
{
// Can't shop if the shop ain't open
if( KFGameType(Level.Game).bWaveInProgress )
return false;
// Don't need to shop if we've just shopped
if( LastShopTime>level.TimeSeconds )
return false;
if( !bHasChecked && FRand()<0.35 )
{
LastShopTime = level.TimeSeconds+FRand()*120;
bHasChecked = True;
Return False;
}
// At the end of the day, it's all about having money, really
return (PlayerReplicationInfo.score>=20);
}
function bool CanDoHeal()
{
if(LastHealTime+2 > level.TimeSeconds)
return false;
if( FindMySyringe()==none || !(MySyringe.GetFireMode(0).AllowFire() ) || InjuredAlly==none || InjuredAlly.health <= 0 ||
(InjuredAlly.healthToGive + InjuredAlly.health >= InjuredAlly.Healthmax) )
return false;
else return true;
}
function array<class<Pickup> > GetLegalPurchases()
{
local int i,j;
local KFLevelRules KFLR;
local array<class<Pickup> > RetList;
KFLR = KFGameType(Level.game).KFLRules;
for(i=0; i<KFLR.MAX_BUYITEMS; i++ )
{
if( CanAfford(KFLR.ItemForSale[i] ) )
{
RetList.length = j+1;
RetList[j++] = KFLR.ItemForSale[i];
}
}
return RetList;
}
function bool CanAfford(class<Pickup> aItem)
{
local class<kfWeaponPickup> aWeapon;
local actor InvIt;
local KFWeapon Weap;
local bool bFoundInInventory;
aWeapon = class<kfWeaponPickup>(aItem);
if(aWeapon!=none)
{
bFoundInInventory=false;
for(InvIt=pawn; InvIt!=none; InvIt=InvIt.Inventory)
{
Weap = KFWeapon(InvIt);
if(Weap!=none)
{
bFoundInInventory=true;
if(Weap.AmmoClass[0]!=none && Weap.Class==aWeapon.default.InventoryType)
{
if( PlayerReplicationInfo.score>aWeapon.default.ammocost )
return true;
}
}
}
// if we didn't find it above, we need to see if we can buy the whole gun, not just ammo
if(!bFoundInInventory && aWeapon.default.Cost < self.PlayerReplicationInfo.score && aWeapon.default.weight < (KFHumanPawn(pawn).MaxCarryWeight - KFHumanPawn(pawn).CurrentWeight) )
return true;
}
return false;
}
function KFWeapon FindWeaponInInv(Class<KFWeaponPickup> TargetClass)
{
local actor InvIt;
local KFWeapon Weap;
if( TargetClass==None )
Return None;
for( InvIt=pawn; InvIt!=none; InvIt=InvIt.Inventory)
{
Weap = KFWeapon(InvIt);
if(Weap!=none && Weap.Class==TargetClass.default.InventoryType )
return Weap;
}
return none;
}
function DoTrading()
{
local KFWeapon Weap;
local int i;
local class<KFWeaponPickup> BuyWeapClass;
local int NumCanAfford, NumNeeded, NumToBuy;
local array<class<Pickup> > ShoppingList;
local int OldCash;
local byte LCount;
LastShopTime = level.TimeSeconds+30+500*FRand();
OldCash = PlayerReplicationInfo.score + 1;
while ( (PlayerReplicationInfo.score > 20) && PlayerReplicationInfo.score!=OldCash && LCount++<10 )
{
OldCash = PlayerReplicationInfo.score;
ShoppingList = GetLegalPurchases();
if( ShoppingList.Length==0 )
Break;
// kludge to stack the odds to the low numbers where the best kit is ;)
if(ShoppingList.length < 3 || frand() < 0.5)
i = rand(ShoppingList.length);
else i = rand(0.5*ShoppingList.length);
BuyWeapClass = class<KFWeaponPickup>(ShoppingList[i]);
if( BuyWeapClass==None )
Continue;
Weap = FindWeaponInInv(BuyWeapClass);
if(Weap!=none) // already own gun, buy ammo
{
NumCanAfford = self.PlayerReplicationInfo.score / (BuyWeapClass.default.ammocost);
NumNeeded = (Weap.AmmoClass[0].default.MaxAmmo-Weap.AmmoCharge[0]) / Weap.MagCapacity;
NumToBuy = Min(NumCanAfford, NumNeeded);
PlayerReplicationInfo.score -= (BuyWeapClass.default.ammocost) * NumToBuy;
Weap.AddAmmo(Weap.MagCapacity * NumToBuy, 0);
}
else // buy that gun
{
Weap = KFWeapon(Spawn(BuyWeapClass.default.InventoryType));
if( Weap!=None )
Weap.GiveTo(pawn);
PlayerReplicationInfo.score -= BuyWeapClass.default.cost;
}
}
SwitchToBestWeapon();
}
function bool GoShopping()
{
if( !GetNearestShop() )
Return false;
GoalString = "SHOPPING";
GotoState('Shopping');
return true;
}
function bool GoHealing()
{
if(!IsInState('dead') && !IsInState('GameEnded') && InjuredAlly!=none)
{
GoalString = "HEALING";
GotoState('Healing');
return true;
}
else return false;
}
function Syringe FindMySyringe()
{
if( MySyringe!=none && MySyringe.Owner==Pawn )
return MySyringe;
MySyringe = None;
for(inv=pawn.Inventory; inv!=none; inv=inv.Inventory)
{
if( Syringe(inv)!=none )
MySyringe = Syringe(inv);
}
return MySyringe;
}
function bool GetNearestShop()
{
local KFGameType KFGT;
local int i,l;
local float Dist,BDist;
local ShopVolume Sp;
KFGT = KFGameType(Level.Game);
if( KFGT==None )
return false;
l = KFGT.ShopList.Length;
for( i=0; i<l; i++ )
{
if( !KFGT.ShopList[i].bCurrentlyOpen || KFGT.ShopList[i].BotPoint==None )
continue;
Dist = VSize(KFGT.ShopList[i].Location-Pawn.Location);
if( Dist<BDist || Sp==None )
{
Sp = KFGT.ShopList[i];
BDist = Dist;
}
}
if( Sp==None )
return false;
ShoppingPath = Sp.BotPoint;
return true;
}
state Shopping extends MoveToGoalNoEnemy
{
ignores EnemyNotVisible;
Begin:
WaitForLanding();
bHasChecked = False;
KeepMoving:
if( KFGameType(Level.Game).bWaveInProgress )
{
LastShopTime = level.TimeSeconds+15+FRand()*30;
WhatToDoNext(152);
}
if( ActorReachable(ShoppingPath) )
MoveToward(ShoppingPath,FaceActor(1),,false);
else
{
MoveTarget = FindPathToward(ShoppingPath);
if( MoveTarget!=none )
MoveToward(MoveTarget,FaceActor(1),,false );
else
{
LastShopTime = level.TimeSeconds+8+FRand()*10;
WhatToDoNext(151);
}
Goto('KeepMoving');
}
Focus = TargetLocker;
Pawn.Acceleration = vect(0,0,0);
Sleep(1+FRand()*3);
DoTrading();
WhatToDoNext(152);
if ( bSoaking )
SoakStop("STUCK IN SHOPPING!");
}
state Healing extends MoveToGoalWithEnemy
{
function TimedFireWeaponAtEnemy()
{
if ( Syringe(Pawn.Weapon)!=none )
{
if(InjuredAlly==none || FireWeaponAt(InjuredAlly))
SetCombatTimer();
else
SetTimer(0.1, True);
}
else
{
global.TimedFireWeaponAtEnemy();
}
}
function SwitchToBestWeapon()
{
if(pawn.Weapon==MySyringe || pawn.PendingWeapon==MySyringe)
return;
else global.SwitchToBestWeapon();
}
function Actor FaceActor(float StrafingModifier)
{
if(Syringe(Pawn.Weapon)!=none || enemy==none)
return InjuredAlly;
else
return Enemy;
}
// TODO: whip syringe out earlier, especially if not under enemy pressure
Begin:
SwitchToBestWeapon();
WaitForLanding();
KeepMoving:
if(enemy==none || vsize(pawn.location -InjuredAlly.location) < vsize(pawn.Location - enemy.location) )
ClientSetWeapon(class'Syringe');
MoveTarget = FindPathToward(InjuredAlly);
if(MoveTarget!=none)
{
MoveToward(MoveTarget,FaceActor(1),,false ); //,GetDesiredOffset(),ShouldStrafeTo(MoveTarget));
}
else
{
LastHealTime = level.TimeSeconds;
// TODO: find why we need the cheap fix above
WhatToDoNext(151);
}
if(MySyringe==none)
{
FindMySyringe();
}
if(InjuredAlly!=none)
{
MeLoc = pawn.Location;
LockLoc = InjuredAlly.Location;
LockerHeight = abs(MeLoc.Z - LockLoc.Z);
MeLoc.Z = 0;
LockLoc.Z = 0;
LockerDist = VSize(MeLoc - LockLoc);
// TODO - do we need to use TriggerHeight/2? Don't know, must check
// right now it'll prove the concept however
if( LockerDist < MySyringe.weaponRange &&
LockerHeight < InjuredAlly.CollisionHeight )
{
// TODO: Proper needle stabby (probably want to add check
// to ExecuteWhatToDoNext() or related for low syringeage,
// or target still under the effects of recent syringification
// TODO: exhaustive check of reasons to abandon stabbiness
if( !CanDoHeal() )
{
LastHealTime = level.TimeSeconds;
WhatToDoNext(162);
}
else
{
sleep(0.5);
goto('KeepMoving');
}
// UBERKLUDGE!!!
//InjuredAlly.Health = InjuredAlly.HealthMax;
}
else Goto('KeepMoving');
}
LastHealTime = level.TimeSeconds;
WhatToDoNext(163);
if ( bSoaking )
SoakStop("STUCK IN HEALING!");
}
function SetPawnClass(string inClass, string inCharacter)
{
}
/* ChooseAttackMode()
Handles tactical attacking state selection - choose which type of attack to do from here
*/
function ChooseAttackMode()
{
local float EnemyStrength, WeaponRating;
GoalString = " ChooseAttackMode last seen "$(Level.TimeSeconds - LastSeenTime);
// should I run away?
if ( (Squad == None) || (Enemy == None) || (Pawn == None) )
log("HERE 1 Squad "$Squad$" Enemy "$Enemy$" pawn "$Pawn);
EnemyStrength = RelativeStrength(Enemy);
/*
if ( Vehicle(Pawn) != None )
{
VehicleFightEnemy(true, EnemyStrength);
return;
}
*/
// This is where the new pawn retreat conditions will be. Keep it simple, stupid.
// if he's hurt. He'll run.
if ( (pawn.Health / pawn.HealthMax) <= 0.25 ||
VSize(location - enemy.Location) < 50 &&
pawn.Weapon != none &&
!pawn.Weapon.bMeleeWeapon)
{
GoalString = "Retreat";
DoRetreat();
GotoState('FallBack');
// Log("Fuck this, im outta here!");
}
/*
if ( !bFrustrated && !Squad.MustKeepEnemy(Enemy) )
{
RetreatThreshold = Aggressiveness;
if ( Pawn.Weapon.CurrentRating > 0.5 )
RetreatThreshold = RetreatThreshold + 0.35 - skill * 0.05;
if ( EnemyStrength > RetreatThreshold )
{
GoalString = "Retreat";
if ( (PlayerReplicationInfo.Team != None) && (FRand() < 0.05) )
SendMessage(None, 'Other', GetMessageIndex('INJURED'), 15, 'TEAM');
DoRetreat();
return;
}
}
*/
if ( (Squad.PriorityObjective(self) == 0) && (Skill + Tactics > 2) && ((EnemyStrength > -0.3) || (Pawn.Weapon.AIRating < 0.5)) )
{
if ( Pawn.Weapon.AIRating < 0.5 )
{
if ( EnemyStrength > 0.3 )
WeaponRating = 0;
else
WeaponRating = Pawn.Weapon.CurrentRating/2000;
}
else if ( EnemyStrength > 0.3 )
WeaponRating = Pawn.Weapon.CurrentRating/2000;
else
WeaponRating = Pawn.Weapon.CurrentRating/1000;
// fallback to better pickup? No. you're in a combat sitatuion.
// being choosy about pickups should only come at round end. (for bots)
/*
if ( FindInventoryGoal(WeaponRating) )
{
if ( InventorySpot(RouteGoal) == None )
GoalString = "fallback - inventory goal is not pickup but "$RouteGoal;
else
GoalString = "Fallback to better pickup "$InventorySpot(RouteGoal).markedItem$" hidden "$InventorySpot(RouteGoal).markedItem.bHidden;
GotoState('FallBack');
return;
}
*/
}
GoalString = "ChooseAttackMode FightEnemy";
FightEnemy(true, EnemyStrength);
}
function FightEnemy(bool bCanCharge, float EnemyStrength)
{
local vector X,Y,Z;
local float enemyDist;
local float AdjustedCombatStyle;
local bool bFarAway, bOldForcedCharge;
if ( (Squad == None) || (Enemy == None) || (Pawn == None) )
log("HERE 3 Squad "$Squad$" Enemy "$Enemy$" pawn "$Pawn);
if ( (Enemy == FailedHuntEnemy) && (Level.TimeSeconds == FailedHuntTime) )
{
GoalString = "FAILED HUNT - HANG OUT";
if ( EnemyVisible() )
bCanCharge = false;
else if ( FindInventoryGoal(0) )
{
SetAttractionState();
return;
}
else
{
WanderOrCamp(true);
return;
}
}
bOldForcedCharge = bMustCharge;
bMustCharge = false;
enemyDist = VSize(Pawn.Location - Enemy.Location);
if( Pawn.Weapon==None )
AdjustedCombatStyle = CombatStyle;
else AdjustedCombatStyle = CombatStyle + Pawn.Weapon.SuggestAttackStyle();
Aggression = 1.5 * FRand() - 0.8 + 2 * AdjustedCombatStyle - 0.5 * EnemyStrength + FRand() * (Normal(Enemy.Velocity - Pawn.Velocity) Dot Normal(Enemy.Location - Pawn.Location));
if ( enemyDist > MAXSTAKEOUTDIST )
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
BlockedPath = None;
Target = Enemy;
if( (Pawn.Weapon!=None && Pawn.Weapon.bMeleeWeapon) || (bCanCharge && bOldForcedCharge) )
{
GoalString = "Charge";
DoCharge();
return;
}
if ( Pawn.Weapon!=None && !Pawn.Weapon.bMeleeWeapon )
{
GoalString = "Long Ranged Attack";
DoRangedAttackOn(Enemy);
return;
}
if ( bCanCharge && (Skill < 5) && bFarAway && (Aggression > 1) && (FRand() < 0.5) )
{
GoalString = "Charge closer";
DoCharge();
return;
}
if ( (Pawn.Weapon!=None && Pawn.Weapon.RecommendRangedAttack()) || IsSniping() || ((FRand() > 0.17 * (skill + Tactics - 1)) && !DefendMelee(enemyDist)) )
{
GoalString = "Ranged Attack";
DoRangedAttackOn(Enemy);
return;
}
if ( bCanCharge )
{
if ( Aggression > 1 )
{
GoalString = "Charge 2";
DoCharge();
return;
}
}
if( (pawn.Health / pawn.HealthMax) <= 0.25 || VSize(location - enemy.Location) < 50 && pawn.Weapon != none && !pawn.Weapon.bMeleeWeapon )
{
GoalString = "Retreat";
DoRetreat();
GotoState('FallBack');
}
GoalString = "Do tactical move";
if ( Pawn.Weapon!=None && !Pawn.Weapon.RecommendSplashDamage() && (FRand() < 0.7) && (3*Jumpiness + 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 DirectedWander(vector WanderDir)
{
GoalString = "DIRECTED WANDER "$GoalString;
Pawn.bWantsToCrouch = false;
if ( TestDirection(WanderDir,Destination) )
GotoState('RestFormation', 'Moving');
else
GotoState('RestFormation', 'Begin');
}
function WanderOrCamp(bool bMayCrouch)
{
Pawn.bWantsToCrouch = false;
GotoState('RestFormation');
}
function SealUpDoor( KFDoorMover Door ) // Called from door whenever bot should unseal this.
{
local Welder W;
if( Enemy!=None && LineOfSightTo(Enemy) )
Return;
W = Welder(Pawn.FindInventoryType(Class'Welder'));
if( W==None )
Return;
ActiveWelder = W;
TargetDoor = Door;
GoToState('UnWeldDoor');
}
State UnWeldDoor
{
Ignores NotifyBump;
function SwitchToBestWeapon()
{
if ( Pawn==None || Pawn.Inventory==None || Pawn.Weapon==ActiveWelder )
return;
Pawn.PendingWeapon = ActiveWelder;
StopFiring();
if ( Pawn.Weapon == None )
Pawn.ChangedWeapon();
else Pawn.Weapon.PutDown();
}
function Timer()
{
if( Pawn.Weapon==ActiveWelder )
FireWeaponAt(TargetDoor);
}
function bool FireWeaponAt(Actor A)
{
Target = A;
if ( Pawn.Weapon==ActiveWelder )
return WeaponFireAgain(Pawn.Weapon.RefireRate(),false);
else return False;
}
function bool WeaponFireAgain(float RefireRate, bool bFinishedFire)
{
LastFireAttempt = Level.TimeSeconds;
Target = TargetDoor;
if( Pawn.Weapon==ActiveWelder )
{
Focus = Target;
bCanFire = true;
bStoppedFiring = false;
return Pawn.Weapon.BotFire(bFinishedFire);
}
StopFiring();
return false;
}
Begin:
Pawn.Acceleration = vect(0,0,0);
Focus = TargetDoor;
Target = TargetDoor;
While( Pawn.Weapon!=ActiveWelder )
{
SwitchToBestWeapon();
Sleep(0.25);
}
FireWeaponAt(TargetDoor);
Sleep(0.5);
if( TargetDoor.bSealed )
GoTo'Begin';
Global.SwitchToBestWeapon();
WhatToDoNext(12);
}
function SetMaxDesiredSpeed()
{
if ( Pawn != None )
Pawn.MaxDesiredSpeed = 1;
}
function SetPeripheralVision()
{
if ( Pawn == None )
return;
if ( Pawn.bStationary || (Pawn.Physics == PHYS_Flying) )
{
bSlowerZAcquire = false;
Pawn.PeripheralVision = -0.7;
return;
}
bSlowerZAcquire = false;
Pawn.PeripheralVision = -0.2;
Pawn.PeripheralVision = FMin(Pawn.PeripheralVision - BaseAlertness, 0.8);
Pawn.SightRadius = Pawn.Default.SightRadius;
}
function rotator AdjustAim(FireProperties FiredAmmunition, vector projStart, int aimerror)
{
local rotator FireRotation, TargetLook;
local float FireDist, TargetDist, ProjSpeed,TravelTime;
local actor HitActor;
local vector FireSpot, FireDir, TargetVel, HitLocation, HitNormal;
local int realYaw;
local bool bDefendMelee, bClean, bLeadTargetNow;
if ( FiredAmmunition.ProjectileClass != None )
projspeed = FiredAmmunition.ProjectileClass.default.speed;
// make sure bot has a valid target
if ( Target == None )
{
Target = Enemy;
if ( Target == None )
return Rotation;
}
if ( Pawn(Target) != None )
Target = Pawn(Target).GetAimTarget();
FireSpot = Target.Location;
TargetDist = VSize(Target.Location - Pawn.Location);
// perfect aim at stationary objects
if ( Pawn(Target) == None )
{
if ( !FiredAmmunition.bTossed )
return rotator(Target.Location - projstart);
else
{
FireDir = AdjustToss(projspeed,ProjStart,Target.Location,true);
SetRotation(Rotator(FireDir));
return Rotation;
}
}
bLeadTargetNow = FiredAmmunition.bLeadTarget && bLeadTarget;
bDefendMelee = ( (Target == Enemy) && DefendMelee(TargetDist) );
aimerror = AdjustAimError(aimerror,TargetDist,bDefendMelee,FiredAmmunition.bInstantHit, bLeadTargetNow);
// lead target with non instant hit projectiles
if ( bLeadTargetNow )
{
TargetVel = Target.Velocity;
TravelTime = TargetDist/projSpeed;
// hack guess at projecting falling velocity of target
if ( Target.Physics == PHYS_Falling )
{
if ( Target.PhysicsVolume.Gravity.Z <= Target.PhysicsVolume.Default.Gravity.Z )
TargetVel.Z = FMin(TargetVel.Z + FMax(-400, Target.PhysicsVolume.Gravity.Z * FMin(1,TargetDist/projSpeed)),0);
else
{
TargetVel.Z = TargetVel.Z + 0.5 * TravelTime * Target.PhysicsVolume.Gravity.Z;
FireSpot = Target.Location + TravelTime*TargetVel;
HitActor = Trace(HitLocation, HitNormal, FireSpot, Target.Location, false);
bLeadTargetNow = false;
if ( HitActor != None )
FireSpot = HitLocation + vect(0,0,2);
}
}
if ( bLeadTargetNow )
{
// more or less lead target (with some random variation)
FireSpot += FMin(1, 0.7 + 0.6 * FRand()) * TargetVel * TravelTime;
FireSpot.Z = FMin(Target.Location.Z, FireSpot.Z);
}
if ( (Target.Physics != PHYS_Falling) && (FRand() < 0.55) && (VSize(FireSpot - ProjStart) > 1000) )
{
// don't always lead far away targets, especially if they are moving sideways with respect to the bot
TargetLook = Target.Rotation;
if ( Target.Physics == PHYS_Walking )
TargetLook.Pitch = 0;
bClean = ( ((Vector(TargetLook) Dot Normal(Target.Velocity)) >= 0.71) && FastTrace(FireSpot, ProjStart) );
}
else // make sure that bot isn't leading into a wall
bClean = FastTrace(FireSpot, ProjStart);
if ( !bClean)
{
// reduce amount of leading
if ( FRand() < 0.3 )
FireSpot = Target.Location;
else
FireSpot = 0.5 * (FireSpot + Target.Location);
}
}
bClean = false; //so will fail first check unless shooting at feet
if ( FiredAmmunition.bTrySplash && (Pawn(Target) != None) && ((Skill >=4) || bDefendMelee)
&& (((Target.Physics == PHYS_Falling) && (Pawn.Location.Z + 80 >= Target.Location.Z))
|| ((Pawn.Location.Z + 19 >= Target.Location.Z) && (bDefendMelee || (skill > 6.5 * FRand() - 0.5)))) )
{
HitActor = Trace(HitLocation, HitNormal, FireSpot - vect(0,0,1) * (Target.CollisionHeight + 6), FireSpot, false);
bClean = (HitActor == None);
if ( !bClean )
{
FireSpot = HitLocation + vect(0,0,3);
bClean = FastTrace(FireSpot, ProjStart);
}
else
bClean = ( (Target.Physics == PHYS_Falling) && FastTrace(FireSpot, ProjStart) );
}
if ( Pawn.Weapon != None && Pawn.Weapon.bSniping )
{
// try head
FireSpot.Z = Target.Location.Z + 0.9 * Target.CollisionHeight;
bClean = FastTrace(FireSpot, ProjStart);
}
if ( !bClean )
{
//try middle
FireSpot.Z = Target.Location.Z;
bClean = FastTrace(FireSpot, ProjStart);
}
if ( FiredAmmunition.bTossed && !bClean && bEnemyInfoValid )
{
FireSpot = LastSeenPos;
HitActor = Trace(HitLocation, HitNormal, FireSpot, ProjStart, false);
if ( HitActor != None )
{
bCanFire = false;
FireSpot += 2 * Target.CollisionHeight * HitNormal;
}
bClean = true;
}
if( !bClean )
{
// try head
FireSpot.Z = Target.Location.Z + 0.9 * Target.CollisionHeight;
bClean = FastTrace(FireSpot, ProjStart);
}
if ( !bClean && (Target == Enemy) && bEnemyInfoValid )
{
FireSpot = LastSeenPos;
if ( Pawn.Location.Z >= LastSeenPos.Z )
FireSpot.Z -= 0.4 * Enemy.CollisionHeight;
HitActor = Trace(HitLocation, HitNormal, FireSpot, ProjStart, false);
if ( HitActor != None )
{
FireSpot = LastSeenPos + 2 * Enemy.CollisionHeight * HitNormal;
if ( Pawn.Weapon != None && Pawn.Weapon.SplashDamage() && (Skill >= 4) )
{
HitActor = Trace(HitLocation, HitNormal, FireSpot, ProjStart, false);
if ( HitActor != None )
FireSpot += 2 * Enemy.CollisionHeight * HitNormal;
}
if ( Pawn.Weapon != None && Pawn.Weapon.RefireRate() < 0.99 )
bCanFire = false;
}
}
// adjust for toss distance
if ( FiredAmmunition.bTossed )
FireDir = AdjustToss(projspeed,ProjStart,FireSpot,true);
else
{
FireDir = FireSpot - ProjStart;
if ( Pawn(Target) != None )
FireDir = FireDir + Pawn(Target).GetTargetLocation() - Target.Location;
}
FireRotation = Rotator(FireDir);
realYaw = FireRotation.Yaw;
FireRotation.Yaw = SetFireYaw(FireRotation.Yaw + aimerror);
FireDir = vector(FireRotation);
// avoid shooting into wall
FireDist = FMin(VSize(FireSpot-ProjStart), 400);
FireSpot = ProjStart + FireDist * FireDir;
HitActor = Trace(HitLocation, HitNormal, FireSpot, ProjStart, false);
if ( HitActor != None )
{
if ( HitNormal.Z < 0.7 )
{
FireRotation.Yaw = SetFireYaw(realYaw - aimerror);
FireDir = vector(FireRotation);
FireSpot = ProjStart + FireDist * FireDir;
HitActor = Trace(HitLocation, HitNormal, FireSpot, ProjStart, false);
}
if ( HitActor != None )
{
FireSpot += HitNormal * 2 * Target.CollisionHeight;
HitActor = Trace(HitLocation, HitNormal, FireSpot, ProjStart, false);
if ( HitActor != None )
FireSpot += Target.CollisionHeight * HitNormal;
FireDir = Normal(FireSpot - ProjStart);
FireRotation = rotator(FireDir);
}
}
InstantWarnTarget(Target,FiredAmmunition,vector(FireRotation));
ShotTarget = Pawn(Target);
SetRotation(FireRotation);
return FireRotation;
}
function float AdjustAimError(float aimerror, float TargetDist, bool bDefendMelee, bool bInstantProj, bool bLeadTargetNow )
{
return Super.AdjustAimError(aimerror,TargetDist,bDefendMelee,bInstantProj,bLeadTargetNow)*0.35;
}
defaultproperties
{
HealPoint=50
HealDist=2500.000000
Aggressiveness=1.000000
BaseAlertness=1.000000
Accuracy=1.000000
CombatStyle=-1.000000
ReactionTime=1.000000
FovAngle=360.000000
bAdrenalineEnabled=False
PawnClass=Class'KFMod.KFHumanPawn'
Skill = 7
}