diff --git a/CREDITS.md b/CREDITS.md index 24236c02d5..3481c9b155 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -386,6 +386,7 @@ This page lists all the individual contributions to the project by their author. - Fix for sidebar not updating queued unit numbers when on hold - New Parabola trajectory - Enhanced Bombard trajectory + - Allow merging AOE damage to buildings into one - **Ollerus** - Build limit group enhancement - Customizable rocker amplitude diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index daa3764824..d3bd74ce56 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -1867,6 +1867,24 @@ In `rulesmd.ini`: NotHuman.DeathSequence= ; integer (1 to 5) ``` +### Allow merging AOE damage to buildings into one + +- Warheads are now able to damage building only once by merging the AOE damage when setting `MergeBuildingDamage` to true, which default to `[CombatDamage]->MergeBuildingDamage`. + +In `rulesmd.ini`: +```ini +[CombatDamage] +MergeBuildingDamage=false ; boolean + +[SOMEWARHEAD] ; Warhead +MergeBuildingDamage= ; boolean +``` + +```{note} +- This is different from `CellSpread.MaxAffect`. +- Due to the rounding of damage, there may be a slight increase in damage. +``` + ## Weapons ### AreaFire target customization diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 5f20aa8bf5..79db15f55e 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -325,6 +325,7 @@ New: - Enhanced Bombard trajectory (by CrimRecya & Ollerus, based on knowledge of NaotoYuuki) - Toggle waypoint for building (by TaranDahl) - Bunkerable checks dehardcode (by TaranDahl) +- Allow merging AOE damage to buildings into one (by CrimRecya) Vanilla fixes: - Aircraft will now behave as expected according to it's `MovementZone` and `SpeedType` when moving onto different surfaces. In particular, this fixes erratic behavior when vanilla aircraft is ordered to move onto water surface and instead the movement order changes to a shore nearby (by CrimRecya) diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index 38c5130c0c..06b5301392 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -194,6 +194,8 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) this->Vehicles_DefaultDigitalDisplayTypes.Read(exINI, GameStrings::AudioVisual, "Vehicles.DefaultDigitalDisplayTypes"); this->Aircraft_DefaultDigitalDisplayTypes.Read(exINI, GameStrings::AudioVisual, "Aircraft.DefaultDigitalDisplayTypes"); + this->MergeBuildingDamage.Read(exINI, GameStrings::CombatDamage, "MergeBuildingDamage"); + this->AircraftLevelLightMultiplier.Read(exINI, GameStrings::AudioVisual, "AircraftLevelLightMultiplier"); this->JumpjetLevelLightMultiplier.Read(exINI, GameStrings::AudioVisual, "JumpjetLevelLightMultiplier"); @@ -399,6 +401,7 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->ShowDesignatorRange) .Process(this->DropPodTrailer) .Process(this->PodImage) + .Process(this->MergeBuildingDamage) .Process(this->AircraftLevelLightMultiplier) .Process(this->JumpjetLevelLightMultiplier) .Process(this->VoxelLightSource) diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index 7f5fc6f240..be5528917d 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -154,6 +154,8 @@ class RulesExt Valueable Promote_VeteranAnimation; Valueable Promote_EliteAnimation; + Valueable MergeBuildingDamage; + Valueable AircraftLevelLightMultiplier; Valueable JumpjetLevelLightMultiplier; @@ -296,6 +298,7 @@ class RulesExt , ShowDesignatorRange { true } , DropPodTrailer { } , PodImage { } + , MergeBuildingDamage { false } , AircraftLevelLightMultiplier { 1.0 } , JumpjetLevelLightMultiplier { 0.0 } , VoxelLightSource { } diff --git a/src/Ext/WarheadType/Body.cpp b/src/Ext/WarheadType/Body.cpp index 8ba9b9967c..54a16a9235 100644 --- a/src/Ext/WarheadType/Body.cpp +++ b/src/Ext/WarheadType/Body.cpp @@ -256,6 +256,8 @@ void WarheadTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->Nonprovocative.Read(exINI, pSection, "Nonprovocative"); + this->MergeBuildingDamage.Read(exINI, pSection, "MergeBuildingDamage"); + this->CombatLightDetailLevel.Read(exINI, pSection, "CombatLightDetailLevel"); this->CombatLightChance.Read(exINI, pSection, "CombatLightChance"); this->CLIsBlack.Read(exINI, pSection, "CLIsBlack"); @@ -487,6 +489,8 @@ void WarheadTypeExt::ExtData::Serialize(T& Stm) .Process(this->Nonprovocative) + .Process(this->MergeBuildingDamage) + .Process(this->CombatLightDetailLevel) .Process(this->CombatLightChance) .Process(this->CLIsBlack) diff --git a/src/Ext/WarheadType/Body.h b/src/Ext/WarheadType/Body.h index c55b0e1a84..64509655aa 100644 --- a/src/Ext/WarheadType/Body.h +++ b/src/Ext/WarheadType/Body.h @@ -140,6 +140,8 @@ class WarheadTypeExt Valueable Nonprovocative; + Nullable MergeBuildingDamage; + Nullable CombatLightDetailLevel; Valueable CombatLightChance; Valueable CLIsBlack; @@ -295,6 +297,8 @@ class WarheadTypeExt , Nonprovocative { false } + , MergeBuildingDamage {} + , CombatLightDetailLevel {} , CombatLightChance { 1.0 } , CLIsBlack { false } diff --git a/src/Ext/WarheadType/Hooks.cpp b/src/Ext/WarheadType/Hooks.cpp index 6f0bd4241f..0c94f3625d 100644 --- a/src/Ext/WarheadType/Hooks.cpp +++ b/src/Ext/WarheadType/Hooks.cpp @@ -262,6 +262,79 @@ DEFINE_HOOK(0x489B49, MapClass_DamageArea_Rocker, 0xA) return 0x489B53; } +#pragma region MergeBuildingDamage + +DEFINE_HOOK(0x4899DA, DamageArea_DamageBuilding_CauseMergeBuildingDamage, 0x7) +{ + GET_BASE(WarheadTypeClass* const, pWH, 0x0C); + + if (!WarheadTypeExt::ExtMap.Find(pWH)->MergeBuildingDamage.Get(RulesExt::Global()->MergeBuildingDamage)) + return 0; + + struct DamageGroup + { + ObjectClass* Target; + int Distance; + }; + + GET_STACK(const DynamicVectorClass, groups, STACK_OFFSET(0xE0, -0xA8)); + GET_STACK(const bool, invincibleWithoutPenetrateAndCloseTo, STACK_OFFSET(0xE0, -0xC9)); + GET_STACK(const int, baseDamage, STACK_OFFSET(0xE0, -0xBC)); + GET_BASE(TechnoClass* const, pAttacker, 0x08); + GET_BASE(HouseClass* const, pAttackHouse, 0x14); + + // Because during the process of causing damage, fragments may be generated that need to continue causing damage, resulting in nested calls + // to this function. Therefore, a single global variable cannot be used to store this data. + std::unordered_map MapBuildings; + { + const auto cellSpread = Game::F2I(pWH->CellSpread * Unsorted::LeptonsPerCell); + const auto percentDifference = 1.0 - pWH->PercentAtMax; // Vanilla will first multiply the damage and round it up, but we don't need to. + + for (const auto& group : groups) + { + if (const auto pBuilding = abstract_cast(group->Target)) + { + if (group->Distance > cellSpread) + continue; + + const auto multiplier = (cellSpread && percentDifference) ? 1.0 - (percentDifference * group->Distance / cellSpread) : 1.0; + MapBuildings[pBuilding] += multiplier > 0 ? multiplier : 0; + } + } + } + + for (const auto& group : groups) // Causing damage to the building alone and avoiding repeated injuries later. + { + if (const auto pBuilding = abstract_cast(group->Target)) + { + if (pBuilding->IsAlive && !pBuilding->Type->InvisibleInGame && (!invincibleWithoutPenetrateAndCloseTo || pBuilding->IsIronCurtained()) + && pBuilding->Health > 0 && pBuilding->IsOnMap && !pBuilding->InLimbo && MapBuildings.contains(pBuilding)) + { + auto receiveDamage = Game::F2I(baseDamage * MapBuildings[pBuilding]); + MapBuildings.erase(pBuilding); + + if (!receiveDamage && baseDamage) + receiveDamage = Math::sgn(baseDamage); + + pBuilding->ReceiveDamage(&receiveDamage, 0, pWH, pAttacker, false, false, pAttackHouse); + } + } + } + + return 0; +} + +DEFINE_HOOK(0x489A1B, DamageArea_DamageBuilding_SkipVanillaBuildingDamage, 0x6) +{ + enum { SkipGameCode = 0x489AC1 }; + + GET_BASE(WarheadTypeClass* const, pWH, 0x0C); + + return WarheadTypeExt::ExtMap.Find(pWH)->MergeBuildingDamage.Get(RulesExt::Global()->MergeBuildingDamage) ? SkipGameCode : 0; +} + +#pragma endregion + #pragma region Nonprovocative // Do not retaliate against being hit by these Warheads.