diff --git a/GameInitOptionsScreen.h b/GameInitOptionsScreen.h index b0dd9c379..6ed14320a 100644 --- a/GameInitOptionsScreen.h +++ b/GameInitOptionsScreen.h @@ -34,7 +34,10 @@ typedef struct UINT8 iCounterAttackGroupSize; BOOLEAN bUnlimitedPoolOfTroops; INT32 iQueensInitialPoolOfTroops; - UINT32 iQueenPoolIncrementPerDifficultyLevel; + INT32 iQueenPoolMaxSizePerDifficultyLevel; + UINT16 iQueenPoolIncrementDaysPerDifficultyLevel; + INT32 iQueenPoolBaseIncrementSizePerDifficultyLevel; + UINT8 iQueenPoolRecruitPercentPerDifficultyLevel; UINT32 iEnemyStartingAlertLevel; UINT32 iEnemyAlertDecay; UINT32 iNumAwareBattles; diff --git a/Strategic/ASD.cpp b/Strategic/ASD.cpp index c634b708c..8a8b38782 100644 --- a/Strategic/ASD.cpp +++ b/Strategic/ASD.cpp @@ -55,9 +55,7 @@ #endif -extern INT32 giReinforcementPool; extern BOOLEAN gfTacticalDoHeliRun; -extern BOOLEAN gfUnlimitedTroops; // Arulco special division decision code diff --git a/Strategic/Assignments.cpp b/Strategic/Assignments.cpp index f34e41052..9e06e8448 100644 --- a/Strategic/Assignments.cpp +++ b/Strategic/Assignments.cpp @@ -74,6 +74,7 @@ #include "MilitiaIndividual.h" // added by Flugente #include "Militia Control.h" // added by Flugente #include "ASD.h" // added by Flugente + #include "Strategic AI.h" #endif #include #include @@ -105,8 +106,6 @@ class SOLDIERTYPE; // Flugente: external sector data extern SECTOR_EXT_DATA SectorExternalData[256][4]; -// sevenfm -extern BOOLEAN gfUnlimitedTroops; // various reason an assignment can be aborted before completion enum{ @@ -7855,8 +7854,6 @@ BOOLEAN TrainTownInSector( SOLDIERTYPE *pTrainer, INT16 sMapX, INT16 sMapY, INT1 return ( FALSE ); } -extern INT32 giReinforcementPool; - void Interrogateprisoner(UINT8 aPrisonerType, FLOAT aChanceModifier, INT8& arMilitiaType, UINT32& arRansom, FLOAT& arIntel ) { arMilitiaType = -1; diff --git a/Strategic/Game Event Hook.cpp b/Strategic/Game Event Hook.cpp index 58b264280..e88d75222 100644 --- a/Strategic/Game Event Hook.cpp +++ b/Strategic/Game Event Hook.cpp @@ -660,6 +660,11 @@ BOOLEAN ExecuteStrategicEvent( STRATEGICEVENT *pEvent ) case EVENT_BANDIT_ATTACK: CreatureAttackTown_OtherCreatures( (UINT8)pEvent->uiParam, CREATURE_ATTACK_TYPE_BANDIT ); break; + + case EVENT_ARMY_FINISH_TRAINING: + giReinforcementPool += (INT32)pEvent->uiParam; + giTotalRecruitsInTraining -= (INT32)pEvent->uiParam; + break; } gfPreventDeletionOfAnyEvent = fOrigPreventFlag; return TRUE; diff --git a/Strategic/Game Event Hook.h b/Strategic/Game Event Hook.h index 3c0315a58..b07a86dc1 100644 --- a/Strategic/Game Event Hook.h +++ b/Strategic/Game Event Hook.h @@ -147,6 +147,7 @@ enum EVENT_BLOODCAT_ATTACK, // Flugente: like a creature attack, but with cats EVENT_ZOMBIE_ATTACK, // Flugente: like a creature attack, but with zombies EVENT_BANDIT_ATTACK, // Flugente: like a creature attack, but with bandits + EVENT_ARMY_FINISH_TRAINING, // Another bunch of Queen Army recruits is about to finish training NUMBER_OF_EVENT_TYPES_PLUS_ONE, NUMBER_OF_EVENT_TYPES = NUMBER_OF_EVENT_TYPES_PLUS_ONE - 1 diff --git a/Strategic/Game Events.cpp b/Strategic/Game Events.cpp index f593a8527..1a62807a9 100644 --- a/Strategic/Game Events.cpp +++ b/Strategic/Game Events.cpp @@ -130,6 +130,13 @@ CHAR16 gEventName[NUMBER_OF_EVENT_TYPES_PLUS_ONE][40]={ L"Weather Thunderstorm", L"Weather Sandstorm", L"Weather Snow", + L"Intel Enrico Email", + L"Intel Photofact verify", + L"Daily raid events", + L"bloodcat attack", + L"zombie attack", + L"bandit attack", + L"ArmyFinishTraining", }; #endif diff --git a/Strategic/Strategic AI.cpp b/Strategic/Strategic AI.cpp index 9cd0f0703..1957f6c5c 100644 --- a/Strategic/Strategic AI.cpp +++ b/Strategic/Strategic AI.cpp @@ -217,9 +217,10 @@ UINT8 gubMinEnemyGroupSize = 0; UINT8 gubHoursGracePeriod = 0; UINT16 gusPlayerBattleVictories = 0; BOOLEAN gfUseAlternateQueenPosition = FALSE; +INT32 giTotalRecruitsInTraining = 0; //padding for generic globals -#define SAI_PADDING_BYTES 97 +#define SAI_PADDING_BYTES 93 INT8 gbPadding[SAI_PADDING_BYTES]; //patrol group info plus padding #define SAVED_PATROL_GROUPS MAX_PATROL_GROUPS @@ -3440,14 +3441,18 @@ void EvaluateQueenSituation() uiOffset += dEnemyGeneralsSpeedupFactor * (zDiffSetting[gGameOptions.ubDifficultyLevel].iBaseDelayInMinutesBetweenEvaluations + Random( zDiffSetting[gGameOptions.ubDifficultyLevel].iEvaluationDelayVariance )); - // sevenfm: allow recruiting when pool size drops below iQueenPoolIncrementPerDifficultyLevel, this should result in more stable strategic AI behavior - if (giReinforcementPool <= 0 || !gfUnlimitedTroops && giReinforcementPool < zDiffSetting[gGameOptions.ubDifficultyLevel].iQueenPoolIncrementPerDifficultyLevel) + // Check/update reinforcements pool if old behavior is enabled + if ( !gfUnlimitedTroops && zDiffSetting[gGameOptions.ubDifficultyLevel].iQueenPoolIncrementDaysPerDifficultyLevel == 0 ) { - //Queen has run out of reinforcements. Simulate recruiting and training new troops - uiOffset *= 10; - giReinforcementPool += ( zDiffSetting[gGameOptions.ubDifficultyLevel].iQueenPoolIncrementPerDifficultyLevel * gGameOptions.ubDifficultyLevel ) * ( 100 + CurrentPlayerProgressPercentage() ) / 100 ; - AddStrategicEvent( EVENT_EVALUATE_QUEEN_SITUATION, GetWorldTotalMin() + uiOffset, 0 ); - return; + // sevenfm: allow recruiting when pool size drops below iQueenPoolBaseIncrementSizePerDifficultyLevel, this should result in more stable strategic AI behavior + if ( giReinforcementPool < zDiffSetting[gGameOptions.ubDifficultyLevel].iQueenPoolBaseIncrementSizePerDifficultyLevel ) + { + //Queen has run out of reinforcements. Simulate recruiting and training new troops + uiOffset *= 10; + giReinforcementPool += ( zDiffSetting[gGameOptions.ubDifficultyLevel].iQueenPoolBaseIncrementSizePerDifficultyLevel ) * ( 100 + CurrentPlayerProgressPercentage() ) / 100; + AddStrategicEvent( EVENT_EVALUATE_QUEEN_SITUATION, GetWorldTotalMin() + uiOffset, 0 ); + return; + } } //Re-post the event @@ -3624,9 +3629,14 @@ BOOLEAN SaveStrategicAI( HWFILE hFile ) FileWrite( hFile, &gfUseAlternateQueenPosition, 1, &uiNumBytesWritten ); if( uiNumBytesWritten != 1 ) return FALSE; + FileWrite( hFile, &giTotalRecruitsInTraining, sizeof( giTotalRecruitsInTraining ), &uiNumBytesWritten ); + if ( uiNumBytesWritten != sizeof( giTotalRecruitsInTraining ) ) + return FALSE; FileWrite( hFile, gbPadding, SAI_PADDING_BYTES, &uiNumBytesWritten ); if( uiNumBytesWritten != SAI_PADDING_BYTES ) return FALSE; + + //Save the army composition (which does get modified) FileWrite( hFile, gArmyComp, NUM_ARMY_COMPOSITIONS * sizeof( ARMY_COMPOSITION ), &uiNumBytesWritten ); if( uiNumBytesWritten != NUM_ARMY_COMPOSITIONS * sizeof( ARMY_COMPOSITION ) ) @@ -3769,10 +3779,13 @@ BOOLEAN LoadStrategicAI( HWFILE hFile ) FileRead( hFile, &gfUseAlternateQueenPosition, 1, &uiNumBytesRead ); if( uiNumBytesRead != 1 ) return FALSE; - FileRead( hFile, gbPadding, SAI_PADDING_BYTES, &uiNumBytesRead ); + FileRead( hFile, &giTotalRecruitsInTraining, sizeof( giTotalRecruitsInTraining ), &uiNumBytesRead ); + if ( uiNumBytesRead != sizeof( giTotalRecruitsInTraining ) ) + return FALSE; + FileRead( hFile, gbPadding, SAI_PADDING_BYTES, &uiNumBytesRead ); if( uiNumBytesRead != SAI_PADDING_BYTES ) return FALSE; - + //Restore the army composition FileRead( hFile, gArmyComp, NUM_ARMY_COMPOSITIONS * sizeof( ARMY_COMPOSITION ), &uiNumBytesRead ); if( uiNumBytesRead != NUM_ARMY_COMPOSITIONS * sizeof( ARMY_COMPOSITION ) ) diff --git a/Strategic/Strategic AI.h b/Strategic/Strategic AI.h index 7ef50ff28..7176e0dc1 100644 --- a/Strategic/Strategic AI.h +++ b/Strategic/Strategic AI.h @@ -81,7 +81,9 @@ extern BOOLEAN gfDisplayStrategicAILogs; extern BOOLEAN gfFirstBattleMeanwhileScenePending; extern UINT8 gubSAIVersion; - +extern BOOLEAN gfUnlimitedTroops; +extern INT32 giReinforcementPool; +extern INT32 giTotalRecruitsInTraining; //These enumerations define all of the various types of stationary garrison diff --git a/Strategic/Strategic Event Handler.cpp b/Strategic/Strategic Event Handler.cpp index 1d4cd2587..917e97dc5 100644 --- a/Strategic/Strategic Event Handler.cpp +++ b/Strategic/Strategic Event Handler.cpp @@ -29,6 +29,7 @@ #include "history.h" #include "Town Militia.h" // added by Flugente #include "Campaign.h" // added by Flugente + #include "Strategic AI.h" #endif #include "Luaglobal.h" @@ -826,6 +827,74 @@ void HandleNPCSystemEvent( UINT32 uiEvent ) //#endif } +static INT32 TakeVolunteers( UINT8 poolRecruitPerc ) +{ + INT32 loyalPopulation = 0; + + for ( UINT8 sX = 1; sX < MAP_WORLD_X - 1; ++sX ) + { + for ( UINT8 sY = 1; sY < MAP_WORLD_Y - 1; ++sY ) + { + if ( StrategicMap[CALCULATE_STRATEGIC_INDEX( sX, sY )].fEnemyControlled ) // if owned by the Army + { + UINT8 ubTownID = StrategicMap[CALCULATE_STRATEGIC_INDEX( sX, sY )].bNameId; + if ( ubTownID != BLANK_SECTOR ) + { + // Supposing rebel sentiment population will never be volunteers, Queen's or neutral rating % should be greater + // than sentiment %. Otherwise there is no volunteer people for sure. + // So if there are Queen loyal population, take sector population into account. + INT32 loyalPercent = MAX_LOYALTY_VALUE - gTownLoyalty[ubTownID].ubRating - gubTownRebelSentiment[ubTownID]; + if ( loyalPercent > 0 ) + { + loyalPopulation += GetSectorPopulation( sX, sY, FALSE ) * loyalPercent / 100; + } + } + } + } + } + + return loyalPopulation * poolRecruitPerc / 100; +} + +static INT32 TakeRecruitsByForce( UINT8 poolRecruitPerc, INT32 maxToTake ) +{ + INT32 recruits = 0; + + if ( poolRecruitPerc != 0 ) + { + for ( UINT8 sX = 1; sX < MAP_WORLD_X - 1; ++sX ) + { + if ( recruits >= maxToTake ) + break; + + for ( UINT8 sY = 1; sY < MAP_WORLD_Y - 1; ++sY ) + { + if ( recruits >= maxToTake ) + break; + + SECTORINFO *pSectorInfo = &( SectorInfo[SECTOR( sX, sY )] ); + if ( StrategicMap[CALCULATE_STRATEGIC_INDEX( sX, sY )].fEnemyControlled && pSectorInfo->ubGarrisonID != NO_GARRISON ) + { + UINT8 ubTownID = StrategicMap[CALCULATE_STRATEGIC_INDEX( sX, sY )].bNameId; + if ( ubTownID != BLANK_SECTOR ) + { + recruits += max( 1, GetSectorPopulation( sX, sY, FALSE ) * poolRecruitPerc / 100 ); + + // A possible drawback of forced recruiting is percent of unresting people increases. + // So increase Player's loyalty on 'poolRecruitPerc' value, but only if Player is known in this town. + if ( gTownLoyalty[ubTownID].ubRating > 0 ) + { + gTownLoyalty[ubTownID].ubRating = min( MAX_LOYALTY_VALUE, gTownLoyalty[ubTownID].ubRating + poolRecruitPerc ); + } + } + } + } + } + } + + return min( recruits, maxToTake ); +} + void HandleEarlyMorningEvents( void ) { // Flugente: no reason to put this into LUA @@ -845,6 +914,49 @@ void HandleEarlyMorningEvents( void ) } } + UINT16 poolIncDays = zDiffSetting[gGameOptions.ubDifficultyLevel].iQueenPoolIncrementDaysPerDifficultyLevel; + if ( !gfUnlimitedTroops && poolIncDays != 0 ) + { + INT32 poolMaxSize = zDiffSetting[gGameOptions.ubDifficultyLevel].iQueenPoolMaxSizePerDifficultyLevel; + INT32 poolIncSize = zDiffSetting[gGameOptions.ubDifficultyLevel].iQueenPoolBaseIncrementSizePerDifficultyLevel; + UINT8 poolRecruitPerc = zDiffSetting[gGameOptions.ubDifficultyLevel].iQueenPoolRecruitPercentPerDifficultyLevel; + + if ( giReinforcementPool < poolMaxSize ) + { + INT32 newRecruitsTaken = 0; + INT32 trainingCapacity = 0; // as first, count up training camps, then multiply it on QueenPoolBaseIncrementSizePerDifficultyLevel + + if ( IsTownUnderCompleteControlByPlayer( ALMA ) == FALSE ) + { + ++trainingCapacity; + } + if ( IsTownUnderCompleteControlByPlayer( MEDUNA ) == FALSE ) + { + ++trainingCapacity; + } + + trainingCapacity *= poolIncSize; + + if ( giTotalRecruitsInTraining < trainingCapacity ) + { + newRecruitsTaken += min( TakeVolunteers( poolRecruitPerc ), trainingCapacity - giTotalRecruitsInTraining ); + + if ( giTotalRecruitsInTraining + newRecruitsTaken < trainingCapacity ) + { + INT32 maxRecruitsToTake = trainingCapacity - giTotalRecruitsInTraining - newRecruitsTaken; + newRecruitsTaken += TakeRecruitsByForce( poolRecruitPerc, maxRecruitsToTake ); + } + + if ( newRecruitsTaken > 0 ) + { + UINT32 endTimeMin = GetWorldTotalMin() + poolIncDays * NUM_MIN_IN_DAY - 1; // -1 minute: training should end right before a new recruiting session, otherwise we may ignore some training space at moment of EarlyMorningEvent + AddStrategicEvent( EVENT_ARMY_FINISH_TRAINING, endTimeMin, (UINT32)newRecruitsTaken ); + giTotalRecruitsInTraining += newRecruitsTaken; + } + } + } + } + #ifdef LUA_STRATEGY_EVENT_HANDLER LetLuaHandleEarlyMorningEvents(0); #else diff --git a/Strategic/Strategic Town Loyalty.cpp b/Strategic/Strategic Town Loyalty.cpp index 64cccd4cb..903b7a0c9 100644 --- a/Strategic/Strategic Town Loyalty.cpp +++ b/Strategic/Strategic Town Loyalty.cpp @@ -46,9 +46,6 @@ #include "GameInitOptionsScreen.h" -// the max loyalty rating for any given town -#define MAX_LOYALTY_VALUE 100 - // loyalty Omerta drops to and maxes out at if the player betrays the rebels #define HOSTILE_OMERTA_LOYALTY_RATING 10 diff --git a/Strategic/Strategic Town Loyalty.h b/Strategic/Strategic Town Loyalty.h index 0de943e45..aa5ad5577 100644 --- a/Strategic/Strategic Town Loyalty.h +++ b/Strategic/Strategic Town Loyalty.h @@ -5,6 +5,8 @@ #include "mapscreen.h" #include "Soldier Control.h" +// the max loyalty rating for any given town +#define MAX_LOYALTY_VALUE 100 // gain pts per real loyalty pt #define GAIN_PTS_PER_LOYALTY_PT 500 @@ -90,6 +92,7 @@ typedef struct TOWN_LOYALTY // the loyalty variables for each town extern TOWN_LOYALTY gTownLoyalty[ MAX_TOWNS ]; +extern UINT8 gubTownRebelSentiment[MAX_TOWNS]; // town names list extern INT32 pTownNamesList[]; diff --git a/XML_DifficultySettings.cpp b/XML_DifficultySettings.cpp index 5d2cbecbb..02a68ab3c 100644 --- a/XML_DifficultySettings.cpp +++ b/XML_DifficultySettings.cpp @@ -58,7 +58,10 @@ DifficultySettingsParseDataStartElementHandle(void *userData, const XML_Char *na strcmp(name, "CounterAttackGroupSize") == 0 || strcmp(name, "UnlimitedPoolOfTroops") == 0 || strcmp(name, "QueensInitialPoolOfTroops") == 0 || - strcmp(name, "QueenPoolIncrementPerDifficultyLevel") == 0 || + strcmp(name, "QueenPoolMaxSizePerDifficultyLevel" ) == 0 || + strcmp(name, "QueenPoolIncrementDaysPerDifficultyLevel" ) == 0 || + strcmp(name, "QueenPoolBaseIncrementSizePerDifficultyLevel" ) == 0 || + strcmp(name, "QueenPoolRecruitPercentPerDifficultyLevel" ) == 0 || strcmp(name, "EnemyStartingAlertLevel") == 0 || strcmp(name, "EnemyAlertDecay") == 0 || strcmp(name, "NumAwareBattles") == 0 || @@ -169,7 +172,10 @@ difficultySettingsEndElementHandle(void *userData, const XML_Char *name) zDiffSetting[pData->curDifficultySettings.uiIndex].iCounterAttackGroupSize = pData->curDifficultySettings.iCounterAttackGroupSize; zDiffSetting[pData->curDifficultySettings.uiIndex].bUnlimitedPoolOfTroops = pData->curDifficultySettings.bUnlimitedPoolOfTroops; zDiffSetting[pData->curDifficultySettings.uiIndex].iQueensInitialPoolOfTroops = pData->curDifficultySettings.iQueensInitialPoolOfTroops; - zDiffSetting[pData->curDifficultySettings.uiIndex].iQueenPoolIncrementPerDifficultyLevel = pData->curDifficultySettings.iQueenPoolIncrementPerDifficultyLevel; + zDiffSetting[pData->curDifficultySettings.uiIndex].iQueenPoolMaxSizePerDifficultyLevel = pData->curDifficultySettings.iQueenPoolMaxSizePerDifficultyLevel; + zDiffSetting[pData->curDifficultySettings.uiIndex].iQueenPoolIncrementDaysPerDifficultyLevel = pData->curDifficultySettings.iQueenPoolIncrementDaysPerDifficultyLevel; + zDiffSetting[pData->curDifficultySettings.uiIndex].iQueenPoolBaseIncrementSizePerDifficultyLevel = pData->curDifficultySettings.iQueenPoolBaseIncrementSizePerDifficultyLevel; + zDiffSetting[pData->curDifficultySettings.uiIndex].iQueenPoolRecruitPercentPerDifficultyLevel = pData->curDifficultySettings.iQueenPoolRecruitPercentPerDifficultyLevel; zDiffSetting[pData->curDifficultySettings.uiIndex].iEnemyStartingAlertLevel = pData->curDifficultySettings.iEnemyStartingAlertLevel; zDiffSetting[pData->curDifficultySettings.uiIndex].iEnemyAlertDecay = pData->curDifficultySettings.iEnemyAlertDecay; zDiffSetting[pData->curDifficultySettings.uiIndex].iNumAwareBattles = pData->curDifficultySettings.iNumAwareBattles; @@ -314,13 +320,31 @@ difficultySettingsEndElementHandle(void *userData, const XML_Char *name) else if(strcmp(name, "QueensInitialPoolOfTroops") == 0) { pData->curElement = ELEMENT; - pData->curDifficultySettings.iQueensInitialPoolOfTroops = (INT32) atol(pData->szCharData); + pData->curDifficultySettings.iQueensInitialPoolOfTroops = max( 0, (INT32)atol( pData->szCharData ) ); } - else if(strcmp(name, "QueenPoolIncrementPerDifficultyLevel") == 0) + else if ( strcmp( name, "QueenPoolIncrementDaysPerDifficultyLevel" ) == 0 ) { pData->curElement = ELEMENT; - pData->curDifficultySettings.iQueenPoolIncrementPerDifficultyLevel = (UINT32) atol(pData->szCharData); - } + pData->curDifficultySettings.iQueenPoolIncrementDaysPerDifficultyLevel = (UINT16)atol( pData->szCharData ); + } + else if ( strcmp( name, "QueenPoolBaseIncrementSizePerDifficultyLevel" ) == 0 ) + { + pData->curElement = ELEMENT; + pData->curDifficultySettings.iQueenPoolBaseIncrementSizePerDifficultyLevel = (INT32)atol( pData->szCharData ); + pData->curDifficultySettings.iQueenPoolBaseIncrementSizePerDifficultyLevel = max( 0, pData->curDifficultySettings.iQueenPoolBaseIncrementSizePerDifficultyLevel ); + } + else if ( strcmp( name, "QueenPoolRecruitPercentPerDifficultyLevel" ) == 0 ) + { + pData->curElement = ELEMENT; + pData->curDifficultySettings.iQueenPoolRecruitPercentPerDifficultyLevel = (UINT8)atol( pData->szCharData ); + pData->curDifficultySettings.iQueenPoolRecruitPercentPerDifficultyLevel = min( 100, pData->curDifficultySettings.iQueenPoolRecruitPercentPerDifficultyLevel ); + } + else if ( strcmp( name, "QueenPoolMaxSizePerDifficultyLevel" ) == 0 ) + { + pData->curElement = ELEMENT; + pData->curDifficultySettings.iQueenPoolMaxSizePerDifficultyLevel = (INT32)atol( pData->szCharData ); + pData->curDifficultySettings.iQueenPoolMaxSizePerDifficultyLevel = max( 0, pData->curDifficultySettings.iQueenPoolMaxSizePerDifficultyLevel ); + } else if(strcmp(name, "EnemyStartingAlertLevel") == 0) { pData->curElement = ELEMENT;