diff --git a/CREDITS.md b/CREDITS.md index ecf82be902..e63dd8b18c 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -692,6 +692,7 @@ This page lists all the individual contributions to the project by their author. - Fix an issue where units recruited by a team with `AreTeamMembersRecruitable=false` cannot be recruited even if they have been liberated by that team - Global default value for `DefaultToGuardArea` - Weapon range finding in cylinder + - Group retaliate - **solar-III (凤九歌)** - Target scanning delay customization (documentation) - Skip target scanning function calling for unarmed technos (documentation) diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 0e2c666d66..58ab37ea73 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -1745,6 +1745,7 @@ Note that this logic is used for [Passenger](https://modenc.renegadeprojects.com ``` ### Initial spawns number + - It is now possible to set the initial amount of spawnees for a spawner, instead of always being filled. Won't work if it's larger than `SpawnsNumber`. In `rulesmd.ini`: @@ -1753,6 +1754,34 @@ In `rulesmd.ini`: InitialSpawnsNumber= ; integer ``` +### Group retaliate + +- In vanilla, only the unit itself will counter - attack when it takes damage. This often makes units stupidly lured away one by one and eliminated. +- Now you can make units "seek support from nearby friendly units" when they take damage. They will retaliate the attacker together with the victim. + - `GroupRetaliate.AllowAI` and `GroupRetaliate.AllowPlayer` control whether this effect applies to AI units and player units respectively. + - The attacker's threat must be higher than the `GroupRetaliate.ThreatThreshold` of the unit's current target to trigger retaliation. + - `GroupRetaliate.GroupRange` controls the range within which the victim can call for friendly forces. + - `GroupRetaliate.TraceExtraRange` controls the distance for retaliating against the attacker. When the attacker is farther from the unit than the unit's guard range + TraceExtraRange, retaliation will not be triggered. +- An additional option `DisableVanillaRetaliateBehavior` is provided to turn off the vanilla retaliation behavior. + +In `rulesmd.ini`: +```ini +[CombatDamage] +GroupRetaliate.AllowAI=false ; boolean +GroupRetaliate.AllowPlayer=false ; boolean +GroupRetaliate.ThreatThreshold=1000.0 ; float, range in cell +GroupRetaliate.GroupRange=7.0 ; float, range in cell +GroupRetaliate.TraceExtraRange=20.0 ; float, range in cell +DisableVanillaRetaliateBehavior=false ; boolean + +[SOMETECHNO] ; TechnoType +GroupRetaliate.GroupRange= ; float, range in cell, default to [General] -> GroupRetaliate.GroupRange +``` + +```{note} +The unit must be able to trigger the vanilla retaliation behavior in order to trigger group retaliate. +``` + ### Initial strength for TechnoTypes and cloned infantry ![image](_static/images/initialstrength.cloning-01.png) diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 975aeed572..03088bc2e6 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -535,6 +535,7 @@ New: - Option to scale `PowerSurplus` setting if enabled to current power drain with `PowerSurplus.ScaleToDrainAmount` (by Starkku) - Global default value for `DefaultToGuardArea` (by TaranDahl) - [Weapon range finding in cylinder](New-or-Enhanced-Logics.md#range-finding-in-cylinder) (by TaranDahl) +- [Group retaliate](New-or-Enhanced-Logics.md#group-retaliate) (by TaranDahl) Vanilla fixes: - Fixed sidebar not updating queued unit numbers when adding or removing units when the production is on hold (by CrimRecya) diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index ca47eeb3f8..1bbb87aa1b 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -1,4 +1,4 @@ -#include "Body.h" +#include "Body.h" #include #include @@ -364,6 +364,15 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) this->CylinderRangefinding.Read(exINI, GameStrings::General, "CylinderRangefinding"); + this->GroupRetaliate_AllowAI.Read(exINI, GameStrings::CombatDamage, "GroupRetaliate.AllowAI"); + this->GroupRetaliate_AllowPlayer.Read(exINI, GameStrings::CombatDamage, "GroupRetaliate.AllowPlayer"); + this->GroupRetaliate_ThreatThreshold.Read(exINI, GameStrings::CombatDamage, "GroupRetaliate.ThreatThreshold"); + this->GroupRetaliate_GroupRange.Read(exINI, GameStrings::CombatDamage, "GroupRetaliate.GroupRange"); + this->GroupRetaliate_TraceExtraRange.Read(exINI, GameStrings::CombatDamage, "GroupRetaliate.TraceExtraRange"); + this->GroupRetaliate_Delay.Read(exINI, GameStrings::CombatDamage, "GroupRetaliate.Delay"); + + this->DisableVanillaRetaliateBehavior.Read(exINI, GameStrings::CombatDamage, "DisableVanillaRetaliateBehavior"); + // Section AITargetTypes int itemsCount = pINI->GetKeyCount("AITargetTypes"); for (int i = 0; i < itemsCount; ++i) @@ -663,6 +672,13 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->AIParadropMission) .Process(this->DefaultToGuardArea) .Process(this->CylinderRangefinding) + .Process(this->GroupRetaliate_AllowAI) + .Process(this->GroupRetaliate_AllowPlayer) + .Process(this->GroupRetaliate_ThreatThreshold) + .Process(this->GroupRetaliate_GroupRange) + .Process(this->GroupRetaliate_TraceExtraRange) + .Process(this->GroupRetaliate_Delay) + .Process(this->DisableVanillaRetaliateBehavior) ; } diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index 4001f0238b..42e2b58edf 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -313,6 +313,14 @@ class RulesExt Valueable DefaultToGuardArea; Valueable CylinderRangefinding; + Valueable GroupRetaliate_AllowAI; + Valueable GroupRetaliate_AllowPlayer; + Valueable GroupRetaliate_ThreatThreshold; + Valueable GroupRetaliate_GroupRange; + Valueable GroupRetaliate_TraceExtraRange; + Valueable GroupRetaliate_Delay; + + Valueable DisableVanillaRetaliateBehavior; ExtData(RulesClass* OwnerObject) : Extension(OwnerObject) , Storage_TiberiumIndex { -1 } @@ -570,6 +578,14 @@ class RulesExt , DefaultToGuardArea { false } , CylinderRangefinding { false } + , GroupRetaliate_AllowAI { false } + , GroupRetaliate_AllowPlayer { false } + , GroupRetaliate_ThreatThreshold { 1000.0 } + , GroupRetaliate_GroupRange { Leptons(7 * Unsorted::LeptonsPerCell) } + , GroupRetaliate_TraceExtraRange { Leptons(20 * Unsorted::LeptonsPerCell) } + , GroupRetaliate_Delay { 15 } + + , DisableVanillaRetaliateBehavior { false } { } virtual ~ExtData() = default; diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index dccbb0d0d0..e5e84dbd25 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -8,6 +8,7 @@ #include #include +#include TechnoExt::ExtContainer TechnoExt::ExtMap; UnitClass* TechnoExt::Deployer = nullptr; @@ -982,6 +983,150 @@ bool TechnoExt::EjectSurvivor(FootClass* pSurvivor, CoordStruct coords, bool sel return true; } +void TechnoExt::ApplyGroupRetaliate(TechnoClass* pThis, ObjectClass* pAttacker, WarheadTypeClass* pWH) +{ + auto range = TechnoTypeExt::ExtMap.Find(pThis->GetTechnoType())->GroupRetaliate_GroupRange.Get(RulesExt::Global()->GroupRetaliate_GroupRange); + + if (range <= 0) + return; + + auto canApproachTarget = [](TechnoClass* pTechno) -> bool + { + if ((pTechno->AbstractFlags & AbstractFlags::Foot) == AbstractFlags::None) + return false; + + auto mission = pTechno->GetCurrentMission(); + if (mission == Mission::Sticky) + return false; + + if (pTechno->DrainTarget) + return false; + + auto pUnit = abstract_cast(pTechno); + if (pUnit && TechnoExt::CannotMove(pUnit)) + return false; + + if (!pTechno->GetTechnoType()->CanApproachTarget) + return false; + + if (pTechno->BunkerLinkedItem) + return false; + + if (pTechno->InOpenToppedTransport) + return false; + + return true; + }; + auto missionAllowAreaSearch = [](Mission mission) + { + switch (mission) + { + case Mission::Patrol: + case Mission::Hunt: + case Mission::Area_Guard: + case Mission::Rescue: + return true; + default : + return false; + } + }; + auto missionAllowRangeSearch = [](Mission mission) + { + switch (mission) + { + case Mission::Move: + case Mission::Guard: + case Mission::Harvest: + return true; + default: + return false; + } + }; + auto canRespondGroupRetaliate = [pThis, pAttacker, pWH, range, canApproachTarget, missionAllowAreaSearch, missionAllowRangeSearch](TechnoClass* pTechno) -> bool + { + // Must be ally + if (!pTechno->Owner->IsAlliedWith(pThis)) + return false; + + // Skip if same target + if (pTechno->Target == pAttacker) + return false; + + // Check timer + if (!TechnoExt::ExtMap.Find(pTechno)->GroupRetaliateTimer.Expired()) + return false; + + // IsAI chcek + if (pTechno->Owner->IsControlledByHuman()) + { + if (!RulesExt::Global()->GroupRetaliate_AllowPlayer) + return false; + } + else + { + if (!RulesExt::Global()->GroupRetaliate_AllowAI) + return false; + } + + // Has less important target + if (auto pTarget = pTechno->Target) + { + auto attackerThreat = pTechno->ThreatCoeffients(pAttacker, &CoordStruct::Empty); + auto currentThreat = (pTechno->Target->AbstractFlags & AbstractFlags::Object) != AbstractFlags::None ? pTechno->ThreatCoeffients(static_cast(pTechno->Target), &CoordStruct::Empty) : 0.0; + + if (attackerThreat - currentThreat < RulesExt::Global()->GroupRetaliate_ThreatThreshold) + return false; + } + + // Must check mission + bool closeEnough = pTechno->IsCloseEnoughToAttack(pAttacker); + auto mission = pTechno->GetCurrentMission(); + bool attackMove = pTechno->MegaMissionIsAttackMove() && pTechno->InAuxiliarySearchRange(pAttacker); + bool canApproach = missionAllowAreaSearch(mission) && canApproachTarget(pTechno); // TODO : && InAreaStrayRange + bool canRangeFire = missionAllowRangeSearch(mission) && closeEnough; + + if (!attackMove && !canApproach && !canRangeFire) + return false; + + // Check simple can retaliate + if (!pTechno->CanRetaliateToAttacker(pAttacker, pWH)) + return false; + + // In search range + if (pThis->DistanceFrom(pTechno) > range) + return false; + + // Attacker not too far + if (pAttacker->DistanceFrom(pTechno) > (int)RulesExt::Global()->GroupRetaliate_TraceExtraRange.Get() + pTechno->GetGuardRange(0)) + return false; + + return true; + }; + auto callForHelp = [pThis, pAttacker, pWH, range, missionAllowRangeSearch](TechnoClass* pTechno) -> void + { + TechnoExt::ExtMap.Find(pTechno)->GroupRetaliateTimer.Start(RulesExt::Global()->GroupRetaliate_Delay); + pTechno->SetTarget(pAttacker); + + if (pTechno->MegaMissionIsAttackMove()) + { + pTechno->QueueMission(Mission::Attack, true); + //pTechno->SetDestination(nullptr, true); + ((FootClass*)pTechno)->HaveAttackMoveTarget = true; + } + else if (missionAllowRangeSearch(pTechno->GetCurrentMission())) + { + pTechno->ShouldLoseTargetNow = true; + } + }; + + auto list = Helpers::Alex::getCellSpreadItems(pThis->GetCoords(), (double)range / Unsorted::LeptonsPerCell, true); + for (auto techno : list) + { + if (canRespondGroupRetaliate(techno)) + callForHelp(techno); + } +} + // ============================= // load / save @@ -1053,6 +1198,7 @@ void TechnoExt::ExtData::Serialize(T& Stm) .Process(this->SpecialTracked) .Process(this->FallingDownTracked) .Process(this->JumpjetStraightAscend) + .Process(this->GroupRetaliateTimer) ; } diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index 41b6eb42c2..ad436a0fb9 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -100,6 +100,8 @@ class TechnoExt bool JumpjetStraightAscend; // Is set to true jumpjet units will ascend straight and do not adjust rotation or position during it. + CDTimerClass GroupRetaliateTimer; + ExtData(TechnoClass* OwnerObject) : Extension(OwnerObject) , TypeExtData { nullptr } , Shield {} @@ -166,6 +168,7 @@ class TechnoExt , SpecialTracked { false } , FallingDownTracked { false } , JumpjetStraightAscend { false } + , GroupRetaliateTimer {} { } void OnEarlyUpdate(); @@ -311,4 +314,6 @@ class TechnoExt static void ApplyKillWeapon(TechnoClass* pThis, TechnoClass* pSource, WarheadTypeClass* pWH); static void ApplyRevengeWeapon(TechnoClass* pThis, TechnoClass* pSource, WarheadTypeClass* pWH); static bool MultiWeaponCanFire(TechnoClass* const pThis, AbstractClass* const pTarget, WeaponTypeClass* const pWeaponType); + + static void ApplyGroupRetaliate(TechnoClass* pThis, ObjectClass* pAttacker, WarheadTypeClass* pWH); }; diff --git a/src/Ext/Techno/Hooks.ReceiveDamage.cpp b/src/Ext/Techno/Hooks.ReceiveDamage.cpp index cae211a938..8a3b735555 100644 --- a/src/Ext/Techno/Hooks.ReceiveDamage.cpp +++ b/src/Ext/Techno/Hooks.ReceiveDamage.cpp @@ -434,3 +434,41 @@ DEFINE_HOOK(0x701CFC, TechnoClass_ReceiveDamage_AllowBerzerkOnAllies, 0x5) enum { IgnoreOwnerCheckFailed = 0x701D0B }; return RulesExt::Global()->AllowBerzerkOnAllies ? IgnoreOwnerCheckFailed : 0; } + +#pragma region GroupRetaliate + +namespace GroupRetaliateContext +{ + bool Processing = false; +} + +DEFINE_HOOK(0x702A31, TechnoClass_ReceiveDamage_GroupRetaliate, 0x7) +{ + if (!RulesExt::Global()->GroupRetaliate_AllowAI && !RulesExt::Global()->GroupRetaliate_AllowPlayer) + return 0; + + GET_STACK(WarheadTypeClass*, pWH, STACK_OFFSET(0xC4, 0xC)); + GET_STACK(ObjectClass*, pAttacker, STACK_OFFSET(0xC4, 0x10)); + GET(TechnoClass*, pThis, ESI); + GroupRetaliateContext::Processing = true; + if (pAttacker) + TechnoExt::ApplyGroupRetaliate(pThis, pAttacker, pWH); + GroupRetaliateContext::Processing = false; + return 0; +} + +// Skip mission check in vanilla checker. We'll check it by ourselves. +DEFINE_HOOK(0x708A13, TechnoClass_CanRetaliateToAttacker_GroupRetaliate1, 0x6) +{ + return GroupRetaliateContext::Processing ? 0x708A2C : 0; +} + +DEFINE_HOOK(0x702A58, TechnoClass_ReceiveDamage_DisableVanilla, 0x5) +{ + enum { SkipGameCode = 0x702B47 }; + GET(TechnoClass*, pThis, ESI); + bool canGroupRetaliate = pThis->Owner->IsControlledByHuman() ? RulesExt::Global()->GroupRetaliate_AllowPlayer : RulesExt::Global()->GroupRetaliate_AllowAI; + return RulesExt::Global()->DisableVanillaRetaliateBehavior && canGroupRetaliate ? SkipGameCode : 0; +} + +#pragma endregion diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index 39560cc224..5709227c9f 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -1,4 +1,4 @@ -#include "Body.h" +#include "Body.h" #include @@ -1150,6 +1150,8 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->ParadropMission.Read(exINI, pSection, "ParadropMission"); this->AIParadropMission.Read(exINI, pSection, "AIParadropMission"); + this->GroupRetaliate_GroupRange.Read(exINI, pSection, "GroupRetaliate.GroupRange"); + // Ares 0.2 this->RadarJamRadius.Read(exINI, pSection, "RadarJamRadius"); @@ -1851,6 +1853,8 @@ void TechnoTypeExt::ExtData::Serialize(T& Stm) .Process(this->ParadropMission) .Process(this->AIParadropMission) + + .Process(this->GroupRetaliate_GroupRange) ; } void TechnoTypeExt::ExtData::LoadFromStream(PhobosStreamReader& Stm) diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index 7d161c89ca..a94d5028fa 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include #include @@ -474,6 +474,7 @@ class TechnoTypeExt Nullable ParadropMission; Nullable AIParadropMission; + Nullable GroupRetaliate_GroupRange; ExtData(TechnoTypeClass* OwnerObject) : Extension(OwnerObject) , HealthBar_Hide { false } @@ -903,6 +904,7 @@ class TechnoTypeExt , ParadropMission {} , AIParadropMission {} + , GroupRetaliate_GroupRange {} { } virtual ~ExtData() = default;