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!

*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;