/*
===========================================================================
Copyright (C) 2000 - 2013, Raven Software, Inc.
Copyright (C) 2001 - 2013, Activision, Inc.
Copyright (C) 2013 - 2015, OpenJK contributors
This file is part of the OpenJK source code.
OpenJK is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License version 2 as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, see .
===========================================================================
*/
////////////////////////////////////////////////////////////////////////////////////////
// RAVEN SOFTWARE - STAR WARS: JK II
// (c) 2002 Activision
//
// Troopers
//
// TODO
// ----
//
//
//
//
////////////////////////////////////////////////////////////////////////////////////////
// Includes
////////////////////////////////////////////////////////////////////////////////////////
#include "b_local.h"
#include "g_navigator.h"
#if !defined(RAVL_VEC_INC)
#include "../Ravl/CVec.h"
#endif
#if !defined(RATL_ARRAY_VS_INC)
#include "../Ratl/array_vs.h"
#endif
#if !defined(RATL_VECTOR_VS_INC)
#include "../Ratl/vector_vs.h"
#endif
#if !defined(RATL_HANDLE_POOL_VS_INC)
#include "../Ratl/handle_pool_vs.h"
#endif
#if !defined(RUFL_HSTRING_INC)
#include "../Rufl/hstring.h"
#endif
////////////////////////////////////////////////////////////////////////////////////////
// Defines
////////////////////////////////////////////////////////////////////////////////////////
#define MAX_TROOPS 100
#define MAX_ENTS_PER_TROOP 7
#define MAX_TROOP_JOIN_DIST2 1000000 //1000 units
#define MAX_TROOP_MERGE_DIST2 250000 //500 units
#define TARGET_POS_VISITED 10000 //100 units
bool NPC_IsTrooper(gentity_t* actor);
enum
{
SPEECH_CHASE,
SPEECH_CONFUSED,
SPEECH_COVER,
SPEECH_DETECTED,
SPEECH_GIVEUP,
SPEECH_LOOK,
SPEECH_LOST,
SPEECH_OUTFLANK,
SPEECH_ESCAPING,
SPEECH_SIGHT,
SPEECH_SOUND,
SPEECH_SUSPICIOUS,
SPEECH_YELL,
SPEECH_PUSHED
};
extern void G_AddVoiceEvent( gentity_t *self, int event, int speakDebounceTime );
extern void CG_DrawEdge( vec3_t start, vec3_t end, int type );
static void HT_Speech( gentity_t *self, int speechType, float failChance )
{
if ( Q_flrand(0.0f, 1.0f) < failChance )
{
return;
}
if ( failChance >= 0 )
{//a negative failChance makes it always talk
if ( self->NPC->group )
{//group AI speech debounce timer
if ( self->NPC->group->speechDebounceTime > level.time )
{
return;
}
/*
else if ( !self->NPC->group->enemy )
{
if ( groupSpeechDebounceTime[self->client->playerTeam] > level.time )
{
return;
}
}
*/
}
else if ( !TIMER_Done( self, "chatter" ) )
{//personal timer
return;
}
}
TIMER_Set( self, "chatter", Q_irand( 2000, 4000 ) );
if ( self->NPC->blockedSpeechDebounceTime > level.time )
{
return;
}
switch( speechType )
{
case SPEECH_CHASE:
G_AddVoiceEvent( self, Q_irand(EV_CHASE1, EV_CHASE3), 2000 );
break;
case SPEECH_CONFUSED:
G_AddVoiceEvent( self, Q_irand(EV_CONFUSE1, EV_CONFUSE3), 2000 );
break;
case SPEECH_COVER:
G_AddVoiceEvent( self, Q_irand(EV_COVER1, EV_COVER5), 2000 );
break;
case SPEECH_DETECTED:
G_AddVoiceEvent( self, Q_irand(EV_DETECTED1, EV_DETECTED5), 2000 );
break;
case SPEECH_GIVEUP:
G_AddVoiceEvent( self, Q_irand(EV_GIVEUP1, EV_GIVEUP4), 2000 );
break;
case SPEECH_LOOK:
G_AddVoiceEvent( self, Q_irand(EV_LOOK1, EV_LOOK2), 2000 );
break;
case SPEECH_LOST:
G_AddVoiceEvent( self, EV_LOST1, 2000 );
break;
case SPEECH_OUTFLANK:
G_AddVoiceEvent( self, Q_irand(EV_OUTFLANK1, EV_OUTFLANK2), 2000 );
break;
case SPEECH_ESCAPING:
G_AddVoiceEvent( self, Q_irand(EV_ESCAPING1, EV_ESCAPING3), 2000 );
break;
case SPEECH_SIGHT:
G_AddVoiceEvent( self, Q_irand(EV_SIGHT1, EV_SIGHT3), 2000 );
break;
case SPEECH_SOUND:
G_AddVoiceEvent( self, Q_irand(EV_SOUND1, EV_SOUND3), 2000 );
break;
case SPEECH_SUSPICIOUS:
G_AddVoiceEvent( self, Q_irand(EV_SUSPICIOUS1, EV_SUSPICIOUS5), 2000 );
break;
case SPEECH_YELL:
G_AddVoiceEvent( self, Q_irand( EV_ANGER1, EV_ANGER3 ), 2000 );
break;
case SPEECH_PUSHED:
G_AddVoiceEvent( self, Q_irand( EV_PUSHED1, EV_PUSHED3 ), 2000 );
break;
default:
break;
}
self->NPC->blockedSpeechDebounceTime = level.time + 2000;
}
////////////////////////////////////////////////////////////////////////////////////////
// The Troop
//
// Troopers primarly derive their behavior from cooperation as a collective group of
// individuals. They join Troops, each of which has a leader responsible for direcing
// the movement of the rest of the group.
//
////////////////////////////////////////////////////////////////////////////////////////
class CTroop
{
////////////////////////////////////////////////////////////////////////////////////
// Various Troop Wide Data
////////////////////////////////////////////////////////////////////////////////////
int mTroopHandle;
int mTroopTeam;
bool mTroopReform;
float mFormSpacingFwd;
float mFormSpacingRight;
public:
bool Empty() {return mActors.empty();}
int Team() {return mTroopTeam;}
int Handle() {return mTroopHandle;}
////////////////////////////////////////////////////////////////////////////////////
// Initialize - Clear out all data, all actors, reset all variables
////////////////////////////////////////////////////////////////////////////////////
void Initialize(int TroopHandle=0)
{
mActors.clear();
mTarget = 0;
mState = TS_NONE;
mTroopHandle = TroopHandle;
mTroopTeam = 0;
mTroopReform = false;
}
////////////////////////////////////////////////////////////////////////////////////
// DistanceSq - Quick Operation to see how far an ent is from the rest of the troop
////////////////////////////////////////////////////////////////////////////////////
float DistanceSq(gentity_t* ent)
{
if (mActors.size())
{
return DistanceSquared(ent->currentOrigin, mActors[0]->currentOrigin);
}
return 0.0f;
}
private:
////////////////////////////////////////////////////////////////////////////////////
// The Actors
//
// Actors are all the troopers who belong to the group, their positions in this
// vector affect their positions in the troop, whith the first actor as the leader
////////////////////////////////////////////////////////////////////////////////////
ratl::vector_vs mActors;
////////////////////////////////////////////////////////////////////////////////////
// MakeActorLeader - Move A Given Index To A Leader Position
////////////////////////////////////////////////////////////////////////////////////
void MakeActorLeader(int index)
{
if (index!=0)
{
mActors[0]->client->leader = 0;
mActors.swap(index, 0);
}
mActors[0]->client->leader = mActors[0];
if (mActors[0])
{
if (mActors[0]->client->NPC_class==CLASS_HAZARD_TROOPER)
{
mFormSpacingFwd = 75.0f;
mFormSpacingRight = 50.0f;
}
else
{
mFormSpacingFwd = 75.0f;
mFormSpacingRight = 20.0f;
}
}
}
public:
////////////////////////////////////////////////////////////////////////////////////
// AddActor - Adds a new actor to the troop & automatically promote to leader
////////////////////////////////////////////////////////////////////////////////////
void AddActor(gentity_t* actor)
{
assert(actor->NPC->troop==0 && !mActors.full());
actor->NPC->troop = mTroopHandle;
mActors.push_back(actor);
mTroopReform = true;
if ((mActors.size()==1) || (actor->NPC->rank > mActors[0]->NPC->rank))
{
MakeActorLeader(mActors.size()-1);
}
if (!mTroopTeam)
{
mTroopTeam = actor->client->playerTeam;
}
}
////////////////////////////////////////////////////////////////////////////////////
// RemoveActor - Removes an actor from the troop & automatically promote leader
////////////////////////////////////////////////////////////////////////////////////
void RemoveActor(gentity_t* actor)
{
assert(actor->NPC->troop==mTroopHandle);
int bestNewLeader=-1;
int numEnts = mActors.size();
//bool found = false;
mTroopReform = true;
// Find The Actor
//----------------
for (int i=0; i=0 && (mActors[i]->NPC->rank > mActors[bestNewLeader]->NPC->rank))
{
bestNewLeader = i;
}
}
if (!mActors.empty() && bestNewLeader>=0)
{
MakeActorLeader(bestNewLeader);
}
//assert(found);
actor->NPC->troop = 0;
}
private:
////////////////////////////////////////////////////////////////////////////////////
// Enemy
//
// The troop has a collective enemy that it knows about, which is updated by all
// the members of the group;
////////////////////////////////////////////////////////////////////////////////////
gentity_t* mTarget;
bool mTargetVisable;
int mTargetVisableStartTime;
int mTargetVisableStopTime;
CVec3 mTargetVisablePosition;
int mTargetIndex;
int mTargetLastKnownTime;
CVec3 mTargetLastKnownPosition;
bool mTargetLastKnownPositionVisited;
////////////////////////////////////////////////////////////////////////////////////
// RegisterTarget - Records That the target is seen, when and where
////////////////////////////////////////////////////////////////////////////////////
void RegisterTarget(gentity_t* target, int index, bool visable)
{
if (!mTarget)
{
HT_Speech(mActors[0], SPEECH_DETECTED, 0);
}
else if ((level.time - mTargetLastKnownTime)>8000)
{
HT_Speech(mActors[0], SPEECH_SIGHT, 0);
}
if (visable)
{
mTargetVisableStopTime = level.time;
if (!mTargetVisable)
{
mTargetVisableStartTime = level.time;
}
CalcEntitySpot(target, SPOT_HEAD, mTargetVisablePosition.v);
mTargetVisablePosition[2] -= 10.0f;
}
mTarget = target;
mTargetVisable = visable;
mTargetIndex = index;
mTargetLastKnownTime = level.time;
mTargetLastKnownPosition = target->currentOrigin;
mTargetLastKnownPositionVisited = false;
}
////////////////////////////////////////////////////////////////////////////////////
// RegisterTarget - Records That the target is seen, when and where
////////////////////////////////////////////////////////////////////////////////////
bool TargetLastKnownPositionVisited()
{
if (!mTargetLastKnownPositionVisited)
{
float dist = DistanceSquared(mTargetLastKnownPosition.v, mActors[0]->currentOrigin);
mTargetLastKnownPositionVisited = (dist1.0f)
{
val = 1.0f;
}
if (val<0.0f)
{
val = 0.0f;
}
return val;
}
////////////////////////////////////////////////////////////////////////////////////
// Target Visibility
//
// Compute all factors that can add visibility to a target
////////////////////////////////////////////////////////////////////////////////////
float TargetVisibility(gentity_t* target)
{
float Scale = 0.8f;
if (target->client && target->client->ps.weapon==WP_SABER && target->client->ps.SaberActive())
{
Scale += 0.1f;
}
return ClampScale(Scale);
}
////////////////////////////////////////////////////////////////////////////////////
//
////////////////////////////////////////////////////////////////////////////////////
float TargetNoiseLevel(gentity_t* target)
{
float Scale = 0.1f;
Scale += target->resultspeed / (float)g_speed->integer;
if (target->client && target->client->ps.weapon==WP_SABER && target->client->ps.SaberActive())
{
Scale += 0.2f;
}
return ClampScale(Scale);
}
////////////////////////////////////////////////////////////////////////////////////
// Scan For Enemies
////////////////////////////////////////////////////////////////////////////////////
void ScanForTarget(int scannerIndex)
{
gentity_t* target;
int targetIndex=0;
int targetStop=ENTITYNUM_WORLD;
CVec3 targetPos;
CVec3 targetDirection;
float targetDistance;
float targetVisibility;
float targetNoiseLevel;
gentity_t* scanner = mActors[scannerIndex];
gNPCstats_t* scannerStats = &(scanner->NPC->stats);
float scannerMaxViewDist = scannerStats->visrange;
float scannerMinVisability = 0.1f;//1.0f - scannerStats->vigilance;
float scannerMaxHearDist = scannerStats->earshot;
float scannerMinNoiseLevel = 0.3f;//1.0f - scannerStats->vigilance;
CVec3 scannerPos(scanner->currentOrigin);
CVec3 scannerFwd(scanner->currentAngles);
scannerFwd.AngToVec();
// If Existing Target, Only Check It
//-----------------------------------
if (mTarget)
{
targetIndex = mTargetIndex;
targetStop = mTargetIndex+1;
}
SaveNPCGlobals();
SetNPCGlobals(scanner);
for (; targetIndexcurrentOrigin;
if (target->client && target->client->ps.leanofs)
{
targetPos = target->client->renderInfo.eyePoint;
}
targetDirection = (targetPos - scannerPos);
targetDistance = targetDirection.SafeNorm();
// Can The Scanner SEE The Target?
//---------------------------------
if (targetDistancescannerMinVisability)
{
if (NPC_ClearLOS(targetPos.v))
{
RegisterTarget(target, targetIndex, true);
RestoreNPCGlobals();
return;
}
}
}
// Can The Scanner HEAR The Target?
//----------------------------------
if (targetDistancescannerMinNoiseLevel)
{
RegisterTarget(target, targetIndex, false);
RestoreNPCGlobals();
return;
}
}
}
RestoreNPCGlobals();
}
private:
////////////////////////////////////////////////////////////////////////////////////
// Troop State
//
// The troop as a whole can be acting under a number of different "behavior states"
////////////////////////////////////////////////////////////////////////////////////
enum ETroopState
{
TS_NONE = 0, // No troop wide activity active
TS_ADVANCE, // CHOOSE A NEW ADVANCE TACTIC
TS_ADVANCE_REGROUP, // All ents move into squad position
TS_ADVANCE_SEARCH, // Slow advance, looking left to right, in formation
TS_ADVANCE_COVER, // One at a time moves forward, goes off path, provides cover
TS_ADVANCE_FORMATION, // In formation jog to goal location
TS_ATTACK, // CHOOSE A NEW ATTACK TACTIC
TS_ATTACK_LINE, // Form 2 lines, front kneel, back stand
TS_ATTACK_FLANK, // Same As Line, except scouting group attemts to get around other side of target
TS_ATTACK_SURROUND, // Get on all sides of target
TS_ATTACK_COVER, //
TS_MAX
};
ETroopState mState;
CVec3 mFormHead;
CVec3 mFormFwd;
CVec3 mFormRight;
////////////////////////////////////////////////////////////////////////////////////
// TroopInFormation - A quick check to see if the troop is currently in formation
////////////////////////////////////////////////////////////////////////////////////
bool TroopInFormation()
{
float maxActorRangeSq = ((mActors.size()/2) + 2) * mFormSpacingFwd;
maxActorRangeSq *= maxActorRangeSq;
for (int actorIndex=1; actorIndexmaxActorRangeSq)
{
return false;
}
}
return true;
}
////////////////////////////////////////////////////////////////////////////////////
// SActorOrder
////////////////////////////////////////////////////////////////////////////////////
struct SActorOrder
{
CVec3 mPosition;
int mCombatPoint;
bool mKneelAndShoot;
};
ratl::array_vs mOrders;
////////////////////////////////////////////////////////////////////////////////////
// LeaderIssueAndUpdateOrders - Tell Everyone Where To Go
////////////////////////////////////////////////////////////////////////////////////
void LeaderIssueAndUpdateOrders(ETroopState NextState)
{
int actorIndex;
int actorCount = mActors.size();
// Always Put Guys Closest To The Order Locations In Those Locations
//-------------------------------------------------------------------
for (int orderIndex=1; orderIndexcurrentOrigin);
float currentDistance = closestActorDistance;
for (actorIndex=orderIndex+1; actorIndexcurrentOrigin);
if (currentDistancepos1);
}
// PHASE I - VOICE COMMANDS & ANIMATIONS
//=======================================
gentity_t* leader = mActors[0];
if (NextState!=mState)
{
if (mActors.size()>0)
{
switch (NextState)
{
case (TS_ADVANCE_REGROUP) :
{
break;
}
case (TS_ADVANCE_SEARCH) :
{
HT_Speech(leader, SPEECH_LOOK, 0);
break;
}
case (TS_ADVANCE_COVER) :
{
HT_Speech(leader, SPEECH_COVER, 0);
NPC_SetAnim(leader, SETANIM_TORSO, TORSO_HANDSIGNAL4, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLDLESS);
break;
}
case (TS_ADVANCE_FORMATION) :
{
HT_Speech(leader, SPEECH_ESCAPING, 0);
break;
}
case (TS_ATTACK_LINE) :
{
HT_Speech(leader, SPEECH_CHASE, 0);
NPC_SetAnim(leader, SETANIM_TORSO, TORSO_HANDSIGNAL1, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLDLESS);
break;
}
case (TS_ATTACK_FLANK) :
{
HT_Speech(leader, SPEECH_OUTFLANK, 0);
NPC_SetAnim(leader, SETANIM_TORSO, TORSO_HANDSIGNAL3, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLDLESS);
break;
}
case (TS_ATTACK_SURROUND) :
{
HT_Speech(leader, SPEECH_GIVEUP, 0);
NPC_SetAnim(leader, SETANIM_TORSO, TORSO_HANDSIGNAL2, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLDLESS);
break;
}
case (TS_ATTACK_COVER) :
{
HT_Speech(leader, SPEECH_COVER, 0);
break;
}
default:
{
}
}
}
}
// If Attacking, And Not Forced To Reform, Don't Recalculate Orders
//------------------------------------------------------------------
else if (NextState>TS_ATTACK && !mTroopReform)
{
return;
}
// PHASE II - COMPUTE THE NEW FORMATION HEAD, FORWARD, AND RIGHT VECTORS
//=======================================================================
mFormHead = leader->currentOrigin;
mFormFwd = (NAV::HasPath(leader))?(NAV::NextPosition(leader)):(mTargetLastKnownPosition);
mFormFwd -= mFormHead;
mFormFwd[2] = 0;
mFormFwd *= -1.0f; // Form Forward Goes Behind The Leader
mFormFwd.Norm();
mFormRight = mFormFwd;
mFormRight.Cross(CVec3::mZ);
// Scale Vectors By Spacing Distances
//------------------------------------
mFormFwd *= mFormSpacingFwd;
mFormRight *= mFormSpacingRight;
// If Attacking, Move Head Forward Some To Center On Target
//----------------------------------------------------------
if (NextState>TS_ATTACK)
{
if (!mTroopReform)
{
int FwdNum = ((actorCount/2)+1);
for (int i=0; icurrentOrigin,
mActors[0]->mins,
mActors[0]->maxs,
mOrders[0].mPosition.v,
mActors[0]->s.number,
mActors[0]->clipmask,
(EG2_Collision)0,
0
);
if (trace.fraction<1.0f)
{
mOrders[0].mPosition = trace.endpos;
}
}
else
{
mOrders[0].mPosition = mTargetLastKnownPosition;
}
VectorCopy(mOrders[0].mPosition.v, mActors[0]->pos1);
CVec3 FormTgtToHead(mFormHead);
FormTgtToHead -= mTargetLastKnownPosition;
/*float FormTgtToHeadDist = */FormTgtToHead.SafeNorm();
CVec3 BaseAngleToHead(FormTgtToHead);
BaseAngleToHead.VecToAng();
// int NumPerSide = mActors.size()/2;
// float WidestAngle = FORMATION_SURROUND_FAN * (NumPerSide+1);
// PHASE III - USE FORMATION VECTORS TO COMPUTE ORDERS FOR ALL ACTORS
//====================================================================
for (actorIndex=1; actorIndexNPC->combatPoint!=-1)
{
NPC_FreeCombatPoint(mActors[actorIndex]->NPC->combatPoint, qfalse);
mActors[actorIndex]->NPC->combatPoint = -1;
}
Order.mPosition = mFormHead;
Order.mCombatPoint = -1;
Order.mKneelAndShoot= false;
// Advance Orders
//----------------
if (NextState=4)
{
int cpFlags = (CP_HAS_ROUTE|CP_AVOID_ENEMY|CP_CLEAR|CP_COVER|CP_FLANK|CP_APPROACH_ENEMY);
float avoidDist = 128.0f;
Order.mCombatPoint = NPC_FindCombatPointRetry(
mActors[actorIndex]->currentOrigin,
mActors[actorIndex]->currentOrigin,
mActors[actorIndex]->currentOrigin,
&cpFlags,
avoidDist,
0);
if (Order.mCombatPoint!=-1 && (cpFlags&CP_CLEAR))
{
Order.mPosition = level.combatPoints[Order.mCombatPoint].origin;
NPC_SetCombatPoint(Order.mCombatPoint);
}
else
{
Order.mPosition.ScaleAdd(mFormFwd, FwdScale);
Order.mPosition.ScaleAdd(mFormRight, SideScale);
}
}
else if (NextState==TS_ATTACK_SURROUND)
{
Order.mPosition.ScaleAdd(mFormFwd, FwdScale);
Order.mPosition.ScaleAdd(mFormRight, SideScale);
/* CVec3 FanAngles = BaseAngleToHead;
FanAngles[YAW] += (SideScale * (WidestAngle-(FwdScale*FORMATION_SURROUND_FAN)));
FanAngles.AngToVec();
Order.mPosition = mTargetLastKnownPosition;
Order.mPosition.ScaleAdd(FanAngles, FormTgtToHeadDist);
*/
}
else if (NextState==TS_ATTACK_COVER)
{
Order.mPosition.ScaleAdd(mFormFwd, FwdScale);
Order.mPosition.ScaleAdd(mFormRight, SideScale);
}
}
if (NextState>=TS_ATTACK)
{
trace_t trace;
CVec3 OrderUp(Order.mPosition);
OrderUp[2] += 10.0f;
gi.trace(&trace,
Order.mPosition.v,
mActors[actorIndex]->mins,
mActors[actorIndex]->maxs,
OrderUp.v,
mActors[actorIndex]->s.number,
CONTENTS_SOLID|CONTENTS_TERRAIN|CONTENTS_MONSTERCLIP|CONTENTS_BOTCLIP,
(EG2_Collision)0,
0);
if (trace.startsolid || trace.allsolid)
{
int cpFlags = (CP_HAS_ROUTE|CP_AVOID_ENEMY|CP_CLEAR|CP_COVER|CP_FLANK|CP_APPROACH_ENEMY);
float avoidDist = 128.0f;
Order.mCombatPoint = NPC_FindCombatPointRetry(
mActors[actorIndex]->currentOrigin,
mActors[actorIndex]->currentOrigin,
mActors[actorIndex]->currentOrigin,
&cpFlags,
avoidDist,
0);
if (Order.mCombatPoint!=-1)
{
Order.mPosition = level.combatPoints[Order.mCombatPoint].origin;
NPC_SetCombatPoint(Order.mCombatPoint);
}
else
{
Order.mPosition = mOrders[0].mPosition;
}
}
}
RestoreNPCGlobals();
}
mTroopReform = false;
mState = NextState;
}
////////////////////////////////////////////////////////////////////////////////////
// SufficientCoverNearby - Look at nearby combat points, see if there is enough
////////////////////////////////////////////////////////////////////////////////////
bool SufficientCoverNearby()
{
// TODO: Evaluate Available Combat Points
return false;
}
public:
////////////////////////////////////////////////////////////////////////////////////
// Update - This is the primary "think" function from the troop
////////////////////////////////////////////////////////////////////////////////////
void Update()
{
if (mActors.empty())
{
return;
}
ScanForTarget(0 /*Q_irand(0, (mActors.size()-1))*/);
if (mTarget)
{
ETroopState NextState = mState;
int TimeSinceLastSeen = (level.time - mTargetVisableStopTime);
// int TimeVisable = (mTargetVisableStopTime - mTargetVisableStartTime);
bool Attack = (TimeSinceLastSeen<2000);
if (Attack)
{
// If Not Currently Attacking, Or We Want To Pick A New Attack Tactic
//--------------------------------------------------------------------
if (mState4)?(TS_ATTACK_FLANK):(TS_ATTACK_LINE);
}
else
{
NextState = (SufficientCoverNearby())?(TS_ATTACK_COVER):(TS_ATTACK_SURROUND);
}
}
}
else
{
if (!TroopInFormation())
{
NextState = TS_ADVANCE_REGROUP;
}
else
{
if (TargetLastKnownPositionVisited())
{
NextState = TS_ADVANCE_SEARCH;
}
else
{
NextState = (TimeSinceLastSeen<10000)?(TS_ADVANCE_COVER):(TS_ADVANCE_FORMATION);
}
}
}
LeaderIssueAndUpdateOrders(NextState);
}
}
////////////////////////////////////////////////////////////////////////////////////
// MergeInto - Merges all actors into anther troop
////////////////////////////////////////////////////////////////////////////////////
void MergeInto(CTroop& Other)
{
int numEnts = mActors.size();
for (int i=0; iclient->leader = 0;
mActors[i]->NPC->troop = 0;
Other.AddActor(mActors[i]);
}
mActors.clear();
if (!Other.mTarget && mTarget)
{
Other.mTarget = mTarget;
Other.mTargetIndex = mTargetIndex;
Other.mTargetLastKnownPosition = mTargetLastKnownPosition;
Other.mTargetLastKnownPositionVisited = mTargetLastKnownPositionVisited;
Other.mTargetLastKnownTime = mTargetLastKnownTime;
Other.mTargetVisableStartTime = mTargetVisableStartTime;
Other.mTargetVisableStopTime = mTargetVisableStopTime;
Other.mTargetVisable = mTargetVisable;
Other.mTargetVisablePosition = mTargetVisablePosition;
Other.LeaderIssueAndUpdateOrders(mState);
}
}
////////////////////////////////////////////////////////////////////////////////////
//
////////////////////////////////////////////////////////////////////////////////////
gentity_t* TrackingTarget()
{
return mTarget;
}
////////////////////////////////////////////////////////////////////////////////////
//
////////////////////////////////////////////////////////////////////////////////////
gentity_t* TroopLeader()
{
return mActors[0];
}
////////////////////////////////////////////////////////////////////////////////////
//
////////////////////////////////////////////////////////////////////////////////////
int TimeSinceSeenTarget()
{
return (level.time - mTargetVisableStopTime);
}
////////////////////////////////////////////////////////////////////////////////////
//
////////////////////////////////////////////////////////////////////////////////////
CVec3& TargetVisablePosition()
{
return mTargetVisablePosition;
}
////////////////////////////////////////////////////////////////////////////////////
//
////////////////////////////////////////////////////////////////////////////////////
float FormSpacingFwd()
{
return mFormSpacingFwd;
}
////////////////////////////////////////////////////////////////////////////////////
//
////////////////////////////////////////////////////////////////////////////////////
gentity_t* TooCloseToTroopMember(gentity_t* actor)
{
for (int i=0; iresultspeed<10.0f)
// {
// continue;
// }
if (i==0)
{
if (Distance(actor->currentOrigin, mActors[i]->currentOrigin)<(mFormSpacingFwd*0.5f))
{
return mActors[i];
}
}
else
{
if (Distance(actor->currentOrigin, mActors[i]->currentOrigin)<(mFormSpacingFwd*0.5f))
{
return mActors[i];
}
}
}
assert("Somehow this actor is not actually in the troop..."==0);
return 0;
}
};
typedef ratl::handle_pool_vs TTroopPool;
TTroopPool mTroops;
////////////////////////////////////////////////////////////////////////////////////////
// Erase All Data, Set To Default Vals Before Entities Spawn
////////////////////////////////////////////////////////////////////////////////////////
void Troop_Reset()
{
mTroops.clear();
}
////////////////////////////////////////////////////////////////////////////////////////
// Entities Have Just Spawned, Initialize
////////////////////////////////////////////////////////////////////////////////////////
void Troop_Initialize()
{
}
////////////////////////////////////////////////////////////////////////////////////////
// Global Update Of All Troops
////////////////////////////////////////////////////////////////////////////////////////
void Troop_Update()
{
for (TTroopPool::iterator i=mTroops.begin(); i!=mTroops.end(); ++i)
{
i->Update();
}
}
////////////////////////////////////////////////////////////////////////////////////////
// Erase All Data, Set To Default Vals Before Entities Spawn
////////////////////////////////////////////////////////////////////////////////////////
void Trooper_UpdateTroop(gentity_t* actor)
{
// Try To Join A Troop
//---------------------
if (!actor->NPC->troop)
{
float curDist = 0;
float closestDist = 0;
TTroopPool::iterator closestTroop = mTroops.end();
trace_t trace;
for (TTroopPool::iterator iTroop=mTroops.begin(); iTroop!=mTroops.end(); ++iTroop)
{
if (iTroop->Team()==actor->client->playerTeam)
{
curDist = iTroop->DistanceSq(actor);
if (curDistcurrentOrigin,
actor->mins,
actor->maxs,
iTroop->TroopLeader()->currentOrigin,
actor->s.number,
CONTENTS_SOLID|CONTENTS_TERRAIN|CONTENTS_MONSTERCLIP|CONTENTS_BOTCLIP,
(EG2_Collision)0,
0);
if (!trace.allsolid &&
!trace.startsolid &&
(trace.fraction>=1.0f || trace.entityNum==iTroop->TroopLeader()->s.number))
{
closestDist = curDist;
closestTroop = iTroop;
}
}
}
}
// If Found, Add The Actor To It
//--------------------------------
if (closestTroop!=mTroops.end())
{
closestTroop->AddActor(actor);
}
// If We Couldn't Find One, Create A New Troop
//---------------------------------------------
else if (!mTroops.full())
{
int nTroopHandle = mTroops.alloc();
mTroops[nTroopHandle].Initialize(nTroopHandle);
mTroops[nTroopHandle].AddActor(actor);
}
}
// If This Is A Leader, Then He Is Responsible For Merging Troops
//----------------------------------------------------------------
else if (actor->client->leader==actor)
{
float curDist = 0;
float closestDist = 0;
TTroopPool::iterator closestTroop = mTroops.end();
for (TTroopPool::iterator iTroop=mTroops.begin(); iTroop!=mTroops.end(); ++iTroop)
{
curDist = iTroop->DistanceSq(actor);
if ((curDistNPC->troop))
{
closestDist = curDist;
closestTroop = iTroop;
}
}
if (closestTroop!=mTroops.end())
{
int oldTroopNum = actor->NPC->troop;
mTroops[oldTroopNum].MergeInto(*closestTroop);
mTroops.free(oldTroopNum);
}
}
}
////////////////////////////////////////////////////////////////////////////////////////
//
////////////////////////////////////////////////////////////////////////////////////////
bool Trooper_UpdateSmackAway(gentity_t* actor, gentity_t* target)
{
if (actor->client->ps.legsAnim==BOTH_MELEE1)
{
if (TIMER_Done(actor, "Trooper_SmackAway"))
{
CVec3 ActorPos(actor->currentOrigin);
CVec3 ActorToTgt(target->currentOrigin);
ActorToTgt -= ActorPos;
float ActorToTgtDist = ActorToTgt.SafeNorm();
if (ActorToTgtDist<100.0f)
{
G_Throw(target, ActorToTgt.v, 200.0f);
}
}
return true;
}
return false;
}
////////////////////////////////////////////////////////////////////////////////////////
//
////////////////////////////////////////////////////////////////////////////////////////
void Trooper_SmackAway(gentity_t* actor, gentity_t* target)
{
assert(actor && actor->NPC);
if (actor->client->ps.legsAnim!=BOTH_MELEE1)
{
NPC_SetAnim(actor, SETANIM_BOTH, BOTH_MELEE1, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD);
TIMER_Set(actor, "Trooper_SmackAway", actor->client->ps.torsoAnimTimer/4.0f);
}
}
////////////////////////////////////////////////////////////////////////////////////////
//
////////////////////////////////////////////////////////////////////////////////////////
bool Trooper_Kneeling(gentity_t* actor)
{
return (actor->NPC->aiFlags&NPCAI_KNEEL || actor->client->ps.legsAnim==BOTH_STAND_TO_KNEEL);
}
////////////////////////////////////////////////////////////////////////////////////////
//
////////////////////////////////////////////////////////////////////////////////////////
void Trooper_KneelDown(gentity_t* actor)
{
assert(actor && actor->NPC);
if (!Trooper_Kneeling(actor) && level.time>actor->NPC->kneelTime)
{
NPC_SetAnim(actor, SETANIM_BOTH, BOTH_STAND_TO_KNEEL, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD);
actor->NPC->aiFlags |= NPCAI_KNEEL;
actor->NPC->kneelTime = level.time + Q_irand(3000, 6000);
}
}
////////////////////////////////////////////////////////////////////////////////////////
//
////////////////////////////////////////////////////////////////////////////////////////
void Trooper_StandUp(gentity_t* actor, bool always=false)
{
assert(actor && actor->NPC);
if (Trooper_Kneeling(actor) && (always || level.time>actor->NPC->kneelTime))
{
actor->NPC->aiFlags &= ~NPCAI_KNEEL;
NPC_SetAnim(actor, SETANIM_BOTH, BOTH_KNEEL_TO_STAND, SETANIM_FLAG_OVERRIDE|SETANIM_FLAG_HOLD);
actor->NPC->kneelTime = level.time + Q_irand(3000, 6000);
}
}
////////////////////////////////////////////////////////////////////////////////////////
//
////////////////////////////////////////////////////////////////////////////////////////
int Trooper_CanHitTarget(gentity_t* actor, gentity_t* target, CTroop& troop, float& MuzzleToTargetDistance, CVec3& MuzzleToTarget)
{
trace_t tr;
CVec3 MuzzlePoint(actor->currentOrigin);
CalcEntitySpot(actor, SPOT_WEAPON, MuzzlePoint.v);
MuzzleToTarget = troop.TargetVisablePosition();
MuzzleToTarget -= MuzzlePoint;
MuzzleToTargetDistance = MuzzleToTarget.SafeNorm();
CVec3 MuzzleDirection(actor->currentAngles);
MuzzleDirection.AngToVec();
// Aiming In The Right Direction?
//--------------------------------
if (MuzzleDirection.Dot(MuzzleToTarget)>0.95)
{
// Clear Line Of Sight To Target?
//--------------------------------
gi.trace(&tr, MuzzlePoint.v, NULL, NULL, troop.TargetVisablePosition().v, actor->s.number, MASK_SHOT, (EG2_Collision)0, 0);
if (tr.startsolid || tr.allsolid)
{
return ENTITYNUM_NONE;
}
if (tr.entityNum==target->s.number || tr.fraction>0.9f)
{
return target->s.number;
}
return tr.entityNum;
}
return ENTITYNUM_NONE;
}
////////////////////////////////////////////////////////////////////////////////////////
// Run The Per Trooper Update
////////////////////////////////////////////////////////////////////////////////////////
void Trooper_Think(gentity_t* actor)
{
gentity_t* target = (actor->NPC->troop)?(mTroops[actor->NPC->troop].TrackingTarget()):(0);
if (target)
{
G_SetEnemy(actor, target);
CTroop& troop = mTroops[actor->NPC->troop];
bool AtPos = STEER::Reached(actor, actor->pos1, 10.0f);
int traceTgt = ENTITYNUM_NONE;
bool traced = false;
bool inSmackAway = false;
float MuzzleToTargetDistance = 0.0f;
CVec3 MuzzleToTarget;
if (actor->NPC->combatPoint!=-1)
{
traceTgt = Trooper_CanHitTarget(actor, target, troop, MuzzleToTargetDistance, MuzzleToTarget);
traced = true;
if (traceTgt==target->s.number)
{
AtPos = true;
}
}
// Smack!
//-------
if (Trooper_UpdateSmackAway(actor, target))
{
traced = true;
AtPos = true;
inSmackAway = true;
}
if (false)
{
CG_DrawEdge(actor->currentOrigin, actor->pos1, EDGE_IMPACT_SAFE);
}
// If There, Stop Moving
//-----------------------
STEER::Activate(actor);
{
gentity_t* fleeFrom = troop.TooCloseToTroopMember(actor);
// If Too Close To The Leader, Get Out Of His Way
//------------------------------------------------
if (fleeFrom)
{
STEER::Flee(actor, fleeFrom->currentOrigin, 1.0f);
AtPos = false;
}
// If In Position, Stop Moving
//-----------------------------
if (AtPos)
{
NAV::ClearPath(actor);
STEER::Stop(actor);
}
// Otherwise, Try To Get To Position
//-----------------------------------
else
{
Trooper_StandUp(actor, true);
// If Close Enough, Persue Our Target Directly
//---------------------------------------------
bool moveSuccess = STEER::GoTo(NPC, actor->pos1, 10.0f, false);
// Otherwise
//-----------
if (!moveSuccess)
{
moveSuccess = NAV::GoTo(NPC, actor->pos1);
}
// If No Way To Get To Position, Stay Here
//-----------------------------------------
if (!moveSuccess || (level.time - actor->lastMoveTime)>4000)
{
AtPos = true;
}
}
}
STEER::DeActivate(actor, &ucmd);
// If There And Target Was Recently Visable
//------------------------------------------
if (AtPos && (troop.TimeSinceSeenTarget()<1500))
{
if (!traced)
{
traceTgt = Trooper_CanHitTarget(actor, target, troop, MuzzleToTargetDistance, MuzzleToTarget);
}
// Shoot!
//--------
if (traceTgt==target->s.number)
{
WeaponThink(qtrue);
}
else if (!inSmackAway)
{
// Otherwise, If Kneeling, Get Up!
//---------------------------------
if (Trooper_Kneeling(actor))
{
Trooper_StandUp(actor);
}
// If The Enemy Is Close Enough, Smack Him Away
//----------------------------------------------
else if (MuzzleToTargetDistance<40.0f)
{
Trooper_SmackAway(actor, target);
}
// If We Would Have It A Friend, Ask Him To Kneel
//------------------------------------------------
else if (traceTgt!=ENTITYNUM_NONE &&
traceTgt!=ENTITYNUM_WORLD &&
g_entities[traceTgt].client &&
g_entities[traceTgt].NPC &&
g_entities[traceTgt].client->playerTeam==actor->client->playerTeam &&
NPC_IsTrooper(&g_entities[traceTgt]) &&
g_entities[traceTgt].resultspeed<1.0f &&
!(g_entities[traceTgt].NPC->aiFlags & NPCAI_KNEEL))
{
Trooper_KneelDown(&g_entities[traceTgt]);
}
}
// Convert To Angles And Set That As Our Desired Look Direction
//--------------------------------------------------------------
if (MuzzleToTargetDistance>100)
{
MuzzleToTarget.VecToAng();
NPCInfo->desiredYaw = MuzzleToTarget[YAW];
NPCInfo->desiredPitch = MuzzleToTarget[PITCH];
}
else
{
MuzzleToTarget = troop.TargetVisablePosition();
MuzzleToTarget.v[2] -= 20.0f; // Aim Lower
MuzzleToTarget -= actor->currentOrigin;
MuzzleToTarget.SafeNorm();
MuzzleToTarget.VecToAng();
NPCInfo->desiredYaw = MuzzleToTarget[YAW];
NPCInfo->desiredPitch = MuzzleToTarget[PITCH];
}
}
NPC_UpdateFiringAngles( qtrue, qtrue );
NPC_UpdateAngles( qtrue, qtrue );
if (Trooper_Kneeling(actor))
{
ucmd.upmove = -127; // Set Crouch Height
}
}
else
{
NPC_BSST_Default();
}
}
////////////////////////////////////////////////////////////////////////////////////////
/*
-------------------------
NPC_BehaviorSet_Trooper
-------------------------
*/
////////////////////////////////////////////////////////////////////////////////////////
void NPC_BehaviorSet_Trooper( int bState )
{
Trooper_UpdateTroop(NPC);
switch( bState )
{
case BS_STAND_GUARD:
case BS_PATROL:
case BS_STAND_AND_SHOOT:
case BS_HUNT_AND_KILL:
case BS_DEFAULT:
Trooper_Think(NPC);
break;
case BS_INVESTIGATE:
NPC_BSST_Investigate();
break;
case BS_SLEEP:
NPC_BSST_Sleep();
break;
default:
Trooper_Think(NPC);
break;
}
}
////////////////////////////////////////////////////////////////////////////////////////
// IsTrooper - return true if you want a given actor to use trooper AI
////////////////////////////////////////////////////////////////////////////////////////
bool NPC_IsTrooper(gentity_t* actor)
{
return (
actor &&
actor->NPC &&
actor->s.weapon &&
!!(actor->NPC->scriptFlags&SCF_NO_GROUPS)// &&
// !(actor->NPC->scriptFlags&SCF_CHASE_ENEMIES)
);
}
void NPC_LeaveTroop(gentity_t* actor)
{
assert(actor->NPC->troop);
int wasInTroop = actor->NPC->troop;
mTroops[actor->NPC->troop].RemoveActor(actor);
if (mTroops[wasInTroop].Empty())
{
mTroops.free(wasInTroop);
}
}