diff --git a/CREDITS.md b/CREDITS.md index 3ecead2d75..7a5f73256b 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 + - Penetration damage on garrisonable structures - 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/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 343dae63eb..b287ff6a7b 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -3171,6 +3171,30 @@ PenetratesTransport.FatalRateMultiplier=1.0 ; double PenetratesTransport.DamageMultiplier=1.0 ; double ``` +### Penetration damage on garrisonable structures + +![image](_static/images/garrison-penetration-01.gif) +*Penetration damage on garrisoned structures example in [C&C: Reloaded](https://www.moddb.com/mods/cncreloaded)* + +- Warheads can now damage garrisoned infantry at impact. +- `PenetratesGarrison` Enables the logic. + - `PenetratesGarrison.RandomTarget` specifies if the damage will go at some random garrisoned soldier or if all infantry should be damaged at the same time. + - `PenetratesGarrison.DamageMultiplier` can be used to modify the damage applied against the garrisoned infantry. A random percentage value will be picked between the specified range. + - `PenetratesGarrison.CleanSound` can be used to specify a sound to play when the structure lost all the garrisoned soldiers with this logic. + - `PenetratesGarrison.Allowed` can be set on garrisonable buildings to protect the garrisoned infantry, or used on garrisoned infantry to make them not be affected by this logic. + +In `rulesmd.ini`: +```ini +[SOMEWARHEAD] ; WarheadType +PenetratesGarrison=false ; boolean +PenetratesGarrison.RandomTarget=true ; boolean +PenetratesGarrison.DamageMultiplier=1.0,1.0 ; floating point value - single or comma-sep. range (percentages) +PenetratesGarrison.CleanSound= ; sound entry + +[SOMETECHNO] ; TechnoType +PenetratesGarrison.Allowed=true ; boolean +``` + ### Remove disguise on impact - Warheads can now remove disguise from disguised spies or mirage tanks. This will work even if the disguised was acquired by default through `PermaDisguise`. diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 68b6030d64..6cafd32529 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) +- [Penetration damage on garrisonable structures](New-or-Enhanced-Logics.md#penetration-damage-on-garrisonable-structures) (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) diff --git a/docs/_static/images/garrison-penetration-01.gif b/docs/_static/images/garrison-penetration-01.gif new file mode 100644 index 0000000000..bc4e845ce5 Binary files /dev/null and b/docs/_static/images/garrison-penetration-01.gif differ diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index ae05a36241..1e7dc5786a 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -1145,6 +1145,8 @@ void TechnoTypeExt::LoadFromINIFile(CCINIClass* const pINI) this->AttackMove_Follow_IncludeAir.Read(exINI, pSection, "AttackMove.Follow.IncludeAir"); this->AttackMove_Follow_IfMindControlIsFull.Read(exINI, pSection, "AttackMove.Follow.IfMindControlIsFull"); + this->PenetratesGarrison_Allowed.Read(exINI, pSection, "PenetratesGarrison.Allowed"); + this->Ammo_AutoConvertMinimumAmount.Read(exINI, pSection, "Ammo.AutoConvertMinimumAmount"); this->Ammo_AutoConvertMaximumAmount.Read(exINI, pSection, "Ammo.AutoConvertMaximumAmount"); this->Ammo_AutoConvertType.Read(exINI, pSection, "Ammo.AutoConvertType"); @@ -1761,6 +1763,8 @@ void TechnoTypeExt::Serialize(T& Stm) .Process(this->FallingDownDamage_Water) .Process(this->FallingDownDamage_AllowEMP) + .Process(this->PenetratesGarrison_Allowed) + .Process(this->Ammo_AutoConvertMinimumAmount) .Process(this->Ammo_AutoConvertMaximumAmount) .Process(this->Ammo_AutoConvertType) diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index c7c2ba9ed6..a9da3dcf5d 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -67,6 +67,7 @@ class TechnoTypeExt : public ObjectTypeExt Nullable Harvester_Counted; Nullable Promote_IncludeSpawns; Valueable ImmuneToCrit; + Valueable PenetratesGarrison_Allowed; Nullable MultiMindControl_ReleaseVictim; Valueable CameoPriority; PhobosPCXFile AltCameoPCX; @@ -496,6 +497,7 @@ class TechnoTypeExt : public ObjectTypeExt , Harvester_Counted {} , Promote_IncludeSpawns {} , ImmuneToCrit { false } + , PenetratesGarrison_Allowed { true } , MultiMindControl_ReleaseVictim {} , CameoPriority { 0 } , AltCameoPCX {} diff --git a/src/Ext/WarheadType/Body.cpp b/src/Ext/WarheadType/Body.cpp index adf07d1090..2c33d9c8c5 100644 --- a/src/Ext/WarheadType/Body.cpp +++ b/src/Ext/WarheadType/Body.cpp @@ -415,6 +415,11 @@ void WarheadTypeExt::LoadFromINIFile(CCINIClass* const pINI) if (this->AffectsAbovePercent > this->AffectsBelowPercent) Debug::Log("[Developer warning][%s] AffectsAbovePercent is bigger than AffectsBelowPercent, the warhead will never activate!\n", pSection); + this->PenetratesGarrison.Read(exINI, pSection, "PenetratesGarrison"); + this->PenetratesGarrison_RandomTarget.Read(exINI, pSection, "PenetratesGarrison.RandomTarget"); + this->PenetratesGarrison_DamageMultiplier.Read(exINI, pSection, "PenetratesGarrison.DamageMultiplier"); + this->PenetratesGarrison_CleanSound.Read(exINI, pSection, "PenetratesGarrison.CleanSound"); + this->ReverseEngineer.Read(exINI, pSection, "ReverseEngineer"); this->UnlimboDetonate.Read(exINI, pSection, "UnlimboDetonate"); @@ -512,6 +517,7 @@ void WarheadTypeExt::LoadFromINIFile(CCINIClass* const pINI) || this->AttachEffects.RemoveGroups.size() > 0 || this->BuildingSell || this->BuildingUndeploy + || this->PenetratesGarrison || this->ReverseEngineer || this->ReturnWarhead || this->PenetratesTransport_Level > 0 @@ -776,6 +782,11 @@ void WarheadTypeExt::Serialize(T& Stm) .Process(this->AirstrikeTargets) + .Process(this->PenetratesGarrison) + .Process(this->PenetratesGarrison_RandomTarget) + .Process(this->PenetratesGarrison_DamageMultiplier) + .Process(this->PenetratesGarrison_CleanSound) + .Process(this->CanKill) .Process(this->ReverseEngineer) diff --git a/src/Ext/WarheadType/Body.h b/src/Ext/WarheadType/Body.h index 7ee03e1291..f0141d1389 100644 --- a/src/Ext/WarheadType/Body.h +++ b/src/Ext/WarheadType/Body.h @@ -262,6 +262,11 @@ class WarheadTypeExt final : public AbstractTypeExt Valueable PenetratesTransport_DamageAll; ValueableIdx PenetratesTransport_CleanSound; + Valueable PenetratesGarrison; + Valueable PenetratesGarrison_RandomTarget; + Valueable> PenetratesGarrison_DamageMultiplier; + NullableIdx PenetratesGarrison_CleanSound; + Valueable Taunt; Nullable Psychedelic_StackingMode; @@ -512,6 +517,11 @@ class WarheadTypeExt final : public AbstractTypeExt , PenetratesTransport_DamageAll { false } , PenetratesTransport_CleanSound { -1 } + , PenetratesGarrison { false } + , PenetratesGarrison_RandomTarget { true } + , PenetratesGarrison_DamageMultiplier { { 1.0, 1.0 } } + , PenetratesGarrison_CleanSound { } + , AffectsEnemies { true } , AffectsOwner {} , EffectsRequireVerses { true } @@ -613,6 +623,7 @@ class WarheadTypeExt final : public AbstractTypeExt void ApplyReverseEngineer(HouseClass* pHouse, TechnoClass* pTarget); void ApplyReturnWarhead(HouseClass* pHouse, TechnoClass* pTarget, TechnoClass* Owner); void ApplyPenetratesTransport(TechnoClass* pTarget, TechnoClass* pInvoker, HouseClass* pInvokerHouse, const CoordStruct& coords, int damage, int distance); + void ApplyPenetratesGarrison(HouseClass* pInvokerHouse, TechnoClass* pTarget, TechnoClass* pInvoker, int damage, const CoordStruct& coords); double GetCritChance(TechnoClass* pFirer) const; void ApplyAmmoModifier(TechnoClass* pTarget); diff --git a/src/Ext/WarheadType/Detonate.cpp b/src/Ext/WarheadType/Detonate.cpp index d8bb3d432f..4299b84a9f 100644 --- a/src/Ext/WarheadType/Detonate.cpp +++ b/src/Ext/WarheadType/Detonate.cpp @@ -233,6 +233,9 @@ void WarheadTypeExt::DetonateOnOneUnit(HouseClass* pHouse, TechnoClass* pTarget, if (this->PenetratesTransport_Level > 0 && damage) this->ApplyPenetratesTransport(pTarget, pOwner, pHouse, coords, damage, distance); + if (this->PenetratesGarrison && damage) + this->ApplyPenetratesGarrison(pHouse, pTarget, pOwner, damage, coords); + if (this->Taunt && pOwner) pTarget->Override_Mission(Mission::Attack, pOwner, nullptr); @@ -923,7 +926,7 @@ void WarheadTypeExt::ApplyPenetratesTransport(TechnoClass* pTarget, TechnoClass* } } -void WarheadTypeExt::ExtData::ApplyAmmoModifier(TechnoClass* pTarget) +void WarheadTypeExt::ApplyAmmoModifier(TechnoClass* pTarget) { const int maxAmmo = pTarget->GetTechnoType()->Ammo; int newCurrentAmmo = this->Ammo + pTarget->Ammo; @@ -931,3 +934,54 @@ void WarheadTypeExt::ExtData::ApplyAmmoModifier(TechnoClass* pTarget) newCurrentAmmo = newCurrentAmmo < 0 ? 0 : newCurrentAmmo; pTarget->Ammo = newCurrentAmmo > maxAmmo ? maxAmmo : newCurrentAmmo; } + +void WarheadTypeExt::ApplyPenetratesGarrison(HouseClass* pInvokerHouse, TechnoClass* pTarget, TechnoClass* pInvoker, int damage, const CoordStruct& coords) +{ + auto const pBuilding = abstract_cast(pTarget); + + if (!pBuilding || !pBuilding->Occupants.Count) + return; + + auto const pTargetTypeExt = TechnoTypeExt::Fetch(pBuilding->Type); + + if (!pTargetTypeExt->PenetratesGarrison_Allowed) + return; + + auto const pWH = this->OwnerObject(); + auto const& location = pTarget->GetCenterCoords(); + const int occupantIndex = this->PenetratesGarrison_RandomTarget ? ScenarioClass::Instance->Random.RandomRanged(0, pBuilding->Occupants.Count - 1) : -1; + const int distance = static_cast(location.DistanceFrom(coords)); + + auto doDamage = [=](int index) + { + auto const pPassenger = pBuilding->Occupants.GetItem(index); + auto const pType = pPassenger->Type; + auto const pTypeExt = TechnoTypeExt::Fetch(pType); + + if (!pTypeExt->PenetratesGarrison_Allowed) + return; + + pPassenger->SetLocation(location); + + int applyDamage = static_cast(damage * GeneralUtils::GetRangedRandomOrSingleValue(this->PenetratesGarrison_DamageMultiplier)); + pPassenger->ReceiveDamage(&applyDamage, distance, pWH, pInvoker, false, true, pInvokerHouse); + }; + + if (occupantIndex < 0) + { + for (int i = pBuilding->Occupants.Count - 1; i >= 0; i--) + doDamage(i); + } + else + { + doDamage(occupantIndex); + } + + if (!pBuilding->Occupants.Count) + { + pBuilding->Mark(MarkType::Change); + + if (this->PenetratesGarrison_CleanSound.isset()) + VocClass::PlayAt(this->PenetratesGarrison_CleanSound.Get(), location); + } +}