From 32482f20f6eddba57355cfea6609d8ec71b802ff Mon Sep 17 00:00:00 2001 From: Sevenfm Date: Fri, 21 Feb 2020 23:33:25 +0000 Subject: [PATCH] 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 --- GameSettings.cpp | 1 + GameSettings.h | 1 + Standard Gaming Platform/TopicIDs.h | 1 + Tactical/Points.cpp | 3 + Tactical/Rotting Corpses.cpp | 68 +- Tactical/Rotting Corpses.h | 2 + Tactical/Soldier Control.cpp | 12 +- Tactical/Soldier Control.h | 4 + TacticalAI/AIInternals.h | 1 + TacticalAI/AIMain.cpp | 196 +++ TacticalAI/AIUtils.cpp | 940 ++++++++++-- TacticalAI/CreatureDecideAction.cpp | 5 + TacticalAI/DecideAction.cpp | 184 +-- TacticalAI/FindLocations.cpp | 647 +++++++- TacticalAI/TacticalAI_VS2010.vcxproj | 1 + TacticalAI/TacticalAI_VS2010.vcxproj.filters | 3 + TacticalAI/TacticalAI_VS2013.vcxproj | 1 + TacticalAI/TacticalAI_VS2013.vcxproj.filters | 3 + TacticalAI/TacticalAI_VS2017.vcxproj | 1 + TacticalAI/ZombieDecideAction.cpp | 1419 ++++++++++++++++++ TacticalAI/ai.h | 57 +- TileEngine/SmokeEffects.cpp | 31 + TileEngine/SmokeEffects.h | 2 + TileEngine/physics.cpp | 23 +- 24 files changed, 3296 insertions(+), 310 deletions(-) create mode 100644 TacticalAI/ZombieDecideAction.cpp diff --git a/GameSettings.cpp b/GameSettings.cpp index 6009c0ee..9d56bb1a 100644 --- a/GameSettings.cpp +++ b/GameSettings.cpp @@ -2295,6 +2295,7 @@ void LoadGameExternalOptions() gGameExternalOptions.fAITacticalRetreat = iniReader.ReadBoolean("Tactical AI Settings", "AI_TACTICAL_RETREAT", FALSE); gGameExternalOptions.fAIMovementMode = iniReader.ReadBoolean("Tactical AI Settings", "AI_MOVEMENT_MODE", TRUE); gGameExternalOptions.fAIPathTweaks = iniReader.ReadBoolean("Tactical AI Settings", "AI_PATH_TWEAKS", TRUE); + gGameExternalOptions.fAIDecisionInfo = iniReader.ReadBoolean("Tactical AI Settings", "AI_DECISION_INFO", false, false); } diff --git a/GameSettings.h b/GameSettings.h index d8e01598..65a8a71e 100644 --- a/GameSettings.h +++ b/GameSettings.h @@ -1555,6 +1555,7 @@ typedef struct BOOLEAN fAITacticalRetreat; BOOLEAN fAIMovementMode; BOOLEAN fAIPathTweaks; + BOOLEAN fAIDecisionInfo; } GAME_EXTERNAL_OPTIONS; diff --git a/Standard Gaming Platform/TopicIDs.h b/Standard Gaming Platform/TopicIDs.h index 1f5bd812..4ec01487 100644 --- a/Standard Gaming Platform/TopicIDs.h +++ b/Standard Gaming Platform/TopicIDs.h @@ -3,6 +3,7 @@ const unsigned INVALID_TOPIC = 0xffff; +const unsigned TOPIC_DECISIONS = 25; const unsigned TOPIC_MEMORY_MANAGER = 24; const unsigned TOPIC_FILE_MANAGER = 23; const unsigned TOPIC_DATABASE_MANAGER = 22; diff --git a/Tactical/Points.cpp b/Tactical/Points.cpp index c30239a0..10bbba30 100644 --- a/Tactical/Points.cpp +++ b/Tactical/Points.cpp @@ -988,6 +988,9 @@ void DeductPoints( SOLDIERTYPE *pSoldier, INT16 sAPCost, INT32 iBPCost, UINT8 ub if ( pSoldier->usSoldierFlagMask & SOLDIER_COVERT_TEMPORARY_OVERT && pSoldier->usSkillCooldown[SOLDIER_COOLDOWN_COVERTOPS_TEMPORARYOVERT_APS] > 0 ) pSoldier->usSkillCooldown[SOLDIER_COOLDOWN_COVERTOPS_TEMPORARYOVERT_APS] = max( 0, (INT16)(pSoldier->usSkillCooldown[SOLDIER_COOLDOWN_COVERTOPS_TEMPORARYOVERT_APS] - sAPCost) ); + // sevenfm: indicate in realtime that soldier spent some action point this turn + pSoldier->usSoldierFlagMask2 |= SOLDIER_SPENT_AP; + pSoldier->StopChatting(); } diff --git a/Tactical/Rotting Corpses.cpp b/Tactical/Rotting Corpses.cpp index fd026418..992ac2e7 100644 --- a/Tactical/Rotting Corpses.cpp +++ b/Tactical/Rotting Corpses.cpp @@ -60,10 +60,6 @@ class OBJECTTYPE; class SOLDIERTYPE; - -#define CORPSE_WARNING_MAX 5 -#define CORPSE_WARNING_DIST 5 - #define CORPSE_INDEX_OFFSET 10000 // Flugente: these are ini values now @@ -501,7 +497,7 @@ INT32 AddRottingCorpse( ROTTING_CORPSE_DEFINITION *pCorpseDef ) uiDirectionUseFlag = ANITILE_USE_DIRECTION_FOR_START_FRAME; - // If we are a soecial type... + // If we are a special type... switch( pCorpseDef->ubType ) { case SMERC_FALL: @@ -527,7 +523,7 @@ INT32 AddRottingCorpse( ROTTING_CORPSE_DEFINITION *pCorpseDef ) if( !(gTacticalStatus.uiFlags & LOADING_SAVED_GAME ) ) { // OK, AS WE ADD, CHECK FOR TOD AND DECAY APPROPRIATELY - if ( ((GetWorldTotalMin( ) - pCorpse->def.uiTimeOfDeath) > gGameExternalOptions.usCorpseDelayUntilRotting) && (pCorpse->def.ubType < ROTTING_STAGE2) ) + if ( ((GetWorldTotalMin( ) - pCorpse->def.uiTimeOfDeath) >= gGameExternalOptions.usCorpseDelayUntilRotting) && (pCorpse->def.ubType < ROTTING_STAGE2) ) { if ( pCorpse->def.ubType <= FMERC_FALLF ) { @@ -1693,17 +1689,17 @@ INT32 FindNearestAvailableGridNoForCorpse( ROTTING_CORPSE_DEFINITION *pDef, INT8 INT16 sTop, sBottom; INT16 sLeft, sRight; INT16 cnt1, cnt2, cnt3; - INT32 sGridNo; - INT32 uiRange, uiLowestRange = 999999; - INT32 sLowestGridNo=0; - INT32 leftmost; + INT32 sGridNo; + INT32 uiRange, uiLowestRange = 999999; + INT32 sLowestGridNo=0; + INT32 leftmost; BOOLEAN fFound = FALSE; SOLDIERTYPE soldier; - INT16 ubSaveNPCAPBudget; - UINT8 ubSaveNPCDistLimit; + INT16 ubSaveNPCAPBudget; + UINT8 ubSaveNPCDistLimit; STRUCTURE_FILE_REF * pStructureFileRef = NULL; - CHAR8 zFilename[150]; - UINT8 ubBestDirection=0; + CHAR8 zFilename[150]; + UINT8 ubBestDirection=0; BOOLEAN fSetDirection = FALSE; cnt3 = 0; @@ -1752,8 +1748,6 @@ INT32 FindNearestAvailableGridNoForCorpse( ROTTING_CORPSE_DEFINITION *pDef, INT8 //(use the fake soldier and the pathing settings) FindBestPath( &soldier, GRIDSIZE, 0, WALKING, COPYREACHABLE, 0 );//dnl ch50 071009 - uiLowestRange = 999999; - for( cnt1 = sBottom; cnt1 <= sTop; cnt1++ ) { leftmost = ( ( sSweetGridNo + ( WORLD_COLS * cnt1 ) )/ WORLD_COLS ) * WORLD_COLS; @@ -1847,12 +1841,15 @@ ROTTING_CORPSE *GetCorpseAtGridNo( INT32 sGridNo, INT8 bLevel ) // Get base.... pBaseStructure = FindBaseStructure( pStructure ); - // Find base gridno... - sBaseGridNo = pBaseStructure->sGridNo; - if ( pBaseStructure != NULL ) { - return(FindCorpseBasedOnStructure( sBaseGridNo, bLevel, pBaseStructure )); + // Find base gridno... + sBaseGridNo = pBaseStructure->sGridNo; + + if (!TileIsOutOfBounds(sBaseGridNo)) + { + return(FindCorpseBasedOnStructure(sBaseGridNo, bLevel, pBaseStructure)); + } } } @@ -2692,6 +2689,7 @@ std::vector GetCorpseIDsNearGridNo( INT32 sGridNo, INT8 bLevel, INT8 sRad } extern UNDERGROUND_SECTORINFO* FindUnderGroundSector( INT16 sMapX, INT16 sMapY, UINT8 bMapZ ); +INT16 gsZombieRaiseSoundNum = -1; // Flugente Zombies: resurrect zombies void RaiseZombies( void ) @@ -2801,8 +2799,32 @@ void RaiseZombies( void ) #ifdef JA2TESTVERSION ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_BETAVERSION, L"A wave of zombies is created"); #endif - // Play sound - PlayJA2SampleFromFile( "Sounds\\zombie1.wav", RATE_11025, HIGHVOLUME, 1, MIDDLEPAN ); + CHAR8 zFilename[512]; + // prepare zombie raise sound + if (gsZombieRaiseSoundNum < 0) + { + gsZombieRaiseSoundNum = 0; + do + { + gsZombieRaiseSoundNum++; + sprintf(zFilename, "Sounds\\Misc\\ZombieRaise%d.ogg", gsZombieRaiseSoundNum); + } while (FileExists(zFilename)); + gsZombieRaiseSoundNum--; + } + if (gsZombieRaiseSoundNum > 0) + { + sprintf(zFilename, "Sounds\\Misc\\ZombieRaise%d.ogg", Random(gsZombieRaiseSoundNum) + 1); + if (FileExists(zFilename)) + { + //PlayJA2SampleFromFile(zFilename, RATE_11025, SoundVolume(MIDVOLUME, this->sGridNo), 1, SoundDir(this->sGridNo)); + PlayJA2SampleFromFile(zFilename, RATE_11025, MIDVOLUME, 1, MIDDLEPAN); + } + } + else + { + // Play default sound + PlayJA2SampleFromFile("Sounds\\zombie1.wav", RATE_11025, HIGHVOLUME, 1, MIDDLEPAN); + } UseCreatureMusic(TRUE); // Madd: music when zombies rise #ifdef NEWMUSIC @@ -3251,5 +3273,5 @@ FLOAT GetCorpseRotFactor( ROTTING_CORPSE* pCorpse ) if ( pCorpse->def.ubType == ROTTING_STAGE2 ) return 1.0f; - return (GetWorldTotalMin( ) - pCorpse->def.uiTimeOfDeath) / gGameExternalOptions.usCorpseDelayUntilRotting; + return (FLOAT)(min(gGameExternalOptions.usCorpseDelayUntilRotting, GetWorldTotalMin() - pCorpse->def.uiTimeOfDeath)) / gGameExternalOptions.usCorpseDelayUntilRotting; } diff --git a/Tactical/Rotting Corpses.h b/Tactical/Rotting Corpses.h index 12b54d0c..79c53918 100644 --- a/Tactical/Rotting Corpses.h +++ b/Tactical/Rotting Corpses.h @@ -4,6 +4,8 @@ #include "tile animation.h" #define NUM_CORPSE_SHADES 17 +#define CORPSE_WARNING_MAX 5 +#define CORPSE_WARNING_DIST 5 enum RottingCorpseDefines { diff --git a/Tactical/Soldier Control.cpp b/Tactical/Soldier Control.cpp index daa4fefd..f710bf2b 100644 --- a/Tactical/Soldier Control.cpp +++ b/Tactical/Soldier Control.cpp @@ -16522,6 +16522,16 @@ BOOLEAN SOLDIERTYPE::IsAssassin( ) return FALSE; } +BOOLEAN SOLDIERTYPE::CheckInitialAP(void) +{ + if (this->bActionPoints < this->bInitialActionPoints || this->usSoldierFlagMask2 & SOLDIER_SPENT_AP) + { + return FALSE; + } + + return TRUE; +} + UINT8 SOLDIERTYPE::GetMultiTurnAction( ) { return usMultiTurnAction; @@ -17679,7 +17689,7 @@ void SOLDIERTYPE::SoldierPropertyUpkeep( ) { // these effects last only one turn this->usSoldierFlagMask &= ~(SOLDIER_AIRDROP_TURN | SOLDIER_ASSAULT_BONUS | SOLDIER_RAISED_REDALERT); - this->usSoldierFlagMask2 &= ~SOLDIER_CONCEALINSERTION; + this->usSoldierFlagMask2 &= ~(SOLDIER_CONCEALINSERTION | SOLDIER_SPENT_AP); // this looks bizarre, but is required if ( this->usSoldierFlagMask2 & SOLDIER_CONCEALINSERTION_DISCOVERED ) diff --git a/Tactical/Soldier Control.h b/Tactical/Soldier Control.h index 7a16ffb4..144fd173 100644 --- a/Tactical/Soldier Control.h +++ b/Tactical/Soldier Control.h @@ -425,6 +425,7 @@ enum #define SOLDIER_SURGERY_BOOSTED 0x00040000 // we are a boosted performing surgery (e.g. by using up a blood bag) #define SOLDIER_DRAG_SOUND 0x00080000 // played sound when started dragging +#define SOLDIER_SPENT_AP 0x00100000 // soldier has spent some AP this turn (including realtime) #define SOLDIER_INTERROGATE_ALL 0x000001F8 // all interrogation flags // ---------------------------------------------------------------- @@ -1854,6 +1855,9 @@ public: // Flugente: are we an assassin? BOOLEAN IsAssassin(); + // sevenfm: service functions + BOOLEAN CheckInitialAP(void); + // Flugente: multi-turn actions UINT8 GetMultiTurnAction(); void StartMultiTurnAction( UINT8 usActionType, INT32 asGridNo ); diff --git a/TacticalAI/AIInternals.h b/TacticalAI/AIInternals.h index 4af7c29a..26d9baea 100644 --- a/TacticalAI/AIInternals.h +++ b/TacticalAI/AIInternals.h @@ -232,6 +232,7 @@ INT32 GoAsFarAsPossibleTowards(SOLDIERTYPE *pSoldier, INT32 sDesGrid, INT8 bActi INT8 HeadForTheStairCase( SOLDIERTYPE * pSoldier ); +BOOLEAN InSmoke(INT32 sGridNo, INT8 bLevel); BOOLEAN InGas( SOLDIERTYPE *pSoldier, INT32 sGridNo ); BOOLEAN InGasOrSmoke( SOLDIERTYPE *pSoldier, INT32 sGridNo ); BOOLEAN InWaterGasOrSmoke( SOLDIERTYPE *pSoldier, INT32 sGridNo ); diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index 85203d56..66c7800b 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -134,6 +134,191 @@ void DebugAI( STR szOutput ) #endif } +STR szAction[] = { + "AI_ACTION_NONE", + + "AI_ACTION_RANDOM_PATROL", + "AI_ACTION_SEEK_FRIEND", + "AI_ACTION_SEEK_OPPONENT", + "AI_ACTION_TAKE_COVER", + "AI_ACTION_GET_CLOSER", + + "AI_ACTION_POINT_PATROL", + "AI_ACTION_LEAVE_WATER_GAS", + "AI_ACTION_SEEK_NOISE", + "AI_ACTION_ESCORTED_MOVE", + "AI_ACTION_RUN_AWAY", + + "AI_ACTION_KNIFE_MOVE", + "AI_ACTION_APPROACH_MERC", + "AI_ACTION_TRACK", + "AI_ACTION_EAT", + "AI_ACTION_PICKUP_ITEM", + + "AI_ACTION_SCHEDULE_MOVE", + "AI_ACTION_WALK", + "AI_ACTION_RUN", + "AI_ACTION_WITHDRAW", + "AI_ACTION_FLANK_LEFT", + "AI_ACTION_FLANK_RIGHT", + "AI_ACTION_MOVE_TO_CLIMB", + + "AI_ACTION_CHANGE_FACING", + + "AI_ACTION_CHANGE_STANCE", + + "AI_ACTION_YELLOW_ALERT", + "AI_ACTION_RED_ALERT", + "AI_ACTION_CREATURE_CALL", + "AI_ACTION_PULL_TRIGGER", + + "AI_ACTION_USE_DETONATOR", + "AI_ACTION_FIRE_GUN", + "AI_ACTION_TOSS_PROJECTILE", + "AI_ACTION_KNIFE_STAB", + "AI_ACTION_THROW_KNIFE", + + "AI_ACTION_GIVE_AID", + "AI_ACTION_WAIT", + "AI_ACTION_PENDING_ACTION", + "AI_ACTION_DROP_ITEM", + "AI_ACTION_COWER", + + "AI_ACTION_STOP_COWERING", + "AI_ACTION_OPEN_OR_CLOSE_DOOR", + "AI_ACTION_UNLOCK_DOOR", + "AI_ACTION_LOCK_DOOR", + "AI_ACTION_LOWER_GUN", + + "AI_ACTION_ABSOLUTELY_NONE", + "AI_ACTION_CLIMB_ROOF", + "AI_ACTION_END_TURN", + "AI_ACTION_END_COWER_AND_MOVE", + "AI_ACTION_TRAVERSE_DOWN", + "AI_ACTION_OFFER_SURRENDER", + "AI_ACTION_RAISE_GUN", + "AI_ACTION_STEAL_MOVE", + + "AI_ACTION_RELOAD_GUN", + + "AI_ACTION_JUMP_WINDOW", + "AI_ACTION_FREE_PRISONER", + "AI_ACTION_USE_SKILL", + "AI_ACTION_DOCTOR", + "AI_ACTION_DOCTOR_SELF", + "AI_ACTION_SELFDETONATE", + "AI_ACTION_STOP_MEDIC" +}; + +// sevenfm +UINT32 guiAIStartCounter = 0, guiAILastCounter = 0; +//UINT8 gubAISelectedSoldier = NOBODY; + +void DebugAI( INT8 bMsgType, SOLDIERTYPE *pSoldier, STR szOutput, INT8 bAction ) +{ + FILE* DebugFile; + CHAR8 msg[1024]; + CHAR8 buf[1024]; + + if (!gGameExternalOptions.fAIDecisionInfo) + { + return; + } + + memset(buf, 0, 1024 * sizeof(char)); + + /*if (bMsgType == AI_MSG_TOPIC && !gGameExternalOptions.fDecisionTopicInfo) + { + return; + }*/ + + if (bMsgType == AI_MSG_START) + { + guiAIStartCounter = GetJA2Clock(); + guiAILastCounter = GetJA2Clock(); + } + + sprintf(msg, ""); + + if (pSoldier) + { + sprintf(buf, "[%d] (%d)", pSoldier->ubID, pSoldier->sGridNo); + strcat(msg, buf); + + if (pSoldier->ubProfile != NO_PROFILE) + { + wcstombs(buf, pSoldier->GetName(), 1024 - 1); + strcat(msg, " "); + strcat(msg, buf); + } + } + + strcat(msg, " "); + + if (bAction >= 0) + { + sprintf(buf, " (total %d ms)", GetJA2Clock() - guiAIStartCounter); + strcat(msg, buf); + } + else + { + sprintf(buf, " (%d ms)", GetJA2Clock() - guiAILastCounter); + strcat(msg, buf); + } + + if (bAction >= AI_ACTION_NONE && bAction <= AI_ACTION_LAST) + { + strcat(msg, " "); + strcat(msg, szAction[bAction]); + + if (pSoldier) + { + sprintf(buf, " %d", pSoldier->aiData.usActionData); + strcat(msg, buf); + } + + if (pSoldier && pSoldier->aiData.bNextAction != AI_ACTION_NONE) + { + strcat(msg, " "); + strcat(msg, szAction[pSoldier->aiData.bNextAction]); + + sprintf(buf, " %d", pSoldier->aiData.usNextActionData); + strcat(msg, buf); + } + } + + strcat(msg, " "); + strcat(msg, szOutput); + + guiAILastCounter = GetJA2Clock(); + + DebugMsg(TOPIC_DECISIONS, DBG_LEVEL_3, szOutput); + + if ((DebugFile = fopen("Logs\\AI_Decisions.txt", "a+t")) != NULL) + { + if (bMsgType == AI_MSG_START) + { + fputs("\n", DebugFile); + } + fputs(msg, DebugFile); + fputs("\n", DebugFile); + fclose(DebugFile); + } + + // also log to individual file for selected soldier + sprintf(buf, "Logs\\AI_Decisions [%d].txt", pSoldier->ubID); + if ((DebugFile = fopen(buf, "a+t")) != NULL) + { + if (bMsgType == AI_MSG_START) + { + fputs("\n", DebugFile); + } + fputs(msg, DebugFile); + fputs("\n", DebugFile); + fclose(DebugFile); + } +} + BOOLEAN InitAI( void ) { @@ -164,6 +349,17 @@ BOOLEAN InitAI( void ) } #endif + // sevenfm: Clear the AI debug txt file to prevent it from getting huge + remove("Logs\\AI_Decisions.txt"); + + // remove all individual files + CHAR8 buf[1024]; + for (UINT8 cnt = 0; cnt < TOTAL_SOLDIERS; cnt++) + { + sprintf(buf, "Logs\\AI_Decisions [%d].txt", cnt); + remove(buf); + } + return( TRUE ); } diff --git a/TacticalAI/AIUtils.cpp b/TacticalAI/AIUtils.cpp index 82e3ee4b..ebc1bd5f 100644 --- a/TacticalAI/AIUtils.cpp +++ b/TacticalAI/AIUtils.cpp @@ -839,7 +839,7 @@ INT16 RandomFriendWithin(SOLDIERTYPE *pSoldier) UINT8 ubDirection; UINT8 ubDirsLeft; BOOLEAN fDirChecked[8]; - BOOLEAN fRangeRestricted = FALSE, fFound = FALSE; + BOOLEAN fFound = FALSE; INT32 usDest, usOrigin; SOLDIERTYPE * pFriend; @@ -847,19 +847,8 @@ INT16 RandomFriendWithin(SOLDIERTYPE *pSoldier) // obtain maximum roaming distance from soldier's origin usMaxDist = RoamingRange(pSoldier,&usOrigin); - // if our movement range is restricted - - // CJC: since RandomFriendWithin is only used in non-combat, ALWAYS restrict range. - fRangeRestricted = TRUE; - /* - if (usMaxDist < MAX_ROAMING_RANGE) - { - fRangeRestricted = TRUE; - } - */ - // if range is restricted, make sure origin is a legal gridno! - if (fRangeRestricted && ((usOrigin < 0) || (usOrigin >= GRIDSIZE))) + if (usOrigin < 0 || usOrigin >= GRIDSIZE) { #ifdef BETAVERSION NameMessage(pSoldier,"has illegal origin, but his roaming range is restricted!",1000); @@ -902,7 +891,6 @@ INT16 RandomFriendWithin(SOLDIERTYPE *pSoldier) } } - while (ubFriendCount && !fFound) { // randomly select one of the remaining friends in the list @@ -910,8 +898,7 @@ INT16 RandomFriendWithin(SOLDIERTYPE *pSoldier) // if our movement range is NOT restricted, or this friend's within range // use distance - 1, because there must be at least 1 tile 1 space closer - if (!fRangeRestricted || - (SpacesAway(usOrigin,Menptr[ubFriendID].sGridNo) - 1) <= usMaxDist) + if (SpacesAway(usOrigin,Menptr[ubFriendID].sGridNo) - 1 <= usMaxDist) { // should be close enough, try to find a legal->pathing.sDestination within 1 tile @@ -1208,12 +1195,12 @@ INT32 ClosestReachableDisturbance(SOLDIERTYPE *pSoldier, BOOLEAN * pfChangeLevel // this is possible if get here from BLACK AI in one of those rare // instances when we couldn't get a meaningful shot off at a guy in sight - if ((*pbPersOL == SEEN_CURRENTLY) && (pOpponent->stats.bLife >= OKLIFE)) + /*if ((*pbPersOL == SEEN_CURRENTLY) && (pOpponent->stats.bLife >= OKLIFE)) { // don't allow this to return any valid values, this guy remains a // serious threat and the last thing we want to do is approach him! return(NOWHERE); - } + }*/ // if personal knowledge is more up to date or at least equal if ((gubKnowledgeValue[*pbPublOL - OLDEST_HEARD_VALUE][*pbPersOL - OLDEST_HEARD_VALUE] > 0) || @@ -1242,6 +1229,18 @@ INT32 ClosestReachableDisturbance(SOLDIERTYPE *pSoldier, BOOLEAN * pfChangeLevel continue; } + // sevenfm: if soldier is zombie and he cannot climb, skip location + if (pSoldier->IsZombie() && pSoldier->pathing.bLevel != bLevel && !gGameExternalOptions.fZombieCanClimb) + { + continue; + } + + // sevenfm: zombies do not attack vehicles + if (pSoldier->IsZombie() && (ARMED_VEHICLE(pOpponent) || (pOpponent->flags.uiStatusFlags & SOLDIER_VEHICLE))) + { + continue; + } + // sevenfm: when in deep water, skip opponents in deep water if (DeepWater(pSoldier->sGridNo, pSoldier->pathing.bLevel) && DeepWater(sGridNo, bLevel)) { @@ -1256,6 +1255,7 @@ INT32 ClosestReachableDisturbance(SOLDIERTYPE *pSoldier, BOOLEAN * pfChangeLevel // if we can get there and it's first reachable enemy or closer than other known enemies if (iPathCost != 0 && + (!pClosestOpponent || pClosestOpponent->stats.bLife < OKLIFE || pOpponent->stats.bLife >= OKLIFE) && (TileIsOutOfBounds(sClosestDisturbance) || iPathCost < iShortestPath || pClosestOpponent && !pClosestOpponent->IsZombie() && pClosestOpponent->stats.bLife < OKLIFE && pOpponent->stats.bLife >= OKLIFE)) @@ -1474,7 +1474,7 @@ INT32 ClosestKnownOpponent(SOLDIERTYPE *pSoldier, INT32 * psGridNo, INT8 * pbLev if (sClosestOpponent == NOWHERE || iRange < iClosestRange || - pClosestOpponent && !pClosestOpponent->IsZombie() && pClosestOpponent->stats.bLife < OKLIFE && pOpponent->stats.bLife >= OKLIFE) + pClosestOpponent && !pClosestOpponent->IsZombie() && !(pSoldier->flags.uiStatusFlags & SOLDIER_BOXER) && pClosestOpponent->stats.bLife < OKLIFE && pOpponent->stats.bLife >= OKLIFE) { iClosestRange = iRange; sClosestOpponent = sGridNo; @@ -1942,6 +1942,12 @@ INT16 EstimatePathCostToLocation( SOLDIERTYPE * pSoldier, INT32 sDestGridNo, INT } else { + // sevenfm: check if zombie cannot climb + if (pSoldier->IsZombie() && !gGameExternalOptions.fZombieCanClimb) + { + return 0; + } + // different levels if (pSoldier->pathing.bLevel == 0) { @@ -2048,6 +2054,8 @@ INT32 ClosestReachableFriendInTrouble(SOLDIERTYPE *pSoldier, BOOLEAN * pfClimbin INT32 sPathCost, sClosestFriend = NOWHERE, sShortestPath = 1000, sClimbGridNo; BOOLEAN fClimbingNecessary, fClosestClimbingNecessary = FALSE; SOLDIERTYPE *pFriend; + INT32 sClosestKnownOpponent; + BOOLEAN fCallHelp; // civilians don't really have any "friends", so they don't bother with this if (PTR_CIVILIAN) @@ -2078,10 +2086,24 @@ INT32 ClosestReachableFriendInTrouble(SOLDIERTYPE *pSoldier, BOOLEAN * pfClimbin continue; // next merc } - // CJC: restrict "last one to radio" to only if that guy saw us this turn or last turn + sClosestKnownOpponent = ClosestKnownOpponent(pFriend, NULL, NULL); - // if this friend is not under fire, and isn't the last one to radio - if ( !(pFriend->aiData.bUnderFire || (pFriend->ubID == gTacticalStatus.Team[pFriend->bTeam].ubLastMercToRadio && GuySawEnemy( pFriend ))) ) + // CJC: restrict "last one to radio" to only if that guy saw us this turn or last turn + fCallHelp = FALSE; + + // if this friend is under fire or he called for help recently + if (pFriend->aiData.bUnderFire || (pFriend->ubID == gTacticalStatus.Team[pFriend->bTeam].ubLastMercToRadio && GuySawEnemy(pFriend))) + { + fCallHelp = TRUE; + } + + // zombies always call for help if they know enemy position + if (pSoldier->IsZombie() && pFriend->IsZombie() && !TileIsOutOfBounds(sClosestKnownOpponent)) + { + fCallHelp = TRUE; + } + + if (!fCallHelp) { continue; // next merc } @@ -2744,15 +2766,20 @@ INT32 CalcManThreatValue( SOLDIERTYPE *pEnemy, INT32 sMyGrid, UINT8 ubReduceForC INT16 RoamingRange(SOLDIERTYPE *pSoldier, INT32 * pusFromGridNo) { BOOL OppPosKnown = FALSE; - if ( CREATURE_OR_BLOODCAT( pSoldier ) ) + if (CREATURE_OR_BLOODCAT(pSoldier)) { - if ( pSoldier->aiData.bAlertStatus == STATUS_BLACK ) + if (pSoldier->aiData.bAlertStatus == STATUS_BLACK) { *pusFromGridNo = pSoldier->sGridNo; // from current position! return(MAX_ROAMING_RANGE); } } - if ( pSoldier->aiData.bOrders == POINTPATROL || pSoldier->aiData.bOrders == RNDPTPATROL ) + // sevenfm: no limits for zombies + if (pSoldier->IsZombie()) + { + return(MAX_ROAMING_RANGE); + } + if (pSoldier->aiData.bOrders == POINTPATROL || pSoldier->aiData.bOrders == RNDPTPATROL) { // roam near NEXT PATROL POINT, not from where merc starts out *pusFromGridNo = pSoldier->aiData.sPatrolGrid[pSoldier->aiData.bNextPatrolPnt]; @@ -2765,121 +2792,123 @@ INT16 RoamingRange(SOLDIERTYPE *pSoldier, INT32 * pusFromGridNo) } //Do we know about any opponent? - for(UINT16 oppID = 0; oppID < MAX_NUM_SOLDIERS; oppID++) + for (UINT16 oppID = 0; oppID < MAX_NUM_SOLDIERS; oppID++) { - if ( pSoldier->aiData.bOppList[oppID] != NOT_HEARD_OR_SEEN && gbPublicOpplist[pSoldier->bTeam][oppID] != NOT_HEARD_OR_SEEN) + if (pSoldier->aiData.bOppList[oppID] != NOT_HEARD_OR_SEEN && gbPublicOpplist[pSoldier->bTeam][oppID] != NOT_HEARD_OR_SEEN) { OppPosKnown = TRUE; break; } } - //TODO: Externalize if people want? - BOOL fLessRestrictiveRoaming = TRUE; - switch (pSoldier->aiData.bOrders) { // JA2 GOLD: give non-NPCs a 5 tile roam range for cover in combat when being shot at // anv: and tanks who are technically NPCs - case STATIONARY: - if ( (pSoldier->ubProfile != NO_PROFILE && !ARMED_VEHICLE( pSoldier )) || (pSoldier->aiData.bAlertStatus < STATUS_BLACK && !(pSoldier->aiData.bUnderFire)) ) + case STATIONARY: + if ((pSoldier->ubProfile != NO_PROFILE && !ARMED_VEHICLE(pSoldier)) || (pSoldier->aiData.bAlertStatus < STATUS_BLACK && !(pSoldier->aiData.bUnderFire))) + { + return(0); + } + else + { + return(5); + } + case ONGUARD: + return(5); + case CLOSEPATROL: + if (pSoldier->aiData.bAlertStatus < STATUS_RED) + { + return(5); + } + else + { + if (!OppPosKnown) { - return( 0 ); + return(15); } else { - return( 5 ); - } - case ONGUARD: - return( 5 ); - case CLOSEPATROL: if (pSoldier->aiData.bAlertStatus < STATUS_RED) - { - return( 5 ); - } - else - { - if(!OppPosKnown || !fLessRestrictiveRoaming) - { - return( 15 ); - } - else - { - return( 30 ); - //return( MAX_ROAMING_RANGE ); - } - } - case POINTPATROL: if (pSoldier->aiData.bAlertStatus < STATUS_RED) - { - return( 10 ); - } - else - { - if(!OppPosKnown || !fLessRestrictiveRoaming) - { - return( 20 ); - } - else - { - return( 40 ); - //return( MAX_ROAMING_RANGE ); - } - } // from nextPatrolGrid, not whereIWas - case RNDPTPATROL: if (pSoldier->aiData.bAlertStatus < STATUS_RED) - { - return( 10 ); - } - else - { - if(!OppPosKnown || !fLessRestrictiveRoaming) - { - return( 20 ); - } - else - { - //return( 40 ); - return( MAX_ROAMING_RANGE ); - } - }// from nextPatrolGrid, not whereIWas - case FARPATROL: if (pSoldier->aiData.bAlertStatus < STATUS_RED) - { - return( 15 ); - } - else - { - if(!OppPosKnown || !fLessRestrictiveRoaming) - { - return( 30 ); - } - else - { - return( MAX_ROAMING_RANGE ); - } - } - case ONCALL: if (pSoldier->aiData.bAlertStatus < STATUS_RED) - { - return( 10 ); - } - else - { - if(!OppPosKnown || !fLessRestrictiveRoaming) - { - return( 30 ); - } - else - { - //return(50); - return( MAX_ROAMING_RANGE ); - } - } - case SEEKENEMY: *pusFromGridNo = pSoldier->sGridNo; // from current position! - return(MAX_ROAMING_RANGE); - case SNIPER: return ( 5 ); - default: + return(30); + //return( MAX_ROAMING_RANGE ); + } + } + case POINTPATROL: + if (pSoldier->aiData.bAlertStatus < STATUS_RED) + { + return(10); + } + else + { + if (!OppPosKnown) + { + return(20); + } + else + { + return(40); + //return( MAX_ROAMING_RANGE ); + } + } // from nextPatrolGrid, not whereIWas + case RNDPTPATROL: + if (pSoldier->aiData.bAlertStatus < STATUS_RED) + { + return(10); + } + else + { + if (!OppPosKnown) + { + return(20); + } + else + { + //return( 40 ); + return(MAX_ROAMING_RANGE); + } + }// from nextPatrolGrid, not whereIWas + case FARPATROL: + if (pSoldier->aiData.bAlertStatus < STATUS_RED) + { + return(15); + } + else + { + if (!OppPosKnown) + { + return(30); + } + else + { + return(MAX_ROAMING_RANGE); + } + } + case ONCALL: + if (pSoldier->aiData.bAlertStatus < STATUS_RED) + { + return(10); + } + else + { + if (!OppPosKnown) + { + return(30); + } + else + { + //return(50); + return(MAX_ROAMING_RANGE); + } + } + case SEEKENEMY: *pusFromGridNo = pSoldier->sGridNo; // from current position! + return(MAX_ROAMING_RANGE); + case SNIPER: return ( 5 ); + default: #ifdef BETAVERSION - sprintf(tempstr,"%s has invalid orders = %d",pSoldier->GetName(),pSoldier->aiData.bOrders); - PopMessage(tempstr); + sprintf(tempstr, "%s has invalid orders = %d", pSoldier->GetName(), pSoldier->aiData.bOrders); + PopMessage(tempstr); #endif - return(0); + return(0); } } @@ -4021,7 +4050,7 @@ UINT8 CountFriendsBlack( SOLDIERTYPE *pSoldier, INT32 sClosestOpponent ) } // check if we have a prone sight cover from known enemies at spot -BOOLEAN ProneSightCoverAtSpot( SOLDIERTYPE *pSoldier, INT32 sSpot ) +BOOLEAN ProneSightCoverAtSpot(SOLDIERTYPE *pSoldier, INT32 sSpot, BOOLEAN fUnlimited) { CHECKF(pSoldier); @@ -4036,6 +4065,8 @@ BOOLEAN ProneSightCoverAtSpot( SOLDIERTYPE *pSoldier, INT32 sSpot ) //INT32 iThreatCertainty; INT8 iThreatLevel; + INT32 iDistanceVisible; + // look through all opponents for those we know of for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++) { @@ -4082,7 +4113,24 @@ BOOLEAN ProneSightCoverAtSpot( SOLDIERTYPE *pSoldier, INT32 sSpot ) //iThreatCertainty = ThreatPercent[*pbPublOL - OLDEST_HEARD_VALUE]; } - if( LocationToLocationLineOfSightTest( sThreatLoc, iThreatLevel, sSpot, pSoldier->pathing.bLevel, TRUE, CALC_FROM_ALL_DIRS, PRONE_LOS_POS, PRONE_LOS_POS) ) + if (TileIsOutOfBounds(sThreatLoc)) + { + continue; + } + + if (fUnlimited) + { + iDistanceVisible = NO_DISTANCE_LIMIT; + } + else + { + gbForceWeaponReady = true; + iDistanceVisible = DistanceVisible(pSoldier, DIRECTION_IRRELEVANT, DIRECTION_IRRELEVANT, sSpot, pSoldier->pathing.bLevel, CoweringShockLevel(pSoldier), GetPercentTunnelVision(pSoldier)); + gbForceWeaponReady = false; + //iDistanceVisible = pSoldier->GetMaxDistanceVisible(sSpot, pSoldier->pathing.bLevel, CALC_FROM_WANTED_DIR); + } + + if (LocationToLocationLineOfSightTest(sThreatLoc, iThreatLevel, sSpot, pSoldier->pathing.bLevel, TRUE, iDistanceVisible, STANDING_LOS_POS, PRONE_LOS_POS)) { return FALSE; } @@ -4092,7 +4140,7 @@ BOOLEAN ProneSightCoverAtSpot( SOLDIERTYPE *pSoldier, INT32 sSpot ) } // check if we have a sight cover from known enemies at spot -BOOLEAN SightCoverAtSpot( SOLDIERTYPE *pSoldier, INT32 sSpot ) +BOOLEAN SightCoverAtSpot(SOLDIERTYPE *pSoldier, INT32 sSpot, BOOLEAN fUnlimited) { CHECKF(pSoldier); @@ -4107,6 +4155,8 @@ BOOLEAN SightCoverAtSpot( SOLDIERTYPE *pSoldier, INT32 sSpot ) //INT32 iThreatCertainty; INT8 iThreatLevel; + INT32 iDistanceVisible; + // look through all opponents for those we know of for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++) { @@ -4153,7 +4203,24 @@ BOOLEAN SightCoverAtSpot( SOLDIERTYPE *pSoldier, INT32 sSpot ) //iThreatCertainty = ThreatPercent[*pbPublOL - OLDEST_HEARD_VALUE]; } - if( LocationToLocationLineOfSightTest( sThreatLoc, iThreatLevel, sSpot, pSoldier->pathing.bLevel, TRUE, CALC_FROM_ALL_DIRS) ) + if (TileIsOutOfBounds(sThreatLoc)) + { + continue; + } + + if(fUnlimited) + { + iDistanceVisible = NO_DISTANCE_LIMIT; + } + else + { + gbForceWeaponReady = true; + iDistanceVisible = DistanceVisible(pSoldier, DIRECTION_IRRELEVANT, DIRECTION_IRRELEVANT, sSpot, pSoldier->pathing.bLevel, CoweringShockLevel(pSoldier), GetPercentTunnelVision(pSoldier)); + gbForceWeaponReady = false; + //iDistanceVisible = pSoldier->GetMaxDistanceVisible(sSpot, pSoldier->pathing.bLevel, CALC_FROM_WANTED_DIR); + } + + if (LocationToLocationLineOfSightTest(sThreatLoc, iThreatLevel, sSpot, pSoldier->pathing.bLevel, TRUE, iDistanceVisible)) { return FALSE; } @@ -4801,3 +4868,622 @@ BOOLEAN AllowDeepWaterFlanking(SOLDIERTYPE *pSoldier) return FALSE; } + +UINT8 AIDirection(INT32 sSpot1, INT32 sSpot2) +{ + if (TileIsOutOfBounds(sSpot1) || TileIsOutOfBounds(sSpot2)) + { + return DIRECTION_IRRELEVANT; + } + + return atan8(CenterX(sSpot1), CenterY(sSpot1), CenterX(sSpot2), CenterY(sSpot2)); +} + +BOOLEAN AICheckIsSniper(SOLDIERTYPE *pSoldier) +{ + CHECKF(pSoldier); + + if (!SoldierAI(pSoldier)) + { + return FALSE; + } + + /*if( AIGunType(pSoldier) != GUN_SN_RIFLE ) + { + return FALSE; + }*/ + + if ((AIGunRange(pSoldier) > DAY_VISION_RANGE || AICheckHasWeaponOfType(pSoldier, GUN_SN_RIFLE)) && + AIGunScoped(pSoldier) && + pSoldier->stats.bMarksmanship > 90 && + gGameOptions.fNewTraitSystem && + HAS_SKILL_TRAIT(pSoldier, SNIPER_NT)) + { + return TRUE; + } + + return FALSE; +} + +BOOLEAN AICheckIsMarksman(SOLDIERTYPE *pSoldier) +{ + CHECKF(pSoldier); + + if (!SoldierAI(pSoldier)) + { + return FALSE; + } + + if (!AICheckHasGun(pSoldier)) + { + return FALSE; + } + + if (AIGunType(pSoldier) != GUN_SN_RIFLE && + AIGunType(pSoldier) != GUN_RIFLE && + AIGunType(pSoldier) != GUN_AS_RIFLE) + { + return FALSE; + } + + if (AIGunRange(pSoldier) >= DAY_VISION_RANGE && + (AIGunScoped(pSoldier) || pSoldier->stats.bMarksmanship > 90 || gGameOptions.fNewTraitSystem && HAS_SKILL_TRAIT(pSoldier, SNIPER_NT))) + { + return TRUE; + } + + return FALSE; +} + +BOOLEAN AICheckIsRadioOperator(SOLDIERTYPE *pSoldier) +{ + CHECKF(pSoldier); + + if (!SoldierAI(pSoldier)) + { + return FALSE; + } + + if (pSoldier->CanUseRadio(FALSE)) + return TRUE; + + return FALSE; +} + +BOOLEAN AICheckIsMedic(SOLDIERTYPE *pSoldier) +{ + CHECKF(pSoldier); + + if (!SoldierAI(pSoldier)) + { + return FALSE; + } + + if (pSoldier->CanMedicAI() && gGameOptions.fNewTraitSystem && HAS_SKILL_TRAIT(pSoldier, DOCTOR_NT)) + { + return TRUE; + } + + /*if( !HAS_SKILL_TRAIT( pSoldier, DOCTOR_NT) ) + { + return FALSE; + } + + if( FindFirstAidKit( pSoldier ) != NO_SLOT || + FindMedKit( pSoldier ) != NO_SLOT ) + { + return TRUE; + }*/ + + return FALSE; +} + +BOOLEAN AICheckIsMortarOperator(SOLDIERTYPE *pSoldier) +{ + CHECKF(pSoldier); + + if (!SoldierAI(pSoldier)) + { + return FALSE; + } + + if (pSoldier->HasMortar()) + { + return TRUE; + } + + return FALSE; +} + +BOOLEAN AICheckIsGLOperator(SOLDIERTYPE *pSoldier) +{ + CHECKF(pSoldier); + + if (!SoldierAI(pSoldier)) + { + return FALSE; + } + + // find GL + INT8 bWeaponIn = FindAIUsableObjClass(pSoldier, IC_LAUNCHER); + if (bWeaponIn != NO_SLOT && + (EnoughAmmo(pSoldier, FALSE, bWeaponIn) || FindAmmoToReload(pSoldier, bWeaponIn, NO_SLOT) != NO_SLOT)) + { + return TRUE; + } + + // check for attached GL + INT8 bGunSlot = FindAIUsableObjClass(pSoldier, IC_GUN); + INT8 bRealWeaponMode = pSoldier->bWeaponMode; + pSoldier->bWeaponMode = WM_ATTACHED_GL; // So that EnoughAmmo will check for a grenade not a bullet + if (bGunSlot != NO_SLOT && + IsGrenadeLauncherAttached(&pSoldier->inv[bGunSlot]) && + (EnoughAmmo(pSoldier, FALSE, bGunSlot) || FindAmmoToReload(pSoldier, bGunSlot, NO_SLOT) != NO_SLOT)) + { + pSoldier->bWeaponMode = bRealWeaponMode; + return TRUE; + } + pSoldier->bWeaponMode = bRealWeaponMode; + + return FALSE; +} + +BOOLEAN AICheckIsOfficer(SOLDIERTYPE *pSoldier) +{ + CHECKF(pSoldier); + + if (!SoldierAI(pSoldier)) + { + return FALSE; + } + + if (gGameOptions.fNewTraitSystem && HAS_SKILL_TRAIT(pSoldier, SQUADLEADER_NT)) + { + return TRUE; + } + + return FALSE; +} + +BOOLEAN AICheckIsCommander(SOLDIERTYPE *pSoldier) +{ + CHECKF(pSoldier); + + if (!SoldierAI(pSoldier)) + { + return FALSE; + } + + if (gGameOptions.fNewTraitSystem && NUM_SKILL_TRAITS(pSoldier, SQUADLEADER_NT) > 1) + { + return TRUE; + } + + return FALSE; +} + +BOOLEAN AICheckIsMachinegunner(SOLDIERTYPE *pSoldier) +{ + CHECKF(pSoldier); + + if (!SoldierAI(pSoldier)) + { + return FALSE; + } + + if (AIGunType(pSoldier) == GUN_LMG) + { + return TRUE; + } + return FALSE; +} + +BOOLEAN AIGunInHandScoped(SOLDIERTYPE *pSoldier) +{ + CHECKF(pSoldier); + + if (UsingNewCTHSystem() == false && IsScoped(&pSoldier->inv[HANDPOS])) + { + return TRUE; + } + + if (UsingNewCTHSystem() == true && NCTHIsScoped(&pSoldier->inv[HANDPOS])) + { + return TRUE; + } + return FALSE; +} + +// check if gun that AI can use is scoped +BOOLEAN AIGunScoped(SOLDIERTYPE *pSoldier) +{ + CHECKF(pSoldier); + + INT8 bWeaponIn; + + bWeaponIn = FindAIUsableObjClass(pSoldier, IC_GUN); + + if (bWeaponIn == NO_SLOT) + { + return FALSE; + } + + if (pSoldier->inv[bWeaponIn].exists()) + { + if (UsingNewCTHSystem()) + { + return NCTHIsScoped(&pSoldier->inv[bWeaponIn]); + } + else + { + return IsScoped(&pSoldier->inv[bWeaponIn]); + } + } + + return FALSE; +} + +// return range for the AI gun in tiles +UINT16 AIGunRange(SOLDIERTYPE *pSoldier) +{ + CHECKF(pSoldier); + + INT8 bWeaponIn; + + bWeaponIn = FindAIUsableObjClass(pSoldier, IC_GUN); + + if (bWeaponIn == NO_SLOT) + { + return 0; + } + + if (pSoldier->inv[bWeaponIn].exists()) + { + return GunRange(&pSoldier->inv[bWeaponIn], pSoldier) / CELL_X_SIZE; + } + return 0; +} + +UINT16 AIGunClass(SOLDIERTYPE *pSoldier) +{ + CHECKF(pSoldier); + + INT8 bWeaponIn; + + bWeaponIn = FindAIUsableObjClass(pSoldier, IC_GUN); + + if (bWeaponIn == NO_SLOT) + { + return 0; + } + + if (pSoldier->inv[bWeaponIn].exists()) + { + return Weapon[pSoldier->inv[bWeaponIn].usItem].ubWeaponClass; + } + return 0; +} + +UINT16 AIGunType(SOLDIERTYPE *pSoldier) +{ + CHECKF(pSoldier); + + INT8 bWeaponIn; + + bWeaponIn = FindAIUsableObjClass(pSoldier, IC_GUN); + + if (bWeaponIn == NO_SLOT) + { + return 0; + } + + if (pSoldier->inv[bWeaponIn].exists()) + { + return Weapon[pSoldier->inv[bWeaponIn].usItem].ubWeaponType; + } + return 0; +} + +UINT8 AIGunDeadliness(SOLDIERTYPE *pSoldier) +{ + CHECKF(pSoldier); + + INT8 bWeaponIn; + + bWeaponIn = FindAIUsableObjClass(pSoldier, IC_GUN); + + if (bWeaponIn == NO_SLOT) + { + return 0; + } + + if (pSoldier->inv[bWeaponIn].exists()) + { + return Weapon[pSoldier->inv[bWeaponIn].usItem].ubDeadliness; + } + + return 0; +} + +UINT16 AIGunAmmo(SOLDIERTYPE *pSoldier) +{ + CHECKF(pSoldier); + + INT8 bWeaponIn = FindAIUsableObjClass(pSoldier, IC_GUN); + + if (bWeaponIn == NO_SLOT) + { + return 0; + } + + if (pSoldier->inv[bWeaponIn].exists()) + { + return pSoldier->inv[bWeaponIn][0]->data.gun.ubGunShotsLeft; + } + + return 0; +} + +BOOLEAN AIGunAutofireCapable(SOLDIERTYPE *pSoldier) +{ + CHECKF(pSoldier); + + INT8 bWeaponIn = FindAIUsableObjClass(pSoldier, IC_GUN); + + if (bWeaponIn == NO_SLOT) + { + return FALSE; + } + + if (pSoldier->inv[bWeaponIn].exists()) + { + return IsGunAutofireCapable(&pSoldier->inv[bWeaponIn]); + } + + return FALSE; +} + +BOOLEAN FindObstacleNearSpot(INT32 sSpot, INT8 bLevel) +{ + if (TileIsOutOfBounds(sSpot)) + { + return FALSE; + } + /*if( !IsLocationSittableExcludingPeople(sSpot, bLevel) ) + { + return FALSE; + }*/ + + UINT8 ubMovementCost; + INT32 sTempGridNo; + UINT8 ubDirection; + + // check adjacent reachable tiles + for (ubDirection = 0; ubDirection < NUM_WORLD_DIRECTIONS; ubDirection++) + { + sTempGridNo = NewGridNo(sSpot, DirectionInc(ubDirection)); + + if (sTempGridNo != sSpot) + { + ubMovementCost = gubWorldMovementCosts[sTempGridNo][ubDirection][bLevel]; + + if (ubMovementCost >= TRAVELCOST_BLOCKED || !IsLocationSittableExcludingPeople(sTempGridNo, bLevel)) + { + return(TRUE); + } + } + } + + return FALSE; +} + +BOOLEAN InSmoke(INT32 sGridNo, INT8 bLevel) +{ + if (TileIsOutOfBounds(sGridNo)) + { + return FALSE; + } + + if (gpWorldLevelData[sGridNo].ubExtFlags[bLevel] & (MAPELEMENT_EXT_SMOKE)) + { + return TRUE; + } + + return FALSE; +} + +BOOLEAN CorpseWarning(SOLDIERTYPE *pSoldier, INT32 sGridNo, INT8 bLevel) +{ + CHECKF(pSoldier); + + INT32 cnt; + ROTTING_CORPSE *pCorpse; + UINT8 ubDistance = CORPSE_WARNING_DIST; + + for (cnt = 0; cnt < giNumRottingCorpse; ++cnt) + { + pCorpse = &(gRottingCorpse[cnt]); + + if (pCorpse && + pCorpse->fActivated && + pCorpse->def.ubType < ROTTING_STAGE2 && + //pCorpse->def.ubType <= FMERC_FALLF && + pCorpse->def.ubBodyType <= REGFEMALE && + pCorpse->def.ubAIWarningValue > 0 && + pCorpse->def.bLevel == bLevel && + !TileIsOutOfBounds(pCorpse->def.sGridNo) && + PythSpacesAway(sGridNo, pCorpse->def.sGridNo) <= ubDistance && + (pSoldier->bTeam == ENEMY_TEAM && CorpseEnemyTeam(pCorpse) || pSoldier->bTeam == MILITIA_TEAM && CorpseMilitiaTeam(pCorpse) || pSoldier->bTeam == CIV_TEAM && !pSoldier->aiData.bNeutral)) + { + return TRUE; + } + } + + return FALSE; +} + +BOOLEAN CorpseEnemyTeam(ROTTING_CORPSE *pCorpse) +{ + CHECKF(pCorpse); + + // check whether corpse has soldier's uniform + for (UINT8 i = UNIFORM_ENEMY_ADMIN; i <= UNIFORM_ENEMY_ELITE; ++i) + { + if (COMPARE_PALETTEREP_ID(pCorpse->def.VestPal, gUniformColors[i].vest) && COMPARE_PALETTEREP_ID(pCorpse->def.PantsPal, gUniformColors[i].pants)) + { + return TRUE; + } + } + + return FALSE; +} + +BOOLEAN CorpseMilitiaTeam(ROTTING_CORPSE *pCorpse) +{ + CHECKF(pCorpse); + + // check whether corpse has soldier's uniform + for (UINT8 i = UNIFORM_MILITIA_ROOKIE; i <= UNIFORM_MILITIA_ELITE; ++i) + { + if (COMPARE_PALETTEREP_ID(pCorpse->def.VestPal, gUniformColors[i].vest) && COMPARE_PALETTEREP_ID(pCorpse->def.PantsPal, gUniformColors[i].pants)) + { + return TRUE; + } + } + + return FALSE; +} + +// check that current loaded sector is underground +BOOLEAN AICheckUnderground(void) +{ + // not underground + if (gbWorldSectorZ > 0) + { + return TRUE; + } + + return FALSE; +} + +// check if we have any sight cover from known enemies at spot +BOOLEAN AnyCoverAtSpot(SOLDIERTYPE *pSoldier, INT32 sSpot) +{ + CHECKF(pSoldier); + + UINT32 uiLoop; + SOLDIERTYPE *pOpponent; + INT32 *pusLastLoc; + INT8 *pbPersOL; + INT8 *pbPublOL; + INT8 *pbLastLevel; + + INT32 sThreatLoc; + //INT32 iThreatCertainty; + INT8 iThreatLevel; + + // look through all opponents for those we know of + for (uiLoop = 0; uiLoop < guiNumMercSlots; uiLoop++) + { + pOpponent = MercSlots[uiLoop]; + + // if this merc is inactive, at base, on assignment, dead, unconscious + if (!pOpponent || pOpponent->stats.bLife < OKLIFE) + { + continue; // next merc + } + + // if this man is neutral / on the same side, he's not an opponent + if (CONSIDERED_NEUTRAL(pSoldier, pOpponent) || (pSoldier->bSide == pOpponent->bSide)) + { + continue; // next merc + } + + pbPersOL = pSoldier->aiData.bOppList + pOpponent->ubID; + pbPublOL = gbPublicOpplist[pSoldier->bTeam] + pOpponent->ubID; + + pusLastLoc = gsLastKnownOppLoc[pSoldier->ubID] + pOpponent->ubID; + pbLastLevel = gbLastKnownOppLevel[pSoldier->ubID] + pOpponent->ubID; + + // if this opponent is unknown personally and publicly + if ((*pbPersOL == NOT_HEARD_OR_SEEN) && (*pbPublOL == NOT_HEARD_OR_SEEN)) + { + continue; // next merc + } + + // if personal knowledge is more up to date or at least equal + // sevenfm: fix for unknown public location + if ((gubKnowledgeValue[*pbPublOL - OLDEST_HEARD_VALUE][*pbPersOL - OLDEST_HEARD_VALUE] > 0) || + (*pbPersOL == *pbPublOL) || + *pbPersOL != NOT_HEARD_OR_SEEN && TileIsOutOfBounds(gsPublicLastKnownOppLoc[pSoldier->bTeam][pOpponent->ubID]) && !TileIsOutOfBounds(gsLastKnownOppLoc[pSoldier->ubID][pOpponent->ubID])) + { + // using personal knowledge, obtain opponent's "best guess" gridno + sThreatLoc = *pusLastLoc; + iThreatLevel = *pbLastLevel; + //iThreatCertainty = ThreatPercent[*pbPersOL - OLDEST_HEARD_VALUE]; + } + else + { + // using public knowledge, obtain opponent's "best guess" gridno + sThreatLoc = gsPublicLastKnownOppLoc[pSoldier->bTeam][pOpponent->ubID]; + iThreatLevel = gbPublicLastKnownOppLevel[pSoldier->bTeam][pOpponent->ubID]; + //iThreatCertainty = ThreatPercent[*pbPublOL - OLDEST_HEARD_VALUE]; + } + + if (!AnyCoverFromSpot(sSpot, pSoldier->pathing.bLevel, sThreatLoc, iThreatLevel) && + LocationToLocationLineOfSightTest(sThreatLoc, iThreatLevel, sSpot, pSoldier->pathing.bLevel, TRUE, NO_DISTANCE_LIMIT, STANDING_LOS_POS, PRONE_LOS_POS)) + { + return FALSE; + } + } + + return TRUE; +} + +BOOLEAN AICheckHasWeaponOfType(SOLDIERTYPE *pSoldier, UINT8 ubWeaponType) +{ + CHECKF(pSoldier); + + INT8 invsize = (INT8)pSoldier->inv.size(); + for (INT8 bLoop = 0; bLoop < invsize; ++bLoop) + { + if (pSoldier->inv[bLoop].exists() && + Item[pSoldier->inv[bLoop].usItem].usItemClass == IC_GUN && + Weapon[Item[pSoldier->inv[bLoop].usItem].ubClassIndex].ubWeaponType == ubWeaponType) + { + return TRUE; + } + } + + return FALSE; + +} + +BOOLEAN AICheckHasGun(SOLDIERTYPE *pSoldier) +{ + CHECKF(pSoldier); + + if (FindAIUsableObjClass(pSoldier, IC_GUN) != NO_SLOT) + { + return TRUE; + } + + return FALSE; +} + +BOOLEAN AICheckShortWeaponRange(SOLDIERTYPE *pSoldier) +{ + CHECKF(pSoldier); + + if (!AICheckHasGun(pSoldier)) + { + return TRUE; + } + + if (AIGunRange(pSoldier) < DAY_VISION_RANGE / 2) + { + return TRUE; + } + + return FALSE; +} diff --git a/TacticalAI/CreatureDecideAction.cpp b/TacticalAI/CreatureDecideAction.cpp index 682ba9e1..ca377ba0 100644 --- a/TacticalAI/CreatureDecideAction.cpp +++ b/TacticalAI/CreatureDecideAction.cpp @@ -15,6 +15,11 @@ class OBJECTTYPE; class SOLDIERTYPE; +extern STR8 gStr8AlertStatus[]; +extern STR8 gStr8Attitude[]; +extern STR8 gStr8Orders[]; +extern STR8 gStr8Team[]; +extern STR8 gStr8Class[]; extern INT8 STRAIGHT; //lal diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 8dac2ca5..fdc37650 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -46,6 +46,8 @@ extern BOOLEAN gfHiddenInterrupt; extern BOOLEAN gfUseAlternateQueenPosition; extern UINT16 PickSoldierReadyAnimation( SOLDIERTYPE *pSoldier, BOOLEAN fEndReady, BOOLEAN fHipStance ); +void LogDecideInfo(SOLDIERTYPE *pSoldier); +void LogKnowledgeInfo(SOLDIERTYPE *pSoldier); // global status time counters to determine what takes the most time @@ -68,6 +70,13 @@ INT8 ArmedVehicleDecideActionYellow(SOLDIERTYPE *pSoldier); INT8 ArmedVehicleDecideActionRed(SOLDIERTYPE *pSoldier); INT8 ArmedVehicleDecideActionBlack(SOLDIERTYPE *pSoldier); +STR8 gStr8AlertStatus[] = { "Green", "Yellow", "Red", "Black" }; +STR8 gStr8Attitude[] = { "DEFENSIVE", "BRAVESOLO", "BRAVEAID", "CUNNINGSOLO", "CUNNINGAID", "AGGRESSIVE", "MAXATTITUDES", "ATTACKSLAYONLY" }; +STR8 gStr8Orders[] = { "STATIONARY", "ONGUARD", "CLOSEPATROL", "FARPATROL", "POINTPATROL", "ONCALL", "SEEKENEMY", "RNDPTPATROL", "SNIPER" }; +STR8 gStr8Team[] = { "OUR_TEAM", "ENEMY_TEAM", "CREATURE_TEAM", "MILITIA_TEAM", "CIV_TEAM", "LAST_TEAM", "PLAYER_PLAN", "LAN_TEAM_ONE", "LAN_TEAM_TWO", "LAN_TEAM_THREE", "LAN_TEAM_FOUR" }; +STR8 gStr8Class[] = { "SOLDIER_CLASS_NONE", "SOLDIER_CLASS_ADMINISTRATOR", "SOLDIER_CLASS_ELITE", "SOLDIER_CLASS_ARMY", "SOLDIER_CLASS_GREEN_MILITIA", "SOLDIER_CLASS_REG_MILITIA", "SOLDIER_CLASS_ELITE_MILITIA", "SOLDIER_CLASS_CREATURE", "SOLDIER_CLASS_MINER", "SOLDIER_CLASS_ZOMBIE", "SOLDIER_CLASS_TANK", "SOLDIER_CLASS_JEEP", "SOLDIER_CLASS_BANDIT" }; +STR8 gStr8Knowledge[] = { "HEARD_3_TURNS_AGO", "HEARD_2_TURNS_AGO", "HEARD_LAST_TURN", "HEARD_THIS_TURN", "NOT_HEARD_OR_SEEN", "SEEN_CURRENTLY", "SEEN_THIS_TURN", "SEEN_LAST_TURN", "SEEN_2_TURNS_AGO", "SEEN_3_TURNS_AGO" }; + void DoneScheduleAction( SOLDIERTYPE * pSoldier ) { pSoldier->aiData.fAIFlags &= (~AI_CHECK_SCHEDULE); @@ -695,10 +704,6 @@ INT8 DecideActionGreen(SOLDIERTYPE *pSoldier) STR16 tempstr; #endif - // Flugente: to prevent an accidental call - if ( pSoldier->IsZombie() ) - return( ZombieDecideActionGreen(pSoldier) ); - if ( ARMED_VEHICLE( pSoldier ) ) return ArmedVehicleDecideActionGreen( pSoldier ); @@ -1547,10 +1552,6 @@ INT8 DecideActionYellow(SOLDIERTYPE *pSoldier) STR16 tempstr; #endif - // Flugente: to prevent an accidental call - if ( pSoldier->IsZombie() ) - return( ZombieDecideActionYellow(pSoldier) ); - if (fCivilian || (gGameExternalOptions.fAllNamedNpcsDecideAction && pSoldier->ubProfile != NO_PROFILE)) { if (pSoldier->flags.uiStatusFlags & SOLDIER_COWERING) @@ -2467,10 +2468,6 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) (pSoldier->aiData.bNeutral && gTacticalStatus.fCivGroupHostile[pSoldier->ubCivilianGroup] == CIV_GROUP_NEUTRAL) || (pSoldier->ubBodyType >= FATCIV && pSoldier->ubBodyType <= CRIPPLECIV) ) ); - // Flugente: to prevent an accidental call - if ( pSoldier->IsZombie() ) - return ZombieDecideActionRed(pSoldier); - // WANNE: Headrock informed me that I should remove that because it needs a lot of CPU! // HEADROCK HAM B2.7: Calculate the overall tactical situation //INT16 ubOverallTacticalSituation = AssessTacticalSituation(pSoldier->bSide); @@ -2484,7 +2481,7 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier) return(AI_ACTION_NONE); } - fProneSightCover = ProneSightCoverAtSpot(pSoldier, pSoldier->sGridNo); + fProneSightCover = ProneSightCoverAtSpot(pSoldier, pSoldier->sGridNo, FALSE); if( !fProneSightCover || pSoldier->aiData.bUnderFire ) { fDangerousSpot = TRUE; @@ -4534,10 +4531,6 @@ INT16 ubMinAPCost; #endif DebugMsg(TOPIC_JA2,DBG_LEVEL_3,String("DecideActionBlack: soldier = %d, orders = %d, attitude = %d",pSoldier->ubID,pSoldier->aiData.bOrders,pSoldier->aiData.bAttitude)); - // Flugente: to prevent an accidental call - if ( pSoldier->IsZombie() ) - return( ZombieDecideActionBlack(pSoldier) ); - ATTACKTYPE BestShot, BestThrow, BestStab ,BestAttack;//dnl ch69 150913 BOOLEAN fCivilian = (PTR_CIVILIAN && (pSoldier->ubCivilianGroup == NON_CIV_GROUP || pSoldier->aiData.bNeutral || (pSoldier->ubBodyType >= FATCIV && pSoldier->ubBodyType <= CRIPPLECIV) ) ); UINT8 ubBestStance = 1; @@ -6697,51 +6690,6 @@ void DecideAlertStatus( SOLDIERTYPE *pSoldier ) } } -#if 0 - else - { - if (pSoldier->bOppCnt > 0) - { - pSoldier->aiData.bAlertStatus = STATUS_BLACK; // opponent(s) in sight - } - else - { - pSoldier->aiData.bAlertStatus = STATUS_RED; // enemy sector - - /* - // wow, JA1 stuff... - // good guys all have a built-in, magic, "enemy detecting radar"... - if (Status.enemies) - pSoldier->aiData.bAlertStatus = STATUS_RED; // enemy sector - else - { - pSoldier->aiData.bAlertStatus = STATUS_GREEN; // secured sector - - // if he just dropped back from alert status, and it's a GUARD - if ((oldStatus >= STATUS_RED) && (pSoldier->manCategory == MAN_GUARD)) - { - if (TileIsOutOfBounds(pSoldier->whereIWas)) // not assigned to any trees - // FUTURE ENHANCEMENT: Look for unguarded trees with tappers - pSoldier->orders = ONCALL; - else // assigned to trees - // FUTURE ENHANCEMENT: If his tree is now tapperless, go ONCALL - pSoldier->orders = CLOSEPATROL; // go back to his tree area - - // turn off any existing bypass to Green and its "hyper-activity" - pSoldier->bypassToGreen = FALSE; - - // turn off the "inTheWay" flag, may have been set during TurnBased - pSoldier->inTheWay = FALSE; - - // make the guard put his gun away if he has it drawn - HandleNoMoreTarget(pSoldier); - } - } - */ - } - } -#endif - if ( gTacticalStatus.bBoxingState == NOT_BOXING ) { @@ -6806,7 +6754,7 @@ void DecideAlertStatus( SOLDIERTYPE *pSoldier ) // ------------------------------ ZOMBIE AI -------------------------- -INT8 ZombieDecideActionGreen(SOLDIERTYPE *pSoldier) +/*INT8 ZombieDecideActionGreen(SOLDIERTYPE *pSoldier) { DOUBLE iChance, iSneaky = 10; INT8 bInWater; @@ -7206,9 +7154,9 @@ INT8 ZombieDecideActionGreen(SOLDIERTYPE *pSoldier) // 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) +/*INT8 ZombieDecideActionYellow(SOLDIERTYPE *pSoldier) { UINT8 ubNoiseDir; INT32 sNoiseGridNo; @@ -7440,10 +7388,10 @@ INT8 ZombieDecideActionYellow(SOLDIERTYPE *pSoldier) // by default, if everything else fails, just stands in place without turning pSoldier->aiData.usActionData = NOWHERE; return(AI_ACTION_NONE); -} +}*/ -INT8 ZombieDecideActionRed(SOLDIERTYPE *pSoldier) +/*INT8 ZombieDecideActionRed(SOLDIERTYPE *pSoldier) { INT32 iDummy; INT32 iChance,sClosestOpponent,sClosestFriend; @@ -8072,14 +8020,6 @@ INT8 ZombieDecideActionRed(SOLDIERTYPE *pSoldier) // 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(ClosestReachableDisturbance(pSoldier, &fClimb))) ) { - // addition: if soldier is bleeding then reduce bleeding and do nothing - /*if ( pSoldier->bBleeding > MIN_BLEEDING_THRESHOLD ) - { - // no! zombies don't bandage themselves! - // reduce bleeding by 1 point per AP (in RT, APs will get recalculated so it's okay) - // pSoldier->bBleeding = __max( 0, pSoldier->bBleeding - (pSoldier->bActionPoints/2) ); - return( AI_ACTION_NONE ); // will end-turn/wait depending on whether we're in TB or realtime - }*/ #ifdef RECORDNET fprintf(NetDebugFile,"\tZombieDecideActionRed: guynum %d switching to GREEN AI...\n",pSoldier->ubID); #endif @@ -8126,9 +8066,9 @@ INT8 ZombieDecideActionRed(SOLDIERTYPE *pSoldier) pSoldier->aiData.usActionData = NOWHERE; return(AI_ACTION_NONE); -} +}*/ -INT8 ZombieDecideActionBlack(SOLDIERTYPE *pSoldier) +/*INT8 ZombieDecideActionBlack(SOLDIERTYPE *pSoldier) { INT32 iCoverPercentBetter = 0; INT32 iOffense, iDefense, iChance; @@ -8685,12 +8625,12 @@ INT8 ZombieDecideActionBlack(SOLDIERTYPE *pSoldier) } // get the location of the closest reachable opponent - /* Flugente 22.02.2012 - A few clarifications: I changed ClosestSeenOpponent so that for zombies, this function also returns an opponent if he is on the - * roof of a building, we are not, but our GridNo belongs to that same building. - * If that is the case, it is clear that we have to get on that roof. However, we cannot do that in BlackState. If, by pure chance, we can still see our - * enemy, we cannot climb (there is no climbing option in BlackState sofar). - * So, I changed the code so that now we will climb the roof. - */ + // Flugente 22.02.2012 - A few clarifications: I changed ClosestSeenOpponent so that for zombies, this function also returns an opponent if he is on the + // roof of a building, we are not, but our GridNo belongs to that same building. + // If that is the case, it is clear that we have to get on that roof. However, we cannot do that in BlackState. If, by pure chance, we can still see our + // enemy, we cannot climb (there is no climbing option in BlackState sofar). + // So, I changed the code so that now we will climb the roof. + INT32 targetGridNo = -1; INT8 targetbLevel = 0; sClosestOpponent = ClosestSeenOpponentWithRoof(pSoldier, &targetGridNo, &targetbLevel); @@ -8838,9 +8778,9 @@ INT8 ZombieDecideActionBlack(SOLDIERTYPE *pSoldier) pSoldier->aiData.usActionData = NOWHERE; return(AI_ACTION_NONE); -} +}*/ -INT8 ZombieDecideAction( SOLDIERTYPE *pSoldier ) +/*INT8 ZombieDecideAction( SOLDIERTYPE *pSoldier ) { INT8 bAction = AI_ACTION_NONE; @@ -8882,7 +8822,7 @@ INT8 ZombieDecideAction( SOLDIERTYPE *pSoldier ) #endif return(bAction); -} +}*/ INT8 ArmedVehicleDecideAction( SOLDIERTYPE *pSoldier ) @@ -9845,7 +9785,7 @@ INT8 ArmedVehicleDecideActionRed( SOLDIERTYPE *pSoldier) return(AI_ACTION_NONE); } - fProneSightCover = ProneSightCoverAtSpot( pSoldier, pSoldier->sGridNo ); + fProneSightCover = ProneSightCoverAtSpot(pSoldier, pSoldier->sGridNo, FALSE); if ( !fProneSightCover || pSoldier->aiData.bUnderFire ) { fDangerousSpot = TRUE; @@ -11901,3 +11841,73 @@ INT8 ArmedVehicleDecideActionBlack( SOLDIERTYPE *pSoldier ) pSoldier->aiData.usActionData = NOWHERE; return(AI_ACTION_NONE); } + + +extern UINT32 guiTurnCnt; +extern UINT32 guiReinforceTurn; +extern UINT32 guiArrived; + +void LogDecideInfo(SOLDIERTYPE *pSoldier) +{ + DebugAI(AI_MSG_INFO, pSoldier, String("Turn num %d aware %d", guiTurnCnt, gTacticalStatus.Team[pSoldier->bTeam].bAwareOfOpposition)); + DebugAI(AI_MSG_INFO, pSoldier, String("current team %d interrupt occurred %d", gTacticalStatus.ubCurrentTeam, gTacticalStatus.fInterruptOccurred)); + 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 Morale %d", pSoldier->stats.bLife, pSoldier->stats.bLifeMax, pSoldier->bBreath, pSoldier->bBreathMax, pSoldier->aiData.bShock, CalcSuppressionTolerance(pSoldier), pSoldier->aiData.bAIMorale, pSoldier->aiData.bMorale)); + DebugAI(AI_MSG_INFO, pSoldier, String("Spot %d level %d opponents %d", pSoldier->sGridNo, pSoldier->pathing.bLevel, pSoldier->aiData.bOppCnt)); + DebugAI(AI_MSG_INFO, pSoldier, String("ubServiceCount %d ubServicePartner %d fDoingSurgery %d", pSoldier->ubServiceCount, pSoldier->ubServicePartner, pSoldier->fDoingSurgery)); + if (pSoldier->usAnimState == COWERING || pSoldier->usAnimState == COWERING_PRONE) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Cowering")); + } + if (pSoldier->usAnimState == GIVING_AID || pSoldier->usAnimState == GIVING_AID_PRN) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Giving aid")); + } + //CHAR8 str8[1024]; + + // show watched locations + INT8 bLoop; + for (bLoop = 0; bLoop < NUM_WATCHED_LOCS; bLoop++) + { + if (!TileIsOutOfBounds(gsWatchedLoc[pSoldier->ubID][bLoop])) + { + DebugAI(AI_MSG_INFO, pSoldier, String("Watched location %d level %d points %d", gsWatchedLoc[pSoldier->ubID][bLoop], gbWatchedLocLevel[pSoldier->ubID][bLoop], gubWatchedLocPoints[pSoldier->ubID][bLoop])); + } + } + + LogKnowledgeInfo(pSoldier); + + DebugAI(AI_MSG_INFO, pSoldier, String("What I know %d", WhatIKnowThatPublicDont(pSoldier, FALSE))); + DebugAI(AI_MSG_INFO, pSoldier, String("Has Gun %d, Short range weapon %d, Gun Range %d, Gun Ammo %d, Gun Scoped %d ", AICheckHasGun(pSoldier), AICheckShortWeaponRange(pSoldier), AIGunRange(pSoldier), AIGunAmmo(pSoldier), AIGunScoped(pSoldier))); +} + +void LogKnowledgeInfo(SOLDIERTYPE *pSoldier) +{ + //CHAR8 str8[1024]; + //memset(str8, 0, 1024 * sizeof(char)); + + // show public opponents + for (UINT16 oppID = 0; oppID < MAX_NUM_SOLDIERS; oppID++) + { + if (gbPublicOpplist[pSoldier->bTeam][oppID] != NOT_HEARD_OR_SEEN && + !MercPtrs[oppID]->aiData.bNeutral) + { + //wcstombs(str8, MercPtrs[oppID]->GetName(), wcslen(MercPtrs[oppID]->GetName())+1); + //wcstombs(str8, MercPtrs[oppID]->GetName(), 1024 - 1); + DebugAI(AI_MSG_INFO, pSoldier, String("public opponent [%d] knowledge %s gridno %d level %d", oppID, gStr8Knowledge[gbPublicOpplist[pSoldier->bTeam][oppID] - OLDEST_HEARD_VALUE], gsPublicLastKnownOppLoc[pSoldier->bTeam][oppID], gbPublicLastKnownOppLevel[pSoldier->bTeam][oppID])); + //swprintf( pStrInfo, L"%s[%d] %s %s\n", pStrInfo, oppID, MercPtrs[oppID]->GetName(), SeenStr(gbPublicOpplist[pSoldier->bTeam][oppID]) ); + } + } + // show personal opponents + for (UINT16 oppID = 0; oppID < MAX_NUM_SOLDIERS; oppID++) + { + if (pSoldier->aiData.bOppList[oppID] != NOT_HEARD_OR_SEEN && + !MercPtrs[oppID]->aiData.bNeutral) + { + //wcstombs(str8, MercPtrs[oppID]->GetName(), wcslen(MercPtrs[oppID]->GetName())+1); + //wcstombs(str8, MercPtrs[oppID]->GetName(), 1024 - 1); + DebugAI(AI_MSG_INFO, pSoldier, String("personal opponent [%d] knowledge %s gridno %d level %d", oppID, gStr8Knowledge[pSoldier->aiData.bOppList[oppID] - OLDEST_HEARD_VALUE], gsLastKnownOppLoc[pSoldier->ubID][oppID], gbLastKnownOppLevel[pSoldier->ubID][oppID])); + //swprintf( pStrInfo, L"%s[%d] %s %s\n", pStrInfo, oppID, MercPtrs[oppID]->GetName(), SeenStr(pSoldier->aiData.bOppList[oppID]) ); + } + } +} \ No newline at end of file diff --git a/TacticalAI/FindLocations.cpp b/TacticalAI/FindLocations.cpp index 839b637a..078a20c7 100644 --- a/TacticalAI/FindLocations.cpp +++ b/TacticalAI/FindLocations.cpp @@ -290,14 +290,13 @@ INT32 CalcCoverValue(SOLDIERTYPE *pMe, INT32 sMyGridNo, INT32 iMyThreat, INT32 i INT16 sTempX, sTempY; FLOAT dMyX, dMyY, dHisX, dHisY; INT8 bHisBestCTGT, bHisActualCTGT, bHisCTGT, bMyCTGT; - INT32 iRangeChange, iRangeFactor, iRangeFactorMultiplier; + INT32 iRangeChange, iRangeFactor, iRCD; SOLDIERTYPE *pHim; // sevenfm INT8 bHisLevel; INT8 bMyLevel; INT32 sDist; - UINT8 ubCoverReduction; dMyX = dMyY = dHisX = dHisY = -1.0; @@ -338,7 +337,8 @@ INT32 CalcCoverValue(SOLDIERTYPE *pMe, INT32 sMyGridNo, INT32 iMyThreat, INT32 i } - if (InWaterOrGas(pHim,sHisGridNo)) + if (DeepWater(sHisGridNo, bHisLevel) || InGas(pHim, sHisGridNo)) + //if (InWaterOrGas(pHim,sHisGridNo)) { bHisActualCTGT = 0; } @@ -378,7 +378,8 @@ INT32 CalcCoverValue(SOLDIERTYPE *pMe, INT32 sMyGridNo, INT32 iMyThreat, INT32 i // if my intended gridno is in water or gas, I can't attack at all from there // here, for smoke, consider bad - if (InWaterGasOrSmoke(pMe,sMyGridNo)) + if (DeepWater(sMyGridNo, bMyLevel) || InGas(pMe, sMyGridNo)) + //if (InWaterGasOrSmoke(pMe,sMyGridNo)) { bMyCTGT = 0; } @@ -392,8 +393,6 @@ INT32 CalcCoverValue(SOLDIERTYPE *pMe, INT32 sMyGridNo, INT32 iMyThreat, INT32 i pHim->dXPos = (FLOAT) sTempX; pHim->dYPos = (FLOAT) sTempY; } - // bMyCTGT = ChanceToGetThrough(pMe,sHisGridNo,FAKE,ACTUAL,TESTWALLS,9999,M9PISTOL,NOT_FOR_LOS); // assume a gunshot - // bMyCTGT = SoldierToLocationChanceToGetThrough( pMe, sHisGridNo, pMe->bTargetLevel, pMe->bTargetCubeLevel ); // let's not assume anything about the stance the enemy might take, so take an average // value... no cover give a higher value than partial cover @@ -423,6 +422,39 @@ INT32 CalcCoverValue(SOLDIERTYPE *pMe, INT32 sMyGridNo, INT32 iMyThreat, INT32 i pHim->dYPos = dHisY; // and the 'y' } + sDist = PythSpacesAway(sMyGridNo, sHisGridNo); + + if (gGameExternalOptions.fAIBetterCover) + { + // sevenfm: special calculations for zombies: zombie is very dangerous at close range + if (pHim->IsZombie() || !AICheckHasGun(pHim)) + { + if (sDist < (INT32)(DAY_VISION_RANGE / 2)) + { + // 100 - 25 + bHisCTGT = 100 - 150 * sDist / DAY_VISION_RANGE; + } + else + { + // 25 - 0 + bHisCTGT = 100 * DAY_VISION_RANGE * DAY_VISION_RANGE / (16 * sDist * sDist); + } + } + + // sevenfm: if soldier has no gun, use different calculation + if (pMe->IsZombie() || !AICheckHasGun(pMe)) + { + if (sDist < (INT32)(DAY_VISION_RANGE / 2)) + { + bMyCTGT = 100 - 150 * sDist / DAY_VISION_RANGE; + } + else + { + bMyCTGT = 100 * DAY_VISION_RANGE * DAY_VISION_RANGE / (16 * sDist * sDist); + } + } + } + // these value should be < 1 million each iHisPosValue = bHisCTGT * Threat[uiThreatIndex].iValue * Threat[uiThreatIndex].iAPs; iMyPosValue = bMyCTGT * iMyThreat * iMyAPsLeft; @@ -431,18 +463,20 @@ INT32 CalcCoverValue(SOLDIERTYPE *pMe, INT32 sMyGridNo, INT32 iMyThreat, INT32 i // max 25% at DAY_VISION_RANGE/2, 0 at zero range if( gGameExternalOptions.fAIBetterCover ) { - if( AnyCoverFromSpot(sMyGridNo, bMyLevel, sHisGridNo, bHisLevel) ) + UINT8 ubCoverBonus = 25; + if (sDist < (INT32)(DAY_VISION_RANGE / 2)) { - ubCoverReduction = 25; - - sDist = PythSpacesAway(sMyGridNo, sHisGridNo); - - if (sDist < (INT32)(DAY_VISION_RANGE / 2)) - { - ubCoverReduction = ubCoverReduction * 2 * sDist / DAY_VISION_RANGE; - } - iHisPosValue -= iHisPosValue * ubCoverReduction / 100; - iMyPosValue += iMyPosValue * ubCoverReduction / 100; + ubCoverBonus = ubCoverBonus * 2 * sDist / DAY_VISION_RANGE; + } + if (!pHim->IsZombie() && AICheckHasGun(pHim) && AnyCoverFromSpot(sMyGridNo, bMyLevel, sHisGridNo, bHisLevel)) + { + iHisPosValue -= iHisPosValue * ubCoverBonus / 100; + iMyPosValue += iMyPosValue * ubCoverBonus / 100; + } + if (!pMe->IsZombie() && AICheckHasGun(pMe) && AnyCoverFromSpot(sHisGridNo, bHisLevel, sMyGridNo, bMyLevel)) + { + iHisPosValue += iHisPosValue * ubCoverBonus / 100; + iMyPosValue -= iMyPosValue * ubCoverBonus / 100; } } @@ -459,66 +493,62 @@ INT32 CalcCoverValue(SOLDIERTYPE *pMe, INT32 sMyGridNo, INT32 iMyThreat, INT32 i } - // if my positional value is worth something at all here - if (iMyPosValue > 0) + // if my positional value is worth something at all here + if (iMyPosValue > 0) { - // if I CAN'T crouch when I get there, that makes it significantly less - // appealing a spot (how much depends on range), so that's a penalty to me - if (iMyAPsLeft < GetAPsCrouch(pMe,TRUE)) - // subtract another 1 % penalty for NOT being able to crouch per tile - // the farther away we are, the bigger a difference crouching will make! - iMyPosValue -= ((iMyPosValue * (AIM_PENALTY_TARGET_CROUCHED + (iRange / CELL_X_SIZE))) / 100); + // if I CAN'T crouch when I get there, that makes it significantly less + // appealing a spot (how much depends on range), so that's a penalty to me + if (iMyAPsLeft < GetAPsCrouch(pMe, TRUE)) + // subtract another 1 % penalty for NOT being able to crouch per tile + // the farther away we are, the bigger a difference crouching will make! + iMyPosValue -= ((iMyPosValue * (AIM_PENALTY_TARGET_CROUCHED + (iRange / CELL_X_SIZE))) / 100); } // high morale prefers decreasing the range (positive factor), while very // low morale (HOPELESS) prefers increasing it -// if (bHisCTGT < 100 || (morale - 1 < 0)) + iRCD = RangeChangeDesire(pMe); + + if (iRCD) { + iRangeChange = Threat[uiThreatIndex].iOrigRange - iRange; - iRangeFactorMultiplier = RangeChangeDesire( pMe ); - - if (iRangeFactorMultiplier) + if (iRangeChange) { - iRangeChange = Threat[uiThreatIndex].iOrigRange - iRange; + //iRangeFactor = (iRangeChange * (morale - 1)) / 4; + iRangeFactor = (iRangeChange * iRCD) / 2; - if (iRangeChange) + // sevenfm: reduce range bonus depending on cover + if (gGameExternalOptions.fAIBetterCover) { - //iRangeFactor = (iRangeChange * (morale - 1)) / 4; - iRangeFactor = (iRangeChange * iRangeFactorMultiplier) / 2; - - // sevenfm: reduce range bonus depending on cover - if( gGameExternalOptions.fAIBetterCover ) + if (!AnyCoverFromSpot(sMyGridNo, bMyLevel, sHisGridNo, bHisLevel) && + CountSeenEnemiesLastTurn(pMe) > CountNearbyFriends(pMe, sMyGridNo, DAY_VISION_RANGE / 2)) { - if( !AnyCoverFromSpot(sMyGridNo, bMyLevel, sHisGridNo, bHisLevel) && - CountSeenEnemiesLastTurn(pMe) > CountNearbyFriends(pMe, sMyGridNo, DAY_VISION_RANGE / 2) ) - { - iRangeFactor = iRangeFactor * (100 - bHisCTGT * __min(Threat[uiThreatIndex].iAPs, APBPConstants[AP_MAXIMUM]) / APBPConstants[AP_MAXIMUM]) / 100; - } - } - - #ifdef DEBUGCOVER - DebugAI( String( "CalcCoverValue: iRangeChange %d, iRangeFactor %d\n", iRangeChange, iRangeFactor ) ); - #endif - - // aggression booster for stupider enemies - iMyPosValue += 100 * iRangeFactor * ( 5 - SoldierDifficultyLevel( pMe ) ) / 5 ; - - // if factor is positive increase positional value, else decrease it - // change both values, since one or the other could be 0 - if (iRangeFactor > 0) - { - - iMyPosValue = (iMyPosValue * (100 + iRangeFactor)) / 100; - iHisPosValue = (100 * iHisPosValue) / (100 + iRangeFactor); - } - else if (iRangeFactor < 0) - { - iMyPosValue = (100 * iMyPosValue) / (100 - iRangeFactor); - iHisPosValue = (iHisPosValue * (100 - iRangeFactor)) / 100; + iRangeFactor = iRangeFactor * (100 - bHisCTGT * __min(Threat[uiThreatIndex].iAPs, APBPConstants[AP_MAXIMUM]) / APBPConstants[AP_MAXIMUM]) / 100; } } + +#ifdef DEBUGCOVER + DebugAI( String( "CalcCoverValue: iRangeChange %d, iRangeFactor %d\n", iRangeChange, iRangeFactor ) ); +#endif + + // aggression booster for stupider enemies + iMyPosValue += 100 * iRangeFactor * (5 - SoldierDifficultyLevel(pMe)) / 5; + + // if factor is positive increase positional value, else decrease it + // change both values, since one or the other could be 0 + if (iRangeFactor > 0) + { + + iMyPosValue = (iMyPosValue * (100 + iRangeFactor)) / 100; + iHisPosValue = (100 * iHisPosValue) / (100 + iRangeFactor); + } + else if (iRangeFactor < 0) + { + iMyPosValue = (100 * iMyPosValue) / (100 - iRangeFactor); + iHisPosValue = (iHisPosValue * (100 - iRangeFactor)) / 100; + } } } @@ -527,7 +557,7 @@ INT32 CalcCoverValue(SOLDIERTYPE *pMe, INT32 sMyGridNo, INT32 iMyThreat, INT32 i iReductionFactor = ((MAX_THREAT_RANGE - iRange) * Threat[uiThreatIndex].iCertainty) / MAX_THREAT_RANGE; - // divide by a 100 to make the numbers more managable and avoid 32-bit limit + // divide by a 100 to make the numbers more manageable and avoid 32-bit limit iThisScale = max( iMyPosValue, iHisPosValue) / 100; iThisScale = (iThisScale * iReductionFactor) / 100; *iTotalScale += iThisScale; @@ -535,7 +565,7 @@ INT32 CalcCoverValue(SOLDIERTYPE *pMe, INT32 sMyGridNo, INT32 iMyThreat, INT32 i // POSITIVE COVER VALUE INDICATES THE COVER BENEFITS ME, NEGATIVE RESULT // MEANS IT BENEFITS THE OTHER GUY. - // divide by a 100 to make the numbers more managable and avoid 32-bit limit + // divide by a 100 to make the numbers more manageable and avoid 32-bit limit iCoverValue = (iMyPosValue - iHisPosValue) / 100; iCoverValue = (iCoverValue * iReductionFactor) / 100; @@ -604,7 +634,7 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB UINT8 ubBackgroundLightLevel; UINT8 ubBackgroundLightPercent = 0; UINT8 ubLightPercentDifference; - BOOLEAN fNight; + BOOLEAN fNight; // sevenfm UINT8 ubNearbyFriends; @@ -1057,6 +1087,13 @@ INT32 FindBestNearbyCover(SOLDIERTYPE *pSoldier, INT32 morale, INT32 *piPercentB continue; } + // zombies only want to hide + if (pSoldier->IsZombie() && !SightCoverAtSpot(pSoldier, sGridNo, TRUE)) + { + //DebugCover(pSoldier, String("zombie: no sight cover at new spot, skip")); + continue; + } + iPathCost = gubAIPathCosts[AI_PATHCOST_RADIUS + sXOffset][AI_PATHCOST_RADIUS + sYOffset]; /* // water is OK, if the only good hiding place requires us to get wet, OK @@ -3145,3 +3182,483 @@ INT32 FindNearestPassableSpot( INT32 sGridNo, UINT8 usSearchRadius ) return( sNearestSpot ); } + +INT32 FindAdvanceSpot(SOLDIERTYPE *pSoldier, INT32 sTargetSpot, INT8 bAction, UINT8 ubType, BOOLEAN fUnlimited) +{ + INT32 sGridNo, sRealGridNo; + INT32 sBestSpot = NOWHERE; + INT32 iSearchRange = min(AI_PATHCOST_RADIUS, MAX_TILES_MOVE_TURN); + INT16 sMaxLeft, sMaxRight, sMaxUp, sMaxDown, sXOffset, sYOffset; + INT32 iPathCost, iBestPathCost = 0; + INT16 usMovementMode; + INT32 iRoamRange, iDistFromOrigin, sOrigin; + //BOOLEAN fClimbingNecessary; + //INT32 sClimbGridNo; + BOOLEAN fHasMortar; + + if (!pSoldier) + { + return NOWHERE; + } + + // check target location + if (!NewOKDestination(pSoldier, sTargetSpot, FALSE, pSoldier->pathing.bLevel)) + { + //ScreenMsg(FONT_ORANGE, MSG_INTERFACE, L"bad destination %d", sTargetSpot); + return NOWHERE; + } + + fHasMortar = AICheckIsMortarOperator(pSoldier); + + usMovementMode = DetermineMovementMode(pSoldier, bAction); + + UINT8 ubReserveAP = 0; + + if (ubType != ADVANCE_SPOT_SIGHT_COVER) + { + if (usMovementMode == RUNNING || usMovementMode == WALKING) + { + ubReserveAP = APBPConstants[AP_CHANGE_FACING] + GetAPsCrouch(pSoldier, TRUE) + GetAPsProne(pSoldier, TRUE); + } + else + { + ubReserveAP = APBPConstants[AP_LOOK_CROUCHED] + GetAPsProne(pSoldier, TRUE); + } + } + + if (pSoldier->bActionPoints <= ubReserveAP) + { + return NOWHERE; + } + + // check that location is reachable + iBestPathCost = EstimatePlotPath(pSoldier, sTargetSpot, FALSE, FALSE, FALSE, usMovementMode, pSoldier->bStealthMode, FALSE, 0); + if (iBestPathCost == 0) + { + return NOWHERE; + } + + iRoamRange = RoamingRange(pSoldier, &sOrigin); + + // set AP limit + gubNPCAPBudget = pSoldier->bActionPoints - ubReserveAP; + // set the distance limit of the square region + gubNPCDistLimit = (UINT8)iSearchRange; + + // determine maximum horizontal limits + sMaxLeft = min(iSearchRange, (pSoldier->sGridNo % MAXCOL)); + //NumMessage("sMaxLeft = ",sMaxLeft); + sMaxRight = min(iSearchRange, MAXCOL - ((pSoldier->sGridNo % MAXCOL) + 1)); + //NumMessage("sMaxRight = ",sMaxRight); + + // determine maximum vertical limits + sMaxUp = min(iSearchRange, (pSoldier->sGridNo / MAXROW)); + //NumMessage("sMaxUp = ",sMaxUp); + sMaxDown = min(iSearchRange, MAXROW - ((pSoldier->sGridNo / MAXROW) + 1)); + //NumMessage("sMaxDown = ",sMaxDown); + + // Call FindBestPath to set flags in all locations that we can + // walk into within range. We have to set some things up first... + + // reset the "reachable" flags in the region we're looking at + for (sYOffset = -sMaxUp; sYOffset <= sMaxDown; sYOffset++) + { + for (sXOffset = -sMaxLeft; sXOffset <= sMaxRight; sXOffset++) + { + sGridNo = pSoldier->sGridNo + sXOffset + (MAXCOL * sYOffset); + if (!(sGridNo >= 0 && sGridNo < WORLD_MAX)) + { + continue; + } + + gpWorldLevelData[sGridNo].uiFlags &= ~(MAPELEMENT_REACHABLE); + } + } + + FindBestPath(pSoldier, GRIDSIZE, pSoldier->pathing.bLevel, usMovementMode, COPYREACHABLE, 0); + + // Turn off the "reachable" flag for his current location + // so we don't consider it + gpWorldLevelData[pSoldier->sGridNo].uiFlags &= ~(MAPELEMENT_REACHABLE); + + for (sYOffset = -sMaxUp; sYOffset <= sMaxDown; sYOffset++) + { + for (sXOffset = -sMaxLeft; sXOffset <= sMaxRight; sXOffset++) + { + // calculate the next potential gridno + sGridNo = pSoldier->sGridNo + sXOffset + (MAXCOL * sYOffset); + + if (!(sGridNo >= 0 && sGridNo < WORLD_MAX)) + { + continue; + } + + if (!(gpWorldLevelData[sGridNo].uiFlags & MAPELEMENT_REACHABLE)) + { + continue; + } + + if (sGridNo == pSoldier->pathing.sBlackList) + { + continue; + } + + // check roaming range + if (iRoamRange < MAX_ROAMING_RANGE && !TileIsOutOfBounds(sOrigin)) + { + iDistFromOrigin = SpacesAway(sOrigin, sGridNo); + if (iDistFromOrigin > iRoamRange) + { + continue; + } + } + + // check if we will be closer to target spot + if (PythSpacesAway(sGridNo, sTargetSpot) > PythSpacesAway(pSoldier->sGridNo, sTargetSpot)) + { + continue; + } + + // exclude locations with tear/mustard gas (at this point, smoke is cool!) + if (InGas(pSoldier, sGridNo)) + { + continue; + } + + // skip lighted spots + if (InLightAtNight(sGridNo, pSoldier->pathing.bLevel) && + !InLightAtNight(pSoldier->sGridNo, pSoldier->pathing.bLevel)) + continue; + + // skip deep water + if (DeepWater(sGridNo, pSoldier->pathing.bLevel)) + { + continue; + } + + // skip water + if (Water(sGridNo, pSoldier->pathing.bLevel)) + { + continue; + } + + // skip locations too close to target spot + if (PythSpacesAway(sGridNo, sTargetSpot) < (INT16)(DAY_VISION_RANGE / 4)) + { + continue; + } + + // avoid smoke + if (InSmoke(sGridNo, pSoldier->pathing.bLevel)) + { + continue; + } + + // skip if this spot has no cover nearby + if ((ubType == ADVANCE_SPOT_ANY_COVER || fUnlimited) && + !FindObstacleNearSpot(sGridNo, pSoldier->pathing.bLevel)) + { + continue; + } + + // avoid locations near fresh corpses + if (!pSoldier->IsZombie() && GetNearestRottingCorpseAIWarning(sGridNo) > 0) + //if (CorpseWarning(pSoldier, sGridNo, pSoldier->pathing.bLevel, TRUE)) + { + continue; + } + + // avoid overcrowding + if (!pSoldier->IsZombie() && NumberOfTeamMatesAdjacent(pSoldier, sGridNo) > 1) + { + continue; + } + + if (!CheckNPCDestination(pSoldier, sGridNo)) + { + continue; + } + + if (!LegalNPCDestination(pSoldier, sGridNo, IGNORE_PATH, NOWATER, 0)) + { + continue; // skip on to the next potential grid + } + + // sevenfm: avoid rooms for mortar operators + if (!AICheckUnderground() && + pSoldier->pathing.bLevel == 0 && + fHasMortar && + InARoom(sGridNo, NULL)) + { + continue; + } + + if (ubType == ADVANCE_SPOT_SIGHT_COVER) + { + // skip locations with no sight cover + if (!SightCoverAtSpot(pSoldier, sGridNo, fUnlimited)) + { + continue; + } + } + else if (ubType == ADVANCE_SPOT_PRONE_COVER) + { + // check prone sight cover at spot + if (!ProneSightCoverAtSpot(pSoldier, sGridNo, fUnlimited)) + { + continue; + } + // check that we'll have enough APs to go prone at target spot + /*iPathCost = EstimatePlotPath( pSoldier, sGridNo, FALSE, FALSE, FALSE, usMovementMode, pSoldier->bStealthMode, FALSE, 0); + if( pSoldier->bActionPoints - iPathCost < GetAPsCrouch(pSoldier, TRUE) + GetAPsProne(pSoldier, TRUE) + APBPConstants[AP_CHANGE_FACING] ) + { + continue; + }*/ + } + else if (ubType == ADVANCE_SPOT_ANY_COVER) + { + // check any cover at spot + if (!AnyCoverAtSpot(pSoldier, sGridNo)) + { + continue; + } + // check that we'll have enough APs to go prone at target spot + /*iPathCost = EstimatePlotPath( pSoldier, sGridNo, FALSE, FALSE, FALSE, usMovementMode, pSoldier->bStealthMode, FALSE, 0); + if( pSoldier->bActionPoints - iPathCost < GetAPsCrouch(pSoldier, TRUE) + GetAPsProne(pSoldier, TRUE) + APBPConstants[AP_CHANGE_FACING] ) + { + continue; + }*/ + } + + sRealGridNo = pSoldier->sGridNo; + pSoldier->sGridNo = sGridNo; + iPathCost = EstimatePlotPath(pSoldier, sTargetSpot, FALSE, FALSE, FALSE, usMovementMode, pSoldier->bStealthMode, FALSE, 0); + //iPathCost = PlotPath(pSoldier, sTargetSpot, FALSE, FALSE, FALSE, usMovementMode, pSoldier->bStealthMode, FALSE, 0); + //iPathCost = EstimatePathCostToLocation( pSoldier, sTargetSpot, pSoldier->pathing.bLevel, TRUE, &fClimbingNecessary, &sClimbGridNo ); + pSoldier->sGridNo = sRealGridNo; + + // skip location if no path to target spot + if (iPathCost == 0) + { + //ScreenMsg(FONT_ORANGE, MSG_INTERFACE, L"cannot find path to destination %d at %d", sTargetSpot, sGridNo); + continue; + } + + //if( sBestSpot == NOWHERE || iPathCost < iBestPathCost ) + if (iPathCost < iBestPathCost) + { + sBestSpot = sGridNo; + iBestPathCost = iPathCost; + } + } + } + + gubNPCAPBudget = 0; + gubNPCDistLimit = 0; + + return(sBestSpot); +} + +// find spot with cover, max dist from opponents +INT32 FindRetreatSpot(SOLDIERTYPE *pSoldier) +{ + INT32 sGridNo; + INT32 sBestSpot = NOWHERE; + INT32 iSearchRange = MAX_TILES_MOVE_TURN; + INT16 sMaxLeft, sMaxRight, sMaxUp, sMaxDown, sXOffset, sYOffset; + INT16 usMovementMode; + INT32 iRoamRange, iDistFromOrigin, sOrigin; + //BOOLEAN fClimbingNecessary; + //INT32 sClimbGridNo; + UINT8 ubReserveAP; + INT8 bAction = AI_ACTION_TAKE_COVER; + INT32 sClosestOpponent; + INT16 sDistance, sClosestDistance; + + if (!pSoldier) + { + return NOWHERE; + } + + sClosestOpponent = ClosestKnownOpponent(pSoldier, NULL, NULL); + if (TileIsOutOfBounds(sClosestOpponent)) + { + // no one to hide from + return NOWHERE; + } + + sClosestDistance = PythSpacesAway(pSoldier->sGridNo, sClosestOpponent); + + //ubReserveAP = GetAPsCrouch(pSoldier, TRUE) + GetAPsProne(pSoldier, TRUE) + APBPConstants[AP_CHANGE_FACING]; + ubReserveAP = GetAPsCrouch(pSoldier, TRUE) + APBPConstants[AP_CHANGE_FACING]; + + if (pSoldier->bActionPoints <= ubReserveAP) + { + return NOWHERE; + } + + iRoamRange = RoamingRange(pSoldier, &sOrigin); + + // set AP limit + gubNPCAPBudget = pSoldier->bActionPoints; + // set the distance limit of the square region + gubNPCDistLimit = (UINT8)iSearchRange; + + // determine maximum horizontal limits + sMaxLeft = min(iSearchRange, (pSoldier->sGridNo % MAXCOL)); + //NumMessage("sMaxLeft = ",sMaxLeft); + sMaxRight = min(iSearchRange, MAXCOL - ((pSoldier->sGridNo % MAXCOL) + 1)); + //NumMessage("sMaxRight = ",sMaxRight); + + // determine maximum vertical limits + sMaxUp = min(iSearchRange, (pSoldier->sGridNo / MAXROW)); + //NumMessage("sMaxUp = ",sMaxUp); + sMaxDown = min(iSearchRange, MAXROW - ((pSoldier->sGridNo / MAXROW) + 1)); + //NumMessage("sMaxDown = ",sMaxDown); + + // Call FindBestPath to set flags in all locations that we can + // walk into within range. We have to set some things up first... + + // reset the "reachable" flags in the region we're looking at + for (sYOffset = -sMaxUp; sYOffset <= sMaxDown; sYOffset++) + { + for (sXOffset = -sMaxLeft; sXOffset <= sMaxRight; sXOffset++) + { + sGridNo = pSoldier->sGridNo + sXOffset + (MAXCOL * sYOffset); + if (!(sGridNo >= 0 && sGridNo < WORLD_MAX)) + { + continue; + } + + gpWorldLevelData[sGridNo].uiFlags &= ~(MAPELEMENT_REACHABLE); + } + } + + usMovementMode = DetermineMovementMode(pSoldier, bAction); + FindBestPath(pSoldier, GRIDSIZE, pSoldier->pathing.bLevel, usMovementMode, COPYREACHABLE, 0); + + // Turn off the "reachable" flag for his current location + // so we don't consider it + gpWorldLevelData[pSoldier->sGridNo].uiFlags &= ~(MAPELEMENT_REACHABLE); + + for (sYOffset = -sMaxUp; sYOffset <= sMaxDown; sYOffset++) + { + for (sXOffset = -sMaxLeft; sXOffset <= sMaxRight; sXOffset++) + { + // calculate the next potential gridno + sGridNo = pSoldier->sGridNo + sXOffset + (MAXCOL * sYOffset); + + if (!(sGridNo >= 0 && sGridNo < WORLD_MAX)) + { + continue; + } + + if (!(gpWorldLevelData[sGridNo].uiFlags & MAPELEMENT_REACHABLE)) + { + continue; + } + + if (sGridNo == pSoldier->pathing.sBlackList) + { + continue; + } + + // check roaming range + if (iRoamRange < MAX_ROAMING_RANGE && !TileIsOutOfBounds(sOrigin)) + { + iDistFromOrigin = SpacesAway(sOrigin, sGridNo); + if (iDistFromOrigin > iRoamRange) + { + continue; + } + } + + // exclude locations with tear/mustard gas (at this point, smoke is cool!) + if (InGas(pSoldier, sGridNo)) + { + continue; + } + + // skip lighted spots + if (InLightAtNight(sGridNo, pSoldier->pathing.bLevel) && + !InLightAtNight(pSoldier->sGridNo, pSoldier->pathing.bLevel)) + continue; + + // skip deep water + if (DeepWater(sGridNo, pSoldier->pathing.bLevel)) + { + continue; + } + + // skip water + if (Water(sGridNo, pSoldier->pathing.bLevel)) + { + continue; + } + + // avoid locations near fresh corpses + if (!pSoldier->IsZombie() && GetNearestRottingCorpseAIWarning(sGridNo) > 0) + //if (CorpseWarning(pSoldier, sGridNo, pSoldier->pathing.bLevel, TRUE)) + { + continue; + } + + if (!pSoldier->IsZombie() && NumberOfTeamMatesAdjacent(pSoldier, sGridNo) > 0) + { + continue; + } + + if (!CheckNPCDestination(pSoldier, sGridNo)) + { + continue; + } + + // avoid smoke + /*if( InSmoke(sGridNo, pSoldier->pathing.bLevel, FALSE) ) + { + continue; + }*/ + + if (!LegalNPCDestination(pSoldier, sGridNo, IGNORE_PATH, NOWATER, 0)) + { + continue; // skip on to the next potential grid + } + + // skip if too close to enemy + //sClosestOpponent = ClosestKnownOpponent(pSoldier, NULL, NULL); + sDistance = PythSpacesAway(sGridNo, sClosestOpponent); + if (sDistance < (INT16)(DAY_VISION_RANGE / 2)) + { + continue; + } + + // skip if this spot has no cover nearby + if (!FindObstacleNearSpot(sGridNo, pSoldier->pathing.bLevel)) + { + continue; + } + + if (!AnyCoverAtSpot(pSoldier, sGridNo)) + { + continue; + } + + // check prone sight cover at spot + /*if( !ProneSightCoverAtSpot(pSoldier, sGridNo) ) + { + continue; + }*/ + + if (sDistance > sClosestDistance) + { + sBestSpot = sGridNo; + sClosestDistance = sDistance; + } + } + } + + gubNPCAPBudget = 0; + gubNPCDistLimit = 0; + + return(sBestSpot); +} diff --git a/TacticalAI/TacticalAI_VS2010.vcxproj b/TacticalAI/TacticalAI_VS2010.vcxproj index 3c145d7a..6d85a403 100644 --- a/TacticalAI/TacticalAI_VS2010.vcxproj +++ b/TacticalAI/TacticalAI_VS2010.vcxproj @@ -45,6 +45,7 @@ + {B369A125-E62E-46AE-9285-58003D688301} diff --git a/TacticalAI/TacticalAI_VS2010.vcxproj.filters b/TacticalAI/TacticalAI_VS2010.vcxproj.filters index d2be0d58..2e680ac7 100644 --- a/TacticalAI/TacticalAI_VS2010.vcxproj.filters +++ b/TacticalAI/TacticalAI_VS2010.vcxproj.filters @@ -71,5 +71,8 @@ Source Files + + Source Files + \ No newline at end of file diff --git a/TacticalAI/TacticalAI_VS2013.vcxproj b/TacticalAI/TacticalAI_VS2013.vcxproj index 67be2a4e..7de0cbd7 100644 --- a/TacticalAI/TacticalAI_VS2013.vcxproj +++ b/TacticalAI/TacticalAI_VS2013.vcxproj @@ -45,6 +45,7 @@ + {B369A125-E62E-46AE-9285-58003D688301} diff --git a/TacticalAI/TacticalAI_VS2013.vcxproj.filters b/TacticalAI/TacticalAI_VS2013.vcxproj.filters index d2be0d58..2e680ac7 100644 --- a/TacticalAI/TacticalAI_VS2013.vcxproj.filters +++ b/TacticalAI/TacticalAI_VS2013.vcxproj.filters @@ -71,5 +71,8 @@ Source Files + + Source Files + \ No newline at end of file diff --git a/TacticalAI/TacticalAI_VS2017.vcxproj b/TacticalAI/TacticalAI_VS2017.vcxproj index 0ecee2b3..65b962ce 100644 --- a/TacticalAI/TacticalAI_VS2017.vcxproj +++ b/TacticalAI/TacticalAI_VS2017.vcxproj @@ -45,6 +45,7 @@ + {B369A125-E62E-46AE-9285-58003D688301} diff --git a/TacticalAI/ZombieDecideAction.cpp b/TacticalAI/ZombieDecideAction.cpp new file mode 100644 index 00000000..8bb7a3c7 --- /dev/null +++ b/TacticalAI/ZombieDecideAction.cpp @@ -0,0 +1,1419 @@ +#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); + } + } + } + } +}*/ diff --git a/TacticalAI/ai.h b/TacticalAI/ai.h index b0c54cb1..cba7e3d4 100644 --- a/TacticalAI/ai.h +++ b/TacticalAI/ai.h @@ -5,6 +5,7 @@ #include "worlddef.h" #include "Soldier Control.h" #include "Isometric Utils.h" +#include "Rotting Corpses.h" #define TESTAICONTROL @@ -109,6 +110,7 @@ typedef enum AI_ACTION_DOCTOR_SELF, // added by Flugente: AI-ONLY! bandage/surgery on self. DO NOT USE THIS FOR MERCS!!! AI_ACTION_SELFDETONATE, // added by Flugente: blow up an explosive in own inventory AI_ACTION_STOP_MEDIC, // sevenfm: stop giving aid animation + AI_ACTION_LAST = AI_ACTION_STOP_MEDIC } ActionType; @@ -153,6 +155,12 @@ enum QuoteActionType extern INT8 gbDiff[MAX_DIFF_PARMS][5]; +enum{ + ADVANCE_SPOT_SIGHT_COVER, + ADVANCE_SPOT_PRONE_COVER, + ADVANCE_SPOT_ANY_COVER +}; + void ActionDone(SOLDIERTYPE *pSoldier); INT16 ActionInProgress(SOLDIERTYPE *pSoldier); @@ -173,7 +181,9 @@ INT32 ClosestUnDisguisedPC( SOLDIERTYPE *pSoldier, INT32 * psDistance ); // Flug BOOLEAN CanAutoBandage( BOOLEAN fDoFullCheck ); void DebugAI( STR szOutput ); -INT8 DecideAction(SOLDIERTYPE *pSoldier); +enum { AI_MSG_START, AI_MSG_DECIDE, AI_MSG_INFO, AI_MSG_TOPIC }; +void DebugAI(INT8 bMsgType, SOLDIERTYPE *pSoldier, STR szOutput, INT8 bAction = -1); +INT8 DecideAction(SOLDIERTYPE *pSoldier); INT8 DecideActionBlack(SOLDIERTYPE *pSoldier); INT8 DecideActionEscort(SOLDIERTYPE *pSoldier); INT8 DecideActionGreen(SOLDIERTYPE *pSoldier); @@ -293,8 +303,13 @@ BOOLEAN AICheckIsFlanking( SOLDIERTYPE *pSoldier ); INT8 CalcMoraleNew(SOLDIERTYPE *pSoldier); -BOOLEAN ProneSightCoverAtSpot( SOLDIERTYPE *pSoldier, INT32 sSpot ); -BOOLEAN SightCoverAtSpot( SOLDIERTYPE *pSoldier, INT32 sSpot ); +BOOLEAN ProneSightCoverAtSpot(SOLDIERTYPE *pSoldier, INT32 sSpot, BOOLEAN fUnlimited); +BOOLEAN SightCoverAtSpot(SOLDIERTYPE *pSoldier, INT32 sSpot, BOOLEAN fUnlimited); +BOOLEAN AnyCoverAtSpot(SOLDIERTYPE *pSoldier, INT32 sSpot); +BOOLEAN AnyCoverFromSpot(INT32 sSpot, INT8 bLevel, INT32 sThreatLoc, INT8 bThreatLevel); + +INT32 FindAdvanceSpot(SOLDIERTYPE *pSoldier, INT32 sTargetSpot, INT8 bAction, UINT8 ubType, BOOLEAN fUnlimited); +INT32 FindRetreatSpot(SOLDIERTYPE *pSoldier); BOOLEAN CheckDoorAtGridno(UINT32 usGridNo); BOOLEAN CheckDoorNearGridno(UINT32 usGridNo); @@ -307,19 +322,53 @@ BOOLEAN FindNearbyExplosiveStructure(INT32 sSpot, INT8 bLevel); INT16 DistanceToClosestActiveOpponent(SOLDIERTYPE *pSoldier, INT32 sSpot); BOOLEAN ValidOpponent(SOLDIERTYPE* pSoldier, SOLDIERTYPE* pOpponent); -BOOLEAN AnyCoverFromSpot( INT32 sSpot, INT8 bLevel, INT32 sThreatLoc, INT8 bThreatLevel ); UINT8 CountSeenEnemiesLastTurn( SOLDIERTYPE *pSoldier ); +BOOLEAN FindObstacleNearSpot(INT32 sSpot, INT8 bLevel); + BOOLEAN CheckNPCDestination(SOLDIERTYPE *pSoldier, INT32 sGridNo); UINT8 SpotDangerLevel(SOLDIERTYPE *pSoldier, INT32 sGridNo); BOOLEAN AllowDeepWaterFlanking(SOLDIERTYPE *pSoldier); +BOOLEAN AICheckUnderground(void); + +BOOLEAN CorpseWarning(SOLDIERTYPE *pSoldier, INT32 sGridNo, INT8 bLevel); +BOOLEAN CorpseEnemyTeam(ROTTING_CORPSE *pCorpse); +BOOLEAN CorpseMilitiaTeam(ROTTING_CORPSE *pCorpse); BOOLEAN NorthSpot(INT32 sSpot, INT8 bLevel); BOOLEAN SoldierAI(SOLDIERTYPE *pSoldier); +UINT8 AIDirection(INT32 sSpot1, INT32 sSpot2); + +BOOLEAN AIGunScoped(SOLDIERTYPE *pSoldier); +BOOLEAN AIGunInHandScoped(SOLDIERTYPE *pSoldier); +UINT16 AIGunRange(SOLDIERTYPE *pSoldier); // gun range in tiles +UINT16 AIGunClass(SOLDIERTYPE *pSoldier); +UINT16 AIGunType(SOLDIERTYPE *pSoldier); +UINT16 AIGunAmmo(SOLDIERTYPE *pSoldier); +BOOLEAN AIGunAutofireCapable(SOLDIERTYPE *pSoldier); +UINT8 AIGunDeadliness(SOLDIERTYPE *pSoldier); + +BOOLEAN AICheckNVG(SOLDIERTYPE *pSoldier); +BOOLEAN AICheckHasWeaponOfType(SOLDIERTYPE *pSoldier, UINT8 ubWeaponType); +BOOLEAN AICheckHasGun(SOLDIERTYPE *pSoldier); +BOOLEAN AICheckShortWeaponRange(SOLDIERTYPE *pSoldier); + +BOOLEAN AICheckIsSniper(SOLDIERTYPE *pSoldier); +BOOLEAN AICheckIsMarksman(SOLDIERTYPE *pSoldier); +BOOLEAN AICheckIsMachinegunner(SOLDIERTYPE *pSoldier); +BOOLEAN AICheckIsRadioOperator(SOLDIERTYPE *pSoldier); +BOOLEAN AICheckIsMedic(SOLDIERTYPE *pSoldier); +BOOLEAN AICheckIsMortarOperator(SOLDIERTYPE *pSoldier); +BOOLEAN AICheckIsGLOperator(SOLDIERTYPE *pSoldier); +BOOLEAN AICheckIsOfficer(SOLDIERTYPE *pSoldier); +BOOLEAN AICheckIsCommander(SOLDIERTYPE *pSoldier); + #define MAX_FLANKS_RED 25 #define MAX_FLANKS_YELLOW 25 +#define MAX_TILES_MOVE_TURN (APBPConstants[AP_MAXIMUM] / (APBPConstants[AP_MOVEMENT_FLAT] + APBPConstants[AP_MODIFIER_RUN])) + // vision range defines #define DAY_VISION_RANGE (gGameExternalOptions.ubStraightSightRange * STRAIGHT_RATIO * 2) #define NIGHT_VISION_RANGE (gGameExternalOptions.ubStraightSightRange * STRAIGHT_RATIO ) diff --git a/TileEngine/SmokeEffects.cpp b/TileEngine/SmokeEffects.cpp index 16b9054f..0a563dcc 100644 --- a/TileEngine/SmokeEffects.cpp +++ b/TileEngine/SmokeEffects.cpp @@ -26,6 +26,9 @@ #include "Campaign Types.h" #include "Tactical Save.h" #include "strategicmap.h" + // sevenfm + #include "environment.h" + #include "Render Fun.h" #endif #include "SaveLoadGame.h" @@ -1188,6 +1191,34 @@ BOOL GetRandomSignalSmokeGridNo(INT32* psGridNo) return TRUE; } +// sevenfm: check smoke effect of certain type at gridno +BOOLEAN CheckSmokeEffect(INT32 sGridNo, INT8 bLevel, INT8 bType) +{ + UINT32 uiCnt; + INT8 bSmokeEffectLevel; + + //loop through all smoke effects + for (uiCnt = 0; uiCnt < guiNumSmokeEffects; uiCnt++) + { + if (gSmokeEffectData[uiCnt].fAllocated && + gSmokeEffectData[uiCnt].sGridNo == sGridNo && + gSmokeEffectData[uiCnt].bType == bType) + { + if (gSmokeEffectData[uiCnt].bFlags & SMOKE_EFFECT_ON_ROOF) + bSmokeEffectLevel = 1; + else + bSmokeEffectLevel = 0; + + if (bSmokeEffectLevel == bLevel) + { + return TRUE; + } + } + } + + return FALSE; +} + BOOLEAN FindVisibleSmokeEffect(INT8 bType) { UINT32 uiCnt; diff --git a/TileEngine/SmokeEffects.h b/TileEngine/SmokeEffects.h index af185f38..c32f2cba 100644 --- a/TileEngine/SmokeEffects.h +++ b/TileEngine/SmokeEffects.h @@ -71,6 +71,8 @@ void UpdateSmokeEffectGraphics( ); // Flugente: get the gridno and blevel of a random smoke signal, if one exists BOOL GetRandomSignalSmokeGridNo(INT32* psGridNo); +// sevenfm: check if smoke effect of certain type is present at gridno +BOOLEAN CheckSmokeEffect(INT32 sGridNo, INT8 bLevel, INT8 bType); // find smoke effect on visible screen BOOLEAN FindVisibleSmokeEffect(INT8 bType); diff --git a/TileEngine/physics.cpp b/TileEngine/physics.cpp index 4983bd2f..730a8ca3 100644 --- a/TileEngine/physics.cpp +++ b/TileEngine/physics.cpp @@ -793,6 +793,7 @@ void PhysicsDeleteObject( REAL_OBJECT *pObject ) } } +INT16 gsWaterSplashSoundNum = -1; BOOLEAN PhysicsCheckForCollisions( REAL_OBJECT *pObject, INT32 *piCollisionID ) { @@ -1113,9 +1114,25 @@ BOOLEAN PhysicsCheckForCollisions( REAL_OBJECT *pObject, INT32 *piCollisionID ) // sevenfm: also play sound CHAR8 zFilename[512]; - sprintf(zFilename, "sounds\\misc\\Splash%d.ogg", Random(3) + 1); - if (FileExists(zFilename)) - PlayJA2SampleFromFile(zFilename, RATE_11025, SoundVolume(MIDVOLUME, pObject->sGridNo), 1, SoundDir(pObject->sGridNo)); + // prepare water splash sound + if (gsWaterSplashSoundNum < 0) + { + gsWaterSplashSoundNum = 0; + do + { + gsWaterSplashSoundNum++; + sprintf(zFilename, "sounds\\misc\\Splash%d.ogg", gsWaterSplashSoundNum); + } while (FileExists(zFilename)); + gsWaterSplashSoundNum--; + } + if (gsWaterSplashSoundNum > 0) + { + sprintf(zFilename, "sounds\\misc\\Splash%d.ogg", Random(gsWaterSplashSoundNum) + 1); + if (FileExists(zFilename)) + { + PlayJA2SampleFromFile(zFilename, RATE_11025, SoundVolume(MIDVOLUME, pObject->sGridNo), 1, SoundDir(pObject->sGridNo)); + } + } } }