diff --git a/CREDITS.md b/CREDITS.md index ff4254c04c..ab2f66aeb9 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -656,7 +656,9 @@ This page lists all the individual contributions to the project by their author. - **solar-III (凤九歌)** - Target scanning delay customization (documentation) - Skip target scanning function calling for unarmed technos (documentation) -- **Flactine** - add target filtering options to attacheffect system +- **Flactine** + - Add target filtering options to attacheffect system + - Add veterancy-based target filtering for weapons and warheads - **tyuah8**: - Drive/Jumpjet/Ship/Teleport locomotor did not power on when it is un-piggybacked bugfix - Destroyed unit leaves sensors bugfix diff --git a/docs/Fixed-or-Improved-Logics.md b/docs/Fixed-or-Improved-Logics.md index aa752884be..d158cd4d73 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -2312,6 +2312,12 @@ Conventional.IgnoreUnits=false ; boolean ### Customizable Warhead trigger conditions - `AffectsBelowPercent` and `AffectsAbovePercent` can be used to set the health percentage thresholds that target needs to be below/equal and/or above of for the Warhead to detonate. If target has zero health left this check is bypassed. +- `AffectsAboveVeterancy` and `AffectsBelowVeterancy` can be used to set veterancy thresholds for Warhead activation. The target's veterancy must be greater than or equal to `AffectsAboveVeterancy`, and strictly less than `AffectsBelowVeterancy`. + - Veterancy values are interpreted as follows: + - `0.0` = Rookie + - `1.0` = Veteran + - `2.0` = Elite + - TechnoTypes with `Trainable=no` are always treated as having a veterancy level of `0.0` (Rookie) for the purpose of this check. - If set to `false`, `AffectsNeutral` makes the warhead can't damage or affect target that belongs to neutral house. - If set to `false`, `EffectsRequireVerses` makes the Phobos-introduced warhead effects trigger even if it can't damage the target because of it's current ArmorType (e.g. 0% in `Verses`). @@ -2320,6 +2326,8 @@ In `rulesmd.ini`: [SOMEWARHEAD] ; WarheadType AffectsBelowPercent=1.0 ; floating point value, percents or absolute AffectsAbovePercent=0.0 ; floating point value, percents or absolute +AffectsBelowVeterancy= ; floating point value, percents or absolute, default to [General] -> VeteranCap + 1.0 +AffectsAboveVeterancy=0.0 ; floating point value, percents or absolute AffectsNeutral=true ; boolean EffectsRequireVerses=false ; boolean ``` diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index c1547abe9c..554da38bbf 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -2848,14 +2848,22 @@ This function is only used as an additional scattering visual display, which is - You can now specify which targets or houses a weapon can fire at. This also affects weapon selection, other than certain special cases where the selection is fixed. - `CanTarget.MaxHealth` and `CanTarget.MinHealth` set health percentage thresholds for allowed targets (TechnoTypes only) that the target's health must be above and/or below/equal to, respectively. If target has zero health left this check is bypassed. - -In `rulesmd.ini`: -```ini -[SOMEWEAPON] ; WeaponType -CanTarget=all ; List of Affected Target Enumeration (none|land|water|empty|infantry|units|buildings|all) -CanTargetHouses=all ; List of Affected House Enumeration (none|owner/self|allies/ally|team|enemies/enemy|all) -CanTarget.MaxHealth=1.0 ; floating point value, percents or absolute -CanTarget.MinHealth=0.0 ; floating point value, percents or absolute + - `CanTarget.MinVeterancy` and `CanTarget.MaxVeterancy` define the allowed veterancy range for targets. The target's veterancy must be greater than or equal to `MinVeterancy` and less than to `MaxVeterancy` in order to be considered a valid target. + - Veterancy values are interpreted as follows: + - `0.0` = Rookie + - `1.0` = Veteran + - `2.0` = Elite + - TechnoTypes with `Trainable=no` are always treated as having a veterancy level of `0.0` (Rookie) for the purpose of this check. + +In `rulesmd.ini`: +```ini +[SOMEWEAPON] ; WeaponType +CanTarget=all ; List of Affected Target Enumeration (none|land|water|empty|infantry|units|buildings|all) +CanTargetHouses=all ; List of Affected House Enumeration (none|owner/self|allies/ally|team|enemies/enemy|all) +CanTarget.MaxHealth=1.0 ; floating point value, percents or absolute +CanTarget.MinHealth=0.0 ; floating point value, percents or absolute +CanTarget.MaxVeterancy= ; floating point value, percents or absolute, default to [General] -> VeteranCap + 1.0 +CanTarget.MinVeterancy=0.0 ; floating point value, percents or absolute ``` ```{note} diff --git a/docs/Whats-New.md b/docs/Whats-New.md index baf3c1e532..113e0c5939 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -469,6 +469,8 @@ New: - [CellSpread damage check if victim is in air or on floor](New-or-Enhanced-Logics.md#cellspread-enhancement) (by TaranDahl) - OpenTopped range bonus and damage multiplier customization for passengers (by Ollerus) - AutoDeath upon ownership change (by Ollerus) +- [Weapon target filtering by target veterancy](New-or-Enhanced-Logics.md#weapon-targeting-filter) (by Flactine) +- [Warhead effect filtering by target veterancy](Fixed-or-Improved-Logics.md#customizable-warhead-trigger-conditions) (by Flactine) 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/Bullet/Hooks.DetonateLogics.cpp b/src/Ext/Bullet/Hooks.DetonateLogics.cpp index ac07a6e057..ce20daa67c 100644 --- a/src/Ext/Bullet/Hooks.DetonateLogics.cpp +++ b/src/Ext/Bullet/Hooks.DetonateLogics.cpp @@ -26,7 +26,7 @@ DEFINE_HOOK(0x4690D4, BulletClass_Logics_NewChecks, 0x6) if (auto const pTarget = abstract_cast(pBullet->Target)) { // Check if the WH should affect the techno target or skip it - if (!pExt->IsHealthInThreshold(pTarget) || (!pExt->AffectsNeutral && pTarget->Owner->IsNeutral())) + if (!pExt->IsHealthInThreshold(pTarget) || !pExt->IsVeterancyInThreshold(pTarget) || (!pExt->AffectsNeutral && pTarget->Owner->IsNeutral())) return GoToExtras; } @@ -380,7 +380,7 @@ DEFINE_HOOK(0x469AA4, BulletClass_Logics_Extras, 0x5) auto const pWHExt = WarheadTypeExt::ExtMap.Find(pWH); auto const pTarget = abstract_cast(pThis->Target); - if (pTarget && !pWHExt->IsHealthInThreshold(pTarget)) + if (pTarget && !pWHExt->IsHealthInThreshold(pTarget) && !pWHExt->IsVeterancyInThreshold(pTarget)) continue; int damage = defaultDamage; @@ -554,7 +554,8 @@ static bool IsAllowedSplitsTarget(TechnoClass* pSource, HouseClass* pOwner, Weap if (!EnumFunctions::CanTargetHouse(pWeaponExt->CanTargetHouses, pOwner, pTarget->Owner) || !EnumFunctions::IsCellEligible(pTarget->GetCell(), pWeaponExt->CanTarget, true, true) || !EnumFunctions::IsTechnoEligible(pTarget, pWeaponExt->CanTarget) - || !pWeaponExt->IsHealthInThreshold(pTarget)) + || !pWeaponExt->IsHealthInThreshold(pTarget) + || !pWeaponExt->IsVeterancyInThreshold(pTarget)) { return false; } diff --git a/src/Ext/Bullet/Hooks.cpp b/src/Ext/Bullet/Hooks.cpp index ad67265d4f..d6bc6e4795 100644 --- a/src/Ext/Bullet/Hooks.cpp +++ b/src/Ext/Bullet/Hooks.cpp @@ -280,7 +280,7 @@ DEFINE_HOOK(0x46A4FB, BulletClass_Shrapnel_Targeting, 0x6) if (!pWeaponExt->SkipWeaponPicking) { if (!EnumFunctions::CanTargetHouse(pWeaponExt->CanTargetHouses, pOwner, pTechno->Owner) || !EnumFunctions::IsTechnoEligible(pTechno, pWeaponExt->CanTarget) - || !pWeaponExt->IsHealthInThreshold(pTechno) || !pWeaponExt->HasRequiredAttachedEffects(pTechno, pSource)) + || !pWeaponExt->IsHealthInThreshold(pTechno) || !pWeaponExt->IsVeterancyInThreshold(pTechno) || !pWeaponExt->HasRequiredAttachedEffects(pTechno, pSource)) { return SkipObject; } diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index 97263846ad..23b9b41e33 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -743,6 +743,16 @@ bool TechnoExt::IsHealthInThreshold(TechnoClass* pObject, double min, double max return (hp > 0 ? hp > min : hp >= min) && hp <= max; } +bool TechnoExt::IsVeterancyInThreshold(TechnoClass* pTechno, double min, double max) +{ + double veterancy = 0.0; + + if (pTechno->GetTechnoType()->Trainable) + veterancy = pTechno->Veterancy.Veterancy; + + return veterancy >= min && veterancy < max; +} + bool TechnoExt::CannotMove(UnitClass* pThis) { const auto loco = pThis->Locomotor; diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index 89ed549edf..26a94bb08b 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -289,6 +289,7 @@ class TechnoExt static void CreateDelayedFireAnim(TechnoClass* pThis, AnimTypeClass* pAnimType, int weaponIndex, bool attach, bool center, bool removeOnNoDelay, bool onTurret, CoordStruct firingCoords); static bool HandleDelayedFireWithPauseSequence(TechnoClass* pThis, WeaponTypeClass* pWeapon, int weaponIndex, int frame, int firingFrame); static bool IsHealthInThreshold(TechnoClass* pObject, double min, double max); + static bool IsVeterancyInThreshold(TechnoClass* pObject, double min, double max); static UnitTypeClass* GetUnitTypeExtra(UnitClass* pUnit, TechnoTypeExt::ExtData* pData); static AircraftTypeClass* GetAircraftTypeExtra(AircraftClass* pAircraft); static bool CannotMove(UnitClass* pThis); diff --git a/src/Ext/Techno/Hooks.Firing.cpp b/src/Ext/Techno/Hooks.Firing.cpp index fedde2a82b..7f3b48ea93 100644 --- a/src/Ext/Techno/Hooks.Firing.cpp +++ b/src/Ext/Techno/Hooks.Firing.cpp @@ -345,6 +345,7 @@ DEFINE_HOOK(0x6FC339, TechnoClass_CanFire, 0x6) if (!EnumFunctions::IsTechnoEligible(pTargetTechno, pWeaponExt->CanTarget) || !EnumFunctions::CanTargetHouse(pWeaponExt->CanTargetHouses, pThis->Owner, pTargetTechno->Owner) || !pWeaponExt->IsHealthInThreshold(pTargetTechno) + || !pWeaponExt->IsVeterancyInThreshold(pTargetTechno) || !pWeaponExt->HasRequiredAttachedEffects(pTargetTechno, pThis)) { return CannotFire; diff --git a/src/Ext/Techno/Hooks.ReceiveDamage.cpp b/src/Ext/Techno/Hooks.ReceiveDamage.cpp index 9e57d97361..eb65b1fd4c 100644 --- a/src/Ext/Techno/Hooks.ReceiveDamage.cpp +++ b/src/Ext/Techno/Hooks.ReceiveDamage.cpp @@ -24,7 +24,7 @@ DEFINE_HOOK(0x701900, TechnoClass_ReceiveDamage_Shield, 0x6) // AffectsAbove/BelowPercent & AffectsNeutral can ignore IgnoreDefenses like AffectsAllies/Enmies/Owner // They should be checked here to cover all cases that directly use ReceiveDamage to deal damage - if (!pWHExt->IsHealthInThreshold(pThis) || (!pWHExt->AffectsNeutral && pThis->Owner->IsNeutral())) + if (!pWHExt->IsHealthInThreshold(pThis) || !pWHExt->IsVeterancyInThreshold(pThis) || (!pWHExt->AffectsNeutral && pThis->Owner->IsNeutral())) { damage = 0; return 0; diff --git a/src/Ext/Techno/WeaponHelpers.cpp b/src/Ext/Techno/WeaponHelpers.cpp index 9eec2f40cf..59b49d606c 100644 --- a/src/Ext/Techno/WeaponHelpers.cpp +++ b/src/Ext/Techno/WeaponHelpers.cpp @@ -43,6 +43,7 @@ int TechnoExt::PickWeaponIndex(TechnoClass* pThis, TechnoClass* pTargetTechno, A if (!EnumFunctions::IsTechnoEligible(pTargetTechno, pSecondExt->CanTarget) || !EnumFunctions::CanTargetHouse(pSecondExt->CanTargetHouses, pThis->Owner, pTargetTechno->Owner) || !pSecondExt->IsHealthInThreshold(pTargetTechno) + || !pSecondExt->IsVeterancyInThreshold(pTargetTechno) || !pSecondExt->HasRequiredAttachedEffects(pTargetTechno, pThis)) { return weaponIndexOne; @@ -73,6 +74,7 @@ int TechnoExt::PickWeaponIndex(TechnoClass* pThis, TechnoClass* pTargetTechno, A if (!EnumFunctions::IsTechnoEligible(pTargetTechno, pFirstExt->CanTarget) || !EnumFunctions::CanTargetHouse(pFirstExt->CanTargetHouses, pThis->Owner, pTargetTechno->Owner) || !pFirstExt->IsHealthInThreshold(pTargetTechno) + || !pFirstExt->IsVeterancyInThreshold(pTargetTechno) || !firstAllowedAE) { return weaponIndexTwo; diff --git a/src/Ext/WarheadType/Body.cpp b/src/Ext/WarheadType/Body.cpp index 1dc39c2a42..6a580d4fb6 100644 --- a/src/Ext/WarheadType/Body.cpp +++ b/src/Ext/WarheadType/Body.cpp @@ -42,6 +42,9 @@ bool WarheadTypeExt::ExtData::CanAffectTarget(TechnoClass* pTarget) const if (!IsHealthInThreshold(pTarget)) return false; + if (!IsVeterancyInThreshold(pTarget)) + return false; + if (!this->EffectsRequireVerses) return true; @@ -56,6 +59,14 @@ bool WarheadTypeExt::ExtData::IsHealthInThreshold(TechnoClass* pTarget) const return TechnoExt::IsHealthInThreshold(pTarget, this->AffectsAbovePercent, this->AffectsBelowPercent); } +bool WarheadTypeExt::ExtData::IsVeterancyInThreshold(TechnoClass* pTarget) const +{ + if (!this->VeterancyCheck) + return true; + + return TechnoExt::IsVeterancyInThreshold(pTarget, this->AffectsAboveVeterancy, this->AffectsBelowVeterancy.Get(RulesClass::Instance->VeteranCap + 1.0)); +} + // Checks if Warhead can affect target that might or might be currently invulnerable. bool WarheadTypeExt::ExtData::CanAffectInvulnerable(TechnoClass* pTarget) const { @@ -286,15 +297,21 @@ void WarheadTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->AffectsBelowPercent.Read(exINI, pSection, "AffectsBelowPercent"); this->AffectsAbovePercent.Read(exINI, pSection, "AffectsAbovePercent"); + this->AffectsBelowVeterancy.Read(exINI, pSection, "AffectsBelowVeterancy"); + this->AffectsAboveVeterancy.Read(exINI, pSection, "AffectsAboveVeterancy"); this->AffectsNeutral.Read(exINI, pSection, "AffectsNeutral"); this->AffectsGround.Read(exINI, pSection, "AffectsGround"); this->AffectsAir.Read(exINI, pSection, "AffectsAir"); this->CellSpread_Cylinder.Read(exINI, pSection, "CellSpread.Cylinder"); - this->HealthCheck = this->AffectsBelowPercent > 0.0 || this->AffectsAbovePercent < 1.0; + this->HealthCheck = this->AffectsAbovePercent > 0.0 || this->AffectsBelowPercent < 1.0; + this->VeterancyCheck = this->AffectsAboveVeterancy > 0.0 || this->AffectsBelowVeterancy.Get(RulesClass::Instance->VeteranCap + 1.0) < RulesClass::Instance->VeteranCap + 1.0; if (this->AffectsAbovePercent > this->AffectsBelowPercent) Debug::Log("[Developer warning][%s] AffectsAbovePercent is bigger than AffectsBelowPercent, the warhead will never activate!\n", pSection); + if (this->AffectsAboveVeterancy > this->AffectsBelowVeterancy.Get(RulesClass::Instance->VeteranCap + 1.0)) + Debug::Log("[Developer warning][%s] AffectsAboveVeterancy is bigger than AffectsBelowVeterancy, the warhead will never activate!\n", pSection); + this->ReverseEngineer.Read(exINI, pSection, "ReverseEngineer"); this->UnlimboDetonate.Read(exINI, pSection, "UnlimboDetonate"); @@ -538,11 +555,14 @@ void WarheadTypeExt::ExtData::Serialize(T& Stm) .Process(this->AffectsBelowPercent) .Process(this->AffectsAbovePercent) + .Process(this->AffectsBelowVeterancy) + .Process(this->AffectsAboveVeterancy) .Process(this->AffectsNeutral) .Process(this->AffectsGround) .Process(this->AffectsAir) .Process(this->CellSpread_Cylinder) .Process(this->HealthCheck) + .Process(this->VeterancyCheck) .Process(this->InflictLocomotor) .Process(this->RemoveInflictedLocomotor) diff --git a/src/Ext/WarheadType/Body.h b/src/Ext/WarheadType/Body.h index fdef381aa8..2ed0779dd2 100644 --- a/src/Ext/WarheadType/Body.h +++ b/src/Ext/WarheadType/Body.h @@ -192,6 +192,8 @@ class WarheadTypeExt Valueable AffectsBelowPercent; Valueable AffectsAbovePercent; + Nullable AffectsBelowVeterancy; + Valueable AffectsAboveVeterancy; Valueable AffectsNeutral; Valueable AffectsGround; Valueable AffectsAir; @@ -230,6 +232,7 @@ class WarheadTypeExt int RemainingAnimCreationInterval; bool PossibleCellSpreadDetonate; bool HealthCheck; + bool VeterancyCheck; TechnoClass* DamageAreaTarget; private: @@ -402,6 +405,8 @@ class WarheadTypeExt , AffectsBelowPercent { 1.0 } , AffectsAbovePercent { 0.0 } + , AffectsBelowVeterancy {} + , AffectsAboveVeterancy { 0.0 } , AffectsNeutral { true } , AffectsGround { true } , AffectsAir { true } @@ -423,6 +428,7 @@ class WarheadTypeExt , RemainingAnimCreationInterval { 0 } , PossibleCellSpreadDetonate { false } , HealthCheck { false } + , VeterancyCheck { false } , DamageAreaTarget {} , CanKill { true } @@ -457,6 +463,7 @@ class WarheadTypeExt bool CanAffectInvulnerable(TechnoClass* pTarget) const; bool EligibleForFullMapDetonation(TechnoClass* pTechno, TechnoTypeClass* pType, HouseClass* pOwner) const; bool IsHealthInThreshold(TechnoClass* pTarget) const; + bool IsVeterancyInThreshold(TechnoClass* pTarget) const; virtual ~ExtData() = default; virtual void LoadFromINIFile(CCINIClass* pINI) override; diff --git a/src/Ext/WeaponType/Body.cpp b/src/Ext/WeaponType/Body.cpp index e3cd9c1d5c..1675edfcad 100644 --- a/src/Ext/WeaponType/Body.cpp +++ b/src/Ext/WeaponType/Body.cpp @@ -46,6 +46,11 @@ bool WeaponTypeExt::ExtData::IsHealthInThreshold(TechnoClass* pTarget) const return TechnoExt::IsHealthInThreshold(pTarget, this->CanTarget_MinHealth, this->CanTarget_MaxHealth); } +bool WeaponTypeExt::ExtData::IsVeterancyInThreshold(TechnoClass* pTarget) const +{ + return TechnoExt::IsVeterancyInThreshold(pTarget, this->CanTarget_MinVeterancy, this->CanTarget_MaxVeterancy.Get(RulesClass::Instance->VeteranCap + 1.0)); +} + void WeaponTypeExt::ExtData::Initialize() { this->RadType = RadTypeClass::FindOrAllocate(GameStrings::Radiation); @@ -104,6 +109,8 @@ void WeaponTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->CanTargetHouses.Read(exINI, pSection, "CanTargetHouses"); this->CanTarget_MaxHealth.Read(exINI, pSection, "CanTarget.MaxHealth"); this->CanTarget_MinHealth.Read(exINI, pSection, "CanTarget.MinHealth"); + this->CanTarget_MaxVeterancy.Read(exINI, pSection, "CanTarget.MaxVeterancy"); + this->CanTarget_MinVeterancy.Read(exINI, pSection, "CanTarget.MinVeterancy"); this->Burst_Delays.Read(exINI, pSection, "Burst.Delays"); this->Burst_FireWithinSequence.Read(exINI, pSection, "Burst.FireWithinSequence"); this->Burst_NoDelay.Read(exINI, pSection, "Burst.NoDelay"); @@ -157,6 +164,7 @@ void WeaponTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) // handle SkipWeaponPicking if (this->CanTarget != AffectedTarget::All || this->CanTargetHouses != AffectedHouse::All || this->CanTarget_MaxHealth < 1.0 || this->CanTarget_MinHealth > 0.0 + || this->CanTarget_MaxVeterancy.Get(RulesClass::Instance->VeteranCap + 1.0) < RulesClass::Instance->VeteranCap + 1.0 || this->CanTarget_MinVeterancy > 0.0 || this->AttachEffect_RequiredTypes.size() || this->AttachEffect_RequiredGroups.size() || this->AttachEffect_DisallowedTypes.size() || this->AttachEffect_DisallowedGroups.size()) { @@ -186,6 +194,8 @@ void WeaponTypeExt::ExtData::Serialize(T& Stm) .Process(this->CanTargetHouses) .Process(this->CanTarget_MaxHealth) .Process(this->CanTarget_MinHealth) + .Process(this->CanTarget_MaxVeterancy) + .Process(this->CanTarget_MinVeterancy) .Process(this->RadType) .Process(this->Burst_Delays) .Process(this->Burst_FireWithinSequence) diff --git a/src/Ext/WeaponType/Body.h b/src/Ext/WeaponType/Body.h index 03d6fe37e0..54cc74fdbb 100644 --- a/src/Ext/WeaponType/Body.h +++ b/src/Ext/WeaponType/Body.h @@ -42,6 +42,8 @@ class WeaponTypeExt Valueable CanTargetHouses; Valueable CanTarget_MaxHealth; Valueable CanTarget_MinHealth; + Nullable CanTarget_MaxVeterancy; + Valueable CanTarget_MinVeterancy; ValueableVector Burst_Delays; Valueable Burst_FireWithinSequence; Valueable Burst_NoDelay; @@ -114,6 +116,8 @@ class WeaponTypeExt , CanTargetHouses { AffectedHouse::All } , CanTarget_MaxHealth { 1.0 } , CanTarget_MinHealth { 0.0 } + , CanTarget_MaxVeterancy {} + , CanTarget_MinVeterancy { 0.0 } , Burst_Delays {} , Burst_FireWithinSequence { false } , Burst_NoDelay { false } @@ -169,6 +173,7 @@ class WeaponTypeExt int GetBurstDelay(int burstIndex) const; bool HasRequiredAttachedEffects(TechnoClass* pTechno, TechnoClass* pFirer) const; bool IsHealthInThreshold(TechnoClass* pTarget) const; + bool IsVeterancyInThreshold(TechnoClass* pTarget) const; virtual ~ExtData() = default;