diff --git a/CREDITS.md b/CREDITS.md index 3ecead2d75..4f755b38fd 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -158,6 +158,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 + - Customizable disguised target evaluation behaviour in new ScriptType attack actions - Unit & infantry auto-conversion on ammo change - Restore the ScriptType action#24 `Play speech` from Tiberian Sun - Modify ammo on impact diff --git a/docs/Fixed-or-Improved-Logics.md b/docs/Fixed-or-Improved-Logics.md index 96b988df86..c9fdf2b071 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -1939,6 +1939,16 @@ ChronoRangeMinimum= ; integer, can be used to set a small range within which ChronoDelay= ; integer, delay after teleport for chronosphere ``` +### Customizable disguised target evaluation behaviour in new ScriptType attack actions + +- By default, any unit with `DetectDisguise=yes` seeking targets via new [attack team missions introduced in Phobos](AI-Scripting-and-Mapping.md#10000-10049-attack-actions) always detects the disguised units. The probability to detect a disguised object now can be customized. +- `DetectDisguise.Percent` values represent the probabilities in hard, medium & easy difficulty, in that order. + +In `rulesmd.ini`: +```ini +[SOMETECHNO] ; TechnoType +DetectDisguise.Percent=1.0, 1.0, 1.0 ; float, percents or absolute +``` ### Customizable veterancy insignias - You can now customize veterancy insignia of TechnoTypes. diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 68b6030d64..23f5f6c493 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -421,6 +421,7 @@ HideShakeEffects=false ; boolean #### New: - [Customized transport plane for teams](AI-Scripting-and-Mapping.md#customized-transport-plane-for-teams) (by FlyStar) - [Modify ammo on impact](New-or-Enhanced-Logics.md#modify-ammo-on-impact) (by FS-21) +- [Customizable disguised target evaluation behaviour in new ScriptType attack actions](Fixed-or-Improved-Logics.md#customizable-disguised-target-evaluation-behaviour-in-new-scripttype-attack-actions) (by FS-21) - [Customize whether mind-controlled Insignificant technos can be auto-targeted](Fixed-or-Improved-Logics.md#customize-whether-mind-controlled-Insignificant-technos-can-be-auto-targeted) (by Noble_Fish) - [AutoDeath based on player power status and player credits](New-or-Enhanced-Logics.md#kill-object-automatically) (by Flactine) - [Roof production anim](New-or-Enhanced-Logics.md#roof-production-anim) (by Noble_Fish) @@ -1028,6 +1029,7 @@ HideShakeEffects=false ; boolean - Airstrike & spy plane fixed spawn distance & height (by Starkku) - Allow enabling application of `Verses` and `PercentAtMax` for negative damage (by Starkku) - In addition to `PlacementGrid.Translucency`, allow to set the transparency of the grid when `PlacementPreview` is enabled, using the `PlacementGrid.TranslucencyWithPreview` tag (by Belonit) +- Customizable disguised target evaluation behaviour in new ScriptType attack actions (by FS-21) - Show briefing screen on singleplayer mission start (by Starkku) - Allow setting mission par times and related messages in `missionmd.ini` (by Starkku) - Allow setting default singleplayer map loading screen and briefing offsets (by Starkku) diff --git a/src/Ext/Script/Body.h b/src/Ext/Script/Body.h index f1ce860587..02cc1a9c1b 100644 --- a/src/Ext/Script/Body.h +++ b/src/Ext/Script/Body.h @@ -234,7 +234,7 @@ class ScriptExt final : public AbstractExt // Mission.Attack.cpp static void Mission_Attack(TeamClass* pTeam, int calcThreatMode = 0, bool repeatAction = true, int attackAITargetType = -1, int idxAITargetTypeItem = -1); - static TechnoClass* GreatestThreat(TechnoClass* pTechno, int method, int calcThreatMode = 0, HouseClass* onlyTargetThisHouseEnemy = nullptr, int attackAITargetType = -1, int idxAITargetTypeItem = -1, bool agentMode = false); + static TechnoClass* GreatestThreat(TechnoClass* pTechno, int method, int calcThreatMode = 0, HouseClass* onlyTargetThisHouseEnemy = nullptr, int attackAITargetType = -1, int idxAITargetTypeItem = -1, bool agentMode = false, const std::vector& disguiseDetection = {}); static bool EvaluateObjectWithMask(TechnoClass* pTechno, int mask, int attackAITargetType = -1, int idxAITargetTypeItem = -1, TechnoClass* pTeamLeader = nullptr); static void Mission_Attack_List(TeamClass* pTeam, int calcThreatMode = 0, bool repeatAction = true, int attackAITargetType = -1); static void Mission_Attack_List1Random(TeamClass* pTeam, int calcThreatMode = 0, bool repeatAction = true, int attackAITargetType = -1); diff --git a/src/Ext/Script/Mission.Attack.cpp b/src/Ext/Script/Mission.Attack.cpp index 6dd2add122..54ede5ecca 100644 --- a/src/Ext/Script/Mission.Attack.cpp +++ b/src/Ext/Script/Mission.Attack.cpp @@ -17,6 +17,7 @@ void ScriptExt::Mission_Attack(TeamClass* pTeam, int calcThreatMode, bool repeat auto& waitNoTargetCounter = pTeamData->WaitNoTargetCounter; auto& waitNoTargetTimer = pTeamData->WaitNoTargetTimer; auto& waitNoTargetAttempts = pTeamData->WaitNoTargetAttempts; + std::vector disguiseDetection = {}; const auto pHouseExt = HouseExt::Fetch(pTeam->Owner); // When the new target wasn't found it sleeps some few frames before the new attempt. This can save cycles and cycles of unnecessary executed lines. @@ -139,6 +140,30 @@ void ScriptExt::Mission_Attack(TeamClass* pTeam, int calcThreatMode, bool repeat } pacifistTeam &= !ScriptExt::IsUnitArmed(pFoot, pTechnoType); + + auto pTechnoData = TechnoTypeExt::Fetch(pTechnoType); + if (pTechnoType->DetectDisguise) + { + auto const AIDifficulty = pFoot->Owner->GetAIDifficultyIndex(); + const auto& percent = pTechnoData->DetectDisguise_Percent.Get(); + double detectionValue = 1.0; + + switch (AIDifficulty) + { + case 0: + detectionValue = percent.X; + break; + case 1: + detectionValue = percent.Y; + break; + case 2: + default: + detectionValue = percent.Z; + break; + } + + disguiseDetection.push_back(detectionValue); + } } } @@ -195,7 +220,7 @@ void ScriptExt::Mission_Attack(TeamClass* pTeam, int calcThreatMode, bool repeat const auto& node = scriptActions[currentMission]; const int targetMask = node.Argument; // This is the target type - const auto pSelectedTarget = ScriptExt::GreatestThreat(pLeaderUnit, targetMask, calcThreatMode, enemyHouse, attackAITargetType, idxAITargetTypeItem, agentMode); + const auto pSelectedTarget = GreatestThreat(pLeaderUnit, targetMask, calcThreatMode, enemyHouse, attackAITargetType, idxAITargetTypeItem, agentMode, disguiseDetection); if (pSelectedTarget) { @@ -451,7 +476,7 @@ void ScriptExt::Mission_Attack(TeamClass* pTeam, int calcThreatMode, bool repeat } } -TechnoClass* ScriptExt::GreatestThreat(TechnoClass* pTechno, int method, int calcThreatMode, HouseClass* onlyTargetThisHouseEnemy, int attackAITargetType, int idxAITargetTypeItem, bool agentMode) +TechnoClass* ScriptExt::GreatestThreat(TechnoClass* pTechno, int method, int calcThreatMode, HouseClass* onlyTargetThisHouseEnemy, int attackAITargetType, int idxAITargetTypeItem, bool agentMode, const std::vector& disguiseDetection) { TechnoClass* pBestObject = nullptr; double bestVal = -1; @@ -501,6 +526,30 @@ TechnoClass* ScriptExt::GreatestThreat(TechnoClass* pTechno, int method, int cal if (!pTargetType->LegalTarget || (!skipImmune && pTargetType->Immune)) continue; + // Checking disguise logic, if the target isn't detected then it will be ignored + if (pTarget->IsDisguised()) + { + if (disguiseDetection.empty()) + continue; + + bool detected = false; + + for (double item : disguiseDetection) + { + int dice = ScenarioClass::Instance->Random.RandomRanged(0, 99); + int detectionValue = static_cast(std::round(item * 100.0)); + + if (detectionValue > dice) + { + detected = true; + break; + } + } + + if (!detected) + continue; + } + // Note: the TEAM LEADER is picked for this task, be careful with leadership values in your mod const auto pWeaponType = pTechno->GetWeapon(pTechno->SelectWeapon(pTarget))->WeaponType; diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index ae05a36241..84440419cf 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -968,6 +968,26 @@ void TechnoTypeExt::LoadFromINIFile(CCINIClass* const pINI) this->DeployFireWeapon.Read(exINI, pSection, "DeployFireWeapon"); this->TargetZoneScanType.Read(exINI, pSection, "TargetZoneScanType"); + this->DetectDisguise_Percent.Read(exINI, pSection, "DetectDisguise.Percent"); + { + auto& percent = *this->DetectDisguise_Percent.GetEx(); + if (percent.ValueCount == 1) + { + percent.Y = percent.X; + percent.Z = percent.X; + percent.ValueCount = 3; + } + else if (percent.ValueCount == 2) + { + percent.Z = percent.Y; + percent.ValueCount = 3; + } + + percent.X = std::clamp(percent.X, 0.0, 1.0); + percent.Y = std::clamp(percent.Y, 0.0, 1.0); + percent.Z = std::clamp(percent.Z, 0.0, 1.0); + } + this->AreaGuardRange.Read(exINI, pSection, "AreaGuardRange"); this->MaxGuardRange.Read(exINI, pSection, "MaxGuardRange"); @@ -1647,6 +1667,8 @@ void TechnoTypeExt::Serialize(T& Stm) .Process(this->SpawnsPipSize) .Process(this->SpawnsPipOffset) + .Process(this->DetectDisguise_Percent) + .Process(this->DroppodType) .Process(this->TiberiumEaterType) diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index c7c2ba9ed6..4bd0c235a9 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -300,6 +300,8 @@ class TechnoTypeExt : public ObjectTypeExt Valueable CrashSpin_Multiplier; + Valueable> DetectDisguise_Percent; + Nullable AINormalTargetingDelay; Nullable PlayerNormalTargetingDelay; Nullable AIGuardAreaTargetingDelay; @@ -722,6 +724,7 @@ class TechnoTypeExt : public ObjectTypeExt , MakesWake { } , CrashSpin_Multiplier { 1.0f } + , DetectDisguise_Percent { { 1.0, 1.0, 1.0 } } , AINormalTargetingDelay {} , PlayerNormalTargetingDelay {}