Files
source/TacticalAI/ZombieDecideAction.cpp
T
Sevenfm 32482f20f6 New option AI_DECISION_INFO enables extended logging for AI decisions.
Moved zombie AI into separate file ZombieDecideAction.cpp
New zombie AI:
- simplified code
- advancing using cover
- if zombie cannot reach any opponent, he tries to hide
- if zombie cannot reach opponent and cannot hide and he is under fire, he will retreat, seeking cover away from known opponents
- if zombie doesn't know enemy location, he will try to run to closest friend who knows enemy location
- new zombie AI has extended AI logging
More water splash sounds (Data\Sounds\Misc\Splash1.ogg .. SplashN.ogg).
CalcCoverValue: 
- allow moving into shallow water if it provides better position
- special calculations for zombies: zombie is very dangerous at close range
- if soldier has no gun, use different calculation based on distance instead of CTGT
FindBestNearbyCover: zombies only choose spots that provide sight cover.
Added functions for extended AI log.
ClosestReachableDisturbance:
- if soldier is zombie and he cannot climb, skip location
- zombies do not attack vehicles
- improved check to avoid choosing dying opponents
EstimatePathCostToLocation: return 0 if checking for zombie which cannot climb and path needs climbing.
RoamingRange: always max roaming range for zombies.
SightCoverAtSpot, ProneSightCoverAtSpot:
- improved max sight range calculation
- added option for unlimited sight range check
New AI functions:
- AIDirection
- AICheckIsSniper
- AICheckIsMarksman
- AICheckIsMedic
- AICheckIsMortarOperator
- AICheckIsGLOperator
- AICheckIsOfficer
- AICheckIsCommander
- AICheckIsMachinegunner
- AIGunInHandScoped
- AIGunScoped
- AIGunRange
- AIGunClass
- AIGunType
- AIGunDeadliness
- AIGunAmmo
- AIGunAutofireCapable
- FindObstacleNearSpot
- InSmoke
- CorpseWarning
- CorpseEnemyTeam
- CorpseMilitiaTeam
- AICheckUnderground
- AnyCoverAtSpot
- AICheckHasWeaponOfType
- AICheckHasGun
- AICheckShortWeaponRange
GetCorpseAtGridNo: additional check for valid gridno.
More zombie raise sounds (Data\Sounds\Misc\ZombieRaise1.ogg .. ZombieRaiseN.ogg).
GetCorpseRotFactor: improved code to return FLOAT value between 0 and 1.
New debug topic id TOPIC_DECISIONS for logging AI decisions.

git-svn-id: https://ja2svn.mooo.com/source/ja2/trunk/GameSource/ja2_v1.13/Build@8760 3b4a5df2-a311-0410-b5c6-a8a6f20db521
2020-02-21 23:33:25 +00:00

1420 lines
52 KiB
C++

