diff --git a/CREDITS.md b/CREDITS.md index 0cfbf108b3..284754e406 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -357,6 +357,7 @@ This page lists all the individual contributions to the project by their author. - Fix the issue where computer players did not search for new enemies after defeating them or forming alliances with them - Customize the damage taken when falling from a bridge - `600 The shield of the attached object is broken` bug fix for the triggered event + - Can use more weapons - **NetsuNegi**: - Forbidding parallel AI queues by type - Jumpjet crash speed fix when crashing onto building diff --git a/Phobos.vcxproj b/Phobos.vcxproj index e036e4902c..c5c6902e26 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -24,6 +24,8 @@ + + diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 7d3f228f5f..9ccfa0bddc 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -1533,6 +1533,24 @@ MindControlLink.VisibleToHouse=all ; Affected House Enumeration (none|owner/s MultiMindControl.ReleaseVictim=false ; boolean ``` +### Multi Weapon + +![Multi Weapon](_static/images/multiweapons.gif) +*Multi Weapon used to release different weapons against different targets in **Zero Boundary** by @[Stormsulfur](https://space.bilibili.com/11638715/lists/5358986)* + +- You are free to decide whether to use Weapon x or not, instead of passively using Primary/secondary. + - TechnoType reads `WeaponX` as their weapon when `MultiWeapon=yes`, be careful not to forget `WeaponCount`. + - `MultiWeapon.IsSecondary` can only be used for infantry and is responsible for determining which weapons should use `SecondaryFire` in the `Sequence`. + - `MultiWeapon.SelectWeapon` determines how many weapons can be selected. maximum value is 4, minimum value is 1, defaults to 2. + +In `rulesmd.ini`: +```ini +[SOMETECHNO] ; TechnoType +MultiWeapon= ; boolean +MultiWeapon.IsSecondary= ; List of integers +MultiWeapon.SelectWeapon= ; integer +``` + ### No Manual Move - You can now specify whether a TechnoType is unable to receive move command. diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 7e34b3afe1..0f8eca348b 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -376,6 +376,7 @@ New: - [Use `InsigniaType` to set the properties of insignia in a batch](Miscellanous.md#insignia-type) (by Ollerus) - [Tiberium eater logic](New-or-Enhanced-Logics.md#tiberium-eater) (by NetsuNegi) - [Customize the damage taken when falling from a bridge](Fixed-or-Improved-Logics.md#customize-bridge-falling-down-damage) (by FlyStar) +- Can use more weapons (by FlyStar) 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/docs/_static/images/multiweapons.gif b/docs/_static/images/multiweapons.gif new file mode 100644 index 0000000000..bd9bc560bd Binary files /dev/null and b/docs/_static/images/multiweapons.gif differ diff --git a/src/Ext/Techno/Body.MultiWeapon.cpp b/src/Ext/Techno/Body.MultiWeapon.cpp new file mode 100644 index 0000000000..197e5fef40 --- /dev/null +++ b/src/Ext/Techno/Body.MultiWeapon.cpp @@ -0,0 +1,143 @@ +#include "Body.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +bool TechnoExt::CheckMultiWeapon(TechnoClass* const pThis, AbstractClass* const pTarget, WeaponTypeClass* const pWeaponType) +{ + if (!pThis || !pTarget) + return false; + + if (!pWeaponType || pWeaponType->NeverUse) + return false; + + if (pThis->InOpenToppedTransport && !pWeaponType->FireInTransport) + return false; + + const auto pBulletType = pWeaponType->Projectile; + const auto pWhatAmI = pTarget->WhatAmI(); + const bool isBuilding = pWhatAmI != AbstractType::Building; + const auto pWH = pWeaponType->Warhead; + const auto pOwner = pThis->Owner; + + if (pTarget->IsInAir()) + { + if (!pBulletType->AA) + return false; + + if (pWH->Airstrike) + return false; + } + else + { + const auto pBulletTypeExt = BulletTypeExt::ExtMap.Find(pBulletType); + + if (pBulletTypeExt->AAOnly.Get()) + { + return false; + } + else if(pWH->ElectricAssault) + { + if (isBuilding) + return false; + + const auto pBuilding = static_cast(pTarget); + + if (!pOwner->IsAlliedWith(pBuilding->Owner) || + !pBuilding->Type || !pBuilding->Type->Overpowerable) + return false; + } + else if (pWH->IsLocomotor) + { + if (isBuilding) + return false; + } + } + + if (pTarget->AbstractFlags & AbstractFlags::Techno) + { + TechnoClass* pTechno = static_cast(pTarget); + + if (pTechno->Health <= 0 || !pTechno->IsAlive) + return false; + + if (pTechno->AttachedBomb) + { + if (pWH->IvanBomb) + return false; + } + else if (pWH->BombDisarm) + { + return false; + } + + const auto pTechnoType = pTechno->GetTechnoType(); + + if (pWH->MindControl && (pTechnoType->ImmuneToPsionics || + pTechno->IsMindControlled() || pOwner == pTechno->Owner)) + return false; + + if (pWH->Parasite && (isBuilding || + static_cast(pTechno)->ParasiteEatingMe)) + return false; + + if (!pWH->Temporal && pTechno->BeingWarpedOut) + return false; + + if (pWeaponType->DrainWeapon && (!pTechnoType->Drainable || + (pTechno->DrainingMe || pOwner->IsAlliedWith(pTechno->Owner)))) + return false; + + if (const auto pWeaponExt = WeaponTypeExt::ExtMap.Find(pWeaponType)) + { + if (!pWeaponExt->HasRequiredAttachedEffects(pTechno, pThis)) + return false; + } + + const auto pTargetExt = TechnoExt::ExtMap.Find(pTechno); + const auto pShield = pTargetExt->Shield.get(); + + if (pShield && pShield->IsActive() && + !pShield->CanBeTargeted(pWeaponType)) + { + return false; + } + else if (GeneralUtils::GetWarheadVersusArmor(pWH, pTechno->GetTechnoType()->Armor) == 0.0) + { + return false; + } + } + else + { + if (pWhatAmI == AbstractType::Terrain) + { + if (!pWH->Wood) + return false; + } + else if (pWhatAmI == AbstractType::Cell) + { + const auto pCell = static_cast(pTarget); + + if (pCell && pCell->OverlayTypeIndex >= 0) + { + auto overlayType = OverlayTypeClass::Array.GetItem(pCell->OverlayTypeIndex); + + if (overlayType->Wall && !pWH->Wall) + return false; + } + } + } + + return true; +} diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index 11e4c189a7..c297ebd5e6 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -225,4 +225,6 @@ class TechnoExt static int GetWeaponIndexAgainstWall(TechnoClass* pThis, OverlayTypeClass* pWallOverlayType); static void ApplyKillWeapon(TechnoClass* pThis, TechnoClass* pSource, WarheadTypeClass* pWH); static void ApplyRevengeWeapon(TechnoClass* pThis, TechnoClass* pSource, WarheadTypeClass* pWH); + + static bool CheckMultiWeapon(TechnoClass* const pThis, AbstractClass* const pTarget, WeaponTypeClass* const pWeaponType); }; diff --git a/src/Ext/Techno/Hooks.Firing.cpp b/src/Ext/Techno/Hooks.Firing.cpp index af7b5dce16..72bc50cf4c 100644 --- a/src/Ext/Techno/Hooks.Firing.cpp +++ b/src/Ext/Techno/Hooks.Firing.cpp @@ -41,6 +41,20 @@ DEFINE_HOOK(0x6F3339, TechnoClass_WhatWeaponShouldIUse_Interceptor, 0x8) return SkipGameCode; } +DEFINE_HOOK(0x6F3360, TechnoClass_WhatWeaponShouldIUse_MultiWeapon, 0x6) +{ + GET(TechnoClass*, pThis, ESI); + enum { SkipGameCode = 0x6F3379 }; + + auto const pTypeExt = TechnoTypeExt::ExtMap.Find(pThis->GetTechnoType()); + + if (pTypeExt->MultiWeapon.Get() && + (pThis->WhatAmI() != AbstractType::Unit || !pThis->GetTechnoType()->Gunner)) + return SkipGameCode; + + return 0; +} + DEFINE_HOOK(0x6F33CD, TechnoClass_WhatWeaponShouldIUse_ForceFire, 0x6) { enum { ReturnWeaponIndex = 0x6F37AF }; @@ -124,6 +138,13 @@ DEFINE_HOOK(0x6F3428, TechnoClass_WhatWeaponShouldIUse_ForceWeapon, 0x6) R->EAX(forceWeaponIndex); return UseWeaponIndex; } + + int multiWeaponIndex = pTypeExt->SelectMultiWeapon(pThis, pTarget); + if (multiWeaponIndex >= 0) + { + R->EAX(multiWeaponIndex); + return UseWeaponIndex; + } } return 0; diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index ecdd782c05..506ebc706b 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -33,6 +33,111 @@ void TechnoTypeExt::ExtData::ApplyTurretOffset(Matrix3D* mtx, double factor) mtx->Translate(x, y, z); } +bool TechnoTypeExt::ExtData::IsSecondary(const int& nWeaponIndex) +{ + const auto pThis = this->OwnerObject(); + + if (pThis->IsGattling) + return nWeaponIndex != 0 && nWeaponIndex % 2 != 0; + + if (this->MultiWeapon.Get() && + !this->MultiWeapon_IsSecondary.empty()) + { + int index = nWeaponIndex + 1; + return this->MultiWeapon_IsSecondary.Contains(index); + } + + return nWeaponIndex != 0; +} + +int TechnoTypeExt::ExtData::SelectMultiWeapon(TechnoClass* const pThis, AbstractClass* const pTarget) +{ + if (!pThis || !pTarget) + return -1; + + const auto pType = this->OwnerObject(); + int WeaponCount = pType->WeaponCount; + + if (WeaponCount > 0 && pType->HasMultipleTurrets() && + (pType->IsGattling || pType->Gunner)) + return -1; + + if (!this->MultiWeapon.Get() || WeaponCount <= 2) + return -1; + + // considering the issue of performance loss, it is sufficient to expand it to four. + int selectweaponCount = this->MultiWeapon_SelectWeapon.Get(); + int weaponCount = WeaponCount; + + if (weaponCount > selectweaponCount) + weaponCount = selectweaponCount; + + if (weaponCount < 2) + return 0; + else if (weaponCount == 2) + return -1; + else if (weaponCount > 4) + weaponCount = 4; + + bool isElite = pThis->Veterancy.IsElite(); + const auto secondary = pType->GetWeapon(1, isElite).WeaponType; + const auto secondaryWH = secondary->Warhead; + bool secondaryCanTarget = TechnoExt::CheckMultiWeapon(pThis, pTarget, secondary); + + if (const auto pTargetTechno = abstract_cast(pTarget)) + { + if (secondaryCanTarget) + { + bool isAllies = pThis->Owner->IsAlliedWith(pTargetTechno->Owner); + + if (pTarget->IsInAir()) + return 1; + else if (secondaryWH->Airstrike) + return 1; + else if (secondary->DrainWeapon && + pTargetTechno->GetTechnoType()->Drainable && + !pThis->DrainTarget && !isAllies) + return 1; + else if (secondaryWH->ElectricAssault && isAllies && + pTargetTechno->WhatAmI() == AbstractType::Building && + static_cast(pTargetTechno)->Type->Overpowerable) + return 1; + } + + if (const auto pCell = pTargetTechno->GetCell()) + { + bool targetOnWater = pCell->LandType == LandType::Water || pCell->LandType == LandType::Beach; + + if (!pTargetTechno->OnBridge && targetOnWater) + { + int result = pThis->SelectNavalTargeting(pTargetTechno); + + if (result == -1) + return 1; + } + } + } + + for (int i = 0; i < weaponCount; i++) + { + if (i == 1) + { + if (secondaryCanTarget) + return i; + + continue; + } + + const auto pWeaponType = pType->GetWeapon(i, isElite).WeaponType; + if (!TechnoExt::CheckMultiWeapon(pThis, pTarget, pWeaponType)) + continue; + + return i; + } + + return 0; +} + // Ares 0.A source const char* TechnoTypeExt::ExtData::GetSelectionGroupID() const { @@ -60,7 +165,7 @@ void TechnoTypeExt::ExtData::ParseBurstFLHs(INI_EX& exArtINI, const char* pArtSe char tempBuffer[32]; char tempBufferFLH[48]; auto pThis = this->OwnerObject(); - bool parseMultiWeapons = pThis->TurretCount > 0 && pThis->WeaponCount > 0; + bool parseMultiWeapons = this->MultiWeapon.Get() || (pThis->TurretCount > 0 && pThis->WeaponCount > 0); auto weaponCount = parseMultiWeapons ? pThis->WeaponCount : 2; nFLH.resize(weaponCount); nEFlh.resize(weaponCount); @@ -1114,6 +1219,11 @@ void TechnoTypeExt::ExtData::Serialize(T& Stm) .Process(this->FallingDownDamage) .Process(this->FallingDownDamage_Water) + + .Process(this->MultiWeapon) + .Process(this->MultiWeapon_IsSecondary) + .Process(this->MultiWeapon_SelectWeapon) + .Process(this->LastMultiWeapon) ; } void TechnoTypeExt::ExtData::LoadFromStream(PhobosStreamReader& Stm) diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index 68dda0e8e1..52b109bcd7 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -351,6 +351,11 @@ class TechnoTypeExt Valueable Harvester_CanGuardArea; Nullable HarvesterScanAfterUnload; + + Valueable MultiWeapon; + ValueableVector MultiWeapon_IsSecondary; + Valueable MultiWeapon_SelectWeapon; + bool LastMultiWeapon; Valueable FallingDownDamage; Nullable FallingDownDamage_Water; @@ -656,12 +661,17 @@ class TechnoTypeExt , Overload_DeathSound {} , Overload_ParticleSys {} , Overload_ParticleSysCount { 5 } - + , Harvester_CanGuardArea { false } , HarvesterScanAfterUnload {} , FallingDownDamage { 1.0 } , FallingDownDamage_Water {} + + , MultiWeapon { false } + , MultiWeapon_IsSecondary {} + , MultiWeapon_SelectWeapon { 2 } + , LastMultiWeapon { false } { } virtual ~ExtData() = default; @@ -675,6 +685,9 @@ class TechnoTypeExt void ApplyTurretOffset(Matrix3D* mtx, double factor = 1.0); + bool IsSecondary(const int& nWeaponIndex); + int SelectMultiWeapon(TechnoClass* const pThis, AbstractClass* const pTarget); + // Ares 0.A const char* GetSelectionGroupID() const; diff --git a/src/Ext/TechnoType/Hooks.MultiWeapon.cpp b/src/Ext/TechnoType/Hooks.MultiWeapon.cpp new file mode 100644 index 0000000000..86293d1cd0 --- /dev/null +++ b/src/Ext/TechnoType/Hooks.MultiWeapon.cpp @@ -0,0 +1,67 @@ +#include "Body.h" +#include + +DEFINE_HOOK(0x7128B2, TechnoTypeClass_ReadINI_MultiWeapon, 0x6) +{ + GET(TechnoTypeClass*, pThis, EBP); + GET(CCINIClass*, pINI, ESI); + enum { ReadWeaponX = 0x7128C0 }; + + INI_EX exINI(pINI); + const char* pSection = pThis->ID; + + const auto pTypeExt = TechnoTypeExt::ExtMap.Find(pThis); + pTypeExt->MultiWeapon.Read(exINI, pSection, "MultiWeapon"); + bool multiWeapon = pThis->HasMultipleTurrets() || pTypeExt->MultiWeapon.Get(); + + if (pTypeExt->LastMultiWeapon != multiWeapon) + { + auto clearWeapon = [pThis](int index) + { + auto& pWeapon = pThis->GetWeapon(index, false); + auto& pEliteWeapon = pThis->GetWeapon(index, true); + + pWeapon = WeaponStruct(); + pEliteWeapon = WeaponStruct(); + }; + + clearWeapon(0); + clearWeapon(1); + + pTypeExt->LastMultiWeapon = multiWeapon; + } + + if (pTypeExt->MultiWeapon.Get()) + { + pTypeExt->MultiWeapon_IsSecondary.Read(exINI, pSection, "MultiWeapon.IsSecondary"); + pTypeExt->MultiWeapon_SelectWeapon.Read(exINI, pSection, "MultiWeapon.SelectWeapon"); + } + + return multiWeapon ? ReadWeaponX : 0; +} + +DEFINE_HOOK(0x715B10, TechnoTypeClass_ReadINI_MultiWeapon2, 0x7) +{ + GET(TechnoTypeClass*, pThis, EBP); + enum { ReadWeaponX = 0x715B1F, Continue = 0x715B17 }; + + const auto pTypeExt = TechnoTypeExt::ExtMap.Find(pThis); + + if (pTypeExt->LastMultiWeapon) + return ReadWeaponX; + + R->AL(pThis->HasMultipleTurrets()); + return Continue; +} + +// Do you think the infantry's way of determining that weapons are secondary is stupid ? +// I think it's kind of stupid. +DEFINE_HOOK(0x520888, InfantryClass_UpdateFiring_IsSecondary, 0x8) +{ + GET(InfantryClass*, pThis, EBP); + GET(int, weaponIdx, EDI); + enum { Primary = 0x5208D6, Secondary = 0x520890 }; + + R->AL(pThis->Crawling); + return TechnoTypeExt::ExtMap.Find(pThis->GetTechnoType())->IsSecondary(weaponIdx) ? Secondary : Primary; +} diff --git a/src/Ext/Unit/Hooks.Unload.cpp b/src/Ext/Unit/Hooks.Unload.cpp index 5981aae963..12452e11be 100644 --- a/src/Ext/Unit/Hooks.Unload.cpp +++ b/src/Ext/Unit/Hooks.Unload.cpp @@ -290,5 +290,3 @@ DEFINE_HOOK(0x4DA9C1, FootClass_AI_DeployToLand, 0x6) return 0; } - -#pragma endregion