diff --git a/CREDITS.md b/CREDITS.md index 491db45ce1..6db4adcba2 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -151,6 +151,7 @@ This page lists all the individual contributions to the project by their author. - Warhead activation target health thresholds enhancements - Event 606: AttachEffect is attaching to a Techno - Linked superweapons + - Script actions for modifying AI anger against other houses - **Starkku**: - Misc. minor bugfixes & improvements - AI script actions: diff --git a/Phobos.vcxproj b/Phobos.vcxproj index dda0f91b28..92d452b78b 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -70,6 +70,7 @@ + diff --git a/docs/AI-Scripting-and-Mapping.md b/docs/AI-Scripting-and-Mapping.md index 32c9b24d71..0f65108283 100644 --- a/docs/AI-Scripting-and-Mapping.md +++ b/docs/AI-Scripting-and-Mapping.md @@ -382,6 +382,174 @@ In `aimd.ini`: x=14003,0 ``` +### `14005` Override OnlyTargetHouseEnemy Value + +- The value of the tag `OnlyTargetHouseEnemy` in AI triggers can be modified for the new attack & move actions. Only affects the next new attack or move action script. +- These anger values are applied only in the house owner of the team. +- Only works for new Phobos actions, not vanilla YR or Ares actions. + +In `aimd.ini`: +```ini +[SOMESCRIPTTYPE] ; ScriptType +x=14005,n ; integer n=-1 +``` + +- The possible argument values are: + +| *Argument* | *Description* | +| :--------: | :-------------------------------------------: | +| -1 | Use default value specified in `OnlyTargetHouseEnemy` | +| 0 | Force `OnlyTargetHouseEnemy` value to `FALSE` | +| 1 | Force `OnlyTargetHouseEnemy` value to `TRUE` | +| 2 | Force `OnlyTargetHouseEnemy` value to `TRUE` or `FALSE` randomly | + +### `14006` Set House Hate Value Modifier + +- Affects how much hate applies to a selected house (depends of the script action). +- Positive values increase hate and negative values decrease hate. +- Affects script actions: `14007`, `14008`, `14009`, `14010`, `14011` & `14012`. + +In `aimd.ini`: +```ini +[SOMESCRIPTTYPE] ; ScriptType +x=14006,n ; integer n=0 +``` + +### `14007` Modify House Hate Using House Index + +- Modifies the team's hate towards a specific house using its house index. + +In `aimd.ini`: +```ini +[SOMESCRIPTTYPE] ; ScriptType +x=14007,n ; integer n >= 0 +``` + +### `14008` Modify Hate Values From A List Of Countries + +- The house team picks a list of countries from the `rulesmd.ini` section called `[AIHousesList]`. +- The house team modify the hate towards all houses in the map that use the countries in that list. + +In `aimd.ini`: +```ini +[SOMESCRIPTTYPE] ; ScriptType +x=14008,n ; integer n >= 0 +``` + +The second parameter is a 0-based index for the `AIHousesList` section that specifies the list of possible `Countries` that can be evaluated. The new `AIHousesList` section must be declared in `rulesmd.ini` for making this script work: + +In `rulesmd.ini`: +```ini +[AIHousesList] ; List of Countries lists +0=SOMECOUNTRY,SOMEOTHERCOUNTRY,SAMPLECOUNTRY +1=ANOTHERCOUNTRY,YETANOTHERCOUNTRY +; ... +``` + +### `14009` Modify Hate Value Against A Random Country From A List Of Countries + +- Like action `14008` but the house owner of the Team only picks 1 house randomly from the specified list of countries. +- The house team modify the hate towards all houses in the map that use the selected country. + +In `aimd.ini`: +```ini +[SOMESCRIPTTYPE] ; ScriptType +x=14009,n ; integer n >= 0 +``` + +### `14010` Set The Most Hated House ("<" Comparison) + +- Increases the team house hate against an enemy house making that enemy house as the main target. + +In `aimd.ini`: +```ini +[SOMESCRIPTTYPE] ; ScriptType +x=14010,n ; integer +``` + +The possible argument values are: + +| *Argument* | *Description* | +| :------: | :-------------------------------------------: | +| -10 | The house with less factories is selected (excluded the aircraft factories) | +| -9 | The house with less aircraft docks is selected | +| -8 | The house with less naval units is selected | +| -7 | The house with less house kills is selected | +| -6 | The house with less free power (free = production - consumption) is selected | +| -5 | The house with less power production is selected | +| -4 | The house with less power consumption is selected | +| -3 | The nearest enemy Human base is selected | +| -2 | The poorest house is selected | +| -1 | The enemy house with nearest unit to the Team Leader is selected | +| > 0 | *Target Type#* index. The house with less threat of the selected *Target Type#* (sum of all the units of the same checked type * threat value) | + +### `14011` Set The Most Hated House (">" Comparison) + +- Increases the team house hate against an enemy house making that enemy house as the main target. + +In `aimd.ini`: +```ini +[SOMESCRIPTTYPE] ; ScriptType +x=14011,n ; integer +``` + +The possible argument values are: + +| *Argument* | *Description* | +| :--------: | :-------------------------------------------: | +| -10 | The house with more factories is selected (excluded the aircraft factories) | +| -9 | The house with more aircraft docks is selected | +| -8 | The house with more naval units is selected | +| -7 | The house with more kills is selected | +| -6 | The house with more free power (free = production - consumption) is selected | +| -5 | The house with more power production is selected | +| -4 | The house with more power consumption is selected | +| -3 | The farthest enemy Human base is selected | +| -2 | The richest house is selected | +| -1 | The enemy house with farthest unit to the Team Leader is selected | +| > 0 | *Target Type#* index. The house with more threat of the selected *Target Type#* (sum of all the units of the same checked type * threat value) | + +### `14012` Set The Most Hated House Randomly + +- Increases the Team house hate against an enemy house picked randomly. + +In `aimd.ini`: +```ini +[SOMESCRIPTTYPE] ; ScriptType +x=14012,0 +``` + +### `14013` Reset Hate Against Other Houses + +- All hate levels in the team house against every House are set to 0. + +In `aimd.ini`: +```ini +[SOMESCRIPTTYPE] ; ScriptType +x=14013,0 +``` + +### `14014` Set A House As The Most Hated House Of The Map + +- A House will become the most hated House of the map (the effects are only visible if the other houses are enemies of the selected house) + +In `aimd.ini`: +```ini +[SOMESCRIPTTYPE] ; ScriptType +x=14014,n ; integer +``` + +The possible argument values are: + +| *Argument* | *Description* | +| :--------: | :-------------------------------------------: | +| -5 | Selects a random House, including civilians. The own house is excluded in the selection of the most hated by everyone | +| -4 | Any random civilian house | +| -3 | All Human players will be hated by everyone | +| -2 | The Team House will be the most hated by everyone (allies won't pick allies as enemies) | +| -1 | Selects a random House. The own house & civilians are excluded in the selection of the most hated by everyone | +| >= 0 | House index that will be hated by everyone | + ### `16000-16999` Flow Control #### `16000` Start a Timed Jump to the Same Line diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 52818ffeaa..346eee72fa 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -2608,7 +2608,10 @@ OmniFire.TurnToTarget=no ; boolean - In addition to allowing custom radiation types, several enhancements are also available to the default radiation type defined in `[Radiation]`, such as ability to set owner & invoker or deal damage against buildings. See [Custom Radiation Types](#custom-radiation-types) for more details. -### Strafing aircraft weapon customization +### `500 - 523` Edit Variable +- Operate a variable's value + - The variable's value type is int16 instead of int32 in trigger actions for some reason, which means it ranges from -2^15 to 2^15-1. + - Any numbers exceeding this limit will lead to unexpected results! ![image](_static/images/strafing-01.gif) *Strafing aircraft weapon customization in [Project Phantom](https://www.moddb.com/mods/project-phantom)* diff --git a/docs/Whats-New.md b/docs/Whats-New.md index f8686b7488..e7873ec445 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -241,6 +241,15 @@ HideLightFlashEffects=false ; boolean 10102=Regroup Temporarily Around the Team Leader,20,0,1,[LONG DESC] 10103=Load Onto Transports,0,0,1,[LONG DESC] 10104=Chronoshift to Enemy Base,20,0,1,[LONG DESC] + 14006=Set House Hate Value Modifier,20,0,1,[LONG DESC] + 14007=Modify House Hate Using House Index,20,0,1,[LONG DESC] + 14008=Modify Hate Values From A List Of Countries,28,0,1,[LONG DESC] + 14009=Modify Hate Value Against A Random Country From A List Of Countries,28,0,1,[LONG DESC] + 14010=Set The Most Hated House ("<" Comparison),20,0,1,[LONG DESC] + 14011=Set The Most Hated House (">" Comparison),20,0,1,[LONG DESC] + 14012=Set The Most Hated House Randomly,0,0,1,[LONG DESC] + 14013=Reset Hate Against Other Houses,0,0,1,[LONG DESC] + 14014=Set A House As The Most Hated House Of The Map,20,0,1,[LONG DESC] 18000=Local variable set,22,0,1,[LONG DESC] 18001=Local variable add,22,0,1,[LONG DESC] 18002=Local variable minus,22,0,1,[LONG DESC] @@ -321,6 +330,10 @@ HideLightFlashEffects=false ; boolean 25=Local variables,-4 26=Global variables,-5 27=Global variables,-6 + 28=AI Houses List, -7 + + [ScriptParamTypes] + 7=AIHousesList,1,1,0 ``` ```` @@ -941,6 +954,7 @@ New: - Script action to regroup temporarily around the Team Leader (by FS-21) - Script action to randomly skip next action (by FS-21) - Script action for timed script action jumps (by FS-21) +- Script action for modifying AI anger against other houses (by FS-21) - ObjectInfo now shows current Target and AI Trigger data (by FS-21) - Shield absorption and passthrough customization (by Morton) - Limbo Delivery of buildings (by Morton) diff --git a/src/Commands/ObjectInfo.cpp b/src/Commands/ObjectInfo.cpp index 98d9768542..db74f90f7c 100644 --- a/src/Commands/ObjectInfo.cpp +++ b/src/Commands/ObjectInfo.cpp @@ -13,6 +13,7 @@ #include #include +#include const char* ObjectInfoCommandClass::GetName() const { diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index c30c2e8849..8d4e3308e0 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include @@ -350,6 +352,24 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) this->AIScriptsLists.emplace_back(std::move(objectsList)); } + + // Section AIHousesList + int houseItemsCount = pINI->GetKeyCount("AIHousesList"); + for (int i = 0; i < houseItemsCount; ++i) + { + std::vector objectsList; + + char* context = nullptr; + pINI->ReadString("AIHousesList", pINI->GetKeyName("AIHousesList", i), "", Phobos::readBuffer); + + for (char* cur = strtok_s(Phobos::readBuffer, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context)) + { + if (const auto pNewHouse = HouseTypeClass::Find(cur)) + objectsList.emplace_back(pNewHouse); + } + + this->AIHousesLists.emplace_back(std::move(objectsList)); + } } // this should load everything that TypeData is not dependant on @@ -389,6 +409,7 @@ void RulesExt::ExtData::Serialize(T& Stm) Stm .Process(this->AITargetTypesLists) .Process(this->AIScriptsLists) + .Process(this->AIHousesLists) .Process(this->Storage_TiberiumIndex) .Process(this->HarvesterDumpAmount) .Process(this->InfantryGainSelfHealCap) diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index 2584a189dc..5a0e1e92c3 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -31,6 +31,7 @@ class RulesExt public: std::vector> AITargetTypesLists; std::vector> AIScriptsLists; + std::vector> AIHousesLists; Valueable Storage_TiberiumIndex; Valueable HarvesterDumpAmount; diff --git a/src/Ext/Script/Body.AngerNodes.cpp b/src/Ext/Script/Body.AngerNodes.cpp new file mode 100644 index 0000000000..3b5d90f0b7 --- /dev/null +++ b/src/Ext/Script/Body.AngerNodes.cpp @@ -0,0 +1,752 @@ +#include "Body.h" + +#include + +void ScriptExt::ResetAngerAgainstHouses(TeamClass* pTeam) +{ + for (auto& angerNode : pTeam->Owner->AngerNodes) + angerNode.AngerLevel = 0; + + pTeam->Owner->EnemyHouseIndex = -1; + + // This action finished + pTeam->StepCompleted = true; // This action finished - FS-21 +} + +void ScriptExt::SetHouseAngerModifier(TeamClass* pTeam, int modifier = 0) +{ + auto pTeamData = TeamExt::ExtMap.Find(pTeam); + if (!pTeamData) + { + // This action finished + pTeam->StepCompleted = true; + return; + } + + if (modifier <= 0) + modifier = pTeam->CurrentScript->Type->ScriptActions[pTeam->CurrentScript->CurrentMission].Argument; + + if (modifier < 0) + modifier = 0; + + pTeamData->AngerNodeModifier = modifier; + + // This action finished + pTeam->StepCompleted = true; +} + +void ScriptExt::ModifyHateHouses_List(TeamClass* pTeam, int idxHousesList = -1) +{ + auto pTeamData = TeamExt::ExtMap.Find(pTeam); + if (!pTeamData) + { + // This action finished + pTeam->StepCompleted = true; + return; + } + + bool changeFailed = true; + + if (idxHousesList <= 0) + idxHousesList = pTeam->CurrentScript->Type->ScriptActions[pTeam->CurrentScript->CurrentMission].Argument; + + if (idxHousesList >= 0 + && idxHousesList < (int)RulesExt::Global()->AIHousesLists.size() + && RulesExt::Global()->AIHousesLists[idxHousesList].size() > 0) + { + std::vector objectsList = RulesExt::Global()->AIHousesLists[idxHousesList]; + + for (const auto pHouseType : objectsList) + { + for (auto& angerNode : pTeam->Owner->AngerNodes) + { + if (angerNode.House->IsObserver()) + continue; + + HouseTypeClass* angerNodeType = angerNode.House->Type; + + if (_stricmp(angerNodeType->ID, pHouseType->ID) == 0) + { + angerNode.AngerLevel += pTeamData->AngerNodeModifier; + changeFailed = false; + } + } + } + } + + // This action finished + if (changeFailed) + { + int currentMission = pTeam->CurrentScript->CurrentMission; + + pTeam->StepCompleted = true; + ScriptExt::Log("[%s][%s] (line: %d = %d,%d) - AngerNodes: Failed to modify AngerNode values against other houses.\n", + pTeam->Type->ID, + pTeam->CurrentScript->Type->ID, + pTeam->CurrentScript->CurrentMission, + pTeam->CurrentScript->Type->ScriptActions[currentMission].Action, + pTeam->CurrentScript->Type->ScriptActions[currentMission].Argument); + } + + ScriptExt::UpdateEnemyHouseIndex(pTeam->Owner); + + // This action finished + pTeam->StepCompleted = true; +} + +void ScriptExt::ModifyHateHouses_List1Random(TeamClass* pTeam, int idxHousesList = -1) +{ + auto pTeamData = TeamExt::ExtMap.Find(pTeam); + + if (!pTeamData || pTeamData->AngerNodeModifier == 0) + { + // This action finished + pTeam->StepCompleted = true; + return; + } + + int changes = 0; + int currentMission = pTeam->CurrentScript->CurrentMission; + + if (idxHousesList < 0) + { + idxHousesList = pTeam->CurrentScript->Type->ScriptActions[pTeam->CurrentScript->CurrentMission].Argument; + + if (idxHousesList < 0) + { + // This action finished + pTeam->StepCompleted = true; + ScriptExt::Log("[%s][%s] (line: %d = %d,%d) - AngerNodes: Invalid [AIHousesLists] index for modifying randomly anger values.\n", + pTeam->Type->ID, + pTeam->CurrentScript->Type->ID, + pTeam->CurrentScript->CurrentMission, + pTeam->CurrentScript->Type->ScriptActions[currentMission].Action, + pTeam->CurrentScript->Type->ScriptActions[currentMission].Argument); + + return; + } + } + + if (idxHousesList < (int)RulesExt::Global()->AIHousesLists.size() + && RulesExt::Global()->AIHousesLists[idxHousesList].size() > 0) + { + std::vector objectsList = RulesExt::Global()->AIHousesLists[idxHousesList]; + int IdxSelectedObject = ScenarioClass::Instance->Random.RandomRanged(0, objectsList.size() - 1); + HouseTypeClass* pHouseType = objectsList[IdxSelectedObject]; + + for (auto& angerNode : pTeam->Owner->AngerNodes) + { + if (angerNode.House->Defeated || angerNode.House->IsObserver()) + continue; + + HouseTypeClass* angerNodeType = angerNode.House->Type; + + if (_stricmp(angerNodeType->ID, pHouseType->ID) == 0) + { + angerNode.AngerLevel += pTeamData->AngerNodeModifier; + changes++; + } + } + } + + if (changes > 0) + { + ScriptExt::UpdateEnemyHouseIndex(pTeam->Owner); + } + else + { + ScriptExt::Log("[%s][%s] (line: %d = %d,%d) - AngerNodes: No anger values were modified.\n", + pTeam->Type->ID, + pTeam->CurrentScript->Type->ID, + pTeam->CurrentScript->CurrentMission, + pTeam->CurrentScript->Type->ScriptActions[currentMission].Action, + pTeam->CurrentScript->Type->ScriptActions[currentMission].Argument); + } + + // This action finished + pTeam->StepCompleted = true; +} + +void ScriptExt::SetTheMostHatedHouse(TeamClass* pTeam, int mask = 0, int mode = 1, bool random = false) +{ + auto pTeamData = TeamExt::ExtMap.Find(pTeam); + if (!pTeamData) + { + // This action finished + pTeam->StepCompleted = true; + return; + } + + if (mask == 0) + { + mask = pTeam->CurrentScript->Type->ScriptActions[pTeam->CurrentScript->CurrentMission].Argument; + + if (mask == 0) + { + // This action finished + pTeam->StepCompleted = true; + return; + } + } + + std::vector objectsList; + int idxSelectedObject = -1; + HouseClass* selectedHouse = nullptr; + int highestHateLevel = 0; + int newHateLevel = 5000; + + if (pTeamData->AngerNodeModifier > 0) + newHateLevel = pTeamData->AngerNodeModifier; + + // Find the highest House hate value + for (const auto& angerNode : pTeam->Owner->AngerNodes) + { + if (pTeam->Owner == angerNode.House + || angerNode.House->Defeated + || angerNode.House->Type->MultiplayPassive + || pTeam->Owner->IsAlliedWith(angerNode.House) + || angerNode.House->IsObserver()) + { + continue; + } + + if (random) + { + objectsList.emplace_back(angerNode.House); + } + else + { + if (angerNode.AngerLevel > highestHateLevel) + highestHateLevel = angerNode.AngerLevel; + } + } + + newHateLevel += highestHateLevel; + + // Pick a enemy house + if (random) + { + if (objectsList.size() > 0) + { + idxSelectedObject = ScenarioClass::Instance->Random.RandomRanged(0, objectsList.size() - 1); + selectedHouse = objectsList.at(idxSelectedObject); + } + } + else + { + selectedHouse = GetTheMostHatedHouse(pTeam, mask, mode); + } + + if (selectedHouse) + { + for (auto& angerNode : pTeam->Owner->AngerNodes) + { + if (angerNode.House->Defeated || angerNode.House->IsObserver()) + continue; + + if (angerNode.House == selectedHouse) + { + angerNode.AngerLevel = newHateLevel; + ScriptExt::Log("[%s][%s] (line: %d = %d,%d) - AngerNodes: Picked a new house as enemy [%s]\n", + pTeam->Type->ID, + pTeam->CurrentScript->Type->ID, + pTeam->CurrentScript->CurrentMission, + pTeam->CurrentScript->Type->ScriptActions[pTeam->CurrentScript->CurrentMission].Action, + pTeam->CurrentScript->Type->ScriptActions[pTeam->CurrentScript->CurrentMission].Argument, + angerNode.House->Type->ID); + } + } + + ScriptExt::UpdateEnemyHouseIndex(pTeam->Owner); + } + else + { + ScriptExt::Log("[%s][%s] (line: %d = %d,%d) - AngerNodes: Failed to pick a new hated house.\n", + pTeam->Type->ID, + pTeam->CurrentScript->Type->ID, + pTeam->CurrentScript->CurrentMission, + pTeam->CurrentScript->Type->ScriptActions[pTeam->CurrentScript->CurrentMission].Action, + pTeam->CurrentScript->Type->ScriptActions[pTeam->CurrentScript->CurrentMission].Argument); + } + + // This action finished + pTeam->StepCompleted = true; +} + +HouseClass* ScriptExt::GetTheMostHatedHouse(TeamClass* pTeam, int mask = 0, int mode = 1) +{ + auto pTeamData = TeamExt::ExtMap.Find(pTeam); + + if (!pTeamData || mask == 0) + { + // This action finished + pTeam->StepCompleted = true; + return nullptr; + } + + // Note regarding "mode": 1 is used for ">" comparisons and 0 for "<" + mode = mode <= 0 ? 0 : 1; + + // Find the Team Leader + FootClass* pLeaderUnit = FindTheTeamLeader(pTeam); + + if (!pLeaderUnit) + { + // This action finished + pTeam->StepCompleted = true; + return nullptr; + } + + bool currentMission = pTeam->CurrentScript->CurrentMission; + HouseClass* enemyHouse = nullptr; + int initialValue = -1; + double objectDistance = initialValue; + double enemyDistance = initialValue; + int currentNavalUnits = 0; + + if (mask <= -2 && mask >= -10) + { + int currentValue = 0; + int selectedValue = initialValue; + + // Is a house power check? It uses a different initial value that can't be reached in-game + if (mask == -4 || mask == -5 || mask == -6) + initialValue = -1000000000; + + for (const auto& pHouse : HouseClass::Array) + { + if (pLeaderUnit->Owner == pHouse + || pHouse->IsObserver() + || pHouse->Defeated + || pHouse->Type->MultiplayPassive + || pLeaderUnit->Owner->IsAlliedWith(pHouse)) + { + continue; + } + + if (mask == -3 && !pHouse->IsControlledByHuman()) // Only human players are valid here + continue; + + bool isValidCandidate = false; + currentValue = 0; + + switch (mask) + { + case -2: // Based on House economy + currentValue = pHouse->Available_Money(); + break; + + case -3: // Based on human controlled check + CoordStruct houseLocation; + houseLocation.X = pHouse->BaseSpawnCell.X; + houseLocation.Y = pHouse->BaseSpawnCell.Y; + houseLocation.Z = 0; + objectDistance = pLeaderUnit->Location.DistanceFrom(houseLocation); // Note: distance is in leptons (*256) + currentValue = objectDistance; // Note: distance is in leptons (*256) + break; + + case -4: // Related to the house's total power demand + currentValue = pHouse->Power_Drain(); + break; + + case -5: // Related to the house's total produced power + currentValue = pHouse->PowerOutput; + break; + + case -6: // Related to the house's unused power + currentValue = pHouse->PowerOutput - pHouse->Power_Drain(); + break; + + case -7: // Based on house's kills + currentValue = pHouse->TotalKilledBuildings + pHouse->TotalKilledUnits; + break; + + case -8: // Based on number of house's naval units + currentNavalUnits = 0; + + for (const auto& pUnit : TechnoClass::Array) + { + if (ScriptExt::IsUnitAvailable(pUnit, false) + && pUnit->Owner == pHouse + && ScriptExt::EvaluateObjectWithMask(pUnit, 31, -1, -1, nullptr)) + { + currentNavalUnits++; + } + } + + currentValue = currentNavalUnits; + break; + + case -9: // Based on number of House aircraft docks + currentValue = pHouse->AirportDocks; + break; + + case -10: // Based on number of house's factories (except aircraft factories) + currentValue = pHouse->NumWarFactories + pHouse->NumConYards + pHouse->NumShipyards + pHouse->NumBarracks; + break; + + default: + break; + } + + if (mode == 0) + isValidCandidate = currentValue < selectedValue; // The lowest is selected + else + isValidCandidate = currentValue > selectedValue; // The big one is selected + + if (isValidCandidate || selectedValue == initialValue) + { + selectedValue = currentValue; + enemyHouse = pHouse; + } + } + } + else if (mask == -1 || mask > 0) + { + // Other cases: Check all the technos and depending of the mode compare what house will be selected as the most hated + int nHouses = HouseClass::Array.Count; + std::vector enemyThreatValue = std::vector(nHouses); + enemyThreatValue[nHouses] = { 0.0 }; + double const& TargetSpecialThreatCoefficientDefault = RulesClass::Instance->TargetSpecialThreatCoefficientDefault; + + for (auto pTechno : TechnoClass::Array) + { + HouseClass* pHouse = pTechno->Owner; + + if (!ScriptExt::IsUnitAvailable(pTechno, false) + || pHouse->Defeated + || pHouse == pTeam->Owner + || pHouse->IsAlliedWith(pTeam->Owner) + || pHouse->Type->MultiplayPassive) + { + continue; + } + + if (mask > 0) // Threat based on the new attack types (or "quarry") used by the new attack actions + { + if (ScriptExt::EvaluateObjectWithMask(pTechno, mask, -1, -1, pLeaderUnit)) // Check if the object type is valid + { + if (auto const pTechnoType = pTechno->GetTechnoType()) + { + enemyThreatValue[pHouse->ArrayIndex] += pTechnoType->ThreatPosed; + + if (pTechnoType->SpecialThreatValue > 0) + enemyThreatValue[pHouse->ArrayIndex] += pTechnoType->SpecialThreatValue * TargetSpecialThreatCoefficientDefault; + } + } + } + else if (mask == -1) // Based on enemy object distances + { + objectDistance = pLeaderUnit->DistanceFrom(pTechno); // Note: distance is in leptons (*256) + bool isValidCandidate = false; + + if (mode == 0) + isValidCandidate = objectDistance < enemyDistance; // The house with the nearest enemy unit + else + isValidCandidate = objectDistance > enemyDistance; // The house with the farthest enemy unit + + if (isValidCandidate || enemyDistance == initialValue) + { + enemyDistance = objectDistance; + enemyHouse = pHouse; + } + } + } + + if (mask > 0) // Pick the house with major thread + { + double enemyThreat = initialValue; + + for (std::size_t i = 0; i < nHouses; i++) + { + auto const pHouse = HouseClass::Array.GetItem(i); + + if (pHouse->Defeated || pHouse->Type->MultiplayPassive || pHouse->IsObserver()) + continue; + + bool isValidCandidate = false; + + if (mode == 0) + isValidCandidate = enemyThreatValue[i] < enemyThreat; // The house with the nearest enemy unit + else + isValidCandidate = enemyThreatValue[i] > enemyThreat; // The house with the farthest enemy unit + + if (isValidCandidate || enemyThreat == initialValue) + { + enemyThreat = enemyThreatValue[i]; + enemyHouse = pHouse; + } + } + } + } + + if (enemyHouse) + { + ScriptExt::Log("[%s][%s] (line: %d = %d,%d) - AngerNodes: [%s] (index: %d) picked [%s] (index: %d).\n", + pTeam->Type->ID, + pTeam->CurrentScript->Type->ID, + currentMission, + pTeam->CurrentScript->Type->ScriptActions[currentMission].Action, + pTeam->CurrentScript->Type->ScriptActions[currentMission].Argument, + pTeam->Owner->Type->ID, + pTeam->Owner->ArrayIndex, + enemyHouse->Type->ID, + enemyHouse->ArrayIndex); + } + + return enemyHouse; +} + +// Possible mode values: +// 0 -> Force "False" +// 1 -> Force "True" +// 2 -> Force "Random boolean" +// -1 -> Use default value in OnlyTargetHouseEnemy tag +// Note: only works for new Phobos script actions, not the original ones +void ScriptExt::OverrideOnlyTargetHouseEnemy(TeamClass* pTeam, int mode = -1) +{ + auto pTeamData = TeamExt::ExtMap.Find(pTeam); + if (!pTeamData) + { + // This action finished + pTeam->StepCompleted = true; + return; + } + + if (mode < 0 || mode > 2) + mode = pTeam->CurrentScript->Type->ScriptActions[pTeam->CurrentScript->CurrentMission].Argument; + + if (mode < -1 || mode > 2) + mode = -1; + + pTeamData->OnlyTargetHouseEnemyMode = mode; + + switch (mode) + { + case 0: + pTeamData->OnlyTargetHouseEnemy = false; + break; + + case 1: + pTeamData->OnlyTargetHouseEnemy = true; + break; + + case 2: + pTeamData->OnlyTargetHouseEnemy = (bool)ScenarioClass::Instance->Random.RandomRanged(0, 1); + break; + + default: + pTeamData->OnlyTargetHouseEnemy = pTeam->Type->OnlyTargetHouseEnemy; + pTeamData->OnlyTargetHouseEnemyMode = -1; + break; + } + + int currentMission = pTeam->CurrentScript->CurrentMission; + ScriptExt::Log("[%s][%s] (line: %d = %d,%d) - AngerNodes: Team's 'OnlyTargetHouseEnemy' value overwrited. Now is '%d'.\n", + pTeam->Type->ID, + pTeam->CurrentScript->Type->ID, + currentMission, + pTeam->CurrentScript->Type->ScriptActions[currentMission].Action, + pTeam->CurrentScript->Type->ScriptActions[currentMission].Argument, + pTeamData->OnlyTargetHouseEnemy); + + // This action finished + pTeam->StepCompleted = true; +} + +void ScriptExt::ModifyHateHouse_Index(TeamClass* pTeam, int idxHouse = -1) +{ + auto pTeamData = TeamExt::ExtMap.Find(pTeam); + + if (!pTeamData || pTeamData->AngerNodeModifier == 0) + { + // This action finished + pTeam->StepCompleted = true; + return; + } + + int currentMission = pTeam->CurrentScript->CurrentMission; + + if (idxHouse < 0) + idxHouse = pTeam->CurrentScript->Type->ScriptActions[currentMission].Argument; + + if (idxHouse < 0) + { + // This action finished + pTeam->StepCompleted = true; + return; + } + + for (auto& angerNode : pTeam->Owner->AngerNodes) + { + if (angerNode.House->ArrayIndex == idxHouse + && !angerNode.House->Defeated + && !angerNode.House->IsObserver()) + { + angerNode.AngerLevel += pTeamData->AngerNodeModifier; + ScriptExt::Log("[%s][%s] (line: %d = %d,%d) - AngerNodes: Modified AngerNode level of [%s](index: %d) against house [%s](index: %d). Current hate value: %d\n", + pTeam->Type->ID, + pTeam->CurrentScript->Type->ID, + currentMission, + pTeam->CurrentScript->Type->ScriptActions[currentMission].Action, + pTeam->CurrentScript->Type->ScriptActions[currentMission].Argument, + pTeam->Owner->Type->ID, + pTeam->Owner->ArrayIndex, + angerNode.House->Type->ID, + angerNode.House->ArrayIndex, + angerNode.AngerLevel); + } + } + + ScriptExt::UpdateEnemyHouseIndex(pTeam->Owner); + + // This action finished + pTeam->StepCompleted = true; +} + +// The selected house will become the most hated of the map (the effects are only visible if the other houses are enemy of the selected house) +void ScriptExt::AggroHouse(TeamClass* pTeam, int index = -1) +{ + auto pTeamData = TeamExt::ExtMap.Find(pTeam); + if (!pTeamData) + { + // This action finished + pTeam->StepCompleted = true; + return; + } + + int currentMission = pTeam->CurrentScript->CurrentMission; + std::vector objectsList; + HouseClass* selectedHouse = nullptr; + int extraHateLevel = 5000; + bool onlySelectHumans = index == -3 ? true : false; + bool onlyCivilians = index == -4 ? true : false; + bool includeCivilians = index == -5 ? true : false; + + // Only if the additional was specified then overwrite the default value + if (pTeamData->AngerNodeModifier > 0) + extraHateLevel = pTeamData->AngerNodeModifier; + + if (index >= 0) // A specific house + { + selectedHouse = HouseClass::Array.GetItem(index); + objectsList.emplace_back(selectedHouse); + } + else if (index == -2) // Team's onwer as candidate + { + selectedHouse = pTeam->Owner; + objectsList.emplace_back(pTeam->Owner); + } + else + { + if (onlySelectHumans && pTeam->Owner->IsControlledByHuman()) // Include the team's onwer as candidate if only select human players + objectsList.emplace_back(pTeam->Owner); + + // Store the list of possible candidate houses for later + for (auto pCandidateHouse : HouseClass::Array) + { + if (pCandidateHouse->Defeated || pCandidateHouse->IsObserver() || (pCandidateHouse == pTeam->Owner)) + continue; + + if (onlySelectHumans) + { + if (pCandidateHouse->IsControlledByHuman()) + objectsList.emplace_back(pCandidateHouse); // Only Human players are candidate + } + else if(onlyCivilians && pCandidateHouse->Type->MultiplayPassive) + { + objectsList.emplace_back(pCandidateHouse); // Only civilians are candidate + } + else + { + if (!includeCivilians && pCandidateHouse->Type->MultiplayPassive) // Ignore civilians, just valid houses + continue; + + objectsList.emplace_back(pCandidateHouse); // Any valid house is candidate + } + } + } + + if (objectsList.size() == 0) + { + ScriptExt::Log("[%s][%s] (line: %d = %d,%d) - AngerNodes: [%s](index: %d) failed to pick a new house as main enemy using index '%d'.\n", + pTeam->Type->ID, + pTeam->CurrentScript->Type->ID, + currentMission, + pTeam->CurrentScript->Type->ScriptActions[currentMission].Action, + pTeam->CurrentScript->Type->ScriptActions[currentMission].Argument, + pTeam->Owner->Type->ID, + pTeam->Owner->ArrayIndex, + index); + + // No candidates. This action finished + pTeam->StepCompleted = true; + return; + } + + if (!selectedHouse && index != -3) // Candidates random index. Only humans case is excluded here + selectedHouse = objectsList[ScenarioClass::Instance->Random.RandomRanged(0, objectsList.size() - 1)]; + + for (auto pHouse : HouseClass::Array) + { + if (pHouse->Defeated || pHouse->IsObserver()) + continue; + + // For each valid house find the highest anger value and sum extra hate; + int highestHateLevel = -1; + + for (const auto& angerNode : pHouse->AngerNodes) + { + if (angerNode.AngerLevel > highestHateLevel) + highestHateLevel = angerNode.AngerLevel; + } + + highestHateLevel += extraHateLevel; + + // Find the houses that must be hated more than anyone + for (auto& angerNode : pHouse->AngerNodes) + { + if (index == -3) + { + // All humans will receive the highest hate value + if (angerNode.House->IsControlledByHuman()) + angerNode.AngerLevel = highestHateLevel; + } + else + { + // Find the select house and set it as the highest hated house + if (selectedHouse == angerNode.House) + angerNode.AngerLevel = highestHateLevel; + } + } + + ScriptExt::UpdateEnemyHouseIndex(pHouse); + } + + // This action finished + pTeam->StepCompleted = true; +} + +// The most hated house must be the main enemy +void ScriptExt::UpdateEnemyHouseIndex(HouseClass* pHouse) +{ + if (!pHouse) + return; + + int angerLevel = 0; + int index = -1; + + for (const auto& angerNode : pHouse->AngerNodes) + { + if (!angerNode.House->Defeated + && !angerNode.House->IsObserver() + && !pHouse->IsAlliedWith(angerNode.House) + && angerNode.AngerLevel > angerLevel) + { + angerLevel = angerNode.AngerLevel; + index = angerNode.House->ArrayIndex; + } + } + + pHouse->EnemyHouseIndex = index; +} diff --git a/src/Ext/Script/Body.cpp b/src/Ext/Script/Body.cpp index 4b16caee8d..2dd28b81c5 100644 --- a/src/Ext/Script/Body.cpp +++ b/src/Ext/Script/Body.cpp @@ -196,6 +196,39 @@ void ScriptExt::ProcessAction(TeamClass* pTeam) case PhobosScripts::RandomSkipNextAction: ScriptExt::SkipNextAction(pTeam); break; + case PhobosScripts::SetHouseAngerModifier: + ScriptExt::SetHouseAngerModifier(pTeam, 0); + break; + case PhobosScripts::OverrideOnlyTargetHouseEnemy: + ScriptExt::OverrideOnlyTargetHouseEnemy(pTeam, -1); + break; + case PhobosScripts::ModifyHateHouseIndex: + ScriptExt::ModifyHateHouse_Index(pTeam, -1); + break; + case PhobosScripts::ModifyHateHousesList: + ScriptExt::ModifyHateHouses_List(pTeam, -1); + break; + case PhobosScripts::ModifyHateHousesList1Random: + ScriptExt::ModifyHateHouses_List1Random(pTeam, -1); + break; + case PhobosScripts::SetTheMostHatedHouseMinorNoRandom: + // <, no random + ScriptExt::SetTheMostHatedHouse(pTeam, 0, 0, false); + break; + case PhobosScripts::SetTheMostHatedHouseMajorNoRandom: + // >, no random + ScriptExt::SetTheMostHatedHouse(pTeam, 0, 1, false); + break; + case PhobosScripts::SetTheMostHatedHouseRandom: + // random + ScriptExt::SetTheMostHatedHouse(pTeam, 0, 0, true); + break; + case PhobosScripts::ResetAngerAgainstHouses: + ScriptExt::ResetAngerAgainstHouses(pTeam); + break; + case PhobosScripts::AggroHouse: + ScriptExt::AggroHouse(pTeam, -1); + break; case PhobosScripts::StopForceJumpCountdown: // Stop Timed Jump ScriptExt::Stop_ForceJump_Countdown(pTeam); diff --git a/src/Ext/Script/Body.h b/src/Ext/Script/Body.h index 49e955c9db..9d261ca346 100644 --- a/src/Ext/Script/Body.h +++ b/src/Ext/Script/Body.h @@ -69,6 +69,16 @@ enum class PhobosScripts : unsigned int IncreaseCurrentAITriggerWeight = 14001, DecreaseCurrentAITriggerWeight = 14002, UnregisterGreatSuccess = 14003, + OverrideOnlyTargetHouseEnemy = 14005, + SetHouseAngerModifier = 14006, + ModifyHateHouseIndex = 14007, + ModifyHateHousesList = 14008, + ModifyHateHousesList1Random = 14009, + SetTheMostHatedHouseMinorNoRandom = 14010, + SetTheMostHatedHouseMajorNoRandom = 14011, + SetTheMostHatedHouseRandom = 14012, + ResetAngerAgainstHouses = 14013, + AggroHouse = 14014, // Range 16000-16999 are flow control actions (jumps, change script, loops, breaks, etc) SameLineForceJumpCountdown = 16000, @@ -209,6 +219,16 @@ class ScriptExt static void JumpBackToPreviousScript(TeamClass* pTeam); static void ChronoshiftToEnemyBase(TeamClass* pTeam, int extraDistance); + static void ResetAngerAgainstHouses(TeamClass* pTeam); + static void SetHouseAngerModifier(TeamClass* pTeam, int modifier); + static void ModifyHateHouses_List(TeamClass* pTeam, int idxHousesList); + static void ModifyHateHouses_List1Random(TeamClass* pTeam, int idxHousesList); + static void ModifyHateHouse_Index(TeamClass* pTeam, int idxHouse); + static void SetTheMostHatedHouse(TeamClass* pTeam, int mask, int mode, bool random); + static void OverrideOnlyTargetHouseEnemy(TeamClass* pTeam, int mode); + static void AggroHouse(TeamClass* pTeam, int index); + static HouseClass* GetTheMostHatedHouse(TeamClass* pTeam, int mask, int mode); + static bool IsExtVariableAction(int action); static void VariablesHandler(TeamClass* pTeam, PhobosScripts eAction, int nArg); template @@ -238,4 +258,5 @@ class ScriptExt static void ModifyCurrentTriggerWeight(TeamClass* pTeam, bool forceJumpLine = true, double modifier = 0); static bool MoveMissionEndStatus(TeamClass* pTeam, TechnoClass* pFocus, FootClass* pLeader = nullptr, int mode = 0); static void ChronoshiftTeamToTarget(TeamClass* pTeam, TechnoClass* pTeamLeader, AbstractClass* pTarget); + static void UpdateEnemyHouseIndex(HouseClass* pHouse); }; diff --git a/src/Ext/Team/Body.cpp b/src/Ext/Team/Body.cpp index 1ef8c952f6..ea51e1c299 100644 --- a/src/Ext/Team/Body.cpp +++ b/src/Ext/Team/Body.cpp @@ -22,6 +22,9 @@ void TeamExt::ExtData::Serialize(T& Stm) .Process(this->ForceJump_RepeatMode) .Process(this->TeamLeader) .Process(this->PreviousScriptList) + .Process(this->AngerNodeModifier) + .Process(this->OnlyTargetHouseEnemy) + .Process(this->OnlyTargetHouseEnemyMode) ; } diff --git a/src/Ext/Team/Body.h b/src/Ext/Team/Body.h index 71a531d4b8..4d854ea921 100644 --- a/src/Ext/Team/Body.h +++ b/src/Ext/Team/Body.h @@ -32,6 +32,9 @@ class TeamExt bool ForceJump_RepeatMode; FootClass* TeamLeader; std::vector PreviousScriptList; + int AngerNodeModifier; + bool OnlyTargetHouseEnemy; + int OnlyTargetHouseEnemyMode; ExtData(TeamClass* OwnerObject) : Extension(OwnerObject) , WaitNoTargetAttempts { 0 } @@ -47,6 +50,9 @@ class TeamExt , ForceJump_RepeatMode { false } , TeamLeader { nullptr } , PreviousScriptList { } + , AngerNodeModifier { 5000 } + , OnlyTargetHouseEnemy { false } + , OnlyTargetHouseEnemyMode { -1 } { } virtual ~ExtData() = default;