#ifdef PRECOMPILEDHEADERS
#include "AI All.h"
#else
#include "ai.h"
#include "AIInternals.h"
#include "Isometric utils.h"
#include "Points.h"
#include "overhead.h"
#include "opplist.h"
#include "items.h"
#include "Weapons.h"
#include "NPC.h"
#include "Soldier Functions.h"
#include "worldman.h"
#include "Scheduling.h"
#include "Message.h"
#include "Structure Wrap.h"
#include "Keys.h"
#include "pathai.h"
#include "Render Fun.h"
#include "Boxing.h"
// #include "Air Raid.h"
#include "Soldier Profile.h"
#include "soldier profile type.h"
#include "Soldier macros.h"
#include "los.h"
#include "Buildings.h"
#include "strategicmap.h"
#include "Quests.h"
#include "Map Screen Interface Map.h"
#include "soldier ani.h"
#include "rotting corpses.h"
#include "GameSettings.h"
#include "Dialogue Control.h"
#endif
#include "connect.h"
#include "Text.h"
extern BOOLEAN gfHiddenInterrupt;
extern void LogDecideInfo(SOLDIERTYPE *pSoldier);
extern STR8 gStr8AlertStatus[];
extern STR8 gStr8Attitude[];
extern STR8 gStr8Orders[];
extern STR8 gStr8Team[];
extern STR8 gStr8Class[];
// ------------------------------ ZOMBIE AI --------------------------
INT8 ZombieDecideActionGreen(SOLDIERTYPE *pSoldier)
{
DOUBLE iChance, iSneaky = 10;
INT8 bInWater;
DebugMsg(TOPIC_JA2,DBG_LEVEL_3,String("DecideActionGreen, orders = %d",pSoldier->aiData.bOrders));
DebugAI( AI_MSG_START, pSoldier, String("[Green Zombie]"));
LogDecideInfo(pSoldier);
//DebugAI( AI_MSG_INFO, pSoldier, String("AP=%d/%d %s %s %s %s %s", pSoldier->bActionPoints, pSoldier->bInitialActionPoints, gStr8AlertStatus[pSoldier->aiData.bAlertStatus], gStr8Orders[pSoldier->aiData.bOrders], gStr8Attitude[pSoldier->aiData.bAttitude], gStr8Team[pSoldier->bTeam], gStr8Class[pSoldier->ubSoldierClass]) );
//DebugAI( AI_MSG_INFO, pSoldier, String("Health %d/%d Breath %d/%d Shock %d Tolerance %d AI Morale %d", pSoldier->stats.bLife, pSoldier->stats.bLifeMax, pSoldier->bBreath, pSoldier->bBreathMax, pSoldier->aiData.bShock, CalcSuppressionTolerance(pSoldier), pSoldier->aiData.bAIMorale) );
gubNPCPathCount = 0;
bInWater = Water( pSoldier->sGridNo, pSoldier->pathing.bLevel );
if ( bInWater && PreRandom( 2 ) )
{
// don't do nuttin!
return( AI_ACTION_NONE );
}
// if real-time, and not in the way, do nothing 90% of the time (for GUARDS!)
// unless in water (could've started there), then we better swim to shore!
////////////////////////////////////////////////////////////////////////////
// POINT PATROL: move towards next point unless getting a bit winded
////////////////////////////////////////////////////////////////////////////
// this takes priority over water/gas checks, so that point patrol WILL work
// from island to island, and through gas covered areas, too
if ((pSoldier->aiData.bOrders == POINTPATROL) && (pSoldier->bBreath >= 25))
{
if (PointPatrolAI(pSoldier))
{
if (!gfTurnBasedAI)
{
// wait after this...
pSoldier->aiData.bNextAction = AI_ACTION_WAIT;
pSoldier->aiData.usNextActionData = RealtimeDelay( pSoldier );
}
return(AI_ACTION_POINT_PATROL);
}
else
{
// Reset path count to avoid dedlok
gubNPCPathCount = 0;
}
}
if ((pSoldier->aiData.bOrders == RNDPTPATROL) && (pSoldier->bBreath >=25))
{
if (RandomPointPatrolAI(pSoldier))
{
if (!gfTurnBasedAI)
{
// wait after this...
pSoldier->aiData.bNextAction = AI_ACTION_WAIT;
pSoldier->aiData.usNextActionData = RealtimeDelay( pSoldier );
}
return(AI_ACTION_POINT_PATROL);
}
else
{
// Reset path count to avoid dedlok
gubNPCPathCount = 0;
}
}
////////////////////////////////////////////////////////////////////////////
// WHEN LEFT IN WATER OR GAS, GO TO NEAREST REACHABLE SPOT OF UNGASSED LAND
////////////////////////////////////////////////////////////////////////////
DebugMsg(TOPIC_JA2,DBG_LEVEL_3,String("ZombieDecideActionGreen: get out of water and gas"));
if ( bInWater )
{
pSoldier->aiData.usActionData = FindNearestUngassedLand(pSoldier);
if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))
{
return(AI_ACTION_LEAVE_WATER_GAS);
}
}
////////////////////////////////////////////////////////////////////////
// REST IF RUNNING OUT OF BREATH
////////////////////////////////////////////////////////////////////////
DebugMsg(TOPIC_JA2,DBG_LEVEL_3,String("ZombieDecideActionGreen: rest if running out of breath"));
// if our breath is running a bit low, and we're not in the way or in water
if ((pSoldier->bBreath < 25) && !bInWater)
{
// take a breather for gods sake!
// for realtime, AI will use a standard wait set outside of here
pSoldier->aiData.usActionData = NOWHERE;
return(AI_ACTION_NONE);
}
////////////////////////////////////////////////////////////////////////////
// CLIMB A BUILDING
////////////////////////////////////////////////////////////////////////////
// sevenfm: disable roof climbing for zombies in Green AI
/*
if ( gGameExternalOptions.fZombieCanClimb && pSoldier->aiData.bLastAction != AI_ACTION_CLIMB_ROOF && !is_networked )
{
iChance = 10 + pSoldier->aiData.bBypassToGreen;
// set base chance and maximum seeking distance according to orders
switch (pSoldier->aiData.bOrders)
{
case STATIONARY: iChance *= 0; break;
case ONGUARD: iChance += 10; break;
case ONCALL: break;
case CLOSEPATROL: iChance += -20; break;
case RNDPTPATROL:
case POINTPATROL: iChance = -30; break;
case FARPATROL: iChance += -40; break;
case SEEKENEMY: iChance += -30; break;
case SNIPER: iChance += 70; break;
}
// modify for attitude
switch (pSoldier->aiData.bAttitude)
{
case DEFENSIVE: iChance *= 1.5; break;
case BRAVESOLO: iChance /= 2; break;
case BRAVEAID: iChance /= 2; break;
case CUNNINGSOLO: iChance *= 1; break;
case CUNNINGAID: iChance /= 1; break;
case AGGRESSIVE: iChance /= 3; break;
case ATTACKSLAYONLY: break;
}
// This is the chance that we want to be on the roof. If already there, invert the chance to see if we want back
// down
if (pSoldier->pathing.bLevel > 0)
{
iChance = 100 - iChance;
}
if ((INT16) PreRandom(100) < iChance)
{
BOOLEAN fUp = FALSE;
if ( pSoldier->pathing.bLevel == 0 )
{
fUp = TRUE;
}
else if (pSoldier->pathing.bLevel > 0 )
{
fUp = FALSE;
}
if ( CanClimbFromHere ( pSoldier ) )
{
DebugMsg ( TOPIC_JA2AI , DBG_LEVEL_3 , String("Zombie %d is climbing roof",pSoldier->ubID) );
return( AI_ACTION_CLIMB_ROOF );
}
else
{
pSoldier->aiData.usActionData = FindClosestClimbPoint(pSoldier, fUp );
// Added the check here because sniper militia who are locked inside of a building without keys
// will still have a >100% chance to want to climb, which means an infinite loop. In fact, any
// time a move is desired, there probably also will be a need to check for a path.
if ( !TileIsOutOfBounds(pSoldier->aiData.usActionData) &&
LegalNPCDestination(pSoldier,pSoldier->aiData.usActionData,ENSURE_PATH,WATEROK, 0 ))
{
return( AI_ACTION_MOVE_TO_CLIMB );
}
}
}
}
*/
////////////////////////////////////////////////////////////////////////////
// RANDOM PATROL: determine % chance to start a new patrol route
////////////////////////////////////////////////////////////////////////////
if (!gubNPCPathCount) // try to limit pathing in Green AI
{
iChance = 25 + pSoldier->aiData.bBypassToGreen;
// sevenfm: limit chance
iChance = iChance * pSoldier->bBreath / 100;
// if we're in water with land miles (> 25 tiles) away,
// OR if we roll under the chance calculated
if (bInWater || ((INT16) PreRandom(100) < iChance))
{
pSoldier->aiData.usActionData = RandDestWithinRange(pSoldier);
if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))
{
pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards( pSoldier, pSoldier->aiData.usActionData, AI_ACTION_RANDOM_PATROL );
}
if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))
{
if (!gfTurnBasedAI)
{
// wait after this...
pSoldier->aiData.bNextAction = AI_ACTION_WAIT;
pSoldier->aiData.usNextActionData = RealtimeDelay( pSoldier );
}
return(AI_ACTION_RANDOM_PATROL);
}
}
}
if (!gubNPCPathCount) // try to limit pathing in Green AI
{
////////////////////////////////////////////////////////////////////////////
// SEEK FRIEND: determine %chance for man to pay a friendly visit
////////////////////////////////////////////////////////////////////////////
iChance = 25 + pSoldier->aiData.bBypassToGreen;
// set base chance and maximum seeking distance according to orders
switch (pSoldier->aiData.bOrders)
{
case STATIONARY: iChance += -20; break;
case ONGUARD: iChance += -15; break;
case ONCALL: break;
case CLOSEPATROL: iChance += +10; break;
case RNDPTPATROL:
case POINTPATROL: iChance = -10; break;
case FARPATROL: iChance += +20; break;
case SEEKENEMY: iChance += -10; break;
case SNIPER: iChance += -10; break;
}
// modify for attitude
switch (pSoldier->aiData.bAttitude)
{
case DEFENSIVE: break;
case BRAVESOLO: iChance /= 2; break; // loners
case BRAVEAID: iChance += 10; break; // friendly
case CUNNINGSOLO: iChance /= 2; break; // loners
case CUNNINGAID: iChance += 10; break; // friendly
case AGGRESSIVE: break;
case ATTACKSLAYONLY: break;
}
if ((INT16) PreRandom(100) < iChance)
{
if (RandomFriendWithin(pSoldier))
{
if ( pSoldier->aiData.usActionData == GoAsFarAsPossibleTowards( pSoldier, pSoldier->aiData.usActionData, AI_ACTION_SEEK_FRIEND ) )
{
if ( !gfTurnBasedAI)
{
// pause at the end of the walk!
pSoldier->aiData.bNextAction = AI_ACTION_WAIT;
pSoldier->aiData.usNextActionData = (UINT16) REALTIME_CIV_AI_DELAY;
}
return(AI_ACTION_SEEK_FRIEND);
}
}
}
}
////////////////////////////////////////////////////////////////////////////
// LOOK AROUND: determine %chance for man to turn in place
////////////////////////////////////////////////////////////////////////////
DebugMsg(TOPIC_JA2,DBG_LEVEL_3,String("ZombieDecideActionGreen: Soldier deciding to turn"));
if (GetAPsToLook( pSoldier ) <= pSoldier->bActionPoints)
{
// avoid 2 consecutive random turns in a row
if (pSoldier->aiData.bLastAction != AI_ACTION_CHANGE_FACING)
{
iChance = 25 + pSoldier->aiData.bBypassToGreen;
// set base chance according to orders
if (pSoldier->aiData.bOrders == STATIONARY || pSoldier->aiData.bOrders == SNIPER)
iChance += 25;
if (pSoldier->aiData.bOrders == ONGUARD)
iChance += 20;
if (pSoldier->aiData.bAttitude == DEFENSIVE)
iChance += 25;
if ( pSoldier->aiData.bOrders == SNIPER && pSoldier->pathing.bLevel == 1)
iChance += 35;
if ((INT16)PreRandom(100) < iChance)
{
// roll random directions (stored in actionData) until different from current
do
{
// if man has a LEGAL dominant facing, and isn't facing it, he will turn
// back towards that facing 50% of the time here (normally just enemies)
if ((pSoldier->aiData.bDominantDir >= 0) && (pSoldier->aiData.bDominantDir <= 8) &&
(pSoldier->ubDirection != pSoldier->aiData.bDominantDir) && PreRandom(2) && pSoldier->aiData.bOrders != SNIPER )
{
pSoldier->aiData.usActionData = pSoldier->aiData.bDominantDir;
}
else
{
INT32 iNoiseValue;
BOOLEAN fClimb;
BOOLEAN fReachable;
INT32 sNoiseGridNo = MostImportantNoiseHeard(pSoldier,&iNoiseValue, &fClimb, &fReachable);
UINT8 ubNoiseDir;
if (TileIsOutOfBounds(sNoiseGridNo) || (ubNoiseDir = AIDirection(pSoldier->sGridNo, sNoiseGridNo)) == pSoldier->ubDirection )
{
pSoldier->aiData.usActionData = PreRandom(8);
}
else
{
pSoldier->aiData.usActionData = ubNoiseDir;
}
}
} while (pSoldier->aiData.usActionData == pSoldier->ubDirection);
DebugMsg(TOPIC_JA2,DBG_LEVEL_3,String("DecideActionGreen: Trying to turn - checking stance validity"));
if ( pSoldier->InternalIsValidStance( (INT8) pSoldier->aiData.usActionData, gAnimControl[ pSoldier->usAnimState ].ubEndHeight ) )
{
if ( !gfTurnBasedAI )
{
// wait after this...
pSoldier->aiData.bNextAction = AI_ACTION_WAIT;
pSoldier->aiData.usNextActionData = RealtimeDelay( pSoldier );
}
DebugMsg(TOPIC_JA2,DBG_LEVEL_3,String("DecideActionGreen: Soldier is turning"));
return(AI_ACTION_CHANGE_FACING);
}
}
}
}
// by default, if everything else fails, just stands in place without turning
// for realtime, regular AI guys will use a standard wait set outside of here
pSoldier->aiData.usActionData = NOWHERE;
return(AI_ACTION_NONE);
}
INT8 ZombieDecideActionYellow(SOLDIERTYPE *pSoldier)
{
UINT8 ubNoiseDir;
INT32 sNoiseGridNo;
INT32 iNoiseValue;
INT32 iChance;
BOOLEAN fClimb;
BOOLEAN fReachable;
DebugAI( AI_MSG_START, pSoldier, String("[Yellow Zombie]"));
LogDecideInfo(pSoldier);
//DebugAI( AI_MSG_INFO, pSoldier, String("AP=%d/%d %s %s %s %s %s", pSoldier->bActionPoints, pSoldier->bInitialActionPoints, gStr8AlertStatus[pSoldier->aiData.bAlertStatus], gStr8Orders[pSoldier->aiData.bOrders], gStr8Attitude[pSoldier->aiData.bAttitude], gStr8Team[pSoldier->bTeam], gStr8Class[pSoldier->ubSoldierClass]) );
//DebugAI( AI_MSG_INFO, pSoldier, String("Health %d/%d Breath %d/%d Shock %d Tolerance %d AI Morale %d", pSoldier->stats.bLife, pSoldier->stats.bLifeMax, pSoldier->bBreath, pSoldier->bBreathMax, pSoldier->aiData.bShock, CalcSuppressionTolerance(pSoldier), pSoldier->aiData.bAIMorale) );
// determine the most important noise heard, and its relative value
sNoiseGridNo = MostImportantNoiseHeard(pSoldier,&iNoiseValue, &fClimb, &fReachable);
if (TileIsOutOfBounds(sNoiseGridNo))
{
// then we have no business being under YELLOW status any more!
#ifdef BETAVERSION
NumMessage("DecideActionYellow: ERROR - No important noise known by guynum ",pSoldier->ubID);
#endif
return(AI_ACTION_NONE);
}
////////////////////////////////////////////////////////////////////////////
// LOOK AROUND TOWARD NOISE: determine %chance for man to turn towards noise
////////////////////////////////////////////////////////////////////////////
// determine direction from this soldier in which the noise lies
ubNoiseDir = AIDirection(pSoldier->sGridNo, sNoiseGridNo);
// if soldier is not already facing in that direction,
// and the noise source is close enough that it could possibly be seen
if ( GetAPsToLook( pSoldier ) <= pSoldier->bActionPoints )
{
if ((pSoldier->ubDirection != ubNoiseDir) && PythSpacesAway(pSoldier->sGridNo,sNoiseGridNo) <= pSoldier->GetMaxDistanceVisible(sNoiseGridNo) )
{
// set base chance according to orders
if ((pSoldier->aiData.bOrders == STATIONARY) || (pSoldier->aiData.bOrders == ONGUARD) )
iChance = 50;
else // all other orders
iChance = 25;
if (pSoldier->aiData.bAttitude == DEFENSIVE)
iChance += 15;
if ((INT16)PreRandom(100) < iChance && pSoldier->InternalIsValidStance( ubNoiseDir, gAnimControl[ pSoldier->usAnimState ].ubEndHeight ) )
{
pSoldier->aiData.usActionData = ubNoiseDir;
return(AI_ACTION_CHANGE_FACING);
}
}
}
////////////////////////////////////////////////////////////////////////////
// RADIO YELLOW ALERT: determine %chance to call others and report noise
////////////////////////////////////////////////////////////////////////////
// if we have the action points remaining to RADIO
// (we never want NPCs to choose to radio if they would have to wait a turn)
if ( (pSoldier->bActionPoints >= APBPConstants[AP_RADIO]) && (gTacticalStatus.Team[pSoldier->bTeam].bMenInSector > 1) )
{
// base chance depends on how much new info we have to radio to the others
iChance = 5 * WhatIKnowThatPublicDont(pSoldier,FALSE); // use 5 * for YELLOW alert
// if I actually know something they don't and I ain't swimming (deep water)
if (iChance && !DeepWater( pSoldier->sGridNo, pSoldier->pathing.bLevel ))
{
// CJC: this addition allows for varying difficulty levels for soldier types
iChance += gbDiff[ DIFF_RADIO_RED_ALERT ][ SoldierDifficultyLevel( pSoldier ) ] / 2;
// Alex: this addition replaces the sectorValue/2 in original JA
//iChance += gsDiff[DIFF_RADIO_RED_ALERT][GameOption[ENEMYDIFFICULTY]] / 2;
// modify base chance according to orders
switch (pSoldier->aiData.bOrders)
{
case STATIONARY: iChance += 20; break;
case ONGUARD: iChance += 15; break;
case ONCALL: iChance += 10; break;
case CLOSEPATROL: break;
case RNDPTPATROL:
case POINTPATROL: break;
case FARPATROL: iChance += -10; break;
case SEEKENEMY: iChance += -20; break;
case SNIPER: iChance += -10; break; //Madd: sniper contacts are supposed to be automatically reported
}
// modify base chance according to attitude
switch (pSoldier->aiData.bAttitude)
{
case DEFENSIVE: iChance += 20; break;
case BRAVESOLO: iChance += -10; break;
case BRAVEAID: break;
case CUNNINGSOLO:iChance += -5; break;
case CUNNINGAID: break;
case AGGRESSIVE: iChance += -20; break;
case ATTACKSLAYONLY: iChance = 0; break;
}
if ((INT16)PreRandom(100) < iChance)
{
return(AI_ACTION_YELLOW_ALERT);
}
}
}
////////////////////////////////////////////////////////////////////////
// REST IF RUNNING OUT OF BREATH
////////////////////////////////////////////////////////////////////////
// if our breath is running a bit low, and we're not in water
if ((pSoldier->bBreath < 25) && !pSoldier->MercInWater())
{
// take a breather for gods sake!
pSoldier->aiData.usActionData = NOWHERE;
return(AI_ACTION_NONE);
}
pSoldier->aiData.usActionData = GoAsFarAsPossibleTowards(pSoldier,sNoiseGridNo,AI_ACTION_SEEK_NOISE);
if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))
{
if ( fClimb )
{
if ( pSoldier->sGridNo == sNoiseGridNo)
{
if (IsActionAffordable(pSoldier, pSoldier->aiData.bAction))
{
return( AI_ACTION_CLIMB_ROOF );
}
else if ( gfTurnBasedAI && pSoldier->bActionPoints < pSoldier->bInitialActionPoints )
{
return AI_ACTION_NONE;
}
}
else
{
pSoldier->aiData.usActionData = sNoiseGridNo;
return( AI_ACTION_MOVE_TO_CLIMB );
}
}
else
{
return(AI_ACTION_SEEK_NOISE);
}
}
////////////////////////////////////////////////////////////////////////////
// SWITCH TO GREEN: determine if soldier acts as if nothing at all was wrong
////////////////////////////////////////////////////////////////////////////
if ((INT16)PreRandom(100) < 50 )
{
// Skip YELLOW until new situation, 15% extra chance to do GREEN actions
pSoldier->aiData.bBypassToGreen = 15;
return(ZombieDecideActionGreen(pSoldier));
}
////////////////////////////////////////////////////////////////////////////
// DO NOTHING: Not enough points left to move, so save them for next turn
////////////////////////////////////////////////////////////////////////////
// by default, if everything else fails, just stands in place without turning
pSoldier->aiData.usActionData = NOWHERE;
return(AI_ACTION_NONE);
}
INT8 ZombieDecideActionRed(SOLDIERTYPE *pSoldier)
{
INT32 iDummy;
INT32 sClosestOpponent,sClosestFriend;
INT32 sClosestDisturbance, sDistVisible;
INT32 sMostImportantNoise;
INT32 sClosestThreat = NOWHERE;
UINT8 ubCanMove,ubOpponentDir;
INT8 bInWater, bInDeepWater;
INT8 bSeekPts = 0, bHelpPts = 0, bWatchPts = 0;
BOOLEAN fClimb;
UINT8 ubThreatID = NOBODY;
INT32 sThreatSpot = NOWHERE;
INT8 bThreatLevel = 0;
DebugMsg (TOPIC_JA2,DBG_LEVEL_3,String("ZombieDecideActionRed: soldier orders = %d",pSoldier->aiData.bOrders));
DebugAI( AI_MSG_START, pSoldier, String("[Red Zombie]"));
LogDecideInfo(pSoldier);
//DebugAI( AI_MSG_INFO, pSoldier, String("AP=%d/%d %s %s %s %s %s", pSoldier->bActionPoints, pSoldier->bInitialActionPoints, gStr8AlertStatus[pSoldier->aiData.bAlertStatus], gStr8Orders[pSoldier->aiData.bOrders], gStr8Attitude[pSoldier->aiData.bAttitude], gStr8Team[pSoldier->bTeam], gStr8Class[pSoldier->ubSoldierClass]) );
//DebugAI( AI_MSG_INFO, pSoldier, String("Health %d/%d Breath %d/%d Shock %d Tolerance %d AI Morale %d", pSoldier->stats.bLife, pSoldier->stats.bLifeMax, pSoldier->bBreath, pSoldier->bBreathMax, pSoldier->aiData.bShock, CalcSuppressionTolerance(pSoldier), pSoldier->aiData.bAIMorale) );
// if we have absolutely no action points, we can't do a thing under RED!
if (!pSoldier->bActionPoints)
{
pSoldier->aiData.usActionData = NOWHERE;
return(AI_ACTION_NONE);
}
// can this guy move to any of the neighbouring squares ? (sets TRUE/FALSE)
ubCanMove = (pSoldier->bActionPoints >= MinPtsToMove(pSoldier));
// determine if we happen to be in water (in which case we're in BIG trouble!)
bInWater = Water( pSoldier->sGridNo, pSoldier->pathing.bLevel );
bInDeepWater = DeepWater( pSoldier->sGridNo, pSoldier->pathing.bLevel );
DebugAI( AI_MSG_INFO, pSoldier, String("CanMove %d InWater %d InDeepWater %d", ubCanMove, bInWater, bInDeepWater));
// calculate our morale
pSoldier->aiData.bAIMorale = CalcMorale(pSoldier);
////////////////////////////////////////////////////////////////////////
// CROUCH & REST IF RUNNING OUT OF BREATH
////////////////////////////////////////////////////////////////////////
DebugAI( AI_MSG_INFO, pSoldier, String("[crouch and rest]"));
// if our breath is running a bit low, and we're not in water or under fire
if ((pSoldier->bBreath < 25) && !bInWater && !pSoldier->aiData.bUnderFire)
{
pSoldier->aiData.usActionData = NOWHERE;
return(AI_ACTION_NONE);
}
DebugMsg (TOPIC_JA2,DBG_LEVEL_3,"decideactionred: main red ai");
////////////////////////////////////////////////////////////////////////////
// MAIN RED AI
////////////////////////////////////////////////////////////////////////////
DebugAI( AI_MSG_INFO, pSoldier, String("Main Red AI"));
// get the location of the closest reachable opponent
//sClosestDisturbance = ClosestReachableDisturbance(pSoldier, &fClimb, ubThreatID, sThreatSpot, bThreatLevel);
sClosestDisturbance = ClosestReachableDisturbance(pSoldier, &fClimb);
sClosestOpponent = ClosestKnownOpponent(pSoldier, NULL, NULL);
sMostImportantNoise = MostImportantNoiseHeard( pSoldier, NULL, NULL, NULL );
DebugAI( AI_MSG_INFO, pSoldier, String("Closest Disturbance %d Closest Opponent %d Most Important Noise %d", sClosestDisturbance, sClosestOpponent, sMostImportantNoise));
sClosestThreat = sClosestDisturbance;
if(TileIsOutOfBounds( sClosestThreat ))
{
sClosestThreat = sClosestOpponent;
}
if( TileIsOutOfBounds( sClosestThreat ) )
{
sClosestThreat = sMostImportantNoise;
}
DebugAI( AI_MSG_INFO, pSoldier, String("Closest Threat %d", sClosestThreat));
if( ubCanMove &&
pSoldier->bActionPoints > APBPConstants[MAX_AP_CARRIED] &&
!gfHiddenInterrupt )
{
DebugAI( AI_MSG_INFO, pSoldier, String("[seek opponent]"));
// if there is an opponent reachable
if (!TileIsOutOfBounds(sClosestDisturbance) )
{
DebugAI( AI_MSG_INFO, pSoldier, String("found disturbance %d climb %d", sClosestDisturbance, fClimb));
//////////////////////////////////////////////////////////////////////
// SEEK CLOSEST DISTURBANCE: GO DIRECTLY TOWARDS CLOSEST KNOWN OPPONENT
//////////////////////////////////////////////////////////////////////
if (fClimb && pSoldier->sGridNo == sClosestDisturbance)
{
DebugAI( AI_MSG_INFO, pSoldier, String("at climb point"));
// wait for next turn if turnbased (to climb with max APs)
if( gfTurnBasedAI &&
pSoldier->bActionPoints < pSoldier->bInitialActionPoints )
{
pSoldier->aiData.bNextAction = AI_ACTION_NONE;
return( AI_ACTION_END_TURN );
}
// climb
if (IsActionAffordable(pSoldier, AI_ACTION_CLIMB_ROOF))
{
pSoldier->aiData.bNextAction = AI_ACTION_NONE;
return( AI_ACTION_CLIMB_ROOF );
}
// cannot climb at all
}
// try to move towards him
pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, sClosestDisturbance, 0, AI_ACTION_SEEK_OPPONENT, 0);
DebugAI( AI_MSG_INFO, pSoldier, String("found seek spot %d", pSoldier->aiData.usActionData));
// if it's possible
if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))
{
DebugAI( AI_MSG_INFO, pSoldier, String("[move to seek spot]"));
// sevenfm: try to advance using sight cover
if( gfTurnBasedAI &&
PythSpacesAway(pSoldier->sGridNo, sClosestDisturbance) < DAY_VISION_RANGE * 2 &&
PythSpacesAway(pSoldier->sGridNo, sClosestDisturbance) > DAY_VISION_RANGE / 4 &&
!SightCoverAtSpot(pSoldier, pSoldier->aiData.usActionData, TRUE))
{
INT32 sAdvanceSpot = FindAdvanceSpot(pSoldier, sClosestDisturbance, AI_ACTION_SEEK_OPPONENT, ADVANCE_SPOT_SIGHT_COVER, TRUE);
DebugAI( AI_MSG_INFO, pSoldier, String("found advance spot %d", sAdvanceSpot));
if( !TileIsOutOfBounds(sAdvanceSpot) )
{
DebugAI( AI_MSG_INFO, pSoldier, String("move to advance spot"));
pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, sAdvanceSpot, 0, AI_ACTION_SEEK_OPPONENT, 0);
if( !TileIsOutOfBounds(pSoldier->aiData.usActionData) )
{
return(AI_ACTION_SEEK_OPPONENT);
}
}
// if we are in cover and new spot has no cover - wait for next turn
if( SightCoverAtSpot(pSoldier, pSoldier->sGridNo, TRUE) &&
gfTurnBasedAI && pSoldier->bActionPoints != pSoldier->bInitialActionPoints ) //&&
//CountFriendsBlack(pSoldier, sClosestDisturbance) == 0 )
{
DebugAI( AI_MSG_INFO, pSoldier, String("check turn to closest disturbance before ending turn"));
// first turn to closest threat
if( GetAPsToLook( pSoldier ) <= pSoldier->bActionPoints )
{
// determine direction from this soldier to the closest opponent
ubOpponentDir = AIDirection(pSoldier->sGridNo, sClosestDisturbance);
DebugAI( AI_MSG_INFO, pSoldier, String("soldier direction %d disturbance direction %d", pSoldier->ubDirection, ubOpponentDir));
sDistVisible = pSoldier->GetMaxDistanceVisible(sClosestThreat, 0, CALC_FROM_ALL_DIRS );
if( pSoldier->ubDirection != ubOpponentDir &&
PythSpacesAway(pSoldier->sGridNo,sClosestThreat) <= sDistVisible &&
pSoldier->InternalIsValidStance( ubOpponentDir, gAnimControl[ pSoldier->usAnimState ].ubEndHeight ) )
{
pSoldier->aiData.usActionData = ubOpponentDir;
return(AI_ACTION_CHANGE_FACING);
}
DebugAI( AI_MSG_INFO, pSoldier, String("cannot turn"));
}
DebugAI( AI_MSG_INFO, pSoldier, String("wait for next turn"));
//ScreenMsg(FONT_GREEN, MSG_INTERFACE, L"[%d] wait for next turn", pSoldier->ubID);
return AI_ACTION_NONE;
}
}
return(AI_ACTION_SEEK_OPPONENT);
}
}
DebugAI( AI_MSG_INFO, pSoldier, String("[seek friend]"));
// if no opponent - go to closest friend
sClosestFriend = ClosestReachableFriendInTrouble(pSoldier, &fClimb );
if (!TileIsOutOfBounds(sClosestFriend) )
{
DebugAI( AI_MSG_INFO, pSoldier, String("found friend %d climb %d", sClosestFriend, fClimb));
//////////////////////////////////////////////////////////////////////
// GO DIRECTLY TOWARDS CLOSEST FRIEND UNDER FIRE OR WHO LAST RADIOED
//////////////////////////////////////////////////////////////////////
if ( fClimb && pSoldier->sGridNo == sClosestFriend )
{
DebugAI( AI_MSG_INFO, pSoldier, String("at climb spot"));
// wait for next turn to have full APs
if( gfTurnBasedAI && pSoldier->bActionPoints < pSoldier->bInitialActionPoints )
{
return( AI_ACTION_END_TURN);
}
// climb
if( IsActionAffordable(pSoldier, AI_ACTION_CLIMB_ROOF) )
{
return( AI_ACTION_CLIMB_ROOF );
}
// don't have enough AP for climbing
}
else
{
DebugAI( AI_MSG_INFO, pSoldier, String("move to friend %d", sClosestFriend));
// try to move towards him
pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, sClosestFriend, 0, AI_ACTION_SEEK_FRIEND, 0);
DebugAI( AI_MSG_INFO, pSoldier, String("found move spot %d", pSoldier->aiData.usActionData));
// if it's possible
if ( !TileIsOutOfBounds(pSoldier->aiData.usActionData) )
{
return(AI_ACTION_SEEK_FRIEND);
}
else
{
DebugAI( AI_MSG_INFO, pSoldier, String("cannot move!"));
}
}
}
DebugAI( AI_MSG_INFO, pSoldier, String("[hide]"));
// if cannot seek or help - hide
if( gfTurnBasedAI &&
pSoldier->bActionPoints == pSoldier->bInitialActionPoints &&
(!SightCoverAtSpot(pSoldier, pSoldier->sGridNo, TRUE) || pSoldier->aiData.bUnderFire) )
{
pSoldier->aiData.usActionData = FindBestNearbyCover(pSoldier, pSoldier->aiData.bAIMorale, &iDummy);
if( !TileIsOutOfBounds(pSoldier->aiData.usActionData) )
{
DebugAI( AI_MSG_INFO, pSoldier, String("found cover spot %d", pSoldier->aiData.usActionData));
return AI_ACTION_TAKE_COVER;
}
else if (pSoldier->aiData.bUnderFire)
{
pSoldier->aiData.usActionData = FindRetreatSpot(pSoldier);
if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))
{
DebugAI(AI_MSG_INFO, pSoldier, String("found retreat spot %d", pSoldier->aiData.usActionData));
return AI_ACTION_TAKE_COVER;
}
}
}
}
DebugAI( AI_MSG_INFO, pSoldier, String("[look around towards opponent]"));
////////////////////////////////////////////////////////////////////////////
// LOOK AROUND TOWARD CLOSEST KNOWN OPPONENT, IF KNOWN
////////////////////////////////////////////////////////////////////////////
if( GetAPsToLook( pSoldier ) <= pSoldier->bActionPoints &&
!TileIsOutOfBounds(sClosestThreat) )
{
// determine direction from this soldier to the closest opponent
ubOpponentDir = AIDirection(pSoldier->sGridNo, sClosestThreat);
DebugAI( AI_MSG_INFO, pSoldier, String("soldier direction %d opponent direction %d", pSoldier->ubDirection, ubOpponentDir));
// if soldier is not already facing in that direction,
// and the opponent is close enough that he could possibly be seen
// note, have to change this to use the level returned from ClosestKnownOpponent
sDistVisible = pSoldier->GetMaxDistanceVisible(sClosestThreat, 0, CALC_FROM_ALL_DIRS );
if( pSoldier->ubDirection != ubOpponentDir &&
PythSpacesAway(pSoldier->sGridNo,sClosestThreat) <= sDistVisible &&
pSoldier->InternalIsValidStance( ubOpponentDir, gAnimControl[ pSoldier->usAnimState ].ubEndHeight ) )
{
pSoldier->aiData.usActionData = ubOpponentDir;
return(AI_ACTION_CHANGE_FACING);
}
DebugAI( AI_MSG_INFO, pSoldier, String("cannot turn"));
}
////////////////////////////////////////////////////////////////////////////
// SWITCH TO GREEN: soldier does ordinary regular patrol, seeks friends
////////////////////////////////////////////////////////////////////////////
DebugAI( AI_MSG_INFO, pSoldier, String("[switch to green]"));
// if not in combat or under fire, and we COULD have moved, just chose not to
if( (pSoldier->aiData.bAlertStatus != STATUS_BLACK) &&
!pSoldier->aiData.bUnderFire &&
ubCanMove &&
(!gfTurnBasedAI || pSoldier->bActionPoints == pSoldier->bInitialActionPoints) &&
TileIsOutOfBounds(sClosestDisturbance) )
{
DebugAI( AI_MSG_INFO, pSoldier, String("decide green"));
// Skip RED until new situation/next turn, 30% extra chance to do GREEN actions
pSoldier->aiData.bBypassToGreen = 30;
return(ZombieDecideActionGreen(pSoldier) );
}
////////////////////////////////////////////////////////////////////////////
// IF UNDER FIRE, FACE THE MOST IMPORTANT NOISE WE KNOW
////////////////////////////////////////////////////////////////////////////
DebugAI( AI_MSG_INFO, pSoldier, String("[face the most important noise]"));
if( pSoldier->aiData.bUnderFire &&
pSoldier->CheckInitialAP() &&
//pSoldier->bActionPoints == pSoldier->bInitialActionPoints &&
GetAPsToLook( pSoldier ) <= pSoldier->bActionPoints &&
!TileIsOutOfBounds( sMostImportantNoise ) )
{
ubOpponentDir = AIDirection(pSoldier->sGridNo, sMostImportantNoise);
DebugAI( AI_MSG_INFO, pSoldier, String("soldier direction %d noise direction %d", pSoldier->ubDirection, ubOpponentDir));
if ( pSoldier->ubDirection != ubOpponentDir &&
pSoldier->InternalIsValidStance( ubOpponentDir, gAnimControl[ pSoldier->usAnimState ].ubEndHeight ))
{
pSoldier->aiData.usActionData = ubOpponentDir;
return( AI_ACTION_CHANGE_FACING );
}
DebugAI( AI_MSG_INFO, pSoldier, String("cannot turn"));
}
////////////////////////////////////////////////////////////////////////////
// DO NOTHING: Not enough points left to move, so save them for next turn
////////////////////////////////////////////////////////////////////////////
DebugAI( AI_MSG_INFO, pSoldier, String("nothing to do!"));
pSoldier->aiData.usActionData = NOWHERE;
return(AI_ACTION_NONE);
}
INT8 ZombieDecideActionBlack(SOLDIERTYPE *pSoldier)
{
INT32 sClosestOpponent;
INT32 sClosestDisturbance;
INT16 ubMinAPCost;
UINT8 ubCanMove;
INT8 bInWater,bInDeepWater;
INT8 bDirection;
UINT8 ubBestAttackAction = AI_ACTION_NONE;
INT8 bCanAttack;
INT8 bWeaponIn;
BOOLEAN fClimb;
INT32 iDummy;
UINT8 ubThreatID = NOBODY;
INT32 sThreatSpot = NOWHERE;
INT8 bThreatLevel = 0;
DebugMsg(TOPIC_JA2,DBG_LEVEL_3,String("DecideActionBlack: soldier = %d, orders = %d, attitude = %d",pSoldier->ubID,pSoldier->aiData.bOrders,pSoldier->aiData.bAttitude));
DebugAI( AI_MSG_START, pSoldier, String("[Black Zombie]"));
LogDecideInfo(pSoldier);
//DebugAI( AI_MSG_INFO, pSoldier, String("AP=%d/%d %s %s %s %s %s", pSoldier->bActionPoints, pSoldier->bInitialActionPoints, gStr8AlertStatus[pSoldier->aiData.bAlertStatus], gStr8Orders[pSoldier->aiData.bOrders], gStr8Attitude[pSoldier->aiData.bAttitude], gStr8Team[pSoldier->bTeam], gStr8Class[pSoldier->ubSoldierClass]) );
//DebugAI( AI_MSG_INFO, pSoldier, String("Health %d/%d Breath %d/%d Shock %d Tolerance %d AI Morale %d", pSoldier->stats.bLife, pSoldier->stats.bLifeMax, pSoldier->bBreath, pSoldier->bBreathMax, pSoldier->aiData.bShock, CalcSuppressionTolerance(pSoldier), pSoldier->aiData.bAIMorale) );
ATTACKTYPE BestStab, BestAttack;
BOOLEAN fAllowCoverCheck = FALSE;
// if we have absolutely no action points, we can't do a thing under BLACK!
if (pSoldier->bActionPoints <= 0)
{
DebugAI( AI_MSG_INFO, pSoldier, String("no AP! nothing to do"));
pSoldier->aiData.usActionData = NOWHERE;
return(AI_ACTION_NONE);
}
// can this guy move to any of the neighbouring squares ? (sets TRUE/FALSE)
ubCanMove = (pSoldier->bActionPoints >= MinPtsToMove(pSoldier));
// determine if we happen to be in water (in which case we're in BIG trouble!)
bInWater = Water( pSoldier->sGridNo, pSoldier->pathing.bLevel );
bInDeepWater = WaterTooDeepForAttacks( pSoldier->sGridNo, pSoldier->pathing.bLevel );
// calculate our morale
pSoldier->aiData.bAIMorale = CalcMorale(pSoldier);
DebugAI( AI_MSG_INFO, pSoldier, String("CanMove %d InWater %d InDeepWater %d", ubCanMove, bInWater, bInDeepWater));
////////////////////////////////////////////////////////////////////////////
// STUCK IN WATER OR GAS, NO COVER, GO TO NEAREST SPOT OF UNGASSED LAND
////////////////////////////////////////////////////////////////////////////
// if soldier in water/gas has enough APs left to move at least 1 square
if ( bInDeepWater && ubCanMove)
{
DebugAI( AI_MSG_INFO, pSoldier, String("in deep water: find nearest land"));
pSoldier->aiData.usActionData = FindNearestUngassedLand(pSoldier);
if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))
{
DebugAI( AI_MSG_INFO, pSoldier, String("leave deep water: move to %d", pSoldier->aiData.usActionData));
return(AI_ACTION_LEAVE_WATER_GAS);
}
DebugAI( AI_MSG_INFO, pSoldier, String("couldn't find ANY land within 25 tiles"));
// couldn't find ANY land within 25 tiles(!), this should never happen...
// look for best place to RUN AWAY to (farthest from the closest threat)
pSoldier->aiData.usActionData = FindSpotMaxDistFromOpponents(pSoldier);
if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))
{
DebugAI( AI_MSG_INFO, pSoldier, String("move to %d", pSoldier->aiData.usActionData));
return(AI_ACTION_RUN_AWAY);
}
}
////////////////////////////////////////////////////////////////////////////
// SOLDIER CAN ATTACK IF NOT IN WATER/GAS AND NOT DOING SOMETHING TOO FUNKY
////////////////////////////////////////////////////////////////////////////
// zombie can always attack
bCanAttack = TRUE;
// NPCs in deep water cannot attack
if ((pSoldier->bActionPoints < 2) || bInDeepWater)
{
DebugAI( AI_MSG_INFO, pSoldier, String("low AP or in deep water - cannot attack"));
bCanAttack = FALSE;
}
BestStab.ubPossible = FALSE; // by default, assume Stabbing isn't possible
// if we are able attack
if (bCanAttack)
{
DebugAI( AI_MSG_INFO, pSoldier, String("[attack]"));
pSoldier->bAimShotLocation = AIM_SHOT_RANDOM;
// nothing (else) to attack with so let's try hand-to-hand
bWeaponIn = FindObj( pSoldier, NOTHING, HANDPOS, NUM_INV_SLOTS );
if (bWeaponIn != NO_SLOT)
{
DebugAI( AI_MSG_INFO, pSoldier, String("found weapon in slot %d", bWeaponIn));
BestStab.bWeaponIn = bWeaponIn;
// if it's in his holster, swap it into his hand temporarily
if (bWeaponIn != HANDPOS)
{
DebugAI( AI_MSG_INFO, pSoldier, String("rearrange pocket temporarily"));
RearrangePocket(pSoldier,HANDPOS,bWeaponIn,TEMPORARILY);
}
// get the minimum cost to attack by HTH
ubMinAPCost = MinAPsToAttack(pSoldier,pSoldier->sLastTarget,DONTADDTURNCOST,0,0);
DebugAI( AI_MSG_INFO, pSoldier, String("min AP to attack %d", ubMinAPCost));
// if we can afford the minimum AP cost to use HTH combat
if (pSoldier->bActionPoints >= ubMinAPCost)
{
DebugAI( AI_MSG_INFO, pSoldier, String("[find target for attack]"));
// then look around for a worthy target (which sets BestStab.ubPossible)
CalcBestStab(pSoldier,&BestStab, FALSE);
if (BestStab.ubPossible && pSoldier->ubSoldierClass == SOLDIER_CLASS_ZOMBIE)
{
//ScreenMsg( FONT_LTGREEN, MSG_INTERFACE, L"[%d] can attack %s", pSoldier->ubID, MercPtrs[BestStab.ubOpponent]->GetName());
// now we KNOW FOR SURE that we will do something (stab, at least)
NPCDoesAct(pSoldier);
ubBestAttackAction = AI_ACTION_KNIFE_MOVE;
DebugAI( AI_MSG_INFO, pSoldier, String("NPC decided to move to stab"));
}
}
// if it was in his holster, swap it back into his holster for now
if (bWeaponIn != HANDPOS)
{
DebugAI( AI_MSG_INFO, pSoldier, String("rearrange pocket back"));
RearrangePocket(pSoldier,HANDPOS,bWeaponIn,TEMPORARILY);
}
}
//////////////////////////////////////////////////////////////////////////
// CHOOSE THE BEST TYPE OF ATTACK OUT OF THOSE FOUND TO BE POSSIBLE
//////////////////////////////////////////////////////////////////////////
// copy the information on the best action selected into BestAttack struct
DebugAI( AI_MSG_INFO, pSoldier, String("copy the information on the best action selected into BestAttack struct"));
switch (ubBestAttackAction)
{
case AI_ACTION_KNIFE_MOVE:
DebugAI( AI_MSG_INFO, pSoldier, String("best attack = stab"));
memcpy(&BestAttack,&BestStab,sizeof(BestAttack));
break;
default:
// set to empty
DebugAI( AI_MSG_INFO, pSoldier, String("best attack = no good attack"));
memset( &BestAttack, 0, sizeof( BestAttack ) );
break;
}
}
// if attack is still desirable (meaning it's also preferred to taking cover)
if (ubBestAttackAction != AI_ACTION_NONE)
{
DebugAI( AI_MSG_INFO, pSoldier, String("prepare attack, AP cost %d CTH %d", BestAttack.ubAPCost, BestAttack.ubChanceToReallyHit));
pSoldier->aiData.bAimTime = BestAttack.ubAimTime;
// sevenfm: dynamically decide stab location
if( BestAttack.ubOpponent != NOBODY )
{
UINT32 uiRoll;
UINT8 ubChanceHead = 0;
UINT16 usRealCTH = BestAttack.ubChanceToReallyHit;
// attack to head randomly
if( gAnimControl[ MercPtrs[BestAttack.ubOpponent]->usAnimState ].ubEndHeight != ANIM_PRONE )
{
ubChanceHead = 6;
ubChanceHead += usRealCTH / 2;
ubChanceHead = ubChanceHead * usRealCTH / 100;
}
DebugAI( AI_MSG_INFO, pSoldier, String("ubRealCTH %d", usRealCTH));
DebugAI( AI_MSG_INFO, pSoldier, String("ubChanceHead %d", ubChanceHead));
// randomly decide hit location
uiRoll = PreRandom( 100 );
if (uiRoll > 100 - ubChanceHead)
{
pSoldier->bAimShotLocation = AIM_SHOT_HEAD;
DebugAI( AI_MSG_INFO, pSoldier, String("aim at head"));
}
else
{
pSoldier->bAimShotLocation = AIM_SHOT_TORSO;
DebugAI( AI_MSG_INFO, pSoldier, String("aim at torso"));
}
}
DebugAI( AI_MSG_INFO, pSoldier, String("AimTime %d AimShotLocation %d", pSoldier->aiData.bAimTime, pSoldier->bAimShotLocation));
// swap weapon to hand if necessary
if (BestAttack.bWeaponIn != HANDPOS)
{
DebugAI( AI_MSG_INFO, pSoldier, String("rearrange pocket for attack"));
RearrangePocket(pSoldier,HANDPOS,BestAttack.bWeaponIn,FOREVER);
}
pSoldier->aiData.usActionData = BestAttack.sTarget;
pSoldier->bTargetLevel = BestAttack.bTargetLevel;
DebugAI( AI_MSG_INFO, pSoldier, String("attack opponent %d at gridno %d level %d", BestAttack.ubOpponent, pSoldier->aiData.usActionData, pSoldier->bTargetLevel));
return(ubBestAttackAction);
}
DebugAI( AI_MSG_INFO, pSoldier, String("[go to closest deisturbance]"));
if( ubCanMove &&
!gfHiddenInterrupt &&
pSoldier->bActionPoints > APBPConstants[MAX_AP_CARRIED] )
{
//sClosestDisturbance = ClosestReachableDisturbance(pSoldier, &fClimb, ubThreatID, sThreatSpot, bThreatLevel);
sClosestDisturbance = ClosestReachableDisturbance(pSoldier, &fClimb);
DebugAI( AI_MSG_INFO, pSoldier, String("found disturbance %d climb %d", sClosestDisturbance, fClimb));
// if there is an opponent reachable
if (!TileIsOutOfBounds(sClosestDisturbance))
{
//////////////////////////////////////////////////////////////////////
// SEEK CLOSEST DISTURBANCE: GO DIRECTLY TOWARDS CLOSEST KNOWN OPPONENT
//////////////////////////////////////////////////////////////////////
if (fClimb && pSoldier->sGridNo == sClosestDisturbance)
{
DebugAI( AI_MSG_INFO, pSoldier, String("at climb point"));
// wait for next turn if turnbased (to climb with max APs)
if( gfTurnBasedAI &&
pSoldier->bActionPoints < pSoldier->bInitialActionPoints )
{
pSoldier->aiData.bNextAction = AI_ACTION_NONE;
return( AI_ACTION_END_TURN );
}
// climb
if (IsActionAffordable(pSoldier, AI_ACTION_CLIMB_ROOF))
{
pSoldier->aiData.bNextAction = AI_ACTION_NONE;
return( AI_ACTION_CLIMB_ROOF );
}
// cannot climb at all
}
else
{
DebugAI( AI_MSG_INFO, pSoldier, String("try to move towards disturbance"));
// try to move towards him
pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, sClosestDisturbance, 0, AI_ACTION_SEEK_OPPONENT, 0);
DebugAI( AI_MSG_INFO, pSoldier, String("found spot %d", pSoldier->aiData.usActionData));
// if it's possible
if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))
{
// sevenfm: try to advance using sight cover
if( gfTurnBasedAI &&
PythSpacesAway(pSoldier->sGridNo, sClosestDisturbance) < DAY_VISION_RANGE * 2 &&
PythSpacesAway(pSoldier->sGridNo, sClosestDisturbance) > DAY_VISION_RANGE / 4 &&
!SightCoverAtSpot(pSoldier, pSoldier->aiData.usActionData, TRUE))
{
DebugAI( AI_MSG_INFO, pSoldier, String("[advance spot]"));
INT32 sAdvanceSpot = FindAdvanceSpot(pSoldier, sClosestDisturbance, AI_ACTION_SEEK_OPPONENT, ADVANCE_SPOT_SIGHT_COVER, TRUE);
DebugAI( AI_MSG_INFO, pSoldier, String("found advance spot %d", sAdvanceSpot));
if( !TileIsOutOfBounds(sAdvanceSpot) )
{
DebugAI( AI_MSG_INFO, pSoldier, String("advance to %d", sAdvanceSpot));
pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, sAdvanceSpot, 0, AI_ACTION_SEEK_OPPONENT, 0);
if( !TileIsOutOfBounds(pSoldier->aiData.usActionData) )
{
return(AI_ACTION_SEEK_OPPONENT);
}
}
else if( SightCoverAtSpot(pSoldier, pSoldier->sGridNo, TRUE) &&
gfTurnBasedAI &&
pSoldier->bActionPoints < pSoldier->bInitialActionPoints)
{
DebugAI( AI_MSG_INFO, pSoldier, String("cannot find advance spot, wait for next turn in sight cover"));
return AI_ACTION_NONE;
}
}
return(AI_ACTION_SEEK_OPPONENT);
}
}
}
DebugAI( AI_MSG_INFO, pSoldier, String("[hide]"));
// if cannot seek or help - hide
if( gfTurnBasedAI &&
pSoldier->bActionPoints == pSoldier->bInitialActionPoints &&
(!SightCoverAtSpot(pSoldier, pSoldier->sGridNo, TRUE) || pSoldier->aiData.bUnderFire) )
{
pSoldier->aiData.usActionData = FindBestNearbyCover(pSoldier, pSoldier->aiData.bAIMorale, &iDummy);
if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))
{
DebugAI(AI_MSG_INFO, pSoldier, String("found hide spot %d", pSoldier->aiData.usActionData));
return AI_ACTION_TAKE_COVER;
}
else if (pSoldier->aiData.bUnderFire)
{
pSoldier->aiData.usActionData = FindRetreatSpot(pSoldier);
if (!TileIsOutOfBounds(pSoldier->aiData.usActionData))
{
DebugAI(AI_MSG_INFO, pSoldier, String("found retreat spot %d", pSoldier->aiData.usActionData));
return AI_ACTION_TAKE_COVER;
}
}
}
}
////////////////////////////////////////////////////////////////////////////
// TURN TO FACE CLOSEST KNOWN OPPONENT (IF NOT FACING THERE ALREADY)
////////////////////////////////////////////////////////////////////////////
DebugAI( AI_MSG_INFO, pSoldier, String("[turn to closest opponent]"));
if( GetAPsToLook( pSoldier ) <= pSoldier->bActionPoints )
{
sClosestOpponent = ClosestSeenOpponent(pSoldier, NULL, NULL);
DebugAI( AI_MSG_INFO, pSoldier, String("closest seen opponent %d", sClosestOpponent));
// if we have a closest seen opponent
if (!TileIsOutOfBounds(sClosestOpponent))
{
bDirection = AIDirection(pSoldier->sGridNo, sClosestOpponent);
DebugAI( AI_MSG_INFO, pSoldier, String("soldier direction %d opponent direction %d", pSoldier->ubDirection, bDirection));
// if we're not facing towards him
if( pSoldier->ubDirection != bDirection &&
pSoldier->InternalIsValidStance( bDirection, gAnimControl[ pSoldier->usAnimState ].ubEndHeight ) )
{
pSoldier->aiData.usActionData = bDirection;
return(AI_ACTION_CHANGE_FACING);
}
DebugAI( AI_MSG_INFO, pSoldier, String("cannot turn"));
}
}
////////////////////////////////////////////////////////////////////////////
// DO NOTHING: Not enough points left to move, so save them for next turn
////////////////////////////////////////////////////////////////////////////
DebugAI( AI_MSG_INFO, pSoldier, String("nothing to do!"));
// by default, if everything else fails, just stand in place and wait
pSoldier->aiData.usActionData = NOWHERE;
return(AI_ACTION_NONE);
}
INT8 ZombieDecideAction( SOLDIERTYPE *pSoldier )
{
INT8 bAction = AI_ACTION_NONE;
switch (pSoldier->aiData.bAlertStatus)
{
case STATUS_GREEN:
bAction = ZombieDecideActionGreen(pSoldier);
break;
case STATUS_YELLOW:
bAction = ZombieDecideActionYellow(pSoldier);
break;
case STATUS_RED:
bAction = ZombieDecideActionRed(pSoldier);
break;
case STATUS_BLACK:
bAction = ZombieDecideActionBlack(pSoldier);
break;
}
#ifdef DEBUGDECISIONS
STR tempstr;
sprintf(tempstr, "ZombieDecideAction: selected action %d, actionData %d\n\n", bAction, pSoldier->aiData.usActionData);
DebugAI(tempstr);
#endif
return(bAction);
}
/*void ZombieDecideAlertStatus( SOLDIERTYPE *pSoldier )
{
INT8 bOldStatus;
INT32 iDummy;
BOOLEAN fClimbDummy, fReachableDummy;
// THE FOUR (4) POSSIBLE ALERT STATUSES ARE:
// GREEN - No one sensed, no suspicious noise heard, go about doing regular stuff
// YELLOW - Suspicious noise was heard personally
// RED - Either saw OPPONENTS in person, or definite contact had been called
// BLACK - Currently has one or more OPPONENTS in sight
// save the man's previous status
bOldStatus = pSoldier->aiData.bAlertStatus;
// determine the current alert status for this category of man
if (pSoldier->aiData.bOppCnt > 0) // opponent(s) in sight
{
pSoldier->aiData.bAlertStatus = STATUS_BLACK;
}
else // no opponents are in sight
{
switch (bOldStatus)
{
case STATUS_BLACK:
// then drop back to RED status
pSoldier->aiData.bAlertStatus = STATUS_RED;
break;
case STATUS_RED:
// RED can never go back down below RED, only up to BLACK
// TODO: perhaps zombies might forget their target? They are pretty dumb after all...
break;
case STATUS_YELLOW:
// if all enemies have been RED alerted, or we're under fire
if (gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition || pSoldier->aiData.bUnderFire)
{
pSoldier->aiData.bAlertStatus = STATUS_RED;
}
else
{
// if we are NOT aware of any uninvestigated noises right now
// and we are not currently in the middle of an action
// (could still be on his way heading to investigate a noise!)
if (( TileIsOutOfBounds(MostImportantNoiseHeard(pSoldier,&iDummy,&fClimbDummy,&fReachableDummy))) && !pSoldier->aiData.bActionInProgress)
{
// then drop back to GREEN status
pSoldier->aiData.bAlertStatus = STATUS_GREEN;
}
}
break;
case STATUS_GREEN:
// if all enemies have been RED alerted, or we're under fire
if (gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition || pSoldier->aiData.bUnderFire)
{
pSoldier->aiData.bAlertStatus = STATUS_RED;
}
else
{
// if we ARE aware of any uninvestigated noises right now
if ( !TileIsOutOfBounds(MostImportantNoiseHeard(pSoldier,&iDummy,&fClimbDummy,&fReachableDummy)))
{
// then move up to YELLOW status
pSoldier->aiData.bAlertStatus = STATUS_YELLOW;
}
}
break;
}
// otherwise, RED stays RED, YELLOW stays YELLOW, GREEN stays GREEN
}
// if the creatures alert status has changed in any way
if (pSoldier->aiData.bAlertStatus != bOldStatus)
{
// HERE ARE TRYING TO AVOID NPCs SHUFFLING BACK & FORTH BETWEEN RED & BLACK
// if either status is < RED (ie. anything but RED->BLACK && BLACK->RED)
if ((bOldStatus < STATUS_RED) || (pSoldier->aiData.bAlertStatus < STATUS_RED))
{
// force a NEW action decision on next pass through HandleManAI()
SetNewSituation( pSoldier );
}
// if this guy JUST discovered that there were opponents here for sure...
if ((bOldStatus < STATUS_RED) && (pSoldier->aiData.bAlertStatus >= STATUS_RED))
{
// might want to make custom to let them go anywhere
CheckForChangingOrders(pSoldier);
}
}
else // status didn't change
{
// if a guy on status GREEN or YELLOW is running low on breath
if (((pSoldier->aiData.bAlertStatus == STATUS_GREEN) && (pSoldier->bBreath < 75)) ||
((pSoldier->aiData.bAlertStatus == STATUS_YELLOW) && (pSoldier->bBreath < 50)))
{
// as long as he's not in water (standing on a bridge is OK)
if (!pSoldier->MercInWater())
{
// force a NEW decision so that he can get some rest
SetNewSituation( pSoldier );
// current action will be canceled. if noise is no longer important
if ((pSoldier->aiData.bAlertStatus == STATUS_YELLOW) &&
(TileIsOutOfBounds(MostImportantNoiseHeard(pSoldier,&iDummy,&fClimbDummy,&fReachableDummy))))
{
// then drop back to GREEN status
pSoldier->aiData.bAlertStatus = STATUS_GREEN;
CheckForChangingOrders(pSoldier);
}
}
}
}
}*/