Bots understand trigger_changetarget

Bots can now navigate doors operated with a trigger_changetarget so they understand the sequence in which triggers must be activated to make it work
This commit is contained in:
RGreenlees 2023-11-02 11:42:29 +00:00 committed by pierow
parent b336ec028c
commit e4d82bef2e
8 changed files with 224 additions and 178 deletions

View file

@ -2011,27 +2011,6 @@ void CTriggerGravity::GravityTouch( CBaseEntity *pOther )
// this is a really bad idea.
class CTriggerChangeTarget : public CBaseDelay
{
public:
void KeyValue( KeyValueData *pkvd );
void Spawn( void );
void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value );
int ObjectCaps( void ) { return CBaseDelay::ObjectCaps() & ~FCAP_ACROSS_TRANSITION; }
virtual int Save( CSave &save );
virtual int Restore( CRestore &restore );
static TYPEDESCRIPTION m_SaveData[];
private:
int m_iszNewTarget;
};
LINK_ENTITY_TO_CLASS( trigger_changetarget, CTriggerChangeTarget );
TYPEDESCRIPTION CTriggerChangeTarget::m_SaveData[] =

View file

@ -59,6 +59,26 @@ private:
CMultiManager* Clone(void);
};
// this is a really bad idea.
class CTriggerChangeTarget : public CBaseDelay
{
public:
void KeyValue(KeyValueData* pkvd);
void Spawn(void);
void Use(CBaseEntity* pActivator, CBaseEntity* pCaller, USE_TYPE useType, float value);
int GetNewTargetName() { return m_iszNewTarget; }
int ObjectCaps(void) { return CBaseDelay::ObjectCaps() & ~FCAP_ACROSS_TRANSITION; }
virtual int Save(CSave& save);
virtual int Restore(CRestore& restore);
static TYPEDESCRIPTION m_SaveData[];
private:
int m_iszNewTarget;
};
class CLadder : public CBaseTrigger
{

View file

@ -2333,6 +2333,10 @@ void CheckAndHandleDoorObstruction(AvHAIPlayer* pBot)
{
AITASK_SetWeldTask(pBot, &pBot->BotNavInfo.MovementTask, Trigger->Edict, true);
}
else if (Trigger->TriggerType == DOOR_BREAK)
{
AITASK_SetAttackTask(pBot, &pBot->BotNavInfo.MovementTask, Trigger->Edict, true);
}
return;
}
@ -3227,7 +3231,6 @@ void LadderMove(AvHAIPlayer* pBot, const Vector StartPoint, const Vector EndPoin
pBot->Button |= IN_DUCK;
}
if (pBot->Player->IsOnLadder())
{
// We're on the ladder and actively climbing
@ -3430,6 +3433,11 @@ void LadderMove(AvHAIPlayer* pBot, const Vector StartPoint, const Vector EndPoin
// If we're going down the ladder and are approaching it, just keep moving towards it
if (pBot->BotNavInfo.IsOnGround && !bIsGoingUpLadder)
{
if (vDist2DSq(pEdict->v.origin, StartPoint) < sqrf(32.0f))
{
pBot->BotNavInfo.bShouldWalk = true;
}
Vector ApproachDir = UTIL_GetVectorNormal2D(EndPoint - pBot->Edict->v.origin);
float Dot = UTIL_GetDotProduct2D(ApproachDir, vForward);
@ -5351,6 +5359,14 @@ void SkipAheadInFlightPath(AvHAIPlayer* pBot)
// Early exit if we don't have a path, or we're already on the last path point
if (BotNavInfo->CurrentPath.size() == 0 || BotNavInfo->CurrentPathPoint == prev(BotNavInfo->CurrentPath.end())) { return; }
if (UTIL_QuickHullTrace(pBot->Edict, pBot->Edict->v.origin, prev(BotNavInfo->CurrentPath.end())->Location, head_hull))
{
pBot->BotNavInfo.CurrentPathPoint = prev(BotNavInfo->CurrentPath.end());
return;
}
// If we are currently in a low area or approaching one, don't try to skip ahead in case it screws us up
if (BotNavInfo->CurrentPathPoint->area == SAMPLE_POLYAREA_CROUCH || (next(BotNavInfo->CurrentPathPoint) != BotNavInfo->CurrentPath.end() && next(BotNavInfo->CurrentPathPoint)->area == SAMPLE_POLYAREA_CROUCH)) { return; }
@ -5414,11 +5430,7 @@ void BotFollowFlightPath(AvHAIPlayer* pBot)
ClosestPointToPath = vClosestPointOnLine(MoveFrom, CurrentMoveDest, pEdict->v.origin);
Vector MoveDir = UTIL_GetVectorNormal(CurrentMoveDest - pEdict->v.origin);
Vector ObstacleCheck = pEdict->v.origin + (MoveDir * 32.0f);
pEdict->v.origin = ClosestPointToPath;
Vector MoveDir = UTIL_GetVectorNormal(CurrentMoveDest - MoveFrom);
if (IsBotStuck(pBot, CurrentMoveDest))
{
@ -5429,13 +5441,28 @@ void BotFollowFlightPath(AvHAIPlayer* pBot)
}
}
float Velocity = vSize2DSq(pEdict->v.velocity);
float CurrentSpeed = vSize3D(pEdict->v.velocity);
if (vDist2DSq(pEdict->v.origin, MoveFrom) > sqrf(100.0f) && vDist2DSq(pEdict->v.origin, CurrentMoveDest) > sqrf(100.0f))
{
Vector NewVelocity = MoveDir;
if (vDist3DSq(pEdict->v.origin, ClosestPointToPath) > sqrf(16.0f))
{
NewVelocity = UTIL_GetVectorNormal((ClosestPointToPath + (MoveDir * 100.0f)) - pEdict->v.origin);
}
NewVelocity = NewVelocity * CurrentSpeed;
pEdict->v.velocity = NewVelocity;
}
bool bMustHugGround = (CurrentMoveArea == SAMPLE_POLYAREA_CROUCH || NextMoveArea == SAMPLE_POLYAREA_CROUCH);
if (!bMustHugGround || MoveFrom.z <= CurrentMoveDest.z)
{
if (Velocity < sqrf(500.f))
if (CurrentSpeed < sqrf(500.f))
{
if (!(pEdict->v.oldbuttons & IN_JUMP))
{
@ -5520,7 +5547,8 @@ void BotFollowSwimPath(AvHAIPlayer* pBot)
float WaterDiff = WaterLevel - pEdict->v.origin.z;
if (WaterDiff > 0.0f)
// If we're below the waterline by a significant amount, then swim up to surface before we move on
if (WaterDiff > 5.0f)
{
Vector MoveDir = UTIL_GetVectorNormal2D(BotNavInfo->CurrentPathPoint->Location - pEdict->v.origin);
pBot->desiredMovementDir = MoveDir;
@ -5537,7 +5565,15 @@ void BotFollowSwimPath(AvHAIPlayer* pBot)
return;
}
NewMove(pBot);
// We're at the surface, now tackle the path the usual way
if (pBot->BotNavInfo.NavProfile.bFlyingProfile)
{
BotFollowFlightPath(pBot);
}
else
{
BotFollowPath(pBot);
}
}
@ -6615,8 +6651,9 @@ void UTIL_PopulateTriggersForEntity(edict_t* Entity, vector<DoorTrigger>& Trigge
CBaseButton* ButtonRef = dynamic_cast<CBaseButton*>(EntityRef);
AvHWeldable* WeldableRef = dynamic_cast<AvHWeldable*>(EntityRef);
CBaseTrigger* TriggerRef = dynamic_cast<CBaseTrigger*>(EntityRef);
CBreakable* BreakableRef = dynamic_cast<CBreakable*>(EntityRef);
if (ButtonRef || WeldableRef || TriggerRef)
if (ButtonRef || WeldableRef || TriggerRef || BreakableRef)
{
CBaseToggle* ToggleRef = dynamic_cast<CBaseToggle*>(EntityRef);
@ -6634,6 +6671,10 @@ void UTIL_PopulateTriggersForEntity(edict_t* Entity, vector<DoorTrigger>& Trigge
{
NewTriggerType = DOOR_WELD;
}
else if (BreakableRef)
{
NewTriggerType = DOOR_BREAK;
}
DoorTrigger NewTrigger;
NewTrigger.Entity = EntityRef;
@ -6680,11 +6721,11 @@ void UTIL_PopulateTriggersForEntity(edict_t* Entity, vector<DoorTrigger>& Trigge
if (FStrEq(STRING(EnvGlobalRef->m_globalstate), GlobalName))
{
UTIL_PopulateTriggersForEntity(EnvGlobalRef->edict(), TriggerList);
}
}
}
}
return;
return;
}
const char* EntityName = STRING(Entity->v.targetname);
@ -6703,6 +6744,47 @@ void UTIL_PopulateTriggersForEntity(edict_t* Entity, vector<DoorTrigger>& Trigge
}
END_FOR_ALL_ENTITIES(kwsWeldableClassName)
FOR_ALL_ENTITIES("trigger_changetarget", CTriggerChangeTarget*)
const char* TargetName = STRING(theEntity->pev->targetname);
const char* NewTargetName = STRING(theEntity->GetNewTargetName());
if (FStrEq(STRING(theEntity->GetNewTargetName()), EntityName))
{
currTrigger = NULL;
while ((currTrigger = UTIL_FindEntityByString(currTrigger, "target", STRING(theEntity->pev->targetname))) != NULL)
{
UTIL_PopulateTriggersForEntity(currTrigger->edict(), TriggerList);
}
currTrigger = NULL;
while ((currTrigger = UTIL_FindEntityByString(currTrigger, "targetname", STRING(theEntity->pev->target))) != NULL)
{
UTIL_PopulateTriggersForEntity(currTrigger->edict(), TriggerList);
}
string NewString = TargetName;
CBaseEntity* CurrWeldable = NULL;
while ((CurrWeldable = UTIL_FindEntityByClassname(CurrWeldable, kwsWeldableClassName)) != NULL)
{
AvHWeldable* ThisWeldableRef = dynamic_cast<AvHWeldable*>(CurrWeldable);
if (ThisWeldableRef)
{
if (ThisWeldableRef->GetTargetOnFinish() == NewString)
{
UTIL_PopulateTriggersForEntity(ThisWeldableRef->edict(), TriggerList);
}
}
}
}
END_FOR_ALL_ENTITIES("trigger_changetarget")
while (((currTrigger = UTIL_FindEntityByClassname(currTrigger, "multi_manager")) != NULL))
{
@ -6746,130 +6828,6 @@ void UTIL_PopulateTriggersForEntity(edict_t* Entity, vector<DoorTrigger>& Trigge
}
void UTIL_LinkTriggerToDoor(const edict_t* DoorEdict, nav_door* DoorRef)
{
CBaseEntity* currTrigger = NULL;
const char* DoorTargetName = STRING(DoorEdict->v.targetname);
while ((currTrigger = UTIL_FindEntityByString(currTrigger, "target", DoorTargetName)) != NULL)
{
CBaseToggle* ToggleRef = dynamic_cast<CBaseToggle*>(currTrigger);
CBaseTrigger* TriggerRef = dynamic_cast<CBaseTrigger*>(currTrigger);
CBaseButton* ButtonRef = dynamic_cast<CBaseButton*>(currTrigger);
DoorActivationType NewTriggerType = DOOR_NONE;
if (TriggerRef)
{
NewTriggerType = DOOR_TRIGGER;
}
else if (ButtonRef)
{
NewTriggerType = DOOR_BUTTON;
}
DoorTrigger NewTrigger;
NewTrigger.Entity = currTrigger;
NewTrigger.Edict = currTrigger->edict();
NewTrigger.ToggleEnt = ToggleRef;
NewTrigger.TriggerType = NewTriggerType;
NewTrigger.bIsActivated = !currTrigger->IsLockedByMaster();
DoorRef->TriggerEnts.push_back(NewTrigger);
}
const string DoorName = DoorTargetName;
currTrigger = NULL;
FOR_ALL_ENTITIES(kwsWeldableClassName, AvHWeldable*)
if (theEntity->GetTargetOnFinish() == DoorName)
{
DoorTrigger NewTrigger;
NewTrigger.Entity = theEntity;
NewTrigger.Edict = theEntity->edict();
NewTrigger.TriggerType = DOOR_WELD;
NewTrigger.bIsActivated = !theEntity->IsLockedByMaster();
DoorRef->TriggerEnts.push_back(NewTrigger);
}
END_FOR_ALL_ENTITIES(kwsWeldableClassName)
// If a door is activated via a multi_manager entity, then we need to find whatever trigger/button targets that multi_manager and tie it to the door
while (((currTrigger = UTIL_FindEntityByClassname(currTrigger, "multi_manager")) != NULL))
{
CMultiManager* MMRef = dynamic_cast<CMultiManager*>(currTrigger);
bool bTargetsDoor = false;
if (MMRef)
{
for (int i = 0; i < MMRef->m_cTargets; i++)
{
if (FStrEq(STRING(DoorEdict->v.targetname), STRING(MMRef->m_iTargetName[i])))
{
bTargetsDoor = true;
break;
}
}
}
if (bTargetsDoor)
{
CBaseEntity* MMTrigger = NULL;
const char* MMNameChar = STRING(MMRef->pev->targetname);
while ((MMTrigger = UTIL_FindEntityByString(MMTrigger, "target", MMNameChar)) != NULL)
{
DoorActivationType NewTriggerType = DOOR_NONE;
CBaseToggle* ToggleRef = dynamic_cast<CBaseToggle*>(MMTrigger);
CBaseTrigger* TriggerRef = dynamic_cast<CBaseTrigger*>(MMTrigger);
CBaseButton* ButtonRef = dynamic_cast<CBaseButton*>(MMTrigger);
if (TriggerRef)
{
NewTriggerType = DOOR_TRIGGER;
}
else if (ButtonRef)
{
NewTriggerType = DOOR_BUTTON;
}
DoorTrigger NewTrigger;
NewTrigger.Entity = currTrigger;
NewTrigger.Edict = currTrigger->edict();
NewTrigger.ToggleEnt = ToggleRef;
NewTrigger.TriggerType = NewTriggerType;
NewTrigger.bIsActivated = currTrigger->IsLockedByMaster();
DoorRef->TriggerEnts.push_back(NewTrigger);
}
const string MMName = MMNameChar;
FOR_ALL_ENTITIES(kwsWeldableClassName, AvHWeldable*)
if (theEntity->GetTargetOnFinish() == MMName)
{
DoorTrigger NewTrigger;
NewTrigger.Entity = theEntity;
NewTrigger.Edict = theEntity->edict();
NewTrigger.TriggerType = DOOR_WELD;
NewTrigger.bIsActivated = !theEntity->IsLockedByMaster();
DoorRef->TriggerEnts.push_back(NewTrigger);
}
END_FOR_ALL_ENTITIES(kwsWeldableClassName)
}
}
}
void UTIL_PopulateWeldableObstacles()
{
UTIL_ClearWeldablesData();
@ -7120,8 +7078,33 @@ void UTIL_UpdateDoorTriggers(nav_door* Door)
continue;
}
}
it->bIsActivated = (it->ToggleEnt) ? !it->ToggleEnt->IsLockedByMaster() : true;
if (FStrEq(STRING(it->Edict->v.target), STRING(Door->DoorEdict->v.targetname)))
{
it->bIsActivated = (it->ToggleEnt) ? !it->ToggleEnt->IsLockedByMaster() : true;
}
else
{
// Weldables and breakables can't be "deactivated" so assume they are always actived
if (it->TriggerType == DOOR_WELD || it->TriggerType == DOOR_BREAK)
{
it->bIsActivated = true;
}
else
{
CBaseEntity* ActivationTarget = UTIL_FindEntityByString(NULL, "targetname", STRING(it->Edict->v.target));
if (!ActivationTarget)
{
it->bIsActivated = true;
}
else
{
const char* classname = STRING(ActivationTarget->pev->classname);
it->bIsActivated = (FStrEq(classname, "multi_manager") || FStrEq(classname, "trigger_changetarget") || FStrEq(classname, "multisource"));
}
}
}
if (it->bIsActivated)
{

View file

@ -76,7 +76,8 @@ enum DoorActivationType
DOOR_TRIGGER,// Door activated by touching a trigger_once or trigger_multiple
DOOR_BUTTON, // Door activated by pressing a button
DOOR_WELD, // Door activated by welding something
DOOR_SHOOT // Door activated by being shot
DOOR_SHOOT, // Door activated by being shot
DOOR_BREAK // Door activated by breaking something
};
typedef struct _DOOR_TRIGGER
@ -173,9 +174,6 @@ void LerkUpdateBotMoveProfile(AvHAIPlayer* pBot, BotMoveStyle MoveStyle);
void FadeUpdateBotMoveProfile(AvHAIPlayer* pBot, BotMoveStyle MoveStyle);
void OnosUpdateBotMoveProfile(AvHAIPlayer* pBot, BotMoveStyle MoveStyle);
// FUTURE FEATURE: Will eventually link a door to the trigger than opens it
void UTIL_LinkTriggerToDoor(const edict_t* DoorEdict, nav_door* DoorRef);
// Finds any random point on the navmesh that is relevant for the bot. Returns ZERO_VECTOR if none found
Vector UTIL_GetRandomPointOnNavmesh(const AvHAIPlayer* pBot);

View file

@ -513,7 +513,7 @@ void BotAttackTarget(AvHAIPlayer* pBot, edict_t* Target)
return;
}
Vector AttackPoint = Target->v.origin;
Vector AttackPoint = (IsEdictPlayer(Target) || IsEdictStructure(Target)) ? Target->v.origin : UTIL_GetButtonFloorLocation(pBot->Edict->v.origin, Target);
if (StructureType == STRUCTURE_ALIEN_HIVE)
{
@ -540,6 +540,13 @@ void BotAttackTarget(AvHAIPlayer* pBot, edict_t* Target)
if (AttackResult == ATTACK_BLOCKED)
{
if (!(IsEdictPlayer(Target) && !IsEdictStructure(Target)))
{
Vector AttackPoint = UTIL_GetButtonFloorLocation(pBot->Edict->v.origin, Target);
MoveTo(pBot, AttackPoint, MOVESTYLE_NORMAL, WeaponRange);
return;
}
if (vIsZero(pBot->BotNavInfo.ActualMoveDestination) || UTIL_TraceEntity(pBot->Edict, pBot->BotNavInfo.ActualMoveDestination + Vector(0.0f, 0.0f, 32.0f), UTIL_GetCentreOfEntity(Target)) != Target)
{
Vector NewAttackLocation = ZERO_VECTOR;
@ -1495,7 +1502,7 @@ void DroneThink(AvHAIPlayer* pBot)
BotProgressTask(pBot, &pBot->PrimaryBotTask);
}
AIDEBUG_DrawBotPath(pBot);
//AIDEBUG_DrawBotPath(pBot);
}
void TestNavThink(AvHAIPlayer* pBot)

View file

@ -641,17 +641,29 @@ void AIMGR_ResetRound()
void AIMGR_ClearBotData()
{
// We shouldn't have any bots in the server when this is called, but this ensures no bots end up "orphans" and no longer tracked by the system
for (auto it = ActiveAIPlayers.begin(); it != ActiveAIPlayers.end();)
// We have to be careful here, depending on how the nav data is being unloaded, there could be stale references in the ActiveAIPlayers list.
for (int i = 1; i <= gpGlobals->maxClients; i++)
{
if (!FNullEnt(it->Edict) && it->Player)
{
it->Player->Kick();
}
edict_t* PlayerEdict = INDEXENT(i);
it = ActiveAIPlayers.erase(it);
if (!FNullEnt(PlayerEdict) && !PlayerEdict->free && (PlayerEdict->v.flags & FL_FAKECLIENT))
{
for (auto it = ActiveAIPlayers.begin(); it != ActiveAIPlayers.end();)
{
if (it->Edict == PlayerEdict && it->Player)
{
it->Player->Kick();
it = ActiveAIPlayers.erase(it);
}
else
{
it++;
}
}
}
}
// We shouldn't have any bots in the server when this is called, but this ensures no bots end up "orphans" and no longer tracked by the system
ActiveAIPlayers.clear();
}

View file

@ -362,6 +362,8 @@ bool AITASK_IsMoveTaskStillValid(AvHAIPlayer* pBot, AvHAIPlayerTask* Task)
{
if (!Task->TaskLocation) { return false; }
if (pBot->BotNavInfo.NavProfile.bFlyingProfile && vEquals(pBot->Edict->v.origin, Task->TaskLocation, 50.0f)) { return false; }
return (vDist2DSq(pBot->Edict->v.origin, Task->TaskLocation) > sqrf(max_player_use_reach) || !UTIL_PointIsDirectlyReachable(pBot->CurrentFloorPosition, Task->TaskLocation));
}
@ -454,18 +456,18 @@ bool AITASK_IsWeaponPickupTaskStillValid(AvHAIPlayer* pBot, AvHAIPlayerTask* Tas
bool AITASK_IsAttackTaskStillValid(AvHAIPlayer* pBot, AvHAIPlayerTask* Task)
{
if (FNullEnt(Task->TaskTarget) || vIsZero(Task->TaskTarget->v.origin)) { return false; }
if (FNullEnt(Task->TaskTarget) || (vIsZero(Task->TaskTarget->v.origin) && vIsZero(Task->TaskLocation))) { return false; }
if ((Task->TaskTarget->v.effects & EF_NODRAW) || (Task->TaskTarget->v.deadflag != DEAD_NO)) { return false; }
if (!UTIL_IsBuildableStructureStillReachable(pBot, Task->TaskTarget)) { return false; }
if (IsEdictStructure(Task->TaskTarget) && !UTIL_IsBuildableStructureStillReachable(pBot, Task->TaskTarget)) { return false; }
if (IsPlayerSkulk(pBot->Edict))
{
if (UTIL_IsStructureElectrified(Task->TaskTarget)) { return false; }
}
if (IsPlayerGorge(pBot->Edict) && !PlayerHasWeapon(pBot->Player, WEAPON_GORGE_BILEBOMB)) { return false; }
if (IsPlayerGorge(pBot->Edict) && (Task->TaskTarget->v.health > 100 && !PlayerHasWeapon(pBot->Player, WEAPON_GORGE_BILEBOMB))) { return false; }
AvHAIDeployableStructureType StructureType = GetStructureTypeFromEdict(Task->TaskTarget);
@ -1567,8 +1569,53 @@ void BotProgressAttackTask(AvHAIPlayer* pBot, AvHAIPlayerTask* Task)
return;
}
AvHAIWeapon Weapon = WEAPON_INVALID;
BotAttackTarget(pBot, Task->TaskTarget);
if (IsPlayerMarine(pBot->Edict))
{
Weapon = BotMarineChooseBestWeaponForStructure(pBot, Task->TaskTarget);
}
else
{
Weapon = BotAlienChooseBestWeaponForStructure(pBot, Task->TaskTarget);
}
BotAttackResult AttackResult = PerformAttackLOSCheck(pBot, Weapon, Task->TaskTarget);
if (AttackResult == ATTACK_SUCCESS)
{
// If we were ducking before then keep ducking
if (pBot->Edict->v.oldbuttons & IN_DUCK)
{
pBot->Button |= IN_DUCK;
}
BotShootTarget(pBot, Weapon, Task->TaskTarget);
return;
}
if (!vIsZero(Task->TaskLocation))
{
MoveTo(pBot, Task->TaskLocation, MOVESTYLE_NORMAL, max_player_use_reach);
return;
}
Vector AttackLocation = (IsEdictPlayer(Task->TaskTarget) || IsEdictStructure(Task->TaskTarget)) ? Task->TaskTarget->v.origin : UTIL_GetButtonFloorLocation(pBot->Edict->v.origin, Task->TaskTarget);
if (vIsZero(AttackLocation))
{
AttackLocation = Task->TaskTarget->v.origin;
}
float WeaponRange = GetMaxIdealWeaponRange(Weapon);
Vector NewTaskLocation = FindClosestNavigablePointToDestination(pBot->BotNavInfo.NavProfile, pBot->CurrentFloorPosition, AttackLocation, WeaponRange);
Task->TaskLocation = (!vIsZero(NewTaskLocation)) ? NewTaskLocation : AttackLocation;
MoveTo(pBot, Task->TaskLocation, MOVESTYLE_NORMAL, max_player_use_reach);
return;
}

View file

@ -9502,7 +9502,7 @@ void AvHPlayer::UpdateAlienUI()
currentMask |= 0x80;
int teamMask=0;
AvHEntityHierarchy& theEntHier=GetGameRules()->GetEntityHierarchy(this->GetTeam());
AvHEntityHierarchy& theEntHier=GetGameRules()->GetEntityHierarchy(this->GetTeam(true));
teamMask |= ( theEntHier.GetNumSensory() & 0x3 );
teamMask <<= 2;
teamMask |= ( theEntHier.GetNumDefense() & 0x3 );