From 2e773a029dcac94808382a0d9edfd8bd01b63bbc Mon Sep 17 00:00:00 2001 From: Flugente Date: Sun, 16 Mar 2014 18:24:40 +0000 Subject: [PATCH] - New Feature: enemies roles allows the AI to use medics and officers, and the player can now identify high-value targets among the enemy. Requires GameDir >= r1987. - If an enemy is spawned with a radio set, he is more likely to become a radio operator. - It is now possible to mount a gun on prone allies if you are facing them on the right angle. git-svn-id: https://ja2svn.mooo.com/source/ja2/trunk/GameSource/ja2_v1.13/Build@7072 3b4a5df2-a311-0410-b5c6-a8a6f20db521 --- GameSettings.cpp | 15 ++- GameSettings.h | 13 +++ Strategic/Assignments.cpp | 2 - Strategic/Assignments.h | 2 + Tactical/Interface.cpp | 121 ++++++++++++++++++++++- Tactical/Interface.h | 3 + Tactical/Morale.cpp | 19 +++- Tactical/Overhead.cpp | 25 +++++ Tactical/Overhead.h | 12 +++ Tactical/Soldier Control.cpp | 178 ++++++++++++++++++++++++++++++++-- Tactical/Soldier Control.h | 13 ++- Tactical/Soldier Create.cpp | 163 +++++++++++++++++-------------- Tactical/Turn Based Input.cpp | 11 ++- Tactical/opplist.cpp | 9 +- TacticalAI/AIMain.cpp | 7 ++ TacticalAI/AIUtils.cpp | 83 +++++++++++++--- TacticalAI/DecideAction.cpp | 127 +++++++++++++++++++++++- TacticalAI/ai.h | 4 + Utils/_ChineseText.cpp | 2 +- Utils/_DutchText.cpp | 2 +- Utils/_EnglishText.cpp | 2 +- Utils/_FrenchText.cpp | 2 +- Utils/_GermanText.cpp | 2 +- Utils/_ItalianText.cpp | 2 +- Utils/_PolishText.cpp | 2 +- Utils/_RussianText.cpp | 2 +- Utils/_TaiwaneseText.cpp | 2 +- 27 files changed, 702 insertions(+), 123 deletions(-) diff --git a/GameSettings.cpp b/GameSettings.cpp index 21f6e579..e469a819 100644 --- a/GameSettings.cpp +++ b/GameSettings.cpp @@ -1472,6 +1472,19 @@ void LoadGameExternalOptions() gGameExternalOptions.fAccessOtherMercInventories = iniReader.ReadBoolean("Tactical Gameplay Settings","ACCESS_OTHER_MERC_INVENTORIES", TRUE); gGameExternalOptions.fBackPackWeightLowersAP = iniReader.ReadBoolean("Tactical Gameplay Settings","BACKPACKWEIGHT_LOWERS_AP", TRUE); + //################# Tactical Enemy Role Settings ################## + // Flugente: enemy roles + gGameExternalOptions.fEnemyRoles = iniReader.ReadBoolean("Tactical Enemy Role Settings", "ENEMYROLES", TRUE); + gGameExternalOptions.usTurnsToUncover = iniReader.ReadInteger("Tactical Enemy Role Settings", "ENEMYROLES_TURNSTOUNCOVER", 6, 0, 20); + gGameExternalOptions.fEnemyMedics = iniReader.ReadBoolean("Tactical Enemy Role Settings", "ENEMY_MEDICS", TRUE); + gGameExternalOptions.dEnemyMedicMedKitDrainFactor = iniReader.ReadFloat ("Tactical Enemy Role Settings", "ENEMY_MEDICS_MEDKITDRAINFACTOR", 0.1f, 0.01f, 1.0f); + gGameExternalOptions.sEnemyMedicsSearchRadius = iniReader.ReadInteger("Tactical Enemy Role Settings", "ENEMY_MEDICS_SEARCHRAIUS", 20, 5, 60); + gGameExternalOptions.fEnemyOfficers = iniReader.ReadBoolean("Tactical Enemy Role Settings", "ENEMY_OFFICERS", TRUE); + gGameExternalOptions.usEnemyOfficersPerTeamSize = iniReader.ReadInteger("Tactical Enemy Role Settings", "ENEMY_OFFICERS_REQUIREDTEAMSIZE", 10, 1, 64); + gGameExternalOptions.usEnemyOfficersMax = iniReader.ReadInteger("Tactical Enemy Role Settings", "ENEMY_OFFICERS_MAX", 4, 1, 10); + gGameExternalOptions.sEnemyOfficerSuppressionResistanceBonus = iniReader.ReadInteger("Tactical Enemy Role Settings", "ENEMY_OFFICERS_SUPPRESSION_RESISTANCE_BONUS", 10, 0, 50); + gGameExternalOptions.dEnemyOfficerMoraleModifier = iniReader.ReadFloat ("Tactical Enemy Role Settings", "ENEMY_OFFICERS_MORALE_MODIFIER", 0.1f, 0.00f, 1.0f); + //################# Tactical Cover System Settings ################## // CPT: Cover System Settings @@ -1768,7 +1781,7 @@ void LoadGameExternalOptions() gGameExternalOptions.ubPrisonerInterrogationPoints[PRISONER_REGULAR] = iniReader.ReadInteger("Strategic Gameplay Settings","PRISONER_INTERROGATION_POINTS_REGULAR", 100, gGameExternalOptions.ubPrisonerInterrogationPoints[PRISONER_ADMIN], 1000); gGameExternalOptions.ubPrisonerInterrogationPoints[PRISONER_ELITE] = iniReader.ReadInteger("Strategic Gameplay Settings","PRISONER_INTERROGATION_POINTS_ELITE", 100, gGameExternalOptions.ubPrisonerInterrogationPoints[PRISONER_REGULAR], 1000); gGameExternalOptions.ubPrisonerInterrogationPoints[PRISONER_SPECIAL] = iniReader.ReadInteger("Strategic Gameplay Settings","PRISONER_INTERROGATION_POINTS_SPECIAL", 100, gGameExternalOptions.ubPrisonerInterrogationPoints[PRISONER_ELITE], 1000); - + // CHRISL: Determine how Skyrider should handle landing in enemy occupied sectors gGameExternalOptions.ubSkyriderHotLZ = iniReader.ReadInteger("Strategic Gameplay Settings", "ALLOW_SKYRIDER_HOT_LZ", 0, 0, 3); diff --git a/GameSettings.h b/GameSettings.h index d24c534c..6d9cee67 100644 --- a/GameSettings.h +++ b/GameSettings.h @@ -1337,6 +1337,19 @@ typedef struct INT8 sMoraleModRacism; INT8 sMoraleModSexism; INT8 sMoraleModXenophobicBackGround; + + // Flugente: enemy roles + BOOLEAN fEnemyRoles; + UINT8 usTurnsToUncover; + BOOLEAN fEnemyMedics; + FLOAT dEnemyMedicMedKitDrainFactor; + INT16 sEnemyMedicsSearchRadius; + BOOLEAN fEnemyOfficers; + UINT16 usEnemyOfficersPerTeamSize; + UINT16 usEnemyOfficersMax; + INT8 sEnemyOfficerSuppressionResistanceBonus; + FLOAT dEnemyOfficerMoraleModifier; + // Sandro: Alternative weapon holding (rifles fired from hip / pistols fired one-handed) UINT8 ubAllowAlternativeWeaponHolding; diff --git a/Strategic/Assignments.cpp b/Strategic/Assignments.cpp index a888a7f7..28944a62 100644 --- a/Strategic/Assignments.cpp +++ b/Strategic/Assignments.cpp @@ -540,7 +540,6 @@ void CheckForAndHandleHospitalPatients( void ); void HealHospitalPatient( SOLDIERTYPE *pPatient, UINT16 usHealingPtsLeft ); BOOLEAN MakeSureToolKitIsInHand( SOLDIERTYPE *pSoldier ); -BOOLEAN MakeSureMedKitIsInHand( SOLDIERTYPE *pSoldier ); void RepositionMouseRegions( void ); void CheckAndUpdateTacticalAssignmentPopUpPositions( void ); @@ -8634,7 +8633,6 @@ BOOLEAN MakeSureMedKitIsInHand( SOLDIERTYPE *pSoldier ) { INT8 bPocket = 0; - fTeamPanelDirty = TRUE; // if there is a MEDICAL BAG in his hand, we're set diff --git a/Strategic/Assignments.h b/Strategic/Assignments.h index 872e46ca..2ae2a191 100644 --- a/Strategic/Assignments.h +++ b/Strategic/Assignments.h @@ -454,6 +454,8 @@ void HandleGatheringInformationBySoldier( SOLDIERTYPE* pSoldier ); BOOLEAN MercStaffsMilitaryHQ(); +BOOLEAN MakeSureMedKitIsInHand( SOLDIERTYPE *pSoldier ); + #endif diff --git a/Tactical/Interface.cpp b/Tactical/Interface.cpp index 0d24b150..7fb2ff52 100644 --- a/Tactical/Interface.cpp +++ b/Tactical/Interface.cpp @@ -253,6 +253,7 @@ UINT32 guiBURSTACCUM; UINT32 guiITEMPOINTERHATCHES; UINT32 guiUNDERWATER; // added by Flugente +UINT32 guiENEMYROLES; // added by Flugente // UI Globals MOUSE_REGION gViewportRegion; @@ -299,6 +300,9 @@ extern void CalculateCoverForSoldier( SOLDIERTYPE* pForSoldier, const INT32& sTa extern FLOAT Distance2D( FLOAT dDeltaX, FLOAT dDeltaY ); +// Flugente: toggle display of enemy role indicators +BOOLEAN gDisplayEnemyRoles = TRUE; + BOOLEAN InitializeFaceGearGraphics() { VOBJECT_DESC VObjectDesc; @@ -545,6 +549,12 @@ BOOLEAN InitializeTacticalInterface( ) if( !AddVideoObject( &VObjectDesc, &guiUNDERWATER ) ) AssertMsg(0, "Missing INTERFACE\\fish.sti" ); + // Flugente: enemy role symbols + VObjectDesc.fCreateFlags = VOBJECT_CREATE_FROMFILE; + FilenameForBPP("INTERFACE\\RoleIcons.sti", VObjectDesc.ImageFile); + if( !AddVideoObject( &VObjectDesc, &guiENEMYROLES ) ) + AssertMsg(0, "Missing INTERFACE\\RoleIcons.sti" ); + //CHRISL: Moved to seperate function so we can call seperately InitializeTacticalPortraits(); @@ -1822,6 +1832,11 @@ void DrawSelectedUIAboveGuy( UINT16 usSoldierID ) } } + if ( gDisplayEnemyRoles && pSoldier->bTeam == ENEMY_TEAM ) + { + ShowSoldierRoleSymbol(pSoldier); + } + if ( !pSoldier->flags.fShowLocator ) { // RETURN IF MERC IS NOT SELECTED @@ -2618,10 +2633,10 @@ void DrawSelectedUIAboveGuy( UINT16 usSoldierID ) if(gGameExternalOptions.fEnemyRank) sY+=10; - iBack = RegisterBackgroundRect(BGND_FLAG_SINGLE, NULL, sX, sY, (INT16)(sX + 11 ), (INT16)(sY + 14 ) ); + iBack = RegisterBackgroundRect(BGND_FLAG_SINGLE, NULL, sX, sY, (INT16)(sX + 22 ), (INT16)(sY + 14 ) ); if ( iBack != -1 ) SetBackgroundRectFilled( iBack ); - + DrawRankIcon( pSoldier->stats.bExpLevel, sX, sY ); } } @@ -6279,3 +6294,105 @@ void DrawEnemyHealthBar( SOLDIERTYPE* pSoldier, INT32 sX, INT32 sY, UINT8 ubLine UnLockVideoSurface( FRAME_BUFFER ); } +// Flugente: show enemy role +BOOLEAN ShowSoldierRoleSymbol(SOLDIERTYPE* pSoldier) +{ + // this only works on enemy soldiers + if ( pSoldier->bTeam != ENEMY_TEAM ) + return false; + + if ( pSoldier->usSkillCounter[SOLDIER_COUNTER_ROLE_OBSERVED] < gGameExternalOptions.usTurnsToUncover ) + return false; + + INT16 sXPos = 0; + INT16 sYPos = 0; + INT32 iBack = 0; + + GetSoldierAboveGuyPositions( pSoldier, &sXPos, &sYPos, TRUE ); + + // Adjust for bars! + sXPos += 50; + sYPos += 25; + + // is this guy an officer? + if ( pSoldier->bSoldierFlagMask & SOLDIER_ENEMY_OFFICER ) + { + // Add bars + iBack = RegisterBackgroundRect( BGND_FLAG_SINGLE, NULL, sXPos, sYPos, (INT16)(sXPos + 20 ), (INT16)(sYPos + 20 ) ); + + if ( iBack != -1 ) + { + SetBackgroundRectFilled( iBack ); + } + + BltVideoObjectFromIndex( FRAME_BUFFER, guiENEMYROLES, 0, sXPos, sYPos, VO_BLT_TRANSSHADOW, NULL ); + + sYPos += 20; + } + + // is this guy radio operator with a radio? + if ( pSoldier->CanUseRadio(false) ) + { + // Add bars + iBack = RegisterBackgroundRect( BGND_FLAG_SINGLE, NULL, sXPos, sYPos, (INT16)(sXPos + 20 ), (INT16)(sYPos + 20 ) ); + + if ( iBack != -1 ) + { + SetBackgroundRectFilled( iBack ); + } + + BltVideoObjectFromIndex( FRAME_BUFFER, guiENEMYROLES, 3, sXPos, sYPos, VO_BLT_TRANSSHADOW, NULL ); + + sYPos += 20; + } + + // is this guy a doctor? we do not check for medical gear, as we would not have that information from a glance + if ( HAS_SKILL_TRAIT( pSoldier, DOCTOR_NT) ) + { + // Add bars + iBack = RegisterBackgroundRect( BGND_FLAG_SINGLE, NULL, sXPos, sYPos, (INT16)(sXPos + 20 ), (INT16)(sYPos + 20 ) ); + + if ( iBack != -1 ) + { + SetBackgroundRectFilled( iBack ); + } + + BltVideoObjectFromIndex( FRAME_BUFFER, guiENEMYROLES, 1, sXPos, sYPos, VO_BLT_TRANSSHADOW, NULL ); + + sYPos += 20; + } + + // is this guy a sniper? Just check for the gun, trait is not necessary + if ( pSoldier->HasSniper() ) + { + // Add bars + iBack = RegisterBackgroundRect( BGND_FLAG_SINGLE, NULL, sXPos, sYPos, (INT16)(sXPos + 20 ), (INT16)(sYPos + 20 ) ); + + if ( iBack != -1 ) + { + SetBackgroundRectFilled( iBack ); + } + + BltVideoObjectFromIndex( FRAME_BUFFER, guiENEMYROLES, 2, sXPos, sYPos, VO_BLT_TRANSSHADOW, NULL ); + + sYPos += 20; + } + + // is this guy a mortar guy? Just check for the gun, trait is not necessary + if ( pSoldier->HasMortar() ) + { + // Add bars + iBack = RegisterBackgroundRect( BGND_FLAG_SINGLE, NULL, sXPos, sYPos, (INT16)(sXPos + 20 ), (INT16)(sYPos + 20 ) ); + + if ( iBack != -1 ) + { + SetBackgroundRectFilled( iBack ); + } + + BltVideoObjectFromIndex( FRAME_BUFFER, guiENEMYROLES, 4, sXPos, sYPos, VO_BLT_TRANSSHADOW, NULL ); + + sYPos += 20; + } + + return true; +} diff --git a/Tactical/Interface.h b/Tactical/Interface.h index 69a49f05..e4444bca 100644 --- a/Tactical/Interface.h +++ b/Tactical/Interface.h @@ -533,4 +533,7 @@ BOOLEAN CoverColorCode( INT8 cover, INT16 &color8, INT16 &color16 ); void DrawRankIcon( INT8 rank, INT32 baseX, INT32 baseY ); void DrawLine( INT32 x1, INT32 y1, INT32 x2, INT32 y2, UINT16 color8, UINT16 color16, UINT8 *pDestBuf ); +// Flugente: show enemy role +BOOLEAN ShowSoldierRoleSymbol(SOLDIERTYPE* pSoldier); + #endif \ No newline at end of file diff --git a/Tactical/Morale.cpp b/Tactical/Morale.cpp index 07eb41b1..013121a3 100644 --- a/Tactical/Morale.cpp +++ b/Tactical/Morale.cpp @@ -129,21 +129,27 @@ INT8 GetMoraleModifier( SOLDIERTYPE * pSoldier ) } else { + INT8 morale = 0; + // use AI morale switch( pSoldier->aiData.bAIMorale ) { case MORALE_HOPELESS: - return( -15 ); + morale = -15; case MORALE_WORRIED: - return( -7 ); + morale = -7; case MORALE_CONFIDENT: - return( 2 ); + morale = -2; case MORALE_FEARLESS: - return( 5 ); + morale = -5; default: - return( 0 ); + morale = 0; } + // Flugente: morale modifiers + morale = max(morale, morale * pSoldier->GetMoraleModifier()); + + return morale; } } @@ -366,6 +372,9 @@ void RefreshSoldierMorale( SOLDIERTYPE * pSoldier ) } } + // Flugente: morale modifiers + iActualMorale = iActualMorale * pSoldier->GetMoraleModifier(); + // Flugente: ubMaxMorale can now be influenced by our food situation if ( gGameOptions.fFoodSystem ) FoodMaxMoraleModifiy(pSoldier, &ubMaxMorale); diff --git a/Tactical/Overhead.cpp b/Tactical/Overhead.cpp index e84e05de..9348cab4 100644 --- a/Tactical/Overhead.cpp +++ b/Tactical/Overhead.cpp @@ -10619,3 +10619,28 @@ INT8 CalcEffectiveShockLevel( SOLDIERTYPE * pSoldier ) } return bShockForCower; } + +// Flugente: count number of enemy officers +UINT8 HighestEnemyOfficersInSector( BOOL& aType ) +{ + SOLDIERTYPE* pSoldier; + INT32 cnt = 0; + UINT8 num = 0; + UINT8 type = OFFICER_NONE; + + for ( cnt = gTacticalStatus.Team[ ENEMY_TEAM ].bFirstID, pSoldier = MercPtrs[cnt]; cnt < gTacticalStatus.Team[ ENEMY_TEAM ].bLastID; pSoldier++, ++cnt ) + { + if ( pSoldier->bActive && pSoldier->bInSector && pSoldier->stats.bLife > 0 ) + { + if ( pSoldier->bSoldierFlagMask & SOLDIER_ENEMY_OFFICER ) + { + // officers with double squadleader trait are captains, otherwise lieutnant + aType = max( aType, NUM_SKILL_TRAITS( pSoldier, SQUADLEADER_NT) ); + + ++num; + } + } + } + + return num; +} \ No newline at end of file diff --git a/Tactical/Overhead.h b/Tactical/Overhead.h index 31c5c6a9..4ea4f269 100644 --- a/Tactical/Overhead.h +++ b/Tactical/Overhead.h @@ -388,5 +388,17 @@ void KillOnePrisoner( SECTORINFO *pSectorInfo ); // sevenfm: calc shock level with modifiers (traits, backgrounds, ...) INT8 CalcEffectiveShockLevel( SOLDIERTYPE * pSoldier ); +// Flugente: enemy officers +enum { + OFFICER_NONE = 0, + OFFICER_LIEUTNANT, + OFFICER_CAPTAIN, + + OFFICER_MAX, +}; + +// Flugente: return number of enemy officers and highest rank found +UINT8 HighestEnemyOfficersInSector(BOOL& aType); + #endif diff --git a/Tactical/Soldier Control.cpp b/Tactical/Soldier Control.cpp index 3899ba32..ebf648e6 100644 --- a/Tactical/Soldier Control.cpp +++ b/Tactical/Soldier Control.cpp @@ -12636,8 +12636,10 @@ UINT32 SOLDIERTYPE::SoldierDressWound( SOLDIERTYPE *pVictim, INT16 sKitPts, INT1 } // if we are going to do the surgery + // Flugente: AI medics are allowed to perform surgery without first aid kits if (pVictim->iHealableInjury > 0 && this->fDoingSurgery && this->ubID != pVictim->ubID && gGameOptions.fNewTraitSystem - && (NUM_SKILL_TRAITS( this, DOCTOR_NT ) >= gSkillTraitValues.ubDONumberTraitsNeededForSurgery) && Item[this->inv[ HANDPOS ].usItem].medicalkit) + && (NUM_SKILL_TRAITS( this, DOCTOR_NT ) >= gSkillTraitValues.ubDONumberTraitsNeededForSurgery) + && (Item[this->inv[ HANDPOS ].usItem].medicalkit || this->bTeam == ENEMY_TEAM) ) { fOnSurgery = TRUE; } @@ -12877,9 +12879,12 @@ UINT32 SOLDIERTYPE::SoldierDressWound( SOLDIERTYPE *pVictim, INT16 sKitPts, INT1 // We are finished !!! this->fDoingSurgery = FALSE; gTacticalStatus.ubLastRequesterSurgeryTargetID = NOBODY; // reset last target - - // throw message - ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, New113Message[ MSG113_SURGERY_FINISHED ], pVictim->GetName() ); + + if ( this->bTeam != ENEMY_TEAM ) + { + // throw message + ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, New113Message[ MSG113_SURGERY_FINISHED ], pVictim->GetName() ); + } // add to record - another surgery undergoed if ( pVictim->ubProfile != NO_PROFILE && usLifeReturned >= 100 ) @@ -14164,9 +14169,25 @@ BOOLEAN SOLDIERTYPE::IsWeaponMounted( void ) { // for some reason I find EXTREMELY FRUSTRATING, we might get a heigth of 2 on a totally empty tile... so we check if we could occupy the tile if ( !IsLocationSittable( nextGridNoinSight, 0 ) ) + { // resting our gun on people would be rude - only allow if nobody is there - if( WhoIsThere2( nextGridNoinSight, 0 ) == NOBODY ) - applybipod = TRUE; + UINT usPersonID = WhoIsThere2( nextGridNoinSight, this->pathing.bLevel ); + if( usPersonID == NOBODY ) + applybipod = TRUE; + else + { + SOLDIERTYPE* pSoldier = MercPtrs[ usPersonID ]; + + // if the other person is an ally an prone + if ( this->bSide == pSoldier->bSide && gAnimControl[ pSoldier->usAnimState ].ubEndHeight == ANIM_PRONE ) + { + // if we are facing the other guy in a 90 degree angle, we can mount our gun on his back + // Once merc's relationship allows angering mercs through actions of others, add a penalty here + if ( this->ubDirection == gTwoCCDirection[ pSoldier->ubDirection ] || this->ubDirection == gTwoCDirection[ pSoldier->ubDirection ] ) + applybipod = TRUE; + } + } + } } } else if ( adjacenttileheight == 4 && (gAnimControl[ this->usAnimState ].ubEndHeight == ANIM_CROUCH || (gAnimControl[ this->usAnimState ].ubEndHeight == ANIM_STAND && (gAnimControl[ this->usAnimState ].uiFlags &(ANIM_ALT_WEAPON_HOLDING))))) @@ -16954,6 +16975,26 @@ INT8 SOLDIERTYPE::GetSuppressionResistanceBonus() bonus += this->GetBackgroundValue(BG_RESI_SUPPRESSION); + // Flugente: enemy roles + if ( this->bTeam == ENEMY_TEAM ) + { + // bonus if we have an officer around + BOOL officertype = OFFICER_NONE; + if ( HighestEnemyOfficersInSector( officertype ) ) + { + switch ( officertype ) + { + case OFFICER_LIEUTNANT: + bonus += gGameExternalOptions.sEnemyOfficerSuppressionResistanceBonus; + break; + + case OFFICER_CAPTAIN: + bonus += gGameExternalOptions.sEnemyOfficerSuppressionResistanceBonus * 2; + break; + } + } + } + return min( 100, max( -100, bonus) ); } @@ -17039,12 +17080,33 @@ UINT8 SOLDIERTYPE::GetMoraleThreshold() { UINT8 threshold = 100; UINT8 moraledamage = 0; - + moraledamage = (moraledamage * (100 - GetFearResistanceBonus())) / 100; - return min(threshold, max(0, threshold - moraledamage)); } +FLOAT SOLDIERTYPE::GetMoraleModifier() +{ + FLOAT mod = 1.0f; + + BOOL officertype = OFFICER_NONE; + if ( HighestEnemyOfficersInSector( officertype ) ) + { + switch ( officertype ) + { + case OFFICER_LIEUTNANT: + mod += gGameExternalOptions.dEnemyOfficerMoraleModifier; + break; + + case OFFICER_CAPTAIN: + mod += gGameExternalOptions.dEnemyOfficerMoraleModifier * 2; + break; + } + } + + return mod; +} + INT16 SOLDIERTYPE::GetInterruptModifier( UINT8 usDistance ) { INT16 bonus = 0; @@ -17086,12 +17148,22 @@ void SOLDIERTYPE::SoldierPropertyUpkeep() // effects eventually run out for (UINT8 counter = 0; counter < SOLDIER_COUNTER_MAX; ++counter) { - if ( counter == SOLDIER_COUNTER_SPOTTER && usSkillCounter[counter] > 0 ) + if ( counter == SOLDIER_COUNTER_ROLE_OBSERVED ) + continue; + else if ( counter == SOLDIER_COUNTER_SPOTTER && usSkillCounter[counter] > 0 ) usSkillCounter[counter] = min(255, usSkillCounter[counter] + 1 ); else usSkillCounter[counter] = max(0, usSkillCounter[counter] - 1 ); } + // if soldier was seen this turn, increase his observed counter + if ( this->bSoldierFlagMask & SOLDIER_ENEMY_OBSERVEDTHISTURN ) + { + this->bSoldierFlagMask &= ~SOLDIER_ENEMY_OBSERVEDTHISTURN; + + ++usSkillCounter[SOLDIER_COUNTER_ROLE_OBSERVED]; + } + // if there is a combat going and we are in sector, note that in the battle report if ( this->bInSector && (gTacticalStatus.uiFlags & INCOMBAT || gTacticalStatus.fEnemyInSector) ) { @@ -18199,6 +18271,94 @@ BOOLEAN SOLDIERTYPE::BecomeSpotter( INT32 sTargetGridNo ) return TRUE; } +// Flugente: enemy roles +BOOLEAN SOLDIERTYPE::HasSniper() +{ + INT8 invsize = (INT8)inv.size(); // remember inventorysize, so we don't call size() repeatedly + for ( INT8 bLoop = 0; bLoop < invsize; ++bLoop) + { + if (inv[bLoop].exists() && Item[inv[ bLoop ].usItem].usItemClass == IC_GUN && Weapon[Item[inv[bLoop].usItem].ubClassIndex].ubWeaponType == GUN_SN_RIFLE ) + { + return TRUE; + } + } + + return FALSE; +} + +// AI-only: can we heal a wounded ally? Do NOT, repeat, NOT use this with mercs! +BOOLEAN SOLDIERTYPE::CanMedicAI() +{ + if ( !gGameExternalOptions.fEnemyRoles || !gGameExternalOptions.fEnemyMedics || this->bTeam != ENEMY_TEAM ) + return FALSE; + + if ( HAS_SKILL_TRAIT( this, DOCTOR_NT) ) + { + if ( FindFirstAidKit(this) != NO_SLOT || FindMedKit(this) != NO_SLOT ) + return TRUE; + } + + return FALSE; +} + +// AI-only: heal a nearby friend. Do NOT, repeat, NOT use this with mercs! +BOOLEAN SOLDIERTYPE::AIDoctorFriend() +{ + if ( this->bTeam != ENEMY_TEAM ) + return FALSE; + + // we can only free people we are facing + INT32 nextGridNoinSight = NewGridNo( this->sGridNo, DirectionInc( this->ubDirection ) ); + + UINT8 target = WhoIsThere2( nextGridNoinSight, this->pathing.bLevel ); + + // is there somebody? + if ( target != NOBODY ) + { + SOLDIERTYPE* pSoldier = MercPtrs[target]; + + if ( pSoldier->bTeam != ENEMY_TEAM ) + return FALSE; + + // if this guy is wounded should always be the case, otherwise this function was calleed needlessly + if ( pSoldier->stats.bLife < pSoldier->stats.bLifeMax ) + { + // move medkit into hand - if we don't have a medkit in our hands, abort + if ( !MakeSureMedKitIsInHand( this ) ) + return FALSE; + + if( gAnimControl[ this->usAnimState ].ubEndHeight == ANIM_CROUCH ) + { + this->EVENT_InitNewSoldierAnim( START_AID, 0 , FALSE ); + } + + // AI medics always perform surgery + this->fDoingSurgery = TRUE; + + UINT16 usKitPts = TotalPoints( &(this->inv[ HANDPOS ] ) ); + + // note the current hp + INT8 oldlife = pSoldier->stats.bLife; + + UINT16 uiPointsUsed = this->SoldierDressWound( pSoldier, usKitPts, usKitPts ); + + UseKitPoints( &(this->inv[ HANDPOS ] ), (UINT16)(uiPointsUsed * gGameExternalOptions.dEnemyMedicMedKitDrainFactor), this ); + + // healing done will be displayed the next time the player sees this soldier + pSoldier->flags.fDisplayDamage = TRUE; + pSoldier->sDamage -= pSoldier->stats.bLife - oldlife; + + // alert both soldiers + this->aiData.bAlertStatus = min(this->aiData.bAlertStatus, STATUS_RED); + pSoldier->aiData.bAlertStatus = min(pSoldier->aiData.bAlertStatus, STATUS_RED); + + return TRUE; + } + } + + return FALSE; +} + INT32 CheckBleeding( SOLDIERTYPE *pSoldier ) { diff --git a/Tactical/Soldier Control.h b/Tactical/Soldier Control.h index fa584f46..28e3a508 100644 --- a/Tactical/Soldier Control.h +++ b/Tactical/Soldier Control.h @@ -384,10 +384,10 @@ enum #define SOLDIER_BATTLE_PARTICIPATION 0x01000000 //16777216 // campaign stats: soldier took part in this battle #define SOLDIER_RAISED_REDALERT 0x02000000 //33554432 // this (AI) soldier has raised red alert. Don't allow him to do so again this turn - either it already worked, or the signal is blocked -/*#define PLAYER_NET_3_LVL_3 0x04000000 //67108864 -#define PLAYER_NET_4_LVL_3 0x08000000 //134217728 +#define SOLDIER_ENEMY_OFFICER 0x04000000 //67108864 // soldier is an enemy officer +#define SOLDIER_ENEMY_OBSERVEDTHISTURN 0x08000000 //134217728 // enemy soldier was seen by the player this turn -#define PLAYER_NET_1_LVL_4 0x10000000 //268435456 +/*#define PLAYER_NET_1_LVL_4 0x10000000 //268435456 #define PLAYER_NET_2_LVL_4 0x20000000 //536870912 #define WH40K_SOLDIER_ILLUSION 0x40000000 //1073741824 // Soldier is an Illusion #define WH40K_SOLDIER_KILLTHISTURN 0x80000000 //2147483648 // Soldier is on a kill streak*/ @@ -565,6 +565,7 @@ enum{ enum { SOLDIER_COUNTER_RADIO_ARTILLERY, // there is actually no need for an artillery timer, but we use one to forbid the same AI guy ordering multiple strikes at once SOLDIER_COUNTER_SPOTTER, // used to determine wether we are a spotter + SOLDIER_COUNTER_ROLE_OBSERVED, // every turn that the player observes an enemy, the enemies counter is increased. If it is high enough, we know his role SOLDIER_COUNTER_MAX = 20, // enough space for fillers }; @@ -1779,6 +1780,7 @@ public: INT16 GetAPBonus(); INT8 GetFearResistanceBonus(); // fear resistance lowers shock and morale damage from horror UINT8 GetMoraleThreshold(); + FLOAT GetMoraleModifier(); INT16 GetInterruptModifier( UINT8 usDistance ); void SoldierPropertyUpkeep(); // update functions for various properties (updating counters, resetting flags etc.) @@ -1820,6 +1822,11 @@ public: BOOLEAN CanSpot( INT32 sTargetGridNo = -1 ); BOOLEAN BecomeSpotter( INT32 sTargetGridNo ); + // Flugente: enemy roles + BOOLEAN HasSniper(); + BOOLEAN CanMedicAI(); // AI-only: can we heal a wounded ally? Do NOT, repeat, NOT use this with mercs! + BOOLEAN AIDoctorFriend(); // AI-only: heal a wounded friend. Do NOT, repeat, NOT use this with mercs! + ////////////////////////////////////////////////////////////////////////////// }; // SOLDIERTYPE; diff --git a/Tactical/Soldier Create.cpp b/Tactical/Soldier Create.cpp index 3df0340e..594da3cd 100644 --- a/Tactical/Soldier Create.cpp +++ b/Tactical/Soldier Create.cpp @@ -1669,56 +1669,25 @@ BOOLEAN TacticalCopySoldierFromCreateStruct( SOLDIERTYPE *pSoldier, SOLDIERCREAT // We have a function for this type = pSoldier->GetSoldierProfileType( pCreateStruct->bTeam ); -/* if ( pCreateStruct->bTeam == ENEMY_TEAM && gGameExternalOptions.fSoldierProfiles_Enemy ) - { - switch( pCreateStruct->ubSoldierClass ) + if ( type > -1 ) { - case SOLDIER_CLASS_ELITE : - type = 2; - break; - case SOLDIER_CLASS_ARMY : - type = 1; - break; - case SOLDIER_CLASS_ADMINISTRATOR : - type = 0; - break; - } - } - else if ( pCreateStruct->bTeam == MILITIA_TEAM && gGameExternalOptions.fSoldierProfiles_Militia ) - { - switch( pCreateStruct->ubSoldierClass ) - { - case SOLDIER_CLASS_ELITE_MILITIA : - type = 5; - break; - case SOLDIER_CLASS_REG_MILITIA : - type = 4; - break; - case SOLDIER_CLASS_GREEN_MILITIA : - type = 3; - break; - } - }*/ - } + INT16 availablenames[NUM_SOLDIER_PROFILES]; + UINT16 cnt = 0; - if ( type > -1 ) - { - INT16 availablenames[NUM_SOLDIER_PROFILES]; - UINT16 cnt = 0; + for (UINT16 i = 1; i < num_found_soldier_profiles[type]; ++i) + { + // make sure the name isn't already currently in use! + if ( !IsProfileInUse(pCreateStruct->bTeam, type, i) ) + availablenames[cnt++] = i; + } - for (UINT16 i = 1; i < num_found_soldier_profiles[type]; ++i) - { - // make sure the name isn't already currently in use! - if ( !IsProfileInUse(pCreateStruct->bTeam, type, i) ) - availablenames[cnt++] = i; - } - - if ( cnt > 0 ) - { - pSoldier->usSoldierProfile = availablenames[Random(cnt)]; + if ( cnt > 0 ) + { + pSoldier->usSoldierProfile = availablenames[Random(cnt)]; - if ( zSoldierProfile[type][pSoldier->usSoldierProfile].uiBodyType > 0 && zSoldierProfile[type][pSoldier->usSoldierProfile].uiBodyType < 5 ) - pSoldier->ubBodyType = zSoldierProfile[type][pSoldier->usSoldierProfile].uiBodyType - 1; + if ( zSoldierProfile[type][pSoldier->usSoldierProfile].uiBodyType > 0 && zSoldierProfile[type][pSoldier->usSoldierProfile].uiBodyType < 5 ) + pSoldier->ubBodyType = zSoldierProfile[type][pSoldier->usSoldierProfile].uiBodyType - 1; + } } } } @@ -1737,6 +1706,22 @@ BOOLEAN TacticalCopySoldierFromCreateStruct( SOLDIERTYPE *pSoldier, SOLDIERCREAT AssignTraitsToSoldier( pSoldier, pCreateStruct ); if ( gGameExternalOptions.fAssignTraitsToMilitia && SOLDIER_CLASS_MILITIA( pSoldier->ubSoldierClass ) ) AssignTraitsToSoldier( pSoldier, pCreateStruct ); + + // Flugente: enemy roles + if ( gGameExternalOptions.fEnemyRoles && gGameExternalOptions.fEnemyOfficers && SOLDIER_CLASS_ENEMY( pSoldier->ubSoldierClass ) ) + { + if ( HAS_SKILL_TRAIT( pSoldier, SQUADLEADER_NT) ) + { + UINT8 numenemies = NumEnemyInSector(); + BOOL officertype = OFFICER_NONE; + UINT8 numofficers = HighestEnemyOfficersInSector( officertype ); + + // this guy becomes an officer if there are enough soldiers around, and we aren't already at max of officers + if ( numenemies > gGameExternalOptions.usEnemyOfficersPerTeamSize * numofficers && numofficers < gGameExternalOptions.usEnemyOfficersMax ) + pSoldier->bSoldierFlagMask |= SOLDIER_ENEMY_OFFICER; + } + } + // SANDRO - If neither of these two options are activated, use the original code // however, "no traits" means no traits at all, so commented out /*if ( !(gGameExternalOptions.fAssignTraitsToEnemy) && !(gGameExternalOptions.fAssignTraitsToMilitia) ) @@ -4649,6 +4634,8 @@ BOOLEAN AssignTraitsToSoldier( SOLDIERTYPE *pSoldier, SOLDIERCREATE_STRUCT *pCre BOOLEAN foundHtH = FALSE; BOOLEAN foundMelee = FALSE; BOOLEAN fRadioSetFound = FALSE; + BOOLEAN fFirstAidKitFound = FALSE; + BOOLEAN fMedKitFound = FALSE; // FIRST FIND OUT THE COMPOSITION OF OUR GEAR INT8 invsize = (INT8)pSoldier->inv.size(); @@ -4675,10 +4662,69 @@ BOOLEAN AssignTraitsToSoldier( SOLDIERTYPE *pSoldier, SOLDIERCREATE_STRUCT *pCre foundMelee = TRUE; else if ( HasItemFlag(pCreateStruct->Inv[bLoop].usItem, RADIO_SET) ) fRadioSetFound = TRUE; + else if ( Item[pCreateStruct->Inv[bLoop].usItem].firstaidkit ) + fFirstAidKitFound = TRUE; + else if ( Item[pCreateStruct->Inv[bLoop].usItem].medicalkit ) + { + // Flugente: for enemy medic purposes, med kits also count as first aid kits + fFirstAidKitFound = TRUE; + fMedKitFound = TRUE; + } } } // NOW BASED ON WHAT WE HAVE FOUND, ASSIGN UNASSIGNED TRAITS + // Flugente: radio operator + if ( gGameOptions.fNewTraitSystem && (!ATraitAssigned || !BTraitAssigned || !CTraitAssigned ) ) + { + // if we have a radio set, give us the corresponding trait so we can use it... + if ( fRadioSetFound ) + { + // this is a minor trait, so try to first fill the minor slot if possible + if ( ubSolClass == SOLDIER_CLASS_ELITE || ubSolClass == SOLDIER_CLASS_ELITE_MILITIA ) + { + pSoldier->stats.ubSkillTraits[2] = RADIO_OPERATOR_NT; + CTraitAssigned = TRUE; + return( TRUE ); // We no longer need to continue from here + } + else if ( !ATraitAssigned ) + { + pSoldier->stats.ubSkillTraits[0] = RADIO_OPERATOR_NT; + ATraitAssigned = TRUE; + } + else if ( !BTraitAssigned ) + { + pSoldier->stats.ubSkillTraits[1] = RADIO_OPERATOR_NT; + BTraitAssigned = TRUE; + } + } + } + + // Flugente: enemy roles allow medics + if ( gGameExternalOptions.fEnemyRoles && gGameExternalOptions.fEnemyMedics && gGameOptions.fNewTraitSystem && (!ATraitAssigned || !BTraitAssigned ) ) + { + // if we have a radio set, give us the corresponding trait so we can use it... + if ( fFirstAidKitFound ) + { + if ( !ATraitAssigned ) + { + pSoldier->stats.ubSkillTraits[0] = DOCTOR_NT; + ATraitAssigned = TRUE; + + // if we found a medkit, chance to become a full doctor + if ( !BTraitAssigned && fMedKitFound && Chance(50) ) + { + pSoldier->stats.ubSkillTraits[1] = DOCTOR_NT; + BTraitAssigned = TRUE; + } + } + else if ( !BTraitAssigned ) + { + pSoldier->stats.ubSkillTraits[1] = DOCTOR_NT; + BTraitAssigned = TRUE; + } + } + } // HEAVY WEAPONS TRAIT if ( foundMortar || foundRocketlauncher || foundGrenadelauncher ) @@ -5213,31 +5259,6 @@ BOOLEAN AssignTraitsToSoldier( SOLDIERTYPE *pSoldier, SOLDIERCREATE_STRUCT *pCre } } } - - // Flugente: new traits - if ( gGameOptions.fNewTraitSystem && (!ATraitAssigned || !BTraitAssigned || !CTraitAssigned ) ) - { - // if we have a radio set, give us the corresponding trait so we can use it... - if ( fRadioSetFound ) - { - if ( !ATraitAssigned ) - { - pSoldier->stats.ubSkillTraits[0] = RADIO_OPERATOR_NT; - ATraitAssigned = TRUE; - } - else if ( !BTraitAssigned ) - { - pSoldier->stats.ubSkillTraits[1] = RADIO_OPERATOR_NT; - BTraitAssigned = TRUE; - } - else if ( ubSolClass == SOLDIER_CLASS_ELITE || ubSolClass == SOLDIER_CLASS_ELITE_MILITIA ) - { - pSoldier->stats.ubSkillTraits[2] = RADIO_OPERATOR_NT; - CTraitAssigned = TRUE; - return( TRUE ); // We no longer need to continue from here - } - } - } } // RETURN TRUE IF ALL TRAITS ASSIGNED OTHERWISE FALSE if (( !gGameOptions.fNewTraitSystem && ATraitAssigned && BTraitAssigned ) || ( gGameOptions.fNewTraitSystem && ATraitAssigned && BTraitAssigned && CTraitAssigned ) ) diff --git a/Tactical/Turn Based Input.cpp b/Tactical/Turn Based Input.cpp index b8198f74..734f801e 100644 --- a/Tactical/Turn Based Input.cpp +++ b/Tactical/Turn Based Input.cpp @@ -276,6 +276,9 @@ void SwapMercPortraits ( SOLDIERTYPE *pSoldier, INT8 bDirection ); extern INT8 GetTeamSlotFromPlayerID( UINT8 ubID ); extern FACETYPE *gpCurrentTalkingFace; +// Flugente: toggle display of enemy role indicators +extern BOOLEAN gDisplayEnemyRoles; + // sevenfm: new mouse commands void HandleAltMouseTBWheel( void ); void HandleAltMouseTBMButton( UINT32 *puiNewEvent ); @@ -2999,7 +3002,7 @@ void GetKeyboardInput( UINT32 *puiNewEvent ) else { HandleTacticalCoverMenu(); - } + } } else { @@ -7820,7 +7823,8 @@ void HandleTacticalCoverMenu( void ) } gzUserDefinedButtonColor[0] = 0; gzUserDefinedButtonColor[1] = FONT_MCOLOR_LTYELLOW; - gzUserDefinedButtonColor[2] = FONT_MCOLOR_LTYELLOW; + gzUserDefinedButtonColor[2] = FONT_MCOLOR_LTYELLOW; + gzUserDefinedButtonColor[4] = FONT_LTRED; gzUserDefinedButtonColor[8] = FONT_ORANGE; gzUserDefinedButtonColor[9] = FONT_ORANGE; gzUserDefinedButtonColor[10] = FONT_ORANGE; @@ -7853,7 +7857,8 @@ void TacticalCoverMessageBoxCallBack( UINT8 ubExitValue ) break; case 4: break; - case 5: + case 5: + gDisplayEnemyRoles = !gDisplayEnemyRoles; break; case 6: break; diff --git a/Tactical/opplist.cpp b/Tactical/opplist.cpp index a53ce76c..fb1681e5 100644 --- a/Tactical/opplist.cpp +++ b/Tactical/opplist.cpp @@ -2170,7 +2170,12 @@ void ManSeesMan(SOLDIERTYPE *pSoldier, SOLDIERTYPE *pOpponent, INT32 sOppGridNo, if ( pOpponent->UsesScubaGear() ) return; // Flugente: update our sight concerning this guy, otherwise we could get way with open attacks because this does not get updated - pSoldier->RecognizeAsCombatant(pOpponent->ubID); + if ( pSoldier->RecognizeAsCombatant(pOpponent->ubID) ) + { + // Flugente: note that this enemy has been seen by mercs this turn + if ( pOpponent->bTeam == ENEMY_TEAM && pSoldier->bTeam == OUR_TEAM ) + pOpponent->bSoldierFlagMask |= SOLDIER_ENEMY_OBSERVEDTHISTURN; + } // if we're seeing a guy we didn't see on our last chance to look for him if (pSoldier->aiData.bOppList[pOpponent->ubID] != SEEN_CURRENTLY) @@ -2506,7 +2511,7 @@ void ManSeesMan(SOLDIERTYPE *pSoldier, SOLDIERTYPE *pOpponent, INT32 sOppGridNo, BOOLEAN fAddAsOpponent = FALSE; if ( !CONSIDERED_NEUTRAL( pSoldier, pOpponent ) && (pSoldier->bSide != pOpponent->bSide) && pSoldier->RecognizeAsCombatant(pOpponent->ubID) ) { - // ... check wether he is not neutral against us (account for the fact that we might be covert!) + // ... check wether he is not neutral against us (account for the fact that we might be covert!) // if we are an NPC assassin if ( pSoldier->bSoldierFlagMask & SOLDIER_ASSASSIN && pSoldier->bSoldierFlagMask & (SOLDIER_COVERT_CIV|SOLDIER_COVERT_SOLDIER) ) { diff --git a/TacticalAI/AIMain.cpp b/TacticalAI/AIMain.cpp index ec9da885..e0415d0e 100644 --- a/TacticalAI/AIMain.cpp +++ b/TacticalAI/AIMain.cpp @@ -2305,6 +2305,13 @@ INT8 ExecuteAction(SOLDIERTYPE *pSoldier) } break; + case AI_ACTION_DOCTOR: + { + pSoldier->AIDoctorFriend(); + ActionDone( pSoldier ); + } + break; + default: #ifdef BETAVERSION NumMessage("ExecuteAction - Illegal action type = ",pSoldier->aiData.bAction); diff --git a/TacticalAI/AIUtils.cpp b/TacticalAI/AIUtils.cpp index 33ae9c74..49cf1b8c 100644 --- a/TacticalAI/AIUtils.cpp +++ b/TacticalAI/AIUtils.cpp @@ -594,6 +594,10 @@ BOOLEAN IsActionAffordable(SOLDIERTYPE *pSoldier) bMinPointsNeeded = 10; // TODO break; + case AI_ACTION_DOCTOR: + bMinPointsNeeded = 20; // TODO + break; + default: #ifdef BETAVERSION //NumMessage("AffordableAction - Illegal action type = ",pSoldier->aiData.bAction); @@ -3064,40 +3068,44 @@ INT32 CalcStraightThreatValue( SOLDIERTYPE *pEnemy ) // Flugente: get the id of the closest soldier with a specific flag that we can currently see UINT8 GetClosestFlaggedSoldierID( SOLDIERTYPE * pSoldier, INT16 aRange, UINT8 auTeam, UINT32 aFlag ) { - UINT8 id = NOBODY; - + UINT8 id = NOBODY; UINT32 uiLoop; - BOOLEAN fRangeRestricted = FALSE, fFound = FALSE; SOLDIERTYPE * pFriend; INT16 range = aRange; - // go through each soldier, looking for "friends" (soldiers on same side) - for (uiLoop = 0; uiLoop < guiNumMercSlots; ++uiLoop) + // go through each soldier, looking for "friends" (soldiers on same team) + for (uiLoop = gTacticalStatus.Team[ auTeam ].bFirstID; uiLoop < gTacticalStatus.Team[ auTeam ].bLastID; ++uiLoop) { - pFriend = MercSlots[ uiLoop ]; + pFriend = MercPtrs[ uiLoop ]; // if this merc is inactive, not in sector, or dead if (!pFriend) continue; + + // skip ourselves + if (pFriend->ubID == pSoldier->ubID) + continue; + + // must be on the same level + if ( pFriend->pathing.bLevel != pSoldier->pathing.bLevel ) + continue; - if ( auTeam != pFriend->bTeam ) + // skip if this guy is dead + if ( pFriend->stats.bLife <= 0 ) continue; // check for flag if ( !(pFriend->bSoldierFlagMask & aFlag) ) continue; - - // skip ourselves - if (pFriend->ubID == pSoldier->ubID) - continue; - - // if we're not already neighbors - if (SpacesAway(pSoldier->sGridNo, pFriend->sGridNo) < range) + + // are we close enough? + INT16 friendrange = SpacesAway(pSoldier->sGridNo, pFriend->sGridNo); + if ( friendrange < range) { // can we see this guy? if ( SoldierTo3DLocationLineOfSightTest( pSoldier, pFriend->sGridNo, pSoldier->pathing.bLevel, 3, TRUE, CALC_FROM_WANTED_DIR ) ) { - range = SpacesAway(pSoldier->sGridNo,pFriend->sGridNo); + range = friendrange; id = pFriend->ubID; } } @@ -3105,3 +3113,48 @@ UINT8 GetClosestFlaggedSoldierID( SOLDIERTYPE * pSoldier, INT16 aRange, UINT8 au return id; } + +// get the id of the closest soldier (closer than x tiles) of a specific team that is wounded that we can currently see +UINT8 GetClosestWoundedSoldierID( SOLDIERTYPE * pSoldier, INT16 aRange, UINT8 auTeam ) +{ + UINT8 id = NOBODY; + UINT32 uiLoop; + SOLDIERTYPE * pFriend; + INT16 range = aRange; + + // go through each soldier, looking for "friends" (soldiers on same team) + for (uiLoop = gTacticalStatus.Team[ auTeam ].bFirstID; uiLoop < gTacticalStatus.Team[ auTeam ].bLastID; ++uiLoop) + { + pFriend = MercPtrs[ uiLoop ]; + + // if this merc is inactive, not in sector, or dead + if (!pFriend) + continue; + + // skip ourselves + if (pFriend->ubID == pSoldier->ubID) + continue; + + // must be on the same level + if ( pFriend->pathing.bLevel != pSoldier->pathing.bLevel ) + continue; + + // skip if this guy is dead, not wounded or cannot be healed + if ( pFriend->stats.bLife <= 0 || pFriend->iHealableInjury <= 0 || pFriend->stats.bLife >= pFriend->stats.bLifeMax ) + continue; + + // are we close enough? + INT16 friendrange = SpacesAway(pSoldier->sGridNo, pFriend->sGridNo); + if ( friendrange < range) + { + // can we see this guy? + if ( SoldierTo3DLocationLineOfSightTest( pSoldier, pFriend->sGridNo, pSoldier->pathing.bLevel, 3, TRUE, CALC_FROM_WANTED_DIR ) ) + { + range = friendrange; + id = pFriend->ubID; + } + } + } + + return id; +} \ No newline at end of file diff --git a/TacticalAI/DecideAction.cpp b/TacticalAI/DecideAction.cpp index 7208fdab..16173633 100644 --- a/TacticalAI/DecideAction.cpp +++ b/TacticalAI/DecideAction.cpp @@ -930,6 +930,48 @@ INT8 DecideActionGreen(SOLDIERTYPE *pSoldier) return( AI_ACTION_RED_ALERT ); } } + + // if we are a doctor with medical gear, we might be able to help a wounded ally + if ( pSoldier->CanMedicAI() ) + { + UINT8 ubPerson = GetClosestWoundedSoldierID( pSoldier, gGameExternalOptions.sEnemyMedicsSearchRadius, pSoldier->bTeam); + + if ( ubPerson != NOBODY ) + { + if ( PythSpacesAway(pSoldier->sGridNo, MercPtrs[ubPerson]->sGridNo) < 2 ) + { + // see if we are facing this person + UINT8 ubDesiredMercDir = atan8(CenterX(pSoldier->sGridNo),CenterY(pSoldier->sGridNo),CenterX(MercPtrs[ubPerson]->sGridNo),CenterY(MercPtrs[ubPerson]->sGridNo)); + + // if not already facing in that direction, + if ( pSoldier->ubDirection != ubDesiredMercDir ) + { + pSoldier->aiData.usActionData = ubDesiredMercDir; + + return( AI_ACTION_CHANGE_FACING ); + } + + // if not already crouched, crouch down first + if ( gAnimControl[ pSoldier->usAnimState ].ubHeight != ANIM_CROUCH && IsValidStance( pSoldier, ANIM_CROUCH ) && GetAPsToChangeStance( pSoldier, ANIM_CROUCH ) <= pSoldier->bActionPoints ) + { + pSoldier->aiData.usActionData = ANIM_CROUCH; + + return(AI_ACTION_CHANGE_STANCE); + } + + return(AI_ACTION_DOCTOR); + } + else + { + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, MercPtrs[ubPerson]->sGridNo, 20, AI_ACTION_SEEK_FRIEND, 0); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + return(AI_ACTION_SEEK_FRIEND); + } + } + } + } } //ddd} @@ -1528,7 +1570,6 @@ INT8 DecideActionYellow(SOLDIERTYPE *pSoldier) // IF YOU SEE CAPTURED FRIENDS, FREE THEM! //////////////////////////////////////////////////////////////////////////// - // Flugente: if we see one of our buddies in handcuffs, its a clear sign of enemy activity! // Flugente: if we see one of our buddies captured, it is a clear sign of enemy activity! if ( gGameExternalOptions.fAllowPrisonerSystem && pSoldier->bTeam == ENEMY_TEAM ) { @@ -1572,6 +1613,48 @@ INT8 DecideActionYellow(SOLDIERTYPE *pSoldier) } } } + + // if we are a doctor with medical gear, we might be able to help a wounded ally + if ( pSoldier->CanMedicAI() ) + { + UINT8 ubPerson = GetClosestWoundedSoldierID( pSoldier, gGameExternalOptions.sEnemyMedicsSearchRadius, pSoldier->bTeam); + + if ( ubPerson != NOBODY ) + { + if ( PythSpacesAway(pSoldier->sGridNo, MercPtrs[ubPerson]->sGridNo) < 2 ) + { + // see if we are facing this person + UINT8 ubDesiredMercDir = atan8(CenterX(pSoldier->sGridNo),CenterY(pSoldier->sGridNo),CenterX(MercPtrs[ubPerson]->sGridNo),CenterY(MercPtrs[ubPerson]->sGridNo)); + + // if not already facing in that direction, + if ( pSoldier->ubDirection != ubDesiredMercDir ) + { + pSoldier->aiData.usActionData = ubDesiredMercDir; + + return( AI_ACTION_CHANGE_FACING ); + } + + // if not already crouched, crouch down first + if ( gAnimControl[ pSoldier->usAnimState ].ubHeight != ANIM_CROUCH && IsValidStance( pSoldier, ANIM_CROUCH ) && GetAPsToChangeStance( pSoldier, ANIM_CROUCH ) <= pSoldier->bActionPoints ) + { + pSoldier->aiData.usActionData = ANIM_CROUCH; + + return(AI_ACTION_CHANGE_STANCE); + } + + return(AI_ACTION_DOCTOR); + } + else + { + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, MercPtrs[ubPerson]->sGridNo, 20, AI_ACTION_SEEK_FRIEND, 0); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + return(AI_ACTION_SEEK_FRIEND); + } + } + } + } } //////////////////////////////////////////////////////////////////////////// @@ -2854,6 +2937,48 @@ INT8 DecideActionRed(SOLDIERTYPE *pSoldier, UINT8 ubUnconsciousOK) } } } + + // if we are a doctor with medical gear, we might be able to help a wounded ally + if ( pSoldier->CanMedicAI() ) + { + UINT8 ubPerson = GetClosestWoundedSoldierID( pSoldier, gGameExternalOptions.sEnemyMedicsSearchRadius, pSoldier->bTeam); + + if ( ubPerson != NOBODY ) + { + if ( PythSpacesAway(pSoldier->sGridNo, MercPtrs[ubPerson]->sGridNo) < 2 ) + { + // see if we are facing this person + UINT8 ubDesiredMercDir = atan8(CenterX(pSoldier->sGridNo),CenterY(pSoldier->sGridNo),CenterX(MercPtrs[ubPerson]->sGridNo),CenterY(MercPtrs[ubPerson]->sGridNo)); + + // if not already facing in that direction, + if ( pSoldier->ubDirection != ubDesiredMercDir ) + { + pSoldier->aiData.usActionData = ubDesiredMercDir; + + return( AI_ACTION_CHANGE_FACING ); + } + + // if not already crouched, crouch down first + if ( gAnimControl[ pSoldier->usAnimState ].ubHeight != ANIM_CROUCH && IsValidStance( pSoldier, ANIM_CROUCH ) && GetAPsToChangeStance( pSoldier, ANIM_CROUCH ) <= pSoldier->bActionPoints ) + { + pSoldier->aiData.usActionData = ANIM_CROUCH; + + return(AI_ACTION_CHANGE_STANCE); + } + + return(AI_ACTION_DOCTOR); + } + else + { + pSoldier->aiData.usActionData = InternalGoAsFarAsPossibleTowards(pSoldier, MercPtrs[ubPerson]->sGridNo, 20, AI_ACTION_SEEK_FRIEND, 0); + + if (!TileIsOutOfBounds(pSoldier->aiData.usActionData)) + { + return(AI_ACTION_SEEK_FRIEND); + } + } + } + } } //////////////////////////////////////////////////////////////////////////// diff --git a/TacticalAI/ai.h b/TacticalAI/ai.h index f7f97ab9..9fbfaa3a 100644 --- a/TacticalAI/ai.h +++ b/TacticalAI/ai.h @@ -104,6 +104,7 @@ typedef enum AI_ACTION_JUMP_WINDOW, // added by Flugente: jump through a window AI_ACTION_FREE_PRISONER, // added by Flugente: free a prisoner AI_ACTION_USE_SKILL, // added by Flugente: perform a skill, which one is stored in usAISkillUse + AI_ACTION_DOCTOR, // added by Flugente: AI-ONLY! bandage/surgery on fellow AI. DO NOT USE THIS FOR MERCS!!! } ActionType; @@ -262,4 +263,7 @@ UINT8 NumberOfTeamMatesAdjacent( SOLDIERTYPE * pSoldier, INT32 sGridNo ); // Flugente: get the id of the closest soldier (coser than x tiles) of a specific team with a specific flag that we can currently see UINT8 GetClosestFlaggedSoldierID( SOLDIERTYPE * pSoldier, INT16 aRange, UINT8 auTeam, UINT32 aFlag ); +// get the id of the closest soldier (closer than x tiles) of a specific team that is wounded that we can currently see +UINT8 GetClosestWoundedSoldierID( SOLDIERTYPE * pSoldier, INT16 aRange, UINT8 auTeam ); + #endif diff --git a/Utils/_ChineseText.cpp b/Utils/_ChineseText.cpp index df3754ce..6755e3df 100644 --- a/Utils/_ChineseText.cpp +++ b/Utils/_ChineseText.cpp @@ -8479,7 +8479,7 @@ STR16 szTacticalCoverDialogString[]= L"Merc", L"", - L"", + L"Roles", // TODO.Translate L"", L"", L"", diff --git a/Utils/_DutchText.cpp b/Utils/_DutchText.cpp index 9d10a473..7975e8b9 100644 --- a/Utils/_DutchText.cpp +++ b/Utils/_DutchText.cpp @@ -8493,7 +8493,7 @@ STR16 szTacticalCoverDialogString[]= L"Merc", L"", - L"", + L"Roles", // TODO.Translate L"", L"", L"", diff --git a/Utils/_EnglishText.cpp b/Utils/_EnglishText.cpp index eac782ee..a1ce7b76 100644 --- a/Utils/_EnglishText.cpp +++ b/Utils/_EnglishText.cpp @@ -8477,7 +8477,7 @@ STR16 szTacticalCoverDialogString[]= L"Merc", L"", - L"", + L"Roles", L"", L"", L"", diff --git a/Utils/_FrenchText.cpp b/Utils/_FrenchText.cpp index 2035fd1e..323e7d41 100644 --- a/Utils/_FrenchText.cpp +++ b/Utils/_FrenchText.cpp @@ -8475,7 +8475,7 @@ STR16 szTacticalCoverDialogString[]= L"Merc.", L"", - L"", + L"Roles", // TODO.Translate L"", L"", L"", diff --git a/Utils/_GermanText.cpp b/Utils/_GermanText.cpp index fa7c548d..5f6bd03c 100644 --- a/Utils/_GermanText.cpp +++ b/Utils/_GermanText.cpp @@ -8303,7 +8303,7 @@ STR16 szTacticalCoverDialogString[]= L"Merc", L"", - L"", + L"Roles", // TODO.Translate L"", L"", L"", diff --git a/Utils/_ItalianText.cpp b/Utils/_ItalianText.cpp index 6557d650..69b6ba10 100644 --- a/Utils/_ItalianText.cpp +++ b/Utils/_ItalianText.cpp @@ -8485,7 +8485,7 @@ STR16 szTacticalCoverDialogString[]= L"Merc", L"", - L"", + L"Roles", // TODO.Translate L"", L"", L"", diff --git a/Utils/_PolishText.cpp b/Utils/_PolishText.cpp index 841474d6..f9e34f8c 100644 --- a/Utils/_PolishText.cpp +++ b/Utils/_PolishText.cpp @@ -8503,7 +8503,7 @@ STR16 szTacticalCoverDialogString[]= L"Merc", L"", - L"", + L"Roles", // TODO.Translate L"", L"", L"", diff --git a/Utils/_RussianText.cpp b/Utils/_RussianText.cpp index bfb4051b..42f5bd9e 100644 --- a/Utils/_RussianText.cpp +++ b/Utils/_RussianText.cpp @@ -8482,7 +8482,7 @@ STR16 szTacticalCoverDialogString[]= L"Merc", L"", - L"", + L"Roles", // TODO.Translate L"", L"", L"", diff --git a/Utils/_TaiwaneseText.cpp b/Utils/_TaiwaneseText.cpp index da9c167b..00117fef 100644 --- a/Utils/_TaiwaneseText.cpp +++ b/Utils/_TaiwaneseText.cpp @@ -8493,7 +8493,7 @@ STR16 szTacticalCoverDialogString[]= L"Merc", L"", - L"", + L"Roles", // TODO.Translate L"", L"", L"",